feat(sheet): 산출근거 줄 · 조회 후 선택(onPick) · 둘째 층 묶음 더하기 · 층 이름 고치기

- 마스터 첫 쪽 도면 줄 아래 산출근거 줄 s:basis — 칸 글 basisLabel(colId) · 없으면 「없음」 · 손으로 못 침
- onPick 을 주면 일위대가 · 도면 · 산출근거 칸은 두 번 누르기 · Enter · F2 로 조회 후 선택(글을 돌려주면 일위대가 칸에 넣음)
- 「<둘째 층 이름> 더하기」 단추 — 같은 종류 묶음 끝에 [종류, 새 분류, 새 열] 열(층 셋 이상일 때)
- 머리 층 줄 머리 칸 두 번 누르기 = 층 이름 고치기(master)
- 시험 test_sheet_group_layer.py · 줄 차례 시험에 산출근거 줄

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Kyw9qnkJ94pC1A4PfpDtA
This commit is contained in:
2026-09-28 09:47:58 +09:00
co-authored by Claude Opus 5.5
parent e4eef2a47b
commit 119a390bd7
7 changed files with 227 additions and 31 deletions
+24 -12
View File
@@ -3,8 +3,9 @@
* 표 그리기 — 문서 + 풀이 결과 → 쪽마다 `<table>`(머리 층 · 단위 줄 · 본문 · 합계 줄).
*
* 칸마다 `data-r`(줄 열쇠) · `data-c`(열 id) — 고치기 · 키보드는 격자(`ui_template_sheet.ts`)가 위임으로 받음.
* 줄 열쇠 — 본문 `d:<줄id>` · 합계 `t:<합계id>` · 마스터 줄 `s:unit|formula|desc|price|drawing`.
* master = 값 대신 열마다 식 · 들어갈 것 · 일위대가 · 도면 줄 · project = 쪽줄마다 쪽을 나눔(쪽마다 머리 · 합계는 마지막 쪽).
* 줄 열쇠 — 본문 `d:<줄id>` · 합계 `t:<합계id>` · 마스터 줄 `s:unit|formula|desc|price|drawing|basis`.
* master = 값 대신 열마다 식 · 들어갈 것 · 일위대가 · 도면 · 산출근거 줄 · project = 쪽줄마다 쪽을 나눔(쪽마다 머리 · 합계는 마지막 쪽).
* 머리 층 줄 머리 칸은 `data-layer`(층 차례) — master 에서 층 이름을 고침.
* ========================================================================== */
import { el } from "@ui/ui_template_elements";
@@ -14,10 +15,12 @@ import {
cellEditable,
isBoundColumn,
isCalcColumn,
KEY_BASIS,
KEY_DRAWING,
KEY_UNIT,
MASTER_ROWS,
type MasterRowKey,
PICK_ROWS,
pagePlan,
type SheetMode,
} from "./ui_template_sheet_ops";
@@ -34,13 +37,17 @@ export interface RenderState {
mode: SheetMode;
result: SheetResult;
sel: { r: string; c: string } | null;
/** 도면 줄 칸 글(열 id) — 없거나 빈 글이면 「없음」. */
/** 도면 · 산출근거 줄 칸 글(열 id) — 없거나 빈 글이면 「없음」. */
drawingLabel?: (colId: string) => string;
basisLabel?: (colId: string) => string;
/** 조회 후 선택 줄(`PICK_ROWS`)을 고름으로 받나 — 칸 겉모양만. */
pick?: boolean;
}
/** 도면 줄 칸 글. */
export const drawingText = (state: RenderState, colId: string): string =>
state.drawingLabel?.(colId)?.trim() || st("Drawing_None");
/** 도면 · 산출근거 줄 칸 글. */
export const refText = (state: RenderState, key: string, colId: string): string =>
(key === KEY_BASIS ? state.basisLabel : state.drawingLabel)?.(colId)?.trim() ||
st("Drawing_None");
/** 열 폭 — 저장 폭(없으면 기본) · 머리 글이 안 잘리는 최소 폭보다 좁지 않게. */
const columnWidth = (doc: SheetDoc, col: SheetColumn, mins: Map<string, number>): number =>
@@ -55,9 +62,9 @@ function headFont(): string {
return `600 ${rem * 0.82}px ${getComputedStyle(document.body).fontFamily}`;
}
/** 칸을 고칠 수 있나 — 줄 열쇠 기준(머리 칸은 따로) · 도면 줄은 읽기만. */
/** 칸을 고칠 수 있나 — 줄 열쇠 기준(머리 칸은 따로) · 도면 · 산출근거 줄은 손으로 못 침. */
export function keyEditable(state: RenderState, key: string, col: SheetColumn): boolean {
if (key === KEY_DRAWING) return false;
if (key === KEY_DRAWING || key === KEY_BASIS) return false;
if (key.startsWith("s:")) return state.mode === "master";
if (key.startsWith("t:")) return false;
const row = state.doc.줄.find((r) => `d:${r.id}` === key);
@@ -86,7 +93,7 @@ export function renderSheet(state: RenderState): HTMLElement {
const names = columnNames(cols);
const font = headFont();
const mins = headMinWidths(cols, depth, font);
// 줄 머리 칸 폭 — 층 이름 · 단위 · 식 · 들어갈 것 · 일위대가 · 도면 · 합계 이름이 안 잘리게
// 줄 머리 칸 폭 — 층 이름 · 단위 · 식 · 들어갈 것 · 일위대가 · 도면 · 산출근거 · 합계 이름이 안 잘리게
const GUTTER = Math.min(
GUTTER_MAX,
Math.max(
@@ -98,6 +105,7 @@ export function renderSheet(state: RenderState): HTMLElement {
st("Row_Desc"),
st("Row_Unit_Price"),
st("Row_Drawing"),
st("Row_Basis"),
...(doc.합계줄 ?? []).map((t) => t.이름),
].map((label) => Math.ceil(textWidth(label, font)) + 16),
),
@@ -165,6 +173,7 @@ export function renderSheet(state: RenderState): HTMLElement {
"s:desc": "Row_Desc",
"s:price": "Row_Unit_Price",
"s:drawing": "Row_Drawing",
"s:basis": "Row_Basis",
} as const
)[key];
const tr = el("tr", { className: "ui-sheet__meta", children: [gutter("th", st(label))] });
@@ -174,11 +183,12 @@ export function renderSheet(state: RenderState): HTMLElement {
? formulaLabel(col.식 ?? "", names, col.id)
: key === "s:desc"
? ""
: key === KEY_DRAWING
? drawingText(state, col.id)
: key === KEY_DRAWING || key === KEY_BASIS
? refText(state, key, col.id)
: (col.일위대가 ?? "");
const td = bodyCell(key, col, j, text);
td.classList.remove("is-num");
if (state.pick && PICK_ROWS.includes(key)) td.classList.add("is-pick");
if (key === "s:formula" && col.식) {
// 같은 종류 열은 짧은 이름 · 풍선은 긴 이름 + 고칠 때 쓰는 id 식
td.textContent = "";
@@ -209,7 +219,9 @@ export function renderSheet(state: RenderState): HTMLElement {
const thead = (): HTMLElement => {
const rows = head.map((cells, level) => {
const tr = el("tr", { children: [gutter("th", doc.층?.[level] ?? "")] });
const layer = gutter("th", doc.층?.[level] ?? "", mode === "master" ? "is-editable" : "");
layer.dataset.layer = String(level);
const tr = el("tr", { children: [layer] });
for (const cell of cells) {
const th = el("th", {
text: cell.label,