fix(b05,b06): 종단도 알약 고름 · 풂 — 측점 없는 알약 = 측점선 안 켬 · B06 다시 누름 = 풀기 한 길 (284)

- 측점 있음 = 제 측점을 심는 알약(272 한 벌 — A군 · C · D 구간형 · 관은 물넘이포장 뺌) · 그 밖 알약은 같은 자리 · 근처 측점선을 짝으로 켜지 않음(plantsStation · stationIdAtStructure)
- B06 알약 다시 누름 = Esc 와 같은 풀기 한 길(카드 · 폼 · 알약 · 범위 테두리) — 옛 처리는 풂 신호를 버림
- B06 측점 없는 구조물 고름 = 노면공처럼 카드 안 고름 · 스크롤만 · 물넘이포장 알약 = 그 관 폼 · 알약 · 고름 값에 알약 id(selectPipeAt)
- B05 물넘이포장 알약 = 고름 값에 알약 id(같은 m 일반 측점선 안 켬)
- 시험: test_284_1 새로 · 옛 시험 글 대조 · 표지 카드 기대값(None) · 헬퍼 stub(진짜 plantsStation)
- 기준점 = 태그 base-284-pill-before(282e9fcfc) · 되돌리기 = 이 커밋 git revert

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6BNBFXFndbqYxu6sZuknK
This commit is contained in:
2026-10-12 19:52:13 +09:00
co-authored by Claude Opus 5.5
parent 282e9fcfc8
commit 54cc970746
17 changed files with 341 additions and 17 deletions
+9 -2
View File
@@ -233,9 +233,16 @@ export function createDesignProfile(ctx: DesignProfileContext) {
/** 한 측점에 알약이 여럿(배수관 · 메쌓기)이면 지금 고른 알약을 그대로 둔다 — 첫 알약으로
* 갈아 끼우면 누른 것과 다른 알약이 켜졌다(계획서 24-6b). */
const structureIdAtStation = (stationId: string | null): string | null =>
structureIdKeeping(stationId, selectedStructureId, irregularStations, structures, detail);
structureIdKeeping(
stationId,
selectedStructureId,
irregularStations,
structures,
detail,
structureTypes,
);
const stationIdAtStructure = (structureId: string | null): string | null =>
stationIdAtStructureOf(structureId, irregularStations, structures);
stationIdAtStructureOf(structureId, irregularStations, structures, structureTypes);
/**
* 측점 선택 — 같은 측점 재선택이면 해제한다(2026-08-04 사용자 지시).
+1
View File
@@ -350,6 +350,7 @@ export async function mountCrossView(
structuresPanel.moveStructureTo(structureId, toChainageM);
},
selectStructure: (structureId) => structuresPanel.selectStructure(structureId),
selectPipe: (chainageM) => structuresPanel.selectPipeAt(chainageM), // 측점 없는 관(요구 178)
pipesChanged: () => void refreshDetailForStructures(),
});
// 폼 → 횡단 캐시 반영은 따로 뗀 모듈이 맡는다(2026-09-02 분리).
+3 -1
View File
@@ -175,7 +175,9 @@ export async function mountProfileView(
// (가상 id `pipe-*`)은 관 정본 소관이라 누가거리로 되돌려 보낸다(2026-08-17).
onStructureSelect: (structureId) => {
const chainage = bridge.pipeMarkChainage(structureId);
rememberPick(bridge.markChainage(structureId), chainage === null ? structureId : null);
// 측점 없는 관(물넘이포장)은 알약 id 도 남김 — 같은 m 일반 측점선을 안 켬(요구 178)
const keepId = chainage === null || bridge.stationlessPipeMark(structureId);
rememberPick(bridge.markChainage(structureId), keepId ? structureId : null);
if (chainage !== null) {
panel.structures.selectPipeByChainage(chainage);
selectStationOfPipe(chainage);
@@ -34,6 +34,7 @@ import type { createRouteProfilePanel } from "./B05_Profile_UI_Profile_Panel";
import { FACILITY_NAMES } from "./B05_Profile_UI_Page_Helpers";
import { followedStation } from "./B05_Profile_UI_Profile_Alignment";
import { followSnapped } from "./B05_Profile_UI_Structures_Snap";
import { plantsStation } from "./B05_Profile_UI_Profile_Structures";
import {
pipeStationMarks,
SAME_STATION_M,
@@ -423,6 +424,11 @@ export function createStructuresBridge(deps: StructuresBridgeDeps) {
clearProjectedStations,
/** 알약 식별자에서 관 누가거리를 되읽는다(관이 아니면 null). */
pipeMarkChainage,
/** 측점 없는 관 알약(물넘이포장)인가 — 고름 값에 알약 id 를 남길지 가름(요구 178). */
stationlessPipeMark: (structureId: string | null): boolean => {
const mark = pipeMarks.find((entry) => entry.structure_id === structureId);
return !!mark && !plantsStation(mark, []);
},
/** 알약 식별자의 누가거리(구조물·관 공통). B06 으로 넘길 선택값을 만들 때 쓴다. */
markChainage,
/** 사이드 목록이 바뀜 — 화면을 맞추고 서버 정본에 저장한다. */
@@ -15,9 +15,17 @@ import {
isPipeStation,
type IrregularStation,
} from "./B05_Profile_UI_IrregularStations";
import { structureAnchorM, type StructureInstance } from "./B05_Profile_Api_Structures";
import {
structureAnchorM,
type StructureInstance,
type StructureType,
} from "./B05_Profile_Api_Structures";
import type { PickGroup } from "./B05_Profile_UI_Structures_Panel_Types";
import { PIPE_MATCH_M } from "../A00_Common/design/design_selection";
import {
pipeStationMarks,
structureStationMarks,
} from "../common_util/common_util_structure_station";
/** 사이드 「구조물 배치」 군 · 종류 드롭다운 목록(`section.pickGroups()`)을 그대로 구조물군 → 종류
* 2단 메뉴 항목으로(129-1 — 종단그래프 · 배수유역도 · 3D · B06 종단 공용 · 가린 군 · 종류는 뺌).
@@ -157,6 +165,7 @@ export function structureIdKeeping(
detail: {
longitudinal: { stations: ReadonlyArray<{ station_id: string; chainage_m: number }> };
} | null,
types: ReadonlyArray<StructureType> = [],
): string | null {
const selected = structures.find((item) => item.structure_id === selectedId);
const at = detail?.longitudinal.stations.find(
@@ -165,20 +174,36 @@ export function structureIdKeeping(
const keeps =
stationId !== null &&
!!selected &&
(stationIdAtStructure(selectedId, stations, structures) === stationId ||
(stationIdAtStructure(selectedId, stations, structures, types) === stationId ||
(at !== undefined && Math.abs(structureAnchorM(selected) - at) < SAME_CHAINAGE_M));
return keeps ? selectedId : structureIdAtStation(stationId, stations, structures);
}
/** 알약(구조물) id → 측점선 id. 세로선이 없는 구조물(A군 외)이면 null. */
/** 제 측점을 심는 알약인가 — 272 측점 심기 한 벌(A군 · C · D군 구간형 · 관은 물넘이포장 뺌).
* 안 심는 구조물 알약은 같은 자리 · 근처 측점선을 짝으로 켜지 않음(요구 178) · 종류를 모르면 옛 길. */
export function plantsStation(
structure: StructureInstance,
types: ReadonlyArray<StructureType>,
): boolean {
if (String(structure.structure_id ?? "").startsWith("pipe-"))
return (
pipeStationMarks({ chainage_m: structureAnchorM(structure), facility: structure.type_id })
.length > 0
);
const type = types.find((entry) => entry.type_id === structure.type_id);
return !type || structureStationMarks(structure, type).length > 0;
}
/** 알약(구조물) id → 측점선 id. 제 측점을 안 심는 구조물(B군 · 물넘이포장 등)이면 null. */
export function stationIdAtStructure(
structureId: string | null,
stations: IrregularStation[],
structures: StructureInstance[],
types: ReadonlyArray<StructureType> = [],
): string | null {
if (structureId === null) return null;
const structure = structures.find((item) => item.structure_id === structureId);
if (!structure) return null;
if (!structure || !plantsStation(structure, types)) return null;
const anchor = structureAnchorM(structure);
const station = stations.find((entry) => Math.abs(entry.chainage_m - anchor) < SAME_CHAINAGE_M);
return station ? irregularStationId(station.id) : null;
@@ -49,6 +49,7 @@ import {
} from "../A00_Common/design/design_selection";
import { showToast } from "@ui/ui_template_elements";
import { snapStructureStation } from "../common_util/common_util_structure_station";
import { plantsStation } from "../B05_Profile/B05_Profile_UI_Profile_Structures";
import {
adjustDockRoot,
refreshAdjustSlots,
@@ -129,6 +130,8 @@ export interface B06StructuresPanel {
preferred: () => string | null;
/** 구조물 하나를 폼에 올리고 그 카드로 간다 — 알약을 누른 길(목록 클릭과 같은 흐름). */
selectStructure: (structureId: string) => void;
/** 측점 없는 관(물넘이포장) 하나를 폼에 올림 — 카드 안 고름 · 스크롤만 · 알약 · 고름 값에 알약 id(요구 178). */
selectPipeAt: (chainageM: number) => void;
/** 지금 목록의 구조물 정본(미저장분 포함) — 카드 찾기에 쓴다. */
getStructures: () => StructureInstance[];
/** 지금 폼에 올라온 계곡 통과 시설의 누가거리(없으면 null). */
@@ -220,11 +223,12 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
currentChainageM = null; // 폼 = 이 구조물 — 옛 관을 붙든 채 남지 않게(165-2)
// 카드 고름이 측점으로 다시 풀면 같은 측점 관이 먼저 열린다 — 누른 구조물을 넘긴다.
// 노면공 = 카드 하나 안 고름 · 범위 카드 전부 테두리 · 그 카드로 스크롤만 · 폼 유지(177-4)
const pave = !coverable(structure);
// 측점 없는 구조물(제 측점을 안 심음)도 같은 길 — 근처 카드 측점선을 켜지 않음(요구 178)
const revealOnly = !coverable(structure) || !plantsStation(structure, markTypes);
const id = structure.structure_id ?? null;
preferredId = id;
// 측점 고름(카드 하나 · 다시 세움)이 연 폼 = 누른 카드 그대로 — 구조물 기준 카드로 옮겨 가지 않음(189-1 · 231-1)
if (!openingAtStation) deps.focusChainage(focusOf(structure), pave);
if (!openingAtStation) deps.focusChainage(focusOf(structure), revealOnly);
preferredId = null;
// 알약은 카드 뒤에 — 카드 측점이 알약을 갈아 끼우거나 끄지 않게(53-2)
deps.markStructure?.(id, !openingAtStation); // 측점 고름이 연 것 = 카드 하나 · 범위 테두리 없음(231-1)
@@ -584,5 +588,22 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
}
return { chainageM, structureId: hit.structureId };
},
selectPipeAt: (chainageM) => {
const pipe = pipeFacilities.find(
(entry) => Math.abs(entry.chainage_m - chainageM) < PIPE_MATCH_M,
);
if (!pipe) return;
const id = `pipe-${pipe.chainage_m.toFixed(2)}`;
deps.focusChainage(pipe.chainage_m, true); // 카드 안 고름 · 스크롤만 — 옛 카드 · 측점선 풂(177-4 길)
currentChainageM = pipe.chainage_m;
refreshSpecDefaults(pipe.chainage_m);
section.selectPipeByChainage(pipe.chainage_m);
deps.markStructure?.(id); // 알약은 폼 뒤(165-2)
selectDesign(
deps.projectId,
{ chainageM: pipe.chainage_m, structureId: id },
{ from: "b06" },
);
},
};
}
@@ -33,6 +33,8 @@ export interface SectionStructureEdit {
moveStructure: (structureId: string, toChainageM: number) => void;
/** 구조물(관 아님) 하나를 폼에 올리고 그 카드로 간다 — 같은 측점 관보다 먼저(52-15). */
selectStructure: (structureId: string) => void;
/** 측점 없는 관(물넘이포장) 하나를 폼에 올림 — 카드 안 고름 · 그 자리로 스크롤만(요구 178). */
selectPipe?: (chainageM: number) => void;
/** 배수 재계산이 관 종류 · 관경을 바꿈 — 카드(미리 뜬 새 관 그림 포함)를 다시 얹음(273-2). */
pipesChanged?: () => void;
}
@@ -23,6 +23,8 @@ import {
selectDesign,
} from "../A00_Common/design/design_selection";
import { paintMapPick } from "../A00_Common/design/design_pick_profile";
import { ESCAPE_FROM } from "../A00_Common/design/design_pick_escape";
import { plantsStation } from "../B05_Profile/B05_Profile_UI_Profile_Structures";
import { fetchLatestRoute, type RouteLatestResponse } from "../B05_Profile/B05_Profile_Api_Fetch";
import type {
CrossSection,
@@ -231,6 +233,11 @@ export function createSectionProfile(
// 알약을 누르면 그 자리에 가장 가까운 측점 카드를 고른다 — 목록 클릭과 같은 규칙.
// 구조물 알약은 그 구조물을 폼에 올린다 — 카드가 측점으로 풀면 같은 측점 관이 먼저다(52-15).
onStructureSelect: (structureId) => {
// 알약 다시 누름 = 풀기 한 길(Esc 와 같은 값 — 카드 · 폼 · 알약 · 범위 테두리 모두 · 요구 178)
if (structureId === null) {
selectDesign(shared.projectId, NO_SELECTION, { from: ESCAPE_FROM });
return;
}
const hit = marks.find((entry) => entry.structure_id === structureId);
if (!hit) return;
const edit = host.edit();
@@ -238,6 +245,11 @@ export function createSectionProfile(
edit.selectStructure(hit.structure_id);
return;
}
// 측점 없는 관(물넘이포장) = 카드 안 고름 · 그 자리로 스크롤만 · 그 관 폼 · 알약만(요구 178)
if (edit?.selectPipe && !plantsStation(hit, types)) {
edit.selectPipe(structureAnchorM(hit));
return;
}
// 목록에서 풀어도 카드는 남음 — 같은 카드면 「다시 보기」만 돼 관이 안 열렸다(53-2)
host.selectCard(null);
host.selectCard(nearestCard(structureAnchorM(hit))?.station_id ?? null);
@@ -159,6 +159,24 @@ const panelModule = loadTs(
setAdjustSideSlots() {},
},
"./B06_Section_UI_Page_Structures_Spec": { withSpecDefaults: (o) => o ?? {} },
// 요구 178 — 알약 측점 짝(진짜 `plantsStation` · 272 측점 심기 한 벌)
"../B05_Profile/B05_Profile_UI_Profile_Structures": loadTs(
path.join(ROOT, "B05_Profile", "B05_Profile_UI_Profile_Structures.ts"),
{
"@ui/ui_template_context_menu": { createMapContextMenu() {} },
"./B05_Profile_UI_IrregularStations": {
irregularStationId: (id) => `irregular:${id}`,
isPipeStation: () => false,
},
"./B05_Profile_Api_Structures": {
structureAnchorM: (s) => s.chainage_m ?? s.start_m ?? 0,
},
"../A00_Common/design/design_selection": selection,
"../common_util/common_util_structure_station": loadTs(
path.join(ROOT, "common_util", "common_util_structure_station.ts"),
),
},
),
},
);
+19 -1
View File
@@ -42,7 +42,7 @@ const structurePick = loadTs(
const PID = "p231";
const TYPES = [
{ type_id: "pavement_concrete", group: "G" },
{ type_id: "retaining_wall", group: "C" },
{ type_id: "retaining_wall", group: "C", placement: "interval" }, // 실제 등록부 = C 구간형(측점 심음)
{ type_id: "hazard_sign", group: "E" },
{ type_id: "pipe", group: "A", managed_by: "pipe_points" },
];
@@ -178,6 +178,24 @@ const panelModule = loadTs(
setAdjustSideSlots() {},
},
"./B06_Section_UI_Page_Structures_Spec": { withSpecDefaults: (o) => o ?? {} },
// 요구 178 — 알약 측점 짝(진짜 `plantsStation` · 272 측점 심기 한 벌)
"../B05_Profile/B05_Profile_UI_Profile_Structures": loadTs(
path.join(ROOT, "B05_Profile", "B05_Profile_UI_Profile_Structures.ts"),
{
"@ui/ui_template_context_menu": { createMapContextMenu() {} },
"./B05_Profile_UI_IrregularStations": {
irregularStationId: (id) => `irregular:${id}`,
isPipeStation: () => false,
},
"./B05_Profile_Api_Structures": {
structureAnchorM: (s) => s.chainage_m ?? s.start_m ?? 0,
},
"../A00_Common/design/design_selection": selection,
"../common_util/common_util_structure_station": loadTs(
path.join(ROOT, "common_util", "common_util_structure_station.ts"),
),
},
),
},
);
+106
View File
@@ -0,0 +1,106 @@
/* 284 알약 고름 헬퍼 — 진짜 `B05_Profile_UI_Profile_Structures.ts`(plantsStation · stationIdAtStructure ·
* structureIdKeeping)를 Node 로 돌려 표준출력 JSON 한 벌로(요구 178 · 179-1).
* 등록부 꼴 = 구조물정의.json 의 group · placement · managed_by 그대로.
*/
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;
}
const selection = loadTs(path.join(ROOT, "A00_Common", "design", "design_selection.ts"), {
"../b_page_state": { readState: () => null, writeState() {} },
});
const PREFIX = "irregular:";
const rule = loadTs(path.join(ROOT, "B05_Profile", "B05_Profile_UI_Profile_Structures.ts"), {
"@ui/ui_template_context_menu": { createMapContextMenu() {} },
"./B05_Profile_UI_IrregularStations": {
irregularStationId: (id) => `${PREFIX}${id}`,
isPipeStation: (station) => station.origin === "pipe",
},
"./B05_Profile_Api_Structures": {
structureAnchorM: (s) => s.chainage_m ?? s.start_m ?? 0,
},
"../A00_Common/design/design_selection": selection,
"../common_util/common_util_structure_station": loadTs(
path.join(ROOT, "common_util", "common_util_structure_station.ts"),
),
});
const TYPES = [
{ type_id: "cross_underdrain", group: "A", placement: "point", managed_by: null },
{ type_id: "retaining_wall", group: "C", placement: "interval", managed_by: null },
{ type_id: "ditch_side", group: "B", placement: "interval", managed_by: null },
{ type_id: "hazard_sign", group: "E", placement: "point", managed_by: null },
{ type_id: "bed_sill", group: "D", placement: "point", managed_by: null },
];
const S = (id, type, at, extra = {}) => ({
structure_id: id,
type_id: type,
chainage_m: at,
...extra,
});
const STRUCTURES = [
S("drain", "cross_underdrain", 40),
S("wall", "retaining_wall", 100, { start_m: 90, end_m: 110 }),
S("ditch", "ditch_side", 80, { start_m: 60, end_m: 100 }),
S("sign", "hazard_sign", 80), // 관 측점(80)과 같은 m — 측점 없는 구조물
S("sill", "bed_sill", 140),
S("unknown", "legacy_x", 160),
S("pipe-80.00", "pipe", 80),
S("pipe-120.00", "ford_pavement", 120),
];
// 종단도 구조물 측점(관 투영 + 심은 구조물) — 물넘이포장은 측점 없음
const STATIONS = [
{ id: "drain", chainage_m: 40, origin: "structure" },
{ id: "pipe-80.00", chainage_m: 80, origin: "pipe" },
{ id: "wall", chainage_m: 100, origin: "structure" },
];
const byId = (id) => STRUCTURES.find((s) => s.structure_id === id);
const plants = Object.fromEntries(
STRUCTURES.map((s) => [s.structure_id, rule.plantsStation(s, TYPES)]),
);
const line = Object.fromEntries(
STRUCTURES.map((s) => [
s.structure_id,
rule.stationIdAtStructure(s.structure_id, STATIONS, STRUCTURES, TYPES),
]),
);
// 종류 없이 부르면(옛 부름) 구조물은 옛 길 — 같은 자리 측점선
const lineNoTypes = rule.stationIdAtStructure("sign", STATIONS, STRUCTURES);
// 관 측점선(80)을 다시 누름 — 고른 것이 측점 없는 표지면 관 알약으로(표지는 선 짝 아님)
const keepSign = rule.structureIdKeeping(
`${PREFIX}pipe-80.00`,
"sign",
STATIONS,
STRUCTURES,
null,
TYPES,
);
// 고른 것이 관이면 그대로
const keepPipe = rule.structureIdKeeping(
`${PREFIX}pipe-80.00`,
"pipe-80.00",
STATIONS,
STRUCTURES,
null,
TYPES,
);
process.stdout.write(
JSON.stringify({ plants, line, lineNoTypes, keepSign, keepPipe, sample: byId("sign") }),
);
+3 -1
View File
@@ -110,7 +110,9 @@ def test_6_겹친_노면공(out):
def test_배선():
panel = _read("B06_Section", "B06_Section_UI_Page_Structures_Panel.ts")
assert "deps.focusChainage(focusOf(structure), pave);" in panel
assert (
"deps.focusChainage(focusOf(structure), revealOnly);" in panel
) # 노면공 · 측점 없는 구조물(요구 178)
assert "openId" not in panel # 범위 안 빈 카드 = 연 노면공 지킴(165-1) 없앰 · 189-1
# 관 알약은 폼 뒤(폼이 연 구조물을 닫으며 알약을 끔)
at = panel.index("section.selectPipeByChainage(hit.pipeChainageM);")
@@ -70,6 +70,9 @@ def test_스크롤만_길은_리스너를_안_거침():
cross = _read("A00_Common", "design", "design_view_cross.ts")
assert "if (revealOnly) sectionView.revealStation(nearest.station_id);" in cross
panel = _read("B06_Section", "B06_Section_UI_Page_Structures_Panel.ts")
assert "const pave = !coverable(structure);" in panel
# 노면공 + 측점 없는 구조물(제 측점을 안 심음)도 스크롤만(요구 178)
assert (
"const revealOnly = !coverable(structure) || !plantsStation(structure, markTypes);" in panel
)
# 목록 고름은 노면공뿐 아니라 늘 고름 기록(231-2)
assert 'if (!openingAtStation) selectDesign(deps.projectId, at, { from: "b06" });' in panel
+2 -2
View File
@@ -60,7 +60,7 @@ def test_같은_카드_목록_고름_뒤_오감도_노면공_안_켬(out) -> Non
assert after["form"] == "sign" # 목록 고름도 기록 — 오감 뒤 그 폼(231-2)
assert after["paveLayer"] is None
assert after["span"] != "pave"
assert after["card"] == 120
assert after["card"] is None # 측점 없는 표지 = 카드 안 고름 · 스크롤만(요구 178)
def test_목록_노면공은_오감해도_범위_강조_그대로(out) -> None:
@@ -99,7 +99,7 @@ def test_B05_3D_고름은_노면공_id_를_안_적음(out) -> 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 "if (!openingAtStation) deps.focusChainage(focusOf(structure), revealOnly);" in panel
assert "deps.markStructure?.(id, !openingAtStation);" in panel
cross = _read("A00_Common", "design", "design_view_cross.ts")
assert (
@@ -51,7 +51,8 @@ def test_카드_120_뒤_목록_관_오감_새로고침_관_폼(out) -> None:
def test_같은_카드_목록_표지는_기록되고_오감_새로고침_뒤_그대로(out) -> None:
sign = {"form": "sign", "paveLayer": None, "span": "sign", "card": 120, "sid": "sign"}
# 측점 없는 표지 = 카드 안 고름 · 스크롤만 · 폼 · 범위 테두리 · 기록은 그대로(요구 178)
sign = {"form": "sign", "paveLayer": None, "span": "sign", "card": None, "sid": "sign"}
assert out["listSign"] == sign
assert out["listSignTrip"] == sign
assert out["listSignF5"] == sign
+99
View File
@@ -0,0 +1,99 @@
# -*- coding: utf-8 -*-
"""284 종단도 알약 고름 · 풂(요구 178 · 179-1 · 조사 AM).
못박는 것
- 측점 있음 = 제 측점을 심는 알약(272 한 벌 — A군 · C · D 구간형 · 관은 물넘이포장 뺌) · 종류 모르면 옛 길.
- 측점 없는 알약(B군 · 표지 · 바닥막이 · 물넘이포장)은 같은 자리 · 근처 측점선을 짝으로 켜지 않음.
- 관 측점선을 다시 누르면 측점 없는 표지가 아니라 그 관 알약.
- B06 알약 다시 누름 = 풀기 한 길(Esc 값) · 측점 없는 관 = 카드 안 고름 · 스크롤만 · 고름 값에 알약 id.
- B05 물넘이포장 알약 = 고름 값에 알약 id(같은 m 일반 측점선 안 켬).
Node 헬퍼 `helper_284_1_pill_pick.cjs` 가 진짜 TS 를 돌림. 화면은 사용자 확인.
"""
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_284_1_pill_pick.cjs")],
capture_output=True,
text=True,
encoding="utf-8",
timeout=60,
)
assert proc.returncode == 0, proc.stderr
return json.loads(proc.stdout)
def test_측점_심기_규칙(out):
assert out["plants"] == {
"drain": True, # A군 점
"wall": True, # C군 구간형
"ditch": False, # B군
"sign": False, # E군 표지
"sill": False, # D군 점
"unknown": True, # 종류 모름 = 옛 길
"pipe-80.00": True, # 관
"pipe-120.00": False, # 물넘이포장
}
def test_측점_없는_알약은_측점선_짝_없음(out):
line = out["line"]
assert line["drain"] == "irregular:drain" and line["wall"] == "irregular:wall"
assert line["pipe-80.00"] == "irregular:pipe-80.00"
# 표지는 관 측점(80)과 같은 m 여도 그 선을 안 켬 · 물넘이포장 · B군 · D군 점도 없음
assert line["sign"] is None and line["pipe-120.00"] is None
assert line["ditch"] is None and line["sill"] is None
assert out["lineNoTypes"] == "irregular:pipe-80.00" # 종류 없이 부르면 옛 길
def test_관_측점선_다시_누름은_관_알약(out):
assert out["keepSign"] == "pipe-80.00"
assert out["keepPipe"] == "pipe-80.00"
def test_종단도는_종류를_넘김():
profile = _read("A00_Common", "design", "design_profile.ts")
assert (
"stationIdAtStructureOf(structureId, irregularStations, structures, structureTypes)"
in profile
)
assert " detail,\n structureTypes,\n );" in profile
def test_B06_알약_풂과_측점_없는_관():
view = _read("B06_Section", "B06_Section_UI_Section_View_Profile.ts")
assert "if (structureId === null) {" in view
assert "selectDesign(shared.projectId, NO_SELECTION, { from: ESCAPE_FROM });" in view
assert "if (edit?.selectPipe && !plantsStation(hit, types)) {" in view
cross = _read("A00_Common", "design", "design_view_cross.ts")
assert "selectPipe: (chainageM) => structuresPanel.selectPipeAt(chainageM)," in cross
panel = _read("B06_Section", "B06_Section_UI_Page_Structures_Panel.ts")
body = panel[panel.index("selectPipeAt: (chainageM) => {") :][:900]
assert "deps.focusChainage(pipe.chainage_m, true);" in body
assert "deps.markStructure?.(id);" in body
assert "{ chainageM: pipe.chainage_m, structureId: id }," in body and '{ from: "b06" },' in body
def test_B05_물넘이포장_알약은_id_를_남김():
view = _read("A00_Common", "design", "design_view_profile.ts")
assert "const keepId = chainage === null || bridge.stationlessPipeMark(structureId);" in view
assert "rememberPick(bridge.markChainage(structureId), keepId ? structureId : null);" in view
page = _read("B05_Profile", "B05_Profile_UI_Page_Structures.ts")
assert "stationlessPipeMark: (structureId: string | null): boolean => {" in page
assert "return !!mark && !plantsStation(mark, []);" in page
+3 -2
View File
@@ -7,6 +7,7 @@
화면 수치(문맥 수 · 루프 · 같은 노드 · 전환 시간)는 ORCA 로 잰다 — 여기서는 길이 끊기지 않게 잠근다.
"""
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent.parent
@@ -77,10 +78,10 @@ def test_창_키_듣기를_뗌():
def test_겹친_알약은_누른_것을_그대로():
profile = _read("A00_Common/design/design_profile.ts")
assert "structureIdKeeping(stationId, selectedStructureId" in profile
assert re.search(r"structureIdKeeping\(\s*stationId,\s*selectedStructureId", profile)
# 판정은 `_Profile_Structures` 로 옮김 — 비정규 측점 짝 · 규칙 측점(B06 카드) 기준점(53-2)
rule = _read("B05_Profile/B05_Profile_UI_Profile_Structures.ts")
assert "stationIdAtStructure(selectedId, stations, structures) === stationId" in rule
assert "stationIdAtStructure(selectedId, stations, structures, types) === stationId" in rule
assert "Math.abs(structureAnchorM(selected) - at) < SAME_CHAINAGE_M" in rule