Skip to content

feat: 앱 공유 바텀시트 에러 상태 분기 - #501

Merged
ychany merged 3 commits into
devfrom
feat/485-share-sheet-error-states
Aug 12, 2026
Merged

feat: 앱 공유 바텀시트 에러 상태 분기#501
ychany merged 3 commits into
devfrom
feat/485-share-sheet-error-states

Conversation

@ychany

@ychany ychany commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

작업 요약

  • 앱 공유 바텀시트의 실패 사유를 시안대로 분기합니다.
  • 비로그인 상태에서 로그인 유도 화면을 노출합니다.
  • 재시도 가능한 오류에 다시 시도 버튼을 추가합니다.

작업 세부 내용

기존에는 실패 사유와 무관하게 "저장하지 못했어요" 한 화면만 떴습니다. 사유는 이미 구분돼 넘어오는데 시트가 버리고 있었습니다.

1. 사유를 식별자로 반환

postWishLinkFromShare 가 문구 대신 reason 을 반환합니다. 문구는 시트가 정합니다.

export type ShareFailureReasonT =
  'unauthenticated' | 'sessionExpired' | 'network' | 'server';

응답 body 파싱을 추가해 서버 code 도 함께 담습니다. 로컬 판단 실패(토큰 없음·네트워크 예외·링크 추출 실패)는 응답이 없으므로 code 가 비어 있습니다.

refreshToken 이 없어 갱신조차 못 한 401 도 sessionExpired 로 묶었습니다 — 기존에는 이 경로가 일반 오류로 빠졌습니다.

2. 화면 3갈래 분기

사유 타이틀 / 서브 버튼
unauthenticated 위시를 담으려면 로그인 해주세요 나중에 할게요 · 로그인하기
sessionExpired 저장하지 못했어요 / 로그인이 만료됐어요 나중에 할게요 · 로그인하기
network 〃 / 네트워크 연결을 확인해주세요 확인 · 다시 시도
server 〃 / 일시적인 오류가 발생했어요 확인 · 다시 시도
재시도 후 재실패 서브 문구 유지 확인

시안상 토큰 만료는 재시도가 아니라 로그인 유도입니다 — 재시도해도 결과가 같기 때문입니다.

문구는 ERROR_MESSAGE_MAP 과 달라 시트에 별도로 뒀습니다.

3. 로그인 유도 일러스트

시안의 태그 일러스트는 기존 에셋에 없어 새로 추가했습니다. Figma 벡터 8조각을 좌표대로 합성해 @3x(354×429)로 내보냈습니다. 표시 크기는 시안 실측값 118×143 입니다.

스크린샷

image

연관 이슈

closes #485

Summary by CodeRabbit

  • 새로운 기능
    • 공유 위시 등록 실패 원인을 인증 실패, 세션 만료, 네트워크 오류, 서버 오류로 구분해 안내합니다.
    • 인증 관련 오류 발생 시 로그인 화면으로 이동합니다.
    • 네트워크 및 서버 오류는 1회 재시도할 수 있으며, 재시도 후에는 확인 버튼을 제공합니다.
    • 공유 URL 누락도 서버 오류로 처리해 적절한 안내 화면을 표시합니다.
    • 성공, 로그인, 재시도 화면의 문구와 버튼 배치를 개선했습니다.

실패 사유와 무관하게 같은 에러 화면만 보여주던 문제를 시안대로 나눈다.

- postWishLinkFromShare 가 message 대신 사유(reason)를 반환하도록 변경
  응답 body 를 파싱해 서버 code 도 함께 담는다
- refreshToken 이 없어 갱신 못 한 401 도 sessionExpired 로 분류
- 로그인 유도 / 재시도 가능 / 재시도 불가 3갈래로 화면 분기
- 재시도는 1회까지, 이후엔 확인 버튼만 노출
- 로그인 유도 화면 태그 일러스트 추가 (Figma 벡터 합성 @3x)
@vercel

vercel Bot commented Aug 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
piki Ready Ready Preview Aug 12, 2026 5:24pm

@github-actions github-actions Bot added the APP Good for newcomers label Aug 12, 2026
@github-actions github-actions Bot added the feature New feature or request label Aug 12, 2026
@github-actions

Copy link
Copy Markdown

Discord 스레드 연동용 메타데이터입니다. discord-pr-bot 워크플로가 자동 생성하며, 수정·삭제하면 PR 과 Discord 알림 연동이 끊깁니다.

@github-actions
github-actions Bot requested a review from iOdiO89 August 12, 2026 11:44
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

공유 위시 등록 결과가 실패 사유와 서버 코드를 반환하도록 변경되었습니다. 바텀시트는 인증·세션·네트워크·서버 오류를 구분합니다. 인증 오류에는 로그인을 유도하고, 재시도 가능한 오류에는 1회 재시도를 제공합니다.

Changes

공유 위시 실패 처리

Layer / File(s) Summary
실패 결과 분류 및 코드 전달
apps/app/utils/postWishLinkFromShare.ts
ShareFailureReasonTretryable을 결과에 추가했습니다. 인증 없음, 세션 만료, 네트워크 오류, 서버 오류를 구분하고 응답 code를 전달합니다.
바텀시트 상태 및 등록 흐름
apps/app/components/ShareBottomSheet.tsx
등록 상태에 실패 사유와 재시도 횟수를 반영했습니다. 상품 URL 누락을 재시도 불가 서버 오류로 처리하고, 재시도 시 등록 effect를 다시 실행합니다.
실패·성공 화면 및 버튼 UI
apps/app/components/ShareBottomSheet.tsx
인증 오류에는 로그인 이동을 표시합니다. 네트워크·서버 오류에는 사유별 문구를 표시합니다. 재시도 가능한 오류는 1회 재시도하고, 재시도 후에는 확인 버튼만 표시합니다.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant 공유 바텀시트
  participant 위시 등록 함수
  participant 서버
  공유 바텀시트->>위시 등록 함수: 공유 위시 등록 요청
  위시 등록 함수->>서버: 위시 링크 전송
  서버-->>위시 등록 함수: 상태 코드와 오류 code 반환
  위시 등록 함수-->>공유 바텀시트: 실패 사유와 재시도 가능 여부 반환
  공유 바텀시트-->>공유 바텀시트: 로그인, 재시도 또는 확인 화면 표시
Loading

Possibly related PRs

  • TeamPiKi/client#353: ShareBottomSheet.tsx의 오류 상태 아이콘을 변경한 PR입니다.

Suggested reviewers: iodio89

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목이 공유 바텀시트의 실패 사유별 상태 분기라는 주요 변경을 명확하게 설명합니다.
Linked Issues check ✅ Passed #485의 실패 사유 분기, 로그인 유도, 재시도 처리, 오류 코드 반환 요구사항을 변경 사항이 충족합니다.
Out of Scope Changes check ✅ Passed 변경 사항은 #485의 공유 바텀시트 오류 처리와 API 결과 구조 변경 범위에 포함됩니다.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/485-share-sheet-error-states

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/app/components/ShareBottomSheet.tsx`:
- Around line 112-114: Update the isLoginPrompt condition in ShareBottomSheet to
use isLoginRequired, so both unauthenticated and sessionExpired reasons display
the login prompt instead of the generic failure state. Preserve the existing
LOGIN_REQUIRED_REASONS classification and downstream rendering behavior.

In `@apps/app/utils/postWishLinkFromShare.ts`:
- Around line 34-42: Update readErrorCode to eliminate undefined returns for
ESLint no-undefined compliance: return null when the response body lacks a code
or JSON parsing fails, and update its callers to check for a non-null error code
before using it.
- Line 12: ShareFailureReasonT의 server 사유만으로 재시도 여부를 판단하지 않도록 명시적 retryable 필드
또는 로컬 실패 사유를 추가하세요. apps/app/utils/postWishLinkFromShare.ts 12-12에서 결과 타입을 확장하고,
49-49의 API 주소 누락 결과는 재시도 불가로 반환하세요. apps/app/components/ShareBottomSheet.tsx
59-61의 상품 URL 누락 결과도 재시도 불가로 저장하세요. 같은 파일 112-115의 canRetry는 로그인 여부가 아니라 결과의 명시적
재시도 가능 여부를 사용해 계산하세요.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6974da19-5afd-430c-ac85-9247b7759411

📥 Commits

Reviewing files that changed from the base of the PR and between f53b255 and 7b3c27b.

⛔ Files ignored due to path filters (1)
  • apps/app/assets/images/share-bottom-sheet/login-tag.png is excluded by !**/*.png
📒 Files selected for processing (2)
  • apps/app/components/ShareBottomSheet.tsx
  • apps/app/utils/postWishLinkFromShare.ts

Comment on lines +112 to +114
const isLoginRequired = LOGIN_REQUIRED_REASONS.includes(reason);
/** 토큰 자체가 없으면 실패가 아니라 로그인 유도 화면 */
const isLoginPrompt = reason === 'unauthenticated';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

sessionExpired에도 로그인 유도 화면을 표시하세요.

LOGIN_REQUIRED_REASONSsessionExpired를 로그인 유도 사유로 분류합니다. 그러나 Line 114는 unauthenticated만 로그인 유도 화면으로 처리합니다. 따라서 세션 만료는 일반 실패 제목과 오류 이미지를 표시합니다. isLoginPrompt 조건에 isLoginRequired를 사용하세요.

수정 예시
-    const isLoginPrompt = reason === 'unauthenticated';
+    const isLoginPrompt = isLoginRequired;

Also applies to: 123-136

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/app/components/ShareBottomSheet.tsx` around lines 112 - 114, Update the
isLoginPrompt condition in ShareBottomSheet to use isLoginRequired, so both
unauthenticated and sessionExpired reasons display the login prompt instead of
the generic failure state. Preserve the existing LOGIN_REQUIRED_REASONS
classification and downstream rendering behavior.

Comment thread apps/app/utils/postWishLinkFromShare.ts
Comment thread apps/app/utils/postWishLinkFromShare.ts
server 사유가 재시도 가능한 5xx 와 불가능한 로컬 오류를 함께
나타내, API 주소 누락·링크 추출 실패에도 "다시 시도" 가 떴다.

- 결과 타입에 retryable 추가, 각 실패 지점이 직접 판단
- canRetry 를 로그인 여부의 부정값 대신 retryable 로 계산

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/app/utils/postWishLinkFromShare.ts (2)

85-91: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

4xx 응답을 재시도 가능으로 분류하지 마세요.

현재 401 이외의 모든 HTTP 오류가 retryable: true가 되어 400, 403, 404, 422에서도 재시도 버튼이 표시됩니다. 4xx는 기본적으로 retryable: false로 반환하고, 재시도가 필요한 429만 명시적으로 허용하세요. ShareBottomSheet.tsx가 이 값을 UI에 직접 전달합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/app/utils/postWishLinkFromShare.ts` around lines 85 - 91, Update the
failure classification in the postWish response handling so 4xx statuses are
non-retryable by default, while preserving sessionExpired for 401 and explicitly
allowing retry only for 429; keep server errors retryable. Ensure the retryable
value returned to ShareBottomSheet reflects these status rules.

65-72: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

HTTP 상태별 오류를 분리하세요.

  • 토큰 갱신 응답은 401일 때만 sessionExpired로 처리하세요. 5xx는 server, retryable: true와 응답 code를 반환하세요. 그 외 4xx는 retryable: false로 반환하세요.
  • 최종 위시 등록 응답은 401만 sessionExpired로 처리하세요. 5xx만 server, retryable: true로 반환하고, 4xx는 retryable: false로 반환하세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/app/utils/postWishLinkFromShare.ts` around lines 65 - 72, Update both
error-handling sites in apps/app/utils/postWishLinkFromShare.ts: the
token-refresh branch at lines 65-72 and the final wish-registration response at
lines 85-91. In the postTokenRefresh failure path, reserve sessionExpired for
401, return server with retryable true and the response code for 5xx, and return
other 4xx errors with retryable false; apply the same status classification to
the final post-wish response, preserving token cleanup for 401 responses.
🧹 Nitpick comments (1)
apps/app/utils/postWishLinkFromShare.ts (1)

91-96: 🗄️ Data Integrity & Integration | 🔵 Trivial

재시도 가능한 POST의 멱등성을 확인하세요.

retryable: true가 반환되면 /api/v1/wishlists POST를 다시 전송합니다. 서버가 요청을 처리한 뒤 응답만 유실된 경우 재시도하면 동일한 위시가 중복 등록될 수 있습니다.

서버가 사용자와 상품 URL 기준의 unique 제약 또는 idempotency key를 보장하는지 확인하세요. 보장하지 않으면 재시도 전에 서버 중복 방지 로직을 추가하세요.

PR 목표의 서버·네트워크 재시도 요구사항과 postWishLink의 POST 계약에 근거합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/app/utils/postWishLinkFromShare.ts` around lines 91 - 96, Verify the
retry behavior in postWishLink so repeated POST requests are idempotent for the
same user and product URL. Confirm that the /api/v1/wishlists server path
enforces a unique constraint or idempotency key; if not, add server-side
duplicate prevention before allowing retryable failures to trigger another
request.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/app/utils/postWishLinkFromShare.ts`:
- Around line 95-96: Update the error handling in postWishLinkFromShare so only
the request’s network/transport failure returns reason "network" with retryable
true. Handle refreshResponse.json() parsing failures and
TokenStorage.setTokens()/clearTokens() failures separately, preserving their
appropriate failure reasons and retry policies instead of converting them to
network errors.
- Around line 56-59: Include the TokenStorage.getAccessToken and getRefreshToken
calls within the try/catch in postWishLinkFromShare so rejected storage reads
return a failure result instead of propagating. Map storage errors to the
appropriate distinct ShareFailureReasonT and retryable policy, preserving
unauthenticated only for a missing access token and the existing registerWish
contract in ShareBottomSheet.

---

Outside diff comments:
In `@apps/app/utils/postWishLinkFromShare.ts`:
- Around line 85-91: Update the failure classification in the postWish response
handling so 4xx statuses are non-retryable by default, while preserving
sessionExpired for 401 and explicitly allowing retry only for 429; keep server
errors retryable. Ensure the retryable value returned to ShareBottomSheet
reflects these status rules.
- Around line 65-72: Update both error-handling sites in
apps/app/utils/postWishLinkFromShare.ts: the token-refresh branch at lines 65-72
and the final wish-registration response at lines 85-91. In the postTokenRefresh
failure path, reserve sessionExpired for 401, return server with retryable true
and the response code for 5xx, and return other 4xx errors with retryable false;
apply the same status classification to the final post-wish response, preserving
token cleanup for 401 responses.

---

Nitpick comments:
In `@apps/app/utils/postWishLinkFromShare.ts`:
- Around line 91-96: Verify the retry behavior in postWishLink so repeated POST
requests are idempotent for the same user and product URL. Confirm that the
/api/v1/wishlists server path enforces a unique constraint or idempotency key;
if not, add server-side duplicate prevention before allowing retryable failures
to trigger another request.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 85359791-34aa-4b2d-baf5-91da2ce55b00

📥 Commits

Reviewing files that changed from the base of the PR and between 7b3c27b and 5ba4d5d.

📒 Files selected for processing (2)
  • apps/app/components/ShareBottomSheet.tsx
  • apps/app/utils/postWishLinkFromShare.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/app/components/ShareBottomSheet.tsx

Comment on lines 56 to +59
const accessToken = await TokenStorage.getAccessToken();
const refreshToken = await TokenStorage.getRefreshToken();

if (!accessToken) return { ok: false, message: '로그인이 필요해요' };
if (!accessToken) return { ok: false, reason: 'unauthenticated', retryable: false };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

토큰 저장소 예외를 실패 결과로 변환하세요.

Line 56-57의 토큰 조회는 try 밖에서 실행됩니다. 저장소 조회가 reject되면 postWishLinkFromShare가 결과를 반환하지 않고 reject됩니다. apps/app/components/ShareBottomSheet.tsxregisterWish는 이 예외를 처리하지 않으므로 바텀시트가 실패 상태로 전환되지 않습니다.

토큰 조회를 예외 처리 범위에 포함하세요. 저장소 오류를 적절한 ShareFailureReasonTretryable 정책으로 매핑하세요. 실제 비로그인과 저장소 장애를 unauthenticated로 합치지 마세요.

apps/app/utils/tokenStorage.ts:30-36의 비동기 조회와 apps/app/components/ShareBottomSheet.tsx:67-77의 호출 계약에 근거합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/app/utils/postWishLinkFromShare.ts` around lines 56 - 59, Include the
TokenStorage.getAccessToken and getRefreshToken calls within the try/catch in
postWishLinkFromShare so rejected storage reads return a failure result instead
of propagating. Map storage errors to the appropriate distinct
ShareFailureReasonT and retryable policy, preserving unauthenticated only for a
missing access token and the existing registerWish contract in ShareBottomSheet.

Comment on lines 95 to +96
} catch {
return { ok: false, message: '네트워크 오류가 발생했어요' };
return { ok: false, reason: 'network', retryable: true };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

네트워크 예외와 내부 오류를 구분하세요.

Line 95-96의 catch는 네트워크 예외뿐 아니라 refreshResponse.json(), TokenStorage.setTokens(), TokenStorage.clearTokens()의 실패도 잡습니다. 잘못된 갱신 응답이나 저장소 오류가 networkretryable: true로 변환됩니다.

전송 오류만 이 경로에서 처리하세요. JSON 파싱 오류와 저장소 오류는 각각의 실패 사유와 재시도 정책으로 분류하세요.

PR 목표의 실패 사유 구분 요구사항에 근거합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/app/utils/postWishLinkFromShare.ts` around lines 95 - 96, Update the
error handling in postWishLinkFromShare so only the request’s network/transport
failure returns reason "network" with retryable true. Handle
refreshResponse.json() parsing failures and
TokenStorage.setTokens()/clearTokens() failures separately, preserving their
appropriate failure reasons and retry policies instead of converting them to
network errors.

@ychany
ychany merged commit ab1c6a1 into dev Aug 12, 2026
6 of 7 checks passed
@ychany
ychany deleted the feat/485-share-sheet-error-states branch August 12, 2026 17:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

APP Good for newcomers feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: 앱 공유 바텀시트 에러 상태 분기 (로그인 유도 · 재시도)

1 participant