diff --git a/frontend/app/(auth)/register/page.tsx b/frontend/app/(auth)/register/page.tsx
new file mode 100644
index 00000000..bd098543
--- /dev/null
+++ b/frontend/app/(auth)/register/page.tsx
@@ -0,0 +1,51 @@
+"use client";
+
+import { useRegister } from "@/hooks/useRegister";
+import { LoginHeader } from "@/components/auth/LoginHeader";
+import { RegisterForm } from "@/components/auth/RegisterForm";
+import { LoginFooter } from "@/components/auth/LoginFooter";
+
+/**
+ * 회원가입 페이지 컴포넌트
+ * 비즈니스 로직은 useRegister 훅에서 처리하고, UI 컴포넌트들을 조합하여 구성
+ */
+export default function RegisterPage() {
+ const {
+ formData,
+ isLoading,
+ error,
+ validationErrors,
+ showPassword,
+ showPasswordConfirm,
+ isFormValid,
+ handleInputChange,
+ handleRegister,
+ togglePasswordVisibility,
+ togglePasswordConfirmVisibility,
+ } = useRegister();
+
+ return (
+
+ );
+}
+
diff --git a/frontend/components/auth/LoginFooter.tsx b/frontend/components/auth/LoginFooter.tsx
index 99b5da16..42edf9e2 100644
--- a/frontend/components/auth/LoginFooter.tsx
+++ b/frontend/components/auth/LoginFooter.tsx
@@ -1,11 +1,29 @@
import { UI_CONFIG } from "@/constants/auth";
+import Link from "next/link";
+
+interface LoginFooterProps {
+ showRegisterLink?: boolean;
+}
/**
* 로그인 페이지 푸터 컴포넌트
*/
-export function LoginFooter() {
+export function LoginFooter({ showRegisterLink = true }: LoginFooterProps) {
return (
+ {showRegisterLink && (
+
+
+ 계정이 없으신가요?{" "}
+
+ 회원가입
+
+
+
+ )}
{UI_CONFIG.COPYRIGHT}
{UI_CONFIG.POWERED_BY}
diff --git a/frontend/components/auth/RegisterForm.tsx b/frontend/components/auth/RegisterForm.tsx
new file mode 100644
index 00000000..2fd26432
--- /dev/null
+++ b/frontend/components/auth/RegisterForm.tsx
@@ -0,0 +1,259 @@
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Eye, EyeOff, Loader2, ArrowLeft } from "lucide-react";
+import { RegisterFormData } from "@/types/auth";
+import { ErrorMessage } from "./ErrorMessage";
+import Link from "next/link";
+
+interface RegisterFormProps {
+ formData: RegisterFormData;
+ isLoading: boolean;
+ error: string;
+ validationErrors: Record;
+ showPassword: boolean;
+ showPasswordConfirm: boolean;
+ isFormValid: boolean;
+ onInputChange: (e: React.ChangeEvent) => void;
+ onSubmit: (e: React.FormEvent) => void;
+ onTogglePassword: () => void;
+ onTogglePasswordConfirm: () => void;
+}
+
+/**
+ * 회원가입 폼 컴포넌트
+ */
+export function RegisterForm({
+ formData,
+ isLoading,
+ error,
+ validationErrors,
+ showPassword,
+ showPasswordConfirm,
+ isFormValid,
+ onInputChange,
+ onSubmit,
+ onTogglePassword,
+ onTogglePasswordConfirm,
+}: RegisterFormProps) {
+ return (
+
+
+ 회원가입
+ 새로운 계정을 생성합니다
+
+
+
+
+
+
+
+ );
+}
+
diff --git a/frontend/hooks/useRegister.ts b/frontend/hooks/useRegister.ts
new file mode 100644
index 00000000..09ec46ad
--- /dev/null
+++ b/frontend/hooks/useRegister.ts
@@ -0,0 +1,262 @@
+import { useState, useEffect } from "react";
+import { useRouter } from "next/navigation";
+import { RegisterFormData } from "@/types/auth";
+import { authApi } from "@/lib/api/auth";
+import { useToast } from "@/hooks/use-toast";
+
+/**
+ * 회원가입 비즈니스 로직 훅
+ */
+export function useRegister() {
+ const router = useRouter();
+ const { toast } = useToast();
+ const [formData, setFormData] = useState({
+ userId: "",
+ password: "",
+ passwordConfirm: "",
+ userName: "",
+ licenseNumber: "",
+ vehicleNumber: "",
+ phoneNumber: "",
+ });
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState("");
+ const [validationErrors, setValidationErrors] = useState>({});
+ const [showPassword, setShowPassword] = useState(false);
+ const [showPasswordConfirm, setShowPasswordConfirm] = useState(false);
+ const [isFormValid, setIsFormValid] = useState(false);
+
+ /**
+ * 실시간 폼 유효성 검사 및 에러 메시지 업데이트
+ */
+ useEffect(() => {
+ const checkFormValidity = () => {
+ const errors: Record = {};
+
+ // 사용자 ID 검사 (입력이 있을 때만)
+ if (formData.userId.length > 0 && formData.userId.length < 2) {
+ errors.userId = "사용자 ID는 최소 2자 이상이어야 합니다";
+ }
+
+ // 비밀번호 검사 (입력이 있을 때만)
+ if (formData.password.length > 0 && formData.password.length < 6) {
+ errors.password = "비밀번호는 최소 6자 이상이어야 합니다";
+ }
+
+ // 비밀번호 확인 검사 (입력이 있을 때만)
+ if (formData.passwordConfirm.length > 0 && formData.password !== formData.passwordConfirm) {
+ errors.passwordConfirm = "비밀번호가 일치하지 않습니다";
+ }
+
+ // 이름 검사 (입력이 있을 때만)
+ if (formData.userName.length > 0 && formData.userName.trim().length < 2) {
+ errors.userName = "이름은 최소 2자 이상이어야 합니다";
+ }
+
+ // 면허번호 검사 (입력이 있을 때만)
+ if (formData.licenseNumber.length > 0 && !validateLicenseNumber(formData.licenseNumber)) {
+ errors.licenseNumber = "올바른 면허번호 형식이 아닙니다 (예: 12-34-567890-12)";
+ }
+
+ // 차량 번호 검사 (입력이 있을 때만)
+ if (formData.vehicleNumber.length > 0 && !validateVehicleNumber(formData.vehicleNumber)) {
+ errors.vehicleNumber = "올바른 차량 번호 형식이 아닙니다 (예: 12가3456, 123가4567)";
+ }
+
+ // 휴대폰 번호 검사 (입력이 있을 때만)
+ if (formData.phoneNumber.length > 0 && !validatePhoneNumber(formData.phoneNumber)) {
+ errors.phoneNumber = "올바른 휴대폰 번호 형식이 아닙니다 (예: 010-1234-5678)";
+ }
+
+ setValidationErrors(errors);
+
+ // 모든 필드가 채워져 있고 에러가 없는지 확인
+ const allFieldsFilled =
+ formData.userId.trim().length >= 2 &&
+ formData.password.length >= 6 &&
+ formData.passwordConfirm.length > 0 &&
+ formData.userName.trim().length >= 2 &&
+ formData.licenseNumber.length > 0 &&
+ formData.vehicleNumber.length > 0 &&
+ formData.phoneNumber.length > 0;
+
+ const isValid = allFieldsFilled && Object.keys(errors).length === 0;
+ setIsFormValid(isValid);
+ };
+
+ checkFormValidity();
+ }, [formData]);
+
+ /**
+ * 입력값 변경 핸들러
+ */
+ const handleInputChange = (e: React.ChangeEvent) => {
+ const { name, value } = e.target;
+ setFormData((prev) => ({ ...prev, [name]: value }));
+
+ // 전역 에러 초기화
+ if (error) setError("");
+ };
+
+ /**
+ * 비밀번호 표시/숨김 토글
+ */
+ const togglePasswordVisibility = () => {
+ setShowPassword((prev) => !prev);
+ };
+
+ /**
+ * 비밀번호 확인 표시/숨김 토글
+ */
+ const togglePasswordConfirmVisibility = () => {
+ setShowPasswordConfirm((prev) => !prev);
+ };
+
+ /**
+ * 면허번호 유효성 검사
+ * 한국 운전면허 형식: 12-34-567890-12 (지역번호-발급년도-일련번호-체크)
+ */
+ const validateLicenseNumber = (licenseNumber: string): boolean => {
+ // 하이픈 포함 형식: 12-34-567890-12
+ const pattern = /^\d{2}-\d{2}-\d{6}-\d{2}$/;
+ return pattern.test(licenseNumber);
+ };
+
+ /**
+ * 차량 번호 유효성 검사
+ * 한국 차량 번호 형식: 12가3456, 123가4567, 서울12가3456 등
+ */
+ const validateVehicleNumber = (vehicleNumber: string): boolean => {
+ // 공백 제거
+ const cleanNumber = vehicleNumber.replace(/\s/g, "");
+
+ // 한국 차량 번호 패턴
+ // 1. 구형: 12가3456 (2자리 숫자 + 한글 1자 + 4자리 숫자)
+ // 2. 신형: 123가4567 (3자리 숫자 + 한글 1자 + 4자리 숫자)
+ // 3. 지역명 포함: 서울12가3456, 서울123가4567
+ const patterns = [
+ /^\d{2}[가-힣]{1}\d{4}$/, // 12가3456
+ /^\d{3}[가-힣]{1}\d{4}$/, // 123가4567
+ /^[가-힣]{2}\d{2}[가-힣]{1}\d{4}$/, // 서울12가3456
+ /^[가-힣]{2}\d{3}[가-힣]{1}\d{4}$/, // 서울123가4567
+ ];
+
+ return patterns.some(pattern => pattern.test(cleanNumber));
+ };
+
+ /**
+ * 휴대폰 번호 유효성 검사
+ * 형식: 010-1234-5678, 011-123-4567 등
+ */
+ const validatePhoneNumber = (phoneNumber: string): boolean => {
+ // 하이픈 포함 형식: 010-1234-5678, 011-123-4567
+ const pattern = /^01[016789]-\d{3,4}-\d{4}$/;
+ return pattern.test(phoneNumber);
+ };
+
+ /**
+ * 폼 유효성 검사
+ */
+ const validateForm = (): boolean => {
+ const errors: Record = {};
+
+ // 사용자 ID 검사
+ if (formData.userId.length < 2) {
+ errors.userId = "사용자 ID는 최소 2자 이상이어야 합니다";
+ }
+
+ // 비밀번호 검사
+ if (formData.password.length < 6) {
+ errors.password = "비밀번호는 최소 6자 이상이어야 합니다";
+ }
+
+ // 비밀번호 확인 검사
+ if (formData.password !== formData.passwordConfirm) {
+ errors.passwordConfirm = "비밀번호가 일치하지 않습니다";
+ }
+
+ // 이름 검사
+ if (formData.userName.trim().length < 2) {
+ errors.userName = "이름은 최소 2자 이상이어야 합니다";
+ }
+
+ // 면허번호 검사
+ if (!validateLicenseNumber(formData.licenseNumber)) {
+ errors.licenseNumber = "올바른 면허번호 형식이 아닙니다 (예: 12-34-567890-12)";
+ }
+
+ // 차량 번호 검사
+ if (!validateVehicleNumber(formData.vehicleNumber)) {
+ errors.vehicleNumber = "올바른 차량 번호 형식이 아닙니다 (예: 12가3456, 123가4567)";
+ }
+
+ // 휴대폰 번호 검사
+ if (!validatePhoneNumber(formData.phoneNumber)) {
+ errors.phoneNumber = "올바른 휴대폰 번호 형식이 아닙니다 (예: 010-1234-5678)";
+ }
+
+ setValidationErrors(errors);
+ return Object.keys(errors).length === 0;
+ };
+
+ /**
+ * 회원가입 핸들러
+ */
+ const handleRegister = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ // 유효성 검사
+ if (!validateForm()) {
+ return;
+ }
+
+ setIsLoading(true);
+ setError("");
+
+ try {
+ const response = await authApi.register({
+ userId: formData.userId,
+ password: formData.password,
+ userName: formData.userName,
+ licenseNumber: formData.licenseNumber,
+ vehicleNumber: formData.vehicleNumber.replace(/\s/g, ""), // 공백 제거
+ phoneNumber: formData.phoneNumber,
+ });
+
+ if (response.success) {
+ // 회원가입 성공 - toast 알림 표시
+ toast({
+ title: "회원가입 완료",
+ description: "회원가입이 성공적으로 완료되었습니다. 로그인 페이지로 이동합니다.",
+ });
+
+ // 로그인 페이지로 이동
+ setTimeout(() => {
+ router.push("/login");
+ }, 1500);
+ } else {
+ setError(response.message || "회원가입에 실패했습니다");
+ }
+ } catch (err: any) {
+ console.error("회원가입 오류:", err);
+ setError(err.message || "회원가입 중 오류가 발생했습니다");
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return {
+ formData,
+ isLoading,
+ error,
+ validationErrors,
+ showPassword,
+ showPasswordConfirm,
+ isFormValid,
+ handleInputChange,
+ handleRegister,
+ togglePasswordVisibility,
+ togglePasswordConfirmVisibility,
+ };
+}
+
diff --git a/frontend/lib/api/auth.ts b/frontend/lib/api/auth.ts
new file mode 100644
index 00000000..119514bc
--- /dev/null
+++ b/frontend/lib/api/auth.ts
@@ -0,0 +1,28 @@
+import { RegisterFormData, RegisterResponse } from "@/types/auth";
+
+/**
+ * 인증 관련 API (임시 mock)
+ */
+export const authApi = {
+ /**
+ * 회원가입 (임시 구현)
+ */
+ async register(data: Omit): Promise {
+ // TODO: 백엔드 API 연동 필요
+ console.log("회원가입 요청:", data);
+
+ // 임시로 성공 응답 반환
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve({
+ success: true,
+ message: "회원가입이 완료되었습니다",
+ data: {
+ userId: data.userId,
+ },
+ });
+ }, 1000);
+ });
+ },
+};
+
diff --git a/frontend/types/auth.ts b/frontend/types/auth.ts
index cd8e65b6..bc7f4af1 100644
--- a/frontend/types/auth.ts
+++ b/frontend/types/auth.ts
@@ -22,3 +22,22 @@ export interface AuthStatus {
isLoggedIn: boolean;
isAdmin?: boolean;
}
+
+export interface RegisterFormData {
+ userId: string;
+ password: string;
+ passwordConfirm: string;
+ userName: string;
+ licenseNumber: string;
+ vehicleNumber: string;
+ phoneNumber: string;
+}
+
+export interface RegisterResponse {
+ success: boolean;
+ message?: string;
+ data?: {
+ userId: string;
+ };
+ errorCode?: string;
+}