feat(804-10): 옵션 드롭다운 기계 · 집게 원문번호 = 「이름 규격 (원문번호)」 글(요구 692-2)

- 값(원문번호)은 그대로 · 글만 바꿈 — 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 <noreply@anthropic.com>
This commit is contained in:
2026-11-08 15:39:58 +09:00
co-authored by Claude Opus 5.5
parent af67973884
commit 324ae1b047
5 changed files with 107 additions and 9 deletions
+8 -6
View File
@@ -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.이름);
+22 -1
View File
@@ -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<string, string> = {};
/** 고르기 글 — 기계 원문번호는 이름 · 규격을 앞에 */
export const optionText = (x: string, known: Record<string, string> = 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({
+11 -1
View File
@@ -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<CfStandards> {
if (!response.ok) throw new Error(`표준 표 읽기 실패 (${response.status})`);
return (await response.json()) as CfStandards;
}
let machineNames: Promise<Record<string, string>> | null = null;
/** 기계 원문번호 → 「이름 규격」 — 한 번 받아 둠(못 받으면 빈 표 · 다음에 다시) */
export function cfMachineNames(): Promise<Record<string, string>> {
machineNames ??= fetch(`${API_BASE_URL}/m02/df/machine-names`, { credentials: "include" })
.then((r) => (r.ok ? (r.json() as Promise<Record<string, string>>) : Promise.reject()))
.catch(() => ((machineNames = null), {}));
return machineNames;
}
+15 -1
View File
@@ -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:
@@ -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