- M02_CF_Router.py — /api/m02/cf/list(내 것 + _sample 견본) · get · save · delete · values · user-elements(만들기 · 지우기 · _data) · 키 CF-0001 · UE-0001 - 저장: 입력 상자 값(요소 · 표 · 변수)을 마스터에서 다시 받고 → 함수 상자 시트를 한 통합문서로 묶어(받는 칸 값 · 함수→함수 시트 사이 참조) Node 로 셈 → 받는 칸에 값만 되돌려 넣음 · 출력 상자 비용 종류별 원가 - 알약 이름: 입력 = 요소 name · 「표」 · 변수 name / 함수 받는 = inputs[].port · 내보내는 = output.name / 출력 받는 = 「값」 - 입력 값: 인력 · 재료(키 · 검색어 묶음) · 유가전력 · 환율 · 요율(rate_percent) · 기계(값 칸 · 시간당 경비 로직) · 소요량 / 계수 표(블록 · 한 줄) · 사용자 요소 - M02_CF_Api.ts — Model 타입(CfLogic · CfGroup · CfUserElement)을 씀 · CfQuery · CfAnswer · CfComputed · 쉬운 말 - main.py 등록 한 문장 · 시험 test_m02_cf_router.py Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KmELpdXFDRZHhfbGuRhSmL
159 lines
6.5 KiB
TypeScript
159 lines
6.5 KiB
TypeScript
/* =============================================================================
|
|
* M02_CF_Api.ts
|
|
* 단가산출 다이어그램 Api — 목록(내 것 + 견본) · 열기 · 저장(서버가 입력 값을 마스터에서 다시 받아
|
|
* 함수 상자 시트를 같은 엔진으로 셈하고 받는 칸에 값만 되돌려 넣음) · 지우기 · 입력 값 한 번에 받기 ·
|
|
* 사용자 요소. 모양 = sub7 Model(CfLogic · CfGroup · CfUserElement) · 아래는 Api 몫만.
|
|
* 서버 = `M02_CF_Router.py`(`/api/m02/cf/*`) · 정함 `docs/raw/M01_쉬운단가/원가다이어그램_정함.md`
|
|
* ========================================================================== */
|
|
|
|
import { API_BASE_URL } from "@config/config_frontend";
|
|
import type { CfGroup, CfLogic, CfUserElement } from "./M02_CF_Model";
|
|
|
|
/** 돈 로직 결과에서 꺼낼 칸(기계 시간당 경비) */
|
|
export type CfField = "노무비" | "재료비" | "경비" | "계";
|
|
|
|
/** 입력 값 물음 한 줄 — 값은 열 때 · 저장 때 마스터(사용자 요소 `UE-…` 는 다이어그램 안 · `_data`)에서
|
|
* · 인력 · 재료 ref = 키 → 그 줄 단가(기존 계산과 같은 셈) · 재료 ref = 검색어 묶음 JSON 글도 됨(unit 을 주면
|
|
* 기존 로직 재료 줄과 같은 단가)
|
|
* · 유가전력 · 환율 = 그 줄 값 · 요율 = 값 묶음의 field(기본 rate_percent · %)
|
|
* · 기계 ref = EQ 키 + field(값 칸 — 가격 · 손료계수 …) 또는 `로직(GC…, 기계='…', 지역=지역)` + inputs → 한 시간 값
|
|
* · 소요량 · 계수 table = {key, row?} → 표 블록(머리 줄 포함 · 조건 칸 → 값 칸 · 범위는 「·아래」 「·위」) */
|
|
export interface CfQuery {
|
|
id: string;
|
|
group: CfGroup;
|
|
ref?: string;
|
|
table?: { key: string; row?: number };
|
|
unit?: string;
|
|
field?: string;
|
|
inputs?: Record<string, unknown>;
|
|
}
|
|
|
|
/** 입력 값 답 — 표는 `rows` · 그 밖은 `value` · 못 구하면 ok false + 쉬운 말 */
|
|
export interface CfAnswer {
|
|
id: string;
|
|
ok: boolean;
|
|
value?: number | string | null;
|
|
rows?: (string | number | null)[][];
|
|
name?: string;
|
|
unit?: string;
|
|
reason?: string;
|
|
}
|
|
|
|
/** 서버 계산 결과 — 저장 답 */
|
|
export interface CfComputed {
|
|
/** 상자 id → 내보내는 알약 이름 → 값(표는 칸 덩어리) */
|
|
ports: Record<string, Record<string, unknown>>;
|
|
/** 출력 상자 id → 값 */
|
|
outputs: Record<string, number | string | boolean | null>;
|
|
cost: {
|
|
노무비: number;
|
|
재료비: number;
|
|
경비: number;
|
|
값들: Record<string, number | string | boolean | null>;
|
|
계: number;
|
|
};
|
|
/** 쉬운 말 */
|
|
errors: string[];
|
|
}
|
|
|
|
/** 목록 한 줄 — `sample` = `_sample` 견본(읽기만 · 저장하면 새 번호) */
|
|
export interface CfListItem {
|
|
key: string;
|
|
name: string;
|
|
resultUnit: string;
|
|
saved: string;
|
|
sample: boolean;
|
|
}
|
|
|
|
/** 저장 답 — `logic` 은 받는 칸에 서버 값이 들어간 다이어그램 */
|
|
export interface CfSaved {
|
|
key: string;
|
|
logic: CfLogic;
|
|
values: CfComputed;
|
|
}
|
|
|
|
/** 서버가 준 몸과 상태 번호 — 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 });
|
|
}
|
|
|
|
/** 입력 값 한 번에 — 열 때 · 예시값을 바꿀 때 화면이 받아 시트에 넣음 ·
|
|
* userElements = 이 다이어그램 안 사용자 요소(ref 가 `UE-…` 일 때 먼저 봄) */
|
|
export async function cfValues(
|
|
items: CfQuery[],
|
|
userElements: CfUserElement[] = [],
|
|
): Promise<CfAnswer[]> {
|
|
return (await send<{ values: CfAnswer[] }>("/values", { items, userElements })).values;
|
|
}
|
|
|
|
/** 사용자 요소 목록 */
|
|
export async function cfUserElements(): Promise<CfUserElement[]> {
|
|
return (await send<{ elements: CfUserElement[] }>("/user-elements")).elements;
|
|
}
|
|
|
|
/** 사용자 요소 만들기(공용 목록 `_data`) — id(`UE-0001`)는 서버가 줌 · 마스터에 안 씀 */
|
|
export async function cfUserElementAdd(element: Omit<CfUserElement, "id">): Promise<CfUserElement> {
|
|
return (await send<{ element: CfUserElement }>("/user-elements", { op: "add", element })).element;
|
|
}
|
|
|
|
export async function cfUserElementDelete(id: string): Promise<void> {
|
|
await send<{ id: string }>("/user-elements", { op: "delete", id });
|
|
}
|
|
|
|
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] ?? "저장하지 못함 — 잠시 뒤 다시 해 주세요";
|
|
}
|