diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-08-21 09:10:06 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-08-21 09:10:06 +0000 |
| commit | 94082bfe915d3b0337f8929a2bb27828abb5d3c7 (patch) | |
| tree | 0a100365ac92a921a5abc83326dd62c94c071173 | |
| parent | 02b1cf005cf3e1df64183d20ba42930eb2767a9f (diff) | |
(최겸) 협력업체 기본정보 기능 개발 및 수정
| -rw-r--r-- | app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/basic-info-client.tsx | 302 | ||||
| -rw-r--r-- | components/vendor-regular-registrations/document-status-dialog.tsx | 14 | ||||
| -rw-r--r-- | lib/vendor-info/service.ts | 139 | ||||
| -rw-r--r-- | lib/vendors/service.ts | 16 |
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 키 사용 }; }); |
