diff --git a/frontend/src/apis/TimeTableAPI.ts b/frontend/src/apis/TimeTableAPI.ts index e607b176..eb02cb74 100644 --- a/frontend/src/apis/TimeTableAPI.ts +++ b/frontend/src/apis/TimeTableAPI.ts @@ -63,6 +63,7 @@ export const usePostTimeSlot = () => { return { createTimeSlot: mutation.mutate, + createTimeSlotAsync: mutation.mutateAsync, error: mutation.error, }; }; diff --git a/frontend/src/features/timeTable/components/AvailableTimeSlot.tsx b/frontend/src/features/timeTable/components/AvailableTimeSlot.tsx index 3dcc8415..edd11cfc 100644 --- a/frontend/src/features/timeTable/components/AvailableTimeSlot.tsx +++ b/frontend/src/features/timeTable/components/AvailableTimeSlot.tsx @@ -8,7 +8,6 @@ import type { } from "@/features/timeTable/TimeTable.type"; import { getTimeSlotGridStyle } from "@/features/timeTable/TimeTable.style"; import { CloseIcon } from "@/assets/icons"; -import { useState } from "react"; import { isBeforeToday } from "@/features/calender/Calender.util"; const { color, typography } = theme; @@ -30,8 +29,6 @@ const AvailableTimeSlot = ({ onDelete, disabled = false, }: AvailableTimeSlotProps) => { - const [hovered, setHovered] = useState(false); - const isPastDate = isBeforeToday(slot.rentalDate); const canInteract = @@ -45,21 +42,19 @@ const AvailableTimeSlot = ({ return (
canInteract && setHovered(true)} - onMouseLeave={() => canInteract && setHovered(false)} css={[ getTimeSlotGridStyle(slot, selectedWeek), - getSlotContainerStyle(isPastDate, variant, canInteract, hovered), + getSlotContainerStyle(isPastDate, variant, canInteract), ]} > {variant === "default" && ( <>
유휴 시간
- {canInteract && hovered && ( + {canInteract && ( @@ -79,7 +74,6 @@ export default AvailableTimeSlot; const BaseSlotStyle = ( variant: "default" | "preview", canInteract: boolean, - hovered: boolean, ) => css` position: relative; height: calc(100% - 16px); // 상하 margin 제외 @@ -91,19 +85,20 @@ const BaseSlotStyle = ( flex-direction: column; justify-content: space-between; pointer-events: ${canInteract ? "auto" : "none"}; + transition: + transform 140ms ease, + box-shadow 140ms ease, + filter 140ms ease; - ${hovered && - canInteract && + ${canInteract && variant === "default" && css` - transition: - transform 140ms ease, - box-shadow 140ms ease, - filter 140ms ease; - will-change: transform, box-shadow; - transform: translateY(-2px) scale(1.01); - box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1); - filter: brightness(0.98); + &:hover { + will-change: transform, box-shadow; + transform: translateY(-2px) scale(1.01); + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1); + filter: brightness(0.98); + } `} `; @@ -126,9 +121,8 @@ const getSlotContainerStyle = ( isPastDate: boolean, variant: "default" | "preview", canInteract: boolean, - hovered: boolean, ) => { - const styles = [BaseSlotStyle(variant, canInteract, hovered)]; + const styles = [BaseSlotStyle(variant, canInteract)]; if (isPastDate) { styles.push(PastSlotStyle); @@ -138,11 +132,16 @@ const getSlotContainerStyle = ( return styles; }; -const DeleteButtonStyle = (hovered: boolean) => css` +const DeleteButtonStyle = css` position: absolute; top: 16px; right: 20px; border: none; cursor: pointer; - color: ${hovered ? color.Maincolor.primary : color.GrayScale.gray4}; + opacity: 0; + color: ${color.Maincolor.primary}; + + *:hover > & { + opacity: 1; + } `; diff --git a/frontend/src/features/timeTable/components/TimeTable.tsx b/frontend/src/features/timeTable/components/TimeTable.tsx index 766217f0..40f6eb85 100644 --- a/frontend/src/features/timeTable/components/TimeTable.tsx +++ b/frontend/src/features/timeTable/components/TimeTable.tsx @@ -79,12 +79,11 @@ const TimeTable = ({ const slotsDisabled = Boolean(previewSlot); const queryClient = useQueryClient(); - const { createTimeSlot, error: postError } = usePostTimeSlot(); + const { createTimeSlotAsync, error: postError } = usePostTimeSlot(); const { timeSlotData, isFetching, error: fetchError, - refetch, } = useGetTimeSlot(selectedCarId, nextWeekKey); // Error 관련 상태 @@ -109,16 +108,24 @@ const TimeTable = ({ setPreviewSlot: setPreviewSlot, }); - // selectedCarId나 selectedWeek이 변경되면 draft 상태 초기화 및 애니메이션 처리 + // selectedCarId나 selectedWeek이 변경되면 상태 초기화 및 애니메이션 처리 + // 기존 Effect 1(draft 초기화 + displayWeek 전환)과 Effect 2(handleCancelClick)를 병합 + // deps: onEditModeChange, queryClient, queryKey는 트리거 목적이 아니므로 의도적 제외 useEffect(() => { + onEditModeChange?.(false); setShowSlots(false); - setTimeSlotsDraft([]); setPreviewSlot(null); - const rafId = window.requestAnimationFrame(() => { + const cachedSlotData = + queryClient.getQueryData(queryKey); + setTimeSlotsDraft(cachedSlotData?.data ?? []); + + const rafId = requestAnimationFrame(() => { setDisplayWeek(selectedWeek); + setShowSlots(true); }); - return () => window.cancelAnimationFrame(rafId); + return () => cancelAnimationFrame(rafId); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedCarId, selectedWeek]); // 새로운 데이터가 로드되면 draft 상태 초기화 @@ -142,17 +149,12 @@ const TimeTable = ({ onEditModeChange?.(false); }, [queryClient, queryKey, onEditModeChange]); - useEffect(() => { - handleCancelClick(); - }, [selectedWeek, selectedCarId, handleCancelClick]); - - const handleSaveClick = () => { - createTimeSlot({ + const handleSaveClick = async () => { + await createTimeSlotAsync({ selectedCarId, weekKey, timeSlots: timeSlotsDraft, }); - void refetch(); onEditModeChange?.(false); }; diff --git a/frontend/src/features/timeTable/services/slotMerger.ts b/frontend/src/features/timeTable/services/slotMerger.ts index af2c927a..8151b0e9 100644 --- a/frontend/src/features/timeTable/services/slotMerger.ts +++ b/frontend/src/features/timeTable/services/slotMerger.ts @@ -3,6 +3,7 @@ import { formatTimeToNumber, } from "@/features/calender/Calender.util"; import type { AvailableTimeSlotType } from "@/features/timeTable/TimeTable.type"; +import { slotUtils } from "@/features/timeTable/utils/slotUtils"; export const slotMerger = { /** @@ -53,7 +54,8 @@ export const slotMerger = { if (overlappingSlots.length > 0) { const mergedSlot = slotMerger.mergeSlots(newSlot, overlappingSlots); const remainingSlots = availableTimeSlots.filter( - (slot) => !overlappingSlots.includes(slot), + (slot) => + !overlappingSlots.some((o) => slotUtils.isSameSlot(o, slot)), ); return [mergedSlot, ...remainingSlots]; diff --git a/frontend/src/features/timeTable/utils/slotUtils.ts b/frontend/src/features/timeTable/utils/slotUtils.ts index 34839ebd..23558d87 100644 --- a/frontend/src/features/timeTable/utils/slotUtils.ts +++ b/frontend/src/features/timeTable/utils/slotUtils.ts @@ -17,30 +17,4 @@ export const slotUtils = { a.rentalEndTime === b.rentalEndTime ); }, - - /** - * 슬롯 배열에서 특정 슬롯을 찾아 제거합니다. - * @param slots 슬롯 배열 - * @param targetSlot 제거할 대상 슬롯 - * @returns 대상 슬롯이 제거된 새로운 배열 - */ - removeSlot: ( - slots: AvailableTimeSlotType[], - targetSlot: AvailableTimeSlotType, - ): AvailableTimeSlotType[] => { - return slots.filter((slot) => !slotUtils.isSameSlot(slot, targetSlot)); - }, - - /** - * 슬롯 배열에 새로운 슬롯을 추가합니다. - * @param slots 기존 슬롯 배열 - * @param newSlot 추가할 새로운 슬롯 - * @returns 새로운 슬롯이 추가된 배열 - */ - addSlot: ( - slots: AvailableTimeSlotType[], - newSlot: AvailableTimeSlotType, - ): AvailableTimeSlotType[] => { - return [newSlot, ...slots]; - }, };