Skip to content

Comments

test: Vitest 테스트 자동화 도입#6

Merged
3rdflr merged 22 commits intomainfrom
dev
Feb 23, 2026
Merged

test: Vitest 테스트 자동화 도입#6
3rdflr merged 22 commits intomainfrom
dev

Conversation

@3rdflr
Copy link
Owner

@3rdflr 3rdflr commented Feb 23, 2026

요약

  • Vitest 테스트 환경 구축: Vitest + jsdom + @testing-library/react 설치 및 vitest.config.ts 설정
  • 유틸 함수 분리: GachaBoardPage, GachaPostDetail에 각각 인라인으로 존재하던 formatDate, formatPrice 함수를 src/lib/formatters.ts로 추출 — 중복 제거 및 테스트 가능 구조로 개선
  • 테스트 파일 3개 추가 (총 21개 테스트, 전부 통과):
파일 테스트 수 내용
wsrvLoader.test.ts 6개 wsrv.nl URL 변환, width/quality 파라미터, 특수문자 인코딩
formatters.test.ts 7개 한국어 날짜 형식, 일본/한국 가격 표시, 가격 미정 처리
useAuthStore.test.ts 8개 user/profile 상태 전환, 프로필 로드 성공/실패 (Supabase mock)
  • npm 스크립트 추가:
    • npm test — 전체 테스트 1회 실행
    • npm run test:watch — 파일 변경 감지 자동 재실행

테스트 체크리스트

  • npm test 실행 시 21개 테스트 전부 통과 확인
  • formatDate, formatPrice 기존 동작 유지 확인
  • Vercel 빌드 에러 없음 확인

🤖 Generated with Claude Code

- /api/delete-account Route Handler 추가 (Service Role Key로 auth 유저 완전 삭제)
- useAuthStore에 deleteAccount 액션 추가
- UserProfileModal 하단에 회원 탈퇴 UI 추가
  - '탈퇴' 직접 입력 후 버튼 활성화되는 이중 확인 방식
  - 탈퇴 완료 후 자동 로그아웃 및 메인 페이지 이동
- userId를 body로 받던 방식 제거 (타인 계정 삭제 취약점)
- Authorization 헤더의 액세스 토큰으로 실제 유저 검증 후 삭제
- 클라이언트에서 supabase.auth.getSession()으로 토큰을 헤더에 포함하여 전송
…ld-time failure

Next.js evaluates module-level code at build time, causing "supabaseKey is required"
error when SUPABASE_SERVICE_ROLE_KEY is not available during the build phase.
Moving createClient() inside the DELETE handler ensures it runs only at request time.
- GachaPostDetail: 상단 메인 이미지 배경색 bg-black → bg-gray-50 (페이지 배경과 통일)
- GachaPostEditor: Supabase 세션 비동기 로드 완료 전 관리자 체크로 인한
  무한 업로드/저장 상태 버그 수정 (authLoading 완료 후 판단하도록 변경)
- adminChecked ref 추가로 관리자 체크를 최초 1회만 수행하도록 변경
- 다른 창으로 이동 후 복귀 시 onAuthStateChange가 재발생해 profile이
  순간 null이 되어 관리자 체크 useEffect가 재실행되던 문제 수정
- 렌더 단의 is_admin 체크도 adminChecked 이후에만 적용되도록 가드 추가
- ISR 캐싱 적용: 메인 페이지(5분), 가챠보드(1분)
- 카카오맵 스크립트를 afterInteractive로 변경해 초기 렌더 차단 제거
- GachaPostEditor를 dynamic import로 분리해 어드민 번들 지연 로드
- GachaPostDetail 첫 번째 이미지 중복 렌더링 제거
- 리뷰 쿼리에 limit(10) 추가
- supabase.ts, Map.tsx의 debug console.log 제거
- useAuthStore: 동일 유저+프로필 로드 완료 시 SIGNED_IN 재처리 생략으로 페이지 이동 시 불필요한 로딩 제거
- Header: 모바일 로고 폰트 축소, 사이드바 네비게이션으로 개편
- Vitest + jsdom + @testing-library/react 설치 및 설정
- vitest.config.ts 추가 (@경로 alias 포함)
- src/lib/formatters.ts: formatDate, formatPrice 유틸 함수 분리
- GachaBoardPage, GachaPostDetail에서 인라인 함수 제거 후 공유 유틸 사용
- 테스트 파일 3개 추가 (총 21개 테스트):
  - wsrvLoader.test.ts: URL 변환, 파라미터, 특수문자 인코딩
  - formatters.test.ts: 날짜 형식, 가격 표시 (일본/한국/미정)
  - useAuthStore.test.ts: 상태 전환, 프로필 로드 성공/실패
- package.json에 test, test:watch 스크립트 추가
@vercel
Copy link

vercel bot commented Feb 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
gacha-map Ready Ready Preview, Comment Feb 23, 2026 6:24am

as any 대신 User 타입 및 vi.mocked() 사용으로 빌드 에러 해결
Repository owner deleted a comment from github-actions bot Feb 23, 2026
github-script uses Issues API for PR comments — requires issues: write
github-script fails silently on rerun — gh CLI is more reliable
and works correctly regardless of event context
@github-actions
Copy link

PR #6 — test: Vitest 테스트 자동화 도입

3rdflr/gacha_map · 1c2d616 · JS/TS 24개 파일 변경


src/app/api/delete-account/route.ts

Import 변화

  • createClient
  • NextResponse

DELETE (Function) — 새로 추가
변수: supabaseAdmin, authHeader, accessToken
파라미터+ request


src/app/gacha-board/[id]/page.tsx

Import 변화

  • Metadata
  • supabase

generateMetadata (Function) — 새로 추가
변수: brandLabel, title, description, imageUrl
파라미터+ { params }
Props+ title: '가챠 정보'
Props+ description: '...'

  • (cond) !post

src/app/gacha-board/create/page.tsx

Import 변화

  • dynamic
  • GachaPostEditor (제거됨)

GachaPostEditor (Component) — 새로 추가
Props+ ssr: false
Props+ loading: () => (


src/app/gacha-board/edit/[id]/page.tsx

Import 변화

  • dynamic
  • GachaPostEditor (제거됨)

GachaPostEditor (Component) — 새로 추가
Props+ ssr: false
Props+ loading: () => (


src/app/layout.tsx

✏️ geistMono (Function) — 변경됨
Props+ title: {
Props+ default: '...'
Props+ template: '%s | 가챠 지도'
Props+ authors: [{ name: '가챠 지도' }]
Props+ creator: '가챠 지도'
Props+ … 외 1개
Props- title: '국내 가챠 지도'
Props- authors: [{ name: '국내 가챠 지도' }]
Props- creator: '국내 가챠 지도'
Props- publisher: '국내 가챠 지도'

✏️ metadata (Function) — 변경됨
Props+ title: '...'
Props+ description: '...'
Props+ siteName: '가챠 지도'
Props+ url: '...'
Props+ alt: '전국 가챠·쿠지 위치 지도'
Props- title: '국내 가챠 지도'
Props- description: '...'
Props- siteName: '사이트 이름'
Props- url: 'og-image.png'
Props- alt: '가챠 굿즈 사이트 이미지'

✏️ RootLayout (Function) — 변경됨


src/components/GachaBoardPage.tsx

Import 변화

  • formatDate
  • formatPrice

✏️ GachaBoardPage (Function) — 변경됨
변수: isAdmin
파라미터- dateString
파라미터- price
파라미터- country
Props- year: 'numeric'
Props- month: 'long'
Props- day: 'numeric'

  • (guard) !price → early return

src/components/GachaPostDetail.tsx

Import 변화

  • GoogleAd
  • formatDate
  • formatPrice
  • ChevronLeft (제거됨)
  • ChevronRight (제거됨)

✏️ GachaPostDetail (Function) — 변경됨
파라미터- dateString
파라미터- price
파라미터- country
Props- year: 'numeric'
Props- month: 'long'
Props- day: 'numeric'

  • state currentImageIdx 제거
  • (guard) !price → early return
  • (return) country === '일본'

✏️ fetchPost (Function) — 변경됨
변수: isAdmin, mainImage, detailImages, captionIdx


src/components/GachaPostEditor.tsx

Import 변화

  • useRef

✏️ GachaPostEditor (Component) — 변경됨
Props+ caption: existingCaptions[idx] || ''

  • (guard) authLoading → early return
  • (guard) adminChecked.current && !profile?.is_admin → early return
  • (cond) data.image_url) existingUrls.push(data.image_url
  • (guard) !profile?.is_admin → early return
    UI: <textarea>

handleCaptionChange (Function) — 새로 추가
변수: router, isMounted, adminChecked
파라미터+ id
파라미터+ caption

  • (setState) setUploadItems((prev)

uploadOne (Function) — 새로 추가
변수: canvas, uploadFile, files, publicUrl, doneItems 외 3개

  • (cond) !isMounted.current) throw new Error('unmounted'

src/components/Header.tsx

Import 변화

  • X
  • Map
  • Newspaper

✏️ Header (Component) — 변경됨
변수: displayName, displayAvatar

  • state isSidebarOpen = false 추가
    UI: <Image>
    UI: <button>
    UI: <X>
    UI: <Link>

src/components/Map.tsx

✏️ loadKakaoMap (Function) — 변경됨

✏️ KakaoMap (Function) — 변경됨


src/components/ShopDetailView.tsx

✏️ ShopDetailView (Function) — 변경됨

  • useEffect [shop.id] 변경 시 실행
  • fetchReviews()

src/components/UserProfileModal.tsx

Import 변화

  • Trash2
  • useAuthStore
  • useRouter

✏️ UserProfileModal (Component) — 변경됨

  • (setState) setShowDeleteConfirm(false)
  • (setState) setDeleteConfirmInput('')
    UI: <button>
    UI: <Trash2>
    UI: <input>

✏️ loadProfile (Function) — 변경됨
변수: router

✏️ handleDeleteAccount (Function) — 변경됨
변수: file, filePath

  • (guard) deleteConfirmInput !== '탈퇴' → early return
  • (setState) setIsDeleting(true)
  • (API) deleteAccount()
  • alert('회원 탈퇴가 완료됐습니다.')
  • onClose()
  • alert()
  • (setState) setIsDeleting(false)

src/lib/formatters.ts

formatDate (Function) — 새로 추가
파라미터+ dateString
Props+ year: 'numeric'
Props+ month: 'long'
Props+ day: 'numeric'

formatPrice (Function) — 새로 추가
파라미터+ price
파라미터+ country

  • (guard) !price → early return

src/lib/supabase.ts

✏️ getVerifiedShops (Function) — 변경됨


src/test/formatters.test.ts

테스트: formatDate

  • 날짜 문자열을 한국어 형식으로 변환한다
  • 1자리 월/일도 올바르게 변환한다

테스트: formatPrice

  • 일본 가격은 엔 단위로 표시한다
  • 일본 가격에 천 단위 구분자를 적용한다
  • 한국 가격은 원 단위로 표시한다
  • price가 0이면 "가격 미정"을 반환한다
  • 알 수 없는 국가는 원 단위로 표시한다

src/test/useAuthStore.test.ts

테스트: setUser / setProfile

  • setUser로 user를 설정한다
  • setUser(null)로 user를 초기화한다
  • setProfile로 profile을 설정한다
  • setProfile(null)로 profile을 초기화한다

테스트: loadProfile

  • 프로필 로드 성공 시 profile 상태를 업데이트한다
  • 프로필 로드 실패 시 profile을 null로 설정한다

테스트: 초기 상태

  • 초기 user는 null이다
  • 초기 profile은 null이다

src/test/wsrvLoader.test.ts

테스트: wsrvLoader

  • 기본 URL을 wsrv.nl 형식으로 변환한다
  • quality가 없으면 기본값 75를 사용한다
  • 지정한 quality 값을 사용한다
  • width 값이 URL에 포함된다
  • output은 항상 webp이다
  • src에 특수문자가 있으면 인코딩한다

vitest.config.ts

플러그인/설정 추가

  • defineConfig
  • react
  • path

🛠 Auto-generated by github-mobile-reader. Do not edit manually.

@github-actions
Copy link

PR #6 — test: Vitest 테스트 자동화 도입

3rdflr/gacha_map · 1c2d616 · JS/TS 24개 파일 변경


src/app/api/delete-account/route.ts

Import 변화

  • createClient
  • NextResponse

DELETE (Function) — 새로 추가
변수: supabaseAdmin, authHeader, accessToken
파라미터+ request


src/app/gacha-board/[id]/page.tsx

Import 변화

  • Metadata
  • supabase

generateMetadata (Function) — 새로 추가
변수: brandLabel, title, description, imageUrl
파라미터+ { params }
Props+ title: '가챠 정보'
Props+ description: '...'

  • (cond) !post

src/app/gacha-board/create/page.tsx

Import 변화

  • dynamic
  • GachaPostEditor (제거됨)

GachaPostEditor (Component) — 새로 추가
Props+ ssr: false
Props+ loading: () => (


src/app/gacha-board/edit/[id]/page.tsx

Import 변화

  • dynamic
  • GachaPostEditor (제거됨)

GachaPostEditor (Component) — 새로 추가
Props+ ssr: false
Props+ loading: () => (


src/app/layout.tsx

✏️ geistMono (Function) — 변경됨
Props+ title: {
Props+ default: '...'
Props+ template: '%s | 가챠 지도'
Props+ authors: [{ name: '가챠 지도' }]
Props+ creator: '가챠 지도'
Props+ … 외 1개
Props- title: '국내 가챠 지도'
Props- authors: [{ name: '국내 가챠 지도' }]
Props- creator: '국내 가챠 지도'
Props- publisher: '국내 가챠 지도'

✏️ metadata (Function) — 변경됨
Props+ title: '...'
Props+ description: '...'
Props+ siteName: '가챠 지도'
Props+ url: '...'
Props+ alt: '전국 가챠·쿠지 위치 지도'
Props- title: '국내 가챠 지도'
Props- description: '...'
Props- siteName: '사이트 이름'
Props- url: 'og-image.png'
Props- alt: '가챠 굿즈 사이트 이미지'

✏️ RootLayout (Function) — 변경됨


src/components/GachaBoardPage.tsx

Import 변화

  • formatDate
  • formatPrice

✏️ GachaBoardPage (Function) — 변경됨
변수: isAdmin
파라미터- dateString
파라미터- price
파라미터- country
Props- year: 'numeric'
Props- month: 'long'
Props- day: 'numeric'

  • (guard) !price → early return

src/components/GachaPostDetail.tsx

Import 변화

  • GoogleAd
  • formatDate
  • formatPrice
  • ChevronLeft (제거됨)
  • ChevronRight (제거됨)

✏️ GachaPostDetail (Function) — 변경됨
파라미터- dateString
파라미터- price
파라미터- country
Props- year: 'numeric'
Props- month: 'long'
Props- day: 'numeric'

  • state currentImageIdx 제거
  • (guard) !price → early return
  • (return) country === '일본'

✏️ fetchPost (Function) — 변경됨
변수: isAdmin, mainImage, detailImages, captionIdx


src/components/GachaPostEditor.tsx

Import 변화

  • useRef

✏️ GachaPostEditor (Component) — 변경됨
Props+ caption: existingCaptions[idx] || ''

  • (guard) authLoading → early return
  • (guard) adminChecked.current && !profile?.is_admin → early return
  • (guard) !profile?.is_admin → early return
  • (cond) data.image_url) existingUrls.push(data.image_url
    UI: <textarea>

handleCaptionChange (Function) — 새로 추가
변수: router, isMounted, adminChecked
파라미터+ id
파라미터+ caption

  • (setState) setUploadItems((prev)

uploadOne (Function) — 새로 추가
변수: canvas, uploadFile, files, publicUrl, doneItems 외 3개

  • (cond) !isMounted.current) throw new Error('unmounted'

src/components/Header.tsx

Import 변화

  • X
  • Map
  • Newspaper

✏️ Header (Component) — 변경됨
변수: displayName, displayAvatar

  • state isSidebarOpen = false 추가
    UI: <Image>
    UI: <button>
    UI: <X>
    UI: <Link>

src/components/Map.tsx

✏️ loadKakaoMap (Function) — 변경됨
✏️ KakaoMap (Function) — 변경됨


src/components/ShopDetailView.tsx

✏️ ShopDetailView (Function) — 변경됨

  • (state) useEffect called

src/components/UserProfileModal.tsx

Import 변화

  • Trash2
  • useAuthStore
  • useRouter

✏️ UserProfileModal (Component) — 변경됨

  • (setState) setShowDeleteConfirm(false)
  • (setState) setDeleteConfirmInput('')
    UI: <button>
    UI: <Trash2>
    UI: <input>

✏️ loadProfile (Function) — 변경됨
변수: router

✏️ handleDeleteAccount (Function) — 변경됨
변수: file, filePath

  • (API) deleteAccount()
  • (guard) deleteConfirmInput !== '탈퇴' → early return
  • (setState) setIsDeleting(true)
  • (setState) setIsDeleting(false)

src/lib/formatters.ts

formatDate (Function) — 새로 추가
파라미터+ dateString
Props+ year: 'numeric'
Props+ month: 'long'
Props+ day: 'numeric'

formatPrice (Function) — 새로 추가
파라미터+ price
파라미터+ country

  • (guard) !price → early return

src/lib/supabase.ts

✏️ getVerifiedShops (Function) — 변경됨


src/test/formatters.test.ts

테스트: formatDate

  • 날짜 문자열을 한국어 형식으로 변환한다
  • 1자리 월/일도 올바르게 변환한다

테스트: formatPrice

  • 일본 가격은 엔 단위로 표시한다
  • 일본 가격에 천 단위 구분자를 적용한다
  • 한국 가격은 원 단위로 표시한다
  • price가 0이면 "가격 미정"을 반환한다
  • 알 수 없는 국가는 원 단위로 표시한다

src/test/useAuthStore.test.ts

테스트: setUser / setProfile

  • setUser로 user를 설정한다
  • setUser(null)로 user를 초기화한다
  • setProfile로 profile을 설정한다
  • setProfile(null)로 profile을 초기화한다

테스트: loadProfile

  • 프로필 로드 성공 시 profile 상태를 업데이트한다
  • 프로필 로드 실패 시 profile을 null로 설정한다

테스트: 초기 상태

  • 초기 user는 null이다
  • 초기 profile은 null이다

src/test/wsrvLoader.test.ts

테스트: wsrvLoader

  • 기본 URL을 wsrv.nl 형식으로 변환한다
  • quality가 없으면 기본값 75를 사용한다
  • 지정한 quality 값을 사용한다
  • width 값이 URL에 포함된다
  • output은 항상 webp이다
  • src에 특수문자가 있으면 인코딩한다

vitest.config.ts

플러그인/설정 추가

  • defineConfig
  • react
  • path

🛠 Auto-generated by github-mobile-reader. Do not edit manually.

@3rdflr 3rdflr merged commit 85c9d3e into main Feb 23, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant