diff --git a/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts index c9cb1e618..04549c6a8 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts @@ -38,6 +38,7 @@ import type { Layer } from "./M02_MasterTemplete_Api_Fetch"; import { baseHeads } from "./M02_MasterTemplete_Sangeun_BaseHead"; import { calcSangeun, removedStrip } from "./M02_MasterTemplete_Sangeun_Cover"; import { openSangeunLinePicker } from "./M02_MasterTemplete_Sangeun_Pick"; +import { createSangeunSide, tabOf, type SourceTab } from "./M02_MasterTemplete_Sangeun_Side"; import { baseUnitOf, buildUnitSwap, type SwapHow } from "./M02_MasterTemplete_Sangeun_Unit"; import { dropBase, @@ -63,6 +64,8 @@ export interface SangeunDetailOptions { owner: string | null; readOnly: boolean; onChanged: (dirty: boolean) => void; + /** 752-1 오른쪽 원문 패널 — 단가산출 화면만 켬(일위대가 줄 모달 안 상세는 끔) */ + source?: boolean; } const LIB = { read: "/m02/logic-lib", calc: "/m02/logic-lib" }; @@ -90,6 +93,9 @@ export function mountSangeunDetail( let fail = ""; let extra: LogicInput[] = []; // 더한줄 로직 입력 중 바탕에 없는 것 = 옵션 칸(725-6 · 727-3) const inputsOf = new Map(); + const infoOf = new Map(); // 더한줄 로직 → 원문 탭(752-1) + let extraKeys: string[] = []; + const side = opt.source ? createSangeunSide() : null; let seq = 0; let alive = true; @@ -141,19 +147,17 @@ export function mountSangeunDetail( touch(false); void loadBase(true); } - const baseHint = createText(L("M02_SangeunBaseViews")); - host.replaceChildren( - el("div", { - className: "m02-sk", - attrs: { "data-sangeun-detail": doc.키 ?? "" }, - children: [ - head, - notes, - el("div", { className: "m02-sk__head", children: [views.root, addButton, baseHint] }), - editor, - ], - }), - ); + const main = el("div", { + className: "m02-sk", + attrs: { "data-sangeun-detail": doc.키 ?? "" }, + children: [ + head, + notes, + el("div", { className: "m02-sk__head", children: [views.root, addButton] }), + editor, + ], + }); + host.replaceChildren(side ? side.wrap(main) : main); const opened = (): boolean => (doc.열린입력 ?? []).length > 0; // 바탕 없는 SK 의 지문 = 이름 · 단위 따라 바뀜 — 바탕이 있을 때만 M01 개정으로 봄(725-3) · 둘째 기초 지문(727-2) @@ -185,11 +189,15 @@ export function mountSangeunDetail( .filter((k) => !inputsOf.has(k)) .map((k) => fetchLogic(k).then( - (r) => void inputsOf.set(k, r.logic.입력 ?? []), + (r) => { + inputsOf.set(k, r.logic.입력 ?? []); + infoOf.set(k, tabOf(r.logic)); + }, () => void inputsOf.set(k, []), ), ), ); + extraKeys = keys; const seen = new Set((base?.logic.입력 ?? []).map((i) => i.이름)); extra = keys .flatMap((k) => inputsOf.get(k) ?? []) @@ -306,7 +314,6 @@ export function mountSangeunDetail( if (!alive) return; drawHead(); drawNotes(); - baseHint.hidden = !base; const rows = ans ? rowsOf(ans, bare, doc) : []; const names = new Map((bare ? rowsOf(bare, null, {}) : []).map((r) => [r.cid, r.이름])); const removed = [ @@ -382,6 +389,10 @@ export function mountSangeunDetail( cover: { ho, est, open: lineOpener(hoCtx) }, // 748-2 텍스트 수식 변수 · 값 누름 = 같은 줄 모달 }); drawCalc(); + side?.set([ + ...(base ? [tabOf(base.logic)] : []), + ...extraKeys.flatMap((k) => (k === baseKey() ? [] : infoOf.has(k) ? [infoOf.get(k)!] : [])), + ]); } /** 재료 고름(줄 모달 · 시험 계산 재료 칸) = SK 문서 `고름`(cid → MT) — 둘째 기초 세부 줄은 보기만 */ diff --git a/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Side.ts b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Side.ts new file mode 100644 index 000000000..f75ee3381 --- /dev/null +++ b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Side.ts @@ -0,0 +1,83 @@ +/* ============================================================================= + * M02_MasterTemplete_Sangeun_Side.ts + * 단가산출(SK) 상세 오른쪽 원문 패널(PLAN 752-1 · 요구 588-3) — 일위대가 상세(47-4 · 85-4)와 같은 부품: + * 오른쪽 서랍(`createSidePanels`) + 원문 보기(`createSourceView` · M02 로그인 길 `/m02/logic-lib/source`) + * 기초마다 탭 — 바탕 로직 · 둘째 기초(더한줄 로직) 각각의 출처 · 기초가 하나면 탭 없음 · 없으면 안내 + * 처음 = 넓은 화면만 열림(좁으면 세로 띠로 접힘) · 열림 · 폭 기억 = `m02-sangeun-side` + * 일위대가 줄 모달 안 상세는 안 만듦(부르는 쪽이 `source` 를 안 켬). + * ========================================================================== */ + +import { createSegment } from "@ui/ui_template_segment"; +import { el } from "@ui/ui_template_elements"; +import { t as L } from "@ui/ui_template_locale"; +import { createSidePanels } from "@ui/ui_template_side_panels"; +import { createText } from "@ui/ui_template_text"; +import { createSourceView } from "../M01_MasterData/M01_MasterData_UI_LogicLab_Source"; +import { tc } from "./M02_MasterTemplete_Labor_Text"; + +export interface SourceTab { + label: string; + src: string; +} + +/** 기초 로직 → 원문 탭(「원문번호 이름」 · 출처 글) */ +export const tabOf = (logic: { + 키: string; + 원문번호?: string; + 이름: string; + 출처?: string; +}): SourceTab => ({ + label: `${logic.원문번호 || logic.키} ${logic.이름}`, + src: logic.출처 ?? "", +}); + +/** 이 폭 이상일 때만 처음부터 열림 — 상세 표 여섯 칸이 좁아지지 않게 */ +const OPEN_FROM = 1500; + +export function createSangeunSide(): { + wrap: (main: HTMLElement) => HTMLElement; + set: (tabs: SourceTab[]) => void; +} { + const view = createSourceView({ + path: "/m02/logic-lib/source", + pdf: "/m02/logic-lib/source-pdf", + }); + const host = el("div", { attrs: { "data-sangeun-source": "" } }); + let tabs: SourceTab[] = []; + let at = 0; + const side = createSidePanels({ + prefKey: "m02-sangeun-side", + follow: false, // 일위대가와 같음 — 상세 고정 높이 칸을 채움 · 본문과 따로 스크롤 + defaultOpen: window.innerWidth >= OPEN_FROM ? "source" : null, + panels: [{ id: "source", title: tc("Labor_Source"), body: host }], + onChange: () => paint(), + }); + function paint(): void { + if (side.current() !== "source") return; // 접힘 = 찾지 않음 + if (!tabs.length) + return void host.replaceChildren(createText(L("M02_SangeunSourceNone"), "muted")); + at = Math.min(at, tabs.length - 1); + view.show(tabs[at].src); + const bar = + tabs.length > 1 + ? [ + createSegment({ + options: tabs.map((x, i) => ({ value: String(i), label: x.label })), + value: String(at), + size: 28, + ariaLabel: tc("Labor_Source"), + onChange: (v) => ((at = Number(v)), paint()), + }).root, + ] + : []; + host.replaceChildren(...bar, view.root); + } + return { + wrap: (main) => el("div", { className: "ui-side-panels-row", children: [main, side.root] }), + set: (next) => { + if (JSON.stringify(next) === JSON.stringify(tabs)) return; + tabs = next; + paint(); + }, + }; +} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Sangeun_View.ts b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_View.ts index e31ca48ec..4a3b91584 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Sangeun_View.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_View.ts @@ -18,5 +18,5 @@ export function mountSangeunView( doc: unknown, opt: SangeunViewOptions, ): { getDoc: () => unknown; destroy: () => void } { - return mountSangeunDetail(host, doc, { ...opt, owner: opt.owner ?? null }); + return mountSangeunDetail(host, doc, { ...opt, owner: opt.owner ?? null, source: true }); // 752-1 } diff --git a/resources/tester/test_721_find_pick.py b/resources/tester/test_721_find_pick.py index b81649914..d13084ff0 100644 --- a/resources/tester/test_721_find_pick.py +++ b/resources/tester/test_721_find_pick.py @@ -26,7 +26,7 @@ def _text(path: Path) -> str: def test_721_1_단추_이름과_자리(): detail = _text(M02 / "M02_MasterTemplete_Sangeun_Detail.ts") - assert "children: [views.root, addButton, baseHint]" in detail + assert "children: [views.root, addButton]" in detail # 750-2 「시험 계산 = 바탕」 안내 걷음 assert "M02_SangeunPickBase" not in detail # 727-2 단추 걷음 locale = _text(ROOT / "ui_template" / "ui_template_locale_m2.ts") assert "M02_SangeunPickBase" not in locale diff --git a/resources/tester/test_750_2_views_one_answer.py b/resources/tester/test_750_2_views_one_answer.py index ca3836567..bd23618aa 100644 --- a/resources/tester/test_750_2_views_one_answer.py +++ b/resources/tester/test_750_2_views_one_answer.py @@ -30,3 +30,14 @@ def test_calc_view_answer_hook_skips_logic_lib(): ts = _ts("M01_MasterData/M01_MasterData_UI_Logic_Calc.ts") assert "ctx.onText && !ctx.answer" in ts # 미리보기 안 부름 assert "if (ctx.answer)" in ts and "runCalc" in ts # M01 옛 길 그대로 + + +def test_752_1_source_panel_only_on_sangeun_screen(): + """752-1 원문 패널 — 단가산출 화면만 켬 · 일위대가 줄 모달 안 상세는 끔 · 기초마다 탭 · 좁은 폭 접힘.""" + side = _ts("M02_MasterTemplete/M02_MasterTemplete_Sangeun_Side.ts") + assert "/m02/logic-lib/source" in side and "createSidePanels" in side + assert "OPEN_FROM" in side and "createSegment" in side + assert "source: true" in _ts("M02_MasterTemplete/M02_MasterTemplete_Sangeun_View.ts") + assert "source" not in _ts("M02_MasterTemplete/M02_MasterTemplete_Labor_RowSk.ts") + detail = _ts("M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts") + assert "opt.source ? createSangeunSide()" in detail and "side?.set(" in detail diff --git a/ui_template/ui_template_locale_m2.ts b/ui_template/ui_template_locale_m2.ts index bbc6ebf11..7c9678d3c 100644 --- a/ui_template/ui_template_locale_m2.ts +++ b/ui_template/ui_template_locale_m2.ts @@ -207,10 +207,7 @@ export const ui_locales_m2 = { "Lines not priced — total uses the rest · drop the line or fill its input", ], M02_SangeunScaleOnly: ["열린 입력 — 품은 배율만", "Open inputs — quantity as multiplier only"], - M02_SangeunBaseViews: [ - "시험 계산 · 텍스트 수식 = 바탕 단가산출 기초 + 고친 품 · 단가(뺀 줄 · 더한 줄 빼고)", - "Test calc · formula = base M01 logic + edited qty · price (no dropped · added lines)", - ], + M02_SangeunSourceNone: ["원문 연결 없음 — 바탕 로직 없음", "No source — no base logic"], M02_SangeunWait: ["셈 기다리는 중", "Calculating…"], M02_SangeunNoInputs: [ "바탕 단가산출 기초 없음 — 시험 입력 없음",