Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
8 changes: 6 additions & 2 deletions src/pages/user/Apply/api/apply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ export const postApplicationForm = async (

export const applicationFormDto = (formData: FormInputs, questionArray: string[]) => {
const interviewDateAnswer: PostInterviewSchedule[] = formData.selectedInterviewSchedule;
const newFormDataAnswers: object[] = [...formData.answers, { interviewDateAnswer }];
let formDataAnswers = formData.answers;

if (interviewDateAnswer.length > 0) {
formDataAnswers = [...formData.answers, { interviewDateAnswer }];
}

return {
email: formData.email,
Expand All @@ -44,7 +48,7 @@ export const applicationFormDto = (formData: FormInputs, questionArray: string[]
phoneNumber: formData.phoneNumber,
department: formData.department,
answers: [
...newFormDataAnswers.map((answer, index) => {
...formDataAnswers.map((answer, index) => {
return {
questionNum: index,
question: questionArray[index],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useDragSelection } from '@/pages/user/Apply/hook/useDragSelection';
import { useUpdateFormValue } from '@/pages/user/Apply/hook/useUpdateFormData';
import { getTimeSlotsArray } from '@/pages/user/Apply/utils/time';
import { Text } from '@/shared/components/Text';
import { TimeSpan, Wrapper, DateText } from './index.styled';
Expand All @@ -8,10 +7,7 @@ import type { InterviewSchedule } from '@/pages/user/Apply/type/apply';
export const InterviewScheduleSelector = ({ availableTime, date }: InterviewSchedule) => {
const timeSlotsArray: [string, string][] = getTimeSlotsArray(availableTime);

const { updateScheduleData } = useUpdateFormValue();

const { handleMouseDown, handleMouseMove, handleMouseUp, selectedTime } = useDragSelection(
updateScheduleData,
const { handleMouseDown, handleMouseMove, handleMouseUp, states } = useDragSelection(
date,
timeSlotsArray,
);
Expand All @@ -24,12 +20,12 @@ export const InterviewScheduleSelector = ({ availableTime, date }: InterviewSche
<TimeSpan
key={idx}
data-index={idx}
selected={selectedTime[idx]}
selected={states.isSelectedStates[idx]}
onMouseDown={handleMouseDown}
onMouseEnter={handleMouseMove}
onMouseUp={handleMouseUp}
>
<Text>{e[0] + '~' + e[1]}</Text>
<Text size='xs'>{`${e[0]}~${e[1]}`}</Text>
</TimeSpan>
);
})}
Expand Down
10 changes: 8 additions & 2 deletions src/pages/user/Apply/components/ApplicationForm/index.styled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,15 @@ export const TimeSpan = styled.span<TimeSpanProps>(({ theme, selected }) => ({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: '1px solid ',
color: selected ? 'white' : theme.colors.gray800,
border: `1px solid ${theme.colors.gray300}`,
borderRadius: '6px',
width: '100px',
height: '30px',
transition: 'background-color 0.15s, color 0.15s',
'&:hover': {
backgroundColor: selected ? theme.colors.primary300 : theme.colors.gray200,
},
}));

export const Wrapper = styled.div({
Expand All @@ -111,5 +117,5 @@ export const DateText = styled.span(({ theme }) => ({
width: '100px',
padding: '10px 0 20px 0',
fontWeight: theme.font.weight.bold,
fontSize: theme.font.size.base,
fontSize: theme.font.size.sm,
}));
14 changes: 14 additions & 0 deletions src/pages/user/Apply/constant/initialDragState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { DragState } from '../type/apply';

export const generateInitialDragState = (statesLength: number): DragState => {
return {
startIndex: -1,
lastHoveredIndex: -1,
currentSelectedIndex: -1,
isSelectionMode: false,
isSelectedStates: new Array(statesLength).fill(false),
isMouseDown: false,
isDragging: false,
previousIndexDiffSign: null,
};
};
33 changes: 33 additions & 0 deletions src/pages/user/Apply/domain/drag.ts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updateDragState랑 utils의 updateSelectedState 함수들은 useDragSelection훅 파일 내부에만 사용이 될 것 같고 함께 두면 응집도도 높아지고 나중에 수정할 때도 한 파일에 모여 있어 코드를 찾기 쉬울 것 같은데 분리하신 이유가 무엇인지 궁금합니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

말씀하신대로 훅 내부에서만 사용되는 함수이고, 같이 두면 찾기 편한 측면은 분명히 존재하는 것 같습니다!
그런데 제가 생각하기에는 응집도는 사용되는 로직들이 함께 위치한다고 높아지는 것은 아니고 수행하는 기능 측면에서 생각하는게 맞다고 생각했습니다.

updateSelectedState는 배열, 시작인덱스, 종료 인덱스만 매개변수로 넣으면 상태 변환된 배열을 출력해주는 함수이고, 드래그 뿐만 아니라 배열의 특정 범위를 바꾸는 영역에서 추가로 사용할 수 있으므로 util 영역이 적절하다고 판단했고,

useDragSelection은 "드래그 방향이 바뀌면 선택/해제를 반대로 한다"는 면접 일정을 선택할 때만 반영되는 어떤 규칙이 반영된 부분이라 util이 아닌 곳에 두어야 한다고 판단했습니다. 지난번에 멘토님이 보내주신 FSD 관련 구조 뿐만 아니라 다수의 프로젝트에서 도메인/비즈니스 로직 등을 따로 관리하는 게 일반적이라 현재 프로젝트에도 domain 폴더가 적합해 보여서 추가했습니다.

결과적으로 useDragSelection 훅 내부에서 util 함수와 domain 함수를 불러서 사용해도 기능 측면에서 응집도는 유지된다고 생각했습니다.

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { DragState } from '../type/apply';

export type UpdatedDragState = {
newStartIndex: number;
newIsSelectionMode: boolean;
newPreviousIndexDiffSign: number | null;
};

export const updateDragState = (
state: DragState,
currentIdx: number,
idxDiffSign: number,
): UpdatedDragState => {
let newStartIndex = state.startIndex;
let newIsSelectionMode = state.isSelectionMode;
let newPreviousIndexDiffSign = state.previousIndexDiffSign;
if (state.previousIndexDiffSign === null || idxDiffSign !== state.previousIndexDiffSign) {
newIsSelectionMode =
state.previousIndexDiffSign === null ? state.isSelectionMode : !state.isSelectionMode;

if (idxDiffSign < 0) {
newStartIndex = currentIdx + 1;
} else if (idxDiffSign > 0) {
newStartIndex = currentIdx - 1;
} else {
newStartIndex = currentIdx;
}
Comment on lines +21 to +27
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

newStartIndex를 계산하는 if/else if/else 블록은 newStartIndex = curtIdx - idxDiffSign; 한 줄로 간단하게 표현할 수 있습니다. idxDiffSign1, -1, 0인 모든 경우를 처리할 수 있어 코드가 더 간결해집니다.

    newStartIndex = curtIdx - idxDiffSign;


newPreviousIndexDiffSign = idxDiffSign;
}

return { newStartIndex, newIsSelectionMode, newPreviousIndexDiffSign };
};
26 changes: 26 additions & 0 deletions src/pages/user/Apply/domain/schedule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { PostInterviewSchedule } from '../type/apply';

export const updateSchedule = (
currentInterviewSchedule: PostInterviewSchedule[],
date: string,
selectedTime: string[],
): PostInterviewSchedule[] => {
const sameDateIndex: number = currentInterviewSchedule.findIndex(
(schedule: PostInterviewSchedule) => schedule.date === date,
);

const selectedInterviewSchedule: PostInterviewSchedule = {
date: date,
selectedTimes: selectedTime ?? [],
};
let updatedSchedule: PostInterviewSchedule[];

if (sameDateIndex !== -1) {
updatedSchedule = [...currentInterviewSchedule];
updatedSchedule[sameDateIndex] = selectedInterviewSchedule;
} else {
updatedSchedule = [...currentInterviewSchedule, selectedInterviewSchedule];
}

return updatedSchedule;
};
8 changes: 4 additions & 4 deletions src/pages/user/Apply/hook/useApplicationForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@ import { useEffect, useState } from 'react';
import { fetchApplicationForm } from '@/pages/user/Apply/api/apply.ts';
import type { ApplicationForm } from '../type/apply';

export const useApplicationForm = (Id: number) => {
export const useApplicationForm = (clubId: number) => {
const [clubApplicationForm, setClubApplicationForm] = useState<ApplicationForm | null>(null);

useEffect(() => {
if (!Id) return;
if (!clubId) return;
const setFormStateAfterFetch = async () => {
const applicationForm = await fetchApplicationForm(Id);
const applicationForm = await fetchApplicationForm(clubId);
setClubApplicationForm(applicationForm);
};
setFormStateAfterFetch();
}, [Id]);
}, [clubId]);

return clubApplicationForm;
};
161 changes: 92 additions & 69 deletions src/pages/user/Apply/hook/useDragSelection.ts
Original file line number Diff line number Diff line change
@@ -1,95 +1,118 @@
import { useRef, useState } from 'react';
import { getSign, type Sign } from '../utils/math';
import { convertSelectionToTimeInterval, mergeContinuousTimeInterval } from '../utils/time';
import { useReducer } from 'react';
import { useInterviewScheduleUpdater } from './useFormDataUpdate';

export function useDragSelection(
updateScheduleData: (data: string, mergedInterviewTime: string[]) => void,
date: string,
timeIntervalArray: [string, string][],
) {
const [prevDiffSign, setPrevDiffSign] = useState<Sign>(0);
const startIndex = useRef<string | undefined>('');
const lastHoveredIndex = useRef<string | null>(null);
const mode = useRef<boolean>(false);
const [isDragging, setIsDragging] = useState<boolean>(false);
const [isMouseDown, setIsMouseDown] = useState<boolean>(false);
const selectedIndex = useRef<string | undefined>('');
const [selectedTime, setSelectedTime] = useState<boolean[]>(() =>
new Array(timeIntervalArray.length).fill(false),
);

function handleIndexChange(newIndex: number) {
const diff = newIndex - Number(lastHoveredIndex.current);
const currentSign = getSign(diff);
import { generateInitialDragState } from '../constant/initialDragState';
import { updateDragState } from '../domain/drag';
import { updateSelectedState } from '../utils/drag';
import { getIndexDiffSign } from '../utils/math';
import type { DragAction, DragState } from '../type/apply';

Comment on lines +1 to 9
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix import order (ESLint import/order) and add MouseEvent type

Reorder imports to place ../utils/drag before ../utils/math and keep type-only imports after value imports. Also import MouseEvent type to avoid relying on global React namespace.

-import { useReducer } from 'react';
-import { getIndexDiffSign } from '../utils/math';
-import { useInterviewScheduleUpdater } from './useFormDataUpdate';
-import type { DragAction, DragState } from '../type/apply';
-
-import { generateInitialDragState } from '../constant/initialDragState';
-import { updateDragState } from '../domain/drag';
-import { updateSelectedState } from '../utils/drag';
+import { useReducer } from 'react';
+import type { MouseEvent } from 'react';
+import { updateSelectedState } from '../utils/drag';
+import { getIndexDiffSign } from '../utils/math';
+import { useInterviewScheduleUpdater } from './useFormDataUpdate';
+import { generateInitialDragState } from '../constant/initialDragState';
+import { updateDragState } from '../domain/drag';
+import type { DragAction, DragState } from '../type/apply';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { useReducer } from 'react';
import { getIndexDiffSign } from '../utils/math';
import { useInterviewScheduleUpdater } from './useFormDataUpdate';
import type { DragAction, DragState } from '../type/apply';
export function useDragSelection(
updateScheduleData: (data: string, mergedInterviewTime: string[]) => void,
date: string,
timeIntervalArray: [string, string][],
) {
const [prevDiffSign, setPrevDiffSign] = useState<Sign>(0);
const startIndex = useRef<string | undefined>('');
const lastHoveredIndex = useRef<string | null>(null);
const mode = useRef<boolean>(false);
const [isDragging, setIsDragging] = useState<boolean>(false);
const [isMouseDown, setIsMouseDown] = useState<boolean>(false);
const selectedIndex = useRef<string | undefined>('');
const [selectedTime, setSelectedTime] = useState<boolean[]>(() =>
new Array(timeIntervalArray.length).fill(false),
);
function handleIndexChange(newIndex: number) {
const diff = newIndex - Number(lastHoveredIndex.current);
const currentSign = getSign(diff);
import { generateInitialDragState } from '../constant/initialDragState';
import { updateDragState } from '../domain/drag';
import { updateSelectedState } from '../utils/drag';
// src/pages/user/Apply/hook/useDragSelection.ts
import { useReducer } from 'react';
import type { MouseEvent } from 'react';
import { updateSelectedState } from '../utils/drag';
import { getIndexDiffSign } from '../utils/math';
import { useInterviewScheduleUpdater } from './useFormDataUpdate';
import { generateInitialDragState } from '../constant/initialDragState';
import { updateDragState } from '../domain/drag';
import type { DragAction, DragState } from '../type/apply';
// …rest of the file…
🧰 Tools
🪛 GitHub Actions: Dongarium FE CI/CD

[error] 2-2: ESLint: '../utils/math import should occur after import of ../utils/drag' import/order

🪛 GitHub Check: lint

[failure] 4-4:
../type/apply type import should occur after import of ../utils/drag


[failure] 2-2:
../utils/math import should occur after import of ../utils/drag

🤖 Prompt for AI Agents
In src/pages/user/Apply/hook/useDragSelection.ts around lines 1 to 9, the
imports are misordered and the MouseEvent type is missing; reorder so value
imports come first, move "../utils/drag" before "../utils/math", place type-only
imports (like DragAction, DragState) after the value imports, and add an
explicit type import for MouseEvent from 'react' (import type { MouseEvent }
from 'react') to avoid relying on the global React namespace.

if (prevDiffSign !== 0 && currentSign !== 0 && currentSign !== prevDiffSign) {
mode.current = !mode.current;
function getSelectedIndex(e: React.MouseEvent<HTMLSpanElement>) {
return Number(e.currentTarget.dataset.index);
}
Comment on lines +10 to +12
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Guard invalid index in getSelectedIndex

Avoids NaN array indexing and dispatch with bad payloads.

-function getSelectedIndex(e: React.MouseEvent<HTMLSpanElement>) {
-  return Number(e.currentTarget.dataset.index);
-}
+function getSelectedIndex(e: MouseEvent<HTMLSpanElement>) {
+  const raw = e.currentTarget.dataset.index;
+  const idx = raw === undefined ? NaN : Number.parseInt(raw, 10);
+  return Number.isFinite(idx) ? idx : -1;
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function getSelectedIndex(e: React.MouseEvent<HTMLSpanElement>) {
return Number(e.currentTarget.dataset.index);
}
function getSelectedIndex(e: MouseEvent<HTMLSpanElement>) {
const raw = e.currentTarget.dataset.index;
const idx = raw === undefined ? NaN : Number.parseInt(raw, 10);
return Number.isFinite(idx) ? idx : -1;
}
🤖 Prompt for AI Agents
In src/pages/user/Apply/hook/useDragSelection.ts around lines 10–12,
getSelectedIndex currently does Number(...) on dataset.index which can produce
NaN and lead to invalid array indexing/dispatch; change it to parse the value
(e.g. const idx = parseInt(e.currentTarget.dataset.index ?? "", 10)), validate
with Number.isFinite(idx) / !Number.isNaN(idx) (or Number.isInteger), and return
a safe sentinel (null or -1) when invalid; also update any callers to check for
the sentinel before indexing or dispatching to avoid using NaN as an index.


if (currentSign < 0) {
startIndex.current = String(newIndex + 1);
} else if (currentSign > 0) {
startIndex.current = String(newIndex - 1);
}
function dragReducer(state: DragState, action: DragAction) {
switch (action.type) {
case 'mouseDown': {
const newSelectedStates: boolean[] = [...state.isSelectedStates];
newSelectedStates[action.index] = action.isSelectionMode;

return {
...state,
startIndex: action.index,
currentSelectedIndex: action.index,
lastHoveredIndex: action.index,
isSelectionMode: action.isSelectionMode,
isSelectedStates: newSelectedStates,
isMouseDown: true,
previousIndexDiffSign: null,
};
}
case 'mouseMove': {
const currentIndex = action.index;
const indexDiffSign = action.indexDiffSign;

const { newStartIndex, newIsSelectionMode, newPreviousIndexDiffSign } = updateDragState(
state,
currentIndex,
indexDiffSign,
);

const newSelectedStates = updateSelectedState(
state.isSelectedStates,
newStartIndex,
currentIndex,
newIsSelectionMode,
);

return {
...state,
isDragging: true,
currentSelectedIndex: currentIndex,
lastHoveredIndex: currentIndex,
isSelectedStates: newSelectedStates,
isSelectionMode: newIsSelectionMode,
startIndex: newStartIndex,
previousIndexDiffSign: newPreviousIndexDiffSign,
};
}
case 'mouseUp': {
return {
...state,
isMouseDown: false,
isDragging: false,
};
}
setPrevDiffSign(currentSign);
default:
return state;
}
}
Comment on lines +14 to +69
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add reset action handling to reducer

Supports safe re-init when date/slots change.

 function dragReducer(state: DragState, action: DragAction) {
   switch (action.type) {
+    case 'reset': {
+      return generateInitialDragState(action.statesLength);
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function dragReducer(state: DragState, action: DragAction) {
switch (action.type) {
case 'mouseDown': {
const newSelectedStates: boolean[] = [...state.isSelectedStates];
newSelectedStates[action.index] = action.isSelectionMode;
return {
...state,
startIndex: action.index,
currentSelectedIndex: action.index,
lastHoveredIndex: action.index,
isSelectionMode: action.isSelectionMode,
isSelectedStates: newSelectedStates,
isMouseDown: true,
previousIndexDiffSign: null,
};
}
case 'mouseMove': {
const currentIndex = action.index;
const indexDiffSign = action.indexDiffSign;
const { newStartIndex, newIsSelectionMode, newPreviousIndexDiffSign } = updateDragState(
state,
currentIndex,
indexDiffSign,
);
const newSelectedStates = updateSelectedState(
state.isSelectedStates,
newStartIndex,
currentIndex,
newIsSelectionMode,
);
return {
...state,
isDragging: true,
currentSelectedIndex: currentIndex,
lastHoveredIndex: currentIndex,
isSelectedStates: newSelectedStates,
isSelectionMode: newIsSelectionMode,
startIndex: newStartIndex,
previousIndexDiffSign: newPreviousIndexDiffSign,
};
}
case 'mouseUp': {
return {
...state,
isMouseDown: false,
isDragging: false,
};
}
setPrevDiffSign(currentSign);
default:
return state;
}
}
function dragReducer(state: DragState, action: DragAction) {
switch (action.type) {
case 'reset': {
return generateInitialDragState(action.statesLength);
}
case 'mouseDown': {
const newSelectedStates: boolean[] = [...state.isSelectedStates];
newSelectedStates[action.index] = action.isSelectionMode;
return {
...state,
startIndex: action.index,
currentSelectedIndex: action.index,
lastHoveredIndex: action.index,
isSelectionMode: action.isSelectionMode,
isSelectedStates: newSelectedStates,
isMouseDown: true,
previousIndexDiffSign: null,
};
}
case 'mouseMove': {
const currentIndex = action.index;
const indexDiffSign = action.indexDiffSign;
const { newStartIndex, newIsSelectionMode, newPreviousIndexDiffSign } = updateDragState(
state,
currentIndex,
indexDiffSign,
);
const newSelectedStates = updateSelectedState(
state.isSelectedStates,
newStartIndex,
currentIndex,
newIsSelectionMode,
);
return {
...state,
isDragging: true,
currentSelectedIndex: currentIndex,
lastHoveredIndex: currentIndex,
isSelectedStates: newSelectedStates,
isSelectionMode: newIsSelectionMode,
startIndex: newStartIndex,
previousIndexDiffSign: newPreviousIndexDiffSign,
};
}
case 'mouseUp': {
return {
...state,
isMouseDown: false,
isDragging: false,
};
}
default:
return state;
}
}
🤖 Prompt for AI Agents
In src/pages/user/Apply/hook/useDragSelection.ts around lines 14 to 69, the
reducer lacks a 'reset' action to safely reinitialize drag state when dates or
slots change; add a 'case "reset"' that returns a fresh state (either from
action.payload or by constructing an initial state) resetting startIndex,
currentSelectedIndex, lastHoveredIndex to null,
isSelectionMode/isMouseDown/isDragging to false, previousIndexDiffSign to null,
and isSelectedStates to the new selection array provided in the payload (or
recomputed), ensuring you return a new object (no in-place mutation) so the hook
fully re-initializes on date/slot changes.


const handleMouseDown = (e: React.MouseEvent<HTMLSpanElement>) => {
e.preventDefault();
setIsMouseDown(true);

startIndex.current = e.currentTarget.dataset.index;
if (startIndex.current) mode.current = !selectedTime[Number(startIndex.current)];
export function useDragSelection(date: string, timeIntervalArray: [string, string][]) {
const { updateInterviewSchedule } = useInterviewScheduleUpdater(date, timeIntervalArray);

const newValue = [...selectedTime];
newValue[Number(startIndex.current)] = mode.current;
const [states, dispatch] = useReducer(
dragReducer,
generateInitialDragState(timeIntervalArray.length),
);
Comment on lines +71 to +77
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reset reducer when date or slots length change

Prevents length mismatch and out-of-bounds updates after switching days or slot lists.

 export function useDragSelection(date: string, timeIntervalArray: [string, string][]) {
   const { updateInterviewSchedule } = useInterviewScheduleUpdater(date, timeIntervalArray);

   const [states, dispatch] = useReducer(
     dragReducer,
     generateInitialDragState(timeIntervalArray.length),
   );
+
+  useEffect(() => {
+    dispatch({ type: 'reset', statesLength: timeIntervalArray.length });
+  }, [date, timeIntervalArray.length]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function useDragSelection(date: string, timeIntervalArray: [string, string][]) {
const { updateInterviewSchedule } = useInterviewScheduleUpdater(date, timeIntervalArray);
const newValue = [...selectedTime];
newValue[Number(startIndex.current)] = mode.current;
const [states, dispatch] = useReducer(
dragReducer,
generateInitialDragState(timeIntervalArray.length),
);
export function useDragSelection(date: string, timeIntervalArray: [string, string][]) {
const { updateInterviewSchedule } = useInterviewScheduleUpdater(date, timeIntervalArray);
const [states, dispatch] = useReducer(
dragReducer,
generateInitialDragState(timeIntervalArray.length),
);
useEffect(() => {
dispatch({ type: 'reset', statesLength: timeIntervalArray.length });
}, [date, timeIntervalArray.length]);
// …rest of your hook…
}
🤖 Prompt for AI Agents
In src/pages/user/Apply/hook/useDragSelection.ts around lines 71-77, the reducer
state can get out-of-sync when the date or the number of time slots changes; add
logic to reset the reducer whenever date or timeIntervalArray.length changes.
Implement a RESET (or REINITIALIZE) action in dragReducer that replaces state
with generateInitialDragState(timeIntervalArray.length), then add a useEffect
that watches [date, timeIntervalArray.length] and dispatches that RESET action
with the freshly generated initial state; alternatively, if dragReducer already
supports a reset action name, dispatch that action from the effect. Ensure the
effect runs on mount and when those dependencies change so state length always
matches slots.


setSelectedTime(newValue);
lastHoveredIndex.current = String(startIndex.current);
const handleMouseDown = (e: React.MouseEvent<HTMLSpanElement>) => {
e.preventDefault();
dispatch({
type: 'mouseDown',
index: getSelectedIndex(e),
isSelectionMode: !states.isSelectedStates[getSelectedIndex(e)],
});
};

const handleMouseMove = (e: React.MouseEvent<HTMLSpanElement>) => {
if (!isMouseDown) return;
setIsDragging(true);

selectedIndex.current = e.currentTarget.dataset.index;
if (!e.currentTarget.dataset.index || selectedIndex.current === lastHoveredIndex.current)
if (
!e.currentTarget.dataset.index ||
getSelectedIndex(e) === states.lastHoveredIndex ||
!states.isMouseDown
)
return;

handleIndexChange(Number(selectedIndex.current));

setSelectedTime((prev) => {
const newSelected = [...prev];
const start = Math.min(Number(startIndex.current), Number(selectedIndex.current));
const end = Math.max(Number(startIndex.current), Number(selectedIndex.current));

for (let i = start; i <= end; i++) {
newSelected[i] = mode.current;
}
return newSelected;
dispatch({
type: 'mouseMove',
index: getSelectedIndex(e),
indexDiffSign: getIndexDiffSign(getSelectedIndex(e), states.lastHoveredIndex),
});
lastHoveredIndex.current = selectedIndex.current!;
};

const handleMouseUp = () => {
if (!isDragging) return;
setIsMouseDown(false);
setIsDragging(false);

const selectedInterviewTime: Set<string> = convertSelectionToTimeInterval(
selectedTime,
timeIntervalArray,
);
const mergedInterviewTime: string[] = mergeContinuousTimeInterval(selectedInterviewTime);
updateScheduleData(date, mergedInterviewTime);
if (!states.isMouseDown) return;
dispatch({
type: 'mouseUp',
});

handleIndexChange(Number(selectedIndex.current));
updateInterviewSchedule(states.isSelectedStates);
};

return {
handleMouseDown,
handleMouseMove,
handleMouseUp,
selectedTime,
states,
};
}
19 changes: 19 additions & 0 deletions src/pages/user/Apply/hook/useFormDataUpdate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useFormContext } from 'react-hook-form';
import { updateSchedule } from '../domain/schedule';
import { convertSelectionToTimeInterval, mergeContinuousTimeInterval } from '../utils/time';
import type { FormInputs, PostInterviewSchedule } from '../type/apply';

export function useInterviewScheduleUpdater(date: string, timeSlotsArray: [string, string][]) {
const { setValue, getValues } = useFormContext<FormInputs>();

const updateInterviewSchedule = (isSelectedStates: boolean[]) => {
const selectedTimes = convertSelectionToTimeInterval(isSelectedStates, timeSlotsArray);
const mergedTimes = mergeContinuousTimeInterval(selectedTimes);
const currentSchedules: PostInterviewSchedule[] = getValues('selectedInterviewSchedule') || [];

const updatedSchedules = updateSchedule(currentSchedules, date, mergedTimes);
setValue('selectedInterviewSchedule', updatedSchedules);
};

return { updateInterviewSchedule };
}
Comment on lines +9 to +19
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updateInterviewSchedule을 [date, timeSlotsArray, getValues, setValue] 의존성 배열로 useCallback으로 감싸는 것도 괜찮을 것 같은데 어떠신가요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분이 조금 헷갈리는데, useCallback으로 내부를 감싸도


export const InterviewScheduleSelector = ({ availableTime, date }: InterviewSchedule) => {
  const timeSlotsArray: [string, string][] = getTimeSlotsArray(availableTime);

  const { handleMouseDown, handleMouseMove, handleMouseUp, states } = useDragSelection(
    date,
    timeSlotsArray,
  );

상위 component가 렌더링 되면 -> hook이 계속 호출되는 구조 -> useCallback에 기대했던 역할을 수행 불가라고 생각했는데,
그래서 리팩토링 과정에서 상위 컴포넌트의 props를 오히려 memo를 통해 매개변수로 넣어야되지 않을까 생각했는데 어떤 게 맞을까요?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 그렇네요!
말씀하신 대로 상위 컴포넌트가 리렌더링되면서 매번 새로운 props를 생성하니까 useCallback을 해도 효과가 별로 없네요
먼저 props나 timeSlotsArray를 useMemo로 고정한 뒤 useCallback을 함께 적용하는 게 더 나은 방법인 것 같아요

Loading