summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-08-19 09:23:47 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-08-19 09:23:47 +0000
commit8077419e40368dc703f94d558fc746b73fbc6702 (patch)
tree333bdfb3b0d84336f1bf7d4f0f1bbced6bec2d4c
parentaa71f75ace013b2fe982e5a104e61440458e0fd2 (diff)
(최겸) 구매 PQ 비밀유지계약서 별첨 첨부파일 추가, 정규업체등록관리 개발
-rw-r--r--app/[lng]/partners/(partners)/registration-status/page.tsx24
-rw-r--r--components/additional-info/join-form.tsx410
-rw-r--r--components/vendor-regular-registrations/additional-info-dialog.tsx122
-rw-r--r--lib/risk-management/table/risks-mail-dialog.tsx10
-rw-r--r--lib/vendor-registration-status/vendor-registration-status-view.tsx300
-rw-r--r--lib/vendor-regular-registrations/safety-qualification-update-sheet.tsx143
-rw-r--r--lib/vendor-regular-registrations/service.ts62
-rw-r--r--lib/vendor-regular-registrations/table/major-items-update-dialog.tsx (renamed from lib/vendor-regular-registrations/major-items-update-sheet.tsx)38
-rw-r--r--lib/vendor-regular-registrations/table/safety-qualification-update-dialog.tsx (renamed from lib/vendor-regular-registrations/table/safety-qualification-update-sheet.tsx)36
-rw-r--r--lib/vendor-regular-registrations/table/vendor-regular-registrations-table-columns.tsx22
-rw-r--r--lib/vendors/service.ts111
-rw-r--r--lib/vendors/table/request-pq-dialog.tsx148
12 files changed, 776 insertions, 650 deletions
diff --git a/app/[lng]/partners/(partners)/registration-status/page.tsx b/app/[lng]/partners/(partners)/registration-status/page.tsx
deleted file mode 100644
index 21bcea59..00000000
--- a/app/[lng]/partners/(partners)/registration-status/page.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import * as React from "react"
-import { Suspense } from "react"
-
-import { Skeleton } from "@/components/ui/skeleton"
-import { Shell } from "@/components/shell"
-import { VendorRegistrationStatusView } from "@/lib/vendor-registration-status/vendor-registration-status-view"
-
-export default async function VendorRegistrationStatusPage() {
- return (
- <Shell className="gap-4">
- <Suspense
- fallback={
- <div className="space-y-4">
- <Skeleton className="h-10 w-full" />
- <Skeleton className="h-64 w-full" />
- <Skeleton className="h-32 w-full" />
- </div>
- }
- >
- <VendorRegistrationStatusView />
- </Suspense>
- </Shell>
- )
-}
diff --git a/components/additional-info/join-form.tsx b/components/additional-info/join-form.tsx
index da2ddac7..d9f5052e 100644
--- a/components/additional-info/join-form.tsx
+++ b/components/additional-info/join-form.tsx
@@ -36,12 +36,16 @@ import {
CommandGroup,
CommandItem,
} from "@/components/ui/command"
-import { Check, ChevronsUpDown, Download, Loader2, Plus, X } from "lucide-react"
+import { Check, ChevronsUpDown, Download, Loader2, Plus, X, FileText, Eye, Upload, CheckCircle } from "lucide-react"
import { cn } from "@/lib/utils"
import { useTranslation } from "@/i18n/client"
import { getVendorDetailById, downloadVendorAttachments, updateVendorInfo } from "@/lib/vendors/service"
import { updateVendorSchema, updateVendorSchemaWithConditions, type UpdateVendorInfoSchema } from "@/lib/vendors/validations"
+import { fetchVendorRegistrationStatus } from "@/lib/vendor-regular-registrations/service"
+import { DocumentStatusDialog } from "@/components/vendor-regular-registrations/document-status-dialog"
+import { AdditionalInfoDialog } from "@/components/vendor-regular-registrations/additional-info-dialog"
+import type { VendorRegularRegistration } from "@/config/vendorRegularRegistrationsColumnsConfig"
import {
Select,
SelectContent,
@@ -124,6 +128,17 @@ const cashFlowRatingScaleMap: Record<string, string[]> = {
const MAX_FILE_SIZE = 3e9
+// 첨부파일 타입 정의
+const ATTACHMENT_TYPES = [
+ { value: "BUSINESS_REGISTRATION", label: "사업자등록증" },
+ { value: "CREDIT_REPORT", label: "신용평가보고서" },
+ { value: "BANK_ACCOUNT_COPY", label: "통장사본" },
+ { value: "ISO_CERTIFICATION", label: "ISO인증서" },
+ { value: "GENERAL", label: "일반 문서" },
+] as const
+
+type AttachmentType = typeof ATTACHMENT_TYPES[number]['value']
+
// 파일 타입 정의
interface AttachmentFile {
id: number
@@ -151,12 +166,25 @@ export function InfoForm() {
const [existingFiles, setExistingFiles] = React.useState<AttachmentFile[]>([])
const [existingCreditFiles, setExistingCreditFiles] = React.useState<AttachmentFile[]>([])
const [existingCashFlowFiles, setExistingCashFlowFiles] = React.useState<AttachmentFile[]>([])
+ const [existingSignatureFiles, setExistingSignatureFiles] = React.useState<AttachmentFile[]>([])
const [selectedFiles, setSelectedFiles] = React.useState<File[]>([])
const [creditRatingFile, setCreditRatingFile] = React.useState<File[]>([])
const [cashFlowRatingFile, setCashFlowRatingFile] = React.useState<File[]>([])
const [isDownloading, setIsDownloading] = React.useState(false);
+ // 정규등록 관련 상태
+ const [registrationData, setRegistrationData] = React.useState<any>(null)
+ const [documentDialogOpen, setDocumentDialogOpen] = React.useState(false)
+ const [additionalInfoDialogOpen, setAdditionalInfoDialogOpen] = React.useState(false)
+
+ // 첨부파일 타입 선택 상태
+ const [selectedAttachmentType, setSelectedAttachmentType] = React.useState<AttachmentType>("GENERAL")
+
+ // 서명/직인 업로드 관련 상태
+ const [signatureFiles, setSignatureFiles] = React.useState<File[]>([])
+ const [hasSignature, setHasSignature] = React.useState(false)
+
// React Hook Form
const form = useForm<UpdateVendorInfoSchema>({
resolver: zodResolver(updateVendorSchemaWithConditions),
@@ -223,19 +251,34 @@ export function InfoForm() {
// 첨부파일 정보 분류 (view에서 이미 파싱된 attachments 배열 사용)
if (vendorData.attachments && Array.isArray(vendorData.attachments)) {
- const generalFiles = vendorData.attachments.filter(
- (file: AttachmentFile) => file.attachmentType === "GENERAL"
- )
+ // 신용평가 관련 파일들만 따로 분리
const creditFiles = vendorData.attachments.filter(
(file: AttachmentFile) => file.attachmentType === "CREDIT_RATING"
)
const cashFlowFiles = vendorData.attachments.filter(
(file: AttachmentFile) => file.attachmentType === "CASH_FLOW_RATING"
)
+
+ // 서명/직인 파일들 분리
+ const signatureFiles = vendorData.attachments.filter(
+ (file: AttachmentFile) =>
+ file.attachmentType === "SIGNATURE" ||
+ file.attachmentType === "SEAL"
+ )
+
+ // 나머지 모든 파일들 (사업자등록증, 신용평가보고서, 통장사본, ISO인증서, 일반문서 등)
+ const otherFiles = vendorData.attachments.filter(
+ (file: AttachmentFile) =>
+ file.attachmentType !== "CREDIT_RATING" &&
+ file.attachmentType !== "CASH_FLOW_RATING" &&
+ file.attachmentType !== "SIGNATURE" &&
+ file.attachmentType !== "SEAL"
+ )
- setExistingFiles(generalFiles)
+ setExistingFiles(otherFiles) // 모든 기타 파일들을 일반 첨부파일 섹션에 표시
setExistingCreditFiles(creditFiles)
setExistingCashFlowFiles(cashFlowFiles)
+ setExistingSignatureFiles(signatureFiles) // 서명/직인 파일들
}
// 폼 기본값 설정 (연락처 포함)
@@ -272,6 +315,25 @@ export function InfoForm() {
replaceContacts(formattedContacts)
}
+
+ // 정규등록 상태 데이터 로드 (없는 경우 에러가 아님)
+ try {
+ const registrationResult = await fetchVendorRegistrationStatus(Number(companyId))
+ if (registrationResult.success) {
+ setRegistrationData(registrationResult.data)
+ } else if (registrationResult.noRegistration) {
+ // 정규등록 데이터가 없는 경우는 정상적인 상황 (기존 정규업체 등)
+ console.log("정규등록 데이터 없음 - 기존 정규업체이거나 아직 등록 진행하지 않음")
+ setRegistrationData(null)
+ } else {
+ // 실제 에러인 경우
+ console.error("정규등록 상태 조회 오류:", registrationResult.error)
+ setRegistrationData(null)
+ }
+ } catch (error) {
+ console.error("정규등록 상태 조회 중 예외 발생:", error)
+ setRegistrationData(null)
+ }
} catch (error) {
console.error("Error fetching vendor data:", error)
toast({
@@ -465,6 +527,7 @@ export function InfoForm() {
setExistingFiles(existingFiles.filter(file => file.id !== fileId))
setExistingCreditFiles(existingCreditFiles.filter(file => file.id !== fileId))
setExistingCashFlowFiles(existingCashFlowFiles.filter(file => file.id !== fileId))
+ setExistingSignatureFiles(existingSignatureFiles.filter(file => file.id !== fileId))
toast({
title: "파일 삭제 표시됨",
@@ -472,6 +535,82 @@ export function InfoForm() {
})
}
+
+
+ // 서명/직인 업로드 핸들러들 (한 개만 허용)
+ const handleSignatureDropAccepted = (acceptedFiles: File[]) => {
+ // 첫 번째 파일만 사용 (한 개만 허용)
+ const newFile = acceptedFiles[0]
+ if (newFile) {
+ // 기존 서명/직인 파일이 있으면 삭제 목록에 추가
+ if (existingSignatureFiles.length > 0) {
+ const existingFileId = existingSignatureFiles[0].id
+ setFilesToDelete([...filesToDelete, existingFileId])
+ setExistingSignatureFiles([]) // UI에서 제거
+
+ toast({
+ title: "서명/직인 교체",
+ description: "기존 서명/직인이 새 파일로 교체됩니다.",
+ })
+ }
+
+ setSignatureFiles([newFile]) // 새 파일 설정
+ setHasSignature(true)
+
+ if (acceptedFiles.length > 1) {
+ toast({
+ title: "파일 제한",
+ description: "서명/직인은 한 개만 등록할 수 있습니다. 첫 번째 파일만 선택되었습니다.",
+ })
+ }
+ }
+ }
+
+ const handleSignatureDropRejected = (fileRejections: any[]) => {
+ fileRejections.forEach((rej) => {
+ toast({
+ variant: "destructive",
+ title: "파일 오류",
+ description: `${rej.file.name}: ${rej.errors[0]?.message || "업로드 실패"}`,
+ })
+ })
+ }
+
+ const removeSignatureFile = () => {
+ setSignatureFiles([])
+ setHasSignature(false)
+ }
+
+ // 파일 타입 라벨 가져오기
+ const getAttachmentTypeLabel = (type: string) => {
+ const attachmentType = ATTACHMENT_TYPES.find(t => t.value === type)
+ return attachmentType?.label || type
+ }
+
+ const handleAdditionalInfoSave = async () => {
+ // 데이터 새로고침
+ try {
+ const registrationResult = await fetchVendorRegistrationStatus(Number(companyId))
+ if (registrationResult.success) {
+ setRegistrationData(registrationResult.data)
+ toast({
+ title: "데이터 새로고침",
+ description: "등록 현황 데이터가 새로고침되었습니다.",
+ })
+ } else if (registrationResult.noRegistration) {
+ // 정규등록 데이터가 없는 경우는 정상적인 상황
+ setRegistrationData(null)
+ } else {
+ // 실제 에러인 경우
+ console.error("정규등록 상태 새로고침 오류:", registrationResult.error)
+ setRegistrationData(null)
+ }
+ } catch (error) {
+ console.error("정규등록 상태 새로고침 중 예외 발생:", error)
+ setRegistrationData(null)
+ }
+ }
+
// Submit
async function onSubmit(values: UpdateVendorInfoSchema) {
if (!companyId) {
@@ -519,8 +658,10 @@ export function InfoForm() {
files: mainFiles,
creditRatingFiles,
cashFlowRatingFiles,
+ signatureFiles, // 서명/직인 파일들
contacts: values.contacts,
filesToDelete, // 삭제할 파일 ID 목록
+ selectedAttachmentType, // 선택된 첨부파일 타입
})
if (!result.error) {
@@ -606,6 +747,172 @@ export function InfoForm() {
<Separator />
+ {/* 정규업체 등록 현황 섹션 */}
+ {registrationData ? (
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ <FileText className="w-5 h-5" />
+ 정규업체 등록 현황
+ </CardTitle>
+ <CardDescription>
+ 정규업체 등록을 위한 현황을 확인하고 관리하세요.
+ </CardDescription>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ {/* 현재 상태 표시 */}
+ {registrationData.registration && (
+ <div className="flex items-center gap-2 p-3 bg-muted/30 rounded-lg">
+ <Badge variant="secondary">
+ {registrationData.registration.status === 'under_review' && '검토중'}
+ {registrationData.registration.status === 'approval_ready' && '조건충족'}
+ {registrationData.registration.status === 'in_review' && '정규등록검토'}
+ {registrationData.registration.status === 'completed' && '등록완료'}
+ {registrationData.registration.status === 'pending_approval' && '장기미등록'}
+ </Badge>
+ </div>
+ )}
+
+ {/* 서명/직인 등록 */}
+ <div className="space-y-2">
+ <h4 className="font-medium text-sm">회사 서명/직인 등록</h4>
+
+ {/* 현재 등록된 서명/직인 파일 표시 (한 개만) */}
+ {(existingSignatureFiles.length > 0 || signatureFiles.length > 0) && (
+ <div className="space-y-2">
+ <div className="flex items-center gap-2 p-2 border rounded-lg bg-green-50">
+ <CheckCircle className="w-4 h-4 text-green-600" />
+ <span className="text-sm text-green-800">서명/직인 등록됨</span>
+ </div>
+
+ {/* 기존 등록된 서명/직인 (첫 번째만 표시) */}
+ {existingSignatureFiles.length > 0 && signatureFiles.length === 0 && (
+ <div className="p-2 border rounded-lg">
+ {(() => {
+ const file = existingSignatureFiles[0];
+ const fileInfo = getFileInfo(file.fileName);
+ return (
+ <div className="flex items-center gap-2">
+ <FileListIcon />
+ <div className="flex-1">
+ <div className="text-xs font-medium">{fileInfo.icon} {file.fileName}</div>
+ <div className="text-xs text-muted-foreground">
+ {getAttachmentTypeLabel(file.attachmentType)} | {file.fileSize ? formatFileSize(file.fileSize) : '크기 정보 없음'}
+ </div>
+ </div>
+ <div className="flex items-center space-x-1">
+ <FileListAction
+ onClick={() => handleDownloadFile(file)}
+ disabled={isDownloading}
+ >
+ {isDownloading ? <Loader2 className="h-3 w-3 animate-spin" /> : <Download className="h-3 w-3" />}
+ </FileListAction>
+ <FileListAction onClick={() => handleDeleteExistingFile(file.id)}>
+ <X className="h-3 w-3" />
+ </FileListAction>
+ </div>
+ </div>
+ );
+ })()}
+ </div>
+ )}
+
+ {/* 새로 업로드된 서명/직인 */}
+ {signatureFiles.length > 0 && (
+ <div className="p-2 border rounded-lg bg-blue-50">
+ {(() => {
+ const file = signatureFiles[0];
+ return (
+ <div className="flex items-center gap-2">
+ <FileListIcon />
+ <div className="flex-1">
+ <div className="text-xs font-medium">{file.name}</div>
+ <div className="text-xs text-muted-foreground">
+ 서명/직인 (새 파일) | {prettyBytes(file.size)}
+ </div>
+ </div>
+ <FileListAction onClick={removeSignatureFile}>
+ <X className="h-3 w-3" />
+ </FileListAction>
+ </div>
+ );
+ })()}
+ </div>
+ )}
+ </div>
+ )}
+
+ {/* 서명/직인 업로드 드롭존 */}
+ <Dropzone
+ maxSize={MAX_FILE_SIZE}
+ onDropAccepted={handleSignatureDropAccepted}
+ onDropRejected={handleSignatureDropRejected}
+ disabled={isSubmitting}
+ >
+ {({ maxSize }) => (
+ <DropzoneZone className="flex justify-center min-h-[50px]">
+ <DropzoneInput />
+ <div className="flex items-center gap-2">
+ <Upload className="w-4 h-4" />
+ <div className="text-sm">
+ <DropzoneTitle>
+ {existingSignatureFiles.length > 0 || signatureFiles.length > 0
+ ? "서명/직인 교체"
+ : "서명/직인 업로드"
+ }
+ </DropzoneTitle>
+ <DropzoneDescription>
+ 한 개 파일만 업로드 가능 {maxSize ? ` | 최대: ${prettyBytes(maxSize)}` : ""}
+ </DropzoneDescription>
+ </div>
+ </div>
+ </DropzoneZone>
+ )}
+ </Dropzone>
+ </div>
+
+ {/* 액션 버튼들 */}
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
+ <Button
+ onClick={() => setDocumentDialogOpen(true)}
+ variant="outline"
+ size="sm"
+ >
+ <Eye className="w-4 h-4 mr-2" />
+ 문서 현황 확인
+ </Button>
+ <Button
+ onClick={() => setAdditionalInfoDialogOpen(true)}
+ variant={registrationData.additionalInfo ? "outline" : "default"}
+ size="sm"
+ >
+ <FileText className="w-4 h-4 mr-2" />
+ {registrationData.additionalInfo ? "추가정보 수정" : "추가정보 등록"}
+ </Button>
+ </div>
+ </CardContent>
+ </Card>
+ ) : (
+ // 정규업체 등록 데이터가 없는 경우 (기존 정규업체이거나 아직 등록 진행 안함)
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ <FileText className="w-5 h-5" />
+ 정규업체 등록 현황
+ </CardTitle>
+ <CardDescription>
+ 현재 정규업체 등록 진행 상황이 없습니다.
+ </CardDescription>
+ </CardHeader>
+ <CardContent>
+ <div className="text-center py-4 text-muted-foreground">
+ <p>이미 정규업체로 등록되어 있거나, 아직 정규업체 등록을 진행하지 않았습니다.</p>
+ <p className="text-sm mt-1">정규업체 등록이 필요한 경우 담당자에게 문의하세요.</p>
+ </div>
+ </CardContent>
+ </Card>
+ )}
+
{/* 첨부파일 요약 카드 - 기존 파일 있는 경우만 표시 */}
{(existingFiles.length > 0 || existingCreditFiles.length > 0 || existingCashFlowFiles.length > 0) && (
<Card>
@@ -619,8 +926,8 @@ export function InfoForm() {
<div className="grid gap-4">
{existingFiles.length > 0 && (
<div>
- <h4 className="font-medium mb-2">일반 첨부파일</h4>
- <ScrollArea className="h-32">
+ <h4 className="font-medium mb-2">첨부파일</h4>
+ <ScrollArea className="h-48">
<FileList className="gap-2">
{existingFiles.map((file) => {
const fileInfo = getFileInfo(file.fileName);
@@ -633,7 +940,7 @@ export function InfoForm() {
{fileInfo.icon} {file.fileName}
</FileListName>
<FileListDescription>
- {file.fileSize ? formatFileSize(file.fileSize) : '크기 정보 없음'}
+ {getAttachmentTypeLabel(file.attachmentType)} | {file.fileSize ? formatFileSize(file.fileSize) : '크기 정보 없음'}
</FileListDescription>
</FileListInfo>
<div className="flex items-center space-x-2">
@@ -659,7 +966,7 @@ export function InfoForm() {
{existingCreditFiles.length > 0 && (
<div>
<h4 className="font-medium mb-2">신용평가 첨부파일</h4>
- <ScrollArea className="h-32">
+ <ScrollArea className="h-24">
<FileList className="gap-2">
{existingCreditFiles.map((file) => {
const fileInfo = getFileInfo(file.fileName);
@@ -672,7 +979,7 @@ export function InfoForm() {
{fileInfo.icon} {file.fileName}
</FileListName>
<FileListDescription>
- {file.fileSize ? formatFileSize(file.fileSize) : '크기 정보 없음'}
+ {getAttachmentTypeLabel(file.attachmentType)} | {file.fileSize ? formatFileSize(file.fileSize) : '크기 정보 없음'}
</FileListDescription>
</FileListInfo>
<div className="flex items-center space-x-2">
@@ -698,7 +1005,7 @@ export function InfoForm() {
{existingCashFlowFiles.length > 0 && (
<div>
<h4 className="font-medium mb-2">현금흐름 첨부파일</h4>
- <ScrollArea className="h-32">
+ <ScrollArea className="h-24">
<FileList className="gap-2">
{existingCashFlowFiles.map((file) => {
const fileInfo = getFileInfo(file.fileName);
@@ -711,7 +1018,7 @@ export function InfoForm() {
{fileInfo.icon} {file.fileName}
</FileListName>
<FileListDescription>
- {file.fileSize ? formatFileSize(file.fileSize) : '크기 정보 없음'}
+ {getAttachmentTypeLabel(file.attachmentType)} | {file.fileSize ? formatFileSize(file.fileSize) : '크기 정보 없음'}
</FileListDescription>
</FileListInfo>
<div className="flex items-center space-x-2">
@@ -1383,13 +1690,33 @@ export function InfoForm() {
───────────────────────────────────────── */}
<div className="rounded-md border p-4 space-y-4">
<h4 className="text-md font-semibold">기타 첨부파일 추가</h4>
+
+ {/* 첨부파일 타입 선택 */}
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+ <div>
+ <label className="text-sm font-medium mb-2 block">파일 타입</label>
+ <Select value={selectedAttachmentType} onValueChange={(value: AttachmentType) => setSelectedAttachmentType(value)}>
+ <SelectTrigger>
+ <SelectValue placeholder="파일 타입 선택" />
+ </SelectTrigger>
+ <SelectContent>
+ {ATTACHMENT_TYPES.map((type) => (
+ <SelectItem key={type.value} value={type.value}>
+ {type.label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ </div>
+ </div>
+
<FormField
control={form.control}
name="attachedFiles"
render={() => (
<FormItem>
<FormLabel>
- 첨부 파일 (추가)
+ 첨부 파일 (추가) - {getAttachmentTypeLabel(selectedAttachmentType)}
</FormLabel>
<Dropzone
maxSize={MAX_FILE_SIZE}
@@ -1427,7 +1754,7 @@ export function InfoForm() {
<FileListInfo>
<FileListName>{file.name}</FileListName>
<FileListDescription>
- {prettyBytes(file.size)}
+ {getAttachmentTypeLabel(selectedAttachmentType)} | {prettyBytes(file.size)}
</FileListDescription>
</FileListInfo>
<FileListAction onClick={() => removeFile(i)}>
@@ -1459,6 +1786,61 @@ export function InfoForm() {
</div>
</form>
</Form>
+
+ {/* 정규등록 관련 다이얼로그들 - 정규등록 데이터가 있을 때만 표시 */}
+ {registrationData && (
+ <>
+ {/* 문서 현황 Dialog */}
+ <DocumentStatusDialog
+ open={documentDialogOpen}
+ onOpenChange={setDocumentDialogOpen}
+ registration={{
+ id: registrationData.registration?.id || 0,
+ vendorId: registrationData.vendor.id,
+ companyName: registrationData.vendor.vendorName,
+ businessNumber: registrationData.vendor.taxId,
+ representative: registrationData.vendor.representativeName || "",
+ country: registrationData.vendor.country || "KR",
+ potentialCode: registrationData.registration?.potentialCode || "",
+ status: registrationData.registration?.status || "under_review",
+ majorItems: "[]",
+ establishmentDate: registrationData.vendor.createdAt || new Date(),
+ registrationRequestDate: registrationData.registration?.registrationRequestDate,
+ assignedDepartment: registrationData.registration?.assignedDepartment,
+ assignedUser: registrationData.registration?.assignedUser,
+ remarks: registrationData.registration?.remarks,
+ safetyQualificationContent: registrationData.registration?.safetyQualificationContent || null,
+ gtcSkipped: registrationData.registration?.gtcSkipped || false,
+ additionalInfo: registrationData.additionalInfo,
+ documentSubmissions: registrationData.documentStatus,
+ contractAgreements: {
+ cp: "not_submitted",
+ gtc: "not_submitted",
+ standardSubcontract: "not_submitted",
+ safetyHealth: "not_submitted",
+ ethics: "not_submitted",
+ domesticCredit: "not_submitted",
+ },
+ basicContracts: registrationData.basicContracts || [],
+ documentFiles: {
+ businessRegistration: [],
+ creditEvaluation: [],
+ bankCopy: [],
+ auditResult: [],
+ },
+ } as VendorRegularRegistration}
+ onRefresh={handleAdditionalInfoSave}
+ />
+
+ {/* 추가정보 입력 Dialog */}
+ <AdditionalInfoDialog
+ open={additionalInfoDialogOpen}
+ onOpenChange={setAdditionalInfoDialogOpen}
+ vendorId={Number(companyId)}
+ onSave={handleAdditionalInfoSave}
+ />
+ </>
+ )}
</div>
</section>
</div>
diff --git a/components/vendor-regular-registrations/additional-info-dialog.tsx b/components/vendor-regular-registrations/additional-info-dialog.tsx
index fbd60515..84475877 100644
--- a/components/vendor-regular-registrations/additional-info-dialog.tsx
+++ b/components/vendor-regular-registrations/additional-info-dialog.tsx
@@ -16,13 +16,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
+
import {
Form,
FormControl,
@@ -54,12 +48,12 @@ const businessContactSchema = z.object({
// 추가정보 스키마
const additionalInfoSchema = z.object({
- businessType: z.string().optional(),
- industryType: z.string().optional(),
- companySize: z.string().optional(),
- revenue: z.string().optional(),
- factoryEstablishedDate: z.string().optional(),
- preferredContractTerms: z.string().optional(),
+ businessType: z.string().min(1, "사업유형은 필수입니다"),
+ industryType: z.string().min(1, "산업유형은 필수입니다"),
+ companySize: z.string().min(1, "기업규모는 필수입니다"),
+ revenue: z.string().min(1, "매출액은 필수입니다"),
+ factoryEstablishedDate: z.string().min(1, "공장설립일은 필수입니다"),
+ preferredContractTerms: z.string().min(1, "선호계약조건은 필수입니다"),
});
// 전체 폼 스키마
@@ -85,28 +79,7 @@ const contactTypes = [
{ value: "tax_invoice", label: "세금계산서", required: true },
];
-const businessTypes = [
- { value: "manufacturing", label: "제조업" },
- { value: "trading", label: "무역업" },
- { value: "service", label: "서비스업" },
- { value: "construction", label: "건설업" },
- { value: "other", label: "기타" },
-];
-const industryTypes = [
- { value: "shipbuilding", label: "조선업" },
- { value: "marine", label: "해양플랜트" },
- { value: "energy", label: "에너지" },
- { value: "automotive", label: "자동차" },
- { value: "other", label: "기타" },
-];
-
-const companySizes = [
- { value: "large", label: "대기업" },
- { value: "medium", label: "중견기업" },
- { value: "small", label: "중소기업" },
- { value: "startup", label: "스타트업" },
-];
export function AdditionalInfoDialog({
open,
@@ -169,16 +142,17 @@ export function AdditionalInfoDialog({
};
});
- // 추가정보 데이터 설정
+ // 추가정보 데이터 설정
+ const additionalInfoData = additionalInfo as any;
const additionalData = {
- businessType: additionalInfo?.businessType || "",
- industryType: additionalInfo?.industryType || "",
- companySize: additionalInfo?.companySize || "",
- revenue: additionalInfo?.revenue || "",
- factoryEstablishedDate: additionalInfo?.factoryEstablishedDate
- ? new Date(additionalInfo.factoryEstablishedDate).toISOString().split('T')[0]
+ businessType: additionalInfoData?.businessType || "",
+ industryType: additionalInfoData?.industryType || "",
+ companySize: additionalInfoData?.companySize || "",
+ revenue: additionalInfoData?.revenue || "",
+ factoryEstablishedDate: additionalInfoData?.factoryEstablishedDate
+ ? new Date(additionalInfoData.factoryEstablishedDate).toISOString().split('T')[0]
: "",
- preferredContractTerms: additionalInfo?.preferredContractTerms || "",
+ preferredContractTerms: additionalInfoData?.preferredContractTerms || "",
};
// 폼 데이터 업데이트
@@ -257,7 +231,6 @@ export function AdditionalInfoDialog({
<CardHeader className="pb-3">
<CardTitle className="text-lg flex items-center gap-2">
{contactType.label} 담당자
- <Badge variant="destructive" className="text-xs">필수</Badge>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
@@ -352,21 +325,10 @@ export function AdditionalInfoDialog({
name="additionalInfo.businessType"
render={({ field }) => (
<FormItem>
- <FormLabel>사업유형</FormLabel>
- <Select onValueChange={field.onChange} defaultValue={field.value}>
- <FormControl>
- <SelectTrigger>
- <SelectValue placeholder="사업유형 선택" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- {businessTypes.map((type) => (
- <SelectItem key={type.value} value={type.value}>
- {type.label}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
+ <FormLabel>사업유형 *</FormLabel>
+ <FormControl>
+ <Input placeholder="사업유형 입력" {...field} />
+ </FormControl>
<FormMessage />
</FormItem>
)}
@@ -376,21 +338,10 @@ export function AdditionalInfoDialog({
name="additionalInfo.industryType"
render={({ field }) => (
<FormItem>
- <FormLabel>산업유형</FormLabel>
- <Select onValueChange={field.onChange} defaultValue={field.value}>
- <FormControl>
- <SelectTrigger>
- <SelectValue placeholder="산업유형 선택" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- {industryTypes.map((type) => (
- <SelectItem key={type.value} value={type.value}>
- {type.label}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
+ <FormLabel>산업유형 *</FormLabel>
+ <FormControl>
+ <Input placeholder="산업유형 입력" {...field} />
+ </FormControl>
<FormMessage />
</FormItem>
)}
@@ -402,21 +353,10 @@ export function AdditionalInfoDialog({
name="additionalInfo.companySize"
render={({ field }) => (
<FormItem>
- <FormLabel>기업규모</FormLabel>
- <Select onValueChange={field.onChange} defaultValue={field.value}>
- <FormControl>
- <SelectTrigger>
- <SelectValue placeholder="기업규모 선택" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- {companySizes.map((size) => (
- <SelectItem key={size.value} value={size.value}>
- {size.label}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
+ <FormLabel>기업규모 *</FormLabel>
+ <FormControl>
+ <Input placeholder="기업규모 입력" {...field} />
+ </FormControl>
<FormMessage />
</FormItem>
)}
@@ -426,7 +366,7 @@ export function AdditionalInfoDialog({
name="additionalInfo.revenue"
render={({ field }) => (
<FormItem>
- <FormLabel>매출액 (억원)</FormLabel>
+ <FormLabel>매출액 (억원) *</FormLabel>
<FormControl>
<Input
placeholder="매출액 입력"
@@ -445,7 +385,7 @@ export function AdditionalInfoDialog({
name="additionalInfo.factoryEstablishedDate"
render={({ field }) => (
<FormItem>
- <FormLabel>공장설립일</FormLabel>
+ <FormLabel>공장설립일 *</FormLabel>
<FormControl>
<Input
placeholder="YYYY-MM-DD"
@@ -463,7 +403,7 @@ export function AdditionalInfoDialog({
name="additionalInfo.preferredContractTerms"
render={({ field }) => (
<FormItem>
- <FormLabel>선호계약조건</FormLabel>
+ <FormLabel>선호계약조건 *</FormLabel>
<FormControl>
<Textarea
placeholder="선호하는 계약조건을 상세히 입력해주세요"
diff --git a/lib/risk-management/table/risks-mail-dialog.tsx b/lib/risk-management/table/risks-mail-dialog.tsx
index 8bee1191..02c470ce 100644
--- a/lib/risk-management/table/risks-mail-dialog.tsx
+++ b/lib/risk-management/table/risks-mail-dialog.tsx
@@ -176,7 +176,7 @@ function RisksMailDialog(props: RisksMailDialogProps) {
setManagerList(managerList);
} catch (error) {
console.error('Error in Loading Risk Event for Managing:', error);
- toast.error(error instanceof Error ? error.message : '구매 담당자 목록을 불러오는 데 실패했어요.');
+ toast.error(error instanceof Error ? error.message : '구매 담당자 목록을 불러오는 데 실패했습니다.');
} finally {
setIsLoadingManagerList(false);
}
@@ -210,7 +210,7 @@ function RisksMailDialog(props: RisksMailDialogProps) {
const file = files[0];
const maxFileSize = 10 * 1024 * 1024
if (file.size > maxFileSize) {
- toast.error('파일 크기는 10MB를 초과할 수 없어요.');
+ toast.error('파일 크기는 10MB를 초과할 수 없습니다.');
return;
}
form.setValue('attachment', file);
@@ -272,15 +272,15 @@ function RisksMailDialog(props: RisksMailDialogProps) {
if (!res.ok) {
const errorData = await res.json();
- throw new Error(errorData.message || '리스크 알림 메일 전송에 실패했어요.');
+ throw new Error(errorData.message || '리스크 알림 메일 전송에 실패했습니다.');
}
- toast.success('리스크 알림 메일이 구매 담당자에게 발송되었어요.');
+ toast.success('리스크 알림 메일이 구매 담당자에게 발송되었습니다.');
onSuccess();
} catch (error) {
console.error('Error in Saving Risk Event:', error);
toast.error(
- error instanceof Error ? error.message : '리스크 알림 메일 발송 중 오류가 발생했어요.',
+ error instanceof Error ? error.message : '리스크 알림 메일 발송 중 오류가 발생했습니다.',
);
}
})
diff --git a/lib/vendor-registration-status/vendor-registration-status-view.tsx b/lib/vendor-registration-status/vendor-registration-status-view.tsx
deleted file mode 100644
index 850dd777..00000000
--- a/lib/vendor-registration-status/vendor-registration-status-view.tsx
+++ /dev/null
@@ -1,300 +0,0 @@
-"use client"
-
-import { useState, useEffect } from "react"
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
-import { Badge } from "@/components/ui/badge"
-import { Button } from "@/components/ui/button"
-
-import {
- CheckCircle,
- XCircle,
- FileText,
- AlertCircle,
- Eye,
- Upload
-} from "lucide-react"
-import { DocumentStatusDialog } from "@/components/vendor-regular-registrations/document-status-dialog"
-import { AdditionalInfoDialog } from "@/components/vendor-regular-registrations/additional-info-dialog"
-import { format } from "date-fns"
-import { toast } from "sonner"
-import { fetchVendorRegistrationStatus } from "@/lib/vendor-regular-registrations/service"
-
-// 세션에서 벤더아이디 가져오기 위한 훅
-import { useSession } from "next-auth/react"
-
-// 상태별 정의
-const statusConfig = {
- audit_pass: {
- label: "실사통과",
- color: "bg-blue-100 text-blue-800",
- description: "품질담당자(QM) 최종 의견에 따라 실사 통과로 결정된 상태"
- },
- cp_submitted: {
- label: "CP등록",
- color: "bg-green-100 text-green-800",
- description: "협력업체에서 실사 통과 후 기본계약문서에 대한 답변 제출/서약 완료한 상태"
- },
- cp_review: {
- label: "CP검토",
- color: "bg-yellow-100 text-yellow-800",
- description: "협력업체에서 제출한 CP/GTC에 대한 법무검토 의뢰한 상태"
- },
- cp_finished: {
- label: "CP완료",
- color: "bg-purple-100 text-purple-800",
- description: "CP 답변에 대한 법무검토 완료되어 정규업체 등록 가능한 상태"
- },
- approval_ready: {
- label: "조건충족",
- color: "bg-emerald-100 text-emerald-800",
- description: "정규업체 등록 문서/자료 접수현황에 누락이 없는 상태"
- },
- in_review: {
- label: "정규등록검토",
- color: "bg-orange-100 text-orange-800",
- description: "구매담당자 요청에 따라 정규업체 등록 관리자가 정규업체 등록 가능여부 검토"
- },
- pending_approval: {
- label: "장기미등록",
- color: "bg-red-100 text-red-800",
- description: "정규업체로 등록 요청되어 3개월 이내 정규업체 등록되지 않은 상태"
- }
-}
-
-// 필수문서 목록
-const requiredDocuments = [
- { key: "businessRegistration", label: "사업자등록증" },
- { key: "creditEvaluation", label: "신용평가서" },
- { key: "bankCopy", label: "통장사본" },
- { key: "cpDocument", label: "CP문서" },
- { key: "gtc", label: "GTC" },
- { key: "standardSubcontract", label: "표준하도급" },
- { key: "safetyHealth", label: "안전보건관리" },
- { key: "ethics", label: "윤리규범준수" },
- { key: "domesticCredit", label: "내국신용장" },
- { key: "safetyQualification", label: "안전적격성평가" },
-]
-
-export function VendorRegistrationStatusView() {
- const [additionalInfoDialogOpen, setAdditionalInfoDialogOpen] = useState(false)
- const [documentDialogOpen, setDocumentDialogOpen] = useState(false)
- const [hasSignature, setHasSignature] = useState(false)
- const [data, setData] = useState<any>(null)
- const [loading, setLoading] = useState(true)
-
- // 세션에서 vendorId 가져오기
- const { data: session, status: sessionStatus } = useSession()
- const vendorId = session?.user?.companyId
- console.log(vendorId)
-
- // 데이터 로드
- useEffect(() => {
- if (!vendorId) return
-
- const initialLoad = async () => {
- try {
- const result = await fetchVendorRegistrationStatus(vendorId)
- if (result.success) {
- setData(result.data)
- } else {
- toast.error(result.error)
- }
- } catch {
- toast.error("데이터 로드 중 오류가 발생했습니다.")
- } finally {
- setLoading(false)
- }
- }
-
- initialLoad()
- }, [vendorId])
-
- if (sessionStatus === "loading" || loading) {
- return <div className="p-8 text-center">로딩 중...</div>
- }
-
- if (!vendorId) {
- return <div className="p-8 text-center">벤더 정보가 없습니다. 다시 로그인 해주세요.</div>
- }
-
- if (!data) {
- return <div className="p-8 text-center">데이터를 불러올 수 없습니다.</div>
- }
-
- const currentStatusConfig = statusConfig[data.registration?.status as keyof typeof statusConfig] || statusConfig.audit_pass
-
- // 미완성 항목 계산
- const missingDocuments = requiredDocuments.filter(
- doc => !data.documentStatus[doc.key as keyof typeof data.documentStatus]
- )
-
- // Document Status Dialog에 전달할 registration 데이터 구성
- const registrationForDialog: any = {
- id: data.registration?.id || 0,
- vendorId: data.vendor.id,
- companyName: data.vendor.vendorName,
- businessNumber: data.vendor.taxId,
- representative: data.vendor.representativeName || "",
- country: data.vendor.country || "KR", // 기본값 KR
- potentialCode: data.registration?.potentialCode || "",
- status: data.registration?.status || "audit_pass",
- majorItems: "[]", // 빈 JSON 문자열
- establishmentDate: data.vendor.createdAt || new Date(),
- registrationRequestDate: data.registration?.registrationRequestDate,
- assignedDepartment: data.registration?.assignedDepartment,
- assignedDepartmentCode: data.registration?.assignedDepartmentCode,
- assignedUser: data.registration?.assignedUser,
- assignedUserCode: data.registration?.assignedUserCode,
- remarks: data.registration?.remarks,
- safetyQualificationContent: data.registration?.safetyQualificationContent || null,
- gtcSkipped: data.registration?.gtcSkipped || false,
- additionalInfo: data.additionalInfo,
- documentSubmissions: data.documentStatus, // documentSubmissions를 documentStatus로 설정
- contractAgreements: [],
- basicContracts: data.basicContracts || [], // 실제 데이터 사용
- documentSubmissionsStatus: data.documentStatus,
- contractAgreementsStatus: {
- cpDocument: data.documentStatus.cpDocument,
- gtc: data.documentStatus.gtc,
- standardSubcontract: data.documentStatus.standardSubcontract,
- safetyHealth: data.documentStatus.safetyHealth,
- ethics: data.documentStatus.ethics,
- domesticCredit: data.documentStatus.domesticCredit,
- },
- createdAt: data.registration?.createdAt || new Date(),
- updatedAt: data.registration?.updatedAt || new Date(),
- }
-
- const handleSignatureUpload = () => {
- // TODO: 서명/직인 업로드 기능 구현
- setHasSignature(true)
- toast.success("서명/직인이 등록되었습니다.")
- }
-
- const handleAdditionalInfoSave = () => {
- // 데이터 새로고침
- loadData()
- }
-
- const loadData = async () => {
- if (!vendorId) return
- try {
- const result = await fetchVendorRegistrationStatus(vendorId)
- if (result.success) {
- setData(result.data)
- toast.success("데이터가 새로고침되었습니다.")
- } else {
- toast.error(result.error)
- }
- } catch {
- toast.error("데이터 로드 중 오류가 발생했습니다.")
- }
- }
-
- return (
- <div className="space-y-6">
- {/* 헤더 섹션 */}
- <div className="space-y-4">
- <div className="flex items-center justify-between">
- <div>
- <h1 className="text-3xl font-bold">정규업체 등록관리 현황</h1>
- <p className="text-muted-foreground">
- {data.registration?.potentialCode || "미등록"} | {data.vendor.companyName}
- </p>
- <p className="text-sm text-muted-foreground mt-1">
- 정규업체 등록 진행현황을 확인하세요.
- </p>
- </div>
- <Badge className={currentStatusConfig.color} variant="secondary">
- {currentStatusConfig.label}
- </Badge>
- </div>
- </div>
-
- {/* 회사 서명/직인 등록 */}
- <Card>
- <CardHeader>
- <CardTitle className="flex items-center gap-2">
- <FileText className="w-5 h-5" />
- 회사 서명/직인 등록
- <Badge variant="destructive" className="text-xs">필수</Badge>
- </CardTitle>
- </CardHeader>
- <CardContent>
- {hasSignature ? (
- <div className="flex items-center gap-3 p-4 border rounded-lg bg-green-50">
- <CheckCircle className="w-5 h-5 text-green-600" />
- <span className="text-green-800">서명/직인이 등록되었습니다.</span>
- </div>
- ) : (
- <Button
- onClick={handleSignatureUpload}
- className="w-full h-20 border-2 border-dashed border-muted-foreground/25 bg-muted/25"
- variant="outline"
- >
- <div className="text-center">
- <Upload className="w-6 h-6 mx-auto mb-2" />
- <span>서명/직인 등록하기</span>
- </div>
- </Button>
- )}
- </CardContent>
- </Card>
-
- {/* 간소화된 액션 버튼들 */}
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
- <Button
- onClick={() => setDocumentDialogOpen(true)}
- variant="outline"
- size="lg"
- className="h-16 flex flex-col items-center gap-2"
- >
- <Eye className="w-6 h-6" />
- <span>문서 현황 확인</span>
- </Button>
- <Button
- onClick={() => setAdditionalInfoDialogOpen(true)}
- variant={data.additionalInfo ? "outline" : "default"}
- size="lg"
- className="h-16 flex flex-col items-center gap-2"
- >
- <FileText className="w-6 h-6" />
- <span>{data.additionalInfo ? "추가정보 수정" : "추가정보 등록"}</span>
- </Button>
- </div>
-
- {/* 상태 설명 */}
- <Card>
- <CardHeader>
- <CardTitle>현재 상태 안내</CardTitle>
- </CardHeader>
- <CardContent>
- <div className="flex items-start gap-3">
- <Badge className={currentStatusConfig.color} variant="secondary">
- {currentStatusConfig.label}
- </Badge>
- <p className="text-sm text-muted-foreground">
- {currentStatusConfig.description}
- </p>
- </div>
- </CardContent>
- </Card>
-
- {/* 문서 현황 Dialog */}
- <DocumentStatusDialog
- open={documentDialogOpen}
- onOpenChange={setDocumentDialogOpen}
- registration={registrationForDialog}
- onRefresh={loadData}
- />
-
- {/* 추가정보 입력 Dialog */}
- <AdditionalInfoDialog
- open={additionalInfoDialogOpen}
- onOpenChange={setAdditionalInfoDialogOpen}
- vendorId={vendorId}
- onSave={handleAdditionalInfoSave}
- />
- </div>
- )
-}
diff --git a/lib/vendor-regular-registrations/safety-qualification-update-sheet.tsx b/lib/vendor-regular-registrations/safety-qualification-update-sheet.tsx
deleted file mode 100644
index a93fbf22..00000000
--- a/lib/vendor-regular-registrations/safety-qualification-update-sheet.tsx
+++ /dev/null
@@ -1,143 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { useForm } from "react-hook-form"
-import { zodResolver } from "@hookform/resolvers/zod"
-import { z } from "zod"
-import { toast } from "sonner"
-
-import {
- Sheet,
- SheetContent,
- SheetDescription,
- SheetHeader,
- SheetTitle,
-} from "@/components/ui/sheet"
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form"
-import { Button } from "@/components/ui/button"
-import { Textarea } from "@/components/ui/textarea"
-import { updateSafetyQualification } from "./service"
-
-const formSchema = z.object({
- safetyQualificationContent: z.string().min(1, "안전적격성 평가 내용을 입력해주세요."),
-})
-
-interface SafetyQualificationUpdateSheetProps {
- open: boolean
- onOpenChange: (open: boolean) => void
- registrationId?: number
- vendorName?: string
- currentContent?: string | null
- onSuccess?: () => void
-}
-
-export function SafetyQualificationUpdateSheet({
- open,
- onOpenChange,
- registrationId,
- vendorName,
- currentContent,
- onSuccess,
-}: SafetyQualificationUpdateSheetProps) {
- const [isLoading, setIsLoading] = React.useState(false)
-
- const form = useForm<z.infer<typeof formSchema>>({
- resolver: zodResolver(formSchema),
- defaultValues: {
- safetyQualificationContent: currentContent || "",
- },
- })
-
- // 폼 값 초기화
- React.useEffect(() => {
- if (open) {
- form.reset({
- safetyQualificationContent: currentContent || "",
- })
- }
- }, [open, currentContent, form])
-
- async function onSubmit(values: z.infer<typeof formSchema>) {
- if (!registrationId) {
- toast.error("등록 ID가 없습니다.")
- return
- }
-
- setIsLoading(true)
- try {
- const result = await updateSafetyQualification(
- registrationId,
- values.safetyQualificationContent
- )
-
- if (result.success) {
- toast.success("안전적격성 평가가 등록되었습니다.")
- onOpenChange(false)
- onSuccess?.()
- } else {
- toast.error(result.error || "안전적격성 평가 등록에 실패했습니다.")
- }
- } catch (error) {
- console.error("안전적격성 평가 등록 오류:", error)
- toast.error("안전적격성 평가 등록 중 오류가 발생했습니다.")
- } finally {
- setIsLoading(false)
- }
- }
-
- return (
- <Sheet open={open} onOpenChange={onOpenChange}>
- <SheetContent className="w-[400px] sm:w-[540px]">
- <SheetHeader>
- <SheetTitle>안전적격성 평가 입력</SheetTitle>
- <SheetDescription>
- {vendorName && `${vendorName}의 `}안전적격성 평가 내용을 입력해주세요.
- </SheetDescription>
- </SheetHeader>
-
- <Form {...form}>
- <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6 mt-6">
- <FormField
- control={form.control}
- name="safetyQualificationContent"
- render={({ field }) => (
- <FormItem>
- <FormLabel>안전적격성 평가 내용</FormLabel>
- <FormControl>
- <Textarea
- placeholder="안전적격성 평가 결과 및 내용을 입력해주세요..."
- className="min-h-[200px]"
- {...field}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- <div className="flex justify-end space-x-2">
- <Button
- type="button"
- variant="outline"
- onClick={() => onOpenChange(false)}
- disabled={isLoading}
- >
- 취소
- </Button>
- <Button type="submit" disabled={isLoading}>
- {isLoading ? "저장 중..." : "저장"}
- </Button>
- </div>
- </form>
- </Form>
- </SheetContent>
- </Sheet>
- )
-}
diff --git a/lib/vendor-regular-registrations/service.ts b/lib/vendor-regular-registrations/service.ts
index 51f4e82b..d64c7b8b 100644
--- a/lib/vendor-regular-registrations/service.ts
+++ b/lib/vendor-regular-registrations/service.ts
@@ -25,7 +25,46 @@ import {
basicContractTemplates
} from "@/db/schema";
import db from "@/db/db";
-import { inArray, eq, desc } from "drizzle-orm";
+import { inArray, eq, desc, and, lt } from "drizzle-orm";
+
+// 3개월 이상 정규등록검토 상태인 등록을 장기미등록으로 변경
+async function updatePendingApprovals() {
+ try {
+ const threeMonthsAgo = new Date();
+ threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3);
+
+ // 3개월 이상 정규등록검토 상태인 등록들을 조회
+ const outdatedRegistrations = await db
+ .select()
+ .from(vendorRegularRegistrations)
+ .where(
+ and(
+ eq(vendorRegularRegistrations.status, "in_review"),
+ lt(vendorRegularRegistrations.updatedAt, threeMonthsAgo)
+ )
+ );
+
+ // 장기미등록으로 상태 변경
+ if (outdatedRegistrations.length > 0) {
+ await db
+ .update(vendorRegularRegistrations)
+ .set({
+ status: "pending_approval",
+ updatedAt: new Date(),
+ })
+ .where(
+ and(
+ eq(vendorRegularRegistrations.status, "in_review"),
+ lt(vendorRegularRegistrations.updatedAt, threeMonthsAgo)
+ )
+ );
+
+ console.log(`${outdatedRegistrations.length}개의 등록이 장기미등록으로 변경되었습니다.`);
+ }
+ } catch (error) {
+ console.error("장기미등록 상태 업데이트 오류:", error);
+ }
+}
// 캐싱과 에러 핸들링이 포함된 조회 함수
export async function fetchVendorRegularRegistrations(input?: {
@@ -37,6 +76,9 @@ export async function fetchVendorRegularRegistrations(input?: {
return unstable_cache(
async () => {
try {
+ // 장기미등록 상태 업데이트 실행
+ await updatePendingApprovals();
+
const registrations = await getVendorRegularRegistrations();
let filteredData = registrations;
@@ -113,6 +155,7 @@ export async function createVendorRegistration(data: {
const registration = await createVendorRegularRegistration({
...data,
+ status: data.status || "under_review", // 기본 상태를 '검토중'으로 설정
majorItems: majorItemsJson,
});
@@ -438,7 +481,7 @@ export async function skipLegalReview(vendorIds: number[], skipReason: string) {
// 새로 생성
const newRegistration = await createVendorRegularRegistration({
vendorId: vendorId,
- status: "cp_finished", // CP완료로 변경
+ status: "under_review", // 검토중으로 변경
remarks: `GTC Skip: ${skipReason}`,
});
registrationId = newRegistration.id;
@@ -451,7 +494,6 @@ export async function skipLegalReview(vendorIds: number[], skipReason: string) {
: `GTC Skip: ${skipReason}`;
await updateVendorRegularRegistration(registrationId, {
- status: "cp_finished", // CP완료로 변경
gtcSkipped: true, // GTC Skip 여부 설정
remarks: newRemarks,
});
@@ -630,7 +672,7 @@ export async function fetchVendorRegistrationStatus(vendorId: number) {
}
}
- // 정규업체 등록 정보
+ // 정규업체 등록 정보 (없을 수도 있음 - 기존 정규업체이거나 아직 등록 진행 안함)
const registration = await db
.select({
id: vendorRegularRegistrations.id,
@@ -653,6 +695,15 @@ export async function fetchVendorRegistrationStatus(vendorId: number) {
.where(eq(vendorRegularRegistrations.vendorId, vendorId))
.limit(1)
+ // 정규업체 등록 정보가 없는 경우 (정상적인 상황)
+ if (!registration[0]) {
+ return {
+ success: false,
+ error: "정규업체 등록 진행 정보가 없습니다.", // 에러가 아닌 정보성 메시지
+ noRegistration: true // 등록 정보가 없음을 명시적으로 표시
+ }
+ }
+
// 벤더 첨부파일 조회
const vendorFiles = await db
.select()
@@ -783,7 +834,8 @@ export async function fetchVendorRegistrationStatus(vendorId: number) {
missingDocuments,
businessContacts,
missingContactTypes,
- additionalInfo: additionalInfoCompleted, // boolean 값으로 변경
+ additionalInfo: additionalInfo[0] || null, // 실제 추가정보 데이터 반환
+ additionalInfoCompleted, // 완료 여부는 별도 필드로 추가
pqSubmission: pqSubmission[0] || null,
auditPassed: investigationFiles.length > 0,
basicContracts: vendorContracts, // 기본계약 정보 추가
diff --git a/lib/vendor-regular-registrations/major-items-update-sheet.tsx b/lib/vendor-regular-registrations/table/major-items-update-dialog.tsx
index ba125bbe..26741a1b 100644
--- a/lib/vendor-regular-registrations/major-items-update-sheet.tsx
+++ b/lib/vendor-regular-registrations/table/major-items-update-dialog.tsx
@@ -6,18 +6,18 @@ import { toast } from "sonner"
import { X, Plus, Search } from "lucide-react"
import {
- Sheet,
- SheetContent,
- SheetDescription,
- SheetHeader,
- SheetTitle,
-} from "@/components/ui/sheet"
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Badge } from "@/components/ui/badge"
import { Label } from "@/components/ui/label"
import { searchItemsForPQ } from "@/lib/items/service"
-import { updateMajorItems } from "./service"
+import { updateMajorItems } from "../service"
// PQ 대상 품목 타입 정의
interface PQItem {
@@ -25,7 +25,7 @@ interface PQItem {
itemName: string
}
-interface MajorItemsUpdateSheetProps {
+interface MajorItemsUpdateDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
registrationId?: number
@@ -34,14 +34,14 @@ interface MajorItemsUpdateSheetProps {
onSuccess?: () => void
}
-export function MajorItemsUpdateSheet({
+export function MajorItemsUpdateDialog({
open,
onOpenChange,
registrationId,
vendorName,
currentItems,
onSuccess,
-}: MajorItemsUpdateSheetProps) {
+}: MajorItemsUpdateDialogProps) {
const [isLoading, setIsLoading] = useState(false)
const [selectedItems, setSelectedItems] = useState<PQItem[]>([])
@@ -144,14 +144,14 @@ export function MajorItemsUpdateSheet({
}
return (
- <Sheet open={open} onOpenChange={onOpenChange}>
- <SheetContent className="w-[400px] sm:w-[540px]">
- <SheetHeader>
- <SheetTitle>주요품목 등록</SheetTitle>
- <SheetDescription>
+ <Dialog open={open} onOpenChange={onOpenChange}>
+ <DialogContent className="w-[400px] sm:w-[540px] max-h-[90vh] overflow-y-auto">
+ <DialogHeader>
+ <DialogTitle>주요품목 등록</DialogTitle>
+ <DialogDescription>
{vendorName && `${vendorName}의 `}주요품목을 등록해주세요.
- </SheetDescription>
- </SheetHeader>
+ </DialogDescription>
+ </DialogHeader>
<div className="space-y-6 mt-6">
{/* 선택된 아이템들 표시 */}
@@ -239,7 +239,7 @@ export function MajorItemsUpdateSheet({
{isLoading ? "저장 중..." : "저장"}
</Button>
</div>
- </SheetContent>
- </Sheet>
+ </DialogContent>
+ </Dialog>
)
}
diff --git a/lib/vendor-regular-registrations/table/safety-qualification-update-sheet.tsx b/lib/vendor-regular-registrations/table/safety-qualification-update-dialog.tsx
index c2aeba70..80084732 100644
--- a/lib/vendor-regular-registrations/table/safety-qualification-update-sheet.tsx
+++ b/lib/vendor-regular-registrations/table/safety-qualification-update-dialog.tsx
@@ -7,12 +7,12 @@ import { z } from "zod"
import { toast } from "sonner"
import {
- Sheet,
- SheetContent,
- SheetDescription,
- SheetHeader,
- SheetTitle,
-} from "@/components/ui/sheet"
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog"
import {
Form,
FormControl,
@@ -29,7 +29,7 @@ const formSchema = z.object({
safetyQualificationContent: z.string().min(1, "안전적격성 평가 내용을 입력해주세요."),
})
-interface SafetyQualificationUpdateSheetProps {
+interface SafetyQualificationUpdateDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
registrationId?: number
@@ -38,14 +38,14 @@ interface SafetyQualificationUpdateSheetProps {
onSuccess?: () => void
}
-export function SafetyQualificationUpdateSheet({
+export function SafetyQualificationUpdateDialog({
open,
onOpenChange,
registrationId,
vendorName,
currentContent,
onSuccess,
-}: SafetyQualificationUpdateSheetProps) {
+}: SafetyQualificationUpdateDialogProps) {
const [isLoading, setIsLoading] = React.useState(false)
const form = useForm<z.infer<typeof formSchema>>({
@@ -93,14 +93,14 @@ export function SafetyQualificationUpdateSheet({
}
return (
- <Sheet open={open} onOpenChange={onOpenChange}>
- <SheetContent className="w-[400px] sm:w-[540px]">
- <SheetHeader>
- <SheetTitle>안전적격성 평가 입력</SheetTitle>
- <SheetDescription>
+ <Dialog open={open} onOpenChange={onOpenChange}>
+ <DialogContent className="w-[400px] sm:w-[540px] max-h-[90vh] overflow-y-auto">
+ <DialogHeader>
+ <DialogTitle>안전적격성 평가 입력</DialogTitle>
+ <DialogDescription>
{vendorName && `${vendorName}의 `}안전적격성 평가 내용을 입력해주세요.
- </SheetDescription>
- </SheetHeader>
+ </DialogDescription>
+ </DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6 mt-6">
@@ -137,7 +137,7 @@ export function SafetyQualificationUpdateSheet({
</div>
</form>
</Form>
- </SheetContent>
- </Sheet>
+ </DialogContent>
+ </Dialog>
)
}
diff --git a/lib/vendor-regular-registrations/table/vendor-regular-registrations-table-columns.tsx b/lib/vendor-regular-registrations/table/vendor-regular-registrations-table-columns.tsx
index 765b0279..7446716b 100644
--- a/lib/vendor-regular-registrations/table/vendor-regular-registrations-table-columns.tsx
+++ b/lib/vendor-regular-registrations/table/vendor-regular-registrations-table-columns.tsx
@@ -13,29 +13,23 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge
import { Eye, FileText, Ellipsis, Shield, Package } from "lucide-react"
import { toast } from "sonner"
import { useState } from "react"
-import { SafetyQualificationUpdateSheet } from "./safety-qualification-update-sheet"
-import { MajorItemsUpdateSheet } from "../major-items-update-sheet"
+import { SafetyQualificationUpdateDialog } from "./safety-qualification-update-dialog"
+import { MajorItemsUpdateDialog } from "./major-items-update-dialog"
const statusLabels = {
- audit_pass: "실사통과",
- cp_submitted: "CP등록",
- cp_review: "CP검토",
- cp_finished: "CP완료",
+ under_review: "검토중",
approval_ready: "조건충족",
- registration_requested: "등록요청됨",
in_review: "정규등록검토",
+ completed: "등록완료",
pending_approval: "장기미등록",
}
const statusColors = {
- audit_pass: "bg-blue-100 text-blue-800",
- cp_submitted: "bg-green-100 text-green-800",
- cp_review: "bg-yellow-100 text-yellow-800",
- cp_finished: "bg-purple-100 text-purple-800",
+ under_review: "bg-blue-100 text-blue-800",
approval_ready: "bg-emerald-100 text-emerald-800",
- registration_requested: "bg-indigo-100 text-indigo-800",
in_review: "bg-orange-100 text-orange-800",
+ completed: "bg-green-100 text-green-800",
pending_approval: "bg-red-100 text-red-800",
}
@@ -295,7 +289,7 @@ export function getColumns(): ColumnDef<VendorRegularRegistration>[] {
</DropdownMenuContent>
</DropdownMenu>
- <SafetyQualificationUpdateSheet
+ <SafetyQualificationUpdateDialog
open={safetyQualificationSheetOpen}
onOpenChange={setSafetyQualificationSheetOpen}
registrationId={registration.id}
@@ -306,7 +300,7 @@ export function getColumns(): ColumnDef<VendorRegularRegistration>[] {
window.location.reload()
}}
/>
- <MajorItemsUpdateSheet
+ <MajorItemsUpdateDialog
open={majorItemsSheetOpen}
onOpenChange={setMajorItemsSheetOpen}
registrationId={registration.id}
diff --git a/lib/vendors/service.ts b/lib/vendors/service.ts
index 2a927069..9af81021 100644
--- a/lib/vendors/service.ts
+++ b/lib/vendors/service.ts
@@ -9,6 +9,8 @@ import crypto from 'crypto';
import fs from 'fs/promises';
import path from 'path';
import { v4 as uuidv4 } from 'uuid';
+import { saveDRMFile } from "@/lib/file-stroage";
+import { decryptWithServerAction } from "@/components/drm/drmUtils";
import { filterColumns } from "@/lib/filter-columns";
import { unstable_cache } from "@/lib/unstable-cache";
@@ -2002,6 +2004,7 @@ export async function getVendorDetailById(id: number) {
if (!vendor) {
return null;
}
+ console.log("vendor", vendor.attachments)
// JSON 문자열로 반환된 contacts와 attachments를 JavaScript 객체로 파싱
const contacts = typeof vendor.contacts === 'string'
@@ -2059,8 +2062,10 @@ export async function updateVendorInfo(params: {
files?: File[]
creditRatingFiles?: File[]
cashFlowRatingFiles?: File[]
+ signatureFiles?: File[] // 서명/직인 파일들
contacts: ContactInfo[]
filesToDelete?: number[] // 삭제할 파일 ID 목록
+ selectedAttachmentType?: string // 선택된 첨부파일 타입
}) {
try {
const {
@@ -2068,8 +2073,10 @@ export async function updateVendorInfo(params: {
files = [],
creditRatingFiles = [],
cashFlowRatingFiles = [],
+ signatureFiles = [],
contacts,
- filesToDelete = []
+ filesToDelete = [],
+ selectedAttachmentType = "GENERAL"
} = params
// 세션 및 권한 확인
@@ -2204,9 +2211,9 @@ export async function updateVendorInfo(params: {
}
// 4. 새 파일 저장 (제공된 storeVendorFiles 함수 활용)
- // 4-1. 일반 파일 저장
+ // 4-1. 일반 파일 저장 (선택된 타입 사용)
if (files.length > 0) {
- await storeVendorFiles(tx, vendorData.id, files, "GENERAL");
+ await storeVendorFiles(tx, vendorData.id, files, selectedAttachmentType);
}
// 4-2. 신용평가 파일 저장
@@ -2218,6 +2225,11 @@ export async function updateVendorInfo(params: {
if (cashFlowRatingFiles.length > 0) {
await storeVendorFiles(tx, vendorData.id, cashFlowRatingFiles, "CASH_FLOW_RATING");
}
+
+ // 4-4. 서명/직인 파일 저장
+ if (signatureFiles.length > 0) {
+ await storeVendorFiles(tx, vendorData.id, signatureFiles, "SIGNATURE");
+ }
})
// 캐시 무효화
@@ -2989,3 +3001,96 @@ export async function requestBasicContractInfo({
};
}
}
+
+/**
+ * 비밀유지 계약서 첨부파일 저장 서버 액션
+ */
+export async function saveNdaAttachments(input: {
+ vendorIds: number[];
+ files: File[];
+ userId: string;
+}) {
+ unstable_noStore();
+
+ try {
+ console.log("📎 비밀유지 계약서 첨부파일 저장 시작");
+ console.log(`벤더 수: ${input.vendorIds.length}, 파일 수: ${input.files.length}`);
+
+ const results = [];
+
+ for (const vendorId of input.vendorIds) {
+ for (const file of input.files) {
+ console.log(`📄 처리 중: 벤더 ID ${vendorId} - ${file.name}`);
+
+ try {
+ // saveDRMFile을 사용해서 파일 저장
+ const saveResult = await saveDRMFile(
+ file,
+ decryptWithServerAction,
+ `vendor-attachments/nda/${vendorId}`,
+ input.userId
+ );
+
+ if (!saveResult.success) {
+ throw new Error(`파일 저장 실패: ${file.name} - ${saveResult.error}`);
+ }
+
+ // vendor_attachments 테이블에 파일 정보 저장
+ const insertedAttachment = await db.insert(vendorAttachments).values({
+ vendorId: vendorId,
+ fileType: file.type || 'application/octet-stream',
+ fileName: saveResult.fileName || file.name,
+ filePath: saveResult.publicPath || '',
+ attachmentType: 'NDA_ATTACHMENT',
+ }).returning();
+
+ results.push({
+ vendorId,
+ fileName: file.name,
+ attachmentId: insertedAttachment[0]?.id || 0,
+ success: true
+ });
+
+ console.log(`✅ 완료: 벤더 ID ${vendorId} - ${file.name}`);
+
+ } catch (error) {
+ console.error(`❌ 실패: 벤더 ID ${vendorId} - ${file.name}`, error);
+ results.push({
+ vendorId,
+ fileName: file.name,
+ success: false,
+ error: error instanceof Error ? error.message : '알 수 없는 오류'
+ });
+ }
+ }
+ }
+
+ // 성공/실패 카운트
+ const successCount = results.filter(r => r.success).length;
+ const failureCount = results.filter(r => !r.success).length;
+
+ console.log(`📊 처리 결과: 성공 ${successCount}개, 실패 ${failureCount}개`);
+
+ // 캐시 무효화
+ revalidateTag("vendor-attachments");
+
+ return {
+ success: true,
+ results,
+ summary: {
+ total: results.length,
+ success: successCount,
+ failure: failureCount
+ }
+ };
+
+ } catch (error) {
+ console.error("비밀유지 계약서 첨부파일 저장 중 오류 발생:", error);
+ return {
+ success: false,
+ error: error instanceof Error
+ ? error.message
+ : "첨부파일 저장 처리 중 오류가 발생했습니다."
+ };
+ }
+}
diff --git a/lib/vendors/table/request-pq-dialog.tsx b/lib/vendors/table/request-pq-dialog.tsx
index 5b5f722c..9fd7b1d8 100644
--- a/lib/vendors/table/request-pq-dialog.tsx
+++ b/lib/vendors/table/request-pq-dialog.tsx
@@ -46,6 +46,7 @@ import { DatePicker } from "@/components/ui/date-picker"
import { getALLBasicContractTemplates } from "@/lib/basic-contract/service"
import type { BasicContractTemplate } from "@/db/schema"
import { searchItemsForPQ } from "@/lib/items/service"
+import { saveNdaAttachments } from "../service"
// import { PQContractViewer } from "../pq-contract-viewer" // 더 이상 사용하지 않음
interface RequestPQDialogProps extends React.ComponentPropsWithoutRef<typeof Dialog> {
@@ -54,16 +55,16 @@ interface RequestPQDialogProps extends React.ComponentPropsWithoutRef<typeof Dia
onSuccess?: () => void
}
-const AGREEMENT_LIST = [
- "준법서약",
- "표준하도급계약",
- "안전보건관리계약",
- "윤리규범 준수 서약",
- "동반성장협약",
- "내국신용장 미개설 합의",
- "기술자료 제출 기본 동의",
- "GTC 합의",
-]
+// const AGREEMENT_LIST = [
+// "준법서약",
+// "표준하도급계약",
+// "안전보건관리계약",
+// "윤리규범 준수 서약",
+// "동반성장협약",
+// "내국신용장 미개설 합의",
+// "기술자료 제출 기본 동의",
+// "GTC 합의",
+// ]
// PQ 대상 품목 타입 정의
interface PQItem {
@@ -92,6 +93,10 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro
const [basicContractTemplates, setBasicContractTemplates] = React.useState<BasicContractTemplate[]>([])
const [selectedTemplateIds, setSelectedTemplateIds] = React.useState<number[]>([])
const [isLoadingTemplates, setIsLoadingTemplates] = React.useState(false)
+
+ // 비밀유지 계약서 첨부파일 관련 상태
+ const [ndaAttachments, setNdaAttachments] = React.useState<File[]>([])
+ const [isUploadingNdaFiles, setIsUploadingNdaFiles] = React.useState(false)
// 아이템 검색 필터링
React.useEffect(() => {
@@ -172,6 +177,8 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro
setItemSearchQuery("")
setFilteredItems([])
setShowItemDropdown(false)
+ setNdaAttachments([])
+ setIsUploadingNdaFiles(false)
}
}, [props.open])
@@ -197,6 +204,30 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro
setPqItems(prev => prev.filter(item => item.itemCode !== itemCode))
}
+ // 비밀유지 계약서 첨부파일 추가 함수
+ const handleAddNdaAttachment = (event: React.ChangeEvent<HTMLInputElement>) => {
+ const files = event.target.files
+ if (files) {
+ const newFiles = Array.from(files)
+ setNdaAttachments(prev => [...prev, ...newFiles])
+ }
+ // input 초기화
+ event.target.value = ''
+ }
+
+ // 비밀유지 계약서 첨부파일 제거 함수
+ const handleRemoveNdaAttachment = (fileIndex: number) => {
+ setNdaAttachments(prev => prev.filter((_, index) => index !== fileIndex))
+ }
+
+ // 비밀유지 계약서가 선택되었는지 확인하는 함수
+ const isNdaTemplateSelected = () => {
+ return basicContractTemplates.some(template =>
+ selectedTemplateIds.includes(template.id) &&
+ template.templateName?.includes("비밀유지")
+ )
+ }
+
const onApprove = () => {
if (!type) return toast.error("PQ 유형을 선택하세요.")
if (type === "PROJECT" && !selectedProjectId) return toast.error("프로젝트를 선택하세요.")
@@ -235,6 +266,23 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro
console.log("📋 기본계약서 백그라운드 처리 시작", templates.length, "개 템플릿")
await processBasicContractsInBackground(templates, vendors)
}
+
+ // 3단계: 비밀유지 계약서 첨부파일이 있는 경우 저장
+ if (isNdaTemplateSelected() && ndaAttachments.length > 0) {
+ console.log("📎 비밀유지 계약서 첨부파일 처리 시작", ndaAttachments.length, "개 파일")
+
+ const ndaResult = await saveNdaAttachments({
+ vendorIds: vendors.map((v) => v.id),
+ files: ndaAttachments,
+ userId: session.user.id.toString()
+ })
+
+ if (ndaResult.success) {
+ toast.success(`비밀유지 계약서 첨부파일이 모두 저장되었습니다 (${ndaResult.summary?.success}/${ndaResult.summary?.total})`)
+ } else {
+ toast.error(`첨부파일 처리 중 일부 오류가 발생했습니다: ${ndaResult.error}`)
+ }
+ }
// 완료 후 다이얼로그 닫기
props.onOpenChange?.(false)
@@ -262,12 +310,14 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro
for (let vendorIndex = 0; vendorIndex < vendors.length; vendorIndex++) {
const vendor = vendors[vendorIndex]
- // 벤더별 템플릿 데이터 생성
+ // 벤더별 템플릿 데이터 생성 (한글 변수명 사용)
const templateData = {
- vendor_name: vendor.vendorName || '협력업체명',
- address: vendor.address || '주소',
+ company_name: vendor.vendorName || '협력업체명',
+ company_address: vendor.address || '주소',
representative_name: vendor.representativeName || '대표자명',
- today_date: new Date().toLocaleDateString('ko-KR'),
+ signature_date: new Date().toLocaleDateString('ko-KR'),
+ tax_id: vendor.taxId || '사업자번호',
+ phone_number: vendor.phone || '전화번호',
}
console.log(`🔄 벤더 ${vendorIndex + 1}/${vendors.length} 템플릿 데이터:`, templateData)
@@ -559,6 +609,76 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro
)}
</div>
+ {/* 비밀유지 계약서 첨부파일 */}
+ {isNdaTemplateSelected() && (
+ <div className="space-y-2">
+ <Label>비밀유지 계약서 첨부파일</Label>
+
+ {/* 선택된 파일들 표시 */}
+ {ndaAttachments.length > 0 && (
+ <div className="space-y-2">
+ <div className="text-sm text-muted-foreground">
+ 선택된 파일 ({ndaAttachments.length}개)
+ </div>
+ <div className="space-y-1 max-h-32 overflow-y-auto border rounded-md p-2">
+ {ndaAttachments.map((file, index) => (
+ <div key={index} className="flex items-center justify-between text-sm bg-muted/50 rounded px-2 py-1">
+ <div className="flex-1 truncate">
+ <span className="font-medium">{file.name}</span>
+ <span className="text-muted-foreground ml-2">
+ ({(file.size / 1024 / 1024).toFixed(2)} MB)
+ </span>
+ </div>
+ <Button
+ type="button"
+ variant="ghost"
+ size="sm"
+ className="h-6 w-6 p-0 hover:bg-destructive hover:text-destructive-foreground"
+ onClick={() => handleRemoveNdaAttachment(index)}
+ >
+ <X className="h-3 w-3" />
+ </Button>
+ </div>
+ ))}
+ </div>
+ </div>
+ )}
+
+ {/* 파일 선택 버튼 */}
+ <div className="flex items-center gap-2">
+ <input
+ type="file"
+ multiple
+ accept=".pdf,.doc,.docx,.xlsx,.xls,.png,.jpg,.jpeg"
+ onChange={handleAddNdaAttachment}
+ className="hidden"
+ id="nda-file-input"
+ />
+ <Button
+ type="button"
+ variant="outline"
+ size="sm"
+ className="gap-2"
+ onClick={() => document.getElementById('nda-file-input')?.click()}
+ disabled={isUploadingNdaFiles}
+ >
+ <Plus className="h-4 w-4" />
+ 파일 추가
+ </Button>
+ {isUploadingNdaFiles && (
+ <div className="text-sm text-muted-foreground">
+ 파일 업로드 중...
+ </div>
+ )}
+ </div>
+
+ <div className="text-xs text-muted-foreground">
+ 비밀유지 계약서와 관련된 첨부파일을 업로드하세요.
+ 각 벤더별로 동일한 파일이 저장됩니다.
+ </div>
+ </div>
+ )}
+
{/* <div className="space-y-2">
<Label>계약 항목 선택</Label>
{AGREEMENT_LIST.map((label) => (