From 9f58485a87a75ce07a1feb1af8dc94c86a9574da Mon Sep 17 00:00:00 2001 From: jungwoo choi Date: Sat, 14 Feb 2026 07:17:43 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20URL=20=EA=B2=BD=EB=A1=9C=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=EC=9C=BC=EB=A1=9C=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=ED=8A=B8=EB=A6=AC=20=EA=B3=84=EC=B8=B5=20=EA=B5=AC=EC=84=B1=20?= =?UTF-8?q?+=20=EC=A4=91=EB=B3=B5=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit parent_url(크롤링 발견 순서) 대신 URL path 계층으로 트리를 구성하여 /howyoutubeworks가 /about 하위로 잘못 표시되는 문제 수정. 동일 URL 중복 노드도 제거. Co-Authored-By: Claude Opus 4.6 --- .../components/site-inspection/PageTree.tsx | 65 ++++++++++++++++--- 1 file changed, 57 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/site-inspection/PageTree.tsx b/frontend/src/components/site-inspection/PageTree.tsx index 8fef722..ad254b3 100644 --- a/frontend/src/components/site-inspection/PageTree.tsx +++ b/frontend/src/components/site-inspection/PageTree.tsx @@ -18,9 +18,50 @@ interface PageTreeProps { aggregateScores: AggregateScores | null; } +/** + * URL 경로 기반으로 부모 페이지를 찾는다. + * 경로 세그먼트를 하나씩 올라가며 실제 존재하는 페이지를 찾고, + * 없으면 null (최상위)을 반환한다. + * + * 예: /about/press → /about (존재) → 부모 + * /howyoutubeworks → / (존재) → 부모 + * /creators/podcasts → / (존재, /creators 없음) → 부모 + */ +function findPathParent(url: string, pageUrls: Set): string | null { + try { + const parsed = new URL(url); + const path = parsed.pathname; + + // 루트 경로는 부모 없음 + if (path === "/" || path === "") return null; + + const cleanPath = path.endsWith("/") ? path.slice(0, -1) : path; + const segments = cleanPath.split("/").filter(Boolean); + + // 경로를 한 단계씩 올라가며 존재하는 페이지 찾기 + for (let i = segments.length - 1; i >= 1; i--) { + const parentPath = "/" + segments.slice(0, i).join("/"); + const parentUrl = `${parsed.origin}${parentPath}`; + if (pageUrls.has(parentUrl)) { + return parentUrl; + } + } + + // 루트 페이지 확인 (trailing slash 유무 모두) + const rootWithSlash = `${parsed.origin}/`; + const rootWithout = parsed.origin; + if (pageUrls.has(rootWithSlash) && rootWithSlash !== url) return rootWithSlash; + if (pageUrls.has(rootWithout) && rootWithout !== url) return rootWithout; + + return null; + } catch { + return null; + } +} + /** * 페이지 트리 사이드바 컴포넌트. - * flat 배열을 parent_url 기준으로 트리 구조로 변환하여 렌더링한다. + * flat 배열을 URL 경로 계층 기준으로 트리 구조로 변환하여 렌더링한다. */ export function PageTree({ pages, @@ -29,25 +70,33 @@ export function PageTree({ aggregateScores, }: PageTreeProps) { /** - * flat 배열에서 parent_url → children 맵 구성. - * root 노드(parent_url === null)를 최상위 자식으로 처리. + * URL 경로 기반으로 트리 구성. + * /about/press → /about 하위, /howyoutubeworks → / 하위 */ const { rootPages, childrenMap, allPagesMap } = useMemo(() => { const childrenMap = new Map(); const allPagesMap = new Map(); const rootPages: DiscoveredPage[] = []; + const pageUrls = new Set(); + // 중복 URL 제거 (첫 번째 등장만 유지) + const uniquePages: DiscoveredPage[] = []; for (const page of pages) { - allPagesMap.set(page.url, page); + if (!pageUrls.has(page.url)) { + pageUrls.add(page.url); + allPagesMap.set(page.url, page); + uniquePages.push(page); + } } - for (const page of pages) { - if (page.parent_url === null) { + for (const page of uniquePages) { + const parentUrl = findPathParent(page.url, pageUrls); + if (parentUrl === null) { rootPages.push(page); } else { - const siblings = childrenMap.get(page.parent_url) || []; + const siblings = childrenMap.get(parentUrl) || []; siblings.push(page); - childrenMap.set(page.parent_url, siblings); + childrenMap.set(parentUrl, siblings); } }