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:
@@ -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 사용자 지시).
|
||||
|
||||
@@ -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 분리).
|
||||
|
||||
@@ -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"),
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -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"),
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -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") }),
|
||||
);
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user