@@ -107,29 +107,60 @@ async function generateBreadcrumbItems(): Promise<BreadcrumbItem[]> {
107107 return breadcrumbItems ;
108108}
109109
110+ /**
111+ * XSS 방지를 위한 HTML escape
112+ */
113+ function escapeHtml ( text : string ) : string {
114+ const div = document . createElement ( 'div' ) ;
115+ div . textContent = text ;
116+ return div . innerHTML ;
117+ }
118+
119+ /**
120+ * href 속성값을 안전하게 escape
121+ * 따옴표, 꺾쇠괄호 등을 HTML 엔티티로 변환
122+ */
123+ function escapeHtmlAttribute ( value : string ) : string {
124+ return value
125+ . replace ( / & / g, '&' )
126+ . replace ( / " / g, '"' )
127+ . replace ( / ' / g, ''' )
128+ . replace ( / < / g, '<' )
129+ . replace ( / > / g, '>' ) ;
130+ }
131+
132+ /**
133+ * Breadcrumb 아이템을 HTML 문자열로 변환
134+ */
135+ function renderBreadcrumbItem ( item : BreadcrumbItem , isLast : boolean ) : string {
136+ const escapedName = escapeHtml ( item . name ) ;
137+ const escapedPath = escapeHtmlAttribute ( item . path ) ;
138+
139+ if ( isLast ) {
140+ return `<span class="truncate text-gray-400 dark:text-gray-300">${ escapedName } </span>` ;
141+ }
142+
143+ if ( item . linkable ) {
144+ return `<a href="${ escapedPath } " class="truncate text-blue-500 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 transition-colors">${ escapedName } </a> / ` ;
145+ }
146+
147+ // linkable=false인 항목은 회색으로 표시 (클릭 불가 시각 표시)
148+ return `<span class="truncate text-gray-500 dark:text-gray-400">${ escapedName } </span> / ` ;
149+ }
150+
110151/**
111152 * Breadcrumb HTML 요소를 생성합니다.
112153 */
113154function createBreadcrumbElement ( items : BreadcrumbItem [ ] ) : HTMLElement {
114155 const breadcrumbNav = document . createElement ( 'nav' ) ;
115156 breadcrumbNav . id = 'breadcrumbs' ;
116- breadcrumbNav . className =
117- 'pb-3 flex min-w-0 items-center gap-2 text-gray-400 dark:text-gray-300 ' ;
157+ // nav 요소에서 색상 클래스 제거 (자식 요소에서 색상 관리)
158+ breadcrumbNav . className = 'pb-3 flex min-w-0 items-center gap-2' ;
118159
119160 const breadcrumbHTML = items
120- . map ( ( item , index ) => {
121- const isLast = index === items . length - 1 ;
122-
123- if ( isLast ) {
124- return `<span class="truncate">${ item . name } </span>` ;
125- }
126-
127- if ( ! item . linkable ) {
128- return `<span class="truncate text-blue-500">${ item . name } </span> / ` ;
129- } else {
130- return `<a href="${ item . path } " class="link truncate">${ item . name } </a> / ` ;
131- }
132- } )
161+ . map ( ( item , index ) =>
162+ renderBreadcrumbItem ( item , index === items . length - 1 )
163+ )
133164 . join ( '' ) ;
134165
135166 breadcrumbNav . innerHTML = breadcrumbHTML ;
0 commit comments