구현할 기능
이미 담긴 상품을 다시 담으면 지금은 "이미 위시리스트에 등록된 상품이에요." 안내만 뜨고 끝난다. 어떤 상품인지 유저가 찾아갈 방법이 없다.
서버가 409 응답 data에 대상 id를 실어주기로 해서, 안내 토스트에 보러가기 버튼을 붙여 그 위시로 데려간다.
POST /api/v1/wishlists 409 WISH-009 data: { wishId }
토스트 "이미 위시리스트에 등록된 상품이에요." [보러가기]
└→ /archive/wish/{wishId}
범위는 위시만이다. 서버가 토너먼트 링크 담기(TOURNAMENT-009)에도 data: { tournamentItemId }를 대칭으로 내려주지만, 그건 core#999의 응답 계약이지 이 이슈가 다루는 화면이 아니다.
서버 선행 조건 — 해소됨
TeamPiKi/core#999 머지 완료. dev 배포 여부만 확인하면 착수 가능하다. ErrorPayload 인터페이스 + AlreadyRegisteredException을 두고 GlobalExceptionHandler가 as?로 가려 응답 data에 싣는 구조다.
data class ExistingWish(val wishId: Long)
함께 바뀌는 것 — 429 → 409 우선순위 역전
서버 PR이 중복 검사를 한도 차감보다 앞으로 옮긴다. 응답이 유실된 뒤의 재시도가 담지도 못한 채 한도만 깎던 문제 때문이다.
그래서 이미 담긴 상품 + 몫 소진 상황의 status가 바뀐다.
기존: 429 WISH-010 (한도 소진) ← 담을 수 있었는데 몫이 없다는 잘못된 안내
변경: 409 WISH-009 (이미 담김)
WISH-010(429)은 에러 코드 동기화 이슈(#594)에서 카탈로그에 추가되는 코드다. 두 작업의 순서가 엇갈리면 이 케이스에서 문구가 빈다 — 어느 쪽이 먼저 머지되든 상관없도록 카탈로그 추가를 선행하는 편이 안전하다.
하위 호환 판단 — 맞다, 다만 타입은 손봐야 한다
status·code·detail이 그대로고 data만 더해지므로 기존 onError 토스트 경로는 그대로 동작한다. 구버전 앱이 웹을 그대로 받아도 깨지지 않는다. data가 없는 응답이면 보러가기 없는 토스트로 폴백한다.
단 한 가지, 클라 타입이 막고 있다.
// apps/web/src/types/api.ts — data 가 리터럴 null 이라 접근 자체가 안 된다
export type ApiErrorResponseT = {
data: null;
code: ApiErrorCodeT;
};
→ ApiErrorResponseT<TData = null>로 넓힌다. 기본값을 null로 두면 기존 사용처는 전부 무변경이다.
결정된 것
payload는 wishId만 — 위시 객체 전체는 안 싣는다
상세 화면의 GET을 없애려고 409 응답에 위시 객체를 통째로 싣는 안은 채택하지 않는다.
- 보러가기는 대부분 안 누른다. 안 눌린 요청마다
memo·item·priceHistory(최대 50건)가 딸려 나간다
- 아껴지는 건 상세 진입 로딩 한 번뿐이고, 그마저 409 시점
prefetchQuery로 payload를 안 키우고 같은 효과를 낼 수 있다
- 에러 응답이 도메인 객체를 나르기 시작하면 상세 응답 스키마 변경이 에러 응답까지 묶인다. 서버가 식별자만 싣기로 한 것도 같은 이유다
링크 담기 다이얼로그도 토스트로 — 단, code로 가른다
ByLinkDialog는 지금 showErrorToast: false로 토스트를 끄고 입력창 helperText에 인라인으로 띄우며 다이얼로그를 열어둔다. 링크 등록 409의 주 경로가 여기인데 토스트가 없어 보러가기가 붙을 자리가 없다.
ERROR_CODE.WISH_ALREADY_EXISTS일 때만 토스트(보러가기) + 다이얼로그 닫기. 그 외 4xx는 지금처럼 인라인 helperText + 다이얼로그 유지.
- status가 아니라 code로 가른다. 같은 409에
USER-003(탈퇴 계정)도 있고 그건 인터셉터가 가져간다. 지금 안 닿는다는 이유로 status로 가르면 서버가 409 코드를 늘렸을 때 조용히 딸려 들어온다. CLAUDE.md의 "code 분기는 같은 status에서 동작이 갈릴 때만 2차로" 에 해당하는 자리다
- 400(형식 오류·미지원 쇼핑몰)에서 다이얼로그를 열어두는 건 URL을 고쳐야 하는 케이스라서다. 이미 담긴 상품은 URL이 틀린 게 아니라 고칠 게 없고, 보러가기를 누르면 상세로 떠나니 닫는 게 맞다
토스트 duration 연장
전역 duration이 3000ms라 버튼을 누를 시간으로 짧다. 액션이 있는 토스트만 연장한다.
작업 상세 내용
타입·유틸
토스트
409 처리
문서
확인 필요
참고
- 서버 PR:
TeamPiKi/core#999 — ErrorPayload / AlreadyRegisteredException
- 대상 파일:
hooks/usePostWishLink.ts, components/get-item-dialog/ByLinkDialog.tsx, components/toast/index.tsx
- 409가 나는 진입 경로: 링크 담기 다이얼로그(
ByLinkDialog), 앱 공유하기(useShareIntentWish)
구현할 기능
이미 담긴 상품을 다시 담으면 지금은 "이미 위시리스트에 등록된 상품이에요." 안내만 뜨고 끝난다. 어떤 상품인지 유저가 찾아갈 방법이 없다.
서버가 409 응답
data에 대상 id를 실어주기로 해서, 안내 토스트에 보러가기 버튼을 붙여 그 위시로 데려간다.범위는 위시만이다. 서버가 토너먼트 링크 담기(
TOURNAMENT-009)에도data: { tournamentItemId }를 대칭으로 내려주지만, 그건core#999의 응답 계약이지 이 이슈가 다루는 화면이 아니다.서버 선행 조건 — 해소됨
TeamPiKi/core#999머지 완료. dev 배포 여부만 확인하면 착수 가능하다.ErrorPayload인터페이스 +AlreadyRegisteredException을 두고GlobalExceptionHandler가as?로 가려 응답data에 싣는 구조다.함께 바뀌는 것 — 429 → 409 우선순위 역전
서버 PR이 중복 검사를 한도 차감보다 앞으로 옮긴다. 응답이 유실된 뒤의 재시도가 담지도 못한 채 한도만 깎던 문제 때문이다.
그래서 이미 담긴 상품 + 몫 소진 상황의 status가 바뀐다.
WISH-010(429)은 에러 코드 동기화 이슈(#594)에서 카탈로그에 추가되는 코드다. 두 작업의 순서가 엇갈리면 이 케이스에서 문구가 빈다 — 어느 쪽이 먼저 머지되든 상관없도록 카탈로그 추가를 선행하는 편이 안전하다.하위 호환 판단 — 맞다, 다만 타입은 손봐야 한다
status·code·detail이 그대로고
data만 더해지므로 기존onError토스트 경로는 그대로 동작한다. 구버전 앱이 웹을 그대로 받아도 깨지지 않는다.data가 없는 응답이면 보러가기 없는 토스트로 폴백한다.단 한 가지, 클라 타입이 막고 있다.
→
ApiErrorResponseT<TData = null>로 넓힌다. 기본값을null로 두면 기존 사용처는 전부 무변경이다.결정된 것
payload는
wishId만 — 위시 객체 전체는 안 싣는다상세 화면의
GET을 없애려고 409 응답에 위시 객체를 통째로 싣는 안은 채택하지 않는다.memo·item·priceHistory(최대 50건)가 딸려 나간다prefetchQuery로 payload를 안 키우고 같은 효과를 낼 수 있다링크 담기 다이얼로그도 토스트로 — 단, code로 가른다
ByLinkDialog는 지금showErrorToast: false로 토스트를 끄고 입력창 helperText에 인라인으로 띄우며 다이얼로그를 열어둔다. 링크 등록 409의 주 경로가 여기인데 토스트가 없어 보러가기가 붙을 자리가 없다.ERROR_CODE.WISH_ALREADY_EXISTS일 때만 토스트(보러가기) + 다이얼로그 닫기. 그 외 4xx는 지금처럼 인라인 helperText + 다이얼로그 유지.USER-003(탈퇴 계정)도 있고 그건 인터셉터가 가져간다. 지금 안 닿는다는 이유로 status로 가르면 서버가 409 코드를 늘렸을 때 조용히 딸려 들어온다. CLAUDE.md의 "code 분기는 같은 status에서 동작이 갈릴 때만 2차로" 에 해당하는 자리다토스트 duration 연장
전역
duration이 3000ms라 버튼을 누를 시간으로 짧다. 액션이 있는 토스트만 연장한다.작업 상세 내용
타입·유틸
types/api.ts—ApiErrorResponseT<TData = null>로 제네릭화utils/apiError.ts—getApiErrorData<T>(error)헬퍼 추가 (getApiErrorCode옆)토스트
components/toast—actionButton스타일 (현재 sonner 기본 스타일이gray-700배경 위에 그대로 뜬다)duration연장409 처리
usePostWishLink.ts—WISH-009분기에서wishId추출, 토스트 + 보러가기 →ROUTES.WISH_EDIT(wishId)ByLinkDialog.tsx—WISH-009만 인라인 대신 토스트 + 다이얼로그 닫기문서
docs/spec/api-status-audit.md—POST /wishlists409 섹션 갱신확인 필요
core#999dev 배포 완료 여부→ 머지 완료core#999머지→data필드명·타입{ wishId },Long목록에 없는 위시일 때 처리→ 포커스 자체가 빠져 해당 없음. 항상 상세로 이동참고
TeamPiKi/core#999—ErrorPayload/AlreadyRegisteredExceptionhooks/usePostWishLink.ts,components/get-item-dialog/ByLinkDialog.tsx,components/toast/index.tsxByLinkDialog), 앱 공유하기(useShareIntentWish)