From 578900b9915eb0404a569ed48f0b44e852606624 Mon Sep 17 00:00:00 2001 From: yoouyeon Date: Sun, 12 Apr 2026 10:45:11 +0900 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20=EC=BA=90=EB=A6=AD=ED=84=B0=20?= =?UTF-8?q?=EB=8F=84=EA=B0=90=20=EC=8B=A4=EC=A0=9C=20API=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - api endpoint 실제 구조에 맞게 수정 - 타입 실제 구조에 맞게 수정 --- src/entities/character/api/character.ts | 18 +++++++++++------- src/entities/character/model/character.type.ts | 15 ++++++++++++--- .../api/useGetCharacterCollection.ts | 6 +++--- .../ui/CharacterItem/index.tsx | 6 +++--- src/mocks/handlers/character.ts | 17 +++++++---------- 5 files changed, 36 insertions(+), 26 deletions(-) diff --git a/src/entities/character/api/character.ts b/src/entities/character/api/character.ts index ef33808..b9a185d 100644 --- a/src/entities/character/api/character.ts +++ b/src/entities/character/api/character.ts @@ -1,9 +1,13 @@ import axiosInstance from '@/shared/api/axios'; -import { CharacterItemsResponse } from '../model/character.type'; +import { + CharacterItemsRawResponse, + CharacterItemsResponse, +} from '../model/character.type'; -export const getCharacterCollection = () => - axiosInstance - .get('/character/collection', { - useMock: true, - }) - .then((res) => res.data); +export const getCharacterCollection = (): Promise => + axiosInstance.get('/collections').then((res) => ({ + collections: res.data.collections.map((item) => ({ + ...item, + acquiredAt: item.acquiredAt ? new Date(item.acquiredAt) : null, + })), + })); diff --git a/src/entities/character/model/character.type.ts b/src/entities/character/model/character.type.ts index 7de3abf..c7d6f2b 100644 --- a/src/entities/character/model/character.type.ts +++ b/src/entities/character/model/character.type.ts @@ -14,10 +14,19 @@ export interface CharacterData { export interface CharacterItemData extends CharacterData { id: number; - isUnlocked: boolean; - unlockedAt: string | null; + acquiredAt: Date | null; +} + +// API 원시 응답 타입 (JSON 파싱 직후, 변환 전) +interface CharacterItemRaw extends CharacterData { + id: number; + acquiredAt: string | null; } export interface CharacterItemsResponse { - characters: CharacterItemData[]; + collections: CharacterItemData[]; +} + +export interface CharacterItemsRawResponse { + collections: CharacterItemRaw[]; } diff --git a/src/features/character-management/api/useGetCharacterCollection.ts b/src/features/character-management/api/useGetCharacterCollection.ts index d422533..5d17fd1 100644 --- a/src/features/character-management/api/useGetCharacterCollection.ts +++ b/src/features/character-management/api/useGetCharacterCollection.ts @@ -7,9 +7,9 @@ const useGetCharacterCollection = () => queryFn: getCharacterCollection, // 획득한 캐릭터가 가장 앞에 오도록 정렬 select: (data) => - [...data.characters].sort((a, b) => { - if (a.isUnlocked === b.isUnlocked) return 0; - return a.isUnlocked ? -1 : 1; + [...data.collections].sort((a, b) => { + if ((a.acquiredAt !== null) === (b.acquiredAt !== null)) return 0; + return a.acquiredAt !== null ? -1 : 1; }), }); diff --git a/src/features/character-management/ui/CharacterItem/index.tsx b/src/features/character-management/ui/CharacterItem/index.tsx index c014ff1..c284a54 100644 --- a/src/features/character-management/ui/CharacterItem/index.tsx +++ b/src/features/character-management/ui/CharacterItem/index.tsx @@ -17,14 +17,14 @@ interface CharacterCardProps { } function CharacterCard({ character }: CharacterCardProps) { - const { imageUrl, name, unlockedAt } = character; + const { imageUrl, name, acquiredAt } = character; return ( {name} - {unlockedAt ? format(new Date(unlockedAt), 'yyyy.MM.dd') : null} + {acquiredAt ? format(acquiredAt, 'yyyy.MM.dd') : null} ); @@ -35,7 +35,7 @@ interface CharacterItemProps { } function CharacterItem({ character }: CharacterItemProps) { - if (!character.isUnlocked) return ; + if (!character.acquiredAt) return ; return ; } diff --git a/src/mocks/handlers/character.ts b/src/mocks/handlers/character.ts index eb382e4..23dcc1d 100644 --- a/src/mocks/handlers/character.ts +++ b/src/mocks/handlers/character.ts @@ -1,45 +1,42 @@ import { http, HttpResponse, passthrough } from 'msw'; -import { CharacterItemsResponse } from '@/entities/character/model/character.type'; +import { CharacterItemsRawResponse } from '@/entities/character/model/character.type'; import getIsMocked from '@/mocks/lib/getIsMocked'; const characterHandlers = [ - http.get('/api/v1/character/collection', async ({ request }) => { + http.get('/api/v1/collections', async ({ request }) => { if (!getIsMocked(request)) return passthrough(); - const dummyCharacterCollectionResponse: CharacterItemsResponse = { - characters: [ + const dummyCharacterCollectionResponse: CharacterItemsRawResponse = { + collections: [ { id: 1, name: '마법사 또또', - isUnlocked: true, imageUrl: 'https://ylsrfiwjufyciwoidcaz.supabase.co/storage/v1/object/public/moddo/wizard_ddoddo.png', imageBigUrl: 'https://ylsrfiwjufyciwoidcaz.supabase.co/storage/v1/object/public/moddo/wizard_ddoddo.png', rarity: 3, - unlockedAt: '2025-02-03T00:00:00Z', + acquiredAt: '2025-02-03T00:00:00Z', }, { id: 2, name: '천사 모또', - isUnlocked: true, imageUrl: 'https://ylsrfiwjufyciwoidcaz.supabase.co/storage/v1/object/public/moddo/angel_moddo.png', imageBigUrl: 'https://ylsrfiwjufyciwoidcaz.supabase.co/storage/v1/object/public/moddo/angel_moddo.png', rarity: 2, - unlockedAt: '2025-02-03T00:00:00Z', + acquiredAt: '2025-02-03T00:00:00Z', }, { id: 3, name: '딸기 또또', - isUnlocked: false, imageUrl: 'https://ylsrfiwjufyciwoidcaz.supabase.co/storage/v1/object/public/moddo/strawberry_ddoddo.png', imageBigUrl: 'https://ylsrfiwjufyciwoidcaz.supabase.co/storage/v1/object/public/moddo/strawberry_ddoddo.png', rarity: 1, - unlockedAt: null, + acquiredAt: null, }, ], }; From 57bff48a32dc21db79e74932c3b416e0e2a80347 Mon Sep 17 00:00:00 2001 From: yoouyeon Date: Sun, 12 Apr 2026 10:50:29 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20=EC=A0=95=EC=82=B0=20=EC=B0=B8?= =?UTF-8?q?=EC=97=AC=EC=9E=90=20=EC=A1=B0=ED=9A=8C(=EB=AA=A8=EC=9E=84?= =?UTF-8?q?=EC=9B=90=20=EC=A1=B0=ED=9A=8C)=20=EC=8B=A4=EC=A0=9C=20API=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - api endpoint 실제 구조에 맞게 수정 - 타입 실제 구조에 맞게 수정 --- src/entities/member/api/getProfiles.ts | 12 +++++++----- src/entities/member/model/member.type.ts | 9 +++++++++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/entities/member/api/getProfiles.ts b/src/entities/member/api/getProfiles.ts index 0d08bf9..1c0d45d 100644 --- a/src/entities/member/api/getProfiles.ts +++ b/src/entities/member/api/getProfiles.ts @@ -1,14 +1,16 @@ import axiosInstance from '@/shared/api/axios'; -import { MemberProfile, MemberProfileData } from '../model/member.type'; +import { MemberProfile, MemberProfileRawData } from '../model/member.type'; // TODO : 기존 groupToken들을 사용하는 방식을 settlementCode를 사용하는 방식으로 변경해야 함. // 모임원 조회 API - 정산 참여자 프로필 조회 export const getProfiles = async ( settlementCode: string ): Promise => { - const response = await axiosInstance.get( - `/groups/${settlementCode}/members`, - { useMock: true } + const response = await axiosInstance.get( + `/groups/${settlementCode}/members` ); - return response.data.members; + return response.data.members.map((member) => ({ + ...member, + paidAt: member.paidAt ? new Date(member.paidAt) : null, + })); }; diff --git a/src/entities/member/model/member.type.ts b/src/entities/member/model/member.type.ts index 88fe6c5..eebd8dd 100644 --- a/src/entities/member/model/member.type.ts +++ b/src/entities/member/model/member.type.ts @@ -27,6 +27,15 @@ export interface MemberProfile { paidAt: Date | null; } +// API 원시 응답 타입 (JSON 파싱 직후, 변환 전) +export interface MemberProfileRaw extends Omit { + paidAt: string | null; +} + export interface MemberProfileData { members: MemberProfile[]; } + +export interface MemberProfileRawData { + members: MemberProfileRaw[]; +} From ce6e6b6c10362f2efe145a67bcd80186ec923ec7 Mon Sep 17 00:00:00 2001 From: yoouyeon Date: Sun, 12 Apr 2026 11:14:22 +0900 Subject: [PATCH 3/7] =?UTF-8?q?feat:=20=EC=B0=B8=EC=97=AC=EC=9E=90=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=20=EC=8B=A4=EC=A0=9C=20API=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 에러 처리 추가 (토스트) - 리페치 대신 응답을 받아 캐시를 업데이트하도록 변경 --- src/entities/member/api/assignMember.ts | 14 +++++++------ src/features/join/api/useAssignMember.ts | 25 ++++++++++++++++++++---- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/entities/member/api/assignMember.ts b/src/entities/member/api/assignMember.ts index cff5f35..1053fde 100644 --- a/src/entities/member/api/assignMember.ts +++ b/src/entities/member/api/assignMember.ts @@ -1,15 +1,17 @@ import axiosInstance from '@/shared/api/axios'; +import { MemberProfile, MemberProfileRaw } from '../model/member.type'; // 참여자 선택 api (로그인한 참여자가 정산에 참여하도록 프로필 설정) export const assignMember = async ( settlementCode: string, memberId: number -): Promise => { - await axiosInstance.post( +): Promise => { + const response = await axiosInstance.post( `/groups/${settlementCode}/members/assign`, - { - memberId, - }, - { useMock: true } + { memberId } ); + return { + ...response.data, + paidAt: response.data.paidAt ? new Date(response.data.paidAt) : null, + }; }; diff --git a/src/features/join/api/useAssignMember.ts b/src/features/join/api/useAssignMember.ts index a980562..5cd34e4 100644 --- a/src/features/join/api/useAssignMember.ts +++ b/src/features/join/api/useAssignMember.ts @@ -1,13 +1,30 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useQueryClient } from '@tanstack/react-query'; import { assignMember } from '@/entities/member/api/assignMember'; +import { MemberProfile } from '@/entities/member/model/member.type'; +import useMutationWithHandlers from '@/shared/hooks/useMutationWithHanders'; +import { showToast } from '@/shared/ui/Toast'; const useAssignMember = (groupToken: string) => { const queryClient = useQueryClient(); - return useMutation({ + + return useMutationWithHandlers({ mutationFn: (memberId: number) => assignMember(groupToken, memberId), - onSuccess: () => { - queryClient.removeQueries({ queryKey: ['profiles', groupToken] }); + onSuccess: (data) => { + // 응답으로 받은 업데이트된 멤버 정보로 캐시를 직접 갱신 (리패치 없이 즉시 반영) + queryClient.setQueryData( + ['profiles', groupToken], + (prev) => prev?.map((member) => (member.id === data.id ? data : member)) + ); + }, + // TODO: 에러 코드 확인 후 케이스별 메시지로 세분화 필요 + errorHandlers: { + default: () => + showToast({ + type: 'error', + content: '참여자 선택에 실패했어요. 다시 시도해 주세요.', + }), }, + ignoreBoundaryErrors: [400, 409], }); }; From e40c99cc0af0744059a0c317952deaecb066bb0c Mon Sep 17 00:00:00 2001 From: yoouyeon Date: Sun, 12 Apr 2026 11:22:10 +0900 Subject: [PATCH 4/7] =?UTF-8?q?refactor:=20=EB=8D=94=20=EC=9D=B4=EC=83=81?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20?= =?UTF-8?q?MemberProfileData=20=ED=83=80=EC=9E=85=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/member/model/member.type.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/entities/member/model/member.type.ts b/src/entities/member/model/member.type.ts index eebd8dd..ae6726c 100644 --- a/src/entities/member/model/member.type.ts +++ b/src/entities/member/model/member.type.ts @@ -32,10 +32,6 @@ export interface MemberProfileRaw extends Omit { paidAt: string | null; } -export interface MemberProfileData { - members: MemberProfile[]; -} - export interface MemberProfileRawData { members: MemberProfileRaw[]; } From 425fe34b9b934130b596cd20676238309a0b6f7d Mon Sep 17 00:00:00 2001 From: yoouyeon Date: Sun, 12 Apr 2026 13:05:07 +0900 Subject: [PATCH 5/7] =?UTF-8?q?refactor:=20=EC=BA=90=EB=A6=AD=ED=84=B0=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=A5=BC=20CHARACTER=5FDATA?= =?UTF-8?q?=EB=A1=9C=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 복잡하게 얽혀있던 타입과 데이터를 CHARACTER_DATA 하나에서 관리하도록 수정 - 타입을 CHARACTER_DATA에서 파생해서 생성하도록 수정 --- src/entities/character/config/character.ts | 81 +++++++------------ .../character/model/character.type.ts | 10 +-- .../ui/CharacterBottomSheet/index.tsx | 4 +- .../characterShare/CharacterSharePage.tsx | 9 +-- 4 files changed, 40 insertions(+), 64 deletions(-) diff --git a/src/entities/character/config/character.ts b/src/entities/character/config/character.ts index d3e27ac..932953a 100644 --- a/src/entities/character/config/character.ts +++ b/src/entities/character/config/character.ts @@ -1,65 +1,46 @@ -import { CharacterType } from '@/entities/character/model/character.type'; - -export const CHARACTER_IMAGE_SIZE: Record< - CharacterType, - { - big: { - width?: string; - height?: string; - }; - small: { - width?: string; - height?: string; - }; - } -> = { +export const CHARACTER_DATA = { '천사 모또': { - big: { - width: '15rem', - }, - small: { - width: '12rem', + imageSize: { + big: { width: '15rem' }, + small: { width: '12rem' }, }, + description: '정산의 수호천사 등장!', }, '러키 모또': { - big: { - width: '12.5rem', - }, - small: { - width: '10rem', + imageSize: { + big: { width: '12.5rem' }, + small: { width: '10rem' }, }, + description: '정산 성공! 좋은 일만 가득하길~', }, '딸기 또또': { - big: { - width: '12.5rem', - }, - small: { - width: '10rem', - height: '10rem', + imageSize: { + big: { width: '12.5rem' }, + small: { width: '10rem', height: '10rem' }, }, + description: '정산 완료! 달콤한 하루 보내~', }, '잠꾸러기 또또': { - big: { - width: '12.5rem', - }, - small: { - width: '10rem', + imageSize: { + big: { width: '12.5rem' }, + small: { width: '10rem' }, }, + description: '정산 끝났어? 이제 푹 잘 수 있겠네~', }, '마법사 또또': { - big: { - height: '13.72844rem', - }, - small: { - height: '10.98275rem', + imageSize: { + big: { height: '13.72844rem' }, + small: { height: '10.98275rem' }, }, + description: '정산? 아브라카다브라! 해결 완료~', }, -}; - -export const CHARACTER_DESCRIPTION: Record = { - '천사 모또': '정산의 수호천사 등장!', - '러키 모또': '정산 성공! 좋은 일만 가득하길~', - '딸기 또또': '정산 완료! 달콤한 하루 보내~', - '잠꾸러기 또또': '정산 끝났어? 이제 푹 잘 수 있겠네~', - '마법사 또또': '정산? 아브라카다브라! 해결 완료~', -}; +} satisfies Record< + string, + { + imageSize: { + big: { width?: string; height?: string }; + small: { width?: string; height?: string }; + }; + description: string; + } +>; diff --git a/src/entities/character/model/character.type.ts b/src/entities/character/model/character.type.ts index c7d6f2b..a9d008e 100644 --- a/src/entities/character/model/character.type.ts +++ b/src/entities/character/model/character.type.ts @@ -1,9 +1,7 @@ -export type CharacterType = - | '천사 모또' - | '러키 모또' - | '딸기 또또' - | '잠꾸러기 또또' - | '마법사 또또'; +import { CHARACTER_DATA } from '@/entities/character/config/character'; + +// 새 캐릭터 추가 시 CHARACTER_DATA에만 추가하면 타입이 자동으로 확장됨 +export type CharacterType = keyof typeof CHARACTER_DATA; export interface CharacterData { name: CharacterType; diff --git a/src/features/character-management/ui/CharacterBottomSheet/index.tsx b/src/features/character-management/ui/CharacterBottomSheet/index.tsx index 7d451fc..6bacadb 100644 --- a/src/features/character-management/ui/CharacterBottomSheet/index.tsx +++ b/src/features/character-management/ui/CharacterBottomSheet/index.tsx @@ -3,7 +3,7 @@ import Text from '@/shared/ui/Text'; import BottomSheet from '@/shared/ui/BottomSheet'; import ButtonGroup from '@/shared/ui/ButtonGroup'; import Button from '@/shared/ui/Button'; -import { CHARACTER_IMAGE_SIZE } from '@/entities/character/config/character'; +import { CHARACTER_DATA } from '@/entities/character/config/character'; import { ROUTE } from '@/shared/config/route'; import useGetCharacter from '@/features/character-management/api/useGetCharacter'; import * as S from './index.styles'; @@ -33,7 +33,7 @@ function CharacterBottomSheet({ open, setOpen }: CharacterBottomSheetProps) { src={data.imageUrl} alt={data.name} style={{ - ...CHARACTER_IMAGE_SIZE[data.name].small, + ...CHARACTER_DATA[data.name].imageSize.small, }} /> diff --git a/src/pages/characterShare/CharacterSharePage.tsx b/src/pages/characterShare/CharacterSharePage.tsx index 4b4f8e5..db32a31 100644 --- a/src/pages/characterShare/CharacterSharePage.tsx +++ b/src/pages/characterShare/CharacterSharePage.tsx @@ -9,10 +9,7 @@ import { ArrowLeft, Download } from '@/shared/assets/svgs/icon'; import Header from '@/shared/ui/Header'; import Text from '@/shared/ui/Text'; import { BottomButtonContainer } from '@/shared/styles/bottomButton.styles'; -import { - CHARACTER_DESCRIPTION, - CHARACTER_IMAGE_SIZE, -} from '@/entities/character/config/character'; +import { CHARACTER_DATA } from '@/entities/character/config/character'; import StarChip from '@/features/character-management/ui/StarChip'; import useGetCharacter from '@/features/character-management/api/useGetCharacter'; import * as S from './CharacterSharePage.styles'; @@ -101,7 +98,7 @@ function CharacterSharePage() { src={data.imageBigUrl} alt={data.name} style={{ - ...CHARACTER_IMAGE_SIZE[data.name].big, + ...CHARACTER_DATA[data.name].imageSize.big, }} /> @@ -109,7 +106,7 @@ function CharacterSharePage() { {data.name} - {CHARACTER_DESCRIPTION[data.name]} + {CHARACTER_DATA[data.name].description} From a8b73df4f81735a3ceac833c50669501d17bb1db Mon Sep 17 00:00:00 2001 From: yoouyeon Date: Sun, 12 Apr 2026 13:12:16 +0900 Subject: [PATCH 6/7] =?UTF-8?q?refactor:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8D=98=20collections=20wrapper=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 백엔드 응답 타입에 맞추기 위해서 collections에 담아서 사용중이었음 실제로 사용할때는 collections에서 꺼낸 형태로 사용하고 있었기 때문에 불필요하게 감쌌다가 다시 꺼내는 로직과 관련 타입 제거 --- src/entities/character/api/character.ts | 12 ++++++------ src/entities/character/model/character.type.ts | 4 ---- .../api/useGetCharacterCollection.ts | 2 +- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/entities/character/api/character.ts b/src/entities/character/api/character.ts index b9a185d..b9f1750 100644 --- a/src/entities/character/api/character.ts +++ b/src/entities/character/api/character.ts @@ -1,13 +1,13 @@ import axiosInstance from '@/shared/api/axios'; import { + CharacterItemData, CharacterItemsRawResponse, - CharacterItemsResponse, } from '../model/character.type'; -export const getCharacterCollection = (): Promise => - axiosInstance.get('/collections').then((res) => ({ - collections: res.data.collections.map((item) => ({ +export const getCharacterCollection = (): Promise => + axiosInstance.get('/collections').then((res) => + res.data.collections.map((item) => ({ ...item, acquiredAt: item.acquiredAt ? new Date(item.acquiredAt) : null, - })), - })); + })) + ); diff --git a/src/entities/character/model/character.type.ts b/src/entities/character/model/character.type.ts index a9d008e..739dea3 100644 --- a/src/entities/character/model/character.type.ts +++ b/src/entities/character/model/character.type.ts @@ -21,10 +21,6 @@ interface CharacterItemRaw extends CharacterData { acquiredAt: string | null; } -export interface CharacterItemsResponse { - collections: CharacterItemData[]; -} - export interface CharacterItemsRawResponse { collections: CharacterItemRaw[]; } diff --git a/src/features/character-management/api/useGetCharacterCollection.ts b/src/features/character-management/api/useGetCharacterCollection.ts index 5d17fd1..7469319 100644 --- a/src/features/character-management/api/useGetCharacterCollection.ts +++ b/src/features/character-management/api/useGetCharacterCollection.ts @@ -7,7 +7,7 @@ const useGetCharacterCollection = () => queryFn: getCharacterCollection, // 획득한 캐릭터가 가장 앞에 오도록 정렬 select: (data) => - [...data.collections].sort((a, b) => { + [...data].sort((a, b) => { if ((a.acquiredAt !== null) === (b.acquiredAt !== null)) return 0; return a.acquiredAt !== null ? -1 : 1; }), From e59b17cb101ea2d764bb40e49b399d30da2f1cb3 Mon Sep 17 00:00:00 2001 From: yoouyeon Date: Sun, 12 Apr 2026 13:57:10 +0900 Subject: [PATCH 7/7] =?UTF-8?q?refactor:=20parseDate=20=ED=97=AC=ED=8D=BC?= =?UTF-8?q?=EB=A1=9C=20Date=20=EB=B3=80=ED=99=98=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/character/api/character.ts | 3 ++- src/entities/member/api/assignMember.ts | 3 ++- src/entities/member/api/getProfiles.ts | 3 ++- src/shared/lib/parseDate.ts | 13 +++++++++++++ 4 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 src/shared/lib/parseDate.ts diff --git a/src/entities/character/api/character.ts b/src/entities/character/api/character.ts index b9f1750..b8cf2dc 100644 --- a/src/entities/character/api/character.ts +++ b/src/entities/character/api/character.ts @@ -1,4 +1,5 @@ import axiosInstance from '@/shared/api/axios'; +import { parseDate } from '@/shared/lib/parseDate'; import { CharacterItemData, CharacterItemsRawResponse, @@ -8,6 +9,6 @@ export const getCharacterCollection = (): Promise => axiosInstance.get('/collections').then((res) => res.data.collections.map((item) => ({ ...item, - acquiredAt: item.acquiredAt ? new Date(item.acquiredAt) : null, + acquiredAt: parseDate(item.acquiredAt), })) ); diff --git a/src/entities/member/api/assignMember.ts b/src/entities/member/api/assignMember.ts index 1053fde..445b86a 100644 --- a/src/entities/member/api/assignMember.ts +++ b/src/entities/member/api/assignMember.ts @@ -1,4 +1,5 @@ import axiosInstance from '@/shared/api/axios'; +import { parseDate } from '@/shared/lib/parseDate'; import { MemberProfile, MemberProfileRaw } from '../model/member.type'; // 참여자 선택 api (로그인한 참여자가 정산에 참여하도록 프로필 설정) @@ -12,6 +13,6 @@ export const assignMember = async ( ); return { ...response.data, - paidAt: response.data.paidAt ? new Date(response.data.paidAt) : null, + paidAt: parseDate(response.data.paidAt), }; }; diff --git a/src/entities/member/api/getProfiles.ts b/src/entities/member/api/getProfiles.ts index 1c0d45d..4adf22f 100644 --- a/src/entities/member/api/getProfiles.ts +++ b/src/entities/member/api/getProfiles.ts @@ -1,4 +1,5 @@ import axiosInstance from '@/shared/api/axios'; +import { parseDate } from '@/shared/lib/parseDate'; import { MemberProfile, MemberProfileRawData } from '../model/member.type'; // TODO : 기존 groupToken들을 사용하는 방식을 settlementCode를 사용하는 방식으로 변경해야 함. @@ -11,6 +12,6 @@ export const getProfiles = async ( ); return response.data.members.map((member) => ({ ...member, - paidAt: member.paidAt ? new Date(member.paidAt) : null, + paidAt: parseDate(member.paidAt), })); }; diff --git a/src/shared/lib/parseDate.ts b/src/shared/lib/parseDate.ts new file mode 100644 index 0000000..295c769 --- /dev/null +++ b/src/shared/lib/parseDate.ts @@ -0,0 +1,13 @@ +import { isValid, parseISO } from 'date-fns'; + +/** + * API 응답의 ISO 날짜 문자열을 Date 객체로 변환합니다. + * + * new Date("invalid")는 truthy한 Invalid Date를 반환해 이후 format() 등에서 RangeError를 반환하기 때문에 parseISO + isValid로 유효성을 검사한 후 변환합니다. + * 유효하지 않으면 null을 반환합니다. + */ +export const parseDate = (value: string | null): Date | null => { + if (!value) return null; + const parsed = parseISO(value); + return isValid(parsed) ? parsed : null; +};