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.tsx614
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