diff --git a/backend/package.json b/backend/package.json index 2c2bc148..bcf121fe 100644 --- a/backend/package.json +++ b/backend/package.json @@ -31,6 +31,7 @@ "@nestjs/typeorm": "^10.0.2", "@nestjs/websockets": "^10.4.6", "@socket.io/redis-adapter": "^8.3.0", + "@types/k6": "^0.54.2", "@types/passport-jwt": "^4.0.1", "axios": "^1.7.7", "class-transformer": "^0.5.1", diff --git a/backend/src/question-list/question-list.service.ts b/backend/src/question-list/question-list.service.ts index d98388a7..505dd18f 100644 --- a/backend/src/question-list/question-list.service.ts +++ b/backend/src/question-list/question-list.service.ts @@ -17,6 +17,7 @@ import { DataSource, In, SelectQueryBuilder } from "typeorm"; import { PaginateMetaDto } from "@/question-list/dto/paginate-meta.dto"; import { PaginateDto } from "@/question-list/dto/paginate.dto"; import { QuestionListDto } from "@/question-list/dto/question-list.dto"; +import { Transactional } from "typeorm-transactional"; @Injectable() export class QuestionListService { @@ -71,6 +72,7 @@ export class QuestionListService { } // 질문 생성 메서드 + @Transactional() async createQuestionList(createQuestionListDto: CreateQuestionListDto) { const { title, contents, categoryNames, isPublic, userId } = createQuestionListDto; @@ -81,37 +83,24 @@ export class QuestionListService { throw new Error("Some category names were not found."); } - const queryRunner = this.dataSource.createQueryRunner(); - await queryRunner.connect(); - await queryRunner.startTransaction(); - - try { - const questionList = new QuestionList(); - questionList.title = title; - questionList.categories = categories; - questionList.isPublic = isPublic; - questionList.userId = userId; - - const createdQuestionList = await queryRunner.manager.save(questionList); + const questionList = new QuestionList(); + questionList.title = title; + questionList.categories = categories; + questionList.isPublic = isPublic; + questionList.userId = userId; - const questions = contents.map((content, index) => { - const question = new Question(); - question.content = content; - question.index = index; - question.questionList = createdQuestionList; + const createdQuestionList = await this.questionListRepository.save(questionList); - return question; - }); - const createdQuestions = await queryRunner.manager.save(questions); + const questions = contents.map((content, index) => { + const question = new Question(); + question.content = content; + question.index = index; + question.questionList = createdQuestionList; - await queryRunner.commitTransaction(); - return { createdQuestionList, createdQuestions }; - } catch (error) { - await queryRunner.rollbackTransaction(); - throw new Error(error.message); - } finally { - await queryRunner.release(); - } + return question; + }); + const createdQuestions = await this.questionRepository.save(questions); + return { createdQuestionList, createdQuestions }; } async getQuestionListContents(questionListId: number, userId: number) { @@ -272,7 +261,8 @@ export class QuestionListService { } async deleteQuestion(deleteQuestionDto: DeleteQuestionDto) { - const { id, questionListId, userId } = deleteQuestionDto; + const { id, userId } = deleteQuestionDto; + const questionListId = await this.questionRepository.getQuestionListIdByQuestionId(id); const question = await this.questionRepository.findOne({ where: { id }, diff --git a/backend/src/question-list/repository/question.respository.ts b/backend/src/question-list/repository/question.respository.ts index 79d57c71..45168946 100644 --- a/backend/src/question-list/repository/question.respository.ts +++ b/backend/src/question-list/repository/question.respository.ts @@ -21,4 +21,13 @@ export class QuestionRepository extends Repository { .orderBy("question.index", "ASC") .getMany(); } + + async getQuestionListIdByQuestionId(questionId: number) { + const result = await this.createQueryBuilder("question") + .select("question.questionListId") + .where("question.id = :questionId", { questionId }) + .getOne(); + + return result ? result.questionListId : null; + } } diff --git a/backend/test/k6/create-question-list-test.ts b/backend/test/k6/create-question-list-test.ts new file mode 100644 index 00000000..ec934df8 --- /dev/null +++ b/backend/test/k6/create-question-list-test.ts @@ -0,0 +1,47 @@ +import http from "k6/http"; +import { check, sleep } from "k6"; + +export const options = { + vus: 100, // 108명의 사용자 + duration: "5s", // 5초 동안 동시 요청을 실행 +}; + +const BASE_URL = "http://localhost:3000"; +const COOKIE = + "accessToken=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsInVzZXJuYW1lIjoiY2FtcGVyXzEwMTYzODkyMyIsImxvZ2luVHlwZSI6ImxvY2FsIiwiaWF0IjoxNzMzMzI2NTIzfQ.OA9o0twIqKUNdTJhnHZkSKytoUvp052VsywKdYvSn30; refreshToken=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzQxOTA1MjM5MDgsImlhdCI6MTczMzMyNjUyMywiYXVkIjoiMSJ9.ZXYCwUL8EOjc1xJ3BJ_bHCLQHa20_qxf1FYqolgxP4I"; + +const questionListData = { + title: "Sample Question List for Test", + contents: [ + "This is 1st question!", + "This is 2nd question!", + "This is 3rd question!", + "This is 4th question!", + "This is 5th question!", + "This is 6th question!", + "This is 7th question!", + "This is 8th question!", + "This is 9th question!", + "This is 10th question!", + ], + categoryNames: ["보안", "네트워크", "자료구조"], + isPublic: true, +}; + +export default function () { + const url = `${BASE_URL}/question-list`; + const params = { + headers: { + Cookie: `${COOKIE}`, + "Content-Type": "application/json", + }, + }; + + const response = http.post(url, JSON.stringify(questionListData), params); + + check(response, { + "is status 200": (r) => r.status === 200, + }); + + sleep(1); +} diff --git a/backend/test/k6/delete-question-test.ts b/backend/test/k6/delete-question-test.ts new file mode 100644 index 00000000..986c96ba --- /dev/null +++ b/backend/test/k6/delete-question-test.ts @@ -0,0 +1,32 @@ +import http from "k6/http"; +import { check, sleep } from "k6"; + +export const options = { + vus: 100, // 100명의 사용자 + duration: "5s", // 5초 동안 동시 요청을 실행 +}; + +const BASE_URL = "http://localhost:3000"; +const COOKIE = + "accessToken=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsInVzZXJuYW1lIjoiY2FtcGVyXzEwMTYzODkyMyIsImxvZ2luVHlwZSI6ImxvY2FsIiwiaWF0IjoxNzMzMzI2NTIzfQ.OA9o0twIqKUNdTJhnHZkSKytoUvp052VsywKdYvSn30; refreshToken=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzQxOTA1MjM5MDgsImlhdCI6MTczMzMyNjUyMywiYXVkIjoiMSJ9.ZXYCwUL8EOjc1xJ3BJ_bHCLQHa20_qxf1FYqolgxP4I"; + +export default function () { + const questionListId = 27; + const questionId = 66176 + (__VU - 1) * 10 + __ITER; + console.log(questionId); + + const url = `${BASE_URL}/question-list/${questionListId}/question/${questionId}`; + const params = { + headers: { + Cookie: `${COOKIE}`, + }, + }; + + const response = http.del(url, null, params); + + check(response, { + "is status 200": (r) => r.status === 200, + }); + + sleep(1); +} diff --git a/frontend/public/preview-logo2.png b/frontend/public/preview-logo2.png new file mode 100644 index 00000000..160a9280 Binary files /dev/null and b/frontend/public/preview-logo2.png differ diff --git a/frontend/src/components/common/Sidebar/Sidebar.tsx b/frontend/src/components/common/Sidebar/Sidebar.tsx index de68d0a7..998d69ab 100644 --- a/frontend/src/components/common/Sidebar/Sidebar.tsx +++ b/frontend/src/components/common/Sidebar/Sidebar.tsx @@ -36,7 +36,7 @@ const Sidebar = () => { return (