-
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.
[FE] Dropdown 컴포넌트 제작 및 시간 선택 hook 구현 (#71)
* feat(Dropdown): 컴포넌트 구현 * test(Dropdown): 컴포넌트 UI 테스트 * feat(useTimeRangeDropdown): 시간 선택 hook 구현 * test(useTimeRangeDropdown): 시작시간과 끝시간을 선택하는 로직에 대한 테스트 케이스 작성 * test(Dropdown): 시간 선택 UI 테스트 추가 * feat(useTimeRangeDropdown): 상수 정의 * refactor: 12시, 24시에 관련된 오전, 오후 구분 로직 수정 * refactor(useTimeRangeDropdown): 선택한 시간에 따라 선택할 수 있는 옵션 값이 변경되도록 구현 * refactor: 정의된 상수 사용 및 함수 네이밍 수정
- Loading branch information
Showing
7 changed files
with
258 additions
and
0 deletions.
There are no files selected for viewing
67 changes: 67 additions & 0 deletions
67
frontend/src/components/_common/Dropdown/Dropdown.stories.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,67 @@ | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
|
||
import { INITIAL_END_TIME, INITIAL_START_TIME } from '@hooks/useTimeRangeDropdown/constants'; | ||
import { | ||
generateEndTimeOptions, | ||
generateStartTimeOptions, | ||
} from '@hooks/useTimeRangeDropdown/useTimeRangeDropdown.utils'; | ||
|
||
import Dropdown from '.'; | ||
|
||
const meta = { | ||
title: 'Components/Dropdown', | ||
component: Dropdown, | ||
tags: ['autodocs'], | ||
parameters: { | ||
layout: 'centered', | ||
}, | ||
argTypes: { | ||
options: { | ||
control: 'object', | ||
description: '드롭다운 목록에 추가할 `value`와 `label`을 담고있는 객체 배열입니다.', | ||
}, | ||
}, | ||
} satisfies Meta<typeof Dropdown>; | ||
|
||
export default meta; | ||
|
||
type Story = StoryObj<typeof meta>; | ||
|
||
export const Default: Story = { | ||
args: { | ||
options: [ | ||
{ | ||
value: '옵션 1', | ||
label: '옵션 1', | ||
}, | ||
{ | ||
value: '옵션 2', | ||
label: '옵션 2', | ||
}, | ||
{ | ||
value: '옵션 3', | ||
label: '옵션 3', | ||
}, | ||
{ | ||
value: '옵션 4', | ||
label: '옵션 4', | ||
}, | ||
{ | ||
value: '옵션 5', | ||
label: '옵션 5', | ||
}, | ||
], | ||
}, | ||
}; | ||
|
||
export const StartTime: Story = { | ||
args: { | ||
options: generateStartTimeOptions(INITIAL_END_TIME), | ||
}, | ||
}; | ||
|
||
export const EndTime: Story = { | ||
args: { | ||
options: generateEndTimeOptions(INITIAL_START_TIME), | ||
}, | ||
}; |
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,8 @@ | ||
import { css } from '@emotion/react'; | ||
|
||
export const s_dropdown = css` | ||
width: 8rem; | ||
height: 2.8rem; | ||
padding: 0.4rem; | ||
border-radius: 0.4rem; | ||
`; |
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,24 @@ | ||
import type { SelectHTMLAttributes } from 'react'; | ||
|
||
import { s_dropdown } from './Dropdown.styles'; | ||
|
||
export type Option = { | ||
value: string; | ||
label: string; | ||
}; | ||
|
||
interface DropdownProps extends SelectHTMLAttributes<HTMLSelectElement> { | ||
options: Option[]; | ||
} | ||
|
||
export default function Dropdown({ options, ...props }: DropdownProps) { | ||
return ( | ||
<select {...props} css={s_dropdown}> | ||
{options.map(({ value, label }) => ( | ||
<option key={value} value={value}> | ||
{label} | ||
</option> | ||
))} | ||
</select> | ||
); | ||
} |
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,5 @@ | ||
export const INITIAL_START_TIME = '0:00'; | ||
export const INITIAL_END_TIME = '24:00'; | ||
|
||
export const MINIMUM_TIME = 0; | ||
export const MAXIMUM_TIME = 24; |
74 changes: 74 additions & 0 deletions
74
frontend/src/hooks/useTimeRangeDropdown/useTimeRangeDropdown.test.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,74 @@ | ||
import { renderHook } from '@testing-library/react'; | ||
import { act } from 'react'; | ||
|
||
import { INITIAL_END_TIME, INITIAL_START_TIME } from './constants'; | ||
import useTimeRangeDropdown from './useTimeRangeDropdown'; | ||
|
||
describe('useTimeRangeDropdown', () => { | ||
it(`초기 시작 시간(startTime)은 ${INITIAL_START_TIME}, 끝 시간(emdTime)은 ${INITIAL_END_TIME}으로 설정된다.`, () => { | ||
const { result } = renderHook(() => useTimeRangeDropdown()); | ||
|
||
expect(result.current.startTime).toBe(INITIAL_START_TIME); | ||
expect(result.current.endTime).toBe(INITIAL_END_TIME); | ||
}); | ||
|
||
it('선택한 시작 시간(startTime)이 끝 시간(endTime)보다 느리다면 선택한 시간값으로 변경되지 않는다.', () => { | ||
const CHANGE_TIME = '1:00'; | ||
const { result } = renderHook(() => useTimeRangeDropdown()); | ||
|
||
act(() => { | ||
result.current.onEndTimeChange('0:00'); | ||
}); | ||
|
||
act(() => { | ||
result.current.onStartTimeChange(CHANGE_TIME); | ||
}); | ||
|
||
expect(result.current.startTime).not.toBe(CHANGE_TIME); | ||
}); | ||
|
||
it('선택한 시작 시간(startTime)이 끝 시간(endTime)보다 빠르다면 값이 선택한 시간값으로 변경된다.', () => { | ||
const CHANGE_TIME = '1:00'; | ||
const { result } = renderHook(() => useTimeRangeDropdown()); | ||
|
||
act(() => { | ||
result.current.onEndTimeChange('23:00'); | ||
}); | ||
|
||
act(() => { | ||
result.current.onStartTimeChange(CHANGE_TIME); | ||
}); | ||
|
||
expect(result.current.startTime).toBe(CHANGE_TIME); | ||
}); | ||
|
||
it('선택한 끝 시간(endTime)이 시작 시간(startTime)보다 빠르다면 선택한 시간값으로 변경되지 않는다.', () => { | ||
const CHANGE_TIME = '1:00'; | ||
const { result } = renderHook(() => useTimeRangeDropdown()); | ||
|
||
act(() => { | ||
result.current.onStartTimeChange('12:00'); | ||
}); | ||
|
||
act(() => { | ||
result.current.onEndTimeChange(CHANGE_TIME); | ||
}); | ||
|
||
expect(result.current.endTime).not.toBe(CHANGE_TIME); | ||
}); | ||
|
||
it('선택한 끝 시간(endTime)이 시작 시간(startTime)보다 느리다면 선택한 시간값으로 변경된다.', () => { | ||
const CHANGE_TIME = '12:00'; | ||
const { result } = renderHook(() => useTimeRangeDropdown()); | ||
|
||
act(() => { | ||
result.current.onStartTimeChange('0:00'); | ||
}); | ||
|
||
act(() => { | ||
result.current.onEndTimeChange(CHANGE_TIME); | ||
}); | ||
|
||
expect(result.current.endTime).toBe(CHANGE_TIME); | ||
}); | ||
}); |
30 changes: 30 additions & 0 deletions
30
frontend/src/hooks/useTimeRangeDropdown/useTimeRangeDropdown.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,30 @@ | ||
import { useState } from 'react'; | ||
|
||
import { INITIAL_END_TIME, INITIAL_START_TIME } from './constants'; | ||
import { isTimeSelectable } from './useTimeRangeDropdown.utils'; | ||
|
||
export default function useTimeRangeDropdown() { | ||
const [startTime, setStartTime] = useState(INITIAL_START_TIME); | ||
const [endTime, setEndTime] = useState(INITIAL_END_TIME); | ||
|
||
// 시작 시간이 끝 시간보다 빠르다면 startTime이 변경되지 않도록 설정 | ||
const handleStartTimeChange = (time: string) => { | ||
if (!isTimeSelectable(time, endTime)) return; | ||
|
||
setStartTime(time); | ||
}; | ||
|
||
// 시작 시간이 끝 시간보다 빠르다면 startTime이 변경되지 않도록 설정 | ||
const handleEndTimeChange = (time: string) => { | ||
if (!isTimeSelectable(startTime, time)) return; | ||
|
||
setEndTime(time); | ||
}; | ||
|
||
return { | ||
startTime, | ||
endTime, | ||
onStartTimeChange: handleStartTimeChange, | ||
onEndTimeChange: handleEndTimeChange, | ||
} as const; | ||
} |
50 changes: 50 additions & 0 deletions
50
frontend/src/hooks/useTimeRangeDropdown/useTimeRangeDropdown.utils.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,50 @@ | ||
import type { Option } from '@components/_common/Dropdown'; | ||
|
||
import { MAXIMUM_TIME, MINIMUM_TIME } from './constants'; | ||
|
||
// label에 보여줄 이름을 변환해주는 함수 | ||
function formatHours(hour: number) { | ||
if (hour === 12) return '오후 12'; | ||
if (hour === 24) return '오전 12'; | ||
|
||
return hour > 12 ? `오후 ${hour - 12}` : `오전 ${hour}`; | ||
} | ||
|
||
// 0시 ~ endTime까지의 시간만 선택 가능한 함수. 시작 시간 options에 사용 | ||
export function generateStartTimeOptions(endTime: string) { | ||
const times: Option[] = []; | ||
const endHours = Number(endTime.split(':')[0]); | ||
|
||
for (let i = MINIMUM_TIME; i < endHours; i++) { | ||
const label = formatHours(i); | ||
|
||
times.push({ value: `${i}:00`, label: label + ':00' }); | ||
} | ||
|
||
return times; | ||
} | ||
|
||
// startTime + 1시 ~ 24시까지의 시간만 선택 가능한 함수. 시작 시간 options에 사용 | ||
export function generateEndTimeOptions(startTime: string) { | ||
const times: Option[] = []; | ||
const startHours = Number(startTime.split(':')[0]); | ||
|
||
for (let i = startHours + 1; i <= MAXIMUM_TIME; i++) { | ||
const label = formatHours(i); | ||
|
||
times.push({ value: `${i}:00`, label: label + ':00' }); | ||
} | ||
|
||
return times; | ||
} | ||
|
||
// 만약 시작 시간보다 끝 시간이 빠르다면 false를 반환하는 함수(@낙타) | ||
export function isTimeSelectable(startTime: string, endTime: string) { | ||
const [startHours, startMinutes] = startTime.split(':'); | ||
const [endHours, endMinutes] = endTime.split(':'); | ||
|
||
if (endHours < startHours) return false; | ||
if (endHours === startHours && endMinutes < startMinutes) return false; | ||
|
||
return true; | ||
} |