/* ============================================================================= * Z01_MasterData_Api_Fetch.ts * 마스터 데이터 읽기 — 서버 `Z01_MasterData_Router.py`(랩탑 메인) · 시스템 관리자만 * * 응답 모양은 2026-09-15 브레인이 못박은 약속 그대로. 읽기 전용 — 고치기는 다음 차례. * ========================================================================== */ import { API_BASE_URL } from "@config/config_frontend"; import type { MasterColumn, OverrideState, ServedKind, SortOrder, TableMeta, } 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 const VALUES_TABLE_ID = "@values"; export interface MasterGroup { /** fingerprint = 폴더 지문(_manifest) — 어느 판으로 셈했는지 못박는 자리(부산물 아님). */ key: "logic" | "base_value" | "byproduct" | "seed" | "fingerprint"; label: string; files: MasterFile[]; } /** 기초단가 표만 editable·locked·formula 를 실음 — 트리 표는 읽기 전용이라 안 옴. */ export interface MasterRows extends TableMeta { columns: MasterColumn[]; rows: Record[]; total: number; } export interface RowsQuery { page: number; size: number; q: string; /** 열 제목을 눌렀을 때만 — 없으면 서버가 원본 차례로 줌 */ sort?: SortOrder | null; } /** * 표 한 벌의 이름 — 화면은 목록을 안 가짐. 갈래·kind·이름·줄 수를 서버에서 받아 세움 * (2026-09-16 브레인 — kind 가 늘 때마다 화면이 빠지던 자리). */ export type BasePriceKind = string; export function fetchBaseKinds(): Promise { return request<{ kinds: ServedKind[] }>("/master-data/base-prices").then((data) => data.kinds); } async function request(path: string, init: RequestInit = {}): Promise { const response = await fetch(`${API_BASE_URL}${path}`, { credentials: "include", headers: { "Content-Type": "application/json" }, ...init, }); const data = (await response.json()) as { detail?: string } & T; if (!response.ok) throw new Error(data.detail ?? "Request failed"); return data; } const pageParams = (query: RowsQuery): Record => ({ page: String(query.page), size: String(query.size), q: query.q, }); 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}`); } export interface OverrideItem { kind: BasePriceKind; row_id: string; row_label: string; column: string; column_label: string; original: unknown; current?: unknown; value: unknown; unit?: string; state: OverrideState; } /** 고친 것 모아 보기 — 서버가 source_changed → orphan → edited 차례로 정렬하고 쪽도 나눠 줌. */ export function fetchOverrides( query: Pick, ): Promise<{ items: OverrideItem[]; total: number }> { const params = new URLSearchParams({ page: String(query.page), size: String(query.size) }); return request(`/master-data/overrides?${params}`); } /** 칸 고치기 — 값 null = 덮개에서 그 칸을 빼 원래 값으로 되돌림. 응답은 고친 뒤 그 줄. */ export function saveBasePrice( kind: BasePriceKind, rowId: string, values: Record, ): Promise> { return request(`/master-data/base-prices/${kind}/${encodeURIComponent(rowId)}`, { method: "PUT", body: JSON.stringify({ values }), }); } export async function fetchMasterTree(): Promise { return (await request<{ groups: MasterGroup[] }>("/master-data/tree")).groups; } export function fetchMasterRows( file: string, table: string, query: RowsQuery, ): Promise { const params = new URLSearchParams({ file, table, ...pageParams(query) }); return request(`/master-data/rows?${params}`); }