summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/[lng]/evcp/(evcp)/(procurement)/pq_new/[vendorId]/[submissionId]/page.tsx6
-rw-r--r--app/[lng]/partners/(partners)/pq_new/[id]/page.tsx35
-rw-r--r--app/[lng]/partners/(partners)/pq_new/page.tsx10
-rw-r--r--components/additional-info/join-form.tsx59
-rw-r--r--components/pq-input/pq-input-tabs.tsx4
-rw-r--r--lib/general-contracts/detail/general-contract-approval-request-dialog.tsx5
-rw-r--r--lib/general-contracts/utils.ts63
-rw-r--r--lib/pq/service.ts2
-rw-r--r--lib/rfq-last/approval-actions.ts26
-rw-r--r--lib/rfq-last/service.ts2
-rw-r--r--lib/rfq-last/vendor-response/editor/vendor-response-editor.tsx44
-rw-r--r--lib/techsales-rfq/approval-actions.ts71
-rw-r--r--middleware.ts9
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}"로 리다이렉트