Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/common/BottomNavigation.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}`}

Expand Down
194 changes: 101 additions & 93 deletions src/components/screens/EditProfileScreen.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className="fixed inset-0 flex items-center justify-center bg-black/40 z-50">
<div className="bg-white rounded-2xl shadow-xl w-80 h-100 p-6 text-center">
<div className={`text-4xl mb-3 ${type === 'success' ? 'text-green-500' : 'text-red-500'}`}>
{type === 'success' ? '🌳' : 'πŸ‚'}
</div>

<p className="text-gray-800 font-semibold mb-4 mt-4">{message}</p>

<button
onClick={handleClick}
className="w-full py-2 rounded-xl font-bold text-white"
style={{ background: type === 'success' ? '#96cb6f' : '#e63e3eff' }}
>
확인
</button>
</div>
</div>
);
}


const styles = `
:root { --brand: ${themeColor}; }
Expand All @@ -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); }
Expand All @@ -63,72 +33,99 @@ const styles = `
}
`;

function Modal({ message, type = 'info', onClose }) {
const handleClick = () => onClose();

return (
<div className="fixed inset-0 flex items-center justify-center bg-black/40 z-50">
<div className="bg-white rounded-2xl shadow-xl w-80 p-6 text-center">
<div
className={`text-4xl mb-3 ${
type === 'success' ? 'text-green-500' : 'text-red-500'
}`}
>
{type === 'success' ? '🌳' : 'πŸ‚'}
</div>
<p className="text-gray-800 font-semibold mb-4 mt-4">{message}</p>
<button
onClick={handleClick}
className="w-full py-2 rounded-xl font-bold text-white"
style={{
background: type === 'success' ? '#96cb6f' : '#e63e3e',
}}
>
확인
</button>
</div>
</div>
);
}

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 {
const res = await api.get("/member/me", {
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);
Expand All @@ -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);
}
Expand All @@ -160,66 +152,76 @@ export default function EditProfileScreen({ onBack }) {
<div className="auth-wrap">
<style>{styles}</style>

{/* βœ… λͺ¨λ‹¬ λ Œλ” */}
{modalOpen && (
<Modal
type={modalType}
message={modalMsg}
onClose={() => setModalOpen(false)}
redirectPath="/mypage" // βœ… 성곡 μ‹œ 이동 경둜
/>
)}

<div className="card">
<h2 className="title">ν”„λ‘œν•„ μˆ˜μ •</h2>
<p className="subtitle">이메일은 λ³€κ²½ν•  수 μ—†μŠ΅λ‹ˆλ‹€.</p>

{/* ν”„λ‘œν•„ 이미지 */}
{avatar ? (
<img src={avatar} alt="ν”„λ‘œν•„" className="profile-image" />
) : (
<div
className="profile-image"
style={{ fontSize: 36, color: "#777", display: "flex", alignItems: "center", justifyContent: "center" }}
style={{
fontSize: 36,
color: "#777",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
πŸ‘€
</div>
)}

{/* 이메일 */}
<div className="field">
<label className="label">이메일</label>
<input className="input filled" value={email} disabled />
</div>

{/* λ‹‰λ„€μž„ */}
<div className="field">
<label className={`label ${nickname ? "filled" : ""}`}>λ‹‰λ„€μž„</label>
<input
className={`input ${nicknameValid ? (nickAvailable === false ? "invalid" : "valid") : ""}`}
className={`input ${
nicknameValid
? nickAvailable === false
? "invalid"
: "valid"
: ""
}`}
value={nickname}
onChange={(e) => setNickname(e.target.value)}
placeholder="λ‹‰λ„€μž„ μž…λ ₯"
/>
</div>

{nicknameValid && nickAvailable === true && (
<span style={{ color: "green" }}>μ‚¬μš© κ°€λŠ₯ν•œ λ‹‰λ„€μž„μž…λ‹ˆλ‹€ </span>
)}
{nicknameValid && nickAvailable === false && (
<span style={{ color: "red" }}>이미 μ‘΄μž¬ν•˜λŠ” λ‹‰λ„€μž„μž…λ‹ˆλ‹€ </span>
)}

<div style={{ fontSize: 12, color: "#666", marginTop: 10 }}>
ν”„λ‘œν•„ 사진 및 λΉ„λ°€λ²ˆν˜Έ 변경은 μΆ”ν›„ 지원 μ˜ˆμ •μž…λ‹ˆλ‹€.
</div>
{/* μƒνƒœ λ©”μ‹œμ§€ */}
{nicknameValid && nickname === originNickname ? (
<span style={{ color: "#d33b3b" }}>ν˜„μž¬ λ‹‰λ„€μž„μž…λ‹ˆλ‹€</span>
) : nicknameValid && nickAvailable === true ? (
<span style={{ color: "#3fa14a" }}>μ‚¬μš© κ°€λŠ₯ν•œ λ‹‰λ„€μž„μž…λ‹ˆλ‹€</span>
) : nicknameValid && nickAvailable === false ? (
<span style={{ color: "#d33b3b" }}>이미 μ‘΄μž¬ν•˜λŠ” λ‹‰λ„€μž„μž…λ‹ˆλ‹€</span>
) : null}

{/* μ €μž₯ */}
<button
className="btn"
style={{ marginTop: 16 }}
disabled={!nicknameValid || nickAvailable === false || loading}
disabled={
!nicknameValid ||
nickname === originNickname ||
nickAvailable === false ||
loading
}
onClick={handleSubmit}
>
{loading ? "μ €μž₯ 쀑..." : "μ €μž₯"}
</button>

{/* λ’€λ‘œκ°€κΈ° */}
<button
style={{
marginTop: 10,
Expand All @@ -230,14 +232,20 @@ export default function EditProfileScreen({ onBack }) {
background: "#fff",
cursor: "pointer",
}}
onClick={() => {
if (onBack) onBack(); // λΆ€λͺ¨μ—μ„œ μ „λ‹¬λœ ν•¨μˆ˜κ°€ 있으면 μ‹€ν–‰
else navigate(-1); // μ—†μœΌλ©΄ λΈŒλΌμš°μ € λ’€λ‘œκ°€κΈ°
}}
onClick={onBack}
>
λ’€λ‘œκ°€κΈ°
</button>
</div>

{/* βœ… λͺ¨λ‹¬ ν‘œμ‹œ */}
{modal && (
<Modal
message={modal.message}
type={modal.type}
onClose={() => setModal(null)}
/>
)}
</div>
);
}
}
Loading