From 6b5a5cd829c9dd96e8e32af0d08ffb147f530ddf Mon Sep 17 00:00:00 2001 From: twalla26 Date: Tue, 26 Nov 2024 21:49:49 +0900 Subject: [PATCH 01/86] =?UTF-8?q?feat:=20=EC=A7=88=EB=AC=B8=EC=A7=80=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../question-list/question-list.controller.ts | 34 +++++++++++++++++++ .../src/question-list/question-list.entity.ts | 4 ++- .../question-list/question-list.repository.ts | 4 +++ .../question-list/question-list.service.ts | 7 ++++ 4 files changed, 48 insertions(+), 1 deletion(-) diff --git a/backend/src/question-list/question-list.controller.ts b/backend/src/question-list/question-list.controller.ts index 9f767306..a219d104 100644 --- a/backend/src/question-list/question-list.controller.ts +++ b/backend/src/question-list/question-list.controller.ts @@ -159,6 +159,40 @@ export class QuestionListController { } } + @Delete("/:questionListId") + @UseGuards(AuthGuard("jwt")) + async deleteQuestionList( + @Res() res, + @JwtPayload() token: IJwtPayload, + @Param("questionListId") questionListId: number + ) { + try { + const userId = token.userId; + const result = await this.questionListService.deleteQuestionList( + questionListId, + userId + ); + + if (result.affected) { + return res.send({ + success: true, + message: "Question list is deleted successfully.", + }); + } else { + return res.send({ + success: true, + message: "Failed to delete question list.", + }); + } + } catch (error) { + return res.send({ + success: true, + message: "Failed to delete question list.", + error: error.message, + }); + } + } + @Get("scrap") @UseGuards(AuthGuard("jwt")) async getScrappedQuestionLists(@Res() res, @JwtPayload() token: IJwtPayload) { diff --git a/backend/src/question-list/question-list.entity.ts b/backend/src/question-list/question-list.entity.ts index 5c2684b7..18a47490 100644 --- a/backend/src/question-list/question-list.entity.ts +++ b/backend/src/question-list/question-list.entity.ts @@ -53,6 +53,8 @@ export class QuestionList { }) categories: Category[]; - @ManyToMany(() => User, (user) => user.scrappedQuestionLists) + @ManyToMany(() => User, (user) => user.scrappedQuestionLists, { + onDelete: "CASCADE", + }) scrappedByUsers: User[]; } diff --git a/backend/src/question-list/question-list.repository.ts b/backend/src/question-list/question-list.repository.ts index 8d36f1ef..4d23ee94 100644 --- a/backend/src/question-list/question-list.repository.ts +++ b/backend/src/question-list/question-list.repository.ts @@ -95,6 +95,10 @@ export class QuestionListRepository { .getCount(); } + deleteQuestionList(questionListId: number) { + return this.dataSource.getRepository(QuestionList).delete(questionListId); + } + scrapQuestionList(questionListId: number, userId: number) { return this.dataSource .createQueryBuilder() diff --git a/backend/src/question-list/question-list.service.ts b/backend/src/question-list/question-list.service.ts index 3d4279eb..38ed560c 100644 --- a/backend/src/question-list/question-list.service.ts +++ b/backend/src/question-list/question-list.service.ts @@ -163,6 +163,13 @@ export class QuestionListService { return categories; } + async deleteQuestionList(questionListId: number, userId: number) { + const user = await this.userRepository.getUserByUserId(userId); + if (!user) throw new Error("User not found."); + + return await this.questionListRepository.deleteQuestionList(questionListId); + } + async getScrappedQuestionLists(userId: number) { const user = await this.userRepository.getUserByUserId(userId); return await this.questionListRepository.getScrappedQuestionListsByUser(user); From d7d43205c146c53a9c7f01395d0daf94ee313dea Mon Sep 17 00:00:00 2001 From: twalla26 Date: Wed, 27 Nov 2024 01:04:18 +0900 Subject: [PATCH 02/86] =?UTF-8?q?feat:=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=EC=97=90=20pagination=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/package.json | 3 +- .../question-list/dto/my-question-list.dto.ts | 3 -- .../question-list/question-list.controller.ts | 36 ++++++++++++---- .../question-list/question-list.repository.ts | 28 +++++++------ .../question-list/question-list.service.ts | 42 ++++++++++++------- 5 files changed, 72 insertions(+), 40 deletions(-) diff --git a/backend/package.json b/backend/package.json index a7a4e2c1..276ccb77 100644 --- a/backend/package.json +++ b/backend/package.json @@ -32,12 +32,13 @@ "@socket.io/redis-adapter": "^8.3.0", "@types/passport-jwt": "^4.0.1", "axios": "^1.7.7", - "cookie-parser": "^1.4.7", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "cookie-parser": "^1.4.7", "dotenv": "^16.4.5", "ioredis": "^5.4.1", "mysql2": "^3.11.4", + "nestjs-paginate": "^10.0.0", "nestjs-redis-om": "^0.1.2", "passport": "^0.7.0", "passport-custom": "^1.1.1", diff --git a/backend/src/question-list/dto/my-question-list.dto.ts b/backend/src/question-list/dto/my-question-list.dto.ts index bc8ff9eb..c568456d 100644 --- a/backend/src/question-list/dto/my-question-list.dto.ts +++ b/backend/src/question-list/dto/my-question-list.dto.ts @@ -1,9 +1,6 @@ -import { Question } from "../question.entity"; - export interface MyQuestionListDto { id: number; title: string; - contents: Question[]; categoryNames: string[]; isPublic: boolean; usage: number; diff --git a/backend/src/question-list/question-list.controller.ts b/backend/src/question-list/question-list.controller.ts index 9f767306..d799f7de 100644 --- a/backend/src/question-list/question-list.controller.ts +++ b/backend/src/question-list/question-list.controller.ts @@ -1,4 +1,15 @@ -import { Body, Controller, Delete, Get, Param, Post, Req, Res, UseGuards } from "@nestjs/common"; +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Query, + Req, + Res, + UseGuards, +} from "@nestjs/common"; import { QuestionListService } from "./question-list.service"; import { CreateQuestionListDto } from "./dto/create-question-list.dto"; import { GetAllQuestionListDto } from "./dto/get-all-question-list.dto"; @@ -7,21 +18,23 @@ import { AuthGuard } from "@nestjs/passport"; import { JwtPayload } from "@/auth/jwt/jwt.decorator"; import { IJwtPayload } from "@/auth/jwt/jwt.model"; import { MyQuestionListDto } from "./dto/my-question-list.dto"; +import { PaginateQuery } from "nestjs-paginate"; @Controller("question-list") export class QuestionListController { constructor(private readonly questionListService: QuestionListService) {} @Get() - async getAllQuestionLists(@Res() res) { + async getAllQuestionLists(@Query() query: PaginateQuery, @Res() res) { try { - const allQuestionLists: GetAllQuestionListDto[] = - await this.questionListService.getAllQuestionLists(); + const { allQuestionLists, meta } = + await this.questionListService.getAllQuestionLists(query); return res.send({ success: true, message: "All question lists received successfully.", data: { allQuestionLists, + meta, }, }); } catch (error) { @@ -82,6 +95,7 @@ export class QuestionListController { @Post("category") async getAllQuestionListsByCategoryName( + @Query() query: PaginateQuery, @Res() res, @Body() body: { @@ -90,13 +104,17 @@ export class QuestionListController { ) { try { const { categoryName } = body; - const allQuestionLists: GetAllQuestionListDto[] = - await this.questionListService.getAllQuestionListsByCategoryName(categoryName); + const { allQuestionLists, meta } = + await this.questionListService.getAllQuestionListsByCategoryName( + categoryName, + query + ); return res.send({ success: true, message: "All question lists received successfully.", data: { allQuestionLists, + meta, }, }); } catch (error) { @@ -110,6 +128,7 @@ export class QuestionListController { @Post("contents") async getQuestionListContents( + @Query() query: PaginateQuery, @Res() res, @Body() body: { @@ -118,13 +137,14 @@ export class QuestionListController { ) { try { const { questionListId } = body; - const questionListContents: QuestionListContentsDto = - await this.questionListService.getQuestionListContents(questionListId); + const { questionListContents, meta } = + await this.questionListService.getQuestionListContents(questionListId, query); return res.send({ success: true, message: "Question list contents received successfully.", data: { questionListContents, + meta, }, }); } catch (error) { diff --git a/backend/src/question-list/question-list.repository.ts b/backend/src/question-list/question-list.repository.ts index 8d36f1ef..c4357646 100644 --- a/backend/src/question-list/question-list.repository.ts +++ b/backend/src/question-list/question-list.repository.ts @@ -4,6 +4,7 @@ import { QuestionList } from "./question-list.entity"; import { Question } from "./question.entity"; import { Category } from "./category.entity"; import { User } from "@/user/user.entity"; +import { PaginateQuery } from "nestjs-paginate"; @Injectable() export class QuestionListRepository { @@ -18,9 +19,10 @@ export class QuestionListRepository { } findPublicQuestionLists() { - return this.dataSource.getRepository(QuestionList).find({ - where: { isPublic: true }, - }); + return this.dataSource + .getRepository(QuestionList) + .createQueryBuilder("question_list") + .where("question_list.is_public = :isPublic", { isPublic: true }); } async getCategoryIdByName(categoryName: string) { @@ -33,13 +35,12 @@ export class QuestionListRepository { } findPublicQuestionListsByCategoryId(categoryId: number) { - return this.dataSource.getRepository(QuestionList).find({ - where: { - isPublic: true, - categories: { id: categoryId }, - }, - relations: ["categories"], - }); + return this.dataSource + .getRepository(QuestionList) + .createQueryBuilder("question_list") + .innerJoin("question_list.categories", "category") + .where("question_list.is_public = :isPublic", { isPublic: true }) + .andWhere("category.id = :categoryId", { categoryId }); } async findCategoryNamesByQuestionListId(questionListId: number) { @@ -66,9 +67,10 @@ export class QuestionListRepository { } getContentsByQuestionListId(questionListId: number) { - return this.dataSource.getRepository(Question).find({ - where: { questionListId }, - }); + return this.dataSource + .getRepository(Question) + .createQueryBuilder("question") + .where("question.question_list_id = :questionListId", { questionListId }); } async getUsernameById(userId: number) { diff --git a/backend/src/question-list/question-list.service.ts b/backend/src/question-list/question-list.service.ts index 3d4279eb..06ff6744 100644 --- a/backend/src/question-list/question-list.service.ts +++ b/backend/src/question-list/question-list.service.ts @@ -8,6 +8,7 @@ import { MyQuestionListDto } from "./dto/my-question-list.dto"; import { Question } from "./question.entity"; import { Transactional } from "typeorm-transactional"; import { QuestionList } from "@/question-list/question-list.entity"; +import { paginate, PaginateQuery } from "nestjs-paginate"; @Injectable() export class QuestionListService { @@ -16,12 +17,16 @@ export class QuestionListService { private readonly userRepository: UserRepository ) {} - async getAllQuestionLists() { + async getAllQuestionLists(query: PaginateQuery) { const allQuestionLists: GetAllQuestionListDto[] = []; const publicQuestionLists = await this.questionListRepository.findPublicQuestionLists(); + const result = await paginate(query, publicQuestionLists, { + sortableColumns: ["usage"], + defaultSortBy: [["usage", "DESC"]], + }); - for (const publicQuestionList of publicQuestionLists) { + for (const publicQuestionList of result.data) { const { id, title, usage } = publicQuestionList; const categoryNames: string[] = await this.questionListRepository.findCategoryNamesByQuestionListId(id); @@ -38,22 +43,26 @@ export class QuestionListService { }; allQuestionLists.push(questionList); } - return allQuestionLists; + return { allQuestionLists, meta: result.meta }; } - async getAllQuestionListsByCategoryName(categoryName: string) { + async getAllQuestionListsByCategoryName(categoryName: string, query: PaginateQuery) { const allQuestionLists: GetAllQuestionListDto[] = []; const categoryId = await this.questionListRepository.getCategoryIdByName(categoryName); if (!categoryId) { - return []; + return {}; } const publicQuestionLists = await this.questionListRepository.findPublicQuestionListsByCategoryId(categoryId); + const result = await paginate(query, publicQuestionLists, { + sortableColumns: ["usage"], + defaultSortBy: [["usage", "DESC"]], + }); - for (const publicQuestionList of publicQuestionLists) { + for (const publicQuestionList of result.data) { const { id, title, usage } = publicQuestionList; const categoryNames: string[] = await this.questionListRepository.findCategoryNamesByQuestionListId(id); @@ -70,7 +79,7 @@ export class QuestionListService { }; allQuestionLists.push(questionList); } - return allQuestionLists; + return { allQuestionLists, meta: result.meta }; } // 질문 생성 메서드 @@ -105,28 +114,34 @@ export class QuestionListService { return { createdQuestionList, createdQuestions }; } - async getQuestionListContents(questionListId: number) { + async getQuestionListContents(questionListId: number, query: PaginateQuery) { const questionList = await this.questionListRepository.getQuestionListById(questionListId); - const { id, title, usage, userId } = questionList; + const { id, title, usage, isPublic, userId } = questionList; + if (!isPublic) { + throw new Error("This is private question list."); + } const contents = await this.questionListRepository.getContentsByQuestionListId(questionListId); + const result = await paginate(query, contents, { + sortableColumns: ["index"], + defaultSortBy: [["index", "ASC"]], + }); const categoryNames = await this.questionListRepository.findCategoryNamesByQuestionListId(questionListId); const username = await this.questionListRepository.getUsernameById(userId); - const questionListContents: QuestionListContentsDto = { id, title, - contents, + contents: result.data, categoryNames, usage, username, }; - return questionListContents; + return { questionListContents, meta: result.meta }; } async getMyQuestionLists(userId: number) { @@ -138,12 +153,9 @@ export class QuestionListService { const categoryNames: string[] = await this.questionListRepository.findCategoryNamesByQuestionListId(id); - const contents = await this.questionListRepository.getContentsByQuestionListId(id); - const questionList: MyQuestionListDto = { id, title, - contents, categoryNames, isPublic, usage, From 81680565063de9eb1a8681e7d4fcda49b712c3be Mon Sep 17 00:00:00 2001 From: yiseungyun Date: Wed, 27 Nov 2024 02:20:51 +0900 Subject: [PATCH 03/86] =?UTF-8?q?feat:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=A7=88=EB=AC=B8=EC=A7=80=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/common/Sidebar.tsx | 1 - .../QuestionList/QuestionItem/index.tsx | 44 +++++++++++++++---- .../QuestionSection/QuestionList/index.tsx | 24 ++++++---- .../mypage/QuestionSection/index.tsx | 2 +- frontend/src/hooks/useAuth.ts | 1 - frontend/src/pages/Login/LoginPage.tsx | 3 +- frontend/src/pages/MyPage.tsx | 2 +- 7 files changed, 55 insertions(+), 22 deletions(-) diff --git a/frontend/src/components/common/Sidebar.tsx b/frontend/src/components/common/Sidebar.tsx index 69762053..52bd45bf 100644 --- a/frontend/src/components/common/Sidebar.tsx +++ b/frontend/src/components/common/Sidebar.tsx @@ -101,5 +101,4 @@ const Sidebar = () => { ); }; - export default Sidebar; diff --git a/frontend/src/components/mypage/QuestionSection/QuestionList/QuestionItem/index.tsx b/frontend/src/components/mypage/QuestionSection/QuestionList/QuestionItem/index.tsx index b9c9b647..48771688 100644 --- a/frontend/src/components/mypage/QuestionSection/QuestionList/QuestionItem/index.tsx +++ b/frontend/src/components/mypage/QuestionSection/QuestionList/QuestionItem/index.tsx @@ -1,21 +1,49 @@ import { useNavigate } from "react-router-dom"; +import { MdEdit } from "react-icons/md"; +import { RiDeleteBin6Fill } from "react-icons/ri"; +import { FaBookmark } from "react-icons/fa"; -const QuestionItem = () => { +interface ItemProps { + type: "my" | "saved"; +} + +const QuestionItem = ({ type }: ItemProps) => { const navigate = useNavigate(); return (
{ navigate("/questions/1"); }} > -

- 프론트엔드 면접 질문 리스트 프론트엔드 면접 질문 리스트 -

- - 작성자 눈드뮴눈드뮴눈드뮴눈드뮴 • 5개의 질문 - +
+
+

프론트엔드

+
+ {type === "my" ? ( + <> + + + + ) : ( + + )} +
+
+ + 작성자 눈드뮴눈드뮴눈드뮴눈드뮴 • 5개의 질문 + +
); }; diff --git a/frontend/src/components/mypage/QuestionSection/QuestionList/index.tsx b/frontend/src/components/mypage/QuestionSection/QuestionList/index.tsx index 6ed08b19..d736f159 100644 --- a/frontend/src/components/mypage/QuestionSection/QuestionList/index.tsx +++ b/frontend/src/components/mypage/QuestionSection/QuestionList/index.tsx @@ -1,16 +1,22 @@ import QuestionItem from "./QuestionItem"; -const QuestionList = () => { +interface ListProps { + tab: "myList" | "savedList"; +} + +const QuestionList = ({ tab }: ListProps) => { return (
- - - - - - - - + {tab === "myList" ? ( + <> + + + + ) : ( + <> + + + )}
); }; diff --git a/frontend/src/components/mypage/QuestionSection/index.tsx b/frontend/src/components/mypage/QuestionSection/index.tsx index d6e3fd7f..01e57188 100644 --- a/frontend/src/components/mypage/QuestionSection/index.tsx +++ b/frontend/src/components/mypage/QuestionSection/index.tsx @@ -30,7 +30,7 @@ const QuestionSection = () => { return (
- +
); diff --git a/frontend/src/hooks/useAuth.ts b/frontend/src/hooks/useAuth.ts index e5b00521..6cd4e0f6 100644 --- a/frontend/src/hooks/useAuth.ts +++ b/frontend/src/hooks/useAuth.ts @@ -35,7 +35,6 @@ const useAuth = () => { } }, [isLoggedIn, nickname]); - const logIn = () => { login(); }; diff --git a/frontend/src/pages/Login/LoginPage.tsx b/frontend/src/pages/Login/LoginPage.tsx index 4c95c711..8ea8e263 100644 --- a/frontend/src/pages/Login/LoginPage.tsx +++ b/frontend/src/pages/Login/LoginPage.tsx @@ -5,6 +5,7 @@ import DrawingSnowman from "@components/common/Animate/DrawingSnowman.tsx"; import Divider from "@components/common/Divider.tsx"; import OAuthContainer from "@components/login/OAuthContainer.tsx"; import DefaultAuthFormContainer from "@components/login/DefaultAuthFormContainer.tsx"; +import { useEffect } from "react"; const LoginPage = () => { const { isLoggedIn, guestLogIn } = useAuth(); @@ -16,7 +17,7 @@ const LoginPage = () => { navigate("/"); } }, [isLoggedIn]); - + const handleOAuthLogin = (provider: "github" | "guest") => { if (provider === "github") { // 깃허브 로그인 diff --git a/frontend/src/pages/MyPage.tsx b/frontend/src/pages/MyPage.tsx index 87182f1a..f436fcb7 100644 --- a/frontend/src/pages/MyPage.tsx +++ b/frontend/src/pages/MyPage.tsx @@ -1,4 +1,3 @@ -import { sectionWithSidebar } from "@/constraints/LayoutConstant.ts"; import { useEffect } from "react"; import useAuth from "@hooks/useAuth.ts"; import { useNavigate } from "react-router-dom"; @@ -7,6 +6,7 @@ import Sidebar from "@components/common/Sidebar.tsx"; import Profile from "@components/mypage/Profile"; import QuestionSection from "@/components/mypage/QuestionSection"; import ProfileEditModal from "@/components/mypage/ProfileEditModal"; +import { sectionWithSidebar } from "@/constants/LayoutConstant"; const MyPage = () => { const { isLoggedIn, nickname } = useAuth(); From 52c4476b1ec550d0d7f33c7571c0ceb0d4d64496 Mon Sep 17 00:00:00 2001 From: twalla26 Date: Wed, 27 Nov 2024 10:55:57 +0900 Subject: [PATCH 04/86] =?UTF-8?q?fix:=20=EC=A7=88=EB=AC=B8=EC=A7=80=20?= =?UTF-8?q?=EB=82=B4=EC=9A=A9=EC=9D=80=20pagination=20=EC=97=86=EC=9D=B4?= =?UTF-8?q?=20=EB=AA=A8=EB=91=90=20=EC=A0=84=EB=8B=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/question-list/question-list.controller.ts | 6 ++---- backend/src/question-list/question-list.repository.ts | 3 ++- backend/src/question-list/question-list.service.ts | 10 +++------- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/backend/src/question-list/question-list.controller.ts b/backend/src/question-list/question-list.controller.ts index d799f7de..3e63e670 100644 --- a/backend/src/question-list/question-list.controller.ts +++ b/backend/src/question-list/question-list.controller.ts @@ -128,7 +128,6 @@ export class QuestionListController { @Post("contents") async getQuestionListContents( - @Query() query: PaginateQuery, @Res() res, @Body() body: { @@ -137,14 +136,13 @@ export class QuestionListController { ) { try { const { questionListId } = body; - const { questionListContents, meta } = - await this.questionListService.getQuestionListContents(questionListId, query); + const questionListContents: QuestionListContentsDto = + await this.questionListService.getQuestionListContents(questionListId); return res.send({ success: true, message: "Question list contents received successfully.", data: { questionListContents, - meta, }, }); } catch (error) { diff --git a/backend/src/question-list/question-list.repository.ts b/backend/src/question-list/question-list.repository.ts index c4357646..0a90d433 100644 --- a/backend/src/question-list/question-list.repository.ts +++ b/backend/src/question-list/question-list.repository.ts @@ -70,7 +70,8 @@ export class QuestionListRepository { return this.dataSource .getRepository(Question) .createQueryBuilder("question") - .where("question.question_list_id = :questionListId", { questionListId }); + .where("question.question_list_id = :questionListId", { questionListId }) + .getMany(); } async getUsernameById(userId: number) { diff --git a/backend/src/question-list/question-list.service.ts b/backend/src/question-list/question-list.service.ts index 06ff6744..2877a0e1 100644 --- a/backend/src/question-list/question-list.service.ts +++ b/backend/src/question-list/question-list.service.ts @@ -114,7 +114,7 @@ export class QuestionListService { return { createdQuestionList, createdQuestions }; } - async getQuestionListContents(questionListId: number, query: PaginateQuery) { + async getQuestionListContents(questionListId: number) { const questionList = await this.questionListRepository.getQuestionListById(questionListId); const { id, title, usage, isPublic, userId } = questionList; if (!isPublic) { @@ -123,10 +123,6 @@ export class QuestionListService { const contents = await this.questionListRepository.getContentsByQuestionListId(questionListId); - const result = await paginate(query, contents, { - sortableColumns: ["index"], - defaultSortBy: [["index", "ASC"]], - }); const categoryNames = await this.questionListRepository.findCategoryNamesByQuestionListId(questionListId); @@ -135,13 +131,13 @@ export class QuestionListService { const questionListContents: QuestionListContentsDto = { id, title, - contents: result.data, + contents, categoryNames, usage, username, }; - return { questionListContents, meta: result.meta }; + return questionListContents; } async getMyQuestionLists(userId: number) { From ea5d5f3e378c5524f2ca48b351fc4c81daacf362 Mon Sep 17 00:00:00 2001 From: twalla26 Date: Wed, 27 Nov 2024 11:03:48 +0900 Subject: [PATCH 05/86] =?UTF-8?q?feat:=20=EB=82=B4=20=EC=A7=88=EB=AC=B8?= =?UTF-8?q?=EC=A7=80=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=EC=97=90=20?= =?UTF-8?q?pagination=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/question-list/question-list.controller.ts | 13 ++++++++++--- .../src/question-list/question-list.repository.ts | 10 +++++++--- backend/src/question-list/question-list.service.ts | 14 ++++++++++---- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/backend/src/question-list/question-list.controller.ts b/backend/src/question-list/question-list.controller.ts index 3e63e670..507c5749 100644 --- a/backend/src/question-list/question-list.controller.ts +++ b/backend/src/question-list/question-list.controller.ts @@ -156,16 +156,23 @@ export class QuestionListController { @Get("my") @UseGuards(AuthGuard("jwt")) - async getMyQuestionLists(@Res() res, @JwtPayload() token: IJwtPayload) { + async getMyQuestionLists( + @Query() query: PaginateQuery, + @Res() res, + @JwtPayload() token: IJwtPayload + ) { try { const userId = token.userId; - const myQuestionLists: MyQuestionListDto[] = - await this.questionListService.getMyQuestionLists(userId); + const { myQuestionLists, meta } = await this.questionListService.getMyQuestionLists( + userId, + query + ); return res.send({ success: true, message: "My question lists received successfully.", data: { myQuestionLists, + meta, }, }); } catch (error) { diff --git a/backend/src/question-list/question-list.repository.ts b/backend/src/question-list/question-list.repository.ts index 0a90d433..d1ae03f9 100644 --- a/backend/src/question-list/question-list.repository.ts +++ b/backend/src/question-list/question-list.repository.ts @@ -83,9 +83,13 @@ export class QuestionListRepository { } getQuestionListsByUserId(userId: number) { - return this.dataSource.getRepository(QuestionList).find({ - where: { userId }, - }); + // return this.dataSource.getRepository(QuestionList).find({ + // where: { userId }, + // }); + return this.dataSource + .getRepository(QuestionList) + .createQueryBuilder("question_list") + .where("question_list.userId = :userId", { userId }); } getQuestionCountByQuestionListId(questionListId: number) { diff --git a/backend/src/question-list/question-list.service.ts b/backend/src/question-list/question-list.service.ts index 2877a0e1..5714bf0a 100644 --- a/backend/src/question-list/question-list.service.ts +++ b/backend/src/question-list/question-list.service.ts @@ -140,11 +140,15 @@ export class QuestionListService { return questionListContents; } - async getMyQuestionLists(userId: number) { + async getMyQuestionLists(userId: number, query: PaginateQuery) { const questionLists = await this.questionListRepository.getQuestionListsByUserId(userId); + const result = await paginate(query, questionLists, { + sortableColumns: ["usage"], + defaultSortBy: [["usage", "DESC"]], + }); const myQuestionLists: MyQuestionListDto[] = []; - for (const myQuestionList of questionLists) { + for (const myQuestionList of result.data) { const { id, title, isPublic, usage } = myQuestionList; const categoryNames: string[] = await this.questionListRepository.findCategoryNamesByQuestionListId(id); @@ -158,7 +162,7 @@ export class QuestionListService { }; myQuestionLists.push(questionList); } - return myQuestionLists; + return { myQuestionLists, meta: result.meta }; } async findCategoriesByNames(categoryNames: string[]) { @@ -184,7 +188,9 @@ export class QuestionListService { if (!questionList) throw new Error("Question list not found."); // 스크랩하려는 질문지가 내가 만든 질문지인지 확인 - const myQuestionLists = await this.questionListRepository.getQuestionListsByUserId(userId); + const myQuestionLists = await this.questionListRepository + .getQuestionListsByUserId(userId) + .getMany(); const isMyQuestionList = myQuestionLists.some((list) => list.id === questionListId); if (isMyQuestionList) throw new Error("Can't scrap my question list."); From 46274402abc6f13603d7ba34c28a359fcd39d804 Mon Sep 17 00:00:00 2001 From: twalla26 Date: Wed, 27 Nov 2024 11:11:32 +0900 Subject: [PATCH 06/86] =?UTF-8?q?feat:=20=EC=8A=A4=ED=81=AC=EB=9E=A9?= =?UTF-8?q?=ED=95=9C=20=EC=A7=88=EB=AC=B8=EC=A7=80=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=EC=97=90=20pagination=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/question-list/question-list.controller.ts | 11 ++++++++--- .../src/question-list/question-list.repository.ts | 11 +++++------ .../src/question-list/question-list.service.ts | 15 +++++++++++---- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/backend/src/question-list/question-list.controller.ts b/backend/src/question-list/question-list.controller.ts index 507c5749..a6a74543 100644 --- a/backend/src/question-list/question-list.controller.ts +++ b/backend/src/question-list/question-list.controller.ts @@ -186,16 +186,21 @@ export class QuestionListController { @Get("scrap") @UseGuards(AuthGuard("jwt")) - async getScrappedQuestionLists(@Res() res, @JwtPayload() token: IJwtPayload) { + async getScrappedQuestionLists( + @Query() query: PaginateQuery, + @Res() res, + @JwtPayload() token: IJwtPayload + ) { try { const userId = token.userId; - const scrappedQuestionLists = - await this.questionListService.getScrappedQuestionLists(userId); + const { scrappedQuestionLists, meta } = + await this.questionListService.getScrappedQuestionLists(userId, query); return res.send({ success: true, message: "Scrapped question lists received successfully.", data: { scrappedQuestionLists, + meta, }, }); } catch (error) { diff --git a/backend/src/question-list/question-list.repository.ts b/backend/src/question-list/question-list.repository.ts index d1ae03f9..a8cb8e9d 100644 --- a/backend/src/question-list/question-list.repository.ts +++ b/backend/src/question-list/question-list.repository.ts @@ -83,9 +83,6 @@ export class QuestionListRepository { } getQuestionListsByUserId(userId: number) { - // return this.dataSource.getRepository(QuestionList).find({ - // where: { userId }, - // }); return this.dataSource .getRepository(QuestionList) .createQueryBuilder("question_list") @@ -116,9 +113,11 @@ export class QuestionListRepository { } getScrappedQuestionListsByUser(user: User) { - return this.dataSource.getRepository(QuestionList).find({ - where: { scrappedByUsers: user }, - }); + return this.dataSource + .getRepository(QuestionList) + .createQueryBuilder("question_list") + .innerJoin("question_list.scrappedByUsers", "user") + .where("user.id = :userId", { userId: user.id }); } unscrapQuestionList(questionListId: number, userId: number) { diff --git a/backend/src/question-list/question-list.service.ts b/backend/src/question-list/question-list.service.ts index 5714bf0a..756fe75a 100644 --- a/backend/src/question-list/question-list.service.ts +++ b/backend/src/question-list/question-list.service.ts @@ -175,9 +175,15 @@ export class QuestionListService { return categories; } - async getScrappedQuestionLists(userId: number) { + async getScrappedQuestionLists(userId: number, query: PaginateQuery) { const user = await this.userRepository.getUserByUserId(userId); - return await this.questionListRepository.getScrappedQuestionListsByUser(user); + const scrappedQuestionLists = + await this.questionListRepository.getScrappedQuestionListsByUser(user); + const result = await paginate(query, scrappedQuestionLists, { + sortableColumns: ["usage"], + defaultSortBy: [["usage", "DESC"]], + }); + return { scrappedQuestionLists: result.data, meta: result.meta }; } async scrapQuestionList(questionListId: number, userId: number) { @@ -195,8 +201,9 @@ export class QuestionListService { if (isMyQuestionList) throw new Error("Can't scrap my question list."); // 스크랩하려는 질문지가 이미 스크랩한 질문지인지 확인 - const alreadyScrappedQuestionLists = - await this.questionListRepository.getScrappedQuestionListsByUser(user); + const alreadyScrappedQuestionLists = await this.questionListRepository + .getScrappedQuestionListsByUser(user) + .getMany(); const isAlreadyScrapped = alreadyScrappedQuestionLists.some( (list) => list.id === questionListId ); From 90cb1fedd9551ea42a92a14a8043437c127d500a Mon Sep 17 00:00:00 2001 From: twalla26 Date: Wed, 27 Nov 2024 11:13:07 +0900 Subject: [PATCH 07/86] =?UTF-8?q?refactor:=20=EC=8A=A4=ED=81=AC=EB=9E=A9?= =?UTF-8?q?=20=EC=9D=91=EB=8B=B5=20=EB=B3=80=EC=88=98=EB=AA=85=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/question-list/question-list.controller.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/question-list/question-list.controller.ts b/backend/src/question-list/question-list.controller.ts index a6a74543..22552f25 100644 --- a/backend/src/question-list/question-list.controller.ts +++ b/backend/src/question-list/question-list.controller.ts @@ -199,7 +199,7 @@ export class QuestionListController { success: true, message: "Scrapped question lists received successfully.", data: { - scrappedQuestionLists, + questionList: scrappedQuestionLists, meta, }, }); @@ -231,7 +231,7 @@ export class QuestionListController { success: true, message: "Question list is scrapped successfully.", data: { - scrappedQuestionList, + questionList: scrappedQuestionList, }, }); } catch (error) { From 7f2b9d0cd82ecc22e6db1368e694935e76e9093a Mon Sep 17 00:00:00 2001 From: yiseungyun Date: Wed, 27 Nov 2024 16:13:34 +0900 Subject: [PATCH 08/86] =?UTF-8?q?feat:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=A7=88=EB=AC=B8=EC=A7=80=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/common/Button/index.tsx | 4 +- .../components/mypage/Profile/ProfileIcon.tsx | 10 ++++ .../src/components/mypage/Profile/index.tsx | 6 +-- .../mypage/ProfileEditModal/ButtonSection.tsx | 7 ++- .../QuestionList/QuestionItem/Category.tsx | 13 +++++ .../QuestionList/QuestionItem/index.tsx | 53 +++++++++++-------- .../detail/ButtonSection.tsx/index.tsx | 9 +++- frontend/src/pages/MyPage.tsx | 4 +- 8 files changed, 74 insertions(+), 32 deletions(-) create mode 100644 frontend/src/components/mypage/Profile/ProfileIcon.tsx create mode 100644 frontend/src/components/mypage/QuestionSection/QuestionList/QuestionItem/Category.tsx diff --git a/frontend/src/components/common/Button/index.tsx b/frontend/src/components/common/Button/index.tsx index 02c6bd6f..66b008cf 100644 --- a/frontend/src/components/common/Button/index.tsx +++ b/frontend/src/components/common/Button/index.tsx @@ -4,9 +4,10 @@ interface ButtonProps { text: string; type: "gray" | "green"; icon?: IconType; + onClick: () => void; } -const Button = ({ text, type, icon: Icon }: ButtonProps) => { +const Button = ({ text, type, icon: Icon, onClick }: ButtonProps) => { const buttonColor = type === "gray" ? "bg-gray-200 text-gray-black" @@ -15,6 +16,7 @@ const Button = ({ text, type, icon: Icon }: ButtonProps) => { return ( +
+ + +
+
+

+ 프론트엔드프론트엔드프론트엔드프론트엔드 +

+
+ {type === "my" ? ( + <> + + + + ) : ( - - ) : ( - - )} + )} +
+ + 작성자 눈드뮴눈드뮴눈드뮴눈드뮴눈드뮴눈드뮴눈드 +
- - 작성자 눈드뮴눈드뮴눈드뮴눈드뮴 • 5개의 질문 - ); diff --git a/frontend/src/components/questions/detail/ButtonSection.tsx/index.tsx b/frontend/src/components/questions/detail/ButtonSection.tsx/index.tsx index 2d82c362..0bf0b306 100644 --- a/frontend/src/components/questions/detail/ButtonSection.tsx/index.tsx +++ b/frontend/src/components/questions/detail/ButtonSection.tsx/index.tsx @@ -5,8 +5,13 @@ import { IoMdShare } from "react-icons/io"; const ButtonSection = () => { return (
-
); }; diff --git a/frontend/src/pages/MyPage.tsx b/frontend/src/pages/MyPage.tsx index f436fcb7..6b6ac65c 100644 --- a/frontend/src/pages/MyPage.tsx +++ b/frontend/src/pages/MyPage.tsx @@ -25,7 +25,7 @@ const MyPage = () => {

@@ -33,7 +33,7 @@ const MyPage = () => {

From 1e6fa84469f7b91b9012673d43345727063deb09 Mon Sep 17 00:00:00 2001 From: yiseungyun Date: Wed, 27 Nov 2024 17:32:58 +0900 Subject: [PATCH 09/86] =?UTF-8?q?refactor:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20UI=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/common/PageTitle.tsx | 13 +++++ .../components/layout/SidebarPageLayout.tsx | 7 +-- frontend/src/pages/MyPage.tsx | 48 ------------------- frontend/src/pages/MyPage/MyPageView.tsx | 24 ++++++++++ frontend/src/pages/MyPage/index.tsx | 22 +++++++++ frontend/src/routes.tsx | 2 +- 6 files changed, 64 insertions(+), 52 deletions(-) create mode 100644 frontend/src/components/common/PageTitle.tsx delete mode 100644 frontend/src/pages/MyPage.tsx create mode 100644 frontend/src/pages/MyPage/MyPageView.tsx create mode 100644 frontend/src/pages/MyPage/index.tsx diff --git a/frontend/src/components/common/PageTitle.tsx b/frontend/src/components/common/PageTitle.tsx new file mode 100644 index 00000000..bcc67011 --- /dev/null +++ b/frontend/src/components/common/PageTitle.tsx @@ -0,0 +1,13 @@ +interface TitleProps { + title: string; +} + +const PageTitle = ({ title }: TitleProps) => { + return ( +

+ {title} +

+ ) +} + +export default PageTitle; \ No newline at end of file diff --git a/frontend/src/components/layout/SidebarPageLayout.tsx b/frontend/src/components/layout/SidebarPageLayout.tsx index bec134e2..2c05fd8a 100644 --- a/frontend/src/components/layout/SidebarPageLayout.tsx +++ b/frontend/src/components/layout/SidebarPageLayout.tsx @@ -3,14 +3,15 @@ import Sidebar from "@components/common/Sidebar.tsx"; interface SidebarPageLayoutProps { children: React.ReactNode; + childrenClassName?: string; } -const SidebarPageLayout = ({ children }: SidebarPageLayoutProps) => { +const SidebarPageLayout = ({ children, childrenClassName = "" }: SidebarPageLayoutProps) => { return (
- {children} -
+
{children}
+ ); }; diff --git a/frontend/src/pages/MyPage.tsx b/frontend/src/pages/MyPage.tsx deleted file mode 100644 index 6b6ac65c..00000000 --- a/frontend/src/pages/MyPage.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { useEffect } from "react"; -import useAuth from "@hooks/useAuth.ts"; -import { useNavigate } from "react-router-dom"; -import useToast from "@hooks/useToast.ts"; -import Sidebar from "@components/common/Sidebar.tsx"; -import Profile from "@components/mypage/Profile"; -import QuestionSection from "@/components/mypage/QuestionSection"; -import ProfileEditModal from "@/components/mypage/ProfileEditModal"; -import { sectionWithSidebar } from "@/constants/LayoutConstant"; - -const MyPage = () => { - const { isLoggedIn, nickname } = useAuth(); - const toast = useToast(); - const navigate = useNavigate(); - - useEffect(() => { - if (!isLoggedIn) { - toast.error("로그인이 필요한 서비스입니다."); - navigate("/login", { replace: true }); - } - }, [isLoggedIn, navigate, toast]); - - return ( -
- -
-

- 마이페이지 -

-
- - - -
-
-
- ); -}; - -export default MyPage; diff --git a/frontend/src/pages/MyPage/MyPageView.tsx b/frontend/src/pages/MyPage/MyPageView.tsx new file mode 100644 index 00000000..37cd3f13 --- /dev/null +++ b/frontend/src/pages/MyPage/MyPageView.tsx @@ -0,0 +1,24 @@ +import SidebarPageLayout from "@components/layout/SidebarPageLayout"; +import PageTitle from "@components/common/PageTitle"; +import ProfileEditModal from "@components/mypage/ProfileEditModal"; +import Profile from "@components/mypage/Profile"; +import QuestionSection from "@components/mypage/QuestionSection"; + +interface MyPageViewProps { + nickname: string; +} + +const MyPageView = ({ nickname }: MyPageViewProps) => { + return ( + + +
+ + + +
+
+ ); +}; + +export default MyPageView; \ No newline at end of file diff --git a/frontend/src/pages/MyPage/index.tsx b/frontend/src/pages/MyPage/index.tsx new file mode 100644 index 00000000..158e125f --- /dev/null +++ b/frontend/src/pages/MyPage/index.tsx @@ -0,0 +1,22 @@ +import { useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import useAuth from "@hooks/useAuth"; +import useToast from "@hooks/useToast"; +import MyPageView from "./MyPageView"; + +const MyPage = () => { + const { isLoggedIn, nickname } = useAuth(); + const toast = useToast(); + const navigate = useNavigate(); + + useEffect(() => { + if (!isLoggedIn) { + toast.error("로그인이 필요한 서비스입니다."); + navigate("/login", { replace: true }); + } + }, [isLoggedIn, navigate, toast]); + + return ; +}; + +export default MyPage; diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index f17ca32f..772fcbd6 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -8,7 +8,7 @@ import ErrorPage from "@/pages/ErrorPage.tsx"; import LoginPage from "@/pages/Login/LoginPage.tsx"; import QuestionListPage from "@/pages/QuestionListPage.tsx"; import AuthCallbackPage from "@/pages/Login/AuthCallbackPage.tsx"; -import MyPage from "@/pages/MyPage.tsx"; +import MyPage from "@/pages/MyPage/index.tsx"; export const routes = [ { From 4dd47834fbe33aa6a0fa88e0215d086f578f63ca Mon Sep 17 00:00:00 2001 From: yiseungyun Date: Wed, 27 Nov 2024 17:41:14 +0900 Subject: [PATCH 10/86] =?UTF-8?q?refactor:=20api=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=20=EC=9D=B4=EB=A6=84=20=EC=A7=81=EA=B4=80?= =?UTF-8?q?=EC=A0=81=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/questions/{getQuestion.ts => getQuestionContent.ts} | 4 ++-- .../components/questions/detail/QuestionList.tsx/index.tsx | 2 +- .../components/questions/detail/QuestionTitle.tsx/index.tsx | 2 +- .../hooks/api/{useGetQuestion.ts => useGetQuestionContent.ts} | 4 ++-- frontend/src/pages/QuestionDetailPage.tsx | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) rename frontend/src/api/questions/{getQuestion.ts => getQuestionContent.ts} (69%) rename frontend/src/hooks/api/{useGetQuestion.ts => useGetQuestionContent.ts} (77%) diff --git a/frontend/src/api/questions/getQuestion.ts b/frontend/src/api/questions/getQuestionContent.ts similarity index 69% rename from frontend/src/api/questions/getQuestion.ts rename to frontend/src/api/questions/getQuestionContent.ts index bb980804..355fe61b 100644 --- a/frontend/src/api/questions/getQuestion.ts +++ b/frontend/src/api/questions/getQuestionContent.ts @@ -1,6 +1,6 @@ import axios from "axios"; -const fetchQuestion = async (questionListId: number) => { +const fetchQuestionContent = async (questionListId: number) => { const { data } = await axios.post("/api/question-list/contents", { questionListId, }); @@ -12,4 +12,4 @@ const fetchQuestion = async (questionListId: number) => { return data.data.questionListContents; }; -export default fetchQuestion; +export default fetchQuestionContent; diff --git a/frontend/src/components/questions/detail/QuestionList.tsx/index.tsx b/frontend/src/components/questions/detail/QuestionList.tsx/index.tsx index da4b5189..027c144f 100644 --- a/frontend/src/components/questions/detail/QuestionList.tsx/index.tsx +++ b/frontend/src/components/questions/detail/QuestionList.tsx/index.tsx @@ -1,4 +1,4 @@ -import { useGetQuestion } from "@hooks/api/useGetQuestion.ts"; +import { useGetQuestion } from "@/hooks/api/useGetQuestionContent"; import QuestionItem from "./QuestionItem"; const QuestionList = ({ questionId }: { questionId: string }) => { diff --git a/frontend/src/components/questions/detail/QuestionTitle.tsx/index.tsx b/frontend/src/components/questions/detail/QuestionTitle.tsx/index.tsx index aa909bee..d2099bd3 100644 --- a/frontend/src/components/questions/detail/QuestionTitle.tsx/index.tsx +++ b/frontend/src/components/questions/detail/QuestionTitle.tsx/index.tsx @@ -1,4 +1,4 @@ -import { useGetQuestion } from "@hooks/api/useGetQuestion.ts"; +import { useGetQuestion } from "@/hooks/api/useGetQuestionContent"; import { MdEdit } from "react-icons/md"; import { RiDeleteBin6Fill } from "react-icons/ri"; import { FaRegBookmark } from "react-icons/fa"; diff --git a/frontend/src/hooks/api/useGetQuestion.ts b/frontend/src/hooks/api/useGetQuestionContent.ts similarity index 77% rename from frontend/src/hooks/api/useGetQuestion.ts rename to frontend/src/hooks/api/useGetQuestionContent.ts index 5da6d3f4..0c0df554 100644 --- a/frontend/src/hooks/api/useGetQuestion.ts +++ b/frontend/src/hooks/api/useGetQuestionContent.ts @@ -1,4 +1,4 @@ -import fetchQuestion from "@/api/questions/getQuestion.ts"; +import fetchQuestion from "@/api/questions/getQuestionContent"; import { useQuery } from "@tanstack/react-query"; interface QuestionContent { @@ -17,7 +17,7 @@ interface ApiResponse { username: string; } -export const useGetQuestion = (questionListId: string) => { +export const useGetQuestionContent = (questionListId: string) => { return useQuery({ queryKey: ["questions", questionListId], queryFn: () => fetchQuestion(Number(questionListId)), diff --git a/frontend/src/pages/QuestionDetailPage.tsx b/frontend/src/pages/QuestionDetailPage.tsx index 90f0d676..75f4dccd 100644 --- a/frontend/src/pages/QuestionDetailPage.tsx +++ b/frontend/src/pages/QuestionDetailPage.tsx @@ -3,7 +3,7 @@ import Sidebar from "@/components/common/Sidebar"; import { sectionWithSidebar } from "@/constants/LayoutConstant.ts"; import QuestionTitle from "@/components/questions/detail/QuestionTitle.tsx"; import QuestionList from "@/components/questions/detail/QuestionList.tsx"; -import { useGetQuestion } from "@hooks/api/useGetQuestion.ts"; +import { useGetQuestionContent } from "@/hooks/api/useGetQuestionContent"; import ButtonSection from "@/components/questions/detail/ButtonSection.tsx"; import { useEffect } from "react"; @@ -16,7 +16,7 @@ const QuestionDetailPage = () => { isLoading, isError, error, - } = useGetQuestion(questionId!); + } = useGetQuestionContent(questionId!); useEffect(() => { if (!questionId) { From 19a436d03e24bcfc2400b3b3b533aa5eb04fbe21 Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Wed, 27 Nov 2024 18:33:49 +0900 Subject: [PATCH 11/86] =?UTF-8?q?refactor:=20=EC=84=B8=EC=85=98=20?= =?UTF-8?q?=ED=97=A4=EB=8D=94=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20?= =?UTF-8?q?=EA=B0=80=EB=8F=85=EC=84=B1=20=ED=96=A5=EC=83=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/session/SessionHeader.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/session/SessionHeader.tsx b/frontend/src/components/session/SessionHeader.tsx index cc8b7d13..af19b08e 100644 --- a/frontend/src/components/session/SessionHeader.tsx +++ b/frontend/src/components/session/SessionHeader.tsx @@ -10,14 +10,20 @@ const SessionHeader = ({ roomMetadata, }: SessionHeaderProps) => { return ( -

- {roomMetadata?.category} {roomMetadata?.title}{" "} +
+ + {roomMetadata?.category} + +

{roomMetadata?.title}

- {" "} {roomMetadata && `(${participantsCount} / ${roomMetadata.maxParticipants})`} -

+
); }; From 985a802affc9e1608ddd331b8aa213d67bbd20ae Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Wed, 27 Nov 2024 19:03:17 +0900 Subject: [PATCH 12/86] =?UTF-8?q?refactor:=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EA=B7=B8=EB=A6=AC=EB=93=9C=EA=B0=80=20=EB=B8=8C=EB=9D=BC?= =?UTF-8?q?=EC=9A=B0=EC=A0=80=20=EC=A0=84=EC=B2=B4=20=EB=86=92=EC=9D=B4?= =?UTF-8?q?=EB=A5=BC=20=EB=B2=97=EC=96=B4=EB=82=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/session/SessionHeader.tsx | 23 +++++++++------ .../src/components/session/VideoContainer.tsx | 2 +- frontend/src/pages/SessionPage.tsx | 28 ++++++++++++------- 3 files changed, 34 insertions(+), 19 deletions(-) diff --git a/frontend/src/components/session/SessionHeader.tsx b/frontend/src/components/session/SessionHeader.tsx index af19b08e..09d1f4d7 100644 --- a/frontend/src/components/session/SessionHeader.tsx +++ b/frontend/src/components/session/SessionHeader.tsx @@ -15,14 +15,21 @@ const SessionHeader = ({ "inline-flex justify-center items-center gap-2 text-center text-medium-xl font-bold w-full pt-4 pb-2" } > - - {roomMetadata?.category} - -

{roomMetadata?.title}

- - {roomMetadata && - `(${participantsCount} / ${roomMetadata.maxParticipants})`} - + {roomMetadata?.title ? ( + <> + {" "} + + {roomMetadata?.category} + +

{roomMetadata?.title}

+ + {roomMetadata && + `(${participantsCount} / ${roomMetadata.maxParticipants})`} + + + ) : ( +

아직 세션에 참가하지 않았습니다.

+ )}
); }; diff --git a/frontend/src/components/session/VideoContainer.tsx b/frontend/src/components/session/VideoContainer.tsx index 7ca6b072..0b79869d 100644 --- a/frontend/src/components/session/VideoContainer.tsx +++ b/frontend/src/components/session/VideoContainer.tsx @@ -50,7 +50,7 @@ const VideoContainer = ({ }; return ( -
+
diff --git a/frontend/src/pages/SessionPage.tsx b/frontend/src/pages/SessionPage.tsx index 011d8a99..349429dc 100644 --- a/frontend/src/pages/SessionPage.tsx +++ b/frontend/src/pages/SessionPage.tsx @@ -41,7 +41,7 @@ const SessionPage = () => { } = useSession(sessionId!); return ( -
+
{/*{!username && (*/} {
-
+
+
- -
+
{ stream={stream!} />
-
+
{peers.map((peer) => ( Date: Wed, 27 Nov 2024 19:16:09 +0900 Subject: [PATCH 13/86] =?UTF-8?q?refactor:=20=EB=B9=84=EB=94=94=EC=98=A4?= =?UTF-8?q?=20=EA=B7=B8=EB=A6=AC=EB=93=9C=EA=B0=80=20=EC=9E=90=EC=97=B0?= =?UTF-8?q?=EC=8A=A4=EB=9F=BD=EA=B2=8C=20=EA=B3=B5=EA=B0=84=EC=9D=84=20?= =?UTF-8?q?=EC=B0=A8=EC=A7=80=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/session/VideoContainer.tsx | 4 ++-- frontend/src/index.css | 22 ++++++++++--------- frontend/src/pages/SessionPage.tsx | 2 +- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/session/VideoContainer.tsx b/frontend/src/components/session/VideoContainer.tsx index 0b79869d..c74dfe1d 100644 --- a/frontend/src/components/session/VideoContainer.tsx +++ b/frontend/src/components/session/VideoContainer.tsx @@ -50,8 +50,8 @@ const VideoContainer = ({ }; return ( -
-
+
+

diff --git a/frontend/src/index.css b/frontend/src/index.css index 4e429d3c..c6b4bbb4 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,13 +1,12 @@ -@import url('https://fonts.googleapis.com/css2?family=Raleway:wght@400;500;600;700&display=swap'); -@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css'); +@import url("https://fonts.googleapis.com/css2?family=Raleway:wght@400;500;600;700&display=swap"); +@import url("https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css"); @tailwind base; @tailwind components; @tailwind utilities; - @layer base { html { - font-family: Pretendard, system-ui, sans-serif; + font-family: Pretendard, system-ui, sans-serif; } } @@ -17,13 +16,13 @@ } .no-scrollbar { - -ms-overflow-style: none; - scrollbar-width: none; + -ms-overflow-style: none; + scrollbar-width: none; } } :root { - --bg-color-default: #FAFAFA; + --bg-color-default: #fafafa; --text-default: #101010; line-height: 1.5; @@ -40,14 +39,17 @@ .dark { --bg-color-default: #141414; - --text-default: #FFFFFF; + --text-default: #ffffff; } +.aspect-4-3 { + aspect-ratio: 4 / 3; +} /* Animation */ @keyframes expand { 0% { - transform: scaleX(0) + transform: scaleX(0); } 100% { transform: scaleX(100%); @@ -71,4 +73,4 @@ transform: scaleY(1); transform-origin: center; } -} \ No newline at end of file +} diff --git a/frontend/src/pages/SessionPage.tsx b/frontend/src/pages/SessionPage.tsx index 349429dc..17c3442b 100644 --- a/frontend/src/pages/SessionPage.tsx +++ b/frontend/src/pages/SessionPage.tsx @@ -72,7 +72,7 @@ const SessionPage = () => { />

Date: Wed, 27 Nov 2024 19:16:46 +0900 Subject: [PATCH 14/86] =?UTF-8?q?refactor:=20reaction=20=EC=83=81=ED=83=9C?= =?UTF-8?q?=EB=A5=BC=20useReaction=20=ED=9B=85=20=EB=82=B4=EB=B6=80?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/hooks/session/useReaction.ts | 8 +++++--- frontend/src/hooks/session/useSession.ts | 6 ++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/src/hooks/session/useReaction.ts b/frontend/src/hooks/session/useReaction.ts index f592f87b..47209947 100644 --- a/frontend/src/hooks/session/useReaction.ts +++ b/frontend/src/hooks/session/useReaction.ts @@ -4,6 +4,7 @@ import { useCallback, useEffect, useRef, + useState, } from "react"; import { Socket } from "socket.io-client"; import { PeerConnection } from "../type/session"; @@ -14,9 +15,10 @@ const REACTION_DURATION = 3000; export const useReaction = ( socket: Socket | null, sessionId: string, - setPeers: Dispatch>, - setReaction: (reaction: string) => void + setPeers: Dispatch> ) => { + const [reaction, setReaction] = useState(""); + const reactionTimeouts = useRef<{ [key: string]: ReturnType; }>({}); @@ -78,5 +80,5 @@ export const useReaction = ( }; }, []); - return { emitReaction, handleReaction }; + return { reaction, emitReaction, handleReaction }; }; diff --git a/frontend/src/hooks/session/useSession.ts b/frontend/src/hooks/session/useSession.ts index 0acf9215..946f4018 100644 --- a/frontend/src/hooks/session/useSession.ts +++ b/frontend/src/hooks/session/useSession.ts @@ -27,7 +27,6 @@ export const useSession = (sessionId: string) => { } = usePeerConnection(socket!); const { nickname: username } = useAuth(); const [nickname, setNickname] = useState(""); - const [reaction, setReaction] = useState(""); const [roomMetadata, setRoomMetadata] = useState(null); const [isHost, setIsHost] = useState(false); @@ -61,11 +60,10 @@ export const useSession = (sessionId: string) => { usePeerConnectionCleanup(peerConnections); useMediaStreamCleanup(stream); - const { emitReaction, handleReaction } = useReaction( + const { reaction, emitReaction, handleReaction } = useReaction( socket, sessionId, - setPeers, - setReaction + setPeers ); useSocketEvents({ From 024cb2b0db4c570f5ab2a604649994afbeb07170 Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Wed, 27 Nov 2024 19:22:18 +0900 Subject: [PATCH 15/86] =?UTF-8?q?chore:=20=EC=BD=98=EC=86=94=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/hooks/session/useSocketEvents.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/src/hooks/session/useSocketEvents.ts b/frontend/src/hooks/session/useSocketEvents.ts index 2323a40f..d9054ff8 100644 --- a/frontend/src/hooks/session/useSocketEvents.ts +++ b/frontend/src/hooks/session/useSocketEvents.ts @@ -123,8 +123,6 @@ export const useSocketEvents = ({ setRoomMetadata(roomMetadata); setIsHost(roomMetadata.host.socketId === socket.id); - console.log(connectionMap); - Object.entries(connectionMap).forEach(([socketId, userInfo]) => { console.log("socketId", socketId, "connection", userInfo); createPeerConnection(socketId, userInfo.nickname, stream, true, { @@ -167,7 +165,6 @@ export const useSocketEvents = ({ sdp: RTCSessionDescription; answerSendID: string; }) => { - console.log(data); const pc = peerConnections.current[data.answerSendID]; if (!pc) return; From ceb2706b766567a2fbce36c81852638e72fa9c52 Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Wed, 27 Nov 2024 19:54:19 +0900 Subject: [PATCH 16/86] =?UTF-8?q?feat:=20=EB=B9=84=EB=94=94=EC=98=A4=20?= =?UTF-8?q?=EC=BC=9C=EA=B3=A0=20=EB=81=8C=EB=95=8C=20=EB=A1=9C=EB=94=A9=20?= =?UTF-8?q?=EC=9D=B8=EB=94=94=EC=BC=80=EC=9D=B4=ED=84=B0=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/public/assets/spinner.lottie | Bin 0 -> 910 bytes .../components/common/LoadingIndicator.tsx | 37 ++++++++++++++---- .../src/components/session/VideoContainer.tsx | 12 ++++++ frontend/src/hooks/session/useMediaDevices.ts | 12 ++++-- frontend/src/hooks/session/useSession.ts | 2 + frontend/src/pages/SessionPage.tsx | 2 + 6 files changed, 54 insertions(+), 11 deletions(-) create mode 100644 frontend/public/assets/spinner.lottie diff --git a/frontend/public/assets/spinner.lottie b/frontend/public/assets/spinner.lottie new file mode 100644 index 0000000000000000000000000000000000000000..c5741fc987ca6a0d30fbfdc6ee80a86e1a3802cc GIT binary patch literal 910 zcmWIWW@Zs#U|`^2C=0EQ^p*-}=sU$&_SiO}C_+DeKto|GB}oX#T|PEe`dO2X<%WyIt~>63J6i=9Aibz%P1X z$nCyui(h(#ywra1eU411VDLP{168s00VvLW(jFOhi;;oh2QveM3DCJf*XAacWaj4; z>!+C-q*@r5ChDdnr<&@T7$+L*CYl>t>KYmv86{borzV@FrhK3QIqp= zDam@R+rxZ+UuySS`%B1G+w7y3iJIesy__H4S07W$*>a1UBd7oT9LvX&{UQ9#h0LL^ zdljPXXU==B{I<+`)sh9zB;3BcglfIG5#(C`tu%d6JWrnpr`3VfCBpytLNqni9sllm zRB3+xdX(3}`F9RP&0ibA-n~50d(yHd`+!`vwkByQwm$3A&T|yHdRkkKis~JFtMta_ z{nqtsa(5kc*`K}r`=i^+YvT9UN4x$%b8YS~jg)fHvmUdKNHFdEW^}=W*I$07yPXJMklXC#uxFO zt`+o=Qk%lM^-IG}kNV%szKTlS)4SMz%<9X%&?5(?Gx730xP9!_)s6c$ve`cRWv!gC z+ob)&`$_*Ut>u3n+Ske|=k=z3{pzjaz3+crKDcM?%k=p2-)Y=|jIMUsUlfdN6E#vT zMCU$_3`|LAi@CRl(RjXUUXT6CYJug2V%aW{d5RnXJ3lPhbm3Ds- { +const LoadingIndicator = ({ + type = "threeDots", + loadingState, + text, +}: LoadingIndicator) => { + const render = () => { + switch (type) { + case "threeDots": + return ( + + ); + case "spinner": + return ( + + ); + } + }; return ( loadingState && (
- + {render()} {text && (

{text}

)} diff --git a/frontend/src/components/session/VideoContainer.tsx b/frontend/src/components/session/VideoContainer.tsx index c74dfe1d..1169b884 100644 --- a/frontend/src/components/session/VideoContainer.tsx +++ b/frontend/src/components/session/VideoContainer.tsx @@ -5,6 +5,7 @@ import { BsCameraVideoOff, } from "react-icons/bs"; import DisplayMediaStream from "./DisplayMediaStream.tsx"; +import LoadingIndicator from "@components/common/LoadingIndicator.tsx"; interface VideoContainerProps { nickname: string; @@ -13,6 +14,7 @@ interface VideoContainerProps { isLocal: boolean; reaction: string; stream: MediaStream; + videoLoading?: boolean; } const VideoContainer = ({ @@ -22,6 +24,7 @@ const VideoContainer = ({ isLocal, reaction, stream, + videoLoading, }: VideoContainerProps) => { const renderReaction = (reactionType: string) => { switch (reactionType) { @@ -62,6 +65,15 @@ const VideoContainer = ({ {renderVideoIcon()}
+ {videoLoading && ( +
+ +
+ )}
{
{ }); setStream(null); } + setVideoLoading(true); const myStream = await navigator.mediaDevices.getUserMedia({ video: selectedVideoDeviceId ? { deviceId: selectedVideoDeviceId } @@ -95,6 +96,8 @@ const useMediaDevices = () => { "미디어 스트림을 가져오는 도중 문제가 발생했습니다.", error ); + } finally { + setVideoLoading(false); } }; @@ -123,6 +126,7 @@ const useMediaDevices = () => { const handleVideoToggle = async (peerConnections: PeerConnectionsMap) => { try { if (!stream) return; + setVideoLoading(true); // 비디오 껐다키기 if (isVideoOn) { @@ -142,7 +146,7 @@ const useMediaDevices = () => { const blackStream = blackCanvas.captureStream(); const blackTrack = blackStream.getVideoTracks()[0]; - Object.values(peerConnections).forEach((pc) => { + Object.values(peerConnections || {}).forEach((pc) => { const sender = pc .getSenders() .find((s) => s.track!.kind === "video"); @@ -159,7 +163,6 @@ const useMediaDevices = () => { const newVideoTrack = videoStream.getVideoTracks()[0]; if (videoStream) { - setVideoLoading(true); if (streamRef.current) { const oldVideoTracks = streamRef.current.getVideoTracks(); oldVideoTracks.forEach((track) => @@ -169,7 +172,7 @@ const useMediaDevices = () => { streamRef.current.addTrack(newVideoTrack); setStream(streamRef.current); - Object.values(peerConnections).forEach((pc) => { + Object.values(peerConnections || {}).forEach((pc) => { const sender = pc .getSenders() .find((s) => s.track?.kind === "video"); @@ -181,11 +184,12 @@ const useMediaDevices = () => { } else { console.error("비디오 스트림을 생성하지 못했습니다."); } - setVideoLoading(false); setIsVideoOn((prev) => !prev); } } catch (error) { console.error("비디오 스트림을 토글 할 수 없었습니다.", error); + } finally { + setVideoLoading(false); } }; diff --git a/frontend/src/hooks/session/useSession.ts b/frontend/src/hooks/session/useSession.ts index 946f4018..b4265520 100644 --- a/frontend/src/hooks/session/useSession.ts +++ b/frontend/src/hooks/session/useSession.ts @@ -43,6 +43,7 @@ export const useSession = (sessionId: string) => { setSelectedAudioDeviceId, setSelectedVideoDeviceId, getMedia, + videoLoading, } = useMediaDevices(); useEffect(() => { @@ -143,5 +144,6 @@ export const useSession = (sessionId: string) => { setSelectedVideoDeviceId, joinRoom, emitReaction, + videoLoading, }; }; diff --git a/frontend/src/pages/SessionPage.tsx b/frontend/src/pages/SessionPage.tsx index 17c3442b..41870a91 100644 --- a/frontend/src/pages/SessionPage.tsx +++ b/frontend/src/pages/SessionPage.tsx @@ -38,6 +38,7 @@ const SessionPage = () => { setSelectedVideoDeviceId, joinRoom, emitReaction, + videoLoading, } = useSession(sessionId!); return ( @@ -87,6 +88,7 @@ const SessionPage = () => { isLocal={true} reaction={reaction || ""} stream={stream!} + videoLoading={videoLoading} />
Date: Wed, 27 Nov 2024 19:55:36 +0900 Subject: [PATCH 17/86] =?UTF-8?q?feat:=20=EB=B9=84=EB=94=94=EC=98=A4?= =?UTF-8?q?=EA=B0=80=20=EB=A1=9C=EB=94=A9=20=EC=A4=91=EC=9D=BC=20=EB=95=8C?= =?UTF-8?q?=EB=8A=94=20=EB=B9=84=EB=94=94=EC=98=A4=20=ED=86=A0=EA=B8=80=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=EB=B9=84=ED=99=9C=EC=84=B1=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/session/SessionToolbar.tsx | 5 ++++- frontend/src/pages/SessionPage.tsx | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/session/SessionToolbar.tsx b/frontend/src/components/session/SessionToolbar.tsx index 81e73514..d0c50e53 100644 --- a/frontend/src/components/session/SessionToolbar.tsx +++ b/frontend/src/components/session/SessionToolbar.tsx @@ -17,6 +17,7 @@ interface Props { setSelectedAudioDeviceId: (deviceId: string) => void; isVideoOn: boolean; isMicOn: boolean; + videoLoading: boolean; } const SessionToolbar = ({ handleVideoToggle, @@ -28,6 +29,7 @@ const SessionToolbar = ({ setSelectedAudioDeviceId, isVideoOn, isMicOn, + videoLoading, }: Props) => { return (
Date: Wed, 27 Nov 2024 20:11:46 +0900 Subject: [PATCH 18/86] =?UTF-8?q?fix:=20index=20already=20exists=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 외부라이브러리를 고쳐서 해결했습니다. --- backend/package.json | 4 +- backend/src/app.module.ts | 2 +- backend/src/main.ts | 5 +- backend/src/room/room.entity.ts | 2 +- backend/src/room/room.module.ts | 2 +- backend/src/room/room.repository.ts | 2 +- backend/src/websocket/websocket.entity.ts | 2 +- backend/src/websocket/websocket.module.ts | 2 +- backend/src/websocket/websocket.repository.ts | 2 +- pnpm-lock.yaml | 66 ++++++++++--------- 10 files changed, 44 insertions(+), 45 deletions(-) diff --git a/backend/package.json b/backend/package.json index a7a4e2c1..5fc7e834 100644 --- a/backend/package.json +++ b/backend/package.json @@ -21,6 +21,7 @@ "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { + "@moozeh/nestjs-redis-om": "^0.1.4", "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", "@nestjs/jwt": "^10.2.0", @@ -32,13 +33,12 @@ "@socket.io/redis-adapter": "^8.3.0", "@types/passport-jwt": "^4.0.1", "axios": "^1.7.7", - "cookie-parser": "^1.4.7", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "cookie-parser": "^1.4.7", "dotenv": "^16.4.5", "ioredis": "^5.4.1", "mysql2": "^3.11.4", - "nestjs-redis-om": "^0.1.2", "passport": "^0.7.0", "passport-custom": "^1.1.1", "passport-jwt": "^4.0.1", diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 20a28fbf..93dd2b4b 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -12,7 +12,7 @@ import "dotenv/config"; import { createDataSource, typeOrmConfig } from "./config/typeorm.config"; import { QuestionListModule } from "./question-list/question-list.module"; -import { RedisOmModule } from "nestjs-redis-om"; +import { RedisOmModule } from "@moozeh/nestjs-redis-om"; import { SigServerModule } from "@/signaling-server/sig-server.module"; @Module({ diff --git a/backend/src/main.ts b/backend/src/main.ts index 7fe63a87..d51b668b 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -1,9 +1,6 @@ import { NestFactory } from "@nestjs/core"; import { AppModule } from "./app.module"; -import { - initializeTransactionalContext, - StorageDriver, -} from "typeorm-transactional"; +import { initializeTransactionalContext, StorageDriver } from "typeorm-transactional"; import * as cookieParser from "cookie-parser"; async function bootstrap() { diff --git a/backend/src/room/room.entity.ts b/backend/src/room/room.entity.ts index 7d31b558..35b4e7aa 100644 --- a/backend/src/room/room.entity.ts +++ b/backend/src/room/room.entity.ts @@ -1,4 +1,4 @@ -import { Entity, Field, Schema } from "nestjs-redis-om"; +import { Entity, Field, Schema } from "@moozeh/nestjs-redis-om"; export interface Connection { socketId: string; diff --git a/backend/src/room/room.module.ts b/backend/src/room/room.module.ts index cd1a0574..b02c430b 100644 --- a/backend/src/room/room.module.ts +++ b/backend/src/room/room.module.ts @@ -3,7 +3,7 @@ import { RoomService } from "./services/room.service"; import { RoomGateway } from "./room.gateway"; import { RoomRepository } from "./room.repository"; import { RoomController } from "./room.controller"; -import { RedisOmModule } from "nestjs-redis-om"; +import { RedisOmModule } from "@moozeh/nestjs-redis-om"; import { RoomEntity } from "./room.entity"; import { RoomLeaveService } from "@/room/services/room-leave.service"; import { RoomHostService } from "@/room/services/room-host.service"; diff --git a/backend/src/room/room.repository.ts b/backend/src/room/room.repository.ts index 599b49d8..f168af2f 100644 --- a/backend/src/room/room.repository.ts +++ b/backend/src/room/room.repository.ts @@ -1,5 +1,5 @@ import { Injectable } from "@nestjs/common"; -import { InjectRepository } from "nestjs-redis-om"; +import { InjectRepository } from "@moozeh/nestjs-redis-om"; import { Repository } from "redis-om"; import { RoomEntity } from "@/room/room.entity"; import { RoomDto } from "@/room/dto/room.dto"; diff --git a/backend/src/websocket/websocket.entity.ts b/backend/src/websocket/websocket.entity.ts index a6f3b482..b77a58df 100644 --- a/backend/src/websocket/websocket.entity.ts +++ b/backend/src/websocket/websocket.entity.ts @@ -1,4 +1,4 @@ -import { Entity, Field, Schema } from "nestjs-redis-om"; +import { Entity, Field, Schema } from "@moozeh/nestjs-redis-om"; @Schema("socket") export class WebsocketEntity extends Entity { diff --git a/backend/src/websocket/websocket.module.ts b/backend/src/websocket/websocket.module.ts index cf3842fd..68cab311 100644 --- a/backend/src/websocket/websocket.module.ts +++ b/backend/src/websocket/websocket.module.ts @@ -2,7 +2,7 @@ import { Global, Module } from "@nestjs/common"; import { WebsocketService } from "@/websocket/websocket.service"; import { WebsocketGateway } from "@/websocket/websocket.gateway"; import { WebsocketRepository } from "@/websocket/websocket.repository"; -import { RedisOmModule } from "nestjs-redis-om"; +import { RedisOmModule } from "@moozeh/nestjs-redis-om"; import { RoomEntity } from "@/room/room.entity"; import { WebsocketEntity } from "@/websocket/websocket.entity"; diff --git a/backend/src/websocket/websocket.repository.ts b/backend/src/websocket/websocket.repository.ts index 8bd27e59..e2fa3ee1 100644 --- a/backend/src/websocket/websocket.repository.ts +++ b/backend/src/websocket/websocket.repository.ts @@ -1,6 +1,6 @@ import { Injectable } from "@nestjs/common"; import { Socket } from "socket.io"; -import { InjectRepository } from "nestjs-redis-om"; +import { InjectRepository } from "@moozeh/nestjs-redis-om"; import { Repository } from "redis-om"; import { WebsocketEntity } from "@/websocket/websocket.entity"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 25908ae8..dd77782f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: backend: dependencies: + '@moozeh/nestjs-redis-om': + specifier: ^0.1.4 + version: 0.1.4(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(redis-om@0.4.7)(reflect-metadata@0.2.2) '@nestjs/common': specifier: ^10.0.0 version: 10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -71,9 +74,6 @@ importers: mysql2: specifier: ^3.11.4 version: 3.11.4 - nestjs-redis-om: - specifier: ^0.1.2 - version: 0.1.2(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(redis-om@0.4.7)(reflect-metadata@0.2.2) passport: specifier: ^0.7.0 version: 0.7.0 @@ -1041,6 +1041,14 @@ packages: resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} engines: {node: '>=8'} + '@moozeh/nestjs-redis-om@0.1.4': + resolution: {integrity: sha512-p0UT1WccsLNKOpa5E69fknJZFZAbK2LOImK4JWuuXtGnFJ2DSGtZ8Jbm4TNV6VkdL6x5yqg8YeX44DdZQHuZfA==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + '@nestjs/core': ^8.0.0 || ^9.0.0 || ^10.0.0 + redis-om: ^0.4.3 + reflect-metadata: ^0.1.13 || ^0.2.0 + '@nestjs/cli@10.4.5': resolution: {integrity: sha512-FP7Rh13u8aJbHe+zZ7hM0CC4785g9Pw4lz4r2TTgRtf0zTxSWMkJaPEwyjX8SK9oWK2GsYxl+fKpwVZNbmnj9A==} engines: {node: '>= 16.14'} @@ -1322,6 +1330,11 @@ packages: '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + '@socket.io/redis-adapter@8.3.0': + resolution: {integrity: sha512-ly0cra+48hDmChxmIpnESKrc94LjRL80TEmZVscuQ/WWkRP81nNj8W8cCGMqbI4L6NCuAaPRSzZF1a9GlAxxnA==} + engines: {node: '>=10.0.0'} + peerDependencies: + socket.io-adapter: ^2.5.4 '@sqltools/formatter@1.2.5': resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==} @@ -1329,24 +1342,19 @@ packages: '@tanstack/query-core@5.60.6': resolution: {integrity: sha512-tI+k0KyCo1EBJ54vxK1kY24LWj673ujTydCZmzEZKAew4NqZzTaVQJEuaG1qKj2M03kUHN46rchLRd+TxVq/zQ==} - '@tanstack/query-devtools@5.59.20': - resolution: {integrity: sha512-vxhuQ+8VV4YWQSFxQLsuM+dnEKRY7VeRzpNabFXdhEwsBYLrjXlF1pM38A8WyKNLqZy8JjyRO8oP4Wd/oKHwuQ==} + '@tanstack/query-devtools@5.61.3': + resolution: {integrity: sha512-AoRco+DMw7Xy9fFs+5BxBop82YPKs1/tWpTPoO1iYVwPLmAU+znnLfWyZ8Qr5OiEqoS0dCyEe6F5V11/JkCK/A==} - '@tanstack/react-query-devtools@5.60.6': - resolution: {integrity: sha512-b55QgJs2PSzOtLdE3p9fxPOPmeXXfkzcsyVBFFyXwINIilSwz/wWoz2D5fwg5Vbp60CX34LccstzryU6Bg6Rqw==} + '@tanstack/react-query-devtools@5.61.3': + resolution: {integrity: sha512-bR/TaiOSqTq0M5dmYY+pJeSnl5QAuCaRRmJg+Q5hEqt6uTHgKz5WO4jdi8BywRJiZhpXLirlUAIOXJsZ8ukqSA==} peerDependencies: - '@tanstack/react-query': ^5.60.6 + '@tanstack/react-query': ^5.61.3 react: ^18 || ^19 - '@tanstack/react-query@5.60.6': - resolution: {integrity: sha512-FUzSDaiPkuZCmuGqrixfRRXJV9u+nrUh9lAlA5Q3ZFrOw1Js1VeBfxi1NIcJO3ZWJdKceBqKeBJdNcWStCYZnw==} - - '@socket.io/redis-adapter@8.3.0': - resolution: {integrity: sha512-ly0cra+48hDmChxmIpnESKrc94LjRL80TEmZVscuQ/WWkRP81nNj8W8cCGMqbI4L6NCuAaPRSzZF1a9GlAxxnA==} - engines: {node: '>=10.0.0'} - + '@tanstack/react-query@5.61.3': + resolution: {integrity: sha512-c3Oz9KaCBapGkRewu7AJLhxE9BVqpMcHsd3KtFxSd7FSCu2qGwqfIN37zbSGoyk6Ix9LGZBNHQDPI6GpWABnmA==} peerDependencies: - socket.io-adapter: ^2.5.4 + react: ^18 || ^19 '@testing-library/dom@10.4.0': resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} @@ -3501,14 +3509,6 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - nestjs-redis-om@0.1.2: - resolution: {integrity: sha512-QTY0EkpIB+Fdm0KXs7fzpgLbF0NJumEcyZ1dA4Fi0/FlPEc4QirFP6Qj9QdkxIIzdg2yi+MB6LIZ3pflSiUEMA==} - peerDependencies: - '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 - '@nestjs/core': ^8.0.0 || ^9.0.0 || ^10.0.0 - redis-om: ^0.4.3 - reflect-metadata: ^0.1.13 || ^0.2.0 - node-abort-controller@3.1.1: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} @@ -5657,6 +5657,14 @@ snapshots: '@lukeed/csprng@1.1.0': {} + '@moozeh/nestjs-redis-om@0.1.4(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(redis-om@0.4.7)(reflect-metadata@0.2.2)': + dependencies: + '@nestjs/common': 10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.4.6(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.6)(@nestjs/websockets@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1) + redis-om: 0.4.7 + reflect-metadata: 0.2.2 + uuid: 9.0.1 + '@nestjs/cli@10.4.5': dependencies: '@angular-devkit/core': 17.3.8(chokidar@3.6.0) @@ -5943,6 +5951,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@sqltools/formatter@1.2.5': {} + '@tanstack/query-core@5.60.6': {} '@tanstack/query-devtools@5.61.3': {} @@ -8569,14 +8579,6 @@ snapshots: neo-async@2.6.2: {} - nestjs-redis-om@0.1.2(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(redis-om@0.4.7)(reflect-metadata@0.2.2): - dependencies: - '@nestjs/common': 10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) - '@nestjs/core': 10.4.6(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.6)(@nestjs/websockets@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1) - redis-om: 0.4.7 - reflect-metadata: 0.2.2 - uuid: 9.0.1 - node-abort-controller@3.1.1: {} node-emoji@1.11.0: From cf8a77f43896393e7e795fec422e238ad1fcaabd Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Wed, 27 Nov 2024 20:17:51 +0900 Subject: [PATCH 19/86] =?UTF-8?q?feat:=20=EB=AF=B8=EB=94=94=EC=96=B4=20?= =?UTF-8?q?=EC=9E=A5=EC=B9=98=20=EC=A4=91=20=ED=95=98=EB=82=98=EB=A7=8C=20?= =?UTF-8?q?=EC=97=86=EB=8A=94=20=EA=B2=BD=EC=9A=B0=EC=97=90=20=ED=86=A0?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=A9=94=EC=8B=9C=EC=A7=80=EC=99=80=20?= =?UTF-8?q?=ED=95=A8=EA=BB=98=20=EC=B0=B8=EC=97=AC=EC=8B=9C=ED=82=A4?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/common/LoadingIndicator.tsx | 2 +- frontend/src/hooks/session/useMediaDevices.ts | 92 ++++++++++++++++--- frontend/src/hooks/session/useSession.ts | 11 ++- 3 files changed, 88 insertions(+), 17 deletions(-) diff --git a/frontend/src/components/common/LoadingIndicator.tsx b/frontend/src/components/common/LoadingIndicator.tsx index 0915e3db..a66d44a6 100644 --- a/frontend/src/components/common/LoadingIndicator.tsx +++ b/frontend/src/components/common/LoadingIndicator.tsx @@ -29,7 +29,7 @@ const LoadingIndicator = ({ autoplay={true} loop={true} speed={1.5} - style={{ width: 120, height: 120 }} + style={{ width: 120, height: 120, opacity: 0.8 }} /> ); } diff --git a/frontend/src/hooks/session/useMediaDevices.ts b/frontend/src/hooks/session/useMediaDevices.ts index dedf2f90..1f833428 100644 --- a/frontend/src/hooks/session/useMediaDevices.ts +++ b/frontend/src/hooks/session/useMediaDevices.ts @@ -67,8 +67,40 @@ const useMediaDevices = () => { } }; }, []); + // + // // 미디어 스트림 가져오기: 자신의 스트림을 가져옴 + // const getMedia = async () => { + // try { + // if (streamRef.current) { + // // 이미 스트림이 있으면 종료 + // streamRef.current.getTracks().forEach((track) => { + // track.stop(); + // }); + // setStream(null); + // } + // setVideoLoading(true); + // const myStream = await navigator.mediaDevices.getUserMedia({ + // video: selectedVideoDeviceId + // ? { deviceId: selectedVideoDeviceId } + // : true, + // audio: selectedAudioDeviceId + // ? { deviceId: selectedAudioDeviceId } + // : true, + // }); + // + // streamRef.current = myStream; + // setStream(myStream); + // return myStream; + // } catch (error) { + // console.error( + // "미디어 스트림을 가져오는 도중 문제가 발생했습니다.", + // error + // ); + // } finally { + // setVideoLoading(false); + // } + // }; - // 미디어 스트림 가져오기: 자신의 스트림을 가져옴 const getMedia = async () => { try { if (streamRef.current) { @@ -79,23 +111,57 @@ const useMediaDevices = () => { setStream(null); } setVideoLoading(true); - const myStream = await navigator.mediaDevices.getUserMedia({ - video: selectedVideoDeviceId - ? { deviceId: selectedVideoDeviceId } - : true, - audio: selectedAudioDeviceId - ? { deviceId: selectedAudioDeviceId } - : true, - }); - - streamRef.current = myStream; - setStream(myStream); - return myStream; + + // 비디오와 오디오 스트림을 따로 가져오기 + let videoStream = null; + let audioStream = null; + + try { + videoStream = await navigator.mediaDevices.getUserMedia({ + video: selectedVideoDeviceId + ? { deviceId: selectedVideoDeviceId } + : true, + audio: false, + }); + } catch (videoError) { + console.warn("비디오 스트림을 가져오는데 실패했습니다:", videoError); + } + + try { + audioStream = await navigator.mediaDevices.getUserMedia({ + video: false, + audio: selectedAudioDeviceId + ? { deviceId: selectedAudioDeviceId } + : true, + }); + } catch (audioError) { + console.warn("오디오 스트림을 가져오는데 실패했습니다:", audioError); + } + + // 스트림 병합 또는 개별 스트림 사용 + let combinedStream = null; + const tracks = [ + ...(videoStream?.getVideoTracks() || []), + ...(audioStream?.getAudioTracks() || []), + ]; + + if (tracks.length > 0) { + combinedStream = new MediaStream(tracks); + streamRef.current = combinedStream; + console.log(combinedStream); + setStream(combinedStream); + return combinedStream; + } else { + throw new Error( + "비디오와 오디오 스트림을 모두 가져오는데 실패했습니다." + ); + } } catch (error) { console.error( "미디어 스트림을 가져오는 도중 문제가 발생했습니다.", error ); + // 에러 처리 로직 (예: 사용자에게 알림) } finally { setVideoLoading(false); } diff --git a/frontend/src/hooks/session/useSession.ts b/frontend/src/hooks/session/useSession.ts index b4265520..1c1914e6 100644 --- a/frontend/src/hooks/session/useSession.ts +++ b/frontend/src/hooks/session/useSession.ts @@ -1,5 +1,4 @@ import { useEffect, useMemo, useState } from "react"; -import { useNavigate } from "react-router-dom"; import useToast from "@hooks/useToast"; import useMediaDevices from "@hooks/session/useMediaDevices"; import usePeerConnection from "@hooks/session/usePeerConnection"; @@ -15,7 +14,6 @@ import useAuth from "@hooks/useAuth.ts"; export const useSession = (sessionId: string) => { const { socket } = useSocket(); - const navigate = useNavigate(); const toast = useToast(); const { @@ -107,8 +105,15 @@ export const useSession = (sessionId: string) => { toast.error( "미디어 스트림을 가져오지 못했습니다. 미디어 장치를 확인 후 다시 시도해주세요." ); - navigate("/sessions"); return; + } else if (mediaStream.getVideoTracks().length === 0) { + toast.error( + "비디오 장치를 찾을 수 없습니다. 비디오 장치 없이 세션에 참가합니다." + ); + } else if (mediaStream.getAudioTracks().length === 0) { + toast.error( + "오디오 장치를 찾을 수 없습니다. 오디오 장치 없이 세션에 참가합니다." + ); } socket.emit(SESSION_EMIT_EVENT.JOIN, { roomId: sessionId, nickname }); From fd3355ab1582207d2e8059fffacb44844e0b659f Mon Sep 17 00:00:00 2001 From: yiseungyun Date: Wed, 27 Nov 2024 20:19:52 +0900 Subject: [PATCH 20/86] =?UTF-8?q?feat:=20=EC=A7=88=EB=AC=B8=EC=A7=80=20api?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=ED=95=A8=EC=88=98=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/api/questions/createQuestionList.ts | 14 ++++++++++++++ .../src/api/questions/deleteQuestionList.ts | 7 +++++++ .../src/api/questions/editQuestionMetadata.ts | 19 +++++++++++++++++++ .../src/api/questions/getQuestionContent.ts | 4 ++-- ...teQuestion.ts => useCreateQuestionList.ts} | 4 ++-- .../src/hooks/api/useGetQuestionContent.ts | 4 ++-- 6 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 frontend/src/api/questions/createQuestionList.ts create mode 100644 frontend/src/api/questions/deleteQuestionList.ts create mode 100644 frontend/src/api/questions/editQuestionMetadata.ts rename frontend/src/hooks/api/{useCreateQuestion.ts => useCreateQuestionList.ts} (83%) diff --git a/frontend/src/api/questions/createQuestionList.ts b/frontend/src/api/questions/createQuestionList.ts new file mode 100644 index 00000000..65fe0487 --- /dev/null +++ b/frontend/src/api/questions/createQuestionList.ts @@ -0,0 +1,14 @@ +import axios from "axios"; + +interface QuestionListRequest { + title: string; + contents: string[]; + categoryNames: string[]; + isPublic: boolean; +} + +export const createQuestionList = async (data: QuestionListRequest) => { + const response = await axios.post("/api/question-list", data); + + return response.data; +}; diff --git a/frontend/src/api/questions/deleteQuestionList.ts b/frontend/src/api/questions/deleteQuestionList.ts new file mode 100644 index 00000000..2dae79bb --- /dev/null +++ b/frontend/src/api/questions/deleteQuestionList.ts @@ -0,0 +1,7 @@ +import axios from "axios"; + +export const deleteQuestionList = async (questionListId: number) => { + const response = await axios.delete(`api/question-list/${questionListId}`); + + return response.data; +}; diff --git a/frontend/src/api/questions/editQuestionMetadata.ts b/frontend/src/api/questions/editQuestionMetadata.ts new file mode 100644 index 00000000..6bcbfd91 --- /dev/null +++ b/frontend/src/api/questions/editQuestionMetadata.ts @@ -0,0 +1,19 @@ +import axios from "axios"; + +interface QuestionMetadataRequest { + title?: string; + categoryNames?: string[]; + isPublic?: boolean; +} + +export const editQuestionMetadata = async ( + questionListId: string, + data: QuestionMetadataRequest +) => { + const response = await axios.patch( + `/api/question-list/${questionListId}`, + data + ); + + return response.data; +}; diff --git a/frontend/src/api/questions/getQuestionContent.ts b/frontend/src/api/questions/getQuestionContent.ts index 355fe61b..396bc7bc 100644 --- a/frontend/src/api/questions/getQuestionContent.ts +++ b/frontend/src/api/questions/getQuestionContent.ts @@ -1,6 +1,6 @@ import axios from "axios"; -const fetchQuestionContent = async (questionListId: number) => { +const getQuestionContent = async (questionListId: number) => { const { data } = await axios.post("/api/question-list/contents", { questionListId, }); @@ -12,4 +12,4 @@ const fetchQuestionContent = async (questionListId: number) => { return data.data.questionListContents; }; -export default fetchQuestionContent; +export default getQuestionContent; diff --git a/frontend/src/hooks/api/useCreateQuestion.ts b/frontend/src/hooks/api/useCreateQuestionList.ts similarity index 83% rename from frontend/src/hooks/api/useCreateQuestion.ts rename to frontend/src/hooks/api/useCreateQuestionList.ts index 93bb67fb..3b0a3653 100644 --- a/frontend/src/hooks/api/useCreateQuestion.ts +++ b/frontend/src/hooks/api/useCreateQuestionList.ts @@ -1,9 +1,9 @@ import { useMutation } from "@tanstack/react-query"; -import { createQuestionList } from "@/api/questions/create.ts"; +import { createQuestionList } from "@/api/questions/createQuestionList"; import useToast from "@hooks/useToast.ts"; import { useNavigate } from "react-router-dom"; -export const useCreateQuestion = () => { +export const useCreateQuestionList = () => { const toast = useToast(); const navigate = useNavigate(); diff --git a/frontend/src/hooks/api/useGetQuestionContent.ts b/frontend/src/hooks/api/useGetQuestionContent.ts index 0c0df554..0fb1967b 100644 --- a/frontend/src/hooks/api/useGetQuestionContent.ts +++ b/frontend/src/hooks/api/useGetQuestionContent.ts @@ -1,4 +1,4 @@ -import fetchQuestion from "@/api/questions/getQuestionContent"; +import getQuestionContent from "@/api/questions/getQuestionContent"; import { useQuery } from "@tanstack/react-query"; interface QuestionContent { @@ -20,6 +20,6 @@ interface ApiResponse { export const useGetQuestionContent = (questionListId: string) => { return useQuery({ queryKey: ["questions", questionListId], - queryFn: () => fetchQuestion(Number(questionListId)), + queryFn: () => getQuestionContent(Number(questionListId)), }); }; From 03a2736850181361ac18b966bb2ba865fa5e19d5 Mon Sep 17 00:00:00 2001 From: yiseungyun Date: Wed, 27 Nov 2024 20:20:20 +0900 Subject: [PATCH 21/86] =?UTF-8?q?refactor:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EA=B5=AC=EC=A1=B0=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - container-presenter 패턴 적용 - ui 컴포넌트 pages/view에서 관리 - view의 레이아웃 컴포넌트만 해당 컴포넌트에서 관리하고, 그 외 하위 컴포넌트는 components/MyPage에서 관리하도록 수정 --- frontend/src/api/questions/create.ts | 14 -------------- frontend/src/components/common/PageTitle.tsx | 6 +++--- .../src/components/layout/SidebarPageLayout.tsx | 7 +++++-- .../{ProfileEditModal => }/ButtonSection.tsx | 0 .../CategoryTap => CategoryTab}/Category.tsx | 0 .../CategoryTap => CategoryTab}/index.tsx | 0 .../mypage/{Profile => }/ProfileIcon.tsx | 0 .../QuestionList/QuestionItem/Category.tsx | 0 .../QuestionList/QuestionItem/index.tsx | 1 - .../{QuestionSection => }/QuestionList/index.tsx | 0 .../questions/create/QuestionForm/index.tsx | 4 ++-- .../questions/detail/QuestionList.tsx/index.tsx | 4 ++-- .../questions/detail/QuestionTitle.tsx/index.tsx | 4 ++-- frontend/src/pages/MyPage/index.tsx | 2 +- .../index.tsx => pages/MyPage/view/Profile.tsx} | 2 +- .../MyPage/view/ProfileEditModal.tsx} | 2 +- .../MyPage/view/QuestionSection.tsx} | 6 +++--- .../MyPage/{MyPageView.tsx => view/index.tsx} | 10 +++++----- 18 files changed, 25 insertions(+), 37 deletions(-) delete mode 100644 frontend/src/api/questions/create.ts rename frontend/src/components/mypage/{ProfileEditModal => }/ButtonSection.tsx (100%) rename frontend/src/components/mypage/{QuestionSection/CategoryTap => CategoryTab}/Category.tsx (100%) rename frontend/src/components/mypage/{QuestionSection/CategoryTap => CategoryTab}/index.tsx (100%) rename frontend/src/components/mypage/{Profile => }/ProfileIcon.tsx (100%) rename frontend/src/components/mypage/{QuestionSection => }/QuestionList/QuestionItem/Category.tsx (100%) rename frontend/src/components/mypage/{QuestionSection => }/QuestionList/QuestionItem/index.tsx (99%) rename frontend/src/components/mypage/{QuestionSection => }/QuestionList/index.tsx (100%) rename frontend/src/{components/mypage/Profile/index.tsx => pages/MyPage/view/Profile.tsx} (91%) rename frontend/src/{components/mypage/ProfileEditModal/index.tsx => pages/MyPage/view/ProfileEditModal.tsx} (96%) rename frontend/src/{components/mypage/QuestionSection/index.tsx => pages/MyPage/view/QuestionSection.tsx} (83%) rename frontend/src/pages/MyPage/{MyPageView.tsx => view/index.tsx} (72%) diff --git a/frontend/src/api/questions/create.ts b/frontend/src/api/questions/create.ts deleted file mode 100644 index 65fe0487..00000000 --- a/frontend/src/api/questions/create.ts +++ /dev/null @@ -1,14 +0,0 @@ -import axios from "axios"; - -interface QuestionListRequest { - title: string; - contents: string[]; - categoryNames: string[]; - isPublic: boolean; -} - -export const createQuestionList = async (data: QuestionListRequest) => { - const response = await axios.post("/api/question-list", data); - - return response.data; -}; diff --git a/frontend/src/components/common/PageTitle.tsx b/frontend/src/components/common/PageTitle.tsx index bcc67011..b8558578 100644 --- a/frontend/src/components/common/PageTitle.tsx +++ b/frontend/src/components/common/PageTitle.tsx @@ -7,7 +7,7 @@ const PageTitle = ({ title }: TitleProps) => {

{title}

- ) -} + ); +}; -export default PageTitle; \ No newline at end of file +export default PageTitle; diff --git a/frontend/src/components/layout/SidebarPageLayout.tsx b/frontend/src/components/layout/SidebarPageLayout.tsx index 2c05fd8a..2f0f593f 100644 --- a/frontend/src/components/layout/SidebarPageLayout.tsx +++ b/frontend/src/components/layout/SidebarPageLayout.tsx @@ -6,12 +6,15 @@ interface SidebarPageLayoutProps { childrenClassName?: string; } -const SidebarPageLayout = ({ children, childrenClassName = "" }: SidebarPageLayoutProps) => { +const SidebarPageLayout = ({ + children, + childrenClassName = "", +}: SidebarPageLayoutProps) => { return (
{children}
-
+
); }; diff --git a/frontend/src/components/mypage/ProfileEditModal/ButtonSection.tsx b/frontend/src/components/mypage/ButtonSection.tsx similarity index 100% rename from frontend/src/components/mypage/ProfileEditModal/ButtonSection.tsx rename to frontend/src/components/mypage/ButtonSection.tsx diff --git a/frontend/src/components/mypage/QuestionSection/CategoryTap/Category.tsx b/frontend/src/components/mypage/CategoryTab/Category.tsx similarity index 100% rename from frontend/src/components/mypage/QuestionSection/CategoryTap/Category.tsx rename to frontend/src/components/mypage/CategoryTab/Category.tsx diff --git a/frontend/src/components/mypage/QuestionSection/CategoryTap/index.tsx b/frontend/src/components/mypage/CategoryTab/index.tsx similarity index 100% rename from frontend/src/components/mypage/QuestionSection/CategoryTap/index.tsx rename to frontend/src/components/mypage/CategoryTab/index.tsx diff --git a/frontend/src/components/mypage/Profile/ProfileIcon.tsx b/frontend/src/components/mypage/ProfileIcon.tsx similarity index 100% rename from frontend/src/components/mypage/Profile/ProfileIcon.tsx rename to frontend/src/components/mypage/ProfileIcon.tsx diff --git a/frontend/src/components/mypage/QuestionSection/QuestionList/QuestionItem/Category.tsx b/frontend/src/components/mypage/QuestionList/QuestionItem/Category.tsx similarity index 100% rename from frontend/src/components/mypage/QuestionSection/QuestionList/QuestionItem/Category.tsx rename to frontend/src/components/mypage/QuestionList/QuestionItem/Category.tsx diff --git a/frontend/src/components/mypage/QuestionSection/QuestionList/QuestionItem/index.tsx b/frontend/src/components/mypage/QuestionList/QuestionItem/index.tsx similarity index 99% rename from frontend/src/components/mypage/QuestionSection/QuestionList/QuestionItem/index.tsx rename to frontend/src/components/mypage/QuestionList/QuestionItem/index.tsx index b7afdd45..c9e078ff 100644 --- a/frontend/src/components/mypage/QuestionSection/QuestionList/QuestionItem/index.tsx +++ b/frontend/src/components/mypage/QuestionList/QuestionItem/index.tsx @@ -3,7 +3,6 @@ import { MdEdit } from "react-icons/md"; import { RiDeleteBin6Fill } from "react-icons/ri"; import { FaBookmark } from "react-icons/fa"; import Category from "./Category"; - interface ItemProps { type: "my" | "saved"; } diff --git a/frontend/src/components/mypage/QuestionSection/QuestionList/index.tsx b/frontend/src/components/mypage/QuestionList/index.tsx similarity index 100% rename from frontend/src/components/mypage/QuestionSection/QuestionList/index.tsx rename to frontend/src/components/mypage/QuestionList/index.tsx diff --git a/frontend/src/components/questions/create/QuestionForm/index.tsx b/frontend/src/components/questions/create/QuestionForm/index.tsx index 2ba8674c..1a7b8c8c 100644 --- a/frontend/src/components/questions/create/QuestionForm/index.tsx +++ b/frontend/src/components/questions/create/QuestionForm/index.tsx @@ -3,14 +3,14 @@ import AccessSection from "./AccessSection"; import CategorySection from "./CategorySection"; import TitleSection from "./TitleSection"; import QuestionInputSection from "./QuestionInputSection"; -import { useCreateQuestion } from "@hooks/api/useCreateQuestion.ts"; +import { useCreateQuestionList } from "@/hooks/api/useCreateQuestionList"; const QuestionForm = () => { const isValid = useQuestionFormStore((state) => state.isFormValid()); const { category, questionTitle, access, questionList } = useQuestionFormStore(); - const mutation = useCreateQuestion(); + const mutation = useCreateQuestionList(); const submitHandler = () => { const requestData = { diff --git a/frontend/src/components/questions/detail/QuestionList.tsx/index.tsx b/frontend/src/components/questions/detail/QuestionList.tsx/index.tsx index 027c144f..d5b16afa 100644 --- a/frontend/src/components/questions/detail/QuestionList.tsx/index.tsx +++ b/frontend/src/components/questions/detail/QuestionList.tsx/index.tsx @@ -1,4 +1,4 @@ -import { useGetQuestion } from "@/hooks/api/useGetQuestionContent"; +import { useGetQuestionContent } from "@/hooks/api/useGetQuestionContent"; import QuestionItem from "./QuestionItem"; const QuestionList = ({ questionId }: { questionId: string }) => { @@ -7,7 +7,7 @@ const QuestionList = ({ questionId }: { questionId: string }) => { isLoading, isError, error, - } = useGetQuestion(questionId); + } = useGetQuestionContent(questionId); if (isLoading) return
로딩 중
; if (isError) return
에러가 발생했습니다: {error.message}
; diff --git a/frontend/src/components/questions/detail/QuestionTitle.tsx/index.tsx b/frontend/src/components/questions/detail/QuestionTitle.tsx/index.tsx index d2099bd3..11d8b9ec 100644 --- a/frontend/src/components/questions/detail/QuestionTitle.tsx/index.tsx +++ b/frontend/src/components/questions/detail/QuestionTitle.tsx/index.tsx @@ -1,4 +1,4 @@ -import { useGetQuestion } from "@/hooks/api/useGetQuestionContent"; +import { useGetQuestionContent } from "@/hooks/api/useGetQuestionContent"; import { MdEdit } from "react-icons/md"; import { RiDeleteBin6Fill } from "react-icons/ri"; import { FaRegBookmark } from "react-icons/fa"; @@ -9,7 +9,7 @@ const QuestionTitle = ({ questionId }: { questionId: string }) => { isLoading, isError, error, - } = useGetQuestion(questionId); + } = useGetQuestionContent(questionId); if (isLoading) return
로딩 중
; if (isError) return
에러가 발생했습니다: {error.message}
; diff --git a/frontend/src/pages/MyPage/index.tsx b/frontend/src/pages/MyPage/index.tsx index 158e125f..ca97f35c 100644 --- a/frontend/src/pages/MyPage/index.tsx +++ b/frontend/src/pages/MyPage/index.tsx @@ -2,7 +2,7 @@ import { useEffect } from "react"; import { useNavigate } from "react-router-dom"; import useAuth from "@hooks/useAuth"; import useToast from "@hooks/useToast"; -import MyPageView from "./MyPageView"; +import MyPageView from "./view"; const MyPage = () => { const { isLoggedIn, nickname } = useAuth(); diff --git a/frontend/src/components/mypage/Profile/index.tsx b/frontend/src/pages/MyPage/view/Profile.tsx similarity index 91% rename from frontend/src/components/mypage/Profile/index.tsx rename to frontend/src/pages/MyPage/view/Profile.tsx index 6dcdc9d1..74c31de7 100644 --- a/frontend/src/components/mypage/Profile/index.tsx +++ b/frontend/src/pages/MyPage/view/Profile.tsx @@ -1,6 +1,6 @@ import useModalStore from "@stores/useModalStore"; import { MdEdit } from "react-icons/md"; -import ProfileIcon from "./ProfileIcon"; +import ProfileIcon from "../../../components/MyPage/ProfileIcon"; const Profile = ({ nickname }: { nickname: string }) => { const { openModal } = useModalStore(); diff --git a/frontend/src/components/mypage/ProfileEditModal/index.tsx b/frontend/src/pages/MyPage/view/ProfileEditModal.tsx similarity index 96% rename from frontend/src/components/mypage/ProfileEditModal/index.tsx rename to frontend/src/pages/MyPage/view/ProfileEditModal.tsx index addf722e..26ff8f2d 100644 --- a/frontend/src/components/mypage/ProfileEditModal/index.tsx +++ b/frontend/src/pages/MyPage/view/ProfileEditModal.tsx @@ -3,7 +3,7 @@ import useModal from "@/hooks/useModal"; import useModalStore from "@/stores/useModalStore"; import { useRef } from "react"; import { IoMdClose } from "react-icons/io"; -import ButtonSection from "./ButtonSection"; +import ButtonSection from "../../../components/MyPage/ButtonSection"; import useAuth from "@/hooks/useAuth"; const ProfileEditModal = () => { diff --git a/frontend/src/components/mypage/QuestionSection/index.tsx b/frontend/src/pages/MyPage/view/QuestionSection.tsx similarity index 83% rename from frontend/src/components/mypage/QuestionSection/index.tsx rename to frontend/src/pages/MyPage/view/QuestionSection.tsx index 01e57188..8afcf12e 100644 --- a/frontend/src/components/mypage/QuestionSection/index.tsx +++ b/frontend/src/pages/MyPage/view/QuestionSection.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; -import CategoryTap from "./CategoryTap"; -import QuestionList from "./QuestionList"; -import Pagination from "@/components/common/Pagination"; +import CategoryTap from "@components/MyPage/CategoryTab"; +import QuestionList from "@components/MyPage/QuestionList"; +import Pagination from "@components/common/Pagination"; type TabName = "myList" | "savedList"; diff --git a/frontend/src/pages/MyPage/MyPageView.tsx b/frontend/src/pages/MyPage/view/index.tsx similarity index 72% rename from frontend/src/pages/MyPage/MyPageView.tsx rename to frontend/src/pages/MyPage/view/index.tsx index 37cd3f13..0ef6815e 100644 --- a/frontend/src/pages/MyPage/MyPageView.tsx +++ b/frontend/src/pages/MyPage/view/index.tsx @@ -1,8 +1,8 @@ import SidebarPageLayout from "@components/layout/SidebarPageLayout"; import PageTitle from "@components/common/PageTitle"; -import ProfileEditModal from "@components/mypage/ProfileEditModal"; -import Profile from "@components/mypage/Profile"; -import QuestionSection from "@components/mypage/QuestionSection"; +import ProfileEditModal from "@/pages/MyPage/view/ProfileEditModal"; +import Profile from "@/pages/MyPage/view/Profile"; +import QuestionSection from "@/pages/MyPage/view/QuestionSection"; interface MyPageViewProps { nickname: string; @@ -17,8 +17,8 @@ const MyPageView = ({ nickname }: MyPageViewProps) => {
- + ); }; -export default MyPageView; \ No newline at end of file +export default MyPageView; From 98eaa251257f8ed8fbc7c50ef821772d981e166c Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Wed, 27 Nov 2024 21:56:21 +0900 Subject: [PATCH 22/86] =?UTF-8?q?refactor:=20=EA=B7=B8=EB=A6=AC=EB=93=9C?= =?UTF-8?q?=20=ED=99=94=EB=A9=B4=EC=9D=B4=20=EB=84=98=EC=B9=98=EB=8D=98=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=EB=A5=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/session/DisplayMediaStream.tsx | 2 +- frontend/src/components/session/SessionSidebar.tsx | 8 ++++++-- frontend/src/components/session/SessionToolbar.tsx | 2 +- frontend/src/components/session/VideoContainer.tsx | 4 ++-- frontend/src/pages/SessionPage.tsx | 8 ++++---- 5 files changed, 14 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/session/DisplayMediaStream.tsx b/frontend/src/components/session/DisplayMediaStream.tsx index 3a6564b6..b08c0b28 100644 --- a/frontend/src/components/session/DisplayMediaStream.tsx +++ b/frontend/src/components/session/DisplayMediaStream.tsx @@ -24,7 +24,7 @@ const DisplayMediaStream = ({ autoPlay playsInline muted={isLocal} - className="w-full" + className="h-full aspect-4-3" /> ); }; diff --git a/frontend/src/components/session/SessionSidebar.tsx b/frontend/src/components/session/SessionSidebar.tsx index 8bffd689..c0f7b23e 100644 --- a/frontend/src/components/session/SessionSidebar.tsx +++ b/frontend/src/components/session/SessionSidebar.tsx @@ -66,7 +66,11 @@ const SessionSidebar = ({ }; return ( -
+
-
+

diff --git a/frontend/src/components/session/SessionToolbar.tsx b/frontend/src/components/session/SessionToolbar.tsx index d0c50e53..6df53f3d 100644 --- a/frontend/src/components/session/SessionToolbar.tsx +++ b/frontend/src/components/session/SessionToolbar.tsx @@ -34,7 +34,7 @@ const SessionToolbar = ({ return (
- + + + ) : ( + - - ) : ( - - )} + )} +
+ + 작성자 눈드뮴눈드뮴눈드뮴눈드뮴눈드뮴눈드뮴눈드 +

- - 작성자 눈드뮴눈드뮴눈드뮴눈드뮴눈드뮴눈드뮴눈드 -
-
+ ); }; diff --git a/frontend/src/hooks/useModal.ts b/frontend/src/hooks/useModal.ts index 7197dbf8..735567e2 100644 --- a/frontend/src/hooks/useModal.ts +++ b/frontend/src/hooks/useModal.ts @@ -1,18 +1,31 @@ -import { useEffect } from "react"; +import { useEffect, useRef, useState } from "react"; -const useModal = ({ - dialogRef, - isModalOpen, -}: { +interface UseModalReturn { dialogRef: React.RefObject; - isModalOpen: boolean; -}) => { + isOpen: boolean; + openModal: () => void; + closeModal: () => void; +} + +const useModal = (): UseModalReturn => { + const dialogRef = useRef(null); + const [isOpen, setIsOpen] = useState(false); + useEffect(() => { const dialog = dialogRef.current; if (!dialog) return; - if (isModalOpen) { + if (isOpen) { dialog.showModal(); + + const handleEscape = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + e.preventDefault(); + setIsOpen(false); + } + }; + + window.addEventListener('keydown', handleEscape); } else { dialog.close(); } @@ -22,7 +35,14 @@ const useModal = ({ dialog.close(); } }; - }, [isModalOpen, dialogRef]); + }, [isOpen]); + + return { + dialogRef, + isOpen, + openModal: () => setIsOpen(true), + closeModal: () => setIsOpen(false) + }; }; export default useModal; diff --git a/frontend/src/pages/MyPage/view/Profile.tsx b/frontend/src/pages/MyPage/view/Profile.tsx index 74c31de7..33298dba 100644 --- a/frontend/src/pages/MyPage/view/Profile.tsx +++ b/frontend/src/pages/MyPage/view/Profile.tsx @@ -1,9 +1,20 @@ -import useModalStore from "@stores/useModalStore"; import { MdEdit } from "react-icons/md"; -import ProfileIcon from "../../../components/MyPage/ProfileIcon"; +import ProfileIcon from "@components/MyPage/ProfileIcon"; -const Profile = ({ nickname }: { nickname: string }) => { - const { openModal } = useModalStore(); +interface UseModalReturn { + dialogRef: React.RefObject; + isOpen: boolean; + openModal: () => void; + closeModal: () => void; +} + +const Profile = ({ + nickname, + modal +}: { + nickname: string + modal: UseModalReturn +}) => { return (
@@ -11,7 +22,7 @@ const Profile = ({ nickname }: { nickname: string }) => {

회원 정보

-
diff --git a/frontend/src/pages/MyPage/view/ProfileEditModal.tsx b/frontend/src/pages/MyPage/view/ProfileEditModal.tsx index 26ff8f2d..5a57d037 100644 --- a/frontend/src/pages/MyPage/view/ProfileEditModal.tsx +++ b/frontend/src/pages/MyPage/view/ProfileEditModal.tsx @@ -1,17 +1,17 @@ -import TitleInput from "@/components/common/TitleInput"; -import useModal from "@/hooks/useModal"; -import useModalStore from "@/stores/useModalStore"; -import { useRef } from "react"; +import TitleInput from "@components/common/TitleInput"; import { IoMdClose } from "react-icons/io"; -import ButtonSection from "../../../components/MyPage/ButtonSection"; -import useAuth from "@/hooks/useAuth"; +import ButtonSection from "@components/MyPage/ButtonSection"; +import useAuth from "@hooks/useAuth"; -const ProfileEditModal = () => { - const dialogRef = useRef(null); - const { isModalOpen, closeModal } = useModalStore(); - const { nickname } = useAuth(); +interface UseModalReturn { + dialogRef: React.RefObject; + isOpen: boolean; + openModal: () => void; + closeModal: () => void; +} - useModal({ isModalOpen, dialogRef }); +const ProfileEditModal = ({ modal: { dialogRef, isOpen, closeModal } }: { modal: UseModalReturn }) => { + const { nickname } = useAuth(); const handleMouseDown = (e: React.MouseEvent) => { if (e.target === dialogRef.current) { @@ -23,6 +23,8 @@ const ProfileEditModal = () => { closeModal(); }; + if (!isOpen) return null; + return ( { {}} + onChange={() => { }} minLength={2} />
diff --git a/frontend/src/pages/MyPage/view/index.tsx b/frontend/src/pages/MyPage/view/index.tsx index 0ef6815e..4ed1925c 100644 --- a/frontend/src/pages/MyPage/view/index.tsx +++ b/frontend/src/pages/MyPage/view/index.tsx @@ -3,18 +3,21 @@ import PageTitle from "@components/common/PageTitle"; import ProfileEditModal from "@/pages/MyPage/view/ProfileEditModal"; import Profile from "@/pages/MyPage/view/Profile"; import QuestionSection from "@/pages/MyPage/view/QuestionSection"; +import useModal from "@/hooks/useModal"; interface MyPageViewProps { nickname: string; } const MyPageView = ({ nickname }: MyPageViewProps) => { + const modal = useModal(); + return (
- - + +
From 7eb8dfde09b0f3168b68684b21a80507a309a645 Mon Sep 17 00:00:00 2001 From: yiseungyun Date: Thu, 28 Nov 2024 04:14:02 +0900 Subject: [PATCH 35/86] =?UTF-8?q?feat:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EC=97=90=EC=84=9C=20=EC=A7=88=EB=AC=B8?= =?UTF-8?q?=EC=A7=80=20=EC=82=AD=EC=A0=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 명세서 보고 작성하고, 백엔드 삭제 코드는 없어서 테스트 불가 --- frontend/src/api/questions/getQuestionList.ts | 16 ++++++++++ .../components/mypage/QuestionList/index.tsx | 9 +++--- .../questions/create/QuestionForm/index.tsx | 4 +-- .../src/hooks/api/useCreateQuestionList.ts | 4 ++- .../src/hooks/api/useDeleteQuestionList.ts | 30 +++++++++++++++++++ frontend/src/hooks/api/useGetQuestionList.ts | 23 ++++++++++++++ .../src/pages/MyPage/view/QuestionSection.tsx | 6 +++- 7 files changed, 84 insertions(+), 8 deletions(-) create mode 100644 frontend/src/api/questions/getQuestionList.ts create mode 100644 frontend/src/hooks/api/useDeleteQuestionList.ts create mode 100644 frontend/src/hooks/api/useGetQuestionList.ts diff --git a/frontend/src/api/questions/getQuestionList.ts b/frontend/src/api/questions/getQuestionList.ts new file mode 100644 index 00000000..f5d87969 --- /dev/null +++ b/frontend/src/api/questions/getQuestionList.ts @@ -0,0 +1,16 @@ +import axios from "axios"; + +interface QuestionListProps { + page: number; + limit: number; +} + +export const getQuestionList = async ({ page, limit }: QuestionListProps) => { + const response = await axios.get("/api/question-list", { + params: { + page, + limit, + }, + }); + return response.data; +}; diff --git a/frontend/src/components/mypage/QuestionList/index.tsx b/frontend/src/components/mypage/QuestionList/index.tsx index d736f159..e949fbcb 100644 --- a/frontend/src/components/mypage/QuestionList/index.tsx +++ b/frontend/src/components/mypage/QuestionList/index.tsx @@ -2,19 +2,20 @@ import QuestionItem from "./QuestionItem"; interface ListProps { tab: "myList" | "savedList"; + page: number; } -const QuestionList = ({ tab }: ListProps) => { +const QuestionList = ({ tab, page }: ListProps) => { return (
{tab === "myList" ? ( <> - - + + ) : ( <> - + )}
diff --git a/frontend/src/components/questions/create/QuestionForm/index.tsx b/frontend/src/components/questions/create/QuestionForm/index.tsx index 1a7b8c8c..66634aef 100644 --- a/frontend/src/components/questions/create/QuestionForm/index.tsx +++ b/frontend/src/components/questions/create/QuestionForm/index.tsx @@ -10,7 +10,7 @@ const QuestionForm = () => { const { category, questionTitle, access, questionList } = useQuestionFormStore(); - const mutation = useCreateQuestionList(); + const createQuestions = useCreateQuestionList(); const submitHandler = () => { const requestData = { @@ -20,7 +20,7 @@ const QuestionForm = () => { isPublic: access === "PUBLIC", }; - mutation.mutate(requestData); + createQuestions(requestData); }; return ( diff --git a/frontend/src/hooks/api/useCreateQuestionList.ts b/frontend/src/hooks/api/useCreateQuestionList.ts index 3b0a3653..a09c82b9 100644 --- a/frontend/src/hooks/api/useCreateQuestionList.ts +++ b/frontend/src/hooks/api/useCreateQuestionList.ts @@ -7,7 +7,7 @@ export const useCreateQuestionList = () => { const toast = useToast(); const navigate = useNavigate(); - return useMutation({ + const { mutate: createQuestions } = useMutation({ mutationFn: createQuestionList, onSuccess: (response) => { const questionListId = response.data.createdQuestionList.id; @@ -18,4 +18,6 @@ export const useCreateQuestionList = () => { toast.error("질문지 생성에 실패했습니다."); }, }); + + return createQuestions; }; diff --git a/frontend/src/hooks/api/useDeleteQuestionList.ts b/frontend/src/hooks/api/useDeleteQuestionList.ts new file mode 100644 index 00000000..2c17bafa --- /dev/null +++ b/frontend/src/hooks/api/useDeleteQuestionList.ts @@ -0,0 +1,30 @@ +import useToast from "../useToast"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { deleteQuestionList } from "@/api/questions/deleteQuestionList"; + +interface UseDeleteQuestionList { + page: Number; + limit: Number; +} + +export const useDeleteQuesitonList = ({ + page, + limit, +}: UseDeleteQuestionList) => { + const queryClient = useQueryClient(); + const toast = useToast(); + + const { mutate: deleteQuestions } = useMutation({ + mutationFn: deleteQuestionList, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: ["questions", page, limit], + }); + }, + onError: () => { + toast.error("질문지 삭제에 실패했습니다."); + }, + }); + + return deleteQuestions; +}; diff --git a/frontend/src/hooks/api/useGetQuestionList.ts b/frontend/src/hooks/api/useGetQuestionList.ts new file mode 100644 index 00000000..b10a2991 --- /dev/null +++ b/frontend/src/hooks/api/useGetQuestionList.ts @@ -0,0 +1,23 @@ +import { getQuestionList } from "@/api/questions/getQuestionList"; +import { useQuery } from "@tanstack/react-query"; + +interface UseGetQuestionListProps { + page: number; + limit: number; +} + +export const useCreateQuestionList = ({ + page, + limit, +}: UseGetQuestionListProps) => { + const { data, isLoading, error } = useQuery({ + queryKey: ["questions", page, limit], + queryFn: () => getQuestionList({ page, limit }), + }); + + return { + questions: data, + isLoading, + error, + }; +}; diff --git a/frontend/src/pages/MyPage/view/QuestionSection.tsx b/frontend/src/pages/MyPage/view/QuestionSection.tsx index 8afcf12e..16d95643 100644 --- a/frontend/src/pages/MyPage/view/QuestionSection.tsx +++ b/frontend/src/pages/MyPage/view/QuestionSection.tsx @@ -9,6 +9,9 @@ const QuestionSection = () => { const [tab, setTab] = useState("myList"); const [myListPage, setMyListPage] = useState(1); const [savedListPage, setSavedListPage] = useState(1); + const [currentPage, setCurrentPage] = useState( + tab === "myList" ? myListPage : savedListPage + ); const totalPages = { myList: 14, @@ -24,13 +27,14 @@ const QuestionSection = () => { } else { setSavedListPage(page); } + setCurrentPage(page); }, }); return (
- +
); From d1bd26ab7dfa1a008f0f6a90ed02ed5ec88f5ebf Mon Sep 17 00:00:00 2001 From: Chanwoo Kim Date: Thu, 28 Nov 2024 11:16:53 +0900 Subject: [PATCH 36/86] =?UTF-8?q?feat:=20=EC=8A=A4=ED=84=B0=EB=94=94=20?= =?UTF-8?q?=EC=84=B8=EC=85=98=20=EC=9E=85=EC=9E=A5=20API=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 질문지 내용도 받아오도록 스펙 변경 --- backend/src/room/dto/room.dto.ts | 1 + backend/src/room/room.entity.ts | 3 +++ backend/src/room/room.repository.ts | 3 +++ backend/src/room/services/room-join.service.ts | 14 ++++++++++++-- 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/backend/src/room/dto/room.dto.ts b/backend/src/room/dto/room.dto.ts index bed01d6c..a5718d73 100644 --- a/backend/src/room/dto/room.dto.ts +++ b/backend/src/room/dto/room.dto.ts @@ -11,6 +11,7 @@ export interface RoomDto { participants: number; maxParticipants: number; maxQuestionListLength: number; + questionListId: number; createdAt: number; connectionMap: Record; } diff --git a/backend/src/room/room.entity.ts b/backend/src/room/room.entity.ts index 0312c06c..f5b6aa8f 100644 --- a/backend/src/room/room.entity.ts +++ b/backend/src/room/room.entity.ts @@ -34,6 +34,9 @@ export class RoomEntity extends Entity { @Field({ type: "number" }) maxParticipants: number; + @Field({ type: "number" }) + questionListId: number; + @Field({ type: "number" }) maxQuestionListLength: number; diff --git a/backend/src/room/room.repository.ts b/backend/src/room/room.repository.ts index a0888377..3e97118c 100644 --- a/backend/src/room/room.repository.ts +++ b/backend/src/room/room.repository.ts @@ -22,6 +22,7 @@ export class RoomRepository { host: JSON.parse(room.host), maxParticipants: room.maxParticipants, maxQuestionListLength: room.maxQuestionListLength, + questionListId: room.questionListId, currentIndex: room.currentIndex, status: room.status, title: room.title, @@ -47,6 +48,7 @@ export class RoomRepository { createdAt: room.createdAt, currentIndex: room.currentIndex, maxQuestionListLength: room.maxQuestionListLength, + questionListId: room.questionListId, host: JSON.parse(room.host), participants: Object.keys(connectionMap).length, maxParticipants: room.maxParticipants, @@ -67,6 +69,7 @@ export class RoomRepository { room.connectionMap = JSON.stringify(dto.connectionMap); room.maxParticipants = dto.maxParticipants; room.maxQuestionListLength = dto.maxQuestionListLength; + room.questionListId = dto.questionListId; room.createdAt = Date.now(); room.host = JSON.stringify(dto.host); diff --git a/backend/src/room/services/room-join.service.ts b/backend/src/room/services/room-join.service.ts index 099498ae..635cd5d8 100644 --- a/backend/src/room/services/room-join.service.ts +++ b/backend/src/room/services/room-join.service.ts @@ -5,13 +5,15 @@ import { RoomRepository } from "@/room/room.repository"; import { RoomDto } from "@/room/dto/room.dto"; import { JoinRoomInternalDto } from "@/room/dto/join-room.dto"; import { WebsocketRepository } from "@/websocket/websocket.repository"; +import { QuestionListRepository } from "@/question-list/question-list.repository"; @Injectable() export class RoomJoinService { public constructor( private readonly roomRepository: RoomRepository, private readonly socketService: WebsocketService, - private readonly socketRepository: WebsocketRepository + private readonly socketRepository: WebsocketRepository, + private readonly questionListRepository: QuestionListRepository ) {} public async joinRoom(dto: JoinRoomInternalDto) { @@ -39,8 +41,16 @@ export class RoomJoinService { await this.roomRepository.setRoom(room); room.connectionMap[socketId] = undefined; + + const questionListContents = await this.questionListRepository.getContentsByQuestionListId( + room.questionListId + ); + // TODO: 성공 / 실패 여부를 전송하는데 있어서 결과에 따라 다르게 해야하는데.. 어떻게 관심 분리를 할까? - socket.emit(EMIT_EVENT.JOIN, room); + socket.emit(EMIT_EVENT.JOIN, { + ...room, + questionListContents, + }); } private isFullRoom(room: RoomDto): boolean { From 605aa2f3ba49174068b3be2db1065975766448aa Mon Sep 17 00:00:00 2001 From: Chanwoo Kim Date: Thu, 28 Nov 2024 11:23:25 +0900 Subject: [PATCH 37/86] =?UTF-8?q?feat:=20=EC=8A=A4=ED=84=B0=EB=94=94=20?= =?UTF-8?q?=EC=B0=B8=EA=B0=80=20=EC=84=B8=EC=85=98=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 진행중인 세션에 참가 시 `server:room__progess` 이벤트를 emit하도록 변경 --- backend/src/room/room.events.ts | 1 + backend/src/room/services/room-join.service.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/backend/src/room/room.events.ts b/backend/src/room/room.events.ts index 73bc2e96..fa0d1c2d 100644 --- a/backend/src/room/room.events.ts +++ b/backend/src/room/room.events.ts @@ -16,6 +16,7 @@ export const EMIT_EVENT = { QUIT: "server:room__quit", FULL: "server:room__full", JOIN: "server:room__join", + IN_PROGRESS: "server:room__progress", FINISH: "server:room__finish", CHANGE_HOST: "server:room__change_host", REACTION: "server:room__reaction", diff --git a/backend/src/room/services/room-join.service.ts b/backend/src/room/services/room-join.service.ts index 635cd5d8..51321894 100644 --- a/backend/src/room/services/room-join.service.ts +++ b/backend/src/room/services/room-join.service.ts @@ -26,6 +26,8 @@ export class RoomJoinService { if (!room) throw new Error("RoomEntity Not found"); + if (room.inProgress) return socket.emit(EMIT_EVENT.IN_PROGRESS, {}); + if (this.isFullRoom(room)) return socket.emit(EMIT_EVENT.FULL, {}); socket.join(roomId); From 1155a245d64feb6796ee37d2454e046715a47e6c Mon Sep 17 00:00:00 2001 From: Chanwoo Kim Date: Thu, 28 Nov 2024 14:45:56 +0900 Subject: [PATCH 38/86] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20/=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9C=20DTO=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/user/dto/update-user.dto.ts | 19 +++++++++++++++++++ backend/src/user/dto/user.dto.ts | 13 +++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 backend/src/user/dto/update-user.dto.ts diff --git a/backend/src/user/dto/update-user.dto.ts b/backend/src/user/dto/update-user.dto.ts new file mode 100644 index 00000000..b2c71f46 --- /dev/null +++ b/backend/src/user/dto/update-user.dto.ts @@ -0,0 +1,19 @@ +import { IsString, MaxLength } from "class-validator"; +import { User } from "@/user/user.entity"; + +export interface ChangePassword { + original: string; + newPassword: string; +} + +export class UpdateUserDto { + @IsString() + @MaxLength(User.USERNAME_MAX_LEN) + nickname?: string; + + @IsString() + @MaxLength(User.URL_MAX_LEN) + avatarUrl?: string; + + password?: ChangePassword; +} diff --git a/backend/src/user/dto/user.dto.ts b/backend/src/user/dto/user.dto.ts index 96447ffc..fd814e78 100644 --- a/backend/src/user/dto/user.dto.ts +++ b/backend/src/user/dto/user.dto.ts @@ -1,7 +1,20 @@ import { QuestionList } from "@/question-list/question-list.entity"; +import { LoginType } from "@/user/user.entity"; export interface UserDto { loginId?: string; + loginType: LoginType; + passwordHash?: string; + githubId?: number; + username: string; + refreshToken: string; + scrappedQuestionLists: QuestionList[]; +} + +export interface UserInternalDto { + id: number; + loginId?: string; + loginType: LoginType; passwordHash?: string; githubId?: number; username: string; From 6ffa91100f49d82a84aac3c411753deccc4740a2 Mon Sep 17 00:00:00 2001 From: Chanwoo Kim Date: Thu, 28 Nov 2024 14:46:11 +0900 Subject: [PATCH 39/86] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=ED=83=80=EC=9E=85=EA=B3=BC=20=EC=95=84?= =?UTF-8?q?=EB=B0=94=ED=83=80=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/user/user.entity.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/backend/src/user/user.entity.ts b/backend/src/user/user.entity.ts index a6d7e4c5..5b014fe6 100644 --- a/backend/src/user/user.entity.ts +++ b/backend/src/user/user.entity.ts @@ -1,11 +1,17 @@ -import { Entity, PrimaryGeneratedColumn, Column, OneToMany, ManyToMany, JoinTable } from "typeorm"; +import { Column, Entity, JoinTable, ManyToMany, OneToMany, PrimaryGeneratedColumn } from "typeorm"; import { QuestionList } from "@/question-list/question-list.entity"; +export enum LoginType { + LOCAL = "local", + GITHUB = "github", +} + @Entity() export class User { + public static readonly URL_MAX_LEN = 320; + public static readonly USERNAME_MAX_LEN = 20; private static LOGIN_ID_MAX_LEN = 20; private static PASSWORD_HASH_MAX_LEN = 256; - private static USERNAME_MAX_LEN = 20; private static REFRESH_TOKEN_MAX_LEN = 200; @PrimaryGeneratedColumn() @@ -43,4 +49,14 @@ export class User { }, }) scrappedQuestionLists: QuestionList[]; + + @Column({ + type: "enum", + enum: LoginType, + default: LoginType.LOCAL, + }) + loginType: LoginType; + + @Column({ length: User.URL_MAX_LEN, nullable: true }) + avatarUrl: string; } From 351fb3feac823d2c396c4eaf54b3245f3abb9a87 Mon Sep 17 00:00:00 2001 From: Chanwoo Kim Date: Thu, 28 Nov 2024 14:47:06 +0900 Subject: [PATCH 40/86] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EA=B8=B0=EB=8A=A5=EC=9D=84=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?=EB=B9=84=EC=A6=88=EB=8B=88=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 비밀번호 변경 - 유저 메타 데이터 업데이트 기능 - 로컬로 로그인할 수 있도록 하는 회원가입 기능 --- backend/src/user/user.service.ts | 74 ++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 backend/src/user/user.service.ts diff --git a/backend/src/user/user.service.ts b/backend/src/user/user.service.ts new file mode 100644 index 00000000..161405ca --- /dev/null +++ b/backend/src/user/user.service.ts @@ -0,0 +1,74 @@ +import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; +import { UserRepository } from "@/user/user.repository"; +import "dotenv/config"; +import { ChangePassword, UpdateUserDto } from "@/user/dto/update-user.dto"; +import { AuthService } from "@/auth/auth.service"; +import { Transactional } from "typeorm-transactional"; +import { LoginType, User } from "@/user/user.entity"; +import { CreateUserDto, CreateUserInternalDto } from "@/user/dto/create-user.dto"; + +@Injectable() +export class UserService { + constructor( + private readonly userRepository: UserRepository, + private readonly authService: AuthService + ) {} + + public async getChangeableUserInfo(userId: number) { + const user = await this.userRepository.getUserByUserId(userId); + if (!user) throw new NotFoundException("User not found"); + return { + userId: user.id, + nickname: user.username, + avatarUrl: user.avatarUrl, + loginType: user.loginType, + }; + } + + @Transactional() + public async updateUserInfo(userId: number, dto: UpdateUserDto) { + const user = await this.userRepository.getUserByUserId(userId); + if (!user) throw new BadRequestException("invalid user!"); + + const validPasswordDto = + dto.password && + dto.password.original && + dto.password.newPassword && + user.loginType === LoginType.LOCAL; + + user.avatarUrl = dto.avatarUrl || user.avatarUrl; + user.passwordHash = validPasswordDto + ? await this.changePassword(user, dto.password) + : user.passwordHash; + user.username = dto.nickname || user.username; + + await this.userRepository.updateUser(user); + return dto; + } + + @Transactional() + public async createUserByLocal(dto: CreateUserDto) { + const userDto: CreateUserInternalDto = { + loginId: dto.id, + loginType: LoginType.LOCAL, + username: dto.nickname, + passwordHash: this.authService.generatePasswordHash(dto.password), + }; + + await this.userRepository.createUser(userDto); + + return { + status: "success", + }; + } + + @Transactional() + private async changePassword(user: User, pair: ChangePassword) { + const originalHash = this.authService.generatePasswordHash(pair.original); + + if (originalHash !== user.passwordHash) + throw new BadRequestException("password does not match!"); + + return this.authService.generatePasswordHash(pair.newPassword); + } +} From 9af2120876bcf2c847d3f6d9480f5d1c95cb6a89 Mon Sep 17 00:00:00 2001 From: Chanwoo Kim Date: Thu, 28 Nov 2024 14:47:52 +0900 Subject: [PATCH 41/86] =?UTF-8?q?feat:=20=20=EC=9C=A0=EC=A0=80=20=EB=A0=88?= =?UTF-8?q?=ED=8F=AC=EC=A7=80=ED=86=A0=EB=A6=AC=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 아이디로 엔티티 검색 가능. - 추후 인덱싱해야함 --- backend/src/user/user.repository.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/backend/src/user/user.repository.ts b/backend/src/user/user.repository.ts index 88a56c6b..b514cfd0 100644 --- a/backend/src/user/user.repository.ts +++ b/backend/src/user/user.repository.ts @@ -1,7 +1,7 @@ import { Injectable } from "@nestjs/common"; import { User } from "./user.entity"; import { DataSource } from "typeorm"; -import { CreateUserDto } from "./dto/create-user.dto"; +import { CreateUserInternalDto } from "./dto/create-user.dto"; import { UserDto } from "./dto/user.dto"; @Injectable() @@ -24,7 +24,16 @@ export class UserRepository { .getOne(); } - createUser(createUserDto: CreateUserDto) { + // TODO: 로그인 ID로 인덱싱 생성 필요? + getUserByLoginId(username: string) { + return this.dataSource + .getRepository(User) + .createQueryBuilder("user") + .where("user.login_id = :id", { id: username }) + .getOne(); + } + + createUser(createUserDto: CreateUserInternalDto) { return this.dataSource.getRepository(User).save(createUserDto); } From 99879c33d47022e8e12752bade898349aa4e0f9a Mon Sep 17 00:00:00 2001 From: Chanwoo Kim Date: Thu, 28 Nov 2024 14:48:08 +0900 Subject: [PATCH 42/86] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20?= =?UTF-8?q?API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/user/user.controller.ts | 50 +++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 backend/src/user/user.controller.ts diff --git a/backend/src/user/user.controller.ts b/backend/src/user/user.controller.ts new file mode 100644 index 00000000..6cefa01d --- /dev/null +++ b/backend/src/user/user.controller.ts @@ -0,0 +1,50 @@ +import { + Body, + Controller, + Get, + Param, + Patch, + Post, + UseGuards, + UsePipes, + ValidationPipe, +} from "@nestjs/common"; +import { AuthGuard } from "@nestjs/passport"; +import { JwtPayload } from "@/auth/jwt/jwt.decorator"; +import { IJwtPayload } from "@/auth/jwt/jwt.model"; +import { UserRepository } from "@/user/user.repository"; +import { UpdateUserDto } from "@/user/dto/update-user.dto"; +import { UserService } from "@/user/user.service"; +import { CreateUserDto } from "@/user/dto/create-user.dto"; + +@Controller("user") +export class UserController { + constructor( + private readonly userRepository: UserRepository, + private readonly userService: UserService + ) {} + + @Post("signup") + @UsePipes(ValidationPipe) + async handleSignup(@Body() dto: CreateUserDto) { + return this.userService.createUserByLocal(dto); + } + + @Get("my") + @UseGuards(AuthGuard("jwt")) + async getMyInfo(@JwtPayload() payload: IJwtPayload) { + return this.userService.getChangeableUserInfo(payload.userId); + } + + @Get(":userId") + @UsePipes(ValidationPipe) + async getUserInfo(@Param("userId") userId: number) { + return this.userService.getChangeableUserInfo(userId); + } + + @Patch("my") + @UseGuards(AuthGuard("jwt")) + async changeMyInfo(@JwtPayload() payload: IJwtPayload, @Body() dto: UpdateUserDto) { + return this.userService.updateUserInfo(payload.userId, dto); + } +} From ff44e8554a37d0a38c969f5e47602134aaa7d553 Mon Sep 17 00:00:00 2001 From: Chanwoo Kim Date: Thu, 28 Nov 2024 14:48:50 +0900 Subject: [PATCH 43/86] =?UTF-8?q?fix:=20=EC=8B=9C=EA=B0=84=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 1초는 1000 밀리 세컨드 .. --- backend/src/utils/time.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/utils/time.ts b/backend/src/utils/time.ts index 27ed6fd1..6cedd5b7 100644 --- a/backend/src/utils/time.ts +++ b/backend/src/utils/time.ts @@ -1,4 +1,4 @@ -export const SEC = 1; +export const SEC = 1000; export const MIN = 60 * SEC; export const HOUR = 60 * MIN; export const DAY = 24 * HOUR; From c5322024f7168d497bffda4d156138992e60818e Mon Sep 17 00:00:00 2001 From: Chanwoo Kim Date: Thu, 28 Nov 2024 14:52:27 +0900 Subject: [PATCH 44/86] =?UTF-8?q?style:=20printWidth=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=BD=94=EB=93=9C=20=EC=8A=A4?= =?UTF-8?q?=ED=83=80=EC=9D=BC=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/auth/jwt/jwt.decorator.ts | 51 +++++++------------ .../jwt/strategy/refresh-token.strategy.ts | 10 +--- 2 files changed, 21 insertions(+), 40 deletions(-) diff --git a/backend/src/auth/jwt/jwt.decorator.ts b/backend/src/auth/jwt/jwt.decorator.ts index f96f461b..3087eab8 100644 --- a/backend/src/auth/jwt/jwt.decorator.ts +++ b/backend/src/auth/jwt/jwt.decorator.ts @@ -6,55 +6,42 @@ import { } from "@nestjs/common"; import { IJwtPayload as IJwtPayload, - IJwtTokenPair as IJwtTokenPair, IJwtToken as IJwtToken, + IJwtTokenPair as IJwtTokenPair, } from "./jwt.model"; -export const JwtPayload = createParamDecorator( - (data: unknown, ctx: ExecutionContext) => { - const request = ctx.switchToHttp().getRequest(); - const payload = request.user.jwtToken; +export const JwtPayload = createParamDecorator((data: unknown, ctx: ExecutionContext) => { + const request = ctx.switchToHttp().getRequest(); + const payload = request.user.jwtToken; - if (!isJwtTokenPayload(payload)) { - throw new UnauthorizedException("Invalid jwt token payload"); - } - - return payload; + if (!isJwtTokenPayload(payload)) { + throw new UnauthorizedException("Invalid jwt token payload"); } -); -export const JwtTokenPair = createParamDecorator( - (data: unknown, ctx: ExecutionContext) => { - const request = ctx.switchToHttp().getRequest(); - const payload = request.user.jwtToken; + return payload; +}); - if (!isJwtTokenPair(payload)) { - throw new InternalServerErrorException("Invalid jwt token"); - } +export const JwtTokenPair = createParamDecorator((data: unknown, ctx: ExecutionContext) => { + const request = ctx.switchToHttp().getRequest(); + const payload = request.user.jwtToken; - return payload; + if (!isJwtTokenPair(payload)) { + throw new InternalServerErrorException("Invalid jwt token"); } -); + + return payload; +}); function isJwtTokenPayload(payload: any): payload is IJwtPayload { - return ( - payload && - typeof payload.userId === "number" && - typeof payload.username === "string" - ); + return payload && typeof payload.userId === "number" && typeof payload.username === "string"; } function isJwtTokenPair(payload: any): payload is IJwtTokenPair { if (!payload.accessToken || !payload.refreshToken) return false; if (!isJwtToken(payload.accessToken)) return false; - if (!isJwtToken(payload.refreshToken)) return false; - return true; + return isJwtToken(payload.refreshToken); } function isJwtToken(token: any): token is IJwtToken { - return ( - token && - typeof token.token === "string" && - typeof token.expireTime === "number" - ); + return token && typeof token.token === "string" && typeof token.expireTime === "number"; } diff --git a/backend/src/auth/jwt/strategy/refresh-token.strategy.ts b/backend/src/auth/jwt/strategy/refresh-token.strategy.ts index 04dafdf6..0d26e8f1 100644 --- a/backend/src/auth/jwt/strategy/refresh-token.strategy.ts +++ b/backend/src/auth/jwt/strategy/refresh-token.strategy.ts @@ -6,10 +6,7 @@ import "dotenv/config"; import { JwtService } from "../jwt.service"; @Injectable() -export class RefreshTokenStrategy extends PassportStrategy( - Strategy, - "jwt-refresh" -) { +export class RefreshTokenStrategy extends PassportStrategy(Strategy, "jwt-refresh") { constructor(private readonly jwtService: JwtService) { super({ jwtFromRequest: (req: Request) => { @@ -26,10 +23,7 @@ export class RefreshTokenStrategy extends PassportStrategy( const { aud, exp } = payload; const refreshToken = req.cookies["refreshToken"]; - const accessToken = await this.jwtService.getNewAccessToken( - parseInt(aud), - refreshToken - ); + const accessToken = await this.jwtService.getNewAccessToken(parseInt(aud), refreshToken); return { jwtToken: { From b85dc4508565cf2010582ee4dc049b4cc3865968 Mon Sep 17 00:00:00 2001 From: Chanwoo Kim Date: Thu, 28 Nov 2024 14:53:52 +0900 Subject: [PATCH 45/86] =?UTF-8?q?fix:=20jwt=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EB=A7=8C=EB=A3=8C=EC=8B=9C=EA=B0=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 현재 시간을 더하는게 아니라, 만료 전체 시간을 더해야했음 - jwt service 설정에 만료시간 설정이 없어서 삭제 --- backend/src/auth/jwt/jwt.service.ts | 48 +++++++++++++---------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/backend/src/auth/jwt/jwt.service.ts b/backend/src/auth/jwt/jwt.service.ts index 29784fba..e2751892 100644 --- a/backend/src/auth/jwt/jwt.service.ts +++ b/backend/src/auth/jwt/jwt.service.ts @@ -3,8 +3,8 @@ import { Injectable, UnauthorizedException } from "@nestjs/common"; import { Transactional } from "typeorm-transactional"; import { UserRepository } from "../../user/user.repository"; import { DAY, HOUR } from "../../utils/time"; -import { User } from "../../user/user.entity"; -import { IJwtToken } from "./jwt.model"; +import { IJwtToken, IJwtTokenPair } from "./jwt.model"; +import { UserInternalDto } from "@/user/dto/user.dto"; @Injectable() export class JwtService { @@ -17,15 +17,12 @@ export class JwtService { ) {} @Transactional() - public async createJwtToken(id: number) { - const user = await this.userRepository.getUserByUserId(id); - - const accessToken = await this.createAccessToken(user); - const refreshToken = await this.createRefreshToken(id); - - user.refreshToken = refreshToken.token; + public async createJwtToken(userData: UserInternalDto): Promise { + const accessToken = await this.createAccessToken(userData); + const refreshToken = await this.createRefreshToken(userData.id); - await this.userRepository.updateUser(user); + userData.refreshToken = refreshToken.token; + await this.userRepository.updateUser(userData); return { accessToken, @@ -33,47 +30,46 @@ export class JwtService { }; } - public async createAccessToken(user: User): Promise { + public async getNewAccessToken(id: number, refreshToken: string) { + const user = await this.userRepository.getUserByUserId(id); + + if (!user) throw new UnauthorizedException("Invalid refresh token"); + if (user.refreshToken !== refreshToken) + throw new UnauthorizedException("Refresh token expired!"); + + return this.createAccessToken(user); + } + + private async createAccessToken(user: UserInternalDto): Promise { const accessToken = this.parent.sign( { userId: user.id, username: user.username, + loginType: user.loginType, }, { secret: process.env.JWT_ACCESS_TOKEN_SECRET_KEY, - expiresIn: process.env.JWT_ACCESS_TOKEN_EXPIRATION_TIME, } ); return { token: accessToken, - expireTime: Date.now() + JwtService.ACCESS_TOKEN_TIME, + expireTime: JwtService.ACCESS_TOKEN_TIME, }; } - public async createRefreshToken(id: number): Promise { + private async createRefreshToken(id: number): Promise { const refreshToken = this.parent.sign( {}, { secret: process.env.JWT_REFRESH_TOKEN_SECRET_KEY, - expiresIn: process.env.JWT_REFRESH_TOKEN_EXPIRATION_TIME, audience: String(id), } ); return { token: refreshToken, - expireTime: Date.now() + JwtService.REFRESH_TOKEN_TIME, + expireTime: JwtService.REFRESH_TOKEN_TIME, }; } - - public async getNewAccessToken(id: number, refreshToken: string) { - const user = await this.userRepository.getUserByUserId(id); - - if (!user) throw new UnauthorizedException("Invalid refresh token"); - if (user.refreshToken !== refreshToken) - throw new UnauthorizedException("Refresh token expired!"); - - return this.createAccessToken(user); - } } From c961ea82b6f86ba4996410a48a2b32bb977e1e71 Mon Sep 17 00:00:00 2001 From: Chanwoo Kim Date: Thu, 28 Nov 2024 14:55:51 +0900 Subject: [PATCH 46/86] =?UTF-8?q?feat:=20=EA=B9=83=ED=97=88=EB=B8=8C=20?= =?UTF-8?q?=EC=A0=84=EB=9E=B5=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `인증`까지만 범위 설정을 어떻게 할지 고민을 많이 했음 - 인증에 성공했다면, 토큰을 받아오도록 통일해야하지 않을까? 라는 생각을 하게 됨 - 그렇게 전략 패턴별로 토큰을 반환하도록 통일함 - GithubProfile 이라는 정보도 나중에 삭제할 예정 --- backend/src/auth/github/github.strategy.ts | 42 ++++++++++++++-------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/backend/src/auth/github/github.strategy.ts b/backend/src/auth/github/github.strategy.ts index 87791b24..06973fcb 100644 --- a/backend/src/auth/github/github.strategy.ts +++ b/backend/src/auth/github/github.strategy.ts @@ -5,47 +5,59 @@ import { Strategy } from "passport-custom"; import { Request } from "express"; import axios from "axios"; import "dotenv/config"; +import { AuthService } from "@/auth/auth.service"; +import { JwtService } from "@/auth/jwt/jwt.service"; @Injectable() export class GithubStrategy extends PassportStrategy(Strategy, "github") { - private static REQUEST_ACCESS_TOKEN_URL = - "https://github.com/login/oauth/access_token"; + private static REQUEST_ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token"; private static REQUEST_USER_URL = "https://api.github.com/user"; - constructor() { + constructor( + private readonly authService: AuthService, + private readonly jwtService: JwtService + ) { super(); } - async validate(req: Request, done: any) { + async validate(req: Request) { const { code } = req.body; if (!code) { throw new UnauthorizedException("Authorization code not found"); } - const tokenResponse = await axios.post( + const { access_token: accessToken } = (await this.fetchAccessToken(code)).data; + + const profile = (await this.fetchGithubUser(accessToken)).data; + + const user = await this.authService.getUserByGithubId(profile.id); + const token = await this.jwtService.createJwtToken(user); + + return { + jwtToken: token, + }; + } + + private async fetchAccessToken(code: string) { + return axios.post( GithubStrategy.REQUEST_ACCESS_TOKEN_URL, { client_id: process.env.OAUTH_GITHUB_ID, client_secret: process.env.OAUTH_GITHUB_SECRET, - code: code, + code, }, { headers: { Accept: "application/json" }, } ); + } - const { access_token } = tokenResponse.data; - - // GitHub 사용자 정보 조회 - const userResponse = await axios.get(GithubStrategy.REQUEST_USER_URL, { + private async fetchGithubUser(accessToken: string) { + return axios.get(GithubStrategy.REQUEST_USER_URL, { headers: { - Authorization: `Bearer ${access_token}`, + Authorization: `Bearer ${accessToken}`, }, }); - - return done(null, { - profile: userResponse.data, - }); } } From d2ca9032efb472a4d5153b97c06178d43e182771 Mon Sep 17 00:00:00 2001 From: twalla26 Date: Thu, 28 Nov 2024 14:55:59 +0900 Subject: [PATCH 47/86] =?UTF-8?q?feat:=20=EC=A7=88=EB=AC=B8=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../question-list/dto/delete-question.dto.ts | 5 +++ .../question-list/question-list.controller.ts | 39 ++++++++++++++++++- .../question-list/question-list.repository.ts | 4 ++ .../question-list/question-list.service.ts | 30 +++++++++++++- 4 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 backend/src/question-list/dto/delete-question.dto.ts diff --git a/backend/src/question-list/dto/delete-question.dto.ts b/backend/src/question-list/dto/delete-question.dto.ts new file mode 100644 index 00000000..f4b0e9cf --- /dev/null +++ b/backend/src/question-list/dto/delete-question.dto.ts @@ -0,0 +1,5 @@ +export interface DeleteQuestionDto { + id: number; + questionListId: number; + userId: number; +} diff --git a/backend/src/question-list/question-list.controller.ts b/backend/src/question-list/question-list.controller.ts index 798cb236..b367c838 100644 --- a/backend/src/question-list/question-list.controller.ts +++ b/backend/src/question-list/question-list.controller.ts @@ -20,6 +20,7 @@ import { IJwtPayload } from "@/auth/jwt/jwt.model"; import { MyQuestionListDto } from "./dto/my-question-list.dto"; import { UpdateQuestionListDto } from "@/question-list/dto/update-question-list.dto"; import { QuestionDto } from "@/question-list/dto/question.dto"; +import { DeleteQuestionDto } from "@/question-list/dto/delete-question.dto"; @Controller("question-list") export class QuestionListController { @@ -283,7 +284,7 @@ export class QuestionListController { @Res() res, @JwtPayload() token: IJwtPayload, @Body() body: { content: string }, - @Param() params: { questionListId: number, questionId: number } + @Param() params: { questionListId: number; questionId: number } ) { try { const userId = token.userId; @@ -314,6 +315,42 @@ export class QuestionListController { } } + @Delete("/:questionListId/question/:questionId") + @UseGuards(AuthGuard("jwt")) + async deleteQuestion( + @Res() res, + @JwtPayload() token: IJwtPayload, + @Param() params: { questionListId: number; questionId: number } + ) { + try { + const userId = token.userId; + const { questionListId, questionId } = params; + const deleteQuestionDto: DeleteQuestionDto = { + id: questionId, + questionListId, + userId, + }; + const result = await this.questionListService.deleteQuestion(deleteQuestionDto); + if (result.affected) { + return res.send({ + success: true, + message: "Question is deleted successfully.", + }); + } else { + return res.send({ + success: true, + message: "Failed to delete question.", + }); + } + } catch (error) { + return res.send({ + success: false, + message: "Failed to delete question.", + error: error.message, + }); + } + } + @Get("scrap") @UseGuards(AuthGuard("jwt")) async getScrappedQuestionLists(@Res() res, @JwtPayload() token: IJwtPayload) { diff --git a/backend/src/question-list/question-list.repository.ts b/backend/src/question-list/question-list.repository.ts index c0e34575..c074312d 100644 --- a/backend/src/question-list/question-list.repository.ts +++ b/backend/src/question-list/question-list.repository.ts @@ -114,6 +114,10 @@ export class QuestionListRepository { }); } + deleteQuestion(question: Question) { + return this.dataSource.getRepository(Question).delete(question); + } + scrapQuestionList(questionListId: number, userId: number) { return this.dataSource .createQueryBuilder() diff --git a/backend/src/question-list/question-list.service.ts b/backend/src/question-list/question-list.service.ts index 83a9e668..21eea2a8 100644 --- a/backend/src/question-list/question-list.service.ts +++ b/backend/src/question-list/question-list.service.ts @@ -10,6 +10,7 @@ import { Transactional } from "typeorm-transactional"; import { QuestionList } from "@/question-list/question-list.entity"; import { UpdateQuestionListDto } from "@/question-list/dto/update-question-list.dto"; import { QuestionDto } from "@/question-list/dto/question.dto"; +import { DeleteQuestionDto } from "@/question-list/dto/delete-question.dto"; @Injectable() export class QuestionListService { @@ -211,7 +212,7 @@ export class QuestionListService { const questionList = await this.questionListRepository.getQuestionListById(questionListId); if (!questionList) throw new Error("Question list not found."); if (questionList.userId !== userId) - throw new Error("You do not have permission to delete this question list."); + throw new Error("You do not have permission to add a question to this question list."); const existingQuestionsCount = await this.questionListRepository.getQuestionCountByQuestionListId(questionListId); @@ -226,13 +227,19 @@ export class QuestionListService { async updateQuestion(questionDto: QuestionDto) { const { id, content, questionListId, userId } = questionDto; + + const question = await this.questionListRepository.getQuestionById(id); + if (!question) throw new Error("Question not found."); + const user = await this.userRepository.getUserByUserId(userId); if (!user) throw new Error("User not found."); const questionList = await this.questionListRepository.getQuestionListById(questionListId); if (!questionList) throw new Error("Question list not found."); if (questionList.userId !== userId) - throw new Error("You do not have permission to delete this question list."); + throw new Error( + "You do not have permission to update the question in this question list." + ); const existingQuestion = await this.questionListRepository.getQuestionById(id); existingQuestion.content = content; @@ -241,6 +248,25 @@ export class QuestionListService { return await this.getQuestionListContents(questionListId); } + async deleteQuestion(deleteQuestionDto: DeleteQuestionDto) { + const { id, questionListId, userId } = deleteQuestionDto; + + const question = await this.questionListRepository.getQuestionById(id); + if (!question) throw new Error("Question not found."); + + const user = await this.userRepository.getUserByUserId(userId); + if (!user) throw new Error("User not found."); + + const questionList = await this.questionListRepository.getQuestionListById(questionListId); + if (!questionList) throw new Error("Question list not found."); + if (questionList.userId !== userId) + throw new Error( + "You do not have permission to delete the question in this question list." + ); + + return await this.questionListRepository.deleteQuestion(question); + } + async getScrappedQuestionLists(userId: number) { const user = await this.userRepository.getUserByUserId(userId); return await this.questionListRepository.getScrappedQuestionListsByUser(user); From ecb71563197fe6cc8064bbb9278899fcf3a58602 Mon Sep 17 00:00:00 2001 From: Chanwoo Kim Date: Thu, 28 Nov 2024 14:56:13 +0900 Subject: [PATCH 48/86] =?UTF-8?q?refactor:=20controller=20-=20strategy=20?= =?UTF-8?q?=EC=97=AD=ED=95=A0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/auth/auth.controller.ts | 57 ++++++++++++++--------------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/backend/src/auth/auth.controller.ts b/backend/src/auth/auth.controller.ts index fe56476c..2bbaa77e 100644 --- a/backend/src/auth/auth.controller.ts +++ b/backend/src/auth/auth.controller.ts @@ -1,12 +1,10 @@ -import { Controller, Get, Post, Res, Req, UseGuards } from "@nestjs/common"; +import { Controller, Get, Post, Res, UseGuards } from "@nestjs/common"; import { AuthGuard } from "@nestjs/passport"; -import { Request, Response } from "express"; +import { Response } from "express"; import { AuthService } from "./auth.service"; -import { GithubProfile } from "./github/gitub-profile.decorator"; -import { Profile } from "passport-github"; -import { setCookieConfig } from "../config/cookie.config"; +import { setCookieConfig } from "@/config/cookie.config"; import { JwtPayload, JwtTokenPair } from "./jwt/jwt.decorator"; -import { IJwtPayload, IJwtTokenPair } from "./jwt/jwt.model"; +import { IJwtPayload, IJwtToken, IJwtTokenPair } from "./jwt/jwt.model"; @Controller("auth") export class AuthController { @@ -18,46 +16,45 @@ export class AuthController { @Post("github") @UseGuards(AuthGuard("github")) async githubCallback( - @Req() req: Request, @Res({ passthrough: true }) res: Response, - @GithubProfile() profile: Profile + @JwtTokenPair() pair: IJwtTokenPair ) { - const id = parseInt(profile.id); - - const result = await this.authService.getTokenByGithubId(id); - - res.cookie("accessToken", result.accessToken.token, { - maxAge: result.accessToken.expireTime, - ...setCookieConfig, - }); - - res.cookie("refreshToken", result.refreshToken.token, { - maxAge: result.refreshToken.expireTime, - ...setCookieConfig, - }); - - return { - success: true, - }; + return this.setCookie(res, pair.accessToken, pair.refreshToken); } @Get("whoami") @UseGuards(AuthGuard("jwt")) - async handleWhoami(@Req() req: Request, @JwtPayload() token: IJwtPayload) { - return token; + async handleWhoami(@JwtPayload() payload: IJwtPayload) { + return payload; } @Get("refresh") @UseGuards(AuthGuard("jwt-refresh")) async handleRefresh( @Res({ passthrough: true }) res: Response, - @JwtTokenPair() token: IJwtTokenPair + @JwtTokenPair() pair: IJwtTokenPair ) { - res.cookie("accessToken", token.accessToken.token, { - maxAge: token.accessToken.expireTime, + return this.setCookie(res, pair.accessToken); + } + + @Post("login") + @UseGuards(AuthGuard("local")) + async login(@Res({ passthrough: true }) res: Response, @JwtTokenPair() pair: IJwtTokenPair) { + return this.setCookie(res, pair.accessToken, pair.refreshToken); + } + + private setCookie(res: Response, accessToken: IJwtToken, refreshToken?: IJwtToken) { + res.cookie("accessToken", accessToken.token, { + maxAge: accessToken.expireTime, ...setCookieConfig, }); + if (refreshToken) + res.cookie("refreshToken", refreshToken.token, { + maxAge: refreshToken.expireTime, + ...setCookieConfig, + }); + return { success: true, }; From 7b5af526d0fbd77a79e987e7122e33e3a52439d0 Mon Sep 17 00:00:00 2001 From: Chanwoo Kim Date: Thu, 28 Nov 2024 14:56:51 +0900 Subject: [PATCH 49/86] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=EC=9D=84=20=EC=9C=84=ED=95=9C=20DTO=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 회원가입을 위한 DTO 추가 - 로그인을 위한 DTO 추가 --- backend/src/auth/dto/login.dto.ts | 12 ++++++++++++ backend/src/user/dto/create-user.dto.ts | 17 ++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 backend/src/auth/dto/login.dto.ts diff --git a/backend/src/auth/dto/login.dto.ts b/backend/src/auth/dto/login.dto.ts new file mode 100644 index 00000000..1e0ba892 --- /dev/null +++ b/backend/src/auth/dto/login.dto.ts @@ -0,0 +1,12 @@ +import { IsNotEmpty, IsString } from "class-validator"; + +// TODO: 프론트에서 정의된 제약사항 이곳에서도 검증하도록 보완하기 +export class LoginDto { + @IsString() + @IsNotEmpty() + userId: string; + + @IsString() + @IsNotEmpty() + password: string; +} diff --git a/backend/src/user/dto/create-user.dto.ts b/backend/src/user/dto/create-user.dto.ts index 61c14de2..08121a46 100644 --- a/backend/src/user/dto/create-user.dto.ts +++ b/backend/src/user/dto/create-user.dto.ts @@ -1,6 +1,21 @@ -export interface CreateUserDto { +import { LoginType } from "@/user/user.entity"; +import { IsNotEmpty } from "class-validator"; + +export class CreateUserDto { + @IsNotEmpty() + id: string; + + @IsNotEmpty() + password: string; + + @IsNotEmpty() + nickname: string; +} + +export interface CreateUserInternalDto { loginId?: string; passwordHash?: string; githubId?: number; username: string; + loginType: LoginType; } From 6f934e5f8ed58f414bbe8f735ed9da83bc37c393 Mon Sep 17 00:00:00 2001 From: Chanwoo Kim Date: Thu, 28 Nov 2024 14:57:04 +0900 Subject: [PATCH 50/86] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=EC=9D=84=20=EC=9C=84=ED=95=9C=20passport=20?= =?UTF-8?q?=EC=A0=84=EB=9E=B5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/auth/local/local.strategy.ts | 29 ++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 backend/src/auth/local/local.strategy.ts diff --git a/backend/src/auth/local/local.strategy.ts b/backend/src/auth/local/local.strategy.ts new file mode 100644 index 00000000..b85ea608 --- /dev/null +++ b/backend/src/auth/local/local.strategy.ts @@ -0,0 +1,29 @@ +import { Strategy } from "passport-local"; +import { PassportStrategy } from "@nestjs/passport"; +import { Injectable, UnauthorizedException } from "@nestjs/common"; +import { JwtService } from "@/auth/jwt/jwt.service"; +import { AuthService } from "@/auth/auth.service"; + +@Injectable() +export class LocalStrategy extends PassportStrategy(Strategy) { + constructor( + private readonly authService: AuthService, + private readonly jwtService: JwtService + ) { + super({ + usernameField: "userId", + passwordField: "password", + }); + } + + async validate(username: string, password: string): Promise { + const user = await this.authService.getUserByLocal(username, password); + + if (!user) throw new UnauthorizedException("Invalid User login!"); + const token = await this.jwtService.createJwtToken(user); + + return { + jwtToken: token, + }; + } +} From e9fdd8ade97754361d8fd4c5b5e72bf1267ec32d Mon Sep 17 00:00:00 2001 From: Chanwoo Kim Date: Thu, 28 Nov 2024 14:57:32 +0900 Subject: [PATCH 51/86] =?UTF-8?q?fix:=20=EC=BF=A0=ED=82=A4=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=8B=9C=20secure=20=EA=B0=92=EC=9D=84=20productio?= =?UTF-8?q?n=20=EC=9D=BC=EB=95=8C=EB=A7=8C=20=EB=90=98=EB=8F=84=EB=A1=9D?= =?UTF-8?q?=20=ED=97=88=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 로컬 로그인 시 http 에서도 테스트할 수 있도록 허용 --- backend/src/config/cookie.config.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/src/config/cookie.config.ts b/backend/src/config/cookie.config.ts index b6fab013..1d02ed80 100644 --- a/backend/src/config/cookie.config.ts +++ b/backend/src/config/cookie.config.ts @@ -1,4 +1,6 @@ +import "dotenv/config"; + export const setCookieConfig = { httpOnly: true, - secure: true, + secure: process.env.NODE_ENV === "production", }; From d90e4142274436f165cf43cdb34cad0a662627d6 Mon Sep 17 00:00:00 2001 From: Chanwoo Kim Date: Thu, 28 Nov 2024 14:58:09 +0900 Subject: [PATCH 52/86] =?UTF-8?q?feat:=20=EB=A1=9C=EC=BB=AC=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=A0=84=EB=9E=B5=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/auth/auth.service.ts | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/backend/src/auth/auth.service.ts b/backend/src/auth/auth.service.ts index 1620988a..b0ea4d9f 100644 --- a/backend/src/auth/auth.service.ts +++ b/backend/src/auth/auth.service.ts @@ -1,14 +1,14 @@ -import { Injectable, UnauthorizedException } from "@nestjs/common"; -import { UserRepository } from "../user/user.repository"; +import { Injectable } from "@nestjs/common"; +import { UserRepository } from "@/user/user.repository"; import { Transactional } from "typeorm-transactional"; import "dotenv/config"; -import { DAY, HOUR } from "../utils/time"; import { JwtService } from "./jwt/jwt.service"; +import { LoginType } from "@/user/user.entity"; +import { createHash } from "node:crypto"; @Injectable() export class AuthService { - private static ACCESS_TOKEN_EXPIRATION_TIME = 3 * HOUR; - private static ACCESS_TOKEN_EXPIRATION = 30 * DAY; + private static PASSWORD_SALT = process.env.PASSWORD_SALT; constructor( private readonly userRepository: UserRepository, @@ -16,16 +16,30 @@ export class AuthService { ) {} @Transactional() - public async getTokenByGithubId(id: number) { + public async getUserByGithubId(id: number) { let user = await this.userRepository.getUserByGithubId(id); - if (!user) { + if (!user) user = await this.userRepository.createUser({ githubId: id, username: `camper_${id}`, + loginType: LoginType.GITHUB, }); - } - return await this.jwtService.createJwtToken(user.id); + return user; + } + + public async getUserByLocal(username: string, password: string) { + const user = await this.userRepository.getUserByLoginId(username); + if (!user) return null; + if (user.passwordHash !== this.generatePasswordHash(password)) return null; + + return user; + } + + public generatePasswordHash(password: string) { + return createHash("sha256") + .update(password + process.env.SESSION_HASH) + .digest("hex"); } } From 47007e01a9f8b739cf451987207ad87d9a177e7e Mon Sep 17 00:00:00 2001 From: Chanwoo Kim Date: Thu, 28 Nov 2024 14:58:20 +0900 Subject: [PATCH 53/86] =?UTF-8?q?feat:=20=EB=B0=94=EB=80=90=20=EB=AA=85?= =?UTF-8?q?=EC=84=B8=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/auth/auth.module.ts | 6 ++++-- backend/src/user/user.module.ts | 8 +++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/backend/src/auth/auth.module.ts b/backend/src/auth/auth.module.ts index 83ac1574..3dd3eaff 100644 --- a/backend/src/auth/auth.module.ts +++ b/backend/src/auth/auth.module.ts @@ -2,12 +2,14 @@ import { Module } from "@nestjs/common"; import { AuthController } from "./auth.controller"; import { AuthService } from "./auth.service"; import { GithubStrategy } from "./github/github.strategy"; -import { UserRepository } from "../user/user.repository"; +import { UserRepository } from "@/user/user.repository"; import { JwtModule } from "./jwt/jwt.module"; +import { LocalStrategy } from "@/auth/local/local.strategy"; +import { UserService } from "@/user/user.service"; @Module({ imports: [JwtModule], controllers: [AuthController], - providers: [AuthService, GithubStrategy, UserRepository], + providers: [AuthService, GithubStrategy, LocalStrategy, UserRepository, UserService], }) export class AuthModule {} diff --git a/backend/src/user/user.module.ts b/backend/src/user/user.module.ts index 196852ba..34663e6c 100644 --- a/backend/src/user/user.module.ts +++ b/backend/src/user/user.module.ts @@ -1,7 +1,13 @@ import { Module } from "@nestjs/common"; import { UserRepository } from "./user.repository"; +import { UserService } from "@/user/user.service"; +import { AuthService } from "@/auth/auth.service"; +import { JwtModule } from "@/auth/jwt/jwt.module"; +import { UserController } from "@/user/user.controller"; @Module({ - providers: [UserRepository], + imports: [JwtModule], + controllers: [UserController], + providers: [UserRepository, UserService, AuthService], }) export class UserModule {} From b30d5f7b6f56a8a6ef8a4cbf02c381488e5e5c7a Mon Sep 17 00:00:00 2001 From: yiseungyun Date: Thu, 28 Nov 2024 14:58:28 +0900 Subject: [PATCH 54/86] =?UTF-8?q?fix:=20=EB=B0=B1=EC=97=94=EB=93=9C=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=EC=97=90=20=EB=A7=9E?= =?UTF-8?q?=EC=B6=B0=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .ts 확장자 문제 발생으로 해당 파일들 확장자 제거 - 백엔드 리팩토링에 맞춰 이벤트명 수정 - 서버에 요청하는 리액션 부분 타입 맞게 변경 --- .../src/components/common/Modal/index.tsx | 2 +- .../QuestionList/QuestionItem/index.tsx | 4 +- .../src/hooks/__test__/useSession.test.ts | 60 +++++++++++-------- .../src/hooks/session/usePeerConnection.ts | 18 ------ frontend/src/hooks/session/useReaction.ts | 2 +- frontend/src/hooks/session/useSession.ts | 4 +- frontend/src/hooks/session/useSocketEvents.ts | 4 +- frontend/src/hooks/useAuth.ts | 2 +- frontend/src/hooks/useModal.ts | 6 +- frontend/src/pages/MyPage/view/Profile.tsx | 7 +-- .../pages/MyPage/view/ProfileEditModal.tsx | 8 ++- 11 files changed, 55 insertions(+), 62 deletions(-) diff --git a/frontend/src/components/common/Modal/index.tsx b/frontend/src/components/common/Modal/index.tsx index 1b412f63..011fc352 100644 --- a/frontend/src/components/common/Modal/index.tsx +++ b/frontend/src/components/common/Modal/index.tsx @@ -25,7 +25,7 @@ const Modal = ({ leftButton, rightButton, type, - onLeftClick = () => { }, + onLeftClick = () => {}, onRightClick, }: ModalProps) => { const handleButtonClick = (callback: () => void) => () => { diff --git a/frontend/src/components/mypage/QuestionList/QuestionItem/index.tsx b/frontend/src/components/mypage/QuestionList/QuestionItem/index.tsx index 2efa10a7..329c21cd 100644 --- a/frontend/src/components/mypage/QuestionList/QuestionItem/index.tsx +++ b/frontend/src/components/mypage/QuestionList/QuestionItem/index.tsx @@ -23,7 +23,7 @@ const QuestionItem = ({ questionListId, type, page }: ItemProps) => { const openDeleteModal = (e: React.MouseEvent) => { e.stopPropagation(); modal.openModal(); - } + }; const deleteHandler = () => { deleteQuestions(questionListId); @@ -38,7 +38,7 @@ const QuestionItem = ({ questionListId, type, page }: ItemProps) => { leftButton="취소하기" rightButton="삭제하기" type="red" - onLeftClick={() => { }} + onLeftClick={() => {}} onRightClick={deleteHandler} />
{ expect(mockGetMedia).toHaveBeenCalled(); // 4. 소켓 이벤트 발생 확인 - expect(mockSocket.emit).toHaveBeenCalledWith("join_room", { + expect(mockSocket.emit).toHaveBeenCalledWith(SESSION_EMIT_EVENT.JOIN, { roomId: "test-session", nickname: "test-user", }); @@ -193,13 +197,13 @@ describe("useSession Hook 테스트", () => { result.current.emitReaction("👍"); }); - expect(mockSocket.emit).toHaveBeenCalledWith("reaction", { + expect(mockSocket.emit).toHaveBeenCalledWith(SESSION_EMIT_EVENT.REACTION, { roomId: "test-session", - reaction: "👍", + reactionType: "👍", }); act(() => { - jest.advanceTimersByTime(3000); + jest.advanceTimersByTime(REACTION_DURATION); }); expect(result.current.reaction).toBe(""); }); @@ -213,15 +217,15 @@ describe("useSession Hook 테스트", () => { it("모든 소켓 이벤트 리스너 등록", () => { const expectedEvents = [ - "all_users", - "getOffer", - "getAnswer", - "getCandidate", - "user_exit", - "room_full", - "master_changed", - "room_finished", - "reaction", + SIGNAL_LISTEN_EVENT.OFFER, + SIGNAL_LISTEN_EVENT.ANSWER, + SIGNAL_LISTEN_EVENT.CANDIDATE, + SESSION_LISTEN_EVENT.FULL, + SESSION_LISTEN_EVENT.QUIT, + SESSION_LISTEN_EVENT.JOIN, + SESSION_LISTEN_EVENT.CHANGE_HOST, + SESSION_LISTEN_EVENT.FINISH, + SESSION_LISTEN_EVENT.REACTION, ]; expectedEvents.forEach((event) => { @@ -229,13 +233,11 @@ describe("useSession Hook 테스트", () => { }); }); - it("room_full 이벤트 발생", () => { - // room_full 이벤트 핸들러 찾기 + it("방이 가득찬 FULL 이벤트 발생", () => { const roomFullHandler = mockSocket.on.mock.calls.find( - ([event]: [string]) => event === "room_full" + ([event]: [string]) => event === SESSION_LISTEN_EVENT.FULL )[1]; - // 이벤트 핸들러 실행 roomFullHandler(); expect(mockToast.error).toHaveBeenCalledWith( @@ -254,33 +256,39 @@ describe("useSession Hook 테스트", () => { // 1. 소켓 이벤트 리스너 제거 expect(mockSocket.off).toHaveBeenCalledWith( - "all_users", + SIGNAL_LISTEN_EVENT.OFFER, + expect.any(Function) + ); + expect(mockSocket.off).toHaveBeenCalledWith( + SIGNAL_LISTEN_EVENT.ANSWER, + expect.any(Function) + ); + expect(mockSocket.off).toHaveBeenCalledWith( + SIGNAL_LISTEN_EVENT.CANDIDATE, expect.any(Function) ); expect(mockSocket.off).toHaveBeenCalledWith( - "getOffer", + SESSION_LISTEN_EVENT.JOIN, expect.any(Function) ); expect(mockSocket.off).toHaveBeenCalledWith( - "getAnswer", + SESSION_LISTEN_EVENT.QUIT, expect.any(Function) ); expect(mockSocket.off).toHaveBeenCalledWith( - "getCandidate", + SESSION_LISTEN_EVENT.FULL, expect.any(Function) ); - expect(mockSocket.off).toHaveBeenCalledWith("user_exit"); - expect(mockSocket.off).toHaveBeenCalledWith("room_full"); expect(mockSocket.off).toHaveBeenCalledWith( - "master_changed", + SESSION_LISTEN_EVENT.CHANGE_HOST, expect.any(Function) ); expect(mockSocket.off).toHaveBeenCalledWith( - "room_finished", + SESSION_LISTEN_EVENT.REACTION, expect.any(Function) ); expect(mockSocket.off).toHaveBeenCalledWith( - "reaction", + SESSION_LISTEN_EVENT.FINISH, expect.any(Function) ); diff --git a/frontend/src/hooks/session/usePeerConnection.ts b/frontend/src/hooks/session/usePeerConnection.ts index 7029b1cc..06d52cd9 100644 --- a/frontend/src/hooks/session/usePeerConnection.ts +++ b/frontend/src/hooks/session/usePeerConnection.ts @@ -107,24 +107,6 @@ const usePeerConnection = (socket: Socket) => { }); }; - // Offer를 생성해야 하는 경우에만 Offer 생성 - // Offer: 초대 - Offer 생성 -> 자신의 설정 저장 -> 상대에게 전송 - // if (isOffer) { - // pc.createOffer() - // .then((offer) => pc.setLocalDescription(offer)) - // .then(() => { - // if (socket && pc.localDescription) { - // socket.emit(SIGNAL_EMIT_EVENT.OFFER, { - // offerReceiveID: peerSocketId, - // sdp: pc.localDescription, - // offerSendID: socket.id, - // offerSendNickname: localUser.nickname, - // }); - // } - // }) - // .catch((error) => console.error("Error creating offer:", error)); - // } - if (isOffer) { try { const offer = await pc.createOffer(); diff --git a/frontend/src/hooks/session/useReaction.ts b/frontend/src/hooks/session/useReaction.ts index f592f87b..15088aaf 100644 --- a/frontend/src/hooks/session/useReaction.ts +++ b/frontend/src/hooks/session/useReaction.ts @@ -7,7 +7,7 @@ import { } from "react"; import { Socket } from "socket.io-client"; import { PeerConnection } from "../type/session"; -import { SESSION_EMIT_EVENT } from "@/constants/WebSocket/SessionEvent.ts"; +import { SESSION_EMIT_EVENT } from "@/constants/WebSocket/SessionEvent"; const REACTION_DURATION = 3000; diff --git a/frontend/src/hooks/session/useSession.ts b/frontend/src/hooks/session/useSession.ts index 0acf9215..af2308e7 100644 --- a/frontend/src/hooks/session/useSession.ts +++ b/frontend/src/hooks/session/useSession.ts @@ -10,8 +10,8 @@ import { usePeerConnectionCleanup } from "@hooks/session/usePeerConnectionCleanu import { useReaction } from "@hooks/session/useReaction"; import { useSocketEvents } from "./useSocketEvents"; import { Socket } from "socket.io-client"; -import { SESSION_EMIT_EVENT } from "@/constants/WebSocket/SessionEvent.ts"; -import useAuth from "@hooks/useAuth.ts"; +import { SESSION_EMIT_EVENT } from "@/constants/WebSocket/SessionEvent"; +import useAuth from "@hooks/useAuth"; export const useSession = (sessionId: string) => { const { socket } = useSocket(); diff --git a/frontend/src/hooks/session/useSocketEvents.ts b/frontend/src/hooks/session/useSocketEvents.ts index 2323a40f..380acc93 100644 --- a/frontend/src/hooks/session/useSocketEvents.ts +++ b/frontend/src/hooks/session/useSocketEvents.ts @@ -18,8 +18,8 @@ import { import { SIGNAL_EMIT_EVENT, SIGNAL_LISTEN_EVENT, -} from "@/constants/WebSocket/SignalingEvent.ts"; -import { SESSION_LISTEN_EVENT } from "@/constants/WebSocket/SessionEvent.ts"; +} from "@/constants/WebSocket/SignalingEvent"; +import { SESSION_LISTEN_EVENT } from "@/constants/WebSocket/SessionEvent"; interface UseSocketEventsProps { socket: Socket | null; diff --git a/frontend/src/hooks/useAuth.ts b/frontend/src/hooks/useAuth.ts index 6cd4e0f6..0ee4fc3d 100644 --- a/frontend/src/hooks/useAuth.ts +++ b/frontend/src/hooks/useAuth.ts @@ -1,4 +1,4 @@ -import useAuthStore from "@stores/useAuthStore.ts"; +import useAuthStore from "@stores/useAuthStore"; import axios from "axios"; import { useEffect } from "react"; diff --git a/frontend/src/hooks/useModal.ts b/frontend/src/hooks/useModal.ts index 735567e2..964afe00 100644 --- a/frontend/src/hooks/useModal.ts +++ b/frontend/src/hooks/useModal.ts @@ -19,13 +19,13 @@ const useModal = (): UseModalReturn => { dialog.showModal(); const handleEscape = (e: KeyboardEvent) => { - if (e.key === 'Escape') { + if (e.key === "Escape") { e.preventDefault(); setIsOpen(false); } }; - window.addEventListener('keydown', handleEscape); + window.addEventListener("keydown", handleEscape); } else { dialog.close(); } @@ -41,7 +41,7 @@ const useModal = (): UseModalReturn => { dialogRef, isOpen, openModal: () => setIsOpen(true), - closeModal: () => setIsOpen(false) + closeModal: () => setIsOpen(false), }; }; diff --git a/frontend/src/pages/MyPage/view/Profile.tsx b/frontend/src/pages/MyPage/view/Profile.tsx index 33298dba..881fea4f 100644 --- a/frontend/src/pages/MyPage/view/Profile.tsx +++ b/frontend/src/pages/MyPage/view/Profile.tsx @@ -10,12 +10,11 @@ interface UseModalReturn { const Profile = ({ nickname, - modal + modal, }: { - nickname: string - modal: UseModalReturn + nickname: string; + modal: UseModalReturn; }) => { - return (
diff --git a/frontend/src/pages/MyPage/view/ProfileEditModal.tsx b/frontend/src/pages/MyPage/view/ProfileEditModal.tsx index 5a57d037..70ad4272 100644 --- a/frontend/src/pages/MyPage/view/ProfileEditModal.tsx +++ b/frontend/src/pages/MyPage/view/ProfileEditModal.tsx @@ -10,7 +10,11 @@ interface UseModalReturn { closeModal: () => void; } -const ProfileEditModal = ({ modal: { dialogRef, isOpen, closeModal } }: { modal: UseModalReturn }) => { +const ProfileEditModal = ({ + modal: { dialogRef, isOpen, closeModal }, +}: { + modal: UseModalReturn; +}) => { const { nickname } = useAuth(); const handleMouseDown = (e: React.MouseEvent) => { @@ -46,7 +50,7 @@ const ProfileEditModal = ({ modal: { dialogRef, isOpen, closeModal } }: { modal: { }} + onChange={() => {}} minLength={2} />
From 2adfbd9d1c609a72400be46b160ffd2825d80e77 Mon Sep 17 00:00:00 2001 From: Chanwoo Kim Date: Thu, 28 Nov 2024 14:59:56 +0900 Subject: [PATCH 55/86] =?UTF-8?q?chore:=20passport-local=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80,=20NODE=5FENV=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=20=EC=A0=9C=EA=B3=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/package.json | 4 +- pnpm-lock.yaml | 396 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 399 insertions(+), 1 deletion(-) diff --git a/backend/package.json b/backend/package.json index f4f00038..22d92a9c 100644 --- a/backend/package.json +++ b/backend/package.json @@ -6,7 +6,7 @@ "private": true, "license": "UNLICENSED", "scripts": { - "build": "nest build", + "build": "NODE_ENV=production nest build", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "start": "nest start", "start:dev": "nest start --watch", @@ -44,6 +44,7 @@ "passport": "^0.7.0", "passport-custom": "^1.1.1", "passport-jwt": "^4.0.1", + "passport-local": "^1.0.0", "redis-om": "^0.4.7", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", @@ -61,6 +62,7 @@ "@types/jest": "^29.5.2", "@types/node": "^20.3.1", "@types/passport-github": "^1.1.12", + "@types/passport-local": "^1.0.38", "@types/supertest": "^6.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dd77782f..bea35a8e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,6 +74,12 @@ importers: mysql2: specifier: ^3.11.4 version: 3.11.4 + nestjs-paginate: + specifier: ^10.0.0 + version: 10.0.0(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/swagger@8.0.7(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2))(express@4.21.1)(fastify@4.28.1)(typeorm@0.3.20(ioredis@5.4.1)(mysql2@3.11.4)(redis@4.7.0)(ts-node@10.9.2(@types/node@20.17.4)(typescript@5.6.3))) + nestjs-redis-om: + specifier: ^0.1.2 + version: 0.1.2(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(redis-om@0.4.7)(reflect-metadata@0.2.2) passport: specifier: ^0.7.0 version: 0.7.0 @@ -83,6 +89,9 @@ importers: passport-jwt: specifier: ^4.0.1 version: 4.0.1 + passport-local: + specifier: ^1.0.0 + version: 1.0.0 redis-om: specifier: ^0.4.7 version: 0.4.7 @@ -129,6 +138,9 @@ importers: '@types/passport-github': specifier: ^1.1.12 version: 1.1.12 + '@types/passport-local': + specifier: ^1.0.38 + version: 1.0.38 '@types/supertest': specifier: ^6.0.0 version: 6.0.2 @@ -786,6 +798,18 @@ packages: resolution: {integrity: sha512-CXtq5nR4Su+2I47WPOlWud98Y5Lv8Kyxp2ukhgFx/eW6Blm18VXJO5WuQylPugRo8nbluoi6GvvxBLqHcvqUUw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@fastify/ajv-compiler@3.6.0': + resolution: {integrity: sha512-LwdXQJjmMD+GwLOkP7TVC68qa+pSSogeWWmznRJ/coyTcfe9qA05AHFSe1eZFwK6q+xVRpChnvFUkf1iYaSZsQ==} + + '@fastify/error@3.4.1': + resolution: {integrity: sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==} + + '@fastify/fast-json-stringify-compiler@4.3.0': + resolution: {integrity: sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==} + + '@fastify/merge-json-schemas@0.1.1': + resolution: {integrity: sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==} + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -1041,6 +1065,9 @@ packages: resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} engines: {node: '>=8'} + '@microsoft/tsdoc@0.15.1': + resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==} + '@moozeh/nestjs-redis-om@0.1.4': resolution: {integrity: sha512-p0UT1WccsLNKOpa5E69fknJZFZAbK2LOImK4JWuuXtGnFJ2DSGtZ8Jbm4TNV6VkdL6x5yqg8YeX44DdZQHuZfA==} peerDependencies: @@ -1097,6 +1124,19 @@ packages: peerDependencies: '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + '@nestjs/mapped-types@2.0.6': + resolution: {integrity: sha512-84ze+CPfp1OWdpRi1/lOu59hOhTz38eVzJvRKrg9ykRFwDz+XleKfMsG0gUqNZYFa6v53XYzeD+xItt8uDW7NQ==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + class-transformer: ^0.4.0 || ^0.5.0 + class-validator: ^0.13.0 || ^0.14.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + '@nestjs/passport@10.0.3': resolution: {integrity: sha512-znJ9Y4S8ZDVY+j4doWAJ8EuuVO7SkQN3yOBmzxbGaXbvcSwFDAdGJ+OMCg52NdzIO4tQoN4pYKx8W6M0ArfFRQ==} peerDependencies: @@ -1121,6 +1161,23 @@ packages: peerDependencies: typescript: '>=4.8.2' + '@nestjs/swagger@8.0.7': + resolution: {integrity: sha512-zaTMCEZ/CxX7QYF110nTqJsn7eCXp4VI9kv7+AdUcIlBmhhgJpggBw2Mx2p6xVjyz1EoWXGfxxWKnxEyaQwFlg==} + peerDependencies: + '@fastify/static': ^6.0.0 || ^7.0.0 + '@nestjs/common': ^9.0.0 || ^10.0.0 + '@nestjs/core': ^9.0.0 || ^10.0.0 + class-transformer: '*' + class-validator: '*' + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + '@fastify/static': + optional: true + class-transformer: + optional: true + class-validator: + optional: true + '@nestjs/testing@10.4.6': resolution: {integrity: sha512-aiDicKhlGibVGNYuew399H5qZZXaseOBT/BS+ERJxxCmco7ZdAqaujsNjSaSbTK9ojDPf27crLT0C4opjqJe3A==} peerDependencies: @@ -1318,6 +1375,9 @@ packages: cpu: [x64] os: [win32] + '@scarf/scarf@1.4.0': + resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} + '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} @@ -1509,6 +1569,9 @@ packages: '@types/passport-jwt@4.0.1': resolution: {integrity: sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==} + '@types/passport-local@1.0.38': + resolution: {integrity: sha512-nsrW4A963lYE7lNTv9cr5WmiUD1ibYJvWrpE13oxApFsRt77b0RdtZvKbCdNIY4v/QZ6TRQWaDDEwV1kCTmcXg==} + '@types/passport-oauth2@1.4.17': resolution: {integrity: sha512-ODiAHvso6JcWJ6ZkHHroVp05EHGhqQN533PtFNBkg8Fy5mERDqsr030AX81M0D69ZcaMvhF92SRckEk2B0HYYg==} @@ -1692,6 +1755,9 @@ packages: resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} deprecated: Use your platform's native atob() and btoa() methods instead + abstract-logging@2.0.1: + resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -1730,6 +1796,14 @@ packages: ajv: optional: true + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv-keywords@3.5.2: resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} peerDependencies: @@ -1827,6 +1901,10 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + autoprefixer@10.4.20: resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} engines: {node: ^10 || ^12 || >=14} @@ -1834,6 +1912,9 @@ packages: peerDependencies: postcss: ^8.1.0 + avvio@8.4.0: + resolution: {integrity: sha512-CDSwaxINFy59iNwhYnkvALBwZiTydGkOecZyPkqBpABYR1KqGEsET0VOOYDwtleZSUIdeY36DC2bSZ24CO1igA==} + aws-ssl-profiles@1.1.2: resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} engines: {node: '>= 6.0.0'} @@ -2562,6 +2643,12 @@ packages: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} + fast-content-type-parse@1.1.0: + resolution: {integrity: sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==} + + fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2575,15 +2662,31 @@ packages: fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + fast-json-stringify@5.16.1: + resolution: {integrity: sha512-KAdnLvy1yu/XrRtP+LJnxbBGrhN+xXu+gt3EUvZhYGKCr3lFHq/7UFJHHFgmJKoqlh6B40bZLEv7w46B0mqn1g==} + fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-querystring@1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + + fast-redact@3.5.0: + resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} + engines: {node: '>=6'} + fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fast-uri@2.4.0: + resolution: {integrity: sha512-ypuAmmMKInk5q7XcepxlnUWDLWv4GFtaJqAzWKqn62IpQ3pejtr5dTVbt3vwqVaMKmkNR55sTT+CqUKIaT21BA==} + fast-uri@3.0.3: resolution: {integrity: sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==} + fastify@4.28.1: + resolution: {integrity: sha512-kFWUtpNr4i7t5vY2EJPCN2KgMVpuqfU4NjnJNCgiNB900oiDeYqaNDRcAfeBbOF5hGixixxcKnOU4KN9z6QncQ==} + fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} @@ -2616,6 +2719,10 @@ packages: resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} engines: {node: '>= 0.8'} + find-my-way@8.2.2: + resolution: {integrity: sha512-Dobi7gcTEq8yszimcfp/R7+owiT4WncAJ7VTTgFH1jYJ5GaG1FbhjwDG820hptN0QDFvzVY3RfCzdInvGPGzjA==} + engines: {node: '>=14'} + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -3190,6 +3297,9 @@ packages: json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-schema-ref-resolver@1.0.1: + resolution: {integrity: sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -3253,6 +3363,9 @@ packages: libphonenumber-js@1.11.14: resolution: {integrity: sha512-sexvAfwcW1Lqws4zFp8heAtAEXbEDnvkYCEGzvOoMgZR7JhXo/IkE9MkkGACgBed5fWqh3ShBGnJBdDnU9N8EQ==} + light-my-request@5.14.0: + resolution: {integrity: sha512-aORPWntbpH5esaYpGOOmri0OHDOe3wC5M2MQxZ9dvMLZm6DnaAn0kJlcbU9hwsQgLzmZyReKwFwwPkR+nHu5kA==} + lilconfig@2.1.0: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} @@ -3509,6 +3622,23 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + nestjs-paginate@10.0.0: + resolution: {integrity: sha512-2nv6JC0tDZ9bPH+4bDRIdF4coQS5iE9J5yO9Z047buxSPv0uSd4XzEBtd2h/Nslj342jhoQrQUg0XpyY0KQ7TA==} + peerDependencies: + '@nestjs/common': ^10.0.0 + '@nestjs/swagger': ^8.0.7 + express: ^4.0.0 + fastify: ^4.0.0 + typeorm: ^0.3.17 + + nestjs-redis-om@0.1.2: + resolution: {integrity: sha512-QTY0EkpIB+Fdm0KXs7fzpgLbF0NJumEcyZ1dA4Fi0/FlPEc4QirFP6Qj9QdkxIIzdg2yi+MB6LIZ3pflSiUEMA==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + '@nestjs/core': ^8.0.0 || ^9.0.0 || ^10.0.0 + redis-om: ^0.4.3 + reflect-metadata: ^0.1.13 || ^0.2.0 + node-abort-controller@3.1.1: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} @@ -3560,6 +3690,10 @@ packages: resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==} engines: {node: '>= 0.4'} + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -3649,6 +3783,10 @@ packages: passport-jwt@4.0.1: resolution: {integrity: sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==} + passport-local@1.0.0: + resolution: {integrity: sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==} + engines: {node: '>= 0.4.0'} + passport-strategy@1.0.0: resolution: {integrity: sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==} engines: {node: '>= 0.4.0'} @@ -3708,6 +3846,16 @@ packages: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} + pino-abstract-transport@2.0.0: + resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} + + pino-std-serializers@7.0.0: + resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==} + + pino@9.5.0: + resolution: {integrity: sha512-xSEmD4pLnV54t0NOUN16yCl7RIB1c5UUOse5HSyEXtBp+FgFQyPeDutc+Q2ZO7/22vImV7VfEjH/1zV2QuqvYw==} + hasBin: true + pirates@4.0.6: resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} @@ -3785,6 +3933,12 @@ packages: process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + process-warning@3.0.0: + resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==} + + process-warning@4.0.0: + resolution: {integrity: sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -3819,6 +3973,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} @@ -3895,6 +4052,10 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + redent@3.0.0: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} @@ -3962,10 +4123,17 @@ packages: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} + ret@0.4.3: + resolution: {integrity: sha512-0f4Memo5QP7WQyUEAYUO3esD/XjOc3Zjjg5CPsAq1p8sIu0XPeMbHJemKA0BO7tV0X7+A0FoEpbmHXWxPyD3wQ==} + engines: {node: '>=10'} + reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} deprecated: Rimraf versions prior to v4 are no longer supported @@ -3996,6 +4164,13 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-regex2@3.1.0: + resolution: {integrity: sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug==} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -4010,6 +4185,9 @@ packages: resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} engines: {node: '>= 10.13.0'} + secure-json-parse@2.7.0: + resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true @@ -4037,6 +4215,9 @@ packages: resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} engines: {node: '>= 0.8.0'} + set-cookie-parser@2.7.1: + resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -4109,6 +4290,9 @@ packages: resolution: {integrity: sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==} engines: {node: '>=10.2.0'} + sonic-boom@4.2.0: + resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -4243,6 +4427,9 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + swagger-ui-dist@5.18.2: + resolution: {integrity: sha512-J+y4mCw/zXh1FOj5wGJvnAajq6XgHOyywsa9yITmwxIlJbMqITq3gYRZHaeqLVH/eV/HOPphE6NjF+nbSNC5Zw==} + symbol-observable@4.0.0: resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} engines: {node: '>=0.10'} @@ -4302,6 +4489,9 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + thread-stream@3.1.0: + resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} @@ -4319,6 +4509,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toad-cache@3.7.0: + resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} + engines: {node: '>=12'} + toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} @@ -5337,6 +5531,22 @@ snapshots: dependencies: levn: 0.4.1 + '@fastify/ajv-compiler@3.6.0': + dependencies: + ajv: 8.17.1 + ajv-formats: 2.1.1(ajv@8.17.1) + fast-uri: 2.4.0 + + '@fastify/error@3.4.1': {} + + '@fastify/fast-json-stringify-compiler@4.3.0': + dependencies: + fast-json-stringify: 5.16.1 + + '@fastify/merge-json-schemas@0.1.1': + dependencies: + fast-deep-equal: 3.1.3 + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -5657,6 +5867,8 @@ snapshots: '@lukeed/csprng@1.1.0': {} + '@microsoft/tsdoc@0.15.1': {} + '@moozeh/nestjs-redis-om@0.1.4(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(redis-om@0.4.7)(reflect-metadata@0.2.2)': dependencies: '@nestjs/common': 10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -5725,6 +5937,14 @@ snapshots: '@types/jsonwebtoken': 9.0.5 jsonwebtoken: 9.0.2 + '@nestjs/mapped-types@2.0.6(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)': + dependencies: + '@nestjs/common': 10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + reflect-metadata: 0.2.2 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.1 + '@nestjs/passport@10.0.3(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(passport@0.7.0)': dependencies: '@nestjs/common': 10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -5776,6 +5996,21 @@ snapshots: transitivePeerDependencies: - chokidar + '@nestjs/swagger@8.0.7(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)': + dependencies: + '@microsoft/tsdoc': 0.15.1 + '@nestjs/common': 10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.4.6(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.6)(@nestjs/websockets@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/mapped-types': 2.0.6(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) + js-yaml: 4.1.0 + lodash: 4.17.21 + path-to-regexp: 3.3.0 + reflect-metadata: 0.2.2 + swagger-ui-dist: 5.18.2 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.1 + '@nestjs/testing@10.4.6(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(@nestjs/platform-express@10.4.6)': dependencies: '@nestjs/common': 10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -5930,6 +6165,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.24.3': optional: true + '@scarf/scarf@1.4.0': {} + '@sinclair/typebox@0.27.8': {} '@sinonjs/commons@3.0.1': @@ -6150,6 +6387,12 @@ snapshots: '@types/jsonwebtoken': 9.0.7 '@types/passport-strategy': 0.2.38 + '@types/passport-local@1.0.38': + dependencies: + '@types/express': 5.0.0 + '@types/passport': 1.0.17 + '@types/passport-strategy': 0.2.38 + '@types/passport-oauth2@1.4.17': dependencies: '@types/express': 5.0.0 @@ -6462,6 +6705,8 @@ snapshots: abab@2.0.6: {} + abstract-logging@2.0.1: {} + accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -6496,6 +6741,14 @@ snapshots: optionalDependencies: ajv: 8.12.0 + ajv-formats@2.1.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + ajv-keywords@3.5.2(ajv@6.12.6): dependencies: ajv: 6.12.6 @@ -6582,6 +6835,8 @@ snapshots: asynckit@0.4.0: {} + atomic-sleep@1.0.0: {} + autoprefixer@10.4.20(postcss@8.4.47): dependencies: browserslist: 4.24.2 @@ -6592,6 +6847,11 @@ snapshots: postcss: 8.4.47 postcss-value-parser: 4.2.0 + avvio@8.4.0: + dependencies: + '@fastify/error': 3.4.1 + fastq: 1.17.1 + aws-ssl-profiles@1.1.2: {} axios@1.7.7: @@ -7445,6 +7705,10 @@ snapshots: iconv-lite: 0.4.24 tmp: 0.0.33 + fast-content-type-parse@1.1.0: {} + + fast-decode-uri-component@1.0.1: {} + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -7459,12 +7723,49 @@ snapshots: fast-json-stable-stringify@2.1.0: {} + fast-json-stringify@5.16.1: + dependencies: + '@fastify/merge-json-schemas': 0.1.1 + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + fast-deep-equal: 3.1.3 + fast-uri: 2.4.0 + json-schema-ref-resolver: 1.0.1 + rfdc: 1.4.1 + fast-levenshtein@2.0.6: {} + fast-querystring@1.1.2: + dependencies: + fast-decode-uri-component: 1.0.1 + + fast-redact@3.5.0: {} + fast-safe-stringify@2.1.1: {} + fast-uri@2.4.0: {} + fast-uri@3.0.3: {} + fastify@4.28.1: + dependencies: + '@fastify/ajv-compiler': 3.6.0 + '@fastify/error': 3.4.1 + '@fastify/fast-json-stringify-compiler': 4.3.0 + abstract-logging: 2.0.1 + avvio: 8.4.0 + fast-content-type-parse: 1.1.0 + fast-json-stringify: 5.16.1 + find-my-way: 8.2.2 + light-my-request: 5.14.0 + pino: 9.5.0 + process-warning: 3.0.0 + proxy-addr: 2.0.7 + rfdc: 1.4.1 + secure-json-parse: 2.7.0 + semver: 7.6.3 + toad-cache: 3.7.0 + fastq@1.17.1: dependencies: reusify: 1.0.4 @@ -7507,6 +7808,12 @@ snapshots: transitivePeerDependencies: - supports-color + find-my-way@8.2.2: + dependencies: + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.2 + safe-regex2: 3.1.0 + find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -8310,6 +8617,10 @@ snapshots: json-parse-even-better-errors@2.3.1: {} + json-schema-ref-resolver@1.0.1: + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -8377,6 +8688,12 @@ snapshots: libphonenumber-js@1.11.14: {} + light-my-request@5.14.0: + dependencies: + cookie: 0.7.2 + process-warning: 3.0.0 + set-cookie-parser: 2.7.1 + lilconfig@2.1.0: {} lilconfig@3.1.2: {} @@ -8579,6 +8896,23 @@ snapshots: neo-async@2.6.2: {} + nestjs-paginate@10.0.0(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/swagger@8.0.7(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2))(express@4.21.1)(fastify@4.28.1)(typeorm@0.3.20(ioredis@5.4.1)(mysql2@3.11.4)(redis@4.7.0)(ts-node@10.9.2(@types/node@20.17.4)(typescript@5.6.3))): + dependencies: + '@nestjs/common': 10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/swagger': 8.0.7(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) + express: 4.21.1 + fastify: 4.28.1 + lodash: 4.17.21 + typeorm: 0.3.20(ioredis@5.4.1)(mysql2@3.11.4)(redis@4.7.0)(ts-node@10.9.2(@types/node@20.17.4)(typescript@5.6.3)) + + nestjs-redis-om@0.1.2(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(redis-om@0.4.7)(reflect-metadata@0.2.2): + dependencies: + '@nestjs/common': 10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.4.6(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.6)(@nestjs/websockets@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1) + redis-om: 0.4.7 + reflect-metadata: 0.2.2 + uuid: 9.0.1 + node-abort-controller@3.1.1: {} node-emoji@1.11.0: @@ -8611,6 +8945,8 @@ snapshots: object-inspect@1.13.2: {} + on-exit-leak-free@2.1.2: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -8710,6 +9046,10 @@ snapshots: jsonwebtoken: 9.0.2 passport-strategy: 1.0.0 + passport-local@1.0.0: + dependencies: + passport-strategy: 1.0.0 + passport-strategy@1.0.0: {} passport@0.7.0: @@ -8749,6 +9089,26 @@ snapshots: pify@2.3.0: {} + pino-abstract-transport@2.0.0: + dependencies: + split2: 4.2.0 + + pino-std-serializers@7.0.0: {} + + pino@9.5.0: + dependencies: + atomic-sleep: 1.0.0 + fast-redact: 3.5.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 2.0.0 + pino-std-serializers: 7.0.0 + process-warning: 4.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.0 + thread-stream: 3.1.0 + pirates@4.0.6: {} pkg-dir@4.2.0: @@ -8817,6 +9177,10 @@ snapshots: process-nextick-args@2.0.1: {} + process-warning@3.0.0: {} + + process-warning@4.0.0: {} + prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -8851,6 +9215,8 @@ snapshots: queue-microtask@1.2.3: {} + quick-format-unescaped@4.0.4: {} + randombytes@2.1.0: dependencies: safe-buffer: 5.2.1 @@ -8934,6 +9300,8 @@ snapshots: dependencies: picomatch: 2.3.1 + real-require@0.2.0: {} + redent@3.0.0: dependencies: indent-string: 4.0.0 @@ -8996,8 +9364,12 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 + ret@0.4.3: {} + reusify@1.0.4: {} + rfdc@1.4.1: {} + rimraf@3.0.2: dependencies: glob: 7.2.3 @@ -9042,6 +9414,12 @@ snapshots: safe-buffer@5.2.1: {} + safe-regex2@3.1.0: + dependencies: + ret: 0.4.3 + + safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} saxes@6.0.0: @@ -9058,6 +9436,8 @@ snapshots: ajv: 6.12.6 ajv-keywords: 3.5.2(ajv@6.12.6) + secure-json-parse@2.7.0: {} + semver@5.7.2: {} semver@6.3.1: {} @@ -9097,6 +9477,8 @@ snapshots: transitivePeerDependencies: - supports-color + set-cookie-parser@2.7.1: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -9225,6 +9607,10 @@ snapshots: - supports-color - utf-8-validate + sonic-boom@4.2.0: + dependencies: + atomic-sleep: 1.0.0 + source-map-js@1.2.1: {} source-map-support@0.5.13: @@ -9364,6 +9750,10 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + swagger-ui-dist@5.18.2: + dependencies: + '@scarf/scarf': 1.4.0 + symbol-observable@4.0.0: {} symbol-tree@3.2.4: {} @@ -9436,6 +9826,10 @@ snapshots: dependencies: any-promise: 1.3.0 + thread-stream@3.1.0: + dependencies: + real-require: 0.2.0 + through@2.3.8: {} tinyexec@0.3.1: {} @@ -9450,6 +9844,8 @@ snapshots: dependencies: is-number: 7.0.0 + toad-cache@3.7.0: {} + toidentifier@1.0.1: {} tough-cookie@4.1.4: From 8c46e16dc29715e9964d850a0580b4d38c726f85 Mon Sep 17 00:00:00 2001 From: twalla26 Date: Thu, 28 Nov 2024 15:24:24 +0900 Subject: [PATCH 56/86] =?UTF-8?q?refactor:=20res.send()=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../question-list/question-list.controller.ts | 118 +++++++++--------- 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/backend/src/question-list/question-list.controller.ts b/backend/src/question-list/question-list.controller.ts index b367c838..654c6ccb 100644 --- a/backend/src/question-list/question-list.controller.ts +++ b/backend/src/question-list/question-list.controller.ts @@ -27,23 +27,23 @@ export class QuestionListController { constructor(private readonly questionListService: QuestionListService) {} @Get() - async getAllQuestionLists(@Res() res) { + async getAllQuestionLists() { try { const allQuestionLists: GetAllQuestionListDto[] = await this.questionListService.getAllQuestionLists(); - return res.send({ + return { success: true, message: "All question lists received successfully.", data: { allQuestionLists, }, - }); + }; } catch (error) { - return res.send({ + return { success: false, message: "Failed to get all question lists.", error: error.message, - }); + }; } } @@ -76,20 +76,20 @@ export class QuestionListController { // 질문지 생성 const { createdQuestionList, createdQuestions } = await this.questionListService.createQuestionList(createQuestionListDto); - return res.send({ + return { success: true, message: "Question list created successfully.", data: { createdQuestionList, createdQuestions, }, - }); + }; } catch (error) { - return res.send({ + return { success: false, message: "Failed to create question list.", error: error.message, - }); + }; } } @@ -105,19 +105,19 @@ export class QuestionListController { const { categoryName } = body; const allQuestionLists: GetAllQuestionListDto[] = await this.questionListService.getAllQuestionListsByCategoryName(categoryName); - return res.send({ + return { success: true, message: "All question lists received successfully.", data: { allQuestionLists, }, - }); + }; } catch (error) { - return res.send({ + return { success: false, message: "Failed to get all question lists.", error: error.message, - }); + }; } } @@ -133,19 +133,19 @@ export class QuestionListController { const { questionListId } = body; const questionListContents: QuestionListContentsDto = await this.questionListService.getQuestionListContents(questionListId); - return res.send({ + return { success: true, message: "Question list contents received successfully.", data: { questionListContents, }, - }); + }; } catch (error) { - return res.send({ + return { success: false, message: "Failed to get question list contents.", error: error.message, - }); + }; } } @@ -156,19 +156,19 @@ export class QuestionListController { const userId = token.userId; const myQuestionLists: MyQuestionListDto[] = await this.questionListService.getMyQuestionLists(userId); - return res.send({ + return { success: true, message: "My question lists received successfully.", data: { myQuestionLists, }, - }); + }; } catch (error) { - return res.send({ + return { success: false, message: "Failed to get my question lists.", error: error.message, - }); + }; } } @@ -193,19 +193,19 @@ export class QuestionListController { const updatedQuestionList = await this.questionListService.updateQuestionList(updateQuestionListDto); - return res.send({ + return { success: true, message: "Question list is updated successfully.", data: { questionList: updatedQuestionList, }, - }); + }; } catch (error) { - return res.send({ + return { success: false, message: "Failed to update question list.", error: error.message, - }); + }; } } @@ -224,22 +224,22 @@ export class QuestionListController { ); if (result.affected) { - return res.send({ + return { success: true, message: "Question list is deleted successfully.", - }); + }; } else { - return res.send({ + return { success: true, message: "Failed to delete question list.", - }); + }; } } catch (error) { - return res.send({ + return { success: true, message: "Failed to delete question list.", error: error.message, - }); + }; } } @@ -262,19 +262,19 @@ export class QuestionListController { const result = await this.questionListService.addQuestion(questionDto); - return res.send({ + return { success: true, message: "The new question is added to the list successfully.", data: { questionList: result, }, - }); + }; } catch (error) { - return res.send({ + return { success: false, message: "Failed to add the new question to the list.", error: error.message, - }); + }; } } @@ -299,19 +299,19 @@ export class QuestionListController { const result = await this.questionListService.updateQuestion(questionDto); - return res.send({ + return { success: true, message: "Question is updated successfully.", data: { questionList: result, }, - }); + }; } catch (error) { - return res.send({ + return { success: false, message: "Failed to update question.", error: error.message, - }); + }; } } @@ -332,22 +332,22 @@ export class QuestionListController { }; const result = await this.questionListService.deleteQuestion(deleteQuestionDto); if (result.affected) { - return res.send({ + return { success: true, message: "Question is deleted successfully.", - }); + }; } else { - return res.send({ + return { success: true, message: "Failed to delete question.", - }); + }; } } catch (error) { - return res.send({ + return { success: false, message: "Failed to delete question.", error: error.message, - }); + }; } } @@ -358,19 +358,19 @@ export class QuestionListController { const userId = token.userId; const scrappedQuestionLists = await this.questionListService.getScrappedQuestionLists(userId); - return res.send({ + return { success: true, message: "Scrapped question lists received successfully.", data: { scrappedQuestionLists, }, - }); + }; } catch (error) { - return res.send({ + return { success: false, message: "Failed to get scrapped question lists.", error: error.message, - }); + }; } } @@ -389,19 +389,19 @@ export class QuestionListController { userId ); - return res.send({ + return { success: true, message: "Question list is scrapped successfully.", data: { scrappedQuestionList, }, - }); + }; } catch (error) { - return res.send({ + return { success: false, message: "Failed to scrap question list.", error: error.message, - }); + }; } } @@ -420,22 +420,22 @@ export class QuestionListController { ); if (unscrappedQuestionList.affected) { - return res.send({ + return { success: true, message: "Question list unscrapped successfully.", - }); + }; } else { - return res.send({ + return { success: false, message: "Failed to unscrap question list.", - }); + }; } } catch (error) { - return res.send({ + return { success: false, message: "Failed to unscrap question list.", error: error.message, - }); + }; } } } From 2f2fc18f75af6bdd2a00f7ad9a548174f4239714 Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Thu, 28 Nov 2024 16:34:50 +0900 Subject: [PATCH 57/86] =?UTF-8?q?feat:=20=EC=A7=88=EB=AC=B8=EC=A7=80=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=EC=97=90=20=EB=8C=80=ED=95=9C=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/hooks/type/session.d.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/frontend/src/hooks/type/session.d.ts b/frontend/src/hooks/type/session.d.ts index 200d7fbf..a013259a 100644 --- a/frontend/src/hooks/type/session.d.ts +++ b/frontend/src/hooks/type/session.d.ts @@ -9,6 +9,9 @@ export interface RoomMetadata { inProgress: boolean; host: UserInfo; category: string | string[]; + questionListId: number; + questionListContents: Question[]; + currentIndex: number; } export interface RoomJoinResponse { @@ -22,6 +25,16 @@ export interface RoomJoinResponse { title: string; id: string; connectionMap: { [socketId: string]: UserInfo }; + questionListId: number; + questionListContents: Question[]; + currentIndex: number; +} + +export interface Question { + id: number; + content: string; + index: number; + questionListId: number; } export interface UserInfo { From 48814da38a566409cc5f93c027aa69318d166e51 Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Thu, 28 Nov 2024 16:35:04 +0900 Subject: [PATCH 58/86] chore: pnpm-lock --- pnpm-lock.yaml | 373 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 373 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dd77782f..b9f71188 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,6 +74,12 @@ importers: mysql2: specifier: ^3.11.4 version: 3.11.4 + nestjs-paginate: + specifier: ^10.0.0 + version: 10.0.0(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/swagger@8.0.7(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2))(express@4.21.1)(fastify@4.28.1)(typeorm@0.3.20(ioredis@5.4.1)(mysql2@3.11.4)(redis@4.7.0)(ts-node@10.9.2(@types/node@20.17.4)(typescript@5.6.3))) + nestjs-redis-om: + specifier: ^0.1.2 + version: 0.1.2(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(redis-om@0.4.7)(reflect-metadata@0.2.2) passport: specifier: ^0.7.0 version: 0.7.0 @@ -786,6 +792,18 @@ packages: resolution: {integrity: sha512-CXtq5nR4Su+2I47WPOlWud98Y5Lv8Kyxp2ukhgFx/eW6Blm18VXJO5WuQylPugRo8nbluoi6GvvxBLqHcvqUUw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@fastify/ajv-compiler@3.6.0': + resolution: {integrity: sha512-LwdXQJjmMD+GwLOkP7TVC68qa+pSSogeWWmznRJ/coyTcfe9qA05AHFSe1eZFwK6q+xVRpChnvFUkf1iYaSZsQ==} + + '@fastify/error@3.4.1': + resolution: {integrity: sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==} + + '@fastify/fast-json-stringify-compiler@4.3.0': + resolution: {integrity: sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==} + + '@fastify/merge-json-schemas@0.1.1': + resolution: {integrity: sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==} + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -1041,6 +1059,9 @@ packages: resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} engines: {node: '>=8'} + '@microsoft/tsdoc@0.15.1': + resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==} + '@moozeh/nestjs-redis-om@0.1.4': resolution: {integrity: sha512-p0UT1WccsLNKOpa5E69fknJZFZAbK2LOImK4JWuuXtGnFJ2DSGtZ8Jbm4TNV6VkdL6x5yqg8YeX44DdZQHuZfA==} peerDependencies: @@ -1097,6 +1118,19 @@ packages: peerDependencies: '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + '@nestjs/mapped-types@2.0.6': + resolution: {integrity: sha512-84ze+CPfp1OWdpRi1/lOu59hOhTz38eVzJvRKrg9ykRFwDz+XleKfMsG0gUqNZYFa6v53XYzeD+xItt8uDW7NQ==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + class-transformer: ^0.4.0 || ^0.5.0 + class-validator: ^0.13.0 || ^0.14.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + '@nestjs/passport@10.0.3': resolution: {integrity: sha512-znJ9Y4S8ZDVY+j4doWAJ8EuuVO7SkQN3yOBmzxbGaXbvcSwFDAdGJ+OMCg52NdzIO4tQoN4pYKx8W6M0ArfFRQ==} peerDependencies: @@ -1121,6 +1155,23 @@ packages: peerDependencies: typescript: '>=4.8.2' + '@nestjs/swagger@8.0.7': + resolution: {integrity: sha512-zaTMCEZ/CxX7QYF110nTqJsn7eCXp4VI9kv7+AdUcIlBmhhgJpggBw2Mx2p6xVjyz1EoWXGfxxWKnxEyaQwFlg==} + peerDependencies: + '@fastify/static': ^6.0.0 || ^7.0.0 + '@nestjs/common': ^9.0.0 || ^10.0.0 + '@nestjs/core': ^9.0.0 || ^10.0.0 + class-transformer: '*' + class-validator: '*' + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + '@fastify/static': + optional: true + class-transformer: + optional: true + class-validator: + optional: true + '@nestjs/testing@10.4.6': resolution: {integrity: sha512-aiDicKhlGibVGNYuew399H5qZZXaseOBT/BS+ERJxxCmco7ZdAqaujsNjSaSbTK9ojDPf27crLT0C4opjqJe3A==} peerDependencies: @@ -1318,6 +1369,9 @@ packages: cpu: [x64] os: [win32] + '@scarf/scarf@1.4.0': + resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} + '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} @@ -1692,6 +1746,9 @@ packages: resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} deprecated: Use your platform's native atob() and btoa() methods instead + abstract-logging@2.0.1: + resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -1730,6 +1787,14 @@ packages: ajv: optional: true + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv-keywords@3.5.2: resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} peerDependencies: @@ -1827,6 +1892,10 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + autoprefixer@10.4.20: resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} engines: {node: ^10 || ^12 || >=14} @@ -1834,6 +1903,9 @@ packages: peerDependencies: postcss: ^8.1.0 + avvio@8.4.0: + resolution: {integrity: sha512-CDSwaxINFy59iNwhYnkvALBwZiTydGkOecZyPkqBpABYR1KqGEsET0VOOYDwtleZSUIdeY36DC2bSZ24CO1igA==} + aws-ssl-profiles@1.1.2: resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} engines: {node: '>= 6.0.0'} @@ -2562,6 +2634,12 @@ packages: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} + fast-content-type-parse@1.1.0: + resolution: {integrity: sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==} + + fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2575,15 +2653,31 @@ packages: fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + fast-json-stringify@5.16.1: + resolution: {integrity: sha512-KAdnLvy1yu/XrRtP+LJnxbBGrhN+xXu+gt3EUvZhYGKCr3lFHq/7UFJHHFgmJKoqlh6B40bZLEv7w46B0mqn1g==} + fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-querystring@1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + + fast-redact@3.5.0: + resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} + engines: {node: '>=6'} + fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fast-uri@2.4.0: + resolution: {integrity: sha512-ypuAmmMKInk5q7XcepxlnUWDLWv4GFtaJqAzWKqn62IpQ3pejtr5dTVbt3vwqVaMKmkNR55sTT+CqUKIaT21BA==} + fast-uri@3.0.3: resolution: {integrity: sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==} + fastify@4.28.1: + resolution: {integrity: sha512-kFWUtpNr4i7t5vY2EJPCN2KgMVpuqfU4NjnJNCgiNB900oiDeYqaNDRcAfeBbOF5hGixixxcKnOU4KN9z6QncQ==} + fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} @@ -2616,6 +2710,10 @@ packages: resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} engines: {node: '>= 0.8'} + find-my-way@8.2.2: + resolution: {integrity: sha512-Dobi7gcTEq8yszimcfp/R7+owiT4WncAJ7VTTgFH1jYJ5GaG1FbhjwDG820hptN0QDFvzVY3RfCzdInvGPGzjA==} + engines: {node: '>=14'} + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -3190,6 +3288,9 @@ packages: json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-schema-ref-resolver@1.0.1: + resolution: {integrity: sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -3253,6 +3354,9 @@ packages: libphonenumber-js@1.11.14: resolution: {integrity: sha512-sexvAfwcW1Lqws4zFp8heAtAEXbEDnvkYCEGzvOoMgZR7JhXo/IkE9MkkGACgBed5fWqh3ShBGnJBdDnU9N8EQ==} + light-my-request@5.14.0: + resolution: {integrity: sha512-aORPWntbpH5esaYpGOOmri0OHDOe3wC5M2MQxZ9dvMLZm6DnaAn0kJlcbU9hwsQgLzmZyReKwFwwPkR+nHu5kA==} + lilconfig@2.1.0: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} @@ -3509,6 +3613,23 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + nestjs-paginate@10.0.0: + resolution: {integrity: sha512-2nv6JC0tDZ9bPH+4bDRIdF4coQS5iE9J5yO9Z047buxSPv0uSd4XzEBtd2h/Nslj342jhoQrQUg0XpyY0KQ7TA==} + peerDependencies: + '@nestjs/common': ^10.0.0 + '@nestjs/swagger': ^8.0.7 + express: ^4.0.0 + fastify: ^4.0.0 + typeorm: ^0.3.17 + + nestjs-redis-om@0.1.2: + resolution: {integrity: sha512-QTY0EkpIB+Fdm0KXs7fzpgLbF0NJumEcyZ1dA4Fi0/FlPEc4QirFP6Qj9QdkxIIzdg2yi+MB6LIZ3pflSiUEMA==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + '@nestjs/core': ^8.0.0 || ^9.0.0 || ^10.0.0 + redis-om: ^0.4.3 + reflect-metadata: ^0.1.13 || ^0.2.0 + node-abort-controller@3.1.1: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} @@ -3560,6 +3681,10 @@ packages: resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==} engines: {node: '>= 0.4'} + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -3708,6 +3833,16 @@ packages: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} + pino-abstract-transport@2.0.0: + resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} + + pino-std-serializers@7.0.0: + resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==} + + pino@9.5.0: + resolution: {integrity: sha512-xSEmD4pLnV54t0NOUN16yCl7RIB1c5UUOse5HSyEXtBp+FgFQyPeDutc+Q2ZO7/22vImV7VfEjH/1zV2QuqvYw==} + hasBin: true + pirates@4.0.6: resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} @@ -3785,6 +3920,12 @@ packages: process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + process-warning@3.0.0: + resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==} + + process-warning@4.0.0: + resolution: {integrity: sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -3819,6 +3960,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} @@ -3895,6 +4039,10 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + redent@3.0.0: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} @@ -3962,10 +4110,17 @@ packages: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} + ret@0.4.3: + resolution: {integrity: sha512-0f4Memo5QP7WQyUEAYUO3esD/XjOc3Zjjg5CPsAq1p8sIu0XPeMbHJemKA0BO7tV0X7+A0FoEpbmHXWxPyD3wQ==} + engines: {node: '>=10'} + reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} deprecated: Rimraf versions prior to v4 are no longer supported @@ -3996,6 +4151,13 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-regex2@3.1.0: + resolution: {integrity: sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug==} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -4010,6 +4172,9 @@ packages: resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} engines: {node: '>= 10.13.0'} + secure-json-parse@2.7.0: + resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true @@ -4037,6 +4202,9 @@ packages: resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} engines: {node: '>= 0.8.0'} + set-cookie-parser@2.7.1: + resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -4109,6 +4277,9 @@ packages: resolution: {integrity: sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==} engines: {node: '>=10.2.0'} + sonic-boom@4.2.0: + resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -4243,6 +4414,9 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + swagger-ui-dist@5.18.2: + resolution: {integrity: sha512-J+y4mCw/zXh1FOj5wGJvnAajq6XgHOyywsa9yITmwxIlJbMqITq3gYRZHaeqLVH/eV/HOPphE6NjF+nbSNC5Zw==} + symbol-observable@4.0.0: resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} engines: {node: '>=0.10'} @@ -4302,6 +4476,9 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + thread-stream@3.1.0: + resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} @@ -4319,6 +4496,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toad-cache@3.7.0: + resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} + engines: {node: '>=12'} + toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} @@ -5337,6 +5518,22 @@ snapshots: dependencies: levn: 0.4.1 + '@fastify/ajv-compiler@3.6.0': + dependencies: + ajv: 8.17.1 + ajv-formats: 2.1.1(ajv@8.17.1) + fast-uri: 2.4.0 + + '@fastify/error@3.4.1': {} + + '@fastify/fast-json-stringify-compiler@4.3.0': + dependencies: + fast-json-stringify: 5.16.1 + + '@fastify/merge-json-schemas@0.1.1': + dependencies: + fast-deep-equal: 3.1.3 + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -5657,6 +5854,8 @@ snapshots: '@lukeed/csprng@1.1.0': {} + '@microsoft/tsdoc@0.15.1': {} + '@moozeh/nestjs-redis-om@0.1.4(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(redis-om@0.4.7)(reflect-metadata@0.2.2)': dependencies: '@nestjs/common': 10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -5725,6 +5924,14 @@ snapshots: '@types/jsonwebtoken': 9.0.5 jsonwebtoken: 9.0.2 + '@nestjs/mapped-types@2.0.6(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)': + dependencies: + '@nestjs/common': 10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + reflect-metadata: 0.2.2 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.1 + '@nestjs/passport@10.0.3(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(passport@0.7.0)': dependencies: '@nestjs/common': 10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -5776,6 +5983,21 @@ snapshots: transitivePeerDependencies: - chokidar + '@nestjs/swagger@8.0.7(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)': + dependencies: + '@microsoft/tsdoc': 0.15.1 + '@nestjs/common': 10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.4.6(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.6)(@nestjs/websockets@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/mapped-types': 2.0.6(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) + js-yaml: 4.1.0 + lodash: 4.17.21 + path-to-regexp: 3.3.0 + reflect-metadata: 0.2.2 + swagger-ui-dist: 5.18.2 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.1 + '@nestjs/testing@10.4.6(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(@nestjs/platform-express@10.4.6)': dependencies: '@nestjs/common': 10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -5930,6 +6152,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.24.3': optional: true + '@scarf/scarf@1.4.0': {} + '@sinclair/typebox@0.27.8': {} '@sinonjs/commons@3.0.1': @@ -6462,6 +6686,8 @@ snapshots: abab@2.0.6: {} + abstract-logging@2.0.1: {} + accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -6496,6 +6722,14 @@ snapshots: optionalDependencies: ajv: 8.12.0 + ajv-formats@2.1.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + ajv-keywords@3.5.2(ajv@6.12.6): dependencies: ajv: 6.12.6 @@ -6582,6 +6816,8 @@ snapshots: asynckit@0.4.0: {} + atomic-sleep@1.0.0: {} + autoprefixer@10.4.20(postcss@8.4.47): dependencies: browserslist: 4.24.2 @@ -6592,6 +6828,11 @@ snapshots: postcss: 8.4.47 postcss-value-parser: 4.2.0 + avvio@8.4.0: + dependencies: + '@fastify/error': 3.4.1 + fastq: 1.17.1 + aws-ssl-profiles@1.1.2: {} axios@1.7.7: @@ -7445,6 +7686,10 @@ snapshots: iconv-lite: 0.4.24 tmp: 0.0.33 + fast-content-type-parse@1.1.0: {} + + fast-decode-uri-component@1.0.1: {} + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -7459,12 +7704,49 @@ snapshots: fast-json-stable-stringify@2.1.0: {} + fast-json-stringify@5.16.1: + dependencies: + '@fastify/merge-json-schemas': 0.1.1 + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + fast-deep-equal: 3.1.3 + fast-uri: 2.4.0 + json-schema-ref-resolver: 1.0.1 + rfdc: 1.4.1 + fast-levenshtein@2.0.6: {} + fast-querystring@1.1.2: + dependencies: + fast-decode-uri-component: 1.0.1 + + fast-redact@3.5.0: {} + fast-safe-stringify@2.1.1: {} + fast-uri@2.4.0: {} + fast-uri@3.0.3: {} + fastify@4.28.1: + dependencies: + '@fastify/ajv-compiler': 3.6.0 + '@fastify/error': 3.4.1 + '@fastify/fast-json-stringify-compiler': 4.3.0 + abstract-logging: 2.0.1 + avvio: 8.4.0 + fast-content-type-parse: 1.1.0 + fast-json-stringify: 5.16.1 + find-my-way: 8.2.2 + light-my-request: 5.14.0 + pino: 9.5.0 + process-warning: 3.0.0 + proxy-addr: 2.0.7 + rfdc: 1.4.1 + secure-json-parse: 2.7.0 + semver: 7.6.3 + toad-cache: 3.7.0 + fastq@1.17.1: dependencies: reusify: 1.0.4 @@ -7507,6 +7789,12 @@ snapshots: transitivePeerDependencies: - supports-color + find-my-way@8.2.2: + dependencies: + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.2 + safe-regex2: 3.1.0 + find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -8310,6 +8598,10 @@ snapshots: json-parse-even-better-errors@2.3.1: {} + json-schema-ref-resolver@1.0.1: + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -8377,6 +8669,12 @@ snapshots: libphonenumber-js@1.11.14: {} + light-my-request@5.14.0: + dependencies: + cookie: 0.7.2 + process-warning: 3.0.0 + set-cookie-parser: 2.7.1 + lilconfig@2.1.0: {} lilconfig@3.1.2: {} @@ -8579,6 +8877,23 @@ snapshots: neo-async@2.6.2: {} + nestjs-paginate@10.0.0(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/swagger@8.0.7(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2))(express@4.21.1)(fastify@4.28.1)(typeorm@0.3.20(ioredis@5.4.1)(mysql2@3.11.4)(redis@4.7.0)(ts-node@10.9.2(@types/node@20.17.4)(typescript@5.6.3))): + dependencies: + '@nestjs/common': 10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/swagger': 8.0.7(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) + express: 4.21.1 + fastify: 4.28.1 + lodash: 4.17.21 + typeorm: 0.3.20(ioredis@5.4.1)(mysql2@3.11.4)(redis@4.7.0)(ts-node@10.9.2(@types/node@20.17.4)(typescript@5.6.3)) + + nestjs-redis-om@0.1.2(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(redis-om@0.4.7)(reflect-metadata@0.2.2): + dependencies: + '@nestjs/common': 10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.4.6(@nestjs/common@10.4.6(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.6)(@nestjs/websockets@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1) + redis-om: 0.4.7 + reflect-metadata: 0.2.2 + uuid: 9.0.1 + node-abort-controller@3.1.1: {} node-emoji@1.11.0: @@ -8611,6 +8926,8 @@ snapshots: object-inspect@1.13.2: {} + on-exit-leak-free@2.1.2: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -8749,6 +9066,26 @@ snapshots: pify@2.3.0: {} + pino-abstract-transport@2.0.0: + dependencies: + split2: 4.2.0 + + pino-std-serializers@7.0.0: {} + + pino@9.5.0: + dependencies: + atomic-sleep: 1.0.0 + fast-redact: 3.5.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 2.0.0 + pino-std-serializers: 7.0.0 + process-warning: 4.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.0 + thread-stream: 3.1.0 + pirates@4.0.6: {} pkg-dir@4.2.0: @@ -8817,6 +9154,10 @@ snapshots: process-nextick-args@2.0.1: {} + process-warning@3.0.0: {} + + process-warning@4.0.0: {} + prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -8851,6 +9192,8 @@ snapshots: queue-microtask@1.2.3: {} + quick-format-unescaped@4.0.4: {} + randombytes@2.1.0: dependencies: safe-buffer: 5.2.1 @@ -8934,6 +9277,8 @@ snapshots: dependencies: picomatch: 2.3.1 + real-require@0.2.0: {} + redent@3.0.0: dependencies: indent-string: 4.0.0 @@ -8996,8 +9341,12 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 + ret@0.4.3: {} + reusify@1.0.4: {} + rfdc@1.4.1: {} + rimraf@3.0.2: dependencies: glob: 7.2.3 @@ -9042,6 +9391,12 @@ snapshots: safe-buffer@5.2.1: {} + safe-regex2@3.1.0: + dependencies: + ret: 0.4.3 + + safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} saxes@6.0.0: @@ -9058,6 +9413,8 @@ snapshots: ajv: 6.12.6 ajv-keywords: 3.5.2(ajv@6.12.6) + secure-json-parse@2.7.0: {} + semver@5.7.2: {} semver@6.3.1: {} @@ -9097,6 +9454,8 @@ snapshots: transitivePeerDependencies: - supports-color + set-cookie-parser@2.7.1: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -9225,6 +9584,10 @@ snapshots: - supports-color - utf-8-validate + sonic-boom@4.2.0: + dependencies: + atomic-sleep: 1.0.0 + source-map-js@1.2.1: {} source-map-support@0.5.13: @@ -9364,6 +9727,10 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + swagger-ui-dist@5.18.2: + dependencies: + '@scarf/scarf': 1.4.0 + symbol-observable@4.0.0: {} symbol-tree@3.2.4: {} @@ -9436,6 +9803,10 @@ snapshots: dependencies: any-promise: 1.3.0 + thread-stream@3.1.0: + dependencies: + real-require: 0.2.0 + through@2.3.8: {} tinyexec@0.3.1: {} @@ -9450,6 +9821,8 @@ snapshots: dependencies: is-number: 7.0.0 + toad-cache@3.7.0: {} + toidentifier@1.0.1: {} tough-cookie@4.1.4: From eb9f7c4a4ce67df9d3e1925a5fa280ab9f5e6d13 Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Thu, 28 Nov 2024 16:35:27 +0900 Subject: [PATCH 59/86] =?UTF-8?q?feat:=20=EC=8A=A4=ED=84=B0=EB=94=94=20?= =?UTF-8?q?=EC=9B=B9=EC=86=8C=EC=BC=93=20API=20=EC=9D=B4=EB=B2=A4=ED=8A=B8?= =?UTF-8?q?=EB=AA=85=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/constants/WebSocket/StudyEvent.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 frontend/src/constants/WebSocket/StudyEvent.ts diff --git a/frontend/src/constants/WebSocket/StudyEvent.ts b/frontend/src/constants/WebSocket/StudyEvent.ts new file mode 100644 index 00000000..1d7b130b --- /dev/null +++ b/frontend/src/constants/WebSocket/StudyEvent.ts @@ -0,0 +1,16 @@ +export const STUDY_EMIT_EVENT = { + NEXT: "client:study__next_question", + CURRENT: "client:study__current_index", + INDEX: "client:study__move_index", + START: "client:study__start_progress", + STOP: "client:study__stop_progress", +}; + +export const STUDY_LISTEN_EVENT = { + NEXT: "server:study__next_question", + CURRENT: "server:study__current_index", + INDEX: "server:study__move_index", + START: "server:study__start_progress", + STOP: "server:study__stop_progress", + PROGRESS: "server:room__progress", +}; From 687c092927adbd7202a12396fe5eaee87c49c989 Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Thu, 28 Nov 2024 16:35:56 +0900 Subject: [PATCH 60/86] =?UTF-8?q?feat:=20=EC=8A=A4=ED=84=B0=EB=94=94?= =?UTF-8?q?=EA=B0=80=20=EC=A7=84=ED=96=89=20=EC=A4=91=EC=9D=B8=EC=A7=80=20?= =?UTF-8?q?=EC=95=84=EB=8B=8C=EC=A7=80=EB=A5=BC=20=EC=A7=81=EA=B4=80?= =?UTF-8?q?=EC=A0=81=EC=9C=BC=EB=A1=9C=20=ED=99=95=EC=9D=B8=ED=95=A0=20?= =?UTF-8?q?=EC=88=98=20=EC=9E=88=EA=B2=8C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/session/SessionHeader.tsx | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/frontend/src/components/session/SessionHeader.tsx b/frontend/src/components/session/SessionHeader.tsx index 09d1f4d7..71e2ccda 100644 --- a/frontend/src/components/session/SessionHeader.tsx +++ b/frontend/src/components/session/SessionHeader.tsx @@ -1,4 +1,5 @@ import { RoomMetadata } from "@hooks/type/session"; +import { useEffect, useState } from "react"; interface SessionHeaderProps { roomMetadata: RoomMetadata | null; @@ -9,6 +10,19 @@ const SessionHeader = ({ participantsCount, roomMetadata, }: SessionHeaderProps) => { + const [uptime, setUptime] = useState(0); + + useEffect(() => { + if (!roomMetadata?.inProgress) return; + const interval = setInterval(() => { + setUptime((prev) => prev + 1); + }, 1000); + + return () => { + clearInterval(interval); + }; + }, [roomMetadata?.inProgress]); + return (
+ {roomMetadata.inProgress ? ( + + + 스터디 진행 중 {uptime}초 + + ) : ( + + + 스터디 시작 전 + + )} ) : (

아직 세션에 참가하지 않았습니다.

From e7f4f528e50d1718ac0bd2d390500fe8ba4ef9f2 Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Thu, 28 Nov 2024 16:36:34 +0900 Subject: [PATCH 61/86] =?UTF-8?q?feat:=20=EC=A7=88=EB=AC=B8=20=EB=84=98?= =?UTF-8?q?=EA=B8=B0=EA=B8=B0=20API=20=EC=97=B0=EB=8F=99=20=EB=B0=8F=20?= =?UTF-8?q?=EC=8A=A4=ED=84=B0=EB=94=94=20=EC=8B=9C=EC=9E=91=20API=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/SessionPage.tsx | 41 +++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/SessionPage.tsx b/frontend/src/pages/SessionPage.tsx index ca6f220a..5019c928 100644 --- a/frontend/src/pages/SessionPage.tsx +++ b/frontend/src/pages/SessionPage.tsx @@ -7,6 +7,7 @@ import useSocket from "@hooks/useSocket"; import SessionHeader from "@components/session/SessionHeader"; import { useEffect } from "react"; import useToast from "@hooks/useToast.ts"; +import { STUDY_EMIT_EVENT } from "@/constants/WebSocket/StudyEvent.ts"; const SessionPage = () => { const { sessionId } = useParams(); @@ -42,6 +43,39 @@ const SessionPage = () => { peerMediaStatus, } = useSession(sessionId!); + const requestChangeIndex = ( + type: "next" | "prev" | "current" | "move", + index?: number + ) => { + if (socket) { + if (isHost && roomMetadata) { + switch (type) { + case "next": + socket.emit(STUDY_EMIT_EVENT.NEXT, { roomId: sessionId }); + break; + case "prev": + socket.emit(STUDY_EMIT_EVENT.INDEX, { + roomId: sessionId, + index: roomMetadata.currentIndex - 1, + }); + break; + case "current": + socket.emit(STUDY_EMIT_EVENT.CURRENT, { roomId: sessionId }); + break; + case "move": + socket.emit(STUDY_EMIT_EVENT.INDEX, { roomId: sessionId, index }); + break; + } + } + } + }; + + const startStudySession = () => { + if (socket) { + socket.emit(STUDY_EMIT_EVENT.START, { roomId: sessionId }); + } + }; + return (
@@ -119,6 +153,7 @@ const SessionPage = () => {
{ isVideoOn={isVideoOn} isMicOn={isMicOn} videoLoading={videoLoading} + isHost={isHost} + isInProgress={roomMetadata?.inProgress ?? false} + startStudySession={startStudySession} />
Date: Thu, 28 Nov 2024 16:36:55 +0900 Subject: [PATCH 62/86] =?UTF-8?q?feat:=20=ED=98=84=EC=9E=AC=20=EC=A7=88?= =?UTF-8?q?=EB=AC=B8=EC=9D=84=20=ED=91=9C=EC=8B=9C=ED=95=98=EA=B3=A0=20?= =?UTF-8?q?=EC=9D=B4=EC=A0=84=20=EC=A7=88=EB=AC=B8=EB=93=A4=EC=9D=98=20?= =?UTF-8?q?=EA=B8=B0=EB=A1=9D=EC=9D=84=20=EC=82=AC=EC=9D=B4=EB=93=9C?= =?UTF-8?q?=EB=B0=94=EC=97=90=20=EB=82=A8=EA=B8=B0=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/session/SessionSidebar.tsx | 55 +++++++++++++++---- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/session/SessionSidebar.tsx b/frontend/src/components/session/SessionSidebar.tsx index c0f7b23e..36dabde0 100644 --- a/frontend/src/components/session/SessionSidebar.tsx +++ b/frontend/src/components/session/SessionSidebar.tsx @@ -1,4 +1,4 @@ -import { FaClipboardList } from "react-icons/fa"; +import { FaClipboardList, FaFolder } from "react-icons/fa"; import { FaUserGroup } from "react-icons/fa6"; import useModalStore from "../../stores/useModalStore"; import Modal from "../common/Modal"; @@ -7,6 +7,7 @@ import { Socket } from "socket.io-client"; import useToast from "../../hooks/useToast"; import { TbCrown } from "react-icons/tb"; import { SESSION_EMIT_EVENT } from "@/constants/WebSocket/SessionEvent.ts"; +import { Question } from "@hooks/type/session"; interface ParticipantsData { nickname: string; @@ -15,7 +16,8 @@ interface ParticipantsData { interface Props { socket: Socket | null; - question: string; + questionList: Question[]; + currentIndex: number; participants: ParticipantsData[]; roomId: string | undefined; // TODO: sessionId가 입력되지 않았을 때(undefined) 처리 필요 isHost: boolean; @@ -23,7 +25,8 @@ interface Props { const SessionSidebar = ({ socket, - question, + questionList, + currentIndex, participants, roomId, isHost, @@ -94,20 +97,29 @@ const SessionSidebar = ({ />
-

+

- 질문 + 현재 질문

-

- {question} -

+ {currentIndex >= 0 ? ( +

+ + Q{questionList[currentIndex].index}.{" "} + + {questionList[currentIndex].content} +

+ ) : ( +

질문 로딩 중...

+ )} +
-
-

+
+

참가자

@@ -123,6 +135,27 @@ const SessionSidebar = ({ ))}
+
+

+ + 이전 질문 +

+
    + {currentIndex <= 0 && ( +
  • + 여기에 이전 질문이 기록됩니다. +
  • + )} + {questionList.map((question, index) => { + if (index < currentIndex) + return ( +
  • + Q{index + 1}. {question.content} +
  • + ); + })} +
+

- + {isHost && isInProgress ? ( +
+ +
+ ) : ( +
+ +
+ )} + {isHost && isInProgress && ( +
+ + +
+ )}
); }; diff --git a/frontend/src/hooks/session/useSocketEvents.ts b/frontend/src/hooks/session/useSocketEvents.ts index eb92bc12..361170c2 100644 --- a/frontend/src/hooks/session/useSocketEvents.ts +++ b/frontend/src/hooks/session/useSocketEvents.ts @@ -14,6 +14,7 @@ import { ResponseMasterChanged, RoomMetadata, PeerConnection, + ProgressResponse, } from "@hooks/type/session"; import { SIGNAL_EMIT_EVENT, @@ -216,11 +217,16 @@ export const useSocketEvents = ({ } }; - const handleStartProgress = () => { - setRoomMetadata((prev) => ({ ...prev!, inProgress: true })); - }; - const handleStopProgress = () => { - setRoomMetadata((prev) => ({ ...prev!, inProgress: false })); + const handleProgress = (data: ProgressResponse) => { + const { status, inProgress } = data; + + if (status === "success") { + setRoomMetadata((prev) => ({ ...prev!, inProgress: inProgress })); + if (inProgress) toast.success("방장이 스터디를 시작했습니다."); + else toast.error("방장이 스터디를 중지했습니다."); + } else { + toast.error("세션 진행을 시작하지 못했습니다."); + } }; socket.on(SIGNAL_LISTEN_EVENT.OFFER, handleGetOffer); @@ -235,8 +241,8 @@ export const useSocketEvents = ({ socket.on(STUDY_LISTEN_EVENT.INDEX, handleChangeIndex); socket.on(STUDY_LISTEN_EVENT.CURRENT, handleChangeIndex); socket.on(STUDY_LISTEN_EVENT.NEXT, handleChangeIndex); - socket.on(STUDY_LISTEN_EVENT.START, handleStartProgress); - socket.on(STUDY_LISTEN_EVENT.STOP, handleStopProgress); + socket.on(STUDY_LISTEN_EVENT.START, handleProgress); + socket.on(STUDY_LISTEN_EVENT.STOP, handleProgress); socket.on(STUDY_LISTEN_EVENT.PROGRESS, handleRoomProgress); return () => { @@ -252,8 +258,8 @@ export const useSocketEvents = ({ socket.off(STUDY_LISTEN_EVENT.INDEX, handleChangeIndex); socket.off(STUDY_LISTEN_EVENT.CURRENT, handleChangeIndex); socket.off(STUDY_LISTEN_EVENT.NEXT, handleChangeIndex); - socket.off(STUDY_LISTEN_EVENT.START, handleStartProgress); - socket.off(STUDY_LISTEN_EVENT.STOP, handleStopProgress); + socket.off(STUDY_LISTEN_EVENT.START, handleProgress); + socket.off(STUDY_LISTEN_EVENT.STOP, handleProgress); if (reactionTimeouts.current) { Object.values(reactionTimeouts.current).forEach(clearTimeout); diff --git a/frontend/src/hooks/type/session.d.ts b/frontend/src/hooks/type/session.d.ts index a013259a..7d4d8f33 100644 --- a/frontend/src/hooks/type/session.d.ts +++ b/frontend/src/hooks/type/session.d.ts @@ -61,3 +61,8 @@ export interface PeerConnection { isHost?: boolean; // 호스트 여부 reaction?: string; } + +export interface ProgressResponse { + status: "success" | "error"; + inProgress: boolean; +} diff --git a/frontend/src/pages/SessionPage.tsx b/frontend/src/pages/SessionPage.tsx index 5019c928..6c1b9eb6 100644 --- a/frontend/src/pages/SessionPage.tsx +++ b/frontend/src/pages/SessionPage.tsx @@ -76,6 +76,12 @@ const SessionPage = () => { } }; + const stopStudySession = () => { + if (socket) { + socket.emit(STUDY_EMIT_EVENT.STOP, { roomId: sessionId }); + } + }; + return (
@@ -167,6 +173,7 @@ const SessionPage = () => { isHost={isHost} isInProgress={roomMetadata?.inProgress ?? false} startStudySession={startStudySession} + stopStudySession={stopStudySession} />
Date: Thu, 28 Nov 2024 19:37:39 +0900 Subject: [PATCH 66/86] =?UTF-8?q?fix:=20=EC=A7=88=EB=AC=B8=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EB=A1=9C=EC=A7=81=EC=97=90=20transactional=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/question-list/question-list.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/question-list/question-list.service.ts b/backend/src/question-list/question-list.service.ts index fd48717d..0cb3762e 100644 --- a/backend/src/question-list/question-list.service.ts +++ b/backend/src/question-list/question-list.service.ts @@ -260,6 +260,7 @@ export class QuestionListService { return await this.getQuestionListContents(questionListId); } + @Transactional() async deleteQuestion(deleteQuestionDto: DeleteQuestionDto) { const { id, questionListId, userId } = deleteQuestionDto; From 1bfb78b046cadc92f6ff08e25bd285ce250656bd Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Thu, 28 Nov 2024 19:51:21 +0900 Subject: [PATCH 67/86] =?UTF-8?q?feat:=20=EC=82=AC=EC=9D=B4=EB=93=9C?= =?UTF-8?q?=EB=B0=94=20=EC=A0=91=EA=B8=B0=20=EB=B2=84=ED=8A=BC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/session/SessionSidebar.tsx | 159 +++++++++--------- 1 file changed, 84 insertions(+), 75 deletions(-) diff --git a/frontend/src/components/session/SessionSidebar.tsx b/frontend/src/components/session/SessionSidebar.tsx index 36dabde0..fe1d22c6 100644 --- a/frontend/src/components/session/SessionSidebar.tsx +++ b/frontend/src/components/session/SessionSidebar.tsx @@ -8,6 +8,7 @@ import useToast from "../../hooks/useToast"; import { TbCrown } from "react-icons/tb"; import { SESSION_EMIT_EVENT } from "@/constants/WebSocket/SessionEvent.ts"; import { Question } from "@hooks/type/session"; +import { BsLayoutSidebarReverse } from "react-icons/bs"; interface ParticipantsData { nickname: string; @@ -70,10 +71,90 @@ const SessionSidebar = ({ return (
+
+ +
+
+
+
+

+ + 현재 질문 +

+
+ {currentIndex >= 0 ? ( +

+ + Q{questionList[currentIndex].index}.{" "} + + {questionList[currentIndex].content} +

+ ) : ( +

질문 로딩 중...

+ )} +
+
+
+

+ + 참가자 +

+
    + {participants.map((participant, index) => ( +
  • + + {participant.nickname} + + {participant.isHost && } + +
  • + ))} +
+
+
+

+ + 이전 질문 +

+
    + {currentIndex <= 0 && ( +
  • + 여기에 이전 질문이 기록됩니다. +
  • + )} + {questionList.map((question, index) => { + if (index < currentIndex) + return ( +
  • + Q{index + 1}. {question.content} +
  • + ); + })} +
+
+
+
+ +
+
+ -
-
-

- - 현재 질문 -

-
- {currentIndex >= 0 ? ( -

- - Q{questionList[currentIndex].index}.{" "} - - {questionList[currentIndex].content} -

- ) : ( -

질문 로딩 중...

- )} -
-
-
-

- - 참가자 -

-
    - {participants.map((participant, index) => ( -
  • - - {participant.nickname} - - {participant.isHost && } - -
  • - ))} -
-
-
-

- - 이전 질문 -

-
    - {currentIndex <= 0 && ( -
  • - 여기에 이전 질문이 기록됩니다. -
  • - )} - {questionList.map((question, index) => { - if (index < currentIndex) - return ( -
  • - Q{index + 1}. {question.content} -
  • - ); - })} -
-
-
-
- -
); }; From b1af3f15e667aabb70d2f0a6ef986c894bd558d1 Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Thu, 28 Nov 2024 20:43:30 +0900 Subject: [PATCH 68/86] =?UTF-8?q?feat:=20=EC=82=AC=EC=9D=B4=EB=93=9C?= =?UTF-8?q?=EB=B0=94=EB=A5=BC=20=EC=A0=91=EC=9D=84=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=82=AC=EC=9D=B4=EB=93=9C=EB=B0=94?= =?UTF-8?q?=EB=A5=BC=20=EA=B0=90=EC=8B=B8=EB=8A=94=20Container=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/session/SessionSidebar.tsx | 12 ++----- .../SessionSidebar/SidebarContainer.tsx | 32 +++++++++++++++++++ frontend/src/pages/SessionPage.tsx | 19 ++++++----- 3 files changed, 46 insertions(+), 17 deletions(-) create mode 100644 frontend/src/components/session/SessionSidebar/SidebarContainer.tsx diff --git a/frontend/src/components/session/SessionSidebar.tsx b/frontend/src/components/session/SessionSidebar.tsx index fe1d22c6..524175d7 100644 --- a/frontend/src/components/session/SessionSidebar.tsx +++ b/frontend/src/components/session/SessionSidebar.tsx @@ -8,7 +8,6 @@ import useToast from "../../hooks/useToast"; import { TbCrown } from "react-icons/tb"; import { SESSION_EMIT_EVENT } from "@/constants/WebSocket/SessionEvent.ts"; import { Question } from "@hooks/type/session"; -import { BsLayoutSidebarReverse } from "react-icons/bs"; interface ParticipantsData { nickname: string; @@ -71,15 +70,10 @@ const SessionSidebar = ({ return (
-
- -
diff --git a/frontend/src/components/session/SessionSidebar/SidebarContainer.tsx b/frontend/src/components/session/SessionSidebar/SidebarContainer.tsx new file mode 100644 index 00000000..6b1cf96e --- /dev/null +++ b/frontend/src/components/session/SessionSidebar/SidebarContainer.tsx @@ -0,0 +1,32 @@ +import { ReactNode, useState } from "react"; +import { FaArrowLeft } from "react-icons/fa"; + +interface SidebarContainerProps { + children: ReactNode; +} + +const SidebarContainer = ({ children }: SidebarContainerProps) => { + const [isCollapsed, setIsCollapsed] = useState(false); + return ( +
+
+ {children} +
+ +
+ ); +}; + +export default SidebarContainer; diff --git a/frontend/src/pages/SessionPage.tsx b/frontend/src/pages/SessionPage.tsx index 6c1b9eb6..5a324fa1 100644 --- a/frontend/src/pages/SessionPage.tsx +++ b/frontend/src/pages/SessionPage.tsx @@ -8,6 +8,7 @@ import SessionHeader from "@components/session/SessionHeader"; import { useEffect } from "react"; import useToast from "@hooks/useToast.ts"; import { STUDY_EMIT_EVENT } from "@/constants/WebSocket/StudyEvent.ts"; +import SidebarContainer from "@components/session/SessionSidebar/SidebarContainer.tsx"; const SessionPage = () => { const { sessionId } = useParams(); @@ -176,14 +177,16 @@ const SessionPage = () => { stopStudySession={stopStudySession} />
- + + +
); From 37f234c7894261e3bcb462dc6f3d55c5a500a219 Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Thu, 28 Nov 2024 20:44:44 +0900 Subject: [PATCH 69/86] =?UTF-8?q?chore:=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=ED=81=B0=20=EB=8B=A8=EC=9C=84=EB=A1=9C=20=ED=8F=B4?= =?UTF-8?q?=EB=8D=94=EB=A1=9C=20=EA=B5=AC=EB=B6=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/session/{ => Sidebar}/SessionSidebar.tsx | 6 +++--- .../{SessionSidebar => Sidebar}/SidebarContainer.tsx | 0 .../src/components/session/{ => Toolbar}/SessionToolbar.tsx | 0 frontend/src/pages/SessionPage.tsx | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) rename frontend/src/components/session/{ => Sidebar}/SessionSidebar.tsx (97%) rename frontend/src/components/session/{SessionSidebar => Sidebar}/SidebarContainer.tsx (100%) rename frontend/src/components/session/{ => Toolbar}/SessionToolbar.tsx (100%) diff --git a/frontend/src/components/session/SessionSidebar.tsx b/frontend/src/components/session/Sidebar/SessionSidebar.tsx similarity index 97% rename from frontend/src/components/session/SessionSidebar.tsx rename to frontend/src/components/session/Sidebar/SessionSidebar.tsx index 524175d7..5bcc1b95 100644 --- a/frontend/src/components/session/SessionSidebar.tsx +++ b/frontend/src/components/session/Sidebar/SessionSidebar.tsx @@ -1,10 +1,10 @@ import { FaClipboardList, FaFolder } from "react-icons/fa"; import { FaUserGroup } from "react-icons/fa6"; -import useModalStore from "../../stores/useModalStore"; -import Modal from "../common/Modal"; +import useModalStore from "@stores/useModalStore.ts"; +import Modal from "../../common/Modal"; import { useNavigate } from "react-router-dom"; import { Socket } from "socket.io-client"; -import useToast from "../../hooks/useToast"; +import useToast from "@hooks/useToast.ts"; import { TbCrown } from "react-icons/tb"; import { SESSION_EMIT_EVENT } from "@/constants/WebSocket/SessionEvent.ts"; import { Question } from "@hooks/type/session"; diff --git a/frontend/src/components/session/SessionSidebar/SidebarContainer.tsx b/frontend/src/components/session/Sidebar/SidebarContainer.tsx similarity index 100% rename from frontend/src/components/session/SessionSidebar/SidebarContainer.tsx rename to frontend/src/components/session/Sidebar/SidebarContainer.tsx diff --git a/frontend/src/components/session/SessionToolbar.tsx b/frontend/src/components/session/Toolbar/SessionToolbar.tsx similarity index 100% rename from frontend/src/components/session/SessionToolbar.tsx rename to frontend/src/components/session/Toolbar/SessionToolbar.tsx diff --git a/frontend/src/pages/SessionPage.tsx b/frontend/src/pages/SessionPage.tsx index 5a324fa1..26c9de8d 100644 --- a/frontend/src/pages/SessionPage.tsx +++ b/frontend/src/pages/SessionPage.tsx @@ -1,14 +1,14 @@ import VideoContainer from "@components/session/VideoContainer.tsx"; import { useParams } from "react-router-dom"; -import SessionSidebar from "@components/session/SessionSidebar.tsx"; -import SessionToolbar from "@components/session/SessionToolbar.tsx"; +import SessionSidebar from "@components/session/Sidebar/SessionSidebar.tsx"; +import SessionToolbar from "@components/session/Toolbar/SessionToolbar.tsx"; import { useSession } from "@hooks/session/useSession"; import useSocket from "@hooks/useSocket"; import SessionHeader from "@components/session/SessionHeader"; import { useEffect } from "react"; import useToast from "@hooks/useToast.ts"; import { STUDY_EMIT_EVENT } from "@/constants/WebSocket/StudyEvent.ts"; -import SidebarContainer from "@components/session/SessionSidebar/SidebarContainer.tsx"; +import SidebarContainer from "@components/session/Sidebar/SidebarContainer.tsx"; const SessionPage = () => { const { sessionId } = useParams(); From 4834f7824ff7162f1fcead6dbb5ac48d9e5ce824 Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Thu, 28 Nov 2024 20:52:19 +0900 Subject: [PATCH 70/86] =?UTF-8?q?refactor:=20=ED=98=B8=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=A7=8C=20=EC=82=AC=EC=9A=A9=ED=95=A0=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EB=8A=94=20=EB=8F=84=EA=B5=AC=EC=99=80=20=EC=9D=BC=EB=B0=98=20?= =?UTF-8?q?=EC=9C=A0=EC=A0=80=EB=93=A4=EB=8F=84=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8A=94=20=EB=8F=84=EA=B5=AC?= =?UTF-8?q?=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=EB=A5=BC=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 컴포넌트 폴더에 하위 폴더를 만들어 구분 --- .../session/Toolbar/CommonTools.tsx | 95 +++++++++++++ .../session/Toolbar/HostOnlyTools.tsx | 71 ++++++++++ .../session/Toolbar/SessionToolbar.tsx | 130 +++--------------- 3 files changed, 187 insertions(+), 109 deletions(-) create mode 100644 frontend/src/components/session/Toolbar/CommonTools.tsx create mode 100644 frontend/src/components/session/Toolbar/HostOnlyTools.tsx diff --git a/frontend/src/components/session/Toolbar/CommonTools.tsx b/frontend/src/components/session/Toolbar/CommonTools.tsx new file mode 100644 index 00000000..3badd676 --- /dev/null +++ b/frontend/src/components/session/Toolbar/CommonTools.tsx @@ -0,0 +1,95 @@ +import { + BsCameraVideo, + BsCameraVideoOff, + BsHandThumbsUp, + BsMic, + BsMicMute, +} from "react-icons/bs"; + +interface CommonToolsProps { + handleVideoToggle: () => void; + handleMicToggle: () => void; + emitReaction: (reactionType: string) => void; + userVideoDevices: MediaDeviceInfo[]; + userAudioDevices: MediaDeviceInfo[]; + setSelectedVideoDeviceId: (deviceId: string) => void; + setSelectedAudioDeviceId: (deviceId: string) => void; + isVideoOn: boolean; + isMicOn: boolean; + videoLoading: boolean; +} +const CommonTools = ({ + handleVideoToggle, + handleMicToggle, + emitReaction, + userVideoDevices, + userAudioDevices, + setSelectedVideoDeviceId, + setSelectedAudioDeviceId, + isVideoOn, + isMicOn, + videoLoading, +}: CommonToolsProps) => { + return ( + <> +
+ + + + + +
+ + ); +}; + +export default CommonTools; diff --git a/frontend/src/components/session/Toolbar/HostOnlyTools.tsx b/frontend/src/components/session/Toolbar/HostOnlyTools.tsx new file mode 100644 index 00000000..ce3d20eb --- /dev/null +++ b/frontend/src/components/session/Toolbar/HostOnlyTools.tsx @@ -0,0 +1,71 @@ +import { FaAngleLeft, FaAngleRight } from "react-icons/fa6"; + +// 툴바에서 호스트만 사용가능 도구들 분리 +interface HostOnlyToolsProps { + isHost: boolean; + isInProgress: boolean; + stopStudySession: () => void; + startStudySession: () => void; + requestChangeIndex: (type: "next" | "prev") => void; +} +const HostOnlyTools = ({ + isHost, + isInProgress, + stopStudySession, + startStudySession, + requestChangeIndex, +}: HostOnlyToolsProps) => { + return ( + isHost && ( + <> + {isInProgress ? ( +
+ +
+ ) : ( +
+ +
+ )} + {isInProgress && ( +
+ + +
+ )} + + ) + ); +}; + +export default HostOnlyTools; diff --git a/frontend/src/components/session/Toolbar/SessionToolbar.tsx b/frontend/src/components/session/Toolbar/SessionToolbar.tsx index b686b82e..4a6a8f10 100644 --- a/frontend/src/components/session/Toolbar/SessionToolbar.tsx +++ b/frontend/src/components/session/Toolbar/SessionToolbar.tsx @@ -1,11 +1,5 @@ -import { FaAngleLeft, FaAngleRight } from "react-icons/fa6"; -import { - BsCameraVideo, - BsCameraVideoOff, - BsMic, - BsMicMute, - BsHandThumbsUp, -} from "react-icons/bs"; +import HostOnlyTools from "@components/session/Toolbar/HostOnlyTools.tsx"; +import CommonTools from "@components/session/Toolbar/CommonTools.tsx"; interface Props { requestChangeIndex: ( @@ -50,107 +44,25 @@ const SessionToolbar = ({ "session-footer h-16 inline-flex w-full justify-center items-center border-t px-6 shrink-0" } > -
- - - - - -
- {isHost && isInProgress ? ( -
- -
- ) : ( -
- -
- )} - {isHost && isInProgress && ( -
- - -
- )} + +
); }; From e1c3588f9239bd51a5c258a93db5381bfac7f6ce Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Thu, 28 Nov 2024 20:55:08 +0900 Subject: [PATCH 71/86] =?UTF-8?q?feat:=20=EC=8A=A4=ED=84=B0=EB=94=94=20?= =?UTF-8?q?=EC=8B=9C=EC=9E=91=ED=95=98=EB=A9=B4=20=EC=8B=9C=EC=9E=91=20?= =?UTF-8?q?=EC=9D=B8=EB=94=94=EC=BC=80=EC=9D=B4=ED=84=B0=EB=A5=BC=20?= =?UTF-8?q?=EB=9D=84=EC=9A=B0=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/session/SessionHeader.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/session/SessionHeader.tsx b/frontend/src/components/session/SessionHeader.tsx index 71e2ccda..9fb85966 100644 --- a/frontend/src/components/session/SessionHeader.tsx +++ b/frontend/src/components/session/SessionHeader.tsx @@ -41,11 +41,14 @@ const SessionHeader = ({ `(${participantsCount} / ${roomMetadata.maxParticipants})`} {roomMetadata.inProgress ? ( - + - 스터디 진행 중 {uptime}초 + 스터디 진행 중 + {uptime}초 ) : ( From dde42e08f0761a25f00f3cd97816ca1d1432e11c Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Thu, 28 Nov 2024 21:06:58 +0900 Subject: [PATCH 72/86] =?UTF-8?q?feat:=20=EB=8B=A4=EC=9D=8C=20=EC=A7=88?= =?UTF-8?q?=EB=AC=B8,=20=EC=9D=B4=EC=A0=84=20=EC=A7=88=EB=AC=B8=20?= =?UTF-8?q?=EB=88=84=EB=A5=B4=EA=B3=A0=202=EC=B4=88=20=ED=9B=84=EC=97=90?= =?UTF-8?q?=20=ED=99=9C=EC=84=B1=ED=99=94=20=EB=90=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../session/Toolbar/HostOnlyTools.tsx | 54 ++++++++++++++++--- frontend/src/index.css | 6 +++ 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/session/Toolbar/HostOnlyTools.tsx b/frontend/src/components/session/Toolbar/HostOnlyTools.tsx index ce3d20eb..dcb5faf1 100644 --- a/frontend/src/components/session/Toolbar/HostOnlyTools.tsx +++ b/frontend/src/components/session/Toolbar/HostOnlyTools.tsx @@ -1,4 +1,5 @@ import { FaAngleLeft, FaAngleRight } from "react-icons/fa6"; +import { useEffect, useState } from "react"; // 툴바에서 호스트만 사용가능 도구들 분리 interface HostOnlyToolsProps { @@ -15,6 +16,21 @@ const HostOnlyTools = ({ startStudySession, requestChangeIndex, }: HostOnlyToolsProps) => { + const [changeCooldown, setChangeCooldown] = useState(false); + const COOLDOWN_TIME = 2000; + + useEffect(() => { + if (!changeCooldown) return; + + const timeout = setTimeout(() => { + setChangeCooldown(false); + }, COOLDOWN_TIME); + + return () => { + clearTimeout(timeout); + }; + }, [changeCooldown]); + return ( isHost && ( <> @@ -24,7 +40,9 @@ const HostOnlyTools = ({ className={ "bg-transparent rounded-xl border h-10 px-3 py-2 text-medium-xs " } - onClick={stopStudySession} + onClick={() => { + stopStudySession(); + }} > 스터디 종료하기 @@ -35,7 +53,9 @@ const HostOnlyTools = ({ className={ "bg-transparent rounded-xl border h-10 px-3 py-2 text-medium-xs " } - onClick={startStudySession} + onClick={() => { + startStudySession(); + }} > 스터디 시작하기 @@ -44,22 +64,44 @@ const HostOnlyTools = ({ {isInProgress && (
)} diff --git a/frontend/src/index.css b/frontend/src/index.css index c6b4bbb4..31fb23af 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -56,6 +56,12 @@ } } +.animate-progress { + animation: expand 2s ease; + animation-iteration-count: 1; + animation-timing-function: linear; +} + .revealExpand { animation: revealExpand 0.2s ease; animation-iteration-count: 1; From 6e96862015b0ae9523f1e77a3913fd9107b62cdd Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Thu, 28 Nov 2024 21:09:36 +0900 Subject: [PATCH 73/86] =?UTF-8?q?fix:=20=EC=A7=88=EB=AC=B8=20=EB=B2=88?= =?UTF-8?q?=ED=98=B8=EA=B0=80=20=EC=9E=98=EB=AA=BB=EB=90=98=EC=96=B4?= =?UTF-8?q?=EC=9E=88=EB=8D=98=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/session/Sidebar/SessionSidebar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/session/Sidebar/SessionSidebar.tsx b/frontend/src/components/session/Sidebar/SessionSidebar.tsx index 5bcc1b95..3b9e1606 100644 --- a/frontend/src/components/session/Sidebar/SessionSidebar.tsx +++ b/frontend/src/components/session/Sidebar/SessionSidebar.tsx @@ -89,7 +89,7 @@ const SessionSidebar = ({ {currentIndex >= 0 ? (

- Q{questionList[currentIndex].index}.{" "} + Q{questionList[currentIndex].index + 1}.{" "} {questionList[currentIndex].content}

From 431d9bc0b0823d347216631a2d162f7935f87649 Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Thu, 28 Nov 2024 21:09:54 +0900 Subject: [PATCH 74/86] =?UTF-8?q?fix:=20=EB=B2=84=ED=8A=BC=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EB=A0=88=EC=8A=A4=EB=B0=94=EA=B0=80=20?= =?UTF-8?q?=EC=98=A4=EB=B2=84=ED=94=8C=EB=A1=9C=EC=9A=B0=20=EB=90=98?= =?UTF-8?q?=EB=8D=98=20=ED=98=84=EC=83=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/session/Toolbar/HostOnlyTools.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/session/Toolbar/HostOnlyTools.tsx b/frontend/src/components/session/Toolbar/HostOnlyTools.tsx index dcb5faf1..c3e40ea0 100644 --- a/frontend/src/components/session/Toolbar/HostOnlyTools.tsx +++ b/frontend/src/components/session/Toolbar/HostOnlyTools.tsx @@ -69,7 +69,7 @@ const HostOnlyTools = ({ setChangeCooldown(true); }} className={ - "relative inline-flex items-center bg-transparent rounded-xl border h-10 px-3 py-2 text-medium-xs disabled:opacity-50" + "relative inline-flex items-center bg-transparent rounded-xl border h-10 px-3 py-2 text-medium-xs disabled:opacity-50 overflow-hidden" } aria-label={"이전 질문 버튼"} disabled={changeCooldown} From 999f345e3fa7a3951ae3a8ce9745bb7e5983add0 Mon Sep 17 00:00:00 2001 From: yiseungyun Date: Thu, 28 Nov 2024 21:16:16 +0900 Subject: [PATCH 75/86] =?UTF-8?q?refactor:=20api=20=EB=82=B4=EB=B6=80=20?= =?UTF-8?q?=ED=8F=B4=EB=8D=94=EB=AA=85=20api=20=EC=97=94=EB=93=9C=ED=8F=AC?= =?UTF-8?q?=EC=9D=B8=ED=8A=B8=EC=99=80=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../createQuestionList.ts | 0 .../deleteQuestionList.ts | 0 .../editQuestionMetadata.ts | 0 .../api/question-list/getMyQuestionList.ts | 23 +++++++++++++++++++ .../api/question-list/getQuestionContent.ts | 15 ++++++++++++ .../getQuestionList.ts | 3 ++- .../api/question-list/getScrapQuestionList.ts | 23 +++++++++++++++++++ .../src/api/questions/getQuestionContent.ts | 15 ------------ 8 files changed, 63 insertions(+), 16 deletions(-) rename frontend/src/api/{questions => question-list}/createQuestionList.ts (100%) rename frontend/src/api/{questions => question-list}/deleteQuestionList.ts (100%) rename frontend/src/api/{questions => question-list}/editQuestionMetadata.ts (100%) create mode 100644 frontend/src/api/question-list/getMyQuestionList.ts create mode 100644 frontend/src/api/question-list/getQuestionContent.ts rename frontend/src/api/{questions => question-list}/getQuestionList.ts (87%) create mode 100644 frontend/src/api/question-list/getScrapQuestionList.ts delete mode 100644 frontend/src/api/questions/getQuestionContent.ts diff --git a/frontend/src/api/questions/createQuestionList.ts b/frontend/src/api/question-list/createQuestionList.ts similarity index 100% rename from frontend/src/api/questions/createQuestionList.ts rename to frontend/src/api/question-list/createQuestionList.ts diff --git a/frontend/src/api/questions/deleteQuestionList.ts b/frontend/src/api/question-list/deleteQuestionList.ts similarity index 100% rename from frontend/src/api/questions/deleteQuestionList.ts rename to frontend/src/api/question-list/deleteQuestionList.ts diff --git a/frontend/src/api/questions/editQuestionMetadata.ts b/frontend/src/api/question-list/editQuestionMetadata.ts similarity index 100% rename from frontend/src/api/questions/editQuestionMetadata.ts rename to frontend/src/api/question-list/editQuestionMetadata.ts diff --git a/frontend/src/api/question-list/getMyQuestionList.ts b/frontend/src/api/question-list/getMyQuestionList.ts new file mode 100644 index 00000000..794fbcfd --- /dev/null +++ b/frontend/src/api/question-list/getMyQuestionList.ts @@ -0,0 +1,23 @@ +import axios from "axios"; + +interface QuestionListProps { + page: number; + limit: number; +} + +const getMyQuestionList = async ({ page, limit }: QuestionListProps) => { + const response = await axios.post("/api/question-list/my", { + params: { + page, + limit, + }, + }); + + if (!response.data.success) { + throw new Error(response.data.message); + } + + return response.data.myQuestionLists; +}; + +export default getMyQuestionList; diff --git a/frontend/src/api/question-list/getQuestionContent.ts b/frontend/src/api/question-list/getQuestionContent.ts new file mode 100644 index 00000000..9fdc4410 --- /dev/null +++ b/frontend/src/api/question-list/getQuestionContent.ts @@ -0,0 +1,15 @@ +import axios from "axios"; + +const getQuestionContent = async (questionListId: number) => { + const response = await axios.post("/api/question-list/contents", { + questionListId, + }); + + if (!response.data.success) { + throw new Error(response.data.message); + } + + return response.data.questionListContents; +}; + +export default getQuestionContent; diff --git a/frontend/src/api/questions/getQuestionList.ts b/frontend/src/api/question-list/getQuestionList.ts similarity index 87% rename from frontend/src/api/questions/getQuestionList.ts rename to frontend/src/api/question-list/getQuestionList.ts index f5d87969..7f918e47 100644 --- a/frontend/src/api/questions/getQuestionList.ts +++ b/frontend/src/api/question-list/getQuestionList.ts @@ -12,5 +12,6 @@ export const getQuestionList = async ({ page, limit }: QuestionListProps) => { limit, }, }); - return response.data; + + return response.data.allQuestionLists; }; diff --git a/frontend/src/api/question-list/getScrapQuestionList.ts b/frontend/src/api/question-list/getScrapQuestionList.ts new file mode 100644 index 00000000..4737b8fe --- /dev/null +++ b/frontend/src/api/question-list/getScrapQuestionList.ts @@ -0,0 +1,23 @@ +import axios from "axios"; + +interface QuestionListProps { + page: number; + limit: number; +} + +const getScrapQuestionList = async ({ page, limit }: QuestionListProps) => { + const response = await axios.post("/api/question-list/scrap", { + params: { + page, + limit, + }, + }); + + if (!response.data.success) { + throw new Error(response.data.message); + } + + return response.data.questionLists; +}; + +export default getScrapQuestionList; diff --git a/frontend/src/api/questions/getQuestionContent.ts b/frontend/src/api/questions/getQuestionContent.ts deleted file mode 100644 index 396bc7bc..00000000 --- a/frontend/src/api/questions/getQuestionContent.ts +++ /dev/null @@ -1,15 +0,0 @@ -import axios from "axios"; - -const getQuestionContent = async (questionListId: number) => { - const { data } = await axios.post("/api/question-list/contents", { - questionListId, - }); - - if (!data.success) { - throw new Error(data.message); - } - - return data.data.questionListContents; -}; - -export default getQuestionContent; From c0b40b70133483d82de95689792a124e194fc251 Mon Sep 17 00:00:00 2001 From: yiseungyun Date: Thu, 28 Nov 2024 21:16:50 +0900 Subject: [PATCH 76/86] =?UTF-8?q?feat:=20tanstack=20query=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=20=EC=BD=94=EB=93=9C=20=ED=9B=85=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/hooks/api/useCreateQuestionList.ts | 2 +- .../src/hooks/api/useDeleteQuestionList.ts | 2 +- .../src/hooks/api/useGetMyQuestionList.ts | 23 +++++++++++++++++++ .../src/hooks/api/useGetQuestionContent.ts | 2 +- frontend/src/hooks/api/useGetQuestionList.ts | 2 +- .../src/hooks/api/useGetScrapQuestionList.ts | 23 +++++++++++++++++++ 6 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 frontend/src/hooks/api/useGetMyQuestionList.ts create mode 100644 frontend/src/hooks/api/useGetScrapQuestionList.ts diff --git a/frontend/src/hooks/api/useCreateQuestionList.ts b/frontend/src/hooks/api/useCreateQuestionList.ts index a09c82b9..0b474c46 100644 --- a/frontend/src/hooks/api/useCreateQuestionList.ts +++ b/frontend/src/hooks/api/useCreateQuestionList.ts @@ -1,5 +1,5 @@ import { useMutation } from "@tanstack/react-query"; -import { createQuestionList } from "@/api/questions/createQuestionList"; +import { createQuestionList } from "@/api/question-list/createQuestionList"; import useToast from "@hooks/useToast.ts"; import { useNavigate } from "react-router-dom"; diff --git a/frontend/src/hooks/api/useDeleteQuestionList.ts b/frontend/src/hooks/api/useDeleteQuestionList.ts index 2c17bafa..2cae9d31 100644 --- a/frontend/src/hooks/api/useDeleteQuestionList.ts +++ b/frontend/src/hooks/api/useDeleteQuestionList.ts @@ -1,6 +1,6 @@ import useToast from "../useToast"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { deleteQuestionList } from "@/api/questions/deleteQuestionList"; +import { deleteQuestionList } from "@/api/question-list/deleteQuestionList"; interface UseDeleteQuestionList { page: Number; diff --git a/frontend/src/hooks/api/useGetMyQuestionList.ts b/frontend/src/hooks/api/useGetMyQuestionList.ts new file mode 100644 index 00000000..e3402e6a --- /dev/null +++ b/frontend/src/hooks/api/useGetMyQuestionList.ts @@ -0,0 +1,23 @@ +import getMyQuestionList from "@/api/question-list/getMyQuestionList"; +import { useQuery } from "@tanstack/react-query"; + +interface UseGetQuestionListProps { + page: number; + limit: number; +} + +export const useGetMyQuestionList = ({ + page, + limit, +}: UseGetQuestionListProps) => { + const { data, isLoading, error } = useQuery({ + queryKey: ["myQuestions", page, limit], + queryFn: () => getMyQuestionList({ page, limit }), + }); + + return { + questions: data, + isLoading, + error, + }; +}; diff --git a/frontend/src/hooks/api/useGetQuestionContent.ts b/frontend/src/hooks/api/useGetQuestionContent.ts index 0fb1967b..57a570ae 100644 --- a/frontend/src/hooks/api/useGetQuestionContent.ts +++ b/frontend/src/hooks/api/useGetQuestionContent.ts @@ -1,4 +1,4 @@ -import getQuestionContent from "@/api/questions/getQuestionContent"; +import getQuestionContent from "@/api/question-list/getQuestionContent"; import { useQuery } from "@tanstack/react-query"; interface QuestionContent { diff --git a/frontend/src/hooks/api/useGetQuestionList.ts b/frontend/src/hooks/api/useGetQuestionList.ts index b10a2991..4968950d 100644 --- a/frontend/src/hooks/api/useGetQuestionList.ts +++ b/frontend/src/hooks/api/useGetQuestionList.ts @@ -1,4 +1,4 @@ -import { getQuestionList } from "@/api/questions/getQuestionList"; +import { getQuestionList } from "@/api/question-list/getQuestionList"; import { useQuery } from "@tanstack/react-query"; interface UseGetQuestionListProps { diff --git a/frontend/src/hooks/api/useGetScrapQuestionList.ts b/frontend/src/hooks/api/useGetScrapQuestionList.ts new file mode 100644 index 00000000..e4f797e6 --- /dev/null +++ b/frontend/src/hooks/api/useGetScrapQuestionList.ts @@ -0,0 +1,23 @@ +import getScrapQuestionList from "@/api/question-list/getScrapQuestionList"; +import { useQuery } from "@tanstack/react-query"; + +interface UseGetQuestionListProps { + page: number; + limit: number; +} + +export const useGetScrapQuestionList = ({ + page, + limit, +}: UseGetQuestionListProps) => { + const { data, isLoading, error } = useQuery({ + queryKey: ["scrapQuestions", page, limit], + queryFn: () => getScrapQuestionList({ page, limit }), + }); + + return { + questions: data, + isLoading, + error, + }; +}; \ No newline at end of file From 582171dd5f3180a3b4e3ea5e1f27d04b76b68a3b Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Thu, 28 Nov 2024 21:56:36 +0900 Subject: [PATCH 77/86] =?UTF-8?q?refactor:=20study=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=20useStudy=20=ED=9B=85=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/hooks/session/useSession.ts | 11 ++++++ frontend/src/hooks/session/useStudy.ts | 46 ++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 frontend/src/hooks/session/useStudy.ts diff --git a/frontend/src/hooks/session/useSession.ts b/frontend/src/hooks/session/useSession.ts index 7670ff27..85f1d8d1 100644 --- a/frontend/src/hooks/session/useSession.ts +++ b/frontend/src/hooks/session/useSession.ts @@ -11,6 +11,7 @@ import { useSocketEvents } from "./useSocketEvents"; import { Socket } from "socket.io-client"; import { SESSION_EMIT_EVENT } from "@/constants/WebSocket/SessionEvent"; import useAuth from "@hooks/useAuth"; +import useStudy from "@hooks/session/useStudy"; export const useSession = (sessionId: string) => { const { socket } = useSocket(); @@ -67,6 +68,13 @@ export const useSession = (sessionId: string) => { setPeers ); + const { requestChangeIndex, stopStudySession, startStudySession } = useStudy( + socket, + sessionId, + setRoomMetadata, + setPeers + ); + useSocketEvents({ socket, stream, @@ -153,5 +161,8 @@ export const useSession = (sessionId: string) => { emitReaction, videoLoading, peerMediaStatus, + requestChangeIndex, + startStudySession, + stopStudySession, }; }; diff --git a/frontend/src/hooks/session/useStudy.ts b/frontend/src/hooks/session/useStudy.ts new file mode 100644 index 00000000..eff50d71 --- /dev/null +++ b/frontend/src/hooks/session/useStudy.ts @@ -0,0 +1,46 @@ +import { STUDY_EMIT_EVENT } from "@/constants/WebSocket/StudyEvent.ts"; + +const useStudy = (socket, isHost, roomMetadata, sessionId) => { + const requestChangeIndex = ( + type: "next" | "prev" | "current" | "move", + index?: number + ) => { + if (socket) { + if (isHost && roomMetadata) { + switch (type) { + case "next": + socket.emit(STUDY_EMIT_EVENT.NEXT, { roomId: sessionId }); + break; + case "prev": + socket.emit(STUDY_EMIT_EVENT.INDEX, { + roomId: sessionId, + index: roomMetadata.currentIndex - 1, + }); + break; + case "current": + socket.emit(STUDY_EMIT_EVENT.CURRENT, { roomId: sessionId }); + break; + case "move": + socket.emit(STUDY_EMIT_EVENT.INDEX, { roomId: sessionId, index }); + break; + } + } + } + }; + + const startStudySession = () => { + if (socket) { + socket.emit(STUDY_EMIT_EVENT.START, { roomId: sessionId }); + } + }; + + const stopStudySession = () => { + if (socket) { + socket.emit(STUDY_EMIT_EVENT.STOP, { roomId: sessionId }); + } + }; + + return { requestChangeIndex, startStudySession, stopStudySession }; +}; + +export default useStudy; From 966d140987de3e8594789160d316c5323b49a502 Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Thu, 28 Nov 2024 21:59:49 +0900 Subject: [PATCH 78/86] =?UTF-8?q?refactor:=20uptime=20=EB=B6=84/=EC=B4=88?= =?UTF-8?q?=EB=A1=9C=20=ED=91=9C=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/session/SessionHeader.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/session/SessionHeader.tsx b/frontend/src/components/session/SessionHeader.tsx index 9fb85966..e336ca9e 100644 --- a/frontend/src/components/session/SessionHeader.tsx +++ b/frontend/src/components/session/SessionHeader.tsx @@ -11,12 +11,12 @@ const SessionHeader = ({ roomMetadata, }: SessionHeaderProps) => { const [uptime, setUptime] = useState(0); - + const SECOND = 1000; useEffect(() => { if (!roomMetadata?.inProgress) return; const interval = setInterval(() => { setUptime((prev) => prev + 1); - }, 1000); + }, SECOND); return () => { clearInterval(interval); @@ -31,7 +31,6 @@ const SessionHeader = ({ > {roomMetadata?.title ? ( <> - {" "} {roomMetadata?.category} @@ -48,7 +47,9 @@ const SessionHeader = ({ } >
스터디 진행 중 - {uptime}초 + + {Math.floor(uptime / 60)}분 {uptime % 60}초 +
) : ( From cac93ce9bb2d2b8340c2d4b0e7ace7f1515176e9 Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Thu, 28 Nov 2024 22:00:51 +0900 Subject: [PATCH 79/86] =?UTF-8?q?refactor:=20useStudy=20->=20useSession?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B0=9B=EC=95=84=EC=98=A8=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/SessionPage.tsx | 43 +++--------------------------- 1 file changed, 3 insertions(+), 40 deletions(-) diff --git a/frontend/src/pages/SessionPage.tsx b/frontend/src/pages/SessionPage.tsx index 26c9de8d..cc473159 100644 --- a/frontend/src/pages/SessionPage.tsx +++ b/frontend/src/pages/SessionPage.tsx @@ -7,7 +7,6 @@ import useSocket from "@hooks/useSocket"; import SessionHeader from "@components/session/SessionHeader"; import { useEffect } from "react"; import useToast from "@hooks/useToast.ts"; -import { STUDY_EMIT_EVENT } from "@/constants/WebSocket/StudyEvent.ts"; import SidebarContainer from "@components/session/Sidebar/SidebarContainer.tsx"; const SessionPage = () => { @@ -42,47 +41,11 @@ const SessionPage = () => { emitReaction, videoLoading, peerMediaStatus, + requestChangeIndex, + startStudySession, + stopStudySession, } = useSession(sessionId!); - const requestChangeIndex = ( - type: "next" | "prev" | "current" | "move", - index?: number - ) => { - if (socket) { - if (isHost && roomMetadata) { - switch (type) { - case "next": - socket.emit(STUDY_EMIT_EVENT.NEXT, { roomId: sessionId }); - break; - case "prev": - socket.emit(STUDY_EMIT_EVENT.INDEX, { - roomId: sessionId, - index: roomMetadata.currentIndex - 1, - }); - break; - case "current": - socket.emit(STUDY_EMIT_EVENT.CURRENT, { roomId: sessionId }); - break; - case "move": - socket.emit(STUDY_EMIT_EVENT.INDEX, { roomId: sessionId, index }); - break; - } - } - } - }; - - const startStudySession = () => { - if (socket) { - socket.emit(STUDY_EMIT_EVENT.START, { roomId: sessionId }); - } - }; - - const stopStudySession = () => { - if (socket) { - socket.emit(STUDY_EMIT_EVENT.STOP, { roomId: sessionId }); - } - }; - return (
From d950de1a15a7fe0c5587a87890ab7c548d524739 Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Thu, 28 Nov 2024 22:04:43 +0900 Subject: [PATCH 80/86] =?UTF-8?q?refactor:=20=EC=9D=B4=EC=A0=84=20?= =?UTF-8?q?=EC=A7=88=EB=AC=B8=20=EB=B2=84=ED=8A=BC=EA=B3=BC=20=EB=8B=A4?= =?UTF-8?q?=EC=9D=8C=20=EC=A7=88=EB=AC=B8=20=EB=B2=84=ED=8A=BC=EC=9D=B4=20?= =?UTF-8?q?=EC=A7=88=EB=AC=B8=EC=A7=80=EC=9D=98=20=EC=96=91=20=EB=81=9D?= =?UTF-8?q?=EB=8B=A8=EC=97=90=EC=84=9C=20=EB=B9=84=ED=99=9C=EC=84=B1?= =?UTF-8?q?=ED=99=94=EB=90=98=EB=8F=84=EB=A1=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/session/Toolbar/HostOnlyTools.tsx | 10 ++++++++-- .../src/components/session/Toolbar/SessionToolbar.tsx | 6 ++++++ frontend/src/pages/SessionPage.tsx | 2 ++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/session/Toolbar/HostOnlyTools.tsx b/frontend/src/components/session/Toolbar/HostOnlyTools.tsx index c3e40ea0..72f2ac74 100644 --- a/frontend/src/components/session/Toolbar/HostOnlyTools.tsx +++ b/frontend/src/components/session/Toolbar/HostOnlyTools.tsx @@ -8,6 +8,8 @@ interface HostOnlyToolsProps { stopStudySession: () => void; startStudySession: () => void; requestChangeIndex: (type: "next" | "prev") => void; + currentIndex: number; + maxQuestionLength: number; } const HostOnlyTools = ({ isHost, @@ -15,6 +17,8 @@ const HostOnlyTools = ({ stopStudySession, startStudySession, requestChangeIndex, + currentIndex, + maxQuestionLength, }: HostOnlyToolsProps) => { const [changeCooldown, setChangeCooldown] = useState(false); const COOLDOWN_TIME = 2000; @@ -72,7 +76,7 @@ const HostOnlyTools = ({ "relative inline-flex items-center bg-transparent rounded-xl border h-10 px-3 py-2 text-medium-xs disabled:opacity-50 overflow-hidden" } aria-label={"이전 질문 버튼"} - disabled={changeCooldown} + disabled={changeCooldown || currentIndex === 0} > 이전 질문 {changeCooldown && ( @@ -92,7 +96,9 @@ const HostOnlyTools = ({ "relative inline-flex items-center bg-transparent rounded-xl border h-10 px-3 py-2 text-medium-xs disabled:opacity-50 overflow-hidden" } aria-label={"다음 질문 버튼"} - disabled={changeCooldown} + disabled={ + changeCooldown || currentIndex === maxQuestionLength - 1 + } > 다음 질문 {changeCooldown && ( diff --git a/frontend/src/components/session/Toolbar/SessionToolbar.tsx b/frontend/src/components/session/Toolbar/SessionToolbar.tsx index 4a6a8f10..f7c0ab48 100644 --- a/frontend/src/components/session/Toolbar/SessionToolbar.tsx +++ b/frontend/src/components/session/Toolbar/SessionToolbar.tsx @@ -20,6 +20,8 @@ interface Props { isInProgress: boolean; startStudySession: () => void; stopStudySession: () => void; + currentIndex: number; + maxQuestionLength: number; } const SessionToolbar = ({ requestChangeIndex, @@ -37,6 +39,8 @@ const SessionToolbar = ({ isInProgress, startStudySession, stopStudySession, + currentIndex, + maxQuestionLength, }: Props) => { return (
); diff --git a/frontend/src/pages/SessionPage.tsx b/frontend/src/pages/SessionPage.tsx index cc473159..89d395bf 100644 --- a/frontend/src/pages/SessionPage.tsx +++ b/frontend/src/pages/SessionPage.tsx @@ -138,6 +138,8 @@ const SessionPage = () => { isInProgress={roomMetadata?.inProgress ?? false} startStudySession={startStudySession} stopStudySession={stopStudySession} + currentIndex={roomMetadata?.currentIndex ?? -1} + maxQuestionLength={roomMetadata?.questionListContents.length ?? 0} />
From 276c66eb964c090a62688e1f1f31093aa6559a02 Mon Sep 17 00:00:00 2001 From: yiseungyun Date: Thu, 28 Nov 2024 22:49:02 +0900 Subject: [PATCH 81/86] =?UTF-8?q?fix:=20=EC=84=B8=EC=85=98=20=EB=A7=8C?= =?UTF-8?q?=EB=93=A4=EA=B8=B0=20=ED=8F=BC=20=EB=AA=A8=EB=8B=AC=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SessionForm/ListSelectModal/index.tsx | 33 ++++++++++++++----- .../SessionForm/QuestionListSection/index.tsx | 8 +++-- .../sessions/create/SessionForm/index.tsx | 6 ++-- 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/frontend/src/components/sessions/create/SessionForm/ListSelectModal/index.tsx b/frontend/src/components/sessions/create/SessionForm/ListSelectModal/index.tsx index debe27f0..8598f5bb 100644 --- a/frontend/src/components/sessions/create/SessionForm/ListSelectModal/index.tsx +++ b/frontend/src/components/sessions/create/SessionForm/ListSelectModal/index.tsx @@ -1,16 +1,23 @@ -import { useRef, useState } from "react"; +import { useState } from "react"; import { IoMdClose } from "react-icons/io"; import CategoryTap from "@components/sessions/create/SessionForm/ListSelectModal/CategoryTab"; import SearchBar from "@components/common/SearchBar"; -import useModalStore from "@stores/useModalStore"; -import useModal from "@hooks/useModal"; import QuestionList from "./QuestionList"; import useSessionFormStore from "@stores/useSessionFormStore"; import Pagination from "@components/common/Pagination"; -const ListSelectModal = () => { - const dialogRef = useRef(null); - const { isModalOpen, closeModal } = useModalStore(); +interface UseModalReturn { + dialogRef: React.RefObject; + isOpen: boolean; + openModal: () => void; + closeModal: () => void; +} + +interface ModalProps { + modal: UseModalReturn; +} + +const ListSelectModal = ({ modal: { dialogRef, closeModal } }: ModalProps) => { const { tab, setTab, setSelectedOpenId } = useSessionFormStore(); const [myListPage, setMyListPage] = useState(1); const [savedListPage, setSavedListPage] = useState(1); @@ -32,16 +39,24 @@ const ListSelectModal = () => { }, }); - useModal({ isModalOpen, dialogRef }); - const closeHandler = () => { closeModal(); setTab("myList"); setSelectedOpenId(-1); }; + const handleMouseDown = (e: React.MouseEvent) => { + if (e.target === dialogRef.current) { + closeModal(); + } + }; + return ( - +

질문 리스트

diff --git a/frontend/src/components/sessions/create/SessionForm/QuestionListSection/index.tsx b/frontend/src/components/sessions/create/SessionForm/QuestionListSection/index.tsx index c1297c96..f4f3131b 100644 --- a/frontend/src/components/sessions/create/SessionForm/QuestionListSection/index.tsx +++ b/frontend/src/components/sessions/create/SessionForm/QuestionListSection/index.tsx @@ -1,10 +1,12 @@ -import useModalStore from "@/stores/useModalStore"; import useSessionFormStore from "@/stores/useSessionFormStore"; import SelectTitle from "@/components/common/SelectTitle"; import { MdOutlineArrowForwardIos } from "react-icons/md"; -const QuestionListSection = () => { - const { openModal } = useModalStore(); +interface ListSectionProps { + openModal: () => void; +} + +const QuestionListSection = ({ openModal }: ListSectionProps) => { const questionTitle = useSessionFormStore((state) => state.questionTitle); return ( diff --git a/frontend/src/components/sessions/create/SessionForm/index.tsx b/frontend/src/components/sessions/create/SessionForm/index.tsx index 8803b327..9266ea2a 100644 --- a/frontend/src/components/sessions/create/SessionForm/index.tsx +++ b/frontend/src/components/sessions/create/SessionForm/index.tsx @@ -14,6 +14,7 @@ import { SESSION_LISTEN_EVENT, } from "@/constants/WebSocket/SessionEvent.ts"; import useAuth from "@hooks/useAuth.ts"; +import useModal from "@/hooks/useModal"; interface RoomCreatedResponse { id?: string; @@ -30,6 +31,7 @@ const SessionForm = () => { const isValid = useSessionFormStore((state) => state.isFormValid()); const navigate = useNavigate(); const toast = useToast(); + const modal = useModal(); const submitHandler = () => { if (!isValid || !socket) { @@ -77,10 +79,10 @@ const SessionForm = () => { return (
- + - +
{ expect(mockSocket.emit).not.toHaveBeenCalled(); }); - it("미디어 스트림 획득 실패 시 에러 처리", async () => { + /*it("미디어 스트림 획득 실패 시 에러 처리", async () => { (useMediaDevices as jest.Mock).mockReturnValue({ ...useMediaDevices(), getMedia: jest.fn().mockResolvedValue(null), @@ -180,7 +180,7 @@ describe("useSession Hook 테스트", () => { "미디어 스트림을 가져오지 못했습니다. 미디어 장치를 확인 후 다시 시도해주세요." ); expect(mockNavigate).toHaveBeenCalledWith("/sessions"); - }); + });*/ }); describe("리액션 기능 테스트", () => { diff --git a/frontend/src/pages/QuestionDetailPage.tsx b/frontend/src/pages/QuestionDetailPage.tsx index 75f4dccd..9bbab5c2 100644 --- a/frontend/src/pages/QuestionDetailPage.tsx +++ b/frontend/src/pages/QuestionDetailPage.tsx @@ -14,9 +14,8 @@ const QuestionDetailPage = () => { const { data: question, isLoading, - isError, error, - } = useGetQuestionContent(questionId!); + } = useGetQuestionContent(Number(questionId!)); useEffect(() => { if (!questionId) { @@ -25,7 +24,7 @@ const QuestionDetailPage = () => { }, [questionId, navigate]); if (isLoading) return
로딩 중
; - if (isError) return
에러가 발생했습니다: {error.message}
; + if (error) return
에러가 발생
; if (!question) return null; return ( From 471f900439e35afcc18c663570aaf35511e0ab62 Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Fri, 29 Nov 2024 00:12:40 +0900 Subject: [PATCH 85/86] =?UTF-8?q?fix:=20=EB=B9=8C=EB=93=9C=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/mypage/CategoryTab/index.tsx | 2 +- frontend/src/components/mypage/QuestionList/index.tsx | 2 +- frontend/src/hooks/session/useSession.ts | 4 ++-- frontend/src/hooks/session/useStudy.ts | 9 ++++++++- frontend/src/pages/MyPage/view/Profile.tsx | 2 +- frontend/src/pages/MyPage/view/ProfileEditModal.tsx | 2 +- frontend/src/pages/MyPage/view/QuestionSection.tsx | 4 ++-- 7 files changed, 16 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/mypage/CategoryTab/index.tsx b/frontend/src/components/mypage/CategoryTab/index.tsx index a4a96457..896ee395 100644 --- a/frontend/src/components/mypage/CategoryTab/index.tsx +++ b/frontend/src/components/mypage/CategoryTab/index.tsx @@ -1,4 +1,4 @@ -import Category from "./Category"; +import Category from "@components/mypage/CategoryTab/Category"; interface TabProps { tab: "myList" | "savedList"; diff --git a/frontend/src/components/mypage/QuestionList/index.tsx b/frontend/src/components/mypage/QuestionList/index.tsx index 619add96..47187583 100644 --- a/frontend/src/components/mypage/QuestionList/index.tsx +++ b/frontend/src/components/mypage/QuestionList/index.tsx @@ -1,5 +1,5 @@ import { useGetMyQuestionList } from "@/hooks/api/useGetMyQuestionList"; -import QuestionItem from "./QuestionItem"; +import QuestionItem from "@components/mypage/QuestionList/QuestionItem"; import { useGetScrapQuestionList } from "@/hooks/api/useGetScrapQuestionList"; interface ListProps { diff --git a/frontend/src/hooks/session/useSession.ts b/frontend/src/hooks/session/useSession.ts index 85f1d8d1..d037cba1 100644 --- a/frontend/src/hooks/session/useSession.ts +++ b/frontend/src/hooks/session/useSession.ts @@ -70,9 +70,9 @@ export const useSession = (sessionId: string) => { const { requestChangeIndex, stopStudySession, startStudySession } = useStudy( socket, + isHost, sessionId, - setRoomMetadata, - setPeers + roomMetadata ); useSocketEvents({ diff --git a/frontend/src/hooks/session/useStudy.ts b/frontend/src/hooks/session/useStudy.ts index eff50d71..d305f8ff 100644 --- a/frontend/src/hooks/session/useStudy.ts +++ b/frontend/src/hooks/session/useStudy.ts @@ -1,6 +1,13 @@ import { STUDY_EMIT_EVENT } from "@/constants/WebSocket/StudyEvent.ts"; +import { Socket } from "socket.io-client"; +import { RoomMetadata } from "@hooks/type/session"; -const useStudy = (socket, isHost, roomMetadata, sessionId) => { +const useStudy = ( + socket: Socket | null, + isHost: boolean, + sessionId: string, + roomMetadata: RoomMetadata | null +) => { const requestChangeIndex = ( type: "next" | "prev" | "current" | "move", index?: number diff --git a/frontend/src/pages/MyPage/view/Profile.tsx b/frontend/src/pages/MyPage/view/Profile.tsx index a265e38a..d955e91a 100644 --- a/frontend/src/pages/MyPage/view/Profile.tsx +++ b/frontend/src/pages/MyPage/view/Profile.tsx @@ -1,5 +1,5 @@ import { MdEdit } from "react-icons/md"; -import ProfileIcon from "@components/MyPage/ProfileIcon"; +import ProfileIcon from "@components/mypage/ProfileIcon"; interface UseModalReturn { dialogRef: React.RefObject; diff --git a/frontend/src/pages/MyPage/view/ProfileEditModal.tsx b/frontend/src/pages/MyPage/view/ProfileEditModal.tsx index 70ad4272..7ad8b5c2 100644 --- a/frontend/src/pages/MyPage/view/ProfileEditModal.tsx +++ b/frontend/src/pages/MyPage/view/ProfileEditModal.tsx @@ -1,6 +1,6 @@ import TitleInput from "@components/common/TitleInput"; import { IoMdClose } from "react-icons/io"; -import ButtonSection from "@components/MyPage/ButtonSection"; +import ButtonSection from "@components/mypage/ButtonSection"; import useAuth from "@hooks/useAuth"; interface UseModalReturn { diff --git a/frontend/src/pages/MyPage/view/QuestionSection.tsx b/frontend/src/pages/MyPage/view/QuestionSection.tsx index 0ad02ed6..0b3e8938 100644 --- a/frontend/src/pages/MyPage/view/QuestionSection.tsx +++ b/frontend/src/pages/MyPage/view/QuestionSection.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; -import CategoryTap from "@components/MyPage/CategoryTab"; -import QuestionList from "@components/MyPage/QuestionList"; +import CategoryTap from "@components/mypage/CategoryTab"; +import QuestionList from "@components/mypage/QuestionList"; import Pagination from "@components/common/Pagination"; import { useGetMyQuestionList } from "@/hooks/api/useGetMyQuestionList"; import { useGetScrapQuestionList } from "@/hooks/api/useGetScrapQuestionList"; From 01f595c928c750a591dd7ffb09c829a46b58b933 Mon Sep 17 00:00:00 2001 From: yiseungyun Date: Fri, 29 Nov 2024 00:32:24 +0900 Subject: [PATCH 86/86] =?UTF-8?q?fix:=20=EC=84=B8=EC=85=98=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EB=AA=A8=EB=8B=AC=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/session/Sidebar/SessionSidebar.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/src/components/session/Sidebar/SessionSidebar.tsx b/frontend/src/components/session/Sidebar/SessionSidebar.tsx index 03376a84..f000d512 100644 --- a/frontend/src/components/session/Sidebar/SessionSidebar.tsx +++ b/frontend/src/components/session/Sidebar/SessionSidebar.tsx @@ -140,9 +140,7 @@ const SessionSidebar = ({