-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: (#20) 삭제, 파일 업로드 버튼 컴포넌트 UI 구현 * feat: (#20) 이미지 업로드 버튼을 눌렀을 때 이미지 업로드 창이 나오도록 구현 및 파일명 변경 * feat: (#20) 투표 선택지 아이템 컴포넌트 UI 구현 * feat: (#20) 투표 선택지 작성 리스트 컴포넌트 UI 구현 * feat: (#20) 훅 테스트 코드 작성 시작 * test: (#20) 투표 선택지 작성에 사용하는 훅 테스트 작성 * feat: (#20) 투표 선택지 작성 훅 구현 * feat: (#20) 투표 선택지 작성 훅 적용 및 UI 구현 * feat: (#20) 50자 이상 적었을 때 사용자에게 안내 기능 구현 * feat: (#20) 사진의 이미지가 5MB가 넘어갈 경우 유저에게 안내하도록 구현 * design: (#20): 삭제 버튼을 감싼 태그가 항상 왼쪽의 공간을 차지하도록 CSS 변경 * refactor: (#20) svg 코드를 assets 폴더로 이동 후 import 하여 사용하도록 수정 회색 버튼을 cssText로 관리하여 공통으로 관리하도록 수정 * refactor: (#20) 코드 가독성을 위한 함수명, 변수명 수정 * design: (#20) 화면 크기에 따라 폰트, 버튼 사이즈 변경되도록 구현 * style: (#20) CSS 속성 순서 변경 및 불필요한 타입 선언 제거 * chore: (#20) 함수 동작 과정에 대한 설명 주석 추가 * chore: (#20) 테스트 문구 변경
- Loading branch information
1 parent
e5e169f
commit 2e136a2
Showing
16 changed files
with
674 additions
and
0 deletions.
There are no files selected for viewing
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,132 @@ | ||
import { renderHook, act } from '@testing-library/react'; | ||
|
||
import { useWritingOption } from '../../src/hooks/useWritingOption'; | ||
|
||
/** | ||
* @jest-environment jsdom | ||
*/ | ||
|
||
const MOCK_MAX_VOTE_OPTION = [ | ||
{ id: 12341, text: '', imageUrl: '' }, | ||
{ id: 123451, text: '', imageUrl: '' }, | ||
{ | ||
id: 1234221, | ||
text: '방학 때 강릉으로 강아지와 기차여행을 하려했지만 장마가 와서 취소했어요. 여행을 별로 좋', | ||
imageUrl: '', | ||
}, | ||
{ | ||
id: 12342261, | ||
text: '방학 때 강릉으로 강아지와 기차여행을 하려했지만 장마가 와서 취소했어요. 여행을 별로 좋', | ||
imageUrl: 'https://source.unsplash.com/random', | ||
}, | ||
{ | ||
id: 1234451, | ||
text: '', | ||
imageUrl: 'https://source.unsplash.com/random', | ||
}, | ||
]; | ||
|
||
const MOCK_MIN_VOTE_OPTION = [ | ||
{ id: 12341, text: '', imageUrl: '' }, | ||
{ id: 123341, text: '', imageUrl: '' }, | ||
]; | ||
describe('useWritingOption 훅을 테스트 한다.', () => { | ||
test('초기 값이 없다면 기본으로 2개의 선택지가 존재해야 한다.', () => { | ||
const { result } = renderHook(() => useWritingOption()); | ||
|
||
const { optionList } = result.current; | ||
|
||
expect(optionList.length).toBe(2); | ||
|
||
expect(optionList[0].text).toBe(''); | ||
|
||
expect(optionList[0].imageUrl).toBe(''); | ||
}); | ||
|
||
test('기존 데이터가 있는 경우 기존 데이터가 선택지의 초기 값으로 존재해야 한다.', () => { | ||
const { result } = renderHook(() => useWritingOption(MOCK_MIN_VOTE_OPTION)); | ||
|
||
const { optionList } = result.current; | ||
|
||
expect(optionList).toEqual(MOCK_MIN_VOTE_OPTION); | ||
}); | ||
|
||
test('투표 선택지를 추가할 수 있어야 한다. 생성된 선택지는 text와 imageUrl 값을 가지고 있다.', () => { | ||
const { result } = renderHook(() => useWritingOption(MOCK_MIN_VOTE_OPTION)); | ||
|
||
const { addOption } = result.current; | ||
|
||
act(() => { | ||
addOption(); | ||
}); | ||
|
||
const { optionList } = result.current; | ||
|
||
expect(optionList.length).toBe(MOCK_MIN_VOTE_OPTION.length + 1); | ||
|
||
expect(optionList[2].text).toBe(''); | ||
|
||
expect(optionList[2].imageUrl).toBe(''); | ||
}); | ||
|
||
test('투표 선택지가 5개일 땐 투표 선택지를 추가할 수 없다', () => { | ||
const { result } = renderHook(() => useWritingOption(MOCK_MAX_VOTE_OPTION)); | ||
|
||
const { addOption } = result.current; | ||
|
||
act(() => { | ||
addOption(); | ||
}); | ||
|
||
const { optionList } = result.current; | ||
|
||
expect(optionList).toEqual(MOCK_MAX_VOTE_OPTION); | ||
}); | ||
|
||
test('투표 선택지가 3개 이상일때는 투표 선택지의 아이디를 이용하여 삭제할 수 있다.', () => { | ||
const { result } = renderHook(() => useWritingOption(MOCK_MAX_VOTE_OPTION)); | ||
|
||
const { deleteOption } = result.current; | ||
|
||
act(() => { | ||
deleteOption(MOCK_MAX_VOTE_OPTION[0].id); | ||
}); | ||
|
||
const { optionList } = result.current; | ||
|
||
expect(optionList).toEqual(MOCK_MAX_VOTE_OPTION.slice(1, 5)); | ||
}); | ||
|
||
test('투표 선택지가 2개일때는 삭제할 수 없다.', () => { | ||
const { result } = renderHook(() => useWritingOption(MOCK_MIN_VOTE_OPTION)); | ||
|
||
const { deleteOption } = result.current; | ||
|
||
act(() => { | ||
deleteOption(MOCK_MIN_VOTE_OPTION[0].id); | ||
}); | ||
|
||
const { optionList } = result.current; | ||
|
||
expect(optionList).toEqual(MOCK_MIN_VOTE_OPTION); | ||
}); | ||
|
||
test('선택한 이미지가 있을 때 취소할 수 있다.', () => { | ||
const MOCK_IMAGE_OPTION = [ | ||
{ id: 12341, text: '', imageUrl: 'https' }, | ||
{ id: 123412, text: '', imageUrl: 'imageUrl' }, | ||
]; | ||
|
||
const { result } = renderHook(() => useWritingOption(MOCK_IMAGE_OPTION)); | ||
|
||
const { removeImage } = result.current; | ||
|
||
act(() => { | ||
removeImage(MOCK_MIN_VOTE_OPTION[0].id); | ||
}); | ||
|
||
const { optionList } = result.current; | ||
|
||
expect(optionList[0].imageUrl).toBe(''); | ||
}); | ||
}); |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions
14
...WritingVoteOptionList/WritingVoteOption/OptionCancelButton/OptionCancelButton.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,14 @@ | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
|
||
import OptionCancelButton from '.'; | ||
|
||
const meta: Meta<typeof OptionCancelButton> = { | ||
component: OptionCancelButton, | ||
}; | ||
|
||
export default meta; | ||
type Story = StoryObj<typeof OptionCancelButton>; | ||
|
||
export const Default: Story = { | ||
render: () => <OptionCancelButton />, | ||
}; |
15 changes: 15 additions & 0 deletions
15
...omponents/optionList/WritingVoteOptionList/WritingVoteOption/OptionCancelButton/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,15 @@ | ||
import React from 'react'; | ||
|
||
import xMarkIcon from '@assets/x_mark_white.svg'; | ||
|
||
import * as S from './style'; | ||
|
||
interface OptionCancelButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {} | ||
|
||
export default function OptionCancelButton({ ...rest }: OptionCancelButtonProps) { | ||
return ( | ||
<S.Container aria-label="삭제" type="button" {...rest}> | ||
<S.Image src={xMarkIcon} alt="" /> | ||
</S.Container> | ||
); | ||
} |
9 changes: 9 additions & 0 deletions
9
...components/optionList/WritingVoteOptionList/WritingVoteOption/OptionCancelButton/style.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,9 @@ | ||
import { styled } from 'styled-components'; | ||
|
||
import { ButtonCssText, IconImage } from '../style'; | ||
|
||
export const Container = styled.button` | ||
${ButtonCssText} | ||
`; | ||
|
||
export const Image = styled(IconImage)``; |
14 changes: 14 additions & 0 deletions
14
...eOptionList/WritingVoteOption/OptionUploadImageButton/OptionUploadImageButton.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,14 @@ | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
|
||
import OptionUploadImageButton from '.'; | ||
|
||
const meta: Meta<typeof OptionUploadImageButton> = { | ||
component: OptionUploadImageButton, | ||
}; | ||
|
||
export default meta; | ||
type Story = StoryObj<typeof OptionUploadImageButton>; | ||
|
||
export const Default: Story = { | ||
render: () => <OptionUploadImageButton labelId={123} />, | ||
}; |
25 changes: 25 additions & 0 deletions
25
...ents/optionList/WritingVoteOptionList/WritingVoteOption/OptionUploadImageButton/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,25 @@ | ||
import React from 'react'; | ||
|
||
import photoIcon from '@assets/photo_white.svg'; | ||
|
||
import * as S from './style'; | ||
|
||
interface OptionUploadImageButtonProps extends React.InputHTMLAttributes<HTMLInputElement> { | ||
optionId: number; | ||
} | ||
|
||
export default function OptionUploadImageButton({ | ||
optionId, | ||
...rest | ||
}: OptionUploadImageButtonProps) { | ||
const id = optionId.toString(); | ||
|
||
return ( | ||
<S.Container> | ||
<S.Label htmlFor={id} aria-label="선택지 이미지 업로드 버튼" title="이미지 업로드"> | ||
<S.Image src={photoIcon} alt="" /> | ||
</S.Label> | ||
<S.FileInput id={id} type="file" accept="image/*" {...rest} /> | ||
</S.Container> | ||
); | ||
} |
19 changes: 19 additions & 0 deletions
19
...nents/optionList/WritingVoteOptionList/WritingVoteOption/OptionUploadImageButton/style.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,19 @@ | ||
import { styled } from 'styled-components'; | ||
|
||
import { ButtonCssText, IconImage } from '../style'; | ||
|
||
export const Container = styled.div` | ||
width: 24px; | ||
height: 24px; | ||
border-radius: 50%; | ||
`; | ||
|
||
export const Label = styled.label` | ||
${ButtonCssText} | ||
`; | ||
|
||
export const FileInput = styled.input` | ||
visibility: hidden; | ||
`; | ||
|
||
export const Image = styled(IconImage)``; |
53 changes: 53 additions & 0 deletions
53
...mponents/optionList/WritingVoteOptionList/WritingVoteOption/WritingVoteOption.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,53 @@ | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
|
||
import WritingVoteOption from '.'; | ||
|
||
const meta: Meta<typeof WritingVoteOption> = { | ||
component: WritingVoteOption, | ||
}; | ||
|
||
export default meta; | ||
type Story = StoryObj<typeof WritingVoteOption>; | ||
|
||
export const IsDeletable: Story = { | ||
render: () => ( | ||
<WritingVoteOption | ||
handleDeleteOptionClick={() => {}} | ||
handleRemoveImageClick={() => {}} | ||
handleUploadImage={() => {}} | ||
optionId={Math.floor(Math.random() * 100000)} | ||
text="방학 때 강릉으로 강아지와 기차여행을 하려했지 | ||
만 장마가 와서 취소했어요. 여행을 별로 좋" | ||
isDeletable={true} | ||
/> | ||
), | ||
}; | ||
|
||
export const IsNotDeletable: Story = { | ||
render: () => ( | ||
<WritingVoteOption | ||
handleDeleteOptionClick={() => {}} | ||
handleRemoveImageClick={() => {}} | ||
handleUploadImage={() => {}} | ||
optionId={Math.floor(Math.random() * 100000)} | ||
text="방학 때 강릉으로 강아지와 기차여행을 하려했지 | ||
만 장마가 와서 취소했어요. 여행을 별로 좋" | ||
isDeletable={false} | ||
/> | ||
), | ||
}; | ||
|
||
export const ShowImage: Story = { | ||
render: () => ( | ||
<WritingVoteOption | ||
handleDeleteOptionClick={() => {}} | ||
handleRemoveImageClick={() => {}} | ||
handleUploadImage={() => {}} | ||
optionId={Math.floor(Math.random() * 100000)} | ||
text="방학 때 강릉으로 강아지와 기차여행을 하려했지 | ||
만 장마가 와서 취소했어요. 여행을 별로 좋" | ||
isDeletable={true} | ||
imageUrl="https://source.unsplash.com/random" | ||
/> | ||
), | ||
}; |
71 changes: 71 additions & 0 deletions
71
frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/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,71 @@ | ||
import React, { ChangeEvent } from 'react'; | ||
|
||
import OptionCancelButton from './OptionCancelButton'; | ||
import OptionUploadImageButton from './OptionUploadImageButton'; | ||
import * as S from './style'; | ||
|
||
interface WritingVoteOptionProps { | ||
optionId: number; | ||
text: string; | ||
isDeletable: boolean; | ||
handleDeleteOptionClick: () => void; | ||
handleRemoveImageClick: () => void; | ||
handleUploadImage: (event: React.ChangeEvent<HTMLInputElement>) => void; | ||
imageUrl?: string; | ||
} | ||
|
||
const MAX_WRITING_LENGTH = 50; | ||
|
||
export default function WritingVoteOption({ | ||
optionId, | ||
text, | ||
isDeletable, | ||
handleDeleteOptionClick, | ||
handleRemoveImageClick, | ||
handleUploadImage, | ||
imageUrl, | ||
}: WritingVoteOptionProps) { | ||
const handleTextChange = (event: ChangeEvent<HTMLTextAreaElement>) => { | ||
const { value } = event.target; | ||
const standard = value.length; | ||
|
||
if (standard === MAX_WRITING_LENGTH) { | ||
event.target.setCustomValidity(`선택지 내용은 ${MAX_WRITING_LENGTH}자까지 입력 가능합니다.`); | ||
event.target.reportValidity(); | ||
return; | ||
} | ||
|
||
event.target.setCustomValidity(''); | ||
}; | ||
|
||
return ( | ||
<S.Container> | ||
<S.CancelButtonWrapper> | ||
{isDeletable && ( | ||
<OptionCancelButton title="선택지 삭제하기" onClick={handleDeleteOptionClick} /> | ||
)} | ||
</S.CancelButtonWrapper> | ||
<S.OptionContainer> | ||
<S.ContentContainer> | ||
<S.ContentTextArea | ||
defaultValue={text} | ||
onChange={handleTextChange} | ||
placeholder="내용을 입력해주세요." | ||
maxLength={MAX_WRITING_LENGTH} | ||
/> | ||
{!imageUrl && ( | ||
<OptionUploadImageButton optionId={optionId} onChange={handleUploadImage} /> | ||
)} | ||
</S.ContentContainer> | ||
{imageUrl && ( | ||
<S.ImageContainer> | ||
<S.Image src={imageUrl} alt={text} /> | ||
<S.ImageCancelWrapper> | ||
<OptionCancelButton onClick={handleRemoveImageClick} /> | ||
</S.ImageCancelWrapper> | ||
</S.ImageContainer> | ||
)} | ||
</S.OptionContainer> | ||
</S.Container> | ||
); | ||
} |
Oops, something went wrong.