diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts index 950395fc..3bc90bf7 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts @@ -124,16 +124,11 @@ export function createFacilityStore() { summary_item?: SummaryItem | null; }>, ): void { - const before = entries; entries = points .map((point) => { - // 서버가 집계 항목 칸을 아직 안 돌려주면(칸 없음) 화면이 쥔 값을 같은 측점에서 이어받는다 — - // 칸이 있으면(null 포함) 응답이 정본이다. - const summary = - point.summary_item === undefined - ? (before.find((entry) => Math.abs(entry.chainage - point.chainage_m) <= 0.005) - ?.attributes.summary_item ?? null) - : point.summary_item; + // 응답이 정본이다 — 서버는 미지정이면 칸을 안 싣는다(`as_dict`). 예전엔 칸이 없으면 화면이 쥔 + // 값을 이어받았는데, 횡단 보기에서 [해제]한 초안이 응답으로 돌아올 때 옛 지정이 되살아났다. + const summary = point.summary_item ?? null; return { chainage: point.chainage_m, attributes: { diff --git a/B05_Profile/B05_Profile_UI_Page_Structures.ts b/B05_Profile/B05_Profile_UI_Page_Structures.ts index 3903281f..49cc9873 100644 --- a/B05_Profile/B05_Profile_UI_Page_Structures.ts +++ b/B05_Profile/B05_Profile_UI_Page_Structures.ts @@ -250,7 +250,9 @@ export function createStructuresBridge(deps: StructuresBridgeDeps) { async function saveStructuresIfDirty(): Promise { if (readPending() === null) return; // 저장 요청이 겹치면 판번호가 어긋나므로 앞의 저장이 끝난 뒤에 보낸다. - structureSaving = structureSaving.then(() => persistStructures(ownStructures)); + // 보낼 것 = 세션 초안 — 횡단 보기에서 고친 조작도 이 초안에 있다(이 보기의 `ownStructures` 는 + // 횡단 보기 고침을 모른다 · 안 그러면 횡단 보기에서 한 지정 · 해제가 [저장]에서 사라짐). + structureSaving = structureSaving.then(() => persistStructures(readPending() ?? ownStructures)); await structureSaving; } diff --git a/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts b/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts index 80ece6bb..3ab9d6fe 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts @@ -259,7 +259,7 @@ export function createTreePanel(opt: TreePanelOptions): TreePanelHandle { if (!(await ready())) return; const got = await askText(L("M02_TreeAddItem"), [ { key: "name", label: L("M02_TreeItemName") }, - { key: "unit", label: L("M02_TreeUnit"), optional: true }, + { key: "unit", label: L("M02_TreeUnit") }, ]); if (got) { await edit({ diff --git a/M02_MasterTemplete/M02_MasterTemplete_TreePanel_Dialogs.ts b/M02_MasterTemplete/M02_MasterTemplete_TreePanel_Dialogs.ts index 962b31c4..2735c7cf 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_TreePanel_Dialogs.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_TreePanel_Dialogs.ts @@ -4,13 +4,7 @@ * 모두 약속(Promise)으로 답함 — 닫기 · Esc · 바깥 누르기 = 취소(null / "cancel"). * ========================================================================== */ -import { - createButton, - createInputField, - createSelectField, - el, - showToast, -} from "@ui/ui_template_elements"; +import { createButton, createInputField, createSelectField, el } from "@ui/ui_template_elements"; import { t as L } from "@ui/ui_template_locale"; import { openModal } from "@ui/ui_template_modal"; @@ -18,7 +12,7 @@ export interface TextField { key: string; label: string; value?: string; - /** 비워도 됨(항목 단위) */ + /** 비워도 됨 — 아니면 비운 채 [확인] 하면 그 칸에 안내 글 */ optional?: boolean; } @@ -47,16 +41,22 @@ export function askText( })); const ok = createButton({ label: okLabel, variant: "filled" }); const submit = (): void => { + // 빈 칸은 그 칸에 빨간 테두리 + 한 줄(칸마다) — 토스트는 창 뒤에 가려 안 보였음 const out: Record = {}; + let bad = false; for (const { f, field } of inputs) { out[f.key] = field.input.value.trim(); - if (!out[f.key] && !f.optional) return showToast(L("M02_NameNeeded"), "error"); + const empty = !out[f.key] && !f.optional; + field.setError(empty ? L("M02_FieldNeeded").replace("{label}", f.label) : undefined); + bad ||= empty; } + if (bad) return; finish(out); close(); }; ok.addEventListener("click", submit); for (const { field } of inputs) { + field.input.addEventListener("input", () => field.setError(undefined)); field.input.addEventListener("keydown", (event) => { if (event.key === "Enter" && !event.isComposing) submit(); }); diff --git a/resources/tester/test_summary_clear_and_tree_dialog.py b/resources/tester/test_summary_clear_and_tree_dialog.py new file mode 100644 index 00000000..62cbe191 --- /dev/null +++ b/resources/tester/test_summary_clear_and_tree_dialog.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +"""고르기 모달 [해제] 가 [저장] 에서 사라지던 흠 · 항목 더하기 창 빈 칸 안내 흠(계획서 26장 끝). + +① 구조물 저장은 **세션 초안**을 보낸다 — 횡단 보기에서 한 지정 · 해제는 그 보기의 초안(`writePendingStructures`) + 에만 있고 종단 보기 몫 `ownStructures` 는 모른다. 옛 코드는 `ownStructures` 를 보내 횡단 보기에서 한 + 모달 [적용] · [해제] 가 [저장] 에서 사라졌다. +② 항목 더하기 창(`askText`)은 빈 칸에 **그 칸 빨간 테두리 + 한 줄**(`setError`) — 토스트는 창 뒤에 가려 안 보였다. +""" + +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] + + +def _text(relative: str) -> str: + return (ROOT / relative).read_text(encoding="utf-8") + + +def test_구조물_저장은_세션_초안을_보냄(): + page = _text("B05_Profile/B05_Profile_UI_Page_Structures.ts") + body = page.split("async function saveStructuresIfDirty()")[1].split("\n }\n")[0] + assert "persistStructures(readPending() ?? ownStructures)" in body + assert "persistStructures(ownStructures)" not in body # 옛 코드 — 횡단 보기 고침이 사라짐 + # 횡단 보기는 같은 초안에 쓴다(보내는 것과 쓰는 것이 한 곳) + cross = _text("B06_Section/B06_Section_UI_Page_Structures_Panel.ts") + assert "writePendingStructures(deps.projectId, structures)" in cross + + +def test_항목_더하기_빈_칸은_칸_안내로_알림(): + dialogs = _text("M02_MasterTemplete/M02_MasterTemplete_TreePanel_Dialogs.ts") + submit = dialogs.split("const submit = (): void => {")[1].split("ok.addEventListener")[0] + assert "field.setError(" in submit and 'L("M02_FieldNeeded")' in submit + assert "showToast" not in dialogs # 창 뒤에 가려 안 보이던 토스트 안 씀 + locale = _text("ui_template/ui_template_locale_m2.ts") + assert re.search(r'M02_FieldNeeded: \["\{label\}', locale) + # 단위도 비우면 안내 — 옵션(optional)이 아님 + panel = _text("M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts") + assert ( + 'label: L("M02_TreeUnit") }' in panel + and "optional: true" + not in panel.split("async function addItem")[1].split("async function rename")[0] + ) + + +def test_관_시설_보관소는_응답을_정본으로_집계_항목이_없으면_미지정(): + """횡단 보기에서 [해제]한 관 초안이 응답으로 돌아올 때 옛 지정이 되살아나면 안 됨.""" + store = _text("B05_Profile/B05_Profile_UI_Drainage_Facility.ts") + body = store.split("replaceFromResponse(")[1].split("},\n /** 한 지점의 시설을 바꾼다")[0] + assert "const summary = point.summary_item ?? null;" in body + assert "before.find" not in body # 화면이 쥔 값을 이어받던 옛 규칙 diff --git a/ui_template/ui_template_item_picker.ts b/ui_template/ui_template_item_picker.ts index 4062fbd8..f01f7d9f 100644 --- a/ui_template/ui_template_item_picker.ts +++ b/ui_template/ui_template_item_picker.ts @@ -284,7 +284,7 @@ export function openItemPicker(opts: ItemPickerOptions): Promise