Merge remote-tracking branch 'origin/sub_laptop_2' into sub_laptop_1

충돌 셋(UI_Side · UI_Style · locale_m2)은 양쪽을 모두 살려 풂 — 옵션 보이기 · 항목 고르기 문구 · 층 화면 문구를 한 벌로, 도면 양식 줄은 시스템 층에서만.
This commit is contained in:
2026-09-29 12:28:14 +09:00
14 changed files with 391 additions and 48 deletions
@@ -53,6 +53,15 @@ function reason(detail: unknown, status: number): string {
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 = <T>(path: string, init: RequestInit = {}): Promise<T> =>
call<T>(`${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 =>
@@ -122,7 +131,7 @@ export const listStructures = async (): Promise<TemplateInfo[]> =>
/** 새로 — 도번 자동 · 빈 도면 (+ 없으면 빈 면적산출 · 재료산출 통합문서) · 이미 있으면 StaleError */
export const createStructure = (column: string): Promise<TemplateDoc> =>
call(`/structures/${enc(column)}`, { method: "POST" });
scoped(`/structures/${enc(column)}`, { method: "POST" });
/** 한 열의 번호 — 도번 `SD` · 면적번 `SA` · 재료번 `SM` + 같은 8자리
* [종류][분류1][분류2][분류3](없는 쪽은 빈 글) */
@@ -134,7 +143,7 @@ export interface StructureNumbers {
/** `{열 id: {도번, 면적번, 재료번}}` — 구조물 열만(글 열 · 없는 열은 키 없음 = 「없음」) */
export const fetchStructureNumbers = (): Promise<Record<string, StructureNumbers>> =>
call("/structures/numbers");
scoped("/structures/numbers");
/** 구조물집계표조합 한 줄 — 구조물 열 하나(키 `SS` + 6자리 · 한 번 준 키는 안 바뀜) */
export interface StructureCombo extends StructureNumbers {
@@ -161,7 +170,7 @@ export interface StructureCombos {
}
/** 조합 표 — 서버가 집계표 [저장] 때 맞춤(읽기만) */
export const fetchStructureCombos = (): Promise<StructureCombos> => call("/structure-combos");
export const fetchStructureCombos = (): Promise<StructureCombos> => scoped("/structure-combos");
/** 조합 갈라 내기 결과 — `새로만듦` false = 같은 담은 로직의 조합이 이미 있어 그 키를 가리킴 */
export interface BranchedCombo {
@@ -182,14 +191,14 @@ export const branchCombo = (
판?: string;
},
): Promise<BranchedCombo> =>
call(`/structures/${enc(column)}/combo`, { method: "POST", body: JSON.stringify(body) });
scoped(`/structures/${enc(column)}/combo`, { method: "POST", body: JSON.stringify(body) });
/** 있는 조합을 이 열에 씀 — 집계표 그 열 일위대가 · 자재조합 키만 바꿈(조합은 그대로 · `새로만듦` false) */
export const setColumnCombo = (
column: string,
body: { 종류: "일위대가" | "자재조합"; 키: string; 판?: string },
): Promise<BranchedCombo> =>
call(`/structures/${enc(column)}/use-combo`, { method: "POST", body: JSON.stringify(body) });
scoped(`/structures/${enc(column)}/use-combo`, { method: "POST", body: JSON.stringify(body) });
/** 자재 조합(키 `UM`) 담은 자재 한 줄 */
export interface MaterialItem {
@@ -210,7 +219,7 @@ export interface MaterialCombo {
}
export const fetchMaterialCombo = (key: string): Promise<MaterialCombo> =>
call(`/material-combos/${enc(key)}`);
scoped(`/material-combos/${enc(key)}`);
/** 이 열의 자재 조합을 고친 담은 자재로 갈라 냄 — 규칙은 `branchCombo` 와 같음 ·
* `본뜬키` 빈 글 = 이 열의 첫 자재 조합 */
@@ -218,7 +227,7 @@ export const branchMaterialCombo = (
column: string,
body: { 본뜬키?: string; 담은자재: MaterialItem[]; 이름: string; 판?: string },
): Promise<BranchedCombo> =>
call(`/structures/${enc(column)}/material-combo`, {
scoped(`/structures/${enc(column)}/material-combo`, {
method: "POST",
body: JSON.stringify(body),
});
@@ -229,14 +238,14 @@ export const listMaterialCombos = (): Promise<{
판: string;
줄: MaterialCombo[];
쓰는곳: Record<string, string[]>;
}> => call("/material-combos");
}> => scoped("/material-combos");
/** 새로 · 본떠 만들기(`본뜬키`) — 키는 서버가 표 「다음」 번호로 줌 */
export const createMaterialCombo = (
판: string,
줄: Omit<MaterialCombo, "키">,
): Promise<{ 키: string; 판: string; 줄: MaterialCombo }> =>
call("/material-combos", { method: "POST", body: JSON.stringify({ 판, 줄 }) });
scoped("/material-combos", { method: "POST", body: JSON.stringify({ 판, 줄 }) });
/** 고치기 — 이름 · 비고 · 담은자재(본뜬키 · 소유는 서버가 있던 것으로) */
export const editMaterialCombo = (
@@ -244,11 +253,11 @@ export const editMaterialCombo = (
판: string,
줄: MaterialCombo,
): Promise<{ 키: string; 판: string; 줄: MaterialCombo }> =>
call(`/material-combos/${enc(키)}`, { method: "PUT", body: JSON.stringify({ 판, 줄 }) });
scoped(`/material-combos/${enc(키)}`, { method: "PUT", body: JSON.stringify({ 판, 줄 }) });
/** 지우기 — 구조물 열이 쓰면 서버가 막고 그 열 이름들을 까닭으로 줌 */
export const deleteMaterialCombo = (키: string, 판: string): Promise<unknown> =>
call(`/material-combos/${enc(키)}?판=${enc(판)}`, { method: "DELETE" });
scoped(`/material-combos/${enc(키)}?판=${enc(판)}`, { method: "DELETE" });
/* --- 구조물 옵션 보이기(마스터 템플릿 `구조물옵션표시.json`) — 좌측 「구조물 옵션 보이기」 줄 --- */
export interface OptionKind {
@@ -275,19 +284,31 @@ export const saveStructureOptions = (판: string, 표시: OptionsDoc["표시"]):
/** 다른 열(`from`) 구조물 도면 내용을 받는 열(`column`)로 가져옴 — 도번 · 열은 받는 쪽 그대로 */
export const copyStructure = async (column: string, from: string): Promise<void> => {
await call(`/structures/${enc(column)}/copy?from=${enc(from)}`, { method: "POST" });
await scoped(`/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" });
await scoped(`/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" });
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<unknown> =>