+
-
-
- {usersQuery.data?.users.length ? (
-
- {usersQuery.data.users.map((user) => (
-
- ))}
-
+
+
+ {usersQuery.isLoading && usersQuery.isFetching ? (
+
로딩중...
+ ) : usersQuery.isLoading ? (
+
검색어를 입력해주세요
+ ) : usersQuery.error ? (
+
에러가 발생했습니다.
) : (
-
- 검색된 사용자가 없습니다
-
+
)}
-
+
);
};
diff --git a/client/src/mocks/data/chats.js b/client/src/mocks/data/chats.js
new file mode 100644
index 00000000..819c27bd
--- /dev/null
+++ b/client/src/mocks/data/chats.js
@@ -0,0 +1,15 @@
+import { faker } from '@faker-js/faker';
+
+import { users } from './users';
+
+export const createMockChat = () => ({
+ id: faker.datatype.uuid(),
+ type: 'TEXT',
+ content: faker.lorem.sentences(),
+ senderId: users[0]._id,
+ updatedAt: '',
+ createdAt: new Date().toISOString(),
+ deletedAt: '',
+});
+
+export const chats = [...Array(20)].map(createMockChat);
diff --git a/client/src/mocks/data/communities.js b/client/src/mocks/data/communities.js
new file mode 100644
index 00000000..492aab64
--- /dev/null
+++ b/client/src/mocks/data/communities.js
@@ -0,0 +1,30 @@
+import { faker } from '@faker-js/faker';
+
+import { chancify } from '../utils/rand';
+import { channelUsers, users } from './users';
+
+export const createMockChannel = () => ({
+ _id: faker.datatype.uuid(),
+ managerId: users[0]._id,
+ name: faker.name.jobType(),
+ isPrivate: chancify(() => true, 50, false),
+ profileUrl: chancify(() => faker.image.avatar(), 50),
+ description: faker.lorem.sentence(1),
+ lastRead: chancify(() => true, 50, false),
+ type: 'Channel',
+ users: channelUsers,
+ createdAt: new Date().toISOString(),
+});
+
+export const channels = [...Array(10)].map(createMockChannel);
+
+export const createMockCommunities = () => ({
+ _id: faker.datatype.uuid(),
+ name: faker.name.jobType(),
+ managerId: faker.datatype.uuid(),
+ profileUrl: chancify(() => faker.image.avatar(), 50),
+ description: faker.lorem.sentence(1),
+ channels,
+});
+
+export const communities = [...Array(15)].map(createMockCommunities);
diff --git a/client/src/mocks/data/users.js b/client/src/mocks/data/users.js
index 1990c30d..a7edc1db 100644
--- a/client/src/mocks/data/users.js
+++ b/client/src/mocks/data/users.js
@@ -6,9 +6,12 @@ export const createMockUser = () => ({
_id: faker.datatype.uuid(),
id: faker.internet.email(),
nickname: faker.name.fullName(),
- status: ['online', 'offline', 'afk'][getRandomInt(3)],
+ status: ['ONLINE', 'OFFLINE', 'AFK'][getRandomInt(3)],
profileUrl: faker.image.avatar(),
description: faker.lorem.sentence(),
});
export const users = [...Array(30)].map(createMockUser);
+export const me = users.at(0);
+export const communityUsers = users.slice(0, 10);
+export const channelUsers = users.slice(0, 10);
diff --git a/client/src/mocks/handlers/Auth.js b/client/src/mocks/handlers/Auth.js
index b06b703b..68adc7cb 100644
--- a/client/src/mocks/handlers/Auth.js
+++ b/client/src/mocks/handlers/Auth.js
@@ -1,15 +1,16 @@
+import endPoint from '@constants/endPoint';
import { API_URL } from '@constants/url';
import { rest } from 'msw';
import { users } from '../data/users';
-const BASE_URL = `${API_URL}/api`;
const devCookies = {
refreshTokenKey: 'dev_refreshToken',
};
// 회원가입
-const SignUp = rest.post(`${BASE_URL}/user/auth/signup`, (req, res, ctx) => {
+const signUpEndPoint = API_URL + endPoint.signUp();
+const SignUp = rest.post(signUpEndPoint, (req, res, ctx) => {
// 응답 메세지 성공-실패를 토글하려면 이 값을 바꿔주세요.
const ERROR = true;
@@ -39,7 +40,8 @@ const SignUp = rest.post(`${BASE_URL}/user/auth/signup`, (req, res, ctx) => {
// 로그인
// 응답 json 데이터로 Access token, 쿠키로 Refresh token을 받는다.
-const SignIn = rest.post(`${BASE_URL}/user/auth/signin`, (req, res, ctx) => {
+const signInEndPoint = API_URL + endPoint.signIn();
+const SignIn = rest.post(signInEndPoint, (req, res, ctx) => {
// 응답 메세지 성공-실패를 토글하려면 이 값을 바꿔주세요.
const ERROR = false;
@@ -70,66 +72,63 @@ const SignIn = rest.post(`${BASE_URL}/user/auth/signin`, (req, res, ctx) => {
});
// 토큰 재발급
-const ReissueToken = rest.post(
- `${BASE_URL}/user/auth/refresh`,
- (req, res, ctx) => {
- // 응답 메세지 성공-실패를 토글하려면 이 값을 바꿔주세요.
- const existsRefreshToken = !!req.cookies[devCookies.refreshTokenKey];
-
- const ERROR = !existsRefreshToken || false;
- const isUnknownError = true;
-
- const successResponse = res(
- ctx.status(200),
- ctx.delay(1000),
- ctx.json({
- statusCode: 200,
- result: {
- accessToken: 'accessToken',
- },
- }),
- );
-
- const unAuthErrorResponse = res(
- ctx.status(401),
- ctx.delay(1000),
- ctx.json({
- statusCode: 401,
- message: 'Unauthorized',
- error: '',
- }),
- );
-
- const unknownErrorResponse = res(
- ctx.status(502),
- ctx.delay(1000),
- ctx.json({
- statusCode: 502,
- message: 'Unknown',
- error: '',
- }),
- );
-
- const errorResponse = isUnknownError
- ? unknownErrorResponse
- : unAuthErrorResponse;
-
- return ERROR ? errorResponse : successResponse;
- },
-);
-
-export const GetMyInfo = rest.get(
- `${BASE_URL}/user/auth/me`,
- (req, res, ctx) => {
- return res(
- ctx.delay(300),
- ctx.status(200),
- ctx.json({
- statusCode: 200,
- result: users[0],
- }),
- );
- },
-);
+const reissueTokenEndPoint = API_URL + endPoint.reissueToken();
+const ReissueToken = rest.post(reissueTokenEndPoint, (req, res, ctx) => {
+ // 응답 메세지 성공-실패를 토글하려면 이 값을 바꿔주세요.
+ const existsRefreshToken = !!req.cookies[devCookies.refreshTokenKey];
+
+ const ERROR = !existsRefreshToken || false;
+ const isUnknownError = true;
+
+ const successResponse = res(
+ ctx.status(200),
+ ctx.delay(1000),
+ ctx.json({
+ statusCode: 200,
+ result: {
+ accessToken: 'accessToken',
+ },
+ }),
+ );
+
+ const unAuthErrorResponse = res(
+ ctx.status(401),
+ ctx.delay(1000),
+ ctx.json({
+ statusCode: 401,
+ message: 'Unauthorized',
+ error: '',
+ }),
+ );
+
+ const unknownErrorResponse = res(
+ ctx.status(502),
+ ctx.delay(1000),
+ ctx.json({
+ statusCode: 502,
+ message: 'Unknown',
+ error: '',
+ }),
+ );
+
+ const errorResponse = isUnknownError
+ ? unknownErrorResponse
+ : unAuthErrorResponse;
+
+ return ERROR ? errorResponse : successResponse;
+});
+
+const getMyInfoEndPoint = API_URL + endPoint.getMyInfo();
+
+export const GetMyInfo = rest.get(getMyInfoEndPoint, (req, res, ctx) => {
+ return res(
+ ctx.delay(300),
+ ctx.status(200),
+ ctx.json({
+ statusCode: 200,
+ result: users[0],
+ }),
+ );
+});
export default [SignUp, SignIn, GetMyInfo, ReissueToken];
diff --git a/client/src/mocks/handlers/Channel.js b/client/src/mocks/handlers/Channel.js
new file mode 100644
index 00000000..78e2057a
--- /dev/null
+++ b/client/src/mocks/handlers/Channel.js
@@ -0,0 +1,70 @@
+import endPoint from '@constants/endPoint';
+import { API_URL } from '@constants/url';
+import { rest } from 'msw';
+
+import { communities } from '../data/communities';
+import { me } from '../data/users';
+import {
+ createErrorContext,
+ createSuccessContext,
+} from '../utils/createContext';
+
+const getChannelEndPoint = API_URL + endPoint.getChannel(':channelId');
+const GetChannel = rest.get(getChannelEndPoint, (req, res, ctx) => {
+ const { channelId } = req.params;
+ const ERROR = false;
+
+ let targetChannel;
+
+ communities.forEach(({ channels: _channels }) => {
+ targetChannel = _channels.find(({ _id }) => _id === channelId);
+ });
+
+ if (!targetChannel) targetChannel = communities[0].channels[0];
+
+ const errorResponse = res(...createErrorContext(ctx));
+ const successResponse = res(
+ ...createSuccessContext(ctx, 200, 500, {
+ ...targetChannel,
+ }),
+ );
+
+ return ERROR ? errorResponse : successResponse;
+});
+
+const createChannelEndPoint = API_URL + endPoint.createChannel();
+const CreateChannel = rest.post(
+ createChannelEndPoint,
+ async (req, res, ctx) => {
+ const { communityId, name, isPrivate, description, profileUrl, type } =
+ await req.json();
+
+ const ERROR = false;
+
+ const newChannel = {
+ _id: crypto.randomUUID(),
+ managerId: me._id,
+ name,
+ isPrivate,
+ profileUrl,
+ description,
+ lastRead: true,
+ type,
+ };
+
+ const errorResponse = res(...createErrorContext(ctx));
+ const successResponse = res(
+ ...createSuccessContext(ctx, 200, 500, newChannel),
+ );
+
+ const targetCommunity = communities.find(
+ (community) => community._id === communityId,
+ );
+
+ targetCommunity.channels.push(newChannel);
+
+ return ERROR ? errorResponse : successResponse;
+ },
+);
+
+export default [GetChannel, CreateChannel];
diff --git a/client/src/mocks/handlers/Chat.js b/client/src/mocks/handlers/Chat.js
new file mode 100644
index 00000000..42fa6b45
--- /dev/null
+++ b/client/src/mocks/handlers/Chat.js
@@ -0,0 +1,44 @@
+import { API_URL } from '@constants/url';
+import { rest } from 'msw';
+
+import { createMockChat } from '../data/chats';
+import {
+ createErrorContext,
+ createSuccessContext,
+} from '../utils/createContext';
+
+const BASE_URL = `${API_URL}/api`;
+
+const MAX_PREVIOUS_PAGE = 2;
+const GetChats = rest.get(`${BASE_URL}/chat/:channelId`, (req, res, ctx) => {
+ // const { channelId } = req.params;
+ const prev = Number(req.url.searchParams.get('prev'));
+ // const nextCursor = req.url.searchParams.get('next');
+
+ const ERROR = false;
+
+ // prevCursor가 undefined이거나 0이면 그대로 undefined를 반환한다.
+ // prevCursor가 -1이면 첫 요청이라는 뜻이므로 최대 페이지 개수를 반환한다.
+ // 그렇지 않으면 prevCursor를 1 줄여 보낸다.
+ const newPrevCursor =
+ isNaN(prev) || prev === 0
+ ? undefined
+ : prev === -1
+ ? MAX_PREVIOUS_PAGE
+ : prev - 1;
+
+ console.log(newPrevCursor);
+ const errorResponse = res(...createErrorContext(ctx));
+ const successResponse = res(
+ ...createSuccessContext(ctx, 200, 1000, {
+ prev: newPrevCursor,
+ chat: Number.isInteger(newPrevCursor)
+ ? [...Array(10)].map(createMockChat)
+ : [],
+ }),
+ );
+
+ return ERROR ? errorResponse : successResponse;
+});
+
+export default [GetChats];
diff --git a/client/src/mocks/handlers/Community.js b/client/src/mocks/handlers/Community.js
new file mode 100644
index 00000000..dffb83f0
--- /dev/null
+++ b/client/src/mocks/handlers/Community.js
@@ -0,0 +1,131 @@
+import endPoint from '@constants/endPoint';
+import { API_URL } from '@constants/url';
+import { rest } from 'msw';
+
+import { communities } from '../data/communities';
+import { communityUsers, users } from '../data/users';
+import {
+ createErrorContext,
+ createSuccessContext,
+} from '../utils/createContext';
+import { colorLog } from '../utils/logging';
+
+const getCommunitiesEndPoint = API_URL + endPoint.getCommunities();
+const GetCommunities = rest.get(getCommunitiesEndPoint, (req, res, ctx) => {
+ const ERROR = false;
+
+ const errorResponse = res(...createErrorContext(ctx));
+
+ const successResponse = res(
+ ...createSuccessContext(ctx, 200, 500, { communities }),
+ );
+
+ return ERROR ? errorResponse : successResponse;
+});
+
+// 커뮤니티 생성
+const createCommunityEndPoint = API_URL + endPoint.createCommunity();
+const CreateCommunity = rest.post(
+ createCommunityEndPoint,
+ async (req, res, ctx) => {
+ const ERROR = false;
+ const { name, description } = await req.json();
+
+ const newCommunity = {
+ name,
+ managerId: '6379beb15d4f08bbe0c940e9',
+ description,
+ profileUrl: '',
+ createdAt: '2022-11-21T10:07:14.390Z',
+ updatedAt: '2022-11-21T10:07:14.390Z',
+ channels: [],
+ users: ['6379beb15d4f08bbe0c940e9'],
+ _id: crypto.randomUUID(),
+ __v: 0,
+ };
+
+ const successResponse = res(
+ ...createSuccessContext(ctx, 201, 500, newCommunity),
+ );
+
+ const errorResponse = res(...createErrorContext(ctx));
+
+ if (!ERROR) {
+ // eslint-disable-next-line no-shadow
+ const { name, _id, managerId, profileUrl, description } = newCommunity;
+
+ communities.push({
+ name,
+ _id,
+ managerId,
+ profileUrl,
+ description,
+ channels: [],
+ });
+ }
+
+ return ERROR ? errorResponse : successResponse;
+ },
+);
+
+const LeaveCommunityEndPoint = endPoint.leaveCommunity(':communityId');
+const LeaveCommunity = rest.delete(LeaveCommunityEndPoint, (req, res, ctx) => {
+ const { communityId } = req.params;
+
+ const ERROR = false;
+ const successDelay = 500;
+
+ const successResponse = res(
+ ...createSuccessContext(ctx, 201, successDelay, {
+ message: '커뮤니티 퇴장 성공~',
+ }),
+ );
+
+ const errorResponse = res(...createErrorContext(ctx));
+
+ if (!ERROR) {
+ setTimeout(() => {
+ colorLog(`커뮤니티 ID ${communityId}에서 퇴장하였습니다.`);
+ const targetIdx = communities.findIndex(({ _id }) => _id === communityId);
+
+ communities.splice(targetIdx, 1);
+ }, successDelay);
+ }
+
+ return ERROR ? errorResponse : successResponse;
+});
+
+const inviteCommunityEndPoint =
+ API_URL + endPoint.inviteCommunity(':communityId');
+const InviteCommunity = rest.post(
+ inviteCommunityEndPoint,
+ async (req, res, ctx) => {
+ const { users: userIds } = await req.json();
+
+ const ERROR = false;
+ const successDelay = 500;
+
+ const successResponse = res(
+ ...createSuccessContext(ctx, 201, successDelay, {
+ message: '커뮤니티 초대 성공~',
+ }),
+ );
+
+ const errorResponse = res(...createErrorContext(ctx));
+
+ if (!ERROR) {
+ setTimeout(() => {
+ communityUsers.push(users.find(({ _id }) => _id === userIds[0]));
+ }, successDelay);
+ }
+
+ return ERROR ? errorResponse : successResponse;
+ },
+);
+
+export default [
+ GetCommunities,
+ CreateCommunity,
+ LeaveCommunity,
+ InviteCommunity,
+];
diff --git a/client/src/mocks/handlers/DM.js b/client/src/mocks/handlers/DM.js
index a8e6b504..71074da6 100644
--- a/client/src/mocks/handlers/DM.js
+++ b/client/src/mocks/handlers/DM.js
@@ -1,4 +1,5 @@
import { API_URL } from '@constants/url';
+import { faker } from '@faker-js/faker';
import { rest } from 'msw';
import { users } from '../data/users';
@@ -11,9 +12,15 @@ const GetDirectMessages = rest.get(
ctx.status(200),
ctx.json({
statusCode: 200,
- result: [...users.slice(0, 5)].map((user, idx) => ({
- _id: idx,
- user,
+ result: [...users.slice(5, 10)].map((user) => ({
+ _id: faker.datatype.uuid(),
+ name: '',
+ users: [users[0]._id, user._id],
+ profileUrl: '',
+ description: '',
+ managerId: [user._id],
+ isPrivate: true,
+ type: 'DM',
})),
}),
);
diff --git a/client/src/mocks/handlers/Friend.js b/client/src/mocks/handlers/Friend.js
index 71558100..f2b36a2f 100644
--- a/client/src/mocks/handlers/Friend.js
+++ b/client/src/mocks/handlers/Friend.js
@@ -1,46 +1,42 @@
+import endPoint from '@constants/endPoint';
import { API_URL } from '@constants/url';
import { rest } from 'msw';
import { users } from '../data/users';
-const BASE_URL = `${API_URL}/api`;
-
-const GetFollowings = rest.get(
- `${BASE_URL}/user/followings`,
- (req, res, ctx) => {
- return res(
- ctx.delay(),
- ctx.status(200),
- ctx.json({
- statusCode: 200,
- result: {
- followings: users,
- },
- }),
- );
- },
-);
-
-const UpdateFollowing = rest.post(
- `${BASE_URL}/user/following/:userId`,
- (req, res, ctx) => {
- const { userId } = req.params;
- const idx = users.findIndex((user) => user._id === userId);
-
- users.splice(idx, 1);
-
- return res(
- ctx.delay(),
- ctx.status(200),
- ctx.json({
- statusCode: 200,
- result: {},
- }),
- );
- },
-);
-
-const GetFollowers = rest.get(`${BASE_URL}/user/followers`, (req, res, ctx) => {
+const getFollowingsEndPoint = API_URL + endPoint.getFollowings();
+const GetFollowings = rest.get(getFollowingsEndPoint, (req, res, ctx) => {
+ return res(
+ ctx.delay(),
+ ctx.status(200),
+ ctx.json({
+ statusCode: 200,
+ result: {
+ followings: users,
+ },
+ }),
+ );
+});
+
+const toggleFollowingEndPoint = API_URL + endPoint.toggleFollowing(':userId');
+const UpdateFollowing = rest.post(toggleFollowingEndPoint, (req, res, ctx) => {
+ const { userId } = req.params;
+ const idx = users.findIndex((user) => user._id === userId);
+
+ users.splice(idx, 1);
+
+ return res(
+ ctx.delay(),
+ ctx.status(200),
+ ctx.json({
+ statusCode: 200,
+ result: {},
+ }),
+ );
+});
+
+const getFollowersEndPoint = API_URL + endPoint.getFollowers();
+const GetFollowers = rest.get(getFollowersEndPoint, (req, res, ctx) => {
return res(
ctx.delay(),
ctx.status(200),
diff --git a/client/src/mocks/handlers/User.js b/client/src/mocks/handlers/User.js
index 358eae5e..4c0e5aa9 100644
--- a/client/src/mocks/handlers/User.js
+++ b/client/src/mocks/handlers/User.js
@@ -1,9 +1,16 @@
+import endPoint from '@constants/endPoint';
import { API_URL } from '@constants/url';
import { rest } from 'msw';
-import { users } from '../data/users';
+import { communityUsers, users } from '../data/users';
+import {
+ createErrorContext,
+ createSuccessContext,
+} from '../utils/createContext';
-const GetFilteredUsers = rest.get(`${API_URL}/api/users`, (req, res, ctx) => {
+const getUsersEndPoint = API_URL + endPoint.getUsers();
+
+const GetUsers = rest.get(getUsersEndPoint, (req, res, ctx) => {
const search = req.url.searchParams.get('search').toUpperCase();
return res(
@@ -22,4 +29,23 @@ const GetFilteredUsers = rest.get(`${API_URL}/api/users`, (req, res, ctx) => {
);
});
-export default [GetFilteredUsers];
+const getCommunityUsersEndPoint =
+ API_URL + endPoint.getCommunityUsers(':communityId');
+const GetCommunityUsers = rest.get(
+ getCommunityUsersEndPoint,
+ (req, res, ctx) => {
+ const ERROR = false;
+
+ // communityUsers는 users의 부분집합
+ // 커뮤니티 초대 모달에서 유저를 검색하면, 일부는 이미 커뮤니티에 속해있는 것을 재현할 수 있음.
+
+ const errorResponse = res(...createErrorContext(ctx));
+ const successResponse = res(
+ ...createSuccessContext(ctx, 200, 500, { users: communityUsers }),
+ );
+
+ return ERROR ? errorResponse : successResponse;
+ },
+);
+
+export default [GetUsers, GetCommunityUsers];
diff --git a/client/src/mocks/handlers/index.js b/client/src/mocks/handlers/index.js
index a411c0e4..8c1d6f85 100644
--- a/client/src/mocks/handlers/index.js
+++ b/client/src/mocks/handlers/index.js
@@ -1,4 +1,7 @@
import AuthHandlers from './Auth';
+import ChannelHandlers from './Channel';
+import ChatHandlers from './Chat';
+import CommunityHandlers from './Community';
import DMHandlers from './DM';
import FriendHandlers from './Friend';
import UserHandlers from './User';
@@ -8,4 +11,7 @@ export const handlers = [
...FriendHandlers,
...UserHandlers,
...DMHandlers,
+ ...CommunityHandlers,
+ ...ChannelHandlers,
+ ...ChatHandlers,
];
diff --git a/client/src/mocks/utils/createContext.js b/client/src/mocks/utils/createContext.js
new file mode 100644
index 00000000..31b8fd8c
--- /dev/null
+++ b/client/src/mocks/utils/createContext.js
@@ -0,0 +1,47 @@
+/**
+ * @param ctx
+ * @param status {number}
+ * @param delay {number}
+ * @param result {any}
+ * @returns {[*,*,*]}
+ * @description ### `res(...creatSuccessContext(ctx));`와 같이 반드시 스프레드 연산자를 사용하세요.
+ */
+export const createSuccessContext = (
+ ctx,
+ status = 200,
+ delay = 500,
+ result = {},
+) => [
+ ctx.status(status),
+ ctx.delay(delay),
+ ctx.json({
+ statusCode: status,
+ result,
+ }),
+];
+
+/**
+ *
+ * @param ctx
+ * @param status {number}
+ * @param delay {number}
+ * @param messages {string | string[]}
+ * @param error {string}
+ * @returns {[*,*,*]}
+ * @description ### `res(...createErrorContext(ctx));`와 같이 반드시 스프레드 연산자를 사용하세요.
+ */
+export const createErrorContext = (
+ ctx,
+ status = 400,
+ delay = 500,
+ messages = '에러가 발생했습니다!',
+ error = '',
+) => [
+ ctx.status(status),
+ ctx.delay(delay),
+ ctx.json({
+ statusCode: status,
+ messages,
+ error,
+ }),
+];
diff --git a/client/src/mocks/utils/logging.js b/client/src/mocks/utils/logging.js
new file mode 100644
index 00000000..1971cd16
--- /dev/null
+++ b/client/src/mocks/utils/logging.js
@@ -0,0 +1,4 @@
+export const colorLog = (message, textColor) => {
+ textColor = textColor ? `color: ${textColor}` : `color: #bada55`;
+ console.log(`%c${message}`, textColor);
+};
diff --git a/client/src/mocks/utils/rand.js b/client/src/mocks/utils/rand.js
index d4fa77ef..514fb2b9 100644
--- a/client/src/mocks/utils/rand.js
+++ b/client/src/mocks/utils/rand.js
@@ -1,3 +1,25 @@
export const getRandomInt = (max) => {
return Math.floor(Math.random() * max);
};
+
+export const getRandomBool = () => Boolean(getRandomInt(2));
+
+/**
+ * @param fn {Function} 실행할 함수
+ * @param percent {number} 0 ~ 100 사이의 숫자
+ * @param notExecutedReturnValue {any} 실행되지 않았을 때 반환할 값
+ * @description n% 확률로 함수를 실행한 결과를 반환.
+ */
+export const chancify = (fn, percent, notExecutedReturnValue = undefined) => {
+ if (percent <= 0) {
+ percent = 0;
+ }
+
+ if (percent >= 100) {
+ percent = 100;
+ }
+
+ percent /= 100;
+
+ return Math.random() < percent ? fn() : notExecutedReturnValue;
+};
diff --git a/client/src/pages/Channel/index.tsx b/client/src/pages/Channel/index.tsx
new file mode 100644
index 00000000..47b06d4f
--- /dev/null
+++ b/client/src/pages/Channel/index.tsx
@@ -0,0 +1,99 @@
+import ChannelMetadata from '@components/ChannelMetadata';
+import ChatItem from '@components/ChatItem';
+import { useChannelQuery } from '@hooks/channel';
+import { useChatsInfiniteQuery } from '@hooks/chat';
+import useIsIntersecting from '@hooks/useIsIntersecting';
+import React, { useRef, useEffect, Fragment } from 'react';
+import Scrollbars from 'react-custom-scrollbars-2';
+import { useParams } from 'react-router-dom';
+
+const Channel = () => {
+ const params = useParams();
+ const roomId = params.roomId as string;
+ const { channelQuery } = useChannelQuery(roomId);
+
+ // TODO: `any` 말고 적절한 타이핑 주기
+ const scrollbarContainerRef = useRef
(null);
+ const fetchPreviousRef = useRef(null);
+ const isFetchPreviousIntersecting =
+ useIsIntersecting(fetchPreviousRef);
+
+ const chatsInfiniteQuery = useChatsInfiniteQuery(roomId);
+
+ useEffect(() => {
+ if (
+ !isFetchPreviousIntersecting ||
+ !chatsInfiniteQuery.hasPreviousPage ||
+ chatsInfiniteQuery.isFetchingPreviousPage
+ )
+ return;
+ chatsInfiniteQuery.fetchPreviousPage();
+ }, [isFetchPreviousIntersecting]);
+
+ if (channelQuery.isLoading || chatsInfiniteQuery.isLoading)
+ return loading
;
+
+ return (
+
+
+
+
+
+ {chatsInfiniteQuery.isFetchingPreviousPage &&
+ '지난 메시지 불러오는 중'}
+
+
+
+
+
+
+
+ 온라인, 오프라인
+
+
+
+ );
+};
+
+export default Channel;
diff --git a/client/src/pages/Community/index.tsx b/client/src/pages/Community/index.tsx
index d2d95b2a..a110e950 100644
--- a/client/src/pages/Community/index.tsx
+++ b/client/src/pages/Community/index.tsx
@@ -1,7 +1,12 @@
import React from 'react';
+import { Outlet } from 'react-router-dom';
const Community = () => {
- return Community
;
+ return (
+
+
+
+ );
};
export default Community;
diff --git a/client/src/pages/Friends/index.tsx b/client/src/pages/Friends/index.tsx
index dd16ff97..ba39d5d4 100644
--- a/client/src/pages/Friends/index.tsx
+++ b/client/src/pages/Friends/index.tsx
@@ -5,15 +5,13 @@ import Followings from '@layouts/Followings';
import UserSearch from '@layouts/UserSearch';
import React, { useState } from 'react';
-// TODO: 네이밍 생각해보기
const TAB = {
FOLLOWINGS: 'followings',
FOLLOWERS: 'followers',
USER_SEARCH: 'user-search',
} as const;
-// TODO: 필드 이름 수정하기
-const tabs = [
+const tabData = [
{
name: '팔로잉',
tab: 'followings',
@@ -28,7 +26,6 @@ const tabs = [
},
] as const;
-// TODO: 컴포넌트 이름 수정하기 (FollowingTab -> Followings)
const TabPanel: Record = {
[TAB.FOLLOWINGS]: ,
[TAB.FOLLOWERS]: ,
@@ -43,17 +40,18 @@ const Friends = () => {
);
return (
-
+
- {tabs.map(({ name, tab: t }) => (
+ {tabData.map(({ name, tab: _tab }) => (
-
-
diff --git a/client/src/pages/Home/index.tsx b/client/src/pages/Home/index.tsx
index aeb911e0..3bd14e9c 100644
--- a/client/src/pages/Home/index.tsx
+++ b/client/src/pages/Home/index.tsx
@@ -1,4 +1,5 @@
-import CreateCommunityModal from '@components/Modals/CreateCommunityModal';
+import CommonModal from '@components/Modals/CommonModal';
+import ContextMenuModal from '@components/Modals/ContextMenuModal';
import Gnb from '@layouts/Gnb';
import Sidebar from '@layouts/Sidebar';
import React from 'react';
@@ -10,7 +11,8 @@ const Home = () => {
-
+
+
);
};
diff --git a/client/src/pages/SignIn/index.tsx b/client/src/pages/SignIn/index.tsx
index 84d87bc2..3491377b 100644
--- a/client/src/pages/SignIn/index.tsx
+++ b/client/src/pages/SignIn/index.tsx
@@ -29,7 +29,7 @@ const SignIn = () => {
const signInMutation = useSignInMutation({
onSuccess: (data) => {
- setAccessToken(data.result.accessToken);
+ setAccessToken(data.accessToken);
navigate('/dms');
},
onError: (error) => {
diff --git a/client/src/queryKeyCreator.ts b/client/src/queryKeyCreator.ts
index 31e04bce..370c7c4b 100644
--- a/client/src/queryKeyCreator.ts
+++ b/client/src/queryKeyCreator.ts
@@ -1,9 +1,41 @@
+const userQueryKey = {
+ all: () => ['users'] as const,
+ communityUsers: (communityId: string) =>
+ [...userQueryKey.all(), { communityId }] as const,
+ channelUsers: (channelId: string) =>
+ [...userQueryKey.all(), { channelId }] as const,
+};
+
const directMessageQueryKey = {
all: ['directMessages'] as const,
list: () => [...directMessageQueryKey.all] as const,
detail: (id: string) => [...directMessageQueryKey.all, id] as const,
} as const;
+const communityQueryKey = {
+ all: () => ['communities'] as const,
+ createCommunity: () => ['createCommunity'] as const,
+ removeCommunity: () => ['removeCommunity'] as const,
+ detail: (communityId: string) =>
+ [...communityQueryKey.all(), 'detail', communityId] as const,
+ leaveCommunity: () => ['leaveCommunity'] as const,
+ inviteCommunity: () => ['inviteCommunity'] as const,
+};
+
+const channelQueryKey = {
+ all: () => ['channels'] as const,
+ list: (communityId: string) =>
+ [...channelQueryKey.all(), 'list', communityId] as const,
+ detail: (channelId: string) =>
+ [...channelQueryKey.all(), 'detail', channelId] as const,
+ createChannel: () => ['createChannel'] as const,
+};
+
+const chatQueryKey = {
+ all: () => ['chats'] as const,
+ list: (channelId: string) => [...chatQueryKey.all(), { channelId }] as const,
+};
+
const queryKeyCreator = {
me: () => ['me'] as const,
signUp: () => ['signUp'] as const,
@@ -13,6 +45,10 @@ const queryKeyCreator = {
reissueToken: () => ['reissueToken'] as const,
userSearch: (filter: string) => ['userSearch', { filter }],
directMessage: directMessageQueryKey,
+ community: communityQueryKey,
+ channel: channelQueryKey,
+ user: userQueryKey,
+ chat: chatQueryKey,
} as const;
export default queryKeyCreator;
diff --git a/client/src/stores/commonModalSlice.ts b/client/src/stores/commonModalSlice.ts
new file mode 100644
index 00000000..f91292d1
--- /dev/null
+++ b/client/src/stores/commonModalSlice.ts
@@ -0,0 +1,68 @@
+import type { ReactNode } from 'react';
+import type { StateCreator } from 'zustand';
+
+import { immer } from 'zustand/middleware/immer';
+
+type OverlayBackground = 'white' | 'black' | 'transparent';
+
+export interface CommonModal {
+ isOpen: boolean;
+ overlayBackground: OverlayBackground;
+ x: number | string;
+ y: number | string;
+ transform?: string;
+ content?: ReactNode;
+ onCancel?: () => void;
+ onSubmit?: () => void;
+}
+
+type SetCommonModal = (commonModalState: Partial
) => void;
+type OpenCommonModal = (
+ commonModalState: Partial>,
+) => void;
+
+export interface CommonModalSlice {
+ commonModal: CommonModal;
+ setCommonModal: SetCommonModal;
+ openCommonModal: OpenCommonModal;
+ closeCommonModal: () => void;
+}
+
+const initialCommonModalValue = {
+ isOpen: false,
+ content: undefined,
+ overlayBackground: 'transparent',
+ onCancel: undefined,
+ onSubmit: undefined,
+ transform: undefined,
+ x: 0,
+ y: 0,
+} as const;
+
+export const commonModalSlice: StateCreator<
+ CommonModalSlice,
+ [],
+ [['zustand/immer', never], ...[]],
+ CommonModalSlice
+> = immer((set) => ({
+ commonModal: initialCommonModalValue,
+ setCommonModal: (commonModalState) =>
+ set((state) => {
+ state.commonModal = {
+ ...state.commonModal,
+ ...commonModalState,
+ };
+ }),
+ openCommonModal: (commonModalState) =>
+ set((state) => {
+ state.commonModal = {
+ ...state.commonModal,
+ ...commonModalState,
+ isOpen: true,
+ };
+ }),
+ closeCommonModal: () =>
+ set((state) => {
+ state.commonModal = initialCommonModalValue;
+ }),
+}));
diff --git a/client/src/stores/contextMenuModalSlice.ts b/client/src/stores/contextMenuModalSlice.ts
new file mode 100644
index 00000000..900202eb
--- /dev/null
+++ b/client/src/stores/contextMenuModalSlice.ts
@@ -0,0 +1,63 @@
+import type { ReactNode } from 'react';
+import type { StateCreator } from 'zustand';
+
+import { immer } from 'zustand/middleware/immer';
+
+export interface ContextMenuModal {
+ isOpen: boolean;
+ x: number | string;
+ y: number | string;
+ transform?: string;
+ content?: ReactNode;
+}
+
+type SetContextMenuModal = (
+ contextMenuModalState: Partial,
+) => void;
+
+type OpenContextMenuModal = (
+ contextMenuModal: Partial>,
+) => void;
+
+export interface ContextMenuModalSlice {
+ contextMenuModal: ContextMenuModal;
+ setContextMenuModal: SetContextMenuModal;
+ openContextMenuModal: OpenContextMenuModal;
+ closeContextMenuModal: () => void;
+}
+
+const initialContextMenuModalValue = {
+ isOpen: false,
+ x: 0,
+ y: 0,
+ transform: undefined,
+ content: undefined,
+} as const;
+
+export const contextMenuModalSlice: StateCreator<
+ ContextMenuModalSlice,
+ [],
+ [['zustand/immer', never], ...[]],
+ ContextMenuModalSlice
+> = immer((set) => ({
+ contextMenuModal: initialContextMenuModalValue,
+ setContextMenuModal: (contextMenuModalState) =>
+ set((state) => {
+ state.contextMenuModal = {
+ ...state.contextMenuModal,
+ ...contextMenuModalState,
+ };
+ }),
+ openContextMenuModal: (contextMenuModalState) =>
+ set((state) => {
+ state.contextMenuModal = {
+ ...state.contextMenuModal,
+ ...contextMenuModalState,
+ isOpen: true,
+ };
+ }),
+ closeContextMenuModal: () =>
+ set((state) => {
+ state.contextMenuModal = initialContextMenuModalValue;
+ }),
+}));
diff --git a/client/src/stores/modalSlice.ts b/client/src/stores/modalSlice.ts
deleted file mode 100644
index 4fca1f10..00000000
--- a/client/src/stores/modalSlice.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-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
index f4421b24..b17ab276 100644
--- a/client/src/stores/rootStore.ts
+++ b/client/src/stores/rootStore.ts
@@ -1,14 +1,16 @@
-import type { ModalSlice } from '@stores/modalSlice';
+import type { CommonModalSlice } from '@stores/commonModalSlice';
+import type { ContextMenuModalSlice } from '@stores/contextMenuModalSlice';
+import { commonModalSlice } from '@stores/commonModalSlice';
+import { contextMenuModalSlice } from '@stores/contextMenuModalSlice';
import store from 'zustand';
import { devtools } from 'zustand/middleware';
-import { createModalSlice } from './modalSlice';
+export type Store = ContextMenuModalSlice & CommonModalSlice;
-export type Store = ModalSlice;
-
-export const useStore = store()(
+export const useRootStore = store()(
devtools((...a) => ({
- ...createModalSlice(...a),
+ ...contextMenuModalSlice(...a),
+ ...commonModalSlice(...a),
})),
);
diff --git a/client/src/utils/date.ts b/client/src/utils/date.ts
new file mode 100644
index 00000000..582aacb9
--- /dev/null
+++ b/client/src/utils/date.ts
@@ -0,0 +1,10 @@
+export const dateStringToKRLocaleDateString = (
+ str: string,
+ options: Intl.DateTimeFormatOptions = {},
+) =>
+ new Date(str).toLocaleDateString('ko', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ ...options,
+ });
diff --git a/client/tsconfig.json b/client/tsconfig.json
index d7bde1a6..cc237550 100644
--- a/client/tsconfig.json
+++ b/client/tsconfig.json
@@ -18,6 +18,7 @@
"@apis/*": ["src/apis/*"],
"@hooks/*": ["src/hooks/*"],
"@errors/*": ["src/errors/*"],
+ "@utils/*": ["src/utils/*"],
"@@types/*": ["src/types/*"],
"@/*": ["src/*"]
}
diff --git a/yarn.lock b/yarn.lock
index df7766c3..35d89cf4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2534,14 +2534,6 @@
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"
@@ -2560,15 +2552,6 @@
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"
@@ -2804,11 +2787,6 @@
"@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"
@@ -2991,11 +2969,6 @@
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"
@@ -3157,7 +3130,7 @@
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197"
integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==
-"@types/node@*", "@types/node@>=10.0.0", "@types/node@>=12.0.0", "@types/node@>=8.9.0":
+"@types/node@*", "@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==
@@ -3281,13 +3254,6 @@
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"
@@ -4132,11 +4098,6 @@ 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"
@@ -4723,7 +4684,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.1, cookie@^0.4.2:
version "0.4.2"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432"
integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==
@@ -4762,7 +4723,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==
@@ -4972,7 +4933,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.3.1, debug@~4.3.2:
+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:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
@@ -6669,6 +6630,11 @@ ignore@^5.2.0:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a"
integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==
+immer@^9.0.16:
+ version "9.0.16"
+ resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.16.tgz#8e7caab80118c2b54b37ad43e05758cdefad0198"
+ integrity sha512-qenGE7CstVm1NrHQbMh8YaSzTZTFNP3zPqr3YU0S0UY441j4bJTg4A2Hh5KAhwgaiU6ZZ1Ar6y/2f4TblnMReQ==
+
import-fresh@^3.0.0, import-fresh@^3.2.1:
version "3.3.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
@@ -11624,11 +11590,6 @@ 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"