feat(M02): 조합 고치기 · 자재 목록을 실제 갈라 내기 길에 연결 · 알약 자재 항목 = 옆 판 목록 · 줄 누르면 이은 칸으로

- 가정하던 서버 함수를 실제 branchCombo · branchMaterialCombo · fetchMaterialCombo(M02 Api_Fetch)로 바꾸고 가정 파일은 열 조합 읽기만 남김
- 재료산출 알약 막대 자재 항목을 그 열 자재 조합 담은 자재로 · 알약 더하기 · 빼기(onItemsChange)와 옆 판 표를 같은 목록으로 맞춤
- 옆 판 줄을 누르면 이은 알약을 눌러 그 칸으로 이동(깜빡임) · 옆 판에서 고치면 알약 막대만 다시 만듦(시트 고침 유지)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NWHNLQYPZN7kKf1rzrrRZq
This commit is contained in:
2026-09-28 19:19:27 +09:00
co-authored by Claude Sonnet 5
parent 76bb9aec55
commit e06d43a6bc
5 changed files with 134 additions and 108 deletions
@@ -24,8 +24,17 @@ import {
type StructurePanelHandle,
} from "./M02_MasterTemplete_Structure";
import { buildComboBar } from "./M02_MasterTemplete_Combo";
import { buildMaterialPanel } from "./M02_MasterTemplete_Materials";
import {
buildMaterialPanel,
loadMaterialState,
toPills,
type MaterialPanel,
type MaterialState,
} from "./M02_MasterTemplete_Materials";
import type { SpreadsheetHandle } from "../A00_Common/spreadsheet/spreadsheet";
import type { LinkOptions } from "../A00_Common/spreadsheet/spreadsheet_links";
import { searchMaterials } from "../M01_MasterData/M01_MasterData_Api_Fetch";
import type { MaterialItem } from "./M02_MasterTemplete_Api_Fetch";
export interface Selection {
layer: Layer;
projectId: string | null;
@@ -110,6 +119,9 @@ export function createMain(isAdmin: boolean, onSaved?: () => void): MainHandle {
let editor: EditorHandle | null = null;
let sheet: SheetEditorHandle | null = null;
let panel: StructurePanelHandle | null = null;
let sheetHandle: SpreadsheetHandle | null = null;
let matPanel: MaterialPanel | null = null;
let matState: MaterialState | null = null;
let seq = 0;
let dirty = false;
@@ -144,6 +156,9 @@ export function createMain(isAdmin: boolean, onSaved?: () => void): MainHandle {
editor?.destroy();
editor = null;
sheet = null;
sheetHandle = null;
matPanel = null;
matState = null;
panel?.destroy();
panel = null;
extra.replaceChildren();
@@ -166,21 +181,36 @@ export function createMain(isAdmin: boolean, onSaved?: () => void): MainHandle {
}
/** 면적산출 · 재료산출 스프레드시트 — 위 알약 막대(sub7 `links`) · 면적 = 그 열 일위대가 입력값 ·
* 재료 = 자재DB 찾기(`pillsFor`) · 시스템 층은 면적 = 조합 줄 + [조합 고치기] · 재료 = 자재 목록 옆 판 */
* 재료 = 자재DB 찾기(`pillsFor`) · 시스템 층은 면적 = 조합 줄 + [조합 고치기] ·
* 재료 = 그 열 자재 조합 담은 자재(알약 항목 = 옆 판 표 = 같은 목록) + 옆 판
* (`pillItems` = 옆 판에서 고친 목록으로 알약 막대만 다시 만들 때) */
async function mountSheet(
doc: unknown,
kind: "area" | "material",
at: Selection,
readOnly: boolean,
pillItems?: MaterialItem[],
): Promise<void> {
const mine = seq;
try {
const master = at.layer === "system" && kind === "material";
if (master && !matState) matState = await loadMaterialState(at.name);
const [{ createSpreadsheet }, pills] = await Promise.all([
import("../A00_Common/spreadsheet/spreadsheet"),
pillsFor(kind, at.name),
master ? null : pillsFor(kind, at.name),
]);
if (mine !== seq) return;
if (pills.notice) showNotice([pills.notice]);
if (pills?.notice) showNotice([pills.notice]);
const links: LinkOptions =
pills?.links ??
({
항목: toPills(pillItems ?? matState?.combo?.담은자재 ?? []),
모드: "자재",
자재찾기: searchMaterials,
// 알약 막대에서 더하기 · 빼기 — 옆 판 표를 같은 목록으로
onItemsChange: (list) =>
matPanel?.setItems(list.flatMap((i) => (i.자재 ? [{ ...i.자재 }] : []))),
} satisfies LinkOptions);
const handle: SpreadsheetHandle = createSpreadsheet(
host,
doc as Parameters<typeof createSpreadsheet>[1],
@@ -188,16 +218,16 @@ export function createMain(isAdmin: boolean, onSaved?: () => void): MainHandle {
readOnly,
tabs: false, // 시트 하나 · 탭 줄 없음
onChange: () => (dirty = !readOnly),
links: pills.links,
links,
},
);
editor = handle;
sheetHandle = handle;
if (at.layer !== "system") return;
const label = at.label ?? at.name;
if (kind === "area") {
const bar = await buildComboBar({
column: at.name,
label,
label: at.label ?? at.name,
isAdmin,
beforeEdit: async () => true,
// 알약만 다시 받음 — 저장 안 한 시트 고침은 그대로 옮김
@@ -213,23 +243,38 @@ export function createMain(isAdmin: boolean, onSaved?: () => void): MainHandle {
if (mine === seq) comboSlot.replaceChildren(...(bar ? [bar] : []));
return;
}
const panel = await buildMaterialPanel({
if (matPanel || !matState) return;
matPanel = buildMaterialPanel({
column: at.name,
label,
label: at.label ?? at.name,
isAdmin,
links: () => handle.getDoc().시트.flatMap((s) => s.연결 ?? []),
state: matState,
links: () => sheetHandle?.getDoc().시트.flatMap((s) => s.연결 ?? []) ?? [],
// 옆 판 표에서 고침 — 알약 막대만 이 목록으로 다시 만듦(저장 안 한 시트 고침은 그대로)
onChange: (items) => {
const kept = sheetHandle?.getDoc();
if (!kept) return;
editor?.destroy();
editor = null;
void mountSheet(kept, kind, at, readOnly, items);
},
goTo: (code) =>
host
.querySelector<HTMLElement>(`.ss-link.is-linked[data-key="${CSS.escape(code)}"]`)
?.click(),
unlink: (codes) => {
const book = handle.getDoc();
const book = sheetHandle?.getDoc();
if (!book) return;
for (const s of book.시트) {
const rest = (s.연결 ?? []).filter((l) => !codes.includes(l.자재?.코드 ?? l.키));
if (rest.length) s.연결 = rest;
else delete s.연결;
}
handle.setDoc(book);
sheetHandle?.setDoc(book);
dirty = !readOnly;
},
});
if (mine === seq) host.append(panel);
host.append(matPanel.root);
} catch {
empty(L("M02_NoSpreadsheet"));
}