Skip to content

Commit dbfe136

Browse files
authored
Merge pull request #129 from codeit-6team/feat/97-profile
✨ feat: 내 프로필 상세 페이지 구현
2 parents f1a6c4e + 3aad51f commit dbfe136

File tree

4 files changed

+200
-30
lines changed

4 files changed

+200
-30
lines changed

src/assets/icons/phone.svg

Lines changed: 4 additions & 0 deletions
Loading

src/pages/profile/Profile.tsx

Lines changed: 166 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,172 @@
1-
import { Link } from 'react-router-dom';
1+
import { useState, useEffect, useContext, useCallback } from 'react';
2+
import { useNavigate } from 'react-router-dom';
3+
import { AuthContext } from '@/context/AuthContext';
4+
import RegisterLayout from '@/components/layout/RegisterLayout';
5+
import Footer from '@/components/layout/Footer';
6+
import Table from '@/components/common/table/Table';
7+
import Button from '@/components/common/Button';
8+
import Modal from '@/components/common/Modal';
9+
import { getUser } from '@/api/userApi';
10+
import { getUserApplications } from '@/api/applicationApi';
11+
import formatPhone from '@/utils/formatPhone';
12+
import phone from '@/assets/icons/phone.svg';
13+
import location from '@/assets/icons/location-red.svg';
14+
215
export default function Profile() {
16+
const navigate = useNavigate();
17+
const { isLoggedIn } = useContext(AuthContext);
18+
const [profileInfo, setProfileInfo] = useState({
19+
name: '',
20+
phone: '',
21+
address: '',
22+
bio: '',
23+
});
24+
const [hasInfo, setHasInfo] = useState(false);
25+
const [hasApplications, setHasApplications] = useState(false);
26+
const [modal, setModal] = useState({
27+
isOpen: false,
28+
message: '',
29+
});
30+
const [buttonSize, setButtonSize] = useState<'large' | 'medium'>('medium');
31+
const [isLoading, setIsLoading] = useState(true);
32+
33+
useEffect(() => {
34+
const handleResize = () => {
35+
if (window.innerWidth >= 768) {
36+
setButtonSize('large');
37+
} else {
38+
setButtonSize('medium');
39+
}
40+
};
41+
42+
handleResize();
43+
window.addEventListener('resize', handleResize);
44+
45+
return () => window.removeEventListener('resize', handleResize);
46+
}, []);
47+
48+
useEffect(() => {
49+
const userId = localStorage.getItem('userId');
50+
51+
if (!userId) {
52+
setModal({
53+
isOpen: true,
54+
message: '로그인이 필요합니다.',
55+
});
56+
return;
57+
}
58+
59+
const fetchUserInfo = async () => {
60+
try {
61+
const userInfo = await getUser(userId);
62+
setProfileInfo({
63+
name: userInfo.item.name ?? '',
64+
phone: userInfo.item.phone ?? '',
65+
address: userInfo.item.address ?? '',
66+
bio: userInfo.item.bio ?? '',
67+
});
68+
setHasInfo(!!userInfo.item.name);
69+
const applications = await getUserApplications(userId);
70+
setHasApplications(applications.count > 0);
71+
} catch (error) {
72+
setModal({
73+
isOpen: true,
74+
message: (error as Error).message,
75+
});
76+
} finally {
77+
setIsLoading(false);
78+
}
79+
};
80+
fetchUserInfo();
81+
}, [isLoggedIn]);
82+
83+
const handleModalConfirm = useCallback(() => {
84+
if (modal.message.includes('로그인')) {
85+
setModal({ isOpen: false, message: '' });
86+
navigate('/login');
87+
} else {
88+
setModal({ isOpen: false, message: '' });
89+
}
90+
}, [modal.message, navigate]);
91+
392
return (
493
<>
5-
<Link to="/profile/edit">등록하기</Link>
94+
{isLoading ? (
95+
<div className="flex min-h-[calc(100vh-102px)] items-center justify-center md:min-h-[calc(100vh-70px)]">
96+
<div className="size-100 animate-spin rounded-full border-8 border-gray-200 border-t-primary" />
97+
</div>
98+
) : (
99+
<div className="flex min-h-[calc(100vh-102px)] flex-col justify-between md:min-h-[calc(100vh-70px)]">
100+
{!hasInfo ? (
101+
<div className="mx-12 flex flex-col gap-16 py-40 md:mx-32 md:gap-24 md:py-60 lg:mx-auto lg:w-964">
102+
<h1 className="text-h3 font-bold md:text-h1">내 프로필</h1>
103+
<RegisterLayout type="profile" />
104+
</div>
105+
) : (
106+
<div className="mx-12 flex flex-col gap-16 py-40 md:mx-32 md:gap-24 md:py-60 lg:mx-auto lg:w-964 lg:flex-row lg:justify-between lg:gap-180">
107+
<h1 className="text-h3 font-bold md:text-h1">내 프로필</h1>
108+
<div className="flex justify-between rounded-[12px] bg-red-10 p-20 md:p-32 lg:flex-1">
109+
<div className="flex flex-col gap-8 md:gap-12">
110+
<div className="flex flex-col gap-8">
111+
<div className="text-body2/17 font-bold text-primary md:text-body1/20">
112+
이름
113+
</div>
114+
<h2 className="text-h2/29 font-bold md:text-h1/34">
115+
{profileInfo.name}
116+
</h2>
117+
</div>
118+
<div className="flex items-center gap-6 text-body2/22 text-gray-50 md:text-body1/26">
119+
<img src={phone} className="size-16 md:size-20" />
120+
{formatPhone(profileInfo.phone)}
121+
</div>
122+
<div className="flex items-center gap-6 text-body2/22 text-gray-50 md:text-body1/26">
123+
<img src={location} className="size-16 md:size-20" />
124+
선호 지역: {profileInfo.address}
125+
</div>
126+
{profileInfo.bio && (
127+
<p className="mt-12 text-body2/22 md:mt-16 md:text-body1/26">
128+
{profileInfo.bio}
129+
</p>
130+
)}
131+
</div>
132+
<Button
133+
size={buttonSize}
134+
solid={false}
135+
onClick={() => navigate('/profile/edit')}
136+
className="w-108 shrink-0 md:w-169"
137+
>
138+
편집하기
139+
</Button>
140+
</div>
141+
</div>
142+
)}
143+
144+
{hasInfo && (
145+
<div className="flex-1 bg-gray-5">
146+
<div className="mx-12 flex flex-col gap-16 pt-40 pb-80 md:mx-32 md:gap-24 md:pt-60 md:pb-120 lg:mx-auto lg:w-964">
147+
<h1 className="text-h3 font-bold md:text-h1">신청 내역</h1>
148+
{!hasApplications ? (
149+
<RegisterLayout type="application" />
150+
) : (
151+
<Table
152+
mode="user"
153+
userId={localStorage.getItem('userId') ?? ''}
154+
/>
155+
)}
156+
</div>
157+
</div>
158+
)}
159+
<Footer />
160+
{modal.isOpen && (
161+
<Modal
162+
onClose={handleModalConfirm}
163+
onButtonClick={handleModalConfirm}
164+
>
165+
{modal.message}
166+
</Modal>
167+
)}
168+
</div>
169+
)}
6170
</>
7171
);
8172
}

src/pages/profile/ProfileForm.tsx

Lines changed: 21 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -30,40 +30,33 @@ export default function ProfileForm() {
3030
});
3131

3232
useEffect(() => {
33-
if (isLoggedIn) {
34-
const userId = localStorage.getItem('userId');
35-
36-
if (!userId) {
37-
setModal({
38-
isOpen: true,
39-
message: '사용자 정보를 가져올 수 없습니다. 다시 로그인해주세요.',
40-
});
41-
return;
42-
}
33+
const userId = localStorage.getItem('userId');
4334

44-
const fetchUserInfo = async () => {
45-
try {
46-
const userInfo = await getUser(userId);
47-
setProfileInfo({
48-
name: userInfo.item.name ?? '',
49-
phone: userInfo.item.phone ?? '',
50-
bio: userInfo.item.bio ?? '',
51-
});
52-
setSelectedAddress((userInfo.item.address as SeoulDistrict) ?? '');
53-
} catch (error) {
54-
setModal({
55-
isOpen: true,
56-
message: (error as Error).message,
57-
});
58-
}
59-
};
60-
fetchUserInfo();
61-
} else {
35+
if (!userId) {
6236
setModal({
6337
isOpen: true,
6438
message: '로그인이 필요합니다.',
6539
});
40+
return;
6641
}
42+
43+
const fetchUserInfo = async () => {
44+
try {
45+
const userInfo = await getUser(userId);
46+
setProfileInfo({
47+
name: userInfo.item.name ?? '',
48+
phone: userInfo.item.phone ?? '',
49+
bio: userInfo.item.bio ?? '',
50+
});
51+
setSelectedAddress((userInfo.item.address as SeoulDistrict) ?? '');
52+
} catch (error) {
53+
setModal({
54+
isOpen: true,
55+
message: (error as Error).message,
56+
});
57+
}
58+
};
59+
fetchUserInfo();
6760
}, [isLoggedIn]);
6861

6962
function handleChange(

src/utils/formatPhone.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export default function formatPhone(phone: string) {
2+
if (phone.length === 11) {
3+
return phone.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3');
4+
} else if (phone.length === 10) {
5+
return phone.replace(/(\d{3})(\d{3})(\d{4})/, '$1-$2-$3');
6+
} else {
7+
return phone; // 그대로 보여줌
8+
}
9+
}

0 commit comments

Comments
 (0)