From 57855c94053a80141ad9c42bdec12f18a660c1ee Mon Sep 17 00:00:00 2001 From: joyeongchan Date: Tue, 25 Aug 2026 20:53:28 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=EC=98=81=EC=88=98=EC=A6=9D=20?= =?UTF-8?q?=EA=B3=B5=EC=9C=A0=20=EB=B2=84=ED=8A=BC=20=ED=86=B5=ED=95=A9=20?= =?UTF-8?q?(=EA=B3=B5=EC=9C=A0=201=EA=B0=9C=20+=20=EC=9D=B8=EC=8A=A4?= =?UTF-8?q?=ED=83=80=20=EC=8A=A4=ED=86=A0=EB=A6=AC=20=EA=B3=B5=EC=9C=A0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReceiptShareDialog.tsx | 118 +++--------------- .../[id]/result/_utils/shareReceiptImage.ts | 41 ------ 2 files changed, 18 insertions(+), 141 deletions(-) diff --git a/apps/web/src/app/tournament/[id]/result/_components/receipt-share-dialog/ReceiptShareDialog.tsx b/apps/web/src/app/tournament/[id]/result/_components/receipt-share-dialog/ReceiptShareDialog.tsx index bd69b526..757ff753 100644 --- a/apps/web/src/app/tournament/[id]/result/_components/receipt-share-dialog/ReceiptShareDialog.tsx +++ b/apps/web/src/app/tournament/[id]/result/_components/receipt-share-dialog/ReceiptShareDialog.tsx @@ -3,23 +3,16 @@ import { useEffect, useRef, useState, useSyncExternalStore } from 'react'; import { toast } from 'sonner'; -import { CopyIconFill, DownloadIconFill, LinkIconOutline } from '@/assets/icons'; -import InstagramIcon from '@/assets/icons/social/instagram.svg'; +import Button from '@/components/button'; import { Drawer, DrawerContent, DrawerDescription, DrawerTitle } from '@/components/drawer'; import Skeleton from '@/components/skeleton'; import { ANALYTICS_EVENT } from '@/consts/analytics'; import { useInstagramStoryShare } from '@/hooks/useInstagramStoryShare'; import { logAnalyticsEvent } from '@/utils/analytics'; -import { cn } from '@/utils/cn'; import { isWebview } from '@/utils/webBridge'; import type { RankedProductT } from '../../../_common/_types/tournament'; -import { - captureReceiptImage, - copyReceiptImage, - saveReceiptImage, - shareReceiptImageFile, -} from '../../_utils/shareReceiptImage'; +import { captureReceiptImage, shareReceiptImageFile } from '../../_utils/shareReceiptImage'; import ReceiptShareCaptureLayer from './ReceiptShareCaptureLayer'; type ReceiptShareDialogProps = { @@ -33,42 +26,6 @@ type ReceiptShareDialogProps = { isApp?: boolean; }; -type ShareActionProps = { - icon: React.ReactNode; - label: string; - /** 원형 배경색 클래스 — 브랜드 아이콘은 고유 배경을 쓴다 */ - iconBackgroundClassName?: string; - disabled?: boolean; - onClick: () => void; -}; - -function ShareAction({ - icon, - label, - iconBackgroundClassName = 'bg-gray-50', - disabled, - onClick, -}: ShareActionProps) { - return ( - - ); -} - function ReceiptShareDialog({ open, onOpenChange, @@ -132,37 +89,6 @@ function ReceiptShareDialog({ }; }, [open]); - const handleSave = () => { - if (!imageBlob) return; - const isSaved = saveReceiptImage(imageBlob); - if (!isSaved) { - toast.error('저장에 실패했습니다.'); - return; - } - - logAnalyticsEvent(ANALYTICS_EVENT.RECEIPT_SHARE, { - tournament_id: tournamentId, - method: 'save', - }); - toast.success('이미지를 저장했어요.'); - }; - - const handleCopy = async () => { - if (!imageBlob) return; - - const isCopied = await copyReceiptImage(imageBlob); - if (!isCopied) { - toast.warning('이 브라우저에서는 복사를 지원하지 않아요. 저장을 이용해주세요.'); - return; - } - - logAnalyticsEvent(ANALYTICS_EVENT.RECEIPT_SHARE, { - tournament_id: tournamentId, - method: 'copy', - }); - toast.success('이미지를 복사했어요.'); - }; - const handleShareToStory = async () => { if (!imageBlob) return; @@ -239,7 +165,7 @@ function ReceiptShareDialog({ 영수증 공유 - 영수증 이미지를 저장하거나 공유할 수 있어요. + 영수증 이미지를 공유할 수 있어요.
@@ -255,34 +181,26 @@ function ReceiptShareDialog({ )}
- {/* 앱에서는 스토리 공유가 더해져 4개 — 좁은 화면에서 넘치지 않게 간격 상한을 둔다 */} -
- } - label="이미지 저장" - disabled={!imageBlob} - onClick={handleSave} - /> - } - label="이미지 복사" - disabled={!imageBlob} - onClick={handleCopy} - /> - } - label="링크 공유" +
+ + {isAppEnvironment && ( - } - iconBackgroundClassName="border border-gray-75 bg-white" - label="스토리 공유" + )}
diff --git a/apps/web/src/app/tournament/[id]/result/_utils/shareReceiptImage.ts b/apps/web/src/app/tournament/[id]/result/_utils/shareReceiptImage.ts index f98398be..001df831 100644 --- a/apps/web/src/app/tournament/[id]/result/_utils/shareReceiptImage.ts +++ b/apps/web/src/app/tournament/[id]/result/_utils/shareReceiptImage.ts @@ -6,20 +6,6 @@ const MIME = 'image/png'; /** 공유 이미지 가로 폭 (디자인 스펙) — 캡처 대상 실제 폭에 맞춰 pixelRatio 를 역산한다 */ export const SHARE_IMAGE_WIDTH = 1080; -const downloadBlob = (blob: Blob, fileName: string) => { - const url = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = fileName; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(url); -}; - -/** `download` 속성 미지원(iOS 웹뷰 등) 이면 클릭해도 저장되지 않는다 */ -const isDownloadSupported = () => 'download' in document.createElement('a'); - const withTimeout = (promise: Promise, ms: number): Promise => new Promise((resolve, reject) => { const timeoutId = window.setTimeout(() => reject(new Error('TIMEOUT')), ms); @@ -86,33 +72,6 @@ export const captureReceiptImage = async (element: HTMLElement): Promise = return blob; }; -/** 캡처된 blob 을 파일로 저장 — 다운로드 시작에 실패하면 false */ -export const saveReceiptImage = (blob: Blob): boolean => { - if (!isDownloadSupported()) return false; - - try { - downloadBlob(blob, FILE_NAME); - return true; - } catch { - return false; - } -}; - -/** - * 캡처된 blob 을 클립보드에 복사. - * ClipboardItem 미지원(안드로이드 웹뷰 등) 이면 false 를 반환해 호출부가 대체 동작을 고른다. - */ -export const copyReceiptImage = async (blob: Blob): Promise => { - if (typeof ClipboardItem === 'undefined' || !navigator.clipboard?.write) return false; - - try { - await navigator.clipboard.write([new ClipboardItem({ [MIME]: blob })]); - return true; - } catch { - return false; - } -}; - /** * 공유 시트가 이미 떠 있는데 navigator.share() 를 다시 부르면 나는 에러. * 브라우저마다 이름이 갈린다 — Chrome 계열은 InvalidStateError, Safari 는 NotAllowedError. From 4a6de2d5dadeeefdb924a1dfdaa8ea05b79f8a4f Mon Sep 17 00:00:00 2001 From: joyeongchan Date: Tue, 25 Aug 2026 21:06:51 +0900 Subject: [PATCH 2/2] =?UTF-8?q?test:=20=EC=98=81=EC=88=98=EC=A6=9D=20?= =?UTF-8?q?=EA=B3=B5=EC=9C=A0=20=EC=8B=9C=ED=8A=B8=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EC=B6=B0=20E2E=20=EC=85=80=EB=A0=89?= =?UTF-8?q?=ED=84=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/e2e/specs/tournament/tournamentResult.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/e2e/specs/tournament/tournamentResult.spec.ts b/apps/web/e2e/specs/tournament/tournamentResult.spec.ts index b8f07ea9..15655a54 100644 --- a/apps/web/e2e/specs/tournament/tournamentResult.spec.ts +++ b/apps/web/e2e/specs/tournament/tournamentResult.spec.ts @@ -79,8 +79,8 @@ test('웹 브라우저에서는 공유 시트에 스토리 공유 버튼이 없 await page.goto('/tournament/3/result'); await page.getByRole('button', { name: '영수증 저장' }).click(); - await expect(page.getByRole('button', { name: '이미지 저장' })).toBeVisible(); - await expect(page.getByRole('button', { name: '스토리 공유' })).toBeHidden(); + await expect(page.getByRole('button', { name: '공유하기', exact: true })).toBeVisible(); + await expect(page.getByRole('button', { name: '스토리로 바로 공유하기' })).toBeHidden(); }); test.describe('앱(웹뷰) 환경', () => { @@ -126,7 +126,7 @@ test.describe('앱(웹뷰) 환경', () => { await page.goto('/tournament/3/result'); await page.getByRole('button', { name: '영수증 저장' }).click(); - const storyButton = page.getByRole('button', { name: '스토리 공유' }); + const storyButton = page.getByRole('button', { name: '스토리로 바로 공유하기' }); await expect(storyButton).toBeEnabled(); await storyButton.click();