From ecc57d4e240896ddba0643e7d68031138ca35165 Mon Sep 17 00:00:00 2001 From: Rulu <79538610+hafnium1923@users.noreply.github.com> Date: Tue, 5 Dec 2023 13:28:48 +0900 Subject: [PATCH 01/20] =?UTF-8?q?[FE]=20sse=20=EB=B0=9C=EC=83=9D=EC=8B=9C?= =?UTF-8?q?=20=ED=8C=80=20=EC=B1=84=ED=8C=85=20=EC=A1=B0=ED=9A=8C=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=A0=9C=EA=B1=B0=20=EB=B0=91=20=EC=9D=B4=EB=B2=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=9D=B4=EC=9A=A9=ED=95=B4=20=EC=B1=84=ED=8C=85=20?= =?UTF-8?q?=EB=A0=8C=EB=8D=94=EB=A7=81=20(#876)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 타입 내보내기 * feat: 팀 피드 staleTime 추가 * feat: 이벤트 발생시 queryData 변경 * refactor: 요토 리뷰 반영 --- frontend/src/apis/feed.ts | 2 +- frontend/src/constants/query.ts | 2 ++ frontend/src/hooks/queries/useFetchThreads.ts | 2 ++ frontend/src/hooks/queries/useSSE.ts | 13 +++++++++---- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/frontend/src/apis/feed.ts b/frontend/src/apis/feed.ts index 192cffd5b..f21b1db1f 100644 --- a/frontend/src/apis/feed.ts +++ b/frontend/src/apis/feed.ts @@ -2,7 +2,7 @@ import { http } from '~/apis/http'; import { THREAD_SIZE } from '~/constants/feed'; import type { Thread, NoticeThread } from '~/types/feed'; -interface ThreadsResponse { +export interface ThreadsResponse { threads: Thread[]; } diff --git a/frontend/src/constants/query.ts b/frontend/src/constants/query.ts index 3c969938b..2640a963a 100644 --- a/frontend/src/constants/query.ts +++ b/frontend/src/constants/query.ts @@ -11,5 +11,7 @@ export const STALE_TIME = { TEAM_LINKS: 1000 * 60, + TEAM_FEED: 1000 * 30, + ICALENDAR_URL: Infinity, }; diff --git a/frontend/src/hooks/queries/useFetchThreads.ts b/frontend/src/hooks/queries/useFetchThreads.ts index 3b0168a1f..9073b8491 100644 --- a/frontend/src/hooks/queries/useFetchThreads.ts +++ b/frontend/src/hooks/queries/useFetchThreads.ts @@ -1,6 +1,7 @@ import { useInfiniteQuery } from '@tanstack/react-query'; import { fetchThreads } from '~/apis/feed'; import { THREAD_SIZE } from '~/constants/feed'; +import { STALE_TIME } from '~/constants/query'; export const useFetchThreads = (teamPlaceId: number) => { const { @@ -16,6 +17,7 @@ export const useFetchThreads = (teamPlaceId: number) => { if (lastPage.threads.length !== THREAD_SIZE) return undefined; return lastPage.threads[THREAD_SIZE - 1].id; }, + staleTime: STALE_TIME.TEAM_FEED, }, ); diff --git a/frontend/src/hooks/queries/useSSE.ts b/frontend/src/hooks/queries/useSSE.ts index 6416f79a0..903c2530a 100644 --- a/frontend/src/hooks/queries/useSSE.ts +++ b/frontend/src/hooks/queries/useSSE.ts @@ -4,6 +4,8 @@ import { baseUrl } from '~/apis/http'; import { EventSourcePolyfill } from 'event-source-polyfill'; import { useToken } from '~/hooks/useToken'; import { useTeamPlace } from '~/hooks/useTeamPlace'; +import type { ThreadsResponse } from '~/apis/feed'; +import type { Thread } from '~/types/feed'; export const useSSE = () => { const queryClient = useQueryClient(); @@ -11,7 +13,6 @@ export const useSSE = () => { const { teamPlaceId } = useTeamPlace(); const connect = useCallback(() => { - console.log(teamPlaceId); if (!teamPlaceId) { return; } @@ -25,10 +26,14 @@ export const useSSE = () => { }, ); - eventSource.addEventListener('new_thread', (e) => { - console.log('1 ' + e.data); + eventSource.addEventListener('new_thread', (e: MessageEvent) => { + const newThread = e.data; - queryClient.invalidateQueries(['threadData', teamPlaceId]); + queryClient.setQueryData(['threadData'], (old) => { + if (old) { + return { threads: [...old.threads, newThread] }; + } + }); }); return () => { From 33158b95e97858ef27bc2d7ef89586ae0027e39e Mon Sep 17 00:00:00 2001 From: Rulu <79538610+hafnium1923@users.noreply.github.com> Date: Tue, 5 Dec 2023 14:29:47 +0900 Subject: [PATCH 02/20] =?UTF-8?q?[FE]=20=EA=B3=B5=EC=A7=80=EC=97=90=20?= =?UTF-8?q?=EC=82=AC=EC=A7=84=20=EC=98=AC=EB=9D=BC=EA=B0=94=EC=9D=84=20?= =?UTF-8?q?=EA=B2=BD=EC=9A=B0=20UI=20=EC=88=98=EC=A0=95=20(#877)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 공지 이미지 컴포넌트 크기 줄이기 * feat: sm 사이즈를 제외하고 사진있을 시 공지에 노출 * refactor: 사용하지 않은 import삭제 --- .../NoticeThread/NoticeThread.stories.tsx | 37 +++++++++++++++++++ .../feed/NoticeThread/NoticeThread.styled.ts | 18 +++++++-- .../feed/NoticeThread/NoticeThread.tsx | 14 +++++-- .../ThumbnailList/ThumbnailList.styled.ts | 32 +++++++++++----- .../feed/ThumbnailList/ThumbnailList.tsx | 6 ++- .../ViewableThumbnail.styled.ts | 4 +- .../src/pages/TeamFeedPage/TeamFeedPage.tsx | 1 - 7 files changed, 92 insertions(+), 20 deletions(-) diff --git a/frontend/src/components/feed/NoticeThread/NoticeThread.stories.tsx b/frontend/src/components/feed/NoticeThread/NoticeThread.stories.tsx index 6eab472fa..9010b656c 100644 --- a/frontend/src/components/feed/NoticeThread/NoticeThread.stories.tsx +++ b/frontend/src/components/feed/NoticeThread/NoticeThread.stories.tsx @@ -67,3 +67,40 @@ export const TooLongContent: Story = { }, }, }; + +export const ImageContent: Story = { + args: { + authorName: '루루', + createdAt: '2023-12-01 04:12', + content: '중요공지!\n중요공지!\n중요공지!', + 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: 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://wrong-link.com/must-show-fallback.png', + }, + { + id: 3493, + isExpired: true, + name: '만료된 사진', + url: '', + }, + ], + onClickImage: () => { + alert('onClickImage'); + }, + }, +}; diff --git a/frontend/src/components/feed/NoticeThread/NoticeThread.styled.ts b/frontend/src/components/feed/NoticeThread/NoticeThread.styled.ts index db9aac880..208d2093f 100644 --- a/frontend/src/components/feed/NoticeThread/NoticeThread.styled.ts +++ b/frontend/src/components/feed/NoticeThread/NoticeThread.styled.ts @@ -5,6 +5,7 @@ import type { NoticeSize } from '~/types/size'; export const Container = styled.div<{ $noticeSize: NoticeSize; $isMobile: boolean; + $hasImage: boolean; }>` position: sticky; top: ${({ $isMobile }) => ($isMobile ? '-4px' : 0)}; @@ -19,11 +20,15 @@ export const Container = styled.div<{ transition: 0.3s; - ${({ $noticeSize }) => { + ${({ $noticeSize, $hasImage }) => { if ($noticeSize === 'sm') return css` height: 80px; `; + if ($noticeSize === 'md' && $hasImage) + return css` + height: 200px; + `; if ($noticeSize === 'md') return css` height: 140px; @@ -110,7 +115,9 @@ export const AuthorInfo = styled.div` height: 16px; `; -export const ContentContainer = styled.div<{ $noticeSize: NoticeSize }>` +export const ContentContainer = styled.div<{ + $noticeSize: NoticeSize; +}>` display: flex; flex-direction: column; justify-content: space-between; @@ -150,11 +157,14 @@ export const timeInfoText = css` color: ${({ theme }) => theme.color.GRAY500}; `; -export const contentField = (noticeSize: NoticeSize) => { +export const contentField = (noticeSize: NoticeSize, hasImage: boolean) => { let height = ''; if (noticeSize === 'sm') height = '24px'; - if (noticeSize === 'md') height = '66px'; + + if (noticeSize === 'md') + if (hasImage) height = '24px'; + else height = '66px'; if (noticeSize === 'lg') height = '100%'; return css` diff --git a/frontend/src/components/feed/NoticeThread/NoticeThread.tsx b/frontend/src/components/feed/NoticeThread/NoticeThread.tsx index 76eb77c8d..be9a4fe70 100644 --- a/frontend/src/components/feed/NoticeThread/NoticeThread.tsx +++ b/frontend/src/components/feed/NoticeThread/NoticeThread.tsx @@ -38,7 +38,11 @@ const NoticeThread = (props: NoticeThreadProps) => { }; return ( - + 0} + > { - + 0)} + > {content} - {images.length > 0 && noticeSize === 'lg' && ( + {images.length > 0 && noticeSize !== 'sm' && ( ` +export const Container = styled.ul<{ + $mode: 'delete' | 'view'; + $size: 'md' | 'sm' | undefined; +}>` display: flex; flex-direction: row; flex-shrink: 0; column-gap: 12px; width: 100%; - height: 116px; - ${({ $mode }) => - $mode === 'view' - ? css` + ${({ $mode, $size }) => { + if ($mode === 'view') + if ($size === 'sm') + return css` + overflow-x: auto; + overflow-y: hidden; + `; + else + return css` + height: 116px; + overflow-x: auto; overflow-y: hidden; padding-bottom: 20px; - ` - : css` - overflow-x: visible; - `} + `; + else + return css` + height: 116px; + + overflow-x: visible; + `; + }} `; diff --git a/frontend/src/components/feed/ThumbnailList/ThumbnailList.tsx b/frontend/src/components/feed/ThumbnailList/ThumbnailList.tsx index 7748c60d0..5aef77b0e 100644 --- a/frontend/src/components/feed/ThumbnailList/ThumbnailList.tsx +++ b/frontend/src/components/feed/ThumbnailList/ThumbnailList.tsx @@ -27,7 +27,11 @@ const ThumbnailList = (props: ThumbnailListProps) => { const { mode, images } = props; return ( - + {mode === 'delete' ? images.map((image) => ( ` flex-shrink: 0; - width: ${({ $size = 'md' }) => ($size === 'md' ? '96px' : '76px')}; - height: ${({ $size = 'md' }) => ($size === 'md' ? '96px' : '76px')}; + width: ${({ $size = 'md' }) => ($size === 'md' ? '96px' : '56px')}; + height: ${({ $size = 'md' }) => ($size === 'md' ? '96px' : '56px')}; border-radius: ${({ $size = 'md' }) => ($size === 'md' ? '12px' : '10px')}; `; diff --git a/frontend/src/pages/TeamFeedPage/TeamFeedPage.tsx b/frontend/src/pages/TeamFeedPage/TeamFeedPage.tsx index dce38e14d..5f16eead0 100644 --- a/frontend/src/pages/TeamFeedPage/TeamFeedPage.tsx +++ b/frontend/src/pages/TeamFeedPage/TeamFeedPage.tsx @@ -14,7 +14,6 @@ import * as S from './TeamFeedPage.styled'; import { useModal } from '~/hooks/useModal'; import { useState } from 'react'; import type { ThreadImage } from '~/types/feed'; -import { useTeamPlace } from '~/hooks/useTeamPlace'; import { getIsMobile } from '~/utils/getIsMobile'; interface TeamFeedPageProps { From ea409b06508d07de18ad4eec41e97e3465827d18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9A=94=EC=88=A0=ED=86=A0=EB=81=BC?= Date: Mon, 11 Dec 2023 13:19:23 +0900 Subject: [PATCH 03/20] =?UTF-8?q?[FE]=20=EC=8A=A4=EC=BC=80=EC=A4=84=20?= =?UTF-8?q?=EB=B0=94=EB=A5=BC=20=EB=93=9C=EB=9E=98=EA=B7=B8=ED=95=98?= =?UTF-8?q?=EC=97=AC=20=EC=88=98=EC=A0=95=ED=95=A0=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20(#857)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 캘린더 드래그가 진행될 가짜 드래그 화면 컴포넌트 생성, backdrop 화면이 나타나도록 구현 * feat: CalendarDragScreen을 캘린더 컴포넌트에 부착 * feat: 캘린더 컴포넌트에서 컨트롤할 수 있도록 스케줄 바 드래그 현황을 관리하는 커스텀 훅 추가 - 스케줄 바 드래그 현황에 대한 데이터를 가짜 드래그 스크린에 prop으로 넘길 수 있도록 값 관리 - 스케줄 바의 드래그를 시작하는 경우 / 드래그를 중지하는 경우에 따라 값을 업데이트할 수 있도록 관리 * refactor: 스케줄 바 컴포넌트 드래그 시작 시 이벤트 객체를 함께 넘기도록 변경 * feat: 좌표를 의미하는 Point 타입 추가 - x, y값을 지님 * feat: 움직이는 스케줄 바 컴포넌트 구현 - 이후 마우스 좌표를 받아 마우스를 따라가는 위치에 렌더링되도록 사용 예정 * test: 움직이는 스케줄 바 컴포넌트에 대한 스토리북 작성 * feat: useCalendarDragScreen 커스텀 훅에 상대좌표 업데이트 기능 및 스케줄 바로의 변환 기능 추가 * feat: 가짜 스크롤 기능 구현을 위해 커스텀 훅과 캘린더 컴포넌트의 로직을 결합 * feat: 캘린더의 크기에 따라 CalendarDragScreen에 표시되는 스케줄 바의 크기가 반영되도록 개선 * feat: 초기 좌표를 CalendarDragScreen 자체에서 관리하고, 커스텀 훅 내부에서 절대 좌표를 사용할 수 있도록 개선 * feat: 비활성 상태일 경우에도 unmount 되지 않도록 변경 - display: none을 이용하여 커스텀 훅의 state와 listener를 유지 * feat: 캘린더 크기와 상대좌표를 이용해 캘린더 칸 이동 수치를 구하는 유틸 함수 작성 - '칸 이동 수치'란 시작 좌표를 기준으로 행, 열을 얼마나 이동했는지를 의미하는 수치이다. 이는 캘린더 바의 모양을 어떻게 정할지를 계산하는 데 사용된다. * feat: 날짜 포맷을 받아 n일 지난 날짜 포맷을 리턴해 주는 유틸 함수 작성 * feat: 움직이는 스케줄 바에 대한 정보를 생성하는 유틸 함수 작성 * feat: 절대 좌표 방식에 의존하지 않도록 개선 * feat: ScheduleBar에서 초기 좌표를 전달하는 방식으로 재변경 - onMouseDown 이벤트로 인해 스케줄 바에 해당하는 일정 모달이 띄워지지 않는 문제가 있음 * feat: ScheduleBar에 mode 타입 추가 - 스케줄 바가 이제 여러 목적(일반, 장식용, 인디케이터)으로 쓰이므로, mode를 통해 목적을 정한다. * feat: ScheduleBar 컴포넌트에 mode 프로퍼티를 추가, no-interaction 모드에 대한 기능을 구현 - no-interaction: 스케줄 바에 hover을 해도 반응이 없으며, 포인터가 바뀌지 않는 등 상호작용이 불가능해진다. 시각적인 용도로 사용해야 할 때 사용한다. * test: ScheduleBar 스토리북에 no-interaction인 경우에 해당하는 스토리를 추가 * feat: MovingScheduleBar의 스케줄 바는 상호작용이 불가능하도록 유틸함수 개선 * feat: ScheduleBar에서 indicator 모드에 대한 기능을 구현 - indicator 모드: 상호작용이 불가능하며, 스케줄 바의 윤곽만 나타난다. 스케줄 바가 놓일 위치를 시각적으로 표시하는 데에 사용한다. * test: ScheduleBar 스토리북에 indicator인 경우에 해당하는 스토리를 추가 * feat: ScheduleIndicator 컴포넌트 구현 - 스케줄 바를 인디케이터 형태로 보여주는 컴포넌트 * test: ScheduleIndicator 컴포넌트에 해당하는 스토리 작성 * feat: 두 종류의 가짜 스케줄 바 컴포넌트를 하나의 컴포넌트로 통합 * feat: CalendarDragScreen에 통합된 가짜 스케줄 바 컴포넌트를 부착 * feat: 스케줄 바의 변수명 변경, 인디케이터 모드일 경우 화살표를 보여주지 않도록 수정 * feat: 가짜 스케줄바를 반환하는 유틸함수명 변경, 세로로 스케줄바를 이동시켜도 자연스럽게 작동하도록 개선 * feat: 가짜 캘린더 바를 이동시킬 때 세로 방향으로도 이동이 자연스럽도록 개선, 전반적인 이동성을 개선 * feat: 스크롤을 끝내면 변경된 스케줄 정보를 post형태로 요청하도록 기능 구현 * feat: 드래그한 이후 일정이 변하지 않았을 경우, API 요청을 보내지 않도록 로직 개선 * refactor: FakeScheduleBarScreen 컴포넌트에서 자주 바뀌는 스타일을 attrs로 받도록 변경 - 불필요함에도 불구하고 클래스가 매번 새롭게 생성되는 현상을 방지하고, 인라인 스타일만 수정되도록 하여 퍼포먼스 향상을 기대할 수 있음 * chore: 불필요한 Point 타입 제거 * chore: 유틸 함수, 커스텀 훅의 파일 위치 변경 * refactor: 캘린더 날짜의 변화값을 구하는 유틸함수의 이름명 변경, 내부 변수 상수화 * fix: 스케줄 바가 범위 밖으로 이동하여 수정이 되었을 때 오류가 생기는 경우를 해결 - 스케줄 바에서 정보를 가져오는 데 이동된 스케줄 바가 더 이상 렌더링되지 않으므로 생기는 문제. 스케줄 바가 비어 있더라도 일정 시작과 끝 정보는 보존되도록 유틸 함수를 수정 * refactor: 꼭 필요한 경우에만 함수가 다시 생성되도록 useCallback 사용, 불필요한 useMemo 제거 - 꼭 필요한 경우에만 생성되도록 한쪽에 쏠린 dependency array를 배분 * fix: 캘린더 바를 드래그할 때 아주 잠깐 잘못된 좌표로 이동하며 버벅거리는 문제 해결 - 이전 캘린더 바의 이동에서 남아있는 상대좌표의 값이 이후의 캘린더 바의 이동에 영향을 끼친 것. 이동 완료 후 상대좌표의 값을 0으로 초기화시키는 것으로 해결 * chore: 스케줄 바 생성 유틸함수의 import 경로를 절대경로로 변경 * refactor: 스케줄 바 타입에 calendarSize 프로퍼티 추가 - 직접 해당 타입을 스케줄 바에서 사용하게 되는 일이 많아짐에 따라 추가하게 됨 * test: 상대좌표 기반 스케줄 바 유틸함수에 대한 좌표 대응 테스트 작성 * test: 상대좌표 기반 스케줄 바 유틸함수에 대한 캘린더 크기 대응 테스트 작성 * test: 상대좌표 기반 스케줄 바 유틸함수에 대한 부가 기능 테스트 작성 * chore: 상대좌표 기반 스케줄 바 유틸함수에 JSDoc 주석 추가 * chore: useScheduleBarDragStatus 커스텀 훅을 hooks 폴더로 이동 * refactor: 불필요 파라미터 제거, onMouseUp의 타입 명시 * refactor: as를 사용하는 대신 빈 스케줄 데이터를 선언해 스케줄이 비었을 경우 사용하도록 변경 * refactor: CalendarDragScreen의 backdrop 색상을 이미 선언된 색상으로 변경 * refactor: 날짜 포맷 props의 타입으로 YYYYMMDDHHMM을 사용하도록 변경 * refactor: 일정 변경 성공 시 드래그 상태에 저장되어 있는 스케줄을 빈 스케줄로 변경 * refactor: dragStatus 타입 분리 및 드래그 스크린에서 값을 dragStatus로 받도록 변경 * refactor: 더미 스케줄 바 정보를 사용하는 대신, null 타입으로 빈 스케줄을 관리하도록 변경 --- .../CalendarDragScreen.styled.ts | 16 + .../CalendarDragScreen/CalendarDragScreen.tsx | 51 +++ .../FakeScheduleBarsScreen.stories.tsx | 127 +++++++ .../FakeScheduleBarsScreen.styled.ts | 22 ++ .../FakeScheduleBarsScreen.tsx | 45 +++ .../ScheduleBar/ScheduleBar.stories.tsx | 48 +++ .../ScheduleBar/ScheduleBar.styled.ts | 39 +- .../team_calendar/ScheduleBar/ScheduleBar.tsx | 32 +- .../TeamCalendar/TeamCalendar.styled.ts | 4 + .../TeamCalendar/TeamCalendar.tsx | 30 +- .../hooks/schedule/useCalendarDragScreen.ts | 150 ++++++++ .../schedule/useScheduleBarDragStatus.ts | 82 ++++ frontend/src/types/schedule.ts | 11 + .../utils/generateScheduleBarsByMousePoint.ts | 132 +++++++ .../utils/test/generateScheduleBars.test.ts | 2 +- .../generateScheduleBarsByMousePoint.test.ts | 353 ++++++++++++++++++ 16 files changed, 1122 insertions(+), 22 deletions(-) create mode 100644 frontend/src/components/team_calendar/CalendarDragScreen/CalendarDragScreen.styled.ts create mode 100644 frontend/src/components/team_calendar/CalendarDragScreen/CalendarDragScreen.tsx create mode 100644 frontend/src/components/team_calendar/FakeScheduleBarsScreen/FakeScheduleBarsScreen.stories.tsx create mode 100644 frontend/src/components/team_calendar/FakeScheduleBarsScreen/FakeScheduleBarsScreen.styled.ts create mode 100644 frontend/src/components/team_calendar/FakeScheduleBarsScreen/FakeScheduleBarsScreen.tsx create mode 100644 frontend/src/hooks/schedule/useCalendarDragScreen.ts create mode 100644 frontend/src/hooks/schedule/useScheduleBarDragStatus.ts create mode 100644 frontend/src/utils/generateScheduleBarsByMousePoint.ts create mode 100644 frontend/src/utils/test/generateScheduleBarsByMousePoint.test.ts diff --git a/frontend/src/components/team_calendar/CalendarDragScreen/CalendarDragScreen.styled.ts b/frontend/src/components/team_calendar/CalendarDragScreen/CalendarDragScreen.styled.ts new file mode 100644 index 000000000..9b746248b --- /dev/null +++ b/frontend/src/components/team_calendar/CalendarDragScreen/CalendarDragScreen.styled.ts @@ -0,0 +1,16 @@ +import { styled } from 'styled-components'; + +export const Container = styled.div<{ $isDragging: boolean }>` + ${({ $isDragging }) => !$isDragging && 'display: none'}; + position: absolute; + overflow: hidden; + left: 0; + top: 0; + + width: 100%; + height: 100%; + + background-color: ${({ theme }) => theme.color.WHITE_BLUR}; + + cursor: all-scroll; +`; diff --git a/frontend/src/components/team_calendar/CalendarDragScreen/CalendarDragScreen.tsx b/frontend/src/components/team_calendar/CalendarDragScreen/CalendarDragScreen.tsx new file mode 100644 index 000000000..500e6153b --- /dev/null +++ b/frontend/src/components/team_calendar/CalendarDragScreen/CalendarDragScreen.tsx @@ -0,0 +1,51 @@ +import * as S from './CalendarDragScreen.styled'; +import { useRef } from 'react'; +import FakeScheduleBarsScreen from '~/components/team_calendar/FakeScheduleBarsScreen/FakeScheduleBarsScreen'; +import type { YYYYMMDDHHMM, DragStatus } from '~/types/schedule'; +import type { CalendarSize } from '~/types/size'; +import { useCalendarDragScreen } from '~/hooks/schedule/useCalendarDragScreen'; + +interface CalendarDragScreenProps { + calendarSize: CalendarSize; + year: number; + month: number; + dragStatus: DragStatus; + onMouseUp: ( + title: string, + startDateTime: YYYYMMDDHHMM, + endDateTime: YYYYMMDDHHMM, + shouldUpdate: boolean, + ) => void; +} + +const CalendarDragScreen = (props: CalendarDragScreenProps) => { + const { calendarSize, year, month, dragStatus, onMouseUp } = props; + const { isDragging, level, schedule, initX, initY } = dragStatus; + const calendarRef = useRef(null); + const { scheduleBars, relativeX, relativeY } = useCalendarDragScreen({ + isDragging, + initX, + initY, + calendarRef, + calendarSize, + onMouseUp, + year, + month, + level, + schedule, + }); + + return ( + + + + + ); +}; + +export default CalendarDragScreen; diff --git a/frontend/src/components/team_calendar/FakeScheduleBarsScreen/FakeScheduleBarsScreen.stories.tsx b/frontend/src/components/team_calendar/FakeScheduleBarsScreen/FakeScheduleBarsScreen.stories.tsx new file mode 100644 index 000000000..66a9c0bc4 --- /dev/null +++ b/frontend/src/components/team_calendar/FakeScheduleBarsScreen/FakeScheduleBarsScreen.stories.tsx @@ -0,0 +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, + }, +}; diff --git a/frontend/src/components/team_calendar/FakeScheduleBarsScreen/FakeScheduleBarsScreen.styled.ts b/frontend/src/components/team_calendar/FakeScheduleBarsScreen/FakeScheduleBarsScreen.styled.ts new file mode 100644 index 000000000..b8e16388a --- /dev/null +++ b/frontend/src/components/team_calendar/FakeScheduleBarsScreen/FakeScheduleBarsScreen.styled.ts @@ -0,0 +1,22 @@ +import { styled } from 'styled-components'; + +export const Container = styled.div.attrs<{ + $relativeX: number; + $relativeY: number; +}>(({ $relativeX, $relativeY }) => ({ + style: { + transform: `translate(${$relativeX}px, ${$relativeY}px)`, + }, +}))` + display: flex; + flex-direction: column; + position: absolute; + + width: 100%; + height: 100%; +`; + +export const CalendarRow = styled.div` + position: relative; + flex-grow: 1; +`; diff --git a/frontend/src/components/team_calendar/FakeScheduleBarsScreen/FakeScheduleBarsScreen.tsx b/frontend/src/components/team_calendar/FakeScheduleBarsScreen/FakeScheduleBarsScreen.tsx new file mode 100644 index 000000000..ab67b64bc --- /dev/null +++ b/frontend/src/components/team_calendar/FakeScheduleBarsScreen/FakeScheduleBarsScreen.tsx @@ -0,0 +1,45 @@ +import * as S from './FakeScheduleBarsScreen.styled'; +import ScheduleBar from '~/components/team_calendar/ScheduleBar/ScheduleBar'; +import { arrayOf } from '~/utils/arrayOf'; +import type { GeneratedScheduleBar } from '~/types/schedule'; + +interface ScheduleModeProps { + mode: 'schedule'; + scheduleBars: GeneratedScheduleBar[]; + relativeX: number; + relativeY: number; +} + +interface IndicatorModeProps { + mode: 'indicator'; + scheduleBars: GeneratedScheduleBar[]; +} + +type FakeScheduleBarsScreenProps = ScheduleModeProps | IndicatorModeProps; + +const FakeScheduleBarsScreen = (props: FakeScheduleBarsScreenProps) => { + const { mode, scheduleBars } = props; + + return ( + + {arrayOf(6).map((_, rowIndex) => ( + + {scheduleBars.map((scheduleBar) => { + return scheduleBar.row === rowIndex ? ( + + ) : null; + })} + + ))} + + ); +}; + +export default FakeScheduleBarsScreen; diff --git a/frontend/src/components/team_calendar/ScheduleBar/ScheduleBar.stories.tsx b/frontend/src/components/team_calendar/ScheduleBar/ScheduleBar.stories.tsx index bc6912594..7cd85f4b7 100644 --- a/frontend/src/components/team_calendar/ScheduleBar/ScheduleBar.stories.tsx +++ b/frontend/src/components/team_calendar/ScheduleBar/ScheduleBar.stories.tsx @@ -119,3 +119,51 @@ export const LongTitle: Story = { onClick: () => alert('clicked!'), }, }; + +/** + * `mode` 값이 `no-interaction`일 경우, 해당 캘린더 바는 오로지 장식 용도가 되며 **상호작용이 불가능**하게 됩니다. 가짜 스케줄 바 드래그 화면 등 시각적인 효과를 위해 사용할 수 있습니다. + */ +export const NoInteraction: Story = { + args: { + id: '1', + scheduleId: 1, + schedule: { + id: 1, + title: 'No Interaction', + startDateTime: '2023-07-07 05:00', + endDateTime: '2023-07-09 10:00', + }, + title: 'No Interaction', + row: 1, + column: 2, + duration: 3, + level: 0, + roundedStart: true, + roundedEnd: true, + mode: 'no-interaction', + }, +}; + +/** + * `mode` 값이 `indicator`일 경우, 해당 캘린더 바는 **상호작용이 불가능하고 캘린더 바의 윤곽만 드러내는** 시각적 요소가 됩니다. 캘린더 바가 놓일 위치를 시각적으로 표시하는 데에 사용합니다. + */ +export const Indicator: Story = { + args: { + id: '1', + scheduleId: 1, + schedule: { + id: 1, + title: 'This should not shown', + startDateTime: '2023-07-07 05:00', + endDateTime: '2023-07-09 10:00', + }, + title: 'This should not shown', + row: 1, + column: 2, + duration: 3, + level: 0, + roundedStart: true, + roundedEnd: true, + mode: 'indicator', + }, +}; diff --git a/frontend/src/components/team_calendar/ScheduleBar/ScheduleBar.styled.ts b/frontend/src/components/team_calendar/ScheduleBar/ScheduleBar.styled.ts index 82885925c..101f68762 100644 --- a/frontend/src/components/team_calendar/ScheduleBar/ScheduleBar.styled.ts +++ b/frontend/src/components/team_calendar/ScheduleBar/ScheduleBar.styled.ts @@ -9,6 +9,7 @@ interface InnerProps { level: number; roundedStart: boolean; roundedEnd: boolean; + mode: 'normal' | 'no-interaction' | 'indicator'; teamPlaceColor: TeamPlaceColor; } @@ -25,6 +26,7 @@ export const Wrapper = styled.div.withConfig({ 'duration', 'roundedStart', 'roundedEnd', + 'mode', ].includes(prop), })< Pick< @@ -35,6 +37,7 @@ export const Wrapper = styled.div.withConfig({ | 'duration' | 'roundedStart' | 'roundedEnd' + | 'mode' > >` position: absolute; @@ -52,9 +55,7 @@ export const Wrapper = styled.div.withConfig({ }} left: ${({ column }) => (column * 100) / 7}%; - width: ${({ duration }) => (duration * 100) / 7}%; - padding: ${({ roundedStart, roundedEnd }) => `0 ${roundedEnd ? '4px' : 0} 0 ${roundedStart ? '4px' : 0}`}; `; @@ -72,6 +73,7 @@ export const Inner = styled.div.withConfig({ 'roundedStart', 'roundedEnd', 'teamPlaceColor', + 'mode', ].includes(prop), })` display: flex; @@ -81,20 +83,39 @@ export const Inner = styled.div.withConfig({ height: 100%; padding-left: 6px; - background-color: ${({ theme, teamPlaceColor = 0 }) => - theme.teamColor[teamPlaceColor]}; + background-color: ${({ theme, teamPlaceColor = 0, mode }) => + mode === 'indicator' ? 'transparent' : theme.teamColor[teamPlaceColor]}; border-radius: ${({ roundedStart, roundedEnd }) => `${roundedStart ? '4px' : '0'} ${roundedEnd ? '4px 4px' : '0 0'} ${ roundedStart ? '4px' : '0' }`}; - filter: brightness(${({ level }) => 1 + level * 0.4}); + ${({ mode, theme }) => + mode === 'indicator' && + css` + margin-top: -2px; + + border: 2px solid ${theme.color.GRAY400}; + + box-shadow: 0 0 24px ${theme.color.GRAY600}; + box-sizing: content-box; + `}; + + ${({ mode, level }) => + mode !== 'indicator' && + css` + filter: brightness(${1 + level * 0.4}); + `}; - cursor: pointer; + ${({ mode }) => + mode === 'normal' && + css` + cursor: pointer; - &:hover { - opacity: 0.8; - } + &:hover { + opacity: 0.8; + } + `}; `; export const scheduleBarTitle = (calendarSize: CalendarSize) => css` diff --git a/frontend/src/components/team_calendar/ScheduleBar/ScheduleBar.tsx b/frontend/src/components/team_calendar/ScheduleBar/ScheduleBar.tsx index 943b50ded..3473542a8 100644 --- a/frontend/src/components/team_calendar/ScheduleBar/ScheduleBar.tsx +++ b/frontend/src/components/team_calendar/ScheduleBar/ScheduleBar.tsx @@ -3,34 +3,50 @@ import * as S from './ScheduleBar.styled'; import type { GeneratedScheduleBar } from '~/types/schedule'; import { DoubleArrowRightIcon } from '~/assets/svg'; import { useTeamPlace } from '~/hooks/useTeamPlace'; -import type { CalendarSize } from '~/types/size'; +import type { MouseEvent } from 'react'; export interface ScheduleBarProps extends GeneratedScheduleBar { - calendarSize?: CalendarSize; onClick?: () => void; + onDragStart?: (e: MouseEvent) => void; } const ScheduleBar = (props: ScheduleBarProps) => { - const { title, onClick, roundedEnd, calendarSize = 'md', ...rest } = props; + const { + title, + onClick, + roundedEnd, + onDragStart, + mode = 'normal', + calendarSize = 'md', + ...rest + } = props; const { teamPlaceColor } = useTeamPlace(); + const isInteractive = mode === 'normal'; + const isIndicator = mode === 'indicator'; return ( - - {title} - - {!roundedEnd && } + {!isIndicator && ( + + {title} + + )} + {!roundedEnd && !isIndicator && } ); diff --git a/frontend/src/components/team_calendar/TeamCalendar/TeamCalendar.styled.ts b/frontend/src/components/team_calendar/TeamCalendar/TeamCalendar.styled.ts index 3244f27a4..0e66e150a 100644 --- a/frontend/src/components/team_calendar/TeamCalendar/TeamCalendar.styled.ts +++ b/frontend/src/components/team_calendar/TeamCalendar/TeamCalendar.styled.ts @@ -17,6 +17,10 @@ export const CalendarHeader = styled.div` justify-content: space-between; `; +export const CalendarGrid = styled.div` + position: relative; +`; + export const ButtonContainer = styled.div` display: flex; align-items: center; diff --git a/frontend/src/components/team_calendar/TeamCalendar/TeamCalendar.tsx b/frontend/src/components/team_calendar/TeamCalendar/TeamCalendar.tsx index 062123453..d64dba882 100644 --- a/frontend/src/components/team_calendar/TeamCalendar/TeamCalendar.tsx +++ b/frontend/src/components/team_calendar/TeamCalendar/TeamCalendar.tsx @@ -16,6 +16,7 @@ import { useModal } from '~/hooks/useModal'; import { useTeamPlace } from '~/hooks/useTeamPlace'; import { useCalendarResizePosition } from '~/hooks/useCalendarResizePosition'; import { usePrefetchSchedules } from '~/hooks/queries/usePrefetchSchedules'; +import { useScheduleDragStatus } from '~/hooks/schedule/useScheduleBarDragStatus'; import { DAYS_OF_WEEK, MODAL_OPEN_TYPE } from '~/constants/calendar'; import { generateScheduleBars } from '~/utils/generateScheduleBars'; import { arrayOf } from '~/utils/arrayOf'; @@ -30,6 +31,7 @@ import { } from '~/assets/svg'; import * as S from './TeamCalendar.styled'; import { parseDate } from '~/utils/parseDate'; +import CalendarDragScreen from '../CalendarDragScreen/CalendarDragScreen'; import Spacing from '~/components/common/Spacing/Spacing'; interface TeamCalendarProps { @@ -39,6 +41,9 @@ interface TeamCalendarProps { const TeamCalendar = (props: TeamCalendarProps) => { const { calendarSize = 'md' } = props; + const { dragStatus, handleDragStart, handleMouseUp } = + useScheduleDragStatus(); + const { teamPlaceId } = useTeamPlace(); const { year, @@ -224,14 +229,21 @@ const TeamCalendar = (props: TeamCalendarProps) => { return {day}; })} -
+ {calendar.map((week, rowIndex) => { return ( {scheduleBars.map((scheduleBar) => { - const { id, scheduleId, row, column, level, duration } = - scheduleBar; + const { + id, + scheduleId, + row, + column, + level, + duration, + schedule, + } = scheduleBar; if (row === rowIndex && level > 2) return arrayOf(duration).map((_, index) => { @@ -272,6 +284,9 @@ const TeamCalendar = (props: TeamCalendarProps) => { level, }); }} + onDragStart={(e) => + handleDragStart(e, level, schedule) + } {...scheduleBar} /> ); @@ -316,7 +331,14 @@ const TeamCalendar = (props: TeamCalendarProps) => { ); })} -
+ +
{modal} diff --git a/frontend/src/hooks/schedule/useCalendarDragScreen.ts b/frontend/src/hooks/schedule/useCalendarDragScreen.ts new file mode 100644 index 000000000..fe98e8005 --- /dev/null +++ b/frontend/src/hooks/schedule/useCalendarDragScreen.ts @@ -0,0 +1,150 @@ +import { useState, useEffect, useCallback } from 'react'; +import { generateScheduleBarsByMousePoint } from '~/utils/generateScheduleBarsByMousePoint'; +import type { RefObject } from 'react'; +import type { Schedule, YYYYMMDDHHMM } from '~/types/schedule'; +import type { CalendarSize } from '~/types/size'; + +interface UseCalendarDragScreenProps { + isDragging: boolean; + calendarRef: RefObject; + calendarSize: CalendarSize; + onMouseUp: ( + title: string, + startDateTime: YYYYMMDDHHMM, + endDateTime: YYYYMMDDHHMM, + shouldUpdate: boolean, + ) => void; + initX: number; + initY: number; + year: number; + month: number; + level: number; + schedule: Schedule | null; +} + +interface CalendarPointInfos { + relativeX: number; + relativeY: number; + calendarWidth: number; + calendarHeight: number; +} + +export const useCalendarDragScreen = (props: UseCalendarDragScreenProps) => { + const { + isDragging, + calendarRef, + calendarSize, + initX, + initY, + onMouseUp, + year, + month, + level, + schedule, + } = props; + const [calendarPointInfos, setCalendarPointInfos] = + useState({ + relativeX: 0, + relativeY: 0, + calendarWidth: 0, + calendarHeight: 0, + }); + const { relativeX, relativeY, calendarWidth, calendarHeight } = + calendarPointInfos; + + const scheduleBarsInfo = + schedule === null + ? null + : generateScheduleBarsByMousePoint({ + schedule, + year, + month, + relativeX, + relativeY, + calendarWidth, + calendarHeight, + level, + calendarSize, + }); + const getProcessedRelativePoint = () => { + const processedRelativeX = + ((relativeX + calendarWidth * (15 / 14)) % (calendarWidth / 7)) - + calendarWidth / 14; + const processedRelativeY = + ((relativeY + calendarHeight * (13 / 12)) % (calendarHeight / 6)) - + calendarHeight / 12; + + return { x: processedRelativeX, y: processedRelativeY }; + }; + + const handleMouseMove = useCallback( + (e: globalThis.MouseEvent) => { + if (!isDragging) { + return; + } + + const { clientX, clientY } = e; + + setCalendarPointInfos((prev) => ({ + ...prev, + relativeX: clientX - initX, + relativeY: clientY - initY, + })); + }, + [initX, initY, isDragging], + ); + + const handleMouseUp = useCallback(() => { + if (!isDragging || !scheduleBarsInfo || !schedule) { + return; + } + + const { title } = schedule; + const { startDateTime, endDateTime } = scheduleBarsInfo; + const shouldUpdate = schedule.startDateTime !== startDateTime; + + onMouseUp(title, startDateTime, endDateTime, shouldUpdate); + + setCalendarPointInfos((prev) => ({ + ...prev, + relativeX: 0, + relativeY: 0, + })); + }, [onMouseUp, schedule, scheduleBarsInfo, isDragging]); + + useEffect(() => { + const calendarElement = calendarRef.current; + + if (!calendarElement) { + return; + } + + const resizeObserver = new ResizeObserver(() => { + const { clientWidth, clientHeight } = calendarElement; + + setCalendarPointInfos((prev) => ({ + ...prev, + calendarWidth: clientWidth, + calendarHeight: clientHeight, + })); + }); + + calendarElement.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + resizeObserver.observe(calendarElement); + + return () => { + calendarElement.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + resizeObserver.disconnect(); + }; + }, [calendarRef, handleMouseMove, handleMouseUp]); + + const processedRelativePoint = getProcessedRelativePoint(); + + return { + scheduleBars: scheduleBarsInfo ? scheduleBarsInfo.scheduleBars : [], + relativeX: processedRelativePoint.x, + relativeY: processedRelativePoint.y, + }; +}; diff --git a/frontend/src/hooks/schedule/useScheduleBarDragStatus.ts b/frontend/src/hooks/schedule/useScheduleBarDragStatus.ts new file mode 100644 index 000000000..c9a0558a3 --- /dev/null +++ b/frontend/src/hooks/schedule/useScheduleBarDragStatus.ts @@ -0,0 +1,82 @@ +import { useState } from 'react'; +import { useTeamPlace } from '~/hooks/useTeamPlace'; +import { useToast } from '~/hooks/useToast'; +import { useModifySchedule } from '~/hooks/queries/useModifySchedule'; +import type { MouseEvent } from 'react'; +import type { Schedule, YYYYMMDDHHMM, DragStatus } from '~/types/schedule'; + +export const useScheduleDragStatus = () => { + const [dragStatus, setDragStatus] = useState({ + isDragging: false, + level: 0, + schedule: null, + initX: 0, + initY: 0, + }); + const { showToast } = useToast(); + const { teamPlaceId } = useTeamPlace(); + const scheduleId = dragStatus.schedule === null ? 0 : dragStatus.schedule.id; + const { mutateModifySchedule } = useModifySchedule(teamPlaceId, scheduleId); + + const handleDragStart = ( + e: MouseEvent, + level: number, + schedule: Schedule, + ) => { + const { clientX, clientY } = e; + + setDragStatus(() => ({ + isDragging: true, + schedule, + level, + initX: clientX, + initY: clientY, + })); + }; + + const handleMouseUp = ( + title: string, + startDateTime: YYYYMMDDHHMM, + endDateTime: YYYYMMDDHHMM, + shouldUpdate: boolean, + ) => { + if (!dragStatus.isDragging) { + return; + } + + setDragStatus((prev) => ({ + ...prev, + isDragging: false, + })); + + if (!shouldUpdate) { + return; + } + + mutateModifySchedule( + { + title, + startDateTime, + endDateTime, + }, + { + onSuccess: () => { + showToast('success', '일정이 수정되었습니다.'); + + setDragStatus((prev) => ({ + ...prev, + schedule: null, + })); + }, + onError: (error) => { + const response = error as Response; + + if (response.status === 500) + showToast('error', '일정 제목이 최대 글자(250자)를 초과했습니다.'); + }, + }, + ); + }; + + return { dragStatus, handleDragStart, handleMouseUp }; +}; diff --git a/frontend/src/types/schedule.ts b/frontend/src/types/schedule.ts index b85aca6dd..661a85396 100644 --- a/frontend/src/types/schedule.ts +++ b/frontend/src/types/schedule.ts @@ -1,4 +1,5 @@ import type { MODAL_OPEN_TYPE } from '~/constants/calendar'; +import type { CalendarSize } from '~/types/size'; export interface Schedule { id: number; @@ -46,4 +47,14 @@ export interface GeneratedScheduleBar { level: number; roundedStart: boolean; roundedEnd: boolean; + calendarSize?: CalendarSize; + mode?: 'normal' | 'no-interaction' | 'indicator'; +} + +export interface DragStatus { + isDragging: boolean; + level: number; + schedule: Schedule | null; + initX: number; + initY: number; } diff --git a/frontend/src/utils/generateScheduleBarsByMousePoint.ts b/frontend/src/utils/generateScheduleBarsByMousePoint.ts new file mode 100644 index 000000000..42b72477c --- /dev/null +++ b/frontend/src/utils/generateScheduleBarsByMousePoint.ts @@ -0,0 +1,132 @@ +import { generateScheduleBars } from '~/utils/generateScheduleBars'; +import { CALENDAR, ONE_DAY } from '~/constants/calendar'; +import type { Schedule, YYYYMMDDHHMM } from '~/types/schedule'; +import type { CalendarSize } from '~/types/size'; + +interface GenerateScheduleBarsByMousePointProps { + schedule: Schedule; + year: number; + month: number; + relativeX: number; + relativeY: number; + calendarWidth: number; + calendarHeight: number; + level: number; + calendarSize: CalendarSize; +} + +/** + * 《generateScheduleBarsByMousePoint》 + * 제공된 마우스 상대좌표를 기반으로 렌더링에 적합한 모양의 스케줄 바와 렌더링에 필요한 부가 정보들을 생성하여 반환합니다. + * + * @typedef {GenerateScheduleBarsByMousePointProps} params + * @property {schedule} schedule - 캘린더 바 생성에 사용할 일정 정보를 의미합니다. + * @property {number} year - 캘린더의 연도를 의미합니다. + * @property {number} month - 캘린더의 달을 의미합니다. 수를 0부터 셈에 주의하세요. + * @property {number} relativeX - 드래그를 시작한 지점을 기준으로 현재 마우스의 상대적인 x좌표를 의미합니다. + * @property {number} relativeY - 드래그를 시작한 지점을 기준으로 현재 마우스의 상대적인 y좌표를 의미합니다. + * @property {number} calendarWidth - 캘린더 컴포넌트의 가로 길이를 의미합니다. + * @property {number} calendarHeight - 캘린더 컴포넌트의 세로 길이를 의미합니다. + * @property {number} level - 생성되는 스케줄 바에 지정되어야 할 레벨을 의미합니다. 레벨이란 여러 스케줄 바가 겹칠 경우 어느 위치에 렌더링되어야 할 지를 결정하는 값으로, 0이 최상단이고 값이 오를수록 아래에 배치됩니다. + * @property {CalendarSize} calendarSize - 이 함수를 사용하는 캘린더의 크기를 의미합니다. 캘린더의 크기에 따라 생성되는 스케줄 바의 크기도 달라집니다. + * + * @returns {Object} + * @property {GeneratedScheduleBar[]} scheduleBars - 생성된 스케줄 바들을 의미합니다. + * @property {YYYYMMDDHHMM} startDateTime - 상대좌표를 고려하여 새롭게 반영된 시작 날짜를 의미합니다. + * @property {YYYYMMDDHHMM} endDateTime - 상대좌표를 고려하여 새롭게 반영된 끝 날짜를 의미합니다. + */ +export const generateScheduleBarsByMousePoint = ( + params: GenerateScheduleBarsByMousePointProps, +) => { + const { + schedule, + year, + month, + relativeX, + relativeY, + calendarWidth, + calendarHeight, + level, + calendarSize, + } = params; + + const difference = getCalendarDateDifferenceByMousePoint( + relativeX, + relativeY, + calendarWidth, + calendarHeight, + ); + + const { startDateTime, endDateTime } = schedule; + const changedStartDateTime = changeDateTimeByDays(startDateTime, difference); + const changedEndDateTime = changeDateTimeByDays(endDateTime, difference); + const generatedScheduleBars = generateScheduleBars(year, month, [ + { + ...schedule, + startDateTime: changedStartDateTime, + endDateTime: changedEndDateTime, + }, + ]).map((scheduleBar) => ({ + ...scheduleBar, + level, + calendarSize, + })); + + return { + scheduleBars: generatedScheduleBars, + startDateTime: changedStartDateTime, + endDateTime: changedEndDateTime, + }; +}; + +/** + * 《getCalendarDateDifferenceByMousePoint》 + * 제공된 마우스 상대좌표를 기반으로 올바른 모양의 캘린더 바를 보여주려면 날짜가 얼마나 바뀌어야 하는지를 계산하여 반환합니다. + * + * @param {number} relativeX - 드래그를 시작한 지점을 기준으로 현재 마우스의 상대적인 x좌표를 의미합니다. + * @param {number} relativeY - 드래그를 시작한 지점을 기준으로 현재 마우스의 상대적인 y좌표를 의미합니다. + * @param {number} calendarWidth - 캘린더 컴포넌트의 가로 길이를 의미합니다. + * @param {number} calendarHeight - 캘린더 컴포넌트의 세로 길이를 의미합니다. + * + * @returns {number} calculatedDifference - 변경되어야 하는 날짜의 일 수를 정수 형태로 변환한 값을 의미합니다. 이 값은 음수일 수 있습니다. + */ +const getCalendarDateDifferenceByMousePoint = ( + relativeX: number, + relativeY: number, + calendarWidth: number, + calendarHeight: number, +) => { + const rowDifference = Math.round( + (relativeY * CALENDAR.ROW_SIZE) / calendarHeight, + ); + const columnDifference = Math.round( + (relativeX * CALENDAR.COLUMN_SIZE) / calendarWidth, + ); + const calculatedDifference = + rowDifference * CALENDAR.COLUMN_SIZE + columnDifference; + + return calculatedDifference; +}; + +/** + * 《changeDateTimeByDays》 + * YYYY-MM-DD 형식의 날짜와 함께 변경되어야 하는 날의 수가 주어지면, 이를 반영하여 똑같이 YYYY-MM-DD 형식으로 변경된 날짜를 반환합니다. + * + * @param {YYYYMMDDHHMM} dateTime - 변경을 진행할 YYYY-MM-DD 형식의 날짜 정보입니다. + * @param {number} days - 입력으로 들어가는 날짜 정보의 날짜를 얼마나 변경할 것인지를 의미합니다. 이 값은 정수여야 합니다. + * + * @returns {YYYYMMDDHHMM} changedDateTime - 변경이 반영된 YYYY-MM-DD 형식의 날짜 정보입니다. + */ +const changeDateTimeByDays = (dateTime: YYYYMMDDHHMM, days: number) => { + const changedDate = new Date(Number(new Date(dateTime)) + ONE_DAY * days); + + const year = String(changedDate.getFullYear()).padStart(4, '0'); + const month = String(changedDate.getMonth() + 1).padStart(2, '0'); + const day = String(changedDate.getDate()).padStart(2, '0'); + const time = dateTime.split(' ')[1]; + const [minute, second] = time.split(':'); + + const changedDateTime: YYYYMMDDHHMM = `${year}-${month}-${day} ${minute}:${second}`; + + return changedDateTime; +}; diff --git a/frontend/src/utils/test/generateScheduleBars.test.ts b/frontend/src/utils/test/generateScheduleBars.test.ts index f34288505..530a27bd1 100644 --- a/frontend/src/utils/test/generateScheduleBars.test.ts +++ b/frontend/src/utils/test/generateScheduleBars.test.ts @@ -1,4 +1,4 @@ -import { generateScheduleBars } from '../generateScheduleBars'; +import { generateScheduleBars } from '~/utils/generateScheduleBars'; import type { GeneratedScheduleBar, Schedule } from '~/types/schedule'; const removeIdFromScheduleBars = (scheduleBars: GeneratedScheduleBar[]) => { diff --git a/frontend/src/utils/test/generateScheduleBarsByMousePoint.test.ts b/frontend/src/utils/test/generateScheduleBarsByMousePoint.test.ts new file mode 100644 index 000000000..49626d509 --- /dev/null +++ b/frontend/src/utils/test/generateScheduleBarsByMousePoint.test.ts @@ -0,0 +1,353 @@ +import { generateScheduleBarsByMousePoint } from '~/utils/generateScheduleBarsByMousePoint'; +import type { + GeneratedScheduleBar, + Schedule, + YYYYMMDDHHMM, +} from '~/types/schedule'; + +const removeIdFromScheduleBars = (scheduleBars: GeneratedScheduleBar[]) => { + /* eslint-disable-next-line */ + const scheduleBarsWithoutId = scheduleBars.map(({ id, ...rest }) => { + return rest; + }); + + return scheduleBarsWithoutId; +}; + +const defaultParams = { + year: 2023, + month: 10, + calendarWidth: 700, + calendarHeight: 600, + level: 0, + calendarSize: 'md' as const, +}; + +type GeneratedScheduleBarWithoutId = Omit; + +interface ResultValue { + startDateTime: YYYYMMDDHHMM; + endDateTime: YYYYMMDDHHMM; + scheduleBars: GeneratedScheduleBarWithoutId[]; +} + +describe('Test #1 - 좌표 대응 테스트', () => { + test('상대 좌표가 우측에 있을 경우, 그에 대응되는 날짜가 이동된 스케줄 바가 반환되어야 한다.', () => { + const schedule: Schedule = { + id: 1, + title: '내 일정', + startDateTime: '2023-11-14 00:00', + endDateTime: '2023-11-16 23:59', + }; + + const params = { + ...defaultParams, + schedule, + relativeX: 155, + relativeY: 0, + }; + + const expectedResult: ResultValue = { + startDateTime: '2023-11-16 00:00', + endDateTime: '2023-11-18 23:59', + scheduleBars: [ + { + scheduleId: 1, + title: '내 일정', + row: 2, + column: 4, + duration: 3, + level: 0, + roundedStart: true, + roundedEnd: true, + schedule: { + id: 1, + title: '내 일정', + startDateTime: '2023-11-16 00:00', + endDateTime: '2023-11-18 23:59', + }, + calendarSize: 'md', + }, + ], + }; + + const { startDateTime, endDateTime, scheduleBars } = + generateScheduleBarsByMousePoint(params); + + expect({ + startDateTime, + endDateTime, + scheduleBars: removeIdFromScheduleBars(scheduleBars), + }).toEqual(expectedResult); + }); + + test('상대 좌표가 좌상단에 있을 경우, 그에 대응되는 날짜가 이동된 스케줄 바가 반환되어야 한다. 또한, 범위 바깥의 스케줄 바는 잘려야 한다.', () => { + const schedule: Schedule = { + id: 1, + title: '내 일정', + startDateTime: '2023-11-14 00:00', + endDateTime: '2023-11-16 23:59', + }; + + const params = { + ...defaultParams, + schedule, + relativeX: -349.9, + relativeY: -150.1, + }; + + const expectedResult: ResultValue = { + startDateTime: '2023-10-28 00:00', + endDateTime: '2023-10-30 23:59', + scheduleBars: [ + { + scheduleId: 1, + title: '내 일정', + row: 0, + column: 0, + duration: 2, + level: 0, + roundedStart: false, + roundedEnd: true, + schedule: { + id: 1, + title: '내 일정', + startDateTime: '2023-10-28 00:00', + endDateTime: '2023-10-30 23:59', + }, + calendarSize: 'md', + }, + ], + }; + + const { startDateTime, endDateTime, scheduleBars } = + generateScheduleBarsByMousePoint(params); + + expect({ + startDateTime, + endDateTime, + scheduleBars: removeIdFromScheduleBars(scheduleBars), + }).toEqual(expectedResult); + }); + + test('상대 좌표가 우하단에 있을 경우, 그에 대응되는 날짜가 이동된 스케줄 바가 반환되어야 한다. 또한, 이동된 일정에 따라 적절하게 스케줄 바의 모양이 바뀌어야 한다.', () => { + const schedule: Schedule = { + id: 1, + title: '빡구현좋아', + startDateTime: '2023-11-14 14:00', + endDateTime: '2023-11-20 16:30', + }; + + const params = { + ...defaultParams, + schedule, + relativeX: 316, + relativeY: 83, + }; + + const expectedResult: ResultValue = { + startDateTime: '2023-11-24 14:00', + endDateTime: '2023-11-30 16:30', + scheduleBars: [ + { + scheduleId: 1, + title: '빡구현좋아', + row: 3, + column: 5, + duration: 2, + level: 0, + roundedStart: true, + roundedEnd: false, + schedule: { + id: 1, + title: '빡구현좋아', + startDateTime: '2023-11-24 14:00', + endDateTime: '2023-11-30 16:30', + }, + calendarSize: 'md', + }, + { + scheduleId: 1, + title: '빡구현좋아', + row: 4, + column: 0, + duration: 5, + level: 0, + roundedStart: false, + roundedEnd: true, + schedule: { + id: 1, + title: '빡구현좋아', + startDateTime: '2023-11-24 14:00', + endDateTime: '2023-11-30 16:30', + }, + calendarSize: 'md', + }, + ], + }; + + const { startDateTime, endDateTime, scheduleBars } = + generateScheduleBarsByMousePoint(params); + + expect({ + startDateTime, + endDateTime, + scheduleBars: removeIdFromScheduleBars(scheduleBars), + }).toEqual(expectedResult); + }); + + test('상대 좌표의 이동거리가 짧아 일정에 변화가 없는 경우, 변화되지 않은 스케줄 바 그대로를 반환해야 한다.', () => { + const schedule: Schedule = { + id: 1, + title: '내 일정', + startDateTime: '2023-11-14 00:00', + endDateTime: '2023-11-16 23:59', + }; + + const params = { + ...defaultParams, + schedule, + relativeX: 0, + relativeY: 49.9999, + }; + + const expectedResult: ResultValue = { + startDateTime: '2023-11-14 00:00', + endDateTime: '2023-11-16 23:59', + scheduleBars: [ + { + scheduleId: 1, + title: '내 일정', + row: 2, + column: 2, + duration: 3, + level: 0, + roundedStart: true, + roundedEnd: true, + schedule: { + id: 1, + title: '내 일정', + startDateTime: '2023-11-14 00:00', + endDateTime: '2023-11-16 23:59', + }, + calendarSize: 'md', + }, + ], + }; + + const { startDateTime, endDateTime, scheduleBars } = + generateScheduleBarsByMousePoint(params); + + expect({ + startDateTime, + endDateTime, + scheduleBars: removeIdFromScheduleBars(scheduleBars), + }).toEqual(expectedResult); + }); +}); + +describe('Test #2 - 캘린더 크기 대응 테스트', () => { + test('캘린더의 크기가 평소와 달라진 경우, 상대 좌표도 다르게 계산하여 반영하여야 한다.', () => { + const schedule: Schedule = { + id: 1, + title: '내 일정', + startDateTime: '2023-11-14 00:00', + endDateTime: '2023-11-16 23:59', + }; + + const params = { + ...defaultParams, + schedule, + calendarWidth: 732, + calendarHeight: 481, + relativeX: 156.8571, + relativeY: -160.3334, + }; + + const expectedResult: ResultValue = { + startDateTime: '2023-11-01 00:00', + endDateTime: '2023-11-03 23:59', + scheduleBars: [ + { + scheduleId: 1, + title: '내 일정', + row: 0, + column: 3, + duration: 3, + level: 0, + roundedStart: true, + roundedEnd: true, + schedule: { + id: 1, + title: '내 일정', + startDateTime: '2023-11-01 00:00', + endDateTime: '2023-11-03 23:59', + }, + calendarSize: 'md', + }, + ], + }; + + const { startDateTime, endDateTime, scheduleBars } = + generateScheduleBarsByMousePoint(params); + + expect({ + startDateTime, + endDateTime, + scheduleBars: removeIdFromScheduleBars(scheduleBars), + }).toEqual(expectedResult); + }); +}); + +describe('Test #3 - 부가 기능 테스트', () => { + test('캘린더 바의 사이즈, 레벨을 별도로 지정한 후 해당 설정으로 반영된 스케줄 바가 반환되어야 한다.', () => { + const schedule: Schedule = { + id: 1, + title: '내 일정', + startDateTime: '2023-11-14 00:00', + endDateTime: '2023-11-16 23:59', + }; + + const params = { + ...defaultParams, + schedule, + relativeX: 23, + relativeY: 81, + calendarSize: 'sm' as const, + level: 2, + }; + + const expectedResult: ResultValue = { + startDateTime: '2023-11-21 00:00', + endDateTime: '2023-11-23 23:59', + scheduleBars: [ + { + scheduleId: 1, + title: '내 일정', + row: 3, + column: 2, + duration: 3, + level: 2, + roundedStart: true, + roundedEnd: true, + schedule: { + id: 1, + title: '내 일정', + startDateTime: '2023-11-21 00:00', + endDateTime: '2023-11-23 23:59', + }, + calendarSize: 'sm', + }, + ], + }; + + const { startDateTime, endDateTime, scheduleBars } = + generateScheduleBarsByMousePoint(params); + + expect({ + startDateTime, + endDateTime, + scheduleBars: removeIdFromScheduleBars(scheduleBars), + }).toEqual(expectedResult); + }); +}); From ad357c9bee9766adc114759b3e91eb5683f61a0d Mon Sep 17 00:00:00 2001 From: Rulu <79538610+hafnium1923@users.noreply.github.com> Date: Tue, 12 Dec 2023 15:45:42 +0900 Subject: [PATCH 04/20] =?UTF-8?q?[FE]=20=EC=B1=84=ED=8C=85=20setQueryData?= =?UTF-8?q?=20=ED=9B=84=20=EC=BD=98=EC=86=94=20=EC=B6=94=EA=B0=80=20(#885)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 채팅 setQueryData 후 콘솔 추가 * refactor: 팀 채팅 query시간 변경 --- frontend/src/constants/query.ts | 2 +- frontend/src/hooks/queries/useSSE.ts | 19 ++++++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/frontend/src/constants/query.ts b/frontend/src/constants/query.ts index 2640a963a..846aa05fb 100644 --- a/frontend/src/constants/query.ts +++ b/frontend/src/constants/query.ts @@ -11,7 +11,7 @@ export const STALE_TIME = { TEAM_LINKS: 1000 * 60, - TEAM_FEED: 1000 * 30, + TEAM_FEED: 1000 * 60 * 5, ICALENDAR_URL: Infinity, }; diff --git a/frontend/src/hooks/queries/useSSE.ts b/frontend/src/hooks/queries/useSSE.ts index 903c2530a..f4837f271 100644 --- a/frontend/src/hooks/queries/useSSE.ts +++ b/frontend/src/hooks/queries/useSSE.ts @@ -29,11 +29,20 @@ export const useSSE = () => { eventSource.addEventListener('new_thread', (e: MessageEvent) => { const newThread = e.data; - queryClient.setQueryData(['threadData'], (old) => { - if (old) { - return { threads: [...old.threads, newThread] }; - } - }); + queryClient.setQueryData( + ['threadData', teamPlaceId], + (old) => { + if (old) { + return { threads: [...old.threads, newThread] }; + } + }, + ); + + const newList = queryClient.getQueryData([ + 'threadData', + teamPlaceId, + ]); + console.log(newList); }); return () => { From 2d73184f1ba0e81bb266f6d0227c85001dca188f Mon Sep 17 00:00:00 2001 From: Rulu <79538610+hafnium1923@users.noreply.github.com> Date: Tue, 12 Dec 2023 16:02:12 +0900 Subject: [PATCH 05/20] =?UTF-8?q?[FE]=20setQuery=EC=9D=98=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=ED=83=80=EC=9E=85=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?(#886)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/hooks/queries/useSSE.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/src/hooks/queries/useSSE.ts b/frontend/src/hooks/queries/useSSE.ts index f4837f271..9b959c0bd 100644 --- a/frontend/src/hooks/queries/useSSE.ts +++ b/frontend/src/hooks/queries/useSSE.ts @@ -1,5 +1,5 @@ import { useCallback, useEffect } from 'react'; -import { useQueryClient } from '@tanstack/react-query'; +import { type InfiniteData, useQueryClient } from '@tanstack/react-query'; import { baseUrl } from '~/apis/http'; import { EventSourcePolyfill } from 'event-source-polyfill'; import { useToken } from '~/hooks/useToken'; @@ -29,11 +29,13 @@ export const useSSE = () => { eventSource.addEventListener('new_thread', (e: MessageEvent) => { const newThread = e.data; - queryClient.setQueryData( + queryClient.setQueryData>( ['threadData', teamPlaceId], (old) => { if (old) { - return { threads: [...old.threads, newThread] }; + old.pages[0].threads = [newThread, ...old.pages[0].threads]; + + return old; } }, ); From 8b4ad665ccd2f3dc1fbfd7c1ac586cbf63cd3e21 Mon Sep 17 00:00:00 2001 From: Rulu <79538610+hafnium1923@users.noreply.github.com> Date: Tue, 12 Dec 2023 16:10:55 +0900 Subject: [PATCH 06/20] =?UTF-8?q?[FE]=20sse=EB=A1=9C=20=EB=B0=9B=EC=9D=80?= =?UTF-8?q?=20event=20data=EB=A5=BC=20=EA=B0=9D=EC=B2=B4=EB=A1=9C=20parse?= =?UTF-8?q?=20(#887)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/hooks/queries/useSSE.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/hooks/queries/useSSE.ts b/frontend/src/hooks/queries/useSSE.ts index 9b959c0bd..38525d4a8 100644 --- a/frontend/src/hooks/queries/useSSE.ts +++ b/frontend/src/hooks/queries/useSSE.ts @@ -26,8 +26,8 @@ export const useSSE = () => { }, ); - eventSource.addEventListener('new_thread', (e: MessageEvent) => { - const newThread = e.data; + eventSource.addEventListener('new_thread', (e) => { + const newThread = JSON.parse(e.data); queryClient.setQueryData>( ['threadData', teamPlaceId], From 3caf782156fd9670e53934d5318e2e1a5b462b6a Mon Sep 17 00:00:00 2001 From: Jae_Philip_Yang Date: Thu, 14 Dec 2023 15:08:35 +0900 Subject: [PATCH 07/20] =?UTF-8?q?[BE]=20=ED=8A=B9=EC=A0=95=20=EA=B8=B0?= =?UTF-8?q?=EA=B0=84=EC=9D=84=20=EC=A7=80=EC=A0=95=ED=95=B4=EC=84=9C=20?= =?UTF-8?q?=EC=9D=BC=EC=A0=95=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#889)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 일정조회 메서드 명 변경 - 컨트롤러 메서드 명 변경 - 서비스 메서드 명 변경 - 특정 기간 조회 기능 추가로 인하여 메서드명 가독성을 위한 변경 * feat: 특정 기간으로 팀일정 조회 서비스 구현 - CalendarPeriod 정적 팩터리 메서드 추가 - 시작과 종료 LocalDate를 인자로 받음 - 입력 형식 오류 예외 추가 (ScheduleException.dateFormatException) - 팀일정 조회 서비스 메서드 생성 * feat: 기간지정 팀 일정 조회 api구현 * feat: 특정 기간 내 내 일정 조회 서비스 구현 - 서비스 기능 구현 - LocalDate 파싱용 클래스 LocalDateParser 구현 * refactor: 분리한 LocalDateParser 사용 * feat: 특정 기간으로 일정 조회 기능 구현 * refactor: Parser 스프링빈으로 생성, 패키지 변경 --- .../presentation/GlobalExceptionHandler.java | 1 + .../MyCalendarScheduleService.java | 28 +++++ .../TeamCalendarScheduleService.java | 25 ++++- .../application/parser/LocalDateParser.java | 22 ++++ .../schedule/domain/CalendarPeriod.java | 26 +++++ .../schedule/exception/ScheduleException.java | 11 +- .../MyCalendarScheduleController.java | 11 ++ .../TeamCalendarScheduleController.java | 17 ++- .../MyCalendarScheduleAcceptanceFixtures.java | 15 +++ ...eamCalendarScheduleAcceptanceFixtures.java | 12 ++ .../MyCalendarScheduleAcceptanceTest.java | 103 ++++++++++++++++++ .../TeamCalendarScheduleAcceptanceTest.java | 92 +++++++++++++++- .../application/LocalDateParserTest.java | 46 ++++++++ .../MyCalendarScheduleServiceTest.java | 40 +++++++ .../TeamCalendarScheduleServiceTest.java | 64 +++++++++-- .../docs/TeamCalendarScheduleApiDocsTest.java | 8 +- .../schedule/domain/CalendarPeriodTest.java | 46 ++++++++ 17 files changed, 542 insertions(+), 25 deletions(-) create mode 100644 backend/src/main/java/team/teamby/teambyteam/schedule/application/parser/LocalDateParser.java create mode 100644 backend/src/test/java/team/teamby/teambyteam/schedule/application/LocalDateParserTest.java create mode 100644 backend/src/test/java/team/teamby/teambyteam/schedule/domain/CalendarPeriodTest.java diff --git a/backend/src/main/java/team/teamby/teambyteam/global/presentation/GlobalExceptionHandler.java b/backend/src/main/java/team/teamby/teambyteam/global/presentation/GlobalExceptionHandler.java index 6f3dbf76d..4e75bc844 100644 --- a/backend/src/main/java/team/teamby/teambyteam/global/presentation/GlobalExceptionHandler.java +++ b/backend/src/main/java/team/teamby/teambyteam/global/presentation/GlobalExceptionHandler.java @@ -123,6 +123,7 @@ public ResponseEntity handleCustomForbiddenException(final Runtim @ExceptionHandler(value = { ScheduleException.SpanWrongOrderException.class, + ScheduleException.dateFormatException.class, TeamPlaceInviteCodeException.LengthException.class, TeamPlaceException.NameLengthException.class, TeamPlaceException.NameBlankException.class, diff --git a/backend/src/main/java/team/teamby/teambyteam/schedule/application/MyCalendarScheduleService.java b/backend/src/main/java/team/teamby/teambyteam/schedule/application/MyCalendarScheduleService.java index 6ec6c8253..f3e36b309 100644 --- a/backend/src/main/java/team/teamby/teambyteam/schedule/application/MyCalendarScheduleService.java +++ b/backend/src/main/java/team/teamby/teambyteam/schedule/application/MyCalendarScheduleService.java @@ -9,11 +9,13 @@ import team.teamby.teambyteam.member.domain.vo.Email; import team.teamby.teambyteam.member.exception.MemberException; import team.teamby.teambyteam.schedule.application.dto.SchedulesWithTeamPlaceIdResponse; +import team.teamby.teambyteam.schedule.application.parser.LocalDateParser; import team.teamby.teambyteam.schedule.domain.CalendarPeriod; import team.teamby.teambyteam.schedule.domain.Schedule; import team.teamby.teambyteam.schedule.domain.ScheduleRepository; import team.teamby.teambyteam.teamplace.domain.TeamPlace; +import java.time.LocalDate; import java.util.List; @Service @@ -23,6 +25,7 @@ public class MyCalendarScheduleService { private final MemberRepository memberRepository; private final ScheduleRepository scheduleRepository; + private final LocalDateParser localDateParser; @Transactional(readOnly = true) public SchedulesWithTeamPlaceIdResponse findScheduleInPeriod( @@ -66,4 +69,29 @@ public SchedulesWithTeamPlaceIdResponse findScheduleInPeriod( return SchedulesWithTeamPlaceIdResponse.of(dailySchedules); } + + @Transactional(readOnly = true) + public SchedulesWithTeamPlaceIdResponse findScheduleInPeriod( + final MemberEmailDto memberEmailDto, + final String startDateString, + final String endDateString + ) { + final Member member = memberRepository.findByEmail(new Email(memberEmailDto.email())) + .orElseThrow(() -> new MemberException.MemberNotFoundException(memberEmailDto.email())); + + final List participatedTeamPlaceIds = member.getTeamPlaces() + .stream() + .map(TeamPlace::getId) + .toList(); + + final LocalDate startDate = localDateParser.parse(startDateString); + final LocalDate endDate = localDateParser.parse(endDateString); + + final CalendarPeriod period = CalendarPeriod.of(startDate, endDate); + + final List dailySchedules = scheduleRepository.findAllByTeamPlaceIdAndPeriod( + participatedTeamPlaceIds, period.startDateTime(), period.endDatetime()); + + return SchedulesWithTeamPlaceIdResponse.of(dailySchedules); + } } diff --git a/backend/src/main/java/team/teamby/teambyteam/schedule/application/TeamCalendarScheduleService.java b/backend/src/main/java/team/teamby/teambyteam/schedule/application/TeamCalendarScheduleService.java index 83fd90d06..2bb969263 100644 --- a/backend/src/main/java/team/teamby/teambyteam/schedule/application/TeamCalendarScheduleService.java +++ b/backend/src/main/java/team/teamby/teambyteam/schedule/application/TeamCalendarScheduleService.java @@ -13,6 +13,7 @@ import team.teamby.teambyteam.schedule.application.event.ScheduleDeleteEvent; import team.teamby.teambyteam.schedule.application.event.ScheduleUpdateEvent; import team.teamby.teambyteam.schedule.application.event.ScheduleUpdateEventDto; +import team.teamby.teambyteam.schedule.application.parser.LocalDateParser; import team.teamby.teambyteam.schedule.domain.CalendarPeriod; import team.teamby.teambyteam.schedule.domain.Schedule; import team.teamby.teambyteam.schedule.domain.ScheduleRepository; @@ -22,6 +23,7 @@ import team.teamby.teambyteam.teamplace.domain.TeamPlaceRepository; import team.teamby.teambyteam.teamplace.exception.TeamPlaceException; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; @@ -34,6 +36,7 @@ public class TeamCalendarScheduleService { private final ScheduleRepository scheduleRepository; private final TeamPlaceRepository teamPlaceRepository; private final ApplicationEventPublisher applicationEventPublisher; + private final LocalDateParser localDateParser; public Long register(final ScheduleRegisterRequest scheduleRegisterRequest, final Long teamPlaceId) { checkTeamPlaceExist(teamPlaceId); @@ -87,7 +90,7 @@ private boolean isNotScheduleOfTeam(final Long teamPlaceId, final Schedule sched } @Transactional(readOnly = true) - public SchedulesResponse findScheduleInPeriod(final Long teamPlaceId, final int targetYear, final int targetMonth) { + public SchedulesResponse findScheduleInMonth(final Long teamPlaceId, final int targetYear, final int targetMonth) { checkTeamPlaceExist(teamPlaceId); final CalendarPeriod period = CalendarPeriod.of(targetYear, targetMonth); @@ -98,7 +101,7 @@ public SchedulesResponse findScheduleInPeriod(final Long teamPlaceId, final int } @Transactional(readOnly = true) - public SchedulesResponse findScheduleInPeriod( + public SchedulesResponse findScheduleInDay( final Long teamPlaceId, final int targetYear, final int targetMonth, @@ -113,6 +116,24 @@ public SchedulesResponse findScheduleInPeriod( return SchedulesResponse.of(dailySchedules); } + @Transactional(readOnly = true) + public SchedulesResponse findScheduleInPeriod( + final Long teaPlaceId, + final String startDateString, + final String endDateString + ) { + checkTeamPlaceExist(teaPlaceId); + + final LocalDate startDate = localDateParser.parse(startDateString); + final LocalDate endDate = localDateParser.parse(endDateString); + final CalendarPeriod period = CalendarPeriod.of(startDate, endDate); + + final List schedules = scheduleRepository. + findAllByTeamPlaceIdAndPeriod(teaPlaceId, period.startDateTime(), period.endDatetime()); + + return SchedulesResponse.of(schedules); + } + public void update(final ScheduleUpdateRequest scheduleUpdateRequest, final Long teamPlaceId, final Long scheduleId) { checkTeamPlaceExist(teamPlaceId); diff --git a/backend/src/main/java/team/teamby/teambyteam/schedule/application/parser/LocalDateParser.java b/backend/src/main/java/team/teamby/teambyteam/schedule/application/parser/LocalDateParser.java new file mode 100644 index 000000000..ab470bb02 --- /dev/null +++ b/backend/src/main/java/team/teamby/teambyteam/schedule/application/parser/LocalDateParser.java @@ -0,0 +1,22 @@ +package team.teamby.teambyteam.schedule.application.parser; + +import org.springframework.stereotype.Component; +import team.teamby.teambyteam.schedule.exception.ScheduleException; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +@Component +public class LocalDateParser { + + private static final DateTimeFormatter DATE_PARAM_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd"); + + public LocalDate parse(final String yearMonthDay) { + try { + return LocalDate.parse(yearMonthDay, DATE_PARAM_FORMAT); + } catch (final DateTimeParseException e) { + throw new ScheduleException.dateFormatException(e); + } + } +} diff --git a/backend/src/main/java/team/teamby/teambyteam/schedule/domain/CalendarPeriod.java b/backend/src/main/java/team/teamby/teambyteam/schedule/domain/CalendarPeriod.java index 5af8c105e..a268cd957 100644 --- a/backend/src/main/java/team/teamby/teambyteam/schedule/domain/CalendarPeriod.java +++ b/backend/src/main/java/team/teamby/teambyteam/schedule/domain/CalendarPeriod.java @@ -1,9 +1,18 @@ package team.teamby.teambyteam.schedule.domain; +import team.teamby.teambyteam.schedule.exception.ScheduleException; + import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +/** + * 캘린더 일정 + * 일정에 해당하려면 startDateTime <= PERIOD < endDateTime + * + * @param startDateTime inclusive DateTime + * @param endDatetime exclusive DateTime + */ public record CalendarPeriod( LocalDateTime startDateTime, LocalDateTime endDatetime @@ -26,4 +35,21 @@ public static CalendarPeriod of(final int year, final int month, final int day) return new CalendarPeriod(LocalDateTime.of(dailyDate, START_TIME_OF_DAY), LocalDateTime.of(nextDay, START_TIME_OF_DAY)); } + + public static CalendarPeriod of(final LocalDate startDate, final LocalDate endDate) { + validateOrder(startDate, endDate); + return new CalendarPeriod( + LocalDateTime.of(startDate, START_TIME_OF_DAY), + LocalDateTime.of(endDate.plusDays(NEXT_DAY_OFFSET), START_TIME_OF_DAY) + ); + } + + private static void validateOrder(final LocalDate startDate, final LocalDate endDate) { + if (endDate.isBefore(startDate)) { + throw new ScheduleException.SpanWrongOrderException( + LocalDateTime.of(startDate, START_TIME_OF_DAY), + LocalDateTime.of(endDate, START_TIME_OF_DAY) + ); + } + } } diff --git a/backend/src/main/java/team/teamby/teambyteam/schedule/exception/ScheduleException.java b/backend/src/main/java/team/teamby/teambyteam/schedule/exception/ScheduleException.java index e6082c286..11d5af703 100644 --- a/backend/src/main/java/team/teamby/teambyteam/schedule/exception/ScheduleException.java +++ b/backend/src/main/java/team/teamby/teambyteam/schedule/exception/ScheduleException.java @@ -8,6 +8,10 @@ public ScheduleException(final String message) { super(message); } + public ScheduleException(String message, Throwable cause) { + super(message, cause); + } + public static class ScheduleNotFoundException extends ScheduleException { public ScheduleNotFoundException(final Long scheduleId) { super(String.format("조회한 일정이 존재하지 않습니다. - request info { schedule_id : %d }", scheduleId)); @@ -31,9 +35,14 @@ public SpanWrongOrderException(final LocalDateTime startDateTime, final LocalDat } public static class TitleBlankException extends ScheduleException { - public TitleBlankException() { super("일정의 제목은 빈 칸일 수 없습니다."); } } + + public static class dateFormatException extends ScheduleException { + public dateFormatException(final Exception e) { + super("잘못된 날짜 입력 형식입니다.", e); + } + } } diff --git a/backend/src/main/java/team/teamby/teambyteam/schedule/presentation/MyCalendarScheduleController.java b/backend/src/main/java/team/teamby/teambyteam/schedule/presentation/MyCalendarScheduleController.java index 16b7f9705..ee81515d9 100644 --- a/backend/src/main/java/team/teamby/teambyteam/schedule/presentation/MyCalendarScheduleController.java +++ b/backend/src/main/java/team/teamby/teambyteam/schedule/presentation/MyCalendarScheduleController.java @@ -40,4 +40,15 @@ public ResponseEntity findDailySchedule( return ResponseEntity.ok(responseBody); } + + @GetMapping(value = "/schedules", params = {"startdate", "enddate"}) + public ResponseEntity findDailySchedule( + @AuthPrincipal final MemberEmailDto memberEmailDto, + @RequestParam(value = "startdate") final String startDate, + @RequestParam(value = "enddate") final String endDate + ) { + final SchedulesWithTeamPlaceIdResponse responseBody = myCalendarScheduleService.findScheduleInPeriod(memberEmailDto, startDate, endDate); + + return ResponseEntity.ok(responseBody); + } } diff --git a/backend/src/main/java/team/teamby/teambyteam/schedule/presentation/TeamCalendarScheduleController.java b/backend/src/main/java/team/teamby/teambyteam/schedule/presentation/TeamCalendarScheduleController.java index fa758f81b..08083c2bb 100644 --- a/backend/src/main/java/team/teamby/teambyteam/schedule/presentation/TeamCalendarScheduleController.java +++ b/backend/src/main/java/team/teamby/teambyteam/schedule/presentation/TeamCalendarScheduleController.java @@ -38,12 +38,12 @@ public ResponseEntity findSpecificSchedule( } @GetMapping(value = "/{teamPlaceId}/calendar/schedules", params = {"year", "month"}) - public ResponseEntity findSchedulesInPeriod( + public ResponseEntity findScheduleInMonth( @PathVariable final Long teamPlaceId, @RequestParam final Integer year, @RequestParam final Integer month ) { - final SchedulesResponse responseBody = teamCalendarScheduleService.findScheduleInPeriod(teamPlaceId, year, month); + final SchedulesResponse responseBody = teamCalendarScheduleService.findScheduleInMonth(teamPlaceId, year, month); return ResponseEntity.ok(responseBody); } @@ -55,7 +55,18 @@ public ResponseEntity findDailySchedule( @RequestParam final Integer month, @RequestParam final Integer day ) { - final SchedulesResponse response = teamCalendarScheduleService.findScheduleInPeriod(teamPlaceId, year, month, day); + final SchedulesResponse response = teamCalendarScheduleService.findScheduleInDay(teamPlaceId, year, month, day); + + return ResponseEntity.ok(response); + } + + @GetMapping(value = "/{teamPlaceId}/calendar/schedules", params = {"startdate", "enddate"}) + public ResponseEntity findDailySchedule( + @PathVariable final Long teamPlaceId, + @RequestParam(value = "startdate") final String startDate, + @RequestParam(value = "enddate") final String endDate + ) { + final SchedulesResponse response = teamCalendarScheduleService.findScheduleInPeriod(teamPlaceId, startDate, endDate); return ResponseEntity.ok(response); } diff --git a/backend/src/test/java/team/teamby/teambyteam/common/fixtures/acceptance/MyCalendarScheduleAcceptanceFixtures.java b/backend/src/test/java/team/teamby/teambyteam/common/fixtures/acceptance/MyCalendarScheduleAcceptanceFixtures.java index e489570d0..3cff4287e 100644 --- a/backend/src/test/java/team/teamby/teambyteam/common/fixtures/acceptance/MyCalendarScheduleAcceptanceFixtures.java +++ b/backend/src/test/java/team/teamby/teambyteam/common/fixtures/acceptance/MyCalendarScheduleAcceptanceFixtures.java @@ -21,6 +21,21 @@ public static ExtractableResponse FIND_PERIOD_SCHEDULE_REQUEST(final S .extract(); } + public static ExtractableResponse FIND_PERIOD_SCHEDULE_REQUEST( + final String token, + final String startDate, + final String endDate + ) { + return RestAssured.given().log().all() + .header(new Header(HttpHeaders.AUTHORIZATION, JWT_PREFIX + token)) + .queryParam("startdate", startDate) + .queryParam("enddate", endDate) + .when().log().all() + .get("/api/my-calendar/schedules") + .then().log().all() + .extract(); + } + public static ExtractableResponse FIND_DAILY_SCHEDULE_REQUEST(final String token, final Integer year, final Integer month, final Integer day) { return RestAssured.given().log().all() .header(new Header(HttpHeaders.AUTHORIZATION, JWT_PREFIX + token)) diff --git a/backend/src/test/java/team/teamby/teambyteam/common/fixtures/acceptance/TeamCalendarScheduleAcceptanceFixtures.java b/backend/src/test/java/team/teamby/teambyteam/common/fixtures/acceptance/TeamCalendarScheduleAcceptanceFixtures.java index 3c6b9f615..8ff889a86 100644 --- a/backend/src/test/java/team/teamby/teambyteam/common/fixtures/acceptance/TeamCalendarScheduleAcceptanceFixtures.java +++ b/backend/src/test/java/team/teamby/teambyteam/common/fixtures/acceptance/TeamCalendarScheduleAcceptanceFixtures.java @@ -44,6 +44,18 @@ public static ExtractableResponse FIND_PERIOD_SCHEDULE_REQUEST(final S .extract(); } + public static ExtractableResponse FIND_PERIOD_SCHEDULE_REQUEST(final String token, final Long teamPlaceId, final String startDate, final String endDate) { + return RestAssured.given().log().all() + .header(new Header(HttpHeaders.AUTHORIZATION, JWT_PREFIX + token)) + .pathParam("teamPlaceId", teamPlaceId) + .queryParam("startdate", startDate) + .queryParam("enddate", endDate) + .when().log().all() + .get("/api/team-place/{teamPlaceId}/calendar/schedules") + .then().log().all() + .extract(); + } + public static ExtractableResponse FIND_DAILY_SCHEDULE_REQUEST(final String token, final Long teamPlaceId, final int year, final int month, final int day) { return RestAssured.given().log().all() .header(new Header(HttpHeaders.AUTHORIZATION, JWT_PREFIX + token)) diff --git a/backend/src/test/java/team/teamby/teambyteam/schedule/acceptance/MyCalendarScheduleAcceptanceTest.java b/backend/src/test/java/team/teamby/teambyteam/schedule/acceptance/MyCalendarScheduleAcceptanceTest.java index 8e9c16600..331426037 100644 --- a/backend/src/test/java/team/teamby/teambyteam/schedule/acceptance/MyCalendarScheduleAcceptanceTest.java +++ b/backend/src/test/java/team/teamby/teambyteam/schedule/acceptance/MyCalendarScheduleAcceptanceTest.java @@ -100,4 +100,107 @@ void success() { }); } } + + + + @Nested + @DisplayName("내 캘린더 일정을 특정 기간 사이에서 조회를 한다") + class MyCalendarFindScheduleInSpecificPeriod { + + @Test + @DisplayName("기간으로 조회 성공한다.") + void success() { + // given + final Member PHILIP = testFixtureBuilder.buildMember(PHILIP()); + + final TeamPlace ENGLISH_TEAM_PLACE = testFixtureBuilder.buildTeamPlace(ENGLISH_TEAM_PLACE()); + final TeamPlace JAPANESE_TEAM_PLACE = testFixtureBuilder.buildTeamPlace(JAPANESE_TEAM_PLACE()); + + testFixtureBuilder.buildMemberTeamPlace(PHILIP, ENGLISH_TEAM_PLACE); + testFixtureBuilder.buildMemberTeamPlace(PHILIP, JAPANESE_TEAM_PLACE); + + final Schedule MONTH_6_AND_MONTH_7_DAY_12_ENGLISH_SCHEDULE = testFixtureBuilder.buildSchedule(MONTH_6_AND_MONTH_7_DAY_12_SCHEDULE(ENGLISH_TEAM_PLACE.getId())); + final Schedule MONTH_7_AND_DAY_12_ALL_DAY_ENGLISH_SCHEDULE = testFixtureBuilder.buildSchedule(MONTH_7_AND_DAY_12_ALL_DAY_SCHEDULE(ENGLISH_TEAM_PLACE.getId())); + final Schedule MONTH_7_AND_DAY_12_N_HOUR_JAPANESE_SCHEDULE = testFixtureBuilder.buildSchedule(MONTH_7_AND_DAY_12_N_HOUR_SCHEDULE(JAPANESE_TEAM_PLACE.getId())); + final Schedule MONTH_7_DAY_28_AND_MONTH_8_SCHEDULE_JAPANESE_SCHEDULE = testFixtureBuilder.buildSchedule(MONTH_7_DAY_28_AND_MONTH_8_SCHEDULE(JAPANESE_TEAM_PLACE.getId())); + + final String startDate = "20230711"; + final String endDate = "20230712"; + + // when + final ExtractableResponse response = FIND_PERIOD_SCHEDULE_REQUEST(jwtTokenProvider.generateAccessToken(PHILIP.getEmail().getValue()), startDate, endDate); + final List schedules = response.jsonPath().getList("schedules", ScheduleWithTeamPlaceIdResponse.class); + + //then + assertSoftly(softly -> { + softly.assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + softly.assertThat(schedules).hasSize(3); + softly.assertThat(schedules.get(0).title()).isEqualTo(MONTH_6_AND_MONTH_7_DAY_12_ENGLISH_SCHEDULE.getTitle().getValue()); + softly.assertThat(schedules.get(1).title()).isEqualTo(MONTH_7_AND_DAY_12_ALL_DAY_ENGLISH_SCHEDULE.getTitle().getValue()); + softly.assertThat(schedules.get(2).title()).isEqualTo(MONTH_7_AND_DAY_12_N_HOUR_JAPANESE_SCHEDULE.getTitle().getValue()); + }); + } + + @Test + @DisplayName("날짜의 순서가 잘못되면 실패한다.") + void failWithWrongPeriodDateOrder() { + // given + final Member PHILIP = testFixtureBuilder.buildMember(PHILIP()); + + final TeamPlace ENGLISH_TEAM_PLACE = testFixtureBuilder.buildTeamPlace(ENGLISH_TEAM_PLACE()); + final TeamPlace JAPANESE_TEAM_PLACE = testFixtureBuilder.buildTeamPlace(JAPANESE_TEAM_PLACE()); + + testFixtureBuilder.buildMemberTeamPlace(PHILIP, ENGLISH_TEAM_PLACE); + testFixtureBuilder.buildMemberTeamPlace(PHILIP, JAPANESE_TEAM_PLACE); + + testFixtureBuilder.buildSchedule(MONTH_6_AND_MONTH_7_DAY_12_SCHEDULE(ENGLISH_TEAM_PLACE.getId())); + testFixtureBuilder.buildSchedule(MONTH_7_AND_DAY_12_ALL_DAY_SCHEDULE(ENGLISH_TEAM_PLACE.getId())); + testFixtureBuilder.buildSchedule(MONTH_7_AND_DAY_12_N_HOUR_SCHEDULE(JAPANESE_TEAM_PLACE.getId())); + testFixtureBuilder.buildSchedule(MONTH_7_DAY_28_AND_MONTH_8_SCHEDULE(JAPANESE_TEAM_PLACE.getId())); + + final String startDate = "20230711"; + final String endDate = "20230710"; + + // when + final ExtractableResponse response = FIND_PERIOD_SCHEDULE_REQUEST(jwtTokenProvider.generateAccessToken(PHILIP.getEmail().getValue()), startDate, endDate); + final String errorMessage = response.jsonPath().get("error"); + + //then + assertSoftly(softly -> { + softly.assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); + softly.assertThat(errorMessage).contains("시작 일자가 종료 일자보다 이후일 수 없습니다."); + }); + } + + @Test + @DisplayName("잘못된 형식으로 조회요청시 실패한다") + void failWithWrongDateFormant() { + // given + final Member PHILIP = testFixtureBuilder.buildMember(PHILIP()); + + final TeamPlace ENGLISH_TEAM_PLACE = testFixtureBuilder.buildTeamPlace(ENGLISH_TEAM_PLACE()); + final TeamPlace JAPANESE_TEAM_PLACE = testFixtureBuilder.buildTeamPlace(JAPANESE_TEAM_PLACE()); + + testFixtureBuilder.buildMemberTeamPlace(PHILIP, ENGLISH_TEAM_PLACE); + testFixtureBuilder.buildMemberTeamPlace(PHILIP, JAPANESE_TEAM_PLACE); + + testFixtureBuilder.buildSchedule(MONTH_6_AND_MONTH_7_DAY_12_SCHEDULE(ENGLISH_TEAM_PLACE.getId())); + testFixtureBuilder.buildSchedule(MONTH_7_AND_DAY_12_ALL_DAY_SCHEDULE(ENGLISH_TEAM_PLACE.getId())); + testFixtureBuilder.buildSchedule(MONTH_7_AND_DAY_12_N_HOUR_SCHEDULE(JAPANESE_TEAM_PLACE.getId())); + testFixtureBuilder.buildSchedule(MONTH_7_DAY_28_AND_MONTH_8_SCHEDULE(JAPANESE_TEAM_PLACE.getId())); + + final String startDate = "2023-07-11"; + final String endDate = "20230712"; + + // when + final ExtractableResponse response = FIND_PERIOD_SCHEDULE_REQUEST(jwtTokenProvider.generateAccessToken(PHILIP.getEmail().getValue()), startDate, endDate); + final String errorMessage = response.jsonPath().get("error"); + + //then + assertSoftly(softly -> { + softly.assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); + softly.assertThat(errorMessage).isEqualTo("잘못된 날짜 입력 형식입니다."); + }); + } + } } diff --git a/backend/src/test/java/team/teamby/teambyteam/schedule/acceptance/TeamCalendarScheduleAcceptanceTest.java b/backend/src/test/java/team/teamby/teambyteam/schedule/acceptance/TeamCalendarScheduleAcceptanceTest.java index 2db8d5719..23a65d5fe 100644 --- a/backend/src/test/java/team/teamby/teambyteam/schedule/acceptance/TeamCalendarScheduleAcceptanceTest.java +++ b/backend/src/test/java/team/teamby/teambyteam/schedule/acceptance/TeamCalendarScheduleAcceptanceTest.java @@ -34,10 +34,22 @@ import static org.assertj.core.api.SoftAssertions.assertSoftly; import static team.teamby.teambyteam.common.fixtures.MemberFixtures.PHILIP; import static team.teamby.teambyteam.common.fixtures.MemberTeamPlaceFixtures.PHILIP_ENGLISH_TEAM_PLACE; -import static team.teamby.teambyteam.common.fixtures.ScheduleFixtures.*; +import static team.teamby.teambyteam.common.fixtures.ScheduleFixtures.MONTH_6_AND_MONTH_7_DAY_12_SCHEDULE; +import static team.teamby.teambyteam.common.fixtures.ScheduleFixtures.MONTH_7_AND_DAY_12_ALL_DAY_SCHEDULE; +import static team.teamby.teambyteam.common.fixtures.ScheduleFixtures.MONTH_7_AND_DAY_12_N_HOUR_SCHEDULE; +import static team.teamby.teambyteam.common.fixtures.ScheduleFixtures.MONTH_7_AND_DAY_12_N_HOUR_SCHEDULE_REGISTER_REQUEST; +import static team.teamby.teambyteam.common.fixtures.ScheduleFixtures.MONTH_7_AND_DAY_12_N_HOUR_SCHEDULE_TITLE; +import static team.teamby.teambyteam.common.fixtures.ScheduleFixtures.MONTH_7_AND_DAY_12_N_HOUR_SCHEDULE_UPDATE_REQUEST; +import static team.teamby.teambyteam.common.fixtures.ScheduleFixtures.MONTH_7_DAY_28_AND_MONTH_8_SCHEDULE; +import static team.teamby.teambyteam.common.fixtures.ScheduleFixtures.MONTH_7_DAY_29_AND_MONTH_8_SCHEDULE; import static team.teamby.teambyteam.common.fixtures.TeamPlaceFixtures.ENGLISH_TEAM_PLACE; import static team.teamby.teambyteam.common.fixtures.TeamPlaceFixtures.JAPANESE_TEAM_PLACE; -import static team.teamby.teambyteam.common.fixtures.acceptance.TeamCalendarScheduleAcceptanceFixtures.*; +import static team.teamby.teambyteam.common.fixtures.acceptance.TeamCalendarScheduleAcceptanceFixtures.DELETE_SCHEDULE_REQUEST; +import static team.teamby.teambyteam.common.fixtures.acceptance.TeamCalendarScheduleAcceptanceFixtures.FIND_DAILY_SCHEDULE_REQUEST; +import static team.teamby.teambyteam.common.fixtures.acceptance.TeamCalendarScheduleAcceptanceFixtures.FIND_PERIOD_SCHEDULE_REQUEST; +import static team.teamby.teambyteam.common.fixtures.acceptance.TeamCalendarScheduleAcceptanceFixtures.FIND_SPECIFIC_SCHEDULE_REQUEST; +import static team.teamby.teambyteam.common.fixtures.acceptance.TeamCalendarScheduleAcceptanceFixtures.REGISTER_SCHEDULE_REQUEST; +import static team.teamby.teambyteam.common.fixtures.acceptance.TeamCalendarScheduleAcceptanceFixtures.UPDATE_SCHEDULE_REQUEST; public class TeamCalendarScheduleAcceptanceTest extends AcceptanceTest { @@ -129,7 +141,7 @@ void failMemberHasNotTeamPlaceSchedule() { @Nested @DisplayName("팀 캘린더 내 기간 일정 조회 시") - class FindTeamCalendarScheduleInPeriod { + class FindTeamCalendarScheduleInMonthlyPeriod { @Test @DisplayName("기간으로 조회 성공한다.") @@ -211,6 +223,79 @@ void failWithWrongQuery() { } } + @Nested + @DisplayName("팀 캘린더 기간 지정 일정 조회 시") + class FindTeamCalendarScheduleInSpecificPeriod { + + @Test + @DisplayName("기간으로 조회 성공한다.") + void success() { + // given + final Member PHILIP = testFixtureBuilder.buildMember(PHILIP()); + final TeamPlace ENGLISH_TEAM_PLACE = testFixtureBuilder.buildTeamPlace(ENGLISH_TEAM_PLACE()); + final MemberTeamPlace PHILIP_ENGLISH_TEAM_PLACE = PHILIP_ENGLISH_TEAM_PLACE(); + PHILIP_ENGLISH_TEAM_PLACE.setMemberAndTeamPlace(PHILIP, ENGLISH_TEAM_PLACE); + testFixtureBuilder.buildMemberTeamPlace(PHILIP_ENGLISH_TEAM_PLACE); + + final List schedulesToSave = List.of( + MONTH_6_AND_MONTH_7_DAY_12_SCHEDULE(ENGLISH_TEAM_PLACE.getId()), + MONTH_7_AND_DAY_12_ALL_DAY_SCHEDULE(ENGLISH_TEAM_PLACE.getId()), + MONTH_7_AND_DAY_12_N_HOUR_SCHEDULE(ENGLISH_TEAM_PLACE.getId()), + MONTH_7_DAY_28_AND_MONTH_8_SCHEDULE(ENGLISH_TEAM_PLACE.getId()), + MONTH_7_DAY_29_AND_MONTH_8_SCHEDULE(ENGLISH_TEAM_PLACE.getId()) + ); + final List expectedSchedules = testFixtureBuilder.buildSchedules(schedulesToSave); + final String startDate = "20230712"; + final String endDate = "20230728"; + + // when + final ExtractableResponse response = FIND_PERIOD_SCHEDULE_REQUEST(jwtTokenProvider.generateAccessToken(PHILIP.getEmail().getValue()), ENGLISH_TEAM_PLACE.getId(), startDate, endDate); + final List actualSchedules = response.jsonPath().getList("schedules", ScheduleResponse.class); + + //then + assertSoftly(softly -> { + softly.assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + softly.assertThat(actualSchedules.size()).isEqualTo(4); + softly.assertThat(actualSchedules.get(0).title()).isEqualTo(expectedSchedules.get(0).getTitle().getValue()); + softly.assertThat(actualSchedules.get(1).title()).isEqualTo(expectedSchedules.get(1).getTitle().getValue()); + softly.assertThat(actualSchedules.get(2).title()).isEqualTo(expectedSchedules.get(2).getTitle().getValue()); + softly.assertThat(actualSchedules.get(3).title()).isEqualTo(expectedSchedules.get(3).getTitle().getValue()); + }); + } + + @Test + @DisplayName("잘못된 날짜 형식으로 요청시 실패한다.") + void failWithWrongFormat() { + // given + final Member PHILIP = testFixtureBuilder.buildMember(PHILIP()); + final TeamPlace ENGLISH_TEAM_PLACE = testFixtureBuilder.buildTeamPlace(ENGLISH_TEAM_PLACE()); + final MemberTeamPlace PHILIP_ENGLISH_TEAM_PLACE = PHILIP_ENGLISH_TEAM_PLACE(); + PHILIP_ENGLISH_TEAM_PLACE.setMemberAndTeamPlace(PHILIP, ENGLISH_TEAM_PLACE); + testFixtureBuilder.buildMemberTeamPlace(PHILIP_ENGLISH_TEAM_PLACE); + + final List schedulesToSave = List.of( + MONTH_6_AND_MONTH_7_DAY_12_SCHEDULE(ENGLISH_TEAM_PLACE.getId()), + MONTH_7_AND_DAY_12_ALL_DAY_SCHEDULE(ENGLISH_TEAM_PLACE.getId()), + MONTH_7_AND_DAY_12_N_HOUR_SCHEDULE(ENGLISH_TEAM_PLACE.getId()), + MONTH_7_DAY_28_AND_MONTH_8_SCHEDULE(ENGLISH_TEAM_PLACE.getId()), + MONTH_7_DAY_29_AND_MONTH_8_SCHEDULE(ENGLISH_TEAM_PLACE.getId()) + ); + testFixtureBuilder.buildSchedules(schedulesToSave); + final String startDate = "2023-07-12"; + final String endDate = "2023-07-28"; + + // when + final ExtractableResponse response = FIND_PERIOD_SCHEDULE_REQUEST(jwtTokenProvider.generateAccessToken(PHILIP.getEmail().getValue()), ENGLISH_TEAM_PLACE.getId(), startDate, endDate); + final String errorMessage = response.jsonPath().get("error"); + + //then + assertSoftly(softly -> { + softly.assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); + softly.assertThat(errorMessage).isEqualTo("잘못된 날짜 입력 형식입니다."); + }); + } + } + @Nested @DisplayName("팀 캘린더 하루 일정 조회 시") class FindTeamCalendarDailySchedule { @@ -499,7 +584,6 @@ private ExtractableResponse wrongDateTimeTypeRegisterScheduleRequest(f } } - @Nested @DisplayName("일정 수정 시") class UpdateSchedule { diff --git a/backend/src/test/java/team/teamby/teambyteam/schedule/application/LocalDateParserTest.java b/backend/src/test/java/team/teamby/teambyteam/schedule/application/LocalDateParserTest.java new file mode 100644 index 000000000..47b9bcdd4 --- /dev/null +++ b/backend/src/test/java/team/teamby/teambyteam/schedule/application/LocalDateParserTest.java @@ -0,0 +1,46 @@ +package team.teamby.teambyteam.schedule.application; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import team.teamby.teambyteam.schedule.application.parser.LocalDateParser; +import team.teamby.teambyteam.schedule.exception.ScheduleException; + +import java.time.LocalDate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class LocalDateParserTest { + + private final LocalDateParser localDateParser = new LocalDateParser(); + + @Test + @DisplayName("LocalDate 파싱을 성공한다") + void success() { + // given + final String input = "20230102"; + + // when + final LocalDate actual = localDateParser.parse(input); + + // then + assertThat(actual).isEqualTo(LocalDate.of(2023, 1, 2)); + } + + @ParameterizedTest + @ValueSource(strings = {"2023-01-01", "2023721", "20230132"}) + @DisplayName("yyyyMMdd형식이 아닌 경우 예외가 발생한다.") + void failWithWrongFormat(final String input) { + // given + + // when + // then + assertThatThrownBy(() -> localDateParser.parse(input)) + .isInstanceOf(ScheduleException.dateFormatException.class) + .hasMessage("잘못된 날짜 입력 형식입니다."); + + } + +} diff --git a/backend/src/test/java/team/teamby/teambyteam/schedule/application/MyCalendarScheduleServiceTest.java b/backend/src/test/java/team/teamby/teambyteam/schedule/application/MyCalendarScheduleServiceTest.java index db0723229..5d80ec35a 100644 --- a/backend/src/test/java/team/teamby/teambyteam/schedule/application/MyCalendarScheduleServiceTest.java +++ b/backend/src/test/java/team/teamby/teambyteam/schedule/application/MyCalendarScheduleServiceTest.java @@ -17,6 +17,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.SoftAssertions.assertSoftly; import static team.teamby.teambyteam.common.fixtures.MemberFixtures.PHILIP; +import static team.teamby.teambyteam.common.fixtures.ScheduleFixtures.MONTH_5_LAST_DAY_SCHEDULE; import static team.teamby.teambyteam.common.fixtures.ScheduleFixtures.MONTH_6_AND_MONTH_7_DAY_12_SCHEDULE; import static team.teamby.teambyteam.common.fixtures.ScheduleFixtures.MONTH_6_AND_MONTH_7_SCHEDULE; import static team.teamby.teambyteam.common.fixtures.ScheduleFixtures.MONTH_7_AND_DAY_12_ALL_DAY_SCHEDULE; @@ -128,4 +129,43 @@ void successEmptyList() { assertThat(dailyScheduleResponse.schedules()).hasSize(0); } } + + @Nested + @DisplayName("특정 기간 안에서 내 캘린더 정보 조회 시") + class FindScheduleInMyCalendarInSpecificPeriod { + + @Test + @DisplayName("내 캘린더 정보 조회를 성공한다.") + void success() { + // given + final Member PHILIP = testFixtureBuilder.buildMember(PHILIP()); + final TeamPlace ENGLISH_TEAM_PLACE = testFixtureBuilder.buildTeamPlace(ENGLISH_TEAM_PLACE()); + final TeamPlace JAPANESE_TEAM_PLACE = testFixtureBuilder.buildTeamPlace(JAPANESE_TEAM_PLACE()); + + testFixtureBuilder.buildMemberTeamPlace(PHILIP, ENGLISH_TEAM_PLACE); + testFixtureBuilder.buildMemberTeamPlace(PHILIP, JAPANESE_TEAM_PLACE); + + final List expectedSchedules = List.of( + MONTH_5_LAST_DAY_SCHEDULE(ENGLISH_TEAM_PLACE.getId()), + MONTH_7_AND_DAY_12_ALL_DAY_SCHEDULE(ENGLISH_TEAM_PLACE.getId()), + MONTH_7_AND_DAY_12_N_HOUR_SCHEDULE(JAPANESE_TEAM_PLACE.getId()) + ); + testFixtureBuilder.buildSchedules(expectedSchedules); + + final MemberEmailDto memberEmailDto = new MemberEmailDto(PHILIP.getEmail().getValue()); + final String startDate = "20230601"; + final String endDate = "20230712"; + + // when + final SchedulesWithTeamPlaceIdResponse scheduleInPeriod = myCalendarScheduleService.findScheduleInPeriod(memberEmailDto, startDate, endDate); + final List scheduleResponses = scheduleInPeriod.schedules(); + + //then + assertSoftly(softly -> { + softly.assertThat(scheduleResponses).hasSize(2); + softly.assertThat(scheduleResponses.get(0).title()).isEqualTo(expectedSchedules.get(1).getTitle().getValue()); + softly.assertThat(scheduleResponses.get(1).title()).isEqualTo(expectedSchedules.get(2).getTitle().getValue()); + }); + } + } } diff --git a/backend/src/test/java/team/teamby/teambyteam/schedule/application/TeamCalendarScheduleServiceTest.java b/backend/src/test/java/team/teamby/teambyteam/schedule/application/TeamCalendarScheduleServiceTest.java index 7ec53b1ed..8f81f0833 100644 --- a/backend/src/test/java/team/teamby/teambyteam/schedule/application/TeamCalendarScheduleServiceTest.java +++ b/backend/src/test/java/team/teamby/teambyteam/schedule/application/TeamCalendarScheduleServiceTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; import team.teamby.teambyteam.common.ServiceTest; @@ -108,8 +109,8 @@ void failFindOtherTeamPlaceSchedule() { } @Test - @DisplayName("팀 캘린더에서 특정 기간 내 일정들을 조회한다.") - void findAllInPeriod() { + @DisplayName("팀 캘린더에서 1달 내 일정들을 조회한다.") + void findAllInMonthlyPeriod() { // given final TeamPlace ENGLISH_TEAM_PLACE = testFixtureBuilder.buildTeamPlace(ENGLISH_TEAM_PLACE()); final Schedule MONTH_6_AND_MONTH_7_SCHEDULE = testFixtureBuilder.buildSchedule(MONTH_6_AND_MONTH_7_DAY_12_SCHEDULE(ENGLISH_TEAM_PLACE.getId())); @@ -121,7 +122,7 @@ void findAllInPeriod() { final int month = 7; // when - final SchedulesResponse schedulesResponse = teamCalendarScheduleService.findScheduleInPeriod(ENGLISH_TEAM_PLACE.getId(), year, month); + final SchedulesResponse schedulesResponse = teamCalendarScheduleService.findScheduleInMonth(ENGLISH_TEAM_PLACE.getId(), year, month); final List scheduleResponses = schedulesResponse.schedules(); //then @@ -134,6 +135,47 @@ void findAllInPeriod() { }); } + @Test + @DisplayName("팀 캘린더에서 입력된 기간내 일정들을 조회한다.") + void findAllInSpecificPeriod() { + // given + final TeamPlace ENGLISH_TEAM_PLACE = testFixtureBuilder.buildTeamPlace(ENGLISH_TEAM_PLACE()); + final Schedule MONTH_6_AND_MONTH_7_SCHEDULE = testFixtureBuilder.buildSchedule(MONTH_6_AND_MONTH_7_DAY_12_SCHEDULE(ENGLISH_TEAM_PLACE.getId())); + final Schedule MONTH_7_AND_DAY_12_N_HOUR_SCHEDULE = testFixtureBuilder.buildSchedule(MONTH_7_AND_DAY_12_N_HOUR_SCHEDULE(ENGLISH_TEAM_PLACE.getId())); + final Schedule MONTH_7_DAY_28_AND_MONTH_8_SCHEDULE = testFixtureBuilder.buildSchedule(MONTH_7_DAY_28_AND_MONTH_8_SCHEDULE(ENGLISH_TEAM_PLACE.getId())); + final Schedule MONTH_7_DAY_29_AND_MONTH_8_SCHEDULE = testFixtureBuilder.buildSchedule(MONTH_7_DAY_29_AND_MONTH_8_SCHEDULE(ENGLISH_TEAM_PLACE.getId())); + + final String startDate = "20230712"; + final String endDate = "20230728"; + + // when + final SchedulesResponse schedulesResponse = teamCalendarScheduleService.findScheduleInPeriod(ENGLISH_TEAM_PLACE.getId(), startDate, endDate); + final List scheduleResponses = schedulesResponse.schedules(); + + //then + assertSoftly(softly -> { + softly.assertThat(scheduleResponses).hasSize(3); + softly.assertThat(scheduleResponses.get(0).title()).isEqualTo(MONTH_6_AND_MONTH_7_SCHEDULE.getTitle().getValue()); + softly.assertThat(scheduleResponses.get(1).title()).isEqualTo(MONTH_7_AND_DAY_12_N_HOUR_SCHEDULE.getTitle().getValue()); + softly.assertThat(scheduleResponses.get(2).title()).isEqualTo(MONTH_7_DAY_28_AND_MONTH_8_SCHEDULE.getTitle().getValue()); + }); + } + + @ParameterizedTest + @CsvSource(value = {"2023712,20230728", "20230712,0728"}, delimiter = ',') + @DisplayName("특정기간 조회시 일정 포멧이 yyyyMMdd와 다르면 예외를 발생시킨다.") + void failWithWrongDateFormat(final String startDate, final String endDate) { + // given + final TeamPlace ENGLISH_TEAM_PLACE = testFixtureBuilder.buildTeamPlace(ENGLISH_TEAM_PLACE()); + + // when + // then + assertThatThrownBy(() -> teamCalendarScheduleService.findScheduleInPeriod(ENGLISH_TEAM_PLACE.getId(), startDate, endDate)) + .isInstanceOf(ScheduleException.dateFormatException.class) + .hasMessage("잘못된 날짜 입력 형식입니다."); + + } + @Test @DisplayName("팀 캘린더에서 일정이 없는 기간 내 일정들을 조회한다.") void findAllInPeriodWith0Schedule() { @@ -143,7 +185,7 @@ void findAllInPeriodWith0Schedule() { final int month = 7; // when - final SchedulesResponse schedulesResponse = teamCalendarScheduleService.findScheduleInPeriod(ENGLISH_TEAM_PLACE.getId(), notExistYear, month); + final SchedulesResponse schedulesResponse = teamCalendarScheduleService.findScheduleInMonth(ENGLISH_TEAM_PLACE.getId(), notExistYear, month); final List scheduleResponses = schedulesResponse.schedules(); //then @@ -162,7 +204,7 @@ void firstAndLastDateScheduleFind() { final int month = MONTH_5_FIRST_DAY_SCHEDULE.getSpan().getStartDateTime().getMonthValue(); // when - final SchedulesResponse schedulesResponse = teamCalendarScheduleService.findScheduleInPeriod(ENGLISH_TEAM_PLACE.getId(), year, month); + final SchedulesResponse schedulesResponse = teamCalendarScheduleService.findScheduleInMonth(ENGLISH_TEAM_PLACE.getId(), year, month); final List scheduleResponses = schedulesResponse.schedules(); //then @@ -194,7 +236,7 @@ void success() { // when final SchedulesResponse dailySchedulesResponse = - teamCalendarScheduleService.findScheduleInPeriod(ENGLISH_TEAM_PLACE_ID, year, month, day); + teamCalendarScheduleService.findScheduleInDay(ENGLISH_TEAM_PLACE_ID, year, month, day); final List dailyTeamCalendarSchedulesResponses = dailySchedulesResponse.schedules(); // then @@ -226,7 +268,7 @@ void wrongYear() { final int day = MONTH_7_AND_DAY_12_N_HOUR_SCHEDULE.getSpan().getStartDateTime().getDayOfMonth(); // when & then - assertThatThrownBy(() -> teamCalendarScheduleService.findScheduleInPeriod(ENGLISH_TEAM_PLACE_ID, wrongYear, month, day)) + assertThatThrownBy(() -> teamCalendarScheduleService.findScheduleInDay(ENGLISH_TEAM_PLACE_ID, wrongYear, month, day)) .isInstanceOf(DateTimeException.class) .hasMessageContaining("Invalid value for Year (valid values -999999999 - 999999999)"); } @@ -243,7 +285,7 @@ void wrongMonth() { final int day = MONTH_7_AND_DAY_12_N_HOUR_SCHEDULE.getSpan().getStartDateTime().getDayOfMonth(); // when & then - assertThatThrownBy(() -> teamCalendarScheduleService.findScheduleInPeriod(ENGLISH_TEAM_PLACE_ID, year, wrongMonth, day)) + assertThatThrownBy(() -> teamCalendarScheduleService.findScheduleInDay(ENGLISH_TEAM_PLACE_ID, year, wrongMonth, day)) .isInstanceOf(DateTimeException.class) .hasMessageContaining("Invalid value for MonthOfYear (valid values 1 - 12)"); } @@ -260,7 +302,7 @@ void wrongDay() { final int wrongDay = -1; // when & then - assertThatThrownBy(() -> teamCalendarScheduleService.findScheduleInPeriod(ENGLISH_TEAM_PLACE_ID, year, month, wrongDay)) + assertThatThrownBy(() -> teamCalendarScheduleService.findScheduleInDay(ENGLISH_TEAM_PLACE_ID, year, month, wrongDay)) .isInstanceOf(DateTimeException.class) .hasMessageContaining("Invalid value for DayOfMonth (valid values 1 - 28/31)"); } @@ -276,7 +318,7 @@ void successNotExistSchedule() { final int day = 1; // when - SchedulesResponse schedules = teamCalendarScheduleService.findScheduleInPeriod(ENGLISH_TEAM_PLACE.getId(), year, month, day); + SchedulesResponse schedules = teamCalendarScheduleService.findScheduleInDay(ENGLISH_TEAM_PLACE.getId(), year, month, day); // then assertThat(schedules.schedules()).hasSize(0); @@ -292,7 +334,7 @@ void failTeamPlaceNotExist() { final int day = 1; // when & then - assertThatThrownBy(() -> teamCalendarScheduleService.findScheduleInPeriod(notExistTeamPlaceId, year, month, day)) + assertThatThrownBy(() -> teamCalendarScheduleService.findScheduleInDay(notExistTeamPlaceId, year, month, day)) .isInstanceOf(TeamPlaceException.NotFoundException.class) .hasMessageContaining("조회한 팀 플레이스가 존재하지 않습니다."); } diff --git a/backend/src/test/java/team/teamby/teambyteam/schedule/docs/TeamCalendarScheduleApiDocsTest.java b/backend/src/test/java/team/teamby/teambyteam/schedule/docs/TeamCalendarScheduleApiDocsTest.java index 01ccf2cfd..0c630f5ec 100644 --- a/backend/src/test/java/team/teamby/teambyteam/schedule/docs/TeamCalendarScheduleApiDocsTest.java +++ b/backend/src/test/java/team/teamby/teambyteam/schedule/docs/TeamCalendarScheduleApiDocsTest.java @@ -426,7 +426,7 @@ void success() throws Exception { List schedules = List.of(schedule1, schedule2); SchedulesResponse response = SchedulesResponse.of(schedules); - given(teamCalendarScheduleService.findScheduleInPeriod(teamPlaceId, 2023, 7)) + given(teamCalendarScheduleService.findScheduleInMonth(teamPlaceId, 2023, 7)) .willReturn(response); // when & then @@ -469,7 +469,7 @@ void failNotExistTeamplaceId() throws Exception { willThrow(new TeamPlaceException.NotFoundException(teamPlaceId)) .given(teamCalendarScheduleService) - .findScheduleInPeriod(teamPlaceId, 2023, 7); + .findScheduleInMonth(teamPlaceId, 2023, 7); // when & then mockMvc.perform(get("/api/team-place/{teamPlaceId}/calendar/schedules", teamPlaceId) @@ -504,7 +504,7 @@ void success() throws Exception { List schedules = List.of(schedule1, schedule2); SchedulesResponse response = SchedulesResponse.of(schedules); - given(teamCalendarScheduleService.findScheduleInPeriod(teamPlaceId, 2023, 7, 12)) + given(teamCalendarScheduleService.findScheduleInDay(teamPlaceId, 2023, 7, 12)) .willReturn(response); // when & then @@ -549,7 +549,7 @@ void failNotExistTeamplaceId() throws Exception { willThrow(new TeamPlaceException.NotFoundException(teamPlaceId)) .given(teamCalendarScheduleService) - .findScheduleInPeriod(teamPlaceId, 2023, 7, 12); + .findScheduleInDay(teamPlaceId, 2023, 7, 12); // when & then mockMvc.perform(get("/api/team-place/{teamPlaceId}/calendar/schedules", teamPlaceId) diff --git a/backend/src/test/java/team/teamby/teambyteam/schedule/domain/CalendarPeriodTest.java b/backend/src/test/java/team/teamby/teambyteam/schedule/domain/CalendarPeriodTest.java new file mode 100644 index 000000000..5407915ab --- /dev/null +++ b/backend/src/test/java/team/teamby/teambyteam/schedule/domain/CalendarPeriodTest.java @@ -0,0 +1,46 @@ +package team.teamby.teambyteam.schedule.domain; + +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import team.teamby.teambyteam.schedule.exception.ScheduleException; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static team.teamby.teambyteam.schedule.domain.CalendarPeriod.of; + +class CalendarPeriodTest { + + @Test + @DisplayName("LocalDate로 생성 테스트") + void createWithLocalDate() { + // given + final LocalDate startDate = LocalDate.of(2023, 1, 1); + final LocalDate endDate = LocalDate.of(2023, 1, 1); + + // when + final CalendarPeriod calendarPeriod = of(startDate, endDate); + + // then + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(calendarPeriod.startDateTime()).isEqualTo(LocalDateTime.of(2023, 1, 1, 0, 0, 0)); + softly.assertThat(calendarPeriod.endDatetime()).isEqualTo(LocalDateTime.of(2023, 1, 2, 0, 0, 0)); + }); + } + + @Test + @DisplayName("시작일보다 이른 종료일로 생성시 예외 발생") + void exceptionWithWrongPeriodOrder() { + // given + final LocalDate startDate = LocalDate.of(2023, 1, 2); + final LocalDate endDate = LocalDate.of(2023, 1, 1); + + // when + // then + assertThatThrownBy(() -> of(startDate, endDate)) + .isInstanceOf(ScheduleException.SpanWrongOrderException.class) + .hasMessageContaining("시작 일자가 종료 일자보다 이후일 수 없습니다."); + } +} From 4e713a5c0a3849e6bb1e792510ecccdbf0022a8e Mon Sep 17 00:00:00 2001 From: Rulu <79538610+hafnium1923@users.noreply.github.com> Date: Sun, 17 Dec 2023 21:45:32 +0900 Subject: [PATCH 08/20] =?UTF-8?q?[FE]=20=EC=B1=84=ED=8C=85=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EB=90=98=EC=97=88=EC=9D=84=20=EC=8B=9C=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A1=A4=20=ED=95=98=EB=8B=A8=20=EB=B3=BC=20=EC=88=98?= =?UTF-8?q?=20=EC=9E=88=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20(#888)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refacor: 채팅 이벤트 관찰을 위한 콘솔 삭제 * refactor: 채팅 첫 페이지의 길이가 달라지면 하단 뷰로 * feat: 팀 전환 시 채팅 스크롤 초기화 * feat: sse 연결시 채팅 쿼리 무효화 * refactor: 사용하지않는 변수 삭제 * refactor: sse 연결 이벤트 이름 변경 --- .../src/components/feed/ThreadList/ThreadList.tsx | 12 ++++++++++-- frontend/src/hooks/queries/useSSE.ts | 13 +++++-------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/feed/ThreadList/ThreadList.tsx b/frontend/src/components/feed/ThreadList/ThreadList.tsx index 7b33be1d6..46c3f3af6 100644 --- a/frontend/src/components/feed/ThreadList/ThreadList.tsx +++ b/frontend/src/components/feed/ThreadList/ThreadList.tsx @@ -56,8 +56,16 @@ const ThreadList = (props: ThreadListProps) => { }, [threadPages?.pages.length]); useEffect(() => { - if (threadPages?.pages.length !== threadPagesRef.current) { - threadPagesRef.current = threadPages?.pages.length ?? 0; + if (!threadEndRef.current) { + return; + } + + threadEndRef.current.scrollIntoView(); + }, [teamPlaceId]); + + useEffect(() => { + if (threadPages?.pages[0].threads.length !== threadPagesRef.current) { + threadPagesRef.current = threadPages?.pages[0].threads.length ?? 0; } else { if (!threadEndRef.current) { return; diff --git a/frontend/src/hooks/queries/useSSE.ts b/frontend/src/hooks/queries/useSSE.ts index 38525d4a8..e321be869 100644 --- a/frontend/src/hooks/queries/useSSE.ts +++ b/frontend/src/hooks/queries/useSSE.ts @@ -4,8 +4,7 @@ import { baseUrl } from '~/apis/http'; import { EventSourcePolyfill } from 'event-source-polyfill'; import { useToken } from '~/hooks/useToken'; import { useTeamPlace } from '~/hooks/useTeamPlace'; -import type { ThreadsResponse } from '~/apis/feed'; -import type { Thread } from '~/types/feed'; +import { type ThreadsResponse } from '~/apis/feed'; export const useSSE = () => { const queryClient = useQueryClient(); @@ -26,6 +25,10 @@ export const useSSE = () => { }, ); + eventSource.addEventListener('connect', () => { + queryClient.invalidateQueries([['threadData', teamPlaceId]]); + }); + eventSource.addEventListener('new_thread', (e) => { const newThread = JSON.parse(e.data); @@ -39,12 +42,6 @@ export const useSSE = () => { } }, ); - - const newList = queryClient.getQueryData([ - 'threadData', - teamPlaceId, - ]); - console.log(newList); }); return () => { From 939d3232fa6ded245480a57093fe4db8b3867992 Mon Sep 17 00:00:00 2001 From: Rulu <79538610+hafnium1923@users.noreply.github.com> Date: Mon, 18 Dec 2023 18:07:17 +0900 Subject: [PATCH 09/20] =?UTF-8?q?[ALL]=20README=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20gif=20=EB=B3=80=EA=B2=BD=20(#893)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3078e34ce..039c12f92 100644 --- a/README.md +++ b/README.md @@ -120,16 +120,16 @@ - + -| 팀 캘린더 | 팀 피드 | +| 팀 캘린더 | 팀 채팅 | | :---------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------: | -| | | +| | | | 팀 링크 | 팀 생성 및 팀 참가 | -| | | +| | |

팀바팀을 더 자세히 알고 싶다면, 여기로! From 15316ec37cb458ae245e2cd596db5e4340bcee74 Mon Sep 17 00:00:00 2001 From: Jae_Philip_Yang Date: Tue, 19 Dec 2023 14:44:27 +0900 Subject: [PATCH 10/20] =?UTF-8?q?[BE]=20=EC=BF=BC=EB=A6=AC=ED=8C=8C?= =?UTF-8?q?=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EC=B9=B4=EB=A9=9C=EC=BC=80?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20(#898)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../schedule/presentation/MyCalendarScheduleController.java | 6 +++--- .../presentation/TeamCalendarScheduleController.java | 6 +++--- .../acceptance/MyCalendarScheduleAcceptanceFixtures.java | 4 ++-- .../acceptance/TeamCalendarScheduleAcceptanceFixtures.java | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/backend/src/main/java/team/teamby/teambyteam/schedule/presentation/MyCalendarScheduleController.java b/backend/src/main/java/team/teamby/teambyteam/schedule/presentation/MyCalendarScheduleController.java index ee81515d9..63edcbc6d 100644 --- a/backend/src/main/java/team/teamby/teambyteam/schedule/presentation/MyCalendarScheduleController.java +++ b/backend/src/main/java/team/teamby/teambyteam/schedule/presentation/MyCalendarScheduleController.java @@ -41,11 +41,11 @@ public ResponseEntity findDailySchedule( return ResponseEntity.ok(responseBody); } - @GetMapping(value = "/schedules", params = {"startdate", "enddate"}) + @GetMapping(value = "/schedules", params = {"startDate", "endDate"}) public ResponseEntity findDailySchedule( @AuthPrincipal final MemberEmailDto memberEmailDto, - @RequestParam(value = "startdate") final String startDate, - @RequestParam(value = "enddate") final String endDate + @RequestParam(value = "startDate") final String startDate, + @RequestParam(value = "endDate") final String endDate ) { final SchedulesWithTeamPlaceIdResponse responseBody = myCalendarScheduleService.findScheduleInPeriod(memberEmailDto, startDate, endDate); diff --git a/backend/src/main/java/team/teamby/teambyteam/schedule/presentation/TeamCalendarScheduleController.java b/backend/src/main/java/team/teamby/teambyteam/schedule/presentation/TeamCalendarScheduleController.java index 08083c2bb..f5f5694c7 100644 --- a/backend/src/main/java/team/teamby/teambyteam/schedule/presentation/TeamCalendarScheduleController.java +++ b/backend/src/main/java/team/teamby/teambyteam/schedule/presentation/TeamCalendarScheduleController.java @@ -60,11 +60,11 @@ public ResponseEntity findDailySchedule( return ResponseEntity.ok(response); } - @GetMapping(value = "/{teamPlaceId}/calendar/schedules", params = {"startdate", "enddate"}) + @GetMapping(value = "/{teamPlaceId}/calendar/schedules", params = {"startDate", "endDate"}) public ResponseEntity findDailySchedule( @PathVariable final Long teamPlaceId, - @RequestParam(value = "startdate") final String startDate, - @RequestParam(value = "enddate") final String endDate + @RequestParam(value = "startDate") final String startDate, + @RequestParam(value = "endDate") final String endDate ) { final SchedulesResponse response = teamCalendarScheduleService.findScheduleInPeriod(teamPlaceId, startDate, endDate); diff --git a/backend/src/test/java/team/teamby/teambyteam/common/fixtures/acceptance/MyCalendarScheduleAcceptanceFixtures.java b/backend/src/test/java/team/teamby/teambyteam/common/fixtures/acceptance/MyCalendarScheduleAcceptanceFixtures.java index 3cff4287e..1bedc1a59 100644 --- a/backend/src/test/java/team/teamby/teambyteam/common/fixtures/acceptance/MyCalendarScheduleAcceptanceFixtures.java +++ b/backend/src/test/java/team/teamby/teambyteam/common/fixtures/acceptance/MyCalendarScheduleAcceptanceFixtures.java @@ -28,8 +28,8 @@ public static ExtractableResponse FIND_PERIOD_SCHEDULE_REQUEST( ) { return RestAssured.given().log().all() .header(new Header(HttpHeaders.AUTHORIZATION, JWT_PREFIX + token)) - .queryParam("startdate", startDate) - .queryParam("enddate", endDate) + .queryParam("startDate", startDate) + .queryParam("endDate", endDate) .when().log().all() .get("/api/my-calendar/schedules") .then().log().all() diff --git a/backend/src/test/java/team/teamby/teambyteam/common/fixtures/acceptance/TeamCalendarScheduleAcceptanceFixtures.java b/backend/src/test/java/team/teamby/teambyteam/common/fixtures/acceptance/TeamCalendarScheduleAcceptanceFixtures.java index 8ff889a86..4c868b1ff 100644 --- a/backend/src/test/java/team/teamby/teambyteam/common/fixtures/acceptance/TeamCalendarScheduleAcceptanceFixtures.java +++ b/backend/src/test/java/team/teamby/teambyteam/common/fixtures/acceptance/TeamCalendarScheduleAcceptanceFixtures.java @@ -48,8 +48,8 @@ public static ExtractableResponse FIND_PERIOD_SCHEDULE_REQUEST(final S return RestAssured.given().log().all() .header(new Header(HttpHeaders.AUTHORIZATION, JWT_PREFIX + token)) .pathParam("teamPlaceId", teamPlaceId) - .queryParam("startdate", startDate) - .queryParam("enddate", endDate) + .queryParam("startDate", startDate) + .queryParam("endDate", endDate) .when().log().all() .get("/api/team-place/{teamPlaceId}/calendar/schedules") .then().log().all() From 5b620481fd76d918934155ab4e381b56138a7e3d Mon Sep 17 00:00:00 2001 From: Rulu <79538610+hafnium1923@users.noreply.github.com> Date: Fri, 22 Dec 2023 00:07:15 +0900 Subject: [PATCH 11/20] =?UTF-8?q?[FE]=20=ED=8C=80=20=EB=A7=81=ED=81=AC=20?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=EB=B2=84=EA=B7=B8=20?= =?UTF-8?q?=ED=94=BD=EC=8A=A4=20(#894)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 팀링크 없는 팀플레이스 * refactor: 팀 링크 테이블 구조 변경 * refactor: 팀 링크 없어도 테이블 제목은 보이게 변경 * refactor: 첫번째 항목빼고 가운데 정렬 --- .../link/LinkTable/LinkTable.styled.ts | 88 +++++++++---------- .../components/link/LinkTable/LinkTable.tsx | 29 +++--- frontend/src/mocks/handlers/link.ts | 2 + 3 files changed, 59 insertions(+), 60 deletions(-) diff --git a/frontend/src/components/link/LinkTable/LinkTable.styled.ts b/frontend/src/components/link/LinkTable/LinkTable.styled.ts index acc5daac1..dbedcff0a 100644 --- a/frontend/src/components/link/LinkTable/LinkTable.styled.ts +++ b/frontend/src/components/link/LinkTable/LinkTable.styled.ts @@ -38,8 +38,22 @@ export const TableContainer = styled.div<{ box-shadow: 0 10px 20px ${({ theme }) => theme.color.GRAY300}; `; -export const tableProperties = css` - & > th, +export const TableWrapper = styled.div` + overflow-y: auto; + + width: 100%; + height: 100%; + + scrollbar-gutter: stable both-edges; +`; + +export const Table = styled.table` + width: 100%; + + font-size: 18px; + + table-layout: fixed; + & td { display: table-cell; overflow: hidden; @@ -51,63 +65,29 @@ export const tableProperties = css` padding: 8px; } - & > tr { - border-bottom: 2px solid ${({ theme }) => theme.color.GRAY200}; - } - - & > th:first-child(), - & td:first-child() { + & td:first-child(), + thead > tr > th:first-child() { width: 40%; } - & > th:nth-child(2), - & td:nth-child(2) { + & td:nth-child(2), + thead > tr > th:nth-child(2) { width: 20%; } - & > th:nth-child(3), - & td:nth-child(3) { + + & td:nth-child(3), + thead > tr > th:nth-child(3) { width: 30%; } - & > th:nth-child(4), - & td:nth-child(4) { + & td:nth-child(4), + thead > tr > th:nth-child(4) { width: 10%; } - & > tr :not(:first-child), - & th { - text-align: center; - } - - font-size: 18px; - - table-layout: fixed; -`; - -export const TableHeader = styled.table` - width: calc(100% - 32px); - height: 60px; - - ${tableProperties} - - & > th { - font-weight: 600; + tbody > tr { + border-bottom: 2px solid ${({ theme }) => theme.color.GRAY200}; } -`; - -export const TableBody = styled.div` - overflow-y: auto; - - width: 100%; - height: 100%; - - scrollbar-gutter: stable both-edges; -`; - -export const Table = styled.table` - width: 100%; - - ${tableProperties} & td > a { font-weight: 700; @@ -118,6 +98,20 @@ export const Table = styled.table` width: 32px; height: 32px; } + + & td:not(:first-child) { + text-align: center; + } +`; + +export const TableHeader = styled.thead` + width: calc(100% - 32px); + height: 48px; + + tr > th { + vertical-align: middle; + font-weight: 600; + } `; export const linkTableTitle = (linkSize: LinkSize) => css` diff --git a/frontend/src/components/link/LinkTable/LinkTable.tsx b/frontend/src/components/link/LinkTable/LinkTable.tsx index bc6722478..7e9d91b47 100644 --- a/frontend/src/components/link/LinkTable/LinkTable.tsx +++ b/frontend/src/components/link/LinkTable/LinkTable.tsx @@ -64,14 +64,16 @@ const LinkTable = (props: LinkTableProps) => { - - {linkTableHeaderValues.map((value) => ( - {value} - ))} - - {teamLinks.length > 0 ? ( - - + + + + + {linkTableHeaderValues.map((value) => ( + {value} + ))} + + + {teamLinks.map(({ id, title, url, memberName, updatedAt }) => ( @@ -100,11 +102,12 @@ const LinkTable = (props: LinkTableProps) => { ))} - - - ) : ( - - )} + + + {teamLinks.length === 0 && ( + + )} + {isModalOpen && } diff --git a/frontend/src/mocks/handlers/link.ts b/frontend/src/mocks/handlers/link.ts index f1d0b6eac..32b1cf778 100644 --- a/frontend/src/mocks/handlers/link.ts +++ b/frontend/src/mocks/handlers/link.ts @@ -36,6 +36,8 @@ export const LinkHandlers = [ if (index === -1) return res(ctx.status(403)); + if (teamPlaceId === 2) + return res(ctx.status(200), ctx.json({ teamLinks: [] })); return res(ctx.status(200), ctx.json({ teamLinks })); }), From 91700d9702591348fd2e1134ecd091c807d479b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9A=94=EC=88=A0=ED=86=A0=EB=81=BC?= Date: Wed, 27 Dec 2023 14:32:58 +0900 Subject: [PATCH 12/20] =?UTF-8?q?[FE]=20=EB=B3=80=EA=B2=BD=EB=90=9C=20?= =?UTF-8?q?=EC=9D=BC=EC=A0=95=20=EC=A1=B0=ED=9A=8C=20=EB=AA=85=EC=84=B8?= =?UTF-8?q?=EA=B0=80=20=EC=9E=91=EB=8F=99=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=ED=8C=80=20=EC=BA=98=EB=A6=B0=EB=8D=94=EC=99=80=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9=20=EC=BA=98=EB=A6=B0=EB=8D=94=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=84=B0=EB=A7=81=20(#897)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: 캘린더 바 렌더링 문제를 재현하도록 모킹 스케줄 데이터를 추가 * feat: Date 객체를 YYYYMMDD 형태로 변환해주는 유틸 함수 구현 * test: Date 객체를 YYYYMMDD 형태로 변환해주는 유틸 함수에 대한 테스트 작성 * refactor: 캘린더의 첫 날, 마지막 날에 대한 Date 객체를 계산하는 유틸 함수를 외부에서 쓸 수 있도록 변경 * fix: 일정 수정 및 드래그 시 msw에서 오류가 발생하는 현상 수정 - teamPlaceId가 누락되어서 그런 것으로 확인 - 맥락 없는 일정 500자 오류가 그동안 발생했던 원인 * refactor: 변경된 일정 API에 따라 년월대신 datetime 문자열을 받도록 변경 * test: 변경된 일정 API랑 동일하게 작동하도록 msw 로직 변경 - startdate, enddate 두 인자를 받고 이 범위에 조금이라도 걸치는 일정만을 필터링하여 반환 - 만약 startdate, enddate 값이 유효하지 않은 경우 400 Bad Request 반환, 이는 #889에서도 400으로 반환하도록 처리하고 있음 * refactor: 일정 조회 비동기 로직에 변경된 일정 명세 적용 * refactor: 일정 조회 프리페칭 로직에 변경된 일정 명세 적용 * refactor: 공통 캘린더 조회 비동기 로직에 변경된 일정 명세 적용 * refactor: 공통 캘린더 조회 프리페칭 로직에 변경된 일정 명세 적용 * feat: 캘린더 하나가 지니는 일수를 상수화 * refactor: 팀 캘린더 컴포넌트에 변경된 일정 명세를 적용 - 프리페칭 로직 포함 * refactor: 공통 캘린더 컴포넌트에 변경된 일정 명세를 적용 * refactor: 변경된 queryString 명세에 따라 startdate → startDate 와 같이 카멜케이스를 적용 * refactor: 스케줄 바 생성 유틸함수의 달력 날짜 계산 함수를 재은닉화 * refactor: YYYYMMDD 생성 함수의 이름을 변경 - 다른 PR에서 generateYYYYMMDD란 이름이 이미 쓰이고 있으며, 하이픈(-) 여부가 다르므로 이를 구분할 수 있도록, 이름이 다소 길어지더라도 충분히 기능을 설명하는 함수명을 채택하여야 한다고 판단하였다. * feat: generateCalendarRangeByYearMonth 함수 구현 - 연월을 받아 즉시 API에 요청 가능한 형태로 내보내는 함수 * test: generateCalendarRangeByYearMonth에 대한 테스트 코드 작성 * feat: DateRange 타입 추가 및 유틸 함수의 반환값 또한 해당 타입으로 설정 - 이렇게 하고, 이 반환값을 그대로 사용해 요청할 수 있도록 만든다면 불필요한 변수 이름을 짓는 것이 불필요해지며 더 간단하게 API 요청 함수를 사용할 수 있어 보인다 * refactor: 변경한 유틸함수명을 msw 로직에도 적용 * refactor: 변경한 유틸 함수를 비동기 로직에 반영 * refactor: 변경한 유틸 함수를 두 일정 컴포넌트에 반영 * refactor: 비동기 함수의 변수명 통일 * refactor: 불필요한 상수 제거 * fix: useFetchMySchedules에서 staleTime에 잘못된 상수를 사용한 점을 수정 * refactor: msw 모킹 로직에서의 변수명 통일 - ~~DateFormat → ~~Date * refactor: 이전 달력/다음 달력에 대한 값을 변수로 저장하여 대입하는 방식으로 변경 --- frontend/src/apis/schedule.ts | 21 +++------ .../my_calendar/MyCalendar/MyCalendar.tsx | 18 +++++-- .../TeamCalendar/TeamCalendar.tsx | 18 ++++--- .../src/hooks/queries/useFetchMySchedules.ts | 9 ++-- .../src/hooks/queries/useFetchSchedules.ts | 10 ++-- .../hooks/queries/usePrefetchMySchedules.ts | 8 ++-- .../src/hooks/queries/usePrefetchSchedules.ts | 9 ++-- frontend/src/mocks/fixtures/schedules.ts | 36 ++++++++++++++ frontend/src/mocks/handlers/calendar.ts | 47 +++++++++++++++++-- frontend/src/types/schedule.ts | 5 ++ .../utils/generateCalendarRangeByYearMonth.ts | 23 +++++++++ .../utils/generateYYYYMMDDWithoutHyphens.ts | 7 +++ .../generateCalendarRangeByYearMonth.test.ts | 17 +++++++ .../generateYYYYMMDDWithoutHyphens.test.ts | 20 ++++++++ 14 files changed, 205 insertions(+), 43 deletions(-) create mode 100644 frontend/src/utils/generateCalendarRangeByYearMonth.ts create mode 100644 frontend/src/utils/generateYYYYMMDDWithoutHyphens.ts create mode 100644 frontend/src/utils/test/generateCalendarRangeByYearMonth.test.ts create mode 100644 frontend/src/utils/test/generateYYYYMMDDWithoutHyphens.test.ts diff --git a/frontend/src/apis/schedule.ts b/frontend/src/apis/schedule.ts index c3459e844..122c24046 100644 --- a/frontend/src/apis/schedule.ts +++ b/frontend/src/apis/schedule.ts @@ -19,25 +19,18 @@ interface ICalendarResponse { export const fetchSchedules = ( teamPlaceId: number, - year: number, - month: number, - day?: number, + startDate: string, + endDate: string, ) => { - const query = day - ? `year=${year}&month=${month}&day=${day}` - : `year=${year}&month=${month}`; - return http.get( - `/api/team-place/${teamPlaceId}/calendar/schedules?${query}`, + `/api/team-place/${teamPlaceId}/calendar/schedules?startDate=${startDate}&endDate=${endDate}`, ); }; -export const fetchMySchedules = (year: number, month: number, day?: number) => { - const query = day - ? `year=${year}&month=${month}&day=${day}` - : `year=${year}&month=${month}`; - - return http.get(`/api/my-calendar/schedules?${query}`); +export const fetchMySchedules = (startDate: string, endDate: string) => { + return http.get( + `/api/my-calendar/schedules?startDate=${startDate}&endDate=${endDate}`, + ); }; export const fetchScheduleById = (teamPlaceId: number, scheduleId: number) => { diff --git a/frontend/src/components/my_calendar/MyCalendar/MyCalendar.tsx b/frontend/src/components/my_calendar/MyCalendar/MyCalendar.tsx index 683921430..1ead0a87c 100644 --- a/frontend/src/components/my_calendar/MyCalendar/MyCalendar.tsx +++ b/frontend/src/components/my_calendar/MyCalendar/MyCalendar.tsx @@ -13,6 +13,7 @@ import TeamBadge from '~/components/team/TeamBadge/TeamBadge'; import { getInfoByTeamPlaceId } from '~/utils/getInfoByTeamPlaceId'; import { useTeamPlace } from '~/hooks/useTeamPlace'; import { usePrefetchMySchedules } from '~/hooks/queries/usePrefetchMySchedules'; +import { generateCalendarRangeByYearMonth } from '~/utils/generateCalendarRangeByYearMonth'; interface MyCalendarProps { onDailyClick: (date: Date) => void; @@ -28,15 +29,22 @@ const MyCalendar = (props: MyCalendarProps) => { today, handlers: { handlePrevButtonClick, handleNextButtonClick }, } = useCalendar(); - const schedules = useFetchMySchedules(year, month); + const { teamPlaces } = useTeamPlace(); + + const prevCalendarYear = month === 0 ? year - 1 : year; + const prevCalendarMonth = month === 0 ? 11 : month - 1; + const nextCalendarYear = month === 11 ? year + 1 : year; + const nextCalendarMonth = month === 11 ? 0 : month + 1; + + const schedules = useFetchMySchedules( + generateCalendarRangeByYearMonth(year, month), + ); usePrefetchMySchedules( - month === 11 ? year + 1 : year, - month === 11 ? 0 : month + 1, + generateCalendarRangeByYearMonth(prevCalendarYear, prevCalendarMonth), ); usePrefetchMySchedules( - month === 0 ? year - 1 : year, - month === 0 ? 11 : month - 1, + generateCalendarRangeByYearMonth(nextCalendarYear, nextCalendarMonth), ); const scheduleCircles = generateScheduleCirclesMatrix(year, month, schedules); diff --git a/frontend/src/components/team_calendar/TeamCalendar/TeamCalendar.tsx b/frontend/src/components/team_calendar/TeamCalendar/TeamCalendar.tsx index d64dba882..f9bb11f65 100644 --- a/frontend/src/components/team_calendar/TeamCalendar/TeamCalendar.tsx +++ b/frontend/src/components/team_calendar/TeamCalendar/TeamCalendar.tsx @@ -21,6 +21,7 @@ import { DAYS_OF_WEEK, MODAL_OPEN_TYPE } from '~/constants/calendar'; import { generateScheduleBars } from '~/utils/generateScheduleBars'; import { arrayOf } from '~/utils/arrayOf'; import { getDateByPosition } from '~/utils/getDateByPosition'; +import { generateCalendarRangeByYearMonth } from '~/utils/generateCalendarRangeByYearMonth'; import type { Position, ModalOpenType } from '~/types/schedule'; import type { CalendarSize } from '~/types/size'; import { @@ -60,17 +61,22 @@ const TeamCalendar = (props: TeamCalendarProps) => { handlers: { handleScheduleModalOpen }, } = useScheduleModal(); - const schedules = useFetchSchedules(teamPlaceId, year, month); - // NOTE: month의 값은 0부터 시작하므로 1월, 11월에 해당하는 month의 값은 각각 0, 11이다. + const prevCalendarYear = month === 0 ? year - 1 : year; + const prevCalendarMonth = month === 0 ? 11 : month - 1; + const nextCalendarYear = month === 11 ? year + 1 : year; + const nextCalendarMonth = month === 11 ? 0 : month + 1; + + const schedules = useFetchSchedules( + teamPlaceId, + generateCalendarRangeByYearMonth(year, month), + ); usePrefetchSchedules( teamPlaceId, - month === 11 ? year + 1 : year, - month === 11 ? 0 : month + 1, + generateCalendarRangeByYearMonth(prevCalendarYear, prevCalendarMonth), ); usePrefetchSchedules( teamPlaceId, - month === 0 ? year - 1 : year, - month === 0 ? 11 : month - 1, + generateCalendarRangeByYearMonth(nextCalendarYear, nextCalendarMonth), ); const [clickedDate, setClickedDate] = useState(currentDate); diff --git a/frontend/src/hooks/queries/useFetchMySchedules.ts b/frontend/src/hooks/queries/useFetchMySchedules.ts index 0cee77716..98b5eea79 100644 --- a/frontend/src/hooks/queries/useFetchMySchedules.ts +++ b/frontend/src/hooks/queries/useFetchMySchedules.ts @@ -1,11 +1,14 @@ import { useQuery } from '@tanstack/react-query'; import { fetchMySchedules } from '~/apis/schedule'; import { STALE_TIME } from '~/constants/query'; +import type { DateRange } from '~/types/schedule'; + +export const useFetchMySchedules = (dateRange: DateRange) => { + const { startDate, endDate } = dateRange; -export const useFetchMySchedules = (year: number, month: number) => { const { data } = useQuery( - ['mySchedules', year, month], - () => fetchMySchedules(year, month + 1), + ['mySchedules', startDate, endDate], + () => fetchMySchedules(startDate, endDate), { staleTime: STALE_TIME.MY_SCHEDULES, }, diff --git a/frontend/src/hooks/queries/useFetchSchedules.ts b/frontend/src/hooks/queries/useFetchSchedules.ts index 1226a5ee6..86fb2d0d5 100644 --- a/frontend/src/hooks/queries/useFetchSchedules.ts +++ b/frontend/src/hooks/queries/useFetchSchedules.ts @@ -1,15 +1,17 @@ import { useQuery } from '@tanstack/react-query'; import { fetchSchedules } from '~/apis/schedule'; import { STALE_TIME } from '~/constants/query'; +import type { DateRange } from '~/types/schedule'; export const useFetchSchedules = ( teamPlaceId: number, - year: number, - month: number, + dateRange: DateRange, ) => { + const { startDate, endDate } = dateRange; + const { data } = useQuery( - ['schedules', teamPlaceId, year, month], - () => fetchSchedules(teamPlaceId, year, month + 1), + ['schedules', teamPlaceId, startDate, endDate], + () => fetchSchedules(teamPlaceId, startDate, endDate), { enabled: teamPlaceId > 0, staleTime: STALE_TIME.SCHEDULES, diff --git a/frontend/src/hooks/queries/usePrefetchMySchedules.ts b/frontend/src/hooks/queries/usePrefetchMySchedules.ts index c87e1fc67..796d1be61 100644 --- a/frontend/src/hooks/queries/usePrefetchMySchedules.ts +++ b/frontend/src/hooks/queries/usePrefetchMySchedules.ts @@ -1,14 +1,16 @@ import { useQueryClient } from '@tanstack/react-query'; import { fetchMySchedules } from '~/apis/schedule'; import { STALE_TIME } from '~/constants/query'; +import type { DateRange } from '~/types/schedule'; -export const usePrefetchMySchedules = async (year: number, month: number) => { - const queryKey = ['mySchedules', year, month]; +export const usePrefetchMySchedules = async (dateRange: DateRange) => { + const { startDate, endDate } = dateRange; + const queryKey = ['mySchedules', startDate, endDate]; const queryClient = useQueryClient(); await queryClient.prefetchQuery( queryKey, - () => fetchMySchedules(year, month + 1), + () => fetchMySchedules(startDate, endDate), { staleTime: STALE_TIME.MY_SCHEDULES, }, diff --git a/frontend/src/hooks/queries/usePrefetchSchedules.ts b/frontend/src/hooks/queries/usePrefetchSchedules.ts index 009ff8fe1..64a2797fe 100644 --- a/frontend/src/hooks/queries/usePrefetchSchedules.ts +++ b/frontend/src/hooks/queries/usePrefetchSchedules.ts @@ -1,19 +1,20 @@ import { useQueryClient } from '@tanstack/react-query'; import { fetchSchedules } from '~/apis/schedule'; import { STALE_TIME } from '~/constants/query'; +import type { DateRange } from '~/types/schedule'; export const usePrefetchSchedules = async ( teamPlaceId: number, - year: number, - month: number, + dateRange: DateRange, ) => { + const { startDate, endDate } = dateRange; const queryClient = useQueryClient(); const enabled = teamPlaceId > 0; if (enabled) { await queryClient.prefetchQuery( - ['schedules', teamPlaceId, year, month], - () => fetchSchedules(teamPlaceId, year, month + 1), + ['schedules', teamPlaceId, startDate, endDate], + () => fetchSchedules(teamPlaceId, startDate, endDate), { staleTime: STALE_TIME.SCHEDULES, }, diff --git a/frontend/src/mocks/fixtures/schedules.ts b/frontend/src/mocks/fixtures/schedules.ts index abd221e13..df458855b 100644 --- a/frontend/src/mocks/fixtures/schedules.ts +++ b/frontend/src/mocks/fixtures/schedules.ts @@ -73,6 +73,42 @@ export const schedules: Schedule[] = [ startDateTime: '2023-09-30 05:00', endDateTime: '2023-10-02 05:00', }, + { + id: 12, + title: '이전 달 일정', + startDateTime: '2023-11-27 10:00', + endDateTime: '2023-11-30 18:00', + }, + { + id: 13, + title: '이번 달과 이전 달에 겹친 일정', + startDateTime: '2023-11-29 10:00', + endDateTime: '2023-12-01 18:00', + }, + { + id: 14, + title: '이번 달 일정', + startDateTime: '2023-12-12 10:00', + endDateTime: '2023-12-14 18:00', + }, + { + id: 15, + title: '이번 달과 다음 달에 걸친 일정', + startDateTime: '2023-12-29 10:00', + endDateTime: '2024-01-01 18:00', + }, + { + id: 16, + title: '다음 달 일정', + startDateTime: '2024-01-01 10:00', + endDateTime: '2024-01-03 18:00', + }, + { + id: 17, + title: '다음 달 일정 2', + startDateTime: '2024-01-02 10:00', + endDateTime: '2024-01-15 18:00', + }, ]; export const mySchedules: ScheduleWithTeamPlaceId[] = [ diff --git a/frontend/src/mocks/handlers/calendar.ts b/frontend/src/mocks/handlers/calendar.ts index 4c6e1bd05..fd261c8d7 100644 --- a/frontend/src/mocks/handlers/calendar.ts +++ b/frontend/src/mocks/handlers/calendar.ts @@ -4,17 +4,36 @@ import { mySchedules as myScheduleData, } from '~/mocks/fixtures/schedules'; import { teamPlaces } from '~/mocks/fixtures/team'; +import { generateYYYYMMDDWithoutHyphens } from '~/utils/generateYYYYMMDDWithoutHyphens'; let schedules = [...scheduleData]; let mySchedules = [...myScheduleData]; export const calendarHandlers = [ //통합캘린더 일정 기간 조회 - rest.get(`/api/my-calendar/schedules`, (_, res, ctx) => { + rest.get(`/api/my-calendar/schedules`, (req, res, ctx) => { + const startDate = req.url.searchParams.get('startDate'); + const endDate = req.url.searchParams.get('endDate'); + + if (!startDate || !endDate) { + return res(ctx.status(400)); + } + + const searchedMySchedules = mySchedules.filter( + ({ startDateTime, endDateTime }) => { + const isScheduleInRange = + startDate <= + generateYYYYMMDDWithoutHyphens(new Date(startDateTime)) || + endDate >= generateYYYYMMDDWithoutHyphens(new Date(endDateTime)); + + return isScheduleInRange; + }, + ); + return res( ctx.status(200), ctx.json({ - schedules: mySchedules, + schedules: searchedMySchedules, }), ); }), @@ -24,16 +43,34 @@ export const calendarHandlers = [ `/api/team-place/:teamPlaceId/calendar/schedules`, (req, res, ctx) => { const teamPlaceId = Number(req.params.teamPlaceId); + const startDate = req.url.searchParams.get('startDate'); + const endDate = req.url.searchParams.get('endDate'); + const index = teamPlaces.findIndex( (teamPlace) => teamPlace.id === teamPlaceId, ); if (index === -1) return res(ctx.status(403)); + if (!startDate || !endDate) { + return res(ctx.status(400)); + } + + const searchedSchedules = schedules.filter( + ({ startDateTime, endDateTime }) => { + const isScheduleInRange = + startDate <= + generateYYYYMMDDWithoutHyphens(new Date(startDateTime)) || + endDate >= generateYYYYMMDDWithoutHyphens(new Date(endDateTime)); + + return isScheduleInRange; + }, + ); + return res( ctx.status(200), ctx.json({ - schedules, + schedules: searchedSchedules, }), ); }, @@ -94,7 +131,9 @@ export const calendarHandlers = [ rest.patch( `/api/team-place/:teamPlaceId/calendar/schedules/:scheduleId`, async (req, res, ctx) => { + const teamPlaceId = Number(req.params.teamPlaceId); const scheduleId = Number(req.params.scheduleId); + const { title, startDateTime, endDateTime } = await req.json(); const index = schedules.findIndex( (schedule) => schedule.id === scheduleId, @@ -116,7 +155,7 @@ export const calendarHandlers = [ mySchedules[myIndex] = { id: scheduleId, - teamPlaceId: mySchedules[myIndex].teamPlaceId, + teamPlaceId, title, startDateTime, endDateTime, diff --git a/frontend/src/types/schedule.ts b/frontend/src/types/schedule.ts index 661a85396..9a752f251 100644 --- a/frontend/src/types/schedule.ts +++ b/frontend/src/types/schedule.ts @@ -58,3 +58,8 @@ export interface DragStatus { initX: number; initY: number; } + +export interface DateRange { + startDate: string; + endDate: string; +} diff --git a/frontend/src/utils/generateCalendarRangeByYearMonth.ts b/frontend/src/utils/generateCalendarRangeByYearMonth.ts new file mode 100644 index 000000000..d0440ee10 --- /dev/null +++ b/frontend/src/utils/generateCalendarRangeByYearMonth.ts @@ -0,0 +1,23 @@ +import { CALENDAR, ONE_DAY } from '~/constants/calendar'; +import { generateYYYYMMDDWithoutHyphens } from '~/utils/generateYYYYMMDDWithoutHyphens'; +import type { DateRange } from '~/types/schedule'; + +export const generateCalendarRangeByYearMonth = ( + year: number, + month: number, +): DateRange => { + const firstDateOfMonth = new Date(year, month); + const firstDateOfCalendar = new Date( + firstDateOfMonth.getTime() - ONE_DAY * firstDateOfMonth.getDay(), + ); + const lastDateOfCalendar = new Date( + firstDateOfCalendar.getTime() + + CALENDAR.ROW_SIZE * CALENDAR.COLUMN_SIZE * ONE_DAY - + 1, + ); + + const startDate = generateYYYYMMDDWithoutHyphens(firstDateOfCalendar); + const endDate = generateYYYYMMDDWithoutHyphens(lastDateOfCalendar); + + return { startDate, endDate }; +}; diff --git a/frontend/src/utils/generateYYYYMMDDWithoutHyphens.ts b/frontend/src/utils/generateYYYYMMDDWithoutHyphens.ts new file mode 100644 index 000000000..abf511745 --- /dev/null +++ b/frontend/src/utils/generateYYYYMMDDWithoutHyphens.ts @@ -0,0 +1,7 @@ +export const generateYYYYMMDDWithoutHyphens = (date: Date) => { + const year = String(date.getFullYear()).padStart(4, '0'); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + + return `${year}${month}${day}`; +}; diff --git a/frontend/src/utils/test/generateCalendarRangeByYearMonth.test.ts b/frontend/src/utils/test/generateCalendarRangeByYearMonth.test.ts new file mode 100644 index 000000000..35e81f89e --- /dev/null +++ b/frontend/src/utils/test/generateCalendarRangeByYearMonth.test.ts @@ -0,0 +1,17 @@ +import { generateCalendarRangeByYearMonth } from '~/utils/generateCalendarRangeByYearMonth'; +describe('캘린더 범위 생성 테스트', () => { + it.each([ + [2023, 0, { startDate: '20230101', endDate: '20230211' }], + [2023, 1, { startDate: '20230129', endDate: '20230311' }], + [2023, 4, { startDate: '20230430', endDate: '20230610' }], + [2023, 6, { startDate: '20230625', endDate: '20230805' }], + [2023, 11, { startDate: '20231126', endDate: '20240106' }], + [2018, 8, { startDate: '20180826', endDate: '20181006' }], + [2025, 3, { startDate: '20250330', endDate: '20250510' }], + ])( + '%s년 %s월 달력(월은 0-based)의 경우 결괏값은 %s 여야 한다.', + (year, month, expected) => { + expect(generateCalendarRangeByYearMonth(year, month)).toEqual(expected); + }, + ); +}); diff --git a/frontend/src/utils/test/generateYYYYMMDDWithoutHyphens.test.ts b/frontend/src/utils/test/generateYYYYMMDDWithoutHyphens.test.ts new file mode 100644 index 000000000..b6d06a296 --- /dev/null +++ b/frontend/src/utils/test/generateYYYYMMDDWithoutHyphens.test.ts @@ -0,0 +1,20 @@ +import { generateYYYYMMDDWithoutHyphens } from '~/utils/generateYYYYMMDDWithoutHyphens'; + +describe('YYYYMMDD 포맷 생성 테스트', () => { + it.each([ + [new Date('2023-12-19'), '20231219'], + [new Date('0072-01-06'), '00720106'], + [new Date('1972-11-21 04:58'), '19721121'], + [new Date('1972-11-21 23:59'), '19721121'], + [new Date('2023-06-13'), '20230613'], + [new Date('2013-12-01'), '20131201'], + [new Date('2020-02-29'), '20200229'], + [new Date('2000-02-29'), '20000229'], + [new Date('1964-12-31'), '19641231'], + ])( + '%s 정보를 지니는 Date 객체에 대해 %s 값이 반환되어야 한다.', + (date, expected) => { + expect(generateYYYYMMDDWithoutHyphens(date)).toBe(expected); + }, + ); +}); From 7319d62f0c2b535458ced2d3ce352a6e60c16be3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9A=94=EC=88=A0=ED=86=A0=EB=81=BC?= Date: Wed, 27 Dec 2023 19:12:23 +0900 Subject: [PATCH 13/20] =?UTF-8?q?[FE]=20=EC=9D=BC=EC=A0=95=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=20=EB=AC=B8=EA=B5=AC=20=EA=B0=9C=EC=84=A0=20(#905)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: generateDateTimeRangeDescription 유틸 함수 구현 - 시작 일자와 마감 일자를 받고, 이에 대응하는 적절한 설명을 문자열 형태로 리턴 * test: feat: generateDateTimeRangeDescription 유틸 함수에 대한 테스트 작성 * feat: 구현한 유틸 함수를 스케줄 모달 컴포넌트에도 적용 --- .../ScheduleModal/ScheduleModal.tsx | 10 ++-- .../utils/generateDateTimeRangeDescription.ts | 34 ++++++++++++++ .../generateDateTimeRangeDescription.test.ts | 47 +++++++++++++++++++ 3 files changed, 85 insertions(+), 6 deletions(-) create mode 100644 frontend/src/utils/generateDateTimeRangeDescription.ts create mode 100644 frontend/src/utils/test/generateDateTimeRangeDescription.test.ts diff --git a/frontend/src/components/team_calendar/ScheduleModal/ScheduleModal.tsx b/frontend/src/components/team_calendar/ScheduleModal/ScheduleModal.tsx index 9c6994dc7..04487c776 100644 --- a/frontend/src/components/team_calendar/ScheduleModal/ScheduleModal.tsx +++ b/frontend/src/components/team_calendar/ScheduleModal/ScheduleModal.tsx @@ -4,7 +4,6 @@ import { useModal } from '~/hooks/useModal'; import * as S from './ScheduleModal.styled'; import { CloseIcon, DeleteIcon, EditIcon } from '~/assets/svg'; import Button from '~/components/common/Button/Button'; -import { formatDateTime } from '~/utils/formatDateTime'; import type { SchedulePosition } from '~/types/schedule'; import { useFetchScheduleById } from '~/hooks/queries/useFetchScheduleById'; import { useDeleteSchedule } from '~/hooks/queries/useDeleteSchedule'; @@ -13,6 +12,7 @@ import { useToast } from '~/hooks/useToast'; import { useTeamPlace } from '~/hooks/useTeamPlace'; import type { CalendarSize } from '~/types/size'; import { getIsMobile } from '~/utils/getIsMobile'; +import { generateDateTimeRangeDescription } from '~/utils/generateDateTimeRangeDescription'; interface ScheduleModalProps { calendarWidth: number; @@ -111,11 +111,9 @@ const ScheduleModal = (props: ScheduleModalProps) => { - ~ - diff --git a/frontend/src/components/common/NavigationBar/NavigationBar.styled.ts b/frontend/src/components/common/NavigationBar/NavigationBar.styled.ts index 1a5d486c9..58b58bdb1 100644 --- a/frontend/src/components/common/NavigationBar/NavigationBar.styled.ts +++ b/frontend/src/components/common/NavigationBar/NavigationBar.styled.ts @@ -9,7 +9,6 @@ export const Nav = styled.nav<{ $isMobile: boolean }>` return css` width: 100%; height: 60px; - padding: 10px; `; return css` diff --git a/frontend/src/components/feed/ImageUploadDrawer/ImageUploadDrawer.stories.tsx b/frontend/src/components/feed/ImageUploadDrawer/ImageUploadDrawer.stories.tsx index 655ed8e08..2a4371776 100644 --- a/frontend/src/components/feed/ImageUploadDrawer/ImageUploadDrawer.stories.tsx +++ b/frontend/src/components/feed/ImageUploadDrawer/ImageUploadDrawer.stories.tsx @@ -33,6 +33,10 @@ const meta = { description: '랜더링할 자식 요소를 의미합니다. `ThumbnailList` 컴포넌트가 여기에 오면 됩니다.', }, + slideDistance: { + description: + '서랍장이 열리게 될 경우 얼마나 많은 거리를 위로 움직여야 할 지를 의미합니다. 입력값은 숫자이며 단위는 `px`입니다.', + }, onClose: { description: '서랍장이 닫히게 될 때 실행시킬 함수를 의미합니다. 서랍장을 실질적으로 닫는 함수를 여기에 넣어 주시면 됩니다.', @@ -47,6 +51,7 @@ type Story = StoryObj; export const Default: Story = { args: { isOpen: false, + slideDistance: 163, children: (

이 자리에 썸네일 리스트 컴포넌트가 올 것입니다. @@ -62,6 +67,23 @@ export const Default: Story = { export const Opened: Story = { args: { isOpen: true, + slideDistance: 163, + children: ( +
+ 이 자리에 썸네일 리스트 컴포넌트가 올 것입니다. +
+ ), + onClose: () => { + alert('onClose();'); + }, + isUploading: false, + }, +}; + +export const CustomDistanceOpened: Story = { + args: { + isOpen: true, + slideDistance: 0, children: (
이 자리에 썸네일 리스트 컴포넌트가 올 것입니다. diff --git a/frontend/src/components/feed/ImageUploadDrawer/ImageUploadDrawer.styled.ts b/frontend/src/components/feed/ImageUploadDrawer/ImageUploadDrawer.styled.ts index 203250698..90752f099 100644 --- a/frontend/src/components/feed/ImageUploadDrawer/ImageUploadDrawer.styled.ts +++ b/frontend/src/components/feed/ImageUploadDrawer/ImageUploadDrawer.styled.ts @@ -1,6 +1,10 @@ import { styled, css } from 'styled-components'; -export const Container = styled.div<{ $isOpen: boolean; $isMobile: boolean }>` +export const Container = styled.div<{ + $isOpen: boolean; + $isMobile: boolean; + $slideDistance: number; +}>` display: flex; position: absolute; @@ -26,7 +30,9 @@ export const Container = styled.div<{ $isOpen: boolean; $isMobile: boolean }>` background: linear-gradient(30deg, #bfc3ff, #eaebff); transition: 0.35s; - transform: translateY(${({ $isOpen }) => ($isOpen ? '-163px' : '0')}); + transform: translateY( + ${({ $isOpen, $slideDistance }) => ($isOpen ? `-${$slideDistance}px` : 0)} + ); `; export const ContentWrapper = styled.div` diff --git a/frontend/src/components/feed/ImageUploadDrawer/ImageUploadDrawer.tsx b/frontend/src/components/feed/ImageUploadDrawer/ImageUploadDrawer.tsx index a9858142a..89d3e066a 100644 --- a/frontend/src/components/feed/ImageUploadDrawer/ImageUploadDrawer.tsx +++ b/frontend/src/components/feed/ImageUploadDrawer/ImageUploadDrawer.tsx @@ -6,6 +6,7 @@ import { getIsMobile } from '~/utils/getIsMobile'; interface ImageUploadDrawerProps { isOpen: boolean; + slideDistance: number; onClose: () => void; isUploading: boolean; } @@ -13,11 +14,15 @@ interface ImageUploadDrawerProps { const ImageUploadDrawer = ( props: PropsWithChildren, ) => { - const { isOpen, onClose, children, isUploading } = props; + const { isOpen, onClose, children, isUploading, slideDistance } = props; const isMobile = getIsMobile(); return ( - + {children} {!isUploading && ( diff --git a/frontend/src/components/feed/Thread/Thread.styled.ts b/frontend/src/components/feed/Thread/Thread.styled.ts index bb2992938..38935c1ab 100644 --- a/frontend/src/components/feed/Thread/Thread.styled.ts +++ b/frontend/src/components/feed/Thread/Thread.styled.ts @@ -47,7 +47,7 @@ export const ContentWrapper = styled.div` position: relative; overflow: hidden; - padding: 16px 28px; + padding: 10px 24px; `; export const ThreadHeader = styled.div` @@ -81,7 +81,7 @@ export const ThumbnailListWrapper = styled.div<{ height: 136px; padding: 40px 20px 0 20px; margin-top: -20px; - margin-bottom: ${({ $marginBottom }) => ($marginBottom ? '40px' : '20px')}; + margin-bottom: ${({ $marginBottom }) => ($marginBottom ? '30px' : '10px')}; background: ${({ theme, $isMe }) => $isMe ? theme.gradient.BLURPLE('116px') : theme.gradient.WHITE('116px')}; @@ -101,7 +101,7 @@ export const contentField = (threadSize: ThreadSize, isMe: boolean) => css` width: 100%; white-space: pre-wrap; - font-size: ${threadSize === 'md' ? 18 : 16}px; + font-size: ${threadSize === 'md' ? 16 : 14}px; color: ${({ theme }) => (isMe ? theme.color.WHITE : theme.color.BLACK)}; word-break: break-all; diff --git a/frontend/src/mobilePages/M_PageTemplate/M_PageTemplate.styled.ts b/frontend/src/mobilePages/M_PageTemplate/M_PageTemplate.styled.ts index 2186de1a4..4004eb925 100644 --- a/frontend/src/mobilePages/M_PageTemplate/M_PageTemplate.styled.ts +++ b/frontend/src/mobilePages/M_PageTemplate/M_PageTemplate.styled.ts @@ -1,6 +1,7 @@ import { styled } from 'styled-components'; export const PageContainer = styled.div` + justify-content: space-between; position: absolute; top: 0; left: 0; @@ -13,5 +14,5 @@ export const PageContainer = styled.div` export const PageWrapper = styled.main` flex: 1; - height: calc(100vh - 170px); + height: calc(var(--vh, 1vh) * 100 - 150px); `; diff --git a/frontend/src/pages/TeamFeedPage/TeamFeedPage.styled.ts b/frontend/src/pages/TeamFeedPage/TeamFeedPage.styled.ts index c5ede931b..103d4a9cd 100644 --- a/frontend/src/pages/TeamFeedPage/TeamFeedPage.styled.ts +++ b/frontend/src/pages/TeamFeedPage/TeamFeedPage.styled.ts @@ -42,8 +42,8 @@ export const ThreadListWrapper = styled.div` display: flex; flex-direction: column; - row-gap: 24px; - padding: 20px 30px; + row-gap: 16px; + padding: 20px 30px 0; background-color: ${({ theme }) => theme.color.WHITE}; `; @@ -113,6 +113,10 @@ export const scrollBottomButton = css` box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1); `; +export const noPaddingButton = css` + padding: 0; +`; + export const noticeText = css` margin-right: 10px; diff --git a/frontend/src/pages/TeamFeedPage/TeamFeedPage.tsx b/frontend/src/pages/TeamFeedPage/TeamFeedPage.tsx index 5f16eead0..789ca35ce 100644 --- a/frontend/src/pages/TeamFeedPage/TeamFeedPage.tsx +++ b/frontend/src/pages/TeamFeedPage/TeamFeedPage.tsx @@ -99,6 +99,7 @@ const TeamFeedPage = (props: TeamFeedPageProps) => { isOpen={isImageDrawerOpen} onClose={handleImageDrawerToggle} isUploading={isSendingImage} + slideDistance={isMobile ? 142 : 162} > { variant="plain" aria-label="이미지 업로드하기" onClick={handleImageDrawerToggle} + css={isMobile && S.noPaddingButton} disabled={isSendingImage} > @@ -146,6 +148,7 @@ const TeamFeedPage = (props: TeamFeedPageProps) => { variant="plain" aria-label="채팅 전송하기" disabled={isSendingImage} + css={isMobile && S.noPaddingButton} > Date: Wed, 3 Jan 2024 15:09:42 +0900 Subject: [PATCH 20/20] [ALL] Release 1.6.0 (#910) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 039c12f92..43638b547 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![](https://img.shields.io/badge/-teamby.team-important?style=flat&logo=airplayvideo&logoColor=white&labelColor=black&color=%233145FF)](https://teamby.team/) [![](https://img.shields.io/badge/-Tech%20Blog-important?style=flat&logo=angellist&logoColor=balck&labelColor=black&color=white) ](https://team-by-team.github.io/) -[![](https://img.shields.io/badge/release-v1.5.2-critical?style=flat&logo=github&logoColor=balck&labelColor=black&color=white) +[![](https://img.shields.io/badge/release-v1.6.0-critical?style=flat&logo=github&logoColor=balck&labelColor=black&color=white) ](https://github.com/woowacourse-teams/2023-team-by-team/releases) # 팀바팀