From 33ea0bbfd2840d28fcc4e97b7e194b17149ba441 Mon Sep 17 00:00:00 2001 From: meatsby Date: Wed, 31 Aug 2022 10:51:27 +0900 Subject: [PATCH 01/19] =?UTF-8?q?[ALL]=20README.md=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EB=B0=8F=20=EB=82=B4=EC=9A=A9=20=EC=9E=91=EC=84=B1=20(#540)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * README 추가 * README 이미지 수정 * README 이미지 수정 * README 이미지 수정 * frontend tech stack 이미지 추가 * docs(README): 팀 레포 리드미 작성 * docs(README): 웹사이트, 팀 블로그 주소 추가 * docs(README): 프론트 기술 스택 추가 및 인프라 인포그래픽 변경 * docs(README): 워크플로우 현황 뱃지 추가 * docs(README): 빌드 로고 변경 Co-authored-by: beomWhale --- README.md | 98 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..005647aa --- /dev/null +++ b/README.md @@ -0,0 +1,98 @@ +

+ +

+ +
+ 📖 함께 사용하는 우리의 공간, 우리가 체크하자! +
+ +
+ +
+ +[![Application](http://img.shields.io/badge/Application-blue?style=flat-square&logo=googlechrome&logoColor=white&link=https://https://gongcheck.day//)](https://gongcheck.day/) +[![Tech Blog](http://img.shields.io/badge/Tech%20Blog-green?style=flat-square&logo=github&logoColor=white&link=https://gong-check.github.io/dev-blog/)](https://gong-check.github.io/dev-blog/) + +
+ +
+ +![](https://img.shields.io/github/workflow/status/woowacourse-teams/2022-gong-check/frontend?label=frontend&logo=github&style=flat-square) +![](https://img.shields.io/github/workflow/status/woowacourse-teams/2022-gong-check/sonarqube%20backend?label=backend&logo=github&style=flat-square) +![](https://img.shields.io/github/workflow/status/woowacourse-teams/2022-gong-check/sonar%20imagestorage?label=imagestorage&logo=github&style=flat-square) + +
+ +## 🛠 Tech Stacks + +### Front-End + +Screen Shot 2022-08-27 at 4 45 07 PM + +### Back-End + +Screen Shot 2022-08-26 at 1 04 01 AM + +### Infra + +Screen Shot 2022-08-26 at 1 05 05 AM + +
+ +## 🛰 Infrastructures + +### Architecture + +Screen Shot 2022-08-27 at 4 45 57 PM + +### CI/CD Pipeline + +Screen Shot 2022-08-27 at 4 46 27 PM + +
+ +## 🫂 Members + +| [코카콜라](https://github.com/intae92) | [온스타](https://github.com/cks3066) | [어썸오](https://github.com/awesomeo184) | [오리](https://github.com/jinyoungchoi95) | [쿼리치](https://github.com/meatsby) | [찬](https://github.com/kimchan123) | [범고래](https://github.com/cndqjacndqja) | +| :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | +| Avatar | Avatar | Avatar | Avatar | Avatar | Avatar | Avatar | +| 프론트엔드 | 프론트엔드 | 백엔드 | 백엔드 | 백엔드 | 백엔드 | 백엔드 | + +
+ +## 🌌 Team Culture + +### 🛎 소통 + +- 모르는 것을 부끄러워하지 말아요. +- 질문은 자유롭게, 주장은 근거있게 해주세요. +- 각 스프린트마다 찐하게 회고해요! + +### 📌 공유 + +- 매일 출근하고 데일리 미팅을 진행해요. +- 주 단위로 팀 전체의 To-do 리스트를 공유해요. +- 매일 퇴근 전에 간단히 체크아웃 미팅을 진행해요. + +### 🔗 약속 + +- 수평적인 관계로 서로를 존중해주세요. +- 10AM-6PM 시간은 잘 지켜주세요. +- 늦으면 벌칙이 있어요! ☕️ + +### 🖍 이슈 관리 + +- 생각나는 이슈가 있으면 바로바로 작성해주세요. +- 사소해도 괜찮아요. +- 자세하게 작성해주세요. +- 이슈 형식에 너무 부담 갖지말아요. + +### 🔎 코드 리뷰 + +- 모든 사람이 Approve 해야 Merge 할 수 있어요. +- PR 은 일과 중에만 날려주세요. +- 품질 높은 코드 리뷰를 위해 구체적으로 PR 을 작성해주세요. + +### 📝 팀 블로그 + +- 서로 배운 내용을 일주일에 한 번씩 글로 작성해서 공유해요. From c1e29d76fa02fed15468de2ce89b6f279c4bd467 Mon Sep 17 00:00:00 2001 From: Seungchan On <62434898+cks3066@users.noreply.github.com> Date: Tue, 13 Sep 2022 21:37:35 +0900 Subject: [PATCH 02/19] =?UTF-8?q?[FE]=20=EA=B8=B0=EC=A1=B4=20=EB=AA=A8?= =?UTF-8?q?=EB=B0=94=EC=9D=BC=20=EA=B8=B0=EC=A4=80=EC=9D=B4=EB=8D=98=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9E=90=20=ED=99=94=EB=A9=B4=EC=9D=84=20?= =?UTF-8?q?=EB=8B=A4=EC=96=91=ED=95=9C=20=EA=B8=B0=EA=B8=B0=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=9E=91=EB=8F=99=ED=95=A0=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=ED=95=9C=EB=8B=A4.=20(#?= =?UTF-8?q?548)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: webp 설정 * feat: transition 변경 * feat: dimmer 모바일 모드 삭제 * feat: 기본 이미지 추가 * feat: lazyloading 컴포넌트 생성 * feat: joblist 페이지 수정 * feat: task list 코드 분리 * feat: transition 교체 * feat: space list 페이지 수정 * feat: useScroll 커스텀훅 수정 * feat: 레이아웃 수정 * feat: task list 페이지 수정 * feat: Sticky 컴포넌트 생성 * feat: transition 교체 * feat: input autocomplete 추가 * feat: 언마운트시 intersectionObserver disconnect --- frontend/src/assets/defaultSpaceImage.webp | Bin 0 -> 27670 bytes .../src/components/common/Checkbox/styles.ts | 1 + .../src/components/common/Dimmer/index.tsx | 5 +- .../src/components/common/Dimmer/styles.ts | 3 +- .../src/components/common/LazyImage/index.tsx | 34 ++++++ .../src/components/common/Sticky/index.tsx | 33 ++++++ .../src/components/common/Sticky/style.ts | 24 +++++ .../host/SectionDetailModal/index.tsx | 2 +- .../components/host/SlackUrlModal/index.tsx | 2 +- .../host/SpaceDeleteModal/index.tsx | 2 +- .../components/user/DetailInfoModal/index.tsx | 2 +- .../src/components/user/JobCard/index.tsx | 4 +- .../src/components/user/JobCard/styles.ts | 15 ++- .../src/components/user/NameModal/index.tsx | 2 +- .../src/components/user/SectionCard/index.tsx | 79 ++++++++++++++ .../src/components/user/SectionCard/styles.ts | 79 ++++++++++++++ .../user/SectionInfoPreview/index.tsx | 6 +- .../src/components/user/SpaceCard/styles.ts | 9 +- .../src/components/user/TaskCard/index.tsx | 51 --------- .../src/components/user/TaskCard/styles.ts | 38 ------- frontend/src/hooks/useScroll.ts | 8 +- frontend/src/hooks/useTransitionSelect.ts | 32 +++--- frontend/src/layouts/HostLayout/styles.ts | 2 +- frontend/src/layouts/UserLayout/index.tsx | 10 +- frontend/src/layouts/UserLayout/styles.ts | 6 +- frontend/src/pages/user/JobList/index.tsx | 7 +- frontend/src/pages/user/JobList/styles.ts | 1 + frontend/src/pages/user/Password/index.tsx | 1 + frontend/src/pages/user/SpaceList/index.tsx | 5 +- frontend/src/pages/user/SpaceList/styles.ts | 2 +- frontend/src/pages/user/TaskList/index.tsx | 68 +++++------- frontend/src/pages/user/TaskList/styles.ts | 102 +++++------------- .../src/pages/user/TaskList/useTaskList.tsx | 16 +-- frontend/src/styles/animation.ts | 24 +++++ frontend/src/styles/global.ts | 8 +- frontend/src/styles/transitions.ts | 43 +++++--- frontend/src/types/global.d.ts | 1 + frontend/webpack.config.js | 2 +- 38 files changed, 437 insertions(+), 292 deletions(-) create mode 100644 frontend/src/assets/defaultSpaceImage.webp create mode 100644 frontend/src/components/common/LazyImage/index.tsx create mode 100644 frontend/src/components/common/Sticky/index.tsx create mode 100644 frontend/src/components/common/Sticky/style.ts create mode 100644 frontend/src/components/user/SectionCard/index.tsx create mode 100644 frontend/src/components/user/SectionCard/styles.ts delete mode 100644 frontend/src/components/user/TaskCard/index.tsx delete mode 100644 frontend/src/components/user/TaskCard/styles.ts diff --git a/frontend/src/assets/defaultSpaceImage.webp b/frontend/src/assets/defaultSpaceImage.webp new file mode 100644 index 0000000000000000000000000000000000000000..41b867160ae164b973285154f11b3646b6cc1e40 GIT binary patch literal 27670 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 Vj} = ({ children, isAbleClick = true, mode = 'full' }) => { +const Dimmer: React.FC = ({ children, isAbleClick = true }) => { const { closeModal } = useModal(); const onClickDimmed = (e: React.MouseEvent) => { @@ -16,7 +15,7 @@ const Dimmer: React.FC = ({ children, isAbleClick = true, mode = 'f }; return ( -
+
{children}
); diff --git a/frontend/src/components/common/Dimmer/styles.ts b/frontend/src/components/common/Dimmer/styles.ts index 1bd2914c..e17d300a 100644 --- a/frontend/src/components/common/Dimmer/styles.ts +++ b/frontend/src/components/common/Dimmer/styles.ts @@ -2,9 +2,8 @@ import { css } from '@emotion/react'; import theme from '@/styles/theme'; -const dimmer = (mode: 'full' | 'mobile') => css` +const dimmer = css` background-color: ${theme.colors.shadow80}; - max-width: ${mode === 'full' ? '100vw' : '414px'}; width: 100vw; height: 100vh; display: flex; diff --git a/frontend/src/components/common/LazyImage/index.tsx b/frontend/src/components/common/LazyImage/index.tsx new file mode 100644 index 00000000..fc717178 --- /dev/null +++ b/frontend/src/components/common/LazyImage/index.tsx @@ -0,0 +1,34 @@ +import { useEffect, useRef, useState } from 'react'; + +interface LazyImageProps extends React.ImgHTMLAttributes { + imageUrl: string | undefined; +} + +const LazyImage: React.FC = ({ imageUrl, ...props }) => { + const lazyImageRef = useRef(null); + const observerRef = useRef(); + const [isLoaded, setIsLoaded] = useState(false); + + const intersectionCallBack = (entries: IntersectionObserverEntry[], io: IntersectionObserver) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + io.unobserve(entry.target); + setIsLoaded(true); + } + }); + }; + + useEffect(() => { + if (!observerRef.current) { + observerRef.current = new IntersectionObserver(intersectionCallBack); + } + + lazyImageRef.current && observerRef.current.observe(lazyImageRef.current); + + return () => observerRef.current && observerRef.current.disconnect(); + }, []); + + return ; +}; + +export default LazyImage; diff --git a/frontend/src/components/common/Sticky/index.tsx b/frontend/src/components/common/Sticky/index.tsx new file mode 100644 index 00000000..0f5aef32 --- /dev/null +++ b/frontend/src/components/common/Sticky/index.tsx @@ -0,0 +1,33 @@ +import styles from './style'; +import { css } from '@emotion/react'; +import { useEffect, useRef, useState } from 'react'; + +import useScroll from '@/hooks/useScroll'; + +import theme from '@/styles/theme'; + +interface StickyProps { + children: React.ReactNode; + defaultPosition: number; +} + +const Sticky: React.FC = ({ children, defaultPosition }) => { + const [isActiveSticky, setIsActiveSticky] = useState(false); + + const progressBarRef = useRef(null); + const { scrollPosition } = useScroll(); + + useEffect(() => { + const isActive = progressBarRef.current?.offsetTop! > defaultPosition; + + setIsActiveSticky(isActive); + }, [scrollPosition]); + + return ( +
+ {children} +
+ ); +}; + +export default Sticky; diff --git a/frontend/src/components/common/Sticky/style.ts b/frontend/src/components/common/Sticky/style.ts new file mode 100644 index 00000000..720556cd --- /dev/null +++ b/frontend/src/components/common/Sticky/style.ts @@ -0,0 +1,24 @@ +import { css } from '@emotion/react'; + +import theme from '@/styles/theme'; + +const sticky = (isActiveSticky: boolean) => css` + position: sticky; + top: 0; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + padding-top: 16px; + background-color: ${theme.colors.white}; + z-index: 1; + + box-shadow: ${isActiveSticky ? `2px 2px 2px 0px ${theme.colors.shadow30}` : ''}; +`; + +const styles = { + sticky, +}; + +export default styles; diff --git a/frontend/src/components/host/SectionDetailModal/index.tsx b/frontend/src/components/host/SectionDetailModal/index.tsx index 9eeea7c2..4c165c13 100644 --- a/frontend/src/components/host/SectionDetailModal/index.tsx +++ b/frontend/src/components/host/SectionDetailModal/index.tsx @@ -36,7 +36,7 @@ const SectionDetailModal: React.FC = props => { return ( - +

diff --git a/frontend/src/components/host/SlackUrlModal/index.tsx b/frontend/src/components/host/SlackUrlModal/index.tsx index 06bd501f..b81a4b17 100644 --- a/frontend/src/components/host/SlackUrlModal/index.tsx +++ b/frontend/src/components/host/SlackUrlModal/index.tsx @@ -19,7 +19,7 @@ interface SlackUrlModalProps { const SlackUrlModal: React.FC = ({ jobs }) => { return ( - +
diff --git a/frontend/src/components/host/SpaceDeleteModal/index.tsx b/frontend/src/components/host/SpaceDeleteModal/index.tsx index a9fa9b6c..586b4237 100644 --- a/frontend/src/components/host/SpaceDeleteModal/index.tsx +++ b/frontend/src/components/host/SpaceDeleteModal/index.tsx @@ -23,7 +23,7 @@ const SpaceDeleteModal: React.FC = ({ text, onClick }) => return ( - +
다음 내용을 입력하시면 공간을 삭제 할 수 있습니다. diff --git a/frontend/src/components/user/DetailInfoModal/index.tsx b/frontend/src/components/user/DetailInfoModal/index.tsx index bb939413..daab1264 100644 --- a/frontend/src/components/user/DetailInfoModal/index.tsx +++ b/frontend/src/components/user/DetailInfoModal/index.tsx @@ -13,7 +13,7 @@ export interface DetailInfoModalProps { const DetailInfoModal: React.FC = ({ name, imageUrl, description }) => { return ( - +

{name}

diff --git a/frontend/src/components/user/JobCard/index.tsx b/frontend/src/components/user/JobCard/index.tsx index d49b4259..bdbb3d6a 100644 --- a/frontend/src/components/user/JobCard/index.tsx +++ b/frontend/src/components/user/JobCard/index.tsx @@ -2,7 +2,7 @@ import useJobCard from './useJobCard'; import { ID } from '@/types'; -import checklistImage from '@/assets/checklistImage.svg'; +import checklistImage from '@/assets/checklistImage2.svg'; import styles from './styles'; @@ -18,7 +18,7 @@ const JobCard: React.FC = ({ jobName, jobId }) => {
{jobName} - 체크리스트 + 체크하러 가기
diff --git a/frontend/src/components/user/JobCard/styles.ts b/frontend/src/components/user/JobCard/styles.ts index 6c979132..af477d20 100644 --- a/frontend/src/components/user/JobCard/styles.ts +++ b/frontend/src/components/user/JobCard/styles.ts @@ -7,25 +7,30 @@ const jobCard = css` justify-content: space-around; align-items: center; border-radius: 24px; - width: 320px; - height: 144px; + max-width: 400px; + height: 180px; + width: 80%; margin: 16px; padding: 0 16px; background-color: ${theme.colors.gray100}; box-shadow: 2px 2px 2px 2px ${theme.colors.shadow40}; cursor: pointer; + :hover { - transform: scale(1.01); + transform: scale(1.02); + background-color: ${theme.colors.gray200}; } `; const textWrapper = css` display: flex; flex-direction: column; - font-size: 30px; + font-size: 28px; font-weight: 600; + color: ${theme.colors.black}; + span + span { - margin: 4px 0; + margin: 8px 0; font-size: 16px; font-size: 200; } diff --git a/frontend/src/components/user/NameModal/index.tsx b/frontend/src/components/user/NameModal/index.tsx index 59f472a8..0ab8b4a2 100644 --- a/frontend/src/components/user/NameModal/index.tsx +++ b/frontend/src/components/user/NameModal/index.tsx @@ -23,7 +23,7 @@ const NameModal: React.FC = ({ title, detail, placeholder, butto return ( - +

{title}

{detail} diff --git a/frontend/src/components/user/SectionCard/index.tsx b/frontend/src/components/user/SectionCard/index.tsx new file mode 100644 index 00000000..66b25f0b --- /dev/null +++ b/frontend/src/components/user/SectionCard/index.tsx @@ -0,0 +1,79 @@ +import SectionInfoPreview from '../SectionInfoPreview'; +import { RiInformationLine } from 'react-icons/ri'; + +import Button from '@/components/common/Button'; +import CheckBox from '@/components/common/Checkbox'; +import DetailInfoModal from '@/components/user/DetailInfoModal'; + +import useModal from '@/hooks/useModal'; + +import apis from '@/apis'; + +import { ID, SectionType, TaskType } from '@/types'; + +import styles from './styles'; + +type SectionCardProps = { + section: SectionType; + sectionsAllCheckMap: any; + onClickSectionDetail: any; + onClickSectionAllCheck: any; +}; + +const SectionCard: React.FC = ({ + section, + sectionsAllCheckMap, + onClickSectionDetail, + onClickSectionAllCheck, +}) => { + const { openModal } = useModal(); + + const onClickCheckBox = (e: React.MouseEvent | React.ChangeEvent, id: ID) => { + e.preventDefault(); + apis.postCheckTask(id); + }; + + const onClickTaskDetail = (task: TaskType) => { + openModal(); + }; + + return ( +
+
+

{section.name}

+
+ {!sectionsAllCheckMap.get(`${section.id}`) && ( + + )} + {(section.imageUrl || section.description) && ( + onClickSectionDetail(section)} /> + )} +
+
+ + {section.tasks.map((task, index) => ( +
+ onClickCheckBox(e, task.id)} + checked={task.checked || false} + id={JSON.stringify(task.id)} + /> +
+ {task.name} + {(task.imageUrl || task.description) && ( + onClickTaskDetail(task)} /> + )} +
+
+ ))} +
+ ); +}; + +export default SectionCard; diff --git a/frontend/src/components/user/SectionCard/styles.ts b/frontend/src/components/user/SectionCard/styles.ts new file mode 100644 index 00000000..0587c383 --- /dev/null +++ b/frontend/src/components/user/SectionCard/styles.ts @@ -0,0 +1,79 @@ +import { css } from '@emotion/react'; + +import theme from '@/styles/theme'; + +const sectionCard = css` + display: flex; + flex-direction: column; + max-width: 354px; + width: 100%; + margin: 16px 0; + padding: 16px; + border: 1px solid ${theme.colors.shadow30}; + border-radius: 16px; + box-shadow: 2px 2px 2px 0px ${theme.colors.shadow30}; + background-color: ${theme.colors.white}; +`; + +const locationHeader = css` + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 8px; + min-height: 48px; +`; + +const locationName = css` + font-size: 24px; + font-weight: 600; + margin: 0; +`; + +const locationHeaderRightItems = css` + display: flex; + gap: 10px; +`; + +const sectionAllCheckButton = css` + width: 40px; + height: 40px; + margin: 0; + padding: 0; + box-shadow: 2px 2px 2px 0px ${theme.colors.shadow30}; +`; + +const task = css` + display: flex; + gap: 16px; + padding: 12px 0; +`; + +const textWrapper = css` + display: flex; + align-items: center; + justify-content: flex-start; + width: 100%; + + position: relative; + + font-weight: 500; +`; + +const icon = css` + color: ${theme.colors.gray500}; + margin-left: 16px; + cursor: pointer; +`; + +const styles = { + sectionCard, + task, + icon, + textWrapper, + locationHeader, + locationName, + locationHeaderRightItems, + sectionAllCheckButton, +}; + +export default styles; diff --git a/frontend/src/components/user/SectionInfoPreview/index.tsx b/frontend/src/components/user/SectionInfoPreview/index.tsx index 3f43566f..638e34c4 100644 --- a/frontend/src/components/user/SectionInfoPreview/index.tsx +++ b/frontend/src/components/user/SectionInfoPreview/index.tsx @@ -1,6 +1,6 @@ import { FaMapMarkedAlt } from 'react-icons/fa'; -import useLazyImage from '@/hooks/useLazyLoading'; +import LazyImage from '@/components/common/LazyImage'; import styles from './styles'; @@ -10,12 +10,10 @@ interface SectionInfoPreviewProps { } const SectionInfoPreview: React.FC = ({ imageUrl, onClick }) => { - const { isLoaded, targetRef: imageRef } = useLazyImage(); - return (
- +
diff --git a/frontend/src/components/user/SpaceCard/styles.ts b/frontend/src/components/user/SpaceCard/styles.ts index 97d78de5..e6b7cd9c 100644 --- a/frontend/src/components/user/SpaceCard/styles.ts +++ b/frontend/src/components/user/SpaceCard/styles.ts @@ -6,8 +6,9 @@ const spaceCard = (imageUrl: string) => css` display: flex; justify-content: flex-start; align-items: flex-end; - width: 90%; - height: 25vh; + max-width: 400px; + height: 180px; + width: 80%; margin: 16px; padding: 24px; box-shadow: 2px 2px 2px 2px ${theme.colors.shadow40}; @@ -17,13 +18,13 @@ const spaceCard = (imageUrl: string) => css` cursor: pointer; :hover { - transform: scale(1.01); + transform: scale(1.02); } `; const title = css` color: ${theme.colors.white}; - font-size: 36px; + font-size: 28px; text-shadow: -1px -1px 6px #000, 1px -1px 6px #000, -1px 1px 6px #000, 1px 1px 6px #000; background-image: linear-gradient(transparent 90%, ${theme.colors.primary} 10%); `; diff --git a/frontend/src/components/user/TaskCard/index.tsx b/frontend/src/components/user/TaskCard/index.tsx deleted file mode 100644 index da660ff8..00000000 --- a/frontend/src/components/user/TaskCard/index.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { RiInformationLine } from 'react-icons/ri'; - -import CheckBox from '@/components/common/Checkbox'; -import DetailInfoModal from '@/components/user/DetailInfoModal'; - -import useModal from '@/hooks/useModal'; - -import apis from '@/apis'; - -import { ID, TaskType } from '@/types'; - -import styles from './styles'; - -type TaskCardProps = { - tasks: TaskType[]; -}; - -const TaskCard: React.FC = ({ tasks }) => { - const { openModal } = useModal(); - - const onClickCheckBox = (e: React.MouseEvent | React.ChangeEvent, id: ID) => { - e.preventDefault(); - apis.postCheckTask(id); - }; - - const onClickTaskDetail = (task: TaskType) => { - openModal(); - }; - - return ( -
- {tasks.map((task, id) => ( -
- onClickCheckBox(e, task.id)} - checked={task.checked || false} - id={JSON.stringify(task.id)} - /> -
- {task.name} - {(task.imageUrl || task.description) && ( - onClickTaskDetail(task)} /> - )} -
-
- ))} -
- ); -}; - -export default TaskCard; diff --git a/frontend/src/components/user/TaskCard/styles.ts b/frontend/src/components/user/TaskCard/styles.ts deleted file mode 100644 index 5ca1d6db..00000000 --- a/frontend/src/components/user/TaskCard/styles.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { css } from '@emotion/react'; - -import theme from '@/styles/theme'; - -const taskCard = css` - display: flex; - flex-direction: column; - width: 320px; - padding: 16px; -`; - -const textWrapper = css` - display: flex; - align-items: center; - - span { - font-weight: 500; - display: flex; - justify-content: center; - align-items: center; - } -`; - -const task = css` - display: flex; - gap: 16px; - margin: 18px 0; -`; - -const icon = css` - color: ${theme.colors.gray500}; - margin-left: 16px; - cursor: pointer; -`; - -const styles = { taskCard, task, icon, textWrapper }; - -export default styles; diff --git a/frontend/src/hooks/useScroll.ts b/frontend/src/hooks/useScroll.ts index 31ac0a18..5e192d98 100644 --- a/frontend/src/hooks/useScroll.ts +++ b/frontend/src/hooks/useScroll.ts @@ -3,12 +3,14 @@ import { useEffect, useState } from 'react'; const useScroll = () => { const [scrollPosition, setScrollPosition] = useState(0); - const updateScroll = () => { - setScrollPosition((scrollY / innerHeight) * 100); + const updateScroll = (e: any) => { + setScrollPosition((scrollY / innerHeight) * 100 || (e.target.scrollTop / innerHeight) * 100); }; useEffect(() => { - window.addEventListener('scroll', updateScroll); + window.addEventListener('scroll', updateScroll, { capture: true, passive: true }); + + return () => document.removeEventListener('scroll', updateScroll); }, []); return { scrollPosition }; diff --git a/frontend/src/hooks/useTransitionSelect.ts b/frontend/src/hooks/useTransitionSelect.ts index 81d442de..aa8413b6 100644 --- a/frontend/src/hooks/useTransitionSelect.ts +++ b/frontend/src/hooks/useTransitionSelect.ts @@ -1,33 +1,41 @@ import { useLocation } from 'react-router-dom'; +const getPageByPath = (pathname: string) => { + const pathList = pathname.split('/'); + + if (pathList.length === 4 && pathList[3] === 'pwd') return 'passwordPage'; + if (pathList.length === 4 && pathList[3] === 'spaces') return 'spaceListPage'; + if (pathList.length === 5) return 'jobListPage'; + if (pathList.length === 6) return 'taskListPage'; + + return ''; +}; + const useTransitionSelect = () => { const location = useLocation(); - const previousPath = sessionStorage.getItem('path'); - const pathLength = location.pathname.split('/').length; - const previousPathLength = previousPath?.split('/').length; - const isPwdPage = location.pathname.split('/')[3] == 'pwd'; - const isPreviousPwdPage = previousPath?.split('/')[3] == 'pwd'; + const previousPage = getPageByPath(previousPath || ''); + const currentPage = getPageByPath(location.pathname); sessionStorage.setItem('path', location.pathname); - if (pathLength === 4 && isPwdPage) { + if (currentPage === 'passwordPage') { return ''; } - if (pathLength === 4) { - if (isPreviousPwdPage) return ''; + if (currentPage === 'spaceListPage') { + if (previousPage === 'passwordPage') return ''; return 'slide-left'; } - if (pathLength === 5) { - if (previousPathLength === 4 && !isPreviousPwdPage) { + if (currentPage === 'jobListPage') { + if (previousPage === 'spaceListPage') { return 'slide-right'; } - if (previousPathLength === 6) { + if (previousPage === 'taskListPage') { return 'left'; } } - if (pathLength === 6) { + if (currentPage === 'taskListPage') { return 'right'; } diff --git a/frontend/src/layouts/HostLayout/styles.ts b/frontend/src/layouts/HostLayout/styles.ts index ae453fdb..e2c1bda4 100644 --- a/frontend/src/layouts/HostLayout/styles.ts +++ b/frontend/src/layouts/HostLayout/styles.ts @@ -8,7 +8,7 @@ const layout = (isManagePath: boolean) => css` width: 100vw; min-height: 100vh; height: fit-content; - background-color: ${theme.colors.background}; + background-color: ${theme.colors.white}; padding-left: ${isManagePath ? '14em' : 0}; @media screen and (max-width: 1024px) { diff --git a/frontend/src/layouts/UserLayout/index.tsx b/frontend/src/layouts/UserLayout/index.tsx index 62697bee..8f60fbb0 100644 --- a/frontend/src/layouts/UserLayout/index.tsx +++ b/frontend/src/layouts/UserLayout/index.tsx @@ -27,12 +27,12 @@ const UserLayout: React.FC = () => { return ( - }> -
- +
+ + }> -
- + +
); diff --git a/frontend/src/layouts/UserLayout/styles.ts b/frontend/src/layouts/UserLayout/styles.ts index 1f752698..832c437f 100644 --- a/frontend/src/layouts/UserLayout/styles.ts +++ b/frontend/src/layouts/UserLayout/styles.ts @@ -11,6 +11,10 @@ const layout = css` background-color: ${theme.colors.white}; `; -const styles = { layout }; +const fallback = css` + opacity: 1; +`; + +const styles = { layout, fallback }; export default styles; diff --git a/frontend/src/pages/user/JobList/index.tsx b/frontend/src/pages/user/JobList/index.tsx index 558fa219..9a5850d4 100644 --- a/frontend/src/pages/user/JobList/index.tsx +++ b/frontend/src/pages/user/JobList/index.tsx @@ -3,6 +3,8 @@ import { IoIosArrowBack } from 'react-icons/io'; import JobCard from '@/components/user/JobCard'; +import DEFAULT_IMAGE from '@/assets/defaultSpaceImage.webp'; + import styles from './styles'; const JobList: React.FC = () => { @@ -10,11 +12,10 @@ const JobList: React.FC = () => { return (
-
- +
+ {spaceData?.name}
- 체크하실 업무를 선택해주세요. {jobsData?.jobs.length === 0 ? (
관리자가 생성한 업무가 없어요
) : ( diff --git a/frontend/src/pages/user/JobList/styles.ts b/frontend/src/pages/user/JobList/styles.ts index 90b1afaa..6ccf5d8a 100644 --- a/frontend/src/pages/user/JobList/styles.ts +++ b/frontend/src/pages/user/JobList/styles.ts @@ -26,6 +26,7 @@ const coverText = css` text-shadow: 0 0 4px ${theme.colors.black}; background-image: linear-gradient(transparent 90%, ${theme.colors.primary} 10%); width: fit-content; + margin-left: 16px; `; const text = css` diff --git a/frontend/src/pages/user/Password/index.tsx b/frontend/src/pages/user/Password/index.tsx index d2ef3907..733805b7 100644 --- a/frontend/src/pages/user/Password/index.tsx +++ b/frontend/src/pages/user/Password/index.tsx @@ -26,6 +26,7 @@ const Password: React.FC = () => { { return (
공간 체크 - 사용하실 공간을 선택해주세요. + 현재 머무르는 공간을 클릭해주세요. {spaceData?.spaces.length === 0 ? (
관리자가 생성한 공간 없어요
) : ( spaceData?.spaces.map(space => ( - + )) )} {} diff --git a/frontend/src/pages/user/SpaceList/styles.ts b/frontend/src/pages/user/SpaceList/styles.ts index 17747515..93324d75 100644 --- a/frontend/src/pages/user/SpaceList/styles.ts +++ b/frontend/src/pages/user/SpaceList/styles.ts @@ -10,7 +10,7 @@ const layout = css` `; const logo = css` - width: 248px; + width: 264px; margin: 32px 0; `; diff --git a/frontend/src/pages/user/TaskList/index.tsx b/frontend/src/pages/user/TaskList/index.tsx index 10efe0f6..b8a7c10c 100644 --- a/frontend/src/pages/user/TaskList/index.tsx +++ b/frontend/src/pages/user/TaskList/index.tsx @@ -3,8 +3,10 @@ import React from 'react'; import { IoIosArrowBack } from 'react-icons/io'; import Button from '@/components/common/Button'; -import SectionInfoPreview from '@/components/user/SectionInfoPreview'; -import TaskCard from '@/components/user/TaskCard'; +import Sticky from '@/components/common/Sticky'; +import SectionCard from '@/components/user/SectionCard'; + +import DEFAULT_IMAGE from '@/assets/defaultSpaceImage.webp'; import styles from './styles'; @@ -22,57 +24,43 @@ const TaskList: React.FC = () => { sectionsData, onClickSectionDetail, onClickSectionAllCheck, - progressBarRef, - isActiveSticky, } = useTaskList(); return (
-
+
-
-
-

{spaceData?.name}

-

{locationState?.jobName}

+
+
+
+

{spaceData?.name}

+

{locationState?.jobName}

+
-
+
{`${checkedCount}/${totalCount}`}
-
-
-
- {sectionsData?.sections.map(section => ( -
-
-

{section.name}

-
- {!sectionsAllCheckMap.get(`${section.id}`) && ( - - )} - {(section.imageUrl || section.description) && ( - onClickSectionDetail(section)} /> - )} -
-
- -
- ))} - -
-
+
+ +
+ {sectionsData?.sections.map(section => ( + + ))} + +
); }; diff --git a/frontend/src/pages/user/TaskList/styles.ts b/frontend/src/pages/user/TaskList/styles.ts index 5563dff7..df6acb1f 100644 --- a/frontend/src/pages/user/TaskList/styles.ts +++ b/frontend/src/pages/user/TaskList/styles.ts @@ -8,54 +8,36 @@ const layout = css` width: 100%; align-items: center; font-size: 16px; - padding: 0 0 32px 0; + padding-bottom: 32px; `; const contents = css` display: flex; flex-direction: column; - width: 100%; + width: 80%; align-items: center; margin-top: 16px; `; -const location = css` - margin: 16px 0; - padding: 24px 16px; - border: 1px solid ${theme.colors.shadow30}; - border-radius: 24px; - box-shadow: 2px 2px 2px 0px ${theme.colors.shadow30}; -`; +const header = css` + position: relative; + width: 100%; -const locationHeader = css` display: flex; - justify-content: space-between; + justify-content: space-evenly; align-items: center; - padding: 0 8px; - min-height: 48px; -`; - -const locationName = css` - font-size: 24px; - font-weight: 600; - margin: 0; -`; - -const locationHeaderRightItems = css` - display: flex; - gap: 10px; + padding: 4em 0 3em 0; `; -const header = css` - position: relative; +const headerInfo = css` + max-width: 420px; width: 100%; display: flex; justify-content: space-evenly; align-items: center; - padding: 4em 0 3em 0; `; -const thumbnail = (imageUrl: string | undefined) => css` +const thumbnail = (imageUrl: string) => css` width: 120px; height: 120px; background-image: url(${imageUrl}); @@ -63,29 +45,30 @@ const thumbnail = (imageUrl: string | undefined) => css` background-repeat: no-repeat; background-size: cover; border-radius: 40%; + box-shadow: 2px 2px 6px 0px ${theme.colors.shadow60}; `; const infoWrapper = css` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + p { margin: 0; } - p:nth-of-type(1) { - font-weight: 500; - font-size: 1em; - } - p:nth-of-type(2) { - font-size: 1.5em; + font-size: 1.8em; font-weight: bold; - margin-top: 0.5em; + margin-top: 0.3em; } `; -const arrowBackIconWrapper = css` +const arrowBackIcon = css` position: absolute; - top: 20px; - left: 20px; + top: 16px; + left: 16px; svg { color: ${theme.colors.black}; @@ -93,26 +76,13 @@ const arrowBackIconWrapper = css` } `; -const progressBarWrapperSticky = (isSticked: boolean | undefined) => css` - position: sticky; - top: 0; - padding-top: 16px; - width: 100%; - height: 100%; - display: flex; - justify-content: center; - align-items: center; - background-color: ${theme.colors.white}; - z-index: 1; - - box-shadow: ${isSticked ? `2px 2px 2px 0px ${theme.colors.shadow30}` : ''}; -`; - const progressBarWrapper = css` + background-color: ${theme.colors.white}; box-shadow: 2px 2px 4px 0px ${theme.colors.shadow40}; border: 1px solid ${theme.colors.green}; border-radius: 4px; height: 30px; + max-width: 380px; width: 80%; position: relative; margin-bottom: 16px; @@ -175,38 +145,18 @@ const button = (isAllChecked: boolean) => background: ${isAllChecked ? theme.colors.primary : theme.colors.gray400}; `; -const form = css` - display: flex; - flex-direction: column; - align-items: center; -`; - -const sectionAllCheckButton = css` - width: 40px; - height: 40px; - margin: 0; - padding: 0; - box-shadow: 2px 2px 2px 0px ${theme.colors.shadow30}; -`; - const styles = { layout, - contents, - location, - locationHeader, - locationName, - locationHeaderRightItems, - arrowBackIconWrapper, header, + headerInfo, thumbnail, + arrowBackIcon, infoWrapper, - progressBarWrapperSticky, progressBarWrapper, progressBar, percentText, + contents, button, - sectionAllCheckButton, - form, }; export default styles; diff --git a/frontend/src/pages/user/TaskList/useTaskList.tsx b/frontend/src/pages/user/TaskList/useTaskList.tsx index 811ffccd..09531072 100644 --- a/frontend/src/pages/user/TaskList/useTaskList.tsx +++ b/frontend/src/pages/user/TaskList/useTaskList.tsx @@ -8,6 +8,7 @@ import NameModal from '@/components/user/NameModal'; import useGoPreviousPage from '@/hooks/useGoPreviousPage'; import useModal from '@/hooks/useModal'; +import useScroll from '@/hooks/useScroll'; import useSectionCheck from '@/hooks/useSectionCheck'; import useToast from '@/hooks/useToast'; @@ -17,7 +18,6 @@ import { ID, SectionType } from '@/types'; import { ApiTaskData } from '@/types/apis'; const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:8080'; -const PROGRESS_BAR_DEFAULT_POSITION = 232; const useTaskList = () => { const navigate = useNavigate(); @@ -32,10 +32,6 @@ const useTaskList = () => { const { goPreviousPage } = useGoPreviousPage(); - const progressBarRef = useRef(null); - - const [isActiveSticky, setIsActiveSticky] = useState(false); - const [sectionsData, setSectionsData] = useState({ sections: [ { @@ -77,12 +73,6 @@ const useTaskList = () => { postSectionAllCheck(sectionId); }; - useEffect(() => { - const isActive = progressBarRef.current?.offsetTop! > PROGRESS_BAR_DEFAULT_POSITION; - - setIsActiveSticky(isActive); - }, [progressBarRef.current?.offsetTop]); - useEffect(() => { const tokenKey = sessionStorage.getItem('tokenKey'); if (!tokenKey) return; @@ -109,6 +99,8 @@ const useTaskList = () => { navigate(`/enter/${hostId}/spaces/${spaceId}`); openToast('SUCCESS', '해당 체크리스트는 제출되었습니다.'); }); + + return () => sse.close(); }, []); return { @@ -124,8 +116,6 @@ const useTaskList = () => { sectionsData, onClickSectionDetail, onClickSectionAllCheck, - progressBarRef, - isActiveSticky, }; }; diff --git a/frontend/src/styles/animation.ts b/frontend/src/styles/animation.ts index 08e8e88b..827af4bc 100644 --- a/frontend/src/styles/animation.ts +++ b/frontend/src/styles/animation.ts @@ -78,6 +78,28 @@ const moveUp = keyframes` } `; +const scaleUp = keyframes` + from { + transform: scale(0.7); + } + to { + transform: scale(1); + } +`; + +const scaleDown = keyframes` + from { + opacity: 1; + transform: scale(1); + + } + to { + opacity: 0; + transform: scale(0.7); + + } +`; + const customMoveUp = (y: string) => keyframes` 0% { opacity: 0; @@ -149,6 +171,8 @@ const animation = { moveRight, moveLeft, wave, + scaleUp, + scaleDown, }; export default animation; diff --git a/frontend/src/styles/global.ts b/frontend/src/styles/global.ts index b8aa8afa..bb836146 100644 --- a/frontend/src/styles/global.ts +++ b/frontend/src/styles/global.ts @@ -19,14 +19,8 @@ const globalStyle = css` color: ${theme.colors.black}; } - // 모달이 웹에서도 모바일 환경 처럼 보일 수 있도록 center 처리 body { margin: 0; - display: flex; - justify-content: center; - overflow: scroll; - overflow-x: hidden; - background-color: ${theme.colors.background}; } button { @@ -39,7 +33,7 @@ const globalStyle = css` justify-content: center; width: 100vw; min-height: 100vh; - background-color: ${theme.colors.gray350}; + background-color: ${theme.colors.white}; } #modal { diff --git a/frontend/src/styles/transitions.ts b/frontend/src/styles/transitions.ts index 42ccd8af..9f2a0d50 100644 --- a/frontend/src/styles/transitions.ts +++ b/frontend/src/styles/transitions.ts @@ -1,8 +1,9 @@ import { css } from '@emotion/react'; +import animation from '@/styles/animation'; + const transitions = css` .transitions-group { - max-width: 414px; width: 100vw; overflow-x: hidden; position: relative; @@ -67,40 +68,56 @@ const transitions = css` .right-enter.right-enter-active { z-index: 1; transform: translate3d(0, 0, 0); - transition: all 500ms; + transition: transform 600ms; } - .right-exit { - z-index: 1; + z-index: 0; transform: translate3d(0, 0, 0); } - .right-exit.right-exit-active { - z-index: 1; + z-index: 0; transform: translate3d(0, 0, 0); - transition: all 700ms; + transition: transform 600ms; } // left .left-enter { - z-index: 1; + z-index: 0; transform: translate3d(0, 0, 0); } .left-enter.left-enter-active { - z-index: 1; + z-index: 0; transform: translate3d(0, 0, 0); - transition: all 500ms; + transition: transform 600ms; } - .left-exit { z-index: 1; transform: translate3d(0, 0, 0); } - .left-exit.left-exit-active { z-index: 1; transform: translate3d(100%, 0, 0); - transition: all 700ms; + transition: transform 600ms; + } + + // image scale up + .image-scale-up-enter.image-scale-up-enter-active { + z-index: 1; + animation: ${animation.scaleUp} 500ms; + } + + .image-scale-down-exit { + z-index: 0; + } + + // image scale down + .image-scale-up-enter { + z-index: 0; + } + + .image-scale-down-exit.image-scale-down-exit-active { + z-index: 1; + animation: ${animation.scaleDown} 500ms; } `; diff --git a/frontend/src/types/global.d.ts b/frontend/src/types/global.d.ts index da7fb7f6..f3cc6b14 100644 --- a/frontend/src/types/global.d.ts +++ b/frontend/src/types/global.d.ts @@ -3,3 +3,4 @@ declare module '*.jpg'; declare module '*.jpeg'; declare module '*.svg'; declare module '*.gif'; +declare module '*.webp'; diff --git a/frontend/webpack.config.js b/frontend/webpack.config.js index 439f5361..a015689d 100644 --- a/frontend/webpack.config.js +++ b/frontend/webpack.config.js @@ -44,7 +44,7 @@ const config = { }, }, { - test: /\.(png|jpe?g|gif|svg)$/, + test: /\.(png|jpe?g|gif|svg|webp)$/, use: { loader: 'file-loader', options: { From 263d875740a50126919cd61a0f14e1c0834a9a1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=88=98=ED=98=84?= <63030569+awesomeo184@users.noreply.github.com> Date: Wed, 14 Sep 2022 14:05:00 +0900 Subject: [PATCH 03/19] =?UTF-8?q?[BE]=20SSE=20timeout=EC=8B=9C=20=EB=B0=9C?= =?UTF-8?q?=EC=83=9D=ED=95=98=EB=8A=94=20=EC=98=88=EC=99=B8=20=ED=9A=8C?= =?UTF-8?q?=ED=94=BC=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20(#546)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix: SSE timeout시 발생하는 예외 회피 코드 추가 --- .../woowacourse/gongcheck/exception/ControllerAdvice.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/backend/src/main/java/com/woowacourse/gongcheck/exception/ControllerAdvice.java b/backend/src/main/java/com/woowacourse/gongcheck/exception/ControllerAdvice.java index 32987e8a..72157d6a 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/exception/ControllerAdvice.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/exception/ControllerAdvice.java @@ -11,6 +11,7 @@ import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.async.AsyncRequestTimeoutException; @RestControllerAdvice @Slf4j @@ -51,6 +52,11 @@ public ResponseEntity handleInfrastructureException(final CustomE return ResponseEntity.internalServerError().body(ErrorResponse.from(e.getErrorCode())); } + @ExceptionHandler(AsyncRequestTimeoutException.class) + public void escapeFromAsyncRequestTimeoutException() { + // do nothing + } + @ExceptionHandler(Exception.class) public ResponseEntity handleInternalServerError(final Exception e) { log.error("Stack Trace : {}", extractStackTrace(e)); From 5efb60a0d17a6c898769557d9ddd4e6b3f99d896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=88=98=ED=98=84?= <63030569+awesomeo184@users.noreply.github.com> Date: Wed, 14 Sep 2022 15:21:43 +0900 Subject: [PATCH 04/19] =?UTF-8?q?[BE]=20HostServiceTest=EC=99=80=20SpaceSe?= =?UTF-8?q?rviceTest=EB=A5=BC=20=EA=B0=9C=EC=84=A0=ED=95=9C=EB=8B=A4=20(#5?= =?UTF-8?q?21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/application/SpaceService.java | 34 +- .../core/application/HostServiceTest.java | 61 ++-- .../core/application/SpaceServiceTest.java | 342 +++++++++--------- 3 files changed, 218 insertions(+), 219 deletions(-) diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/application/SpaceService.java b/backend/src/main/java/com/woowacourse/gongcheck/core/application/SpaceService.java index 8f625352..0017b94e 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/core/application/SpaceService.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/application/SpaceService.java @@ -46,6 +46,12 @@ public SpaceService(final HostRepository hostRepository, final SpaceRepository s this.runningTaskRepository = runningTaskRepository; } + public SpaceResponse findSpace(final Long hostId, final Long spaceId) { + Host host = hostRepository.getById(hostId); + Space space = spaceRepository.getByHostAndId(host, spaceId); + return SpaceResponse.from(space); + } + public SpacesResponse findSpaces(final Long hostId) { Host host = hostRepository.getById(hostId); List spaces = spaceRepository.findAllByHost(host); @@ -67,23 +73,6 @@ public Long createSpace(final Long hostId, final SpaceCreateRequest request) { .getId(); } - public SpaceResponse findSpace(final Long hostId, final Long spaceId) { - Host host = hostRepository.getById(hostId); - Space space = spaceRepository.getByHostAndId(host, spaceId); - return SpaceResponse.from(space); - } - - @Transactional - public void changeSpace(final Long hostId, final Long spaceId, final SpaceChangeRequest request) { - Host host = hostRepository.getById(hostId); - Space space = spaceRepository.getByHostAndId(host, spaceId); - Name changeName = new Name(request.getName()); - checkDuplicateSpaceName(changeName, host, space); - - space.changeName(changeName); - space.changeImageUrl(request.getImageUrl()); - } - @Transactional public void removeSpace(final Long hostId, final Long spaceId) { Host host = hostRepository.getById(hostId); @@ -101,6 +90,17 @@ public void removeSpace(final Long hostId, final Long spaceId) { spaceRepository.deleteById(spaceId); } + @Transactional + public void changeSpace(final Long hostId, final Long spaceId, final SpaceChangeRequest request) { + Host host = hostRepository.getById(hostId); + Space space = spaceRepository.getByHostAndId(host, spaceId); + Name changeName = new Name(request.getName()); + checkDuplicateSpaceName(changeName, host, space); + + space.changeName(changeName); + space.changeImageUrl(request.getImageUrl()); + } + private void checkDuplicateSpaceName(final Name spaceName, final Host host) { if (spaceRepository.existsByHostAndName(host, spaceName)) { String message = String.format("이미 존재하는 이름입니다. hostId = %d, spaceName = %s", host.getId(), diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/application/HostServiceTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/application/HostServiceTest.java index b5276855..2308edec 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/core/application/HostServiceTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/application/HostServiceTest.java @@ -10,7 +10,6 @@ import com.woowacourse.gongcheck.core.domain.host.Host; import com.woowacourse.gongcheck.core.presentation.request.SpacePasswordChangeRequest; import com.woowacourse.gongcheck.exception.NotFoundException; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; @@ -19,7 +18,7 @@ import org.springframework.beans.factory.annotation.Autowired; @ApplicationTest -@DisplayName("HostService 클래스") +@DisplayName("HostService 클래스의") @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) class HostServiceTest { @@ -36,48 +35,35 @@ class HostServiceTest { class changeSpacePassword_메소드는 { @Nested - class 존재하는_Host의_id와_수정할_패스워드를_받는_경우 { + class 존재하는_HostId와_수정할_패스워드를_입력받는_경우 { - private static final String ORIGIN_PASSWORD = "1234"; private static final String CHANGING_PASSWORD = "4567"; - private static final long GITHUB_ID = 1234L; - private SpacePasswordChangeRequest spacePasswordChangeRequest; - private Long hostId; - - @BeforeEach - void setUp() { - spacePasswordChangeRequest = new SpacePasswordChangeRequest(CHANGING_PASSWORD); - hostId = repository.save(Host_생성(ORIGIN_PASSWORD, GITHUB_ID)) - .getId(); - } + private final Host host = repository.save(Host_생성("1234", 1L)); + private final SpacePasswordChangeRequest spacePasswordChangeRequest = new SpacePasswordChangeRequest( + CHANGING_PASSWORD); @Test void 패스워드를_수정한다() { - hostService.changeSpacePassword(hostId, spacePasswordChangeRequest); - Host actual = repository.getById(Host.class, hostId); + hostService.changeSpacePassword(host.getId(), spacePasswordChangeRequest); + Host actual = repository.getById(Host.class, host.getId()); assertThat(actual.getSpacePassword().getValue()).isEqualTo(CHANGING_PASSWORD); } } @Nested - class 존재하지_않는_Host의_id를_받는_경우 { + class 존재하지_않는_HostId를_입력받는_경우 { private static final String CHANGING_PASSWORD = "4567"; + private static final long NON_EXIST_HOST_ID = 0L; - private SpacePasswordChangeRequest spacePasswordChangeRequest; - private Long hostId; - - @BeforeEach - void setUp() { - spacePasswordChangeRequest = new SpacePasswordChangeRequest(CHANGING_PASSWORD); - hostId = 0L; - } + private final SpacePasswordChangeRequest spacePasswordChangeRequest = new SpacePasswordChangeRequest( + CHANGING_PASSWORD); @Test void 예외를_발생시킨다() { - assertThatThrownBy(() -> hostService.changeSpacePassword(hostId, spacePasswordChangeRequest)) + assertThatThrownBy(() -> hostService.changeSpacePassword(NON_EXIST_HOST_ID, spacePasswordChangeRequest)) .isInstanceOf(NotFoundException.class) .hasMessageContaining("존재하지 않는 호스트입니다."); } @@ -88,31 +74,26 @@ void setUp() { class createEntranceCode_메소드는 { @Nested - class 존재하는_Host의_id를_받는_경우 { + class 존재하는_HostId를_입력받는_경우 { - private Long hostId; - private String expected; - - @BeforeEach - void setUp() { - hostId = repository.save(Host_생성("1234", 1111L)) - .getId(); - expected = entranceCodeProvider.createEntranceCode(hostId); - } + private final Host host = repository.save(Host_생성("1234", 1L)); + private final String expected = entranceCodeProvider.createEntranceCode(host.getId()); @Test - void 입장코드를_반환한다() { - String actual = hostService.createEntranceCode(hostId); + void entranceCode를_반환한다() { + String actual = hostService.createEntranceCode(host.getId()); assertThat(actual).isEqualTo(expected); } } @Nested - class 존재하지_않는_Host의_id를_받는_경우 { + class 존재하지_않는_HostId를_입력받는_경우 { + + private static final long NON_EXIST_HOST_ID = 0L; @Test void 예외를_발생시킨다() { - assertThatThrownBy(() -> hostService.createEntranceCode(0L)) + assertThatThrownBy(() -> hostService.createEntranceCode(NON_EXIST_HOST_ID)) .isInstanceOf(NotFoundException.class) .hasMessageContaining("존재하지 않는 호스트입니다."); } diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/application/SpaceServiceTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/application/SpaceServiceTest.java index 08fa625d..2e8c108b 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/core/application/SpaceServiceTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/application/SpaceServiceTest.java @@ -18,13 +18,16 @@ import com.woowacourse.gongcheck.core.domain.job.Job; import com.woowacourse.gongcheck.core.domain.section.Section; import com.woowacourse.gongcheck.core.domain.space.Space; +import com.woowacourse.gongcheck.core.domain.space.SpaceRepository; import com.woowacourse.gongcheck.core.domain.task.RunningTask; import com.woowacourse.gongcheck.core.domain.task.Task; +import com.woowacourse.gongcheck.core.domain.vo.Name; +import com.woowacourse.gongcheck.core.presentation.request.SpaceChangeRequest; import com.woowacourse.gongcheck.core.presentation.request.SpaceCreateRequest; import com.woowacourse.gongcheck.exception.BusinessException; import com.woowacourse.gongcheck.exception.NotFoundException; +import java.time.LocalDateTime; import java.util.List; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; @@ -43,37 +46,101 @@ class SpaceServiceTest { @Autowired private SupportRepository repository; + @Autowired + private SpaceRepository spaceRepository; + @Nested - class findSpaces_메서드는 { + class findSpace_메서드는 { + + @Nested + class 입력받은_Host가_입력받은_Space를_소유하고_있는_경우 { + + private static final String SPACE_NAME = "잠실 캠퍼스"; + + private final Host host = repository.save(Host_생성("1234", 1L)); + private final Space space = repository.save(Space_생성(host, SPACE_NAME)); + + @Test + void Space_응답을_반환한다() { + SpaceResponse actual = spaceService.findSpace(host.getId(), space.getId()); + + assertAll( + () -> assertThat(actual.getId()).isEqualTo(space.getId()), + () -> assertThat(actual.getName()).isEqualTo(SPACE_NAME) + ); + } + } @Nested - class 존재하지_않는_Host_id를_입력받는_경우 { + class 입력받은_Host가_입력받은_Space를_소유하고_있지_않은_경우 { + + private final Host host = repository.save(Host_생성("1234", 1L)); + private final Host anotherHost = repository.save(Host_생성("1234", 2L)); + private final Space space = repository.save(Space_생성(anotherHost, "잠실 캠퍼스")); @Test void 예외를_발생시킨다() { - assertThatThrownBy(() -> spaceService.findSpaces(0L)) + assertThatThrownBy(() -> spaceService.findSpace(host.getId(), space.getId())) + .isInstanceOf(NotFoundException.class) + .hasMessageContaining("존재하지 않는 공간입니다."); + } + } + + @Nested + class 존재하지_않는_Host_id를_입력받은_경우 { + + private static final long NON_EXIST_HOST_ID = 0L; + + private final Host host = repository.save(Host_생성("1234", 1L)); + private final Space dummySpace = repository.save(Space_생성(host, "잠실 캠퍼스")); + + @Test + void 예외를_발생시킨다() { + assertThatThrownBy(() -> spaceService.findSpace(NON_EXIST_HOST_ID, dummySpace.getId())) .isInstanceOf(NotFoundException.class) .hasMessageContaining("존재하지 않는 호스트입니다."); } } @Nested - class 존재하는_Host의_id를_입력받은_경우 { + class 존재하지_않는_Space_id를_입력받은_경우 { + + private static final long NON_EXIST_SPACE_ID = 0L; - private Host host; - private SpacesResponse expected; + private final Host host = repository.save(Host_생성("1234", 1L)); - @BeforeEach - void setUp() { - host = repository.save(Host_생성("1234", 1234L)); + @Test + void 예외를_발생시킨다() { + assertThatThrownBy(() -> spaceService.findSpace(host.getId(), NON_EXIST_SPACE_ID)) + .isInstanceOf(NotFoundException.class) + .hasMessageContaining("존재하지 않는 공간입니다."); + } + } + } - Space space_1 = Space_생성(host, "잠실 캠퍼스"); - Space space_2 = Space_생성(host, "선릉 캠퍼스"); - Space space_3 = Space_생성(host, "양평같은방"); - List spaces = repository.saveAll(List.of(space_1, space_2, space_3)); + @Nested + class findSpaces_메서드는 { - expected = SpacesResponse.from(spaces); + @Nested + class 존재하지_않는_HostId를_입력받는_경우 { + + private static final long NON_EXIST_HOST_ID = 0L; + + @Test + void 예외를_발생시킨다() { + assertThatThrownBy(() -> spaceService.findSpaces(NON_EXIST_HOST_ID)) + .isInstanceOf(NotFoundException.class) + .hasMessageContaining("존재하지 않는 호스트입니다."); } + } + + @Nested + class 올바른_입력을_받는_경우 { + + private final Host host = repository.save(Host_생성("1234", 1L)); + private final SpacesResponse expected = SpacesResponse.from( + repository.saveAll(List.of(Space_생성(host, "잠실 캠퍼스"), Space_생성(host, "선릉 캠퍼스"), + Space_생성(host, "양평같은방")))); @Test void 해당_Host가_소유한_Space를_응답으로_반환한다() { @@ -90,17 +157,12 @@ void setUp() { class createSpace_메서드는 { @Nested - class Host가_입력받은_Space_이름과_같은_Space를_이미_가지고_있는_경우 { + class 입력받은_Space_이름이_Host가_소유한_Space의_이름과_중복되는_경우 { - private Host host; - private SpaceCreateRequest request; - - @BeforeEach - void setUp() { - host = repository.save(Host_생성("1234", 1234L)); - Space space = repository.save(Space_생성(host, "잠실 캠퍼스")); - request = new SpaceCreateRequest(space.getName().getValue(), "https://image.gongcheck.shop/123sdf5"); - } + private final Host host = repository.save(Host_생성("1234", 1L)); + private final SpaceCreateRequest request = new SpaceCreateRequest( + repository.save(Space_생성(host, "잠실 캠퍼스")).getName().getValue(), + "https://image.gongcheck.shop/123sdf5"); @Test void 예외를_발생시킨다() { @@ -111,16 +173,12 @@ void setUp() { } @Nested - class 존재하지_않는_Host_id를_입력받은_경우 { + class 존재하지_않는_HostId를_입력받는_경우 { private static final long NON_EXIST_HOST_ID = 0L; - private SpaceCreateRequest request; - - @BeforeEach - void setUp() { - request = new SpaceCreateRequest("이것은 유일한 Space이름", "https://image.gongcheck.shop/123sdf5"); - } + private final SpaceCreateRequest request = new SpaceCreateRequest("이것은 유일한 Space이름", + "https://image.gongcheck.shop/123sdf5"); @Test void 예외를_발생시킨다() { @@ -131,19 +189,13 @@ void setUp() { } @Nested - class 입력받은_Host가_존재하는_경우 { + class 올바른_입력을_받는_경우 { private static final String SPACE_NAME = "잠실 캠퍼스"; private static final String SPACE_IMAGE_URL = "https://image.gongcheck.shop/123sdf5"; - private Host host; - private SpaceCreateRequest request; - - @BeforeEach - void setUp() { - host = repository.save(Host_생성("1234", 1234L)); - request = new SpaceCreateRequest(SPACE_NAME, SPACE_IMAGE_URL); - } + private final Host host = repository.save(Host_생성("1234", 1L)); + private final SpaceCreateRequest request = new SpaceCreateRequest(SPACE_NAME, SPACE_IMAGE_URL); @Test void Space를_생성한다() { @@ -159,193 +211,159 @@ void setUp() { } @Nested - class findSpace_메서드는 { + class removeSpace_메서드는 { @Nested - class Space_목록이_존재하는_경우 { - - private static final String SPACE_NAME = "잠실 캠퍼스"; - - private Host host; - private Space space; + class 입력받은_Host_id가_존재하지_않는_경우 { - @BeforeEach - void setUp() { - host = repository.save(Host_생성("1234", 2345L)); - space = repository.save(Space_생성(host, SPACE_NAME)); - } + private static final long NON_EXIST_HOST_ID = 0L; + private static final long DUMMY_SPACE_ID = 1L; @Test - void Job_목록을_조회한다() { - SpaceResponse actual = spaceService.findSpace(host.getId(), space.getId()); - - assertAll( - () -> assertThat(actual.getId()).isEqualTo(space.getId()), - () -> assertThat(actual.getName()).isEqualTo(SPACE_NAME) - ); + void 예외를_발생시킨다() { + assertThatThrownBy(() -> spaceService.removeSpace(NON_EXIST_HOST_ID, DUMMY_SPACE_ID)) + .isInstanceOf(NotFoundException.class) + .hasMessageContaining("존재하지 않는 호스트입니다."); } } @Nested - class 입력받은_Host가_입력받은_Space를_가지고_있지_않은_경우 { - - private Space space; - private Host anotherHost; + class 입력받은_Host가_입력받은_Space를_소유하고_있지_않은_경우 { - @BeforeEach - void setUp() { - Host host = repository.save(Host_생성("1234", 1234L)); - space = repository.save(Space_생성(host, "잠실 캠퍼스")); - anotherHost = repository.save(Host_생성("1234", 2345L)); - } + private final Host host = repository.save(Host_생성("1234", 1L)); + private final Host anotherHost = repository.save(Host_생성("1234", 2L)); + private final Space space = repository.save(Space_생성(anotherHost, "잠실 캠퍼스")); @Test void 예외를_발생시킨다() { - assertThatThrownBy(() -> spaceService.findSpace(anotherHost.getId(), space.getId())) + assertThatThrownBy(() -> spaceService.removeSpace(host.getId(), space.getId())) .isInstanceOf(NotFoundException.class) .hasMessageContaining("존재하지 않는 공간입니다."); } } @Nested - class 존재하지_않는_Host_id를_입력받은_경우 { - - private static final long NON_EXIST_HOST_ID = 0L; + class 올바른_입력을_받는_경우 { - private Space space; - - @BeforeEach - void setUp() { - Host host = repository.save(Host_생성("1234", 1234L)); - space = repository.save(Space_생성(host, "잠실 캠퍼스")); - } + private final Host host = repository.save(Host_생성("1234", 1L)); + private final Space space = repository.save(Space_생성(host, "잠실 캠퍼스")); + private final Job job = repository.save(Job_생성(space, "청소")); + private final Section section = repository.save(Section_생성(job, "대강의실")); + private final Task task = repository.save(Task_생성(section, "책상 닦기")); + private final RunningTask runningTask = repository.save(RunningTask_생성(task.getId(), false)); @Test - void 예외를_발생시킨다() { - assertThatThrownBy(() -> spaceService.findSpace(NON_EXIST_HOST_ID, space.getId())) - .isInstanceOf(NotFoundException.class) - .hasMessageContaining("존재하지 않는 호스트입니다."); + void 해당_Space_및_관련된_Job_Section_Task_RunningTask를_삭제한다() { + spaceService.removeSpace(host.getId(), space.getId()); + + assertAll( + () -> assertThat(repository.findById(Space.class, space.getId())).isEmpty(), + () -> assertThat(repository.findById(Job.class, job.getId())).isEmpty(), + () -> assertThat(repository.findById(Section.class, section.getId())).isEmpty(), + () -> assertThat(repository.findById(Task.class, task.getId())).isEmpty(), + () -> assertThat(repository.findById(RunningTask.class, runningTask.getTaskId())).isEmpty() + ); } } + } + + @Nested + class changeSpace_메서드는 { @Nested - class 존재하지_않는_Space_id를_입력받은_경우 { + class 입력받은_Host가_존재하지_않는_경우 { - private Host host; + private static final long NON_EXIST_HOST_ID = 0L; + private static final long DUMMY_SPACE_ID = 1L; - @BeforeEach - void setUp() { - host = repository.save(Host_생성("1234", 1234L)); - } + private final SpaceChangeRequest spaceChangeRequest = new SpaceChangeRequest("잠실 캠퍼스", "changeImageUrl"); @Test void 예외를_발생시킨다() { - assertThatThrownBy(() -> spaceService.findSpace(host.getId(), 0L)) + assertThatThrownBy( + () -> spaceService.changeSpace(NON_EXIST_HOST_ID, DUMMY_SPACE_ID, spaceChangeRequest)) .isInstanceOf(NotFoundException.class) - .hasMessageContaining("존재하지 않는 공간입니다."); + .hasMessageContaining("존재하지 않는 호스트입니다."); } } @Nested - class 입력받은_Host가_입력받은_Space를_소유하고_있는_경우 { - - private static final String SPACE_NAME = "잠실 캠퍼스"; + class 입력받은_Space가_존재하지_않는_경우 { - private Host host; - private Space space; + private static final long NON_EXIST_SPACE_ID = 0L; - @BeforeEach - void setUp() { - host = repository.save(Host_생성("1234", 1234L)); - space = repository.save(Space_생성(host, SPACE_NAME)); - } + private final Host host = repository.save(Host_생성("1234", 1L)); + private final SpaceChangeRequest spaceChangeRequest = new SpaceChangeRequest("잠실 캠퍼스", "changeImageUrl"); @Test - void Space_응답을_반환한다() { - SpaceResponse actual = spaceService.findSpace(host.getId(), space.getId()); - - assertAll( - () -> assertThat(actual.getId()).isEqualTo(space.getId()), - () -> assertThat(actual.getName()).isEqualTo(SPACE_NAME) - ); + void 예외를_발생시킨다() { + assertThatThrownBy( + () -> spaceService.changeSpace(host.getId(), NON_EXIST_SPACE_ID, spaceChangeRequest)) + .isInstanceOf(NotFoundException.class) + .hasMessageContaining("존재하지 않는 공간입니다."); } } - } - - @Nested - class removeSpace_메서드는 { @Nested - class 입력받은_Host_id가_존재하지_않는_경우 { - - private static final long NON_EXIST_HOST_ID = 0L; + class 입력받은_Host가_입력받은_Space를_소유하지_않은_경우 { - private Space space; - - @BeforeEach - void setUp() { - Host host = repository.save(Host_생성("1234", 1234L)); - space = repository.save(Space_생성(host, "잠실 캠퍼스")); - } + private final Host host = repository.save(Host_생성("1234", 1L)); + private final Host anotherHost = repository.save(Host_생성("1234", 2L)); + private final Space space = repository.save(Space_생성(anotherHost, "잠실 캠퍼스")); + private final SpaceChangeRequest spaceChangeRequest = new SpaceChangeRequest("changeName", + "changeImageUrl"); @Test void 예외를_발생시킨다() { - assertThatThrownBy(() -> spaceService.removeSpace(NON_EXIST_HOST_ID, space.getId())) + assertThatThrownBy( + () -> spaceService.changeSpace(host.getId(), space.getId(), spaceChangeRequest)) .isInstanceOf(NotFoundException.class) - .hasMessageContaining("존재하지 않는 호스트입니다."); + .hasMessageContaining("존재하지 않는 공간입니다."); } } @Nested - class 입력받은_Host가_입력받은_Space를_소유하고_있지_않은_경우 { + class 입력받은_Space의_이름이_입력받은_Host가_소유한_Space_이름과_중복되는_경우 { - private Host anotherHost; - private Space space; + private static final String SPACE_NAME = "잠실 캠퍼스"; - @BeforeEach - void setUp() { - Host host = repository.save(Host_생성("1234", 1234L)); - anotherHost = repository.save(Host_생성("1234", 4567L)); - space = repository.save(Space_생성(host, "잠실 캠퍼스")); - } + private final Host host = repository.save(Host_생성("1234", 1L)); + private final Space existSpace = repository.save(Space_생성(host, SPACE_NAME)); + private final Space spaceToChangeName = repository.save(Space_생성(host, "선릉 캠퍼스")); + private final SpaceChangeRequest spaceChangeRequest = new SpaceChangeRequest(SPACE_NAME, "image"); @Test void 예외를_발생시킨다() { - assertThatThrownBy(() -> spaceService.removeSpace(anotherHost.getId(), space.getId())) - .isInstanceOf(NotFoundException.class); + assertThatThrownBy( + () -> spaceService.changeSpace(host.getId(), spaceToChangeName.getId(), spaceChangeRequest)) + .isInstanceOf(BusinessException.class) + .hasMessageContaining("이미 존재하는 이름입니다."); } } @Nested - class 입력받은_Space_id가_존재하면 { - - private Host host; - private Space space; - private Job job; - private Section section; - private Task task; - private RunningTask runningTask; - - @BeforeEach - void setUp() { - host = repository.save(Host_생성("1234", 1234L)); - space = repository.save(Space_생성(host, "잠실 캠퍼스")); - job = repository.save(Job_생성(space, "청소")); - section = repository.save(Section_생성(job, "대강의실")); - task = repository.save(Task_생성(section, "책상 닦기")); - runningTask = repository.save(RunningTask_생성(task.getId(), false)); - } + class 올바른_입력을_받는_경우 { + + private static final String CHANGE_NAME = "잠실 캠퍼스"; + private static final String CHANGE_IMG_URL = "changeUrl"; + + private final Host host = repository.save(Host_생성("1234", 1L)); + private final Space space = repository.save(Space.builder() + .host(host) + .name(new Name("잠실 캠퍼스")) + .imageUrl("imageUrl") + .createdAt(LocalDateTime.now()) + .build()); + private final SpaceChangeRequest spaceChangeRequest = new SpaceChangeRequest(CHANGE_NAME, CHANGE_IMG_URL); @Test - void 해당_Space_및_관련된_Job_Section_Task_RunningTask를_삭제한다() { - spaceService.removeSpace(host.getId(), space.getId()); + void Space_이름을_변경한다() { + spaceService.changeSpace(host.getId(), space.getId(), spaceChangeRequest); + Space actual = spaceRepository.getByHostAndId(host, space.getId()); assertAll( - () -> assertThat(repository.findById(Space.class, space.getId())).isEmpty(), - () -> assertThat(repository.findById(Job.class, job.getId())).isEmpty(), - () -> assertThat(repository.findById(Section.class, section.getId())).isEmpty(), - () -> assertThat(repository.findById(Task.class, task.getId())).isEmpty(), - () -> assertThat(repository.findById(RunningTask.class, runningTask.getTaskId())).isEmpty() + () -> assertThat(actual.getName()).isEqualTo(new Name(CHANGE_NAME)), + () -> assertThat(actual.getImageUrl()).isEqualTo(CHANGE_IMG_URL) ); } } From 6f16ba3a6916e0f0237819ac8749fd467989deed Mon Sep 17 00:00:00 2001 From: Seungchan On <62434898+cks3066@users.noreply.github.com> Date: Thu, 15 Sep 2022 12:37:36 +0900 Subject: [PATCH 05/19] =?UTF-8?q?[FE]=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=EC=9D=84=20=EB=AA=A8=EB=B0=94=EC=9D=BC=20?= =?UTF-8?q?=EB=B0=8F=20=ED=83=9C=EB=B8=94=EB=A6=BF=EC=97=90=EC=84=9C?= =?UTF-8?q?=EB=8F=84=20=EB=B3=BC=20=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D?= =?UTF-8?q?=20=ED=95=9C=EB=8B=A4.=20(#557)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * design: Button 컴포넌트 width 수정 * design: 깃허브 로그인 컴포넌트 디자인 변경 * feat: 모바일, 데스크탑 네이게이터 분리 * feat: 모달창이 모바일에서도 정상적으로 보이도록 수정 * feat: 공간 정보 컴포넌트 반응형으로 변경 * feat: 제출내역 컴포넌트 반응형으로 변경 * feat: 대시보드 페이지가 모바일에서도 볼 수 있어야한다. * feat: 업무 생성, 수정 컴포넌트 반응형 리팩터링 * feat: 세부 반응형 요소 추가 * design: 홈페이지 디자인 수정 * feat: 애니메이션 수정 및 유틸 함수 추가 * style: 풀필요 코드 삭제 --- .../src/components/common/Button/styles.ts | 4 +- .../common/GitHubLoginButton/styles.ts | 7 +- .../src/components/common/SlideMenu/index.tsx | 12 +++ .../src/components/common/SlideMenu/styles.ts | 19 +++++ .../src/components/host/ImageBox/styles.ts | 1 + .../src/components/host/JobControl/styles.ts | 12 +-- .../src/components/host/JobListCard/index.tsx | 4 +- .../src/components/host/JobListCard/styles.ts | 21 +++-- .../host/{Navigation => Menu}/index.tsx | 18 ++--- .../host/{Navigation => Menu}/styles.ts | 37 +-------- .../useHostNavigation.ts => Menu/useMenu.ts} | 4 +- .../host/Navigator/LeftNavigator/index.tsx | 25 ++++++ .../host/Navigator/LeftNavigator/styles.ts | 37 +++++++++ .../host/Navigator/TopNavigator/index.tsx | 39 ++++++++++ .../host/Navigator/TopNavigator/styles.ts | 39 ++++++++++ .../src/components/host/SectionCard/styles.ts | 12 ++- .../host/SectionDetailModal/index.tsx | 6 +- .../host/SectionDetailModal/styles.ts | 20 +++-- .../src/components/host/SlackUrlBox/styles.ts | 10 ++- .../components/host/SlackUrlModal/index.tsx | 10 +-- .../components/host/SlackUrlModal/styles.ts | 19 +++-- .../host/SpaceDeleteButton/index.tsx | 4 +- .../host/SpaceDeleteButton/styles.ts | 28 ++++++- .../host/SpaceDeleteModal/index.tsx | 5 +- .../host/SpaceDeleteModal/styles.ts | 26 +++++-- .../src/components/host/SpaceInfo/styles.ts | 28 ++++--- .../host/SpaceInfoCreateBox/index.tsx | 2 +- .../host/SpaceInfoCreateBox/styles.ts | 25 ++++-- .../host/SpaceInfoDisplayBox/styles.ts | 6 +- .../host/SpaceInfoUpdateBox/index.tsx | 2 +- .../host/SpaceInfoUpdateBox/styles.ts | 16 +++- .../src/components/host/Submissions/index.tsx | 41 +++++----- .../src/components/host/Submissions/styles.ts | 56 ++++++------- .../src/components/host/TaskBox/index.tsx | 2 +- frontend/src/layouts/HostLayout/styles.ts | 15 ++-- frontend/src/layouts/ManageLayout/index.tsx | 27 ++++++- frontend/src/pages/host/DashBoard/index.tsx | 2 +- frontend/src/pages/host/DashBoard/styles.ts | 78 +++++++++++++++---- .../src/pages/host/DashBoard/useDashBoard.tsx | 14 ++-- frontend/src/pages/host/Home/index.tsx | 71 ++--------------- frontend/src/pages/host/Home/styles.ts | 36 +++++++++ frontend/src/pages/host/JobCreate/styles.ts | 28 +------ frontend/src/pages/host/JobUpdate/index.tsx | 2 - frontend/src/pages/host/JobUpdate/styles.ts | 26 +------ .../src/pages/host/PasswordUpdate/styles.ts | 9 ++- frontend/src/pages/host/SpaceCreate/styles.ts | 18 +---- frontend/src/pages/host/SpaceUpdate/styles.ts | 3 +- frontend/src/styles/animation.ts | 20 +++++ frontend/src/utils/isNull.ts | 5 ++ 49 files changed, 578 insertions(+), 373 deletions(-) create mode 100644 frontend/src/components/common/SlideMenu/index.tsx create mode 100644 frontend/src/components/common/SlideMenu/styles.ts rename frontend/src/components/host/{Navigation => Menu}/index.tsx (78%) rename frontend/src/components/host/{Navigation => Menu}/styles.ts (69%) rename frontend/src/components/host/{Navigation/useHostNavigation.ts => Menu/useMenu.ts} (92%) create mode 100644 frontend/src/components/host/Navigator/LeftNavigator/index.tsx create mode 100644 frontend/src/components/host/Navigator/LeftNavigator/styles.ts create mode 100644 frontend/src/components/host/Navigator/TopNavigator/index.tsx create mode 100644 frontend/src/components/host/Navigator/TopNavigator/styles.ts create mode 100644 frontend/src/pages/host/Home/styles.ts create mode 100644 frontend/src/utils/isNull.ts diff --git a/frontend/src/components/common/Button/styles.ts b/frontend/src/components/common/Button/styles.ts index 4018ab41..737da9cc 100644 --- a/frontend/src/components/common/Button/styles.ts +++ b/frontend/src/components/common/Button/styles.ts @@ -4,10 +4,10 @@ import theme from '@/styles/theme'; const button = css` background: ${theme.colors.primary}; - width: 224px; + width: auto; height: 48px; border-radius: 12px; - font-size: 16px; + font-size: 14px; font-weight: 600; color: ${theme.colors.white}; margin: 24px; diff --git a/frontend/src/components/common/GitHubLoginButton/styles.ts b/frontend/src/components/common/GitHubLoginButton/styles.ts index 84339728..a6613032 100644 --- a/frontend/src/components/common/GitHubLoginButton/styles.ts +++ b/frontend/src/components/common/GitHubLoginButton/styles.ts @@ -4,15 +4,16 @@ const wrapper = css` display: flex; justify-content: center; align-items: center; - width: 260px; + width: auto; border-radius: 24px; color: white; background-color: #21262c; + padding: 0 16px; `; const text = css` - margin-left: 16px; - font-size: 24px; + margin-left: 12px; + font-size: 18px; `; const styles = { wrapper, text }; diff --git a/frontend/src/components/common/SlideMenu/index.tsx b/frontend/src/components/common/SlideMenu/index.tsx new file mode 100644 index 00000000..aa2c8a70 --- /dev/null +++ b/frontend/src/components/common/SlideMenu/index.tsx @@ -0,0 +1,12 @@ +import styles from './styles'; + +interface SlideMenuProps { + children: React.ReactNode; + isShowMenu: boolean | null; +} + +const SlideMenu: React.FC = ({ children, isShowMenu }) => { + return
{children}
; +}; + +export default SlideMenu; diff --git a/frontend/src/components/common/SlideMenu/styles.ts b/frontend/src/components/common/SlideMenu/styles.ts new file mode 100644 index 00000000..0dfad9fd --- /dev/null +++ b/frontend/src/components/common/SlideMenu/styles.ts @@ -0,0 +1,19 @@ +import { css } from '@emotion/react'; + +import animation from '@/styles/animation'; +import theme from '@/styles/theme'; + +const slideMenu = (isShowMenu: boolean | null) => css` + position: absolute; + min-height: 100vh; + background-color: ${theme.colors.white}; + z-index: 100; + width: 100%; + box-shadow: 0 -2px 2px 0px ${theme.colors.shadow30}; + animation: ${isShowMenu === true ? animation.navigatorOpen : animation.navigatorClose} 0.2s ease-out; + animation-fill-mode: forwards; +`; + +const styles = { slideMenu }; + +export default styles; diff --git a/frontend/src/components/host/ImageBox/styles.ts b/frontend/src/components/host/ImageBox/styles.ts index aeb717c8..4bafeb96 100644 --- a/frontend/src/components/host/ImageBox/styles.ts +++ b/frontend/src/components/host/ImageBox/styles.ts @@ -30,6 +30,7 @@ const imageChangeBox = (imageUrl: string, borderStyle?: string) => css` `; const imageInput = css` + width: 100%; opacity: 0; z-index: -1; `; diff --git a/frontend/src/components/host/JobControl/styles.ts b/frontend/src/components/host/JobControl/styles.ts index 7e954b18..b7947cfb 100644 --- a/frontend/src/components/host/JobControl/styles.ts +++ b/frontend/src/components/host/JobControl/styles.ts @@ -11,29 +11,25 @@ const header = css` border-bottom: 1px solid ${theme.colors.gray300}; padding: 16px 32px; font-size: 16px; - - @media screen and (max-width: 720px) { - font-size: 14px; - } `; const createButton = css` margin: 0; margin-left: 12px; - font-size: 1.2em; + font-size: 1.1em; width: fit-content; height: fit-content; - padding: 12px; + padding: 8px 10px; background-color: ${theme.colors.green}; `; const jobNameInput = css` border: none; border-radius: 12px; - width: 55%; + width: 64%; height: 1.5em; padding: 1em; - font-size: 1.5em; + font-size: 1.2em; font-weight: 500; margin: 12px 0; background-color: ${theme.colors.white}; diff --git a/frontend/src/components/host/JobListCard/index.tsx b/frontend/src/components/host/JobListCard/index.tsx index a14e29c6..2f8a761f 100644 --- a/frontend/src/components/host/JobListCard/index.tsx +++ b/frontend/src/components/host/JobListCard/index.tsx @@ -19,7 +19,7 @@ const JobListCard: React.FC = ({ jobs }) => { return (
- 공간 업무 목록 +

업무 목록

@@ -28,7 +28,7 @@ const JobListCard: React.FC = ({ jobs }) => { {jobs.length === 0 ? (
-
생성된 업무가 없어요.
+
생성된 업무가 없습니다.
) : ( jobs.map(job => ) diff --git a/frontend/src/components/host/JobListCard/styles.ts b/frontend/src/components/host/JobListCard/styles.ts index 2a1d7465..43531c44 100644 --- a/frontend/src/components/host/JobListCard/styles.ts +++ b/frontend/src/components/host/JobListCard/styles.ts @@ -4,19 +4,27 @@ import theme from '@/styles/theme'; const layout = css` min-width: 320px; - width: 100%; - height: 30.2rem; background-color: ${theme.colors.white}; box-shadow: 2px 2px 2px 2px ${theme.colors.shadow10}; border-radius: 8px; + + @media screen and (min-width: 1024px) { + height: 452px; + width: 100%; + } + + @media screen and (max-width: 1023px) { + height: 360px; + width: 90%; + } `; const title = css` display: flex; align-items: center; justify-content: space-between; - padding: 1rem 1.25rem; - font-size: 1.4rem; + padding: 0 24px; + font-size: 1.2rem; border-bottom: 1px solid ${theme.colors.gray300}; `; @@ -49,11 +57,8 @@ const jobList = css` const newJobButton = css` width: auto; height: 2rem; - padding: 6px 10px; - font-weight: 500; - font-size: 1rem; + font-size: 0.9rem; padding: 0 12px; - margin: 0; `; diff --git a/frontend/src/components/host/Navigation/index.tsx b/frontend/src/components/host/Menu/index.tsx similarity index 78% rename from frontend/src/components/host/Navigation/index.tsx rename to frontend/src/components/host/Menu/index.tsx index 925accac..f27c82be 100644 --- a/frontend/src/components/host/Navigation/index.tsx +++ b/frontend/src/components/host/Menu/index.tsx @@ -1,20 +1,14 @@ -import useHostNavigation from './useHostNavigation'; +import useMenu from './useMenu'; import { CgHomeAlt } from 'react-icons/cg'; import { RiLockPasswordLine } from 'react-icons/ri'; -import navigationLogo from '@/assets/navigationLogo.png'; - import styles from './styles'; -const Navigation: React.FC = () => { - const { selectedSpaceId, spaceData, onClickPasswordUpdate, onClickSpace, onClickNewSpace } = useHostNavigation(); +const Menu = () => { + const { selectedSpaceId, spaceData, onClickPasswordUpdate, onClickSpace, onClickNewSpace } = useMenu(); return ( -
-
- -
- + <>
Menu
@@ -47,8 +41,8 @@ const Navigation: React.FC = () => {
-
+ ); }; -export default Navigation; +export default Menu; diff --git a/frontend/src/components/host/Navigation/styles.ts b/frontend/src/components/host/Menu/styles.ts similarity index 69% rename from frontend/src/components/host/Navigation/styles.ts rename to frontend/src/components/host/Menu/styles.ts index 0afe853a..ea9e3f0c 100644 --- a/frontend/src/components/host/Navigation/styles.ts +++ b/frontend/src/components/host/Menu/styles.ts @@ -2,38 +2,6 @@ import { css } from '@emotion/react'; import theme from '@/styles/theme'; -const layout = css` - font-size: 16px; - position: fixed; - top: 0; - left: 0; - height: 100vh; - width: 14em; - background-color: ${theme.colors.white}; - box-shadow: 6px 0 8px ${theme.colors.gray350}; - z-index: 1; - - @media screen and (max-width: 1024px) { - font-size: 14px; - } - @media screen and (max-width: 720px) { - font-size: 12px; - } -`; - -const logo = css` - display: flex; - justify-content: center; - align-items: center; - width: 100%; - height: 200px; -`; - -const logoImage = css` - width: 160px; - height: 160px; -`; - const category = css` width: 100%; display: flex; @@ -42,7 +10,7 @@ const category = css` `; const categoryTitle = css` - font-size: 1em; + font-size: 0.9em; font-weight: 600; color: ${theme.colors.gray800}; margin: 8px 0; @@ -103,9 +71,6 @@ const addNewSpace = css` `; const styles = { - layout, - logo, - logoImage, category, categoryTitle, categoryList, diff --git a/frontend/src/components/host/Navigation/useHostNavigation.ts b/frontend/src/components/host/Menu/useMenu.ts similarity index 92% rename from frontend/src/components/host/Navigation/useHostNavigation.ts rename to frontend/src/components/host/Menu/useMenu.ts index a876eb4c..4b6f1e0f 100644 --- a/frontend/src/components/host/Navigation/useHostNavigation.ts +++ b/frontend/src/components/host/Menu/useMenu.ts @@ -6,7 +6,7 @@ import apiSpace from '@/apis/space'; import { ID } from '@/types'; -const useHostNavigation = () => { +const useHostNavigator = () => { const navigate = useNavigate(); const { spaceId } = useParams() as { spaceId: ID }; @@ -31,4 +31,4 @@ const useHostNavigation = () => { return { selectedSpaceId, spaceData, onClickPasswordUpdate, onClickSpace, onClickNewSpace }; }; -export default useHostNavigation; +export default useHostNavigator; diff --git a/frontend/src/components/host/Navigator/LeftNavigator/index.tsx b/frontend/src/components/host/Navigator/LeftNavigator/index.tsx new file mode 100644 index 00000000..adc9b23c --- /dev/null +++ b/frontend/src/components/host/Navigator/LeftNavigator/index.tsx @@ -0,0 +1,25 @@ +import Menu from '../../Menu'; +import { useNavigate } from 'react-router-dom'; + +import logo from '@/assets/navigationLogo.png'; + +import styles from './styles'; + +const LeftNavigator: React.FC = () => { + const navigate = useNavigate(); + + const onClickLogo = () => { + if (location.pathname.split('/').length !== 4) navigate(-1); + }; + + return ( +
+
+ +
+ +
+ ); +}; + +export default LeftNavigator; diff --git a/frontend/src/components/host/Navigator/LeftNavigator/styles.ts b/frontend/src/components/host/Navigator/LeftNavigator/styles.ts new file mode 100644 index 00000000..39254066 --- /dev/null +++ b/frontend/src/components/host/Navigator/LeftNavigator/styles.ts @@ -0,0 +1,37 @@ +import { css } from '@emotion/react'; + +import theme from '@/styles/theme'; + +const layout = css` + font-size: 16px; + height: 100vh; + width: 14em; + position: fixed; + top: 0; + left: 0; + background-color: ${theme.colors.white}; + box-shadow: 6px 0 8px ${theme.colors.gray350}; + z-index: 1; +`; + +const logo = css` + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 200px; + cursor: pointer; +`; + +const logoImage = css` + width: 160px; + height: 160px; +`; + +const styles = { + layout, + logo, + logoImage, +}; + +export default styles; diff --git a/frontend/src/components/host/Navigator/TopNavigator/index.tsx b/frontend/src/components/host/Navigator/TopNavigator/index.tsx new file mode 100644 index 00000000..fbdd00e7 --- /dev/null +++ b/frontend/src/components/host/Navigator/TopNavigator/index.tsx @@ -0,0 +1,39 @@ +import Menu from '../../Menu'; +import isNull from '@/utils/isNull'; +import { useState } from 'react'; +import { BiMenu, BiX } from 'react-icons/bi'; +import { useNavigate } from 'react-router-dom'; + +import SlideMenu from '@/components/common/SlideMenu'; + +import logo from '@/assets/logoTitle.png'; + +import styles from './styles'; + +const TopNavigator: React.FC = () => { + const navigate = useNavigate(); + + const [isShowMenu, setIsShowMenu] = useState(null); + + const onClickMenuIcon = () => setIsShowMenu(prev => !prev); + + const onClickLogo = () => { + if (location.pathname.split('/').length !== 4) navigate(-1); + }; + + return ( + <> +
+ {isShowMenu ? : } + +
+ {!isNull(isShowMenu) && ( + + + + )} + + ); +}; + +export default TopNavigator; diff --git a/frontend/src/components/host/Navigator/TopNavigator/styles.ts b/frontend/src/components/host/Navigator/TopNavigator/styles.ts new file mode 100644 index 00000000..0479d7a0 --- /dev/null +++ b/frontend/src/components/host/Navigator/TopNavigator/styles.ts @@ -0,0 +1,39 @@ +import { css } from '@emotion/react'; + +import theme from '@/styles/theme'; + +const layout = css` + font-size: 16px; + position: fixed; + top: 0; + display: flex; + justify-content: center; + align-items: center; + + background-color: ${theme.colors.white}; + z-index: 1; + + width: 100vw; + height: 64px; + box-shadow: 0 2px 2px 0px ${theme.colors.shadow30}; + + svg { + color: ${theme.colors.gray800}; + cursor: pointer; + position: absolute; + left: 12px; + } +`; + +const logo = css` + height: 40px; + transform: translateY(4px); + cursor: pointer; +`; + +const styles = { + layout, + logo, +}; + +export default styles; diff --git a/frontend/src/components/host/SectionCard/styles.ts b/frontend/src/components/host/SectionCard/styles.ts index 4f978f21..594e387c 100644 --- a/frontend/src/components/host/SectionCard/styles.ts +++ b/frontend/src/components/host/SectionCard/styles.ts @@ -7,10 +7,10 @@ const container = css` display: flex; flex-direction: column; width: 100%; - max-width: 480px; - height: 360px; + min-width: 320px; + height: 356px; overflow-y: scroll; - padding: 32px; + padding: 40px 32px; background-color: ${theme.colors.white}; box-shadow: 2px 2px 2px 2px ${theme.colors.shadow20}; border-radius: 8px; @@ -42,7 +42,6 @@ const titleWrapper = css` display: flex; justify-content: space-between; align-items: center; - font-size: 20px; margin-bottom: 8px; padding-bottom: 16px; border-bottom: 2px solid ${theme.colors.shadow20}; @@ -79,11 +78,10 @@ const newTaskButton = css` `; const input = css` - font-size: 18px; - line-height: 38px; + font-size: 16px; border: 1px solid ${theme.colors.shadow30}; border-radius: 12px; - padding: 0 16px; + padding: 4px 16px; background-color: ${theme.colors.white}; width: 80%; diff --git a/frontend/src/components/host/SectionDetailModal/index.tsx b/frontend/src/components/host/SectionDetailModal/index.tsx index 4c165c13..2301e3f5 100644 --- a/frontend/src/components/host/SectionDetailModal/index.tsx +++ b/frontend/src/components/host/SectionDetailModal/index.tsx @@ -47,7 +47,7 @@ const SectionDetailModal: React.FC = props => { fileInput.current?.click()} /> @@ -55,14 +55,14 @@ const SectionDetailModal: React.FC = props => {