diff options
| author | joonhoekim <26rote@gmail.com> | 2025-11-06 12:20:19 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-11-06 12:20:19 +0900 |
| commit | ec67f25270df089fa65315091afa24f0e8995b98 (patch) | |
| tree | 8a0d0622b4efb4fef10d4c0bb676eef47c5fe8fc | |
| parent | ceaf46cd523f2bc94bbb35429e5ec0708a242caf (diff) | |
(김준회) 결재 후처리 강제실행 기능 추가
| -rw-r--r-- | app/[lng]/evcp/(evcp)/(system)/approval/log/[apInfId]/approval-log-detail-view.tsx | 68 | ||||
| -rw-r--r-- | config/menuConfig.ts | 11 | ||||
| -rw-r--r-- | i18n/locales/en/menu.json | 3 | ||||
| -rw-r--r-- | i18n/locales/ko/menu.json | 3 | ||||
| -rw-r--r-- | lib/approval-log/actions.ts | 89 | ||||
| -rw-r--r-- | lib/approval-log/table/approval-log-table-column.tsx | 50 |
6 files changed, 206 insertions, 18 deletions
diff --git a/app/[lng]/evcp/(evcp)/(system)/approval/log/[apInfId]/approval-log-detail-view.tsx b/app/[lng]/evcp/(evcp)/(system)/approval/log/[apInfId]/approval-log-detail-view.tsx index 80cf4379..eb59fb28 100644 --- a/app/[lng]/evcp/(evcp)/(system)/approval/log/[apInfId]/approval-log-detail-view.tsx +++ b/app/[lng]/evcp/(evcp)/(system)/approval/log/[apInfId]/approval-log-detail-view.tsx @@ -3,9 +3,14 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Separator } from "@/components/ui/separator"; +import { Button } from "@/components/ui/button"; import { ApprovalLogDetail } from "@/lib/approval-log/service"; import { formatDate } from "@/lib/utils"; -import { Clock, Mail, User, FileText, Shield, AlertCircle, CheckCircle, XCircle, Zap } from "lucide-react"; +import { Clock, Mail, User, FileText, Shield, AlertCircle, CheckCircle, XCircle, Zap, PlayCircle } from "lucide-react"; +import { forcePostProcessApproval } from "@/lib/approval-log/actions"; +import { toast } from "sonner"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; interface ApprovalLogDetailViewProps { detail: ApprovalLogDetail; @@ -13,6 +18,38 @@ interface ApprovalLogDetailViewProps { export function ApprovalLogDetailView({ detail }: ApprovalLogDetailViewProps) { const { approvalLog, pendingAction } = detail; + const router = useRouter(); + const [isProcessing, setIsProcessing] = useState(false); + + // pendingAction이 있으면 후처리 버튼 표시 (서버에서 검증) + const canPostProcess = !!pendingAction; + + const handlePostProcess = async () => { + if (isProcessing) return; + + setIsProcessing(true); + try { + const result = await forcePostProcessApproval(approvalLog.apInfId); + + if (result.success) { + toast.success('후처리 성공', { + description: result.message, + }); + // 페이지 새로고침 + router.refresh(); + } else { + toast.error('후처리 실패', { + description: result.error, + }); + } + } catch (error) { + toast.error('후처리 오류', { + description: error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.', + }); + } finally { + setIsProcessing(false); + } + }; // 상태 텍스트 변환 const getStatusText = (status: string) => { @@ -252,13 +289,28 @@ export function ApprovalLogDetailView({ detail }: ApprovalLogDetailViewProps) { {pendingAction && ( <Card> <CardHeader> - <CardTitle className="flex items-center gap-2"> - <CheckCircle className="h-5 w-5" /> - 액션 정보 - </CardTitle> - <CardDescription> - 결재와 연결된 Pending Action 정보입니다. - </CardDescription> + <div className="flex items-center justify-between"> + <div> + <CardTitle className="flex items-center gap-2"> + <CheckCircle className="h-5 w-5" /> + 액션 정보 + </CardTitle> + <CardDescription> + 결재와 연결된 Pending Action 정보입니다. + </CardDescription> + </div> + {canPostProcess && ( + <Button + onClick={handlePostProcess} + disabled={isProcessing || pendingAction.status === 'executed'} + size="sm" + variant={pendingAction.status === 'executed' ? 'outline' : 'default'} + > + <PlayCircle className="mr-2 h-4 w-4" /> + {isProcessing ? '처리중...' : pendingAction.status === 'executed' ? '후처리 완료됨' : '후처리 실행'} + </Button> + )} + </div> </CardHeader> <CardContent className="space-y-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> diff --git a/config/menuConfig.ts b/config/menuConfig.ts index bbc126c1..948ef20e 100644 --- a/config/menuConfig.ts +++ b/config/menuConfig.ts @@ -475,11 +475,12 @@ export const mainNav: MenuSection[] = [ href: '/evcp/approval/line', groupKey: 'groups.approval', }, - { - titleKey: 'menu.information_system.approval_after', - href: '/evcp/approval/after', - groupKey: 'groups.approval', - }, + // 결재 후처리는 결재 로그에서 진행 + // { + // titleKey: 'menu.information_system.approval_after', + // href: '/evcp/approval/after', + // groupKey: 'groups.approval', + // }, { titleKey: 'menu.information_system.email_template', href: '/evcp/email-template', diff --git a/i18n/locales/en/menu.json b/i18n/locales/en/menu.json index 8a245e04..5a7e3297 100644 --- a/i18n/locales/en/menu.json +++ b/i18n/locales/en/menu.json @@ -187,10 +187,9 @@ "menu_access_dept": "Menu Access Control (By Department)", "integration_list": "Interface List Management", "integration_log": "Interface History Inquiry", - "approval_log": "Approval History Inquiry", + "approval_log": "Approval History And After Processing", "approval_template": "Approval Template Management", "approval_line": "Approval Line Management", - "approval_after": "Post-Approval Management", "email_template": "Email Template Management", "email_receiver": "Email Recipient Management", "email_log": "Email Transmission History Inquiry", diff --git a/i18n/locales/ko/menu.json b/i18n/locales/ko/menu.json index 407c490b..44e41e59 100644 --- a/i18n/locales/ko/menu.json +++ b/i18n/locales/ko/menu.json @@ -191,10 +191,9 @@ "menu_access_dept": "메뉴 접근제어 (부서별)", "integration_list": "인터페이스 목록 관리", "integration_log": "인터페이스 이력 조회", - "approval_log": "결재 이력 조회", + "approval_log": "결재 이력 조회 및 후처리", "approval_template": "결재 서식 관리", "approval_line": "결재선 관리", - "approval_after": "결재 후처리 관리", "email_template": "이메일 서식 관리", "email_receiver": "이메일 수신인 관리", "email_log": "이메일 발신 이력 조회", diff --git a/lib/approval-log/actions.ts b/lib/approval-log/actions.ts new file mode 100644 index 00000000..64393909 --- /dev/null +++ b/lib/approval-log/actions.ts @@ -0,0 +1,89 @@ +'use server'; + +import { revalidatePath } from 'next/cache'; +import { ApprovalExecutionSaga } from '@/lib/approval/approval-saga'; +import { getApprovalLogDetail } from './service'; +import db from '@/db/db'; +import { pendingActions } from '@/db/schema/knox/pending-actions'; +import { eq } from 'drizzle-orm'; + +/** + * 결재 후처리 강제 실행 + * + * 외부 시스템(Knox) 장애 시 결재가 승인되었지만 + * 후속 작업이 실행되지 않은 경우 강제로 실행 + * + * @param apInfId 결재 ID + * @returns 성공 여부 및 결과 메시지 + */ +export async function forcePostProcessApproval(apInfId: string) { + try { + console.log(`[ForcePostProcess] Starting forced post-process for ${apInfId}`); + + // 1. 결재 로그 및 Pending Action 조회 + const detail = await getApprovalLogDetail(apInfId); + + if (!detail) { + return { + success: false, + error: '결재 로그를 찾을 수 없습니다.', + }; + } + + const { pendingAction } = detail; + + // 2. Pending Action이 없는 경우 + if (!pendingAction) { + return { + success: false, + error: '실행할 후처리 작업이 없습니다. 이 결재는 후처리 작업이 연결되지 않은 결재입니다.', + }; + } + + // 3. 이미 실행된 경우 + if (pendingAction.status === 'executed') { + return { + success: false, + error: '이미 후처리가 완료된 결재입니다.', + }; + } + + // 4. Pending Action 상태를 'pending'으로 재설정 (강제 실행을 위해) + console.log(`[ForcePostProcess] Resetting pending action status to 'pending'`); + await db.update(pendingActions) + .set({ + status: 'pending', + errorMessage: null, + executionResult: null, + }) + .where(eq(pendingActions.apInfId, apInfId)); + + // 5. ApprovalExecutionSaga를 통해 강제 실행 + console.log(`[ForcePostProcess] Executing ApprovalExecutionSaga`); + const saga = new ApprovalExecutionSaga(apInfId); + const result = await saga.execute(); + + // 6. 캐시 무효화 + revalidatePath(`/[lng]/evcp/approval/log`, 'page'); + revalidatePath(`/[lng]/evcp/approval/log/${apInfId}`, 'page'); + + console.log(`[ForcePostProcess] ✅ Post-process completed successfully`); + + return { + success: true, + message: '후처리가 성공적으로 실행되었습니다.', + result, + }; + + } catch (error) { + console.error('[ForcePostProcess] ❌ Post-process failed:', error); + + return { + success: false, + error: error instanceof Error + ? `후처리 실행 중 오류가 발생했습니다: ${error.message}` + : '후처리 실행 중 알 수 없는 오류가 발생했습니다.', + }; + } +} + diff --git a/lib/approval-log/table/approval-log-table-column.tsx b/lib/approval-log/table/approval-log-table-column.tsx index 747ce5ce..6005b0ff 100644 --- a/lib/approval-log/table/approval-log-table-column.tsx +++ b/lib/approval-log/table/approval-log-table-column.tsx @@ -8,14 +8,17 @@ import { Checkbox } from "@/components/ui/checkbox" import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" import { type ApprovalLog } from "../service" import { formatDate } from "@/lib/utils" -import { MoreHorizontal, Eye } from "lucide-react" +import { MoreHorizontal, Eye, PlayCircle } from "lucide-react" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { useParams, useRouter } from "next/navigation" +import { forcePostProcessApproval } from "../actions" +import { toast } from "sonner" interface GetColumnsProps { setRowAction: React.Dispatch<React.SetStateAction<{ @@ -256,6 +259,39 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<Approva id: "actions", cell: ({ row }) => { const apInfId = row.original.apInfId; + // eslint-disable-next-line react-hooks/rules-of-hooks + const [isProcessing, setIsProcessing] = React.useState(false); + + // 항상 후처리 메뉴를 표시 (서버에서 검증) + const canPostProcess = true; + + const handlePostProcess = async () => { + if (isProcessing) return; + + setIsProcessing(true); + try { + const result = await forcePostProcessApproval(apInfId); + + if (result.success) { + toast.success('후처리 성공', { + description: result.message, + }); + // 페이지 새로고침 + router.refresh(); + } else { + toast.error('후처리 실패', { + description: result.error, + }); + } + } catch (error) { + toast.error('후처리 오류', { + description: error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.', + }); + } finally { + setIsProcessing(false); + } + }; + return ( <DropdownMenu> <DropdownMenuTrigger asChild> @@ -276,6 +312,18 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<Approva <Eye className="mr-2 size-4" aria-hidden="true" /> 상세보기 </DropdownMenuItem> + {canPostProcess && ( + <> + <DropdownMenuSeparator /> + <DropdownMenuItem + onClick={handlePostProcess} + disabled={isProcessing} + > + <PlayCircle className="mr-2 size-4" aria-hidden="true" /> + {isProcessing ? '처리중...' : '후처리 실행'} + </DropdownMenuItem> + </> + )} </DropdownMenuContent> </DropdownMenu> ); |
