diff options
| -rw-r--r-- | app/[lng]/evcp/(evcp)/(procurement)/pq_new/[vendorId]/[submissionId]/page.tsx | 6 | ||||
| -rw-r--r-- | app/[lng]/partners/(partners)/pq_new/[id]/page.tsx | 35 | ||||
| -rw-r--r-- | app/[lng]/partners/(partners)/pq_new/page.tsx | 10 | ||||
| -rw-r--r-- | components/additional-info/join-form.tsx | 59 | ||||
| -rw-r--r-- | components/pq-input/pq-input-tabs.tsx | 4 | ||||
| -rw-r--r-- | lib/general-contracts/detail/general-contract-approval-request-dialog.tsx | 5 | ||||
| -rw-r--r-- | lib/general-contracts/utils.ts | 63 | ||||
| -rw-r--r-- | lib/pq/service.ts | 2 | ||||
| -rw-r--r-- | lib/rfq-last/approval-actions.ts | 26 | ||||
| -rw-r--r-- | lib/rfq-last/service.ts | 2 | ||||
| -rw-r--r-- | lib/rfq-last/vendor-response/editor/vendor-response-editor.tsx | 44 | ||||
| -rw-r--r-- | lib/techsales-rfq/approval-actions.ts | 71 | ||||
| -rw-r--r-- | middleware.ts | 9 |
13 files changed, 314 insertions, 22 deletions
diff --git a/app/[lng]/evcp/(evcp)/(procurement)/pq_new/[vendorId]/[submissionId]/page.tsx b/app/[lng]/evcp/(evcp)/(procurement)/pq_new/[vendorId]/[submissionId]/page.tsx index 974550c6..5baf6efb 100644 --- a/app/[lng]/evcp/(evcp)/(procurement)/pq_new/[vendorId]/[submissionId]/page.tsx +++ b/app/[lng]/evcp/(evcp)/(procurement)/pq_new/[vendorId]/[submissionId]/page.tsx @@ -54,7 +54,11 @@ export default async function PQReviewPage(props: PQReviewPageProps) { const pqSubmission = await getPQById(submissionId, vendorId)
// PQ 데이터 조회 (질문과 답변)
- const pqData = await getPQDataByVendorId(vendorId, pqSubmission.projectId || undefined)
+ const pqData = await getPQDataByVendorId(
+ vendorId,
+ pqSubmission.projectId || undefined,
+ pqSubmission.type as "GENERAL" | "PROJECT" | "NON_INSPECTION"
+ )
// 협력업체 정보 (pqSubmission에 이미 포함되어 있음)
const vendorInfo = {
diff --git a/app/[lng]/partners/(partners)/pq_new/[id]/page.tsx b/app/[lng]/partners/(partners)/pq_new/[id]/page.tsx index cb905f39..448267d0 100644 --- a/app/[lng]/partners/(partners)/pq_new/[id]/page.tsx +++ b/app/[lng]/partners/(partners)/pq_new/[id]/page.tsx @@ -30,6 +30,32 @@ export default async function PQEditPage(props: PQEditPageProps) { const params = await props.params; const pqSubmissionId = parseInt(params.id, 10); + if (Number.isNaN(pqSubmissionId)) { + return ( + <Shell className="gap-6"> + <div className="flex items-center justify-between"> + <div> + <h2 className="text-2xl font-bold tracking-tight">잘못된 PQ ID</h2> + <p className="text-muted-foreground">유효한 PQ ID가 아닙니다.</p> + </div> + </div> + <div className="flex flex-col items-center justify-center py-12 text-center"> + <div className="rounded-lg border border-dashed p-10 shadow-sm"> + <p className="mb-6 text-muted-foreground"> + 요청하신 PQ ID를 확인하고 다시 시도해주세요. + </p> + <Button asChild> + <Link href="/partners/pq_new"> + <ArrowLeft className="mr-2 h-4 w-4" /> + 목록으로 돌아가기 + </Link> + </Button> + </div> + </div> + </Shell> + ); + } + // 인증 확인 const session = await getServerSession(authOptions); @@ -101,12 +127,17 @@ export default async function PQEditPage(props: PQEditPageProps) { } // PQ 데이터 조회 (pqCriterias와 답변) - const pqData = await getPQDataByVendorId(idAsNumber, pqSubmission.projectId || undefined); + const pqData = await getPQDataByVendorId( + idAsNumber, + pqSubmission.projectId || undefined, + pqSubmission.type as "GENERAL" | "PROJECT" | "NON_INSPECTION" + ); // 상태에 따른 읽기 전용 모드 결정 - const isReadOnly = [ "APPROVED"].includes(pqSubmission.status); + const isReadOnly = [ "QM_APPROVED"].includes(pqSubmission.status); const statusText = pqSubmission.status === "SUBMITTED" ? "제출됨" : pqSubmission.status === "APPROVED" ? "승인됨" : + pqSubmission.status === "QM_APPROVED" ? "최종 승인됨" : pqSubmission.status === "REJECTED" ? "거부됨" : "작성 중"; const pageTitle = pqSubmission.type === "PROJECT" diff --git a/app/[lng]/partners/(partners)/pq_new/page.tsx b/app/[lng]/partners/(partners)/pq_new/page.tsx index e72144c0..89a646a8 100644 --- a/app/[lng]/partners/(partners)/pq_new/page.tsx +++ b/app/[lng]/partners/(partners)/pq_new/page.tsx @@ -51,6 +51,12 @@ function getStatusBadge(status: string) { return <Badge variant="default">승인됨</Badge>; case "REJECTED": return <Badge variant="destructive">거부됨</Badge>; + case "QM_REVIEWING": + return <Badge variant="secondary">QM 검토 중</Badge>; + case "QM_APPROVED": + return <Badge variant="default">최종 승인됨</Badge>; + case "QM_REJECTED": + return <Badge variant="destructive">최종 거부됨</Badge>; default: return <Badge variant="outline">{status}</Badge>; } @@ -262,8 +268,8 @@ export default async function PQListPage({ params }: IndexPageProps) { </TableRow> ) : ( pqList.map((pq) => { - const canEdit = ["REQUESTED", "IN_PROGRESS", "REJECTED"].includes(pq.status); - const canView = ["SUBMITTED", "APPROVED"].includes(pq.status); + const canEdit = ["REQUESTED", "IN_PROGRESS", "REJECTED", "SAFETY_APPROVED", "QM_REVIEWING"].includes(pq.status); + const canView = ["SUBMITTED", "APPROVED", "QM_APPROVED", "SAFETY_REJECTED", "QM_REJECTED"].includes(pq.status); return ( <TableRow key={pq.id}> diff --git a/components/additional-info/join-form.tsx b/components/additional-info/join-form.tsx index fe5698d8..1642962f 100644 --- a/components/additional-info/join-form.tsx +++ b/components/additional-info/join-form.tsx @@ -223,6 +223,8 @@ export function InfoForm() { }) const isFormValid = form.formState.isValid + const watchedCountry = form.watch("country") + const isDomesticVendor = watchedCountry === "KR" // Field array for contacts const { fields: contactFields, append: addContact, remove: removeContact, replace: replaceContacts } = @@ -369,6 +371,29 @@ export function InfoForm() { fetchVendorData() }, [companyId, form, replaceContacts]) + // 도로명주소 검색 결과 수신 (내자 벤더만) + React.useEffect(() => { + if (!isDomesticVendor) return + + const handleMessage = (event: MessageEvent) => { + if (!event.data || event.data.type !== "JUSO_SELECTED") return + const { zipNo, roadAddrPart1, roadAddrPart2, addrDetail } = event.data.payload || {} + const road = [roadAddrPart1, roadAddrPart2].filter(Boolean).join(" ").trim() + + form.setValue("postalCode", zipNo || form.getValues("postalCode") || "", { shouldDirty: true }) + form.setValue("address", road || form.getValues("address") || "", { shouldDirty: true }) + form.setValue("addressDetail", addrDetail || form.getValues("addressDetail") || "", { shouldDirty: true }) + } + + window.addEventListener("message", handleMessage) + return () => window.removeEventListener("message", handleMessage) + }, [isDomesticVendor, form]) + + const handleJusoSearch = () => { + if (!isDomesticVendor) return + window.open("/api/juso", "jusoSearch", "width=570,height=420,scrollbars=yes,resizable=yes") + } + // 컴포넌트 언마운트 시 미리보기 URL 정리 (blob URL만) React.useEffect(() => { return () => { @@ -1224,11 +1249,29 @@ export function InfoForm() { name="address" render={({ field }) => ( <FormItem> - <FormLabel className="after:content-['*'] after:ml-0.5 after:text-red-500"> - 주소 - </FormLabel> + <div className="flex items-center justify-between gap-2"> + <FormLabel className="after:content-['*'] after:ml-0.5 after:text-red-500"> + 주소 + </FormLabel> + {isDomesticVendor && ( + <Button + type="button" + variant="secondary" + size="sm" + onClick={handleJusoSearch} + disabled={isSubmitting} + > + 주소 검색 + </Button> + )} + </div> <FormControl> - <Input {...field} disabled={isSubmitting} /> + <Input + {...field} + disabled={isSubmitting} + readOnly={isDomesticVendor} + className={cn(isDomesticVendor && "bg-muted text-muted-foreground")} + /> </FormControl> <FormMessage /> </FormItem> @@ -1258,7 +1301,13 @@ export function InfoForm() { <FormItem> <FormLabel>우편번호</FormLabel> <FormControl> - <Input {...field} disabled={isSubmitting} placeholder="우편번호를 입력해주세요" /> + <Input + {...field} + disabled={isSubmitting} + readOnly={isDomesticVendor} + className={cn(isDomesticVendor && "bg-muted text-muted-foreground")} + placeholder="우편번호를 입력해주세요" + /> </FormControl> <FormMessage /> </FormItem> diff --git a/components/pq-input/pq-input-tabs.tsx b/components/pq-input/pq-input-tabs.tsx index 6c9a1254..6ffd637a 100644 --- a/components/pq-input/pq-input-tabs.tsx +++ b/components/pq-input/pq-input-tabs.tsx @@ -651,8 +651,8 @@ export function PQInputTabs({ if (result.ok) { toast({ - title: "PQ Submitted", - description: "Your PQ information has been submitted successfully", + title: "PQ 제출 완료", + description: "PQ 정보가 성공적으로 제출되었습니다", }); // 제출 후 PQ 목록 페이지로 리디렉션 window.location.href = "/partners/pq_new"; diff --git a/lib/general-contracts/detail/general-contract-approval-request-dialog.tsx b/lib/general-contracts/detail/general-contract-approval-request-dialog.tsx index db0901cb..04054369 100644 --- a/lib/general-contracts/detail/general-contract-approval-request-dialog.tsx +++ b/lib/general-contracts/detail/general-contract-approval-request-dialog.tsx @@ -245,7 +245,10 @@ export function ContractApprovalRequestDialog({ if (contractData) { summary.basicInfo = { ...summary.basicInfo, - externalYardEntry: contractData.externalYardEntry || 'N' + externalYardEntry: contractData.externalYardEntry || 'N', + vendorCountry: (contractData as any)?.vendorCountry || summary.basicInfo.vendorCountry, + vendorName: (contractData as any)?.vendorName || summary.basicInfo.vendorName, + vendorCode: (contractData as any)?.vendorCode || summary.basicInfo.vendorCode, } } } catch { diff --git a/lib/general-contracts/utils.ts b/lib/general-contracts/utils.ts index 5bbb5980..1262dc4d 100644 --- a/lib/general-contracts/utils.ts +++ b/lib/general-contracts/utils.ts @@ -170,6 +170,51 @@ export function mapContractDataToTemplateVariables(contractSummary: ContractSumm ).join('\n') : '' + // PDFTron 템플릿 루프용 데이터 ({{#storageList}} ... {{/storageList}} 사용) + const storageList = storageItems.map(item => ({ + project: (item.projectName || item.projectCode || '').toString().trim(), + poNumber: (item.poNumber || '').toString().trim(), + hullNumber: (item.hullNumber || '').toString().trim(), + remainingAmount: formatCurrency(item.remainingAmount), + })) + + // 일반 견적 품목 루프용 데이터 ({{#itemsList}} ... {{/itemsList}} 사용) + const itemsList = (items || []).map((item, idx) => { + const quantityRaw = item.quantity ?? item.qty ?? '' + const unitPriceRaw = item.contractUnitPrice ?? item.unitPrice ?? '' + const amountRaw = + item.contractAmount ?? + (Number(quantityRaw) * Number(unitPriceRaw)) + + const quantityNum = Number(quantityRaw) + const hasQuantity = !isNaN(quantityNum) + const unitPriceNum = Number(unitPriceRaw) + const hasUnitPrice = !isNaN(unitPriceNum) + const hasAmountCalc = !isNaN(amountRaw as number) + + const amount = hasAmountCalc ? formatCurrency(amountRaw) : '' + + return { + no: idx + 1, // NO + hullNumber: (item.projectCode || basicInfo.projectCode || '').toString().trim(), // 호선번호 + shipType: (item.projectName || basicInfo.projectName || '').toString().trim(), // 선종/선형 + exportCountry: (basicInfo.vendorCountry || basicInfo.country || '').toString().trim(), // 수출국 + itemName: (item.itemInfo || item.description || item.itemCode || '').toString().trim(), // 품목 + unit: (item.quantityUnit || '').toString().trim(), // 단위 + unitPrice: hasUnitPrice ? formatCurrency(unitPriceNum) : formatCurrency(unitPriceRaw), // 단가 + amount, // 금액 + remark: (item.remark || item.remarks || item.note || '').toString().trim(), // 비고 + // 보존용 기존 필드 + itemCode: (item.itemCode || item.itemInfo || '').toString().trim(), + quantity: hasQuantity ? quantityNum : (quantityRaw ?? ''), + } + }) + + // 루프 미지원 템플릿을 위한 품목 텍스트 fallback + const itemsTableText = itemsList.length > 0 + ? itemsList.map(i => `${i.no}. ${i.hullNumber || '-'} / ${i.shipType || '-'} / ${i.exportCountry || '-'} / ${i.itemName || '-'} / 단위:${i.unit || '-'} / 단가:${i.unitPrice || '-'} / 금액:${i.amount || '-'} / 비고:${i.remark || '-'}`).join('\n') + : '' + // ═══════════════════════════════════════════════════════════════ // 변수 매핑 시작 @@ -296,7 +341,13 @@ export function mapContractDataToTemplateVariables(contractSummary: ContractSumm // ---------------------------------- storageTableText: storageTableText, // {{storageTableText}} (fallback) // PDFTron에서 배열을 받아 테이블 루프를 돌릴 수 있다면 아래 키를 사용 - storageList: storageItems, + storageList, + + // ---------------------------------- + // 일반 견적 품목 루프 (템플릿 표에 {{#itemsList}} 사용) + // ---------------------------------- + itemsList, + itemsTableText, } // 3. 모든 키를 순회하며 undefined나 null을 빈 문자열로 변환 (안전장치) @@ -306,5 +357,13 @@ export function mapContractDataToTemplateVariables(contractSummary: ContractSumm } }) - return variables + // 4. PDF 템플릿에서 추출한 {{ }} 변수명이 공백을 포함할 수 있어 trim 처리 후 매핑 + const normalizedVariables: Record<string, any> = {} + Object.entries(variables).forEach(([key, value]) => { + const trimmedKey = key.trim() + const trimmedValue = typeof value === 'string' ? value.trim() : value + normalizedVariables[trimmedKey] = trimmedValue + }) + + return normalizedVariables } diff --git a/lib/pq/service.ts b/lib/pq/service.ts index 15e71c4d..ea8a389c 100644 --- a/lib/pq/service.ts +++ b/lib/pq/service.ts @@ -2641,7 +2641,7 @@ export async function approvePQAction({ await db .update(vendorPQSubmissions) .set({ - status: "APPROVED", + status: "QM_APPROVED", approvedAt: currentDate, updatedAt: currentDate, }) diff --git a/lib/rfq-last/approval-actions.ts b/lib/rfq-last/approval-actions.ts index be435931..2f9d0843 100644 --- a/lib/rfq-last/approval-actions.ts +++ b/lib/rfq-last/approval-actions.ts @@ -8,6 +8,7 @@ import { ApprovalSubmissionSaga } from '@/lib/approval'; import { mapRfqSendToTemplateVariables } from './approval-handlers'; +import { prepareEmailAttachments } from './service'; interface RfqSendApprovalData { // RFQ 기본 정보 @@ -95,7 +96,27 @@ export async function requestRfqSendWithApproval(data: RfqSendApprovalData) { applicationReason: data.applicationReason, }); - // 3. 결재 상신용 payload 구성 + // 3. Knox 상신용 첨부파일 준비 (실제 파일 객체로 변환) + const emailAttachments = await prepareEmailAttachments(data.rfqId, data.attachmentIds); + type PreparedAttachment = { + filename?: string | null; + content: BlobPart; + contentType?: string | null; + }; + const knoxAttachments = (emailAttachments as PreparedAttachment[]) + .filter((att): att is PreparedAttachment => Boolean(att && att.content)) + .map( + (att) => + new File([att.content], att.filename || 'attachment', { + type: att.contentType || 'application/octet-stream', + }) + ); + + if (knoxAttachments.length === 0) { + throw new Error('상신할 첨부파일을 준비하지 못했습니다.'); + } + + // 4. 결재 상신용 payload 구성 // ⚠️ cronjob 환경에서 실행되므로 currentUser 정보를 포함해야 함 const approvalPayload = { rfqId: data.rfqId, @@ -113,7 +134,7 @@ export async function requestRfqSendWithApproval(data: RfqSendApprovalData) { }, }; - // 4. Saga로 결재 상신 + // 5. Saga로 결재 상신 const saga = new ApprovalSubmissionSaga( 'rfq_send_with_attachments', // 핸들러 키 approvalPayload, // 결재 승인 후 실행될 데이터 @@ -128,6 +149,7 @@ export async function requestRfqSendWithApproval(data: RfqSendApprovalData) { epId: data.currentUser.epId, email: data.currentUser.email, }, + attachments: knoxAttachments, } ); diff --git a/lib/rfq-last/service.ts b/lib/rfq-last/service.ts index 68cfdac7..23f5f63a 100644 --- a/lib/rfq-last/service.ts +++ b/lib/rfq-last/service.ts @@ -3349,7 +3349,7 @@ async function getProjectInfo(projectId: number) { return project; } -async function prepareEmailAttachments(rfqId: number, attachmentIds: number[]) { +export async function prepareEmailAttachments(rfqId: number, attachmentIds: number[]) { const attachments = await db .select({ attachment: rfqLastAttachments, diff --git a/lib/rfq-last/vendor-response/editor/vendor-response-editor.tsx b/lib/rfq-last/vendor-response/editor/vendor-response-editor.tsx index 8c70b8dd..18fc5d50 100644 --- a/lib/rfq-last/vendor-response/editor/vendor-response-editor.tsx +++ b/lib/rfq-last/vendor-response/editor/vendor-response-editor.tsx @@ -9,6 +9,14 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com import { Button } from "@/components/ui/button" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Badge } from "@/components/ui/badge" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" import { toast } from "sonner" import RfqInfoHeader from "./rfq-info-header" import CommercialTermsForm from "./commercial-terms-form" @@ -130,6 +138,7 @@ export default function VendorResponseEditor({ const [deletedAttachments, setDeletedAttachments] = useState<any[]>([]) const [uploadProgress, setUploadProgress] = useState(0) // 추가 const [currencyDecimalPlaces, setCurrencyDecimalPlaces] = useState<number>(2) // 통화별 소수점 자리수 + const [confirmOpen, setConfirmOpen] = useState(false) console.log(existingResponse,"existingResponse") @@ -682,7 +691,7 @@ export default function VendorResponseEditor({ <Button type="button" variant="default" - onClick={() => handleFormSubmit(true)} // 직접 핸들러 호출 + onClick={() => setConfirmOpen(true)} // 제출 전 확인 다이얼로그 disabled={loading || !allContractsSigned || isSubmitted || activeTab !== 'attachments'} > {!allContractsSigned ? ( @@ -714,6 +723,39 @@ export default function VendorResponseEditor({ </Button> </div> + {/* 최종 제출 확인 다이얼로그 */} + <Dialog open={confirmOpen} onOpenChange={setConfirmOpen}> + <DialogContent> + <DialogHeader> + <DialogTitle>최종 제출</DialogTitle> + <DialogDescription> + 최종 제출하시겠습니까? + </DialogDescription> + </DialogHeader> + <DialogFooter className="flex gap-2 sm:justify-end"> + <Button + type="button" + variant="outline" + onClick={() => setConfirmOpen(false)} + disabled={loading} + > + 취소 + </Button> + <Button + type="button" + variant="default" + onClick={() => { + setConfirmOpen(false) + handleFormSubmit(true) + }} + disabled={loading || !allContractsSigned || isSubmitted || activeTab !== 'attachments'} + > + 제출하기 + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + </div> </form> </FormProvider> diff --git a/lib/techsales-rfq/approval-actions.ts b/lib/techsales-rfq/approval-actions.ts index cf914592..c85a950f 100644 --- a/lib/techsales-rfq/approval-actions.ts +++ b/lib/techsales-rfq/approval-actions.ts @@ -97,7 +97,13 @@ export async function requestTechSalesRfqSendWithApproval(data: TechSalesRfqSend applicationReason: data.applicationReason, }); - // 5. 결재 상신용 payload 구성 + // 5. Knox 상신용 첨부파일 준비 + const knoxAttachments = await prepareKnoxDrmAttachments(data.drmAttachmentIds); + if (knoxAttachments.length === 0) { + throw new Error('상신할 DRM 첨부파일을 준비하지 못했습니다.'); + } + + // 6. 결재 상신용 payload 구성 const approvalPayload = { rfqId: data.rfqId, rfqCode: data.rfqCode, @@ -112,7 +118,7 @@ export async function requestTechSalesRfqSendWithApproval(data: TechSalesRfqSend }, }; - // 6. Saga로 결재 상신 + // 7. Saga로 결재 상신 const saga = new ApprovalSubmissionSaga( 'tech_sales_rfq_send_with_drm', // 핸들러 키 approvalPayload, // 결재 승인 후 실행될 데이터 @@ -127,6 +133,7 @@ export async function requestTechSalesRfqSendWithApproval(data: TechSalesRfqSend epId: data.currentUser.epId, email: data.currentUser.email, }, + attachments: knoxAttachments, } ); @@ -171,6 +178,59 @@ function getTechSalesRevalidationPath(rfqType: "SHIP" | "TOP" | "HULL"): string return "/evcp/budgetary-tech-sales-ship"; } } + +/** + * Knox 상신용 DRM 첨부파일을 File 객체로 준비 + */ +async function prepareKnoxDrmAttachments(attachmentIds: number[]): Promise<File[]> { + if (!attachmentIds || attachmentIds.length === 0) return []; + + const db = (await import('@/db/db')).default; + const { techSalesAttachments } = await import('@/db/schema/techSales'); + const { inArray } = await import('drizzle-orm'); + + const attachments = await db.query.techSalesAttachments.findMany({ + where: inArray(techSalesAttachments.id, attachmentIds), + columns: { + id: true, + filePath: true, + originalFileName: true, + fileName: true, + fileType: true, + }, + }); + + const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || process.env.NEXT_PUBLIC_URL; + const files: File[] = []; + + for (const attachment of attachments) { + if (!attachment.filePath || !baseUrl) { + console.error('[TechSales RFQ Approval] 첨부파일 경로나 BASE_URL이 없습니다.', attachment.id); + continue; + } + + const fileUrl = `${baseUrl}${attachment.filePath}`; + const response = await fetch(fileUrl); + + if (!response.ok) { + console.error(`[TechSales RFQ Approval] 첨부파일 다운로드 실패: ${fileUrl} (status: ${response.status})`); + continue; + } + + const blob = await response.blob(); + const file = new File( + [blob], + attachment.originalFileName || attachment.fileName || 'attachment', + { + type: attachment.fileType || blob.type || 'application/octet-stream', + } + ); + + files.push(file); + } + + return files; +} /** * 기술영업 RFQ DRM 첨부 해제 결재 상신 * @@ -214,6 +274,12 @@ export async function requestRfqResendWithDrmApproval(data: { applicationReason: data.applicationReason, }); + // DRM 첨부파일을 Knox 상신용 File 객체로 준비 + const knoxAttachments = await prepareKnoxDrmAttachments(data.drmAttachmentIds); + if (knoxAttachments.length === 0) { + throw new Error('상신할 DRM 첨부파일을 준비하지 못했습니다.'); + } + // 결재 payload 구성 const approvalPayload = { rfqId: data.rfqId, @@ -244,6 +310,7 @@ export async function requestRfqResendWithDrmApproval(data: { epId: data.currentUser.epId, email: data.currentUser.email, }, + attachments: knoxAttachments, } ); diff --git a/middleware.ts b/middleware.ts index 2ff8408e..493c9adc 100644 --- a/middleware.ts +++ b/middleware.ts @@ -213,6 +213,15 @@ export async function middleware(request: NextRequest) { } const { pathname, searchParams, origin } = request.nextUrl; + + /** + * 2-1. TrustNet 등 정적 경로는 언어 경로 추가하지 않고 그대로 통과 (최우선 처리) + * TrustNet은 Tomcat 서버에서 처리되므로 Next.js middleware가 개입하지 않음 + */ + if (pathname.startsWith('/trustnet')) { + console.log('[Middleware] TrustNet 경로 감지, 통과:', pathname, 'query:', searchParams.toString()); + return NextResponse.next(); + } /** * 3. "/" 경로로 들어온 경우 -> "/{lng}"로 리다이렉트 |
