diff --git a/M01_MasterData/M01_MasterData_Store.py b/M01_MasterData/M01_MasterData_Store.py index 3ec32d1e..ac5a6e21 100644 --- a/M01_MasterData/M01_MasterData_Store.py +++ b/M01_MasterData/M01_MasterData_Store.py @@ -343,7 +343,10 @@ def logic(key: str) -> dict: def _brief(ref: str, row: dict) -> dict: return { "ref": ref, - **{k: row.get(k) for k in ("원문번호", "이름", "규격", "단위", "값", "값칸")}, + **{ + k: row.get(k) + for k in ("원문번호", "구분", "상세구분", "이름", "규격", "단위", "값", "값칸") + }, } diff --git a/M01_MasterData/M01_MasterData_UI_Page.ts b/M01_MasterData/M01_MasterData_UI_Page.ts index 5ad2e830..61a863fc 100644 --- a/M01_MasterData/M01_MasterData_UI_Page.ts +++ b/M01_MasterData/M01_MasterData_UI_Page.ts @@ -23,7 +23,15 @@ import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch"; import { saveFiles } from "./M01_MasterData_Api_Fetch"; import { discard, isStale, onDraftChange, payload, totalCount } from "./M01_MasterData_Draft"; import { renderRows } from "./M01_MasterData_UI_Rows"; -import { buildSide, fileLabel, loadView, saveView, type Pick } from "./M01_MasterData_UI_Side"; +import { + buildSide, + fileLabel, + loadView, + saveView, + setPendingLogicKey, + takePendingLogicKey, + type Pick, +} from "./M01_MasterData_UI_Side"; import { renderTables } from "./M01_MasterData_UI_Tables"; import "./M01_MasterData_UI_Style.css"; @@ -95,8 +103,9 @@ function buildPage(): HTMLElement { current = { ...pick, view: undefined }; const groupTitle = pick.group === pick.label ? [pick.group] : [pick.group, pick.label]; title.textContent = groupTitle.join(" · "); - const view = TABLE_GROUPS.includes(pick.group) ? renderTables : renderRows; - dispose = view(body, pick, query); + dispose = TABLE_GROUPS.includes(pick.group) + ? renderTables(body, pick, query, openLogicTab) + : renderRows(body, pick, query); showNotice(isStale(pick.file.file, pick.file.version) ? [L("M01_FileStale")] : []); }; @@ -165,18 +174,35 @@ function buildPage(): HTMLElement { return [L("M01_Stale").replace("{value}", stale.map(fileLabel).join(", ")), again]; }; + /** 로직 탭으로 전환 — `key` 가 있으면 들고 가서 로직 목록 찾기 칸에 넣으라고 안내 + * (로직 목록이 직접 그 키를 열지는 못함 — 로직 부분은 sub1 담당 파일). */ + const notifyLogicKey = (key: string): void => { + navigator.clipboard?.writeText(key).catch(() => undefined); + showToast(L("M01_LogicKeyCopy").replace("{value}", key), "success"); + }; + + const openLogicTab = (key?: string): void => { + if (key) setPendingLogicKey(key); + dispose(); + dispose = (): void => {}; + current = null; + showMode("logic"); + if (logicMounted) { + if (key) notifyLogicKey(key); + return; + } + logicMounted = true; + void import("./M01_MasterData_UI_Logic_Page").then((m) => { + void m.mountM01Logic(logicHost, side); + const pending = takePendingLogicKey(); + if (pending) notifyLogicKey(pending); + }); + }; + /* --- 좌측: 컨테이너 아홉 --- */ const side = buildSide( openFile, - () => { - dispose(); - dispose = (): void => {}; - current = null; - showMode("logic"); - if (logicMounted) return; - logicMounted = true; - void import("./M01_MasterData_UI_Logic_Page").then((m) => m.mountM01Logic(logicHost, side)); - }, + () => openLogicTab(), () => { dispose(); dispose = (): void => {}; diff --git a/M01_MasterData/M01_MasterData_UI_Side.ts b/M01_MasterData/M01_MasterData_UI_Side.ts index 68d7e46e..a40b9894 100644 --- a/M01_MasterData/M01_MasterData_UI_Side.ts +++ b/M01_MasterData/M01_MasterData_UI_Side.ts @@ -36,6 +36,27 @@ export function saveView(group: string, patch: Partial): void { } } +const PENDING_LOGIC_KEY = "m01.logic.pending"; + +/** 소요량·계수 표의 「쓰는 로직」 키를 눌러 로직 탭으로 넘어갈 때 들고 갈 키 — 로직 목록 화면이 직접 열지는 못함(로직 부분은 sub1). */ +export function setPendingLogicKey(key: string): void { + try { + sessionStorage.setItem(PENDING_LOGIC_KEY, key); + } catch { + /* 기억 못 해도 로직 탭 전환은 됨 */ + } +} + +export function takePendingLogicKey(): string | null { + try { + const key = sessionStorage.getItem(PENDING_LOGIC_KEY); + sessionStorage.removeItem(PENDING_LOGIC_KEY); + return key; + } catch { + return null; + } +} + /** 요소 화면에서 열 것 — 파일 · 하위 거름(구분·세부분류) · 상세구분 · 제목 */ export interface Pick { group: string; diff --git a/M01_MasterData/M01_MasterData_UI_Style.css b/M01_MasterData/M01_MasterData_UI_Style.css index 53dd4bc5..7da36a04 100644 --- a/M01_MasterData/M01_MasterData_UI_Style.css +++ b/M01_MasterData/M01_MasterData_UI_Style.css @@ -230,6 +230,16 @@ color: var(--color-text-muted); } +.m01-master__logic-key { + padding: 0; + border: 0; + background: transparent; + color: var(--color-accent); + font: inherit; + cursor: pointer; + text-decoration: underline; +} + /* 조달 찾기 모달 */ .m01-procure { position: fixed; diff --git a/M01_MasterData/M01_MasterData_UI_Tables.ts b/M01_MasterData/M01_MasterData_UI_Tables.ts index 300af822..bff01e29 100644 --- a/M01_MasterData/M01_MasterData_UI_Tables.ts +++ b/M01_MasterData/M01_MasterData_UI_Tables.ts @@ -36,8 +36,13 @@ const LIST_COLS = ["키", "원문번호", "구분", "상세구분", "이름", " /** 용도 대상 거름 드롭다운 후보 — PLAN 1-1 셋째 줄 그대로. */ const USAGE_TARGETS = ["인력", "기계", "재료", "할증", "환산", "작업량 조건"]; -/** 돌려받은 함수 = 이 화면을 걷을 때 부를 해제. */ -export function renderTables(host: HTMLElement, pick: Pick, q: string): () => void { +/** 돌려받은 함수 = 이 화면을 걷을 때 부를 해제. `openLogic` = 용도의 로직키 눌렀을 때 로직 탭으로. */ +export function renderTables( + host: HTMLElement, + pick: Pick, + q: string, + openLogic: (key: string) => void, +): () => void { let stop = (): void => {}; let usage = pick.view?.usage ?? ""; const list = (page = pick.view?.page ?? 1): void => { @@ -50,7 +55,7 @@ export function renderTables(host: HTMLElement, pick: Pick, q: string): () => vo usage, page, (file, key) => { - stop = openTable(host, file, key, () => list(page)); + stop = openTable(host, file, key, () => list(page), openLogic); }, (next) => { usage = next; @@ -140,7 +145,13 @@ async function showList( } /** 표 하나. 돌려받은 함수 = 해제. */ -function openTable(host: HTMLElement, file: string, key: string, back: () => void): () => void { +function openTable( + host: HTMLElement, + file: string, + key: string, + back: () => void, + openLogic: (key: string) => void, +): () => void { let off = (): void => {}; void (async () => { let version: string; @@ -209,6 +220,15 @@ function openTable(host: HTMLElement, file: string, key: string, back: () => voi el("p", { className: "m01-master__muted", text: n }), ); const usage = table.용도 as TableUsage | undefined; + const logicLinks = (usage?.로직키 ?? []).map((k, i) => { + const btn = el("button", { + className: "m01-master__logic-key", + text: k, + attrs: { type: "button", title: L("M01_OpenLogic") }, + }); + btn.addEventListener("click", () => openLogic(k)); + return i === 0 ? btn : el("span", { text: ", ", children: [btn] }); + }); host.replaceChildren( el("div", { className: "m01-master__bar", @@ -223,10 +243,8 @@ function openTable(host: HTMLElement, file: string, key: string, back: () => voi ? [ el("span", { className: "m01-master__muted", - attrs: { title: usage.로직키.join(", ") }, - text: `${L("M01_TableUsage")}: ${usage.공종} · ${usage.대상.join(", ")} · ${L( - "M01_TableUsageLogicCount", - ).replace("{value}", String(usage.로직키.length))}`, + text: `${L("M01_TableUsage")}: ${usage.공종} · ${usage.대상.join(", ")} · `, + children: logicLinks, }), ] : []), diff --git a/resources/master_data/_화면_계약.md b/resources/master_data/_화면_계약.md index 606ada66..80362313 100644 --- a/resources/master_data/_화면_계약.md +++ b/resources/master_data/_화면_계약.md @@ -14,18 +14,18 @@ ## 2. 읽기 -| 길 | 받음 | 줌 | -| --------------------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `GET /groups` | | `{groups: [{group, files, rows}]}` — 그룹 차례는 `_틀.md` 2장 | -| `GET /groups/{group}/files` | | `{files: [{file, book, chapter, edition, rows, version, order}]}` — 머리 `차례` 순 | -| `GET /subs` | `file` 또는 `group` | `{file, version, slot, subs: [{name, book, details}]}` — 인력 `구분` · 기계 `세부분류`(머리에 등록된 것) · `group`(소요량 · 계수 · 로직)은 장 파일에서 모음(구분 = 원문 + 부문 · 상세구분 = 장 · 차례대로) | -| `GET /rows` | `file` · `page`(1) · `size`(50) · `q` · `sub` · `detail` | `{file, version, total, page, size, rows: [줄…]}` | -| `GET /tables` | `file` 또는 `group` · `sub` · `detail` · `q` · `page`(1) · `size`(30) | `{file, version, total, page, size, tables: [{file, 키, 원문번호, 구분, 상세구분, 이름, 기준, 출처, 조건, 값칸, count}]}` — 파일을 가로지른 한 목록 | -| `GET /table` | `file` · `key` | `{file, version, table: {표 하나 통째}}` | -| `GET /logics` | `sub` · `detail` · `q` · `blocked`(0/1) | `{logics: [{file, 키, 원문번호, 구분, 상세구분, 이름, 결과단위, 출처, blocked, reasons}]}` — 장 차례대로 | -| `GET /logic` | `key` | `{file, version, logic: {로직 한 줄 통째}, blocked, reasons, prices: {요소: 요약 \| null}}` | -| `GET /elements` | `group` · `q` · `limit`(50 · 200까지) | `{total, items: [{ref, file, 원문번호, 이름, 규격, 단위, 값, 값칸}]}` | -| `GET /materials` | `sub`(구분) · `detail` · `spec` · `region`(자재지역) · `limit`(50 · 200까지) | `{total, 기본, items: [{ref, 구분, 상세구분, 이름, 규격, 단위, 값, 관급}]}` — 재료 고르기 조건 안 후보 · `기본` = 시험 계산이 쓸 줄 | +| 길 | 받음 | 줌 | +| --------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `GET /groups` | | `{groups: [{group, files, rows}]}` — 그룹 차례는 `_틀.md` 2장 | +| `GET /groups/{group}/files` | | `{files: [{file, book, chapter, edition, rows, version, order}]}` — 머리 `차례` 순 | +| `GET /subs` | `file` 또는 `group` | `{file, version, slot, subs: [{name, book, details}]}` — 인력 `구분` · 기계 `세부분류`(머리에 등록된 것) · `group`(소요량 · 계수 · 로직)은 장 파일에서 모음(구분 = 원문 + 부문 · 상세구분 = 장 · 차례대로) | +| `GET /rows` | `file` · `page`(1) · `size`(50) · `q` · `sub` · `detail` | `{file, version, total, page, size, rows: [줄…]}` | +| `GET /tables` | `file` 또는 `group` · `sub` · `detail` · `q` · `page`(1) · `size`(30) | `{file, version, total, page, size, tables: [{file, 키, 원문번호, 구분, 상세구분, 이름, 기준, 출처, 조건, 값칸, count}]}` — 파일을 가로지른 한 목록 | +| `GET /table` | `file` · `key` | `{file, version, table: {표 하나 통째}}` | +| `GET /logics` | `sub` · `detail` · `q` · `blocked`(0/1) | `{logics: [{file, 키, 원문번호, 구분, 상세구분, 이름, 결과단위, 출처, blocked, reasons}]}` — 장 차례대로 | +| `GET /logic` | `key` | `{file, version, logic: {로직 한 줄 통째}, blocked, reasons, prices: {요소: 요약 \| null}}` | +| `GET /elements` | `group` · `q` · `limit`(50 · 200까지) | `{total, items: [{ref, file, 원문번호, 구분, 상세구분, 이름, 규격, 단위, 값, 값칸}]}` | +| `GET /materials` | `sub`(구분) · `detail` · `spec` · `region`(자재지역) · `limit`(50 · 200까지) | `{total, 기본, items: [{ref, 구분, 상세구분, 이름, 규격, 단위, 값, 관급}]}` — 재료 고르기 조건 안 후보 · `기본` = 시험 계산이 쓸 줄 | - `rows` 는 요소·로직 파일은 `줄`, 표형 파일(소요량·계수)은 `표` 를 줄로 봄. - `q` = 찾기 — `키`·`원문번호`·`이름`·`옛이름` 에 든 글(대소문자 무시). diff --git a/resources/tester/test_m01_api.py b/resources/tester/test_m01_api.py index f2674423..a8b5a14c 100644 --- a/resources/tester/test_m01_api.py +++ b/resources/tester/test_m01_api.py @@ -243,6 +243,13 @@ def test_더함_지움_로직_파일도_같은_길(client: TestClient) -> None: assert client.get("/api/m01/logic", params={"key": new}).status_code == 404 +def test_요소_찾기_기계는_구분_상세구분_실림(client: TestClient) -> None: + """테스트 컨테이너(sub3)가 기계 후보를 구분→상세구분으로 거르려면 필요.""" + found = _get(client, "/api/m01/elements", group="기계", q="불도저") + assert found["items"] + assert all(x["구분"] and x["상세구분"] for x in found["items"]) + + def test_단가_자동_요소_찾기_저장전_시험계산(client: TestClient) -> None: one = _get(client, "/api/m01/logic", key=LOGIC_KEY) labor = one["prices"][key_of("LB:1016")] diff --git a/ui_template/ui_template_locale_m1.ts b/ui_template/ui_template_locale_m1.ts index 9cbb1b7c..3b4af4f0 100644 --- a/ui_template/ui_template_locale_m1.ts +++ b/ui_template/ui_template_locale_m1.ts @@ -63,6 +63,11 @@ export const ui_locales_m1 = { M01_UsageTarget: ["대상", "Target"], M01_TableUsage: ["용도", "Usage"], M01_TableUsageLogicCount: ["로직 {value}개", "{value} logic rules"], + M01_OpenLogic: ["눌러 로직 탭으로", "Click to jump to the Logic tab"], + M01_LogicKeyCopy: [ + "로직 탭으로 이동 — 키 {value} 복사함(찾기에 붙여넣을 것)", + "Switched to Logic tab — copied key {value} (paste it into the search box)", + ], M01_GroupLabor: ["인력", "Labor"], M01_GroupMaterial: ["재료", "Materials"], M01_GroupMachine: ["기계", "Machines"],