feat(m01-sl): 표 상자 열 차례를 서버 표 블록에 맞춤 — 값 칸 범위도 아래 · 위 두 칸 · 숫자 하나는 같은 수

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ACSGaHdLgnkEkoA4LDtMU
This commit is contained in:
2026-10-01 13:28:59 +09:00
co-authored by Claude Sonnet 5.5
parent 184c66e14e
commit 679a759e84
3 changed files with 57 additions and 19 deletions
+5 -1
View File
@@ -165,7 +165,11 @@ async function loadInfo(key: string): Promise<TblInfo> {
/* ── 역할 · 알약 ── */
const rolesOf = (node: SlNode): TblRoles => ({
cond: node.pillsIn.map((p) => p.name),
out: node.pillsOut.filter((p) => !p.outside).map((p) => p.name),
out: [
...new Set(
node.pillsOut.filter((p) => !p.outside).map((p) => p.name.replace(/·(아래|위)$/, "")),
),
],
});
/** 내 표 읽기 — 시트 블록에서 · 조건 칸은 입력 알약 이름 */
+19 -18
View File
@@ -77,17 +77,20 @@ const at = (r: number, c: number): string => `${colName(c)}${r}`;
const MARGIN = 20;
const quote = (s: string): string => `"${s.replace(/"/g, '""')}"`;
/** 블록 열 차례 — 조건 칸 · 값 칸 차례 그대로 · 범위 칸은 둘로 */
/** 범위 칸인가 — 조건 칸이 「범위」 이거나 값 칸에 범위(배열) 줄이 하나라도 있으면(서버 표 블록과 같은 규칙) */
export const isSplit = (info: TblInfo, c: string): boolean =>
info.conds[c] === "범위" || (c in info.vals && info.rows.some((r) => Array.isArray(r[c])));
/** 블록 열 차례 — 조건 칸 → 값 칸 · 범위 칸은 「이름·아래」 「이름·위」 둘로(서버 `_table` 과 같은 차례) */
export function layoutOf(info: TblInfo): TblCol[] {
const out: TblCol[] = [];
for (const c of Object.keys(info.conds))
if (info.conds[c] === "범위")
for (const c of [...Object.keys(info.conds), ...Object.keys(info.vals)])
if (isSplit(info, c))
out.push(
{ header: `${c}·아래`, of: c, part: "아래" },
{ header: `${c}·위`, of: c, part: "위" },
);
else out.push({ header: c, of: c, part: "값" });
for (const c of Object.keys(info.vals)) out.push({ header: c, of: c, part: "값" });
return out;
}
@@ -106,13 +109,8 @@ export function blockCells(info: TblInfo): Record<string, Cell> {
info.rows.forEach((row, i) =>
cols.forEach((col, c) => {
const v = row[col.of];
const x = Array.isArray(v)
? col.part === "아래"
? v[0]
: v[1]
: col.part === "값"
? v
: null;
// 범위 칸에 숫자 하나면 아래 · 위에 같은 수
const x = Array.isArray(v) ? (col.part === "아래" ? v[0] : v[1]) : v;
const cell = cellOf(x);
if (cell) cells[at(i + 2, c)] = cell;
}),
@@ -258,10 +256,11 @@ export function planLookup(info: TblInfo, roles: TblRoles): TblPlan {
);
const outCells: Record<string, string> = {};
for (const c of roles.out.filter((o) => o in info.vals)) {
outCells[c] = idxRow(
c,
`IF(${statusCell}=${quote("찾음")},INDEX(${rng(letter(c))},${idx}),NA())`,
);
for (const h of isSplit(info, c) ? [`${c}·아래`, `${c}·위`] : [c])
outCells[h] = idxRow(
h,
`IF(${statusCell}=${quote("찾음")},INDEX(${rng(letter(h))},${idx}),NA())`,
);
}
return {
cols,
@@ -286,7 +285,7 @@ export function pillsOf(
const pillsOut = Object.entries(plan.outCells).map(([name, cell]): SlPill => ({
name,
cell,
unit: info.vals[name] || undefined,
unit: info.vals[name.replace(/·(아래|위)$/, "")] || undefined,
}));
if (outside) pillsOut.push({ name: "표", cell: "A1", outside: true });
return { pillsIn, pillsOut };
@@ -416,8 +415,10 @@ export function infoFromSheet(sheet: Sheet, prev: Pick<TblInfo, "conds" | "name"
names.push(m ? { of: m[1], part: m[2] as "아래" | "위", c } : { of: h, part: "값", c });
});
for (const nm of names) {
if (nm.part !== "값") conds[nm.of] = "범위";
else if (prev.conds[nm.of]) conds[nm.of] = prev.conds[nm.of];
if (nm.part !== "값") {
if (prev.conds[nm.of]) conds[nm.of] = "범위";
else vals[nm.of] = "";
} else if (prev.conds[nm.of]) conds[nm.of] = prev.conds[nm.of];
else vals[nm.of] = "";
}
const rows: Rec[] = [];
@@ -197,3 +197,36 @@ def test_표가_비었어도_수식이_깨지지_않음():
empty = {**MINE, "rows": [{}, {}]}
r = solve(empty, {"경사": "급"}, ["할증"])
assert r["상태"] == "맞는 줄 없음" and r["할증"] == "#N/A"
def test_값_칸_범위도_아래_위_두_칸_숫자_하나는_같은_수():
"""서버 표 블록 차례 — 범위 값은 「이름·아래」 「이름·위」 · 숫자 하나면 두 칸이 같은 수."""
table = {
**R,
"conds": {"높이": "범위"},
"vals": {"증가율": "%"},
"rule": "초과·이하",
"rows": [{"높이": [3, 4], "증가율": [80, 100]}, {"높이": [4, None], "증가율": 30}],
}
low = solve(table, {"높이": 3.5}, ["증가율"])
assert float(low["증가율·아래"]) == 80 and float(low["증가율·위"]) == 100
one = solve(table, {"높이": 5}, ["증가율"])
assert float(one["증가율·아래"]) == 30 and float(one["증가율·위"]) == 30
def test_블록_머리_차례():
table = {
**R,
"conds": {"가": "고르기", "높이": "범위"},
"vals": {"율": "%", "폭": "m"},
"rows": [{"가": "a", "높이": [1, 2], "율": 1, "폭": [20, 45]}],
}
plan = run([{"op": "plan", "info": table, "roles": {"cond": ["가"], "out": ["율"]}}])[0]["plan"]
assert [c["header"] for c in plan["cols"]] == [
"가",
"높이·아래",
"높이·위",
"율",
"폭·아래",
"폭·위",
]