Files
Aislo/M01_MasterData/M01_MasterData_UI_LogicLab_Side.ts
T
eomsangdonandClaude Opus 5.5 1ca99ed3ae fix(M01): 로직 상세 오른쪽 두 패널 공용 접기 카드로 — 제목 행 누름 · 캐럿 · 하나만 열림
- 흐름도 · 원문 = ui-collapsible 카드(M02 면적산출 오른쪽 패널 꼴) · attachCollapsible 토글 위에 하나만 열림 규칙
- 따로 만든 원문 토글 단추 · 얇은 띠 제거 · 흐름도 패널 자체 접기 단추는 M01 에서 숨김
- 둘 다 접힘 = 제목 행 둘(칸 140px) · 취향 기억(m01-logic-panel) 그대로
- 44-4 화면 시험 카드 꼴로 고침

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NPJGtfXo6GMbcpiboQC4uK
2026-10-05 12:25:12 +09:00

199 lines
7.1 KiB
TypeScript

/* =============================================================================
* M01_MasterData_UI_LogicLab_Side.ts
* 로직 상세 오른쪽 두 패널 — 「흐름도」(다이어그램 · 상세 내용 = M02_DF_Panel) · 「원문」(품셈 본문 절)
* 공용 접기 카드(`ui-collapsible` · 제목 행 누름 · 캐럿) 두 장 — attachCollapsible 토글 위에 하나만 열림 규칙
* 둘 다 접기 됨 · 접힌 카드 = 제목 행만
* 열린 패널은 계정 취향(`m01-logic-panel`)으로 기억 · 원문은 출처 글로 서버가 찾음(`GET /m01/logic/source`)
* ========================================================================== */
import { currentLanguageIndex } from "@ui/ui_template_locale";
import { attachCollapsible } from "@ui/ui_template_collapsible";
import { el } from "@ui/ui_template_elements";
import { API_BASE_URL } from "@config/config_frontend";
import { readStateRaw, writeStateRaw } from "../A00_Common/b_page_state";
import type { SplitHandle } from "./M01_MasterData_UI_LogicLab_Flow";
import "../M02_MasterTemplete/DataFlow/M02_DF_Panel.css";
import "./M01_MasterData_UI_LogicLab_Side.css";
const TEXT = {
Flow: ["흐름도", "Flow"],
Source: ["원문", "Source"],
Loading: ["원문 찾는 중", "Looking up the source"],
None: ["원문 연결 없음", "No linked source"],
From: ["출처", "Source"],
NoSource: ["출처 없음", "No source"],
Near: ["{v} 절이 없어 위 절", "No {v} section — showing the parent"],
} as const;
const ts = (key: keyof typeof TEXT, v = ""): string =>
TEXT[key][currentLanguageIndex === 1 ? 1 : 0].replace("{v}", v);
/** 둘 다 접힘 — 제목 행 둘이 들어가는 칸 너비 */
const FOLDED = 140;
export type PanelPick = "flow" | "source" | "none";
interface Block {
h?: number;
text?: string;
p?: string;
table?: string[][];
}
interface Found {
number: string;
matched: string;
file: string;
blocks: Block[];
}
export interface LogicSide {
/** 흐름도 패널에 넘길 칸 — right = 흐름도 자리 · setRight = 흐름도 패널이 알린 너비 */
flowSplit: SplitHandle;
flowFolded(): boolean;
pick(next: PanelPick): void;
/** 흐름도 패널이 뜨면 접기 함수 · 내리면 null */
bindFlow(fold: ((on: boolean) => void) | null): void;
/** 연 로직의 출처 글 — 원문 패널이 열려 있으면 찾음 */
showSource(src: string): void;
}
const readPick = (): PanelPick => {
const got = readStateRaw("m01-logic-panel");
return got === "source" || got === "none" ? got : "flow";
};
function blockNode(b: Block): HTMLElement {
if (b.table)
return el("div", {
className: "m01-logic__scroll",
children: [
el("table", {
className: "m01-logic__grid m01lab-src__table",
children: [
el("tbody", {
children: b.table.map((r) =>
el("tr", { children: r.map((c) => el("td", { text: c })) }),
),
}),
],
}),
],
});
if (b.h) return el("h4", { className: "m01lab-src__head", text: b.text ?? "" });
return el("p", { className: "m01lab-src__p", text: b.p ?? "" });
}
export function buildLogicSide(split: SplitHandle): LogicSide {
const flowSlot = el("div", { className: "m01lab-side__slot" });
const body = el("div", { className: "m01lab-src__body" });
const title = el("strong", { className: "m02-dfp__title" });
const card = (label: string, inner: HTMLElement): HTMLElement =>
el("section", {
className: "m01lab-side__card ui-collapsible ui-sidebar-section ui-sidebar-card",
children: [el("h3", { className: "ui-collapsible__title", text: label }), inner],
});
const flowCard = card(ts("Flow"), flowSlot);
const sourceCard = card(
ts("Source"),
el("div", { className: "m01lab-src", children: [title, body] }),
);
const side = el("div", {
className: "m01lab-side ui-sidebar-panel",
children: [flowCard, sourceCard],
});
attachCollapsible(side);
// 공용 토글 뒤 — 연 카드만 남기고 다른 카드는 접음(접었으면 둘 다 닫힘)
side.addEventListener("click", (ev) => {
const at = (ev.target as HTMLElement).closest(".ui-collapsible__title")?.parentElement;
if (at !== flowCard && at !== sourceCard) return;
pick(at.classList.contains("is-collapsed") ? "none" : at === flowCard ? "flow" : "source");
});
split.right.replaceChildren(side);
let now = readPick();
let flowPx: number | null = null;
let flowFold: ((on: boolean) => void) | null = null;
let want = "";
let shown: string | null = null;
/** 카드 안팎 여백(좌우 안쪽 + 테두리) — 흐름도 패널 너비에 더함 */
const pad = (): number => {
const s = getComputedStyle(flowCard);
return ["paddingLeft", "paddingRight", "borderLeftWidth", "borderRightWidth"].reduce(
(n, k) => n + (parseFloat(s[k as "paddingLeft"]) || 0),
0,
);
};
/** 칸 너비 — 원문(또는 흐름도가 아직 안 뜸) = 두 칸 비율 · 흐름도 = 패널 너비 + 카드 여백 · 둘 다 접힘 = 제목 행 */
const width = (): void =>
split.setRight(
now === "source" || (now === "flow" && flowPx === null)
? null
: now === "flow"
? (flowPx ?? 0) + pad()
: FOLDED,
);
const load = (): void => {
if (now !== "source" || shown === want) return;
const src = (shown = want);
title.textContent = src || ts("NoSource");
body.replaceChildren(el("p", { className: "m01-logic__muted", text: ts("Loading") }));
const none = (): void =>
body.replaceChildren(
el("p", { className: "m01lab-src__none", text: ts("None") }),
el("p", { className: "m01-logic__muted", text: `${ts("From")} · ${src || "—"}` }),
);
if (!src.trim()) return none();
void fetch(`${API_BASE_URL}/m01/logic/source?${new URLSearchParams({ src })}`, {
credentials: "include",
})
.then((r) => (r.ok ? (r.json() as Promise<{ found: Found[] }>) : { found: [] }))
.catch(() => ({ found: [] as Found[] }))
.then(({ found }) => {
if (shown !== src) return;
if (!found.length) return none();
body.replaceChildren(
...found.flatMap((f) => [
el("p", {
className: "m01-logic__muted m01lab-src__file",
text: f.matched === f.number ? f.file : `${f.file} · ${ts("Near", f.number)}`,
}),
...f.blocks.map(blockNode),
]),
);
});
};
const pick = (next: PanelPick): void => {
if (next !== now) writeStateRaw("m01-logic-panel", next);
now = next;
flowFold?.(now !== "flow"); // 칸을 줄이기 전에 — 흐름도 패널이 접기 전 너비를 재어 둠
flowCard.classList.toggle("is-collapsed", now !== "flow");
sourceCard.classList.toggle("is-collapsed", now !== "source");
width();
load();
};
pick(now);
return {
flowSplit: {
root: split.root,
right: flowSlot,
setRight: (px) => {
flowPx = px;
width();
},
},
flowFolded: () => now !== "flow",
pick,
bindFlow: (f) => {
flowFold = f;
},
showSource: (src) => {
want = src;
load();
},
};
}