Skip to content

feat: 이미 담긴 상품 재등록 시 토스트에서 해당 위시로 이동 #595

Description

@iOdiO89

구현할 기능

이미 담긴 상품을 다시 담으면 지금은 "이미 위시리스트에 등록된 상품이에요." 안내만 뜨고 끝난다. 어떤 상품인지 유저가 찾아갈 방법이 없다.

서버가 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을 두고 GlobalExceptionHandleras?로 가려 응답 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라 버튼을 누를 시간으로 짧다. 액션이 있는 토스트만 연장한다.

작업 상세 내용

타입·유틸

  • types/api.tsApiErrorResponseT<TData = null> 로 제네릭화
  • utils/apiError.tsgetApiErrorData<T>(error) 헬퍼 추가 (getApiErrorCode 옆)

토스트

  • components/toastactionButton 스타일 (현재 sonner 기본 스타일이 gray-700 배경 위에 그대로 뜬다)
  • 액션 토스트 duration 연장

409 처리

  • usePostWishLink.tsWISH-009 분기에서 wishId 추출, 토스트 + 보러가기 → ROUTES.WISH_EDIT(wishId)
  • ByLinkDialog.tsxWISH-009만 인라인 대신 토스트 + 다이얼로그 닫기

문서

  • docs/spec/api-status-audit.mdPOST /wishlists 409 섹션 갱신

확인 필요

  • 디자인 — 보러가기 버튼 스펙 (핸드오프 예정)
  • 서버core#999 dev 배포 완료 여부
  • core#999 머지 → 머지 완료
  • data 필드명·타입{ wishId }, Long
  • 목록에 없는 위시일 때 처리 → 포커스 자체가 빠져 해당 없음. 항상 상세로 이동

참고

  • 서버 PR: TeamPiKi/core#999ErrorPayload / AlreadyRegisteredException
  • 대상 파일: hooks/usePostWishLink.ts, components/get-item-dialog/ByLinkDialog.tsx, components/toast/index.tsx
  • 409가 나는 진입 경로: 링크 담기 다이얼로그(ByLinkDialog), 앱 공유하기(useShareIntentWish)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

WEBfeatureNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions