Files
Aislo/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts
T
eomsangdonandClaude Sonnet 5.5 e1b17a523c feat(732-1): M02 실무문서 칸 · 복사 탭 · 서버 길 걷음
- 왼쪽 「실무문서」 칸(Practice.ts) · 일위대가 가져오기 「실무문서」 탭 · 복사 원본 고르기 · 낱 CSS · locale 열쇠 걷음
- 서버 `from_layer=practice`(_copy_practice) · `DELETE /api/m02/practice`(drop_practice) · 조합 목록 · 키 하나 읽기 길 걷음 · 조합 길 시스템 관리자 문 걷음(담을 로직 읽기는 그대로)
- 옛 마스터 조합 파일 `일위대가조합.json`(UA 37) = 쓰는 곳 있음(구조물집계표 일위대가 칸 · labor 본뜬키 · 프로젝트 초기값 사본 · 불러오기 맞춤 씨앗) → 파일 · 읽기 길 유지 · 지우지 않음
- 낡은 시험: test_m02_practice 지움(화면 · 길 없음) · 조합 길 읽던 시험(combo · combo_inputs · combo_layers)은 저장소 함수로 · 실무문서 복사 시험(labor_copy · pair_copy) 줄 지움 · 탭 · 차례 단정(layer_screen · structure_options · structure_panel · 720 · precombo_scope · ui_loading) 고침

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-02 13:17:24 +09:00

289 lines
11 KiB
TypeScript

/* =============================================================================
* M02_MasterTemplete_Labor_Api.ts
* 일위대가 서버 길 — 담을 로직 읽기 = `/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, LaborFixed, LaborRef, LaborSpec } from "./M02_MasterTemplete_Labor_Calc";
import type { LinePatch } from "./logic/M02_Sangeun_Cover_Model";
import {
call,
getActiveLayer,
getActiveOwner,
type ItemLogic,
type Layer,
} from "./M02_MasterTemplete_Api_Fetch";
async function request<T>(url: string, method = "GET", body?: unknown): Promise<T> {
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;
/* --- 담을 로직(읽기만 · 로그인) — M01 로직 길과 같은 모양 --- */
export const fetchLibLogics = (): Promise<LogicSummary[]> =>
request<{ logics: LogicSummary[] }>("/logic-lib/logics").then((d) => d.logics);
export const fetchLibLogicSubs = (): Promise<SubBrief[]> =>
request<{ subs: SubBrief[] }>("/logic-lib/subs").then((d) => d.subs);
/* --- 기준 목록 · 계산서 보기(704 · 서버 M01 엔진이 셈 · `M02_Logic_Standard`) --- */
/** 기준 목록 한 줄 — 로직 하나를 대표 규격 하나로 펼친 줄 · `바꿈` = 바탕에서 바꾼 입력 */
export interface StandardRow {
키: string;
이름: string;
규격: string;
단위: string;
금액: number | null;
출처: string;
공종: string;
별칭: string;
구분: string;
상세구분: string;
막힘: boolean;
바꿈: Record<string, string | number>;
}
export interface StandardList {
판: string;
공종: string[];
줄: StandardRow[];
}
export interface StandardSheet {
이름: string;
규격: string;
단위: string;
출처: string;
판: string;
묶음: {
비목: string;
소계: number | null;
끝수: string;
줄: { 이름: string; 글: string; 금액: number | null }[];
}[];
계: number | null;
}
let standard: Promise<StandardList> | null = null; // 페이지에 한 벌(서버 저장본 · 판 바뀌면 새로 고침)
export const fetchStandard = (): Promise<StandardList> =>
(standard ??= request<StandardList>("/logic-lib/standard").catch((error) => {
standard = null;
throw error;
}));
export const fetchStandardSheet = (row: StandardRow): Promise<StandardSheet> =>
request("/logic-lib/sheet", "POST", { key: row.키, 바꿈: row.바꿈 });
/* --- 일위대가 문서(39 · 구조물 열마다 `labor/<열>.json` · 틀 = sub7 39-1a) --- */
/** 손 입력 수량 — `null` 이면 면적산출 알약 연결(칸)을 따름 · 손이 칸보다 앞섬 */
export type LaborQty = { 손: number } | null;
/** 담은 로직 한 줄 — 입력값 = 그 로직 입력 이름만(빈 값 = 자동값) ·
* 참조 줄(단가승격 · 사전조합 · 50-3) = `참조` 있고 `로직` 빈 글 · 줄 가름은 `lineId` */
export interface LaborRow {
로직: string;
참조?: LaborRef;
/** 사용자가 지은 이름(44-6 · 빈 것 = null) */
별칭?: string | null;
메모: string | null;
입력값: Record<string, string | number>;
수량: LaborQty;
/** 켜쌓기 적용율 %(85-2 · 대상 로직만 · 없음 = 100) */
켜쌓기율?: number | null;
/** 고른 로직 자재(103-1) — {호표 차례: MT키} · 없으면 엔진 기본 */
고름?: Record<string, string>;
/** 단가승격(134-1 · 로직 줄 구분 드롭다운) — 없음 = 단가산출 · 금액 무관 · B09 호표 몫 */
승격?: boolean;
/** 로직 자재 세 칸(148-4) — {호표 차례: 세 칸} · 금액 무관 */
자재표시?: Record<string, MatMarks>;
/** 고정 줄(705-1 · 705-2 붙여넣기) — 숫자 그대로 · 로직 · 참조 없음 */
고정?: LaborFixed;
/** 그 줄 덮개(715-3 · 717-3 단가산출 모달) — 로직 줄 · SK 참조 줄 · 서버가 검사 */
패치?: LinePatch;
}
/** 자재 세 칸(148-4 · 서버 `M02_Labor_Doc.MARKS` 와 같음) — 빈 칸 = 읽을 때 기본값 · 금액 무관 */
export const MAT_MARKS = {
자재구분: ["관급", "사급"],
별도계상: ["별도계상", "직접계산"],
운반비: ["포함", "미포함"],
} as const;
export type MatMarkKey = keyof typeof MAT_MARKS;
export type MatMarks = { [K in MatMarkKey]?: (typeof MAT_MARKS)[K][number] };
/** 「전국」 자재(361-1 · `본뜬.전국자재` · 키 · 단가 없음 · 보기만 · `Labor_National`) */
export type NationalMat = MatMarks & {
이름: string;
규격: string;
단위: string;
수량: { 손: number };
};
/** 읽을 때 기본값(162-1 · 서버 `MARK_DEFAULT` 와 같음) — 저장은 비움 */
export const MAT_MARK_DEFAULT: Required<MatMarks> = {
자재구분: "사급",
별도계상: "별도계상",
운반비: "포함",
};
/** 허용 값만 남김(서버 `marks` 와 같은 규칙) */
export function cleanMarks(v: unknown): MatMarks {
const got = (v && typeof v === "object" ? v : {}) as Record<string, unknown>;
const out: Record<string, unknown> = {};
for (const [k, allowed] of Object.entries(MAT_MARKS))
if ((allowed as readonly unknown[]).includes(got[k])) out[k] = got[k];
return out as MatMarks;
}
/** 로직 줄 `자재표시` 정리 — 빈 차례는 뺌 · 다 비면 칸 없음 */
function cleanShown(v: unknown): { 자재표시?: Record<string, MatMarks> } {
const out: Record<string, MatMarks> = {};
for (const [at, one] of Object.entries(v && typeof v === "object" ? v : {})) {
const kept = cleanMarks(one);
if (Object.keys(kept).length) out[at] = kept;
}
return Object.keys(out).length ? { 자재표시: out } : {};
}
export interface LaborMat extends MatMarks {
코드: string;
이름: string | null;
규격: string | null;
단위: string | null;
수량: LaborQty;
}
export interface LaborDoc {
종류: "일위대가";
판?: number;
열: string;
일위대가번?: string;
단위: string | null;
본뜬?: { 층: string; 주인: string | null; 키: string; 전국자재?: NationalMat[] } | null;
담은로직: LaborRow[];
자재: LaborMat[];
계산값?: unknown;
/** 규격 열(704-4 · `Labor_Spec`) — 없으면 옛 문서(열 하나) */
규격?: LaborSpec | null;
}
/** 받은 문서를 화면 모양으로 — 빈 칸 · 옛 모양도 받음(빈 일위대가) */
export function normalizeLabor(doc: unknown, column: string): LaborDoc {
const d = (doc && typeof doc === "object" ? doc : {}) as Partial<LaborDoc>;
return {
...d,
종류: "일위대가",
열: d.열 ?? column,
단위: d.단위 ?? null,
담은로직: (d.담은로직 ?? []).map((r) => ({
로직: r.로직 ?? "",
...(r.참조 ? { 참조: r.참조 } : {}),
별칭: r.별칭 ?? null,
메모: r.메모 ?? null,
입력값: r.입력값 ?? {},
수량: r.수량 ?? null,
...(r.참조 ? { 참조: r.참조 } : {}),
...(r.켜쌓기율 != null ? { 켜쌓기율: r.켜쌓기율 } : {}),
...(r.고름 && Object.keys(r.고름).length ? { 고름: { ...r.고름 } } : {}),
...(r.승격 === true && !r.참조 ? { 승격: true } : {}),
...(r.참조 || r.고정 ? {} : cleanShown(r.자재표시)),
...(r.고정 ? { 고정: r.고정 } : {}), // 705-1 — 읽을 때 떨구면 [저장]에서 줄이 사라짐
...(r.패치 ? { 패치: r.패치 } : {}), // 717-3 — 떨구면 [저장]에서 줄 덮개가 사라짐
})),
자재: (d.자재 ?? []).map((m) => ({
코드: m.코드,
이름: m.이름 ?? null,
규격: m.규격 ?? null,
단위: m.단위 ?? null,
수량: m.수량 ?? null,
...cleanMarks(m),
})),
};
}
/** 면적산출 문서의 알약 → {칸, 값}(식 칸 = 서버 `계산값` · 그 밖 = 칸 값 · 수만) — 서버 `area_values` 와 같은 규칙 */
export function areaLinks(area: unknown): Map<string, { 칸: string; 값: number | null }> {
const out = new Map<string, { 칸: string; 값: number | null }>();
const doc = (area && typeof area === "object" ? area : {}) as {
시트?: {
id?: string;
칸?: Record<string, { 값?: unknown }>;
연결?: { 키?: string; 칸?: string }[];
}[];
계산값?: Record<string, Record<string, { 수?: unknown }>>;
};
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/{열}…`) · `who` 를 넘기면 그 주인(134-3) */
const laborAt = (layer: Layer, column: string, tail: string, who?: string | null): string => {
const owner = who !== undefined ? who : 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<string, number>,
owner?: string | null,
): Promise<LaborCalc & { 알약: unknown[] }> =>
request(laborAt(layer, column, "/calc", owner), "POST", {
...(doc ? { doc } : {}),
...(수량 ? { 수량 } : {}),
});
/** 서고 한 줄(50-3) — 키 = SP-· SC-###### · 제목 · 단위 = 문서 것 */
export interface LibraryRow {
이름: string;
제목?: string | null;
단위?: string | null;
/** 사전조합 = 사용자가 고름(51-12) */
구분?: string | null;
상세구분?: string | null;
}
/** 서고 목록 — 그 층(주인) 사전조합 `precombo` 문서 */
export const fetchLibrary = async (
ref: Omit<LaborRef, "키">,
kind: "precombo" | "sangeun",
): Promise<LibraryRow[]> => {
const q = ref.주인 && ref.층 !== "system" ? `?owner=${enc(ref.주인)}` : "";
const got = await call<{ 양식: (LibraryRow & { 종류: string })[] }>(
`/layers/${ref.층}/templates${q}`,
);
return got.양식.filter((r) => r.종류 === kind);
};
/** 담은 로직 입력 목록 + 자동값(입력 모달 · `M02_Labor_Inputs`) */
export const fetchLogicInputs = (
keys: string[],
): Promise<{ 로직: ItemLogic[]; 빠진로직: string[] }> =>
keys.length
? request(`/logic-lib/inputs?keys=${enc(keys.join(","))}`)
: Promise.resolve({ 로직: [], 빠진로직: [] });