diff options
Diffstat (limited to 'lib/vendor-basic-info/basic-info-client.tsx')
| -rw-r--r-- | lib/vendor-basic-info/basic-info-client.tsx | 1650 |
1 files changed, 1650 insertions, 0 deletions
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 ( + <div className="flex items-center space-x-2"> + <Checkbox + id={fieldKey} + checked={value as boolean} + onCheckedChange={(checked) => onChange?.(checked)} + /> + <Label + htmlFor={fieldKey} + className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" + > + {value ? "있음" : "없음"} + </Label> + </div> + ); + + case "dropdown": + return ( + <Select + value={displayValue as string} + onValueChange={(val) => onChange?.(val)} + > + <SelectTrigger className="h-8"> + <SelectValue placeholder={placeholder || "선택하세요"} /> + </SelectTrigger> + <SelectContent> + {options.map((option) => ( + <SelectItem key={option.value} value={option.value}> + {option.label} + </SelectItem> + ))} + </SelectContent> + </Select> + ); + + case "file-button": + return ( + <Button + type="button" + variant="outline" + className="h-8 text-xs" + onClick={onFileButtonClick} + > + {displayValue || "파일 관리"} + </Button> + ); + + case "text": + default: + return ( + <Input + id={fieldKey} + value={displayValue as string} + onChange={(e) => onChange?.(e.target.value)} + className="h-8" + placeholder={placeholder} + /> + ); + } + }; + + const renderReadOnlyField = () => { + switch (type) { + case "checkbox": + return ( + <div className="flex items-center space-x-2"> + <Checkbox + id={`readonly-${fieldKey}`} + checked={value as boolean} + disabled={true} + /> + <Label + htmlFor={`readonly-${fieldKey}`} + className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" + > + {value ? "있음" : "없음"} + </Label> + </div> + ); + + case "file-button": + return ( + <Button + type="button" + variant="ghost" + className="h-auto p-0 text-xs text-left justify-start" + onClick={onFileButtonClick} + disabled={!onFileButtonClick} + > + {displayValue || noDataString} + </Button> + ); + + case "dropdown": + case "text": + case "readonly": + default: + return showNoData ? noDataString : displayValue; + } + }; + + return ( + <div className="grid grid-cols-2 gap-2 py-1 min-w-0"> + <div className="text-sm text-muted-foreground break-words">{title}:</div> + <div className="text-sm font-medium space-y-1 break-words overflow-hidden min-w-0"> + {canEdit ? ( + <div className="space-y-1"> + <Label htmlFor={fieldKey} className="sr-only"> + {title} + </Label> + {renderEditableField()} + </div> + ) : ( + <span>{renderReadOnlyField()}</span> + )} + </div> + </div> + ); +}; + +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 ( + <div className="space-y-4"> + <div className="text-sm font-semibold text-center">조직도</div> + <div className="grid grid-cols-3 gap-4"> + {organizationFields.map((field) => ( + <div key={field.key} className="flex flex-col items-center space-y-2"> + <div className="text-sm font-medium text-center whitespace-nowrap"> + {field.label} + </div> + <div className="text-center"> + {editMode ? ( + <Input + value={data?.[field.key]?.toString() || ""} + onChange={(e) => onChange?.(field.key, e.target.value)} + className="h-8 w-16 text-center" + placeholder="0" + /> + ) : ( + <span className="text-sm text-muted-foreground"> + {pqData && Array.isArray(pqData) && pqData.find((a: any) => a.criteriaCode === field.code)?.answer || data?.[field.key]?.toString() || noDataString} + </span> + )} + </div> + </div> + ))} + </div> + </div> + ); +}; + +const InfoSection = ({ + title, + subtitle, + column1, + column2, + column3, + additionalContent, +}: { + title: string; + subtitle?: string; + column1: React.ReactNode; + column2: React.ReactNode; + column3: React.ReactNode; + additionalContent?: React.ReactNode; +}) => ( + <div className="border"> + <div className="flex"> + <div className="w-32 bg-muted p-4 border-r flex flex-col"> + <div className="text-sm font-semibold text-center w-full">{title}</div> + {subtitle && ( + <div className="text-sm text-muted-foreground text-center w-full mt-1"> + {subtitle} + </div> + )} + </div> + <div className="flex-1 grid grid-cols-3 min-w-0"> + <div className="p-4 border-r min-w-0 overflow-hidden">{column1}</div> + <div className="p-4 border-r min-w-0 overflow-hidden">{column2}</div> + <div className="p-4 min-w-0 overflow-hidden">{column3}</div> + </div> + </div> + {additionalContent && ( + <div className="flex"> + <div className="w-32 bg-muted border-r"></div> + <div className="flex-1 p-4 border-t">{additionalContent}</div> + </div> + )} + </div> +); + +const WideInfoSection = ({ + title, + subtitle, + content, + noPadding = false, +}: { + title?: string; + subtitle?: string; + content: React.ReactNode; + noPadding?: boolean; +}) => ( + <div className="border"> + <div className="flex"> + <div className="w-32 bg-muted p-4 border-r flex flex-col"> + <div className="text-sm font-semibold text-center w-full">{title}</div> + {subtitle && ( + <div className="text-sm text-muted-foreground text-center w-full mt-1"> + {subtitle} + </div> + )} + </div> + <div className={`flex-1 min-w-0 overflow-x-auto ${noPadding ? '' : 'p-4'}`}> + {content} + </div> + </div> + </div> +); + +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<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 [pqData, setPqData] = useState<any[]>([]); + const [pqSubmissionData, setPqSubmissionData] = useState<any[]>([]); + const [additionalInfo, setAdditionalInfo] = useState<any>(null); + const [businessContacts, setBusinessContacts] = useState<any[]>([]); + const [formData, setFormData] = useState<VendorFormData>({ + 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 ( + <div className="p-6 bg-background max-w-full"> + <div className="text-center py-8"> + <p className="text-muted-foreground">{noDataString}</p> + </div> + </div> + ); + } + + // attachmentsByType는 상태로 관리되고 있으므로 제거 + + return ( + <div className="p-6 bg-background w-full overflow-x-auto"> + <div className="mb-6 flex justify-between items-center"> + <h2 className="text-xl font-bold">협력업체 기본정보</h2> + <div className="flex gap-2"> + {editMode ? ( + <> + <Button + onClick={handleSave} + disabled={isPending} + className="flex items-center gap-1" + > + <Save className="w-4 h-4" /> + 저장 + </Button> + <Button + variant="outline" + onClick={handleCancel} + disabled={isPending} + className="flex items-center gap-1" + > + <X className="w-4 h-4" /> + 취소 + </Button> + </> + ) : ( + <> + <Button + onClick={() => setEditMode(true)} + className="flex items-center gap-1" + > + <Edit className="w-4 h-4" /> + 수정 + </Button> + <Button + variant="outline" + onClick={handleContractView} + className="flex items-center gap-1" + > + 정규업체등록 현황 + </Button> + </> + )} + </div> + </div> + + <div className="space-y-4"> + {/* 업체정보 */} + <InfoSection + title="업체정보" + column1={ + <div className="space-y-2"> + <InfoItem + title="업체명" + value={formData.vendorName} + isEditable={true} + editMode={editMode} + fieldKey="vendorName" + onChange={(value) => updateField("vendorName", value)} + /> + {/* <InfoItem + title="설립일" + // 현재 필드 없고 linter error 나도 무시. createdAt은 데이터베이스 생성시점이므로 잘못된 필드. + value={initialData.establishmentDate} + type="readonly" + /> */} + <InfoItem + title="대표전화" + value={formData.phone} + isEditable={true} + editMode={editMode} + fieldKey="phone" + onChange={(value) => updateField("phone", value)} + /> + {/* <InfoItem + title="팩스" + value={formData.fax} + isEditable={true} + editMode={editMode} + fieldKey="fax" + onChange={(value) => updateField("fax", value)} + /> */} + <InfoItem + title="업체유형" + value={formData.businessType} + isEditable={true} + editMode={editMode} + fieldKey="businessType" + onChange={(value) => updateField("businessType", value)} + /> + {/* <InfoItem + title="소개자료" + value={`회사: ${ + attachmentsByType.COMPANY_INTRO?.length || 0 + }건 / 제품: ${attachmentsByType.PRODUCT_INTRO?.length || 0}건`} + isEditable={true} + editMode={editMode} + type="file-button" + onFileButtonClick={() => handleFileManagement("소개자료")} + /> */} + <InfoItem + title="정기평가 등급" + value={periodicGrade || ""} + type="readonly" + /> + </div> + } + column2={ + <div className="space-y-2"> + <InfoItem + title="사업자번호" + value={initialData.taxId} + type="readonly" + /> + <InfoItem + title="법인등록번호" + value={initialData.corporateRegistrationNumber} + type="readonly" + /> + <InfoItem + title="회사주소" + value={initialData.address || ""} + type="readonly" + /> + <InfoItem + title="상세주소" + value={initialData.addressDetail || ""} + type="readonly" + /> + <InfoItem + title="우편번호" + value={initialData.postalCode || ""} + type="readonly" + /> + <InfoItem + title="E-mail" + value={formData.email} + isEditable={true} + editMode={editMode} + fieldKey="email" + onChange={(value) => updateField("email", value)} + /> + {/* <InfoItem + title="사업유형" + value={formData.businessType} + isEditable={true} + /> + <InfoItem + title="기업규모" + value={formData.businessSize} + isEditable={true} + editMode={editMode} + fieldKey="businessSize" + type="dropdown" + options={[ + { value: "A", label: "대기업 (A)" }, + { value: "B", label: "중견기업 (B)" }, + { value: "C", label: "중소기업 (C)" }, + { value: "D", label: "소상공인 (D)" }, + ]} + onChange={(value) => updateField("businessSize", value)} + placeholder="기업규모를 선택하세요" + /> */} + + {/* <InfoItem + title="안전적격성평가" + value={ + initialData.evaluationInfo?.safetyQualificationEvaluation || + null + } + type="readonly" + /> */} + </div> + } + column3={ + <div className="space-y-2"> + <InfoItem + title="업체분류" + value={vendorTypeInfo?.vendorTypeName || ""} + type="readonly" + /> + {/* <InfoItem + title="그룹사" + value={initialData.classificationInfo?.groupCompany || null} + isEditable={true} + /> */} + <InfoItem + title="국가" + value={formData.country} + isEditable={true} + editMode={editMode} + fieldKey="country" + onChange={(value) => updateField("country", value)} + /> + <InfoItem + title="선호언어" + value={ + initialData.classificationInfo?.preferredLanguage || "" + } + isEditable={true} + /> + {/* <InfoItem + title="산업유형" + value={initialData.classificationInfo?.industryType || ""} + isEditable={true} + /> */} + + {/* <InfoItem + title="당사거래비중" + value={ + initialData.evaluationInfo?.companyTransactionRatio || "" + } + type="readonly" + /> */} + </div> + } + /> + + {/* 첨부파일 */} + <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> + } + /> + + + {/* 상세정보 */} + <InfoSection + title="상세정보" + column1={ + <div className="space-y-2"> + <InfoItem + title="대표자명" + value={formData.representativeName} + isEditable={true} + editMode={editMode} + fieldKey="representativeName" + onChange={(value) => updateField("representativeName", value)} + /> + <InfoItem + title="대표자 당사근무경험" + value={formData.representativeWorkExperience} + isEditable={true} + editMode={editMode} + fieldKey="representativeWorkExperience" + type="checkbox" + onChange={(value) => + updateField("representativeWorkExperience", value) + } + /> + <InfoItem + title="대표자 생년월일" + value={formData.representativeBirth || ""} + isEditable={true} + editMode={editMode} + fieldKey="representativeBirth" + onChange={(value) => updateField("representativeBirth", value)} + /> + <InfoItem + title="임직원수" + value={getPQAnswerByCode("1-8-3") || formData.employeeCount.toString()} + type="readonly" + /> + </div> + } + column2={ + <div className="space-y-2"> + <InfoItem + title="대표자Tel" + value={formData.representativePhone} + isEditable={true} + editMode={editMode} + fieldKey="representativePhone" + onChange={(value) => updateField("representativePhone", value)} + /> + <InfoItem + title="대표자 주소" + value={formData.address || ""} + isEditable={true} + editMode={editMode} + fieldKey="address" + onChange={(value) => updateField("address", value)} + /> + <InfoItem + title="연간 매출" + value={getPQAnswerByCode("1-7-1") || getPQAnswerByCode("1-7-2") || initialData.capacityInfo?.annualSales || ""} + type="readonly" + /> + </div> + } + column3={ + <div className="space-y-2"> + <InfoItem + title="대표자 E-mail" + value={formData.representativeEmail} + isEditable={true} + editMode={editMode} + fieldKey="representativeEmail" + onChange={(value) => updateField("representativeEmail", value)} + /> + <InfoItem + title="생산능력" + value={getPQAnswerByCode("1-9-1") || getPQAnswerByCode("1-9-2") || initialData.capacityInfo?.productionCapacity || ""} + type="readonly" + /> + </div> + } + additionalContent={ + <div className="grid grid-cols-2 gap-8 py-4 min-w-0 overflow-x-auto"> + <OrganizationChart + data={initialData.organization} + editMode={editMode} + pqData={pqSubmissionData && Array.isArray(pqSubmissionData) ? pqSubmissionData.flatMap(s => s.answers || []) : []} + onChange={(field, value) => { + // TODO: 조직도 업데이트 로직 구현 + }} + /> + <div className="flex flex-col items-center gap-3"> + <div className="text-sm font-semibold text-center"> + 관련 첨부파일 + </div> + <div className="space-y-2"> + <Button + variant="outline" + className="text-xs w-32 flex items-center gap-2" + onClick={() => { + const files = getPQAttachmentsByCode("1-10"); + if (files.length > 0) { + files.forEach((file, index) => { + setTimeout(() => { + handleAttachmentDownload(file.filePath, file.fileName); + }, index * 500); + }); + } else { + toast.info("협력업체정보 파일이 없습니다."); + } + }} + > + 협력업체정보 ({getPQAttachmentsByCode("1-10").length}건) + </Button> + <Button + variant="outline" + className="text-xs w-32 flex items-center gap-2" + onClick={() => { + const files = getPQAttachmentsByCode("1-12"); + if (files.length > 0) { + files.forEach((file, index) => { + setTimeout(() => { + handleAttachmentDownload(file.filePath, file.fileName); + }, index * 500); + }); + } else { + toast.info("외주화정보 파일이 없습니다."); + } + }} + > + 외주화정보 ({getPQAttachmentsByCode("1-12").length}건) + </Button> + <Button + variant="outline" + className="text-xs w-32 flex items-center gap-2" + onClick={() => { + const files = getPQAttachmentsByCode("1-13"); + if (files.length > 0) { + files.forEach((file, index) => { + setTimeout(() => { + handleAttachmentDownload(file.filePath, file.fileName); + }, index * 500); + }); + } else { + toast.info("A/S 네트워크 파일이 없습니다."); + } + }} + > + A/S 네트워크 ({getPQAttachmentsByCode("1-13").length}건) + </Button> + </div> + </div> + </div> + } + /> + + {/* <Separator /> */} + + {/* 매출정보 */} + <WideInfoSection + title="매출정보" + subtitle="(3개년)" + noPadding={true} + content={ + <Table> + <TableHeader> + <TableRow> + <TableHead + rowSpan={2} + className="text-center border-r align-middle" + > + 기준일 + </TableHead> + <TableHead colSpan={3} className="text-center border-r"> + 자산 구성 + </TableHead> + <TableHead + rowSpan={2} + className="text-center border-r align-middle" + > + 영업이익 + <br /> + (백만원) + </TableHead> + <TableHead + rowSpan={2} + className="text-center border-r align-middle" + > + 당기순이익 + <br /> + (백만원) + </TableHead> + <TableHead + rowSpan={2} + className="text-center border-r align-middle" + > + 부채비율 + <br /> + (%) + </TableHead> + <TableHead + rowSpan={2} + className="text-center border-r align-middle" + > + 차입금의존도 + <br /> + (%) + </TableHead> + <TableHead + rowSpan={2} + className="text-center border-r align-middle" + > + 영업이익률 + <br /> + (%) + </TableHead> + <TableHead + rowSpan={2} + className="text-center border-r align-middle" + > + 순이익률 + <br /> + (%) + </TableHead> + <TableHead + rowSpan={2} + className="text-center border-r align-middle" + > + 매출액증감 + <br /> + (%) + </TableHead> + <TableHead rowSpan={2} className="text-center align-middle"> + 유동비율 + <br /> + (%) + </TableHead> + </TableRow> + <TableRow> + <TableHead className="text-center border-r">총자산</TableHead> + <TableHead className="text-center border-r"> + 부채총계 + </TableHead> + <TableHead className="text-center border-r"> + 자본총계 + </TableHead> + </TableRow> + </TableHeader> + <TableBody> + {["20231231", "20221231", "20211231"].map((dateKey) => { + const year = dateKey; + const salesData = initialData.salesInfo?.[year]; + const metricsData = initialData.calculatedMetrics?.[dateKey]; + + return ( + <TableRow key={dateKey}> + <TableCell className="text-center font-medium border-r bg-yellow-50"> + {year} + </TableCell> + <TableCell className="text-right border-r"> + {salesData + ? ( + parseInt(salesData.totalDebt.replace(/,/g, "")) + + parseInt(salesData.totalEquity.replace(/,/g, "")) + ).toLocaleString() + : "-"} + </TableCell> + <TableCell className="text-right border-r"> + {salesData?.totalDebt || "-"} + </TableCell> + <TableCell className="text-right border-r"> + {salesData?.totalEquity || "-"} + </TableCell> + <TableCell className="text-right border-r"> + {salesData?.operatingProfit || "-"} + </TableCell> + <TableCell className="text-right border-r"> + {salesData?.netIncome || "-"} + </TableCell> + <TableCell className="text-right border-r"> + {metricsData?.debtRatio?.toFixed(1) || "-"} + </TableCell> + <TableCell className="text-right border-r"> + {metricsData?.borrowingDependency?.toFixed(1) || "-"} + </TableCell> + <TableCell className="text-right border-r"> + {metricsData?.operatingMargin?.toFixed(1) || "-"} + </TableCell> + <TableCell className="text-right border-r"> + {metricsData?.netMargin?.toFixed(1) || "-"} + </TableCell> + <TableCell className="text-right border-r"> + {metricsData?.salesGrowth?.toFixed(1) || "-"} + </TableCell> + <TableCell className="text-right"> + {metricsData?.currentRatio?.toFixed(1) || "-"} + </TableCell> + </TableRow> + ); + })} + </TableBody> + </Table> + } + /> + + {/* <Separator /> */} + + {/* 실사정보 */} + {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 ( + <InfoSection + key={submission.submission.id} + title="실사정보" + subtitle={`${submission.submission.type || "일반"} - ${submission.submission.status || "상태없음"}`} + column1={ + <div className="space-y-2"> + <InfoItem + title="공장주소" + value={factoryInfo.factoryAddress || ""} + type="readonly" + /> + <InfoItem + title="공장 전화" + value={factoryInfo.factoryPhone || ""} + type="readonly" + /> + <InfoItem + title="공장 팩스" + value={factoryInfo.factoryFax || ""} + type="readonly" + /> + </div> + } + column2={ + <div className="space-y-2"> + <InfoItem + title="공장 담당자" + value={ + factoryInfo.factoryPIC + ? `${factoryInfo.factoryPIC} [${factoryInfo.factoryPICPosition || ""}] [${factoryInfo.factoryPICContact || ""}] [${factoryInfo.factoryPICEmail || ""}]` + : "" + } + type="readonly" + /> + <InfoItem + title="실사결과" + value={factoryInfo.inspectionResult || ""} + type="readonly" + /> + {inspectionFiles.length > 0 && ( + <Button + variant="outline" + size="sm" + onClick={() => { + inspectionFiles.forEach((file, idx) => { + setTimeout(() => { + handleAttachmentDownload(file.filePath, file.fileName); + }, idx * 500); + }); + }} + > + 실사결과 파일 다운로드 ({inspectionFiles.length}건) + </Button> + )} + </div> + } + column3={ + <div className="flex flex-col gap-2"> + <div className="space-y-2"> + <InfoItem + title="대표공급품목" + value={factoryInfo.mainSupplyItems || ""} + type="readonly" + /> + </div> + </div> + } + additionalContent={ + <div className="grid grid-cols-5 gap-4 min-w-0 overflow-x-auto"> + <div className="text-center min-w-0"> + <div className="text-sm font-medium mb-2 break-words"> + 공장소개자료 + </div> + <div className="text-sm text-muted-foreground"> + {getPQAttachmentsByCode("1-4").length}건 + </div> + {getPQAttachmentsByCode("1-4").length > 0 && ( + <Button + variant="outline" + size="sm" + className="mt-2" + onClick={() => { + const files = getPQAttachmentsByCode("1-4"); + files.forEach((file, idx) => { + setTimeout(() => { + handleAttachmentDownload(file.filePath, file.fileName); + }, idx * 500); + }); + }} + > + 파일 다운로드 + </Button> + )} + </div> + <div className="text-center min-w-0"> + <div className="text-sm font-medium mb-2 break-words"> + QMS Cert + </div> + <div className="text-sm text-muted-foreground"> + {getPQAttachmentsByCode("2-1").length}건 + </div> + {getPQAttachmentsByCode("2-1").length > 0 && ( + <Button + variant="outline" + size="sm" + className="mt-2" + onClick={() => { + const files = getPQAttachmentsByCode("2-1"); + files.forEach((file, idx) => { + setTimeout(() => { + handleAttachmentDownload(file.filePath, file.fileName); + }, idx * 500); + }); + }} + > + 파일 다운로드 + </Button> + )} + </div> + <div className="text-center min-w-0"> + <div className="text-sm font-medium mb-2 break-words"> + Product Cert + </div> + <div className="text-sm text-muted-foreground"> + {getPQAttachmentsByCode("2-2").length}건 + </div> + {getPQAttachmentsByCode("2-2").length > 0 && ( + <Button + variant="outline" + size="sm" + className="mt-2" + onClick={() => { + const files = getPQAttachmentsByCode("2-2"); + files.forEach((file, idx) => { + setTimeout(() => { + handleAttachmentDownload(file.filePath, file.fileName); + }, idx * 500); + }); + }} + > + 파일 다운로드 + </Button> + )} + </div> + <div className="text-center min-w-0"> + <div className="text-sm font-medium mb-2 break-words"> + Ex. Cert + </div> + <div className="text-sm text-muted-foreground"> + {getPQAttachmentsByCode("2-17").length}건 + </div> + {getPQAttachmentsByCode("2-17").length > 0 && ( + <Button + variant="outline" + size="sm" + className="mt-2" + onClick={() => { + const files = getPQAttachmentsByCode("2-17"); + files.forEach((file, idx) => { + setTimeout(() => { + handleAttachmentDownload(file.filePath, file.fileName); + }, idx * 500); + }); + }} + > + 파일 다운로드 + </Button> + )} + </div> + <div className="text-center min-w-0"> + <div className="text-sm font-medium mb-2 break-words"> + HSE Cert + </div> + <div className="text-sm text-muted-foreground"> + {getPQAttachmentsByCode("3-1").length}건 + </div> + {getPQAttachmentsByCode("3-1").length > 0 && ( + <Button + variant="outline" + size="sm" + className="mt-2" + onClick={() => { + const files = getPQAttachmentsByCode("3-1"); + files.forEach((file, idx) => { + setTimeout(() => { + handleAttachmentDownload(file.filePath, file.fileName); + }, idx * 500); + }); + }} + > + 파일 다운로드 + </Button> + )} + </div> + </div> + } + /> + ); + })} + {/* 추가정보 */} + <InfoSection + title="추가정보" + // subtitle="정규업체 등록 시 입력된 정보" + column1={ + <div className="space-y-2"> + <InfoItem + title="사업유형" + value={additionalInfo?.businessType || ""} + type="readonly" + /> + <InfoItem + title="산업유형" + value={additionalInfo?.industryType || ""} + type="readonly" + /> + <InfoItem + title="회사규모" + value={additionalInfo?.companySize || ""} + type="readonly" + /> + </div> + } + column2={ + <div className="space-y-2"> + <InfoItem + title="매출액" + value={additionalInfo?.revenue ? `${additionalInfo.revenue.toLocaleString()}원` : ""} + type="readonly" + /> + <InfoItem + title="공장설립일" + value={additionalInfo?.factoryEstablishedDate ? new Date(additionalInfo.factoryEstablishedDate).toLocaleDateString('ko-KR') : ""} + type="readonly" + /> + <InfoItem + title="선호계약조건" + value={additionalInfo?.preferredContractTerms || ""} + type="readonly" + /> + </div> + } + column3={ + <div className="space-y-2"> + {/* 추가 정보가 더 있다면 여기에 배치 */} + </div> + } + /> + {/* 업무담당자 */} + <InfoSection + title="업무담당자" + column1={ + <div className="space-y-3"> + <div className="space-y-1"> + <div className="text-sm font-medium">영업 담당자</div> + <InfoItem title="이름" value={businessContacts.find(c => c.contactType === "sales")?.contactName || ""} type="readonly" /> + <InfoItem title="직급" value={businessContacts.find(c => c.contactType === "sales")?.position || ""} type="readonly" /> + <InfoItem title="부서" value={businessContacts.find(c => c.contactType === "sales")?.department || ""} type="readonly" /> + <InfoItem title="담당업무" value={businessContacts.find(c => c.contactType === "sales")?.responsibility || ""} type="readonly" /> + <InfoItem title="E-mail" value={businessContacts.find(c => c.contactType === "sales")?.email || ""} type="readonly" /> + </div> + </div> + } + column2={ + <div className="space-y-3"> + <div className="space-y-1"> + <div className="text-sm font-medium">설계 담당자</div> + <InfoItem title="이름" value={businessContacts.find(c => c.contactType === "design")?.contactName || ""} type="readonly" /> + <InfoItem title="직급" value={businessContacts.find(c => c.contactType === "design")?.position || ""} type="readonly" /> + <InfoItem title="부서" value={businessContacts.find(c => c.contactType === "design")?.department || ""} type="readonly" /> + <InfoItem title="담당업무" value={businessContacts.find(c => c.contactType === "design")?.responsibility || ""} type="readonly" /> + <InfoItem title="E-mail" value={businessContacts.find(c => c.contactType === "design")?.email || ""} type="readonly" /> + </div> + <div className="space-y-1"> + <div className="text-sm font-medium">납기 담당자</div> + <InfoItem title="이름" value={businessContacts.find(c => c.contactType === "delivery")?.contactName || ""} type="readonly" /> + <InfoItem title="직급" value={businessContacts.find(c => c.contactType === "delivery")?.position || ""} type="readonly" /> + <InfoItem title="부서" value={businessContacts.find(c => c.contactType === "delivery")?.department || ""} type="readonly" /> + <InfoItem title="담당업무" value={businessContacts.find(c => c.contactType === "delivery")?.responsibility || ""} type="readonly" /> + <InfoItem title="E-mail" value={businessContacts.find(c => c.contactType === "delivery")?.email || ""} type="readonly" /> + </div> + </div> + } + column3={ + <div className="space-y-3"> + <div className="space-y-1"> + <div className="text-sm font-medium">품질 담당자</div> + <InfoItem title="이름" value={businessContacts.find(c => c.contactType === "quality")?.contactName || ""} type="readonly" /> + <InfoItem title="직급" value={businessContacts.find(c => c.contactType === "quality")?.position || ""} type="readonly" /> + <InfoItem title="부서" value={businessContacts.find(c => c.contactType === "quality")?.department || ""} type="readonly" /> + <InfoItem title="담당업무" value={businessContacts.find(c => c.contactType === "quality")?.responsibility || ""} type="readonly" /> + <InfoItem title="E-mail" value={businessContacts.find(c => c.contactType === "quality")?.email || ""} type="readonly" /> + </div> + <div className="space-y-1"> + <div className="text-sm font-medium">세금계산서 담당자</div> + <InfoItem title="이름" value={businessContacts.find(c => c.contactType === "tax_invoice")?.contactName || ""} type="readonly" /> + <InfoItem title="직급" value={businessContacts.find(c => c.contactType === "tax_invoice")?.position || ""} type="readonly" /> + <InfoItem title="부서" value={businessContacts.find(c => c.contactType === "tax_invoice")?.department || ""} type="readonly" /> + <InfoItem title="담당업무" value={businessContacts.find(c => c.contactType === "tax_invoice")?.responsibility || ""} type="readonly" /> + <InfoItem title="E-mail" value={businessContacts.find(c => c.contactType === "tax_invoice")?.email || ""} type="readonly" /> + </div> + </div> + } + /> + + </div> + <DocumentStatusDialog + open={contractDialogOpen} + onOpenChange={setContractDialogOpen} + registration={registrationData} + isVendorUser={false} + /> + </div> + ); +} |
