From 2d459f7b13cb95437ab5da7b916fdf45bf1840fa Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 20:01:08 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat(m02-df):=20=ED=91=9C=20=EB=B2=94?= =?UTF-8?q?=EC=9C=84=20=EC=B9=B8=20=EC=88=AB=EC=9E=90=20=ED=95=98=EB=82=98?= =?UTF-8?q?(=EC=95=84=EB=9E=98=20=3D=20=EC=9C=84)=EB=8A=94=20=EA=B0=99?= =?UTF-8?q?=EC=9D=8C=EC=9C=BC=EB=A1=9C=20=EB=A7=9E=EC=B6=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 표에서 찾기 범위 칸: 아래 = 위면 값 == 그 수 · 다르면 아래 < 값 <= 위(빈 끝 열림) — 견치돌 높이 4 같은 줄 - 시험 24개 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VBToTCGk5dqDKeXCEP3vhk --- M02_MasterTemplete/DataFlow/M02_DF_Rule.ts | 6 ++++-- resources/tester/test_m02_df_model.py | 11 +++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Rule.ts b/M02_MasterTemplete/DataFlow/M02_DF_Rule.ts index 99b13b959..adf25c2fa 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Rule.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Rule.ts @@ -3,7 +3,8 @@ * 선 규칙 · 끝수 → 엑셀 수식 — 우리 스프레드시트 엔진 한 벌로 셈 · 선 위 규칙 글(쉬운 말) · DOM 없음 * * 한 번 셈 = 시트 하나: A1 부터 보낸 값(표면 머리 줄 포함 덩어리) · 그 오른쪽에 조건 값 · 맨 끝 칸에 규칙 수식 - * lookup = 줄마다 「맞음」 칸(같음 · 범위는 「이름·아래」 < 값 ≤ 「이름·위」 · 빈 끝은 열림) → 맞은 줄 수 · INDEX/MATCH + * lookup = 줄마다 「맞음」 칸(같음 · 범위는 「이름·아래」 < 값 ≤ 「이름·위」 · 빈 끝은 열림 · 아래 = 위면 값 == 그 수) + * → 맞은 줄 수 · INDEX/MATCH * gate = IF(조건, 보낸 값, NA()) · scale = × ÷ (1 ± %) · round = ROUND · ROUNDUP · ROUNDDOWN * ========================================================================== */ @@ -177,7 +178,8 @@ function lookup( else if (low && high) tests.push( (r) => - `AND(OR(ISBLANK(${low}${r}),${low}${r}<${ref}),OR(ISBLANK(${high}${r}),${ref}<=${high}${r}))`, + `IF(AND(NOT(ISBLANK(${low}${r})),${low}${r}=${high}${r}),${ref}=${low}${r},` + + `AND(OR(ISBLANK(${low}${r}),${low}${r}<${ref}),OR(ISBLANK(${high}${r}),${ref}<=${high}${r})))`, ); else bad = true; }); diff --git a/resources/tester/test_m02_df_model.py b/resources/tester/test_m02_df_model.py index 56bdf7c3d..449a25a04 100644 --- a/resources/tester/test_m02_df_model.py +++ b/resources/tester/test_m02_df_model.py @@ -609,3 +609,14 @@ def test_다_다른_로직_받는_알약_예시값_차례() -> None: assert (a, b, c) == (70000, 14000, 21000) assert designs == [["변수", "길이", 7], ["다른 로직", "계수", 10]] assert plain == [7, None] + + +def test_표_범위칸_숫자하나는_같음() -> None: + got = _run( + RULE + + "const T = [['높이·아래', '높이·위', '석공'], [4, 4, 0.5], [4, 6, 0.6], [null, 3, 0.4]];\n" + "const rule = { kind: 'lookup', conds: [{ col: '높이', from: { box: 'b1', name: '높이' } }], col: '석공' };\n" + "out([4, 5, 2, 3.5, 6].map(h => { const r = R.applyRule(rule, T, { 높이: h }); return [r.value, r.why]; }));" + ) + # 4 = 「4」 줄만(같음) · 4 는 「4~6」(초과) 에 안 듦 · 빈 아래 끝은 열림 + assert got == [[0.5, None], [0.6, None], [0.4, None], [None, "표에 없는 조건"], [0.6, None]] From 1fc2885f76c4dfa746321bf52516d295be796bed Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 20:01:23 +0900 Subject: [PATCH 2/2] =?UTF-8?q?refactor(m02-df):=20=EC=9E=AC=EB=A3=8C=20?= =?UTF-8?q?=EC=B0=BE=EA=B8=B0=20=C2=B7=20=EB=8B=A8=EA=B0=80=20=C2=B7=20?= =?UTF-8?q?=EA=B8=88=EC=95=A1=20=ED=91=9C=EC=A4=80=EC=9D=84=20=EC=84=9C?= =?UTF-8?q?=EB=B2=84=20=EA=B8=B8(cfMaterials=20=C2=B7=20cfStandards)?= =?UTF-8?q?=EB=A1=9C=20=EB=B0=94=EA=BE=B8=EA=B3=A0=20=EB=8C=80=EC=8B=A0=20?= =?UTF-8?q?=EA=B8=B8=EC=9D=84=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_01EauA2ATmqNRbeV48s9CcBr --- .../DataFlow/M02_DF_Material.ts | 96 +++---------------- M02_MasterTemplete/DataFlow/M02_DF_Round.ts | 31 ++---- 2 files changed, 22 insertions(+), 105 deletions(-) diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Material.ts b/M02_MasterTemplete/DataFlow/M02_DF_Material.ts index 01e1083b2..27204e11a 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Material.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Material.ts @@ -1,20 +1,13 @@ /* ============================================================================= * M02_DF_Material.ts - * 검증 칸이 쓰는 재료 찾기 · 이름 · 단가 — 서버 `GET /api/m02/df/materials?q&sub&detail&page&size` - * → {total, page, size, items}(아직 없으면 M01 재료 찾기로 대신) · 단가는 `cfValues`(재료 ref = 키) + * 검증 칸이 쓰는 재료 찾기 · 이름 · 단가 — 서버 `GET /api/m02/df/materials`(Api `cfMaterials` · + * 띄어쓰기로 나눈 말을 모두 포함 · 쪽 넘기기) · 단가 = 그 줄의 단가(값을 받는 길과 같은 셈) * 모양은 docs/raw/M01_쉬운단가/흐름도_정함.md 5. 이 폴더째 지워도 됨. * ========================================================================== */ -import { API_BASE_URL } from "@config/config_frontend"; -import { searchElements } from "../../M01_MasterData/M01_MasterData_UI_Logic_Api"; -import { cfValues } from "./M02_DF_Api"; +import { cfMaterials, type CfMaterial } from "./M02_DF_Api"; -export interface CfMat { - ref: string; - name: string; - spec: string; - unit: string; -} +export type CfMat = CfMaterial; export interface CfMatPage { total: number; @@ -23,99 +16,34 @@ export interface CfMatPage { items: CfMat[]; } -const text = (v: unknown): string => - typeof v === "string" || typeof v === "number" ? String(v) : ""; - -const norm = (it: Record): CfMat => ({ - ref: text(it.ref ?? it.키), - name: text(it.이름 ?? it.name), - spec: text(it.규격 ?? it.spec), - unit: text(it.단위 ?? it.unit), -}); - /** 재료 찾기 — q = 찾는 말 · sub = 구분 · detail = 상세구분 · page 는 1 부터 */ -export async function findMaterials(cond: { +export function findMaterials(cond: { q?: string; sub?: string; detail?: string; page?: number; size?: number; }): Promise { - const page = cond.page ?? 1; - const size = cond.size ?? 10; - try { - const qs = new URLSearchParams({ - q: cond.q ?? "", - sub: cond.sub ?? "", - detail: cond.detail ?? "", - page: String(page), - size: String(size), - }); - const r = await fetch(`${API_BASE_URL}/m02/df/materials?${qs.toString()}`, { - credentials: "include", - }); - if (r.ok) { - const d = (await r.json()) as { - total: number; - page?: number; - size?: number; - items: Record[]; - }; - return { - total: d.total, - page: d.page ?? page, - size: d.size ?? size, - items: d.items.map(norm), - }; - } - } catch { - /* 서버 길이 아직 없으면 M01 재료 찾기로 */ - } - const got = await searchElements("재료", cond.q ?? ""); - const all = got.items.filter((it) => { - const x = it as unknown as Record; - return (!cond.sub || x.구분 === cond.sub) && (!cond.detail || x.상세구분 === cond.detail); - }); - const from = (page - 1) * size; - return { - total: all.length, - page, - size, - items: all.slice(from, from + size).map((it) => norm(it as unknown as Record)), - }; + return cfMaterials({ ...cond, page: cond.page ?? 1, size: cond.size ?? 10 }); } -const names = new Map(); +const byKey = new Map(); -/** 재료 키 → 이름 · 규격 (한 번 물은 것은 다시 안 물음) */ +/** 재료 키 → 그 줄(이름 · 규격 · 단가) — 한 번 물은 것은 다시 안 물음 */ export async function materialOf(key: string): Promise { if (!key) return null; - if (names.has(key)) return names.get(key) ?? null; + if (byKey.has(key)) return byKey.get(key) ?? null; let hit: CfMat | null = null; try { - const got = await searchElements("재료", key); - const it = got.items.find((x) => x.ref === key); - hit = it ? norm(it as unknown as Record) : null; + hit = (await cfMaterials({ q: key, size: 20 })).items.find((m) => m.ref === key) ?? null; } catch { hit = null; } - names.set(key, hit); + byKey.set(key, hit); return hit; } -const prices = new Map(); - /** 재료 키 → 단가(원) — 못 구하면 null */ export async function priceOf(key: string): Promise { - if (!key) return null; - if (prices.has(key)) return prices.get(key) ?? null; - let v: number | null = null; - try { - const a = (await cfValues([{ id: "p", group: "재료", ref: key }]))[0]; - v = a?.ok && typeof a.value === "number" ? a.value : null; - } catch { - v = null; - } - prices.set(key, v); - return v; + return (await materialOf(key))?.price ?? null; } diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Round.ts b/M02_MasterTemplete/DataFlow/M02_DF_Round.ts index b4976a9d7..3d1a54d90 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Round.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Round.ts @@ -2,11 +2,11 @@ * M02_DF_Round.ts * 끝수 기본값 — 금액 단위 표준(산림품셈 1-2-2 「금액의 단위표준」)을 읽어 * 출력(비용) 기본 = 일위대가표 금액란 · 원가 합계 기본 = 일위대가표 계금 (모두 미만 버림) - * 표 읽기 = 서버 `GET /api/m02/df/standards` → {unit, money, saved} · 아직 없으면 `_standard/금액단위표준.json` + * 표 읽기 = 서버 `GET /api/m02/df/standards`(Api `cfStandards`) → {unit, money, saved} * 모양은 docs/raw/M01_쉬운단가/흐름도_정함.md 11 · 12. 이 폴더째 지워도 됨. * ========================================================================== */ -import { API_BASE_URL } from "@config/config_frontend"; +import { cfStandards } from "./M02_DF_Api"; import type { CfRound } from "./M02_DF_Model"; /** 표 한 줄 — 원문 그대로(종목 · 단위 · 지위 · 비고) */ @@ -17,11 +17,6 @@ export interface CfMoneyRow { 비고?: string; } -const FILES = import.meta.glob<{ 줄?: CfMoneyRow[] }>("./_standard/금액단위표준.json", { - eager: true, - import: "default", -}); - /** 표 모양 둘(`{줄: […]}` · 줄 목록)을 줄 목록으로 */ const rowsOf = (x: unknown): CfMoneyRow[] => { const list = Array.isArray(x) ? x : (x as { 줄?: unknown } | null)?.줄; @@ -30,21 +25,15 @@ const rowsOf = (x: unknown): CfMoneyRow[] => { let loaded: Promise | null = null; -/** 금액 단위 표준 줄 — 사용자가 고쳐 저장한 것이 있으면 그것 · 서버가 없으면 처음 값 */ +/** 금액 단위 표준 줄 — 서버가 고친 것(`_data`)이 있으면 그것 · 없으면 처음 값을 줌 */ export function loadMoneyStandard(): Promise { - loaded ??= (async () => { - try { - const r = await fetch(`${API_BASE_URL}/m02/df/standards`, { credentials: "include" }); - if (r.ok) { - const d = (await r.json()) as { money?: unknown; saved?: { money?: unknown } | null }; - const rows = rowsOf(d.saved?.money ?? d.money); - if (rows.length) return rows; - } - } catch { - /* 서버가 없으면 처음 값 */ - } - return rowsOf(Object.values(FILES)[0]); - })(); + loaded ??= cfStandards().then( + (s) => rowsOf(s.money), + () => { + loaded = null; // 못 읽었으면 다음에 다시 — 지금은 정해 둔 기본값으로 + return []; + }, + ); return loaded; }