-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* chore: 퍼널 패턴에서 사용할 타입 정의 - readonly 타입의 문자열 스텝들을 StepType으로 정의 - FunnelProps, StepProps의 타입을 StepType에 포함되는 Steps 타입을 사용해서 정의 * feat: 현재 렌더링 해야 할 자식 컴포넌트를 결정하는 FunnelMain 컴포넌트 구현 - Funnel 컴포넌트의 여러 자식 컴포넌트 중 child 컴포넌트가 스텝에 속하는 컴포넌트인지 확인 - 현재 스텝에 맞는 targetStep 컴포넌트를 반환 * feat: 복잡한 UI 흐름을 관리하는 useFunnel 커스텀 훅 구현 - react-router-dom 라이브러리가 제공하는 useLocation, useNavigate 훅을 활용해서 라우팅 기능 구현 - 새로고침해도 현재 스텝은 유지 - Step 컴포넌트는 Funnel의 여러 자식 컴포넌트들로 현재 스텝에 맞는 컴포넌트만 렌더링 * chore: useInput 커스텀 훅의 반환값 타입 정의 - useInput 커스텀 훅의 반환값을 props로 받는 컴포넌트 정의에서 활용할 수 있도록 useInput 커스텀 훅의 반환값 타입을 정의 * chore: useTimeRangeDropdown 커스텀 훅의 반환값 타입 정의 - useTimeRangeDropdown 커스텀 훅의 반환값을 props로 받는 컴포넌트 정의에서 활용할 수 있도록 useTimeRangeDropdown 커스텀 훅의 반환값 타입을 정의 * feat: 약속을 생성할 때 필요한 모든 지역 상태를 관리하는 useCreateMeeting 커스텀 훅 구현 * feat: 약속 이름을 입력받는 컴포넌트 구현 * feat: 약속 주최자 정보(닉네임, 비밀번호)를 입력받는 컴포넌트 구현 * feat: 약속 날짜, 시간 범위를 입력받는 컴포넌트 구현 * feat: 뷰포트 하단에 고정되는 바텀 버튼 컴포넌트 구현 * feat: 모바일 키보드 위로 바텀에 고정된 버튼이 올라올 수 있도록 하는 커스텀 훅 구현 * feat: 약속 생성 UI 흐름을 3단계로 구분하는 약속 생성 페이지 구현 * design: 모바일 화면에서 스크롤이 되는 문제를 해결하기 위해서 height 속성값 수정 * chore: 사용하지 않는 html 태그 제거 * chore: 바텀 고정 버튼 초기 높이 상수화 * chore: RouteFunnel 컴포넌트 오타 수정 * chore: useInput 커스텀 훅의 반환 타입을 ReturnType 유틸리티 타입을 활용하는 것으로 수정 * chore: useTimeRangeDropdown 커스텀 훅의 반환 타입을 ReturnType 유틸리티 타입을 활용하는 것으로 수정 * refactor: 약속 생성 api 함수 추가, 입력 필드 유효성 검증 함수 추가 분리, invalid를 하나의 단어로 사용하도록 수정 * chore: 입력 최소길이 상수 추가 * chore: invalid를 하나의 단어로 사용해서 props 정의, api 요청 함수 추가 * chore: 약속 생성 스텝 네이밍 상수화 * chore: invalid로 props 네이밍 수정 * chore: invalid로 props 네이밍 수정, 약속 생성 api 핸들러 추가 * chore: 사용하지 않는 hidden css 속성 제거 * refactor: 선택된 날짜 상태 관리를 set 자료구조를 사용하는 것으로 수정 * chore: Invalid 단어 오타 수정
- Loading branch information
Showing
16 changed files
with
509 additions
and
3 deletions.
There are no files selected for viewing
28 changes: 28 additions & 0 deletions
28
frontend/src/components/_common/Buttons/BottomFixedButton/BottomFixedButton.styles.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
`; |
33 changes: 33 additions & 0 deletions
33
frontend/src/components/_common/Buttons/BottomFixedButton/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
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<HTMLButtonElement> { | ||
children: React.ReactNode; | ||
height?: number; | ||
isLoading?: boolean; | ||
} | ||
|
||
export default function BottomFixedButton({ | ||
children, | ||
height = 0, | ||
isLoading = false, | ||
...props | ||
}: BottomFixedButtonProps) { | ||
return ( | ||
<div css={s_bottomFixedButtonContainer(height)}> | ||
<Button | ||
variant="primary" | ||
size="full" | ||
isLoading={isLoading} | ||
borderRadius={0} | ||
customCss={s_bottomFixedStyles} | ||
{...props} | ||
> | ||
{children} | ||
</Button> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export const CREATE_MEETING_STEPS = { | ||
meetingName: '약속이름', | ||
meetingHostInfo: '약속주최자정보', | ||
meetingDateTime: '약속날짜시간정보', | ||
} as const; | ||
|
||
export const meetingStepValues = Object.values(CREATE_MEETING_STEPS); |
27 changes: 27 additions & 0 deletions
27
frontend/src/hooks/useButtonOnKeyboard/useButtonOnKeyboard.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { useEffect, useState } from 'react'; | ||
|
||
const INITIAL_BUTTON_HEIGHT = 0; | ||
|
||
const useButtonOnKeyboard = () => { | ||
const [resizedButtonHeight, setResizedButtonHeight] = useState(INITIAL_BUTTON_HEIGHT); | ||
|
||
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
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 { 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 = 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 = | ||
checkInputInvalid(hostNickNameInput.value, hostNickNameInput.errorMessage) || | ||
checkInputInvalid(hostPasswordInput.value, hostPasswordInput.errorMessage); | ||
|
||
const [selectedDates, setSelectedDates] = useState<Set<string>>(new Set()); | ||
|
||
const hasDate = (date: string) => selectedDates.has(date); | ||
const handleDateClick = (date: string) => { | ||
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); | ||
|
||
const { mutation: postMeetingMutation } = usePostMeetingMutation(); | ||
|
||
const handleMeetingCreateButtonClick = () => { | ||
const selectedDatesArray = Array.from(selectedDates); | ||
|
||
postMeetingMutation.mutate({ | ||
meetingName: meetingNameInput.value, | ||
hostName: hostNickNameInput.value, | ||
hostPassword: hostPasswordInput.value, | ||
availableMeetingDates: selectedDatesArray, | ||
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, | ||
hostNickNameInput, | ||
hostPasswordInput, | ||
isHostInfoInvalid, | ||
hasDate, | ||
handleDateClick, | ||
meetingTimeInput, | ||
isCreateMeetingFormInvalid, | ||
handleMeetingCreateButtonClick, | ||
}; | ||
}; | ||
|
||
export default useCreateMeeting; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 = <Steps extends StepType>( | ||
child: React.ReactNode, | ||
): child is ReactElement<StepProps<Steps>> => { | ||
return isValidElement(child) && typeof child.props.name === 'string'; | ||
}; | ||
|
||
export default function FunnelMain<Steps extends StepType>({ | ||
steps, | ||
step, | ||
children, | ||
}: FunnelProps<Steps>) { | ||
const childrenArray = Children.toArray(children) | ||
.filter(isValidFunnelChild<Steps>) | ||
.filter((child) => steps.includes(child.props.name)); | ||
|
||
const targetStep = childrenArray.find((child) => child.props.name === step); | ||
|
||
if (!targetStep) return null; | ||
|
||
return <>{targetStep}</>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 extends StepType>(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 = <Steps extends StepType>({ children }: StepProps<Steps>) => { | ||
return <>{children}</>; | ||
}; | ||
|
||
// 컴포넌트가 다시 렌더링 될 때마다, Funnel 인스턴스가 다시 생성되는 문제가 있어서, useMemo로 감싸는 것으로 수정(@해리) | ||
const Funnel = useMemo( | ||
() => | ||
Object.assign( | ||
function RouteFunnel(props: RouteFunnelProps<Steps>) { | ||
const step = | ||
(location.state as { currentStep?: Steps[number] })?.currentStep || initialStep; | ||
|
||
return <FunnelMain<Steps> steps={steps} step={step} {...props} />; | ||
}, | ||
{ | ||
Step, | ||
}, | ||
), | ||
[location.state, initialStep, steps], | ||
); | ||
|
||
return [setStep, Funnel] as const; | ||
}; | ||
|
||
export default useFunnel; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import type { ReactElement } from 'react'; | ||
|
||
export type StepType = Readonly<Array<string>>; | ||
|
||
export interface FunnelProps<Steps extends StepType> { | ||
steps: Steps; | ||
step: Steps[number]; | ||
children: Array<ReactElement<StepProps<Steps>>>; | ||
} | ||
|
||
export type RouteFunnelProps<Steps extends StepType> = Omit<FunnelProps<Steps>, 'steps' | 'step'>; | ||
|
||
export interface StepProps<Steps extends StepType> { | ||
name: Steps[number]; | ||
children: React.ReactNode; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
51 changes: 51 additions & 0 deletions
51
frontend/src/pages/CreateMeetingFunnelPage/MeetingDateTime.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
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 { UseTimeRangeDropdownReturn } from '@hooks/useTimeRangeDropdown/useTimeRangeDropdown'; | ||
|
||
// 시작, 끝이 추가된 달력이랑 합쳐진 후, interface 수정예정(@해리) | ||
interface MeetingDateInput { | ||
hasDate: (date: string) => boolean; | ||
onDateClick: (date: string) => void; | ||
} | ||
|
||
interface MeetingDateTimeProps { | ||
meetingDateInput: MeetingDateInput; | ||
meetingTimeInput: UseTimeRangeDropdownReturn; | ||
isCreateMeetingFormInvalid: boolean; | ||
onMeetingCreateButtonClick: () => void; | ||
} | ||
|
||
export default function MeetingDateTime({ | ||
meetingDateInput, | ||
meetingTimeInput, | ||
isCreateMeetingFormInvalid, | ||
onMeetingCreateButtonClick, | ||
}: MeetingDateTimeProps) { | ||
const { hasDate, onDateClick } = meetingDateInput; | ||
const { startTime, endTime, handleStartTimeChange, handleEndTimeChange } = meetingTimeInput; | ||
|
||
return ( | ||
<> | ||
<Field> | ||
<Field.Label id="날짜선택" labelText="약속 날짜 선택" /> | ||
<Calendar hasDate={hasDate} onDateClick={onDateClick} /> | ||
</Field> | ||
|
||
<Field> | ||
<Field.Label id="약속시간범위선택" labelText="약속 시간 범위 선택" /> | ||
<TimeRangeSelector | ||
startTime={startTime} | ||
endTime={endTime} | ||
handleStartTimeChange={handleStartTimeChange} | ||
handleEndTimeChange={handleEndTimeChange} | ||
/> | ||
</Field> | ||
<BottomFixedButton onClick={onMeetingCreateButtonClick} disabled={isCreateMeetingFormInvalid}> | ||
약속 생성하기 | ||
</BottomFixedButton> | ||
</> | ||
); | ||
} |
Oops, something went wrong.