From ce5982b71e3b448b9ea3da41cafd6ebb4b346b7e Mon Sep 17 00:00:00 2001 From: KimGaeun Date: Thu, 7 Dec 2023 02:20:54 +0900 Subject: [PATCH 001/108] =?UTF-8?q?=E2=9C=A8=20Add=20Alert=20component?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AlertDialog와는 다른 역할을 하는 Alert 컴포넌트 생성 --- packages/client/src/shared/ui/alert/Alert.tsx | 87 +++++++++++++++++++ packages/client/src/shared/ui/alert/index.ts | 1 + .../client/src/shared/ui/alertDialog/index.ts | 1 + 3 files changed, 89 insertions(+) create mode 100644 packages/client/src/shared/ui/alert/Alert.tsx create mode 100644 packages/client/src/shared/ui/alert/index.ts create mode 100644 packages/client/src/shared/ui/alertDialog/index.ts diff --git a/packages/client/src/shared/ui/alert/Alert.tsx b/packages/client/src/shared/ui/alert/Alert.tsx new file mode 100644 index 0000000..b6444e5 --- /dev/null +++ b/packages/client/src/shared/ui/alert/Alert.tsx @@ -0,0 +1,87 @@ +import styled from '@emotion/styled'; +import { Body02ME, Title01 } from '../styles'; +import { css } from '@emotion/react'; +import { Button } from '..'; +import { useState } from 'react'; + +interface PropsTypes extends React.HTMLAttributes { + title: string; + description?: string; +} + +export default function Alert({ title, description, ...args }: PropsTypes) { + const [isOpen, setIsOpen] = useState(true); + + const handleConfirmButton = () => setIsOpen(false); + + if (!isOpen) return null; + + return ( + + + {title} + {description && {description}} + + + + + + + ); +} + +const Overlay = styled.div` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 998; + background-color: rgba(0, 0, 0, 0.5); +`; + +const Layout = styled.div` + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 999; + + display: flex; + flex-direction: column; + width: 516px; + border-radius: 16px; + padding: 32px; + margin: 12px 0 0 0; + + ${({ theme: { colors } }) => css` + background-color: ${colors.background.bdp01_80}; + border: 2px solid ${colors.warning.filled}; + `}; +`; + +const Title = styled.h1` + display: flex; + justify-content: flex-start; + margin: 0 0 8px 0; + color: ${({ theme: { colors } }) => colors.text.primary}; + ${Title01} +`; + +const Description = styled.p` + color: ${({ theme: { colors } }) => colors.text.third}; + ${Body02ME} +`; + +const ButtonWrapper = styled.div` + display: flex; + justify-content: flex-end; + margin: 32px 0 0 0; +`; diff --git a/packages/client/src/shared/ui/alert/index.ts b/packages/client/src/shared/ui/alert/index.ts new file mode 100644 index 0000000..79e3b15 --- /dev/null +++ b/packages/client/src/shared/ui/alert/index.ts @@ -0,0 +1 @@ +export * from './Alert'; diff --git a/packages/client/src/shared/ui/alertDialog/index.ts b/packages/client/src/shared/ui/alertDialog/index.ts new file mode 100644 index 0000000..76a8200 --- /dev/null +++ b/packages/client/src/shared/ui/alertDialog/index.ts @@ -0,0 +1 @@ +export * from './AlertDialog'; From 20aee1da4dbb2554502c2c2987a4edcba8161e95 Mon Sep 17 00:00:00 2001 From: KimGaeun Date: Thu, 7 Dec 2023 16:11:47 +0900 Subject: [PATCH 002/108] =?UTF-8?q?=E2=9C=A8=20Add=20Guest=20Route?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 검색 혹은 링크를 통해 들어왔을 때 보여줄 라우터 추가 --- packages/client/src/app/Router.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/client/src/app/Router.tsx b/packages/client/src/app/Router.tsx index c71a6ee..327042c 100644 --- a/packages/client/src/app/Router.tsx +++ b/packages/client/src/app/Router.tsx @@ -36,6 +36,14 @@ export const router = createBrowserRouter( } /> } /> + + }> + + + } /> + + + , ), ); From 4bf7bdb7c5f2a7107e3befe9230997ee46fc67de Mon Sep 17 00:00:00 2001 From: KimGaeun Date: Thu, 7 Dec 2023 16:12:42 +0900 Subject: [PATCH 003/108] =?UTF-8?q?=F0=9F=A9=B9=20Change=20Label=20conditi?= =?UTF-8?q?on?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - view가 무엇인지와 상관없이 라벨이 항상 보이도록 변경 --- packages/client/src/entities/posts/ui/Post.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/entities/posts/ui/Post.tsx b/packages/client/src/entities/posts/ui/Post.tsx index 647590d..002decc 100644 --- a/packages/client/src/entities/posts/ui/Post.tsx +++ b/packages/client/src/entities/posts/ui/Post.tsx @@ -64,7 +64,7 @@ export default function Post({ data, postId, title }: PropsType) { brightness={data.brightness} shape={data.shape} > - {view === 'DETAIL' && isHovered && ( + {isHovered && ( From b25f26baa70f943dc8e1b8f1f377c9907c6213a3 Mon Sep 17 00:00:00 2001 From: KimGaeun Date: Thu, 7 Dec 2023 16:13:59 +0900 Subject: [PATCH 004/108] =?UTF-8?q?=F0=9F=8E=A8=20Fix=20store=20codes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 코드를 더 간단하게 변경 --- packages/client/src/shared/store/useOwnerStore.ts | 4 ++-- packages/client/src/shared/store/useScreenSwitchStore.ts | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/client/src/shared/store/useOwnerStore.ts b/packages/client/src/shared/store/useOwnerStore.ts index e2205a3..93df3a7 100644 --- a/packages/client/src/shared/store/useOwnerStore.ts +++ b/packages/client/src/shared/store/useOwnerStore.ts @@ -14,7 +14,7 @@ export const useOwnerStore = create((set) => ({ if (value === true) set((state) => ({ ...state, pageOwnerNickName: '' })); set((state) => ({ ...state, isMyPage: value })); }, - setPageOwnerNickName: (value) => { - set((state) => ({ ...state, pageOwnerNickName: value })); + setPageOwnerNickName: (pageOwnerNickName) => { + set((state) => ({ ...state, pageOwnerNickName })); }, })); diff --git a/packages/client/src/shared/store/useScreenSwitchStore.ts b/packages/client/src/shared/store/useScreenSwitchStore.ts index 438c88f..188627b 100644 --- a/packages/client/src/shared/store/useScreenSwitchStore.ts +++ b/packages/client/src/shared/store/useScreenSwitchStore.ts @@ -7,7 +7,5 @@ interface ScreenSwitchState { export const useScreenSwitchStore = create((set) => ({ isSwitching: false, - setIsSwitching: (value) => { - set((state) => ({ ...state, isSwitching: value })); - }, + setIsSwitching: (isSwitching) => set((state) => ({ ...state, isSwitching })), })); From bf9168329666ef2298d8cbc8b148da313c09c220 Mon Sep 17 00:00:00 2001 From: KimGaeun Date: Thu, 7 Dec 2023 23:51:13 +0900 Subject: [PATCH 005/108] =?UTF-8?q?=E2=9C=A8=20Change=20routes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PrivateRoute로 감싸진 곳은 로그인해야만 접근 가능함 - PublicRoute로 감싸진 곳은 로그인하지 않은 상태에만 접근 가능함 --- packages/client/src/app/Router.tsx | 39 +++++++++++---- .../client/src/shared/routes/PrivateRoute.tsx | 25 ++++++++++ .../client/src/shared/routes/PublicRoute.tsx | 26 ++++++++++ .../galaxyCustomModal/GalaxyCustom.tsx | 50 ------------------- .../src/widgets/loginModal/LoginModal.tsx | 3 -- .../nickNameSetModal/NickNameSetModal.tsx | 3 -- .../src/widgets/signupModal/SignUpModal.tsx | 3 -- 7 files changed, 79 insertions(+), 70 deletions(-) create mode 100644 packages/client/src/shared/routes/PrivateRoute.tsx create mode 100644 packages/client/src/shared/routes/PublicRoute.tsx delete mode 100644 packages/client/src/widgets/galaxyCustomModal/GalaxyCustom.tsx diff --git a/packages/client/src/app/Router.tsx b/packages/client/src/app/Router.tsx index 327042c..fd782bf 100644 --- a/packages/client/src/app/Router.tsx +++ b/packages/client/src/app/Router.tsx @@ -14,27 +14,34 @@ import { PostModal } from 'features/postModal'; import GalaxyCustomModal from 'widgets/galaxyCustomModal/GalaxyCustomModal'; import StarCustomModal from 'widgets/starCustomModal/StarCustomModal'; import ShareModal from 'widgets/shareModal/ShareModal'; +import PrivateRoute from '../shared/routes/PrivateRoute'; +import PublicRoute from 'shared/routes/PublicRoute'; export const router = createBrowserRouter( createRoutesFromElements( <> }> } /> - } /> - } /> - }> - + + }> + } /> + } /> + }> + + - }> - - } /> + }> + }> + + } /> + + } /> + } /> + } /> + } /> - } /> - } /> - } /> - } /> }> @@ -44,6 +51,16 @@ export const router = createBrowserRouter( + + }> + }> + + + } /> + + + + , ), ); diff --git a/packages/client/src/shared/routes/PrivateRoute.tsx b/packages/client/src/shared/routes/PrivateRoute.tsx new file mode 100644 index 0000000..1bfca89 --- /dev/null +++ b/packages/client/src/shared/routes/PrivateRoute.tsx @@ -0,0 +1,25 @@ +import { Navigate, Outlet } from 'react-router-dom'; +import { getSignInInfo } from 'shared/apis'; +import { useEffect, useState } from 'react'; + +export default function PrivateRoute() { + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + (async () => { + try { + await getSignInInfo(); + setIsAuthenticated(true); + setIsLoading(false); + } catch (error) { + setIsAuthenticated(false); + setIsLoading(false); + } + })(); + }, []); + + if (isLoading) return ; + + return isAuthenticated && !isLoading ? : ; +} diff --git a/packages/client/src/shared/routes/PublicRoute.tsx b/packages/client/src/shared/routes/PublicRoute.tsx new file mode 100644 index 0000000..dacaf06 --- /dev/null +++ b/packages/client/src/shared/routes/PublicRoute.tsx @@ -0,0 +1,26 @@ +import { Navigate, Outlet } from 'react-router-dom'; +import { getSignInInfo } from 'shared/apis'; +import { useEffect, useState } from 'react'; +import Home from 'pages/Home/Home'; + +export default function PublicRoute() { + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + (async () => { + try { + await getSignInInfo(); + setIsAuthenticated(true); + setIsLoading(false); + } catch (error) { + setIsAuthenticated(false); + setIsLoading(false); + } + })(); + }, []); + + if (isLoading) return ; + + return isAuthenticated && !isLoading ? : ; +} diff --git a/packages/client/src/widgets/galaxyCustomModal/GalaxyCustom.tsx b/packages/client/src/widgets/galaxyCustomModal/GalaxyCustom.tsx deleted file mode 100644 index 4e533a5..0000000 --- a/packages/client/src/widgets/galaxyCustomModal/GalaxyCustom.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { Modal } from 'shared/ui'; -import { useNavigate } from 'react-router-dom'; -import { RightButton, SampleScreen, Sliders } from './ui'; -import { useViewStore } from 'shared/store'; -import styled from '@emotion/styled'; -import { useGalaxyStore, useCustomStore } from 'shared/store'; - -export default function GalaxyCustom() { - const navigate = useNavigate(); - const { setView } = useViewStore(); - const { setSpiral, setDensity, setStart, setThickness, setZDist } = - useGalaxyStore(); - const { spiral, density, start, thickness, zDist } = useCustomStore(); - - const handleSubmit = () => { - setSpiral(spiral); - setDensity(density); - setStart(start); - setThickness(thickness); - setZDist(zDist); - }; - - return ( -
{ - e.preventDefault(); - handleSubmit(); - }} - > - { - navigate('/home'); - setView('MAIN'); - }} - rightButton={} - > - - - - - -
- ); -} - -const Container = styled.div` - display: flex; - gap: 24px; -`; diff --git a/packages/client/src/widgets/loginModal/LoginModal.tsx b/packages/client/src/widgets/loginModal/LoginModal.tsx index 8394627..b075038 100644 --- a/packages/client/src/widgets/loginModal/LoginModal.tsx +++ b/packages/client/src/widgets/loginModal/LoginModal.tsx @@ -4,7 +4,6 @@ import { useNavigate } from 'react-router-dom'; import Cookies from 'js-cookie'; import { useState } from 'react'; import { postLogin } from 'shared/apis'; -import { useCheckLogin } from 'shared/hooks'; import { useScreenSwitchStore } from 'shared/store/useScreenSwitchStore'; export default function LoginModal() { @@ -35,8 +34,6 @@ export default function LoginModal() { ); }; - useCheckLogin(); - return (
{ diff --git a/packages/client/src/widgets/nickNameSetModal/NickNameSetModal.tsx b/packages/client/src/widgets/nickNameSetModal/NickNameSetModal.tsx index e99ce7e..765c0dc 100644 --- a/packages/client/src/widgets/nickNameSetModal/NickNameSetModal.tsx +++ b/packages/client/src/widgets/nickNameSetModal/NickNameSetModal.tsx @@ -5,7 +5,6 @@ import { postSignUp } from 'shared/apis'; import { useSignUpStore } from 'shared/store/useSignUpStore'; import { useToastStore } from 'shared/store/useToastStore'; import { useNavigate, useParams } from 'react-router-dom'; -import { useCheckLogin } from 'shared/hooks'; export default function NickNameSetModal() { const [validNickName, setValidNickName] = useState(''); @@ -14,8 +13,6 @@ export default function NickNameSetModal() { const { setText } = useToastStore(); const { id, pw } = useSignUpStore(); - useCheckLogin(); - const handleSaveButton = async () => { try { let response; diff --git a/packages/client/src/widgets/signupModal/SignUpModal.tsx b/packages/client/src/widgets/signupModal/SignUpModal.tsx index 65d043c..2ce797c 100644 --- a/packages/client/src/widgets/signupModal/SignUpModal.tsx +++ b/packages/client/src/widgets/signupModal/SignUpModal.tsx @@ -6,7 +6,6 @@ import PwInputContainer from './ui/PwInputContainer'; import CheckPwInputContainer from './ui/CheckPwInputContainer'; import { useSignUpStore } from 'shared/store/useSignUpStore'; import { useNavigate } from 'react-router-dom'; -import { useCheckLogin } from 'shared/hooks'; export default function SignUpModal() { const navigate = useNavigate(); @@ -19,8 +18,6 @@ export default function SignUpModal() { const { setId, setPw } = useSignUpStore(); - useCheckLogin(); - useEffect(() => { if (validId && validPw && validCheckPw) { setIsAllInputValid(true); From 95a682ef3fc40fd377758f37b7b15d67ae86c1b0 Mon Sep 17 00:00:00 2001 From: KimGaeun Date: Thu, 7 Dec 2023 23:54:06 +0900 Subject: [PATCH 006/108] =?UTF-8?q?=E2=9C=A8=20Change=20PrivateRoute,=20Pu?= =?UTF-8?q?blic=20Route=20loading=20screen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 접근되지 않는 화면에 접근했을 때 잠시 그 화면이 보이는 문제 해결 --- packages/client/src/shared/routes/PrivateRoute.tsx | 2 +- packages/client/src/shared/routes/PublicRoute.tsx | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/client/src/shared/routes/PrivateRoute.tsx b/packages/client/src/shared/routes/PrivateRoute.tsx index 1bfca89..20ca741 100644 --- a/packages/client/src/shared/routes/PrivateRoute.tsx +++ b/packages/client/src/shared/routes/PrivateRoute.tsx @@ -19,7 +19,7 @@ export default function PrivateRoute() { })(); }, []); - if (isLoading) return ; + if (isLoading) return null; return isAuthenticated && !isLoading ? : ; } diff --git a/packages/client/src/shared/routes/PublicRoute.tsx b/packages/client/src/shared/routes/PublicRoute.tsx index dacaf06..76e6264 100644 --- a/packages/client/src/shared/routes/PublicRoute.tsx +++ b/packages/client/src/shared/routes/PublicRoute.tsx @@ -1,7 +1,6 @@ import { Navigate, Outlet } from 'react-router-dom'; import { getSignInInfo } from 'shared/apis'; import { useEffect, useState } from 'react'; -import Home from 'pages/Home/Home'; export default function PublicRoute() { const [isAuthenticated, setIsAuthenticated] = useState(false); @@ -20,7 +19,7 @@ export default function PublicRoute() { })(); }, []); - if (isLoading) return ; + if (isLoading) return null; return isAuthenticated && !isLoading ? : ; } From 017bed7eeb2e9aa12a246f745f328222a36b5e46 Mon Sep 17 00:00:00 2001 From: KimGaeun Date: Fri, 8 Dec 2023 03:57:09 +0900 Subject: [PATCH 007/108] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Change=20route=20h?= =?UTF-8?q?ostId=20to=20hostNickname?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/client/src/app/Router.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/client/src/app/Router.tsx b/packages/client/src/app/Router.tsx index fd782bf..55517dc 100644 --- a/packages/client/src/app/Router.tsx +++ b/packages/client/src/app/Router.tsx @@ -45,7 +45,7 @@ export const router = createBrowserRouter( }> - + } /> @@ -54,7 +54,7 @@ export const router = createBrowserRouter( }> }> - + } /> From 2838fc317d5042f091666f7e0bef1199f8ca0755 Mon Sep 17 00:00:00 2001 From: KimGaeun Date: Fri, 8 Dec 2023 03:57:49 +0900 Subject: [PATCH 008/108] =?UTF-8?q?=F0=9F=94=A5=20Remove=20useCheckLogin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 쓸모없어진 코드 제거 --- .../client/src/shared/hooks/useCheckLogin.ts | 23 ------------------- 1 file changed, 23 deletions(-) delete mode 100644 packages/client/src/shared/hooks/useCheckLogin.ts diff --git a/packages/client/src/shared/hooks/useCheckLogin.ts b/packages/client/src/shared/hooks/useCheckLogin.ts deleted file mode 100644 index 078c7e7..0000000 --- a/packages/client/src/shared/hooks/useCheckLogin.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; -import instance from 'shared/apis/AxiosInterceptor'; - -export const useCheckLogin = () => { - const navigate = useNavigate(); - useEffect(() => { - const checkLogin = async () => { - try { - const res = await instance({ - method: 'GET', - url: '/auth/check-signin', - }); - if (res.status === 200) { - navigate('/home'); - } - } catch (error) { - console.error(error); - } - }; - checkLogin(); - }, []); -}; From 63e57d117d246a99536fe75c367a80bfc504cf16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=A4=80=EC=84=AD?= Date: Fri, 8 Dec 2023 10:01:57 +0900 Subject: [PATCH 009/108] :bug: Fix ShareLink Api MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ShareLink에 대하여 nickname이 아닌 username 반환하던 오류 수정 --- packages/server/src/auth/auth.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/auth/auth.service.ts b/packages/server/src/auth/auth.service.ts index 50fa812..ed39113 100644 --- a/packages/server/src/auth/auth.service.ts +++ b/packages/server/src/auth/auth.service.ts @@ -294,6 +294,6 @@ export class AuthService { throw new InternalServerErrorException('link user not found'); } - return linkUser.username; + return linkUser.nickname; } } From 78064b02f42329b8855697db5c262c3417a5f02d Mon Sep 17 00:00:00 2001 From: KimGaeun Date: Fri, 8 Dec 2023 10:30:42 +0900 Subject: [PATCH 010/108] =?UTF-8?q?=E2=9C=A8=20Make=20hook=20that=20can=20?= =?UTF-8?q?check=20route=20and=20nickname?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 현재 어떤 라우터에 있는지와 닉네임을 확인할 수 있는 훅 구현 --- .../src/shared/hooks/useCheckNickName.ts | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 packages/client/src/shared/hooks/useCheckNickName.ts diff --git a/packages/client/src/shared/hooks/useCheckNickName.ts b/packages/client/src/shared/hooks/useCheckNickName.ts new file mode 100644 index 0000000..40755ff --- /dev/null +++ b/packages/client/src/shared/hooks/useCheckNickName.ts @@ -0,0 +1,42 @@ +import { useEffect, useState } from 'react'; +import { useLocation } from 'react-router-dom'; +import { getSignInInfo } from 'shared/apis'; +import { getShareLinkHostNickName } from 'shared/apis/search'; + +export default function useCheckNickName() { + const location = useLocation(); + const [page, setPage] = useState(''); + const [nickName, setNickName] = useState(''); + + useEffect(() => { + const path = location.pathname.split('/')[1]; + const hostNickName = location.pathname.split('/')[2]; + + switch (path) { + case 'home': + setPage('home'); + (async () => { + const res = await getSignInInfo(); + setNickName(res.nickname); + })(); + break; + + case 'search': + setPage('search'); + setNickName(hostNickName); + break; + + case 'guest': + setPage('guest'); + (async () => { + const res = await getShareLinkHostNickName(hostNickName); + setNickName(res); + })(); + break; + default: + break; + } + }, [location]); + + return { page, nickName }; +} From 4ba4b390b8b31c48717275158da4a0a764042129 Mon Sep 17 00:00:00 2001 From: KimGaeun Date: Fri, 8 Dec 2023 10:34:35 +0900 Subject: [PATCH 011/108] =?UTF-8?q?=E2=9C=A8=20Seperate=20home,=20guest,?= =?UTF-8?q?=20search=20route?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 내 화면, 검색해서 들어간 화면, 링크 타고 들어간 화면 route를 분리함 - 각 컴포넌트에서 useCheckNickName을 사용해서 라우트와 닉네임을 확인함 --- .../client/src/entities/posts/ui/Posts.tsx | 14 +++--- packages/client/src/pages/Home/Home.tsx | 44 +++++------------- packages/client/src/shared/apis/login.ts | 45 +++++++------------ packages/client/src/shared/apis/share.ts | 9 ++++ packages/client/src/shared/hooks/index.ts | 1 - packages/client/src/shared/store/index.ts | 3 ++ .../src/widgets/loginModal/LoginModal.tsx | 27 ++++++----- .../client/src/widgets/underBar/UnderBar.tsx | 20 +++++---- .../client/src/widgets/upperBar/UpperBar.tsx | 43 +++++++++++++----- packages/server/src/board/board.controller.ts | 2 +- 10 files changed, 104 insertions(+), 104 deletions(-) diff --git a/packages/client/src/entities/posts/ui/Posts.tsx b/packages/client/src/entities/posts/ui/Posts.tsx index 68085e4..b50ded1 100644 --- a/packages/client/src/entities/posts/ui/Posts.tsx +++ b/packages/client/src/entities/posts/ui/Posts.tsx @@ -1,22 +1,18 @@ import Post from './Post'; import { useState } from 'react'; import { StarData } from 'shared/lib/types/star'; -import { useOwnerStore } from 'shared/store/useOwnerStore'; import { getPostListByNickName } from 'shared/apis/star'; import { useEffect } from 'react'; -import { useViewStore } from 'shared/store'; import { getMyPost } from '../apis/getMyPost'; +import useCheckNickName from 'shared/hooks/useCheckNickName'; export default function Posts() { const [postData, setPostData] = useState(); - const { isMyPage, pageOwnerNickName } = useOwnerStore(); - const { view } = useViewStore(); + const { page, nickName } = useCheckNickName(); useEffect(() => { - if (view !== 'MAIN') return; - - if (isMyPage) { + if (page === 'home') { (async () => { const myPostData = await getMyPost(); setPostData(myPostData); @@ -25,10 +21,10 @@ export default function Posts() { } (async () => { - const otherPostData = await getPostListByNickName(pageOwnerNickName); + const otherPostData = await getPostListByNickName(nickName); setPostData(otherPostData); })(); - }, [isMyPage, view]); + }, [page, nickName]); return ( <> diff --git a/packages/client/src/pages/Home/Home.tsx b/packages/client/src/pages/Home/Home.tsx index 2a0c8ae..be7099c 100644 --- a/packages/client/src/pages/Home/Home.tsx +++ b/packages/client/src/pages/Home/Home.tsx @@ -1,58 +1,37 @@ import Screen from 'widgets/screen/Screen'; import { useViewStore } from 'shared/store/useViewStore'; -import { Outlet, useNavigate } from 'react-router-dom'; +import { Outlet } from 'react-router-dom'; import UnderBar from 'widgets/underBar/UnderBar'; import UpperBar from '../../widgets/upperBar/UpperBar'; import WarpScreen from 'widgets/warpScreen/WarpScreen'; -import { useEffect, useState } from 'react'; -import instance from 'shared/apis/AxiosInterceptor'; +import { useEffect } from 'react'; import { useScreenSwitchStore } from 'shared/store/useScreenSwitchStore'; import Cookies from 'js-cookie'; -import { getSignInInfo } from 'shared/apis'; import { getGalaxy } from 'shared/apis'; -import { useGalaxyStore } from 'shared/store'; +import { useErrorStore, useGalaxyStore, useToastStore } from 'shared/store'; import { Toast } from 'shared/ui'; -import { useToastStore } from 'shared/store'; -import { useOwnerStore } from 'shared/store/useOwnerStore'; import { SPIRAL, GALAXY_THICKNESS, SPIRAL_START, ARMS_Z_DIST, } from 'widgets/galaxy/lib/constants'; +import Alert from 'shared/ui/alert/Alert'; +import useCheckNickName from 'shared/hooks/useCheckNickName'; export default function Home() { const { view } = useViewStore(); const { isSwitching } = useScreenSwitchStore(); const { text } = useToastStore(); - const { pageOwnerNickName } = useOwnerStore(); - const [nickname, setNickname] = useState(''); + const { message } = useErrorStore(); + const { nickName } = useCheckNickName(); - const navigate = useNavigate(); const { setSpiral, setStart, setThickness, setZDist } = useGalaxyStore(); useEffect(() => { - (async () => { - try { - const res = await instance({ - method: 'GET', - url: `/auth/check-signin`, - }); + Cookies.set('nickname', nickName); - if (res.status !== 200) navigate('/login'); - } catch (error) { - navigate('/login'); - } - })(); - - getSignInInfo().then((res) => { - Cookies.set('nickname', res.nickname); - setNickname(res.nickname); - }); - }, []); - - useEffect(() => { - getGalaxy(pageOwnerNickName).then((res) => { + getGalaxy(nickName).then((res) => { if (!res.spiral) setSpiral(SPIRAL); else setSpiral(res.spiral); @@ -65,19 +44,20 @@ export default function Home() { if (!res.zDist) setZDist(ARMS_Z_DIST); else setZDist(res.zDist); }); - }, [pageOwnerNickName]); + }, [nickName]); return ( <> + {message && } {isSwitching && } {text && {text}} {view === 'MAIN' && ( <> - + )} diff --git a/packages/client/src/shared/apis/login.ts b/packages/client/src/shared/apis/login.ts index 825c52d..6088345 100644 --- a/packages/client/src/shared/apis/login.ts +++ b/packages/client/src/shared/apis/login.ts @@ -1,36 +1,21 @@ -import axios, { AxiosError } from 'axios'; -import Cookies from 'js-cookie'; -import { NavigateFunction } from 'react-router-dom'; import instance from './AxiosInterceptor'; -axios.defaults.withCredentials = true; +interface PostLoginTypes { + username: string; + password: string; +} -export const postLogin = async ( - data: { - username: string; - password: string; - }, - setIdState: React.Dispatch>, - setPasswordState: React.Dispatch>, - navigate: NavigateFunction, - setIsSwitching: (value: boolean) => void, -) => { - try { - await instance({ - method: 'POST', - url: '/auth/signin', - data, - }); - Cookies.set('userId', data.username, { path: '/', expires: 7 }); - navigate('/home'); - setIsSwitching(true); - } catch (err) { - if (err instanceof AxiosError) { - if (err.response?.status === 404) setIdState(false); - else if (err.response?.status === 401) setPasswordState(false); - else alert(err); - } else alert(err); - } +export const postLogin = async ({ username, password }: PostLoginTypes) => { + const { data } = await instance({ + method: 'POST', + url: '/auth/signin', + data: { + username, + password, + }, + }); + + return data; }; export const getSignInInfo = async () => { diff --git a/packages/client/src/shared/apis/share.ts b/packages/client/src/shared/apis/share.ts index 53a84df..2c5bf2f 100644 --- a/packages/client/src/shared/apis/share.ts +++ b/packages/client/src/shared/apis/share.ts @@ -20,3 +20,12 @@ export const patchShareStatus = async (status: 'private' | 'public') => { return data; }; + +export const getShareLinkHostNickName = async (shareLink: string) => { + const { data } = await instance({ + method: 'GET', + url: `/auth/shareLink/${shareLink}`, + }); + + return data; +}; diff --git a/packages/client/src/shared/hooks/index.ts b/packages/client/src/shared/hooks/index.ts index 80fe8e2..3093c6f 100644 --- a/packages/client/src/shared/hooks/index.ts +++ b/packages/client/src/shared/hooks/index.ts @@ -1,3 +1,2 @@ export * from './useFetch'; export * from './useForwardRef'; -export * from './useCheckLogin'; diff --git a/packages/client/src/shared/store/index.ts b/packages/client/src/shared/store/index.ts index 7392599..41332f3 100644 --- a/packages/client/src/shared/store/index.ts +++ b/packages/client/src/shared/store/index.ts @@ -5,3 +5,6 @@ export * from './useViewStore'; export * from './useCustomStore'; export * from './useGalaxyStore'; export * from './usePostStore'; +export * from './useErrorStore'; +export * from './useOwnerStore'; +export * from './useScreenSwitchStore'; diff --git a/packages/client/src/widgets/loginModal/LoginModal.tsx b/packages/client/src/widgets/loginModal/LoginModal.tsx index b075038..62736b4 100644 --- a/packages/client/src/widgets/loginModal/LoginModal.tsx +++ b/packages/client/src/widgets/loginModal/LoginModal.tsx @@ -5,6 +5,7 @@ import Cookies from 'js-cookie'; import { useState } from 'react'; import { postLogin } from 'shared/apis'; import { useScreenSwitchStore } from 'shared/store/useScreenSwitchStore'; +import { AxiosError } from 'axios'; export default function LoginModal() { const [id, setId] = useState(Cookies.get('userId') ?? ''); @@ -20,18 +21,22 @@ export default function LoginModal() { const handleLoginSubmit = async () => { if (!isValid()) return; - const data = { - username: id, - password: password, - }; + setPassword(''); - await postLogin( - data, - setIdState, - setPasswordState, - navigate, - setIsSwitching, - ); + + try { + await postLogin({ username: id, password }); + + Cookies.set('userId', id, { path: '/', expires: 7 }); + navigate('/home'); + setIsSwitching(true); + } catch (err) { + if (err instanceof AxiosError) { + if (err.response?.status === 404) setIdState(false); + else if (err.response?.status === 401) setPasswordState(false); + else alert(err); + } else alert(err); + } }; return ( diff --git a/packages/client/src/widgets/underBar/UnderBar.tsx b/packages/client/src/widgets/underBar/UnderBar.tsx index 14310aa..1d23429 100644 --- a/packages/client/src/widgets/underBar/UnderBar.tsx +++ b/packages/client/src/widgets/underBar/UnderBar.tsx @@ -12,17 +12,21 @@ import { useNavigate } from 'react-router-dom'; import Cookies from 'js-cookie'; import instance from 'shared/apis/AxiosInterceptor'; import { useViewStore } from 'shared/store'; -import { useOwnerStore } from 'shared/store/useOwnerStore'; +import { useEffect, useState } from 'react'; +import useCheckNickName from 'shared/hooks/useCheckNickName'; -interface PropsType { - nickname: string; -} - -export default function UnderBar({ nickname }: PropsType) { +export default function UnderBar() { const navigate = useNavigate(); + const [isMyPage, setIsMyPage] = useState(true); const { setView } = useViewStore(); - const { isMyPage, pageOwnerNickName } = useOwnerStore(); + const { page, nickName } = useCheckNickName(); + + useEffect(() => { + if (!page) return; + if (page === 'home') return setIsMyPage(true); + setIsMyPage(false); + }, [page]); const handleLogoutButton = async () => { await instance.get(`${BASE_URL}auth/signout`); @@ -51,7 +55,7 @@ export default function UnderBar({ nickname }: PropsType) { return ( - {isMyPage ? nickname : pageOwnerNickName}님의 은하 + {nickName}님의 은하 diff --git a/packages/client/src/widgets/upperBar/UpperBar.tsx b/packages/client/src/widgets/upperBar/UpperBar.tsx index 9168463..ff9f1fd 100644 --- a/packages/client/src/widgets/upperBar/UpperBar.tsx +++ b/packages/client/src/widgets/upperBar/UpperBar.tsx @@ -5,8 +5,11 @@ import { MAX_WIDTH1, MAX_WIDTH2 } from '@constants'; import { useState, useEffect } from 'react'; import { getNickNames } from 'shared/apis/search'; import { useScreenSwitchStore } from 'shared/store/useScreenSwitchStore'; -import { useOwnerStore } from 'shared/store/useOwnerStore'; import Cookies from 'js-cookie'; +import { getIsAvailableNickName } from 'shared/apis'; +import { useErrorStore } from 'shared/store'; +import { useNavigate } from 'react-router-dom'; +import useCheckNickName from 'shared/hooks/useCheckNickName'; export default function UpperBar() { // TODO: ui 분리하기 @@ -14,14 +17,18 @@ export default function UpperBar() { const [debouncedSearchValue, setDebouncedSearchValue] = useState(''); const [searchResults, setSearchResults] = useState([]); - const { isMyPage, setIsMyPage } = useOwnerStore(); const { setIsSwitching } = useScreenSwitchStore(); - const { setPageOwnerNickName } = useOwnerStore(); + const { setMessage } = useErrorStore(); + const { page, nickName } = useCheckNickName(); const userNickName = Cookies.get('nickname'); + const navigate = useNavigate(); + const DEBOUNCE_TIME = 200; + useEffect(() => {}, [page, nickName]); + useEffect(() => { const debounce = setTimeout(() => { setDebouncedSearchValue(searchValue); @@ -48,22 +55,34 @@ export default function UpperBar() { }, [debouncedSearchValue]); const handleSearchButton = async () => { - setPageOwnerNickName(debouncedSearchValue); + try { + await getIsAvailableNickName(debouncedSearchValue); - setSearchValue(''); - setDebouncedSearchValue(''); - setSearchResults([]); + return setMessage('존재하지 않는 닉네임입니다.'); + } catch (error) { + if (debouncedSearchValue === userNickName) + return setMessage('내 은하로는 이동할 수 없습니다.'); - setIsMyPage(false); - setIsSwitching(true); + navigate(`/search/${debouncedSearchValue}`); + + setSearchValue(''); + setDebouncedSearchValue(''); + setSearchResults([]); + setIsSwitching(true); + } }; - const iconButtonVisibility = isMyPage ? 'hidden' : 'visible'; + const iconButtonVisibility = page === 'home' ? 'hidden' : 'visible'; const handleGoBackButton = () => { - setIsMyPage(true); + if (page === 'guest') { + navigate('/'); + setIsSwitching(true); + return; + } + + navigate('/home'); setIsSwitching(true); - setPageOwnerNickName(userNickName!); }; return ( diff --git a/packages/server/src/board/board.controller.ts b/packages/server/src/board/board.controller.ts index 89ff1d4..e61c74e 100644 --- a/packages/server/src/board/board.controller.ts +++ b/packages/server/src/board/board.controller.ts @@ -124,7 +124,7 @@ export class BoardController { const files = []; for (let image of found.images) { const file: Buffer = await this.boardService.downloadFile(image.filename); - console.log(file); + formData.append('file', file, { filename: image.filename, contentType: image.mimetype, From ba5a0c5575227f34e3bf295917784849ca09e1da Mon Sep 17 00:00:00 2001 From: KimGaeun Date: Fri, 8 Dec 2023 10:46:58 +0900 Subject: [PATCH 012/108] =?UTF-8?q?=F0=9F=90=9B=20Fix=20import?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/client/src/shared/hooks/useCheckNickName.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/shared/hooks/useCheckNickName.ts b/packages/client/src/shared/hooks/useCheckNickName.ts index 40755ff..d048ec1 100644 --- a/packages/client/src/shared/hooks/useCheckNickName.ts +++ b/packages/client/src/shared/hooks/useCheckNickName.ts @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { useLocation } from 'react-router-dom'; import { getSignInInfo } from 'shared/apis'; -import { getShareLinkHostNickName } from 'shared/apis/search'; +import { getShareLinkHostNickName } from 'shared/apis/share'; export default function useCheckNickName() { const location = useLocation(); From 2e6250471f2309a74e4c03cf8b44ff035502d7ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=A4=80=EC=84=AD?= Date: Fri, 8 Dec 2023 16:18:21 +0900 Subject: [PATCH 013/108] :test_tube: Add Auth E2E Test todo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Auth E2E 테스트 it.todo 작성 --- packages/server/test/auth/auth.e2e-spec.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/server/test/auth/auth.e2e-spec.ts b/packages/server/test/auth/auth.e2e-spec.ts index 3f1f197..eb3230e 100644 --- a/packages/server/test/auth/auth.e2e-spec.ts +++ b/packages/server/test/auth/auth.e2e-spec.ts @@ -188,4 +188,12 @@ describe('AuthController (/auth, e2e)', () => { 'accessToken=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure; SameSite=None', ); }); + + it.todo('GET /auth/search'); + + it.todo('PATCH /auth/status'); + + it.todo('GET /auth/sharelink'); + + it.todo('GET /auth/sharelink/:sharelink'); }); From c3229177138509335d3997e12afe195cb6e39a45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=A4=80=EC=84=AD?= Date: Fri, 8 Dec 2023 17:26:32 +0900 Subject: [PATCH 014/108] :white_check_mark: Add Search Api Test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Search Api 테스트 통과하도록 구현 --- packages/server/test/auth/auth.e2e-spec.ts | 49 +++++++++++++++++++- packages/server/test/board/board.e2e-spec.ts | 1 - 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/packages/server/test/auth/auth.e2e-spec.ts b/packages/server/test/auth/auth.e2e-spec.ts index eb3230e..0f410e7 100644 --- a/packages/server/test/auth/auth.e2e-spec.ts +++ b/packages/server/test/auth/auth.e2e-spec.ts @@ -189,7 +189,54 @@ describe('AuthController (/auth, e2e)', () => { ); }); - it.todo('GET /auth/search'); + it('GET /auth/search', async () => { + const randomeBytes = Math.random().toString(36).slice(2, 10); + + const newUser = { + username: randomeBytes, + nickname: randomeBytes, + password: randomeBytes, + }; + + await request(app.getHttpServer()).post('/auth/signup').send(newUser); + + const includeResponse1 = request(app.getHttpServer()).get( + `/auth/search?nickname=${randomeBytes.slice(0, 3)}`, + ); + const includeResponse2 = request(app.getHttpServer()).get( + `/auth/search?nickname=${randomeBytes.slice(0, 4)}`, + ); + const includeResponse3 = request(app.getHttpServer()).get( + `/auth/search?nickname=${randomeBytes}`, + ); + const includeResult = await Promise.all([ + includeResponse1, + includeResponse2, + includeResponse3, + ]); + includeResult.forEach((response) => { + expect(response).toHaveProperty('body'); + const users = response.body; + expect(users.length).toBe(1); + expect(users[0]['nickname']).toBe(randomeBytes); + }); + + const excludeResponse1 = request(app.getHttpServer()).get( + `/auth/search?nickname=${randomeBytes}123124`, + ); + const excludeResponse2 = request(app.getHttpServer()).get( + `/auth/search?nickname=123124${randomeBytes}`, + ); + const excludeResult = await Promise.all([ + excludeResponse1, + excludeResponse2, + ]); + excludeResult.forEach((response) => { + expect(response).toHaveProperty('body'); + const users = response.body; + expect(users.length).toBe(0); + }); + }); it.todo('PATCH /auth/status'); diff --git a/packages/server/test/board/board.e2e-spec.ts b/packages/server/test/board/board.e2e-spec.ts index ab27743..a082a51 100644 --- a/packages/server/test/board/board.e2e-spec.ts +++ b/packages/server/test/board/board.e2e-spec.ts @@ -2,7 +2,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import * as request from 'supertest'; import { AppModule } from '../../src/app.module'; -import { Board } from '../../src/board/entities/board.entity'; import { UpdateBoardDto } from '../../src/board/dto/update-board.dto'; import { CreateBoardDto } from '../../src/board/dto/create-board.dto'; import * as cookieParser from 'cookie-parser'; From cd5d49dd9ad631d8a0a07ff785d95ba319032d4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=A4=80=EC=84=AD?= Date: Fri, 8 Dec 2023 17:29:51 +0900 Subject: [PATCH 015/108] :white_check_mark: Add Patch Status Api Test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Status 변경 Api 테스트 통과하도록 구현 --- packages/server/test/auth/auth.e2e-spec.ts | 40 +++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/packages/server/test/auth/auth.e2e-spec.ts b/packages/server/test/auth/auth.e2e-spec.ts index 0f410e7..0cbb975 100644 --- a/packages/server/test/auth/auth.e2e-spec.ts +++ b/packages/server/test/auth/auth.e2e-spec.ts @@ -238,7 +238,45 @@ describe('AuthController (/auth, e2e)', () => { }); }); - it.todo('PATCH /auth/status'); + it('PATCH /auth/status', async () => { + const randomeBytes = Math.random().toString(36).slice(2, 10); + + const newUser = { + username: randomeBytes, + nickname: randomeBytes, + password: randomeBytes, + }; + + await request(app.getHttpServer()).post('/auth/signup').send(newUser); + + const signInResponse = await request(app.getHttpServer()) + .post('/auth/signin') + .send(newUser); + + let accessToken: string; + signInResponse.headers['set-cookie'].forEach((cookie: string) => { + if (cookie.includes('accessToken')) { + accessToken = cookie.split(';')[0].split('=')[1]; + } + }); + + const successResponse = await request(app.getHttpServer()) + .patch('/auth/status') + .set('Cookie', [`accessToken=${accessToken}`]) + .send({ status: 'private' }) + .expect(200); + + expect(successResponse).toHaveProperty('body'); + const user = successResponse.body; + expect(user).toHaveProperty('status'); + expect(user.status).toBe('private'); + + await request(app.getHttpServer()) + .patch('/auth/status') + .set('Cookie', [`accessToken=${accessToken}`]) + .send({ status: 'invalid status' }) + .expect(400); + }); it.todo('GET /auth/sharelink'); From 488f46ed890052b69223b80f15f2d7314ed2a5b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=A4=80=EC=84=AD?= Date: Fri, 8 Dec 2023 18:20:33 +0900 Subject: [PATCH 016/108] :bug: Fix Sharelink Api MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 공유 링크 엔티티 eager 옵션 관련 에러 해결 --- packages/server/src/auth/auth.module.ts | 2 +- packages/server/src/auth/auth.service.ts | 28 +++++++++---------- ...re_link.entity.ts => share-link.entity.ts} | 4 +-- .../server/src/auth/entities/user.entity.ts | 2 +- 4 files changed, 18 insertions(+), 18 deletions(-) rename packages/server/src/auth/entities/{share_link.entity.ts => share-link.entity.ts} (84%) diff --git a/packages/server/src/auth/auth.module.ts b/packages/server/src/auth/auth.module.ts index ad1cde9..601d6d9 100644 --- a/packages/server/src/auth/auth.module.ts +++ b/packages/server/src/auth/auth.module.ts @@ -8,7 +8,7 @@ import { JwtModule } from '@nestjs/jwt'; import { jwtConfig } from '../config/jwt.config'; import { RedisRepository } from './redis.repository'; import { CookieAuthGuard } from './cookie-auth.guard'; -import { ShareLink } from './entities/share_link.entity'; +import { ShareLink } from './entities/share-link.entity'; import { MongooseModule } from '@nestjs/mongoose'; import { Galaxy, GalaxySchema } from '../galaxy/schemas/galaxy.schema'; import { diff --git a/packages/server/src/auth/auth.service.ts b/packages/server/src/auth/auth.service.ts index ed39113..f19d3b5 100644 --- a/packages/server/src/auth/auth.service.ts +++ b/packages/server/src/auth/auth.service.ts @@ -24,7 +24,7 @@ import { } from '../util/auth.util'; import { v4 as uuid } from 'uuid'; import { UserDataDto } from './dto/user-data.dto'; -import { ShareLink } from './entities/share_link.entity'; +import { ShareLink } from './entities/share-link.entity'; import { InjectModel } from '@nestjs/mongoose'; import { Galaxy } from '../galaxy/schemas/galaxy.schema'; import { Model } from 'mongoose'; @@ -253,33 +253,33 @@ export class AuthService { throw new BadRequestException('nickname is required'); } - const user = await this.userRepository.findOneBy({ nickname }); + const user: User = await this.userRepository.findOneBy({ nickname }); if (!user) { throw new NotFoundException('user not found'); } - const foundLink = await this.shareLinkRepository.findOneBy({ - user: user.id, + const shareLink = await this.shareLinkRepository.findOneBy({ + user: { id: user.id }, }); - if (foundLink) { - return foundLink; + if (shareLink) { + return shareLink.link; } - const newLink = this.shareLinkRepository.create({ - user: user.id, + const newShareLink = this.shareLinkRepository.create({ link: uuid(), + user: user, }); - const savedLink = await this.shareLinkRepository.save(newLink); - savedLink.user = undefined; - return savedLink; + const savedShareLink = await this.shareLinkRepository.save(newShareLink); + return savedShareLink.link; } async getUsernameByShareLink(shareLink: string) { - const foundLink = await this.shareLinkRepository.findOneBy({ - link: shareLink, + const foundLink = await this.shareLinkRepository.findOne({ + where: { link: shareLink }, + relations: ['user'], }); if (!foundLink) { @@ -287,7 +287,7 @@ export class AuthService { } const linkUser = await this.userRepository.findOneBy({ - id: foundLink.user, + id: foundLink.user.id, }); if (!linkUser) { diff --git a/packages/server/src/auth/entities/share_link.entity.ts b/packages/server/src/auth/entities/share-link.entity.ts similarity index 84% rename from packages/server/src/auth/entities/share_link.entity.ts rename to packages/server/src/auth/entities/share-link.entity.ts index ca061b1..781b301 100644 --- a/packages/server/src/auth/entities/share_link.entity.ts +++ b/packages/server/src/auth/entities/share-link.entity.ts @@ -15,10 +15,10 @@ export class ShareLink { @Column({ type: 'varchar', length: 200, nullable: false, unique: true }) link: string; - @OneToOne(() => User, (user) => user.id, { + @OneToOne(() => User, (user) => user.shareLink, { eager: false, onDelete: 'CASCADE', }) @JoinColumn() - user: number; + user: User; } diff --git a/packages/server/src/auth/entities/user.entity.ts b/packages/server/src/auth/entities/user.entity.ts index 7ee627b..af2a06b 100644 --- a/packages/server/src/auth/entities/user.entity.ts +++ b/packages/server/src/auth/entities/user.entity.ts @@ -8,7 +8,7 @@ import { OneToOne, PrimaryGeneratedColumn, } from 'typeorm'; -import { ShareLink } from './share_link.entity'; +import { ShareLink } from './share-link.entity'; import { UserShareStatus } from '../enums/user.enum'; @Entity() From 96883c70a7c74d3071bebfcea2403d971537d603 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=A4=80=EC=84=AD?= Date: Fri, 8 Dec 2023 18:21:04 +0900 Subject: [PATCH 017/108] :white_check_mark: Add Sharelink Test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Sharelink API 테스트 작성 --- packages/server/test/auth/auth.e2e-spec.ts | 26 ++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/server/test/auth/auth.e2e-spec.ts b/packages/server/test/auth/auth.e2e-spec.ts index 0cbb975..821e36d 100644 --- a/packages/server/test/auth/auth.e2e-spec.ts +++ b/packages/server/test/auth/auth.e2e-spec.ts @@ -278,7 +278,29 @@ describe('AuthController (/auth, e2e)', () => { .expect(400); }); - it.todo('GET /auth/sharelink'); + it('GET /auth/sharelink', async () => { + const randomeBytes = Math.random().toString(36).slice(2, 10); + + const newUser = { + username: randomeBytes, + nickname: randomeBytes, + password: randomeBytes, + }; + + await request(app.getHttpServer()).post('/auth/signup').send(newUser); + + const response = await request(app.getHttpServer()) + .get(`/auth/sharelink?nickname=${randomeBytes}`) + .expect(200); - it.todo('GET /auth/sharelink/:sharelink'); + expect(response).toHaveProperty('text'); + const sharelink = response.text; + + const sharelinkResponse = await request(app.getHttpServer()) + .get(`/auth/sharelink/${sharelink}`) + .expect(200); + + const nickname = sharelinkResponse.text; + expect(nickname).toBe(randomeBytes); + }); }); From aeba09a127c8ba4b043be0994cc23ac91172543f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=A4=80=EC=84=AD?= Date: Fri, 8 Dec 2023 20:35:19 +0900 Subject: [PATCH 018/108] :white_check_mark: Set Auth Controller Test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Auth Controller 태스트를 위해 모듈 설정 --- .../server/test/auth/auth.controller.spec.ts | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/server/test/auth/auth.controller.spec.ts b/packages/server/test/auth/auth.controller.spec.ts index 994027f..1f42637 100644 --- a/packages/server/test/auth/auth.controller.spec.ts +++ b/packages/server/test/auth/auth.controller.spec.ts @@ -1,6 +1,15 @@ import { Test, TestingModule } from '@nestjs/testing'; import { AuthController } from '../../src/auth/auth.controller'; import { AuthService } from '../../src/auth/auth.service'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { User } from '../../src/auth/entities/user.entity'; +import { ShareLink } from '../../src/auth/entities/share-link.entity'; +import { JwtService } from '@nestjs/jwt'; +import { Galaxy } from '../../src/galaxy/schemas/galaxy.schema'; +import { Model } from 'mongoose'; +import { getModelToken } from '@nestjs/mongoose'; +import { RedisRepository } from '../../src/auth/redis.repository'; describe('AuthController', () => { let controller: AuthController; @@ -8,7 +17,27 @@ describe('AuthController', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [AuthController], - providers: [AuthService], + providers: [ + AuthService, + { + provide: getRepositoryToken(User), + useClass: Repository, + }, + { + provide: getRepositoryToken(ShareLink), + useClass: Repository, + }, + RedisRepository, + JwtService, + { + provide: getModelToken(Galaxy.name), + useValue: Model, + }, + { + provide: getModelToken('Exception'), + useValue: Model, + }, + ], }).compile(); controller = module.get(AuthController); From c6faba7a8984aa13cbedfc5d0387775ab6c8aaf0 Mon Sep 17 00:00:00 2001 From: MinboyKim Date: Fri, 8 Dec 2023 23:06:24 +0900 Subject: [PATCH 019/108] =?UTF-8?q?=F0=9F=92=84=20BGM=20button=20style?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 배경음악 버튼 스타일 --- packages/client/src/widgets/underBar/UnderBar.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/client/src/widgets/underBar/UnderBar.tsx b/packages/client/src/widgets/underBar/UnderBar.tsx index c3bcce2..2196676 100644 --- a/packages/client/src/widgets/underBar/UnderBar.tsx +++ b/packages/client/src/widgets/underBar/UnderBar.tsx @@ -15,6 +15,7 @@ import { useViewStore } from 'shared/store'; import { useOwnerStore } from 'shared/store/useOwnerStore'; import { usePlayingStore } from 'shared/store/useAudioStore'; import { useGalaxyStore } from 'shared/store'; +import { Share2, Volume2, VolumeX } from 'lucide-react'; interface PropsType { nickname: string; @@ -25,7 +26,7 @@ export default function UnderBar({ nickname }: PropsType) { const { setView } = useViewStore(); const { isMyPage, pageOwnerNickName } = useOwnerStore(); - const { setPlaying } = usePlayingStore(); + const { playing, setPlaying } = usePlayingStore(); const { reset } = useGalaxyStore(); const handleLogoutButton = async () => { @@ -68,9 +69,10 @@ export default function UnderBar({ nickname }: PropsType) { size="m" buttonType="Button" disabled={!isMyPage} - onClick={handleShareButton} + onClick={() => setPlaying()} + style={{ width: '74.5px' }} > - 공유하기 + {playing ? : } @@ -94,10 +96,10 @@ export default function UnderBar({ nickname }: PropsType) { size="l" buttonType="Button" disabled={!isMyPage} - onClick={() => setPlaying()} + onClick={handleShareButton} > - 별 스킨 만들기 - 별 스킨 만들기 + + 공유하기 Date: Sat, 9 Dec 2023 00:13:55 +0900 Subject: [PATCH 020/108] :white_check_mark: Add check-signin Test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Auth E2E 테스트에 check-signin 테스트 추가 --- packages/server/test/auth/auth.e2e-spec.ts | 33 ++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/packages/server/test/auth/auth.e2e-spec.ts b/packages/server/test/auth/auth.e2e-spec.ts index 821e36d..337081f 100644 --- a/packages/server/test/auth/auth.e2e-spec.ts +++ b/packages/server/test/auth/auth.e2e-spec.ts @@ -146,6 +146,39 @@ describe('AuthController (/auth, e2e)', () => { .expect(401); }); + it('GET /auth/check-signin', async () => { + const randomeBytes = Math.random().toString(36).slice(2, 10); + + const newUser = { + username: randomeBytes, + nickname: randomeBytes, + password: randomeBytes, + }; + + await request(app.getHttpServer()).post('/auth/signup').send(newUser); + + const signInResponse = await request(app.getHttpServer()) + .post('/auth/signin') + .send(newUser); + + const accessToken = signInResponse.headers['set-cookie'][0] + .split(';')[0] + .split('=')[1]; + + const response = await request(app.getHttpServer()) + .get('/auth/check-signin') + .set('Cookie', [`accessToken=${accessToken}`]) + .expect(200); + + expect(response).toHaveProperty('body'); + const user = response.body; + console.log(user); + expect(user).toHaveProperty('username'); + expect(user.username).toBe(newUser.username); + expect(user).toHaveProperty('nickname'); + expect(user.nickname).toBe(newUser.nickname); + }); + // #33 [05-02] 로그아웃 요청을 받으면 토큰을 읽어 해당 회원의 로그인 여부를 확인한다. // #34 [05-03] 로그인을 하지 않은 사용자의 요청이라면 BadRequest 에러를 반환한다. // #35 [05-04] 로그인을 한 사용자라면 Redis의 Refresh Token 정보를 삭제한다. From a102eb5734f87cc4f2404b782d03827ba861b91d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EC=A4=80=EC=84=AD?= Date: Sat, 9 Dec 2023 00:28:56 +0900 Subject: [PATCH 021/108] :white_check_mark: Add signup Unit Test of Auth Controller MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Auth Controller signup 유닛 테스트 추가 --- .../server/test/auth/auth.controller.spec.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/packages/server/test/auth/auth.controller.spec.ts b/packages/server/test/auth/auth.controller.spec.ts index 1f42637..d6b5f94 100644 --- a/packages/server/test/auth/auth.controller.spec.ts +++ b/packages/server/test/auth/auth.controller.spec.ts @@ -13,6 +13,7 @@ import { RedisRepository } from '../../src/auth/redis.repository'; describe('AuthController', () => { let controller: AuthController; + let service: AuthService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -41,9 +42,35 @@ describe('AuthController', () => { }).compile(); controller = module.get(AuthController); + service = module.get(AuthService); }); it('should be defined', () => { expect(controller).toBeDefined(); }); + + it('POST /auth/signup', async () => { + expect(controller.signUp).toBeDefined(); + + jest.spyOn(service, 'signUp').mockImplementation(async () => { + return { + id: 1, + username: 'test', + nickname: 'test', + }; + }); + + const result = await controller.signUp({ + username: 'test', + nickname: 'test', + password: 'test', + }); + + expect(result).toBeDefined(); + expect(result).toHaveProperty('id'); + expect(result).toHaveProperty('username'); + expect(result.username).toBe('test'); + expect(result).toHaveProperty('nickname'); + expect(result.nickname).toBe('test'); + }); }); From 108b3866768c9dc3dd778bc520c6a44227fc47c9 Mon Sep 17 00:00:00 2001 From: KimGaeun Date: Sat, 9 Dec 2023 11:23:19 +0900 Subject: [PATCH 022/108] =?UTF-8?q?=F0=9F=90=9B=20Fix=20PostModal=20error?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 다른 사람 은하에서 별 클릭했을 때 내 은하로 이동하는 에러 해결 --- .../client/src/entities/posts/ui/Post.tsx | 15 ++++++---- .../src/features/postModal/ui/PostModal.tsx | 29 ++++++++++++------- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/packages/client/src/entities/posts/ui/Post.tsx b/packages/client/src/entities/posts/ui/Post.tsx index 002decc..5a3d6b6 100644 --- a/packages/client/src/entities/posts/ui/Post.tsx +++ b/packages/client/src/entities/posts/ui/Post.tsx @@ -6,7 +6,7 @@ import styled from '@emotion/styled'; import { useViewStore } from 'shared/store/useViewStore'; import * as THREE from 'three'; import { StarType } from 'shared/lib/types/star'; -import { useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import { useState } from 'react'; import theme from 'shared/ui/styles/theme'; import Star from 'features/star/Star'; @@ -19,25 +19,30 @@ interface PropsType { export default function Post({ data, postId, title }: PropsType) { const { targetView, setTargetView } = useCameraStore(); - const { view, setView } = useViewStore(); + const { setView } = useViewStore(); const meshRef = useRef(null!); const [isHovered, setIsHovered] = useState(false); const navigate = useNavigate(); + const location = useLocation(); const handleMeshClick = (e: ThreeEvent) => { e.stopPropagation(); + const splitedPath = location.pathname.split('/'); + const page = splitedPath[1]; + const nickName = splitedPath[2]; + const path = '/' + page + '/' + nickName + '/'; + if (meshRef.current !== targetView) { setView('DETAIL'); setTargetView(meshRef.current); - navigate(`/home/${postId}`); - return; + return navigate(path + postId); } setView('POST'); - navigate(`/home/${postId}/detail`); + navigate(path + postId + '/detail'); }; const handlePointerOver = (e: ThreeEvent) => { diff --git a/packages/client/src/features/postModal/ui/PostModal.tsx b/packages/client/src/features/postModal/ui/PostModal.tsx index f772481..4513fcd 100644 --- a/packages/client/src/features/postModal/ui/PostModal.tsx +++ b/packages/client/src/features/postModal/ui/PostModal.tsx @@ -5,7 +5,7 @@ import remarkGfm from 'remark-gfm'; import styled from '@emotion/styled'; import { useEffect, useState } from 'react'; import AlertDialog from 'shared/ui/alertDialog/AlertDialog'; -import { useNavigate, useParams } from 'react-router-dom'; +import { useLocation, useNavigate, useParams } from 'react-router-dom'; import { useFetch } from 'shared/hooks'; import { PostData } from 'shared/lib/types/post'; import { deletePost } from '../api/deletePost'; @@ -16,15 +16,18 @@ import instance from 'shared/apis/AxiosInterceptor'; import { useToastStore } from 'shared/store'; export default function PostModal() { - const { setView } = useViewStore(); const [deleteModal, setDeleteModal] = useState(false); - const { postId } = useParams(); - const navigate = useNavigate(); - const { data, refetch } = useFetch(`post/${postId}`); const [isEdit, setIsEdit] = useState(false); const [content, setContent] = useState(''); const [title, setTitle] = useState(''); + const { setText } = useToastStore(); + const { setView } = useViewStore(); + const { postId } = useParams(); + const { data, refetch } = useFetch(`post/${postId}`); + + const navigate = useNavigate(); + const location = useLocation(); useEffect(() => { setContent(data?.content ?? ''); @@ -109,12 +112,21 @@ export default function PostModal() { setText('글을 삭제했습니다.'); setView('MAIN'); navigate('/home'); - // window.location.reload(); } else { setText('글 삭제에 실패했습니다.'); } }; + const handleGoBackButton = () => { + const splitedPath = location.pathname.split('/'); + const page = splitedPath[1]; + const nickName = splitedPath[2]; + const path = '/' + page + '/' + nickName + '/'; + + setView('MAIN'); + navigate(path + postId); + }; + return ( data && ( @@ -125,10 +137,7 @@ export default function PostModal() { leftButton={ isEdit ? null : } - onClickGoBack={() => { - setView('MAIN'); - navigate(`/home/${postId}`); - }} + onClickGoBack={handleGoBackButton} > {data.images.length > 0 && !isEdit && ( From d79bcac65aff59383cdb0f18051ff667c9328b7e Mon Sep 17 00:00:00 2001 From: KimGaeun Date: Sat, 9 Dec 2023 14:38:56 +0900 Subject: [PATCH 023/108] =?UTF-8?q?=F0=9F=92=84=20Change=20Underbar=20desi?= =?UTF-8?q?gn?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 별 스킨 만들기 버튼 삭제 - 내 우주가 아니면 버튼이 disabled되는게 아니라 아예 안보이도록 변경 --- .../client/src/widgets/underBar/UnderBar.tsx | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/packages/client/src/widgets/underBar/UnderBar.tsx b/packages/client/src/widgets/underBar/UnderBar.tsx index aa00e53..fed192d 100644 --- a/packages/client/src/widgets/underBar/UnderBar.tsx +++ b/packages/client/src/widgets/underBar/UnderBar.tsx @@ -2,11 +2,7 @@ import styled from '@emotion/styled'; import { Button } from 'shared/ui'; import { Title01 } from '../../shared/ui/styles'; import PlanetEditIcon from '@icons/icon-planetedit-24-white.svg'; -import PlanetEditIconGray from '@icons/icon-planetedit-24-gray.svg'; -import AddIcon from '@icons/icon-add-24-white.svg'; -import AddIconGray from '@icons/icon-add-24-gray.svg'; import WriteIcon from '@icons/icon-writte-24-white.svg'; -import WriteIconGray from '@icons/icon-writte-24-gray.svg'; import { BASE_URL, MAX_WIDTH1, MAX_WIDTH2 } from 'shared/lib/constants'; import { useNavigate } from 'react-router-dom'; import Cookies from 'js-cookie'; @@ -72,30 +68,27 @@ export default function UnderBar() { - + - 은하 수정하기 + 은하 수정하기 은하 수정하기 - 별 스킨 만들기 별 스킨 만들기 - + */} - 글쓰기 + 글쓰기 글쓰기 @@ -153,7 +146,9 @@ const ButtonsContainer = styled.div` const SmallButtonsContainer = styled.div` display: flex; flex-direction: column; + justify-content: center; gap: 4px; + height: 76px; `; const BigButtonsContainer = styled.div` From 46c26564d8fd9984099a85e32723d3b54d9e5799 Mon Sep 17 00:00:00 2001 From: KimGaeun Date: Sat, 9 Dec 2023 14:49:17 +0900 Subject: [PATCH 024/108] =?UTF-8?q?=F0=9F=90=9B=20Fix=20Posts=20useless=20?= =?UTF-8?q?api=20connect?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Posts에서 조건문이 잘못되어 잘못 호출되고 있는 api 수정 - 기존에는 home이 아닐 때 호출되도록 했는데, share이나 search일 때 호출되도록 명시해줌 --- packages/client/src/entities/posts/ui/Posts.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/client/src/entities/posts/ui/Posts.tsx b/packages/client/src/entities/posts/ui/Posts.tsx index 22e0c35..1c03ece 100644 --- a/packages/client/src/entities/posts/ui/Posts.tsx +++ b/packages/client/src/entities/posts/ui/Posts.tsx @@ -19,10 +19,12 @@ export default function Posts() { return; } - (async () => { - const otherPostData = await getPostListByNickName(nickName); - setPostData(otherPostData); - })(); + if (page === 'search' || page === 'share') { + (async () => { + const otherPostData = await getPostListByNickName(nickName); + setPostData(otherPostData); + })(); + } }, [page, nickName]); return ( From 3849d5c6b7eca2ddfee7bc94c28c17319bf34e0c Mon Sep 17 00:00:00 2001 From: KimGaeun Date: Sat, 9 Dec 2023 14:54:59 +0900 Subject: [PATCH 025/108] =?UTF-8?q?=F0=9F=A9=B9=20Edit=20MAX=5FWIDTH2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 반응형 디자인에서 조금 더 줄어들 수 있도록 width값 수정 --- packages/client/src/shared/lib/constants/width.ts | 2 +- packages/client/src/widgets/underBar/UnderBar.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/client/src/shared/lib/constants/width.ts b/packages/client/src/shared/lib/constants/width.ts index b6a62d3..8c602a9 100644 --- a/packages/client/src/shared/lib/constants/width.ts +++ b/packages/client/src/shared/lib/constants/width.ts @@ -1,2 +1,2 @@ export const MAX_WIDTH1 = 1210; -export const MAX_WIDTH2 = 930; +export const MAX_WIDTH2 = 810; diff --git a/packages/client/src/widgets/underBar/UnderBar.tsx b/packages/client/src/widgets/underBar/UnderBar.tsx index fed192d..81cce22 100644 --- a/packages/client/src/widgets/underBar/UnderBar.tsx +++ b/packages/client/src/widgets/underBar/UnderBar.tsx @@ -135,7 +135,7 @@ const Layout = styled.div` } @media (max-width: ${MAX_WIDTH2}px) { - width: 900px; + width: ${MAX_WIDTH2 - 30}px; } `; From c7e4bbf928539af06f68dfdd873c6157a29cf547 Mon Sep 17 00:00:00 2001 From: KimGaeun Date: Sat, 9 Dec 2023 15:07:39 +0900 Subject: [PATCH 026/108] =?UTF-8?q?=F0=9F=8E=A8=20Change=20code=20to=20ear?= =?UTF-8?q?ly=20return=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/client/src/entities/posts/ui/Posts.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/client/src/entities/posts/ui/Posts.tsx b/packages/client/src/entities/posts/ui/Posts.tsx index 1c03ece..1fc9ba9 100644 --- a/packages/client/src/entities/posts/ui/Posts.tsx +++ b/packages/client/src/entities/posts/ui/Posts.tsx @@ -11,6 +11,8 @@ export default function Posts() { const { page, nickName } = useCheckNickName(); useEffect(() => { + if (!page) return; + if (page === 'home') { (async () => { const myPostData = await getMyPost(); @@ -19,12 +21,10 @@ export default function Posts() { return; } - if (page === 'search' || page === 'share') { - (async () => { - const otherPostData = await getPostListByNickName(nickName); - setPostData(otherPostData); - })(); - } + (async () => { + const otherPostData = await getPostListByNickName(nickName); + setPostData(otherPostData); + })(); }, [page, nickName]); return ( From a5faa4ebe78b82b55b4032e65633f4f548f7cadd Mon Sep 17 00:00:00 2001 From: KimGaeun Date: Sat, 9 Dec 2023 15:13:01 +0900 Subject: [PATCH 027/108] =?UTF-8?q?=F0=9F=8E=A8=20Add=20finally=20to=20Pub?= =?UTF-8?q?licRoute,=20PrivateRoute?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - try와 catch에 공통적으로 쓰이던 코드를 finally로 옮김 --- packages/client/src/shared/routes/PrivateRoute.tsx | 2 +- packages/client/src/shared/routes/PublicRoute.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/client/src/shared/routes/PrivateRoute.tsx b/packages/client/src/shared/routes/PrivateRoute.tsx index 20ca741..486b7a8 100644 --- a/packages/client/src/shared/routes/PrivateRoute.tsx +++ b/packages/client/src/shared/routes/PrivateRoute.tsx @@ -11,9 +11,9 @@ export default function PrivateRoute() { try { await getSignInInfo(); setIsAuthenticated(true); - setIsLoading(false); } catch (error) { setIsAuthenticated(false); + } finally { setIsLoading(false); } })(); diff --git a/packages/client/src/shared/routes/PublicRoute.tsx b/packages/client/src/shared/routes/PublicRoute.tsx index 76e6264..0c0a21a 100644 --- a/packages/client/src/shared/routes/PublicRoute.tsx +++ b/packages/client/src/shared/routes/PublicRoute.tsx @@ -11,9 +11,9 @@ export default function PublicRoute() { try { await getSignInInfo(); setIsAuthenticated(true); - setIsLoading(false); } catch (error) { setIsAuthenticated(false); + } finally { setIsLoading(false); } })(); From baf569f30c36090260f773a0e35dd680a14448d7 Mon Sep 17 00:00:00 2001 From: MinboyKim Date: Sat, 9 Dec 2023 15:19:56 +0900 Subject: [PATCH 028/108] =?UTF-8?q?=E2=9C=A8=20Sentiment=20analysis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 감정 분석 버튼 구현 --- .../client/src/shared/store/useAudioStore.ts | 2 +- .../starCustomModal/StarCustomModal.tsx | 2 + .../starCustomModal/apis/getSentimentColor.ts | 11 +++ .../starCustomModal/ui/SentimentButton.tsx | 74 +++++++++++++++++++ .../client/src/widgets/underBar/UnderBar.tsx | 2 - 5 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 packages/client/src/widgets/starCustomModal/apis/getSentimentColor.ts create mode 100644 packages/client/src/widgets/starCustomModal/ui/SentimentButton.tsx diff --git a/packages/client/src/shared/store/useAudioStore.ts b/packages/client/src/shared/store/useAudioStore.ts index 77bda17..613c8eb 100644 --- a/packages/client/src/shared/store/useAudioStore.ts +++ b/packages/client/src/shared/store/useAudioStore.ts @@ -6,6 +6,6 @@ interface PlayingState { } export const usePlayingStore = create((set, get) => ({ - playing: true, + playing: false, setPlaying: () => set({ playing: !get().playing }), })); diff --git a/packages/client/src/widgets/starCustomModal/StarCustomModal.tsx b/packages/client/src/widgets/starCustomModal/StarCustomModal.tsx index 831f3f3..b8243de 100644 --- a/packages/client/src/widgets/starCustomModal/StarCustomModal.tsx +++ b/packages/client/src/widgets/starCustomModal/StarCustomModal.tsx @@ -19,6 +19,7 @@ import BrightnessSlider from './ui/BrightnessSlider'; import { getRandomFloat } from '@utils/random'; import { ARMS_X_DIST } from 'widgets/galaxy/lib/constants'; import { shapeTypes } from '@constants'; +import SentimentButton from './ui/SentimentButton'; export default function StarCustomModal() { const { setView } = useViewStore(); @@ -91,6 +92,7 @@ export default function StarCustomModal() { + { + const response = await instance({ + method: 'POST', + url: '/sentiment', + data: { content }, + }); + + return response; +}; diff --git a/packages/client/src/widgets/starCustomModal/ui/SentimentButton.tsx b/packages/client/src/widgets/starCustomModal/ui/SentimentButton.tsx new file mode 100644 index 0000000..7da5299 --- /dev/null +++ b/packages/client/src/widgets/starCustomModal/ui/SentimentButton.tsx @@ -0,0 +1,74 @@ +import styled from '@emotion/styled'; +import { useState } from 'react'; +import { Button } from 'shared/ui'; +import { Body01ME } from 'shared/ui/styles'; +import { getSentimentColor } from '../apis/getSentimentColor'; +import { HelpCircle } from 'lucide-react'; + +interface PropsType { + content: string; + setColor: React.Dispatch>; +} + +export default function SentimentButton({ content, setColor }: PropsType) { + const [isHover, setIsHover] = useState(false); + + const handleRecommendColor = async () => { + const res = await getSentimentColor(content); + if (res?.status === 200) setColor(res.data.color); + }; + + return ( + + + + setIsHover(true)} + onMouseLeave={() => setIsHover(false)} + /> + + {isHover && ( + + 작성한 글의 감정을 분석하여 색상을
+ 추천해드려요! +
+ )} +
+ ); +} + +const Container = styled.div` + display: flex; + align-items: flex-start; + justify-content: space-between; + height: 50px; +`; + +const InnerContainer = styled.div` + display: flex; + align-items: center; + justify-content: center; +`; + +const Description = styled.div` + color: ${({ theme }) => theme.colors.text.secondary}; + margin-bottom: 10px; + padding: 10px; + border-radius: 10px; + background-color: ${({ theme }) => theme.colors.background.bdp01_80}; + border: 1px solid ${({ theme }) => theme.colors.stroke.sc}; + text-align: center; + width: 120px; + z-index: 99; + + ${Body01ME} +`; + +const HelpCircleIcon = styled(HelpCircle)` + color: ${({ theme }) => theme.colors.text.secondary}; + margin-left: 10px; + cursor: pointer; +`; diff --git a/packages/client/src/widgets/underBar/UnderBar.tsx b/packages/client/src/widgets/underBar/UnderBar.tsx index 2196676..77cacfa 100644 --- a/packages/client/src/widgets/underBar/UnderBar.tsx +++ b/packages/client/src/widgets/underBar/UnderBar.tsx @@ -3,8 +3,6 @@ import { Button } from 'shared/ui'; import { Title01 } from '../../shared/ui/styles'; import PlanetEditIcon from '@icons/icon-planetedit-24-white.svg'; import PlanetEditIconGray from '@icons/icon-planetedit-24-gray.svg'; -import AddIcon from '@icons/icon-add-24-white.svg'; -import AddIconGray from '@icons/icon-add-24-gray.svg'; import WriteIcon from '@icons/icon-writte-24-white.svg'; import WriteIconGray from '@icons/icon-writte-24-gray.svg'; import { BASE_URL, MAX_WIDTH1, MAX_WIDTH2 } from 'shared/lib/constants'; From aa7df0b09ef77259631b148801685f9a25363cb4 Mon Sep 17 00:00:00 2001 From: qkrwogk Date: Sat, 9 Dec 2023 15:25:22 +0900 Subject: [PATCH 029/108] =?UTF-8?q?=E2=9C=85=20service=20unit=20test=20abo?= =?UTF-8?q?ut=20star?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 별 서비스에 대한 유닛 테스트 작성 (커버리지 100% 달성) --- .../server/test/star/star.service.spec.ts | 261 +++++++++++++++++- 1 file changed, 260 insertions(+), 1 deletion(-) diff --git a/packages/server/test/star/star.service.spec.ts b/packages/server/test/star/star.service.spec.ts index 68d40d8..df9d301 100644 --- a/packages/server/test/star/star.service.spec.ts +++ b/packages/server/test/star/star.service.spec.ts @@ -1,18 +1,277 @@ import { Test, TestingModule } from '@nestjs/testing'; import { StarService } from '../../src/star/star.service'; +import { Repository } from 'typeorm'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Model } from 'mongoose'; +import { Star } from '../../src/star/schemas/star.schema'; +import { Board } from '../../src/board/entities/board.entity'; +import { getModelToken } from '@nestjs/mongoose'; +import { + BadRequestException, + InternalServerErrorException, + NotFoundException, +} from '@nestjs/common'; describe('StarService', () => { let service: StarService; + let boardRepository: Repository; + let starModel: Model; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [StarService], + providers: [ + StarService, + { + provide: getRepositoryToken(Board), + useClass: Repository, + }, + { + provide: getModelToken(Star.name), + useValue: Model, + }, + ], }).compile(); service = module.get(StarService); + boardRepository = module.get>(getRepositoryToken(Board)); + starModel = module.get>(getModelToken(Star.name)); }); it('should be defined', () => { expect(service).toBeDefined(); }); + + describe('findAllStarsByAuthor', () => { + it('should throw BadRequestException with empty author', async () => { + await expect(service.findAllStarsByAuthor('')).rejects.toThrow( + 'author is required', + ); + await expect(service.findAllStarsByAuthor('')).rejects.toThrow( + BadRequestException, + ); + }); + + it('should return starDataList', async () => { + const createQueryBuilder: any = { + select: jest.fn().mockReturnThis(), + from: jest.fn().mockReturnThis(), + leftJoin: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + getMany: jest + .fn() + .mockResolvedValue([{ id: 1, star: 1, title: 'title' }]), + }; + + jest + .spyOn(boardRepository, 'createQueryBuilder') + .mockImplementation(() => createQueryBuilder); + + jest + .spyOn(starModel, 'findOne') + .mockResolvedValue({ _id: 1, position: { x: 1, y: 1, z: 1 } }); + + await expect(service.findAllStarsByAuthor('author')).resolves.toEqual([ + { + id: 1, + star: { _id: 1, position: { x: 1, y: 1, z: 1 } }, + title: 'title', + }, + ]); + }); + + it('should throw NotFoundException with not existed star', async () => { + const createQueryBuilder: any = { + select: jest.fn().mockReturnThis(), + from: jest.fn().mockReturnThis(), + leftJoin: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + getMany: jest.fn().mockResolvedValue([{ id: 1, star: undefined }]), + }; + + jest + .spyOn(boardRepository, 'createQueryBuilder') + .mockImplementation(() => createQueryBuilder); + + await expect(service.findAllStarsByAuthor('author')).rejects.toThrow( + 'no star id', + ); + await expect(service.findAllStarsByAuthor('author')).rejects.toThrow( + NotFoundException, + ); + }); + + it('should return NotFoundException with not existed star', async () => { + const createQueryBuilder: any = { + select: jest.fn().mockReturnThis(), + from: jest.fn().mockReturnThis(), + leftJoin: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + getMany: jest.fn().mockResolvedValue([{ id: 1, star: 1 }]), + }; + + jest + .spyOn(boardRepository, 'createQueryBuilder') + .mockImplementation(() => createQueryBuilder); + + jest.spyOn(starModel, 'findOne').mockResolvedValue(undefined); + + await expect(service.findAllStarsByAuthor('author')).rejects.toThrow( + 'star not found', + ); + await expect(service.findAllStarsByAuthor('author')).rejects.toThrow( + NotFoundException, + ); + }); + }); + + describe('updateStarByPostId', () => { + it('should throw NotFoundException with not existed board', async () => { + const post_id = 1; + const updateStarDto = { position: { x: 1, y: 1, z: 1 } }; + const userData = { + userId: 1, + nickname: 'nickname', + username: 'username', + status: 'public', + }; + + jest.spyOn(boardRepository, 'findOneBy').mockResolvedValue(undefined); + + await expect( + service.updateStarByPostId(post_id, updateStarDto, userData), + ).rejects.toThrow('post not found'); + await expect( + service.updateStarByPostId(post_id, updateStarDto, userData), + ).rejects.toThrow(NotFoundException); + }); + + it('should throw BadRequestException with not your star', async () => { + const post_id = 1; + const updateStarDto = { position: { x: 1, y: 1, z: 1 } }; + const userData = { + userId: 1, + nickname: 'nickname', + username: 'username', + status: 'public', + }; + + jest.spyOn(boardRepository, 'findOneBy').mockResolvedValue({ + user: { id: 2, username: 'username', nickname: 'nickname' }, + } as any); + + await expect( + service.updateStarByPostId(post_id, updateStarDto, userData), + ).rejects.toThrow('not your star'); + await expect( + service.updateStarByPostId(post_id, updateStarDto, userData), + ).rejects.toThrow(BadRequestException); + }); + + it('should throw BadRequestException with not existed star', async () => { + const post_id = 1; + const updateStarDto = { position: { x: 1, y: 1, z: 1 } }; + const userData = { + userId: 1, + nickname: 'nickname', + username: 'username', + status: 'public', + }; + + jest.spyOn(boardRepository, 'findOneBy').mockResolvedValue({ + user: { id: 1, username: 'username', nickname: 'nickname' }, + star: undefined, + } as any); + + await expect( + service.updateStarByPostId(post_id, updateStarDto, userData), + ).rejects.toThrow('star not found'); + await expect( + service.updateStarByPostId(post_id, updateStarDto, userData), + ).rejects.toThrow(BadRequestException); + }); + + it('should return InternalServerError when update star failed', async () => { + const post_id = 1; + const updateStarDto = { position: { x: 1, y: 1, z: 1 } }; + const userData = { + userId: 1, + nickname: 'nickname', + username: 'username', + status: 'public', + }; + + jest.spyOn(boardRepository, 'findOneBy').mockResolvedValue({ + user: { id: 1, username: 'username', nickname: 'nickname' }, + star: 1, + } as any); + + jest.spyOn(starModel, 'updateOne').mockResolvedValue(undefined); + + await expect( + service.updateStarByPostId(post_id, updateStarDto, userData), + ).rejects.toThrow('update star failed'); + await expect( + service.updateStarByPostId(post_id, updateStarDto, userData), + ).rejects.toThrow(InternalServerErrorException); + }); + + it('should return NotFoundException when star not found', async () => { + const post_id = 1; + const updateStarDto = { position: { x: 1, y: 1, z: 1 } }; + const userData = { + userId: 1, + nickname: 'nickname', + username: 'username', + status: 'public', + }; + + jest.spyOn(boardRepository, 'findOneBy').mockResolvedValue({ + user: { id: 1, username: 'username', nickname: 'nickname' }, + star: 1, + } as any); + + jest.spyOn(starModel, 'updateOne').mockResolvedValue({ + matchedCount: 0, + } as any); + + await expect( + service.updateStarByPostId(post_id, updateStarDto, userData), + ).rejects.toThrow('star not found'); + await expect( + service.updateStarByPostId(post_id, updateStarDto, userData), + ).rejects.toThrow(NotFoundException); + }); + + it('should return BadRequestException when nothing to update', async () => { + const post_id = 1; + const updateStarDto = { position: { x: 1, y: 1, z: 1 } }; + const userData = { + userId: 1, + nickname: 'nickname', + username: 'username', + status: 'public', + }; + + jest.spyOn(boardRepository, 'findOneBy').mockResolvedValue({ + user: { id: 1, username: 'username', nickname: 'nickname' }, + star: 1, + } as any); + + jest.spyOn(starModel, 'updateOne').mockResolvedValue({ + matchedCount: 1, + modifiedCount: 0, + } as any); + + await expect( + service.updateStarByPostId(post_id, updateStarDto, userData), + ).rejects.toThrow('nothing to update'); + await expect( + service.updateStarByPostId(post_id, updateStarDto, userData), + ).rejects.toThrow(BadRequestException); + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); }); From 7c0fead0e88ecaa31027123996b1ef5dacd852af Mon Sep 17 00:00:00 2001 From: qkrwogk Date: Sat, 9 Dec 2023 15:25:59 +0900 Subject: [PATCH 030/108] =?UTF-8?q?=E2=9C=85=20service=20unit=20test=20abo?= =?UTF-8?q?ut=20star=20dto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 별 서비스 관련 dto에 대한 유닛 테스트 작성 --- .../test/star/dto/get-star-res.dto.spec.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 packages/server/test/star/dto/get-star-res.dto.spec.ts diff --git a/packages/server/test/star/dto/get-star-res.dto.spec.ts b/packages/server/test/star/dto/get-star-res.dto.spec.ts new file mode 100644 index 0000000..daa0bdc --- /dev/null +++ b/packages/server/test/star/dto/get-star-res.dto.spec.ts @@ -0,0 +1,16 @@ +import { GetStarResDto } from '../../../src/star/dto/get-star-res.dto'; + +describe('GetStarResDto', () => { + it('should be defined', () => { + expect(GetStarResDto).toBeDefined(); + }); + + it('should have id, title', () => { + const getStarResDto: GetStarResDto = { + id: 1, + title: 'title', + }; + expect(getStarResDto).toHaveProperty('id'); + expect(getStarResDto).toHaveProperty('title'); + }); +}); From a74305b89c8709779b9c4beb69ef1b033454e0b9 Mon Sep 17 00:00:00 2001 From: qkrwogk Date: Sat, 9 Dec 2023 15:26:59 +0900 Subject: [PATCH 031/108] =?UTF-8?q?=F0=9F=A5=85=20catch=20errors=20in=20fi?= =?UTF-8?q?ndAllStarsByAuthor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - board, star NotFound 에러 처리 추가 --- packages/server/src/star/star.service.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/server/src/star/star.service.ts b/packages/server/src/star/star.service.ts index 636394a..584e464 100644 --- a/packages/server/src/star/star.service.ts +++ b/packages/server/src/star/star.service.ts @@ -39,10 +39,14 @@ export class StarService { const starDataList: GetStarResDto[] = []; for (let board of boards) { - if (!board.star) continue; + if (!board.star) { + throw new NotFoundException('no star id'); + } const star = await this.starModel.findOne({ _id: board.star }); - if (!star) continue; + if (!star) { + throw new NotFoundException('star not found'); + } // __v 필드 제거 star.__v = undefined; From c81fad18c3aecd92e3f19e0c3d77c6ad978793bd Mon Sep 17 00:00:00 2001 From: qkrwogk Date: Sat, 9 Dec 2023 15:28:01 +0900 Subject: [PATCH 032/108] =?UTF-8?q?=E2=9C=85=20service=20unit=20test=20abo?= =?UTF-8?q?ut=20board?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 보드 서비스 유닛 테스트 코드 구현 및 테스트 --- .../server/test/board/board.service.spec.ts | 381 +++++++++++++++++- 1 file changed, 377 insertions(+), 4 deletions(-) diff --git a/packages/server/test/board/board.service.spec.ts b/packages/server/test/board/board.service.spec.ts index 1938a18..9941ec4 100644 --- a/packages/server/test/board/board.service.spec.ts +++ b/packages/server/test/board/board.service.spec.ts @@ -1,18 +1,391 @@ import { Test, TestingModule } from '@nestjs/testing'; import { BoardService } from '../../src/board/board.service'; +import { DataSource, Repository } from 'typeorm'; +import { User } from '../../src/auth/entities/user.entity'; +import { Board } from '../../src/board/entities/board.entity'; +import { getDataSourceToken, getRepositoryToken } from '@nestjs/typeorm'; +import { Model } from 'mongoose'; +import { Star } from '../../src/star/schemas/star.schema'; +import { getModelToken } from '@nestjs/mongoose'; +import { + BadRequestException, + InternalServerErrorException, + NotFoundException, +} from '@nestjs/common'; +import { FileService } from '../../src/board/file.service'; +import { UserDataDto } from 'src/auth/dto/user-data.dto'; describe('BoardService', () => { - let service: BoardService; + let boardService: BoardService; + let fileService: FileService; + let dataSource: jest.Mocked; + let userRepository: Repository; + let boardRepository: Repository; + let starModel: Model; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [BoardService], + providers: [ + BoardService, + FileService, + { + provide: DataSource, + useValue: { + transaction: jest.fn(), + manager: { + findOneBy: jest.fn(), + delete: jest.fn(), + }, + }, + }, + { + provide: getRepositoryToken(User), + useClass: Repository, + }, + { + provide: getRepositoryToken(Board), + useClass: Repository, + }, + { + provide: getModelToken(Star.name), + useValue: Model, + }, + ], }).compile(); - service = module.get(BoardService); + boardService = module.get(BoardService); + fileService = module.get(FileService); + userRepository = module.get>(getRepositoryToken(User)); + boardRepository = module.get>(getRepositoryToken(Board)); + starModel = module.get>(getModelToken(Star.name)); + dataSource = module.get(DataSource); }); it('should be defined', () => { - expect(service).toBeDefined(); + expect(boardService).toBeDefined(); + }); + + describe('getIsLiked', () => { + it('should return NotFoundException with not existed board', async () => { + const userData: UserDataDto = { + userId: 1, + nickname: 'nickname', + username: 'username', + status: 'public', + }; + + jest.spyOn(boardRepository, 'findOneBy').mockResolvedValue(undefined); + + await expect(boardService.getIsLiked(1, userData)).rejects.toThrow( + 'board not found', + ); + await expect(boardService.getIsLiked(1, userData)).rejects.toThrow( + NotFoundException, + ); + }); + + it('should return true if already liked', async () => { + const userData: UserDataDto = { + userId: 1, + nickname: 'nickname', + username: 'username', + status: 'public', + }; + + const boardData: Board = new Board(); + boardData.likes = [{ id: 1 } as any]; + + jest.spyOn(boardRepository, 'findOneBy').mockResolvedValue(boardData); + + await expect(boardService.getIsLiked(1, userData)).resolves.toBe(true); + }); + + it('should return false if not liked', async () => { + const userData: UserDataDto = { + userId: 1, + nickname: 'nickname', + username: 'username', + status: 'public', + }; + + const boardData: Board = new Board(); + boardData.likes = []; + + jest.spyOn(boardRepository, 'findOneBy').mockResolvedValue(boardData); + + await expect(boardService.getIsLiked(1, userData)).resolves.toBe(false); + }); + }); + + describe('patchLike', () => { + it('should return NotFoundException with not existed board', async () => { + const userData: UserDataDto = { + userId: 1, + nickname: 'nickname', + username: 'username', + status: 'public', + }; + + jest.spyOn(boardRepository, 'findOneBy').mockResolvedValue(undefined); + + await expect(boardService.patchLike(1, userData)).rejects.toThrow( + 'board not found', + ); + await expect(boardService.patchLike(1, userData)).rejects.toThrow( + NotFoundException, + ); + }); + + it('should return BadRequestException with already liked', async () => { + const userData: UserDataDto = { + userId: 1, + nickname: 'nickname', + username: 'username', + status: 'public', + }; + + const boardData: Board = new Board(); + boardData.likes = [{ id: 1 } as any]; + + jest.spyOn(boardRepository, 'findOneBy').mockResolvedValue(boardData); + + await expect(boardService.patchLike(1, userData)).rejects.toThrow( + 'already liked', + ); + await expect(boardService.patchLike(1, userData)).rejects.toThrow( + BadRequestException, + ); + }); + + it('should return NotFoundException with not existed user', async () => { + const userData: UserDataDto = { + userId: 1, + nickname: 'nickname', + username: 'username', + status: 'public', + }; + + jest + .spyOn(boardRepository, 'findOneBy') + .mockResolvedValue({ likes: [] } as any); + + jest.spyOn(userRepository, 'findOneBy').mockResolvedValue(undefined); + + await expect(boardService.patchLike(1, userData)).rejects.toThrow( + 'user not found', + ); + await expect(boardService.patchLike(1, userData)).rejects.toThrow( + NotFoundException, + ); + }); + }); + + describe('patchUnlike', () => { + it('should return NotFoundException with not existed board', async () => { + const userData: UserDataDto = { + userId: 1, + nickname: 'nickname', + username: 'username', + status: 'public', + }; + + jest.spyOn(boardRepository, 'findOneBy').mockResolvedValue(undefined); + + await expect(boardService.patchUnlike(1, userData)).rejects.toThrow( + 'board not found', + ); + await expect(boardService.patchUnlike(1, userData)).rejects.toThrow( + NotFoundException, + ); + }); + + it('should return BadRequestException with not liked', async () => { + const userData: UserDataDto = { + userId: 1, + nickname: 'nickname', + username: 'username', + status: 'public', + }; + + const boardData: Board = new Board(); + boardData.likes = []; + + jest.spyOn(boardRepository, 'findOneBy').mockResolvedValue(boardData); + + await expect(boardService.patchUnlike(1, userData)).rejects.toThrow( + 'not liked', + ); + await expect(boardService.patchUnlike(1, userData)).rejects.toThrow( + BadRequestException, + ); + }); + + it('should return NotFoundException with not existed user', async () => { + const userData: UserDataDto = { + userId: 1, + nickname: 'nickname', + username: 'username', + status: 'public', + }; + + const boardData: Board = new Board(); + boardData.likes = [{ id: 1 } as any]; + + jest.spyOn(boardRepository, 'findOneBy').mockResolvedValue(boardData); + + jest.spyOn(userRepository, 'findOneBy').mockResolvedValue(undefined); + + await expect(boardService.patchUnlike(1, userData)).rejects.toThrow( + 'user not found', + ); + await expect(boardService.patchUnlike(1, userData)).rejects.toThrow( + NotFoundException, + ); + }); + }); + + describe('deleteBoard', () => { + it('should return NotFoundException with not existed board', async () => { + jest + .spyOn(dataSource, 'transaction') + .mockImplementation(async (callback: any) => { + await callback(dataSource.manager); + }); + + jest.spyOn(dataSource.manager, 'findOneBy').mockResolvedValue(undefined); + + await expect(boardService.deleteBoard(1, {} as any)).rejects.toThrow( + 'board not found', + ); + await expect(boardService.deleteBoard(1, {} as any)).rejects.toThrow( + NotFoundException, + ); + }); + + it('should return BadRequestException with not your board', async () => { + jest + .spyOn(dataSource, 'transaction') + .mockImplementation(async (callback: any) => { + await callback(dataSource.manager); + }); + + jest.spyOn(dataSource.manager, 'findOneBy').mockResolvedValue({ + user: { id: 2, username: 'username', nickname: 'nickname' }, + } as any); + + await expect(boardService.deleteBoard(1, {} as any)).rejects.toThrow( + 'not your post', + ); + await expect(boardService.deleteBoard(1, {} as any)).rejects.toThrow( + BadRequestException, + ); + }); + + it('should delte images', async () => { + const userData: UserDataDto = { + userId: 1, + nickname: 'nickname', + username: 'username', + status: 'public', + }; + + jest + .spyOn(dataSource, 'transaction') + .mockImplementation(async (callback: any) => { + await callback(dataSource.manager); + }); + + jest.spyOn(dataSource.manager, 'findOneBy').mockResolvedValue({ + user: { id: 1, username: 'username', nickname: 'nickname' }, + images: [{ id: 1, key: 'key' }], + } as any); + + jest.spyOn(dataSource.manager, 'delete').mockResolvedValue({ + affected: 1, + } as any); + + jest.spyOn(fileService, 'deleteFile').mockResolvedValue(undefined); + + expect(await boardService.deleteBoard(1, userData)).toMatchObject({ + affected: 1, + } as any); + }); + }); + + describe('updateBoard', () => { + it('should return NotFoundException with not existed board', async () => { + jest + .spyOn(dataSource, 'transaction') + .mockImplementation(async (callback: any) => { + await callback(dataSource.manager); + }); + + jest.spyOn(dataSource.manager, 'findOneBy').mockResolvedValue(undefined); + + await expect( + boardService.updateBoard(1, {} as any, {} as any, []), + ).rejects.toThrow('board not found'); + await expect( + boardService.updateBoard(1, {} as any, {} as any, []), + ).rejects.toThrow(NotFoundException); + }); + + it('should return BadRequestException with not your board', async () => { + const userData = { + userId: 1, + nickname: 'nickname', + username: 'username', + status: 'public', + }; + + jest + .spyOn(dataSource, 'transaction') + .mockImplementation(async (callback: any) => { + await callback(dataSource.manager); + }); + + jest.spyOn(dataSource.manager, 'findOneBy').mockResolvedValue({ + user: { id: 2, username: 'username', nickname: 'nickname' }, + } as any); + + await expect( + boardService.updateBoard(1, {} as any, userData, []), + ).rejects.toThrow('not your post'); + await expect( + boardService.updateBoard(1, {} as any, userData, []), + ).rejects.toThrow(BadRequestException); + }); + + it('sould throw BadRequestException when request to update star', async () => { + const userData = { + userId: 1, + nickname: 'nickname', + username: 'username', + status: 'public', + }; + + const board = new Board(); + board.user = { id: 1, username: 'username', nickname: 'nickname' } as any; + board.star = 'star_id'; + + jest + .spyOn(dataSource, 'transaction') + .mockImplementation(async (callback: any) => { + await callback(dataSource.manager); + }); + + jest.spyOn(dataSource.manager, 'findOneBy').mockResolvedValue(board); + + await expect( + boardService.updateBoard(1, { star: { a: 'b' } } as any, userData, []), + ).rejects.toThrow('cannot update star'); + await expect( + boardService.updateBoard(1, { star: { a: 'b' } } as any, userData, []), + ).rejects.toThrow(BadRequestException); + }); + }); + + afterEach(() => { + jest.clearAllMocks(); }); }); From be9718c5b1d9bd568ae554a3fe4d39bd114f4fe1 Mon Sep 17 00:00:00 2001 From: qkrwogk Date: Sat, 9 Dec 2023 15:28:50 +0900 Subject: [PATCH 033/108] =?UTF-8?q?=E2=9C=85=20e2e=20test=20with=20image?= =?UTF-8?q?=20files=20on=20board?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 게시글 e2e 테스트 시 실제 이미지 파일을 활용하여 커버리지 증가 --- packages/server/test/board/board.e2e-spec.ts | 102 ++++++++++++++++++- packages/server/test/board/sample-image.ts | 2 + 2 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 packages/server/test/board/sample-image.ts diff --git a/packages/server/test/board/board.e2e-spec.ts b/packages/server/test/board/board.e2e-spec.ts index ab27743..eecf84b 100644 --- a/packages/server/test/board/board.e2e-spec.ts +++ b/packages/server/test/board/board.e2e-spec.ts @@ -7,6 +7,7 @@ import { UpdateBoardDto } from '../../src/board/dto/update-board.dto'; import { CreateBoardDto } from '../../src/board/dto/create-board.dto'; import * as cookieParser from 'cookie-parser'; import { encryptAes } from '../../src/util/aes.util'; +import { sampleImageBase64 } from './sample-image'; describe('BoardController (/board, e2e)', () => { let app: INestApplication; @@ -53,7 +54,14 @@ describe('BoardController (/board, e2e)', () => { const postedBoard = await request(app.getHttpServer()) .post('/post') .set('Cookie', [`accessToken=${accessToken}`]) - .send(board); + .set('Content-Type', 'multipart/form-data') + .field('title', board.title) + .field('content', board.content) + .field('star', board.star) + .attach('file', Buffer.from(sampleImageBase64, 'base64'), { + filename: 'test_image.jpg', + contentType: 'image/jpg', + }); post_id = postedBoard.body.id; }); @@ -83,6 +91,40 @@ describe('BoardController (/board, e2e)', () => { expect(typeof body.star).toBe('string'); }); + it('POST /post with images', async () => { + const board = { + title: 'test', + content: 'test', + star: '{}', + }; + + const response = await request(app.getHttpServer()) + .post('/post') + .set('Cookie', [`accessToken=${accessToken}`]) + .set('Content-Type', 'multipart/form-data') + .field('title', board.title) + .field('content', board.content) + .field('star', board.star) + .attach('file', Buffer.from(sampleImageBase64, 'base64'), { + filename: 'test_image.jpg', + contentType: 'image/jpg', + }) + .expect(201); + + expect(response).toHaveProperty('body'); + const { body } = response; + expect(body).toHaveProperty('id'); + expect(typeof body.id).toBe('number'); + expect(body).toHaveProperty('title'); + expect(body.title).toBe(board.title); + expect(body).toHaveProperty('content'); + expect(body.content).toBe(encryptAes(board.content)); // 암호화되었는지 확인 + expect(body).toHaveProperty('star'); + expect(typeof body.star).toBe('string'); + expect(body).toHaveProperty('images'); + expect(Array.isArray(body.images)).toBe(true); + }); + // #39 [06-02] 서버는 사용자의 글 데이터를 전송한다. it('GET /post/:id', async () => { const board: CreateBoardDto = { @@ -160,6 +202,54 @@ describe('BoardController (/board, e2e)', () => { expect(updatedBoard.content).toBe(encryptAes(toUpdate.content)); }); + it('PATCH /post/:id with images', async () => { + const board = { + title: 'test', + content: 'test', + star: '{}', + }; + const createdBoard = ( + await request(app.getHttpServer()) + .post('/post') + .set('Cookie', [`accessToken=${accessToken}`]) + .send(board) + ).body; + expect(createdBoard).toHaveProperty('id'); + const id = createdBoard.id; + + const toUpdate: UpdateBoardDto = { + title: 'updated', + content: 'updated', + }; + + const updated = await request(app.getHttpServer()) + .patch(`/post/${id}`) + .set('Cookie', [`accessToken=${accessToken}`]) + .set('Content-Type', 'multipart/form-data') + .field('title', toUpdate.title) + .field('content', toUpdate.content) + .attach('file', Buffer.from(sampleImageBase64, 'base64'), { + filename: 'test_image_updated1.jpg', + contentType: 'image/jpg', + }) + .attach('file', Buffer.from(sampleImageBase64, 'base64'), { + filename: 'test_image_updated2.jpg', + contentType: 'image/jpg', + }) + .expect(200); + + expect(updated).toHaveProperty('body'); + const updatedBoard = updated.body; + expect(updatedBoard).toHaveProperty('id'); + expect(updatedBoard.id).toBe(id); + expect(updatedBoard).toHaveProperty('title'); + expect(updatedBoard.title).toBe(toUpdate.title); + expect(updatedBoard).toHaveProperty('content'); + expect(updatedBoard.content).toBe(encryptAes(toUpdate.content)); + expect(updatedBoard).toHaveProperty('images'); + expect(Array.isArray(updatedBoard.images)).toBe(true); + }); + // #45 [06-08] 서버는 좋아요 / 좋아요 취소 요청을 받아 데이터베이스의 데이터를 수정한다. it('PATCH /post/:id/like', async () => { const board = { @@ -187,6 +277,7 @@ describe('BoardController (/board, e2e)', () => { expect(cntAfterLike).toBe(cntBeforeLike + 1); }); + it('PATCH /post/:id/unlike', async () => { const board = { title: 'test', @@ -241,6 +332,15 @@ describe('BoardController (/board, e2e)', () => { await request(app.getHttpServer()).get(`/post/${newBoard.id}`).expect(404); }); + it('GET /post/:id', async () => { + const { body } = await request(app.getHttpServer()) + .get(`/post/${post_id}`) + .expect(200); + + expect(body).toHaveProperty('id'); + expect(body.id).toBe(post_id); + }); + afterEach(async () => { // 로그아웃 await request(app.getHttpServer()) diff --git a/packages/server/test/board/sample-image.ts b/packages/server/test/board/sample-image.ts new file mode 100644 index 0000000..5266be7 --- /dev/null +++ b/packages/server/test/board/sample-image.ts @@ -0,0 +1,2 @@ +export const sampleImageBase64 = + '/9j/4AAQSkZJRgABAQAAkACQAAD/4QCeRXhpZgAATU0AKgAAAAgABQESAAMAAAABAAEAAAEaAAUAAAABAAAASgEbAAUAAAABAAAAUgEoAAMAAAABAAIAAIdpAAQAAAABAAAAWgAAAAAAAACQAAAAAQAAAJAAAAABAAOShgAHAAAAEgAAAISgAgAEAAAAAQAAAAqgAwAEAAAAAQAAAAoAAAAAQVNDSUkAAABTY3JlZW5zaG90/+EJIWh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8APD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIi8+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPD94cGFja2V0IGVuZD0idyI/PgD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCyBOmACZjs+EJ+/+IP0ElDQ19QUk9GSUxFAAEBAAAPwGFwcGwCEAAAbW50clJHQiBYWVogB+cAAgAUABAAOwA5YWNzcEFQUEwAAAAAQVBQTAAAAAAAAAAAAAAAAAAAAAAAAPbWAAEAAAAA0y1hcHBsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARZGVzYwAAAVAAAABiZHNjbQAAAbQAAAScY3BydAAABlAAAAAjd3RwdAAABnQAAAAUclhZWgAABogAAAAUZ1hZWgAABpwAAAAUYlhZWgAABrAAAAAUclRSQwAABsQAAAgMYWFyZwAADtAAAAAgdmNndAAADvAAAAAwbmRpbgAADyAAAAA+bW1vZAAAD2AAAAAodmNncAAAD4gAAAA4YlRSQwAABsQAAAgMZ1RSQwAABsQAAAgMYWFiZwAADtAAAAAgYWFnZwAADtAAAAAgZGVzYwAAAAAAAAAIRGlzcGxheQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAG1sdWMAAAAAAAAAJgAAAAxockhSAAAAFAAAAdhrb0tSAAAADAAAAexuYk5PAAAAEgAAAfhpZAAAAAAAEgAAAgpodUhVAAAAFAAAAhxjc0NaAAAAFgAAAjBkYURLAAAAHAAAAkZubE5MAAAAFgAAAmJmaUZJAAAAEAAAAnhpdElUAAAAGAAAAohlc0VTAAAAFgAAAqByb1JPAAAAEgAAArZmckNBAAAAFgAAAshhcgAAAAAAFAAAAt51a1VBAAAAHAAAAvJoZUlMAAAAFgAAAw56aFRXAAAACgAAAyR2aVZOAAAADgAAAy5za1NLAAAAFgAAAzx6aENOAAAACgAAAyRydVJVAAAAJAAAA1JlbkdCAAAAFAAAA3ZmckZSAAAAFgAAA4ptcwAAAAAAEgAAA6BoaUlOAAAAEgAAA7J0aFRIAAAADAAAA8RjYUVTAAAAGAAAA9BlbkFVAAAAFAAAA3Zlc1hMAAAAEgAAArZkZURFAAAAEAAAA+hlblVTAAAAEgAAA/hwdEJSAAAAGAAABApwbFBMAAAAEgAABCJlbEdSAAAAIgAABDRzdlNFAAAAEAAABFZ0clRSAAAAFAAABGZwdFBUAAAAFgAABHpqYUpQAAAADAAABJAATABDAEQAIAB1ACAAYgBvAGoAac7st+wAIABMAEMARABGAGEAcgBnAGUALQBMAEMARABMAEMARAAgAFcAYQByAG4AYQBTAHoA7QBuAGUAcwAgAEwAQwBEAEIAYQByAGUAdgBuAP0AIABMAEMARABMAEMARAAtAGYAYQByAHYAZQBzAGsA5gByAG0ASwBsAGUAdQByAGUAbgAtAEwAQwBEAFYA5AByAGkALQBMAEMARABMAEMARAAgAGEAIABjAG8AbABvAHIAaQBMAEMARAAgAGEAIABjAG8AbABvAHIATABDAEQAIABjAG8AbABvAHIAQQBDAEwAIABjAG8AdQBsAGUAdQByIA8ATABDAEQAIAZFBkQGSAZGBikEGgQ+BDsETAQ+BEAEPgQyBDgEOQAgAEwAQwBEIA8ATABDAEQAIAXmBdEF4gXVBeAF2V9pgnIATABDAEQATABDAEQAIABNAOAAdQBGAGEAcgBlAGIAbgD9ACAATABDAEQEJgQyBDUEQgQ9BD4EOQAgBBYEGgAtBDQEOARBBD8EOwQ1BDkAQwBvAGwAbwB1AHIAIABMAEMARABMAEMARAAgAGMAbwB1AGwAZQB1AHIAVwBhAHIAbgBhACAATABDAEQJMAkCCRcJQAkoACAATABDAEQATABDAEQAIA4qDjUATABDAEQAIABlAG4AIABjAG8AbABvAHIARgBhAHIAYgAtAEwAQwBEAEMAbwBsAG8AcgAgAEwAQwBEAEwAQwBEACAAQwBvAGwAbwByAGkAZABvAEsAbwBsAG8AcgAgAEwAQwBEA4gDswPHA8EDyQO8A7cAIAO/A7gDzAO9A7cAIABMAEMARABGAOQAcgBnAC0ATABDAEQAUgBlAG4AawBsAGkAIABMAEMARABMAEMARAAgAGEAIABjAG8AcgBlAHMwqzDpMPwATABDAER0ZXh0AAAAAENvcHlyaWdodCBBcHBsZSBJbmMuLCAyMDIzAABYWVogAAAAAAAA81EAAQAAAAEWzFhZWiAAAAAAAACD3wAAPb////+7WFlaIAAAAAAAAEq/AACxNwAACrlYWVogAAAAAAAAKDgAABELAADIuWN1cnYAAAAAAAAEAAAAAAUACgAPABQAGQAeACMAKAAtADIANgA7AEAARQBKAE8AVABZAF4AYwBoAG0AcgB3AHwAgQCGAIsAkACVAJoAnwCjAKgArQCyALcAvADBAMYAywDQANUA2wDgAOUA6wDwAPYA+wEBAQcBDQETARkBHwElASsBMgE4AT4BRQFMAVIBWQFgAWcBbgF1AXwBgwGLAZIBmgGhAakBsQG5AcEByQHRAdkB4QHpAfIB+gIDAgwCFAIdAiYCLwI4AkECSwJUAl0CZwJxAnoChAKOApgCogKsArYCwQLLAtUC4ALrAvUDAAMLAxYDIQMtAzgDQwNPA1oDZgNyA34DigOWA6IDrgO6A8cD0wPgA+wD+QQGBBMEIAQtBDsESARVBGMEcQR+BIwEmgSoBLYExATTBOEE8AT+BQ0FHAUrBToFSQVYBWcFdwWGBZYFpgW1BcUF1QXlBfYGBgYWBicGNwZIBlkGagZ7BowGnQavBsAG0QbjBvUHBwcZBysHPQdPB2EHdAeGB5kHrAe/B9IH5Qf4CAsIHwgyCEYIWghuCIIIlgiqCL4I0gjnCPsJEAklCToJTwlkCXkJjwmkCboJzwnlCfsKEQonCj0KVApqCoEKmAquCsUK3ArzCwsLIgs5C1ELaQuAC5gLsAvIC+EL+QwSDCoMQwxcDHUMjgynDMAM2QzzDQ0NJg1ADVoNdA2ODakNww3eDfgOEw4uDkkOZA5/DpsOtg7SDu4PCQ8lD0EPXg96D5YPsw/PD+wQCRAmEEMQYRB+EJsQuRDXEPURExExEU8RbRGMEaoRyRHoEgcSJhJFEmQShBKjEsMS4xMDEyMTQxNjE4MTpBPFE+UUBhQnFEkUahSLFK0UzhTwFRIVNBVWFXgVmxW9FeAWAxYmFkkWbBaPFrIW1hb6Fx0XQRdlF4kXrhfSF/cYGxhAGGUYihivGNUY+hkgGUUZaxmRGbcZ3RoEGioaURp3Gp4axRrsGxQbOxtjG4obshvaHAIcKhxSHHscoxzMHPUdHh1HHXAdmR3DHeweFh5AHmoelB6+HukfEx8+H2kflB+/H+ogFSBBIGwgmCDEIPAhHCFIIXUhoSHOIfsiJyJVIoIiryLdIwojOCNmI5QjwiPwJB8kTSR8JKsk2iUJJTglaCWXJccl9yYnJlcmhya3JugnGCdJJ3onqyfcKA0oPyhxKKIo1CkGKTgpaymdKdAqAio1KmgqmyrPKwIrNitpK50r0SwFLDksbiyiLNctDC1BLXYtqy3hLhYuTC6CLrcu7i8kL1ovkS/HL/4wNTBsMKQw2zESMUoxgjG6MfIyKjJjMpsy1DMNM0YzfzO4M/E0KzRlNJ402DUTNU01hzXCNf02NzZyNq426TckN2A3nDfXOBQ4UDiMOMg5BTlCOX85vDn5OjY6dDqyOu87LTtrO6o76DwnPGU8pDzjPSI9YT2hPeA+ID5gPqA+4D8hP2E/oj/iQCNAZECmQOdBKUFqQaxB7kIwQnJCtUL3QzpDfUPARANER0SKRM5FEkVVRZpF3kYiRmdGq0bwRzVHe0fASAVIS0iRSNdJHUljSalJ8Eo3Sn1KxEsMS1NLmkviTCpMcky6TQJNSk2TTdxOJU5uTrdPAE9JT5NP3VAnUHFQu1EGUVBRm1HmUjFSfFLHUxNTX1OqU/ZUQlSPVNtVKFV1VcJWD1ZcVqlW91dEV5JX4FgvWH1Yy1kaWWlZuFoHWlZaplr1W0VblVvlXDVchlzWXSddeF3JXhpebF69Xw9fYV+zYAVgV2CqYPxhT2GiYfViSWKcYvBjQ2OXY+tkQGSUZOllPWWSZedmPWaSZuhnPWeTZ+loP2iWaOxpQ2maafFqSGqfavdrT2una/9sV2yvbQhtYG25bhJua27Ebx5veG/RcCtwhnDgcTpxlXHwcktypnMBc11zuHQUdHB0zHUodYV14XY+dpt2+HdWd7N4EXhueMx5KnmJeed6RnqlewR7Y3vCfCF8gXzhfUF9oX4BfmJ+wn8jf4R/5YBHgKiBCoFrgc2CMIKSgvSDV4O6hB2EgITjhUeFq4YOhnKG14c7h5+IBIhpiM6JM4mZif6KZIrKizCLlov8jGOMyo0xjZiN/45mjs6PNo+ekAaQbpDWkT+RqJIRknqS45NNk7aUIJSKlPSVX5XJljSWn5cKl3WX4JhMmLiZJJmQmfyaaJrVm0Kbr5wcnImc951kndKeQJ6unx2fi5/6oGmg2KFHobaiJqKWowajdqPmpFakx6U4pammGqaLpv2nbqfgqFKoxKk3qamqHKqPqwKrdavprFys0K1ErbiuLa6hrxavi7AAsHWw6rFgsdayS7LCszizrrQltJy1E7WKtgG2ebbwt2i34LhZuNG5SrnCuju6tbsuu6e8IbybvRW9j74KvoS+/796v/XAcMDswWfB48JfwtvDWMPUxFHEzsVLxcjGRsbDx0HHv8g9yLzJOsm5yjjKt8s2y7bMNcy1zTXNtc42zrbPN8+40DnQutE80b7SP9LB00TTxtRJ1MvVTtXR1lXW2Ndc1+DYZNjo2WzZ8dp22vvbgNwF3IrdEN2W3hzeot8p36/gNuC94UThzOJT4tvjY+Pr5HPk/OWE5g3mlucf56noMui86Ubp0Opb6uXrcOv77IbtEe2c7ijutO9A78zwWPDl8XLx//KM8xnzp/Q09ML1UPXe9m32+/eK+Bn4qPk4+cf6V/rn+3f8B/yY/Sn9uv5L/tz/bf//cGFyYQAAAAAAAwAAAAJmZgAA8qcAAA1ZAAAT0AAAClt2Y2d0AAAAAAAAAAEAAQAAAAAAAAABAAAAAQAAAAAAAAABAAAAAQAAAAAAAAABAABuZGluAAAAAAAAADYAAK4UAABR7AAAQ9cAALCkAAAmZgAAD1wAAFANAABUOQACMzMAAjMzAAIzMwAAAAAAAAAAbW1vZAAAAAAAAAYQAACgTv1ibWIAAAAAAAAAAAAAAAAAAAAAAAAAAHZjZ3AAAAAAAAMAAAACZmYAAwAAAAJmZgADAAAAAmZmAAAAAjMzNAAAAAACMzM0AAAAAAIzMzQA/8AAEQgACgAKAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMAHBwcHBwcMBwcMEQwMDBEXERERERcdFxcXFxcdIx0dHR0dHSMjIyMjIyMjKioqKioqMTExMTE3Nzc3Nzc3Nzc3P/bAEMBIiQkODQ4YDQ0YOacgJzm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5v/dAAQAAf/aAAwDAQACEQMRAD8Aw6KKKAP/2Q=='; From 9928e5282cb343fc4d669fc1a6f0408caa550800 Mon Sep 17 00:00:00 2001 From: qkrwogk Date: Sat, 9 Dec 2023 15:32:47 +0900 Subject: [PATCH 034/108] =?UTF-8?q?=E2=9C=85=20board=20dto=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 게시글 dto에 대한 유닛 테스트 --- .../board/dto/get-board-by-id-res.dto.spec.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 packages/server/test/board/dto/get-board-by-id-res.dto.spec.ts diff --git a/packages/server/test/board/dto/get-board-by-id-res.dto.spec.ts b/packages/server/test/board/dto/get-board-by-id-res.dto.spec.ts new file mode 100644 index 0000000..853395c --- /dev/null +++ b/packages/server/test/board/dto/get-board-by-id-res.dto.spec.ts @@ -0,0 +1,30 @@ +import { GetBoardByIdResDto } from 'src/board/dto/get-board-by-id-res.dto'; + +describe('GetBoardByIdResDto', () => { + it('should be defined', () => { + expect(GetBoardByIdResDto).toBeDefined(); + }); + + it('should be defined with id, title, content, like_cnt, images', () => { + const getBoardByIdResDto: GetBoardByIdResDto = { + id: 1, + title: 'test', + content: 'test', + like_cnt: 1, + images: ['test'], + }; + + expect(getBoardByIdResDto).toMatchObject({ + id: 1, + title: 'test', + content: 'test', + like_cnt: 1, + images: ['test'], + }); + expect(getBoardByIdResDto).toHaveProperty('id'); + expect(getBoardByIdResDto).toHaveProperty('title'); + expect(getBoardByIdResDto).toHaveProperty('content'); + expect(getBoardByIdResDto).toHaveProperty('like_cnt'); + expect(getBoardByIdResDto).toHaveProperty('images'); + }); +}); From 58800c4fb35fd295131a682313a56b779ed65dd5 Mon Sep 17 00:00:00 2001 From: qkrwogk Date: Sat, 9 Dec 2023 15:33:30 +0900 Subject: [PATCH 035/108] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20remove=20image=20r?= =?UTF-8?q?epository?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 사용하지 않는 image repository 인젝션은 제거 --- packages/server/src/board/board.service.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/server/src/board/board.service.ts b/packages/server/src/board/board.service.ts index 5dc5848..67f422c 100644 --- a/packages/server/src/board/board.service.ts +++ b/packages/server/src/board/board.service.ts @@ -24,8 +24,6 @@ export class BoardService { private readonly dataSource: DataSource, @InjectRepository(Board) private readonly boardRepository: Repository, - @InjectRepository(Image) - private readonly imageRepository: Repository, @InjectRepository(User) private readonly userRepository: Repository, @InjectModel(Star.name) From cd6210872f4ca10ed080755592a7af824cb25485 Mon Sep 17 00:00:00 2001 From: MinboyKim Date: Sat, 9 Dec 2023 15:39:17 +0900 Subject: [PATCH 036/108] =?UTF-8?q?=E2=9C=A8=20BGM=20button?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 배경음악 설정 버튼 구현 --- .../src/shared/ui/audioButton/AudioButton.tsx | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 packages/client/src/shared/ui/audioButton/AudioButton.tsx diff --git a/packages/client/src/shared/ui/audioButton/AudioButton.tsx b/packages/client/src/shared/ui/audioButton/AudioButton.tsx new file mode 100644 index 0000000..0a71d6c --- /dev/null +++ b/packages/client/src/shared/ui/audioButton/AudioButton.tsx @@ -0,0 +1,29 @@ +import { Volume2, VolumeX } from 'lucide-react'; +import { Button } from '..'; +import { usePlayingStore } from 'shared/store/useAudioStore'; +import styled from '@emotion/styled'; + +export default function AudioButton() { + const { playing, setPlaying } = usePlayingStore(); + + return ( + setPlaying()}> + {playing ? : } + + ); +} + +const BGMButton = styled(Button)` + position: absolute; + top: 20px; + left: 20px; + z-index: 100; +`; + +const MuteIcon = styled(VolumeX)` + color: ${({ theme }) => theme.colors.text.secondary}; +`; + +const UnMuteIcon = styled(Volume2)` + color: ${({ theme }) => theme.colors.text.secondary}; +`; From 15cb5b7935a5ce5e4c5239e2b35e721113084a38 Mon Sep 17 00:00:00 2001 From: MinboyKim Date: Sat, 9 Dec 2023 16:25:56 +0900 Subject: [PATCH 037/108] =?UTF-8?q?=E2=9C=A8=20BGM=20-=20=EC=8B=9C?= =?UTF-8?q?=EC=9E=91=ED=99=94=EB=A9=B4=EC=97=90=EB=8F=84=20=EB=B0=B0?= =?UTF-8?q?=EA=B2=BD=EC=9D=8C=EC=95=85=20=EB=B0=8F=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/client/src/pages/Home/Home.tsx | 2 ++ packages/client/src/pages/Landing/Landing.tsx | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/packages/client/src/pages/Home/Home.tsx b/packages/client/src/pages/Home/Home.tsx index 59b73a9..b1e957a 100644 --- a/packages/client/src/pages/Home/Home.tsx +++ b/packages/client/src/pages/Home/Home.tsx @@ -27,6 +27,7 @@ import { FullScreen, useFullScreenHandle } from 'react-full-screen'; import Audio from 'features/audio/Audio'; import styled from '@emotion/styled'; import { keyframes } from '@emotion/react'; +import AudioButton from 'shared/ui/audioButton/AudioButton'; export default function Home() { const { view, setView } = useViewStore(); @@ -92,6 +93,7 @@ export default function Home() {