feat(구조물표): 322-2 M02 표 화면 — 탭 셋(한눈 표 · 종류별 · 부품) · 반영 · 부품 · 알약 색 저장 · 목록 + enabled 함께 · 검사 짝(화면 = 서버 · 1-3 규칙 · 만든 뒤 고정) · 명세 폼 보임 따름 · 판 알림
- 한눈 표(M02_SO_Def_Table): 군 · 종류 ▲▼ · 이름 · 목록 · 자리꼴 · 정본(고정) · 횡단도 · 카드 이름 · 형상(+ 벽 기본 형태 · 물빼기) · 알약 색 · 3D · 검사 · 처음 값 = 흐림 「기본」 · 바꿈 = 진함 · [처음 값으로] - 종류별: 반영 칸 + 명세 이름 나무 줄마다 보임(컨테이너 = 그 안 칸) · 못 숨김 · 미리보기 숨김 따름 · 「안 씀」 = 목록 B06 만 - 부품 탭(M02_SO_Def_Parts): 부품 · 읽는 칸 · 자리꼴 · 정본 · 쓰는 종류 · 켬/끔(정의 부품 = 끈 것만) - 편집 손질(M02_SO_Def_Reflect): 처음 값이면 반영에서 지움 · [저장] 때 enabled = 목록 both 일 때만 참 - 검사 짝 M02_SO_Def_Check.ts ↔ M02_SO_Check.py(글까지 같음): 반영 · 부품 꼴 · 형상 none → 횡단도 none · 부품 자리꼴 · 정본 · 기본값 없는 읽는 칸 새로 숨김 금지(앞 판 숨김 그대로) · 종류 지우기 · 자리꼴 · 정본 · 군 글자 · 선택지 값 고정 · 서버 check(doc, 지금 판) · 앞 판 없으면 꼴만 - 명세 폼: 쓰는 옵션 키가 모두 숨긴 칸 · 상자는 안 그림(값 그대로) · B05 · B06 시설 · 구조물 명세 폼이 정의 「표시」를 따름 - 판 알림: GET 구조물 목록에 definition_version · structures.json 판 ≠ 지금 판이면 「구조물 정의가 판 N 으로 바뀜 — 저장하면 반영」 한 번 - 시험: test_322_2_check_mirror(거울 · 규칙 · 반영 손질 · 서버 저장 · 422) · 판 알림 · 옛 정의 화면 시험 = 선택지 값 대신 이름 표 · 못 숨김 아닌 칸 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
This commit is contained in:
@@ -141,6 +141,8 @@ export interface StructureListResponse {
|
||||
project_id: string;
|
||||
revision: number;
|
||||
structures: StructureInstance[];
|
||||
/** 정본을 적을 때의 구조물 정의 판(322-2) — 없으면 옛 파일 · 아직 안 적음. */
|
||||
definition_version?: number | null;
|
||||
}
|
||||
|
||||
export interface StructureSaveResponse {
|
||||
@@ -211,6 +213,21 @@ export async function dropStaleStructureTypes(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
const noticed = new Set<string>();
|
||||
|
||||
/** 프로젝트 정본이 옛 정의 판으로 적혔으면 알림 한 줄(322-2 · 탭 수명 동안 프로젝트 · 판마다 한 번). */
|
||||
export function staleDefinitionNotice(
|
||||
projectId: string,
|
||||
saved: number | null | undefined,
|
||||
): string | null {
|
||||
if (saved === null || saved === undefined || typesVersion === null || saved === typesVersion)
|
||||
return null;
|
||||
const key = `${projectId}:${saved}:${typesVersion}`;
|
||||
if (noticed.has(key)) return null;
|
||||
noticed.add(key);
|
||||
return `구조물 정의가 판 ${typesVersion} 으로 바뀜 — 저장하면 반영`;
|
||||
}
|
||||
|
||||
const staleListeners = new Set<() => void>();
|
||||
|
||||
function checkStaleOnShow(): void {
|
||||
|
||||
@@ -69,6 +69,17 @@ def load_structures(project_root: str) -> tuple[int, list[StructureInstance]]:
|
||||
return revision, structures
|
||||
|
||||
|
||||
def load_saved_definition_version(project_root: str) -> int | None:
|
||||
"""정본을 적을 때의 구조물 정의 판(322-2 판 알림) — 파일이 없거나 옛 파일이면 None."""
|
||||
path = structures_file_path(project_root)
|
||||
try:
|
||||
with open(path, encoding="utf-8") as handle:
|
||||
value = json.load(handle).get("definition_version")
|
||||
except (OSError, ValueError, TypeError, AttributeError):
|
||||
return None
|
||||
return value if isinstance(value, int) and not isinstance(value, bool) else None
|
||||
|
||||
|
||||
def load_migrated_legacy(project_root: str) -> set[str]:
|
||||
"""이미 구조물 정본으로 옮긴 구 비정규 측점의 표식 집합.
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ from B05_Profile.B05_Profile_Structures_Migration import migrate_irregular_stati
|
||||
from B05_Profile.B05_Profile_Structures_Repository import (
|
||||
StructureRevisionConflict,
|
||||
load_migrated_legacy,
|
||||
load_saved_definition_version,
|
||||
load_structures,
|
||||
requires_downstream_invalidation,
|
||||
save_structures,
|
||||
@@ -159,7 +160,10 @@ async def read_structures(project_id: UUID) -> StructureListResponse | JSONRespo
|
||||
return JSONResponse(status_code=404, content=_PROJECT_PATH_MISSING)
|
||||
revision, structures = load_structures(root)
|
||||
return StructureListResponse(
|
||||
project_id=str(project_id), revision=revision, structures=structures
|
||||
project_id=str(project_id),
|
||||
revision=revision,
|
||||
structures=structures,
|
||||
definition_version=load_saved_definition_version(root),
|
||||
)
|
||||
except LookupError:
|
||||
# 저장 경로 조회가 예외로 알려온 "프로젝트 없음" — 500이 아니라 404다.
|
||||
|
||||
@@ -305,3 +305,5 @@ class StructureListResponse(BaseModel):
|
||||
project_id: str
|
||||
revision: int
|
||||
structures: list[StructureInstance] = Field(default_factory=list)
|
||||
# 정본을 적을 때의 정의 판(322-2) — 지금 판과 다르면 화면이 「저장하면 반영」 한 줄 알림
|
||||
definition_version: int | None = None
|
||||
|
||||
@@ -50,7 +50,7 @@ import {
|
||||
outletTiersRow,
|
||||
revetChoiceRow,
|
||||
} from "./B05_Profile_UI_Drainage_Revet_Choice";
|
||||
import { optionDefaults } from "./B05_Profile_UI_Structures_Fields";
|
||||
import { hiddenOptionKeys, optionDefaults } from "./B05_Profile_UI_Structures_Fields";
|
||||
import {
|
||||
createSpecForm,
|
||||
type SpecForm,
|
||||
@@ -607,6 +607,7 @@ export function createFacilityOptionsForm(
|
||||
const names = definition?.spec_names ?? {};
|
||||
// 필수 상자 빈 고르기 칸이 받는 정의 기본값(54-43) — 옛 키는 열 때와 같은 길로 바꿈
|
||||
const defaults = optionDefaults(definition?.options ?? []);
|
||||
const hidden = hiddenOptionKeys(definition?.options ?? []); // 정의 「표시」 숨김(322-2)
|
||||
if (facility === "pipe") {
|
||||
// 정의 파일의 옛 이름 「흙막이 추가」 — 명세 이름 「흙막이」가 앞섬(54-1)
|
||||
const { soil_add: _old, ...pipeNames } = names;
|
||||
@@ -616,11 +617,18 @@ export function createFacilityOptionsForm(
|
||||
!["revet", "I", "L", "U"].includes(String(kind ?? ""))
|
||||
? { structure: kind }
|
||||
: null;
|
||||
pipeForm.write(PIPE_SPEC, options, pipeNames, groups, defaults);
|
||||
pipeForm.write(PIPE_SPEC, options, pipeNames, groups, defaults, hidden);
|
||||
}
|
||||
if (facility === "box_culvert") {
|
||||
// 본체 규격은 저장 안 함 — 폭 · 높이로 되찾음 · 옛 설치 「있음」 = 날개벽
|
||||
boxForm.write(BOX_SPEC, boxOptionsIn(options), names, groups, boxOptionsIn(defaults));
|
||||
boxForm.write(
|
||||
BOX_SPEC,
|
||||
boxOptionsIn(options),
|
||||
names,
|
||||
groups,
|
||||
boxOptionsIn(defaults),
|
||||
hidden,
|
||||
);
|
||||
for (const [id, value] of Object.entries(bodyOf(options)))
|
||||
boxForm.setValue("body", id, value);
|
||||
}
|
||||
@@ -631,9 +639,10 @@ export function createFacilityOptionsForm(
|
||||
names,
|
||||
groups,
|
||||
withLegacyGuards(defaults),
|
||||
hidden,
|
||||
);
|
||||
if (facility === "ford_pavement")
|
||||
paveForm.write(FORD_PAVEMENT_SPEC, options, names, groups, defaults);
|
||||
paveForm.write(FORD_PAVEMENT_SPEC, options, names, groups, defaults, hidden);
|
||||
if (facility === "ford_bridge") fordFlowBox.fill();
|
||||
if (facility === "ford_pavement") paveFlowBox.fill();
|
||||
wingInFields.write(options);
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
fetchStructures,
|
||||
dropStaleStructureTypes,
|
||||
fetchStructureTypes,
|
||||
staleDefinitionNotice,
|
||||
saveStructures,
|
||||
readPendingStructures,
|
||||
writePendingStructures,
|
||||
@@ -369,6 +370,8 @@ export function createStructuresBridge(deps: StructuresBridgeDeps) {
|
||||
]),
|
||||
);
|
||||
structureRevision = stored.revision;
|
||||
const note = staleDefinitionNotice(deps.projectId, stored.definition_version); // 322-2
|
||||
if (note) showToast(note);
|
||||
// 저장하지 않고 나갔던 조작분이 있으면 그것으로 화면을 세운다(2026-08-29).
|
||||
const pending = readPending();
|
||||
ownStructures = pending ?? stored.structures;
|
||||
|
||||
@@ -101,6 +101,11 @@ export function optionControl(
|
||||
return element;
|
||||
}
|
||||
|
||||
/** 정의 「표시」로 숨긴 옵션 키(서버 `visible` 거짓 · 322-2) — 명세 폼이 그 칸 · 상자를 안 그림. */
|
||||
export const hiddenOptionKeys = (
|
||||
options: ReadonlyArray<{ key: string; visible?: boolean }>,
|
||||
): Set<string> => new Set(options.filter((o) => o.visible === false).map((o) => o.key));
|
||||
|
||||
/** 새 구조물 칸에 실을 등록부 기본값(PLAN 46-7) — 기본값이 있는 칸만 · 없는 칸은 빈 칸(미확정). */
|
||||
export function optionDefaults(
|
||||
options: ReadonlyArray<{ key: string; default: string | number | null }>,
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
import { STRUCTURE_SPECS } from "../A00_Common/design/design_structure_pipe";
|
||||
import { ADDON_SPECS } from "../A00_Common/design/design_structure_slope_tier";
|
||||
import { specKeys, type StructureSpec } from "../A00_Common/design/design_structure_spec";
|
||||
import { optionDefaults } from "./B05_Profile_UI_Structures_Fields";
|
||||
import { hiddenOptionKeys, optionDefaults } from "./B05_Profile_UI_Structures_Fields";
|
||||
import type { WideningBox } from "./B05_Profile_UI_Structures_Widening";
|
||||
|
||||
const forms = new WeakMap<HTMLElement, SpecForm>();
|
||||
@@ -86,7 +86,15 @@ export function paintStructureSpec(
|
||||
form.root.hidden = !spec;
|
||||
if (!spec) return;
|
||||
const groups = Object.fromEntries((type?.groups ?? []).map((g) => [g.id, g.name]));
|
||||
form.write(spec, values, type?.spec_names ?? {}, groups, optionDefaults(type?.options ?? []));
|
||||
const options = type?.options ?? [];
|
||||
form.write(
|
||||
spec,
|
||||
values,
|
||||
type?.spec_names ?? {},
|
||||
groups,
|
||||
optionDefaults(options),
|
||||
hiddenOptionKeys(options),
|
||||
);
|
||||
}
|
||||
|
||||
/** 명세 폼 값(저장 키 그대로) — 명세 없는 종류면 빈 묶음. */
|
||||
|
||||
Reference in New Issue
Block a user