CHORE(merge): merge from develop
Some checks failed
Build Docker Image / build-and-push (push) Has been cancelled
CI / lint-and-build (push) Has been cancelled

- Initial setup and all features from develop branch
- Includes: auth, deploy, docker, style fixes
- K3S deployment configuration
This commit is contained in:
2026-01-06 17:29:16 +09:00
parent b4ce36ba3b
commit f78454c2a1
159 changed files with 18365 additions and 774 deletions

View File

@@ -0,0 +1,288 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { signUp, type SignUpData } from "@/lib/services";
// 비밀번호 검증 함수 (영문/숫자 포함 8자 이상)
const validatePassword = (password: string): boolean => {
const hasLetter = /[A-Za-z]/.test(password);
const hasNumber = /\d/.test(password);
const isLongEnough = password.length >= 8;
return hasLetter && hasNumber && isLongEnough;
};
const SignUpForm = () => {
const {
register,
handleSubmit,
watch,
setError,
formState: { errors, isSubmitting },
} = useForm<SignUpData>({
mode: "onChange",
defaultValues: {
userId: "",
userPassword: "",
userCheckPassword: "",
userName: "",
userPhone: "",
authCode: "",
},
});
const password = watch("userPassword");
const [submitError, setSubmitError] = useState<string | null>(null);
const router = useRouter();
const onSubmit = async (data: SignUpData) => {
setSubmitError(null);
try {
await signUp(data);
// 회원가입 성공
alert("회원가입이 완료되었습니다. 로그인해주세요.");
router.push("/login");
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "회원가입에 실패했습니다.";
// 아이디 중복 에러
if (errorMessage.includes("아이디")) {
setError("userId", {
type: "manual",
message: errorMessage,
});
}
// 전화번호 중복 에러
else if (errorMessage.includes("전화번호")) {
setError("userPhone", {
type: "manual",
message: errorMessage,
});
}
// 기타 에러
else {
setSubmitError(errorMessage);
}
}
};
return (
<div className="bg-white rounded-2xl shadow-xl border border-gray-100 p-8">
<form onSubmit={handleSubmit(onSubmit)} className="space-y-5">
{/* 아이디 */}
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">
<span className="text-red-500">*</span>
</label>
<input
type="text"
{...register("userId", {
required: "아이디를 입력해주세요",
minLength: {
value: 4,
message: "아이디는 4자 이상이어야 합니다",
},
})}
disabled={isSubmitting}
placeholder="아이디를 입력해주세요"
className={`w-full h-12 px-4 py-3 bg-gray-50 border rounded-xl focus:outline-none focus:ring-2 focus:border-transparent disabled:opacity-50 transition-all ${
errors.userId
? "border-red-300 focus:ring-red-400"
: "border-gray-200 focus:ring-[#6b95c6]"
}`}
/>
{errors.userId && (
<p className="text-red-500 text-xs mt-1">{errors.userId.message}</p>
)}
</div>
{/* 비밀번호 */}
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">
<span className="text-red-500">*</span>
</label>
<input
type="password"
{...register("userPassword", {
required: "비밀번호를 입력해주세요",
validate: {
validPassword: (value) =>
validatePassword(value) ||
"영문과 숫자를 포함하여 8자 이상 입력해주세요",
},
})}
disabled={isSubmitting}
placeholder="영문/숫자 포함 8자 이상"
className={`w-full h-12 px-4 py-3 bg-gray-50 border rounded-xl focus:outline-none focus:ring-2 focus:border-transparent disabled:opacity-50 transition-all ${
errors.userPassword
? "border-red-300 focus:ring-red-400"
: "border-gray-200 focus:ring-[#6b95c6]"
}`}
/>
{errors.userPassword && (
<p className="text-red-500 text-xs mt-1">
{errors.userPassword.message}
</p>
)}
</div>
{/* 비밀번호 확인 */}
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">
<span className="text-red-500">*</span>
</label>
<input
type="password"
{...register("userCheckPassword", {
required: "비밀번호를 다시 입력해주세요",
validate: {
matchPassword: (value) =>
value === password || "비밀번호가 일치하지 않습니다",
},
})}
disabled={isSubmitting}
placeholder="비밀번호를 다시 입력해주세요"
className={`w-full h-12 px-4 py-3 bg-gray-50 border rounded-xl focus:outline-none focus:ring-2 focus:border-transparent disabled:opacity-50 transition-all ${
errors.userCheckPassword
? "border-red-300 focus:ring-red-400"
: "border-gray-200 focus:ring-[#6b95c6]"
}`}
/>
{errors.userCheckPassword && (
<p className="text-red-500 text-xs mt-1">
{errors.userCheckPassword.message}
</p>
)}
</div>
{/* 이름 */}
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">
<span className="text-red-500">*</span>
</label>
<input
type="text"
{...register("userName", {
required: "이름을 입력해주세요",
minLength: {
value: 2,
message: "이름은 2자 이상이어야 합니다",
},
})}
disabled={isSubmitting}
placeholder="이름을 입력해주세요"
className={`w-full h-12 px-4 py-3 bg-gray-50 border rounded-xl focus:outline-none focus:ring-2 focus:border-transparent disabled:opacity-50 transition-all ${
errors.userName
? "border-red-300 focus:ring-red-400"
: "border-gray-200 focus:ring-[#6b95c6]"
}`}
/>
{errors.userName && (
<p className="text-red-500 text-xs mt-1">{errors.userName.message}</p>
)}
</div>
{/* 전화번호 */}
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">
<span className="text-red-500">*</span>
</label>
<input
type="tel"
{...register("userPhone", {
required: "전화번호를 입력해주세요",
pattern: {
value: /^01[0-9][0-9]{7,8}$/,
message: "하이픈(-) 없이 숫자만 입력해주세요 (예: 01012345678)",
},
})}
disabled={isSubmitting}
placeholder="01012345678"
className={`w-full h-12 px-4 py-3 bg-gray-50 border rounded-xl focus:outline-none focus:ring-2 focus:border-transparent disabled:opacity-50 transition-all ${
errors.userPhone
? "border-red-300 focus:ring-red-400"
: "border-gray-200 focus:ring-[#6b95c6]"
}`}
/>
{errors.userPhone && (
<p className="text-red-500 text-xs mt-1">
{errors.userPhone.message}
</p>
)}
</div>
{/* 승인번호 */}
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">
<span className="text-red-500">*</span>
</label>
<input
type="text"
{...register("authCode", {
required: "승인번호를 입력해주세요",
})}
disabled={isSubmitting}
className={`w-full h-12 px-4 py-3 bg-gray-50 border rounded-xl focus:outline-none focus:ring-2 focus:border-transparent disabled:opacity-50 transition-all ${
errors.authCode
? "border-red-300 focus:ring-red-400"
: "border-gray-200 focus:ring-[#6b95c6]"
}`}
/>
{errors.authCode && (
<p className="text-red-500 text-xs mt-1">
{errors.authCode.message}
</p>
)}
</div>
{/* 제출 에러 메시지 */}
{submitError && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg text-sm">
{submitError}
</div>
)}
<button
type="submit"
disabled={isSubmitting}
className="w-full h-12 bg-linear-to-br from-[#7ba5d6] to-[#6b95c6] hover:from-[#6b95c6] hover:to-[#5b85b6] text-white rounded-xl shadow-lg hover:shadow-xl hover:scale-[1.02] transition-all disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 flex items-center justify-center font-semibold mt-6"
>
{isSubmitting ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2" />
...
</>
) : (
"회원가입"
)}
</button>
{/* 로그인 페이지로 */}
<div className="text-center text-sm">
<span className="text-gray-600"> ? </span>
<Link
href="/login"
className="text-[#6b95c6] hover:text-[#5b85b6] font-semibold hover:underline transition-colors"
>
</Link>
</div>
{/* 홈으로 돌아가기 */}
<Link
href="/"
className="block text-center text-sm text-gray-600 hover:text-[#6b95c6] transition-colors"
>
</Link>
</form>
</div>
);
};
export default SignUpForm;