From 9051e0ea00c1047b451829320f1d7f558be55253 Mon Sep 17 00:00:00 2001 From: Rulu Date: Sun, 29 Dec 2024 16:38:47 +0900 Subject: [PATCH 01/20] =?UTF-8?q?refactor:=20=EC=BA=98=EB=A6=B0=EB=8D=94?= =?UTF-8?q?=20=EB=93=B1=EB=A1=9D,=20=EC=88=98=EC=A0=95=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20=EC=82=AC=EC=9D=B4=EC=A6=88=20=EC=A4=84=EC=9D=B4?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ScheduleAddModal.styled.ts | 27 +++++++------ .../ScheduleAddModal/ScheduleAddModal.tsx | 18 +++++---- .../ScheduleEditModal.styled.ts | 39 +++++++++++-------- .../ScheduleEditModal/ScheduleEditModal.tsx | 18 ++++----- .../TimeTableMenu/TimeTableMenu.styled.ts | 3 +- 5 files changed, 57 insertions(+), 48 deletions(-) diff --git a/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.styled.ts b/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.styled.ts index 2b1583a0b..99f0020d3 100644 --- a/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.styled.ts +++ b/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.styled.ts @@ -39,9 +39,9 @@ export const Container = styled.div<{ `; return css` - width: 400px; - min-height: 320px; - padding: 18px 22px; + width: 380px; + min-height: 300px; + padding: 16px 20px; `; }} @@ -56,7 +56,7 @@ export const Container = styled.div<{ display: flex; flex-direction: column; - row-gap: ${({ $isMobile }) => ($isMobile ? '10px' : '16px')}; + row-gap: ${({ $isMobile }) => ($isMobile ? '10px' : '10px')}; } `; @@ -65,7 +65,7 @@ export const Header = styled.div` justify-content: flex-end; width: 100%; - height: 34px; + height: 30px; margin-bottom: 18px; border-bottom: ${({ theme }) => `1px solid ${theme.color.GRAY300}`}; @@ -94,6 +94,7 @@ export const CheckboxContainer = styled.div` export const TimeSelectContainer = styled.div<{ $isMobile: boolean }>` display: flex; + justify-content: space-between; width: 100%; height: ${({ $isMobile }) => ($isMobile ? '74px' : '40px')}; @@ -116,9 +117,7 @@ export const InputWrapper = styled.div<{ $isMobile: boolean }>` align-items: center; justify-content: space-between; - width: ${({ $isMobile }) => !$isMobile && 'calc(100% - 80px)'}; - - margin-left: ${({ $isMobile }) => !$isMobile && 'auto'}; + width: ${({ $isMobile }) => !$isMobile && 'calc(100% - 70px)'}; `; export const TeamNameContainer = styled.div` @@ -145,18 +144,18 @@ export const title = css` border-radius: 10px; background-color: ${({ theme }) => theme.color.GRAY200}; - font-size: 18px; + font-size: 16px; `; export const closeButton = css` - width: 28px; - height: 28px; + width: 24px; + height: 24px; padding: 0; margin-bottom: 4px; svg { - width: 28px; - height: 28px; + width: 24px; + height: 24px; } `; @@ -184,6 +183,6 @@ export const teamPlaceName = css` `; export const submitButton = css` - width: 80px; + width: 76px; padding: 0; `; diff --git a/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.tsx b/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.tsx index 54de44fa4..3214d2371 100644 --- a/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.tsx +++ b/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.tsx @@ -76,13 +76,11 @@ const ScheduleAddModal = (props: ScheduleAddModalProps) => { - - 일정 시작 - + 일정 시작 { - + 일정 마감 { - - {!isMobile && {displayName}} + + {!isMobile && ( + + {displayName} + + )} - - - ); -}; - -const images = [ - { - id: 9283, - isExpired: false, - name: 'neon.png', - url: 'https://images.unsplash.com/photo-1508700115892-45ecd05ae2ad?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2069&q=80', - }, - { - id: 4165, - isExpired: false, - name: 'donut.png', - url: 'https://images.unsplash.com/photo-1551106652-a5bcf4b29ab6?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1965&q=80', - }, - { - id: 8729, - isExpired: false, - name: 'zXwMd93Xwz2V03M5xAw_fVmxzEwNiDv_93-xVm__902XvC-2XzOqPdR93F3Xz_24RzV01IjSwmOkVeZmIoPlLliFmMVc2__s9Xz.png', - url: 'https://images.unsplash.com/photo-1591382386627-349b692688ff?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1887&q=80', - }, - { - id: 1092, - isExpired: false, - name: 'icon.png', - url: 'https://img.icons8.com/?size=256&id=VUoFEYkLOaMn&format=png&color=1A6DFF,C822FF', - }, - { - id: 3493, - isExpired: true, - name: '만료된 사진', - url: '', - }, -]; - -export const Default: Story = { - render: () => , - args: { - images: [], - initialPage: 1, - }, -}; +import type { Meta, StoryObj } from '@storybook/react'; +import ThreadImageModal from '~/components/feed/ThreadImageModal/ThreadImageModal'; +import { useModal } from '~/hooks/useModal'; +import Button from '~/components/common/Button/Button'; + +const meta = { + title: 'feed/ThreadImageModal', + component: ThreadImageModal, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const SampleModal = () => { + const { openModal } = useModal(); + + return ( + <> + + + + ); +}; + +const images = [ + { + id: 9283, + isExpired: false, + name: 'neon.png', + url: 'https://images.unsplash.com/photo-1508700115892-45ecd05ae2ad?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2069&q=80', + }, + { + id: 4165, + isExpired: false, + name: 'donut.png', + url: 'https://images.unsplash.com/photo-1551106652-a5bcf4b29ab6?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1965&q=80', + }, + { + id: 8729, + isExpired: false, + name: 'zXwMd93Xwz2V03M5xAw_fVmxzEwNiDv_93-xVm__902XvC-2XzOqPdR93F3Xz_24RzV01IjSwmOkVeZmIoPlLliFmMVc2__s9Xz.png', + url: 'https://images.unsplash.com/photo-1591382386627-349b692688ff?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1887&q=80', + }, + { + id: 1092, + isExpired: false, + name: 'icon.png', + url: 'https://img.icons8.com/?size=256&id=VUoFEYkLOaMn&format=png&color=1A6DFF,C822FF', + }, + { + id: 3493, + isExpired: true, + name: '만료된 사진', + url: '', + }, +]; + +export const Default: Story = { + render: () => , + args: { + images: [], + initialPage: 1, + }, +}; diff --git a/frontend/src/components/feed/ThumbnailList/ThumbnailList.stories.tsx b/frontend/src/components/feed/ThumbnailList/ThumbnailList.stories.tsx index 50f0ea914..2061dbb9a 100644 --- a/frontend/src/components/feed/ThumbnailList/ThumbnailList.stories.tsx +++ b/frontend/src/components/feed/ThumbnailList/ThumbnailList.stories.tsx @@ -1,158 +1,158 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import type { PreviewImage, ThreadImage } from '~/types/feed'; -import ThumbnailList from './ThumbnailList'; - -/** - * `ThumbnailList` 은 이미지 서랍, 또는 채팅에서 사용할 수 있는 썸네일 모음집입니다. - */ -const meta = { - title: 'feed/ThumbnailList', - component: ThumbnailList, - tags: ['autodocs'], - argTypes: { - mode: { - description: - '썸네일 리스트를 어떤 용도로 사용할 지를 정할 수 있습니다. `delete`일 경우 리스트의 이미지들을 삭제할 수 있으며, `view`일 경우 리스트의 썸네일을 클릭하여 모달에 이미지를 띄울 수 있습니다.', - }, - images: { - description: - '썸네일 리스트를 보여주기 위해 사용할 이미지들의 정보입니다.', - }, - onDelete: { - description: - "**`mode = 'delete'` 일때만 필요합니다.** 이미지가 클릭되었을 때 이미지를 지우는 함수를 의미합니다.", - }, - onClick: { - description: - "**`mode = 'view'` 일때만 필요합니다.** 이미지가 클릭되었을 때 모달을 띄우는 함수를 의미합니다.", - }, - }, -} satisfies Meta; - -export default meta; - -type Story = StoryObj; - -const deleteModeImages: PreviewImage[] = [ - { - uuid: '69aaaf99-a02d-4800-a175-7314c64e2a84', - url: 'https://images.unsplash.com/photo-1508700115892-45ecd05ae2ad?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2069&q=80', - }, - { - uuid: 'aaf9a0de-8289-455e-8112-37eebc42944a', - url: 'https://images.unsplash.com/photo-1551106652-a5bcf4b29ab6?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1965&q=80', - }, - { - uuid: 'ac49b5ed-11f4-468b-b278-5880fcf7bf16', - url: 'https://images.unsplash.com/photo-1591382386627-349b692688ff?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1887&q=80', - }, - { - uuid: '3e658b3c-5664-4225-b94a-25e6cece4ac5', - url: 'https://img.icons8.com/?size=256&id=VUoFEYkLOaMn&format=png&color=1A6DFF,C822FF', - }, -]; - -const viewModeImages: ThreadImage[] = [ - { - id: 9283, - isExpired: false, - name: 'neon.png', - url: 'https://images.unsplash.com/photo-1508700115892-45ecd05ae2ad?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2069&q=80', - }, - { - id: 4165, - isExpired: false, - name: 'donut.png', - url: 'https://images.unsplash.com/photo-1551106652-a5bcf4b29ab6?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1965&q=80', - }, - { - id: 8729, - isExpired: false, - name: 'zXwMd93Xwz2V03M5xAw_fVmxzEwNiDv_93-xVm__902XvC-2XzOqPdR93F3Xz_24RzV01IjSwmOkVeZmIoPlLliFmMVc2__s9Xz.png', - url: 'https://images.unsplash.com/photo-1591382386627-349b692688ff?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1887&q=80', - }, - { - id: 1092, - isExpired: false, - name: 'icon.png', - url: 'https://img.icons8.com/?size=256&id=VUoFEYkLOaMn&format=png&color=1A6DFF,C822FF', - }, - { - id: 3493, - isExpired: true, - name: '만료된 사진', - url: '', - }, -]; - -/** - * 이미지 서랍에 사용할 경우 이 옵션을 사용하세요. `mode = 'delete'` 로 설정하시면 됩니다. - */ -export const DeletableList: Story = { - args: { - mode: 'delete', - images: [], - onDelete: (imageUuid) => { - alert(`onDelete(${imageUuid});`); - }, - onChange: () => { - alert(`onChange()`); - }, - isUploading: false, - }, -}; - -/** - * `mode = delete`이고, 이미지 개수가 최대로 올릴 수 있는 이미지 개수를 넘지 않았다면, 이미지 추가 버튼이 보이게 됩니다. - */ -export const NotMaxDeletableList: Story = { - args: { - mode: 'delete', - images: deleteModeImages.slice(0, 2), - onDelete: (imageUuid) => { - alert(`onDelete(${imageUuid});`); - }, - onChange: () => { - alert(`onChange()`); - }, - isUploading: false, - }, -}; - -export const EmptyDeletableList: Story = { - args: { - mode: 'delete', - images: [], - onDelete: (imageUuid) => { - alert(`onDelete(${imageUuid});`); - }, - onChange: () => { - alert(`onChange()`); - }, - isUploading: false, - }, -}; - -/** - * 채팅 메시지에 사용할 경우 이 옵션을 사용하세요. `mode = 'view'` 로 설정하시면 됩니다. - */ -export const ViewableList: Story = { - args: { - mode: 'view', - images: viewModeImages, - onClick: (images: ThreadImage[], selectedImage: number) => { - alert(`onClick(${JSON.stringify(images)}, ${selectedImage});`); - }, - }, -}; - -export const ViewableListSmall: Story = { - args: { - mode: 'view', - size: 'sm', - images: viewModeImages, - onClick: (images: ThreadImage[], selectedImage: number) => { - alert(`onClick(${JSON.stringify(images)}, ${selectedImage});`); - }, - }, -}; +import type { Meta, StoryObj } from '@storybook/react'; +import type { PreviewImage, ThreadImage } from '~/types/feed'; +import ThumbnailList from './ThumbnailList'; + +/** + * `ThumbnailList` 은 이미지 서랍, 또는 채팅에서 사용할 수 있는 썸네일 모음집입니다. + */ +const meta = { + title: 'feed/ThumbnailList', + component: ThumbnailList, + tags: ['autodocs'], + argTypes: { + mode: { + description: + '썸네일 리스트를 어떤 용도로 사용할 지를 정할 수 있습니다. `delete`일 경우 리스트의 이미지들을 삭제할 수 있으며, `view`일 경우 리스트의 썸네일을 클릭하여 모달에 이미지를 띄울 수 있습니다.', + }, + images: { + description: + '썸네일 리스트를 보여주기 위해 사용할 이미지들의 정보입니다.', + }, + onDelete: { + description: + "**`mode = 'delete'` 일때만 필요합니다.** 이미지가 클릭되었을 때 이미지를 지우는 함수를 의미합니다.", + }, + onClick: { + description: + "**`mode = 'view'` 일때만 필요합니다.** 이미지가 클릭되었을 때 모달을 띄우는 함수를 의미합니다.", + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +const deleteModeImages: PreviewImage[] = [ + { + uuid: '69aaaf99-a02d-4800-a175-7314c64e2a84', + url: 'https://images.unsplash.com/photo-1508700115892-45ecd05ae2ad?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2069&q=80', + }, + { + uuid: 'aaf9a0de-8289-455e-8112-37eebc42944a', + url: 'https://images.unsplash.com/photo-1551106652-a5bcf4b29ab6?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1965&q=80', + }, + { + uuid: 'ac49b5ed-11f4-468b-b278-5880fcf7bf16', + url: 'https://images.unsplash.com/photo-1591382386627-349b692688ff?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1887&q=80', + }, + { + uuid: '3e658b3c-5664-4225-b94a-25e6cece4ac5', + url: 'https://img.icons8.com/?size=256&id=VUoFEYkLOaMn&format=png&color=1A6DFF,C822FF', + }, +]; + +const viewModeImages: ThreadImage[] = [ + { + id: 9283, + isExpired: false, + name: 'neon.png', + url: 'https://images.unsplash.com/photo-1508700115892-45ecd05ae2ad?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2069&q=80', + }, + { + id: 4165, + isExpired: false, + name: 'donut.png', + url: 'https://images.unsplash.com/photo-1551106652-a5bcf4b29ab6?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1965&q=80', + }, + { + id: 8729, + isExpired: false, + name: 'zXwMd93Xwz2V03M5xAw_fVmxzEwNiDv_93-xVm__902XvC-2XzOqPdR93F3Xz_24RzV01IjSwmOkVeZmIoPlLliFmMVc2__s9Xz.png', + url: 'https://images.unsplash.com/photo-1591382386627-349b692688ff?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1887&q=80', + }, + { + id: 1092, + isExpired: false, + name: 'icon.png', + url: 'https://img.icons8.com/?size=256&id=VUoFEYkLOaMn&format=png&color=1A6DFF,C822FF', + }, + { + id: 3493, + isExpired: true, + name: '만료된 사진', + url: '', + }, +]; + +/** + * 이미지 서랍에 사용할 경우 이 옵션을 사용하세요. `mode = 'delete'` 로 설정하시면 됩니다. + */ +export const DeletableList: Story = { + args: { + mode: 'delete', + images: [], + onDelete: (imageUuid) => { + alert(`onDelete(${imageUuid});`); + }, + onChange: () => { + alert(`onChange()`); + }, + isUploading: false, + }, +}; + +/** + * `mode = delete`이고, 이미지 개수가 최대로 올릴 수 있는 이미지 개수를 넘지 않았다면, 이미지 추가 버튼이 보이게 됩니다. + */ +export const NotMaxDeletableList: Story = { + args: { + mode: 'delete', + images: deleteModeImages.slice(0, 2), + onDelete: (imageUuid) => { + alert(`onDelete(${imageUuid});`); + }, + onChange: () => { + alert(`onChange()`); + }, + isUploading: false, + }, +}; + +export const EmptyDeletableList: Story = { + args: { + mode: 'delete', + images: [], + onDelete: (imageUuid) => { + alert(`onDelete(${imageUuid});`); + }, + onChange: () => { + alert(`onChange()`); + }, + isUploading: false, + }, +}; + +/** + * 채팅 메시지에 사용할 경우 이 옵션을 사용하세요. `mode = 'view'` 로 설정하시면 됩니다. + */ +export const ViewableList: Story = { + args: { + mode: 'view', + images: viewModeImages, + onClick: (images: ThreadImage[], selectedImage: number) => { + alert(`onClick(${JSON.stringify(images)}, ${selectedImage});`); + }, + }, +}; + +export const ViewableListSmall: Story = { + args: { + mode: 'view', + size: 'sm', + images: viewModeImages, + onClick: (images: ThreadImage[], selectedImage: number) => { + alert(`onClick(${JSON.stringify(images)}, ${selectedImage});`); + }, + }, +}; diff --git a/frontend/src/components/feed/ViewableThumbnail/ViewableThumbnail.stories.tsx b/frontend/src/components/feed/ViewableThumbnail/ViewableThumbnail.stories.tsx index 62627e2c3..298677c30 100644 --- a/frontend/src/components/feed/ViewableThumbnail/ViewableThumbnail.stories.tsx +++ b/frontend/src/components/feed/ViewableThumbnail/ViewableThumbnail.stories.tsx @@ -1,67 +1,67 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import ViewableThumbnail from './ViewableThumbnail'; - -/** - * `ViewableThumbnail` 은 이미지 업로드 서랍에서 사용할 수 있는 단일 이미지 썸네일 컴포넌트입니다. 이미지를 클릭할 경우 모달을 띄우기 위한 함수를 호출합니다. - */ -const meta = { - title: 'feed/ViewableThumbnail', - component: ViewableThumbnail, - tags: ['autodocs'], - argTypes: { - image: { - description: '썸네일로 보여줄 이미지의 정보', - }, - onClick: { - description: - '썸네일에 해당하는 이미지가 클릭되었을 때, 해당 썸네일을 이미지 모달에 띄우기 위한 함수', - }, - }, -} satisfies Meta; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = { - args: { - image: { - id: 2918, - isExpired: false, - name: 'rabbit.png', - url: 'https://images.unsplash.com/photo-1599169713100-120531cef331?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1887&q=80', - }, - onClick: () => { - alert('onClick()'); - }, - }, -}; - -export const Small: Story = { - args: { - image: { - id: 1145, - isExpired: false, - name: 'rabbit.png', - url: 'https://images.unsplash.com/photo-1599169713100-120531cef331?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1887&q=80', - }, - size: 'sm', - onClick: () => { - alert('onClick()'); - }, - }, -}; - -export const ExpiredThumbnail: Story = { - args: { - image: { - id: 1029, - isExpired: true, - name: '만료된 이미지', - url: '', - }, - onClick: () => { - alert('onClick()'); - }, - }, -}; +import type { Meta, StoryObj } from '@storybook/react'; +import ViewableThumbnail from './ViewableThumbnail'; + +/** + * `ViewableThumbnail` 은 이미지 업로드 서랍에서 사용할 수 있는 단일 이미지 썸네일 컴포넌트입니다. 이미지를 클릭할 경우 모달을 띄우기 위한 함수를 호출합니다. + */ +const meta = { + title: 'feed/ViewableThumbnail', + component: ViewableThumbnail, + tags: ['autodocs'], + argTypes: { + image: { + description: '썸네일로 보여줄 이미지의 정보', + }, + onClick: { + description: + '썸네일에 해당하는 이미지가 클릭되었을 때, 해당 썸네일을 이미지 모달에 띄우기 위한 함수', + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + image: { + id: 2918, + isExpired: false, + name: 'rabbit.png', + url: 'https://images.unsplash.com/photo-1599169713100-120531cef331?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1887&q=80', + }, + onClick: () => { + alert('onClick()'); + }, + }, +}; + +export const Small: Story = { + args: { + image: { + id: 1145, + isExpired: false, + name: 'rabbit.png', + url: 'https://images.unsplash.com/photo-1599169713100-120531cef331?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1887&q=80', + }, + size: 'sm', + onClick: () => { + alert('onClick()'); + }, + }, +}; + +export const ExpiredThumbnail: Story = { + args: { + image: { + id: 1029, + isExpired: true, + name: '만료된 이미지', + url: '', + }, + onClick: () => { + alert('onClick()'); + }, + }, +}; diff --git a/frontend/src/components/landing/FeedDecoration/FeedDecoration.stories.tsx b/frontend/src/components/landing/FeedDecoration/FeedDecoration.stories.tsx index b9f7ca4ca..f52862b5b 100644 --- a/frontend/src/components/landing/FeedDecoration/FeedDecoration.stories.tsx +++ b/frontend/src/components/landing/FeedDecoration/FeedDecoration.stories.tsx @@ -1,40 +1,40 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import FeedDecoration from './FeedDecoration'; - -/** - * `FeedDecoration` 컴포넌트는 랜딩 페이지의 장식 컴포넌트인 `IntroCardPile` 의 두 번째 장면 해당하는 컴포넌트입니다. - * **팀 피드**에 대한 모형을 애니메이션과 함께 보여줍니다. - */ -const meta = { - title: 'landing/FeedDecoration', - component: FeedDecoration, - tags: ['autodocs'], - decorators: [ - (Story) => { - return ( -
- -
- ); - }, - ], -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -/** - * 하늘색의 컨테이너는 본 컴포넌트에 포함되지 않습니다. - */ -export const Default: Story = { - args: {}, -}; +import type { Meta, StoryObj } from '@storybook/react'; +import FeedDecoration from './FeedDecoration'; + +/** + * `FeedDecoration` 컴포넌트는 랜딩 페이지의 장식 컴포넌트인 `IntroCardPile` 의 두 번째 장면 해당하는 컴포넌트입니다. + * **팀 피드**에 대한 모형을 애니메이션과 함께 보여줍니다. + */ +const meta = { + title: 'landing/FeedDecoration', + component: FeedDecoration, + tags: ['autodocs'], + decorators: [ + (Story) => { + return ( +
+ +
+ ); + }, + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +/** + * 하늘색의 컨테이너는 본 컴포넌트에 포함되지 않습니다. + */ +export const Default: Story = { + args: {}, +}; diff --git a/frontend/src/components/landing/FileDriveDecoration/FileDriveDecoration.stories.tsx b/frontend/src/components/landing/FileDriveDecoration/FileDriveDecoration.stories.tsx index b45edf3ae..0d6a99e97 100644 --- a/frontend/src/components/landing/FileDriveDecoration/FileDriveDecoration.stories.tsx +++ b/frontend/src/components/landing/FileDriveDecoration/FileDriveDecoration.stories.tsx @@ -1,41 +1,41 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import FileDriveDecoration from './FileDriveDecoration'; - -/** - * `FileDriveDecoration` 컴포넌트는 랜딩 페이지의 장식 컴포넌트인 `IntroCardPile` 의 세 번째 장면 해당하는 컴포넌트입니다. - * **팀 드라이브**에 대한 모형을 애니메이션과 함께 보여줍니다. - * 이 컴포넌트를 작성하는 시점에서, 팀 드라이브의 UI는 구상이 되어 있지 않았기에, 추후 구상이 완료될 경우 이 UI는 바뀔 수도 있습니다. - */ -const meta = { - title: 'landing/FileDriveDecoration', - component: FileDriveDecoration, - tags: ['autodocs'], - decorators: [ - (Story) => { - return ( -
- -
- ); - }, - ], -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -/** - * 하늘색의 컨테이너는 본 컴포넌트에 포함되지 않습니다. - */ -export const Default: Story = { - args: {}, -}; +import type { Meta, StoryObj } from '@storybook/react'; +import FileDriveDecoration from './FileDriveDecoration'; + +/** + * `FileDriveDecoration` 컴포넌트는 랜딩 페이지의 장식 컴포넌트인 `IntroCardPile` 의 세 번째 장면 해당하는 컴포넌트입니다. + * **팀 드라이브**에 대한 모형을 애니메이션과 함께 보여줍니다. + * 이 컴포넌트를 작성하는 시점에서, 팀 드라이브의 UI는 구상이 되어 있지 않았기에, 추후 구상이 완료될 경우 이 UI는 바뀔 수도 있습니다. + */ +const meta = { + title: 'landing/FileDriveDecoration', + component: FileDriveDecoration, + tags: ['autodocs'], + decorators: [ + (Story) => { + return ( +
+ +
+ ); + }, + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +/** + * 하늘색의 컨테이너는 본 컴포넌트에 포함되지 않습니다. + */ +export const Default: Story = { + args: {}, +}; diff --git a/frontend/src/components/landing/IntroCardPile/IntroCardPile.stories.tsx b/frontend/src/components/landing/IntroCardPile/IntroCardPile.stories.tsx index c14e36a26..b72afd1af 100644 --- a/frontend/src/components/landing/IntroCardPile/IntroCardPile.stories.tsx +++ b/frontend/src/components/landing/IntroCardPile/IntroCardPile.stories.tsx @@ -1,31 +1,31 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import IntroCardPile from './IntroCardPile'; - -/** - * `IntroCardPile` 컴포넌트는 랜딩 페이지의 부속품에 해당하는 컴포넌트로, - * 여러 장의 카드를 이용하여 팀바팀 서비스의 간략화된 UI를 미리 보여줍니다. - * 랜딩 페이지의 왼쪽에 배치하여 메인 디자인 요소로 사용될 것입니다. - */ -const meta = { - title: 'landing/IntroCardPile', - component: IntroCardPile, - tags: ['autodocs'], -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - args: {}, -}; - -/** - * 이 옵션은 랜딩 페이지와 동일한 배경을 보여주어야 하지만 애니메이션을 이용하여 사용자의 시선을 끌기에는 적합하지 않은 페이지에 적합합니다. - * - * 예를 들면, 팀 초대 링크를 입력하는 페이지가 있습니다. - */ -export const NoAnimation: Story = { - args: { - animation: false, - }, -}; +import type { Meta, StoryObj } from '@storybook/react'; +import IntroCardPile from './IntroCardPile'; + +/** + * `IntroCardPile` 컴포넌트는 랜딩 페이지의 부속품에 해당하는 컴포넌트로, + * 여러 장의 카드를 이용하여 팀바팀 서비스의 간략화된 UI를 미리 보여줍니다. + * 랜딩 페이지의 왼쪽에 배치하여 메인 디자인 요소로 사용될 것입니다. + */ +const meta = { + title: 'landing/IntroCardPile', + component: IntroCardPile, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: {}, +}; + +/** + * 이 옵션은 랜딩 페이지와 동일한 배경을 보여주어야 하지만 애니메이션을 이용하여 사용자의 시선을 끌기에는 적합하지 않은 페이지에 적합합니다. + * + * 예를 들면, 팀 초대 링크를 입력하는 페이지가 있습니다. + */ +export const NoAnimation: Story = { + args: { + animation: false, + }, +}; diff --git a/frontend/src/components/landing/TeamCalendarDecoration/TeamCalendarDecoration.stories.tsx b/frontend/src/components/landing/TeamCalendarDecoration/TeamCalendarDecoration.stories.tsx index 4e32191ef..7f554135f 100644 --- a/frontend/src/components/landing/TeamCalendarDecoration/TeamCalendarDecoration.stories.tsx +++ b/frontend/src/components/landing/TeamCalendarDecoration/TeamCalendarDecoration.stories.tsx @@ -1,53 +1,53 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import TeamCalendarDecoration from './TeamCalendarDecoration'; - -/** - * `TeamCalendarDecoration` 컴포넌트는 랜딩 페이지의 장식 컴포넌트인 `IntroCardPile` 의 첫 번째 장면 해당하는 컴포넌트입니다. - * **팀 캘린더**에 대한 모형을 애니메이션과 함께 보여줍니다. - */ -const meta = { - title: 'landing/TeamCalendarDecoration', - component: TeamCalendarDecoration, - tags: ['autodocs'], - decorators: [ - (Story) => { - return ( -
- -
- ); - }, - ], -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -/** - * 하늘색의 컨테이너는 본 컴포넌트에 포함되지 않습니다. - */ -export const Default: Story = { - args: {}, -}; - -/** - * 이 옵션은 이 컴포넌트가 주목을 끌어서는 안 되는 페이지에 사용하기에 적합합니다. - * 랜딩 페이지를 제외한 페이지에서는 이 옵션이 사용될 것입니다. - * - * 참고로, 다른 `IntroCardPile` 의 장면들의 경우 이 옵션이 없는데, - * 이는 `IntroCardPile` 에서 애니메이션을 보여주지 않는 옵션이 켜졌을 경우 다른 장면들은 랜더링될 일이 없기 때문입니다. - */ -export const NoAnimation: Story = { - args: { - animation: false, - }, -}; +import type { Meta, StoryObj } from '@storybook/react'; +import TeamCalendarDecoration from './TeamCalendarDecoration'; + +/** + * `TeamCalendarDecoration` 컴포넌트는 랜딩 페이지의 장식 컴포넌트인 `IntroCardPile` 의 첫 번째 장면 해당하는 컴포넌트입니다. + * **팀 캘린더**에 대한 모형을 애니메이션과 함께 보여줍니다. + */ +const meta = { + title: 'landing/TeamCalendarDecoration', + component: TeamCalendarDecoration, + tags: ['autodocs'], + decorators: [ + (Story) => { + return ( +
+ +
+ ); + }, + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +/** + * 하늘색의 컨테이너는 본 컴포넌트에 포함되지 않습니다. + */ +export const Default: Story = { + args: {}, +}; + +/** + * 이 옵션은 이 컴포넌트가 주목을 끌어서는 안 되는 페이지에 사용하기에 적합합니다. + * 랜딩 페이지를 제외한 페이지에서는 이 옵션이 사용될 것입니다. + * + * 참고로, 다른 `IntroCardPile` 의 장면들의 경우 이 옵션이 없는데, + * 이는 `IntroCardPile` 에서 애니메이션을 보여주지 않는 옵션이 켜졌을 경우 다른 장면들은 랜더링될 일이 없기 때문입니다. + */ +export const NoAnimation: Story = { + args: { + animation: false, + }, +}; diff --git a/frontend/src/components/link/EmptyLinkPlaceholder/EmptyLinkPlaceholder.stories.tsx b/frontend/src/components/link/EmptyLinkPlaceholder/EmptyLinkPlaceholder.stories.tsx index f8fbafe17..797c8fec6 100644 --- a/frontend/src/components/link/EmptyLinkPlaceholder/EmptyLinkPlaceholder.stories.tsx +++ b/frontend/src/components/link/EmptyLinkPlaceholder/EmptyLinkPlaceholder.stories.tsx @@ -1,19 +1,19 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import EmptyLinkPlaceholder from './EmptyLinkPlaceholder'; - -/** - * `EmptyLinkPlaceholder` 는 `LinkTable` 컴포넌트에 있는 링크가 하나도 없을 경우, 대신 보여줄 화면을 구성하는 컴포넌트입니다. - */ -const meta = { - title: 'Link/EmptyLinkPlaceholder', - component: EmptyLinkPlaceholder, - tags: ['autodocs'], -} satisfies Meta; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = { - args: {}, -}; +import type { Meta, StoryObj } from '@storybook/react'; +import EmptyLinkPlaceholder from './EmptyLinkPlaceholder'; + +/** + * `EmptyLinkPlaceholder` 는 `LinkTable` 컴포넌트에 있는 링크가 하나도 없을 경우, 대신 보여줄 화면을 구성하는 컴포넌트입니다. + */ +const meta = { + title: 'Link/EmptyLinkPlaceholder', + component: EmptyLinkPlaceholder, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: {}, +}; diff --git a/frontend/src/components/link/LinkTable/LinkTable.stories.tsx b/frontend/src/components/link/LinkTable/LinkTable.stories.tsx index 7f28402f4..c26fc59a9 100644 --- a/frontend/src/components/link/LinkTable/LinkTable.stories.tsx +++ b/frontend/src/components/link/LinkTable/LinkTable.stories.tsx @@ -1,36 +1,36 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import TeamLinkTable from './LinkTable'; - -/** - * `LinkTable` 는 팀 링크 목록을 표시할 메뉴 컴포넌트입니다. - */ -const meta = { - title: 'Link/TeamLinkTable', - component: TeamLinkTable, - tags: ['autodocs'], - decorators: [ - (Story) => ( -
- -
- ), - ], -} satisfies Meta; - -export default meta; - -type Story = StoryObj; - -/** - * 회색 배경은 컴포넌트를 고정시키고 크기를 조절하기 위해 사용한 것으로, 컴포넌트에는 포함되지 않습니다. - */ -export const Default: Story = { - args: {}, -}; +import type { Meta, StoryObj } from '@storybook/react'; +import TeamLinkTable from './LinkTable'; + +/** + * `LinkTable` 는 팀 링크 목록을 표시할 메뉴 컴포넌트입니다. + */ +const meta = { + title: 'Link/TeamLinkTable', + component: TeamLinkTable, + tags: ['autodocs'], + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * 회색 배경은 컴포넌트를 고정시키고 크기를 조절하기 위해 사용한 것으로, 컴포넌트에는 포함되지 않습니다. + */ +export const Default: Story = { + args: {}, +}; diff --git a/frontend/src/components/team_calendar/FakeScheduleBarsScreen/FakeScheduleBarsScreen.stories.tsx b/frontend/src/components/team_calendar/FakeScheduleBarsScreen/FakeScheduleBarsScreen.stories.tsx index 66a9c0bc4..daa46eea2 100644 --- a/frontend/src/components/team_calendar/FakeScheduleBarsScreen/FakeScheduleBarsScreen.stories.tsx +++ b/frontend/src/components/team_calendar/FakeScheduleBarsScreen/FakeScheduleBarsScreen.stories.tsx @@ -1,127 +1,127 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import type { ComponentType } from 'react'; -import FakeScheduleBarsScreen from '~/components/team_calendar/FakeScheduleBarsScreen/FakeScheduleBarsScreen'; -import type { GeneratedScheduleBar } from '~/types/schedule'; - -/** - * `FakeScheduleBarsScreen` 는 캘린더 바의 드래그 기능을 구현하기 위해 사용자에게 보여주는 가짜 캘린더 바로 구성된, 시각적인 컴포넌트입니다. - * - * `mode = schedule`일 경우, 마우스 조작을 통해 x, y 값을 계속해서 업데이트하면 마우스를 따라다니듯이 작동하도록 만들 수 있습니다. x, y 값을 변경하면서 컴포넌트의 변화를 테스트하세요. - */ -const meta = { - title: 'Schedule/FakeScheduleBarsScreen', - component: FakeScheduleBarsScreen, - tags: ['autodocs'], - decorators: [ - (Story: ComponentType) => ( -
- -
- ), - ], - argTypes: { - mode: { - description: - '이 컴포넌트의 모드를 의미합니다. 사용 목적에 따라 `schedule`과 `indicator` 중 하나를 명시해 주세요.', - }, - scheduleBars: { - description: '렌더링할 스케줄 바들의 정보를 의미합니다.', - }, - relativeX: { - description: - '기존 좌표에서 좌우로 얼마나 이동한 위치에 렌더링 시킬 것인지를 의미합니다. 이 값이 양수이면 기존 좌표에서 수치만큼 오른쪽으로 이동하여 렌더링되고, 음수일 경우 왼쪽으로 이동하여 렌더링됩니다. 단위는 픽셀(px)입니다. **이 프로퍼티는 `mode = schedule`일 때만 사용할 수 있습니다.**', - }, - relativeY: { - description: - '기존 좌표에서 상하로 얼마나 이동한 위치에 렌더링 시킬 것인지를 의미합니다. 이 값이 양수이면 기존 좌표에서 수치만큼 아래쪽으로 이동하여 렌더링되고, 음수일 경우 위쪽으로 이동하여 렌더링됩니다. 단위는 픽셀(px)입니다. **이 프로퍼티는 `mode = schedule`일 때만 사용할 수 있습니다.**', - }, - }, -} satisfies Meta; - -export default meta; - -type Story = StoryObj; - -const scheduleBars: GeneratedScheduleBar[] = [ - { - id: '1', - scheduleId: 1105, - title: '바쁜 필립의 3주짜리 일정', - row: 0, - column: 1, - duration: 6, - level: 0, - roundedStart: true, - roundedEnd: false, - schedule: { - id: 1105, - title: '바쁜 필립의 3주짜리 일정', - startDateTime: '2023-06-26 00:00', - endDateTime: '2023-07-12 23:59', - }, - }, - { - id: '2', - scheduleId: 1105, - title: '바쁜 필립의 3주짜리 일정', - row: 1, - column: 0, - duration: 7, - level: 0, - roundedStart: false, - roundedEnd: false, - schedule: { - id: 1105, - title: '바쁜 필립의 3주짜리 일정', - startDateTime: '2023-06-26 00:00', - endDateTime: '2023-07-12 23:59', - }, - }, - { - id: '3', - scheduleId: 1105, - title: '바쁜 필립의 3주짜리 일정', - row: 2, - column: 0, - duration: 4, - level: 0, - roundedStart: false, - roundedEnd: true, - schedule: { - id: 1105, - title: '바쁜 필립의 3주짜리 일정', - startDateTime: '2023-06-26 00:00', - endDateTime: '2023-07-12 23:59', - }, - }, -]; - -/** - * 이 모드는 가짜 스케줄 바를 보여줘야 할 경우에 사용합니다. - */ -export const ScheduleMode: Story = { - args: { - mode: 'schedule', - scheduleBars, - relativeX: 0, - relativeY: 0, - }, -}; - -/** - * 이 모드는 스케줄 바가 놓일 위치를 시각적으로 보여줘야 할 경우에 사용합니다. - */ -export const IndicatorMode: Story = { - args: { - mode: 'indicator', - scheduleBars, - }, -}; +import type { Meta, StoryObj } from '@storybook/react'; +import type { ComponentType } from 'react'; +import FakeScheduleBarsScreen from '~/components/team_calendar/FakeScheduleBarsScreen/FakeScheduleBarsScreen'; +import type { GeneratedScheduleBar } from '~/types/schedule'; + +/** + * `FakeScheduleBarsScreen` 는 캘린더 바의 드래그 기능을 구현하기 위해 사용자에게 보여주는 가짜 캘린더 바로 구성된, 시각적인 컴포넌트입니다. + * + * `mode = schedule`일 경우, 마우스 조작을 통해 x, y 값을 계속해서 업데이트하면 마우스를 따라다니듯이 작동하도록 만들 수 있습니다. x, y 값을 변경하면서 컴포넌트의 변화를 테스트하세요. + */ +const meta = { + title: 'Schedule/FakeScheduleBarsScreen', + component: FakeScheduleBarsScreen, + tags: ['autodocs'], + decorators: [ + (Story: ComponentType) => ( +
+ +
+ ), + ], + argTypes: { + mode: { + description: + '이 컴포넌트의 모드를 의미합니다. 사용 목적에 따라 `schedule`과 `indicator` 중 하나를 명시해 주세요.', + }, + scheduleBars: { + description: '렌더링할 스케줄 바들의 정보를 의미합니다.', + }, + relativeX: { + description: + '기존 좌표에서 좌우로 얼마나 이동한 위치에 렌더링 시킬 것인지를 의미합니다. 이 값이 양수이면 기존 좌표에서 수치만큼 오른쪽으로 이동하여 렌더링되고, 음수일 경우 왼쪽으로 이동하여 렌더링됩니다. 단위는 픽셀(px)입니다. **이 프로퍼티는 `mode = schedule`일 때만 사용할 수 있습니다.**', + }, + relativeY: { + description: + '기존 좌표에서 상하로 얼마나 이동한 위치에 렌더링 시킬 것인지를 의미합니다. 이 값이 양수이면 기존 좌표에서 수치만큼 아래쪽으로 이동하여 렌더링되고, 음수일 경우 위쪽으로 이동하여 렌더링됩니다. 단위는 픽셀(px)입니다. **이 프로퍼티는 `mode = schedule`일 때만 사용할 수 있습니다.**', + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +const scheduleBars: GeneratedScheduleBar[] = [ + { + id: '1', + scheduleId: 1105, + title: '바쁜 필립의 3주짜리 일정', + row: 0, + column: 1, + duration: 6, + level: 0, + roundedStart: true, + roundedEnd: false, + schedule: { + id: 1105, + title: '바쁜 필립의 3주짜리 일정', + startDateTime: '2023-06-26 00:00', + endDateTime: '2023-07-12 23:59', + }, + }, + { + id: '2', + scheduleId: 1105, + title: '바쁜 필립의 3주짜리 일정', + row: 1, + column: 0, + duration: 7, + level: 0, + roundedStart: false, + roundedEnd: false, + schedule: { + id: 1105, + title: '바쁜 필립의 3주짜리 일정', + startDateTime: '2023-06-26 00:00', + endDateTime: '2023-07-12 23:59', + }, + }, + { + id: '3', + scheduleId: 1105, + title: '바쁜 필립의 3주짜리 일정', + row: 2, + column: 0, + duration: 4, + level: 0, + roundedStart: false, + roundedEnd: true, + schedule: { + id: 1105, + title: '바쁜 필립의 3주짜리 일정', + startDateTime: '2023-06-26 00:00', + endDateTime: '2023-07-12 23:59', + }, + }, +]; + +/** + * 이 모드는 가짜 스케줄 바를 보여줘야 할 경우에 사용합니다. + */ +export const ScheduleMode: Story = { + args: { + mode: 'schedule', + scheduleBars, + relativeX: 0, + relativeY: 0, + }, +}; + +/** + * 이 모드는 스케줄 바가 놓일 위치를 시각적으로 보여줘야 할 경우에 사용합니다. + */ +export const IndicatorMode: Story = { + args: { + mode: 'indicator', + scheduleBars, + }, +}; diff --git a/frontend/src/components/user/AccountDeleteModal/AccountDeleteModal.stories.tsx b/frontend/src/components/user/AccountDeleteModal/AccountDeleteModal.stories.tsx index b2e73fc79..98fd1b9a1 100644 --- a/frontend/src/components/user/AccountDeleteModal/AccountDeleteModal.stories.tsx +++ b/frontend/src/components/user/AccountDeleteModal/AccountDeleteModal.stories.tsx @@ -1,33 +1,33 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import AccountDeleteModal from './AccountDeleteModal'; -import { useModal } from '~/hooks/useModal'; -import Button from '~/components/common/Button/Button'; - -/** - * `AccountDeleteModal` 는 계정 탈퇴 진행을 위한 모달입니다. - */ -const meta = { - title: 'user/AccountDeleteModal', - component: AccountDeleteModal, - tags: ['autodocs'], -} satisfies Meta; - -export default meta; - -type Story = StoryObj; - -const SampleModal = () => { - const { openModal } = useModal(); - - return ( - <> - - - - ); -}; - -export const Default: Story = { - render: () => , - args: {}, -}; +import type { Meta, StoryObj } from '@storybook/react'; +import AccountDeleteModal from './AccountDeleteModal'; +import { useModal } from '~/hooks/useModal'; +import Button from '~/components/common/Button/Button'; + +/** + * `AccountDeleteModal` 는 계정 탈퇴 진행을 위한 모달입니다. + */ +const meta = { + title: 'user/AccountDeleteModal', + component: AccountDeleteModal, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +const SampleModal = () => { + const { openModal } = useModal(); + + return ( + <> + + + + ); +}; + +export const Default: Story = { + render: () => , + args: {}, +}; From 2d333a57e272be7ac96afafdfda5054a8374e43a Mon Sep 17 00:00:00 2001 From: Rulu Date: Fri, 3 Jan 2025 16:46:42 +0900 Subject: [PATCH 03/20] =?UTF-8?q?feat:=20switch=20=EA=B3=B5=EC=9A=A9=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 --- .../common/Switch/Switch.stories.tsx | 238 ++++++++++++++++++ .../components/common/Switch/Switch.styled.ts | 222 ++++++++++++++++ .../src/components/common/Switch/Switch.tsx | 148 +++++++++++ frontend/src/types/size.ts | 2 + 4 files changed, 610 insertions(+) create mode 100644 frontend/src/components/common/Switch/Switch.stories.tsx create mode 100644 frontend/src/components/common/Switch/Switch.styled.ts create mode 100644 frontend/src/components/common/Switch/Switch.tsx diff --git a/frontend/src/components/common/Switch/Switch.stories.tsx b/frontend/src/components/common/Switch/Switch.stories.tsx new file mode 100644 index 000000000..59ea68a10 --- /dev/null +++ b/frontend/src/components/common/Switch/Switch.stories.tsx @@ -0,0 +1,238 @@ +import { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; + +import Switch from './Switch'; + +/** + * 공용 Switch 컴포넌트 + */ +const meta: Meta = { + title: 'Common/Switch', + component: Switch, + tags: ['autodocs'], + render: (args) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const [checked, setChecked] = useState(false); + + const handleChange = () => { + setChecked((prevChecked) => !prevChecked); + }; + + return ; + }, + argTypes: { + checked: { control: 'boolean' }, + onChange: { action: 'clicked' }, + size: { + control: { + type: 'select', + options: ['xs', 'sm', 'md', 'lg'], + }, + description: '스위치 크기', + }, + variant: { + control: { + type: 'select', + options: ['solid', 'raised'], + }, + description: + 'solid : track안에 thumb이 들어 있는 유형, raised : track보다 thumb이 큰 유형', + }, + readonly: { control: 'boolean' }, + disabled: { control: 'boolean' }, + description: { + control: 'text', + description: '스위치에 대한 설명이 되는 컴포넌트', + }, + descriptionPosition: { + control: { + type: 'select', + options: ['top', 'bottom', 'left', 'right'], + }, + description: '설명 위치(상/하/좌/우)', + }, + onLabel: { + control: 'text', + description: + '스위치가 켜져 있을 때의 트랙 라벨 내 들어갈 텍스트 또는 아이콘', + }, + offLabel: { + control: 'text', + description: + '스위치가 꺼져 있을 때의 트랙 라벨 내 들어갈 텍스트 또는 아이콘', + }, + onThumb: { + control: 'text', + description: '스위치가 켜져 있을 때의 thumb 내 들어갈 텍스트 또는 아이콘', + }, + offThumb: { + control: 'text', + description: '스위치가 꺼져 있을 때의 thumb 내 들어갈 텍스트 또는 아이콘', + }, + onColor: { + control: 'color', + description: '스위치가 켜져 있을 때의 트랙 색상', + }, + offColor: { + control: 'color', + description: '스위치가 꺼져 있을 때의 트랙 색상', + }, + thumbOnColor: { + control: 'color', + description: '스위치가 켜져 있을 때의 thumb 색상', + }, + thumbOffColor: { + control: 'color', + description: '스위치가 꺼져 있을 때의 thumb 색상', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Solid: Story = { + args: { + size: 'md', + checked: false, + onChange: () => console.log('Switch Changed'), + }, +}; + +export const Raised: Story = { + args: { + size: 'md', + checked: false, + onChange: () => console.log('Switch Changed'), + variant: 'raised', + }, +}; + +export const ExtraSmall: Story = { + args: { + size: 'xs', + checked: false, + onChange: () => console.log('Switch Changed'), + }, +}; + +export const Small: Story = { + args: { + size: 'sm', + checked: false, + onChange: () => console.log('Switch Changed'), + }, +}; + +export const Medium: Story = { + args: { + size: 'md', + checked: false, + onChange: () => console.log('Switch Changed'), + }, +}; + +export const Large: Story = { + args: { + size: 'lg', + checked: false, + onChange: () => console.log('Switch Changed'), + }, +}; + +export const WithDescription: Story = { + args: { + size: 'md', + checked: false, + onChange: () => console.log('Switch Changed'), + description:
이것은 설명입니다.
, + }, +}; + +export const WithDescriptionComponent: Story = { + args: { + size: 'md', + checked: false, + onChange: () => console.log('Switch changed'), + description: ( +
+ 로그인 +
+ ), + }, +}; + +export const WithCustomColor: Story = { + args: { + size: 'md', + checked: false, + onChange: () => console.log('Switch Changed'), + onColor: 'rgb(21, 99, 223)', + offColor: '#99b4d9', + thumbOnColor: '#1a0cdc', + thumbOffColor: '#040d32', + }, +}; + +export const WithThumbText: Story = { + args: { + size: 'lg', + checked: false, + onChange: () => console.log('Switch Changed'), + onThumb: 'ON', + offThumb: 'OFF', + }, +}; + +export const WithInnerLabel: Story = { + args: { + size: 'lg', + checked: false, + onChange: () => console.log('Switch Changed'), + onLabel: '자동 업데이트', + offLabel: '수동 업데이트', + }, +}; + +export const Disabled: Story = { + args: { + size: 'md', + checked: false, + onChange: () => console.log('Switch Changed'), + description: 'Disabled switch', + disabled: true, + }, +}; + +export const ReadOnly: Story = { + args: { + size: 'md', + checked: false, + onChange: () => console.log('Switch Changed'), + description: 'Read-only switch', + readonly: true, + }, +}; + +export const Playground: Story = { + render: () => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const [checked, setChecked] = useState(false); + + const handleChange = () => { + console.log('Switch Changed'); + setChecked((prevChecked) => !prevChecked); + }; + + return ( +
+ + {checked &&
💡
} +
+ ); + }, + args: { + size: 'md', + variant: 'solid', + }, +}; diff --git a/frontend/src/components/common/Switch/Switch.styled.ts b/frontend/src/components/common/Switch/Switch.styled.ts new file mode 100644 index 000000000..05b7d0095 --- /dev/null +++ b/frontend/src/components/common/Switch/Switch.styled.ts @@ -0,0 +1,222 @@ +import styled, { css } from 'styled-components'; +import theme from '~/styles/theme'; +import type { SwitchSize } from '~/types/size'; + +const backgroundColor = ( + $isOn: boolean, + $offColor?: string, + $onColor?: string, +) => css` + background-color: ${$isOn + ? $onColor || theme.color.BLACK + : $offColor || theme.color.GRAY200}; +`; + +const flexStyles = { + column: css` + flex-direction: column; + `, + row: css` + flex-direction: row; + `, +}; + +const sizeStyles = { + xs: css` + min-width: 28px; + height: 16px; + `, + sm: css` + min-width: 38px; + height: 20px; + `, + md: css` + min-width: 48px; + height: 24px; + `, + lg: css` + min-width: 58px; + height: 30px; + `, +}; + +const thumbSizes = { + xs: css` + font-size: 4px; + width: 12px; + height: 12px; + `, + sm: css` + font-size: 6px; + width: 16px; + height: 16px; + `, + md: css` + font-size: 8px; + width: 20px; + height: 20px; + `, + lg: css` + font-size: 10px; + width: 25px; + height: 25px; + `, +}; + +export const ContainerDiv = styled.div<{ + $descriptionPosition: 'top' | 'bottom' | 'left' | 'right'; +}>` + display: flex; + align-items: center; + gap: 10px; + + ${({ $descriptionPosition }) => + ['top', 'bottom'].includes($descriptionPosition) + ? flexStyles.column + : flexStyles.row} +`; + +export const Label = styled.label<{ + $size: SwitchSize; +}>` + width: auto; + display: flex; + align-items: center; + cursor: pointer; + + ${({ $size }) => sizeStyles[$size]} +`; + +export const TrackDiv = styled.div<{ + $variant: 'solid' | 'raised'; + $isOn: boolean; + $offColor: string | undefined; + $onColor: string | undefined; +}>` + position: relative; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + border-radius: 25px; + transition: background 0.4s; + background-color: ${({ $offColor }) => + $offColor ? $offColor : theme.color.GRAY200}; + + ${({ $variant }) => { + if ($variant === 'solid') + return css` + height: 100%; + `; + + return css` + height: 40%; + `; + }} + + ${({ $isOn, $offColor, $onColor }) => + backgroundColor($isOn, $offColor, $onColor)}; +`; + +export const LabelSpan = styled.span<{ + $size: SwitchSize; + $isOn: boolean; +}>` + font-size: 10px; + white-space: nowrap; + color: ${theme.color.WHITE}; + flex-grow: 1; + text-align: center; + + ${({ $size }) => { + if ($size === 'xs' || $size === 'sm') + return css` + font-size: 8px; + `; + }} + + ${({ $isOn }) => { + if ($isOn) + return css` + margin-left: 10px; + `; + + return css` + margin-right: 10px; + `; + }} + + + ${({ $size, $isOn }) => + css` + margin-${$isOn ? 'right' : 'left'}: calc(${ + $size === 'xs' + ? '18px' + : $size === 'sm' + ? '23px' + : $size === 'md' + ? '28px' + : '34px' + }); + `}; +`; + +export const ThumbSpan = styled.span<{ + $variant: 'solid' | 'raised'; + $size: SwitchSize; + $isOn: boolean; + $offColor: string | undefined; + $onColor: string | undefined; +}>` + position: absolute; + display: flex; + justify-content: center; + align-items: center; + border-radius: 50%; + + ${({ $isOn, $offColor, $onColor }) => + backgroundColor( + $isOn, + $offColor ?? theme.color.WHITE, + $onColor ?? theme.color.WHITE, + )}; + + ${({ $size }) => thumbSizes[$size]}; + + ${({ $size, $isOn, $variant }) => { + const isSolid = $variant === 'solid'; + const solidLeftPositions = { + xs: 'calc(100% - 14px)', + sm: 'calc(100% - 19px)', + md: 'calc(100% - 23px)', + lg: 'calc(100% - 29px)', + }; + + if (isSolid) { + return css` + left: ${$isOn + ? solidLeftPositions[$size] + : $size === 'xs' + ? '2px' + : '3px'}; + transition: left 0.4s; + `; + } + + return css` + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + left: 0; + transition: transform 0.4s; + ${$isOn && 'transform: translateX(142%);'} + `; + }} +`; + +export const ToggleInput = styled.input.attrs({ type: 'checkbox' })` + display: none; + + &:disabled + ${TrackDiv},&:disabled + ${ThumbSpan} { + opacity: 0.7; + cursor: not-allowed; + } +`; diff --git a/frontend/src/components/common/Switch/Switch.tsx b/frontend/src/components/common/Switch/Switch.tsx new file mode 100644 index 000000000..279b34f1a --- /dev/null +++ b/frontend/src/components/common/Switch/Switch.tsx @@ -0,0 +1,148 @@ +import { + useState, + useRef, + useEffect, + useCallback, + type CSSProperties, + type ReactNode, +} from 'react'; + +import * as S from './Switch.styled'; +import type { SwitchSize } from '~/types/size'; + +export interface SwitchProps { + checked: boolean; + onChange: () => void; + size?: SwitchSize; + variant?: 'solid' | 'raised'; + style?: CSSProperties; + description?: ReactNode; + descriptionPosition?: 'top' | 'bottom' | 'left' | 'right'; + onLabel?: string | ReactNode; + offLabel?: string | ReactNode; + onThumb?: string | ReactNode; + offThumb?: string | ReactNode; + readonly?: boolean; + disabled?: boolean; + onColor?: string; + offColor?: string; + thumbOnColor?: string; + thumbOffColor?: string; +} + +/** + * Switch 컴포넌트 + * + * @param {boolean} checked - 스위치의 현재 상태를 나타냅니다. true이면 켜짐, false이면 꺼짐. + * @param {function} onChange - 스위치 상태가 변경될 때 호출되는 콜백 함수. + * @param {"xs" | "sm" | "md" | "lg"} [size="md"] - 스위치의 크기를 설정합니다. + * @param {"solid" | "raised"} [variant="solid"] - 스위치의 스타일 유형을 설정합니다. `solid`: 트랙 안에 thumb이 들어 있는 유형. `raised`: 트랙보다 thumb이 큰 유형. + * @param {CSSProperties} [style] - 추가 스타일을 적용할 수 있는 인라인 스타일 객체입니다. + * @param {ReactNode} [description] - 스위치에 대한 설명이 되는 컴포넌트입니다. + * @param {"top" | "bottom" | "left" | "right"} [descriptionPosition="right"] - 설명의 위치를 설정합니다. `top`: 위쪽, `bottom`: 아래쪽, `left`: 왼쪽, `right`: 오른쪽. + * @param {string | ReactNode} [onLabel] - 스위치가 켜져 있을 때 트랙 라벨에 들어갈 텍스트 또는 아이콘. + * @param {string | ReactNode} [offLabel] - 스위치가 꺼져 있을 때 트랙 라벨에 들어갈 텍스트 또는 아이콘. + * @param {string | ReactNode} [onThumb] - 스위치가 켜져 있을 때 thumb 안에 들어갈 텍스트 또는 아이콘. + * @param {string | ReactNode} [offThumb] - 스위치가 꺼져 있을 때 thumb 안에 들어갈 텍스트 또는 아이콘. + * @param {boolean} [readonly=false] - 스위치를 읽기 전용으로 설정합니다. + * @param {boolean} [disabled=false] - 스위치를 비활성화 상태로 설정합니다. + * @param {string} [onColor] - 스위치가 켜져 있을 때의 트랙 색상을 설정합니다. + * @param {string} [offColor] - 스위치가 꺼져 있을 때의 트랙 색상을 설정합니다. + * @param {string} [thumbOnColor] - 스위치가 켜져 있을 때 thumb의 색상을 설정합니다. + * @param {string} [thumbOffColor] - 스위치가 꺼져 있을 때 thumb의 색상을 설정합니다. + */ +const Switch = ({ + checked, + onChange, + size = 'md', + variant = 'solid', + style, + description, + descriptionPosition = 'right', + onLabel, + offLabel, + onThumb, + offThumb, + readonly = false, + disabled = false, + onColor, + offColor, + thumbOnColor, + thumbOffColor, +}: SwitchProps) => { + const [isOn, setIsOn] = useState(checked); + const throttleTimeout = useRef(null); + + const throttledOnChange = useCallback(() => { + if (throttleTimeout.current === null) { + onChange(); + throttleTimeout.current = window.setTimeout(() => { + throttleTimeout.current = null; + }, 500); + } + }, [onChange]); + + const handleClickToggle = () => { + if (!readonly && !disabled) { + setIsOn((prevState) => !prevState); + throttledOnChange(); + } + }; + + useEffect(() => { + if (checked !== isOn) { + setIsOn(checked); + } + }, [checked, isOn]); + + useEffect(() => { + return () => { + if (throttleTimeout.current !== null) { + clearTimeout(throttleTimeout.current); + } + }; + }, []); + + return ( + + {description && + ['top', 'left'].includes(descriptionPosition) && + description} + + + + + + {isOn ? onLabel : offLabel} + + + {isOn ? onThumb : offThumb} + + + + + {description && + ['bottom', 'right'].includes(descriptionPosition) && + description} + + ); +}; + +export default Switch; diff --git a/frontend/src/types/size.ts b/frontend/src/types/size.ts index c7a92cc91..b1e9d89f5 100644 --- a/frontend/src/types/size.ts +++ b/frontend/src/types/size.ts @@ -21,3 +21,5 @@ export type TeamBadgeSize = Extract; export type CalendarSize = Extract; export type LinkSize = Extract; + +export type SwitchSize = Extract; From 275733ff3edd45d28f9966a5005452f4ca7ecffd Mon Sep 17 00:00:00 2001 From: Rulu Date: Fri, 3 Jan 2025 17:17:52 +0900 Subject: [PATCH 04/20] =?UTF-8?q?feat:=20=EC=BA=98=EB=A6=B0=EB=8D=94=20?= =?UTF-8?q?=EC=88=98=EC=A0=95,=20=EB=93=B1=EB=A1=9D=20=EB=AA=A8=EB=8B=AC?= =?UTF-8?q?=20=EC=A2=85=EC=9D=BC=20=EC=B2=B4=ED=81=AC=EB=B0=95=EC=8A=A4?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=8A=A4=EC=9C=84=EC=B9=98=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/common/Switch/Switch.styled.ts | 15 ++++++++--- .../src/components/common/Switch/Switch.tsx | 13 +++++++++- .../ScheduleAddModal.styled.ts | 2 +- .../ScheduleAddModal/ScheduleAddModal.tsx | 15 ++++++----- .../ScheduleEditModal.styled.ts | 2 +- .../ScheduleEditModal/ScheduleEditModal.tsx | 25 +++++++++++++------ 6 files changed, 50 insertions(+), 22 deletions(-) diff --git a/frontend/src/components/common/Switch/Switch.styled.ts b/frontend/src/components/common/Switch/Switch.styled.ts index 05b7d0095..10352ad77 100644 --- a/frontend/src/components/common/Switch/Switch.styled.ts +++ b/frontend/src/components/common/Switch/Switch.styled.ts @@ -121,15 +121,24 @@ export const TrackDiv = styled.div<{ export const LabelSpan = styled.span<{ $size: SwitchSize; $isOn: boolean; + $offColor: string | undefined; + $onColor: string | undefined; }>` - font-size: 10px; + font-size: 14px; white-space: nowrap; - color: ${theme.color.WHITE}; + color: ${({ $isOn, $onColor, $offColor }) => + $isOn ? $onColor || theme.color.WHITE : $offColor || theme.color.GRAY700}; flex-grow: 1; text-align: center; ${({ $size }) => { - if ($size === 'xs' || $size === 'sm') + if ($size === 'sm') { + return css` + font-size: 12px; + `; + } + + if ($size === 'xs') return css` font-size: 8px; `; diff --git a/frontend/src/components/common/Switch/Switch.tsx b/frontend/src/components/common/Switch/Switch.tsx index 279b34f1a..e47254bec 100644 --- a/frontend/src/components/common/Switch/Switch.tsx +++ b/frontend/src/components/common/Switch/Switch.tsx @@ -26,6 +26,8 @@ export interface SwitchProps { disabled?: boolean; onColor?: string; offColor?: string; + onLabelColor?: string; + offLabelColor?: string; thumbOnColor?: string; thumbOffColor?: string; } @@ -48,6 +50,8 @@ export interface SwitchProps { * @param {boolean} [disabled=false] - 스위치를 비활성화 상태로 설정합니다. * @param {string} [onColor] - 스위치가 켜져 있을 때의 트랙 색상을 설정합니다. * @param {string} [offColor] - 스위치가 꺼져 있을 때의 트랙 색상을 설정합니다. + * @param {string} [onLabelColor] - 스위치가 켜져 있을 때의 라벨 색상을 설정합니다. + * @param {string} [offLabelColor] - 스위치가 꺼져 있을 때의 라벨 색상을 설정합니다. * @param {string} [thumbOnColor] - 스위치가 켜져 있을 때 thumb의 색상을 설정합니다. * @param {string} [thumbOffColor] - 스위치가 꺼져 있을 때 thumb의 색상을 설정합니다. */ @@ -67,6 +71,8 @@ const Switch = ({ disabled = false, onColor, offColor, + onLabelColor, + offLabelColor, thumbOnColor, thumbOffColor, }: SwitchProps) => { @@ -123,7 +129,12 @@ const Switch = ({ $offColor={offColor} $onColor={onColor} > - + {isOn ? onLabel : offLabel} {
- - 종일 - - -

{ - - 종일 - - + onLabel={'종일'} + offLabel={'종일'} + onColor={theme.color.PRIMARY} + />{' '} +

+ {isAllDay + ? '종일 일정이 선택되었습니다.' + : '종일 일정이 해제되었습니다.'} +

From 06097e2592a57a05f3437165198f73e8a9427ee4 Mon Sep 17 00:00:00 2001 From: Rulu Date: Fri, 3 Jan 2025 22:07:27 +0900 Subject: [PATCH 05/20] =?UTF-8?q?feat:=20=EC=BA=98=EB=A6=B0=EB=8D=94=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D,=20=EC=88=98=EC=A0=95=20=EB=AA=A8=EB=8B=AC?= =?UTF-8?q?=EC=97=90=20=EB=A9=94=EB=AA=A8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/assets/svg/index.ts | 1 + frontend/src/assets/svg/memo.svg | 4 ++ .../src/components/common/Svg/Svg.styled.ts | 7 +++ frontend/src/components/common/Svg/Svg.tsx | 29 ++++++++++ .../ScheduleAddModal.styled.ts | 39 ++++++++++++- .../ScheduleAddModal/ScheduleAddModal.tsx | 56 ++++++++++++++++++- .../ScheduleEditModal.styled.ts | 39 ++++++++++++- .../ScheduleEditModal/ScheduleEditModal.tsx | 50 ++++++++++++++++- .../src/hooks/schedule/useDateTimeRange.ts | 18 +++++- .../src/hooks/schedule/useScheduleAddModal.ts | 40 ++++++++++++- .../hooks/schedule/useScheduleEditModal.ts | 46 ++++++++++++++- frontend/src/mocks/fixtures/schedules.ts | 8 +++ frontend/src/mocks/handlers/calendar.ts | 12 ++-- frontend/src/types/schedule.ts | 1 + 14 files changed, 332 insertions(+), 18 deletions(-) create mode 100644 frontend/src/assets/svg/memo.svg create mode 100644 frontend/src/components/common/Svg/Svg.styled.ts create mode 100644 frontend/src/components/common/Svg/Svg.tsx diff --git a/frontend/src/assets/svg/index.ts b/frontend/src/assets/svg/index.ts index bce88a1d8..8d9dad207 100644 --- a/frontend/src/assets/svg/index.ts +++ b/frontend/src/assets/svg/index.ts @@ -34,3 +34,4 @@ export { ReactComponent as QuestionIcon } from './question.svg'; export { ReactComponent as ExportIcon } from './export.svg'; export { ReactComponent as TeamSmallIcon } from './team-small.svg'; export { ReactComponent as EnterIcon } from './enter.svg'; +export { ReactComponent as MemoIcon } from './memo.svg'; diff --git a/frontend/src/assets/svg/memo.svg b/frontend/src/assets/svg/memo.svg new file mode 100644 index 000000000..c558ea1d8 --- /dev/null +++ b/frontend/src/assets/svg/memo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/components/common/Svg/Svg.styled.ts b/frontend/src/components/common/Svg/Svg.styled.ts new file mode 100644 index 000000000..174ebb220 --- /dev/null +++ b/frontend/src/components/common/Svg/Svg.styled.ts @@ -0,0 +1,7 @@ +import styled from 'styled-components'; + +export const Container = styled.svg<{ $fill?: string }>` + path { + fill: ${({ $fill }) => $fill || 'currentColor'}; + } +`; diff --git a/frontend/src/components/common/Svg/Svg.tsx b/frontend/src/components/common/Svg/Svg.tsx new file mode 100644 index 000000000..bd68298e1 --- /dev/null +++ b/frontend/src/components/common/Svg/Svg.tsx @@ -0,0 +1,29 @@ +import { type CSSProperties, type HTMLAttributes } from 'react'; +import type { CSSProp } from 'styled-components'; +import * as Icons from '~/assets/svg'; +import * as S from './Svg.styled'; + +interface SvgProps extends HTMLAttributes { + type: keyof typeof Icons; + fill?: string; + stroke?: string; + style?: CSSProperties; + size?: string | number; + width?: string; + height?: string; + css?: CSSProp; +} + +const Svg = ({ type, fill, stroke, style, size, ...rest }: SvgProps) => { + const SvgIcon = Icons[type]; + + const svgProps = { + style, + ...(size ? { width: String(size), height: String(size) } : {}), + ...(stroke ? { stroke } : {}), + }; + + return ; +}; + +export default Svg; diff --git a/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.styled.ts b/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.styled.ts index 89dac9ddf..63149d6ed 100644 --- a/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.styled.ts +++ b/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.styled.ts @@ -1,4 +1,5 @@ import { styled, css } from 'styled-components'; +import theme from '~/styles/theme'; import type { CalendarSize } from '~/types/size'; export const Backdrop = styled.div` @@ -84,7 +85,7 @@ export const InnerContainer = styled.div` width: 100%; `; -export const CheckboxContainer = styled.div` +export const ConvenientContainer = styled.div` display: flex; align-items: center; justify-content: flex-start; @@ -137,6 +138,25 @@ export const ControlButtonWrapper = styled.div` height: 38px; `; +export const DescriptionTextarea = styled.textarea` + padding: 6px 10px; + border: none; + border-bottom: 1px solid ${theme.color.GRAY200}; + border-radius: 10px; + + font-size: 14px; + + width: 100%; + white-space: normal; + overflow-wrap: break-word; + display: inline-block; +`; + +export const WarnDiv = styled.div` + display: flex; + justify-content: flex-end; +`; + export const title = css` padding: 10px 20px; @@ -186,3 +206,20 @@ export const submitButton = css` width: 76px; padding: 0; `; + +export const descriptionButton = ($isDescription: boolean) => css` + display: flex; + padding: 2px 6px; + align-items: center; + border: 1px solid ${theme.color.PRIMARY}; + border-radius: 25px; + background-color: ${$isDescription ? theme.color.PRIMARY : theme.color.WHITE}; +`; + +export const descriptionText = ($isDescription: boolean) => css` + color: ${$isDescription ? theme.color.WHITE : theme.color.PRIMARY}; +`; + +export const errorText = css` + color: ${theme.color.RED}; +`; diff --git a/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.tsx b/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.tsx index 5c1098448..9f23200c3 100644 --- a/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.tsx +++ b/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.tsx @@ -14,6 +14,7 @@ import type { CalendarSize } from '~/types/size'; import { getIsMobile } from '~/utils/getIsMobile'; import Switch from '~/components/common/Switch/Switch'; import theme from '~/styles/theme'; +import Svg from '~/components/common/Svg/Svg'; interface ScheduleAddModalProps { calendarSize?: CalendarSize; @@ -29,6 +30,9 @@ const ScheduleAddModal = (props: ScheduleAddModalProps) => { schedule, isAllDay, times, + isDescription, + isDescriptionMaxLength, + handlers: { handleScheduleChange, handleScheduleBlur, @@ -36,15 +40,22 @@ const ScheduleAddModal = (props: ScheduleAddModalProps) => { handleStartTimeChange, handleEndTimeChange, handleScheduleSubmit, + handleIsDescription, + handleDescriptionInput, }, } = useScheduleAddModal(clickedDate); const titleInputRef = useRef(null); + const descriptionInputRef = useRef(null); useEffect(() => { titleInputRef.current?.focus(); }, []); + useEffect(() => { + if (isDescription) descriptionInputRef.current?.focus(); + }, [isDescription]); + return ( @@ -124,7 +135,7 @@ const ScheduleAddModal = (props: ScheduleAddModalProps) => { )} - + { ? '종일 일정이 선택되었습니다.' : '종일 일정이 해제되었습니다.'}

-
+ + +
+ {isDescription && ( + + )} + + {isDescription && + (!isDescriptionMaxLength ? ( + ({schedule.description.length} / 100자) + ) : ( + + 최대 100자까지 입력가능합니다. + + ))} + +
diff --git a/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.styled.ts b/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.styled.ts index 89dac9ddf..63149d6ed 100644 --- a/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.styled.ts +++ b/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.styled.ts @@ -1,4 +1,5 @@ import { styled, css } from 'styled-components'; +import theme from '~/styles/theme'; import type { CalendarSize } from '~/types/size'; export const Backdrop = styled.div` @@ -84,7 +85,7 @@ export const InnerContainer = styled.div` width: 100%; `; -export const CheckboxContainer = styled.div` +export const ConvenientContainer = styled.div` display: flex; align-items: center; justify-content: flex-start; @@ -137,6 +138,25 @@ export const ControlButtonWrapper = styled.div` height: 38px; `; +export const DescriptionTextarea = styled.textarea` + padding: 6px 10px; + border: none; + border-bottom: 1px solid ${theme.color.GRAY200}; + border-radius: 10px; + + font-size: 14px; + + width: 100%; + white-space: normal; + overflow-wrap: break-word; + display: inline-block; +`; + +export const WarnDiv = styled.div` + display: flex; + justify-content: flex-end; +`; + export const title = css` padding: 10px 20px; @@ -186,3 +206,20 @@ export const submitButton = css` width: 76px; padding: 0; `; + +export const descriptionButton = ($isDescription: boolean) => css` + display: flex; + padding: 2px 6px; + align-items: center; + border: 1px solid ${theme.color.PRIMARY}; + border-radius: 25px; + background-color: ${$isDescription ? theme.color.PRIMARY : theme.color.WHITE}; +`; + +export const descriptionText = ($isDescription: boolean) => css` + color: ${$isDescription ? theme.color.WHITE : theme.color.PRIMARY}; +`; + +export const errorText = css` + color: ${theme.color.RED}; +`; diff --git a/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.tsx b/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.tsx index d5915604e..6ad7b0485 100644 --- a/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.tsx +++ b/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.tsx @@ -14,6 +14,7 @@ import type { CalendarSize } from '~/types/size'; import { getIsMobile } from '~/utils/getIsMobile'; import Switch from '~/components/common/Switch/Switch'; import theme from '~/styles/theme'; +import Svg from '~/components/common/Svg/Svg'; interface ScheduleEditModalProps { calendarSize?: CalendarSize; @@ -31,6 +32,9 @@ const ScheduleEditModal = (props: ScheduleEditModalProps) => { schedule, times, isAllDay, + isDescription, + isDescriptionMaxLength, + handlers: { handleScheduleChange, handleScheduleBlur, @@ -38,6 +42,8 @@ const ScheduleEditModal = (props: ScheduleEditModalProps) => { handleStartTimeChange, handleEndTimeChange, handleIsAllDayChange, + handleIsDescription, + handleDescriptionInput, }, } = useScheduleEditModal(scheduleId, initialSchedule); @@ -119,7 +125,7 @@ const ScheduleEditModal = (props: ScheduleEditModalProps) => { )} - + { ? '종일 일정이 선택되었습니다.' : '종일 일정이 해제되었습니다.'}

-
+ + +
+ {isDescription && ( + + )} + + {isDescription && + (!isDescriptionMaxLength ? ( + ({schedule.description.length} / 100자) + ) : ( + + 최대 100자까지 입력가능합니다. + + ))} + +
diff --git a/frontend/src/hooks/schedule/useDateTimeRange.ts b/frontend/src/hooks/schedule/useDateTimeRange.ts index 29667f10c..7a645afb9 100644 --- a/frontend/src/hooks/schedule/useDateTimeRange.ts +++ b/frontend/src/hooks/schedule/useDateTimeRange.ts @@ -8,6 +8,7 @@ import type { Schedule, YYYYMMDD } from '~/types/schedule'; interface DateTimeRange { title: string; + description: string; startDate: string; startTime: string; endDate: string; @@ -46,6 +47,7 @@ const isDateTimeRangeValid = (dateTimeRange: DateTimeRange) => { const generateDateTimeRange = ( dateData: Date | Schedule | undefined, title: string | undefined, + initDescription: string | undefined, ) => { if (!dateData) { return { @@ -56,6 +58,7 @@ const generateDateTimeRange = ( endTime: '10:00', dateDifference: 0, isAllDay: false, + description: initDescription ?? '', }; } @@ -70,6 +73,7 @@ const generateDateTimeRange = ( endTime: '10:00', dateDifference: 0, isAllDay: false, + description: initDescription ?? '', }; } @@ -88,18 +92,21 @@ const generateDateTimeRange = ( endTime, dateDifference, isAllDay: endTime === '23:59', + description: initDescription ?? '', }; }; export const useDateTimeRange = ( dateData: Date | Schedule | undefined, initTitle: string | undefined, + initDescription: string | undefined, ) => { const [dateTimeRange, setDateTimeRange] = useState( - generateDateTimeRange(dateData, initTitle), + generateDateTimeRange(dateData, initTitle, initDescription), ); const { title, + description, startDate, endDate, startTime, @@ -108,6 +115,13 @@ export const useDateTimeRange = ( isAllDay, } = dateTimeRange; + const handleDescriptionChange = (description: string) => { + setDateTimeRange((prev) => ({ + ...prev, + description: description, + })); + }; + const handleScheduleChange: ChangeEventHandler = (e) => { const { name, value } = e.target; @@ -189,10 +203,12 @@ export const useDateTimeRange = ( handleStartTimeChange, handleEndTimeChange, handleIsAllDayChange, + handleDescriptionChange, title, startDate, endDate, + description, startTime: isAllDay ? '00:00' : startTime, endTime: isAllDay ? '23:59' : endTime, isValid: isDateTimeRangeValid(dateTimeRange), diff --git a/frontend/src/hooks/schedule/useScheduleAddModal.ts b/frontend/src/hooks/schedule/useScheduleAddModal.ts index db3128d1f..444fd2fc0 100644 --- a/frontend/src/hooks/schedule/useScheduleAddModal.ts +++ b/frontend/src/hooks/schedule/useScheduleAddModal.ts @@ -1,7 +1,7 @@ import { useSendSchedule } from '~/hooks/queries/useSendSchedule'; import { useModal } from '~/hooks/useModal'; import { isYYYYMMDDHHMM } from '~/types/typeGuard'; -import type { FormEventHandler } from 'react'; +import { type ChangeEvent, useState, type FormEventHandler } from 'react'; import { useToast } from '~/hooks/useToast'; import { useTeamPlace } from '~/hooks/useTeamPlace'; import { useDateTimeRange } from '~/hooks/schedule/useDateTimeRange'; @@ -9,6 +9,7 @@ import { useDateTimeRange } from '~/hooks/schedule/useDateTimeRange'; export const useScheduleAddModal = (clickedDate: Date) => { const { title, + description, startDate, endDate, startTime, @@ -20,15 +21,43 @@ export const useScheduleAddModal = (clickedDate: Date) => { handleStartTimeChange, handleEndTimeChange, handleIsAllDayChange, - } = useDateTimeRange(clickedDate, ''); + handleDescriptionChange, + } = useDateTimeRange(clickedDate, '', ''); + const [isDescription, setIsDescription] = useState(false); + const [isDescriptionMaxLength, setIsDescriptionMaxLength] = useState(false); const { closeModal } = useModal(); const { showToast } = useToast(); const { teamPlaceId } = useTeamPlace(); const { mutateSendSchedule } = useSendSchedule(teamPlaceId); - const schedule = { title, startDate, endDate }; + const schedule = { title, description, startDate, endDate }; const times = { startTime, endTime }; + const handleIsDescription = () => { + setIsDescription((prev) => { + if (prev) { + handleDescriptionChange(''); + setIsDescriptionMaxLength(false); + } + return !prev; + }); + }; + + const handleDescriptionInput = (e: ChangeEvent) => { + const textarea = e.target; + + textarea.style.height = 'auto'; + textarea.style.height = `${textarea.scrollHeight}px`; + + if (textarea.value.length > 100) { + setIsDescriptionMaxLength(true); + } else { + setIsDescriptionMaxLength(false); + } + + handleDescriptionChange(textarea.value.trim().slice(0, 100)); + }; + const handleScheduleSubmit: FormEventHandler = (e) => { e.preventDefault(); @@ -52,6 +81,7 @@ export const useScheduleAddModal = (clickedDate: Date) => { title, startDateTime, endDateTime, + description, }, { onSuccess: () => { @@ -71,6 +101,8 @@ export const useScheduleAddModal = (clickedDate: Date) => { schedule, isAllDay, times, + isDescription, + isDescriptionMaxLength, handlers: { handleScheduleChange, @@ -79,6 +111,8 @@ export const useScheduleAddModal = (clickedDate: Date) => { handleStartTimeChange, handleEndTimeChange, handleScheduleSubmit, + handleIsDescription, + handleDescriptionInput, }, }; }; diff --git a/frontend/src/hooks/schedule/useScheduleEditModal.ts b/frontend/src/hooks/schedule/useScheduleEditModal.ts index c1c74f333..30913eecf 100644 --- a/frontend/src/hooks/schedule/useScheduleEditModal.ts +++ b/frontend/src/hooks/schedule/useScheduleEditModal.ts @@ -1,7 +1,7 @@ import { useModifySchedule } from '~/hooks/queries/useModifySchedule'; import { useModal } from '~/hooks/useModal'; import { isYYYYMMDDHHMM } from '~/types/typeGuard'; -import type { FormEventHandler } from 'react'; +import { type ChangeEvent, useState, type FormEventHandler } from 'react'; import type { Schedule } from '~/types/schedule'; import { useToast } from '~/hooks/useToast'; import { useTeamPlace } from '~/hooks/useTeamPlace'; @@ -13,6 +13,7 @@ export const useScheduleEditModal = ( ) => { const { title, + description, startDate, endDate, startTime, @@ -24,15 +25,50 @@ export const useScheduleEditModal = ( handleStartTimeChange, handleEndTimeChange, handleIsAllDayChange, - } = useDateTimeRange(initialSchedule, initialSchedule?.title); + handleDescriptionChange, + } = useDateTimeRange( + initialSchedule, + initialSchedule?.title, + initialSchedule?.description ?? '', + ); + const [isDescription, setIsDescription] = useState( + initialSchedule?.description ? true : false, + ); + const [isDescriptionMaxLength, setIsDescriptionMaxLength] = useState(false); + const { closeModal } = useModal(); const { showToast } = useToast(); const { teamPlaceId } = useTeamPlace(); const { mutateModifySchedule } = useModifySchedule(teamPlaceId, scheduleId); - const schedule = { title, startDate, endDate }; + const schedule = { title, description, startDate, endDate }; const times = { startTime, endTime }; + const handleIsDescription = () => { + setIsDescription((prev) => { + if (prev) { + handleDescriptionChange(''); + setIsDescriptionMaxLength(false); + } + return !prev; + }); + }; + + const handleDescriptionInput = (e: ChangeEvent) => { + const textarea = e.target; + + textarea.style.height = 'auto'; + textarea.style.height = `${textarea.scrollHeight}px`; + + if (textarea.value.length > 100) { + setIsDescriptionMaxLength(true); + } else { + setIsDescriptionMaxLength(false); + } + + handleDescriptionChange(textarea.value.trim().slice(0, 100)); + }; + const handleScheduleSubmit: FormEventHandler = (e) => { e.preventDefault(); @@ -75,6 +111,8 @@ export const useScheduleEditModal = ( schedule, times, isAllDay, + isDescription, + isDescriptionMaxLength, handlers: { handleScheduleChange, @@ -83,6 +121,8 @@ export const useScheduleEditModal = ( handleStartTimeChange, handleEndTimeChange, handleIsAllDayChange, + handleIsDescription, + handleDescriptionInput, }, }; }; diff --git a/frontend/src/mocks/fixtures/schedules.ts b/frontend/src/mocks/fixtures/schedules.ts index df458855b..d3d754cd2 100644 --- a/frontend/src/mocks/fixtures/schedules.ts +++ b/frontend/src/mocks/fixtures/schedules.ts @@ -169,4 +169,12 @@ export const mySchedules: ScheduleWithTeamPlaceId[] = [ startDateTime: '2023-06-30 05:00', endDateTime: '2023-07-02 05:00', }, + { + id: 8, + teamPlaceId: 1, + title: 'test7', + startDateTime: '2025-01-03 05:00', + endDateTime: '2023-01-04 05:00', + description: '멤모멤모테스트', + }, ]; diff --git a/frontend/src/mocks/handlers/calendar.ts b/frontend/src/mocks/handlers/calendar.ts index 04e89768d..9b1213a34 100644 --- a/frontend/src/mocks/handlers/calendar.ts +++ b/frontend/src/mocks/handlers/calendar.ts @@ -80,8 +80,6 @@ export const calendarHandlers = [ const scheduleId = Number(params.scheduleId); const data = schedules.find((schedule) => schedule.id === scheduleId); - console.log('테스트', { scheduleId, data }); - const teamPlaceId = Number(params.teamPlaceId); const index = teamPlaces.findIndex( (teamPlace) => teamPlace.id === teamPlaceId, @@ -99,12 +97,14 @@ export const calendarHandlers = [ http.post<{ teamPlaceId: string }, ScheduleWithoutId>( `/api/team-place/:teamPlaceId/calendar/schedules`, async ({ request, params }) => { - const { title, startDateTime, endDateTime } = await request.json(); + const { title, startDateTime, endDateTime, description } = + await request.json(); const newSchedule = { id: Date.now(), title, startDateTime, endDateTime, + description, }; const teamPlaceId = Number(params.teamPlaceId); const index = teamPlaces.findIndex( @@ -133,8 +133,8 @@ export const calendarHandlers = [ const teamPlaceId = Number(params.teamPlaceId); const scheduleId = Number(params.scheduleId); - const { title, startDateTime, endDateTime } = await request.json(); - console.log('테스트', title, startDateTime, endDateTime); + const { title, startDateTime, endDateTime, description } = + await request.json(); const index = schedules.findIndex( (schedule) => schedule.id === scheduleId, ); @@ -152,6 +152,7 @@ export const calendarHandlers = [ title, startDateTime, endDateTime, + description, }; mySchedules[myIndex] = { @@ -160,6 +161,7 @@ export const calendarHandlers = [ title, startDateTime, endDateTime, + description, }; return new HttpResponse(null); diff --git a/frontend/src/types/schedule.ts b/frontend/src/types/schedule.ts index 4798e192f..be2627987 100644 --- a/frontend/src/types/schedule.ts +++ b/frontend/src/types/schedule.ts @@ -6,6 +6,7 @@ export interface Schedule { title: string; startDateTime: YYYYMMDDHHMM; endDateTime: YYYYMMDDHHMM; + description?: string; } export interface ScheduleWithTeamPlaceId extends Schedule { From 7a58270abff5607feda75fbd4f7c3e9867adbb3d Mon Sep 17 00:00:00 2001 From: Rulu Date: Fri, 3 Jan 2025 22:32:48 +0900 Subject: [PATCH 06/20] =?UTF-8?q?feat:=20=EC=BA=98=EB=A6=B0=EB=8D=94=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EB=A9=94=EB=AA=A8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ScheduleModal/ScheduleModal.styled.ts | 18 ++++++++++++------ .../ScheduleModal/ScheduleModal.tsx | 13 +++++++++++-- frontend/src/mocks/fixtures/schedules.ts | 10 +++++++++- frontend/src/mocks/handlers/user.ts | 5 +++++ 4 files changed, 37 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/team_calendar/ScheduleModal/ScheduleModal.styled.ts b/frontend/src/components/team_calendar/ScheduleModal/ScheduleModal.styled.ts index 75c1c1f62..8b6ff1b33 100644 --- a/frontend/src/components/team_calendar/ScheduleModal/ScheduleModal.styled.ts +++ b/frontend/src/components/team_calendar/ScheduleModal/ScheduleModal.styled.ts @@ -6,18 +6,18 @@ export const Container = styled.div<{ $css: CSSProp; $isMobile: boolean }>` position: absolute; flex-direction: column; z-index: ${({ theme }) => theme.zIndex.MODAL}; - gap: 16px; + gap: 10px; ${({ $isMobile }) => { if ($isMobile) return css` width: 300px; - padding: 10px 10px 20px 26px; + padding: 10px 26px 20px; `; return css` - width: 446px; - padding: 18px 22px; + width: 436px; + padding: 12px 16px; `; }} @@ -76,6 +76,12 @@ export const PeriodWrapper = styled.div<{ $isMobile: boolean }>` }} `; +export const DescriptionDiv = styled.div` + display: flex; + align-items: center; + gap: 10px; +`; + export const teamName = css` overflow: hidden; white-space: nowrap; @@ -111,8 +117,8 @@ export const closeButton = ($isMobile: boolean) => css` align-items: center; align-self: flex-end; - width: 80px; - height: 42px; + width: 76px; + height: 36px; ${$isMobile && css` margin-right: 10px; diff --git a/frontend/src/components/team_calendar/ScheduleModal/ScheduleModal.tsx b/frontend/src/components/team_calendar/ScheduleModal/ScheduleModal.tsx index c6f7807a5..609ad8728 100644 --- a/frontend/src/components/team_calendar/ScheduleModal/ScheduleModal.tsx +++ b/frontend/src/components/team_calendar/ScheduleModal/ScheduleModal.tsx @@ -13,6 +13,8 @@ import { useTeamPlace } from '~/hooks/useTeamPlace'; import type { CalendarSize } from '~/types/size'; import { getIsMobile } from '~/utils/getIsMobile'; import { generateDateTimeRangeDescription } from '~/utils/generateDateTimeRangeDescription'; +import Svg from '~/components/common/Svg/Svg'; +import theme from '~/styles/theme'; interface ScheduleModalProps { calendarWidth: number; @@ -41,7 +43,7 @@ const ScheduleModal = (props: ScheduleModalProps) => { const { mutateDeleteSchedule } = useDeleteSchedule(teamPlaceId, scheduleId); if (scheduleById === undefined) return; - const { title, startDateTime, endDateTime } = scheduleById; + const { title, startDateTime, endDateTime, description } = scheduleById; const { row, column, level } = position; const handleScheduleDelete = () => { @@ -72,7 +74,7 @@ const ScheduleModal = (props: ScheduleModalProps) => { > - + {!isMobile && (
{displayName} @@ -116,6 +118,13 @@ const ScheduleModal = (props: ScheduleModalProps) => { + {description && ( + + + {description} + + )} + -
- {isDescription && ( - - )} + + - {isDescription && - (!isDescriptionMaxLength ? ( - ({schedule.description.length} / 100자) - ) : ( - - 최대 100자까지 입력가능합니다. - - ))} + {!isDescriptionMaxLength ? ( + ({schedule.description.length} / 100자) + ) : ( + + 최대 100자까지 입력가능합니다. + + )} -
+ diff --git a/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.styled.ts b/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.styled.ts index 63149d6ed..2c500f586 100644 --- a/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.styled.ts +++ b/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.styled.ts @@ -57,7 +57,7 @@ export const Container = styled.div<{ display: flex; flex-direction: column; - row-gap: ${({ $isMobile }) => ($isMobile ? '10px' : '10px')}; + row-gap: 10px; } `; @@ -138,18 +138,23 @@ export const ControlButtonWrapper = styled.div` height: 38px; `; +export const DescriptionDiv = styled.div<{ $isDescription: boolean }>` + display: ${({ $isDescription }) => ($isDescription ? 'block' : 'none')}; +`; + export const DescriptionTextarea = styled.textarea` + display: inline-block; + width: 100%; padding: 6px 10px; + border: none; border-bottom: 1px solid ${theme.color.GRAY200}; border-radius: 10px; + resize: none; font-size: 14px; - - width: 100%; white-space: normal; overflow-wrap: break-word; - display: inline-block; `; export const WarnDiv = styled.div` diff --git a/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.tsx b/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.tsx index 6ad7b0485..09f4458bb 100644 --- a/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.tsx +++ b/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.tsx @@ -162,27 +162,24 @@ const ScheduleEditModal = (props: ScheduleEditModalProps) => { -
- {isDescription && ( - - )} + + - {isDescription && - (!isDescriptionMaxLength ? ( - ({schedule.description.length} / 100자) - ) : ( - - 최대 100자까지 입력가능합니다. - - ))} + {!isDescriptionMaxLength ? ( + ({schedule.description.length} / 100자) + ) : ( + + 최대 100자까지 입력가능합니다. + + )} -
+ diff --git a/frontend/src/constants/calendar.ts b/frontend/src/constants/calendar.ts index 74a8cd5e4..14781eeff 100644 --- a/frontend/src/constants/calendar.ts +++ b/frontend/src/constants/calendar.ts @@ -18,6 +18,8 @@ export const TIME_TABLE = arrayOf(48).map((_, i) => { export const SCHEDULE_CIRCLE_MAX_COUNT = 3; +export const SCHEDULE_DESCRIPTION_MAX_LENGTH = 100; + export const MODAL_OPEN_TYPE = { ADD: 'add', VIEW: 'view', diff --git a/frontend/src/hooks/schedule/useScheduleAddModal.ts b/frontend/src/hooks/schedule/useScheduleAddModal.ts index 444fd2fc0..8d427ce1e 100644 --- a/frontend/src/hooks/schedule/useScheduleAddModal.ts +++ b/frontend/src/hooks/schedule/useScheduleAddModal.ts @@ -5,6 +5,7 @@ import { type ChangeEvent, useState, type FormEventHandler } from 'react'; import { useToast } from '~/hooks/useToast'; import { useTeamPlace } from '~/hooks/useTeamPlace'; import { useDateTimeRange } from '~/hooks/schedule/useDateTimeRange'; +import { SCHEDULE_DESCRIPTION_MAX_LENGTH } from '~/constants/calendar'; export const useScheduleAddModal = (clickedDate: Date) => { const { @@ -49,13 +50,15 @@ export const useScheduleAddModal = (clickedDate: Date) => { textarea.style.height = 'auto'; textarea.style.height = `${textarea.scrollHeight}px`; - if (textarea.value.length > 100) { + if (textarea.value.length > SCHEDULE_DESCRIPTION_MAX_LENGTH) { setIsDescriptionMaxLength(true); } else { setIsDescriptionMaxLength(false); } - handleDescriptionChange(textarea.value.trim().slice(0, 100)); + handleDescriptionChange( + textarea.value.slice(0, SCHEDULE_DESCRIPTION_MAX_LENGTH), + ); }; const handleScheduleSubmit: FormEventHandler = (e) => { diff --git a/frontend/src/hooks/schedule/useScheduleEditModal.ts b/frontend/src/hooks/schedule/useScheduleEditModal.ts index 30913eecf..f6a11af5f 100644 --- a/frontend/src/hooks/schedule/useScheduleEditModal.ts +++ b/frontend/src/hooks/schedule/useScheduleEditModal.ts @@ -6,6 +6,7 @@ import type { Schedule } from '~/types/schedule'; import { useToast } from '~/hooks/useToast'; import { useTeamPlace } from '~/hooks/useTeamPlace'; import { useDateTimeRange } from '~/hooks/schedule/useDateTimeRange'; +import { SCHEDULE_DESCRIPTION_MAX_LENGTH } from '~/constants/calendar'; export const useScheduleEditModal = ( scheduleId: Schedule['id'], @@ -60,13 +61,15 @@ export const useScheduleEditModal = ( textarea.style.height = 'auto'; textarea.style.height = `${textarea.scrollHeight}px`; - if (textarea.value.length > 100) { + if (textarea.value.length > SCHEDULE_DESCRIPTION_MAX_LENGTH) { setIsDescriptionMaxLength(true); } else { setIsDescriptionMaxLength(false); } - handleDescriptionChange(textarea.value.trim().slice(0, 100)); + handleDescriptionChange( + textarea.value.slice(0, SCHEDULE_DESCRIPTION_MAX_LENGTH), + ); }; const handleScheduleSubmit: FormEventHandler = (e) => { @@ -92,6 +95,7 @@ export const useScheduleEditModal = ( title, startDateTime, endDateTime, + description, }, { onSuccess: () => { From f714b6be0de177230a53be46be168a428f1422e6 Mon Sep 17 00:00:00 2001 From: Rulu Date: Mon, 13 Jan 2025 22:03:28 +0900 Subject: [PATCH 09/20] =?UTF-8?q?fix:=20=EB=A9=94=EB=AA=A8=20=EC=97=86?= =?UTF-8?q?=EC=9D=84=20=EB=95=8C=20=EB=93=B1=EB=A1=9D=EC=95=88=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EB=B2=84=EA=B7=B8=20=ED=94=BD=EC=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/common/Switch/Switch.tsx | 18 -------- .../ScheduleAddModal.styled.ts | 4 -- .../ScheduleAddModal/ScheduleAddModal.tsx | 45 +++++++++++-------- .../ScheduleEditModal.styled.ts | 4 -- .../ScheduleEditModal/ScheduleEditModal.tsx | 42 +++++++++-------- 5 files changed, 50 insertions(+), 63 deletions(-) diff --git a/frontend/src/components/common/Switch/Switch.tsx b/frontend/src/components/common/Switch/Switch.tsx index e56c137b0..b232b4d5c 100644 --- a/frontend/src/components/common/Switch/Switch.tsx +++ b/frontend/src/components/common/Switch/Switch.tsx @@ -79,20 +79,10 @@ const Switch = ({ const [isOn, setIsOn] = useState(checked); const throttleTimeout = useRef(null); - const throttledOnChange = useCallback(() => { - if (throttleTimeout.current === null) { - onChange(); - throttleTimeout.current = window.setTimeout(() => { - throttleTimeout.current = null; - }, 500); - } - }, [onChange]); - const handleClickToggle = () => { if (!readonly && !disabled) { setIsOn((prevState) => !prevState); onChange(); - // throttledOnChange(); } }; @@ -102,14 +92,6 @@ const Switch = ({ } }, [checked, isOn]); - // useEffect(() => { - // return () => { - // if (throttleTimeout.current !== null) { - // clearTimeout(throttleTimeout.current); - // } - // }; - // }, []); - return ( {description && diff --git a/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.styled.ts b/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.styled.ts index a9d658564..a288d47c5 100644 --- a/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.styled.ts +++ b/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.styled.ts @@ -138,10 +138,6 @@ export const ControlButtonWrapper = styled.div` height: 38px; `; -export const DescriptionDiv = styled.div<{ $isDescription: boolean }>` - display: ${({ $isDescription }) => ($isDescription ? 'block' : 'none')}; -`; - export const DescriptionTextarea = styled.textarea` display: inline-block; width: 100%; diff --git a/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.tsx b/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.tsx index 40211e1ef..e296b8080 100644 --- a/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.tsx +++ b/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.tsx @@ -15,6 +15,7 @@ import { getIsMobile } from '~/utils/getIsMobile'; import Switch from '~/components/common/Switch/Switch'; import theme from '~/styles/theme'; import Svg from '~/components/common/Svg/Svg'; +import { SCHEDULE_DESCRIPTION_MAX_LENGTH } from '~/constants/calendar'; interface ScheduleAddModalProps { calendarSize?: CalendarSize; @@ -172,25 +173,31 @@ const ScheduleAddModal = (props: ScheduleAddModalProps) => { - - - - {!isDescriptionMaxLength ? ( - ({schedule.description.length} / 100자) - ) : ( - - 최대 100자까지 입력가능합니다. - - )} - - + {isDescription && ( + <> + + + {!isDescriptionMaxLength ? ( + + ({schedule.description.length} / $ + {SCHEDULE_DESCRIPTION_MAX_LENGTH}자) + + ) : ( + + 최대 ${SCHEDULE_DESCRIPTION_MAX_LENGTH}자까지 + 입력가능합니다. + + )} + + + )} + diff --git a/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.styled.ts b/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.styled.ts index 2c500f586..4991d0bd5 100644 --- a/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.styled.ts +++ b/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.styled.ts @@ -138,10 +138,6 @@ export const ControlButtonWrapper = styled.div` height: 38px; `; -export const DescriptionDiv = styled.div<{ $isDescription: boolean }>` - display: ${({ $isDescription }) => ($isDescription ? 'block' : 'none')}; -`; - export const DescriptionTextarea = styled.textarea` display: inline-block; width: 100%; diff --git a/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.tsx b/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.tsx index 09f4458bb..5a95aa92a 100644 --- a/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.tsx +++ b/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.tsx @@ -15,6 +15,7 @@ import { getIsMobile } from '~/utils/getIsMobile'; import Switch from '~/components/common/Switch/Switch'; import theme from '~/styles/theme'; import Svg from '~/components/common/Svg/Svg'; +import { SCHEDULE_DESCRIPTION_MAX_LENGTH } from '~/constants/calendar'; interface ScheduleEditModalProps { calendarSize?: CalendarSize; @@ -162,24 +163,29 @@ const ScheduleEditModal = (props: ScheduleEditModalProps) => { - - - - {!isDescriptionMaxLength ? ( - ({schedule.description.length} / 100자) - ) : ( - - 최대 100자까지 입력가능합니다. - - )} - - + {isDescription && ( + <> + + + {!isDescriptionMaxLength ? ( + + ({schedule.description.length} / $ + {SCHEDULE_DESCRIPTION_MAX_LENGTH}자) + + ) : ( + + 최대 ${SCHEDULE_DESCRIPTION_MAX_LENGTH}자까지 + 입력가능합니다. + + )} + + + )} From 584a7a837b47d0639e9dfa3f8a1f45c2d0e802bf Mon Sep 17 00:00:00 2001 From: Rulu Date: Mon, 13 Jan 2025 22:28:05 +0900 Subject: [PATCH 10/20] =?UTF-8?q?refactor:=20=EC=98=A4=ED=83=80=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 --- .../team_calendar/ScheduleAddModal/ScheduleAddModal.tsx | 2 +- .../team_calendar/ScheduleEditModal/ScheduleEditModal.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.tsx b/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.tsx index e296b8080..42dcf186e 100644 --- a/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.tsx +++ b/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.tsx @@ -185,7 +185,7 @@ const ScheduleAddModal = (props: ScheduleAddModalProps) => { {!isDescriptionMaxLength ? ( - ({schedule.description.length} / $ + ({schedule.description.length} / {SCHEDULE_DESCRIPTION_MAX_LENGTH}자) ) : ( diff --git a/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.tsx b/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.tsx index 5a95aa92a..f9f7d83b3 100644 --- a/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.tsx +++ b/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.tsx @@ -174,7 +174,7 @@ const ScheduleEditModal = (props: ScheduleEditModalProps) => { {!isDescriptionMaxLength ? ( - ({schedule.description.length} / $ + ({schedule.description.length} / {SCHEDULE_DESCRIPTION_MAX_LENGTH}자) ) : ( From 15c54bac36b96877879e564924496a539ef2b2d2 Mon Sep 17 00:00:00 2001 From: Rulu Date: Sun, 29 Dec 2024 16:38:47 +0900 Subject: [PATCH 11/20] =?UTF-8?q?refactor:=20=EC=BA=98=EB=A6=B0=EB=8D=94?= =?UTF-8?q?=20=EB=93=B1=EB=A1=9D,=20=EC=88=98=EC=A0=95=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20=EC=82=AC=EC=9D=B4=EC=A6=88=20=EC=A4=84=EC=9D=B4?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ScheduleAddModal.styled.ts | 27 +++++++------ .../ScheduleAddModal/ScheduleAddModal.tsx | 18 +++++---- .../ScheduleEditModal.styled.ts | 39 +++++++++++-------- .../ScheduleEditModal/ScheduleEditModal.tsx | 18 ++++----- .../TimeTableMenu/TimeTableMenu.styled.ts | 3 +- 5 files changed, 57 insertions(+), 48 deletions(-) diff --git a/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.styled.ts b/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.styled.ts index 2b1583a0b..99f0020d3 100644 --- a/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.styled.ts +++ b/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.styled.ts @@ -39,9 +39,9 @@ export const Container = styled.div<{ `; return css` - width: 400px; - min-height: 320px; - padding: 18px 22px; + width: 380px; + min-height: 300px; + padding: 16px 20px; `; }} @@ -56,7 +56,7 @@ export const Container = styled.div<{ display: flex; flex-direction: column; - row-gap: ${({ $isMobile }) => ($isMobile ? '10px' : '16px')}; + row-gap: ${({ $isMobile }) => ($isMobile ? '10px' : '10px')}; } `; @@ -65,7 +65,7 @@ export const Header = styled.div` justify-content: flex-end; width: 100%; - height: 34px; + height: 30px; margin-bottom: 18px; border-bottom: ${({ theme }) => `1px solid ${theme.color.GRAY300}`}; @@ -94,6 +94,7 @@ export const CheckboxContainer = styled.div` export const TimeSelectContainer = styled.div<{ $isMobile: boolean }>` display: flex; + justify-content: space-between; width: 100%; height: ${({ $isMobile }) => ($isMobile ? '74px' : '40px')}; @@ -116,9 +117,7 @@ export const InputWrapper = styled.div<{ $isMobile: boolean }>` align-items: center; justify-content: space-between; - width: ${({ $isMobile }) => !$isMobile && 'calc(100% - 80px)'}; - - margin-left: ${({ $isMobile }) => !$isMobile && 'auto'}; + width: ${({ $isMobile }) => !$isMobile && 'calc(100% - 70px)'}; `; export const TeamNameContainer = styled.div` @@ -145,18 +144,18 @@ export const title = css` border-radius: 10px; background-color: ${({ theme }) => theme.color.GRAY200}; - font-size: 18px; + font-size: 16px; `; export const closeButton = css` - width: 28px; - height: 28px; + width: 24px; + height: 24px; padding: 0; margin-bottom: 4px; svg { - width: 28px; - height: 28px; + width: 24px; + height: 24px; } `; @@ -184,6 +183,6 @@ export const teamPlaceName = css` `; export const submitButton = css` - width: 80px; + width: 76px; padding: 0; `; diff --git a/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.tsx b/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.tsx index 54de44fa4..3214d2371 100644 --- a/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.tsx +++ b/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.tsx @@ -76,13 +76,11 @@ const ScheduleAddModal = (props: ScheduleAddModalProps) => { - - 일정 시작 - + 일정 시작 { - + 일정 마감 { - - {!isMobile && {displayName}} + + {!isMobile && ( + + {displayName} + + )} - - - ); -}; - -const images = [ - { - id: 9283, - isExpired: false, - name: 'neon.png', - url: 'https://images.unsplash.com/photo-1508700115892-45ecd05ae2ad?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2069&q=80', - }, - { - id: 4165, - isExpired: false, - name: 'donut.png', - url: 'https://images.unsplash.com/photo-1551106652-a5bcf4b29ab6?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1965&q=80', - }, - { - id: 8729, - isExpired: false, - name: 'zXwMd93Xwz2V03M5xAw_fVmxzEwNiDv_93-xVm__902XvC-2XzOqPdR93F3Xz_24RzV01IjSwmOkVeZmIoPlLliFmMVc2__s9Xz.png', - url: 'https://images.unsplash.com/photo-1591382386627-349b692688ff?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1887&q=80', - }, - { - id: 1092, - isExpired: false, - name: 'icon.png', - url: 'https://img.icons8.com/?size=256&id=VUoFEYkLOaMn&format=png&color=1A6DFF,C822FF', - }, - { - id: 3493, - isExpired: true, - name: '만료된 사진', - url: '', - }, -]; - -export const Default: Story = { - render: () => , - args: { - images: [], - initialPage: 1, - }, -}; +import type { Meta, StoryObj } from '@storybook/react'; +import ThreadImageModal from '~/components/feed/ThreadImageModal/ThreadImageModal'; +import { useModal } from '~/hooks/useModal'; +import Button from '~/components/common/Button/Button'; + +const meta = { + title: 'feed/ThreadImageModal', + component: ThreadImageModal, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const SampleModal = () => { + const { openModal } = useModal(); + + return ( + <> + + + + ); +}; + +const images = [ + { + id: 9283, + isExpired: false, + name: 'neon.png', + url: 'https://images.unsplash.com/photo-1508700115892-45ecd05ae2ad?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2069&q=80', + }, + { + id: 4165, + isExpired: false, + name: 'donut.png', + url: 'https://images.unsplash.com/photo-1551106652-a5bcf4b29ab6?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1965&q=80', + }, + { + id: 8729, + isExpired: false, + name: 'zXwMd93Xwz2V03M5xAw_fVmxzEwNiDv_93-xVm__902XvC-2XzOqPdR93F3Xz_24RzV01IjSwmOkVeZmIoPlLliFmMVc2__s9Xz.png', + url: 'https://images.unsplash.com/photo-1591382386627-349b692688ff?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1887&q=80', + }, + { + id: 1092, + isExpired: false, + name: 'icon.png', + url: 'https://img.icons8.com/?size=256&id=VUoFEYkLOaMn&format=png&color=1A6DFF,C822FF', + }, + { + id: 3493, + isExpired: true, + name: '만료된 사진', + url: '', + }, +]; + +export const Default: Story = { + render: () => , + args: { + images: [], + initialPage: 1, + }, +}; diff --git a/frontend/src/components/feed/ThumbnailList/ThumbnailList.stories.tsx b/frontend/src/components/feed/ThumbnailList/ThumbnailList.stories.tsx index 50f0ea914..2061dbb9a 100644 --- a/frontend/src/components/feed/ThumbnailList/ThumbnailList.stories.tsx +++ b/frontend/src/components/feed/ThumbnailList/ThumbnailList.stories.tsx @@ -1,158 +1,158 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import type { PreviewImage, ThreadImage } from '~/types/feed'; -import ThumbnailList from './ThumbnailList'; - -/** - * `ThumbnailList` 은 이미지 서랍, 또는 채팅에서 사용할 수 있는 썸네일 모음집입니다. - */ -const meta = { - title: 'feed/ThumbnailList', - component: ThumbnailList, - tags: ['autodocs'], - argTypes: { - mode: { - description: - '썸네일 리스트를 어떤 용도로 사용할 지를 정할 수 있습니다. `delete`일 경우 리스트의 이미지들을 삭제할 수 있으며, `view`일 경우 리스트의 썸네일을 클릭하여 모달에 이미지를 띄울 수 있습니다.', - }, - images: { - description: - '썸네일 리스트를 보여주기 위해 사용할 이미지들의 정보입니다.', - }, - onDelete: { - description: - "**`mode = 'delete'` 일때만 필요합니다.** 이미지가 클릭되었을 때 이미지를 지우는 함수를 의미합니다.", - }, - onClick: { - description: - "**`mode = 'view'` 일때만 필요합니다.** 이미지가 클릭되었을 때 모달을 띄우는 함수를 의미합니다.", - }, - }, -} satisfies Meta; - -export default meta; - -type Story = StoryObj; - -const deleteModeImages: PreviewImage[] = [ - { - uuid: '69aaaf99-a02d-4800-a175-7314c64e2a84', - url: 'https://images.unsplash.com/photo-1508700115892-45ecd05ae2ad?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2069&q=80', - }, - { - uuid: 'aaf9a0de-8289-455e-8112-37eebc42944a', - url: 'https://images.unsplash.com/photo-1551106652-a5bcf4b29ab6?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1965&q=80', - }, - { - uuid: 'ac49b5ed-11f4-468b-b278-5880fcf7bf16', - url: 'https://images.unsplash.com/photo-1591382386627-349b692688ff?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1887&q=80', - }, - { - uuid: '3e658b3c-5664-4225-b94a-25e6cece4ac5', - url: 'https://img.icons8.com/?size=256&id=VUoFEYkLOaMn&format=png&color=1A6DFF,C822FF', - }, -]; - -const viewModeImages: ThreadImage[] = [ - { - id: 9283, - isExpired: false, - name: 'neon.png', - url: 'https://images.unsplash.com/photo-1508700115892-45ecd05ae2ad?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2069&q=80', - }, - { - id: 4165, - isExpired: false, - name: 'donut.png', - url: 'https://images.unsplash.com/photo-1551106652-a5bcf4b29ab6?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1965&q=80', - }, - { - id: 8729, - isExpired: false, - name: 'zXwMd93Xwz2V03M5xAw_fVmxzEwNiDv_93-xVm__902XvC-2XzOqPdR93F3Xz_24RzV01IjSwmOkVeZmIoPlLliFmMVc2__s9Xz.png', - url: 'https://images.unsplash.com/photo-1591382386627-349b692688ff?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1887&q=80', - }, - { - id: 1092, - isExpired: false, - name: 'icon.png', - url: 'https://img.icons8.com/?size=256&id=VUoFEYkLOaMn&format=png&color=1A6DFF,C822FF', - }, - { - id: 3493, - isExpired: true, - name: '만료된 사진', - url: '', - }, -]; - -/** - * 이미지 서랍에 사용할 경우 이 옵션을 사용하세요. `mode = 'delete'` 로 설정하시면 됩니다. - */ -export const DeletableList: Story = { - args: { - mode: 'delete', - images: [], - onDelete: (imageUuid) => { - alert(`onDelete(${imageUuid});`); - }, - onChange: () => { - alert(`onChange()`); - }, - isUploading: false, - }, -}; - -/** - * `mode = delete`이고, 이미지 개수가 최대로 올릴 수 있는 이미지 개수를 넘지 않았다면, 이미지 추가 버튼이 보이게 됩니다. - */ -export const NotMaxDeletableList: Story = { - args: { - mode: 'delete', - images: deleteModeImages.slice(0, 2), - onDelete: (imageUuid) => { - alert(`onDelete(${imageUuid});`); - }, - onChange: () => { - alert(`onChange()`); - }, - isUploading: false, - }, -}; - -export const EmptyDeletableList: Story = { - args: { - mode: 'delete', - images: [], - onDelete: (imageUuid) => { - alert(`onDelete(${imageUuid});`); - }, - onChange: () => { - alert(`onChange()`); - }, - isUploading: false, - }, -}; - -/** - * 채팅 메시지에 사용할 경우 이 옵션을 사용하세요. `mode = 'view'` 로 설정하시면 됩니다. - */ -export const ViewableList: Story = { - args: { - mode: 'view', - images: viewModeImages, - onClick: (images: ThreadImage[], selectedImage: number) => { - alert(`onClick(${JSON.stringify(images)}, ${selectedImage});`); - }, - }, -}; - -export const ViewableListSmall: Story = { - args: { - mode: 'view', - size: 'sm', - images: viewModeImages, - onClick: (images: ThreadImage[], selectedImage: number) => { - alert(`onClick(${JSON.stringify(images)}, ${selectedImage});`); - }, - }, -}; +import type { Meta, StoryObj } from '@storybook/react'; +import type { PreviewImage, ThreadImage } from '~/types/feed'; +import ThumbnailList from './ThumbnailList'; + +/** + * `ThumbnailList` 은 이미지 서랍, 또는 채팅에서 사용할 수 있는 썸네일 모음집입니다. + */ +const meta = { + title: 'feed/ThumbnailList', + component: ThumbnailList, + tags: ['autodocs'], + argTypes: { + mode: { + description: + '썸네일 리스트를 어떤 용도로 사용할 지를 정할 수 있습니다. `delete`일 경우 리스트의 이미지들을 삭제할 수 있으며, `view`일 경우 리스트의 썸네일을 클릭하여 모달에 이미지를 띄울 수 있습니다.', + }, + images: { + description: + '썸네일 리스트를 보여주기 위해 사용할 이미지들의 정보입니다.', + }, + onDelete: { + description: + "**`mode = 'delete'` 일때만 필요합니다.** 이미지가 클릭되었을 때 이미지를 지우는 함수를 의미합니다.", + }, + onClick: { + description: + "**`mode = 'view'` 일때만 필요합니다.** 이미지가 클릭되었을 때 모달을 띄우는 함수를 의미합니다.", + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +const deleteModeImages: PreviewImage[] = [ + { + uuid: '69aaaf99-a02d-4800-a175-7314c64e2a84', + url: 'https://images.unsplash.com/photo-1508700115892-45ecd05ae2ad?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2069&q=80', + }, + { + uuid: 'aaf9a0de-8289-455e-8112-37eebc42944a', + url: 'https://images.unsplash.com/photo-1551106652-a5bcf4b29ab6?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1965&q=80', + }, + { + uuid: 'ac49b5ed-11f4-468b-b278-5880fcf7bf16', + url: 'https://images.unsplash.com/photo-1591382386627-349b692688ff?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1887&q=80', + }, + { + uuid: '3e658b3c-5664-4225-b94a-25e6cece4ac5', + url: 'https://img.icons8.com/?size=256&id=VUoFEYkLOaMn&format=png&color=1A6DFF,C822FF', + }, +]; + +const viewModeImages: ThreadImage[] = [ + { + id: 9283, + isExpired: false, + name: 'neon.png', + url: 'https://images.unsplash.com/photo-1508700115892-45ecd05ae2ad?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2069&q=80', + }, + { + id: 4165, + isExpired: false, + name: 'donut.png', + url: 'https://images.unsplash.com/photo-1551106652-a5bcf4b29ab6?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1965&q=80', + }, + { + id: 8729, + isExpired: false, + name: 'zXwMd93Xwz2V03M5xAw_fVmxzEwNiDv_93-xVm__902XvC-2XzOqPdR93F3Xz_24RzV01IjSwmOkVeZmIoPlLliFmMVc2__s9Xz.png', + url: 'https://images.unsplash.com/photo-1591382386627-349b692688ff?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1887&q=80', + }, + { + id: 1092, + isExpired: false, + name: 'icon.png', + url: 'https://img.icons8.com/?size=256&id=VUoFEYkLOaMn&format=png&color=1A6DFF,C822FF', + }, + { + id: 3493, + isExpired: true, + name: '만료된 사진', + url: '', + }, +]; + +/** + * 이미지 서랍에 사용할 경우 이 옵션을 사용하세요. `mode = 'delete'` 로 설정하시면 됩니다. + */ +export const DeletableList: Story = { + args: { + mode: 'delete', + images: [], + onDelete: (imageUuid) => { + alert(`onDelete(${imageUuid});`); + }, + onChange: () => { + alert(`onChange()`); + }, + isUploading: false, + }, +}; + +/** + * `mode = delete`이고, 이미지 개수가 최대로 올릴 수 있는 이미지 개수를 넘지 않았다면, 이미지 추가 버튼이 보이게 됩니다. + */ +export const NotMaxDeletableList: Story = { + args: { + mode: 'delete', + images: deleteModeImages.slice(0, 2), + onDelete: (imageUuid) => { + alert(`onDelete(${imageUuid});`); + }, + onChange: () => { + alert(`onChange()`); + }, + isUploading: false, + }, +}; + +export const EmptyDeletableList: Story = { + args: { + mode: 'delete', + images: [], + onDelete: (imageUuid) => { + alert(`onDelete(${imageUuid});`); + }, + onChange: () => { + alert(`onChange()`); + }, + isUploading: false, + }, +}; + +/** + * 채팅 메시지에 사용할 경우 이 옵션을 사용하세요. `mode = 'view'` 로 설정하시면 됩니다. + */ +export const ViewableList: Story = { + args: { + mode: 'view', + images: viewModeImages, + onClick: (images: ThreadImage[], selectedImage: number) => { + alert(`onClick(${JSON.stringify(images)}, ${selectedImage});`); + }, + }, +}; + +export const ViewableListSmall: Story = { + args: { + mode: 'view', + size: 'sm', + images: viewModeImages, + onClick: (images: ThreadImage[], selectedImage: number) => { + alert(`onClick(${JSON.stringify(images)}, ${selectedImage});`); + }, + }, +}; diff --git a/frontend/src/components/feed/ViewableThumbnail/ViewableThumbnail.stories.tsx b/frontend/src/components/feed/ViewableThumbnail/ViewableThumbnail.stories.tsx index 62627e2c3..298677c30 100644 --- a/frontend/src/components/feed/ViewableThumbnail/ViewableThumbnail.stories.tsx +++ b/frontend/src/components/feed/ViewableThumbnail/ViewableThumbnail.stories.tsx @@ -1,67 +1,67 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import ViewableThumbnail from './ViewableThumbnail'; - -/** - * `ViewableThumbnail` 은 이미지 업로드 서랍에서 사용할 수 있는 단일 이미지 썸네일 컴포넌트입니다. 이미지를 클릭할 경우 모달을 띄우기 위한 함수를 호출합니다. - */ -const meta = { - title: 'feed/ViewableThumbnail', - component: ViewableThumbnail, - tags: ['autodocs'], - argTypes: { - image: { - description: '썸네일로 보여줄 이미지의 정보', - }, - onClick: { - description: - '썸네일에 해당하는 이미지가 클릭되었을 때, 해당 썸네일을 이미지 모달에 띄우기 위한 함수', - }, - }, -} satisfies Meta; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = { - args: { - image: { - id: 2918, - isExpired: false, - name: 'rabbit.png', - url: 'https://images.unsplash.com/photo-1599169713100-120531cef331?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1887&q=80', - }, - onClick: () => { - alert('onClick()'); - }, - }, -}; - -export const Small: Story = { - args: { - image: { - id: 1145, - isExpired: false, - name: 'rabbit.png', - url: 'https://images.unsplash.com/photo-1599169713100-120531cef331?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1887&q=80', - }, - size: 'sm', - onClick: () => { - alert('onClick()'); - }, - }, -}; - -export const ExpiredThumbnail: Story = { - args: { - image: { - id: 1029, - isExpired: true, - name: '만료된 이미지', - url: '', - }, - onClick: () => { - alert('onClick()'); - }, - }, -}; +import type { Meta, StoryObj } from '@storybook/react'; +import ViewableThumbnail from './ViewableThumbnail'; + +/** + * `ViewableThumbnail` 은 이미지 업로드 서랍에서 사용할 수 있는 단일 이미지 썸네일 컴포넌트입니다. 이미지를 클릭할 경우 모달을 띄우기 위한 함수를 호출합니다. + */ +const meta = { + title: 'feed/ViewableThumbnail', + component: ViewableThumbnail, + tags: ['autodocs'], + argTypes: { + image: { + description: '썸네일로 보여줄 이미지의 정보', + }, + onClick: { + description: + '썸네일에 해당하는 이미지가 클릭되었을 때, 해당 썸네일을 이미지 모달에 띄우기 위한 함수', + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + image: { + id: 2918, + isExpired: false, + name: 'rabbit.png', + url: 'https://images.unsplash.com/photo-1599169713100-120531cef331?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1887&q=80', + }, + onClick: () => { + alert('onClick()'); + }, + }, +}; + +export const Small: Story = { + args: { + image: { + id: 1145, + isExpired: false, + name: 'rabbit.png', + url: 'https://images.unsplash.com/photo-1599169713100-120531cef331?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1887&q=80', + }, + size: 'sm', + onClick: () => { + alert('onClick()'); + }, + }, +}; + +export const ExpiredThumbnail: Story = { + args: { + image: { + id: 1029, + isExpired: true, + name: '만료된 이미지', + url: '', + }, + onClick: () => { + alert('onClick()'); + }, + }, +}; diff --git a/frontend/src/components/landing/FeedDecoration/FeedDecoration.stories.tsx b/frontend/src/components/landing/FeedDecoration/FeedDecoration.stories.tsx index b9f7ca4ca..f52862b5b 100644 --- a/frontend/src/components/landing/FeedDecoration/FeedDecoration.stories.tsx +++ b/frontend/src/components/landing/FeedDecoration/FeedDecoration.stories.tsx @@ -1,40 +1,40 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import FeedDecoration from './FeedDecoration'; - -/** - * `FeedDecoration` 컴포넌트는 랜딩 페이지의 장식 컴포넌트인 `IntroCardPile` 의 두 번째 장면 해당하는 컴포넌트입니다. - * **팀 피드**에 대한 모형을 애니메이션과 함께 보여줍니다. - */ -const meta = { - title: 'landing/FeedDecoration', - component: FeedDecoration, - tags: ['autodocs'], - decorators: [ - (Story) => { - return ( -
- -
- ); - }, - ], -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -/** - * 하늘색의 컨테이너는 본 컴포넌트에 포함되지 않습니다. - */ -export const Default: Story = { - args: {}, -}; +import type { Meta, StoryObj } from '@storybook/react'; +import FeedDecoration from './FeedDecoration'; + +/** + * `FeedDecoration` 컴포넌트는 랜딩 페이지의 장식 컴포넌트인 `IntroCardPile` 의 두 번째 장면 해당하는 컴포넌트입니다. + * **팀 피드**에 대한 모형을 애니메이션과 함께 보여줍니다. + */ +const meta = { + title: 'landing/FeedDecoration', + component: FeedDecoration, + tags: ['autodocs'], + decorators: [ + (Story) => { + return ( +
+ +
+ ); + }, + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +/** + * 하늘색의 컨테이너는 본 컴포넌트에 포함되지 않습니다. + */ +export const Default: Story = { + args: {}, +}; diff --git a/frontend/src/components/landing/FileDriveDecoration/FileDriveDecoration.stories.tsx b/frontend/src/components/landing/FileDriveDecoration/FileDriveDecoration.stories.tsx index b45edf3ae..0d6a99e97 100644 --- a/frontend/src/components/landing/FileDriveDecoration/FileDriveDecoration.stories.tsx +++ b/frontend/src/components/landing/FileDriveDecoration/FileDriveDecoration.stories.tsx @@ -1,41 +1,41 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import FileDriveDecoration from './FileDriveDecoration'; - -/** - * `FileDriveDecoration` 컴포넌트는 랜딩 페이지의 장식 컴포넌트인 `IntroCardPile` 의 세 번째 장면 해당하는 컴포넌트입니다. - * **팀 드라이브**에 대한 모형을 애니메이션과 함께 보여줍니다. - * 이 컴포넌트를 작성하는 시점에서, 팀 드라이브의 UI는 구상이 되어 있지 않았기에, 추후 구상이 완료될 경우 이 UI는 바뀔 수도 있습니다. - */ -const meta = { - title: 'landing/FileDriveDecoration', - component: FileDriveDecoration, - tags: ['autodocs'], - decorators: [ - (Story) => { - return ( -
- -
- ); - }, - ], -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -/** - * 하늘색의 컨테이너는 본 컴포넌트에 포함되지 않습니다. - */ -export const Default: Story = { - args: {}, -}; +import type { Meta, StoryObj } from '@storybook/react'; +import FileDriveDecoration from './FileDriveDecoration'; + +/** + * `FileDriveDecoration` 컴포넌트는 랜딩 페이지의 장식 컴포넌트인 `IntroCardPile` 의 세 번째 장면 해당하는 컴포넌트입니다. + * **팀 드라이브**에 대한 모형을 애니메이션과 함께 보여줍니다. + * 이 컴포넌트를 작성하는 시점에서, 팀 드라이브의 UI는 구상이 되어 있지 않았기에, 추후 구상이 완료될 경우 이 UI는 바뀔 수도 있습니다. + */ +const meta = { + title: 'landing/FileDriveDecoration', + component: FileDriveDecoration, + tags: ['autodocs'], + decorators: [ + (Story) => { + return ( +
+ +
+ ); + }, + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +/** + * 하늘색의 컨테이너는 본 컴포넌트에 포함되지 않습니다. + */ +export const Default: Story = { + args: {}, +}; diff --git a/frontend/src/components/landing/IntroCardPile/IntroCardPile.stories.tsx b/frontend/src/components/landing/IntroCardPile/IntroCardPile.stories.tsx index c14e36a26..b72afd1af 100644 --- a/frontend/src/components/landing/IntroCardPile/IntroCardPile.stories.tsx +++ b/frontend/src/components/landing/IntroCardPile/IntroCardPile.stories.tsx @@ -1,31 +1,31 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import IntroCardPile from './IntroCardPile'; - -/** - * `IntroCardPile` 컴포넌트는 랜딩 페이지의 부속품에 해당하는 컴포넌트로, - * 여러 장의 카드를 이용하여 팀바팀 서비스의 간략화된 UI를 미리 보여줍니다. - * 랜딩 페이지의 왼쪽에 배치하여 메인 디자인 요소로 사용될 것입니다. - */ -const meta = { - title: 'landing/IntroCardPile', - component: IntroCardPile, - tags: ['autodocs'], -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - args: {}, -}; - -/** - * 이 옵션은 랜딩 페이지와 동일한 배경을 보여주어야 하지만 애니메이션을 이용하여 사용자의 시선을 끌기에는 적합하지 않은 페이지에 적합합니다. - * - * 예를 들면, 팀 초대 링크를 입력하는 페이지가 있습니다. - */ -export const NoAnimation: Story = { - args: { - animation: false, - }, -}; +import type { Meta, StoryObj } from '@storybook/react'; +import IntroCardPile from './IntroCardPile'; + +/** + * `IntroCardPile` 컴포넌트는 랜딩 페이지의 부속품에 해당하는 컴포넌트로, + * 여러 장의 카드를 이용하여 팀바팀 서비스의 간략화된 UI를 미리 보여줍니다. + * 랜딩 페이지의 왼쪽에 배치하여 메인 디자인 요소로 사용될 것입니다. + */ +const meta = { + title: 'landing/IntroCardPile', + component: IntroCardPile, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: {}, +}; + +/** + * 이 옵션은 랜딩 페이지와 동일한 배경을 보여주어야 하지만 애니메이션을 이용하여 사용자의 시선을 끌기에는 적합하지 않은 페이지에 적합합니다. + * + * 예를 들면, 팀 초대 링크를 입력하는 페이지가 있습니다. + */ +export const NoAnimation: Story = { + args: { + animation: false, + }, +}; diff --git a/frontend/src/components/landing/TeamCalendarDecoration/TeamCalendarDecoration.stories.tsx b/frontend/src/components/landing/TeamCalendarDecoration/TeamCalendarDecoration.stories.tsx index 4e32191ef..7f554135f 100644 --- a/frontend/src/components/landing/TeamCalendarDecoration/TeamCalendarDecoration.stories.tsx +++ b/frontend/src/components/landing/TeamCalendarDecoration/TeamCalendarDecoration.stories.tsx @@ -1,53 +1,53 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import TeamCalendarDecoration from './TeamCalendarDecoration'; - -/** - * `TeamCalendarDecoration` 컴포넌트는 랜딩 페이지의 장식 컴포넌트인 `IntroCardPile` 의 첫 번째 장면 해당하는 컴포넌트입니다. - * **팀 캘린더**에 대한 모형을 애니메이션과 함께 보여줍니다. - */ -const meta = { - title: 'landing/TeamCalendarDecoration', - component: TeamCalendarDecoration, - tags: ['autodocs'], - decorators: [ - (Story) => { - return ( -
- -
- ); - }, - ], -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -/** - * 하늘색의 컨테이너는 본 컴포넌트에 포함되지 않습니다. - */ -export const Default: Story = { - args: {}, -}; - -/** - * 이 옵션은 이 컴포넌트가 주목을 끌어서는 안 되는 페이지에 사용하기에 적합합니다. - * 랜딩 페이지를 제외한 페이지에서는 이 옵션이 사용될 것입니다. - * - * 참고로, 다른 `IntroCardPile` 의 장면들의 경우 이 옵션이 없는데, - * 이는 `IntroCardPile` 에서 애니메이션을 보여주지 않는 옵션이 켜졌을 경우 다른 장면들은 랜더링될 일이 없기 때문입니다. - */ -export const NoAnimation: Story = { - args: { - animation: false, - }, -}; +import type { Meta, StoryObj } from '@storybook/react'; +import TeamCalendarDecoration from './TeamCalendarDecoration'; + +/** + * `TeamCalendarDecoration` 컴포넌트는 랜딩 페이지의 장식 컴포넌트인 `IntroCardPile` 의 첫 번째 장면 해당하는 컴포넌트입니다. + * **팀 캘린더**에 대한 모형을 애니메이션과 함께 보여줍니다. + */ +const meta = { + title: 'landing/TeamCalendarDecoration', + component: TeamCalendarDecoration, + tags: ['autodocs'], + decorators: [ + (Story) => { + return ( +
+ +
+ ); + }, + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +/** + * 하늘색의 컨테이너는 본 컴포넌트에 포함되지 않습니다. + */ +export const Default: Story = { + args: {}, +}; + +/** + * 이 옵션은 이 컴포넌트가 주목을 끌어서는 안 되는 페이지에 사용하기에 적합합니다. + * 랜딩 페이지를 제외한 페이지에서는 이 옵션이 사용될 것입니다. + * + * 참고로, 다른 `IntroCardPile` 의 장면들의 경우 이 옵션이 없는데, + * 이는 `IntroCardPile` 에서 애니메이션을 보여주지 않는 옵션이 켜졌을 경우 다른 장면들은 랜더링될 일이 없기 때문입니다. + */ +export const NoAnimation: Story = { + args: { + animation: false, + }, +}; diff --git a/frontend/src/components/link/EmptyLinkPlaceholder/EmptyLinkPlaceholder.stories.tsx b/frontend/src/components/link/EmptyLinkPlaceholder/EmptyLinkPlaceholder.stories.tsx index f8fbafe17..797c8fec6 100644 --- a/frontend/src/components/link/EmptyLinkPlaceholder/EmptyLinkPlaceholder.stories.tsx +++ b/frontend/src/components/link/EmptyLinkPlaceholder/EmptyLinkPlaceholder.stories.tsx @@ -1,19 +1,19 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import EmptyLinkPlaceholder from './EmptyLinkPlaceholder'; - -/** - * `EmptyLinkPlaceholder` 는 `LinkTable` 컴포넌트에 있는 링크가 하나도 없을 경우, 대신 보여줄 화면을 구성하는 컴포넌트입니다. - */ -const meta = { - title: 'Link/EmptyLinkPlaceholder', - component: EmptyLinkPlaceholder, - tags: ['autodocs'], -} satisfies Meta; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = { - args: {}, -}; +import type { Meta, StoryObj } from '@storybook/react'; +import EmptyLinkPlaceholder from './EmptyLinkPlaceholder'; + +/** + * `EmptyLinkPlaceholder` 는 `LinkTable` 컴포넌트에 있는 링크가 하나도 없을 경우, 대신 보여줄 화면을 구성하는 컴포넌트입니다. + */ +const meta = { + title: 'Link/EmptyLinkPlaceholder', + component: EmptyLinkPlaceholder, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: {}, +}; diff --git a/frontend/src/components/link/LinkTable/LinkTable.stories.tsx b/frontend/src/components/link/LinkTable/LinkTable.stories.tsx index 7f28402f4..c26fc59a9 100644 --- a/frontend/src/components/link/LinkTable/LinkTable.stories.tsx +++ b/frontend/src/components/link/LinkTable/LinkTable.stories.tsx @@ -1,36 +1,36 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import TeamLinkTable from './LinkTable'; - -/** - * `LinkTable` 는 팀 링크 목록을 표시할 메뉴 컴포넌트입니다. - */ -const meta = { - title: 'Link/TeamLinkTable', - component: TeamLinkTable, - tags: ['autodocs'], - decorators: [ - (Story) => ( -
- -
- ), - ], -} satisfies Meta; - -export default meta; - -type Story = StoryObj; - -/** - * 회색 배경은 컴포넌트를 고정시키고 크기를 조절하기 위해 사용한 것으로, 컴포넌트에는 포함되지 않습니다. - */ -export const Default: Story = { - args: {}, -}; +import type { Meta, StoryObj } from '@storybook/react'; +import TeamLinkTable from './LinkTable'; + +/** + * `LinkTable` 는 팀 링크 목록을 표시할 메뉴 컴포넌트입니다. + */ +const meta = { + title: 'Link/TeamLinkTable', + component: TeamLinkTable, + tags: ['autodocs'], + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * 회색 배경은 컴포넌트를 고정시키고 크기를 조절하기 위해 사용한 것으로, 컴포넌트에는 포함되지 않습니다. + */ +export const Default: Story = { + args: {}, +}; diff --git a/frontend/src/components/team_calendar/FakeScheduleBarsScreen/FakeScheduleBarsScreen.stories.tsx b/frontend/src/components/team_calendar/FakeScheduleBarsScreen/FakeScheduleBarsScreen.stories.tsx index 66a9c0bc4..daa46eea2 100644 --- a/frontend/src/components/team_calendar/FakeScheduleBarsScreen/FakeScheduleBarsScreen.stories.tsx +++ b/frontend/src/components/team_calendar/FakeScheduleBarsScreen/FakeScheduleBarsScreen.stories.tsx @@ -1,127 +1,127 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import type { ComponentType } from 'react'; -import FakeScheduleBarsScreen from '~/components/team_calendar/FakeScheduleBarsScreen/FakeScheduleBarsScreen'; -import type { GeneratedScheduleBar } from '~/types/schedule'; - -/** - * `FakeScheduleBarsScreen` 는 캘린더 바의 드래그 기능을 구현하기 위해 사용자에게 보여주는 가짜 캘린더 바로 구성된, 시각적인 컴포넌트입니다. - * - * `mode = schedule`일 경우, 마우스 조작을 통해 x, y 값을 계속해서 업데이트하면 마우스를 따라다니듯이 작동하도록 만들 수 있습니다. x, y 값을 변경하면서 컴포넌트의 변화를 테스트하세요. - */ -const meta = { - title: 'Schedule/FakeScheduleBarsScreen', - component: FakeScheduleBarsScreen, - tags: ['autodocs'], - decorators: [ - (Story: ComponentType) => ( -
- -
- ), - ], - argTypes: { - mode: { - description: - '이 컴포넌트의 모드를 의미합니다. 사용 목적에 따라 `schedule`과 `indicator` 중 하나를 명시해 주세요.', - }, - scheduleBars: { - description: '렌더링할 스케줄 바들의 정보를 의미합니다.', - }, - relativeX: { - description: - '기존 좌표에서 좌우로 얼마나 이동한 위치에 렌더링 시킬 것인지를 의미합니다. 이 값이 양수이면 기존 좌표에서 수치만큼 오른쪽으로 이동하여 렌더링되고, 음수일 경우 왼쪽으로 이동하여 렌더링됩니다. 단위는 픽셀(px)입니다. **이 프로퍼티는 `mode = schedule`일 때만 사용할 수 있습니다.**', - }, - relativeY: { - description: - '기존 좌표에서 상하로 얼마나 이동한 위치에 렌더링 시킬 것인지를 의미합니다. 이 값이 양수이면 기존 좌표에서 수치만큼 아래쪽으로 이동하여 렌더링되고, 음수일 경우 위쪽으로 이동하여 렌더링됩니다. 단위는 픽셀(px)입니다. **이 프로퍼티는 `mode = schedule`일 때만 사용할 수 있습니다.**', - }, - }, -} satisfies Meta; - -export default meta; - -type Story = StoryObj; - -const scheduleBars: GeneratedScheduleBar[] = [ - { - id: '1', - scheduleId: 1105, - title: '바쁜 필립의 3주짜리 일정', - row: 0, - column: 1, - duration: 6, - level: 0, - roundedStart: true, - roundedEnd: false, - schedule: { - id: 1105, - title: '바쁜 필립의 3주짜리 일정', - startDateTime: '2023-06-26 00:00', - endDateTime: '2023-07-12 23:59', - }, - }, - { - id: '2', - scheduleId: 1105, - title: '바쁜 필립의 3주짜리 일정', - row: 1, - column: 0, - duration: 7, - level: 0, - roundedStart: false, - roundedEnd: false, - schedule: { - id: 1105, - title: '바쁜 필립의 3주짜리 일정', - startDateTime: '2023-06-26 00:00', - endDateTime: '2023-07-12 23:59', - }, - }, - { - id: '3', - scheduleId: 1105, - title: '바쁜 필립의 3주짜리 일정', - row: 2, - column: 0, - duration: 4, - level: 0, - roundedStart: false, - roundedEnd: true, - schedule: { - id: 1105, - title: '바쁜 필립의 3주짜리 일정', - startDateTime: '2023-06-26 00:00', - endDateTime: '2023-07-12 23:59', - }, - }, -]; - -/** - * 이 모드는 가짜 스케줄 바를 보여줘야 할 경우에 사용합니다. - */ -export const ScheduleMode: Story = { - args: { - mode: 'schedule', - scheduleBars, - relativeX: 0, - relativeY: 0, - }, -}; - -/** - * 이 모드는 스케줄 바가 놓일 위치를 시각적으로 보여줘야 할 경우에 사용합니다. - */ -export const IndicatorMode: Story = { - args: { - mode: 'indicator', - scheduleBars, - }, -}; +import type { Meta, StoryObj } from '@storybook/react'; +import type { ComponentType } from 'react'; +import FakeScheduleBarsScreen from '~/components/team_calendar/FakeScheduleBarsScreen/FakeScheduleBarsScreen'; +import type { GeneratedScheduleBar } from '~/types/schedule'; + +/** + * `FakeScheduleBarsScreen` 는 캘린더 바의 드래그 기능을 구현하기 위해 사용자에게 보여주는 가짜 캘린더 바로 구성된, 시각적인 컴포넌트입니다. + * + * `mode = schedule`일 경우, 마우스 조작을 통해 x, y 값을 계속해서 업데이트하면 마우스를 따라다니듯이 작동하도록 만들 수 있습니다. x, y 값을 변경하면서 컴포넌트의 변화를 테스트하세요. + */ +const meta = { + title: 'Schedule/FakeScheduleBarsScreen', + component: FakeScheduleBarsScreen, + tags: ['autodocs'], + decorators: [ + (Story: ComponentType) => ( +
+ +
+ ), + ], + argTypes: { + mode: { + description: + '이 컴포넌트의 모드를 의미합니다. 사용 목적에 따라 `schedule`과 `indicator` 중 하나를 명시해 주세요.', + }, + scheduleBars: { + description: '렌더링할 스케줄 바들의 정보를 의미합니다.', + }, + relativeX: { + description: + '기존 좌표에서 좌우로 얼마나 이동한 위치에 렌더링 시킬 것인지를 의미합니다. 이 값이 양수이면 기존 좌표에서 수치만큼 오른쪽으로 이동하여 렌더링되고, 음수일 경우 왼쪽으로 이동하여 렌더링됩니다. 단위는 픽셀(px)입니다. **이 프로퍼티는 `mode = schedule`일 때만 사용할 수 있습니다.**', + }, + relativeY: { + description: + '기존 좌표에서 상하로 얼마나 이동한 위치에 렌더링 시킬 것인지를 의미합니다. 이 값이 양수이면 기존 좌표에서 수치만큼 아래쪽으로 이동하여 렌더링되고, 음수일 경우 위쪽으로 이동하여 렌더링됩니다. 단위는 픽셀(px)입니다. **이 프로퍼티는 `mode = schedule`일 때만 사용할 수 있습니다.**', + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +const scheduleBars: GeneratedScheduleBar[] = [ + { + id: '1', + scheduleId: 1105, + title: '바쁜 필립의 3주짜리 일정', + row: 0, + column: 1, + duration: 6, + level: 0, + roundedStart: true, + roundedEnd: false, + schedule: { + id: 1105, + title: '바쁜 필립의 3주짜리 일정', + startDateTime: '2023-06-26 00:00', + endDateTime: '2023-07-12 23:59', + }, + }, + { + id: '2', + scheduleId: 1105, + title: '바쁜 필립의 3주짜리 일정', + row: 1, + column: 0, + duration: 7, + level: 0, + roundedStart: false, + roundedEnd: false, + schedule: { + id: 1105, + title: '바쁜 필립의 3주짜리 일정', + startDateTime: '2023-06-26 00:00', + endDateTime: '2023-07-12 23:59', + }, + }, + { + id: '3', + scheduleId: 1105, + title: '바쁜 필립의 3주짜리 일정', + row: 2, + column: 0, + duration: 4, + level: 0, + roundedStart: false, + roundedEnd: true, + schedule: { + id: 1105, + title: '바쁜 필립의 3주짜리 일정', + startDateTime: '2023-06-26 00:00', + endDateTime: '2023-07-12 23:59', + }, + }, +]; + +/** + * 이 모드는 가짜 스케줄 바를 보여줘야 할 경우에 사용합니다. + */ +export const ScheduleMode: Story = { + args: { + mode: 'schedule', + scheduleBars, + relativeX: 0, + relativeY: 0, + }, +}; + +/** + * 이 모드는 스케줄 바가 놓일 위치를 시각적으로 보여줘야 할 경우에 사용합니다. + */ +export const IndicatorMode: Story = { + args: { + mode: 'indicator', + scheduleBars, + }, +}; diff --git a/frontend/src/components/user/AccountDeleteModal/AccountDeleteModal.stories.tsx b/frontend/src/components/user/AccountDeleteModal/AccountDeleteModal.stories.tsx index b2e73fc79..98fd1b9a1 100644 --- a/frontend/src/components/user/AccountDeleteModal/AccountDeleteModal.stories.tsx +++ b/frontend/src/components/user/AccountDeleteModal/AccountDeleteModal.stories.tsx @@ -1,33 +1,33 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import AccountDeleteModal from './AccountDeleteModal'; -import { useModal } from '~/hooks/useModal'; -import Button from '~/components/common/Button/Button'; - -/** - * `AccountDeleteModal` 는 계정 탈퇴 진행을 위한 모달입니다. - */ -const meta = { - title: 'user/AccountDeleteModal', - component: AccountDeleteModal, - tags: ['autodocs'], -} satisfies Meta; - -export default meta; - -type Story = StoryObj; - -const SampleModal = () => { - const { openModal } = useModal(); - - return ( - <> - - - - ); -}; - -export const Default: Story = { - render: () => , - args: {}, -}; +import type { Meta, StoryObj } from '@storybook/react'; +import AccountDeleteModal from './AccountDeleteModal'; +import { useModal } from '~/hooks/useModal'; +import Button from '~/components/common/Button/Button'; + +/** + * `AccountDeleteModal` 는 계정 탈퇴 진행을 위한 모달입니다. + */ +const meta = { + title: 'user/AccountDeleteModal', + component: AccountDeleteModal, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +const SampleModal = () => { + const { openModal } = useModal(); + + return ( + <> + + + + ); +}; + +export const Default: Story = { + render: () => , + args: {}, +}; From 4c7c560234d5ad609ebe9cc5aa5068837886b279 Mon Sep 17 00:00:00 2001 From: Rulu Date: Fri, 3 Jan 2025 16:46:42 +0900 Subject: [PATCH 13/20] =?UTF-8?q?feat:=20switch=20=EA=B3=B5=EC=9A=A9=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 --- .../common/Switch/Switch.stories.tsx | 238 ++++++++++++++++++ .../components/common/Switch/Switch.styled.ts | 222 ++++++++++++++++ .../src/components/common/Switch/Switch.tsx | 148 +++++++++++ frontend/src/types/size.ts | 2 + 4 files changed, 610 insertions(+) create mode 100644 frontend/src/components/common/Switch/Switch.stories.tsx create mode 100644 frontend/src/components/common/Switch/Switch.styled.ts create mode 100644 frontend/src/components/common/Switch/Switch.tsx diff --git a/frontend/src/components/common/Switch/Switch.stories.tsx b/frontend/src/components/common/Switch/Switch.stories.tsx new file mode 100644 index 000000000..59ea68a10 --- /dev/null +++ b/frontend/src/components/common/Switch/Switch.stories.tsx @@ -0,0 +1,238 @@ +import { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; + +import Switch from './Switch'; + +/** + * 공용 Switch 컴포넌트 + */ +const meta: Meta = { + title: 'Common/Switch', + component: Switch, + tags: ['autodocs'], + render: (args) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const [checked, setChecked] = useState(false); + + const handleChange = () => { + setChecked((prevChecked) => !prevChecked); + }; + + return ; + }, + argTypes: { + checked: { control: 'boolean' }, + onChange: { action: 'clicked' }, + size: { + control: { + type: 'select', + options: ['xs', 'sm', 'md', 'lg'], + }, + description: '스위치 크기', + }, + variant: { + control: { + type: 'select', + options: ['solid', 'raised'], + }, + description: + 'solid : track안에 thumb이 들어 있는 유형, raised : track보다 thumb이 큰 유형', + }, + readonly: { control: 'boolean' }, + disabled: { control: 'boolean' }, + description: { + control: 'text', + description: '스위치에 대한 설명이 되는 컴포넌트', + }, + descriptionPosition: { + control: { + type: 'select', + options: ['top', 'bottom', 'left', 'right'], + }, + description: '설명 위치(상/하/좌/우)', + }, + onLabel: { + control: 'text', + description: + '스위치가 켜져 있을 때의 트랙 라벨 내 들어갈 텍스트 또는 아이콘', + }, + offLabel: { + control: 'text', + description: + '스위치가 꺼져 있을 때의 트랙 라벨 내 들어갈 텍스트 또는 아이콘', + }, + onThumb: { + control: 'text', + description: '스위치가 켜져 있을 때의 thumb 내 들어갈 텍스트 또는 아이콘', + }, + offThumb: { + control: 'text', + description: '스위치가 꺼져 있을 때의 thumb 내 들어갈 텍스트 또는 아이콘', + }, + onColor: { + control: 'color', + description: '스위치가 켜져 있을 때의 트랙 색상', + }, + offColor: { + control: 'color', + description: '스위치가 꺼져 있을 때의 트랙 색상', + }, + thumbOnColor: { + control: 'color', + description: '스위치가 켜져 있을 때의 thumb 색상', + }, + thumbOffColor: { + control: 'color', + description: '스위치가 꺼져 있을 때의 thumb 색상', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Solid: Story = { + args: { + size: 'md', + checked: false, + onChange: () => console.log('Switch Changed'), + }, +}; + +export const Raised: Story = { + args: { + size: 'md', + checked: false, + onChange: () => console.log('Switch Changed'), + variant: 'raised', + }, +}; + +export const ExtraSmall: Story = { + args: { + size: 'xs', + checked: false, + onChange: () => console.log('Switch Changed'), + }, +}; + +export const Small: Story = { + args: { + size: 'sm', + checked: false, + onChange: () => console.log('Switch Changed'), + }, +}; + +export const Medium: Story = { + args: { + size: 'md', + checked: false, + onChange: () => console.log('Switch Changed'), + }, +}; + +export const Large: Story = { + args: { + size: 'lg', + checked: false, + onChange: () => console.log('Switch Changed'), + }, +}; + +export const WithDescription: Story = { + args: { + size: 'md', + checked: false, + onChange: () => console.log('Switch Changed'), + description:
이것은 설명입니다.
, + }, +}; + +export const WithDescriptionComponent: Story = { + args: { + size: 'md', + checked: false, + onChange: () => console.log('Switch changed'), + description: ( +
+ 로그인 +
+ ), + }, +}; + +export const WithCustomColor: Story = { + args: { + size: 'md', + checked: false, + onChange: () => console.log('Switch Changed'), + onColor: 'rgb(21, 99, 223)', + offColor: '#99b4d9', + thumbOnColor: '#1a0cdc', + thumbOffColor: '#040d32', + }, +}; + +export const WithThumbText: Story = { + args: { + size: 'lg', + checked: false, + onChange: () => console.log('Switch Changed'), + onThumb: 'ON', + offThumb: 'OFF', + }, +}; + +export const WithInnerLabel: Story = { + args: { + size: 'lg', + checked: false, + onChange: () => console.log('Switch Changed'), + onLabel: '자동 업데이트', + offLabel: '수동 업데이트', + }, +}; + +export const Disabled: Story = { + args: { + size: 'md', + checked: false, + onChange: () => console.log('Switch Changed'), + description: 'Disabled switch', + disabled: true, + }, +}; + +export const ReadOnly: Story = { + args: { + size: 'md', + checked: false, + onChange: () => console.log('Switch Changed'), + description: 'Read-only switch', + readonly: true, + }, +}; + +export const Playground: Story = { + render: () => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const [checked, setChecked] = useState(false); + + const handleChange = () => { + console.log('Switch Changed'); + setChecked((prevChecked) => !prevChecked); + }; + + return ( +
+ + {checked &&
💡
} +
+ ); + }, + args: { + size: 'md', + variant: 'solid', + }, +}; diff --git a/frontend/src/components/common/Switch/Switch.styled.ts b/frontend/src/components/common/Switch/Switch.styled.ts new file mode 100644 index 000000000..05b7d0095 --- /dev/null +++ b/frontend/src/components/common/Switch/Switch.styled.ts @@ -0,0 +1,222 @@ +import styled, { css } from 'styled-components'; +import theme from '~/styles/theme'; +import type { SwitchSize } from '~/types/size'; + +const backgroundColor = ( + $isOn: boolean, + $offColor?: string, + $onColor?: string, +) => css` + background-color: ${$isOn + ? $onColor || theme.color.BLACK + : $offColor || theme.color.GRAY200}; +`; + +const flexStyles = { + column: css` + flex-direction: column; + `, + row: css` + flex-direction: row; + `, +}; + +const sizeStyles = { + xs: css` + min-width: 28px; + height: 16px; + `, + sm: css` + min-width: 38px; + height: 20px; + `, + md: css` + min-width: 48px; + height: 24px; + `, + lg: css` + min-width: 58px; + height: 30px; + `, +}; + +const thumbSizes = { + xs: css` + font-size: 4px; + width: 12px; + height: 12px; + `, + sm: css` + font-size: 6px; + width: 16px; + height: 16px; + `, + md: css` + font-size: 8px; + width: 20px; + height: 20px; + `, + lg: css` + font-size: 10px; + width: 25px; + height: 25px; + `, +}; + +export const ContainerDiv = styled.div<{ + $descriptionPosition: 'top' | 'bottom' | 'left' | 'right'; +}>` + display: flex; + align-items: center; + gap: 10px; + + ${({ $descriptionPosition }) => + ['top', 'bottom'].includes($descriptionPosition) + ? flexStyles.column + : flexStyles.row} +`; + +export const Label = styled.label<{ + $size: SwitchSize; +}>` + width: auto; + display: flex; + align-items: center; + cursor: pointer; + + ${({ $size }) => sizeStyles[$size]} +`; + +export const TrackDiv = styled.div<{ + $variant: 'solid' | 'raised'; + $isOn: boolean; + $offColor: string | undefined; + $onColor: string | undefined; +}>` + position: relative; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + border-radius: 25px; + transition: background 0.4s; + background-color: ${({ $offColor }) => + $offColor ? $offColor : theme.color.GRAY200}; + + ${({ $variant }) => { + if ($variant === 'solid') + return css` + height: 100%; + `; + + return css` + height: 40%; + `; + }} + + ${({ $isOn, $offColor, $onColor }) => + backgroundColor($isOn, $offColor, $onColor)}; +`; + +export const LabelSpan = styled.span<{ + $size: SwitchSize; + $isOn: boolean; +}>` + font-size: 10px; + white-space: nowrap; + color: ${theme.color.WHITE}; + flex-grow: 1; + text-align: center; + + ${({ $size }) => { + if ($size === 'xs' || $size === 'sm') + return css` + font-size: 8px; + `; + }} + + ${({ $isOn }) => { + if ($isOn) + return css` + margin-left: 10px; + `; + + return css` + margin-right: 10px; + `; + }} + + + ${({ $size, $isOn }) => + css` + margin-${$isOn ? 'right' : 'left'}: calc(${ + $size === 'xs' + ? '18px' + : $size === 'sm' + ? '23px' + : $size === 'md' + ? '28px' + : '34px' + }); + `}; +`; + +export const ThumbSpan = styled.span<{ + $variant: 'solid' | 'raised'; + $size: SwitchSize; + $isOn: boolean; + $offColor: string | undefined; + $onColor: string | undefined; +}>` + position: absolute; + display: flex; + justify-content: center; + align-items: center; + border-radius: 50%; + + ${({ $isOn, $offColor, $onColor }) => + backgroundColor( + $isOn, + $offColor ?? theme.color.WHITE, + $onColor ?? theme.color.WHITE, + )}; + + ${({ $size }) => thumbSizes[$size]}; + + ${({ $size, $isOn, $variant }) => { + const isSolid = $variant === 'solid'; + const solidLeftPositions = { + xs: 'calc(100% - 14px)', + sm: 'calc(100% - 19px)', + md: 'calc(100% - 23px)', + lg: 'calc(100% - 29px)', + }; + + if (isSolid) { + return css` + left: ${$isOn + ? solidLeftPositions[$size] + : $size === 'xs' + ? '2px' + : '3px'}; + transition: left 0.4s; + `; + } + + return css` + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + left: 0; + transition: transform 0.4s; + ${$isOn && 'transform: translateX(142%);'} + `; + }} +`; + +export const ToggleInput = styled.input.attrs({ type: 'checkbox' })` + display: none; + + &:disabled + ${TrackDiv},&:disabled + ${ThumbSpan} { + opacity: 0.7; + cursor: not-allowed; + } +`; diff --git a/frontend/src/components/common/Switch/Switch.tsx b/frontend/src/components/common/Switch/Switch.tsx new file mode 100644 index 000000000..279b34f1a --- /dev/null +++ b/frontend/src/components/common/Switch/Switch.tsx @@ -0,0 +1,148 @@ +import { + useState, + useRef, + useEffect, + useCallback, + type CSSProperties, + type ReactNode, +} from 'react'; + +import * as S from './Switch.styled'; +import type { SwitchSize } from '~/types/size'; + +export interface SwitchProps { + checked: boolean; + onChange: () => void; + size?: SwitchSize; + variant?: 'solid' | 'raised'; + style?: CSSProperties; + description?: ReactNode; + descriptionPosition?: 'top' | 'bottom' | 'left' | 'right'; + onLabel?: string | ReactNode; + offLabel?: string | ReactNode; + onThumb?: string | ReactNode; + offThumb?: string | ReactNode; + readonly?: boolean; + disabled?: boolean; + onColor?: string; + offColor?: string; + thumbOnColor?: string; + thumbOffColor?: string; +} + +/** + * Switch 컴포넌트 + * + * @param {boolean} checked - 스위치의 현재 상태를 나타냅니다. true이면 켜짐, false이면 꺼짐. + * @param {function} onChange - 스위치 상태가 변경될 때 호출되는 콜백 함수. + * @param {"xs" | "sm" | "md" | "lg"} [size="md"] - 스위치의 크기를 설정합니다. + * @param {"solid" | "raised"} [variant="solid"] - 스위치의 스타일 유형을 설정합니다. `solid`: 트랙 안에 thumb이 들어 있는 유형. `raised`: 트랙보다 thumb이 큰 유형. + * @param {CSSProperties} [style] - 추가 스타일을 적용할 수 있는 인라인 스타일 객체입니다. + * @param {ReactNode} [description] - 스위치에 대한 설명이 되는 컴포넌트입니다. + * @param {"top" | "bottom" | "left" | "right"} [descriptionPosition="right"] - 설명의 위치를 설정합니다. `top`: 위쪽, `bottom`: 아래쪽, `left`: 왼쪽, `right`: 오른쪽. + * @param {string | ReactNode} [onLabel] - 스위치가 켜져 있을 때 트랙 라벨에 들어갈 텍스트 또는 아이콘. + * @param {string | ReactNode} [offLabel] - 스위치가 꺼져 있을 때 트랙 라벨에 들어갈 텍스트 또는 아이콘. + * @param {string | ReactNode} [onThumb] - 스위치가 켜져 있을 때 thumb 안에 들어갈 텍스트 또는 아이콘. + * @param {string | ReactNode} [offThumb] - 스위치가 꺼져 있을 때 thumb 안에 들어갈 텍스트 또는 아이콘. + * @param {boolean} [readonly=false] - 스위치를 읽기 전용으로 설정합니다. + * @param {boolean} [disabled=false] - 스위치를 비활성화 상태로 설정합니다. + * @param {string} [onColor] - 스위치가 켜져 있을 때의 트랙 색상을 설정합니다. + * @param {string} [offColor] - 스위치가 꺼져 있을 때의 트랙 색상을 설정합니다. + * @param {string} [thumbOnColor] - 스위치가 켜져 있을 때 thumb의 색상을 설정합니다. + * @param {string} [thumbOffColor] - 스위치가 꺼져 있을 때 thumb의 색상을 설정합니다. + */ +const Switch = ({ + checked, + onChange, + size = 'md', + variant = 'solid', + style, + description, + descriptionPosition = 'right', + onLabel, + offLabel, + onThumb, + offThumb, + readonly = false, + disabled = false, + onColor, + offColor, + thumbOnColor, + thumbOffColor, +}: SwitchProps) => { + const [isOn, setIsOn] = useState(checked); + const throttleTimeout = useRef(null); + + const throttledOnChange = useCallback(() => { + if (throttleTimeout.current === null) { + onChange(); + throttleTimeout.current = window.setTimeout(() => { + throttleTimeout.current = null; + }, 500); + } + }, [onChange]); + + const handleClickToggle = () => { + if (!readonly && !disabled) { + setIsOn((prevState) => !prevState); + throttledOnChange(); + } + }; + + useEffect(() => { + if (checked !== isOn) { + setIsOn(checked); + } + }, [checked, isOn]); + + useEffect(() => { + return () => { + if (throttleTimeout.current !== null) { + clearTimeout(throttleTimeout.current); + } + }; + }, []); + + return ( + + {description && + ['top', 'left'].includes(descriptionPosition) && + description} + + + + + + {isOn ? onLabel : offLabel} + + + {isOn ? onThumb : offThumb} + + + + + {description && + ['bottom', 'right'].includes(descriptionPosition) && + description} + + ); +}; + +export default Switch; diff --git a/frontend/src/types/size.ts b/frontend/src/types/size.ts index c7a92cc91..b1e9d89f5 100644 --- a/frontend/src/types/size.ts +++ b/frontend/src/types/size.ts @@ -21,3 +21,5 @@ export type TeamBadgeSize = Extract; export type CalendarSize = Extract; export type LinkSize = Extract; + +export type SwitchSize = Extract; From d06940cc500322e2f3238dad2e7ddf81d5584a4f Mon Sep 17 00:00:00 2001 From: Rulu Date: Fri, 3 Jan 2025 17:17:52 +0900 Subject: [PATCH 14/20] =?UTF-8?q?feat:=20=EC=BA=98=EB=A6=B0=EB=8D=94=20?= =?UTF-8?q?=EC=88=98=EC=A0=95,=20=EB=93=B1=EB=A1=9D=20=EB=AA=A8=EB=8B=AC?= =?UTF-8?q?=20=EC=A2=85=EC=9D=BC=20=EC=B2=B4=ED=81=AC=EB=B0=95=EC=8A=A4?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=8A=A4=EC=9C=84=EC=B9=98=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/common/Switch/Switch.styled.ts | 15 ++++++++--- .../src/components/common/Switch/Switch.tsx | 13 +++++++++- .../ScheduleAddModal.styled.ts | 2 +- .../ScheduleAddModal/ScheduleAddModal.tsx | 15 ++++++----- .../ScheduleEditModal.styled.ts | 2 +- .../ScheduleEditModal/ScheduleEditModal.tsx | 25 +++++++++++++------ 6 files changed, 50 insertions(+), 22 deletions(-) diff --git a/frontend/src/components/common/Switch/Switch.styled.ts b/frontend/src/components/common/Switch/Switch.styled.ts index 05b7d0095..10352ad77 100644 --- a/frontend/src/components/common/Switch/Switch.styled.ts +++ b/frontend/src/components/common/Switch/Switch.styled.ts @@ -121,15 +121,24 @@ export const TrackDiv = styled.div<{ export const LabelSpan = styled.span<{ $size: SwitchSize; $isOn: boolean; + $offColor: string | undefined; + $onColor: string | undefined; }>` - font-size: 10px; + font-size: 14px; white-space: nowrap; - color: ${theme.color.WHITE}; + color: ${({ $isOn, $onColor, $offColor }) => + $isOn ? $onColor || theme.color.WHITE : $offColor || theme.color.GRAY700}; flex-grow: 1; text-align: center; ${({ $size }) => { - if ($size === 'xs' || $size === 'sm') + if ($size === 'sm') { + return css` + font-size: 12px; + `; + } + + if ($size === 'xs') return css` font-size: 8px; `; diff --git a/frontend/src/components/common/Switch/Switch.tsx b/frontend/src/components/common/Switch/Switch.tsx index 279b34f1a..e47254bec 100644 --- a/frontend/src/components/common/Switch/Switch.tsx +++ b/frontend/src/components/common/Switch/Switch.tsx @@ -26,6 +26,8 @@ export interface SwitchProps { disabled?: boolean; onColor?: string; offColor?: string; + onLabelColor?: string; + offLabelColor?: string; thumbOnColor?: string; thumbOffColor?: string; } @@ -48,6 +50,8 @@ export interface SwitchProps { * @param {boolean} [disabled=false] - 스위치를 비활성화 상태로 설정합니다. * @param {string} [onColor] - 스위치가 켜져 있을 때의 트랙 색상을 설정합니다. * @param {string} [offColor] - 스위치가 꺼져 있을 때의 트랙 색상을 설정합니다. + * @param {string} [onLabelColor] - 스위치가 켜져 있을 때의 라벨 색상을 설정합니다. + * @param {string} [offLabelColor] - 스위치가 꺼져 있을 때의 라벨 색상을 설정합니다. * @param {string} [thumbOnColor] - 스위치가 켜져 있을 때 thumb의 색상을 설정합니다. * @param {string} [thumbOffColor] - 스위치가 꺼져 있을 때 thumb의 색상을 설정합니다. */ @@ -67,6 +71,8 @@ const Switch = ({ disabled = false, onColor, offColor, + onLabelColor, + offLabelColor, thumbOnColor, thumbOffColor, }: SwitchProps) => { @@ -123,7 +129,12 @@ const Switch = ({ $offColor={offColor} $onColor={onColor} > - + {isOn ? onLabel : offLabel} {
- - 종일 - - -

{ - - 종일 - - + onLabel={'종일'} + offLabel={'종일'} + onColor={theme.color.PRIMARY} + />{' '} +

+ {isAllDay + ? '종일 일정이 선택되었습니다.' + : '종일 일정이 해제되었습니다.'} +

From 3f7cf02573a2cbd78bf178860260475e0221edaa Mon Sep 17 00:00:00 2001 From: Rulu Date: Fri, 3 Jan 2025 22:07:27 +0900 Subject: [PATCH 15/20] =?UTF-8?q?feat:=20=EC=BA=98=EB=A6=B0=EB=8D=94=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D,=20=EC=88=98=EC=A0=95=20=EB=AA=A8=EB=8B=AC?= =?UTF-8?q?=EC=97=90=20=EB=A9=94=EB=AA=A8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/assets/svg/index.ts | 1 + frontend/src/assets/svg/memo.svg | 4 ++ .../src/components/common/Svg/Svg.styled.ts | 7 +++ frontend/src/components/common/Svg/Svg.tsx | 29 ++++++++++ .../ScheduleAddModal.styled.ts | 39 ++++++++++++- .../ScheduleAddModal/ScheduleAddModal.tsx | 56 ++++++++++++++++++- .../ScheduleEditModal.styled.ts | 39 ++++++++++++- .../ScheduleEditModal/ScheduleEditModal.tsx | 50 ++++++++++++++++- .../src/hooks/schedule/useDateTimeRange.ts | 18 +++++- .../src/hooks/schedule/useScheduleAddModal.ts | 40 ++++++++++++- .../hooks/schedule/useScheduleEditModal.ts | 46 ++++++++++++++- frontend/src/mocks/fixtures/schedules.ts | 8 +++ frontend/src/mocks/handlers/calendar.ts | 12 ++-- frontend/src/types/schedule.ts | 1 + 14 files changed, 332 insertions(+), 18 deletions(-) create mode 100644 frontend/src/assets/svg/memo.svg create mode 100644 frontend/src/components/common/Svg/Svg.styled.ts create mode 100644 frontend/src/components/common/Svg/Svg.tsx diff --git a/frontend/src/assets/svg/index.ts b/frontend/src/assets/svg/index.ts index bce88a1d8..8d9dad207 100644 --- a/frontend/src/assets/svg/index.ts +++ b/frontend/src/assets/svg/index.ts @@ -34,3 +34,4 @@ export { ReactComponent as QuestionIcon } from './question.svg'; export { ReactComponent as ExportIcon } from './export.svg'; export { ReactComponent as TeamSmallIcon } from './team-small.svg'; export { ReactComponent as EnterIcon } from './enter.svg'; +export { ReactComponent as MemoIcon } from './memo.svg'; diff --git a/frontend/src/assets/svg/memo.svg b/frontend/src/assets/svg/memo.svg new file mode 100644 index 000000000..c558ea1d8 --- /dev/null +++ b/frontend/src/assets/svg/memo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/components/common/Svg/Svg.styled.ts b/frontend/src/components/common/Svg/Svg.styled.ts new file mode 100644 index 000000000..174ebb220 --- /dev/null +++ b/frontend/src/components/common/Svg/Svg.styled.ts @@ -0,0 +1,7 @@ +import styled from 'styled-components'; + +export const Container = styled.svg<{ $fill?: string }>` + path { + fill: ${({ $fill }) => $fill || 'currentColor'}; + } +`; diff --git a/frontend/src/components/common/Svg/Svg.tsx b/frontend/src/components/common/Svg/Svg.tsx new file mode 100644 index 000000000..bd68298e1 --- /dev/null +++ b/frontend/src/components/common/Svg/Svg.tsx @@ -0,0 +1,29 @@ +import { type CSSProperties, type HTMLAttributes } from 'react'; +import type { CSSProp } from 'styled-components'; +import * as Icons from '~/assets/svg'; +import * as S from './Svg.styled'; + +interface SvgProps extends HTMLAttributes { + type: keyof typeof Icons; + fill?: string; + stroke?: string; + style?: CSSProperties; + size?: string | number; + width?: string; + height?: string; + css?: CSSProp; +} + +const Svg = ({ type, fill, stroke, style, size, ...rest }: SvgProps) => { + const SvgIcon = Icons[type]; + + const svgProps = { + style, + ...(size ? { width: String(size), height: String(size) } : {}), + ...(stroke ? { stroke } : {}), + }; + + return ; +}; + +export default Svg; diff --git a/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.styled.ts b/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.styled.ts index 89dac9ddf..63149d6ed 100644 --- a/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.styled.ts +++ b/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.styled.ts @@ -1,4 +1,5 @@ import { styled, css } from 'styled-components'; +import theme from '~/styles/theme'; import type { CalendarSize } from '~/types/size'; export const Backdrop = styled.div` @@ -84,7 +85,7 @@ export const InnerContainer = styled.div` width: 100%; `; -export const CheckboxContainer = styled.div` +export const ConvenientContainer = styled.div` display: flex; align-items: center; justify-content: flex-start; @@ -137,6 +138,25 @@ export const ControlButtonWrapper = styled.div` height: 38px; `; +export const DescriptionTextarea = styled.textarea` + padding: 6px 10px; + border: none; + border-bottom: 1px solid ${theme.color.GRAY200}; + border-radius: 10px; + + font-size: 14px; + + width: 100%; + white-space: normal; + overflow-wrap: break-word; + display: inline-block; +`; + +export const WarnDiv = styled.div` + display: flex; + justify-content: flex-end; +`; + export const title = css` padding: 10px 20px; @@ -186,3 +206,20 @@ export const submitButton = css` width: 76px; padding: 0; `; + +export const descriptionButton = ($isDescription: boolean) => css` + display: flex; + padding: 2px 6px; + align-items: center; + border: 1px solid ${theme.color.PRIMARY}; + border-radius: 25px; + background-color: ${$isDescription ? theme.color.PRIMARY : theme.color.WHITE}; +`; + +export const descriptionText = ($isDescription: boolean) => css` + color: ${$isDescription ? theme.color.WHITE : theme.color.PRIMARY}; +`; + +export const errorText = css` + color: ${theme.color.RED}; +`; diff --git a/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.tsx b/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.tsx index 5c1098448..9f23200c3 100644 --- a/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.tsx +++ b/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.tsx @@ -14,6 +14,7 @@ import type { CalendarSize } from '~/types/size'; import { getIsMobile } from '~/utils/getIsMobile'; import Switch from '~/components/common/Switch/Switch'; import theme from '~/styles/theme'; +import Svg from '~/components/common/Svg/Svg'; interface ScheduleAddModalProps { calendarSize?: CalendarSize; @@ -29,6 +30,9 @@ const ScheduleAddModal = (props: ScheduleAddModalProps) => { schedule, isAllDay, times, + isDescription, + isDescriptionMaxLength, + handlers: { handleScheduleChange, handleScheduleBlur, @@ -36,15 +40,22 @@ const ScheduleAddModal = (props: ScheduleAddModalProps) => { handleStartTimeChange, handleEndTimeChange, handleScheduleSubmit, + handleIsDescription, + handleDescriptionInput, }, } = useScheduleAddModal(clickedDate); const titleInputRef = useRef(null); + const descriptionInputRef = useRef(null); useEffect(() => { titleInputRef.current?.focus(); }, []); + useEffect(() => { + if (isDescription) descriptionInputRef.current?.focus(); + }, [isDescription]); + return ( @@ -124,7 +135,7 @@ const ScheduleAddModal = (props: ScheduleAddModalProps) => { )} - + { ? '종일 일정이 선택되었습니다.' : '종일 일정이 해제되었습니다.'}

-
+ + +
+ {isDescription && ( + + )} + + {isDescription && + (!isDescriptionMaxLength ? ( + ({schedule.description.length} / 100자) + ) : ( + + 최대 100자까지 입력가능합니다. + + ))} + +
diff --git a/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.styled.ts b/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.styled.ts index 89dac9ddf..63149d6ed 100644 --- a/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.styled.ts +++ b/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.styled.ts @@ -1,4 +1,5 @@ import { styled, css } from 'styled-components'; +import theme from '~/styles/theme'; import type { CalendarSize } from '~/types/size'; export const Backdrop = styled.div` @@ -84,7 +85,7 @@ export const InnerContainer = styled.div` width: 100%; `; -export const CheckboxContainer = styled.div` +export const ConvenientContainer = styled.div` display: flex; align-items: center; justify-content: flex-start; @@ -137,6 +138,25 @@ export const ControlButtonWrapper = styled.div` height: 38px; `; +export const DescriptionTextarea = styled.textarea` + padding: 6px 10px; + border: none; + border-bottom: 1px solid ${theme.color.GRAY200}; + border-radius: 10px; + + font-size: 14px; + + width: 100%; + white-space: normal; + overflow-wrap: break-word; + display: inline-block; +`; + +export const WarnDiv = styled.div` + display: flex; + justify-content: flex-end; +`; + export const title = css` padding: 10px 20px; @@ -186,3 +206,20 @@ export const submitButton = css` width: 76px; padding: 0; `; + +export const descriptionButton = ($isDescription: boolean) => css` + display: flex; + padding: 2px 6px; + align-items: center; + border: 1px solid ${theme.color.PRIMARY}; + border-radius: 25px; + background-color: ${$isDescription ? theme.color.PRIMARY : theme.color.WHITE}; +`; + +export const descriptionText = ($isDescription: boolean) => css` + color: ${$isDescription ? theme.color.WHITE : theme.color.PRIMARY}; +`; + +export const errorText = css` + color: ${theme.color.RED}; +`; diff --git a/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.tsx b/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.tsx index d5915604e..6ad7b0485 100644 --- a/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.tsx +++ b/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.tsx @@ -14,6 +14,7 @@ import type { CalendarSize } from '~/types/size'; import { getIsMobile } from '~/utils/getIsMobile'; import Switch from '~/components/common/Switch/Switch'; import theme from '~/styles/theme'; +import Svg from '~/components/common/Svg/Svg'; interface ScheduleEditModalProps { calendarSize?: CalendarSize; @@ -31,6 +32,9 @@ const ScheduleEditModal = (props: ScheduleEditModalProps) => { schedule, times, isAllDay, + isDescription, + isDescriptionMaxLength, + handlers: { handleScheduleChange, handleScheduleBlur, @@ -38,6 +42,8 @@ const ScheduleEditModal = (props: ScheduleEditModalProps) => { handleStartTimeChange, handleEndTimeChange, handleIsAllDayChange, + handleIsDescription, + handleDescriptionInput, }, } = useScheduleEditModal(scheduleId, initialSchedule); @@ -119,7 +125,7 @@ const ScheduleEditModal = (props: ScheduleEditModalProps) => { )} - + { ? '종일 일정이 선택되었습니다.' : '종일 일정이 해제되었습니다.'}

-
+ + +
+ {isDescription && ( + + )} + + {isDescription && + (!isDescriptionMaxLength ? ( + ({schedule.description.length} / 100자) + ) : ( + + 최대 100자까지 입력가능합니다. + + ))} + +
diff --git a/frontend/src/hooks/schedule/useDateTimeRange.ts b/frontend/src/hooks/schedule/useDateTimeRange.ts index 29667f10c..7a645afb9 100644 --- a/frontend/src/hooks/schedule/useDateTimeRange.ts +++ b/frontend/src/hooks/schedule/useDateTimeRange.ts @@ -8,6 +8,7 @@ import type { Schedule, YYYYMMDD } from '~/types/schedule'; interface DateTimeRange { title: string; + description: string; startDate: string; startTime: string; endDate: string; @@ -46,6 +47,7 @@ const isDateTimeRangeValid = (dateTimeRange: DateTimeRange) => { const generateDateTimeRange = ( dateData: Date | Schedule | undefined, title: string | undefined, + initDescription: string | undefined, ) => { if (!dateData) { return { @@ -56,6 +58,7 @@ const generateDateTimeRange = ( endTime: '10:00', dateDifference: 0, isAllDay: false, + description: initDescription ?? '', }; } @@ -70,6 +73,7 @@ const generateDateTimeRange = ( endTime: '10:00', dateDifference: 0, isAllDay: false, + description: initDescription ?? '', }; } @@ -88,18 +92,21 @@ const generateDateTimeRange = ( endTime, dateDifference, isAllDay: endTime === '23:59', + description: initDescription ?? '', }; }; export const useDateTimeRange = ( dateData: Date | Schedule | undefined, initTitle: string | undefined, + initDescription: string | undefined, ) => { const [dateTimeRange, setDateTimeRange] = useState( - generateDateTimeRange(dateData, initTitle), + generateDateTimeRange(dateData, initTitle, initDescription), ); const { title, + description, startDate, endDate, startTime, @@ -108,6 +115,13 @@ export const useDateTimeRange = ( isAllDay, } = dateTimeRange; + const handleDescriptionChange = (description: string) => { + setDateTimeRange((prev) => ({ + ...prev, + description: description, + })); + }; + const handleScheduleChange: ChangeEventHandler = (e) => { const { name, value } = e.target; @@ -189,10 +203,12 @@ export const useDateTimeRange = ( handleStartTimeChange, handleEndTimeChange, handleIsAllDayChange, + handleDescriptionChange, title, startDate, endDate, + description, startTime: isAllDay ? '00:00' : startTime, endTime: isAllDay ? '23:59' : endTime, isValid: isDateTimeRangeValid(dateTimeRange), diff --git a/frontend/src/hooks/schedule/useScheduleAddModal.ts b/frontend/src/hooks/schedule/useScheduleAddModal.ts index db3128d1f..444fd2fc0 100644 --- a/frontend/src/hooks/schedule/useScheduleAddModal.ts +++ b/frontend/src/hooks/schedule/useScheduleAddModal.ts @@ -1,7 +1,7 @@ import { useSendSchedule } from '~/hooks/queries/useSendSchedule'; import { useModal } from '~/hooks/useModal'; import { isYYYYMMDDHHMM } from '~/types/typeGuard'; -import type { FormEventHandler } from 'react'; +import { type ChangeEvent, useState, type FormEventHandler } from 'react'; import { useToast } from '~/hooks/useToast'; import { useTeamPlace } from '~/hooks/useTeamPlace'; import { useDateTimeRange } from '~/hooks/schedule/useDateTimeRange'; @@ -9,6 +9,7 @@ import { useDateTimeRange } from '~/hooks/schedule/useDateTimeRange'; export const useScheduleAddModal = (clickedDate: Date) => { const { title, + description, startDate, endDate, startTime, @@ -20,15 +21,43 @@ export const useScheduleAddModal = (clickedDate: Date) => { handleStartTimeChange, handleEndTimeChange, handleIsAllDayChange, - } = useDateTimeRange(clickedDate, ''); + handleDescriptionChange, + } = useDateTimeRange(clickedDate, '', ''); + const [isDescription, setIsDescription] = useState(false); + const [isDescriptionMaxLength, setIsDescriptionMaxLength] = useState(false); const { closeModal } = useModal(); const { showToast } = useToast(); const { teamPlaceId } = useTeamPlace(); const { mutateSendSchedule } = useSendSchedule(teamPlaceId); - const schedule = { title, startDate, endDate }; + const schedule = { title, description, startDate, endDate }; const times = { startTime, endTime }; + const handleIsDescription = () => { + setIsDescription((prev) => { + if (prev) { + handleDescriptionChange(''); + setIsDescriptionMaxLength(false); + } + return !prev; + }); + }; + + const handleDescriptionInput = (e: ChangeEvent) => { + const textarea = e.target; + + textarea.style.height = 'auto'; + textarea.style.height = `${textarea.scrollHeight}px`; + + if (textarea.value.length > 100) { + setIsDescriptionMaxLength(true); + } else { + setIsDescriptionMaxLength(false); + } + + handleDescriptionChange(textarea.value.trim().slice(0, 100)); + }; + const handleScheduleSubmit: FormEventHandler = (e) => { e.preventDefault(); @@ -52,6 +81,7 @@ export const useScheduleAddModal = (clickedDate: Date) => { title, startDateTime, endDateTime, + description, }, { onSuccess: () => { @@ -71,6 +101,8 @@ export const useScheduleAddModal = (clickedDate: Date) => { schedule, isAllDay, times, + isDescription, + isDescriptionMaxLength, handlers: { handleScheduleChange, @@ -79,6 +111,8 @@ export const useScheduleAddModal = (clickedDate: Date) => { handleStartTimeChange, handleEndTimeChange, handleScheduleSubmit, + handleIsDescription, + handleDescriptionInput, }, }; }; diff --git a/frontend/src/hooks/schedule/useScheduleEditModal.ts b/frontend/src/hooks/schedule/useScheduleEditModal.ts index c1c74f333..30913eecf 100644 --- a/frontend/src/hooks/schedule/useScheduleEditModal.ts +++ b/frontend/src/hooks/schedule/useScheduleEditModal.ts @@ -1,7 +1,7 @@ import { useModifySchedule } from '~/hooks/queries/useModifySchedule'; import { useModal } from '~/hooks/useModal'; import { isYYYYMMDDHHMM } from '~/types/typeGuard'; -import type { FormEventHandler } from 'react'; +import { type ChangeEvent, useState, type FormEventHandler } from 'react'; import type { Schedule } from '~/types/schedule'; import { useToast } from '~/hooks/useToast'; import { useTeamPlace } from '~/hooks/useTeamPlace'; @@ -13,6 +13,7 @@ export const useScheduleEditModal = ( ) => { const { title, + description, startDate, endDate, startTime, @@ -24,15 +25,50 @@ export const useScheduleEditModal = ( handleStartTimeChange, handleEndTimeChange, handleIsAllDayChange, - } = useDateTimeRange(initialSchedule, initialSchedule?.title); + handleDescriptionChange, + } = useDateTimeRange( + initialSchedule, + initialSchedule?.title, + initialSchedule?.description ?? '', + ); + const [isDescription, setIsDescription] = useState( + initialSchedule?.description ? true : false, + ); + const [isDescriptionMaxLength, setIsDescriptionMaxLength] = useState(false); + const { closeModal } = useModal(); const { showToast } = useToast(); const { teamPlaceId } = useTeamPlace(); const { mutateModifySchedule } = useModifySchedule(teamPlaceId, scheduleId); - const schedule = { title, startDate, endDate }; + const schedule = { title, description, startDate, endDate }; const times = { startTime, endTime }; + const handleIsDescription = () => { + setIsDescription((prev) => { + if (prev) { + handleDescriptionChange(''); + setIsDescriptionMaxLength(false); + } + return !prev; + }); + }; + + const handleDescriptionInput = (e: ChangeEvent) => { + const textarea = e.target; + + textarea.style.height = 'auto'; + textarea.style.height = `${textarea.scrollHeight}px`; + + if (textarea.value.length > 100) { + setIsDescriptionMaxLength(true); + } else { + setIsDescriptionMaxLength(false); + } + + handleDescriptionChange(textarea.value.trim().slice(0, 100)); + }; + const handleScheduleSubmit: FormEventHandler = (e) => { e.preventDefault(); @@ -75,6 +111,8 @@ export const useScheduleEditModal = ( schedule, times, isAllDay, + isDescription, + isDescriptionMaxLength, handlers: { handleScheduleChange, @@ -83,6 +121,8 @@ export const useScheduleEditModal = ( handleStartTimeChange, handleEndTimeChange, handleIsAllDayChange, + handleIsDescription, + handleDescriptionInput, }, }; }; diff --git a/frontend/src/mocks/fixtures/schedules.ts b/frontend/src/mocks/fixtures/schedules.ts index df458855b..d3d754cd2 100644 --- a/frontend/src/mocks/fixtures/schedules.ts +++ b/frontend/src/mocks/fixtures/schedules.ts @@ -169,4 +169,12 @@ export const mySchedules: ScheduleWithTeamPlaceId[] = [ startDateTime: '2023-06-30 05:00', endDateTime: '2023-07-02 05:00', }, + { + id: 8, + teamPlaceId: 1, + title: 'test7', + startDateTime: '2025-01-03 05:00', + endDateTime: '2023-01-04 05:00', + description: '멤모멤모테스트', + }, ]; diff --git a/frontend/src/mocks/handlers/calendar.ts b/frontend/src/mocks/handlers/calendar.ts index 04e89768d..9b1213a34 100644 --- a/frontend/src/mocks/handlers/calendar.ts +++ b/frontend/src/mocks/handlers/calendar.ts @@ -80,8 +80,6 @@ export const calendarHandlers = [ const scheduleId = Number(params.scheduleId); const data = schedules.find((schedule) => schedule.id === scheduleId); - console.log('테스트', { scheduleId, data }); - const teamPlaceId = Number(params.teamPlaceId); const index = teamPlaces.findIndex( (teamPlace) => teamPlace.id === teamPlaceId, @@ -99,12 +97,14 @@ export const calendarHandlers = [ http.post<{ teamPlaceId: string }, ScheduleWithoutId>( `/api/team-place/:teamPlaceId/calendar/schedules`, async ({ request, params }) => { - const { title, startDateTime, endDateTime } = await request.json(); + const { title, startDateTime, endDateTime, description } = + await request.json(); const newSchedule = { id: Date.now(), title, startDateTime, endDateTime, + description, }; const teamPlaceId = Number(params.teamPlaceId); const index = teamPlaces.findIndex( @@ -133,8 +133,8 @@ export const calendarHandlers = [ const teamPlaceId = Number(params.teamPlaceId); const scheduleId = Number(params.scheduleId); - const { title, startDateTime, endDateTime } = await request.json(); - console.log('테스트', title, startDateTime, endDateTime); + const { title, startDateTime, endDateTime, description } = + await request.json(); const index = schedules.findIndex( (schedule) => schedule.id === scheduleId, ); @@ -152,6 +152,7 @@ export const calendarHandlers = [ title, startDateTime, endDateTime, + description, }; mySchedules[myIndex] = { @@ -160,6 +161,7 @@ export const calendarHandlers = [ title, startDateTime, endDateTime, + description, }; return new HttpResponse(null); diff --git a/frontend/src/types/schedule.ts b/frontend/src/types/schedule.ts index 4798e192f..be2627987 100644 --- a/frontend/src/types/schedule.ts +++ b/frontend/src/types/schedule.ts @@ -6,6 +6,7 @@ export interface Schedule { title: string; startDateTime: YYYYMMDDHHMM; endDateTime: YYYYMMDDHHMM; + description?: string; } export interface ScheduleWithTeamPlaceId extends Schedule { From 8cb20d63140ecde5de8b9c19102ea8999ba3b4ff Mon Sep 17 00:00:00 2001 From: Rulu Date: Fri, 3 Jan 2025 22:32:48 +0900 Subject: [PATCH 16/20] =?UTF-8?q?feat:=20=EC=BA=98=EB=A6=B0=EB=8D=94=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EB=A9=94=EB=AA=A8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ScheduleModal/ScheduleModal.styled.ts | 18 ++++++++++++------ .../ScheduleModal/ScheduleModal.tsx | 13 +++++++++++-- frontend/src/mocks/fixtures/schedules.ts | 10 +++++++++- frontend/src/mocks/handlers/user.ts | 5 +++++ 4 files changed, 37 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/team_calendar/ScheduleModal/ScheduleModal.styled.ts b/frontend/src/components/team_calendar/ScheduleModal/ScheduleModal.styled.ts index 75c1c1f62..8b6ff1b33 100644 --- a/frontend/src/components/team_calendar/ScheduleModal/ScheduleModal.styled.ts +++ b/frontend/src/components/team_calendar/ScheduleModal/ScheduleModal.styled.ts @@ -6,18 +6,18 @@ export const Container = styled.div<{ $css: CSSProp; $isMobile: boolean }>` position: absolute; flex-direction: column; z-index: ${({ theme }) => theme.zIndex.MODAL}; - gap: 16px; + gap: 10px; ${({ $isMobile }) => { if ($isMobile) return css` width: 300px; - padding: 10px 10px 20px 26px; + padding: 10px 26px 20px; `; return css` - width: 446px; - padding: 18px 22px; + width: 436px; + padding: 12px 16px; `; }} @@ -76,6 +76,12 @@ export const PeriodWrapper = styled.div<{ $isMobile: boolean }>` }} `; +export const DescriptionDiv = styled.div` + display: flex; + align-items: center; + gap: 10px; +`; + export const teamName = css` overflow: hidden; white-space: nowrap; @@ -111,8 +117,8 @@ export const closeButton = ($isMobile: boolean) => css` align-items: center; align-self: flex-end; - width: 80px; - height: 42px; + width: 76px; + height: 36px; ${$isMobile && css` margin-right: 10px; diff --git a/frontend/src/components/team_calendar/ScheduleModal/ScheduleModal.tsx b/frontend/src/components/team_calendar/ScheduleModal/ScheduleModal.tsx index c6f7807a5..609ad8728 100644 --- a/frontend/src/components/team_calendar/ScheduleModal/ScheduleModal.tsx +++ b/frontend/src/components/team_calendar/ScheduleModal/ScheduleModal.tsx @@ -13,6 +13,8 @@ import { useTeamPlace } from '~/hooks/useTeamPlace'; import type { CalendarSize } from '~/types/size'; import { getIsMobile } from '~/utils/getIsMobile'; import { generateDateTimeRangeDescription } from '~/utils/generateDateTimeRangeDescription'; +import Svg from '~/components/common/Svg/Svg'; +import theme from '~/styles/theme'; interface ScheduleModalProps { calendarWidth: number; @@ -41,7 +43,7 @@ const ScheduleModal = (props: ScheduleModalProps) => { const { mutateDeleteSchedule } = useDeleteSchedule(teamPlaceId, scheduleId); if (scheduleById === undefined) return; - const { title, startDateTime, endDateTime } = scheduleById; + const { title, startDateTime, endDateTime, description } = scheduleById; const { row, column, level } = position; const handleScheduleDelete = () => { @@ -72,7 +74,7 @@ const ScheduleModal = (props: ScheduleModalProps) => { > - + {!isMobile && (
{displayName} @@ -116,6 +118,13 @@ const ScheduleModal = (props: ScheduleModalProps) => { + {description && ( + + + {description} + + )} + -
- {isDescription && ( - - )} + + - {isDescription && - (!isDescriptionMaxLength ? ( - ({schedule.description.length} / 100자) - ) : ( - - 최대 100자까지 입력가능합니다. - - ))} + {!isDescriptionMaxLength ? ( + ({schedule.description.length} / 100자) + ) : ( + + 최대 100자까지 입력가능합니다. + + )} -
+ diff --git a/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.styled.ts b/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.styled.ts index 63149d6ed..2c500f586 100644 --- a/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.styled.ts +++ b/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.styled.ts @@ -57,7 +57,7 @@ export const Container = styled.div<{ display: flex; flex-direction: column; - row-gap: ${({ $isMobile }) => ($isMobile ? '10px' : '10px')}; + row-gap: 10px; } `; @@ -138,18 +138,23 @@ export const ControlButtonWrapper = styled.div` height: 38px; `; +export const DescriptionDiv = styled.div<{ $isDescription: boolean }>` + display: ${({ $isDescription }) => ($isDescription ? 'block' : 'none')}; +`; + export const DescriptionTextarea = styled.textarea` + display: inline-block; + width: 100%; padding: 6px 10px; + border: none; border-bottom: 1px solid ${theme.color.GRAY200}; border-radius: 10px; + resize: none; font-size: 14px; - - width: 100%; white-space: normal; overflow-wrap: break-word; - display: inline-block; `; export const WarnDiv = styled.div` diff --git a/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.tsx b/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.tsx index 6ad7b0485..09f4458bb 100644 --- a/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.tsx +++ b/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.tsx @@ -162,27 +162,24 @@ const ScheduleEditModal = (props: ScheduleEditModalProps) => { -
- {isDescription && ( - - )} + + - {isDescription && - (!isDescriptionMaxLength ? ( - ({schedule.description.length} / 100자) - ) : ( - - 최대 100자까지 입력가능합니다. - - ))} + {!isDescriptionMaxLength ? ( + ({schedule.description.length} / 100자) + ) : ( + + 최대 100자까지 입력가능합니다. + + )} -
+ diff --git a/frontend/src/constants/calendar.ts b/frontend/src/constants/calendar.ts index 74a8cd5e4..14781eeff 100644 --- a/frontend/src/constants/calendar.ts +++ b/frontend/src/constants/calendar.ts @@ -18,6 +18,8 @@ export const TIME_TABLE = arrayOf(48).map((_, i) => { export const SCHEDULE_CIRCLE_MAX_COUNT = 3; +export const SCHEDULE_DESCRIPTION_MAX_LENGTH = 100; + export const MODAL_OPEN_TYPE = { ADD: 'add', VIEW: 'view', diff --git a/frontend/src/hooks/schedule/useScheduleAddModal.ts b/frontend/src/hooks/schedule/useScheduleAddModal.ts index 444fd2fc0..8d427ce1e 100644 --- a/frontend/src/hooks/schedule/useScheduleAddModal.ts +++ b/frontend/src/hooks/schedule/useScheduleAddModal.ts @@ -5,6 +5,7 @@ import { type ChangeEvent, useState, type FormEventHandler } from 'react'; import { useToast } from '~/hooks/useToast'; import { useTeamPlace } from '~/hooks/useTeamPlace'; import { useDateTimeRange } from '~/hooks/schedule/useDateTimeRange'; +import { SCHEDULE_DESCRIPTION_MAX_LENGTH } from '~/constants/calendar'; export const useScheduleAddModal = (clickedDate: Date) => { const { @@ -49,13 +50,15 @@ export const useScheduleAddModal = (clickedDate: Date) => { textarea.style.height = 'auto'; textarea.style.height = `${textarea.scrollHeight}px`; - if (textarea.value.length > 100) { + if (textarea.value.length > SCHEDULE_DESCRIPTION_MAX_LENGTH) { setIsDescriptionMaxLength(true); } else { setIsDescriptionMaxLength(false); } - handleDescriptionChange(textarea.value.trim().slice(0, 100)); + handleDescriptionChange( + textarea.value.slice(0, SCHEDULE_DESCRIPTION_MAX_LENGTH), + ); }; const handleScheduleSubmit: FormEventHandler = (e) => { diff --git a/frontend/src/hooks/schedule/useScheduleEditModal.ts b/frontend/src/hooks/schedule/useScheduleEditModal.ts index 30913eecf..f6a11af5f 100644 --- a/frontend/src/hooks/schedule/useScheduleEditModal.ts +++ b/frontend/src/hooks/schedule/useScheduleEditModal.ts @@ -6,6 +6,7 @@ import type { Schedule } from '~/types/schedule'; import { useToast } from '~/hooks/useToast'; import { useTeamPlace } from '~/hooks/useTeamPlace'; import { useDateTimeRange } from '~/hooks/schedule/useDateTimeRange'; +import { SCHEDULE_DESCRIPTION_MAX_LENGTH } from '~/constants/calendar'; export const useScheduleEditModal = ( scheduleId: Schedule['id'], @@ -60,13 +61,15 @@ export const useScheduleEditModal = ( textarea.style.height = 'auto'; textarea.style.height = `${textarea.scrollHeight}px`; - if (textarea.value.length > 100) { + if (textarea.value.length > SCHEDULE_DESCRIPTION_MAX_LENGTH) { setIsDescriptionMaxLength(true); } else { setIsDescriptionMaxLength(false); } - handleDescriptionChange(textarea.value.trim().slice(0, 100)); + handleDescriptionChange( + textarea.value.slice(0, SCHEDULE_DESCRIPTION_MAX_LENGTH), + ); }; const handleScheduleSubmit: FormEventHandler = (e) => { @@ -92,6 +95,7 @@ export const useScheduleEditModal = ( title, startDateTime, endDateTime, + description, }, { onSuccess: () => { From 53e9223d6e6bc9bfccf0c64e1a5fca0826c44e9a Mon Sep 17 00:00:00 2001 From: Rulu Date: Mon, 13 Jan 2025 22:03:28 +0900 Subject: [PATCH 19/20] =?UTF-8?q?fix:=20=EB=A9=94=EB=AA=A8=20=EC=97=86?= =?UTF-8?q?=EC=9D=84=20=EB=95=8C=20=EB=93=B1=EB=A1=9D=EC=95=88=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EB=B2=84=EA=B7=B8=20=ED=94=BD=EC=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/common/Switch/Switch.tsx | 18 -------- .../ScheduleAddModal.styled.ts | 4 -- .../ScheduleAddModal/ScheduleAddModal.tsx | 45 +++++++++++-------- .../ScheduleEditModal.styled.ts | 4 -- .../ScheduleEditModal/ScheduleEditModal.tsx | 42 +++++++++-------- 5 files changed, 50 insertions(+), 63 deletions(-) diff --git a/frontend/src/components/common/Switch/Switch.tsx b/frontend/src/components/common/Switch/Switch.tsx index e56c137b0..b232b4d5c 100644 --- a/frontend/src/components/common/Switch/Switch.tsx +++ b/frontend/src/components/common/Switch/Switch.tsx @@ -79,20 +79,10 @@ const Switch = ({ const [isOn, setIsOn] = useState(checked); const throttleTimeout = useRef(null); - const throttledOnChange = useCallback(() => { - if (throttleTimeout.current === null) { - onChange(); - throttleTimeout.current = window.setTimeout(() => { - throttleTimeout.current = null; - }, 500); - } - }, [onChange]); - const handleClickToggle = () => { if (!readonly && !disabled) { setIsOn((prevState) => !prevState); onChange(); - // throttledOnChange(); } }; @@ -102,14 +92,6 @@ const Switch = ({ } }, [checked, isOn]); - // useEffect(() => { - // return () => { - // if (throttleTimeout.current !== null) { - // clearTimeout(throttleTimeout.current); - // } - // }; - // }, []); - return ( {description && diff --git a/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.styled.ts b/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.styled.ts index a9d658564..a288d47c5 100644 --- a/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.styled.ts +++ b/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.styled.ts @@ -138,10 +138,6 @@ export const ControlButtonWrapper = styled.div` height: 38px; `; -export const DescriptionDiv = styled.div<{ $isDescription: boolean }>` - display: ${({ $isDescription }) => ($isDescription ? 'block' : 'none')}; -`; - export const DescriptionTextarea = styled.textarea` display: inline-block; width: 100%; diff --git a/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.tsx b/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.tsx index 40211e1ef..e296b8080 100644 --- a/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.tsx +++ b/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.tsx @@ -15,6 +15,7 @@ import { getIsMobile } from '~/utils/getIsMobile'; import Switch from '~/components/common/Switch/Switch'; import theme from '~/styles/theme'; import Svg from '~/components/common/Svg/Svg'; +import { SCHEDULE_DESCRIPTION_MAX_LENGTH } from '~/constants/calendar'; interface ScheduleAddModalProps { calendarSize?: CalendarSize; @@ -172,25 +173,31 @@ const ScheduleAddModal = (props: ScheduleAddModalProps) => { - - - - {!isDescriptionMaxLength ? ( - ({schedule.description.length} / 100자) - ) : ( - - 최대 100자까지 입력가능합니다. - - )} - - + {isDescription && ( + <> + + + {!isDescriptionMaxLength ? ( + + ({schedule.description.length} / $ + {SCHEDULE_DESCRIPTION_MAX_LENGTH}자) + + ) : ( + + 최대 ${SCHEDULE_DESCRIPTION_MAX_LENGTH}자까지 + 입력가능합니다. + + )} + + + )} + diff --git a/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.styled.ts b/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.styled.ts index 2c500f586..4991d0bd5 100644 --- a/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.styled.ts +++ b/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.styled.ts @@ -138,10 +138,6 @@ export const ControlButtonWrapper = styled.div` height: 38px; `; -export const DescriptionDiv = styled.div<{ $isDescription: boolean }>` - display: ${({ $isDescription }) => ($isDescription ? 'block' : 'none')}; -`; - export const DescriptionTextarea = styled.textarea` display: inline-block; width: 100%; diff --git a/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.tsx b/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.tsx index 09f4458bb..5a95aa92a 100644 --- a/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.tsx +++ b/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.tsx @@ -15,6 +15,7 @@ import { getIsMobile } from '~/utils/getIsMobile'; import Switch from '~/components/common/Switch/Switch'; import theme from '~/styles/theme'; import Svg from '~/components/common/Svg/Svg'; +import { SCHEDULE_DESCRIPTION_MAX_LENGTH } from '~/constants/calendar'; interface ScheduleEditModalProps { calendarSize?: CalendarSize; @@ -162,24 +163,29 @@ const ScheduleEditModal = (props: ScheduleEditModalProps) => { - - - - {!isDescriptionMaxLength ? ( - ({schedule.description.length} / 100자) - ) : ( - - 최대 100자까지 입력가능합니다. - - )} - - + {isDescription && ( + <> + + + {!isDescriptionMaxLength ? ( + + ({schedule.description.length} / $ + {SCHEDULE_DESCRIPTION_MAX_LENGTH}자) + + ) : ( + + 최대 ${SCHEDULE_DESCRIPTION_MAX_LENGTH}자까지 + 입력가능합니다. + + )} + + + )} From a475c3b87405b62d7e8f4368184887397efc25dd Mon Sep 17 00:00:00 2001 From: Rulu Date: Mon, 13 Jan 2025 22:28:05 +0900 Subject: [PATCH 20/20] =?UTF-8?q?refactor:=20=EC=98=A4=ED=83=80=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 --- .../team_calendar/ScheduleAddModal/ScheduleAddModal.tsx | 2 +- .../team_calendar/ScheduleEditModal/ScheduleEditModal.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.tsx b/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.tsx index e296b8080..42dcf186e 100644 --- a/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.tsx +++ b/frontend/src/components/team_calendar/ScheduleAddModal/ScheduleAddModal.tsx @@ -185,7 +185,7 @@ const ScheduleAddModal = (props: ScheduleAddModalProps) => { {!isDescriptionMaxLength ? ( - ({schedule.description.length} / $ + ({schedule.description.length} / {SCHEDULE_DESCRIPTION_MAX_LENGTH}자) ) : ( diff --git a/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.tsx b/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.tsx index 5a95aa92a..f9f7d83b3 100644 --- a/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.tsx +++ b/frontend/src/components/team_calendar/ScheduleEditModal/ScheduleEditModal.tsx @@ -174,7 +174,7 @@ const ScheduleEditModal = (props: ScheduleEditModalProps) => { {!isDescriptionMaxLength ? ( - ({schedule.description.length} / $ + ({schedule.description.length} / {SCHEDULE_DESCRIPTION_MAX_LENGTH}자) ) : (