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: