summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/approval-log/actions.ts89
-rw-r--r--lib/approval-log/table/approval-log-table-column.tsx50
2 files changed, 138 insertions, 1 deletions
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>
);