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:
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user