diff --git a/A00_Common/spreadsheet/spreadsheet_toolbar.ts b/A00_Common/spreadsheet/spreadsheet_toolbar.ts index 5ff071829..9a934bf69 100644 --- a/A00_Common/spreadsheet/spreadsheet_toolbar.ts +++ b/A00_Common/spreadsheet/spreadsheet_toolbar.ts @@ -63,12 +63,14 @@ const BORDER_LINES: BorderLine[] = [ type TextKey = Parameters[0]; const UNIT_GROUPS: [label: TextKey, items: string[]][] = [ - ["UnitGroup_area", ["㎡", "㎥", "m", "㎝", "㎜", "㎞"]], + ["UnitGroup_area", ["㎡", "㎥", "m", "㎝", "㎜", "㎞", "Φ"]], ["UnitGroup_weight", ["kg", "t", "ℓ", "㎖"]], ["UnitGroup_count", ["개", "본", "식", "EA", "매"]], ["UnitGroup_rate", ["m/s", "°", "%", "‰"]], ["UnitGroup_op", ["×", "÷", "±", "≤", "≥", "Σ"]], ]; +/** 숫자 앞에 붙는 단위(관 지름 Φ300) — 나머지는 뒤에 붙음 */ +const UNIT_PREFIX = ["Φ"]; const UNIT_RECENT_MAX = 8; const UNIT_RECENT_KEY = "aislo-ss-recent-units"; @@ -91,10 +93,14 @@ function pushRecentUnit(unit: string): string[] { return list; } -/** 숫자 형식 코드 끝 따옴표 글자(전에 붙인 단위)를 떼고 새 단위를 붙임 — 없으면 "0" 에서 시작. */ +/** 숫자 형식 코드 끝 따옴표 글자(전에 붙인 단위)를 떼고 새 단위를 붙임 — 없으면 "0" 에서 시작. + * 앞 단위(Φ)는 코드 앞에 붙임(`"Φ"0`) — 앞에 붙은 앞 단위는 먼저 뗌. */ function attachUnitToFormat(code: string, unit: string): string { - const base = (code || "0").replace(/"[^"]*"\s*$/, ""); - return base + `"${unit}"`; + const base = code || "0"; + if (!UNIT_PREFIX.includes(unit)) return base.replace(/"[^"]*"\s*$/, "") + `"${unit}"`; + let plain = base; + for (const p of UNIT_PREFIX) if (plain.startsWith(`"${p}"`)) plain = plain.slice(p.length + 2); + return `"${unit}"` + plain; } /** 활성 칸에 지금 걸린 서식 — 칸 자기 것 없으면 열 · 행 · 시트 기본(`Sheet.기본.서식`) · 그다음 0. */ @@ -399,10 +405,13 @@ export function mountToolbar(ctx: SpreadsheetContext): PartHandle { if (ctx.editor.editing()) { const active = document.activeElement; if (active instanceof HTMLTextAreaElement && ctx.root.contains(active)) { - const s = active.selectionStart ?? active.value.length; - const e = active.selectionEnd ?? active.value.length; + const front = UNIT_PREFIX.includes(unit); + const s = front ? 0 : (active.selectionStart ?? active.value.length); + const e = front ? 0 : (active.selectionEnd ?? active.value.length); + const was = active.selectionStart ?? active.value.length; active.value = active.value.slice(0, s) + unit + active.value.slice(e); - active.setSelectionRange(s + unit.length, s + unit.length); + const at = front ? was + unit.length : s + unit.length; + active.setSelectionRange(at, at); active.dispatchEvent(new Event("input", { bubbles: true })); } } else if (unitAsFormat.checked) { @@ -412,7 +421,12 @@ export function mountToolbar(ctx: SpreadsheetContext): PartHandle { const { r, c } = ctx.selection.활성; const sheet = ctx.sheet(); const key = toA1(r, c); - const text = cellToText(sheet.칸[key]) + unit; + const now = cellToText(sheet.칸[key]); + const text = UNIT_PREFIX.includes(unit) + ? now.startsWith(unit) + ? now + : unit + now // 앞 단위 — 「300」 → 「Φ300」 + : now + unit; ctx.dispatch({ 종류: "칸", 시트: sheet.id, 칸: { [key]: textToCell(text, sheet.칸[key]) } }); } renderRecent(pushRecentUnit(unit)); diff --git a/resources/tester/test_spreadsheet_unit_prefix.py b/resources/tester/test_spreadsheet_unit_prefix.py new file mode 100644 index 000000000..ff96c3f63 --- /dev/null +++ b/resources/tester/test_spreadsheet_unit_prefix.py @@ -0,0 +1,70 @@ +"""스프레드시트 단위 Φ(PLAN 81-5 · `spreadsheet_toolbar.ts`) — 길이 묶음 끝에 Φ · 숫자 앞에 붙임. + +형식 코드 갈래(`attachUnitToFormat`)는 Node 로 함수를 떼어 돌리고, 칸 글 · 편집기 갈래는 글로 확인 +(화면 조작은 ORCA). +""" + +from __future__ import annotations + +import json +import re +import shutil +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[2] +TOOLBAR = ROOT / "A00_Common" / "spreadsheet" / "spreadsheet_toolbar.ts" + + +def _text() -> str: + return TOOLBAR.read_text(encoding="utf-8") + + +@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음") +def test_형식_코드_앞_단위_뒤_단위(): + src = _text() + prefix = re.search(r"^const UNIT_PREFIX = .*;$", src, re.M) + func = re.search(r"^function attachUnitToFormat[\s\S]*?^}", src, re.M) + assert prefix and func + script = ( + prefix.group(0) + + "\n" + + func.group(0).replace(": string", "") # 형 이름 뗌 — Node 가 그대로 돌게 + + """ +const f = attachUnitToFormat; +console.log(JSON.stringify([ + f("", "Φ"), // 처음 = 「Φ」 + 0 + f("0.00", "Φ"), // 코드 앞 + f('"Φ"0', "Φ"), // 두 번 눌러도 한 번 + f('0"m"', "Φ"), // 뒤 단위는 그대로 + f('"Φ"0', "m"), // 뒤 단위를 붙여도 앞 단위 살아 있음 + f('0"m"', "㎜"), // 옛 동작 — 뒤 단위 바꿈 + f("", "m"), +])); +""" + ) + out = subprocess.run( + ["node", "-e", script], capture_output=True, text=True, encoding="utf-8", check=True + ) + assert json.loads(out.stdout) == [ + '"Φ"0', + '"Φ"0.00', + '"Φ"0', + '"Φ"0"m"', + '"Φ"0"m"', + '0"㎜"', + '0"m"', + ] + + +def test_Φ_길이_묶음_끝_칸_글_편집기(): + src = _text() + assert '["UnitGroup_area", ["㎡", "㎥", "m", "㎝", "㎜", "㎞", "Φ"]]' in src + body = src.split("function insertUnit")[1].split("function renderRecent")[0] + assert "UNIT_PREFIX.includes(unit)" in body + assert ( + "now.startsWith(unit)" in body and "unit + now" in body + ) # 「300」 → 「Φ300」 · 두 번 안 붙음 + assert "const s = front ? 0" in body # 편집 중에도 글 맨 앞