diff --git a/apps/web/api/items.ts b/apps/web/api/items.ts index 20ba7487..35c65b0a 100644 --- a/apps/web/api/items.ts +++ b/apps/web/api/items.ts @@ -1,7 +1,31 @@ import { API_ENDPOINTS } from "@repo/constants"; -import { type ISeat } from "@repo/types"; +import { + type TBaseItem, + type TItemType, + type IRoom, + type ISeat, + type IEquipment, + type MessageResponse, +} from "@repo/types"; import { axiosRequester } from "@/lib/axios"; +// 특정 타입의 아이템 조회 +interface GetAllItemsParams { + itemType: TItemType; +} + +export const getAllItems = async (params: GetAllItemsParams): Promise => { + const { itemType } = params; + const { data } = await axiosRequester({ + options: { + method: "GET", + url: API_ENDPOINTS.ITEMS.GET_ALL(itemType), + }, + }); + + return data; +}; + /** * 모든 좌석 데이터를 가져옵니다. * @returns 모든 좌석 데이터를 포함하는 Promise. @@ -17,6 +41,59 @@ export const getAllSeats = async (): Promise => { return data; }; +// 아이템 생성 +interface CreateItemParams { + itemType: TItemType; + itemData: Partial; +} + +export const createItem = async (params: CreateItemParams): Promise => { + const { itemType, itemData } = params; + const { data } = await axiosRequester({ + options: { + method: "POST", + url: API_ENDPOINTS.ITEMS.CREATE_ITEM, + data: { + itemType, + ...itemData, + }, + }, + }); + + return data; +}; + +// 아이템 업데이트 +interface UpdateItemParams { + itemId: string; + itemData: Partial; +} + +export const updateItem = async (params: UpdateItemParams): Promise => { + const { itemId, itemData } = params; + const { data } = await axiosRequester({ + options: { + method: "PATCH", + url: API_ENDPOINTS.ITEMS.UPDATE_ITEM(itemId), + data: itemData, + }, + }); + + return data; +}; + +// 아이템 삭제 +export const deleteItem = async (itemId: string): Promise => { + const { data } = await axiosRequester({ + options: { + method: "DELETE", + url: API_ENDPOINTS.ITEMS.DELETE_ITEM(itemId), + }, + }); + + return data; +}; + /** * 특정 아이템 데이터를 수정합니다. * @returns 수정 결과 메시지를 포함하는 Promise. @@ -29,5 +106,6 @@ export const patchItem = async (itemId: string, formData: FormData): Promise => { + const { userId } = params; + const { data } = await axiosRequester({ + options: { + method: "GET", + url: API_ENDPOINTS.RESERVATION.GET_USER_RESERVATIONS(userId), + }, + }); + + return data; +}; + +// 아이템 타입 및 날짜에 대한 예약 조회 +interface GetReservationsByTypeAndDateParams { + itemType: "room" | "seat" | "equipment"; + date: string; + status?: TReservationStatus; +} + +export const getReservationsByTypeAndDate = async ( + params: GetReservationsByTypeAndDateParams, +): Promise => { + const { itemType, date, status } = params; + const { data } = await axiosRequester({ + options: { + method: "GET", + url: API_ENDPOINTS.RESERVATION.GET_RESERVATIONS_BY_TYPE_AND_DATE(itemType, date), + params: { + status, + }, + }, + }); + + return data; +}; + +// 예약 생성 +export interface CreateReservationParams { + itemId: string; + savedReservation: IReservation; +} + +export interface CreateReservationRequest { + userId: string; + itemType: "room"; + startAt: string; + endAt: string; + status: "reserved"; + notes: string; + attendees: string[]; +} + +export interface CreateReservationResponse { + message: string; + savedReservation: IReservation; +} + +export const createReservation = async ( + itemId: string, + reservationData: CreateReservationRequest, +): Promise => { + const { data } = await axiosRequester({ + options: { + method: "POST", + url: API_ENDPOINTS.RESERVATION.CREATE_RESERVATION(itemId), + data: reservationData, + }, + }); + + return data.savedReservation; +}; + +export interface UpdateReservationRequest { + startAt?: string; + endAt?: string; + status?: TReservationStatus; + notes?: string; + attendees?: string[]; +} + +export interface UpdateReservationResponse { + message: string; + updatedReservation: IReservation; +} + +export const updateReservation = async ( + reservationId: string, + reservationData: UpdateReservationRequest, +): Promise => { + const { data } = await axiosRequester({ + options: { + method: "PATCH", + url: API_ENDPOINTS.RESERVATION.UPDATE_RESERVATION(reservationId), + data: reservationData, + }, + }); + + return data.updatedReservation; +}; + +export const deleteReservation = async (reservationId: string): Promise => { + await axiosRequester({ + options: { + method: "DELETE", + url: API_ENDPOINTS.RESERVATION.DELETE_RESERVATION(reservationId), + }, + }); +}; diff --git a/apps/web/app/_hooks/useCurrentTimePosition.ts b/apps/web/app/_hooks/useCurrentTimePosition.ts new file mode 100644 index 00000000..e9ee2a18 --- /dev/null +++ b/apps/web/app/_hooks/useCurrentTimePosition.ts @@ -0,0 +1,54 @@ +import { useEffect, useState } from "react"; + +interface UseCurrentTimePositionProps { + slotWidth: number; + startHour: number; + endHour: number; +} + +interface UseCurrentTimePositionReturn { + currentPosition: number | null; + currentTime: string; +} + +export function useCurrentTimePosition({ + slotWidth, + startHour, + endHour, +}: UseCurrentTimePositionProps): UseCurrentTimePositionReturn { + const [currentPosition, setCurrentPosition] = useState(null); + const [currentTime, setCurrentTime] = useState(""); + + useEffect(() => { + const updatePosition = (): void => { + const now = new Date(); + const currentMinutes = now.getHours() * 60 + now.getMinutes(); + const totalMinutes = (endHour - startHour) * 60; + + if (currentMinutes < startHour * 60 || currentMinutes >= endHour * 60) { + setCurrentPosition(null); + setCurrentTime(""); + return; + } + + const relativeMinutes = currentMinutes - startHour * 60; + const scheduleWidth = slotWidth * (endHour - startHour) * 2; // Assuming 30-minute slots + const position = (relativeMinutes / totalMinutes) * scheduleWidth; + + setCurrentPosition(position); + + const hours = String(now.getHours()).padStart(2, "0"); + const minutes = String(now.getMinutes()).padStart(2, "0"); + setCurrentTime(`${hours}:${minutes}`); + }; + + updatePosition(); + const interval = setInterval(updatePosition, 60000); + + return () => { + clearInterval(interval); + }; + }, [slotWidth, startHour, endHour]); + + return { currentPosition, currentTime }; +} diff --git a/apps/web/app/_hooks/useSlotReservations.ts b/apps/web/app/_hooks/useSlotReservations.ts new file mode 100644 index 00000000..86709bbc --- /dev/null +++ b/apps/web/app/_hooks/useSlotReservations.ts @@ -0,0 +1,40 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { useMemo } from "react"; +import { type IReservation } from "@repo/types"; +import { timeToMinutes } from "../utils/timeToMinutes"; + +/** + * 예약 데이터를 슬롯 인덱스에 매핑하는 커스텀 훅 + * @param schedules 예약 데이터 배열 + * @param startHour 시작 시간 + * @param endHour 종료 시간 + * @param minutesPerSlot 슬롯당 분 단위 + * @returns 슬롯별 예약 상태 배열 + */ +export const useSlotReservations = ( + schedules: IReservation[], + startHour: number, + endHour: number, + minutesPerSlot: number, +): (IReservation | null)[] => { + return useMemo(() => { + const totalSlots = (endHour - startHour) * 2; // 30분 단위 + const slotReservations: (IReservation | null)[] = Array(totalSlots).fill(null); + + schedules.forEach((schedule) => { + const startMinutes = timeToMinutes(schedule.startAt) - startHour * 60; + const endMinutes = timeToMinutes(schedule.endAt) - startHour * 60; + + const startIndex = Math.floor(startMinutes / minutesPerSlot); + const endIndex = Math.ceil(endMinutes / minutesPerSlot); + + for (let i = startIndex; i < endIndex; i++) { + if (i >= 0 && i < totalSlots) { + slotReservations[i] = schedule; + } + } + }); + + return slotReservations; + }, [schedules, startHour, endHour, minutesPerSlot]); +}; diff --git a/apps/web/app/api/reservation.ts b/apps/web/app/api/reservation.ts deleted file mode 100644 index bb40fe46..00000000 --- a/apps/web/app/api/reservation.ts +++ /dev/null @@ -1,65 +0,0 @@ -// apps/web/app/api/reservations.ts -import { type IReservation, type TReservationStatus } from "@repo/types/src/reservationType"; -import { axiosRequester } from "@/lib/axios"; - -// 특정 유저의 오늘 날짜 예약 전체 조회 -interface GetUserReservationsParams { - userId: string; -} - -export const getUserReservations = async (params: GetUserReservationsParams): Promise => { - const { userId } = params; - const { data } = await axiosRequester({ - options: { - method: "GET", - url: `/dashboard/${userId}`, - }, - }); - - return data; -}; - -// 아이템 타입 및 날짜에 대한 예약 조회 -interface GetReservationsByTypeAndDateParams { - itemType: "room" | "seat" | "equipment"; - date?: string; // YYYY-MM-DD 형식 - status?: TReservationStatus; -} - -export const getReservationsByTypeAndDate = async ( - params: GetReservationsByTypeAndDateParams, -): Promise => { - const { itemType, date, status } = params; - const { data } = await axiosRequester({ - options: { - method: "GET", - url: `/${itemType}`, - params: { - date, - status, - }, - }, - }); - - return data; -}; - -// 예약 생성 -interface CreateReservationParams { - itemId: string; - savedReservation: IReservation; - message: string; -} - -export const createReservation = async (params: CreateReservationParams): Promise => { - const { itemId, ...data } = params; - const { data: reservation } = await axiosRequester<{ message: string; savedReservation: IReservation }>({ - options: { - method: "POST", - url: `/${itemId}`, - data, - }, - }); - - return reservation.savedReservation; -}; diff --git a/apps/web/app/constants/meetingRoomsType.ts b/apps/web/app/constants/meetingRoomsType.ts new file mode 100644 index 00000000..080b4570 --- /dev/null +++ b/apps/web/app/constants/meetingRoomsType.ts @@ -0,0 +1 @@ +export const MEETING_ROOMS_TYPE = "room"; diff --git a/apps/web/app/constants/reservationConstants.ts b/apps/web/app/constants/reservationConstants.ts new file mode 100644 index 00000000..9813f9f3 --- /dev/null +++ b/apps/web/app/constants/reservationConstants.ts @@ -0,0 +1,35 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +export enum ModalType { + CREATE_UPDATE = "create/update", + DELETE = "delete", +} + +export const NOTIFICATION_MESSAGES = { + create: "회의실이 예약되었습니다.", + update: "예약이 수정되었습니다.", + delete: "예약이 삭제되었습니다.", +}; + +export const QUERY_KEYS = { + meetings: (date: string, type: string) => ["meetings", date, type] as const, +}; + +export const MODAL_TEXT = { + titles: { + [ModalType.CREATE_UPDATE]: "회의실을 예약하시겠어요?", + [ModalType.DELETE]: "예약을 삭제하시겠어요?", + edit: "회의실 예약을 수정하시겠어요?", + }, + contents: { + [ModalType.CREATE_UPDATE]: "선택한 시간대의 회의실이 예약됩니다.", + edit: "선택한 시간대의 회의실 예약이 수정됩니다.", + [ModalType.DELETE]: "선택한 예약이 삭제됩니다.", + }, + confirmButtonNames: { + [ModalType.CREATE_UPDATE]: "예약하기", + edit: "수정하기", + [ModalType.DELETE]: "삭제하기", + }, + cancelButtonName: "취소하기", +}; diff --git a/apps/web/app/constants/reservationFormConstants.ts b/apps/web/app/constants/reservationFormConstants.ts new file mode 100644 index 00000000..e0293c49 --- /dev/null +++ b/apps/web/app/constants/reservationFormConstants.ts @@ -0,0 +1,34 @@ +export const FORM_LABELS = { + meetingTitle: "미팅 제목", + meetingTitlePlaceholder: "미팅 제목을 입력해주세요.", + selectRoom: "회의실 선택", + selectStartTime: "시작 시간", + selectEndTime: "종료 시간", + selectAttendees: "참여자 선택", +}; + +export const BUTTON_TEXT = { + delete: "삭제하기", + update: "수정하기", + create: "예약하기", + cancel: "취소하기", +}; + +export const ERROR_MESSAGES = { + meetingTitleRequired: "미팅 제목을 입력해주세요.", + timeRequired: "시작 시간과 종료 시간을 모두 선택해주세요.", + endTimeMinimum: "종료 시간은 시작 시간보다 최소 30분 이후여야 합니다.", + timeOverlap: "선택한 시간에 이미 예약이 있습니다.", + userFetchError: "사용자 데이터를 불러오는 데 실패했습니다.", + roomFetchError: "회의실 정보를 불러오는 데 실패했습니다.", +}; + +export const QUERY_KEYS = { + meetings: (date: string, type: string) => ["meetings", date, type] as const, + rooms: "Rooms", + allUsers: "AllUsers", +}; + +export const TIME_INTERVAL = { + minimumDifference: 30, +}; diff --git a/apps/web/app/constants/timeOptions.ts b/apps/web/app/constants/timeOptions.ts index 696af9c7..ccf2d5d8 100644 --- a/apps/web/app/constants/timeOptions.ts +++ b/apps/web/app/constants/timeOptions.ts @@ -47,5 +47,4 @@ export const timeOptions = [ "22:30", "23:00", "23:30", - "23:40", ]; diff --git a/apps/web/app/meetings/_components/MeetingRoomSchedule.tsx b/apps/web/app/meetings/_components/MeetingRoomSchedule.tsx new file mode 100644 index 00000000..caa2bf0e --- /dev/null +++ b/apps/web/app/meetings/_components/MeetingRoomSchedule.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { type IReservation, type TBaseItem } from "@repo/types"; +import { useQuery } from "@tanstack/react-query"; +import { useDateStore } from "@/app/store/useDateStore"; +import { MEETING_ROOMS_TYPE } from "@/app/constants/meetingRoomsType"; +import { formatDate } from "@/app/utils/formatDate"; +import { getAllItems } from "@/api/items"; +import { getReservationsByTypeAndDate } from "@/api/reservations"; +import ScheduleTable from "./Schedule/ScheduleTable"; +import MeetingsSkeleton from "./skeleton"; + +export default function MeetingRoomSchedule(): JSX.Element { + const { selectedDate } = useDateStore(); + + const formattedDate = formatDate(selectedDate); + + const { data: meetingsData = [], isLoading: meetingsIsLoading } = useQuery({ + queryKey: ["meetings", formattedDate, MEETING_ROOMS_TYPE], + queryFn: () => getReservationsByTypeAndDate({ itemType: MEETING_ROOMS_TYPE, date: formattedDate }), + }); + + const { data: roomsData = [], isLoading: roomsIsLoading } = useQuery({ + queryKey: ["Rooms", MEETING_ROOMS_TYPE], + queryFn: () => getAllItems({ itemType: MEETING_ROOMS_TYPE }), + }); + + if (meetingsIsLoading || roomsIsLoading) return ; + + return ; +} diff --git a/apps/web/app/meetings/_components/Reservation/DesktopReservationSheet.tsx b/apps/web/app/meetings/_components/Reservation/DesktopReservationSheet.tsx index 6034b4ec..9c4f9db0 100644 --- a/apps/web/app/meetings/_components/Reservation/DesktopReservationSheet.tsx +++ b/apps/web/app/meetings/_components/Reservation/DesktopReservationSheet.tsx @@ -1,31 +1,21 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -"use client"; - -import { useEffect, useState } from "react"; -import Sidebar from "@/components/common/Sidebar"; -import { type ScheduleFormData, type Schedule } from "@/app/types/scheduletypes"; +import { useEffect } from "react"; +import { type IReservation } from "@repo/types"; import { useSidebarStore } from "@/app/store/useSidebarStore"; -import ReservationForm from "./ReservationForm"; -import ReservationModal from "./ReservationModal"; +import Sidebar from "@/components/common/Sidebar"; +import { type SelectedRoom } from "@/app/types/scheduletypes"; +import ReservationSheetContent from "./ReservationSheetContent"; interface DesktopReservationSheetProps { onClose: () => void; selectedTime: string; - selectedSchedule?: Schedule | null; - selectedRoom: string; + selectedSchedule?: IReservation | null; + selectedRoom?: SelectedRoom | null; } export default function DesktopReservationSheet(props: DesktopReservationSheetProps): JSX.Element { const { onClose, selectedTime, selectedSchedule, selectedRoom } = props; - const { isSidebarOpen, closeSidebar } = useSidebarStore(); - const [isModalOpen, setIsModalOpen] = useState(false); - - const handleSubmit = (data: ScheduleFormData): void => { - setIsModalOpen(true); - }; - useEffect(() => { if (!isSidebarOpen) { closeSidebar(); @@ -41,23 +31,13 @@ export default function DesktopReservationSheet(props: DesktopReservationSheetPr onClose(); }} > - - { - setIsModalOpen(false); - }} - onConfirm={() => { - setIsModalOpen(false); - onClose(); - }} - /> ); } diff --git a/apps/web/app/meetings/_components/Reservation/MobileReservationSheet.tsx b/apps/web/app/meetings/_components/Reservation/MobileReservationSheet.tsx index 2e6db1c0..01cad395 100644 --- a/apps/web/app/meetings/_components/Reservation/MobileReservationSheet.tsx +++ b/apps/web/app/meetings/_components/Reservation/MobileReservationSheet.tsx @@ -1,57 +1,33 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ import { Sheet } from "react-modal-sheet"; -import { useState } from "react"; -import { type ScheduleFormData, type Schedule } from "@/app/types/scheduletypes"; -import ReservationForm from "./ReservationForm"; -import ReservationModal from "./ReservationModal"; +import { type IReservation } from "@repo/types"; +import { type SelectedRoom } from "@/app/types/scheduletypes"; +import ReservationSheetContent from "./ReservationSheetContent"; interface MobileReservationSheetProps { isOpen: boolean; onClose: () => void; selectedTime: string; - selectedSchedule?: Schedule | null; - selectedRoom: string; + selectedSchedule?: IReservation | null; + selectedRoom?: SelectedRoom | null; } -export default function MobileReservationSheet({ - isOpen, - onClose, - selectedTime, - selectedSchedule, - selectedRoom, -}: MobileReservationSheetProps): JSX.Element { - const [isModalOpen, setIsModalOpen] = useState(false); - - const handleSubmit = (data: ScheduleFormData): void => { - setIsModalOpen(true); - }; +export default function MobileReservationSheet(props: MobileReservationSheetProps): JSX.Element { + const { isOpen, onClose, selectedTime, selectedSchedule, selectedRoom } = props; return ( - <> - - - - - - - - - - { - setIsModalOpen(false); - }} - onConfirm={() => { - setIsModalOpen(false); - onClose(); - }} - /> - + + + + + + + + + ); } diff --git a/apps/web/app/meetings/_components/Reservation/ReservationForm.tsx b/apps/web/app/meetings/_components/Reservation/ReservationForm.tsx index 6726f9e8..71d56fd6 100644 --- a/apps/web/app/meetings/_components/Reservation/ReservationForm.tsx +++ b/apps/web/app/meetings/_components/Reservation/ReservationForm.tsx @@ -1,48 +1,80 @@ "use client"; -import Input from "@ui/src/components/common/Input"; -import Dropdown from "@ui/src/components/common/Dropdown"; -import { useForm, Controller } from "react-hook-form"; -import Button from "@ui/src/components/common/Button"; -import MultiSelectDropdown from "@repo/ui/src/components/common/Dropdown/MultiSelectDropdown"; -import { useEffect } from "react"; -import { timeOptions } from "@/app/constants/timeOptions"; -import Profile from "@/components/common/Profile"; -import { type ScheduleFormData, type Schedule } from "@/app/types/scheduletypes"; - -interface ReservationFormProps { - onSubmit: (data: ScheduleFormData) => void; - selectedTime: string; - selectedSchedule?: Schedule | null; - resetTrigger?: number; - selectedRoom?: string | null; -} +import { useEffect, useState } from "react"; +import { useForm, type SubmitHandler } from "react-hook-form"; +import { useQuery } from "@tanstack/react-query"; +import { format, parse, differenceInMinutes, addMinutes } from "date-fns"; +import { type TBaseItem, type IReservation } from "@repo/types"; +import { Button } from "@ui/index"; +import { useAuthStore } from "@/src/stores/useAuthStore"; +import { useDateStore } from "@/app/store/useDateStore"; +import { getAllItems } from "@/api/items"; +import { getReservationsByTypeAndDate, type CreateReservationRequest } from "@/api/reservations"; +import { BUTTON_TEXT, ERROR_MESSAGES } from "@/app/constants/reservationFormConstants"; +import { MEETING_ROOMS_TYPE } from "@/app/constants/meetingRoomsType"; +import { formatDate } from "@/app/utils/formatDate"; +import { type SelectedRoom } from "@/app/types/scheduletypes"; +import { type ReservationFormProps } from "@/app/types/ReservationFormTypes"; +import { validateEndAt } from "@/app/utils/validateTime"; +import { getMembers } from "@/api/members"; +import { AttendeesMultiSelect } from "./ReservationForm/AttendeesMultiSelect"; +import { DeleteButton } from "./ReservationForm/DeleteButton"; +import { NotesInput } from "./ReservationForm/NotesInput"; +import { RoomDropdown } from "./ReservationForm/RoomDropdown"; +import { TimeSelectors } from "./ReservationForm/TimeSelectors"; + +export function ReservationForm({ + onSubmit, + selectedTime, + resetTrigger, + selectedRoom, + selectedSchedule, + onDelete, +}: ReservationFormProps): JSX.Element { + const user = useAuthStore((state) => state.user); + const isEditMode = selectedSchedule && selectedSchedule.user._id === user?._id; + + const { selectedDate } = useDateStore(); -const addMinutes = (time: string, minutesToAdd: number): string => { - const parts = time.split(":"); + const formattedDate = formatDate(selectedDate); - if (parts.length !== 2) { - throw new Error("Invalid time format. Expected format HH:MM."); - } + const { data: meetingsData = [] } = useQuery({ + queryKey: ["meetings", formattedDate, MEETING_ROOMS_TYPE], + queryFn: () => getReservationsByTypeAndDate({ itemType: MEETING_ROOMS_TYPE, date: formattedDate }), + }); - const [hoursStr, minutesStr] = parts; - const hours = Number(hoursStr); - const minutes = Number(minutesStr); + const { data: roomsData = [] } = useQuery({ + queryKey: ["Rooms", MEETING_ROOMS_TYPE], + queryFn: () => getAllItems({ itemType: MEETING_ROOMS_TYPE }), + }); - if (isNaN(hours) || isNaN(minutes)) { - throw new Error("Invalid time format. Hours and minutes must be numbers."); - } + const { + data: allUsersData, + isLoading: allUsersIsLoading, + isError: allUsersIsError, + } = useQuery({ + queryKey: ["members", "newest"], + queryFn: () => + getMembers({ + selectedSort: "newest", + }), + }); - const totalMinutes = hours * 60 + minutes + minutesToAdd; - const newHours = Math.floor(totalMinutes / 60) % 24; - const newMinutes = totalMinutes % 60; - const formattedHours = newHours.toString().padStart(2, "0"); - const formattedMinutes = newMinutes.toString().padStart(2, "0"); - return `${formattedHours}:${formattedMinutes}`; -}; + const getDefaultEndAt = (startAt: string): string => { + const start = parse(startAt, "HH:mm", new Date()); + const end = addMinutes(start, 30); + return format(end, "HH:mm"); + }; -export default function ReservationForm(props: ReservationFormProps): JSX.Element { - const { onSubmit, selectedTime, resetTrigger, selectedRoom } = props; + const defaultValues: CreateReservationRequest = { + userId: user?._id ?? "", + itemType: "room", + notes: selectedSchedule?.notes ?? "", + startAt: selectedSchedule ? format(new Date(selectedSchedule.startAt), "HH:mm") : selectedTime, + endAt: selectedSchedule ? format(new Date(selectedSchedule.endAt), "HH:mm") : getDefaultEndAt(selectedTime), + status: "reserved", + attendees: [], + }; const { control, @@ -50,268 +82,189 @@ export default function ReservationForm(props: ReservationFormProps): JSX.Elemen watch, formState: { errors, isValid }, reset, + getValues, + trigger, setError, setValue, clearErrors, - } = useForm({ - defaultValues: { - meetingTitle: "", - selectedRoom: selectedRoom ?? "", - startTime: selectedTime, - customStartTime: "", - endTime: "", - customEndTime: "", - participants: [], - }, + } = useForm({ + defaultValues, + mode: "onChange", }); - const rooms = ["미팅룸 A", "미팅룸 B", "미팅룸 C", "미팅룸 D", "미팅룸 E", "녹음실 A", "녹음실 B", "녹음실 C"]; - const mockParticipants = [ - "배영준", - "조현지", - "김보경", - "신승헌", - "소혜린", - "이대양", - "이영훈", - "이정민", - "이지현", - "천권희", - ]; - - const startTimeValue = watch("startTime"); - const customStartTimeValue = watch("customStartTime"); - const endTimeValue = watch("endTime"); - const participantsSelected = watch("participants").length > 0; - - // resetTrigger가 변경될 때마다 폼을 리셋 - useEffect(() => { - reset({ - meetingTitle: "", - selectedRoom: selectedRoom ?? "", // selectedRoom 포함 - startTime: selectedTime, - customStartTime: "", - endTime: "", - customEndTime: "", - participants: [], + const validateEndAtFunction = (endAt: string): boolean | string => { + return validateEndAt({ + endAt, + getValues, + selectedMeetingRoom, + selectedDate, + meetingsData, + selectedReservationId: selectedSchedule?._id, // 추가된 부분 }); - }, [resetTrigger, reset, selectedTime, selectedRoom]); + }; - // startTime 또는 customStartTime이 변경될 때 endTime을 설정 - useEffect(() => { - let currentStartTime = selectedTime; + const [selectedMeetingRoom, setSelectedMeetingRoom] = useState(selectedRoom); - if (startTimeValue === "custom-start" && customStartTimeValue) { - currentStartTime = customStartTimeValue; - } else if (startTimeValue && startTimeValue !== "custom-start") { - currentStartTime = startTimeValue; + const attendeesSelected = watch("attendees").length > 0; + const startAtValue = watch("startAt"); + const endAtValue = watch("endAt"); + + useEffect(() => { + if (allUsersData) { + const newEndAt = selectedSchedule + ? format(new Date(selectedSchedule.endAt), "HH:mm") + : getDefaultEndAt(selectedTime); + const attendeeNames = selectedSchedule?.attendees + ? selectedSchedule.attendees.map((attendee) => attendee.name) + : []; + reset({ + ...defaultValues, + endAt: newEndAt, + attendees: attendeeNames, + }); } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [resetTrigger, reset, selectedTime, selectedRoom, selectedSchedule, allUsersData]); - if (currentStartTime) { - try { - const newEndTime = addMinutes(currentStartTime, 30); - setValue("endTime", newEndTime, { shouldValidate: true }); - } catch (error) { - null; + useEffect(() => { + if (startAtValue) { + const currentEndAt = parse(endAtValue, "HH:mm", new Date()); + const calculatedEndAt = addMinutes(parse(startAtValue, "HH:mm", new Date()), 30); + + if (differenceInMinutes(calculatedEndAt, currentEndAt) > 0) { + const newEndAt = format(calculatedEndAt, "HH:mm"); + setValue("endAt", newEndAt); } } - }, [startTimeValue, customStartTimeValue, setValue, selectedTime]); + }, [startAtValue, endAtValue, setValue]); - useEffect(() => { - const compareTimes = (start: string, end: string): boolean => { - const [startHour = 0, startMinute = 0] = start.split(":").map((value) => { - const num = parseInt(value, 10); - return isNaN(num) ? 0 : num; - }); + const onFormSubmit: SubmitHandler = (data) => { + if (!user || !selectedMeetingRoom?._id) { + return; + } + + const { year, month, day } = selectedDate; - const [endHour = 0, endMinute = 0] = end.split(":").map((value) => { - const num = parseInt(value, 10); - return isNaN(num) ? 0 : num; + const newStart = new Date( + `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}T${data.startAt}:00`, + ); + + const newEnd = new Date( + `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}T${data.endAt}:00`, + ); + + if (newEnd <= newStart) { + setError("endAt", { + type: "manual", + message: ERROR_MESSAGES.endTimeMinimum, }); + return; + } - return startHour > endHour || (startHour === endHour && startMinute >= endMinute); - }; + const isOverlap = meetingsData.some((reservation) => { + if (reservation.itemType !== MEETING_ROOMS_TYPE) { + return false; + } - const currentStartTime = startTimeValue === "custom-start" ? customStartTimeValue : startTimeValue; - if (currentStartTime && endTimeValue && compareTimes(currentStartTime, endTimeValue)) { - setError("endTime", { + let itemId: string; + if (typeof reservation.item === "string") { + itemId = reservation.item; + } else if ("_id" in reservation.item) { + itemId = reservation.item._id; + } else { + return false; + } + + if (itemId !== selectedMeetingRoom._id) { + return false; + } + + if (selectedSchedule && reservation._id === selectedSchedule._id) { + return false; + } + + const existingStart = new Date(reservation.startAt); + const existingEnd = new Date(reservation.endAt); + + return newStart < existingEnd && newEnd > existingStart; + }); + + if (isOverlap) { + setError("startAt", { type: "manual", - message: "종료 시간은 시작 시간보다 이후여야 합니다.", + message: ERROR_MESSAGES.timeOverlap, }); - } else { - clearErrors("endTime"); + setError("endAt", { + type: "manual", + message: ERROR_MESSAGES.timeOverlap, + }); + return; } - }, [startTimeValue, customStartTimeValue, endTimeValue, setError, clearErrors]); + + const attendeeIds = data.attendees + .map((name: string) => { + const selectedUser = allUsersData?.members.find((member) => member.name === name); + return selectedUser ? selectedUser._id : null; + }) + .filter((id): id is string => id !== null); + + const startAt = newStart.toISOString(); + const endAt = newEnd.toISOString(); + + const mappedData: CreateReservationRequest = { + userId: user._id, + itemType: "room", + startAt, + endAt, + status: "reserved", + notes: data.notes, + attendees: attendeeIds, + }; + + onSubmit(mappedData, selectedMeetingRoom._id, selectedSchedule?._id); + }; return (
- {/* 미팅 제목 입력 */} - } + + + { + const roomData = roomsData.find((room) => room.name === value); + if (roomData) { + setSelectedMeetingRoom({ _id: roomData._id, name: roomData.name }); + void trigger("endAt"); + } + }} /> - {/* 미팅룸 선택 */} - ( - { - field.onChange(value); - }} - isError={Boolean(errors.selectedRoom)} - errorMessage={errors.selectedRoom?.message ?? ""} - > - {field.value || "회의실 선택"} - - {" "} - {/* 스크롤 추가 */} - {rooms.map((room) => ( - - {room} - - ))} - - - )} + errors={errors} + trigger={trigger} + clearErrors={clearErrors} + validateEndAt={validateEndAtFunction} /> - {/* 시작 시간 및 종료 시간 선택 */} -
- {/* 시작 시간 */} -
- ( - { - field.onChange(value); - }} - isError={Boolean(errors.startTime)} - errorMessage={errors.startTime?.message ?? ""} - > - {field.value || selectedTime} - - {" "} - {/* 스크롤 추가 */} - 직접입력 - {timeOptions.map((time) => ( - - {time} - - ))} - - - )} - /> - {startTimeValue === "custom-start" && ( - } - /> - )} -
- - {/* 종료 시간 */} -
- ( - { - field.onChange(value); - }} - isError={Boolean(errors.endTime)} - errorMessage={errors.endTime?.message ?? ""} - > - {field.value || "종료 시간 선택"} - - 직접입력 - {timeOptions.map((time) => ( - - {time} - - ))} - - - )} - /> - {endTimeValue === "custom-end" && ( - } - /> - )} -
-
- {/* 참여자 선택 */} - ( - { - field.onChange(value); - }} - > - - {field.value.length > 0 ? ( -
- {field.value.slice(0, 3).map((name) => ( - - ))} - {field.value.length > 3 && ( - +{field.value.length - 3}명 - )} -
- ) : ( - "참여자 선택" - )} -
- - {" "} - {/* 스크롤 추가 */} - {mockParticipants - .sort((a, b) => { - const isASelected = field.value.includes(a); - const isBSelected = field.value.includes(b); - - if (isASelected && !isBSelected) return -1; - if (!isASelected && isBSelected) return 1; - return 0; - }) - .map((name) => ( - - - - ))} - -
- )} + allUsersData={allUsersData?.members ?? []} + isLoading={allUsersIsLoading} + isError={allUsersIsError} /> + + {isEditMode ? : null} +
); diff --git a/apps/web/app/meetings/_components/Reservation/ReservationForm/AttendeesMultiSelect.tsx b/apps/web/app/meetings/_components/Reservation/ReservationForm/AttendeesMultiSelect.tsx new file mode 100644 index 00000000..053e3288 --- /dev/null +++ b/apps/web/app/meetings/_components/Reservation/ReservationForm/AttendeesMultiSelect.tsx @@ -0,0 +1,80 @@ +import MultiSelectDropdown from "@ui/src/components/common/Dropdown/MultiSelectDropdown"; +import { Controller } from "react-hook-form"; +import { Badge } from "@ui/index"; +import { type IUser } from "@repo/types"; +import Profile from "@/components/common/Profile"; +import { ERROR_MESSAGES, FORM_LABELS } from "@/app/constants/reservationFormConstants"; +import { type AttendeesMultiSelectProps } from "@/app/types/ReservationFormTypes"; + +export function AttendeesMultiSelect({ + control, + allUsersData, + isLoading, + isError, +}: AttendeesMultiSelectProps): JSX.Element { + return ( + ( + { + field.onChange(value); + }} + > + + {field.value.length > 0 ? ( +
+ {field.value.slice(0, 3).map((name) => { + const matchingUser = allUsersData.find((user) => user.name === name); + return matchingUser ? ( + + {matchingUser.name} + + ) : null; + })} + {field.value.length > 3 && ( + +{field.value.length - 3}명 + )} +
+ ) : ( + FORM_LABELS.selectAttendees + )} +
+ + {isLoading ?
사용자 데이터 로딩 중...
: null} + {isError && !isLoading ?
{ERROR_MESSAGES.userFetchError}
: null} + {!isLoading && !isError && allUsersData.length === 0 &&
참여자가 없습니다.
} + {!isLoading && + !isError && + allUsersData.length > 0 && + allUsersData.map((user: IUser) => ( + +
+ +
+ {user.teams.map((team: string) => ( +
+ {team} +
+ ))} +
+
+
+ ))} +
+
+ )} + /> + ); +} diff --git a/apps/web/app/meetings/_components/Reservation/ReservationForm/DeleteButton.tsx b/apps/web/app/meetings/_components/Reservation/ReservationForm/DeleteButton.tsx new file mode 100644 index 00000000..f22d1df4 --- /dev/null +++ b/apps/web/app/meetings/_components/Reservation/ReservationForm/DeleteButton.tsx @@ -0,0 +1,11 @@ +import Button from "@ui/src/components/common/Button"; +import { BUTTON_TEXT } from "@/app/constants/reservationFormConstants"; +import { type DeleteButtonProps } from "@/app/types/ReservationFormTypes"; + +export function DeleteButton({ onDelete }: DeleteButtonProps): JSX.Element { + return ( + + ); +} diff --git a/apps/web/app/meetings/_components/Reservation/ReservationForm/NotesInput.tsx b/apps/web/app/meetings/_components/Reservation/ReservationForm/NotesInput.tsx new file mode 100644 index 00000000..ed3f5309 --- /dev/null +++ b/apps/web/app/meetings/_components/Reservation/ReservationForm/NotesInput.tsx @@ -0,0 +1,22 @@ +import { Controller } from "react-hook-form"; +import Input from "@ui/src/components/common/Input"; +import { ERROR_MESSAGES, FORM_LABELS } from "@/app/constants/reservationFormConstants"; +import { type NotesInputProps } from "@/app/types/ReservationFormTypes"; + +export function NotesInput({ control }: NotesInputProps): JSX.Element { + return ( + ( + + )} + /> + ); +} diff --git a/apps/web/app/meetings/_components/Reservation/ReservationForm/RoomDropdown.tsx b/apps/web/app/meetings/_components/Reservation/ReservationForm/RoomDropdown.tsx new file mode 100644 index 00000000..bfc5ae39 --- /dev/null +++ b/apps/web/app/meetings/_components/Reservation/ReservationForm/RoomDropdown.tsx @@ -0,0 +1,45 @@ +/* eslint-disable no-nested-ternary */ +import Dropdown from "@ui/src/components/common/Dropdown"; +import { useQuery } from "@tanstack/react-query"; +import { type TBaseItem } from "@repo/types"; +import { getAllItems } from "@/api/items"; +import { MEETING_ROOMS_TYPE } from "@/app/constants/meetingRoomsType"; +import { ERROR_MESSAGES, FORM_LABELS } from "@/app/constants/reservationFormConstants"; +import { type RoomDropdownProps } from "@/app/types/ReservationFormTypes"; + +export function RoomDropdown({ selectedRoom, onSelect }: RoomDropdownProps): JSX.Element { + const { + data: roomsData = [], + isLoading, + isError, + } = useQuery({ + queryKey: ["Rooms", MEETING_ROOMS_TYPE], + queryFn: () => getAllItems({ itemType: MEETING_ROOMS_TYPE }), + }); + + return ( + { + if (typeof value === "string") { + onSelect(value); + } + }} + > + {selectedRoom || FORM_LABELS.selectRoom} + + {isLoading ? ( +
회의실 로딩 중...
+ ) : isError ? ( +
{ERROR_MESSAGES.roomFetchError}
+ ) : ( + roomsData.map((room) => ( + + {room.name} + + )) + )} +
+
+ ); +} diff --git a/apps/web/app/meetings/_components/Reservation/ReservationForm/TimeSelectors.tsx b/apps/web/app/meetings/_components/Reservation/ReservationForm/TimeSelectors.tsx new file mode 100644 index 00000000..5068ce69 --- /dev/null +++ b/apps/web/app/meetings/_components/Reservation/ReservationForm/TimeSelectors.tsx @@ -0,0 +1,99 @@ +/* eslint-disable @typescript-eslint/no-floating-promises */ + +import Dropdown from "@ui/src/components/common/Dropdown"; +import { Controller } from "react-hook-form"; +import { timeOptions } from "@/app/constants/timeOptions"; +import { FORM_LABELS, ERROR_MESSAGES } from "@/app/constants/reservationFormConstants"; +import { type TimeSelectorsProps } from "@/app/types/ReservationFormTypes"; + +export function TimeSelectors({ + control, + errors, + trigger, + clearErrors, + validateEndAt, +}: TimeSelectorsProps): JSX.Element { + const errorMessages = []; + if (errors.startAt) { + errorMessages.push(errors.startAt.message); + } + if (errors.endAt) { + errorMessages.push(errors.endAt.message); + } + + return ( +
+
+
+ ( + { + field.onChange(value); + clearErrors("endAt"); + trigger("endAt"); + }} + isError={Boolean(errors.startAt)} + > + + {field.value || FORM_LABELS.selectStartTime} + + + {timeOptions.map((time) => ( + + {time} + + ))} + + + )} + /> +
+ +
+ ( + { + field.onChange(value); + trigger("endAt"); + }} + isError={Boolean(errors.endAt)} + > + + {field.value || FORM_LABELS.selectEndTime} + + + {timeOptions.map((time) => ( + + {time} + + ))} + + + )} + /> +
+
+ + {errorMessages.length > 0 && ( +
+ {errorMessages.map((msg, _) => ( +
{msg}
+ ))} +
+ )} +
+ ); +} diff --git a/apps/web/app/meetings/_components/Reservation/ReservationModal.tsx b/apps/web/app/meetings/_components/Reservation/ReservationModal.tsx index 3869d1ab..0f8c8b66 100644 --- a/apps/web/app/meetings/_components/Reservation/ReservationModal.tsx +++ b/apps/web/app/meetings/_components/Reservation/ReservationModal.tsx @@ -1,27 +1,36 @@ "use client"; import AlertModal from "@ui/src/components/common/ConditionalActionModal/AlertModal"; -import { notify } from "@/app/store/useToastStore"; +import { type ReactNode } from "react"; interface ReservationModalProps { isOpen: boolean; onClose: () => void; onConfirm: () => void; + title: string; // 추가 + content: ReactNode; // 추가 + cancelButtonName: string; // 추가 + confirmButtonName: string; // 추가 } -export default function ReservationModal({ isOpen, onClose, onConfirm }: ReservationModalProps): JSX.Element { +export default function ReservationModal({ + isOpen, + onClose, + onConfirm, + title, + content, + cancelButtonName, + confirmButtonName, +}: ReservationModalProps): JSX.Element { return ( { - onConfirm(); - notify("success", "회의실이 예약되었습니다!"); - }} - title="회의실을 예약하시겠어요?" - content={<>선택한 시간대의 회의실이 예약됩니다.} - cancelButtonName="취소하기" - confirmButtonName="예약하기" + onConfirm={onConfirm} + title={title} + content={content} + cancelButtonName={cancelButtonName} + confirmButtonName={confirmButtonName} /> ); } diff --git a/apps/web/app/meetings/_components/Reservation/ReservationSheetContent.tsx b/apps/web/app/meetings/_components/Reservation/ReservationSheetContent.tsx new file mode 100644 index 00000000..31b2fb2c --- /dev/null +++ b/apps/web/app/meetings/_components/Reservation/ReservationSheetContent.tsx @@ -0,0 +1,139 @@ +/* eslint-disable @typescript-eslint/no-shadow */ +/* eslint-disable @typescript-eslint/no-unnecessary-condition */ + +"use client"; + +import { useState } from "react"; +import { type IReservation } from "@repo/types"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { type SelectedRoom } from "@/app/types/scheduletypes"; +import { + createReservation, + deleteReservation, + updateReservation, + type CreateReservationRequest, +} from "@/api/reservations"; +import { useAuthStore } from "@/src/stores/useAuthStore"; +import { useDateStore } from "@/app/store/useDateStore"; +import { MEETING_ROOMS_TYPE } from "@/app/constants/meetingRoomsType"; +import { formatDate } from "@/app/utils/formatDate"; +import { MODAL_TEXT, ModalType, NOTIFICATION_MESSAGES, QUERY_KEYS } from "@/app/constants/reservationConstants"; +import { notify } from "@/app/store/useToastStore"; +import ReservationModal from "./ReservationModal"; +import { ReservationForm } from "./ReservationForm"; + +interface ReservationSheetContentProps { + onClose: () => void; + selectedTime: string; + selectedSchedule?: IReservation | null; + selectedRoom?: SelectedRoom | null; +} + +export default function ReservationSheetContent(props: ReservationSheetContentProps): JSX.Element { + const { onClose, selectedTime, selectedSchedule, selectedRoom } = props; + const queryClient = useQueryClient(); + const [isModalOpen, setIsModalOpen] = useState(false); + const [modalType, setModalType] = useState(ModalType.CREATE_UPDATE); + + const user = useAuthStore((state) => state.user); + const { selectedDate } = useDateStore(); + + const formattedDate = formatDate(selectedDate); + + const [formData, setFormData] = useState<{ + data: CreateReservationRequest; + itemId: string; + reservationId?: string; + } | null>(null); + + const isEditMode = selectedSchedule && selectedSchedule.user._id === user?._id; + + const createOrUpdateReservation = useMutation({ + mutationFn: (formData: { data: CreateReservationRequest; itemId: string; reservationId?: string }) => { + if (isEditMode && formData.reservationId) { + // 수정 모드일 경우 + return updateReservation(formData.reservationId, formData.data); + } + // 생성 모드일 경우 + return createReservation(formData.itemId, formData.data); + }, + onSuccess: async () => { + notify("success", isEditMode ? NOTIFICATION_MESSAGES.update : NOTIFICATION_MESSAGES.create); + await queryClient.invalidateQueries({ + queryKey: QUERY_KEYS.meetings(formattedDate, MEETING_ROOMS_TYPE), + }); + setIsModalOpen(false); + onClose(); + }, + }); + + const deleteReservationMutation = useMutation({ + mutationFn: (reservationId: string) => deleteReservation(reservationId), + onSuccess: async () => { + notify("success", NOTIFICATION_MESSAGES.delete); + await queryClient.invalidateQueries({ + queryKey: QUERY_KEYS.meetings(formattedDate, MEETING_ROOMS_TYPE), + }); + setIsModalOpen(false); + onClose(); + }, + }); + + const handleSubmit = (data: CreateReservationRequest, itemId: string, reservationId?: string): void => { + setFormData({ data, itemId, reservationId }); + setModalType(isEditMode ? ModalType.CREATE_UPDATE : ModalType.CREATE_UPDATE); // 동일하게 설정, 추후 수정 가능 + setIsModalOpen(true); + }; + + const handleDelete = (): void => { + setModalType(ModalType.DELETE); + setIsModalOpen(true); + }; + + const confirmDelete = (): void => { + if (selectedSchedule?._id) { + deleteReservationMutation.mutate(selectedSchedule._id); + } + }; + + const modalTitle = isEditMode ? MODAL_TEXT.titles.edit : MODAL_TEXT.titles[modalType]; + + const modalContent = isEditMode ? MODAL_TEXT.contents.edit : MODAL_TEXT.contents[modalType]; + + const modalConfirmButtonName = isEditMode + ? MODAL_TEXT.confirmButtonNames.edit + : MODAL_TEXT.confirmButtonNames[modalType]; + + return ( + <> + + { + setIsModalOpen(false); + }} + onConfirm={() => { + if (modalType === ModalType.CREATE_UPDATE) { + if (formData) { + createOrUpdateReservation.mutate(formData); + } + } else if (modalType === ModalType.DELETE) { + confirmDelete(); + } + setIsModalOpen(false); + onClose(); + }} + title={modalTitle} + content={modalContent} + cancelButtonName={MODAL_TEXT.cancelButtonName} + confirmButtonName={modalConfirmButtonName} + /> + + ); +} diff --git a/apps/web/app/meetings/_components/Reservation/ReservationSheets.tsx b/apps/web/app/meetings/_components/Reservation/ReservationSheets.tsx new file mode 100644 index 00000000..eb08a7e1 --- /dev/null +++ b/apps/web/app/meetings/_components/Reservation/ReservationSheets.tsx @@ -0,0 +1,42 @@ +"use client"; + +import { type IReservation } from "@repo/types"; +import { type SelectedRoom } from "@/app/types/scheduletypes"; +import DesktopReservationSheet from "./DesktopReservationSheet"; +import MobileReservationSheet from "./MobileReservationSheet"; + +interface ReservationSheetsProps { + isOpen: boolean; + onClose: () => void; + selectedTime: string | null; + selectedSchedule: IReservation | null; + selectedRoom: SelectedRoom | null; +} + +export default function ReservationSheets(props: ReservationSheetsProps): JSX.Element { + const { isOpen, onClose, selectedTime, selectedSchedule, selectedRoom } = props; + if (!selectedTime || !selectedRoom) return
; + + return ( + <> +
+ +
+ +
+ +
+ + ); +} diff --git a/apps/web/app/meetings/_components/Schedule/RoomName.tsx b/apps/web/app/meetings/_components/Schedule/RoomName.tsx index 11df9781..e3a522b3 100644 --- a/apps/web/app/meetings/_components/Schedule/RoomName.tsx +++ b/apps/web/app/meetings/_components/Schedule/RoomName.tsx @@ -1,15 +1,17 @@ "use client"; +import { type ReactNode } from "react"; + interface RoomNameProps { - name: string; + children: ReactNode; } export default function RoomName(props: RoomNameProps): JSX.Element { - const { name } = props; + const { children } = props; return (
- {name} + {children}
); } diff --git a/apps/web/app/meetings/_components/Schedule/ScheduleRow/CurrentTimeIndicator.tsx b/apps/web/app/meetings/_components/Schedule/ScheduleRow/CurrentTimeIndicator.tsx index 98627b55..a5ef9c21 100644 --- a/apps/web/app/meetings/_components/Schedule/ScheduleRow/CurrentTimeIndicator.tsx +++ b/apps/web/app/meetings/_components/Schedule/ScheduleRow/CurrentTimeIndicator.tsx @@ -1,6 +1,7 @@ "use client"; -import { useEffect, useState } from "react"; +import { useCurrentTimePosition } from "@/app/_hooks/useCurrentTimePosition"; +import CurrentTimeLabel from "./CurrentTimeLabel"; interface CurrentTimeIndicatorProps { slotWidth: number; @@ -10,57 +11,24 @@ interface CurrentTimeIndicatorProps { export default function CurrentTimeIndicator(props: CurrentTimeIndicatorProps): JSX.Element { const { slotWidth, startHour, endHour } = props; - const [currentPosition, setCurrentPosition] = useState(null); - const [currentTime, setCurrentTime] = useState(""); - - useEffect(() => { - const updatePosition = (): void => { - const now = new Date(); - const currentMinutes = now.getHours() * 60 + now.getMinutes(); - const totalMinutes = (endHour - startHour) * 60; - - if (currentMinutes < startHour * 60 || currentMinutes >= endHour * 60) { - setCurrentPosition(null); - setCurrentTime(""); - return; - } - - const relativeMinutes = currentMinutes - startHour * 60; - const totalSlots = (endHour - startHour) * 2; - const scheduleWidth = slotWidth * totalSlots; - const position = (relativeMinutes / totalMinutes) * scheduleWidth; - - setCurrentPosition(position); - - const hours = String(now.getHours()).padStart(2, "0"); - const minutes = String(now.getMinutes()).padStart(2, "0"); - setCurrentTime(`${hours}:${minutes}`); - }; - - updatePosition(); - const interval = setInterval(updatePosition, 60000); - - return () => { - clearInterval(interval); - }; - }, [slotWidth, startHour, endHour]); + const { currentPosition, currentTime } = useCurrentTimePosition({ + slotWidth, + startHour, + endHour, + }); if (currentPosition === null) { return
; } return ( -
+
- -
- {currentTime} +
+
); diff --git a/apps/web/app/meetings/_components/Schedule/ScheduleRow/CurrentTimeLabel.tsx b/apps/web/app/meetings/_components/Schedule/ScheduleRow/CurrentTimeLabel.tsx new file mode 100644 index 00000000..f0d9a44e --- /dev/null +++ b/apps/web/app/meetings/_components/Schedule/ScheduleRow/CurrentTimeLabel.tsx @@ -0,0 +1,16 @@ +interface CurrentTimeLabelProps { + position: number; + time: string; +} + +export default function CurrentTimeLabel(props: CurrentTimeLabelProps): JSX.Element { + const { position, time } = props; + return ( +
+ {time} +
+ ); +} diff --git a/apps/web/app/meetings/_components/Schedule/ScheduleRow/ScheduleItem.tsx b/apps/web/app/meetings/_components/Schedule/ScheduleRow/ScheduleItem.tsx index 4a503750..2c696b36 100644 --- a/apps/web/app/meetings/_components/Schedule/ScheduleRow/ScheduleItem.tsx +++ b/apps/web/app/meetings/_components/Schedule/ScheduleRow/ScheduleItem.tsx @@ -2,11 +2,11 @@ /* eslint-disable jsx-a11y/no-static-element-interactions */ "use client"; -import { type Schedule } from "@/app/types/scheduletypes"; +import { type IReservation } from "@repo/types"; import ScheduleTooltip from "./ScheduleTooltip"; interface ScheduleItemProps { - schedule: Schedule; + schedule: IReservation; leftPosition: number; scheduleWidth: number; isCurrentUser: boolean; @@ -39,7 +39,7 @@ export default function ScheduleItem(props: ScheduleItemProps): JSX.Element { role="button" tabIndex={0} > - {isCurrentUser ? null : } + {isCurrentUser ? null : {schedule.notes}}
); diff --git a/apps/web/app/meetings/_components/Schedule/ScheduleRow/ScheduleSlot.tsx b/apps/web/app/meetings/_components/Schedule/ScheduleRow/ScheduleSlot.tsx index bedf6d1b..f9ef3f79 100644 --- a/apps/web/app/meetings/_components/Schedule/ScheduleRow/ScheduleSlot.tsx +++ b/apps/web/app/meetings/_components/Schedule/ScheduleRow/ScheduleSlot.tsx @@ -1,50 +1,62 @@ /* eslint-disable jsx-a11y/click-events-have-key-events */ - -"use client"; import { useEffect, useState } from "react"; +import { type IReservation } from "@repo/types"; import { useSidebarStore } from "@/app/store/useSidebarStore"; +import { useAuthStore } from "@/src/stores/useAuthStore"; interface ScheduleSlotProps { index: number; slotWidth: number; slotHeight: number; - onClick: (index: number) => void; + slotCount: number; + onClick: () => void; + isReserved: boolean; + schedule?: IReservation; } export default function ScheduleSlot(props: ScheduleSlotProps): JSX.Element { - const { index, slotHeight, slotWidth, onClick } = props; + const { index, slotHeight, slotWidth, slotCount, onClick, isReserved, schedule } = props; - const { isSidebarOpen, openSidebar } = useSidebarStore(); + const { isSidebarOpen } = useSidebarStore(); const [isClicked, setIsClicked] = useState(false); + const user = useAuthStore((state) => state.user); + const handleClick = (): void => { setIsClicked(true); - openSidebar(); - onClick(index); + onClick(); }; - // Sidebar가 닫힐 때 클릭 상태 초기화 useEffect(() => { if (!isSidebarOpen) { setIsClicked(false); } }, [isSidebarOpen]); + const isUserReservation = isReserved && schedule && schedule.user._id === user?._id; + return (
- {isClicked ?
: null} -
+ {isReserved && schedule ? ( +
+
+
+ ) : null} + {index % 2 === 0 ? (
) : (
)} +
); diff --git a/apps/web/app/meetings/_components/Schedule/ScheduleRow/ScheduleSlots.tsx b/apps/web/app/meetings/_components/Schedule/ScheduleRow/ScheduleSlots.tsx new file mode 100644 index 00000000..5a0ebe2c --- /dev/null +++ b/apps/web/app/meetings/_components/Schedule/ScheduleRow/ScheduleSlots.tsx @@ -0,0 +1,64 @@ +import React from "react"; +import { type IReservation } from "@repo/types"; +import ScheduleSlot from "./ScheduleSlot"; + +interface ScheduleSlotsProps { + slotReservations: (IReservation | null)[]; + slotWidth: number; + slotHeight: number; + onSlotClick: (index: number, schedule?: IReservation) => void; +} + +export default function ScheduleSlots(props: ScheduleSlotsProps): JSX.Element { + const { slotReservations, slotWidth, slotHeight, onSlotClick } = props; + const slots: JSX.Element[] = []; + const totalSlots = slotReservations.length; + + for (let i = 0; i < totalSlots; i++) { + const schedule = slotReservations[i]; + if (schedule) { + // 예약된 슬롯의 시작 위치를 찾기 위해 이전 슬롯을 확인 + if (i === 0 || slotReservations[i - 1] !== schedule) { + // 예약된 슬롯의 시작 + const start = i; + let end = i; + // 예약된 슬롯의 끝 위치를 찾음 + while (end < totalSlots && slotReservations[end] === schedule) { + end++; + } + slots.push( + { + onSlotClick(start, schedule); + }} + isReserved + schedule={schedule} + />, + ); + i = end - 1; // 다음 반복에서 end 위치부터 시작 + } + } else { + // 예약되지 않은 슬롯은 개별적으로 렌더링 + slots.push( + { + onSlotClick(i); + }} + isReserved={false} + />, + ); + } + } + + return <>{slots}; +} diff --git a/apps/web/app/meetings/_components/Schedule/ScheduleRow/ScheduleTooltip.tsx b/apps/web/app/meetings/_components/Schedule/ScheduleRow/ScheduleTooltip.tsx index 35f4b54f..0f5435aa 100644 --- a/apps/web/app/meetings/_components/Schedule/ScheduleRow/ScheduleTooltip.tsx +++ b/apps/web/app/meetings/_components/Schedule/ScheduleRow/ScheduleTooltip.tsx @@ -1,15 +1,17 @@ "use client"; +import { type ReactNode } from "react"; + interface ScheduleTooltipProps { - title: string; + children?: ReactNode; } export default function ScheduleTooltip(props: ScheduleTooltipProps): JSX.Element { - const { title } = props; + const { children } = props; return (
- {title} + {children}
diff --git a/apps/web/app/meetings/_components/Schedule/ScheduleRow/index.tsx b/apps/web/app/meetings/_components/Schedule/ScheduleRow/index.tsx index 94d8063a..f8306cd9 100644 --- a/apps/web/app/meetings/_components/Schedule/ScheduleRow/index.tsx +++ b/apps/web/app/meetings/_components/Schedule/ScheduleRow/index.tsx @@ -1,48 +1,37 @@ -/* eslint-disable react/no-array-index-key */ -"use client"; - import { useState } from "react"; -import { type Schedule } from "@/app/types/scheduletypes"; +import { type IReservation } from "@repo/types"; import { useSidebarStore } from "@/app/store/useSidebarStore"; -import MobileReservationSheet from "../../Reservation/MobileReservationSheet"; -import DesktopReservationSheet from "../../Reservation/DesktopReservationSheet"; -import ScheduleSlot from "./ScheduleSlot"; -import ScheduleItem from "./ScheduleItem"; +import { type SelectedRoom } from "@/app/types/scheduletypes"; +import { useSlotReservations } from "@/app/_hooks/useSlotReservations"; +import ReservationSheets from "../../Reservation/ReservationSheets"; import CurrentTimeIndicator from "./CurrentTimeIndicator"; +import ScheduleSlots from "./ScheduleSlots"; interface ScheduleRowProps { - schedules: Schedule[]; - room: string; + schedules: IReservation[]; + room: SelectedRoom; slotWidth?: number; slotHeight?: number; - onSlotClick?: (time: string, schedule?: Schedule, room?: string) => void; + onSlotClick?: (time: string, schedule?: IReservation, room?: { name: string; _id: string }) => void; } export default function ScheduleRow(props: ScheduleRowProps): JSX.Element { const { schedules, room, slotHeight = 80, slotWidth = 72 } = props; - const startHour = 0; - const endHour = 24; - const totalSlots = (endHour - startHour) * 2; - const minutesPerSlot = 30; - const totalMinutes = (endHour - startHour) * 60; - - const timeToMinutes = (time: string): number => { - const [hoursStr, minutesStr] = time.split(":"); - const hours = Number(hoursStr); - const minutes = Number(minutesStr); - return hours * 60 + minutes; - }; - const [selectedTime, setSelectedTime] = useState(null); - const [selectedSchedule, setSelectedSchedule] = useState(null); - const [selectedRoom, setSelectedRoom] = useState(room || null); + const [selectedSchedule, setSelectedSchedule] = useState(null); const openSidebar = useSidebarStore((state) => state.openSidebar); const closeSidebar = useSidebarStore((state) => state.closeSidebar); const isSidebarOpen = useSidebarStore((state) => state.isSidebarOpen); - const handleSlotClick = (index: number, schedule?: Schedule): void => { + const startHour = 0; + const endHour = 24; + const minutesPerSlot = 30; + + const slotReservations = useSlotReservations(schedules, startHour, endHour, minutesPerSlot); + + const handleSlotClick = (index: number, schedule?: IReservation): void => { const clickedTimeMinutes = startHour * 60 + index * minutesPerSlot; const hours = Math.floor(clickedTimeMinutes / 60); const minutes = clickedTimeMinutes % 60; @@ -50,7 +39,6 @@ export default function ScheduleRow(props: ScheduleRowProps): JSX.Element { setSelectedTime(timeString); setSelectedSchedule(schedule ?? null); - setSelectedRoom(room); openSidebar(); }; @@ -58,75 +46,30 @@ export default function ScheduleRow(props: ScheduleRowProps): JSX.Element { closeSidebar(); setSelectedSchedule(null); setSelectedTime(null); - setSelectedRoom(null); }; return (
- {Array.from({ length: totalSlots }).map((_, index) => ( - { - handleSlotClick(index); - }} - /> - ))} +
-
+
- {schedules.map((schedule) => { - const startMinutes = timeToMinutes(schedule.start_time) - startHour * 60; - const endMinutes = timeToMinutes(schedule.end_time) - startHour * 60; - const scheduleDuration = endMinutes - startMinutes; - - if (startMinutes < 0 || endMinutes > totalMinutes) return null; - - const leftPosition = (startMinutes / totalMinutes) * (slotWidth * totalSlots); - const scheduleWidth = (scheduleDuration / totalMinutes) * (slotWidth * totalSlots); - - return ( - { - handleSlotClick(-1, schedule); - }} - /> - ); - })} - - {selectedTime && selectedRoom ? ( - <> -
- -
- -
- -
- - ) : null} +
); } diff --git a/apps/web/app/meetings/_components/Schedule/ScheduleTable/ScheduleTableDesktop.tsx b/apps/web/app/meetings/_components/Schedule/ScheduleTable/ScheduleTableDesktop.tsx index c2d08caa..5a45c3b9 100644 --- a/apps/web/app/meetings/_components/Schedule/ScheduleTable/ScheduleTableDesktop.tsx +++ b/apps/web/app/meetings/_components/Schedule/ScheduleTable/ScheduleTableDesktop.tsx @@ -1,38 +1,55 @@ "use client"; -import { type ScheduleDate } from "@/app/types/scheduletypes"; +import { type TBaseItem, type IReservation } from "@repo/types"; import RoomName from "../RoomName"; import ScheduleRow from "../ScheduleRow"; import CurrentTimeIndicator from "../ScheduleRow/CurrentTimeIndicator"; import TimeText from "../ScheduleRow/TimeText"; -type ScheduleTableDesktopProps = ScheduleDate; +interface ScheduleTableDesktopProps { + rooms: TBaseItem[]; + meetingsData: IReservation[]; + selectedDate: string; +} export default function ScheduleTableDesktop(props: ScheduleTableDesktopProps): JSX.Element { - const { rooms, selectedDate } = props; + const { rooms, meetingsData, selectedDate } = props; return (
{rooms.map((room) => ( -
- +
+ {room.name}
))}
- {rooms.map((room) => ( -
- schedule.date === selectedDate)} - slotWidth={72} - slotHeight={80} - room={room.title} - /> -
- ))} + {rooms.map((room) => { + const roomSchedules = meetingsData.filter((schedule) => { + const scheduleItemId = typeof schedule.item === "string" ? schedule.item : schedule.item._id; + + const isSameRoom = scheduleItemId === room._id; + + const scheduleDate = new Date(schedule.startAt).toLocaleDateString("en-CA", { timeZone: "Asia/Seoul" }); + const isSameDate = scheduleDate === selectedDate; + + return isSameRoom && isSameDate; + }); + + return ( +
+ +
+ ); + })}
diff --git a/apps/web/app/meetings/_components/Schedule/ScheduleTable/ScheduleTableMobile.tsx b/apps/web/app/meetings/_components/Schedule/ScheduleTable/ScheduleTableMobile.tsx index 7763a47d..e9031233 100644 --- a/apps/web/app/meetings/_components/Schedule/ScheduleTable/ScheduleTableMobile.tsx +++ b/apps/web/app/meetings/_components/Schedule/ScheduleTable/ScheduleTableMobile.tsx @@ -1,31 +1,51 @@ "use client"; -import { type ScheduleDate } from "@/app/types/scheduletypes"; +import { type TBaseItem } from "@repo/types"; +import { type IReservation } from "@repo/types/src/reservationType"; import RoomName from "../RoomName"; import ScheduleRow from "../ScheduleRow"; import TimeText from "../ScheduleRow/TimeText"; -type ScheduleTableMobileProps = ScheduleDate; +interface ScheduleTableMobileProps { + rooms: TBaseItem[]; + meetingsData: IReservation[]; + selectedDate: string; +} export default function ScheduleTableMobile(props: ScheduleTableMobileProps): JSX.Element { - const { rooms, selectedDate } = props; + const { rooms, meetingsData, selectedDate } = props; return (
- {rooms.map((room) => ( -
- -
- -
- schedule.date === selectedDate)} - room={room.title} - /> + {rooms.map((room) => { + const roomSchedules = meetingsData.filter((schedule) => { + const scheduleItemId = typeof schedule.item === "string" ? schedule.item : schedule.item._id; + + const isSameRoom = scheduleItemId === room._id; + + const scheduleDate = new Date(schedule.startAt).toLocaleDateString("en-CA", { timeZone: "Asia/Seoul" }); + const isSameDate = scheduleDate === selectedDate; + + return isSameRoom && isSameDate; + }); + + return ( +
+ {room.name} +
+ +
+ +
-
- ))} + ); + })}
); } diff --git a/apps/web/app/meetings/_components/Schedule/ScheduleTable/index.tsx b/apps/web/app/meetings/_components/Schedule/ScheduleTable/index.tsx index 075de610..d38fe666 100644 --- a/apps/web/app/meetings/_components/Schedule/ScheduleTable/index.tsx +++ b/apps/web/app/meetings/_components/Schedule/ScheduleTable/index.tsx @@ -1,18 +1,22 @@ "use client"; -import { type ScheduleDate } from "@/app/types/scheduletypes"; +import { type TBaseItem, type IReservation } from "@repo/types"; import ScheduleTableMobile from "./ScheduleTableMobile"; import ScheduleTableDesktop from "./ScheduleTableDesktop"; -type ScheduleTableProps = ScheduleDate; +interface ScheduleTableProps { + rooms: TBaseItem[]; + meetingsData: IReservation[]; + selectedDate: string; +} export default function ScheduleTable(props: ScheduleTableProps): JSX.Element { - const { rooms, selectedDate } = props; + const { rooms, meetingsData, selectedDate } = props; return (
- - + +
); } diff --git a/apps/web/app/meetings/_components/main.tsx b/apps/web/app/meetings/_components/main.tsx deleted file mode 100644 index e1c70c9b..00000000 --- a/apps/web/app/meetings/_components/main.tsx +++ /dev/null @@ -1,13 +0,0 @@ -"use client"; - -import { useDateStore } from "@/app/store/useDateStore"; -import { rooms } from "../../mocks/mockData"; -import ScheduleTable from "./Schedule/ScheduleTable"; - -export default function MeetingRoomSchedule(): JSX.Element { - const { selectedDate } = useDateStore(); - - const formattedDate = `${String(selectedDate.year)}-${String(selectedDate.month).padStart(2, "0")}-${String(selectedDate.day).padStart(2, "0")}`; - - return ; -} diff --git a/apps/web/app/meetings/_components/skeleton/RoomNameSkeleton.tsx b/apps/web/app/meetings/_components/skeleton/RoomNameSkeleton.tsx new file mode 100644 index 00000000..300a5b7d --- /dev/null +++ b/apps/web/app/meetings/_components/skeleton/RoomNameSkeleton.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { memo } from "react"; + +function RoomNameSkeleton(): JSX.Element { + return ( +
+
+
+ ); +} + +export default memo(RoomNameSkeleton); diff --git a/apps/web/app/meetings/_components/skeleton/ScheduleSlotSkeleton.tsx b/apps/web/app/meetings/_components/skeleton/ScheduleSlotSkeleton.tsx new file mode 100644 index 00000000..39966f6e --- /dev/null +++ b/apps/web/app/meetings/_components/skeleton/ScheduleSlotSkeleton.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { memo } from "react"; + +function ScheduleSlotSkeleton(): JSX.Element { + return ( +
+ {Array.from({ length: 7 }, (_, index) => ( +
+
+
+
+
+
+ ))} +
+ ); +} + +export default memo(ScheduleSlotSkeleton); diff --git a/apps/web/app/meetings/_components/skeleton/TimeTextSkeleton.tsx b/apps/web/app/meetings/_components/skeleton/TimeTextSkeleton.tsx new file mode 100644 index 00000000..4a4401dc --- /dev/null +++ b/apps/web/app/meetings/_components/skeleton/TimeTextSkeleton.tsx @@ -0,0 +1,17 @@ +"use client"; + +import { memo } from "react"; + +function TimeTextSkeleton(): JSX.Element { + return ( +
+ {Array.from({ length: 24 }, (_, index) => ( +
+
+
+ ))} +
+ ); +} + +export default memo(TimeTextSkeleton); diff --git a/apps/web/app/meetings/_components/skeleton/index.tsx b/apps/web/app/meetings/_components/skeleton/index.tsx new file mode 100644 index 00000000..5cd0f519 --- /dev/null +++ b/apps/web/app/meetings/_components/skeleton/index.tsx @@ -0,0 +1,31 @@ +/* eslint-disable react/no-array-index-key */ +import { memo } from "react"; +import RoomNameSkeleton from "./RoomNameSkeleton"; +import ScheduleSlotSkeleton from "./ScheduleSlotSkeleton"; +import TimeTextSkeleton from "./TimeTextSkeleton"; + +function MeetingsSkeleton(): JSX.Element { + return ( +
+
+
+ {Array.from({ length: 7 }).map((_, index) => ( +
+ +
+ ))} +
+
+ + {Array.from({ length: 1 }).map((_, index) => ( +
+ +
+ ))} +
+
+
+ ); +} + +export default memo(MeetingsSkeleton); diff --git a/apps/web/app/meetings/page.tsx b/apps/web/app/meetings/page.tsx index aafee684..b2cc33a5 100644 --- a/apps/web/app/meetings/page.tsx +++ b/apps/web/app/meetings/page.tsx @@ -1,5 +1,5 @@ -import Main from "./_components/main"; +import MeetingRoomSchedule from "./_components/MeetingRoomSchedule"; -export default function MeetingsPage(): JSX.Element { - return
; +export default function MeetingsPaga(): JSX.Element { + return ; } diff --git a/apps/web/app/types/ReservationFormTypes.ts b/apps/web/app/types/ReservationFormTypes.ts new file mode 100644 index 00000000..0d70d14e --- /dev/null +++ b/apps/web/app/types/ReservationFormTypes.ts @@ -0,0 +1,41 @@ +import { type Control, type FieldErrors, type UseFormTrigger, type UseFormClearErrors } from "react-hook-form"; +import { type IReservation, type IUser } from "@repo/types"; +import { type CreateReservationRequest } from "@/api/reservations"; +import { type SelectedRoom } from "@/app/types/scheduletypes"; + +export interface ReservationFormProps { + onSubmit: (data: CreateReservationRequest, itemId: string, reservationId?: string) => void; + selectedTime: string; + selectedSchedule?: IReservation | null; + resetTrigger?: number; + selectedRoom?: SelectedRoom | null; + onDelete: () => void; +} + +export interface NotesInputProps { + control: Control; +} + +export interface RoomDropdownProps { + selectedRoom: string; + onSelect: (value: string) => void; +} + +export interface TimeSelectorsProps { + control: Control; + errors: FieldErrors; + trigger: UseFormTrigger; + clearErrors: UseFormClearErrors; + validateEndAt: (endAt: string) => boolean | string; +} + +export interface AttendeesMultiSelectProps { + control: Control; + allUsersData: IUser[]; + isLoading: boolean; + isError: boolean; +} + +export interface DeleteButtonProps { + onDelete: () => void; +} diff --git a/apps/web/app/types/scheduletypes.ts b/apps/web/app/types/scheduletypes.ts index 3140a406..1072e070 100644 --- a/apps/web/app/types/scheduletypes.ts +++ b/apps/web/app/types/scheduletypes.ts @@ -19,10 +19,15 @@ export interface ScheduleDate { export interface ScheduleFormData { meetingTitle: string; - selectedRoom: string; + selectedRoom: SelectedRoom | null; // name과 _id를 포함 startTime: string; customStartTime: string; endTime: string; customEndTime: string; participants: string[]; } + +export interface SelectedRoom { + name: string; + _id: string; +} diff --git a/apps/web/app/utils/formatDate.ts b/apps/web/app/utils/formatDate.ts new file mode 100644 index 00000000..4646a8fa --- /dev/null +++ b/apps/web/app/utils/formatDate.ts @@ -0,0 +1,3 @@ +export function formatDate(date: { year: number; month: number; day: number }): string { + return `${String(date.year)}-${String(date.month).padStart(2, "0")}-${String(date.day).padStart(2, "0")}`; +} diff --git a/apps/web/app/utils/timeToMinutes.ts b/apps/web/app/utils/timeToMinutes.ts new file mode 100644 index 00000000..8e96a35e --- /dev/null +++ b/apps/web/app/utils/timeToMinutes.ts @@ -0,0 +1,14 @@ +import { parseISO } from "date-fns"; + +/** + * 주어진 시간(Date 객체 또는 ISO 문자열)을 분 단위로 변환합니다. + * @param time Date 객체 또는 ISO 문자열 + * @returns 분 단위의 숫자 + */ + +export const timeToMinutes = (time: Date | string): number => { + const date = typeof time === "string" ? parseISO(time) : time; + const hours = date.getHours(); + const minutes = date.getMinutes(); + return hours * 60 + minutes; +}; diff --git a/apps/web/app/utils/validateTime.ts b/apps/web/app/utils/validateTime.ts new file mode 100644 index 00000000..398eac46 --- /dev/null +++ b/apps/web/app/utils/validateTime.ts @@ -0,0 +1,89 @@ +import { parse, differenceInMinutes } from "date-fns"; +import { type IReservation } from "@repo/types"; +import { type UseFormGetValues } from "react-hook-form"; +import { ERROR_MESSAGES, TIME_INTERVAL } from "@/app/constants/reservationFormConstants"; +import { MEETING_ROOMS_TYPE } from "@/app/constants/meetingRoomsType"; +import { type SelectedRoom } from "@/app/types/scheduletypes"; +import { type CreateReservationRequest } from "@/api/reservations"; + +interface ValidateTimeParams { + endAt: string; + getValues: UseFormGetValues; + selectedMeetingRoom: SelectedRoom | null | undefined; + selectedDate: { + year: number; + month: number; + day: number; + }; + meetingsData: IReservation[]; + selectedReservationId?: string; +} + +export function validateEndAt({ + endAt, + getValues, + selectedMeetingRoom, + selectedDate, + meetingsData, + selectedReservationId, +}: ValidateTimeParams): boolean | string { + const startAtValue = getValues("startAt"); + + if (!startAtValue || !endAt) { + return ERROR_MESSAGES.timeRequired; + } + + const start = parse(startAtValue, "HH:mm", new Date()); + const end = parse(endAt, "HH:mm", new Date()); + + const diff = differenceInMinutes(end, start); + + if (diff < TIME_INTERVAL.minimumDifference) { + return ERROR_MESSAGES.endTimeMinimum; + } + + if (!selectedMeetingRoom?._id) { + return true; + } + + const { year, month, day } = selectedDate; + + const newStart = new Date( + `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}T${startAtValue}:00`, + ); + const newEnd = new Date(`${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}T${endAt}:00`); + + const isOverlap = meetingsData.some((reservation) => { + if (reservation.itemType !== MEETING_ROOMS_TYPE) { + return false; + } + + let itemId: string; + if (typeof reservation.item === "string") { + itemId = reservation.item; + } else if ("_id" in reservation.item) { + itemId = reservation.item._id; + } else { + return false; + } + + if (itemId !== selectedMeetingRoom._id) { + return false; + } + + if (selectedReservationId === reservation._id) { + return false; + } + + const existingStart = new Date(reservation.startAt); + const existingEnd = new Date(reservation.endAt); + + return newStart < existingEnd && newEnd > existingStart; + }); + + if (isOverlap) { + return ERROR_MESSAGES.timeOverlap; + } + + return true; +} diff --git a/apps/web/components/common/Profile/index.tsx b/apps/web/components/common/Profile/index.tsx index 8ab0348b..d0693960 100644 --- a/apps/web/components/common/Profile/index.tsx +++ b/apps/web/components/common/Profile/index.tsx @@ -32,7 +32,7 @@ function Profile({ src, name, className, textColor = "white" }: ProfileProps): J src={src} width={32} height={32} - style={{ borderRadius: 9999 }} + className="h-32 w-32 rounded-full object-cover" onError={() => { setIsError(true); }} diff --git a/apps/web/package.json b/apps/web/package.json index d29de05f..93b2028d 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -14,6 +14,7 @@ "@tanstack/react-query": "^5.59.0", "axios": "^1.7.7", "cookies-next": "^4.3.0", + "date-fns": "^4.1.0", "es-toolkit": "^1.26.1", "framer-motion": "^11.11.9", "next": "14.2.6", diff --git a/packages/ui/src/components/common/Dropdown/MultiSelectDropdown.tsx b/packages/ui/src/components/common/Dropdown/MultiSelectDropdown.tsx index 2151a614..c7fa77c8 100644 --- a/packages/ui/src/components/common/Dropdown/MultiSelectDropdown.tsx +++ b/packages/ui/src/components/common/Dropdown/MultiSelectDropdown.tsx @@ -153,9 +153,10 @@ function Toggle({ children, title }: ToggleProps): JSX.Element { interface WrapperProps { children: ReactNode; + className?: string; } -function Wrapper({ children }: WrapperProps): JSX.Element { +function Wrapper({ children, className }: WrapperProps): JSX.Element { const { isOpen, searchTerm, setSearchTerm } = useContext(DropdownContext); const filteredChildren = @@ -177,6 +178,7 @@ function Wrapper({ children }: WrapperProps): JSX.Element { ; export const Default: Story = { render: () => { - const [selectedValue, setSelectedValue] = useState(""); + const [selectedValue, setSelectedValue] = useState(""); return ( - setSelectedValue(value)}> + setSelectedValue(_value)}> 최신순 아이템 1 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4be5847c..2e4a159f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -221,7 +221,7 @@ importers: version: 13.0.3(expo@51.0.38(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))) nativewind: specifier: ^4.1.21 - version: 4.1.23(react-native-reanimated@3.10.1(@babel/core@7.26.0)(react-native@0.74.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.10.5(react-native@0.74.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1))(react-native@0.74.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.14(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.3.3))) + version: 4.1.23(react-native-reanimated@3.10.1(@babel/core@7.26.0)(react-native@0.74.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.10.5(react-native@0.74.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1))(react-native@0.74.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.14(ts-node@10.9.2(typescript@5.3.3))) react: specifier: ^18.2.0 version: 18.3.1 @@ -248,7 +248,7 @@ importers: version: 0.19.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1) tailwindcss: specifier: ^3.4.14 - version: 3.4.14(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.3.3)) + version: 3.4.14(ts-node@10.9.2(typescript@5.3.3)) devDependencies: '@babel/core': specifier: ^7.20.0 @@ -279,10 +279,10 @@ importers: version: 7.1.2(eslint@8.57.1)(typescript@5.3.3) jest: specifier: ^29.2.1 - version: 29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.3.3)) + version: 29.7.0(ts-node@10.9.2(typescript@5.3.3)) jest-expo: specifier: ~51.0.3 - version: 51.0.4(@babel/core@7.26.0)(jest@29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.3.3)))(react@18.3.1) + version: 51.0.4(@babel/core@7.26.0)(jest@29.7.0(ts-node@10.9.2(typescript@5.3.3)))(react@18.3.1) react-test-renderer: specifier: 18.2.0 version: 18.2.0(react@18.3.1) @@ -413,6 +413,9 @@ importers: cookies-next: specifier: ^4.3.0 version: 4.3.0 + date-fns: + specifier: ^4.1.0 + version: 4.1.0 es-toolkit: specifier: ^1.26.1 version: 1.26.1 @@ -566,7 +569,7 @@ importers: version: 7.18.0(eslint@8.57.1)(typescript@5.2.2) '@vercel/style-guide': specifier: ^6.0.0 - version: 6.0.0(@next/eslint-plugin-next@14.2.6)(eslint@8.57.1)(jest@29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.2.2)))(prettier@3.3.3)(typescript@5.2.2) + version: 6.0.0(@next/eslint-plugin-next@14.2.6)(eslint@8.57.1)(jest@29.7.0)(prettier@3.3.3)(typescript@5.2.2) eslint-config-prettier: specifier: ^9.1.0 version: 9.1.0(eslint@8.57.1) @@ -620,7 +623,7 @@ importers: version: 29.5.14 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.2.2)) + version: 29.7.0(ts-node@10.9.2(typescript@5.2.2)) jest-environment-jsdom: specifier: ^29.7.0 version: 29.7.0 @@ -629,7 +632,7 @@ importers: version: 18.3.1(react@18.3.1) ts-jest: specifier: ^29.2.5 - version: 29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(jest@29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.2.2)))(typescript@5.2.2) + version: 29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(jest@29.7.0(ts-node@10.9.2(typescript@5.2.2)))(typescript@5.2.2) typescript: specifier: 5.2.2 version: 5.2.2 @@ -648,7 +651,7 @@ importers: dependencies: tailwindcss-rem-to-px: specifier: ^0.1.1 - version: 0.1.1(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.2.2)) + version: 0.1.1(ts-node@10.9.2(typescript@5.2.2)) devDependencies: '@repo/typescript-config': specifier: workspace:* @@ -661,7 +664,7 @@ importers: version: 8.4.47 tailwindcss: specifier: ^3.4.0 - version: 3.4.14(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.2.2)) + version: 3.4.14(ts-node@10.9.2(typescript@5.2.2)) typescript: specifier: 5.2.2 version: 5.2.2 @@ -4705,6 +4708,9 @@ packages: resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} engines: {node: '>= 0.4'} + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + dayjs@1.11.13: resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} @@ -11627,7 +11633,7 @@ snapshots: jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@29.7.0(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.2.2))': + '@jest/core@29.7.0(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -11641,7 +11647,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.2.2)) + jest-config: 29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -11662,7 +11668,7 @@ snapshots: - supports-color - ts-node - '@jest/core@29.7.0(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.3.3))': + '@jest/core@29.7.0(ts-node@10.9.2(typescript@5.2.2))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -11676,7 +11682,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.3.3)) + jest-config: 29.7.0(@types/node@20.17.6)(ts-node@10.9.2(typescript@5.2.2)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -11697,7 +11703,7 @@ snapshots: - supports-color - ts-node - '@jest/core@29.7.0(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3))': + '@jest/core@29.7.0(ts-node@10.9.2(typescript@5.3.3))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -11711,7 +11717,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) + jest-config: 29.7.0(@types/node@20.17.6)(ts-node@10.9.2(typescript@5.3.3)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -14115,7 +14121,7 @@ snapshots: graphql: 15.8.0 wonka: 4.0.15 - '@vercel/style-guide@6.0.0(@next/eslint-plugin-next@14.2.6)(eslint@8.57.1)(jest@29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.2.2)))(prettier@3.3.3)(typescript@5.2.2)': + '@vercel/style-guide@6.0.0(@next/eslint-plugin-next@14.2.6)(eslint@8.57.1)(jest@29.7.0)(prettier@3.3.3)(typescript@5.2.2)': dependencies: '@babel/core': 7.26.0 '@babel/eslint-parser': 7.25.9(@babel/core@7.26.0)(eslint@8.57.1) @@ -14127,9 +14133,9 @@ snapshots: eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.2.2))(eslint-plugin-import@2.31.0)(eslint@8.57.1) eslint-plugin-eslint-comments: 3.2.0(eslint@8.57.1) eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.2.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) - eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.2.2))(eslint@8.57.1)(typescript@5.2.2))(eslint@8.57.1)(jest@29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.2.2)))(typescript@5.2.2) + eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.2.2))(eslint@8.57.1)(typescript@5.2.2))(eslint@8.57.1)(jest@29.7.0)(typescript@5.2.2) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) - eslint-plugin-playwright: 1.8.3(eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.2.2))(eslint@8.57.1)(typescript@5.2.2))(eslint@8.57.1)(jest@29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.2.2)))(typescript@5.2.2))(eslint@8.57.1) + eslint-plugin-playwright: 1.8.3(eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.2.2))(eslint@8.57.1)(typescript@5.2.2))(eslint@8.57.1)(jest@29.7.0)(typescript@5.2.2))(eslint@8.57.1) eslint-plugin-react: 7.37.2(eslint@8.57.1) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.1) eslint-plugin-testing-library: 6.4.0(eslint@8.57.1)(typescript@5.2.2) @@ -15058,13 +15064,13 @@ snapshots: optionalDependencies: typescript: 5.6.3 - create-jest@29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.2.2)): + create-jest@29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.2.2)) + jest-config: 29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -15073,13 +15079,13 @@ snapshots: - supports-color - ts-node - create-jest@29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.3.3)): + create-jest@29.7.0(ts-node@10.9.2(typescript@5.2.2)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.3.3)) + jest-config: 29.7.0(@types/node@20.17.6)(ts-node@10.9.2(typescript@5.2.2)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -15088,13 +15094,13 @@ snapshots: - supports-color - ts-node - create-jest@29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)): + create-jest@29.7.0(ts-node@10.9.2(typescript@5.3.3)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) + jest-config: 29.7.0(@types/node@20.17.6)(ts-node@10.9.2(typescript@5.3.3)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -15213,6 +15219,8 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.1 + date-fns@4.1.0: {} + dayjs@1.11.13: {} debug@2.6.9: @@ -15956,13 +15964,13 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.2.2))(eslint@8.57.1)(typescript@5.2.2))(eslint@8.57.1)(jest@29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.2.2)))(typescript@5.2.2): + eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.2.2))(eslint@8.57.1)(typescript@5.2.2))(eslint@8.57.1)(jest@29.7.0)(typescript@5.2.2): dependencies: '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.2.2) eslint: 8.57.1 optionalDependencies: '@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.2.2))(eslint@8.57.1)(typescript@5.2.2) - jest: 29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.2.2)) + jest: 29.7.0(ts-node@10.9.2(typescript@5.2.2)) transitivePeerDependencies: - supports-color - typescript @@ -15998,12 +16006,12 @@ snapshots: eslint-plugin-only-warn@1.1.0: {} - eslint-plugin-playwright@1.8.3(eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.2.2))(eslint@8.57.1)(typescript@5.2.2))(eslint@8.57.1)(jest@29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.2.2)))(typescript@5.2.2))(eslint@8.57.1): + eslint-plugin-playwright@1.8.3(eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.2.2))(eslint@8.57.1)(typescript@5.2.2))(eslint@8.57.1)(jest@29.7.0)(typescript@5.2.2))(eslint@8.57.1): dependencies: eslint: 8.57.1 globals: 13.24.0 optionalDependencies: - eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.2.2))(eslint@8.57.1)(typescript@5.2.2))(eslint@8.57.1)(jest@29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.2.2)))(typescript@5.2.2) + eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.2.2))(eslint@8.57.1)(typescript@5.2.2))(eslint@8.57.1)(jest@29.7.0)(typescript@5.2.2) eslint-plugin-prettier@5.2.1(@types/eslint@8.56.12)(eslint-config-prettier@9.1.0(eslint@8.57.1))(eslint@8.57.1)(prettier@3.3.3): dependencies: @@ -17362,16 +17370,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.2.2)): + jest-cli@29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.2.2)) + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.2.2)) + create-jest: 29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.2.2)) + jest-config: 29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -17381,16 +17389,16 @@ snapshots: - supports-color - ts-node - jest-cli@29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.3.3)): + jest-cli@29.7.0(ts-node@10.9.2(typescript@5.2.2)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.3.3)) + '@jest/core': 29.7.0(ts-node@10.9.2(typescript@5.2.2)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.3.3)) + create-jest: 29.7.0(ts-node@10.9.2(typescript@5.2.2)) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.3.3)) + jest-config: 29.7.0(@types/node@20.17.6)(ts-node@10.9.2(typescript@5.2.2)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -17400,16 +17408,16 @@ snapshots: - supports-color - ts-node - jest-cli@29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)): + jest-cli@29.7.0(ts-node@10.9.2(typescript@5.3.3)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) + '@jest/core': 29.7.0(ts-node@10.9.2(typescript@5.3.3)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) + create-jest: 29.7.0(ts-node@10.9.2(typescript@5.3.3)) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) + jest-config: 29.7.0(@types/node@20.17.6)(ts-node@10.9.2(typescript@5.3.3)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -17419,7 +17427,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.2.2)): + jest-config@29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)): dependencies: '@babel/core': 7.26.0 '@jest/test-sequencer': 29.7.0 @@ -17445,12 +17453,12 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 20.17.6 - ts-node: 10.9.2(@types/node@20.17.6)(typescript@5.2.2) + ts-node: 10.9.2(@types/node@20.17.6)(typescript@5.6.3) transitivePeerDependencies: - babel-plugin-macros - supports-color - jest-config@29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.3.3)): + jest-config@29.7.0(@types/node@20.17.6)(ts-node@10.9.2(typescript@5.2.2)): dependencies: '@babel/core': 7.26.0 '@jest/test-sequencer': 29.7.0 @@ -17476,12 +17484,12 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 20.17.6 - ts-node: 10.9.2(@types/node@20.17.6)(typescript@5.3.3) + ts-node: 10.9.2(typescript@5.2.2) transitivePeerDependencies: - babel-plugin-macros - supports-color - jest-config@29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)): + jest-config@29.7.0(@types/node@20.17.6)(ts-node@10.9.2(typescript@5.3.3)): dependencies: '@babel/core': 7.26.0 '@jest/test-sequencer': 29.7.0 @@ -17507,7 +17515,7 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 20.17.6 - ts-node: 10.9.2(@types/node@20.17.6)(typescript@5.6.3) + ts-node: 10.9.2(typescript@5.3.3) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -17555,7 +17563,7 @@ snapshots: jest-mock: 29.7.0 jest-util: 29.7.0 - jest-expo@51.0.4(@babel/core@7.26.0)(jest@29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.3.3)))(react@18.3.1): + jest-expo@51.0.4(@babel/core@7.26.0)(jest@29.7.0(ts-node@10.9.2(typescript@5.3.3)))(react@18.3.1): dependencies: '@expo/config': 9.0.4 '@expo/json-file': 8.3.3 @@ -17564,7 +17572,7 @@ snapshots: find-up: 5.0.0 jest-environment-jsdom: 29.7.0 jest-watch-select-projects: 2.0.0 - jest-watch-typeahead: 2.2.1(jest@29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.3.3))) + jest-watch-typeahead: 2.2.1(jest@29.7.0(ts-node@10.9.2(typescript@5.3.3))) json5: 2.2.3 lodash: 4.17.21 react-test-renderer: 18.2.0(react@18.3.1) @@ -17753,11 +17761,11 @@ snapshots: chalk: 3.0.0 prompts: 2.4.2 - jest-watch-typeahead@2.2.1(jest@29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.3.3))): + jest-watch-typeahead@2.2.1(jest@29.7.0(ts-node@10.9.2(typescript@5.3.3))): dependencies: ansi-escapes: 6.2.1 chalk: 4.1.2 - jest: 29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.3.3)) + jest: 29.7.0(ts-node@10.9.2(typescript@5.3.3)) jest-regex-util: 29.6.3 jest-watcher: 29.7.0 slash: 5.1.0 @@ -17782,36 +17790,36 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.2.2)): + jest@29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.2.2)) + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.2.2)) + jest-cli: 29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros - supports-color - ts-node - jest@29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.3.3)): + jest@29.7.0(ts-node@10.9.2(typescript@5.2.2)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.3.3)) + '@jest/core': 29.7.0(ts-node@10.9.2(typescript@5.2.2)) '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.3.3)) + jest-cli: 29.7.0(ts-node@10.9.2(typescript@5.2.2)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros - supports-color - ts-node - jest@29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)): + jest@29.7.0(ts-node@10.9.2(typescript@5.3.3)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) + '@jest/core': 29.7.0(ts-node@10.9.2(typescript@5.3.3)) '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) + jest-cli: 29.7.0(ts-node@10.9.2(typescript@5.3.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -18596,12 +18604,12 @@ snapshots: nanoid@3.3.7: {} - nativewind@4.1.23(react-native-reanimated@3.10.1(@babel/core@7.26.0)(react-native@0.74.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.10.5(react-native@0.74.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1))(react-native@0.74.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.14(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.3.3))): + nativewind@4.1.23(react-native-reanimated@3.10.1(@babel/core@7.26.0)(react-native@0.74.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.10.5(react-native@0.74.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1))(react-native@0.74.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.14(ts-node@10.9.2(typescript@5.3.3))): dependencies: comment-json: 4.2.5 debug: 4.3.7(supports-color@5.5.0) - react-native-css-interop: 0.1.22(react-native-reanimated@3.10.1(@babel/core@7.26.0)(react-native@0.74.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.10.5(react-native@0.74.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1))(react-native@0.74.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.14(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.3.3))) - tailwindcss: 3.4.14(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.3.3)) + react-native-css-interop: 0.1.22(react-native-reanimated@3.10.1(@babel/core@7.26.0)(react-native@0.74.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.10.5(react-native@0.74.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1))(react-native@0.74.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.14(ts-node@10.9.2(typescript@5.3.3))) + tailwindcss: 3.4.14(ts-node@10.9.2(typescript@5.3.3)) transitivePeerDependencies: - react - react-native @@ -19054,29 +19062,29 @@ snapshots: camelcase-css: 2.0.1 postcss: 8.4.47 - postcss-load-config@4.0.2(postcss@8.4.47)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.2.2)): + postcss-load-config@4.0.2(postcss@8.4.47)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)): dependencies: lilconfig: 3.1.2 yaml: 2.6.0 optionalDependencies: postcss: 8.4.47 - ts-node: 10.9.2(@types/node@20.17.6)(typescript@5.2.2) + ts-node: 10.9.2(@types/node@20.17.6)(typescript@5.6.3) - postcss-load-config@4.0.2(postcss@8.4.47)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.3.3)): + postcss-load-config@4.0.2(postcss@8.4.47)(ts-node@10.9.2(typescript@5.2.2)): dependencies: lilconfig: 3.1.2 yaml: 2.6.0 optionalDependencies: postcss: 8.4.47 - ts-node: 10.9.2(@types/node@20.17.6)(typescript@5.3.3) + ts-node: 10.9.2(typescript@5.2.2) - postcss-load-config@4.0.2(postcss@8.4.47)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)): + postcss-load-config@4.0.2(postcss@8.4.47)(ts-node@10.9.2(typescript@5.3.3)): dependencies: lilconfig: 3.1.2 yaml: 2.6.0 optionalDependencies: postcss: 8.4.47 - ts-node: 10.9.2(@types/node@20.17.6)(typescript@5.6.3) + ts-node: 10.9.2(typescript@5.3.3) postcss-nested@6.2.0(postcss@8.4.47): dependencies: @@ -19317,7 +19325,7 @@ snapshots: framer-motion: 11.11.11(@emotion/is-prop-valid@1.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 - react-native-css-interop@0.1.22(react-native-reanimated@3.10.1(@babel/core@7.26.0)(react-native@0.74.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.10.5(react-native@0.74.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1))(react-native@0.74.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.14(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.3.3))): + react-native-css-interop@0.1.22(react-native-reanimated@3.10.1(@babel/core@7.26.0)(react-native@0.74.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.10.5(react-native@0.74.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1))(react-native@0.74.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.14(ts-node@10.9.2(typescript@5.3.3))): dependencies: '@babel/helper-module-imports': 7.25.9 '@babel/traverse': 7.25.9 @@ -19328,7 +19336,7 @@ snapshots: react-native: 0.74.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.2.79)(react@18.3.1) react-native-reanimated: 3.10.1(@babel/core@7.26.0)(react-native@0.74.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1) semver: 7.6.3 - tailwindcss: 3.4.14(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.3.3)) + tailwindcss: 3.4.14(ts-node@10.9.2(typescript@5.3.3)) optionalDependencies: react-native-safe-area-context: 4.10.5(react-native@0.74.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.2.79)(react@18.3.1))(react@18.3.1) transitivePeerDependencies: @@ -20356,13 +20364,13 @@ snapshots: tailwind-merge@2.5.4: {} - tailwindcss-rem-to-px@0.1.1(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.2.2)): + tailwindcss-rem-to-px@0.1.1(ts-node@10.9.2(typescript@5.2.2)): dependencies: - tailwindcss: 3.4.14(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.2.2)) + tailwindcss: 3.4.14(ts-node@10.9.2(typescript@5.2.2)) transitivePeerDependencies: - ts-node - tailwindcss@3.4.14(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.2.2)): + tailwindcss@3.4.14(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -20381,7 +20389,7 @@ snapshots: postcss: 8.4.47 postcss-import: 15.1.0(postcss@8.4.47) postcss-js: 4.0.1(postcss@8.4.47) - postcss-load-config: 4.0.2(postcss@8.4.47)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.2.2)) + postcss-load-config: 4.0.2(postcss@8.4.47)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) postcss-nested: 6.2.0(postcss@8.4.47) postcss-selector-parser: 6.1.2 resolve: 1.22.8 @@ -20389,7 +20397,7 @@ snapshots: transitivePeerDependencies: - ts-node - tailwindcss@3.4.14(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.3.3)): + tailwindcss@3.4.14(ts-node@10.9.2(typescript@5.2.2)): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -20408,7 +20416,7 @@ snapshots: postcss: 8.4.47 postcss-import: 15.1.0(postcss@8.4.47) postcss-js: 4.0.1(postcss@8.4.47) - postcss-load-config: 4.0.2(postcss@8.4.47)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.3.3)) + postcss-load-config: 4.0.2(postcss@8.4.47)(ts-node@10.9.2(typescript@5.2.2)) postcss-nested: 6.2.0(postcss@8.4.47) postcss-selector-parser: 6.1.2 resolve: 1.22.8 @@ -20416,7 +20424,7 @@ snapshots: transitivePeerDependencies: - ts-node - tailwindcss@3.4.14(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)): + tailwindcss@3.4.14(ts-node@10.9.2(typescript@5.3.3)): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -20435,7 +20443,7 @@ snapshots: postcss: 8.4.47 postcss-import: 15.1.0(postcss@8.4.47) postcss-js: 4.0.1(postcss@8.4.47) - postcss-load-config: 4.0.2(postcss@8.4.47)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) + postcss-load-config: 4.0.2(postcss@8.4.47)(ts-node@10.9.2(typescript@5.3.3)) postcss-nested: 6.2.0(postcss@8.4.47) postcss-selector-parser: 6.1.2 resolve: 1.22.8 @@ -20606,12 +20614,12 @@ snapshots: babel-jest: 29.7.0(@babel/core@7.26.0) esbuild: 0.20.2 - ts-jest@29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(jest@29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.2.2)))(typescript@5.2.2): + ts-jest@29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(jest@29.7.0(ts-node@10.9.2(typescript@5.2.2)))(typescript@5.2.2): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.2.2)) + jest: 29.7.0(ts-node@10.9.2(typescript@5.2.2)) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 @@ -20625,7 +20633,7 @@ snapshots: '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@7.26.0) - ts-node@10.9.2(@types/node@20.17.6)(typescript@5.2.2): + ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 @@ -20639,47 +20647,45 @@ snapshots: create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.2.2 + typescript: 5.6.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 - optional: true - ts-node@10.9.2(@types/node@20.17.6)(typescript@5.3.3): + ts-node@10.9.2(typescript@5.2.2): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 20.17.6 acorn: 8.14.0 acorn-walk: 8.3.4 arg: 4.1.3 create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.3.3 + typescript: 5.2.2 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 optional: true - ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3): + ts-node@10.9.2(typescript@5.3.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 20.17.6 acorn: 8.14.0 acorn-walk: 8.3.4 arg: 4.1.3 create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.6.3 + typescript: 5.3.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + optional: true tsconfck@3.1.4(typescript@5.6.3): optionalDependencies: