feat(815-2): M01 요소 표 「별칭」 한 열 · 마스터 기계 별칭 다섯 옮김

- 인력 · 기계 · 재료 자재 · 유가전력 표에 비고 앞 「별칭」 열 — 눌러 고침 · 콤마로 여러 개 · 빈 글이면 칸 없음
- 저장 길 = 그대로 `/api/m01/save`(서버가 앞뒤 빈칸 뗌 · 빈 글 = 칸 지움 · 그 줄만 다시 씀)
- check_master — 요소 줄 열 한 벌 밖 덧칸 `별칭` 허용(빈 글 · 글 아님은 흠)
- 마스터 기계 별칭 다섯(맞춤.json) → 기계 줄 15 개 `별칭` · 옛 마스터 장부 파일 걷음
- M01 로직 1,513 셈 지문 전후 같음
- 새 시험 test_815_2_alias_column.py

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-11 14:20:09 +09:00
co-authored by Claude Opus 5.5
parent afb3fcff8d
commit 4549e28fa5
6 changed files with 179 additions and 165 deletions
@@ -283,6 +283,12 @@ def save(batch: list[dict], confirmed: bool = False) -> dict:
"""`/save` — 로직 정본 줄 고침은 `confirmed`(「원문 개정 반영」) 없으면 409(`Store_Revise`) ·
자체 파일 새 줄(키 없음)은 서버가 칸을 잡고 GX 다음 키 — {files, added, warnings}."""
revise.guard(batch, confirmed)
for part in batch: # 별칭(815-2) = 글 그대로 · 앞뒤 빈칸만 뗌 · 빈 글이면 칸 없음
for ch in part.get("changes") or []:
row = ch.get("row")
if isinstance(row, dict) and "별칭" in row:
text = str(row["별칭"] or "").strip()
row.update(별칭=text) if text else row.pop("별칭")
fresh = False
for part in batch:
if part.get("file") != OWN:
+28 -21
View File
@@ -37,6 +37,9 @@ const MARKET = "재료_자재품목.json";
const LINKED = "재료_품셈재료.json";
const JOB = ["인력.json"];
const NOTE = "비고"; // 끝 열(인력은 준용 앞) · 줄에 없으면 빈 칸 · 눌러 적음
// 별칭(815-2 · 요구 764) = 비고 앞 한 열 · 콤마로 여러 개 · 빈 글이면 칸을 지움 · 이 넷만
const ALIAS = "별칭";
const ALIASED = ["인력.json", "기계.json", MARKET, "재료_유가전력.json"];
const FALLBACK: Row = { 키: "", 원문번호: "", 이름: "", 규격: "", 단위: "", 값: null, 출처: "" };
/** 새 줄 바탕 — 본 줄과 같은 칸 · 글 칸은 "" · 수·값 칸은 null. */
@@ -44,16 +47,16 @@ function blank(sample: Row): Row {
const out: Row = {};
for (const [k, v] of Object.entries(sample)) {
if (v && typeof v === "object" && !Array.isArray(v)) out[k] = blank(v as Row);
else out[k] = typeof v === "string" && k !== "값" ? "" : null;
else if (k !== ALIAS) out[k] = typeof v === "string" && k !== "값" ? "" : null;
}
return out;
}
/** 비고 칸 고침 — 빈 글이면 칸을 지움 */
function withNote(row: Row, text: string): Row {
/** 비고 · 별칭 칸 고침 — 빈 글이면 칸을 지움(별칭은 앞뒤 빈칸 뗌) */
function withNote(row: Row, text: string, col = NOTE): Row {
const out = { ...row };
if (text.trim()) out[NOTE] = text;
else delete out[NOTE];
if (text.trim()) out[col] = col === ALIAS ? text.trim() : text;
else delete out[col];
return out;
}
@@ -117,8 +120,9 @@ export function renderRows(
const cols: string[] = [];
for (const row of [...adds, ...d.rows]) {
for (const k of Object.keys(flatten(row)))
if (!hidden(file, k) && k !== NOTE && !cols.includes(k)) cols.push(k);
if (!hidden(file, k) && k !== NOTE && k !== ALIAS && !cols.includes(k)) cols.push(k);
}
const aliased = ALIASED.includes(file);
const heads = (HEADS[file] ?? []).map((h) =>
el("th", { text: h.startsWith("M01_") ? L(h as "M01_PriceCol") : h }),
);
@@ -127,6 +131,7 @@ export function renderRows(
el("th"),
...cols.map((c) => el("th", { text: c })),
...(JOB.includes(file) ? [] : heads),
...(aliased ? [el("th", { text: ALIAS })] : []),
el("th", { text: NOTE }),
...(JOB.includes(file) ? heads : []),
],
@@ -145,12 +150,13 @@ export function renderRows(
}),
);
}
tr.append(
buildCell(show(row[NOTE]), {
changed: true,
onEdit: (text) => setAdd(file, i, withNote(row, text)),
}),
);
for (const col of aliased ? [ALIAS, NOTE] : [NOTE])
tr.append(
buildCell(show(row[col]), {
changed: true,
onEdit: (text) => setAdd(file, i, withNote(row, text, col)),
}),
);
if (JOB.includes(file)) tr.append(el("td"));
body.append(tr);
});
@@ -186,15 +192,16 @@ export function renderRows(
? undefined
: (next: Row): void => editRow(file, d.version, key, row, next);
if (file === LINKED) tr.append(linkedCell(row, cur, put));
tr.append(
buildCell(show(cur[NOTE]), {
changed: !same(cur[NOTE], row[NOTE]),
onEdit:
deleted || !isScalar(row[NOTE])
? undefined
: (text) => editRow(file, d.version, key, row, withNote(cur, text)),
}),
);
for (const col of aliased ? [ALIAS, NOTE] : [NOTE])
tr.append(
buildCell(show(cur[col]), {
changed: !same(cur[col], row[col]),
onEdit:
deleted || !isScalar(row[col])
? undefined
: (text) => editRow(file, d.version, key, row, withNote(cur, text, col)),
}),
);
if (JOB.includes(file)) tr.append(wageCell(row, cur, put));
body.append(tr);
}