feat: 영수증 공유 바텀시트 추가 (미리보기 + 이미지 저장·복사·채팅방 공유) - #418
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
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 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 configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (2)
📒 Files selected for processing (7)
📝 WalkthroughWalkthrough토너먼트 결과 화면의 영수증 공유를 즉시 캡처 방식에서 공유 다이얼로그 방식으로 변경했습니다. 캡처, 저장, 복사, 파일 공유 기능을 분리하고 영수증 캡처 레이어를 추가했습니다. Changes영수증 공유
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: 저장, 복사 또는 파일 공유 실행
Possibly related issues
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
|
Discord 스레드 연동용 메타데이터입니다. discord-pr-bot 워크플로가 자동 생성하며, 수정·삭제하면 PR 과 Discord 알림 연동이 끊깁니다. |
|
@coderabbitai full review |
✅ Action performedFull 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. |
| saveReceiptImage(imageBlob); | ||
| logAnalyticsEvent(ANALYTICS_EVENT.RECEIPT_SHARE, { tournament_id: tournamentId }); |
There was a problem hiding this comment.
구분 없이 receipt_share로 집계되는거 맞아? 이렇게 한 이유가 있는지 궁금하다
어떤 방식이 실제로 쓰이는지 알 수 없으니까 저장,복사,공유 파라미터를 추가하는 건 어떨까
| saveReceiptImage(imageBlob); | ||
| logAnalyticsEvent(ANALYTICS_EVENT.RECEIPT_SHARE, { tournament_id: tournamentId }); | ||
| toast.success('이미지를 저장했어요.'); |
There was a problem hiding this comment.
이미지 저장되는 거 테스트해봤어?
try/catch가 없어서 저장에 실패해도 항상 성공 토스트가 뜰 것 같아서 조금 우려됨..
There was a problem hiding this comment.
테스트 해보긴 했는데 안전장치 더 달아놨어!
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" />} |
There was a problem hiding this comment.
이거 receipt 오타인 것 같다! 저장할 때 잘못 들어간 것 같은데 수정 부탁해
There was a problem hiding this comment.
우와 이거 꽤 옛날부터 있던 오타네... 다른곳도 점검해서 수정했어!
| <ShareAction | ||
| icon={<KakaoIcon className="size-7" />} | ||
| iconBackgroundClassName="bg-[#FEE500]" | ||
| label="채팅방에 공유" | ||
| disabled={!imageBlob} | ||
| onClick={handleShareToChat} | ||
| /> |
There was a problem hiding this comment.
이 버튼 클릭하면 공유 시트가 올라오는 거 맞지?
뭔가 내 생각엔 카카오톡 이미지니까 바로 카톡이 열릴 거라고 기대했는데 앱 목록이 뜨는게 좀 어색한 것 같아
라벨을 공유하기 로 바꾸거나 아이콘을 수정하는 게 좋을 것 같아~
There was a problem hiding this comment.
맞아 그래서 디자인팀에 물어보니 카카오톡만 생각한게 맞다고 해서 카카오 SDK를 도입해보려고!
619e363 to
cff4714
Compare
There was a problem hiding this comment.
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캡처 실패 후 사용자가 복구할 수 없습니다.
캡처가 실패하면
imageBlob과previewUrl은null로 남습니다. 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]);
captureError가true이면 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가inertboolean 속성을 지원하므로 이 컨테이너에 함께 적용하세요.♻️ 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 valueReact 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
📒 Files selected for processing (5)
apps/web/src/app/tournament/[id]/result/_components/ReceiptDrawMachine.tsxapps/web/src/app/tournament/[id]/result/_components/ResultClient.tsxapps/web/src/app/tournament/[id]/result/_components/receipt-share-dialog/ReceiptShareCaptureLayer.tsxapps/web/src/app/tournament/[id]/result/_components/receipt-share-dialog/ReceiptShareDialog.tsxapps/web/src/app/tournament/[id]/result/_utils/shareReceiptImage.ts
| const RECEIPT_ZOOM = 1.28; | ||
| const RECEIPT_RENDER_WIDTH_PX = 370 / RECEIPT_ZOOM; |
There was a problem hiding this comment.
📐 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:
- 1: https://semisignal.com/rendering-web-content-to-image-with-svg-foreign-object/
- 2: https://dev.to/x_kernel27795/katex-html-to-image-outputs-a-blank-white-image-heres-why-and-how-to-fix-it-2ikd
- 3: Not able to open downloaded SVG in Illustrator, Photoshop, or SVG editors bubkoo/html-to-image#274
- 4: https://github.com/bubkoo/html-to-image/blob/master/src/clone-node.ts
- 5: https://github.com/bubkoo/html-to-image
- 6: WebKit/WebKit@bbc4a14
- 7: https://caniuse.com/css-zoom
- 8: The foreignobject elements do not render on PNG bubkoo/html-to-image#520
- 9: Add option to selectively include style properties when cloning element bubkoo/html-to-image#436
- 10: https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/zoom
CSS zoom 대신 transform: scale()을 사용하세요.
ReceiptShareCaptureLayer가 html-to-image를 통한 SVG foreignObject 직렬화 경로에서 캡처 대상이므로, 비표준인 CSS zoom이 캡처 결과 크기에서 브라우저나 iOS Safari/Android WebView마다 다르게 적용될 수 있습니다. RECEIPT_ZOOM은 style={{ 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.
저장·복사·공유가 모두 receipt_share 로만 집계돼 어떤 방식이 쓰이는지 구분할 수 없어 method(save/copy/share) 를 함께 보낸다.
saveReceiptImage 가 결과를 반환하지 않아 저장에 실패해도 항상 성공으로 안내했다. download 미지원 환경과 예외를 구분해 false 를 반환하고, 호출부에서 실패를 안내한다.
아이콘 파일명(recipt.svg)과 export(ReciptIcon*) 에 있던 오타를 바로잡고 사용처 3곳을 함께 수정했다.
* 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곳을 함께 수정했다.
작업 요약
작업 세부 내용
결과 화면의 "영수증 공유" 버튼이 Figma 시안대로 바텀시트를 열고, 시트 안에서 영수증 미리보기와 공유 액션을 제공하도록 구현했습니다.
기존에는 버튼을 누르면 즉시 캡처 → Web Share(미지원 시 다운로드) 단일 흐름이었습니다.
바텀시트 (ReceiptShareDialog)
Drawer기반, 타이틀 "영수증 공유" + 영수증 미리보기 + 액션 3종navigator.clipboard.write([ClipboardItem])navigator.share({ files })로 OS 공유 시트 → 사용자가 카톡 선택공유 이미지 캡처 레이어 (ReceiptShareCaptureLayer)
화면의
ReceiptDrawMachine은 프린터 연출·마스크 때문에 그대로 캡처하면 잘려서, 공유 전용 레이아웃(하늘색 배경 + 영수증 + 장바구니)을 따로 그려 캡처합니다.공유 이미지 시안 스펙에 맞춰 1080x1920 고정 캔버스로 출력됩니다 (디자이너 요청: width 1080). 1/2 크기로 렌더하고 캡처 시 2배 확대하는 방식입니다.
ReceiptPaper내부가 화면용 px 고정 크기라, 시안 비율에 맞추기 위해 zoom 으로 확대-top-6)·하단 톱니(top-full)가 레이아웃 자리를 차지하지 않아 padding 으로 확보캡처 유틸 분리 (shareReceiptImage.ts)
기존 "캡처 + 공유" 단일 함수를
captureReceiptImage/saveReceiptImage/copyReceiptImage/shareReceiptImageFile로 분리했습니다.includeQueryParams: true추가 — 상품 이미지가 모두 첫 번째 이미지로 중복되던 버그 수정html-to-image의 캐시 키가 기본적으로 쿼리스트링을 잘라내는데, 상품 이미지가/_next/image?url=...형태라 모든 상품이 같은 키(/_next/image)로 취급됐습니다기타
ReceiptDrawMachine의 forwardRef 캡처 인터페이스 제거시안의 4번째 액션인 스토리 공유는 이 PR 에 포함되어 있지 않습니다. 이슈의 1단계(웹만으로 완결 가능한 3종) 범위만 작업했습니다.
미포함 사유:
instagram-stories://share딥링크가 필요한 네이티브 전용 동작이라 웹에서 동작하지 않습니다BRIDGE_GATE등록 + 앱 릴리즈가 선행되어야 합니다따라서 현재 바텀시트에는 액션 버튼이 3개만 노출됩니다. 스토리 공유는 앱 담당자 협의 + 아이콘 에셋 준비 후 2단계에서 별도로 진행합니다.
참고
스크린샷
연관 이슈
closes #417
Summary by CodeRabbit
새로운 기능
버그 수정