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
29 changes: 25 additions & 4 deletions src/app/(global)/(mypage)/myInfo/_components/myExperiences.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,21 @@
import MyExperienceCard from '@/src/components/pages/myExperiences/MyExperienceCard';
import Button from '@/src/components/primitives/Button';
import LoadingSpinner from '@/src/components/primitives/LoadingSpinner';
import { useQuery } from '@tanstack/react-query';
import { useMutation, useQuery } from '@tanstack/react-query';
import Image from 'next/image';
import BackIcon from '@/public/images/icons/BackIcon.svg';
import { useContext } from 'react';
import { useContext, useState } from 'react';
import { TabContext } from '../pageContext';
import { queries } from '@/src/services/primitives/queries';
import ConfirmModal from '@/src/components/primitives/modal/ConfirmModal';
import Link from 'next/link';

export default function MyExperiencesPage() {
const [isModalVisible, setIsModalVisible] = useState(false);
const [selectedExperience, setSelectedExperience] = useState<number>(0);
const { setIsTabOpen } = useContext(TabContext);
const { data, isPending } = useQuery(queries.myExperiencesOptions());
const mutation = useMutation(queries.myExperiencesMutationOptions());

return (
<section className='flex flex-col items-center gap-8'>
Expand All @@ -31,14 +36,21 @@ export default function MyExperiencesPage() {
</p>
</div>
</div>
<Button size='lg'>체험 등록하기</Button>
<Link href={'/myCreateExperiences'}>
<Button size='lg'>체험 등록하기</Button>
</Link>
</div>
{!isPending ? (
<>
{data && data.totalCount !== 0 ? (
<>
{data.activities.map((activity) => (
<MyExperienceCard data={activity} key={activity.id} />
<MyExperienceCard
data={activity}
key={activity.id}
callbackId={setSelectedExperience}
setIsModalVisible={setIsModalVisible}
/>
))}
</>
) : (
Expand All @@ -59,6 +71,15 @@ export default function MyExperiencesPage() {
) : (
<LoadingSpinner />
)}
<ConfirmModal
onConfirm={() => {
mutation.mutate(selectedExperience);
setIsModalVisible(false);
}}
onCancel={() => setIsModalVisible(false)}
isOpen={isModalVisible}
message='정말 체험을 삭제하시겠습니까?'
/>
</section>
);
}
2 changes: 1 addition & 1 deletion src/app/(global)/(mypage)/myInfo/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export default function MypageLayout() {
<TabContext.Provider value={{ tab, setTab, isTabOpen, setIsTabOpen }}>
<section
id='my-info-body'
className='w-full pt-[85px] pb-[64px] overflow-hidden md:pt-[120px] md:pb-[54px] h-full'
className='w-full pt-[85px] pb-[64px] overflow-hidden md:pt-[120px] md:pb-[54px]'
>
<div
className={cn(
Expand Down
36 changes: 27 additions & 9 deletions src/app/(global)/(mypage)/myUpdateExperiences/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { Controller, useForm } from 'react-hook-form';
import { format } from 'date-fns';

import AvailableTimeSlots from '@/src/components/pages/myCreateExperiences/AvailableTimeSlots';

import UploadBannerImage from '@/src/components/pages/myCreateExperiences/UploadBannerImage';
import Button from '@/src/components/primitives/Button';
import FormInput from '@/src/components/primitives/input/FormInput';
Expand Down Expand Up @@ -48,6 +47,15 @@ interface ExperiencesFormData {
schedules: Schedule[];
}

const dropdownItem = [
{ id: 1, title: '문화 · 예술' },
{ id: 2, title: '식음료' },
{ id: 3, title: '스포츠' },
{ id: 4, title: '투어' },
{ id: 5, title: '관광' },
{ id: 6, title: '웰빙' },
];

export default function MyUpdateExperiencesPage() {
const { id } = useParams<{ id: string }>();
const router = useRouter();
Expand Down Expand Up @@ -119,6 +127,12 @@ export default function MyUpdateExperiencesPage() {
const [bannerUrls, setBannerUrls] = useState<string[]>([]);
const [subUrls, setSubUrls] = useState<string[]>([]);

// 서버에서 받아온 카테고리 상태
const [categoryDefaultValue, setCategoryDefaultValue] = useState<{
id: number;
title: string;
} | null>(null);

// 1️⃣ 기존 데이터 불러오기
useEffect(() => {
const fetchData = async () => {
Expand All @@ -136,6 +150,11 @@ export default function MyUpdateExperiencesPage() {
address: detail.address,
});

// 카테고리 상태 세팅
setCategoryDefaultValue(
dropdownItem.filter((el) => el.title === detail.category)[0]
);

// 이미지 상태 세팅
setBannerUrls([detail.bannerImageUrl]);
setSubUrls(
Expand Down Expand Up @@ -197,6 +216,11 @@ export default function MyUpdateExperiencesPage() {
console.log('📌 새로 업로드한 이미지 URL:', subImageUrlsToAdd);
}

// 드롭다운 이름 파싱
data.category = dropdownItem.find(
(el) => el.id === parseInt(data.category)
)!.title;

// 4️⃣ payload 구성 (기존 이미지는 API에서 자동 유지됨)
const payload: UpdateExperiencePayload = {
...data,
Expand Down Expand Up @@ -247,16 +271,10 @@ export default function MyUpdateExperiencesPage() {
render={({ field, fieldState }) => (
<Dropdown
label='카테고리'
items={[
'문화 · 예술',
'식음료',
'스포츠',
'투어',
'관광',
'웰빙',
]}
items={dropdownItem}
value={field.value}
onChange={field.onChange}
defaultValue={categoryDefaultValue!}
error={fieldState.error?.message}
/>
)}
Expand Down
22 changes: 20 additions & 2 deletions src/components/pages/detail/ActivityFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { IActivity } from '@/src/types/scheduleType';
import Button from '../../primitives/Button';
import { format } from 'date-fns';
import { useReservationStore } from '@/src/store/ReservationStore';
import { createReservation } from '@/src/services/pages/detail/postReservation';
import { useToastStore } from '@/src/store/useToastStore';

export default function ActivityFooter({
activity,
Expand All @@ -16,12 +18,28 @@ export default function ActivityFooter({
const timeInfo = activity.schedules.find(
(schedule) => schedule.id === timeSelector.timeId
);
const createToast = useToastStore((state) => state.createToast);

const handleReserveClick = () => {
console.log({
createReservation(activity.id, {
scheduleId: timeSelector.timeId!,
headCount: personSelector.person,
});
})
.then(() => {
createToast({
message: '예약이 완료되었습니다!',
type: 'success',
});
})
.catch((err) => {
createToast({
message:
err.response.status === 409
? '이미 예약된 스케줄입니다!'
: '예약이 실패했습니다!',
type: 'failed',
});
});
};

return (
Expand Down
21 changes: 19 additions & 2 deletions src/components/pages/detail/ReservationUI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,32 @@ import Calendar from './DateSelector/Calendar';
import { useReservationStore } from '@/src/store/ReservationStore';
import TimeSelectorButtons from './DateSelector/TimeSelector';
import { createReservation } from '@/src/services/pages/detail/postReservation';
import { useToastStore } from '@/src/store/useToastStore';

export default function ReservationUI({ activity }: { activity: IActivity }) {
const { personSelector, timeSelector } = useReservationStore();
const createToast = useToastStore((state) => state.createToast);

const handleReserveClick = async () => {
const data = await createReservation(activity.id, {
createReservation(activity.id, {
scheduleId: timeSelector.timeId!,
headCount: personSelector.person,
});
})
.then(() => {
createToast({
message: '예약이 완료되었습니다!',
type: 'success',
});
})
.catch((err) => {
createToast({
message:
err.response.status === 409
? '이미 예약된 스케줄입니다!'
: '예약이 실패했습니다!',
type: 'failed',
});
});
};

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
'use client';

import TimepickerDropdown from './Dropdown';
import { useBreakPoint } from '@/src/hooks/useBreakPoint';
import CalendarInput from '../../primitives/input/CalendarInput';
import TimepickerDropdown from './TimePickerDropdown';
import { useTimeSlotStore } from '@/src/store/TimeSlotStore';
import Image from 'next/image';

Expand Down
31 changes: 24 additions & 7 deletions src/components/pages/myExperiences/MyExperienceCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@
import { IActivity } from '@/src/types/scheduleType';
import StarIcon from '@/public/images/icons/StarFilled.svg';
import ExperienceButton from './ExperienceButton';
import Link from 'next/link';

export default function MyExperienceCard({ data }: { data: IActivity }) {
console.log(data);
// TODO: 카드 클릭하면 체험 관리 페이지로 가게
export default function MyExperienceCard({
data,
callbackId,
setIsModalVisible,
}: {
data: IActivity;
callbackId: (id: number) => void;
setIsModalVisible: (state: boolean) => void;
}) {
return (
<div className='w-full p-6 flex justify-between rounded-3xl shadow'>
<div className='flex flex-col gap-2.5 lg:gap-3'>
Expand All @@ -24,10 +31,20 @@ export default function MyExperienceCard({ data }: { data: IActivity }) {
&#8361; {data.price.toLocaleString()} <span>&#47; 인</span>
</h2>
<div className='flex gap-2'>
<ExperienceButton size='sm' variant='outline'>
수정하기
</ExperienceButton>
<ExperienceButton size='sm' variant='outline' alert={true}>
<Link href={`/myUpdateExperiences/${data.id}`}>
<ExperienceButton size='sm' variant='outline'>
수정하기
</ExperienceButton>
</Link>
<ExperienceButton
size='sm'
variant='outline'
alert={true}
onClick={() => {
callbackId(data.id);
setIsModalVisible(true);
}}
>
삭제하기
</ExperienceButton>
</div>
Expand Down
18 changes: 15 additions & 3 deletions src/components/primitives/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,20 @@ interface DropdownProps {
value: string | null;
onChange: (id: number) => void;
error?: string;
defaultValue?: DropdownItem;
}

export default function Dropdown({
label,
items,
value,
onChange,
defaultValue,
}: DropdownProps) {
const [isOpen, setIsOpen] = useState(false);
const [selectedItem, setSelectedItem] = useState<DropdownItem | null>(null);
const [selectedItem, setSelectedItem] = useState<DropdownItem | null>(
defaultValue ?? null
);
const [contentUp, setContentUp] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
Expand All @@ -50,6 +54,7 @@ export default function Dropdown({
}, [isOpen]);

useEffect(() => {
if (defaultValue) setSelectedItem(defaultValue);
function handleClickOutside(event: MouseEvent) {
if (
dropdownRef.current &&
Expand All @@ -62,14 +67,21 @@ export default function Dropdown({
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);

useEffect(() => {
if (defaultValue) setSelectedItem(defaultValue);
}, [defaultValue]);
return (
<div
ref={dropdownRef}
className='relative w-full font-bold text-base text-gray-950'
>
{label}
<button
onClick={() => setIsOpen((prev) => !prev)}
onClick={(e) => {
e.preventDefault();
setIsOpen((prev) => !prev);
}}
className='inline-flex w-full justify-between items-center border border-gray-100 rounded-2xl px-5 py-4 font-medium text-gray-400 mt-2.5'
>
<span className={value ? 'text-gray-900' : 'text-gray-400'}>
Expand All @@ -86,7 +98,7 @@ export default function Dropdown({
<div
className={cn(
contentUp ? 'bottom-full' : 'top-full',
'absolute right-0 z-10 mt-2 w-full p-3 origin-top-right bg-white border border-gray-100 rounded-2xl'
'absolute right-0 z-10 mt-2 w-full h-44 lg:h-64 p-3 origin-top-right bg-white border border-gray-100 rounded-2xl overflow-scroll'
)}
ref={contentRef}
>
Expand Down
5 changes: 3 additions & 2 deletions src/services/pages/detail/postReservation.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import { IReservation } from '@/src/types/activityType';
import { apiClient } from '../../primitives/apiClient';
import { CancelMyReservationResponse } from '@/src/types/myReservationType';

export async function createReservation(
activityId: number,
body: IReservation
) {
try {
const res = await apiClient.post(
const res = await apiClient.post<CancelMyReservationResponse>(
`/activities/${activityId}/reservations`,
body
);
if (res.status !== 201) throw new Error(res.data.message);
return res.data;
} catch (err) {
if (err instanceof Error)
console.error('예약에 실패했습니다!', err.message);
throw err;
}
}
12 changes: 12 additions & 0 deletions src/services/pages/myExperiences/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,15 @@ export async function getMyReservationStatus(
throw err;
}
}

// 내 체험 지우기
export async function deleteMyExperiences(experienceId: number) {
try {
await apiClient.delete(`/my-activities/${experienceId}`);

return { ok: true };
} catch (err) {
if (err instanceof Error)
console.error('체험을 지우는데 실패했습니다!', err.message);
}
}
Loading