Merge remote-tracking branch 'origin/sub_laptop_7' into main_laptop_1
This commit is contained in:
@@ -164,6 +164,18 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
markTypes.find((type) => type.type_id === entry.type_id)?.group !== "G";
|
||||
/** 다음 카드 고름이 열 구조물 — 같은 측점에 관이 있어도 누른 것이 먼저다(52-15). */
|
||||
let preferredId: string | null = null;
|
||||
/** 폼에 열린 구조물 — 범위 안 빈 카드를 눌러도 연 노면공을 지킴(165-1). */
|
||||
let openId: string | null = null;
|
||||
/** 그 구조물로 갈 카드 측점 — 구간형은 범위 안 카드 중 기준점에 가까운 것(경계 기준점이 범위 밖 카드로 안 감 · 165-2). */
|
||||
const focusOf = (structure: StructureInstance): number => {
|
||||
const anchor = structureAnchorM(structure);
|
||||
const inside = (deps.detail?.()?.cross_sections ?? [])
|
||||
.map((entry) => entry.chainage_m)
|
||||
.filter((at) => coversChainage(structure, at));
|
||||
return inside.length
|
||||
? inside.reduce((best, at) => (Math.abs(at - anchor) < Math.abs(best - anchor) ? at : best))
|
||||
: anchor;
|
||||
};
|
||||
/** 종단 알약 레인에 올릴 목록 — 구조물 정본 + 관 정본(가상 구조물). */
|
||||
const pushMarks = (): void =>
|
||||
deps.onMarks?.([...structures, ...pipesToStructureMarks(pipeFacilities)], markTypes);
|
||||
@@ -197,13 +209,15 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
}
|
||||
},
|
||||
onSelect: (structure) => {
|
||||
openId = structure?.structure_id ?? null;
|
||||
if (!structure) {
|
||||
deps.markStructure?.(null); // 풂 = 알약도 끔(53-2)
|
||||
return;
|
||||
}
|
||||
currentChainageM = null; // 폼 = 이 구조물 — 옛 관을 붙든 채 남지 않게(165-2)
|
||||
// 카드 고름이 측점으로 다시 풀면 같은 측점 관이 먼저 열린다 — 누른 구조물을 넘긴다.
|
||||
preferredId = structure.structure_id ?? null;
|
||||
deps.focusChainage(structureAnchorM(structure));
|
||||
deps.focusChainage(focusOf(structure));
|
||||
preferredId = null;
|
||||
// 알약은 카드 뒤에 — 카드 측점이 알약을 갈아 끼우거나 끄지 않게(53-2)
|
||||
deps.markStructure?.(structure.structure_id ?? null);
|
||||
@@ -534,14 +548,25 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
// 계곡 통과 시설이 먼저다 — 같은 측점에 구조물 정본 항목이 겹쳐도 횡단도에
|
||||
// 그려지는 것은 이쪽이다(순서 · 거리는 `resolveSelection` 한 곳).
|
||||
const hit = resolveSelection(selection, pipeFacilities, structures, coverable);
|
||||
// 범위 안 빈 카드(관 · 덮는 구조물 없음) = 연 노면공 그대로 · 카드만 옮김(165-1)
|
||||
const open = structures.find((entry) => entry.structure_id === openId);
|
||||
if (
|
||||
chainageM !== null &&
|
||||
hit.pipeChainageM === null &&
|
||||
hit.structureId === null &&
|
||||
open &&
|
||||
!coverable(open) &&
|
||||
coversChainage(open, chainageM)
|
||||
)
|
||||
return { chainageM, structureId: openId };
|
||||
// 폼은 위에서 푼 값으로 엶 — 공용 `selectAt` 은 노면공을 덮어 엶(152-5)
|
||||
if (hit.pipeChainageM !== null) {
|
||||
currentChainageM = hit.pipeChainageM;
|
||||
refreshSpecDefaults(hit.pipeChainageM);
|
||||
deps.markStructure?.(`pipe-${hit.pipeChainageM.toFixed(2)}`); // 측점선 없는 물넘이포장도(53-2)
|
||||
}
|
||||
// 폼은 위에서 푼 값으로 엶 — 공용 `selectAt` 은 노면공을 덮어 엶(152-5)
|
||||
if (hit.pipeChainageM !== null) section.selectPipeByChainage(hit.pipeChainageM);
|
||||
else section.selectById(hit.structureId);
|
||||
section.selectPipeByChainage(hit.pipeChainageM);
|
||||
// 알약은 폼 뒤 — 폼이 연 구조물을 닫으며 알약을 끄므로(165-2 · 측점선 없는 물넘이포장도 53-2)
|
||||
deps.markStructure?.(`pipe-${hit.pipeChainageM.toFixed(2)}`);
|
||||
} else section.selectById(hit.structureId);
|
||||
return { chainageM, structureId: hit.structureId };
|
||||
},
|
||||
};
|
||||
|
||||
@@ -245,12 +245,16 @@ export function createSectionView(
|
||||
const at = section.chainage_m;
|
||||
return at >= Math.min(start, end) - 0.01 && at <= Math.max(start, end) + 0.01;
|
||||
};
|
||||
const isPavement = (structureId: string | null): boolean => {
|
||||
const mark = spanMarks.find((entry) => entry.structure_id === structureId);
|
||||
return !!mark && spanTypes.find((type) => type.type_id === mark.type_id)?.group === "G";
|
||||
};
|
||||
const applySpan = (card: HTMLElement, section: CrossSection): void => {
|
||||
const mark = spanMarkId
|
||||
? spanMarks.find((entry) => entry.structure_id === spanMarkId)
|
||||
: undefined;
|
||||
const span = inSpan(section, mark);
|
||||
const pave = span && spanTypes.find((type) => type.type_id === mark?.type_id)?.group === "G";
|
||||
const pave = span && isPavement(spanMarkId);
|
||||
card.classList.toggle("b06-cross-card--span", span && !pave);
|
||||
highlightPavement(card, pave ? mark : undefined);
|
||||
};
|
||||
@@ -541,8 +545,8 @@ export function createSectionView(
|
||||
}
|
||||
},
|
||||
selectMark: (structureId, span = true) => {
|
||||
// span 끔 = 알약만 · 카드는 고른 측점 하나(137-4 — 측점 고름 리스너)
|
||||
spanMarkId = span ? structureId : null;
|
||||
// span 끔 = 알약만 · 카드는 고른 측점 하나(137-4 — 측점 고름 리스너) · 노면공 포장층은 테두리가 아니라 늘 켬(165-2)
|
||||
spanMarkId = span || isPavement(structureId) ? structureId : null;
|
||||
syncSpanHighlights();
|
||||
profile.selectMark(structureId);
|
||||
},
|
||||
|
||||
@@ -0,0 +1,336 @@
|
||||
/* 165 노면공 고름 일관성 헬퍼 — 진짜 `createB06StructuresPanel`(SPN)에 폼 · 횡단 카드 · 종단 알약을 흉내 내 붙이고
|
||||
* 조사 K 재현 차례 6단계를 돌려 단계마다 {폼 · 알약 · 포장층 · 고른 카드 · 세션 sid} 를 표준출력 JSON 한 벌로.
|
||||
* 카드 고름 리스너는 design_view_cross.ts 의 `setStationSelectListener` 본문을, 카드 · 알약 흉내는 B06 Section_View ·
|
||||
* Section_View_Profile 의 `selectStation` · `focusStation` · `selectMark` · `showCard` 를 그대로 옮김(TS 는 그 자리 트랜스파일).
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
// 세션(structure-pick) — 메모리 한 벌
|
||||
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 PID = "p165";
|
||||
const TYPES = [
|
||||
{ type_id: "pavement_concrete", group: "G" },
|
||||
{ type_id: "hazard_sign", group: "E" },
|
||||
{ type_id: "pipe", group: "A", managed_by: "pipe_points" },
|
||||
];
|
||||
const PAVE = {
|
||||
structure_id: "pave",
|
||||
type_id: "pavement_concrete",
|
||||
placement: "interval",
|
||||
chainage_m: 30,
|
||||
start_m: 30,
|
||||
end_m: 250,
|
||||
};
|
||||
const PAVE2 = {
|
||||
structure_id: "pave2",
|
||||
type_id: "pavement_concrete",
|
||||
placement: "interval",
|
||||
chainage_m: 100,
|
||||
start_m: 100,
|
||||
end_m: 200,
|
||||
};
|
||||
const PIPES = [{ chainage_m: 80.17, facility: "pipe", source: "spacing", options: {} }];
|
||||
// 카드 = 20 m 측점 + 관 측점(비정규)
|
||||
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 의 알림 차례) ── */
|
||||
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);
|
||||
},
|
||||
};
|
||||
|
||||
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": { readStructurePick: () => null },
|
||||
"../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 ?? {} },
|
||||
},
|
||||
);
|
||||
|
||||
/* ── 종단 알약 흉내(Section_View_Profile) ── */
|
||||
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);
|
||||
// 관 측점 = 그 측점선 짝 알약 · 일반 측점 = 꺼짐 — 고른 구조물이 덮으면 지킴(53-1)
|
||||
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;
|
||||
},
|
||||
};
|
||||
|
||||
/* ── 횡단 카드 흉내(Section_View) ── */
|
||||
let markTypes = TYPES;
|
||||
const view = {
|
||||
selected: null,
|
||||
spanMarkId: null,
|
||||
listener: null,
|
||||
isPavement(id) {
|
||||
const mark = marks.find((entry) => entry.structure_id === id);
|
||||
return !!mark && markTypes.find((type) => type.type_id === mark.type_id)?.group === "G";
|
||||
},
|
||||
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);
|
||||
},
|
||||
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 || this.isPavement(id) ? id : null;
|
||||
profile.selectMark(id);
|
||||
},
|
||||
};
|
||||
|
||||
let wallSelecting = false;
|
||||
const focusStationAt = (at) => {
|
||||
const nearest = CARDS.reduce((best, card) =>
|
||||
Math.abs(card.chainage_m - at) < Math.abs(best.chainage_m - at) ? card : best,
|
||||
);
|
||||
view.focusStation(nearest.station_id);
|
||||
};
|
||||
|
||||
const current = { structures: [PAVE] };
|
||||
let panel = null;
|
||||
|
||||
/** design_view_cross.ts 카드 고름 리스너 본문(벽 → 소유 측점) — 그대로 옮김. */
|
||||
const ownerOf = (card) => (wallOwner && card.chainage_m === wallOwner.card ? wallOwner : null);
|
||||
let wallOwner = null;
|
||||
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 owner = wallSelecting ? ownerOf(target) : null;
|
||||
const opened = panel.showAtChainage(owner?.chainage_m ?? target.chainage_m);
|
||||
if (!wallSelecting) selection.selectDesign(PID, opened, { 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) => {
|
||||
if (!wallSelecting) focusStationAt(at);
|
||||
},
|
||||
markStructure: (id) => view.selectMark(id),
|
||||
onMarks: (next, types) => {
|
||||
marks = next;
|
||||
markTypes = types;
|
||||
},
|
||||
});
|
||||
await panel.load();
|
||||
}
|
||||
|
||||
/** 지금 상태 한 줄 — 포장층 = 고른 노면공 범위 안 카드 수(포장층 is-active 와 같은 판정). */
|
||||
function snap() {
|
||||
const mark = marks.find((entry) => entry.structure_id === view.spanMarkId);
|
||||
const pave = mark && view.isPavement(mark.structure_id);
|
||||
const inSpan = (at) =>
|
||||
at >= Math.min(mark.start_m, mark.end_m) - 0.01 &&
|
||||
at <= Math.max(mark.start_m, mark.end_m) + 0.01;
|
||||
const card = CARDS.find((entry) => entry.station_id === view.selected);
|
||||
return {
|
||||
form: form.id ?? (form.pipe === null ? null : `pipe-${form.pipe.toFixed(2)}`),
|
||||
pill: profile.pill,
|
||||
paveLayer: pave ? mark.structure_id : null,
|
||||
paveCards: pave ? CARDS.filter((entry) => inSpan(entry.chainage_m)).length : 0,
|
||||
card: card ? card.chainage_m : null,
|
||||
sid: selection.readSelection(PID)?.structureId ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
/** 좌측 목록 / 종단 알약 = 폼에 그 구조물(B05 `loadForm` · SVP `edit.selectStructure`). */
|
||||
const listPick = (id) => loadForm(structures.find((entry) => entry.structure_id === id));
|
||||
const pillPick = (id) => {
|
||||
profile.pill = id; // 종단 그림이 먼저 그 알약을 켬
|
||||
panel.selectStructure(id);
|
||||
};
|
||||
const cardPick = (at) => view.selectStation(cardAt(at).station_id);
|
||||
/** 벽 누름 — SPS `revetOffset.select` 감싸개(소유 측점 폼) 뒤 카드 고름(벽 고르는 턴). */
|
||||
const wallPick = (cardAtM, ownerM) => {
|
||||
wallOwner = { card: cardAtM, chainage_m: ownerM };
|
||||
wallSelecting = true;
|
||||
selection.selectDesign(PID, panel.selectionAt(ownerM), { partKey: "inlet", from: "b06" });
|
||||
panel.showAtChainage(ownerM);
|
||||
if (view.selected !== cardAt(cardAtM).station_id) cardPick(cardAtM);
|
||||
wallSelecting = false;
|
||||
};
|
||||
|
||||
(async () => {
|
||||
const out = {};
|
||||
await build([PAVE, { structure_id: "sign", type_id: "hazard_sign", chainage_m: 127.86 }]);
|
||||
listPick("pave");
|
||||
out.step1 = snap();
|
||||
cardPick(140);
|
||||
out.step2 = snap();
|
||||
cardPick(100);
|
||||
out.step2b = snap();
|
||||
wallPick(100, 80.17); // 고른 카드의 벽 = 카드 고름 리스너 안 돎
|
||||
out.step3 = snap();
|
||||
pillPick("pave");
|
||||
out.step4 = snap();
|
||||
cardPick(80); // 관 자리 카드 = 관(지금처럼)
|
||||
out.pipeCard = snap();
|
||||
pillPick("pave");
|
||||
cardPick(280); // 범위 밖 = 모두 풂
|
||||
out.outside = snap();
|
||||
|
||||
// 5. B05 알약 → B06 복귀 — 넘김값 {범위 첫 카드 · sid} · `restoreStructurePick` 의 focusStation 차례
|
||||
await build([PAVE]);
|
||||
selection.selectDesign(PID, { chainageM: 40, structureId: "pave" }, { from: "b05" });
|
||||
panel.prefer("pave");
|
||||
view.focusStation(cardAt(40).station_id);
|
||||
panel.prefer(null);
|
||||
panel.selectStructure("pave");
|
||||
out.step5 = snap();
|
||||
|
||||
// 6. 같은 측점에 노면공 둘 — 목록 둘째 → 그 층만 · 범위 안 카드 눌러도 둘째 그대로
|
||||
await build([PAVE, PAVE2]);
|
||||
listPick("pave2");
|
||||
out.step6 = snap();
|
||||
cardPick(160);
|
||||
out.step6b = snap();
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
})().catch((error) => {
|
||||
process.stderr.write(String(error?.stack ?? error));
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -25,7 +25,7 @@ def test_범위_강조_클래스는_고름_목록갱신_카드재생성_세_길
|
||||
# applySpan 한 곳 — syncSpanHighlights · buildCrossCard 가 부름
|
||||
assert VIEW.count("b06-cross-card--span") == 1
|
||||
assert re.search(
|
||||
r"selectMark: \(structureId, span = true\) => \{\s+(//[^\n]*\s+)?spanMarkId = span \? structureId : null;",
|
||||
r"selectMark: \(structureId, span = true\) => \{\s+(//[^\n]*\s+)?spanMarkId = span \|\| isPavement\(structureId\) \? structureId : null;",
|
||||
VIEW,
|
||||
)
|
||||
assert re.search(
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""165 B06 노면공 고름 일관성 — 조사 K 재현 차례 6단계.
|
||||
|
||||
못박는 것(단계마다 폼 · 알약 · 포장층 · 고른 카드 · 세션 sid 가 같은 것을 가리킴)
|
||||
1. 목록 노면공 = 범위 안 카드 · 포장층 · 알약 · 폼 · sid 모두 노면공(기준점이 범위 경계여도 범위 밖 카드로 안 감).
|
||||
2. 범위 안 빈 카드(관 · 덮는 구조물 없음) = 노면공 그대로 · 카드만 옮김(165-1).
|
||||
3. 고른 카드의 벽 = 관 폼 + 관 알약(알약이 꺼진 채 폼만 남지 않음 · 165-2) · 포장층 풂.
|
||||
4. 알약 다시 = 1 과 같음. 관 자리 카드 = 관 · 범위 밖 카드 = 모두 풂(지금처럼).
|
||||
5. B05 알약 → B06 복귀 = 1 과 같음 · 넘김 카드에서 안 튐.
|
||||
6. 겹친 노면공 둘 = 고른 층만 · 범위 안 카드 눌러도 그 층 그대로.
|
||||
|
||||
Node 헬퍼 `helper_165_pavement_flow.cjs` 가 진짜 SPN 에 폼 · 카드 · 알약 흉내를 붙여 돌림. 화면은 사용자 확인.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.abspath(os.path.join(HERE, "..", ".."))
|
||||
|
||||
|
||||
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():
|
||||
proc = subprocess.run(
|
||||
["node", os.path.join(HERE, "helper_165_pavement_flow.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def _pave(card, layer="pave", cards=12):
|
||||
return {
|
||||
"form": layer,
|
||||
"pill": layer,
|
||||
"paveLayer": layer,
|
||||
"paveCards": cards,
|
||||
"card": card,
|
||||
"sid": layer,
|
||||
}
|
||||
|
||||
|
||||
PIPE = {"form": "pipe-80.17", "pill": "pipe-80.17", "paveLayer": None, "paveCards": 0}
|
||||
|
||||
|
||||
def test_1_목록_노면공(out):
|
||||
assert out["step1"] == _pave(40) # 기준점 30 = 범위 경계 → 범위 안 첫 카드(20 아님)
|
||||
|
||||
|
||||
def test_2_범위_안_빈_카드는_노면공_그대로(out):
|
||||
assert out["step2"] == _pave(140)
|
||||
assert out["step2b"] == _pave(100)
|
||||
|
||||
|
||||
def test_3_벽은_관_폼과_관_알약_함께(out):
|
||||
assert out["step3"] == {**PIPE, "card": 100, "sid": None}
|
||||
|
||||
|
||||
def test_4_알약_다시_관_카드_범위_밖(out):
|
||||
assert out["step4"] == _pave(40)
|
||||
assert out["pipeCard"] == {**PIPE, "card": 80, "sid": None}
|
||||
assert out["outside"] == {
|
||||
"form": None,
|
||||
"pill": None,
|
||||
"paveLayer": None,
|
||||
"paveCards": 0,
|
||||
"card": 280,
|
||||
"sid": None,
|
||||
}
|
||||
|
||||
|
||||
def test_5_B05_복귀(out):
|
||||
assert out["step5"] == _pave(40)
|
||||
|
||||
|
||||
def test_6_겹친_노면공(out):
|
||||
assert out["step6"] == _pave(100, "pave2", 6)
|
||||
assert out["step6b"] == _pave(160, "pave2", 6)
|
||||
|
||||
|
||||
def test_배선():
|
||||
panel = _read("B06_Section", "B06_Section_UI_Page_Structures_Panel.ts")
|
||||
assert "deps.focusChainage(focusOf(structure));" in panel
|
||||
assert "return { chainageM, structureId: openId };" in panel
|
||||
# 관 알약은 폼 뒤(폼이 연 구조물을 닫으며 알약을 끔)
|
||||
at = panel.index("section.selectPipeByChainage(hit.pipeChainageM);")
|
||||
assert at < panel.index("deps.markStructure?.(`pipe-${hit.pipeChainageM.toFixed(2)}`);", at)
|
||||
view = _read("B06_Section", "B06_Section_UI_Section_View.ts")
|
||||
assert "spanMarkId = span || isPavement(structureId) ? structureId : null;" in view
|
||||
Reference in New Issue
Block a user