diff --git a/src/pages/user/Apply/api/apply.ts b/src/pages/user/Apply/api/apply.ts index b6b60a88..e40c46a2 100644 --- a/src/pages/user/Apply/api/apply.ts +++ b/src/pages/user/Apply/api/apply.ts @@ -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, @@ -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], diff --git a/src/pages/user/Apply/components/ApplicationForm/InterviewScheduleSelector.tsx b/src/pages/user/Apply/components/ApplicationForm/InterviewScheduleSelector.tsx index 2e4a8b9c..efc6d045 100644 --- a/src/pages/user/Apply/components/ApplicationForm/InterviewScheduleSelector.tsx +++ b/src/pages/user/Apply/components/ApplicationForm/InterviewScheduleSelector.tsx @@ -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'; @@ -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, ); @@ -24,12 +20,12 @@ export const InterviewScheduleSelector = ({ availableTime, date }: InterviewSche - {e[0] + '~' + e[1]} + {`${e[0]}~${e[1]}`} ); })} diff --git a/src/pages/user/Apply/components/ApplicationForm/index.styled.ts b/src/pages/user/Apply/components/ApplicationForm/index.styled.ts index 4445562b..dbcf3c2e 100644 --- a/src/pages/user/Apply/components/ApplicationForm/index.styled.ts +++ b/src/pages/user/Apply/components/ApplicationForm/index.styled.ts @@ -96,9 +96,15 @@ export const TimeSpan = styled.span(({ 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({ @@ -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, })); diff --git a/src/pages/user/Apply/constant/initialDragState.ts b/src/pages/user/Apply/constant/initialDragState.ts new file mode 100644 index 00000000..2f3c2e92 --- /dev/null +++ b/src/pages/user/Apply/constant/initialDragState.ts @@ -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, + }; +}; diff --git a/src/pages/user/Apply/domain/drag.ts b/src/pages/user/Apply/domain/drag.ts new file mode 100644 index 00000000..4d71e3a0 --- /dev/null +++ b/src/pages/user/Apply/domain/drag.ts @@ -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; + } + + newPreviousIndexDiffSign = idxDiffSign; + } + + return { newStartIndex, newIsSelectionMode, newPreviousIndexDiffSign }; +}; diff --git a/src/pages/user/Apply/domain/schedule.ts b/src/pages/user/Apply/domain/schedule.ts new file mode 100644 index 00000000..70e8b857 --- /dev/null +++ b/src/pages/user/Apply/domain/schedule.ts @@ -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; +}; diff --git a/src/pages/user/Apply/hook/useApplicationForm.ts b/src/pages/user/Apply/hook/useApplicationForm.ts index fc8ec5c0..5f00d520 100644 --- a/src/pages/user/Apply/hook/useApplicationForm.ts +++ b/src/pages/user/Apply/hook/useApplicationForm.ts @@ -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(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; }; diff --git a/src/pages/user/Apply/hook/useDragSelection.ts b/src/pages/user/Apply/hook/useDragSelection.ts index 714b9a06..3d43cb38 100644 --- a/src/pages/user/Apply/hook/useDragSelection.ts +++ b/src/pages/user/Apply/hook/useDragSelection.ts @@ -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(0); - const startIndex = useRef(''); - const lastHoveredIndex = useRef(null); - const mode = useRef(false); - const [isDragging, setIsDragging] = useState(false); - const [isMouseDown, setIsMouseDown] = useState(false); - const selectedIndex = useRef(''); - const [selectedTime, setSelectedTime] = useState(() => - 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'; - if (prevDiffSign !== 0 && currentSign !== 0 && currentSign !== prevDiffSign) { - mode.current = !mode.current; +function getSelectedIndex(e: React.MouseEvent) { + return Number(e.currentTarget.dataset.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; } +} - const handleMouseDown = (e: React.MouseEvent) => { - 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), + ); - setSelectedTime(newValue); - lastHoveredIndex.current = String(startIndex.current); + const handleMouseDown = (e: React.MouseEvent) => { + e.preventDefault(); + dispatch({ + type: 'mouseDown', + index: getSelectedIndex(e), + isSelectionMode: !states.isSelectedStates[getSelectedIndex(e)], + }); }; const handleMouseMove = (e: React.MouseEvent) => { - 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 = 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, }; } diff --git a/src/pages/user/Apply/hook/useFormDataUpdate.ts b/src/pages/user/Apply/hook/useFormDataUpdate.ts new file mode 100644 index 00000000..ede9ebf5 --- /dev/null +++ b/src/pages/user/Apply/hook/useFormDataUpdate.ts @@ -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(); + + 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 }; +} diff --git a/src/pages/user/Apply/hook/useUpdateFormData.ts b/src/pages/user/Apply/hook/useUpdateFormData.ts deleted file mode 100644 index 55995d53..00000000 --- a/src/pages/user/Apply/hook/useUpdateFormData.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { useFormContext } from 'react-hook-form'; -import { type FormInputs, type PostInterviewSchedule } from '../type/apply'; - -export function useUpdateFormValue() { - const { setValue, getValues } = useFormContext(); - - function updateScheduleData(date: string, mergedInterviewTime: string[]) { - const currentInterviewSchedule = getValues('selectedInterviewSchedule') || []; - - const selectedInterviewSchedule: PostInterviewSchedule = { - date: date, - selectedTimes: mergedInterviewTime ?? [], - }; - - const sameDateIndex: number = currentInterviewSchedule.findIndex( - (schedule: PostInterviewSchedule) => schedule.date === date, - ); - - let updatedSchedule: PostInterviewSchedule[]; - - if (sameDateIndex !== -1) { - updatedSchedule = [...currentInterviewSchedule]; - updatedSchedule[sameDateIndex] = selectedInterviewSchedule; - } else { - updatedSchedule = [...currentInterviewSchedule, selectedInterviewSchedule]; - } - - setValue('selectedInterviewSchedule', updatedSchedule); - } - - return { - updateScheduleData, - }; -} diff --git a/src/pages/user/Apply/type/apply.ts b/src/pages/user/Apply/type/apply.ts index 136ef9ed..f37da6fa 100644 --- a/src/pages/user/Apply/type/apply.ts +++ b/src/pages/user/Apply/type/apply.ts @@ -67,3 +67,19 @@ export type FormInputs = { answers: object[]; selectedInterviewSchedule: PostInterviewSchedule[]; }; + +export type DragState = { + startIndex: number; + lastHoveredIndex: number; + currentSelectedIndex: number; + isSelectionMode: boolean; + isSelectedStates: boolean[]; + isMouseDown: boolean; + isDragging: boolean; + previousIndexDiffSign: null | number; +}; + +export type DragAction = + | { type: 'mouseDown'; index: number; isSelectionMode: boolean } + | { type: 'mouseMove'; index: number; indexDiffSign: number } + | { type: 'mouseUp' }; diff --git a/src/pages/user/Apply/utils/drag.ts b/src/pages/user/Apply/utils/drag.ts new file mode 100644 index 00000000..ca05ed82 --- /dev/null +++ b/src/pages/user/Apply/utils/drag.ts @@ -0,0 +1,13 @@ +export const updateSelectedState = ( + selectedStates: boolean[], + startIndex: number, + selectedIndex: number, + selectionMode: boolean, +): boolean[] => { + const start = Math.min(startIndex, selectedIndex); + const end = Math.max(startIndex, selectedIndex); + + return selectedStates.map((isSelected, index) => + index >= start && index <= end ? selectionMode : isSelected, + ); +}; diff --git a/src/pages/user/Apply/utils/math.ts b/src/pages/user/Apply/utils/math.ts index 7d8d30ae..30a218ac 100644 --- a/src/pages/user/Apply/utils/math.ts +++ b/src/pages/user/Apply/utils/math.ts @@ -5,3 +5,11 @@ export function getSign(diff: number): Sign { if (diff < 0) return -1; return 0; } + +export function getDiff(A: number, B: number): number { + return A - B; +} + +export function getIndexDiffSign(currentSelectedIndex: number, lastHoveredIndex: number): Sign { + return getSign(getDiff(currentSelectedIndex, lastHoveredIndex)); +}