- 링크결제_분리승인 상세 추가

This commit is contained in:
HyeonJongKim
2025-10-21 17:57:01 +09:00
parent bb7d34bf7d
commit 40a1ee95ce
11 changed files with 355 additions and 185 deletions

View File

@@ -29,7 +29,7 @@ export const LinkPaymentDetailPage = () => {
const [titleInfo, setTitleInfo] = useState<TitleInfo>();
const [detailInfo, setDetailInfo] = useState<DetailInfo>();
const [paymentInfo, setPaymentInfo] = useState<PaymentInfo>();
const [detailExposure, setDetailExposure] = useState<boolean>(false);
const [showPayment, setShowPayment] = useState<boolean>(false);
useSetHeaderTitle('링크결제 상세');
@@ -54,10 +54,11 @@ export const LinkPaymentDetailPage = () => {
setTitleInfo(rs.titleInfo)
setDetailInfo(rs.detailInfo)
setPaymentInfo(rs.paymentInfo)
setDetailExposure(rs.detailExposure ?? false)
})
}
//발송 API
//발송 API
const resendPayment = () => {
let resendParam: ExtensionLinkPayHistoryResendParams = {
mid: mid,
@@ -107,7 +108,7 @@ export const LinkPaymentDetailPage = () => {
const onClickToSeparateApproval = () => {
navigate(PATHS.additionalService.linkPayment.separateApproval, {
state: { mid, tid }
state: { mid, requestId }
});
};
@@ -154,19 +155,20 @@ export const LinkPaymentDetailPage = () => {
detailInfo={detailInfo}
></DetailInfoWrap>
</div>
{/* <div className="apply-row">
<div className="apply-row">
<button
className="btn-50 btn-blue flex-1"
onClick={() => onClickToSeparateApproval()}
disabled={false}
> </button>
</div> */}
<div className="apply-row">
</div>
{/* <div className="apply-row">
<button
className="btn-50 btn-blue flex-1"
onClick={() => onClickToResend()}
disabled={!isResendEnabled()}
>재발송</button>
</div>
</div> */}
</div>
</div>
</main >

View File

@@ -10,19 +10,27 @@ import {
export interface LinkPaymentApplyFailPageProps {
pageOn: boolean;
setPageOn: (pageOn: boolean) => void;
resultMessage?: string;
errorMessage?: string;
successCount?: number;
failCount?: number;
totalCount?: number;
onClose?: () => void;
}
export const LinkPaymentApplyFailPage = ({
pageOn,
setPageOn,
errorMessage
resultMessage,
errorMessage,
onClose
}: LinkPaymentApplyFailPageProps) => {
const { navigate } = useNavigate();
const onClickToClose = () => {
setPageOn(false);
navigate(PATHS.additionalService.linkPayment.shippingHistory);
if (onClose) {
onClose();
}
};
return (
@@ -44,10 +52,14 @@ export const LinkPaymentApplyFailPage = ({
></div>
<h1 className="success-title">
<span>_분리승인</span>
<br />
<span> </span>
</h1>
{resultMessage && (
<p className="result-text" style={{ textAlign: 'center', marginBottom: '20px' }}>
{resultMessage}
</p>
)}
<div className="success-result">
<p className="result-text align-left position_label">
<span> :</span>

View File

@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react';
import { useEffect, useState, useCallback } from 'react';
import { PATHS } from '@/shared/constants/paths';
import { useLocation } from 'react-router';
import { useNavigate } from '@/shared/lib/hooks/use-navigate';
import { HeaderType } from '@/entities/common/model/types';
import {
@@ -12,46 +13,35 @@ import { ExtendedPeriodBottomSheet } from '@/entities/additional-service/ui/link
import { LinkBreakBottomSheet } from '@/entities/additional-service/ui/link-payment/bottom-sheet/link-break-bottom-sheet';
import { LinkPaymentApplySuccessPage } from './link-payment-separate-approval-success-page';
import { LinkPaymentApplyFailPage } from './link-payment-separate-approval-fail';
interface SeparateApprovalItem {
tid: string;
merchantId: string;
amount: number;
status: string;
validityPeriod: string;
approvalCount: number;
}
import { useExtensionLinkPaySeparateAction } from '@/entities/additional-service/api/link-payment/use-extension-link-pay-separate-action-mutaion';
import { useExtensionLinkPaySeparateDetail } from '@/entities/additional-service/api/link-payment/use-extension-link-pay-separate-detail-mutation';
import { ExtensionLinkPaySeparateDetailItem, LinkPaymentSeparateAction, LinkPaymentSeparateType } from '@/entities/additional-service/model/link-pay/types';
import { useStore } from '@/shared/model/store';
import moment from 'moment';
export const LinkPaymentSeparateApprovalPage = () => {
const { navigate } = useNavigate();
const location = useLocation();
const userMid = useStore.getState().UserStore.mid;
const { mid: stateMid, requestId } = location.state || {};
const mid = stateMid || userMid;
const [items, setItems] = useState<ExtensionLinkPaySeparateDetailItem[]>([]);
const [selectedItems, setSelectedItems] = useState<string[]>([]);
const [extendPeriods, setExtendPeriods] = useState<{ [key: string]: string }>({});
const [extendedPeriodBottomSheetOn, setExtendedPeriodBottomSheetOn] = useState<boolean>(false);
const [linkBreakBottomSheetOn, setLinkBreakBottomSheetOn] = useState<boolean>(false);
const [successPageOn, setSuccessPageOn] = useState<boolean>(false);
const [failPageOn, setFailPageOn] = useState<boolean>(false);
const [resultMessage, setResultMessage] = useState<string>('');
const [errorMessage, setErrorMessage] = useState<string>('');
const [successCount, setSuccessCount] = useState<number>(0);
const [failCount, setFailCount] = useState<number>(0);
const [totalCount, setTotalCount] = useState<number>(0);
const [mainItem] = useState<SeparateApprovalItem>({
tid: 'NLPDAR2025101315330',
merchantId: '100,000',
amount: 100000,
status: '활성화',
validityPeriod: '2025/10/13',
approvalCount: 0
});
const [additionalItems] = useState<SeparateApprovalItem[]>([
{
tid: '',
merchantId: '(NNN,NNN)',
amount: 0,
status: '(결제상태)',
validityPeriod: 'YYYY/MM/DD',
approvalCount: 0
}
]);
const { mutateAsync: callItems } = useExtensionLinkPaySeparateDetail();
const { mutateAsync: linkPaySeparateAction } = useExtensionLinkPaySeparateAction();
useSetHeaderTitle('분리승인 상세');
useSetHeaderType(HeaderType.RightClose);
@@ -60,30 +50,27 @@ export const LinkPaymentSeparateApprovalPage = () => {
});
useSetFooterMode(false);
const handleCheckboxChange = (tid: string) => {
const handleCheckboxChange = (subRequestId: string) => {
setSelectedItems(prev => {
if (prev.includes(tid)) {
return prev.filter(id => id !== tid);
if (prev.includes(subRequestId)) {
return prev.filter(id => id !== subRequestId);
} else {
return [...prev, tid];
return [...prev, subRequestId];
}
});
};
const handleMainCheckboxChange = () => {
const mainTid = mainItem.tid;
setSelectedItems(prev => {
if (prev.includes(mainTid)) {
return prev.filter(id => id !== mainTid);
} else {
return [...prev, mainTid];
}
});
const handleExtendPeriodChange = (itemId: string, days: string) => {
setExtendPeriods(prev => ({
...prev,
[itemId]: days
}));
};
const handleAdditionalCheckboxChange = (index: number) => {
const itemTid = `additional-${index}`;
handleCheckboxChange(itemTid);
// 기간연장 버튼 활성화 조건: 선택된 항목이 있고, 모든 선택된 항목의 연장기간이 설정됨
const isExtendButtonEnabled = () => {
if (selectedItems.length === 0) return false;
return selectedItems.every(id => extendPeriods[id] && extendPeriods[id] !== '');
};
const onClickToValidityPeriod = () => {
@@ -100,42 +87,134 @@ export const LinkPaymentSeparateApprovalPage = () => {
// 바텀시트 닫기
setExtendedPeriodBottomSheetOn(false);
// 기간연장 API 호출 로직
console.log('기간연장 실행', selectedItems);
// TODO: 실제 API 호출
const apiCallSuccess = true; // 임시로 성공으로 설정
if (apiCallSuccess) {
setResultMessage('기간이 성공적으로 연장되었습니다');
setSuccessPageOn(true);
} else {
setErrorMessage('기간 연장에 실패했습니다');
// 선택된 항목이 없으면 리턴
if (selectedItems.length === 0) {
setErrorMessage('선택된 항목이 없습니다.');
setFailPageOn(true);
return;
}
// 모든 선택된 항목들의 연장 기간이 설정되었는지 확인
const allHaveExtendPeriod = selectedItems.every(id => extendPeriods[id]);
if (!allHaveExtendPeriod) {
setErrorMessage('모든 선택된 항목의 연장 기간을 선택해주세요.');
setFailPageOn(true);
return;
}
// 첫 번째 선택된 항목의 연장 기간 사용 (모든 항목이 같은 기간으로 연장)
const firstSelectedId = selectedItems[0] as string;
const extendDays = extendPeriods[firstSelectedId] as string;
// 연장 날짜 계산 (오늘 날짜 + 연장일수)
const extendDate = moment().add(parseInt(extendDays), 'days').format('YYYYMMDD');
// selectedItems를 API 형식으로 변환
const selectedItemsData = selectedItems.map(itemId => {
const item = items.find(i => (i.subRequestId || `item-${items.indexOf(i)}`) === itemId);
return {
type: item?.type || '',
subRequestId: item?.subRequestId || itemId
};
});
// 기간연장 API 호출
linkPaySeparateAction({
mid: mid,
action: LinkPaymentSeparateAction.EXTEND,
reqId: requestId,
selectedItems: selectedItemsData,
extendDate: extendDate
}).then((response) => {
setSuccessCount(response.successCount);
setFailCount(response.failCount);
setTotalCount(response.totalCount);
if (response.success) {
setResultMessage(`전체요청 성공했습니다.`);
setSuccessPageOn(true);
} else {
setResultMessage(`유효기간 연장에 실패했습니다. 개별 상태를 확인해주세요.`);
setErrorMessage('');
setFailPageOn(true);
}
}).catch((error) => {
console.error('기간연장 실패:', error);
setResultMessage('유효기간 연장에 실패했습니다.');
setFailPageOn(true);
});
};
const handleLinkBreak = () => {
// 바텀시트 닫기
setLinkBreakBottomSheetOn(false);
// 링크중단 API 호출 로직
console.log('링크중단 실행', selectedItems);
// TODO: 실제 API 호출
const apiCallSuccess = true; // 임시로 성공으로 설정
if (apiCallSuccess) {
setResultMessage('링크가 성공적으로 중단되었습니다');
setSuccessPageOn(true);
} else {
setErrorMessage('링크 중단에 실패했습니다');
// 선택된 항목이 없으면 리턴
if (selectedItems.length === 0) {
setErrorMessage('선택된 항목이 없습니다.');
setFailPageOn(true);
return;
}
const selectedItemsData = selectedItems.map(itemId => {
const item = items.find(i => (i.subRequestId || `item-${items.indexOf(i)}`) === itemId);
return {
type: item?.type || '',
subRequestId: item?.subRequestId || itemId
};
});
// 링크중단 API 호출
linkPaySeparateAction({
mid: mid,
action: LinkPaymentSeparateAction.DEACTIVATE,
reqId: requestId,
selectedItems: selectedItemsData
}).then((response) => {
setSuccessCount(response.successCount);
setFailCount(response.failCount);
setTotalCount(response.totalCount);
if (response.success) {
setResultMessage(`전체요청 성공했습니다.`);
setSuccessPageOn(true);
} else {
setResultMessage('링크중단 요청에 실패했습니다. 개별 상태를 확인해주세요.');
setErrorMessage('');
setFailPageOn(true);
}
}).catch((error) => {
console.error('링크중단 실패:', error);
setResultMessage('링크중단 요청에 실패했습니다.');
setFailPageOn(true);
});
};
// 데이터 새로고침 함수
const refreshItems = useCallback(() => {
if (requestId) {
callItems({ reqId: requestId }).then((response) => {
console.log('Separate Detail Response:', response);
// type이 MAIN인 항목을 최상위로 정렬
const sortedItems = [...(response.details || [])].sort((a, b) => {
if (a.type === LinkPaymentSeparateType.MAIN) return -1;
if (b.type === LinkPaymentSeparateType.MAIN) return 1;
return 0;
});
setItems(sortedItems);
// 선택 항목과 연장 기간 초기화
setSelectedItems([]);
setExtendPeriods({});
}).catch((error) => {
console.error('Failed to load separate detail:', error);
});
}
}, [requestId, callItems]);
useEffect(() => {
// API 호출하여 데이터 로드
// const { mid, tid } = location.state || {};
}, []);
refreshItems();
}, [refreshItems]);
return (
<>
@@ -149,107 +228,63 @@ export const LinkPaymentSeparateApprovalPage = () => {
</div>
<div className="approval-cards-wrapper">
{/* Main Item */}
<div className={`approval-card ${selectedItems.includes(mainItem.tid) ? 'selected' : ''}`}>
<div className="card-header">
<input
type="checkbox"
id={`checkbox-${mainItem.tid}`}
name={`checkbox-${mainItem.tid}`}
checked={selectedItems.includes(mainItem.tid)}
onChange={handleMainCheckboxChange}
className="card-checkbox"
/>
<span className="card-tag">[MAIN]</span>
<span className="card-tid">{mainItem.tid}</span>
</div>
<div className="card-body">
<ul className="info-list">
<li>
<span className="label"> :</span>
<span className="value">{mainItem.merchantId}</span>
</li>
<li>
<span className="label"> :</span>
<span className="value">{mainItem.status}</span>
</li>
<li>
<span className="label"> :</span>
<span className="value">{mainItem.validityPeriod}</span>
</li>
<li>
<span className="label"> :</span>
<span className="value">{mainItem.approvalCount}</span>
</li>
</ul>
</div>
<div className="card-footer">
<div className="period-selector">
<label></label>
<select>
<option value=""></option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="7">7</option>
</select>
</div>
</div>
</div>
{/* Additional Items */}
{additionalItems.map((item, index) => (
<div key={index} className={`approval-card ${selectedItems.includes(`additional-${index}`) ? 'selected' : ''}`}>
<input
type="checkbox"
checked={selectedItems.includes(`additional-${index}`)}
onChange={() => handleAdditionalCheckboxChange(index)}
className="card-checkbox"
/>
<div className="card-header">
<span className="card-tag">()</span>
<span className="card-tig"></span>
</div>
<div className="card-body">
<ul className="info-list">
<li>
<span className="label"> :</span>
<span className="value">{item.merchantId}</span>
</li>
<li>
<span className="label"> :</span>
<span className="value">{item.status}</span>
</li>
<li>
<span className="label"> :</span>
<span className="value">{item.validityPeriod}</span>
</li>
<li>
<span className="label"> :</span>
<span className="value">(N)</span>
</li>
</ul>
</div>
<div className="card-footer">
<div className="period-selector">
<label></label>
<select>
<option value=""></option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="7">7</option>
</select>
{items.map((item, index) => {
const itemId = item.subRequestId || `item-${index}`;
return (
<div key={itemId} className={`approval-card ${selectedItems.includes(itemId) ? 'selected' : ''}`}>
<div className="card-header">
<input
type="checkbox"
id={`checkbox-${itemId}`}
name={`checkbox-${itemId}`}
checked={selectedItems.includes(itemId)}
onChange={() => handleCheckboxChange(itemId)}
className="card-checkbox"
/>
<span className="card-tag">[{item.type}]</span>
<span className="card-tid">{item.moid}</span>
</div>
<div className="card-body">
<ul className="info-list">
<li>
<span className="label"> :</span>
<span className="value">{item.amount.toLocaleString()}</span>
</li>
<li>
<span className="label"> :</span>
<span className="value">{item.paymentStatusName}</span>
</li>
<li>
<span className="label"> :</span>
<span className="value">{item.paymentLimitDate}</span>
</li>
<li>
<span className="label"> :</span>
<span className="value">{item.paymentLimitCount}</span>
</li>
</ul>
</div>
<div className="card-footer">
<div className="period-selector">
<label></label>
<select
value={extendPeriods[itemId] || ''}
onChange={(e) => handleExtendPeriodChange(itemId, e.target.value)}
>
<option value=""></option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="7">7</option>
</select>
</div>
</div>
</div>
</div>
))}
);
})}
</div>
</div>
@@ -257,11 +292,13 @@ export const LinkPaymentSeparateApprovalPage = () => {
<button
className="btn-50 btn-blue flex-1"
onClick={onClickToValidityPeriod}
disabled={!isExtendButtonEnabled()}
>
</button>
<button
className="btn-50 btn-blue flex-1"
onClick={onClickToSendLink}
disabled={selectedItems.length === 0}
>
</button>
</div>
@@ -285,12 +322,15 @@ export const LinkPaymentSeparateApprovalPage = () => {
pageOn={successPageOn}
setPageOn={setSuccessPageOn}
resultMessage={resultMessage}
onClose={refreshItems}
/>
<LinkPaymentApplyFailPage
pageOn={failPageOn}
setPageOn={setFailPageOn}
resultMessage={resultMessage}
errorMessage={errorMessage}
onClose={refreshItems}
/>
</>
);

View File

@@ -11,18 +11,27 @@ export interface LinkPaymentApplySuccessPageProps {
pageOn: boolean;
setPageOn: (pageOn: boolean) => void;
resultMessage?: string;
successCount?: number;
failCount?: number;
totalCount?: number;
onClose?: () => void;
}
export const LinkPaymentApplySuccessPage = ({
pageOn,
setPageOn,
resultMessage
resultMessage,
successCount,
failCount,
totalCount,
onClose
}: LinkPaymentApplySuccessPageProps) => {
const { navigate } = useNavigate();
const onClickToClose = () => {
setPageOn(false);
navigate(PATHS.additionalService.linkPayment.shippingHistory);
if (onClose) {
onClose();
}
};
return (
@@ -45,10 +54,11 @@ export const LinkPaymentApplySuccessPage = ({
<h1 className="success-title">
<span>_분리승인</span>
</h1>
<div className="success-result">
<p className="result-text align-left position_label">
<span> :</span>
<span>{resultMessage || '성공적으로 처리되었습니다'}</span>
<span>{resultMessage || '전체요청 성공했습니다.'}</span>
</p>
</div>
</div>