diff --git a/src/components/approval/ApprovalBox/index.tsx b/src/components/approval/ApprovalBox/index.tsx index 3ae364cf..034765ae 100644 --- a/src/components/approval/ApprovalBox/index.tsx +++ b/src/components/approval/ApprovalBox/index.tsx @@ -1,6 +1,7 @@ 'use client'; import { useState, useMemo, useCallback, useEffect, useTransition } from 'react'; +import { useRouter } from 'next/navigation'; import { FileCheck, Check, @@ -78,6 +79,7 @@ interface InboxSummary { } export function ApprovalBox() { + const router = useRouter(); const [isPending, startTransition] = useTransition(); // ===== 상태 관리 ===== @@ -108,8 +110,9 @@ export function ApprovalBox() { const [totalPages, setTotalPages] = useState(1); const [isLoading, setIsLoading] = useState(true); - // 통계 데이터 + // 통계 데이터 (전체 탭 기준으로 고정 유지) const [summary, setSummary] = useState(null); + const [fixedStats, setFixedStats] = useState({ all: 0, pending: 0, approved: 0, rejected: 0 }); // ===== 데이터 로드 ===== const loadData = useCallback(async () => { @@ -195,15 +198,24 @@ export function ApprovalBox() { } }, [selectedItems.size, data]); - // ===== 통계 데이터 (API summary 사용) ===== - const stats = useMemo(() => { - return { - all: summary?.total ?? 0, - pending: summary?.pending ?? 0, - approved: summary?.approved ?? 0, - rejected: summary?.rejected ?? 0, - }; - }, [summary]); + // ===== 전체 탭일 때만 통계 업데이트 ===== + useEffect(() => { + if (activeTab === 'all' && data.length > 0) { + const pending = data.filter(item => item.status === 'pending').length; + const approved = data.filter(item => item.status === 'approved').length; + const rejected = data.filter(item => item.status === 'rejected').length; + + setFixedStats({ + all: totalCount, + pending, + approved, + rejected, + }); + } + }, [data, totalCount, activeTab]); + + // ===== 통계 데이터 (고정 값 사용) ===== + const stats = fixedStats; // ===== 승인/반려 핸들러 ===== const handleApproveClick = useCallback(() => { @@ -310,10 +322,16 @@ export function ApprovalBox() { }, []); const handleModalEdit = useCallback(() => { - // TODO: 수정 페이지로 이동 - 라우터 연동 필요 - console.log('[ApprovalBox] 문서 수정 - 라우터 연동 필요:', selectedDocument?.id); - setIsModalOpen(false); - }, [selectedDocument]); + if (selectedDocument) { + router.push(`/ko/approval/draft/new?id=${selectedDocument.id}&mode=edit`); + setIsModalOpen(false); + } + }, [selectedDocument, router]); + + // 리스트에서 수정 버튼 클릭 시 핸들러 + const handleEditClick = useCallback((item: ApprovalRecord) => { + router.push(`/ko/approval/draft/new?id=${item.id}&mode=edit`); + }, [router]); const handleModalCopy = useCallback(() => { // TODO: 문서 복제 API 개발 필요 - POST /api/v1/approvals/{id}/copy @@ -460,7 +478,8 @@ export function ApprovalBox() { @@ -468,7 +487,7 @@ export function ApprovalBox() { ); - }, [selectedItems, toggleSelection, handleDocumentClick]); + }, [selectedItems, toggleSelection, handleDocumentClick, handleEditClick]); // ===== 모바일 카드 렌더링 ===== const renderMobileCard = useCallback(( @@ -669,6 +688,7 @@ export function ApprovalBox() { onOpenChange={setIsModalOpen} documentType={getDocumentType(selectedDocument.approvalType)} data={convertToModalData(selectedDocument)} + mode="inbox" onEdit={handleModalEdit} onCopy={handleModalCopy} onApprove={handleModalApprove} diff --git a/src/components/approval/DocumentCreate/index.tsx b/src/components/approval/DocumentCreate/index.tsx index fbb9f6ce..cf48618c 100644 --- a/src/components/approval/DocumentCreate/index.tsx +++ b/src/components/approval/DocumentCreate/index.tsx @@ -408,6 +408,12 @@ export function DocumentCreate() { drafter, }; default: + // 이미 업로드된 파일 URL (Next.js 프록시 사용) + 새로 추가된 파일 미리보기 URL + const uploadedFileUrls = (proposalData.uploadedFiles || []).map(f => + `/api/files/${f.id}/download` + ); + const newFileUrls = proposalData.attachments.map(f => URL.createObjectURL(f)); + return { documentNo: basicInfo.documentNo || '미발급', createdAt: basicInfo.draftDate, @@ -417,7 +423,7 @@ export function DocumentCreate() { description: proposalData.description || '-', reason: proposalData.reason || '-', estimatedCost: proposalData.estimatedCost, - attachments: proposalData.attachments.map(f => f.name), + attachments: [...uploadedFileUrls, ...newFileUrls], approvers, drafter, }; @@ -550,6 +556,19 @@ export function DocumentCreate() { onOpenChange={setIsPreviewOpen} documentType={basicInfo.documentType as ModalDocumentType} data={getPreviewData()} + mode="draft" + documentStatus="draft" + onCopy={() => { + // 복제: 현재 데이터를 기반으로 새 문서 등록 화면으로 이동 + if (documentId) { + router.push(`/ko/approval/draft/new?copyFrom=${documentId}`); + setIsPreviewOpen(false); + } + }} + onSubmit={() => { + setIsPreviewOpen(false); + handleSubmit(); + }} /> ); diff --git a/src/components/approval/DocumentDetail/index.tsx b/src/components/approval/DocumentDetail/index.tsx index b608baac..bcd486e7 100644 --- a/src/components/approval/DocumentDetail/index.tsx +++ b/src/components/approval/DocumentDetail/index.tsx @@ -25,6 +25,7 @@ import { Mail, Phone, MessageCircle, + Send, } from 'lucide-react'; import { ProposalDocument } from './ProposalDocument'; import { ExpenseReportDocument } from './ExpenseReportDocument'; @@ -42,11 +43,17 @@ export function DocumentDetailModal({ onOpenChange, documentType, data, + mode = 'inbox', // 기본값: 결재함 (승인/반려 표시) + documentStatus, onEdit, onCopy, onApprove, onReject, + onSubmit, }: DocumentDetailModalProps) { + // 기안함 모드에서 임시저장 상태일 때만 수정/상신 가능 + const canEdit = mode === 'inbox' || documentStatus === 'draft'; + const canSubmit = mode === 'draft' && documentStatus === 'draft'; const getDocumentTitle = () => { switch (documentType) { case 'proposal': @@ -115,29 +122,47 @@ export function DocumentDetailModal({ {/* 버튼 영역 - 고정 */}
- - - - + {/* 기안함 모드 + 임시저장 상태: 복제, 상신, 인쇄 */} + {mode === 'draft' && documentStatus === 'draft' && ( + <> + + + + )} + + {/* 기안함 모드 + 결재대기 이후 상태: 인쇄만 (버튼 없음, 아래 인쇄 버튼만 표시) */} + + {/* 결재함 모드: 수정, 반려, 승인, 인쇄 */} + {mode === 'inbox' && ( + <> + + + + + )} + - {/* 공유 드롭다운 */} - + {/* TODO: 공유 기능 추가 예정 - PDF 다운로드, 이메일, 팩스, 카카오톡 공유 */} + {/*
{/* 문서 영역 - 스크롤 */} diff --git a/src/components/approval/DocumentDetail/types.ts b/src/components/approval/DocumentDetail/types.ts index ced20508..1ff24588 100644 --- a/src/components/approval/DocumentDetail/types.ts +++ b/src/components/approval/DocumentDetail/types.ts @@ -72,14 +72,23 @@ export interface ExpenseEstimateDocumentData { drafter: Approver; } +// 문서 상세 모달 모드 +export type DocumentDetailMode = 'draft' | 'inbox' | 'reference'; + +// 문서 상태 (기안함 기준) +export type DocumentStatus = 'draft' | 'pending' | 'approved' | 'rejected'; + // 문서 상세 모달 Props export interface DocumentDetailModalProps { open: boolean; onOpenChange: (open: boolean) => void; documentType: DocumentType; data: ProposalDocumentData | ExpenseReportDocumentData | ExpenseEstimateDocumentData; + mode?: DocumentDetailMode; // 'draft': 기안함 (상신), 'inbox': 결재함 (승인/반려) + documentStatus?: DocumentStatus; // 문서 상태 (임시저장일 때만 수정/상신 가능) onEdit?: () => void; onCopy?: () => void; onApprove?: () => void; onReject?: () => void; + onSubmit?: () => void; // 상신 콜백 } \ No newline at end of file diff --git a/src/components/approval/DraftBox/index.tsx b/src/components/approval/DraftBox/index.tsx index 2cd5e605..abb75eb2 100644 --- a/src/components/approval/DraftBox/index.tsx +++ b/src/components/approval/DraftBox/index.tsx @@ -13,6 +13,7 @@ import { toast } from 'sonner'; import { getDrafts, getDraftsSummary, + getDraftById, deleteDraft, deleteDrafts, submitDraft, @@ -238,14 +239,21 @@ export function DraftBox() { // ===== 문서 클릭/수정 핸들러 (조건부 로직) ===== // 임시저장 → 문서 작성 페이지 (수정 모드) - // 그 외 → 문서 상세 모달 - const handleDocumentClick = useCallback((item: DraftRecord) => { + // 그 외 → 문서 상세 모달 (상세 API 호출하여 content 포함된 데이터 가져옴) + const handleDocumentClick = useCallback(async (item: DraftRecord) => { if (item.status === 'draft') { // 임시저장 상태 → 문서 작성 페이지로 이동 (수정 모드) router.push(`/ko/approval/draft/new?id=${item.id}&mode=edit`); } else { - // 그 외 상태 → 문서 상세 모달 열기 - setSelectedDocument(item); + // 그 외 상태 → 문서 상세 API 호출 후 모달 열기 + // 목록 API에서는 content가 포함되지 않을 수 있으므로 상세 조회 필요 + const detailData = await getDraftById(item.id); + if (detailData) { + setSelectedDocument(detailData); + } else { + // 상세 조회 실패 시 기존 데이터 사용 + setSelectedDocument(item); + } setIsModalOpen(true); } }, [router]); @@ -258,9 +266,14 @@ export function DraftBox() { }, [selectedDocument, router]); const handleModalCopy = useCallback(() => { + console.log('[DraftBox] handleModalCopy 호출됨, selectedDocument:', selectedDocument); if (selectedDocument) { - router.push(`/ko/approval/draft/new?copyFrom=${selectedDocument.id}`); + const copyUrl = `/ko/approval/draft/new?copyFrom=${selectedDocument.id}`; + console.log('[DraftBox] 복제 URL로 이동:', copyUrl); + router.push(copyUrl); setIsModalOpen(false); + } else { + console.log('[DraftBox] selectedDocument가 없음'); } }, [selectedDocument, router]); @@ -276,6 +289,29 @@ export function DraftBox() { setIsModalOpen(false); }, []); + // ===== 모달에서 상신 핸들러 ===== + const handleModalSubmit = useCallback(async () => { + if (!selectedDocument) return; + + startTransition(async () => { + try { + const result = await submitDraft(selectedDocument.id); + if (result.success) { + toast.success('문서를 상신했습니다.'); + setIsModalOpen(false); + setSelectedDocument(null); + loadData(); + loadSummary(); + } else { + toast.error(result.error || '상신에 실패했습니다.'); + } + } catch (error) { + console.error('Submit error:', error); + toast.error('상신 중 오류가 발생했습니다.'); + } + }); + }, [selectedDocument, loadData, loadSummary]); + // ===== DraftRecord → 모달용 데이터 변환 ===== const getDocumentType = (item: DraftRecord): DocumentType => { // documentTypeCode 우선 사용, 없으면 documentType(양식명)으로 추론 @@ -371,7 +407,11 @@ export function DraftBox() { } default: { // proposal (품의서) - // API content 구조: { vendor, vendorPaymentDate, title, description, reason, estimatedCost } + // API content 구조: { vendor, vendorPaymentDate, title, description, reason, estimatedCost, files } + const files = (content.files as Array<{ id: number; name: string; url?: string }>) || []; + // Next.js 프록시 URL 사용 (인증된 요청 프록시) + const attachmentUrls = files.map(f => `/api/files/${f.id}/download`); + return { documentNo: item.documentNo, createdAt: item.draftDate, @@ -381,7 +421,7 @@ export function DraftBox() { description: (content.description as string) || '', reason: (content.reason as string) || '', estimatedCost: (content.estimatedCost as number) || 0, - attachments: [], + attachments: attachmentUrls, approvers, drafter, }; @@ -451,7 +491,8 @@ export function DraftBox() { e.stopPropagation()}> - {isSelected && ( + {/* 임시저장 상태일 때만 수정/삭제 버튼 표시 */} + {isSelected && item.status === 'draft' && (