diff --git a/package-lock.json b/package-lock.json index 8296699..d8a1a63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "framer-motion": "^11.18.1", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-icons": "^5.4.0", "react-router-dom": "^7.1.2", "react-toastify": "^11.0.3" }, @@ -3665,6 +3666,15 @@ "react": "^18.3.1" } }, + "node_modules/react-icons": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.4.0.tgz", + "integrity": "sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-router": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.1.2.tgz", diff --git a/package.json b/package.json index bc76a59..42bbf46 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "framer-motion": "^11.18.1", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-icons": "^5.4.0", "react-router-dom": "^7.1.2", "react-toastify": "^11.0.3" }, diff --git a/src/App.tsx b/src/App.tsx index 2a631c0..92503ba 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,6 +4,8 @@ import LoginPage from "./pages/LoginPage"; import RegisterPage from "./pages/RegisterPage"; import "react-toastify/dist/ReactToastify.css"; import { ToastContainer } from "react-toastify"; +import MobileLoginPage from "./pages/mobile/MobileLoginPage"; +import MobileRegisterPage from "./pages/mobile/MobileRegisterPage"; const App = () => { return ( @@ -18,6 +20,8 @@ const App = () => { } /> } /> + } /> + } /> } /> diff --git a/src/components/auth/RegisterForm.tsx b/src/components/auth/RegisterForm.tsx index fc1f8ac..e7f05c9 100644 --- a/src/components/auth/RegisterForm.tsx +++ b/src/components/auth/RegisterForm.tsx @@ -1,7 +1,8 @@ import React, { useState, useRef } from "react"; import VirtualKeypad from "./VirtualKeypad"; -import { ToastContainer, toast } from "react-toastify"; // Import ToastContainer and toast -import "react-toastify/dist/ReactToastify.css"; // Import styles for toast notifications +import { ToastContainer, toast } from "react-toastify"; +import "react-toastify/dist/ReactToastify.css"; +import { Link } from "react-router-dom"; interface RegisterFormProps { formData: { @@ -17,6 +18,7 @@ interface RegisterFormProps { onInputChange: (e: React.ChangeEvent) => void; onPinChange: (name: string, value: string) => void; onRegister: () => void; + isMobile?: boolean; } const RegisterForm: React.FC = ({ @@ -24,15 +26,38 @@ const RegisterForm: React.FC = ({ onInputChange, onPinChange, onRegister, + isMobile = false, }) => { const [step, setStep] = useState(1); const [direction, setDirection] = useState<"next" | "prev">("next"); const containerRef = useRef(null); - const [isPinInput, setIsPinInput] = useState(false); const [currentPinField, setCurrentPinField] = useState<"pin" | "confirmPin">( "pin" ); + const mobileStyles = { + wrapper: "min-h-screen bg-white flex flex-col", // flexbox를 이용해 세로 정렬 + container: "flex flex-col p-4 pt-6", // 세로 방향으로 아이템 배치 + title: "text-3xl font-bold mb-6", + inputBase: + "w-full px-4 py-3 rounded-lg border border-gray-300 focus:border-blue-500 focus:outline-none", + button: "w-full py-4 rounded-lg font-medium", + pinContainer: "mt-8", + pinBox: "w-12 h-12 border-2 rounded-lg", + }; + + const desktopStyles = { + container: "relative flex-1 flex items-center justify-center p-6", + title: "text-4xl font-bold mb-8", + inputBase: + "w-full px-0 py-2 border-b border-gray-300 focus:border-blue-500 focus:outline-none", + button: "flex-1 py-3 rounded", + pinContainer: "space-y-4", + pinBox: "w-10 h-10 border-b-2", + }; + + const styles = isMobile ? mobileStyles : desktopStyles; + const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { if (step < 6) { @@ -130,24 +155,28 @@ const RegisterForm: React.FC = ({ ) => { if (!bypassValidate && !validateForm()) return; - setDirection(dir); - const container = containerRef.current; - if (container) { - container.classList.add("transition-transform", "duration-300"); - container.style.transform = - dir === "next" ? "translateX(-50%)" : "translateX(50%)"; - - setTimeout(() => { - setStep(nextStep); - container.classList.remove("transition-transform"); + if (isMobile) { + setStep(nextStep); + } else { + setDirection(dir); + const container = containerRef.current; + if (container) { + container.classList.add("transition-transform", "duration-300"); container.style.transform = - dir === "next" ? "translateX(50%)" : "translateX(-50%)"; + dir === "next" ? "translateX(-50%)" : "translateX(50%)"; + + setTimeout(() => { + setStep(nextStep); + container.classList.remove("transition-transform"); + container.style.transform = + dir === "next" ? "translateX(50%)" : "translateX(-50%)"; - requestAnimationFrame(() => { - container.classList.add("transition-transform"); - container.style.transform = "translateX(0)"; - }); - }, 300); + requestAnimationFrame(() => { + container.classList.add("transition-transform"); + container.style.transform = "translateX(0)"; + }); + }, 300); + } } }; @@ -302,18 +331,26 @@ const RegisterForm: React.FC = ({ return (
-

{currentStep.title}

+

+ {currentStep.title} +

{currentStep.subtitle}

{currentStep.input} -
+
{step > 1 && ( @@ -324,7 +361,7 @@ const RegisterForm: React.FC = ({ if (step === 6) onRegister(); else handleNavigation(step + 1, "next"); }} - className="flex-1 bg-blue-600 text-white py-3 rounded hover:bg-blue-700 transition-colors" + className={`${styles.button} bg-blue-600 text-white hover:bg-blue-700`} > {step === 6 ? "회원가입" : "다음"} @@ -334,29 +371,34 @@ const RegisterForm: React.FC = ({ }; return ( -
-
-

회원가입

+
+
+

회원가입

{renderStep()}
{step !== 5 && ( -
+
이미 계정이 있으신가요?{" "} - + 로그인 - +
)}
- {/* Add the ToastContainer for toast notifications */} +
); }; diff --git a/src/components/mobile/BottomNavigation.tsx b/src/components/mobile/BottomNavigation.tsx new file mode 100644 index 0000000..761f2e7 --- /dev/null +++ b/src/components/mobile/BottomNavigation.tsx @@ -0,0 +1,34 @@ +import React from "react"; +import { useLocation } from "react-router-dom"; +import { FaHome, FaKey, FaTag, FaUser } from "react-icons/fa"; + +const BottomNavigation = () => { + const location = useLocation(); + + const navItems = [ + { id: "home", label: "홈", icon: , path: "/app/home" }, + { id: "auth", label: "인증", icon: , path: "/app/auth" }, + { id: "token", label: "토큰", icon: , path: "/app/myinfo/token" }, + { id: "mypage", label: "마이페이지", icon: , path: "/app/mypage" }, + ]; + + return ( +
+
+ {navItems.map((item) => ( + + ))} +
+
+ ); +}; + +export default BottomNavigation; diff --git a/src/components/mobile/MobileContainer.tsx b/src/components/mobile/MobileContainer.tsx new file mode 100644 index 0000000..10ed6fe --- /dev/null +++ b/src/components/mobile/MobileContainer.tsx @@ -0,0 +1,17 @@ +import React from "react"; +import BottomNavigation from "./BottomNavigation"; + +interface MobileContainerProps { + children: React.ReactNode; +} + +const MobileContainer: React.FC = ({ children }) => { + return ( +
+
{children}
+ +
+ ); +}; + +export default MobileContainer \ No newline at end of file diff --git a/src/pages/mobile/MobileLoginPage.tsx b/src/pages/mobile/MobileLoginPage.tsx new file mode 100644 index 0000000..cbc3f0b --- /dev/null +++ b/src/pages/mobile/MobileLoginPage.tsx @@ -0,0 +1,81 @@ +import React from "react"; +import MobileContainer from "../../components/mobile/MobileContainer"; +import { Link } from "react-router-dom"; + +const MobileLoginPage = () => { + const [formData, setFormData] = React.useState({ + email: "", + password: "", + rememberMe: false, + }); + + const handleInputChange = (e: { + target: { name: any; value: any; type: any; checked: any }; + }) => { + const { name, value, type, checked } = e.target; + setFormData((prev) => ({ + ...prev, + [name]: type === "checkbox" ? checked : value, + })); + }; + + const handleLogin = (type: string) => { + console.log(`Logging in with ${type}`, formData); + }; + + return ( + +
+

로그인

+
+
+ + +
+ + 비밀번호를 잊으셨나요? + + +
+ +
+
+ +
+ 계정이 없으신가요?{" "} + + NANU ID 생성하기 + +
+
+
+ ); +}; + +export default MobileLoginPage; diff --git a/src/pages/mobile/MobileRegisterPage.tsx b/src/pages/mobile/MobileRegisterPage.tsx new file mode 100644 index 0000000..8cd6d0b --- /dev/null +++ b/src/pages/mobile/MobileRegisterPage.tsx @@ -0,0 +1,56 @@ +import React, { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import LoginContainer from "../../components/auth/LoginContainer"; +import LoginBanner from "../../components/auth/LoginBanner"; +import RegisterForm from "../../components/auth/RegisterForm"; +import { RegisterFormData } from "../../types/Auth"; +import MobileContainer from "../../components/mobile/MobileContainer"; + +const MobileRegisterPage = () => { + const [formData, setFormData] = useState({ + email: "", + password: "", + confirmPassword: "", + name: "", + birthDate: "", + pin: "", + confirmPin: "", + termsAccepted: false, + }); + const navigate = useNavigate(); + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value, type, checked } = e.target; + setFormData((prev) => ({ + ...prev, + [name]: type === "checkbox" ? checked : value, + })); + }; + + const handlePinChange = (name: string, value: string) => { + setFormData((prev) => ({ + ...prev, + [name]: value, + })); + }; + + const handleRegister = () => { + console.log("Register:", formData); + }; + + return ( + +
+ +
+
+ ); +}; + +export default MobileRegisterPage;