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
@@ -237,6 +237,7 @@ export async function mountM01Logic(host: HTMLElement, side: SideHandle): Promis
total: list.total(),
subLabel: L("M01_LaborSub"),
detailLabel: L("M01_LaborDetail"),
restore: true,
onPick: (sub, detail) => {
list.setPick(sub, detail);
if (opened) show(null);
+7 -3
View File
@@ -23,7 +23,7 @@ import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch";
import { saveFiles } from "./M01_MasterData_Api_Fetch";
import { discard, isStale, onDraftChange, payload, totalCount } from "./M01_MasterData_Draft";
import { renderRows } from "./M01_MasterData_UI_Rows";
import { buildSide, fileLabel, type Pick } from "./M01_MasterData_UI_Side";
import { buildSide, fileLabel, loadView, saveView, type Pick } from "./M01_MasterData_UI_Side";
import { renderTables } from "./M01_MasterData_UI_Tables";
import "./M01_MasterData_UI_Style.css";
@@ -85,7 +85,11 @@ function buildPage(): HTMLElement {
const openFile = (pick: Pick): void => {
dispose();
showMode(false);
current = pick;
if (pick.view) {
query = pick.view.q;
search.value = query;
} else saveView(pick.group, { q: query, page: 1 });
current = { ...pick, view: undefined };
const groupTitle = pick.group === pick.label ? [pick.group] : [pick.group, pick.label];
title.textContent = groupTitle.join(" · ");
const view = TABLE_GROUPS.includes(pick.group) ? renderTables : renderRows;
@@ -114,7 +118,7 @@ function buildPage(): HTMLElement {
const pick = current;
try {
const again = (await side.refresh(pick.group)).find((f) => f.file === pick.file.file);
if (again) openFile({ ...pick, file: again });
if (again) openFile({ ...pick, file: again, view: loadView(pick.group) });
} catch (error) {
showToast(error instanceof Error ? error.message : L("M01_LoadFailed"), "error");
}
+3 -2
View File
@@ -6,7 +6,7 @@
import { createButton, el, showToast } from "@ui/ui_template_elements";
import { t as L } from "@ui/ui_template_locale";
import { openPickModal } from "./M01_MasterData_UI_Pick";
import type { Pick } from "./M01_MasterData_UI_Side";
import { saveView, type Pick } from "./M01_MasterData_UI_Side";
import { fetchRows, type Row, type RowsPage } from "./M01_MasterData_Api_Fetch";
import {
addRow,
@@ -92,7 +92,7 @@ const remember = (ref: string | null, name?: string): void => {
export function renderRows(host: HTMLElement, pick: Pick, q: string): () => void {
const { sub, detail, state } = pick;
const file = pick.file.file;
let page = 1;
let page = pick.view?.page ?? 1;
let data: RowsPage | null = null;
let unlinked = false;
@@ -235,6 +235,7 @@ export function renderRows(host: HTMLElement, pick: Pick, q: string): () => void
try {
data = await fetchRows(file, page, SIZE, q, unlinked, sub, detail, state);
for (const [key, name] of Object.entries(data.refs ?? {})) names.set(key, name);
saveView(pick.group, { page: data.page });
paint();
} catch (error) {
showToast(error instanceof Error ? error.message : L("M01_LoadFailed"), "error");
+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();
});
+3 -2
View File
@@ -7,7 +7,7 @@
import { createButton, el, showToast } from "@ui/ui_template_elements";
import { t as L } from "@ui/ui_template_locale";
import { fetchTable, fetchTables, type Row, type TableHead } from "./M01_MasterData_Api_Fetch";
import type { Pick } from "./M01_MasterData_UI_Side";
import { saveView, type Pick } from "./M01_MasterData_UI_Side";
import {
onDraftChange,
peek,
@@ -31,7 +31,7 @@ const LIST_COLS = ["키", "원문번호", "구분", "상세구분", "이름", "
/** 돌려받은 함수 = 이 화면을 걷을 때 부를 해제. */
export function renderTables(host: HTMLElement, pick: Pick, q: string): () => void {
let stop = (): void => {};
const list = (page = 1): void => {
const list = (page = pick.view?.page ?? 1): void => {
stop();
stop = (): void => {};
void showList(host, pick, q, page, (file, key) => {
@@ -68,6 +68,7 @@ async function showList(
showToast(error instanceof Error ? error.message : L("M01_LoadFailed"), "error");
return;
}
saveView(pick.group, { page });
const rows = got.tables.map((t) => {
const tr = el("tr", {
className: "m01-master__table-row",