From 31b7459965a2cbaf5e2aaa9316dfdfc21924522e Mon Sep 17 00:00:00 2001 From: jjamming Date: Sat, 7 Mar 2026 22:13:30 +0900 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20handleSaveClick=20=EB=B9=84=EB=8F=99?= =?UTF-8?q?=EA=B8=B0=20=EC=B2=98=EB=A6=AC=20=EB=B0=8F=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20API=20=ED=98=B8=EC=B6=9C=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - usePostTimeSlot에서 mutateAsync 노출 (createTimeSlotAsync) - handleSaveClick을 async/await로 변경하여 mutation 완료 대기 - refetch() 제거 (onSuccess에서 setQueryData로 캐시 업데이트 중이므로 불필요) - onEditModeChange는 mutation 성공 후 호출 Co-Authored-By: Claude Opus 4.6 --- frontend/src/apis/TimeTableAPI.ts | 1 + frontend/src/features/timeTable/components/TimeTable.tsx | 8 +++----- 2 files changed, 4 insertions(+), 5 deletions(-) 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/TimeTable.tsx b/frontend/src/features/timeTable/components/TimeTable.tsx index 766217f0..ef681f16 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 관련 상태 @@ -146,13 +145,12 @@ const TimeTable = ({ handleCancelClick(); }, [selectedWeek, selectedCarId, handleCancelClick]); - const handleSaveClick = () => { - createTimeSlot({ + const handleSaveClick = async () => { + await createTimeSlotAsync({ selectedCarId, weekKey, timeSlots: timeSlotsDraft, }); - void refetch(); onEditModeChange?.(false); }; From a25e8a4d85338e3a6de7005b5888a95183b94a41 Mon Sep 17 00:00:00 2001 From: jjamming Date: Sat, 7 Mar 2026 22:14:03 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20useEffect=20=EA=B2=BD=EC=9F=81=20?= =?UTF-8?q?=EC=A1=B0=EA=B1=B4=20=ED=95=B4=EC=86=8C=20-=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=20effect=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - selectedCarId/selectedWeek deps를 공유하는 두 개의 useEffect를 단일 effect로 병합 - handleCancelClick 호출 대신 직접 상태 초기화 수행 (deps 변경에 의한 불필요한 effect 재실행 방지) - rAF로 displayWeek 전환 + showSlots 복원을 단일 effect에서 관리 - window.requestAnimationFrame → requestAnimationFrame 통일 Co-Authored-By: Claude Opus 4.6 --- .../timeTable/components/TimeTable.tsx | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/frontend/src/features/timeTable/components/TimeTable.tsx b/frontend/src/features/timeTable/components/TimeTable.tsx index ef681f16..40f6eb85 100644 --- a/frontend/src/features/timeTable/components/TimeTable.tsx +++ b/frontend/src/features/timeTable/components/TimeTable.tsx @@ -108,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 상태 초기화 @@ -141,10 +149,6 @@ const TimeTable = ({ onEditModeChange?.(false); }, [queryClient, queryKey, onEditModeChange]); - useEffect(() => { - handleCancelClick(); - }, [selectedWeek, selectedCarId, handleCancelClick]); - const handleSaveClick = async () => { await createTimeSlotAsync({ selectedCarId, From 5f855bfcf0321ac2ef558bb96f6fef7b15efb580 Mon Sep 17 00:00:00 2001 From: jjamming Date: Sat, 7 Mar 2026 22:15:11 +0900 Subject: [PATCH 3/3] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=20=EC=9C=84?= =?UTF-8?q?=EC=83=9D=20=EA=B0=9C=EC=84=A0=20(hover=20=EC=B5=9C=EC=A0=81?= =?UTF-8?q?=ED=99=94,=20=EC=B0=B8=EC=A1=B0=EB=B9=84=EA=B5=90=20=EC=88=98?= =?UTF-8?q?=EC=A0=95,=20=EB=AF=B8=EC=82=AC=EC=9A=A9=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20=EC=A0=9C=EA=B1=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AvailableTimeSlot: hover useState → CSS :hover로 전환 (슬롯당 리렌더링 2회 → 0회) - AvailableTimeSlot: 삭제 버튼 opacity: 0 → :hover opacity: 1 패턴 적용 - slotMerger: includes 참조 비교 → slotUtils.isSameSlot 값 비교로 수정 - slotUtils: 미사용 removeSlot, addSlot 함수 제거 Co-Authored-By: Claude Opus 4.6 --- .../components/AvailableTimeSlot.tsx | 45 +++++++++---------- .../features/timeTable/services/slotMerger.ts | 4 +- .../src/features/timeTable/utils/slotUtils.ts | 26 ----------- 3 files changed, 25 insertions(+), 50 deletions(-) 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/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]; - }, };