이제 마지막(일 수도 있고 아닐 수도 있지만 일단은 마지막일 것 같은)으로 추가해야 하는 기능은 유저가 클릭한 날짜 사이에 기존 예약이 있으면 클릭을 취소 시키는 것이다. 이건 아마 store에서 시도해야할듯?
지금은 이렇게 기예약 날짜가 사이에 껴있어도 예약하기가 가능한 상태이다.
이러면 안되는거좌나?
import { configureStore, createSlice, PayloadAction } from '@reduxjs/toolkit';
import { monthlyReservationStore } from './MonthlyReservationStore';
import { StartEndDateProps } from '../model/IStartEndDateProps';
type DateProps = { year: number; month: number; date: number };
// isWithinPeriod 함수를 생성하여 유저가 클릭한 날짜 사이에 기존 예약이 있는지 확인한다.
function isWithinPeriod(
startDate: DateProps,
endDate: DateProps,
reservation: StartEndDateProps,
) {
const start = new Date(startDate.year, startDate.month - 1, startDate.date);
const end = new Date(endDate.year, endDate.month - 1, endDate.date);
const reservationStart = new Date(
reservation.startDate.year,
reservation.startDate.month - 1,
reservation.startDate.date,
);
const reservationEnd = new Date(
reservation.endDate.year,
reservation.endDate.month - 1,
reservation.endDate.date,
);
// 시작 날짜와 종료 날짜가 예약 기간 안에 있는지 확인
if (start >= reservationStart && start <= reservationEnd) return true;
if (end >= reservationStart && end <= reservationEnd) return true;
// 예약이 시작 날짜와 종료 날짜 사이에 있는지 확인
if (reservationStart >= start && reservationStart <= end) return true;
if (reservationEnd >= start && reservationEnd <= end) return true;
return false;
}
type ReservationProps = {
startDate: { year: number; month: number; date: number } | null;
endDate: { year: number; month: number; date: number } | null;
};
const initialReservationState: ReservationProps = {
startDate: null,
endDate: null,
};
export const reservation = createSlice({
name: 'reservationReducer',
initialState: initialReservationState,
reducers: {
setStartDate: (state, action: PayloadAction<ReservationProps>) => {
const newStart = action.payload.startDate;
if (!newStart || !newStart.date) return;
const newStartYear = newStart.year;
const newStartMonth = newStart.month;
const newStartDate = newStart.date;
const today = new Date();
const todaysYear = today.getFullYear();
const todaysMonth = today.getMonth() + 1;
const todaysDate = today.getDate();
if (
newStartYear > todaysYear ||
(newStartYear === todaysYear && newStartMonth > todaysMonth) ||
(newStartYear === todaysYear &&
newStartMonth === todaysMonth &&
newStartDate >= todaysDate)
) {
state.startDate = newStart;
}
},
setEndDate: (state, action: PayloadAction<ReservationProps>) => {
const newEnd = action.payload.endDate;
if (!newEnd || !newEnd.date) return;
const newEndYear = newEnd.year;
const newEndMonth = newEnd.month;
const newEndDate = newEnd.date;
const currentStart = state.startDate;
if (!currentStart) return;
const currentStartYear = currentStart.year;
const currentStartMonth = currentStart.month;
const currentStartDate = currentStart.date;
// getState을 사용하여 기존 예약 내역 상태를 가져온다.
const { reservationsDate1, reservationsDate2 } =
monthlyReservationStore.getState();
const allReservations = [...reservationsDate1, ...reservationsDate2];
// 유저가 클릭한 날짜 사이에 기존 예약이 있는지 확인한다.
for (const reservation of allReservations) {
if (isWithinPeriod(currentStart, newEnd, reservation)) {
alert(
'선택하신 기간에는 이미 예약이 있습니다. 다른 기간을 선택해 주세요.',
);
return;
}
}
if (
currentStartYear < newEndYear ||
(currentStartYear === newEndYear && currentStartMonth < newEndMonth) ||
(currentStartYear === newEndYear &&
currentStartMonth === newEndMonth &&
currentStartDate <= newEndDate)
) {
state.endDate = newEnd;
}
},
clearReservationDates: () => {
return initialReservationState;
},
},
});
export const reservationStore = configureStore({
reducer: reservation.reducer,
});
export const { setStartDate, setEndDate } = reservation.actions;
유저가 클릭한 날짜 사이에 기존 예약이 있는지 확인하는 isWithinPeriod 함수를 생성했다.
그리고 getDate를 이용하여 MonthlyReservationStore의 상태를 얻어올 수 있다. 다만 store끼리 상태 정보를 공유하는 것은 상태 간 응집력(cohesion)이 강해지게 만들어 유지보수 측면에서 그닥 좋은 결정은 아니라고 한다. 되도록이면 피해야 하지만, 나는 아직 초보이기 때문에 일단은 이러한 문제 발생 가능성을 인지한 채로 사용을 결정하고, 나중에 리팩토링 방안을 마련해보기로 했다.
근데... 버그 발생!!!!
9월 2일부터 9월 21일까지 예약이 왜 가능하죠?
clickRightArrow: (
state,
action: PayloadAction<{ startDate: string; endDate: string }[]>,
) => {
state.reservationsDate1 = state.reservationsDate2;
state.reservationsDate2 = turnStringArrIntoDateObjectArr(action.payload);
},
분명 오른쪽 화살표를 클릭했을 때 reservation1에 8월 달력이 저장될 것이고, reservation2에 9월 달력이 저장될텐데 말입니다. 그리고 아래 코드에서 allReservations에 8월과 9월의 예약이 들어갈텐데 말입니다!!!!!!!
const { reservationsDate1, reservationsDate2 } =
monthlyReservationStore.getState();
const allReservations = [...reservationsDate1, ...reservationsDate2];
console.log('allReservations', allReservations);
for (const reservation of allReservations) {
if (isWithinPeriod(currentStart, newEnd, reservation)) {
alert(
'선택하신 기간에는 이미 예약이 있습니다. 다른 기간을 선택해 주세요.',
);
return;
}
}
왜 8월, 9월의 예약 데이터를 순회하면서 확인을 할텐데 9월의 데이터는 왜 false를 뱉었을까?
아 원인을 찾았다.
setEndDate: (state, action: PayloadAction<ReservationProps>) => {
const newEnd = action.payload.endDate;
if (!newEnd || !newEnd.date) return;
const newEndYear = newEnd.year;
const newEndMonth = newEnd.month;
const newEndDate = newEnd.date;
const currentStart = state.startDate;
if (!currentStart) return;
const currentStartYear = currentStart.year;
const currentStartMonth = currentStart.month;
const currentStartDate = currentStart.date;
const { reservationsDate1, reservationsDate2 } =
monthlyReservationStore.getState();
// BUG: 7, 8월 데이터만 들어가 있는 상태에서 for문을 돌리면, 9월에는 예약이 있는지 확인할 수 없다. 😈
const allReservations = [...reservationsDate1, ...reservationsDate2];
console.log('allReservations', allReservations);
for (const reservation of allReservations) {
if (isWithinPeriod(currentStart, newEnd, reservation)) {
alert(
'선택하신 기간에는 이미 예약이 있습니다. 다른 기간을 선택해 주세요.',
);
return;
}
}
if (
currentStartYear < newEndYear ||
(currentStartYear === newEndYear && currentStartMonth < newEndMonth) ||
(currentStartYear === newEndYear &&
currentStartMonth === newEndMonth &&
currentStartDate <= newEndDate)
) {
state.endDate = newEnd;
}
},
현재 코드에서는 allReservations에 왼쪽 달력, 오른쪽 달력 2개만 포함시켜서 for문을 돌리고 있다. 처음 렌더링되었을 때 allReservations에는 7, 8월만 들어갔을 것이고, 9월부터는 포함이 안되어서 alert가 안 나는 것이었다. 그렇다면 allReservations에 서버에서 들어오는 모든 데이터를 다 포함시켜야겠네? 아무래도 서버에서 들어오는 모든 데이터를 저장하는 배열을 만들어야겠다.
interface MonthlyReservationProps {
productTitle: string;
baseFee: number;
feePerDay: number;
minimumRentalPeriod: number;
reservationsDate1: StartEndDateProps[];
reservationsDate2: StartEndDateProps[];
// 모든 reservations를 담는 배열을 새로 만듦.
allReservations: StartEndDateProps[];
}
setMonthlyReservation: (
state,
action: PayloadAction<MonthlyReservationProps>,
) => {
state.productTitle = action.payload.productTitle;
state.baseFee = action.payload.baseFee;
state.feePerDay = action.payload.feePerDay;
state.minimumRentalPeriod = action.payload.minimumRentalPeriod;
state.reservationsDate1 = action.payload.reservationsDate1;
state.reservationsDate2 = action.payload.reservationsDate2;
state.allReservations = state.allReservations.concat(
action.payload.reservationsDate1,
action.payload.reservationsDate2,
);
},
clickRightArrow: (
state,
action: PayloadAction<{ startDate: string; endDate: string }[]>,
) => {
state.reservationsDate1 = state.reservationsDate2;
state.reservationsDate2 = turnStringArrIntoDateObjectArr(action.payload);
state.allReservations = state.allReservations.concat(
turnStringArrIntoDateObjectArr(action.payload),
);
},
clickLeftArrow: (
state,
action: PayloadAction<{ startDate: string; endDate: string }[]>,
) => {
state.reservationsDate2 = state.reservationsDate1;
state.reservationsDate1 = turnStringArrIntoDateObjectArr(action.payload);
state.allReservations = state.allReservations.concat(
turnStringArrIntoDateObjectArr(action.payload),
);
},
payload로 예약 데이터가 들어오는 족족 allReservations에 넣으면 될 줄 알았는데,
오히려 7, 8월도 (alert 되던 달도) 안된다.
이게 가능해지면 어떻게 하냐고 갑자기....
어라 근데 allReservations가 계속 빈 배열로 찍히네?
이게 이상하다.
const { reservationsDate1, reservationsDate2 } =
monthlyReservationStore.getState();
const allReservations = [...reservationsDate1, ...reservationsDate2];
console.log('allReservations', allReservations);
for (const reservation of allReservations) {
if (
state.endDate &&
isWithinPeriod(newStart, state.endDate, reservation)
) {
alert(
'선택하신 기간에는 이미 예약이 있습니다. 다른 기간을 선택해 주세요.',
);
return;
}
} const { reservationsDate1, reservationsDate2 } =
monthlyReservationStore.getState();
const allReservations = [...reservationsDate1, ...reservationsDate2];
console.log('allReservations', allReservations);
for (const reservation of allReservations) {
if (
state.endDate &&
isWithinPeriod(newStart, state.endDate, reservation)
) {
alert(
'선택하신 기간에는 이미 예약이 있습니다. 다른 기간을 선택해 주세요.',
);
return;
}
}
이 로직을 setStartDate에도 사용해봤는데 여전히 allReservations가 7, 8월 예약 내역만 가지고 있다.
달력을 오른쪽으로 넘겼는데 allReservations에 왜 9월 데이터가 안들어가있는거지?
이렇게 된 이상 setStartDate와 setEndDate 액션을 호출할 때부터 allReservations의 상태를 인자로 넘겨줘봤다.
const allReservations = useSelector((state: RootState) => [
...state.monthlyReservation.reservationsDate1,
...state.monthlyReservation.reservationsDate2,
]);
const showDates = dates.map((week, i) => (
<tr key={i}>
{week.map((date, j) => (
<EachDate
key={j}
current={calendar}
row={{
week: i,
finalWeek: dates.findIndex(
(week, index) => index !== 0 && week.includes(1),
),
}}
day={j}
reservationData={reservationData}
onClick={() => {
console.log({ ...calendar, date });
if (!reservationState.startDate) {
dispatch(
setStartDate({
...reservationState,
allReservations: allReservations,
startDate: { ...calendar, date },
}),
);
} else {
dispatch(
setEndDate({
...reservationState,
allReservations: allReservations,
endDate: { ...calendar, date },
}),
);
}
}}
>
{date ? date : null}
</EachDate>
))}
</tr>
));
return <DatesContainer>{showDates}</DatesContainer>;
}
export default Dates;
두근두근....됐다!!!!!!!! 됐어!!!!!!!! 9월인데도 이제 alert된다!!!!!!!
10월도 된다!!!!!
allReservations를 store에서 getState()으로 정적으로 받아오는 게 아니라 시작 날짜와 마감 날짜를 유저가 지정하는 action이 호출되는 시점에서 동적으로 allReservations가 지정되면 되는 것이었다.
왘ㅋ!!!!!!!!!!!!! 달력 건드리면서 역대급으로 어려웠던 숙제를 풀었다.
기뻐서 책상 뚜들기는 중 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
아주 기쁘지만 침착하게 문제의 원인과 내가 해결했던 과정을 정리해보자.
문제는 getState()로 상태를 받아왔을 때, 그 시점에 이미 정의된 reservationsDate1(7월의 예약 데이터 상태)와 reservationsDate2(8월의 예약 데이터 상태)만 포함되어 있어서 문제가 생겼었다. 이렇게 되면 사용자가 새로운 달력 날짜를 클릭하더라도 이전에 불러온 예약 정보(7, 8월의 예약 데이터)만 사용하여 확인하기 때문에, 새로운 달력의 예약 상태(9월 이후부터)를 확인할 수 없었다.
하지만 문제를 해결하기 위해 "사용자가 날짜를 선택하는 시점"에 모든 예약 상태 정보(사용자가 9월을 불러왔으면 reservationDate1에 해당하는 8월, reservationDate2에 해당하는 9월)를 동적으로 불러오는 방식을 적용하였다. 이렇게 해서 현재 사용자가 보고 있는 모든 달력의 예약 상태를 확인하고 적절한 alert을 출력할 수 있게 된 것이다.
그리고 또 한 가지!!
아무래도 redux-toolkit을 사용하면서 immer library에 지나치게 의존하게 된 것 같다. reducer의 순수성 유지에 대한 고려가 부족해지니, 액션 내부에서 다른 슬라이스의 상태를 getState()으로 참조하는 것이 리듀서의 순수성에 어떤 여파를 가져올지 고려하지 못한 것..! 함수 내에서 동일한 입력에 대해 동일한 출력이 반환되도록 순수성을 유지하고, 유지보수와 테스트가 용이하도록 store의 독립성을 유지하자. 좋은 교훈이었군!!