fix(B05·B06): 고르기 모달 [해제] 가 [저장] 에서 사라지던 흠 · 항목 더하기 창 빈 칸 안내 (PLAN 26장)
- 구조물 저장은 세션 초안을 보냄(횡단 보기에서 한 지정 · 해제가 종단 보기 메모리 때문에 사라지던 것) - 관 시설 보관소는 서버 응답을 정본으로(집계 항목 칸이 없으면 미지정 · 옛 지정이 되살아나던 것) - 항목 더하기 · 새 항목 창: 빈 이름 · 빈 단위에 칸 빨간 테두리 + 한 줄 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EauA2ATmqNRbeV48s9CcBr
This commit is contained in:
@@ -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: {
|
||||
|
||||
@@ -250,7 +250,9 @@ export function createStructuresBridge(deps: StructuresBridgeDeps) {
|
||||
async function saveStructuresIfDirty(): Promise<void> {
|
||||
if (readPending() === null) return;
|
||||
// 저장 요청이 겹치면 판번호가 어긋나므로 앞의 저장이 끝난 뒤에 보낸다.
|
||||
structureSaving = structureSaving.then(() => persistStructures(ownStructures));
|
||||
// 보낼 것 = 세션 초안 — 횡단 보기에서 고친 조작도 이 초안에 있다(이 보기의 `ownStructures` 는
|
||||
// 횡단 보기 고침을 모른다 · 안 그러면 횡단 보기에서 한 지정 · 해제가 [저장]에서 사라짐).
|
||||
structureSaving = structureSaving.then(() => persistStructures(readPending() ?? ownStructures));
|
||||
await structureSaving;
|
||||
}
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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<string, string> = {};
|
||||
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();
|
||||
});
|
||||
|
||||
@@ -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 # 화면이 쥔 값을 이어받던 옛 규칙
|
||||
@@ -284,7 +284,7 @@ export function openItemPicker(opts: ItemPickerOptions): Promise<ItemPickerResul
|
||||
note: fill(NEW_TEXT.note, where),
|
||||
fields: [
|
||||
{ key: "name", label: NEW_TEXT.name, required: true },
|
||||
{ key: "unit", label: NEW_TEXT.unit, placeholder: "m · ㎥ · 개소" },
|
||||
{ key: "unit", label: NEW_TEXT.unit, placeholder: "m · ㎥ · 개소", required: true },
|
||||
],
|
||||
okLabel: NEW_TEXT.ok,
|
||||
cancelLabel: NEW_TEXT.cancel,
|
||||
|
||||
@@ -22,6 +22,7 @@ export const ui_locales_m2 = {
|
||||
M02_Create: ["만들기", "Create"],
|
||||
M02_Close: ["닫기", "Close"],
|
||||
M02_NameNeeded: ["이름을 적을 것", "Enter a name"],
|
||||
M02_FieldNeeded: ["{label} 을(를) 적을 것", "Enter {label}"],
|
||||
M02_DeleteConfirm: ["「{value}」 양식을 지울까?", "Delete template “{value}”?"],
|
||||
M02_InUse: [
|
||||
"쓰는 양식이라 못 지움 「{value}」 — {use}",
|
||||
|
||||
Reference in New Issue
Block a user