From 7465cc13edd9f7dae6cf6f7f411b1438b8d5f5dd Mon Sep 17 00:00:00 2001 From: hwinkr Date: Sat, 21 Sep 2024 10:26:13 +0900 Subject: [PATCH 01/27] =?UTF-8?q?chore:=20=ED=8D=BC=EB=84=90=20=ED=8C=A8?= =?UTF-8?q?=ED=84=B4=EC=97=90=EC=84=9C=20=EC=82=AC=EC=9A=A9=ED=95=A0=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - readonly 타입의 문자열 스텝들을 StepType으로 정의 - FunnelProps, StepProps의 타입을 StepType에 포함되는 Steps 타입을 사용해서 정의 --- frontend/src/hooks/useFunnel/useFunnel.type.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 frontend/src/hooks/useFunnel/useFunnel.type.ts diff --git a/frontend/src/hooks/useFunnel/useFunnel.type.ts b/frontend/src/hooks/useFunnel/useFunnel.type.ts new file mode 100644 index 000000000..adf2fc844 --- /dev/null +++ b/frontend/src/hooks/useFunnel/useFunnel.type.ts @@ -0,0 +1,16 @@ +import type { ReactElement } from 'react'; + +export type StepType = Readonly>; + +export interface FunnelProps { + steps: Steps; + step: Steps[number]; + children: Array>>; +} + +export type RouteFunnelProps = Omit, 'steps' | 'step'>; + +export interface StepProps { + name: Steps[number]; + children: React.ReactNode; +} From 90e369313c3670f31315b59e097b6e7ff7fbe462 Mon Sep 17 00:00:00 2001 From: hwinkr Date: Sat, 21 Sep 2024 10:26:21 +0900 Subject: [PATCH 02/27] =?UTF-8?q?feat:=20=ED=98=84=EC=9E=AC=20=EB=A0=8C?= =?UTF-8?q?=EB=8D=94=EB=A7=81=20=ED=95=B4=EC=95=BC=20=ED=95=A0=20=EC=9E=90?= =?UTF-8?q?=EC=8B=9D=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=EB=A5=BC=20?= =?UTF-8?q?=EA=B2=B0=EC=A0=95=ED=95=98=EB=8A=94=20FunnelMain=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Funnel 컴포넌트의 여러 자식 컴포넌트 중 child 컴포넌트가 스텝에 속하는 컴포넌트인지 확인 - 현재 스텝에 맞는 targetStep 컴포넌트를 반환 --- frontend/src/hooks/useFunnel/FunnelMain.tsx | 26 +++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 frontend/src/hooks/useFunnel/FunnelMain.tsx diff --git a/frontend/src/hooks/useFunnel/FunnelMain.tsx b/frontend/src/hooks/useFunnel/FunnelMain.tsx new file mode 100644 index 000000000..15214a7d4 --- /dev/null +++ b/frontend/src/hooks/useFunnel/FunnelMain.tsx @@ -0,0 +1,26 @@ +import type { ReactElement } from 'react'; +import React, { Children, isValidElement } from 'react'; + +import type { FunnelProps, StepProps, StepType } from './useFunnel.type'; + +const isValidFunnelChild = ( + child: React.ReactNode, +): child is ReactElement> => { + return isValidElement(child) && typeof child.props.name === 'string'; +}; + +export default function FunnelMain({ + steps, + step, + children, +}: FunnelProps) { + const childrenArray = Children.toArray(children) + .filter(isValidFunnelChild) + .filter((child) => steps.includes(child.props.name)); + + const targetStep = childrenArray.find((child) => child.props.name === step); + + if (!targetStep) return null; + + return <>{targetStep}; +} From d2514146e41eb8661666c7149749158fe9041a82 Mon Sep 17 00:00:00 2001 From: hwinkr Date: Sat, 21 Sep 2024 10:29:41 +0900 Subject: [PATCH 03/27] =?UTF-8?q?feat:=20=EB=B3=B5=EC=9E=A1=ED=95=9C=20UI?= =?UTF-8?q?=20=ED=9D=90=EB=A6=84=EC=9D=84=20=EA=B4=80=EB=A6=AC=ED=95=98?= =?UTF-8?q?=EB=8A=94=20useFunnel=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=ED=9B=85?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - react-router-dom 라이브러리가 제공하는 useLocation, useNavigate 훅을 활용해서 라우팅 기능 구현 - 새로고침해도 현재 스텝은 유지 - Step 컴포넌트는 Funnel의 여러 자식 컴포넌트들로 현재 스텝에 맞는 컴포넌트만 렌더링 --- frontend/src/hooks/useFunnel/useFunnel.tsx | 48 ++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 frontend/src/hooks/useFunnel/useFunnel.tsx diff --git a/frontend/src/hooks/useFunnel/useFunnel.tsx b/frontend/src/hooks/useFunnel/useFunnel.tsx new file mode 100644 index 000000000..04b0e9530 --- /dev/null +++ b/frontend/src/hooks/useFunnel/useFunnel.tsx @@ -0,0 +1,48 @@ +import { useMemo } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; + +import FunnelMain from './FunnelMain'; +import type { RouteFunnelProps, StepProps, StepType } from './useFunnel.type'; + +const useFunnel = (steps: Steps, initialStep: Steps[number]) => { + const location = useLocation(); + const navigate = useNavigate(); + + const setStep = (step: Steps[number]) => { + navigate(location.pathname, { + state: { + currentStep: step, + }, + }); + }; + + // 아직 헤더 디자인을 하지 않은 상태이기 때문에, goPrevStep은 사용하지 않는 상태입니다.(@해리) + const goPrevStep = () => { + navigate(-1); + }; + + const Step = ({ children }: StepProps) => { + return <>{children}; + }; + + // 컴포넌트가 다시 렌더링 될 때마다, Funnel 인스턴스가 다시 생성되는 문제가 있어서, useMemo로 감싸는 것으로 수정(@해리) + const Funnel = useMemo( + () => + Object.assign( + function RoteFunnel(props: RouteFunnelProps) { + const step = + (location.state as { currentStep?: Steps[number] })?.currentStep || initialStep; + + return steps={steps} step={step} {...props} />; + }, + { + Step, + }, + ), + [location.state, initialStep, steps], + ); + + return [setStep, Funnel] as const; +}; + +export default useFunnel; From caa868fb9f63373fb8b50d3013e6413363d704c2 Mon Sep 17 00:00:00 2001 From: hwinkr Date: Sat, 21 Sep 2024 10:31:27 +0900 Subject: [PATCH 04/27] =?UTF-8?q?chore:=20useInput=20=EC=BB=A4=EC=8A=A4?= =?UTF-8?q?=ED=85=80=20=ED=9B=85=EC=9D=98=20=EB=B0=98=ED=99=98=EA=B0=92=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - useInput 커스텀 훅의 반환값을 props로 받는 컴포넌트 정의에서 활용할 수 있도록 useInput 커스텀 훅의 반환값 타입을 정의 --- frontend/src/hooks/useInput/useInput.ts | 4 +++- frontend/src/hooks/useInput/useInput.type.ts | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 frontend/src/hooks/useInput/useInput.type.ts diff --git a/frontend/src/hooks/useInput/useInput.ts b/frontend/src/hooks/useInput/useInput.ts index 6aba728ee..7e8e5aa30 100644 --- a/frontend/src/hooks/useInput/useInput.ts +++ b/frontend/src/hooks/useInput/useInput.ts @@ -1,12 +1,14 @@ import type { ChangeEvent } from 'react'; import { useState } from 'react'; +import type { UseInputResult } from './useInput.type'; + interface ValidationRules { pattern?: RegExp; errorMessage?: string; } -const useInput = (rules?: ValidationRules) => { +const useInput = (rules?: ValidationRules): UseInputResult => { const [value, setValue] = useState(''); const [errorMessage, setErrorMessage] = useState(null); diff --git a/frontend/src/hooks/useInput/useInput.type.ts b/frontend/src/hooks/useInput/useInput.type.ts new file mode 100644 index 000000000..d2f64e575 --- /dev/null +++ b/frontend/src/hooks/useInput/useInput.type.ts @@ -0,0 +1,5 @@ +export interface UseInputResult { + value: string; + errorMessage: string | null; + onValueChange: (e: React.ChangeEvent) => void; +} From fa23d7d20607397d0b315e7499aa29688c6550d1 Mon Sep 17 00:00:00 2001 From: hwinkr Date: Sat, 21 Sep 2024 10:32:01 +0900 Subject: [PATCH 05/27] =?UTF-8?q?chore:=20useTimeRangeDropdown=20=EC=BB=A4?= =?UTF-8?q?=EC=8A=A4=ED=85=80=20=ED=9B=85=EC=9D=98=20=EB=B0=98=ED=99=98?= =?UTF-8?q?=EA=B0=92=20=ED=83=80=EC=9E=85=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - useTimeRangeDropdown 커스텀 훅의 반환값을 props로 받는 컴포넌트 정의에서 활용할 수 있도록 useTimeRangeDropdown 커스텀 훅의 반환값 타입을 정의 --- .../useTimeRangeDropdown/useTimeRangeDropdown.ts | 3 ++- .../useTimeRangeDropdown.type.ts | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 frontend/src/hooks/useTimeRangeDropdown/useTimeRangeDropdown.type.ts diff --git a/frontend/src/hooks/useTimeRangeDropdown/useTimeRangeDropdown.ts b/frontend/src/hooks/useTimeRangeDropdown/useTimeRangeDropdown.ts index b85287b24..61a0f31da 100644 --- a/frontend/src/hooks/useTimeRangeDropdown/useTimeRangeDropdown.ts +++ b/frontend/src/hooks/useTimeRangeDropdown/useTimeRangeDropdown.ts @@ -1,6 +1,7 @@ import { useState } from 'react'; import { INITIAL_END_TIME, INITIAL_START_TIME } from './constants'; +import type { UseTimeRangeDropdownResult } from './useTimeRangeDropdown.type'; import { addHoursToCurrentTime, generateEndTimeOptions, @@ -8,7 +9,7 @@ import { isTimeSelectable, } from './useTimeRangeDropdown.utils'; -export default function useTimeRangeDropdown() { +export default function useTimeRangeDropdown(): UseTimeRangeDropdownResult { const [startTime, setStartTime] = useState(INITIAL_START_TIME); const [endTime, setEndTime] = useState(INITIAL_END_TIME); diff --git a/frontend/src/hooks/useTimeRangeDropdown/useTimeRangeDropdown.type.ts b/frontend/src/hooks/useTimeRangeDropdown/useTimeRangeDropdown.type.ts new file mode 100644 index 000000000..958430fe7 --- /dev/null +++ b/frontend/src/hooks/useTimeRangeDropdown/useTimeRangeDropdown.type.ts @@ -0,0 +1,14 @@ +import type { Option } from '@components/_common/Dropdown'; + +export interface UseTimeRangeDropdownResult { + startTime: { + value: string; + options: Option[]; + }; + endTime: { + value: string; + options: Option[]; + }; + handleStartTimeChange: (time: string) => void; + handleEndTimeChange: (time: string) => void; +} From 56bb925ff2bd6c9cf40e34667eda6bc488fd9176 Mon Sep 17 00:00:00 2001 From: hwinkr Date: Sat, 21 Sep 2024 10:32:46 +0900 Subject: [PATCH 06/27] =?UTF-8?q?feat:=20=EC=95=BD=EC=86=8D=EC=9D=84=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=ED=95=A0=20=EB=95=8C=20=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EB=AA=A8=EB=93=A0=20=EC=A7=80=EC=97=AD=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=EB=A5=BC=20=EA=B4=80=EB=A6=AC=ED=95=98=EB=8A=94=20use?= =?UTF-8?q?CreateMeeting=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=ED=9B=85=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../useCreateMeeting/useCreateMeeting.ts | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 frontend/src/hooks/useCreateMeeting/useCreateMeeting.ts diff --git a/frontend/src/hooks/useCreateMeeting/useCreateMeeting.ts b/frontend/src/hooks/useCreateMeeting/useCreateMeeting.ts new file mode 100644 index 000000000..5c6a3f860 --- /dev/null +++ b/frontend/src/hooks/useCreateMeeting/useCreateMeeting.ts @@ -0,0 +1,64 @@ +import { useState } from 'react'; + +import useInput from '@hooks/useInput/useInput'; +import useTimeRangeDropdown from '@hooks/useTimeRangeDropdown/useTimeRangeDropdown'; + +import { FIELD_DESCRIPTIONS, INPUT_FIELD_PATTERN } from '@constants/inputFields'; + +const useCreateMeeting = () => { + const meetingNameInput = useInput({ + pattern: INPUT_FIELD_PATTERN.meetingName, + errorMessage: FIELD_DESCRIPTIONS.meetingName, + }); + const isMeetingNameInvalid = + meetingNameInput.errorMessage !== null || meetingNameInput.value.length < 1; + + const hostNickNameInput = useInput({ + pattern: INPUT_FIELD_PATTERN.nickname, + errorMessage: FIELD_DESCRIPTIONS.nickname, + }); + + const hostPasswordInput = useInput({ + pattern: INPUT_FIELD_PATTERN.password, + errorMessage: FIELD_DESCRIPTIONS.password, + }); + const isHostInfoInValid = + hostNickNameInput.value.length < 1 || + hostPasswordInput.value.length < 1 || + hostNickNameInput.errorMessage !== null || + hostPasswordInput.errorMessage !== null; + + const [selectedDates, setSelectedDates] = useState([]); + const areDatesUnselected = selectedDates.length < 1; + const hasDate = (date: string) => selectedDates.includes(date); + + const handleDateClick = (date: string) => { + setSelectedDates((prevDates) => + hasDate(date) ? prevDates.filter((d) => d !== date) : [...prevDates, date], + ); + }; + + const { startTime, endTime, handleStartTimeChange, handleEndTimeChange } = useTimeRangeDropdown(); + + const isCreateMeetingFormInValid = + isMeetingNameInvalid || (isHostInfoInValid && areDatesUnselected); + + return { + meetingNameInput, + isMeetingNameInvalid, + hostNickNameInput, + hostPasswordInput, + isHostInfoInValid, + hasDate, + handleDateClick, + meetingTimeInput: { + startTime, + endTime, + handleEndTimeChange, + handleStartTimeChange, + }, + isCreateMeetingFormInValid, + }; +}; + +export default useCreateMeeting; From 2ac567e9a15c6ecb7b7c9430f42544aec7a977b9 Mon Sep 17 00:00:00 2001 From: hwinkr Date: Sat, 21 Sep 2024 10:34:48 +0900 Subject: [PATCH 07/27] =?UTF-8?q?feat:=20=EC=95=BD=EC=86=8D=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=EC=9D=84=20=EC=9E=85=EB=A0=A5=EB=B0=9B=EB=8A=94=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CreateMeetingFunnelPage/MeetingName.tsx | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 frontend/src/pages/CreateMeetingFunnelPage/MeetingName.tsx diff --git a/frontend/src/pages/CreateMeetingFunnelPage/MeetingName.tsx b/frontend/src/pages/CreateMeetingFunnelPage/MeetingName.tsx new file mode 100644 index 000000000..a54223960 --- /dev/null +++ b/frontend/src/pages/CreateMeetingFunnelPage/MeetingName.tsx @@ -0,0 +1,52 @@ +import BottomFixedButton from '@components/_common/Buttons/BottomFixedButton'; +import Field from '@components/_common/Field'; +import Input from '@components/_common/Input'; + +import useButtonOnKeyboard from '@hooks/useButtonOnKeyboard/useButtonOnKeyboard'; +import type { UseInputResult } from '@hooks/useInput/useInput.type'; + +import { FIELD_DESCRIPTIONS } from '@constants/inputFields'; + +interface MeetingNameProps { + meetingNameInput: UseInputResult; + isMeetingNameInvalid: boolean; + onNextStep: () => void; +} + +export default function MeetingName({ + meetingNameInput, + isMeetingNameInvalid, + onNextStep, +}: MeetingNameProps) { + const { + value: meetingName, + onValueChange: handleMeetingNameChange, + errorMessage: meetingNameErrorMessage, + } = meetingNameInput; + + const resizedButtonHeight = useButtonOnKeyboard(); + + return ( + <> + + + + + + + + 다음 + + + ); +} From 11d1186367b02f9a214eb02cda1a374ccf27ef5c Mon Sep 17 00:00:00 2001 From: hwinkr Date: Sat, 21 Sep 2024 10:35:40 +0900 Subject: [PATCH 08/27] =?UTF-8?q?feat:=20=EC=95=BD=EC=86=8D=20=EC=A3=BC?= =?UTF-8?q?=EC=B5=9C=EC=9E=90=20=EC=A0=95=EB=B3=B4(=EB=8B=89=EB=84=A4?= =?UTF-8?q?=EC=9E=84,=20=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8)=EB=A5=BC=20?= =?UTF-8?q?=EC=9E=85=EB=A0=A5=EB=B0=9B=EB=8A=94=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MeetingHostInfo.tsx | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 frontend/src/pages/CreateMeetingFunnelPage/MeetingHostInfo.tsx diff --git a/frontend/src/pages/CreateMeetingFunnelPage/MeetingHostInfo.tsx b/frontend/src/pages/CreateMeetingFunnelPage/MeetingHostInfo.tsx new file mode 100644 index 000000000..87d5b2ece --- /dev/null +++ b/frontend/src/pages/CreateMeetingFunnelPage/MeetingHostInfo.tsx @@ -0,0 +1,75 @@ +import { css } from '@emotion/react'; +import { useEffect, useRef } from 'react'; + +import BottomFixedButton from '@components/_common/Buttons/BottomFixedButton'; +import Field from '@components/_common/Field'; +import Input from '@components/_common/Input'; + +import useButtonOnKeyboard from '@hooks/useButtonOnKeyboard/useButtonOnKeyboard'; +import type { UseInputResult } from '@hooks/useInput/useInput.type'; + +import { FIELD_DESCRIPTIONS } from '@constants/inputFields'; + +interface MeetingHostInfoProps { + hostNickNameInput: UseInputResult; + hostPasswordInput: UseInputResult; + isHostInfoInvalid: boolean; + onNextStep: () => void; +} + +export default function MeetingHostInfo({ + hostNickNameInput, + hostPasswordInput, + isHostInfoInvalid, + onNextStep, +}: MeetingHostInfoProps) { + const { + value: hostNickName, + onValueChange: handleHostNickNameChange, + errorMessage: hostNickNameErrorMessage, + } = hostNickNameInput; + const { + value: hostPassword, + onValueChange: handleHostPasswordChange, + errorMessage: hostPasswordErrorMessage, + } = hostPasswordInput; + + const resizedButtonHeight = useButtonOnKeyboard(); + + return ( +
+ + + + + + + + + + + + + + + 다음 + +
+ ); +} From 0e7adc5020517c87db3a2de9c3eca4c51db0941a Mon Sep 17 00:00:00 2001 From: hwinkr Date: Sat, 21 Sep 2024 10:36:27 +0900 Subject: [PATCH 09/27] =?UTF-8?q?feat:=20=EC=95=BD=EC=86=8D=20=EB=82=A0?= =?UTF-8?q?=EC=A7=9C,=20=EC=8B=9C=EA=B0=84=20=EB=B2=94=EC=9C=84=EB=A5=BC?= =?UTF-8?q?=20=EC=9E=85=EB=A0=A5=EB=B0=9B=EB=8A=94=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MeetingDateTime.tsx | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 frontend/src/pages/CreateMeetingFunnelPage/MeetingDateTime.tsx diff --git a/frontend/src/pages/CreateMeetingFunnelPage/MeetingDateTime.tsx b/frontend/src/pages/CreateMeetingFunnelPage/MeetingDateTime.tsx new file mode 100644 index 000000000..24f0ed081 --- /dev/null +++ b/frontend/src/pages/CreateMeetingFunnelPage/MeetingDateTime.tsx @@ -0,0 +1,48 @@ +import TimeRangeSelector from '@components/TimeRangeSelector'; +import BottomFixedButton from '@components/_common/Buttons/BottomFixedButton'; +import Calendar from '@components/_common/Calendar'; +import Field from '@components/_common/Field'; + +import type { UseTimeRangeDropdownResult } from '@hooks/useTimeRangeDropdown/useTimeRangeDropdown.type'; + +interface MeetingDateInput { + hasDate: (date: string) => boolean; + onDateClick: (date: string) => void; +} + +interface MeetingDateTimeProps { + meetingDateInput: MeetingDateInput; + meetingTimeInput: UseTimeRangeDropdownResult; + isCreateMeetingFormInValid: boolean; +} + +export default function MeetingDateTime({ + meetingDateInput, + meetingTimeInput, + isCreateMeetingFormInValid, +}: MeetingDateTimeProps) { + const { hasDate, onDateClick } = meetingDateInput; + const { startTime, endTime, handleStartTimeChange, handleEndTimeChange } = meetingTimeInput; + + return ( + <> + + + + + + + + + + console.log('hi')} disabled={isCreateMeetingFormInValid}> + 약속 생성하기 + + + ); +} From 3c9f39c594f1928f4d51d62995477ee2fbfcbecc Mon Sep 17 00:00:00 2001 From: hwinkr Date: Sat, 21 Sep 2024 10:38:48 +0900 Subject: [PATCH 10/27] =?UTF-8?q?feat:=20=EB=B7=B0=ED=8F=AC=ED=8A=B8=20?= =?UTF-8?q?=ED=95=98=EB=8B=A8=EC=97=90=20=EA=B3=A0=EC=A0=95=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EB=B0=94=ED=85=80=20=EB=B2=84=ED=8A=BC=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BottomFixedButton.styles.ts | 28 +++++++++++++++ .../Buttons/BottomFixedButton/index.tsx | 34 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 frontend/src/components/_common/Buttons/BottomFixedButton/BottomFixedButton.styles.ts create mode 100644 frontend/src/components/_common/Buttons/BottomFixedButton/index.tsx diff --git a/frontend/src/components/_common/Buttons/BottomFixedButton/BottomFixedButton.styles.ts b/frontend/src/components/_common/Buttons/BottomFixedButton/BottomFixedButton.styles.ts new file mode 100644 index 000000000..319ff735f --- /dev/null +++ b/frontend/src/components/_common/Buttons/BottomFixedButton/BottomFixedButton.styles.ts @@ -0,0 +1,28 @@ +import { css } from '@emotion/react'; + +export const s_bottomFixedStyles = css` + width: calc(100% + 1.6rem * 2); + max-width: 43rem; + + /* 버튼 컴포넌트의 full variants를 사용하려고 했으나 6rem보다 height값이 작아 직접 높이를 정의했어요(@해리) + full 버튼에 이미 의존하고 있는 컴포넌트들이 많아서 높이를 full 스타일을 변경할 수는 없었습니다. + */ + height: 6rem; + box-shadow: 0 -4px 4px rgb(0 0 0 / 25%); +`; + +export const s_bottomFixedButtonContainer = (height = 0) => css` + position: fixed; + bottom: 0; + left: 0; + transform: translateY(-${height}px); + + display: flex; + align-items: center; + justify-content: center; + + width: 100%; + height: 6rem; + + background-color: transparent; +`; diff --git a/frontend/src/components/_common/Buttons/BottomFixedButton/index.tsx b/frontend/src/components/_common/Buttons/BottomFixedButton/index.tsx new file mode 100644 index 000000000..6f785af37 --- /dev/null +++ b/frontend/src/components/_common/Buttons/BottomFixedButton/index.tsx @@ -0,0 +1,34 @@ +import type { ButtonHTMLAttributes } from 'react'; +import React from 'react'; + +import { Button } from '../Button'; +import { s_bottomFixedButtonContainer, s_bottomFixedStyles } from './BottomFixedButton.styles'; + +interface BottomFixedButtonProps extends ButtonHTMLAttributes { + children: React.ReactNode; + height?: number; + isLoading?: boolean; +} + +export default function BottomFixedButton({ + children, + height = 0, + isLoading = false, + ...props +}: BottomFixedButtonProps) { + return ( +
+ +
+
+ ); +} From 2c3b09b488feea51437947b89258833cae29e1e2 Mon Sep 17 00:00:00 2001 From: hwinkr Date: Sat, 21 Sep 2024 10:39:16 +0900 Subject: [PATCH 11/27] =?UTF-8?q?feat:=20=EB=AA=A8=EB=B0=94=EC=9D=BC=20?= =?UTF-8?q?=ED=82=A4=EB=B3=B4=EB=93=9C=20=EC=9C=84=EB=A1=9C=20=EB=B0=94?= =?UTF-8?q?=ED=85=80=EC=97=90=20=EA=B3=A0=EC=A0=95=EB=90=9C=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=EC=9D=B4=20=EC=98=AC=EB=9D=BC=EC=98=AC=20=EC=88=98=20?= =?UTF-8?q?=EC=9E=88=EB=8F=84=EB=A1=9D=20=ED=95=98=EB=8A=94=20=EC=BB=A4?= =?UTF-8?q?=EC=8A=A4=ED=85=80=20=ED=9B=85=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../useButtonOnKeyboard.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 frontend/src/hooks/useButtonOnKeyboard/useButtonOnKeyboard.ts diff --git a/frontend/src/hooks/useButtonOnKeyboard/useButtonOnKeyboard.ts b/frontend/src/hooks/useButtonOnKeyboard/useButtonOnKeyboard.ts new file mode 100644 index 000000000..f27af2a36 --- /dev/null +++ b/frontend/src/hooks/useButtonOnKeyboard/useButtonOnKeyboard.ts @@ -0,0 +1,25 @@ +import { useEffect, useState } from 'react'; + +const useButtonOnKeyboard = () => { + const [resizedButtonHeight, setResizedButtonHeight] = useState(0); + + useEffect(() => { + const handleButtonHeightResize = () => { + if (!visualViewport?.height) return; + + setResizedButtonHeight(window.innerHeight - visualViewport.height); + }; + + // 약속 이름 -> 약속 주최자 정보 입력으로 넘어갈 때 다음 버튼을 모바일 키보드로 올리기 위해서 resize 이벤트가 발생하지 않더라도 초기에 실행되도록 구현했어요.(@해리) + handleButtonHeightResize(); + visualViewport?.addEventListener('resize', handleButtonHeightResize); + + return () => { + visualViewport?.removeEventListener('resize', handleButtonHeightResize); + }; + }, []); + + return resizedButtonHeight; +}; + +export default useButtonOnKeyboard; From a0695ec1211fc8197bec7f6e4c035008361d3b81 Mon Sep 17 00:00:00 2001 From: hwinkr Date: Sat, 21 Sep 2024 10:39:47 +0900 Subject: [PATCH 12/27] =?UTF-8?q?feat:=20=EC=95=BD=EC=86=8D=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20UI=20=ED=9D=90=EB=A6=84=EC=9D=84=203=EB=8B=A8?= =?UTF-8?q?=EA=B3=84=EB=A1=9C=20=EA=B5=AC=EB=B6=84=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EC=95=BD=EC=86=8D=20=EC=83=9D=EC=84=B1=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/CreateMeetingFunnelPage/index.tsx | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 frontend/src/pages/CreateMeetingFunnelPage/index.tsx diff --git a/frontend/src/pages/CreateMeetingFunnelPage/index.tsx b/frontend/src/pages/CreateMeetingFunnelPage/index.tsx new file mode 100644 index 000000000..c8e318fe3 --- /dev/null +++ b/frontend/src/pages/CreateMeetingFunnelPage/index.tsx @@ -0,0 +1,51 @@ +import MeetingDateTime from '@pages/CreateMeetingFunnelPage/MeetingDateTime'; +import MeetingHostInfo from '@pages/CreateMeetingFunnelPage/MeetingHostInfo'; +import MeetingName from '@pages/CreateMeetingFunnelPage/MeetingName'; + +import useCreateMeeting from '@hooks/useCreateMeeting/useCreateMeeting'; +import useFunnel from '@hooks/useFunnel/useFunnel'; + +const testSteps = ['약속이름', '약속주최자정보', '약속날짜시간정보'] as const; +type TestSteps = typeof testSteps; + +export default function FunnelTestPage() { + const [setStep, Funnel] = useFunnel(testSteps, '약속이름'); + const { + meetingNameInput, + isMeetingNameInvalid, + hostNickNameInput, + hostPasswordInput, + isHostInfoInValid, + hasDate, + handleDateClick, + meetingTimeInput, + isCreateMeetingFormInValid, + } = useCreateMeeting(); + + return ( + + + setStep('약속주최자정보')} + /> + + + setStep('약속날짜시간정보')} + /> + + + + + + ); +} From 1a506368db9546c577d00a993098db417a9d33ba Mon Sep 17 00:00:00 2001 From: hwinkr Date: Sat, 21 Sep 2024 10:40:05 +0900 Subject: [PATCH 13/27] =?UTF-8?q?design:=20=EB=AA=A8=EB=B0=94=EC=9D=BC=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=EC=97=90=EC=84=9C=20=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A1=A4=EC=9D=B4=20=EB=90=98=EB=8A=94=20=EB=AC=B8=EC=A0=9C?= =?UTF-8?q?=EB=A5=BC=20=ED=95=B4=EA=B2=B0=ED=95=98=EA=B8=B0=20=EC=9C=84?= =?UTF-8?q?=ED=95=B4=EC=84=9C=20height=20=EC=86=8D=EC=84=B1=EA=B0=92=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/styles/global.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/styles/global.ts b/frontend/src/styles/global.ts index ae46b9fc6..5a07967ef 100644 --- a/frontend/src/styles/global.ts +++ b/frontend/src/styles/global.ts @@ -100,11 +100,12 @@ const globalStyles = css` } body { + overflow: hidden; display: flex; justify-content: center; width: 100%; - min-height: 100vh; + height: 100dvh; font-family: Pretendard, sans-serif; font-size: 1.6rem; From 08ff17c729bd15a9c596f7176bcb26aa3bb1fa5f Mon Sep 17 00:00:00 2001 From: hwinkr Date: Mon, 7 Oct 2024 11:43:46 +0900 Subject: [PATCH 14/27] =?UTF-8?q?chore:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20html=20=ED=83=9C=EA=B7=B8=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/_common/Buttons/BottomFixedButton/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/components/_common/Buttons/BottomFixedButton/index.tsx b/frontend/src/components/_common/Buttons/BottomFixedButton/index.tsx index 6f785af37..9e3d2450e 100644 --- a/frontend/src/components/_common/Buttons/BottomFixedButton/index.tsx +++ b/frontend/src/components/_common/Buttons/BottomFixedButton/index.tsx @@ -28,7 +28,6 @@ export default function BottomFixedButton({ > {children} -
); } From 8d81b1b1f8a3c8e2925ed85f4e54c4f748b51f6f Mon Sep 17 00:00:00 2001 From: hwinkr Date: Mon, 7 Oct 2024 11:43:58 +0900 Subject: [PATCH 15/27] =?UTF-8?q?chore:=20=EB=B0=94=ED=85=80=20=EA=B3=A0?= =?UTF-8?q?=EC=A0=95=20=EB=B2=84=ED=8A=BC=20=EC=B4=88=EA=B8=B0=20=EB=86=92?= =?UTF-8?q?=EC=9D=B4=20=EC=83=81=EC=88=98=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/hooks/useButtonOnKeyboard/useButtonOnKeyboard.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/hooks/useButtonOnKeyboard/useButtonOnKeyboard.ts b/frontend/src/hooks/useButtonOnKeyboard/useButtonOnKeyboard.ts index f27af2a36..6a33a9568 100644 --- a/frontend/src/hooks/useButtonOnKeyboard/useButtonOnKeyboard.ts +++ b/frontend/src/hooks/useButtonOnKeyboard/useButtonOnKeyboard.ts @@ -1,7 +1,9 @@ import { useEffect, useState } from 'react'; +const INITIAL_BUTTON_HEIGHT = 0; + const useButtonOnKeyboard = () => { - const [resizedButtonHeight, setResizedButtonHeight] = useState(0); + const [resizedButtonHeight, setResizedButtonHeight] = useState(INITIAL_BUTTON_HEIGHT); useEffect(() => { const handleButtonHeightResize = () => { From 61ac806142201e386f0cf0180f9b4f73675434d0 Mon Sep 17 00:00:00 2001 From: hwinkr Date: Mon, 7 Oct 2024 11:46:18 +0900 Subject: [PATCH 16/27] =?UTF-8?q?chore:=20RouteFunnel=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=98=A4=ED=83=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/hooks/useFunnel/useFunnel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/hooks/useFunnel/useFunnel.tsx b/frontend/src/hooks/useFunnel/useFunnel.tsx index 04b0e9530..9f5502d57 100644 --- a/frontend/src/hooks/useFunnel/useFunnel.tsx +++ b/frontend/src/hooks/useFunnel/useFunnel.tsx @@ -29,7 +29,7 @@ const useFunnel = (steps: Steps, initialStep: Steps[numb const Funnel = useMemo( () => Object.assign( - function RoteFunnel(props: RouteFunnelProps) { + function RouteFunnel(props: RouteFunnelProps) { const step = (location.state as { currentStep?: Steps[number] })?.currentStep || initialStep; From 301ebcdbb6e0befef95b6dafd63716825fff882a Mon Sep 17 00:00:00 2001 From: hwinkr Date: Mon, 7 Oct 2024 11:47:08 +0900 Subject: [PATCH 17/27] =?UTF-8?q?chore:=20useInput=20=EC=BB=A4=EC=8A=A4?= =?UTF-8?q?=ED=85=80=20=ED=9B=85=EC=9D=98=20=EB=B0=98=ED=99=98=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=EC=9D=84=20ReturnType=20=EC=9C=A0=ED=8B=B8=EB=A6=AC?= =?UTF-8?q?=ED=8B=B0=20=ED=83=80=EC=9E=85=EC=9D=84=20=ED=99=9C=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EA=B2=83=EC=9C=BC=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/hooks/useInput/useInput.ts | 6 +++--- frontend/src/hooks/useInput/useInput.type.ts | 5 ----- 2 files changed, 3 insertions(+), 8 deletions(-) delete mode 100644 frontend/src/hooks/useInput/useInput.type.ts diff --git a/frontend/src/hooks/useInput/useInput.ts b/frontend/src/hooks/useInput/useInput.ts index 7e8e5aa30..4d1080315 100644 --- a/frontend/src/hooks/useInput/useInput.ts +++ b/frontend/src/hooks/useInput/useInput.ts @@ -1,14 +1,12 @@ import type { ChangeEvent } from 'react'; import { useState } from 'react'; -import type { UseInputResult } from './useInput.type'; - interface ValidationRules { pattern?: RegExp; errorMessage?: string; } -const useInput = (rules?: ValidationRules): UseInputResult => { +const useInput = (rules?: ValidationRules) => { const [value, setValue] = useState(''); const [errorMessage, setErrorMessage] = useState(null); @@ -37,4 +35,6 @@ const useInput = (rules?: ValidationRules): UseInputResult => { }; }; +export type UseInputReturn = ReturnType; + export default useInput; diff --git a/frontend/src/hooks/useInput/useInput.type.ts b/frontend/src/hooks/useInput/useInput.type.ts deleted file mode 100644 index d2f64e575..000000000 --- a/frontend/src/hooks/useInput/useInput.type.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface UseInputResult { - value: string; - errorMessage: string | null; - onValueChange: (e: React.ChangeEvent) => void; -} From 9ee0f2cc938202a8622e370c4bae0dc245cf5ed7 Mon Sep 17 00:00:00 2001 From: hwinkr Date: Mon, 7 Oct 2024 11:47:22 +0900 Subject: [PATCH 18/27] =?UTF-8?q?chore:=20useTimeRangeDropdown=20=EC=BB=A4?= =?UTF-8?q?=EC=8A=A4=ED=85=80=20=ED=9B=85=EC=9D=98=20=EB=B0=98=ED=99=98=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=EC=9D=84=20ReturnType=20=EC=9C=A0=ED=8B=B8?= =?UTF-8?q?=EB=A6=AC=ED=8B=B0=20=ED=83=80=EC=9E=85=EC=9D=84=20=ED=99=9C?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EB=8A=94=20=EA=B2=83=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../useTimeRangeDropdown/useTimeRangeDropdown.ts | 9 ++++++--- .../useTimeRangeDropdown.type.ts | 14 -------------- 2 files changed, 6 insertions(+), 17 deletions(-) delete mode 100644 frontend/src/hooks/useTimeRangeDropdown/useTimeRangeDropdown.type.ts diff --git a/frontend/src/hooks/useTimeRangeDropdown/useTimeRangeDropdown.ts b/frontend/src/hooks/useTimeRangeDropdown/useTimeRangeDropdown.ts index 61a0f31da..0a6fb8dfe 100644 --- a/frontend/src/hooks/useTimeRangeDropdown/useTimeRangeDropdown.ts +++ b/frontend/src/hooks/useTimeRangeDropdown/useTimeRangeDropdown.ts @@ -1,7 +1,6 @@ import { useState } from 'react'; import { INITIAL_END_TIME, INITIAL_START_TIME } from './constants'; -import type { UseTimeRangeDropdownResult } from './useTimeRangeDropdown.type'; import { addHoursToCurrentTime, generateEndTimeOptions, @@ -9,7 +8,7 @@ import { isTimeSelectable, } from './useTimeRangeDropdown.utils'; -export default function useTimeRangeDropdown(): UseTimeRangeDropdownResult { +const useTimeRangeDropdown = () => { const [startTime, setStartTime] = useState(INITIAL_START_TIME); const [endTime, setEndTime] = useState(INITIAL_END_TIME); @@ -40,4 +39,8 @@ export default function useTimeRangeDropdown(): UseTimeRangeDropdownResult { handleStartTimeChange, handleEndTimeChange, } as const; -} +}; + +export type UseTimeRangeDropdownReturn = ReturnType; + +export default useTimeRangeDropdown; diff --git a/frontend/src/hooks/useTimeRangeDropdown/useTimeRangeDropdown.type.ts b/frontend/src/hooks/useTimeRangeDropdown/useTimeRangeDropdown.type.ts deleted file mode 100644 index 958430fe7..000000000 --- a/frontend/src/hooks/useTimeRangeDropdown/useTimeRangeDropdown.type.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { Option } from '@components/_common/Dropdown'; - -export interface UseTimeRangeDropdownResult { - startTime: { - value: string; - options: Option[]; - }; - endTime: { - value: string; - options: Option[]; - }; - handleStartTimeChange: (time: string) => void; - handleEndTimeChange: (time: string) => void; -} From ed17243497b68a1b34e82fff4a696ac0812d8bdd Mon Sep 17 00:00:00 2001 From: hwinkr Date: Mon, 7 Oct 2024 11:59:41 +0900 Subject: [PATCH 19/27] =?UTF-8?q?refactor:=20=EC=95=BD=EC=86=8D=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20api=20=ED=95=A8=EC=88=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80,=20=EC=9E=85=EB=A0=A5=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=A6=9D=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=20=EC=B6=94=EA=B0=80=20=EB=B6=84=EB=A6=AC,=20invalid?= =?UTF-8?q?=EB=A5=BC=20=ED=95=98=EB=82=98=EC=9D=98=20=EB=8B=A8=EC=96=B4?= =?UTF-8?q?=EB=A1=9C=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../useCreateMeeting/useCreateMeeting.ts | 53 ++++++++++++------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/frontend/src/hooks/useCreateMeeting/useCreateMeeting.ts b/frontend/src/hooks/useCreateMeeting/useCreateMeeting.ts index 5c6a3f860..b1a4d415e 100644 --- a/frontend/src/hooks/useCreateMeeting/useCreateMeeting.ts +++ b/frontend/src/hooks/useCreateMeeting/useCreateMeeting.ts @@ -1,48 +1,69 @@ import { useState } from 'react'; import useInput from '@hooks/useInput/useInput'; +import { INITIAL_END_TIME, INITIAL_START_TIME } from '@hooks/useTimeRangeDropdown/constants'; import useTimeRangeDropdown from '@hooks/useTimeRangeDropdown/useTimeRangeDropdown'; -import { FIELD_DESCRIPTIONS, INPUT_FIELD_PATTERN } from '@constants/inputFields'; +import { usePostMeetingMutation } from '@stores/servers/meeting/mutation'; + +import { FIELD_DESCRIPTIONS, INPUT_FIELD_PATTERN, INPUT_RULES } from '@constants/inputFields'; + +const checkInputInvalid = (value: string, errorMessage: string | null) => + value.length < INPUT_RULES.minimumLength || errorMessage !== null; const useCreateMeeting = () => { const meetingNameInput = useInput({ pattern: INPUT_FIELD_PATTERN.meetingName, errorMessage: FIELD_DESCRIPTIONS.meetingName, }); - const isMeetingNameInvalid = - meetingNameInput.errorMessage !== null || meetingNameInput.value.length < 1; + const isMeetingNameInvalid = checkInputInvalid( + meetingNameInput.value, + meetingNameInput.errorMessage, + ); const hostNickNameInput = useInput({ pattern: INPUT_FIELD_PATTERN.nickname, errorMessage: FIELD_DESCRIPTIONS.nickname, }); - const hostPasswordInput = useInput({ pattern: INPUT_FIELD_PATTERN.password, errorMessage: FIELD_DESCRIPTIONS.password, }); const isHostInfoInValid = - hostNickNameInput.value.length < 1 || - hostPasswordInput.value.length < 1 || - hostNickNameInput.errorMessage !== null || - hostPasswordInput.errorMessage !== null; + checkInputInvalid(hostNickNameInput.value, hostNickNameInput.errorMessage) || + checkInputInvalid(hostPasswordInput.value, hostPasswordInput.errorMessage); const [selectedDates, setSelectedDates] = useState([]); const areDatesUnselected = selectedDates.length < 1; const hasDate = (date: string) => selectedDates.includes(date); - const handleDateClick = (date: string) => { setSelectedDates((prevDates) => hasDate(date) ? prevDates.filter((d) => d !== date) : [...prevDates, date], ); }; - const { startTime, endTime, handleStartTimeChange, handleEndTimeChange } = useTimeRangeDropdown(); + const meetingTimeInput = useTimeRangeDropdown(); - const isCreateMeetingFormInValid = + const isCreateMeetingFormInvalid = isMeetingNameInvalid || (isHostInfoInValid && areDatesUnselected); + const { mutation: postMeetingMutation } = usePostMeetingMutation(); + + const handleMeetingCreateButtonClick = () => { + postMeetingMutation.mutate({ + meetingName: meetingNameInput.value, + hostName: hostNickNameInput.value, + hostPassword: hostPasswordInput.value, + availableMeetingDates: selectedDates, + meetingStartTime: meetingTimeInput.startTime.value, + // 시간상 24시는 존재하지 않기 때문에 백엔드에서 오류가 발생. 따라서 오전 12:00으로 표현하지만, 서버에 00:00으로 전송(@낙타) + meetingEndTime: + meetingTimeInput.endTime.value === INITIAL_END_TIME + ? INITIAL_START_TIME + : meetingTimeInput.endTime.value, + }); + }; + return { meetingNameInput, isMeetingNameInvalid, @@ -51,13 +72,9 @@ const useCreateMeeting = () => { isHostInfoInValid, hasDate, handleDateClick, - meetingTimeInput: { - startTime, - endTime, - handleEndTimeChange, - handleStartTimeChange, - }, - isCreateMeetingFormInValid, + meetingTimeInput, + isCreateMeetingFormInvalid, + handleMeetingCreateButtonClick, }; }; From b237cd8c480f147bb48a4ec5884119f37cb6f82a Mon Sep 17 00:00:00 2001 From: hwinkr Date: Mon, 7 Oct 2024 11:59:51 +0900 Subject: [PATCH 20/27] =?UTF-8?q?chore:=20=EC=9E=85=EB=A0=A5=20=EC=B5=9C?= =?UTF-8?q?=EC=86=8C=EA=B8=B8=EC=9D=B4=20=EC=83=81=EC=88=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/constants/inputFields.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/src/constants/inputFields.ts b/frontend/src/constants/inputFields.ts index e8ed5ef7b..63c563616 100644 --- a/frontend/src/constants/inputFields.ts +++ b/frontend/src/constants/inputFields.ts @@ -4,6 +4,10 @@ export const INPUT_FIELD_PATTERN = { password: /^\d{4}$/, // 4자리 숫자 }; +export const INPUT_RULES = { + minimumLength: 1, +}; + export const FIELD_DESCRIPTIONS = { meetingName: '약속 이름은 1~10자 사이로 입력해 주세요.', nickname: '닉네임은 1~5자 사이로 입력해 주세요.', From a0c1a037b1f0f655b5e12326009309287b4b8e2b Mon Sep 17 00:00:00 2001 From: hwinkr Date: Mon, 7 Oct 2024 12:00:57 +0900 Subject: [PATCH 21/27] =?UTF-8?q?chore:=20invalid=EB=A5=BC=20=ED=95=98?= =?UTF-8?q?=EB=82=98=EC=9D=98=20=EB=8B=A8=EC=96=B4=EB=A1=9C=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=B4=EC=84=9C=20props=20=EC=A0=95=EC=9D=98,=20api?= =?UTF-8?q?=20=EC=9A=94=EC=B2=AD=20=ED=95=A8=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/CreateMeetingFunnelPage/index.tsx | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/frontend/src/pages/CreateMeetingFunnelPage/index.tsx b/frontend/src/pages/CreateMeetingFunnelPage/index.tsx index c8e318fe3..e21663b1c 100644 --- a/frontend/src/pages/CreateMeetingFunnelPage/index.tsx +++ b/frontend/src/pages/CreateMeetingFunnelPage/index.tsx @@ -5,11 +5,12 @@ import MeetingName from '@pages/CreateMeetingFunnelPage/MeetingName'; import useCreateMeeting from '@hooks/useCreateMeeting/useCreateMeeting'; import useFunnel from '@hooks/useFunnel/useFunnel'; -const testSteps = ['약속이름', '약속주최자정보', '약속날짜시간정보'] as const; -type TestSteps = typeof testSteps; +import { CREATE_MEETING_STEPS, meetingStepValues } from '@constants/meeting'; + +type Steps = typeof meetingStepValues; export default function FunnelTestPage() { - const [setStep, Funnel] = useFunnel(testSteps, '약속이름'); + const [setStep, Funnel] = useFunnel(meetingStepValues, '약속이름'); const { meetingNameInput, isMeetingNameInvalid, @@ -19,31 +20,33 @@ export default function FunnelTestPage() { hasDate, handleDateClick, meetingTimeInput, - isCreateMeetingFormInValid, + isCreateMeetingFormInvalid, + handleMeetingCreateButtonClick, } = useCreateMeeting(); return ( - + setStep('약속주최자정보')} + onNextStep={() => setStep(CREATE_MEETING_STEPS.meetingHostInfo)} /> - + setStep('약속날짜시간정보')} + onNextStep={() => setStep(CREATE_MEETING_STEPS.meetingDateTime)} /> - + From b19834a66d1dc6cfee46242cb6adcf0f9df8aca0 Mon Sep 17 00:00:00 2001 From: hwinkr Date: Mon, 7 Oct 2024 12:01:11 +0900 Subject: [PATCH 22/27] =?UTF-8?q?chore:=20=EC=95=BD=EC=86=8D=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=8A=A4=ED=85=9D=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20?= =?UTF-8?q?=EC=83=81=EC=88=98=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/constants/meeting.ts | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 frontend/src/constants/meeting.ts diff --git a/frontend/src/constants/meeting.ts b/frontend/src/constants/meeting.ts new file mode 100644 index 000000000..14f859be9 --- /dev/null +++ b/frontend/src/constants/meeting.ts @@ -0,0 +1,7 @@ +export const CREATE_MEETING_STEPS = { + meetingName: '약속이름', + meetingHostInfo: '약속주최자정보', + meetingDateTime: '약속날짜시간정보', +} as const; + +export const meetingStepValues = Object.values(CREATE_MEETING_STEPS); From 080f014d46b0c8b0ac50e9bbd4b16928f1a32ec9 Mon Sep 17 00:00:00 2001 From: hwinkr Date: Mon, 7 Oct 2024 12:01:42 +0900 Subject: [PATCH 23/27] =?UTF-8?q?chore:=20invalid=EB=A1=9C=20props=20?= =?UTF-8?q?=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MeetingHostInfo.tsx | 18 +++++------------- .../CreateMeetingFunnelPage/MeetingName.tsx | 4 ++-- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/frontend/src/pages/CreateMeetingFunnelPage/MeetingHostInfo.tsx b/frontend/src/pages/CreateMeetingFunnelPage/MeetingHostInfo.tsx index 87d5b2ece..77493d6c7 100644 --- a/frontend/src/pages/CreateMeetingFunnelPage/MeetingHostInfo.tsx +++ b/frontend/src/pages/CreateMeetingFunnelPage/MeetingHostInfo.tsx @@ -1,18 +1,15 @@ -import { css } from '@emotion/react'; -import { useEffect, useRef } from 'react'; - import BottomFixedButton from '@components/_common/Buttons/BottomFixedButton'; import Field from '@components/_common/Field'; import Input from '@components/_common/Input'; import useButtonOnKeyboard from '@hooks/useButtonOnKeyboard/useButtonOnKeyboard'; -import type { UseInputResult } from '@hooks/useInput/useInput.type'; +import type { UseInputReturn } from '@hooks/useInput/useInput'; import { FIELD_DESCRIPTIONS } from '@constants/inputFields'; interface MeetingHostInfoProps { - hostNickNameInput: UseInputResult; - hostPasswordInput: UseInputResult; + hostNickNameInput: UseInputReturn; + hostPasswordInput: UseInputReturn; isHostInfoInvalid: boolean; onNextStep: () => void; } @@ -37,12 +34,7 @@ export default function MeetingHostInfo({ const resizedButtonHeight = useButtonOnKeyboard(); return ( -
+ <> @@ -70,6 +62,6 @@ export default function MeetingHostInfo({ > 다음 -
+ ); } diff --git a/frontend/src/pages/CreateMeetingFunnelPage/MeetingName.tsx b/frontend/src/pages/CreateMeetingFunnelPage/MeetingName.tsx index a54223960..98ec588b2 100644 --- a/frontend/src/pages/CreateMeetingFunnelPage/MeetingName.tsx +++ b/frontend/src/pages/CreateMeetingFunnelPage/MeetingName.tsx @@ -3,12 +3,12 @@ import Field from '@components/_common/Field'; import Input from '@components/_common/Input'; import useButtonOnKeyboard from '@hooks/useButtonOnKeyboard/useButtonOnKeyboard'; -import type { UseInputResult } from '@hooks/useInput/useInput.type'; +import type { UseInputReturn } from '@hooks/useInput/useInput'; import { FIELD_DESCRIPTIONS } from '@constants/inputFields'; interface MeetingNameProps { - meetingNameInput: UseInputResult; + meetingNameInput: UseInputReturn; isMeetingNameInvalid: boolean; onNextStep: () => void; } From 2c31e0d02f2ce69be4a6ae06044479e68e2c2140 Mon Sep 17 00:00:00 2001 From: hwinkr Date: Mon, 7 Oct 2024 12:01:59 +0900 Subject: [PATCH 24/27] =?UTF-8?q?chore:=20invalid=EB=A1=9C=20props=20?= =?UTF-8?q?=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EC=88=98=EC=A0=95,=20=EC=95=BD?= =?UTF-8?q?=EC=86=8D=20=EC=83=9D=EC=84=B1=20api=20=ED=95=B8=EB=93=A4?= =?UTF-8?q?=EB=9F=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CreateMeetingFunnelPage/MeetingDateTime.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/frontend/src/pages/CreateMeetingFunnelPage/MeetingDateTime.tsx b/frontend/src/pages/CreateMeetingFunnelPage/MeetingDateTime.tsx index 24f0ed081..a71f7fb6b 100644 --- a/frontend/src/pages/CreateMeetingFunnelPage/MeetingDateTime.tsx +++ b/frontend/src/pages/CreateMeetingFunnelPage/MeetingDateTime.tsx @@ -3,8 +3,9 @@ import BottomFixedButton from '@components/_common/Buttons/BottomFixedButton'; import Calendar from '@components/_common/Calendar'; import Field from '@components/_common/Field'; -import type { UseTimeRangeDropdownResult } from '@hooks/useTimeRangeDropdown/useTimeRangeDropdown.type'; +import type { UseTimeRangeDropdownReturn } from '@hooks/useTimeRangeDropdown/useTimeRangeDropdown'; +// 시작, 끝이 추가된 달력이랑 합쳐진 후, interface 수정예정(@해리) interface MeetingDateInput { hasDate: (date: string) => boolean; onDateClick: (date: string) => void; @@ -12,14 +13,16 @@ interface MeetingDateInput { interface MeetingDateTimeProps { meetingDateInput: MeetingDateInput; - meetingTimeInput: UseTimeRangeDropdownResult; - isCreateMeetingFormInValid: boolean; + meetingTimeInput: UseTimeRangeDropdownReturn; + isCreateMeetingFormInvalid: boolean; + onMeetingCreateButtonClick: () => void; } export default function MeetingDateTime({ meetingDateInput, meetingTimeInput, - isCreateMeetingFormInValid, + isCreateMeetingFormInvalid, + onMeetingCreateButtonClick, }: MeetingDateTimeProps) { const { hasDate, onDateClick } = meetingDateInput; const { startTime, endTime, handleStartTimeChange, handleEndTimeChange } = meetingTimeInput; @@ -40,7 +43,7 @@ export default function MeetingDateTime({ handleEndTimeChange={handleEndTimeChange} /> - console.log('hi')} disabled={isCreateMeetingFormInValid}> + 약속 생성하기 From 2d5897123768d15ebe3a94ad6508c636b961628b Mon Sep 17 00:00:00 2001 From: hwinkr Date: Mon, 7 Oct 2024 12:02:09 +0900 Subject: [PATCH 25/27] =?UTF-8?q?chore:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20hidden=20css=20=EC=86=8D?= =?UTF-8?q?=EC=84=B1=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/styles/global.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/styles/global.ts b/frontend/src/styles/global.ts index 5a07967ef..25ade3c17 100644 --- a/frontend/src/styles/global.ts +++ b/frontend/src/styles/global.ts @@ -100,7 +100,6 @@ const globalStyles = css` } body { - overflow: hidden; display: flex; justify-content: center; From 2faa1aab828f0f0e6e1f0b23cce7b7f72500a1f6 Mon Sep 17 00:00:00 2001 From: hwinkr Date: Mon, 7 Oct 2024 20:26:41 +0900 Subject: [PATCH 26/27] =?UTF-8?q?refactor:=20=EC=84=A0=ED=83=9D=EB=90=9C?= =?UTF-8?q?=20=EB=82=A0=EC=A7=9C=20=EC=83=81=ED=83=9C=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EB=A5=BC=20set=20=EC=9E=90=EB=A3=8C=EA=B5=AC=EC=A1=B0=EB=A5=BC?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8A=94=20=EA=B2=83=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../useCreateMeeting/useCreateMeeting.ts | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/frontend/src/hooks/useCreateMeeting/useCreateMeeting.ts b/frontend/src/hooks/useCreateMeeting/useCreateMeeting.ts index b1a4d415e..128dc2dc0 100644 --- a/frontend/src/hooks/useCreateMeeting/useCreateMeeting.ts +++ b/frontend/src/hooks/useCreateMeeting/useCreateMeeting.ts @@ -29,32 +29,38 @@ const useCreateMeeting = () => { pattern: INPUT_FIELD_PATTERN.password, errorMessage: FIELD_DESCRIPTIONS.password, }); - const isHostInfoInValid = + const isHostInfoInvalid = checkInputInvalid(hostNickNameInput.value, hostNickNameInput.errorMessage) || checkInputInvalid(hostPasswordInput.value, hostPasswordInput.errorMessage); - const [selectedDates, setSelectedDates] = useState([]); - const areDatesUnselected = selectedDates.length < 1; - const hasDate = (date: string) => selectedDates.includes(date); + const [selectedDates, setSelectedDates] = useState>(new Set()); + + const hasDate = (date: string) => selectedDates.has(date); const handleDateClick = (date: string) => { - setSelectedDates((prevDates) => - hasDate(date) ? prevDates.filter((d) => d !== date) : [...prevDates, date], - ); + setSelectedDates((prevDates) => { + const newSelectedDates = new Set(prevDates); + newSelectedDates.has(date) ? newSelectedDates.delete(date) : newSelectedDates.add(date); + + return newSelectedDates; + }); }; + const areDatesUnselected = selectedDates.size < 1; const meetingTimeInput = useTimeRangeDropdown(); const isCreateMeetingFormInvalid = - isMeetingNameInvalid || (isHostInfoInValid && areDatesUnselected); + isMeetingNameInvalid || (isHostInfoInvalid && areDatesUnselected); const { mutation: postMeetingMutation } = usePostMeetingMutation(); const handleMeetingCreateButtonClick = () => { + const selectedDatesArray = Array.from(selectedDates); + postMeetingMutation.mutate({ meetingName: meetingNameInput.value, hostName: hostNickNameInput.value, hostPassword: hostPasswordInput.value, - availableMeetingDates: selectedDates, + availableMeetingDates: selectedDatesArray, meetingStartTime: meetingTimeInput.startTime.value, // 시간상 24시는 존재하지 않기 때문에 백엔드에서 오류가 발생. 따라서 오전 12:00으로 표현하지만, 서버에 00:00으로 전송(@낙타) meetingEndTime: @@ -69,7 +75,7 @@ const useCreateMeeting = () => { isMeetingNameInvalid, hostNickNameInput, hostPasswordInput, - isHostInfoInValid, + isHostInfoInvalid, hasDate, handleDateClick, meetingTimeInput, From 5c5be28f39061968200a228face264896a8172e6 Mon Sep 17 00:00:00 2001 From: hwinkr Date: Mon, 7 Oct 2024 20:26:53 +0900 Subject: [PATCH 27/27] =?UTF-8?q?chore:=20Invalid=20=EB=8B=A8=EC=96=B4=20?= =?UTF-8?q?=EC=98=A4=ED=83=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/CreateMeetingFunnelPage/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/CreateMeetingFunnelPage/index.tsx b/frontend/src/pages/CreateMeetingFunnelPage/index.tsx index e21663b1c..9f9874c37 100644 --- a/frontend/src/pages/CreateMeetingFunnelPage/index.tsx +++ b/frontend/src/pages/CreateMeetingFunnelPage/index.tsx @@ -16,7 +16,7 @@ export default function FunnelTestPage() { isMeetingNameInvalid, hostNickNameInput, hostPasswordInput, - isHostInfoInValid, + isHostInfoInvalid, hasDate, handleDateClick, meetingTimeInput, @@ -37,7 +37,7 @@ export default function FunnelTestPage() { setStep(CREATE_MEETING_STEPS.meetingDateTime)} />