diff --git a/src/entities/common/ui/cash-receipt-sample.tsx b/src/entities/common/ui/cash-receipt-sample.tsx index 0cba221..6ca96c8 100644 --- a/src/entities/common/ui/cash-receipt-sample.tsx +++ b/src/entities/common/ui/cash-receipt-sample.tsx @@ -1,5 +1,5 @@ import { IMAGE_ROOT } from '@/shared/constants/common'; -import { snackBar, appBridge } from '@/shared/lib'; +import { snackBar } from '@/shared/lib'; import { toPng } from 'html-to-image'; import { useTranslation } from 'react-i18next'; import '@/shared/ui/assets/css/style-tax-invoice.css'; @@ -8,6 +8,7 @@ import { ClipLoader, FadeLoader } from 'react-spinners'; import { NumericFormat } from 'react-number-format'; import { AmountInfo, CustomerInfo, IssueInfo, MerchantInfo, ProductInfo, TransactionInfo } from '@/entities/transaction/model/types'; import moment from 'moment'; +import { useAppBridge } from '@/hooks/useAppBridge'; const override: CSSProperties = { position: 'fixed', @@ -42,6 +43,7 @@ export const CashReceiptSample = ({ let [color, setColor] = useState('#0b0606'); const { t } = useTranslation(); + const { isIOS, saveImage } = useAppBridge(); const downloadImage = async () => { const section = document.getElementById('image-section') as HTMLElement; @@ -51,23 +53,27 @@ export const CashReceiptSample = ({ const fileName = 'cash-receipt-' + moment().format('YYMMDDHHmmss'); // iOS 네이티브 환경인 경우 네이티브 브릿지 사용 - if (appBridge.isIOS()) { + if (isIOS) { try { // data:image/png;base64, 부분 제거하고 순수 base64 데이터만 추출 const base64Data = imageDataUrl.split(',')[1]; - const result = await appBridge.saveImage({ + if (!base64Data) { + throw new Error('Failed to extract base64 data'); + } + + const result = await saveImage({ imageData: base64Data, fileName: fileName + '.png', mimeType: 'image/png' }); - if (result.success) { + if (result?.success) { snackBar(t('common.imageSaved'), function(){ onClickToClose(); }); } else { - throw new Error(result.error || 'Failed to save image'); + throw new Error(result?.error || 'Failed to save image'); } } catch (error) { console.error('Native image save failed:', error); diff --git a/src/entities/common/ui/deposit-receipt-sample.tsx b/src/entities/common/ui/deposit-receipt-sample.tsx index d9d1ef6..d6e9c7d 100644 --- a/src/entities/common/ui/deposit-receipt-sample.tsx +++ b/src/entities/common/ui/deposit-receipt-sample.tsx @@ -8,6 +8,7 @@ import { CSSProperties, useEffect, useState } from 'react'; import { NumericFormat } from 'react-number-format'; import { DepositInfo } from '@/entities/transaction/model/types'; import moment from 'moment'; +import { useAppBridge } from '@/hooks/useAppBridge'; export interface DepositReceiptSampleProps { depositReceiptSampleOn: boolean; @@ -31,23 +32,60 @@ export const DepositReceiptSample = ({ depositInfo }: DepositReceiptSampleProps) => { const { t } = useTranslation(); + const { isIOS, saveImage } = useAppBridge(); let [loading, setLoading] = useState(false); let [color, setColor] = useState('#0b0606'); - const downloadImage = () => { - const section = document.getElementById('image-section') as HTMLElement - toPng(section).then((image) => { - const link = document.createElement('a'); - let fileName = 'receipt-confirmation-' + moment().format('YYMMDDHHmmss'); - link.download = fileName + '.png'; - link.href = image + '#' + fileName + '.png'; - link.click(); - snackBar(t('common.imageRequested'), () => { - onClickToClose(); - }); - }).finally(() => { + const downloadImage = async () => { + const section = document.getElementById('image-section') as HTMLElement; + + try { + const imageDataUrl = await toPng(section); + const fileName = 'receipt-confirmation-' + moment().format('YYMMDDHHmmss'); + + // iOS 네이티브 환경인 경우 네이티브 브릿지 사용 + if (isIOS) { + try { + // data:image/png;base64, 부분 제거하고 순수 base64 데이터만 추출 + const base64Data = imageDataUrl.split(',')[1]; + + if (!base64Data) { + throw new Error('Failed to extract base64 data'); + } + + const result = await saveImage({ + imageData: base64Data, + fileName: fileName + '.png', + mimeType: 'image/png' + }); + + if (result?.success) { + snackBar(t('common.imageSaved'), () => { + onClickToClose(); + }); + } else { + throw new Error(result?.error || 'Failed to save image'); + } + } catch (error) { + console.error('Native image save failed:', error); + snackBar(t('common.imageSaveFailed')); + } + } else { + // Android 또는 웹 환경인 경우 기존 방식 사용 (다운로드 링크) + const link = document.createElement('a'); + link.download = fileName + '.png'; + link.href = imageDataUrl + '#' + fileName + '.png'; + link.click(); + snackBar(t('common.imageRequested'), () => { + onClickToClose(); + }); + } + } catch (error) { + console.error('Image generation failed:', error); + snackBar(t('common.imageGenerationFailed')); + } finally { setLoading(false); - }); + } } const onClickToClose = () => { setDepositReceiptSampleOn(false); diff --git a/src/entities/common/ui/tax-invoice-sample.tsx b/src/entities/common/ui/tax-invoice-sample.tsx index e928fc3..51eba6e 100644 --- a/src/entities/common/ui/tax-invoice-sample.tsx +++ b/src/entities/common/ui/tax-invoice-sample.tsx @@ -9,6 +9,7 @@ import { NumericFormat } from 'react-number-format'; import { FilterMotionDuration, FilterMotionVariants } from '../model/constant'; import { RecipientInfo, SupplierInfo, TransactionDetails } from '@/entities/vat-return/model/types'; import '@/shared/ui/assets/css/style-tax-invoice.css'; +import { useAppBridge } from '@/hooks/useAppBridge'; export interface TaxInvoiceSampleProps { taxInvoiceSampleOn: boolean; @@ -34,18 +35,56 @@ export const TaxInvoiceSample = ({ transactionDetails }: TaxInvoiceSampleProps) => { const { t } = useTranslation(); + const { isIOS, saveImage } = useAppBridge(); - const downloadImage = () => { + const downloadImage = async () => { const section = document.getElementById('image-section') as HTMLElement; - toPng(section).then((image) => { - const link = document.createElement('a'); - link.download = 'downloadImage.png'; - link.href = image; - link.click(); - snackBar(t('common.imageRequested'), function(){ - onClickToClose(); - }); - }); + + try { + const imageDataUrl = await toPng(section); + const fileName = 'tax-invoice-' + moment().format('YYMMDDHHmmss'); + + // iOS 네이티브 환경인 경우 네이티브 브릿지 사용 + if (isIOS) { + try { + // data:image/png;base64, 부분 제거하고 순수 base64 데이터만 추출 + const base64Data = imageDataUrl.split(',')[1]; + + if (!base64Data) { + throw new Error('Failed to extract base64 data'); + } + + const result = await saveImage({ + imageData: base64Data, + fileName: fileName + '.png', + mimeType: 'image/png' + }); + + if (result?.success) { + snackBar(t('common.imageSaved'), function(){ + onClickToClose(); + }); + } else { + throw new Error(result?.error || 'Failed to save image'); + } + } catch (error) { + console.error('Native image save failed:', error); + snackBar(t('common.imageSaveFailed')); + } + } else { + // Android 또는 웹 환경인 경우 기존 방식 사용 (다운로드 링크) + const link = document.createElement('a'); + link.download = fileName + '.png'; + link.href = imageDataUrl + '#' + fileName + '.png'; + link.click(); + snackBar(t('common.imageRequested'), function(){ + onClickToClose(); + }); + } + } catch (error) { + console.error('Image generation failed:', error); + snackBar(t('common.imageGenerationFailed')); + } }; const onClickToClose = () => { setTaxInvoiceSampleOn(false); diff --git a/src/hooks/useAppBridge.tsx b/src/hooks/useAppBridge.tsx index 27b9a16..6b569b8 100644 --- a/src/hooks/useAppBridge.tsx +++ b/src/hooks/useAppBridge.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useCallback } from 'react'; import { appBridge } from '@/utils/appBridge'; -import { DeviceInfo, NotificationSettingResponse, ShareContent } from '@/types'; +import { DeviceInfo, NotificationSettingResponse, ShareContent, SaveImageRequest, SaveImageResponse } from '@/types'; import { LoginResponse } from '@/entities/user/model/types'; interface UseAppBridgeReturn { @@ -75,6 +75,9 @@ interface UseAppBridgeReturn { // 알림 링크 확인 checkAlarmLink: () => Promise; + + // 이미지 저장 + saveImage: (request: SaveImageRequest) => Promise; } export const useAppBridge = (): UseAppBridgeReturn => { @@ -325,6 +328,14 @@ export const useAppBridge = (): UseAppBridgeReturn => { return appBridge.safeCall(() => appBridge.checkAlarmLink()); }, [isNativeEnvironment]); + const saveImage = useCallback(async (request: SaveImageRequest): Promise => { + if (!isIOS) { + // iOS가 아닌 경우 undefined 반환 (호출하는 쪽에서 fallback 처리) + return undefined; + } + return appBridge.safeCall(() => appBridge.saveImage(request)); + }, [isIOS]); + return { isNativeEnvironment, isAndroid, @@ -353,6 +364,7 @@ export const useAppBridge = (): UseAppBridgeReturn => { getNotificationSetting, setNotificationSetting, getLanguage, - checkAlarmLink + checkAlarmLink, + saveImage }; }; \ No newline at end of file diff --git a/src/utils/appBridge.ts b/src/utils/appBridge.ts index 021bafa..65d18e4 100644 --- a/src/utils/appBridge.ts +++ b/src/utils/appBridge.ts @@ -3,7 +3,9 @@ import { AppBridgeResponse, BridgeMessageType, DeviceInfo, - ShareContent + ShareContent, + SaveImageRequest, + SaveImageResponse } from '@/types'; class AppBridge { @@ -230,6 +232,11 @@ class AppBridge { return this.sendMessage(BridgeMessageType.CHECK_ALARM_LINK); } + // 이미지 저장 (iOS/Android 네이티브) + async saveImage(request: SaveImageRequest): Promise { + return this.sendMessage(BridgeMessageType.SAVE_IMAGE, request); + } + // 네이티브 환경 체크 isNativeEnvironment(): boolean { return !!(