useAppBridge Hook 패턴으로 이미지 저장 기능 개선
iOS 웹뷰에서 이미지 다운로드를 위한 네이티브 브릿지 기능을 React Hook 패턴으로 리팩토링하여 일관성과 재사용성 개선 변경사항: - useAppBridge Hook에 saveImage 메서드 추가 - utils/appBridge.ts에 saveImage 메서드 구현 - 세 개의 샘플 컴포넌트에서 useAppBridge Hook 사용 * cash-receipt-sample.tsx * deposit-receipt-sample.tsx * tax-invoice-sample.tsx - 직접 appBridge import 제거, Hook 패턴으로 통일 - TypeScript 타입 안전성 개선 (null 체크 추가) 기술 개선: - React Hook 패턴으로 컴포넌트 라이프사이클과 통합 - safeCall을 통한 자동 에러 처리 - iOS 환경에서만 네이티브 브릿지 사용 - Android/웹은 기존 다운로드 방식 유지 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { IMAGE_ROOT } from '@/shared/constants/common';
|
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 { toPng } from 'html-to-image';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import '@/shared/ui/assets/css/style-tax-invoice.css';
|
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 { NumericFormat } from 'react-number-format';
|
||||||
import { AmountInfo, CustomerInfo, IssueInfo, MerchantInfo, ProductInfo, TransactionInfo } from '@/entities/transaction/model/types';
|
import { AmountInfo, CustomerInfo, IssueInfo, MerchantInfo, ProductInfo, TransactionInfo } from '@/entities/transaction/model/types';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import { useAppBridge } from '@/hooks/useAppBridge';
|
||||||
|
|
||||||
const override: CSSProperties = {
|
const override: CSSProperties = {
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
@@ -42,6 +43,7 @@ export const CashReceiptSample = ({
|
|||||||
let [color, setColor] = useState<string>('#0b0606');
|
let [color, setColor] = useState<string>('#0b0606');
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { isIOS, saveImage } = useAppBridge();
|
||||||
|
|
||||||
const downloadImage = async () => {
|
const downloadImage = async () => {
|
||||||
const section = document.getElementById('image-section') as HTMLElement;
|
const section = document.getElementById('image-section') as HTMLElement;
|
||||||
@@ -51,23 +53,27 @@ export const CashReceiptSample = ({
|
|||||||
const fileName = 'cash-receipt-' + moment().format('YYMMDDHHmmss');
|
const fileName = 'cash-receipt-' + moment().format('YYMMDDHHmmss');
|
||||||
|
|
||||||
// iOS 네이티브 환경인 경우 네이티브 브릿지 사용
|
// iOS 네이티브 환경인 경우 네이티브 브릿지 사용
|
||||||
if (appBridge.isIOS()) {
|
if (isIOS) {
|
||||||
try {
|
try {
|
||||||
// data:image/png;base64, 부분 제거하고 순수 base64 데이터만 추출
|
// data:image/png;base64, 부분 제거하고 순수 base64 데이터만 추출
|
||||||
const base64Data = imageDataUrl.split(',')[1];
|
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,
|
imageData: base64Data,
|
||||||
fileName: fileName + '.png',
|
fileName: fileName + '.png',
|
||||||
mimeType: 'image/png'
|
mimeType: 'image/png'
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.success) {
|
if (result?.success) {
|
||||||
snackBar(t('common.imageSaved'), function(){
|
snackBar(t('common.imageSaved'), function(){
|
||||||
onClickToClose();
|
onClickToClose();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
throw new Error(result.error || 'Failed to save image');
|
throw new Error(result?.error || 'Failed to save image');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Native image save failed:', error);
|
console.error('Native image save failed:', error);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { CSSProperties, useEffect, useState } from 'react';
|
|||||||
import { NumericFormat } from 'react-number-format';
|
import { NumericFormat } from 'react-number-format';
|
||||||
import { DepositInfo } from '@/entities/transaction/model/types';
|
import { DepositInfo } from '@/entities/transaction/model/types';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import { useAppBridge } from '@/hooks/useAppBridge';
|
||||||
|
|
||||||
export interface DepositReceiptSampleProps {
|
export interface DepositReceiptSampleProps {
|
||||||
depositReceiptSampleOn: boolean;
|
depositReceiptSampleOn: boolean;
|
||||||
@@ -31,23 +32,60 @@ export const DepositReceiptSample = ({
|
|||||||
depositInfo
|
depositInfo
|
||||||
}: DepositReceiptSampleProps) => {
|
}: DepositReceiptSampleProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { isIOS, saveImage } = useAppBridge();
|
||||||
let [loading, setLoading] = useState<boolean>(false);
|
let [loading, setLoading] = useState<boolean>(false);
|
||||||
let [color, setColor] = useState<string>('#0b0606');
|
let [color, setColor] = useState<string>('#0b0606');
|
||||||
|
|
||||||
const downloadImage = () => {
|
const downloadImage = async () => {
|
||||||
const section = document.getElementById('image-section') as HTMLElement
|
const section = document.getElementById('image-section') as HTMLElement;
|
||||||
toPng(section).then((image) => {
|
|
||||||
const link = document.createElement('a');
|
try {
|
||||||
let fileName = 'receipt-confirmation-' + moment().format('YYMMDDHHmmss');
|
const imageDataUrl = await toPng(section);
|
||||||
link.download = fileName + '.png';
|
const fileName = 'receipt-confirmation-' + moment().format('YYMMDDHHmmss');
|
||||||
link.href = image + '#' + fileName + '.png';
|
|
||||||
link.click();
|
// iOS 네이티브 환경인 경우 네이티브 브릿지 사용
|
||||||
snackBar(t('common.imageRequested'), () => {
|
if (isIOS) {
|
||||||
onClickToClose();
|
try {
|
||||||
});
|
// data:image/png;base64, 부분 제거하고 순수 base64 데이터만 추출
|
||||||
}).finally(() => {
|
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);
|
setLoading(false);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
const onClickToClose = () => {
|
const onClickToClose = () => {
|
||||||
setDepositReceiptSampleOn(false);
|
setDepositReceiptSampleOn(false);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { NumericFormat } from 'react-number-format';
|
|||||||
import { FilterMotionDuration, FilterMotionVariants } from '../model/constant';
|
import { FilterMotionDuration, FilterMotionVariants } from '../model/constant';
|
||||||
import { RecipientInfo, SupplierInfo, TransactionDetails } from '@/entities/vat-return/model/types';
|
import { RecipientInfo, SupplierInfo, TransactionDetails } from '@/entities/vat-return/model/types';
|
||||||
import '@/shared/ui/assets/css/style-tax-invoice.css';
|
import '@/shared/ui/assets/css/style-tax-invoice.css';
|
||||||
|
import { useAppBridge } from '@/hooks/useAppBridge';
|
||||||
|
|
||||||
export interface TaxInvoiceSampleProps {
|
export interface TaxInvoiceSampleProps {
|
||||||
taxInvoiceSampleOn: boolean;
|
taxInvoiceSampleOn: boolean;
|
||||||
@@ -34,18 +35,56 @@ export const TaxInvoiceSample = ({
|
|||||||
transactionDetails
|
transactionDetails
|
||||||
}: TaxInvoiceSampleProps) => {
|
}: TaxInvoiceSampleProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { isIOS, saveImage } = useAppBridge();
|
||||||
|
|
||||||
const downloadImage = () => {
|
const downloadImage = async () => {
|
||||||
const section = document.getElementById('image-section') as HTMLElement;
|
const section = document.getElementById('image-section') as HTMLElement;
|
||||||
toPng(section).then((image) => {
|
|
||||||
const link = document.createElement('a');
|
try {
|
||||||
link.download = 'downloadImage.png';
|
const imageDataUrl = await toPng(section);
|
||||||
link.href = image;
|
const fileName = 'tax-invoice-' + moment().format('YYMMDDHHmmss');
|
||||||
link.click();
|
|
||||||
snackBar(t('common.imageRequested'), function(){
|
// iOS 네이티브 환경인 경우 네이티브 브릿지 사용
|
||||||
onClickToClose();
|
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 = () => {
|
const onClickToClose = () => {
|
||||||
setTaxInvoiceSampleOn(false);
|
setTaxInvoiceSampleOn(false);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { appBridge } from '@/utils/appBridge';
|
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';
|
import { LoginResponse } from '@/entities/user/model/types';
|
||||||
|
|
||||||
interface UseAppBridgeReturn {
|
interface UseAppBridgeReturn {
|
||||||
@@ -75,6 +75,9 @@ interface UseAppBridgeReturn {
|
|||||||
|
|
||||||
// 알림 링크 확인
|
// 알림 링크 확인
|
||||||
checkAlarmLink: () => Promise<void>;
|
checkAlarmLink: () => Promise<void>;
|
||||||
|
|
||||||
|
// 이미지 저장
|
||||||
|
saveImage: (request: SaveImageRequest) => Promise<SaveImageResponse | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAppBridge = (): UseAppBridgeReturn => {
|
export const useAppBridge = (): UseAppBridgeReturn => {
|
||||||
@@ -325,6 +328,14 @@ export const useAppBridge = (): UseAppBridgeReturn => {
|
|||||||
return appBridge.safeCall(() => appBridge.checkAlarmLink());
|
return appBridge.safeCall(() => appBridge.checkAlarmLink());
|
||||||
}, [isNativeEnvironment]);
|
}, [isNativeEnvironment]);
|
||||||
|
|
||||||
|
const saveImage = useCallback(async (request: SaveImageRequest): Promise<SaveImageResponse | undefined> => {
|
||||||
|
if (!isIOS) {
|
||||||
|
// iOS가 아닌 경우 undefined 반환 (호출하는 쪽에서 fallback 처리)
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return appBridge.safeCall(() => appBridge.saveImage(request));
|
||||||
|
}, [isIOS]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isNativeEnvironment,
|
isNativeEnvironment,
|
||||||
isAndroid,
|
isAndroid,
|
||||||
@@ -353,6 +364,7 @@ export const useAppBridge = (): UseAppBridgeReturn => {
|
|||||||
getNotificationSetting,
|
getNotificationSetting,
|
||||||
setNotificationSetting,
|
setNotificationSetting,
|
||||||
getLanguage,
|
getLanguage,
|
||||||
checkAlarmLink
|
checkAlarmLink,
|
||||||
|
saveImage
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -3,7 +3,9 @@ import {
|
|||||||
AppBridgeResponse,
|
AppBridgeResponse,
|
||||||
BridgeMessageType,
|
BridgeMessageType,
|
||||||
DeviceInfo,
|
DeviceInfo,
|
||||||
ShareContent
|
ShareContent,
|
||||||
|
SaveImageRequest,
|
||||||
|
SaveImageResponse
|
||||||
} from '@/types';
|
} from '@/types';
|
||||||
|
|
||||||
class AppBridge {
|
class AppBridge {
|
||||||
@@ -230,6 +232,11 @@ class AppBridge {
|
|||||||
return this.sendMessage(BridgeMessageType.CHECK_ALARM_LINK);
|
return this.sendMessage(BridgeMessageType.CHECK_ALARM_LINK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 이미지 저장 (iOS/Android 네이티브)
|
||||||
|
async saveImage(request: SaveImageRequest): Promise<SaveImageResponse> {
|
||||||
|
return this.sendMessage<SaveImageResponse>(BridgeMessageType.SAVE_IMAGE, request);
|
||||||
|
}
|
||||||
|
|
||||||
// 네이티브 환경 체크
|
// 네이티브 환경 체크
|
||||||
isNativeEnvironment(): boolean {
|
isNativeEnvironment(): boolean {
|
||||||
return !!(
|
return !!(
|
||||||
|
|||||||
Reference in New Issue
Block a user