Merge remote-tracking branch 'origin/main_laptop_1' into sub_laptop_1
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;
|
||||
}
|
||||
|
||||
@@ -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)}`]?.값;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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("기준")}
|
||||
|
||||
|
||||
@@ -214,6 +214,9 @@ def test_가져온_시트로직_값이_calc_와_같음(row: dict, given: dict, m
|
||||
sheets = {s["id"] for s in sl["workbook"]["시트"]}
|
||||
assert len(sheets) == len(sl["nodes"]) and all(n["sheetId"] in sheets for n in sl["nodes"])
|
||||
ids = {n["id"]: n for n in sl["nodes"]}
|
||||
for n in sl["nodes"]: # 표 상자 = 「표」 바깥 값 출력 알약(E1 · Table 상자와 같은 꼴)
|
||||
if n["kind"] == "table":
|
||||
assert {"name": "표", "cell": "E1", "outside": True} in n["pillsOut"]
|
||||
for e in sl["edges"]:
|
||||
assert any(p["name"] == e["from"]["pill"] for p in ids[e["from"]["node"]]["pillsOut"])
|
||||
assert any(p["name"] == e["to"]["pill"] for p in ids[e["to"]["node"]]["pillsIn"])
|
||||
|
||||
@@ -222,7 +222,7 @@ def test_바깥값_칸목록과_표_덩어리() -> None:
|
||||
]
|
||||
assert (e1, f2, e3, f3) == ({"값": "뒷길이"}, {"값": 0.15}, {"값": 30}, None)
|
||||
assert lg == {"결과": 12.5}
|
||||
assert "「메쌓기 표」의 입력 알약 「조건」이 비어 있음" in check
|
||||
assert check == []
|
||||
|
||||
|
||||
def test_알약_정하기_이름바꾸면_선이_따라감() -> None:
|
||||
@@ -361,3 +361,53 @@ def test_항목_틀은_Item_노무_틀과_같은_칸() -> None:
|
||||
["단가", "B5"], ["금액", "B6"], ["노무비", "B7"], ["재료비", "B8"], ["경비", "B9"], ["사용 수량", "B10"],
|
||||
] # fmt: skip
|
||||
assert ref == '{"what":"노무"}'
|
||||
|
||||
|
||||
# 표 상자 이음매 — Table_Build(sub3) 가 짠 품셈 표 상자에 서버 꼴 블록(머리 줄 + 줄 · A1 부터)을 넣고 찾기
|
||||
TABLE = r"""
|
||||
const T = loadTs(path.join(ROOT, "M01_MasterData", "SheetLogic", "M01_SL_Table_Build.ts"));
|
||||
let sl = M.newLogic("표 시험");
|
||||
let r = M.addNode(sl, "table", "메쌓기"); sl = r.sl; const tb = r.node;
|
||||
const info = { key: "QF000421", name: "메쌓기", base: "1㎡", source: "", no: "13-4-1",
|
||||
conds: { 뒷길이: "수", 돌: "고르기" }, vals: { 석공: "인" }, rule: null, notes: [],
|
||||
rows: [{ 뒷길이: 25, 돌: "깬잡석", 석공: 0.15 }, { 뒷길이: 30, 돌: "깬돌", 석공: 0.26 },
|
||||
{ 뒷길이: 35, 돌: "깬돌", 석공: 0.3 }] };
|
||||
sl = T.applyTable(sl, tb.id, info, { cond: ["뒷길이", "돌"], out: ["석공"] },
|
||||
{ ref: "QF000421", fillBlock: false, outside: true }).sl;
|
||||
r = M.addNode(sl, "input", "뒷길이"); sl = r.sl; const len = r.node;
|
||||
r = M.addNode(sl, "input", "돌"); sl = r.sl; const stone = r.node;
|
||||
sl = E.applyOutside(sl, [{ node: len.id, cell: "B2", value: 35 }, { node: stone.id, cell: "B2", value: "깬돌" }]);
|
||||
sl = link(sl, len, "뒷길이", sl.nodes.find(n => n.id === tb.id), "뒷길이");
|
||||
sl = link(sl, stone, "돌", sl.nodes.find(n => n.id === tb.id), "돌");
|
||||
const ROWS = [["뒷길이", "돌", "석공"], [25, "깬잡석", 0.15], [30, "깬돌", 0.26], [35, "깬돌", 0.3]];
|
||||
const cells = E.outsideCells(sl);
|
||||
"""
|
||||
|
||||
|
||||
def test_표_상자_바깥값_블록_찾기_줄면_옛줄_지움_머리바뀜_알림() -> None:
|
||||
got = _run(
|
||||
TABLE
|
||||
+ "const first = E.answersToValues(cells, [{ id: cells[0].id, ok: true, rows: ROWS }], sl);\n"
|
||||
"const s1 = E.applyOutside(sl, first.values);\n"
|
||||
"const shrink = E.answersToValues(cells, [{ id: cells[0].id, ok: true, rows: ROWS.slice(0, 3) }], s1);\n"
|
||||
"const s2 = E.applyOutside(s1, shrink.values);\n"
|
||||
"const moved = E.answersToValues(cells, [{ id: cells[0].id, ok: true,"
|
||||
" rows: [['돌', '뒷길이', '석공'], ['깬돌', 30, 0.26]] }], s1);\n"
|
||||
"out([cells, first.errors, E.evalLogic(s1).pills[tb.id], shrink.errors, sheet(s2, tb.id).칸.A4 ?? null,"
|
||||
" sheet(s2, tb.id).칸.C4 ?? null, E.evalLogic(s2).pills[tb.id], moved.errors]);"
|
||||
)
|
||||
cells, first_err, found, shrink_err, a4, c4, gone, moved_err = got
|
||||
assert cells == [
|
||||
{
|
||||
"id": "n1!A1",
|
||||
"node": "n1",
|
||||
"cell": "A1",
|
||||
"kind": "table",
|
||||
"ref": "QF000421",
|
||||
"source": {"kind": "표", "table": "QF000421"},
|
||||
}
|
||||
]
|
||||
assert first_err == [] and found["석공"] == 0.3 and found["표"] == "뒷길이"
|
||||
assert shrink_err == [] and a4 is None and c4 is None
|
||||
assert gone["석공"] == "#N/A"
|
||||
assert moved_err == ["「표 · 메쌓기」 표 머리 줄이 바뀜 — 표를 다시 골라야 함"]
|
||||
|
||||
@@ -36,10 +36,10 @@ TABLE = {
|
||||
"출처": "자체",
|
||||
"조건": {"돌": "고르기", "높이": "범위"},
|
||||
"범위규칙": "초과·이하",
|
||||
"값칸": {"석공": "인"},
|
||||
"값칸": {"석공": "인", "증가율": "%"},
|
||||
"줄": [
|
||||
{"돌": "깬돌", "높이": [None, 3], "석공": 0.5},
|
||||
{"돌": "깬돌", "높이": [3, None], "석공": 0.7},
|
||||
{"돌": "깬돌", "높이": [None, 3], "석공": 0.5, "증가율": [80, 100]},
|
||||
{"돌": "견치돌", "높이": 4, "석공": 0.7},
|
||||
],
|
||||
"주": [],
|
||||
}
|
||||
@@ -158,23 +158,23 @@ def test_저장_열기_목록_지우기_서버가_다시_셈(client: TestClient,
|
||||
assert Decimal(book["계산값"]["s1"]["B1"]["수"]) == 2 * price # 서버 다시 셈(999 는 버림)
|
||||
assert "C1" not in cells # 출처 없는 바깥 알약은 안 넣음
|
||||
table = book["시트"][1]["칸"]
|
||||
assert [table[c]["값"] for c in ("A1", "B1", "C1", "D1")] == [
|
||||
"돌",
|
||||
"높이·아래",
|
||||
"높이·위",
|
||||
"석공",
|
||||
]
|
||||
assert (
|
||||
"B2" not in table and table["C2"]["값"] == 3 and table["D3"]["값"] == 0.7
|
||||
) # 빈 끝은 빈 칸
|
||||
head = [table[c]["값"] for c in ("A1", "B1", "C1", "D1", "E1")]
|
||||
assert head == ["돌", "높이·아래", "높이·위", "석공", "증가율"] # 머리 줄 · A1 부터
|
||||
assert "B2" not in table and table["C2"]["값"] == 3 and table["E2"]["값"] == "80,100"
|
||||
assert "B3" not in table and "C3" not in table and table["D3"]["값"] == 0.7 # 범위 아님 → 빈 칸
|
||||
assert saved["logic"]["edges"] == _logic(labor)["edges"]
|
||||
answers = {v["id"]: v for v in saved["values"]}
|
||||
assert set(answers) == {"n1!A1", "n2!A1", "n1:노무비"} and all(
|
||||
v["ok"] for v in answers.values()
|
||||
)
|
||||
|
||||
second = _post(client, "/api/m01/sl/save", {"logic": _logic(labor)})
|
||||
# 블록 왼위 = 표 상자 「표」 바깥 알약 칸(고정 A1 아님 — Import 는 E1)
|
||||
at_e1 = _logic(labor)
|
||||
at_e1["nodes"][1]["pillsOut"][0]["cell"] = "E1"
|
||||
second = _post(client, "/api/m01/sl/save", {"logic": at_e1})
|
||||
assert second["key"] == "SL-0002"
|
||||
moved = second["logic"]["workbook"]["시트"][1]["칸"]
|
||||
assert moved["E1"]["값"] == "돌" and moved["I2"]["값"] == "80,100" and "A1" not in moved
|
||||
renamed = {**saved["logic"], "name": "고친 이름"}
|
||||
assert _post(client, "/api/m01/sl/save", {"logic": renamed})["key"] == "SL-0001"
|
||||
|
||||
@@ -221,8 +221,17 @@ def test_바깥_값_표_노임_로직_틀린_종류(client: TestClient) -> None:
|
||||
{"id": "z", "kind": "모름"},
|
||||
]
|
||||
got = {v["id"]: v for v in _post(client, "/api/m01/sl/outside", {"items": items})["values"]}
|
||||
assert got["t"]["rows"][0] == ["돌", "높이·아래", "높이·위", "석공"]
|
||||
assert got["t"]["rows"][1:] == [["깬돌", None, 3, 0.5], ["깬돌", 3, None, 0.7]]
|
||||
assert got["t"]["rows"][0] == [
|
||||
"돌",
|
||||
"높이·아래",
|
||||
"높이·위",
|
||||
"석공",
|
||||
"증가율",
|
||||
] # sub3 layoutOf 차례
|
||||
assert got["t"]["rows"][1:] == [
|
||||
["깬돌", None, 3, 0.5, "80,100"],
|
||||
["견치돌", None, None, 0.7, None],
|
||||
]
|
||||
assert Decimal(str(got["l"]["value"])) == price and got["l"]["unit"]
|
||||
assert Decimal(str(got["g"]["value"])) == Decimal("0.5") * price
|
||||
assert got["g2"]["value"] == 0
|
||||
|
||||
Reference in New Issue
Block a user