From 02b9c8be9243767d23fd4c50dab79d49bb131f8c Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 20 Sep 2026 15:18:26 +0900 Subject: [PATCH] =?UTF-8?q?feat(M01):=20=EC=9D=B8=EB=A0=A5=20=ED=95=9C=20?= =?UTF-8?q?=ED=91=9C=20=C2=B7=20=EC=99=BC=EC=AA=BD=20=E3=80=8C=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=E3=80=8D=20+=20=EA=B5=AC=EB=B6=84=20=E2=86=92=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=EA=B5=AC=EB=B6=84=20=EB=91=90=20=EB=8B=A8=20?= =?UTF-8?q?=EA=B1=B0=EB=A6=84=20=C2=B7=20=EC=A4=80=EC=9A=A9=20=EB=AA=A8?= =?UTF-8?q?=EB=93=A0=20=EC=A4=84=20=EA=B3=A0=EB=A5=B4=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 인력 열 순서 = 키 … 옛이름 · 비고 · 준용 · 상태 읽기 전용 - 구분·상세구분 목록은 서버 /subs · 고른 거름은 sessionStorage 기억 Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_016WRFJmmhPi4exAzHgPZHMT --- M01_MasterData/M01_MasterData_UI_Rows.ts | 19 ++--- M01_MasterData/M01_MasterData_UI_Side.ts | 82 +++++++++++++++++++++- M01_MasterData/M01_MasterData_UI_Style.css | 12 ++++ ui_template/ui_template_locale_m1.ts | 2 + 4 files changed, 105 insertions(+), 10 deletions(-) diff --git a/M01_MasterData/M01_MasterData_UI_Rows.ts b/M01_MasterData/M01_MasterData_UI_Rows.ts index d4e876c3..365103bb 100644 --- a/M01_MasterData/M01_MasterData_UI_Rows.ts +++ b/M01_MasterData/M01_MasterData_UI_Rows.ts @@ -34,7 +34,7 @@ const SIZE = 50; const MARKET = "재료_시중물가.json"; const LINKED = "재료_품셈재료.json"; const JOB = ["인력.json"]; -const NOTE = "비고"; +const NOTE = "비고"; // 끝 열(인력은 준용 앞) · 줄에 없으면 빈 칸 · 눌러 적음 const SUPPLY = ["값", "조달"]; const SOURCE = [...SUPPLY, "출처"]; const NARA = "나라장터:"; @@ -68,7 +68,7 @@ function hidden(file: string, col: string): boolean { /** 키(한 번 주면 안 바뀜) · 빌더가 원문에서 채운 칸(구분 · 상세구분 · 옛이름) — 읽기 전용. */ const fixed = (file: string, col: string): boolean => col === "키" || - (JOB.includes(file) && ["구분", "상세구분", "옛이름"].includes(col.split(SEP)[0])); + (JOB.includes(file) && ["구분", "상세구분", "상태", "옛이름"].includes(col.split(SEP)[0])); const HEADS: Record = { [MARKET]: ["조달 값", "조달 기준", "M01_ProcureCol"], @@ -115,14 +115,16 @@ export function renderRows( for (const k of Object.keys(flatten(row))) if (!hidden(file, k) && k !== NOTE && !cols.includes(k)) cols.push(k); } + const heads = (HEADS[file] ?? []).map((h) => + el("th", { text: h.startsWith("M01_") ? L(h as "M01_PriceCol") : h }), + ); const head = el("tr", { children: [ el("th"), ...cols.map((c) => el("th", { text: c })), - ...(HEADS[file] ?? []).map((h) => - el("th", { text: h.startsWith("M01_") ? L(h as "M01_PriceCol") : h }), - ), - el("th", { text: NOTE }), // 비고 — 항상 끝 열(줄에 없으면 빈 칸 · 눌러 적음) + ...(JOB.includes(file) ? [] : heads), + el("th", { text: NOTE }), + ...(JOB.includes(file) ? heads : []), ], }); const body = el("tbody"); @@ -145,6 +147,7 @@ export function renderRows( onEdit: (text) => setAdd(file, i, withNote(row, text)), }), ); + if (JOB.includes(file)) tr.append(el("td")); body.append(tr); }); @@ -178,7 +181,6 @@ export function renderRows( : (next: Row): void => editRow(file, d.version, key, row, next); if (file === MARKET) tr.append(...marketCells(row, cur, put)); if (file === LINKED) tr.append(linkedCell(row, cur, put)); - if (JOB.includes(file)) tr.append(wageCell(row, cur, put)); tr.append( buildCell(show(cur[NOTE]), { changed: !same(cur[NOTE], row[NOTE]), @@ -188,6 +190,7 @@ export function renderRows( : (text) => editRow(file, d.version, key, row, withNote(cur, text)), }), ); + if (JOB.includes(file)) tr.append(wageCell(row, cur, put)); body.append(tr); } @@ -328,7 +331,7 @@ function linkedCell(row: Row, cur: Row, put?: Put): HTMLTableCellElement { function wageCell(row: Row, cur: Row, put?: Put): HTMLTableCellElement { const text = show(cur["준용"]); const td = buildCell(named(text), { changed: text !== show(row["준용"]) }); - if (put && row["값"] === null) { + if (put) { const btn = rowBtn(L("M01_JobFind")); btn.addEventListener("click", () => openPickModal({ diff --git a/M01_MasterData/M01_MasterData_UI_Side.ts b/M01_MasterData/M01_MasterData_UI_Side.ts index f73ef4a5..059882fc 100644 --- a/M01_MasterData/M01_MasterData_UI_Side.ts +++ b/M01_MasterData/M01_MasterData_UI_Side.ts @@ -35,6 +35,8 @@ export interface SideHandle { setActive: (id: string | null) => void; } +const LABOR_FILTER = "m01.laborFilter"; +const LABOR_ID = "인력|"; const MATERIAL_NAMES: Record = { 나라장터자재: "나라장터", 오피넷유가: "유가" }; /** 파일 이름 → 목록 글자 — 「소요량_건설품셈_10장_창호…」 → 「건설품셈_10장_창호…」 */ @@ -98,6 +100,78 @@ export function buildSide(onOpen: (pick: Pick) => void, onLogic: () => void): Si return button; }; + /** 인력 — 「전체」 한 줄 + 구분 → 상세구분 두 단 거름(서버가 준 목록 · 고른 것은 기억) */ + const laborFilter = ( + file: FileInfo, + open: (file: FileInfo, sub: string, label: string, detail?: string) => void, + ): HTMLElement => { + const list = kinds.get("인력") ?? []; + const saved = (() => { + try { + return JSON.parse(sessionStorage.getItem(LABOR_FILTER) ?? "[]") as string[]; + } catch { + return []; + } + })(); + const pickSub = list.find((k) => k.name === saved[0]); + const choose = (name: string, values: string[], value: string): HTMLSelectElement => { + const select = el("select", { + className: "m01-side__select", + attrs: { "aria-label": name }, + children: [L("M01_All"), ...values].map((v, i) => + el("option", { text: v, attrs: { value: i ? v : "" } }), + ), + }); + select.value = value; + return select; + }; + const sub = choose( + L("M01_LaborSub"), + list.map((k) => k.name), + pickSub?.name ?? "", + ); + const detail = choose(L("M01_LaborDetail"), pickSub?.details ?? [], saved[1] ?? ""); + detail.disabled = !pickSub?.details.length; + const run = (): void => { + try { + sessionStorage.setItem(LABOR_FILTER, JSON.stringify([sub.value, detail.value])); + } catch { + /* 기억 못 해도 거름은 됨 */ + } + const label = [sub.value, detail.value].filter(Boolean).join(" · ") || L("M01_All"); + setActive(LABOR_ID); + open(file, sub.value, label, detail.value); + }; + sub.addEventListener("change", () => { + const details = list.find((k) => k.name === sub.value)?.details ?? []; + detail.replaceChildren( + ...[L("M01_All"), ...details].map((v, i) => + el("option", { text: v, attrs: { value: i ? v : "" } }), + ), + ); + detail.disabled = details.length === 0; + run(); + }); + detail.addEventListener("change", run); + const all = make({ id: LABOR_ID, label: L("M01_All"), count: file.rows, run }, "", 0, () => { + sub.value = detail.value = ""; + sub.dispatchEvent(new Event("change")); + }); + return el("div", { + children: [ + all, + el("label", { + className: "m01-side__filter", + children: [el("span", { text: L("M01_LaborSub") }), sub], + }), + el("label", { + className: "m01-side__filter", + children: [el("span", { text: L("M01_LaborDetail") }), detail], + }), + ], + }); + }; + const drawGroup = (group: Group): void => { const files = lists.get(group) ?? []; const body = bodies.get(group); @@ -105,7 +179,11 @@ export function buildSide(onOpen: (pick: Pick) => void, onLogic: () => void): Si const open = (file: FileInfo, sub: string, label: string, detail = ""): void => onOpen({ group, file, sub, detail, label }); const one = files[0]; - if (group === "인력" || group === "기계") { + if (group === "인력" && one) { + body.replaceChildren(laborFilter(one, open)); + return; + } + if (group === "기계") { const list = kinds.get(group) ?? []; const all: TreeNode[] = one ? [ @@ -170,7 +248,7 @@ export function buildSide(onOpen: (pick: Pick) => void, onLogic: () => void): Si const subs = await fetchSubs(file); kinds.set(group, subs); await Promise.all( - subs.map(async (k) => + (group === "기계" ? subs : []).map(async (k) => counts.set(`${group}\n${k.name}`, (await fetchRows(file, 1, 1, "", false, k.name)).total), ), ); diff --git a/M01_MasterData/M01_MasterData_UI_Style.css b/M01_MasterData/M01_MasterData_UI_Style.css index d9548f8f..def7f125 100644 --- a/M01_MasterData/M01_MasterData_UI_Style.css +++ b/M01_MasterData/M01_MasterData_UI_Style.css @@ -299,3 +299,15 @@ flex-wrap: wrap; gap: 8px; } + +.m01-side__filter { + display: flex; + align-items: center; + gap: var(--spacing-8); + padding: var(--spacing-4) var(--spacing-8); +} + +.m01-side__select { + flex: 1; + min-width: 0; +} diff --git a/ui_template/ui_template_locale_m1.ts b/ui_template/ui_template_locale_m1.ts index 40ec02f6..3395888b 100644 --- a/ui_template/ui_template_locale_m1.ts +++ b/ui_template/ui_template_locale_m1.ts @@ -56,6 +56,8 @@ export const ui_locales_m1 = { M01_JobTitle: ["준용 직종 고르기", "Pick substitute job"], M01_JobCol: ["준용", "Substitute"], M01_All: ["전체", "All"], + M01_LaborSub: ["구분", "Category"], + M01_LaborDetail: ["상세구분", "Detail"], M01_GroupLabor: ["인력", "Labor"], M01_GroupMaterial: ["재료", "Materials"], M01_GroupMachine: ["기계", "Machines"],