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