Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions frontend/public/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# 공유 피드백 링크(/share/<token>)는 "링크를 받은 사람만" 보라고 만든 것이다.
# 사용자가 그 링크를 블로그·커뮤니티에 붙이면 크롤러가 따라 들어와 리포트 전문
# (강점·약점·학습 방향 — 전부 본인 이력서에서 파생된 내용)을 색인한다.
# 프론트는 정적 파일로 배포돼 응답 헤더(X-Robots-Tag)를 붙일 수 없으므로
# 크롤링 자체를 막는다. 토큰은 UUIDv4 라 추측으로는 못 찾고, 유출 경로는 링크뿐이다.
User-agent: *
Disallow: /share/

# 로그인해야 내용이 보이는 경로 — 크롤러에겐 빈 셸만 나가므로 색인할 가치가 없다.
Disallow: /workspace
Disallow: /sessions/
Disallow: /auth/
3 changes: 3 additions & 0 deletions frontend/src/pages/SharedFeedback/ui/SharedFeedbackPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? '')

Expand Down
1 change: 1 addition & 0 deletions frontend/src/shared/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
19 changes: 19 additions & 0 deletions frontend/src/shared/hooks/useNoIndex.test.ts
Original file line number Diff line number Diff line change
@@ -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()
})
})
20 changes: 20 additions & 0 deletions frontend/src/shared/hooks/useNoIndex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useEffect } from 'react'

/**
* 이 페이지가 떠 있는 동안 검색엔진 색인을 거부한다.
*
* <p>`robots.txt` 로 크롤링을 막는 게 1차 방어지만 그건 규칙을 지키는 크롤러에게만
* 통한다. JS 는 실행하면서 robots.txt 는 무시하는 쪽을 위한 2차 방어다.
*
* <p>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()
}, [])
}
Loading