feat(M02): 50-3③ 서고 새로 · 복사를 서버 키 길로 — POST /layers/{층}/library/{종류} · 참조 줄 보존

- 새로 = 서버가 그 층 다음 SP · SC 키 · 복사 = 본뜬 원본 → 고른 이름으로 고쳐 씀
- 일위대가 문서 줄의 참조 {층, 주인, 키} 를 화면 캐시에서 안 버림 · 로직 입력 받기에서 참조 줄 건너뜀

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt
This commit is contained in:
2026-10-05 18:45:01 +09:00
co-authored by Claude Opus 5.5
parent b1a5f38a16
commit 027ad3da98
4 changed files with 45 additions and 29 deletions
@@ -126,6 +126,19 @@ export const deleteLayerTemplate = (
method: "DELETE",
});
/** 서고 새로 · 복사(50-3) — 새로 = `문서` · 복사 = `본뜬` {층, 주인, 키} · 키는 서버가 줌(답 `이름`) */
export type LibraryBody =
{ 문서: Record<string, unknown> } | { 본뜬: { 층: Layer; 주인: string | null; 키: string } };
export const newLibrary = (
layer: Layer,
kind: "promote" | "precombo",
body: LibraryBody,
): Promise<TemplateInfo> =>
call(owned(layer, `/layers/${layer}/library/${kind}`), {
method: "POST",
body: JSON.stringify(body),
});
/* --- 구조물 도면(종류 structure · 이름 = 구조물집계표 열 id) ---
* 읽기 · 저장 · 지우기는 위 readTemplate · saveTemplate · deleteTemplate 에 "structure" */
@@ -126,6 +126,8 @@ export interface LaborRow {
메모: string | null;
입력값: Record<string, string | number>;
수량: LaborQty;
/** 서고 참조 줄(50-3 · 사전조합 · 로직 없음) — 단가승격 · 사전조합 {층, 주인, 키} */
참조?: { 층: string; 주인: string | null; 키: string };
}
export interface LaborMat {
코드: string;
@@ -159,6 +161,7 @@ export function normalizeLabor(doc: unknown, column: string): LaborDoc {
메모: r.메모 ?? null,
입력값: r.입력값 ?? {},
수량: r.수량 ?? null,
...(r.참조 ? { 참조: r.참조 } : {}),
})),
자재: (d.자재 ?? []).map((m) => ({
코드: m.코드,
@@ -283,7 +283,7 @@ export function mountLaborDetail(
/** 아직 안 받은 로직 입력 목록만 받음 */
const loadLogics = async (): Promise<void> => {
const want = cur.담은로직.map((r) => r.로직).filter((k) => !logics.has(k));
const want = cur.담은로직.map((r) => r.로직).filter((k) => k && !logics.has(k)); // 참조 줄 = 로직 없음
if (!want.length) return;
try {
const got = await fetchLogicInputs(want);
@@ -20,10 +20,12 @@ import {
deleteLayerTemplate,
deleteTemplate,
getActiveOwner,
newLibrary,
readTemplate,
saveTemplate,
type Kind,
type Layer,
type LibraryBody,
type TemplateInfo,
} from "./M02_MasterTemplete_Api_Fetch";
import { openLogicPicker } from "./M02_MasterTemplete_Labor_Pick";
@@ -31,10 +33,10 @@ import type { Selection } from "./M02_MasterTemplete_UI_Main";
export type PrecomboKind = Extract<Kind, "promote" | "precombo">;
/** 종류 · 키 머리 · 화면 이름(= 문서 `종류` 글) */
const PARTS: { kind: PrecomboKind; prefix: string; label: string }[] = [
{ kind: "promote", prefix: "SP", label: "단가승격" },
{ kind: "precombo", prefix: "SC", label: "사전조합" },
/** 종류 · 화면 이름(= 문서 `종류` 글 · 키 SP- · SC- 는 서버가 줌) */
const PARTS: { kind: PrecomboKind; label: string }[] = [
{ kind: "promote", label: "단가승격" },
{ kind: "precombo", label: "사전조합" },
];
export const PRECOMBO_TITLE = "일위대가 조합";
export const isPrecombo = (kind: Kind): kind is PrecomboKind =>
@@ -45,15 +47,6 @@ type Info = TemplateInfo & { 제목?: string };
export const precomboLabel = (info: Info): string =>
info.제목 ? `${info.이름} ${info.제목}` : info.이름;
/** 그 층 다음 키 — 지금 목록의 가장 큰 수 + 1(서버가 같은 키면 409 로 막음) */
const nextKey = (prefix: string, items: Info[]): string => {
const top = items.reduce((max, i) => {
const m = new RegExp(`^${prefix}-(\\d+)$`).exec(i.이름);
return m ? Math.max(max, Number(m[1])) : max;
}, 0);
return `${prefix}-${String(top + 1).padStart(6, "0")}`;
};
export interface PrecomboOptions {
layer: Layer;
canEdit: boolean;
@@ -95,10 +88,9 @@ export function buildPrecomboSection(opt: PrecomboOptions): PrecomboHandle {
});
};
/** 새로 · 복사 — 다음 키로 저장 후 엶 */
const create = async (kind: PrecomboKind, doc: Record<string, unknown>): Promise<void> => {
const key = nextKey(PARTS.find((p) => p.kind === kind)!.prefix, mine(kind));
await saveTemplate(opt.layer, kind, key, null, "", doc);
/** 새로 — 서버가 그 층 다음 키로 씀 → 엶 */
const create = async (kind: PrecomboKind, body: LibraryBody): Promise<void> => {
const key = (await newLibrary(opt.layer, kind, body)).이름;
showToast(L("M02_Created"), "success");
await opt.refresh();
const info = mine(kind).find((i) => i.이름 === key);
@@ -142,10 +134,11 @@ export function buildPrecomboSection(opt: PrecomboOptions): PrecomboHandle {
!go
? undefined
: create("promote", {
종류: "단가승격",
이름: x.이름,
단위: x.결과단위 || null,
담은로직: [{ 로직: x.키, 입력값: {} }],
문서: {
이름: x.이름,
단위: x.결과단위 || null,
담은로직: [{ 로직: x.키, 입력값: {} }],
},
}).catch(failed),
);
});
@@ -153,20 +146,27 @@ export function buildPrecomboSection(opt: PrecomboOptions): PrecomboHandle {
const newPrecombo = (): void =>
nameDialog(L("M02_NewTitle"), "", async (name) => {
if (!(await opt.confirmLeave())) return;
await create("precombo", { 종류: "사전조합", 이름: name, 단위: null, 담은로직: [] });
await create("precombo", { 문서: { 이름: name, 단위: null, 담은로직: [] } });
});
/** 복사 = 같은 층 새 키 · 이름 「(2)」 · 본뜬 = 원래 키 */
/** 복사 = 같은 층 새 키(서버 · 본뜬 = 원래 키) → 고른 이름으로 고쳐 씀 */
const copy = (kind: PrecomboKind): void => {
const from = current(kind);
if (!from) return;
nameDialog(L("M02_CopyTitle"), `${from.제목 ?? from.이름} (2)`, async (name) => {
if (!(await opt.confirmLeave())) return;
const got = await readTemplate(opt.layer, kind, from.이름, null);
const doc: Record<string, unknown> = { ...(got.문서 as object), 이름: name };
doc.본뜬 = { 층: opt.layer, 주인: getActiveOwner(), 키: from.이름 };
delete doc.판;
await create(kind, doc);
const made = await newLibrary(opt.layer, kind, {
본뜬: { 층: opt.layer, 주인: getActiveOwner(), 키: from.이름 },
});
const got = await readTemplate(opt.layer, kind, made.이름, null);
await saveTemplate(opt.layer, kind, made.이름, null, made.판, {
...(got.문서 as object),
이름: name,
});
showToast(L("M02_Created"), "success");
await opt.refresh();
const info = mine(kind).find((i) => i.이름 === made.이름);
if (info) await select(info);
});
};