feat(804-7): 줄 쌓기 「수식」 탭 수정 2차(요구 687 · 688)

- 로직 삭제 = 서버 정본에서 바로(로직 줄 + 수식 문서 · `/logic/remove`) → 로직 목록 · 부르는 곳(다른 수식 문서 · 일위대가 조합 · M02 단가산출 바탕)이 있으면 확인 창 전에 막고 까닭(`/logic/users`)
- 머리 단추 다섯 — [신규 로직] · [로직 복사/생성] · [로직 삭제] · [되돌림] · [저장] · [저장] 한 번 = 기본정보 + 수식 · [되돌림] = 둘 다 저장 뒤 상태 · 줄 쌓기 [고침 버림] · [수식 저장]은 M01 에서 걷음(M02 사본은 그대로)
- 합계 상자 화면 아래 고정 · 원문 손잡이 브라우저 오른쪽 끝
- 왼쪽: 그룹 머리 한 줄(접어도 [+] · X 자리 그대로) · 「비우면 번호 없음」 · 가나다 체크 걷음 · 금액 줄 한 줄 배치 · 이름 단추 = 그 이름 찾은 찾기 창 · 수량 기호 = 값 + 출처 꼬리표 · [+] 메뉴(단가산출 · 율 · 식 · 근거 글 · 틀 찾기 걷음) · 식 글 눌러 그 자리 고침([식 고치기] 걷음) · 단위 = 공용 단위 고르기(친 글도 씀) · 만약 안 탄 갈래 숨김
- 오른쪽 기계 줄: 비목 수 링크 걷음 · 제목 줄 누름 = 시간당 경비 펼침 · 펼친 제목 보통 글
- 요소 키가 기호 이름으로 샌 것(`k_EQ0006`) — 변환기가 자원 이름으로(`k_하베스터`) · 수식 문서 다시 빌드(10 파일 · 78 기호 · 금액 같음)
- 시험 페이지 `tools/line-stack-test` · main.py 마운트 · vite 프록시 · M01 왼쪽 「시험: 줄 쌓기」 걷음
- 단위 표준 표 읽기 = 로그인만(M02 일반 사용자 단위 목록)
- 시험: test_804_7 새로 · 804_2 · 804_4 이음 글 고침(createStackScreen 줄 나뉨) · 552_3 여는 단추 기본 ⋯ 꼴로 고침(부르는 쪽이 [+] 로 바꿀 수 있음)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-08 13:33:36 +09:00
co-authored by Claude Opus 5.5
parent 89292a75df
commit abe722f378
43 changed files with 1025 additions and 2061 deletions
+58 -12
View File
@@ -6,6 +6,9 @@
*
* 데이터 흐름(CLAUDE.md 5장) — 고친 것은 캐시(sessionStorage)에 쌓고 [저장] 한 번에 `POST /save`.
* 409 = 그 사이 파일이 바뀜 · 422 = 검사 걸림(아무것도 안 씀) — 까닭은 서버 글 그대로 보임.
* 머리 단추(804-7 · 요구 687-2) = [신규 로직] · [로직 복사/생성] · [로직 삭제] · [되돌림] · [저장]
* [저장] = 기본정보 · 별칭(로직 줄 `/save`) + 수식(줄 쌓기 `/logic/doc/save`) 한 번에 · [되돌림] = 둘 다 저장 뒤 상태로
* [로직 삭제] = 확인 → 서버 정본에서 바로(`/logic/remove` — 로직 줄 + 수식 문서 · 부르는 곳 있으면 막고 까닭) → 로직 목록
* ========================================================================== */
import { t as L } from "@ui/ui_template_locale";
@@ -23,6 +26,8 @@ import {
fetchLogicFiles,
fetchLogics,
fetchLogicSubs,
fetchLogicUsers,
removeLogic,
saveFiles,
type CalcLine,
type TextAnswer,
@@ -108,7 +113,12 @@ export async function mountM01LogicLab(
let recalc: number | undefined;
const drive = createDrive();
// 수식 = 줄 쌓기(804-2 · 804-6 옛 수식 문서 탭 걷음) — 수식 문서 · 캐시 m01_doc_drafts
const stack = createStackScreen({ base: "/m01/logic/doc", admin, cache: "m01_doc_drafts" });
const stack = createStackScreen({
base: "/m01/logic/doc",
admin,
cache: "m01_doc_drafts",
page: () => persist(),
});
const leave = (): Promise<boolean> => stack.canLeave();
const editor = el("div", { className: "m01-logic__editor" });
// 로직 상세 — 왼쪽 지금 내용 · 오른쪽 접는 패널 「원문」
@@ -166,8 +176,7 @@ export async function mountM01LogicLab(
list.setMarks(marks, extra, aliases);
const n = Object.keys(drafts).length;
pending.textContent = n ? tx("Bar_Pending", { n }) : "";
saveButton.disabled = discardButton.disabled = n === 0;
saveButton.hidden = discardButton.hidden = n === 0; // 저장 안 한 고친 것이 없으면 숨김
saveButton.disabled = discardButton.disabled = n === 0 && !stack.dirty(); // 로직 줄 · 수식 둘 다 봄
};
const touch = (): void => {
@@ -307,6 +316,7 @@ export async function mountM01LogicLab(
};
let allLogics: LogicSummary[] = [];
let saving = false; // [저장] 뒤 목록 다시 읽는 중 — 거름 되살림이 연 로직을 떠나지 않게(저장 뒤 다시 엶)
const reload = async (): Promise<void> => {
const [logics, subs] = await Promise.all([fetchLogics(), fetchLogicSubs()]);
@@ -324,7 +334,7 @@ export async function mountM01LogicLab(
restore: true,
onPick: (sub, detail) => {
list.setPick(sub, detail);
if (opened) void leave().then((ok) => ok && show(null));
if (opened && !saving) void leave().then((ok) => ok && show(null));
},
}),
);
@@ -351,8 +361,21 @@ export async function mountM01LogicLab(
}
};
/** 부르는 곳 까닭 띠 */
const usedBy = (used: string[]): void => {
errors.replaceChildren(
el("strong", { text: tx("Delete_Used") }),
...used.map((x) => el("div", { text: x })),
);
errors.hidden = false;
};
const onDelete = async (): Promise<void> => {
if (!opened?.row) return;
if (opened.origKey) {
const used = (await fetchLogicUsers(opened.origKey).catch(() => null))?.쓰는곳 ?? [];
if (used.length) return usedBy(used); // 확인 창 전에 막음
}
if (
!(await showConfirmDialog(
tx("Confirm_Delete", { v: opened.row.원문번호 || opened.row.키 }),
@@ -360,21 +383,38 @@ export async function mountM01LogicLab(
))
)
return;
if (opened.origKey === null) {
delete drafts[opened.id];
const was = opened;
if (was.origKey === null) {
delete drafts[was.id];
persist();
show(null);
return;
}
drafts[opened.id] = pick(opened, null);
opened.row = null;
persist();
show(opened);
const key = was.origKey;
showLoadingOverlay();
try {
const one = await fetchLogic(key); // 지금 판(캐시에 옛 판이 남았어도)
const done = await saveRevised((ok) => removeLogic(key, one.file, one.version, ok));
if (!done) return; // 개정 확인 취소
delete drafts[was.id];
await stack.discard(); // 그 로직 수식 캐시도 비움
await reload();
show(null); // 로직 목록
showToast(tx("Delete_Done", { v: was.row?.원문번호 || key }), "success");
} catch (error) {
const used =
error instanceof ApiError ? (error.detail as { 쓰는곳?: string[] })?.쓰는곳 : null;
if (used?.length) usedBy(used);
else failed(error);
} finally {
hideLoadingOverlay();
}
};
const onDiscard = async (): Promise<void> => {
if (!(await showConfirmDialog(tx("Confirm_Discard"), tx("Bar_Discard")))) return;
drafts = {};
await stack.discard();
persist();
const was = opened;
if (was && was.origKey !== null)
@@ -393,7 +433,10 @@ export async function mountM01LogicLab(
byFile.set(d.file, part);
}
if (!byFile.size) {
showToast(tx("Save_Nothing"), "info");
// 수식만 고침 — 줄 쌓기 정본 저장
if (!stack.dirty()) return void showToast(tx("Save_Nothing"), "info");
if (await stack.save()) showToast(tx("Save_Done"), "success");
persist();
return;
}
showLoadingOverlay();
@@ -403,13 +446,16 @@ export async function mountM01LogicLab(
if (!saved) return; // 개정 확인 취소 = 캐시 그대로
const was = opened;
drafts = {};
await reload();
saving = true;
await reload().finally(() => (saving = false));
showToast(tx("Save_Done"), "success");
const newKey = was?.row && !was.row.키 ? saved.added?.at(-1)?.key : undefined;
const made = newKey ? allLogics.find((x) => x.키 === newKey) : undefined;
if (made) await open(logicId(made.구분, made.키), made.구분, made.키);
else if (was?.row?.키) await open(logicId(was.sub, was.row.키), was.sub, was.row.키);
else show(null);
if (opened) await stack.save(); // 수식 고침도 같은 [저장]에(여는 중이면 다 읽고 · 새 로직 = 새 키로 이어 받은 뒤 · 고침 없으면 그냥)
persist();
} catch (error) {
failed(error);
} finally {