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/EditProfileScreen.jsx b/src/components/screens/EditProfileScreen.jsx index 0437ac9..a89aade 100644 --- a/src/components/screens/EditProfileScreen.jsx +++ b/src/components/screens/EditProfileScreen.jsx @@ -3,35 +3,6 @@ import { useNavigate } from 'react-router-dom'; import api from "../../api/axios"; const themeColor = "#96cb6f"; -function Modal({ message, type = 'info', onClose, redirectPath = '/mypage' }) { - const handleClick = () => { - if (type === 'success') { - window.location.href = redirectPath; // ✅ 저장 성공 시 /mypage로 이동 - } else { - onClose(); - } - }; - return ( -
-
-
- {type === 'success' ? '🌳' : '🍂'} -
- -

{message}

- - -
-
- ); -} - const styles = ` :root { --brand: ${themeColor}; } @@ -43,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); } @@ -63,21 +33,46 @@ const styles = ` } `; +function Modal({ message, type = 'info', onClose }) { + const handleClick = () => onClose(); + + return ( +
+
+
+ {type === 'success' ? '🌳' : '🍂'} +
+

{message}

+ +
+
+ ); +} + export default function EditProfileScreen({ onBack }) { 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 [modalOpen, setModalOpen] = useState(false); - const [modalType, setModalType] = useState('info'); // 'success' | 'error' | 'info' - const [modalMsg, setModalMsg] = useState(''); - + const [modal, setModal] = useState(null); const token = localStorage.getItem("token"); + // 1. 기존 회원정보 불러오기 useEffect(() => { const fetchMyInfo = async () => { try { @@ -85,50 +80,52 @@ 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); } catch { - // alert("로그인이 필요합니다."); - setModalType('error'); - setModalMsg('로그인이 필요합니다.'); - setModalOpen(true); + setModal({ message: "로그인이 필요합니다 🍂", type: "error" }); } }; fetchMyInfo(); }, []); + // 2. 닉네임 중복 검사 (debounce) useEffect(() => { if (!nickname || nickname.length < 2) { setNickAvailable(null); return; } + + if (nickname === originNickname) { + setNickAvailable(null); + return; + } + const timer = setTimeout(async () => { try { - const res = await api.get("/member/check-nickname", { params: { nickname } }); + const res = await api.get("/member/check-nickname", { + params: { nickname }, + }); const isDuplicate = res.data.data.state; setNickAvailable(!isDuplicate); } catch { setNickAvailable(null); } }, 400); + return () => clearTimeout(timer); }, [nickname]); - // ✅ 저장 시 모달로 성공/실패 표시 + // 3. 닉네임 변경 const handleSubmit = async () => { - if (!nickname || nickname.length < 2) { - setModalType('error'); - setModalMsg('닉네임을 입력해주세요.'); - setModalOpen(true); - return; - } - if (nickAvailable === false) { - setModalType('error'); - setModalMsg('이미 사용 중인 닉네임입니다.'); - setModalOpen(true); - return; - } + 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); @@ -137,18 +134,13 @@ export default function EditProfileScreen({ onBack }) { { nickname }, { headers: { Authorization: `Bearer ${token}` } } ); - - // ✅ 성공 모달 오픈 (확인 누르면 /mypage로 이동) - setModalType('success'); - setModalMsg('닉네임이 변경되었습니다.'); - setModalOpen(true); - - // ❌ navigate('/mypage'); // 모달에서 이동 처리 - // onBack?.(); // 모달 UX 유지 위해 주석 + setModal({ message: "회원정보 수정이 완료되었습니다 ", type: "success" }); + setTimeout(() => { + navigate("/mypage"); + onBack?.(); + }, 1000); } catch { - setModalType('error'); - setModalMsg('수정 실패. 다시 시도해주세요.'); - setModalOpen(true); + setModal({ message: "다시 시도해주세요", type: "error" }); } finally { setLoading(false); } @@ -160,66 +152,76 @@ export default function EditProfileScreen({ onBack }) {
- {/* ✅ 모달 렌더 */} - {modalOpen && ( - setModalOpen(false)} - redirectPath="/mypage" // ✅ 성공 시 이동 경로 - /> - )} -

프로필 수정

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

+ {/* 프로필 이미지 */} {avatar ? ( 프로필 ) : (
👤
)} + {/* 이메일 */}
+ {/* 닉네임 */}
setNickname(e.target.value)} placeholder="닉네임 입력" />
- {nicknameValid && nickAvailable === true && ( - 사용 가능한 닉네임입니다 - )} - {nicknameValid && nickAvailable === false && ( - 이미 존재하는 닉네임입니다 - )} - -
- 프로필 사진 및 비밀번호 변경은 추후 지원 예정입니다. -
+ {/* 상태 메시지 */} + {nicknameValid && nickname === originNickname ? ( + 현재 닉네임입니다 + ) : nicknameValid && nickAvailable === true ? ( + 사용 가능한 닉네임입니다 + ) : nicknameValid && nickAvailable === false ? ( + 이미 존재하는 닉네임입니다 + ) : null} + {/* 저장 */} + {/* 뒤로가기 */}
+ + {/* ✅ 모달 표시 */} + {modal && ( + setModal(null)} + /> + )}
); -} +} \ No newline at end of file diff --git a/src/components/screens/LoginSignupScreen.jsx b/src/components/screens/LoginSignupScreen.jsx index 47fe482..43c819d 100644 --- a/src/components/screens/LoginSignupScreen.jsx +++ b/src/components/screens/LoginSignupScreen.jsx @@ -15,9 +15,9 @@ 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`; }; // 전역 스타일 그대로 유지 @@ -32,9 +32,9 @@ 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; } - .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); } @@ -47,169 +47,221 @@ const styles = ` .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; + } +`; - .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') { + onClose(); + } else { + onClose(); + } + }; -`; + return ( +
+
+
+ {type === 'success' ? '🌳' : '🍂'} +
+

{message}

+ +
+
+ ); +} +/* ------------------ 로그인 / 회원가입 통합 ------------------ */ export default function LoginSignupScreen() { - const [page, setPage] = useState('login'); - const [userInfo, setUserInfo] = 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' ? ( - - ) : ( - - ) - ) : ( - setPage('HomeScreen') - )} - - {!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) => ( + + ))}
- - ); -} + )} -/* ------------------ 로그인 ------------------ */ -function LoginForm({ setUserInfo }) { - 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, - }); - console.log("login response: ", res.data.data); - localStorage.setItem('token', res.data.data.accessToken); - localStorage.setItem('memberId', res.data.data.memberId); - const info = await api.get('/member/me', { - headers: { - Authorization: `Bearer ${res.data.data.accessToken}`, - }, - }); - - setUserInfo(info.data.data); - alert('로그인 성공'); - - // 로그인 성공 시 메인으로 이동 - window.location.href = '/'; - } catch (e) { - alert('로그인 실패'); - } - }; - - return ( -
e.preventDefault()}> - setTEmail(true)} - isValid={emailValid} - touched={tEmail} - /> - - setTPw(true)} - isValid={pwValid} - touched={tPw} - /> + {!userInfo ? ( + page === 'login' ? ( + + ) : ( + + ) + ) : ( + + )} + {!userInfo && ( - - ); + )} +
+ + {/* ✅ 모달 표시 */} + {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' }); + 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 }) { +function SignupForm({ setPage, setModal }) { const [email, setEmail] = useState(''); const [emailAvailable, setEmailAvailable] = useState(null); const [password, setPassword] = useState(''); @@ -230,7 +282,7 @@ function SignupForm({ setPage }) { emailAvailable === true && nickAvailable === true; - // 이메일 중복검사 + // 이메일 중복 검사 useEffect(() => { if (!emailValid) { setEmailAvailable(null); @@ -248,7 +300,7 @@ function SignupForm({ setPage }) { return () => clearTimeout(timer); }, [email]); - // 닉네임 중복검사 + // 닉네임 중복 검사 useEffect(() => { if (!nicknameValid) { setNickAvailable(null); @@ -256,7 +308,9 @@ function SignupForm({ setPage }) { } const timer = setTimeout(async () => { try { - const res = await api.get('/member/check-nickname', { params: { nickname } }); + const res = await api.get('/member/check-nickname', { + params: { nickname }, + }); const state = res.data.data.state; setNickAvailable(!state); } catch { @@ -269,83 +323,68 @@ function SignupForm({ setPage }) { const submitSignup = async () => { try { await api.post('/member', { email, password, nickname }); - alert('회원가입 완료. 로그인 해주세요'); - setPage('login'); + setModal({ + message: '회원가입 완료! 로그인해주세요 🌿', + type: 'success', + }); + setTimeout(() => setPage('login'), 1000); } catch { - alert('회원가입 실패'); + setModal({ + message: '회원가입 실패. 다시 시도해주세요 🍂', + type: 'error', + }); } }; return (
e.preventDefault()}> - {/* 이메일 */} -
- - setEmail(e.target.value)} - className={`input ${email.length > 0 ? 'filled' : ''}`} - placeholder="이메일" - /> - {email.length > 0 && !emailValid && ( -
이메일 형식이 아닙니다.
- )} - {emailValid && emailAvailable === true && ( - 사용 가능한 이메일입니다 - )} - {emailValid && emailAvailable === false && ( - 이미 등록된 이메일입니다 - )} -
- - {/* 비밀번호 */} -
- - setPassword(e.target.value)} - className={`input ${password.length > 0 ? 'filled' : ''}`} - placeholder="비밀번호" - /> - {password.length > 0 && !pwValid && ( -
비밀번호는 6자 이상 입력해주세요.
- )} -
- - {/* 비밀번호 확인 */} -
- - setConfirm(e.target.value)} - className={`input ${confirm.length > 0 ? 'filled' : ''}`} - placeholder="비밀번호 확인" - /> - {confirm.length > 0 && !confirmValid && ( -
입력한 비밀번호와 일치하지 않습니다.
- )} -
- - {/* 닉네임 */} -
- - setNickname(e.target.value)} - className={`input ${nickname.length > 0 ? 'filled' : ''}`} - placeholder="닉네임" - /> - {nicknameValid && nickAvailable === true && ( - 사용 가능한 닉네임입니다 - )} - {nicknameValid && nickAvailable === false && ( - 이미 존재하는 닉네임입니다 - )} -
+ 0} + /> + {emailValid && emailAvailable === true && ( + 사용 가능한 이메일입니다 + )} + {emailValid && emailAvailable === false && ( + 이미 등록된 이메일입니다 + )} + + 0} + /> + + 0} + /> + + 0} + /> + {nicknameValid && nickAvailable === true && ( + 사용 가능한 닉네임입니다 + )} + {nicknameValid && nickAvailable === false && ( + 이미 존재하는 닉네임입니다 + )} + +
+ + + ); +} + 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)} + /> + )}
    ); }