충돌 해결: iOS 네이티브 브릿지와 로딩 스피너 통합
- iOS 환경에서 네이티브 브릿지로 이미지 저장
- 로딩 스피너 유지
- 파일명 형식 통일 (cash-receipt-YYMMDDHHmmss.png)
- 다운로드 후 자동으로 팝업 닫기

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jay Sheen
2025-11-12 14:42:19 +09:00
9 changed files with 215 additions and 121 deletions

View File

@@ -78,6 +78,7 @@
"react-router": "^7.8.2",
"react-router-dom": "^7.8.2",
"react-slidedown": "^2.4.7",
"react-spinners": "^0.17.0",
"react-tabs": "^6.1.0",
"react-toastify": "^11.0.5",
"react-tooltip": "^5.29.1",

14
pnpm-lock.yaml generated
View File

@@ -209,6 +209,9 @@ importers:
react-slidedown:
specifier: ^2.4.7
version: 2.4.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
react-spinners:
specifier: ^0.17.0
version: 0.17.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
react-tabs:
specifier: ^6.1.0
version: 6.1.0(react@19.1.1)
@@ -4829,6 +4832,12 @@ packages:
react: ^16.3.0 || 17
react-dom: ^16.3.0 || 17
react-spinners@0.17.0:
resolution: {integrity: sha512-L/8HTylaBmIWwQzIjMq+0vyaRXuoAevzWoD35wKpNTxxtYXWZp+xtgkfD7Y4WItuX0YvdxMPU79+7VhhmbmuTQ==}
peerDependencies:
react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-tabs@6.1.0:
resolution: {integrity: sha512-6QtbTRDKM+jA/MZTTefvigNxo0zz+gnBTVFw2CFVvq+f2BuH0nF0vDLNClL045nuTAdOoK/IL1vTP0ZLX0DAyQ==}
peerDependencies:
@@ -11218,6 +11227,11 @@ snapshots:
react-dom: 19.1.1(react@19.1.1)
tslib: 2.8.1
react-spinners@0.17.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
dependencies:
react: 19.1.1
react-dom: 19.1.1(react@19.1.1)
react-tabs@6.1.0(react@19.1.1):
dependencies:
clsx: 2.1.1

View File

@@ -3,11 +3,20 @@ import { snackBar, appBridge } from '@/shared/lib';
import { toPng } from 'html-to-image';
import { useTranslation } from 'react-i18next';
import '@/shared/ui/assets/css/style-tax-invoice.css';
import { useEffect } from 'react';
import { useEffect, useState, CSSProperties } from 'react';
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';
const override: CSSProperties = {
position: 'fixed',
display: 'block',
margin: '0 auto',
top: '50%',
left: '50%',
};
export interface CashReceiptSampleProps {
cashReceiptSampleOn: boolean;
setCashReceiptSampleOn: (cashReceiptSampleOn: boolean) => void;
@@ -29,6 +38,9 @@ export const CashReceiptSample = ({
customerInfo,
productInfo
}: CashReceiptSampleProps) => {
let [loading, setLoading] = useState<boolean>(false);
let [color, setColor] = useState<string>('#0b0606');
const { t } = useTranslation();
const downloadImage = async () => {
@@ -36,22 +48,24 @@ export const CashReceiptSample = ({
try {
const imageDataUrl = await toPng(section);
const fileName = 'cash-receipt-' + moment().format('YYMMDDHHmmss');
// iOS 네이티브 환경인 경우 네이티브 브릿지 사용
if (appBridge.isIOS()) {
try {
// data:image/png;base64, 부분 제거하고 순수 base64 데이터만 추출
const base64Data = imageDataUrl.split(',')[1];
const fileName = `cash_receipt_${moment().format('YYYYMMDDHHmmss')}.png`;
const result = await appBridge.saveImage({
imageData: base64Data,
fileName: fileName,
fileName: fileName + '.png',
mimeType: 'image/png'
});
if (result.success) {
snackBar(t('common.imageSaved'));
snackBar(t('common.imageSaved'), function(){
onClickToClose();
});
} else {
throw new Error(result.error || 'Failed to save image');
}
@@ -62,14 +76,18 @@ export const CashReceiptSample = ({
} else {
// Android 또는 웹 환경인 경우 기존 방식 사용 (다운로드 링크)
const link = document.createElement('a');
link.download = `cash_receipt_${moment().format('YYYYMMDDHHmmss')}.png`;
link.href = imageDataUrl;
link.download = fileName + '.png';
link.href = imageDataUrl + '#' + fileName + '.png';
link.click();
snackBar(t('common.imageRequested'));
snackBar(t('common.imageRequested'), function(){
onClickToClose();
});
}
} catch (error) {
console.error('Image generation failed:', error);
snackBar(t('common.imageGenerationFailed'));
} finally {
setLoading(false);
}
};
const onClickToClose = () => {
@@ -95,6 +113,7 @@ export const CashReceiptSample = ({
useEffect(() => {
if(!!cashReceiptSampleOn){
setLoading(true);
setTimeout(() => {
downloadImage();
}, 300);
@@ -270,6 +289,21 @@ export const CashReceiptSample = ({
</div>
</div>
</div>
{ !!loading &&
<>
<div
className="bg-dim"
style={{ opacity: '0.4' }}
></div>
<FadeLoader
color={ color }
loading={ loading }
cssOverride={ override }
aria-label="Loading Spinner"
data-testid="loader"
/>
</>
}
</>
);
};

View File

@@ -3,9 +3,11 @@ 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';
import { useEffect } from 'react';
import { ClipLoader, FadeLoader } from 'react-spinners';
import { CSSProperties, useEffect, useState } from 'react';
import { NumericFormat } from 'react-number-format';
import { DepositInfo } from '@/entities/transaction/model/types';
import moment from 'moment';
export interface DepositReceiptSampleProps {
depositReceiptSampleOn: boolean;
@@ -13,23 +15,38 @@ export interface DepositReceiptSampleProps {
depositInfo?: DepositInfo
};
const override: CSSProperties = {
position: 'fixed',
display: 'block',
margin: '0 auto',
top: '50%',
left: '50%',
zIndex: 2000
};
export const DepositReceiptSample = ({
depositReceiptSampleOn,
setDepositReceiptSampleOn,
depositInfo
}: DepositReceiptSampleProps) => {
const { t } = useTranslation();
let [loading, setLoading] = useState<boolean>(false);
let [color, setColor] = useState<string>('#0b0606');
const downloadImage = () => {
const section = document.getElementById('image-section') as HTMLElement
toPng(section).then((image) => {
const link = document.createElement('a');
link.download = 'downloadImage.png';
link.href = image;
let fileName = 'receipt-confirmation-' + moment().format('YYMMDDHHmmss');
link.download = fileName + '.png';
link.href = image + '#' + fileName + '.png';
link.click();
snackBar(t('common.imageRequested'), () => {
onClickToClose();
});
}).finally(() => {
setLoading(false);
});
}
const onClickToClose = () => {
@@ -37,12 +54,13 @@ export const DepositReceiptSample = ({
};
useEffect(() => {
if(!!depositReceiptSampleOn){
if (!!depositReceiptSampleOn) {
setLoading(true);
setTimeout(() => {
downloadImage();
}, 300);
}
}, [depositReceiptSampleOn]);
}, [depositReceiptSampleOn, depositInfo]);
return (
<>
@@ -53,7 +71,7 @@ export const DepositReceiptSample = ({
<div className="header-top">
<img
className="logo"
src={ IMAGE_ROOT + '/mail_nicepay_logo.svg' }
src={IMAGE_ROOT + '/mail_nicepay_logo.svg'}
alt="NICEPAY"
/>
</div>
@@ -83,21 +101,21 @@ export const DepositReceiptSample = ({
</div>
<div className="row">
<div className="k"></div>
<div className="v">{ depositInfo?.depositDate}</div>
<div className="v">{depositInfo?.depositDate}</div>
</div>
<div className="row">
<div className="k"></div>
<div className="v">{ depositInfo?.depositBank }</div>
<div className="v">{depositInfo?.depositBank}</div>
</div>
<div className="row">
<div className="k"></div>
<div className="v">{ depositInfo?.depositAccount }</div>
<div className="v">{depositInfo?.depositAccount}</div>
</div>
<div className="row">
<div className="k"></div>
<div className="v">
<NumericFormat
value={ depositInfo?.amount }
value={depositInfo?.amount}
thousandSeparator
displayType='text'
suffix='원'
@@ -106,16 +124,31 @@ export const DepositReceiptSample = ({
</div>
<div className="row">
<div className="k"></div>
<div className="v">{ depositInfo?.depositReason }</div>
<div className="v">{depositInfo?.depositReason}</div>
</div>
<div className="row">
<div className="k">ID</div>
<div className="v">{ depositInfo?.depositId }</div>
<div className="v">{depositInfo?.depositId}</div>
</div>
</div>
</div>
</div>
</div>
{!!loading &&
<>
<div
className="bg-dim"
style={{zIndex: 1500, opacity: '0.4' }}
></div>
<FadeLoader
color={color}
loading={loading}
cssOverride={override}
aria-label="Loading Spinner"
data-testid="loader"
/>
</>
}
</>
);
};

View File

@@ -194,7 +194,7 @@ export const ListItem = ({
rs.push(
<div
className="transaction-details"
key={ tid }
key={ tid + 'details' }
>
<span>{ getTime() }</span>
<span className="separator">|</span>
@@ -213,7 +213,10 @@ export const ListItem = ({
}
else if(transactionCategory === TransactionCategory.CashReceipt){
rs.push(
<div className="transaction-details">
<div
className="transaction-details"
key={ tid + 'details' }
>
<span>{ getTime() }</span>
<span className="separator">|</span>
<span>{ transactionType }</span>
@@ -226,7 +229,10 @@ export const ListItem = ({
}
else if(transactionCategory === TransactionCategory.Escrow){
rs.push(
<div className="transaction-details">
<div
className="transaction-details"
key={ tid + 'details' }
>
<span>{ getTime() }</span>
<span className="separator">|</span>
<span>{ deliveryStatus }</span>
@@ -246,7 +252,10 @@ export const ListItem = ({
}
else if(transactionCategory === TransactionCategory.Billing){
rs.push(
<div className="transaction-details">
<div
className="transaction-details"
key={ tid + 'details' }
>
<span>{ getTime() }</span>
<span className="separator">|</span>
<span>{ processResult }</span>

View File

@@ -216,7 +216,9 @@ export const AmountInfoSection = ({
};
cashReceiptReceiptSendEamil(params).then((rs: CashReceiptReceiptSendEmailResponse) => {
console.log(rs);
snackBar('이메일로 현금영수증 요청이 완료되었습니다.');
if(rs.message){
snackBar(rs.message);
}
}).catch((e: any) => {
if(e.response?.data?.error?.message){
snackBar(e.response?.data?.error?.message);

View File

@@ -3,8 +3,8 @@ import { API_URL_VAT_RETURN } from '@/shared/api/api-url-vat-return';
import { resultify } from '@/shared/lib/resultify';
import { NiceAxiosError } from '@/shared/@types/error';
import {
VatReturnTaxInvoiceSendEmailResponse,
VatReturnTaxInvoiceSendEmailParams,
VatReturnTaxInvoiceSendEmailResponse
} from '../model/types';
import {
useMutation,

View File

@@ -55,7 +55,9 @@ export const AmountSection = ({
};
vatReturnTaxInvoiceSendEmail(params).then((rs: VatReturnTaxInvoiceSendEmailResponse) => {
console.log(rs);
snackBar('이메일로 세금계산서 요청이 완료되었습니다.');
if(rs.message){
snackBar(rs.message);
}
}).catch((e: any) => {
if(e.response?.data?.error?.message){
snackBar(e.response?.data?.error?.message);

View File

@@ -121,7 +121,6 @@ export const HomePage = () => {
let userFavorite = useStore.getState().UserStore.userFavorite;
setFavoriteItems(userFavorite);
callHomeBannerList();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const setBottomBannerEffect = (mode: boolean) => {