diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-01 09:09:15 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-01 09:09:15 +0000 |
| commit | 089c70ffbe2303ab5e2611a152ddd3aed0e6e718 (patch) | |
| tree | 1ce91012dba99495dde5eb8b414b2732197bfec4 /app | |
| parent | 69648a25c2ac62bbc3354b3a0e41abc932273b7c (diff) | |
(최겸) 구매 pq, 기본정보 수정
Diffstat (limited to 'app')
7 files changed, 3 insertions, 1718 deletions
diff --git a/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/actions.ts b/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/actions.ts deleted file mode 100644 index 866103a6..00000000 --- a/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/actions.ts +++ /dev/null @@ -1,44 +0,0 @@ -"use server"; - -import { getVendorBasicInfo } from "@/lib/vendors/service"; -import { VendorFormData } from "./types"; - -/** - * 벤더 기본정보를 가져오는 서버 액션 - */ -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; - } -} - -/** - * 벤더 기본정보를 업데이트하는 서버 액션 (향후 구현) - */ -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/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 deleted file mode 100644 index 78d21719..00000000 --- a/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/basic-info-client.tsx +++ /dev/null @@ -1,1458 +0,0 @@ -"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 { - 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 } 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, -}: { - data: any; - editMode?: boolean; - onChange?: (field: string, value: string) => void; -}) => { - const organizationFields = [ - { key: "representative", label: "대표" }, - { key: "sales", label: "영업" }, - { key: "design", label: "설계" }, - { key: "procurement", label: "구매" }, - { key: "production", label: "생산" }, - { key: "quality", label: "품질" }, - ]; - - 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"> - {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 [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 })); - }; - - // PQ 조회 핸들러 - const handlePQView = () => { - setPqDialogOpen(true); - }; - - // 실사 정보 조회 핸들러 - const handleSiteVisitView = async () => { - try { - const siteVisitRequests = await getSiteVisitRequestsByVendorId(parseInt(vendorId)); - if (siteVisitRequests.length === 0) { - toast.info("실사 정보가 없습니다."); - return; - } - setSelectedSiteVisitRequest(siteVisitRequests[0]); // 첫 번째 실사 정보 선택 - setSiteVisitDialogOpen(true); - } catch (error) { - console.error("실사 정보 조회 오류:", error); - toast.error("실사 정보를 불러오는데 실패했습니다."); - } - }; - - // 기본계약 현황 조회 핸들러 - 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 handleAdditionalInfoView = async () => { - try { - const result = await fetchVendorRegistrationStatus(parseInt(vendorId)); - if (!result.success || !result.data) { - toast.info("추가정보가 없습니다."); - return; - } - - // 추가정보가 있는지 확인 (업무담당자 또는 추가정보 데이터가 있는지 체크) - const { businessContacts, additionalInfo } = result.data; - const hasBusinessContacts = businessContacts && businessContacts.length > 0; - const hasAdditionalInfo = additionalInfo && Object.keys(additionalInfo).length > 0; - - if (!hasBusinessContacts && !hasAdditionalInfo) { - toast.info("추가정보가 없습니다."); - return; - } - - setAdditionalInfoDialogOpen(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); - } - } 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("파일 다운로드에 실패했습니다."); - } - }; - - // 첨부파일 관리 핸들러 (타입별) - 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> - )} - </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> - } - /> - - <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> - } - /> - - <Separator /> - - {/* 상세정보 */} - {/* <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 || null} - isEditable={true} - /> - <InfoItem - title="임직원수" - value={formData.employeeCount.toString() || null} - isEditable={true} - /> - </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 || null} - isEditable={true} - /> - <InfoItem - title="연간 매출" - value={initialData.capacityInfo?.annualSales || null} - isEditable={true} - /> - </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={initialData.capacityInfo?.productionCapacity || null} - isEditable={true} - /> - </div> - } - additionalContent={ - <div className="grid grid-cols-2 gap-8 py-4 min-w-0 overflow-x-auto"> - <OrganizationChart - data={initialData.organization} - editMode={editMode} - onChange={(field, value) => { - // TODO: 조직도 업데이트 로직 구현 - toast.info( - `[개발중] 조직도 ${field} 필드 업데이트 기능을 구현 예정입니다.` - ); - }} - /> - <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={() => handleFileManagement("협력업체정보")} - > - 협력업체정보 - </Button> - <Button - variant="outline" - className="text-xs w-32 flex items-center gap-2" - onClick={() => handleFileManagement("외주화정보")} - > - 외주화정보 - </Button> - <Button - variant="outline" - className="text-xs w-32 flex items-center gap-2" - onClick={() => handleFileManagement("A/S 네트워크")} - > - A/S 네트워크 - </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 /> */} - - {/* 실사정보 */} - {/* <InfoSection - title="실사정보" - subtitle="(3년)" - column1={ - <div className="space-y-2"> - <InfoItem - title="공장주소" - value={initialData.factoryInfo?.factoryAddress || null} - /> - <InfoItem - title="공장설립일" - value={ - initialData.factoryInfo?.factoryEstablishmentDate || null - } - /> - </div> - } - column2={ - <div className="space-y-2"> - <InfoItem - title="공장 담당자" - value={ - initialData.factoryInfo?.factoryPIC - ? `${initialData.factoryInfo.factoryPIC} [${ - initialData.factoryInfo.factoryPICContact || "" - }] [${initialData.factoryInfo.factoryPICEmail || ""}]` - : null - } - /> - <InfoItem - title="실사결과" - value={ - initialData.inspectionInfo?.inspectionResult - ? `${initialData.inspectionInfo.inspectionResult} (${ - initialData.inspectionInfo.inspectionDate || "" - })` - : null - } - /> - </div> - } - column3={ - <div className="flex flex-col gap-2"> - <div className="space-y-2"> - <InfoItem - title="대표공급품목" - value={initialData.capacityInfo?.mainSupplyItems || null} - /> - </div> - <Button - variant="outline" - onClick={() => handleFileManagement("대표공급품목")} - > - 대표 공급품목 상세보기 - </Button> - </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"> - {attachmentsByType.BUSINESS_REGISTRATION?.length || 0}건 - </div> - </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"> - {attachmentsByType.ISO_CERTIFICATION?.length || 0}건 - </div> - </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"> - {attachmentsByType.PRODUCT_CERT?.length || 0}건 - </div> - </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"> - {attachmentsByType.EX_CERT?.length || 0}건 - </div> - </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"> - {attachmentsByType.HSE_CERT?.length || 0}건 - </div> - </div> - </div> - } - /> */} - - {/* <Separator /> */} - - {/* 계약정보 */} - {/* <InfoSection - title="계약정보" - column1={ - <div className="space-y-2"> - <InfoItem - title="정규등록현황" - value={ - initialData.contractDetails?.regularRegistrationStatus || null - } - /> - </div> - } - column2={ - <div className="space-y-2"> - <InfoItem - title="선호 계약조건" - value={ - initialData.contractDetails?.preferredContractTerms || null - } - /> - </div> - } - column3={ - <div className="space-y-2"> - <InfoItem - title="최근 거래현황" - value={ - initialData.contractDetails?.recentTransactionStatus || null - } - /> - </div> - } - additionalContent={ - <div className="grid grid-cols-10 gap-4 min-w-0 overflow-x-auto"> - {[ - { - title: "준법서약", - value: - initialData.contractDetails?.compliancePledgeDate || null, - }, - { - title: "기술자료", - value: initialData.contractDetails?.technicalDataDate || null, - }, - { - title: "비밀유지", - value: - initialData.contractDetails?.confidentialityDate || null, - }, - { - title: "GTC", - value: initialData.contractDetails?.gtcDate || null, - }, - { - title: "표준하도급", - value: - initialData.contractDetails?.standardSubcontractDate || - null, - }, - { - title: "안전보건", - value: initialData.contractDetails?.safetyHealthDate || null, - }, - { - title: "직납자재", - value: - initialData.contractDetails?.directMaterialDate || null, - }, - { - title: "내국신용장", - value: initialData.contractDetails?.domesticLCDate || null, - }, - { - title: "동반성장", - value: initialData.contractDetails?.mutualGrowthDate || null, - }, - { - title: "윤리규범", - value: initialData.contractDetails?.ethicsDate || null, - }, - ].map((item, index) => ( - <div key={index} className="text-center min-w-0"> - <div className="text-sm font-medium mb-2 break-words"> - {item.title} - </div> - <div className="text-sm text-muted-foreground"> - {item.value || "-"} - </div> - </div> - ))} - </div> - } - /> */} - - - - {/* 추가 조회 기능 버튼들 */} - <div className="border rounded-lg p-6"> - <div className="text-lg font-semibold mb-4">상세 정보 조회</div> - <div className="grid grid-cols-2 md:grid-cols-4 gap-4"> - <Button - variant="outline" - onClick={handlePQView} - className="h-20 flex flex-col items-center justify-center space-y-2" - > - <div className="text-sm font-medium">PQ 조회</div> - <div className="text-xs text-muted-foreground">제출된 PQ 정보 확인</div> - </Button> - - <Button - variant="outline" - onClick={handleSiteVisitView} - className="h-20 flex flex-col items-center justify-center space-y-2" - > - <div className="text-sm font-medium">실사 정보</div> - <div className="text-xs text-muted-foreground">협력업체 방문실사 조회</div> - </Button> - - <Button - variant="outline" - onClick={handleContractView} - className="h-20 flex flex-col items-center justify-center space-y-2" - > - <div className="text-sm font-medium">정규업체 등록 현황</div> - <div className="text-xs text-muted-foreground">정규업체 등록 현황 보기</div> - </Button> - - <Button - variant="outline" - onClick={handleAdditionalInfoView} - className="h-20 flex flex-col items-center justify-center space-y-2" - > - <div className="text-sm font-medium">추가정보</div> - <div className="text-xs text-muted-foreground">업체 추가정보 조회</div> - </Button> - </div> - </div> - </div> - - {/* 다이얼로그들 */} - <PQSimpleDialog - open={pqDialogOpen} - onOpenChange={setPqDialogOpen} - vendorId={vendorId} - /> - - <SiteVisitDetailDialog - isOpen={siteVisitDialogOpen} - onOpenChange={setSiteVisitDialogOpen} - selectedRequest={selectedSiteVisitRequest} - /> - - {registrationData && ( - <DocumentStatusDialog - open={contractDialogOpen} - onOpenChange={setContractDialogOpen} - registration={registrationData} - /> - )} - - <AdditionalInfoDialog - open={additionalInfoDialogOpen} - onOpenChange={setAdditionalInfoDialogOpen} - vendorId={parseInt(vendorId)} - readonly={true} - /> - </div> - ); -} diff --git a/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/constants.ts b/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/constants.ts deleted file mode 100644 index d16f791f..00000000 --- a/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const noDataString = "-";
\ No newline at end of file diff --git a/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/page.tsx b/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/page.tsx index ae63d77d..629717fb 100644 --- a/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/page.tsx +++ b/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/page.tsx @@ -1,5 +1,5 @@ -import { getVendorData } from "./actions"; -import BasicInfoClient from "./basic-info-client"; +import { getVendorData } from "@/lib/vendor-basic-info/actions"; +import BasicInfoClient from "@/lib/vendor-basic-info/basic-info-client"; interface VendorBasicPageProps { params: { diff --git a/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/types.ts b/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/types.ts deleted file mode 100644 index ead3a44c..00000000 --- a/app/[lng]/evcp/(evcp)/vendors/[id]/info/basic/types.ts +++ /dev/null @@ -1,180 +0,0 @@ -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/app/[lng]/partners/(partners)/sales-force-test/page.tsx b/app/[lng]/partners/(partners)/sales-force-test/page.tsx deleted file mode 100644 index 8d6cbfbc..00000000 --- a/app/[lng]/partners/(partners)/sales-force-test/page.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import path from "path"; -import { promises as fs } from "fs"; - -type PageProps = { - params: { lng: string }; -}; - -export default async function Page({ params }: PageProps) { - const filePath = path.join( - process.cwd(), - "app", - "[lng]", - "partners", - "(partners)", - "sales-force-test", - "AF_poc.html" - ); - - const html = await fs.readFile(filePath, "utf8"); - - return ( - <div className="w-full h-[100vh]"> - <iframe - title="Salesforce LWC Test" - className="w-full h-full border-0" - srcDoc={html} - /> - </div> - ); -} - - diff --git a/app/[lng]/partners/pq_new/[id]/page.tsx b/app/[lng]/partners/pq_new/[id]/page.tsx index 41c59b47..3c2858f2 100644 --- a/app/[lng]/partners/pq_new/[id]/page.tsx +++ b/app/[lng]/partners/pq_new/[id]/page.tsx @@ -160,7 +160,7 @@ export default async function PQEditPage(props: PQEditPageProps) { )} {/* PQ 입력 컴포넌트 */} - <div className={isReadOnly ? "pointer-events-none opacity-60" : ""}> + <div className={isReadOnly ? "opacity-60" : ""}> <PQInputTabs data={pqData} vendorId={idAsNumber} |
