From 39c0d163ffa7dc5c4079c13f2cfa5356fa5b8bdb Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 20:01:27 +0900 Subject: [PATCH 1/3] =?UTF-8?q?fix(m02-df):=20=ED=91=9C=20=EB=B2=94?= =?UTF-8?q?=EC=9C=84=20=EC=B9=B8=EC=97=90=20=EC=88=AB=EC=9E=90=20=ED=95=98?= =?UTF-8?q?=EB=82=98(=EC=95=84=EB=9E=98=20=3D=20=EC=9C=84)=EB=A9=B4=20?= =?UTF-8?q?=E3=80=8C=EA=B0=99=EC=9D=8C=E3=80=8D=20=E2=80=94=20=EB=B8=8C?= =?UTF-8?q?=EB=A0=88=EC=9D=B8=20=EC=A0=95=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 아래 ≠ 위는 지금대로(아래 < 값 ≤ 위 · 빈 끝 열림) · 견치돌 「높이 4」 같은 줄이 맞음 - 시험 — 저장 다시 셈 4 → 0.7 줄 · apply_rule 4 / 3.5 / 3 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KmELpdXFDRZHhfbGuRhSmL --- M02_MasterTemplete/DataFlow/M02_DF_Compute.py | 4 +++- resources/tester/test_m02_df_router.py | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Compute.py b/M02_MasterTemplete/DataFlow/M02_DF_Compute.py index c50191906..cc7d9c1ed 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Compute.py +++ b/M02_MasterTemplete/DataFlow/M02_DF_Compute.py @@ -314,7 +314,7 @@ def _rounded(value: Any, r: dict) -> Any: def _lookup(rule: dict, block: Any, conds: dict) -> tuple[Any, str | None]: - """표에서 찾기 — 같음 · 범위(「이름·아래」 < 값 ≤ 「이름·위」 · 빈 끝 열림) · 맞은 줄 하나만.""" + """표에서 찾기 — 같음 · 범위(아래 < 값 ≤ 위 · 빈 끝 열림 · 아래 = 위면 같음) · 맞은 줄 하나.""" if not isinstance(block, list) or len(block) < 2: return None, "오류" head = [str(h if h is not None else "") for h in block[0]] @@ -336,6 +336,8 @@ def _lookup(rule: dict, block: Any, conds: dict) -> tuple[Any, str | None]: def fits(row, lo=lo, hi=hi, x=x) -> bool: low, high = _num_of(row[lo]), _num_of(row[hi]) + if low is not None and low == high: # 숫자 하나(아래 = 위) = 같음(브레인 정함) + return x is not None and x == low return ( x is not None and (row[lo] is None or (low is not None and low < x)) diff --git a/resources/tester/test_m02_df_router.py b/resources/tester/test_m02_df_router.py index 833a40096..4af744437 100644 --- a/resources/tester/test_m02_df_router.py +++ b/resources/tester/test_m02_df_router.py @@ -547,6 +547,24 @@ def test_선_규칙_끝수_재료_조건_설계_때_정할_것(client: TestClien assert _d( _post(client, "/api/m02/df/save", {"logic": lg})["values"]["ports"]["f"]["합"] ) == int(Decimal("0.5") + 33000 + 3) + lg["boxes"][0]["input"]["variables"][0]["example"] = ( + "견치돌" # 「높이 4」 줄 = 아래 = 위 → 같음(정함) + ) + lg["boxes"][0]["input"]["variables"][1]["example"] = 4 + assert _d( + _post(client, "/api/m02/df/save", {"logic": lg})["values"]["ports"]["f"]["합"] + ) == int(Decimal("0.7") + 33000 + 4) + lookup = { + "kind": "lookup", + "col": "석공", + "conds": [{"col": "높이", "from": {"box": "v", "name": "높이"}}], + } + block = [["높이·아래", "높이·위", "석공"], [4, 4, 0.7], [None, 3, 0.5]] + assert [cf.dc.apply_rule(lookup, block, {"높이": h})[0] for h in (4, 3.5, 3)] == [ + 0.7, + None, + 0.5, + ] lg["boxes"][0]["input"]["variables"][2]["example"] = "D999" # 맞는 재료 없음 lg["boxes"][0]["input"]["variables"][0]["example"] = "없는돌" # 표에 없는 조건 → 함수 멈춤 bad = _post(client, "/api/m02/df/save", {"logic": lg})["values"] From 0158c940cbc8296e68c43c48d391d24588bb5be1 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 20:01:40 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix(M02-=ED=9D=90=EB=A6=84=EB=8F=84):=20Uni?= =?UTF-8?q?tPick=20=ED=91=9C=EC=A4=80=20=ED=91=9C=EB=A5=BC=20=EC=84=9C?= =?UTF-8?q?=EB=B2=84=20/standards=20=EB=A1=9C=20=EC=9D=BD=EC=9D=8C=20-=20?= =?UTF-8?q?=EB=B2=88=EB=93=A4=20=EB=8C=80=EC=8B=A0=20=EA=B8=B8=20=EC=A7=80?= =?UTF-8?q?=EC=9B=80?= 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_01D2nUMUMTsqm93PbQtipo6c --- .../DataFlow/M02_DF_UnitPick.ts | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/M02_MasterTemplete/DataFlow/M02_DF_UnitPick.ts b/M02_MasterTemplete/DataFlow/M02_DF_UnitPick.ts index aabe898b4..2883c6835 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_UnitPick.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_UnitPick.ts @@ -2,12 +2,13 @@ * M02_DF_UnitPick.ts * 단가산출 흐름도 공용 단위 고르기 — 치면 걸러진 목록 · 고르기만(손글 안 받음) · 「-」 = 없음 * 목록 = 단위 표준(산림품셈 1-2-2) 단위 + 부른 쪽이 주는 마스터 결과단위 등 + 원 - * 표준 표 읽는 길은 `setUnitSource` 로 바꿔 끼움(기본 = 처음 값 `_standard/단위표준.json`) — 다른 판도 이것을 씀 + * 표준 표 읽는 길은 `setUnitSource` 로 바꿔 끼움(기본 = 서버 /standards) — 다른 판도 이것을 씀 * 글 [한, 영] 짝은 아래 TEXT * ========================================================================== */ import { currentLanguageIndex } from "@ui/ui_template_locale"; import { createButton, el } from "@ui/ui_template_elements"; +import { cfStandards } from "./M02_DF_Api"; import "./M02_DF_UnitPick.css"; const TEXT = { @@ -36,9 +37,9 @@ interface StandardRow { "단위수량 소수자리"?: number | null; } -/** 처음 값 — 번들에 든 표준 표(단위표준.json) */ -const bundled: UnitSource = async () => { - const table = (await import("./_standard/단위표준.json")).default as { 줄: StandardRow[] }; +/** 서버 표준 표(/standards) — 고친 표가 있으면 그것, 없으면 처음 값 */ +const fromServer: UnitSource = async () => { + const table = (await cfStandards()).unit as { 줄: StandardRow[] }; const units = table.줄 .flatMap((r) => [r["규격 단위"], r["단위수량 단위"]]) .filter((u): u is string => !!u); @@ -52,13 +53,13 @@ const bundled: UnitSource = async () => { const places = Object.fromEntries( Object.entries(count).map(([u, c]) => [ u, - Number(Object.entries(c).sort((a, b) => b[1] - a[1])[0][0]), + Number(Object.entries(c).sort((x, y) => y[1] - x[1])[0][0]), ]), ); return { units, places }; }; -let source: UnitSource = bundled; +let source: UnitSource = fromServer; let cache: Promise | null = null; /** 표준 표 읽는 길 바꾸기(서버에서 고친 표를 읽을 때) — 목록을 다시 받음 */ @@ -67,15 +68,20 @@ export function setUnitSource(next: UnitSource): void { cache = null; } -/** 표준 단위 + 원 — 같은 글은 한 번 · 「없음」 은 뺌 */ -export function loadStandard(): Promise { +/** 표준 단위 + 원 — 같은 글은 한 번 · 「없음」 은 뺌 · 서버를 못 읽으면 원만(다음에 다시 읽음) */ +export async function loadStandard(): Promise { cache ??= source().then((std) => { const set = new Set([...std.units, "원"]); set.delete("없음"); set.delete(NO_UNIT); return { units: [...set], places: std.places }; }); - return cache; + try { + return await cache; + } catch { + cache = null; + return { units: ["원"], places: {} }; + } } /** 단위를 고르면 표준 표에서 소수자리 제안 — 없으면 null */ From 67e631ac8c7f1fb134ed0e4d20a74c7bc8e96157 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 20:03:12 +0900 Subject: [PATCH 3/3] =?UTF-8?q?refactor(m02-df):=20=EC=9E=85=EB=A0=A5=20?= =?UTF-8?q?=ED=8C=90=20=EC=9E=90=EC=9E=AC=20=EC=B0=BE=EA=B8=B0=EB=A5=BC=20?= =?UTF-8?q?=EC=84=9C=EB=B2=84=20=EA=B8=B8(cfMaterials)=EB=A1=9C=20?= =?UTF-8?q?=E2=80=94=20=EB=8C=80=EC=8B=A0=20=EA=B8=B8=20=EC=A7=80=EC=9B=80?= 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 --- .../DataFlow/M02_DF_InputPanel.ts | 36 +------------- .../DataFlow/M02_DF_InputPanel_Material.ts | 48 ++----------------- 2 files changed, 6 insertions(+), 78 deletions(-) diff --git a/M02_MasterTemplete/DataFlow/M02_DF_InputPanel.ts b/M02_MasterTemplete/DataFlow/M02_DF_InputPanel.ts index 3fb3a69c5..a6a67a002 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_InputPanel.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_InputPanel.ts @@ -21,12 +21,10 @@ import { type SubInfo, } from "../../M01_MasterData/M01_MasterData_Api_Fetch"; import { cfPlain, cfUserElementAdd, cfUserElements, cfValues } from "./M02_DF_Api"; -import type { CfBox, CfElementRef, CfGroup, CfLogic } from "./M02_DF_Model"; +import type { CfBox, CfGroup, CfLogic } from "./M02_DF_Model"; import { GROUPS, M01_OF, - MACHINE_SEP, - USER_PREFIX, addElement, elementOf, machineFields, @@ -243,39 +241,10 @@ function drawChosen( if (node && a.ok && a.value !== null && a.value !== undefined) node.textContent = typeof a.value === "number" ? fmt(a.value) : String(a.value); }), - // 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); - }), - ), + () => undefined, // 값을 못 받아도 요소는 넣을 수 있음 — 값 칸만 「—」 ); } -/** 요소 한 개의 값을 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, @@ -406,7 +375,6 @@ async function drawFind( list, ); void load(); - void USER_PREFIX; } /** 새 요소 — M01 표 「추가」 화면 꼴(이름 · 규격 · 단위 · 값) · 마스터에 안 쓰고 사용자 요소로 */ diff --git a/M02_MasterTemplete/DataFlow/M02_DF_InputPanel_Material.ts b/M02_MasterTemplete/DataFlow/M02_DF_InputPanel_Material.ts index 0d2e45136..80a288717 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_InputPanel_Material.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_InputPanel_Material.ts @@ -1,11 +1,10 @@ /* ============================================================================= * M02_DF_InputPanel_Material.ts * 입력 판 — 재료 요소 방식 셋(고정 · 선택 자리 · 조건 찾기) · 자재 찾기(띄어쓰기 AND · 규격까지 · 쪽 넘기기 · 전체 수) - * 서버 `/api/m02/df/materials` 를 먼저 부르고 아직 없으면 M01 `/pick?kind=price` 로 같은 모양을 만듦 + * 서버 `/api/m02/df/materials`(cfMaterials) * 글 [한, 영] 짝은 아래 TEXT · 단추는 공용 작은 크기 * ========================================================================== */ -import { API_BASE_URL } from "@config/config_frontend"; import { currentLanguageIndex } from "@ui/ui_template_locale"; import { createButton, @@ -15,7 +14,7 @@ import { showToast, } from "@ui/ui_template_elements"; import { fetchFiles, fetchSubs, type SubInfo } from "../../M01_MasterData/M01_MasterData_Api_Fetch"; -import { cfValues } from "./M02_DF_Api"; +import { cfMaterials, cfValues } from "./M02_DF_Api"; import type { CfElementRef, CfLogic, CfMaterialMode, CfMaterialRule } from "./M02_DF_Model"; import { catchNote, @@ -101,48 +100,9 @@ export interface MatPage { items: MatItem[]; } -async function getJson(path: string): Promise { - const res = await fetch(`${API_BASE_URL}${path}`, { credentials: "include" }); - if (!res.ok) throw new Error(String(res.status)); - return (await res.json()) as T; -} - -/** 서버가 없으면 M01 값 줄에서 같은 모양으로 — 띄어쓰기 AND(이름 + 규격) · 쪽은 앞에서 잘라 줌 */ -async function fallback(q: MatQuery): Promise { - const r = await getJson<{ items: Record[] }>( - `/m01/pick?${new URLSearchParams({ kind: "price", q: q.q, limit: "200" })}`, - ); - const rows = r.items.filter((i) => !q.sub || i["구분"] === q.sub); - const items = rows.map((i): MatItem => ({ - ref: String(i["ref"]), - name: String(i["이름"] ?? ""), - spec: String(i["규격"] ?? ""), - unit: String(i["단위"] ?? ""), - price: typeof i["값"] === "number" ? i["값"] : null, - 구분: String(i["구분"] ?? ""), - })); - const from = (q.page - 1) * q.size; - return { - total: items.length, - page: q.page, - size: q.size, - items: items.slice(from, from + q.size), - }; -} - +/** 서버 자재 찾기 — 띄어쓰기로 나눈 말을 모두 포함 · 쪽 넘기기 · 전체 수 */ export async function searchMaterials(q: MatQuery): Promise { - const params = new URLSearchParams({ - q: q.q, - page: String(q.page), - size: String(q.size), - sub: q.sub ?? "", - detail: q.detail ?? "", - }); - try { - return await getJson(`/m02/df/materials?${params}`); - } catch { - return fallback(q); - } + return cfMaterials({ q: q.q, sub: q.sub, detail: q.detail, page: q.page, size: q.size }); } let subsCache: SubInfo[] | null = null;