fix(b05·b06): 정본 다른 종류(관 ↔ 맹암거 · 개거 등)로 바꿈 = 옛 것 지우고 같은 측점에 새 종류 · 일반 측점 자리 종류 바꿈 · 지움 = 구조물만 뺀 사본에 새 종류 · 중심고 상자에 겹친 Y 눈금 숨기지 않음(상자 위 · 안 비치는 바탕) (319 · 요구 217)
- 공용 구조물 패널 종류 바꿈: 관 지점 ↔ 구조물이면 새로 놓기 폼으로 빠지던 것 → 옛 것 지움 → 우클릭 추가 길(addAt)로 같은 측점 · 옛 측점 조정값 버림(318-4) - 횡단 초안 맞춤: 일반 측점 자리(grid)는 종류가 바뀌어도 저장분(옛 관 그림)이 남던 것 → 구조물만 뺀 사본(지형 쪽 고친 값 그대로) + 새 종류 - 시험: test_319_kind_swap 8 · test_318 중심고 겹침 기대값 바꿈 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C6BNBFXFndbqYxu6sZuknK
This commit is contained in:
@@ -465,9 +465,7 @@ export function createStructuresSection(
|
||||
const loadPipeForm = (pipe: PipeFacilityItem): void => loadPipeFormInto(panelContext, pipe);
|
||||
const emit = (): void => emitFrom(panelContext);
|
||||
const commit = (live = false): Promise<void> => commitInto(panelContext, live);
|
||||
|
||||
/* 입력 배선은 700줄 제한으로 `_Structures_Panel_Events` 로 옮겼다 — 처리 함수는 여기 그대로다. */
|
||||
/* 저장·배선이 함께 보는 패널 컨텍스트 — 가변 상태의 주인은 그대로 패널이다. */
|
||||
/* 입력 배선(700줄 제한 · `_Structures_Panel_Events`)과 저장이 함께 보는 컨텍스트 — 가변 상태 주인 = 패널. */
|
||||
const panelContext: StructuresEventContext = {
|
||||
groupSelect,
|
||||
typeSelect,
|
||||
@@ -493,6 +491,7 @@ export function createStructuresSection(
|
||||
commitPosition: () => commitPosition(),
|
||||
liveCommit: () => liveCommit(),
|
||||
loadForm,
|
||||
addAt: (at, typeId) => api.addAt(at, typeId), // 정본 다른 종류로 바꿈 = 같은 측점 새로 놓기(217)
|
||||
cancelTempPipe: () => cancelTempPipe(),
|
||||
syncTypeOptions: (keepTypeId) => syncTypeOptions(keepTypeId),
|
||||
syncPlacementFields: () => syncPlacementFields(),
|
||||
@@ -543,7 +542,7 @@ export function createStructuresSection(
|
||||
syncButtons();
|
||||
renderList();
|
||||
|
||||
return {
|
||||
const api: StructuresSection = {
|
||||
root,
|
||||
facility: { ...facilityOptions, recount: () => (facilityOptions.recount(), renderList()) }, // 단 수 → 목록 램프(54-15)
|
||||
listRoot: list,
|
||||
@@ -697,4 +696,5 @@ export function createStructuresSection(
|
||||
return true;
|
||||
},
|
||||
};
|
||||
return api;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,11 @@
|
||||
* 처리 함수(commit·loadForm 등)는 그대로 패널 소유다 — 여기서는 이어 붙이기만 한다.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { StructureInstance, StructureType } from "./B05_Profile_Api_Structures";
|
||||
import {
|
||||
structureAnchorM,
|
||||
type StructureInstance,
|
||||
type StructureType,
|
||||
} from "./B05_Profile_Api_Structures";
|
||||
import type { StructuresCommitContext } from "./B05_Profile_UI_Structures_Panel_Commit";
|
||||
import { asRoad } from "./B05_Profile_UI_Structures_Define";
|
||||
import { readState, writeState } from "../A00_Common/b_page_state";
|
||||
@@ -34,6 +38,8 @@ export interface StructuresEventContext extends StructuresCommitContext {
|
||||
commitPosition: () => void;
|
||||
liveCommit: () => void;
|
||||
loadForm: (target: StructureInstance | null) => void;
|
||||
/** 우클릭 추가와 같은 길 — 그 측점에 그 종류를 놓고 고름(패널 `addAt`). */
|
||||
addAt: (chainageM: number, typeId: string) => void;
|
||||
emit: () => void;
|
||||
optionRow: HTMLElement;
|
||||
}
|
||||
@@ -82,9 +88,22 @@ export function bindStructuresEvents(ctx: StructuresEventContext): {
|
||||
? null
|
||||
: ctx.pipeFacilities().find((entry) => Math.abs(entry.chainage_m - pipe) < PIPE_MATCH_M);
|
||||
const prev = ctx.typeMap().get(record ? asRoad(record).type_id : (pipeItem?.facility ?? ""));
|
||||
// 정본이 다른 종류(관 지점 ↔ 구조물)로 · 빈칸으로 = 새로 놓기
|
||||
if (!next || !prev || !!next.managed_by !== !!prev.managed_by)
|
||||
return startNew(ctx.groupSelect.value, next?.type_id);
|
||||
if (!next || !prev) return startNew(ctx.groupSelect.value, next?.type_id); // 빈칸 = 새로 놓기
|
||||
// 정본이 다른 종류(관 지점 ↔ 구조물 — 맹암거 · 개거 등)로 = 옛 것을 지워 「없음」을 거친 뒤 같은
|
||||
// 측점에 새 종류를 놓고 고름(우클릭 추가 길 · 새 종류 기본값 · 요구 217) · 옛 조정값은 318-4 대로 버림
|
||||
if (!!next.managed_by !== !!prev.managed_by) {
|
||||
const at = record ? structureAnchorM(record) : pipeItem!.chainage_m;
|
||||
if (record) {
|
||||
ctx.structures.splice(index, 1);
|
||||
ctx.setEditingId(null);
|
||||
ctx.emit();
|
||||
} else {
|
||||
ctx.callbacks.onPipeRemove(at);
|
||||
ctx.setSelectedPipeChainage(null);
|
||||
}
|
||||
forgetStationAdjusts(ctx.projectId, at, prev.type_id, next.type_id);
|
||||
return ctx.addAt(at, next.type_id);
|
||||
}
|
||||
const key = record ? `s:${id}` : `p:${pipe}`;
|
||||
// 구조물은 정본 기록(안 보이는 상세 칸까지) · 관은 아직 옛 종류로 그려진 폼에서 읽음
|
||||
stashTypeValues(
|
||||
|
||||
@@ -103,7 +103,21 @@ async function previewOf(
|
||||
return chainages.flatMap((at) => previews.get(key(at)) ?? []);
|
||||
}
|
||||
|
||||
/** 미리 뜬 횡단에 카드 이름 · 관 그림 — 서버가 [저장] 때 붙이는 것과 같은 규칙. */
|
||||
/** 일반 측점 자리(grid · 272)의 「구조물 없는 꼴」(217) — 저장분에서 시설 · 이름만 뺀 사본(한 벌을 다시 씀 ·
|
||||
* 지형 쪽 고친 값은 그 측점 것 그대로) · 새 종류는 그 위에 얹음(`decorate`) · 측점은 남으므로 숨기지 않음. */
|
||||
const bares = new WeakMap<CrossSection, CrossSection>();
|
||||
function bareOf(section: CrossSection): CrossSection {
|
||||
let copy = bares.get(section);
|
||||
if (!copy) {
|
||||
copy = { ...section, preview: true };
|
||||
const record = copy as unknown as Record<string, unknown>;
|
||||
for (const key of ["culvert", "ford", "box", "ford_pavement", "structure"]) delete record[key];
|
||||
bares.set(section, copy);
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
/** 미리 뜬 횡단 · 일반 측점 자리 사본에 카드 이름 · 관 그림 — 서버가 [저장] 때 붙이는 것과 같은 규칙. */
|
||||
function decorate(
|
||||
fresh: CrossSection[],
|
||||
pipes: Pipes,
|
||||
@@ -138,6 +152,7 @@ function decorate(
|
||||
section.preview = true;
|
||||
const hit = labels.find(([at]) => near(at, section.chainage_m));
|
||||
if (hit) section.structure = hit[1];
|
||||
else delete section.structure; // 구조물이 다 빠진 일반 측점 자리(217)
|
||||
}
|
||||
const own = pipes.filter((pipe) =>
|
||||
fresh.some((section) => near(section.chainage_m, pipe.chainage_m)),
|
||||
@@ -206,19 +221,22 @@ export async function withDraftStations(
|
||||
const was = kindsAt(oldPipes, oldStructures, section.chainage_m);
|
||||
return was !== "" && was !== kindsAt(pipes, structures, section.chainage_m);
|
||||
};
|
||||
// 일반 측점 자리(grid · 272)는 측점이 남음 — 종류가 바뀌거나 빠지면 구조물 없는 사본에 새 종류(217)
|
||||
const isStale = (section: CrossSection): boolean =>
|
||||
section.kind === "irregular" &&
|
||||
!section.grid &&
|
||||
(planted.every((at) => !near(at, section.chainage_m)) || replaced(section));
|
||||
(section.grid
|
||||
? replaced(section)
|
||||
: planted.every((at) => !near(at, section.chainage_m)) || replaced(section));
|
||||
stale = all.filter(isStale);
|
||||
live = all.filter((section) => !isStale(section));
|
||||
const bare = stale.filter((section) => section.grid).map(bareOf);
|
||||
live = [...all.filter((section) => !isStale(section)), ...bare];
|
||||
const missing = planted.filter(
|
||||
(at, index) =>
|
||||
planted.findIndex((other) => near(other, at)) === index &&
|
||||
live.every((section) => !near(section.chainage_m, at)),
|
||||
);
|
||||
fresh = missing.length ? await previewOf(projectId, routeId, missing).catch(() => []) : [];
|
||||
if (fresh.length) decorate(fresh, pipes, structures, types);
|
||||
if (fresh.length || bare.length) decorate([...fresh, ...bare], pipes, structures, types);
|
||||
}
|
||||
}
|
||||
if (turns.get(detail) !== turn) return; // 더 새 맞춤이 돎 — 그쪽이 얹음
|
||||
|
||||
@@ -117,7 +117,8 @@ export function createCrossAxes(
|
||||
const yDecimals = Math.max(0, Math.ceil(-Math.log10(yStep) - 1e-9));
|
||||
const screenYOf = (raw: number) =>
|
||||
ty + scale * y(options.elevationMid + (raw - options.elevationMid) * options.exaggeration);
|
||||
// 중심고 상자 자리 — 보이는 범위 안일 때만 · 그 높이 눈금 글자는 상자에 양보
|
||||
// 중심고 상자 자리 — 보이는 범위 안일 때만 · 겹친 눈금 글도 그림(상자를 맨 뒤에 붙여 위에 ·
|
||||
// 바탕이 안 비쳐 가림 · 319-1)
|
||||
const centerZ = options.centerZ;
|
||||
const centerY =
|
||||
typeof centerZ === "number" && centerZ >= rawBottom && centerZ <= rawTop
|
||||
@@ -134,7 +135,6 @@ export function createCrossAxes(
|
||||
class: "b06-chart__grid",
|
||||
}),
|
||||
);
|
||||
if (centerY !== null && Math.abs(screenY - centerY) < CENTER_BOX_H) continue;
|
||||
layer.append(
|
||||
svgText(raw.toFixed(yDecimals), {
|
||||
x: plotLeft - Y_LABEL_GAP_PX,
|
||||
|
||||
@@ -395,7 +395,8 @@ html.is-bm-route .b06-section__notice-bar {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* 중심고 Y축 라벨(318-2) — 눈금과 같은 10px · 둥근 상자 · 테마 강조색 · 축선 앞에서 멈춤 */
|
||||
/* 중심고 Y축 라벨(318-2) — 눈금과 같은 10px · 둥근 상자 · 테마 강조색 · 축선 앞에서 멈춤 ·
|
||||
바탕 = 불투명 섞음(겹친 눈금 글을 위에서 가림 · 319-1) */
|
||||
.b06-chart__center-box {
|
||||
fill: color-mix(in srgb, var(--color-accent) 14%, var(--color-surface-raised));
|
||||
stroke: var(--color-accent);
|
||||
|
||||
@@ -277,6 +277,7 @@ const out = {};
|
||||
tick_x: ticks.length ? Number(ticks[0].attrs.x) : null,
|
||||
tick_near_center: ticks.filter((t) => Math.abs(Number(t.attrs.y) - 3 - centerY) < 14).length,
|
||||
tick_count: ticks.length,
|
||||
on_top: layer.children.slice(-2).map((node) => node.className), // 319-1 상자 · 글 = 맨 뒤(위)
|
||||
hidden_when_out_of_view: draw(800).querySelector(".b06-chart__center-box") === null,
|
||||
absent_without_center: draw(null).querySelector(".b06-chart__center-box") === null,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
/* PLAN 319 단위검증 헬퍼 — 정본이 다른 종류로 바꿈(관 지점 ↔ 구조물 · 맹암거 · 개거) = 옛 것 지우고 같은
|
||||
* 측점에 새 종류(패널 입력 배선) · 일반 측점 자리(grid) 종류 바꿈 · 지움 = 구조물만 뺀 사본에 새 종류(횡단 초안 맞춤).
|
||||
* 진짜 TS 를 그 자리에서 트랜스파일해 Node 로 돌림(서버 · 무거운 화면 모듈은 흉내) · 결과 = 표준출력 JSON 한 벌.
|
||||
* 짝 = `test_319_kind_swap.py`.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const B05 = (name) => path.join(ROOT, "B05_Profile", `${name}.ts`);
|
||||
const B06 = (name) => path.join(ROOT, "B06_Section", `${name}.ts`);
|
||||
const TYPES = JSON.parse(
|
||||
fs.readFileSync(path.join(ROOT, "resources", "master_template", "구조물정의.json"), "utf8"),
|
||||
).types;
|
||||
|
||||
class FakeStorage {
|
||||
constructor() {
|
||||
this.map = new Map();
|
||||
}
|
||||
get length() {
|
||||
return this.map.size;
|
||||
}
|
||||
key(index) {
|
||||
return [...this.map.keys()][index] ?? null;
|
||||
}
|
||||
getItem(key) {
|
||||
return this.map.has(key) ? this.map.get(key) : null;
|
||||
}
|
||||
setItem(key, value) {
|
||||
this.map.set(key, String(value));
|
||||
}
|
||||
removeItem(key) {
|
||||
this.map.delete(key);
|
||||
}
|
||||
}
|
||||
globalThis.window = {
|
||||
sessionStorage: new FakeStorage(),
|
||||
localStorage: new FakeStorage(),
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {},
|
||||
dispatchEvent: () => true,
|
||||
requestAnimationFrame: (fn) => fn(),
|
||||
setTimeout,
|
||||
clearTimeout,
|
||||
};
|
||||
globalThis.sessionStorage = window.sessionStorage;
|
||||
globalThis.localStorage = window.localStorage;
|
||||
globalThis.requestAnimationFrame = (fn) => fn();
|
||||
globalThis.document = { activeElement: null, addEventListener: () => {} };
|
||||
|
||||
const anything = () =>
|
||||
new Proxy({}, { get: (_t, key) => (key === "__esModule" ? false : () => undefined) });
|
||||
|
||||
/** `real` 에 든 파일만 진짜로 읽고 그 밖 상대 경로는 흉내 · `stubs` 는 지정자 그대로 갈음. */
|
||||
function makeLoader(real, stubs = {}) {
|
||||
const cache = new Map();
|
||||
const realSet = new Set(real.map((file) => path.resolve(file)));
|
||||
const load = (file) => {
|
||||
const full = path.resolve(file);
|
||||
if (cache.has(full)) return cache.get(full);
|
||||
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(full, box.exports);
|
||||
const need = (spec) => {
|
||||
if (spec in stubs) return stubs[spec];
|
||||
let target = null;
|
||||
if (spec.startsWith("@util/")) target = path.join(ROOT, "common_util", `${spec.slice(6)}.ts`);
|
||||
else if (spec.startsWith(".")) target = path.join(path.dirname(full), `${spec}.ts`);
|
||||
if (target && realSet.has(path.resolve(target))) return load(target);
|
||||
return anything();
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, need);
|
||||
return box.exports;
|
||||
};
|
||||
return load;
|
||||
}
|
||||
|
||||
const out = {};
|
||||
|
||||
// ── 1. 패널 종류 바꿈(319-2) — 정본 다른 종류 = 지우고 같은 측점 addAt ─────────────
|
||||
{
|
||||
const load = makeLoader(
|
||||
[
|
||||
B05("B05_Profile_UI_Structures_Panel_Events"),
|
||||
B05("B05_Profile_Api_Structures"),
|
||||
B06("B06_Section_Api_Culvert_Options"),
|
||||
path.join(ROOT, "A00_Common", "b_page_state.ts"),
|
||||
path.join(ROOT, "A00_Common", "design", "design_selection.ts"),
|
||||
path.join(ROOT, "common_util", "common_util_summary_item.ts"),
|
||||
],
|
||||
{
|
||||
"./B05_Profile_UI_Structures_Define": { asRoad: (record) => record },
|
||||
"@ui/ui_template_elements": { showConfirmDialog: async () => true, showToast: () => {} },
|
||||
"./B06_Section_Section_Drafts": { forgetPreviewAdjusts: () => {} },
|
||||
},
|
||||
);
|
||||
const { bindStructuresEvents } = load(B05("B05_Profile_UI_Structures_Panel_Events"));
|
||||
const typeMap = new Map(TYPES.map((type) => [type.type_id, type]));
|
||||
const el = () => ({
|
||||
value: "",
|
||||
listeners: {},
|
||||
addEventListener(t, fn) {
|
||||
this.listeners[t] = fn;
|
||||
},
|
||||
});
|
||||
const fields = () => ({ station: el(), remainder: el(), wrap: { hidden: false } });
|
||||
const run = ({ structures = [], pipes = [], editing = null, pipeAt = null, next }) => {
|
||||
const calls = {
|
||||
removed: [],
|
||||
addAt: [],
|
||||
emit: 0,
|
||||
live: 0,
|
||||
reset: 0,
|
||||
editingSet: [],
|
||||
pipeSet: [],
|
||||
};
|
||||
let editingId = editing;
|
||||
let selectedPipe = pipeAt;
|
||||
const ctx = {
|
||||
groupSelect: el(),
|
||||
typeSelect: el(),
|
||||
primary: el(),
|
||||
removeButton: el(),
|
||||
resetButton: el(),
|
||||
positionRow: el(),
|
||||
memoField: el(),
|
||||
optionRow: el(),
|
||||
optionInputs: [],
|
||||
anchorFields: fields(),
|
||||
startFields: fields(),
|
||||
endFields: fields(),
|
||||
facilityOptions: { root: el(), readOptions: () => ({}) },
|
||||
groupBoxes: { shapes: () => ({}), get: () => ({}), set: () => {}, setShapes: () => {} },
|
||||
structures,
|
||||
projectId: "P",
|
||||
callbacks: {
|
||||
onPipeRemove: (at) => calls.removed.push(at),
|
||||
onListRelease: () => {},
|
||||
},
|
||||
pipeFacilities: () => pipes,
|
||||
types: () => TYPES,
|
||||
typeMap: () => typeMap,
|
||||
currentType: () => (next ? typeMap.get(next) : null),
|
||||
editingId: () => editingId,
|
||||
setEditingId: (value) => {
|
||||
editingId = value;
|
||||
calls.editingSet.push(value);
|
||||
},
|
||||
selectedPipeChainage: () => selectedPipe,
|
||||
setSelectedPipeChainage: (value) => {
|
||||
selectedPipe = value;
|
||||
calls.pipeSet.push(value);
|
||||
},
|
||||
summary: () => null,
|
||||
setSummary: () => {},
|
||||
cancelTempPipe: () => {},
|
||||
resetForm: () => (calls.reset += 1),
|
||||
syncTypeOptions: () => {},
|
||||
syncPlacementFields: () => {},
|
||||
renderOptionFields: () => {},
|
||||
syncFacilityForm: () => {},
|
||||
liveCommit: () => (calls.live += 1),
|
||||
emit: () => (calls.emit += 1),
|
||||
addAt: (at, typeId) => calls.addAt.push([at, typeId]),
|
||||
interval: () => 20,
|
||||
};
|
||||
bindStructuresEvents(ctx);
|
||||
ctx.typeSelect.listeners.change();
|
||||
return { ...calls, left: structures.map((s) => s.type_id) };
|
||||
};
|
||||
const store = window.sessionStorage;
|
||||
const kindreset = () => JSON.parse(store.getItem("aislo:draft:kindreset:P") ?? "null");
|
||||
store.setItem(
|
||||
"aislo:draft:culvertopt:P:1",
|
||||
JSON.stringify({ "80.00": { a: 1 }, "300.00": { b: 2 } }),
|
||||
);
|
||||
out.pipe_to_underdrain = {
|
||||
...run({ pipes: [{ chainage_m: 80, facility: "pipe" }], pipeAt: 80, next: "cross_underdrain" }),
|
||||
kindreset: kindreset(),
|
||||
culvertopt: JSON.parse(store.getItem("aislo:draft:culvertopt:P:1")),
|
||||
};
|
||||
// 새로 놓인 맹암거(같은 측점) → 처음 종류(배관)로 되돌림 = 비울 자리 기록이 빠짐(318-4 규칙)
|
||||
out.underdrain_back_to_pipe = {
|
||||
...run({
|
||||
structures: [
|
||||
{ structure_id: "n1", type_id: "cross_underdrain", placement: "point", chainage_m: 80 },
|
||||
],
|
||||
editing: "n1",
|
||||
next: "pipe",
|
||||
}),
|
||||
kindreset: kindreset(),
|
||||
};
|
||||
out.open_ditch_to_box = {
|
||||
...run({
|
||||
structures: [
|
||||
{ structure_id: "s1", type_id: "sign", placement: "point", chainage_m: 10 },
|
||||
{ structure_id: "s9", type_id: "open_ditch", placement: "point", chainage_m: 120 },
|
||||
],
|
||||
editing: "s9",
|
||||
next: "box_culvert",
|
||||
}),
|
||||
kindreset: kindreset(),
|
||||
};
|
||||
out.pipe_to_box = run({
|
||||
pipes: [{ chainage_m: 200, facility: "pipe" }],
|
||||
pipeAt: 200,
|
||||
next: "box_culvert",
|
||||
});
|
||||
out.underdrain_to_open_ditch = run({
|
||||
structures: [
|
||||
{ structure_id: "s2", type_id: "cross_underdrain", placement: "point", chainage_m: 60 },
|
||||
],
|
||||
editing: "s2",
|
||||
next: "open_ditch",
|
||||
});
|
||||
out.to_blank = run({ pipes: [{ chainage_m: 200, facility: "pipe" }], pipeAt: 200, next: null });
|
||||
}
|
||||
|
||||
// ── 2. 일반 측점 자리(grid) — 종류 바꿈 = 새로 뜬 횡단 · 다 빠짐 = 시설 뺀 사본(319-2) ──────
|
||||
const state = { draftPipes: null, draftStructures: null, savedPipes: [], savedStructures: [] };
|
||||
(async () => {
|
||||
const load = makeLoader(
|
||||
[
|
||||
B06("B06_Section_Section_Drafts"),
|
||||
path.join(ROOT, "common_util", "common_util_culvert_sets.ts"),
|
||||
path.join(ROOT, "common_util", "common_util_structure_station.ts"),
|
||||
],
|
||||
{
|
||||
"../B04_PreProcess/B04_PreProcess_Api_Fetch": {
|
||||
fetchDetailPipePoints: async () => ({ pipe_points: state.savedPipes }),
|
||||
},
|
||||
"../B05_Profile/B05_Profile_Api_Structures": {
|
||||
fetchStructures: async () => ({ structures: state.savedStructures }),
|
||||
fetchStructureTypes: async () => TYPES,
|
||||
readPendingStructures: () => state.draftStructures,
|
||||
},
|
||||
"../B05_Profile/B05_Profile_Api_Pipes_Draft": { readPendingPipes: () => state.draftPipes },
|
||||
"./B06_Section_Api_Fetch": {
|
||||
previewStructureSections: async (_p, chainages) =>
|
||||
chainages.map((at) => ({ chainage_m: at, kind: "irregular", design: { fresh: true } })),
|
||||
},
|
||||
},
|
||||
);
|
||||
const { withDraftStations, forgetDraftStations } = load(B06("B06_Section_Section_Drafts"));
|
||||
const D800 = { pipe_kind: "파형강관", pipe_diameter_mm: "800" };
|
||||
const detailWith = (spec) => ({
|
||||
cross_sections: [
|
||||
{ chainage_m: 20, kind: "regular", design: {} },
|
||||
{ chainage_m: 40, kind: "irregular", grid: true, design: { keep: 1 }, ...spec },
|
||||
{ chainage_m: 60, kind: "regular", design: {} },
|
||||
],
|
||||
});
|
||||
const at40 = (detail) => {
|
||||
const section = detail.cross_sections.find((s) => Math.abs(s.chainage_m - 40) < 0.01);
|
||||
return {
|
||||
count: detail.cross_sections.length,
|
||||
keys: Object.keys(section).filter((k) =>
|
||||
["culvert", "box", "ford", "ford_pavement"].includes(k),
|
||||
),
|
||||
structure: section.structure ?? null,
|
||||
preview: Boolean(section.preview),
|
||||
fresh: Boolean(section.design?.fresh),
|
||||
keep: section.design?.keep ?? null,
|
||||
};
|
||||
};
|
||||
const grid = {};
|
||||
// 저장분 = 격자 40 위 배관
|
||||
state.savedPipes = [{ chainage_m: 40, facility: "pipe", options: D800 }];
|
||||
state.savedStructures = [];
|
||||
let detail = detailWith({ structure: "파형강관 D800", culvert: { chainage_m: 40 } });
|
||||
state.draftPipes = [];
|
||||
state.draftStructures = [
|
||||
{
|
||||
structure_id: "n1",
|
||||
type_id: "cross_underdrain",
|
||||
placement: "point",
|
||||
chainage_m: 40,
|
||||
options: {},
|
||||
},
|
||||
];
|
||||
await withDraftStations(detail, "P", 1);
|
||||
grid.pipe_to_underdrain = at40(detail);
|
||||
state.draftStructures = [];
|
||||
await withDraftStations(detail, "P", 1);
|
||||
grid.pipe_deleted = at40(detail);
|
||||
state.draftPipes = [{ chainage_m: 40, facility: "pipe", options: D800 }];
|
||||
state.draftStructures = null;
|
||||
await withDraftStations(detail, "P", 1);
|
||||
grid.pipe_back = at40(detail);
|
||||
state.draftPipes = [{ chainage_m: 40, facility: "box_culvert" }];
|
||||
await withDraftStations(detail, "P", 1);
|
||||
grid.pipe_to_box = at40(detail);
|
||||
// 저장분 = 격자 40 위 맹암거 → 배관
|
||||
forgetDraftStations("P");
|
||||
state.savedPipes = [];
|
||||
state.savedStructures = [
|
||||
{
|
||||
structure_id: "s1",
|
||||
type_id: "cross_underdrain",
|
||||
placement: "point",
|
||||
chainage_m: 40,
|
||||
options: {},
|
||||
},
|
||||
];
|
||||
detail = detailWith({ structure: "맹암거", underdrain: { chainage_m: 40 } });
|
||||
state.draftPipes = [{ chainage_m: 40, facility: "pipe", options: D800 }];
|
||||
state.draftStructures = [];
|
||||
await withDraftStations(detail, "P", 1);
|
||||
grid.underdrain_to_pipe = at40(detail);
|
||||
forgetDraftStations("P");
|
||||
out.grid = grid;
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
})().catch((error) => {
|
||||
process.stderr.write(String(error && error.stack ? error.stack : error));
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -78,7 +78,11 @@ def test_center_tag_on_y_axis(result: dict) -> None:
|
||||
assert box["x"] > 0
|
||||
assert abs(box["y"] + box["height"] / 2 - center["center_y"]) < 0.01 # 중심고 높이에
|
||||
assert center["text_x"] == center["tick_x"] # 글 오른끝 = 다른 눈금 자리
|
||||
assert center["tick_near_center"] == 0 # 겹치는 눈금 글은 비켜 줌
|
||||
assert center["tick_near_center"] >= 1 # 겹친 눈금 글도 그대로(319-1)
|
||||
assert center["on_top"] == [ # 상자 · 글을 맨 뒤에 붙여 위에서 가림(319-1)
|
||||
"b06-chart__center-box",
|
||||
"b06-chart__tick b06-chart__center-text",
|
||||
]
|
||||
assert center["tick_count"] > 3
|
||||
assert center["hidden_when_out_of_view"]
|
||||
assert center["absent_without_center"]
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
"""PLAN 319 — 횡단배수 종류 바꿈이 맹암거 · 개거처럼 정본이 다른 종류에서도 먹음(요구 217).
|
||||
|
||||
- 패널: 관 지점 ↔ 구조물로 바꿈 = 옛 것 지움(「없음」) → 같은 측점에 새 종류(우클릭 추가 길 `addAt`)
|
||||
· 그 측점 옛 조정값 버림 · 처음 종류로 되돌리면 비울 자리 기록이 빠짐 · 같은 정본끼리는 종전 그대로
|
||||
- 횡단 초안 맞춤: 일반 측점 자리(grid)도 종류가 바뀌거나 빠지면 구조물만 뺀 사본(측점 남음 · 고친 값
|
||||
그대로)에 새 종류 · 저장분 종류로 되돌리면 저장분 그대로
|
||||
|
||||
실제 TS 를 `helper_319_kind_swap.cjs` 가 Node 로 돌린다(서버 부르는 곳 흉내).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = os.path.dirname(__file__)
|
||||
ROOT = os.path.join(HERE, "..", "..")
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not os.path.isdir(os.path.join(ROOT, "config", "node_modules")),
|
||||
reason="프론트엔드 의존성 미설치",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def result() -> dict:
|
||||
out = subprocess.run(
|
||||
["node", os.path.join(HERE, "helper_319_kind_swap.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=120,
|
||||
check=True,
|
||||
)
|
||||
return json.loads(out.stdout)
|
||||
|
||||
|
||||
def _src(*parts: str) -> str:
|
||||
with open(os.path.join(ROOT, *parts), encoding="utf-8") as handle:
|
||||
return handle.read()
|
||||
|
||||
|
||||
def test_pipe_to_underdrain_removes_pipe_and_places_at_same_station(result: dict) -> None:
|
||||
swap = result["pipe_to_underdrain"]
|
||||
assert swap["removed"] == [80]
|
||||
assert swap["pipeSet"] == [None]
|
||||
assert swap["addAt"] == [[80, "cross_underdrain"]]
|
||||
assert swap["reset"] == 0 and swap["live"] == 0 # 새로 놓기 폼으로 빠지지 않음
|
||||
assert swap["kindreset"] == {"80.00": "pipe"} # [저장] 때 그 측점 옛 조정값 비움(318-4)
|
||||
assert swap["culvertopt"] == {"300.00": {"b": 2}} # 그 측점 세션 조정값만 버림
|
||||
|
||||
|
||||
def test_back_to_first_kind_drops_reset_mark(result: dict) -> None:
|
||||
back = result["underdrain_back_to_pipe"]
|
||||
assert back["left"] == [] and back["emit"] == 1 and back["editingSet"] == [None]
|
||||
assert back["addAt"] == [[80, "pipe"]]
|
||||
assert back["kindreset"] is None
|
||||
|
||||
|
||||
def test_structure_to_pipe_kind(result: dict) -> None:
|
||||
swap = result["open_ditch_to_box"]
|
||||
assert swap["left"] == ["sign"] # 고른 개거만 빠짐
|
||||
assert swap["removed"] == []
|
||||
assert swap["addAt"] == [[120, "box_culvert"]]
|
||||
assert swap["kindreset"]["120.00"] == "open_ditch"
|
||||
|
||||
|
||||
def test_same_owner_switch_unchanged(result: dict) -> None:
|
||||
for case in ("pipe_to_box", "underdrain_to_open_ditch"):
|
||||
same = result[case]
|
||||
assert same["addAt"] == [] and same["removed"] == [] and same["live"] == 1
|
||||
assert result["to_blank"]["reset"] == 1 and result["to_blank"]["addAt"] == []
|
||||
|
||||
|
||||
def test_grid_station_kind_change_uses_bare_copy(result: dict) -> None:
|
||||
# 일반 측점 자리 = 측점이 남음 — 저장분에서 구조물만 뺀 사본에 새 종류(지형 쪽 고친 값 그대로)
|
||||
grid = result["grid"]
|
||||
assert grid["pipe_to_underdrain"] == {
|
||||
"count": 3,
|
||||
"keys": [],
|
||||
"structure": "맹암거",
|
||||
"preview": True,
|
||||
"fresh": False,
|
||||
"keep": 1,
|
||||
}
|
||||
assert grid["pipe_to_box"]["keys"] == ["box"] and grid["pipe_to_box"]["keep"] == 1
|
||||
assert grid["underdrain_to_pipe"]["keys"] == ["culvert"]
|
||||
assert grid["underdrain_to_pipe"]["structure"] == "파형강관 D800"
|
||||
assert grid["underdrain_to_pipe"]["keep"] == 1
|
||||
|
||||
|
||||
def test_grid_station_emptied_keeps_station_without_facility(result: dict) -> None:
|
||||
assert result["grid"]["pipe_deleted"] == {
|
||||
"count": 3,
|
||||
"keys": [],
|
||||
"structure": None,
|
||||
"preview": True,
|
||||
"fresh": False,
|
||||
"keep": 1,
|
||||
}
|
||||
|
||||
|
||||
def test_grid_station_back_to_saved_kind_restores_saved(result: dict) -> None:
|
||||
back = result["grid"]["pipe_back"]
|
||||
assert back["keys"] == ["culvert"] and not back["preview"] and back["keep"] == 1
|
||||
|
||||
|
||||
def test_panel_wires_add_at() -> None:
|
||||
panel = _src("B05_Profile", "B05_Profile_UI_Structures_Panel.ts")
|
||||
assert "addAt: (at, typeId) => api.addAt(at, typeId)" in panel
|
||||
assert "return api;" in panel
|
||||
assert len(panel.splitlines()) <= 700
|
||||
Reference in New Issue
Block a user