Skip to content

Commit 39b0e90

Browse files
authored
Merge pull request #310 from prgrms-web-devcourse-final-project/refactor/mypage-userpage
[REFACTOR] 마이페이지 / 유저페이지 리펙토링
2 parents e08138b + dc0efe2 commit 39b0e90

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+644
-458
lines changed

src/App.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,15 @@ import KaKaoRedirection from '@/components/KaKaoRedirection';
2020
import { useSSE } from '@/hooks/useSSE';
2121
import { useYotube } from '@/hooks/useYoutube';
2222
import { useSpotifyAuth } from '@/hooks/spotify/useSpotifyAuth';
23-
import UserProfile from '@/pages/user/UserProfile';
2423
import PublicRoute from '@/routes/PublicRoute';
24+
import CardDetailModalTemp from '@/components/modalSheet/CardDetailModalTemp';
25+
import MyPage from '@/pages/mypage/MyPage';
26+
import UserPage from '@/pages/userpage/UserPage';
2527

2628
function App() {
2729
const { isRequestSendingSheetOpen, isRequestReceivingSheetOpen } = useSheetStore();
2830

2931
useSpotifyAuth();
30-
3132
useSSE(); // SSE연결
3233
useYotube();
3334

@@ -48,10 +49,14 @@ function App() {
4849
<Route path="/post" element={<Post />} />
4950
<Route path="/post/:postId/edit" element={<Post />} />
5051
<Route path="/chatroom/:chatRoomId" element={<ChatRoom />} />
51-
<Route path="/mypage" element={<UserProfile isMyPage={true} />} />
52+
<Route path="/mypage" element={<MyPage />}>
53+
<Route path=":id" element={<CardDetailModalTemp />} />
54+
</Route>
5255
<Route path="/mypage/edit" element={<EditProfile />} />
5356
<Route path="/mypage/blocklist" element={<BlockList />} />
54-
<Route path="/user/:userId" element={<UserProfile />} />
57+
<Route path="/user/:userId" element={<UserPage />}>
58+
<Route path=":id" element={<CardDetailModalTemp />} />
59+
</Route>
5560
</Route>
5661

5762
<Route path="/auth/login/kakao/callback" element={<KaKaoRedirection />} />

src/components/ScrollToTop.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ function ScrollToTop() {
77
useEffect(() => {
88
window.scrollTo({
99
top: 0,
10-
behavior: 'smooth', // 부드럽게 스크롤 (즉시 이동은 'auto')
1110
});
1211
}, [pathname]);
1312

src/components/index.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import MusicCard from './MusicCard';
2+
import EmotionBadge from './EmotionBadge';
3+
import InfoMessage from './InfoMessage';
4+
import EmotionRecordCard from './user/EmotionRecordCard';
5+
import UserProfileCard from './user/UserProfileCard';
6+
import EmotionRecordSection from './user/EmotionRecordSection';
7+
import EmotionRecordCardList from './user/EmotionRecordCardList';
8+
9+
export {
10+
InfoMessage,
11+
EmotionRecordCard,
12+
EmotionBadge,
13+
UserProfileCard,
14+
MusicCard,
15+
EmotionRecordSection,
16+
EmotionRecordCardList,
17+
};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Link, useLocation } from 'react-router';
2+
import closeIcon from '@assets/icons/close-icon.svg';
3+
const BackLink = () => {
4+
const location = useLocation();
5+
6+
const pathSegments = location.pathname.split('/').filter(Boolean); // ['', 'post', '123'] → ['post', '123']
7+
pathSegments.pop(); // 마지막 세그먼트 제거
8+
const newPath = '/' + pathSegments.join('/');
9+
return (
10+
<Link to={newPath} className="flex items-center justify-center w-6 h-6 cursor-pointer">
11+
<img src={closeIcon} alt="닫기" />
12+
</Link>
13+
);
14+
};
15+
16+
export default BackLink;

src/components/modalSheet/CardDetailButtons.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ interface CardDetailButtonsProps {
66

77
function CardDetailButtons({ icon, label, onClick }: CardDetailButtonsProps) {
88
return (
9-
<div className="flex flex-col items-center gap-1 cursor-pointer" onClick={onClick}>
10-
<div className="w-[38px] h-[38px] rounded-full bg-gray-5 flex justify-center items-center hover:bg-gray-10">
9+
<button className="flex flex-col items-center gap-1 cursor-pointer" onClick={onClick}>
10+
<span className="w-[38px] h-[38px] rounded-full bg-gray-5 flex justify-center items-center hover:bg-gray-10">
1111
<img src={icon} alt="아이콘" />
12-
</div>
12+
</span>
1313
<span className="text-[9px] text-gray-50 font-normal">{label}</span>
14-
</div>
14+
</button>
1515
);
1616
}
1717

src/components/modalSheet/CardDetailModal.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@ interface CardDetailModalProps {
1919

2020
function CardDetailModal({ recordId, handleDelete, handleEdit }: CardDetailModalProps) {
2121
const { isCardSheetOpen, setCurrentRecord } = useSheetStore();
22-
const { setVideoId, players, setIsPlaying } = useYouTubeStore();
23-
const isPlaying = players['3']?.isPlaying || false;
22+
const { setVideoId } = useYouTubeStore();
2423

2524
const { data } = useQuery({
2625
queryKey: ['emotionRecord', recordId],
@@ -118,11 +117,9 @@ function CardDetailModal({ recordId, handleDelete, handleEdit }: CardDetailModal
118117
<div className="flex gap-10">
119118
<ChatActionButtons
120119
recordId={recordId}
121-
isChatting={isChatting}
122-
isPlaying={isPlaying}
123120
isOwnPost={!data?.data?.disable}
124121
authorId={data?.data?.loginId}
125-
onPlayPauseToggle={() => setIsPlaying('3', !isPlaying)}
122+
videoId={data?.data?.spotifyMusic.videoId}
126123
/>
127124
</div>
128125
</div>
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import EmotionBadge from '@/components/EmotionBadge';
2+
import ChatActionButtons from '@/components/modalSheet/ChatActionButtons';
3+
import defaultImage from '@assets/images/default.png';
4+
import { useQuery } from '@tanstack/react-query';
5+
import { getEmotionRecordById } from '@/apis/emotionRecord';
6+
import { formatDate } from '@/utils/formatDate';
7+
import ModalSheetLayoutTemp from '@/layouts/ModalSheetLayoutTemp';
8+
import { useParams } from 'react-router';
9+
import { Loading } from '@/components/loading';
10+
11+
interface CardDetailModalProps {}
12+
13+
function CardDetailModalTemp({}: CardDetailModalProps) {
14+
const id = useParams().id!;
15+
16+
const { data: record, isLoading } = useQuery({
17+
queryKey: ['emotionRecord', Number(id)],
18+
queryFn: () => getEmotionRecordById(Number(id)),
19+
select: (data) => data.data,
20+
});
21+
22+
return (
23+
<>
24+
<ModalSheetLayoutTemp isOwnPost={!record?.disable}>
25+
<div className="px-[36.5px] flex flex-col items-center gap-4">
26+
{/* 게시물 정보 */}
27+
<div className="flex flex-col items-center gap-1">
28+
<div className="flex items-center gap-1 caption-r text-gray-60">
29+
<span>{record?.nickName}</span>
30+
{/* <img src={headsetIcon} alt="헤드셋 아이콘" /> */}
31+
</div>
32+
<div className="flex items-center gap-2">
33+
<span className=" font-light text-gray-60 text-[10px]">
34+
{formatDate(record?.createdAt)}
35+
</span>
36+
<EmotionBadge size="small" emotion={record?.emotion} />
37+
</div>
38+
</div>
39+
40+
<div className="flex flex-col gap-2 max-w-[250px]">
41+
<div className="flex flex-col items-center gap-2">
42+
<img
43+
src={record?.spotifyMusic.albumImage}
44+
alt="앨범 이미지"
45+
onError={(e) => {
46+
const target = e.target as HTMLImageElement;
47+
target.onerror = null; // 무한 루프 방지
48+
target.src = defaultImage; // 기본 이미지로 변경
49+
}}
50+
className="w-[80px] h-[80px] rounded-[8px]"
51+
/>
52+
<div className="flex flex-col items-center min-w-0">
53+
<span className="overflow-hidden body-large-b text-ellipsis whitespace-nowrap">
54+
{record?.spotifyMusic.title}
55+
</span>
56+
<span className="overflow-hidden body-m text-ellipsis whitespace-nowrap">
57+
{record?.spotifyMusic.artist}
58+
</span>
59+
</div>
60+
61+
<ChatActionButtons
62+
recordId={record?.recordId}
63+
isOwnPost={!record?.disable}
64+
authorId={record?.loginId}
65+
videoId={record?.spotifyMusic.videoId}
66+
/>
67+
</div>
68+
</div>
69+
70+
<div className="body-r">{record?.comment}</div>
71+
</div>
72+
</ModalSheetLayoutTemp>
73+
{isLoading && <Loading />}
74+
</>
75+
);
76+
}
77+
78+
export default CardDetailModalTemp;
79+
80+
// 사용예시
81+
82+
// <CardDetailModal recordId={selectedRecordId} isChatting={true} />;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import MoreOptionsSelect from '@/components/MoreOptionsSelect';
2+
import { useDeleteEmotionRecord } from '@/hooks/useDeleteEmotionRecord';
3+
import { useModalStore } from '@/store/modalStore';
4+
import { useNavigate, useParams } from 'react-router';
5+
6+
const CardDetailMoreMenu = () => {
7+
const id = useParams().id!;
8+
const navigate = useNavigate();
9+
const { openModal, closeModal } = useModalStore();
10+
// 감정 기록 삭제
11+
const { mutate } = useDeleteEmotionRecord();
12+
13+
// 삭제모달 띄우기
14+
const handleDeleteModal = () => {
15+
openModal({
16+
title: '등록된 글을 삭제할까요?',
17+
message: '삭제된 글은 복구할 수 없습니다',
18+
onConfirm: () => {
19+
mutate(Number(id));
20+
navigate(`/mypage`);
21+
closeModal();
22+
},
23+
onCancel: () => {
24+
closeModal();
25+
},
26+
});
27+
};
28+
29+
// 수정
30+
const handleEdit = () => {
31+
navigate(`/post/${id}/edit`);
32+
};
33+
34+
return (
35+
<MoreOptionsSelect
36+
items={[
37+
{ label: '수정', onClick: handleEdit },
38+
{ label: '삭제', onClick: handleDeleteModal },
39+
]}
40+
/>
41+
);
42+
};
43+
44+
export default CardDetailMoreMenu;

src/components/modalSheet/ChatActionButtons.tsx

Lines changed: 8 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,120 +1,32 @@
1-
import commentIcon from '@assets/icons/comment-icon.svg';
21
import homeIcon from '@assets/icons/home-icon.svg';
32
import CardDetailButtons from '@/components/modalSheet/CardDetailButtons';
4-
import headsetIcon from '@assets/icons/headset-icon-gray.svg';
5-
import playIcon from '@assets/icons/play/play-icon-gray.svg';
6-
import pauseIcon from '@assets/icons/pause-icon-gray.svg';
73
import { useNavigate } from 'react-router';
8-
import { useSheetStore } from '@/store/sheetStore';
9-
// import { cancelChatRequest, requestChat } from '@/apis/chat';
10-
import { requestChat } from '@/apis/chat';
11-
import { useModalStore } from '@/store/modalStore';
12-
// import { useChatStore } from '@/store/chatStore';
4+
import PlayToggleButton from '@/components/modalSheet/PlayToggleButton';
5+
import ChatRequestButton from '@/components/modalSheet/ChatRequestButton';
136

147
interface ChatActionButtonsProps {
158
recordId: number;
16-
isChatting: boolean;
17-
isPlaying: boolean;
9+
videoId: string;
1810
isOwnPost: boolean; // 본인 글 여부
1911
authorId: string; // 작성자 id
20-
onPlayPauseToggle: () => void;
2112
}
2213

2314
// 카드상세 모달에서 채팅중인지에 따라서 버튼 속성 결정
24-
function ChatActionButtons({
25-
recordId,
26-
isChatting,
27-
isPlaying,
28-
isOwnPost,
29-
onPlayPauseToggle,
30-
authorId,
31-
}: ChatActionButtonsProps) {
15+
function ChatActionButtons({ recordId, videoId, isOwnPost, authorId }: ChatActionButtonsProps) {
3216
const navigate = useNavigate();
3317

34-
// const { openSheet, closeSheet, currentRecord } = useSheetStore(); // 모달 시트
35-
const { openSheet } = useSheetStore(); // 모달 시트
36-
// const { pastRecord } = useChatStore();
37-
const { openModal, closeModal } = useModalStore();
38-
3918
const handleGoToUserPage = () => {
40-
// closeAllSheets(); // 모든 시트를 닫아야할지 카드모달시트만 닫으면 될지 고민중
4119
navigate(`/user/${authorId}`); // 유저 페이지로 이동
4220
};
4321

44-
//채팅 요청 취소(요청 보낸 사람)
45-
// const cancel = async () => {
46-
// if (!currentRecord?.recordId && !pastRecord?.recordId) {
47-
// // console.log('record가 존재하지 않습니다');
48-
// return;
49-
// }
50-
// try {
51-
// // console.log(currentRecord);
52-
// const { code } = await cancelChatRequest(
53-
// currentRecord?.recordId || Number(pastRecord?.recordId),
54-
// );
55-
// if (code === 200) {
56-
// // console.log('취소 요청 성공');
57-
// closeSheet('isRequestSendingSheetOpen');
58-
// } else {
59-
// throw new Error('취소 요청 실패');
60-
// }
61-
// } catch (error) {
62-
// console.error(error);
63-
// closeSheet('isRequestSendingSheetOpen'); // 취소 요청 실패시 창 닫기
64-
// }
65-
// };
66-
67-
//채팅 요청
68-
//요청 보내면 sse로 recordId, 보낸 사람 정보 보내줘야 함
69-
const request = async () => {
70-
// if (!recordId) {
71-
// // console.log('recordId가 존재하지 않습니다.');
72-
// return;
73-
// }
74-
try {
75-
const { code } = await requestChat(recordId);
76-
if (code === 200) {
77-
openSheet('isRequestSendingSheetOpen');
78-
} else if (code === 202) {
79-
openModal({
80-
title: '현재 로그아웃 중입니다.',
81-
onConfirm: () => {
82-
closeModal();
83-
// cancel();
84-
},
85-
});
86-
} else {
87-
throw new Error('잠시 후 다시 시도해 주세요');
88-
}
89-
} catch (error) {
90-
// console.log(error);
91-
openModal({
92-
title: '잠시 후 다시 시도해 주세요',
93-
onConfirm: () => {
94-
closeModal();
95-
},
96-
});
97-
}
98-
};
99-
100-
// 채팅 유무에 따라서 props다르게
101-
const chatButtonProps = isChatting
102-
? { icon: headsetIcon, label: '대화 중...' }
103-
: { icon: commentIcon, label: '대화하기', onClick: request };
104-
105-
// 노래 재생 유무에 따라서 다르게
106-
const playButtonProps = isPlaying
107-
? { icon: pauseIcon, label: '재생 중...', onClick: onPlayPauseToggle }
108-
: { icon: playIcon, label: '재생하기', onClick: onPlayPauseToggle };
109-
11022
return (
111-
<>
112-
{!isOwnPost && <CardDetailButtons {...chatButtonProps} />}
113-
{<CardDetailButtons {...playButtonProps} />}
23+
<div className="flex gap-10">
24+
{!isOwnPost && <ChatRequestButton recordId={recordId} />}
25+
<PlayToggleButton videoId={videoId} />
11426
{!isOwnPost && (
11527
<CardDetailButtons icon={homeIcon} label="구경가기" onClick={handleGoToUserPage} />
11628
)}
117-
</>
29+
</div>
11830
);
11931
}
12032

0 commit comments

Comments
 (0)