diff --git a/A00_Common/spreadsheet/spreadsheet_text.ts b/A00_Common/spreadsheet/spreadsheet_text.ts index 9291c67a6..a5798a4ad 100644 --- a/A00_Common/spreadsheet/spreadsheet_text.ts +++ b/A00_Common/spreadsheet/spreadsheet_text.ts @@ -134,6 +134,7 @@ const TEXT = { UnitGroup_op: ["연산·비교", "Operators"], UnitRecent: ["최근 씀", "Recently used"], UnitAsFormat: ["숫자 형식으로 붙이기", "Attach as number format"], + UnitPhiTitle: ["지름(Φ) — 숫자 앞에 붙음", "Diameter (Φ) — attached before the number"], FormulaHint_label: [ "엑셀 수식까지: 엑셀에서 Ctrl+` → 복사 → 여기 Ctrl+V · 끝나면 엑셀 Ctrl+` 다시", "Formulas too: in Excel Ctrl+` → copy → paste here (Ctrl+V) · then Ctrl+` again in Excel", diff --git a/A00_Common/spreadsheet/spreadsheet_toolbar.ts b/A00_Common/spreadsheet/spreadsheet_toolbar.ts index 5ff071829..a6e38dd9a 100644 --- a/A00_Common/spreadsheet/spreadsheet_toolbar.ts +++ b/A00_Common/spreadsheet/spreadsheet_toolbar.ts @@ -62,8 +62,11 @@ const BORDER_LINES: BorderLine[] = [ type TextKey = Parameters[0]; -const UNIT_GROUPS: [label: TextKey, items: string[]][] = [ - ["UnitGroup_area", ["㎡", "㎥", "m", "㎝", "㎜", "㎞"]], +/** 지름 기호 — 숫자 앞에 붙임(81-5 · 자재 규격에서 압도적 · Φ300). */ +export const PHI_UNIT = "Φ"; + +export const UNIT_GROUPS: [label: TextKey, items: string[]][] = [ + ["UnitGroup_area", ["㎡", "㎥", "m", "㎝", "㎜", "㎞", PHI_UNIT]], ["UnitGroup_weight", ["kg", "t", "ℓ", "㎖"]], ["UnitGroup_count", ["개", "본", "식", "EA", "매"]], ["UnitGroup_rate", ["m/s", "°", "%", "‰"]], @@ -92,11 +95,17 @@ function pushRecentUnit(unit: string): string[] { } /** 숫자 형식 코드 끝 따옴표 글자(전에 붙인 단위)를 떼고 새 단위를 붙임 — 없으면 "0" 에서 시작. */ -function attachUnitToFormat(code: string, unit: string): string { +export function attachUnitToFormat(code: string, unit: string): string { const base = (code || "0").replace(/"[^"]*"\s*$/, ""); return base + `"${unit}"`; } +/** Φ(지름 기호) 는 숫자 앞 — 서식 코드 머리의 글자를 바꿔 끼움(81-5). */ +export function prependUnitToFormat(code: string, unit: string): string { + const base = (code || "0").replace(/^\s*"[^"]*"/, ""); + return `"${unit}"` + base; +} + /** 활성 칸에 지금 걸린 서식 — 칸 자기 것 없으면 열 · 행 · 시트 기본(`Sheet.기본.서식`) · 그다음 0. */ function styleAt(ctx: SpreadsheetContext, r: number, c: number): CellStyle { const sheet = ctx.sheet(); @@ -407,12 +416,22 @@ export function mountToolbar(ctx: SpreadsheetContext): PartHandle { } } else if (unitAsFormat.checked) { const { r, c } = ctx.selection.활성; - patch({ 형식: attachUnitToFormat(styleAt(ctx, r, c).형식 ?? "", unit) }); + const 형식 = styleAt(ctx, r, c).형식 ?? ""; + patch({ + 형식: unit === PHI_UNIT ? prependUnitToFormat(형식, unit) : attachUnitToFormat(형식, unit), + }); } else { const { r, c } = ctx.selection.활성; const sheet = ctx.sheet(); const key = toA1(r, c); - const text = cellToText(sheet.칸[key]) + unit; + const before = cellToText(sheet.칸[key]); + // Φ 는 글 앞 — 강제 텍스트 홑낫표(')가 있으면 그 뒤에(숫자 앞) 끼워 넣음(81-5) + const text = + unit === PHI_UNIT + ? before[0] === "'" + ? "'" + unit + before.slice(1) + : unit + before + : before + unit; ctx.dispatch({ 종류: "칸", 시트: sheet.id, 칸: { [key]: textToCell(text, sheet.칸[key]) } }); } renderRecent(pushRecentUnit(unit)); @@ -436,6 +455,7 @@ export function mountToolbar(ctx: SpreadsheetContext): PartHandle { for (const u of items) { const b = createButton({ label: u, variant: "ghost", onClick: () => insertUnit(u) }); b.className += " ss-toolbar__unit-btn"; + if (u === PHI_UNIT) b.title = st("UnitPhiTitle"); grid.append(b); } unitPop.append( diff --git a/resources/tester/spreadsheet/spreadsheet_toolbar_unit_entry.ts b/resources/tester/spreadsheet/spreadsheet_toolbar_unit_entry.ts new file mode 100644 index 000000000..18e2f66c1 --- /dev/null +++ b/resources/tester/spreadsheet/spreadsheet_toolbar_unit_entry.ts @@ -0,0 +1,36 @@ +/* 단위 Φ(81-5) 순수 길 시험 진입 — vite --ssr 로 번들해 Node 로 돌림. + * 결과 = JSON {pass, fail, failures} 한 줄(`test_spreadsheet_toolbar_unit.py` 가 읽음) */ +import { + attachUnitToFormat, + PHI_UNIT, + prependUnitToFormat, + UNIT_GROUPS, +} from "../../../A00_Common/spreadsheet/spreadsheet_toolbar"; + +let pass = 0; +const failures: string[] = []; +const eq = (name: string, got: unknown, want: unknown): void => { + if (JSON.stringify(got) === JSON.stringify(want)) pass += 1; + else failures.push(`${name}: ${JSON.stringify(got)} ≠ ${JSON.stringify(want)}`); +}; + +// Φ 는 길이 묶음(UnitGroup_area) 끝 — 다른 묶음엔 안 들어감 +const lengthGroup = UNIT_GROUPS.find(([label]) => label === "UnitGroup_area")!; +eq("Φ = 길이 묶음 끝", lengthGroup[1][lengthGroup[1].length - 1], PHI_UNIT); +eq( + "Φ 는 다른 묶음에 없음", + UNIT_GROUPS.filter(([label]) => label !== "UnitGroup_area").some((g) => g[1].includes(PHI_UNIT)), + false, +); + +// 보통 단위(뒤에 붙음) — 지금처럼 +eq("보통 단위 서식 — 뒤", attachUnitToFormat("0", "㎡"), '0"㎡"'); +eq("보통 단위 서식 — 옛 단위 바꿈", attachUnitToFormat('0"㎡"', "㎝"), '0"㎝"'); + +// Φ(지름) — 숫자 앞 +eq("Φ 서식 — 앞", prependUnitToFormat("0", PHI_UNIT), '"Φ"0'); +eq("Φ 서식 — 빈 코드도 0 기준", prependUnitToFormat("", PHI_UNIT), '"Φ"0'); +eq("Φ 서식 — 옛 Φ 바꿈(겹쳐 안 붙음)", prependUnitToFormat('"Φ"0', PHI_UNIT), '"Φ"0'); +eq("Φ 서식 — 뒤 단위와 공존", prependUnitToFormat('0"㎡"', PHI_UNIT), '"Φ"0"㎡"'); + +console.log(JSON.stringify({ pass, fail: failures.length, failures })); diff --git a/resources/tester/test_spreadsheet_toolbar_unit.py b/resources/tester/test_spreadsheet_toolbar_unit.py new file mode 100644 index 000000000..ca727888f --- /dev/null +++ b/resources/tester/test_spreadsheet_toolbar_unit.py @@ -0,0 +1,57 @@ +"""단위 Φ(81-5) — `spreadsheet_toolbar.ts` UNIT_GROUPS 길이 묶음 끝에 Φ · 숫자 앞에 붙임. + +순수 길(`attachUnitToFormat` · `prependUnitToFormat` · `UNIT_GROUPS`)을 vite `--ssr` 로 +번들해 Node 로 돌림. 화면(단위 ▾ 팝오버 · 누르면 붙음)은 ORCA 로 확인. +""" + +from __future__ import annotations + +import json +import shutil +import subprocess +import tempfile +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[2] +ENTRY = ROOT / "resources" / "tester" / "spreadsheet" / "spreadsheet_toolbar_unit_entry.ts" +VITE = ROOT / "config" / "node_modules" / "vite" / "bin" / "vite.js" + + +@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음") +def test_단위_파이_순수_길(): + with tempfile.TemporaryDirectory(prefix="spreadsheet_toolbar_unit_") as outdir: + entry_rel = "../" + str(ENTRY.relative_to(ROOT)).replace("\\", "/") + build = subprocess.run( + [ + "node", + str(VITE), + "build", + "--configLoader", + "runner", + "--ssr", + entry_rel, + "--outDir", + outdir, + ], + cwd=str(ROOT), + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + timeout=120, + ) + assert build.returncode == 0, f"vite ssr 번들 실패:\n{build.stdout}\n{build.stderr}" + run = subprocess.run( + ["node", str(Path(outdir) / "spreadsheet_toolbar_unit_entry.js")], + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + timeout=60, + ) + assert run.stdout.strip(), run.stderr + result = json.loads(run.stdout) + assert result["fail"] == 0, result["failures"] + assert result["pass"] == 8