Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KmELpdXFDRZHhfbGuRhSmL
177 lines
7.2 KiB
TypeScript
177 lines
7.2 KiB
TypeScript
/* =============================================================================
|
|
* M02_CF_Api.ts
|
|
* 단가산출 다이어그램 Api — 목록(내 것 + 견본) · 열기 · 저장(서버가 입력 값을 마스터에서 다시 넣고
|
|
* 같은 엔진으로 함수 상자 시트를 다시 셈) · 지우기 · 입력 값 한 번에 받기 · 사용자 요소
|
|
* 서버 = `M02_CF_Router.py`(`/api/m02/cf/*`) · 정함 `docs/raw/M01_쉬운단가/원가다이어그램_정함.md`
|
|
* ========================================================================== */
|
|
|
|
import { API_BASE_URL } from "@config/config_frontend";
|
|
import type { Workbook } from "../../A00_Common/spreadsheet/spreadsheet_types";
|
|
|
|
/** 입력 그룹 — M01 저장 그룹 + 사용자 요소 */
|
|
export type CfGroup =
|
|
"인력" | "재료" | "유가전력" | "기계" | "소요량" | "계수" | "환율" | "요율" | "사용자";
|
|
|
|
/** 비용 종류 칸 */
|
|
export type CfCost = "노무비" | "재료비" | "경비" | "계";
|
|
|
|
/** 입력 값 물음 한 줄 — 값은 열 때 · 저장 때 마스터(사용자 요소는 `_data`)에서 새로
|
|
* · 인력 · 재료 = 그 줄 단가(기존 계산과 같은 셈 · 준용 · 낮은 값) — 재료 key 는 키 또는 검색어 묶음 JSON 글,
|
|
* unit(그 줄 단위)을 주면 기존 로직 재료 줄과 같은 단가
|
|
* · 유가전력 · 환율 · 사용자 = 그 줄 값 · 요율 = 값 묶음의 field(기본 rate_percent · %)
|
|
* · 기계 = key(EQ…) + field(값 칸 이름 — 가격 · 손료계수 …) 또는 element(기계 시간당 경비 로직 부름 글) + inputs → 한 시간 값
|
|
* · 소요량 · 계수 = 표 블록(머리 줄 포함 · 조건 칸 → 값 칸 · 범위는 「이름·아래」 「이름·위」) · row 를 주면 그 줄 하나 */
|
|
export type CfQuery =
|
|
| { id: string; group: "인력" | "유가전력" | "환율" | "사용자"; key: string }
|
|
| { id: string; group: "요율"; key: string; field?: string }
|
|
| { id: string; group: "재료"; key: string; unit?: string }
|
|
| { id: string; group: "기계"; key: string; field?: string }
|
|
| {
|
|
id: string;
|
|
group: "기계";
|
|
element: string;
|
|
inputs: Record<string, unknown>;
|
|
field?: CfCost;
|
|
}
|
|
| { id: string; group: "소요량" | "계수"; table: string; row?: number };
|
|
|
|
/** 입력 값 답 — 표는 `rows`(머리 줄 포함) · 그 밖은 `value` · 못 구하면 ok false + 쉬운 말 */
|
|
export interface CfValue {
|
|
id: string;
|
|
ok: boolean;
|
|
value?: number | string | null;
|
|
rows?: (string | number | null)[][];
|
|
name?: string;
|
|
unit?: string;
|
|
reason?: string;
|
|
}
|
|
|
|
/** 저장 때 입력 값을 넣을 자리 — 함수 상자 시트 id · 칸(표는 이 칸을 왼쪽 위로 펼침) */
|
|
export interface CfFill {
|
|
item: CfQuery;
|
|
sheet: string;
|
|
cell: string;
|
|
}
|
|
|
|
/** 다이어그램 한 벌 = 통합문서(함수 상자 = 시트) + 상자 · 선(다른 창 칸은 그대로 저장 · 돌려줌) */
|
|
export interface CfLogic {
|
|
/** 새 것 null · 저장하면 서버가 `CF-0001` 꼴로 줌 · 견본은 그 이름(저장하면 새 번호) */
|
|
key: string | null;
|
|
name: string;
|
|
resultUnit?: string;
|
|
workbook: Workbook;
|
|
/** 입력 값 자리 — 서버가 저장 때 마스터에서 다시 넣음 */
|
|
fills?: CfFill[];
|
|
[extra: string]: unknown;
|
|
}
|
|
|
|
/** 목록 한 줄 — `sample` = `_sample` 견본(읽기만 · 저장하면 새 번호) */
|
|
export interface CfListItem {
|
|
key: string;
|
|
name: string;
|
|
resultUnit: string;
|
|
saved: string;
|
|
sample: boolean;
|
|
}
|
|
|
|
/** 저장 답 — `logic.workbook.계산값` 은 서버가 다시 셈한 값 · `values` 는 넣은 입력 값 */
|
|
export interface CfSaved {
|
|
key: string;
|
|
logic: CfLogic;
|
|
values: CfValue[];
|
|
}
|
|
|
|
/** 사용자 요소 — 마스터에 없는 요소(시험 폴더 `_data` 에만) · 키 `UE-0001` 꼴 */
|
|
export interface CfUserElement {
|
|
key: string;
|
|
group: CfGroup;
|
|
이름: string;
|
|
규격: string;
|
|
단위: string;
|
|
값: number | null;
|
|
비고?: string;
|
|
}
|
|
|
|
/** 서버가 준 몸과 상태 번호 — 0 = 서버에 못 닿음 */
|
|
export class CfError extends Error {
|
|
constructor(
|
|
public status: number,
|
|
public detail: unknown,
|
|
) {
|
|
super(typeof detail === "string" ? detail : JSON.stringify(detail));
|
|
}
|
|
}
|
|
|
|
async function send<T>(path: string, body?: unknown): Promise<T> {
|
|
let response: Response;
|
|
try {
|
|
response = await fetch(`${API_BASE_URL}/m02/cf${path}`, {
|
|
method: body === undefined ? "GET" : "POST",
|
|
credentials: "include",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
});
|
|
} catch {
|
|
throw new CfError(0, "");
|
|
}
|
|
const data = (await response.json().catch(() => ({}))) as { detail?: unknown } & T;
|
|
if (!response.ok) throw new CfError(response.status, data.detail ?? response.statusText);
|
|
return data;
|
|
}
|
|
|
|
/** 내 다이어그램(저장 차례) + 견본 */
|
|
export async function cfList(): Promise<CfListItem[]> {
|
|
return (await send<{ logics: CfListItem[] }>("/list")).logics;
|
|
}
|
|
|
|
export async function cfGet(key: string): Promise<CfLogic> {
|
|
return (await send<{ logic: CfLogic }>(`/get?${new URLSearchParams({ key }).toString()}`)).logic;
|
|
}
|
|
|
|
/** 저장 — 서버가 입력 값을 마스터에서 다시 넣고 같은 엔진으로 다시 셈(브라우저 계산값은 버림) ·
|
|
* 견본을 저장하면 새 번호 */
|
|
export async function cfSave(logic: CfLogic): Promise<CfSaved> {
|
|
return send<CfSaved>("/save", { logic });
|
|
}
|
|
|
|
export async function cfDelete(key: string): Promise<void> {
|
|
await send<{ key: string }>("/delete", { key });
|
|
}
|
|
|
|
/** 입력 값 한 번에 — 열 때 · 예시값을 바꿀 때 화면이 받아 시트에 넣음 */
|
|
export async function cfValues(items: CfQuery[]): Promise<CfValue[]> {
|
|
return (await send<{ values: CfValue[] }>("/values", { items })).values;
|
|
}
|
|
|
|
/** 사용자 요소 목록 */
|
|
export async function cfUserElements(): Promise<CfUserElement[]> {
|
|
return (await send<{ elements: CfUserElement[] }>("/user-elements")).elements;
|
|
}
|
|
|
|
/** 사용자 요소 만들기 — 키는 서버가 줌 */
|
|
export async function cfUserElementAdd(
|
|
element: Omit<CfUserElement, "key">,
|
|
): Promise<CfUserElement> {
|
|
return (await send<{ element: CfUserElement }>("/user-elements", { op: "add", element })).element;
|
|
}
|
|
|
|
export async function cfUserElementDelete(key: string): Promise<void> {
|
|
await send<{ key: string }>("/user-elements", { op: "delete", key });
|
|
}
|
|
|
|
const PLAIN: Record<number, string> = {
|
|
0: "서버와 이어지지 않음 — 잠시 뒤 다시 해 주세요",
|
|
400: "넣은 값의 모양이 맞지 않음",
|
|
403: "견본은 바꾸거나 지울 수 없음 — 저장하면 새 다이어그램으로 남음",
|
|
404: "찾을 수 없음 — 이미 지워졌을 수 있음 · 목록을 다시 열어 주세요",
|
|
503: "엑셀 계산을 서버에서 못 함 — 잠시 뒤 다시 해 주세요",
|
|
};
|
|
|
|
/** 실패 → 쉬운 말(키 · 날 글이 섞인 서버 글은 뭉뚱그림) */
|
|
export function cfPlain(e: unknown): string {
|
|
if (!(e instanceof CfError)) return e instanceof Error ? e.message : PLAIN[0];
|
|
const text = typeof e.detail === "string" ? e.detail : "";
|
|
if (e.status === 400 && text && !/[A-Z]{2}\d{6}|[{}]/.test(text)) return text;
|
|
return PLAIN[e.status] ?? "저장하지 못함 — 잠시 뒤 다시 해 주세요";
|
|
}
|