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];
- },
};