Merge remote-tracking branch 'origin/main_laptop_1' into sub_laptop_1

This commit is contained in:
2026-10-01 13:27:48 +09:00
7 changed files with 139 additions and 45 deletions
+33
View File
@@ -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;
}
+3 -2
View File
@@ -384,7 +384,8 @@ function newTable(b: Build, n: Extract<Ast, { t: "find" }>, sig: string): TableB
const cols = tableCols(table);
const rows = tableRows(table);
b.row(node, t("Table"), { 값: n.head }, t("Outside"));
b.pill(node, "in", { name: t("Table"), cell: "E1", outside: true });
// 바깥 값 「표」 알약 — Table 상자와 같은 꼴(출력 · 블록 왼위 · 머리 줄 포함) · Eval · 서버가 이 칸에서 블록을 채움
b.pill(node, "out", { name: "표", cell: "E1", outside: true });
rows.forEach((r, i) =>
r.forEach((v, j) => {
const c = cellOf(v);
@@ -653,7 +654,7 @@ export function outsideOf(sl: SheetLogic, calc?: CalcValues): SlOutsideAt[] {
const out: SlOutsideAt[] = [];
for (const node of sl.nodes) {
const passed: Record<string, unknown> = {};
const outside = node.pillsIn.filter((p) => p.outside);
const outside = [...node.pillsIn, ...node.pillsOut].filter((p) => p.outside);
const sheet = sl.workbook.시트.find((s) => s.id === node.sheetId);
for (const p of node.pillsIn) {
const note = sheet?.칸[`C${p.cell.slice(1)}`]?.값;
+9 -15
View File
@@ -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;
+16 -12
View File
@@ -161,7 +161,10 @@ def _logic(key: str, inputs: dict, field: str | None) -> dict:
def _table(key: str) -> dict:
"""품셈 표 통째 → 머리 줄 + 줄(범위 · 범위 값은 「이름·아래」 「이름·위」 두 칸)."""
"""품셈 표 통째 → 머리 줄 + 줄 — `M01_SL_Table_Build.layoutOf · blockCells` 와 같은 열 차례.
조건 칸(범위 조건은 「이름·아래」 「이름·위」 두 칸) → 값 칸(늘 한 칸 · 범위 값은 「아래,위」).
"""
files = store.loaded()[0]
table = next(
(
@@ -175,24 +178,25 @@ def _table(key: str) -> dict:
)
if table is None:
return {"ok": False, "reason": f"없는 표 「{key}」"}
names = [*(table.get("조건") or {}), *(table.get("값칸") or {})]
split = {
n
for n in names
if (table.get("조건") or {}).get(n) == "범위"
or any(isinstance(r.get(n), list) for r in table.get("줄") or [])
}
conds = table.get("조건") or {}
names = [*conds, *(table.get("값칸") or {})]
split = {n for n in conds if conds[n] == "범위"}
head = [h for n in names for h in ((n + RANGE_LOW, n + RANGE_HIGH) if n in split else (n,))]
def plain(v: Any) -> Any: # JS String(배열) 과 같게 — 「80,100」 · 빈 끝은 빈 글
if isinstance(v, list):
return ",".join("" if x is None else str(x) for x in v)
return None if v == "" else v
rows = []
for r in table.get("줄") or []:
line = []
for n in names:
v = r.get(n)
if n in split:
low, high = v if isinstance(v, list) else (v, v)
line += [low, high]
if n in split: # 범위 칸 — 목록이 아니면 둘 다 빈 칸
line += list(v) if isinstance(v, list) else [None, None]
else:
line.append(v if not isinstance(v, (list, dict)) else str(v))
line.append(plain(v))
rows.append(line)
return {"ok": True, "rows": [head, *rows], "name": table.get("이름"), "unit": table.get("기준")}