feat(M02): 일위대가 카드 · 다른 곳에서 가져오기 — 카드 차례 도면 → 일위대가 → 면적산출 · 일위대가 몸 = 보기 갈래 · 빈 일위대가 두 단추 · 가져오기 = 같은 층 · 다른 층 · 사람 · 실무문서(일위대가)
39-5 H5 — copyPartFrom(from_layer · from_owner · practice · 39-1b 길) · 집계표 일위대가 줄 「조회 후 선택」 = 일위대가 복사해 가져오기 · CardKind(카드만 labor · 프로젝트 고르기 모달 EditKind 그대로). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt
This commit is contained in:
@@ -299,6 +299,24 @@ export const fetchStructureOptions = (): Promise<OptionsDoc> => call("/structure
|
||||
export const saveStructureOptions = (판: string, 표시: OptionsDoc["표시"]): Promise<OptionsDoc> =>
|
||||
call("/structure-options", { method: "PUT", body: JSON.stringify({ 판, 표시 }) });
|
||||
|
||||
/** 항목 하나 복사해 가져오기(39-5 · 39-1b) — 받는 열(`column`)에 덮어씀 · 번호 · 열은 그대로 ·
|
||||
* `source` = 원본 열 id(실무문서면 조합 키) · `from` 없음 = 지금 층 · 층 · 주인 = 다른 층(원본 읽기 권한) ·
|
||||
* `practice` = 실무문서 조합 → 일위대가(시스템 관리자만) */
|
||||
export const copyPartFrom = async (
|
||||
kind: "structure" | "labor" | "area" | "material",
|
||||
column: string,
|
||||
source: string,
|
||||
from: { layer: Layer | "practice"; owner: string | null } | null,
|
||||
): Promise<void> => {
|
||||
const q = new URLSearchParams({ from: source });
|
||||
if (from) q.set("from_layer", from.layer);
|
||||
if (from?.owner) q.set("from_owner", from.owner);
|
||||
const part = kind === "structure" ? "structures" : kind;
|
||||
await call(owned(activeLayer, `/layers/${activeLayer}/${part}/${enc(column)}/copy?${q}`), {
|
||||
method: "POST",
|
||||
});
|
||||
};
|
||||
|
||||
/** 다른 열(`from`) 구조물 도면 내용을 받는 열(`column`)로 가져옴 — 도번 · 열은 받는 쪽 그대로 */
|
||||
export const copyStructure = async (column: string, from: string): Promise<void> => {
|
||||
await scoped(`/structures/${enc(column)}/copy?from=${enc(from)}`, { method: "POST" });
|
||||
|
||||
@@ -35,18 +35,23 @@ import {
|
||||
type DrawingCardHandle,
|
||||
} from "./M02_MasterTemplete_DrawingCard";
|
||||
import type { ItemPart, ItemSource } from "./M02_MasterTemplete_ItemSource";
|
||||
import type { LaborViewHandle } from "./M02_MasterTemplete_Labor_View";
|
||||
import "./M02_MasterTemplete_Structure.css";
|
||||
|
||||
/** 집계표 열 하나에 딸린 문서 셋 — 번호 칸 · 이름 */
|
||||
export type EditKind = ItemPart;
|
||||
/** 카드 종류 — 열 문서 셋 + 일위대가(39장 · 보기 갈래 카드 · 고침은 일위대가 상세) */
|
||||
export type CardKind = EditKind | "labor";
|
||||
export type SheetKind = Exclude<EditKind, "structure">;
|
||||
export const PART_FIELD: Record<EditKind, keyof StructureNumbers> = {
|
||||
export const PART_FIELD: Record<CardKind, keyof StructureNumbers> = {
|
||||
structure: "도번",
|
||||
labor: "일위대가번",
|
||||
area: "면적번",
|
||||
material: "재료번",
|
||||
};
|
||||
export const PART_WORD: Record<EditKind, string> = {
|
||||
export const PART_WORD: Record<CardKind, string> = {
|
||||
structure: "도면",
|
||||
labor: "일위대가",
|
||||
area: "면적산출",
|
||||
material: "재료산출",
|
||||
};
|
||||
@@ -103,7 +108,7 @@ export interface ItemCardsOptions {
|
||||
/** [도면 수정] · [면적산출 수정] · [재료산출 수정] */
|
||||
onEdit: (kind: EditKind) => void;
|
||||
/** 문서에 번호가 없을 때 쓸 번호(집계표 번호 줄) */
|
||||
numberOf?: (kind: EditKind, column: string) => string | undefined;
|
||||
numberOf?: (kind: CardKind, column: string) => string | undefined;
|
||||
/** 못 읽었을 때 카드에 까닭을 보일까 — 기본 보임(M02 는 번호가 있는 열만 · 없는 열은 「없음」) */
|
||||
warnOnFail?: (column: string) => boolean;
|
||||
/** 세 카드 뒤에 덧붙는 카드(선택 입력 …) */
|
||||
@@ -113,7 +118,10 @@ export interface ItemCardsOptions {
|
||||
/** 면적산출 · 재료산출 카드를 미리보기에서 고침(31-3) — 없으면 보기만 */
|
||||
sheetEdit?: SheetEditOptions;
|
||||
/** 「복사해 가져오기」 · 빈 카드 [복사 생성](39-5) — 다른 항목의 이 칸을 골라 덮어씀 · 없으면 단추 없음 */
|
||||
onImport?: (kind: EditKind) => void;
|
||||
onImport?: (kind: CardKind) => void;
|
||||
/** 일위대가 카드(39-5 · 몸 = 일위대가 보기 갈래 `Labor_View`) — 도면 다음 · 지금 층에서 읽음 ·
|
||||
* [일위대가 수정] · 빈 카드 [새로 만들기] = 이것(일위대가 상세) */
|
||||
labor?: () => void;
|
||||
}
|
||||
|
||||
/** 카드 스프레드시트를 다루는 손잡이 — 알약 · 자재 조합 판이 씀 */
|
||||
@@ -161,7 +169,7 @@ export interface ItemCardsHandle {
|
||||
|
||||
/** 카드 하나 — 틀 · [저장] · ? · [수정] · 부제 번호 · 이름 */
|
||||
interface Card {
|
||||
kind: EditKind;
|
||||
kind: CardKind;
|
||||
shell: CardShell;
|
||||
saver: SaveControl;
|
||||
help: HTMLElement;
|
||||
@@ -180,15 +188,15 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle {
|
||||
let preview: DrawingCardHandle | null = null;
|
||||
let seq = 0;
|
||||
/** 빈 카드에서 [새로 만들기] 를 누른 칸 — 빈 문서라도 미리보기 편집을 띄움(열이 바뀌면 비움) */
|
||||
const started = new Set<EditKind>();
|
||||
const started = new Set<CardKind>();
|
||||
|
||||
const numberOf = (kind: EditKind, id: string): string => opts.numberOf?.(kind, id) ?? "";
|
||||
const numberOf = (kind: CardKind, id: string): string => opts.numberOf?.(kind, id) ?? "";
|
||||
const anyDirty = (): boolean => cards.some((c) => c.saver.dirty());
|
||||
const reload = (): void => void load(column ? { id: column, label } : null, true);
|
||||
|
||||
/** 카드 틀 하나 — 제목 줄 오른쪽 = ? · [저장] · [〇〇 수정](빈 단추 한 모양) · 부제 = 번호 · 이름 */
|
||||
const makeCard = (
|
||||
kind: EditKind,
|
||||
kind: CardKind,
|
||||
title: string,
|
||||
body: HTMLElement,
|
||||
save: () => Promise<void>,
|
||||
@@ -207,7 +215,7 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle {
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
});
|
||||
edit.addEventListener("click", () => opts.onEdit(kind));
|
||||
edit.addEventListener("click", () => (kind === "labor" ? opts.labor?.() : opts.onEdit(kind)));
|
||||
const take = createButton({ label: TEXT.take, variant: "ghost", size: "sm" });
|
||||
take.title = fill(TEXT.takeTip, PART_WORD[kind]);
|
||||
take.addEventListener("click", () => opts.onImport?.(kind));
|
||||
@@ -290,11 +298,38 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle {
|
||||
});
|
||||
paintPills(readStateRaw("m02-card-pills") === "true");
|
||||
sheets[0].shell.tools.prepend(pills);
|
||||
const cards: Card[] = [info, ...sheets];
|
||||
const defaults: Record<EditKind, string> = {
|
||||
// 일위대가 카드 — 보기만(고침 = [일위대가 수정] 상세) · 부품은 처음 열 때 받음
|
||||
const laborHost = el("div", { className: "m02-struct__labor" });
|
||||
const laborCard = opts.labor
|
||||
? makeCard("labor", PART_WORD.labor, laborHost, () => Promise.resolve())
|
||||
: null;
|
||||
let laborView: LaborViewHandle | null = null;
|
||||
/** 일위대가 카드 — 그 열 문서 · calc 를 받아 그림 · 빈 일위대가(로직 · 자재 없음)면 두 단추 */
|
||||
const paintLabor = async (at: string, mine: number): Promise<void> => {
|
||||
if (!laborCard) return;
|
||||
try {
|
||||
laborView ??= (await import("./M02_MasterTemplete_Labor_View")).createLaborView();
|
||||
} catch (error) {
|
||||
if (mine === seq) laborCard.shell.fail(fill(TEXT.noSheet, why(error)), reload);
|
||||
return;
|
||||
}
|
||||
if (mine !== seq) return;
|
||||
const view = laborView;
|
||||
laborHost.replaceChildren(view.root);
|
||||
await view.show(opts.source().layer, at);
|
||||
if (mine !== seq) return;
|
||||
const d = view.doc();
|
||||
laborCard.no.textContent =
|
||||
d?.일위대가번 || numberOf("labor", at) || fill(TEXT.noFieldNumber, PART_FIELD.labor);
|
||||
if (blank(laborCard, !!d && !d.담은로직.length && !d.자재.length))
|
||||
laborHost.replaceChildren(startBox(laborCard, () => opts.labor?.()));
|
||||
};
|
||||
const cards: Card[] = [info, ...(laborCard ? [laborCard] : []), ...sheets];
|
||||
const defaults: Record<CardKind, string> = {
|
||||
structure: TEXT.edit,
|
||||
area: fill(TEXT.editSheet, PART_WORD.area),
|
||||
material: fill(TEXT.editSheet, PART_WORD.material),
|
||||
labor: fill(TEXT.editSheet, PART_WORD.labor),
|
||||
};
|
||||
|
||||
const root = el("div", {
|
||||
@@ -309,7 +344,7 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle {
|
||||
const drawEdit = drawingEditable(opts.editable(), opts.source());
|
||||
const sheetEdit = !!opts.sheetEdit?.can();
|
||||
for (const c of cards) {
|
||||
const can = c.kind === "structure" ? drawEdit : sheetEdit;
|
||||
const can = c.kind === "structure" ? drawEdit : c.kind !== "labor" && sheetEdit;
|
||||
c.saver.button.hidden = c.help.hidden = !can;
|
||||
c.edit.hidden = !opts.editable();
|
||||
}
|
||||
@@ -454,6 +489,8 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle {
|
||||
doc = null;
|
||||
docVersion = null;
|
||||
previewHost.replaceChildren();
|
||||
laborHost.replaceChildren();
|
||||
void paintLabor(col.id, mine);
|
||||
for (const c of sheets) {
|
||||
c.doc = null;
|
||||
c.number = "";
|
||||
@@ -496,7 +533,7 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle {
|
||||
relabel: (labels) => {
|
||||
for (const c of cards) {
|
||||
(c.edit.querySelector(".ui-btn__label") ?? c.edit).textContent =
|
||||
labels[c.kind] ?? defaults[c.kind];
|
||||
(c.kind === "labor" ? null : labels[c.kind]) ?? defaults[c.kind];
|
||||
}
|
||||
},
|
||||
column: () => column,
|
||||
@@ -511,6 +548,7 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle {
|
||||
destroy: () => {
|
||||
seq += 1;
|
||||
drop();
|
||||
laborView?.destroy();
|
||||
root.remove();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -26,9 +26,7 @@ import type { LinkOptions } from "../A00_Common/spreadsheet/spreadsheet_links";
|
||||
import { searchMaterials } from "../M01_MasterData/M01_MasterData_Api_Fetch";
|
||||
import { fetchUsableCombos } from "./M02_MasterTemplete_Labor_Api";
|
||||
import {
|
||||
copyArea,
|
||||
copyMaterial,
|
||||
copyStructure,
|
||||
copyPartFrom,
|
||||
fetchLaborPills,
|
||||
fetchStructureNumbers,
|
||||
getActiveLayer,
|
||||
@@ -41,7 +39,7 @@ import {
|
||||
createItemCards,
|
||||
PART_FIELD,
|
||||
PART_WORD,
|
||||
type EditKind,
|
||||
type CardKind,
|
||||
type SheetCardControl,
|
||||
type SheetEditOptions,
|
||||
type SheetKind,
|
||||
@@ -52,7 +50,7 @@ import { pickHere, pickedName, type From } from "./M02_MasterTemplete_ItemPick";
|
||||
import { createInputsCard, inputPills, type InputsCardHandle } from "./M02_MasterTemplete_Inputs";
|
||||
import "./M02_MasterTemplete_Structure.css";
|
||||
|
||||
export type { EditKind, SheetKind } from "./M02_MasterTemplete_ItemCards";
|
||||
export type { CardKind, EditKind, SheetKind } from "./M02_MasterTemplete_ItemCards";
|
||||
|
||||
/** 구조물 도면이 걸리는 집계표 이름 — 서버 `M02_MasterTemplete_Store.STRUCTURE_TABLE` 과 같은 글 */
|
||||
export const STRUCTURE_TABLE = "구조물집계표";
|
||||
@@ -62,27 +60,20 @@ const TEXT = {
|
||||
find: "찾기",
|
||||
clear: "비움",
|
||||
none: "고를 것이 없음",
|
||||
pickPrice: "일위대가 고르기",
|
||||
combosFailed: "일위대가 목록을 못 받음 — {value}",
|
||||
saveFirst: "새 열은 구조물 조합을 먼저 저장해야 번호가 생김",
|
||||
overwrite: "{from} 의 {what} 내용을 {to} 에 덮어씀 — 번호 · 열은 그대로",
|
||||
pickCopy: "{value} 복사해 가져오기",
|
||||
copied: "복사해 가져옴",
|
||||
acrossLater: "다른 곳(층 · 사람)에서 가져오기는 서버 길이 선 뒤 — 지금은 같은 곳만",
|
||||
copyFailed: "복사 실패 — {value}",
|
||||
noPills: "이 열 일위대가에 담은 로직 · 자재가 없어 알약 없음 — 일위대가 상세에서 담음",
|
||||
pillsFailed: "일위대가 알약을 못 받음 — {value}",
|
||||
discardCards: "카드(도면 · 면적산출 · 재료산출)에 저장 안 한 고침이 있음 — 버리고 넘어갈까?",
|
||||
};
|
||||
|
||||
/** 집계표 열 하나에 딸린 문서 셋 — 번호 칸 · 이름 · 복사 */
|
||||
const COPY: Record<EditKind, (to: string, from: string) => Promise<void>> = {
|
||||
structure: copyStructure,
|
||||
area: copyArea,
|
||||
material: copyMaterial,
|
||||
};
|
||||
/** 집계표 줄 열쇠 → 문서 */
|
||||
const ROW_KIND: Record<string, EditKind> = {
|
||||
const ROW_KIND: Record<string, CardKind> = {
|
||||
"s:price": "labor", // 일위대가 = 열마다 문서(39장) — 다른 항목 것을 복사해 옴
|
||||
"s:drawing": "structure",
|
||||
"s:area": "area",
|
||||
"s:material": "material",
|
||||
@@ -366,7 +357,7 @@ export interface StructurePanelOptions {
|
||||
isAdmin: boolean;
|
||||
/** [도면 수정] · [면적산출 수정] · [재료산출 수정] — 그 열을 CAD · 스프레드시트 전체 편집으로 엶
|
||||
* (페이지의 `open`) · label 은 화면 제목에 씀 */
|
||||
onEdit: (column: string, label: string, kind: EditKind) => void;
|
||||
onEdit: (column: string, label: string, kind: CardKind) => void;
|
||||
/** 집계표에 저장 안 한 고침이 있으면 버릴지 물음 — 취소면 false */
|
||||
confirmLeave: () => Promise<boolean>;
|
||||
/** 「선택 입력」 카드에서 자리를 골랐음 — 저장 안 한 고침 표시 */
|
||||
@@ -418,6 +409,7 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane
|
||||
extraCards: [inputs.root],
|
||||
// 「복사해 가져오기」 · 빈 카드 [복사 생성](39-5) — 집계표 줄 「조회 후 선택」 과 같은 길
|
||||
onImport: (kind) => void pickCopy(kind, shown ?? undefined),
|
||||
labor: () => void openEdit("labor"), // 일위대가 카드(보기 갈래 · 39-3) — 도면 다음
|
||||
// 면적산출 · 재료산출 카드 미리보기 수정(31-3) — 전체 편집기와 같은 알약 · 저장 길
|
||||
sheetEdit: {
|
||||
can: () => opts.isAdmin,
|
||||
@@ -455,15 +447,9 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane
|
||||
if (col) cards.setLabel((columnLabel = columnHead(col))); // 머리를 고쳤을 수 있음
|
||||
};
|
||||
|
||||
const pickPrice = async (colId: string, columns: StructureColumn[]): Promise<string | null> => {
|
||||
const col = columns.find((c) => c.id === colId);
|
||||
const got = await pickCombo(col ? `${TEXT.pickPrice} — ${columnHead(col)}` : TEXT.pickPrice);
|
||||
return got ? got.key : null;
|
||||
};
|
||||
|
||||
/** 다른 항목의 도면 · 면적산출(…)을 골라 내용만 복사(39-5) — 탭 = 읽을 수 있는 곳 · 덮어쓰기 확인 ·
|
||||
* 번호 · 열은 그대로 · 보고 있던 열이면 다시 읽음 · 집계표 줄 「조회 후 선택」 · 카드 「복사해 가져오기」가 씀 */
|
||||
const pickCopy = async (kind: EditKind, to: StructureColumn | undefined): Promise<void> => {
|
||||
const pickCopy = async (kind: CardKind, to: StructureColumn | undefined): Promise<void> => {
|
||||
const word = PART_WORD[kind];
|
||||
const toNo = to && numbers[to.id]?.[PART_FIELD[kind]];
|
||||
if (!to || !toNo) return void showToast(TEXT.saveFirst, "error");
|
||||
@@ -480,10 +466,18 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane
|
||||
.replace("{what}", word)
|
||||
.replace("{to}", target),
|
||||
});
|
||||
if (!picked || !("row" in picked)) return;
|
||||
if (!picked) return;
|
||||
try {
|
||||
if (!same(picked.from)) throw new Error(TEXT.acrossLater); // 층 넘는 복사 길(39-1b) 뒤에 이음
|
||||
await COPY[kind](to.id, picked.row.열);
|
||||
// 같은 곳 = 다른 열 · 다른 층 · 사람 = 원본 층 · 실무문서 = 조합 키 → 일위대가(39-1b)
|
||||
if ("combo" in picked)
|
||||
await copyPartFrom(kind, to.id, picked.combo.키, { layer: "practice", owner: null });
|
||||
else
|
||||
await copyPartFrom(
|
||||
kind,
|
||||
to.id,
|
||||
picked.row.열,
|
||||
same(picked.from) ? null : { layer: picked.from.layer, owner: picked.from.owner },
|
||||
);
|
||||
showToast(TEXT.copied, "success");
|
||||
if (column === to.id) void load(shown);
|
||||
} catch (error) {
|
||||
@@ -497,7 +491,6 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane
|
||||
columns: StructureColumn[],
|
||||
): Promise<string | null> => {
|
||||
if (!opts.isAdmin || !colId) return null;
|
||||
if (row === "s:price") return pickPrice(colId, columns);
|
||||
if (ROW_KIND[row])
|
||||
await pickCopy(
|
||||
ROW_KIND[row],
|
||||
@@ -506,7 +499,7 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane
|
||||
return null;
|
||||
};
|
||||
|
||||
async function openEdit(kind: EditKind): Promise<void> {
|
||||
async function openEdit(kind: CardKind): Promise<void> {
|
||||
const at = column;
|
||||
if (!at) return;
|
||||
// 저장 전 새 열(분류 더하기) — 문서 · 번호가 아직 없음 · 떠나면 집계표 고침까지 버리게 됨
|
||||
|
||||
Reference in New Issue
Block a user