/* ============================================================================= * M01_MasterData_Api_Fetch.ts * 마스터 요소 화면 서버 호출 — 계약 `resources/master_data/_화면_계약.md` · 시스템 관리자만 * ========================================================================== */ import { API_BASE_URL } from "@config/config_frontend"; /** 줄·표 한 개 — 파일 그대로(한글 칸). */ export type Row = Record; export interface GroupInfo { group: string; files: number; rows: number; } export interface FileInfo { file: string; book: string | null; chapter: string; edition: string | null; rows: number; version: string; } /** 하위 거름 한 갈래 — 인력 「구분」 · 기계 「세부분류」 · 장 그룹은 원문+부문(서버가 모음). */ export interface SubInfo { name: string; book: string | null; details: string[]; } export interface RowsPage { file: string; version: string; total: number; page: number; size: number; rows: Row[]; /** 참조 칸 키 → 이름(키 옆에 같이 보임) */ refs: Record; } export interface TableHead { file: string; 키: string; 원문번호: string; 구분: string; 상세구분: string; 이름: string; 기준: string; 출처: string; 비고?: string; 조건: Record; 값칸: Record; count: number; } export interface TablesPage { total: number; page: number; size: number; tables: TableHead[]; } export interface Change { op: "edit" | "add" | "delete"; key?: string; row?: Row; } export interface FileChanges { file: string; version: string; changes: Change[]; } /** 저장 결과 — 200 은 새 판본 · 나머지는 화면이 그대로 안내할 상태. */ export type SaveResult = | { status: 200; files: { file: string; version: string }[] } | { status: 409; stale: string[] } | { status: 422; errors: string[] } | { status: number; detail: string }; async function call(path: string, init: RequestInit = {}): Promise<{ status: number; body: T }> { const response = await fetch(`${API_BASE_URL}/m01${path}`, { credentials: "include", headers: { "Content-Type": "application/json" }, ...init, }); return { status: response.status, body: (await response.json()) as T }; } async function get(path: string, params: Record): Promise { const query = new URLSearchParams(Object.entries(params).map(([k, v]) => [k, String(v)])); const { status, body } = await call(`${path}?${query}`); if (status !== 200) throw new Error(String(body.detail ?? `HTTP ${status}`)); return body; } export const fetchGroups = async (): Promise => (await get<{ groups: GroupInfo[] }>("/groups", {})).groups; export const fetchFiles = async (group: string): Promise => (await get<{ files: FileInfo[] }>(`/groups/${encodeURIComponent(group)}/files`, {})).files; export const fetchRows = ( file: string, page: number, size: number, q: string, unlinked = false, sub = "", detail = "", state = "", ): Promise => get("/rows", { file, page, size, q, unlinked: unlinked ? 1 : 0, sub, detail, state }); /** 한 테이블 파일은 `file` · 장 그룹(소요량 · 계수 · 로직)은 `group` 으로 부름. */ export const fetchSubsAll = (where: { file?: string; group?: string; }): Promise<{ subs: SubInfo[]; states?: string[] }> => get("/subs", { file: where.file ?? "", group: where.group ?? "" }); export const fetchSubs = async (where: { file?: string; group?: string }): Promise => (await fetchSubsAll(where)).subs; export const fetchTables = ( group: string, sub: string, detail: string, q: string, page: number, size: number, ): Promise => get("/tables", { group, sub, detail, q, page, size }); export const fetchTable = ( file: string, key: string, ): Promise<{ version: string; table: Row & { 줄: Row[] } }> => get("/table", { file, key }); export async function saveFiles(files: FileChanges[]): Promise { const { status, body } = await call>("/save", { method: "POST", body: JSON.stringify({ files }), }); const detail = body.detail as { stale?: string[]; errors?: string[] } | string | undefined; if (status === 200) return { status, files: body.files as { file: string; version: string }[] }; if (status === 409 && typeof detail === "object") return { status, stale: detail.stale ?? [] }; if (status === 422 && typeof detail === "object") return { status, errors: detail.errors ?? [] }; return { status, detail: typeof detail === "string" ? detail : JSON.stringify(detail) }; } export type PickKind = "price" | "job"; export interface PickItem { ref: string; 이름: string; 규격: string; 단위: string; 값: unknown; 관급: boolean; } export const fetchPick = ( kind: PickKind, q: string, ): Promise<{ total: number; items: PickItem[] }> => get("/pick", { kind, q, limit: 50 });