From aa3b4f7c9cf4f94928a7013d5e81db314a73cab5 Mon Sep 17 00:00:00 2001 From: ThisTimeNull Date: Wed, 4 Feb 2026 17:09:03 +0900 Subject: [PATCH 1/7] =?UTF-8?q?[fix]=20breadcrumb=20=EB=A7=81=ED=81=AC=20?= =?UTF-8?q?=EC=83=89=EC=83=81=20=EB=B0=8F=20XSS=20=EB=B0=A9=EC=A7=80=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - XSS 방지를 위한 escapeHtml 함수 추가 - renderBreadcrumbItem 함수로 렌더링 로직 분리 (순수 함수) - 링크 색상을 Tailwind 클래스로 명시 (파란색, 다크 모드 지원) - linkable=false 항목을 회색으로 시각화 (클릭 불가 표시) - nav 요소의 색상 클래스를 자식 요소로 이동하여 색상 관리 개선 Co-Authored-By: Claude Sonnet 4.5 --- src/scripts/breadcrumb.ts | 45 ++++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/src/scripts/breadcrumb.ts b/src/scripts/breadcrumb.ts index 58d180d..395e306 100644 --- a/src/scripts/breadcrumb.ts +++ b/src/scripts/breadcrumb.ts @@ -107,29 +107,44 @@ async function generateBreadcrumbItems(): Promise { return breadcrumbItems; } +/** + * XSS 방지를 위한 HTML escape + */ +function escapeHtml(text: string): string { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} + +/** + * Breadcrumb 아이템을 HTML 문자열로 변환 + */ +function renderBreadcrumbItem(item: BreadcrumbItem, isLast: boolean): string { + const escapedName = escapeHtml(item.name); + + if (isLast) { + return `${escapedName}`; + } + + if (item.linkable) { + return `${escapedName} / `; + } + + // linkable=false인 항목은 회색으로 표시 (클릭 불가 시각 표시) + return `${escapedName} / `; +} + /** * Breadcrumb HTML 요소를 생성합니다. */ function createBreadcrumbElement(items: BreadcrumbItem[]): HTMLElement { const breadcrumbNav = document.createElement('nav'); breadcrumbNav.id = 'breadcrumbs'; - breadcrumbNav.className = - 'pb-3 flex min-w-0 items-center gap-2 text-gray-400 dark:text-gray-300'; + // nav 요소에서 색상 클래스 제거 (자식 요소에서 색상 관리) + breadcrumbNav.className = 'pb-3 flex min-w-0 items-center gap-2'; const breadcrumbHTML = items - .map((item, index) => { - const isLast = index === items.length - 1; - - if (isLast) { - return `${item.name}`; - } - - if (!item.linkable) { - return `${item.name} / `; - } else { - return `${item.name} / `; - } - }) + .map((item, index) => renderBreadcrumbItem(item, index === items.length - 1)) .join(''); breadcrumbNav.innerHTML = breadcrumbHTML; From 9a01a517fe150e6ea19e6f0bd61218a1bb95229b Mon Sep 17 00:00:00 2001 From: ThisTimeNull Date: Wed, 4 Feb 2026 17:09:19 +0900 Subject: [PATCH 2/7] =?UTF-8?q?[ui]=20BEM=20=EA=B5=AC=EC=A1=B0=20=EB=8F=84?= =?UTF-8?q?=EC=9E=85=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - index.html: #content에 .markdown-content 클래스 추가 - CSS를 ID 셀렉터(#content)에서 클래스 셀렉터(.markdown-content)로 전환 - BEM 네이밍 컨벤션 적용으로 다른 요소와의 충돌 방지 - 테스트 환경에 .markdown-content 클래스 추가 - tests/markdown.test.ts - tests/table-contents.test.ts Co-Authored-By: Claude Sonnet 4.5 --- index.html | 2 +- tests/markdown.test.ts | 2 +- tests/table-contents.test.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/index.html b/index.html index 57a21c3..6b5c745 100644 --- a/index.html +++ b/index.html @@ -32,7 +32,7 @@
diff --git a/tests/markdown.test.ts b/tests/markdown.test.ts index 273cc8f..00fdbd3 100644 --- a/tests/markdown.test.ts +++ b/tests/markdown.test.ts @@ -27,7 +27,7 @@ afterAll(() => { beforeEach(() => { document = testEnv.document; - document.body.innerHTML = '
'; + document.body.innerHTML = '
'; contentElement = document.getElementById('content')!; }); diff --git a/tests/table-contents.test.ts b/tests/table-contents.test.ts index 5f08922..d0fa411 100644 --- a/tests/table-contents.test.ts +++ b/tests/table-contents.test.ts @@ -25,7 +25,7 @@ beforeAll(async () => { testMarkdownContent = await marked.parse(rawMarkdown); dom = new JSDOM( - '
' + '
' ); // JSDOM 글로벌 환경 설정 @@ -47,7 +47,7 @@ beforeAll(async () => { beforeEach(() => { /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ document = (global as any).document; - document.body.innerHTML = '
'; + document.body.innerHTML = '
'; contentElement = document.getElementById('content')!; tocElement = document.getElementById('toc')!; From 6f58f206f180c8d8b35294075566a26e5ae1d946 Mon Sep 17 00:00:00 2001 From: ThisTimeNull Date: Wed, 4 Feb 2026 17:10:46 +0900 Subject: [PATCH 3/7] =?UTF-8?q?[ui]=20=EB=A7=88=ED=81=AC=EB=8B=A4=EC=9A=B4?= =?UTF-8?q?=20=EC=8A=A4=ED=83=80=EC=9D=BC=EB=A7=81=20=EC=A0=84=EB=A9=B4=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CSS 변수 시스템 구축: - --md-spacing-*: 8px base unit 기반 vertical spacing scale - --md-code-*: 코드 블록 색상 (GitHub 스타일, 다크 모드 지원) - --md-link-*: 링크 색상 (다크 모드 지원) 코드 블록 개선: - GitHub 스타일 배경색 및 테두리 적용 - 다크 모드 자동 전환 지원 - 인라인 코드와 코드 블록 명확히 구분 - 코드 블록 여백 및 내부 패딩 증가 (가독성 향상) - 모노스페이스 폰트 패밀리 명시 링크 스타일 개선: - 모든 하위 링크에 일관된 파란색 적용 - visited 링크도 동일한 색상 유지 (보라색 변경 없음) - 다크 모드 링크 색상 지원 - 키보드 네비게이션 접근성 개선 (focus-visible) Vertical spacing 시스템 적용: - Proximity principle: 제목과 하위 콘텐츠는 가까이 배치 - Visual hierarchy: h2(48px) > h3(40px) > h4(32px) 상단 여백 차등 - 문단 간격 2배 증가 (8px → 16px) - 가장 큰 체감 변화 - 리스트/인용구 여백 통일 (24px) - Contextual spacing: 제목+문단, 제목+코드블록 간격 최적화 - Line height 증가 (1.6 → 1.7) - 가독성 향상 BEM 구조 전환: - #content → .markdown-content 클래스 셀렉터 - 직접 자식 셀렉터(>) 제거하여 중첩 구조 지원 - CSS 선언 순서 최적화 (인라인 코드 먼저, 코드 블록 나중) Co-Authored-By: Claude Sonnet 4.5 --- src/styles/content_style.css | 319 +++++++++++++++++++++++++++-------- 1 file changed, 251 insertions(+), 68 deletions(-) diff --git a/src/styles/content_style.css b/src/styles/content_style.css index 7b808f3..b616ca4 100644 --- a/src/styles/content_style.css +++ b/src/styles/content_style.css @@ -1,154 +1,337 @@ /* * Markdown 콘텐츠 전용 스타일 * 직접 자식만 타겟팅하여 Web Components 제외 + * + * BEM 구조: + * Block: .markdown-content (마크다운 콘텐츠 컨테이너) + * CSS 변수 네임스페이스: --md-code-* (코드 블록 관련) */ +/* ======================================== + CSS 변수 정의 + ======================================== */ +:root { + /* ======================================== + Spacing Scale - 8px base unit + 네임스페이스: md-spacing (markdown-spacing) + 원칙: Vertical rhythm 유지, Proximity principle 적용 + ======================================== */ + --md-spacing-xs: 0.5rem; /* 8px - 리스트 아이템 간격 */ + --md-spacing-sm: 0.75rem; /* 12px - h4 하단, blockquote 내부 문단 */ + --md-spacing-md: 1rem; /* 16px - 기본 문단 간격, h2/h3 하단 */ + --md-spacing-lg: 1.5rem; /* 24px - h1 상하, 코드/인용 상하 */ + --md-spacing-xl: 2rem; /* 32px - h4 상단 */ + --md-spacing-2xl: 2.5rem; /* 40px - h3 상단 */ + --md-spacing-3xl: 3rem; /* 48px - h2 상단 (섹션 구분 강화) */ + + /* ======================================== + 코드 블록 색상 + 네임스페이스: md-code (markdown-code) + ======================================== */ + /* Light mode - GitHub 스타일 */ + --md-code-bg-light: #f6f8fa; + --md-code-border-light: #d0d7de; + --md-code-text-light: #24292f; + --md-code-inline-bg-light: rgba(175, 184, 193, 0.2); + --md-code-inline-border-light: rgba(175, 184, 193, 0.3); + + /* Dark mode - GitHub 스타일 */ + --md-code-bg-dark: #161b22; + --md-code-border-dark: #30363d; + --md-code-text-dark: #e6edf3; + --md-code-inline-bg-dark: rgba(110, 118, 129, 0.3); + --md-code-inline-border-dark: rgba(110, 118, 129, 0.4); + + /* Active theme (기본값: light) */ + --md-code-bg: var(--md-code-bg-light); + --md-code-border: var(--md-code-border-light); + --md-code-text: var(--md-code-text-light); + --md-code-inline-bg: var(--md-code-inline-bg-light); + --md-code-inline-border: var(--md-code-inline-border-light); + + /* 공통 폰트 스택 */ + --md-code-font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', + Consolas, 'Courier New', monospace; + + /* ======================================== + 링크 색상 + 네임스페이스: md-link (markdown-link) + WCAG AA 기준 충족 (4.5:1 대비) + Note: visited 링크도 동일한 색상 유지 (보라색 변경 없음) + ======================================== */ + /* Light mode - GitHub 스타일 */ + --md-link-color-light: #0969da; + --md-link-color-hover-light: #0550ae; + + /* Dark mode - GitHub 스타일 */ + --md-link-color-dark: #58a6ff; + --md-link-color-hover-dark: #79c0ff; + + /* Active theme (기본값: light) */ + --md-link-color: var(--md-link-color-light); + --md-link-color-hover: var(--md-link-color-hover-light); +} + +@media (prefers-color-scheme: dark) { + :root { + --md-code-bg: var(--md-code-bg-dark); + --md-code-border: var(--md-code-border-dark); + --md-code-text: var(--md-code-text-dark); + --md-code-inline-bg: var(--md-code-inline-bg-dark); + --md-code-inline-border: var(--md-code-inline-border-dark); + + /* 링크 다크 모드 */ + --md-link-color: var(--md-link-color-dark); + --md-link-color-hover: var(--md-link-color-hover-dark); + } +} + /* ======================================== 타이포그래피 + 원칙: + - Proximity principle: 제목은 하위 콘텐츠와 가까이 (margin-top > margin-bottom) + - Visual hierarchy: h2 > h3 > h4 순으로 상단 여백 차등 적용 + - Vertical rhythm: 8px base unit 사용 ======================================== */ -#content > h1, -#content > h2, -#content > h3, -#content > h4 { +.markdown-content > h1, +.markdown-content > h2, +.markdown-content > h3, +.markdown-content > h4 { font-weight: 500; - margin-bottom: 1rem; - margin-top: 2rem; color: #444; } -#content > h1 { +/* h1 - 페이지 제목 */ +.markdown-content > h1 { font-size: 2rem; font-weight: bold; - margin-top: 1rem; + margin-top: var(--md-spacing-lg); /* 24px - 페이지 시작 여백 */ + margin-bottom: var(--md-spacing-lg); /* 24px - 하위 콘텐츠와 적절한 거리 */ } -#content > h2 { +/* h2 - 섹션 구분 (가장 중요한 구조) */ +.markdown-content > h2 { font-size: 1.75rem; + margin-top: var(--md-spacing-3xl); /* 48px - 섹션 구분 강화 */ + margin-bottom: var(--md-spacing-md); /* 16px - 하위 콘텐츠와 가까이 */ } -#content > h3 { +/* h3 - 하위 섹션 */ +.markdown-content > h3 { font-size: 1.5rem; + margin-top: var(--md-spacing-2xl); /* 40px - 중간 계층 구분 */ + margin-bottom: var(--md-spacing-md); /* 16px - 하위 콘텐츠와 가까이 */ } -#content > h4 { +/* h4 - 세부 항목 */ +.markdown-content > h4 { font-size: 1.2rem; + margin-top: var(--md-spacing-xl); /* 32px - 최소 구분 여백 */ + margin-bottom: var(--md-spacing-sm); /* 12px - 하위 콘텐츠와 매우 가까이 */ } -#content > p { +/* p - 문단 (가장 중요한 변경) */ +.markdown-content > p { font-size: 1rem; - line-height: 1.6; - margin-bottom: 0.5rem; + line-height: 1.7; /* 1.6 → 1.7 증가: 가독성 향상 */ + margin-bottom: var(--md-spacing-md); /* 16px - 0.5rem(8px)에서 증가 */ color: #222; } +/* ======================================== + Contextual spacing - 제목 바로 다음 요소 + 원칙: 제목과 첫 번째 콘텐츠는 더욱 가까이 + ======================================== */ +.markdown-content > h2 + p, +.markdown-content > h3 + p, +.markdown-content > h4 + p { + margin-top: 0; /* 제목의 margin-bottom만 적용 */ +} + /* ======================================== 리스트 + 원칙: + - 리스트 컨테이너는 다른 블록 요소와 동일한 하단 여백 + - 리스트 아이템 간격은 최소 단위 사용 (밀도 유지) + - 중첩 리스트는 상하 여백 최소화 ======================================== */ -#content > ul, -#content > ol { +/* 리스트 컨테이너 */ +.markdown-content > ul, +.markdown-content > ol { margin-left: 1.5rem; padding-left: 1rem; + margin-bottom: var(--md-spacing-lg); /* 24px - 블록 요소 간격 통일 */ color: #222; } -#content > ul { +.markdown-content > ul { list-style-type: disc; } -#content > ol { +.markdown-content > ol { list-style-type: decimal; } -/* 리스트 아이템은 ul/ol의 직접 자식 */ -#content > ul > li, -#content > ol > li { - margin-bottom: 0.5rem; +/* 리스트 아이템 */ +.markdown-content > ul > li, +.markdown-content > ol > li { + margin-bottom: var(--md-spacing-xs); /* 8px - 최소 단위 */ font-size: 1rem; - line-height: 1.6; + line-height: 1.7; /* 1.6 → 1.7: 문단과 통일 */ +} + +/* 마지막 아이템은 하단 여백 제거 */ +.markdown-content > ul > li:last-child, +.markdown-content > ol > li:last-child { + margin-bottom: 0; } /* 중첩 리스트 */ -#content ul ul, -#content ol ol, -#content ul ol, -#content ol ul { +.markdown-content ul ul, +.markdown-content ol ol, +.markdown-content ul ol, +.markdown-content ol ul { margin-left: 1.5rem; padding-left: 1rem; + margin-top: var(--md-spacing-xs); /* 8px - 상위 아이템과 간격 */ + margin-bottom: var(--md-spacing-xs); /* 8px - 다음 아이템과 간격 */ } /* ======================================== - 코드 블록 + 코드 블록 - BEM 구조 + Block: .markdown-content + Element (논리적): pre (코드 블록), code (코드 텍스트/인라인) + Note: 마크다운 파서가 생성하는 HTML이므로 Element 클래스를 직접 붙일 수 없음 + 대신 하위 셀렉터로 타겟팅 ======================================== */ -#content > pre { - padding: 0.5rem; - border: 2px solid #e1e2e6; +/* Element (논리적): 인라인 코드 () - pre 안에 없는 경우만 */ +.markdown-content code { + font-size: 0.875rem; + font-family: var(--md-code-font-family); + background-color: var(--md-code-inline-bg); + color: var(--md-code-text); + padding: 0.125rem 0.375rem; + border: 1px solid var(--md-code-inline-border); + border-radius: 4px; + white-space: nowrap; +} + +/* Element (논리적): 코드 블록 컨테이너 (
) - 자손 셀렉터 사용 */
+.markdown-content pre {
+  padding: 1.25rem; /* 20px - 1rem(16px)에서 증가: 내부 여유 공간 확보 */
+  background-color: var(--md-code-bg);
+  border: 1px solid var(--md-code-border);
   border-radius: 8px;
   overflow-x: auto;
   line-height: 1.5;
-  margin-bottom: 0.5rem;
+  margin-top: var(--md-spacing-lg);    /* 24px - 1rem(16px)에서 증가 */
+  margin-bottom: var(--md-spacing-lg); /* 24px - 1rem(16px)에서 증가 */
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
 }
 
-#content pre code {
-  display: block;
-  background: #fff;
-  color: #000000;
-  white-space: pre-wrap;
+/* Contextual spacing - 제목 바로 다음 코드 블록 */
+.markdown-content > h2 + pre,
+.markdown-content > h3 + pre,
+.markdown-content > h4 + pre {
+  margin-top: var(--md-spacing-md); /* 16px - 제목과 가까이 */
 }
 
-#content code {
-  font-size: 13px;
-  background-color: #e1e2e6;
-  color: #000000;
-  padding: 2px 6px;
-  border-radius: 4px;
-  white-space: nowrap;
+/* Contextual spacing - 코드 블록 바로 다음 제목 */
+.markdown-content > pre + h2,
+.markdown-content > pre + h3,
+.markdown-content > pre + h4 {
+  margin-top: var(--md-spacing-3xl); /* 48px - 섹션 전환 명확화 */
 }
 
-/* ========================================
-   기타 요소
-   ======================================== */
+/* Dark mode 추가 조정 */
+@media (prefers-color-scheme: dark) {
+  .markdown-content pre {
+    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
+  }
+}
 
-#content > blockquote {
-  border-left: 4px solid #007bff;
-  padding: 0.75rem 1rem;
-  margin: 1rem 0;
-  background-color: #f8f9fa;
-  font-style: italic;
-  color: #555;
+/* Element (논리적): 코드 블록 내부 텍스트 (
) - 인라인 스타일 오버라이드 */
+.markdown-content pre code {
+  display: block;
+  background: transparent;
+  color: var(--md-code-text);
+  white-space: pre;
+  font-family: var(--md-code-font-family);
+  font-size: 0.875rem;
+  line-height: 1.6;
+  padding: 0;
+  border: none;
+  border-radius: 0;
 }
 
-#content > a {
-  color: #007bff;
+/* ========================================
+   링크 스타일
+   Note: visited 링크도 동일한 색상 유지 (보라색 변경 없음)
+   ======================================== */
+
+/* 모든 하위 링크에 기본 스타일 적용 */
+.markdown-content a {
+  color: var(--md-link-color);
   text-decoration: none;
   transition: color 0.2s ease-in-out;
 }
 
-#content > a:hover {
-  color: #007bff;
+.markdown-content a:hover {
+  color: var(--md-link-color-hover);
+}
+
+/* visited 링크도 동일한 파란색 유지 */
+.markdown-content a:visited {
+  color: var(--md-link-color);
+}
+
+/* 키보드 네비게이션 접근성 */
+.markdown-content a:focus-visible {
+  outline: 2px solid var(--md-link-color);
+  outline-offset: 2px;
+  border-radius: 2px;
 }
 
-#content > a.external-link::after {
+/* 외부 링크 아이콘 */
+.markdown-content a.external-link::after {
   content: ' 🔗';
   font-size: 0.8em;
   opacity: 0.7;
 }
 
-/* 문단 내 링크 */
-#content p a {
-  color: #007bff;
-  text-decoration: none;
-  transition: color 0.2s ease-in-out;
+/* ========================================
+   인용구 (blockquote)
+   원칙: 코드 블록과 동일한 상하 여백, 내부는 밀도 높게
+   ======================================== */
+
+.markdown-content > blockquote {
+  border-left: 4px solid #007bff;
+  padding: var(--md-spacing-md) 1.25rem; /* 상하 16px, 좌우 20px */
+  margin-top: var(--md-spacing-lg);      /* 24px */
+  margin-bottom: var(--md-spacing-lg);   /* 24px */
+  background-color: #f8f9fa;
+  font-style: italic;
+  color: #555;
+}
+
+/* blockquote 내부 문단 간격 */
+.markdown-content > blockquote > p {
+  margin-bottom: var(--md-spacing-sm); /* 12px - 인용구 내부는 밀도 높게 */
 }
 
-#content p a:hover {
-  color: #0056b3;
+.markdown-content > blockquote > p:last-child {
+  margin-bottom: 0; /* 마지막 문단은 여백 제거 */
 }
 
+/* ========================================
+   기타 요소
+   ======================================== */
+
 /* YouTube 임베드는 클래스 기반이므로 그대로 */
-#content .youtube-video {
+.markdown-content .youtube-video {
   position: relative;
   padding-bottom: 56.25%;
   height: 0;
@@ -157,7 +340,7 @@
   max-height: auto;
 }
 
-#content .youtube-video iframe {
+.markdown-content .youtube-video iframe {
   position: absolute;
   top: 0;
   left: 0;

From cb5900d27b2308fa8c55240214b4fe99c483496c Mon Sep 17 00:00:00 2001
From: ThisTimeNull 
Date: Wed, 4 Feb 2026 17:47:28 +0900
Subject: [PATCH 4/7] =?UTF-8?q?[fix]=20sticky=20header=20=EB=86=92?=
 =?UTF-8?q?=EC=9D=B4=20=EB=B6=88=EC=9D=BC=EC=B9=98=EB=A1=9C=20=EC=9D=B8?=
 =?UTF-8?q?=ED=95=9C=20nav/aside=20=EA=B2=B9=EC=B9=A8=20=EB=AC=B8=EC=A0=9C?=
 =?UTF-8?q?=20=ED=95=B4=EA=B2=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

sticky header의 높이가 top-16(64px)으로 변경되면서 좌측 네비게이션과 우측 TOC가 헤더와 겹치는 문제를 수정했습니다.
nav#sidebar와 aside#aside-toc의 top 값을 top-12에서 top-16으로 조정하여 일관성을 유지합니다.

Co-Authored-By: Claude Sonnet 4.5 
---
 index.html | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/index.html b/index.html
index 6b5c745..1d6c058 100644
--- a/index.html
+++ b/index.html
@@ -24,7 +24,7 @@
       
@@ -38,7 +38,7 @@
       
       

From 94c14b274203a31e3bdb327b8d4e848c4460a42c Mon Sep 17 00:00:00 2001
From: ThisTimeNull 
Date: Wed, 4 Feb 2026 17:57:31 +0900
Subject: [PATCH 5/7] =?UTF-8?q?[chore]=20Prettier=20=EC=9E=90=EB=8F=99=20?=
 =?UTF-8?q?=ED=8F=AC=EB=A7=B7=ED=8C=85=20=EC=A0=81=EC=9A=A9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-Authored-By: Claude Sonnet 4.5 
---
 src/scripts/breadcrumb.ts    |  4 +++-
 src/styles/content_style.css | 35 ++++++++++++++++++-----------------
 tests/table-contents.test.ts |  3 ++-
 3 files changed, 23 insertions(+), 19 deletions(-)

diff --git a/src/scripts/breadcrumb.ts b/src/scripts/breadcrumb.ts
index 395e306..272e481 100644
--- a/src/scripts/breadcrumb.ts
+++ b/src/scripts/breadcrumb.ts
@@ -144,7 +144,9 @@ function createBreadcrumbElement(items: BreadcrumbItem[]): HTMLElement {
   breadcrumbNav.className = 'pb-3 flex min-w-0 items-center gap-2';
 
   const breadcrumbHTML = items
-    .map((item, index) => renderBreadcrumbItem(item, index === items.length - 1))
+    .map((item, index) =>
+      renderBreadcrumbItem(item, index === items.length - 1)
+    )
     .join('');
 
   breadcrumbNav.innerHTML = breadcrumbHTML;
diff --git a/src/styles/content_style.css b/src/styles/content_style.css
index b616ca4..3a3af69 100644
--- a/src/styles/content_style.css
+++ b/src/styles/content_style.css
@@ -16,13 +16,13 @@
      네임스페이스: md-spacing (markdown-spacing)
      원칙: Vertical rhythm 유지, Proximity principle 적용
      ======================================== */
-  --md-spacing-xs: 0.5rem;   /* 8px  - 리스트 아이템 간격 */
-  --md-spacing-sm: 0.75rem;  /* 12px - h4 하단, blockquote 내부 문단 */
-  --md-spacing-md: 1rem;     /* 16px - 기본 문단 간격, h2/h3 하단 */
-  --md-spacing-lg: 1.5rem;   /* 24px - h1 상하, 코드/인용 상하 */
-  --md-spacing-xl: 2rem;     /* 32px - h4 상단 */
-  --md-spacing-2xl: 2.5rem;  /* 40px - h3 상단 */
-  --md-spacing-3xl: 3rem;    /* 48px - h2 상단 (섹션 구분 강화) */
+  --md-spacing-xs: 0.5rem; /* 8px  - 리스트 아이템 간격 */
+  --md-spacing-sm: 0.75rem; /* 12px - h4 하단, blockquote 내부 문단 */
+  --md-spacing-md: 1rem; /* 16px - 기본 문단 간격, h2/h3 하단 */
+  --md-spacing-lg: 1.5rem; /* 24px - h1 상하, 코드/인용 상하 */
+  --md-spacing-xl: 2rem; /* 32px - h4 상단 */
+  --md-spacing-2xl: 2.5rem; /* 40px - h3 상단 */
+  --md-spacing-3xl: 3rem; /* 48px - h2 상단 (섹션 구분 강화) */
 
   /* ========================================
      코드 블록 색상
@@ -50,8 +50,9 @@
   --md-code-inline-border: var(--md-code-inline-border-light);
 
   /* 공통 폰트 스택 */
-  --md-code-font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono',
-                         Consolas, 'Courier New', monospace;
+  --md-code-font-family:
+    'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New',
+    monospace;
 
   /* ========================================
      링크 색상
@@ -106,28 +107,28 @@
 .markdown-content > h1 {
   font-size: 2rem;
   font-weight: bold;
-  margin-top: var(--md-spacing-lg);   /* 24px - 페이지 시작 여백 */
+  margin-top: var(--md-spacing-lg); /* 24px - 페이지 시작 여백 */
   margin-bottom: var(--md-spacing-lg); /* 24px - 하위 콘텐츠와 적절한 거리 */
 }
 
 /* h2 - 섹션 구분 (가장 중요한 구조) */
 .markdown-content > h2 {
   font-size: 1.75rem;
-  margin-top: var(--md-spacing-3xl);  /* 48px - 섹션 구분 강화 */
+  margin-top: var(--md-spacing-3xl); /* 48px - 섹션 구분 강화 */
   margin-bottom: var(--md-spacing-md); /* 16px - 하위 콘텐츠와 가까이 */
 }
 
 /* h3 - 하위 섹션 */
 .markdown-content > h3 {
   font-size: 1.5rem;
-  margin-top: var(--md-spacing-2xl);  /* 40px - 중간 계층 구분 */
+  margin-top: var(--md-spacing-2xl); /* 40px - 중간 계층 구분 */
   margin-bottom: var(--md-spacing-md); /* 16px - 하위 콘텐츠와 가까이 */
 }
 
 /* h4 - 세부 항목 */
 .markdown-content > h4 {
   font-size: 1.2rem;
-  margin-top: var(--md-spacing-xl);   /* 32px - 최소 구분 여백 */
+  margin-top: var(--md-spacing-xl); /* 32px - 최소 구분 여백 */
   margin-bottom: var(--md-spacing-sm); /* 12px - 하위 콘텐츠와 매우 가까이 */
 }
 
@@ -195,7 +196,7 @@
 .markdown-content ol ul {
   margin-left: 1.5rem;
   padding-left: 1rem;
-  margin-top: var(--md-spacing-xs);    /* 8px - 상위 아이템과 간격 */
+  margin-top: var(--md-spacing-xs); /* 8px - 상위 아이템과 간격 */
   margin-bottom: var(--md-spacing-xs); /* 8px - 다음 아이템과 간격 */
 }
 
@@ -227,7 +228,7 @@
   border-radius: 8px;
   overflow-x: auto;
   line-height: 1.5;
-  margin-top: var(--md-spacing-lg);    /* 24px - 1rem(16px)에서 증가 */
+  margin-top: var(--md-spacing-lg); /* 24px - 1rem(16px)에서 증가 */
   margin-bottom: var(--md-spacing-lg); /* 24px - 1rem(16px)에서 증가 */
   box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
 }
@@ -310,8 +311,8 @@
 .markdown-content > blockquote {
   border-left: 4px solid #007bff;
   padding: var(--md-spacing-md) 1.25rem; /* 상하 16px, 좌우 20px */
-  margin-top: var(--md-spacing-lg);      /* 24px */
-  margin-bottom: var(--md-spacing-lg);   /* 24px */
+  margin-top: var(--md-spacing-lg); /* 24px */
+  margin-bottom: var(--md-spacing-lg); /* 24px */
   background-color: #f8f9fa;
   font-style: italic;
   color: #555;
diff --git a/tests/table-contents.test.ts b/tests/table-contents.test.ts
index d0fa411..d3d6a76 100644
--- a/tests/table-contents.test.ts
+++ b/tests/table-contents.test.ts
@@ -47,7 +47,8 @@ beforeAll(async () => {
 beforeEach(() => {
   /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
   document = (global as any).document;
-  document.body.innerHTML = '
'; + document.body.innerHTML = + '
'; contentElement = document.getElementById('content')!; tocElement = document.getElementById('toc')!; From a0131afc4c936ee893be5a2beaeb9c26d94d7f1a Mon Sep 17 00:00:00 2001 From: ThisTimeNull Date: Wed, 4 Feb 2026 19:08:00 +0900 Subject: [PATCH 6/7] =?UTF-8?q?[fix]=20XSS=20=EC=B7=A8=EC=95=BD=EC=A0=90?= =?UTF-8?q?=20=EB=B0=A9=EC=A7=80=20-=20breadcrumb=20href=20=EC=86=8D?= =?UTF-8?q?=EC=84=B1=20=EC=9D=B4=EC=8A=A4=EC=BC=80=EC=9D=B4=ED=94=84=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copilot PR 피드백을 반영하여 보안 취약점을 개선했습니다. - escapeHtmlAttribute() 함수 추가: href 속성값을 안전하게 이스케이프 - item.path를 escapedPath로 변환하여 XSS 공격 벡터 차단 - 특수문자(&, <, >, ", ')를 HTML 엔티티로 치환 Co-Authored-By: Claude Sonnet 4.5 --- src/scripts/breadcrumb.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/scripts/breadcrumb.ts b/src/scripts/breadcrumb.ts index 272e481..c0360ea 100644 --- a/src/scripts/breadcrumb.ts +++ b/src/scripts/breadcrumb.ts @@ -116,18 +116,32 @@ function escapeHtml(text: string): string { return div.innerHTML; } +/** + * href 속성값을 안전하게 escape + * 따옴표, 꺾쇠괄호 등을 HTML 엔티티로 변환 + */ +function escapeHtmlAttribute(value: string): string { + return value + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(//g, '>'); +} + /** * Breadcrumb 아이템을 HTML 문자열로 변환 */ function renderBreadcrumbItem(item: BreadcrumbItem, isLast: boolean): string { const escapedName = escapeHtml(item.name); + const escapedPath = escapeHtmlAttribute(item.path); if (isLast) { return `${escapedName}`; } if (item.linkable) { - return `${escapedName} / `; + return `${escapedName} / `; } // linkable=false인 항목은 회색으로 표시 (클릭 불가 시각 표시) From 2f74c2887fe583e1194e6841b7b5073bd2d00d04 Mon Sep 17 00:00:00 2001 From: ThisTimeNull Date: Wed, 4 Feb 2026 19:08:43 +0900 Subject: [PATCH 7/7] =?UTF-8?q?[fix]=20CSS=20=EC=85=80=EB=A0=89=ED=84=B0?= =?UTF-8?q?=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20=EC=A3=BC=EC=84=9D=20?= =?UTF-8?q?=EB=AA=85=ED=99=95=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copilot PR 피드백을 반영하여 CSS 코드 품질을 개선했습니다. - 인라인 코드 셀렉터 정밀도 향상: .markdown-content :not(pre) > code (pre 태그 내부 코드 블록 제외, 직접 자식만 선택) - 유효하지 않은 CSS 속성 제거: max-height: auto - 주석 개선: 직접 자식 셀렉터(>)와 자손 셀렉터(공백) 사용 전략 명확화 Co-Authored-By: Claude Sonnet 4.5 --- src/styles/content_style.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/styles/content_style.css b/src/styles/content_style.css index 3a3af69..cb9214f 100644 --- a/src/styles/content_style.css +++ b/src/styles/content_style.css @@ -1,6 +1,7 @@ /* * Markdown 콘텐츠 전용 스타일 - * 직접 자식만 타겟팅하여 Web Components 제외 + * 기본적으로 .markdown-content의 직접 자식만 타겟팅하여 Web Components 경계를 침범하지 않으며, + * 코드/링크 등 인라인 요소에 한해서는 예외적으로 자손 셀렉터도 사용합니다. * * BEM 구조: * Block: .markdown-content (마크다운 콘텐츠 컨테이너) @@ -209,7 +210,7 @@ ======================================== */ /* Element (논리적): 인라인 코드 () - pre 안에 없는 경우만 */ -.markdown-content code { +.markdown-content :not(pre) > code { font-size: 0.875rem; font-family: var(--md-code-font-family); background-color: var(--md-code-inline-bg); @@ -338,7 +339,6 @@ height: 0; overflow: hidden; max-width: 100%; - max-height: auto; } .markdown-content .youtube-video iframe {