From 786d71a6974a0ac3a518b5c363bfb3b599f04be6 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Mon, 10 Nov 2025 17:12:47 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=B0=8F=20=ED=8F=BC?= =?UTF-8?q?=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 회원가입 페이지 및 폼 컴포넌트 추가 - 로그인 페이지와 일관된 디자인 - 아이디, 비밀번호, 이름, 차량번호, 휴대폰번호 입력 필드 - 비밀번호 확인 필드 추가 - 유효성 검사 기능 구현 - 차량번호: 한국 차량번호 형식 검증 (12가3456, 123가4567, 서울12가3456 등) - 휴대폰번호: 하이픈 포함 형식 검증 (010-1234-5678) - 비밀번호: 최소 6자 이상, 확인 일치 검증 - 사용자ID: 최소 4자 이상 - 이름: 최소 2자 이상 - UI/UX 개선 - 각 필드별 실시간 유효성 검사 및 에러 메시지 표시 - 비밀번호 표시/숨김 토글 버튼 - 자동 설정된 필드에 안내 문구 표시 - 로그인 페이지로 돌아가기 버튼 추가 - 로그인 페이지에 회원가입 링크 추가 - 타입 및 훅 추가 - RegisterFormData, RegisterResponse 타입 정의 - useRegister 훅으로 비즈니스 로직 분리 - auth API mock 함수 (백엔드 연동 준비) - 사용자 경험 고려 - 입력 필드별 placeholder 예시 제공 - 도움말 텍스트로 입력 형식 안내 - 로딩 상태 표시 --- frontend/app/(auth)/register/page.tsx | 51 +++++ frontend/components/auth/LoginFooter.tsx | 20 +- frontend/components/auth/RegisterForm.tsx | 259 +++++++++++++++++++++ frontend/hooks/useRegister.ts | 262 ++++++++++++++++++++++ frontend/lib/api/auth.ts | 28 +++ frontend/types/auth.ts | 19 ++ 6 files changed, 638 insertions(+), 1 deletion(-) create mode 100644 frontend/app/(auth)/register/page.tsx create mode 100644 frontend/components/auth/RegisterForm.tsx create mode 100644 frontend/hooks/useRegister.ts create mode 100644 frontend/lib/api/auth.ts 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 ( + + + 회원가입 + 새로운 계정을 생성합니다 + + + + +
+ {/* 사용자 ID */} +
+ + + {validationErrors.userId && ( +

{validationErrors.userId}

+ )} +
+ + {/* 비밀번호 */} +
+ +
+ + +
+ {validationErrors.password && ( +

{validationErrors.password}

+ )} +
+ + {/* 비밀번호 확인 */} +
+ +
+ + +
+ {validationErrors.passwordConfirm && ( +

{validationErrors.passwordConfirm}

+ )} +
+ + {/* 이름 */} +
+ + + {validationErrors.userName && ( +

{validationErrors.userName}

+ )} +
+ + {/* 면허번호 */} +
+ + + {validationErrors.licenseNumber && ( +

{validationErrors.licenseNumber}

+ )} +

+ 운전면허번호를 하이픈(-)을 포함하여 입력해주세요 +

+
+ + {/* 차량 번호 */} +
+ + + {validationErrors.vehicleNumber && ( +

{validationErrors.vehicleNumber}

+ )} +

+ 한국 차량 번호 형식으로 입력해주세요 +

+
+ + {/* 휴대폰 번호 */} +
+ + + {validationErrors.phoneNumber && ( +

{validationErrors.phoneNumber}

+ )} +

+ 하이픈(-)을 포함하여 입력해주세요 +

+
+ + {/* 버튼 그룹 */} +
+ + + + +
+
+
+
+ ); +} + 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; +} -- 2.43.0 From c18cd26ab4335b6cbcb549040246bbc9ec54f7b5 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Tue, 11 Nov 2025 17:35:24 +0900 Subject: [PATCH 2/3] =?UTF-8?q?=EB=AA=A8=EB=8B=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/components/admin/SqlQueryModal.tsx | 77 +++++++++++-------- .../admin/dashboard/DashboardDesigner.tsx | 32 +++++--- 2 files changed, 63 insertions(+), 46 deletions(-) diff --git a/frontend/components/admin/SqlQueryModal.tsx b/frontend/components/admin/SqlQueryModal.tsx index a578afd5..4c01f472 100644 --- a/frontend/components/admin/SqlQueryModal.tsx +++ b/frontend/components/admin/SqlQueryModal.tsx @@ -2,7 +2,13 @@ import { useState, useEffect, ChangeEvent } from "react"; import { Button } from "@/components/ui/button"; -import { ResizableDialog, ResizableDialogContent, ResizableDialogHeader, DialogDescription } from "@/components/ui/resizable-dialog"; +import { + ResizableDialog, + ResizableDialogContent, + ResizableDialogHeader, + ResizableDialogTitle, + ResizableDialogDescription, +} from "@/components/ui/resizable-dialog"; import { Textarea } from "@/components/ui/textarea"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; @@ -119,21 +125,20 @@ export const SqlQueryModal: React.FC = ({ isOpen, onClose, c // SELECT 쿼리만 허용하는 검증 const trimmedQuery = query.trim().toUpperCase(); - if (!trimmedQuery.startsWith('SELECT')) { + if (!trimmedQuery.startsWith("SELECT")) { toast({ title: "보안 오류", - description: "외부 데이터베이스에서는 SELECT 쿼리만 실행할 수 있습니다. INSERT, UPDATE, DELETE는 허용되지 않습니다.", + description: + "외부 데이터베이스에서는 SELECT 쿼리만 실행할 수 있습니다. INSERT, UPDATE, DELETE는 허용되지 않습니다.", variant: "destructive", }); return; } // 위험한 키워드 검사 - const dangerousKeywords = ['INSERT', 'UPDATE', 'DELETE', 'DROP', 'CREATE', 'ALTER', 'TRUNCATE', 'EXEC', 'EXECUTE']; - const hasDangerousKeyword = dangerousKeywords.some(keyword => - trimmedQuery.includes(keyword) - ); - + const dangerousKeywords = ["INSERT", "UPDATE", "DELETE", "DROP", "CREATE", "ALTER", "TRUNCATE", "EXEC", "EXECUTE"]; + const hasDangerousKeyword = dangerousKeywords.some((keyword) => trimmedQuery.includes(keyword)); + if (hasDangerousKeyword) { toast({ title: "보안 오류", @@ -161,13 +166,13 @@ export const SqlQueryModal: React.FC = ({ isOpen, onClose, c variant: "destructive", }); } - } catch (error) { - console.error("쿼리 실행 오류:", error); - toast({ - title: "오류", - description: error instanceof Error ? error.message : "쿼리 실행 중 오류가 발생했습니다.", - variant: "destructive", - }); + } catch (error) { + console.error("쿼리 실행 오류:", error); + toast({ + title: "오류", + description: error instanceof Error ? error.message : "쿼리 실행 중 오류가 발생했습니다.", + variant: "destructive", + }); } finally { setLoading(false); } @@ -182,7 +187,7 @@ export const SqlQueryModal: React.FC = ({ isOpen, onClose, c 데이터베이스에 대해 SQL SELECT 쿼리를 실행하고 결과를 확인할 수 있습니다. - + {/* 쿼리 입력 영역 */}
@@ -220,18 +225,18 @@ export const SqlQueryModal: React.FC = ({ isOpen, onClose, c
{/* 테이블 정보 */} -
+
-

사용 가능한 테이블

+

사용 가능한 테이블

{tables.map((table) => ( -
+
-

{table.table_name}

-
{table.description && ( -

{table.description}

+

{table.description}

)}
))} @@ -254,12 +259,12 @@ export const SqlQueryModal: React.FC = ({ isOpen, onClose, c {/* 선택된 테이블의 컬럼 정보 */} {selectedTable && (
-

테이블 컬럼 정보: {selectedTable}

+

테이블 컬럼 정보: {selectedTable}

{loadingColumns ? ( -
컬럼 정보 로딩 중...
+
컬럼 정보 로딩 중...
) : selectedTableColumns.length > 0 ? (
-
+
@@ -275,7 +280,7 @@ export const SqlQueryModal: React.FC = ({ isOpen, onClose, c {column.column_name} {column.data_type} {column.is_nullable} - {column.column_default || '-'} + {column.column_default || "-"} ))} @@ -283,7 +288,7 @@ export const SqlQueryModal: React.FC = ({ isOpen, onClose, c ) : ( -
컬럼 정보를 불러올 수 없습니다.
+
컬럼 정보를 불러올 수 없습니다.
)} )} @@ -316,20 +321,24 @@ export const SqlQueryModal: React.FC = ({ isOpen, onClose, c {/* 결과 섹션 */}
-
- {loading ? "쿼리 실행 중..." : results.length > 0 ? `${results.length}개의 결과가 있습니다.` : "실행된 쿼리가 없습니다."} +
+ {loading + ? "쿼리 실행 중..." + : results.length > 0 + ? `${results.length}개의 결과가 있습니다.` + : "실행된 쿼리가 없습니다."}
- + {/* 결과 그리드 */} -
+
{results.length > 0 ? ( <> - + {Object.keys(results[0]).map((key) => ( diff --git a/frontend/components/admin/dashboard/DashboardDesigner.tsx b/frontend/components/admin/dashboard/DashboardDesigner.tsx index a9f86027..5560f1cb 100644 --- a/frontend/components/admin/dashboard/DashboardDesigner.tsx +++ b/frontend/components/admin/dashboard/DashboardDesigner.tsx @@ -12,15 +12,21 @@ import { Resolution, RESOLUTIONS, detectScreenResolution } from "./ResolutionSel import { DashboardProvider } from "@/contexts/DashboardContext"; import { useMenu } from "@/contexts/MenuContext"; import { useKeyboardShortcuts } from "./hooks/useKeyboardShortcuts"; -import { ResizableDialog, ResizableDialogContent, ResizableDialogDescription, ResizableDialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { + ResizableDialog, + ResizableDialogContent, + ResizableDialogDescription, + ResizableDialogHeader, + ResizableDialogTitle, +} from "@/components/ui/resizable-dialog"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, - AlertResizableDialogContent, - AlertResizableDialogDescription, + AlertDialogContent, + AlertDialogDescription, AlertDialogFooter, - AlertResizableDialogHeader, + AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; @@ -610,21 +616,23 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D /> {/* 저장 성공 모달 */} - { setSuccessModalOpen(false); router.push("/admin/dashboard"); }} > - - + +
- 저장 완료 - 대시보드가 성공적으로 저장되었습니다. -
+ 저장 완료 + + 대시보드가 성공적으로 저장되었습니다. + +
-
-
+ + {/* 초기화 확인 모달 */} -- 2.43.0 From aeef1dc215af85523a53f7fa8de02a149bed3974 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Tue, 11 Nov 2025 17:42:22 +0900 Subject: [PATCH 3/3] =?UTF-8?q?=ED=9A=8C=EC=9B=90=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/app/(auth)/register/page.tsx | 51 ----- frontend/components/auth/LoginFooter.tsx | 20 +- frontend/components/auth/RegisterForm.tsx | 259 --------------------- frontend/hooks/useRegister.ts | 262 ---------------------- frontend/lib/api/auth.ts | 28 --- frontend/types/auth.ts | 19 -- 6 files changed, 1 insertion(+), 638 deletions(-) delete mode 100644 frontend/app/(auth)/register/page.tsx delete mode 100644 frontend/components/auth/RegisterForm.tsx delete mode 100644 frontend/hooks/useRegister.ts delete mode 100644 frontend/lib/api/auth.ts diff --git a/frontend/app/(auth)/register/page.tsx b/frontend/app/(auth)/register/page.tsx deleted file mode 100644 index bd098543..00000000 --- a/frontend/app/(auth)/register/page.tsx +++ /dev/null @@ -1,51 +0,0 @@ -"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 42edf9e2..99b5da16 100644 --- a/frontend/components/auth/LoginFooter.tsx +++ b/frontend/components/auth/LoginFooter.tsx @@ -1,29 +1,11 @@ import { UI_CONFIG } from "@/constants/auth"; -import Link from "next/link"; - -interface LoginFooterProps { - showRegisterLink?: boolean; -} /** * 로그인 페이지 푸터 컴포넌트 */ -export function LoginFooter({ showRegisterLink = true }: LoginFooterProps) { +export function LoginFooter() { return (
- {showRegisterLink && ( -
-

- 계정이 없으신가요?{" "} - - 회원가입 - -

-
- )}

{UI_CONFIG.COPYRIGHT}

{UI_CONFIG.POWERED_BY}

diff --git a/frontend/components/auth/RegisterForm.tsx b/frontend/components/auth/RegisterForm.tsx deleted file mode 100644 index 2fd26432..00000000 --- a/frontend/components/auth/RegisterForm.tsx +++ /dev/null @@ -1,259 +0,0 @@ -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 ( - - - 회원가입 - 새로운 계정을 생성합니다 - - - - -
- {/* 사용자 ID */} -
- - - {validationErrors.userId && ( -

{validationErrors.userId}

- )} -
- - {/* 비밀번호 */} -
- -
- - -
- {validationErrors.password && ( -

{validationErrors.password}

- )} -
- - {/* 비밀번호 확인 */} -
- -
- - -
- {validationErrors.passwordConfirm && ( -

{validationErrors.passwordConfirm}

- )} -
- - {/* 이름 */} -
- - - {validationErrors.userName && ( -

{validationErrors.userName}

- )} -
- - {/* 면허번호 */} -
- - - {validationErrors.licenseNumber && ( -

{validationErrors.licenseNumber}

- )} -

- 운전면허번호를 하이픈(-)을 포함하여 입력해주세요 -

-
- - {/* 차량 번호 */} -
- - - {validationErrors.vehicleNumber && ( -

{validationErrors.vehicleNumber}

- )} -

- 한국 차량 번호 형식으로 입력해주세요 -

-
- - {/* 휴대폰 번호 */} -
- - - {validationErrors.phoneNumber && ( -

{validationErrors.phoneNumber}

- )} -

- 하이픈(-)을 포함하여 입력해주세요 -

-
- - {/* 버튼 그룹 */} -
- - - - -
- -
-
- ); -} - diff --git a/frontend/hooks/useRegister.ts b/frontend/hooks/useRegister.ts deleted file mode 100644 index 09ec46ad..00000000 --- a/frontend/hooks/useRegister.ts +++ /dev/null @@ -1,262 +0,0 @@ -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 deleted file mode 100644 index 119514bc..00000000 --- a/frontend/lib/api/auth.ts +++ /dev/null @@ -1,28 +0,0 @@ -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 bc7f4af1..cd8e65b6 100644 --- a/frontend/types/auth.ts +++ b/frontend/types/auth.ts @@ -22,22 +22,3 @@ 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; -} -- 2.43.0