- 일위대가 조합: 담은 로직 차례 · 메모까지 같아야 있는 키(원래 키 먼저) — 차례 · 메모만 바꿔도 새 조합 - 자재 조합: 담은 자재 차례까지 비교 - M02 Api_Fetch: detail 문장 · errors 목록 · 입력 검사 목록을 알림 글로(HTTP 422 대신) - 새 조합 자동 이름: 원래 이름 끝 「 (n)」 뗌(겹친 (2) (2) 막음) - test_m02_combo_branch.py 새 규칙으로 고침 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Kyw9qnkJ94pC1A4PfpDtA
258 lines
10 KiB
TypeScript
258 lines
10 KiB
TypeScript
/* =============================================================================
|
|
* 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<T>(path: string, init: RequestInit = {}): Promise<T> {
|
|
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<TemplateInfo[]> => {
|
|
const got = await call<TemplateInfo[] | { 양식: TemplateInfo[] }>(
|
|
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<TemplateDoc> => call(item(layer, kind, name, projectId));
|
|
|
|
export const saveTemplate = (
|
|
layer: Layer,
|
|
kind: Kind,
|
|
name: string,
|
|
projectId: string | null,
|
|
판: string,
|
|
문서: unknown,
|
|
): Promise<TemplateInfo> =>
|
|
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<string, unknown>;
|
|
}
|
|
|
|
/** 시스템 층 구조물 도면 목록 */
|
|
export const listStructures = async (): Promise<TemplateInfo[]> =>
|
|
(await listTemplates("system", null)).filter((t) => t.종류 === "structure");
|
|
|
|
/** 새로 — 도번 자동 · 빈 도면 (+ 없으면 빈 면적산출 · 재료산출 통합문서) · 이미 있으면 StaleError */
|
|
export const createStructure = (column: string): Promise<TemplateDoc> =>
|
|
call(`/structures/${enc(column)}`, { method: "POST" });
|
|
|
|
/** 한 열의 번호 — 도번 `SD` · 면적번 `SA` · 재료번 `SM` + 같은 8자리
|
|
* [종류][분류1][분류2][분류3](없는 쪽은 빈 글) */
|
|
export interface StructureNumbers {
|
|
도번: string;
|
|
면적번: string;
|
|
재료번: string;
|
|
}
|
|
|
|
/** `{열 id: {도번, 면적번, 재료번}}` — 구조물 열만(글 열 · 없는 열은 키 없음 = 「없음」) */
|
|
export const fetchStructureNumbers = (): Promise<Record<string, StructureNumbers>> =>
|
|
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<StructureCombos> => 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<BranchedCombo> =>
|
|
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<MaterialCombo> =>
|
|
call(`/material-combos/${enc(key)}`);
|
|
|
|
/** 이 열의 자재 조합을 고친 담은 자재로 갈라 냄 — 규칙은 `branchCombo` 와 같음 ·
|
|
* `본뜬키` 빈 글 = 이 열의 첫 자재 조합 */
|
|
export const branchMaterialCombo = (
|
|
column: string,
|
|
body: { 본뜬키?: string; 담은자재: MaterialItem[]; 이름: string; 판?: string },
|
|
): Promise<BranchedCombo> =>
|
|
call(`/structures/${enc(column)}/material-combo`, {
|
|
method: "POST",
|
|
body: JSON.stringify(body),
|
|
});
|
|
|
|
/** 다른 열(`from`) 구조물 도면 내용을 받는 열(`column`)로 가져옴 — 도번 · 열은 받는 쪽 그대로 */
|
|
export const copyStructure = async (column: string, from: string): Promise<void> => {
|
|
await call(`/structures/${enc(column)}/copy?from=${enc(from)}`, { method: "POST" });
|
|
};
|
|
|
|
/** 다른 열(`from`) 면적산출 내용(알약 연결 포함)을 받는 열(`column`)로 가져옴 — 면적번 · 열은 그대로 */
|
|
export const copyArea = async (column: string, from: string): Promise<void> => {
|
|
await call(`/area/${enc(column)}/copy?from=${enc(from)}`, { method: "POST" });
|
|
};
|
|
|
|
/** 다른 열(`from`) 재료산출 내용(알약 연결 포함)을 받는 열(`column`)로 가져옴 — 재료번 · 열은 그대로 */
|
|
export const copyMaterial = async (column: string, from: string): Promise<void> => {
|
|
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<unknown> =>
|
|
call(path, { method: "POST", body: JSON.stringify(body) });
|
|
|
|
export const resetProject = (id: string): Promise<unknown> => post(project(id, "reset"));
|
|
|
|
export const applyToProject = (
|
|
id: string,
|
|
from: { from: Layer; user_id?: string; project_id?: string; 종류?: Kind; 이름?: string },
|
|
): Promise<unknown> => post(project(id, "apply"), from);
|
|
|
|
export const saveProjectAs = (
|
|
id: string,
|
|
to: "personal" | "company",
|
|
종류: Kind,
|
|
이름: string,
|
|
): Promise<unknown> => post(project(id, "save-as"), { to, 종류, 이름 });
|
|
|
|
export const fetchSources = (id: string): Promise<unknown> => call(`/projects/${enc(id)}/sources`);
|
|
|
|
/** 설계값을 채운 표 문서 — 서버가 그때그때 채움 · 저장 안 함 */
|
|
export const readFilled = (id: string, name: string): Promise<TemplateDoc> =>
|
|
call(`/projects/${enc(id)}/tables/${enc(name)}/filled`);
|