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 loadPipeForm = (pipe: PipeFacilityItem): void => loadPipeFormInto(panelContext, pipe);
|
||||||
const emit = (): void => emitFrom(panelContext);
|
const emit = (): void => emitFrom(panelContext);
|
||||||
const commit = (live = false): Promise<void> => commitInto(panelContext, live);
|
const commit = (live = false): Promise<void> => commitInto(panelContext, live);
|
||||||
|
/* 입력 배선(700줄 제한 · `_Structures_Panel_Events`)과 저장이 함께 보는 컨텍스트 — 가변 상태 주인 = 패널. */
|
||||||
/* 입력 배선은 700줄 제한으로 `_Structures_Panel_Events` 로 옮겼다 — 처리 함수는 여기 그대로다. */
|
|
||||||
/* 저장·배선이 함께 보는 패널 컨텍스트 — 가변 상태의 주인은 그대로 패널이다. */
|
|
||||||
const panelContext: StructuresEventContext = {
|
const panelContext: StructuresEventContext = {
|
||||||
groupSelect,
|
groupSelect,
|
||||||
typeSelect,
|
typeSelect,
|
||||||
@@ -493,6 +491,7 @@ export function createStructuresSection(
|
|||||||
commitPosition: () => commitPosition(),
|
commitPosition: () => commitPosition(),
|
||||||
liveCommit: () => liveCommit(),
|
liveCommit: () => liveCommit(),
|
||||||
loadForm,
|
loadForm,
|
||||||
|
addAt: (at, typeId) => api.addAt(at, typeId), // 정본 다른 종류로 바꿈 = 같은 측점 새로 놓기(217)
|
||||||
cancelTempPipe: () => cancelTempPipe(),
|
cancelTempPipe: () => cancelTempPipe(),
|
||||||
syncTypeOptions: (keepTypeId) => syncTypeOptions(keepTypeId),
|
syncTypeOptions: (keepTypeId) => syncTypeOptions(keepTypeId),
|
||||||
syncPlacementFields: () => syncPlacementFields(),
|
syncPlacementFields: () => syncPlacementFields(),
|
||||||
@@ -543,7 +542,7 @@ export function createStructuresSection(
|
|||||||
syncButtons();
|
syncButtons();
|
||||||
renderList();
|
renderList();
|
||||||
|
|
||||||
return {
|
const api: StructuresSection = {
|
||||||
root,
|
root,
|
||||||
facility: { ...facilityOptions, recount: () => (facilityOptions.recount(), renderList()) }, // 단 수 → 목록 램프(54-15)
|
facility: { ...facilityOptions, recount: () => (facilityOptions.recount(), renderList()) }, // 단 수 → 목록 램프(54-15)
|
||||||
listRoot: list,
|
listRoot: list,
|
||||||
@@ -697,4 +696,5 @@ export function createStructuresSection(
|
|||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
return api;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,11 @@
|
|||||||
* 처리 함수(commit·loadForm 등)는 그대로 패널 소유다 — 여기서는 이어 붙이기만 한다.
|
* 처리 함수(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 type { StructuresCommitContext } from "./B05_Profile_UI_Structures_Panel_Commit";
|
||||||
import { asRoad } from "./B05_Profile_UI_Structures_Define";
|
import { asRoad } from "./B05_Profile_UI_Structures_Define";
|
||||||
import { readState, writeState } from "../A00_Common/b_page_state";
|
import { readState, writeState } from "../A00_Common/b_page_state";
|
||||||
@@ -34,6 +38,8 @@ export interface StructuresEventContext extends StructuresCommitContext {
|
|||||||
commitPosition: () => void;
|
commitPosition: () => void;
|
||||||
liveCommit: () => void;
|
liveCommit: () => void;
|
||||||
loadForm: (target: StructureInstance | null) => void;
|
loadForm: (target: StructureInstance | null) => void;
|
||||||
|
/** 우클릭 추가와 같은 길 — 그 측점에 그 종류를 놓고 고름(패널 `addAt`). */
|
||||||
|
addAt: (chainageM: number, typeId: string) => void;
|
||||||
emit: () => void;
|
emit: () => void;
|
||||||
optionRow: HTMLElement;
|
optionRow: HTMLElement;
|
||||||
}
|
}
|
||||||
@@ -82,9 +88,22 @@ export function bindStructuresEvents(ctx: StructuresEventContext): {
|
|||||||
? null
|
? null
|
||||||
: ctx.pipeFacilities().find((entry) => Math.abs(entry.chainage_m - pipe) < PIPE_MATCH_M);
|
: 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 ?? ""));
|
const prev = ctx.typeMap().get(record ? asRoad(record).type_id : (pipeItem?.facility ?? ""));
|
||||||
// 정본이 다른 종류(관 지점 ↔ 구조물)로 · 빈칸으로 = 새로 놓기
|
if (!next || !prev) return startNew(ctx.groupSelect.value, next?.type_id); // 빈칸 = 새로 놓기
|
||||||
if (!next || !prev || !!next.managed_by !== !!prev.managed_by)
|
// 정본이 다른 종류(관 지점 ↔ 구조물 — 맹암거 · 개거 등)로 = 옛 것을 지워 「없음」을 거친 뒤 같은
|
||||||
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}`;
|
const key = record ? `s:${id}` : `p:${pipe}`;
|
||||||
// 구조물은 정본 기록(안 보이는 상세 칸까지) · 관은 아직 옛 종류로 그려진 폼에서 읽음
|
// 구조물은 정본 기록(안 보이는 상세 칸까지) · 관은 아직 옛 종류로 그려진 폼에서 읽음
|
||||||
stashTypeValues(
|
stashTypeValues(
|
||||||
|
|||||||
@@ -103,7 +103,21 @@ async function previewOf(
|
|||||||
return chainages.flatMap((at) => previews.get(key(at)) ?? []);
|
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(
|
function decorate(
|
||||||
fresh: CrossSection[],
|
fresh: CrossSection[],
|
||||||
pipes: Pipes,
|
pipes: Pipes,
|
||||||
@@ -138,6 +152,7 @@ function decorate(
|
|||||||
section.preview = true;
|
section.preview = true;
|
||||||
const hit = labels.find(([at]) => near(at, section.chainage_m));
|
const hit = labels.find(([at]) => near(at, section.chainage_m));
|
||||||
if (hit) section.structure = hit[1];
|
if (hit) section.structure = hit[1];
|
||||||
|
else delete section.structure; // 구조물이 다 빠진 일반 측점 자리(217)
|
||||||
}
|
}
|
||||||
const own = pipes.filter((pipe) =>
|
const own = pipes.filter((pipe) =>
|
||||||
fresh.some((section) => near(section.chainage_m, pipe.chainage_m)),
|
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);
|
const was = kindsAt(oldPipes, oldStructures, section.chainage_m);
|
||||||
return was !== "" && was !== kindsAt(pipes, structures, section.chainage_m);
|
return was !== "" && was !== kindsAt(pipes, structures, section.chainage_m);
|
||||||
};
|
};
|
||||||
|
// 일반 측점 자리(grid · 272)는 측점이 남음 — 종류가 바뀌거나 빠지면 구조물 없는 사본에 새 종류(217)
|
||||||
const isStale = (section: CrossSection): boolean =>
|
const isStale = (section: CrossSection): boolean =>
|
||||||
section.kind === "irregular" &&
|
section.kind === "irregular" &&
|
||||||
!section.grid &&
|
(section.grid
|
||||||
(planted.every((at) => !near(at, section.chainage_m)) || replaced(section));
|
? replaced(section)
|
||||||
|
: planted.every((at) => !near(at, section.chainage_m)) || replaced(section));
|
||||||
stale = all.filter(isStale);
|
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(
|
const missing = planted.filter(
|
||||||
(at, index) =>
|
(at, index) =>
|
||||||
planted.findIndex((other) => near(other, at)) === index &&
|
planted.findIndex((other) => near(other, at)) === index &&
|
||||||
live.every((section) => !near(section.chainage_m, at)),
|
live.every((section) => !near(section.chainage_m, at)),
|
||||||
);
|
);
|
||||||
fresh = missing.length ? await previewOf(projectId, routeId, missing).catch(() => []) : [];
|
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; // 더 새 맞춤이 돎 — 그쪽이 얹음
|
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 yDecimals = Math.max(0, Math.ceil(-Math.log10(yStep) - 1e-9));
|
||||||
const screenYOf = (raw: number) =>
|
const screenYOf = (raw: number) =>
|
||||||
ty + scale * y(options.elevationMid + (raw - options.elevationMid) * options.exaggeration);
|
ty + scale * y(options.elevationMid + (raw - options.elevationMid) * options.exaggeration);
|
||||||
// 중심고 상자 자리 — 보이는 범위 안일 때만 · 그 높이 눈금 글자는 상자에 양보
|
// 중심고 상자 자리 — 보이는 범위 안일 때만 · 겹친 눈금 글도 그림(상자를 맨 뒤에 붙여 위에 ·
|
||||||
|
// 바탕이 안 비쳐 가림 · 319-1)
|
||||||
const centerZ = options.centerZ;
|
const centerZ = options.centerZ;
|
||||||
const centerY =
|
const centerY =
|
||||||
typeof centerZ === "number" && centerZ >= rawBottom && centerZ <= rawTop
|
typeof centerZ === "number" && centerZ >= rawBottom && centerZ <= rawTop
|
||||||
@@ -134,7 +135,6 @@ export function createCrossAxes(
|
|||||||
class: "b06-chart__grid",
|
class: "b06-chart__grid",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
if (centerY !== null && Math.abs(screenY - centerY) < CENTER_BOX_H) continue;
|
|
||||||
layer.append(
|
layer.append(
|
||||||
svgText(raw.toFixed(yDecimals), {
|
svgText(raw.toFixed(yDecimals), {
|
||||||
x: plotLeft - Y_LABEL_GAP_PX,
|
x: plotLeft - Y_LABEL_GAP_PX,
|
||||||
|
|||||||
@@ -395,7 +395,8 @@ html.is-bm-route .b06-section__notice-bar {
|
|||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 중심고 Y축 라벨(318-2) — 눈금과 같은 10px · 둥근 상자 · 테마 강조색 · 축선 앞에서 멈춤 */
|
/* 중심고 Y축 라벨(318-2) — 눈금과 같은 10px · 둥근 상자 · 테마 강조색 · 축선 앞에서 멈춤 ·
|
||||||
|
바탕 = 불투명 섞음(겹친 눈금 글을 위에서 가림 · 319-1) */
|
||||||
.b06-chart__center-box {
|
.b06-chart__center-box {
|
||||||
fill: color-mix(in srgb, var(--color-accent) 14%, var(--color-surface-raised));
|
fill: color-mix(in srgb, var(--color-accent) 14%, var(--color-surface-raised));
|
||||||
stroke: var(--color-accent);
|
stroke: var(--color-accent);
|
||||||
|
|||||||
@@ -277,6 +277,7 @@ const out = {};
|
|||||||
tick_x: ticks.length ? Number(ticks[0].attrs.x) : null,
|
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_near_center: ticks.filter((t) => Math.abs(Number(t.attrs.y) - 3 - centerY) < 14).length,
|
||||||
tick_count: ticks.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,
|
hidden_when_out_of_view: draw(800).querySelector(".b06-chart__center-box") === null,
|
||||||
absent_without_center: draw(null).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 box["x"] > 0
|
||||||
assert abs(box["y"] + box["height"] / 2 - center["center_y"]) < 0.01 # 중심고 높이에
|
assert abs(box["y"] + box["height"] / 2 - center["center_y"]) < 0.01 # 중심고 높이에
|
||||||
assert center["text_x"] == center["tick_x"] # 글 오른끝 = 다른 눈금 자리
|
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["tick_count"] > 3
|
||||||
assert center["hidden_when_out_of_view"]
|
assert center["hidden_when_out_of_view"]
|
||||||
assert center["absent_without_center"]
|
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