From 324ae1b0479088c6c659d5d45272528c6973f7c9 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 8 Nov 2026 15:39:58 +0900 Subject: [PATCH] =?UTF-8?q?feat(804-10):=20=EC=98=B5=EC=85=98=20=EB=93=9C?= =?UTF-8?q?=EB=A1=AD=EB=8B=A4=EC=9A=B4=20=EA=B8=B0=EA=B3=84=20=C2=B7=20?= =?UTF-8?q?=EC=A7=91=EA=B2=8C=20=EC=9B=90=EB=AC=B8=EB=B2=88=ED=98=B8=20=3D?= =?UTF-8?q?=20=E3=80=8C=EC=9D=B4=EB=A6=84=20=EA=B7=9C=EA=B2=A9=20(?= =?UTF-8?q?=EC=9B=90=EB=AC=B8=EB=B2=88=ED=98=B8)=E3=80=8D=20=EA=B8=80(?= =?UTF-8?q?=EC=9A=94=EA=B5=AC=20692-2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 값(원문번호)은 그대로 · 글만 바꿈 — M02 사본 저장 · 로직 부름 · 엑셀 이음 그대로 - 서버 GET /api/m02/df/machine-names(로그인만) = 기계 원문번호 → 이름 규격 · 브라우저는 한 번 받아 둠 - 수식 탭 옵션(formula_doc_options) · 단가산출 상세 옵션(calc_sheet varRow) 같은 도우미 · M01 · M02 같이 - 목록이 모두 원문번호 꼴일 때만(빈 칸 뺌) · 문서 선택 목록 원문번호 전수 이름 있음 - test_804_10 셋 Co-Authored-By: Claude Opus 5.5 --- A00_Common/calc_sheet.ts | 14 ++--- A00_Common/formula_doc_options.ts | 23 ++++++++- M02_MasterTemplete/DataFlow/M02_DF_Api.ts | 12 ++++- M02_MasterTemplete/DataFlow/M02_DF_Router.py | 16 +++++- .../test_804_10_machine_option_names.py | 51 +++++++++++++++++++ 5 files changed, 107 insertions(+), 9 deletions(-) create mode 100644 resources/tester/test_804_10_machine_option_names.py diff --git a/A00_Common/calc_sheet.ts b/A00_Common/calc_sheet.ts index cb14d8581..5096b3401 100644 --- a/A00_Common/calc_sheet.ts +++ b/A00_Common/calc_sheet.ts @@ -17,6 +17,7 @@ import { createInputField, createSelectField } from "@ui/ui_template_input"; import { currentLanguageIndex } from "@ui/ui_template_locale"; import { createText } from "@ui/ui_template_text"; import { formatLineMoney, formatMoney } from "../common_util/common_util_money"; +import { nameMachines, optionText } from "./formula_doc_options"; import "./calc_sheet.css"; export interface SheetLine { @@ -162,16 +163,17 @@ const lineRow = (line: SheetLine, label = line.이름): HTMLElement => export function varRow(spec: SheetInput, value: string, onInput: (v: string) => void): HTMLElement { let control: HTMLElement; if (spec.고르기?.length) { - control = createSelectField({ - options: [...(value ? [] : [""]), ...spec.고르기.map(String)].map((o) => ({ - value: o, - text: o, - })), + const list = [...(value ? [] : [""]), ...spec.고르기.map(String)]; + const items = () => list.map((o) => ({ value: o, text: optionText(o) })); // 기계 번호 = 이름(요구 692-2) + const f = createSelectField({ + options: items(), value, compact: true, ariaLabel: spec.이름, onChange: onInput, - }).root; + }); + nameMachines(list, () => f.setOptions(items(), f.select.value)); + control = f.root; } else { const box = createInputField({ value, compact: true }); box.input.setAttribute("aria-label", spec.이름); diff --git a/A00_Common/formula_doc_options.ts b/A00_Common/formula_doc_options.ts index cdc35169f..a04ef7037 100644 --- a/A00_Common/formula_doc_options.ts +++ b/A00_Common/formula_doc_options.ts @@ -2,10 +2,29 @@ * formula_doc_options.ts * 수식 문서 옵션 가로(PLAN 802-3 · 802-4b-2 · 요구 655 「옵션 선택은 상단에 수평」) — 선택 · 입력 기호마다 칸 하나 * 값 = 화면 값(`given`) › 문서 값 · 고침은 부른 쪽 `set`(M01 = 화면 값만 · M02 사본 = 문서 값) · `lock` = 읽기만 + * 목록이 모두 기계 원문번호(`0101-0007`)면 글 = 「이름 규격 (원문번호)」(요구 692-2 · 값은 그대로 · 이름은 한 번 받아 둠) * ========================================================================== */ import { createInputField, createSelectField } from "@ui/ui_template_input"; import { varId, type DocVar, type FormulaDoc } from "../common_util/common_util_formula_doc"; +import { cfMachineNames } from "../M02_MasterTemplete/DataFlow/M02_DF_Api"; + +const MACHINE = /^\d{4}-\d{4}$/; +let names: Record = {}; + +/** 고르기 글 — 기계 원문번호는 이름 · 규격을 앞에 */ +export const optionText = (x: string, known: Record = names): string => + known[x] ? `${known[x]} (${x})` : x; + +/** 목록이 모두 원문번호(빈 칸 빼고)면 이름을 받아 `redraw`(글만 다시) — 수식 탭 옵션 · 단가산출 상세 옵션 */ +export function nameMachines(list: string[], redraw: () => void): void { + const codes = list.filter(Boolean); + if (codes.length && codes.every((x) => MACHINE.test(x)) && codes.some((x) => !(x in names))) + void cfMachineNames().then((got) => { + names = got; + redraw(); + }); +} export function buildOptions( doc: FormulaDoc | null, @@ -22,9 +41,10 @@ export function buildOptions( const now = k in given ? given[k] : v.연결?.값; if (kind === "선택") { const list = (v.연결?.목록 ?? []).map(String); + const items = () => list.map((x) => ({ value: x, text: optionText(x) })); const f = createSelectField({ label, - options: list.map((x) => ({ value: x, text: x })), + options: items(), value: String(now ?? ""), compact: true, size: 28, @@ -32,6 +52,7 @@ export function buildOptions( onChange: (x) => set(k, v, x), }); f.root.dataset.option = k; + nameMachines(list, () => f.setOptions(items(), f.select.value)); out.push(f.root); } else { const f = createInputField({ diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Api.ts b/M02_MasterTemplete/DataFlow/M02_DF_Api.ts index bdaec0f98..0319e83e8 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Api.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Api.ts @@ -1,7 +1,7 @@ /* ============================================================================= * M02_DF_Api.ts * 단위 · 금액 표준 표 읽기 — 서버 `GET /api/m02/df/standards`(`M02_DF_Router.py`) - * 부르는 곳 = 단위 고르기(`M02_DF_UnitPick`) · 끝수 표준(`M02_DF_Round`) + * 부르는 곳 = 단위 고르기(`M02_DF_UnitPick`) · 끝수 표준(`M02_DF_Round`) · 옵션 기계 이름(`formula_doc_options`) * ========================================================================== */ import { API_BASE_URL } from "@config/config_frontend"; @@ -18,3 +18,13 @@ export async function cfStandards(): Promise { if (!response.ok) throw new Error(`표준 표 읽기 실패 (${response.status})`); return (await response.json()) as CfStandards; } + +let machineNames: Promise> | null = null; + +/** 기계 원문번호 → 「이름 규격」 — 한 번 받아 둠(못 받으면 빈 표 · 다음에 다시) */ +export function cfMachineNames(): Promise> { + machineNames ??= fetch(`${API_BASE_URL}/m02/df/machine-names`, { credentials: "include" }) + .then((r) => (r.ok ? (r.json() as Promise>) : Promise.reject())) + .catch(() => ((machineNames = null), {})); + return machineNames; +} diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Router.py b/M02_MasterTemplete/DataFlow/M02_DF_Router.py index 6d293fa2c..f33abcc85 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Router.py +++ b/M02_MasterTemplete/DataFlow/M02_DF_Router.py @@ -1,4 +1,4 @@ -"""단위 · 금액 표준 표 읽기 — `GET /api/m02/df/standards` → {unit, money, saved}. +"""단위 · 금액 표준 표 읽기 — `GET /api/m02/df/standards` → {unit, money, saved} · 기계 이름 `GET /machine-names`. 부르는 곳 = 단위 고르기(`M02_DF_UnitPick.ts`) · 끝수 표준(`M02_DF_Round.ts`). 표 = `_standard/` 파일(git 이 따라감) · 권한 = 로그인만(읽기만 · `main.py` 한 줄 · M02 줄 쌓기 단위 고르기). @@ -19,6 +19,20 @@ router = APIRouter(prefix="/api/m02/df", tags=["M02 DataFlow"]) STANDARD = Path(__file__).resolve().parent / "_standard" +@router.get("/machine-names") +def get_machine_names() -> dict: + """기계 원문번호 → 「이름 규격」(요구 692-2) — 옵션 드롭다운 라벨만 · 값은 원문번호 그대로 · 겹친 번호는 첫 줄.""" + out: dict[str, str] = {} + for data in store.loaded()[0].values(): + if data.get("그룹") != "기계": + continue + for row in data.get(store.items_key(data)) or []: + code = str(row.get("원문번호") or "") + if code and code not in out: + out[code] = f"{row.get('이름') or ''} {row.get('규격') or ''}".strip() + return out + + @router.get("/standards") def get_standards() -> dict: try: diff --git a/resources/tester/test_804_10_machine_option_names.py b/resources/tester/test_804_10_machine_option_names.py new file mode 100644 index 000000000..23b60eede --- /dev/null +++ b/resources/tester/test_804_10_machine_option_names.py @@ -0,0 +1,51 @@ +"""804-10(요구 692-2) — 옵션 드롭다운 기계 · 집게 등 원문번호 목록 = 「이름 규격 (원문번호)」 글 · 값은 그대로. + +① 서버 `GET /api/m02/df/machine-names` = 기계 원문번호 → 「이름 규격」(로그인만) +② 수식 문서 선택 목록 중 모두 원문번호 꼴인 목록 — 번호마다 이름이 있음(1,518 전수) +③ 화면 부품 `formula_doc_options.ts` — 글만 바꿈(value = 원문번호) · 이름은 한 번 받아 둠 +""" + +from __future__ import annotations + +import json +import re +from pathlib import Path + +from M02_MasterTemplete.DataFlow import M02_DF_Router as df + +ROOT = Path(__file__).resolve().parents[2] +CODE = re.compile(r"^\d{4}-\d{4}$") + + +def test_기계_이름_표_문서_목록_전수() -> None: + names = df.get_machine_names() + assert names["0101-0007"] == "불도저(무한궤도) 7ton" + lists: list[list[str]] = [] + for f in (ROOT / "resources/master_data/수식문서").glob("*.json"): + for doc in json.loads(f.read_text(encoding="utf-8")).get("줄") or []: + for v in doc.get("변수") or []: + link = v.get("연결") or {} + got = [str(x) for x in link.get("목록") or []] + if link.get("갈래") == "선택" and got and all(CODE.match(x) for x in got): + lists.append(got) + assert len(lists) > 200 + missing = {x for got in lists for x in got if not names.get(x)} + assert not missing, sorted(missing)[:10] + + +def test_옵션_부품_글만_바꿈() -> None: + src = (ROOT / "A00_Common/formula_doc_options.ts").read_text(encoding="utf-8") + assert "known[x] ? `${known[x]} (${x})` : x" in src + assert "list.map((x) => ({ value: x, text: optionText(x) }))" in src + assert "nameMachines(list, () => f.setOptions(items(), f.select.value));" in src + api = (ROOT / "M02_MasterTemplete/DataFlow/M02_DF_Api.ts").read_text(encoding="utf-8") + assert "machineNames ??= fetch(" in api # 한 번만 받음 + main = (ROOT / "main.py").read_text(encoding="utf-8") + at = main.index("M02_MasterTemplete.DataFlow.M02_DF_Router") + assert "dependencies=protected," in main[at : at + 120] + + +def test_상세_탭_옵션_같은_도우미() -> None: + sheet = (ROOT / "A00_Common/calc_sheet.ts").read_text(encoding="utf-8") + assert "nameMachines(list, () => f.setOptions(items(), f.select.value));" in sheet + assert "text: optionText(o)" in sheet