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
+1 -1
View File
@@ -73,7 +73,7 @@ const TEXT = {
Before_After: ["계", "Sum"],
Job_Done: ["적용함", "Applied"],
Dirty_First: [
"저장 안 한 고침이 있음 — [수식 저장] 또는 [고침 버림] 먼저",
"저장 안 한 고침이 있음 — [저장] 또는 [되돌림] 먼저",
"Save or discard edits first",
],
No_Key: [
+14 -24
View File
@@ -1,13 +1,14 @@
/* =============================================================================
* line_stack_screen.ts
* 단가산출 「줄 쌓기」 탭(PLAN 804-2 · 요구 670~677) — 공용(M01 단가산출 기초 · M02 단가산출 = 길 앞머리만 바꿈)
* 위 = 옵션 가로(`formula_doc_options.ts`) · [고침 버림] · [수식 저장](관리자 · 층 쓰기) · M02 사본 [단가 갱신] · [재생성]
* M01(`page` 있음 · 804-7) = 자기 단추 없음 — 페이지 [저장] · [되돌림]이 `save` · `discard` 를 부름 · 고침이 바뀌면 `page()`
* 위 = 옵션 가로(`formula_doc_options.ts`) · M02 사본 [단가 갱신] · [재생성] · 「M01 새 판」 배지(층 쓰기)
* `page` 있음(M01 804-7 · M02 804-8) = 저장 · 되돌림 단추 없음 — 페이지 [저장] · [되돌림]이 `save` · `discard` 를 부름 ·
* 고침이 바뀌면 `page()` · `page` 없음(일위대가 줄 모달 등) = 위에 [고침 버림] · [수식 저장]
* 가운데 = 왼쪽 줄 쌓기 편집(`line_stack_edit.ts`) | 오른쪽 산출근거 출력(`line_stack_out.ts` · 기계 시간당 경비 펼침)
* 아래 = 합계 상자(노무비 · 재료비 · 경비 · 계 · 화면 아래 고정 · 요구 687-3)
* 정본 = 수식 문서(탭을 바꾸면 다시 읽음) · 줄 쌓기 차림 = 문서 칸 `쌓기` · 804-6 옛 수식 문서 탭 걷음 = 이 화면이 「수식」 탭
* 셈 = 브라우저 계산기(`calcDoc`) 즉시 · 캐시에 없는 연결만 뒤에서 `POST {길}/preview`(옛 요청 끊음)
* 고침 = 캐시(sessionStorage)에 쌓고 [수식 저장] 한 번에 `POST {길}/save`(서버가 정본으로 다시 셈) · 자동저장 없음
* 고침 = 캐시(sessionStorage)에 쌓고 [저장] 한 번에 `POST {길}/save`(서버가 정본으로 다시 셈) · 자동저장 없음
* M02 사본(`copy`) — 저장 몸 {문서, 끊음(받은 그대로), 판} · 판 바뀜(409)은 문서가 그대로면 새 판으로 한 번 더
* [단가 갱신] · [재생성] = 미리 셈 → 알림 → 저장(`formula_doc_copy.ts` · 고친 것 먼저 저장)
* ========================================================================== */
@@ -28,7 +29,6 @@ import type { DocTable } from "../common_util/common_util_formula_doc_table";
import { createDocCache, type DocRecord } from "../common_util/common_util_formula_doc_cache";
import {
docOf,
notesOf,
stackOf,
type StackDoc,
type StackItem,
@@ -61,7 +61,7 @@ export interface StackScreenOptions {
copy?: CopyOptions;
/** M02 SK 층(804-4) — 줄 쌓기 다른 단가산출 · 일위대가 복사 길 · 없으면(M01) 복사 없음 */
scope?: { layer: Layer; owner: string | null };
/** 페이지가 저장 · 되돌림을 맡음(M01 · 804-7) — 고침 있음이 바뀔 때 부름 · 화면 위 [고침 버림] · [수식 저장] 없음 */
/** 페이지가 저장 · 되돌림을 맡음(M01 804-7 · M02 804-8) — 고침 있음이 바뀔 때 부름 · 화면 위 [고침 버림] · [수식 저장] 없음 */
page?: () => void;
}
@@ -80,14 +80,13 @@ const clone = <T>(v: T): T => JSON.parse(JSON.stringify(v)) as T;
export function createStackScreen(opt: StackScreenOptions) {
const head = el("div", { className: "lstk__head" });
const notes = el("div", { className: "lstk__notes" });
const left = el("div", { className: "lstk__left" });
const right = el("div", { className: "lstk__right" });
const foot = el("div", { className: "lstk__foot" });
const root = el("div", {
className: "lstk",
attrs: { "data-line-stack": "" },
children: [head, notes, el("div", { className: "lstk__areas", children: [left, right] }), foot],
children: [head, el("div", { className: "lstk__areas", children: [left, right] }), foot],
});
// 산출근거 고정 높이(요구 683) — 스크롤 조상(없으면 창) 높이 · 넓을 때만 고정(line_stack.css)
const fit = (): void => {
@@ -245,7 +244,7 @@ export function createStackScreen(opt: StackScreenOptions) {
if (from === "left") r[0]?.scrollIntoView({ block: "nearest", behavior: "smooth" });
};
/** 복사해 펼침(804-4) — 서버 조각 → 새 항목(고침 = 캐시 · [수식 저장] 때 서버가 다시 셈) */
/** 복사해 펼침(804-4) — 서버 조각 → 새 항목(고침 = 캐시 · [저장] 때 서버가 다시 셈) */
const takeIn = async (src: string): Promise<void> => {
try {
const got = await call<Taken>(`/take?src=${encodeURIComponent(src)}`);
@@ -263,7 +262,7 @@ export function createStackScreen(opt: StackScreenOptions) {
const edit = (): boolean => opt.admin && (!!key || !cp);
const draw = (): void => {
if (!doc || !answer) {
[head, notes, left, right, foot].forEach((x) => x.replaceChildren());
[head, left, right, foot].forEach((x) => x.replaceChildren());
return;
}
const options = buildOptions(doc, given, !!cp && !opt.admin, (k, v, x) => {
@@ -274,10 +273,10 @@ export function createStackScreen(opt: StackScreenOptions) {
draw();
});
const busy = dirty();
const tools =
edit() && key && !opt.page
const tools = [
...(cp && edit() && key ? copyTools(true, (job) => void runJob(job)) : []),
...(edit() && key && !opt.page
? [
...(cp ? copyTools(true, (job) => void runJob(job)) : []),
createButton({
label: ft("Discard"),
variant: "ghost",
@@ -293,7 +292,8 @@ export function createStackScreen(opt: StackScreenOptions) {
onClick: () => void save(),
}),
]
: [];
: []),
];
head.replaceChildren(
...(options.length ? [el("div", { className: "fdoc__options", children: options })] : []),
el("div", {
@@ -310,17 +310,6 @@ export function createStackScreen(opt: StackScreenOptions) {
],
}),
);
const kept = notesOf(doc);
notes.replaceChildren(
...(kept.length
? [
el("p", {
className: "ui-text ui-text--muted",
text: `${lt("Notes")} — ${kept.join(" · ")}`,
}),
]
: []),
);
const items = stackOf(doc);
root.dataset.edit = edit() ? "1" : "";
left.replaceChildren(
@@ -468,6 +457,7 @@ export function createStackScreen(opt: StackScreenOptions) {
if (all[next]) doc = clone(all[next] as StackDoc);
recalc();
draw();
opt.page?.(); // 캐시에서 이어 받은 고침 — 페이지 [저장] · [되돌림] 켬
} catch (e) {
if (mine === seq)
head.replaceChildren(
-4
View File
@@ -60,10 +60,6 @@ const TEXT = {
Empty_Item: ["줄 없음 — [+]", "No lines — [+]"],
Empty: ["빈 단가산출 — [+ 그룹] 으로 시작", "Empty — start with [+ Group]"],
Text_Ph: ["< 품셈 근거 > · ※ 메모 · 별산", "Note"],
Notes: [
"줄 쌓기에서 못 고치는 칸(보존됨 · 수식 탭에서 고침)",
"Kept fields (edit in Equation tab)",
],
Unsaved_New: ["저장한 단가산출만 — 페이지 [저장] 뒤", "Saved unit prices only"],
Find_Title: ["찾아 넣기", "Find and add"],
Repick: ["찾아 바꾸기", "Find and change"],