From 41cdf22d82f8a311e54f182875e9634fb1aec1dc Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 1 Oct 2026 19:46:47 +0900 Subject: [PATCH] =?UTF-8?q?feat(m02-df):=20=EB=8B=A8=EA=B0=80=EC=82=B0?= =?UTF-8?q?=EC=B6=9C=20=ED=9D=90=EB=A6=84=EB=8F=84(=EC=8B=9C=ED=97=98)=20?= =?UTF-8?q?=EB=93=A4=EC=96=B4=EA=B0=80=EB=8A=94=20=EA=B8=B8=20=C2=B7=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=C2=B7=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=EB=82=B4=EC=9A=A9=20=EB=A8=B8=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 시험판 ▾ 목록 「단가산출 흐름도(시험)」 · main.py DataFlow 서버 등록 줄 - 페이지 제목 「단가산출 흐름도」 · 단추 공용 작은 크기(size "sm") · 큰 단추 css 덮기 없앰 - 상세 내용(옛 「고른 상자」) — 머리 왼쪽 「상세 내용」 · 오른쪽 고른 상자 제목 · 알약이면 알약 이름 · 설정 · 본문 · 경고 차례 - 고름 = 상자 · 선 · 알약 — 종류마다 판을 바꿔 끼움(선 = 보내는 곳 · 받는 곳 · 선 규칙 글 · 알약 = 온 길 · 가는 길) - 경고 = 검사 · 계산 글 가운데 고른 것 몫만 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt --- M01_MasterData/M01_MasterData_UI_LogicLab.ts | 4 + M02_MasterTemplete/DataFlow/M02_DF_Entry.css | 75 ++++++- M02_MasterTemplete/DataFlow/M02_DF_Entry.ts | 109 ++++++++--- .../DataFlow/M02_DF_Entry_Detail.ts | 183 ++++++++++++++++++ main.py | 5 + 5 files changed, 342 insertions(+), 34 deletions(-) create mode 100644 M02_MasterTemplete/DataFlow/M02_DF_Entry_Detail.ts diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab.ts b/M01_MasterData/M01_MasterData_UI_LogicLab.ts index c9faeac41..a74a98bb9 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab.ts @@ -41,6 +41,8 @@ import { buildNewLogic } from "./M01_MasterData_UI_LogicLab_New"; import { tn } from "./M01_MasterData_UI_LogicLab_New_Text"; // 단가산출 다이어그램 시험 길 — 폴더째 지우면 이 두 줄도 지움 import { createCostFlowButton } from "../M02_MasterTemplete/CostFlow/M02_CF_Entry"; +// 단가산출 흐름도 시험 길 — 폴더째 지우면 이 두 줄도 지움 +import { createDataFlowButton } from "../M02_MasterTemplete/DataFlow/M02_DF_Entry"; import "./M01_MasterData_UI_Logic_Style.css"; /** 저장 안 한 로직 하나 — origKey null = 새 로직 · row null = 지움 */ @@ -447,6 +449,8 @@ export async function mountM01LogicLab( trialMenu([ // 단가산출 다이어그램 시험 길 — 폴더째 지우면 이 두 줄도 지움 createCostFlowButton(), + // 단가산출 흐름도 시험 길 — 폴더째 지우면 이 두 줄도 지움 + createDataFlowButton(), ]), createButton({ label: tx("Bar_Delete"), variant: "danger", onClick: () => void onDelete() }), discardButton, diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Entry.css b/M02_MasterTemplete/DataFlow/M02_DF_Entry.css index 3b453c20b..0047e4863 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Entry.css +++ b/M02_MasterTemplete/DataFlow/M02_DF_Entry.css @@ -67,12 +67,6 @@ padding: var(--spacing-16) var(--spacing-24) var(--spacing-24); } -.m02-df__big { - min-height: 56px; - padding: 0 var(--spacing-24); - font-size: var(--text-body); -} - /* 시작 화면 */ .m02-df__start { display: grid; @@ -218,3 +212,72 @@ background: none; cursor: pointer; } + +/* 상세 내용 — 머리(왼쪽 「상세 내용」 · 오른쪽 고른 상자 제목 · 알약 이름) · 설정 · 본문 · 경고 */ +.m02-df__detail-head { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: var(--spacing-12); + padding: var(--spacing-8) var(--spacing-12); + border-bottom: 1px solid var(--color-border); +} + +.m02-df__detail-head h3 { + margin: 0; + padding: 0; + border: 0; +} + +.m02-df__detail-name { + display: flex; + flex-wrap: wrap; + align-items: baseline; + justify-content: flex-end; + gap: var(--spacing-8); +} + +.m02-df__detail-pill { + color: var(--color-text-secondary); + font-size: var(--text-body-sm); +} + +.m02-df__detail-row { + display: flex; + flex-direction: column; + gap: var(--spacing-4); + padding: var(--spacing-8) 0; + border-bottom: 1px dashed var(--color-border); +} + +.m02-df__detail-row:last-child { + border-bottom: 0; +} + +.m02-df__detail-row h4 { + margin: 0; + color: var(--color-text-secondary); + font-size: var(--text-body-sm); +} + +.m02-df__detail-warn { + margin: 0; + padding-left: var(--spacing-16); + font-size: var(--text-body-sm); +} + +.m02-df__detail-dl { + display: grid; + grid-template-columns: auto 1fr; + gap: var(--spacing-4) var(--spacing-12); + margin: 0; +} + +.m02-df__detail-dl dd { + margin: 0; +} + +.m02-df__detail-trace { + margin: 0; + padding-left: var(--spacing-16); +} diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Entry.ts b/M02_MasterTemplete/DataFlow/M02_DF_Entry.ts index 7fa2de71a..5f35149da 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Entry.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Entry.ts @@ -1,11 +1,11 @@ /* ============================================================================= * M02_DF_Entry.ts - * 단가산출 다이어그램(시험) — 들어가는 단추 · 화면 전체를 덮는 시험판 페이지 · 시작 화면 · 고치는 화면 · 잇기 - * 모양은 `docs/raw/M01_쉬운단가/원가다이어그램_정함.md` 10 · 12 · 13 · 이 폴더째 지워도 됨 + * 단가산출 흐름도(시험) — 들어가는 단추 · 화면 전체를 덮는 시험판 페이지 · 시작 화면 · 고치는 화면 · 잇기 + * 모양은 `docs/raw/M01_쉬운단가/흐름도_정함.md`(1판 약속 = 원가다이어그램_정함.md) · 이 폴더째 지워도 됨 * (폴더 밖 참조는 M01 LogicLab.ts 두 줄 + 서버 등록 한 줄). DataFlow 의 다른 파일은 import() 로 늦게. - * 잇기: 열기(cfGet · 견본 sampleLoad) → 입력 값(cfValues) → renderDiagram · 고른 상자 판(종류별) · renderSummary - * → 바뀔 때마다 기록(객체 그대로) · evalLogic → 저장 cfSave(서버 원가와 화면 대조) · 지우기 · 실행 취소 · Ctrl+Z · Ctrl+S. - * 바깥 페이지 하나만 세로로 밀림(안쪽 스크롤 없음). + * 잇기: 열기 → 입력 값(cfValues) → renderDiagram · 「상세 내용」(상자 · 알약 · 선 고름마다 판을 바꿔 끼움 — + * M02_DF_Entry_Detail) · renderSummary → 바뀔 때마다 기록(객체 그대로) · evalLogic → 저장(서버 대조) · 지우기 · + * 실행 취소 · Ctrl+Z · Ctrl+S. 단추 · 칸은 공용 작은 크기. 바깥 페이지 하나만 세로로 밀림. * ========================================================================== */ import { currentLanguageIndex } from "@ui/ui_template_locale"; @@ -23,6 +23,14 @@ import { fetchLogics } from "../../M01_MasterData/M01_MasterData_UI_Logic_Api"; import type { CfListItem, CfQuery } from "./M02_DF_Api"; import type { CfEvalResult, CfValues } from "./M02_DF_Eval"; import type { CfKind, CfLogic, CfPort } from "./M02_DF_Model"; +import { + detailFrame, + drawHead, + drawWarnings, + lineBody, + pillBody, + type DfPick, +} from "./M02_DF_Entry_Detail"; import "./M02_DF_Entry.css"; /** DataFlow 의 다른 파일 — 페이지를 열 때 한 번에 불러옴(엔진이 M01 첫 묶음에 안 들어가게) */ @@ -37,7 +45,8 @@ const loadMods = () => import("./M02_DF_Summary"), import("./M02_DF_Sample"), import("./M02_DF_InputPanel"), - ]).then(([model, ev, api, diagram, func, output, summary, sample, input]) => ({ + import("./M02_DF_Rule"), + ]).then(([model, ev, api, diagram, func, output, summary, sample, input, rule]) => ({ model, ev, api, @@ -47,12 +56,13 @@ const loadMods = () => summary, sample, input, + rule, })); type Mods = Awaited>; const TEXT = { - Open: ["단가산출 다이어그램(시험)", "Cost diagram (trial)"], - Title: ["단가산출 다이어그램", "Cost diagram"], + Open: ["단가산출 흐름도(시험)", "Cost flow (trial)"], + Title: ["단가산출 흐름도", "Cost flow"], Trial: ["시험판 — 임시 폴더에 저장", "Trial — saved in a temporary folder"], Close: ["닫기", "Close"], Loading: ["불러오는 중", "Loading"], @@ -69,8 +79,6 @@ const TEXT = { Delete: ["지우기", "Delete"], Unsaved: ["저장 안 함", "Not saved"], Diagram: ["다이어그램", "Diagram"], - Box: ["고른 상자", "Chosen box"], - PickBox: ["다이어그램에서 상자를 고르면 여기에 그 상자 판이 보임", "Choose a box in the diagram"], Summary: ["원가", "Cost"], Color: ["상자 색", "Box colour"], NoName: ["로직 이름을 넣어 주세요", "Enter a logic name"], @@ -164,7 +172,7 @@ async function fetchValues(M: Mods, lg: CfLogic): Promise<{ values: CfValues; er /** M01 단가산출 막대의 다른 시험판 단추 옆에 놓는 단추 — 누르면 시험판 페이지 */ export function createDataFlowButton(): HTMLButtonElement { - return createButton({ label: te("Open"), variant: "filled", onClick: openDataFlow }); + return createButton({ label: te("Open"), variant: "filled", size: "sm", onClick: openDataFlow }); } function openDataFlow(): void { @@ -181,6 +189,7 @@ function openDataFlow(): void { const close = createButton({ label: te("Close"), variant: "ghost", + size: "sm", onClick: () => void (async () => { if (await mayLeave()) paper ? void showStart() : shut(); @@ -317,7 +326,8 @@ function openDataFlow(): void { let lg = first; let savedLg: CfLogic | null = saved ? first : null; let stack: CfLogic[] = []; - let selected: string | null = null; + let pick: DfPick = null; + const pickedBox = (): string | null => (pick?.kind === "box" ? pick.id : null); let values: CfValues = {}; let valueSig = ""; let notes: string[] = []; @@ -327,14 +337,14 @@ function openDataFlow(): void { const unsaved = createTag(te("Unsaved"), "warning"); const note = el("p", { className: "m02-df__note", attrs: { role: "status" } }); const diagramHost = el("div"); - const pickHint = muted(te("PickBox")); // 상자마다 색 바꾸기(정함 점검 9) — 고른 상자 판 머리에 하나 const colorBox = el("input", { className: "m02-df__color-input", attrs: { type: "color", "aria-label": te("Color") }, }); colorBox.addEventListener("change", () => { - if (selected) edit(M.model.setColor(lg, selected, colorBox.value)); + const id = pickedBox(); + if (id) edit(M.model.setColor(lg, id, colorBox.value)); }); const colorRow = el("label", { className: "m02-df__color", @@ -345,6 +355,11 @@ function openDataFlow(): void { const outHost = el("div"); const inHost = el("div"); const summaryHost = el("div"); + const lineHost = el("div"); + const pillHost = el("div"); + const detail = detailFrame(); + detail.settings.append(colorRow); + detail.body.append(inHost, funcHost, outHost, lineHost, pillHost); const evalNow = (): CfEvalResult => { try { @@ -403,7 +418,16 @@ function openDataFlow(): void { const diagramOn = { select: (id: string | null): void => { - selected = id; + pick = !id + ? null + : lg.links.some((l) => l.id === id) + ? { kind: "link", id } + : { kind: "box", id }; + drawPanel(); + }, + /** 알약 고름 — 다이어그램(sub8)이 알려 주면 상세 내용이 알약 판으로 */ + pill: (p: { box: string; name: string; dir: "in" | "out" } | null): void => { + pick = p ? { kind: "pill", ...p } : null; drawPanel(); }, move: (id: string, x: number, y: number): void => edit(M.model.moveBox(lg, id, x, y)), @@ -416,19 +440,50 @@ function openDataFlow(): void { unlink: (id: string): void => editFromDiagram(M.model.unlink(lg, id)), add: (kind: CfKind, x: number, y: number): void => { const got = M.model.addBox(lg, kind, x, y); - selected = got.box.id; + pick = { kind: "box", id: got.box.id }; editFromDiagram(got.lg); }, remove: (id: string): void => { - if (selected === id) selected = null; + if (pickedBox() === id) pick = null; editFromDiagram(M.model.removeBox(lg, id)); }, }; const drawPanel = (): void => { - const box = lg.boxes.find((b) => b.id === selected); - pickHint.hidden = !!box; + const boxId = pickedBox(); + if (boxId && !lg.boxes.some((b) => b.id === boxId)) pick = null; + const box = lg.boxes.find((b) => b.id === pickedBox()); + drawHead(detail, lg, pick); colorRow.hidden = !box; + lineHost.hidden = pick?.kind !== "link"; + pillHost.hidden = pick?.kind !== "pill"; + if (pick?.kind === "link") { + const linkId = pick.id; + const rule = lg.links.find((l) => l.id === linkId)?.rule; + let text: string | null = null; + try { + text = rule ? M.rule.ruleLabel(rule) : null; + } catch { + text = rule ? rule.kind : null; + } + lineBody(lineHost, lg, pick.id, text); + } + if (pick?.kind === "pill") { + const port = { box: pick.box, name: pick.name }; + pillBody( + pillHost, + lg, + () => M.model.traceBack(lg, port), + () => M.model.traceForward(lg, port), + ); + } + let checks: string[] = []; + try { + checks = M.ev.checkLogic(lg, values); + } catch { + checks = []; + } + drawWarnings(detail, lg, pick, [...checks, ...(result?.errors ?? [])]); if (box && /^#[0-9a-f]{6}$/i.test(box.color)) colorBox.value = box.color; funcHost.hidden = box?.kind !== "function"; outHost.hidden = box?.kind !== "output"; @@ -445,6 +500,8 @@ function openDataFlow(): void { unsaved.hidden = lg === savedLg; undo.disabled = !stack.length; deleteButton.hidden = !lg.key; + const selected = + pick?.kind === "box" || pick?.kind === "link" ? pick.id : (pick?.box ?? null); M.diagram.renderDiagram(diagramHost, lg, diagramOn, { values: result.ports, selected }); drawPanel(); // 「고칠 것」 검사가 받은 입력 값으로 하도록 값도 넘김 — Summary 가 opts.values 를 받으면 쓰임 @@ -517,10 +574,11 @@ function openDataFlow(): void { } }; - const undo = createButton({ label: te("Undo"), variant: "ghost", onClick: doUndo }); + const undo = createButton({ label: te("Undo"), variant: "ghost", size: "sm", onClick: doUndo }); const deleteButton = createButton({ label: te("Delete"), variant: "danger", + size: "sm", onClick: () => void doDelete(), }); strip.replaceChildren( @@ -531,7 +589,7 @@ function openDataFlow(): void { className: "m02-df__actions", children: [ undo, - createButton({ label: te("Save"), onClick: () => void save() }), + createButton({ label: te("Save"), size: "sm", onClick: () => void save() }), deleteButton, ], }), @@ -544,10 +602,7 @@ function openDataFlow(): void { part(te("Diagram"), "m02-df__diagram", [diagramHost]), el("div", { className: "m02-df__lower", - children: [ - part(te("Box"), "m02-df__box", [pickHint, colorRow, inHost, funcHost, outHost]), - part(te("Summary"), "m02-df__summary", [summaryHost]), - ], + children: [detail.root, part(te("Summary"), "m02-df__summary", [summaryHost])], }), ], }), @@ -573,7 +628,5 @@ function part(title: string, className: string, children: HTMLElement[]): HTMLEl } function bigButton(label: string, onClick: () => void): HTMLButtonElement { - const button = createButton({ label, variant: "filled", onClick }); - button.classList.add("m02-df__big"); - return button; + return createButton({ label, variant: "filled", size: "sm", onClick }); } diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Entry_Detail.ts b/M02_MasterTemplete/DataFlow/M02_DF_Entry_Detail.ts new file mode 100644 index 000000000..be13d2dd1 --- /dev/null +++ b/M02_MasterTemplete/DataFlow/M02_DF_Entry_Detail.ts @@ -0,0 +1,183 @@ +/* ============================================================================= + * M02_DF_Entry_Detail.ts + * 단가산출 흐름도 — 「상세 내용」 칸 틀(정함 9): 머리(왼쪽 「상세 내용」 · 오른쪽 고른 상자 제목 · 알약이면 그 옆 알약 이름) + * · 설정 · 본문 · 경고 차례. 상자 · 알약 · 선 무엇을 골라도 바뀜 — 본문은 Entry 가 종류별 판을 끼움. + * 선 · 알약 본문은 규칙 판 · 알약 판(sub2)이 오기 전까지 이 파일이 흐름 줄(보내는 곳 → 받는 곳 · 출처 사슬 · 가는 곳)만 보임. + * ========================================================================== */ + +import { currentLanguageIndex } from "@ui/ui_template_locale"; +import { el } from "@ui/ui_template_elements"; +import type { CfLogic, CfPort, CfStep } from "./M02_DF_Model"; + +const TEXT = { + Detail: ["상세 내용", "Details"], + Settings: ["설정", "Settings"], + Body: ["본문", "Content"], + Warn: ["경고", "Warnings"], + WarnNone: ["경고 없음", "No warnings"], + PickSome: [ + "다이어그램에서 상자 · 알약 · 선을 고르면 여기에 그 상세 내용이 보임", + "Choose a box, pill or line in the diagram", + ], + Line: ["선", "Line"], + From: ["보내는 곳", "From"], + To: ["받는 곳", "To"], + Rule: ["선 규칙", "Line rule"], + RuleNone: ["없음 — 값을 그대로 보냄", "None — sends the value as is"], + Back: ["온 길(출처)", "Came from"], + Forward: ["가는 길", "Goes to"], + NoTrace: ["아직 따라갈 수 없음", "Can't trace yet"], + Nowhere: ["이어진 곳 없음", "Not connected"], + In: ["받는 알약", "Incoming pill"], + Out: ["내보내는 알약", "Outgoing pill"], +} as const; + +const tt = (key: keyof typeof TEXT): string => + (TEXT[key][currentLanguageIndex as 0 | 1] ?? TEXT[key][0]) as string; + +/** 고른 것 — 상자 · 선 · 알약 */ +export type DfPick = + | { kind: "box"; id: string } + | { kind: "link"; id: string } + | { kind: "pill"; box: string; name: string; dir: "in" | "out" } + | null; + +export interface DetailParts { + root: HTMLElement; + title: HTMLElement; + pill: HTMLElement; + hint: HTMLElement; + settings: HTMLElement; + body: HTMLElement; + warn: HTMLElement; +} + +const row = (label: string, host: HTMLElement): HTMLElement => + el("div", { + className: "m02-df__detail-row", + children: [el("h4", { text: label }), host], + }); + +/** 상세 내용 칸 — 머리 + 설정 · 본문 · 경고 */ +export function detailFrame(): DetailParts { + const title = el("strong", { className: "m02-df__detail-title" }); + const pill = el("span", { className: "m02-df__detail-pill" }); + const hint = el("p", { className: "m02-df__muted", text: tt("PickSome") }); + const settings = el("div", { className: "m02-df__detail-settings" }); + const body = el("div", { className: "m02-df__detail-body" }); + const warn = el("ul", { className: "m02-df__detail-warn" }); + const root = el("section", { + className: "m02-df__part m02-df__detail", + children: [ + el("div", { + className: "m02-df__detail-head", + children: [ + el("h3", { text: tt("Detail") }), + el("div", { className: "m02-df__detail-name", children: [title, pill] }), + ], + }), + el("div", { + className: "m02-df__part-body", + children: [ + hint, + row(tt("Settings"), settings), + row(tt("Body"), body), + row(tt("Warn"), warn), + ], + }), + ], + }); + return { root, title, pill, hint, settings, body, warn }; +} + +const boxName = (lg: CfLogic, id: string): string => lg.boxes.find((b) => b.id === id)?.name ?? id; +const portText = (lg: CfLogic, p: CfPort): string => `${boxName(lg, p.box)} · ${p.name}`; + +/** 머리 · 빈 자리 — 고른 것에 맞춤 · 설정 · 경고 줄은 Entry 가 채움 */ +export function drawHead(d: DetailParts, lg: CfLogic, pick: DfPick): void { + d.hint.hidden = !!pick; + for (const r of [d.settings, d.body, d.warn]) (r.parentElement as HTMLElement).hidden = !pick; + const link = pick?.kind === "link" ? lg.links.find((l) => l.id === pick.id) : undefined; + d.title.textContent = + pick?.kind === "box" + ? boxName(lg, pick.id) + : pick?.kind === "pill" + ? boxName(lg, pick.box) + : link + ? `${tt("Line")} · ${boxName(lg, link.from.box)} → ${boxName(lg, link.to.box)}` + : ""; + d.pill.textContent = + pick?.kind === "pill" ? `${tt(pick.dir === "in" ? "In" : "Out")} · ${pick.name}` : ""; + d.pill.hidden = pick?.kind !== "pill"; +} + +/** 경고 — 화면 계산 · 검사 글 가운데 고른 것(상자 이름 · 알약 이름)이 든 줄만 */ +export function drawWarnings(d: DetailParts, lg: CfLogic, pick: DfPick, all: string[]): void { + const names: string[] = []; + if (pick?.kind === "box") names.push(`「${boxName(lg, pick.id)}」`); + if (pick?.kind === "pill") names.push(`「${boxName(lg, pick.box)}」`); + if (pick?.kind === "link") { + const l = lg.links.find((x) => x.id === pick.id); + if (l) names.push(`「${boxName(lg, l.from.box)}」`, `「${boxName(lg, l.to.box)}」`); + } + const mine = [...new Set(all)].filter( + (w) => names.some((n) => w.includes(n)) && (pick?.kind !== "pill" || w.includes(pick.name)), + ); + d.warn.replaceChildren( + ...(mine.length + ? mine.map((w) => el("li", { text: w })) + : [el("li", { className: "m02-df__muted", text: tt("WarnNone") })]), + ); +} + +/** 선 본문 — 보내는 곳 → 받는 곳 · 선 규칙 글(규칙 판이 오면 그쪽이 그림) */ +export function lineBody( + host: HTMLElement, + lg: CfLogic, + id: string, + ruleText: string | null, +): void { + const l = lg.links.find((x) => x.id === id); + if (!l) return void host.replaceChildren(); + host.replaceChildren( + el("dl", { + className: "m02-df__detail-dl", + children: [ + el("dt", { text: tt("From") }), + el("dd", { text: portText(lg, l.from) }), + el("dt", { text: tt("To") }), + el("dd", { text: portText(lg, l.to) }), + el("dt", { text: tt("Rule") }), + el("dd", { text: ruleText ?? tt("RuleNone") }), + ], + }), + ); +} + +/** 알약 본문 — 온 길(출처 사슬) · 가는 길 · 흐름 따라가기(Model traceBack · traceForward)가 아직이면 한 줄 */ +export function pillBody( + host: HTMLElement, + lg: CfLogic, + back: () => CfStep[], + forward: () => CfStep[], +): void { + const list = (steps: () => CfStep[]): HTMLElement => { + let got: CfStep[]; + try { + got = steps(); + } catch { + return el("p", { className: "m02-df__muted", text: tt("NoTrace") }); + } + if (!got.length) return el("p", { className: "m02-df__muted", text: tt("Nowhere") }); + return el("ol", { + className: "m02-df__detail-trace", + children: got.map((s) => el("li", { text: portText(lg, { box: s.box, name: s.port }) })), + }); + }; + host.replaceChildren( + el("h5", { text: tt("Back") }), + list(back), + el("h5", { text: tt("Forward") }), + list(forward), + ); +} diff --git a/main.py b/main.py index a499660bc..33befc4bb 100644 --- a/main.py +++ b/main.py @@ -643,6 +643,11 @@ app.include_router( __import__("M02_MasterTemplete.CostFlow.M02_CF_Router", fromlist=["router"]).router, dependencies=system_admin_only, ) +# 단가산출 흐름도 시험 길 — 폴더째 지우면 이 줄도 지움 +app.include_router( + __import__("M02_MasterTemplete.DataFlow.M02_DF_Router", fromlist=["router"]).router, + dependencies=system_admin_only, +) # 마스터 템플릿 — 시스템 층은 시스템 관리자만 · 층(회사·개인·프로젝트)·도면 길은 로그인만(권한은 길 안에서). app.include_router(m02_master_template_router, dependencies=system_admin_only) app.include_router(m02_layers_router, dependencies=protected)