feat(804-3): 줄 쌓기 수정 1차 — 표 값 고르기 · 산출근거 들여쓰기 · 찾아 넣기 창 · 짝 강조 · 기계 이름

- 표 값(요구 678-1): 「표」 꼬리표가 글뿐이라 눌러도 안 열림 → [표 · 고른 줄 · 칸 ▾] 단추 = 품셈 표 창(값 칸 누름 = 그 줄 · 칸)
  옵션에 매인 조건(폼타이규격 등) = 그 줄만 고름 · 다른 줄 막힘 + 「위 옵션에서 고름」 안내 · M02 길 표 값 글 수도 고름
- 산출근거 들여쓰기(678-2): 머리 0 · 항목 1 · 가나다 2 · 줄 = 항목 + 1 · 펼친 블록 = 그 줄 + 1(수준 = stackLevels)
- 찾아 넣기(678-3): [+ 줄] 인력 · 재료 · 기계 · 다른 단가산출 = M02 [항목 추가] 창 그대로(그 종류만) · 고르면 줄 생김(닫으면 없음)
  있는 줄 = [찾아 바꾸기](같은 로직 기계 = 지역 · 보정작업 넘김 그대로) · 옛 찾기 칸 + 고름 목록 · 기종 코드 고르개 걷음
  빈 틀 = [찾아 넣기](돈 아닌 단가산출 결과만 — Find_Pick results)
- 짝 강조(679-1): 오른쪽 줄 누름 = 왼쪽 카드 강조 + 보이는 자리로 · 왼쪽 누름 = 오른쪽 같은 줄 강조
- 기계 이름(679-2): 기계 줄 = 기종 이름 · 규격 · machine-hourly 답에 로직 키 · 원문번호 더함(기존 칸 그대로)
- 모델 common_util_line_stack_pick.ts(boundConds · currentRow · pickCell · addPicked · repick · stackLevels)
- 시험 test_804_2 넓힘 3(표 고름 = 값 + 출처 · 서버 셈 같음 · 찾아 넣기 넷 = 요소 키 · 서버 금액 · 다시 고름 · 수준)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-08 01:27:20 +09:00
co-authored by Claude Opus 5.5
parent 6c14e73a5c
commit 0eb7822772
12 changed files with 690 additions and 160 deletions
+39 -3
View File
@@ -22,6 +22,7 @@ import {
} from "@ui/ui_template_elements";
import { blankDoc, type FormulaDoc } from "../common_util/common_util_formula_doc";
import { calcDoc, type DocAnswer } from "../common_util/common_util_formula_doc_calc";
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,
@@ -31,7 +32,6 @@ import {
type StackItem,
} from "../common_util/common_util_line_stack";
import { copyBadges, type CopyMark, type CopyOptions } from "./formula_doc_copy";
import type { Pick } from "./formula_doc_defs";
import { docFoot } from "./formula_doc_foot";
import { buildOptions } from "./formula_doc_options";
import { ft } from "./formula_doc_text";
@@ -90,7 +90,9 @@ export function createStackScreen(opt: StackScreenOptions) {
let seq = 0;
let asked = "";
let ctl: AbortController | null = null;
const tables = new Map<string, Promise<DocTable | null>>(); // 표 고르기(문서 열 때 비움)
const opened = new Set<string>(); // 산출근거 펼친 자리
let paired = ""; // 짝 강조 자리(679 — 항목 `k` · 줄 `k.j`)
const blocks = new Set<string>(); // 풀린 공식 묶음
const call = async <T>(path: string, body?: unknown, signal?: AbortSignal): Promise<T> => {
@@ -187,6 +189,22 @@ export function createStackScreen(opt: StackScreenOptions) {
}
};
/** 표 하나 — 수식 탭과 같은 길(`/table`) · 받은 표는 계산기 캐시에도 */
const tableOf = (k: string): Promise<DocTable | null> => {
if (!tables.has(k))
tables.set(
k,
call<DocTable & { 끊김?: string }>(`/table?key=${encodeURIComponent(k)}`)
.then((t) => {
if (t.끊김) return null;
cache.putTable(k, t);
return t;
})
.catch(() => null),
);
return tables.get(k)!;
};
/** 고침 — 문서 · 차림을 고친 뒤 문서로 굳힘 */
const change = (fn: (d: StackDoc, items: StackItem[]) => void): void => {
if (!doc) return;
@@ -198,6 +216,20 @@ export function createStackScreen(opt: StackScreenOptions) {
draw();
};
/** 짝 강조 — 왼쪽 카드 · 오른쪽 줄(같은 자리) · from = 누른 쪽(반대쪽을 보이는 자리로) */
const pair = (loc: string, from?: "left" | "right"): void => {
paired = loc;
for (const x of root.querySelectorAll(".is-pair")) x.classList.remove("is-pair");
if (!loc) return;
const sel = `[data-loc="${CSS.escape(loc)}"]`;
const l = left.querySelector<HTMLElement>(sel);
const r = [...right.querySelectorAll<HTMLElement>(sel)];
l?.classList.add("is-pair");
r.forEach((x) => x.classList.add("is-pair"));
if (from === "right") l?.scrollIntoView({ block: "nearest", behavior: "smooth" });
if (from === "left") r[0]?.scrollIntoView({ block: "nearest", behavior: "smooth" });
};
// ── 그림 ──
const edit = (): boolean => opt.admin && (!!key || !cp);
const draw = (): void => {
@@ -269,12 +301,12 @@ export function createStackScreen(opt: StackScreenOptions) {
answer,
items,
change,
ask: (kind, words, group) =>
call<{ 후보: Pick[] }>("/links", { 갈래: kind, 검색: words, 분류: group }),
table: tableOf,
subDoc,
name: (k) => cache.names.get(k) ?? "",
open: blocks,
templates,
pair: (loc) => pair(loc, "left"),
saveTpl: (t) => localStorage.setItem(TPL_KEY, JSON.stringify([...templates(), t])),
}),
]
@@ -291,10 +323,12 @@ export function createStackScreen(opt: StackScreenOptions) {
draw();
},
miss: ask,
pair: (loc) => pair(loc, "right"),
})
: el("p", { className: "ui-text ui-text--muted", text: lt("Empty") }),
);
foot.replaceChildren(...(doc.결과 ? [] : [docFoot(answer)]));
pair(paired);
};
const take = (got: Loaded): void => {
@@ -342,6 +376,7 @@ export function createStackScreen(opt: StackScreenOptions) {
const was = key;
shown = next;
if (next !== key) {
paired = "";
opened.clear();
blocks.clear();
given = {};
@@ -350,6 +385,7 @@ export function createStackScreen(opt: StackScreenOptions) {
doc = answer = null;
asked = "";
cache = createDocCache();
tables.clear();
[cut, mark, version] = [{}, undefined, ""];
draw();
if (!next) {