summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-11-06 12:20:19 +0900
committerjoonhoekim <26rote@gmail.com>2025-11-06 12:20:19 +0900
commitec67f25270df089fa65315091afa24f0e8995b98 (patch)
tree8a0d0622b4efb4fef10d4c0bb676eef47c5fe8fc
parentceaf46cd523f2bc94bbb35429e5ec0708a242caf (diff)
(김준회) 결재 후처리 강제실행 기능 추가
-rw-r--r--app/[lng]/evcp/(evcp)/(system)/approval/log/[apInfId]/approval-log-detail-view.tsx68
-rw-r--r--config/menuConfig.ts11
-rw-r--r--i18n/locales/en/menu.json3
-rw-r--r--i18n/locales/ko/menu.json3
-rw-r--r--lib/approval-log/actions.ts89
-rw-r--r--lib/approval-log/table/approval-log-table-column.tsx50
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>
);