Skip to content

Commit b9bbdeb

Browse files
authored
Merge pull request #115 from codeit-2team/feat/100
Feat/100 내 체험 관리 페이지(UI 및 API연동)
2 parents b79f27a + 9879658 commit b9bbdeb

File tree

10 files changed

+544
-23
lines changed

10 files changed

+544
-23
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import React from 'react';
2+
3+
const MoreOptionsIcon = ({ size = 24, ...props }) => (
4+
<svg
5+
xmlns='http://www.w3.org/2000/svg'
6+
width={size}
7+
height={size}
8+
fill='none'
9+
viewBox='0 0 40 40'
10+
{...props}
11+
>
12+
<circle cx='20' cy='9' r='3' fill='#79747E' />
13+
<circle cx='20' cy='20' r='3' fill='#79747E' />
14+
<circle cx='20' cy='31' r='3' fill='#79747E' />
15+
</svg>
16+
);
17+
18+
export default MoreOptionsIcon;

src/apis/myActivities.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { privateInstance } from './privateInstance';
2+
import { MyActivitiesResponse } from '@/types/dashboardTypes';
3+
4+
/**
5+
* 내 체험 리스트 조회 (무한 스크롤용)
6+
* GET /my-activities
7+
*/
8+
export const getMyActivitiesWithPagination = async (params?: {
9+
cursorId?: number;
10+
size?: number;
11+
}): Promise<MyActivitiesResponse> => {
12+
const queryParams = new URLSearchParams();
13+
if (params?.cursorId)
14+
queryParams.append('cursorId', params.cursorId.toString());
15+
if (params?.size) queryParams.append('size', params.size.toString());
16+
17+
const url = `/my-activities${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
18+
const response = await privateInstance.get(url);
19+
return response.data;
20+
};
21+
22+
/**
23+
* 내 체험 삭제
24+
* DELETE /deleteActivity/{id}
25+
*/
26+
export const deleteMyActivity = async (id: number): Promise<void> => {
27+
await privateInstance.delete(`/deleteActivity/${id}`);
28+
};
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
'use client';
2+
3+
import { ProfileNavigation } from '@/app/(with-header)/mypage/components';
4+
import useResponsiveRouting from '@/hooks/useResponsiveRouting';
5+
import { useMyProfile } from '@/hooks/useMyPageQueries';
6+
7+
export default function MyActivityLayout({
8+
children,
9+
}: {
10+
children: React.ReactNode;
11+
}) {
12+
const { mounted } = useResponsiveRouting();
13+
const { isLoading, error } = useMyProfile();
14+
15+
// mounted + API 로딩 상태 모두 체크
16+
if (!mounted || isLoading) {
17+
return (
18+
<div className='min-h-screen bg-gray-100'>
19+
<div className='mx-auto max-w-1200 px-20 py-24 lg:py-72'>
20+
<div className='flex gap-24'>
21+
{/* 좌측 프로필 네비게이션 스켈레톤 - 데스크톱/태블릿 */}
22+
<div className='hidden flex-shrink-0 animate-pulse md:block'>
23+
<div className='h-432 w-251 rounded border border-gray-300 bg-white p-24 lg:w-384'>
24+
{/* 프로필 이미지 영역 */}
25+
<div className='mb-32 flex justify-center'>
26+
<div className='h-160 w-160 rounded-full bg-gray-200'></div>
27+
</div>
28+
{/* 메뉴 리스트 영역 */}
29+
<div className='space-y-2'>
30+
{[1, 2, 3, 4].map((i) => (
31+
<div
32+
key={i}
33+
className='h-44 w-203 rounded-xl bg-gray-200 lg:w-336'
34+
></div>
35+
))}
36+
</div>
37+
</div>
38+
</div>
39+
{/* 메인 스켈레톤 */}
40+
<div className='flex-grow animate-pulse rounded bg-gray-200'></div>
41+
</div>
42+
</div>
43+
</div>
44+
);
45+
}
46+
47+
if (error) {
48+
return (
49+
<div className='flex min-h-screen items-center justify-center bg-gray-100'>
50+
<div className='text-center'>
51+
<h2 className='mb-2 text-xl font-bold text-red-500'>
52+
로그인이 필요합니다
53+
</h2>
54+
<p className='text-gray-600'>다시 로그인해주세요.</p>
55+
</div>
56+
</div>
57+
);
58+
}
59+
60+
// API 로딩 완료 + mounted 상태일 때만 실행
61+
return (
62+
<div className='min-h-screen bg-gray-100'>
63+
<div className='mx-auto max-w-1200 px-20 py-24 lg:py-72'>
64+
<div className='flex gap-24'>
65+
{/* 좌측 프로필 네비게이션 섹션 - 데스크톱/태블릿에서만 표시 */}
66+
<ProfileNavigation />
67+
68+
{/* 우측 메인 콘텐츠 섹션 */}
69+
<div className='flex-grow'>{children}</div>
70+
</div>
71+
</div>
72+
</div>
73+
);
74+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
'use client';
2+
3+
import Image from 'next/image';
4+
import { useState } from 'react';
5+
import { MyActivity } from '@/types/dashboardTypes';
6+
import { useRouter } from 'next/navigation';
7+
import Star from '@assets/svg/star';
8+
import MoreOptionsIcon from '@assets/svg/moreOptionsIcon';
9+
10+
interface ActivityCardProps {
11+
activity: MyActivity;
12+
onDelete: (activityId: number) => void;
13+
}
14+
15+
export default function ActivityCard({
16+
activity,
17+
onDelete,
18+
}: ActivityCardProps) {
19+
const [isMenuOpen, setIsMenuOpen] = useState(false);
20+
const router = useRouter();
21+
22+
const { id, title, price, bannerImageUrl, rating, reviewCount } = activity;
23+
24+
const handleEdit = () => {
25+
router.push(`/myactivity/${id}`);
26+
};
27+
28+
const handleDelete = () => {
29+
onDelete(id);
30+
setIsMenuOpen(false);
31+
};
32+
33+
return (
34+
<div className='flex h-204 w-792 rounded-3xl border border-gray-300 bg-white'>
35+
{/* 이미지 영역 */}
36+
<div className='relative h-204 w-204 flex-shrink-0 overflow-hidden rounded-l-xl'>
37+
<Image src={bannerImageUrl} alt={title} fill className='object-cover' />
38+
</div>
39+
40+
{/* 콘텐츠 영역 */}
41+
<div className='flex flex-1 flex-col justify-start px-24 py-14'>
42+
{/* 별점 및 리뷰 */}
43+
<div className='flex items-center gap-6'>
44+
<div className='flex items-center gap-2'>
45+
<Star size={19} />
46+
<span className='font-base font-normal text-black'>{rating}</span>
47+
<span className='font-base font-normal text-black'>
48+
({reviewCount})
49+
</span>
50+
</div>
51+
</div>
52+
53+
{/* 제목 */}
54+
<div className='mt-6'>
55+
<h3 className='text-nomad text-xl font-bold'>{title}</h3>
56+
</div>
57+
58+
<div className='mt-auto flex items-center justify-between'>
59+
{/* 가격 */}
60+
<p className='text-2xl font-medium text-gray-900'>
61+
{price.toLocaleString()} / 인
62+
</p>
63+
64+
{/* 더보기 옵션 */}
65+
<div className='relative'>
66+
<button
67+
onClick={() => setIsMenuOpen(!isMenuOpen)}
68+
className='flex h-40 w-40 items-center justify-center rounded-full hover:bg-gray-100'
69+
>
70+
<MoreOptionsIcon size={40} />
71+
</button>
72+
73+
{isMenuOpen && (
74+
<>
75+
<div
76+
className='fixed inset-0 z-40'
77+
onClick={() => setIsMenuOpen(false)}
78+
/>
79+
80+
{/* 드롭다운 메뉴 */}
81+
<div className='absolute top-full right-0 z-50 w-160 rounded-md border border-gray-300 bg-white shadow-lg'>
82+
<button
83+
onClick={handleEdit}
84+
className='flex h-62 w-full items-center justify-center border-b border-gray-300 px-46 py-18 text-center text-lg font-medium text-gray-900 hover:bg-gray-50'
85+
>
86+
수정하기
87+
</button>
88+
<button
89+
onClick={handleDelete}
90+
className='flex h-62 w-full items-center justify-center px-46 py-18 text-center text-lg font-medium text-gray-900 hover:bg-gray-50'
91+
>
92+
삭제하기
93+
</button>
94+
</div>
95+
</>
96+
)}
97+
</div>
98+
</div>
99+
</div>
100+
</div>
101+
);
102+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
'use client';
2+
3+
import Modal from '@/components/Modal';
4+
import Button from '@/components/Button';
5+
import CheckIcon from '@assets/svg/check';
6+
7+
interface DeleteActivityModalProps {
8+
isOpen: boolean;
9+
onCancel: () => void;
10+
onConfirm: () => void;
11+
isLoading?: boolean;
12+
}
13+
14+
export default function DeleteActivityModal({
15+
isOpen,
16+
onCancel,
17+
onConfirm,
18+
isLoading = false,
19+
}: DeleteActivityModalProps) {
20+
return (
21+
<Modal isOpen={isOpen} onOpenChange={(open) => !open && onCancel()}>
22+
<Modal.Content
23+
className='!h-184 !w-298 !max-w-none !min-w-0 !rounded-xl !p-0'
24+
zIndex={999}
25+
backdropClassName='!z-999'
26+
>
27+
<div className='flex h-full w-full flex-col items-center justify-center gap-24 rounded-xl bg-white p-16 shadow-[0px_4px_16px_0px_rgba(17,34,17,0.05)]'>
28+
{/* 체크 아이콘 */}
29+
<div className='flex justify-center'>
30+
<CheckIcon size={24} />
31+
</div>
32+
33+
{/* 메시지 */}
34+
<p className='text-nomad text-center text-lg font-medium'>
35+
체험을 삭제하시겠어요?
36+
</p>
37+
38+
{/* 버튼 */}
39+
<div className='flex gap-12'>
40+
<Button
41+
variant='secondary'
42+
className='text-md h-38 w-80 rounded-lg border border-gray-300 font-medium'
43+
onClick={onCancel}
44+
disabled={isLoading}
45+
>
46+
아니오
47+
</Button>
48+
<Button
49+
variant='primary'
50+
className='text-md bg-nomad h-38 w-80 rounded-lg font-medium text-white'
51+
onClick={onConfirm}
52+
disabled={isLoading}
53+
>
54+
{isLoading ? '삭제 중...' : '삭제하기'}
55+
</Button>
56+
</div>
57+
</div>
58+
</Modal.Content>
59+
</Modal>
60+
);
61+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import EmptyDocumentIcon from '@assets/svg/empty-document';
2+
3+
export default function EmptyActivities() {
4+
return (
5+
<div className='flex flex-col items-center justify-center py-120'>
6+
{/* 빈 상태 아이콘 */}
7+
<div className='mb-24'>
8+
<EmptyDocumentIcon size={131} />
9+
</div>
10+
11+
{/* 빈 상태 메시지 */}
12+
<p className='text-2xl font-normal text-gray-700'>
13+
아직 등록한 체험이 없어요
14+
</p>
15+
</div>
16+
);
17+
}

0 commit comments

Comments
 (0)