From b92ba4592e4f104fe7db113ac7fae8e8555052a9 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 17 Aug 2026 20:04:32 +0900 Subject: [PATCH] =?UTF-8?q?refactor(B05):=20=EA=B5=AC=EC=A1=B0=EB=AC=BC=20?= =?UTF-8?q?=EC=84=A4=EC=B9=98=EC=B8=A1=C2=B7=EC=9D=B4=EA=B2=A9=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=20=ED=8F=90=EC=A7=80=20=E2=80=94=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=ED=95=84=EB=93=9C=EA=B9=8C=EC=A7=80=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사용자 지시로 두 값을 걷어낸다. 구조물별 옵션·정의는 앞으로 타입마다 따로 정한다. 어차피 소비처가 없어(B06~B08은 structures를 읽지 않는다) 화면에만 남아 있던 값이었다. - 폼: 설치측 select·이격 칸 행 삭제, 목록 부제·종단 벌룬에서 측 표기 제거 - 계약: StructureSide 타입, StructureInstance.side/offset_m 삭제(프론트·스키마) - 설계지문에서 두 값 제외 — 후속 단계 무효화 판정에서 빠진다 - 구 저장분은 읽을 때 두 키만 떨어낸다. extra="forbid"라 그냥 두면 정본이 통째로 버려진다. 다른 낯선 키는 계속 거절한다 --- B05_Profile/B05_Profile_Api_Structures.ts | 4 --- .../B05_Profile_Structures_Migration.py | 1 - .../B05_Profile_Structures_Repository.py | 24 +++++++++++--- B05_Profile/B05_Profile_Structures_Schema.py | 2 -- B05_Profile/B05_Profile_UI_Page_Structures.ts | 2 -- B05_Profile/B05_Profile_UI_Structures_List.ts | 5 +-- .../B05_Profile_UI_Structures_Marks.ts | 9 +----- .../B05_Profile_UI_Structures_Panel.ts | 32 ++----------------- 8 files changed, 23 insertions(+), 56 deletions(-) diff --git a/B05_Profile/B05_Profile_Api_Structures.ts b/B05_Profile/B05_Profile_Api_Structures.ts index 363c9828..6eebc22a 100644 --- a/B05_Profile/B05_Profile_Api_Structures.ts +++ b/B05_Profile/B05_Profile_Api_Structures.ts @@ -15,8 +15,6 @@ import { API_BASE_URL, API_TIMEOUT_MS } from "@config/config_frontend"; /** 배치형태 — 점형(측점 1개) / 구간형(시~종점) / 부지형(위치+면적). */ export type StructurePlacement = "point" | "interval" | "site"; -/** 노선 기준 설치 측. 횡단(B06) 연계에 쓴다. */ -export type StructureSide = "left" | "right" | "center" | "cross"; export interface StructureOptionField { key: string; @@ -58,8 +56,6 @@ export interface StructureInstance { chainage_m?: number | null; start_m?: number | null; end_m?: number | null; - side: StructureSide; - offset_m: number; options: Record; memo: string; placement_source: "manual" | "suggested" | "automatic"; diff --git a/B05_Profile/B05_Profile_Structures_Migration.py b/B05_Profile/B05_Profile_Structures_Migration.py index ad9baeb8..1b9800dc 100644 --- a/B05_Profile/B05_Profile_Structures_Migration.py +++ b/B05_Profile/B05_Profile_Structures_Migration.py @@ -94,7 +94,6 @@ def migrate_irregular_stations(entries: Iterable[dict[str, Any]]) -> list[Struct data: dict[str, Any] = { "type_id": type_id, "placement": definition.placement, - "side": "center", "options": _options_for(type_id, entry), "placement_source": "manual", } diff --git a/B05_Profile/B05_Profile_Structures_Repository.py b/B05_Profile/B05_Profile_Structures_Repository.py index 4ddfa9e7..aeb8c833 100644 --- a/B05_Profile/B05_Profile_Structures_Repository.py +++ b/B05_Profile/B05_Profile_Structures_Repository.py @@ -10,7 +10,7 @@ import json import os import uuid -from typing import Iterable +from typing import Any, Iterable from B05_Profile.B05_Profile_Structures_Schema import StructureInstance, structure_type_map from common_util.common_util_json import atomic_write_json @@ -31,6 +31,12 @@ class StructureRevisionConflict(RuntimeError): self.actual = actual +# 2026-08-17 사용자 지시로 걷어낸 필드 — 설치측·이격은 받지 않는다. 스키마가 +# extra="forbid"라 남아 있는 저장분을 그대로 넘기면 정본 전체가 통째로 버려지므로, +# 읽을 때 이 두 키만 떨어낸다. 다른 낯선 키는 계속 거절해 정본이 조용히 썩는 것을 막는다. +_DROPPED_KEYS = ("side", "offset_m") + + def structures_file_path(project_root: str) -> str: """프로젝트 저장소 안의 구조물 정본 경로.""" return os.path.join(project_root, _STAGE_DIR, STRUCTURES_FILE_NAME) @@ -50,13 +56,23 @@ def load_structures(project_root: str) -> tuple[int, list[StructureInstance]]: payload = json.load(handle) revision = int(payload.get("revision", 0)) structures = [ - StructureInstance.model_validate(item) for item in payload.get("structures", []) + StructureInstance.model_validate(_without_dropped_keys(item)) + for item in payload.get("structures", []) ] except (OSError, ValueError, TypeError): return 0, [] return revision, structures +def _without_dropped_keys(item: Any) -> Any: + """폐지된 필드가 남아 있는 저장분을 지금 스키마로 읽을 수 있게 손질한다.""" + if not isinstance(item, dict): + return item + if not any(key in item for key in _DROPPED_KEYS): + return item + return {key: value for key, value in item.items() if key not in _DROPPED_KEYS} + + def save_structures( project_root: str, structures: Iterable[StructureInstance], @@ -97,7 +113,7 @@ def requires_downstream_invalidation( 메모나 표시용 값만 바뀐 경우까지 후속 단계를 깨면, 사용자가 메모 한 줄 고칠 때마다 횡단·수량을 다시 돌려야 한다. 그래서 설계에 실제로 영향을 주는 값 - (타입·위치·범위·측·오프셋·제원)만 비교한다. 목록 순서는 설계와 무관하므로 무시한다. + (타입·위치·범위·제원)만 비교한다. 목록 순서는 설계와 무관하므로 무시한다. """ return _design_fingerprint(previous) != _design_fingerprint(current) @@ -112,8 +128,6 @@ def _design_fingerprint(items: Iterable[StructureInstance]) -> set[str]: item.chainage_m, item.start_m, item.end_m, - item.side, - item.offset_m, item.options, item.geometry, ], diff --git a/B05_Profile/B05_Profile_Structures_Schema.py b/B05_Profile/B05_Profile_Structures_Schema.py index 486ff705..f387398f 100644 --- a/B05_Profile/B05_Profile_Structures_Schema.py +++ b/B05_Profile/B05_Profile_Structures_Schema.py @@ -98,8 +98,6 @@ class StructureInstance(BaseModel): chainage_m: float | None = Field(default=None, ge=0) start_m: float | None = Field(default=None, ge=0) end_m: float | None = Field(default=None, ge=0) - side: Literal["left", "right", "center", "cross"] = "center" - offset_m: float = 0.0 options: dict[str, Any] = Field(default_factory=dict) memo: str = "" placement_source: Literal["manual", "suggested", "automatic"] = "manual" diff --git a/B05_Profile/B05_Profile_UI_Page_Structures.ts b/B05_Profile/B05_Profile_UI_Page_Structures.ts index 3361e062..5c7358f8 100644 --- a/B05_Profile/B05_Profile_UI_Page_Structures.ts +++ b/B05_Profile/B05_Profile_UI_Page_Structures.ts @@ -177,8 +177,6 @@ export function createStructuresBridge(deps: StructuresBridgeDeps) { chainage_m: pipe.chainage_m, start_m: null, end_m: null, - side: "cross", - offset_m: 0, options: pipe.options ?? {}, memo: "", placement_source: "automatic", diff --git a/B05_Profile/B05_Profile_UI_Structures_List.ts b/B05_Profile/B05_Profile_UI_Structures_List.ts index a8d897d5..e846b674 100644 --- a/B05_Profile/B05_Profile_UI_Structures_List.ts +++ b/B05_Profile/B05_Profile_UI_Structures_List.ts @@ -25,8 +25,6 @@ export interface StructureListParams { typeMap: Map; /** 측점 표기용 측점 간격(m). */ intervalM: number; - /** 설치측 표시 이름 — 패널과 같은 목록을 쓴다. */ - sideLabels: ReadonlyArray<[string, string]>; /** 지금 폼에 올라와 있는 구조물(없으면 null). */ editingId: string | null; /** 지금 폼에 올라와 있는 계곡 통과 시설의 누가거리(없으면 null). */ @@ -96,8 +94,7 @@ export function renderStructureList(params: StructureListParams): void { const { structure } = row; item.classList.toggle("is-selected", structure.structure_id === editingId); name.textContent = labelOfStructure(structure, typeMap, intervalM); - const sideLabel = params.sideLabels.find(([value]) => value === structure.side)?.[1] ?? ""; - info.textContent = [sideLabel, structure.memo].filter(Boolean).join(" · "); + info.textContent = structure.memo ?? ""; item.addEventListener("click", () => params.onSelectStructure(structure)); if (structure.structure_id === editingId) item.scrollIntoView({ block: "nearest" }); } else { diff --git a/B05_Profile/B05_Profile_UI_Structures_Marks.ts b/B05_Profile/B05_Profile_UI_Structures_Marks.ts index 7cf7d5d0..85e7a988 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Marks.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Marks.ts @@ -79,13 +79,6 @@ function positionText(structure: StructureInstance, intervalM: number): string { : formatStation(structure.chainage_m ?? 0, intervalM); } -const SIDE_TEXT: Record = { - left: "좌측", - right: "우측", - center: "중심", - cross: "횡단", -}; - /** 알약 세로 자리 배정 — 이웃과 겹치는 것만 두 줄로 나누고, 나머지는 가운데에 둔다 * (2026-08-17 사용자 지시 2). 값 0 = 윗줄, 1 = 아랫줄, null = 가운데. */ function rowAssignments( @@ -215,7 +208,7 @@ function buildBalloon( const title = document.createElement("strong"); title.textContent = type?.name ?? structure.type_id; const position = document.createElement("span"); - position.textContent = `${positionText(structure, stationIntervalM)} · ${SIDE_TEXT[structure.side] ?? ""}`; + position.textContent = positionText(structure, stationIntervalM); balloon.append(title, position); const summary = optionSummary(structure, type); diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel.ts b/B05_Profile/B05_Profile_UI_Structures_Panel.ts index 14d384fc..34bb76ee 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel.ts @@ -19,7 +19,6 @@ import { structureAnchorM, type StructureInstance, type StructurePlacement, - type StructureSide, type StructureType, } from "./B05_Profile_Api_Structures"; import type { PipeFacility, PipeSource } from "../B04_PreProcess/B04_PreProcess_Api_Fetch"; @@ -42,13 +41,6 @@ const GROUP_LABELS: Record = { 호환: "기타", }; -const SIDE_LABELS: Array<[StructureSide, string]> = [ - ["left", "좌측"], - ["right", "우측"], - ["center", "중심"], - ["cross", "횡단"], -]; - /** 구간형 신규 추가 시 기본 구간 길이(m). 사용자가 종점을 바로 고칠 수 있게 짧게 잡는다. */ const DEFAULT_INTERVAL_LENGTH_M = 15; @@ -120,14 +112,11 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu const groupSelect = select([]); const typeSelect = select([]); - const sideSelect = select(SIDE_LABELS); // 위치 입력 순서 = 시작 측점 → 기준 측점 → 종료 측점 (2026-08-17 사용자 확정). // 점형은 기준 측점만 보인다. const startFields = stationFields("시작 측점"); const anchorFields = stationFields("기준 측점"); const endFields = stationFields("종료 측점"); - const offsetField = numberInput(); - offsetField.value = "0"; const memoField = document.createElement("input"); memoField.type = "text"; memoField.placeholder = "메모(선택)"; @@ -142,10 +131,6 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu typeRow.className = "b05-structure__grid"; typeRow.append(field("구조물군", groupSelect), field("종류", typeSelect)); - const sideRow = document.createElement("div"); - sideRow.className = "b05-structure__grid"; - sideRow.append(field("설치측", sideSelect), field("이격 (m)", offsetField)); - // 옵션 칸은 타입마다 다르므로 선택할 때마다 새로 그린다. const optionRow = document.createElement("div"); optionRow.className = "b05-structure__grid"; @@ -193,7 +178,6 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu typeRow, positionRow, positionDivider, - sideRow, optionRow, facilityOptions.root, actions, @@ -229,9 +213,8 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu const interval = (): number => callbacks.getInterval(); /** 배치형태·관리 주체에 맞춰 위치 칸을 바꾼다. 계곡 통과 시설(배수관 등)은 점형이라 - * 기준 측점 하나만 받고(2026-08-17 사용자 지시 2 — 시작·종료 불필요), 설치측·이격도 - * 정본(관 지점) 소관이라 감춘다. 종류 미선택(빈값)이면 입력 칸 전체를 숨긴다 — - * 리셋 직후 상태. */ + * 기준 측점 하나만 받는다(2026-08-17 사용자 지시 2 — 시작·종료 불필요). + * 종류 미선택(빈값)이면 입력 칸 전체를 숨긴다 — 리셋 직후 상태. */ function syncPlacementFields(): void { const type = currentType(); const managed = !!type?.managed_by; @@ -240,7 +223,6 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu positionDivider.hidden = !type; startFields.wrap.hidden = !isInterval; endFields.wrap.hidden = !isInterval; - sideRow.hidden = !type || managed; primary.disabled = !type; anchorFields.wrap.querySelector("span")!.textContent = isInterval ? "기준 측점 (비우면 시작)" @@ -327,7 +309,6 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu pipeFacilities, typeMap: typeMap(), intervalM: interval(), - sideLabels: SIDE_LABELS, editingId, selectedPipeChainage, onSelectStructure: (structure) => @@ -352,8 +333,6 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu anchorFields.write(structureAnchorM(target), step); startFields.write(target.start_m ?? null, step); endFields.write(target.end_m ?? null, step); - sideSelect.value = target.side; - offsetField.value = String(target.offset_m ?? 0); memoField.value = target.memo ?? ""; renderOptionFields(target.options); syncFacilityForm(target.options); @@ -361,7 +340,6 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu anchorFields.write(null, step); startFields.write(null, step); endFields.write(null, step); - offsetField.value = "0"; memoField.value = ""; renderOptionFields(); syncFacilityForm(); @@ -480,8 +458,6 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu chainage_m: anchor, start_m: start, end_m: end, - side: sideSelect.value as StructureSide, - offset_m: Number(offsetField.value) || 0, options: readOptions(), memo: memoField.value.trim(), placement_source: "manual" as const, @@ -636,8 +612,6 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu placement === "interval" ? chainageM + DEFAULT_INTERVAL_LENGTH_M : null, step, ); - sideSelect.value = "center"; - offsetField.value = "0"; memoField.value = ""; syncPlacementFields(); renderOptionFields(); @@ -656,8 +630,6 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu chainage_m: chainageM, start_m: placement === "interval" ? chainageM : null, end_m: placement === "interval" ? chainageM + DEFAULT_INTERVAL_LENGTH_M : null, - side: "center", - offset_m: 0, options: defaultOptions(type), memo: "", placement_source: "manual",