feat(z01): 열 제목 눌러 차례 — 누르기 전엔 표시 없음(원본 차례) · 오름→내림→원본 · 세우기는 서버 몫(sort·desc 보냄 · 서버가 거절하면 누른 표시 지움) · 제목은 서버가 준 sortable 열만 눌림(눌렀는데 그대로인 화면 막음)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
This commit is contained in:
@@ -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<string, string> => ({
|
||||
|
||||
export function fetchBasePrices(kind: BasePriceKind, query: RowsQuery): Promise<MasterRows> {
|
||||
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<MasterRows>(`/master-data/base-prices/${kind}?${params}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string, string>;
|
||||
|
||||
@@ -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<void> {
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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},
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user