/* ============================================================================= * M02_MasterTemplete_Api_Fetch.ts * 마스터 템플릿 서버 호출 — 계약 `6_계약.md` 서버 길 · 시스템 층은 시스템 관리자만 * ========================================================================== */ import { API_BASE_URL } from "@config/config_frontend"; /** 층 이름 — 서버와 같은 글 */ export type Layer = "system" | "company" | "personal" | "project"; /** 서버 종류 — 화면의 「표 양식」 = table · 「도면 양식」 = drawing · 「구조물 도면」 = structure · * 「면적산출」 = area · 「재료산출」 = material(둘 다 구조물집계표 열마다 한 통합문서) */ export type Kind = "table" | "drawing" | "structure" | "area" | "material"; export interface TemplateInfo { 종류: Kind; 이름: string; 판: string; 수정일: string; /** 프로그램이 이름으로 부르는 양식이면 그 쓰는 곳 — 있으면 못 지움 */ 쓰는곳?: string; } export interface TemplateDoc { 종류: Kind; 이름: string; /** 프로젝트 층에서 작업본이 아직 없으면(옛 프로젝트) 시스템 양식이 와서 null */ 판: string | null; 문서: unknown; } /** 서버가 판 불일치로 막았을 때 */ export class StaleError extends Error {} async function call(path: string, init: RequestInit = {}): Promise { const response = await fetch(`${API_BASE_URL}/m02${path}`, { credentials: "include", headers: { "Content-Type": "application/json" }, ...init, }); const body = (await response.json().catch(() => ({}))) as { detail?: unknown } & T; if (response.status === 409) throw new StaleError(reason(body.detail, response.status)); if (!response.ok) throw new Error(reason(body.detail, response.status)); return body; } /** 서버가 막은 까닭 그대로 — 문장 · `{errors: [...]}` · 입력 검사 목록(`[{msg}]`) */ function reason(detail: unknown, status: number): string { if (typeof detail === "string") return detail; const list = Array.isArray(detail) ? detail : (detail as { errors?: unknown } | null)?.errors; const lines = Array.isArray(list) ? list.map((x) => (typeof x === "string" ? x : String((x as { msg?: unknown })?.msg ?? ""))) : []; return lines.filter(Boolean).join(" · ") || `HTTP ${status}`; } const enc = encodeURIComponent; const q = (projectId: string | null): string => (projectId ? `?project_id=${enc(projectId)}` : ""); const head = (layer: Layer): string => layer === "system" ? "/templates" : `/layers/${layer}/templates`; const item = (layer: Layer, kind: Kind, name: string, projectId: string | null): string => `${head(layer)}/${kind}/${enc(name)}${layer === "system" ? "" : q(projectId)}`; /** 시스템 길은 배열 · 층 길은 `{층, 양식: [...]}` — 둘 다 목록으로 */ export const listTemplates = async ( layer: Layer, projectId: string | null, ): Promise => { const got = await call( layer === "system" ? head(layer) : `${head(layer)}${q(projectId)}`, ); return Array.isArray(got) ? got : got.양식; }; export const readTemplate = ( layer: Layer, kind: Kind, name: string, projectId: string | null, ): Promise => call(item(layer, kind, name, projectId)); export const saveTemplate = ( layer: Layer, kind: Kind, name: string, projectId: string | null, 판: string, 문서: unknown, ): Promise => call(item(layer, kind, name, projectId), { method: "PUT", body: JSON.stringify({ 판, 문서 }) }); /** 시스템 층만 지움 */ export const deleteTemplate = (kind: Kind, name: string, 판: string): Promise<{ ok: boolean }> => call(`/templates/${kind}/${enc(name)}?판=${enc(판)}`, { method: "DELETE" }); /** 개인·회사 층에서 지움 — 판이 맞을 때만(다르면 409) */ export const deleteLayerTemplate = ( layer: Layer, kind: Kind, name: string, 판: string, ): Promise<{ ok: boolean }> => call(`/layers/${layer}/templates/${kind}/${enc(name)}?판=${enc(판)}`, { method: "DELETE", }); /* --- 구조물 도면(종류 structure · 이름 = 구조물집계표 열 id) --- * 읽기 · 저장 · 지우기는 위 readTemplate · saveTemplate · deleteTemplate 에 "structure" */ /** 구조물 도면 문서 — `도면` = 도면 양식과 같은 CAD 문서 · 면적산출 · 재료산출은 따로(같은 열 id) */ export interface StructureDoc { 양식: "구조물도면"; 종류: "structure"; 판: number; 열: string; 도번: string; 도면: { entities: unknown[] } & Record; } /** 시스템 층 구조물 도면 목록 */ export const listStructures = async (): Promise => (await listTemplates("system", null)).filter((t) => t.종류 === "structure"); /** 새로 — 도번 자동 · 빈 도면 (+ 없으면 빈 면적산출 · 재료산출 통합문서) · 이미 있으면 StaleError */ export const createStructure = (column: string): Promise => call(`/structures/${enc(column)}`, { method: "POST" }); /** 한 열의 번호 — 도번 `SD` · 면적번 `SA` · 재료번 `SM` + 같은 8자리 * [종류][분류1][분류2][분류3](없는 쪽은 빈 글) */ export interface StructureNumbers { 도번: string; 면적번: string; 재료번: string; } /** `{열 id: {도번, 면적번, 재료번}}` — 구조물 열만(글 열 · 없는 열은 키 없음 = 「없음」) */ export const fetchStructureNumbers = (): Promise> => call("/structures/numbers"); /** 구조물집계표조합 한 줄 — 구조물 열 하나(키 `SS` + 6자리 · 한 번 준 키는 안 바뀜) */ export interface StructureCombo extends StructureNumbers { 키: string; 열: string; 종류: string | null; 분류1: string | null; 분류2: string | null; 분류3: string | null; 단위: string | null; 일위대가: string | null; /** 자재 조합 키 `UM` */ 자재조합: string | null; 바인딩: unknown; 펼침: unknown; } export interface StructureCombos { 그룹: string; 판: number; 비고: string; 다음: number; 줄: StructureCombo[]; } /** 조합 표 — 서버가 집계표 [저장] 때 맞춤(읽기만) */ export const fetchStructureCombos = (): Promise => call("/structure-combos"); /** 조합 갈라 내기 결과 — `새로만듦` false = 같은 담은 로직의 조합이 이미 있어 그 키를 가리킴 */ export interface BranchedCombo { 키: string; 이름: string; 새로만듦: boolean; 집계표판: string; } /** 이 열이 가리키는 일위대가 조합을 고친 담은 로직으로 갈라 냄 — 새 UA · 집계표 그 열만 새 키 · * `판`(집계표 판)을 주면 어긋날 때 StaleError · 원래 조합은 그대로 */ export const branchCombo = ( column: string, body: { 본뜬키: string; 담은로직: { 로직: string; 메모?: string | null }[]; 이름: string; 판?: string; }, ): Promise => call(`/structures/${enc(column)}/combo`, { method: "POST", body: JSON.stringify(body) }); /** 자재 조합(키 `UM`) 담은 자재 한 줄 */ export interface MaterialItem { 코드: string; 이름: string | null; 규격: string | null; 단위: string | null; } /** 자재 조합 한 줄 — 본뜬키 = 갈라 낸 원래 키(없으면 null) */ export interface MaterialCombo { 키: string; 이름: string; 담은자재: MaterialItem[]; 본뜬키: string | null; 소유: string; 비고: string | null; } export const fetchMaterialCombo = (key: string): Promise => call(`/material-combos/${enc(key)}`); /** 이 열의 자재 조합을 고친 담은 자재로 갈라 냄 — 규칙은 `branchCombo` 와 같음 · * `본뜬키` 빈 글 = 이 열의 첫 자재 조합 */ export const branchMaterialCombo = ( column: string, body: { 본뜬키?: string; 담은자재: MaterialItem[]; 이름: string; 판?: string }, ): Promise => call(`/structures/${enc(column)}/material-combo`, { method: "POST", body: JSON.stringify(body), }); /* --- 자재 조합 표(마스터 템플릿 `자재조합.json`) — 좌측 「자재 조합」 컨테이너 --- */ /** 표 통째 — 판(파일 판본) · 줄(담은 자재 포함) · 쓰는곳(키 → 쓰는 구조물 열 이름들) */ export const listMaterialCombos = (): Promise<{ 판: string; 줄: MaterialCombo[]; 쓰는곳: Record; }> => call("/material-combos"); /** 새로 · 본떠 만들기(`본뜬키`) — 키는 서버가 표 「다음」 번호로 줌 */ export const createMaterialCombo = ( 판: string, 줄: Omit, ): Promise<{ 키: string; 판: string; 줄: MaterialCombo }> => call("/material-combos", { method: "POST", body: JSON.stringify({ 판, 줄 }) }); /** 고치기 — 이름 · 비고 · 담은자재(본뜬키 · 소유는 서버가 있던 것으로) */ export const editMaterialCombo = ( 키: string, 판: string, 줄: MaterialCombo, ): Promise<{ 키: string; 판: string; 줄: MaterialCombo }> => call(`/material-combos/${enc(키)}`, { method: "PUT", body: JSON.stringify({ 판, 줄 }) }); /** 지우기 — 구조물 열이 쓰면 서버가 막고 그 열 이름들을 까닭으로 줌 */ export const deleteMaterialCombo = (키: string, 판: string): Promise => call(`/material-combos/${enc(키)}?판=${enc(판)}`, { method: "DELETE" }); /** 다른 열(`from`) 구조물 도면 내용을 받는 열(`column`)로 가져옴 — 도번 · 열은 받는 쪽 그대로 */ export const copyStructure = async (column: string, from: string): Promise => { await call(`/structures/${enc(column)}/copy?from=${enc(from)}`, { method: "POST" }); }; /** 다른 열(`from`) 면적산출 내용(알약 연결 포함)을 받는 열(`column`)로 가져옴 — 면적번 · 열은 그대로 */ export const copyArea = async (column: string, from: string): Promise => { await call(`/area/${enc(column)}/copy?from=${enc(from)}`, { method: "POST" }); }; /** 다른 열(`from`) 재료산출 내용(알약 연결 포함)을 받는 열(`column`)로 가져옴 — 재료번 · 열은 그대로 */ export const copyMaterial = async (column: string, from: string): Promise => { await call(`/material/${enc(column)}/copy?from=${enc(from)}`, { method: "POST" }); }; /* --- 프로젝트 층 단추 다섯 --- */ const project = (id: string, tail: string): string => `/projects/${enc(id)}/templates/${tail}`; const post = (path: string, body: object = {}): Promise => call(path, { method: "POST", body: JSON.stringify(body) }); export const resetProject = (id: string): Promise => post(project(id, "reset")); export const applyToProject = ( id: string, from: { from: Layer; user_id?: string; project_id?: string; 종류?: Kind; 이름?: string }, ): Promise => post(project(id, "apply"), from); export const saveProjectAs = ( id: string, to: "personal" | "company", 종류: Kind, 이름: string, ): Promise => post(project(id, "save-as"), { to, 종류, 이름 }); export const fetchSources = (id: string): Promise => call(`/projects/${enc(id)}/sources`); /** 설계값을 채운 표 문서 — 서버가 그때그때 채움 · 저장 안 함 */ export const readFilled = (id: string, name: string): Promise => call(`/projects/${enc(id)}/tables/${enc(name)}/filled`);