/* ============================================================================= * M02_MasterTemplete_Labor_Api.ts * 일위대가 조합 서버 길 — 층(마스터 UA · 회사 UC · 개인 UP) 조합 표 `일위대가조합.json` * 길 = `/api/m02/layers/{layer}/combos`(PLAN 27-2a) · 키 하나 = `/api/m02/combos/key/{키}` · * 담을 로직 읽기 = `/api/m02/logic-lib/…` · 모양 = `_화면_계약.md` 9장 * ========================================================================== */ import { API_BASE_URL } from "@config/config_frontend"; import { ApiError, type LogicSummary, type SubBrief, } from "../M01_MasterData/M01_MasterData_UI_Logic_Api"; import type { LaborCalc } from "./M02_MasterTemplete_Labor_Calc"; import { getActiveLayer, getActiveOwner, type ItemLogic, type Layer, } from "./M02_MasterTemplete_Api_Fetch"; /** 담은 로직 한 줄 — 로직 키와 메모뿐(수량·계산은 마스터에 두지 않음 · 계약 9장) */ export interface ComboLogic { 로직: string; 메모: string | null; } /** 조합 한 줄 통째 — `_틀.md` 10장 · 키·소유는 서버 것이 이김 */ export interface Combo { 키: string; 이름: string; 구분: string | null; 상세구분: string | null; 단위: string | null; 담은로직: ComboLogic[]; 출처: string | null; 소유: string | null; /** 이 조합을 본떠 만든 원본 키 — 비면 원본(M02 가 갈라 낸 조합에 채워짐) */ 본뜬키: string | null; 비고: string | null; } export interface ComboSummary extends Combo { count: number; blocked: boolean; reasons: string[]; } /** `GET /combo` 의 `줄` — 담은 로직에 이름을 붙인 것 */ export interface ComboLine { 차례: number; 로직: string; 메모: string | null; ok: boolean; 까닭?: string; 이름?: string; 구분?: string; 상세구분?: string; 결과단위?: string; } export interface ComboOne { file: string; version: string; combo: Combo; 줄: ComboLine[]; } /** 조합 층 — 층 + 주인(회사 `7` · 개인 `7/42` · 없으면 내 것) */ export interface LaborScope { layer: Exclude; owner: string | null; } export const SYSTEM_SCOPE: LaborScope = { layer: "system", owner: null }; /** 폴더 = [구분] · [구분, 상세구분] — 깊이 2(PLAN 27-2a) */ export interface ComboList { 판: string; 폴더: string[][]; 줄: ComboSummary[]; } async function request(url: string, method = "GET", body?: unknown): Promise { const response = await fetch(`${API_BASE_URL}/m02${url}`, { method, 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 enc = encodeURIComponent; /** 층 길 — `path` 뒤에 주인 · 더 붙일 물음 */ const at = (scope: LaborScope, path = "", more: Record = {}): string => { const params = new URLSearchParams(more); if (scope.owner) params.set("owner", scope.owner); const q = params.toString(); return `/layers/${scope.layer}/combos${path}${q ? `?${q}` : ""}`; }; export const fetchComboList = (scope: LaborScope = SYSTEM_SCOPE): Promise => request(at(scope)); export const fetchCombos = (scope: LaborScope = SYSTEM_SCOPE): Promise => fetchComboList(scope).then((d) => d.줄); /** 키 하나 — 어느 층이든(그 층 읽기 권한) · `version` = 그 층 조합 표 판 */ export const fetchCombo = (key: string): Promise => request & LaborScope & { 판: string }>( `/combos/key/${enc(key)}`, ).then((d) => ({ ...d, file: "일위대가조합.json", version: d.판 })); /* --- 담을 로직(읽기만 · 로그인) — M01 로직 길과 같은 모양 --- */ export const fetchLibLogics = (): Promise => request<{ logics: LogicSummary[] }>("/logic-lib/logics").then((d) => d.logics); export const fetchLibLogicSubs = (): Promise => request<{ subs: SubBrief[] }>("/logic-lib/subs").then((d) => d.subs); /* --- 일위대가 문서(39 · 구조물 열마다 `labor/<열>.json` · 틀 = sub7 39-1a) --- */ /** 손 입력 수량 — `null` 이면 면적산출 알약 연결(칸)을 따름 · 손이 칸보다 앞섬 */ export type LaborQty = { 손: number } | null; /** 담은 로직 한 줄 — 입력값 = 그 로직 입력 이름만(빈 값 = 자동값) */ export interface LaborRow { 로직: string; 메모: string | null; 입력값: Record; 수량: LaborQty; } export interface LaborMat { 코드: string; 이름: string | null; 규격: string | null; 단위: string | null; 수량: LaborQty; } export interface LaborDoc { 종류: "일위대가"; 판?: number; 열: string; 일위대가번?: string; 단위: string | null; 본뜬?: { 층: string; 주인: string | null; 키: string } | null; 담은로직: LaborRow[]; 자재: LaborMat[]; 계산값?: unknown; } /** 받은 문서를 화면 모양으로 — 빈 칸 · 옛 모양도 받음(빈 일위대가) */ export function normalizeLabor(doc: unknown, column: string): LaborDoc { const d = (doc && typeof doc === "object" ? doc : {}) as Partial; return { ...d, 종류: "일위대가", 열: d.열 ?? column, 단위: d.단위 ?? null, 담은로직: (d.담은로직 ?? []).map((r) => ({ 로직: r.로직, 메모: r.메모 ?? null, 입력값: r.입력값 ?? {}, 수량: r.수량 ?? null, })), 자재: (d.자재 ?? []).map((m) => ({ 코드: m.코드, 이름: m.이름 ?? null, 규격: m.규격 ?? null, 단위: m.단위 ?? null, 수량: m.수량 ?? null, })), }; } /** 면적산출 문서의 알약 → {칸, 값}(식 칸 = 서버 `계산값` · 그 밖 = 칸 값 · 수만) — 서버 `area_values` 와 같은 규칙 */ export function areaLinks(area: unknown): Map { const out = new Map(); const doc = (area && typeof area === "object" ? area : {}) as { 시트?: { id?: string; 칸?: Record; 연결?: { 키?: string; 칸?: string }[]; }[]; 계산값?: Record>; }; for (const sheet of doc.시트 ?? []) { const solved = doc.계산값?.[sheet.id ?? ""] ?? {}; for (const link of sheet.연결 ?? []) { if (!link.키 || !link.칸) continue; const raw = solved[link.칸]?.수 ?? sheet.칸?.[link.칸]?.값; const n = raw === null || raw === undefined || raw === "" ? NaN : Number(String(raw).replace(/,/g, "")); out.set(link.키, { 칸: link.칸, 값: Number.isFinite(n) ? n : null }); } } return out; } /** calc 답 모양 · 수량 · 반영 짝 셈 = `M02_MasterTemplete_Labor_Calc.ts`(sub7 · ↔ `M02_Labor_Doc.py`) */ export type { CalcLine as LaborCalcLine, CalcMaterial as LaborCalcMat, LaborCalc, Money as LaborCost, } from "./M02_MasterTemplete_Labor_Calc"; /** 지금 층이면 `owner` 를 붙인 층 길(`/layers/{층}/labor/{열}…`) */ const laborAt = (layer: Layer, column: string, tail: string): string => { const owner = layer === getActiveLayer() ? getActiveOwner() : null; const q = owner && layer !== "system" && layer !== "project" ? `?owner=${enc(owner)}` : ""; return `/layers/${layer}/labor/${enc(column)}${tail}${q}`; }; /** 계산(서버 · 저장 안 함) — `doc` = 화면 캐시(없으면 저장본) · `수량` = {알약 키: 칸 값} 바꿔 끼우기 */ export const calcLabor = ( layer: Layer, column: string, doc: LaborDoc | null, 수량?: Record, ): Promise => request(laborAt(layer, column, "/calc"), "POST", { ...(doc ? { doc } : {}), ...(수량 ? { 수량 } : {}), }); /** 담은 로직 입력 목록 + 자동값(입력 모달 · `M02_Labor_Inputs`) */ export const fetchLogicInputs = ( keys: string[], ): Promise<{ 로직: ItemLogic[]; 빠진로직: string[] }> => keys.length ? request(`/logic-lib/inputs?keys=${enc(keys.join(","))}`) : Promise.resolve({ 로직: [], 빠진로직: [] });