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.tsx561
1 files changed, 342 insertions, 219 deletions
diff --git a/components/signup/join-form.tsx b/components/signup/join-form.tsx
index 2dcde518..71ecbd1c 100644
--- a/components/signup/join-form.tsx
+++ b/components/signup/join-form.tsx
@@ -75,54 +75,102 @@ import { checkEmailExists } from '@/lib/vendor-users/service';
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,
-}));
-
-const sortedCountryArray = [...countryArray].sort((a, b) => {
- if (a.code === "KR") return -1;
- if (b.code === "KR") return 1;
- return a.label.localeCompare(b.label);
-});
-
-const enhancedCountryArray = sortedCountryArray.map(country => ({
- ...country,
- label: locale === "ko" && country.code === "KR"
- ? "대한민국 (South Korea)"
- : country.label
-}));
-
-const contactTaskOptions = [
- { value: "PRESIDENT_DIRECTOR", label: "회사대표 President/Director" },
- { value: "SALES_MANAGEMENT", label: "영업관리 Sales Management" },
- { value: "ENGINEERING_DESIGN", label: "설계/기술 Engineering/Design" },
- { value: "PROCUREMENT", label: "구매 Procurement" },
- { value: "DELIVERY_CONTROL", label: "납기/출하/운송 Delivery Control" },
- { value: "PM_MANUFACTURING", label: "PM/생산관리 PM/Manufacturing" },
- { value: "QUALITY_MANAGEMENT", label: "품질관리 Quality Management" },
- { value: "SHIPPING_DOC_MANAGEMENT", label: "세금계산서/납품서관리 Shipping Doc. Management" },
- { value: "AS_MANAGEMENT", label: "A/S 관리 A/S Management" },
- { value: "FIELD_SERVICE_ENGINEER", label: "FSE(야드작업자) Field Service Engineer" }
-];
+// Types
+interface CountryOption {
+ code: string;
+ label: string;
+}
-const MAX_FILE_SIZE = 3e9;
+interface VendorType {
+ id: number;
+ nameKo: string;
+ nameEn: string;
+}
+
+interface ContactTaskOption {
+ value: string;
+ label: string;
+}
+
+interface Contact {
+ contactName: string;
+ contactPosition: string;
+ contactDepartment: string;
+ contactTask: string;
+ contactEmail: string;
+ contactPhone: string;
+}
+
+interface AccountData {
+ name: string;
+ email: string;
+ phone: string;
+ country: string;
+}
+
+interface VendorData {
+ vendorName: string;
+ vendorTypeId?: number;
+ items: string;
+ taxId: string;
+ address: string;
+ email: string;
+ phone: string;
+ country: string;
+ website: string;
+ representativeName: string;
+ representativeBirth: string;
+ representativeEmail: string;
+ representativePhone: string;
+ corporateRegistrationNumber: string;
+ representativeWorkExpirence: boolean;
+ contacts: Contact[];
+}
+
+interface ConsentData {
+ privacy: boolean;
+ terms: boolean;
+ marketing: boolean;
+}
+
+interface PhoneValidation {
+ isValid: boolean;
+ error: string | null;
+ formatted: string;
+ international?: string;
+}
+
+// Country data setup
+const getCountryData = (lng: string): CountryOption[] => {
+ const locale = lng === 'ko' ? "ko" : "en";
+ const countryMap = i18nIsoCountries.getNames(locale, { select: "official" });
+ const countryArray = Object.entries(countryMap).map(([code, label]) => ({
+ code,
+ label,
+ }));
+
+ const sortedCountryArray = [...countryArray].sort((a, b) => {
+ if (a.code === "KR") return -1;
+ if (b.code === "KR") return 1;
+ return a.label.localeCompare(b.label);
+ });
-// 스텝 정의
-const STEPS = [
- { id: 1, title: '약관 동의', description: '서비스 이용 약관 동의', icon: FileText },
- { id: 2, title: '계정 생성', description: '개인 계정 정보 입력', icon: User },
- { id: 3, title: '업체 등록', description: '업체 정보 및 서류 제출', icon: Building }
-];
+ return sortedCountryArray.map(country => ({
+ ...country,
+ label: locale === "ko" && country.code === "KR"
+ ? "대한민국 (South Korea)"
+ : country.label
+ }));
+};
+
+const MAX_FILE_SIZE = 3e9;
// ========== 전화번호 처리 유틸리티 함수들 ==========
/**
* 국가별 전화번호 예시를 가져오는 함수
*/
-function getPhoneExample(countryCode) {
+function getPhoneExample(countryCode: string) {
if (!countryCode) return null;
try {
return getExampleNumber(countryCode, examples);
@@ -134,97 +182,93 @@ function getPhoneExample(countryCode) {
/**
* 국가별 전화번호 플레이스홀더를 생성하는 함수
*/
-function getPhonePlaceholder(countryCode) {
- if (!countryCode) return "국가를 먼저 선택해주세요";
+function getPhonePlaceholder(countryCode: string, t: (key: string) => string): string {
+ if (!countryCode) return t('selectCountryFirst');
const example = getPhoneExample(countryCode);
if (example) {
- // 국내 형식으로 표시 (더 친숙함)
return example.formatNational();
}
- // 예시가 없는 경우 국가 코드 기반 플레이스홀더
try {
const callingCode = getCountryCallingCode(countryCode);
return `+${callingCode} ...`;
} catch {
- return "전화번호를 입력하세요";
+ return t('enterPhoneNumber');
}
}
/**
* 국가별 전화번호 설명을 생성하는 함수
*/
-function getPhoneDescription(countryCode) {
- if (!countryCode) return "국가를 먼저 선택하여 올바른 전화번호 형식을 확인하세요.";
+function getPhoneDescription(countryCode: string, t: (key: string, options?: any) => string): string {
+ if (!countryCode) return t('selectCountryForPhoneFormat');
const example = getPhoneExample(countryCode);
if (example) {
- return `예시: ${example.formatNational()} 또는 ${example.formatInternational()}`;
+ return t('phoneExample', {
+ national: example.formatNational(),
+ international: example.formatInternational()
+ });
}
try {
const callingCode = getCountryCallingCode(countryCode);
- return `국가 코드: +${callingCode}. 국내 형식 또는 국제 형식으로 입력 가능합니다.`;
+ return t('phoneCountryCode', { code: callingCode });
} catch {
- return "올바른 전화번호 형식으로 입력해주세요.";
+ return t('enterValidPhoneFormat');
}
}
/**
* 전화번호 검증 결과를 반환하는 함수
*/
-function validatePhoneNumber(phoneNumber, countryCode) {
+function validatePhoneNumber(phoneNumber: string, countryCode: string, t: (key: string) => string): PhoneValidation {
if (!phoneNumber || !countryCode) {
return { isValid: false, error: null, formatted: phoneNumber };
}
try {
- // 1. 기본 파싱 시도
const parsed = parsePhoneNumberFromString(phoneNumber, countryCode);
if (!parsed) {
return {
isValid: false,
- error: "올바른 전화번호 형식이 아닙니다.",
+ error: t('invalidPhoneFormat'),
formatted: phoneNumber
};
}
- // 2. 길이 검증
const lengthValidation = validatePhoneNumberLength(phoneNumber, countryCode);
if (lengthValidation !== undefined) {
- const lengthErrors = {
- 'TOO_SHORT': '전화번호가 너무 짧습니다.',
- 'TOO_LONG': '전화번호가 너무 깁니다.',
- 'INVALID_LENGTH': '전화번호 길이가 올바르지 않습니다.'
+ const lengthErrors: Record<string, string> = {
+ 'TOO_SHORT': t('phoneTooShort'),
+ 'TOO_LONG': t('phoneTooLong'),
+ 'INVALID_LENGTH': t('phoneInvalidLength')
};
return {
isValid: false,
- error: lengthErrors[lengthValidation] || '전화번호 길이가 올바르지 않습니다.',
+ error: lengthErrors[lengthValidation] || t('phoneInvalidLength'),
formatted: phoneNumber
};
}
- // 3. 가능성 검증
if (!isPossiblePhoneNumber(phoneNumber, countryCode)) {
return {
isValid: false,
- error: "이 국가에서 가능하지 않은 전화번호 형식입니다.",
+ error: t('phoneNotPossible'),
formatted: phoneNumber
};
}
- // 4. 유효성 검증
if (!isValidPhoneNumber(phoneNumber, countryCode)) {
return {
isValid: false,
- error: "유효하지 않은 전화번호입니다.",
+ error: t('phoneInvalid'),
formatted: phoneNumber
};
}
- // 모든 검증 통과
return {
isValid: true,
error: null,
@@ -235,7 +279,7 @@ function validatePhoneNumber(phoneNumber, countryCode) {
} catch (error) {
return {
isValid: false,
- error: "전화번호 형식을 확인해주세요.",
+ error: t('phoneFormatError'),
formatted: phoneNumber
};
}
@@ -244,8 +288,8 @@ function validatePhoneNumber(phoneNumber, countryCode) {
/**
* 실시간 전화번호 포맷팅을 위한 커스텀 훅
*/
-function usePhoneFormatter(countryCode) {
- const [formatter, setFormatter] = useState(null);
+function usePhoneFormatter(countryCode: string) {
+ const [formatter, setFormatter] = useState<AsYouType | null>(null);
useEffect(() => {
if (countryCode) {
@@ -255,10 +299,8 @@ function usePhoneFormatter(countryCode) {
}
}, [countryCode]);
- const formatPhone = useCallback((value) => {
- if (!formatter) return value;
-
- // AsYouType은 매번 새로운 인스턴스를 사용해야 함
+ const formatPhone = useCallback((value: string) => {
+ if (!formatter || !countryCode) return value;
const newFormatter = new AsYouType(countryCode);
return newFormatter.input(value);
}, [countryCode, formatter]);
@@ -268,33 +310,42 @@ function usePhoneFormatter(countryCode) {
// ========== 전화번호 입력 컴포넌트 ==========
+interface PhoneInputProps {
+ value: string;
+ onChange: (value: string) => void;
+ countryCode: string;
+ placeholder?: string;
+ disabled?: boolean;
+ onBlur?: () => void;
+ className?: string;
+ showValidation?: boolean;
+ t: (key: string, options?: any) => string;
+}
+
function PhoneInput({
value,
onChange,
countryCode,
placeholder,
disabled = false,
- onBlur = null,
+ onBlur = undefined,
className = "",
- showValidation = true
-}) {
+ showValidation = true,
+ t
+}: PhoneInputProps) {
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 validation = validatePhoneNumber(localValue, countryCode, t);
- const handleInputChange = (e) => {
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const inputValue = e.target.value;
-
- // 실시간 포맷팅 적용
const formattedValue = countryCode ? formatPhone(inputValue) : inputValue;
-
setLocalValue(formattedValue);
onChange(formattedValue);
};
@@ -314,7 +365,7 @@ function PhoneInput({
value={localValue}
onChange={handleInputChange}
onBlur={handleBlur}
- placeholder={placeholder || getPhonePlaceholder(countryCode)}
+ placeholder={placeholder || getPhonePlaceholder(countryCode, t)}
disabled={disabled}
className={cn(
className,
@@ -325,22 +376,19 @@ function PhoneInput({
{showValidation && (
<div className="text-xs space-y-1">
- {/* 설명 텍스트 */}
<p className="text-muted-foreground">
- {getPhoneDescription(countryCode)}
+ {getPhoneDescription(countryCode, t)}
</p>
- {/* 오류 메시지 */}
{showError && (
<p className="text-red-500 font-medium">
{validation.error}
</p>
)}
- {/* 성공 메시지 */}
{showSuccess && (
<p className="text-green-600 font-medium">
- ✓ 올바른 전화번호입니다.
+ ✓ {t('validPhoneNumber')}
{validation.international && ` (${validation.international})`}
</p>
)}
@@ -355,30 +403,30 @@ function PhoneInput({
export default function JoinForm() {
const params = useParams() || {};
const lng = params.lng ? String(params.lng) : "ko";
- const { t } = useTranslation(lng, "translation");
+ const { t } = useTranslation(lng, "join");
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());
+ const [completedSteps, setCompletedSteps] = useState(new Set<number>());
// 각 스텝별 데이터
- const [consentData, setConsentData] = useState({
+ const [consentData, setConsentData] = useState<ConsentData>({
privacy: false,
terms: false,
marketing: false
});
- const [accountData, setAccountData] = useState({
+ const [accountData, setAccountData] = useState<AccountData>({
name: '',
email: '',
phone: '',
- country: 'KR' // 기본값을 한국으로 설정
+ country: 'KR'
});
- const [vendorData, setVendorData] = useState({
+ const [vendorData, setVendorData] = useState<VendorData>({
vendorName: "",
vendorTypeId: undefined,
items: "",
@@ -386,7 +434,7 @@ export default function JoinForm() {
address: "",
email: "",
phone: "",
- country: "", // 기본값을 빈 문자열로 설정하여 계정 데이터에서 가져오도록 함
+ country: "",
website: "",
representativeName: "",
representativeBirth: "",
@@ -407,19 +455,41 @@ export default function JoinForm() {
});
// 업체 타입 및 파일 상태
- const [vendorTypes, setVendorTypes] = useState([]);
+ const [vendorTypes, setVendorTypes] = useState<VendorType[]>([]);
const [isLoadingVendorTypes, setIsLoadingVendorTypes] = useState(true);
- const [businessRegistrationFiles, setBusinessRegistrationFiles] = useState([]);
- const [isoCertificationFiles, setIsoCertificationFiles] = useState([]);
- const [creditReportFiles, setCreditReportFiles] = useState([]);
- const [bankAccountFiles, setBankAccountFiles] = useState([]);
+ const [businessRegistrationFiles, setBusinessRegistrationFiles] = useState<File[]>([]);
+ const [isoCertificationFiles, setIsoCertificationFiles] = useState<File[]>([]);
+ const [creditReportFiles, setCreditReportFiles] = useState<File[]>([]);
+ const [bankAccountFiles, setBankAccountFiles] = useState<File[]>([]);
- const [policyVersions, setPolicyVersions] = useState({
+ const [policyVersions] = useState({
privacy_policy: '1.0',
terms_of_service: '1.0'
});
+ // 스텝 정의
+ const STEPS = [
+ { id: 1, title: t('consentStep'), description: t('consentStepDesc'), icon: FileText },
+ { id: 2, title: t('accountStep'), description: t('accountStepDesc'), icon: User },
+ { id: 3, title: t('vendorStep'), description: t('vendorStepDesc'), icon: Building }
+ ];
+
const progress = ((currentStep - 1) / (STEPS.length - 1)) * 100;
+ const enhancedCountryArray = getCountryData(lng);
+
+ // Contact task options
+ const contactTaskOptions: ContactTaskOption[] = [
+ { value: "PRESIDENT_DIRECTOR", label: t('taskPresidentDirector') },
+ { value: "SALES_MANAGEMENT", label: t('taskSalesManagement') },
+ { value: "ENGINEERING_DESIGN", label: t('taskEngineeringDesign') },
+ { value: "PROCUREMENT", label: t('taskProcurement') },
+ { value: "DELIVERY_CONTROL", label: t('taskDeliveryControl') },
+ { value: "PM_MANUFACTURING", label: t('taskPmManufacturing') },
+ { value: "QUALITY_MANAGEMENT", label: t('taskQualityManagement') },
+ { value: "SHIPPING_DOC_MANAGEMENT", label: t('taskShippingDocManagement') },
+ { value: "AS_MANAGEMENT", label: t('taskAsManagement') },
+ { value: "FIELD_SERVICE_ENGINEER", label: t('taskFieldServiceEngineer') }
+ ];
// 정책 버전 및 업체 타입 로드
useEffect(() => {
@@ -437,22 +507,22 @@ export default function JoinForm() {
console.error("Failed to load vendor types:", error);
toast({
variant: "destructive",
- title: "Error",
- description: "Failed to load vendor types",
+ title: t('error'),
+ description: t('failedToLoadVendorTypes'),
});
} finally {
setIsLoadingVendorTypes(false);
}
};
- const handleStepComplete = (step) => {
+ const handleStepComplete = (step: number) => {
setCompletedSteps(prev => new Set([...prev, step]));
if (step < STEPS.length) {
setCurrentStep(step + 1);
}
};
- const handleStepClick = (stepId) => {
+ const handleStepClick = (stepId: number) => {
if (stepId <= Math.max(...completedSteps) + 1) {
setCurrentStep(stepId);
}
@@ -463,7 +533,7 @@ export default function JoinForm() {
{/* 진행률 표시 */}
<div className="mb-8">
<div className="flex items-center justify-between mb-4">
- <h1 className="text-2xl font-bold">파트너 등록</h1>
+ <h1 className="text-2xl font-bold">{t('partnerRegistration')}</h1>
<span className="text-sm text-muted-foreground">
{currentStep} / {STEPS.length}
</span>
@@ -530,6 +600,7 @@ export default function JoinForm() {
data={consentData}
onChange={setConsentData}
onNext={() => handleStepComplete(1)}
+ lng={lng}
/>
)}
@@ -540,6 +611,7 @@ export default function JoinForm() {
onNext={() => handleStepComplete(2)}
onBack={() => setCurrentStep(1)}
enhancedCountryArray={enhancedCountryArray}
+ t={t}
/>
)}
@@ -547,7 +619,6 @@ export default function JoinForm() {
<VendorStep
data={{
...vendorData,
- // 업체 국가가 설정되지 않았다면 계정의 국가를 사용
country: vendorData.country || accountData.country,
}}
onChange={setVendorData}
@@ -572,6 +643,7 @@ export default function JoinForm() {
contactTaskOptions={contactTaskOptions}
lng={lng}
policyVersions={policyVersions}
+ t={t}
/>
)}
</div>
@@ -580,22 +652,28 @@ export default function JoinForm() {
}
// Step 2: 계정 생성
+interface AccountStepProps {
+ data: AccountData;
+ onChange: (updater: (prev: AccountData) => AccountData) => void;
+ onNext: () => void;
+ onBack: () => void;
+ enhancedCountryArray: CountryOption[];
+ t: (key: string, options?: any) => string;
+}
+
function AccountStep({
data, onChange, onNext, onBack,
- enhancedCountryArray
-}) {
+ enhancedCountryArray, t
+}: AccountStepProps) {
const [isLoading, setIsLoading] = useState(false);
const [emailCheckError, setEmailCheckError] = useState('');
- // 입력 핸들러
- const handleInputChange = (field, value) => {
+ const handleInputChange = (field: keyof AccountData, value: string) => {
onChange(prev => ({ ...prev, [field]: value }));
};
- // 전화번호 validation
- const phoneValidation = validatePhoneNumber(data.phone, data.country);
+ const phoneValidation = validatePhoneNumber(data.phone, data.country, t);
- // 전체 입력 유효성
const isValid =
data.name &&
data.email &&
@@ -603,7 +681,6 @@ function AccountStep({
data.phone &&
phoneValidation.isValid;
- // 이메일 중복체크 + 다음단계
const handleNext = async () => {
if (!isValid) return;
@@ -612,12 +689,12 @@ function AccountStep({
try {
const isUsed = await checkEmailExists(data.email);
if (isUsed) {
- setEmailCheckError('이미 사용 중인 이메일입니다.');
+ setEmailCheckError(t('emailAlreadyInUse'));
return;
}
onNext();
} catch (error) {
- setEmailCheckError('이메일 확인 중 오류가 발생했습니다.');
+ setEmailCheckError(t('emailCheckError'));
} finally {
setIsLoading(false);
}
@@ -626,20 +703,20 @@ function AccountStep({
return (
<div className="space-y-6">
<div>
- <h2 className="text-xl font-semibold mb-2">계정 정보 입력</h2>
+ <h2 className="text-xl font-semibold mb-2">{t('accountInfoInput')}</h2>
<p className="text-gray-600 text-sm">
- 서비스 이용을 위한 개인 계정을 생성합니다.
+ {t('accountInfoDescription')}
</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>
+ {t('name')} <span className="text-red-500">*</span>
</label>
<Input
type="text"
- placeholder="이름을 입력하세요"
+ placeholder={t('enterName')}
value={data.name}
onChange={(e) => handleInputChange('name', e.target.value)}
/>
@@ -647,11 +724,11 @@ function AccountStep({
<div>
<label className="block text-sm font-medium mb-1">
- 이메일 <span className="text-red-500">*</span>
+ {t('email')} <span className="text-red-500">*</span>
</label>
<Input
type="email"
- placeholder="이메일을 입력하세요"
+ placeholder={t('enterEmail')}
value={data.email}
onChange={(e) => handleInputChange('email', e.target.value)}
/>
@@ -662,7 +739,7 @@ function AccountStep({
<div>
<label className="block text-sm font-medium mb-1">
- 국가 <span className="text-red-500">*</span>
+ {t('country')} <span className="text-red-500">*</span>
</label>
<Popover>
<PopoverTrigger asChild>
@@ -674,15 +751,15 @@ function AccountStep({
!data.country && "text-muted-foreground"
)}
>
- {enhancedCountryArray.find(c => c.code === data.country)?.label || "국가 선택"}
+ {enhancedCountryArray.find(c => c.code === data.country)?.label || t('selectCountry')}
<ChevronsUpDown className="ml-2 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<Command>
- <CommandInput placeholder="국가 검색..." />
+ <CommandInput placeholder={t('searchCountry')} />
<CommandList>
- <CommandEmpty>No country found.</CommandEmpty>
+ <CommandEmpty>{t('noCountryFound')}</CommandEmpty>
<CommandGroup>
{enhancedCountryArray.map((country) => (
<CommandItem
@@ -710,23 +787,24 @@ function AccountStep({
<div>
<label className="block text-sm font-medium mb-1">
- 전화번호 <span className="text-red-500">*</span>
+ {t('phoneNumber')} <span className="text-red-500">*</span>
</label>
<PhoneInput
value={data.phone}
onChange={(value) => handleInputChange('phone', value)}
countryCode={data.country}
showValidation={true}
+ t={t}
/>
</div>
</div>
<div className="flex justify-between">
<Button variant="outline" onClick={onBack}>
- 이전
+ {t('previous')}
</Button>
<Button onClick={handleNext} disabled={!isValid || isLoading}>
- {isLoading ? '확인 중...' : '다음 단계'}
+ {isLoading ? t('checking') : t('nextStep')}
</Button>
</div>
</div>
@@ -734,19 +812,45 @@ function AccountStep({
}
// Step 3: 업체 등록
-function VendorStep(props) {
+interface VendorStepProps {
+ data: VendorData;
+ onChange: (updater: (prev: VendorData) => VendorData) => void;
+ onBack: () => void;
+ onComplete: () => void;
+ accountData: AccountData;
+ consentData: ConsentData;
+ vendorTypes: VendorType[];
+ isLoadingVendorTypes: boolean;
+ businessRegistrationFiles: File[];
+ setBusinessRegistrationFiles: (files: File[]) => void;
+ isoCertificationFiles: File[];
+ setIsoCertificationFiles: (files: File[]) => void;
+ creditReportFiles: File[];
+ setCreditReportFiles: (files: File[]) => void;
+ bankAccountFiles: File[];
+ setBankAccountFiles: (files: File[]) => void;
+ enhancedCountryArray: CountryOption[];
+ contactTaskOptions: ContactTaskOption[];
+ lng: string;
+ policyVersions: {
+ privacy_policy: string;
+ terms_of_service: string;
+ };
+ t: (key: string, options?: any) => string;
+}
+
+function VendorStep(props: VendorStepProps) {
return <CompleteVendorForm {...props} />;
}
-// 나머지 CompeleteVendorForm과 FileUploadSection은 기존과 동일하되,
-// PhoneInput 컴포넌트를 사용하도록 수정
function CompleteVendorForm({
data, onChange, onBack, onComplete, accountData, consentData,
vendorTypes, isLoadingVendorTypes, businessRegistrationFiles, setBusinessRegistrationFiles,
isoCertificationFiles, setIsoCertificationFiles, creditReportFiles, setCreditReportFiles,
- bankAccountFiles, setBankAccountFiles, enhancedCountryArray, contactTaskOptions, lng, policyVersions
-}) {
+ bankAccountFiles, setBankAccountFiles, enhancedCountryArray, contactTaskOptions, lng, policyVersions, t
+}: VendorStepProps) {
const [isSubmitting, setIsSubmitting] = useState(false);
+ const { toast } = useToast();
// 담당자 관리 함수들
const addContact = () => {
@@ -763,14 +867,14 @@ function CompleteVendorForm({
}));
};
- const removeContact = (index) => {
+ const removeContact = (index: number) => {
onChange(prev => ({
...prev,
contacts: prev.contacts.filter((_, i) => i !== index)
}));
};
- const updateContact = (index, field, value) => {
+ const updateContact = (index: number, field: keyof Contact, value: string) => {
onChange(prev => ({
...prev,
contacts: prev.contacts.map((contact, i) =>
@@ -779,27 +883,26 @@ function CompleteVendorForm({
}));
};
- // 폼 입력 변경 핸들러
- const handleInputChange = (field, value) => {
+ const handleInputChange = (field: keyof VendorData, value: any) => {
onChange(prev => ({ ...prev, [field]: value }));
};
// 파일 업로드 핸들러들
- const createFileUploadHandler = (setFiles, currentFiles) => ({
- onDropAccepted: (acceptedFiles) => {
+ const createFileUploadHandler = (setFiles: (files: File[]) => void, currentFiles: File[]) => ({
+ onDropAccepted: (acceptedFiles: File[]) => {
const newFiles = [...currentFiles, ...acceptedFiles];
setFiles(newFiles);
},
- onDropRejected: (fileRejections) => {
+ onDropRejected: (fileRejections: any[]) => {
fileRejections.forEach((rej) => {
toast({
variant: "destructive",
- title: "File Error",
- description: `${rej.file.name}: ${rej.errors[0]?.message || "Upload failed"}`,
+ title: t('fileError'),
+ description: `${rej.file.name}: ${rej.errors[0]?.message || t('uploadFailed')}`,
});
});
},
- removeFile: (index) => {
+ removeFile: (index: number) => {
const updated = [...currentFiles];
updated.splice(index, 1);
setFiles(updated);
@@ -812,34 +915,34 @@ function CompleteVendorForm({
const bankAccountHandler = createFileUploadHandler(setBankAccountFiles, bankAccountFiles);
// 유효성 검사
- const validateRequiredFiles = () => {
- const errors = [];
+ const validateRequiredFiles = (): string[] => {
+ const errors: string[] = [];
if (businessRegistrationFiles.length === 0) {
- errors.push("사업자등록증을 업로드해주세요.");
+ errors.push(t('uploadBusinessRegistration'));
}
if (isoCertificationFiles.length === 0) {
- errors.push("ISO 인증서를 업로드해주세요.");
+ errors.push(t('uploadIsoCertification'));
}
if (creditReportFiles.length === 0) {
- errors.push("신용평가보고서를 업로드해주세요.");
+ errors.push(t('uploadCreditReport'));
}
if (data.country !== "KR" && bankAccountFiles.length === 0) {
- errors.push("대금지급 통장사본을 업로드해주세요.");
+ errors.push(t('uploadBankAccount'));
}
return errors;
};
// 전화번호 검증
- const vendorPhoneValidation = validatePhoneNumber(data.phone, data.country);
+ const vendorPhoneValidation = validatePhoneNumber(data.phone, data.country, t);
const contactsValid = data.contacts.length > 0 &&
data.contacts[0].contactName &&
data.contacts.every(contact =>
- contact.contactPhone ? validatePhoneNumber(contact.contactPhone, data.country).isValid : true
+ contact.contactPhone ? validatePhoneNumber(contact.contactPhone, data.country, t).isValid : true
);
const isFormValid = data.vendorName && data.vendorTypeId && data.items &&
@@ -847,13 +950,13 @@ function CompleteVendorForm({
contactsValid &&
validateRequiredFiles().length === 0;
- // 최종 제출 (기존과 동일)
+ // 최종 제출
const handleSubmit = async () => {
const fileErrors = validateRequiredFiles();
if (fileErrors.length > 0) {
toast({
variant: "destructive",
- title: "파일 업로드 필수",
+ title: t('fileUploadRequired'),
description: fileErrors.join("\n"),
});
return;
@@ -914,23 +1017,23 @@ function CompleteVendorForm({
if (response.ok) {
toast({
- title: "등록 완료",
- description: "회원가입 및 업체 등록이 완료되었습니다. 관리자 승인 후 서비스를 이용하실 수 있습니다.",
+ title: t('registrationComplete'),
+ description: t('registrationCompleteDesc'),
});
onComplete();
} else {
toast({
variant: "destructive",
- title: "오류",
- description: result.error || "등록에 실패했습니다.",
+ title: t('error'),
+ description: result.error || t('registrationFailed'),
});
}
} catch (error) {
console.error(error);
toast({
variant: "destructive",
- title: "서버 에러",
- description: error.message || "에러가 발생했습니다.",
+ title: t('serverError'),
+ description: t('errorOccurred'),
});
} finally {
setIsSubmitting(false);
@@ -940,20 +1043,20 @@ function CompleteVendorForm({
return (
<div className="space-y-8">
<div>
- <h2 className="text-xl font-semibold mb-2">업체 정보 등록</h2>
+ <h2 className="text-xl font-semibold mb-2">{t('vendorInfoRegistration')}</h2>
<p className="text-gray-600 text-sm">
- 업체 정보와 필요한 서류를 등록해주세요. 모든 정보는 관리자 검토 후 승인됩니다.
+ {t('vendorInfoDescription')}
</p>
</div>
{/* 기본 정보 */}
<div className="rounded-md border p-6 space-y-4">
- <h4 className="text-md font-semibold">기본 정보</h4>
+ <h4 className="text-md font-semibold">{t('basicInformation')}</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>
+ {t('vendorType')} <span className="text-red-500">*</span>
</label>
<Popover>
<PopoverTrigger asChild>
@@ -967,16 +1070,16 @@ function CompleteVendorForm({
disabled={isSubmitting || isLoadingVendorTypes}
>
{isLoadingVendorTypes
- ? "Loading..."
- : vendorTypes.find(type => type.id === data.vendorTypeId)?.[lng === "ko" ? "nameKo" : "nameEn"] || "업체유형 선택"}
+ ? t('loading')
+ : vendorTypes.find(type => type.id === data.vendorTypeId)?.[lng === "ko" ? "nameKo" : "nameEn"] || t('selectVendorType')}
<ChevronsUpDown className="ml-2 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<Command>
- <CommandInput placeholder="업체유형 검색..." />
+ <CommandInput placeholder={t('searchVendorType')} />
<CommandList>
- <CommandEmpty>No vendor type found.</CommandEmpty>
+ <CommandEmpty>{t('noVendorTypeFound')}</CommandEmpty>
<CommandGroup>
{vendorTypes.map((type) => (
<CommandItem
@@ -1005,7 +1108,7 @@ function CompleteVendorForm({
{/* 업체명 */}
<div>
<label className="block text-sm font-medium mb-1">
- 업체명 <span className="text-red-500">*</span>
+ {t('vendorName')} <span className="text-red-500">*</span>
</label>
<Input
value={data.vendorName}
@@ -1014,15 +1117,15 @@ function CompleteVendorForm({
/>
<p className="text-xs text-gray-500 mt-1">
{(data.country || accountData.country) === "KR"
- ? "사업자 등록증에 표기된 정확한 회사명을 입력하세요."
- : "해외 업체의 경우 영문 회사명을 입력하세요."}
+ ? t('vendorNameHintKorea')
+ : t('vendorNameHintOverseas')}
</p>
</div>
{/* 공급품목 */}
<div>
<label className="block text-sm font-medium mb-1">
- 공급품목 <span className="text-red-500">*</span>
+ {t('supplyItems')} <span className="text-red-500">*</span>
</label>
<Input
value={data.items}
@@ -1030,14 +1133,14 @@ function CompleteVendorForm({
disabled={isSubmitting}
/>
<p className="text-xs text-gray-500 mt-1">
- 공급 가능한 제품/서비스를 입력하세요
+ {t('supplyItemsHint')}
</p>
</div>
{/* 사업자등록번호 */}
<div>
<label className="block text-sm font-medium mb-1">
- 사업자등록번호 <span className="text-red-500">*</span>
+ {t('businessRegistrationNumber')} <span className="text-red-500">*</span>
</label>
<Input
value={data.taxId}
@@ -1049,7 +1152,7 @@ function CompleteVendorForm({
{/* 주소 */}
<div>
- <label className="block text-sm font-medium mb-1">주소</label>
+ <label className="block text-sm font-medium mb-1">{t('address')}</label>
<Input
value={data.address}
onChange={(e) => handleInputChange('address', e.target.value)}
@@ -1060,7 +1163,7 @@ function CompleteVendorForm({
{/* 국가 */}
<div>
<label className="block text-sm font-medium mb-1">
- 국가 <span className="text-red-500">*</span>
+ {t('country')} <span className="text-red-500">*</span>
</label>
<Popover>
<PopoverTrigger asChild>
@@ -1073,15 +1176,15 @@ function CompleteVendorForm({
)}
disabled={isSubmitting}
>
- {enhancedCountryArray.find(c => c.code === data.country)?.label || "국가 선택"}
+ {enhancedCountryArray.find(c => c.code === data.country)?.label || t('selectCountry')}
<ChevronsUpDown className="ml-2 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<Command>
- <CommandInput placeholder="국가 검색..." />
+ <CommandInput placeholder={t('searchCountry')} />
<CommandList>
- <CommandEmpty>No country found.</CommandEmpty>
+ <CommandEmpty>{t('noCountryFound')}</CommandEmpty>
<CommandGroup>
{enhancedCountryArray.map((country) => (
<CommandItem
@@ -1110,7 +1213,7 @@ function CompleteVendorForm({
{/* 대표 전화 - PhoneInput 사용 */}
<div>
<label className="block text-sm font-medium mb-1">
- 대표 전화 <span className="text-red-500">*</span>
+ {t('representativePhone')} <span className="text-red-500">*</span>
</label>
<PhoneInput
value={data.phone}
@@ -1118,13 +1221,14 @@ function CompleteVendorForm({
countryCode={data.country}
disabled={isSubmitting}
showValidation={true}
+ t={t}
/>
</div>
{/* 대표 이메일 */}
<div>
<label className="block text-sm font-medium mb-1">
- 대표 이메일 <span className="text-red-500">*</span>
+ {t('representativeEmail')} <span className="text-red-500">*</span>
</label>
<Input
value={data.email}
@@ -1133,13 +1237,13 @@ function CompleteVendorForm({
placeholder={accountData.email}
/>
<p className="text-xs text-gray-500 mt-1">
- 비워두면 계정 이메일({accountData.email})을 사용합니다.
+ {t('emailHint', { email: accountData.email })}
</p>
</div>
{/* 웹사이트 */}
<div>
- <label className="block text-sm font-medium mb-1">웹사이트</label>
+ <label className="block text-sm font-medium mb-1">{t('website')}</label>
<Input
value={data.website}
onChange={(e) => handleInputChange('website', e.target.value)}
@@ -1149,10 +1253,10 @@ 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>
+ <h4 className="text-md font-semibold">{t('contactInfo')}</h4>
<Button
type="button"
variant="outline"
@@ -1160,7 +1264,7 @@ function CompleteVendorForm({
disabled={isSubmitting}
>
<Plus className="mr-1 h-4 w-4" />
- 담당자 추가
+ {t('addContact')}
</Button>
</div>
@@ -1173,7 +1277,7 @@ function CompleteVendorForm({
<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>
+ {t('contactName')} <span className="text-red-500">*</span>
</label>
<Input
value={contact.contactName}
@@ -1184,7 +1288,7 @@ function CompleteVendorForm({
<div>
<label className="block text-sm font-medium mb-1">
- 직급 <span className="text-red-500">*</span>
+ {t('position')} <span className="text-red-500">*</span>
</label>
<Input
value={contact.contactPosition}
@@ -1195,7 +1299,7 @@ function CompleteVendorForm({
<div>
<label className="block text-sm font-medium mb-1">
- 부서 <span className="text-red-500">*</span>
+ {t('department')} <span className="text-red-500">*</span>
</label>
<Input
value={contact.contactDepartment}
@@ -1206,7 +1310,7 @@ function CompleteVendorForm({
<div>
<label className="block text-sm font-medium mb-1">
- 담당업무 <span className="text-red-500">*</span>
+ {t('responsibility')} <span className="text-red-500">*</span>
</label>
<Select
value={contact.contactTask}
@@ -1214,7 +1318,7 @@ function CompleteVendorForm({
disabled={isSubmitting}
>
<SelectTrigger>
- <SelectValue placeholder="담당업무를 선택하세요" />
+ <SelectValue placeholder={t('selectResponsibility')} />
</SelectTrigger>
<SelectContent>
{contactTaskOptions.map((option) => (
@@ -1228,7 +1332,7 @@ function CompleteVendorForm({
<div>
<label className="block text-sm font-medium mb-1">
- 이메일 <span className="text-red-500">*</span>
+ {t('email')} <span className="text-red-500">*</span>
</label>
<Input
value={contact.contactEmail}
@@ -1239,7 +1343,7 @@ function CompleteVendorForm({
<div>
<label className="block text-sm font-medium mb-1">
- 전화번호 <span className="text-red-500">*</span>
+ {t('phoneNumber')} <span className="text-red-500">*</span>
</label>
<PhoneInput
value={contact.contactPhone}
@@ -1247,6 +1351,7 @@ function CompleteVendorForm({
countryCode={data.country}
disabled={isSubmitting}
showValidation={true}
+ t={t}
/>
</div>
</div>
@@ -1259,7 +1364,7 @@ function CompleteVendorForm({
disabled={isSubmitting}
>
<X className="mr-1 h-4 w-4" />
- 삭제
+ {t('delete')}
</Button>
</div>
)}
@@ -1268,14 +1373,14 @@ 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>
+ <h4 className="text-md font-semibold">{t('koreanBusinessInfo')}</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>
+ {t('representativeName')} <span className="text-red-500">*</span>
</label>
<Input
value={data.representativeName}
@@ -1285,7 +1390,7 @@ function CompleteVendorForm({
</div>
<div>
<label className="block text-sm font-medium mb-1">
- 대표자 생년월일 <span className="text-red-500">*</span>
+ {t('representativeBirth')} <span className="text-red-500">*</span>
</label>
<Input
placeholder="YYYY-MM-DD"
@@ -1296,7 +1401,7 @@ function CompleteVendorForm({
</div>
<div>
<label className="block text-sm font-medium mb-1">
- 대표자 이메일 <span className="text-red-500">*</span>
+ {t('representativeEmail')} <span className="text-red-500">*</span>
</label>
<Input
value={data.representativeEmail}
@@ -1306,7 +1411,7 @@ function CompleteVendorForm({
</div>
<div>
<label className="block text-sm font-medium mb-1">
- 대표자 전화번호 <span className="text-red-500">*</span>
+ {t('representativePhone')} <span className="text-red-500">*</span>
</label>
<PhoneInput
value={data.representativePhone}
@@ -1314,11 +1419,12 @@ function CompleteVendorForm({
countryCode="KR"
disabled={isSubmitting}
showValidation={true}
+ t={t}
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">
- 법인등록번호 <span className="text-red-500">*</span>
+ {t('corporateRegistrationNumber')} <span className="text-red-500">*</span>
</label>
<Input
value={data.corporateRegistrationNumber}
@@ -1336,7 +1442,7 @@ function CompleteVendorForm({
className="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
<label htmlFor="work-experience" className="text-sm">
- 대표자 삼성중공업 근무이력
+ {t('samsungWorkExperience')}
</label>
</div>
</div>
@@ -1345,63 +1451,67 @@ function CompleteVendorForm({
{/* 필수 첨부 서류 */}
<div className="rounded-md border p-6 space-y-6">
- <h4 className="text-md font-semibold">필수 첨부 서류</h4>
+ <h4 className="text-md font-semibold">{t('requiredDocuments')}</h4>
<FileUploadSection
- title="사업자등록증"
- description="사업자등록증 스캔본 또는 사진을 업로드해주세요."
+ title={t('businessRegistrationCertificate')}
+ description={t('businessRegistrationDesc')}
files={businessRegistrationFiles}
onDropAccepted={businessRegistrationHandler.onDropAccepted}
onDropRejected={businessRegistrationHandler.onDropRejected}
removeFile={businessRegistrationHandler.removeFile}
isSubmitting={isSubmitting}
+ t={t}
/>
<FileUploadSection
- title="ISO 인증서"
- description="ISO 9001, ISO 14001 등 품질/환경 관리 인증서를 업로드해주세요."
+ title={t('isoCertificate')}
+ description={t('isoCertificateDesc')}
files={isoCertificationFiles}
onDropAccepted={isoCertificationHandler.onDropAccepted}
onDropRejected={isoCertificationHandler.onDropRejected}
removeFile={isoCertificationHandler.removeFile}
isSubmitting={isSubmitting}
+ t={t}
/>
<FileUploadSection
- title="신용평가보고서"
- description="신용평가기관에서 발급한 발행 1년 이내의 신용평가보고서를 업로드해주세요."
+ title={t('creditReport')}
+ description={t('creditReportDesc')}
files={creditReportFiles}
onDropAccepted={creditReportHandler.onDropAccepted}
onDropRejected={creditReportHandler.onDropRejected}
removeFile={creditReportHandler.removeFile}
isSubmitting={isSubmitting}
+ t={t}
/>
{data.country !== "KR" && (
<FileUploadSection
- title="대금지급 통장사본"
- description="대금 지급용 은행 계좌의 통장 사본 또는 계좌증명서를 업로드해주세요."
+ title={t('bankAccountCopy')}
+ description={t('bankAccountDesc')}
files={bankAccountFiles}
onDropAccepted={bankAccountHandler.onDropAccepted}
onDropRejected={bankAccountHandler.onDropRejected}
removeFile={bankAccountHandler.removeFile}
isSubmitting={isSubmitting}
+ t={t}
/>
)}
</div>
<div className="flex justify-between">
<Button variant="outline" onClick={onBack}>
- 이전
+ {t('previous')}
</Button>
<Button onClick={handleSubmit} disabled={!isFormValid || isSubmitting}>
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
- 등록 중...
+ {t('registering')}
</>
) : (
- "등록 완료"
+ t('completeRegistration')
)}
</Button>
</div>
@@ -1409,7 +1519,19 @@ function CompleteVendorForm({
);
}
-// 파일 업로드 섹션 컴포넌트 (기존과 동일)
+// 파일 업로드 섹션 컴포넌트
+interface FileUploadSectionProps {
+ title: string;
+ description: string;
+ files: File[];
+ onDropAccepted: (files: File[]) => void;
+ onDropRejected: (fileRejections: any[]) => void;
+ removeFile: (index: number) => void;
+ isSubmitting: boolean;
+ required?: boolean;
+ t: (key: string, options?: any) => string;
+}
+
function FileUploadSection({
title,
description,
@@ -1418,8 +1540,9 @@ function FileUploadSection({
onDropRejected,
removeFile,
isSubmitting,
- required = true
-}) {
+ required = true,
+ t
+}: FileUploadSectionProps) {
return (
<div className="space-y-4">
<div>
@@ -1443,10 +1566,10 @@ function FileUploadSection({
<div className="flex items-center gap-4">
<DropzoneUploadIcon />
<div className="grid gap-1">
- <DropzoneTitle>파일 업로드</DropzoneTitle>
+ <DropzoneTitle>{t('fileUpload')}</DropzoneTitle>
<DropzoneDescription>
- 드래그 또는 클릭
- {maxSize ? ` (최대: ${prettyBytes(maxSize)})` : null}
+ {t('dragOrClick')}
+ {maxSize ? ` (${t('maxSize')}: ${prettyBytes(maxSize)})` : null}
</DropzoneDescription>
</div>
</div>