refactor(B05): 구조물 설치측·이격 입력 폐지 — 데이터 필드까지 제거
사용자 지시로 두 값을 걷어낸다. 구조물별 옵션·정의는 앞으로 타입마다 따로 정한다. 어차피 소비처가 없어(B06~B08은 structures를 읽지 않는다) 화면에만 남아 있던 값이었다. - 폼: 설치측 select·이격 칸 행 삭제, 목록 부제·종단 벌룬에서 측 표기 제거 - 계약: StructureSide 타입, StructureInstance.side/offset_m 삭제(프론트·스키마) - 설계지문에서 두 값 제외 — 후속 단계 무효화 판정에서 빠진다 - 구 저장분은 읽을 때 두 키만 떨어낸다. extra="forbid"라 그냥 두면 정본이 통째로 버려진다. 다른 낯선 키는 계속 거절한다
This commit is contained in:
@@ -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<string, string | number>;
|
||||
memo: string;
|
||||
placement_source: "manual" | "suggested" | "automatic";
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -25,8 +25,6 @@ export interface StructureListParams {
|
||||
typeMap: Map<string, StructureType>;
|
||||
/** 측점 표기용 측점 간격(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 {
|
||||
|
||||
@@ -79,13 +79,6 @@ function positionText(structure: StructureInstance, intervalM: number): string {
|
||||
: formatStation(structure.chainage_m ?? 0, intervalM);
|
||||
}
|
||||
|
||||
const SIDE_TEXT: Record<string, string> = {
|
||||
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);
|
||||
|
||||
@@ -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<string, string> = {
|
||||
호환: "기타",
|
||||
};
|
||||
|
||||
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",
|
||||
|
||||
Reference in New Issue
Block a user