From 1b7e6a322b4e3510535a443788e1666e74b80e5d Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 20 Sep 2026 20:12:34 +0900 Subject: [PATCH] =?UTF-8?q?feat(M01):=20=EA=B1=B0=EB=A6=84=20=EC=BB=A8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=84=88=20=ED=8E=BC=EC=B9=98=EB=A9=B4=20?= =?UTF-8?q?=EC=A0=84=EC=B2=B4=20=C2=B7=20=EB=8B=AB=EC=95=84=EB=8F=84=20?= =?UTF-8?q?=EA=B1=B0=EB=A6=84=C2=B7=EC=B0=BE=EA=B8=B0=C2=B7=EC=AA=BD=20?= =?UTF-8?q?=EC=9C=A0=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 인력·기계·소요량·계수·로직 펼치면 전체(또는 닫기 전 거름)가 바로 열림 - 닫을 때는 화면 그대로 · 컨테이너마다 찾기 글·쪽을 sessionStorage 에 따로 기억 Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_016WRFJmmhPi4exAzHgPZHMT --- .../M01_MasterData_UI_Logic_Page.ts | 1 + M01_MasterData/M01_MasterData_UI_Page.ts | 10 +++- M01_MasterData/M01_MasterData_UI_Rows.ts | 5 +- M01_MasterData/M01_MasterData_UI_Side.ts | 59 ++++++++++++++++++- M01_MasterData/M01_MasterData_UI_Tables.ts | 5 +- 5 files changed, 71 insertions(+), 9 deletions(-) diff --git a/M01_MasterData/M01_MasterData_UI_Logic_Page.ts b/M01_MasterData/M01_MasterData_UI_Logic_Page.ts index a6d98903..92589788 100644 --- a/M01_MasterData/M01_MasterData_UI_Logic_Page.ts +++ b/M01_MasterData/M01_MasterData_UI_Logic_Page.ts @@ -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); diff --git a/M01_MasterData/M01_MasterData_UI_Page.ts b/M01_MasterData/M01_MasterData_UI_Page.ts index 08799466..79e47e04 100644 --- a/M01_MasterData/M01_MasterData_UI_Page.ts +++ b/M01_MasterData/M01_MasterData_UI_Page.ts @@ -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"); } diff --git a/M01_MasterData/M01_MasterData_UI_Rows.ts b/M01_MasterData/M01_MasterData_UI_Rows.ts index 439f8f64..07f5b564 100644 --- a/M01_MasterData/M01_MasterData_UI_Rows.ts +++ b/M01_MasterData/M01_MasterData_UI_Rows.ts @@ -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"); diff --git a/M01_MasterData/M01_MasterData_UI_Side.ts b/M01_MasterData/M01_MasterData_UI_Side.ts index ebf58894..e9ec3b65 100644 --- a/M01_MasterData/M01_MasterData_UI_Side.ts +++ b/M01_MasterData/M01_MasterData_UI_Side.ts @@ -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): 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 = { 유가전력: "유가·전력" }; @@ -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(); @@ -81,6 +112,8 @@ export function buildSide(onOpen: (pick: Pick) => void, onLogic: () => void): Si // 하위 거름은 서버가 준 목록(파일 머리에 등록된 구분·세부분류) — 새 조사가 늘어도 화면은 그대로 const kinds = new Map(); const states = new Map(); + const runs = new Map 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> = { 인력: { 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(); }); diff --git a/M01_MasterData/M01_MasterData_UI_Tables.ts b/M01_MasterData/M01_MasterData_UI_Tables.ts index 2e675a0a..394899a4 100644 --- a/M01_MasterData/M01_MasterData_UI_Tables.ts +++ b/M01_MasterData/M01_MasterData_UI_Tables.ts @@ -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",