diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-08-11 09:02:00 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-08-11 09:02:00 +0000 |
| commit | cbb4c7fe0b94459162ad5e998bc05cd293e0ff96 (patch) | |
| tree | 0a26712f7685e4f6511e637b9a81269d90a47c8f /components/signup/join-form.tsx | |
| parent | eb654f88214095f71be142b989e620fd28db3f69 (diff) | |
(대표님) 입찰, EDP 변경사항 대응, spreadJS 오류 수정, 벤더실사 수정
Diffstat (limited to 'components/signup/join-form.tsx')
| -rw-r--r-- | components/signup/join-form.tsx | 614 |
1 files changed, 402 insertions, 212 deletions
diff --git a/components/signup/join-form.tsx b/components/signup/join-form.tsx index e9773d28..c94d435e 100644 --- a/components/signup/join-form.tsx +++ b/components/signup/join-form.tsx @@ -1,6 +1,6 @@ 'use client' -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Progress } from '@/components/ui/progress'; @@ -49,13 +49,28 @@ import { } from '@/components/ui/file-list'; import { ScrollArea } from '@/components/ui/scroll-area'; import prettyBytes from 'pretty-bytes'; +import { useToast } from "@/hooks/use-toast"; -// 기존 JoinForm에서 가져온 데이터들 +// libphonenumber-js 관련 imports +import { + parsePhoneNumberFromString, + isPossiblePhoneNumber, + isValidPhoneNumber, + validatePhoneNumberLength, + getExampleNumber, + AsYouType, + getCountries, + getCountryCallingCode +} from 'libphonenumber-js' +import examples from 'libphonenumber-js/examples.mobile.json' + +// 기존 imports... 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'; +import { checkEmailExists } from '@/lib/vendor-users/service'; i18nIsoCountries.registerLocale(enLocale); i18nIsoCountries.registerLocale(koLocale); @@ -75,8 +90,8 @@ const sortedCountryArray = [...countryArray].sort((a, b) => { const enhancedCountryArray = sortedCountryArray.map(country => ({ ...country, - label: locale === "ko" && country.code === "KR" - ? "대한민국 (South Korea)" + label: locale === "ko" && country.code === "KR" + ? "대한민국 (South Korea)" : country.label })); @@ -93,44 +108,6 @@ const contactTaskOptions = [ { value: "FIELD_SERVICE_ENGINEER", label: "FSE(야드작업자) Field Service Engineer" } ]; -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", - BY: "+375", BE: "+32", BZ: "+501", BJ: "+229", BM: "+1-441", BT: "+975", - BO: "+591", BA: "+387", BW: "+267", BR: "+55", BN: "+673", BG: "+359", - BF: "+226", BI: "+257", KH: "+855", CM: "+237", CA: "+1", CV: "+238", - KY: "+1-345", CF: "+236", TD: "+235", CL: "+56", CN: "+86", CO: "+57", - KM: "+269", CG: "+242", CD: "+243", CR: "+506", CI: "+225", HR: "+385", - CU: "+53", CY: "+357", CZ: "+420", DK: "+45", DJ: "+253", DM: "+1-767", - DO: "+1-809", EC: "+593", EG: "+20", SV: "+503", GQ: "+240", ER: "+291", - EE: "+372", ET: "+251", FJ: "+679", FI: "+358", FR: "+33", GA: "+241", - GM: "+220", GE: "+995", DE: "+49", GH: "+233", GR: "+30", GD: "+1-473", - GT: "+502", GN: "+224", GW: "+245", GY: "+592", HT: "+509", HN: "+504", - HK: "+852", HU: "+36", IS: "+354", IN: "+91", ID: "+62", IR: "+98", - IQ: "+964", IE: "+353", IL: "+972", IT: "+39", JM: "+1-876", JP: "+81", - JO: "+962", KZ: "+7", KE: "+254", KI: "+686", KR: "+82", KW: "+965", - KG: "+996", LA: "+856", LV: "+371", LB: "+961", LS: "+266", LR: "+231", - LY: "+218", LI: "+423", LT: "+370", LU: "+352", MK: "+389", MG: "+261", - MW: "+265", MY: "+60", MV: "+960", ML: "+223", MT: "+356", MH: "+692", - MR: "+222", MU: "+230", MX: "+52", FM: "+691", MD: "+373", MC: "+377", - MN: "+976", ME: "+382", MA: "+212", MZ: "+258", MM: "+95", NA: "+264", - NR: "+674", NP: "+977", NL: "+31", NZ: "+64", NI: "+505", NE: "+227", - NG: "+234", NU: "+683", KP: "+850", NO: "+47", OM: "+968", PK: "+92", - PW: "+680", PS: "+970", PA: "+507", PG: "+675", PY: "+595", PE: "+51", - PH: "+63", PL: "+48", PT: "+351", PR: "+1-787", QA: "+974", RO: "+40", - RU: "+7", RW: "+250", KN: "+1-869", LC: "+1-758", VC: "+1-784", WS: "+685", - SM: "+378", ST: "+239", SA: "+966", SN: "+221", RS: "+381", SC: "+248", - SL: "+232", SG: "+65", SK: "+421", SI: "+386", SB: "+677", SO: "+252", - ZA: "+27", SS: "+211", ES: "+34", LK: "+94", SD: "+249", SR: "+597", - SZ: "+268", SE: "+46", CH: "+41", SY: "+963", TW: "+886", TJ: "+992", - TZ: "+255", TH: "+66", TL: "+670", TG: "+228", TK: "+690", TO: "+676", - TT: "+1-868", TN: "+216", TR: "+90", TM: "+993", TV: "+688", UG: "+256", - UA: "+380", AE: "+971", GB: "+44", US: "+1", UY: "+598", UZ: "+998", - VU: "+678", VA: "+39-06", VE: "+58", VN: "+84", YE: "+967", ZM: "+260", - ZW: "+263" -}; - const MAX_FILE_SIZE = 3e9; // 스텝 정의 @@ -140,6 +117,241 @@ const STEPS = [ { id: 3, title: '업체 등록', description: '업체 정보 및 서류 제출', icon: Building } ]; +// ========== 전화번호 처리 유틸리티 함수들 ========== + +/** + * 국가별 전화번호 예시를 가져오는 함수 + */ +function getPhoneExample(countryCode) { + if (!countryCode) return null; + try { + return getExampleNumber(countryCode, examples); + } catch { + return null; + } +} + +/** + * 국가별 전화번호 플레이스홀더를 생성하는 함수 + */ +function getPhonePlaceholder(countryCode) { + if (!countryCode) return "국가를 먼저 선택해주세요"; + + const example = getPhoneExample(countryCode); + if (example) { + // 국내 형식으로 표시 (더 친숙함) + return example.formatNational(); + } + + // 예시가 없는 경우 국가 코드 기반 플레이스홀더 + try { + const callingCode = getCountryCallingCode(countryCode); + return `+${callingCode} ...`; + } catch { + return "전화번호를 입력하세요"; + } +} + +/** + * 국가별 전화번호 설명을 생성하는 함수 + */ +function getPhoneDescription(countryCode) { + if (!countryCode) return "국가를 먼저 선택하여 올바른 전화번호 형식을 확인하세요."; + + const example = getPhoneExample(countryCode); + if (example) { + return `예시: ${example.formatNational()} 또는 ${example.formatInternational()}`; + } + + try { + const callingCode = getCountryCallingCode(countryCode); + return `국가 코드: +${callingCode}. 국내 형식 또는 국제 형식으로 입력 가능합니다.`; + } catch { + return "올바른 전화번호 형식으로 입력해주세요."; + } +} + +/** + * 전화번호 검증 결과를 반환하는 함수 + */ +function validatePhoneNumber(phoneNumber, countryCode) { + if (!phoneNumber || !countryCode) { + return { isValid: false, error: null, formatted: phoneNumber }; + } + + try { + // 1. 기본 파싱 시도 + const parsed = parsePhoneNumberFromString(phoneNumber, countryCode); + + if (!parsed) { + return { + isValid: false, + error: "올바른 전화번호 형식이 아닙니다.", + formatted: phoneNumber + }; + } + + // 2. 길이 검증 + const lengthValidation = validatePhoneNumberLength(phoneNumber, countryCode); + if (lengthValidation !== undefined) { + const lengthErrors = { + 'TOO_SHORT': '전화번호가 너무 짧습니다.', + 'TOO_LONG': '전화번호가 너무 깁니다.', + 'INVALID_LENGTH': '전화번호 길이가 올바르지 않습니다.' + }; + return { + isValid: false, + error: lengthErrors[lengthValidation] || '전화번호 길이가 올바르지 않습니다.', + formatted: phoneNumber + }; + } + + // 3. 가능성 검증 + if (!isPossiblePhoneNumber(phoneNumber, countryCode)) { + return { + isValid: false, + error: "이 국가에서 가능하지 않은 전화번호 형식입니다.", + formatted: phoneNumber + }; + } + + // 4. 유효성 검증 + if (!isValidPhoneNumber(phoneNumber, countryCode)) { + return { + isValid: false, + error: "유효하지 않은 전화번호입니다.", + formatted: phoneNumber + }; + } + + // 모든 검증 통과 + return { + isValid: true, + error: null, + formatted: parsed.formatNational(), + international: parsed.formatInternational() + }; + + } catch (error) { + return { + isValid: false, + error: "전화번호 형식을 확인해주세요.", + formatted: phoneNumber + }; + } +} + +/** + * 실시간 전화번호 포맷팅을 위한 커스텀 훅 + */ +function usePhoneFormatter(countryCode) { + const [formatter, setFormatter] = useState(null); + + useEffect(() => { + if (countryCode) { + setFormatter(new AsYouType(countryCode)); + } else { + setFormatter(null); + } + }, [countryCode]); + + const formatPhone = useCallback((value) => { + if (!formatter) return value; + + // AsYouType은 매번 새로운 인스턴스를 사용해야 함 + const newFormatter = new AsYouType(countryCode); + return newFormatter.input(value); + }, [countryCode, formatter]); + + return formatPhone; +} + +// ========== 전화번호 입력 컴포넌트 ========== + +function PhoneInput({ + value, + onChange, + countryCode, + placeholder, + disabled = false, + onBlur = null, + className = "", + showValidation = true +}) { + const [touched, setTouched] = useState(false); + const [localValue, setLocalValue] = useState(value || ''); + const formatPhone = usePhoneFormatter(countryCode); + + // value prop이 변경될 때 localValue 동기화 + useEffect(() => { + setLocalValue(value || ''); + }, [value]); + + const validation = validatePhoneNumber(localValue, countryCode); + + const handleInputChange = (e) => { + const inputValue = e.target.value; + + // 실시간 포맷팅 적용 + const formattedValue = countryCode ? formatPhone(inputValue) : inputValue; + + setLocalValue(formattedValue); + onChange(formattedValue); + }; + + const handleBlur = () => { + setTouched(true); + if (onBlur) onBlur(); + }; + + const showError = showValidation && touched && localValue && !validation.isValid; + const showSuccess = showValidation && touched && localValue && validation.isValid; + + return ( + <div className="space-y-2"> + <Input + type="tel" + value={localValue} + onChange={handleInputChange} + onBlur={handleBlur} + placeholder={placeholder || getPhonePlaceholder(countryCode)} + disabled={disabled} + className={cn( + className, + showError && "border-red-500 focus:border-red-500", + showSuccess && "border-green-500 focus:border-green-500" + )} + /> + + {showValidation && ( + <div className="text-xs space-y-1"> + {/* 설명 텍스트 */} + <p className="text-muted-foreground"> + {getPhoneDescription(countryCode)} + </p> + + {/* 오류 메시지 */} + {showError && ( + <p className="text-red-500 font-medium"> + {validation.error} + </p> + )} + + {/* 성공 메시지 */} + {showSuccess && ( + <p className="text-green-600 font-medium"> + ✓ 올바른 전화번호입니다. + {validation.international && ` (${validation.international})`} + </p> + )} + </div> + )} + </div> + ); +} + +// ========== 메인 컴포넌트 ========== + export default function JoinForm() { const params = useParams() || {}; const lng = params.lng ? String(params.lng) : "ko"; @@ -147,6 +359,7 @@ export default function JoinForm() { const router = useRouter(); const searchParams = useSearchParams() || new URLSearchParams(); const defaultTaxId = searchParams.get("taxID") ?? ""; + const { toast } = useToast(); const [currentStep, setCurrentStep] = useState(1); const [completedSteps, setCompletedSteps] = useState(new Set()); @@ -161,9 +374,8 @@ export default function JoinForm() { const [accountData, setAccountData] = useState({ name: '', email: '', - password: '', - confirmPassword: '', - phone: '' + phone: '', + country: 'KR' // 기본값을 한국으로 설정 }); const [vendorData, setVendorData] = useState({ @@ -174,7 +386,7 @@ export default function JoinForm() { address: "", email: "", phone: "", - country: "", + country: "KR", // 기본값을 한국으로 설정 website: "", representativeName: "", representativeBirth: "", @@ -211,20 +423,9 @@ export default function JoinForm() { // 정책 버전 및 업체 타입 로드 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 { @@ -257,42 +458,8 @@ export default function JoinForm() { } }; - // 전화번호 플레이스홀더 함수들 - const getPhonePlaceholder = (countryCode) => { - 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} 전화번호`; - } - }; - - 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="w-full max-w-4xl mx-auto py-8 px-4"> {/* 진행률 표시 */} <div className="mb-8"> <div className="flex items-center justify-between mb-4"> @@ -302,7 +469,7 @@ export default function JoinForm() { </span> </div> <Progress value={progress} className="mb-6" /> - + {/* 스텝 네비게이션 */} <div className="flex items-center justify-between"> {STEPS.map((step, index) => { @@ -310,21 +477,21 @@ export default function JoinForm() { 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 + <div className={cn( - "flex flex-col items-center cursor-pointer transition-all", + "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 + isCompleted + ? "bg-green-500 border-green-500 text-white" + : isCurrent ? "bg-blue-500 border-blue-500 text-white" : "border-gray-300 text-gray-400" )}> @@ -346,7 +513,7 @@ export default function JoinForm() { </div> </div> </div> - + {index < STEPS.length - 1 && ( <ChevronRight className="w-5 h-5 text-gray-300 mx-4" /> )} @@ -359,31 +526,30 @@ export default function JoinForm() { {/* 스텝 콘텐츠 */} <div className="bg-white rounded-lg border shadow-sm p-6"> {currentStep === 1 && ( - <ConsentStep + <ConsentStep data={consentData} onChange={setConsentData} onNext={() => handleStepComplete(1)} - policyVersions={policyVersions} /> )} - + {currentStep === 2 && ( - <AccountStep + <AccountStep data={accountData} onChange={setAccountData} onNext={() => handleStepComplete(2)} onBack={() => setCurrentStep(1)} + enhancedCountryArray={enhancedCountryArray} /> )} - + {currentStep === 3 && ( - <VendorStep + <VendorStep data={vendorData} onChange={setVendorData} onBack={() => setCurrentStep(2)} onComplete={() => { handleStepComplete(3); - // 완료 후 대시보드로 이동 router.push(`/${lng}/partners/dashboard`); }} accountData={accountData} @@ -398,8 +564,6 @@ export default function JoinForm() { setCreditReportFiles={setCreditReportFiles} bankAccountFiles={bankAccountFiles} setBankAccountFiles={setBankAccountFiles} - getPhonePlaceholder={getPhonePlaceholder} - getPhoneDescription={getPhoneDescription} enhancedCountryArray={enhancedCountryArray} contactTaskOptions={contactTaskOptions} lng={lng} @@ -411,45 +575,45 @@ export default function JoinForm() { ); } - // Step 2: 계정 생성 -function AccountStep({ data, onChange, onNext, onBack }) { +function AccountStep({ + data, onChange, onNext, onBack, + enhancedCountryArray +}) { const [isLoading, setIsLoading] = useState(false); + const [emailCheckError, setEmailCheckError] = useState(''); - 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 })); }; + // 전화번호 validation + const phoneValidation = validatePhoneNumber(data.phone, data.country); + + // 전체 입력 유효성 + const isValid = + data.name && + data.email && + data.country && + data.phone && + phoneValidation.isValid; + + // 이메일 중복체크 + 다음단계 const handleNext = async () => { if (!isValid) return; setIsLoading(true); + setEmailCheckError(''); 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); + const isUsed = await checkEmailExists(data.email); + if (isUsed) { + setEmailCheckError('이미 사용 중인 이메일입니다.'); + return; } - onNext(); } catch (error) { - console.error('Email check error:', error); - alert('이메일 확인 중 오류가 발생했습니다.'); + setEmailCheckError('이메일 확인 중 오류가 발생했습니다.'); } finally { setIsLoading(false); } @@ -487,48 +651,69 @@ function AccountStep({ data, onChange, onNext, onBack }) { value={data.email} onChange={(e) => handleInputChange('email', e.target.value)} /> + {emailCheckError && ( + <p className="text-xs text-red-500 mt-1">{emailCheckError}</p> + )} </div> <div> <label className="block text-sm font-medium mb-1"> - 비밀번호 <span className="text-red-500">*</span> + 국가 <span className="text-red-500">*</span> </label> - <Input - type="password" - placeholder="비밀번호를 입력하세요" - value={data.password} - onChange={(e) => handleInputChange('password', e.target.value)} - /> + <Popover> + <PopoverTrigger asChild> + <Button + variant="outline" + role="combobox" + className={cn( + "w-full justify-between", + !data.country && "text-muted-foreground" + )} + > + {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 - 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" + <PhoneInput value={data.phone} - onChange={(e) => handleInputChange('phone', e.target.value)} + onChange={(value) => handleInputChange('phone', value)} + countryCode={data.country} + showValidation={true} /> - <p className="text-xs text-gray-500 mt-1"> - SMS 인증에 사용됩니다. 국제번호 형식으로 입력해주세요. - </p> </div> </div> @@ -544,19 +729,18 @@ function AccountStep({ data, onChange, onNext, onBack }) { ); } -// Step 3: 업체 등록 (기존 JoinForm 내용) +// Step 3: 업체 등록 function VendorStep(props) { return <CompleteVendorForm {...props} />; } - -// 완전한 업체 등록 폼 컴포넌트 (기존 JoinForm 내용) +// 나머지 CompeleteVendorForm과 FileUploadSection은 기존과 동일하되, +// PhoneInput 컴포넌트를 사용하도록 수정 function CompleteVendorForm({ data, onChange, onBack, onComplete, accountData, consentData, vendorTypes, isLoadingVendorTypes, businessRegistrationFiles, setBusinessRegistrationFiles, isoCertificationFiles, setIsoCertificationFiles, creditReportFiles, setCreditReportFiles, - bankAccountFiles, setBankAccountFiles, getPhonePlaceholder, getPhoneDescription, - enhancedCountryArray, contactTaskOptions, lng, policyVersions + bankAccountFiles, setBankAccountFiles, enhancedCountryArray, contactTaskOptions, lng, policyVersions }) { const [isSubmitting, setIsSubmitting] = useState(false); @@ -585,7 +769,7 @@ function CompleteVendorForm({ const updateContact = (index, field, value) => { onChange(prev => ({ ...prev, - contacts: prev.contacts.map((contact, i) => + contacts: prev.contacts.map((contact, i) => i === index ? { ...contact, [field]: value } : contact ) })); @@ -626,33 +810,40 @@ function CompleteVendorForm({ // 유효성 검사 const validateRequiredFiles = () => { const errors = []; - + if (businessRegistrationFiles.length === 0) { errors.push("사업자등록증을 업로드해주세요."); } - + if (isoCertificationFiles.length === 0) { errors.push("ISO 인증서를 업로드해주세요."); } - + if (creditReportFiles.length === 0) { errors.push("신용평가보고서를 업로드해주세요."); } - + if (data.country !== "KR" && bankAccountFiles.length === 0) { errors.push("대금지급 통장사본을 업로드해주세요."); } - + return errors; }; - 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; + // 전화번호 검증 + const vendorPhoneValidation = validatePhoneNumber(data.phone, data.country); + const contactsValid = data.contacts.length > 0 && + data.contacts[0].contactName && + data.contacts.every(contact => + contact.contactPhone ? validatePhoneNumber(contact.contactPhone, data.country).isValid : true + ); - // 최종 제출 + const isFormValid = data.vendorName && data.vendorTypeId && data.items && + data.country && data.phone && vendorPhoneValidation.isValid && data.email && + contactsValid && + validateRequiredFiles().length === 0; + + // 최종 제출 (기존과 동일) const handleSubmit = async () => { const fileErrors = validateRequiredFiles(); if (fileErrors.length > 0) { @@ -668,12 +859,11 @@ function CompleteVendorForm({ try { const formData = new FormData(); - // 통합 데이터 준비 const completeData = { account: accountData, vendor: { ...data, - email: data.email || accountData.email, // 업체 이메일이 없으면 계정 이메일 사용 + email: data.email || accountData.email, }, consents: { privacy_policy: { @@ -693,7 +883,6 @@ function CompleteVendorForm({ formData.append('completeData', JSON.stringify(completeData)); - // 파일들 추가 businessRegistrationFiles.forEach(file => { formData.append('businessRegistration', file); }); @@ -773,8 +962,8 @@ function CompleteVendorForm({ )} disabled={isSubmitting || isLoadingVendorTypes} > - {isLoadingVendorTypes - ? "Loading..." + {isLoadingVendorTypes + ? "Loading..." : vendorTypes.find(type => type.id === data.vendorTypeId)?.[lng === "ko" ? "nameKo" : "nameEn"] || "업체유형 선택"} <ChevronsUpDown className="ml-2 opacity-50" /> </Button> @@ -820,7 +1009,7 @@ function CompleteVendorForm({ disabled={isSubmitting} /> <p className="text-xs text-gray-500 mt-1"> - {data.country === "KR" + {data.country === "KR" ? "사업자 등록증에 표기된 정확한 회사명을 입력하세요." : "해외 업체의 경우 영문 회사명을 입력하세요."} </p> @@ -914,20 +1103,18 @@ function CompleteVendorForm({ </Popover> </div> - {/* 대표 전화 */} + {/* 대표 전화 - PhoneInput 사용 */} <div> <label className="block text-sm font-medium mb-1"> 대표 전화 <span className="text-red-500">*</span> </label> - <Input + <PhoneInput value={data.phone} - onChange={(e) => handleInputChange('phone', e.target.value)} - placeholder={getPhonePlaceholder(data.country)} + onChange={(value) => handleInputChange('phone', value)} + countryCode={data.country} disabled={isSubmitting} + showValidation={true} /> - <p className="text-xs text-gray-500 mt-1"> - {getPhoneDescription(data.country)} - </p> </div> {/* 대표 이메일 */} @@ -958,7 +1145,7 @@ function CompleteVendorForm({ </div> </div> - {/* 담당자 정보 */} + {/* 담당자 정보 - PhoneInput 사용 */} <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> @@ -1017,7 +1204,7 @@ function CompleteVendorForm({ <label className="block text-sm font-medium mb-1"> 담당업무 <span className="text-red-500">*</span> </label> - <Select + <Select value={contact.contactTask} onValueChange={(value) => updateContact(index, 'contactTask', value)} disabled={isSubmitting} @@ -1050,11 +1237,12 @@ function CompleteVendorForm({ <label className="block text-sm font-medium mb-1"> 전화번호 <span className="text-red-500">*</span> </label> - <Input + <PhoneInput value={contact.contactPhone} - onChange={(e) => updateContact(index, 'contactPhone', e.target.value)} - placeholder={getPhonePlaceholder(data.country)} + onChange={(value) => updateContact(index, 'contactPhone', value)} + countryCode={data.country} disabled={isSubmitting} + showValidation={true} /> </div> </div> @@ -1076,7 +1264,7 @@ function CompleteVendorForm({ </div> </div> - {/* 한국 사업자 정보 */} + {/* 한국 사업자 정보 - PhoneInput 사용 */} {data.country === "KR" && ( <div className="rounded-md border p-6 space-y-4"> <h4 className="text-md font-semibold">한국 사업자 정보</h4> @@ -1116,10 +1304,12 @@ function CompleteVendorForm({ <label className="block text-sm font-medium mb-1"> 대표자 전화번호 <span className="text-red-500">*</span> </label> - <Input + <PhoneInput value={data.representativePhone} - onChange={(e) => handleInputChange('representativePhone', e.target.value)} + onChange={(value) => handleInputChange('representativePhone', value)} + countryCode="KR" disabled={isSubmitting} + showValidation={true} /> </div> <div> @@ -1152,7 +1342,7 @@ function CompleteVendorForm({ {/* 필수 첨부 서류 */} <div className="rounded-md border p-6 space-y-6"> <h4 className="text-md font-semibold">필수 첨부 서류</h4> - + <FileUploadSection title="사업자등록증" description="사업자등록증 스캔본 또는 사진을 업로드해주세요." @@ -1215,16 +1405,16 @@ function CompleteVendorForm({ ); } -// 파일 업로드 섹션 컴포넌트 -function FileUploadSection({ - title, - description, - files, - onDropAccepted, - onDropRejected, +// 파일 업로드 섹션 컴포넌트 (기존과 동일) +function FileUploadSection({ + title, + description, + files, + onDropAccepted, + onDropRejected, removeFile, isSubmitting, - required = true + required = true }) { return ( <div className="space-y-4"> @@ -1235,7 +1425,7 @@ function FileUploadSection({ </h5> <p className="text-xs text-muted-foreground mt-1">{description}</p> </div> - + <Dropzone maxSize={MAX_FILE_SIZE} multiple @@ -1259,7 +1449,7 @@ function FileUploadSection({ </DropzoneZone> )} </Dropzone> - + {files.length > 0 && ( <div className="mt-2"> <ScrollArea className="max-h-32"> @@ -1286,4 +1476,4 @@ function FileUploadSection({ )} </div> ); -} +}
\ No newline at end of file |
