Files
Aislo/M01_MasterData/M01_MasterData_Api_Fetch.ts
T
eomsangdonandClaude Opus 5 9de61eb251 knowledge(마스터): 재료 열 한 벌 정리 — 시중물가 → 자재품목 · 값 다섯 열 · 유가·환율도 같은 열
- 재료_시중물가.json → 재료_자재품목.json(테이블ID MT · 키 그대로) · 모든 줄이 같은 열 한 벌(키 · 원문번호 · 구분 · 상세구분 · 이름 · 규격 · 단위 · 물가자료 · 유통물가 · 물가정보 · 거래가격 · 관급 · 출처 · 비고 · 면수)
- 값 묶음(시중·조달)을 풀어 다섯 값을 열로 · 관급 = 조달 값(4,552 줄 · 기간은 비고 「조달 26.7」) · 원문 가격정보만 남은 72 줄은 값 열에 안 넣고 비고 「가격정보 ○○」 · 구분·상세구분은 원문에 분류가 없어 비움
- 재료_오피넷유가 · 환율_한국은행환율도 열 한 벌(기준일 → 규격 · 유가 구분 = 전국평균/시도별 · 상세구분 = 지역) · 상품코드·지역코드 삭제
- 엔진 단가(연결된 줄의 낮은 값 · 출처 = 「<키>.<열>」) · check_master 열 검사 · M01 고르기(procure 걷어냄) · 빌더 · _틀.md · 화면 계약 · 시험 같이 고침

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgUhN55Z3BCYhUJTjwTNfj
2026-09-20 15:55:40 +09:00

147 lines
4.4 KiB
TypeScript

/* =============================================================================
* M01_MasterData_Api_Fetch.ts
* 마스터 요소 화면 서버 호출 — 계약 `resources/master_data/_화면_계약.md` · 시스템 관리자만
* ========================================================================== */
import { API_BASE_URL } from "@config/config_frontend";
/** 줄·표 한 개 — 파일 그대로(한글 칸). */
export type Row = Record<string, unknown>;
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<string, string>;
}
export interface TableHead {
: string;
원문번호: string;
이름: string;
기준: string;
출처: string;
조건: Record<string, string>;
값칸: Record<string, string>;
count: number;
}
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<T>(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<T>(path: string, params: Record<string, string | number>): Promise<T> {
const query = new URLSearchParams(Object.entries(params).map(([k, v]) => [k, String(v)]));
const { status, body } = await call<T & { detail?: unknown }>(`${path}?${query}`);
if (status !== 200) throw new Error(String(body.detail ?? `HTTP ${status}`));
return body;
}
export const fetchGroups = async (): Promise<GroupInfo[]> =>
(await get<{ groups: GroupInfo[] }>("/groups", {})).groups;
export const fetchFiles = async (group: string): Promise<FileInfo[]> =>
(await get<{ files: FileInfo[] }>(`/groups/${encodeURIComponent(group)}/files`, {})).files;
export const fetchRows = (
file: string,
page: number,
size: number,
q: string,
unlinked = false,
sub = "",
detail = "",
): Promise<RowsPage> =>
get<RowsPage>("/rows", { file, page, size, q, unlinked: unlinked ? 1 : 0, sub, detail });
export const fetchSubs = async (file: string): Promise<SubInfo[]> =>
(await get<{ subs: SubInfo[] }>("/subs", { file })).subs;
export const fetchTables = (
file: string,
q: string,
): Promise<{ version: string; tables: TableHead[] }> => get("/tables", { file, q });
export const fetchTable = (
file: string,
key: string,
): Promise<{ version: string; table: Row & { : Row[] } }> => get("/table", { file, key });
export async function saveFiles(files: FileChanges[]): Promise<SaveResult> {
const { status, body } = await call<Record<string, unknown>>("/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 });