feat(WEB): 결재함 문서 상세 모달 데이터 연동 개선

- ApprovalBox: 문서 클릭 시 API 데이터 로드하여 모달에 표시
- DocumentCreate: 품의서 폼 개선 및 actions 수정
- 결재자 정보 (직책, 부서) 표시 개선
This commit is contained in:
2026-01-22 23:19:37 +09:00
parent 2d3a7064e3
commit c8890c1a85
6 changed files with 213 additions and 85 deletions

View File

@@ -139,8 +139,8 @@ function mapDocumentStatus(status: string): string {
* API 데이터 → 프론트엔드 데이터 변환
*/
function transformApiToFrontend(data: InboxApiData): ApprovalRecord {
// 현재 사용자의 결재 단계 정보 추출
const currentStep = data.steps?.find(s => s.step_type === 'approval');
// 현재 사용자의 결재 단계 정보 추출 ('approval' 또는 'agreement' 타입)
const currentStep = data.steps?.find(s => s.step_type === 'approval' || s.step_type === 'agreement');
const approver = currentStep?.approver;
const stepStatus = currentStep?.status || 'pending';

View File

@@ -20,6 +20,7 @@ import {
approveDocumentsBulk,
rejectDocumentsBulk,
} from './actions';
import { getApprovalById } from '@/components/approval/DocumentCreate/actions';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Badge } from '@/components/ui/badge';
@@ -109,6 +110,8 @@ export function ApprovalBox() {
// ===== 문서 상세 모달 상태 =====
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedDocument, setSelectedDocument] = useState<ApprovalRecord | null>(null);
const [modalData, setModalData] = useState<ProposalDocumentData | ExpenseReportDocumentData | ExpenseEstimateDocumentData | null>(null);
const [isModalLoading, setIsModalLoading] = useState(false);
// API 데이터
const [data, setData] = useState<ApprovalRecord[]>([]);
@@ -274,9 +277,118 @@ export function ApprovalBox() {
}, [pendingSelectedItems, rejectComment, pendingClearSelection, loadData]);
// ===== 문서 클릭 핸들러 =====
const handleDocumentClick = useCallback((item: ApprovalRecord) => {
const handleDocumentClick = useCallback(async (item: ApprovalRecord) => {
setSelectedDocument(item);
setIsModalLoading(true);
setIsModalOpen(true);
try {
const result = await getApprovalById(parseInt(item.id));
if (result.success && result.data) {
const formData = result.data;
const docType = getDocumentType(item.approvalType);
// 기안자 정보
const drafter = {
id: 'drafter-1',
name: formData.basicInfo.drafter,
position: formData.basicInfo.drafterPosition || '',
department: formData.basicInfo.drafterDepartment || '',
status: 'approved' as const,
};
// 결재자 정보
const approvers = formData.approvalLine.map((person, index) => ({
id: person.id,
name: person.name,
position: person.position,
department: person.department,
status:
item.status === 'approved'
? ('approved' as const)
: item.status === 'rejected'
? ('rejected' as const)
: index === 0
? ('pending' as const)
: ('none' as const),
}));
let convertedData: ProposalDocumentData | ExpenseReportDocumentData | ExpenseEstimateDocumentData;
switch (docType) {
case 'expenseEstimate':
convertedData = {
documentNo: formData.basicInfo.documentNo,
createdAt: formData.basicInfo.draftDate,
items: formData.expenseEstimateData?.items.map(item => ({
id: item.id,
expectedPaymentDate: item.expectedPaymentDate,
category: item.category,
amount: item.amount,
vendor: item.vendor,
account: item.memo || '',
})) || [],
totalExpense: formData.expenseEstimateData?.totalExpense || 0,
accountBalance: formData.expenseEstimateData?.accountBalance || 0,
finalDifference: formData.expenseEstimateData?.finalDifference || 0,
approvers,
drafter,
};
break;
case 'expenseReport':
convertedData = {
documentNo: formData.basicInfo.documentNo,
createdAt: formData.basicInfo.draftDate,
requestDate: formData.expenseReportData?.requestDate || '',
paymentDate: formData.expenseReportData?.paymentDate || '',
items: formData.expenseReportData?.items.map((item, index) => ({
id: item.id,
no: index + 1,
description: item.description,
amount: item.amount,
note: item.note,
})) || [],
cardInfo: formData.expenseReportData?.cardId || '-',
totalAmount: formData.expenseReportData?.totalAmount || 0,
attachments: formData.expenseReportData?.uploadedFiles?.map(f => f.name) || [],
approvers,
drafter,
};
break;
default:
// 품의서
const uploadedFileUrls = (formData.proposalData?.uploadedFiles || []).map(f =>
`/api/proxy/files/${f.id}/download`
);
convertedData = {
documentNo: formData.basicInfo.documentNo,
createdAt: formData.basicInfo.draftDate,
vendor: formData.proposalData?.vendor || '-',
vendorPaymentDate: formData.proposalData?.vendorPaymentDate || '',
title: formData.proposalData?.title || item.title,
description: formData.proposalData?.description || '-',
reason: formData.proposalData?.reason || '-',
estimatedCost: formData.proposalData?.estimatedCost || 0,
attachments: uploadedFileUrls,
approvers,
drafter,
};
break;
}
setModalData(convertedData);
} else {
toast.error(result.error || '문서 조회에 실패했습니다.');
setIsModalOpen(false);
}
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('Failed to load document:', error);
toast.error('문서를 불러오는데 실패했습니다.');
setIsModalOpen(false);
} finally {
setIsModalLoading(false);
}
}, []);
const handleModalEdit = useCallback(() => {
@@ -333,77 +445,7 @@ export function ApprovalBox() {
return 'proposal';
}
};
// ===== 모달용 데이터 변환 =====
const convertToModalData = (
item: ApprovalRecord
): ProposalDocumentData | ExpenseReportDocumentData | ExpenseEstimateDocumentData => {
const docType = getDocumentType(item.approvalType);
const drafter = {
id: 'drafter-1',
name: item.drafter,
position: item.drafterPosition,
department: item.drafterDepartment,
status: 'approved' as const,
};
const approvers = [
{
id: 'approver-1',
name: item.approver || '미지정',
position: '부장',
department: '경영지원팀',
status:
item.status === 'approved'
? ('approved' as const)
: item.status === 'rejected'
? ('rejected' as const)
: ('pending' as const),
},
];
switch (docType) {
case 'expenseEstimate':
return {
documentNo: item.documentNo,
createdAt: item.draftDate,
items: [],
totalExpense: 0,
accountBalance: 0,
finalDifference: 0,
approvers,
drafter,
};
case 'expenseReport':
return {
documentNo: item.documentNo,
createdAt: item.draftDate,
requestDate: item.draftDate,
paymentDate: item.draftDate,
items: [],
cardInfo: '',
totalAmount: 0,
attachments: [],
approvers,
drafter,
};
default:
return {
documentNo: item.documentNo,
createdAt: item.draftDate,
vendor: '거래처',
vendorPaymentDate: item.draftDate,
title: item.title,
description: item.title,
reason: '업무상 필요',
estimatedCost: 0,
attachments: [],
approvers,
drafter,
};
}
};
// ===== 탭 옵션 =====
// ===== 탭 옵션 =====
const tabs: TabOption[] = useMemo(
() => [
{
@@ -739,12 +781,17 @@ export function ApprovalBox() {
</AlertDialog>
{/* 문서 상세 모달 */}
{selectedDocument && (
{selectedDocument && modalData && (
<DocumentDetailModal
open={isModalOpen}
onOpenChange={setIsModalOpen}
onOpenChange={(open) => {
setIsModalOpen(open);
if (!open) {
setModalData(null);
}
}}
documentType={getDocumentType(selectedDocument.approvalType)}
data={convertToModalData(selectedDocument)}
data={modalData}
mode="inbox"
onEdit={handleModalEdit}
onCopy={handleModalCopy}
@@ -778,6 +825,7 @@ export function ApprovalBox() {
handleRejectConfirm,
selectedDocument,
isModalOpen,
modalData,
handleModalEdit,
handleModalCopy,
handleModalApprove,