/* ============================================================================= * 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}`; } /** 지금 화면의 층 — 시스템 = 옛 길 그대로 · 회사 · 개인 = `/layers/{층}` 아래(구조물집계표 · 조합 길 모두) */ let activeLayer: Layer = "system"; export const setActiveLayer = (layer: Layer): void => { activeLayer = layer; }; export const getActiveLayer = (): Layer => activeLayer; const scoped = (path: string, init: RequestInit = {}): Promise => call(`${activeLayer === "system" ? "" : `/layers/${activeLayer}`}${path}`, init); 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 => scoped(`/structures/${enc(column)}`, { method: "POST" }); /** 한 열의 번호 — 도번 `SD` · 면적번 `SA` · 재료번 `SM` + 같은 8자리 * [종류][분류1][분류2][분류3](없는 쪽은 빈 글) */ export interface StructureNumbers { 도번: string; 면적번: string; 재료번: string; } /** `{열 id: {도번, 면적번, 재료번}}` — 구조물 열만(글 열 · 없는 열은 키 없음 = 「없음」) */ export const fetchStructureNumbers = (): Promise> => scoped("/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 => scoped("/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 => scoped(`/structures/${enc(column)}/combo`, { method: "POST", body: JSON.stringify(body) }); /** 있는 조합을 이 열에 씀 — 집계표 그 열 일위대가 · 자재조합 키만 바꿈(조합은 그대로 · `새로만듦` false) */ export const setColumnCombo = ( column: string, body: { 종류: "일위대가" | "자재조합"; 키: string; 판?: string }, ): Promise => scoped(`/structures/${enc(column)}/use-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 => scoped(`/material-combos/${enc(key)}`); /** 이 열의 자재 조합을 고친 담은 자재로 갈라 냄 — 규칙은 `branchCombo` 와 같음 · * `본뜬키` 빈 글 = 이 열의 첫 자재 조합 */ export const branchMaterialCombo = ( column: string, body: { 본뜬키?: string; 담은자재: MaterialItem[]; 이름: string; 판?: string }, ): Promise => scoped(`/structures/${enc(column)}/material-combo`, { method: "POST", body: JSON.stringify(body), }); /* --- 자재 조합 표(마스터 템플릿 `자재조합.json`) — 좌측 「자재 조합」 컨테이너 --- */ /** 표 통째 — 판(파일 판본) · 줄(담은 자재 포함) · 쓰는곳(키 → 쓰는 구조물 열 이름들) */ export const listMaterialCombos = (): Promise<{ 판: string; 줄: MaterialCombo[]; 쓰는곳: Record; }> => scoped("/material-combos"); /** 새로 · 본떠 만들기(`본뜬키`) — 키는 서버가 표 「다음」 번호로 줌 */ export const createMaterialCombo = ( 판: string, 줄: Omit, ): Promise<{ 키: string; 판: string; 줄: MaterialCombo }> => scoped("/material-combos", { method: "POST", body: JSON.stringify({ 판, 줄 }) }); /** 고치기 — 이름 · 비고 · 담은자재(본뜬키 · 소유는 서버가 있던 것으로) */ export const editMaterialCombo = ( 키: string, 판: string, 줄: MaterialCombo, ): Promise<{ 키: string; 판: string; 줄: MaterialCombo }> => scoped(`/material-combos/${enc(키)}`, { method: "PUT", body: JSON.stringify({ 판, 줄 }) }); /** 지우기 — 구조물 열이 쓰면 서버가 막고 그 열 이름들을 까닭으로 줌 */ export const deleteMaterialCombo = (키: string, 판: string): Promise => scoped(`/material-combos/${enc(키)}?판=${enc(판)}`, { method: "DELETE" }); /* --- 구조물 옵션 보이기(마스터 템플릿 `구조물옵션표시.json`) — 좌측 「구조물 옵션 보이기」 줄 --- */ export interface OptionKind { type_id: string; group: string; name: string; /** 거짓이면 B05 선택지에 안 나오는 종류 */ enabled: boolean; options: { key: string; label: string; phase: "b05" | "detail"; required: boolean }[]; } /** 판 · 표시(숨긴 것만 `{종류: {옵션: false}}`) · 종류(옵션이 있는 것과 그 옵션) */ export interface OptionsDoc { 판: string; 표시: Record>; 종류: OptionKind[]; } export const fetchStructureOptions = (): Promise => call("/structure-options"); /** 고쳐 씀 — 숨긴 것만 보냄 · 낡은 판이면 StaleError · 돌려받는 것은 새 문서 */ export const saveStructureOptions = (판: string, 표시: OptionsDoc["표시"]): Promise => call("/structure-options", { method: "PUT", body: JSON.stringify({ 판, 표시 }) }); /** 다른 열(`from`) 구조물 도면 내용을 받는 열(`column`)로 가져옴 — 도번 · 열은 받는 쪽 그대로 */ export const copyStructure = async (column: string, from: string): Promise => { await scoped(`/structures/${enc(column)}/copy?from=${enc(from)}`, { method: "POST" }); }; /** 다른 열(`from`) 면적산출 내용(알약 연결 포함)을 받는 열(`column`)로 가져옴 — 면적번 · 열은 그대로 */ export const copyArea = async (column: string, from: string): Promise => { await scoped(`/area/${enc(column)}/copy?from=${enc(from)}`, { method: "POST" }); }; /** 다른 열(`from`) 재료산출 내용(알약 연결 포함)을 받는 열(`column`)로 가져옴 — 재료번 · 열은 그대로 */ export const copyMaterial = async (column: string, from: string): Promise => { await scoped(`/material/${enc(column)}/copy?from=${enc(from)}`, { method: "POST" }); }; /** 회사 · 개인 층의 집계표 항목(열 하나 + 도면 · 면적 · 재료 파일 + 자재조합 줄)을 내 개인 층으로 복사 — * 새 SS 키로 만들고 `본뜬` 표시 · `fromOwner` = 회사 층 회사 · 개인 층 「회사/사용자」 */ export const copyItemToPersonal = (body: { fromLayer: Layer; fromOwner: string | null; key: string; }): Promise<{ key: string }> => call("/layers/personal/items/copy", { method: "POST", body: JSON.stringify({ from_layer: body.fromLayer, from_owner: body.fromOwner, key: body.key }), }); /* --- 프로젝트 층 단추 다섯 --- */ 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`);