Skip to content

feat: 영수증 공유 바텀시트 추가 (미리보기 + 이미지 저장·복사·채팅방 공유) - #418

Merged
ychany merged 10 commits into
devfrom
feat/417-receipt-share-sheet
Aug 3, 2026
Merged

feat: 영수증 공유 바텀시트 추가 (미리보기 + 이미지 저장·복사·채팅방 공유)#418
ychany merged 10 commits into
devfrom
feat/417-receipt-share-sheet

Conversation

@ychany

@ychany ychany commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

작업 요약

  • 영수증 공유 바텀시트를 추가합니다 (미리보기 + 이미지 저장·복사·채팅방 공유)

작업 세부 내용

결과 화면의 "영수증 공유" 버튼이 Figma 시안대로 바텀시트를 열고, 시트 안에서 영수증 미리보기와 공유 액션을 제공하도록 구현했습니다.

기존에는 버튼을 누르면 즉시 캡처 → Web Share(미지원 시 다운로드) 단일 흐름이었습니다.

바텀시트 (ReceiptShareDialog)

  • Drawer 기반, 타이틀 "영수증 공유" + 영수증 미리보기 + 액션 3종
  • 시트가 열릴 때 한 번만 캡처하고 그 blob 을 모든 액션이 재사용 (기존에는 액션마다 매번 캡처)
  • 캡처 완료 전에는 스켈레톤 노출, 닫을 때 objectURL 정리
액션 동작 미지원 환경
이미지 저장 blob 다운로드
이미지 복사 navigator.clipboard.write([ClipboardItem]) "저장을 이용해주세요" 안내
채팅방에 공유 navigator.share({ files }) 로 OS 공유 시트 → 사용자가 카톡 선택 "저장을 이용해주세요" 안내

채팅방 공유는 카카오 SDK 가 아니라 이미지 파일을 OS 공유 시트로 넘기는 방식입니다. 아이콘만 카카오이고 실제 앱 선택은 사용자가 합니다.

공유 이미지 캡처 레이어 (ReceiptShareCaptureLayer)

화면의 ReceiptDrawMachine 은 프린터 연출·마스크 때문에 그대로 캡처하면 잘려서, 공유 전용 레이아웃(하늘색 배경 + 영수증 + 장바구니)을 따로 그려 캡처합니다.

공유 이미지 시안 스펙에 맞춰 1080x1920 고정 캔버스로 출력됩니다 (디자이너 요청: width 1080). 1/2 크기로 렌더하고 캡처 시 2배 확대하는 방식입니다.

  • ReceiptPaper 내부가 화면용 px 고정 크기라, 시안 비율에 맞추기 위해 zoom 으로 확대
  • 상단 그라데이션(-top-6)·하단 톱니(top-full)가 레이아웃 자리를 차지하지 않아 padding 으로 확보
  • 상품 4개 케이스가 캔버스를 넘지 않도록 zoom·여백 조정

캡처 유틸 분리 (shareReceiptImage.ts)

기존 "캡처 + 공유" 단일 함수를 captureReceiptImage / saveReceiptImage / copyReceiptImage / shareReceiptImageFile 로 분리했습니다.

  • 결과 폭을 항상 1080 으로 고정 (기기 dpr 과 무관하게 동일한 이미지)
  • includeQueryParams: true 추가 — 상품 이미지가 모두 첫 번째 이미지로 중복되던 버그 수정
    • html-to-image 의 캐시 키가 기본적으로 쿼리스트링을 잘라내는데, 상품 이미지가 /_next/image?url=... 형태라 모든 상품이 같은 키(/_next/image)로 취급됐습니다

기타

  • 보관함 "결과 공유하기" 진입도 바텀시트를 열도록 변경 (캡처가 시트로 옮겨져 기존 2초 지연 타이머 제거)
  • 사용되지 않게 된 ReceiptDrawMachine 의 forwardRef 캡처 인터페이스 제거

⚠️ 이 PR 에 포함되지 않은 것 — 인스타 스토리 공유

시안의 4번째 액션인 스토리 공유는 이 PR 에 포함되어 있지 않습니다. 이슈의 1단계(웹만으로 완결 가능한 3종) 범위만 작업했습니다.

미포함 사유:

  • 인스타 스토리 공유는 instagram-stories://share 딥링크가 필요한 네이티브 전용 동작이라 웹에서 동작하지 않습니다
  • 웹브릿지 메시지 신규 추가 + 앱(RN) 핸들러 구현 + BRIDGE_GATE 등록 + 앱 릴리즈가 선행되어야 합니다
  • 인스타그램 아이콘 에셋이 아직 없습니다 (전달받은 SVG 는 내부에 JPEG 가 포함된 형태여서 사용 불가 — 벡터 SVG 재요청 필요)

따라서 현재 바텀시트에는 액션 버튼이 3개만 노출됩니다. 스토리 공유는 앱 담당자 협의 + 아이콘 에셋 준비 후 2단계에서 별도로 진행합니다.

참고

스크린샷

바텀시트 실제 공유 이미지 (1080x1920)
영수증 공유 바텀시트 영수증 공유 이미지

연관 이슈

closes #417

Summary by CodeRabbit

  • 새로운 기능

    • 토너먼트 결과 영수증을 미리 보고 저장, 복사하거나 카카오톡으로 공유할 수 있는 다이얼로그를 추가했습니다.
    • 공유 이미지가 일관된 크기와 품질로 생성됩니다.
    • 이미지 생성, 저장, 복사 및 공유 결과에 따라 안내 메시지를 제공합니다.
  • 버그 수정

    • 이미지·폰트 로딩이 완료된 후 영수증을 생성해 공유 이미지 품질을 개선했습니다.
    • 공유 및 복사를 지원하지 않는 환경에서도 적절한 안내를 제공합니다.

@vercel

vercel Bot commented Aug 1, 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 3, 2026 3:41pm

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@ychany, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 33 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ccd5f40c-0875-4e19-a6ce-57dff8866735

📥 Commits

Reviewing files that changed from the base of the PR and between cff4714 and 5988b80.

⛔ Files ignored due to path filters (2)
  • apps/web/src/assets/icons/fill/receipt.svg is excluded by !**/*.svg
  • apps/web/src/assets/icons/outline/receipt.svg is excluded by !**/*.svg
📒 Files selected for processing (7)
  • apps/web/src/app/archive/tournament/_components/TournamentStatusTab.tsx
  • apps/web/src/app/tournament/[id]/result/_components/ResultClient.tsx
  • apps/web/src/app/tournament/[id]/result/_components/receipt-share-dialog/ReceiptShareDialog.tsx
  • apps/web/src/app/tournament/[id]/result/_utils/shareReceiptImage.ts
  • apps/web/src/assets/icons/fill/index.ts
  • apps/web/src/assets/icons/outline/index.ts
  • apps/web/src/components/tournament-card/MorePopover.tsx
📝 Walkthrough

Walkthrough

토너먼트 결과 화면의 영수증 공유를 즉시 캡처 방식에서 공유 다이얼로그 방식으로 변경했습니다. 캡처, 저장, 복사, 파일 공유 기능을 분리하고 영수증 캡처 레이어를 추가했습니다. ReceiptDrawMachine의 imperative ref API는 제거했습니다.

Changes

영수증 공유

Layer / File(s) Summary
영수증 캡처 레이어
apps/web/src/app/tournament/[id]/result/_components/ReceiptDrawMachine.tsx, apps/web/src/app/tournament/[id]/result/_components/receipt-share-dialog/ReceiptShareCaptureLayer.tsx
ReceiptDrawMachine을 일반 함수 컴포넌트로 변경하고 외부 DOM 조회용 handle을 제거했습니다. 캡처 전용 레이어는 확대된 ReceiptPaper와 Piki 로고를 렌더링합니다.
이미지 캡처 및 공유 유틸리티
apps/web/src/app/tournament/[id]/result/_utils/shareReceiptImage.ts
폰트와 이미지 로딩을 대기한 뒤 1080px 기준으로 이미지를 캡처합니다. 저장, 클립보드 복사, 파일 공유를 별도 함수로 제공합니다.
공유 다이얼로그와 결과 화면 연결
apps/web/src/app/tournament/[id]/result/_components/receipt-share-dialog/ReceiptShareDialog.tsx, apps/web/src/app/tournament/[id]/result/_components/ResultClient.tsx
다이얼로그가 열릴 때 이미지를 한 번 캡처하고 미리보기 URL과 Blob을 관리합니다. 결과 화면의 공유 버튼과 쿼리 액션은 다이얼로그를 엽니다.

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

Sequence Diagram(s)

sequenceDiagram
  participant ResultClient
  participant ReceiptShareDialog
  participant ReceiptShareCaptureLayer
  participant shareReceiptImage
  ResultClient->>ReceiptShareDialog: 공유 다이얼로그 열기
  ReceiptShareDialog->>ReceiptShareCaptureLayer: 캡처 대상 렌더링
  ReceiptShareDialog->>shareReceiptImage: 영수증 Blob 캡처
  shareReceiptImage-->>ReceiptShareDialog: Blob과 미리보기 URL 제공
  ReceiptShareDialog->>shareReceiptImage: 저장, 복사 또는 파일 공유 실행
Loading

Possibly related issues

  • TeamPiKi/client 399: 영수증 공유 UI 개편과 캡처·저장·복사·시스템 공유 분리를 다룹니다.
  • TeamPiKi/client 419: 이 변경의 캡처 유틸리티가 Instagram Story 공유 구현에 사용될 수 있지만, 웹 브리지와 네이티브 핸들러는 포함하지 않습니다.

Possibly related PRs

  • TeamPiKi/client#358: ReceiptDrawMachine.tsxResultClient.tsx의 영수증 렌더링 및 공유 동작 변경이 겹칩니다.
  • TeamPiKi/client#414: ResultClient.tsx의 영수증 CTA 변경과 직접 연결됩니다.

Suggested labels: refactor

Suggested reviewers: iodio89

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목이 영수증 공유 바텀시트와 미리보기, 저장·복사·채팅방 공유 기능이라는 주요 변경 사항을 정확히 설명합니다.
Linked Issues check ✅ Passed [#417] 바텀시트, 1회 캡처 재사용, 미리보기, 이미지 저장·복사·채팅방 공유를 구현했으며 스토리 공유 제외 사유도 명확합니다.
Out of Scope Changes check ✅ Passed 변경 사항은 영수증 캡처 구조와 공유 바텀시트 구현에 직접 관련되며, 제공된 목표와 무관한 코드 변경은 확인되지 않습니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/417-receipt-share-sheet

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.

@github-actions github-actions Bot added feature New feature or request WEB labels Aug 1, 2026
@github-actions
github-actions Bot requested a review from kanghaeun August 1, 2026 16:28
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

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

@iOdiO89

iOdiO89 commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 59 minutes.

@kanghaeun kanghaeun left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

고생했다~👍

Comment on lines +115 to +116
saveReceiptImage(imageBlob);
logAnalyticsEvent(ANALYTICS_EVENT.RECEIPT_SHARE, { tournament_id: tournamentId });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

구분 없이 receipt_share로 집계되는거 맞아? 이렇게 한 이유가 있는지 궁금하다
어떤 방식이 실제로 쓰이는지 알 수 없으니까 저장,복사,공유 파라미터를 추가하는 건 어떨까

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

파라미터 추가 완료!

Comment on lines +115 to +117
saveReceiptImage(imageBlob);
logAnalyticsEvent(ANALYTICS_EVENT.RECEIPT_SHARE, { tournament_id: tournamentId });
toast.success('이미지를 저장했어요.');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

이미지 저장되는 거 테스트해봤어?
try/catch가 없어서 저장에 실패해도 항상 성공 토스트가 뜰 것 같아서 조금 우려됨..

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

테스트 해보긴 했는데 안전장치 더 달아놨어!

saveReceiptImage가 boolean을 반환하도록 바꿨고, 실패 시 저장에 실패했습니다. 토스트를 띄웁니다. try/catch에 더해 download 속성 지원 검사도 넣었습니다 — iOS 웹뷰는 예외 없이 조용히 실패해서 try/catch만으론 못 잡힙니다.

leadingIcon={<DownloadIconFill aria-hidden className="size-5" />}
onClick={handleShareReceiptImage}
disabled={isCapturing}
leadingIcon={<ReciptIconOutline aria-hidden className="size-5" />}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

이거 receipt 오타인 것 같다! 저장할 때 잘못 들어간 것 같은데 수정 부탁해

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

우와 이거 꽤 옛날부터 있던 오타네... 다른곳도 점검해서 수정했어!

Comment on lines +196 to +202
<ShareAction
icon={<KakaoIcon className="size-7" />}
iconBackgroundClassName="bg-[#FEE500]"
label="채팅방에 공유"
disabled={!imageBlob}
onClick={handleShareToChat}
/>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

이 버튼 클릭하면 공유 시트가 올라오는 거 맞지?

뭔가 내 생각엔 카카오톡 이미지니까 바로 카톡이 열릴 거라고 기대했는데 앱 목록이 뜨는게 좀 어색한 것 같아
라벨을 공유하기 로 바꾸거나 아이콘을 수정하는 게 좋을 것 같아~

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

맞아 그래서 디자인팀에 물어보니 카카오톡만 생각한게 맞다고 해서 카카오 SDK를 도입해보려고!

@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: 1

🧹 Nitpick comments (4)
apps/web/src/app/tournament/[id]/result/_components/receipt-share-dialog/ReceiptShareDialog.tsx (1)

91-101: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

캡처 실패 후 사용자가 복구할 수 없습니다.

캡처가 실패하면 imageBlobpreviewUrlnull 로 남습니다. Skeleton 이 계속 표시되고 액션 버튼 3개는 계속 비활성 상태가 됩니다. 시트를 닫고 다시 열어야만 재시도가 가능합니다. 실패 상태를 저장하고 재시도 버튼을 제공하세요.

♻️ 실패 상태와 재시도 추가 예시
   const [previewUrl, setPreviewUrl] = useState<string | null>(null);
+  const [captureError, setCaptureError] = useState(false);
+  const [retryCount, setRetryCount] = useState(0);
 
   /** 시트가 열릴 때 한 번만 캡처하고, 그 blob 을 모든 액션이 재사용한다 */
   useEffect(() => {
     if (!open) return;
@@
       try {
         const blob = await captureReceiptImage(element);
         if (!isActive) return;
 
         objectUrl = URL.createObjectURL(blob);
         setImageBlob(blob);
         setPreviewUrl(objectUrl);
       } catch {
-        if (isActive) toast.error('영수증 이미지를 만들지 못했어요');
+        if (!isActive) return;
+        setCaptureError(true);
+        toast.error('영수증 이미지를 만들지 못했어요');
       }
     };
 
     void capture();
 
     return () => {
       isActive = false;
       if (objectUrl) URL.revokeObjectURL(objectUrl);
       setImageBlob(null);
       setPreviewUrl(null);
+      setCaptureError(false);
     };
-  }, [open]);
+  }, [open, retryCount]);

captureErrortrue 이면 Skeleton 대신 재시도 버튼을 렌더하고, 클릭 시 setRetryCount(count => count + 1) 을 호출하세요.

🤖 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/web/src/app/tournament/`[id]/result/_components/receipt-share-dialog/ReceiptShareDialog.tsx
around lines 91 - 101, Update ReceiptShareDialog’s capture flow to store a
captureError state when captureReceiptImage fails, then render a retry button
instead of the Skeleton when that state is true. Have the retry action increment
the existing capture trigger via setRetryCount(count => count + 1), and reset
the failure state when a new capture begins or succeeds so the image and actions
recover normally.
apps/web/src/app/tournament/[id]/result/_utils/shareReceiptImage.ts (1)

118-124: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

권한 거부 오류가 '미지원' 으로 분류됩니다.

navigator.share 는 사용자 제스처가 소실되거나 권한이 거부되면 NotAllowedError 를 던집니다. 현재 코드는 이 경우를 'unsupported' 로 반환하므로 호출부가 "이 환경에서는 공유를 지원하지 않아요" 를 표시합니다. 실제 원인과 안내가 다릅니다. 실패 상태를 하나 더 구분하면 안내 문구를 정확하게 맞출 수 있습니다.

♻️ 실패 사유 분리 예시
 export const shareReceiptImageFile = async (
   blob: Blob
-): Promise<'shared' | 'cancelled' | 'unsupported'> => {
+): Promise<'shared' | 'cancelled' | 'unsupported' | 'failed'> => {
@@
   try {
     await navigator.share({ files: [file] });
     return 'shared';
   } catch (error) {
     if (error instanceof DOMException && error.name === 'AbortError') return 'cancelled';
-    return 'unsupported';
+    return 'failed';
   }
 };
🤖 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/web/src/app/tournament/`[id]/result/_utils/shareReceiptImage.ts around
lines 118 - 124, Update the navigator.share error handling in shareReceiptImage
to distinguish NotAllowedError from unsupported sharing. Preserve the existing
cancelled result for AbortError, return a dedicated permission-denied result for
DOMException name NotAllowedError, and keep unsupported for other failures.
Update the return type and any callers to handle the new result with the
appropriate user guidance.
apps/web/src/app/tournament/[id]/result/_components/receipt-share-dialog/ReceiptShareCaptureLayer.tsx (2)

21-21: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

inert을 함께 적용해 캡처용 레이어의 포커스 이전을 차단하세요.

aria-hidden만 있으면 컨테이너가 접근성 트리에 제외되더라도 <Link> 상품 카드는 포커스 대상이 될 수 있습니다. React 19가 inert boolean 속성을 지원하므로 이 컨테이너에 함께 적용하세요.

♻️ inert 추가
-      <div aria-hidden className="pointer-events-none fixed top-0 -left-250">
+      <div inert aria-hidden className="pointer-events-none fixed top-0 -left-250">
🤖 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/web/src/app/tournament/`[id]/result/_components/receipt-share-dialog/ReceiptShareCaptureLayer.tsx
at line 21, Update the capture-layer container in ReceiptShareCaptureLayer to
add the inert boolean attribute alongside aria-hidden, ensuring its nested Link
product cards cannot receive focus while preserving the existing layout and
pointer-event behavior.

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

React 19에서 ref prop 방식으로 전환하세요.

React 19에서는 ref가 일반 prop 입니다. forwardRef 래핑과 import를 제거하고, 타입에 ref?: React.Ref<HTMLDivElement>;를 추가한 후 function으로 declare하면 코딩 가이드와 일치합니다.

🤖 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/web/src/app/tournament/`[id]/result/_components/receipt-share-dialog/ReceiptShareCaptureLayer.tsx
at line 1, Update the receipt share capture component to use the React 19 ref
prop pattern: remove the forwardRef import and wrapper, declare the component as
a function, and add an optional React.Ref<HTMLDivElement> ref property to its
props while preserving the existing ref behavior.

Source: Coding guidelines

🤖 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/web/src/app/tournament/`[id]/result/_components/receipt-share-dialog/ReceiptShareCaptureLayer.tsx:
- Around line 8-9: Update ReceiptShareCaptureLayer to replace the nonstandard
CSS zoom usage with transform: scale(RECEIPT_ZOOM) and transformOrigin set to
the left top, while retaining the rendered width through
RECEIPT_RENDER_WIDTH_PX. Adjust surrounding spacing or the capture-only node
sizing as needed so the captured receipt dimensions remain consistent across
browsers and WebViews.

---

Nitpick comments:
In
`@apps/web/src/app/tournament/`[id]/result/_components/receipt-share-dialog/ReceiptShareCaptureLayer.tsx:
- Line 21: Update the capture-layer container in ReceiptShareCaptureLayer to add
the inert boolean attribute alongside aria-hidden, ensuring its nested Link
product cards cannot receive focus while preserving the existing layout and
pointer-event behavior.
- Line 1: Update the receipt share capture component to use the React 19 ref
prop pattern: remove the forwardRef import and wrapper, declare the component as
a function, and add an optional React.Ref<HTMLDivElement> ref property to its
props while preserving the existing ref behavior.

In
`@apps/web/src/app/tournament/`[id]/result/_components/receipt-share-dialog/ReceiptShareDialog.tsx:
- Around line 91-101: Update ReceiptShareDialog’s capture flow to store a
captureError state when captureReceiptImage fails, then render a retry button
instead of the Skeleton when that state is true. Have the retry action increment
the existing capture trigger via setRetryCount(count => count + 1), and reset
the failure state when a new capture begins or succeeds so the image and actions
recover normally.

In `@apps/web/src/app/tournament/`[id]/result/_utils/shareReceiptImage.ts:
- Around line 118-124: Update the navigator.share error handling in
shareReceiptImage to distinguish NotAllowedError from unsupported sharing.
Preserve the existing cancelled result for AbortError, return a dedicated
permission-denied result for DOMException name NotAllowedError, and keep
unsupported for other failures. Update the return type and any callers to handle
the new result with the appropriate user guidance.
🪄 Autofix (Beta)

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: 9bd09e44-a60c-4a41-a71f-b70c66020cd8

📥 Commits

Reviewing files that changed from the base of the PR and between a9f8ab0 and cff4714.

📒 Files selected for processing (5)
  • apps/web/src/app/tournament/[id]/result/_components/ReceiptDrawMachine.tsx
  • apps/web/src/app/tournament/[id]/result/_components/ResultClient.tsx
  • apps/web/src/app/tournament/[id]/result/_components/receipt-share-dialog/ReceiptShareCaptureLayer.tsx
  • apps/web/src/app/tournament/[id]/result/_components/receipt-share-dialog/ReceiptShareDialog.tsx
  • apps/web/src/app/tournament/[id]/result/_utils/shareReceiptImage.ts

Comment on lines +8 to +9
const RECEIPT_ZOOM = 1.28;
const RECEIPT_RENDER_WIDTH_PX = 370 / RECEIPT_ZOOM;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

html-to-image CSS zoom property foreignObject rendering support

💡 Result:

The html-to-image library relies on the SVG element to render HTML content into an image [1][2]. Because of this architectural choice, it inherits the inherent complexities and limitations associated with rendering foreignObject content [1][3]. Regarding your specific query: 1. CSS zoom property support: The html-to-image library copies computed styles from the original DOM node to the cloned node [4]. If the browser's getComputedStyle reports the zoom property, the library typically includes it in the clone [5][4]. However, the CSS zoom property is non-standard, and its behavior inside SVG foreignObject can be unpredictable across browsers [6][7]. For example, there have been historical issues where browsers applied zoom scaling inconsistently—such as applying it twice to font sizes—when nested within a foreignObject [6]. Because html-to-image converts the DOM to an SVG-based format, these rendering quirks are often inherited from the underlying browser engine [1]. 2. foreignObject rendering: The library's core mechanism involves serializing DOM elements into an SVG, wrapping them in a foreignObject, and then drawing that SVG to a canvas [2]. This process is prone to several common failure modes: - External resources: Assets like fonts or cross-origin images must be fully loaded and embedded (often via base64 data URIs) to render correctly [1]. If they are not ready before the capture occurs, they will fail to appear [1][2]. - Browser inconsistencies: Different browsers (Chrome vs. Safari vs. Firefox) handle foreignObject rendering with varying levels of stability, particularly with complex layouts or specific CSS properties [5][8]. - Security restrictions: Content rendered via foreignObject is subject to strict security policies, which can cause issues if the generated SVG is opened in external image editors (like Illustrator) or if it attempts to load external resources without proper CORS headers [1][3]. If you are experiencing rendering issues, common community-recommended workarounds include: - Ensuring all styles (including the zoom property) are explicitly included if using custom style filtering, or allowing the library to copy the computed style automatically [9]. - Implementing a "double-render" approach if fonts are failing to appear, which helps prime the browser's internal font cache [2]. - Using transform: scale instead of zoom, as transform is a standard property that often behaves more predictably during the SVG serialization process [10]. Given that html-to-image is effectively a client-side DOM-to-SVG-to-Canvas bridge, it cannot perfectly replicate every aspect of a live HTML document's rendering, especially when non-standard CSS properties like zoom are involved [1][2].

Citations:


CSS zoom 대신 transform: scale()을 사용하세요.

ReceiptShareCaptureLayerhtml-to-image를 통한 SVG foreignObject 직렬화 경로에서 캡처 대상이므로, 비표준인 CSS zoom이 캡처 결과 크기에서 브라우저나 iOS Safari/Android WebView마다 다르게 적용될 수 있습니다. RECEIPT_ZOOMstyle={{ transform: 'scale(' + RECEIPT_ZOOM + ')', transformOrigin: 'left top', width: RECEIPT_RENDER_WIDTH_PX + 'px' }}처럼 변경하고, 주변 레이아웃 간격을 보정하거나 캡처 노드만 작게 렌더링하는 편이 더 예측 가능합니다.
[maintainability和코드_quality]

🤖 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/web/src/app/tournament/`[id]/result/_components/receipt-share-dialog/ReceiptShareCaptureLayer.tsx
around lines 8 - 9, Update ReceiptShareCaptureLayer to replace the nonstandard
CSS zoom usage with transform: scale(RECEIPT_ZOOM) and transformOrigin set to
the left top, while retaining the rendered width through
RECEIPT_RENDER_WIDTH_PX. Adjust surrounding spacing or the capture-only node
sizing as needed so the captured receipt dimensions remain consistent across
browsers and WebViews.

ychany added 4 commits August 4, 2026 00:30
저장·복사·공유가 모두 receipt_share 로만 집계돼 어떤 방식이 쓰이는지
구분할 수 없어 method(save/copy/share) 를 함께 보낸다.
saveReceiptImage 가 결과를 반환하지 않아 저장에 실패해도 항상 성공으로 안내했다.
download 미지원 환경과 예외를 구분해 false 를 반환하고, 호출부에서 실패를 안내한다.
아이콘 파일명(recipt.svg)과 export(ReciptIcon*) 에 있던 오타를 바로잡고
사용처 3곳을 함께 수정했다.
@ychany
ychany merged commit 78d59d9 into dev Aug 3, 2026
7 checks passed
@ychany
ychany deleted the feat/417-receipt-share-sheet branch August 3, 2026 15:41
iOdiO89 pushed a commit that referenced this pull request Aug 4, 2026
* feat: 영수증 공유 바텀시트 추가 (미리보기 + 저장·복사·채팅방 공유)

* fix: 영수증 캡처 시 상품 이미지가 첫 항목으로 중복되는 문제 수정

* fix: 공유 이미지 상단 영수증 그라데이션 잘림 수정

* fix: 공유 이미지를 시안 스펙(1080x1920 고정 캔버스)에 맞춤

* fix: 공유 이미지 영수증 내용을 시안 비율로 확대

* fix: 영수증 공유 이미지를 시안에 맞춤

* feat: 영수증 공유 이벤트에 method 파라미터 추가

저장·복사·공유가 모두 receipt_share 로만 집계돼 어떤 방식이 쓰이는지
구분할 수 없어 method(save/copy/share) 를 함께 보낸다.

* fix: 영수증 이미지 저장 실패 시 성공 토스트가 뜨던 문제 수정

saveReceiptImage 가 결과를 반환하지 않아 저장에 실패해도 항상 성공으로 안내했다.
download 미지원 환경과 예외를 구분해 false 를 반환하고, 호출부에서 실패를 안내한다.

* fix: Recipt 오타를 Receipt 로 수정

아이콘 파일명(recipt.svg)과 export(ReciptIcon*) 에 있던 오타를 바로잡고
사용처 3곳을 함께 수정했다.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or request WEB

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: 영수증 공유 바텀시트 추가 (미리보기 + 이미지 저장·복사·채팅방 공유, 스토리 공유 앱 분기)

3 participants