diff --git a/Mocks/reservationStatus.mock.ts b/Mocks/reservationStatus.mock.ts new file mode 100644 index 0000000..36ba65e --- /dev/null +++ b/Mocks/reservationStatus.mock.ts @@ -0,0 +1,44 @@ +import { Reservation } from "@/feature/reservationStatus/types/reservation"; + +export const mockReservations: Reservation[] = [ + { + id: "선기훈", + title: "함께 배우는 플로잉 댄스", + date: "2025-12-01T11:00:00", + price: 10000, + people: 1, + status: "pending", + }, + { + id: "정만철", + title: "함께 배우는 플로잉 댄스", + date: "2025-02-01T18:00:00", + price: 10000, + people: 1, + status: "pending", + }, + { + id: "오승환", + title: "내 강아지 인생 사진 찍어주기", + date: "2026-02-11T13:00:00", + price: 35000, + people: 1, + status: "canceled", + }, + { + id: "이대호", + title: "이색 액티비티 체험", + date: "2026-01-10T10:00:00", + price: 60000, + people: 3, + status: "declined", + }, + { + id: "양의지", + title: "별과 함께하는 북촌 체험", + date: "2026-01-14T15:00:00", + price: 40000, + people: 2, + status: "completed", + }, +]; diff --git a/feature/Experience/schedule/DateInput.tsx b/feature/Experience/schedule/DateInput.tsx index e460ac4..f4249d6 100644 --- a/feature/Experience/schedule/DateInput.tsx +++ b/feature/Experience/schedule/DateInput.tsx @@ -2,7 +2,7 @@ import { useState, useRef } from "react"; import CalendarIcon from "@/public/icon_calendar.svg"; import DatePicker from "./Datepicker"; -import { Input } from "@/components/common/input"; +import { Input } from "@/components/input/Input"; import { useClickOutside } from "@/hooks/useClickOutside"; interface Props { diff --git a/feature/MyInfo/MyInfoClient.tsx b/feature/MyInfo/MyInfoClient.tsx index c2ce4dc..72f584e 100644 --- a/feature/MyInfo/MyInfoClient.tsx +++ b/feature/MyInfo/MyInfoClient.tsx @@ -6,7 +6,7 @@ import Sidebar from "@/feature/MyInfo/Sidebar"; import MyInfoView from "@/feature/MyInfo/MyInfoView"; import ReservationView from "@/feature/MyInfo/ReservationView"; import MyExperinenceView from "@/feature/MyInfo/MyExperinenceView"; -import ReservaionStatusView from "@/feature/MyInfo/ReservaionStatusView"; +import ReservationStatusPage from "@/feature/reservationStatus/ReservationStatusPage"; import type { SidebarMenu } from "@/types/SidebarTypes"; const DEFAULT_MENU: SidebarMenu = "MY_INFO"; @@ -15,8 +15,7 @@ export default function MyInfoClient() { const searchParams = useSearchParams(); const router = useRouter(); - const activeMenu = - (searchParams.get("menu") as SidebarMenu) ?? DEFAULT_MENU; + const activeMenu = (searchParams.get("menu") as SidebarMenu) ?? DEFAULT_MENU; const handleMenuChange = (menu: SidebarMenu) => { router.push(`/myinfo?menu=${menu}`); @@ -32,9 +31,7 @@ export default function MyInfoClient() { {activeMenu === "MY_INFO" && } {activeMenu === "RESERVATIONS" && } {activeMenu === "MY_EXPERIENCE" && } - {activeMenu === "RESERVATION_STATUS" && ( - - )} + {activeMenu === "RESERVATION_STATUS" && } diff --git a/feature/MyInfo/ReservaionStatusView.tsx b/feature/MyInfo/ReservaionStatusView.tsx deleted file mode 100644 index 65987cf..0000000 --- a/feature/MyInfo/ReservaionStatusView.tsx +++ /dev/null @@ -1,29 +0,0 @@ -"use client"; - - -type Props = { - mode?: "view" | "edit"; - onEdit?: () => void; - onCancel?: () => void; -}; - -export default function ReservaionStatusView({ - mode = "view", - onEdit, - onCancel, -}: Props) { -// const { data, loading, error } = useMyInfo(); - -// if (loading) return
로딩중...
; -// if (error || !data) return
에러
; - - return ( -
예약현황 들어갈곳
- // - ); -} diff --git a/feature/reservationStatus/Calendar/CalendarCell.tsx b/feature/reservationStatus/Calendar/CalendarCell.tsx new file mode 100644 index 0000000..bb054f1 --- /dev/null +++ b/feature/reservationStatus/Calendar/CalendarCell.tsx @@ -0,0 +1,103 @@ +"use client"; + +import { CalendarDate } from "../types/calendar"; +import { ReservationBadge } from "@/feature/reservationStatus/types/reservationStatus"; +import { cn } from "@/lib/utils/twmerge"; +import { StatusBadge } from "./StatusBadge"; +import { toDateKey } from "@/lib/utils/date"; + +interface Props { + date: CalendarDate; + badges: ReservationBadge[]; + isSelected: boolean; + onSelectDate: ( + key: string, + position: { top: number; left: number; width: number; height: number } + ) => void; + containerRef: React.RefObject; +} + +export default function CalendarCell({ + date, + badges, + isSelected, + onSelectDate, + containerRef, +}: Props) { + const handleClick = (e: React.MouseEvent) => { + if (!date.isCurrentMonth) return; + if (!containerRef.current) return; + + const rect = e.currentTarget.getBoundingClientRect(); + const containerRect = containerRef.current.getBoundingClientRect(); + + const dateKey = toDateKey(date.date); + const MODAL_VERTICAL_OFFSET = 70; + + const position = { + top: + rect.top - + containerRect.top + + rect.height / 2 + + MODAL_VERTICAL_OFFSET, + left: + rect.left - + containerRect.left + + rect.width + + 12, + width: rect.width, + height: rect.height, + }; + + + + onSelectDate(dateKey, position); + }; + + return ( +
+ {/* 날짜 숫자 */} +
+ + {date.day} + + + {/* 예약 있음 표시 (점) */} + {badges.length > 0 && ( + + )} +
+ + {/* 상태 배지 */} +
+ {badges.map((badge) => ( + + ))} +
+
+ ); +} diff --git a/feature/reservationStatus/Calendar/CalendarGrid.tsx b/feature/reservationStatus/Calendar/CalendarGrid.tsx new file mode 100644 index 0000000..1a3eed3 --- /dev/null +++ b/feature/reservationStatus/Calendar/CalendarGrid.tsx @@ -0,0 +1,66 @@ +import { CalendarDate } from "@/feature/reservationStatus/types/calendar"; +import { ReservationMap } from "../utils/mapReservationsToCalendar"; +import { toDateKey } from "@/lib/utils/date"; +import CalendarCell from "./CalendarCell"; +import { useRef } from "react"; + +const DAYS = [ + { id: 0, label: "S" }, + { id: 1, label: "M" }, + { id: 2, label: "T" }, + { id: 3, label: "W" }, + { id: 4, label: "T" }, + { id: 5, label: "F" }, + { id: 6, label: "S" }, +]; + +interface Props { + dates: CalendarDate[]; + badgesMap: ReservationMap; + selectedDateKey: string | null; + onSelectDate: ( + key: string, + position: { top: number; left: number; width: number; height: number } + ) => void; +} + + +export default function CalendarGrid({ + dates, + badgesMap, + selectedDateKey, + onSelectDate, +}: Props) { + const gridRef = useRef(null); + + return ( +
+ {/* 요일 */} +
+ {DAYS.map((day) => ( +
+ {day.label} +
+ ))} +
+ + {/* 날짜 */} +
+ {dates.map((date) => { + const dateKey = toDateKey(date.date); + + return ( + + ); + })} +
+
+ ); +} diff --git a/feature/reservationStatus/Calendar/CalendarHeader.tsx b/feature/reservationStatus/Calendar/CalendarHeader.tsx new file mode 100644 index 0000000..5850f55 --- /dev/null +++ b/feature/reservationStatus/Calendar/CalendarHeader.tsx @@ -0,0 +1,33 @@ +// CalendarHeader.tsx +import ArrowLeft from "@/public/icon_arrow_left.svg" +import ArrowRight from "@/public/icon_arrow_right.svg" + +interface Props { + year: number; + month: number; + onPrev: () => void; + onNext: () => void; +} + +export default function CalendarHeader({ + year, + month, + onPrev, + onNext, +}: Props) { + return ( +
+ + +

+ {year}년 {month + 1}월 +

+ + +
+ ); +} diff --git a/feature/reservationStatus/Calendar/ReservationCalendar.tsx b/feature/reservationStatus/Calendar/ReservationCalendar.tsx new file mode 100644 index 0000000..c4e093f --- /dev/null +++ b/feature/reservationStatus/Calendar/ReservationCalendar.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { getMonthCalendar } from "@/feature/reservationStatus/types/calendar"; +import { mapReservationsToCalendar } from "../utils/mapReservationsToCalendar"; +import { Reservation } from "../types/reservation"; +import CalendarHeader from "./CalendarHeader"; +import CalendarGrid from "./CalendarGrid"; +import { useCalendar } from "@/hooks/useCalendar"; + +interface Props { + reservations: Reservation[]; + selectedDateKey: string | null; + onSelectDate: (key: string, position: { top: number; left: number }) => void; +} + +export default function ReservationCalendar({ + reservations, + selectedDateKey, + onSelectDate, +}: Props) { + const today = new Date(); + + const { year, month, prevMonth, nextMonth } = useCalendar( + today.getFullYear(), + today.getMonth(), + ); + + const calendarDates = getMonthCalendar(year, month); + const reservationMap = mapReservationsToCalendar(reservations); + + return ( +
+ + + +
+ ); +} diff --git a/feature/reservationStatus/Calendar/StatusBadge.tsx b/feature/reservationStatus/Calendar/StatusBadge.tsx new file mode 100644 index 0000000..009ee16 --- /dev/null +++ b/feature/reservationStatus/Calendar/StatusBadge.tsx @@ -0,0 +1,27 @@ +import { + ReservationStatusCode, + RESERVATION_STATUS_LABEL, +} from "@/feature/reservationStatus/types/reservationStatus"; + +interface StatusBadgeProps { + status: ReservationStatusCode; + count: number; +} + +const STYLE_MAP: Record = { + pending: "bg-blue-50 text-blue-500", + confirmed: "bg-orange-50 text-orange-500", + declined: "bg-red-50 text-red-500", + canceled: "bg-gray-100 text-gray-400", + completed: "bg-gray-100 text-gray-500", +}; + +export function StatusBadge({ status, count }: StatusBadgeProps) { + return ( +
+ {RESERVATION_STATUS_LABEL[status]} {count} +
+ ); +} diff --git a/feature/reservationStatus/Category.tsx b/feature/reservationStatus/Category.tsx new file mode 100644 index 0000000..c64c1f5 --- /dev/null +++ b/feature/reservationStatus/Category.tsx @@ -0,0 +1,32 @@ +"use client"; + +import { Reservation } from "./types/reservation"; + +interface Props { + reservations: Reservation[]; + selectedTitle: string | null; + onChange: (title: string | null) => void; +} + +export default function Category({ + reservations, + selectedTitle, + onChange, +}: Props) { + const titles = Array.from(new Set(reservations.map((r) => r.title))); + + return ( + + ); +} diff --git a/feature/reservationStatus/MobileBottomSheet.tsx b/feature/reservationStatus/MobileBottomSheet.tsx new file mode 100644 index 0000000..835f021 --- /dev/null +++ b/feature/reservationStatus/MobileBottomSheet.tsx @@ -0,0 +1,38 @@ +"use client"; + +import ReservationDetailContent from "./ReservationDetailContent"; +import { Reservation } from "./types/reservation"; + +interface Props { + dateKey: string; + reservations: Reservation[]; + onClose: () => void; +} + +export default function ReservationBottomSheet({ + dateKey, + reservations, + onClose, +}: Props) { + return ( + <> + {/* 배경 오버레이 */} +
+ + {/* Bottom Sheet */} +
+ {/* 드래그 핸들 */} +
+ + +
+ + ); +} diff --git a/feature/reservationStatus/ReservaionStatusView.tsx b/feature/reservationStatus/ReservaionStatusView.tsx index e051f3a..fa9a7be 100644 --- a/feature/reservationStatus/ReservaionStatusView.tsx +++ b/feature/reservationStatus/ReservaionStatusView.tsx @@ -63,18 +63,16 @@ export default function ReservationStatusView({ style={{ top: modalPosition.top, left: modalPosition.left, - transform: "translate(-50%, 10px)", }} > { setSelectedDateKey(null); setModalPosition(null); }} - // 위치 좌표 전달 - position={modalPosition} />
)} diff --git a/feature/reservationStatus/ReservationDetailContent.tsx b/feature/reservationStatus/ReservationDetailContent.tsx new file mode 100644 index 0000000..c8280a6 --- /dev/null +++ b/feature/reservationStatus/ReservationDetailContent.tsx @@ -0,0 +1,41 @@ +import { Reservation } from "./types/reservation"; +import useReservationDetail from "@/hooks/useReservationDetail"; +import ReservationHeader from "./ReservationHeader"; +import ReservationTabs from "./ReservationTabs"; +import ReservationTimeFilter from "./ReservationTimeFilter"; +import ReservationList from "./ReservationList"; + +interface Props { + dateKey: string; + reservations: Reservation[]; + onClose: () => void; +} + +export default function ReservationDetailContent(props: Props) { + const { + activeTab, + selectedTime, + setActiveTab, + setSelectedTime, + filteredReservations, + getCount, + handleStatusChange, + } = useReservationDetail(props.reservations); + + return ( + <> + + + + + + ); +} diff --git a/feature/reservationStatus/ReservationHeader.tsx b/feature/reservationStatus/ReservationHeader.tsx new file mode 100644 index 0000000..2800872 --- /dev/null +++ b/feature/reservationStatus/ReservationHeader.tsx @@ -0,0 +1,18 @@ +import DeleteIcon from "@/public/icon_delete.svg"; + +export default function ReservationHeader({ + dateKey, + onClose, +}: { + dateKey: string; + onClose: () => void; +}) { + return ( +
+

{dateKey}

+ +
+ ); +} diff --git a/feature/reservationStatus/ReservationItem.tsx b/feature/reservationStatus/ReservationItem.tsx new file mode 100644 index 0000000..e806222 --- /dev/null +++ b/feature/reservationStatus/ReservationItem.tsx @@ -0,0 +1,74 @@ +import { Reservation,ReservationStatusCode } from "./types/reservation"; +import { STATUS_UI_CONFIG } from "./constants/ReservationUI"; + +interface Props { + reservation: Reservation; + activeTab: ReservationStatusCode; + onStatusChange: ( + id: string | number, + status: ReservationStatusCode + ) => void; +} + +export default function ReservationItem({ + reservation, + activeTab, + onStatusChange, +}: Props) { + return ( +
  • +
    +
    +
    + + 닉네임 + + + {reservation.id} + +
    + +
    + + 인원 + + + {reservation.people}명 + +
    +
    + +
    + {activeTab === "pending" ? ( +
    + + +
    + ) : ( + + {STATUS_UI_CONFIG[reservation.status]?.label} + + )} +
    +
    +
  • + ); +} \ No newline at end of file diff --git a/feature/reservationStatus/ReservationList.tsx b/feature/reservationStatus/ReservationList.tsx new file mode 100644 index 0000000..f76017a --- /dev/null +++ b/feature/reservationStatus/ReservationList.tsx @@ -0,0 +1,40 @@ +import { Reservation,ReservationStatusCode } from "./types/reservation"; +import ReservationItem from "./ReservationItem"; + +interface Props { + reservations: Reservation[]; + activeTab: ReservationStatusCode; + onStatusChange: ( + id: string | number, + status: ReservationStatusCode + ) => void; +} + +export default function ReservationList({ + reservations, + activeTab, + onStatusChange, +}: Props) { + return ( +
    +

    예약 내역

    + + {reservations.length === 0 ? ( +

    + 내역이 없습니다. +

    + ) : ( +
      + {reservations.map((reservation) => ( + + ))} +
    + )} +
    + ); +} \ No newline at end of file diff --git a/feature/reservationStatus/ReservationSidemodal.tsx b/feature/reservationStatus/ReservationSidemodal.tsx new file mode 100644 index 0000000..bb006f5 --- /dev/null +++ b/feature/reservationStatus/ReservationSidemodal.tsx @@ -0,0 +1,24 @@ +import { Reservation } from "./types/reservation"; +import ReservationDetailContent from "./ReservationDetailContent"; + +interface Props { + dateKey: string; + reservations: Reservation[]; + onClose: () => void; +} + +export default function ReservationSideModal({ + dateKey, + reservations, + onClose, +}: Props) { + return ( + + ); +} diff --git a/feature/reservationStatus/ReservationStatusPage.tsx b/feature/reservationStatus/ReservationStatusPage.tsx new file mode 100644 index 0000000..4728203 --- /dev/null +++ b/feature/reservationStatus/ReservationStatusPage.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { useState } from "react"; +import Category from "./Category"; +import ReservationStatusView from "./ReservaionStatusView"; +import { mockReservations } from "@/Mocks/reservationStatus.mock"; + +export default function ReservationStatusPage() { + const [selectedTitle, setSelectedTitle] = useState(null); + + return ( +
    + {/* 헤더 */} +
    +

    예약현황

    +

    + 내 체험에 예약된 내역들을 한 눈에 확인할 수 있습니다. +

    +
    + + {/* 카테고리 */} +
    + +
    + + {/* 달력 */} + +
    + ); +} diff --git a/feature/reservationStatus/ReservationTabs.tsx b/feature/reservationStatus/ReservationTabs.tsx new file mode 100644 index 0000000..4a4ebc2 --- /dev/null +++ b/feature/reservationStatus/ReservationTabs.tsx @@ -0,0 +1,35 @@ +import { ReservationStatusCode } from "./types/reservation"; +import { TABS } from "./constants/ReservationUI"; + +interface Props { + activeTab: ReservationStatusCode; + onChange: (status: ReservationStatusCode) => void; + getCount: (status: ReservationStatusCode) => number; +} + +export default function ReservationTabs({ + activeTab, + onChange, + getCount, +}: Props) { + return ( + + ); +} \ No newline at end of file diff --git a/feature/reservationStatus/ReservationTimeFilter.tsx b/feature/reservationStatus/ReservationTimeFilter.tsx new file mode 100644 index 0000000..6f1827c --- /dev/null +++ b/feature/reservationStatus/ReservationTimeFilter.tsx @@ -0,0 +1,37 @@ +import { TIME_SLOTS } from "./constants/ReservationUI"; + +interface Props { + value: string; + onChange: (value: string) => void; +} + +export default function ReservationTimeFilter({ + value, + onChange, +}: Props) { + return ( +
    + + +
    + + +
    + ▼ +
    +
    +
    + ); +} \ No newline at end of file diff --git a/feature/reservationStatus/constants/ReservationUI.tsx b/feature/reservationStatus/constants/ReservationUI.tsx new file mode 100644 index 0000000..1395d5a --- /dev/null +++ b/feature/reservationStatus/constants/ReservationUI.tsx @@ -0,0 +1,42 @@ +import { ReservationStatusCode } from "../types/reservation"; + +export const TIME_SLOTS = [ + "전체", + ...Array.from({ length: 24 }, (_, i) => { + const start = String(i).padStart(2, "0") + ":00"; + const end = String(i + 1).padStart(2, "0") + ":00"; + return `${start} - ${end}`; + }), +]; + +export const TABS: { label: string; status: ReservationStatusCode }[] = [ + { label: "신청", status: "pending" }, + { label: "승인", status: "confirmed" }, + { label: "거절", status: "declined" }, +]; + +export const STATUS_UI_CONFIG: Record< + ReservationStatusCode, + { label: string; badgeStyle: string } +> = { + pending: { + label: "신청", + badgeStyle: "bg-blue-50 text-blue-600 border-blue-100", + }, + confirmed: { + label: "예약 승인", + badgeStyle: "bg-cyan-50 text-cyan-500 border-cyan-100", + }, + declined: { + label: "예약 거절", + badgeStyle: "bg-red-50 text-red-400 border-red-100", + }, + canceled: { + label: "예약 취소", + badgeStyle: "bg-gray-100 text-gray-500 border-gray-200", + }, + completed: { + label: "방문 완료", + badgeStyle: "bg-blue-50 text-blue-600 border-blue-100", + }, +} as const; diff --git a/feature/reservationStatus/constants/statusMap.ts b/feature/reservationStatus/constants/statusMap.ts new file mode 100644 index 0000000..c0f6abf --- /dev/null +++ b/feature/reservationStatus/constants/statusMap.ts @@ -0,0 +1,13 @@ +import { ReservationStatusCode } from "../types/reservation"; +import { ReservationStatusLabel } from "../types/reservationStatus"; + +export const STATUS_TO_CALENDAR: Record< + ReservationStatusCode, + ReservationStatusLabel | null +> = { + pending: "예약", + confirmed: "승인", + completed: "완료", + declined: null, + canceled: null, +}; diff --git a/feature/reservationStatus/types/calendar.ts b/feature/reservationStatus/types/calendar.ts new file mode 100644 index 0000000..8baff1a --- /dev/null +++ b/feature/reservationStatus/types/calendar.ts @@ -0,0 +1,48 @@ +export interface CalendarDate { + date: Date; + day: number; + isCurrentMonth: boolean; + isToday: boolean; +} + +export function getMonthCalendar(year: number, month: number): CalendarDate[] { + const result: CalendarDate[] = []; + const today = new Date(); + const todayReset = new Date(today.getFullYear(), today.getMonth(), today.getDate()).getTime(); + + const firstDayOfMonth = new Date(year, month, 1); + const lastDayOfMonth = new Date(year, month + 1, 0); + + const startDayOfWeek = firstDayOfMonth.getDay(); + const totalDays = lastDayOfMonth.getDate(); + + for (let i = startDayOfWeek; i > 0; i--) { + const date = new Date(year, month, 1 - i); + result.push(formatDateObject(date, false, todayReset)); + } + + for (let day = 1; day <= totalDays; day++) { + const date = new Date(year, month, day); + result.push(formatDateObject(date, true, todayReset)); + } + + const remainingSlots = 7 - (result.length % 7); + if (remainingSlots < 7) { + for (let i = 1; i <= remainingSlots; i++) { + const date = new Date(year, month + 1, i); + result.push(formatDateObject(date, false, todayReset)); + } + } + + return result; +} + +// 헬퍼 함수: 반복적인 객체 생성을 깔끔하게 분리 +function formatDateObject(date: Date, isCurrentMonth: boolean, todayTime: number): CalendarDate { + return { + date, + day: date.getDate(), + isCurrentMonth, + isToday: new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime() === todayTime, + }; +} \ No newline at end of file diff --git a/feature/reservationStatus/types/reservation.ts b/feature/reservationStatus/types/reservation.ts new file mode 100644 index 0000000..388c118 --- /dev/null +++ b/feature/reservationStatus/types/reservation.ts @@ -0,0 +1,16 @@ +// feature/reservation/types/reservation.ts +export type ReservationStatusCode = + | "pending" + | "confirmed" + | "declined" + | "canceled" + | "completed"; + +export interface Reservation { + id: number | string; + title: string; + date: string; + price: number; + people: number; + status: ReservationStatusCode; +} diff --git a/feature/reservationStatus/types/reservationStatus.ts b/feature/reservationStatus/types/reservationStatus.ts new file mode 100644 index 0000000..2f00d3c --- /dev/null +++ b/feature/reservationStatus/types/reservationStatus.ts @@ -0,0 +1,25 @@ +export type ReservationStatusCode = + | "pending" + | "confirmed" + | "declined" + | "canceled" + | "completed"; + +export type ReservationStatusLabel = "신청" | "승인" | "거절" | "취소" | "완료"; + +export const RESERVATION_STATUS_LABEL: Record< + ReservationStatusCode, + ReservationStatusLabel +> = { + pending: "신청", + confirmed: "승인", + declined: "거절", + canceled: "취소", + completed: "완료", +}; + +export interface ReservationBadge { + id: string; + status: ReservationStatusCode; + count: number; +} diff --git a/feature/reservationStatus/utils/mapReservationsToCalendar.ts b/feature/reservationStatus/utils/mapReservationsToCalendar.ts new file mode 100644 index 0000000..b780684 --- /dev/null +++ b/feature/reservationStatus/utils/mapReservationsToCalendar.ts @@ -0,0 +1,36 @@ +import dayjs from "dayjs"; +import { Reservation } from "../types/reservation"; +import { ReservationBadge } from + "@/feature/reservationStatus/types/reservationStatus"; + +export type ReservationMap = Record; + +export function mapReservationsToCalendar( + reservations: Reservation[] +): ReservationMap { + const map: ReservationMap = {}; + + reservations.forEach((reservation) => { + const dateKey = dayjs(reservation.date).format("YYYY-MM-DD"); + + if (!map[dateKey]) { + map[dateKey] = []; + } + + const existing = map[dateKey].find( + (badge) => badge.status === reservation.status + ); + + if (existing) { + existing.count += 1; + } else { + map[dateKey].push({ + id: `${dateKey}-${reservation.status}`, + status: reservation.status, + count: 1, + }); + } + }); + + return map; +} diff --git a/hooks/useCalendar.ts b/hooks/useCalendar.ts new file mode 100644 index 0000000..218bba8 --- /dev/null +++ b/hooks/useCalendar.ts @@ -0,0 +1,25 @@ +import { useState } from "react"; + +export function useCalendar(initialYear: number, initialMonth: number) { + const [current, setCurrent] = useState({ + year: initialYear, + month: initialMonth, + }); + + const changeMonth = (delta: number) => { + setCurrent((prev) => { + const date = new Date(prev.year, prev.month + delta, 1); + return { + year: date.getFullYear(), + month: date.getMonth(), + }; + }); + }; + + return { + year: current.year, + month: current.month, + prevMonth: () => changeMonth(-1), + nextMonth: () => changeMonth(1), + }; +} diff --git a/hooks/useIsCompact.ts b/hooks/useIsCompact.ts new file mode 100644 index 0000000..59b54f7 --- /dev/null +++ b/hooks/useIsCompact.ts @@ -0,0 +1,17 @@ +import { useEffect, useState } from "react"; + +export function useIsCompact() { + const [isCompact, setIsCompact] = useState(false); + + useEffect(() => { + const check = () => { + setIsCompact(window.innerWidth < 1024); + }; + + check(); + window.addEventListener("resize", check); + return () => window.removeEventListener("resize", check); + }, []); + + return isCompact; +} diff --git a/hooks/useReservationDetail.ts b/hooks/useReservationDetail.ts new file mode 100644 index 0000000..ef77691 --- /dev/null +++ b/hooks/useReservationDetail.ts @@ -0,0 +1,47 @@ +import { useState } from "react"; +import { + Reservation, + ReservationStatusCode, +} from "@/feature/reservationStatus/types/reservation"; + +export default function useReservationDetail( + reservations: Reservation[] +) { + const [localReservations, setLocalReservations] = + useState(reservations); + + const [activeTab, setActiveTab] = + useState("pending"); + + const [selectedTime, setSelectedTime] = useState("전체"); + + const handleStatusChange = ( + id: string | number, + status: ReservationStatusCode + ) => { + setLocalReservations((prev) => + prev.map((r) => (r.id === id ? { ...r, status } : r)) + ); + }; + + const filteredReservations = localReservations.filter((r) => { + if (r.status !== activeTab) return false; + if (selectedTime === "전체") return true; + + const hour = new Date(r.date).getHours(); + return hour === Number(selectedTime.split(":")[0]); + }); + + const getCount = (status: ReservationStatusCode) => + localReservations.filter((r) => r.status === status).length; + + return { + activeTab, + selectedTime, + setActiveTab, + setSelectedTime, + filteredReservations, + getCount, + handleStatusChange, + }; +} diff --git a/lib/utils/date.ts b/lib/utils/date.ts new file mode 100644 index 0000000..aa2a7c3 --- /dev/null +++ b/lib/utils/date.ts @@ -0,0 +1,6 @@ +import dayjs from "dayjs"; + +export function toDateKey(date: Date) { + return dayjs(date).format("YYYY-MM-DD") + +} diff --git a/lib/utils/dayjs.ts b/lib/utils/dayjs.ts new file mode 100644 index 0000000..8a6da8d --- /dev/null +++ b/lib/utils/dayjs.ts @@ -0,0 +1,10 @@ +import dayjs from "dayjs"; +import isSameOrBefore from "dayjs/plugin/isSameOrBefore"; +import isSameOrAfter from "dayjs/plugin/isSameOrAfter"; +import weekday from "dayjs/plugin/weekday"; + +dayjs.extend(isSameOrBefore); +dayjs.extend(isSameOrAfter); +dayjs.extend(weekday); + +export default dayjs; diff --git a/package-lock.json b/package-lock.json index eaac6ff..69792e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "axios": "^1.13.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "dayjs": "^1.11.19", "next": "16.1.0", "react": "19.2.3", "react-datepicker": "^9.1.0", @@ -3819,6 +3820,12 @@ "integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==", "license": "MIT" }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", diff --git a/package.json b/package.json index a1ac4ff..39bd1f2 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "axios": "^1.13.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "dayjs": "^1.11.19", "next": "16.1.0", "react": "19.2.3", "react-datepicker": "^9.1.0",