/* ============================================================================= * M01_MasterData_UI_Logic_Api.ts * 로직 화면이 부르는 서버 길 — 계약 `resources/master_data/_화면_계약.md` · `/api/m01` * ========================================================================== */ import { API_BASE_URL } from "@config/config_frontend"; /** 로직 한 줄 — 파일 그대로(한글 칸) · 틀 `_틀.md` 6장 */ export interface LogicInput { 이름: string; 단위?: string; 고르기?: (string | number)[]; 범위?: [number, number]; } export interface HoLine { 종류: string; 요소: string; 이름?: string; 규격?: string; 단위?: string; 수량: string; 비목?: string; } export interface NamedFormula { 이름: string; 식: string; 비목?: string; 출처?: string; } export interface LogicRow { /** 서버가 주는 키 — 새 로직은 "" (저장 때 대장의 다음 번호) */ 키: string; 원문번호: string; 이름: string; 결과단위: string; 출처: string; 소유?: string; 입력: LogicInput[]; 중간: NamedFormula[]; 호표?: HoLine[]; 결과?: { 식: string }; 덧줄?: NamedFormula[]; 끝수?: string | null; 비고?: string; [extra: string]: unknown; } export interface LogicSummary { file: string; 구분: string; 상세구분: string; 키: string; 원문번호: string; 이름: string; 결과단위: string; 출처: string; blocked: boolean; reasons: string[]; } export interface ElementBrief { ref: string; file?: string; 이름?: string; 규격?: string; 단위?: unknown; 값?: unknown; 값칸?: Record | null; } export interface LogicOne { file: string; version: string; logic: LogicRow; blocked: boolean; reasons: string[]; prices: Record; } export interface CalcLine { 이름: string; 단위: string; 수량: number; 단가: number; 금액: number; 비목: Record; /** 값 없는 요소를 다른 값으로 셈 — 「준용: <직종>」 · 「산정: …」 */ 출처?: string; } export type CalcAnswer = | { ok: true; lines?: CalcLine[]; sums?: Record; result?: unknown; middle: Record; } | { ok: false; reason: string }; export interface TextLine { 이름: string; 글: string; /** 같은 줄을 변수 이름으로 적은 식 — 왼쪽 칸 */ 이름글?: string; /** 줄 번호(답 전체에서 하나씩) — 두 칸 줄 맞춤 · 밝히기 */ 짝?: number; 금액: number; 까닭?: string; } /** `POST /text` 답 — 서버가 만든 읽는 식 줄(화면은 그대로 찍음) */ export type TextAnswer = | { ok: true; groups: { 비목: string; 줄: TextLine[]; 소계: number; 끝수: string }[]; 계: number; } | { ok: false; reason: string }; export interface SubBrief { name: string; book: string | null; details: string[]; } export interface LogicFile { file: string; book: string; chapter: string; version: string; } export interface SaveChange { op: "edit" | "add" | "delete"; key?: string; row?: LogicRow; } export interface SaveFile { file: string; version: string; changes: SaveChange[]; } /** 서버가 준 몸과 상태 번호 — 409·422 는 몸(`detail`)을 화면이 풀어 보임 */ export class ApiError extends Error { constructor( public status: number, public detail: unknown, ) { super(typeof detail === "string" ? detail : JSON.stringify(detail)); } } async function request(path: string, body?: unknown): Promise { const response = await fetch(`${API_BASE_URL}/m01${path}`, { method: body === undefined ? "GET" : "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), }); const data = (await response.json().catch(() => ({}))) as { detail?: unknown } & T; if (!response.ok) throw new ApiError(response.status, data.detail ?? response.statusText); return data; } const query = (params: Record): string => new URLSearchParams(params).toString(); export const fetchLogics = (): Promise => request<{ logics: LogicSummary[] }>("/logics").then((d) => d.logics); /** 왼쪽 트리 목록 — 서버가 준 구분·상세구분(장 차례대로) */ export const fetchLogicSubs = (): Promise => request<{ subs: SubBrief[] }>(`/subs?${query({ group: "로직" })}`).then((d) => d.subs); export const fetchLogic = (key: string): Promise => request(`/logic?${query({ key })}`); export const fetchLogicFiles = (): Promise => request<{ files: LogicFile[] }>(`/groups/${encodeURIComponent("로직")}/files`).then( (d) => d.files, ); export const searchElements = ( group: string, q: string, ): Promise<{ total: number; items: ElementBrief[] }> => request(`/elements?${query({ group, q, limit: "100" })}`); export const searchPrice = (q: string): Promise<{ total: number; items: ElementBrief[] }> => request(`/pick?${query({ kind: "price", q, limit: "100" })}`); export const fetchMaterials = (cond: { sub: string; detail?: string; spec?: string; region?: string; }): Promise<{ total: number; 기본: string | null; items: ElementBrief[] }> => request(`/materials?${query({ ...cond, limit: "100" })}`); export const runCalc = (body: { key: string; inputs: Record; row?: LogicRow; file?: string; }): Promise => request("/calc", body); export const runText = (body: { key: string; inputs: Record; row?: LogicRow; file?: string; }): Promise => request("/text", body); export const saveFiles = ( files: SaveFile[], ): Promise<{ files: { file: string; version: string }[] }> => request("/save", { files }); /** 본떠 만들기 — 정본·자체 둘 다 본뜸 · 새 키(`GX…`)는 서버가 줌(계약 5장) */ export const copyLogic = ( key: string, ): Promise<{ file: string; version: string; key: string; logic: LogicRow }> => request("/logic/copy", { key }); /** 복사해서 만들기(계약 7장) — 찾기·로직 부르기 한 덩이를 이름 하나(별칭)로 보임 */ export interface FormulaAlias { 이름: string; 종류: "찾기" | "로직"; 참조: string; /** 되돌릴 원문 부르기 글 — 저장 때 이 글로 갈아 끼움 */ 원문: string; 밑이름: string; 값칸: Record; 결과단위: string; 자리: string[]; } export interface FormulaLine { 자리: string; 갈래: "중간" | "호표" | "덧줄" | "결과"; 차례: number; 이름: string; 단위: string; 비목: string; 식: string; 출처: string; 종류?: string; 요소?: unknown; } export interface LogicFormula { file: string; version: string; key: string; 소유: string; prices: Record; 로직: LogicRow; 별칭: FormulaAlias[]; 줄: FormulaLine[]; } export const fetchLogicFormula = (key: string): Promise => request(`/logic/formula?${query({ key })}`); export const saveLogicFormula = (body: { 로직: LogicRow; 별칭: FormulaAlias[]; owner: string; 이름?: string; 본뜬키?: string; }): Promise<{ file: string; version: string; key: string; logic: LogicRow }> => request("/logic/formula/save", body); /** 저장 전 시험 계산용 — 별칭을 원문으로 되돌린 로직 줄만(저장 안 함) */ export const previewLogicFormula = ( 로직: LogicRow, 별칭: FormulaAlias[], ): Promise<{ logic: LogicRow }> => request("/logic/formula/preview", { 로직, 별칭 }); /* ── 식으로 새 로직 만들기(계약 7장) ── */ export interface DraftFind { 표: string; 값칸: string; 조건: Record; } /** 변수마다 정한 것 */ export interface DraftDecided { 무엇?: string; 단위?: string; 비목?: string; 요소?: string; 찾기?: DraftFind; 값?: string; 설명?: string; } export interface DraftVar { 이름: string; 처음: number; 갈래: "정의됨" | "미정" | "비목합"; 정의줄: number | null; 쓰임: number[]; 무엇: string | null; 단위: string | null; 비목: string | null; 정할것: string[]; } export interface DraftProblem { 줄: number | null; 자리: number | null; 갈래: string; 말: string; } export interface DraftAnswer { 줄: { 줄: number; 이름: string; 식: string; 앞: number }[]; 변수: DraftVar[]; 결과: string; 문제: DraftProblem[]; 다됨: boolean; logic: LogicRow | null; /** 저장했을 때만 */ file?: string; version?: string; key?: string; } export interface DraftHead { 이름: string; 결과단위: string; 원문번호?: string; 출처?: string; 비고?: string; 끝수?: string; } /** 검사·미리보기 · `save` 면 자체 로직(키 `GX`)으로 저장 — 문제가 있으면 422 */ export const draftLogic = (body: { text: string; decided: Record; head?: DraftHead; save?: boolean; owner?: string; }): Promise => request("/logic/draft", body);