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 ? (
-

- ) : (
-
- 🙂
-
- )}
- {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 ? (
+

+ ) : (
+
+ 🙂
+
+ )}
+ {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 ? (
-

- ) : (
-
-
🙂
+
+ {/* ✅ 프로필 중앙 정렬만 적용 */}
+
+
+ {ranks[0]?.imageUrl ? (
+

+ ) : (
+
+ 🙂
+
+ )}
+
+ {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 ? (
-

- ) : (
-
- 🙂
-
- )}
- {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 ? (
+

+ ) : (
+
+ 🙂
+
+ )}
+ {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}