From 0acffdc3411a0d75328c8a996b4250031acdbe42 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 09:47:26 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix(M01):=20=ED=91=9C=20=EB=B3=B4=EA=B3=A0?= =?UTF-8?q?=20=EA=B3=A0=EB=A5=B4=EA=B8=B0=20=E2=80=94=20=EC=84=A4=EA=B3=84?= =?UTF-8?q?=20=EB=95=8C=20=EC=A0=95=ED=95=98=EB=8A=94=20=EC=A1=B0=EA=B1=B4?= =?UTF-8?q?=20=EC=A4=84=20=EB=88=84=EB=A6=84=EC=97=90=20=EA=B3=A0=EC=A0=95?= =?UTF-8?q?=20=EC=95=88=EB=82=B4=20=ED=95=9C=20=EC=A4=84(34-3c)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 설계 값에 묶인 조건이 있는 줄을 누르면 「이 값은 설계 때 정함 — 늘 이 줄로 하려면 [이 값으로 고정]」 - 누름 한 번으로 그 조건들을 누른 줄 값으로 고정 · 안내 사라짐 - 줄 수 700 아래로 맞춤(표 머리 짜기 줄임) - 시험 6 통과 · typecheck 통과 · ORCA GF000219 줄 35·깬잡석·골쌓기 → 고정 한 번 → 새 식 세 조건 고정 · 87,395 / 132,133 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ly2oHAaTDYRjcniAmHR81z --- .../EasyCalc/M01_EasyCalc_Table.css | 16 ++++ M01_MasterData/EasyCalc/M01_EasyCalc_Table.ts | 74 ++++++++++++------- 2 files changed, 63 insertions(+), 27 deletions(-) diff --git a/M01_MasterData/EasyCalc/M01_EasyCalc_Table.css b/M01_MasterData/EasyCalc/M01_EasyCalc_Table.css index 79aa7fd24..b2788c7b7 100644 --- a/M01_MasterData/EasyCalc/M01_EasyCalc_Table.css +++ b/M01_MasterData/EasyCalc/M01_EasyCalc_Table.css @@ -124,6 +124,22 @@ opacity: 0.5; } +.m01-easytable__hint { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--spacing-8); + margin: 0; + padding: var(--spacing-8) var(--spacing-12); + border-radius: var(--radius-lg); + background: color-mix(in srgb, var(--color-accent) 10%, var(--color-canvas)); + font-size: var(--text-body-sm); +} + +.m01-easytable__hint:empty { + display: none; +} + .m01-easytable__warn { margin: 0; color: var(--color-danger); diff --git a/M01_MasterData/EasyCalc/M01_EasyCalc_Table.ts b/M01_MasterData/EasyCalc/M01_EasyCalc_Table.ts index 68af5ab3e..14b8be121 100644 --- a/M01_MasterData/EasyCalc/M01_EasyCalc_Table.ts +++ b/M01_MasterData/EasyCalc/M01_EasyCalc_Table.ts @@ -45,6 +45,10 @@ const TEXT = { Total: ["합계", "Total"], Wait: ["계산 중", "Calculating"], Stopped: ["계산 멈춤", "Calculation stopped"], + DesignHint: [ + "이 값은 설계 때 정함 — 늘 이 줄로 하려면", + "This value is set at design — to always use this row,", + ], Mismatch: [ "고른 줄이 이 조건으로는 안 걸림 — 조건을 다시 보세요", "The picked row isn't matched by these conditions", @@ -351,6 +355,8 @@ interface CallState { picked: number | null; /** 「설계 때 정함」 조건을 고른 줄 값으로 미리 봄 */ tryValues: Record; + /** 마지막으로 누른 줄이 미리 보기만 한 「설계 때 정함」 조건 */ + designHint: string[]; } const showCell = (v: Cell): string => { @@ -367,7 +373,7 @@ function startState(call: FindCall, doc: EasyDoc): CallState { const mode: Mode = names.has(a.rhs) ? "design" : lit !== null ? "fix" : "asis"; return { key: a.key, orig: a.rhs, mode, input, fix: lit }; }); - return { conds, col: call.col, picked: null, tryValues: {} }; + return { conds, col: call.col, picked: null, tryValues: {}, designHint: [] }; } function stateArgs(st: CallState, table: EasyTable | null): FindArg[] | null { @@ -465,6 +471,7 @@ async function mountTable( const grid = el("div", { className: "m01-easytable__grid" }); const money = el("div", { className: "m01-easytable__money" }); const warn = el("p", { className: "m01-easytable__warn" }); + const hint = el("p", { className: "m01-easytable__hint" }); const use = createButton({ label: tt("Use"), onClick: () => finish(newFormula()) }); const cancel = createButton({ label: tt("Cancel"), @@ -491,6 +498,7 @@ async function mountTable( const st = states[now]; const row = table?.줄[idx]; if (!table || !row) return; + st.designHint = []; for (const key of Object.keys(table.조건)) { if (row[key] === undefined) continue; let c = st.conds.find((x) => x.key === key); @@ -504,6 +512,7 @@ async function mountTable( ? rangeNumber(row[key] as [number | null, number | null], table.범위규칙 ?? "") : row[key]; st.tryValues[c.input] = n; + st.designHint.push(key); } else { c.mode = "fix"; c.fix = row[key]; @@ -521,9 +530,36 @@ async function mountTable( c.fix = row?.[c.key] ?? (c.input ? (inputs()[c.input] as Cell) : null); } c.mode = mode; + states[now].designHint = states[now].designHint.filter((k) => k !== c.key); void refresh(); }; + /** 누른 줄의 「설계 때 정함」 조건을 한 번에 그 줄 값으로 고정 */ + const fixPicked = (): void => { + const st = states[now]; + const row = st.picked === null ? null : tbl[now]?.줄[st.picked]; + if (!row) return; + for (const c of st.conds) + if (st.designHint.includes(c.key)) { + c.mode = "fix"; + c.fix = row[c.key]; + } + st.designHint = []; + void refresh(); + }; + + const paintHint = (): void => { + const st = states[now]; + hint.replaceChildren( + ...(st.picked !== null && st.designHint.length + ? [ + el("span", { text: `${tt("DesignHint")} ` }), + createButton({ label: tt("Fix"), size: "sm", onClick: fixPicked }), + ] + : []), + ); + }; + const paintConds = (): void => { const st = states[now]; condBox.replaceChildren( @@ -579,10 +615,7 @@ async function mountTable( const th = (text: string, cls = ""): HTMLElement => el("th", { className: cls, text }); const rows = table.줄.map((row, idx) => { const tr = el("tr", { - className: [ - idx === hit && !busy ? "is-hit" : "", - idx === st.picked ? "is-picked" : "", - ].join(" "), + className: `${idx === hit && !busy ? "is-hit" : ""} ${idx === st.picked ? "is-picked" : ""}`, children: [ ...conds.map((k) => el("td", { text: showCell(row[k]) })), ...vals.map((k) => @@ -600,28 +633,13 @@ async function mountTable( }); return tr; }); - grid.replaceChildren( - el("table", { - children: [ - el("thead", { - children: [ - el("tr", { - children: [ - ...conds.map((k) => th(k)), - ...vals.map((k) => - th( - `${k}${table.값칸[k] ? ` (${table.값칸[k]})` : ""}`, - k === st.col ? "is-col" : "", - ), - ), - ], - }), - ], - }), - el("tbody", { children: rows }), - ], - }), - ); + const unit = (k: string): string => (table.값칸[k] ? `${k} (${table.값칸[k]})` : k); + const top = [ + ...conds.map((k) => th(k)), + ...vals.map((k) => th(unit(k), k === st.col ? "is-col" : "")), + ]; + const thead = el("thead", { children: [el("tr", { children: top })] }); + grid.replaceChildren(el("table", { children: [thead, el("tbody", { children: rows })] })); }; const paint = (): void => { @@ -646,6 +664,7 @@ async function mountTable( el("span", { className: "m01-easytable__note", text: tt("Hint") }), ); paintConds(); + paintHint(); paintGrid(); const picked = states[now].picked; const mismatch = !busy && picked !== null && last.hits[now] !== picked; @@ -667,6 +686,7 @@ async function mountTable( which, head, condBox, + hint, grid, money, warn, From a06863e2ecc2e573e08572c1efff857d5599e604 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 09:49:18 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat(m01-easycalc):=20usesOf=20=ED=9A=8C?= =?UTF-8?q?=EC=82=AC=20=C2=B7=20=EA=B0=9C=EC=9D=B8=20=EC=B8=B5=20=EC=A1=B0?= =?UTF-8?q?=ED=95=A9=EA=B9=8C=EC=A7=80=20=E2=80=94=20M02=20=EC=B8=B5=20?= =?UTF-8?q?=EA=B8=B8=20=EC=9D=BD=EA=B8=B0=EB=A7=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 내 층 /combos/all · 남의 회사 /layers/company/owners · 회사마다 사람 /layers/personal/owners → 층 조합 하나씩 읽어 담은 것만 - EasyUse.where(「회사 · 이름」 · 「개인 · 이름」) · 못 읽은 층은 「일부를 못 읽음」 한 줄 - 시험 — holding 견본(담은 것만 · 자리 글) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KmELpdXFDRZHhfbGuRhSmL --- M01_MasterData/EasyCalc/M01_EasyCalc_Api.ts | 111 +++++++++++++++++++- resources/tester/test_m01_easycalc_api.py | 28 +++++ 2 files changed, 134 insertions(+), 5 deletions(-) diff --git a/M01_MasterData/EasyCalc/M01_EasyCalc_Api.ts b/M01_MasterData/EasyCalc/M01_EasyCalc_Api.ts index 2fb76bacf..694236d86 100644 --- a/M01_MasterData/EasyCalc/M01_EasyCalc_Api.ts +++ b/M01_MasterData/EasyCalc/M01_EasyCalc_Api.ts @@ -53,9 +53,12 @@ export interface EasySaved { row: LogicRow; } -/** 쓰는 곳 한 줄 — 일위대가 조합 · 이 로직을 부르는 다른 로직 */ +/** 쓰는 곳 한 줄 — 일위대가 조합 · 이 로직을 부르는 다른 로직 + * `where` = 회사 · 개인 층 조합의 자리 글(「회사 · 산림조합」) · 마스터 층 · 로직은 없음 + * 층을 못 읽었으면 `key` "" · `name` = 「일부를 못 읽음」 한 줄(지우기 전에 확인하게) */ export interface EasyUse { kind: "combo" | "logic"; + where?: string; key: string; name: string; } @@ -158,6 +161,14 @@ const TEXT = { "이 기계는 시간당 값을 셀 자료가 없음 — 다른 기계를 골라 주세요", "No hourly cost data for this machine — pick another", ], + AtCompany: ["회사 · {name}", "Company · {name}"], + AtPerson: ["개인 · {name}", "Personal · {name}"], + MyCompany: ["내 회사", "My company"], + Me: ["나", "Me"], + Unseen: [ + "회사 · 개인 조합 일부를 못 읽음 — 지우기 전에 확인 필요", + "Some company or personal combos could not be read — check before deleting", + ], } as const satisfies Record; type SayKey = keyof typeof TEXT; @@ -256,6 +267,26 @@ export function machineCall(m: MachineLogic, code: string): EasyMachineElement { const rest = needInputs.map((i) => `, ${quote(i.이름)}=${quote(i.이름)}`).join(""); return { element: `로직(${m.key}, ${MACHINE}='${code}'${rest})`, needInputs }; } + +/** 층 조합 줄 통째(`담은로직` 포함) */ +export interface LayerCombo { + 키: string; + 이름?: string; + 담은로직?: { 로직?: string }[]; +} + +/** 이 로직을 담은 층 조합 — `layer` = company · personal · `name` = 층 주인 이름 */ +export function holding( + combos: LayerCombo[], + key: string, + layer: "company" | "personal", + name: string, +): EasyUse[] { + const where = say(layer === "company" ? "AtCompany" : "AtPerson", { name }); + return combos + .filter((c) => (c.담은로직 ?? []).some((one) => one?.로직 === key)) + .map((c): EasyUse => ({ kind: "combo", where, key: c.키, name: c.이름 ?? "" })); +} /* ── 순수 끝 ── */ const OWN = "로직_자체.json"; // 내가 만든 것 한 자리(서버 `Store_Make.OWN`) @@ -263,10 +294,15 @@ const OWNER = "현장"; const CALC_WAIT_MS = 300; const MACHINE_FILE = "건설기계"; // 기계 시간당 경비 로직이 든 장 파일(「08장 건설기계」) -async function request(path: string, body?: unknown): Promise { +/** `/api/m01` 길 · M02 층 조합 읽기는 `m02` */ +const request = (path: string, body?: unknown): Promise => + send(`${API_BASE_URL}/m01${path}`, body); +const m02 = (path: string): Promise => send(`${API_BASE_URL}/m02${path}`); + +async function send(url: string, body?: unknown): Promise { let response: Response; try { - response = await fetch(`${API_BASE_URL}/m01${path}`, { + response = await fetch(url, { method: body === undefined ? "GET" : "POST", credentials: "include", headers: { "Content-Type": "application/json" }, @@ -385,13 +421,15 @@ export async function deleteEasy(key: string, version: string): Promise<{ versio return { version: got.version }; } -/** 쓰는 곳 — 담은 일위대가 조합(마스터 층) + 이 로직을 부르는 내가 만든 것. +/** 쓰는 곳 — 담은 일위대가 조합(마스터 · 회사 · 개인 층) + 이 로직을 부르는 내가 만든 것. + * 지우기 누를 때 부름(무거움) · 서버 지우기 검사는 마스터 층 조합만 막음. * ponytail: 부르는 로직은 내가 만든 것만 하나씩 받아 훑음(품셈 기본은 내 것을 안 부름) — * 내 것이 수백이 되면 서버에 「부르는 로직」 길을 더할 것 */ export async function usesOf(key: string): Promise { - const [combos, mine] = await Promise.all([ + const [combos, mine, layered] = await Promise.all([ request<{ combos: { 키: string; 이름: string }[] }>(`/logic/combos?${query({ key })}`), request<{ logics: Summary[] }>(`/logics?${query({ sub: "자체" })}`), + layerUses(key), ]); const call = `로직(${key}`; const others = mine.logics.filter((s) => s.키 !== key); @@ -401,10 +439,73 @@ export async function usesOf(key: string): Promise { .map((one): EasyUse => ({ kind: "logic", key: one.logic.키, name: one.logic.이름 })); return [ ...combos.combos.map((c): EasyUse => ({ kind: "combo", key: c.키, name: c.이름 })), + ...layered, ...callers, ]; } +interface LayerTarget { + layer: "company" | "personal"; + owner: string; + name: string; + rows?: { 키: string }[]; +} + +/** 회사 · 개인 층 조합 가운데 이 로직을 담은 것 — M02 층 길 읽기만(시스템 관리자는 모든 층). + * 내 층 = `/combos/all` · 남의 회사 = `/layers/company/owners` · 회사마다 사람 = `/layers/personal/owners`. + * 못 읽은 층이 있으면 「일부를 못 읽음」 한 줄을 더함. + * ponytail: 층마다 조합 하나씩 받아 훑음 — 조합이 수천이면 서버에 「이 로직을 담은 층 조합」 길을 더할 것 */ +async function layerUses(key: string): Promise { + let missed = false; + const soft = (p: Promise, empty: T): Promise => + p.catch(() => { + missed = true; + return empty; + }); + type Own = { layer: string; owner: string | null; 줄: { 키: string }[] }; + type Owner = { owner: string; name: string }; + const own = await soft(m02("/combos/all"), []); + const targets: LayerTarget[] = []; + let myCompany = ""; + for (const one of own) { + if (one.layer === "system" || !one.owner) continue; + const company = one.layer === "company"; + if (company) myCompany = one.owner; + targets.push({ + layer: company ? "company" : "personal", + owner: one.owner, + name: say(company ? "MyCompany" : "Me"), + rows: one.줄, + }); + } + const companies = await soft(m02("/layers/company/owners"), []); + for (const c of companies) targets.push({ layer: "company", ...c }); + const people = await Promise.all( + [myCompany, ...companies.map((c) => c.owner)] + .filter(Boolean) + .map((c) => soft(m02(`/layers/personal/owners?${query({ company: c })}`), [])), + ); + for (const p of people.flat()) targets.push({ layer: "personal", ...p }); + + const found = await Promise.all( + targets.map(async (t) => { + const at = `/layers/${t.layer}/combos`; + const own = `?${query({ owner: t.owner })}`; + const rows = t.rows ?? (await soft(m02<{ 줄: { 키: string }[] }>(at + own), { 줄: [] })).줄; + const combos = await Promise.all( + rows.map((r) => + soft(m02<{ combo: LayerCombo }>(`${at}/${encodeURIComponent(r.키)}${own}`), null), + ), + ); + const full = combos.flatMap((x) => (x ? [x.combo] : [])); + return holding(full, key, t.layer, t.name); + }), + ); + const out = found.flat(); + if (missed) out.push({ kind: "combo", key: "", name: say("Unseen") }); + return out; +} + let family: Promise | null = null; /** 기계 시간당 경비 로직 묶음 — 한 번 받아 둠(실패하면 다음에 다시) */ diff --git a/resources/tester/test_m01_easycalc_api.py b/resources/tester/test_m01_easycalc_api.py index 6f3001696..35015959c 100644 --- a/resources/tester/test_m01_easycalc_api.py +++ b/resources/tester/test_m01_easycalc_api.py @@ -282,6 +282,34 @@ def test_기계_부름글이_서버_검사와_계산을_지남(client: TestClien _call(client, "/api/m01/logic/delete", {"key": made["key"], "version": made["version"]}) +# ── 회사 · 개인 층 조합 ──────────────────────────────────────────────── +@pytest.mark.skipif(NO_NODE, reason="node 가 없음") +def test_층_조합_가운데_이_로직을_담은_것만_자리_글과_함께() -> None: + combos = [ + { + "키": "UC3-000001", + "이름": "돌쌓기 한 벌", + "담은로직": [{"로직": "GX000001", "메모": None}], + }, + {"키": "UC3-000002", "이름": "다른 것", "담은로직": [{"로직": "GX000002"}]}, + {"키": "UC3-000003", "이름": "빈 것"}, + { + "키": "UC3-000004", + "이름": "둘째 자리", + "담은로직": [{"로직": "GF000001"}, {"로직": "GX000001"}], + }, + ] + got = _node( + f"holding({json.dumps(combos, ensure_ascii=False)}, 'GX000001', 'company', '산림조합')" + ) + assert got == [ + {"kind": "combo", "where": "회사 · 산림조합", "key": "UC3-000001", "name": "돌쌓기 한 벌"}, + {"kind": "combo", "where": "회사 · 산림조합", "key": "UC3-000004", "name": "둘째 자리"}, + ] + person = _node("holding([{키: 'UP15-000001', 담은로직: [{로직: 'K'}]}], 'K', 'personal', '나')") + assert person == [{"kind": "combo", "where": "개인 · 나", "key": "UP15-000001", "name": ""}] + + # ── 쉬운 말 ───────────────────────────────────────────────────────────── @pytest.mark.skipif(NO_NODE, reason="node 가 없음") def test_plainError_상태와_검사글을_쉬운_말로() -> None: