diff --git a/A00_Common/spreadsheet/spreadsheet_toolbar.ts b/A00_Common/spreadsheet/spreadsheet_toolbar.ts index 3f4ad3eda..5c450eade 100644 --- a/A00_Common/spreadsheet/spreadsheet_toolbar.ts +++ b/A00_Common/spreadsheet/spreadsheet_toolbar.ts @@ -103,6 +103,15 @@ export function attachUnitToFormat(code: string, unit: string): string { return `"${unit}"` + plain; } +/** 앞 단위를 글 앞에 붙임(「300」 → 「Φ300」) — 글 낫표(강제 글 `'300`)는 맨 앞에 두고 그 뒤에 + * 붙임(`'Φ300` → 칸 값 「Φ300」) · 이미 붙어 있거나 식(`=`)이면 그대로 */ +export function prefixUnit(text: string, unit: string): string { + if (text.startsWith("=")) return text; + const lead = text.startsWith("'") ? "'" : ""; + const body = text.slice(lead.length); + return body.startsWith(unit) ? text : lead + unit + body; +} + /** 활성 칸에 지금 걸린 서식 — 칸 자기 것 없으면 열 · 행 · 시트 기본(`Sheet.기본.서식`) · 그다음 0. */ function styleAt(ctx: SpreadsheetContext, r: number, c: number): CellStyle { const sheet = ctx.sheet(); @@ -405,13 +414,18 @@ export function mountToolbar(ctx: SpreadsheetContext): PartHandle { if (ctx.editor.editing()) { const active = document.activeElement; if (active instanceof HTMLTextAreaElement && ctx.root.contains(active)) { - 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); - const at = front ? was + unit.length : s + unit.length; - active.setSelectionRange(at, at); + if (UNIT_PREFIX.includes(unit)) { + const next = prefixUnit(active.value, unit); + const at = was + next.length - active.value.length; + active.value = next; + active.setSelectionRange(at, at); + } else { + const s = was; + const e = active.selectionEnd ?? active.value.length; + active.value = active.value.slice(0, s) + unit + active.value.slice(e); + active.setSelectionRange(s + unit.length, s + unit.length); + } active.dispatchEvent(new Event("input", { bubbles: true })); } } else if (unitAsFormat.checked) { @@ -422,11 +436,7 @@ export function mountToolbar(ctx: SpreadsheetContext): PartHandle { const sheet = ctx.sheet(); const key = toA1(r, c); const now = cellToText(sheet.칸[key]); - const text = UNIT_PREFIX.includes(unit) - ? now.startsWith(unit) - ? now - : unit + now // 앞 단위 — 「300」 → 「Φ300」 - : now + unit; + const text = UNIT_PREFIX.includes(unit) ? prefixUnit(now, unit) : now + unit; ctx.dispatch({ 종류: "칸", 시트: sheet.id, 칸: { [key]: textToCell(text, sheet.칸[key]) } }); } renderRecent(pushRecentUnit(unit)); diff --git a/resources/tester/spreadsheet/spreadsheet_toolbar_unit_entry.ts b/resources/tester/spreadsheet/spreadsheet_toolbar_unit_entry.ts index ef70a469e..eda0ed3e0 100644 --- a/resources/tester/spreadsheet/spreadsheet_toolbar_unit_entry.ts +++ b/resources/tester/spreadsheet/spreadsheet_toolbar_unit_entry.ts @@ -3,6 +3,7 @@ * 결과 = JSON {pass, fail, failures} 한 줄(`test_spreadsheet_toolbar_unit.py` 가 읽음) */ import { attachUnitToFormat, + prefixUnit, UNIT_GROUPS, UNIT_PREFIX, } from "../../../A00_Common/spreadsheet/spreadsheet_toolbar"; @@ -36,4 +37,12 @@ eq("Φ 서식 — 빈 코드도 0 기준", attachUnitToFormat("", PHI), '"Φ"0') eq("Φ 서식 — 옛 Φ 바꿈(겹쳐 안 붙음)", attachUnitToFormat('"Φ"0', PHI), '"Φ"0'); eq("Φ 서식 — 뒤 단위와 공존", attachUnitToFormat('0"㎡"', PHI), '"Φ"0"㎡"'); +// 앞 단위 글 — 낫표(강제 글)는 맨 앞에 두고 그 뒤에 붙임 · 이미 붙었으면 그대로(81-5 흠) +eq("Φ 글 — 숫자 앞", prefixUnit("300", PHI), "Φ300"); +eq("Φ 글 — 낫표 뒤", prefixUnit("'300", PHI), "'Φ300"); +eq("Φ 글 — 두 번 안 붙음", prefixUnit("Φ300", PHI), "Φ300"); +eq("Φ 글 — 낫표 뒤 두 번 안 붙음", prefixUnit("'Φ300", PHI), "'Φ300"); +eq("Φ 글 — 빈 칸", prefixUnit("", PHI), "Φ"); +eq("Φ 글 — 식은 그대로", prefixUnit("=A1", PHI), "=A1"); + 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 index 2df38cb7c..5fa31707e 100644 --- a/resources/tester/test_spreadsheet_toolbar_unit.py +++ b/resources/tester/test_spreadsheet_toolbar_unit.py @@ -54,4 +54,4 @@ def test_단위_파이_순수_길(): assert run.stdout.strip(), run.stderr result = json.loads(run.stdout) assert result["fail"] == 0, result["failures"] - assert result["pass"] == 9 + assert result["pass"] == 15 diff --git a/resources/tester/test_spreadsheet_unit_prefix.py b/resources/tester/test_spreadsheet_unit_prefix.py index 9710e049d..17bcae407 100644 --- a/resources/tester/test_spreadsheet_unit_prefix.py +++ b/resources/tester/test_spreadsheet_unit_prefix.py @@ -66,7 +66,23 @@ def test_Φ_길이_묶음_끝_칸_글_편집기(): 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 # 편집 중에도 글 맨 앞 + assert "prefixUnit(now, unit)" in body # 칸 글 — 「300」 → 「Φ300」 · 두 번 안 붙음 + assert "prefixUnit(active.value, unit)" in body # 편집 중에도 같은 규칙 + + +@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음") +def test_앞_단위_글_낫표_뒤에_붙음(): + """강제 글 낫표(`'300`)는 맨 앞에 두고 그 뒤에 붙임 — 「Φ'300」 처럼 낫표가 가운데 남지 않음.""" + func = re.search(r"^export function prefixUnit[\s\S]*?^}", _text(), re.M) + assert func + script = ( + func.group(0).replace("export ", "").replace(": string", "") + + """ +const f = prefixUnit; +console.log(JSON.stringify([f("300", "Φ"), f("'300", "Φ"), f("Φ300", "Φ"), f("'Φ300", "Φ"), f("=A1", "Φ"), f("", "Φ")])); +""" + ) + out = subprocess.run( + ["node", "-e", script], capture_output=True, text=True, encoding="utf-8", check=True + ) + assert json.loads(out.stdout) == ["Φ300", "'Φ300", "Φ300", "'Φ300", "=A1", "Φ"]