summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/basic-info-client.tsx302
-rw-r--r--components/vendor-regular-registrations/document-status-dialog.tsx14
-rw-r--r--lib/vendor-info/service.ts139
-rw-r--r--lib/vendors/service.ts16
4 files changed, 387 insertions, 84 deletions
diff --git a/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/basic-info-client.tsx b/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/basic-info-client.tsx
index e92edc11..0e4dccf4 100644
--- a/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/basic-info-client.tsx
+++ b/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/basic-info-client.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useState, useTransition } from "react";
+import React, { useState, useTransition } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -24,6 +24,8 @@ import { DocumentStatusDialog } from "@/components/vendor-regular-registrations/
import { AdditionalInfoDialog } from "@/components/vendor-regular-registrations/additional-info-dialog";
import { getSiteVisitRequestsByVendorId } from "@/lib/site-visit/service";
import { fetchVendorRegistrationStatus } from "@/lib/vendor-regular-registrations/service";
+import { getVendorAttachmentsByType, getVendorPeriodicGrade, getVendorTypeInfo } from "@/lib/vendor-info/service";
+import { downloadFile } from "@/lib/file-download";
import {
Table,
TableBody,
@@ -330,6 +332,11 @@ export default function BasicInfoClient({
// 각 다이얼로그에 필요한 데이터 상태
const [selectedSiteVisitRequest, setSelectedSiteVisitRequest] = useState<any>(null);
const [registrationData, setRegistrationData] = useState<any>(null);
+
+ // 첨부파일 및 평가 정보 상태
+ const [attachmentsByType, setAttachmentsByType] = useState<Record<string, any[]>>({});
+ const [periodicGrade, setPeriodicGrade] = useState<string | null>(null);
+ const [vendorTypeInfo, setVendorTypeInfo] = useState<any>(null);
const [formData, setFormData] = useState<VendorFormData>({
vendorName: initialData?.vendorName || "",
representativeName: initialData?.representativeName || "",
@@ -400,13 +407,6 @@ export default function BasicInfoClient({
setFormData((prev) => ({ ...prev, [field]: value }));
};
- const handleFileManagement = (attachmentType: string) => {
- // TODO: 파일 관리 다이얼로그 열기
- toast.info(
- `[개발중] ${attachmentType} [필요시] 조회/삭제/추가 기능을 구현 예정입니다.`
- );
- };
-
// PQ 조회 핸들러
const handlePQView = () => {
setPqDialogOpen(true);
@@ -437,14 +437,29 @@ export default function BasicInfoClient({
return;
}
- // 등록 데이터가 있는지 확인
- const registrationRecord = result.data;
- if (!registrationRecord || !registrationRecord.documentSubmissions) {
- toast.info("정규등록 정보가 없습니다.");
- return;
- }
+ // DocumentStatusDialog가 기대하는 형태로 데이터 구성
+ const dialogData = {
+ // 기본 정보
+ companyName: result.data.vendor.vendorName,
+ businessNumber: result.data.vendor.taxId,
+ representative: result.data.vendor.representativeName,
+ country: result.data.vendor.country,
+ status: result.data.registration?.status || "정보없음",
+
+ // 문서 제출 현황 - documentSubmissions 속성으로 매핑
+ documentSubmissions: result.data.documentStatus,
+
+ // 기본계약 정보
+ basicContracts: result.data.basicContracts || [],
+
+ // 안전적격성 평가
+ safetyQualificationContent: result.data.registration?.safetyQualificationContent || null,
+
+ // 추가정보 완료 여부
+ additionalInfo: result.data.additionalInfoCompleted,
+ };
- setRegistrationData(registrationRecord);
+ setRegistrationData(dialogData);
setContractDialogOpen(true);
} catch (error) {
console.error("기본계약 정보 조회 오류:", error);
@@ -457,6 +472,77 @@ export default function BasicInfoClient({
setAdditionalInfoDialogOpen(true);
};
+ // 첨부파일 및 평가 정보 로드
+ const loadVendorData = async () => {
+ try {
+ // 첨부파일 조회
+ const attachmentsResult = await getVendorAttachmentsByType(parseInt(vendorId));
+ if (attachmentsResult.success && attachmentsResult.data) {
+ setAttachmentsByType(attachmentsResult.data);
+ }
+
+ // 정기평가 등급 조회
+ const gradeResult = await getVendorPeriodicGrade(parseInt(vendorId));
+ if (gradeResult.success && gradeResult.data) {
+ setPeriodicGrade(gradeResult.data.finalGrade);
+ }
+
+ // 벤더 타입 정보 조회
+ const typeResult = await getVendorTypeInfo(parseInt(vendorId));
+ if (typeResult.success && typeResult.data) {
+ setVendorTypeInfo(typeResult.data);
+ }
+ } catch (error) {
+ console.error("벤더 데이터 로드 오류:", error);
+ }
+ };
+
+ // 컴포넌트 마운트 시 데이터 로드
+ React.useEffect(() => {
+ if (vendorId) {
+ loadVendorData();
+ }
+ }, [vendorId]);
+
+ // 첨부파일 다운로드 핸들러
+ const handleAttachmentDownload = async (filePath: string, fileName: string) => {
+ try {
+ const result = await downloadFile(filePath, fileName);
+ if (result.success) {
+ toast.success(`${fileName} 파일이 다운로드되었습니다.`);
+ } else {
+ toast.error(result.error || "파일 다운로드에 실패했습니다.");
+ }
+ } catch (error) {
+ console.error("파일 다운로드 오류:", error);
+ toast.error("파일 다운로드에 실패했습니다.");
+ }
+ };
+
+ // 첨부파일 관리 핸들러 (타입별)
+ const handleAttachmentFileManagement = (attachmentType: string, typeName: string) => {
+ const files = attachmentsByType[attachmentType] || [];
+
+ if (files.length === 0) {
+ toast.info(`${typeName} 파일이 없습니다.`);
+ return;
+ }
+
+ // 파일이 하나인 경우 바로 다운로드
+ if (files.length === 1) {
+ handleAttachmentDownload(files[0].filePath, files[0].fileName);
+ return;
+ }
+
+ // 파일이 여러 개인 경우 순차적으로 모든 파일 다운로드
+ toast.info(`${typeName} 파일 ${files.length}개를 다운로드합니다.`);
+ files.forEach((file, index) => {
+ setTimeout(() => {
+ handleAttachmentDownload(file.filePath, file.fileName);
+ }, index * 500); // 500ms 간격으로 순차 다운로드
+ });
+ };
+
if (!initialData) {
return (
<div className="p-6 bg-background max-w-full">
@@ -467,16 +553,7 @@ export default function BasicInfoClient({
);
}
- const attachmentsByType = initialData.attachments.reduce(
- (acc: Record<string, VendorAttachment[]>, attachment: VendorAttachment) => {
- if (!acc[attachment.attachmentType]) {
- acc[attachment.attachmentType] = [];
- }
- acc[attachment.attachmentType].push(attachment);
- return acc;
- },
- {}
- );
+ // attachmentsByType는 상태로 관리되고 있으므로 제거
return (
<div className="p-6 bg-background w-full overflow-x-auto">
@@ -571,9 +648,7 @@ export default function BasicInfoClient({
/> */}
<InfoItem
title="정기평가 등급"
- value={
- initialData.evaluationInfo?.regularEvaluationGrade || null
- }
+ value={periodicGrade || ""}
type="readonly"
/>
</div>
@@ -592,27 +667,18 @@ export default function BasicInfoClient({
/>
<InfoItem
title="회사주소"
- value={formData.address}
- isEditable={true}
- editMode={editMode}
- fieldKey="address"
- onChange={(value) => updateField("address", value)}
+ value={initialData.address || ""}
+ type="readonly"
/>
<InfoItem
title="상세주소"
- value={formData.addressDetail}
- isEditable={true}
- editMode={editMode}
- fieldKey="addressDetail"
- onChange={(value) => updateField("addressDetail", value)}
+ value={initialData.addressDetail || ""}
+ type="readonly"
/>
<InfoItem
title="우편번호"
- value={formData.postalCode}
- isEditable={true}
- editMode={editMode}
- fieldKey="postalCode"
- onChange={(value) => updateField("postalCode", value)}
+ value={initialData.postalCode || ""}
+ type="readonly"
/>
<InfoItem
title="E-mail"
@@ -643,34 +709,23 @@ export default function BasicInfoClient({
onChange={(value) => updateField("businessSize", value)}
placeholder="기업규모를 선택하세요"
/> */}
- <InfoItem
- title="사업자등록증"
- value={`${
- attachmentsByType.BUSINESS_REGISTRATION?.length || 0
- }건`}
- isEditable={true}
- editMode={editMode}
- type="file-button"
- onFileButtonClick={() => handleFileManagement("사업자등록증")}
- />
- <InfoItem
+
+ {/* <InfoItem
title="안전적격성평가"
value={
initialData.evaluationInfo?.safetyQualificationEvaluation ||
null
}
type="readonly"
- />
+ /> */}
</div>
}
column3={
<div className="space-y-2">
<InfoItem
title="업체분류"
- value={
- initialData.classificationInfo?.vendorClassification || null
- }
- isEditable={true}
+ value={vendorTypeInfo?.vendorTypeName || ""}
+ type="readonly"
/>
{/* <InfoItem
title="그룹사"
@@ -692,26 +747,129 @@ export default function BasicInfoClient({
}
isEditable={true}
/>
- <InfoItem
+ {/* <InfoItem
title="산업유형"
value={initialData.classificationInfo?.industryType || ""}
isEditable={true}
- />
- <InfoItem
- title="ISO Cert"
- value={`${attachmentsByType.ISO_CERTIFICATION?.length || 0}건`}
- isEditable={true}
- editMode={editMode}
- type="file-button"
- onFileButtonClick={() => handleFileManagement("ISO 인증서")}
- />
- <InfoItem
+ /> */}
+
+ {/* <InfoItem
title="당사거래비중"
value={
initialData.evaluationInfo?.companyTransactionRatio || ""
}
type="readonly"
- />
+ /> */}
+ </div>
+ }
+ />
+
+ <Separator />
+
+ {/* 첨부파일 */}
+ <WideInfoSection
+ title="첨부파일"
+ content={
+ <div className="grid grid-cols-2 md:grid-cols-5 gap-4 p-4">
+ {/* 사업자등록증 */}
+ <div className="flex flex-col items-center gap-3">
+ <div className="text-sm font-medium text-center">사업자등록증</div>
+ <div className="text-center">
+ <div className="text-lg font-semibold text-primary">
+ {attachmentsByType.BUSINESS_REGISTRATION?.length || 0}건
+ </div>
+ {attachmentsByType.BUSINESS_REGISTRATION?.length > 0 && (
+ <Button
+ variant="outline"
+ size="sm"
+ className="mt-2"
+ onClick={() => handleAttachmentFileManagement("BUSINESS_REGISTRATION", "사업자등록증")}
+ >
+ 파일 보기
+ </Button>
+ )}
+ </div>
+ </div>
+
+ {/* 신용평가보고서 */}
+ <div className="flex flex-col items-center gap-3">
+ <div className="text-sm font-medium text-center">신용평가보고서</div>
+ <div className="text-center">
+ <div className="text-lg font-semibold text-primary">
+ {attachmentsByType.CREDIT_REPORT?.length || 0}건
+ </div>
+ {attachmentsByType.CREDIT_REPORT?.length > 0 && (
+ <Button
+ variant="outline"
+ size="sm"
+ className="mt-2"
+ onClick={() => handleAttachmentFileManagement("CREDIT_REPORT", "신용평가보고서")}
+ >
+ 파일 보기
+ </Button>
+ )}
+ </div>
+ </div>
+
+ {/* 통장사본 */}
+ <div className="flex flex-col items-center gap-3">
+ <div className="text-sm font-medium text-center">통장사본</div>
+ <div className="text-center">
+ <div className="text-lg font-semibold text-primary">
+ {attachmentsByType.BANK_ACCOUNT_COPY?.length || 0}건
+ </div>
+ {attachmentsByType.BANK_ACCOUNT_COPY?.length > 0 && (
+ <Button
+ variant="outline"
+ size="sm"
+ className="mt-2"
+ onClick={() => handleAttachmentFileManagement("BANK_ACCOUNT_COPY", "통장사본")}
+ >
+ 파일 보기
+ </Button>
+ )}
+ </div>
+ </div>
+
+ {/* ISO 인증서 */}
+ <div className="flex flex-col items-center gap-3">
+ <div className="text-sm font-medium text-center">ISO 인증서</div>
+ <div className="text-center">
+ <div className="text-lg font-semibold text-primary">
+ {attachmentsByType.ISO_CERTIFICATION?.length || 0}건
+ </div>
+ {attachmentsByType.ISO_CERTIFICATION?.length > 0 && (
+ <Button
+ variant="outline"
+ size="sm"
+ className="mt-2"
+ onClick={() => handleAttachmentFileManagement("ISO_CERTIFICATION", "ISO 인증서")}
+ >
+ 파일 보기
+ </Button>
+ )}
+ </div>
+ </div>
+
+ {/* 기타 첨부파일 (GENERAL) */}
+ <div className="flex flex-col items-center gap-3">
+ <div className="text-sm font-medium text-center">기타 첨부파일</div>
+ <div className="text-center">
+ <div className="text-lg font-semibold text-primary">
+ {attachmentsByType.GENERAL?.length || 0}건
+ </div>
+ {attachmentsByType.GENERAL?.length > 0 && (
+ <Button
+ variant="outline"
+ size="sm"
+ className="mt-2"
+ onClick={() => handleAttachmentFileManagement("GENERAL", "기타 첨부파일")}
+ >
+ 파일 보기
+ </Button>
+ )}
+ </div>
+ </div>
</div>
}
/>
@@ -1189,7 +1347,7 @@ export default function BasicInfoClient({
}
/> */}
- {/* <Separator /> */}
+
{/* 추가 조회 기능 버튼들 */}
<div className="border rounded-lg p-6">
diff --git a/components/vendor-regular-registrations/document-status-dialog.tsx b/components/vendor-regular-registrations/document-status-dialog.tsx
index db3defe6..1b10760a 100644
--- a/components/vendor-regular-registrations/document-status-dialog.tsx
+++ b/components/vendor-regular-registrations/document-status-dialog.tsx
@@ -72,16 +72,6 @@ export function DocumentStatusDialog({
}: DocumentStatusDialogProps) {
if (!registration) return null;
- // 디버깅: registration 데이터 확인
- console.log(`📋 DocumentStatusDialog - Partners 등록 데이터:`, {
- companyName: registration.companyName,
- businessNumber: registration.businessNumber,
- representative: registration.representative,
- safetyQualificationContent: registration.safetyQualificationContent,
- basicContractsLength: registration.basicContracts?.length || 0,
- additionalInfo: registration.additionalInfo
- });
-
// 파일 다운로드 핸들러
// const handleFileDownload = async (docKey: string, fileIndex: number = 0) => {
// try {
@@ -186,9 +176,9 @@ export function DocumentStatusDialog({
<div>액션</div>
</div>
{documentStatusColumns.map((doc) => {
- const isSubmitted = registration.documentSubmissions[
+ const isSubmitted = registration.documentSubmissions?.[
doc.key as keyof typeof registration.documentSubmissions
- ] as boolean;
+ ] as boolean || false;
// 내자인 경우 통장사본은 표시하지 않음
const isForeign = registration.country !== 'KR';
diff --git a/lib/vendor-info/service.ts b/lib/vendor-info/service.ts
new file mode 100644
index 00000000..6002179f
--- /dev/null
+++ b/lib/vendor-info/service.ts
@@ -0,0 +1,139 @@
+"use server";
+
+import db from "@/db/db";
+import { vendorAttachments, evaluationTargets, periodicEvaluations, vendors, vendorTypes } from "@/db/schema";
+import { eq, desc } from "drizzle-orm";
+
+// 벤더 첨부파일 조회
+export async function getVendorAttachmentsByType(vendorId: number) {
+ try {
+ const attachments = await db
+ .select({
+ id: vendorAttachments.id,
+ fileName: vendorAttachments.fileName,
+ filePath: vendorAttachments.filePath,
+ attachmentType: vendorAttachments.attachmentType,
+ fileType: vendorAttachments.fileType,
+ createdAt: vendorAttachments.createdAt,
+ })
+ .from(vendorAttachments)
+ .where(eq(vendorAttachments.vendorId, vendorId));
+
+ // 타입별로 그룹화
+ const attachmentsByType = attachments.reduce((acc, attachment) => {
+ const type = attachment.attachmentType || 'GENERAL';
+ if (!acc[type]) {
+ acc[type] = [];
+ }
+ acc[type].push(attachment);
+ return acc;
+ }, {} as Record<string, typeof attachments>);
+
+ return {
+ success: true,
+ data: attachmentsByType,
+ };
+ } catch (error) {
+ console.error("첨부파일 조회 오류:", error);
+ return {
+ success: false,
+ error: "첨부파일을 불러오는데 실패했습니다.",
+ };
+ }
+}
+
+// 정기평가 등급 조회
+export async function getVendorPeriodicGrade(vendorId: number) {
+ try {
+ // evaluation_targets에서 vendorId로 조회하여 평가 대상 ID 찾기
+ const evaluationTarget = await db
+ .select({
+ id: evaluationTargets.id,
+ })
+ .from(evaluationTargets)
+ .where(eq(evaluationTargets.vendorId, vendorId))
+ .limit(1);
+
+ if (evaluationTarget.length === 0) {
+ return {
+ success: true,
+ data: null, // 평가 대상이 없음
+ };
+ }
+
+ // periodic_evaluations에서 최신 finalGrade 조회
+ const latestEvaluation = await db
+ .select({
+ finalGrade: periodicEvaluations.finalGrade,
+ evaluationPeriod: periodicEvaluations.evaluationPeriod,
+ finalizedAt: periodicEvaluations.finalizedAt,
+ })
+ .from(periodicEvaluations)
+ .where(eq(periodicEvaluations.evaluationTargetId, evaluationTarget[0].id))
+ .orderBy(desc(periodicEvaluations.finalizedAt))
+ .limit(1);
+
+ return {
+ success: true,
+ data: latestEvaluation[0] || null,
+ };
+ } catch (error) {
+ console.error("정기평가 등급 조회 오류:", error);
+ return {
+ success: false,
+ error: "정기평가 등급을 불러오는데 실패했습니다.",
+ };
+ }
+}
+
+// 첨부파일 개수 조회 (특정 타입)
+export async function getAttachmentCount(vendorId: number, attachmentType: string) {
+ try {
+ const count = await db
+ .select({ count: vendorAttachments.id })
+ .from(vendorAttachments)
+ .where(
+ eq(vendorAttachments.vendorId, vendorId) &&
+ eq(vendorAttachments.attachmentType, attachmentType)
+ );
+
+ return count.length;
+ } catch (error) {
+ console.error(`${attachmentType} 첨부파일 개수 조회 오류:`, error);
+ return 0;
+ }
+}
+
+// 벤더 타입 정보 조회 (잠재업체/정규업체 구분)
+export async function getVendorTypeInfo(vendorId: number) {
+ try {
+ const vendorWithType = await db
+ .select({
+ vendorTypeName: vendorTypes.nameKo,
+ vendorTypeCode: vendorTypes.code,
+ vendorTypeNameEn: vendorTypes.nameEn,
+ })
+ .from(vendors)
+ .leftJoin(vendorTypes, eq(vendors.vendorTypeId, vendorTypes.id))
+ .where(eq(vendors.id, vendorId))
+ .limit(1);
+
+ if (vendorWithType.length === 0) {
+ return {
+ success: true,
+ data: null,
+ };
+ }
+
+ return {
+ success: true,
+ data: vendorWithType[0],
+ };
+ } catch (error) {
+ console.error("벤더 타입 정보 조회 오류:", error);
+ return {
+ success: false,
+ error: "벤더 타입 정보를 불러오는데 실패했습니다.",
+ };
+ }
+}
diff --git a/lib/vendors/service.ts b/lib/vendors/service.ts
index 9cb653ea..4cca3b12 100644
--- a/lib/vendors/service.ts
+++ b/lib/vendors/service.ts
@@ -2537,6 +2537,8 @@ export async function getVendorBasicInfo(vendorId: number) {
vendorCode: vendor.vendorCode,
taxId: vendor.taxId,
address: vendor.address,
+ addressDetail: vendor.addressDetail || "",
+ postalCode: vendor.postalCode || "",
businessSize: vendor.businessSize || "", // vendorsWithTypesView에 businessSize 필드가 없을 경우 대비
country: vendor.country,
phone: vendor.phone,
@@ -2606,6 +2608,20 @@ export async function getVendorBasicInfo(vendorId: number) {
capacityInfo: null,
+ // 누락된 필수 필드들 추가
+ processInfo: {
+ processCount: 0,
+ processPIC: "",
+ processApprovalDate: "",
+ implementationApproval: ""
+ },
+
+ contractInfo: {
+ contractRegistrationNumber: "",
+ contractPeriod: "",
+ lastEquipmentInspection: ""
+ },
+
calculatedMetrics: null, // 구현 시 { "20231231": { debtRatio: 0, ... }, "20221231": { ... } } 형태로 YYYYMMDD 키 사용
};
});