From 7206ec7ee259ec71b054c1342cd53ff7057e9cfe Mon Sep 17 00:00:00 2001 From: ryuyena0305 Date: Thu, 6 Nov 2025 20:00:56 +0900 Subject: [PATCH 1/2] =?UTF-8?q?refactor=20:=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EB=B0=8F=20=EB=AA=A8=EB=8B=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/BottomNavigation.jsx | 2 +- src/components/screens/LoginSignupScreen.jsx | 444 ++++++++++--------- src/components/screens/MyPageScreen.jsx | 213 +++++---- 3 files changed, 360 insertions(+), 299 deletions(-) diff --git a/src/components/common/BottomNavigation.jsx b/src/components/common/BottomNavigation.jsx index 1920418..e919b40 100644 --- a/src/components/common/BottomNavigation.jsx +++ b/src/components/common/BottomNavigation.jsx @@ -40,7 +40,7 @@ export function BottomNavigation({ active = 'home', onChange = () => {} }) { }} aria-current={isActive ? 'page' : undefined} aria-label={t.label} - className={`flex-1 flex flex-col items-center gap-1 py-2 focus:outline-none focus:ring-2 focus:ring-[#4CAF50] ${ + className={`flex-1 flex flex-col items-center gap-1 py-2 focus:outline-none focus:ring-2 focus:ring-[#FFFFFF] ${ isActive ? 'text-[#4CAF50]' : 'text-gray-400' }`} diff --git a/src/components/screens/LoginSignupScreen.jsx b/src/components/screens/LoginSignupScreen.jsx index cc7e84e..2c82639 100644 --- a/src/components/screens/LoginSignupScreen.jsx +++ b/src/components/screens/LoginSignupScreen.jsx @@ -1,6 +1,5 @@ import React, { useState, useEffect } from 'react'; import api from '../../api/axios'; -// ↓ redux 안 쓰면 이 부분 제거해도됨 import { useDispatch } from 'react-redux'; import { updateProfile } from '../../store/slices/userSlice'; import kakaoBtn from '../../assets/kakao_login_medium_wide.png'; @@ -9,18 +8,18 @@ import HomeScreen from './HomeScreen'; // 테마 컬러 const themeColor = '#96cb6f'; -// 검증 함수 +// 검증 함수 const validateEmail = (email) => /[^@\s]+@[^@\s]+\.[^@\s]+/.test(email); const validatePassword = (password) => password.length >= 6; -// 카카오 로그인 버튼 클릭 시 이동 +// 카카오 로그인 const kakaoLogin = () => { window.location.href = `${ import.meta.env.VITE_APP_SERVER_URL }/oauth2/authorization/kakao`; }; -// 전역 스타일 그대로 유지 +// 전역 스타일 const styles = ` :root { --brand: ${themeColor}; } *{ box-sizing: border-box; } @@ -34,31 +33,61 @@ const styles = ` .tab.active{ color:var(--brand); border-bottom-color:var(--brand); } .field{ margin:14px 0; } .label{ display:block; font-weight:600; color:#333; margin-bottom:6px; transition:color .2s ease; } - .label.filled{ } .input{ width:100%; padding:12px 14px; border-radius:12px; border:2px solid #e5e7eb; outline:none; transition:border-color .15s ease, box-shadow .15s ease, background .15s ease; } .input:focus{ border-color:var(--brand); box-shadow:0 0 0 4px rgba(133,193,75,.15); } .input.filled{ background:#f9fff2; border-color:#cfe8ae; } .input.valid{ border-color:var(--brand); } .input.invalid{ border-color:#e11d48; box-shadow:0 0 0 4px rgba(225,29,72,.10); } - .hint{ font-size:.85rem; color:#6b7280; margin-top:6px; } - .error{ font-size:.85rem; color:#e11d48; margin-top:6px; } + .hint{ font-size:.85rem; color:#96cb6f; margin-top:6px; } + .error{ font-size:.85rem; color:#e11d48; } .btn{ width:100%; padding:12px 14px; border-radius:12px; border:0; background:var(--brand); color:#fff; font-weight:800; cursor:pointer; margin-top:6px;} .btn:disabled{ opacity:.5; cursor:not-allowed; } .kakao{ width:100%; margin-top:12px; padding:12px 14px; border-radius:12px; border:0; background:#FEE500; color:#3C1E1E; font-weight:700; cursor:pointer; } - - .tab.active{ color:var(--brand); border-bottom-color:var(--brand); } -.tab:focus{ outline:none; box-shadow:none; } - `; +/* ------------------ 모달 ------------------ */ +function Modal({ message, type = 'info', onClose }) { + const handleClick = () => { + if (type === 'success') { + window.location.href = '/'; + } else { + onClose(); + } + }; + return ( +
+
+
+ {type === 'success' ? '🌳' : '🍂'} +
+

{message}

+ +
+
+ ); +} + +/* ------------------ 메인 컴포넌트 ------------------ */ export default function LoginSignupScreen() { const [page, setPage] = useState('login'); const [userInfo, setUserInfo] = useState(null); - const dispatch = useDispatch(); // redux없는 사람은 제거 가능 + const [modal, setModal] = useState(null); + const dispatch = useDispatch(); - // 로그인 유지 useEffect(() => { const token = localStorage.getItem('token'); if (!token) return; @@ -80,15 +109,15 @@ export default function LoginSignupScreen() { return ( <> -
+
-
-
GreenMap
-
그린맵
+
+
GreenMap
+
그린맵
{!userInfo && ( -
+
{['login', 'signup'].map((tab) => ( )}
+ + {modal && ( + setModal(null)} + /> + )} ); } /* ------------------ 로그인 ------------------ */ -function LoginForm({ setUserInfo }) { +function LoginForm({ setUserInfo, setModal }) { const [email, setEmail] = useState(''); const [tEmail, setTEmail] = useState(false); const [password, setPassword] = useState(''); @@ -150,11 +187,7 @@ function LoginForm({ setUserInfo }) { const submitLogin = async () => { try { - const res = await api.post('/member/login', { - email, - password, - }); - + const res = await api.post('/member/login', { email, password }); localStorage.setItem('token', res.data.data.accessToken); const info = await api.get('/member/me', { @@ -164,41 +197,39 @@ function LoginForm({ setUserInfo }) { }); setUserInfo(info.data.data); - alert('로그인 성공'); - - // 로그인 성공 시 메인으로 이동 - window.location.href = '/'; + setModal({ message: '로그인 성공!', type: 'success' }); } catch (e) { - alert('로그인 실패'); + setModal({ + message: '로그인 실패. 이메일 또는 비밀번호를 확인해주세요.', + type: 'error', + }); } }; return (
e.preventDefault()}> setTEmail(true)} isValid={emailValid} touched={tEmail} /> - setTPw(true)} isValid={pwValid} touched={tPw} /> - - - ); +function SignupForm({ setPage, setModal }) { + const [email, setEmail] = useState(''); + const [emailAvailable, setEmailAvailable] = useState(null); + + const [password, setPassword] = useState(''); + const [confirm, setConfirm] = useState(''); + + const [nickname, setNickname] = useState(''); + const [nickAvailable, setNickAvailable] = useState(null); + + const emailValid = validateEmail(email); + const pwValid = validatePassword(password); + const confirmValid = confirm === password && confirm.length > 0; + const nicknameValid = nickname.length >= 2; + + + const formValid = + emailValid && + pwValid && + confirmValid && + nicknameValid && + emailAvailable === false && + nickAvailable === false; + + // 이메일 중복 검사 + useEffect(() => { + if (!emailValid) { + setEmailAvailable(null); + return; + } + const timer = setTimeout(async () => { + try { + const res = await api.get('/member/check-email', { + params: { email }, + }); + + const state = res.data.data.state; // true: 존재, false: 사용 가능 + console.log(state); + setEmailAvailable(!state); + } catch { + setEmailAvailable(true); + } + }, 400); + return () => clearTimeout(timer); + }, [email]); + + // 닉네임 중복 검사 + useEffect(() => { + if (!nicknameValid) { + setNickAvailable(null); + return; + } + const timer = setTimeout(async () => { + try { + const res = await api.get('/member/check-nickname', { + params: { nickname }, + }); + const state = res.data.data.state; // true: 존재 + setNickAvailable(!state); + } catch { + setNickAvailable(true); + } + }, 400); + return () => clearTimeout(timer); + }, [nickname]); + + const submitSignup = async () => { + try { + await api.post('/member', { email, password, nickname }); + setModal({ message: '회원가입 완료! 로그인 해주세요.', type: 'success' }); + setTimeout(() => setPage('login'), 1200); + } catch { + setModal({ message: '회원가입 실패. 다시 시도해주세요.', type: 'error' }); + } + }; + + return ( +
e.preventDefault()}> + {/* 이메일 */} + 0} + /> + {email.length > 0 && !emailValid && ( +
이메일 형식이 아닙니다.
+ )} + {emailValid && emailAvailable === true && ( +
사용 가능한 이메일입니다.
+ )} + {emailValid && emailAvailable === false && ( +
이미 등록된 이메일입니다.
+ )} + + {/* 비밀번호 */} + 0} + /> + {password.length > 0 && !pwValid && ( +
비밀번호는 6자 이상 입력해주세요.
+ )} + + {/* 비밀번호 확인 */} + 0} + /> + {confirm.length > 0 && !confirmValid && ( +
입력하신 비밀번호와 일치하지 않습니다.
+ )} + + {/* 닉네임 */} + 0} + /> + {nickname.length > 0 && !nicknameValid && ( +
닉네임은 2자 이상 입력해주세요.
+ )} + {nicknameValid && nickAvailable === true && ( +
사용 가능한 닉네임입니다.
+ )} + {nicknameValid && nickAvailable === false && ( +
이미 등록된 닉네임입니다.
+ )} + + + + ); } -/* ------------------ 재사용 input ------------------ */ -function InputField({ - label, - type, - value, - onChange, - onBlur, - isValid, - touched, -}) { + +/* ------------------ 공용 Input ------------------ */ +function InputField({ label, type, value, onChange, onBlur, isValid, touched }) { const filled = value?.length > 0; const showInvalid = touched && !isValid && filled; - const inputClass = 'input ' + (filled ? 'filled ' : '') + @@ -380,7 +413,7 @@ function InputField({ (showInvalid ? 'invalid' : ''); return ( -
+
- {showInvalid ? ( -
입력값을 확인해주세요.
- ) : ( -
- {type === 'password' ?

:

} -
- )}
); } diff --git a/src/components/screens/MyPageScreen.jsx b/src/components/screens/MyPageScreen.jsx index 65c0956..d1288bc 100644 --- a/src/components/screens/MyPageScreen.jsx +++ b/src/components/screens/MyPageScreen.jsx @@ -1,10 +1,40 @@ -import React, { useEffect, useMemo } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { setActiveTab } from '../../store/slices/appSlice'; -import { fetchMyPageData } from '../../store/slices/userSlice'; -import { logout } from '../../store/slices/userSlice'; +import { fetchMyPageData, logout } from '../../store/slices/userSlice'; import { calculateEarnedBadges } from '../../store/slices/badgeSlice'; +const themeColor = '#96cb6f'; + +/* 로그아웃 모달 컴포넌트 */ +function LogoutModal({ onConfirm, onClose }) { + return ( +
+
+
🌳
+

+ 로그아웃 하시겠습니까? +

+
+ + +
+
+
+ ); +} + export default function MyPageScreen({ onNavigate }) { const dispatch = useDispatch(); const { isLoggedIn, profile, stats, ranking, loading, error } = useSelector( @@ -12,9 +42,10 @@ export default function MyPageScreen({ onNavigate }) { ); const { allBadges, earnedIds } = useSelector((state) => state.badge); - const [showSetting, setShowSetting] = React.useState(true); + const [showSetting, setShowSetting] = useState(false); + const [showLogoutModal, setShowLogoutModal] = useState(false); // 로그아웃 모달 상태 - // 현재 획득한 최고 레벨 뱃지 찾기 + // 현재 획득한 최고 레벨 뱃지 찾기 const myBadge = useMemo(() => { const earnedBadges = allBadges.filter((badge) => earnedIds.includes(badge.id) @@ -24,11 +55,9 @@ export default function MyPageScreen({ onNavigate }) { return allBadges[0] || { name: '첫 발자국' }; } - return earnedBadges.reduce((highest, current) => { - return current.requiredPoint > highest.requiredPoint - ? current - : highest; - }, earnedBadges[0]); + return earnedBadges.reduce((highest, current) => + current.requiredPoint > highest.requiredPoint ? current : highest + ); }, [allBadges, earnedIds]); useEffect(() => { @@ -46,19 +75,24 @@ export default function MyPageScreen({ onNavigate }) { dispatch(setActiveTab(tab)); }; + // 로그아웃 모달 열기 const handleLogout = () => { - if (window.confirm('로그아웃 하시겠습니까?')) { - dispatch(logout()); - navigate('home'); - } + setShowLogoutModal(true); + }; + + // 로그아웃 실행 + const confirmLogout = () => { + dispatch(logout()); + navigate('home'); + setShowLogoutModal(false); }; if (loading) { return ( -
-
-
-

정보를 불러오는 중...

+
+
+
+

정보를 불러오는 중...

); @@ -66,21 +100,21 @@ export default function MyPageScreen({ onNavigate }) { if (!isLoggedIn) { return ( -
-
-
🔒
-

+
+
+
🔒
+

로그인이 필요해요

-

+

마이페이지를 확인하려면 로그인해주세요

{error && ( -

{error}

+

{error}

)} @@ -89,175 +123,176 @@ export default function MyPageScreen({ onNavigate }) { ); } - // 로그인 됨 - 마이페이지 표시 return ( -
-
-
-

- 마이페이지 -

-
+
+ {/* 상단 영역 */} +
+
+

마이페이지

+
{showSetting && ( -
+
)}
-
-
-
+ {/* 프로필 영역 */} +
+
+
{profile.avatar ? ( 프로필 ) : ( - 👤 + 👤 )}
-
-

+
+

{profile.nickname || profile.name || '사용자'}

-

+

{profile.email || '이메일 없음'}

-
+
- {/* 통계 그리드 */} -
+ {/* 통계 */} +
+ {/* 메뉴 카드 */} -
-
-

- 메뉴 -

-
    +
    +
    +

    메뉴

    +
    + {/* 로그아웃 버튼 */}
    - {/* 버전 정보 */} -
    + {/* 하단 버전 */} +
    그린맵 v1.0.0
    + + {/* 로그아웃 모달 */} + {showLogoutModal && ( + setShowLogoutModal(false)} + /> + )}
    ); } From 54e4668fb4aff10175fd6327a2014033e40af478 Mon Sep 17 00:00:00 2001 From: ryuyena0305 Date: Fri, 7 Nov 2025 10:35:28 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=ED=99=94=EB=A9=B4=EC=97=90=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/screens/EditProfileScreen.jsx | 99 ++- src/components/screens/LoginSignupScreen.jsx | 735 +++++++++---------- 2 files changed, 441 insertions(+), 393 deletions(-) diff --git a/src/components/screens/EditProfileScreen.jsx b/src/components/screens/EditProfileScreen.jsx index 3515f20..078f81d 100644 --- a/src/components/screens/EditProfileScreen.jsx +++ b/src/components/screens/EditProfileScreen.jsx @@ -14,7 +14,6 @@ const styles = ` .subtitle{ color:#6b7280; margin-bottom:14px; } .field{ margin:14px 0; text-align:left; } .label{ display:block; font-weight:600; color:#333; margin-bottom:6px; transition:color .2s ease; } - .label.filled{ color:var(--brand); } .input{ width:100%; padding:12px 14px; border-radius:12px; border:2px solid #e5e7eb; outline:none; transition:border-color .15s ease, box-shadow .15s ease, background .15s ease; } .input:focus{ border-color:var(--brand); box-shadow:0 0 0 4px rgba(133,193,75,.15); } @@ -34,14 +33,43 @@ const styles = ` } `; +function Modal({ message, type = 'info', onClose }) { + const handleClick = () => onClose(); + + return ( +
    +
    +
    + {type === 'success' ? '🌳' : '🍂'} +
    +

    {message}

    + +
    +
    + ); +} + export default function EditProfileScreen({ onBack }) { - const navigate = useNavigate(); + const navigate = useNavigate(); const [nickname, setNickname] = useState(""); + const [originNickname, setOriginNickname] = useState(""); const [email, setEmail] = useState(""); const [avatar, setAvatar] = useState(null); const [nickAvailable, setNickAvailable] = useState(null); const [loading, setLoading] = useState(false); - + const [modal, setModal] = useState(null); const token = localStorage.getItem("token"); // 1. 기존 회원정보 불러오기 @@ -52,11 +80,12 @@ export default function EditProfileScreen({ onBack }) { headers: { Authorization: `Bearer ${token}` }, }); const data = res.data.data; + setOriginNickname(data.nickname); setNickname(data.nickname); setEmail(data.email); - setAvatar(data.image.imageUrl || data.avatarUrl || null); // 이미지 필드명 대응 + setAvatar(data.image?.imageUrl || data.avatarUrl || null); } catch { - alert("로그인이 필요합니다."); + setModal({ message: "로그인이 필요합니다 🍂", type: "error" }); } }; fetchMyInfo(); @@ -69,6 +98,11 @@ export default function EditProfileScreen({ onBack }) { return; } + if (nickname === originNickname) { + setNickAvailable(null); + return; + } + const timer = setTimeout(async () => { try { const res = await api.get("/member/check-nickname", { @@ -86,8 +120,12 @@ export default function EditProfileScreen({ onBack }) { // 3. 닉네임 변경 const handleSubmit = async () => { - if (!nickname || nickname.length < 2) return alert("닉네임을 입력해주세요."); - if (nickAvailable === false) return alert("이미 사용 중인 닉네임입니다."); + if (!nickname || nickname.length < 2) + return setModal({ message: "닉네임을 입력해주세요", type: "error" }); + if (nickname === originNickname) + return setModal({ message: "현재 닉네임과 동일합니다", type: "error" }); + if (nickAvailable === false) + return setModal({ message: "이미 사용 중인 닉네임입니다", type: "error" }); try { setLoading(true); @@ -96,11 +134,13 @@ export default function EditProfileScreen({ onBack }) { { nickname }, { headers: { Authorization: `Bearer ${token}` } } ); - alert("닉네임이 변경되었습니다."); - navigate('/mypage'); - onBack?.(); + setModal({ message: "회원정보 수정이 완료되었습니다 ", type: "success" }); + setTimeout(() => { + navigate("/mypage"); + onBack?.(); + }, 1000); } catch { - alert("수정 실패. 다시 시도해주세요."); + setModal({ message: "다시 시도해주세요", type: "error" }); } finally { setLoading(false); } @@ -116,7 +156,7 @@ export default function EditProfileScreen({ onBack }) {

    프로필 수정

    이메일은 변경할 수 없습니다.

    - {/* 프로필 이미지 (수정 불가, 표시만) */} + {/* 프로필 이미지 */} {avatar ? ( 프로필 ) : ( @@ -157,23 +197,25 @@ export default function EditProfileScreen({ onBack }) { />
- {nicknameValid && nickAvailable === true && ( - 사용 가능한 닉네임입니다 - )} - {nicknameValid && nickAvailable === false && ( - 이미 존재하는 닉네임입니다 - )} - - {/* 안내 */} -
- 프로필 사진 및 비밀번호 변경은 추후 지원 예정입니다. -
+ {/* 상태 메시지 */} + {nicknameValid && nickname === originNickname ? ( + 현재 닉네임입니다 + ) : nicknameValid && nickAvailable === true ? ( + 사용 가능한 닉네임입니다 + ) : nicknameValid && nickAvailable === false ? ( + 이미 존재하는 닉네임입니다 + ) : null} {/* 저장 */}
+ + {/* ✅ 모달 표시 */} + {modal && ( + setModal(null)} + /> + )}
); } diff --git a/src/components/screens/LoginSignupScreen.jsx b/src/components/screens/LoginSignupScreen.jsx index 2c82639..43c819d 100644 --- a/src/components/screens/LoginSignupScreen.jsx +++ b/src/components/screens/LoginSignupScreen.jsx @@ -1,5 +1,6 @@ import React, { useState, useEffect } from 'react'; import api from '../../api/axios'; +// ↓ redux 안 쓰면 이 부분 제거해도됨 import { useDispatch } from 'react-redux'; import { updateProfile } from '../../store/slices/userSlice'; import kakaoBtn from '../../assets/kakao_login_medium_wide.png'; @@ -8,18 +9,18 @@ import HomeScreen from './HomeScreen'; // 테마 컬러 const themeColor = '#96cb6f'; -// 검증 함수 +// 검증 함수 const validateEmail = (email) => /[^@\s]+@[^@\s]+\.[^@\s]+/.test(email); const validatePassword = (password) => password.length >= 6; -// 카카오 로그인 +// 카카오 로그인 버튼 클릭 시 이동 const kakaoLogin = () => { - window.location.href = `${ - import.meta.env.VITE_APP_SERVER_URL - }/oauth2/authorization/kakao`; + window.location.href = `${ + import.meta.env.VITE_APP_SERVER_URL + }/oauth2/authorization/kakao`; }; -// 전역 스타일 +// 전역 스타일 그대로 유지 const styles = ` :root { --brand: ${themeColor}; } *{ box-sizing: border-box; } @@ -31,6 +32,7 @@ const styles = ` .tabs{ display:flex; gap:8px; border-bottom:1px solid #eaeaea; margin-bottom:18px; } .tab{ flex:1; padding:12px 8px; text-align:center; font-weight:700; border:0; background:transparent; cursor:pointer; border-bottom:3px solid transparent; transition:all .2s ease; } .tab.active{ color:var(--brand); border-bottom-color:var(--brand); } + .button.focus{ outline : none ,box-shadow:none } .field{ margin:14px 0; } .label{ display:block; font-weight:600; color:#333; margin-bottom:6px; transition:color .2s ease; } .input{ width:100%; padding:12px 14px; border-radius:12px; border:2px solid #e5e7eb; outline:none; @@ -39,390 +41,385 @@ const styles = ` .input.filled{ background:#f9fff2; border-color:#cfe8ae; } .input.valid{ border-color:var(--brand); } .input.invalid{ border-color:#e11d48; box-shadow:0 0 0 4px rgba(225,29,72,.10); } - .hint{ font-size:.85rem; color:#96cb6f; margin-top:6px; } - .error{ font-size:.85rem; color:#e11d48; } + .hint{ font-size:.85rem; color:#6b7280; margin-top:6px; } + .error{ font-size:.85rem; color:#e11d48; margin-top:6px; } .btn{ width:100%; padding:12px 14px; border-radius:12px; border:0; background:var(--brand); color:#fff; font-weight:800; cursor:pointer; margin-top:6px;} .btn:disabled{ opacity:.5; cursor:not-allowed; } .kakao{ width:100%; margin-top:12px; padding:12px 14px; border-radius:12px; border:0; background:#FEE500; color:#3C1E1E; font-weight:700; cursor:pointer; } + .valid-text { + display: block; + margin-top: 6px; + margin-left: 4px; + font-size: 0.88rem; + color: #3fa14a; + transition: color 0.2s ease; + } + .invalid-text { + display: block; + margin-top: 6px; + margin-left: 4px; + font-size: 0.88rem; + color: #d33b3b; + transition: color 0.2s ease; + } `; -/* ------------------ 모달 ------------------ */ + +/* ✅ 모달 컴포넌트 */ function Modal({ message, type = 'info', onClose }) { - const handleClick = () => { - if (type === 'success') { - window.location.href = '/'; - } else { - onClose(); - } - }; - - return ( -
-
-
- {type === 'success' ? '🌳' : '🍂'} -
-

{message}

- -
+ const handleClick = () => { + if (type === 'success') { + onClose(); + } else { + onClose(); + } + }; + + return ( +
+
+
+ {type === 'success' ? '🌳' : '🍂'}
- ); +

{message}

+ +
+
+ ); } -/* ------------------ 메인 컴포넌트 ------------------ */ +/* ------------------ 로그인 / 회원가입 통합 ------------------ */ export default function LoginSignupScreen() { - const [page, setPage] = useState('login'); - const [userInfo, setUserInfo] = useState(null); - const [modal, setModal] = useState(null); - const dispatch = useDispatch(); - - useEffect(() => { - const token = localStorage.getItem('token'); - if (!token) return; - - api.get('/member/me', { - headers: { Authorization: `Bearer ${token}` }, - }) - .then((res) => { - setUserInfo(res.data.data); - dispatch( - updateProfile?.({ - name: res.data.data.nickname, - email: res.data.data.email, - }) - ); - }) - .catch(() => localStorage.removeItem('token')); - }, [dispatch]); - - return ( - <> -
- - -
-
GreenMap
-
그린맵
- - {!userInfo && ( -
- {['login', 'signup'].map((tab) => ( - - ))} -
- )} - - {!userInfo ? ( - page === 'login' ? ( - - ) : ( - - ) - ) : ( - - )} - - {!userInfo && ( - - )} -
+ const [page, setPage] = useState('login'); + const [userInfo, setUserInfo] = useState(null); + const [modal, setModal] = useState(null); + const dispatch = useDispatch(); // redux없는 사람은 제거 가능 + + // 로그인 유지 + useEffect(() => { + const token = localStorage.getItem('token'); + if (!token) return; + + api + .get('/member/me', { + headers: { Authorization: `Bearer ${token}` }, + }) + .then((res) => { + setUserInfo(res.data.data); + dispatch( + updateProfile?.({ + name: res.data.data.nickname, + email: res.data.data.email, + }) + ); + }) + .catch(() => localStorage.removeItem('token')); + }, [dispatch]); + + return ( + <> +
+ + +
+
GreenMap
+
그린맵
+ + {!userInfo && ( +
+ {['login', 'signup'].map((tab) => ( + + ))}
+ )} + + {!userInfo ? ( + page === 'login' ? ( + + ) : ( + + ) + ) : ( + + )} + + {!userInfo && ( + + )} +
- {modal && ( - setModal(null)} - /> - )} - - ); + {/* ✅ 모달 표시 */} + {modal && ( + setModal(null)} + /> + )} +
+ + ); } /* ------------------ 로그인 ------------------ */ function LoginForm({ setUserInfo, setModal }) { - const [email, setEmail] = useState(''); - const [tEmail, setTEmail] = useState(false); - const [password, setPassword] = useState(''); - const [tPw, setTPw] = useState(false); - - const emailValid = validateEmail(email); - const pwValid = validatePassword(password); - const formValid = emailValid && pwValid; - - const submitLogin = async () => { - try { - const res = await api.post('/member/login', { email, password }); - localStorage.setItem('token', res.data.data.accessToken); - - const info = await api.get('/member/me', { - headers: { - Authorization: `Bearer ${res.data.data.accessToken}`, - }, - }); - - setUserInfo(info.data.data); - setModal({ message: '로그인 성공!', type: 'success' }); - } catch (e) { - setModal({ - message: '로그인 실패. 이메일 또는 비밀번호를 확인해주세요.', - type: 'error', - }); - } - }; - - return ( -
e.preventDefault()}> - setTEmail(true)} - isValid={emailValid} - touched={tEmail} - /> - setTPw(true)} - isValid={pwValid} - touched={tPw} - /> - - - ); + const [email, setEmail] = useState(''); + const [tEmail, setTEmail] = useState(false); + const [password, setPassword] = useState(''); + const [tPw, setTPw] = useState(false); + + const emailValid = validateEmail(email); + const pwValid = validatePassword(password); + const formValid = emailValid && pwValid; + + const submitLogin = async () => { + try { + const res = await api.post('/member/login', { email, password }); + localStorage.setItem('token', res.data.data.accessToken); + + const info = await api.get('/member/me', { + headers: { Authorization: `Bearer ${res.data.data.accessToken}` }, + }); + setUserInfo(info.data.data); + + setModal({ message: '로그인 성공!', type: 'success' }); + setTimeout(() => (window.location.href = '/'), 800); + } catch { + setModal({ + message: '이메일 또는 비밀번호를 확인해주세요.', + type: 'error', + }); + } + }; + + return ( +
e.preventDefault()}> + setTEmail(true)} + isValid={emailValid} + touched={tEmail} + /> + + setTPw(true)} + isValid={pwValid} + touched={tPw} + /> + + + + ); } /* ------------------ 회원가입 ------------------ */ function SignupForm({ setPage, setModal }) { - const [email, setEmail] = useState(''); - const [emailAvailable, setEmailAvailable] = useState(null); - - const [password, setPassword] = useState(''); - const [confirm, setConfirm] = useState(''); - - const [nickname, setNickname] = useState(''); - const [nickAvailable, setNickAvailable] = useState(null); - - const emailValid = validateEmail(email); - const pwValid = validatePassword(password); - const confirmValid = confirm === password && confirm.length > 0; - const nicknameValid = nickname.length >= 2; - - - const formValid = - emailValid && - pwValid && - confirmValid && - nicknameValid && - emailAvailable === false && - nickAvailable === false; - - // 이메일 중복 검사 - useEffect(() => { - if (!emailValid) { - setEmailAvailable(null); - return; - } - const timer = setTimeout(async () => { - try { - const res = await api.get('/member/check-email', { - params: { email }, - }); - - const state = res.data.data.state; // true: 존재, false: 사용 가능 - console.log(state); - setEmailAvailable(!state); - } catch { - setEmailAvailable(true); - } - }, 400); - return () => clearTimeout(timer); - }, [email]); - - // 닉네임 중복 검사 - useEffect(() => { - if (!nicknameValid) { - setNickAvailable(null); - return; - } - const timer = setTimeout(async () => { - try { - const res = await api.get('/member/check-nickname', { - params: { nickname }, - }); - const state = res.data.data.state; // true: 존재 - setNickAvailable(!state); - } catch { - setNickAvailable(true); - } - }, 400); - return () => clearTimeout(timer); - }, [nickname]); - - const submitSignup = async () => { - try { - await api.post('/member', { email, password, nickname }); - setModal({ message: '회원가입 완료! 로그인 해주세요.', type: 'success' }); - setTimeout(() => setPage('login'), 1200); - } catch { - setModal({ message: '회원가입 실패. 다시 시도해주세요.', type: 'error' }); - } - }; - - return ( -
e.preventDefault()}> - {/* 이메일 */} - 0} - /> - {email.length > 0 && !emailValid && ( -
이메일 형식이 아닙니다.
- )} - {emailValid && emailAvailable === true && ( -
사용 가능한 이메일입니다.
- )} - {emailValid && emailAvailable === false && ( -
이미 등록된 이메일입니다.
- )} - - {/* 비밀번호 */} - 0} - /> - {password.length > 0 && !pwValid && ( -
비밀번호는 6자 이상 입력해주세요.
- )} - - {/* 비밀번호 확인 */} - 0} - /> - {confirm.length > 0 && !confirmValid && ( -
입력하신 비밀번호와 일치하지 않습니다.
- )} - - {/* 닉네임 */} - 0} - /> - {nickname.length > 0 && !nicknameValid && ( -
닉네임은 2자 이상 입력해주세요.
- )} - {nicknameValid && nickAvailable === true && ( -
사용 가능한 닉네임입니다.
- )} - {nicknameValid && nickAvailable === false && ( -
이미 등록된 닉네임입니다.
- )} - - - - ); + const [email, setEmail] = useState(''); + const [emailAvailable, setEmailAvailable] = useState(null); + const [password, setPassword] = useState(''); + const [confirm, setConfirm] = useState(''); + const [nickname, setNickname] = useState(''); + const [nickAvailable, setNickAvailable] = useState(null); + + const emailValid = validateEmail(email); + const pwValid = validatePassword(password); + const confirmValid = confirm === password && confirm.length > 0; + const nicknameValid = nickname.length >= 2; + + const formValid = + emailValid && + pwValid && + confirmValid && + nicknameValid && + emailAvailable === true && + nickAvailable === true; + + // 이메일 중복 검사 + useEffect(() => { + if (!emailValid) { + setEmailAvailable(null); + return; + } + const timer = setTimeout(async () => { + try { + const res = await api.get('/member/check-email', { params: { email } }); + const state = res.data.data.state; + setEmailAvailable(!state); + } catch { + setEmailAvailable(false); + } + }, 400); + return () => clearTimeout(timer); + }, [email]); + + // 닉네임 중복 검사 + useEffect(() => { + if (!nicknameValid) { + setNickAvailable(null); + return; + } + const timer = setTimeout(async () => { + try { + const res = await api.get('/member/check-nickname', { + params: { nickname }, + }); + const state = res.data.data.state; + setNickAvailable(!state); + } catch { + setNickAvailable(true); + } + }, 400); + return () => clearTimeout(timer); + }, [nickname]); + + const submitSignup = async () => { + try { + await api.post('/member', { email, password, nickname }); + setModal({ + message: '회원가입 완료! 로그인해주세요 🌿', + type: 'success', + }); + setTimeout(() => setPage('login'), 1000); + } catch { + setModal({ + message: '회원가입 실패. 다시 시도해주세요 🍂', + type: 'error', + }); + } + }; + + return ( +
e.preventDefault()}> + 0} + /> + {emailValid && emailAvailable === true && ( + 사용 가능한 이메일입니다 + )} + {emailValid && emailAvailable === false && ( + 이미 등록된 이메일입니다 + )} + + 0} + /> + + 0} + /> + + 0} + /> + {nicknameValid && nickAvailable === true && ( + 사용 가능한 닉네임입니다 + )} + {nicknameValid && nickAvailable === false && ( + 이미 존재하는 닉네임입니다 + )} + + + + ); } - - -/* ------------------ 공용 Input ------------------ */ +/* ------------------ 재사용 Input ------------------ */ function InputField({ label, type, value, onChange, onBlur, isValid, touched }) { - const filled = value?.length > 0; - const showInvalid = touched && !isValid && filled; - const inputClass = - 'input ' + - (filled ? 'filled ' : '') + - (isValid && filled ? 'valid ' : '') + - (showInvalid ? 'invalid' : ''); - - return ( -
- - onChange(e.target.value)} - onBlur={onBlur} - className={inputClass} - placeholder={label} - /> -
- ); + const filled = value?.length > 0; + const showInvalid = touched && !isValid && filled; + + const inputClass = + 'input ' + + (filled ? 'filled ' : '') + + (isValid && filled ? 'valid ' : '') + + (showInvalid ? 'invalid' : ''); + + return ( +
+ + onChange(e.target.value)} + onBlur={onBlur} + className={inputClass} + placeholder={label} + /> +
+ ); }