diff --git a/client/src/pages/Home/index.tsx b/client/src/pages/Home/index.tsx
index bac9f50a..aeb911e0 100644
--- a/client/src/pages/Home/index.tsx
+++ b/client/src/pages/Home/index.tsx
@@ -1,14 +1,16 @@
-import Gnb from '@features/Gnb';
-import Sidebar from '@features/Sidebar';
+import CreateCommunityModal from '@components/Modals/CreateCommunityModal';
+import Gnb from '@layouts/Gnb';
+import Sidebar from '@layouts/Sidebar';
import React from 'react';
import { Outlet } from 'react-router-dom';
const Home = () => {
return (
-
+
+
);
};
diff --git a/client/src/pages/Root/index.tsx b/client/src/pages/Root/index.tsx
index 7c44f79c..ced43a86 100644
--- a/client/src/pages/Root/index.tsx
+++ b/client/src/pages/Root/index.tsx
@@ -1,7 +1,21 @@
+import { useMyInfo } from '@hooks/useMyInfoQuery';
+import { useTokenStore } from '@stores/tokenStore';
import React from 'react';
+import { Navigate } from 'react-router-dom';
+/**
+ * @description
+ * ## 인증 상태에 따라 리다이렉트 분기처리하는 페이지
+ * - 로그인 되어있으면 **`/dms`** 로 이동한다.
+ * - 로그인 되어있지 않으면, **`/sign-in`** 으로 이동한다.
+ * - 조건문에 user || accessToken 중 하나라도 없으면 **`/sign-in`** -> **`/`** -> **`/sign-in`** ... 무한루프 발생함.
+ */
const Root = () => {
- return
;
+ const user = useMyInfo();
+ const accessToken = useTokenStore((state) => state.accessToken);
+
+ if (user || accessToken) return
;
+ return
;
};
export default Root;
diff --git a/client/src/pages/SignIn/index.tsx b/client/src/pages/SignIn/index.tsx
index 9848110e..84d87bc2 100644
--- a/client/src/pages/SignIn/index.tsx
+++ b/client/src/pages/SignIn/index.tsx
@@ -1,91 +1,51 @@
+import type { SignInRequest } from '@apis/auth';
+
import AuthInput from '@components/AuthInput';
import Button from '@components/Button';
import ErrorMessage from '@components/ErrorMessage';
import Logo from '@components/Logo';
import TextButton from '@components/TextButton';
-import { API_URL } from '@constants/url';
+import REGEX from '@constants/regex';
+import defaultErrorHandler from '@errors/defaultErrorHandler';
+import useSignInMutation from '@hooks/useSignInMutation';
import { useTokenStore } from '@stores/tokenStore';
-import { useMutation } from '@tanstack/react-query';
-import axios, { AxiosError } from 'axios';
import React from 'react';
import { Controller, useForm } from 'react-hook-form';
-import { Navigate, useNavigate } from 'react-router-dom';
-import { toast } from 'react-toastify';
-
-interface SignInFields {
- id: string;
- password: string;
-}
-
-interface SuccessResponse
{
- statusCode: number;
- result: T;
-}
-
-type SignInApi = (
- fields: SignInFields,
-) => Promise>;
-
-const endPoint = `${API_URL}/api/user/auth/signin`;
+import { useNavigate } from 'react-router-dom';
-const signInApi: SignInApi = ({ id, password }) => {
- return axios
- .post(endPoint, { id, password })
- .then((response) => response.data);
+const signUpFormDefaultValues = {
+ id: '',
+ password: '',
};
-// 액세스 토큰으로 다시 유저 정보 요청해야함
-// _id, id(이메일), nickname, status, profileUrl, description
const SignIn = () => {
- const accessToken = useTokenStore((state) => state.accessToken);
const setAccessToken = useTokenStore((state) => state.setAccessToken);
- const { control, handleSubmit, reset } = useForm({
+ const { control, handleSubmit, reset } = useForm({
mode: 'all',
- defaultValues: {
- id: '',
- password: '',
- },
+ defaultValues: signUpFormDefaultValues,
});
const navigate = useNavigate();
- const signInMutate = useMutation(['signIn'], signInApi, {
+ const signInMutation = useSignInMutation({
onSuccess: (data) => {
setAccessToken(data.result.accessToken);
+ navigate('/dms');
},
onError: (error) => {
reset();
- if (error instanceof AxiosError) {
- const errorMessage =
- error?.response?.data?.message || '에러가 발생했습니다!';
-
- if (Array.isArray(errorMessage)) {
- errorMessage.forEach((message) => {
- toast.error(message);
- });
- return;
- }
-
- toast.error(errorMessage);
- return;
- }
-
- toast.error('Unknown Error');
+ defaultErrorHandler(error);
},
});
- const handleSubmitSignInForm = ({ id, password }: SignInFields) => {
- signInMutate.mutate({ id, password });
+ const handleSubmitSignInForm = ({ id, password }: SignInRequest) => {
+ signInMutation.mutate({ id, password });
};
const handleNavigateSignUpPage = () => {
navigate('/sign-up');
};
- if (accessToken) {
- return ;
- }
-
return (
@@ -102,7 +62,7 @@ const SignIn = () => {
control={control}
rules={{
pattern: {
- value: /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/,
+ value: REGEX.EMAIL,
message: '아이디는 이메일 형식으로 입력해야 합니다!',
},
}}
@@ -149,7 +109,7 @@ const SignIn = () => {
size="md"
type="submit"
minWidth={340}
- disabled={signInMutate.isLoading}
+ disabled={signInMutation.isLoading}
>
로그인
diff --git a/client/src/pages/SignUp/index.tsx b/client/src/pages/SignUp/index.tsx
index 00c1b267..59163f27 100644
--- a/client/src/pages/SignUp/index.tsx
+++ b/client/src/pages/SignUp/index.tsx
@@ -1,84 +1,52 @@
+import type { SignUpRequest } from '@apis/auth';
+
import AuthInput from '@components/AuthInput';
import Button from '@components/Button';
import ErrorMessage from '@components/ErrorMessage';
import Logo from '@components/Logo';
import SuccessMessage from '@components/SuccessMessage';
import TextButton from '@components/TextButton';
-import { API_URL } from '@constants/url';
-import { useMutation } from '@tanstack/react-query';
-import axios, { AxiosError } from 'axios';
+import REGEX from '@constants/regex';
+import defaultErrorHandler from '@errors/defaultErrorHandler';
+import useSignUpMutation from '@hooks/useSignUpMutation';
import React from 'react';
import { useForm, Controller } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify';
-interface SignUpFields {
- id: string;
- nickname: string;
- password: string;
+interface SignUpFormFields extends SignUpRequest {
passwordCheck: string;
}
-interface SuccessResponse
{
- statusCode: number;
- result: T;
-}
-
-type SignUpApi = (
- fields: Omit,
-) => Promise>;
-
-const endPoint = `${API_URL}/api/user/auth/signup`;
-
-const signUpApi: SignUpApi = ({ id, nickname, password }) => {
- return axios
- .post(endPoint, { id, nickname, password })
- .then((response) => response.data);
+const signUpFormDefaultValues = {
+ id: '',
+ nickname: '',
+ password: '',
+ passwordCheck: '',
};
const SignUp = () => {
- // TODO: 리팩토링 하자
- const { control, handleSubmit, watch, reset } = useForm({
+ const { control, handleSubmit, watch, reset } = useForm({
mode: 'all',
- defaultValues: {
- id: '',
- nickname: '',
- password: '',
- passwordCheck: '',
- },
+ defaultValues: signUpFormDefaultValues,
});
+ const password = watch('password');
+
const navigate = useNavigate();
- const signUpMutate = useMutation(['signUp'], signUpApi, {
+ const signUpMutation = useSignUpMutation({
onSuccess: () => {
toast.success('회원가입에 성공했습니다.');
reset();
},
onError: (error) => {
- if (error instanceof AxiosError) {
- const errorMessage =
- error?.response?.data?.message || '에러가 발생했습니다!';
-
- if (Array.isArray(errorMessage)) {
- errorMessage.forEach((message) => {
- toast.error(message);
- });
- return;
- }
-
- toast.error(errorMessage);
- return;
- }
-
- toast.error('Unknown Error');
+ defaultErrorHandler(error);
},
});
- const password = watch('password');
-
- const handleSubmitSignUpForm = (fields: SignUpFields) => {
- signUpMutate.mutate(fields);
+ const handleSubmitSignUpForm = (fields: SignUpFormFields) => {
+ signUpMutation.mutate(fields);
};
const handleNavigateSignInPage = () => {
@@ -101,7 +69,7 @@ const SignUp = () => {
control={control}
rules={{
pattern: {
- value: /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/,
+ value: REGEX.EMAIL,
message: '아이디는 이메일 형식으로 입력해야 합니다!',
},
required: '필수 요소입니다!',
@@ -220,7 +188,7 @@ const SignUp = () => {
size="md"
type="submit"
minWidth={340}
- disabled={signUpMutate.isLoading}
+ disabled={signUpMutation.isLoading}
>
회원가입
diff --git a/client/src/pages/UnAuthorizedLayer/index.tsx b/client/src/pages/UnAuthorizedLayer/index.tsx
new file mode 100644
index 00000000..94963043
--- /dev/null
+++ b/client/src/pages/UnAuthorizedLayer/index.tsx
@@ -0,0 +1,37 @@
+import { useMyInfo } from '@hooks/useMyInfoQuery';
+import useReissueTokenMutation from '@hooks/useReissueTokenMutation';
+import { useTokenStore } from '@stores/tokenStore';
+import React, { useEffect, useState } from 'react';
+import { Outlet, Navigate, useLocation } from 'react-router-dom';
+
+/**
+ * ## 로그인 하지 않은 유저들만 머무를 수 있는 페이지.
+ * - 새로고침시 토큰 갱신을 시도하며, 로그인한(유저 상태나 액세스 토큰 상태가 있는) 유저가 접근하면 **`/`** 로 리다이렉트된다.
+ * - 토큰 갱신 요청시, 유효하지 않은 토큰 에러나 알 수 없는 에러가 발생하면 페이지 이동 없이 그대로 유지한다.
+ */
+const UnAuthorizedLayer = () => {
+ const user = useMyInfo();
+ const location = useLocation();
+
+ const accessToken = useTokenStore((state) => state.accessToken);
+ const [isTryingReissueToken, setIsTryingReissueToken] = useState(true);
+
+ const handleReissueTokenError = () => setIsTryingReissueToken(false);
+
+ const reissueTokenMutation = useReissueTokenMutation(
+ handleReissueTokenError,
+ handleReissueTokenError,
+ );
+
+ useEffect(() => {
+ if (user) return;
+ reissueTokenMutation.mutate();
+ }, []);
+
+ if (user || accessToken) return ;
+ if (location.state?.alreadyTriedReissueToken) return ;
+ if (isTryingReissueToken) return 로딩중...
;
+ return ;
+};
+
+export default UnAuthorizedLayer;
diff --git a/client/src/pages/UnknownError/index.tsx b/client/src/pages/UnknownError/index.tsx
new file mode 100644
index 00000000..f7f0485a
--- /dev/null
+++ b/client/src/pages/UnknownError/index.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+
+const UnknownError = () => {
+ console.log(`[${location.pathname}]`);
+
+ return Unknown Error
;
+};
+
+export default UnknownError;
diff --git a/client/src/pages/UserSearch/index.tsx b/client/src/pages/UserSearch/index.tsx
deleted file mode 100644
index 356d07d3..00000000
--- a/client/src/pages/UserSearch/index.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import React from 'react';
-
-const UserSearch = () => {
- return UserSearch
;
-};
-
-export default UserSearch;
diff --git a/client/src/queryKeyCreator.ts b/client/src/queryKeyCreator.ts
new file mode 100644
index 00000000..31e04bce
--- /dev/null
+++ b/client/src/queryKeyCreator.ts
@@ -0,0 +1,22 @@
+const directMessageQueryKey = {
+ all: ['directMessages'] as const,
+ list: () => [...directMessageQueryKey.all] as const,
+ detail: (id: string) => [...directMessageQueryKey.all, id] as const,
+} as const;
+
+const queryKeyCreator = {
+ me: () => ['me'] as const,
+ signUp: () => ['signUp'] as const,
+ signIn: () => ['signIn'] as const,
+ followings: (): [string] => ['followings'],
+ followers: (): [string] => ['followers'],
+ reissueToken: () => ['reissueToken'] as const,
+ userSearch: (filter: string) => ['userSearch', { filter }],
+ directMessage: directMessageQueryKey,
+} as const;
+
+export default queryKeyCreator;
+
+type QueryKeyCreatorType = typeof queryKeyCreator;
+export type QueryKeyCreator =
+ QueryKeyCreatorType[T];
diff --git a/client/src/stores/modalSlice.ts b/client/src/stores/modalSlice.ts
new file mode 100644
index 00000000..4fca1f10
--- /dev/null
+++ b/client/src/stores/modalSlice.ts
@@ -0,0 +1,15 @@
+import type { StateCreator } from 'zustand';
+
+export interface ModalSlice {
+ createCommunityModal: {
+ open: boolean;
+ };
+}
+
+export const createModalSlice: StateCreator = (
+ set,
+) => ({
+ createCommunityModal: {
+ open: false,
+ },
+});
diff --git a/client/src/stores/rootStore.ts b/client/src/stores/rootStore.ts
new file mode 100644
index 00000000..f4421b24
--- /dev/null
+++ b/client/src/stores/rootStore.ts
@@ -0,0 +1,14 @@
+import type { ModalSlice } from '@stores/modalSlice';
+
+import store from 'zustand';
+import { devtools } from 'zustand/middleware';
+
+import { createModalSlice } from './modalSlice';
+
+export type Store = ModalSlice;
+
+export const useStore = store()(
+ devtools((...a) => ({
+ ...createModalSlice(...a),
+ })),
+);
diff --git a/client/src/stores/tokenStore.ts b/client/src/stores/tokenStore.ts
index b13ad94a..854fdd91 100644
--- a/client/src/stores/tokenStore.ts
+++ b/client/src/stores/tokenStore.ts
@@ -9,7 +9,7 @@ type TokenStore = {
export const tokenStore = createVanillaStore()(
devtools((set) => ({
- accessToken: null,
+ accessToken: process.env.NODE_ENV === 'development' ? 'null' : null,
setAccessToken: (newAccessToken) =>
set(() => ({ accessToken: newAccessToken })),
})),
diff --git a/client/src/types/apis/process.d.ts b/client/src/types/apis/process.d.ts
new file mode 100644
index 00000000..cac93912
--- /dev/null
+++ b/client/src/types/apis/process.d.ts
@@ -0,0 +1,6 @@
+declare namespace NodeJS {
+ interface ProcessEnv {
+ NODE_ENV?: string;
+ API_URL?: string;
+ }
+}
diff --git a/client/src/types/apis/response.ts b/client/src/types/apis/response.ts
new file mode 100644
index 00000000..4d63b12f
--- /dev/null
+++ b/client/src/types/apis/response.ts
@@ -0,0 +1,10 @@
+export interface SuccessResponse {
+ statusCode: number;
+ result: T;
+}
+
+export interface ErrorResponse {
+ statusCode: number;
+ message: string | string[];
+ error: string;
+}
diff --git a/client/src/utils/axios.ts b/client/src/utils/axios.ts
new file mode 100644
index 00000000..b78d783e
--- /dev/null
+++ b/client/src/utils/axios.ts
@@ -0,0 +1,48 @@
+import { API_URL } from '@constants/url';
+import { tokenStore } from '@stores/tokenStore';
+import axios from 'axios';
+
+const { getState } = tokenStore;
+
+/**
+ * ## Asnity api server 전용 Axios instance
+ * - `baseURL`은 Asnity server 이다. 따라서 엔드포인트 작성시 `baseURL`이후 부분만 적는다.
+ * - Api 요청시 전역 상태에서 관리하는 accessToken을 Authorization header에 삽입하고 보낸다.
+ * - accessToken이 없다면 요청 Promise가 Reject된다.
+ * - 토큰 만료 응답시 Response interceptors에서 재발급 후 Request 재요청 하는 로직은 추후에 추가할 예정.
+ */
+export const tokenAxios = axios.create({
+ baseURL: API_URL,
+});
+
+/**
+ * ## Asnity api server 전용 Axios instance
+ * - `baseURL`은 Asnity server 이다. 따라서 엔드포인트 작성시 `baseURL`이후 부분만 적는다.
+ * - accessToken이 필요없는 요청을 보낼 때 사용한다.
+ */
+export const publicAxios = axios.create({
+ baseURL: API_URL,
+});
+
+tokenAxios.interceptors.request.use(
+ (config) => {
+ const { accessToken } = getState();
+
+ console.warn('tokenAxios 사용 확인용 로그. 무시하시면 됩니다.');
+
+ if (!accessToken) {
+ console.warn(`accessToken이 없습니다.`);
+ return Promise.reject(
+ `tokenAxios instance로 요청을 보내기 위해서는 accessToken이 필요합니다.`,
+ );
+ }
+
+ config.headers = {
+ Authorization: `Bearer ${accessToken}`,
+ };
+ return config;
+ },
+ function (error) {
+ return Promise.reject(error);
+ },
+);
diff --git a/client/tsconfig.json b/client/tsconfig.json
index 929f22a5..d7bde1a6 100644
--- a/client/tsconfig.json
+++ b/client/tsconfig.json
@@ -1,7 +1,9 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
- "lib": ["DOM"],
+ "esModuleInterop": true,
+ "moduleResolution": "Node",
+ "lib": ["DOM", "es2015.iterable"],
"jsx": "react-jsx",
"baseUrl": ".",
"outDir": "./build",
@@ -14,11 +16,13 @@
"@icons/*": ["src/assets/icons/*"],
"@constants/*": ["src/constants/*"],
"@apis/*": ["src/apis/*"],
+ "@hooks/*": ["src/hooks/*"],
+ "@errors/*": ["src/errors/*"],
+ "@@types/*": ["src/types/*"],
+ "@/*": ["src/*"]
}
},
"include": ["src", "config"],
"exclude": ["node_modules", "build", "dist"],
- "references": [
- { "path": "../shared" }
- ]
-}
\ No newline at end of file
+ "references": [{ "path": "../shared" }]
+}
diff --git a/server/__mock__/community.mock.ts b/server/__mock__/community.mock.ts
new file mode 100644
index 00000000..f631960e
--- /dev/null
+++ b/server/__mock__/community.mock.ts
@@ -0,0 +1,8 @@
+import { CreateCommunityDto } from '@api/src/community/dto/create-community.dto';
+
+export const communityDto1 = {
+ name: 'asnity commu',
+ managerId: '63734af9e62b37012c73e399',
+ description: 'test description',
+ profileUrl: 'test profileUrl',
+} as CreateCommunityDto;
diff --git a/server/apps/api/src/api.module.ts b/server/apps/api/src/api.module.ts
index ecc0fc45..fefba2ab 100644
--- a/server/apps/api/src/api.module.ts
+++ b/server/apps/api/src/api.module.ts
@@ -9,9 +9,6 @@ import { UserModule } from './user/user.module';
import * as winston from 'winston';
import { utilities as nestWinstonModuleUtilities, WinstonModule } from 'nest-winston';
import { AuthModule } from './auth/auth.module';
-import { APP_INTERCEPTOR } from '@nestjs/core';
-import { SentryInterceptor } from '../../webhook.interceptor';
-import { RavenInterceptor, RavenModule } from 'nest-raven';
@Module({
imports: [
diff --git a/server/apps/api/src/auth/dto/index.ts b/server/apps/api/src/auth/dto/index.ts
index 0ee0429b..aa80f504 100644
--- a/server/apps/api/src/auth/dto/index.ts
+++ b/server/apps/api/src/auth/dto/index.ts
@@ -1 +1,2 @@
-export * from './auth.dto';
+export * from './sign-in.dto';
+export * from './sign-up.dto';
diff --git a/server/apps/api/src/auth/dto/sign-in.dto.ts b/server/apps/api/src/auth/dto/sign-in.dto.ts
new file mode 100644
index 00000000..f23a0ee1
--- /dev/null
+++ b/server/apps/api/src/auth/dto/sign-in.dto.ts
@@ -0,0 +1,11 @@
+import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
+
+export class SignInDto {
+ @IsEmail()
+ @IsNotEmpty()
+ id: string;
+
+ @IsString()
+ @MinLength(8)
+ password: string;
+}
diff --git a/server/apps/api/src/auth/dto/auth.dto.ts b/server/apps/api/src/auth/dto/sign-up.dto.ts
similarity index 67%
rename from server/apps/api/src/auth/dto/auth.dto.ts
rename to server/apps/api/src/auth/dto/sign-up.dto.ts
index 87a5fdcd..66bda45e 100644
--- a/server/apps/api/src/auth/dto/auth.dto.ts
+++ b/server/apps/api/src/auth/dto/sign-up.dto.ts
@@ -1,15 +1,5 @@
import { IsEmail, IsNotEmpty, IsString, Length, MinLength } from 'class-validator';
-export class SignInDto {
- @IsEmail()
- @IsNotEmpty()
- id: string;
-
- @IsString()
- @MinLength(8)
- password: string;
-}
-
export class SignUpDto {
@IsEmail()
@IsNotEmpty()
diff --git a/server/apps/api/src/auth/strategy/jwt-access.strategy.ts b/server/apps/api/src/auth/strategy/jwt-access.strategy.ts
index e890d211..e82bd868 100644
--- a/server/apps/api/src/auth/strategy/jwt-access.strategy.ts
+++ b/server/apps/api/src/auth/strategy/jwt-access.strategy.ts
@@ -19,6 +19,6 @@ export class JwtAccessStrategy extends PassportStrategy(Strategy, 'jwt-access-to
throw new ForbiddenException('잘못된 요청입니다.');
}
delete user.password;
- return user;
+ return { ...user, _id: user._id.toString() };
}
}
diff --git a/server/apps/api/src/channel/channel.controller.ts b/server/apps/api/src/channel/channel.controller.ts
index fff4d422..6fa995b6 100644
--- a/server/apps/api/src/channel/channel.controller.ts
+++ b/server/apps/api/src/channel/channel.controller.ts
@@ -1,4 +1,27 @@
-import { Controller } from '@nestjs/common';
+import { Body, Controller, Inject, LoggerService, Post, Req, UseGuards } from '@nestjs/common';
+import { ChannelService } from '@api/src/channel/channel.service';
+import { JwtAccessGuard } from '@api/src/auth/guard';
+import { CreateChannelDto } from '@api/src/channel/dto';
+import { responseForm } from '@utils/responseForm';
+import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
-@Controller('channel')
-export class ChannelController {}
+@Controller('api/channel')
+export class ChannelController {
+ constructor(
+ @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService,
+ private channelService: ChannelService,
+ ) {}
+
+ @Post()
+ @UseGuards(JwtAccessGuard)
+ async createChannel(@Body() createChannelDto: CreateChannelDto, @Req() req: any) {
+ const _id = req.user._id;
+ try {
+ await this.channelService.createChannel({ ...createChannelDto, managerId: _id });
+ return responseForm(200, { message: '채널 생성 성공!' });
+ } catch (error) {
+ this.logger.error(JSON.stringify(error.response));
+ throw error;
+ }
+ }
+}
diff --git a/server/apps/api/src/channel/channel.module.ts b/server/apps/api/src/channel/channel.module.ts
index fe632c18..6dd53985 100644
--- a/server/apps/api/src/channel/channel.module.ts
+++ b/server/apps/api/src/channel/channel.module.ts
@@ -1,9 +1,22 @@
import { Module } from '@nestjs/common';
import { ChannelController } from './channel.controller';
import { ChannelService } from './channel.service';
+import { MongooseModule } from '@nestjs/mongoose';
+import { ChannelRepository } from '@repository/channel.repository';
+import { ChannelSchema } from '@schemas/channel.schema';
+import { Channel } from 'diagnostics_channel';
+import { CommunityRepository } from '@repository/community.repository';
+import { CommunityModule } from '@community/community.module';
+import { UserRepository } from '@repository/user.repository';
+import { UserModule } from '@user/user.module';
@Module({
+ imports: [
+ MongooseModule.forFeature([{ name: Channel.name, schema: ChannelSchema }]),
+ CommunityModule,
+ UserModule,
+ ],
controllers: [ChannelController],
- providers: [ChannelService]
+ providers: [ChannelService, ChannelRepository, CommunityRepository, UserRepository],
})
export class ChannelModule {}
diff --git a/server/apps/api/src/channel/channel.service.ts b/server/apps/api/src/channel/channel.service.ts
index fc8cd3a7..dd1dadc6 100644
--- a/server/apps/api/src/channel/channel.service.ts
+++ b/server/apps/api/src/channel/channel.service.ts
@@ -1,4 +1,39 @@
-import { Injectable } from '@nestjs/common';
+import { BadRequestException, Injectable } from '@nestjs/common';
+import { ChannelRepository } from '@repository/channel.repository';
+import { CreateChannelDto } from '@api/src/channel/dto';
+import { CommunityRepository } from '@repository/community.repository';
+import { UserRepository } from '@repository/user.repository';
+import { addChannelToUserForm } from '@utils/addObjectForm';
@Injectable()
-export class ChannelService {}
+export class ChannelService {
+ constructor(
+ private readonly channelRepository: ChannelRepository,
+ private readonly communityRepository: CommunityRepository,
+ private readonly userRepository: UserRepository,
+ ) {}
+
+ async createChannel(createChannelDto: CreateChannelDto) {
+ // 자신이 속한 커뮤니티 찾기
+ try {
+ const community = await this.communityRepository.findById(createChannelDto.communityId);
+
+ // 채널 생성
+ const channel = await this.channelRepository.create({
+ ...createChannelDto,
+ users: [createChannelDto.managerId],
+ });
+
+ // 커뮤니티 목록에 채널 업데이트
+ await this.communityRepository.addArrAtArr({ _id: community.id }, 'channels', [
+ channel._id.toString(),
+ ]);
+
+ // 유저 목록에 자신이 속한 채널 업데이트
+ const newChannel = addChannelToUserForm(community._id, channel._id);
+ await this.userRepository.updateObject({ _id: createChannelDto.managerId }, newChannel);
+ } catch (error) {
+ throw new BadRequestException('채널 생성 중 오류 발생!');
+ }
+ }
+}
diff --git a/server/apps/api/src/channel/dto/create-channel.dto.ts b/server/apps/api/src/channel/dto/create-channel.dto.ts
new file mode 100644
index 00000000..95d020d5
--- /dev/null
+++ b/server/apps/api/src/channel/dto/create-channel.dto.ts
@@ -0,0 +1,37 @@
+import { IsBoolean, IsNotEmpty, IsOptional, IsString, Length } from 'class-validator';
+import { Transform } from 'class-transformer';
+
+export class CreateChannelDto {
+ @IsString()
+ @IsNotEmpty()
+ communityId: string;
+
+ @IsString()
+ @Length(2, 20)
+ @IsNotEmpty()
+ name: string;
+
+ @IsString()
+ @IsOptional()
+ managerId: string;
+
+ @IsString()
+ @IsOptional()
+ description: string;
+
+ @IsString()
+ @IsOptional()
+ profileUrl: string;
+
+ @IsString()
+ @IsNotEmpty()
+ type: 'Channel' | 'DM';
+
+ @IsBoolean()
+ @IsNotEmpty()
+ @Transform(({ value }) => value === 'true')
+ isPrivate: boolean;
+
+ @IsOptional()
+ users: string[];
+}
diff --git a/server/apps/api/src/channel/dto/index.ts b/server/apps/api/src/channel/dto/index.ts
new file mode 100644
index 00000000..a21d4e9f
--- /dev/null
+++ b/server/apps/api/src/channel/dto/index.ts
@@ -0,0 +1 @@
+export * from './create-channel.dto';
diff --git a/server/apps/api/src/community/community.controller.ts b/server/apps/api/src/community/community.controller.ts
index a169f161..7fc975fc 100644
--- a/server/apps/api/src/community/community.controller.ts
+++ b/server/apps/api/src/community/community.controller.ts
@@ -1,4 +1,104 @@
-import { Controller } from '@nestjs/common';
+import {
+ Body,
+ Controller,
+ Delete,
+ Get,
+ Inject,
+ LoggerService,
+ Param,
+ Patch,
+ Post,
+ Req,
+ UseGuards,
+} from '@nestjs/common';
+import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
+import { CommunityService } from '@api/src/community/community.service';
+import { responseForm } from '@utils/responseForm';
+import { JwtAccessGuard } from '@api/src/auth/guard';
+import {
+ CreateCommunityDto,
+ AppendUsersToCommunityDto,
+ ModifyCommunityDto,
+ DeleteCommunityDto,
+} from './dto';
-@Controller('community')
-export class CommunityController {}
+@Controller('api/community')
+export class CommunityController {
+ constructor(
+ @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService,
+ private communityService: CommunityService,
+ ) {}
+
+ @Get()
+ @UseGuards(JwtAccessGuard)
+ async getCommunities(@Req() req: any) {
+ try {
+ const result = await this.communityService.getCommunities(req.user._doc);
+ return responseForm(200, result);
+ } catch (error) {
+ this.logger.error(JSON.stringify(error.response));
+ throw error;
+ }
+ }
+
+ @Post()
+ @UseGuards(JwtAccessGuard)
+ async crateCommunity(@Body() createCommunityDto: CreateCommunityDto, @Req() req: any) {
+ try {
+ const _id = req.user._id;
+ const result = await this.communityService.createCommunity({
+ managerId: _id,
+ ...createCommunityDto,
+ });
+ return responseForm(200, result);
+ } catch (error) {
+ this.logger.error(JSON.stringify(error.response));
+ throw error;
+ }
+ }
+
+ @Post('participants')
+ @UseGuards(JwtAccessGuard)
+ async appendParticipantsToCommunity(
+ @Body() appendUsersToCommunityDto: AppendUsersToCommunityDto,
+ @Req() req: any,
+ ) {
+ try {
+ await this.communityService.appendParticipantsToCommunity(
+ req.user,
+ appendUsersToCommunityDto,
+ );
+ return responseForm(200, { message: '커뮤니티 사용자 추가 완료' });
+ } catch (error) {
+ this.logger.error(JSON.stringify(error.response));
+ throw error;
+ }
+ }
+
+ @Patch('settings')
+ @UseGuards(JwtAccessGuard)
+ async modifyCommunitySetting(@Body() modifyCommunityDto: ModifyCommunityDto, @Req() req: any) {
+ try {
+ const _id = req.user._id;
+ await this.communityService.modifyCommunity({ ...modifyCommunityDto, managerId: _id });
+ return responseForm(200, { message: '커뮤니티 정보 수정 완료' });
+ } catch (error) {
+ this.logger.error(JSON.stringify(error.response));
+ throw error;
+ }
+ }
+
+ @Delete(':_id')
+ @UseGuards(JwtAccessGuard)
+ async deleteCommunity(@Param('_id') community_id: string, @Req() req: any) {
+ try {
+ const managerId = req.user._id;
+ const deleteCommunityDto: DeleteCommunityDto = { managerId, community_id };
+ await this.communityService.deleteCommunity(deleteCommunityDto);
+ return responseForm(200, { message: '커뮤니티 삭제 성공' });
+ } catch (error) {
+ this.logger.error(JSON.stringify(error.response));
+ throw error;
+ }
+ }
+}
diff --git a/server/apps/api/src/community/community.module.ts b/server/apps/api/src/community/community.module.ts
index d4fb4995..474dda22 100644
--- a/server/apps/api/src/community/community.module.ts
+++ b/server/apps/api/src/community/community.module.ts
@@ -1,9 +1,19 @@
import { Module } from '@nestjs/common';
import { CommunityController } from './community.controller';
import { CommunityService } from './community.service';
+import { MongooseModule } from '@nestjs/mongoose';
+import { Community, CommunitySchema } from '@schemas/community.schema';
+import { CommunityRepository } from '@repository/community.repository';
+import { UserRepository } from '@repository/user.repository';
+import { UserModule } from '@user/user.module';
@Module({
+ imports: [
+ MongooseModule.forFeature([{ name: Community.name, schema: CommunitySchema }]),
+ UserModule,
+ ],
controllers: [CommunityController],
- providers: [CommunityService],
+ providers: [CommunityService, CommunityRepository, UserRepository],
+ exports: [MongooseModule],
})
export class CommunityModule {}
diff --git a/server/apps/api/src/community/community.service.ts b/server/apps/api/src/community/community.service.ts
index 4cd3bcc7..1e0fc2d0 100644
--- a/server/apps/api/src/community/community.service.ts
+++ b/server/apps/api/src/community/community.service.ts
@@ -1,4 +1,164 @@
-import { Injectable } from '@nestjs/common';
+import { BadRequestException, Injectable } from '@nestjs/common';
+import { CommunityRepository } from '@repository/community.repository';
+import { UserRepository } from '@repository/user.repository';
+import {
+ CreateCommunityDto,
+ AppendUsersToCommunityDto,
+ ModifyCommunityDto,
+ DeleteCommunityDto,
+} from './dto';
+import { IsUserInCommunity, makeCommunityObj } from '@community/helper';
+import { communityInUser } from '@user/dto/community-in-user.dto';
@Injectable()
-export class CommunityService {}
+export class CommunityService {
+ constructor(
+ private readonly communityRepository: CommunityRepository,
+ private readonly userRepository: UserRepository,
+ ) {}
+
+ async getCommunities(user2) {
+ const user = await this.userRepository.findById('637f2abb146636e4082885b1');
+ const infos = [];
+ await Promise.all(
+ Array.from(user.communities.values()).map(async (userCommunity) => {
+ const { _id, channels } = userCommunity as communityInUser;
+ const community = await this.communityRepository.findById(_id);
+ if (!community) {
+ throw new BadRequestException('해당하는 커뮤니티의 _id가 올바르지 않습니다.');
+ }
+ const result = Array.from(channels.keys()).filter(
+ (channel_id: string) => !community.channels.includes(channel_id),
+ );
+ if (result.length > 0) {
+ console.log(result);
+ throw new BadRequestException('커뮤니티에 없는 비정상적인 채널이 존재합니다.');
+ }
+ const info = {};
+ info[_id] = [];
+ await Promise.all(
+ Array.from(channels.keys()).map(async (channelId) => {
+ const lastRead = channels.get(channelId);
+ console.log(lastRead);
+ // TODO: soft delete이면 조건 다시 설정
+ // const channel = await this.channelRepository.findById(channelId);
+ // if (!channel) {
+ // throw new BadRequestException('존재하지 않는 채널입니다.');
+ // }
+ // info[_id][channelId] = lastRead < channel.updatedAt;
+ // console.log(info[_id]);
+ }),
+ );
+ console.log(info);
+
+ infos.push(info);
+ }),
+ );
+ return infos;
+ }
+ async createCommunity(createCommunityDto: CreateCommunityDto) {
+ const community = await this.communityRepository.create({
+ ...createCommunityDto,
+ users: [createCommunityDto.managerId],
+ });
+ const newCommunity = makeCommunityObj(community._id.toString());
+ await this.userRepository.updateObject({ _id: createCommunityDto.managerId }, newCommunity);
+ return community;
+ }
+
+ async appendParticipantsToCommunity(
+ reqUser,
+ appendUsersToCommunityDto: AppendUsersToCommunityDto,
+ ) {
+ const communityId = appendUsersToCommunityDto.community_id;
+ if (!IsUserInCommunity(reqUser, communityId)) {
+ throw new BadRequestException(`커뮤니티에 속하지 않는 사용자는 요청할 수 없습니다.`);
+ }
+ const newCommunity = makeCommunityObj(communityId);
+ await Promise.all(
+ // 사용자 document 검증 (올바른 사용자인지, 해당 사용자가 이미 커뮤니티에 참여하고 있는건 아닌지)
+ appendUsersToCommunityDto.users.map(async (user_id) => {
+ const user = await this.userRepository.findById(user_id);
+ if (!user) {
+ throw new BadRequestException(
+ `커뮤니티에 추가를 요청한 사용자 _id(${user_id})가 올바르지 않습니다.`,
+ );
+ } else if (IsUserInCommunity(user, communityId)) {
+ throw new BadRequestException(`이미 커뮤니티에 추가된 사용자 입니다.`);
+ }
+ }),
+ );
+ await Promise.all(
+ // 사용자 document 검증이 끝난 후 update
+ appendUsersToCommunityDto.users.map(async (user_id) => {
+ await this.userRepository.updateObject({ _id: user_id }, newCommunity);
+ }),
+ );
+ const community = await this.communityRepository.addArrAtArr(
+ { _id: appendUsersToCommunityDto.community_id },
+ 'users',
+ appendUsersToCommunityDto.users,
+ );
+ if (!community) {
+ await Promise.all(
+ // 사용자 document에서 다시 삭제
+ appendUsersToCommunityDto.users.map((user_id) => {
+ this.userRepository.deleteObject({ _id: user_id }, newCommunity);
+ }),
+ );
+ throw new BadRequestException('해당하는 커뮤니티의 _id가 올바르지 않습니다.');
+ }
+ }
+
+ async modifyCommunity(modifyCommunityDto: ModifyCommunityDto) {
+ // TODO : refactoring을 findAndUpdate로 해서 매니저 id, deletedAt인지 바로 검증이랑 동시에 하도록..
+ const community = await this.verfiyCommunity(modifyCommunityDto.community_id);
+ if (community.managerId != modifyCommunityDto.managerId) {
+ throw new BadRequestException('사용자의 커뮤니티 수정 권한이 없습니다.');
+ }
+ const { managerId, community_id, ...updateField } = modifyCommunityDto;
+ // TODO: 꼭 기다려줘야하는지 생각해보기
+ return await this.communityRepository.updateOne({ _id: community_id }, updateField);
+ }
+
+ async deleteCommunity(deleteCommunityDto: DeleteCommunityDto) {
+ const updateField = { deletedAt: new Date() };
+ let community = await this.communityRepository.findAndUpdateOne(
+ {
+ _id: deleteCommunityDto.community_id,
+ managerId: deleteCommunityDto.managerId,
+ deletedAt: { $exists: false },
+ },
+ updateField,
+ );
+ if (!community) {
+ community = await this.verfiyCommunity(deleteCommunityDto.community_id);
+ if (community.managerId != deleteCommunityDto.managerId) {
+ throw new BadRequestException('사용자의 커뮤니티 수정 권한이 없습니다.');
+ } else if (community.deletedAt) {
+ throw new BadRequestException('이미 삭제된 커뮤니티입니다.');
+ }
+ }
+ await Promise.all(
+ community.users.map((user_id) =>
+ this.deleteCommunityAtUserDocument(user_id, community._id.toString()),
+ ),
+ );
+ }
+ async verfiyCommunity(community_id: string) {
+ const community = await this.communityRepository.findOne({ _id: community_id });
+ if (!community) {
+ throw new BadRequestException('해당하는 커뮤니티의 _id가 올바르지 않습니다.');
+ }
+ return community;
+ }
+
+ deleteCommunityAtUserDocument(user_id: string, community_id: string) {
+ this.userRepository.deleteElementAtArr(
+ { _id: user_id },
+ {
+ communities: [community_id],
+ },
+ );
+ }
+}
diff --git a/server/apps/api/src/community/dto/append-participants-to-community.dto.ts b/server/apps/api/src/community/dto/append-participants-to-community.dto.ts
new file mode 100644
index 00000000..fb6b8ab7
--- /dev/null
+++ b/server/apps/api/src/community/dto/append-participants-to-community.dto.ts
@@ -0,0 +1,23 @@
+import {
+ ArrayNotEmpty,
+ IsArray,
+ IsNotEmpty,
+ IsNumber,
+ IsOptional,
+ IsString,
+} from 'class-validator';
+
+export class AppendUsersToCommunityDto {
+ @IsNotEmpty()
+ @IsString()
+ community_id: string;
+
+ @IsOptional()
+ @IsString()
+ requestUser_id: string;
+
+ @IsNotEmpty()
+ @IsArray()
+ @ArrayNotEmpty()
+ users: string[];
+}
diff --git a/server/apps/api/src/community/dto/create-community.dto.ts b/server/apps/api/src/community/dto/create-community.dto.ts
new file mode 100644
index 00000000..7d7d173d
--- /dev/null
+++ b/server/apps/api/src/community/dto/create-community.dto.ts
@@ -0,0 +1,22 @@
+import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
+
+export class CreateCommunityDto {
+ @IsNotEmpty()
+ @IsString()
+ name: string;
+
+ @IsOptional()
+ @IsString()
+ managerId: string;
+
+ @IsOptional()
+ @IsString()
+ description: string;
+
+ @IsOptional()
+ @IsString()
+ profileUrl: string;
+
+ @IsOptional()
+ users: string[];
+}
diff --git a/server/apps/api/src/community/dto/delete-community.dto.ts b/server/apps/api/src/community/dto/delete-community.dto.ts
new file mode 100644
index 00000000..0ad34141
--- /dev/null
+++ b/server/apps/api/src/community/dto/delete-community.dto.ts
@@ -0,0 +1,11 @@
+import { IsNotEmpty, IsString } from 'class-validator';
+
+export class DeleteCommunityDto {
+ @IsNotEmpty()
+ @IsString()
+ community_id: string;
+
+ @IsNotEmpty()
+ @IsString()
+ managerId: string;
+}
diff --git a/server/apps/api/src/community/dto/index.ts b/server/apps/api/src/community/dto/index.ts
new file mode 100644
index 00000000..a50668c8
--- /dev/null
+++ b/server/apps/api/src/community/dto/index.ts
@@ -0,0 +1,4 @@
+export * from './append-participants-to-community.dto';
+export * from './create-community.dto';
+export * from './modify-community.dto';
+export * from './delete-community.dto';
diff --git a/server/apps/api/src/community/dto/modify-community.dto.ts b/server/apps/api/src/community/dto/modify-community.dto.ts
new file mode 100644
index 00000000..18a5a292
--- /dev/null
+++ b/server/apps/api/src/community/dto/modify-community.dto.ts
@@ -0,0 +1,23 @@
+import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
+
+export class ModifyCommunityDto {
+ @IsNotEmpty()
+ @IsString()
+ community_id: string;
+
+ @IsOptional()
+ @IsString()
+ name: string;
+
+ @IsOptional()
+ @IsString()
+ managerId: string;
+
+ @IsOptional()
+ @IsString()
+ description: string;
+
+ @IsOptional()
+ @IsString()
+ profileUrl: string;
+}
diff --git a/server/apps/api/src/community/helper/checkUserIsInCommunity.ts b/server/apps/api/src/community/helper/checkUserIsInCommunity.ts
new file mode 100644
index 00000000..eb279d47
--- /dev/null
+++ b/server/apps/api/src/community/helper/checkUserIsInCommunity.ts
@@ -0,0 +1,2 @@
+export const IsUserInCommunity = (user, communityId) =>
+ (user.communities ?? false) && Array.from(user.communities.keys()).includes(communityId);
diff --git a/server/apps/api/src/community/helper/index.ts b/server/apps/api/src/community/helper/index.ts
new file mode 100644
index 00000000..cd9c8465
--- /dev/null
+++ b/server/apps/api/src/community/helper/index.ts
@@ -0,0 +1,2 @@
+export * from './checkUserIsInCommunity';
+export * from './makeCommunityObj';
diff --git a/server/apps/api/src/community/helper/makeCommunityObj.ts b/server/apps/api/src/community/helper/makeCommunityObj.ts
new file mode 100644
index 00000000..108db276
--- /dev/null
+++ b/server/apps/api/src/community/helper/makeCommunityObj.ts
@@ -0,0 +1,8 @@
+export const makeCommunityObj = (community_id: string) => {
+ const newCommunity = {};
+ newCommunity[`communities.${community_id}`] = {
+ _id: community_id,
+ channels: {},
+ };
+ return newCommunity;
+};
diff --git a/server/apps/api/src/main.ts b/server/apps/api/src/main.ts
index 22661db8..8e5e4cdb 100644
--- a/server/apps/api/src/main.ts
+++ b/server/apps/api/src/main.ts
@@ -8,7 +8,11 @@ import * as cookieParser from 'cookie-parser';
async function bootstrap() {
const app = await NestFactory.create(ApiModule);
- app.enableCors();
+ app.enableCors({
+ origin: true,
+ methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS',
+ credentials: true,
+ });
if (process.env.NODE_ENV == 'prod') {
Sentry.init({
dsn: process.env.SENTRY_DSN,
diff --git a/server/apps/api/src/user/dto/community-in-user.dto.ts b/server/apps/api/src/user/dto/community-in-user.dto.ts
new file mode 100644
index 00000000..07a69aee
--- /dev/null
+++ b/server/apps/api/src/user/dto/community-in-user.dto.ts
@@ -0,0 +1,4 @@
+export interface communityInUser {
+ _id: string;
+ channels?: Map;
+}
diff --git a/server/apps/api/src/user/dto/index.ts b/server/apps/api/src/user/dto/index.ts
new file mode 100644
index 00000000..c9dac182
--- /dev/null
+++ b/server/apps/api/src/user/dto/index.ts
@@ -0,0 +1,4 @@
+export * from './modify-user.dto';
+export * from './create-user.dto';
+export * from './follower.dto';
+export * from './user-basic-info.dto';
diff --git a/server/apps/api/src/user/user.controller.ts b/server/apps/api/src/user/user.controller.ts
index 5d24caed..d077262a 100644
--- a/server/apps/api/src/user/user.controller.ts
+++ b/server/apps/api/src/user/user.controller.ts
@@ -8,15 +8,16 @@ import {
Param,
Patch,
Post,
+ Req,
Res,
+ UseGuards,
} from '@nestjs/common';
import { UserService } from './user.service';
-import { FollowerDto } from '@user/dto/follower.dto';
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
import { responseForm } from '@utils/responseForm';
import { ObjectIdValidationPipe } from '@custom_pipe/mongodbObjectIdValidation.pipe';
-import { ModifyUserDto } from '@user/dto/modify-user.dto';
-import { Response } from 'express';
+import { JwtAccessGuard } from '@api/src/auth/guard';
+import { FollowerDto, ModifyUserDto } from './dto';
@Controller('api/user')
export class UserController {
@@ -25,33 +26,25 @@ export class UserController {
private userService: UserService,
) {}
- // @Get()
- // getUsers() {
- // const createUserDto: CreateUserDto = { id: 'mj', pw: 'mjpw' };
- // this.userService.createUser(createUserDto);
- // return 'hello user';
- // }
-
@Post('following/:id')
- async addFollowing(@Param('id', ObjectIdValidationPipe) id: string) {
+ @UseGuards(JwtAccessGuard)
+ async addFollowing(@Param('id', ObjectIdValidationPipe) id: string, @Req() req: any) {
try {
- const myId = '63786b635d4f08bbe0c940de';
- // TODO: Request Header에서 access token으로 현재 사용자 알아내기
+ const myId = req.user._id;
const addFollowingDto: FollowerDto = { myId, followId: id };
const result = await this.userService.toggleFollowing(addFollowingDto);
return responseForm(200, result);
} catch (error) {
this.logger.error(JSON.stringify(error.response));
- // res.status(400).json(error.response);
throw error;
}
}
@Get('followers')
- async getFollowers() {
+ @UseGuards(JwtAccessGuard)
+ async getFollowers(@Req() req: any) {
try {
- const _id = '63786b635d4f08bbe0c940dc';
- // TODO: Request Header에서 access token으로 현재 사용자 알아내기
+ const _id = req.user._id;
const result = await this.userService.getRelatedUsers(_id, 'followers');
return responseForm(200, { followers: result });
} catch (error) {
@@ -61,10 +54,10 @@ export class UserController {
}
@Get('followings')
- async getFollowings() {
+ @UseGuards(JwtAccessGuard)
+ async getFollowings(@Req() req: any) {
try {
- const _id = '63739b643969101c3fec884';
- // TODO: Request Header에서 access token으로 현재 사용자 알아내기
+ const _id = req.user._id;
const result = await this.userService.getRelatedUsers(_id, 'followings');
return responseForm(200, { followings: result });
} catch (error) {
@@ -77,7 +70,7 @@ export class UserController {
async getUser(@Param('id') id: string) {
try {
const result = await this.userService.getUser(id);
- return responseForm(200, { result });
+ return responseForm(200, { users: result });
} catch (error) {
this.logger.error(JSON.stringify(error.response));
throw error;
@@ -85,10 +78,10 @@ export class UserController {
}
@Patch('settings')
- async modifyUserSetting(@Body() modifyUserDto: ModifyUserDto) {
+ @UseGuards(JwtAccessGuard)
+ async modifyUserSetting(@Body() modifyUserDto: ModifyUserDto, @Req() req: any) {
try {
- const _id = '63786b635d4f08bbe0c940de';
- // TODO: Request Header에서 access token으로 현재 사용자 알아내기
+ const _id = req.user._id;
await this.userService.modifyUser({ ...modifyUserDto, _id });
return responseForm(200, {});
} catch (error) {
@@ -96,9 +89,4 @@ export class UserController {
throw error;
}
}
-
- // @Post()
- // createUser(@Body() createUserDto: CreateUserDto) {
- // this.usersService.createUser(createUserDto);
- // }
}
diff --git a/server/apps/api/src/user/user.module.ts b/server/apps/api/src/user/user.module.ts
index 7a7310b2..8726018d 100644
--- a/server/apps/api/src/user/user.module.ts
+++ b/server/apps/api/src/user/user.module.ts
@@ -2,15 +2,14 @@ import { Module } from '@nestjs/common';
import { UserController } from '@user/user.controller';
import { UserService } from '@user/user.service';
import { MongooseModule } from '@nestjs/mongoose';
-import { WinstonModule } from 'nest-winston';
import { User, UserSchema } from '@schemas/user.schema';
import { AuthModule } from '../auth/auth.module';
import { UserRepository } from '@repository/user.repository';
-import { AuthController } from '../auth/auth.controller';
@Module({
imports: [MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]), AuthModule],
controllers: [UserController],
providers: [UserService, UserRepository],
+ exports: [MongooseModule],
})
export class UserModule {}
diff --git a/server/apps/api/src/user/user.service.ts b/server/apps/api/src/user/user.service.ts
index 32508f9b..2526a47c 100644
--- a/server/apps/api/src/user/user.service.ts
+++ b/server/apps/api/src/user/user.service.ts
@@ -1,8 +1,6 @@
import { BadRequestException, ConflictException, Injectable, Param } from '@nestjs/common';
-import { FollowerDto } from '@user/dto/follower.dto';
import { UserRepository } from '@repository/user.repository';
-import { getUserBasicInfo } from '@user/dto/user-basic-info.dto';
-import { ModifyUserDto } from '@user/dto/modify-user.dto';
+import { FollowerDto, ModifyUserDto, getUserBasicInfo } from './dto';
@Injectable()
export class UserService {
diff --git a/server/apps/api/test/community.service.spec.ts b/server/apps/api/test/community.service.spec.ts
new file mode 100644
index 00000000..36870c3c
--- /dev/null
+++ b/server/apps/api/test/community.service.spec.ts
@@ -0,0 +1,54 @@
+import { Test, TestingModule } from '@nestjs/testing';
+import { getModelToken } from '@nestjs/mongoose';
+import * as _ from 'lodash';
+import { CommunityService } from '@api/src/community/community.service';
+import { Community } from '@schemas/community.schema';
+import { CommunityRepository } from '@repository/community.repository';
+import { user1 } from '@mock/user.mock';
+import { UserRepository } from '@repository/user.repository';
+import { communityDto1 } from '@mock/community.mock';
+import { UserModule } from '@user/user.module';
+import { User } from '@schemas/user.schema';
+
+describe('[Community Service]', () => {
+ let communityService: CommunityService;
+ let communityRepository: CommunityRepository;
+ let userRepository: UserRepository;
+
+ beforeEach(async () => {
+ const module: TestingModule = await Test.createTestingModule({
+ providers: [
+ CommunityService,
+ CommunityRepository,
+ UserRepository,
+ {
+ provide: getModelToken(Community.name),
+ useFactory: () => {},
+ },
+ {
+ provide: getModelToken(User.name),
+ useFactory: () => {},
+ },
+ ],
+ }).compile();
+
+ communityService = module.get(CommunityService);
+ communityRepository = module.get(CommunityRepository);
+ userRepository = module.get(UserRepository);
+ });
+
+ it('should be defined', () => {
+ expect(communityService).toBeDefined();
+ });
+
+ describe('[createCommunity] 커뮤니티 생성', () => {
+ it('커뮤니티 생성 정상 동작', async () => {
+ const community1 = _.cloneDeep(user1);
+ community1.users = [user1._id];
+ jest.spyOn(userRepository, 'findById').mockResolvedValue(user1);
+ jest.spyOn(communityRepository, 'create').mockResolvedValue(community1);
+ const result = await communityService.createCommunity(communityDto1);
+ expect(result).toEqual(community1);
+ });
+ });
+});
diff --git a/server/dao/repository/channel.repository.ts b/server/dao/repository/channel.repository.ts
new file mode 100644
index 00000000..fd182f90
--- /dev/null
+++ b/server/dao/repository/channel.repository.ts
@@ -0,0 +1,18 @@
+import { Injectable } from '@nestjs/common';
+import { InjectModel } from '@nestjs/mongoose';
+import { Model } from 'mongoose';
+import { Channel, ChannelDocument } from '@schemas/channel.schema';
+import { CreateChannelDto } from '@api/src/channel/dto';
+
+@Injectable()
+export class ChannelRepository {
+ constructor(@InjectModel(Channel.name) private channelModel: Model) {}
+
+ async create(createChannelDto: CreateChannelDto) {
+ return await this.channelModel.create(createChannelDto);
+ }
+
+ async findOne(condition: any) {
+ return await this.channelModel.findOne(condition);
+ }
+}
diff --git a/server/dao/repository/community.repository.ts b/server/dao/repository/community.repository.ts
new file mode 100644
index 00000000..5376e0f5
--- /dev/null
+++ b/server/dao/repository/community.repository.ts
@@ -0,0 +1,38 @@
+import { Injectable } from '@nestjs/common';
+import { InjectModel } from '@nestjs/mongoose';
+import { Model } from 'mongoose';
+import { Community, CommunityDocument } from '@schemas/community.schema';
+import { CreateCommunityDto } from '@api/src/community/dto/create-community.dto';
+
+@Injectable()
+export class CommunityRepository {
+ constructor(@InjectModel(Community.name) private communityModel: Model) {}
+
+ async create(createCommunityDto: CreateCommunityDto) {
+ const result = await this.communityModel.create(createCommunityDto);
+ return (result as any)._doc;
+ }
+
+ async findById(_id: string) {
+ return await this.communityModel.findById(_id);
+ }
+
+ async addArrAtArr(filter, attribute, appendArr) {
+ const addArr = {};
+ addArr[attribute] = { $each: appendArr };
+ return await this.communityModel.findOneAndUpdate(filter, { $addToSet: addArr }, { new: true });
+ // console.log('pass');
+ }
+
+ async findOne(condition: any) {
+ return await this.communityModel.findOne(condition);
+ }
+
+ async updateOne(filter, updateField) {
+ await this.communityModel.updateOne(filter, updateField);
+ }
+
+ async findAndUpdateOne(filter, updateField) {
+ return await this.communityModel.findOneAndUpdate(filter, updateField, { new: true });
+ }
+}
diff --git a/server/dao/repository/user.repository.ts b/server/dao/repository/user.repository.ts
index 60d99b9d..66e7536d 100644
--- a/server/dao/repository/user.repository.ts
+++ b/server/dao/repository/user.repository.ts
@@ -33,7 +33,31 @@ export class UserRepository {
await this.userModel.updateOne(filter, { $push: appendElement });
}
+ async updateObject(filter, appendElement) {
+ return await this.userModel.updateOne(filter, { $set: appendElement });
+ }
+
+ async deleteObject(filter, appendElement) {
+ await this.userModel.updateOne(filter, { $unset: appendElement });
+ }
+
async deleteElementAtArr(filter, removeElement) {
await this.userModel.updateOne(filter, { $pullAll: removeElement });
}
+
+ async deleteElementAtArr2(_id, removeElement) {
+ await this.userModel.findByIdAndUpdate(_id, { $pullAll: removeElement }, { new: true });
+ }
+
+ async addArrAtArr(filter, attribute, appendArr) {
+ const addArr = {};
+ addArr[attribute] = { $each: appendArr };
+ return await this.userModel.findByIdAndUpdate(filter, { $addToSet: addArr }, { new: true });
+ }
+
+ // async set(filter, obj) {
+ // const user = new User();
+ //
+ // this.userModel.find(filter).communities.set();
+ // }
}
diff --git a/server/dao/schemas/channel.schema.ts b/server/dao/schemas/channel.schema.ts
index 605b3d0d..5d77e202 100644
--- a/server/dao/schemas/channel.schema.ts
+++ b/server/dao/schemas/channel.schema.ts
@@ -1,30 +1,28 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
-import { Document } from 'mongoose';
+import mongoose, { Document } from 'mongoose';
import { CHANNEL_TYPE } from '@utils/def';
-import { IsBoolean, IsIn, IsNotEmpty, IsString } from 'class-validator';
+import { IsBoolean, IsIn, IsNotEmpty, IsString, Length } from 'class-validator';
export type ChannelDocument = Channel & Document;
-@Schema()
+@Schema({ timestamps: true })
export class Channel {
@Prop({
required: true,
- unique: true,
})
@IsString()
@IsNotEmpty()
+ @Length(2, 20)
name: string;
@Prop({
required: true,
- unique: true,
})
@IsString()
communityId: string;
@Prop({
required: true,
- unique: true,
})
@IsString()
@IsNotEmpty()
@@ -53,5 +51,8 @@ export class Channel {
@Prop()
@IsString()
chatLists: string[];
+
+ @Prop({ type: mongoose.Schema.Types.Date })
+ deletedAt: Date;
}
export const ChannelSchema = SchemaFactory.createForClass(Channel);
diff --git a/server/dao/schemas/community.schema.ts b/server/dao/schemas/community.schema.ts
index 87b231f8..e6fe5806 100644
--- a/server/dao/schemas/community.schema.ts
+++ b/server/dao/schemas/community.schema.ts
@@ -4,7 +4,7 @@ import { IsNotEmpty, IsString } from 'class-validator';
export type CommunityDocument = Community & Document;
-@Schema()
+@Schema({ timestamps: true })
export class Community {
@Prop()
@IsString()
@@ -13,7 +13,6 @@ export class Community {
@Prop({
required: true,
- unique: true,
})
@IsString()
@IsNotEmpty()
@@ -27,12 +26,6 @@ export class Community {
@IsString()
profileUrl: string;
- @Prop({ default: new Date(), type: mongoose.Schema.Types.Date })
- createdAt: Date;
-
- @Prop({ default: new Date(), type: mongoose.Schema.Types.Date })
- updatedAt: Date;
-
@Prop({ type: mongoose.Schema.Types.Date })
deletedAt: Date;
diff --git a/server/dao/schemas/user.schema.ts b/server/dao/schemas/user.schema.ts
index b7ec5881..9c0597f3 100644
--- a/server/dao/schemas/user.schema.ts
+++ b/server/dao/schemas/user.schema.ts
@@ -1,11 +1,11 @@
-import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
+import { Prop, raw, Schema, SchemaFactory } from '@nestjs/mongoose';
import { IsIn, IsString } from 'class-validator';
import mongoose, { Document } from 'mongoose';
import { STATUS } from '@utils/def';
export type UserDocument = User & Document;
-@Schema()
+@Schema({ timestamps: true })
export class User {
@Prop()
name: string;
@@ -49,12 +49,6 @@ export class User {
@Prop({ default: new Date(), type: mongoose.Schema.Types.Date })
lastLogin: Date;
- @Prop({ default: new Date(), type: mongoose.Schema.Types.Date })
- createdAt: Date;
-
- @Prop({ default: new Date(), type: mongoose.Schema.Types.Date })
- updatedAt: Date;
-
@Prop({ type: mongoose.Schema.Types.Date })
deletedAt: Date;
@@ -66,7 +60,13 @@ export class User {
@IsString()
followers: string[];
- @Prop()
- communities: string[];
+ @Prop(
+ raw({
+ type: Map,
+ of: new mongoose.Schema({ _id: { type: String }, channels: { type: Map, of: Date } }),
+ }),
+ )
+ communities;
}
+
export const UserSchema = SchemaFactory.createForClass(User);
diff --git a/server/tsconfig.json b/server/tsconfig.json
index b43f558a..28fdce23 100644
--- a/server/tsconfig.json
+++ b/server/tsconfig.json
@@ -23,6 +23,7 @@
"@utils/*": ["utils/*"],
"@custom_pipe/*": ["custom_pipe/*"],
"@user/*": ["apps/api/src/user/*"],
+ "@community/*": ["apps/api/src/community/*"],
"@api/*": ["apps/api/*"],
"@mock/*": ["__mock__/*"],
}
diff --git a/server/utils/addObjectForm.ts b/server/utils/addObjectForm.ts
new file mode 100644
index 00000000..1514c0ed
--- /dev/null
+++ b/server/utils/addObjectForm.ts
@@ -0,0 +1,6 @@
+export function addChannelToUserForm(communityId, channelId) {
+ const newChannel = {};
+ newChannel[`communities.${communityId.toString()}.channels.${channelId.toString()}`] = new Date();
+
+ return newChannel;
+}
diff --git a/shared/lib/user.ts b/shared/lib/user.ts
index 2b1bf864..ba778278 100644
--- a/shared/lib/user.ts
+++ b/shared/lib/user.ts
@@ -1,15 +1,10 @@
+export type UserStatus = 'online' | 'offline' | 'afk';
+
export interface User {
_id: string;
id: string;
nickname: string;
- status: string;
+ status: UserStatus;
profileUrl: string;
descrption: string;
}
-
-export interface GetUsersReponse {
- statusCode: number;
- result: {
- users: User[];
- };
-}
diff --git a/yarn.lock b/yarn.lock
index e4da8cb0..ad0153df 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1844,6 +1844,11 @@
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
+"@faker-js/faker@^7.6.0":
+ version "7.6.0"
+ resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-7.6.0.tgz#9ea331766084288634a9247fcd8b84f16ff4ba07"
+ integrity sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw==
+
"@graphql-tools/merge@8.3.11":
version "8.3.11"
resolved "https://registry.yarnpkg.com/@graphql-tools/merge/-/merge-8.3.11.tgz#f5eab764e8d7032c1b7e32d5dc6dea5b2f5bb21e"
@@ -2529,6 +2534,14 @@
multer "1.4.4-lts.1"
tslib "2.4.1"
+"@nestjs/platform-socket.io@^9.2.0":
+ version "9.2.0"
+ resolved "https://registry.yarnpkg.com/@nestjs/platform-socket.io/-/platform-socket.io-9.2.0.tgz#5194a13d4ef5c70b32b2bcc64379e07674ddf0ab"
+ integrity sha512-ttxXtqHV3Cpk5AfZOxfE8urILV5oLBpG21vdyqUHiL0YDuhHdc2tBz5GKSYAfsWefmVeQQiBAV9dqaa23Rf0nQ==
+ dependencies:
+ socket.io "4.5.3"
+ tslib "2.4.1"
+
"@nestjs/schematics@^9.0.0":
version "9.0.3"
resolved "https://registry.yarnpkg.com/@nestjs/schematics/-/schematics-9.0.3.tgz#175218350fb3829c9a903e980046a11950310e24"
@@ -2547,6 +2560,15 @@
dependencies:
tslib "2.4.1"
+"@nestjs/websockets@^9.2.0":
+ version "9.2.0"
+ resolved "https://registry.yarnpkg.com/@nestjs/websockets/-/websockets-9.2.0.tgz#cbe8d446eff653d9c63234ef396ccc1ea031e875"
+ integrity sha512-AbG4eN9p9O6QmNSOWsk0lrA+CtHkrdDkogcl1sGyTrg+LRd6IUlkaTu9fFK9Hl6o7bs2ieGgDmxAvl+Xd156Aw==
+ dependencies:
+ iterare "1.2.1"
+ object-hash "3.0.0"
+ tslib "2.4.1"
+
"@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3":
version "2.1.8-no-fsevents.3"
resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz#323d72dd25103d0c4fbdce89dadf574a787b1f9b"
@@ -2782,6 +2804,11 @@
"@types/node" ">=12.0.0"
axios "^0.21.4"
+"@socket.io/component-emitter@~3.1.0":
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553"
+ integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==
+
"@tanstack/match-sorter-utils@8.1.1":
version "8.1.1"
resolved "https://registry.yarnpkg.com/@tanstack/match-sorter-utils/-/match-sorter-utils-8.1.1.tgz#895f407813254a46082a6bbafad9b39b943dc834"
@@ -2964,6 +2991,11 @@
resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.2.tgz#66ad9331f63fe8a3d3d9d8c6e3906dd10f6446e8"
integrity sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==
+"@types/cors@^2.8.12":
+ version "2.8.12"
+ resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080"
+ integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==
+
"@types/debug@^4.1.7":
version "4.1.7"
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82"
@@ -3125,7 +3157,7 @@
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197"
integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==
-"@types/node@*", "@types/node@>=12.0.0", "@types/node@>=8.9.0":
+"@types/node@*", "@types/node@>=10.0.0", "@types/node@>=12.0.0", "@types/node@>=8.9.0":
version "18.11.9"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.9.tgz#02d013de7058cea16d36168ef2fc653464cfbad4"
integrity sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==
@@ -3196,6 +3228,13 @@
dependencies:
"@types/react" "*"
+"@types/react-modal@^3.13.1":
+ version "3.13.1"
+ resolved "https://registry.yarnpkg.com/@types/react-modal/-/react-modal-3.13.1.tgz#5b9845c205fccc85d9a77966b6e16dc70a60825a"
+ integrity sha512-iY/gPvTDIy6Z+37l+ibmrY+GTV4KQTHcCyR5FIytm182RQS69G5ps4PH2FxtC7bAQ2QRHXMevsBgck7IQruHNg==
+ dependencies:
+ "@types/react" "*"
+
"@types/react@*", "@types/react@^18.0.25":
version "18.0.25"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.25.tgz#8b1dcd7e56fe7315535a4af25435e0bb55c8ae44"
@@ -3242,6 +3281,13 @@
dependencies:
"@types/node" "*"
+"@types/socket.io@^3.0.2":
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/@types/socket.io/-/socket.io-3.0.2.tgz#606c9639e3f93bb8454cba8f5f0a283d47917759"
+ integrity sha512-pu0sN9m5VjCxBZVK8hW37ZcMe8rjn4HHggBN5CbaRTvFwv5jOmuIRZEuddsBPa9Th0ts0SIo3Niukq+95cMBbQ==
+ dependencies:
+ socket.io "*"
+
"@types/sockjs@^0.3.33":
version "0.3.33"
resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.33.tgz#570d3a0b99ac995360e3136fd6045113b1bd236f"
@@ -3640,6 +3686,11 @@ acorn@^8.0.4, acorn@^8.1.0, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.1, acorn@^8.8
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73"
integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==
+add-px-to-style@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/add-px-to-style/-/add-px-to-style-1.0.0.tgz#d0c135441fa8014a8137904531096f67f28f263a"
+ integrity sha512-YMyxSlXpPjD8uWekCQGuN40lV4bnZagUwqa2m/uFv1z/tNImSk9fnXVMUI5qwME/zzI3MMQRvjZ+69zyfSSyew==
+
agent-base@6:
version "6.0.2"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
@@ -4069,6 +4120,11 @@ base64-js@^1.3.1:
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
+base64id@2.0.0, base64id@~2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6"
+ integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==
+
batch@0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16"
@@ -4650,7 +4706,7 @@ cookie@0.5.0:
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b"
integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==
-cookie@^0.4.1, cookie@^0.4.2:
+cookie@^0.4.1, cookie@^0.4.2, cookie@~0.4.1:
version "0.4.2"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432"
integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==
@@ -4689,7 +4745,7 @@ core-util-is@~1.0.0:
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
-cors@2.8.5:
+cors@2.8.5, cors@~2.8.5:
version "2.8.5"
resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==
@@ -4899,7 +4955,7 @@ debug@2.6.9, debug@^2.6.9:
dependencies:
ms "2.0.0"
-debug@4, debug@4.x, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4:
+debug@4, debug@4.x, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
@@ -5118,6 +5174,15 @@ dom-converter@^0.2.0:
dependencies:
utila "~0.4"
+dom-css@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/dom-css/-/dom-css-2.1.0.tgz#fdbc2d5a015d0a3e1872e11472bbd0e7b9e6a202"
+ integrity sha512-w9kU7FAbaSh3QKijL6n59ofAhkkmMJ31GclJIz/vyQdjogfyxcB6Zf8CZyibOERI5o0Hxz30VmJS7+7r5fEj2Q==
+ dependencies:
+ add-px-to-style "1.0.0"
+ prefix-style "2.0.1"
+ to-camel-case "1.0.0"
+
dom-serializer@^1.0.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30"
@@ -5242,6 +5307,27 @@ end-of-stream@^1.1.0:
dependencies:
once "^1.4.0"
+engine.io-parser@~5.0.3:
+ version "5.0.4"
+ resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.4.tgz#0b13f704fa9271b3ec4f33112410d8f3f41d0fc0"
+ integrity sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==
+
+engine.io@~6.2.0:
+ version "6.2.1"
+ resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.2.1.tgz#e3f7826ebc4140db9bbaa9021ad6b1efb175878f"
+ integrity sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA==
+ dependencies:
+ "@types/cookie" "^0.4.1"
+ "@types/cors" "^2.8.12"
+ "@types/node" ">=10.0.0"
+ accepts "~1.3.4"
+ base64id "2.0.0"
+ cookie "~0.4.1"
+ cors "~2.8.5"
+ debug "~4.3.1"
+ engine.io-parser "~5.0.3"
+ ws "~8.2.3"
+
enhanced-resolve@^5.0.0, enhanced-resolve@^5.10.0, enhanced-resolve@^5.7.0:
version "5.10.0"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.10.0.tgz#0dc579c3bb2a1032e357ac45b8f3a6f3ad4fb1e6"
@@ -5748,6 +5834,11 @@ execa@^5.0.0:
signal-exit "^3.0.3"
strip-final-newline "^2.0.0"
+exenv@^1.2.0:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d"
+ integrity sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==
+
exit@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
@@ -8091,7 +8182,7 @@ logform@^2.3.2, logform@^2.4.0:
safe-stable-stringify "^2.3.1"
triple-beam "^1.3.0"
-loose-envify@^1.1.0, loose-envify@^1.4.0:
+loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@@ -8946,6 +9037,11 @@ pause@0.0.1:
resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d"
integrity sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==
+performance-now@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
+ integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==
+
picocolors@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
@@ -9270,6 +9366,11 @@ postcss@^8.4.17, postcss@^8.4.18, postcss@^8.4.19:
picocolors "^1.0.0"
source-map-js "^1.0.2"
+prefix-style@2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/prefix-style/-/prefix-style-2.0.1.tgz#66bba9a870cfda308a5dc20e85e9120932c95a06"
+ integrity sha512-gdr1MBNVT0drzTq95CbSNdsrBDoHGlb2aDJP/FoY+1e+jSDPOb1Cv554gH2MGiSr2WTcXi/zu+NaFzfcHQkfBQ==
+
prelude-ls@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
@@ -9346,7 +9447,7 @@ prompts@^2.0.1:
kleur "^3.0.3"
sisteransi "^1.0.5"
-prop-types@^15.8.1:
+prop-types@^15.5.10, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -9413,6 +9514,13 @@ quick-lru@^5.1.1:
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932"
integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==
+raf@^3.1.0:
+ version "3.4.1"
+ resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
+ integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==
+ dependencies:
+ performance-now "^2.1.0"
+
randombytes@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
@@ -9435,6 +9543,15 @@ raw-body@2.5.1:
iconv-lite "0.4.24"
unpipe "1.0.0"
+react-custom-scrollbars-2@^4.5.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/react-custom-scrollbars-2/-/react-custom-scrollbars-2-4.5.0.tgz#cff18e7368bce9d570aea0be780045eda392c745"
+ integrity sha512-/z0nWAeXfMDr4+OXReTpYd1Atq9kkn4oI3qxq3iMXGQx1EEfwETSqB8HTAvg1X7dEqcCachbny1DRNGlqX5bDQ==
+ dependencies:
+ dom-css "^2.0.0"
+ prop-types "^15.5.10"
+ raf "^3.1.0"
+
react-dom@^18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
@@ -9463,6 +9580,21 @@ react-is@^18.0.0:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
+react-lifecycles-compat@^3.0.0:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
+ integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
+
+react-modal@^3.16.1:
+ version "3.16.1"
+ resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-3.16.1.tgz#34018528fc206561b1a5467fc3beeaddafb39b2b"
+ integrity sha512-VStHgI3BVcGo7OXczvnJN7yT2TWHJPDXZWyI/a0ssFNhGZWsPmB8cF0z33ewDXq4VfYMO1vXgiv/g8Nj9NDyWg==
+ dependencies:
+ exenv "^1.2.0"
+ prop-types "^15.7.2"
+ react-lifecycles-compat "^3.0.0"
+ warning "^4.0.3"
+
react-refresh@^0.14.0:
version "0.14.0"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.0.tgz#4e02825378a5f227079554d4284889354e5f553e"
@@ -10040,6 +10172,31 @@ smart-buffer@^4.2.0:
resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae"
integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==
+socket.io-adapter@~2.4.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.4.0.tgz#b50a4a9ecdd00c34d4c8c808224daa1a786152a6"
+ integrity sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg==
+
+socket.io-parser@~4.2.0:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.1.tgz#01c96efa11ded938dcb21cbe590c26af5eff65e5"
+ integrity sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g==
+ dependencies:
+ "@socket.io/component-emitter" "~3.1.0"
+ debug "~4.3.1"
+
+socket.io@*, socket.io@4.5.3, socket.io@^4.5.3:
+ version "4.5.3"
+ resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.5.3.tgz#44dffea48d7f5aa41df4a66377c386b953bc521c"
+ integrity sha512-zdpnnKU+H6mOp7nYRXH4GNv1ux6HL6+lHL8g7Ds7Lj8CkdK1jJK/dlwsKDculbyOHifcJ0Pr/yeXnZQ5GeFrcg==
+ dependencies:
+ accepts "~1.3.4"
+ base64id "~2.0.0"
+ debug "~4.3.2"
+ engine.io "~6.2.0"
+ socket.io-adapter "~2.4.0"
+ socket.io-parser "~4.2.0"
+
sockjs@^0.3.24:
version "0.3.24"
resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce"
@@ -10553,11 +10710,23 @@ tmpl@1.0.5:
resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc"
integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==
+to-camel-case@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/to-camel-case/-/to-camel-case-1.0.0.tgz#1a56054b2f9d696298ce66a60897322b6f423e46"
+ integrity sha512-nD8pQi5H34kyu1QDMFjzEIYqk0xa9Alt6ZfrdEMuHCFOfTLhDG5pgTu/aAM9Wt9lXILwlXmWP43b8sav0GNE8Q==
+ dependencies:
+ to-space-case "^1.0.0"
+
to-fast-properties@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==
+to-no-case@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/to-no-case/-/to-no-case-1.0.2.tgz#c722907164ef6b178132c8e69930212d1b4aa16a"
+ integrity sha512-Z3g735FxuZY8rodxV4gH7LxClE4H0hTIyHNIHdk+vpQxjLm0cwnKXq/OFVZ76SOQmto7txVcwSCwkU5kqp+FKg==
+
to-regex-range@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
@@ -10565,6 +10734,13 @@ to-regex-range@^5.0.1:
dependencies:
is-number "^7.0.0"
+to-space-case@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/to-space-case/-/to-space-case-1.0.0.tgz#b052daafb1b2b29dc770cea0163e5ec0ebc9fc17"
+ integrity sha512-rLdvwXZ39VOn1IxGL3V6ZstoTbwLRckQmn/U8ZDLuWwIXNpuZDhQ3AiRUlhTbOXFVE9C+dR51wM0CBDhk31VcA==
+ dependencies:
+ to-no-case "^1.0.0"
+
toidentifier@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
@@ -10926,6 +11102,13 @@ walker@^1.0.8:
dependencies:
makeerror "1.0.12"
+warning@^4.0.3:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
+ integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==
+ dependencies:
+ loose-envify "^1.0.0"
+
watchpack@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"
@@ -11309,6 +11492,11 @@ ws@^5.2.0:
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591"
integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==
+ws@~8.2.3:
+ version "8.2.3"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba"
+ integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==
+
xml-name-validator@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835"