summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-11-05 16:46:43 +0900
committerjoonhoekim <26rote@gmail.com>2025-11-05 16:46:43 +0900
commita2c78d3a00c569a37ab93f65b58a11ba3519b596 (patch)
tree1909ff3d52bb6f17a5b376d332255291cc71ecf5
parent208ed7ff11d0f822d3d243c5833d31973904349e (diff)
(김준회) 실사의뢰/실사재의뢰 누락된 userId 추가해서 pendingActions에 추가하도록 변경
-rw-r--r--app/[lng]/evcp/(evcp)/(system)/approval/log/[apInfId]/approval-log-detail-view.tsx377
-rw-r--r--app/[lng]/evcp/(evcp)/(system)/approval/log/[apInfId]/page.tsx56
-rw-r--r--db/schema/knox/pending-actions.ts10
-rw-r--r--lib/approval-log/service.ts59
-rw-r--r--lib/approval-log/table/approval-log-table-column.tsx10
-rw-r--r--lib/pq/service.ts22
-rw-r--r--lib/vendor-investigation/approval-actions.ts2
-rw-r--r--lib/vendor-investigation/handlers.ts25
8 files changed, 551 insertions, 10 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
new file mode 100644
index 00000000..80cf4379
--- /dev/null
+++ b/app/[lng]/evcp/(evcp)/(system)/approval/log/[apInfId]/approval-log-detail-view.tsx
@@ -0,0 +1,377 @@
+"use client";
+
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+import { Separator } from "@/components/ui/separator";
+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";
+
+interface ApprovalLogDetailViewProps {
+ detail: ApprovalLogDetail;
+}
+
+export function ApprovalLogDetailView({ detail }: ApprovalLogDetailViewProps) {
+ const { approvalLog, pendingAction } = detail;
+
+ // 상태 텍스트 변환
+ const getStatusText = (status: string) => {
+ const statusMap: Record<string, string> = {
+ '-3': '암호화실패',
+ '-2': '암호화중',
+ '-1': '예약상신',
+ '0': '보류',
+ '1': '진행중',
+ '2': '완결',
+ '3': '반려',
+ '4': '상신취소',
+ '5': '전결',
+ '6': '후완결'
+ };
+ return statusMap[status] || '알 수 없음';
+ };
+
+ const getStatusVariant = (status: string) => {
+ switch (status) {
+ case '2': return 'default'; // 완결
+ case '3': return 'destructive'; // 반려
+ case '4': return 'destructive'; // 상신취소
+ case '5': return 'default'; // 전결
+ case '6': return 'default'; // 후완결
+ case '1': return 'secondary'; // 진행중
+ default: return 'outline'; // 기타
+ }
+ };
+
+ const getSecurityText = (type: string) => {
+ switch (type) {
+ case 'CONFIDENTIAL_STRICT': return '극비';
+ case 'CONFIDENTIAL': return '기밀';
+ case 'PERSONAL': return '개인';
+ default: return type || '개인';
+ }
+ };
+
+ const getSecurityVariant = (type: string) => {
+ switch (type) {
+ case 'CONFIDENTIAL_STRICT': return 'destructive';
+ case 'CONFIDENTIAL': return 'secondary';
+ default: return 'outline';
+ }
+ };
+
+ // Pending Action 상태 텍스트 및 뱃지
+ const getPendingActionStatusText = (status: string) => {
+ const statusMap: Record<string, string> = {
+ 'pending': '결재 대기 중',
+ 'approved': '결재 승인됨 (실행 대기)',
+ 'executed': '실행 완료',
+ 'failed': '실행 실패',
+ 'rejected': '결재 반려됨',
+ 'cancelled': '결재 취소됨',
+ };
+ return statusMap[status] || status;
+ };
+
+ const getPendingActionStatusVariant = (status: string) => {
+ switch (status) {
+ case 'executed': return 'default';
+ case 'failed': return 'destructive';
+ case 'rejected': return 'destructive';
+ case 'cancelled': return 'destructive';
+ case 'approved': return 'secondary';
+ case 'pending': return 'outline';
+ default: return 'outline';
+ }
+ };
+
+ // 상신일시 포맷
+ const formatSbmDt = (sbmDt: string | null) => {
+ if (!sbmDt) return '-';
+ return sbmDt.replace(
+ /(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/,
+ '$1-$2-$3 $4:$5:$6'
+ );
+ };
+
+ return (
+ <div className="space-y-6">
+ {/* 결재 기본 정보 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ <FileText className="h-5 w-5" />
+ 결재 기본 정보
+ </CardTitle>
+ <CardDescription>결재 문서의 기본 정보입니다.</CardDescription>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+ <div className="space-y-2">
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
+ <FileText className="h-4 w-4" />
+ 결재 ID
+ </div>
+ <div className="font-mono text-sm bg-muted p-2 rounded">
+ {approvalLog.apInfId}
+ </div>
+ </div>
+
+ <div className="space-y-2">
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
+ <AlertCircle className="h-4 w-4" />
+ 상태
+ </div>
+ <div>
+ <Badge variant={getStatusVariant(approvalLog.status)}>
+ {getStatusText(approvalLog.status)}
+ </Badge>
+ </div>
+ </div>
+
+ <div className="space-y-2">
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
+ <User className="h-4 w-4" />
+ 사용자 ID
+ </div>
+ <div className="text-sm">{approvalLog.userId || '-'}</div>
+ </div>
+
+ <div className="space-y-2">
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
+ <Mail className="h-4 w-4" />
+ 이메일
+ </div>
+ <div className="text-sm">{approvalLog.emailAddress}</div>
+ </div>
+
+ <div className="space-y-2">
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
+ <Clock className="h-4 w-4" />
+ 상신일시
+ </div>
+ <div className="text-sm">{formatSbmDt(approvalLog.sbmDt)}</div>
+ </div>
+
+ <div className="space-y-2">
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
+ <Shield className="h-4 w-4" />
+ 보안등급
+ </div>
+ <div>
+ <Badge variant={getSecurityVariant(approvalLog.docSecuType)}>
+ {getSecurityText(approvalLog.docSecuType)}
+ </Badge>
+ </div>
+ </div>
+
+ <div className="space-y-2">
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
+ <Zap className="h-4 w-4" />
+ 긴급여부
+ </div>
+ <div>
+ {approvalLog.urgYn === 'Y' ? (
+ <Badge variant="destructive">긴급</Badge>
+ ) : (
+ <span className="text-sm">일반</span>
+ )}
+ </div>
+ </div>
+
+ <div className="space-y-2">
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
+ <FileText className="h-4 w-4" />
+ 본문종류
+ </div>
+ <div className="text-sm">{approvalLog.contentsType}</div>
+ </div>
+ </div>
+
+ <Separator />
+
+ <div className="space-y-2">
+ <div className="text-sm font-medium text-muted-foreground">제목</div>
+ <div className="text-base font-medium">{approvalLog.subject}</div>
+ </div>
+
+ {approvalLog.opinion && (
+ <>
+ <Separator />
+ <div className="space-y-2">
+ <div className="text-sm font-medium text-muted-foreground">상신의견</div>
+ <div className="text-sm">{approvalLog.opinion}</div>
+ </div>
+ </>
+ )}
+ </CardContent>
+ </Card>
+
+ {/* 결재선 정보 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ <User className="h-5 w-5" />
+ 결재선 정보
+ </CardTitle>
+ <CardDescription>결재 승인 라인 정보입니다.</CardDescription>
+ </CardHeader>
+ <CardContent>
+ <div className="space-y-2">
+ <pre className="text-xs bg-muted p-4 rounded overflow-auto max-h-[400px]">
+ {JSON.stringify(approvalLog.aplns, null, 2)}
+ </pre>
+ </div>
+ </CardContent>
+ </Card>
+
+ {/* 결재 본문 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ <FileText className="h-5 w-5" />
+ 결재 본문
+ </CardTitle>
+ <CardDescription>결재 문서의 상세 내용입니다.</CardDescription>
+ </CardHeader>
+ <CardContent>
+ {approvalLog.contentsType === 'HTML' ? (
+ <div
+ className="prose prose-sm max-w-none dark:prose-invert"
+ dangerouslySetInnerHTML={{ __html: approvalLog.content }}
+ />
+ ) : (
+ <pre className="text-sm whitespace-pre-wrap bg-muted p-4 rounded">
+ {approvalLog.content}
+ </pre>
+ )}
+ </CardContent>
+ </Card>
+
+ {/* Pending Action 정보 */}
+ {pendingAction && (
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ <CheckCircle className="h-5 w-5" />
+ 액션 정보
+ </CardTitle>
+ <CardDescription>
+ 결재와 연결된 Pending Action 정보입니다.
+ </CardDescription>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+ <div className="space-y-2">
+ <div className="text-sm text-muted-foreground">액션 ID</div>
+ <div className="font-mono text-sm">{pendingAction.id}</div>
+ </div>
+
+ <div className="space-y-2">
+ <div className="text-sm text-muted-foreground">액션 타입</div>
+ <div className="text-sm font-medium">{pendingAction.actionType}</div>
+ </div>
+
+ <div className="space-y-2">
+ <div className="text-sm text-muted-foreground">상태</div>
+ <div>
+ <Badge variant={getPendingActionStatusVariant(pendingAction.status)}>
+ {getPendingActionStatusText(pendingAction.status)}
+ </Badge>
+ </div>
+ </div>
+
+ {pendingAction.executedAt && (
+ <div className="space-y-2">
+ <div className="text-sm text-muted-foreground">실행 시간</div>
+ <div className="text-sm">{formatDate(pendingAction.executedAt)}</div>
+ </div>
+ )}
+
+ <div className="space-y-2">
+ <div className="text-sm text-muted-foreground">생성일</div>
+ <div className="text-sm">{formatDate(pendingAction.createdAt)}</div>
+ </div>
+
+ <div className="space-y-2">
+ <div className="text-sm text-muted-foreground">수정일</div>
+ <div className="text-sm">{formatDate(pendingAction.updatedAt)}</div>
+ </div>
+ </div>
+
+ <Separator />
+
+ <div className="space-y-2">
+ <div className="text-sm font-medium text-muted-foreground">액션 페이로드</div>
+ <pre className="text-xs bg-muted p-4 rounded overflow-auto max-h-[300px]">
+ {JSON.stringify(pendingAction.actionPayload, null, 2)}
+ </pre>
+ </div>
+
+ {pendingAction.executionResult && (
+ <>
+ <Separator />
+ <div className="space-y-2">
+ <div className="text-sm font-medium text-muted-foreground">실행 결과</div>
+ <pre className="text-xs bg-muted p-4 rounded overflow-auto max-h-[300px]">
+ {JSON.stringify(pendingAction.executionResult, null, 2)}
+ </pre>
+ </div>
+ </>
+ )}
+
+ {pendingAction.errorMessage && (
+ <>
+ <Separator />
+ <div className="space-y-2">
+ <div className="flex items-center gap-2 text-sm font-medium text-destructive">
+ <XCircle className="h-4 w-4" />
+ 에러 메시지
+ </div>
+ <div className="text-sm text-destructive bg-destructive/10 p-3 rounded">
+ {pendingAction.errorMessage}
+ </div>
+ </div>
+ </>
+ )}
+ </CardContent>
+ </Card>
+ )}
+
+ {/* 메타데이터 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ <Clock className="h-5 w-5" />
+ 메타데이터
+ </CardTitle>
+ <CardDescription>생성 및 수정 정보입니다.</CardDescription>
+ </CardHeader>
+ <CardContent>
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+ <div className="space-y-2">
+ <div className="text-sm text-muted-foreground">생성일</div>
+ <div className="text-sm">{formatDate(approvalLog.createdAt)}</div>
+ </div>
+
+ <div className="space-y-2">
+ <div className="text-sm text-muted-foreground">수정일</div>
+ <div className="text-sm">{formatDate(approvalLog.updatedAt)}</div>
+ </div>
+
+ <div className="space-y-2">
+ <div className="text-sm text-muted-foreground">타임존</div>
+ <div className="text-sm">{approvalLog.timeZone}</div>
+ </div>
+
+ <div className="space-y-2">
+ <div className="text-sm text-muted-foreground">상신언어</div>
+ <div className="text-sm">{approvalLog.sbmLang}</div>
+ </div>
+ </div>
+ </CardContent>
+ </Card>
+ </div>
+ );
+}
+
diff --git a/app/[lng]/evcp/(evcp)/(system)/approval/log/[apInfId]/page.tsx b/app/[lng]/evcp/(evcp)/(system)/approval/log/[apInfId]/page.tsx
new file mode 100644
index 00000000..3567d87a
--- /dev/null
+++ b/app/[lng]/evcp/(evcp)/(system)/approval/log/[apInfId]/page.tsx
@@ -0,0 +1,56 @@
+import { notFound } from "next/navigation";
+import { Shell } from "@/components/shell";
+import { getApprovalLogDetail } from "@/lib/approval-log/service";
+import { ApprovalLogDetailView } from "./approval-log-detail-view";
+import { InformationButton } from "@/components/information/information-button";
+import { Button } from "@/components/ui/button";
+import { ArrowLeft } from "lucide-react";
+import Link from "next/link";
+
+interface ApprovalLogDetailPageProps {
+ params: {
+ lng: string;
+ apInfId: string;
+ };
+}
+
+export default async function ApprovalLogDetailPage({
+ params,
+}: ApprovalLogDetailPageProps) {
+ const { lng, apInfId } = params;
+
+ // 상세 정보 조회
+ const detail = await getApprovalLogDetail(apInfId);
+
+ if (!detail) {
+ notFound();
+ }
+
+ return (
+ <Shell className="gap-4">
+ <div className="flex items-center justify-between">
+ <div className="flex items-center gap-4">
+ <Link href={`/${lng}/evcp/approval/log`}>
+ <Button variant="ghost" size="icon">
+ <ArrowLeft className="h-4 w-4" />
+ </Button>
+ </Link>
+ <div>
+ <div className="flex items-center gap-2">
+ <h2 className="text-2xl font-bold tracking-tight">
+ 결재 로그 상세
+ </h2>
+ <InformationButton pagePath="evcp/approval/log" />
+ </div>
+ <p className="text-sm text-muted-foreground mt-1">
+ {detail.approvalLog.subject}
+ </p>
+ </div>
+ </div>
+ </div>
+
+ <ApprovalLogDetailView detail={detail} />
+ </Shell>
+ );
+}
+
diff --git a/db/schema/knox/pending-actions.ts b/db/schema/knox/pending-actions.ts
index e909dc27..ef41dca7 100644
--- a/db/schema/knox/pending-actions.ts
+++ b/db/schema/knox/pending-actions.ts
@@ -20,10 +20,12 @@ export const pendingActions = knoxSchema.table("pending_actions", {
// 기본 정보
id: serial("id").primaryKey(),
- // 결재 연결 (approvalLogs의 apInfId 참조)
- apInfId: text("ap_inf_id")
- .references(() => approvalLogs.apInfId, { onDelete: "cascade" })
- .notNull(),
+ // 결재 연결 (approvalLogs의 apInfId와 논리적으로 연결)
+ // 주의: FK 제약 조건을 사용하지 않음
+ // 이유: pending_actions는 Knox 상신 전에 생성되지만,
+ // approval_logs는 Knox와 동기화되어 나중에 생성되므로
+ // FK 제약이 있으면 Saga Pattern의 순서(DB → Knox)를 지킬 수 없음
+ apInfId: text("ap_inf_id").notNull(),
// 액션 정보
actionType: text("action_type").notNull(), // 예: 'vendor_investigation_request', 'purchase_order_request'
diff --git a/lib/approval-log/service.ts b/lib/approval-log/service.ts
index 4d1ad7f8..5690e0f9 100644
--- a/lib/approval-log/service.ts
+++ b/lib/approval-log/service.ts
@@ -13,6 +13,7 @@ import {
import db from '@/db/db';
import { approvalLogs } from '@/db/schema/knox/approvals';
+import { pendingActions } from '@/db/schema/knox/pending-actions';
import { filterColumns } from '@/lib/filter-columns';
// ---------------------------------------------
@@ -198,3 +199,61 @@ export async function getApprovalLogListAction(input: ListInput) {
};
}
}
+
+// ----------------------------------------------------
+// Get approval log detail with pending action
+// ----------------------------------------------------
+export type PendingAction = typeof pendingActions.$inferSelect;
+
+export interface ApprovalLogDetail {
+ approvalLog: ApprovalLog;
+ pendingAction: PendingAction | null;
+}
+
+export async function getApprovalLogDetail(apInfId: string): Promise<ApprovalLogDetail | null> {
+ try {
+ // approvalLog 조회
+ const approvalLog = await getApprovalLog(apInfId);
+ if (!approvalLog) {
+ return null;
+ }
+
+ // pendingAction 조회
+ const [pendingAction] = await db
+ .select()
+ .from(pendingActions)
+ .where(eq(pendingActions.apInfId, apInfId))
+ .limit(1);
+
+ return {
+ approvalLog,
+ pendingAction: pendingAction || null,
+ };
+ } catch (error) {
+ console.error('Error fetching approval log detail:', error);
+ return null;
+ }
+}
+
+// ----------------------------------------------------
+// Server Action for getting approval log detail
+// ----------------------------------------------------
+export async function getApprovalLogDetailAction(apInfId: string) {
+ try {
+ const data = await getApprovalLogDetail(apInfId);
+ if (!data) {
+ return {
+ success: false,
+ error: '결재 로그를 찾을 수 없습니다.',
+ data: null,
+ };
+ }
+ return { success: true, data };
+ } catch (error) {
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : '결재 로그 상세 조회에 실패했습니다.',
+ data: null,
+ };
+ }
+}
diff --git a/lib/approval-log/table/approval-log-table-column.tsx b/lib/approval-log/table/approval-log-table-column.tsx
index a77ed0d3..747ce5ce 100644
--- a/lib/approval-log/table/approval-log-table-column.tsx
+++ b/lib/approval-log/table/approval-log-table-column.tsx
@@ -15,6 +15,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
+import { useParams, useRouter } from "next/navigation"
interface GetColumnsProps {
setRowAction: React.Dispatch<React.SetStateAction<{
@@ -24,6 +25,12 @@ interface GetColumnsProps {
}
export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<ApprovalLog>[] {
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const router = useRouter();
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const params = useParams();
+ const lng = params.lng as string;
+
return [
{
id: "select",
@@ -248,6 +255,7 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<Approva
{
id: "actions",
cell: ({ row }) => {
+ const apInfId = row.original.apInfId;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
@@ -262,7 +270,7 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<Approva
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuItem
onClick={() => {
- setRowAction({ type: "view", row });
+ router.push(`/${lng}/evcp/approval/log/${apInfId}`);
}}
>
<Eye className="mr-2 size-4" aria-hidden="true" />
diff --git a/lib/pq/service.ts b/lib/pq/service.ts
index d3974964..b39bf7bd 100644
--- a/lib/pq/service.ts
+++ b/lib/pq/service.ts
@@ -2985,10 +2985,26 @@ export async function cancelInvestigationAction(investigationIds: number[]) {
}
// 실사 재의뢰 서버 액션
-export async function reRequestInvestigationAction(investigationIds: number[]) {
+export async function reRequestInvestigationAction(
+ investigationIds: number[],
+ currentUser?: { id: number } // ✅ 핸들러에서 호출 시 사용자 정보 전달
+) {
try {
- const session = await getServerSession(authOptions)
- const userId = session?.user?.id ? Number(session.user.id) : null
+ let userId: number | null = null;
+
+ if (currentUser) {
+ // 핸들러에서 호출 시 (결재 승인 후)
+ userId = currentUser.id;
+
+ // ✅ 핸들러에서 호출 시 userId 검증: 없으면 잘못된 상황 (예외 처리)
+ if (!userId || userId <= 0) {
+ throw new Error('핸들러에서 호출 시 currentUser.id가 필수입니다.');
+ }
+ } else {
+ // 직접 호출 시 (세션에서 가져오기)
+ const session = await getServerSession(authOptions);
+ userId = session?.user?.id ? Number(session.user.id) : null;
+ }
if (!userId) {
return { success: false, error: "인증된 사용자만 실사를 재의뢰할 수 있습니다." }
diff --git a/lib/vendor-investigation/approval-actions.ts b/lib/vendor-investigation/approval-actions.ts
index 5da30011..e8e24ddc 100644
--- a/lib/vendor-investigation/approval-actions.ts
+++ b/lib/vendor-investigation/approval-actions.ts
@@ -100,6 +100,7 @@ export async function requestPQInvestigationWithApproval(data: {
forecastedAt: data.forecastedAt,
investigationAddress: data.investigationAddress,
investigationNotes: data.investigationNotes,
+ currentUserId: data.currentUser.id, // ✅ 결재 승인 후 핸들러 실행 시 필요
},
// approvalConfig: 결재 상신 정보 (템플릿 포함)
@@ -227,6 +228,7 @@ export async function reRequestPQInvestigationWithApproval(data: {
// actionPayload: 결재 승인 후 핸들러에 전달될 데이터 (최소 데이터만)
{
investigationIds: data.investigationIds,
+ currentUserId: data.currentUser.id, // ✅ 결재 승인 후 핸들러 실행 시 필요
},
// approvalConfig: 결재 상신 정보 (템플릿 포함)
diff --git a/lib/vendor-investigation/handlers.ts b/lib/vendor-investigation/handlers.ts
index 28a218b5..3165df06 100644
--- a/lib/vendor-investigation/handlers.ts
+++ b/lib/vendor-investigation/handlers.ts
@@ -22,19 +22,28 @@ export async function requestPQInvestigationInternal(payload: {
forecastedAt: Date;
investigationAddress: string;
investigationNotes?: string;
+ currentUserId: number; // ✅ 결재 상신한 사용자 ID
}) {
debugLog('[PQInvestigationHandler] 실사 의뢰 핸들러 시작', {
pqCount: payload.pqSubmissionIds.length,
qmManagerId: payload.qmManagerId,
+ currentUserId: payload.currentUserId,
});
+ // ✅ userId 검증: 핸들러에서 userId가 없으면 잘못된 상황 (예외 처리)
+ if (!payload.currentUserId || payload.currentUserId <= 0) {
+ const errorMessage = 'currentUserId가 없습니다. actionPayload에 currentUserId가 포함되지 않았습니다.';
+ debugError('[PQInvestigationHandler]', errorMessage);
+ throw new Error(errorMessage);
+ }
+
try {
// 기존 PQ 서비스 함수 사용 (DB 트랜잭션 포함)
const { requestInvestigationAction } = await import('@/lib/pq/service');
const result = await requestInvestigationAction(
payload.pqSubmissionIds,
- { id: 0, epId: null, email: undefined }, // 핸들러에서는 currentUser 불필요
+ { id: payload.currentUserId, epId: null, email: undefined }, // ✅ 실제 사용자 ID 전달
{
qmManagerId: payload.qmManagerId,
forecastedAt: payload.forecastedAt,
@@ -106,16 +115,28 @@ export async function mapPQInvestigationToTemplateVariables(payload: {
*/
export async function reRequestPQInvestigationInternal(payload: {
investigationIds: number[];
+ currentUserId: number; // ✅ 결재 상신한 사용자 ID
}) {
debugLog('[PQReRequestHandler] 실사 재의뢰 핸들러 시작', {
investigationCount: payload.investigationIds.length,
+ currentUserId: payload.currentUserId,
});
+ // ✅ userId 검증: 핸들러에서 userId가 없으면 잘못된 상황 (예외 처리)
+ if (!payload.currentUserId || payload.currentUserId <= 0) {
+ const errorMessage = 'currentUserId가 없습니다. actionPayload에 currentUserId가 포함되지 않았습니다.';
+ debugError('[PQReRequestHandler]', errorMessage);
+ throw new Error(errorMessage);
+ }
+
try {
// 기존 PQ 서비스 함수 사용
const { reRequestInvestigationAction } = await import('@/lib/pq/service');
- const result = await reRequestInvestigationAction(payload.investigationIds);
+ const result = await reRequestInvestigationAction(
+ payload.investigationIds,
+ { id: payload.currentUserId } // ✅ 실제 사용자 ID 전달
+ );
if (!result.success) {
debugError('[PQReRequestHandler] 실사 재의뢰 실패', result.error);