fix(B06): 측점만 남은 고름의 노면공 범위 강조 막음 · B05 3D 같은 구멍 · 카드 옮겨 감 · 관 넘김 카드(231-1)
- B06 폼 다시 세움(load) 의 selectAt 에 노면공 거름(coverable) · 측점 고름이 연 폼은 카드 그대로 · 범위 테두리 없음 - B05 3D 부재 고름 resolveAt · selectAt 에 같은 거름 — 노면공 id 를 고름 한 칸에 안 적음 - 넘김 되얹기 카드 = 찾는 거리 안 가장 가까운 카드(관 80.17 이 카드 80 으로 안 감) - 새 시험 test_231_1 · test_106 배선 문자열 맞춤 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STqEqztUthVFBmEP18wBSz
This commit is contained in:
@@ -191,7 +191,7 @@ export async function mountCrossView(
|
||||
if (!isWallSelecting()) focusStationAt(chainageM, revealOnly);
|
||||
},
|
||||
releaseCard: () => releaseCross(),
|
||||
markStructure: (structureId) => sectionView.selectMark(structureId),
|
||||
markStructure: (structureId, span) => sectionView.selectMark(structureId, span),
|
||||
applyPipeOptions: (chainageM, patch) =>
|
||||
applyPipeOptionsToCache(pipeOptionsContext, chainageM, patch),
|
||||
});
|
||||
|
||||
@@ -356,7 +356,14 @@ export async function mountProfileView(
|
||||
shared,
|
||||
);
|
||||
|
||||
wireStructurePick(viewer.structurePick, activeProjectId, panel.structures, selectStationOfPipe);
|
||||
// 3D 부재 고름도 측점 고름과 같은 거름 — 노면공 id 를 고름 한 칸에 적지 않음(231-1)
|
||||
wireStructurePick(
|
||||
viewer.structurePick,
|
||||
activeProjectId,
|
||||
panel.structures,
|
||||
selectStationOfPipe,
|
||||
(entry) => coversAtStation(bridge.typeGroup(entry.type_id)),
|
||||
);
|
||||
|
||||
/** 입력·마커 변경 시 측점 라인만 다시 그린다. 지형 구분·등급·기울기 세부 입력은 종단 그래프의
|
||||
* 위반 판정 기준까지 **즉시** 갈아 끼운다(2026-08-19 사용자 지시 6). */
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from "../A00_Common/design/design_selection";
|
||||
import type { StructurePick, StructurePickControls } from "./B05_Profile_UI_Viewer_Structure_Pick";
|
||||
import type { StructuresSection } from "./B05_Profile_UI_Structures_Panel_Types";
|
||||
import type { StructureInstance } from "./B05_Profile_Api_Structures";
|
||||
|
||||
/** B06으로 넘기는 선택 — B06이 쓰는 `{ 측점, 부재키 }` 한 쌍에 구조물 id 가 붙는다. */
|
||||
export interface StructurePickHandoff {
|
||||
@@ -53,6 +54,8 @@ export function wireStructurePick(
|
||||
structures: StructuresSection,
|
||||
/** 기존 선택 동기화(`selectStationOfPipe`) — 누가거리 하나로 전 화면을 맞춘다. */
|
||||
selectStation: (chainageM: number | null) => void,
|
||||
/** 측점이 덮어 열어도 되는 구조물 — 노면공은 뺌(측점선 · 카드 고름과 같은 거름 · 231-1). */
|
||||
coverable?: (entry: StructureInstance) => boolean,
|
||||
): void {
|
||||
controls.onPick = (pick: StructurePick | null): void => {
|
||||
if (!pick) {
|
||||
@@ -61,10 +64,10 @@ export function wireStructurePick(
|
||||
return;
|
||||
}
|
||||
const selection: DesignSelection = { chainageM: pick.chainageM, structureId: null };
|
||||
const { structureId } = structures.resolveAt(selection);
|
||||
const { structureId } = structures.resolveAt(selection, coverable);
|
||||
selectDesign(projectId, { ...selection, structureId }, { partKey: pick.key, from: "b05" });
|
||||
selectStation(pick.chainageM);
|
||||
if (!structures.hasSelection()) structures.selectAt({ ...selection, structureId });
|
||||
if (!structures.hasSelection()) structures.selectAt({ ...selection, structureId }, coverable);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -616,7 +616,8 @@ export function createStructuresSection(
|
||||
if (!pipe) return;
|
||||
loadPipeForm(pipe);
|
||||
},
|
||||
resolveAt: (selection) => resolveSelection(selection, pipeFacilities, structures),
|
||||
resolveAt: (selection, coverable) =>
|
||||
resolveSelection(selection, pipeFacilities, structures, coverable),
|
||||
selectAt(selection, coverable) {
|
||||
const hit = resolveSelection(selection, pipeFacilities, structures, coverable);
|
||||
if (hit.pipeChainageM !== null) this.selectPipeByChainage(hit.pipeChainageM);
|
||||
|
||||
@@ -171,8 +171,11 @@ export interface StructuresSection {
|
||||
hasSelection: () => boolean;
|
||||
/** 종단도·3D에서 고른 구조물을 폼에 올린다. null이면 선택 해제. */
|
||||
selectById: (structureId: string | null) => void;
|
||||
/** 측점 · 구조물 고름 한 값을 폼이 열 항목으로 푼다 — 열지는 않는다(관 계열이 먼저). */
|
||||
resolveAt: (selection: DesignSelection) => ResolvedSelection;
|
||||
/** 측점 · 구조물 고름 한 값을 폼이 열 항목으로 푼다 — 열지는 않는다(관 계열이 먼저). `coverable` = selectAt 과 같음. */
|
||||
resolveAt: (
|
||||
selection: DesignSelection,
|
||||
coverable?: (entry: StructureInstance) => boolean,
|
||||
) => ResolvedSelection;
|
||||
/** 고름 한 값으로 폼을 연다(관 → 구조물 순) — 어느 화면에서 골라도 같은 거리 · 같은 순서다.
|
||||
* 고를 항목이 없으면 폼을 해제한다. 푼 결과를 돌려준다. `coverable` = 덮어 열어도 되는 구조물(측점 고름 · 216-1). */
|
||||
selectAt: (
|
||||
|
||||
@@ -19,7 +19,7 @@ import type { RevetPick } from "./B06_Section_UI_Cross_Culvert";
|
||||
import type { StructurePickHandoff } from "../B05_Profile/B05_Profile_UI_Structure_Pick_Session";
|
||||
import {
|
||||
coversChainage,
|
||||
isNear,
|
||||
nearestWithin,
|
||||
type SelectableStructure,
|
||||
} from "../A00_Common/design/design_selection";
|
||||
|
||||
@@ -57,7 +57,8 @@ export function applyStructurePick(
|
||||
const covering = structures.find((entry) => coversChainage(entry, handoff.at));
|
||||
const target =
|
||||
(byId && cardOfStructure(sections, byId, handoff.at)) ||
|
||||
sections.find((section) => isNear(section.chainage_m, handoff.at)) ||
|
||||
// 찾는 거리 안 가장 가까운 카드 — 첫 카드면 관 80.17 이 이웃 측점 80 카드로 감(231-1)
|
||||
nearestWithin(sections, handoff.at, (section) => section.chainage_m) ||
|
||||
(covering && cardOfStructure(sections, covering, handoff.at));
|
||||
if (!target) return false;
|
||||
// 조정창은 앞칸(측 이름)만 본다 — 뒤칸은 3D 강조를 부재 하나로 좁히는 몫이다.
|
||||
@@ -83,7 +84,7 @@ export function applyStructurePick(
|
||||
type Sections = SectionDetailResponse["cross_sections"];
|
||||
|
||||
/** 구조물이 자리한 카드 — 구간형이면 그 안의 첫 카드, 점형이면 가장 가까운 카드. 옹벽 · 기슭막이는
|
||||
* 횡단 제원이라 시작 측점 카드가 주인이다(관은 `isNear` 로 카드를 바로 찾는다). */
|
||||
* 횡단 제원이라 시작 측점 카드가 주인이다(관은 `nearestWithin` 으로 카드를 바로 찾는다). */
|
||||
function cardOfStructure(
|
||||
sections: Sections,
|
||||
structure: SelectableStructure,
|
||||
|
||||
@@ -89,8 +89,8 @@ export interface B06StructuresPanelDeps {
|
||||
focusChainage: (chainageM: number, revealOnly?: boolean) => void;
|
||||
/** 목록에서 고른 줄을 다시 눌러 풂 — 횡단 카드 선택도 끔(54-8). */
|
||||
releaseCard?: () => void;
|
||||
/** 종단 알약 강조를 목록에서 고른 것에 맞춘다(null = 관 · 해제 — 측점선 짝을 따름). */
|
||||
markStructure?: (structureId: string | null) => void;
|
||||
/** 종단 알약 강조를 목록에서 고른 것에 맞춘다(null = 관 · 해제 — 측점선 짝을 따름 · span 끔 = 범위 테두리 없이 · 137-4). */
|
||||
markStructure?: (structureId: string | null, span?: boolean) => void;
|
||||
/** 목록이 바뀔 때마다 종단 알약 레인에 같은 목록을 넘긴다(2026-09-07 표시 통일). */
|
||||
onMarks?: (structures: StructureInstance[], types: StructureType[]) => void;
|
||||
/** 바뀐 값을 그 측점 횡단 캐시(culvert 스펙)에 얹고 카드를 다시 그린다 —
|
||||
@@ -167,6 +167,8 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
coversAtStation(markTypes.find((type) => type.type_id === entry.type_id)?.group);
|
||||
/** 다음 카드 고름이 열 구조물 — 같은 측점에 관이 있어도 누른 것이 먼저다(52-15). */
|
||||
let preferredId: string | null = null;
|
||||
/** 측점 고름(카드 하나 · 측점만 남은 고름 다시 세움)이 폼을 여는 중 — 폼 알림이 카드를 옮기지 않게(231-1). */
|
||||
let openingAtStation = false;
|
||||
/** 그 구조물로 갈 카드 측점 — 구간형은 범위 안 카드 중 기준점에 가까운 것(경계 기준점이 범위 밖 카드로 안 감 · 165-2). */
|
||||
const focusOf = (structure: StructureInstance): number => {
|
||||
const anchor = structureAnchorM(structure);
|
||||
@@ -220,10 +222,11 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
const pave = !coverable(structure);
|
||||
const id = structure.structure_id ?? null;
|
||||
preferredId = id;
|
||||
deps.focusChainage(focusOf(structure), pave);
|
||||
// 측점 고름(카드 하나 · 다시 세움)이 연 폼 = 누른 카드 그대로 — 구조물 기준 카드로 옮겨 가지 않음(189-1 · 231-1)
|
||||
if (!openingAtStation) deps.focusChainage(focusOf(structure), pave);
|
||||
preferredId = null;
|
||||
// 알약은 카드 뒤에 — 카드 측점이 알약을 갈아 끼우거나 끄지 않게(53-2)
|
||||
deps.markStructure?.(id);
|
||||
deps.markStructure?.(id, !openingAtStation); // 측점 고름이 연 것 = 카드 하나 · 범위 테두리 없음(231-1)
|
||||
// 카드 고름 리스너를 안 거치니 세션 고름은 여기서(B05 로 가도 그 노면공)
|
||||
const at = { chainageM: focusOf(structure), structureId: id };
|
||||
if (pave) selectDesign(deps.projectId, at, { from: "b06" });
|
||||
@@ -406,11 +409,15 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
const pick = readStructurePick(deps.projectId);
|
||||
const picked = currentChainageM ?? pick?.at ?? null;
|
||||
// 관이면 그 관, 옹벽 · 기슭막이 같은 구조물이면 저장된 구조물 id 로 폼을 세운다.
|
||||
// 측점만 남은 고름은 노면공을 덮어 열지 않음 — 카드 고름과 같은 거름(152-5 · 231-1)
|
||||
if (picked !== null) {
|
||||
section.selectAt({
|
||||
chainageM: picked,
|
||||
structureId: currentChainageM === null ? (pick?.structureId ?? null) : null,
|
||||
});
|
||||
const structureId = currentChainageM === null ? (pick?.structureId ?? null) : null;
|
||||
openingAtStation = structureId === null;
|
||||
try {
|
||||
section.selectAt({ chainageM: picked, structureId }, coverable);
|
||||
} finally {
|
||||
openingAtStation = false;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
showToast(
|
||||
@@ -567,7 +574,14 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
section.selectPipeByChainage(hit.pipeChainageM);
|
||||
// 알약은 폼 뒤 — 폼이 연 구조물을 닫으며 알약을 끄므로(165-2 · 측점선 없는 물넘이포장도 53-2)
|
||||
deps.markStructure?.(`pipe-${hit.pipeChainageM.toFixed(2)}`);
|
||||
} else section.selectById(hit.structureId);
|
||||
} else {
|
||||
openingAtStation = preferredId === null;
|
||||
try {
|
||||
section.selectById(hit.structureId);
|
||||
} finally {
|
||||
openingAtStation = false;
|
||||
}
|
||||
}
|
||||
return { chainageM, structureId: hit.structureId };
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,385 @@
|
||||
/* 231-1 노면공 범위 고름 — 「측점만 남은 고름」이 B05↔B06 오감 · 다시 세움에서 노면공 범위를 켜지 않는지.
|
||||
* 진짜 `createB06StructuresPanel` · `applyStructurePick` · `wireStructurePick` · `readStructurePick` 를 Node 로 돌리고
|
||||
* 폼 · 카드 · 알약 흉내는 helper_165 와 같은 꼴(공용 폼 `selectAt` = B05 패널 본문 그대로). 표준출력 JSON 한 벌.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
|
||||
function loadTs(file, stubs = {}) {
|
||||
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
const localRequire = (request) => {
|
||||
if (stubs[request]) return stubs[request];
|
||||
throw new Error(`stub 없음: ${request}`);
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||||
return box.exports;
|
||||
}
|
||||
|
||||
global.requestAnimationFrame = (fn) => fn();
|
||||
|
||||
const store = new Map();
|
||||
const selection = loadTs(path.join(ROOT, "A00_Common", "design", "design_selection.ts"), {
|
||||
"../b_page_state": {
|
||||
readState: (key, pid) => store.get(`${key}:${pid}`) ?? null,
|
||||
writeState: (key, value, pid) => store.set(`${key}:${pid}`, value),
|
||||
},
|
||||
});
|
||||
const pickSession = loadTs(
|
||||
path.join(ROOT, "B05_Profile", "B05_Profile_UI_Structure_Pick_Session.ts"),
|
||||
{ "../A00_Common/design/design_selection": selection },
|
||||
);
|
||||
const structurePick = loadTs(
|
||||
path.join(ROOT, "B06_Section", "B06_Section_UI_Page_Structure_Pick.ts"),
|
||||
{ "../A00_Common/design/design_selection": selection },
|
||||
);
|
||||
|
||||
const PID = "p231";
|
||||
const TYPES = [
|
||||
{ type_id: "pavement_concrete", group: "G" },
|
||||
{ type_id: "retaining_wall", group: "C" },
|
||||
{ type_id: "hazard_sign", group: "E" },
|
||||
{ type_id: "pipe", group: "A", managed_by: "pipe_points" },
|
||||
];
|
||||
const groupOf = (typeId) => TYPES.find((type) => type.type_id === typeId)?.group;
|
||||
const atStation = (entry) => selection.coversAtStation(groupOf(entry.type_id));
|
||||
const PAVE = {
|
||||
structure_id: "pave",
|
||||
type_id: "pavement_concrete",
|
||||
placement: "interval",
|
||||
chainage_m: 30,
|
||||
start_m: 30,
|
||||
end_m: 250,
|
||||
};
|
||||
const WALL = {
|
||||
structure_id: "wall",
|
||||
type_id: "retaining_wall",
|
||||
placement: "interval",
|
||||
chainage_m: 100,
|
||||
start_m: 100,
|
||||
end_m: 160,
|
||||
};
|
||||
const SIGN = { structure_id: "sign", type_id: "hazard_sign", chainage_m: 127.86 };
|
||||
const PIPES = [{ chainage_m: 80.17, facility: "pipe", source: "spacing", options: {} }];
|
||||
const CARDS = [...Array.from({ length: 16 }, (_, i) => i * 20), 80.17]
|
||||
.sort((a, b) => a - b)
|
||||
.map((at) => ({ station_id: `s${at}`, chainage_m: at }));
|
||||
const cardAt = (at) => CARDS.find((card) => Math.abs(card.chainage_m - at) < 0.01);
|
||||
|
||||
/* ── 공용 폼 흉내(B05 패널 loadForm · loadPipeForm · resetForm · selectAt 본문) ── */
|
||||
let binding = null;
|
||||
let structures = [];
|
||||
const form = { id: null, pipe: null };
|
||||
const loadForm = (target) => {
|
||||
form.id = target?.structure_id ?? null;
|
||||
form.pipe = null;
|
||||
binding.onSelect(target);
|
||||
};
|
||||
const loadPipeForm = (chainageM) => {
|
||||
const hadStructure = form.id !== null;
|
||||
form.id = null;
|
||||
form.pipe = chainageM;
|
||||
if (hadStructure) binding.onSelect(null);
|
||||
};
|
||||
const resetForm = () => {
|
||||
const hadPipe = form.pipe !== null;
|
||||
form.pipe = null;
|
||||
loadForm(null);
|
||||
if (hadPipe) binding.onPipeSelect(null);
|
||||
};
|
||||
const node = { addEventListener() {}, querySelector: () => ({ append() {} }) };
|
||||
const section = {
|
||||
root: node,
|
||||
listRoot: node,
|
||||
facility: { root: node, setRevetSideLabels() {}, recount() {} },
|
||||
setTypes() {},
|
||||
setStructures(next) {
|
||||
structures = next;
|
||||
},
|
||||
setPipeFacilities() {},
|
||||
hasSelection: () => form.id !== null || form.pipe !== null,
|
||||
selectById(id) {
|
||||
if (id === null) {
|
||||
if (form.id !== null || form.pipe !== null) resetForm();
|
||||
return;
|
||||
}
|
||||
loadForm(structures.find((entry) => entry.structure_id === id) ?? null);
|
||||
},
|
||||
selectPipeByChainage(chainageM) {
|
||||
if (chainageM === null) {
|
||||
if (form.pipe !== null) resetForm();
|
||||
return;
|
||||
}
|
||||
loadPipeForm(chainageM);
|
||||
},
|
||||
resolveAt: (sel, coverable) => selection.resolveSelection(sel, PIPES, structures, coverable),
|
||||
selectAt(sel, coverable) {
|
||||
const hit = selection.resolveSelection(sel, PIPES, structures, coverable);
|
||||
if (hit.pipeChainageM !== null) this.selectPipeByChainage(hit.pipeChainageM);
|
||||
else this.selectById(hit.structureId);
|
||||
return hit;
|
||||
},
|
||||
};
|
||||
|
||||
const current = { structures: [] };
|
||||
const panelModule = loadTs(
|
||||
path.join(ROOT, "B06_Section", "B06_Section_UI_Page_Structures_Panel.ts"),
|
||||
{
|
||||
"../A00_Common/design/design_shared": {
|
||||
bindDesignView: (_shared, _view, parts) => {
|
||||
binding = parts.structures;
|
||||
},
|
||||
sharedStructures: () => section,
|
||||
},
|
||||
"../B05_Profile/B05_Profile_Api_Structures": {
|
||||
fetchStructureTypes: async () => TYPES,
|
||||
fetchStructures: async () => ({ structures: [] }),
|
||||
readPendingStructures: () => current.structures,
|
||||
pipesToStructureMarks: (pipes) =>
|
||||
pipes.map((pipe) => ({
|
||||
structure_id: `pipe-${pipe.chainage_m.toFixed(2)}`,
|
||||
type_id: pipe.facility,
|
||||
chainage_m: pipe.chainage_m,
|
||||
start_m: null,
|
||||
end_m: null,
|
||||
})),
|
||||
structureAnchorM: (s) => s.chainage_m ?? s.start_m ?? 0,
|
||||
writePendingStructures() {},
|
||||
},
|
||||
"../B05_Profile/B05_Profile_Api_Pipes_Draft": {
|
||||
readPendingPipes: () => null,
|
||||
writePendingPipes() {},
|
||||
},
|
||||
"../B05_Profile/B05_Profile_UI_Drainage_Facility_Merge": { mergeFacilityOptions: (x) => x },
|
||||
"../B04_PreProcess/B04_PreProcess_Api_Fetch": {
|
||||
fetchDetailPipePoints: async () => ({ pipe_points: PIPES, basins: [] }),
|
||||
},
|
||||
"../common_util/common_util_summary_item": {
|
||||
linkFields: () => ({}),
|
||||
readGroupItems: (x) => x ?? {},
|
||||
readSummaryItem: (x) => x ?? null,
|
||||
},
|
||||
"../B05_Profile/B05_Profile_UI_Structure_Pick_Session": pickSession,
|
||||
"../A00_Common/design/design_selection": selection,
|
||||
"@ui/ui_template_elements": { showToast() {} },
|
||||
"./B06_Section_UI_Adjust_Dock": {
|
||||
adjustDockRoot: () => ({}),
|
||||
refreshAdjustSlots() {},
|
||||
resetAdjustDock() {},
|
||||
setAdjustSideSlots() {},
|
||||
},
|
||||
"./B06_Section_UI_Page_Structures_Spec": { withSpecDefaults: (o) => o ?? {} },
|
||||
},
|
||||
);
|
||||
|
||||
/* ── 종단 알약 · 횡단 카드 흉내(helper_165 와 같음) ── */
|
||||
let marks = [];
|
||||
const profile = {
|
||||
markedId: null,
|
||||
pill: null,
|
||||
selectMark(id) {
|
||||
this.markedId = id;
|
||||
this.pill = id;
|
||||
},
|
||||
showCard(card) {
|
||||
if (!card) return;
|
||||
const pipe = PIPES.find((entry) => Math.abs(entry.chainage_m - card.chainage_m) < 0.51);
|
||||
this.pill = pipe ? `pipe-${pipe.chainage_m.toFixed(2)}` : null;
|
||||
const marked = marks.find((entry) => entry.structure_id === this.markedId);
|
||||
if (!pipe && marked && selection.coversChainage(marked, card.chainage_m))
|
||||
this.pill = this.markedId;
|
||||
},
|
||||
};
|
||||
const isPavement = (id) => {
|
||||
const mark = marks.find((entry) => entry.structure_id === id);
|
||||
return !!mark && groupOf(mark.type_id) === "G";
|
||||
};
|
||||
const view = {
|
||||
selected: null,
|
||||
spanMarkId: null,
|
||||
listener: null,
|
||||
selectStation(id) {
|
||||
const previous = this.selected;
|
||||
this.selected = this.selected === id ? null : id;
|
||||
profile.showCard(CARDS.find((card) => card.station_id === this.selected) ?? null);
|
||||
if (this.selected !== previous) this.listener(this.selected);
|
||||
},
|
||||
revealStation() {
|
||||
this.selected = null;
|
||||
profile.showCard(null);
|
||||
},
|
||||
focusStation(id) {
|
||||
if (id === null) this.selected = null;
|
||||
else if (this.selected !== id) this.selectStation(id);
|
||||
else profile.showCard(CARDS.find((card) => card.station_id === id));
|
||||
},
|
||||
selectMark(id, span = true) {
|
||||
this.spanMarkId = span || isPavement(id) ? id : null;
|
||||
profile.selectMark(id);
|
||||
},
|
||||
};
|
||||
const focusStationAt = (at, revealOnly = false) => {
|
||||
const nearest = CARDS.reduce((best, card) =>
|
||||
Math.abs(card.chainage_m - at) < Math.abs(best.chainage_m - at) ? card : best,
|
||||
);
|
||||
if (revealOnly) view.revealStation(nearest.station_id);
|
||||
else view.focusStation(nearest.station_id);
|
||||
};
|
||||
|
||||
let panel = null;
|
||||
/** design_view_cross.ts 카드 고름 리스너 본문(벽 누름 없음). */
|
||||
view.listener = (stationId) => {
|
||||
const target = stationId ? CARDS.find((card) => card.station_id === stationId) : null;
|
||||
if (!target) {
|
||||
panel.showAtChainage(null);
|
||||
selection.selectDesign(PID, selection.NO_SELECTION, { from: "b06" });
|
||||
return;
|
||||
}
|
||||
const preferred = panel.preferred() !== null;
|
||||
const opened = panel.showAtChainage(target.chainage_m);
|
||||
const picked = preferred ? opened : { chainageM: opened.chainageM, structureId: null };
|
||||
selection.selectDesign(PID, picked, { from: "b06" });
|
||||
const pipeAt = panel.currentChainage();
|
||||
const mark = opened.structureId ?? (pipeAt === null ? null : `pipe-${pipeAt.toFixed(2)}`);
|
||||
view.selectMark(mark, false);
|
||||
};
|
||||
|
||||
async function build(list) {
|
||||
current.structures = list;
|
||||
store.clear();
|
||||
form.id = null;
|
||||
form.pipe = null;
|
||||
view.selected = null;
|
||||
view.spanMarkId = null;
|
||||
profile.markedId = null;
|
||||
profile.pill = null;
|
||||
panel = panelModule.createB06StructuresPanel({
|
||||
projectId: PID,
|
||||
shared: { view: () => "cross" },
|
||||
detail: () => ({ cross_sections: CARDS }),
|
||||
stationInterval: () => 20,
|
||||
focusChainage: (at, revealOnly) => focusStationAt(at, revealOnly),
|
||||
markStructure: (id, span) => view.selectMark(id, span),
|
||||
onMarks: (next) => {
|
||||
marks = next;
|
||||
},
|
||||
});
|
||||
await panel.load();
|
||||
}
|
||||
|
||||
/** design_view_cross.ts `restoreStructurePick` 의 focusStation 차례 그대로. */
|
||||
function restore() {
|
||||
const handoff = pickSession.readStructurePick(PID);
|
||||
structurePick.applyStructurePick(
|
||||
handoff,
|
||||
{ cross_sections: CARDS },
|
||||
{
|
||||
focusStation: (stationId) => {
|
||||
panel.prefer(handoff?.structureId ?? null);
|
||||
view.focusStation(stationId);
|
||||
panel.prefer(null);
|
||||
const sid = handoff?.structureId;
|
||||
if (sid && structures.some((entry) => entry.structure_id === sid))
|
||||
panel.selectStructure(sid);
|
||||
},
|
||||
refreshCard() {},
|
||||
revetSelect() {},
|
||||
fordSelect() {},
|
||||
boxSelect() {},
|
||||
},
|
||||
structures,
|
||||
);
|
||||
}
|
||||
|
||||
/** B05 로 갔다 B06 으로 돌아옴 — B05 `restorePick`(폼 비었을 때만 · id 길) → B06 onShow(load → 되얹기). */
|
||||
async function roundTrip() {
|
||||
const pick = pickSession.readStructurePick(PID);
|
||||
if (!section.hasSelection() && pick?.structureId)
|
||||
section.selectAt({ chainageM: pick.at, structureId: pick.structureId });
|
||||
await panel.load();
|
||||
restore();
|
||||
}
|
||||
|
||||
function snap() {
|
||||
const mark = marks.find((entry) => entry.structure_id === view.spanMarkId);
|
||||
const pave = mark && isPavement(mark.structure_id);
|
||||
const card = CARDS.find((entry) => entry.station_id === view.selected);
|
||||
return {
|
||||
form: form.id ?? (form.pipe === null ? null : `pipe-${form.pipe.toFixed(2)}`),
|
||||
paveLayer: pave ? mark.structure_id : null,
|
||||
span: view.spanMarkId,
|
||||
card: card ? card.chainage_m : null,
|
||||
sid: selection.readSelection(PID)?.structureId ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
const cardPick = (at) => view.selectStation(cardAt(at).station_id);
|
||||
const listPick = (id) => loadForm(structures.find((entry) => entry.structure_id === id));
|
||||
|
||||
/** B05 3D 부재 고름 — 진짜 `wireStructurePick` 에 B05 와 같은 거름(design_view_profile). */
|
||||
function pick3d(chainageM) {
|
||||
const controls = {};
|
||||
pickSession.wireStructurePick(controls, PID, section, () => undefined, atStation);
|
||||
controls.onPick({ chainageM, kind: "revet", key: "own" });
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const out = {};
|
||||
// A. 노면공 범위 안 빈 카드 → 오감
|
||||
await build([PAVE, SIGN]);
|
||||
cardPick(120);
|
||||
out.cardBefore = snap();
|
||||
await roundTrip();
|
||||
out.cardAfter = snap();
|
||||
await roundTrip();
|
||||
out.cardAfter2 = snap();
|
||||
|
||||
// B. 같은 카드에서 목록 표지 → 오감(세션 = 측점만 그대로)
|
||||
await build([PAVE, SIGN]);
|
||||
cardPick(120);
|
||||
listPick("sign");
|
||||
out.signPick = snap();
|
||||
await roundTrip();
|
||||
out.signAfter = snap();
|
||||
|
||||
// C. 목록 노면공 → 오감 = 그대로 범위 강조(id 길)
|
||||
await build([PAVE, SIGN]);
|
||||
listPick("pave");
|
||||
out.paveBefore = snap();
|
||||
await roundTrip();
|
||||
out.paveAfter = snap();
|
||||
|
||||
// D. 옹벽 범위 안 카드 = 그 카드 그대로(기준 카드 100 으로 안 옮김) · 오감 뒤도
|
||||
await build([PAVE, WALL]);
|
||||
cardPick(120);
|
||||
out.wallCard = snap();
|
||||
await roundTrip();
|
||||
out.wallAfter = snap();
|
||||
listPick("wall"); // 목록 고름은 기준 카드로 감(그대로)
|
||||
out.wallList = snap();
|
||||
|
||||
// E. 관 80.17 넘김(측점만) = 카드 80.17(이웃 80 아님)
|
||||
await build([PAVE]);
|
||||
selection.selectDesign(PID, { chainageM: 80.17, structureId: null }, { from: "b05" });
|
||||
restore();
|
||||
out.pipeHandoff = snap();
|
||||
|
||||
// F. B05 3D 부재 고름 — 노면공 범위 안이라도 노면공 id 를 적지 않음
|
||||
await build([PAVE, WALL]);
|
||||
pick3d(120);
|
||||
out.pick3dWall = { sid: snap().sid, form: snap().form };
|
||||
await build([PAVE]);
|
||||
pick3d(200);
|
||||
out.pick3dBare = { sid: snap().sid, form: snap().form };
|
||||
await roundTrip();
|
||||
out.pick3dAfter = snap();
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
})().catch((error) => {
|
||||
process.stderr.write(String(error?.stack ?? error));
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -40,7 +40,10 @@ def test_측점_고름은_카드_하나_목록_고름은_여럿():
|
||||
"""137-4 — 측점 고름 리스너는 span 끔 · 목록(노면공) 길 · 알약은 기본(여럿) 그대로."""
|
||||
cross = _read("A00_Common", "design", "design_view_cross.ts")
|
||||
assert "sectionView.selectMark(mark, false);" in cross # 152-5 덮는 것 없으면 null = 풂
|
||||
assert "markStructure: (structureId) => sectionView.selectMark(structureId)," in cross
|
||||
# 목록 길 = 기본(여럿) · 측점 고름이 연 폼만 span 끔(231-1)
|
||||
assert (
|
||||
"markStructure: (structureId, span) => sectionView.selectMark(structureId, span)," in cross
|
||||
)
|
||||
types = _read("B06_Section", "B06_Section_UI_Section_View_Types.ts")
|
||||
assert "selectMark: (structureId: string | null, span?: boolean) => void;" in types
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""231-1 노면공 범위 고름 — 측점만 남은 고름은 노면공 범위 강조를 켜지 않음(조사 AD 1안 + 같은 구멍).
|
||||
|
||||
까닭: B06 폼 다시 세움(`load()` — B05↔B06 오감 · 새로 고침)이 측점만 남은 고름을 거름 없이 `selectAt` 으로
|
||||
열어 「그 측점을 덮는 첫 구조물」= 노면공이 열리고 범위 강조 · 고름 기록까지 노면공으로 바뀜.
|
||||
B05 3D 부재 고름(`wireStructurePick`)도 거름 없이 노면공 id 를 고름 한 칸에 적었음.
|
||||
고침: 측점 고름이 여는 모든 길에 `coversAtStation`(노면공 뺌 · 152-5) 한 벌 · 노면공은 목록 · 알약 · 형상 누름(id)만.
|
||||
곁: 측점 고름이 연 폼은 카드를 구조물 기준 카드로 옮기지 않음 · 넘김 카드 = 찾는 거리 안 가장 가까운 카드.
|
||||
진짜 TS 를 `helper_231_1_pave_reload.cjs` 가 Node 로 돌린다.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.abspath(os.path.join(HERE, "..", ".."))
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not os.path.isdir(os.path.join(ROOT, "config", "node_modules")),
|
||||
reason="프론트엔드 의존성 미설치",
|
||||
)
|
||||
|
||||
|
||||
def _read(*parts: str) -> str:
|
||||
with open(os.path.join(ROOT, *parts), encoding="utf-8") as handle:
|
||||
return handle.read()
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def out() -> dict:
|
||||
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", os.path.join(HERE, "helper_231_1_pave_reload.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
CARD_120 = {"form": None, "paveLayer": None, "span": None, "card": 120, "sid": None}
|
||||
PAVE = {"form": "pave", "paveLayer": "pave", "span": "pave", "card": None, "sid": "pave"}
|
||||
|
||||
|
||||
def test_범위_안_카드_고름_오감해도_노면공_안_켬_폼_그대로(out) -> None:
|
||||
assert out["cardBefore"] == CARD_120
|
||||
assert out["cardAfter"] == CARD_120
|
||||
assert out["cardAfter2"] == CARD_120 # 두 번 오가도
|
||||
|
||||
|
||||
def test_같은_카드_목록_고름_뒤_오감도_노면공_안_켬(out) -> None:
|
||||
assert out["signPick"]["form"] == "sign"
|
||||
after = out["signAfter"]
|
||||
assert after["form"] != "pave"
|
||||
assert after["paveLayer"] is None
|
||||
assert after["span"] is None
|
||||
assert after["card"] == 120
|
||||
|
||||
|
||||
def test_목록_노면공은_오감해도_범위_강조_그대로(out) -> None:
|
||||
assert out["paveBefore"] == PAVE
|
||||
assert out["paveAfter"] == PAVE
|
||||
|
||||
|
||||
def test_옹벽_범위_안_카드는_그_카드_그대로(out) -> None:
|
||||
wall = {"form": "wall", "paveLayer": None, "span": None, "card": 120, "sid": None}
|
||||
assert out["wallCard"] == wall # 기준 카드 100 으로 안 옮김
|
||||
assert out["wallAfter"] == wall # 오감 뒤도 · 범위 테두리 없음(137-4)
|
||||
# 목록 고름은 그대로 기준 카드 · 범위 테두리 · id 기록
|
||||
assert out["wallList"] == {
|
||||
"form": "wall",
|
||||
"paveLayer": None,
|
||||
"span": "wall",
|
||||
"card": 100,
|
||||
"sid": "wall",
|
||||
}
|
||||
|
||||
|
||||
def test_관_넘김은_가장_가까운_카드(out) -> None:
|
||||
assert out["pipeHandoff"]["card"] == 80.17 # 이웃 측점 80 아님
|
||||
assert out["pipeHandoff"]["form"] == "pipe-80.17"
|
||||
|
||||
|
||||
def test_B05_3D_고름은_노면공_id_를_안_적음(out) -> None:
|
||||
assert out["pick3dWall"] == {"sid": "wall", "form": "wall"} # 범위 안 옹벽 = 옹벽
|
||||
assert out["pick3dBare"] == {"sid": None, "form": None} # 노면공만 덮음 = 안 엶
|
||||
after = out["pick3dAfter"]
|
||||
assert after["paveLayer"] is None
|
||||
assert after["sid"] is None
|
||||
assert after["form"] is None
|
||||
|
||||
|
||||
def test_배선() -> None:
|
||||
panel = _read("B06_Section", "B06_Section_UI_Page_Structures_Panel.ts")
|
||||
assert "section.selectAt({ chainageM: picked, structureId }, coverable);" in panel
|
||||
assert "if (!openingAtStation) deps.focusChainage(focusOf(structure), pave);" in panel
|
||||
assert "deps.markStructure?.(id, !openingAtStation);" in panel
|
||||
cross = _read("A00_Common", "design", "design_view_cross.ts")
|
||||
assert (
|
||||
"markStructure: (structureId, span) => sectionView.selectMark(structureId, span)," in cross
|
||||
)
|
||||
pick = _read("B05_Profile", "B05_Profile_UI_Structure_Pick_Session.ts")
|
||||
assert "structures.resolveAt(selection, coverable)" in pick
|
||||
assert "structures.selectAt({ ...selection, structureId }, coverable)" in pick
|
||||
profile = _read("A00_Common", "design", "design_view_profile.ts")
|
||||
at = profile.index("wireStructurePick(")
|
||||
assert "coversAtStation(bridge.typeGroup(entry.type_id))" in profile[at : at + 300]
|
||||
handoff = _read("B06_Section", "B06_Section_UI_Page_Structure_Pick.ts")
|
||||
assert "nearestWithin(sections, handoff.at, (section) => section.chainage_m)" in handoff
|
||||
Reference in New Issue
Block a user