feat(m01): 단가산출 상세 수량 칸 알약화 — 표 찾기만 알약 · 나머지는 글자

수량 칸을 「알약 + 사칙연산 글자」로 그림 — 찾기(...) 표 호출만 알약(조각 계약
`ref/_분석_알약_상세줄.md` ㉮ 표대로) · 나머지(숫자·연산기호·중간값·설계입력
이름)는 글자. 서버 조각 API 는 아직이라 화면에서 임시로 쪼갬(Logic_Note.ts
찾기() 규칙과 같음) — 서버가 붙으면 걷어냄. 단가·금액 칸은 안 건드림.

알약을 누르면 그 표만 바로 모달로(모달 안 알약 목록은 없앰) · 줄을 누르면 그
줄의 단가·다른 로직을 바로 보임(알약 없이 한 가지만). 인력·재료·경비 세 표를
table-layout: fixed + 같은 칸 폭으로 맞춤 · 알약은 칸 폭 기준 줄바꿈.

원 단위 소수점 0 — 돈 자리(단가·금액·소계·계)만 정수 · 수량·비율은 그대로 ·
조합 미리 보기(Combo_Preview)도 같이 고침.

검증: typecheck 통과 · ORCA 로 13-4-1·12-4·12-17-1 열어 확인(알약 클릭·행
클릭·칸 폭 동일 729.57px·정수 표시 확인, data-sum 은 원래 값 그대로 유지).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1MKKZKpUHTPKb513FneU8
This commit is contained in:
2026-09-22 19:59:48 +09:00
co-authored by Claude Sonnet 5
parent 14046361dd
commit f7dd866876
5 changed files with 206 additions and 121 deletions
@@ -1,44 +1,44 @@
/* =============================================================================
* M01_MasterData_UI_LogicLab_Pill.ts
* 줄 모달의 알약 단추 — 쓰인 자료를 알약으로만 늘어놓고, 누른 것 하나만 펼침(다시 누르면 접힘)
* 알약 단추 — 수량 칸 안 조각(표 찾기 등)을 누르면 곧장 그 자료 모달을 엶.
* (이전에는 알약을 늘어놓고 아래 자리에서 펼쳤으나, 알약이 수량 칸 안으로 들어가며
* 「누르면 모달」 로 바뀜 — 모달 안에 알약 목록을 또 두지 않음.)
* ========================================================================== */
import { el } from "@ui/ui_template_elements";
export interface Pill {
/** 알약 글 — 보기 「표 QF000421」 · 「인력 석공」 */
/** 알약 글 — 보기 「표 QF000421」 */
label: string;
/** 펼칠 때 그림 — 표처럼 읽어 와야 하면 Promise */
view: () => HTMLElement | Promise<HTMLElement>;
/** 누르면 할 일 — 그 자료 모달을 엶 */
onClick: () => void;
}
export function pillBar(pills: Pill[]): HTMLElement {
const stage = el("div", { className: "m01lab__pill-stage" });
const buttons: HTMLButtonElement[] = [];
let open = -1;
let turn = 0;
const show = async (i: number): Promise<void> => {
const mine = ++turn;
open = open === i ? -1 : i;
buttons.forEach((b, k) => {
b.setAttribute("aria-expanded", String(k === open));
b.classList.toggle("is-open", k === open);
});
if (open < 0) return stage.replaceChildren();
const view = await pills[open].view();
if (mine === turn) stage.replaceChildren(view); // 빨리 눌러도 마지막 것만
};
pills.forEach((p, i) => {
const b = el("button", {
className: "m01lab__pill",
text: p.label,
attrs: { type: "button", "aria-expanded": "false", "data-pill": p.label },
});
b.addEventListener("click", () => void show(i));
buttons.push(b);
/** 알약 하나 — 글자 조각 사이에 끼워 쓰는 단추 */
export function pillButton(p: Pill): HTMLButtonElement {
const b = el("button", {
className: "m01lab__pill",
text: p.label,
attrs: { type: "button", "data-pill": p.label },
});
return el("div", {
className: "m01lab__pills",
children: [el("div", { className: "m01lab__pill-row", children: buttons }), stage],
b.addEventListener("click", (ev) => {
ev.stopPropagation(); // 줄 전체 누르기(행 모달)와 안 겹치게
p.onClick();
});
return b;
}
/** 수량 칸 조각 — 알약 아니면 그냥 글자(연산기호·숫자·중간값 이름) */
export type Fragment = { pill: Pill } | { text: string };
/** 조각을 이어 그림 — 칸 폭을 넘으면 자동으로 줄바꿈(`flex-wrap`) */
export function fragmentRow(fragments: Fragment[]): HTMLElement {
return el("span", {
className: "m01lab__qty-frags",
children: fragments.map((f) =>
"pill" in f
? pillButton(f.pill)
: el("span", { className: "m01lab__qty-text", text: f.text }),
),
});
}