diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Modal.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Modal.ts index 0fbfa52f..7fd3ca41 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Modal.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Modal.ts @@ -16,6 +16,7 @@ import { type RelatedFind, type TableRow, } from "./M01_MasterData_UI_Logic_Note"; +import { jumpToMaster, type MasterJump } from "./M01_MasterData_UI_Side"; import { pillBar, type Pill } from "./M01_MasterData_UI_LogicLab_Pill"; import { tl } from "./M01_MasterData_UI_LogicLab_Text"; import "./M01_MasterData_UI_LogicLab_Pill.css"; @@ -72,6 +73,7 @@ function tableView( className: "m01-logic__muted", text: `${find.source} → ${find.find.col} · ${hit ? tl("Modal_Hit") : tl("Modal_NoHit")}`, }), + goButton({ kind: "table", key: table.키 }), el("div", { className: "m01-logic__scroll", children: [ @@ -93,6 +95,14 @@ function tableView( const groupOf = (ref: string): string => ref.startsWith("LB") ? "인력" : ref.startsWith("M") ? "재료" : ref.startsWith("EQ") ? "기계" : ""; +function goButton(target: MasterJump): HTMLElement { + return createButton({ + label: tl("Pill_Go"), + variant: "ghost", + onClick: () => jumpToMaster(target), + }); +} + function priceView(ref: string, brief: ElementBrief): HTMLElement { const cols = Object.entries(brief.값칸 ?? {}); return el("div", { @@ -114,6 +124,7 @@ function priceView(ref: string, brief: ElementBrief): HTMLElement { ...cols.flatMap(([k, v]) => [el("dt", { text: k }), el("dd", { text: String(v) })]), ], }), + goButton({ kind: "element", group: groupOf(ref), ref: brief.ref ?? ref, file: brief.file }), ], }); } @@ -153,7 +164,11 @@ export function openMaterialModal(target: ModalTarget): void { } else if (target.element && !logicRef) { pills.push({ label: `${groupOf(base)} ${target.title}`.trim(), - view: () => el("p", { text: target.element }), + view: () => + el("p", { + className: "m01-logic__muted", + text: `${target.element} — ${tl(target.element?.includes("{") ? "Pill_NoPick" : "Pill_NoValue")}`, + }), }); } if (logicRef) { diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Text.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Text.ts index 8d21e699..91943b63 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Text.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Text.ts @@ -52,6 +52,13 @@ const TEXT = { Modal_UnitPrice: ["단가", "Unit price"], Pill_Table: ["표", "Table"], Pill_Logic: ["로직", "Logic"], + Pill_Go: ["마스터 화면에서 보기", "Open in master"], + Pill_Back: ["← 로직 개선 시험으로 돌아가기", "← Back to logic lab"], + Pill_NoPick: [ + "고르기 조건에 맞는 마스터 줄이 없음 — 단가를 못 찾음", + "No master row matches the pick conditions", + ], + Pill_NoValue: ["관리자 값 미확보 — 마스터에 단가가 없음", "No price in master yet"], Modal_Qty: ["수량 식", "Quantity formula"], Modal_Price: ["단가 자료", "Price source"], Modal_Value: ["값", "Value"], diff --git a/M01_MasterData/M01_MasterData_UI_Page.ts b/M01_MasterData/M01_MasterData_UI_Page.ts index ae12a8b3..08f2e216 100644 --- a/M01_MasterData/M01_MasterData_UI_Page.ts +++ b/M01_MasterData/M01_MasterData_UI_Page.ts @@ -20,7 +20,7 @@ import { createWorkflowOverlays } from "@ui/ui_template_overlay"; import { ROUTES } from "@config/config_frontend"; import { navigateTo } from "../A00_Common/router"; import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch"; -import { saveFiles } from "./M01_MasterData_Api_Fetch"; +import { saveFiles, type FileInfo } from "./M01_MasterData_Api_Fetch"; import { discard, isStale, onDraftChange, payload, totalCount } from "./M01_MasterData_Draft"; import { renderRows } from "./M01_MasterData_UI_Rows"; import { @@ -28,10 +28,14 @@ import { fileLabel, loadView, saveView, + setMasterJump, setPendingLogicKey, takePendingLogicKey, + type MasterJump, type Pick, } from "./M01_MasterData_UI_Side"; +import { loadTable } from "./M01_MasterData_UI_Logic_Note"; +import { tl } from "./M01_MasterData_UI_LogicLab_Text"; import { renderTables } from "./M01_MasterData_UI_Tables"; import type { LogicHandle } from "./M01_MasterData_UI_Logic_Page"; import type { ComboHandle } from "./M01_MasterData_UI_Combo"; @@ -227,6 +231,45 @@ function buildPage(): HTMLElement { }); }; + /** 알약 → 마스터 화면 — 찾기 글을 그 키로 두고 열고, 알림줄에 「시험으로 돌아가기」를 둠 */ + const jump = async (target: MasterJump): Promise => { + let group = target.kind === "element" ? target.group : ""; + let file: FileInfo | undefined; + let ref = ""; + if (target.kind === "table") { + const table = await loadTable(target.key); + group = + ["소요량", "계수"].find((g) => side.files(g).some((f) => f.file === table?.file)) ?? ""; + file = side.files(group).find((f) => f.file === table?.file); + ref = target.key; + } else { + const list = side.files(group); + file = list.find((f) => f.file === target.file) ?? list[0]; + ref = target.ref; + } + if (!file) return showToast(L("M01_LoadFailed"), "error"); + side.setActive(null); + openFile({ group, file, sub: "", detail: "", label: group, view: { q: ref, page: 1 } }); + const back = createButton({ label: tl("Pill_Back"), variant: "ghost" }); + back.addEventListener("click", () => { + showNotice([]); + openLabTab(); + }); + showNotice([back]); + if (target.kind === "table") { + const key = target.key; + const seen = new MutationObserver(() => { + const row = body.querySelector(`[data-key="${CSS.escape(key)}"]`); + if (!row) return; + seen.disconnect(); + row.click(); + }); + seen.observe(body, { childList: true, subtree: true }); + window.setTimeout(() => seen.disconnect(), 8000); + } + }; + setMasterJump((target) => void jump(target)); + /* --- 좌측: 컨테이너 --- */ const side = buildSide(openFile, () => openLogicTab(), openLabTab, openComboTab); diff --git a/M01_MasterData/M01_MasterData_UI_Side.ts b/M01_MasterData/M01_MasterData_UI_Side.ts index 1e30922d..4d586ae9 100644 --- a/M01_MasterData/M01_MasterData_UI_Side.ts +++ b/M01_MasterData/M01_MasterData_UI_Side.ts @@ -59,6 +59,16 @@ export function takePendingLogicKey(): string | null { } } +/** 「로직 개선 시험」 알약 → 마스터 화면 — 표(키) 또는 요소(그룹 · 키 · 파일) */ +export type MasterJump = + { kind: "table"; key: string } | { kind: "element"; group: string; ref: string; file?: string }; + +let masterJump: (target: MasterJump) => void = () => {}; +export const setMasterJump = (fn: (target: MasterJump) => void): void => { + masterJump = fn; +}; +export const jumpToMaster = (target: MasterJump): void => masterJump(target); + /** 요소 화면에서 열 것 — 파일 · 하위 거름(구분·세부분류) · 상세구분 · 제목 */ export interface Pick { group: string; @@ -82,6 +92,8 @@ export interface SideHandle { labHost: HTMLElement; /** 그룹의 파일 판본을 다시 받음(저장 뒤) — 돌려받는 것 = 새 목록 */ refresh: (group: string) => Promise; + /** 그룹의 파일 목록(이미 받은 것) */ + files: (group: string) => FileInfo[]; setActive: (id: string | null) => void; /** 좁은 화면에서 사용자가 거름을 다 골랐을 때 부를 것 — 패널을 접어 목록을 드러냄 */ setOnPicked: (fn: () => void) => void; @@ -456,5 +468,6 @@ export function buildSide( const setOnPicked = (fn: () => void): void => { onPicked = fn; }; - return { root, logicHost, comboHost, labHost, refresh, setActive, setOnPicked, filter }; + const files = (group: string): FileInfo[] => lists.get(group as Group) ?? []; + return { root, logicHost, comboHost, labHost, refresh, files, setActive, setOnPicked, filter }; }