From 3a61d4c1586688ce2ea5d74ca2eb1343678d4e59 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 16 Sep 2026 10:59:36 +0900 Subject: [PATCH] =?UTF-8?q?feat(z01):=20=EC=97=B4=20=EC=A0=9C=EB=AA=A9=20?= =?UTF-8?q?=EB=88=8C=EB=9F=AC=20=EC=B0=A8=EB=A1=80=20=E2=80=94=20=EB=88=84?= =?UTF-8?q?=EB=A5=B4=EA=B8=B0=20=EC=A0=84=EC=97=94=20=ED=91=9C=EC=8B=9C=20?= =?UTF-8?q?=EC=97=86=EC=9D=8C(=EC=9B=90=EB=B3=B8=20=EC=B0=A8=EB=A1=80)=20?= =?UTF-8?q?=C2=B7=20=EC=98=A4=EB=A6=84=E2=86=92=EB=82=B4=EB=A6=BC=E2=86=92?= =?UTF-8?q?=EC=9B=90=EB=B3=B8=20=C2=B7=20=EC=84=B8=EC=9A=B0=EA=B8=B0?= =?UTF-8?q?=EB=8A=94=20=EC=84=9C=EB=B2=84=20=EB=AA=AB(sort=C2=B7desc=20?= =?UTF-8?q?=EB=B3=B4=EB=83=84=20=C2=B7=20=EC=84=9C=EB=B2=84=EA=B0=80=20?= =?UTF-8?q?=EA=B1=B0=EC=A0=88=ED=95=98=EB=A9=B4=20=EB=88=84=EB=A5=B8=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=20=EC=A7=80=EC=9B=80)=20=C2=B7=20=EC=A0=9C?= =?UTF-8?q?=EB=AA=A9=EC=9D=80=20=EC=84=9C=EB=B2=84=EA=B0=80=20=EC=A4=80=20?= =?UTF-8?q?sortable=20=EC=97=B4=EB=A7=8C=20=EB=88=8C=EB=A6=BC(=EB=88=8C?= =?UTF-8?q?=EB=A0=80=EB=8A=94=EB=8D=B0=20=EA=B7=B8=EB=8C=80=EB=A1=9C?= =?UTF-8?q?=EC=9D=B8=20=ED=99=94=EB=A9=B4=20=EB=A7=89=EC=9D=8C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq --- Z01_MasterData/Z01_MasterData_Api_Fetch.ts | 9 ++++++- Z01_MasterData/Z01_MasterData_UI_Cells.ts | 17 ++++++++++++ Z01_MasterData/Z01_MasterData_UI_Rows.ts | 30 ++++++++++++++++++++-- Z01_MasterData/Z01_MasterData_UI_Style.css | 9 +++++++ resources/tester/test_z01_master_cells.py | 18 ++++++++++++- 5 files changed, 79 insertions(+), 4 deletions(-) diff --git a/Z01_MasterData/Z01_MasterData_Api_Fetch.ts b/Z01_MasterData/Z01_MasterData_Api_Fetch.ts index e4daa002..7d881ee6 100644 --- a/Z01_MasterData/Z01_MasterData_Api_Fetch.ts +++ b/Z01_MasterData/Z01_MasterData_Api_Fetch.ts @@ -6,7 +6,7 @@ * ========================================================================== */ import { API_BASE_URL } from "@config/config_frontend"; -import type { MasterColumn, OverrideState, TableMeta } from "./Z01_MasterData_UI_Cells"; +import type { MasterColumn, OverrideState, SortOrder, TableMeta } from "./Z01_MasterData_UI_Cells"; export interface MasterTable { id: string; @@ -43,6 +43,8 @@ export interface RowsQuery { page: number; size: number; q: string; + /** 열 제목을 눌렀을 때만 — 없으면 서버가 원본 차례로 줌 */ + sort?: SortOrder | null; } /** 기초단가 다섯 — 고친 값은 원본이 아니라 덮개 파일에 쌓임(품셈이 갱신돼도 안 날아감). */ @@ -68,6 +70,11 @@ const pageParams = (query: RowsQuery): Record => ({ export function fetchBasePrices(kind: BasePriceKind, query: RowsQuery): Promise { const params = new URLSearchParams(pageParams(query)); + // 안 누르면 sort 를 안 보냄 — 서버가 원본 차례로 줌(요율·기계는 차례가 뜻을 담음). + if (query.sort) { + params.set("sort", query.sort.key); + params.set("desc", query.sort.desc ? "1" : "0"); + } return request(`/master-data/base-prices/${kind}?${params}`); } diff --git a/Z01_MasterData/Z01_MasterData_UI_Cells.ts b/Z01_MasterData/Z01_MasterData_UI_Cells.ts index 3fd34c1c..9ca85955 100644 --- a/Z01_MasterData/Z01_MasterData_UI_Cells.ts +++ b/Z01_MasterData/Z01_MasterData_UI_Cells.ts @@ -85,6 +85,18 @@ export function outdatedBanner(sources?: TableSource[]): string[] { }); } +/** 열 제목을 눌러 세운 차례 — null 이면 원본 차례(서버가 sort 없이 주는 그대로). */ +export interface SortOrder { + key: string; + desc: boolean; +} + +/** 같은 열을 누르면 오름 → 내림 → 원본 차례, 다른 열이면 그 열 오름부터. */ +export function nextSort(current: SortOrder | null, key: string): SortOrder | null { + if (current?.key !== key) return { key, desc: false }; + return current.desc ? null : { key, desc: true }; +} + /** 알림은 한 줄로도 목록으로도 옴 — 받은 대로 줄 목록으로. */ export function noticeLines(notice?: string | string[]): string[] { if (!notice) return []; @@ -108,6 +120,11 @@ export interface TableMeta { * 화면이 사전을 가지면 두 벌이 됨. */ notice?: string | string[]; + /** + * 서버가 세워 줄 수 있는 열 — 이 목록에 있는 제목만 눌림. + * 없으면 제목이 안 눌림(원본 차례) — 눌렀는데 그대로인 화면을 안 만들려고 서버에 물어서 켬. + */ + sortable?: string[]; editable?: string[]; /** 열key → 왜 못 고치나 */ locked?: Record; diff --git a/Z01_MasterData/Z01_MasterData_UI_Rows.ts b/Z01_MasterData/Z01_MasterData_UI_Rows.ts index d09f1747..2e8fae42 100644 --- a/Z01_MasterData/Z01_MasterData_UI_Rows.ts +++ b/Z01_MasterData/Z01_MasterData_UI_Rows.ts @@ -16,6 +16,7 @@ import { clampPage, columnTitle, latestOnly, + nextSort, noticeLines, outdatedBanner, pageCount, @@ -25,6 +26,7 @@ import { valueWithUnit, type CellInfo, type MasterColumn, + type SortOrder, } from "./Z01_MasterData_UI_Cells"; const PAGE_SIZE = 50; @@ -69,7 +71,7 @@ function stateText(info: CellInfo, column: MasterColumn): string { export function buildRowsView(): RowsView { let source: RowsSource | null = null; - const state = { page: 1, q: "" }; + const state = { page: 1, q: "", sort: null as SortOrder | null }; let showHidden = false; let last: MasterRows | null = null; let selected: { index: number; key: string } | null = null; @@ -150,13 +152,24 @@ export function buildRowsView(): RowsView { async function load(): Promise { if (!source) return; + const asked = state.sort; grid.classList.add("is-loading"); let data: MasterRows | undefined; try { - data = await fetchLatest(source, { page: state.page, size: PAGE_SIZE, q: state.q }); + data = await fetchLatest(source, { + page: state.page, + size: PAGE_SIZE, + q: state.q, + sort: state.sort, + }); } catch (error) { grid.classList.remove("is-loading"); showToast(error instanceof Error ? error.message : L("Z01_MasterData_LoadFailed"), "error"); + // 서버가 그 열로는 못 세운다고 하면(400) 누른 표시를 지움 — 눌렀는데 안 바뀐 것처럼 두지 않음. + if (asked && state.sort === asked) { + state.sort = null; + draw(); + } return; } // 옛 요청 — 로딩 표시는 뒤 요청 몫이라 그대로 둠. @@ -184,6 +197,18 @@ export function buildRowsView(): RowsView { if (column.label !== column.key) { th.append(el("span", { className: "z01-master__key", text: column.key })); } + // 차례 — 누르기 전에는 아무 표시도 없음(원본 차례라는 것이 보이게 · 2026-09-16 브레인). + if (meta.sortable?.includes(column.key)) { + th.classList.add("is-sortable"); + if (state.sort?.key === column.key) { + th.append(el("span", { className: "z01-master__sort", text: state.sort.desc ? "▼" : "▲" })); + } + th.addEventListener("click", () => { + state.sort = nextSort(state.sort, column.key); + state.page = 1; + void load(); + }); + } return th; } @@ -368,6 +393,7 @@ export function buildRowsView(): RowsView { function show(picked: RowsSource): void { source = picked; state.page = 1; + state.sort = null; title.textContent = picked.title; titleKey.textContent = picked.key; notice.hidden = true; diff --git a/Z01_MasterData/Z01_MasterData_UI_Style.css b/Z01_MasterData/Z01_MasterData_UI_Style.css index d25b3950..a8959587 100644 --- a/Z01_MasterData/Z01_MasterData_UI_Style.css +++ b/Z01_MasterData/Z01_MasterData_UI_Style.css @@ -235,6 +235,15 @@ margin-left: 0; } +.z01-master__grid th.is-sortable { + cursor: pointer; +} + +.z01-master__sort { + margin-left: var(--spacing-4); + color: var(--color-accent); +} + .z01-master__mark { margin-left: var(--spacing-4); cursor: help; diff --git a/resources/tester/test_z01_master_cells.py b/resources/tester/test_z01_master_cells.py index 13983ba2..26cdc294 100644 --- a/resources/tester/test_z01_master_cells.py +++ b/resources/tester/test_z01_master_cells.py @@ -25,9 +25,17 @@ _RUNNER = """ import { writeFileSync } from "node:fs"; import { cellInfo, cellText, clampPage, columnTitle, latestOnly, pageCount, parseCellInput, rowCells, - boldSegments, noticeLines, outdatedBanner, shownColumns, valueWithUnit, + boldSegments, nextSort, noticeLines, outdatedBanner, shownColumns, valueWithUnit, } from "./Z01_MasterData_UI_Cells.js"; +// 열 제목 누르기 — 없음 → 오름 → 내림 → 없음(원본 차례) · 다른 열을 누르면 그 열 오름부터 +const sorts = [ + nextSort(null, "price_krw"), + nextSort({ key: "price_krw", desc: false }, "price_krw"), + nextSort({ key: "price_krw", desc: true }, "price_krw"), + nextSort({ key: "price_krw", desc: true }, "item_code"), +]; + // 표 수준 알림 — 서버가 한 벌로 목록을 줌(첫 줄 「새로 만드는 프로젝트부터」) · 글자에 **굵게** 가 섞임 const notices = [ noticeLines(["여기서 고친 값은 **새로 만드는 프로젝트부터** 쓰임", "법이 정한 값 — 고치면…"]), @@ -168,6 +176,7 @@ writeFileSync(process.argv[2], JSON.stringify({ banners, notices, bolds, + sorts, })); """ @@ -276,3 +285,10 @@ def test_칸_글자와_숨김_열(tmp_path: Path) -> None: ], [{"text": "굵게 없음", "bold": False}], ] + # 세 번째 누르면 원본 차례로 — 누르기 전에는 차례 표시가 아예 없어야 함 + assert got["sorts"] == [ + {"key": "price_krw", "desc": False}, + {"key": "price_krw", "desc": True}, + None, + {"key": "item_code", "desc": False}, + ]