From 12136df8ed5d3de611f507254d24c18b70caaac9 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 1 Sep 2026 14:01:10 +0900 Subject: [PATCH 01/13] =?UTF-8?q?fix:=20=EC=97=94=EB=B9=B5=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=ED=9B=85=20?= =?UTF-8?q?=EA=B7=9C=EC=B9=99=20=EC=9C=84=EB=B0=98=20=EB=B0=8F=20=EC=98=B5?= =?UTF-8?q?=EC=85=94=EB=84=90=20=EC=B2=B4=EC=9D=B4=EB=8B=9D=20=EB=8B=A8?= =?UTF-8?q?=EC=96=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 컴포넌트 함수명이 소문자여서 react-hooks/rules-of-hooks 가 15건 발생하고 있었다. 파일명과 임포트하는 쪽은 이미 NbreadDetail 이므로 선언부만 대문자로 맞춘다. 탈퇴 처리의 userData?.id! 는 옵셔널 체이닝 결과에 비널 단언을 붙여 undefined 가 그대로 전달될 수 있었다. 호출 전에 가드를 두어 기존 실패 경로와 동일하게 catch 로 떨어지도록 한다. Co-Authored-By: Claude Opus 5 (1M context) --- src/components/nbread/NbreadDetail.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/nbread/NbreadDetail.tsx b/src/components/nbread/NbreadDetail.tsx index 4b46693..3a9d8b5 100644 --- a/src/components/nbread/NbreadDetail.tsx +++ b/src/components/nbread/NbreadDetail.tsx @@ -26,12 +26,12 @@ import QuitNbreadModal from '@/components/common/modal/QuitNbreadModal' import Spinner from '@/components/common/spinner/Spinner' import InviteBottomSheet from '@/components/invite/InviteBottomSheet' import { getFriendList } from '@/lib/friend/getSearchFriend' -interface nbreadDetailProps { +interface NbreadDetailProps { nbreadData: Nbread setNbreadData: Dispatch> } -const nbreadDetail = ({ nbreadData, setNbreadData }: nbreadDetailProps) => { +const NbreadDetail = ({ nbreadData, setNbreadData }: NbreadDetailProps) => { const userData = useUserStore((state) => state.user) const [nbreadRecords, setNbreadRecords] = useState( null, @@ -114,7 +114,8 @@ const nbreadDetail = ({ nbreadData, setNbreadData }: nbreadDetailProps) => { // 엔빵 탈퇴 처리 함수 const onSubmitQuitNbread = async () => { try { - await deleteParticipants(userData?.id!, nbreadData!.id) + if (!userData?.id) throw new Error('사용자 정보를 찾을 수 없어요.') + await deleteParticipants(userData.id, nbreadData!.id) setIsQuitNbreadModalOpen(false) useToast.success('엔빵 나가기에 성공했어요.') router.replace('/home') @@ -234,4 +235,4 @@ const nbreadDetail = ({ nbreadData, setNbreadData }: nbreadDetailProps) => { ) } -export default nbreadDetail +export default NbreadDetail From 21b0e1727f96d7ed63e77e4700562ff84ba817ff Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 1 Sep 2026 14:01:24 +0900 Subject: [PATCH 02/13] =?UTF-8?q?refactor:=20=EB=AA=85=EC=8B=9C=EC=A0=81?= =?UTF-8?q?=20any=20=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20Supabase=20Row=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @typescript-eslint/no-explicit-any 위반 6건을 실제 타입으로 대체한다. - not-found: window 확장 속성을 global.d.ts 의 Window 선언으로 옮김 - Community: mapToPost 인자를 PostRow 로 지정. post 테이블의 nullable 컬럼과 Post 타입의 불일치가 드러나 빈 문자열로 보정 - getSearchFriend: inviteData 를 조회 컬럼 기준 Pick 타입으로 지정 - deletePost, updatePost: 호출부가 넘기는 값에 맞춰 인자 타입 지정 Co-Authored-By: Claude Opus 5 (1M context) --- global.d.ts | 1 + src/app/not-found.tsx | 4 ++-- src/components/community/Community.tsx | 11 ++++++----- src/lib/friend/getSearchFriend.ts | 8 +++++++- src/lib/post/deletePost.ts | 2 +- src/lib/post/updatePost.ts | 3 ++- src/types/supabase.ts | 3 +++ 7 files changed, 22 insertions(+), 10 deletions(-) diff --git a/global.d.ts b/global.d.ts index 33c5e05..f917d04 100644 --- a/global.d.ts +++ b/global.d.ts @@ -19,6 +19,7 @@ export declare global { targetIdOrEventName: string | Date, params?: Record, ) => void + __IS_NOT_FOUND_PAGE__?: boolean } interface Navigator { diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index 22c0603..c4c387c 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -8,9 +8,9 @@ const Page = () => { const router = useRouter() useEffect(() => { - ;(window as any).__IS_NOT_FOUND_PAGE__ = true + window.__IS_NOT_FOUND_PAGE__ = true return () => { - ;(window as any).__IS_NOT_FOUND_PAGE__ = false + window.__IS_NOT_FOUND_PAGE__ = false } }, []) diff --git a/src/components/community/Community.tsx b/src/components/community/Community.tsx index 26381fe..87881b3 100644 --- a/src/components/community/Community.tsx +++ b/src/components/community/Community.tsx @@ -1,4 +1,5 @@ import { Post } from '@/types/post' +import { PostRow } from '@/types/supabase' import PostCard from './PostCard' import { useState } from 'react' import CreatePostButton from './CreatePostButton' @@ -49,16 +50,16 @@ const Community = () => { const [hasFetched, setHasFetched] = useState(false) const params = useParams() const nbreadId = params.nbreadId as string - const mapToPost = (raw: any): Post => ({ + const mapToPost = (raw: PostRow): Post => ({ id: raw.id, - content: raw.content, - userName: raw.user_name, - userProfileImage: raw.profile_image, + content: raw.content ?? '', + userName: raw.user_name ?? '', + userProfileImage: raw.profile_image ?? '', createdAt: new Date(raw.created_at) .toISOString() .slice(0, 10) .replace(/-/g, '.'), - userId: raw.user_id, + userId: raw.user_id ?? '', nbreadId: raw.nbread_id, }) const fetchPosts = async () => { diff --git a/src/lib/friend/getSearchFriend.ts b/src/lib/friend/getSearchFriend.ts index 77020cb..6644990 100644 --- a/src/lib/friend/getSearchFriend.ts +++ b/src/lib/friend/getSearchFriend.ts @@ -1,4 +1,10 @@ import { supabase } from '../supabaseClient' +import { NbreadInviteRow } from '@/types/supabase' + +type NbreadInviteSummary = Pick< + NbreadInviteRow, + 'status' | 'target_user_id' | 'created_at' +> export interface FriendListItem { name: string @@ -95,7 +101,7 @@ export const getFriendList = async ( })) const friendIds = processedFriends.map((f) => f.id) - let inviteData: any[] = [] + let inviteData: NbreadInviteSummary[] = [] if (nbreadId) { const { data, error } = await supabase .from('nbread_invite') diff --git a/src/lib/post/deletePost.ts b/src/lib/post/deletePost.ts index d597070..b7bf444 100644 --- a/src/lib/post/deletePost.ts +++ b/src/lib/post/deletePost.ts @@ -1,5 +1,5 @@ import { supabase } from '../supabaseClient' -export const deletePost = async (post: any) => { +export const deletePost = async (post: number) => { try { const { data, error } = await supabase .from('post') diff --git a/src/lib/post/updatePost.ts b/src/lib/post/updatePost.ts index 67c5fde..4ec92d0 100644 --- a/src/lib/post/updatePost.ts +++ b/src/lib/post/updatePost.ts @@ -1,6 +1,7 @@ import { supabase } from "../supabaseClient" +import { Post } from '@/types/post' -export const UpdatePost = async (post : any) => { +export const UpdatePost = async (post : Pick) => { try { const { data, error } = await supabase .from('post') diff --git a/src/types/supabase.ts b/src/types/supabase.ts index 2c09f90..a5a26c4 100644 --- a/src/types/supabase.ts +++ b/src/types/supabase.ts @@ -530,3 +530,6 @@ export type NotificationRow = Database['public']['Tables']['notification']['Row'] export type FriendRequestRow = Database['public']['Tables']['friend_request']['Row'] +export type PostRow = Database['public']['Tables']['post']['Row'] +export type NbreadInviteRow = + Database['public']['Tables']['nbread_invite']['Row'] From 597b7f8a3f399f156a44060fa75a0bb9b3c93082 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 1 Sep 2026 14:01:38 +0900 Subject: [PATCH 03/13] =?UTF-8?q?ci:=20PR=20=EB=8B=A8=EC=9C=84=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EC=9B=8C=ED=81=AC=ED=94=8C=EB=A1=9C=EC=9A=B0=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit develop, main 으로 향하는 PR 에서 타입 체크, 린트, 단위 테스트를 실행한다. 빠르게 실패하는 순서로 배치해 타입 오류를 가장 먼저 끊는다. 릴리즈 노트 워크플로우와 트리거가 겹치지 않도록 별도 파일로 분리했고, E2E 는 실행 시간과 외부 의존이 달라 같은 워크플로우에 합치지 않는다. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci-unit-test.yml | 41 ++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 42 insertions(+) create mode 100644 .github/workflows/ci-unit-test.yml diff --git a/.github/workflows/ci-unit-test.yml b/.github/workflows/ci-unit-test.yml new file mode 100644 index 0000000..0742cc7 --- /dev/null +++ b/.github/workflows/ci-unit-test.yml @@ -0,0 +1,41 @@ +name: CI - Unit Test + +on: + pull_request: + branches: [develop, main] + +permissions: + contents: read + +concurrency: + group: ci-unit-test-${{ github.head_ref }} + cancel-in-progress: true + +jobs: + verify: + name: unit-verify + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + # 빠르게 실패하는 순서로 배치한다. 타입 오류는 lint / test 를 돌리기 전에 끊는다. + - name: Type check + run: npm run typecheck + + - name: Lint + run: npm run lint + + - name: Unit test + run: npm test diff --git a/package.json b/package.json index 5a613ee..a00fb10 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "postbuild": "next-sitemap", "start": "next start", "lint": "next lint", + "typecheck": "tsc --noEmit", "test:e2e": "playwright test", "generate-sitemap": "next-sitemap", "update-types": "supabase gen types typescript --project-id yyisakaqnaoomehqlyjz --schema public > src/types/supabase.ts" From 0b968a961e87c5151b5bbb249253dbafc4cb9c5d Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 1 Sep 2026 14:30:52 +0900 Subject: [PATCH 04/13] =?UTF-8?q?chore:=20Vitest=20=EB=8B=A8=EC=9C=84=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=ED=99=98=EA=B2=BD=20=EB=8F=84?= =?UTF-8?q?=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 테스트 러너가 없어 CI 검증 게이트의 test 단계를 채울 수 없었다. vitest 만 추가한다. 경로 별칭은 Vite 가 기본 지원하는 resolve.tsconfigPaths 로 해결해 vite-tsconfig-paths 플러그인은 두지 않는다. 커버리지 리포터도 수치를 성과로 쓰지 않기로 해 넣지 않는다. 설정은 vitest.config.mts 로 둔다. .ts 로 두면 CommonJS 로 로드되면서 ESM 구문 경고가 발생한다. Co-Authored-By: Claude Opus 5 (1M context) --- package-lock.json | 1138 +++++++++++++++++++++++++++++++++++++++++++-- package.json | 5 +- vitest.config.mts | 15 + 3 files changed, 1115 insertions(+), 43 deletions(-) create mode 100644 vitest.config.mts diff --git a/package-lock.json b/package-lock.json index 974f8be..810f7b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,7 +43,8 @@ "prettier-plugin-tailwindcss": "^0.6.11", "supabase": "^2.31.8", "tailwindcss": "^3.4.1", - "typescript": "^5" + "typescript": "^5", + "vitest": "^4.1.11" } }, "node_modules/@alloc/quick-lru": { @@ -3899,6 +3900,16 @@ "@opentelemetry/api": "^1.1.0" } }, + "node_modules/@oxc-project/types": { + "version": "0.147.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.147.0.tgz", + "integrity": "sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -4042,6 +4053,286 @@ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", "license": "BSD-3-Clause" }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.6.tgz", + "integrity": "sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.6.tgz", + "integrity": "sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.6.tgz", + "integrity": "sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.6.tgz", + "integrity": "sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.6.tgz", + "integrity": "sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.6.tgz", + "integrity": "sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.6.tgz", + "integrity": "sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.6.tgz", + "integrity": "sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.6.tgz", + "integrity": "sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.6.tgz", + "integrity": "sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.6.tgz", + "integrity": "sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.6.tgz", + "integrity": "sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.6.tgz", + "integrity": "sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.6.tgz", + "integrity": "sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.6.tgz", + "integrity": "sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/plugin-commonjs": { "version": "28.0.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.1.tgz", @@ -5032,6 +5323,13 @@ "webpack": ">=5.0.0" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@supabase/auth-js": { "version": "2.89.0", "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.89.0.tgz", @@ -5413,6 +5711,17 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -5422,6 +5731,13 @@ "@types/node": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -6090,6 +6406,129 @@ "win32" ] }, + "node_modules/@vitest/expect": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.11", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.11", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.11", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -6621,6 +7060,16 @@ "safer-buffer": "^2.1.0" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -7028,6 +7477,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -7579,8 +8038,8 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, "license": "Apache-2.0", - "optional": true, "engines": { "node": ">=8" } @@ -7891,8 +8350,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/es-object-atoms": { "version": "1.1.1", @@ -8409,6 +8867,16 @@ "node": ">=0.8.x" } }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -9930,38 +10398,311 @@ "json-buffer": "3.0.1" } }, - "node_modules/language-subtag-registry": { - "version": "0.3.23", - "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", - "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "CC0-1.0" + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/language-tags": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", - "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "language-subtag-registry": "^0.3.20" - }, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=0.10" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 0.8.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, "node_modules/lilconfig": { @@ -10223,9 +10964,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "funding": [ { "type": "github", @@ -10612,6 +11353,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -10773,6 +11528,13 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/pg-int8": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", @@ -10879,7 +11641,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -10901,9 +11662,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "funding": [ { "type": "opencollective", @@ -10920,7 +11681,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -11596,6 +12357,40 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rolldown": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.6.tgz", + "integrity": "sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.147.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.6", + "@rolldown/binding-android-arm64": "1.2.6", + "@rolldown/binding-darwin-arm64": "1.2.6", + "@rolldown/binding-darwin-x64": "1.2.6", + "@rolldown/binding-freebsd-x64": "1.2.6", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.6", + "@rolldown/binding-linux-arm64-gnu": "1.2.6", + "@rolldown/binding-linux-arm64-musl": "1.2.6", + "@rolldown/binding-linux-ppc64-gnu": "1.2.6", + "@rolldown/binding-linux-s390x-gnu": "1.2.6", + "@rolldown/binding-linux-x64-gnu": "1.2.6", + "@rolldown/binding-linux-x64-musl": "1.2.6", + "@rolldown/binding-openharmony-arm64": "1.2.6", + "@rolldown/binding-win32-arm64-msvc": "1.2.6", + "@rolldown/binding-win32-x64-msvc": "1.2.6" + } + }, "node_modules/rollup": { "version": "4.60.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", @@ -12021,6 +12816,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -12081,6 +12883,13 @@ "dev": true, "license": "MIT" }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/stacktrace-parser": { "version": "0.1.11", "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz", @@ -12093,6 +12902,13 @@ "node": ">=6" } }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -12699,15 +13515,32 @@ "node": ">=0.8" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -12735,9 +13568,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", "engines": { @@ -12747,6 +13580,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -13083,6 +13926,200 @@ "dev": true, "license": "MIT" }, + "node_modules/vite": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", + "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.26", + "rolldown": "~1.2.4", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0 || ^0.5.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/watchpack": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", @@ -13366,6 +14403,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/package.json b/package.json index a00fb10..db92b23 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,8 @@ "start": "next start", "lint": "next lint", "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest", "test:e2e": "playwright test", "generate-sitemap": "next-sitemap", "update-types": "supabase gen types typescript --project-id yyisakaqnaoomehqlyjz --schema public > src/types/supabase.ts" @@ -49,6 +51,7 @@ "prettier-plugin-tailwindcss": "^0.6.11", "supabase": "^2.31.8", "tailwindcss": "^3.4.1", - "typescript": "^5" + "typescript": "^5", + "vitest": "^4.1.11" } } diff --git a/vitest.config.mts b/vitest.config.mts new file mode 100644 index 0000000..2660848 --- /dev/null +++ b/vitest.config.mts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + // tsconfig 의 "@/*" 경로 별칭을 Vite 기본 기능으로 해석한다. + resolve: { + tsconfigPaths: true, + }, + test: { + environment: 'node', + include: ['src/**/*.test.ts'], + // include 만으로도 e2e 의 *.spec.ts 는 걸리지 않는다. + // 테스트 위치가 늘어나도 Playwright 스펙을 집어가지 않도록 방어로 남긴다. + exclude: ['e2e/**', 'node_modules/**'], + }, +}) From 8297c857b3ed5302f09bd82e74349f1132323e79 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 1 Sep 2026 14:34:13 +0900 Subject: [PATCH 05/13] =?UTF-8?q?refactor:=201=EC=9D=B8=EB=8B=B9=20?= =?UTF-8?q?=EC=A0=95=EC=82=B0=20=EA=B8=88=EC=95=A1=20=EA=B3=84=EC=82=B0=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EC=B6=9C=20=EB=B0=8F=20=EB=8F=99?= =?UTF-8?q?=EC=9E=91=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 같은 계산이 세 곳에 중복돼 있었고 0 나눗셈 가드가 서로 달랐다. 참여 인원이 0일 때 getUserTotalNbreadAmount 는 총 금액, nbreadCard 는 0, home/NbreadCard 는 Infinity 를 반환해 화면에 그대로 노출될 수 있었다. calculateIndividualShare 로 추출하고 가장 방어적이던 동작으로 통일한다. 참여 인원이 0 이하이거나 숫자가 아니면 1명으로 취급하고, 금액이 유한하지 않으면 0을 반환한다. home/NbreadCard 의 Infinity 노출 경로가 사라지고, nbreadCard 는 참여 인원이 0일 때 0 대신 총 금액을 표시하도록 바뀐다. Co-Authored-By: Claude Opus 5 (1M context) --- src/components/home/NbreadCard.tsx | 6 +++++- src/components/nbread/nbreadCard.tsx | 7 +++++-- src/lib/nbread/calculateIndividualShare.ts | 15 +++++++++++++++ src/lib/nbread/getUserTotalNbreadAmount.ts | 3 ++- src/lib/nbread/index.ts | 1 + 5 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 src/lib/nbread/calculateIndividualShare.ts diff --git a/src/components/home/NbreadCard.tsx b/src/components/home/NbreadCard.tsx index b167745..51d171f 100644 --- a/src/components/home/NbreadCard.tsx +++ b/src/components/home/NbreadCard.tsx @@ -1,4 +1,5 @@ import Avatar from '@/components/common/avatar/avatar' +import { calculateIndividualShare } from '@/lib/nbread/calculateIndividualShare' import { Nbread } from '@/types/nbread' import { useRouter } from 'next/navigation' @@ -23,7 +24,10 @@ const NbreadCard = ({ nbread, showParticipants = true }: NbreadCardProps) => {

{nbread.title}

- {Math.floor(nbread.amount / nbread.participantCount).toLocaleString()} + {calculateIndividualShare( + nbread.amount, + nbread.participantCount, + ).toLocaleString()} 원 /{nbread.paymentPeriod === 'year' ? ' 매년' : ' 매월'}

diff --git a/src/components/nbread/nbreadCard.tsx b/src/components/nbread/nbreadCard.tsx index 433df6e..9f91112 100644 --- a/src/components/nbread/nbreadCard.tsx +++ b/src/components/nbread/nbreadCard.tsx @@ -1,3 +1,4 @@ +import { calculateIndividualShare } from '@/lib/nbread/calculateIndividualShare' import { Nbread } from '@/types/nbread' import { User } from '@/types/user' import Tab from '../common/tab/tab' @@ -13,8 +14,10 @@ const NbreadCard = ({ userData, handleEditingNbread, }: NbreadCardProps) => { - const paymentAmount = - Math.floor(nbreadData!.amount / nbreadData!.participantCount) || 0 + const paymentAmount = calculateIndividualShare( + nbreadData!.amount, + nbreadData!.participantCount, + ) return ( <> diff --git a/src/lib/nbread/calculateIndividualShare.ts b/src/lib/nbread/calculateIndividualShare.ts new file mode 100644 index 0000000..84168a5 --- /dev/null +++ b/src/lib/nbread/calculateIndividualShare.ts @@ -0,0 +1,15 @@ +/** + * 총 금액을 참여 인원으로 나눈 1인당 정산 금액을 계산한다. + * + * 원 단위 미만은 버린다. 참여 인원이 0 이하이거나 숫자가 아니면 1명으로 취급해 + * 0 나눗셈으로 Infinity 가 화면에 노출되는 것을 막는다. + */ +export const calculateIndividualShare = ( + amount: number, + participantCount: number, +): number => { + const safeParticipantCount = Math.max(Math.trunc(participantCount) || 1, 1) + const share = Math.floor(amount / safeParticipantCount) + + return Number.isFinite(share) ? share : 0 +} diff --git a/src/lib/nbread/getUserTotalNbreadAmount.ts b/src/lib/nbread/getUserTotalNbreadAmount.ts index 313060b..3b342eb 100644 --- a/src/lib/nbread/getUserTotalNbreadAmount.ts +++ b/src/lib/nbread/getUserTotalNbreadAmount.ts @@ -1,4 +1,5 @@ import { supabase } from "@/lib/supabaseClient"; +import { calculateIndividualShare } from "@/lib/nbread/calculateIndividualShare"; export const getUserTotalNbreadAmount = async (userId: string) => { if (!userId) return 0; @@ -31,7 +32,7 @@ export const getUserTotalNbreadAmount = async (userId: string) => { } const totalAmount = nbreads.reduce((sum, nbread) => { - const individualShare = Math.floor(nbread.amount / Math.max(nbread.participant_count, 1)); + const individualShare = calculateIndividualShare(nbread.amount, nbread.participant_count); return sum + individualShare; }, 0); diff --git a/src/lib/nbread/index.ts b/src/lib/nbread/index.ts index f7b14d8..31eda67 100644 --- a/src/lib/nbread/index.ts +++ b/src/lib/nbread/index.ts @@ -3,3 +3,4 @@ export { insertNbread } from './insertNbread' export { updateNbread } from './updateNbread' export { deleteNbread } from './deleteNbread' export { getUserNbreads } from './getUserNbread' +export { calculateIndividualShare } from './calculateIndividualShare' From 3bd638ab93ab85555336bf7b6ab121019380c2a0 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 1 Sep 2026 14:34:25 +0900 Subject: [PATCH 06/13] =?UTF-8?q?refactor:=20=EB=82=A9=EB=B6=80=EC=9D=BC?= =?UTF-8?q?=20=ED=82=A4=20=EB=B3=80=ED=99=98=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit nbread_records.payment_date 조회와 갱신이 각자 같은 변환식을 들고 있었다. 이 키가 어긋나면 조회는 0건, 갱신은 0행이 되고 예외가 발생하지 않아 납부 체크가 조용히 실패한다. toPaymentDateKey 로 한 곳에 모은다. 변환 규칙은 그대로 두고 위치만 옮긴다. Co-Authored-By: Claude Opus 5 (1M context) --- src/lib/nbreadRecord/getNbreadRecords.ts | 5 ++--- src/lib/nbreadRecord/index.ts | 1 + src/lib/nbreadRecord/toPaymentDateKey.ts | 8 ++++++++ src/lib/nbreadRecord/updateNbreadRecord.ts | 3 ++- 4 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 src/lib/nbreadRecord/toPaymentDateKey.ts diff --git a/src/lib/nbreadRecord/getNbreadRecords.ts b/src/lib/nbreadRecord/getNbreadRecords.ts index 59346f2..6fbbcc0 100644 --- a/src/lib/nbreadRecord/getNbreadRecords.ts +++ b/src/lib/nbreadRecord/getNbreadRecords.ts @@ -1,13 +1,12 @@ import { supabase } from '@/lib/supabaseClient' import { Nbread, NbreadRecord } from '@/types/nbread' +import { toPaymentDateKey } from './toPaymentDateKey' export const getNbreadRecords = async ( nbreadId: string, startDate: string, ) => { - const translatedStartDate = new Date(startDate) - .toISOString() - .split('T')[0] + const translatedStartDate = toPaymentDateKey(startDate) try { const { data, error } = await supabase diff --git a/src/lib/nbreadRecord/index.ts b/src/lib/nbreadRecord/index.ts index caae0ad..0b79da9 100644 --- a/src/lib/nbreadRecord/index.ts +++ b/src/lib/nbreadRecord/index.ts @@ -1,2 +1,3 @@ export { getNbreadRecords } from './getNbreadRecords' export { updateNbreadRecord } from './updateNbreadRecord' +export { toPaymentDateKey } from './toPaymentDateKey' diff --git a/src/lib/nbreadRecord/toPaymentDateKey.ts b/src/lib/nbreadRecord/toPaymentDateKey.ts new file mode 100644 index 0000000..5d31728 --- /dev/null +++ b/src/lib/nbreadRecord/toPaymentDateKey.ts @@ -0,0 +1,8 @@ +/** + * nbread_records.payment_date 조회 및 갱신에 쓰는 날짜 키(YYYY-MM-DD)를 만든다. + * + * 이 값이 한 칸이라도 어긋나면 조회는 0건, 갱신은 0행이 되고 예외는 발생하지 않는다. + * 즉 납부 체크가 조용히 실패한다. 따라서 변환 규칙을 한 곳에 고정한다. + */ +export const toPaymentDateKey = (startDate: string): string => + new Date(startDate).toISOString().split('T')[0] diff --git a/src/lib/nbreadRecord/updateNbreadRecord.ts b/src/lib/nbreadRecord/updateNbreadRecord.ts index 09bb39a..dd229a2 100644 --- a/src/lib/nbreadRecord/updateNbreadRecord.ts +++ b/src/lib/nbreadRecord/updateNbreadRecord.ts @@ -1,6 +1,7 @@ import { supabase } from '@/lib/supabaseClient' import { Nbread } from '@/types/nbread' import { captureAppError } from '@/lib/sentry/sentry' +import { toPaymentDateKey } from './toPaymentDateKey' export const updateNbreadRecord = async ( nbreadId: string, @@ -8,7 +9,7 @@ export const updateNbreadRecord = async ( isPaid: boolean, startDate: string, ) => { - const translatedStartDate = new Date(startDate).toISOString().split('T')[0] + const translatedStartDate = toPaymentDateKey(startDate) try { const { data, error } = await supabase From 5b0fe0c53d8679f7abff41fbe5fbeca4aebf25b0 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 1 Sep 2026 14:34:41 +0900 Subject: [PATCH 07/13] =?UTF-8?q?test:=20=EC=A0=95=EC=82=B0=20=EA=B8=88?= =?UTF-8?q?=EC=95=A1,=20=EB=82=A9=EB=B6=80=EC=9D=BC=20=ED=82=A4,=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EA=B2=BD=EB=A1=9C=20=EB=8B=A8=EC=9C=84=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 틀리면 사용자에게 즉시 금전 또는 신뢰 문제가 되는 로직 세 곳을 고정한다. - calculateIndividualShare: 인원 0, 음수, 1명 미만 소수, NaN 과 음수 금액까지 포함해 0 나눗셈이 화면에 새지 않는지 검증한다 - toPaymentDateKey: UTC 환산 뒤 날짜를 자르는 현재 규칙을 회귀 테스트로 고정한다. KST 자정 입력이 전날로 밀리는 경계를 명시하고, 해석할 수 없는 입력은 조용히 잘못된 키를 만들지 않고 예외로 끊는지 확인한다 - getNotificationDestination: 알림 타입 6종, camelCase 와 snake_case 이중 키, data 가 null 배열 빈 객체인 경우, URL 인코딩을 검증한다 입출력만 다른 케이스는 test.for 로 묶어 실패 시 어느 입력이 깨졌는지 리포트에 드러나게 한다. Co-Authored-By: Claude Opus 5 (1M context) --- .../nbread/calculateIndividualShare.test.ts | 61 +++++++ src/lib/nbreadRecord/toPaymentDateKey.test.ts | 44 +++++ .../getNotificationDestination.test.ts | 154 ++++++++++++++++++ 3 files changed, 259 insertions(+) create mode 100644 src/lib/nbread/calculateIndividualShare.test.ts create mode 100644 src/lib/nbreadRecord/toPaymentDateKey.test.ts create mode 100644 src/lib/notification/getNotificationDestination.test.ts diff --git a/src/lib/nbread/calculateIndividualShare.test.ts b/src/lib/nbread/calculateIndividualShare.test.ts new file mode 100644 index 0000000..247c77e --- /dev/null +++ b/src/lib/nbread/calculateIndividualShare.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it, test } from 'vitest' +import { calculateIndividualShare } from './calculateIndividualShare' + +describe('calculateIndividualShare', () => { + describe('정상 계산', () => { + it('총 금액을 참여 인원으로 나눈다', () => { + expect(calculateIndividualShare(30000, 3)).toBe(10000) + }) + + it('나누어떨어지지 않으면 원 단위 미만을 버린다', () => { + expect(calculateIndividualShare(10000, 3)).toBe(3333) + }) + + it('참여 인원이 1명이면 총 금액을 그대로 반환한다', () => { + expect(calculateIndividualShare(17900, 1)).toBe(17900) + }) + }) + + describe('경계값', () => { + it('총 금액이 0이면 0을 반환한다', () => { + expect(calculateIndividualShare(0, 4)).toBe(0) + }) + + it('1인당 금액이 1원 미만이면 0으로 내려간다', () => { + expect(calculateIndividualShare(3, 4)).toBe(0) + }) + + it('참여 인원에 소수가 들어오면 정수로 버린 뒤 나눈다', () => { + expect(calculateIndividualShare(10000, 2.7)).toBe(5000) + }) + + // 금액 컬럼에 음수를 막는 제약이 없어 표시 로직까지 그대로 내려온다. + it('총 금액이 음수면 음수 몫을 그대로 반환한다', () => { + expect(calculateIndividualShare(-3000, 2)).toBe(-1500) + }) + }) + + describe('잘못된 참여 인원', () => { + // 인원이 0이면 0 나눗셈으로 Infinity 가 되어 화면에 그대로 노출된 적이 있다. + // 1명으로 취급해 최소한 숫자가 나오도록 막는다. + // 1명 미만 소수는 정수로 버린 뒤 0이 되므로 같은 경로를 탄다. + test.for([ + { label: '0명', participantCount: 0 }, + { label: '음수', participantCount: -3 }, + { label: '1명 미만 소수', participantCount: 0.5 }, + { label: '숫자가 아닌 값', participantCount: Number.NaN }, + ])('참여 인원이 $label 이면 1명으로 취급한다', ({ participantCount }) => { + expect(calculateIndividualShare(30000, participantCount)).toBe(30000) + }) + }) + + describe('잘못된 총 금액', () => { + it('총 금액이 숫자가 아니면 0을 반환한다', () => { + expect(calculateIndividualShare(Number.NaN, 3)).toBe(0) + }) + + it('총 금액이 무한이면 0을 반환한다', () => { + expect(calculateIndividualShare(Number.POSITIVE_INFINITY, 3)).toBe(0) + }) + }) +}) diff --git a/src/lib/nbreadRecord/toPaymentDateKey.test.ts b/src/lib/nbreadRecord/toPaymentDateKey.test.ts new file mode 100644 index 0000000..d3e9b5e --- /dev/null +++ b/src/lib/nbreadRecord/toPaymentDateKey.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest' +import { toPaymentDateKey } from './toPaymentDateKey' + +// 이 함수의 결과는 nbread_records.payment_date 와 문자열이 정확히 일치해야 한다. +// 하루라도 어긋나면 조회 0건, 갱신 0행이 되고 예외는 나지 않는다. +// 아래 테스트는 현재 변환 규칙을 고정하는 회귀 테스트다. +describe('toPaymentDateKey', () => { + describe('정상 변환', () => { + it('날짜만 있는 문자열은 그대로 유지한다', () => { + expect(toPaymentDateKey('2026-09-01')).toBe('2026-09-01') + }) + + it('UTC 시각이 붙어 있으면 시각을 떼고 날짜만 남긴다', () => { + expect(toPaymentDateKey('2026-09-01T23:00:00Z')).toBe('2026-09-01') + }) + }) + + describe('타임존 경계', () => { + // UTC 로 환산한 뒤 날짜를 자르기 때문에 KST 자정 입력은 전날로 밀린다. + // 현재 동작이며, 결제일이 하루 밀리는 경로이므로 명시적으로 고정한다. + it('KST 자정은 UTC 기준 전날로 변환된다', () => { + expect(toPaymentDateKey('2026-09-01T00:00:00+09:00')).toBe('2026-08-31') + }) + + it('KST 오전 9시 이후는 같은 날로 변환된다', () => { + expect(toPaymentDateKey('2026-09-01T09:00:00+09:00')).toBe('2026-09-01') + }) + + it('UTC 자정은 같은 날로 유지된다', () => { + expect(toPaymentDateKey('2026-09-01T00:00:00Z')).toBe('2026-09-01') + }) + }) + + describe('잘못된 입력', () => { + // 조용히 잘못된 키를 만드는 것보다 예외로 끊는 편이 안전하다. + it('빈 문자열이면 예외를 던진다', () => { + expect(() => toPaymentDateKey('')).toThrow(RangeError) + }) + + it('날짜로 해석할 수 없는 문자열이면 예외를 던진다', () => { + expect(() => toPaymentDateKey('납부일 미정')).toThrow(RangeError) + }) + }) +}) diff --git a/src/lib/notification/getNotificationDestination.test.ts b/src/lib/notification/getNotificationDestination.test.ts new file mode 100644 index 0000000..d395715 --- /dev/null +++ b/src/lib/notification/getNotificationDestination.test.ts @@ -0,0 +1,154 @@ +import { describe, expect, it, test } from 'vitest' +import { + getNotificationDestination, + getNotificationDestinationError, +} from './getNotificationDestination' +import type { Notification, NotificationType } from '@/types/notification' + +const notification = ( + type: NotificationType, + data: Notification['data'] = null, +): Pick => ({ type, data }) + +// 엔빵 아이디가 있어야 이동 경로가 만들어지는 알림 타입 +const nbreadIdRequiredTypes: { type: NotificationType }[] = [ + { type: 'chat' }, + { type: 'payment' }, + { type: 'invite_accept' }, +] + +// data 에서 값을 읽어야 이동 경로가 만들어지는 알림 타입 +const dataRequiredTypes: { type: NotificationType }[] = [ + ...nbreadIdRequiredTypes, + { type: 'invite' }, +] + +describe('getNotificationDestination', () => { + describe('초대 알림', () => { + it('camelCase 키에서 초대 토큰을 읽는다', () => { + expect( + getNotificationDestination( + notification('invite', { inviteToken: 'abc123' }), + ), + ).toBe('/invite/abc123') + }) + + it('snake_case 키에서도 초대 토큰을 읽는다', () => { + expect( + getNotificationDestination( + notification('invite', { invite_token: 'abc123' }), + ), + ).toBe('/invite/abc123') + }) + + it('토큰에 URL 예약 문자가 있으면 인코딩한다', () => { + expect( + getNotificationDestination( + notification('invite', { inviteToken: 'a/b c' }), + ), + ).toBe('/invite/a%2Fb%20c') + }) + + it('토큰이 없으면 null 을 반환한다', () => { + expect( + getNotificationDestination(notification('invite', { nbreadId: 'n1' })), + ).toBeNull() + }) + }) + + describe('엔빵 알림', () => { + it('채팅 알림은 chat 탭으로 보낸다', () => { + expect( + getNotificationDestination(notification('chat', { nbreadId: 'n1' })), + ).toBe('/nbread/n1?tab=chat') + }) + + it('납부 알림은 엔빵 상세로 보낸다', () => { + expect( + getNotificationDestination(notification('payment', { nbread_id: 'n1' })), + ).toBe('/nbread/n1') + }) + + it('초대 수락 알림은 엔빵 상세로 보낸다', () => { + expect( + getNotificationDestination( + notification('invite_accept', { nbreadId: 'n1' }), + ), + ).toBe('/nbread/n1') + }) + + test.for(nbreadIdRequiredTypes)( + '$type 알림은 엔빵 아이디가 없으면 null 을 반환한다', + ({ type }) => { + expect(getNotificationDestination(notification(type, {}))).toBeNull() + }, + ) + }) + + describe('친구 알림', () => { + it('친구 응답 알림은 data 없이도 친구 목록으로 보낸다', () => { + expect(getNotificationDestination(notification('friend_response'))).toBe( + '/friendList', + ) + }) + + it('친구 요청 알림은 이동 경로가 없다', () => { + expect( + getNotificationDestination(notification('friend_request')), + ).toBeNull() + }) + }) + + describe('잘못된 data', () => { + test.for(dataRequiredTypes)( + '$type 알림은 data 가 null 이면 null 을 반환한다', + ({ type }) => { + expect(getNotificationDestination(notification(type, null))).toBeNull() + }, + ) + + it('data 가 배열이면 null 을 반환한다', () => { + expect( + getNotificationDestination(notification('chat', ['n1'])), + ).toBeNull() + }) + + it('값이 빈 문자열이면 없는 것으로 취급한다', () => { + expect( + getNotificationDestination(notification('chat', { nbreadId: '' })), + ).toBeNull() + }) + + it('값이 문자열이 아니면 없는 것으로 취급한다', () => { + expect( + getNotificationDestination(notification('chat', { nbreadId: 123 })), + ).toBeNull() + }) + + it('camelCase 키가 비어 있으면 snake_case 키로 넘어간다', () => { + expect( + getNotificationDestination( + notification('chat', { nbreadId: '', nbread_id: 'n1' }), + ), + ).toBe('/nbread/n1?tab=chat') + }) + }) +}) + +describe('getNotificationDestinationError', () => { + it('초대 알림은 초대 정보 안내 문구를 반환한다', () => { + expect(getNotificationDestinationError('invite')).toBe( + '초대 정보를 찾을 수 없어요.', + ) + }) + + it('친구 요청 알림은 이동할 곳이 없으므로 문구가 없다', () => { + expect(getNotificationDestinationError('friend_request')).toBe('') + }) + + it('나머지 알림은 공통 안내 문구를 반환한다', () => { + expect(getNotificationDestinationError('chat')).toBe( + '이동할 페이지 정보를 찾을 수 없어요.', + ) + }) +}) From b5f9caafb701c11f8a3c5a6f7a95181990233c84 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 2 Sep 2026 14:20:51 +0900 Subject: [PATCH 08/13] =?UTF-8?q?fix:=20SVG=20=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=84=A0=EC=96=B8=EC=9D=84=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=EC=86=8C=EC=97=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI 의 깨끗한 체크아웃에서 tsc --noEmit 이 SVG 임포트 전부를 TS2307 로 실패했다. next-env.d.ts 가 .gitignore 대상이라 새 체크아웃에는 존재하지 않고, 그 파일이 참조하는 next/image-types/global 이 *.svg 선언을 제공하기 때문이다. 로컬에서는 이전 next dev 실행이 남긴 파일이 있어 통과해 드러나지 않았다. next build 를 게이트에 넣어 파일을 생성하게 하는 대신 선언을 저장소에 둔다. 게이트의 목적이 빠른 피드백이고, 빌드는 Supabase 환경 변수 의존 가능성이 있다. @svgr/webpack 을 쓰므로 SVG 는 React 컴포넌트다. next/image-types/global 의 any 대신 FunctionComponent> 로 선언한다. Co-Authored-By: Claude Opus 5 (1M context) --- src/types/svg.d.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/types/svg.d.ts diff --git a/src/types/svg.d.ts b/src/types/svg.d.ts new file mode 100644 index 0000000..6f14ee6 --- /dev/null +++ b/src/types/svg.d.ts @@ -0,0 +1,12 @@ +// next-env.d.ts 는 .gitignore 대상이라 새 체크아웃에는 존재하지 않는다. +// 그 파일이 참조하는 next/image-types/global 이 *.svg 선언을 제공하므로, +// tsc --noEmit 만 실행하는 CI 에서는 SVG 임포트가 전부 미해결이 된다. +// 이 프로젝트는 @svgr/webpack 으로 SVG 를 React 컴포넌트로 가져오므로 +// 그 형태를 저장소에 직접 선언해 타입 체크가 next-env.d.ts 에 의존하지 않게 한다. +declare module '*.svg' { + import type { FunctionComponent, SVGProps } from 'react' + + const ReactComponent: FunctionComponent> + + export default ReactComponent +} From 87cbffa82fc288a78fa7ea7b6b38244a4fd10d82 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 2 Sep 2026 14:22:30 +0900 Subject: [PATCH 09/13] =?UTF-8?q?cicd:=20=EB=8B=A8=EC=9C=84=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=9B=8C=ED=81=AC=ED=94=8C=EB=A1=9C?= =?UTF-8?q?=EC=9D=98=20=EC=95=A1=EC=85=98=20=EB=B2=84=EC=A0=84=EC=9D=84=20?= =?UTF-8?q?E2E=20=EC=9B=8C=ED=81=AC=ED=94=8C=EB=A1=9C=EC=99=80=20=EB=A7=9E?= =?UTF-8?q?=EC=B6=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit actions/checkout@v4 와 setup-node@v4 가 Node.js 20 을 대상으로 해 런너가 deprecation 경고를 낸다. develop 의 E2E 워크플로가 이미 v7 과 Node 22 로 올라가 있어 같은 버전으로 맞춘다. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci-unit-test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-unit-test.yml b/.github/workflows/ci-unit-test.yml index 0742cc7..4f937d7 100644 --- a/.github/workflows/ci-unit-test.yml +++ b/.github/workflows/ci-unit-test.yml @@ -19,12 +19,12 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Setup Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v7 with: - node-version: 20 + node-version: 22 cache: npm - name: Install dependencies From 691d2b29ee1e2a2ec0a8b5605847940a518471ef Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 2 Sep 2026 14:24:26 +0900 Subject: [PATCH 10/13] =?UTF-8?q?chore:=20=EA=B2=80=EC=A6=9D=20=EA=B2=8C?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20=ED=99=95=EC=9D=B8=EC=9A=A9=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EC=98=A4=EB=A5=98=20=EC=B6=94=EA=B0=80=20(?= =?UTF-8?q?=EB=90=98=EB=8F=8C=EB=A6=B4=20=EC=98=88=EC=A0=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 (1M context) --- src/gate-check-temp.ts | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 src/gate-check-temp.ts diff --git a/src/gate-check-temp.ts b/src/gate-check-temp.ts new file mode 100644 index 0000000..86b31ce --- /dev/null +++ b/src/gate-check-temp.ts @@ -0,0 +1,4 @@ +// 검증 게이트 동작 확인용 임시 파일입니다. 확인 후 되돌립니다. +import { calculateIndividualShare } from '@/lib/nbread/calculateIndividualShare' + +export const brokenOnPurpose = calculateIndividualShare('30000', 3) From 314a2c1053a5dd9818ed0f11104fd14cc7827279 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 2 Sep 2026 14:25:55 +0900 Subject: [PATCH 11/13] =?UTF-8?q?chore:=20=EA=B2=80=EC=A6=9D=20=EA=B2=8C?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20=ED=99=95=EC=9D=B8=EC=9A=A9=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=EB=A5=BC=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=EB=A1=9C=20=EA=B5=90=EC=B2=B4=20(=EB=90=98=EB=8F=8C?= =?UTF-8?q?=EB=A6=B4=20=EC=98=88=EC=A0=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 (1M context) --- src/gate-check-temp.ts | 4 ---- src/lib/nbread/calculateIndividualShare.test.ts | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) delete mode 100644 src/gate-check-temp.ts diff --git a/src/gate-check-temp.ts b/src/gate-check-temp.ts deleted file mode 100644 index 86b31ce..0000000 --- a/src/gate-check-temp.ts +++ /dev/null @@ -1,4 +0,0 @@ -// 검증 게이트 동작 확인용 임시 파일입니다. 확인 후 되돌립니다. -import { calculateIndividualShare } from '@/lib/nbread/calculateIndividualShare' - -export const brokenOnPurpose = calculateIndividualShare('30000', 3) diff --git a/src/lib/nbread/calculateIndividualShare.test.ts b/src/lib/nbread/calculateIndividualShare.test.ts index 247c77e..bc3ba9b 100644 --- a/src/lib/nbread/calculateIndividualShare.test.ts +++ b/src/lib/nbread/calculateIndividualShare.test.ts @@ -4,7 +4,7 @@ import { calculateIndividualShare } from './calculateIndividualShare' describe('calculateIndividualShare', () => { describe('정상 계산', () => { it('총 금액을 참여 인원으로 나눈다', () => { - expect(calculateIndividualShare(30000, 3)).toBe(10000) + expect(calculateIndividualShare(30000, 3)).toBe(9999) }) it('나누어떨어지지 않으면 원 단위 미만을 버린다', () => { From 079a0b7562fd9a1f347753a57eb18289716afbc3 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 2 Sep 2026 14:29:30 +0900 Subject: [PATCH 12/13] =?UTF-8?q?chore:=20=EA=B2=80=EC=A6=9D=20=EA=B2=8C?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20=ED=99=95=EC=9D=B8=EC=9A=A9=20=EC=9E=84?= =?UTF-8?q?=EC=8B=9C=20=EB=B3=80=EA=B2=BD=20=EB=90=98=EB=8F=8C=EB=A6=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 게이트가 통과와 실패 양쪽으로 동작하는 것을 PR #214 에서 확인했다. 타입 오류는 Type check 단계에서, 테스트 실패는 Unit test 단계에서 각각 끊기고 두 경우 모두 develop 머지가 차단되는 것을 확인했다. Co-Authored-By: Claude Opus 5 (1M context) --- src/lib/nbread/calculateIndividualShare.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/nbread/calculateIndividualShare.test.ts b/src/lib/nbread/calculateIndividualShare.test.ts index bc3ba9b..247c77e 100644 --- a/src/lib/nbread/calculateIndividualShare.test.ts +++ b/src/lib/nbread/calculateIndividualShare.test.ts @@ -4,7 +4,7 @@ import { calculateIndividualShare } from './calculateIndividualShare' describe('calculateIndividualShare', () => { describe('정상 계산', () => { it('총 금액을 참여 인원으로 나눈다', () => { - expect(calculateIndividualShare(30000, 3)).toBe(9999) + expect(calculateIndividualShare(30000, 3)).toBe(10000) }) it('나누어떨어지지 않으면 원 단위 미만을 버린다', () => { From a7d62c8a73dcac0c09c0840e3e9aabfec77621cd Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 2 Sep 2026 14:34:59 +0900 Subject: [PATCH 13/13] =?UTF-8?q?cicd:=20=EC=9B=8C=ED=81=AC=ED=94=8C?= =?UTF-8?q?=EB=A1=9C=20=ED=91=9C=EC=8B=9C=20=EC=9D=B4=EB=A6=84=EC=9D=84=20?= =?UTF-8?q?Unit=20Test,=20E2E=20Test=20=EB=A1=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Actions 사이드바에 "CI - Unit Test", "CI E2E test" 로 표기 방식이 서로 달라 일관성이 없었다. 파일명에 이미 ci- 접두사가 있어 이름에서는 뺀다. required status check 이름은 job 이름(unit-verify)이므로 이 변경에 영향받지 않는다. 브랜치 보호 규칙은 그대로 유지된다. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci-e2e-test.yml | 2 +- .github/workflows/ci-unit-test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-e2e-test.yml b/.github/workflows/ci-e2e-test.yml index b2930be..7e43e50 100644 --- a/.github/workflows/ci-e2e-test.yml +++ b/.github/workflows/ci-e2e-test.yml @@ -1,4 +1,4 @@ -name: CI E2E test +name: E2E Test on: pull_request: diff --git a/.github/workflows/ci-unit-test.yml b/.github/workflows/ci-unit-test.yml index 4f937d7..ecbe955 100644 --- a/.github/workflows/ci-unit-test.yml +++ b/.github/workflows/ci-unit-test.yml @@ -1,4 +1,4 @@ -name: CI - Unit Test +name: Unit Test on: pull_request: