Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/src/apis/TimeTableAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export const usePostTimeSlot = () => {

return {
createTimeSlot: mutation.mutate,
createTimeSlotAsync: mutation.mutateAsync,
error: mutation.error,
};
};
45 changes: 22 additions & 23 deletions frontend/src/features/timeTable/components/AvailableTimeSlot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -30,8 +29,6 @@ const AvailableTimeSlot = ({
onDelete,
disabled = false,
}: AvailableTimeSlotProps) => {
const [hovered, setHovered] = useState(false);

const isPastDate = isBeforeToday(slot.rentalDate);

const canInteract =
Expand All @@ -45,21 +42,19 @@ const AvailableTimeSlot = ({

return (
<div
onMouseEnter={() => canInteract && setHovered(true)}
onMouseLeave={() => canInteract && setHovered(false)}
css={[
getTimeSlotGridStyle(slot, selectedWeek),
getSlotContainerStyle(isPastDate, variant, canInteract, hovered),
getSlotContainerStyle(isPastDate, variant, canInteract),
]}
>
{variant === "default" && (
<>
<header>유휴 시간</header>
{canInteract && hovered && (
{canInteract && (
<button
type="button"
onClick={handleDeleteClick}
css={DeleteButtonStyle(hovered)}
css={DeleteButtonStyle}
>
<CloseIcon fill={color.Maincolor.primary} />
</button>
Expand All @@ -79,7 +74,6 @@ export default AvailableTimeSlot;
const BaseSlotStyle = (
variant: "default" | "preview",
canInteract: boolean,
hovered: boolean,
) => css`
position: relative;
height: calc(100% - 16px); // 상하 margin 제외
Expand All @@ -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);
}
`}
`;

Expand All @@ -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);
Expand All @@ -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;
}
`;
28 changes: 15 additions & 13 deletions frontend/src/features/timeTable/components/TimeTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 관련 상태
Expand All @@ -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<TimeSlotAPIResponse>(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 상태 초기화
Expand All @@ -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);
};

Expand Down
4 changes: 3 additions & 1 deletion frontend/src/features/timeTable/services/slotMerger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
/**
Expand Down Expand Up @@ -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];
Expand Down
26 changes: 0 additions & 26 deletions frontend/src/features/timeTable/utils/slotUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
},
};