- 페이지 삭제 시 독립 섹션 목록 갱신 추가 (독립 엔티티 아키텍처) - ItemForm 컴포넌트 분리 완료 (1607→415줄, 74% 감소) - ItemMasterDataManagement 중복 코드 제거 (getInputTypeLabel 헬퍼) - 문서 업데이트 (realtime-sync-fixes.md) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
765 lines
30 KiB
TypeScript
765 lines
30 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { useTranslations } from "next-intl";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { LanguageSelect } from "@/components/LanguageSelect";
|
|
import { ThemeSelect } from "@/components/ThemeSelect";
|
|
import { companyInfoSchema, userInfoSchema, planSelectionSchema } from "@/lib/validations/auth";
|
|
import {
|
|
ArrowLeft,
|
|
Building2,
|
|
User,
|
|
Mail,
|
|
Phone,
|
|
Lock,
|
|
Briefcase,
|
|
Users,
|
|
FileText
|
|
} from "lucide-react";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
|
|
export function SignupPage() {
|
|
const router = useRouter();
|
|
const t = useTranslations("auth");
|
|
const tSignup = useTranslations("signup");
|
|
const tValidation = useTranslations("validation");
|
|
const [step, setStep] = useState(1);
|
|
const [formData, setFormData] = useState({
|
|
// 회사 정보
|
|
companyName: "",
|
|
businessNumber: "",
|
|
industry: "",
|
|
companySize: "",
|
|
|
|
// 담당자 정보
|
|
name: "",
|
|
position: "",
|
|
email: "",
|
|
phone: "",
|
|
userId: "",
|
|
password: "",
|
|
passwordConfirm: "",
|
|
|
|
// 플랜 및 추천인
|
|
plan: "demo",
|
|
salesCode: "",
|
|
|
|
// 약관
|
|
agreeTerms: false,
|
|
agreePrivacy: false,
|
|
});
|
|
|
|
const handleInputChange = (field: string, value: string | boolean) => {
|
|
setFormData(prev => ({ ...prev, [field]: value }));
|
|
};
|
|
|
|
// 사업자등록번호 자동 포맷팅 (000-00-00000)
|
|
const formatBusinessNumber = (value: string) => {
|
|
// 숫자만 추출
|
|
const numbers = value.replace(/[^\d]/g, '');
|
|
|
|
// 최대 10자리까지만
|
|
const limited = numbers.slice(0, 10);
|
|
|
|
// 형식에 맞게 하이픈 추가
|
|
if (limited.length <= 3) {
|
|
return limited;
|
|
} else if (limited.length <= 5) {
|
|
return `${limited.slice(0, 3)}-${limited.slice(3)}`;
|
|
} else {
|
|
return `${limited.slice(0, 3)}-${limited.slice(3, 5)}-${limited.slice(5)}`;
|
|
}
|
|
};
|
|
|
|
const handleBusinessNumberChange = (value: string) => {
|
|
const formatted = formatBusinessNumber(value);
|
|
handleInputChange("businessNumber", formatted);
|
|
};
|
|
|
|
// 핸드폰 번호 자동 포맷팅 (010-1111-1111 or 010-111-1111)
|
|
const formatPhoneNumber = (value: string) => {
|
|
// 숫자만 추출
|
|
const numbers = value.replace(/[^\d]/g, '');
|
|
|
|
// 최대 11자리까지만
|
|
const limited = numbers.slice(0, 11);
|
|
|
|
// 형식에 맞게 하이픈 추가
|
|
if (limited.length <= 3) {
|
|
return limited;
|
|
} else if (limited.length <= 6) {
|
|
return `${limited.slice(0, 3)}-${limited.slice(3)}`;
|
|
} else if (limited.length === 10) {
|
|
// 10자리: 010-111-1111
|
|
return `${limited.slice(0, 3)}-${limited.slice(3, 6)}-${limited.slice(6)}`;
|
|
} else {
|
|
// 11자리: 010-1111-1111
|
|
return `${limited.slice(0, 3)}-${limited.slice(3, 7)}-${limited.slice(7)}`;
|
|
}
|
|
};
|
|
|
|
const handlePhoneNumberChange = (value: string) => {
|
|
const formatted = formatPhoneNumber(value);
|
|
handleInputChange("phone", formatted);
|
|
};
|
|
|
|
// 전체 동의 처리
|
|
const handleAgreeAll = (checked: boolean) => {
|
|
setFormData(prev => ({
|
|
...prev,
|
|
agreeTerms: checked,
|
|
agreePrivacy: checked,
|
|
}));
|
|
};
|
|
|
|
// 전체 동의 체크박스 상태 계산
|
|
const isAllAgreed = formData.agreeTerms && formData.agreePrivacy;
|
|
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [error, setError] = useState("");
|
|
const [isChecking, setIsChecking] = useState(true);
|
|
|
|
// 이미 로그인된 상태인지 확인 (페이지 로드 시, 뒤로가기 시)
|
|
useEffect(() => {
|
|
const checkAuth = async () => {
|
|
try {
|
|
// 🔵 Next.js 내부 API - 쿠키에서 토큰 확인 (PHP 호출 X, 성능 최적화)
|
|
const response = await fetch('/api/auth/check');
|
|
|
|
if (response.ok) {
|
|
// 이미 로그인됨 → 대시보드로 리다이렉트 (replace로 히스토리에서 제거)
|
|
router.replace('/dashboard');
|
|
return;
|
|
}
|
|
// 인증 안됨 (401) → 현재 페이지 유지
|
|
} catch {
|
|
// API 호출 실패 → 현재 페이지 유지
|
|
} finally {
|
|
setIsChecking(false);
|
|
}
|
|
};
|
|
|
|
checkAuth();
|
|
}, [router]);
|
|
|
|
const handleSubmit = async () => {
|
|
setIsLoading(true);
|
|
setError("");
|
|
|
|
try {
|
|
// Prepare request body matching backend format
|
|
const requestBody = {
|
|
user_id: formData.userId,
|
|
name: formData.name,
|
|
email: formData.email,
|
|
phone: formData.phone,
|
|
password: formData.password,
|
|
password_confirmation: formData.passwordConfirm,
|
|
position: formData.position || "",
|
|
company_name: formData.companyName,
|
|
business_num: formData.businessNumber,
|
|
company_scale: formData.companySize,
|
|
industry: formData.industry,
|
|
};
|
|
|
|
// 🔵 Next.js 프록시 → PHP /api/v1/register (회원가입 처리)
|
|
const response = await fetch('/api/auth/signup', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(requestBody),
|
|
credentials: 'include',
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (!response.ok) {
|
|
setError(data.error || 'Registration failed');
|
|
setIsLoading(false);
|
|
return;
|
|
}
|
|
|
|
console.log('✅ Signup successful:', data);
|
|
|
|
// Navigate to login page after successful signup
|
|
router.push("/login?registered=true");
|
|
|
|
} catch (err) {
|
|
console.error('Signup error:', err);
|
|
setError('Network error. Please try again.');
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const [stepErrors, setStepErrors] = useState<{ [key: string]: string }>({});
|
|
|
|
const validateStep1 = () => {
|
|
const result = companyInfoSchema.safeParse({
|
|
companyName: formData.companyName,
|
|
businessNumber: formData.businessNumber,
|
|
industry: formData.industry,
|
|
companySize: formData.companySize,
|
|
});
|
|
|
|
if (!result.success) {
|
|
const firstError = result.error.issues[0];
|
|
setStepErrors({ step1: firstError.message });
|
|
return false;
|
|
}
|
|
setStepErrors({});
|
|
return true;
|
|
};
|
|
|
|
const validateStep2 = () => {
|
|
const result = userInfoSchema.safeParse({
|
|
name: formData.name,
|
|
position: formData.position,
|
|
email: formData.email,
|
|
phone: formData.phone,
|
|
userId: formData.userId,
|
|
password: formData.password,
|
|
passwordConfirm: formData.passwordConfirm,
|
|
});
|
|
|
|
if (!result.success) {
|
|
const firstError = result.error.issues[0];
|
|
setStepErrors({ step2: firstError.message });
|
|
return false;
|
|
}
|
|
setStepErrors({});
|
|
return true;
|
|
};
|
|
|
|
const validateStep3 = () => {
|
|
const result = planSelectionSchema.safeParse({
|
|
plan: formData.plan,
|
|
salesCode: formData.salesCode,
|
|
agreeTerms: formData.agreeTerms,
|
|
agreePrivacy: formData.agreePrivacy,
|
|
});
|
|
|
|
if (!result.success) {
|
|
const firstError = result.error.issues[0];
|
|
setStepErrors({ step3: firstError.message });
|
|
return false;
|
|
}
|
|
setStepErrors({});
|
|
return true;
|
|
};
|
|
|
|
const isStep1Valid = formData.companyName && formData.businessNumber && formData.industry && formData.companySize;
|
|
const isStep2Valid = formData.name && formData.email && formData.phone && formData.userId && formData.password && formData.password === formData.passwordConfirm;
|
|
const isStep3Valid = formData.agreeTerms && formData.agreePrivacy;
|
|
|
|
// 인증 체크 중일 때는 로딩 표시
|
|
if (isChecking) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center">
|
|
<div className="text-center space-y-4">
|
|
<div className="inline-block h-12 w-12 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent"></div>
|
|
<p className="text-muted-foreground font-medium">불러오는 중...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-background flex flex-col">
|
|
{/* Header */}
|
|
<header className="clean-glass border-b border-border">
|
|
<div className="container mx-auto px-6 py-4">
|
|
<div className="flex items-center justify-between">
|
|
<button
|
|
onClick={() => router.push("/")}
|
|
className="flex items-center space-x-3 hover:opacity-80 transition-opacity"
|
|
>
|
|
<div className="w-10 h-10 rounded-xl flex items-center justify-center clean-shadow relative overflow-hidden" style={{ backgroundColor: '#3B82F6' }}>
|
|
<div className="text-white font-bold text-lg">S</div>
|
|
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-transparent opacity-30"></div>
|
|
</div>
|
|
<div>
|
|
<h1 className="text-xl font-bold tracking-wide">SAM</h1>
|
|
<p className="text-xs text-muted-foreground">{t("signupTitle")}</p>
|
|
</div>
|
|
</button>
|
|
<div className="flex items-center gap-3">
|
|
<ThemeSelect native={false} />
|
|
<LanguageSelect native={false} />
|
|
<Button variant="ghost" onClick={() => router.push("/login")} className="rounded-xl">
|
|
{t("login")}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Main Content */}
|
|
<div className="flex-1 container mx-auto px-6 py-12">
|
|
<div className="max-w-2xl mx-auto">
|
|
{/* Progress Steps */}
|
|
<div className="mb-12">
|
|
<div className="flex items-center justify-between mb-4">
|
|
{[1, 2, 3].map((stepNumber) => (
|
|
<div key={stepNumber} className={`flex items-center ${stepNumber === 2 ? 'justify-center' : 'flex-1'}`}>
|
|
{(stepNumber === 2 || stepNumber === 3) && (
|
|
<div className={`flex-1 h-1 mx-4 rounded transition-colors ${
|
|
step > stepNumber - 1 ? "bg-primary" : "bg-muted"
|
|
}`} />
|
|
)}
|
|
<div className={`w-10 h-10 rounded-full flex items-center justify-center font-semibold transition-colors ${
|
|
step >= stepNumber
|
|
? "bg-primary text-white"
|
|
: "bg-muted text-muted-foreground"
|
|
}`}>
|
|
{stepNumber}
|
|
</div>
|
|
{(stepNumber === 1 || stepNumber === 2) && (
|
|
<div className={`flex-1 h-1 mx-4 rounded transition-colors ${
|
|
step > stepNumber ? "bg-primary" : "bg-muted"
|
|
}`} />
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className="flex justify-between text-sm">
|
|
<span className={step >= 1 ? "text-foreground font-medium" : "text-muted-foreground"}>
|
|
{t("companyInfo")}
|
|
</span>
|
|
<span className={step >= 2 ? "text-foreground font-medium" : "text-muted-foreground"}>
|
|
{t("userInfo")}
|
|
</span>
|
|
<span className={step >= 3 ? "text-foreground font-medium" : "text-muted-foreground"}>
|
|
{t("planSelection")}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Step 1: 회사 정보 */}
|
|
{step === 1 && (
|
|
<div className="clean-glass rounded-2xl p-8 clean-shadow space-y-6">
|
|
<div>
|
|
<h2 className="mb-2 text-foreground">{t("step1Title")}</h2>
|
|
<p className="text-muted-foreground">{t("step1Desc")}</p>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div>
|
|
<Label htmlFor="companyName" className="flex items-center space-x-2 mb-2">
|
|
<Building2 className="w-4 h-4" />
|
|
<span>{t("companyName")} {t("required")}</span>
|
|
</Label>
|
|
<Input
|
|
id="companyName"
|
|
name="company_name"
|
|
autoComplete="organization"
|
|
placeholder={t("companyNamePlaceholder")}
|
|
value={formData.companyName}
|
|
onChange={(e) => handleInputChange("companyName", e.target.value)}
|
|
className="clean-input"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="businessNumber" className="flex items-center space-x-2 mb-2">
|
|
<FileText className="w-4 h-4" />
|
|
<span>{t("businessNumber")} {t("required")}</span>
|
|
</Label>
|
|
<Input
|
|
id="businessNumber"
|
|
name="business_number"
|
|
autoComplete="off"
|
|
placeholder={t("businessNumberPlaceholder")}
|
|
value={formData.businessNumber}
|
|
onChange={(e) => handleBusinessNumberChange(e.target.value)}
|
|
className="clean-input"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="industry" className="flex items-center space-x-2 mb-2">
|
|
<Briefcase className="w-4 h-4" />
|
|
<span>{t("industry")} {t("required")}</span>
|
|
</Label>
|
|
<Select value={formData.industry} onValueChange={(value) => handleInputChange("industry", value)}>
|
|
<SelectTrigger className="clean-input">
|
|
<SelectValue placeholder={t("industryPlaceholder")} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="electronics">{tSignup("industries.electronics")}</SelectItem>
|
|
<SelectItem value="machinery">{tSignup("industries.machinery")}</SelectItem>
|
|
<SelectItem value="automotive">{tSignup("industries.automotive")}</SelectItem>
|
|
<SelectItem value="chemical">{tSignup("industries.chemical")}</SelectItem>
|
|
<SelectItem value="food">{tSignup("industries.food")}</SelectItem>
|
|
<SelectItem value="textile">{tSignup("industries.textile")}</SelectItem>
|
|
<SelectItem value="metal">{tSignup("industries.metal")}</SelectItem>
|
|
<SelectItem value="other">{tSignup("industries.other")}</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="companySize" className="flex items-center space-x-2 mb-2">
|
|
<Users className="w-4 h-4" />
|
|
<span>{t("companySize")} {t("required")}</span>
|
|
</Label>
|
|
<Select value={formData.companySize} onValueChange={(value) => handleInputChange("companySize", value)}>
|
|
<SelectTrigger className="clean-input">
|
|
<SelectValue placeholder={t("companySizePlaceholder")} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="small">{tSignup("companySizes.small")}</SelectItem>
|
|
<SelectItem value="medium">{tSignup("companySizes.medium")}</SelectItem>
|
|
<SelectItem value="large">{tSignup("companySizes.large")}</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
{stepErrors.step1 && (
|
|
<div className="bg-destructive/10 border border-destructive/30 rounded-xl p-4">
|
|
<p className="text-sm text-destructive text-center">{stepErrors.step1}</p>
|
|
</div>
|
|
)}
|
|
|
|
<Button
|
|
onClick={() => {
|
|
if (validateStep1()) {
|
|
setStep(2);
|
|
}
|
|
}}
|
|
disabled={!isStep1Valid}
|
|
className="w-full rounded-xl bg-primary hover:bg-primary/90"
|
|
>
|
|
{t("nextStep")}
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Step 2: 담당자 정보 */}
|
|
{step === 2 && (
|
|
<div className="clean-glass rounded-2xl p-8 clean-shadow space-y-6">
|
|
<div>
|
|
<h2 className="mb-2 text-foreground">{t("step2Title")}</h2>
|
|
<p className="text-muted-foreground">{t("step2Desc")}</p>
|
|
</div>
|
|
|
|
<form onSubmit={(e) => { e.preventDefault(); if (validateStep2()) setStep(3); }} className="space-y-4">
|
|
<div>
|
|
<Label htmlFor="name" className="flex items-center space-x-2 mb-2">
|
|
<User className="w-4 h-4"/>
|
|
<span>{t("name")} {t("required")}</span>
|
|
</Label>
|
|
<Input
|
|
id="name"
|
|
name="name"
|
|
autoComplete="name"
|
|
placeholder={t("namePlaceholder")}
|
|
value={formData.name}
|
|
onChange={(e) => handleInputChange("name", e.target.value)}
|
|
className="clean-input"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="position" className="flex items-center space-x-2 mb-2">
|
|
<Briefcase className="w-4 h-4"/>
|
|
<span>{t("position")} {t("optional")}</span>
|
|
</Label>
|
|
<Input
|
|
id="position"
|
|
name="position"
|
|
autoComplete="organization-title"
|
|
placeholder={t("positionPlaceholder")}
|
|
value={formData.position}
|
|
onChange={(e) => handleInputChange("position", e.target.value)}
|
|
className="clean-input"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="email" className="flex items-center space-x-2 mb-2">
|
|
<Mail className="w-4 h-4"/>
|
|
<span>{t("email")} {t("required")}</span>
|
|
</Label>
|
|
<Input
|
|
id="email"
|
|
name="email"
|
|
type="email"
|
|
autoComplete="email"
|
|
placeholder={t("emailPlaceholder")}
|
|
value={formData.email}
|
|
onChange={(e) => handleInputChange("email", e.target.value)}
|
|
className="clean-input"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="phone" className="flex items-center space-x-2 mb-2">
|
|
<Phone className="w-4 h-4"/>
|
|
<span>{t("phone")} {t("required")}</span>
|
|
</Label>
|
|
<Input
|
|
id="phone"
|
|
name="phone"
|
|
type="tel"
|
|
autoComplete="tel"
|
|
placeholder={t("phonePlaceholder")}
|
|
value={formData.phone}
|
|
onChange={(e) => handlePhoneNumberChange(e.target.value)}
|
|
className="clean-input"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="userId" className="flex items-center space-x-2 mb-2">
|
|
<User className="w-4 h-4"/>
|
|
<span>{t("userId")} {t("required")}</span>
|
|
</Label>
|
|
<Input
|
|
id="userId"
|
|
name="user_id"
|
|
autoComplete="username"
|
|
placeholder={t("userIdPlaceholder2")}
|
|
value={formData.userId}
|
|
onChange={(e) => handleInputChange("userId", e.target.value)}
|
|
className="clean-input"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="password" className="flex items-center space-x-2 mb-2">
|
|
<Lock className="w-4 h-4"/>
|
|
<span>{t("password")} {t("required")}</span>
|
|
</Label>
|
|
<Input
|
|
id="password"
|
|
name="password"
|
|
type="password"
|
|
autoComplete="new-password"
|
|
placeholder={t("passwordPlaceholder2")}
|
|
value={formData.password}
|
|
onChange={(e) => handleInputChange("password", e.target.value)}
|
|
className="clean-input"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="passwordConfirm" className="flex items-center space-x-2 mb-2">
|
|
<Lock className="w-4 h-4"/>
|
|
<span>{t("passwordConfirm")} {t("required")}</span>
|
|
</Label>
|
|
<Input
|
|
id="passwordConfirm"
|
|
name="password_confirm"
|
|
type="password"
|
|
autoComplete="new-password"
|
|
placeholder={t("passwordConfirmPlaceholder")}
|
|
value={formData.passwordConfirm}
|
|
onChange={(e) => handleInputChange("passwordConfirm", e.target.value)}
|
|
className="clean-input"
|
|
/>
|
|
{formData.passwordConfirm && formData.password !== formData.passwordConfirm && (
|
|
<p className="text-sm text-destructive mt-1">{tValidation("passwordMismatch")}</p>
|
|
)}
|
|
</div>
|
|
|
|
{stepErrors.step2 && (
|
|
<div className="bg-destructive/10 border border-destructive/30 rounded-xl p-4">
|
|
<p className="text-sm text-destructive text-center">{stepErrors.step2}</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex space-x-4">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => setStep(1)}
|
|
className="flex-1 rounded-xl"
|
|
>
|
|
<ArrowLeft className="w-4 h-4 mr-2"/>
|
|
{t("previousStep")}
|
|
</Button>
|
|
<Button
|
|
type="submit"
|
|
disabled={!isStep2Valid}
|
|
className="flex-1 rounded-xl bg-primary hover:bg-primary/90"
|
|
>
|
|
{t("nextStep")}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
)}
|
|
|
|
{/* Step 3: 플랜 선택 */}
|
|
{step === 3 && (
|
|
<div className="space-y-6">
|
|
<div className="clean-glass rounded-2xl p-8 clean-shadow space-y-6">
|
|
{/*
|
|
<div>
|
|
<h2 className="mb-2 text-foreground">플랜을 선택해주세요</h2>
|
|
<p className="text-muted-foreground">먼저 30일 무료 체험으로 시작해보세요</p>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
{[
|
|
{ id: "demo", name: "데모 체험판", desc: "30일 무료 체험 (모든 기능 이용)", badge: "추천" },
|
|
{ id: "standard", name: "스탠다드", desc: "중소기업 최적화 플랜" },
|
|
{ id: "premium", name: "프리미엄", desc: "중견기업 맞춤형 플랜" },
|
|
].map((plan) => (
|
|
<button
|
|
key={plan.id}
|
|
onClick={() => handleInputChange("plan", plan.id)}
|
|
className={`w-full p-4 rounded-xl border-2 transition-all text-left ${
|
|
formData.plan === plan.id
|
|
? "border-primary bg-primary/5"
|
|
: "border-border hover:border-primary/50"
|
|
}`}
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<div className="flex items-center space-x-2">
|
|
<span className="font-semibold">{plan.name}</span>
|
|
{plan.badge && (
|
|
<Badge className="bg-primary text-white text-xs">
|
|
{plan.badge}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
<p className="text-sm text-muted-foreground mt-1">{plan.desc}</p>
|
|
</div>
|
|
{formData.plan === plan.id && (
|
|
<CheckCircle2 className="w-6 h-6 text-primary" />
|
|
)}
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="salesCode" className="flex items-center space-x-2 mb-2">
|
|
<Tag className="w-4 h-4" />
|
|
<span>영업사원 추천코드 (선택)</span>
|
|
</Label>
|
|
<div className="relative">
|
|
<Input
|
|
id="salesCode"
|
|
placeholder="추천코드를 입력하면 할인 혜택을 받을 수 있습니다"
|
|
value={formData.salesCode}
|
|
onChange={(e) => handleSalesCodeChange(e.target.value)}
|
|
className={`clean-input pr-10 ${
|
|
salesCodeValid === true ? "border-green-500" :
|
|
salesCodeValid === false ? "border-destructive" : ""
|
|
}`}
|
|
/>
|
|
{salesCodeValid === true && (
|
|
<CheckCircle2 className="w-5 h-5 text-green-500 absolute right-3 top-1/2 -translate-y-1/2" />
|
|
)}
|
|
</div>
|
|
{salesCodeValid === true && (
|
|
<p className="text-sm text-green-600 mt-2 flex items-center space-x-2">
|
|
<CheckCircle2 className="w-4 h-4" />
|
|
<span>유효한 코드입니다! {discount}% 할인이 적용됩니다</span>
|
|
</p>
|
|
)}
|
|
{salesCodeValid === false && (
|
|
<p className="text-sm text-destructive mt-2">유효하지 않은 코드입니다</p>
|
|
)}
|
|
<p className="text-xs text-muted-foreground mt-2">
|
|
💡 예시 코드: SALES2024 (20%), PARTNER30 (30%), VIP50 (50%)
|
|
</p>
|
|
</div>
|
|
*/}
|
|
<div className="space-y-3 pt-4 border-t border-border">
|
|
{/* 전체 동의 */}
|
|
<label className="flex items-center space-x-3 cursor-pointer pt-3 pb-3 pe-3 rounded-lg bg-muted/30 hover:bg-muted/50 transition-colors">
|
|
<input
|
|
type="checkbox"
|
|
checked={isAllAgreed}
|
|
onChange={(e) => handleAgreeAll(e.target.checked)}
|
|
className="w-5 h-5 rounded border-border flex-shrink-0"
|
|
/>
|
|
<span className="text-sm font-semibold text-foreground">
|
|
{t("agreeAll")}
|
|
</span>
|
|
</label>
|
|
|
|
{/* 개별 약관 */}
|
|
<div className="space-y-2">
|
|
<label className="flex items-center space-x-3 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={formData.agreeTerms}
|
|
onChange={(e) => handleInputChange("agreeTerms", e.target.checked)}
|
|
className="w-4 h-4 rounded border-border flex-shrink-0"
|
|
/>
|
|
<span className="text-sm">
|
|
<span className="font-medium text-foreground">{t("required")}</span> {t("agreeTerms")}
|
|
</span>
|
|
</label>
|
|
<label className="flex items-center space-x-3 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={formData.agreePrivacy}
|
|
onChange={(e) => handleInputChange("agreePrivacy", e.target.checked)}
|
|
className="w-4 h-4 rounded border-border flex-shrink-0"
|
|
/>
|
|
<span className="text-sm">
|
|
<span className="font-medium text-foreground">{t("required")}</span> {t("agreePrivacy")}
|
|
</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="bg-destructive/10 border border-destructive/30 rounded-xl p-4">
|
|
<p className="text-sm text-destructive text-center">{error}</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex space-x-4">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setStep(2)}
|
|
className="flex-1 rounded-xl"
|
|
disabled={isLoading}
|
|
>
|
|
<ArrowLeft className="w-4 h-4 mr-2" />
|
|
{t("previousStep")}
|
|
</Button>
|
|
<Button
|
|
onClick={() => {
|
|
if (validateStep3()) {
|
|
void handleSubmit();
|
|
}
|
|
}}
|
|
disabled={!isStep3Valid || isLoading}
|
|
className="flex-1 rounded-xl bg-primary hover:bg-primary/90"
|
|
>
|
|
{isLoading ? t("processing") || "처리 중..." : t("complete")}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Login Link */}
|
|
<div className="text-center mt-6">
|
|
<p className="text-sm text-muted-foreground">
|
|
{t("alreadyHaveAccount")}{" "}
|
|
<button
|
|
onClick={() => router.push("/login")}
|
|
className="text-primary font-medium hover:underline"
|
|
>
|
|
{t("login")}
|
|
</button>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
} |