From 089c70ffbe2303ab5e2611a152ddd3aed0e6e718 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 1 Sep 2025 09:09:15 +0000 Subject: (최겸) 구매 pq, 기본정보 수정 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/vendor-basic-info/actions.ts | 193 +++ lib/vendor-basic-info/basic-info-client.tsx | 1650 ++++++++++++++++++++ lib/vendor-basic-info/constants.ts | 1 + lib/vendor-basic-info/types.ts | 180 +++ .../vendor-regular-registrations-table-columns.tsx | 1 + lib/vendors/service.ts | 11 +- 6 files changed, 2031 insertions(+), 5 deletions(-) create mode 100644 lib/vendor-basic-info/actions.ts create mode 100644 lib/vendor-basic-info/basic-info-client.tsx create mode 100644 lib/vendor-basic-info/constants.ts create mode 100644 lib/vendor-basic-info/types.ts (limited to 'lib') diff --git a/lib/vendor-basic-info/actions.ts b/lib/vendor-basic-info/actions.ts new file mode 100644 index 00000000..8428deb9 --- /dev/null +++ b/lib/vendor-basic-info/actions.ts @@ -0,0 +1,193 @@ +"use server"; + +import { getVendorBasicInfo } from "@/lib/vendors/service"; +import { VendorFormData } from "./types"; +import { getPQDataByVendorId } from "@/lib/pq/service"; +import db from "@/db/db" +import { vendorPQSubmissions, vendorPqCriteriaAnswers, pqCriterias, vendorCriteriaAttachments, vendorAdditionalInfo } from "@/db/schema" +import { vendorBusinessContacts } from "@/db/schema" + +import { eq } from "drizzle-orm"; + +/** + * 벤더 기본정보를 가져오는 서버 액션 + */ +export async function getVendorData(vendorId: string) { + try { + const id = parseInt(vendorId); + if (isNaN(id)) { + return null; + } + + const vendorData = await getVendorBasicInfo(id); + return vendorData; + } catch (error) { + console.error("Error in getVendorData:", error); + return null; + } +} + +/** + * 벤더의 PQ 데이터를 가져오는 서버 액션 + */ +export async function getVendorPQData(vendorId: string) { + try { + const id = parseInt(vendorId); + if (isNaN(id)) { + return null; + } + + const pqData = await getPQDataByVendorId(id); + return pqData; + } catch (error) { + console.error("Error in getVendorPQData:", error); + return null; + } +} + +/** + * 벤더의 PQ 제출 데이터와 답변을 가져오는 서버 액션 + */ +export async function getVendorPQSubmissionData(vendorId: string) { + try { + const id = parseInt(vendorId); + if (isNaN(id)) { + return null; + } + + // 벤더의 모든 PQ 제출 데이터 조회 + const submissions = await db + .select() + .from(vendorPQSubmissions) + .where(eq(vendorPQSubmissions.vendorId, id)); + + if (submissions.length === 0) { + return null; + } + + // 각 제출에 대한 답변 데이터 조회 + const submissionData = await Promise.all( + submissions.map(async (submission) => { + const answers = await db + .select({ + id: vendorPqCriteriaAnswers.id, + vendorId: vendorPqCriteriaAnswers.vendorId, + criteriaId: vendorPqCriteriaAnswers.criteriaId, + projectId: vendorPqCriteriaAnswers.projectId, + answer: vendorPqCriteriaAnswers.answer, + shiComment: vendorPqCriteriaAnswers.shiComment, + vendorReply: vendorPqCriteriaAnswers.vendorReply, + createdAt: vendorPqCriteriaAnswers.createdAt, + updatedAt: vendorPqCriteriaAnswers.updatedAt, + criteriaCode: pqCriterias.code, + checkPoint: pqCriterias.checkPoint, + description: pqCriterias.description, + groupName: pqCriterias.groupName, + subGroupName: pqCriterias.subGroupName, + inputFormat: pqCriterias.inputFormat + }) + .from(vendorPqCriteriaAnswers) + .leftJoin(pqCriterias, eq(vendorPqCriteriaAnswers.criteriaId, pqCriterias.id)) + .where(eq(vendorPqCriteriaAnswers.vendorId, id)); + + // 각 답변에 대한 첨부파일 정보 조회 + const answersWithAttachments = await Promise.all( + answers.map(async (answer) => { + const attachments = await db + .select({ + id: vendorCriteriaAttachments.id, + fileName: vendorCriteriaAttachments.fileName, + originalFileName: vendorCriteriaAttachments.originalFileName, + filePath: vendorCriteriaAttachments.filePath, + fileType: vendorCriteriaAttachments.fileType, + fileSize: vendorCriteriaAttachments.fileSize + }) + .from(vendorCriteriaAttachments) + .where(eq(vendorCriteriaAttachments.vendorCriteriaAnswerId, answer.id)); + + return { + ...answer, + uploadedFiles: attachments + }; + }) + ); + + return { + submission, + answers: answersWithAttachments + }; + }) + ); + + return submissionData; + } catch (error) { + console.error("Error in getVendorPQSubmissionData:", error); + return null; + } +} + +/** + * 벤더의 추가정보를 가져오는 서버 액션 + */ +export async function getVendorAdditionalInfo(vendorId: string) { + try { + const id = parseInt(vendorId); + if (isNaN(id)) { + return null; + } + + const additionalInfo = await db + .select() + .from(vendorAdditionalInfo) + .where(eq(vendorAdditionalInfo.vendorId, id)); + + return additionalInfo.length > 0 ? additionalInfo[0] : null; + } catch (error) { + console.error("Error in getVendorAdditionalInfo:", error); + return null; + } +} + +/** + * 벤더의 업무담당자 정보를 가져오는 서버 액션 + */ +export async function getVendorBusinessContacts(vendorId: string) { + try { + const id = parseInt(vendorId); + if (isNaN(id)) { + return []; + } + + const rows = await db + .select() + .from(vendorBusinessContacts) + .where(eq(vendorBusinessContacts.vendorId, id)); + + return rows; + } catch (error) { + console.error("Error in getVendorBusinessContacts:", error); + return []; + } +} + +/** + * 벤더 기본정보를 업데이트하는 서버 액션 (향후 구현) + */ +export async function updateVendorData(vendorId: string, formData: VendorFormData) { + try { + // TODO: 실제 업데이트 로직 구현 + console.log("Updating vendor data:", { vendorId, formData }); + + // 임시로 성공 응답 반환 + return { + success: true, + message: "(개발중입니다) 벤더 정보가 성공적으로 업데이트되었습니다.", + }; + } catch (error) { + console.error("Error in updateVendorData:", error); + return { + success: false, + message: "업데이트 중 오류가 발생했습니다.", + }; + } +} \ No newline at end of file diff --git a/lib/vendor-basic-info/basic-info-client.tsx b/lib/vendor-basic-info/basic-info-client.tsx new file mode 100644 index 00000000..ce8e4dfc --- /dev/null +++ b/lib/vendor-basic-info/basic-info-client.tsx @@ -0,0 +1,1650 @@ +"use client"; + +import React, { useState, useTransition } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Separator } from "@/components/ui/separator"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Edit, Save, X } from "lucide-react"; +import { toast } from "sonner"; +import { VendorData, VendorFormData, VendorAttachment } from "./types"; +import { updateVendorData, getVendorPQData, getVendorPQSubmissionData, getVendorAdditionalInfo } from "./actions"; +import { getVendorBusinessContacts } from "./actions"; +import { noDataString } from "./constants"; +import { PQSimpleDialog } from "@/components/vendor-info/pq-simple-dialog"; +import { SiteVisitDetailDialog } from "@/lib/site-visit/site-visit-detail-dialog"; +import { DocumentStatusDialog } from "@/components/vendor-regular-registrations/document-status-dialog"; +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"; +// downloadFile은 동적으로 import +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; + +interface BasicInfoClientProps { + initialData: VendorData | null; + vendorId: string; +} + +interface DropdownOption { + value: string; + label: string; +} + +interface InfoItemProps { + title: string; + value: string | boolean; + isEditable?: boolean; + editMode?: boolean; + onChange?: (value: string | boolean) => void; + fieldKey?: string; + type?: "text" | "checkbox" | "dropdown" | "file-button" | "readonly"; + options?: DropdownOption[]; // dropdown용 옵션들 + onFileButtonClick?: () => void; // 파일 버튼 클릭 핸들러 + placeholder?: string; // input placeholder +} + +const InfoItem = ({ + title, + value, + isEditable = false, + editMode = false, + onChange, + fieldKey, + type = "text", + options = [], + onFileButtonClick, + placeholder, +}: InfoItemProps) => { + // 편집 가능 여부 결정 (readonly 타입은 항상 읽기 전용) + const canEdit = isEditable && editMode && type !== "readonly"; + + // 표시할 값 결정 (빈 값일 때 처리) + const displayValue = value || ""; + const showNoData = !value && !canEdit; + + const renderEditableField = () => { + switch (type) { + case "checkbox": + return ( +
+ onChange?.(checked)} + /> + +
+ ); + + case "dropdown": + return ( + + ); + + case "file-button": + return ( + + ); + + case "text": + default: + return ( + onChange?.(e.target.value)} + className="h-8" + placeholder={placeholder} + /> + ); + } + }; + + const renderReadOnlyField = () => { + switch (type) { + case "checkbox": + return ( +
+ + +
+ ); + + case "file-button": + return ( + + ); + + case "dropdown": + case "text": + case "readonly": + default: + return showNoData ? noDataString : displayValue; + } + }; + + return ( +
+
{title}:
+
+ {canEdit ? ( +
+ + {renderEditableField()} +
+ ) : ( + {renderReadOnlyField()} + )} +
+
+ ); +}; + +const OrganizationChart = ({ + data, + editMode = false, + onChange, + pqData, +}: { + data: any; + editMode?: boolean; + onChange?: (field: string, value: string) => void; + pqData: any[]; +}) => { + const organizationFields = [ + { key: "representative", label: "대표", code: "1-5-1" }, + { key: "sales", label: "영업", code: "1-5-2" }, + { key: "design", label: "설계", code: "1-5-3" }, + { key: "procurement", label: "구매", code: "1-5-4" }, + { key: "production", label: "생산", code: "1-5-5" }, + { key: "quality", label: "품질", code: "1-5-6" }, + ]; + + return ( +
+
조직도
+
+ {organizationFields.map((field) => ( +
+
+ {field.label} +
+
+ {editMode ? ( + onChange?.(field.key, e.target.value)} + className="h-8 w-16 text-center" + placeholder="0" + /> + ) : ( + + {pqData && Array.isArray(pqData) && pqData.find((a: any) => a.criteriaCode === field.code)?.answer || data?.[field.key]?.toString() || noDataString} + + )} +
+
+ ))} +
+
+ ); +}; + +const InfoSection = ({ + title, + subtitle, + column1, + column2, + column3, + additionalContent, +}: { + title: string; + subtitle?: string; + column1: React.ReactNode; + column2: React.ReactNode; + column3: React.ReactNode; + additionalContent?: React.ReactNode; +}) => ( +
+
+
+
{title}
+ {subtitle && ( +
+ {subtitle} +
+ )} +
+
+
{column1}
+
{column2}
+
{column3}
+
+
+ {additionalContent && ( +
+
+
{additionalContent}
+
+ )} +
+); + +const WideInfoSection = ({ + title, + subtitle, + content, + noPadding = false, +}: { + title?: string; + subtitle?: string; + content: React.ReactNode; + noPadding?: boolean; +}) => ( +
+
+
+
{title}
+ {subtitle && ( +
+ {subtitle} +
+ )} +
+
+ {content} +
+
+
+); + +export default function BasicInfoClient({ + initialData, + vendorId, +}: BasicInfoClientProps) { + const [editMode, setEditMode] = useState(false); + const [isPending, startTransition] = useTransition(); + + // 다이얼로그 상태 + const [pqDialogOpen, setPqDialogOpen] = useState(false); + const [siteVisitDialogOpen, setSiteVisitDialogOpen] = useState(false); + const [contractDialogOpen, setContractDialogOpen] = useState(false); + const [additionalInfoDialogOpen, setAdditionalInfoDialogOpen] = useState(false); + + // 각 다이얼로그에 필요한 데이터 상태 + const [selectedSiteVisitRequest, setSelectedSiteVisitRequest] = useState(null); + const [registrationData, setRegistrationData] = useState(null); + + // 첨부파일 및 평가 정보 상태 + const [attachmentsByType, setAttachmentsByType] = useState>({}); + const [periodicGrade, setPeriodicGrade] = useState(null); + const [vendorTypeInfo, setVendorTypeInfo] = useState(null); + const [pqData, setPqData] = useState([]); + const [pqSubmissionData, setPqSubmissionData] = useState([]); + const [additionalInfo, setAdditionalInfo] = useState(null); + const [businessContacts, setBusinessContacts] = useState([]); + const [formData, setFormData] = useState({ + vendorName: initialData?.vendorName || "", + representativeName: initialData?.representativeName || "", + representativeWorkExperience: + initialData?.representativeWorkExperience || false, + representativeBirth: initialData?.representativeBirth || "", + representativePhone: initialData?.representativePhone || "", + representativeEmail: initialData?.representativeEmail || "", + phone: initialData?.phone || "", + fax: initialData?.fax || "", + email: initialData?.email || "", + address: initialData?.address || "", + addressDetail: initialData?.addressDetail || "", + postalCode: initialData?.postalCode || "", + businessSize: initialData?.businessSize || "", + country: initialData?.country || "", + website: initialData?.website || "", + businessType: initialData?.additionalInfo?.businessType || "", + employeeCount: initialData?.additionalInfo?.employeeCount || 0, + mainBusiness: initialData?.additionalInfo?.mainBusiness || "", + }); + + const handleSave = () => { + // startTransition(async () => { + // try { + // const result = await updateVendorData(vendorId, formData); + // if (result.success) { + // toast.success("[개발중] 저장되지 않습니다. 업데이트는 구현중입니다."); + // setEditMode(false); + // } else { + // toast.error(result.message || "저장에 실패했습니다."); + // } + // } catch { + // toast.error("저장 중 오류가 발생했습니다."); + // } + // }); + }; + + const handleCancel = () => { + setFormData({ + vendorName: initialData?.vendorName || "", + representativeName: initialData?.representativeName || "", + representativeWorkExperience: + initialData?.representativeWorkExperience || false, + representativeBirth: initialData?.representativeBirth || "", + representativePhone: initialData?.representativePhone || "", + representativeEmail: initialData?.representativeEmail || "", + phone: initialData?.phone || "", + fax: initialData?.fax || "", + email: initialData?.email || "", + address: initialData?.address || "", + addressDetail: initialData?.addressDetail || "", + postalCode: initialData?.postalCode || "", + businessSize: initialData?.businessSize || "", + country: initialData?.country || "", + website: initialData?.website || "", + businessType: initialData?.additionalInfo?.businessType || "", + employeeCount: initialData?.additionalInfo?.employeeCount || 0, + mainBusiness: initialData?.additionalInfo?.mainBusiness || "", + }); + setEditMode(false); + }; + + const updateField = ( + field: keyof VendorFormData, + value: string | number | boolean + ) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; + + // 기본계약 현황 조회 핸들러 + const handleContractView = async () => { + try { + const result = await fetchVendorRegistrationStatus(parseInt(vendorId)); + if (!result.success || !result.data) { + toast.info("기본계약 정보가 없습니다."); + return; + } + + // DocumentStatusDialog가 기대하는 형태로 데이터 구성 + const dialogData = { + // 기본 정보 + id: result.data.registration?.id || 0, + vendorId: parseInt(vendorId), + 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, + + // 문서별 파일 정보 추가 + documentFiles: result.data.documentFiles || { + businessRegistration: [], + creditEvaluation: [], + bankCopy: [], + auditResult: [] + }, + + // 기본계약 정보 + basicContracts: result.data.basicContracts || [], + + // 안전적격성 평가 + safetyQualificationContent: result.data.registration?.safetyQualificationContent || null, + + // 추가정보 완료 여부 + additionalInfo: result.data.additionalInfoCompleted, + }; + + setRegistrationData(dialogData); + setContractDialogOpen(true); + } catch (error) { + console.error("기본계약 정보 조회 오류:", error); + toast.error("기본계약 정보를 불러오는데 실패했습니다."); + } + }; + + // 첨부파일 및 평가 정보 로드 + 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); + } + + // PQ 데이터 조회 + const pqResult = await getVendorPQData(vendorId); + if (pqResult) { + setPqData(pqResult); + } + + // PQ 제출 데이터 조회 + const pqSubmissionResult = await getVendorPQSubmissionData(vendorId); + if (pqSubmissionResult) { + setPqSubmissionData(pqSubmissionResult); + } + + // 추가정보 조회 + const additionalInfoResult = await getVendorAdditionalInfo(vendorId); + if (additionalInfoResult) { + setAdditionalInfo(additionalInfoResult); + } + + // 업무담당자 정보 조회 + const contacts = await getVendorBusinessContacts(vendorId); + setBusinessContacts(contacts || []); + } catch (error) { + console.error("벤더 데이터 로드 오류:", error); + } + }; + + // 컴포넌트 마운트 시 데이터 로드 + React.useEffect(() => { + if (vendorId) { + loadVendorData(); + } + }, [vendorId]); + + // 첨부파일 다운로드 핸들러 + const handleAttachmentDownload = async (filePath: string, fileName: string) => { + try { + // 동적으로 downloadFile 함수 import + const { downloadFile } = await import('@/lib/file-download') + + const result = await downloadFile(filePath, fileName); + if (result.success) { + toast.success(`${fileName} 파일이 다운로드되었습니다.`); + } else { + toast.error(result.error || "파일 다운로드에 실패했습니다."); + } + } catch (error) { + console.error("파일 다운로드 오류:", error); + toast.error("파일 다운로드에 실패했습니다."); + } + }; + + // PQ 데이터에서 실사 정보 추출 + const extractFactoryInfo = (pqData: any[]) => { + const factoryInfo = { + factoryAddress: "", + factoryPhone: "", + factoryFax: "", + factoryPIC: "", + factoryPICPosition: "", + factoryPICContact: "", + factoryPICEmail: "", + mainSupplyItems: "", + inspectionResult: "", + inspectionDate: "", + inspectionFiles: [] as any[] + }; + + if (!pqData || !Array.isArray(pqData)) { + return factoryInfo; + } + + pqData.forEach(group => { + if (group && group.items && Array.isArray(group.items)) { + group.items.forEach((item: any) => { + const code = item.code; + const answer = item.answer; + const files = item.uploadedFiles || []; + + // 공장주소 (1-4-1) + if (code === "1-4-1") { + factoryInfo.factoryAddress = answer || ""; + } + // 공장 전화 (1-4-2) + else if (code === "1-4-2") { + factoryInfo.factoryPhone = answer || ""; + } + // 공장 팩스 (1-4-3) + else if (code === "1-4-3") { + factoryInfo.factoryFax = answer || ""; + } + // 공장 대표/담당자 이름 (1-4-4) + else if (code === "1-4-4") { + factoryInfo.factoryPIC = answer || ""; + } + // 공장 대표/담당자 직책 (1-4-5) + else if (code === "1-4-5") { + factoryInfo.factoryPICPosition = answer || ""; + } + // 공장 대표/담당자 전화 (1-4-6) + else if (code === "1-4-6") { + factoryInfo.factoryPICContact = answer || ""; + } + // 공장 대표/담당자 이메일 (1-4-7) + else if (code === "1-4-7") { + factoryInfo.factoryPICEmail = answer || ""; + } + // 공급품목 (첫 번째 것만 가져오기) + else if (code.startsWith("1-5") && !factoryInfo.mainSupplyItems) { + try { + const supplyItems = JSON.parse(answer || "[]"); + if (Array.isArray(supplyItems) && supplyItems.length > 0) { + factoryInfo.mainSupplyItems = supplyItems[0].name || supplyItems[0] || ""; + } + } catch { + factoryInfo.mainSupplyItems = answer || ""; + } + } + // 실사 결과 + else if (code.startsWith("4-") && answer && !factoryInfo.inspectionResult) { + factoryInfo.inspectionResult = answer; + factoryInfo.inspectionFiles = files; + } + }); + } + }); + + return factoryInfo; + }; + + // PQ 제출 데이터에서 특정 코드의 답변 가져오기 + const getPQAnswerByCode = (targetCode: string) => { + if (!pqSubmissionData || !Array.isArray(pqSubmissionData)) { + return ""; + } + + for (const submission of pqSubmissionData) { + if (submission && submission.answers && Array.isArray(submission.answers)) { + const answer = submission.answers.find((a: any) => a.criteriaCode === targetCode); + if (answer) { + return answer.answer; + } + } + } + return ""; + }; + + // PQ 제출 데이터에서 특정 코드의 첨부파일 가져오기 + const getPQAttachmentsByCode = (targetCode: string) => { + const files: any[] = []; + + if (!pqSubmissionData || !Array.isArray(pqSubmissionData)) { + return files; + } + + for (const submission of pqSubmissionData) { + if (submission && submission.answers && Array.isArray(submission.answers)) { + const answer = submission.answers.find((a: any) => a.criteriaCode === targetCode); + if (answer && answer.uploadedFiles) { + files.push(...answer.uploadedFiles); + } + } + } + + return files; + }; + + // 첨부파일 관리 핸들러 (타입별) + 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 ( +
+
+

{noDataString}

+
+
+ ); + } + + // attachmentsByType는 상태로 관리되고 있으므로 제거 + + return ( +
+
+

협력업체 기본정보

+
+ {editMode ? ( + <> + + + + ) : ( + <> + + + + )} +
+
+ +
+ {/* 업체정보 */} + + updateField("vendorName", value)} + /> + {/* */} + updateField("phone", value)} + /> + {/* updateField("fax", value)} + /> */} + updateField("businessType", value)} + /> + {/* handleFileManagement("소개자료")} + /> */} + +
+ } + column2={ +
+ + + + + + updateField("email", value)} + /> + {/* + updateField("businessSize", value)} + placeholder="기업규모를 선택하세요" + /> */} + + {/* */} +
+ } + column3={ +
+ + {/* */} + updateField("country", value)} + /> + + {/* */} + + {/* */} +
+ } + /> + + {/* 첨부파일 */} + + {/* 사업자등록증 */} +
+
사업자등록증
+
+
+ {attachmentsByType.BUSINESS_REGISTRATION?.length || 0}건 +
+ {attachmentsByType.BUSINESS_REGISTRATION?.length > 0 && ( + + )} +
+
+ + {/* 신용평가보고서 */} +
+
신용평가보고서
+
+
+ {attachmentsByType.CREDIT_REPORT?.length || 0}건 +
+ {attachmentsByType.CREDIT_REPORT?.length > 0 && ( + + )} +
+
+ + {/* 통장사본 */} +
+
통장사본
+
+
+ {attachmentsByType.BANK_ACCOUNT_COPY?.length || 0}건 +
+ {attachmentsByType.BANK_ACCOUNT_COPY?.length > 0 && ( + + )} +
+
+ + {/* ISO 인증서 */} +
+
ISO 인증서
+
+
+ {attachmentsByType.ISO_CERTIFICATION?.length || 0}건 +
+ {attachmentsByType.ISO_CERTIFICATION?.length > 0 && ( + + )} +
+
+ + {/* 기타 첨부파일 (GENERAL) */} +
+
기타 첨부파일
+
+
+ {attachmentsByType.GENERAL?.length || 0}건 +
+ {attachmentsByType.GENERAL?.length > 0 && ( + + )} +
+
+
+ } + /> + + + {/* 상세정보 */} + + updateField("representativeName", value)} + /> + + updateField("representativeWorkExperience", value) + } + /> + updateField("representativeBirth", value)} + /> + + + } + column2={ +
+ updateField("representativePhone", value)} + /> + updateField("address", value)} + /> + +
+ } + column3={ +
+ updateField("representativeEmail", value)} + /> + +
+ } + additionalContent={ +
+ s.answers || []) : []} + onChange={(field, value) => { + // TODO: 조직도 업데이트 로직 구현 + }} + /> +
+
+ 관련 첨부파일 +
+
+ + + +
+
+
+ } + /> + + {/* */} + + {/* 매출정보 */} + + + + + 기준일 + + + 자산 구성 + + + 영업이익 +
+ (백만원) +
+ + 당기순이익 +
+ (백만원) +
+ + 부채비율 +
+ (%) +
+ + 차입금의존도 +
+ (%) +
+ + 영업이익률 +
+ (%) +
+ + 순이익률 +
+ (%) +
+ + 매출액증감 +
+ (%) +
+ + 유동비율 +
+ (%) +
+
+ + 총자산 + + 부채총계 + + + 자본총계 + + +
+ + {["20231231", "20221231", "20211231"].map((dateKey) => { + const year = dateKey; + const salesData = initialData.salesInfo?.[year]; + const metricsData = initialData.calculatedMetrics?.[dateKey]; + + return ( + + + {year} + + + {salesData + ? ( + parseInt(salesData.totalDebt.replace(/,/g, "")) + + parseInt(salesData.totalEquity.replace(/,/g, "")) + ).toLocaleString() + : "-"} + + + {salesData?.totalDebt || "-"} + + + {salesData?.totalEquity || "-"} + + + {salesData?.operatingProfit || "-"} + + + {salesData?.netIncome || "-"} + + + {metricsData?.debtRatio?.toFixed(1) || "-"} + + + {metricsData?.borrowingDependency?.toFixed(1) || "-"} + + + {metricsData?.operatingMargin?.toFixed(1) || "-"} + + + {metricsData?.netMargin?.toFixed(1) || "-"} + + + {metricsData?.salesGrowth?.toFixed(1) || "-"} + + + {metricsData?.currentRatio?.toFixed(1) || "-"} + + + ); + })} + + + } + /> + + {/* */} + + {/* 실사정보 */} + {pqSubmissionData && pqSubmissionData.length > 0 && pqSubmissionData.map((submission, index) => { + const factoryInfo = extractFactoryInfo([{ + groupName: "Factory Info", + items: submission.answers.map((answer: any) => ({ + code: answer.criteriaCode, + answer: answer.answer, + uploadedFiles: answer.uploadedFiles || [] + })) + }]); + const inspectionFiles = getPQAttachmentsByCode("4-1"); + + return ( + + + + + + } + column2={ +
+ + + {inspectionFiles.length > 0 && ( + + )} +
+ } + column3={ +
+
+ +
+
+ } + additionalContent={ +
+
+
+ 공장소개자료 +
+
+ {getPQAttachmentsByCode("1-4").length}건 +
+ {getPQAttachmentsByCode("1-4").length > 0 && ( + + )} +
+
+
+ QMS Cert +
+
+ {getPQAttachmentsByCode("2-1").length}건 +
+ {getPQAttachmentsByCode("2-1").length > 0 && ( + + )} +
+
+
+ Product Cert +
+
+ {getPQAttachmentsByCode("2-2").length}건 +
+ {getPQAttachmentsByCode("2-2").length > 0 && ( + + )} +
+
+
+ Ex. Cert +
+
+ {getPQAttachmentsByCode("2-17").length}건 +
+ {getPQAttachmentsByCode("2-17").length > 0 && ( + + )} +
+
+
+ HSE Cert +
+
+ {getPQAttachmentsByCode("3-1").length}건 +
+ {getPQAttachmentsByCode("3-1").length > 0 && ( + + )} +
+
+ } + /> + ); + })} + {/* 추가정보 */} + + + + + + } + column2={ +
+ + + +
+ } + column3={ +
+ {/* 추가 정보가 더 있다면 여기에 배치 */} +
+ } + /> + {/* 업무담당자 */} + +
+
영업 담당자
+ c.contactType === "sales")?.contactName || ""} type="readonly" /> + c.contactType === "sales")?.position || ""} type="readonly" /> + c.contactType === "sales")?.department || ""} type="readonly" /> + c.contactType === "sales")?.responsibility || ""} type="readonly" /> + c.contactType === "sales")?.email || ""} type="readonly" /> +
+ + } + column2={ +
+
+
설계 담당자
+ c.contactType === "design")?.contactName || ""} type="readonly" /> + c.contactType === "design")?.position || ""} type="readonly" /> + c.contactType === "design")?.department || ""} type="readonly" /> + c.contactType === "design")?.responsibility || ""} type="readonly" /> + c.contactType === "design")?.email || ""} type="readonly" /> +
+
+
납기 담당자
+ c.contactType === "delivery")?.contactName || ""} type="readonly" /> + c.contactType === "delivery")?.position || ""} type="readonly" /> + c.contactType === "delivery")?.department || ""} type="readonly" /> + c.contactType === "delivery")?.responsibility || ""} type="readonly" /> + c.contactType === "delivery")?.email || ""} type="readonly" /> +
+
+ } + column3={ +
+
+
품질 담당자
+ c.contactType === "quality")?.contactName || ""} type="readonly" /> + c.contactType === "quality")?.position || ""} type="readonly" /> + c.contactType === "quality")?.department || ""} type="readonly" /> + c.contactType === "quality")?.responsibility || ""} type="readonly" /> + c.contactType === "quality")?.email || ""} type="readonly" /> +
+
+
세금계산서 담당자
+ c.contactType === "tax_invoice")?.contactName || ""} type="readonly" /> + c.contactType === "tax_invoice")?.position || ""} type="readonly" /> + c.contactType === "tax_invoice")?.department || ""} type="readonly" /> + c.contactType === "tax_invoice")?.responsibility || ""} type="readonly" /> + c.contactType === "tax_invoice")?.email || ""} type="readonly" /> +
+
+ } + /> + + + + + ); +} diff --git a/lib/vendor-basic-info/constants.ts b/lib/vendor-basic-info/constants.ts new file mode 100644 index 00000000..d16f791f --- /dev/null +++ b/lib/vendor-basic-info/constants.ts @@ -0,0 +1 @@ +export const noDataString = "-"; \ No newline at end of file diff --git a/lib/vendor-basic-info/types.ts b/lib/vendor-basic-info/types.ts new file mode 100644 index 00000000..ead3a44c --- /dev/null +++ b/lib/vendor-basic-info/types.ts @@ -0,0 +1,180 @@ +export interface VendorContact { + id: number; + contactName: string; + contactPosition: string; + contactEmail: string; + contactPhone: string; + isPrimary: boolean; +} + +export interface VendorAttachment { + id: number; + fileName: string; + filePath: string; + attachmentType: string; + createdAt: string; +} + +export interface VendorProcessInfo { + processCount: number; + processPIC: string; + processApprovalDate: string; + implementationApproval: string; +} + +export interface VendorContractInfo { + contractRegistrationNumber: string; + contractPeriod: string; + lastEquipmentInspection: string; +} + +export interface VendorSalesData { + totalSales: string; + totalDebt: string; + totalEquity: string; + operatingProfit: string; + netIncome: string; +} + +export interface VendorAdditionalInfo { + postalCode: string; + detailAddress: string; + mainBusiness: string; + employeeCount: number; + businessType: string; +} + +export interface VendorOrganization { + representative: number; + sales: number; + design: number; + procurement: number; + production: number; + quality: number; +} + +export interface VendorFactoryInfo { + factoryAddress: string; + factoryEstablishmentDate: string; + factoryPIC: string; + factoryPICContact: string; + factoryPICEmail: string; +} + +export interface VendorInspectionInfo { + inspectionResult: string; + inspectionDate: string; + inspectionReportUrl?: string; +} + +export interface VendorEvaluationInfo { + regularEvaluationGrade: string; + safetyQualificationEvaluation: string; + companyTransactionRatio: string; +} + +export interface VendorClassificationInfo { + vendorClassification: string; + groupCompany: string; + preferredLanguage: string; + industryType: string; + isoCertification: string; +} + +export interface VendorContractDetails { + regularRegistrationStatus: string; + preferredContractTerms: string; + recentTransactionStatus: string; + compliancePledgeDate: string; + technicalDataDate: string; + confidentialityDate: string; + gtcDate: string; + standardSubcontractDate: string; + safetyHealthDate: string; + directMaterialDate: string; + domesticLCDate: string; + mutualGrowthDate: string; + ethicsDate: string; +} + +export interface VendorCapacityInfo { + annualSales: string; + productionCapacity: string; + mainSupplyItems: string; +} + +export interface VendorCalculatedMetrics { + debtRatio: number; + borrowingDependency: number; + operatingMargin: number; + netMargin: number; + salesGrowth: number; + currentRatio: number; +} + +export interface VendorData { + id: number; + vendorName: string; + vendorCode: string; + taxId: string; + address: string; + addressDetail: string; + postalCode: string; + businessSize: string; + country: string; + phone: string; + fax: string; + email: string; + website: string; + status: string; + representativeName: string; + representativeBirth: string; + representativeEmail: string; + representativePhone: string; + representativeWorkExperience: boolean; + corporateRegistrationNumber: string; + creditAgency: string; + creditRating: string; + cashFlowRating: string; + createdAt: string; + updatedAt: string; + contacts: VendorContact[]; + attachments: VendorAttachment[]; + processInfo: VendorProcessInfo; + contractInfo: VendorContractInfo; + salesInfo: { + [year: string]: VendorSalesData; + }; + additionalInfo: VendorAdditionalInfo; + organization: VendorOrganization; + factoryInfo: VendorFactoryInfo; + inspectionInfo: VendorInspectionInfo; + evaluationInfo: VendorEvaluationInfo; + classificationInfo: VendorClassificationInfo; + contractDetails: VendorContractDetails; + capacityInfo: VendorCapacityInfo; + calculatedMetrics: { + [year: string]: VendorCalculatedMetrics; + }; +} + +export interface VendorFormData { + vendorName: string; + representativeName: string; + representativeWorkExperience: boolean; + representativeBirth: string; + representativePhone: string; + representativeEmail: string; + addressDetail: string; + postalCode: string; + phone: string; + fax: string; + email: string; + address: string; + businessSize: string; + country: string; + website: string; + businessType: string; + employeeCount: number; + mainBusiness: string; +} \ No newline at end of file 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 7446716b..8d21df24 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 @@ -214,6 +214,7 @@ export function getColumns(): ColumnDef[] { open={documentDialogOpen} onOpenChange={setDocumentDialogOpen} registration={registration} + isVendorUser={false} /> ) diff --git a/lib/vendors/service.ts b/lib/vendors/service.ts index e3a38891..5d790a6e 100644 --- a/lib/vendors/service.ts +++ b/lib/vendors/service.ts @@ -2779,10 +2779,8 @@ export async function requestPQVendors(input: ApproveVendorsInput & { ? `[eVCP] You are invited to submit Non-Inspection PQ ${vendorPQ?.pqNumber || ""}` : `[eVCP] You are invited to submit PQ ${vendorPQ?.pqNumber || ""}`; - const baseLoginUrl = `${host}/partners/pq`; - const loginUrl = input.projectId - ? `${baseLoginUrl}?projectId=${input.projectId}` - : baseLoginUrl; + const baseUrl = process.env.NEXT_PUBLIC_URL || `http://${host}`; + const loginUrl = `${baseUrl}/partners/pq_new`; // 체크된 계약 항목 배열 생성 const contracts = input.agreements @@ -2807,6 +2805,8 @@ export async function requestPQVendors(input: ApproveVendorsInput & { } } + console.log("loginUrl-pq", loginUrl); + await sendEmail({ to: vendor.email, subject, @@ -2959,8 +2959,9 @@ export async function requestBasicContractInfo({ const headersList = await headers(); const host = headersList.get('host') || 'localhost:3000'; // 로그인 또는 서명 페이지 URL 생성 - const baseUrl = `http://${host}` + const baseUrl = process.env.NEXT_PUBLIC_URL || `http://${host}`; const loginUrl = `${baseUrl}/partners/basic-contract`; + console.log("loginUrl-basic-contract", loginUrl); // 사용자 언어 설정 (기본값은 한국어) const userLang = "ko"; -- cgit v1.2.3