feat(M01): 거름 컨테이너 펼치면 전체 · 닫아도 거름·찾기·쪽 유지

- 인력·기계·소요량·계수·로직 펼치면 전체(또는 닫기 전 거름)가 바로 열림
- 닫을 때는 화면 그대로 · 컨테이너마다 찾기 글·쪽을 sessionStorage 에 따로 기억

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WRFJmmhPi4exAzHgPZHMT
This commit is contained in:
2026-09-20 20:12:34 +09:00
co-authored by Claude Sonnet 5
parent 4b1eeec524
commit 1b7e6a322b
5 changed files with 71 additions and 9 deletions
+57 -2
View File
@@ -11,6 +11,30 @@ import { t as L } from "@ui/ui_template_locale";
import { fetchFiles, fetchSubsAll, type FileInfo, type SubInfo } from "./M01_MasterData_Api_Fetch";
import { renderTree, type MakeRow } from "./M01_MasterData_UI_Tree";
/** 컨테이너마다 기억하는 화면 상태 — 찾기 글 · 쪽 (거름은 `m01.filter.*`) */
export interface View {
q: string;
page: number;
}
const viewKey = (group: string): string => `m01.view.${group}`;
export function loadView(group: string): View {
try {
return { q: "", page: 1, ...JSON.parse(sessionStorage.getItem(viewKey(group)) ?? "{}") };
} catch {
return { q: "", page: 1 };
}
}
export function saveView(group: string, patch: Partial<View>): void {
try {
sessionStorage.setItem(viewKey(group), JSON.stringify({ ...loadView(group), ...patch }));
} catch {
/* 기억 못 해도 화면은 됨 */
}
}
/** 요소 화면에서 열 것 — 파일 · 하위 거름(구분·세부분류) · 상세구분 · 제목 */
export interface Pick {
group: string;
@@ -20,6 +44,8 @@ export interface Pick {
/** 인력 상태 거름 · 비면 전체 */
state?: string;
label: string;
/** 다시 펼칠 때만 — 닫기 전 찾기 글 · 쪽 */
view?: View;
}
export interface SideHandle {
@@ -49,6 +75,8 @@ export interface FilterArgs {
states?: string[];
stateLabel?: string;
onPick: (sub: string, detail: string, label: string, state: string) => void;
/** 만들자마자 고른 거름으로 한 번 부름 */
restore?: boolean;
}
const MATERIAL_NAMES: Record<string, string> = { 유가전력: "유가·전력" };
@@ -73,6 +101,9 @@ const GROUP_TITLE = {
type Group = keyof typeof GROUP_TITLE;
const GROUPS = Object.keys(GROUP_TITLE) as Group[];
const LEAF: Group[] = ["환율", "요율"];
/** 거름이 있는 컨테이너 — 펼치면 「전체」(또는 닫기 전 거름) 이 바로 열림 */
const FILTERED: Group[] = ["인력", "기계", "소요량", "계수"];
const LOGIC_ID = "로직|";
export function buildSide(onOpen: (pick: Pick) => void, onLogic: () => void): SideHandle {
const lists = new Map<Group, FileInfo[]>();
@@ -81,6 +112,8 @@ export function buildSide(onOpen: (pick: Pick) => void, onLogic: () => void): Si
// 하위 거름은 서버가 준 목록(파일 머리에 등록된 구분·세부분류) — 새 조사가 늘어도 화면은 그대로
const kinds = new Map<Group, SubInfo[]>();
const states = new Map<Group, string[]>();
const runs = new Map<string, () => void>();
let restoring = false;
const setActive = (id: string | null): void => {
for (const [key, button] of buttons) button.classList.toggle("is-active", key === id);
@@ -169,6 +202,7 @@ export function buildSide(onOpen: (pick: Pick) => void, onLogic: () => void): Si
s,
);
};
runs.set(a.id, run);
sub.select.addEventListener("change", () => {
if (detail) {
const details = a.subs.find((k) => k.name === sub.select.value)?.details ?? [];
@@ -189,6 +223,7 @@ export function buildSide(onOpen: (pick: Pick) => void, onLogic: () => void): Si
sub.select.dispatchEvent(new Event("change"));
},
);
if (a.restore) run();
return el("div", {
children: [
all,
@@ -210,7 +245,15 @@ export function buildSide(onOpen: (pick: Pick) => void, onLogic: () => void): Si
const body = bodies.get(group);
if (!body) return;
const open = (file: FileInfo, sub: string, label: string, detail = "", state = ""): void =>
onOpen({ group, file, sub, detail, state, label });
onOpen({
group,
file,
sub,
detail,
state,
label,
view: restoring ? loadView(group) : undefined,
});
const one = files[0];
const dropped: Partial<Record<Group, { label: string; detail?: string }>> = {
인력: { label: L("M01_LaborSub"), detail: L("M01_LaborDetail") },
@@ -310,13 +353,25 @@ export function buildSide(onOpen: (pick: Pick) => void, onLogic: () => void): Si
onOpen({ group, file: one, sub: "", detail: "", label: L(GROUP_TITLE[group]) });
});
}
if (FILTERED.includes(group)) {
root.querySelector(".ui-collapsible__title")?.addEventListener("click", () => {
if (!root.classList.contains("is-collapsed")) return; // 닫을 때는 그대로 둠
restoring = true;
try {
runs.get(`${group}|`)?.();
} finally {
restoring = false;
}
});
}
return root;
});
const logicHost = el("div", { className: "m01-side__items" });
const logic = section(L("M01_GroupLogic"), logicHost);
logic.querySelector(".ui-collapsible__title")?.addEventListener("click", () => {
setActive(null);
if (!logic.classList.contains("is-collapsed")) return; // 닫을 때는 그대로 둠
setActive(buttons.has(LOGIC_ID) ? LOGIC_ID : null);
onLogic();
});