diff --git a/A00_Common/main.ts b/A00_Common/main.ts index 8f218420..26d914b8 100644 --- a/A00_Common/main.ts +++ b/A00_Common/main.ts @@ -26,6 +26,7 @@ const FOOTERLESS_ROUTES: readonly RoutePath[] = [ ROUTES.B09_ESTIMATION, ROUTES.B10_PAYMENT, ROUTES.B11_STATUS, + ROUTES.Z01_MASTER_DATA, ]; function bootstrap(): void { diff --git a/A00_Common/router.ts b/A00_Common/router.ts index a3c56318..b6c99652 100644 --- a/A00_Common/router.ts +++ b/A00_Common/router.ts @@ -59,6 +59,8 @@ const routeTable: Partial Promise>> = { (await import("../B11_Status/B11_Status_UI_Page")).renderB11Status, [ROUTES.B11_LOADING]: async () => (await import("../B11_Status/B11_Status_UI_Loading")).renderB11Loading, + [ROUTES.Z01_MASTER_DATA]: async () => + (await import("../Z01_MasterData/Z01_MasterData_UI_Page")).renderZ01MasterData, }; /** 로그인 여부 (토큰 존재 확인) */ diff --git a/B01_Dashboard/B01_Dashboard_UI_Page.ts b/B01_Dashboard/B01_Dashboard_UI_Page.ts index 6b445015..9f675b88 100644 --- a/B01_Dashboard/B01_Dashboard_UI_Page.ts +++ b/B01_Dashboard/B01_Dashboard_UI_Page.ts @@ -130,6 +130,19 @@ async function loadRoleData(state: DashboardState): Promise { } } +function buildSystemSettingsPanel(): HTMLElement { + const wrap = document.createElement("div"); + wrap.className = "b01-dashboard__actions"; + wrap.append( + createButton({ + label: L("B01_Dashboard_MasterData"), + variant: "ghost", + onClick: () => navigateTo(ROUTES.Z01_MASTER_DATA), + }), + ); + return wrap; +} + function buildPage(state: DashboardState): HTMLElement { // 제목·여백은 공용 템플릿을 따른다 (2026-09-06 사용자 지시) — B02 등 다른 화면과 같은 모양. const page = document.createElement("div"); @@ -138,8 +151,15 @@ function buildPage(state: DashboardState): HTMLElement { grid.className = "b01-dashboard__grid"; if (state.user.role === "SYSTEM_ADMIN") { + // 리소스 현황 오른쪽에 시스템 설정 — 앞으로 다른 설정 단추도 이 칸에 듦 (2026-09-15 브레인 Z01). + const topRow = document.createElement("div"); + topRow.className = "b01-dashboard__top-row"; + topRow.append( + section(L("B01_Dashboard_Resources"), buildResourcePanel(state.resources)), + section(L("B01_Dashboard_SystemSettings"), buildSystemSettingsPanel()), + ); grid.append( - section(L("B01_Dashboard_Resources"), buildResourcePanel(state.resources), true), + topRow, section(L("B01_Dashboard_Projects"), projectTable(state.allProjects, state.user), true, [ createButton({ label: "+", onClick: () => navigateTo(ROUTES.B02_PROJ_REGISTER) }), ]), diff --git a/B01_Dashboard/B01_Dashboard_UI_Style.css b/B01_Dashboard/B01_Dashboard_UI_Style.css index 35f94502..ceab81c6 100644 --- a/B01_Dashboard/B01_Dashboard_UI_Style.css +++ b/B01_Dashboard/B01_Dashboard_UI_Style.css @@ -31,6 +31,14 @@ grid-column: 1 / -1; } +/* 리소스 현황 | 시스템 설정 — 설정 칸은 단추만 들어 좁게. */ +.b01-dashboard__top-row { + grid-column: 1 / -1; + display: grid; + grid-template-columns: minmax(0, 3fr) minmax(0, 1fr); + gap: var(--spacing-24); +} + .b01-dashboard__actions { display: flex; flex-wrap: wrap; @@ -280,7 +288,8 @@ @media (max-width: 860px) { .b01-dashboard__header, - .b01-dashboard__grid { + .b01-dashboard__grid, + .b01-dashboard__top-row { display: block; } diff --git a/Z01_MasterData/Z01_MasterData_Api_Fetch.ts b/Z01_MasterData/Z01_MasterData_Api_Fetch.ts new file mode 100644 index 00000000..ca287649 --- /dev/null +++ b/Z01_MasterData/Z01_MasterData_Api_Fetch.ts @@ -0,0 +1,63 @@ +/* ============================================================================= + * Z01_MasterData_Api_Fetch.ts + * 마스터 데이터 읽기 — 서버 `Z01_MasterData_Router.py`(랩탑 메인) · 시스템 관리자만 + * + * 응답 모양은 2026-09-15 브레인이 못박은 약속 그대로. 읽기 전용 — 고치기는 다음 차례. + * ========================================================================== */ + +import { API_BASE_URL } from "@config/config_frontend"; +import type { MasterColumn } from "./Z01_MasterData_UI_Cells"; + +export interface MasterTable { + id: string; + label: string; + key: string; + row_count: number; +} + +export interface MasterFile { + id: string; + label: string; + key: string; + tables: MasterTable[]; +} + +export interface MasterGroup { + key: "logic" | "base" | "byproduct" | "seed"; + label: string; + files: MasterFile[]; +} + +export interface MasterRows { + columns: MasterColumn[]; + rows: Record[]; + total: number; +} + +async function request(path: string): Promise { + const response = await fetch(`${API_BASE_URL}${path}`, { credentials: "include" }); + const data = (await response.json()) as { detail?: string } & T; + if (!response.ok) throw new Error(data.detail ?? "Request failed"); + return data; +} + +export async function fetchMasterTree(): Promise { + return (await request<{ groups: MasterGroup[] }>("/master-data/tree")).groups; +} + +export function fetchMasterRows(query: { + file: string; + table: string; + page: number; + size: number; + q: string; +}): Promise { + const params = new URLSearchParams({ + file: query.file, + table: query.table, + page: String(query.page), + size: String(query.size), + q: query.q, + }); + return request(`/master-data/rows?${params}`); +} diff --git a/Z01_MasterData/Z01_MasterData_UI_Cells.ts b/Z01_MasterData/Z01_MasterData_UI_Cells.ts new file mode 100644 index 00000000..7669fe5e --- /dev/null +++ b/Z01_MasterData/Z01_MasterData_UI_Cells.ts @@ -0,0 +1,38 @@ +/* ============================================================================= + * Z01_MasterData_UI_Cells.ts + * 마스터 표 칸 글자 — 숨김 열 · 열 제목 · 칸 값 · 쪽 수 (import 없음 · 시험이 그대로 돌림) + * + * 한글 이름은 API 가 `label` 로 줌(이름표 파일 `resources/data_master_labels/`) — 화면은 + * 사전을 두지 않고 받은 대로 보임. 두 벌이 되면 어긋나기 때문(2026-09-15 브레인 Z01 ①). + * ========================================================================== */ + +export interface MasterColumn { + key: string; + label: string; + unit?: string | null; + /** 내부 id · sha · 생성시각 — 기본으로 안 보임 */ + hidden?: boolean; +} + +export function shownColumns(columns: MasterColumn[], showHidden: boolean): MasterColumn[] { + return showHidden ? columns : columns.filter((column) => !column.hidden); +} + +export function columnTitle(column: MasterColumn): string { + return column.unit ? `${column.label} (${column.unit})` : column.label; +} + +/** 단위가 붙은 수만 쉼표 — 연도·코드 같은 수(단위 없음)에 쉼표가 끼면 틀린 값처럼 보임. */ +export function cellText(value: unknown, unit?: string | null): string { + if (value === null || value === undefined) return ""; + if (typeof value === "boolean") return value ? "예" : "아니오"; + if (typeof value === "number" && unit) { + return value.toLocaleString("ko-KR", { maximumFractionDigits: 10 }); + } + if (typeof value === "object") return JSON.stringify(value); + return String(value); +} + +export function pageCount(total: number, size: number): number { + return Math.max(1, Math.ceil(total / size)); +} diff --git a/Z01_MasterData/Z01_MasterData_UI_Page.ts b/Z01_MasterData/Z01_MasterData_UI_Page.ts new file mode 100644 index 00000000..7d8fd84c --- /dev/null +++ b/Z01_MasterData/Z01_MasterData_UI_Page.ts @@ -0,0 +1,266 @@ +/* ============================================================================= + * Z01_MasterData_UI_Page.ts + * 마스터 데이터 — 좌 트리(갈래 → 파일 → 표) / 우 고른 표의 실제 줄(쪽 나누기 · 검색) + * + * 시스템 관리자만(서버도 require_system_admin 으로 다시 막음) · 읽기 전용부터 + * (2026-09-15 브레인 Z01 — 마스터를 사람이 보고 고칠 수 있게 먼저 만들고 그 위에 일위대가 재조립). + * ========================================================================== */ + +import { ROUTES } from "@config/config_frontend"; +import { + createButton, + createInputField, + el, + hideLoadingOverlay, + showLoadingOverlay, + showToast, +} from "@ui/ui_template_elements"; +import { createGeneralLayout } from "@ui/ui_template_general_layout"; +import { t as L } from "@ui/ui_template_locale"; +import { navigateTo } from "../A00_Common/router"; +import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch"; +import { + fetchMasterRows, + fetchMasterTree, + type MasterFile, + type MasterGroup, + type MasterRows, + type MasterTable, +} from "./Z01_MasterData_Api_Fetch"; +import { cellText, columnTitle, pageCount, shownColumns } from "./Z01_MasterData_UI_Cells"; +import "./Z01_MasterData_UI_Style.css"; + +const PAGE_SIZE = 50; +const SEARCH_DELAY_MS = 300; + +export async function renderZ01MasterData(root: HTMLElement): Promise { + const user = await fetchSessionUser().catch(() => null); + if (user?.role !== "SYSTEM_ADMIN") { + showToast(L("Z01_MasterData_AdminOnly"), "error"); + navigateTo(ROUTES.B01_ACCOUNT); + return; + } + showLoadingOverlay(); + let groups: MasterGroup[] = []; + try { + groups = await fetchMasterTree(); + } catch (error) { + showToast(error instanceof Error ? error.message : L("Z01_MasterData_LoadFailed"), "error"); + } finally { + hideLoadingOverlay(); + } + root.innerHTML = ""; + root.append(buildPage(groups)); +} + +function buildPage(groups: MasterGroup[]): HTMLElement { + const view = buildRowsView(); + const tree = buildTree(groups, view.pick); + const content = el("div", { className: "z01-master__content", children: [tree, view.root] }); + return createGeneralLayout({ + pageClass: "z01-master", + title: L("Z01_MasterData_Title"), + subtitle: L("Z01_MasterData_Subtitle"), + content, + }).root; +} + +function buildTree( + groups: MasterGroup[], + pick: (file: MasterFile, table: MasterTable) => void, +): HTMLElement { + const nav = el("nav", { className: "z01-master__tree" }); + for (const group of groups) { + const groupBox = folder("z01-master__group", group.label, String(group.files.length)); + groupBox.open = true; + // 부산물은 계산이 남긴 기록 — 고칠 정본이 아님(2026-09-15 브레인 갈래 나눔표). + if (group.key === "byproduct") { + groupBox.append( + el("p", { className: "z01-master__note", text: L("Z01_MasterData_Byproduct") }), + ); + } + for (const file of group.files) { + const fileBox = folder( + "z01-master__file", + file.label, + file.label === file.key ? "" : file.key, + ); + for (const table of file.tables) { + const button = el("button", { + className: "z01-master__table", + attrs: { type: "button", title: table.key }, + children: [ + el("span", { text: table.label }), + el("span", { + className: "z01-master__key", + text: table.row_count.toLocaleString("ko-KR"), + }), + ], + }); + button.addEventListener("click", () => { + nav.querySelector(".z01-master__table.is-active")?.classList.remove("is-active"); + button.classList.add("is-active"); + pick(file, table); + }); + fileBox.append(button); + } + groupBox.append(fileBox); + } + nav.append(groupBox); + } + return nav; +} + +/** 접는 칸 — 브라우저 기본
로 둠. */ +function folder(className: string, label: string, aside: string): HTMLDetailsElement { + const summary = el("summary", { + children: [ + el("span", { text: label }), + el("span", { className: "z01-master__key", text: aside }), + ], + }); + return el("details", { className, children: [summary] }); +} + +interface RowsView { + root: HTMLElement; + pick: (file: MasterFile, table: MasterTable) => void; +} + +function buildRowsView(): RowsView { + const state = { + file: null as MasterFile | null, + table: null as MasterTable | null, + page: 1, + q: "", + }; + let showHidden = false; + let last: MasterRows | null = null; + let requestSeq = 0; + let searchTimer = 0; + + const title = el("h2", { className: "z01-master__title", text: L("Z01_MasterData_PickTable") }); + const titleKey = el("span", { className: "z01-master__key" }); + const search = createInputField({ + type: "search", + placeholder: L("Z01_MasterData_Search"), + onInput: (value) => { + window.clearTimeout(searchTimer); + searchTimer = window.setTimeout(() => { + state.q = value.trim(); + state.page = 1; + void load(); + }, SEARCH_DELAY_MS); + }, + }); + const hiddenToggle = el("input", { attrs: { type: "checkbox" } }); + hiddenToggle.addEventListener("change", () => { + showHidden = hiddenToggle.checked; + draw(); + }); + const hiddenLabel = el("label", { + className: "z01-master__check", + children: [hiddenToggle, L("Z01_MasterData_ShowHidden")], + }); + const grid = el("div", { className: "z01-master__grid-wrap" }); + const pageInfo = el("span", { className: "z01-master__page-info" }); + const prev = createButton({ + label: L("Z01_MasterData_Prev"), + variant: "ghost", + onClick: () => turn(-1), + }); + const next = createButton({ + label: L("Z01_MasterData_Next"), + variant: "ghost", + onClick: () => turn(1), + }); + const toolbar = el("div", { + className: "z01-master__toolbar", + children: [search.root, hiddenLabel], + }); + const pager = el("div", { className: "z01-master__pager", children: [prev, pageInfo, next] }); + const root = el("section", { + className: "z01-master__panel", + children: [el("div", { children: [title, titleKey] }), toolbar, grid, pager], + }); + toolbar.hidden = true; + pager.hidden = true; + + function turn(step: number): void { + state.page += step; + void load(); + } + + async function load(): Promise { + if (!state.file || !state.table) return; + const mine = ++requestSeq; + grid.classList.add("is-loading"); + try { + const data = await fetchMasterRows({ + file: state.file.id, + table: state.table.id, + page: state.page, + size: PAGE_SIZE, + q: state.q, + }); + if (mine !== requestSeq) return; + last = data; + draw(); + } catch (error) { + if (mine === requestSeq) { + showToast(error instanceof Error ? error.message : L("Z01_MasterData_LoadFailed"), "error"); + } + } finally { + if (mine === requestSeq) grid.classList.remove("is-loading"); + } + } + + function draw(): void { + if (!last) return; + const columns = shownColumns(last.columns, showHidden); + const headRow = el("tr"); + for (const column of columns) { + const th = el("th", { children: [el("span", { text: columnTitle(column) })] }); + if (column.label !== column.key) { + th.append(el("span", { className: "z01-master__key", text: column.key })); + } + headRow.append(th); + } + const body = el("tbody"); + for (const row of last.rows) { + const tr = el("tr"); + for (const column of columns) { + const text = cellText(row[column.key], column.unit); + tr.append(el("td", { text, attrs: { title: text } })); + } + body.append(tr); + } + grid.replaceChildren( + last.rows.length + ? el("table", { + className: "z01-master__grid", + children: [el("thead", { children: [headRow] }), body], + }) + : el("p", { className: "z01-master__note", text: L("Z01_MasterData_NoRows") }), + ); + const pages = pageCount(last.total, PAGE_SIZE); + pageInfo.textContent = `${state.page} / ${pages} ${L("Z01_MasterData_Page")} · ${last.total.toLocaleString("ko-KR")} ${L("Z01_MasterData_Rows")}`; + prev.disabled = state.page <= 1; + next.disabled = state.page >= pages; + } + + function pick(file: MasterFile, table: MasterTable): void { + state.file = file; + state.table = table; + state.page = 1; + title.textContent = `${file.label} › ${table.label}`; + titleKey.textContent = `${file.key} / ${table.key}`; + toolbar.hidden = false; + pager.hidden = false; + last = null; + grid.replaceChildren(); + void load(); + } + + return { root, pick }; +} diff --git a/Z01_MasterData/Z01_MasterData_UI_Style.css b/Z01_MasterData/Z01_MasterData_UI_Style.css new file mode 100644 index 00000000..7a37171a --- /dev/null +++ b/Z01_MasterData/Z01_MasterData_UI_Style.css @@ -0,0 +1,167 @@ +/* Z01 마스터 데이터 — 좌 트리 / 우 표 */ +.z01-master [hidden] { + display: none !important; +} + +.z01-master__content { + display: grid; + grid-template-columns: minmax(220px, 300px) minmax(0, 1fr); + gap: var(--spacing-24); + align-items: start; +} + +.z01-master__tree { + position: sticky; + top: var(--spacing-16); + max-height: calc(100vh - 160px); + overflow-y: auto; + padding: var(--spacing-8); + border: 1px solid var(--color-border); + border-radius: var(--radius-cards); + background: var(--color-surface); + font-size: var(--text-body-sm); +} + +.z01-master__tree summary { + display: flex; + justify-content: space-between; + gap: var(--spacing-8); + padding: var(--spacing-4) var(--spacing-8); + cursor: pointer; + color: var(--color-text-body); +} + +.z01-master__group > summary { + font-weight: var(--font-weight-bold); + color: var(--color-text); +} + +.z01-master__file { + margin-left: var(--spacing-12); +} + +.z01-master__key { + margin-left: var(--spacing-4); + color: var(--color-text-muted); + font-size: var(--text-caption); + font-weight: var(--font-weight-regular); +} + +.z01-master__note { + margin: 0 var(--spacing-8) var(--spacing-4); + color: var(--color-text-muted); + font-size: var(--text-caption); +} + +.z01-master__table { + display: flex; + justify-content: space-between; + gap: var(--spacing-8); + width: 100%; + padding: var(--spacing-4) var(--spacing-8) var(--spacing-4) var(--spacing-24); + border: 0; + border-radius: var(--radius-sm); + background: transparent; + color: var(--color-text-body); + font: inherit; + text-align: left; + cursor: pointer; +} + +.z01-master__table:hover { + background: var(--color-surface-raised); +} + +.z01-master__table.is-active { + background: var(--color-mist-violet); + color: var(--color-accent); +} + +.z01-master__panel { + display: flex; + flex-direction: column; + gap: var(--spacing-16); + min-width: 0; +} + +.z01-master__title { + display: inline; + color: var(--color-text); + font-size: var(--text-body); +} + +.z01-master__toolbar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--spacing-16); +} + +.z01-master__check { + display: flex; + align-items: center; + gap: var(--spacing-4); + color: var(--color-text-secondary); + font-size: var(--text-body-sm); +} + +.z01-master__grid-wrap { + max-height: calc(100vh - 300px); + overflow: auto; + border: 1px solid var(--color-border); + border-radius: var(--radius-cards); +} + +.z01-master__grid-wrap.is-loading { + opacity: 0.5; +} + +.z01-master__grid { + border-collapse: collapse; + font-size: var(--text-body-sm); +} + +.z01-master__grid th, +.z01-master__grid td { + max-width: 320px; + padding: var(--spacing-8) var(--spacing-12); + overflow: hidden; + border-bottom: 1px solid var(--color-border); + text-align: left; + text-overflow: ellipsis; + white-space: nowrap; +} + +.z01-master__grid th { + position: sticky; + top: 0; + z-index: 1; + background: var(--color-surface); + color: var(--color-text-secondary); + font-weight: var(--font-weight-medium); +} + +.z01-master__grid th .z01-master__key { + display: block; + margin-left: 0; +} + +.z01-master__pager { + display: flex; + align-items: center; + justify-content: flex-end; + gap: var(--spacing-12); + color: var(--color-text-secondary); + font-size: var(--text-body-sm); +} + +@media (max-width: 860px) { + .z01-master__content { + display: block; + } + + .z01-master__tree { + position: static; + margin-bottom: var(--spacing-24); + } +} diff --git a/config/config_frontend.ts b/config/config_frontend.ts index bdd275a1..277a9238 100644 --- a/config/config_frontend.ts +++ b/config/config_frontend.ts @@ -116,6 +116,8 @@ export const ROUTES = { B11_STATUS: "b11-status", // 대시보드에서 B그룹으로 처음 들어갈 때 3D·등고선을 미리 받아 두는 준비 화면. B11_LOADING: "b11-loading", + // 시스템 관리자 전용 — 로직·기초값 마스터 보기 (2026-09-15 브레인 Z01). + Z01_MASTER_DATA: "z01-master-data", } as const; export type RouteKey = keyof typeof ROUTES; @@ -138,6 +140,7 @@ export const PROTECTED_ROUTES: readonly RoutePath[] = [ ROUTES.B10_PAYMENT, ROUTES.B11_STATUS, ROUTES.B11_LOADING, + ROUTES.Z01_MASTER_DATA, ]; /* ----------------------------------------------------------------------------- diff --git a/resources/tester/test_z01_master_cells.py b/resources/tester/test_z01_master_cells.py new file mode 100644 index 00000000..82c485e1 --- /dev/null +++ b/resources/tester/test_z01_master_cells.py @@ -0,0 +1,82 @@ +"""Z01 마스터 데이터 표 칸 — 숨김 열 · 열 제목(한글 label + 단위) · 칸 글자 · 쪽 수 (2026-09-15 브레인 Z01). + + ① 한글 이름은 API 가 `label` 로 줌 — 화면은 사전을 안 가짐(받은 대로 보임) + ② `hidden` 열(내부 id · sha · 생성시각)은 기본 숨김 · 「숨긴 열 보기」면 보임 + ③ 단위가 있는 열의 수는 천 단위 쉼표(소수 자리는 자르지 않음) · 단위 없는 수(연도 등)는 그대로 + ④ 쪽 수는 줄이 없어도 1 +TS 를 실제로 돌린다(`test_b06_berm_review_info` 와 같은 방식). +""" + +from __future__ import annotations + +import json +import subprocess +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +TSC = PROJECT_ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc" +SOURCE = PROJECT_ROOT / "Z01_MasterData" / "Z01_MasterData_UI_Cells.ts" + +_RUNNER = """ +import { writeFileSync } from "node:fs"; +import { cellText, columnTitle, pageCount, shownColumns } from "./Z01_MasterData_UI_Cells.js"; + +const columns = [ + { key: "id", label: "id", unit: null, hidden: true }, + { key: "occupation_name", label: "직종명", unit: null, hidden: false }, + { key: "daily_wage_krw", label: "일 노임", unit: "원", hidden: false }, +]; +writeFileSync(process.argv[2], JSON.stringify({ + shown: shownColumns(columns, false).map((c) => c.key), + shownAll: shownColumns(columns, true).map((c) => c.key), + titles: columns.map(columnTitle), + cells: [ + cellText(null, null), + cellText(250000, "원"), + cellText(1.23456, "㎥"), + cellText(2026, null), + cellText({ a: 1 }, null), + cellText(true, null), + cellText("보통인부", null), + ], + pages: [pageCount(0, 50), pageCount(6999, 50), pageCount(50, 50)], +})); +""" + + +def test_칸_글자와_숨김_열(tmp_path: Path) -> None: + out = tmp_path / "js" + subprocess.run( # noqa: S603 — 고정 실행 파일 + [ + "node", + str(TSC), + str(SOURCE), + "--outDir", + str(out), + "--module", + "esnext", + "--target", + "es2022", + "--ignoreConfig", + "--noCheck", + "--noResolve", + ], + cwd=str(PROJECT_ROOT), + check=True, + capture_output=True, + ) + (out / "runner.mjs").write_text(_RUNNER, encoding="utf-8") + result = tmp_path / "output.json" + subprocess.run( # noqa: S603 + ["node", str(out / "runner.mjs"), str(result)], + cwd=str(PROJECT_ROOT), + check=True, + capture_output=True, + ) + got = json.loads(result.read_text(encoding="utf-8")) + + assert got["shown"] == ["occupation_name", "daily_wage_krw"] + assert got["shownAll"] == ["id", "occupation_name", "daily_wage_krw"] + assert got["titles"] == ["id", "직종명", "일 노임 (원)"] + assert got["cells"] == ["", "250,000", "1.23456", "2026", '{"a":1}', "예", "보통인부"] + assert got["pages"] == [1, 140, 1] diff --git a/tsconfig.json b/tsconfig.json index 3c703b96..4b9da6ad 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -32,6 +32,6 @@ "maplibre-gl": ["config/node_modules/maplibre-gl"], } }, - "include": ["A00_Common", "A0*", "B0*", "B1*", "ui_template", "config/**/*", "common_util"], + "include": ["A00_Common", "A0*", "B0*", "B1*", "Z0*", "ui_template", "config/**/*", "common_util"], "exclude": ["0_old", "node_modules", "venv", "dist"] } diff --git a/ui_template/ui_template_locale_b3.ts b/ui_template/ui_template_locale_b3.ts index f5bd2680..67183d6a 100644 --- a/ui_template/ui_template_locale_b3.ts +++ b/ui_template/ui_template_locale_b3.ts @@ -136,4 +136,29 @@ export const ui_locales_b3 = { "법령 별표2 Ⅰ.2.바.(2) 가 요구하는 구간만 자동 — 종단 8% 초과 사질·점토(우리 프리셋은 토사 하나라 토사 전부) · 8% 이하 연약·습윤은 칸(판정 근거 없음). 교본 3-2 는 「콘크리트 포장 구간 이외」 전부라 고르면 넓힘. 두께 제안 0.10(교본) · C·L 제안 소광 관측(원문 표에 혼합석 줄 없음 · 역 C 와 방향이 반대)", "Only the stations required by the forest act table 2 are automatic — grade > 8% sandy/clay soil (our preset has one soil class) and ≤ 8% soft/wet ranges you enter. Manual 3-2 covers all non-concrete stations. Suggested thickness 0.10 (manual) · C·L from practice (no gravel row in the standard table)", ], + /* --- B01 시스템 설정 · Z01 마스터 데이터 (2026-09-15 브레인 Z01) --- */ + B01_Dashboard_SystemSettings: ["시스템 설정", "System Settings"], + B01_Dashboard_MasterData: ["마스터 데이터", "Master Data"], + Z01_MasterData_Title: ["마스터 데이터", "Master Data"], + Z01_MasterData_Subtitle: [ + "로직·기초값을 표로 봄 — 읽기 전용", + "Browse logic and base values — read only", + ], + Z01_MasterData_AdminOnly: ["시스템 관리자만 볼 수 있음", "System administrators only"], + Z01_MasterData_LoadFailed: ["마스터 데이터를 못 읽음", "Failed to load master data"], + Z01_MasterData_PickTable: ["왼쪽에서 표를 고를 것", "Pick a table on the left"], + Z01_MasterData_Search: ["검색", "Search"], + Z01_MasterData_ShowHidden: [ + "숨긴 열 보기(내부 id · sha · 생성시각)", + "Show hidden columns (id · sha · created)", + ], + Z01_MasterData_Byproduct: [ + "정본 아님 — 계산이 남긴 기록", + "Not source data — calculation records", + ], + Z01_MasterData_NoRows: ["줄 없음", "No rows"], + Z01_MasterData_Prev: ["이전", "Prev"], + Z01_MasterData_Next: ["다음", "Next"], + Z01_MasterData_Page: ["쪽", "pages"], + Z01_MasterData_Rows: ["줄", "rows"], } as const;