diff options
Diffstat (limited to 'components/signup')
| -rw-r--r-- | components/signup/conset-step.tsx | 79 | ||||
| -rw-r--r-- | components/signup/join-form.tsx | 561 |
2 files changed, 391 insertions, 249 deletions
diff --git a/components/signup/conset-step.tsx b/components/signup/conset-step.tsx index 3260a7b7..4d0a544f 100644 --- a/components/signup/conset-step.tsx +++ b/components/signup/conset-step.tsx @@ -5,6 +5,7 @@ import { Separator } from '@/components/ui/separator'; import { ChevronDown, ChevronUp, FileText, Shield, Loader2 } from 'lucide-react'; import { cn } from '@/lib/utils'; import { getCurrentPolicies } from '@/lib/polices/service'; +import { useTranslation } from '@/i18n/client'; // ✅ 정책 데이터 타입 개선 interface PolicyData { @@ -30,9 +31,12 @@ interface ConsentStepProps { }; onChange: (updater: (prev: any) => any) => void; onNext: () => void; + lng: string; // 언어 코드 추가 } -export default function ConsentStep({ data, onChange, onNext }: ConsentStepProps) { +export default function ConsentStep({ data, onChange, onNext, lng }: ConsentStepProps) { + const { t } = useTranslation(lng, 'consent'); + const [policyData, setPolicyData] = useState<PolicyVersions | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState<string | null>(null); @@ -58,11 +62,11 @@ export default function ConsentStep({ data, onChange, onNext }: ConsentStepProps console.log('Policy data loaded:', result.data); setPolicyData(result.data); } else { - setError(result.error || '정책 데이터를 불러올 수 없습니다.'); + setError(result.error || t('policyLoadError')); console.error('Failed to fetch policy data:', result.error); } } catch (error) { - const errorMessage = '정책 데이터를 불러오는 중 오류가 발생했습니다.'; + const errorMessage = t('policyFetchError'); setError(errorMessage); console.error('Failed to fetch policy data:', error); } finally { @@ -100,7 +104,7 @@ export default function ConsentStep({ data, onChange, onNext }: ConsentStepProps // ✅ 정책 미리보기 텍스트 생성 const renderPolicyPreview = (policy: PolicyData, maxLength = 200): string => { - if (!policy?.content) return '내용 없음'; + if (!policy?.content) return t('noContent'); const textContent = stripHtmlTags(policy.content); return textContent.length > maxLength @@ -114,7 +118,7 @@ export default function ConsentStep({ data, onChange, onNext }: ConsentStepProps <div className="flex items-center justify-center p-8"> <div className="flex items-center gap-2 text-gray-600"> <Loader2 className="h-4 w-4 animate-spin" /> - 정책 내용을 불러오는 중... + {t('loadingPolicies')} </div> </div> ); @@ -125,10 +129,10 @@ export default function ConsentStep({ data, onChange, onNext }: ConsentStepProps return ( <div className="text-center p-8"> <div className="text-red-600 mb-4"> - {error || '정책 내용을 불러올 수 없습니다.'} + {error || t('policyLoadError')} </div> <Button onClick={fetchPolicyData} variant="outline"> - 다시 시도 + {t('retryButton')} </Button> </div> ); @@ -139,11 +143,11 @@ export default function ConsentStep({ data, onChange, onNext }: ConsentStepProps return ( <div className="text-center p-8"> <div className="text-amber-600 mb-4"> - 일부 정책이 설정되지 않았습니다. 관리자에게 문의해주세요. + {t('policiesNotConfigured')} </div> <div className="text-sm text-gray-500"> - {!policyData.privacy_policy && '개인정보 처리방침이 없습니다.'}<br /> - {!policyData.terms_of_service && '이용약관이 없습니다.'} + {!policyData.privacy_policy && t('missingPrivacyPolicy')}<br /> + {!policyData.terms_of_service && t('missingTermsOfService')} </div> </div> ); @@ -152,9 +156,9 @@ export default function ConsentStep({ data, onChange, onNext }: ConsentStepProps 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('consentTitle')}</h2> <p className="text-gray-600 text-sm"> - 서비스 이용을 위해 다음 약관에 동의해주세요. 각 항목을 클릭하여 상세 내용을 확인할 수 있습니다. + {t('consentDescription')} </p> </div> @@ -169,11 +173,13 @@ export default function ConsentStep({ data, onChange, onNext }: ConsentStepProps policy={policyData.privacy_policy} isRequired={true} icon={<Shield className="w-4 h-4" />} - title="개인정보 처리방침" - description="개인정보 수집, 이용, 보관 및 파기에 관한 정책입니다." + title={t('privacyPolicyTitle')} + description={t('privacyPolicyDescription')} expanded={expandedSections.privacy} onToggleExpand={() => toggleSection('privacy')} onShowModal={() => setShowPrivacyModal(true)} + t={t} + lng={lng} /> )} @@ -189,15 +195,16 @@ export default function ConsentStep({ data, onChange, onNext }: ConsentStepProps policy={policyData.terms_of_service} isRequired={true} icon={<FileText className="w-4 h-4" />} - title="이용약관" - description="서비스 이용 시 준수해야 할 규칙과 조건입니다." + title={t('termsOfServiceTitle')} + description={t('termsOfServiceDescription')} expanded={expandedSections.terms} onToggleExpand={() => toggleSection('terms')} onShowModal={() => setShowTermsModal(true)} + t={t} + lng={lng} /> )} - {/* ✅ 전체 동의 */} <div className="pt-4 border-t bg-gray-50 p-4 rounded-lg"> <div className="flex items-center space-x-3"> @@ -216,7 +223,7 @@ export default function ConsentStep({ data, onChange, onNext }: ConsentStepProps className="h-5 w-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500" /> <label htmlFor="all-consent" className="text-base font-medium cursor-pointer"> - 위 내용에 모두 동의합니다 + {t('agreeToAll')} </label> </div> </div> @@ -224,7 +231,7 @@ export default function ConsentStep({ data, onChange, onNext }: ConsentStepProps <div className="flex justify-end"> <Button onClick={onNext} disabled={!isValid} size="lg"> - 다음 단계로 + {t('nextStep')} </Button> </div> @@ -237,6 +244,8 @@ export default function ConsentStep({ data, onChange, onNext }: ConsentStepProps onChange(prev => ({ ...prev, privacy: true })); setShowPrivacyModal(false); }} + t={t} + lng={lng} /> )} @@ -249,6 +258,8 @@ export default function ConsentStep({ data, onChange, onNext }: ConsentStepProps onChange(prev => ({ ...prev, terms: true })); setShowTermsModal(false); }} + t={t} + lng={lng} /> )} </div> @@ -269,15 +280,17 @@ interface PolicyConsentSectionProps { expanded: boolean; onToggleExpand: () => void; onShowModal: () => void; + t: (key: string, options?: any) => string; + lng: string; } function PolicyConsentSection({ id, type, checked, onChange, policy, isRequired, icon, title, description, - expanded, onToggleExpand, onShowModal + expanded, onToggleExpand, onShowModal, t, lng }: PolicyConsentSectionProps) { // ✅ HTML에서 텍스트 추출 const renderPolicyPreview = (content: string, maxLength = 300): string => { - if (!content) return '내용 없음'; + if (!content) return t('noContent'); const textContent = content .replace(/<[^>]*>/g, '') // HTML 태그 제거 @@ -311,7 +324,7 @@ function PolicyConsentSection({ {icon} <label htmlFor={id} className="text-sm font-medium cursor-pointer"> <span className={isRequired ? "text-red-500" : "text-blue-500"}> - [{isRequired ? "필수" : "선택"}] + [{isRequired ? t('required') : t('optional')}] </span> {title} (v{policy.version}) </label> </div> @@ -331,7 +344,7 @@ function PolicyConsentSection({ className="flex items-center space-x-1 text-blue-600 hover:underline" > {expanded ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />} - <span>{expanded ? '간략히' : '더보기'}</span> + <span>{expanded ? t('showLess') : t('showMore')}</span> </button> <span className="text-gray-400">|</span> <button @@ -339,11 +352,13 @@ function PolicyConsentSection({ onClick={onShowModal} className="text-blue-600 hover:underline" > - 전문보기 + {t('viewFullText')} </button> <span className="text-gray-400">|</span> <span className="text-gray-500"> - 시행일: {new Date(policy.effectiveDate).toLocaleDateString('ko-KR')} + {t('effectiveDate')}: {new Date(policy.effectiveDate).toLocaleDateString( + lng === 'ko' ? 'ko-KR' : 'en-US' + )} </span> </div> </div> @@ -357,11 +372,13 @@ interface PolicyModalProps { policy: PolicyData; onClose: () => void; onAgree: () => void; + t: (key: string, options?: any) => string; + lng: string; } -function PolicyModal({ policy, onClose, onAgree }: PolicyModalProps) { +function PolicyModal({ policy, onClose, onAgree, t, lng }: PolicyModalProps) { const getPolicyTitle = (policyType: string): string => { - return policyType === 'privacy_policy' ? '개인정보 처리방침' : '이용약관'; + return policyType === 'privacy_policy' ? t('privacyPolicyTitle') : t('termsOfServiceTitle'); }; return ( @@ -374,7 +391,9 @@ function PolicyModal({ policy, onClose, onAgree }: PolicyModalProps) { {getPolicyTitle(policy.policyType)} </h3> <p className="text-sm text-gray-600 mt-1"> - 버전 {policy.version} | 시행일: {new Date(policy.effectiveDate).toLocaleDateString('ko-KR')} + {t('version')} {policy.version} | {t('effectiveDate')}: {new Date(policy.effectiveDate).toLocaleDateString( + lng === 'ko' ? 'ko-KR' : 'en-US' + )} </p> </div> <button @@ -400,13 +419,13 @@ function PolicyModal({ policy, onClose, onAgree }: PolicyModalProps) { onClick={onClose} className="flex-1" > - 닫기 + {t('close')} </Button> <Button onClick={onAgree} className="flex-1" > - 동의하고 닫기 + {t('agreeAndClose')} </Button> </div> </div> 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> |
