Merge branch 'main' of https://gitea.bpsoft.co.kr/nicepayments/nice-app-web
충돌 해결: 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:
@@ -78,6 +78,7 @@
|
|||||||
"react-router": "^7.8.2",
|
"react-router": "^7.8.2",
|
||||||
"react-router-dom": "^7.8.2",
|
"react-router-dom": "^7.8.2",
|
||||||
"react-slidedown": "^2.4.7",
|
"react-slidedown": "^2.4.7",
|
||||||
|
"react-spinners": "^0.17.0",
|
||||||
"react-tabs": "^6.1.0",
|
"react-tabs": "^6.1.0",
|
||||||
"react-toastify": "^11.0.5",
|
"react-toastify": "^11.0.5",
|
||||||
"react-tooltip": "^5.29.1",
|
"react-tooltip": "^5.29.1",
|
||||||
|
|||||||
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
@@ -209,6 +209,9 @@ importers:
|
|||||||
react-slidedown:
|
react-slidedown:
|
||||||
specifier: ^2.4.7
|
specifier: ^2.4.7
|
||||||
version: 2.4.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
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:
|
react-tabs:
|
||||||
specifier: ^6.1.0
|
specifier: ^6.1.0
|
||||||
version: 6.1.0(react@19.1.1)
|
version: 6.1.0(react@19.1.1)
|
||||||
@@ -4829,6 +4832,12 @@ packages:
|
|||||||
react: ^16.3.0 || 17
|
react: ^16.3.0 || 17
|
||||||
react-dom: ^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:
|
react-tabs@6.1.0:
|
||||||
resolution: {integrity: sha512-6QtbTRDKM+jA/MZTTefvigNxo0zz+gnBTVFw2CFVvq+f2BuH0nF0vDLNClL045nuTAdOoK/IL1vTP0ZLX0DAyQ==}
|
resolution: {integrity: sha512-6QtbTRDKM+jA/MZTTefvigNxo0zz+gnBTVFw2CFVvq+f2BuH0nF0vDLNClL045nuTAdOoK/IL1vTP0ZLX0DAyQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -11218,6 +11227,11 @@ snapshots:
|
|||||||
react-dom: 19.1.1(react@19.1.1)
|
react-dom: 19.1.1(react@19.1.1)
|
||||||
tslib: 2.8.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):
|
react-tabs@6.1.0(react@19.1.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
clsx: 2.1.1
|
clsx: 2.1.1
|
||||||
|
|||||||
@@ -3,11 +3,20 @@ import { snackBar, appBridge } 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';
|
||||||
import { useEffect } from 'react';
|
import { useEffect, useState, CSSProperties } from 'react';
|
||||||
|
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';
|
||||||
|
|
||||||
|
const override: CSSProperties = {
|
||||||
|
position: 'fixed',
|
||||||
|
display: 'block',
|
||||||
|
margin: '0 auto',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
};
|
||||||
|
|
||||||
export interface CashReceiptSampleProps {
|
export interface CashReceiptSampleProps {
|
||||||
cashReceiptSampleOn: boolean;
|
cashReceiptSampleOn: boolean;
|
||||||
setCashReceiptSampleOn: (cashReceiptSampleOn: boolean) => void;
|
setCashReceiptSampleOn: (cashReceiptSampleOn: boolean) => void;
|
||||||
@@ -29,6 +38,9 @@ export const CashReceiptSample = ({
|
|||||||
customerInfo,
|
customerInfo,
|
||||||
productInfo
|
productInfo
|
||||||
}: CashReceiptSampleProps) => {
|
}: CashReceiptSampleProps) => {
|
||||||
|
let [loading, setLoading] = useState<boolean>(false);
|
||||||
|
let [color, setColor] = useState<string>('#0b0606');
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const downloadImage = async () => {
|
const downloadImage = async () => {
|
||||||
@@ -36,22 +48,24 @@ export const CashReceiptSample = ({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const imageDataUrl = await toPng(section);
|
const imageDataUrl = await toPng(section);
|
||||||
|
const fileName = 'cash-receipt-' + moment().format('YYMMDDHHmmss');
|
||||||
|
|
||||||
// iOS 네이티브 환경인 경우 네이티브 브릿지 사용
|
// iOS 네이티브 환경인 경우 네이티브 브릿지 사용
|
||||||
if (appBridge.isIOS()) {
|
if (appBridge.isIOS()) {
|
||||||
try {
|
try {
|
||||||
// data:image/png;base64, 부분 제거하고 순수 base64 데이터만 추출
|
// data:image/png;base64, 부분 제거하고 순수 base64 데이터만 추출
|
||||||
const base64Data = imageDataUrl.split(',')[1];
|
const base64Data = imageDataUrl.split(',')[1];
|
||||||
const fileName = `cash_receipt_${moment().format('YYYYMMDDHHmmss')}.png`;
|
|
||||||
|
|
||||||
const result = await appBridge.saveImage({
|
const result = await appBridge.saveImage({
|
||||||
imageData: base64Data,
|
imageData: base64Data,
|
||||||
fileName: fileName,
|
fileName: fileName + '.png',
|
||||||
mimeType: 'image/png'
|
mimeType: 'image/png'
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
snackBar(t('common.imageSaved'));
|
snackBar(t('common.imageSaved'), function(){
|
||||||
|
onClickToClose();
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
throw new Error(result.error || 'Failed to save image');
|
throw new Error(result.error || 'Failed to save image');
|
||||||
}
|
}
|
||||||
@@ -62,14 +76,18 @@ export const CashReceiptSample = ({
|
|||||||
} else {
|
} else {
|
||||||
// Android 또는 웹 환경인 경우 기존 방식 사용 (다운로드 링크)
|
// Android 또는 웹 환경인 경우 기존 방식 사용 (다운로드 링크)
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.download = `cash_receipt_${moment().format('YYYYMMDDHHmmss')}.png`;
|
link.download = fileName + '.png';
|
||||||
link.href = imageDataUrl;
|
link.href = imageDataUrl + '#' + fileName + '.png';
|
||||||
link.click();
|
link.click();
|
||||||
snackBar(t('common.imageRequested'));
|
snackBar(t('common.imageRequested'), function(){
|
||||||
|
onClickToClose();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Image generation failed:', error);
|
console.error('Image generation failed:', error);
|
||||||
snackBar(t('common.imageGenerationFailed'));
|
snackBar(t('common.imageGenerationFailed'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const onClickToClose = () => {
|
const onClickToClose = () => {
|
||||||
@@ -95,6 +113,7 @@ export const CashReceiptSample = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(!!cashReceiptSampleOn){
|
if(!!cashReceiptSampleOn){
|
||||||
|
setLoading(true);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
downloadImage();
|
downloadImage();
|
||||||
}, 300);
|
}, 300);
|
||||||
@@ -270,6 +289,21 @@ export const CashReceiptSample = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -3,9 +3,11 @@ 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';
|
||||||
import { useEffect } from 'react';
|
import { ClipLoader, FadeLoader } from 'react-spinners';
|
||||||
|
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';
|
||||||
|
|
||||||
export interface DepositReceiptSampleProps {
|
export interface DepositReceiptSampleProps {
|
||||||
depositReceiptSampleOn: boolean;
|
depositReceiptSampleOn: boolean;
|
||||||
@@ -13,23 +15,38 @@ export interface DepositReceiptSampleProps {
|
|||||||
depositInfo?: DepositInfo
|
depositInfo?: DepositInfo
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const override: CSSProperties = {
|
||||||
|
position: 'fixed',
|
||||||
|
display: 'block',
|
||||||
|
margin: '0 auto',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
zIndex: 2000
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export const DepositReceiptSample = ({
|
export const DepositReceiptSample = ({
|
||||||
depositReceiptSampleOn,
|
depositReceiptSampleOn,
|
||||||
setDepositReceiptSampleOn,
|
setDepositReceiptSampleOn,
|
||||||
depositInfo
|
depositInfo
|
||||||
}: DepositReceiptSampleProps) => {
|
}: DepositReceiptSampleProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
let [loading, setLoading] = useState<boolean>(false);
|
||||||
|
let [color, setColor] = useState<string>('#0b0606');
|
||||||
|
|
||||||
const downloadImage = () => {
|
const downloadImage = () => {
|
||||||
const section = document.getElementById('image-section') as HTMLElement
|
const section = document.getElementById('image-section') as HTMLElement
|
||||||
toPng(section).then((image) => {
|
toPng(section).then((image) => {
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.download = 'downloadImage.png';
|
let fileName = 'receipt-confirmation-' + moment().format('YYMMDDHHmmss');
|
||||||
link.href = image;
|
link.download = fileName + '.png';
|
||||||
|
link.href = image + '#' + fileName + '.png';
|
||||||
link.click();
|
link.click();
|
||||||
snackBar(t('common.imageRequested'), () => {
|
snackBar(t('common.imageRequested'), () => {
|
||||||
onClickToClose();
|
onClickToClose();
|
||||||
});
|
});
|
||||||
|
}).finally(() => {
|
||||||
|
setLoading(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const onClickToClose = () => {
|
const onClickToClose = () => {
|
||||||
@@ -38,11 +55,12 @@ export const DepositReceiptSample = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!!depositReceiptSampleOn) {
|
if (!!depositReceiptSampleOn) {
|
||||||
|
setLoading(true);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
downloadImage();
|
downloadImage();
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
}, [depositReceiptSampleOn]);
|
}, [depositReceiptSampleOn, depositInfo]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -116,6 +134,21 @@ export const DepositReceiptSample = ({
|
|||||||
</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"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ export const ListItem = ({
|
|||||||
rs.push(
|
rs.push(
|
||||||
<div
|
<div
|
||||||
className="transaction-details"
|
className="transaction-details"
|
||||||
key={ tid }
|
key={ tid + 'details' }
|
||||||
>
|
>
|
||||||
<span>{ getTime() }</span>
|
<span>{ getTime() }</span>
|
||||||
<span className="separator">|</span>
|
<span className="separator">|</span>
|
||||||
@@ -213,7 +213,10 @@ export const ListItem = ({
|
|||||||
}
|
}
|
||||||
else if(transactionCategory === TransactionCategory.CashReceipt){
|
else if(transactionCategory === TransactionCategory.CashReceipt){
|
||||||
rs.push(
|
rs.push(
|
||||||
<div className="transaction-details">
|
<div
|
||||||
|
className="transaction-details"
|
||||||
|
key={ tid + 'details' }
|
||||||
|
>
|
||||||
<span>{ getTime() }</span>
|
<span>{ getTime() }</span>
|
||||||
<span className="separator">|</span>
|
<span className="separator">|</span>
|
||||||
<span>{ transactionType }</span>
|
<span>{ transactionType }</span>
|
||||||
@@ -226,7 +229,10 @@ export const ListItem = ({
|
|||||||
}
|
}
|
||||||
else if(transactionCategory === TransactionCategory.Escrow){
|
else if(transactionCategory === TransactionCategory.Escrow){
|
||||||
rs.push(
|
rs.push(
|
||||||
<div className="transaction-details">
|
<div
|
||||||
|
className="transaction-details"
|
||||||
|
key={ tid + 'details' }
|
||||||
|
>
|
||||||
<span>{ getTime() }</span>
|
<span>{ getTime() }</span>
|
||||||
<span className="separator">|</span>
|
<span className="separator">|</span>
|
||||||
<span>{ deliveryStatus }</span>
|
<span>{ deliveryStatus }</span>
|
||||||
@@ -246,7 +252,10 @@ export const ListItem = ({
|
|||||||
}
|
}
|
||||||
else if(transactionCategory === TransactionCategory.Billing){
|
else if(transactionCategory === TransactionCategory.Billing){
|
||||||
rs.push(
|
rs.push(
|
||||||
<div className="transaction-details">
|
<div
|
||||||
|
className="transaction-details"
|
||||||
|
key={ tid + 'details' }
|
||||||
|
>
|
||||||
<span>{ getTime() }</span>
|
<span>{ getTime() }</span>
|
||||||
<span className="separator">|</span>
|
<span className="separator">|</span>
|
||||||
<span>{ processResult }</span>
|
<span>{ processResult }</span>
|
||||||
|
|||||||
@@ -216,7 +216,9 @@ export const AmountInfoSection = ({
|
|||||||
};
|
};
|
||||||
cashReceiptReceiptSendEamil(params).then((rs: CashReceiptReceiptSendEmailResponse) => {
|
cashReceiptReceiptSendEamil(params).then((rs: CashReceiptReceiptSendEmailResponse) => {
|
||||||
console.log(rs);
|
console.log(rs);
|
||||||
snackBar('이메일로 현금영수증 요청이 완료되었습니다.');
|
if(rs.message){
|
||||||
|
snackBar(rs.message);
|
||||||
|
}
|
||||||
}).catch((e: any) => {
|
}).catch((e: any) => {
|
||||||
if(e.response?.data?.error?.message){
|
if(e.response?.data?.error?.message){
|
||||||
snackBar(e.response?.data?.error?.message);
|
snackBar(e.response?.data?.error?.message);
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { API_URL_VAT_RETURN } from '@/shared/api/api-url-vat-return';
|
|||||||
import { resultify } from '@/shared/lib/resultify';
|
import { resultify } from '@/shared/lib/resultify';
|
||||||
import { NiceAxiosError } from '@/shared/@types/error';
|
import { NiceAxiosError } from '@/shared/@types/error';
|
||||||
import {
|
import {
|
||||||
VatReturnTaxInvoiceSendEmailResponse,
|
|
||||||
VatReturnTaxInvoiceSendEmailParams,
|
VatReturnTaxInvoiceSendEmailParams,
|
||||||
|
VatReturnTaxInvoiceSendEmailResponse
|
||||||
} from '../model/types';
|
} from '../model/types';
|
||||||
import {
|
import {
|
||||||
useMutation,
|
useMutation,
|
||||||
|
|||||||
@@ -55,7 +55,9 @@ export const AmountSection = ({
|
|||||||
};
|
};
|
||||||
vatReturnTaxInvoiceSendEmail(params).then((rs: VatReturnTaxInvoiceSendEmailResponse) => {
|
vatReturnTaxInvoiceSendEmail(params).then((rs: VatReturnTaxInvoiceSendEmailResponse) => {
|
||||||
console.log(rs);
|
console.log(rs);
|
||||||
snackBar('이메일로 세금계산서 요청이 완료되었습니다.');
|
if(rs.message){
|
||||||
|
snackBar(rs.message);
|
||||||
|
}
|
||||||
}).catch((e: any) => {
|
}).catch((e: any) => {
|
||||||
if(e.response?.data?.error?.message){
|
if(e.response?.data?.error?.message){
|
||||||
snackBar(e.response?.data?.error?.message);
|
snackBar(e.response?.data?.error?.message);
|
||||||
|
|||||||
@@ -121,7 +121,6 @@ export const HomePage = () => {
|
|||||||
let userFavorite = useStore.getState().UserStore.userFavorite;
|
let userFavorite = useStore.getState().UserStore.userFavorite;
|
||||||
setFavoriteItems(userFavorite);
|
setFavoriteItems(userFavorite);
|
||||||
callHomeBannerList();
|
callHomeBannerList();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const setBottomBannerEffect = (mode: boolean) => {
|
const setBottomBannerEffect = (mode: boolean) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user