From 55a4ab45e16e6807bfcbd1ccf8e65221c92fca8b Mon Sep 17 00:00:00 2001 From: Emithen <86219540+Emithen@users.noreply.github.com> Date: Thu, 27 Aug 2026 00:57:12 +0900 Subject: [PATCH] =?UTF-8?q?fix(my):=20=EB=A7=88=EC=9D=B4=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EB=AA=A9=EB=A1=9D=20API=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=97=94=EB=B2=A8=EB=A1=9C=ED=94=84=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=EB=8C=80=EC=9D=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 서버 dev 브랜치에서 마이 계열 목록 응답이 배열에서 { <목록키>, page, size, hasNext } 엔벨로프로 바뀌면서, 배열을 가정한 컴포넌트가 `.map is not a function`으로 크래시했다. - unwrapList 유틸 추가: 배열/엔벨로프를 모두 배열로 정규화 (서버 main은 아직 배열이라 두 형태를 동시에 지원해야 함) - /votes/mine/created, /votes/mine/voted, /comments/mine 정규화 적용 - 페이지네이션 UI가 없으므로 size=50(서버 상한) 요청해 기존 동작 유지 - /member/point-history: 기본 size=10 절삭 방지 (크래시는 없었음) - 화면 명세의 TODO/추정 엔드포인트를 확정된 값으로 갱신 컴포넌트는 계속 배열을 받으므로 화면 코드 변경 없음. 서버 전환이 전 환경에 끝나면 unwrapList를 제거한다. Co-Authored-By: Claude Opus 5 (cherry picked from commit 1b70f0fd45875b7fea5435c736e0adc1217f62ba) --- apps/web/src/api/member/point.ts | 5 ++- apps/web/src/api/myComments.ts | 8 ++-- apps/web/src/api/votes.ts | 15 ++++--- apps/web/src/types/api/votes.ts | 11 ++++- apps/web/src/utils/pagedResponse.ts | 42 +++++++++++++++++++ docs/screens/SCR-MY-COMMENT-001_my-comment.md | 6 ++- docs/screens/SCR-MY-CREATED-001_my-created.md | 4 +- docs/screens/SCR-MY-POINT-001_my-point.md | 5 ++- docs/screens/SCR-MY-VOTED-001_my-voted.md | 4 +- 9 files changed, 84 insertions(+), 16 deletions(-) create mode 100644 apps/web/src/utils/pagedResponse.ts diff --git a/apps/web/src/api/member/point.ts b/apps/web/src/api/member/point.ts index add9d15..281e9fc 100644 --- a/apps/web/src/api/member/point.ts +++ b/apps/web/src/api/member/point.ts @@ -1,10 +1,13 @@ import { authApi } from '../instance/authApi' import { PointHistoryResponse } from '@/types/_shared/pointHistory' +import { MAX_PAGE_SIZE } from '@/utils/pagedResponse' export const fetchPointHistory = async (): Promise => { try { + // 서버가 기본 size=10으로 잘라 보내므로 명시적으로 최대치를 요청한다. + // (엔벨로프 키 `pointHistory`는 전환 전후 동일해 별도 정규화가 필요 없다.) const response = await authApi.get( - '/member/point-history', + `/member/point-history?size=${MAX_PAGE_SIZE}`, ) return response.data } catch (error) { diff --git a/apps/web/src/api/myComments.ts b/apps/web/src/api/myComments.ts index cd44d13..c00a0f7 100644 --- a/apps/web/src/api/myComments.ts +++ b/apps/web/src/api/myComments.ts @@ -1,12 +1,14 @@ import { authApi } from '@/api/instance/authApi' import { MyCommentsResponse } from '@/types/api/myComments' +import { MAX_PAGE_SIZE, unwrapList } from '@/utils/pagedResponse' export const fetchMyComments = async (sort: string = 'latest') => { try { - const res = await authApi.get( - `/comments/mine?sort=${sort}`, + // 페이지네이션 UI가 없으므로 한 번에 최대치까지 받아 기존 동작을 유지한다. + const res = await authApi.get( + `/comments/mine?sort=${sort}&size=${MAX_PAGE_SIZE}`, ) - return res.data + return unwrapList(res.data, 'comments') } catch (error) { throw error } diff --git a/apps/web/src/api/votes.ts b/apps/web/src/api/votes.ts index 91bd54b..c33bea7 100644 --- a/apps/web/src/api/votes.ts +++ b/apps/web/src/api/votes.ts @@ -1,4 +1,5 @@ -import { CreateVoteData, MineVotesResponse } from '@/types/api/votes' +import { CreateVoteData, MineVoteItem } from '@/types/api/votes' +import { MAX_PAGE_SIZE, unwrapList } from '@/utils/pagedResponse' import { authApi } from './instance/authApi' export interface VoteOption { @@ -80,11 +81,13 @@ export const fetchMineVotesCreated = async ( params.append('category', category) } params.append('sort', sort) + // 페이지네이션 UI가 없으므로 한 번에 최대치까지 받아 기존 동작을 유지한다. + params.append('size', String(MAX_PAGE_SIZE)) - const response = await authApi.get( + const response = await authApi.get( `/votes/mine/created?${params.toString()}`, ) - return response.data + return unwrapList(response.data, 'votes') } catch (error) { throw error } @@ -101,11 +104,13 @@ export const fetchMineVotesVoted = async ( params.append('category', category) } params.append('sort', sort) + // 페이지네이션 UI가 없으므로 한 번에 최대치까지 받아 기존 동작을 유지한다. + params.append('size', String(MAX_PAGE_SIZE)) - const response = await authApi.get( + const response = await authApi.get( `/votes/mine/voted?${params.toString()}`, ) - return response.data + return unwrapList(response.data, 'votes') } catch (error) { throw error } diff --git a/apps/web/src/types/api/votes.ts b/apps/web/src/types/api/votes.ts index 66177b1..ab3d661 100644 --- a/apps/web/src/types/api/votes.ts +++ b/apps/web/src/types/api/votes.ts @@ -1,4 +1,5 @@ import { VoteCategory } from '../_shared/vote' +import type { PageMeta } from '@/utils/pagedResponse' export type CreateVoteOption = { content: string @@ -17,7 +18,8 @@ export type MineVoteOption = { imageUrl?: string | null } -export type MineVotesResponse = { +/** 내가 만든/투표한 밸런스 게임 목록의 개별 항목 */ +export type MineVoteItem = { voteId: number title: string content: string | null @@ -25,4 +27,9 @@ export type MineVotesResponse = { totalVoteCount: number createdAt: string options: MineVoteOption[] -}[] +} + +/** `/votes/mine/*` 페이지 엔벨로프 응답 */ +export type PagedMineVotesResponse = PageMeta & { + votes: MineVoteItem[] +} diff --git a/apps/web/src/utils/pagedResponse.ts b/apps/web/src/utils/pagedResponse.ts new file mode 100644 index 0000000..e0d3b21 --- /dev/null +++ b/apps/web/src/utils/pagedResponse.ts @@ -0,0 +1,42 @@ +/** + * 목록 API 응답 형태 과도기 대응 유틸. + * + * 서버가 목록 응답을 배열에서 페이지 엔벨로프로 전환하는 중이다. + * + * - 배열: `[{...}, {...}]` + * - 엔벨로프: `{ votes: [...], page, size, hasNext }` + * + * 서버 `dev`에는 엔벨로프가 적용됐지만 `main`에는 아직 배열이라, + * 동일한 프론트 코드가 두 환경 모두에서 동작해야 한다. + * 서버 전환이 모든 환경에 끝나면 이 유틸을 제거하고 엔벨로프 타입만 남긴다. + */ + +/** 페이지 엔벨로프 공통 메타 (서버 PaginationValidator 기준) */ +export type PageMeta = { + page: number + size: number + hasNext: boolean +} + +/** 서버가 허용하는 최대 page size. 초과 시 400을 반환한다. */ +export const MAX_PAGE_SIZE = 50 + +/** + * 배열 또는 페이지 엔벨로프를 항상 배열로 정규화한다. + * + * @param data 응답 본문 (배열 또는 엔벨로프) + * @param key 엔벨로프 안에서 목록이 담긴 키 (예: `votes`, `comments`) + */ +export function unwrapList(data: unknown, key: string): T[] { + if (Array.isArray(data)) return data as T[] + + if (data !== null && typeof data === 'object') { + const items = (data as Record)[key] + if (Array.isArray(items)) return items as T[] + } + + // 예상 밖의 형태는 빈 목록으로 처리한다. + // 배열을 가정한 컴포넌트에서 `.map is not a function`으로 터지는 것보다, + // 빈 상태 문구를 보여주는 편이 낫다. + return [] +} diff --git a/docs/screens/SCR-MY-COMMENT-001_my-comment.md b/docs/screens/SCR-MY-COMMENT-001_my-comment.md index a4d1b5d..31ed57b 100644 --- a/docs/screens/SCR-MY-COMMENT-001_my-comment.md +++ b/docs/screens/SCR-MY-COMMENT-001_my-comment.md @@ -70,8 +70,10 @@ | Method | Endpoint | 용도 | |---|---|---| -| GET | `/member/comments` (추정) | 내 댓글 목록 | -| DELETE | (TODO) | 다중 댓글 삭제 | +| GET | `/comments/mine?sort&size=50` | 내 댓글 목록 (응답 키 `comments`) | +| DELETE | `/comments/{commentId}` | 댓글 삭제 (다중 선택 시 병렬 호출) | +> **응답 형태 과도기**: 서버가 목록 응답을 배열 → `{ <목록키>, page, size, hasNext }` 엔벨로프로 전환 중이다(서버 `dev` 적용 / `main` 미적용). 프론트는 `unwrapList`로 두 형태를 모두 배열로 정규화하며, 페이지네이션 UI가 없으므로 `size=50`(서버 상한)을 요청해 기존 동작을 유지한다. + ## 📎 관련 문서 diff --git a/docs/screens/SCR-MY-CREATED-001_my-created.md b/docs/screens/SCR-MY-CREATED-001_my-created.md index a2da1fe..fbead04 100644 --- a/docs/screens/SCR-MY-CREATED-001_my-created.md +++ b/docs/screens/SCR-MY-CREATED-001_my-created.md @@ -48,7 +48,9 @@ | Method | Endpoint | 용도 | |---|---|---| -| GET | (TODO: HistoryPage 컴포넌트 확인) | 내 생성 게임 목록 | +| GET | `/votes/mine/created?category&sort&size=50` | 내 생성 게임 목록 (응답 키 `votes`) | +> **응답 형태 과도기**: 서버가 목록 응답을 배열 → `{ <목록키>, page, size, hasNext }` 엔벨로프로 전환 중이다(서버 `dev` 적용 / `main` 미적용). 프론트는 `unwrapList`로 두 형태를 모두 배열로 정규화하며, 페이지네이션 UI가 없으므로 `size=50`(서버 상한)을 요청해 기존 동작을 유지한다. + ## 📎 관련 문서 diff --git a/docs/screens/SCR-MY-POINT-001_my-point.md b/docs/screens/SCR-MY-POINT-001_my-point.md index 72bedf7..863582e 100644 --- a/docs/screens/SCR-MY-POINT-001_my-point.md +++ b/docs/screens/SCR-MY-POINT-001_my-point.md @@ -126,7 +126,10 @@ | Method | Endpoint | 용도 | |---|---|---| -| GET | `/member/point-history` | 포인트 내역 + 잔액(각 항목 `remainingPoint`) 조회 | +| GET | `/member/point-history?size=50` | 포인트 내역 + 잔액(각 항목 `remainingPoint`) 조회 | + +> **페이지네이션**: 서버 기본 `size=10`으로 내역이 잘리므로 상한 50을 명시 요청한다. 엔벨로프 키 `pointHistory`는 전환 전후 동일해 별도 정규화가 필요 없다. + ## 🎨 디자인 토큰 참조 diff --git a/docs/screens/SCR-MY-VOTED-001_my-voted.md b/docs/screens/SCR-MY-VOTED-001_my-voted.md index 9119575..c8d0056 100644 --- a/docs/screens/SCR-MY-VOTED-001_my-voted.md +++ b/docs/screens/SCR-MY-VOTED-001_my-voted.md @@ -49,7 +49,9 @@ | Method | Endpoint | 용도 | |---|---|---| -| GET | (TODO: HistoryPage 컴포넌트 확인) | 내 투표 이력 | +| GET | `/votes/mine/voted?category&sort&size=50` | 내 투표 이력 (응답 키 `votes`) | +> **응답 형태 과도기**: 서버가 목록 응답을 배열 → `{ <목록키>, page, size, hasNext }` 엔벨로프로 전환 중이다(서버 `dev` 적용 / `main` 미적용). 프론트는 `unwrapList`로 두 형태를 모두 배열로 정규화하며, 페이지네이션 UI가 없으므로 `size=50`(서버 상한)을 요청해 기존 동작을 유지한다. + ## 📎 관련 문서