diff --git a/src/components/screens/EditProfileScreen.jsx b/src/components/screens/EditProfileScreen.jsx index ee60e31..55e6f2b 100644 --- a/src/components/screens/EditProfileScreen.jsx +++ b/src/components/screens/EditProfileScreen.jsx @@ -33,8 +33,14 @@ const styles = ` } `; -function Modal({ message, type = 'info', onClose }) { - const handleClick = () => onClose(); +function Modal({ message, type = 'info', onClose, action }) { + const navigate = useNavigate(); + const handleClick = () => { + if (action === 'mypage') navigate('/mypage'); + else if (action === 'home') navigate('/'); + + onClose(); + } return (
@@ -85,7 +91,8 @@ export default function EditProfileScreen({ onBack }) { setEmail(data.email); setAvatar(data.image?.imageUrl || data.avatarUrl || null); } catch { - setModal({ message: "로그인이 필요합니다 🍂", type: "error" }); + setModal({ message: "로그인이 필요합니다 ", type: "error" }); + navigate("/login") } }; fetchMyInfo(); @@ -134,11 +141,11 @@ export default function EditProfileScreen({ onBack }) { { nickname }, { headers: { Authorization: `Bearer ${token}` } } ); - setModal({ message: "회원정보 수정이 완료되었습니다 ", type: "success" }); + setModal({ message: "회원정보 수정이 완료되었습니다 ", type: "success", action : "mypage" }); setTimeout(() => { navigate("/mypage"); onBack?.(); - }, 1000); + }, 50000); } catch { setModal({ message: "다시 시도해주세요", type: "error" }); } finally { @@ -154,12 +161,11 @@ export default function EditProfileScreen({ onBack }) { "/member/deactivate", { headers: { Authorization: `Bearer ${token}` } } ); - setModal({ message: "회원 탈퇴가 완료되었습니다 ", type: "success" }); + setModal({ message: "회원 탈퇴가 완료되었습니다 ", type: "success" , action :"home"}); localStorage.clear(); setTimeout(() => { - navigate("/mypage"); onBack?.(); - }, 1000); + }, 50000); } catch { setModal({ message: "다시 시도해주세요", type: "error" }); } finally { @@ -272,13 +278,13 @@ export default function EditProfileScreen({ onBack }) { 뒤로가기
- - {/* ✅ 모달 표시 */} + {modal && ( setModal(null)} + action={modal.action} /> )} diff --git a/src/components/screens/LoginSignupScreen.jsx b/src/components/screens/LoginSignupScreen.jsx index aaa6644..31aca1c 100644 --- a/src/components/screens/LoginSignupScreen.jsx +++ b/src/components/screens/LoginSignupScreen.jsx @@ -1,27 +1,25 @@ import React, { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; import api from '../../api/axios'; -// ↓ redux 안 쓰면 이 부분 제거해도됨 import { useDispatch } from 'react-redux'; import { updateProfile, login, fetchPointInfo } from '../../store/slices/userSlice'; import kakaoBtn from '../../assets/kakao_login_medium_wide.png'; 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; } @@ -33,47 +31,28 @@ 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; - transition:border-color .15s ease, box-shadow .15s ease, background .15s ease; } + .label{ display:block; font-weight:600; color:#333; margin-bottom:6px; } + .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; } .btn{ width:100%; padding:12px 14px; border-radius:12px; border:0; background:var(--brand); color:#fff; font-weight:800; cursor:pointer; margin-top:6px;} - button:focus{ outline:none; box-shadow:none; } .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; - } + .valid-text{ font-size:.88rem; color:#3fa14a; margin-top:6px; margin-left:4px; } + .invalid-text{ font-size:.88rem; color:#d33b3b; margin-top:6px; margin-left:4px; } `; -function Modal({ message, type = 'info', onClose }) { +/* 모달 */ +function Modal({ message, type = 'info', onClose, action }) { + const navigate = useNavigate(); + const handleClick = () => { - if (type === 'success') { - onClose(); - } else { - onClose(); - } + if (action === 'mypage') navigate('/mypage'); + else if (action === 'home') navigate('/'); + else if (action === 'login') navigate('/login'); + onClose(); }; return ( @@ -101,25 +80,23 @@ function Modal({ message, type = 'info', onClose }) { ); } -/* ------------------ 로그인 / 회원가입 통합 ------------------ */ +/* 로그인 / 회원가입 통합 화면 */ export default function LoginSignupScreen({ onNavigate }) { const [page, setPage] = useState('login'); const [userInfo, setUserInfo] = useState(null); - const [modal, setModal] = useState(null); - const dispatch = useDispatch(); // redux없는 사람은 제거 가능 + 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); - // Redux 상태 업데이트 dispatch(login({ token })); dispatch( updateProfile({ @@ -130,7 +107,6 @@ export default function LoginSignupScreen({ onNavigate }) { memberId: res.data.data.memberId, }) ); - // 포인트 정보 가져오기 dispatch(fetchPointInfo()); }) .catch(() => { @@ -143,7 +119,6 @@ export default function LoginSignupScreen({ onNavigate }) { <>
-
GreenMap
그린맵
@@ -164,7 +139,11 @@ export default function LoginSignupScreen({ onNavigate }) { {!userInfo ? ( page === 'login' ? ( - + ) : ( ) @@ -172,34 +151,32 @@ export default function LoginSignupScreen({ onNavigate }) { )} - -{!userInfo && page === 'login' && ( - -)} - + {!userInfo && page === 'login' && ( + + )}
- {modal && ( setModal(null)} + action={modal.action} /> )}
@@ -207,7 +184,7 @@ export default function LoginSignupScreen({ onNavigate }) { ); } -/* ------------------ 로그인 ------------------ */ + function LoginForm({ setUserInfo, setModal, onNavigate }) { const dispatch = useDispatch(); const [email, setEmail] = useState(''); @@ -224,15 +201,14 @@ function LoginForm({ setUserInfo, setModal, onNavigate }) { const res = await api.post('/member/login', { email, password }); const token = res.data.data.accessToken; const memberId = res.data.data.memberId; - + localStorage.setItem('token', token); localStorage.setItem('memberId', memberId); const info = await api.get('/member/me', { headers: { Authorization: `Bearer ${token}` }, }); - - // Redux 상태 업데이트 + dispatch(login({ token })); dispatch( updateProfile({ @@ -243,20 +219,10 @@ function LoginForm({ setUserInfo, setModal, onNavigate }) { memberId: info.data.data.memberId, }) ); - // 포인트 정보 가져오기 dispatch(fetchPointInfo()); - + setUserInfo(info.data.data); - setModal({ message: '로그인 성공!', type: 'success' }); - - // 네비게이션 처리 - 새로고침 없이 이동 - setTimeout(() => { - if (onNavigate) { - onNavigate('home'); - } else if (window.location.pathname === '/login') { - window.location.href = '/'; - } - }, 800); + setModal({ message: '로그인 성공!', type: 'success', action: 'home' }); } catch { setModal({ message: '이메일 또는 비밀번호를 확인해주세요.', @@ -300,8 +266,8 @@ function LoginForm({ setUserInfo, setModal, onNavigate }) { ); } -/* ------------------ 회원가입 ------------------ */ function SignupForm({ setPage, setModal }) { + const navigate = useNavigate(); const [email, setEmail] = useState(''); const [emailAvailable, setEmailAvailable] = useState(null); const [password, setPassword] = useState(''); @@ -364,10 +330,10 @@ function SignupForm({ setPage, setModal }) { try { await api.post('/member', { email, password, nickname }); setModal({ - message: '회원가입 성공 로그인해주세요', + message: '회원가입 성공! 로그인해주세요', type: 'success', + action: 'login', }); - setTimeout(() => setPage('login'), 1000); } catch { setModal({ message: '다시 시도해주세요', @@ -438,7 +404,7 @@ function SignupForm({ setPage, setModal }) { ); } -/* ------------------ 재사용 Input ------------------ */ + function InputField({ label, type, value, onChange, onBlur, isValid, touched }) { const filled = value?.length > 0; const showInvalid = touched && !isValid && filled; diff --git a/src/components/screens/RankingScreen.jsx b/src/components/screens/RankingScreen.jsx index dd4a98b..4e19a88 100644 --- a/src/components/screens/RankingScreen.jsx +++ b/src/components/screens/RankingScreen.jsx @@ -132,206 +132,152 @@ export default function RankingScreen({ onNavigate, onBack, navigation }) {
{/* 2위 */}
-
- - 2 - - 2위 -
-
- {ranks[1]?.imageUrl ? ( - 2위 프로필 - ) : ( -
- 🙂 -
- )} - {ranks[1]?.badgeUrl && ( - 뱃지 - )} -
-
- {ranks[1]?.nickname || '익명'} -
-
- {( - ranks[1]?.memberPoint || - ranks[1]?.point || - 0 - ).toLocaleString()} - P -
-
- 탄소{' '} - {( - ranks[1]?.carbonSave || 0 - ).toFixed(1)} - kg -
- {/* 광택 오버레이 (2위 - gray tone) - - 얇은 대각선 하이라이트가 카드 상단을 스윕(animate-sheen) - - via-white/30 + mix-blend-screen으로 내용 가독성 유지 */} -
-
-
-
+
+
+ 2위 +
+
+ {ranks[1]?.imageUrl ? ( + 2위 프로필 + ) : ( +
+ 🙂 +
+ )} + {ranks[1]?.badgeUrl && ( + 뱃지 + )} +
+
+ {ranks[1]?.nickname || '익명'} +
+
+ {( + ranks[1]?.memberPoint || + ranks[1]?.point || + 0 + ).toLocaleString()} + P +
+
+ 탄소{' '} + {( + ranks[1]?.carbonSave || 0 + ).toFixed(1)} + kg +
- {/* 1위 */} -
+ {/* 1위 (👑 중심 강조) */} +
- - 1 - 1위
-
- {ranks[0]?.imageUrl ? ( - 1위 프로필 - ) : ( -
- 🙂 + + {/* ✅ 프로필 중앙 정렬만 적용 */} +
+
+ {ranks[0]?.imageUrl ? ( + 1위 프로필 + ) : ( +
+ 🙂 +
+ )} + + {ranks[0]?.badgeUrl && ( + 뱃지 + )} +
+ 👑
- )} - {ranks[0]?.badgeUrl && ( - 뱃지 - )} -
- 👑
+
{ranks[0]?.nickname || '익명'}
-
- {( - ranks[0]?.memberPoint || - ranks[0]?.point || - 0 - ).toLocaleString()} - P +
+ {(ranks[0]?.memberPoint || ranks[0]?.point || 0).toLocaleString()}P
- 탄소{' '} - {( - ranks[0]?.carbonSave || 0 - ).toFixed(1)} - kg + 탄소 {(ranks[0]?.carbonSave || 0).toFixed(1)}kg
- {/* 광택 오버레이 (1위 - emerald tone) - - emerald 색조의 하이라이트로 1위 카드 은은 강조 */} -
-
-
-
-
-
+ + {/* 3위 */}
-
- - 3 - - 3위 -
-
- {ranks[2]?.imageUrl ? ( - 3위 프로필 - ) : ( -
- 🙂 -
- )} - {ranks[2]?.badgeUrl && ( - 뱃지 - )} -
-
- {ranks[2]?.nickname || '익명'} -
-
- {( - ranks[2]?.memberPoint || - ranks[2]?.point || - 0 - ).toLocaleString()} - P -
-
- 탄소{' '} - {( - ranks[2]?.carbonSave || 0 - ).toFixed(1)} - kg -
- {/* 광택 오버레이 (3위 - amber tone) - - amber 색조 + 딜레이로 위계감 부여 */} -
-
-
-
+
+
+ 3위 +
+
+ {ranks[2]?.imageUrl ? ( + 3위 프로필 + ) : ( +
+ 🙂 +
+ )} + {ranks[2]?.badgeUrl && ( + 뱃지 + )} +
+
+ {ranks[2]?.nickname || '익명'} +
+
+ {( + ranks[2]?.memberPoint || + ranks[2]?.point || + 0 + ).toLocaleString()} + P +
+
+ 탄소{' '} + {( + ranks[2]?.carbonSave || 0 + ).toFixed(1)} + kg +
@@ -369,20 +315,18 @@ export default function RankingScreen({ onNavigate, onBack, navigation }) { transition={{ delay: index * 0.05, }} - className={`bg-white rounded-xl p-4 shadow-sm hover:shadow-md transition-all duration-200 flex items-center justify-between border ${ - isMe - ? 'border-[#4CAF50]/60' - : 'border-gray-100' - } hover:border-[#4CAF50]/30 group`} + className={`bg-white rounded-xl p-4 shadow-sm hover:shadow-md transition-all duration-200 flex items-center justify-between border ${isMe + ? 'border-[#4CAF50]/60' + : 'border-gray-100' + } hover:border-[#4CAF50]/30 group`} >
{/* 순위 배지 */}
{currentRank}