From e2dfbe687b42d7d093aad5ef755d4f229bff18db Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 16:13:08 +0900 Subject: [PATCH] =?UTF-8?q?feat(m02-cf):=20InputPanel=20=EC=99=84=EC=84=B1?= =?UTF-8?q?=20=E2=80=94=20=EC=84=9C=EB=B2=84=20Api=20=EC=B5=9C=EC=A2=85=20?= =?UTF-8?q?=EA=BC=B4(ref=20=C2=B7=20Model=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=9A=94=EC=86=8C)=EC=97=90=20=EB=A7=9E=EC=B6=A4=20=C2=B7=20?= =?UTF-8?q?=ED=99=98=EC=9C=A8=20=EC=9C=A0=EA=B0=80=EC=A0=84=EB=A0=A5=20?= =?UTF-8?q?=EC=95=8C=EC=95=BD=20=EC=9D=B4=EB=A6=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_019ACSGaHdLgnkEkoA4LDtMU --- .../CostFlow/M02_CF_InputPanel.ts | 113 ++++++++++++------ .../CostFlow/M02_CF_InputPanel_Build.ts | 11 +- .../tester/test_m02_cf_inputpanel_build.py | 21 +++- 3 files changed, 99 insertions(+), 46 deletions(-) diff --git a/M02_MasterTemplete/CostFlow/M02_CF_InputPanel.ts b/M02_MasterTemplete/CostFlow/M02_CF_InputPanel.ts index f6d357350..168a14005 100644 --- a/M02_MasterTemplete/CostFlow/M02_CF_InputPanel.ts +++ b/M02_MasterTemplete/CostFlow/M02_CF_InputPanel.ts @@ -20,11 +20,12 @@ import { fetchSubs, type SubInfo, } from "../../M01_MasterData/M01_MasterData_Api_Fetch"; -import { cfPlain, cfUserElementAdd, cfUserElements, cfValues, type CfQuery } from "./M02_CF_Api"; -import type { CfBox, CfGroup, CfLogic } from "./M02_CF_Model"; +import { cfPlain, cfUserElementAdd, cfUserElements, cfValues } from "./M02_CF_Api"; +import type { CfBox, CfElementRef, CfGroup, CfLogic } from "./M02_CF_Model"; import { GROUPS, M01_OF, + MACHINE_SEP, USER_PREFIX, addElement, elementOf, @@ -39,6 +40,24 @@ import { renderTablePick } from "./M02_CF_InputPanel_Table"; import { renderVariables } from "./M02_CF_InputPanel_Vars"; import "./M02_CF_InputPanel.css"; +/** Api 모양 — 서버 Api 가 채워지면(사용자 요소 = Model 모양 · 값 물음 = ref) 그 모양을 그대로 씀 */ +interface Answer { + id: string; + ok: boolean; + value?: number | string | null; +} +interface UserEl { + id: string; + group: string; + name: string; + spec: string; + unit: string; + value: number | null; +} +const apiValues = cfValues as unknown as (items: object[], users?: object[]) => Promise; +const apiUsers = cfUserElements as unknown as () => Promise; +const apiAdd = cfUserElementAdd as unknown as (e: Omit) => Promise; + const TEXT = { Kind: ["무엇을 넣을까요?", "What goes in?"], Variable: ["변수", "Variable"], @@ -202,18 +221,47 @@ function drawChosen( : [el("p", { className: "m02-cfip__muted", text: t("NoneChosen") })]), ); if (!els.length) return; - const items = els.map((e) => ({ id: e.ref, ...queryOf(group, e.ref) }) as unknown as CfQuery); - void cfValues(items).then( + const items = els.map((e) => ({ id: e.ref, ...queryOf(group, e.ref) })); + void apiValues(items, lg.userElements).then( (answers) => answers.forEach((a) => { const node = values.get(a.id); if (node && a.ok && a.value !== null && a.value !== undefined) node.textContent = typeof a.value === "number" ? fmt(a.value) : String(a.value); }), - () => undefined, // 값을 못 받아도 요소는 넣을 수 있음 — 값 칸만 「—」 + // Api 가 아직 값을 못 주면 M01 줄에서 바로 읽어 보임(값 칸만 — 요소는 그대로) + () => + els.forEach( + (e) => + void m01Value(group, e).then((v) => { + const node = values.get(e.ref); + if (node && v !== null) node.textContent = fmt(v); + }), + ), ); } +/** 요소 한 개의 값을 M01 줄에서 읽음 — 이름으로 찾아 키가 같은 줄 · 못 찾으면 null */ +async function m01Value(group: CfGroup, e: CfElementRef): Promise { + if (e.ref.startsWith(USER_PREFIX)) return null; + try { + const [key, field] = e.ref.split(MACHINE_SEP); + const page = await fetchRows( + await fileOf(group), + 1, + 30, + e.name.split(" · ")[0].split(" ")[0], + false, + "", + "", + ); + const row = (page.rows as M01Row[]).find((r) => r["키"] === key); + return row ? valueOf(group, row, field || "가격") : null; + } catch { + return null; + } +} + /** M01 줄 찾기 — 구분 · 상세구분 · 이름 · 내가 만든 것도 함께 */ async function drawFind( host: HTMLElement, @@ -259,21 +307,20 @@ async function drawFind( const word = q.input.value.trim(); const [page, users] = await Promise.all([ fetchRows(file, 1, 40, word, false, sub, detail), - cfUserElements() - .then((u) => - u.filter((x) => (x.group as string) === group || (x.group as string) === "사용자"), - ) - .catch(() => []), + apiUsers().catch((): UserEl[] => []), ]); if (mine !== seq) return; - const mineRows: M01Row[] = users - .filter((u) => !word || `${u.이름} ${u.규격}`.includes(word)) + const known = new Map(); + for (const u of [...lg.userElements, ...users]) + if (u.group === group) known.set(u.id, u as UserEl); + const mineRows: M01Row[] = [...known.values()] + .filter((u) => !word || `${u.name} ${u.spec}`.includes(word)) .map((u) => ({ - 키: u.key, - 이름: u.이름, - 규격: u.규격, - 단위: u.단위, - 값: u.값, + 키: u.id, + 이름: u.name, + 규격: u.spec, + 단위: u.unit, + 값: u.value, __mine: true, })); const rows = [...mineRows, ...(page.rows as M01Row[])]; @@ -368,34 +415,24 @@ function newForm( if (n !== null && !Number.isFinite(n)) return value.setError(t("NeedValue")); name.setError(undefined); value.setError(undefined); - cfUserElementAdd({ + apiAdd({ group, - 이름: name.input.value.trim(), - 규격: spec.input.value.trim(), - 단위: unit.input.value.trim(), - 값: n, + name: name.input.value.trim(), + spec: spec.input.value.trim(), + unit: unit.input.value.trim(), + value: n, }).then( (made) => { const el1 = elementOf(group, { - 키: made.key, - 이름: made.이름, - 규격: made.규격, - 단위: made.단위, - 값: made.값, + 키: made.id, + 이름: made.name, + 규격: made.spec, + 단위: made.unit, + 값: made.value, }); const withUser: CfLogic = { ...lg, - userElements: [ - ...lg.userElements, - { - id: made.key, - group, - name: made.이름, - spec: made.규격, - unit: made.단위, - value: made.값, - }, - ], + userElements: [...lg.userElements.filter((u) => u.id !== made.id), { ...made, group }], }; on.change(addElement(withUser, box.id, el1)); }, diff --git a/M02_MasterTemplete/CostFlow/M02_CF_InputPanel_Build.ts b/M02_MasterTemplete/CostFlow/M02_CF_InputPanel_Build.ts index d811a65ef..ce2f0799b 100644 --- a/M02_MasterTemplete/CostFlow/M02_CF_InputPanel_Build.ts +++ b/M02_MasterTemplete/CostFlow/M02_CF_InputPanel_Build.ts @@ -80,7 +80,9 @@ export function elementOf(group: CfGroup, row: M01Row, field = "가격"): CfElem spec, unit: text(row["단위"]) || "-", }; - const named = [text(row["이름"]), spec].filter(Boolean).join(" "); // 알약 이름 = 이름 + 규격(규격만 다른 요소가 부딪히지 않게) + // 환율 · 유가전력의 규격은 날짜라 알약 이름에 안 넣음(날짜가 바뀌어도 이름이 같게) — 유가전력은 상세구분(지역)으로 가름 + const tail = group === "환율" ? "" : group === "유가전력" ? text(row["상세구분"]) : spec; + const named = [text(row["이름"]), tail].filter(Boolean).join(" "); // 알약 이름 = 이름 + 규격(규격만 다른 요소가 부딪히지 않게) if (group === "기계") return { ref: `${key}${MACHINE_SEP}${field}`, @@ -108,12 +110,11 @@ export function pillNames(box: CfBox): string[] { /** 입력 값 물음 — Api `cfValues` 한 줄(id 는 부르는 쪽이 정함) */ export function queryOf(group: CfGroup, ref: string): Record { - if (ref.startsWith(USER_PREFIX)) return { group: "사용자", key: ref }; - if (group === "기계") { + if (group === "기계" && !ref.startsWith(USER_PREFIX)) { const [key, field] = ref.split(MACHINE_SEP); - return { group, key, field: field || "가격" }; + return { group, ref: key, field: field || "가격" }; } - return { group, key: ref }; + return { group, ref }; } /** 상자 단위 — 모두 같으면 그 단위 · 여럿이거나 없으면 「-」 */ diff --git a/resources/tester/test_m02_cf_inputpanel_build.py b/resources/tester/test_m02_cf_inputpanel_build.py index 646ec4bd5..890b0639d 100644 --- a/resources/tester/test_m02_cf_inputpanel_build.py +++ b/resources/tester/test_m02_cf_inputpanel_build.py @@ -113,7 +113,7 @@ def test_기계는_값_칸을_골라_요소가_됨(): assert got["name"].endswith("· 손료계수") assert call("queryOf", "기계", "EQ000001#손료계수") == { "group": "기계", - "key": "EQ000001", + "ref": "EQ000001", "field": "손료계수", } @@ -127,8 +127,9 @@ def test_값_읽기_재료는_낮은_값_요율은_비율(): assert call("valueOf", "기계", MACHINE, "가격") == 76547 -def test_사용자_요소는_사용자_그룹으로_물음(): - assert call("queryOf", "인력", "UE-0001") == {"group": "사용자", "key": "UE-0001"} +def test_사용자_요소는_그_그룹의_ref_로_물음(): + assert call("queryOf", "인력", "UE-0001") == {"group": "인력", "ref": "UE-0001"} + assert call("queryOf", "재료", "MT1") == {"group": "재료", "ref": "MT1"} row = {"키": "UE-0001", "이름": "특수공", "규격": "", "단위": "원/일", "값": 300000} assert call("elementOf", "인력", row)["unit"] == "원/일" assert call("elementOf", "요율", {**row, "단위": "%"})["unit"] == "%" @@ -204,3 +205,17 @@ def test_변수_검사는_쉬운_말(): assert call("checkVariable", many, []) == "선택 목록은 둘 이상" bad = {**ok, "type": "choice", "choices": ["가", "나"], "example": "다"} assert call("checkVariable", bad, []) == "예시값이 선택 목록에 없음" + + +def test_환율_유가전력_알약_이름에_날짜가_안_들어감(): + fx = {"키": "FX1", "이름": "USD", "규격": "2026-08-14", "단위": "원/USD", "값": 1414.9} + assert call("elementOf", "환율", fx)["name"] == "USD" + oil = { + "키": "MO1", + "이름": "휘발유", + "규격": "2026-08-14", + "상세구분": "서울", + "단위": "원/L", + "값": 1900, + } + assert call("elementOf", "유가전력", oil)["name"] == "휘발유 서울"