From 3e22037659bdaca1a24250ea1a9db7d8537b6b26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EB=B3=91=EC=B2=A0?= Date: Tue, 17 Mar 2026 14:53:34 +0900 Subject: [PATCH] =?UTF-8?q?chore:=20=EB=B0=B1=EC=97=85/=EB=A0=88=EA=B1=B0?= =?UTF-8?q?=EC=8B=9C=20=ED=8C=8C=EC=9D=BC=20=EC=A0=95=EB=A6=AC=20(-9,927?= =?UTF-8?q?=EC=A4=84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - approval_backup_v1/ 전체 삭제 (27파일) - SalaryManagement_backup_20260312/ 삭제 (5파일) - AccountManagement/_legacy/ 삭제 - vehicle/types.ts 삭제 --- .../approval_backup_v1/ApprovalBox/actions.ts | 314 ------ .../approval_backup_v1/ApprovalBox/index.tsx | 900 ------------------ .../approval_backup_v1/ApprovalBox/types.ts | 95 -- .../DocumentCreate/ApprovalLineSection.tsx | 108 --- .../DocumentCreate/BasicInfoSection.tsx | 81 -- .../DocumentCreate/ExpenseEstimateForm.tsx | 164 ---- .../DocumentCreate/ExpenseReportForm.tsx | 240 ----- .../DocumentCreate/ProposalForm.tsx | 234 ----- .../DocumentCreate/ReferenceSection.tsx | 108 --- .../DocumentCreate/actions.ts | 719 -------------- .../DocumentCreate/documentCreateConfig.ts | 34 - .../DocumentCreate/index.tsx | 631 ------------ .../DocumentCreate/types.ts | 106 --- .../DocumentDetail/ApprovalLineBox.tsx | 85 -- .../DocumentDetail/DocumentDetailModalV2.tsx | 98 -- .../ExpenseEstimateDocument.tsx | 127 --- .../DocumentDetail/ExpenseReportDocument.tsx | 136 --- .../DocumentDetail/LinkedDocumentContent.tsx | 133 --- .../DocumentDetail/ProposalDocument.tsx | 114 --- .../DocumentDetail/index.tsx | 182 ---- .../DocumentDetail/types.ts | 117 --- .../approval_backup_v1/DraftBox/actions.ts | 257 ----- .../approval_backup_v1/DraftBox/index.tsx | 754 --------------- .../approval_backup_v1/DraftBox/types.ts | 90 -- .../ReferenceBox/actions.ts | 170 ---- .../approval_backup_v1/ReferenceBox/index.tsx | 692 -------------- .../approval_backup_v1/ReferenceBox/types.ts | 89 -- .../SalaryDetailDialog.tsx | 524 ---------- .../SalaryRegistrationDialog.tsx | 571 ----------- .../actions.ts | 349 ------- .../index.tsx | 793 --------------- .../SalaryManagement_backup_20260312/types.ts | 100 -- .../_legacy/AccountDetail.tsx | 370 ------- src/components/vehicle/types.ts | 442 --------- 34 files changed, 9927 deletions(-) delete mode 100644 src/components/approval_backup_v1/ApprovalBox/actions.ts delete mode 100644 src/components/approval_backup_v1/ApprovalBox/index.tsx delete mode 100644 src/components/approval_backup_v1/ApprovalBox/types.ts delete mode 100644 src/components/approval_backup_v1/DocumentCreate/ApprovalLineSection.tsx delete mode 100644 src/components/approval_backup_v1/DocumentCreate/BasicInfoSection.tsx delete mode 100644 src/components/approval_backup_v1/DocumentCreate/ExpenseEstimateForm.tsx delete mode 100644 src/components/approval_backup_v1/DocumentCreate/ExpenseReportForm.tsx delete mode 100644 src/components/approval_backup_v1/DocumentCreate/ProposalForm.tsx delete mode 100644 src/components/approval_backup_v1/DocumentCreate/ReferenceSection.tsx delete mode 100644 src/components/approval_backup_v1/DocumentCreate/actions.ts delete mode 100644 src/components/approval_backup_v1/DocumentCreate/documentCreateConfig.ts delete mode 100644 src/components/approval_backup_v1/DocumentCreate/index.tsx delete mode 100644 src/components/approval_backup_v1/DocumentCreate/types.ts delete mode 100644 src/components/approval_backup_v1/DocumentDetail/ApprovalLineBox.tsx delete mode 100644 src/components/approval_backup_v1/DocumentDetail/DocumentDetailModalV2.tsx delete mode 100644 src/components/approval_backup_v1/DocumentDetail/ExpenseEstimateDocument.tsx delete mode 100644 src/components/approval_backup_v1/DocumentDetail/ExpenseReportDocument.tsx delete mode 100644 src/components/approval_backup_v1/DocumentDetail/LinkedDocumentContent.tsx delete mode 100644 src/components/approval_backup_v1/DocumentDetail/ProposalDocument.tsx delete mode 100644 src/components/approval_backup_v1/DocumentDetail/index.tsx delete mode 100644 src/components/approval_backup_v1/DocumentDetail/types.ts delete mode 100644 src/components/approval_backup_v1/DraftBox/actions.ts delete mode 100644 src/components/approval_backup_v1/DraftBox/index.tsx delete mode 100644 src/components/approval_backup_v1/DraftBox/types.ts delete mode 100644 src/components/approval_backup_v1/ReferenceBox/actions.ts delete mode 100644 src/components/approval_backup_v1/ReferenceBox/index.tsx delete mode 100644 src/components/approval_backup_v1/ReferenceBox/types.ts delete mode 100644 src/components/hr/SalaryManagement_backup_20260312/SalaryDetailDialog.tsx delete mode 100644 src/components/hr/SalaryManagement_backup_20260312/SalaryRegistrationDialog.tsx delete mode 100644 src/components/hr/SalaryManagement_backup_20260312/actions.ts delete mode 100644 src/components/hr/SalaryManagement_backup_20260312/index.tsx delete mode 100644 src/components/hr/SalaryManagement_backup_20260312/types.ts delete mode 100644 src/components/settings/AccountManagement/_legacy/AccountDetail.tsx delete mode 100644 src/components/vehicle/types.ts diff --git a/src/components/approval_backup_v1/ApprovalBox/actions.ts b/src/components/approval_backup_v1/ApprovalBox/actions.ts deleted file mode 100644 index 7d1805af..00000000 --- a/src/components/approval_backup_v1/ApprovalBox/actions.ts +++ /dev/null @@ -1,314 +0,0 @@ -/** - * 결재함 서버 액션 - * - * API Endpoints: - * - GET /api/v1/approvals/inbox - 결재함 목록 조회 - * - GET /api/v1/approvals/inbox/summary - 결재함 통계 - * - POST /api/v1/approvals/{id}/approve - 승인 처리 - * - POST /api/v1/approvals/{id}/reject - 반려 처리 - */ - -'use server'; - - -import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action'; -import { buildApiUrl } from '@/lib/api/query-params'; -import type { PaginatedApiResponse } from '@/lib/api/types'; -import type { ApprovalRecord, ApprovalType, ApprovalStatus } from './types'; - -// ============================================ -// API 응답 타입 정의 -// ============================================ - -interface InboxSummary { - total: number; - pending: number; - approved: number; - rejected: number; -} - -interface InboxApiData { - id: number; - document_number: string; - title: string; - status: string; - form?: { id: number; name: string; code: string; category: string }; - drafter?: { id: number; name: string; position?: string; department?: { name: string } }; - steps?: InboxStepApiData[]; - created_at: string; - updated_at: string; -} - -interface InboxStepApiData { - id: number; - step_order: number; - step_type: string; - approver_id: number; - approver?: { id: number; name: string; position?: string; department?: { name: string } }; - status: string; - processed_at?: string; - comment?: string; -} - -// ============================================ -// 헬퍼 함수 -// ============================================ - -function mapApiStatus(apiStatus: string): ApprovalStatus { - const statusMap: Record = { - 'pending': 'pending', 'approved': 'approved', 'rejected': 'rejected', - }; - return statusMap[apiStatus] || 'pending'; -} - -function mapTabToApiStatus(tabStatus: string): string | undefined { - const statusMap: Record = { - 'pending': 'requested', 'approved': 'completed', 'rejected': 'rejected', - }; - return statusMap[tabStatus]; -} - -function mapApprovalType(formCategory?: string): ApprovalType { - const typeMap: Record = { - 'expense_report': 'expense_report', 'proposal': 'proposal', 'expense_estimate': 'expense_estimate', '문서': 'document', 'document': 'document', - }; - return typeMap[formCategory || ''] || 'proposal'; -} - -function mapDocumentStatus(status: string): string { - const statusMap: Record = { - 'pending': '진행중', 'approved': '완료', 'rejected': '반려', - }; - return statusMap[status] || '진행중'; -} - -function transformApiToFrontend(data: InboxApiData): ApprovalRecord { - const currentStep = data.steps?.find(s => s.step_type === 'approval' || s.step_type === 'agreement'); - const approver = currentStep?.approver; - const stepStatus = currentStep?.status || 'pending'; - - return { - id: String(data.id), - documentNo: data.document_number, - approvalType: mapApprovalType(data.form?.category), - documentStatus: mapDocumentStatus(data.status), - title: data.title, - draftDate: data.created_at.replace('T', ' ').substring(0, 16), - drafter: data.drafter?.name || '', - drafterDepartment: data.drafter?.department?.name || '', - drafterPosition: data.drafter?.position || '', - approvalDate: currentStep?.processed_at?.replace('T', ' ').substring(0, 16), - approver: approver?.name, - status: mapApiStatus(stepStatus), - priority: 'normal', - createdAt: data.created_at, - updatedAt: data.updated_at, - }; -} - -// ============================================ -// API 함수 -// ============================================ - -export async function getInbox(params?: { - page?: number; per_page?: number; search?: string; status?: string; - approval_type?: string; sort_by?: string; sort_dir?: 'asc' | 'desc'; - start_date?: string; end_date?: string; -}): Promise<{ data: ApprovalRecord[]; total: number; lastPage: number; __authError?: boolean }> { - const result = await executeServerAction>({ - url: buildApiUrl('/api/v1/approvals/inbox', { - page: params?.page, - per_page: params?.per_page, - search: params?.search, - status: params?.status && params.status !== 'all' ? mapTabToApiStatus(params.status) : undefined, - approval_type: params?.approval_type !== 'all' ? params?.approval_type : undefined, - sort_by: params?.sort_by, - sort_dir: params?.sort_dir, - start_date: params?.start_date, - end_date: params?.end_date, - }), - errorMessage: '결재함 목록 조회에 실패했습니다.', - }); - - if (result.__authError) return { data: [], total: 0, lastPage: 1, __authError: true }; - if (!result.success || !result.data?.data) return { data: [], total: 0, lastPage: 1 }; - - return { - data: result.data.data.map(transformApiToFrontend), - total: result.data.total, - lastPage: result.data.last_page, - }; -} - -export async function getInboxSummary(): Promise { - const result = await executeServerAction({ - url: buildApiUrl('/api/v1/approvals/inbox/summary'), - errorMessage: '결재함 통계 조회에 실패했습니다.', - }); - return result.success ? result.data || null : null; -} - -export async function approveDocument(id: string, comment?: string): Promise { - return executeServerAction({ - url: buildApiUrl(`/api/v1/approvals/${id}/approve`), - method: 'POST', - body: { comment: comment || '' }, - errorMessage: '승인 처리에 실패했습니다.', - }); -} - -export async function rejectDocument(id: string, comment: string): Promise { - if (!comment?.trim()) return { success: false, error: '반려 사유를 입력해주세요.' }; - return executeServerAction({ - url: buildApiUrl(`/api/v1/approvals/${id}/reject`), - method: 'POST', - body: { comment }, - errorMessage: '반려 처리에 실패했습니다.', - }); -} - -export async function approveDocumentsBulk(ids: string[], comment?: string): Promise<{ success: boolean; failedIds?: string[]; error?: string }> { - const failedIds: string[] = []; - for (const id of ids) { - const result = await approveDocument(id, comment); - if (!result.success) failedIds.push(id); - } - if (failedIds.length > 0) { - return { success: false, failedIds, error: `${failedIds.length}건의 승인 처리에 실패했습니다.` }; - } - return { success: true }; -} - -// ============================================ -// 연결 문서(Document) 조회 -// ============================================ - -interface LinkedDocumentApiData { - id: number; - document_number: string; - title: string; - status: string; - drafter?: { - id: number; name: string; position?: string; - department?: { name: string }; - tenant_profile?: { position_key?: string; department?: { name: string } }; - }; - steps?: InboxStepApiData[]; - linkable?: { - id: number; - title: string; - document_no: string; - status: string; - created_at: string; - linkable_type?: string; - linkable_id?: number; - template?: { id: number; name: string; code: string }; - data?: Array<{ id: number; field_key: string; field_label?: string; field_value?: unknown; value?: unknown }>; - approvals?: Array<{ id: number; step: number; status: string; acted_at?: string; user?: { id: number; name: string } }>; - attachments?: Array<{ id: number; display_name: string; file_path: string }>; - }; -} - -interface LinkedDocumentResult { - documentNo: string; - createdAt: string; - title: string; - templateName: string; - templateCode: string; - status: string; - workOrderId?: number; - documentData: Array<{ fieldKey: string; fieldLabel: string; value: unknown }>; - approvers: Array<{ id: string; name: string; position: string; department: string; status: 'pending' | 'approved' | 'rejected' | 'none' }>; - drafter: { id: string; name: string; position: string; department: string; status: 'approved' | 'pending' | 'rejected' | 'none' }; - attachments?: Array<{ id: number; name: string; url: string }>; -} - -function getPositionLabel(positionKey: string | null | undefined): string { - if (!positionKey) return ''; - const labels: Record = { - 'EXECUTIVE': '임원', 'DIRECTOR': '부장', 'MANAGER': '과장', - 'SENIOR': '대리', 'STAFF': '사원', 'INTERN': '인턴', - }; - return labels[positionKey] ?? positionKey; -} - -export async function getDocumentApprovalById(id: number): Promise<{ - success: boolean; - data?: LinkedDocumentResult; - error?: string; -}> { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await executeServerAction({ - url: buildApiUrl(`/api/v1/approvals/${id}`), - errorMessage: '문서 조회에 실패했습니다.', - }); - if (!result.success || !result.data) return { success: false, error: result.error }; - - const apiData = result.data as LinkedDocumentApiData; - const linkable = apiData.linkable; - - const drafter = { - id: String(apiData.drafter?.id || ''), - name: apiData.drafter?.name || '', - position: apiData.drafter?.tenant_profile?.position_key - ? getPositionLabel(apiData.drafter.tenant_profile.position_key) - : (apiData.drafter?.position || ''), - department: apiData.drafter?.tenant_profile?.department?.name - || apiData.drafter?.department?.name || '', - status: 'approved' as const, - }; - - const approvers = (apiData.steps || []) - .filter(s => s.step_type === 'approval' || s.step_type === 'agreement') - .map(step => ({ - id: String(step.approver?.id || step.approver_id), - name: step.approver?.name || '', - position: step.approver?.position || '', - department: step.approver?.department?.name || '', - status: (step.status === 'approved' ? 'approved' - : step.status === 'rejected' ? 'rejected' - : step.status === 'pending' ? 'pending' - : 'none') as 'pending' | 'approved' | 'rejected' | 'none', - })); - - // work_order 연결 문서인 경우 workOrderId 추출 - const workOrderId = linkable?.linkable_type === 'work_order' ? linkable.linkable_id : undefined; - - return { - success: true, - data: { - documentNo: linkable?.document_no || apiData.document_number, - createdAt: linkable?.created_at || '', - title: linkable?.title || apiData.title, - templateName: linkable?.template?.name || '', - templateCode: linkable?.template?.code || '', - status: linkable?.status || apiData.status, - workOrderId, - documentData: (linkable?.data || []).map(d => ({ - fieldKey: d.field_key, - fieldLabel: d.field_label || d.field_key, - value: d.field_value ?? d.value, - })), - approvers, - drafter, - attachments: (linkable?.attachments || []).map(a => ({ - id: a.id, - name: a.display_name, - url: `/api/proxy/files/${a.id}/download`, - })), - }, - }; -} - -export async function rejectDocumentsBulk(ids: string[], comment: string): Promise<{ success: boolean; failedIds?: string[]; error?: string }> { - if (!comment?.trim()) return { success: false, error: '반려 사유를 입력해주세요.' }; - const failedIds: string[] = []; - for (const id of ids) { - const result = await rejectDocument(id, comment); - if (!result.success) failedIds.push(id); - } - if (failedIds.length > 0) { - return { success: false, failedIds, error: `${failedIds.length}건의 반려 처리에 실패했습니다.` }; - } - return { success: true }; -} \ No newline at end of file diff --git a/src/components/approval_backup_v1/ApprovalBox/index.tsx b/src/components/approval_backup_v1/ApprovalBox/index.tsx deleted file mode 100644 index 7f2cd85f..00000000 --- a/src/components/approval_backup_v1/ApprovalBox/index.tsx +++ /dev/null @@ -1,900 +0,0 @@ -'use client'; - -import { useState, useMemo, useCallback, useEffect, useTransition, useRef } from 'react'; -import { useRouter } from 'next/navigation'; -import { useDateRange } from '@/hooks'; -import { - FileCheck, - Check, - X, - Clock, - FileX, - Files, -} from 'lucide-react'; -import { toast } from 'sonner'; -import { - getInbox, - approveDocument, - rejectDocument, - approveDocumentsBulk, - rejectDocumentsBulk, - getDocumentApprovalById, -} 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'; -import { TableRow, TableCell } from '@/components/ui/table'; -import { Textarea } from '@/components/ui/textarea'; -import { Label } from '@/components/ui/label'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; -import { ConfirmDialog } from '@/components/ui/confirm-dialog'; -import { - UniversalListPage, - type UniversalListConfig, - type TabOption, -} from '@/components/templates/UniversalListPage'; -import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard'; -// import { DocumentDetailModal } from '@/components/approval/DocumentDetail'; -import { DocumentDetailModalV2 as DocumentDetailModal } from '@/components/approval/DocumentDetail'; -import type { - DocumentType, - ProposalDocumentData, - ExpenseReportDocumentData, - ExpenseEstimateDocumentData, - LinkedDocumentData, -} from '@/components/approval/DocumentDetail/types'; -import type { - ApprovalTabType, - ApprovalRecord, - ApprovalType, - SortOption, - FilterOption, -} from './types'; -import { - APPROVAL_TAB_LABELS, - SORT_OPTIONS, - FILTER_OPTIONS, - APPROVAL_TYPE_LABELS, - APPROVAL_STATUS_LABELS, - APPROVAL_STATUS_COLORS, -} from './types'; -import { isNextRedirectError } from '@/lib/utils/redirect-error'; -import { usePermission } from '@/hooks/usePermission'; -import { InspectionReportModal } from '@/components/production/WorkOrders/documents/InspectionReportModal'; - -export function ApprovalBox() { - const router = useRouter(); - const [, startTransition] = useTransition(); - const { canApprove } = usePermission(); - - // ===== 상태 관리 ===== - const [activeTab, setActiveTab] = useState('all'); - const [searchQuery, setSearchQuery] = useState(''); - const [filterOption, setFilterOption] = useState('all'); - const [sortOption, setSortOption] = useState('latest'); - const [currentPage, setCurrentPage] = useState(1); - const itemsPerPage = 20; - - // 날짜 범위 상태 - const { startDate, endDate, setStartDate, setEndDate } = useDateRange('currentYear'); - - // 다이얼로그 상태 - const [approveDialogOpen, setApproveDialogOpen] = useState(false); - const [rejectDialogOpen, setRejectDialogOpen] = useState(false); - const [rejectComment, setRejectComment] = useState(''); - const [pendingSelectedItems, setPendingSelectedItems] = useState>(new Set()); - const [pendingClearSelection, setPendingClearSelection] = useState<(() => void) | null>(null); - - // ===== 문서 상세 모달 상태 ===== - const [isModalOpen, setIsModalOpen] = useState(false); - const [selectedDocument, setSelectedDocument] = useState(null); - const [modalData, setModalData] = useState(null); - const [, setIsModalLoading] = useState(false); - - // ===== 검사성적서 모달 상태 (work_order 연결 문서용) ===== - const [isInspectionModalOpen, setIsInspectionModalOpen] = useState(false); - const [inspectionWorkOrderId, setInspectionWorkOrderId] = useState(null); - - // API 데이터 - const [data, setData] = useState([]); - const [totalCount, setTotalCount] = useState(0); - const [totalPages, setTotalPages] = useState(1); - const [isLoading, setIsLoading] = useState(true); - const isInitialLoadDone = useRef(false); - - // 통계 데이터 - const [fixedStats, setFixedStats] = useState({ all: 0, pending: 0, approved: 0, rejected: 0 }); - - // ===== 데이터 로드 ===== - const loadData = useCallback(async () => { - if (!isInitialLoadDone.current) { - setIsLoading(true); - } - try { - const sortConfig: { sort_by: string; sort_dir: 'asc' | 'desc' } = (() => { - switch (sortOption) { - case 'latest': - return { sort_by: 'created_at', sort_dir: 'desc' }; - case 'oldest': - return { sort_by: 'created_at', sort_dir: 'asc' }; - case 'draftDateAsc': - return { sort_by: 'created_at', sort_dir: 'asc' }; - case 'draftDateDesc': - return { sort_by: 'created_at', sort_dir: 'desc' }; - default: - return { sort_by: 'created_at', sort_dir: 'desc' }; - } - })(); - - const result = await getInbox({ - page: currentPage, - per_page: itemsPerPage, - search: searchQuery || undefined, - status: activeTab !== 'all' ? activeTab : undefined, - approval_type: filterOption !== 'all' ? filterOption : undefined, - start_date: startDate || undefined, - end_date: endDate || undefined, - ...sortConfig, - }); - - setData(result.data); - setTotalCount(result.total); - setTotalPages(result.lastPage); - } catch (error) { - if (isNextRedirectError(error)) throw error; - console.error('Failed to load inbox:', error); - toast.error('결재함 목록을 불러오는데 실패했습니다.'); - } finally { - setIsLoading(false); - isInitialLoadDone.current = true; - } - }, [currentPage, itemsPerPage, searchQuery, filterOption, sortOption, activeTab, startDate, endDate]); - - // ===== 초기 로드 ===== - useEffect(() => { - loadData(); - }, [loadData]); - - // ===== 검색어/필터/탭 변경 시 페이지 초기화 ===== - useEffect(() => { - setCurrentPage(1); - }, [searchQuery, filterOption, sortOption, activeTab]); - - // ===== 탭 변경 핸들러 ===== - const handleTabChange = useCallback((value: string) => { - setActiveTab(value as ApprovalTabType); - setSearchQuery(''); - }, []); - - // ===== 전체 탭일 때만 통계 업데이트 ===== - 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 handleApproveClick = useCallback( - (selectedItems: Set, onClearSelection: () => void) => { - if (selectedItems.size === 0) return; - setPendingSelectedItems(selectedItems); - setPendingClearSelection(() => onClearSelection); - setApproveDialogOpen(true); - }, - [] - ); - - const handleApproveConfirm = useCallback(async () => { - const ids = Array.from(pendingSelectedItems); - - startTransition(async () => { - try { - const result = await approveDocumentsBulk(ids); - if (result.success) { - toast.success('승인 완료', { - description: '결재 승인이 완료되었습니다.', - }); - pendingClearSelection?.(); - loadData(); - } else { - toast.error(result.error || '승인 처리에 실패했습니다.'); - } - } catch (error) { - if (isNextRedirectError(error)) throw error; - console.error('Approve error:', error); - toast.error('승인 처리 중 오류가 발생했습니다.'); - } - }); - - setApproveDialogOpen(false); - setPendingSelectedItems(new Set()); - setPendingClearSelection(null); - }, [pendingSelectedItems, pendingClearSelection, loadData]); - - const handleRejectClick = useCallback( - (selectedItems: Set, onClearSelection: () => void) => { - if (selectedItems.size === 0) return; - setPendingSelectedItems(selectedItems); - setPendingClearSelection(() => onClearSelection); - setRejectComment(''); - setRejectDialogOpen(true); - }, - [] - ); - - const handleRejectConfirm = useCallback(async () => { - if (!rejectComment.trim()) { - toast.error('반려 사유를 입력해주세요.'); - return; - } - - const ids = Array.from(pendingSelectedItems); - - startTransition(async () => { - try { - const result = await rejectDocumentsBulk(ids, rejectComment); - if (result.success) { - toast.success('반려 완료', { - description: '결재 반려가 완료되었습니다.', - }); - pendingClearSelection?.(); - setRejectComment(''); - loadData(); - } else { - toast.error(result.error || '반려 처리에 실패했습니다.'); - } - } catch (error) { - if (isNextRedirectError(error)) throw error; - console.error('Reject error:', error); - toast.error('반려 처리 중 오류가 발생했습니다.'); - } - }); - - setRejectDialogOpen(false); - setPendingSelectedItems(new Set()); - setPendingClearSelection(null); - }, [pendingSelectedItems, rejectComment, pendingClearSelection, loadData]); - - // ===== 문서 클릭 핸들러 ===== - const handleDocumentClick = useCallback(async (item: ApprovalRecord) => { - setSelectedDocument(item); - setIsModalLoading(true); - setIsModalOpen(true); - - try { - // 문서 결재(document) 타입은 별도 API로 연결 문서 데이터 조회 - if (item.approvalType === 'document') { - const result = await getDocumentApprovalById(parseInt(item.id)); - if (result.success && result.data) { - // work_order 연결 문서 → InspectionReportModal로 열기 - if (result.data.workOrderId) { - setIsModalOpen(false); - setIsModalLoading(false); - setInspectionWorkOrderId(String(result.data.workOrderId)); - setIsInspectionModalOpen(true); - return; - } - setModalData(result.data as LinkedDocumentData); - } else { - toast.error(result.error || '문서 조회에 실패했습니다.'); - setIsModalOpen(false); - } - return; - } - - // 기존 결재 문서 타입 (품의서, 지출결의서, 지출예상내역서) - 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(() => { - if (selectedDocument) { - router.push(`/ko/approval/draft/new?id=${selectedDocument.id}&mode=edit`); - setIsModalOpen(false); - } - }, [selectedDocument, router]); - - const handleModalCopy = useCallback(() => { - toast.info('문서 복제 기능은 준비 중입니다.'); - setIsModalOpen(false); - }, []); - - const handleModalApprove = useCallback(async () => { - if (!selectedDocument?.id) return; - const result = await approveDocument(selectedDocument.id); - if (result.success) { - toast.success('문서가 승인되었습니다.'); - loadData(); - } else { - toast.error(result.error || '승인에 실패했습니다.'); - } - setIsModalOpen(false); - }, [selectedDocument, loadData]); - - const handleModalReject = useCallback(async () => { - if (!selectedDocument?.id) return; - const result = await rejectDocument(selectedDocument.id, '반려'); - if (result.success) { - toast.success('문서가 반려되었습니다.'); - loadData(); - } else { - toast.error(result.error || '반려에 실패했습니다.'); - } - setIsModalOpen(false); - }, [selectedDocument, loadData]); - - // ===== 문서 타입 변환 ===== - const getDocumentType = (approvalType: ApprovalType): DocumentType => { - switch (approvalType) { - case 'expense_estimate': - return 'expenseEstimate'; - case 'expense_report': - return 'expenseReport'; - case 'document': - return 'document'; - default: - return 'proposal'; - } - }; - // ===== 탭 옵션 ===== - const tabs: TabOption[] = useMemo( - () => [ - { - value: 'all', - label: APPROVAL_TAB_LABELS.all, - count: fixedStats.all, - color: 'blue', - }, - { - value: 'pending', - label: APPROVAL_TAB_LABELS.pending, - count: fixedStats.pending, - color: 'yellow', - }, - { - value: 'approved', - label: APPROVAL_TAB_LABELS.approved, - count: fixedStats.approved, - color: 'green', - }, - { - value: 'rejected', - label: APPROVAL_TAB_LABELS.rejected, - count: fixedStats.rejected, - color: 'red', - }, - ], - [fixedStats] - ); - - // ===== UniversalListPage 설정 ===== - const approvalBoxConfig: UniversalListConfig = useMemo( - () => ({ - title: '결재함', - description: '결재 문서를 관리합니다', - icon: FileCheck, - basePath: '/approval/inbox', - - idField: 'id', - - actions: { - getList: async () => ({ - success: true, - data: data, - totalCount: totalCount, - totalPages: totalPages, - }), - }, - - columns: [ - { key: 'no', label: '번호', className: 'w-[60px] text-center' }, - { key: 'documentNo', label: '문서번호', copyable: true }, - { key: 'approvalType', label: '문서유형', copyable: true }, - { key: 'title', label: '제목', copyable: true }, - { key: 'drafter', label: '기안자', copyable: true }, - { key: 'approver', label: '결재자', copyable: true }, - { key: 'draftDate', label: '기안일시', copyable: true }, - { key: 'status', label: '상태', className: 'text-center' }, - ], - - tabs: tabs, - defaultTab: activeTab, - - // 검색창 (공통 컴포넌트에서 자동 생성) - hideSearch: true, - searchValue: searchQuery, - onSearchChange: setSearchQuery, - - dateRangeSelector: { - enabled: true, - showPresets: true, - startDate, - endDate, - onStartDateChange: setStartDate, - onEndDateChange: setEndDate, - }, - - searchPlaceholder: '제목, 기안자, 부서 검색...', - searchFilter: (item: ApprovalRecord, search: string) => { - const s = search.toLowerCase(); - return ( - item.title?.toLowerCase().includes(s) || - item.drafter?.toLowerCase().includes(s) || - item.drafterDepartment?.toLowerCase().includes(s) || - false - ); - }, - - itemsPerPage: itemsPerPage, - - // 모바일 필터 설정 - filterConfig: [ - { - key: 'approvalType', - label: '문서유형', - type: 'single', - options: FILTER_OPTIONS.filter((o) => o.value !== 'all'), - }, - { - key: 'sort', - label: '정렬', - type: 'single', - options: SORT_OPTIONS, - }, - ], - initialFilters: { - approvalType: filterOption, - sort: sortOption, - }, - filterTitle: '결재함 필터', - - computeStats: () => [ - { - label: '전체결재', - value: `${fixedStats.all}건`, - icon: Files, - iconColor: 'text-blue-500', - }, - { - label: '미결재', - value: `${fixedStats.pending}건`, - icon: Clock, - iconColor: 'text-yellow-500', - }, - { - label: '결재완료', - value: `${fixedStats.approved}건`, - icon: FileCheck, - iconColor: 'text-green-500', - }, - { - label: '결재반려', - value: `${fixedStats.rejected}건`, - icon: FileX, - iconColor: 'text-red-500', - }, - ], - - selectionActions: ({ selectedItems, onClearSelection }) => canApprove ? ( - <> - - - - ) : null, - - tableHeaderActions: ( -
- - - -
- ), - - renderTableRow: (item, index, globalIndex, handlers) => { - const { isSelected, onToggle } = handlers; - - return ( - handleDocumentClick(item)} - > - e.stopPropagation()}> - - - {globalIndex} - {item.documentNo} - - {APPROVAL_TYPE_LABELS[item.approvalType]} - - - {item.title} - - {item.drafter} - {item.approver || '-'} - {item.draftDate} - - - {APPROVAL_STATUS_LABELS[item.status]} - - - - ); - }, - - renderMobileCard: (item, index, globalIndex, handlers) => { - const { isSelected, onToggle } = handlers; - - return ( - - {APPROVAL_TYPE_LABELS[item.approvalType]} - - {APPROVAL_STATUS_LABELS[item.status]} - - - } - isSelected={isSelected} - onToggleSelection={onToggle} - infoGrid={ -
- - - - - - -
- } - actions={ - item.status === 'pending' && isSelected && canApprove ? ( -
- - -
- ) : undefined - } - onClick={() => handleDocumentClick(item)} - /> - ); - }, - - renderDialogs: () => ( - <> - {/* 승인 확인 다이얼로그 */} - - - {/* 반려 확인 다이얼로그 */} - - - - 결재 반려 - - {pendingSelectedItems.size}건의 결재를 반려합니다. 반려 사유를 - 입력해주세요. - - -
- -