feat(z01): 서버가 주는 거르기 상자 — 표에 filters 가 오면 찾기 칸 옆에 고르기 상자를 세우고 고른 값만 key=value 로 보냄
- 이름·선택지·줄 수·기본값도, 어느 줄을 거를지 판정도 서버 것(화면은 판정 안 함 · 2026-09-17 브레인) - 안 골랐으면 서버 default 를 보임 — 첫 선택지를 보이면 「전부」로 보이며 걸러진 목록이 뜸 - 고르면 1쪽부터 · 다른 표로 가면 고른 것 비움 · filters 없는 표(산림 등)는 상자 없음 - 화면 확인: 진짜 건설 공종 축 위에 가짜 filters 얹어 기본 「임도가 쓰는 것」 → 「전부」 전환 · 산림 상자 없음 - 시험: 기본값 = 서버 default(첫 선택지 아님) · 고르면 고른 값 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
This commit is contained in:
@@ -51,6 +51,8 @@ export interface RowsQuery {
|
|||||||
q: string;
|
q: string;
|
||||||
/** 열 제목을 눌렀을 때만 — 없으면 서버가 원본 차례로 줌 */
|
/** 열 제목을 눌렀을 때만 — 없으면 서버가 원본 차례로 줌 */
|
||||||
sort?: SortOrder | null;
|
sort?: SortOrder | null;
|
||||||
|
/** 사람이 고른 거르기만(`TableMeta.filters`) — 안 고른 것은 서버 기본값 */
|
||||||
|
filters?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -87,6 +89,7 @@ export function fetchBasePrices(kind: BasePriceKind, query: RowsQuery): Promise<
|
|||||||
params.set("sort", query.sort.key);
|
params.set("sort", query.sort.key);
|
||||||
params.set("desc", query.sort.desc ? "1" : "0");
|
params.set("desc", query.sort.desc ? "1" : "0");
|
||||||
}
|
}
|
||||||
|
for (const [key, value] of Object.entries(query.filters ?? {})) params.set(key, value);
|
||||||
return request<MasterRows>(`/master-data/base-prices/${kind}?${params}`);
|
return request<MasterRows>(`/master-data/base-prices/${kind}?${params}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -134,8 +134,26 @@ export function boldSegments(text: string): { text: string; bold: boolean }[] {
|
|||||||
.filter((part) => part.text !== "");
|
.filter((part) => part.text !== "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 서버가 주는 거르기 한 벌(건설품셈 「임도가 쓰는 것」 따위) — 이름·선택지·줄 수·기본값 모두 서버 것.
|
||||||
|
* 어느 줄을 거를지도 서버가 판정함 — 화면은 고른 값을 `key=value` 로 보내기만(2026-09-17 브레인).
|
||||||
|
*/
|
||||||
|
export interface TableFilter {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
options: { value: string; label: string; rows?: number }[];
|
||||||
|
/** 값을 안 보냈을 때 서버가 쓴 선택지 — 화면이 이것을 보여야 보이는 것과 걸린 것이 같음 */
|
||||||
|
default: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 거르기 상자에 보일 값 — 사람이 고른 것 · 안 골랐으면 서버가 이미 건 기본값. */
|
||||||
|
export function filterValue(filter: TableFilter, picked: Record<string, string>): string {
|
||||||
|
return picked[filter.key] ?? filter.default;
|
||||||
|
}
|
||||||
|
|
||||||
/** 기초단가 표 머리 — 열 하나에 한 문장이라 표에 한 번만 옴(2026-09-15 브레인 ④). */
|
/** 기초단가 표 머리 — 열 하나에 한 문장이라 표에 한 번만 옴(2026-09-15 브레인 ④). */
|
||||||
export interface TableMeta {
|
export interface TableMeta {
|
||||||
|
filters?: TableFilter[];
|
||||||
/** 한 표가 모은 원본마다 한 줄 */
|
/** 한 표가 모은 원본마다 한 줄 */
|
||||||
source?: TableSource[];
|
source?: TableSource[];
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
import {
|
import {
|
||||||
createButton,
|
createButton,
|
||||||
createInputField,
|
createInputField,
|
||||||
|
createSelectField,
|
||||||
el,
|
el,
|
||||||
showConfirmDialog,
|
showConfirmDialog,
|
||||||
showToast,
|
showToast,
|
||||||
@@ -21,6 +22,7 @@ import {
|
|||||||
cellInfo,
|
cellInfo,
|
||||||
clampPage,
|
clampPage,
|
||||||
columnTitle,
|
columnTitle,
|
||||||
|
filterValue,
|
||||||
latestOnly,
|
latestOnly,
|
||||||
nextSort,
|
nextSort,
|
||||||
noticeLines,
|
noticeLines,
|
||||||
@@ -90,7 +92,12 @@ function stateText(info: CellInfo, column: MasterColumn): string {
|
|||||||
|
|
||||||
export function buildRowsView(): RowsView {
|
export function buildRowsView(): RowsView {
|
||||||
let source: RowsSource | null = null;
|
let source: RowsSource | null = null;
|
||||||
const state = { page: 1, q: "", sort: null as SortOrder | null };
|
const state = {
|
||||||
|
page: 1,
|
||||||
|
q: "",
|
||||||
|
sort: null as SortOrder | null,
|
||||||
|
filters: {} as Record<string, string>,
|
||||||
|
};
|
||||||
let showHidden = false;
|
let showHidden = false;
|
||||||
let last: MasterRows | null = null;
|
let last: MasterRows | null = null;
|
||||||
let selected: { index: number; key: string } | null = null;
|
let selected: { index: number; key: string } | null = null;
|
||||||
@@ -137,9 +144,10 @@ export function buildRowsView(): RowsView {
|
|||||||
const prev = pagerButton("Z01_MasterData_Prev", () => goTo(state.page - 1));
|
const prev = pagerButton("Z01_MasterData_Prev", () => goTo(state.page - 1));
|
||||||
const next = pagerButton("Z01_MasterData_Next", () => goTo(state.page + 1));
|
const next = pagerButton("Z01_MasterData_Next", () => goTo(state.page + 1));
|
||||||
const end = pagerButton("Z01_MasterData_Last", () => goTo(Infinity));
|
const end = pagerButton("Z01_MasterData_Last", () => goTo(Infinity));
|
||||||
|
const filterBox = el("div", { className: "z01-master__toolbar" });
|
||||||
const toolbar = el("div", {
|
const toolbar = el("div", {
|
||||||
className: "z01-master__toolbar",
|
className: "z01-master__toolbar",
|
||||||
children: [search.root, hiddenLabel],
|
children: [search.root, filterBox, hiddenLabel],
|
||||||
});
|
});
|
||||||
const pager = el("div", {
|
const pager = el("div", {
|
||||||
className: "z01-master__pager",
|
className: "z01-master__pager",
|
||||||
@@ -180,6 +188,7 @@ export function buildRowsView(): RowsView {
|
|||||||
size: PAGE_SIZE,
|
size: PAGE_SIZE,
|
||||||
q: state.q,
|
q: state.q,
|
||||||
sort: state.sort,
|
sort: state.sort,
|
||||||
|
filters: state.filters,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
grid.classList.remove("is-loading");
|
grid.classList.remove("is-loading");
|
||||||
@@ -250,6 +259,28 @@ export function buildRowsView(): RowsView {
|
|||||||
const outdated = outdatedBanner(meta.source);
|
const outdated = outdatedBanner(meta.source);
|
||||||
banner.replaceChildren(...outdated.map((text) => el("p", { text: `⚠ ${text}` })));
|
banner.replaceChildren(...outdated.map((text) => el("p", { text: `⚠ ${text}` })));
|
||||||
banner.hidden = outdated.length === 0;
|
banner.hidden = outdated.length === 0;
|
||||||
|
// 거르기 — 선택지·이름·줄 수·기본값도, 어느 줄을 거를지 판정도 서버 것(화면은 고른 값만 보냄).
|
||||||
|
filterBox.replaceChildren(
|
||||||
|
...(meta.filters ?? []).map(
|
||||||
|
(filter) =>
|
||||||
|
createSelectField({
|
||||||
|
label: filter.label,
|
||||||
|
options: filter.options.map((option) => ({
|
||||||
|
value: option.value,
|
||||||
|
text:
|
||||||
|
option.rows == null
|
||||||
|
? option.label
|
||||||
|
: `${option.label} (${option.rows.toLocaleString("ko-KR")})`,
|
||||||
|
})),
|
||||||
|
value: filterValue(filter, state.filters),
|
||||||
|
onChange: (value) => {
|
||||||
|
state.filters[filter.key] = value;
|
||||||
|
state.page = 1;
|
||||||
|
void load();
|
||||||
|
},
|
||||||
|
}).root,
|
||||||
|
),
|
||||||
|
);
|
||||||
const columns = shownColumns(meta.columns, showHidden);
|
const columns = shownColumns(meta.columns, showHidden);
|
||||||
const headRow = el("tr", { children: columns.map((column) => headCell(meta, column)) });
|
const headRow = el("tr", { children: columns.map((column) => headCell(meta, column)) });
|
||||||
const body = el("tbody");
|
const body = el("tbody");
|
||||||
@@ -429,6 +460,8 @@ export function buildRowsView(): RowsView {
|
|||||||
source = picked;
|
source = picked;
|
||||||
state.page = 1;
|
state.page = 1;
|
||||||
state.sort = null;
|
state.sort = null;
|
||||||
|
state.filters = {};
|
||||||
|
filterBox.replaceChildren();
|
||||||
title.textContent = picked.title;
|
title.textContent = picked.title;
|
||||||
titleKey.textContent = picked.key;
|
titleKey.textContent = picked.key;
|
||||||
notice.hidden = true;
|
notice.hidden = true;
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ _RUNNER = """
|
|||||||
import { writeFileSync } from "node:fs";
|
import { writeFileSync } from "node:fs";
|
||||||
import {
|
import {
|
||||||
cellInfo, cellText, clampPage, columnTitle, latestOnly, pageCount, parseCellInput, rowCells,
|
cellInfo, cellText, clampPage, columnTitle, latestOnly, pageCount, parseCellInput, rowCells,
|
||||||
boldSegments, groupKinds, nextSort, noticeLines, outdatedBanner, shownColumns, valueWithUnit,
|
boldSegments, filterValue, groupKinds, nextSort, noticeLines, outdatedBanner, shownColumns, valueWithUnit,
|
||||||
} from "./Z01_MasterData_UI_Cells.js";
|
} from "./Z01_MasterData_UI_Cells.js";
|
||||||
|
|
||||||
// kind 목록은 서버가 줌 — 화면이 제 목록을 들면 kind 가 늘 때마다 빠짐(2026-09-16 브레인)
|
// kind 목록은 서버가 줌 — 화면이 제 목록을 들면 kind 가 늘 때마다 빠짐(2026-09-16 브레인)
|
||||||
@@ -41,6 +41,15 @@ const served = [
|
|||||||
];
|
];
|
||||||
const grouped = groupKinds(served).map((g) => [g.label, g.items.map((i) => i.kind)]);
|
const grouped = groupKinds(served).map((g) => [g.label, g.items.map((i) => i.kind)]);
|
||||||
|
|
||||||
|
// 거르기 — 안 골랐으면 서버가 이미 건 기본값을 보임(보이는 것 = 걸린 것) · 고르면 고른 값
|
||||||
|
const roadFilter = {
|
||||||
|
key: "use",
|
||||||
|
label: "보기",
|
||||||
|
options: [{ value: "all", label: "전부", rows: 995 }, { value: "road", label: "임도가 쓰는 것", rows: 120 }],
|
||||||
|
default: "road",
|
||||||
|
};
|
||||||
|
const filtered = [filterValue(roadFilter, {}), filterValue(roadFilter, { use: "all" })];
|
||||||
|
|
||||||
// 열 제목 누르기 — 없음 → 오름 → 내림 → 없음(원본 차례) · 다른 열을 누르면 그 열 오름부터
|
// 열 제목 누르기 — 없음 → 오름 → 내림 → 없음(원본 차례) · 다른 열을 누르면 그 열 오름부터
|
||||||
const sorts = [
|
const sorts = [
|
||||||
nextSort(null, "price_krw"),
|
nextSort(null, "price_krw"),
|
||||||
@@ -207,6 +216,7 @@ writeFileSync(process.argv[2], JSON.stringify({
|
|||||||
bolds,
|
bolds,
|
||||||
sorts,
|
sorts,
|
||||||
grouped,
|
grouped,
|
||||||
|
filtered,
|
||||||
}));
|
}));
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -332,3 +342,5 @@ def test_칸_글자와_숨김_열(tmp_path: Path) -> None:
|
|||||||
["공종 축", ["forest"]],
|
["공종 축", ["forest"]],
|
||||||
["later_group", ["brand_new"]],
|
["later_group", ["brand_new"]],
|
||||||
]
|
]
|
||||||
|
# 거르기 상자 — 첫 선택지가 아니라 서버 기본값(어긋나면 「전부」로 보이며 걸러진 목록이 뜸)
|
||||||
|
assert got["filtered"] == ["road", "all"]
|
||||||
|
|||||||
Reference in New Issue
Block a user