Skip to content

Commit

Permalink
[FE] Dropdown 컴포넌트 제작 및 시간 선택 hook 구현 (#71)
Browse files Browse the repository at this point in the history
* feat(Dropdown): 컴포넌트 구현

* test(Dropdown): 컴포넌트 UI 테스트

* feat(useTimeRangeDropdown): 시간 선택 hook 구현

* test(useTimeRangeDropdown): 시작시간과 끝시간을 선택하는 로직에 대한 테스트 케이스 작성

* test(Dropdown): 시간 선택 UI 테스트 추가

* feat(useTimeRangeDropdown): 상수 정의

* refactor: 12시, 24시에 관련된 오전, 오후 구분 로직 수정

* refactor(useTimeRangeDropdown): 선택한 시간에 따라 선택할 수 있는 옵션 값이 변경되도록 구현

* refactor: 정의된 상수 사용 및 함수 네이밍 수정
  • Loading branch information
Largopie authored Jul 25, 2024
1 parent aa50b1c commit 8a83bd9
Show file tree
Hide file tree
Showing 7 changed files with 258 additions and 0 deletions.
67 changes: 67 additions & 0 deletions frontend/src/components/_common/Dropdown/Dropdown.stories.tsx
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),
},
};
8 changes: 8 additions & 0 deletions frontend/src/components/_common/Dropdown/Dropdown.styles.ts
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;
`;
24 changes: 24 additions & 0 deletions frontend/src/components/_common/Dropdown/index.tsx
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>
);
}
5 changes: 5 additions & 0 deletions frontend/src/hooks/useTimeRangeDropdown/constants.ts
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;
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 frontend/src/hooks/useTimeRangeDropdown/useTimeRangeDropdown.ts
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;
}
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;
}

0 comments on commit 8a83bd9

Please sign in to comment.