fix(M02): 31-3 합치기 충돌 표시 풀기 — 도면 카드(31-4) · 엑셀 카드(31-3) 둘 다 살림

- main_laptop_1 의 auto 커밋(99a514d9)이 ItemCards · Structure · UI_Main 에 충돌 표시를 그대로 올림 — 군데마다 양쪽을 합침
- ItemCards: 글(도면 버림 물음 + 저장 글) · 옵션(onDirty + sheetEdit) · 손잡이 dirty = 도면 또는 엑셀 · saveAll = 도면 먼저 · 엑셀 · discard 새로 · 옛 hideToolbar 는 DrawingCard 몫이라 뺌
- DrawingCard: save() 드러냄(위 [저장]이 먼저 부름 · 못 하면 던짐)
- Structure: 다른 항목 고름 물음은 한 번(카드 셋 모두) · 확인이면 discard 뒤 읽음 · 손잡이 dirty 한 벌 · saveCards
- UI_Main: 떠나기 · 창 닫기 막기 = busy()(이 화면 + 카드 둘)
- 시험 test_m02_drawing_card 물음 판정을 합친 모양으로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ly2oHAaTDYRjcniAmHR81z
This commit is contained in:
2026-09-30 20:38:52 +09:00
co-authored by Claude Opus 5.5
parent 99a514d9b8
commit 63c6fa360c
5 changed files with 29 additions and 57 deletions
@@ -75,6 +75,8 @@ export interface DrawingCardHandle {
/** 카드 머리에 붙일 [저장] · ? (보기만이면 빈 칸) */
tools: HTMLElement;
dirty: () => boolean;
/** 고친 것이 있으면 카드 [저장]과 같이 저장 — 못 하면 던짐(위 [저장]이 먼저 부름) */
save: () => Promise<void>;
destroy: () => void;
}
@@ -153,6 +155,11 @@ export function mountDrawingCard(host: HTMLElement, opt: DrawingCardOptions): Dr
return {
tools: el("span", { className: "m02-struct__tools", children: edit ? [help, save] : [] }),
dirty: () => dirty,
save: async () => {
if (!dirty) return;
await doSave();
if (dirty) throw new Error(TEXT.failed.replace("{value}", ""));
},
destroy: () => {
handle.destroy();
helpBox.remove();
@@ -47,13 +47,10 @@ const TEXT = {
noFieldNumber: "{value} 없음",
noSheet: "스프레드시트를 못 띄움 — {value}",
readFailed: "구조물 도면을 못 읽음 — {value}",
<<<<<<< HEAD
discard: "도면 카드에 저장 안 한 고침이 있음 — 버리고 다른 항목을 열까요?",
=======
save: "저장",
saved: "저장했음",
saveFailed: "저장 못 함 — {value}",
>>>>>>> origin/sub_laptop_8
};
const why = (error: unknown): string => (error instanceof Error ? error.message : "");
@@ -84,10 +81,8 @@ export interface ItemCardsOptions {
warnOnFail?: (column: string) => boolean;
/** 세 카드 뒤에 덧붙는 카드(선택 입력 …) */
extraCards?: HTMLElement[];
<<<<<<< HEAD
/** 도면 카드에서 저장 안 한 고침이 생김 · 없어짐(PLAN 31-4) */
onDirty?: (dirty: boolean) => void;
=======
/** 면적산출 · 재료산출 카드를 미리보기에서 고침(31-3) — 없으면 보기만 */
sheetEdit?: SheetEditOptions;
}
@@ -114,7 +109,6 @@ export interface SheetEditOptions {
) => Promise<{ links?: LinkOptions; below?: HTMLElement; refresh?: () => void }>;
/** 카드 [저장] — 전체 편집기와 같은 저장 길 · 새 판을 돌려줌 */
save: (kind: SheetKind, column: string, doc: unknown, version: string) => Promise<string>;
>>>>>>> origin/sub_laptop_8
}
export interface ItemCardsHandle {
@@ -127,15 +121,12 @@ export interface ItemCardsHandle {
relabel: (labels: Partial<Record<EditKind, string | null>>) => void;
/** 지금 보이는 열 id */
column: () => string | null;
<<<<<<< HEAD
/** 도면 카드에 저장 안 한 고침이 있나 — 페이지가 떠날 때 물음 */
/** 카드(도면 · 면적산출 · 재료산출)에 저장 안 한 고침이 있나 — 페이지가 떠날 때 물음 */
dirty: () => boolean;
=======
/** 카드에 저장 안 한 고침이 있나(31-3) */
dirty: () => boolean;
/** 고친 카드를 모두 저장 — 실패하면 던짐 */
/** 고친 카드를 모두 저장(도면 · 엑셀) — 실패하면 던짐 */
saveAll: () => Promise<void>;
>>>>>>> origin/sub_laptop_8
/** 저장 안 한 카드 고침을 버림(물음에서 「확인」 뒤) */
discard: () => void;
destroy: () => void;
}
@@ -240,8 +231,6 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle {
}
};
<<<<<<< HEAD
=======
/** 카드 [저장] — 읽은 문서(번호 등 계약 밖 칸)에 시트를 덮어 저장(전체 편집기 `doSave` 와 같은 모양) */
async function saveCard(c: SheetCard): Promise<void> {
const at = column;
@@ -311,21 +300,6 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle {
c.save.disabled = true;
};
/** 미리보기 CAD 는 iframe — 안쪽 도구 모음(제목줄 · 리본)을 스타일로 숨김(같은 출처라 접근 가능) */
const hideToolbar = (frame: HTMLIFrameElement): void => {
const apply = (): void => {
const inner = frame.contentDocument;
if (!inner?.head || inner.getElementById("m02-struct-hide")) return;
const style = inner.createElement("style");
style.id = "m02-struct-hide";
style.textContent = ".cad-titlebar, .cad-ribbon { display: none !important; }";
inner.head.append(style);
};
frame.addEventListener("load", apply);
apply();
};
>>>>>>> origin/sub_laptop_8
/** 스프레드시트 미리보기 — 보기만(고침은 [수정] 이 여는 전체 화면) · 부품을 늦게 받음 ·
* 그 사이 열이 바뀌면 버림 */
const paintSheet = async (c: SheetCard, mine: number): Promise<void> => {
@@ -438,14 +412,15 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle {
}
},
column: () => column,
<<<<<<< HEAD
dirty: () => !!preview?.dirty(),
=======
dirty: () => sheets.some((c) => c.dirty),
dirty: () => !!preview?.dirty() || sheets.some((c) => c.dirty),
saveAll: async () => {
await preview?.save(); // 도면 카드(31-4)
for (const c of sheets) await saveCard(c);
},
>>>>>>> origin/sub_laptop_8
discard: () => {
drop();
opts.onDirty?.(false);
},
destroy: () => {
seq += 1;
drop();
@@ -70,7 +70,7 @@ const TEXT = {
noPrice: "이 열에 일위대가가 없어 알약 없음 — 구조물 조합 일위대가 줄에서 고름",
noInputs: "일위대가 {value} 에 입력값이 없어 알약 없음",
inputsFailed: "일위대가 입력값을 못 받음 — {value}",
discardCards: "면적산출 · 재료산출 카드에 저장 안 한 고침이 있음 — 버리고 넘어갈까?",
discardCards: "카드(도면 · 면적산출 · 재료산출)에 저장 안 한 고침이 있음 — 버리고 넘어갈까?",
};
/** 집계표 열 하나에 딸린 문서 셋 — 번호 칸 · 이름 · 복사 */
@@ -381,14 +381,12 @@ export interface StructurePanelHandle {
reload: () => Promise<void>;
/** 「선택 입력」 카드에서 고른 자리를 서버에 저장 — 집계표 [저장] 뒤에 부름(실패하면 던짐) */
saveInputs: () => Promise<void>;
/** 면적산출 · 재료산출 카드에 저장 안 한 고침(31-3) */
/** 카드에 저장 안 한 고침(도면 31-4 · 면적산출 · 재료산출 31-3) — 페이지가 떠날 때 함께 물음 */
dirty: () => boolean;
/** 고친 카드 저장 — 집계표 [저장] 이 카드를 다시 읽기 전에 부름(실패하면 던짐) */
/** 고친 카드 저장(도면 · 엑셀) — 집계표 [저장] 이 카드를 다시 읽기 전에 부름(실패하면 던짐) */
saveCards: () => Promise<void>;
/** 고른 열이 바뀜 — 같은 열이면 이름만 새로(CAD 를 다시 안 띄움) */
show: (colId: string | null, columns: StructureColumn[]) => void;
/** 도면 카드에 저장 안 한 고침(31-4 · 카드 [저장] 몫) — 페이지가 떠날 때 함께 물음 */
dirty: () => boolean;
/** 보던 열을 다시 읽음 — 집계표 [저장] 뒤(서버가 새 열 파일 · 번호를 만듦) */
refresh: (columns: StructureColumn[]) => void;
/** 집계표 조회 후 선택(`onPick`) — 일위대가 = 고른 키(「비움」 = 빈 글 · 닫으면 null) ·
@@ -443,8 +441,9 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane
// 카드에 저장 안 한 고침 — 버릴지 물음 · 머물면 표의 고른 열을 되돌림
const from = column;
void showConfirmDialog(TEXT.discardCards).then((ok) => {
if (ok) void load(col);
else if (from) opts.keepColumn?.(from);
if (!ok) return void (from && opts.keepColumn?.(from));
cards.discard(); // 한 번 물었음 — 카드가 다시 묻지 않게
void load(col);
});
return;
}
@@ -522,10 +521,7 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane
},
saveInputs: () => inputs.save(),
dirty: () => cards.dirty(),
<<<<<<< HEAD
=======
saveCards: () => cards.saveAll(),
>>>>>>> origin/sub_laptop_8
show,
refresh: (columns) => {
const at = column;
@@ -194,26 +194,18 @@ export function createMain(
let cpanel: ComboPanel | null = null; // 면적산출 · 재료산출 오른쪽 조합 패널(프로젝트 층 아님)
let seq = 0;
let dirty = false;
/** 저장 안 한 고침 — 이 화면 + 면적산출 · 재료산출 카드(31-3) */
/** 저장 안 한 고침 — 이 화면 + 카드(도면 31-4 · 면적산출 · 재료산출 31-3) */
const busy = (): boolean => dirty || !!panel?.dirty();
const guard = (event: BeforeUnloadEvent): void => {
if (!root.isConnected) return window.removeEventListener("beforeunload", guard);
<<<<<<< HEAD
if (!dirty && !panel?.dirty()) return;
=======
if (!busy()) return;
>>>>>>> origin/sub_laptop_8
event.preventDefault();
event.returnValue = "";
};
window.addEventListener("beforeunload", guard);
const confirmLeave = async (): Promise<boolean> => {
<<<<<<< HEAD
if (!dirty && !panel?.dirty()) return true; // 도면 카드 고침(31-4)도 같은 물음
=======
if (!busy()) return true;
>>>>>>> origin/sub_laptop_8
if (!busy()) return true; // 카드 고침(도면 · 엑셀)도 같은 물음
if (!(await showConfirmDialog(L("M02_DiscardConfirm")))) {
// 취소 — 초점을 시트(고른 칸 입력 자리) · 집계표 · 도면으로 돌려 키보드 그대로
host
+4 -2
View File
@@ -45,7 +45,9 @@ def test_저장_안_한_카드_고침은_물음():
"if (preview?.dirty() && col?.id !== column && !(await showConfirmDialog(TEXT.discard)))"
in cards
)
assert "dirty: () => !!preview?.dirty()," in cards
# 도면 카드 + 엑셀 카드(31-3) 둘 다 봄
assert "dirty: () => !!preview?.dirty() || sheets.some((c) => c.dirty)," in cards
assert "dirty: () => cards.dirty()," in _text("M02_MasterTemplete_Structure.ts")
main = _text("M02_MasterTemplete_UI_Main.ts")
assert main.count("!dirty && !panel?.dirty()") == 2 # 떠날 때 물음 · 창 닫기 막음
assert "const busy = (): boolean => dirty || !!panel?.dirty();" in main
assert main.count("!busy()") >= 2 # 떠날 때 물음 · 창 닫기 막음