fix(spreadsheet): 엑셀 붙여넣기 서식 — 묶음 선택자 클래스 · 선 굵기별 종류

- <style> 안 클래스 규칙 읽기가 「.xl65, .xl70 {...}」 같은 묶음 선택자에서 앞쪽 클래스를
  놓치던 것 고침(전체 선택자를 쉼표로 나눠 각 클래스에 선언 병합)
- 테두리 pt 굵기 → 우리 선 종류 매핑 추가(.5pt 이하 hair · ~1.25pt thin · ~2pt medium · 그 위
  thick, dashed·dotted·double 은 굵기와 상관없이 그 모양) — 「합계 줄 위 굵은 선」처럼 같은
  칸 안 굵기가 다른 테두리를 구별해 살림
- 실제 엑셀 내보내기 짜임(머리 채움 · 가는 테두리 · 합계 줄 위 굵은 테두리 · #,##0 숫자 형식)을
  재현한 새 견본 resources/tester/spreadsheet/fixture_excel_style_paste.html 로 시험
- ORCA 실 엔진 확인 — 머리 칸 굵게·채움 #d9d9d9·가운데 정렬·사방 hair, 숫자 칸 오른쪽 정렬·
  형식 #,##0·사방 hair, 합계 줄 굵게·위 테두리만 medium(나머지 hair)까지 전부 반영 확인

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeeQRLknEpP4SFjVkjDWDs
This commit is contained in:
2026-09-28 09:45:50 +09:00
co-authored by Claude Sonnet 5
parent e4eef2a47b
commit b2e2fcff91
2 changed files with 97 additions and 10 deletions
+20 -10
View File
@@ -252,29 +252,39 @@ function unescapeMsoNumFmt(v: string): string {
return v.replace(/^"|"$/g, "").replace(/\\(.)/g, "$1");
}
const BORDER_STYLE_MAP: Record<string, BorderLine> = {
solid: "thin",
dashed: "dashed",
dotted: "dotted",
double: "double",
};
/** pt 굵기 → 우리 선 종류(엑셀 내보내기 흔한 값 기준 — hairline ≈ .5pt 이하 · thin ≈ 1pt · medium ≈
* 1.5~2pt · thick ≈ 2pt 넘음). `solid` 아닌 것은 굵기와 상관없이 그 모양. */
function ptToBorderLine(pt: number, cssStyle: string): BorderLine {
if (cssStyle === "dashed") return "dashed";
if (cssStyle === "dotted") return "dotted";
if (cssStyle === "double") return "double";
if (pt <= 0.5) return "hair";
if (pt <= 1.25) return "thin";
if (pt <= 2) return "medium";
return "thick";
}
function parseBorderShorthand(v: string): BorderSide | undefined {
const m = /([\d.]+)\s*(?:pt|px)\s+(solid|dashed|dotted|double)\s*([^;]*)/i.exec(v);
if (!m) return undefined;
const 선 = BORDER_STYLE_MAP[m[2].toLowerCase()] ?? "thin";
const 선 = ptToBorderLine(Number(m[1]), m[2].toLowerCase());
const 색 = cssColorToHex(m[3]);
return 색 ? { 선, 색 } : { 선 };
}
/** `<style>...</style>` 안 `.xl65 {...}` 짜임(엑셀이 인라인 대신 클래스로 서식을 실을 때) → 클래스별 선언. */
/** `<style>...</style>` 안 `.xl65 {...}` · `.xl65, .xl70 {...}`(묶음 선택자) 짜임(엑셀이 인라인 대신
* 클래스로 서식을 실을 때) → 클래스별 선언(같은 클래스가 여러 규칙에 나오면 뒤엣것이 이김). */
function parseClassRules(doc: Document): Record<string, Record<string, string>> {
const css = Array.from(doc.querySelectorAll("style"))
.map((s) => s.textContent ?? "")
.join("\n");
const rules: Record<string, Record<string, string>> = {};
for (const m of css.matchAll(/\.([a-zA-Z0-9_-]+)\s*\{([^}]*)\}/g)) {
rules[m[1]] = parseStyleAttr(m[2]);
for (const m of css.matchAll(/([^{}]+)\{([^}]*)\}/g)) {
const decls = parseStyleAttr(m[2]);
for (const selectorPart of m[1].split(",")) {
const cls = /\.([a-zA-Z0-9_-]+)\s*$/.exec(selectorPart.trim());
if (cls) Object.assign((rules[cls[1]] ??= {}), decls);
}
}
return rules;
}