summaryrefslogtreecommitdiff
path: root/components/signup/join-form.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'components/signup/join-form.tsx')
-rw-r--r--components/signup/join-form.tsx2021
1 files changed, 1070 insertions, 951 deletions
diff --git a/components/signup/join-form.tsx b/components/signup/join-form.tsx
index 60f600b9..e9773d28 100644
--- a/components/signup/join-form.tsx
+++ b/components/signup/join-form.tsx
@@ -1,32 +1,19 @@
-"use client"
-
-import * as React from "react"
-import { zodResolver } from "@hookform/resolvers/zod"
-import { useForm, useFieldArray } from "react-hook-form"
-import { useRouter, useSearchParams, useParams } from "next/navigation"
-
-import i18nIsoCountries from "i18n-iso-countries"
-import enLocale from "i18n-iso-countries/langs/en.json"
-import koLocale from "i18n-iso-countries/langs/ko.json"
-
-import { Button } from "@/components/ui/button"
-import { Separator } from "@/components/ui/separator"
-import {
- Form,
- FormControl,
- FormDescription,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form"
-import { Input } from "@/components/ui/input"
-import { toast } from "@/hooks/use-toast"
+'use client'
+
+import React, { useState, useEffect } from 'react';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Progress } from '@/components/ui/progress';
+import { Check, ChevronRight, User, Building, FileText, Plus, X, ChevronsUpDown, Loader2 } from 'lucide-react';
+import { cn } from '@/lib/utils';
+import { useRouter, useParams, useSearchParams } from 'next/navigation';
+import { useTranslation } from '@/i18n/client';
+import { toast } from '@/hooks/use-toast';
import {
Popover,
PopoverTrigger,
PopoverContent,
-} from "@/components/ui/popover"
+} from '@/components/ui/popover';
import {
Command,
CommandList,
@@ -34,21 +21,14 @@ import {
CommandEmpty,
CommandGroup,
CommandItem,
-} from "@/components/ui/command"
-import { Check, ChevronsUpDown, Loader2, Plus, X } from "lucide-react"
-import { cn } from "@/lib/utils"
-import { useTranslation } from "@/i18n/client"
-
-import { getVendorTypes } from "@/lib/vendors/service"
-import { createVendorSchema, CreateVendorSchema } from "@/lib/vendors/validations"
+} from '@/components/ui/command';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
-} from "@/components/ui/select"
-
+} from '@/components/ui/select';
import {
Dropzone,
DropzoneZone,
@@ -56,7 +36,7 @@ import {
DropzoneUploadIcon,
DropzoneTitle,
DropzoneDescription,
-} from "@/components/ui/dropzone"
+} from '@/components/ui/dropzone';
import {
FileList,
FileListItem,
@@ -66,33 +46,33 @@ import {
FileListName,
FileListDescription,
FileListAction,
-} from "@/components/ui/file-list"
-import { Badge } from "@/components/ui/badge"
-import { ScrollArea } from "@/components/ui/scroll-area"
-import prettyBytes from "pretty-bytes"
-import { Checkbox } from "../ui/checkbox"
-
-i18nIsoCountries.registerLocale(enLocale)
-i18nIsoCountries.registerLocale(koLocale)
-
-const locale = "ko"
-const countryMap = i18nIsoCountries.getNames(locale, { select: "official" })
+} from '@/components/ui/file-list';
+import { ScrollArea } from '@/components/ui/scroll-area';
+import prettyBytes from 'pretty-bytes';
+
+// 기존 JoinForm에서 가져온 데이터들
+import i18nIsoCountries from "i18n-iso-countries";
+import enLocale from "i18n-iso-countries/langs/en.json";
+import koLocale from "i18n-iso-countries/langs/ko.json";
+import { getVendorTypes } from '@/lib/vendors/service';
+import ConsentStep from './conset-step';
+
+i18nIsoCountries.registerLocale(enLocale);
+i18nIsoCountries.registerLocale(koLocale);
+
+const locale = "ko";
+const countryMap = i18nIsoCountries.getNames(locale, { select: "official" });
const countryArray = Object.entries(countryMap).map(([code, label]) => ({
code,
label,
-}))
+}));
-// Sort countries to put Korea first, then alphabetically
const sortedCountryArray = [...countryArray].sort((a, b) => {
- // Put Korea (KR) at the top
if (a.code === "KR") return -1;
if (b.code === "KR") return 1;
-
- // Otherwise sort alphabetically
return a.label.localeCompare(b.label);
});
-// Add English names for Korean locale
const enhancedCountryArray = sortedCountryArray.map(country => ({
...country,
label: locale === "ko" && country.code === "KR"
@@ -100,7 +80,6 @@ const enhancedCountryArray = sortedCountryArray.map(country => ({
: country.label
}));
-// Contact task options
const contactTaskOptions = [
{ value: "PRESIDENT_DIRECTOR", label: "회사대표 President/Director" },
{ value: "SALES_MANAGEMENT", label: "영업관리 Sales Management" },
@@ -114,8 +93,7 @@ const contactTaskOptions = [
{ value: "FIELD_SERVICE_ENGINEER", label: "FSE(야드작업자) Field Service Engineer" }
];
-// Comprehensive list of country dial codes
-export const countryDialCodes: { [key: string]: string } = {
+export const countryDialCodes = {
AF: "+93", AL: "+355", DZ: "+213", AS: "+1-684", AD: "+376", AO: "+244",
AI: "+1-264", AG: "+1-268", AR: "+54", AM: "+374", AW: "+297", AU: "+61",
AT: "+43", AZ: "+994", BS: "+1-242", BH: "+973", BD: "+880", BB: "+1-246",
@@ -153,312 +131,1102 @@ export const countryDialCodes: { [key: string]: string } = {
ZW: "+263"
};
-const MAX_FILE_SIZE = 3e9
-
-export function JoinForm() {
- const params = useParams() || {};
- const lng = params.lng ? String(params.lng) : "ko";
- const { t } = useTranslation(lng, "translation")
-
- const router = useRouter()
- const searchParams = useSearchParams() || new URLSearchParams();
- const defaultTaxId = searchParams.get("taxID") ?? ""
-
- // Define VendorType interface
- interface VendorType {
- id: number;
- code: string;
- nameKo: string;
- nameEn: string;
- }
-
- // Vendor Types state with proper typing
- const [vendorTypes, setVendorTypes] = React.useState<VendorType[]>([])
- const [isLoadingVendorTypes, setIsLoadingVendorTypes] = React.useState(true)
-
- // Individual file states
- const [businessRegistrationFiles, setBusinessRegistrationFiles] = React.useState<File[]>([])
- const [isoCertificationFiles, setIsoCertificationFiles] = React.useState<File[]>([])
- const [creditReportFiles, setCreditReportFiles] = React.useState<File[]>([])
- const [bankAccountFiles, setBankAccountFiles] = React.useState<File[]>([])
-
- const [isSubmitting, setIsSubmitting] = React.useState(false)
-
- // Fetch vendor types on component mount
- React.useEffect(() => {
- async function loadVendorTypes() {
- setIsLoadingVendorTypes(true)
- try {
- const result = await getVendorTypes()
- if (result.data) {
- setVendorTypes(result.data)
- }
- } catch (error) {
- console.error("Failed to load vendor types:", error)
- toast({
- variant: "destructive",
- title: "Error",
- description: "Failed to load vendor types",
- })
- } finally {
- setIsLoadingVendorTypes(false)
+const MAX_FILE_SIZE = 3e9;
+
+// 스텝 정의
+const STEPS = [
+ { id: 1, title: '약관 동의', description: '서비스 이용 약관 동의', icon: FileText },
+ { id: 2, title: '계정 생성', description: '개인 계정 정보 입력', icon: User },
+ { id: 3, title: '업체 등록', description: '업체 정보 및 서류 제출', icon: Building }
+];
+
+export default function JoinForm() {
+ const params = useParams() || {};
+ const lng = params.lng ? String(params.lng) : "ko";
+ const { t } = useTranslation(lng, "translation");
+ const router = useRouter();
+ const searchParams = useSearchParams() || new URLSearchParams();
+ const defaultTaxId = searchParams.get("taxID") ?? "";
+
+ const [currentStep, setCurrentStep] = useState(1);
+ const [completedSteps, setCompletedSteps] = useState(new Set());
+
+ // 각 스텝별 데이터
+ const [consentData, setConsentData] = useState({
+ privacy: false,
+ terms: false,
+ marketing: false
+ });
+
+ const [accountData, setAccountData] = useState({
+ name: '',
+ email: '',
+ password: '',
+ confirmPassword: '',
+ phone: ''
+ });
+
+ const [vendorData, setVendorData] = useState({
+ vendorName: "",
+ vendorTypeId: undefined,
+ items: "",
+ taxId: defaultTaxId,
+ address: "",
+ email: "",
+ phone: "",
+ country: "",
+ website: "",
+ representativeName: "",
+ representativeBirth: "",
+ representativeEmail: "",
+ representativePhone: "",
+ corporateRegistrationNumber: "",
+ representativeWorkExpirence: false,
+ contacts: [
+ {
+ contactName: "",
+ contactPosition: "",
+ contactDepartment: "",
+ contactTask: "",
+ contactEmail: "",
+ contactPhone: "",
+ },
+ ],
+ });
+
+ // 업체 타입 및 파일 상태
+ const [vendorTypes, setVendorTypes] = useState([]);
+ const [isLoadingVendorTypes, setIsLoadingVendorTypes] = useState(true);
+ const [businessRegistrationFiles, setBusinessRegistrationFiles] = useState([]);
+ const [isoCertificationFiles, setIsoCertificationFiles] = useState([]);
+ const [creditReportFiles, setCreditReportFiles] = useState([]);
+ const [bankAccountFiles, setBankAccountFiles] = useState([]);
+
+ const [policyVersions, setPolicyVersions] = useState({
+ privacy_policy: '1.0',
+ terms_of_service: '1.0'
+ });
+
+ const progress = ((currentStep - 1) / (STEPS.length - 1)) * 100;
+
+ // 정책 버전 및 업체 타입 로드
+ useEffect(() => {
+ fetchPolicyVersions();
+ loadVendorTypes();
+ }, []);
+
+ const fetchPolicyVersions = async () => {
+ try {
+ const response = await fetch('/api/consent/policy-versions');
+ const versions = await response.json();
+ setPolicyVersions(versions);
+ } catch (error) {
+ console.error('Failed to fetch policy versions:', error);
+ }
+ };
+
+ const loadVendorTypes = async () => {
+ setIsLoadingVendorTypes(true);
+ try {
+ const result = await getVendorTypes();
+ if (result.data) {
+ setVendorTypes(result.data);
}
+ } catch (error) {
+ console.error("Failed to load vendor types:", error);
+ toast({
+ variant: "destructive",
+ title: "Error",
+ description: "Failed to load vendor types",
+ });
+ } finally {
+ setIsLoadingVendorTypes(false);
+ }
+ };
+
+ const handleStepComplete = (step) => {
+ setCompletedSteps(prev => new Set([...prev, step]));
+ if (step < STEPS.length) {
+ setCurrentStep(step + 1);
+ }
+ };
+
+ const handleStepClick = (stepId) => {
+ if (stepId <= Math.max(...completedSteps) + 1) {
+ setCurrentStep(stepId);
}
+ };
+
+ // 전화번호 플레이스홀더 함수들
+ const getPhonePlaceholder = (countryCode) => {
+ if (!countryCode || !countryDialCodes[countryCode]) return "+82 010-1234-5678";
- loadVendorTypes()
- }, [])
-
- // React Hook Form
- const form = useForm<CreateVendorSchema>({
- resolver: zodResolver(createVendorSchema),
- defaultValues: {
- vendorName: "",
- vendorTypeId: undefined,
- items: "",
- taxId: defaultTaxId,
- address: "",
- email: "",
- phone: "",
- country: "",
- representativeName: "",
- representativeBirth: "",
- representativeEmail: "",
- representativePhone: "",
- corporateRegistrationNumber: "",
- representativeWorkExpirence: false,
- // contacts (updated with new fields)
- contacts: [
- {
- contactName: "",
- contactPosition: "",
- contactDepartment: "",
- contactTask: "",
- contactEmail: "",
- contactPhone: "",
- },
- ],
+ const dialCode = countryDialCodes[countryCode];
+
+ switch (countryCode) {
+ case 'KR': return `${dialCode} 010-1234-5678`;
+ case 'US':
+ case 'CA': return `${dialCode} 555-123-4567`;
+ case 'JP': return `${dialCode} 90-1234-5678`;
+ case 'CN': return `${dialCode} 138-0013-8000`;
+ case 'GB': return `${dialCode} 20-7946-0958`;
+ case 'DE': return `${dialCode} 30-12345678`;
+ case 'FR': return `${dialCode} 1-42-86-83-16`;
+ default: return `${dialCode} 전화번호`;
+ }
+ };
+
+ const getPhoneDescription = (countryCode) => {
+ if (!countryCode) return "국가를 먼저 선택해주세요.";
+
+ const dialCode = countryDialCodes[countryCode];
+
+ switch (countryCode) {
+ case 'KR': return `${dialCode}로 시작하는 국제번호 또는 010으로 시작하는 국내번호를 입력하세요.`;
+ case 'US':
+ case 'CA': return `${dialCode}로 시작하는 10자리 번호를 입력하세요.`;
+ case 'JP': return `${dialCode}로 시작하는 일본 전화번호를 입력하세요.`;
+ case 'CN': return `${dialCode}로 시작하는 중국 전화번호를 입력하세요.`;
+ default: return `${dialCode}로 시작하는 국제 전화번호를 입력하세요.`;
+ }
+ };
+
+ return (
+ <div className="container max-w-6xl mx-auto py-8">
+ {/* 진행률 표시 */}
+ <div className="mb-8">
+ <div className="flex items-center justify-between mb-4">
+ <h1 className="text-2xl font-bold">파트너 등록</h1>
+ <span className="text-sm text-muted-foreground">
+ {currentStep} / {STEPS.length}
+ </span>
+ </div>
+ <Progress value={progress} className="mb-6" />
+
+ {/* 스텝 네비게이션 */}
+ <div className="flex items-center justify-between">
+ {STEPS.map((step, index) => {
+ const Icon = step.icon;
+ const isCompleted = completedSteps.has(step.id);
+ const isCurrent = currentStep === step.id;
+ const isAccessible = step.id <= Math.max(...completedSteps) + 1;
+
+ return (
+ <React.Fragment key={step.id}>
+ <div
+ className={cn(
+ "flex flex-col items-center cursor-pointer transition-all",
+ isAccessible ? "opacity-100" : "opacity-50 cursor-not-allowed"
+ )}
+ onClick={() => handleStepClick(step.id)}
+ >
+ <div className={cn(
+ "w-12 h-12 rounded-full flex items-center justify-center mb-2 border-2 transition-all",
+ isCompleted
+ ? "bg-green-500 border-green-500 text-white"
+ : isCurrent
+ ? "bg-blue-500 border-blue-500 text-white"
+ : "border-gray-300 text-gray-400"
+ )}>
+ {isCompleted ? (
+ <Check className="w-6 h-6" />
+ ) : (
+ <Icon className="w-6 h-6" />
+ )}
+ </div>
+ <div className="text-center">
+ <div className={cn(
+ "text-sm font-medium",
+ isCurrent ? "text-blue-600" : isCompleted ? "text-green-600" : "text-gray-500"
+ )}>
+ {step.title}
+ </div>
+ <div className="text-xs text-gray-400 mt-1">
+ {step.description}
+ </div>
+ </div>
+ </div>
+
+ {index < STEPS.length - 1 && (
+ <ChevronRight className="w-5 h-5 text-gray-300 mx-4" />
+ )}
+ </React.Fragment>
+ );
+ })}
+ </div>
+ </div>
+
+ {/* 스텝 콘텐츠 */}
+ <div className="bg-white rounded-lg border shadow-sm p-6">
+ {currentStep === 1 && (
+ <ConsentStep
+ data={consentData}
+ onChange={setConsentData}
+ onNext={() => handleStepComplete(1)}
+ policyVersions={policyVersions}
+ />
+ )}
+
+ {currentStep === 2 && (
+ <AccountStep
+ data={accountData}
+ onChange={setAccountData}
+ onNext={() => handleStepComplete(2)}
+ onBack={() => setCurrentStep(1)}
+ />
+ )}
+
+ {currentStep === 3 && (
+ <VendorStep
+ data={vendorData}
+ onChange={setVendorData}
+ onBack={() => setCurrentStep(2)}
+ onComplete={() => {
+ handleStepComplete(3);
+ // 완료 후 대시보드로 이동
+ router.push(`/${lng}/partners/dashboard`);
+ }}
+ accountData={accountData}
+ consentData={consentData}
+ vendorTypes={vendorTypes}
+ isLoadingVendorTypes={isLoadingVendorTypes}
+ businessRegistrationFiles={businessRegistrationFiles}
+ setBusinessRegistrationFiles={setBusinessRegistrationFiles}
+ isoCertificationFiles={isoCertificationFiles}
+ setIsoCertificationFiles={setIsoCertificationFiles}
+ creditReportFiles={creditReportFiles}
+ setCreditReportFiles={setCreditReportFiles}
+ bankAccountFiles={bankAccountFiles}
+ setBankAccountFiles={setBankAccountFiles}
+ getPhonePlaceholder={getPhonePlaceholder}
+ getPhoneDescription={getPhoneDescription}
+ enhancedCountryArray={enhancedCountryArray}
+ contactTaskOptions={contactTaskOptions}
+ lng={lng}
+ policyVersions={policyVersions}
+ />
+ )}
+ </div>
+ </div>
+ );
+}
+
+
+// Step 2: 계정 생성
+function AccountStep({ data, onChange, onNext, onBack }) {
+ const [isLoading, setIsLoading] = useState(false);
+
+ const isValid = data.name && data.email && data.password &&
+ data.confirmPassword && data.phone &&
+ data.password === data.confirmPassword;
+
+ const handleInputChange = (field, value) => {
+ onChange(prev => ({ ...prev, [field]: value }));
+ };
+
+ const handleNext = async () => {
+ if (!isValid) return;
+
+ setIsLoading(true);
+ try {
+ // 이메일 중복 확인
+ const response = await fetch('/api/auth/check-email', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ email: data.email })
+ });
+
+ const result = await response.json();
+
+ if (!response.ok) {
+ if (result.error === 'EMAIL_EXISTS') {
+ alert('이미 사용 중인 이메일입니다.');
+ return;
+ }
+ throw new Error(result.error);
+ }
+
+ onNext();
+ } catch (error) {
+ console.error('Email check error:', error);
+ alert('이메일 확인 중 오류가 발생했습니다.');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+ <div className="space-y-6">
+ <div>
+ <h2 className="text-xl font-semibold mb-2">계정 정보 입력</h2>
+ <p className="text-gray-600 text-sm">
+ 서비스 이용을 위한 개인 계정을 생성합니다.
+ </p>
+ </div>
+
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+ <div>
+ <label className="block text-sm font-medium mb-1">
+ 이름 <span className="text-red-500">*</span>
+ </label>
+ <Input
+ type="text"
+ placeholder="이름을 입력하세요"
+ value={data.name}
+ onChange={(e) => handleInputChange('name', e.target.value)}
+ />
+ </div>
+
+ <div>
+ <label className="block text-sm font-medium mb-1">
+ 이메일 <span className="text-red-500">*</span>
+ </label>
+ <Input
+ type="email"
+ placeholder="이메일을 입력하세요"
+ value={data.email}
+ onChange={(e) => handleInputChange('email', e.target.value)}
+ />
+ </div>
+
+ <div>
+ <label className="block text-sm font-medium mb-1">
+ 비밀번호 <span className="text-red-500">*</span>
+ </label>
+ <Input
+ type="password"
+ placeholder="비밀번호를 입력하세요"
+ value={data.password}
+ onChange={(e) => handleInputChange('password', e.target.value)}
+ />
+ </div>
+
+ <div>
+ <label className="block text-sm font-medium mb-1">
+ 비밀번호 확인 <span className="text-red-500">*</span>
+ </label>
+ <Input
+ type="password"
+ placeholder="비밀번호를 다시 입력하세요"
+ value={data.confirmPassword}
+ onChange={(e) => handleInputChange('confirmPassword', e.target.value)}
+ />
+ {data.confirmPassword && data.password !== data.confirmPassword && (
+ <p className="text-xs text-red-500 mt-1">비밀번호가 일치하지 않습니다.</p>
+ )}
+ </div>
+
+ <div className="md:col-span-2">
+ <label className="block text-sm font-medium mb-1">
+ 전화번호 <span className="text-red-500">*</span>
+ </label>
+ <Input
+ type="tel"
+ placeholder="+82-10-1234-5678"
+ value={data.phone}
+ onChange={(e) => handleInputChange('phone', e.target.value)}
+ />
+ <p className="text-xs text-gray-500 mt-1">
+ SMS 인증에 사용됩니다. 국제번호 형식으로 입력해주세요.
+ </p>
+ </div>
+ </div>
+
+ <div className="flex justify-between">
+ <Button variant="outline" onClick={onBack}>
+ 이전
+ </Button>
+ <Button onClick={handleNext} disabled={!isValid || isLoading}>
+ {isLoading ? '확인 중...' : '다음 단계'}
+ </Button>
+ </div>
+ </div>
+ );
+}
+
+// Step 3: 업체 등록 (기존 JoinForm 내용)
+function VendorStep(props) {
+ return <CompleteVendorForm {...props} />;
+}
+
+
+// 완전한 업체 등록 폼 컴포넌트 (기존 JoinForm 내용)
+function CompleteVendorForm({
+ data, onChange, onBack, onComplete, accountData, consentData,
+ vendorTypes, isLoadingVendorTypes, businessRegistrationFiles, setBusinessRegistrationFiles,
+ isoCertificationFiles, setIsoCertificationFiles, creditReportFiles, setCreditReportFiles,
+ bankAccountFiles, setBankAccountFiles, getPhonePlaceholder, getPhoneDescription,
+ enhancedCountryArray, contactTaskOptions, lng, policyVersions
+}) {
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ // 담당자 관리 함수들
+ const addContact = () => {
+ onChange(prev => ({
+ ...prev,
+ contacts: [...prev.contacts, {
+ contactName: "",
+ contactPosition: "",
+ contactDepartment: "",
+ contactTask: "",
+ contactEmail: "",
+ contactPhone: "",
+ }]
+ }));
+ };
+
+ const removeContact = (index) => {
+ onChange(prev => ({
+ ...prev,
+ contacts: prev.contacts.filter((_, i) => i !== index)
+ }));
+ };
+
+ const updateContact = (index, field, value) => {
+ onChange(prev => ({
+ ...prev,
+ contacts: prev.contacts.map((contact, i) =>
+ i === index ? { ...contact, [field]: value } : contact
+ )
+ }));
+ };
+
+ // 폼 입력 변경 핸들러
+ const handleInputChange = (field, value) => {
+ onChange(prev => ({ ...prev, [field]: value }));
+ };
+
+ // 파일 업로드 핸들러들
+ const createFileUploadHandler = (setFiles, currentFiles) => ({
+ onDropAccepted: (acceptedFiles) => {
+ const newFiles = [...currentFiles, ...acceptedFiles];
+ setFiles(newFiles);
},
- mode: "onChange",
- })
+ onDropRejected: (fileRejections) => {
+ fileRejections.forEach((rej) => {
+ toast({
+ variant: "destructive",
+ title: "File Error",
+ description: `${rej.file.name}: ${rej.errors[0]?.message || "Upload failed"}`,
+ });
+ });
+ },
+ removeFile: (index) => {
+ const updated = [...currentFiles];
+ updated.splice(index, 1);
+ setFiles(updated);
+ }
+ });
- // Custom validation for file uploads
+ const businessRegistrationHandler = createFileUploadHandler(setBusinessRegistrationFiles, businessRegistrationFiles);
+ const isoCertificationHandler = createFileUploadHandler(setIsoCertificationFiles, isoCertificationFiles);
+ const creditReportHandler = createFileUploadHandler(setCreditReportFiles, creditReportFiles);
+ const bankAccountHandler = createFileUploadHandler(setBankAccountFiles, bankAccountFiles);
+
+ // 유효성 검사
const validateRequiredFiles = () => {
- const errors = []
+ const errors = [];
if (businessRegistrationFiles.length === 0) {
- errors.push("사업자등록증을 업로드해주세요.")
+ errors.push("사업자등록증을 업로드해주세요.");
}
if (isoCertificationFiles.length === 0) {
- errors.push("ISO 인증서를 업로드해주세요.")
+ errors.push("ISO 인증서를 업로드해주세요.");
}
if (creditReportFiles.length === 0) {
- errors.push("신용평가보고서를 업로드해주세요.")
+ errors.push("신용평가보고서를 업로드해주세요.");
}
- if (form.watch("country") !== "KR" && bankAccountFiles.length === 0) {
- errors.push("대금지급 통장사본을 업로드해주세요.")
+ if (data.country !== "KR" && bankAccountFiles.length === 0) {
+ errors.push("대금지급 통장사본을 업로드해주세요.");
}
- return errors
- }
-
- const isFormValid = form.formState.isValid && validateRequiredFiles().length === 0
-
- // Field array for contacts
- const { fields: contactFields, append: addContact, remove: removeContact } =
- useFieldArray({
- control: form.control,
- name: "contacts",
- })
-
- // File upload handlers
- const createFileUploadHandler = (
- setFiles: React.Dispatch<React.SetStateAction<File[]>>,
- currentFiles: File[]
- ) => ({
- onDropAccepted: (acceptedFiles: File[]) => {
- const newFiles = [...currentFiles, ...acceptedFiles]
- setFiles(newFiles)
- },
- onDropRejected: (fileRejections: any[]) => {
- fileRejections.forEach((rej) => {
- toast({
- variant: "destructive",
- title: "File Error",
- description: `${rej.file.name}: ${rej.errors[0]?.message || "Upload failed"}`,
- })
- })
- },
- removeFile: (index: number) => {
- const updated = [...currentFiles]
- updated.splice(index, 1)
- setFiles(updated)
- }
- })
+ return errors;
+ };
- const businessRegistrationHandler = createFileUploadHandler(setBusinessRegistrationFiles, businessRegistrationFiles)
- const isoCertificationHandler = createFileUploadHandler(setIsoCertificationFiles, isoCertificationFiles)
- const creditReportHandler = createFileUploadHandler(setCreditReportFiles, creditReportFiles)
- const bankAccountHandler = createFileUploadHandler(setBankAccountFiles, bankAccountFiles)
+ const isFormValid = data.vendorName && data.vendorTypeId && data.items &&
+ data.country && data.phone && data.email &&
+ data.contacts.length > 0 &&
+ data.contacts[0].contactName &&
+ validateRequiredFiles().length === 0;
- // Submit
- async function onSubmit(values: CreateVendorSchema) {
- const fileErrors = validateRequiredFiles()
+ // 최종 제출
+ const handleSubmit = async () => {
+ const fileErrors = validateRequiredFiles();
if (fileErrors.length > 0) {
toast({
variant: "destructive",
title: "파일 업로드 필수",
description: fileErrors.join("\n"),
- })
- return
+ });
+ return;
}
- setIsSubmitting(true)
+ setIsSubmitting(true);
try {
- const formData = new FormData()
-
- // Add vendor data
- const vendorData = {
- vendorName: values.vendorName,
- vendorTypeId: values.vendorTypeId,
- items: values.items,
- vendorCode: values.vendorCode,
- website: values.website,
- taxId: values.taxId,
- address: values.address,
- email: values.email,
- phone: values.phone,
- country: values.country,
- status: "PENDING_REVIEW" as const,
- representativeName: values.representativeName || "",
- representativeBirth: values.representativeBirth || "",
- representativeEmail: values.representativeEmail || "",
- representativePhone: values.representativePhone || "",
- corporateRegistrationNumber: values.corporateRegistrationNumber || "",
- representativeWorkExpirence: values.representativeWorkExpirence || false
- }
+ const formData = new FormData();
+
+ // 통합 데이터 준비
+ const completeData = {
+ account: accountData,
+ vendor: {
+ ...data,
+ email: data.email || accountData.email, // 업체 이메일이 없으면 계정 이메일 사용
+ },
+ consents: {
+ privacy_policy: {
+ agreed: consentData.privacy,
+ version: policyVersions.privacy_policy
+ },
+ terms_of_service: {
+ agreed: consentData.terms,
+ version: policyVersions.terms_of_service
+ },
+ marketing: {
+ agreed: consentData.marketing,
+ version: policyVersions.privacy_policy
+ }
+ }
+ };
- formData.append('vendorData', JSON.stringify(vendorData))
- formData.append('contacts', JSON.stringify(values.contacts))
+ formData.append('completeData', JSON.stringify(completeData));
- // Add files with specific types
+ // 파일들 추가
businessRegistrationFiles.forEach(file => {
- formData.append('businessRegistration', file)
- })
+ formData.append('businessRegistration', file);
+ });
isoCertificationFiles.forEach(file => {
- formData.append('isoCertification', file)
- })
+ formData.append('isoCertification', file);
+ });
creditReportFiles.forEach(file => {
- formData.append('creditReport', file)
- })
+ formData.append('creditReport', file);
+ });
- if (values.country !== "KR") {
+ if (data.country !== "KR") {
bankAccountFiles.forEach(file => {
- formData.append('bankAccount', file)
- })
+ formData.append('bankAccount', file);
+ });
}
- const response = await fetch('/api/vendors', {
+ const response = await fetch('/api/auth/signup-with-vendor', {
method: 'POST',
body: formData,
- })
+ });
- const result = await response.json()
+ const result = await response.json();
if (response.ok) {
toast({
title: "등록 완료",
- description: "회사 등록이 완료되었습니다. (status=PENDING_REVIEW)",
- })
- router.push("/ko/partners")
+ description: "회원가입 및 업체 등록이 완료되었습니다. 관리자 승인 후 서비스를 이용하실 수 있습니다.",
+ });
+ onComplete();
} else {
toast({
variant: "destructive",
title: "오류",
description: result.error || "등록에 실패했습니다.",
- })
+ });
}
- } catch (error: any) {
- console.error(error)
+ } catch (error) {
+ console.error(error);
toast({
variant: "destructive",
title: "서버 에러",
description: error.message || "에러가 발생했습니다.",
- })
+ });
} finally {
- setIsSubmitting(false)
- }
- }
-
- // Get country code for phone number placeholder
- const getPhonePlaceholder = (countryCode: string) => {
- if (!countryCode || !countryDialCodes[countryCode]) return "+82 010-1234-5678";
-
- const dialCode = countryDialCodes[countryCode];
-
- switch (countryCode) {
- case 'KR':
- return `${dialCode} 010-1234-5678`;
- case 'US':
- case 'CA':
- return `${dialCode} 555-123-4567`;
- case 'JP':
- return `${dialCode} 90-1234-5678`;
- case 'CN':
- return `${dialCode} 138-0013-8000`;
- case 'GB':
- return `${dialCode} 20-7946-0958`;
- case 'DE':
- return `${dialCode} 30-12345678`;
- case 'FR':
- return `${dialCode} 1-42-86-83-16`;
- default:
- return `${dialCode} 전화번호`;
+ setIsSubmitting(false);
}
};
- const getPhoneDescription = (countryCode: string) => {
- if (!countryCode) return "국가를 먼저 선택해주세요.";
-
- const dialCode = countryDialCodes[countryCode];
-
- switch (countryCode) {
- case 'KR':
- return `${dialCode}로 시작하는 국제번호 또는 010으로 시작하는 국내번호를 입력하세요.`;
- case 'US':
- case 'CA':
- return `${dialCode}로 시작하는 10자리 번호를 입력하세요.`;
- case 'JP':
- return `${dialCode}로 시작하는 일본 전화번호를 입력하세요.`;
- case 'CN':
- return `${dialCode}로 시작하는 중국 전화번호를 입력하세요.`;
- default:
- return `${dialCode}로 시작하는 국제 전화번호를 입력하세요.`;
- }
- };
+ return (
+ <div className="space-y-8">
+ <div>
+ <h2 className="text-xl font-semibold mb-2">업체 정보 등록</h2>
+ <p className="text-gray-600 text-sm">
+ 업체 정보와 필요한 서류를 등록해주세요. 모든 정보는 관리자 검토 후 승인됩니다.
+ </p>
+ </div>
- // File display component
- const FileUploadSection = ({
- title,
- description,
- files,
- onDropAccepted,
- onDropRejected,
- removeFile,
- required = true
- }: {
- title: string;
- description: string;
- files: File[];
- onDropAccepted: (files: File[]) => void;
- onDropRejected: (rejections: any[]) => void;
- removeFile: (index: number) => void;
- required?: boolean;
- }) => (
+ {/* 기본 정보 */}
+ <div className="rounded-md border p-6 space-y-4">
+ <h4 className="text-md font-semibold">기본 정보</h4>
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+ {/* 업체 유형 */}
+ <div>
+ <label className="block text-sm font-medium mb-1">
+ 업체유형 <span className="text-red-500">*</span>
+ </label>
+ <Popover>
+ <PopoverTrigger asChild>
+ <Button
+ variant="outline"
+ role="combobox"
+ className={cn(
+ "w-full justify-between",
+ !data.vendorTypeId && "text-muted-foreground"
+ )}
+ disabled={isSubmitting || isLoadingVendorTypes}
+ >
+ {isLoadingVendorTypes
+ ? "Loading..."
+ : vendorTypes.find(type => type.id === data.vendorTypeId)?.[lng === "ko" ? "nameKo" : "nameEn"] || "업체유형 선택"}
+ <ChevronsUpDown className="ml-2 opacity-50" />
+ </Button>
+ </PopoverTrigger>
+ <PopoverContent className="w-full p-0">
+ <Command>
+ <CommandInput placeholder="업체유형 검색..." />
+ <CommandList>
+ <CommandEmpty>No vendor type found.</CommandEmpty>
+ <CommandGroup>
+ {vendorTypes.map((type) => (
+ <CommandItem
+ key={type.id}
+ value={lng === "ko" ? type.nameKo : type.nameEn}
+ onSelect={() => handleInputChange('vendorTypeId', type.id)}
+ >
+ <Check
+ className={cn(
+ "mr-2",
+ type.id === data.vendorTypeId
+ ? "opacity-100"
+ : "opacity-0"
+ )}
+ />
+ {lng === "ko" ? type.nameKo : type.nameEn}
+ </CommandItem>
+ ))}
+ </CommandGroup>
+ </CommandList>
+ </Command>
+ </PopoverContent>
+ </Popover>
+ </div>
+
+ {/* 업체명 */}
+ <div>
+ <label className="block text-sm font-medium mb-1">
+ 업체명 <span className="text-red-500">*</span>
+ </label>
+ <Input
+ value={data.vendorName}
+ onChange={(e) => handleInputChange('vendorName', e.target.value)}
+ disabled={isSubmitting}
+ />
+ <p className="text-xs text-gray-500 mt-1">
+ {data.country === "KR"
+ ? "사업자 등록증에 표기된 정확한 회사명을 입력하세요."
+ : "해외 업체의 경우 영문 회사명을 입력하세요."}
+ </p>
+ </div>
+
+ {/* 공급품목 */}
+ <div>
+ <label className="block text-sm font-medium mb-1">
+ 공급품목 <span className="text-red-500">*</span>
+ </label>
+ <Input
+ value={data.items}
+ onChange={(e) => handleInputChange('items', e.target.value)}
+ disabled={isSubmitting}
+ />
+ <p className="text-xs text-gray-500 mt-1">
+ 공급 가능한 제품/서비스를 입력하세요
+ </p>
+ </div>
+
+ {/* 사업자등록번호 */}
+ <div>
+ <label className="block text-sm font-medium mb-1">
+ 사업자등록번호 <span className="text-red-500">*</span>
+ </label>
+ <Input
+ value={data.taxId}
+ onChange={(e) => handleInputChange('taxId', e.target.value)}
+ disabled={isSubmitting}
+ placeholder="123-45-67890"
+ />
+ </div>
+
+ {/* 주소 */}
+ <div>
+ <label className="block text-sm font-medium mb-1">주소</label>
+ <Input
+ value={data.address}
+ onChange={(e) => handleInputChange('address', e.target.value)}
+ disabled={isSubmitting}
+ />
+ </div>
+
+ {/* 국가 */}
+ <div>
+ <label className="block text-sm font-medium mb-1">
+ 국가 <span className="text-red-500">*</span>
+ </label>
+ <Popover>
+ <PopoverTrigger asChild>
+ <Button
+ variant="outline"
+ role="combobox"
+ className={cn(
+ "w-full justify-between",
+ !data.country && "text-muted-foreground"
+ )}
+ disabled={isSubmitting}
+ >
+ {enhancedCountryArray.find(c => c.code === data.country)?.label || "국가 선택"}
+ <ChevronsUpDown className="ml-2 opacity-50" />
+ </Button>
+ </PopoverTrigger>
+ <PopoverContent className="w-full p-0">
+ <Command>
+ <CommandInput placeholder="국가 검색..." />
+ <CommandList>
+ <CommandEmpty>No country found.</CommandEmpty>
+ <CommandGroup>
+ {enhancedCountryArray.map((country) => (
+ <CommandItem
+ key={country.code}
+ value={country.label}
+ onSelect={() => handleInputChange('country', country.code)}
+ >
+ <Check
+ className={cn(
+ "mr-2",
+ country.code === data.country
+ ? "opacity-100"
+ : "opacity-0"
+ )}
+ />
+ {country.label}
+ </CommandItem>
+ ))}
+ </CommandGroup>
+ </CommandList>
+ </Command>
+ </PopoverContent>
+ </Popover>
+ </div>
+
+ {/* 대표 전화 */}
+ <div>
+ <label className="block text-sm font-medium mb-1">
+ 대표 전화 <span className="text-red-500">*</span>
+ </label>
+ <Input
+ value={data.phone}
+ onChange={(e) => handleInputChange('phone', e.target.value)}
+ placeholder={getPhonePlaceholder(data.country)}
+ disabled={isSubmitting}
+ />
+ <p className="text-xs text-gray-500 mt-1">
+ {getPhoneDescription(data.country)}
+ </p>
+ </div>
+
+ {/* 대표 이메일 */}
+ <div>
+ <label className="block text-sm font-medium mb-1">
+ 대표 이메일 <span className="text-red-500">*</span>
+ </label>
+ <Input
+ value={data.email}
+ onChange={(e) => handleInputChange('email', e.target.value)}
+ disabled={isSubmitting}
+ placeholder={accountData.email}
+ />
+ <p className="text-xs text-gray-500 mt-1">
+ 비워두면 계정 이메일({accountData.email})을 사용합니다.
+ </p>
+ </div>
+
+ {/* 웹사이트 */}
+ <div>
+ <label className="block text-sm font-medium mb-1">웹사이트</label>
+ <Input
+ value={data.website}
+ onChange={(e) => handleInputChange('website', e.target.value)}
+ disabled={isSubmitting}
+ />
+ </div>
+ </div>
+ </div>
+
+ {/* 담당자 정보 */}
+ <div className="rounded-md border p-6 space-y-4">
+ <div className="flex items-center justify-between">
+ <h4 className="text-md font-semibold">담당자 정보 (최소 1명)</h4>
+ <Button
+ type="button"
+ variant="outline"
+ onClick={addContact}
+ disabled={isSubmitting}
+ >
+ <Plus className="mr-1 h-4 w-4" />
+ 담당자 추가
+ </Button>
+ </div>
+
+ <div className="space-y-4">
+ {data.contacts.map((contact, index) => (
+ <div
+ key={index}
+ className="bg-muted/10 rounded-md p-4 space-y-4"
+ >
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
+ <div>
+ <label className="block text-sm font-medium mb-1">
+ 담당자명 <span className="text-red-500">*</span>
+ </label>
+ <Input
+ value={contact.contactName}
+ onChange={(e) => updateContact(index, 'contactName', e.target.value)}
+ disabled={isSubmitting}
+ />
+ </div>
+
+ <div>
+ <label className="block text-sm font-medium mb-1">
+ 직급 <span className="text-red-500">*</span>
+ </label>
+ <Input
+ value={contact.contactPosition}
+ onChange={(e) => updateContact(index, 'contactPosition', e.target.value)}
+ disabled={isSubmitting}
+ />
+ </div>
+
+ <div>
+ <label className="block text-sm font-medium mb-1">
+ 부서 <span className="text-red-500">*</span>
+ </label>
+ <Input
+ value={contact.contactDepartment}
+ onChange={(e) => updateContact(index, 'contactDepartment', e.target.value)}
+ disabled={isSubmitting}
+ />
+ </div>
+
+ <div>
+ <label className="block text-sm font-medium mb-1">
+ 담당업무 <span className="text-red-500">*</span>
+ </label>
+ <Select
+ value={contact.contactTask}
+ onValueChange={(value) => updateContact(index, 'contactTask', value)}
+ disabled={isSubmitting}
+ >
+ <SelectTrigger>
+ <SelectValue placeholder="담당업무를 선택하세요" />
+ </SelectTrigger>
+ <SelectContent>
+ {contactTaskOptions.map((option) => (
+ <SelectItem key={option.value} value={option.value}>
+ {option.label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ </div>
+
+ <div>
+ <label className="block text-sm font-medium mb-1">
+ 이메일 <span className="text-red-500">*</span>
+ </label>
+ <Input
+ value={contact.contactEmail}
+ onChange={(e) => updateContact(index, 'contactEmail', e.target.value)}
+ disabled={isSubmitting}
+ />
+ </div>
+
+ <div>
+ <label className="block text-sm font-medium mb-1">
+ 전화번호 <span className="text-red-500">*</span>
+ </label>
+ <Input
+ value={contact.contactPhone}
+ onChange={(e) => updateContact(index, 'contactPhone', e.target.value)}
+ placeholder={getPhonePlaceholder(data.country)}
+ disabled={isSubmitting}
+ />
+ </div>
+ </div>
+
+ {data.contacts.length > 1 && (
+ <div className="flex justify-end">
+ <Button
+ variant="destructive"
+ onClick={() => removeContact(index)}
+ disabled={isSubmitting}
+ >
+ <X className="mr-1 h-4 w-4" />
+ 삭제
+ </Button>
+ </div>
+ )}
+ </div>
+ ))}
+ </div>
+ </div>
+
+ {/* 한국 사업자 정보 */}
+ {data.country === "KR" && (
+ <div className="rounded-md border p-6 space-y-4">
+ <h4 className="text-md font-semibold">한국 사업자 정보</h4>
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+ <div>
+ <label className="block text-sm font-medium mb-1">
+ 대표자 이름 <span className="text-red-500">*</span>
+ </label>
+ <Input
+ value={data.representativeName}
+ onChange={(e) => handleInputChange('representativeName', e.target.value)}
+ disabled={isSubmitting}
+ />
+ </div>
+ <div>
+ <label className="block text-sm font-medium mb-1">
+ 대표자 생년월일 <span className="text-red-500">*</span>
+ </label>
+ <Input
+ placeholder="YYYY-MM-DD"
+ value={data.representativeBirth}
+ onChange={(e) => handleInputChange('representativeBirth', e.target.value)}
+ disabled={isSubmitting}
+ />
+ </div>
+ <div>
+ <label className="block text-sm font-medium mb-1">
+ 대표자 이메일 <span className="text-red-500">*</span>
+ </label>
+ <Input
+ value={data.representativeEmail}
+ onChange={(e) => handleInputChange('representativeEmail', e.target.value)}
+ disabled={isSubmitting}
+ />
+ </div>
+ <div>
+ <label className="block text-sm font-medium mb-1">
+ 대표자 전화번호 <span className="text-red-500">*</span>
+ </label>
+ <Input
+ value={data.representativePhone}
+ onChange={(e) => handleInputChange('representativePhone', e.target.value)}
+ disabled={isSubmitting}
+ />
+ </div>
+ <div>
+ <label className="block text-sm font-medium mb-1">
+ 법인등록번호 <span className="text-red-500">*</span>
+ </label>
+ <Input
+ value={data.corporateRegistrationNumber}
+ onChange={(e) => handleInputChange('corporateRegistrationNumber', e.target.value)}
+ disabled={isSubmitting}
+ />
+ </div>
+ <div className="flex items-center space-x-2">
+ <input
+ type="checkbox"
+ id="work-experience"
+ checked={data.representativeWorkExpirence}
+ onChange={(e) => handleInputChange('representativeWorkExpirence', e.target.checked)}
+ disabled={isSubmitting}
+ className="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
+ />
+ <label htmlFor="work-experience" className="text-sm">
+ 대표자 삼성중공업 근무이력
+ </label>
+ </div>
+ </div>
+ </div>
+ )}
+
+ {/* 필수 첨부 서류 */}
+ <div className="rounded-md border p-6 space-y-6">
+ <h4 className="text-md font-semibold">필수 첨부 서류</h4>
+
+ <FileUploadSection
+ title="사업자등록증"
+ description="사업자등록증 스캔본 또는 사진을 업로드해주세요."
+ files={businessRegistrationFiles}
+ onDropAccepted={businessRegistrationHandler.onDropAccepted}
+ onDropRejected={businessRegistrationHandler.onDropRejected}
+ removeFile={businessRegistrationHandler.removeFile}
+ isSubmitting={isSubmitting}
+ />
+
+ <FileUploadSection
+ title="ISO 인증서"
+ description="ISO 9001, ISO 14001 등 품질/환경 관리 인증서를 업로드해주세요."
+ files={isoCertificationFiles}
+ onDropAccepted={isoCertificationHandler.onDropAccepted}
+ onDropRejected={isoCertificationHandler.onDropRejected}
+ removeFile={isoCertificationHandler.removeFile}
+ isSubmitting={isSubmitting}
+ />
+
+ <FileUploadSection
+ title="신용평가보고서"
+ description="신용평가기관에서 발급한 발행 1년 이내의 신용평가보고서를 업로드해주세요."
+ files={creditReportFiles}
+ onDropAccepted={creditReportHandler.onDropAccepted}
+ onDropRejected={creditReportHandler.onDropRejected}
+ removeFile={creditReportHandler.removeFile}
+ isSubmitting={isSubmitting}
+ />
+
+ {data.country !== "KR" && (
+ <FileUploadSection
+ title="대금지급 통장사본"
+ description="대금 지급용 은행 계좌의 통장 사본 또는 계좌증명서를 업로드해주세요."
+ files={bankAccountFiles}
+ onDropAccepted={bankAccountHandler.onDropAccepted}
+ onDropRejected={bankAccountHandler.onDropRejected}
+ removeFile={bankAccountHandler.removeFile}
+ isSubmitting={isSubmitting}
+ />
+ )}
+ </div>
+
+ <div className="flex justify-between">
+ <Button variant="outline" onClick={onBack}>
+ 이전
+ </Button>
+ <Button onClick={handleSubmit} disabled={!isFormValid || isSubmitting}>
+ {isSubmitting ? (
+ <>
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+ 등록 중...
+ </>
+ ) : (
+ "등록 완료"
+ )}
+ </Button>
+ </div>
+ </div>
+ );
+}
+
+// 파일 업로드 섹션 컴포넌트
+function FileUploadSection({
+ title,
+ description,
+ files,
+ onDropAccepted,
+ onDropRejected,
+ removeFile,
+ isSubmitting,
+ required = true
+}) {
+ return (
<div className="space-y-4">
<div>
<h5 className="text-sm font-medium">
@@ -517,654 +1285,5 @@ export function JoinForm() {
</div>
)}
</div>
- )
-
- // Render
- return (
- <div className="container py-6">
- <section className="overflow-hidden rounded-md border bg-background shadow-sm">
- <div className="p-6 md:p-10 space-y-6">
- <div className="space-y-2">
- <h3 className="text-xl font-semibold">
- {defaultTaxId}{" "}
- {t("joinForm.title", {
- defaultValue: "Vendor Administrator Creation",
- })}
- </h3>
- <p className="text-sm text-muted-foreground">
- {t("joinForm.description", {
- defaultValue:
- "Please provide basic company information and attach any required documents (e.g., business registration). We will review and approve as soon as possible.",
- })}
- </p>
- </div>
-
- <Separator />
-
- <Form {...form}>
- <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
- {/* ─────────────────────────────────────────
- Basic Info
- ───────────────────────────────────────── */}
- <div className="rounded-md border p-4 space-y-4">
- <h4 className="text-md font-semibold">기본 정보</h4>
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
- {/* Vendor Type */}
- <FormField
- control={form.control}
- name="vendorTypeId"
- render={({ field }) => {
- const selectedType = vendorTypes.find(type => type.id === field.value);
- const displayName = lng === "ko" ?
- (selectedType?.nameKo || "") :
- (selectedType?.nameEn || "");
-
- return (
- <FormItem>
- <FormLabel className="after:content-['*'] after:ml-0.5 after:text-red-500">
- 업체유형
- </FormLabel>
- <Popover>
- <PopoverTrigger asChild>
- <FormControl>
- <Button
- variant="outline"
- role="combobox"
- className={cn(
- "w-full justify-between",
- !field.value && "text-muted-foreground"
- )}
- disabled={isSubmitting || isLoadingVendorTypes}
- >
- {isLoadingVendorTypes
- ? "Loading..."
- : displayName || "업체유형 선택"}
- <ChevronsUpDown className="ml-2 opacity-50" />
- </Button>
- </FormControl>
- </PopoverTrigger>
- <PopoverContent className="w-full p-0">
- <Command>
- <CommandInput placeholder="업체유형 검색..." />
- <CommandList>
- <CommandEmpty>No vendor type found.</CommandEmpty>
- <CommandGroup>
- {vendorTypes.map((type) => (
- <CommandItem
- key={type.id}
- value={lng === "ko" ? type.nameKo : type.nameEn}
- onSelect={() => field.onChange(type.id)}
- >
- <Check
- className={cn(
- "mr-2",
- type.id === field.value
- ? "opacity-100"
- : "opacity-0"
- )}
- />
- {lng === "ko" ? type.nameKo : type.nameEn}
- </CommandItem>
- ))}
- </CommandGroup>
- </CommandList>
- </Command>
- </PopoverContent>
- </Popover>
- <FormMessage />
- </FormItem>
- );
- }}
- />
-
- {/* vendorName */}
- <FormField
- control={form.control}
- name="vendorName"
- render={({ field }) => (
- <FormItem>
- <FormLabel className="after:content-['*'] after:ml-0.5 after:text-red-500">
- 업체명
- </FormLabel>
- <FormControl>
- <Input {...field} disabled={isSubmitting} />
- </FormControl>
- <FormDescription>
- {form.watch("country") === "KR"
- ? "사업자 등록증에 표기된 정확한 회사명을 입력하세요."
- : "해외 업체의 경우 영문 회사명을 입력하세요."}
- </FormDescription>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* Items */}
- <FormField
- control={form.control}
- name="items"
- render={({ field }) => (
- <FormItem>
- <FormLabel className="after:content-['*'] after:ml-0.5 after:text-red-500">
- 공급품목
- </FormLabel>
- <FormControl>
- <Input {...field} disabled={isSubmitting} />
- </FormControl>
- <FormDescription>
- 공급 가능한 제품/서비스를 입력하세요
- </FormDescription>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* Address */}
- <FormField
- control={form.control}
- name="address"
- render={({ field }) => (
- <FormItem>
- <FormLabel>주소</FormLabel>
- <FormControl>
- <Input {...field} disabled={isSubmitting} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* Country */}
- <FormField
- control={form.control}
- name="country"
- render={({ field }) => {
- const selectedCountry = enhancedCountryArray.find(
- (c) => c.code === field.value
- )
- return (
- <FormItem>
- <FormLabel className="after:content-['*'] after:ml-0.5 after:text-red-500">
- 국가
- </FormLabel>
- <Popover>
- <PopoverTrigger asChild>
- <FormControl>
- <Button
- variant="outline"
- role="combobox"
- className={cn(
- "w-full justify-between",
- !field.value && "text-muted-foreground"
- )}
- disabled={isSubmitting}
- >
- {selectedCountry
- ? selectedCountry.label
- : "국가 선택"}
- <ChevronsUpDown className="ml-2 opacity-50" />
- </Button>
- </FormControl>
- </PopoverTrigger>
- <PopoverContent className="w-full p-0">
- <Command>
- <CommandInput placeholder="국가 검색..." />
- <CommandList>
- <CommandEmpty>No country found.</CommandEmpty>
- <CommandGroup>
- {enhancedCountryArray.map((country) => (
- <CommandItem
- key={country.code}
- value={country.label}
- onSelect={() =>
- field.onChange(country.code)
- }
- >
- <Check
- className={cn(
- "mr-2",
- country.code === field.value
- ? "opacity-100"
- : "opacity-0"
- )}
- />
- {country.label}
- </CommandItem>
- ))}
- </CommandGroup>
- </CommandList>
- </Command>
- </PopoverContent>
- </Popover>
- <FormMessage />
- </FormItem>
- )
- }}
- />
- {/* Phone */}
- <FormField
- control={form.control}
- name="phone"
- render={({ field }) => (
- <FormItem>
- <FormLabel className="after:content-['*'] after:ml-0.5 after:text-red-500">
- 대표 전화
- </FormLabel>
- <FormControl>
- <Input
- {...field}
- placeholder={getPhonePlaceholder(form.watch("country"))}
- disabled={isSubmitting}
- className={cn(
- form.formState.errors.phone && "border-red-500"
- )}
- />
- </FormControl>
- <FormDescription className="text-xs text-muted-foreground">
- {getPhoneDescription(form.watch("country"))}
- </FormDescription>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* Email */}
- <FormField
- control={form.control}
- name="email"
- render={({ field }) => (
- <FormItem>
- <FormLabel className="after:content-['*'] after:ml-0.5 after:text-red-500">
- 대표 이메일
- </FormLabel>
- <FormControl>
- <Input {...field} disabled={isSubmitting} />
- </FormControl>
- <FormDescription>
- 회사 도메인 이메일을 사용하세요. (naver.com, gmail.com, daum.net 등의 개인 이메일은 지양해주세요)
- </FormDescription>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* Website */}
- <FormField
- control={form.control}
- name="website"
- render={({ field }) => (
- <FormItem>
- <FormLabel>웹사이트</FormLabel>
- <FormControl>
- <Input {...field} disabled={isSubmitting} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- </div>
- </div>
-
- {/* ─────────────────────────────────────────
- 담당자 정보 (contacts)
- ───────────────────────────────────────── */}
- <div className="rounded-md border p-4 space-y-4">
- <div className="flex items-center justify-between">
- <h4 className="text-md font-semibold">담당자 정보 (최소 1명)</h4>
- <Button
- type="button"
- variant="outline"
- onClick={() =>
- addContact({
- contactName: "",
- contactPosition: "",
- contactDepartment: "",
- contactTask: "",
- contactEmail: "",
- contactPhone: "",
- })
- }
- disabled={isSubmitting}
- >
- <Plus className="mr-1 h-4 w-4" />
- Add Contact
- </Button>
- </div>
-
- <div className="space-y-2">
- {contactFields.map((contact, index) => (
- <div
- key={contact.id}
- className="bg-muted/10 rounded-md p-4 space-y-4"
- >
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
- {/* contactName */}
- <FormField
- control={form.control}
- name={`contacts.${index}.contactName`}
- render={({ field }) => (
- <FormItem>
- <FormLabel className="after:content-['*'] after:ml-0.5 after:text-red-500">
- 담당자명
- </FormLabel>
- <FormControl>
- <Input {...field} disabled={isSubmitting} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* contactPosition */}
- <FormField
- control={form.control}
- name={`contacts.${index}.contactPosition`}
- render={({ field }) => (
- <FormItem>
- <FormLabel className="after:content-['*'] after:ml-0.5 after:text-red-500">
- 직급
- </FormLabel>
- <FormControl>
- <Input {...field} disabled={isSubmitting} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* contactDepartment */}
- <FormField
- control={form.control}
- name={`contacts.${index}.contactDepartment`}
- render={({ field }) => (
- <FormItem>
- <FormLabel className="after:content-['*'] after:ml-0.5 after:text-red-500">
- 부서
- </FormLabel>
- <FormControl>
- <Input {...field} disabled={isSubmitting} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* contactTask - Dropdown */}
- <FormField
- control={form.control}
- name={`contacts.${index}.contactTask`}
- render={({ field }) => (
- <FormItem>
- <FormLabel className="after:content-['*'] after:ml-0.5 after:text-red-500">
- 담당업무
- </FormLabel>
- <Select onValueChange={field.onChange} value={field.value} disabled={isSubmitting}>
- <FormControl>
- <SelectTrigger>
- <SelectValue placeholder="담당업무를 선택하세요" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- {contactTaskOptions.map((option) => (
- <SelectItem key={option.value} value={option.value}>
- {option.label}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* contactEmail */}
- <FormField
- control={form.control}
- name={`contacts.${index}.contactEmail`}
- render={({ field }) => (
- <FormItem>
- <FormLabel className="after:content-['*'] after:ml-0.5 after:text-red-500">
- 이메일
- </FormLabel>
- <FormControl>
- <Input {...field} disabled={isSubmitting} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* contactPhone */}
- <FormField
- control={form.control}
- name={`contacts.${index}.contactPhone`}
- render={({ field }) => (
- <FormItem>
- <FormLabel className="after:content-['*'] after:ml-0.5 after:text-red-500">
- 전화번호
- </FormLabel>
- <FormControl>
- <Input
- {...field}
- placeholder={getPhonePlaceholder(form.watch("country"))}
- disabled={isSubmitting}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- </div>
-
- {/* Remove contact button row */}
- {contactFields.length > 1 && (
- <div className="flex justify-end">
- <Button
- variant="destructive"
- onClick={() => removeContact(index)}
- disabled={isSubmitting}
- >
- <X className="mr-1 h-4 w-4" />
- Remove
- </Button>
- </div>
- )}
- </div>
- ))}
- </div>
- </div>
-
- {/* ─────────────────────────────────────────
- 한국 사업자 (country === "KR")
- ───────────────────────────────────────── */}
- {form.watch("country") === "KR" && (
- <div className="rounded-md border p-4 space-y-4">
- <h4 className="text-md font-semibold">한국 사업자 정보</h4>
-
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
- <FormField
- control={form.control}
- name="representativeName"
- render={({ field }) => (
- <FormItem>
- <FormLabel className="after:content-['*'] after:ml-0.5 after:text-red-500">
- 대표자 이름
- </FormLabel>
- <FormControl>
- <Input {...field} disabled={isSubmitting} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={form.control}
- name="representativeBirth"
- render={({ field }) => (
- <FormItem>
- <FormLabel className="after:content-['*'] after:ml-0.5 after:text-red-500">
- 대표자 생년월일
- </FormLabel>
- <FormControl>
- <Input
- placeholder="YYYY-MM-DD"
- {...field}
- disabled={isSubmitting}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={form.control}
- name="representativeEmail"
- render={({ field }) => (
- <FormItem>
- <FormLabel className="after:content-['*'] after:ml-0.5 after:text-red-500">
- 대표자 이메일
- </FormLabel>
- <FormControl>
- <Input {...field} disabled={isSubmitting} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={form.control}
- name="representativePhone"
- render={({ field }) => (
- <FormItem>
- <FormLabel className="after:content-['*'] after:ml-0.5 after:text-red-500">
- 대표자 전화번호
- </FormLabel>
- <FormControl>
- <Input {...field} disabled={isSubmitting} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={form.control}
- name="corporateRegistrationNumber"
- render={({ field }) => (
- <FormItem>
- <FormLabel className="after:content-['*'] after:ml-0.5 after:text-red-500">
- 법인등록번호
- </FormLabel>
- <FormControl>
- <Input {...field} disabled={isSubmitting} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- <FormField
- control={form.control}
- name="representativeWorkExpirence"
- render={({ field }) => (
- <FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
- <FormControl>
- <Checkbox
- checked={field.value}
- onCheckedChange={field.onChange}
- disabled={isSubmitting}
- />
- </FormControl>
- <div className="space-y-1 leading-none">
- <FormLabel>
- 대표자 삼성중공업 근무이력
- </FormLabel>
- <FormDescription>
- 대표자가 삼성중공업에서 근무한 경험이 있는 경우 체크해주세요.
- </FormDescription>
- </div>
- </FormItem>
- )}
- />
-
- </div>
- </div>
- )}
-
- {/* ─────────────────────────────────────────
- Required Document Uploads
- ───────────────────────────────────────── */}
- <div className="rounded-md border p-4 space-y-6">
- <h4 className="text-md font-semibold">필수 첨부 서류</h4>
-
- {/* Business Registration */}
- <FileUploadSection
- title="사업자등록증"
- description="사업자등록증 스캔본 또는 사진을 업로드해주세요. 모든 내용이 선명하게 보여야 합니다."
- files={businessRegistrationFiles}
- onDropAccepted={businessRegistrationHandler.onDropAccepted}
- onDropRejected={businessRegistrationHandler.onDropRejected}
- removeFile={businessRegistrationHandler.removeFile}
- />
-
- <Separator />
-
- {/* ISO Certification */}
- <FileUploadSection
- title="ISO 인증서"
- description="ISO 9001, ISO 14001 등 품질/환경 관리 인증서를 업로드해주세요. 유효기간이 확인 가능해야 합니다."
- files={isoCertificationFiles}
- onDropAccepted={isoCertificationHandler.onDropAccepted}
- onDropRejected={isoCertificationHandler.onDropRejected}
- removeFile={isoCertificationHandler.removeFile}
- />
-
- <Separator />
-
- {/* Credit Report */}
- <FileUploadSection
- title="신용평가보고서"
- description="신용평가기관(KIS, NICE 등)에서 발급한 발행 1년 이내의 신용평가보고서를 업로드해주세요. 전년도 재무제표 필수표시. 신규업체, 영세업체로 재무제표 및 신용평가 결과가 없을 경우는 국세, 지방세 납입 증명으로 신용평가를 갈음할 수 있음"
- files={creditReportFiles}
- onDropAccepted={creditReportHandler.onDropAccepted}
- onDropRejected={creditReportHandler.onDropRejected}
- removeFile={creditReportHandler.removeFile}
- />
-
- {/* Bank Account Copy - Only for non-Korean companies */}
- {form.watch("country") !== "KR" && (
- <>
- <Separator />
- <FileUploadSection
- title="대금지급 통장사본"
- description="대금 지급용 은행 계좌의 통장 사본 또는 계좌증명서를 업로드해주세요. 계좌번호와 예금주명이 명확히 보여야 합니다."
- files={bankAccountFiles}
- onDropAccepted={bankAccountHandler.onDropAccepted}
- onDropRejected={bankAccountHandler.onDropRejected}
- removeFile={bankAccountHandler.removeFile}
- />
- </>
- )}
- </div>
-
- {/* ─────────────────────────────────────────
- Submit
- ───────────────────────────────────────── */}
- <div className="flex justify-end">
- <Button type="submit" disabled={!isFormValid || isSubmitting}>
- {isSubmitting ? (
- <>
- <Loader2 className="mr-2 h-4 w-4 animate-spin" />
- 등록 중...
- </>
- ) : (
- "Submit"
- )}
- </Button>
- </div>
- </form>
- </Form>
- </div>
- </section>
- </div>
- )
-} \ No newline at end of file
+ );
+}