feat(836-2): M02 단가산출 화면 — 열 때 「M01 이 바뀌어 다시 복사함」 알림(항목 · M01 전 → 후 · 지워진 내 고침) · 자동 층 [단가 갱신] · [재생성] · 「새 값」 · 「사본 없는 줄」 걷음

- 새 formula_doc_renew.ts(확인창 부품 + 표) · [확인] = 저장(다시 안 뜸) · [나중에] = 다음에 또
- line_stack_screen 두 곳(단추 줄 조건 · 열기 뒤 알림) · 프로젝트 층 = 단추 그대로
- 내 기계 글 셋 = 「새 값」 → [단가 갱신] 안내를 자동 따라옴으로 · 알림 글 금액 줄 꼴 다듬음

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-15 13:45:18 +09:00
co-authored by Claude Opus 5.5
parent ea95007a32
commit 00514598f0
6 changed files with 72 additions and 7 deletions
+8 -2
View File
@@ -8,6 +8,7 @@
* (821-3) 값 줄만 바뀜(M01 단가 · 내 기계 고침) = 「새 값 있음」(같은 자리 · 같은 꼴) · 지운 키(M01 · 내 기계) = 「지워짐 n」 ·
* [단가 갱신] 확인 창 = 바뀐 자원 줄(이름 · 바뀐 칸 전 → 후 · 지워짐 = 마지막 값 유지)
* (822-1) 값 사본에 없는 셈 줄(옛 저장본 빈틈) = 「사본 없는 줄 n」(지금 값으로 셈 · 저장하면 굳음)
* (836-2) 표지 `자동`(프로젝트 층 밖 = M01 자동 따라옴) = 단추 · 「새 값」 · 「사본 없는 줄」 안 그림(지워짐만)
* ========================================================================== */
import { createBadge } from "@ui/ui_template_badge";
@@ -18,6 +19,7 @@ import type { FormulaDoc } from "../common_util/common_util_formula_doc";
import type { Basis } from "../common_util/common_util_formula_doc_import";
import { swappedCount } from "../common_util/common_util_line_stack_pick";
import { ft } from "./formula_doc_text";
import type { RenewNote } from "./formula_doc_renew";
export type CopyJob = "단가갱신" | "재생성";
@@ -32,6 +34,10 @@ export interface CopyMark {
따로?: boolean;
/** 가져온 식(엑셀 불러오기 · 807-1) 단가기준 — 엑셀 = 엑셀 값 기억 · 우리 = 연결 값 */
단가기준?: Basis;
/** M01 자동 따라옴(836-2 · 프로젝트 층 밖) — [단가 갱신] · [재생성] · 「새 값」 · 「사본 없는 줄」 없음 */
자동?: boolean;
/** 열 때 알림(836-2) — M01 에서 바뀐 항목의 지운 고침 */
갱신?: RenewNote[];
}
export interface CopyOptions {
@@ -58,7 +64,7 @@ export function isStale(m: CopyMark | undefined): boolean {
export function copyBadges(m: CopyMark | undefined, edit: boolean): HTMLElement[] {
const out: HTMLElement[] = [];
const doc = !!m?.M01새판 || !!m?.문서새판?.length;
if (doc || m?.단가새값) {
if ((doc || m?.단가새값) && !m?.자동) {
const b = createBadge(ft(doc ? "New_M01" : "New_Value"), "danger");
b.dataset.copyStale = doc ? "doc" : "value";
out.push(b);
@@ -69,7 +75,7 @@ export function copyBadges(m: CopyMark | undefined, edit: boolean): HTMLElement[
b.dataset.copyGone = m.지운키.join(",");
out.push(b);
}
if (m?.사본없음) {
if (m?.사본없음 && !m.자동) {
const b = createBadge(`${ft("Gap")} ${m.사본없음}`, "warning");
b.title = ft("Gap_Note");
b.dataset.copyGap = String(m.사본없음);
+45
View File
@@ -0,0 +1,45 @@
/* =============================================================================
* formula_doc_renew.ts
* M01 자동 따라옴 알림(PLAN 836-2 · 요구 854) — M02 단가산출(프로젝트 층 밖)을 열 때
* M01 에서 바뀐 항목에 얹힌 내 고침을 지우고 다시 복사한 것을 한 번 보임(서버 `M02_Sk_AutoRenew.notes`)
* 표 = 항목 · M01 전 → 후 · 지워진 내 고침 · [확인] = 저장(다시 안 뜸) · [나중에] = 다음에 열면 또 뜸
* 고침 없는 항목은 조용히 따라옴(알림 없음)
* ========================================================================== */
import { askConfirm, el } from "@ui/ui_template_elements";
import { createTable } from "@ui/ui_template_table";
import { ft } from "./formula_doc_text";
/** 서버 알림 한 줄 — 갈래 = 문서(바탕 수식) · 부름(굳힌 부른 문서) · 기계(기계 덮음) */
export interface RenewNote {
키: string;
이름?: string;
갈래?: string;
M01?: string[];
내고침?: string[];
}
/** 칸 안 여러 줄 — 줄마다 한 덩이 */
const lines = (xs: string[] | undefined): HTMLElement =>
el("div", { children: (xs ?? []).map((t) => el("div", { text: t })) });
/** 알림 창 — 확인이면 true(부르는 쪽이 저장) */
export function askRenew(notes: RenewNote[]): Promise<boolean> {
const table = createTable(
[
{ label: ft("Renew_Item"), width: "28%" },
{ label: ft("Renew_M01") },
{ label: ft("Renew_Mine") },
],
notes.map((n) => [lines([n.이름 || n.키, n.키]), lines(n.M01), lines(n.내고침)]),
{ density: "data" },
);
table.dataset.renewNotes = String(notes.length);
return askConfirm({
title: ft("Renew_Title"),
message: ft("Renew_Ask"),
okLabel: ft("Renew_Ok"),
cancelLabel: ft("Renew_Later"),
detail: table,
});
}
+10
View File
@@ -78,6 +78,16 @@ const TEXT = {
Changed: ["바뀐 자원", "Changed resources"],
Before_After: ["계", "Sum"],
Job_Done: ["적용함", "Applied"],
Renew_Title: ["M01 이 바뀌어 다시 복사함", "Re-copied from updated M01"],
Renew_Ask: [
"M01 에서 바뀐 항목에 있던 내 고침을 지우고 M01 에서 다시 복사함 — [확인] 하면 저장(다시 안 뜸)",
"Your edits on items changed in M01 were removed and re-copied — OK saves (no repeat)",
],
Renew_Item: ["항목", "Item"],
Renew_M01: ["M01 전 → 후", "M01 before → after"],
Renew_Mine: ["지워진 내 고침", "Removed edits"],
Renew_Ok: ["확인", "OK"],
Renew_Later: ["나중에", "Later"],
Basis_Excel: ["엑셀 단가 그대로", "Excel prices"],
Basis_Ours: ["우리 단가로", "Our prices"],
Dirty_First: [
+5 -1
View File
@@ -49,6 +49,7 @@ import {
swapBadge,
} from "./formula_doc_copy";
import { docFoot } from "./formula_doc_foot";
import { askRenew } from "./formula_doc_renew";
import { buildOptions } from "./formula_doc_options";
import { ft } from "./formula_doc_text";
import { addTaken, type Taken } from "../common_util/common_util_line_stack_copy";
@@ -306,7 +307,7 @@ export function createStackScreen(opt: StackScreenOptions) {
const busy = dirty();
const tools = [
...(mark?.단가기준 && opt.page ? [basisPick(mark.단가기준)] : []), // 모달 = 머리 판 단추(793-3b)
...(cp && edit() && key ? copyTools(true, (job) => void runJob(job)) : []),
...(cp && edit() && key && !mark?.자동 ? copyTools(true, (job) => void runJob(job)) : []), // 836-2 자동 = 없음
...(edit() && key && !opt.page
? [
createButton({
@@ -517,6 +518,9 @@ export function createStackScreen(opt: StackScreenOptions) {
recalc();
draw();
opt.page?.(); // 캐시에서 이어 받은 고침 — 페이지 [저장] · [되돌림] 켬
const notes = (mark as CopyMark | undefined)?.갱신; // 836-2 M01 자동 따라옴 알림(take 가 채움)
if (cp && edit() && !dirty() && notes?.length)
void askRenew(notes).then((ok) => ok && save({ 할일: "갱신확인", 판: version }, ""));
} catch (e) {
if (mine === seq)
head.replaceChildren(
+3 -3
View File
@@ -33,7 +33,7 @@ const TEXT = {
Copied: "「{value}」 값을 복사 — 고친 뒤 [저장]",
Own_Value: "개인 값",
Own_Note:
"개인 값 = 이 기계 고유 값(가격 · 손료계수 · 연료 ℓ · 잡품 % · 조종원 수) · 단가(노임 · 유가)는 M01 값 · 단가산출엔 만들 때 복사 → 고친 뒤 「새 값」 → [단가 갱신]",
"개인 값 = 이 기계 고유 값(가격 · 손료계수 · 연료 ℓ · 잡품 % · 조종원 수) · 단가(노임 · 유가)는 M01 값 · 단가산출(프로젝트 밖)은 고치면 저절로 따라옴",
Server_Hourly: "서버 셈 시간당",
Hourly_None: "시간당 못 셈",
Uses: "쓰는 단가산출",
@@ -59,7 +59,7 @@ const TEXT = {
Win_M01: "M01 기계",
Win_Copy: "이 단가산출에 복사된 값",
Win_Live: "원본 지금 값",
Win_Newer: "원본에 새 값 있음 — [단가 갱신] 으로 반영",
Win_Newer: "원본에 새 값 있음 — 프로젝트 사본은 [단가 갱신] 으로 반영",
Win_Swap: "이 기계로 바꾸기",
Win_Edit: "고쳐 쓰기",
Win_Blank: "빈 양식에서 새 기계",
@@ -67,7 +67,7 @@ const TEXT = {
Win_Cancel: "취소",
Win_Edit_Note:
"[확인] = 이 단가산출에 초안으로 끼움 · 단가산출 [저장] 때 내 기계로 저장 · 저장 안 하고 나가면 사라짐",
Win_Uses: "이 기계를 쓰는 다른 단가산출 {value} 건 — 저장하면 그쪽에 「새 값」 배지",
Win_Uses: "이 기계를 쓰는 다른 단가산출 {value} 건 — 저장하면 그쪽도 새 값으로 셈",
Win_Hint: "왼쪽에서 기계를 고르면 값이 보임",
Win_View: "조회",
Why_M01: "기계 값은 M01 기계 상세에서",
+1 -1
View File
@@ -125,7 +125,7 @@ def diff(a: Any, b: Any) -> list[str]:
if dc.fp(x) == dc.fp(y):
continue
name = str((y or x or {}).get("이름") or f"금액 {n + 1}")
show = lambda z: "—" if not z else f"{z.get('수량') or ''} × {z.get('단가') or ''}" # noqa: E731
show = lambda z: " × ".join(str(z[k]) for k in ("수량", "단가") if z and z.get(k)) or "—" # noqa: E731
out.append(f"{name} {show(x)} → {show(y)}")
for slot in ("식", "끝수", "결과단위"):
if dc.fp(a.get(slot)) != dc.fp(b.get(slot)):