feat(m01-sl): 표 상자 이음매 — 블록은 표 알약 칸부터 · 줄면 옛 줄 지움 · 머리 줄 바뀜 알림
- 표 블록 왼위 = 그 표 상자의 「표」 바깥 값 알약 칸(A1 · E1 무엇이든) · applyOutside 가 그 칸부터 머리 줄 포함 채움 - 표가 줄었으면 블록 열 아래 남은 옛 값 칸을 지움(찾기 판 수식은 블록 열 밖이라 그대로) - answersToValues(cells, 답, sl) — 시트 머리 줄과 받은 머리 줄이 다르면 쉬운 말 - addNode(table) = 빈 시트(Table 이 블록 · 찾기 판 · 알약을 채움 — 틀 칸이 블록과 겹치지 않게) - 시험 15개(Table_Build 와 잇는 시험 더함) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VBToTCGk5dqDKeXCEP3vhk
This commit is contained in:
@@ -54,6 +54,11 @@ const TEXT = {
|
||||
"Outside value 「{pill}」 of 「{box}」 not filled yet",
|
||||
],
|
||||
NoResult: ["결과 상자가 없음", "There is no result box"],
|
||||
/** {box} */
|
||||
HeadChanged: [
|
||||
"「{box}」 표 머리 줄이 바뀜 — 표를 다시 골라야 함",
|
||||
"Table headers of 「{box}」 changed — choose the table again",
|
||||
],
|
||||
/** {cell} {why} */
|
||||
NoAnswer: [
|
||||
"{cell} 칸 바깥 값을 못 받음 {why}",
|
||||
@@ -102,6 +107,7 @@ export interface SlOutsideAnswer {
|
||||
export function answersToValues(
|
||||
cells: SlOutside[],
|
||||
answers: SlOutsideAnswer[],
|
||||
sl?: SheetLogic,
|
||||
): { values: SlOutsideValue[]; errors: string[] } {
|
||||
const values: SlOutsideValue[] = [];
|
||||
const errors: string[] = [];
|
||||
@@ -111,11 +117,29 @@ export function answersToValues(
|
||||
errors.push(tl("NoAnswer", { cell: c.cell, why: a?.reason ?? "" }));
|
||||
continue;
|
||||
}
|
||||
const head = sl && a.rows ? headChanged(sl, c, a.rows[0] ?? []) : null;
|
||||
if (head) errors.push(head);
|
||||
values.push({ node: c.node, cell: c.cell, value: a.rows ?? a.value ?? null });
|
||||
}
|
||||
return { values, errors };
|
||||
}
|
||||
|
||||
/** 표 블록 머리 줄이 시트에 있던 것과 다르면 쉬운 말(열 차례가 바뀌면 찾기 판이 엉뚱한 열을 봄) */
|
||||
function headChanged(
|
||||
sl: SheetLogic,
|
||||
c: SlOutside,
|
||||
head: (string | number | null)[],
|
||||
): string | null {
|
||||
const sheet = nodeSheet(sl, c.node);
|
||||
const at = parseA1(c.cell);
|
||||
if (!sheet || !at) return null;
|
||||
const now = head.map((_, dc) => sheet.칸[toA1(at.r, at.c + dc)]?.값 ?? null);
|
||||
if (now.every((x) => x === null)) return null; // 처음 채움
|
||||
const same = head.length === now.length && head.every((h, i) => h === now[i]);
|
||||
const box = sl.nodes.find((n) => n.id === c.node)?.name ?? "";
|
||||
return same ? null : tl("HeadChanged", { box });
|
||||
}
|
||||
|
||||
/** 칸 하나에 값 — null 이면 값을 뺌(서식은 둠) */
|
||||
function putValue(칸: Record<string, Cell>, cell: string, value: SlValue): void {
|
||||
const style = 칸[cell]?.서식;
|
||||
@@ -139,6 +163,15 @@ export function applyOutside(sl: SheetLogic, values: SlOutsideValue[]): SheetLog
|
||||
v.value.forEach((line, dr) =>
|
||||
line.forEach((x, dc) => putValue(sheet.칸, toA1(at.r + dr, at.c + dc), x)),
|
||||
);
|
||||
// 표가 줄었으면 블록 열 아래 남은 옛 줄(값 칸)을 지움 — 수식 칸(찾기 판 · 열쇠)은 블록 열 밖이라 안 건드림
|
||||
const width = Math.max(0, ...v.value.map((line) => line.length));
|
||||
const last = at.r + v.value.length - 1;
|
||||
for (const a of Object.keys(sheet.칸)) {
|
||||
const p = parseA1(a);
|
||||
const c = sheet.칸[a];
|
||||
if (p && p.r > last && p.c >= at.c && p.c < at.c + width && c.식 === undefined)
|
||||
putValue(sheet.칸, a, null);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -52,7 +52,8 @@ export interface SlEdge {
|
||||
export type SlField = "노무비" | "재료비" | "경비" | "계";
|
||||
|
||||
/** 바깥 값 출처 — 서버 `/api/m01/sl/outside` 의 물음 한 줄(id 뺌) 그대로
|
||||
* · 표 = 품셈 표 통째(머리 줄 + 줄 · 범위 칸은 「이름·아래」 「이름·위」 두 칸 — `rangeHead`)
|
||||
* · 표 = 품셈 표 통째(머리 줄 + 줄 · 열 차례 조건 칸 → 값 칸 · 범위 칸은 「이름·아래」 「이름·위」 두 칸 — `rangeHead`)
|
||||
* 블록 왼위 = 그 표 상자의 「표」 바깥 값 알약 칸(pillsOut · outside) — 고정 A1 아님 · Eval 이 그 칸부터 채움
|
||||
* · 노임 · 자재 = 그 줄 단가 · 장비 = 기계경비 로직 부름 글 + 넘길 값 · 로직 = 기존 로직 결과 */
|
||||
export type SlSource =
|
||||
| { kind: "표"; table: string }
|
||||
@@ -107,7 +108,6 @@ const TEXT = {
|
||||
Fixed: ["늘 이 값", "Always this value"],
|
||||
Cond: ["조건", "Condition"],
|
||||
Found: ["결과", "Result"],
|
||||
FromTable: ["표에서 찾은 값", "Value found in the table"],
|
||||
ValueA: ["값1", "Value 1"],
|
||||
ValueB: ["값2", "Value 2"],
|
||||
Times: ["값1 × 값2", "Value 1 × Value 2"],
|
||||
@@ -216,14 +216,8 @@ function template(kind: SlKind, name: string): { rows: Row[]; ins: SlPill[]; out
|
||||
case "const":
|
||||
return { rows: [[name, v(0), tl("Fixed")]], ins: [], outs: [{ name, cell: "B2" }] };
|
||||
case "table":
|
||||
return {
|
||||
rows: [
|
||||
[tl("Cond"), null, tl("QtyNote")],
|
||||
[tl("Found"), null, tl("FromTable")],
|
||||
],
|
||||
ins: [{ name: tl("Cond"), cell: "B2" }],
|
||||
outs: [{ name: tl("Found"), cell: "B3" }],
|
||||
};
|
||||
// 빈 시트 — 표를 고르면 Table(sub3)이 A1 블록 · 찾기 판 · 알약을 채움(머리 줄이 표 블록과 겹치지 않게)
|
||||
return { rows: [], ins: [], outs: [] };
|
||||
case "calc":
|
||||
return {
|
||||
rows: [
|
||||
@@ -335,11 +329,11 @@ export function addNode(
|
||||
const title = uniqueName(book, name);
|
||||
const item = kind === "item" ? itemSheet() : null;
|
||||
const t = item ?? template(kind, title);
|
||||
const 칸: Record<string, Cell> = item?.cells ?? {
|
||||
A1: { 값: tl("Name") },
|
||||
B1: { 값: tl("Value") },
|
||||
C1: { 값: tl("Note") },
|
||||
};
|
||||
const 칸: Record<string, Cell> =
|
||||
item?.cells ??
|
||||
(kind === "table"
|
||||
? {}
|
||||
: { A1: { 값: tl("Name") }, B1: { 값: tl("Value") }, C1: { 값: tl("Note") } });
|
||||
t.rows.forEach(([label, cell, note], i) => {
|
||||
칸[`A${i + 2}`] = { 값: label };
|
||||
if (cell) 칸[`B${i + 2}`] = cell;
|
||||
|
||||
Reference in New Issue
Block a user