feat(804-8): M02 단가산출 수식 탭 = M01 과 같은 단추 · 저장 한 번 · 머리 단추 줄바꿈 막음(요구 689)

- 공용 줄 쌓기 화면: 페이지가 저장을 맡는 꼴(`page`)을 M02 단가산출 화면에도 — 탭 위 [고침 버림] · [수식 저장] 걷음 · M02 사본 [단가 갱신] · [재생성] · 「M01 새 판」 배지는 그대로 · 「줄 쌓기에서 못 고치는 칸」 안내 줄 걷음(금액 줄 기호 = 율 줄이 앞 줄 금액을 부르는 속 이름 · 화면에서 고칠 값 아님 · 보존 그대로)
- M02 머리 단추 = [초기화] · [삭제] · [되돌림] · [저장] — 단가산출 상세는 [새로고침] 숨김 · [되돌림] · [저장] = 고침 있을 때만(기본정보 + 수식) · [저장] 한 번 = 수식 먼저 저장(새 판 · 수식 칸 받음) → 단가산출 문서 저장 · [되돌림] · 버리고 떠남 = 수식 캐시도 비움
- 보기 고르기(수식 · 단가산출 상세) = M01 처럼 머리 줄 · [항목 추가] · [율 줄 추가] 걷음(줄 더하기 = 수식 탭 [+] 단가산출 · 일위대가 복사) · 일위대가 줄 모달 안 상세는 그대로
- 머리 단추 줄 맞춤(부품 상자 `fitButtonRow`): 단추가 아래로 떨어지거나 제목이 여러 줄이면 아이콘 단추는 아이콘만(글 = 풍선 도움말) · M01 단추 다섯 · M02 단추 넷 아이콘 · 단가산출 상세 긴 이름은 제목 안에서 줄바꿈
- 사본 할일 막힘 알림 글 = [저장] 또는 [되돌림] 먼저
- 시험: test_804_7 에 M02 단추 · 저장 한 번 · 줄 맞춤 지킴 더함 · test_804_4 권한 시험에 수식 저장 판 = 페이지 저장 판 이어 씀 확인 더함

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-08 14:56:31 +09:00
co-authored by Claude Opus 5.5
parent ef5fb0632f
commit eb64bf64ac
16 changed files with 209 additions and 53 deletions
@@ -5,9 +5,11 @@
* [초기화] = 서버 `-reset` 미리 보기(걷힐 고침 수) → 물음 → 그 층에 씀 → 다시 읽음.
* 덮개 · 뺀줄 · 더한줄 단가를 M01 키 값으로 되돌림 · 옵션(입력값) · 기본정보는 그대로(서버 740-1).
* 둘 다 쓰기 권한이 있는 층에서만 보임(`update`) · 상세에 저장 안 한 고침이 있으면 먼저 물음.
* 단가산출 상세(804-8 · M01 단가산출 기초와 같은 단추 규칙) = [새로고침] 숨김 · [되돌림] = 물음 → 다시 읽음(수식 캐시도 비움) ·
* [되돌림] · [저장] = 고침 있을 때만(`dim`)
* ========================================================================== */
import { createButton, showToast } from "@ui/ui_template_elements";
import { createButton, showConfirmDialog, showToast } from "@ui/ui_template_elements";
import { currentLanguageIndex } from "@ui/ui_template_locale";
import { askConfirm } from "@ui/ui_template_modal";
import {
@@ -41,6 +43,11 @@ const TEXT = {
Done: ["M01 값으로 되돌렸음", "Reverted to M01 values"],
DeleteAsk: ["「{name}」 을(를) 지울까?", "Delete 「{name}」?"],
Deleted: ["지웠음", "Deleted"],
Undo: ["되돌림", "Revert"],
UndoAsk: [
"저장 안 한 고친 것(기본정보 · 수식)을 버리고 저장 뒤 상태로 되돌릴까?",
"Discard unsaved edits (info · formula) and revert to the saved state?",
],
Failed: ["못 했음 — {why}", "Failed — {why}"],
} as const;
const tx = (key: keyof typeof TEXT): string =>
@@ -58,6 +65,8 @@ export interface MainActionsCtx {
open: (sel: Selection | null) => Promise<void>;
/** 좌측 목록을 다시 읽음 */
changed: () => void;
/** 페이지 머리 단추 — 단가산출 상세에서 숨김 · 흐림 */
page: { reload: HTMLButtonElement; save: HTMLButtonElement };
}
/** 문서 안 모든 `뺀줄` 길이 합(단가산출 · 일위대가 줄 안 덮개까지) */
@@ -74,10 +83,15 @@ const why = (error: unknown): string => (error instanceof Error ? error.message
export function createMainActions(ctx: MainActionsCtx): {
reset: HTMLButtonElement;
del: HTMLButtonElement;
undo: HTMLButtonElement;
update: (writable: boolean) => void;
/** 고침 있음 — 단가산출 상세의 [되돌림] · [저장] 흐림 */
dim: (dirty: boolean) => void;
} {
const reset = createButton({ label: tx("Reset"), variant: "ghost" });
const del = createButton({ label: tx("Delete"), variant: "danger-ghost" });
const reset = createButton({ label: tx("Reset"), variant: "ghost", iconStart: "rotateCcw" });
const del = createButton({ label: tx("Delete"), variant: "danger-ghost", iconStart: "trash" });
const undo = createButton({ label: tx("Undo"), variant: "ghost", iconStart: "undo" });
undo.dataset.mainUndo = "";
reset.title = tx("ResetTip");
del.title = tx("DeleteTip");
reset.dataset.mainReset = "";
@@ -145,12 +159,27 @@ export function createMainActions(ctx: MainActionsCtx): {
}
});
undo.addEventListener("click", async () => {
const at = ctx.sel();
if (at && (await showConfirmDialog(tx("UndoAsk"), tx("Undo")))) await ctx.open(at);
});
const sk = (): boolean => target()?.kind === "sangeun";
const dim = (dirty: boolean): void => {
undo.disabled = !dirty;
ctx.page.save.disabled = sk() && !dirty;
};
return {
reset,
del,
undo,
update: (writable) => {
const on = writable && !!target();
reset.hidden = del.hidden = !on;
undo.hidden = !on || !sk();
if (sk()) ctx.page.reload.hidden = true;
dim(false);
},
dim,
};
}