diff --git a/frontend/public/robots.txt b/frontend/public/robots.txt new file mode 100644 index 0000000..eeb8c1c --- /dev/null +++ b/frontend/public/robots.txt @@ -0,0 +1,12 @@ +# 공유 피드백 링크(/share/)는 "링크를 받은 사람만" 보라고 만든 것이다. +# 사용자가 그 링크를 블로그·커뮤니티에 붙이면 크롤러가 따라 들어와 리포트 전문 +# (강점·약점·학습 방향 — 전부 본인 이력서에서 파생된 내용)을 색인한다. +# 프론트는 정적 파일로 배포돼 응답 헤더(X-Robots-Tag)를 붙일 수 없으므로 +# 크롤링 자체를 막는다. 토큰은 UUIDv4 라 추측으로는 못 찾고, 유출 경로는 링크뿐이다. +User-agent: * +Disallow: /share/ + +# 로그인해야 내용이 보이는 경로 — 크롤러에겐 빈 셸만 나가므로 색인할 가치가 없다. +Disallow: /workspace +Disallow: /sessions/ +Disallow: /auth/ diff --git a/frontend/src/pages/SharedFeedback/ui/SharedFeedbackPage.tsx b/frontend/src/pages/SharedFeedback/ui/SharedFeedbackPage.tsx index 6ad6e46..cc9466e 100644 --- a/frontend/src/pages/SharedFeedback/ui/SharedFeedbackPage.tsx +++ b/frontend/src/pages/SharedFeedback/ui/SharedFeedbackPage.tsx @@ -3,9 +3,12 @@ import { SiteNav } from '@/widgets/site-nav' import { SiteFooter } from '@/widgets/site-footer' import { FeedbackReport, useSharedFeedback } from '@/features/feedback' import { PageHeader } from '@/shared/ui' +import { useNoIndex } from '@/shared/hooks' // 공유 토큰으로 피드백을 보는 공개(비로그인) 페이지. export default function SharedFeedbackPage() { + // 링크를 받은 사람만 보라고 만든 페이지다 — 검색 결과에 뜨면 안 된다. + useNoIndex() const { token } = useParams<{ token: string }>() const { data, isLoading, isError } = useSharedFeedback(token ?? '') diff --git a/frontend/src/shared/hooks/index.ts b/frontend/src/shared/hooks/index.ts index 3c23ce3..e437e86 100644 --- a/frontend/src/shared/hooks/index.ts +++ b/frontend/src/shared/hooks/index.ts @@ -3,3 +3,4 @@ export { useAnalysisProgress, analysisProgress } from './useAnalysisProgress' export type { AnalysisProgress } from './useAnalysisProgress' export { useCopyToClipboard } from './useCopyToClipboard' export { useQuestionRunner } from './useQuestionRunner' +export { useNoIndex } from './useNoIndex' diff --git a/frontend/src/shared/hooks/useNoIndex.test.ts b/frontend/src/shared/hooks/useNoIndex.test.ts new file mode 100644 index 0000000..91b073d --- /dev/null +++ b/frontend/src/shared/hooks/useNoIndex.test.ts @@ -0,0 +1,19 @@ +import { renderHook } from '@testing-library/react' +import { describe, expect, it } from 'vitest' +import { useNoIndex } from './useNoIndex' + +const robotsMeta = () => document.head.querySelector('meta[name="robots"]') + +describe('useNoIndex', () => { + it('마운트 동안 robots noindex 를 심는다', () => { + renderHook(() => useNoIndex()) + expect(robotsMeta()?.getAttribute('content')).toContain('noindex') + }) + + // 다른 페이지로 이동했는데 태그가 남으면 앱 전체가 색인에서 빠진다. + it('언마운트하면 걷어낸다', () => { + const { unmount } = renderHook(() => useNoIndex()) + unmount() + expect(robotsMeta()).toBeNull() + }) +}) diff --git a/frontend/src/shared/hooks/useNoIndex.ts b/frontend/src/shared/hooks/useNoIndex.ts new file mode 100644 index 0000000..fc1939f --- /dev/null +++ b/frontend/src/shared/hooks/useNoIndex.ts @@ -0,0 +1,20 @@ +import { useEffect } from 'react' + +/** + * 이 페이지가 떠 있는 동안 검색엔진 색인을 거부한다. + * + *

`robots.txt` 로 크롤링을 막는 게 1차 방어지만 그건 규칙을 지키는 크롤러에게만 + * 통한다. JS 는 실행하면서 robots.txt 는 무시하는 쪽을 위한 2차 방어다. + * + *

index.html 에 정적으로 넣을 수 없다 — SPA 라 문서가 하나뿐이라서, 그렇게 하면 + * 랜딩·소개 페이지까지 통째로 색인에서 빠진다. 그래서 페이지 단위로 붙였다 뗀다. + */ +export function useNoIndex(): void { + useEffect(() => { + const meta = document.createElement('meta') + meta.name = 'robots' + meta.content = 'noindex, nofollow, noarchive' + document.head.appendChild(meta) + return () => meta.remove() + }, []) +}