Files
Aislo/M01_MasterData/M01_MasterData_Api_Fetch.ts
T
eomsangdonandClaude Opus 5.5 69d989e5c8 feat(717-1): 단가산출 상세 부품 길 주입 · M02 logic-lib 읽기 · 셈 길 넓힘
- Logic_Api.ts · Api_Fetch.ts · Logic_Note.ts — `/m01` 박힘 대신 setLogicApiPaths({ read, calc }) 길 묶음 · 안 넘기면 /m01 그대로 · 저장 · 만들기 · 관리자 길은 늘 /m01
- 새 M02_Logic_Lib_Read.py — /api/m02/logic-lib/ 로그인 길(logic/auto · logic/flow · elements · pick · materials · rows(인력 · 재료 · 기계만) · table(소요량 · 계수만) · calc · text) · M01 길 함수 그대로 부름 · row = 저장 안 한 줄 셈만 · 쓰기 없음
- main.py 등록(로그인만) · 지킴 시험 test_717_1_logic_lib.py + helper

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 15:39:36 +09:00

203 lines
5.9 KiB
TypeScript

/* =============================================================================
* M01_MasterData_Api_Fetch.ts
* 마스터 요소 화면 서버 호출 — 계약 `resources/master_data/_화면_계약.md` · 시스템 관리자만
* ========================================================================== */
import { API_BASE_URL } from "@config/config_frontend";
import { logicApiPaths } from "./M01_MasterData_UI_Logic_Api";
/** 줄·표 한 개 — 파일 그대로(한글 칸). */
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 TableUsage {
공종: string;
대상: string[];
로직키: string[];
}
export interface TableHead {
file: string;
키: string;
원문번호: string;
구분: string;
상세구분: string;
이름: string;
기준: string;
출처: string;
비고?: string;
조건: Record<string, string>;
값칸: Record<string, string>;
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<T>(
path: string,
init: RequestInit = {},
base = "/m01",
): Promise<{ status: number; body: T }> {
const response = await fetch(`${API_BASE_URL}${base}${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>,
base = "/m01",
): 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}`, {}, base);
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 = "",
state = "",
): Promise<RowsPage> =>
get<RowsPage>(
"/rows",
{ file, page, size, q, unlinked: unlinked ? 1 : 0, sub, detail, state },
logicApiPaths().read, // 고르기 표 · 품목 찾기 — 상세 부품 길(717-1)
);
/** 한 테이블 파일은 `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<SubInfo[]> =>
(await fetchSubsAll(where)).subs;
export const fetchTables = (
group: string,
sub: string,
detail: string,
q: string,
page: number,
size: number,
usage = "",
): Promise<TablesPage> => get("/tables", { group, sub, detail, q, page, size, usage });
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 }, logicApiPaths().read);
/** 자재 찾기 — `/pick?kind=price` 감쌈 · 이름·규격 낱말이 모두 든 줄 */
export const searchMaterials = async (
q: string,
): Promise<{ 코드: string; 이름: string; 규격: string; 단위: string }[]> =>
(await fetchPick("price", q)).items.map((m) => ({
코드: m.ref,
이름: m.이름,
규격: m.규격,
단위: m.단위,
}));