From 6ff34a92f29a812c5dfc85fac93157d559b95d7a Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 10 Oct 2026 23:43:55 +0900 Subject: [PATCH] =?UTF-8?q?feat(B08):=20=EA=B1=B0=EB=A6=AC=EC=9D=B4?= =?UTF-8?q?=EC=A0=95=ED=91=9C=20=EB=93=B1=EB=A1=9D=20=EB=AA=A8=EB=8B=AC=20?= =?UTF-8?q?=ED=95=AD=EB=AA=A9=20=3D=20=EC=86=90=20=EC=9E=85=EB=A0=A5=20+?= =?UTF-8?q?=20=EB=B6=84=EB=A5=98=EB=B3=84=20=EC=A0=9C=EC=95=88=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D(244-14)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 항목 칸 = 손 입력 + datalist 제안(거래처 = 견본 다섯 · 기준위치 = 기준점) · 목록 밖 글도 받음 - 분류 바꿈 = 제안만 바뀜 · 쓴 글 그대로 · 줄마다 제안 목록 따로 - 시험 test_244_14 (실제 rowField 를 node 로) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01STqEqztUthVFBmEP18wBSz --- .../B08_Quantity_UI_Milestone_Modal.ts | 25 +++--- .../test_244_14_b08_milestone_item_input.py | 83 +++++++++++++++++++ 2 files changed, 96 insertions(+), 12 deletions(-) create mode 100644 resources/tester/test_244_14_b08_milestone_item_input.py diff --git a/B08_Quantity/B08_Quantity_UI_Milestone_Modal.ts b/B08_Quantity/B08_Quantity_UI_Milestone_Modal.ts index 74bc8a704..f0454ab76 100644 --- a/B08_Quantity/B08_Quantity_UI_Milestone_Modal.ts +++ b/B08_Quantity/B08_Quantity_UI_Milestone_Modal.ts @@ -3,7 +3,7 @@ * 거리이정표 다이어그램 모달(PLAN 244-4 · 244-13 · 사용자 147번 5 · 8) — 페이지 이동 없이 창에서 보고 고침. * * 등록 모달(244-13) = 공사지 · 기준점 [+] · 거래처 · 기준점 상자 누름 같은 한 벌(`openPlaceModal`) — 더 큰 창 · - * 2열(분류(기준위치 · 거래처) · 항목(분류 따라 고르기 목록) · 거래처 이름 · 목적지) + 소재지(주소 찾기 · + * 2열(분류(기준위치 · 거래처) · 항목(손 입력 + 분류 따라 제안 · 244-14) · 거래처 이름 · 목적지) + 소재지(주소 찾기 · * 다음(카카오) 지도에서 고르기 · `…_UI_Milestone_Place`) · 추가 = [등록] · 상자 = [고침] · [삭제] * 분류 기준위치 = 기준점(하나 · 있으면 고르기 막음 · 다이어그램 [기준점 +] 없앰) · 목적지 = 시점 고정 * 같은 거래처 이름 = 그 경로에 표 줄만 · 방위 = 소재지 좌표로 자동 자리 @@ -56,23 +56,24 @@ export function toastKm(text: string): void { if (text) showToast(`길찾기 — ${text}`, text.includes(" — ") ? "warning" : "success"); } -/** 항목 ▾ · 목적지 ▾ 한 줄 — 목록 밖 옛 항목 글도 보임 */ +let listSeq = 0; + +/** 항목(손 입력 + 제안 목록 · 244-14) · 목적지 ▾ 한 줄 — 목록 밖 글도 받음 · 분류 바꾸면 제안만 바뀜 */ function rowField(r: MilestoneRow, kind: "route" | "base") { - const item = createSelectField({ label: "항목", compact: true, options: [] }); + const item = createInputField({ label: "항목", compact: true }); + const list = el("datalist", { attrs: { id: `b08-milestone-items-${++listSeq}` } }); + item.input.setAttribute("list", list.id); + item.input.value = r.item; + item.root.append(list); const dest = createSelectField({ label: "목적지", compact: true, options: DEST_OPTIONS }); const setKind = (k: "route" | "base"): void => { - const list = [...ITEMS[k]]; - if (r.item && !list.includes(r.item)) list.unshift(r.item); - const keep = list.includes(item.select.value) ? item.select.value : r.item || list[0]; - item.setOptions( - list.map((t) => ({ value: t, text: t })), - keep, - ); + list.replaceChildren(...ITEMS[k].map((t) => el("option", { attrs: { value: t } }))); + item.input.placeholder = `예 ${ITEMS[k][0]}`; dest.root.style.visibility = k === "base" ? "hidden" : ""; // 기준점 → 시점 고정 }; setKind(kind); dest.select.value = r.dest; - item.select.dataset.b08MilestoneAdd = "item"; + item.input.dataset.b08MilestoneAdd = "item"; return { item, dest, note: r.note, setKind }; } @@ -169,7 +170,7 @@ export function openPlaceModal( setKind(kind); const rowValues = (): MilestoneRow[] => rows.map((r) => ({ - item: r.item.select.value, + item: r.item.input.value.trim(), dest: r.dest.select.value as Dest, note: r.note, })); diff --git a/resources/tester/test_244_14_b08_milestone_item_input.py b/resources/tester/test_244_14_b08_milestone_item_input.py new file mode 100644 index 000000000..f13a81e1a --- /dev/null +++ b/resources/tester/test_244_14_b08_milestone_item_input.py @@ -0,0 +1,83 @@ +"""244-14 B08 거리이정표 [+] 모달 「항목」 = 손 입력 + 제안 목록(사용자 120-답 ③ 「항목: 수기 입력」). + +거래처 제안 = 견본 다섯 · 기준위치 제안 = 기준점 · 목록 밖 글도 받음 · 분류 바꾸면 제안만 바뀜(쓴 글 그대로) +— node 로 실제 rowField 를 돌림(가짜 DOM). +""" + +import json +import re +import subprocess +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +MODAL = ROOT / "B08_Quantity/B08_Quantity_UI_Milestone_Modal.ts" +IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE) + +STUBS = """ +type N = any; +const el = (tag: string, o: any = {}): N => { + const n: N = { tag, children: [] as N[], attrs: { ...(o.attrs ?? {}) }, dataset: {}, style: {}, + value: "", placeholder: "", + get id() { return n.attrs.id; }, + setAttribute(k: string, v: string) { n.attrs[k] = v; }, + append(...c: N[]) { n.children.push(...c); }, + replaceChildren(...c: N[]) { n.children = c; } }; + return n; +}; +const createInputField = (o: any) => { + const input = el("input"); + return { root: el("div", { children: [] }), input, label: o.label, setError: () => undefined }; +}; +const createSelectField = (o: any) => { + const select = el("select"); + return { root: el("div"), select, setOptions: () => undefined }; +}; +""" + +SCRIPT = """ +const sug = (f: any) => f.item.root.children.find((c: N) => c.tag === "datalist") + .children.map((c: N) => c.attrs.value); +const a = rowField({ item: "", dest: "start", note: "" }, "route"); +const empty = { value: a.item.input.value, list: sug(a), ph: a.item.input.placeholder, + linked: a.item.input.attrs.list === a.item.root.children[0].id }; +a.item.input.value = " 레미콘(25-21-150) "; // 목록 밖 손 글 +a.setKind("base"); +const base = { value: a.item.input.value, list: sug(a), ph: a.item.input.placeholder }; +a.setKind("route"); +const old = rowField({ item: "옛 항목", dest: "mid", note: "" }, "route"); +const b = rowField({ item: "", dest: "start", note: "" }, "route"); +console.log(JSON.stringify({ empty, base, back: a.item.input.value, list: sug(a), + old: [old.item.input.value, old.dest.select.value, sug(old)], + ids: new Set([a, old, b].map((f) => f.item.input.attrs.list)).size, + tag: a.item.input.dataset.b08MilestoneAdd })); +""" + + +def test_항목_손_입력_제안_분류_바꿔도_글_유지(): + src = STUBS + IMPORTS.sub("", MODAL.read_text(encoding="utf-8")) + result = subprocess.run( + ["node", "--experimental-strip-types", "--no-warnings", "--input-type=module-typescript"], + input=src + "\n" + SCRIPT, capture_output=True, text=True, encoding="utf-8", timeout=60, + ) # fmt: skip + assert result.returncode == 0, result.stderr + got = json.loads(result.stdout) + five = ["장비(20톤)", "장비(8톤)", "혼합골재", "전석운반", "철근"] + # 빈 줄 = 빈 손 입력 칸 + 거래처 제안 다섯(datalist 이어짐) + assert got["empty"] == {"value": "", "list": five, "ph": "예 장비(20톤)", "linked": True} + # 분류 → 기준위치 = 제안만 기준점 · 쓴 글 그대로 + assert got["base"] == { + "value": " 레미콘(25-21-150) ", + "list": ["기준점"], + "ph": "예 기준점", + } + assert got["back"] == " 레미콘(25-21-150) " and got["list"] == five + # 옛 항목 글 = 그대로 칸에 · 목적지 그대로 + assert got["old"] == ["옛 항목", "mid", five] + assert got["ids"] == 3 and got["tag"] == "item" # 줄마다 제안 목록 따로 + + +def test_저장_값_손_글_그대로(): + modal = MODAL.read_text(encoding="utf-8") + assert "item: r.item.input.value.trim()," in modal # 목록 밖 글도 받음(앞뒤 빈칸만 뺌) + assert 'createSelectField({ label: "항목"' not in modal + assert len(modal.splitlines()) <= 700