From 02bbcbf6447cf4ff6d6c05fdda16409b1929378f Mon Sep 17 00:00:00 2001 From: Devheun Date: Mon, 18 Nov 2024 21:16:33 +0900 Subject: [PATCH 01/48] =?UTF-8?q?fix::=20=EC=8B=9C=EA=B0=84=20=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=EA=B0=95=EC=9D=98=20return=20-=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EC=97=86=EB=8A=94=20=EA=B0=95=EC=9D=98=20(courseDe?= =?UTF-8?q?tails=20=EA=B0=80=20=EC=97=86=EB=8A=94=20=EA=B0=95=EC=9D=98)?= =?UTF-8?q?=EB=8A=94=20timetable=20id=EB=A1=9C=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=ED=95=B4=EB=8F=84=20=EC=A1=B0=ED=9A=8C=EB=90=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8A=94=20=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 --- src/timetable/timetable.service.ts | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/timetable/timetable.service.ts b/src/timetable/timetable.service.ts index 0e25fd34..98d02f5f 100644 --- a/src/timetable/timetable.service.ts +++ b/src/timetable/timetable.service.ts @@ -247,7 +247,7 @@ export class TimetableService { color: timetable.color, timetableName: timetable.timetableName, }; - timetable.timetableCourses.forEach((courseEntry) => { + for (const courseEntry of timetable.timetableCourses) { const { id: courseId, professorName, @@ -256,7 +256,25 @@ export class TimetableService { syllabus, } = courseEntry.course; - courseEntry.course.courseDetails.forEach((detailEntry) => { + if ( + !courseEntry.course.courseDetails || + courseEntry.course.courseDetails.length === 0 + ) { + getTimetableByTimetableIdResponse.courses.push({ + courseId, + professorName, + courseName, + courseCode, + syllabus, + day: null, + startTime: null, + endTime: null, + classroom: null, + }); + continue; + } + + for (const detailEntry of courseEntry.course.courseDetails) { const { day, startTime, endTime, classroom } = detailEntry; // 강의 정보 객체 @@ -271,8 +289,8 @@ export class TimetableService { endTime, classroom, }); - }); - }); + } + } // 스케줄 정보 객체 schedules.forEach((schedule) => { From eebc854cac62edb4fd9ec36330040d73488aa188 Mon Sep 17 00:00:00 2001 From: Devheun Date: Tue, 26 Nov 2024 11:27:22 +0900 Subject: [PATCH 02/48] =?UTF-8?q?mod::=20=EC=8B=9C=EA=B0=84=ED=91=9C=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EC=8B=9C=20=EC=9E=AC=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84=20-=20=ED=95=B4?= =?UTF-8?q?=EB=8B=B9=20=ED=95=99=EA=B8=B0=EC=97=90=20=EC=8B=9C=EA=B0=84?= =?UTF-8?q?=ED=91=9C=EA=B0=80=20=ED=95=98=EB=82=98=EB=A7=8C=20=EC=A1=B4?= =?UTF-8?q?=EC=9E=AC=ED=95=A0=20=EB=95=8C=20=EA=B7=B8=20=EC=8B=9C=EA=B0=84?= =?UTF-8?q?=ED=91=9C=EB=A5=BC=20=EC=82=AD=EC=A0=9C=ED=95=98=EB=A9=B4=20?= =?UTF-8?q?=ED=95=B4=EB=8B=B9=20=ED=95=99=EA=B8=B0=EC=97=90=20=EC=83=88?= =?UTF-8?q?=EB=A1=9C=EC=9A=B4=20=EC=8B=9C=EA=B0=84=ED=91=9C=EB=A5=BC=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20-=20=EA=B7=B8=20=EC=9D=B4=EC=99=B8?= =?UTF-8?q?=EC=9D=98=20=EC=83=81=ED=99=A9=EC=97=90=EC=84=9C=EB=8A=94=20?= =?UTF-8?q?=EC=83=88=EB=A1=9C=EC=9A=B4=20=EC=8B=9C=EA=B0=84=ED=91=9C=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20X?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 32 +++++++++++-------- .../dto/delete-timetable-response.dto.ts | 14 ++++++++ src/timetable/timetable.service.ts | 24 ++++++++++++-- 3 files changed, 54 insertions(+), 16 deletions(-) create mode 100644 src/timetable/dto/delete-timetable-response.dto.ts diff --git a/package-lock.json b/package-lock.json index 78076d85..84a6f2f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5909,13 +5909,14 @@ } }, "node_modules/@nestjs/platform-express": { - "version": "10.4.4", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.4.tgz", - "integrity": "sha512-y52q1MxhbHaT3vAgWd08RgiYon0lJgtTa8U6g6gV0KI0IygwZhDQFJVxnrRDUdxQGIP5CKHmfQu3sk9gTNFoEA==", + "version": "10.4.8", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.8.tgz", + "integrity": "sha512-bDz6wQD9LzGeK6uAAFv9l9AbrpyPwHStNObL8J95HBAXJesOblVlQMBAhdfci1YVMQUfOc36qq0IpRSa1II9Mg==", + "license": "MIT", "dependencies": { "body-parser": "1.20.3", "cors": "2.8.5", - "express": "4.21.0", + "express": "4.21.1", "multer": "1.4.4-lts.1", "tslib": "2.7.0" }, @@ -10556,9 +10557,10 @@ "dev": true }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -10662,9 +10664,10 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -11370,16 +11373,17 @@ } }, "node_modules/express": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", - "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", diff --git a/src/timetable/dto/delete-timetable-response.dto.ts b/src/timetable/dto/delete-timetable-response.dto.ts new file mode 100644 index 00000000..b38cb5d2 --- /dev/null +++ b/src/timetable/dto/delete-timetable-response.dto.ts @@ -0,0 +1,14 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { CommonDeleteResponseDto } from './common-delete-response.dto'; + +export class DeleteTimetableResponseDto extends CommonDeleteResponseDto { + constructor(deleted: boolean, timetableId?: number) { + super(deleted); + this.createdTimetableId = timetableId; + } + + @ApiPropertyOptional({ + description: '시간표 삭제 후 추가로 기본 시간표 생성될 때의 시간표 id', + }) + createdTimetableId?: number; +} diff --git a/src/timetable/timetable.service.ts b/src/timetable/timetable.service.ts index 0e25fd34..ed9b5501 100644 --- a/src/timetable/timetable.service.ts +++ b/src/timetable/timetable.service.ts @@ -17,6 +17,7 @@ import { TimetableCourseEntity } from 'src/entities/timetable-course.entity'; import { isConflictingTime } from 'src/utils/time-utils'; import { DayType } from 'src/common/types/day-type.utils'; import { throwKukeyException } from 'src/utils/exception.util'; +import { DeleteTimetableResponseDto } from './dto/delete-timetable-response.dto'; @Injectable() export class TimetableService { @@ -360,7 +361,7 @@ export class TimetableService { transactionManager: EntityManager, timetableId: number, user: AuthorizedUserDto, - ): Promise { + ): Promise { const timetable = await transactionManager.findOne(TimetableEntity, { where: { id: timetableId, userId: user.id }, relations: ['timetableCourses', 'schedules'], // soft-remove cascade 조건을 위해 추가 @@ -391,7 +392,26 @@ export class TimetableService { } } await transactionManager.softRemove(timetable); - return new CommonDeleteResponseDto(true); + + // 삭제 후에 해당 학기에 시간표가 하나도 존재하지 않으면 추가로 하나 생성 (그 시간표가 대표시간표) + const remainingTimetables = await transactionManager.find(TimetableEntity, { + where: { userId: user.id }, + }); + + if (remainingTimetables.length === 0) { + const newTimetable = transactionManager.create(TimetableEntity, { + userId: user.id, + timetableName: 'timetable 1', + semester: timetable.semester, + year: timetable.year, + mainTimetable: true, + }); + + await transactionManager.save(newTimetable); + return new DeleteTimetableResponseDto(true, newTimetable.id); + } + + return new DeleteTimetableResponseDto(true, null); } async getMainTimetable( From 57a2e1585ef9bd7aa20f6449321c1f376ff24a88 Mon Sep 17 00:00:00 2001 From: Devheun Date: Tue, 26 Nov 2024 12:02:13 +0900 Subject: [PATCH 03/48] =?UTF-8?q?mod::=20=ED=95=B4=EB=8B=B9=20=ED=95=99?= =?UTF-8?q?=EA=B8=B0=EC=97=90=20=EC=8B=9C=EA=B0=84=ED=91=9C=20=EC=A1=B4?= =?UTF-8?q?=EC=9E=AC=ED=95=98=EC=A7=80=20=EC=95=8A=EC=9D=84=20=EC=8B=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=EB=A1=9C=20=ED=95=98=EB=82=98=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20-=20=EB=8C=80=ED=91=9C=EC=8B=9C=EA=B0=84=ED=91=9C?= =?UTF-8?q?=EA=B0=80=20=EC=97=86=EB=8A=94=20=EA=B2=BD=EC=9A=B0=EB=8A=94=20?= =?UTF-8?q?=EA=B2=B0=EA=B5=AD=20=ED=95=B4=EB=8B=B9=20=ED=95=99=EA=B8=B0?= =?UTF-8?q?=EC=97=90=20=EC=8B=9C=EA=B0=84=ED=91=9C=EA=B0=80=20=EC=97=86?= =?UTF-8?q?=EB=8B=A4=EB=8A=94=20=EC=9D=98=EB=AF=B8=EA=B8=B0=EB=95=8C?= =?UTF-8?q?=EB=AC=B8=EC=97=90=20=EB=8C=80=ED=91=9C=EC=8B=9C=EA=B0=84?= =?UTF-8?q?=ED=91=9C=20=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81=EC=97=90=20?= =?UTF-8?q?=ED=95=B4=EB=8B=B9=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 --- src/timetable/timetable.service.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/timetable/timetable.service.ts b/src/timetable/timetable.service.ts index ed9b5501..e042f94c 100644 --- a/src/timetable/timetable.service.ts +++ b/src/timetable/timetable.service.ts @@ -428,7 +428,16 @@ export class TimetableService { }); if (!mainTimetable) { - throwKukeyException('TIMETABLE_NOT_FOUND'); + // 대표 시간표 없으면 시간표 하나 바로 생성 + return await this.createTimetable( + this.timetableRepository.manager, + { + timetableName: 'timetable 1', + semester: timetableDto.semester, + year: timetableDto.year, + }, + user, + ); } return mainTimetable; } From e6e1ba9624c9dd91d2550007cbf32590f533881d Mon Sep 17 00:00:00 2001 From: Devheun Date: Tue, 26 Nov 2024 13:20:19 +0900 Subject: [PATCH 04/48] =?UTF-8?q?refactor::=20=EC=8B=9C=EA=B0=84=ED=91=9C?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C=20=EC=8B=9C=20=EB=82=A8=EC=9D=80=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=ED=91=9C=20=EA=B0=9C=EC=88=98=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95=20-=20find?= =?UTF-8?q?=EC=97=90=EC=84=9C=20findOne=EC=9C=BC=EB=A1=9C=20query=20?= =?UTF-8?q?=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/timetable/timetable.service.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/timetable/timetable.service.ts b/src/timetable/timetable.service.ts index e042f94c..d62827c9 100644 --- a/src/timetable/timetable.service.ts +++ b/src/timetable/timetable.service.ts @@ -394,11 +394,14 @@ export class TimetableService { await transactionManager.softRemove(timetable); // 삭제 후에 해당 학기에 시간표가 하나도 존재하지 않으면 추가로 하나 생성 (그 시간표가 대표시간표) - const remainingTimetables = await transactionManager.find(TimetableEntity, { - where: { userId: user.id }, - }); + const remainingTimetable = await transactionManager.findOne( + TimetableEntity, + { + where: { userId: user.id }, + }, + ); - if (remainingTimetables.length === 0) { + if (remainingTimetable) { const newTimetable = transactionManager.create(TimetableEntity, { userId: user.id, timetableName: 'timetable 1', From 97311751f80cb66446f30e6c6780d9d286d03274 Mon Sep 17 00:00:00 2001 From: Devheun Date: Tue, 26 Nov 2024 13:25:51 +0900 Subject: [PATCH 05/48] =?UTF-8?q?fix::=20=EC=8B=9C=EA=B0=84=ED=91=9C=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EC=8B=9C=20=EB=82=A8=EC=9D=80=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=ED=91=9C=20=EC=A1=B0=ED=9A=8C=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95=20-=20=EB=B0=98?= =?UTF-8?q?=EB=8C=80=EB=A1=9C=20=ED=96=88=EB=8D=98=EA=B1=B0=20=EC=98=AC?= =?UTF-8?q?=EB=B0=94=EB=A5=B4=EA=B2=8C=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/timetable/timetable.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/timetable/timetable.service.ts b/src/timetable/timetable.service.ts index d62827c9..c830a44c 100644 --- a/src/timetable/timetable.service.ts +++ b/src/timetable/timetable.service.ts @@ -401,7 +401,7 @@ export class TimetableService { }, ); - if (remainingTimetable) { + if (!remainingTimetable) { const newTimetable = transactionManager.create(TimetableEntity, { userId: user.id, timetableName: 'timetable 1', From 57fc49b9a40e06e878e0ef217b50ea82e69b370e Mon Sep 17 00:00:00 2001 From: Devheun Date: Fri, 29 Nov 2024 16:29:29 +0900 Subject: [PATCH 06/48] =?UTF-8?q?fix::=20=EC=97=B0=EB=8F=84=EC=99=80=20?= =?UTF-8?q?=ED=95=99=EA=B8=B0=20=EC=A1=B0=EA=B1=B4=20=EB=88=84=EB=9D=BD?= =?UTF-8?q?=EB=90=9C=20=EA=B2=83=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/timetable/timetable.service.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/timetable/timetable.service.ts b/src/timetable/timetable.service.ts index c830a44c..9c9c0d89 100644 --- a/src/timetable/timetable.service.ts +++ b/src/timetable/timetable.service.ts @@ -397,7 +397,11 @@ export class TimetableService { const remainingTimetable = await transactionManager.findOne( TimetableEntity, { - where: { userId: user.id }, + where: { + userId: user.id, + semester: timetable.semester, + year: timetable.year, + }, }, ); From b49878c9282a064cff436d752f7a25d69f008f41 Mon Sep 17 00:00:00 2001 From: Devheun Date: Mon, 9 Dec 2024 16:43:53 +0900 Subject: [PATCH 07/48] =?UTF-8?q?feat::=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=8B=9C=20=EB=94=94=EC=8A=A4=EC=BD=94=EB=93=9C=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 --- package-lock.json | 25 +++++++++++++++++++++++++ package.json | 1 + src/auth/auth.service.ts | 16 ++++++++++++++++ src/user/user.repository.ts | 4 ++++ src/user/user.service.ts | 4 ++++ 5 files changed, 50 insertions(+) diff --git a/package-lock.json b/package-lock.json index 84a6f2f1..87096edf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "class-validator": "^0.14.1", "cross-env": "^7.0.3", "deepl-node": "^1.13.0", + "discord-webhook-node": "^1.1.8", "express-basic-auth": "^1.2.1", "multer": "^1.4.5-lts.1", "multer-s3": "^3.0.1", @@ -10877,6 +10878,30 @@ "node": ">=8" } }, + "node_modules/discord-webhook-node": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/discord-webhook-node/-/discord-webhook-node-1.1.8.tgz", + "integrity": "sha512-3u0rrwywwYGc6HrgYirN/9gkBYqmdpvReyQjapoXARAHi0P0fIyf3W5tS5i3U3cc7e44E+e7dIHYUeec7yWaug==", + "license": "MIT", + "dependencies": { + "form-data": "^3.0.0", + "node-fetch": "^2.6.0" + } + }, + "node_modules/discord-webhook-node/node_modules/form-data": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.2.tgz", + "integrity": "sha512-sJe+TQb2vIaIyO783qN6BlMYWMw3WBOHA1Ay2qxsnjuafEOQFJ2JakedOQirT6D5XPRxDvS7AHYyem9fTpb4LQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", diff --git a/package.json b/package.json index af8492b2..3d9eaeae 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "class-validator": "^0.14.1", "cross-env": "^7.0.3", "deepl-node": "^1.13.0", + "discord-webhook-node": "^1.1.8", "express-basic-auth": "^1.2.1", "multer": "^1.4.5-lts.1", "multer-s3": "^3.0.1", diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 5daee1ea..cc551884 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -25,6 +25,7 @@ import { ChangePasswordResponseDto } from './dto/change-password-response.dto'; import { SendTempPasswordResponseDto } from './dto/send-temporary-password.dto'; import { EntityManager } from 'typeorm'; import { throwKukeyException } from 'src/utils/exception.util'; +import { Webhook } from 'discord-webhook-node'; @Injectable() export class AuthService { @@ -254,6 +255,21 @@ export class AuthService { user, ); + const discordUrl = await this.configService.get('DISCORD_WEBHOOK_URL'); + const hook = new Webhook(discordUrl); + const totalUsers = await this.userService.getTotalUsersCount(); + + await hook + .send( + 'The ' + + totalUsers + + 'th user has signed up for KU-KEY! (' + + studentNumber + + ')', + ) + .then(() => console.log('Sent new user request to discord')) + .catch((err) => console.log(err.message)); + return new SignUpResponseDto(true, studentNumber); } diff --git a/src/user/user.repository.ts b/src/user/user.repository.ts index 979aeef9..1533d962 100644 --- a/src/user/user.repository.ts +++ b/src/user/user.repository.ts @@ -124,4 +124,8 @@ export class UserRepository extends Repository { return updateResult.affected ? true : false; } + + async getTotalUsersCount(): Promise { + return await this.count(); + } } diff --git a/src/user/user.service.ts b/src/user/user.service.ts index 9336c24d..dec9d135 100644 --- a/src/user/user.service.ts +++ b/src/user/user.service.ts @@ -442,4 +442,8 @@ export class UserService { return new SelectCharacterLevelResponseDto(selectedLevel); } + + async getTotalUsersCount(): Promise { + return await this.userRepository.getTotalUsersCount(); + } } From 4ad7b13610c9ab5e50f83c768c49173dce731d13 Mon Sep 17 00:00:00 2001 From: Kim SeongHyeon Date: Wed, 8 Jan 2025 16:24:55 +0900 Subject: [PATCH 08/48] =?UTF-8?q?feat:=20=EB=B0=B0=EB=84=88=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EC=A1=B0=ED=9A=8C,=20=EC=B6=94=EA=B0=80,?= =?UTF-8?q?=20=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 --- src/app.module.ts | 2 + src/decorators/docs/banner.decorator.ts | 80 +++++++++++++++++++++++++ src/entities/banner.entity.ts | 11 ++++ src/home/banner/banner.controller.ts | 48 +++++++++++++++ src/home/banner/banner.module.ts | 14 +++++ src/home/banner/banner.service.ts | 60 +++++++++++++++++++ src/home/banner/dto/banner.dto.ts | 9 +++ src/utils/exception.util.ts | 25 ++++++++ 8 files changed, 249 insertions(+) create mode 100644 src/decorators/docs/banner.decorator.ts create mode 100644 src/entities/banner.entity.ts create mode 100644 src/home/banner/banner.controller.ts create mode 100644 src/home/banner/banner.module.ts create mode 100644 src/home/banner/banner.service.ts create mode 100644 src/home/banner/dto/banner.dto.ts diff --git a/src/app.module.ts b/src/app.module.ts index 2661eb45..991fecbb 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -25,6 +25,7 @@ import { AttendanceCheckModule } from './attendance-check/attendance-check.modul import { APP_FILTER } from '@nestjs/core'; import { UnhandledExceptionFilter } from './common/filter/unhandled-exception.filter'; import { KukeyExceptionFilter } from './common/filter/kukey-exception.filter'; +import { BannerModule } from './home/banner/banner.module'; console.log(`.env.${process.env.NODE_ENV}`); @@ -75,6 +76,7 @@ console.log(`.env.${process.env.NODE_ENV}`); CalendarModule, ReportModule, AttendanceCheckModule, + BannerModule, ], controllers: [AppController], providers: [ diff --git a/src/decorators/docs/banner.decorator.ts b/src/decorators/docs/banner.decorator.ts new file mode 100644 index 00000000..2455109c --- /dev/null +++ b/src/decorators/docs/banner.decorator.ts @@ -0,0 +1,80 @@ +import { + ApiBearerAuth, + ApiBody, + ApiConsumes, + ApiOperation, + ApiParam, + ApiResponse, +} from '@nestjs/swagger'; +import { MethodNames } from 'src/common/types/method'; +import { BannerController } from 'src/home/banner/banner.controller'; +import { bannerDto } from 'src/home/banner/dto/banner.dto'; + +type BannerEndPoints = MethodNames; + +const BannerDocsMap: Record = { + getBannerImages: [ + ApiOperation({ + summary: '배너 이미지 목록 조회', + description: '배너 이미지 목록을 조회합니다.(최신순)', + }), + ApiResponse({ + status: 200, + description: '배너 이미지 목록 조회 성공', + isArray: true, + type: bannerDto, + }), + ], + createBannerImage: [ + ApiBearerAuth('accessToken'), + ApiOperation({ + summary: '배너 이미지 생성', + description: '배너 이미지를 생성합니다.', + }), + ApiConsumes('multipart/form-data'), + ApiBody({ + schema: { + type: 'object', + properties: { + image: { + type: 'string', + format: 'binary', + nullable: false, + }, + }, + }, + }), + ApiResponse({ + status: 201, + description: '배너 이미지 생성 성공', + type: bannerDto, + }), + ], + deleteBannerImage: [ + ApiBearerAuth('accessToken'), + ApiOperation({ + summary: '배너 이미지 삭제', + description: '배너 이미지를 삭제합니다.', + }), + ApiParam({ name: 'id', required: true, description: '배너 이미지 id' }), + ApiResponse({ + status: 200, + description: '배너 이미지 삭제 성공', + }), + ], +}; + +export function BannerDocs(target: typeof BannerController) { + for (const key in BannerDocsMap) { + const methodDecorators = BannerDocsMap[key as BannerEndPoints]; + + const descripter = Object.getOwnPropertyDescriptor(target.prototype, key); + if (descripter) { + for (const decorator of methodDecorators) { + decorator(target.prototype, key, descripter); + } + Object.defineProperty(target.prototype, key, descripter); + } + } + return target; +} diff --git a/src/entities/banner.entity.ts b/src/entities/banner.entity.ts new file mode 100644 index 00000000..79f1e414 --- /dev/null +++ b/src/entities/banner.entity.ts @@ -0,0 +1,11 @@ +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; +import { CommonEntity } from './common.entity'; + +@Entity('banner') +export class BannerEntity extends CommonEntity { + @PrimaryGeneratedColumn({ type: 'bigint' }) + id: number; + + @Column('varchar', { nullable: false }) + imageUrl: string; +} diff --git a/src/home/banner/banner.controller.ts b/src/home/banner/banner.controller.ts new file mode 100644 index 00000000..2974172d --- /dev/null +++ b/src/home/banner/banner.controller.ts @@ -0,0 +1,48 @@ +import { + Controller, + Delete, + Get, + Param, + Post, + UploadedFile, + UseGuards, + UseInterceptors, +} from '@nestjs/common'; +import { BannerService } from './banner.service'; +import { bannerDto } from './dto/banner.dto'; +import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard'; +import { RolesGuard } from 'src/auth/guards/role.guard'; +import { Roles } from 'src/decorators/roles.decorator'; +import { Role } from 'src/enums/role.enum'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { ApiTags } from '@nestjs/swagger'; +import { BannerDocs } from 'src/decorators/docs/banner.decorator'; + +@Controller('banner') +@ApiTags('banner') +@BannerDocs +export class BannerController { + constructor(private readonly bannerService: BannerService) {} + + @Get() + async getBannerImages(): Promise { + return await this.bannerService.getBannerImages(); + } + + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles(Role.admin) + @UseInterceptors(FileInterceptor('image')) + @Post() + async createBannerImage( + @UploadedFile() image: Express.Multer.File, + ): Promise { + return await this.bannerService.createBannerImage(image); + } + + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles(Role.admin) + @Delete('/:id') + async deleteBannerImage(@Param('id') id: number): Promise { + return await this.bannerService.deleteBannerImage(id); + } +} diff --git a/src/home/banner/banner.module.ts b/src/home/banner/banner.module.ts new file mode 100644 index 00000000..46c221be --- /dev/null +++ b/src/home/banner/banner.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { CommonModule } from 'src/common/common.module'; +import { BannerService } from './banner.service'; +import { BannerController } from './banner.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { BannerEntity } from 'src/entities/banner.entity'; +import { UserRepository } from 'src/user/user.repository'; + +@Module({ + imports: [TypeOrmModule.forFeature([BannerEntity]), CommonModule], + providers: [BannerService, UserRepository], + controllers: [BannerController], +}) +export class BannerModule {} diff --git a/src/home/banner/banner.service.ts b/src/home/banner/banner.service.ts new file mode 100644 index 00000000..3abd7ec0 --- /dev/null +++ b/src/home/banner/banner.service.ts @@ -0,0 +1,60 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { FileService } from 'src/common/file.service'; +import { BannerEntity } from 'src/entities/banner.entity'; +import { Repository } from 'typeorm'; +import { bannerDto } from './dto/banner.dto'; +import { throwKukeyException } from 'src/utils/exception.util'; + +@Injectable() +export class BannerService { + constructor( + @InjectRepository(BannerEntity) + private readonly bannerRepository: Repository, + private readonly fileService: FileService, + ) {} + + async getBannerImages(): Promise { + const banners = await this.bannerRepository.find({ + order: { createdAt: 'DESC' }, + }); + return banners.map((banner) => { + return { + id: banner.id, + imageUrl: banner.imageUrl, + }; + }); + } + + async createBannerImage(image: Express.Multer.File): Promise { + if (!image) { + throwKukeyException('BANNER_IMAGE_REQUIRED'); + } + if (!this.fileService.imagefilter(image)) { + throwKukeyException('NOT_IMAGE_FILE'); + } + const fileDir = await this.fileService.uploadFile(image, 'home', 'banner'); + const imageUrl = this.fileService.makeUrlByFileDir(fileDir); + const banner = this.bannerRepository.create({ + imageUrl, + }); + const savedBanner = await this.bannerRepository.save(banner); + return { + id: savedBanner.id, + imageUrl: savedBanner.imageUrl, + }; + } + + async deleteBannerImage(id: number): Promise { + const banner = await this.bannerRepository.findOne({ where: { id } }); + if (!banner) { + throwKukeyException('BANNER_NOT_FOUND'); + } + const deleted = await this.bannerRepository.softDelete({ id }); + if (deleted.affected === 0) { + throwKukeyException('BANNER_DELETE_FAILED'); + } + const fileDir = this.fileService.getFileDirFromUrl(banner.imageUrl); + await this.fileService.deleteFile(fileDir); + } +} diff --git a/src/home/banner/dto/banner.dto.ts b/src/home/banner/dto/banner.dto.ts new file mode 100644 index 00000000..48d39fa9 --- /dev/null +++ b/src/home/banner/dto/banner.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class bannerDto { + @ApiProperty({ description: '배너 id' }) + id: number; + + @ApiProperty({ description: '배너 이미지 URL' }) + imageUrl: string; +} diff --git a/src/utils/exception.util.ts b/src/utils/exception.util.ts index c268328e..9480494a 100644 --- a/src/utils/exception.util.ts +++ b/src/utils/exception.util.ts @@ -654,6 +654,31 @@ export const kukeyExceptions = createKukeyExceptions({ errorCode: 5102, statusCode: 500, }, + // - 52xx : Banner + BANNER_NOT_FOUND: { + name: 'BANNER_NOT_FOUND', + message: 'Banner not found.', + errorCode: 5200, + statusCode: 404, + }, + BANNER_DELETE_FAILED: { + name: 'BANNER_DELETE_FAILED', + message: 'Banner delete failed.', + errorCode: 5201, + statusCode: 500, + }, + BANNER_UPDATE_FAILED: { + name: 'BANNER_UPDATE_FAILED', + message: 'Banner update failed.', + errorCode: 5202, + statusCode: 500, + }, + BANNER_IMAGE_REQUIRED: { + name: 'BANNER_IMAGE_REQUIRED', + message: 'Banner image required.', + errorCode: 5203, + statusCode: 400, + }, // 6xxx : S3, File 관련 예외 NOT_IMAGE_FILE: { name: 'NOT_IMAGE_FILE', From 397d142f73304a6306a0d79cde24fb9e0b11c637 Mon Sep 17 00:00:00 2001 From: Devheun <86945971+Devheun@users.noreply.github.com> Date: Tue, 14 Jan 2025 20:42:45 +0900 Subject: [PATCH 09/48] =?UTF-8?q?chore::=20CICD=20yml=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docker-build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 039c3919..129e590e 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -75,6 +75,7 @@ jobs: echo "REDIS_HOST=${{ secrets.REDIS_HOST }}" >> .env.test echo "REDIS_PORT=${{ secrets.REDIS_PORT }}" >> .env.test echo "REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }}" >> .env.test + echo "DISCORD_WEBHOOK_URL=${{ secrets.DISCORD_WEBHOOK_URL }}" >> .env.test cat .env.test @@ -112,6 +113,7 @@ jobs: echo "REDIS_HOST=${{ secrets.REDIS_HOST }}" >> .env.prod echo "REDIS_PORT=${{ secrets.REDIS_PORT }}" >> .env.prod echo "REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }}" >> .env.prod + echo "DISCORD_WEBHOOK_URL=${{ secrets.DISCORD_WEBHOOK_URL }}" >> .env.prod cat .env.prod From 10000f3121ca39fc99d4b33b6bfe3d77e380ee2b Mon Sep 17 00:00:00 2001 From: Kim SeongHyeon Date: Wed, 15 Jan 2025 16:22:01 +0900 Subject: [PATCH 10/48] =?UTF-8?q?feat:=20=EB=B0=B0=EB=84=88=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=EC=97=90=20=EC=A0=9C=EB=AA=A9=EC=A7=80?= =?UTF-8?q?=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/decorators/docs/banner.decorator.ts | 12 ++---------- src/entities/banner.entity.ts | 3 +++ src/home/banner/banner.controller.ts | 5 ++++- src/home/banner/banner.service.ts | 8 +++++++- src/home/banner/dto/banner.dto.ts | 3 +++ src/home/banner/dto/createBannerRequest.dto.ts | 15 +++++++++++++++ 6 files changed, 34 insertions(+), 12 deletions(-) create mode 100644 src/home/banner/dto/createBannerRequest.dto.ts diff --git a/src/decorators/docs/banner.decorator.ts b/src/decorators/docs/banner.decorator.ts index 2455109c..560066c8 100644 --- a/src/decorators/docs/banner.decorator.ts +++ b/src/decorators/docs/banner.decorator.ts @@ -9,6 +9,7 @@ import { import { MethodNames } from 'src/common/types/method'; import { BannerController } from 'src/home/banner/banner.controller'; import { bannerDto } from 'src/home/banner/dto/banner.dto'; +import { createBannerRequestDto } from 'src/home/banner/dto/createBannerRequest.dto'; type BannerEndPoints = MethodNames; @@ -33,16 +34,7 @@ const BannerDocsMap: Record = { }), ApiConsumes('multipart/form-data'), ApiBody({ - schema: { - type: 'object', - properties: { - image: { - type: 'string', - format: 'binary', - nullable: false, - }, - }, - }, + type: createBannerRequestDto, }), ApiResponse({ status: 201, diff --git a/src/entities/banner.entity.ts b/src/entities/banner.entity.ts index 79f1e414..0e9fef2c 100644 --- a/src/entities/banner.entity.ts +++ b/src/entities/banner.entity.ts @@ -8,4 +8,7 @@ export class BannerEntity extends CommonEntity { @Column('varchar', { nullable: false }) imageUrl: string; + + @Column('varchar', { nullable: false }) + title: string; } diff --git a/src/home/banner/banner.controller.ts b/src/home/banner/banner.controller.ts index 2974172d..4971cd04 100644 --- a/src/home/banner/banner.controller.ts +++ b/src/home/banner/banner.controller.ts @@ -1,4 +1,5 @@ import { + Body, Controller, Delete, Get, @@ -17,6 +18,7 @@ import { Role } from 'src/enums/role.enum'; import { FileInterceptor } from '@nestjs/platform-express'; import { ApiTags } from '@nestjs/swagger'; import { BannerDocs } from 'src/decorators/docs/banner.decorator'; +import { createBannerRequestDto } from 'src/home/banner/dto/createBannerRequest.dto'; @Controller('banner') @ApiTags('banner') @@ -35,8 +37,9 @@ export class BannerController { @Post() async createBannerImage( @UploadedFile() image: Express.Multer.File, + @Body() body: createBannerRequestDto, ): Promise { - return await this.bannerService.createBannerImage(image); + return await this.bannerService.createBannerImage(image, body.title); } @UseGuards(JwtAuthGuard, RolesGuard) diff --git a/src/home/banner/banner.service.ts b/src/home/banner/banner.service.ts index 3abd7ec0..6554453e 100644 --- a/src/home/banner/banner.service.ts +++ b/src/home/banner/banner.service.ts @@ -22,11 +22,15 @@ export class BannerService { return { id: banner.id, imageUrl: banner.imageUrl, + title: banner.title, }; }); } - async createBannerImage(image: Express.Multer.File): Promise { + async createBannerImage( + image: Express.Multer.File, + title: string, + ): Promise { if (!image) { throwKukeyException('BANNER_IMAGE_REQUIRED'); } @@ -37,11 +41,13 @@ export class BannerService { const imageUrl = this.fileService.makeUrlByFileDir(fileDir); const banner = this.bannerRepository.create({ imageUrl, + title, }); const savedBanner = await this.bannerRepository.save(banner); return { id: savedBanner.id, imageUrl: savedBanner.imageUrl, + title: savedBanner.title, }; } diff --git a/src/home/banner/dto/banner.dto.ts b/src/home/banner/dto/banner.dto.ts index 48d39fa9..645f8ce6 100644 --- a/src/home/banner/dto/banner.dto.ts +++ b/src/home/banner/dto/banner.dto.ts @@ -6,4 +6,7 @@ export class bannerDto { @ApiProperty({ description: '배너 이미지 URL' }) imageUrl: string; + + @ApiProperty({ description: '배너 제목' }) + title: string; } diff --git a/src/home/banner/dto/createBannerRequest.dto.ts b/src/home/banner/dto/createBannerRequest.dto.ts new file mode 100644 index 00000000..8804450f --- /dev/null +++ b/src/home/banner/dto/createBannerRequest.dto.ts @@ -0,0 +1,15 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty } from 'class-validator'; + +export class createBannerRequestDto { + @ApiProperty({ + description: '배너 이미지 파일', + type: 'string', + format: 'binary', + }) + image: any; + + @ApiProperty({ description: '배너 제목' }) + @IsNotEmpty() + title: string; +} From 605afcf2f311db836824efef529c9da15ecbdfcd Mon Sep 17 00:00:00 2001 From: Devheun <86945971+Devheun@users.noreply.github.com> Date: Wed, 15 Jan 2025 21:07:48 +0900 Subject: [PATCH 11/48] =?UTF-8?q?feat::=20=EA=B2=80=EC=83=89=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=EC=A0=81=EC=9D=B8=20=EC=A0=84=EB=9E=B5=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 - category에 따라 전공, 교양, 학문의 기초가 되게 --- .../strategy/academic-foundation-search-strategy.ts | 9 +++++++++ src/course/strategy/general-search-strategy.ts | 9 +++++++++ src/course/strategy/major-search-strategy.ts | 9 +++++++++ src/course/strategy/search-strategy.ts | 3 +++ 4 files changed, 30 insertions(+) create mode 100644 src/course/strategy/academic-foundation-search-strategy.ts create mode 100644 src/course/strategy/general-search-strategy.ts create mode 100644 src/course/strategy/major-search-strategy.ts create mode 100644 src/course/strategy/search-strategy.ts diff --git a/src/course/strategy/academic-foundation-search-strategy.ts b/src/course/strategy/academic-foundation-search-strategy.ts new file mode 100644 index 00000000..89623af1 --- /dev/null +++ b/src/course/strategy/academic-foundation-search-strategy.ts @@ -0,0 +1,9 @@ +import { SearchStrategy } from './search-strategy'; + +const ACADEMIC_FOUNDATION = 'Academic Foundations'; + +export class AcademicFoundationSearchStrategy implements SearchStrategy { + supports(category: string): boolean { + return category === ACADEMIC_FOUNDATION; + } +} diff --git a/src/course/strategy/general-search-strategy.ts b/src/course/strategy/general-search-strategy.ts new file mode 100644 index 00000000..6073cb1b --- /dev/null +++ b/src/course/strategy/general-search-strategy.ts @@ -0,0 +1,9 @@ +import { SearchStrategy } from './search-strategy'; + +const GENERAL = 'General Studies'; + +export class GeneralSearchStrategy implements SearchStrategy { + supports(category: string): boolean { + return category === GENERAL; + } +} diff --git a/src/course/strategy/major-search-strategy.ts b/src/course/strategy/major-search-strategy.ts new file mode 100644 index 00000000..e983f574 --- /dev/null +++ b/src/course/strategy/major-search-strategy.ts @@ -0,0 +1,9 @@ +import { SearchStrategy } from './search-strategy'; + +const MAJOR = 'Major'; + +export class MajorSearchStrategy implements SearchStrategy { + supports(category: string): boolean { + return category === MAJOR; + } +} diff --git a/src/course/strategy/search-strategy.ts b/src/course/strategy/search-strategy.ts new file mode 100644 index 00000000..d254910f --- /dev/null +++ b/src/course/strategy/search-strategy.ts @@ -0,0 +1,3 @@ +export interface SearchStrategy { + supports(category: string): boolean; +} From 907daf3dd002327adda6bf2f2aa76f8047ea33e0 Mon Sep 17 00:00:00 2001 From: Devheun <86945971+Devheun@users.noreply.github.com> Date: Tue, 21 Jan 2025 19:54:11 +0900 Subject: [PATCH 12/48] =?UTF-8?q?feat::=20=EA=B0=95=EC=9D=98=20=EC=B9=B4?= =?UTF-8?q?=ED=85=8C=EA=B3=A0=EB=A6=AC=20enum=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EB=B0=8F=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/enums/course-category.enum.ts | 6 ++++++ src/utils/exception.util.ts | 6 ++++++ 2 files changed, 12 insertions(+) create mode 100644 src/enums/course-category.enum.ts diff --git a/src/enums/course-category.enum.ts b/src/enums/course-category.enum.ts new file mode 100644 index 00000000..4199f8eb --- /dev/null +++ b/src/enums/course-category.enum.ts @@ -0,0 +1,6 @@ +export enum CourseCategory { + ALL_CLASS = 'All Class', + MAJOR = 'Major', + GENERAL_STUDIES = 'General Studies', + ACADEMIC_FOUNDATIONS = 'Academic Foundations', +} diff --git a/src/utils/exception.util.ts b/src/utils/exception.util.ts index c268328e..ddecb4ca 100644 --- a/src/utils/exception.util.ts +++ b/src/utils/exception.util.ts @@ -309,6 +309,12 @@ export const kukeyExceptions = createKukeyExceptions({ errorCode: 3004, statusCode: 409, }, + COURSE_SEARCH_STRATEGY_NOT_FOUND: { + name: 'COURSE_SEARCH_STRATEGY_NOT_FOUND', + message: 'Course search strategy not found.', + errorCode: 3005, + statusCode: 404, + }, // - 31xx : Schedule INVALID_TIME_RANGE: { name: 'INVALID_TIME_RANGE', From a564651cb03afe43355b4c81096953b521ebd747 Mon Sep 17 00:00:00 2001 From: Devheun <86945971+Devheun@users.noreply.github.com> Date: Tue, 21 Jan 2025 19:57:05 +0900 Subject: [PATCH 13/48] =?UTF-8?q?feat::=20=EA=B0=95=EC=9D=98=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=A5=B8=20=EC=83=88=EB=A1=9C=EC=9A=B4=20DTO=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/course/dto/search-course-new.dto.ts | 44 +++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/course/dto/search-course-new.dto.ts diff --git a/src/course/dto/search-course-new.dto.ts b/src/course/dto/search-course-new.dto.ts new file mode 100644 index 00000000..120a93fd --- /dev/null +++ b/src/course/dto/search-course-new.dto.ts @@ -0,0 +1,44 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsEnum, IsInt, IsOptional, IsString, Length } from 'class-validator'; +import { CourseCategory } from 'src/enums/course-category.enum'; + +export class SearchCourseNewDto { + @ApiPropertyOptional({ + description: '커서 id, 값이 존재하지 않으면 첫 페이지', + }) + @IsInt() + @IsOptional() + cursorId?: number; + + @ApiProperty({ description: '연도' }) + @IsString() + @Length(4) + year: string; + + @ApiProperty({ description: '학기' }) + @IsString() + @Length(1) + semester: string; + + @ApiProperty({ + description: '강의 카테고리 (모든 강의, 전공, 교양, 학문의 기초)', + enum: CourseCategory, + }) + @IsEnum(CourseCategory) + category: CourseCategory; + + @ApiPropertyOptional({ + description: '검색 키워드 (강의명, 교수명, 학수번호)', + }) + @Length(2) + @IsOptional() + keyword?: string; + + @ApiPropertyOptional({ + description: + 'Major일때 major를, Academic Foundation일 때 college를 넣어주세요.', + }) + @IsString() + @IsOptional() + classification?: string; +} From 60605df5d1c02812b3bd23003445419afdeab84c Mon Sep 17 00:00:00 2001 From: JeongYeonSeung Date: Sat, 25 Jan 2025 15:47:49 +0900 Subject: [PATCH 14/48] =?UTF-8?q?refactor::=20friendship=20=EA=B0=84?= =?UTF-8?q?=EB=8B=A8=ED=95=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 함수명 오타 수정 - 불필요한 코드 삭제 --- src/friendship/friendship.controller.ts | 30 +++++++++---------------- src/friendship/friendship.repository.ts | 2 +- src/friendship/friendship.service.ts | 2 +- 3 files changed, 13 insertions(+), 21 deletions(-) diff --git a/src/friendship/friendship.controller.ts b/src/friendship/friendship.controller.ts index 43ae6257..35be3dc7 100644 --- a/src/friendship/friendship.controller.ts +++ b/src/friendship/friendship.controller.ts @@ -43,19 +43,17 @@ export class FriendshipController { @User() user: AuthorizedUserDto, @Query('keyword') keyword?: string, ): Promise { - const userId = user.id; - return await this.friendshipService.getFriendList(userId, keyword); + return await this.friendshipService.getFriendList(user.id, keyword); } @UseGuards(JwtAuthGuard) @Get('search-user') async searchUserForFriendshipRequest( - @Query() searchUserRequestDto: SearchUserRequestDto, @User() user: AuthorizedUserDto, + @Query() searchUserRequestDto: SearchUserRequestDto, ): Promise { - const myId = user.id; return await this.friendshipService.searchUserForFriendshipRequest( - myId, + user.id, searchUserRequestDto, ); } @@ -63,8 +61,8 @@ export class FriendshipController { @UseGuards(JwtAuthGuard) @Get('friend-timetable') async getFriendTimetable( - @Query() getFriendTimetableRequestDto: GetFriendTimetableRequestDto, @User() user: AuthorizedUserDto, + @Query() getFriendTimetableRequestDto: GetFriendTimetableRequestDto, ): Promise { return await this.friendshipService.getFriendTimetable( user.id, @@ -77,14 +75,13 @@ export class FriendshipController { @UseInterceptors(TransactionInterceptor) async sendFriendshipRequest( @TransactionManager() transactionManager: EntityManager, - @Body() sendFriendDto: SendFriendshipRequestDto, @User() user: AuthorizedUserDto, + @Body() sendFriendDto: SendFriendshipRequestDto, ): Promise { - const toUsername = sendFriendDto.toUsername; return await this.friendshipService.sendFriendshipRequest( transactionManager, user, - toUsername, + sendFriendDto.toUsername, ); } @@ -93,8 +90,7 @@ export class FriendshipController { async getReceivedWaitingFriendList( @User() user: AuthorizedUserDto, ): Promise { - const userId = user.id; - return await this.friendshipService.getReceivedWaitingFriendList(userId); + return await this.friendshipService.getReceivedWaitingFriendList(user.id); } @UseGuards(JwtAuthGuard) @@ -102,8 +98,7 @@ export class FriendshipController { async getSentWaitingFriendList( @User() user: AuthorizedUserDto, ): Promise { - const userId = user.id; - return await this.friendshipService.getSentWaitingFriendList(userId); + return await this.friendshipService.getSentWaitingFriendList(user.id); } @UseGuards(JwtAuthGuard) @@ -129,10 +124,9 @@ export class FriendshipController { @User() user: AuthorizedUserDto, @Param('friendshipId') friendshipId: number, ): Promise { - const userId = user.id; return await this.friendshipService.rejectFriendshipRequest( transactionManager, - userId, + user.id, friendshipId, ); } @@ -145,10 +139,9 @@ export class FriendshipController { @User() user: AuthorizedUserDto, @Param('friendshipId') friendshipId: number, ): Promise { - const userId = user.id; return await this.friendshipService.cancelFriendshipRequest( transactionManager, - userId, + user.id, friendshipId, ); } @@ -161,10 +154,9 @@ export class FriendshipController { @User() user: AuthorizedUserDto, @Param('friendshipId') friendshipId: number, ): Promise { - const userId = user.id; return await this.friendshipService.deleteFriendship( transactionManager, - userId, + user.id, friendshipId, ); } diff --git a/src/friendship/friendship.repository.ts b/src/friendship/friendship.repository.ts index 708d2eeb..b24976fc 100644 --- a/src/friendship/friendship.repository.ts +++ b/src/friendship/friendship.repository.ts @@ -44,7 +44,7 @@ export class FriendshipRepository extends Repository { }); } - async findFriendshipByUserIdAndKeyword( + async findFriendshipsByUserIdAndKeyword( userId: number, keyword: string, ): Promise { diff --git a/src/friendship/friendship.service.ts b/src/friendship/friendship.service.ts index 82899859..cef9ca4a 100644 --- a/src/friendship/friendship.service.ts +++ b/src/friendship/friendship.service.ts @@ -37,7 +37,7 @@ export class FriendshipService { if (keyword) { friendships = - await this.friendshipRepository.findFriendshipByUserIdAndKeyword( + await this.friendshipRepository.findFriendshipsByUserIdAndKeyword( userId, keyword, ); From 1339ecca28c0263f8d34fc48964771d10f5a835e Mon Sep 17 00:00:00 2001 From: JeongYeonSeung Date: Sat, 25 Jan 2025 16:46:04 +0900 Subject: [PATCH 15/48] =?UTF-8?q?feat:=20=EB=B0=9B=EC=9D=80=20=EC=B9=9C?= =?UTF-8?q?=EA=B5=AC=20=EC=9A=94=EC=B2=AD=20=EA=B0=9C=EC=88=98=20=EB=B0=98?= =?UTF-8?q?=ED=99=98=20=EC=97=94=EB=93=9C=ED=8F=AC=EC=9D=B8=ED=8A=B8=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/decorators/docs/friendship.decorator.ts | 12 ++++++++++++ src/entities/friendship.entity.ts | 3 +++ ...et-received-friendship-request-count.dto.ts | 14 ++++++++++++++ src/friendship/friendship.controller.ts | 11 +++++++++++ src/friendship/friendship.repository.ts | 18 ++++++++++++++++++ src/friendship/friendship.service.ts | 10 ++++++++++ 6 files changed, 68 insertions(+) create mode 100644 src/friendship/dto/get-received-friendship-request-count.dto.ts diff --git a/src/decorators/docs/friendship.decorator.ts b/src/decorators/docs/friendship.decorator.ts index 0d928a73..f1674045 100644 --- a/src/decorators/docs/friendship.decorator.ts +++ b/src/decorators/docs/friendship.decorator.ts @@ -17,6 +17,7 @@ import { UpdateFriendshipResponseDto } from 'src/friendship/dto/update-friendshi import { FriendshipController } from 'src/friendship/friendship.controller'; import { GetTimetableByTimetableIdDto } from 'src/timetable/dto/get-timetable-timetable.dto'; import { ApiKukeyExceptionResponse } from '../api-kukey-exception-response'; +import { GetReceivedFriendshipRequestCountDto } from 'src/friendship/dto/get-received-friendship-request-count.dto'; type FriendshipEndPoints = MethodNames; @@ -118,6 +119,17 @@ const FriendshipDocsMap: Record = { type: GetWaitingFriendResponseDto, }), ], + getReceivedFriendshipRequestCount: [ + ApiOperation({ + summary: '나에게 온 친구 요청 개수 조회', + description: + '나에게 온 친구 요청 전체 개수 / 아직 확인하지 않은 개수를 조회합니다.', + }), + ApiOkResponse({ + description: '나에게 온 친구 요청 전체 개수 / 아직 확인하지 않은 개수', + type: GetReceivedFriendshipRequestCountDto, + }), + ], getSentWaitingFriendList: [ ApiOperation({ summary: '내가 친구 요청을 보낸 유저 목록 조회', diff --git a/src/entities/friendship.entity.ts b/src/entities/friendship.entity.ts index 87726281..de73b91f 100644 --- a/src/entities/friendship.entity.ts +++ b/src/entities/friendship.entity.ts @@ -33,4 +33,7 @@ export class FriendshipEntity extends CommonEntity { @Column('boolean', { nullable: false }) areWeFriend: boolean; + + @Column('boolean', { nullable: false, default: false }) + isRead: boolean; } diff --git a/src/friendship/dto/get-received-friendship-request-count.dto.ts b/src/friendship/dto/get-received-friendship-request-count.dto.ts new file mode 100644 index 00000000..85eb278a --- /dev/null +++ b/src/friendship/dto/get-received-friendship-request-count.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class GetReceivedFriendshipRequestCountDto { + @ApiProperty({ description: '전체 받은 친구 요청 개수' }) + totalCount: number; + + @ApiProperty({ description: '확인하지 않은 받은 친구 요청 개수' }) + unreadCount: number; + + constructor(totalCount: number, unreadCount: number) { + this.totalCount = totalCount; + this.unreadCount = unreadCount; + } +} diff --git a/src/friendship/friendship.controller.ts b/src/friendship/friendship.controller.ts index 35be3dc7..889b8262 100644 --- a/src/friendship/friendship.controller.ts +++ b/src/friendship/friendship.controller.ts @@ -29,6 +29,7 @@ import { TransactionManager } from 'src/decorators/manager.decorator'; import { EntityManager } from 'typeorm'; import { SearchUserRequestDto } from './dto/search-user-query.dto'; import { FriendshipDocs } from 'src/decorators/docs/friendship.decorator'; +import { GetReceivedFriendshipRequestCountDto } from './dto/get-received-friendship-request-count.dto'; @Controller('friendship') @ApiTags('friendship') @@ -93,6 +94,16 @@ export class FriendshipController { return await this.friendshipService.getReceivedWaitingFriendList(user.id); } + @UseGuards(JwtAuthGuard) + @Get('received/count') + async getReceivedFriendshipRequestCount( + @User() user: AuthorizedUserDto, + ): Promise { + return await this.friendshipService.getReceivedFriendshipRequestCount( + user.id, + ); + } + @UseGuards(JwtAuthGuard) @Get('sent') async getSentWaitingFriendList( diff --git a/src/friendship/friendship.repository.ts b/src/friendship/friendship.repository.ts index b24976fc..bdf5ef3a 100644 --- a/src/friendship/friendship.repository.ts +++ b/src/friendship/friendship.repository.ts @@ -112,4 +112,22 @@ export class FriendshipRepository extends Repository { ], }); } + + async countReceivedFriendships( + userId: number, + ): Promise<{ totalCount: number; unreadCount: number }> { + const result = await this.createQueryBuilder('friendship') + .select([ + 'COUNT(*) AS totalCount', + 'COALESCE(SUM(CASE WHEN friendship.isRead = false THEN 1 ELSE 0 END), 0) AS unreadCount', + ]) + .where('friendship.toUserId = :userId', { userId }) + .andWhere('friendship.areWeFriend = false') + .getRawOne(); + + return { + totalCount: parseInt(result.totalCount, 10), + unreadCount: parseInt(result.unreadCount, 10), + }; + } } diff --git a/src/friendship/friendship.service.ts b/src/friendship/friendship.service.ts index cef9ca4a..cfbefbac 100644 --- a/src/friendship/friendship.service.ts +++ b/src/friendship/friendship.service.ts @@ -1,3 +1,4 @@ +import { GetReceivedFriendshipRequestCountDto } from './dto/get-received-friendship-request-count.dto'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { FriendshipRepository } from './friendship.repository'; @@ -176,6 +177,15 @@ export class FriendshipService { return waitingFriendList; } + async getReceivedFriendshipRequestCount( + userId: number, + ): Promise { + const { totalCount, unreadCount } = + await this.friendshipRepository.countReceivedFriendships(userId); + + return new GetReceivedFriendshipRequestCountDto(totalCount, unreadCount); + } + async getSentWaitingFriendList( userId: number, ): Promise { From 49682d09ed905e7009f2b98d7850f849f33750ca Mon Sep 17 00:00:00 2001 From: JeongYeonSeung Date: Sat, 25 Jan 2025 17:13:22 +0900 Subject: [PATCH 16/48] =?UTF-8?q?mod::=20=EB=B0=9B=EC=9D=80=20=EC=B9=9C?= =?UTF-8?q?=EA=B5=AC=20=EC=9A=94=EC=B2=AD=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C?= =?UTF-8?q?=20=EC=9D=BD=EC=9D=8C=20=EC=B2=98=EB=A6=AC=EB=90=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - friendship entity의 isRead를 true로 update --- src/friendship/friendship.controller.ts | 7 ++++++- src/friendship/friendship.repository.ts | 14 ------------- src/friendship/friendship.service.ts | 28 +++++++++++++++---------- 3 files changed, 23 insertions(+), 26 deletions(-) diff --git a/src/friendship/friendship.controller.ts b/src/friendship/friendship.controller.ts index 889b8262..59bd45e4 100644 --- a/src/friendship/friendship.controller.ts +++ b/src/friendship/friendship.controller.ts @@ -88,10 +88,15 @@ export class FriendshipController { @UseGuards(JwtAuthGuard) @Get('received') + @UseInterceptors(TransactionInterceptor) async getReceivedWaitingFriendList( + @TransactionManager() transactionManager: EntityManager, @User() user: AuthorizedUserDto, ): Promise { - return await this.friendshipService.getReceivedWaitingFriendList(user.id); + return await this.friendshipService.getReceivedWaitingFriendList( + transactionManager, + user.id, + ); } @UseGuards(JwtAuthGuard) diff --git a/src/friendship/friendship.repository.ts b/src/friendship/friendship.repository.ts index bdf5ef3a..f9f0ac27 100644 --- a/src/friendship/friendship.repository.ts +++ b/src/friendship/friendship.repository.ts @@ -85,20 +85,6 @@ export class FriendshipRepository extends Repository { .getMany(); } - async findReceivedFriendshipsByUserId( - userId: number, - ): Promise { - return await this.find({ - where: [{ toUserId: userId, areWeFriend: false }], - relations: [ - 'fromUser', - 'toUser', - 'fromUser.character', - 'toUser.character', - ], - }); - } - async findSentFriendshipsByUserId( userId: number, ): Promise { diff --git a/src/friendship/friendship.service.ts b/src/friendship/friendship.service.ts index cfbefbac..67275d8d 100644 --- a/src/friendship/friendship.service.ts +++ b/src/friendship/friendship.service.ts @@ -157,24 +157,30 @@ export class FriendshipService { } async getReceivedWaitingFriendList( + transactionManager: EntityManager, userId: number, ): Promise { - const friendshipRequests = - await this.friendshipRepository.findReceivedFriendshipsByUserId(userId); + const receivedFriendshipRequests = await transactionManager.find( + FriendshipEntity, + { + where: { toUserId: userId, areWeFriend: false }, + relations: ['fromUser', 'fromUser.character'], + }, + ); - if (friendshipRequests.length === 0) { + if (receivedFriendshipRequests.length === 0) { return []; } - const waitingFriendList = friendshipRequests.map((friendshipRequest) => { - const waitingFriend = friendshipRequest.fromUser; - return new GetWaitingFriendResponseDto( - friendshipRequest.id, - waitingFriend, - ); - }); + await transactionManager.update( + FriendshipEntity, + { toUserId: userId, areWeFriend: false, isRead: false }, + { isRead: true }, + ); - return waitingFriendList; + return receivedFriendshipRequests.map((r) => { + return new GetWaitingFriendResponseDto(r.id, r.fromUser); + }); } async getReceivedFriendshipRequestCount( From 76881fcb94a8cdc13c233ac0f47da04aa426382d Mon Sep 17 00:00:00 2001 From: Devheun <86945971+Devheun@users.noreply.github.com> Date: Mon, 27 Jan 2025 21:45:58 +0900 Subject: [PATCH 17/48] =?UTF-8?q?refactor::=20Course=20=EC=BB=A8=ED=8A=B8?= =?UTF-8?q?=EB=A1=A4=EB=9F=AC=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EB=8D=B0?= =?UTF-8?q?=EC=BD=94=EB=A0=88=EC=9D=B4=ED=84=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/course/course.controller.ts | 148 +-------------- src/decorators/docs/course.decorator.ts | 240 +----------------------- 2 files changed, 16 insertions(+), 372 deletions(-) diff --git a/src/course/course.controller.ts b/src/course/course.controller.ts index a5bd4832..09d3ff30 100644 --- a/src/course/course.controller.ts +++ b/src/course/course.controller.ts @@ -1,17 +1,10 @@ -import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common'; +import { Controller, Get, Query, UseGuards } from '@nestjs/common'; import { CourseService } from './course.service'; import { ApiTags } from '@nestjs/swagger'; -import { CommonCourseResponseDto } from './dto/common-course-response.dto'; import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard'; -import { SearchCourseCodeDto } from './dto/search-course-code.dto'; -import { SearchCourseNameDto } from './dto/search-course-name.dto'; -import { SearchProfessorNameDto } from './dto/search-professor-name.dto'; import { PaginatedCoursesDto } from './dto/paginated-courses.dto'; import { CourseDocs } from 'src/decorators/docs/course.decorator'; -import { GetGeneralCourseDto } from './dto/get-general-course.dto'; -import { GetMajorCourseDto } from './dto/get-major-course.dto'; -import { GetAcademicFoundationCourseDto } from './dto/get-academic-foundation-course.dto'; -import { SearchCoursesWithKeywordDto } from './dto/search-courses-with-keyword.dto'; +import { SearchCourseNewDto } from './dto/search-course-new.dto'; @ApiTags('course') @CourseDocs @@ -20,139 +13,10 @@ export class CourseController { constructor(private courseService: CourseService) {} @UseGuards(JwtAuthGuard) - @Get('search-all') - async searchAllCourses( - @Query() searchCoursesWithKeywordDto: SearchCoursesWithKeywordDto, + @Get() + async searchCourses( + @Query() searchCourseNewDto: SearchCourseNewDto, ): Promise { - return await this.courseService.searchAllCourses( - searchCoursesWithKeywordDto, - ); - } - - @UseGuards(JwtAuthGuard) - @Get('search-major') - async searchMajorCourses( - @Query('major') major: string, - @Query() searchCoursesWithKeywordDto: SearchCoursesWithKeywordDto, - ): Promise { - return await this.courseService.searchMajorCourses( - major, - searchCoursesWithKeywordDto, - ); - } - - @UseGuards(JwtAuthGuard) - @Get('search-general') - async searchGeneralCourses( - @Query() searchCoursesWithKeywordDto: SearchCoursesWithKeywordDto, - ): Promise { - return await this.courseService.searchGeneralCourses( - searchCoursesWithKeywordDto, - ); - } - - @UseGuards(JwtAuthGuard) - @Get('search-academic-foundation') - async searchAcademicFoundationCourses( - @Query('college') college: string, - @Query() searchCoursesWithKeywordDto: SearchCoursesWithKeywordDto, - ): Promise { - return await this.courseService.searchAcademicFoundationCourses( - college, - searchCoursesWithKeywordDto, - ); - } - - // 학수번호 검색 - @UseGuards(JwtAuthGuard) - @Get('search-course-code') - async searchCourseCode( - @Query() searchCourseCodeDto: SearchCourseCodeDto, - ): Promise { - return await this.courseService.searchCourseCode(searchCourseCodeDto); - } - - // 전공 -- 과목명 검색 - @UseGuards(JwtAuthGuard) - @Get('search-major-course-name') - async searchMajorCourseName( - @Query('major') major: string, - @Query() searchCourseNameDto: SearchCourseNameDto, - ): Promise { - return await this.courseService.searchMajorCourseName( - major, - searchCourseNameDto, - ); - } - - // 교양 - 과목명 검색 - @UseGuards(JwtAuthGuard) - @Get('search-general-course-name') - async searchGeneralCourseName( - @Query() searchCourseNameDto: SearchCourseNameDto, - ): Promise { - return await this.courseService.searchGeneralCourseName( - searchCourseNameDto, - ); - } - - // 전공 - 교수님 성함 검색 - @UseGuards(JwtAuthGuard) - @Get('search-major-professor-name') - async searchMajorProfessorName( - @Query('major') major: string, - @Query() searchProfessorNameDto: SearchProfessorNameDto, - ): Promise { - return await this.courseService.searchMajorProfessorName( - major, - searchProfessorNameDto, - ); - } - - // 교양 - 교수님 성함 검색 - @UseGuards(JwtAuthGuard) - @Get('search-general-professor-name') - async searchGeneralProfessorName( - @Query() searchProfessorNameDto: SearchProfessorNameDto, - ): Promise { - return await this.courseService.searchGeneralProfessorName( - searchProfessorNameDto, - ); - } - - // 교양 리스트 - @UseGuards(JwtAuthGuard) - @Get('general') - async getGeneralCourses( - @Query() getGeneralCourseDto: GetGeneralCourseDto, - ): Promise { - return await this.courseService.getGeneralCourses(getGeneralCourseDto); - } - - // 전공 리스트 (학부별) - @UseGuards(JwtAuthGuard) - @Get('major') - async getMajorCourses( - @Query() getMajorCourseDto: GetMajorCourseDto, - ): Promise { - return await this.courseService.getMajorCourses(getMajorCourseDto); - } - - // 학문의 기초 리스트 - @UseGuards(JwtAuthGuard) - @Get('academic-foundation') - async getAcademicFoundationCourses( - @Query() getAcademicFoundationCourseDto: GetAcademicFoundationCourseDto, - ): Promise { - return await this.courseService.getAcademicFoundationCourses( - getAcademicFoundationCourseDto, - ); - } - - @Get('/:courseId') - async getCourse( - @Param('courseId') courseId: number, - ): Promise { - return await this.courseService.getCourse(courseId); + return await this.courseService.searchCourses(searchCourseNewDto); } } diff --git a/src/decorators/docs/course.decorator.ts b/src/decorators/docs/course.decorator.ts index 50d15880..49c335ee 100644 --- a/src/decorators/docs/course.decorator.ts +++ b/src/decorators/docs/course.decorator.ts @@ -1,281 +1,61 @@ import { ApiBearerAuth, ApiOperation, - ApiParam, ApiQuery, ApiResponse, } from '@nestjs/swagger'; import { MethodNames } from 'src/common/types/method'; import { CourseController } from 'src/course/course.controller'; -import { CommonCourseResponseDto } from 'src/course/dto/common-course-response.dto'; import { PaginatedCoursesDto } from 'src/course/dto/paginated-courses.dto'; import { ApiKukeyExceptionResponse } from '../api-kukey-exception-response'; type CourseEndPoints = MethodNames; const CourseDocsMap: Record = { - searchAllCourses: [ + searchCourses: [ ApiBearerAuth('accessToken'), ApiOperation({ - summary: 'keyword로 전체 강의 검색', - description: 'keyword를 입력하여 전체 강의에서 검색합니다.', - }), - ApiResponse({ - status: 200, - description: 'keyword로 강의 검색 성공 시', - type: PaginatedCoursesDto, - }), - ], - searchMajorCourses: [ - ApiBearerAuth('accessToken'), - ApiOperation({ - summary: 'keyword로 전공 강의 검색', - description: 'keyword를 입력하여 전공 강의에서 검색합니다.', - }), - ApiQuery({ - name: 'major', - required: true, - type: 'string', - }), - ApiResponse({ - status: 200, - description: 'keyword로 강의 검색 성공 시', - type: PaginatedCoursesDto, - }), - ApiKukeyExceptionResponse(['MAJOR_REQUIRED']), - ], - searchGeneralCourses: [ - ApiBearerAuth('accessToken'), - ApiOperation({ - summary: 'keyword로 교양 강의 검색', - description: 'keyword를 입력하여 교양 강의에서 검색합니다.', - }), - ApiResponse({ - status: 200, - description: 'keyword로 강의 검색 성공 시', - type: PaginatedCoursesDto, - }), - ], - searchAcademicFoundationCourses: [ - ApiBearerAuth('accessToken'), - ApiOperation({ - summary: 'keyword로 학문의 기초 강의 검색', - description: - 'keyword를 입력하여 단과대 별 학문의 기초 강의에서 검색합니다.', - }), - ApiQuery({ - name: 'college', - required: true, - type: 'string', - }), - ApiResponse({ - status: 200, - description: 'keyword로 강의 검색 성공 시', - type: PaginatedCoursesDto, - }), - ApiKukeyExceptionResponse(['COLLEGE_REQUIRED']), - ], - searchCourseCode: [ - ApiBearerAuth('accessToken'), - ApiOperation({ - summary: '학수번호로 강의 검색', - description: '학수번호를 입력하여 강의를 검색합니다.', - }), - ApiQuery({ - name: 'courseCode', - required: true, - type: 'string', + summary: '강의 검색', + description: '하나의 엔드포인트로 모든 강의검색 로직을 통합했습니다.', }), ApiQuery({ name: 'cursorId', required: false, type: 'number', }), - ApiResponse({ - status: 200, - description: '학수번호로 강의 검색 성공 시', - type: PaginatedCoursesDto, - }), - ], - searchMajorCourseName: [ - ApiBearerAuth('accessToken'), - ApiOperation({ - summary: '전공 과목명 강의 검색', - description: '전공 과목명을 입력하여 강의를 검색합니다.', - }), ApiQuery({ - name: 'major', + name: 'year', required: true, type: 'string', }), ApiQuery({ - name: 'courseName', + name: 'semester', required: true, type: 'string', }), ApiQuery({ - name: 'cursorId', - required: false, - type: 'number', - }), - ApiResponse({ - status: 200, - description: '전공 과목명으로 강의 검색 성공 시', - type: PaginatedCoursesDto, - }), - ApiKukeyExceptionResponse(['MAJOR_REQUIRED']), - ], - searchGeneralCourseName: [ - ApiBearerAuth('accessToken'), - ApiOperation({ - summary: '교양 과목명 강의 검색', - description: '교양 과목명을 입력하여 강의를 검색합니다.', - }), - ApiQuery({ - name: 'courseName', + name: 'category', required: true, - type: 'string', + type: 'enum', }), ApiQuery({ - name: 'cursorId', + name: 'keyword', required: false, - type: 'number', - }), - ApiResponse({ - status: 200, - description: '교양 과목명으로 강의 검색 성공 시', - type: PaginatedCoursesDto, - }), - ], - searchMajorProfessorName: [ - ApiBearerAuth('accessToken'), - ApiOperation({ - summary: '전공 과목 담당 교수님 성함으로 강의 검색', - description: '전공 과목 담당 교수님 성함을 입력하여 강의를 검색합니다.', - }), - ApiQuery({ - name: 'major', - required: true, - type: 'string', - }), - ApiQuery({ - name: 'professorName', - required: true, type: 'string', }), ApiQuery({ - name: 'cursorId', + name: 'classification', required: false, - type: 'number', - }), - ApiResponse({ - status: 200, - description: '전공 과목 담당 교수님 성함으로 강의 검색 성공 시', - type: PaginatedCoursesDto, - }), - ApiKukeyExceptionResponse(['MAJOR_REQUIRED']), - ], - searchGeneralProfessorName: [ - ApiBearerAuth('accessToken'), - ApiOperation({ - summary: '교양 담당 교수님 성함으로 강의 검색', - description: '교양 담당 교수님 성함을 입력하여 강의를 검색합니다.', - }), - ApiQuery({ - name: 'professorName', - required: true, type: 'string', }), - ApiQuery({ - name: 'cursorId', - required: false, - type: 'number', - }), - ApiResponse({ - status: 200, - description: '교양 담당 교수님 성함으로 강의 검색 성공 시', - type: PaginatedCoursesDto, - }), - ], - getGeneralCourses: [ - ApiBearerAuth('accessToken'), - ApiOperation({ - summary: '교양 강의 조회', - description: '모든 교양 강의를 조회합니다.', - }), - ApiQuery({ - name: 'cursorId', - required: false, - type: 'number', - }), ApiResponse({ status: 200, - description: '교양 강의 조회 성공 시', - type: PaginatedCoursesDto, - }), - ], - getMajorCourses: [ - ApiBearerAuth('accessToken'), - ApiOperation({ - summary: '전공 강의 조회', - description: '해당 과의 모든 전공 강의를 조회합니다.', - }), - ApiQuery({ - name: 'major', - required: true, - type: 'string', - }), - ApiQuery({ - name: 'cursorId', - required: false, - type: 'number', - }), - ApiResponse({ - status: 200, - description: '전공 강의 조회 성공 시', + description: '강의 검색 성공 시', type: PaginatedCoursesDto, }), ApiKukeyExceptionResponse(['MAJOR_REQUIRED']), - ], - getAcademicFoundationCourses: [ - ApiBearerAuth('accessToken'), - ApiOperation({ - summary: '학문의 기초 강의 조회', - description: '해당 단과대의 모든 학문의 기초 강의를 조회합니다.', - }), - ApiQuery({ - name: 'college', - required: true, - type: 'string', - }), - ApiQuery({ - name: 'cursorId', - required: false, - type: 'number', - }), - ApiResponse({ - status: 200, - description: '학문의 기초 강의 조회 성공 시', - type: PaginatedCoursesDto, - }), ApiKukeyExceptionResponse(['COLLEGE_REQUIRED']), ], - getCourse: [ - ApiOperation({ - summary: '특정 강의 조회', - description: '특정 강의를 조회합니다.', - }), - ApiParam({ - name: 'courseId', - description: '특정 강의 ID', - }), - ApiResponse({ - status: 200, - description: '특정 강의 조회 성공 시', - type: CommonCourseResponseDto, - }), - ApiKukeyExceptionResponse(['COURSE_NOT_FOUND']), - ], }; export function CourseDocs(target: typeof CourseController) { From 6ed09fa9664b9b82a1b1f83671c32eef9bf0214a Mon Sep 17 00:00:00 2001 From: Devheun <86945971+Devheun@users.noreply.github.com> Date: Mon, 27 Jan 2025 21:46:45 +0900 Subject: [PATCH 18/48] =?UTF-8?q?refactor::=20=EA=B0=95=EC=9D=98=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EC=A0=84=EB=9E=B5=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../academic-foundation-search-strategy.ts | 64 +++++++++++++++++-- .../strategy/all-courses-search-strategy.ts | 54 ++++++++++++++++ src/course/strategy/course-search-strategy.ts | 9 +++ .../strategy/general-search-strategy.ts | 58 +++++++++++++++-- src/course/strategy/major-search-strategy.ts | 64 +++++++++++++++++-- 5 files changed, 234 insertions(+), 15 deletions(-) create mode 100644 src/course/strategy/all-courses-search-strategy.ts create mode 100644 src/course/strategy/course-search-strategy.ts diff --git a/src/course/strategy/academic-foundation-search-strategy.ts b/src/course/strategy/academic-foundation-search-strategy.ts index 89623af1..84008d63 100644 --- a/src/course/strategy/academic-foundation-search-strategy.ts +++ b/src/course/strategy/academic-foundation-search-strategy.ts @@ -1,9 +1,63 @@ -import { SearchStrategy } from './search-strategy'; +import { CourseCategory } from 'src/enums/course-category.enum'; +import { CourseSearchStrategy } from './course-search-strategy'; +import { PaginatedCoursesDto } from '../dto/paginated-courses.dto'; +import { SearchCourseNewDto } from '../dto/search-course-new.dto'; +import { throwKukeyException } from 'src/utils/exception.util'; +import { CourseService } from '../course.service'; +import { Brackets } from 'typeorm'; +import { Injectable } from '@nestjs/common'; -const ACADEMIC_FOUNDATION = 'Academic Foundations'; +@Injectable() +export class AcademicFoundationSearchStrategy implements CourseSearchStrategy { + constructor(private readonly courseService: CourseService) {} + supports(category: CourseCategory): boolean { + return category === CourseCategory.ACADEMIC_FOUNDATIONS; + } + + async search( + searchCourseNewDto: SearchCourseNewDto, + ): Promise { + if (!searchCourseNewDto.classification) { + throwKukeyException('COLLEGE_REQUIRED'); + } + const courseRepository = await this.courseService.getCourseRepository(); + const { keyword, cursorId, year, semester } = searchCourseNewDto; + + const LIMIT = PaginatedCoursesDto.LIMIT; + + let queryBuilder = courseRepository + .createQueryBuilder('course') + .leftJoinAndSelect('course.courseDetails', 'courseDetails') + .where('course.year = :year', { year }) + .andWhere('course.semester = :semester', { semester }) + .andWhere('course.category = :category', { + category: CourseCategory.ACADEMIC_FOUNDATIONS, + }) + .andWhere('course.college = :college', { + college: searchCourseNewDto.classification, + }); + + queryBuilder = queryBuilder.andWhere( + new Brackets((qb) => { + qb.where('course.courseName LIKE :keyword', { keyword: `%${keyword}%` }) + .orWhere('course.professorName LIKE :keyword', { + keyword: `%${keyword}%`, + }) + .orWhere('course.courseCode LIKE :keyword', { + keyword: `%${keyword}%`, + }); + }), + ); + + if (cursorId) { + queryBuilder = queryBuilder.andWhere('course.id > :cursorId', { + cursorId, + }); + } + + queryBuilder = queryBuilder.orderBy('course.id', 'ASC').take(LIMIT); -export class AcademicFoundationSearchStrategy implements SearchStrategy { - supports(category: string): boolean { - return category === ACADEMIC_FOUNDATION; + const courses = await queryBuilder.getMany(); + return await this.courseService.mappingCourseDetailsToCourses(courses); } } diff --git a/src/course/strategy/all-courses-search-strategy.ts b/src/course/strategy/all-courses-search-strategy.ts new file mode 100644 index 00000000..10cc4f5f --- /dev/null +++ b/src/course/strategy/all-courses-search-strategy.ts @@ -0,0 +1,54 @@ +import { CourseCategory } from 'src/enums/course-category.enum'; +import { Injectable } from '@nestjs/common'; +import { CourseSearchStrategy } from './course-search-strategy'; +import { PaginatedCoursesDto } from '../dto/paginated-courses.dto'; +import { SearchCourseNewDto } from '../dto/search-course-new.dto'; +import { CourseService } from '../course.service'; +import { Brackets } from 'typeorm'; + +@Injectable() +export class AllCoursesSearchStrategy implements CourseSearchStrategy { + constructor(private readonly courseService: CourseService) {} + + supports(category: CourseCategory): boolean { + return category === CourseCategory.ALL_CLASS; + } + + async search( + searchCourseNewDto: SearchCourseNewDto, + ): Promise { + const courseRepository = await this.courseService.getCourseRepository(); + const { keyword, cursorId, year, semester } = searchCourseNewDto; + + const LIMIT = PaginatedCoursesDto.LIMIT; + + let queryBuilder = courseRepository + .createQueryBuilder('course') + .leftJoinAndSelect('course.courseDetails', 'courseDetails') + .where('course.year = :year', { year }) + .andWhere('course.semester = :semester', { semester }); + + queryBuilder = queryBuilder.andWhere( + new Brackets((qb) => { + qb.where('course.courseName LIKE :keyword', { keyword: `%${keyword}%` }) + .orWhere('course.professorName LIKE :keyword', { + keyword: `%${keyword}%`, + }) + .orWhere('course.courseCode LIKE :keyword', { + keyword: `%${keyword}%`, + }); + }), + ); + + if (cursorId) { + queryBuilder = queryBuilder.andWhere('course.id > :cursorId', { + cursorId, + }); + } + + queryBuilder = queryBuilder.orderBy('course.id', 'ASC').take(LIMIT); + + const courses = await queryBuilder.getMany(); + return await this.courseService.mappingCourseDetailsToCourses(courses); + } +} diff --git a/src/course/strategy/course-search-strategy.ts b/src/course/strategy/course-search-strategy.ts new file mode 100644 index 00000000..e7963d73 --- /dev/null +++ b/src/course/strategy/course-search-strategy.ts @@ -0,0 +1,9 @@ +import { CourseCategory } from 'src/enums/course-category.enum'; +import { PaginatedCoursesDto } from '../dto/paginated-courses.dto'; +import { SearchCourseNewDto } from '../dto/search-course-new.dto'; + +export interface CourseSearchStrategy { + supports(category: CourseCategory): boolean; + + search(searchCourseNewDto: SearchCourseNewDto): Promise; +} diff --git a/src/course/strategy/general-search-strategy.ts b/src/course/strategy/general-search-strategy.ts index 6073cb1b..a7c786fa 100644 --- a/src/course/strategy/general-search-strategy.ts +++ b/src/course/strategy/general-search-strategy.ts @@ -1,9 +1,57 @@ -import { SearchStrategy } from './search-strategy'; +import { CourseCategory } from 'src/enums/course-category.enum'; +import { Injectable } from '@nestjs/common'; +import { CourseSearchStrategy } from './course-search-strategy'; +import { SearchCourseNewDto } from '../dto/search-course-new.dto'; +import { PaginatedCoursesDto } from '../dto/paginated-courses.dto'; +import { CourseService } from '../course.service'; +import { Brackets } from 'typeorm'; -const GENERAL = 'General Studies'; +@Injectable() +export class GeneralSearchStrategy implements CourseSearchStrategy { + constructor(private readonly courseService: CourseService) {} -export class GeneralSearchStrategy implements SearchStrategy { - supports(category: string): boolean { - return category === GENERAL; + supports(category: CourseCategory): boolean { + return category === CourseCategory.GENERAL_STUDIES; + } + + async search( + searchCourseNewDto: SearchCourseNewDto, + ): Promise { + const courseRepository = await this.courseService.getCourseRepository(); + const { keyword, cursorId, year, semester } = searchCourseNewDto; + + const LIMIT = PaginatedCoursesDto.LIMIT; + + let queryBuilder = courseRepository + .createQueryBuilder('course') + .leftJoinAndSelect('course.courseDetails', 'courseDetails') + .where('course.year = :year', { year }) + .andWhere('course.semester = :semester', { semester }) + .andWhere('course.category = :category', { + category: CourseCategory.GENERAL_STUDIES, + }); + + queryBuilder = queryBuilder.andWhere( + new Brackets((qb) => { + qb.where('course.courseName LIKE :keyword', { keyword: `%${keyword}%` }) + .orWhere('course.professorName LIKE :keyword', { + keyword: `%${keyword}%`, + }) + .orWhere('course.courseCode LIKE :keyword', { + keyword: `%${keyword}%`, + }); + }), + ); + + if (cursorId) { + queryBuilder = queryBuilder.andWhere('course.id > :cursorId', { + cursorId, + }); + } + + queryBuilder = queryBuilder.orderBy('course.id', 'ASC').take(LIMIT); + + const courses = await queryBuilder.getMany(); + return await this.courseService.mappingCourseDetailsToCourses(courses); } } diff --git a/src/course/strategy/major-search-strategy.ts b/src/course/strategy/major-search-strategy.ts index e983f574..0b9520c6 100644 --- a/src/course/strategy/major-search-strategy.ts +++ b/src/course/strategy/major-search-strategy.ts @@ -1,9 +1,63 @@ -import { SearchStrategy } from './search-strategy'; +import { Injectable } from '@nestjs/common'; +import { CourseCategory } from 'src/enums/course-category.enum'; +import { CourseSearchStrategy } from './course-search-strategy'; +import { SearchCourseNewDto } from '../dto/search-course-new.dto'; +import { PaginatedCoursesDto } from '../dto/paginated-courses.dto'; +import { CourseService } from '../course.service'; +import { throwKukeyException } from 'src/utils/exception.util'; +import { Brackets } from 'typeorm'; -const MAJOR = 'Major'; +@Injectable() +export class MajorSearchStrategy implements CourseSearchStrategy { + constructor(private readonly courseService: CourseService) {} + supports(category: CourseCategory): boolean { + return category === CourseCategory.MAJOR; + } + + async search( + searchCourseNewDto: SearchCourseNewDto, + ): Promise { + if (!searchCourseNewDto.classification) { + throwKukeyException('MAJOR_REQUIRED'); + } + const courseRepository = await this.courseService.getCourseRepository(); + const { keyword, cursorId, year, semester } = searchCourseNewDto; + + const LIMIT = PaginatedCoursesDto.LIMIT; + + let queryBuilder = courseRepository + .createQueryBuilder('course') + .leftJoinAndSelect('course.courseDetails', 'courseDetails') + .where('course.year = :year', { year }) + .andWhere('course.semester = :semester', { semester }) + .andWhere('course.category = :category', { + category: CourseCategory.MAJOR, + }) + .andWhere('course.major = :major', { + major: searchCourseNewDto.classification, + }); + + queryBuilder = queryBuilder.andWhere( + new Brackets((qb) => { + qb.where('course.courseName LIKE :keyword', { keyword: `%${keyword}%` }) + .orWhere('course.professorName LIKE :keyword', { + keyword: `%${keyword}%`, + }) + .orWhere('course.courseCode LIKE :keyword', { + keyword: `%${keyword}%`, + }); + }), + ); + + if (cursorId) { + queryBuilder = queryBuilder.andWhere('course.id > :cursorId', { + cursorId, + }); + } + + queryBuilder = queryBuilder.orderBy('course.id', 'ASC').take(LIMIT); -export class MajorSearchStrategy implements SearchStrategy { - supports(category: string): boolean { - return category === MAJOR; + const courses = await queryBuilder.getMany(); + return await this.courseService.mappingCourseDetailsToCourses(courses); } } From 47aecd10980db9068567191163c3b284d4e19ab6 Mon Sep 17 00:00:00 2001 From: Devheun <86945971+Devheun@users.noreply.github.com> Date: Mon, 27 Jan 2025 21:47:14 +0900 Subject: [PATCH 19/48] =?UTF-8?q?refactor::=20=EA=B0=95=EC=9D=98=EA=B2=80?= =?UTF-8?q?=EC=83=89=20service=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/course/course.service.ts | 425 ++---------------------- src/course/dto/search-course-new.dto.ts | 2 +- src/course/strategy/search-strategy.ts | 3 - 3 files changed, 25 insertions(+), 405 deletions(-) delete mode 100644 src/course/strategy/search-strategy.ts diff --git a/src/course/course.service.ts b/src/course/course.service.ts index 686fe7a6..67055751 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -1,77 +1,26 @@ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { CourseRepository } from './course.repository'; import { CourseEntity } from 'src/entities/course.entity'; import { CourseDetailEntity } from 'src/entities/course-detail.entity'; import { CourseDetailRepository } from './course-detail.repository'; -import { Brackets, EntityManager, Like, MoreThan } from 'typeorm'; +import { EntityManager, Like } from 'typeorm'; import { CommonCourseResponseDto } from './dto/common-course-response.dto'; -import { SearchCourseCodeDto } from './dto/search-course-code.dto'; -import { SearchCourseNameDto } from './dto/search-course-name.dto'; -import { SearchProfessorNameDto } from './dto/search-professor-name.dto'; import { PaginatedCoursesDto } from './dto/paginated-courses.dto'; import { throwKukeyException } from 'src/utils/exception.util'; -import { GetGeneralCourseDto } from './dto/get-general-course.dto'; -import { GetMajorCourseDto } from './dto/get-major-course.dto'; -import { GetAcademicFoundationCourseDto } from './dto/get-academic-foundation-course.dto'; -import { SearchCoursesWithKeywordDto } from './dto/search-courses-with-keyword.dto'; +import { SearchCourseNewDto } from './dto/search-course-new.dto'; +import { CourseSearchStrategy } from './strategy/course-search-strategy'; @Injectable() export class CourseService { constructor( private courseRepository: CourseRepository, private courseDetailRepository: CourseDetailRepository, + @Inject('CourseSearchStrategy') + private readonly strategies: CourseSearchStrategy[], ) {} - async searchAllCourses( - searchCoursesWithKeywordDto: SearchCoursesWithKeywordDto, - ): Promise { - const courses = await this.runSearchCoursesQuery( - searchCoursesWithKeywordDto, - ); - return await this.mappingCourseDetailsToCourses(courses); - } - - async searchMajorCourses( - major: string, - searchCoursesWithKeywordDto: SearchCoursesWithKeywordDto, - ): Promise { - if (!major) throwKukeyException('MAJOR_REQUIRED'); - - const courses = await this.runSearchCoursesQuery( - searchCoursesWithKeywordDto, - { - major, - category: 'Major', - }, - ); - return await this.mappingCourseDetailsToCourses(courses); - } - - async searchGeneralCourses( - searchCoursesWithKeywordDto: SearchCoursesWithKeywordDto, - ): Promise { - const courses = await this.runSearchCoursesQuery( - searchCoursesWithKeywordDto, - { - category: 'General Studies', - }, - ); - return await this.mappingCourseDetailsToCourses(courses); - } - - async searchAcademicFoundationCourses( - college: string, - searchCoursesWithKeywordDto: SearchCoursesWithKeywordDto, - ): Promise { - if (!college) throwKukeyException('COLLEGE_REQUIRED'); - const courses = await this.runSearchCoursesQuery( - searchCoursesWithKeywordDto, - { - college, - category: 'Academic Foundations', - }, - ); - return await this.mappingCourseDetailsToCourses(courses); + async getCourseRepository(): Promise { + return this.courseRepository; } async getCourse(courseId: number): Promise { @@ -124,298 +73,6 @@ export class CourseService { }); } - // 학수번호 검색 - async searchCourseCode( - searchCourseCodeDto: SearchCourseCodeDto, - ): Promise { - let courses: CourseEntity[] = []; - if (searchCourseCodeDto.cursorId) { - courses = await this.courseRepository.find({ - where: { - courseCode: Like(`${searchCourseCodeDto.courseCode}%`), - id: MoreThan(searchCourseCodeDto.cursorId), - year: searchCourseCodeDto.year, - semester: searchCourseCodeDto.semester, - }, - order: { id: 'ASC' }, - take: 21, - relations: ['courseDetails'], - }); - } else { - courses = await this.courseRepository.find({ - where: { - courseCode: Like(`${searchCourseCodeDto.courseCode}%`), - year: searchCourseCodeDto.year, - semester: searchCourseCodeDto.semester, - }, - order: { id: 'ASC' }, - take: 21, - relations: ['courseDetails'], - }); - } - return await this.mappingCourseDetailsToCourses(courses); - } - - // 전공 과목명 검색 (최소 3글자 이상 입력 ) - async searchMajorCourseName( - major: string, - searchCourseNameDto: SearchCourseNameDto, - ): Promise { - if (!major) throwKukeyException('MAJOR_REQUIRED'); - - let courses = []; - - if (searchCourseNameDto.cursorId) { - courses = await this.courseRepository.find({ - where: { - courseName: Like(`%${searchCourseNameDto.courseName}%`), - major: major, - category: 'Major', - id: MoreThan(searchCourseNameDto.cursorId), - year: searchCourseNameDto.year, - semester: searchCourseNameDto.semester, - }, - order: { id: 'ASC' }, - take: 21, - relations: ['courseDetails'], - }); - } else { - courses = await this.courseRepository.find({ - where: { - courseName: Like(`%${searchCourseNameDto.courseName}%`), - major: major, - category: 'Major', - year: searchCourseNameDto.year, - semester: searchCourseNameDto.semester, - }, - order: { id: 'ASC' }, - take: 21, - relations: ['courseDetails'], - }); - } - - return await this.mappingCourseDetailsToCourses(courses); - } - - // 전공 교수님 성함 검색 - async searchMajorProfessorName( - major: string, - searchProfessorNameDto: SearchProfessorNameDto, - ): Promise { - if (!major) { - throwKukeyException('MAJOR_REQUIRED'); - } - let courses = []; - - if (searchProfessorNameDto.cursorId) { - courses = await this.courseRepository.find({ - where: { - professorName: Like(`%${searchProfessorNameDto.professorName}%`), - major: major, - category: 'Major', - id: MoreThan(searchProfessorNameDto.cursorId), - year: searchProfessorNameDto.year, - semester: searchProfessorNameDto.semester, - }, - order: { id: 'ASC' }, - take: 21, - relations: ['courseDetails'], - }); - } else { - courses = await this.courseRepository.find({ - where: { - professorName: Like(`%${searchProfessorNameDto.professorName}%`), - major: major, - category: 'Major', - year: searchProfessorNameDto.year, - semester: searchProfessorNameDto.semester, - }, - order: { id: 'ASC' }, - take: 21, - relations: ['courseDetails'], - }); - } - - return await this.mappingCourseDetailsToCourses(courses); - } - - // 교양 과목명 검색 (최소 3글자 이상 입력) - async searchGeneralCourseName( - searchCourseNameDto: SearchCourseNameDto, - ): Promise { - let courses = []; - - if (searchCourseNameDto.cursorId) { - courses = await this.courseRepository.find({ - where: { - courseName: Like(`%${searchCourseNameDto.courseName}%`), - category: 'General Studies', - id: MoreThan(searchCourseNameDto.cursorId), - year: searchCourseNameDto.year, - semester: searchCourseNameDto.semester, - }, - order: { id: 'ASC' }, - take: 21, - relations: ['courseDetails'], - }); - } else { - courses = await this.courseRepository.find({ - where: { - courseName: Like(`%${searchCourseNameDto.courseName}%`), - category: 'General Studies', - year: searchCourseNameDto.year, - semester: searchCourseNameDto.semester, - }, - order: { id: 'ASC' }, - take: 21, - relations: ['courseDetails'], - }); - } - - return await this.mappingCourseDetailsToCourses(courses); - } - - // 교양 교수님 성함 검색 - async searchGeneralProfessorName( - searchProfessorNameDto: SearchProfessorNameDto, - ): Promise { - let courses = []; - - if (searchProfessorNameDto.cursorId) { - courses = await this.courseRepository.find({ - where: { - professorName: Like(`%${searchProfessorNameDto.professorName}%`), - category: 'General Studies', - id: MoreThan(searchProfessorNameDto.cursorId), - year: searchProfessorNameDto.year, - semester: searchProfessorNameDto.semester, - }, - order: { id: 'ASC' }, - take: 21, - relations: ['courseDetails'], - }); - } else { - courses = await this.courseRepository.find({ - where: { - professorName: Like(`%${searchProfessorNameDto.professorName}%`), - category: 'General Studies', - year: searchProfessorNameDto.year, - semester: searchProfessorNameDto.semester, - }, - order: { id: 'ASC' }, - take: 21, - relations: ['courseDetails'], - }); - } - - return await this.mappingCourseDetailsToCourses(courses); - } - - // 교양 리스트 반환 - async getGeneralCourses( - getGeneralCourseDto: GetGeneralCourseDto, - ): Promise { - let courses = []; - if (getGeneralCourseDto.cursorId) { - courses = await this.courseRepository.find({ - where: { - category: 'General Studies', - id: MoreThan(getGeneralCourseDto.cursorId), - year: getGeneralCourseDto.year, - semester: getGeneralCourseDto.semester, - }, - order: { id: 'ASC' }, - take: 21, - relations: ['courseDetails'], - }); - } else { - courses = await this.courseRepository.find({ - where: { - category: 'General Studies', - year: getGeneralCourseDto.year, - semester: getGeneralCourseDto.semester, - }, - order: { id: 'ASC' }, - take: 21, - relations: ['courseDetails'], - }); - } - - return await this.mappingCourseDetailsToCourses(courses); - } - - // 전공 리스트 반환 - async getMajorCourses( - getMajorCourseDto: GetMajorCourseDto, - ): Promise { - if (!getMajorCourseDto.major) throwKukeyException('MAJOR_REQUIRED'); - let courses = []; - if (getMajorCourseDto.cursorId) { - courses = await this.courseRepository.find({ - where: { - category: 'Major', - major: getMajorCourseDto.major, - id: MoreThan(getMajorCourseDto.cursorId), - year: getMajorCourseDto.year, - semester: getMajorCourseDto.semester, - }, - order: { id: 'ASC' }, - take: 21, - relations: ['courseDetails'], - }); - } else { - courses = await this.courseRepository.find({ - where: { - category: 'Major', - major: getMajorCourseDto.major, - year: getMajorCourseDto.year, - semester: getMajorCourseDto.semester, - }, - order: { id: 'ASC' }, - take: 21, - relations: ['courseDetails'], - }); - } - - return await this.mappingCourseDetailsToCourses(courses); - } - - // 학문의 기초 리스트 반환 - async getAcademicFoundationCourses( - getAcademicFoundationCourseDto: GetAcademicFoundationCourseDto, - ): Promise { - if (!getAcademicFoundationCourseDto.college) - throwKukeyException('COLLEGE_REQUIRED'); - let courses = []; - if (getAcademicFoundationCourseDto.cursorId) { - courses = await this.courseRepository.find({ - where: { - category: 'Academic Foundations', - college: getAcademicFoundationCourseDto.college, - id: MoreThan(getAcademicFoundationCourseDto.cursorId), - year: getAcademicFoundationCourseDto.year, - semester: getAcademicFoundationCourseDto.semester, - }, - order: { id: 'ASC' }, - take: 21, - relations: ['courseDetails'], - }); - } else { - courses = await this.courseRepository.find({ - where: { - category: 'Academic Foundations', - college: getAcademicFoundationCourseDto.college, - year: getAcademicFoundationCourseDto.year, - semester: getAcademicFoundationCourseDto.semester, - }, - order: { id: 'ASC' }, - take: 21, - relations: ['courseDetails'], - }); - } - return await this.mappingCourseDetailsToCourses(courses); - } - async updateCourseTotalRate( courseIds: number[], totalRate: number, @@ -437,60 +94,26 @@ export class CourseService { return new PaginatedCoursesDto(courseInformations); } - private async runSearchCoursesQuery( - searchCoursesWithKeywordDto: SearchCoursesWithKeywordDto, - options?: { major?: string; college?: string; category?: string }, - ): Promise { - const { keyword, cursorId, year, semester } = searchCoursesWithKeywordDto; - - const LIMIT = PaginatedCoursesDto.LIMIT; - - let queryBuilder = this.courseRepository - .createQueryBuilder('course') - .leftJoinAndSelect('course.courseDetails', 'courseDetails') - .where('course.year = :year', { year }) - .andWhere('course.semester = :semester', { semester }); - - // Optional: 추가 조건 적용 - if (options?.major) { - queryBuilder = queryBuilder.andWhere('course.major = :major', { - major: options.major, - }); - } - - if (options?.college) { - queryBuilder = queryBuilder.andWhere('course.college = :college', { - college: options.college, - }); - } - - if (options?.category) { - queryBuilder = queryBuilder.andWhere('course.category = :category', { - category: options.category, - }); - } + async searchCourses( + searchCourseNewDto: SearchCourseNewDto, + ): Promise { + // 해당하는 검색 전략 찾아오기 + const searchStrategy = await this.findSearchStrategy(searchCourseNewDto); + return await searchStrategy.search(searchCourseNewDto); + } - // 검색 조건(LIKE) - queryBuilder = queryBuilder.andWhere( - new Brackets((qb) => { - qb.where('course.courseName LIKE :keyword', { keyword: `%${keyword}%` }) - .orWhere('course.professorName LIKE :keyword', { - keyword: `%${keyword}%`, - }) - .orWhere('course.courseCode LIKE :keyword', { - keyword: `%${keyword}%`, - }); - }), + private async findSearchStrategy( + searchCourseNewDto: SearchCourseNewDto, + ): Promise { + const { category } = searchCourseNewDto; + const searchStrategy = this.strategies.find((strategy) => + strategy.supports(category), ); - if (cursorId) { - queryBuilder = queryBuilder.andWhere('course.id > :cursorId', { - cursorId, - }); + if (!searchStrategy) { + throwKukeyException('COURSE_SEARCH_STRATEGY_NOT_FOUND'); } - queryBuilder = queryBuilder.orderBy('course.id', 'ASC').take(LIMIT); - - return await queryBuilder.getMany(); + return searchStrategy; } } diff --git a/src/course/dto/search-course-new.dto.ts b/src/course/dto/search-course-new.dto.ts index 120a93fd..cbcd812b 100644 --- a/src/course/dto/search-course-new.dto.ts +++ b/src/course/dto/search-course-new.dto.ts @@ -36,7 +36,7 @@ export class SearchCourseNewDto { @ApiPropertyOptional({ description: - 'Major일때 major를, Academic Foundation일 때 college를 넣어주세요.', + 'cateogry가 Major일때 특정 과를, category가 Academic Foundation일 때 특정 단과대를 넣어주세요.', }) @IsString() @IsOptional() diff --git a/src/course/strategy/search-strategy.ts b/src/course/strategy/search-strategy.ts deleted file mode 100644 index d254910f..00000000 --- a/src/course/strategy/search-strategy.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface SearchStrategy { - supports(category: string): boolean; -} From 1dbcec1817194c9407e12bd13263847910347c0f Mon Sep 17 00:00:00 2001 From: Devheun <86945971+Devheun@users.noreply.github.com> Date: Mon, 27 Jan 2025 21:47:27 +0900 Subject: [PATCH 20/48] =?UTF-8?q?refactor::=20=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/course/course.module.ts | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/course/course.module.ts b/src/course/course.module.ts index 6b21e6e1..082e538c 100644 --- a/src/course/course.module.ts +++ b/src/course/course.module.ts @@ -6,11 +6,43 @@ import { CourseDetailRepository } from './course-detail.repository'; import { TypeOrmModule } from '@nestjs/typeorm'; import { CourseEntity } from 'src/entities/course.entity'; import { CourseDetailEntity } from 'src/entities/course-detail.entity'; +import { AcademicFoundationSearchStrategy } from './strategy/academic-foundation-search-strategy'; +import { AllCoursesSearchStrategy } from './strategy/all-courses-search-strategy'; +import { GeneralSearchStrategy } from './strategy/general-search-strategy'; +import { MajorSearchStrategy } from './strategy/major-search-strategy'; @Module({ imports: [TypeOrmModule.forFeature([CourseEntity, CourseDetailEntity])], controllers: [CourseController], - providers: [CourseService, CourseRepository, CourseDetailRepository], + providers: [ + CourseService, + CourseRepository, + CourseDetailRepository, + AcademicFoundationSearchStrategy, + AllCoursesSearchStrategy, + GeneralSearchStrategy, + MajorSearchStrategy, + { + provide: 'CourseSearchStrategy', + useFactory: ( + academicFoundationSearchStrategy: AcademicFoundationSearchStrategy, + allCoursesSearchStrategy: AllCoursesSearchStrategy, + generalSearchStrategy: GeneralSearchStrategy, + majorSearchStrategy: MajorSearchStrategy, + ) => [ + academicFoundationSearchStrategy, + allCoursesSearchStrategy, + generalSearchStrategy, + majorSearchStrategy, + ], + inject: [ + AcademicFoundationSearchStrategy, + AllCoursesSearchStrategy, + GeneralSearchStrategy, + MajorSearchStrategy, + ], + }, + ], exports: [CourseService], }) export class CourseModule {} From 8f85ce52cfe54c4c109d515ab326f60f61933863 Mon Sep 17 00:00:00 2001 From: Devheun <86945971+Devheun@users.noreply.github.com> Date: Tue, 28 Jan 2025 02:03:41 +0900 Subject: [PATCH 21/48] =?UTF-8?q?refactor::=20=ED=95=84=EC=9A=94=EC=97=86?= =?UTF-8?q?=EB=8A=94=20CourseCategory=20enum=EA=B0=92=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/enums/course-category.enum.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/enums/course-category.enum.ts b/src/enums/course-category.enum.ts index 4199f8eb..64497345 100644 --- a/src/enums/course-category.enum.ts +++ b/src/enums/course-category.enum.ts @@ -1,5 +1,4 @@ export enum CourseCategory { - ALL_CLASS = 'All Class', MAJOR = 'Major', GENERAL_STUDIES = 'General Studies', ACADEMIC_FOUNDATIONS = 'Academic Foundations', From 4679553aecccf77a5a3d125e710d61565d8d03c0 Mon Sep 17 00:00:00 2001 From: Devheun <86945971+Devheun@users.noreply.github.com> Date: Tue, 28 Jan 2025 02:04:48 +0900 Subject: [PATCH 22/48] =?UTF-8?q?refactor::=20enum=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20dto=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/course/dto/search-course-new.dto.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/course/dto/search-course-new.dto.ts b/src/course/dto/search-course-new.dto.ts index cbcd812b..a6ec3b74 100644 --- a/src/course/dto/search-course-new.dto.ts +++ b/src/course/dto/search-course-new.dto.ts @@ -20,12 +20,15 @@ export class SearchCourseNewDto { @Length(1) semester: string; - @ApiProperty({ - description: '강의 카테고리 (모든 강의, 전공, 교양, 학문의 기초)', + @ApiPropertyOptional({ + description: + '강의 카테고리 (모든 강의, 전공, 교양, 학문의 기초), 모든 강의는 값을 넘겨주지 않음', enum: CourseCategory, + nullable: true, }) + @IsOptional() @IsEnum(CourseCategory) - category: CourseCategory; + category?: CourseCategory; @ApiPropertyOptional({ description: '검색 키워드 (강의명, 교수명, 학수번호)', From 420362900a2b70d5ba578234e027ff83ced3cae9 Mon Sep 17 00:00:00 2001 From: Devheun <86945971+Devheun@users.noreply.github.com> Date: Tue, 28 Jan 2025 02:06:32 +0900 Subject: [PATCH 23/48] =?UTF-8?q?refactor::=20=EC=88=9C=ED=99=98=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=ED=95=B4=EA=B2=B0=20=EB=B0=8F=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/course/course.module.ts | 10 ++-- src/course/course.service.ts | 45 +++++++++++++++- .../academic-foundation-search-strategy.ts | 48 +++-------------- .../strategy/all-courses-search-strategy.ts | 50 +++-------------- src/course/strategy/course-search-strategy.ts | 8 ++- .../strategy/general-search-strategy.ts | 53 +++---------------- src/course/strategy/major-search-strategy.ts | 48 +++-------------- 7 files changed, 86 insertions(+), 176 deletions(-) diff --git a/src/course/course.module.ts b/src/course/course.module.ts index 082e538c..eecf6409 100644 --- a/src/course/course.module.ts +++ b/src/course/course.module.ts @@ -7,9 +7,9 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { CourseEntity } from 'src/entities/course.entity'; import { CourseDetailEntity } from 'src/entities/course-detail.entity'; import { AcademicFoundationSearchStrategy } from './strategy/academic-foundation-search-strategy'; -import { AllCoursesSearchStrategy } from './strategy/all-courses-search-strategy'; import { GeneralSearchStrategy } from './strategy/general-search-strategy'; import { MajorSearchStrategy } from './strategy/major-search-strategy'; +import { AllCoursesSearchStrategy } from './strategy/all-courses-search-strategy'; @Module({ imports: [TypeOrmModule.forFeature([CourseEntity, CourseDetailEntity])], @@ -19,27 +19,27 @@ import { MajorSearchStrategy } from './strategy/major-search-strategy'; CourseRepository, CourseDetailRepository, AcademicFoundationSearchStrategy, - AllCoursesSearchStrategy, GeneralSearchStrategy, MajorSearchStrategy, + AllCoursesSearchStrategy, { provide: 'CourseSearchStrategy', useFactory: ( academicFoundationSearchStrategy: AcademicFoundationSearchStrategy, - allCoursesSearchStrategy: AllCoursesSearchStrategy, generalSearchStrategy: GeneralSearchStrategy, majorSearchStrategy: MajorSearchStrategy, + allCoursesSearchStrategy: AllCoursesSearchStrategy, ) => [ academicFoundationSearchStrategy, - allCoursesSearchStrategy, generalSearchStrategy, majorSearchStrategy, + allCoursesSearchStrategy, ], inject: [ AcademicFoundationSearchStrategy, - AllCoursesSearchStrategy, GeneralSearchStrategy, MajorSearchStrategy, + AllCoursesSearchStrategy, ], }, ], diff --git a/src/course/course.service.ts b/src/course/course.service.ts index 67055751..c27268da 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -3,7 +3,7 @@ import { CourseRepository } from './course.repository'; import { CourseEntity } from 'src/entities/course.entity'; import { CourseDetailEntity } from 'src/entities/course-detail.entity'; import { CourseDetailRepository } from './course-detail.repository'; -import { EntityManager, Like } from 'typeorm'; +import { Brackets, EntityManager, Like } from 'typeorm'; import { CommonCourseResponseDto } from './dto/common-course-response.dto'; import { PaginatedCoursesDto } from './dto/paginated-courses.dto'; import { throwKukeyException } from 'src/utils/exception.util'; @@ -97,9 +97,50 @@ export class CourseService { async searchCourses( searchCourseNewDto: SearchCourseNewDto, ): Promise { + const { keyword, cursorId } = searchCourseNewDto; + const LIMIT = PaginatedCoursesDto.LIMIT; // 해당하는 검색 전략 찾아오기 const searchStrategy = await this.findSearchStrategy(searchCourseNewDto); - return await searchStrategy.search(searchCourseNewDto); + + let queryBuilder = this.courseRepository + .createQueryBuilder('course') + .leftJoinAndSelect('course.courseDetails', 'courseDetails') + .where('course.year = :year', { year: searchCourseNewDto.year }) + .andWhere('course.semester = :semester', { + semester: searchCourseNewDto.semester, + }); + + queryBuilder = await searchStrategy.buildQuery( + queryBuilder, + searchCourseNewDto, + ); + + if (keyword) { + queryBuilder = queryBuilder.andWhere( + new Brackets((qb) => { + qb.where('course.courseName LIKE :keyword', { + keyword: `%${keyword}%`, + }) + .orWhere('course.professorName LIKE :keyword', { + keyword: `%${keyword}%`, + }) + .orWhere('course.courseCode LIKE :keyword', { + keyword: `%${keyword}%`, + }); + }), + ); + } + + if (cursorId) { + queryBuilder = queryBuilder.andWhere('course.id > :cursorId', { + cursorId, + }); + } + + queryBuilder = queryBuilder.orderBy('course.id', 'ASC').take(LIMIT); + + const courses = await queryBuilder.getMany(); + return await this.mappingCourseDetailsToCourses(courses); } private async findSearchStrategy( diff --git a/src/course/strategy/academic-foundation-search-strategy.ts b/src/course/strategy/academic-foundation-search-strategy.ts index 84008d63..3910d8e3 100644 --- a/src/course/strategy/academic-foundation-search-strategy.ts +++ b/src/course/strategy/academic-foundation-search-strategy.ts @@ -1,63 +1,31 @@ import { CourseCategory } from 'src/enums/course-category.enum'; import { CourseSearchStrategy } from './course-search-strategy'; -import { PaginatedCoursesDto } from '../dto/paginated-courses.dto'; import { SearchCourseNewDto } from '../dto/search-course-new.dto'; import { throwKukeyException } from 'src/utils/exception.util'; -import { CourseService } from '../course.service'; -import { Brackets } from 'typeorm'; +import { SelectQueryBuilder } from 'typeorm'; import { Injectable } from '@nestjs/common'; +import { CourseEntity } from 'src/entities/course.entity'; @Injectable() export class AcademicFoundationSearchStrategy implements CourseSearchStrategy { - constructor(private readonly courseService: CourseService) {} supports(category: CourseCategory): boolean { return category === CourseCategory.ACADEMIC_FOUNDATIONS; } - async search( + async buildQuery( + queryBuilder: SelectQueryBuilder, searchCourseNewDto: SearchCourseNewDto, - ): Promise { + ): Promise> { if (!searchCourseNewDto.classification) { throwKukeyException('COLLEGE_REQUIRED'); } - const courseRepository = await this.courseService.getCourseRepository(); - const { keyword, cursorId, year, semester } = searchCourseNewDto; - const LIMIT = PaginatedCoursesDto.LIMIT; + const { classification } = searchCourseNewDto; - let queryBuilder = courseRepository - .createQueryBuilder('course') - .leftJoinAndSelect('course.courseDetails', 'courseDetails') - .where('course.year = :year', { year }) - .andWhere('course.semester = :semester', { semester }) + return queryBuilder .andWhere('course.category = :category', { category: CourseCategory.ACADEMIC_FOUNDATIONS, }) - .andWhere('course.college = :college', { - college: searchCourseNewDto.classification, - }); - - queryBuilder = queryBuilder.andWhere( - new Brackets((qb) => { - qb.where('course.courseName LIKE :keyword', { keyword: `%${keyword}%` }) - .orWhere('course.professorName LIKE :keyword', { - keyword: `%${keyword}%`, - }) - .orWhere('course.courseCode LIKE :keyword', { - keyword: `%${keyword}%`, - }); - }), - ); - - if (cursorId) { - queryBuilder = queryBuilder.andWhere('course.id > :cursorId', { - cursorId, - }); - } - - queryBuilder = queryBuilder.orderBy('course.id', 'ASC').take(LIMIT); - - const courses = await queryBuilder.getMany(); - return await this.courseService.mappingCourseDetailsToCourses(courses); + .andWhere('course.college = :college', { college: classification }); } } diff --git a/src/course/strategy/all-courses-search-strategy.ts b/src/course/strategy/all-courses-search-strategy.ts index 10cc4f5f..eda531a2 100644 --- a/src/course/strategy/all-courses-search-strategy.ts +++ b/src/course/strategy/all-courses-search-strategy.ts @@ -1,54 +1,20 @@ import { CourseCategory } from 'src/enums/course-category.enum'; import { Injectable } from '@nestjs/common'; import { CourseSearchStrategy } from './course-search-strategy'; -import { PaginatedCoursesDto } from '../dto/paginated-courses.dto'; +import { CourseEntity } from 'src/entities/course.entity'; +import { SelectQueryBuilder } from 'typeorm'; import { SearchCourseNewDto } from '../dto/search-course-new.dto'; -import { CourseService } from '../course.service'; -import { Brackets } from 'typeorm'; @Injectable() export class AllCoursesSearchStrategy implements CourseSearchStrategy { - constructor(private readonly courseService: CourseService) {} - supports(category: CourseCategory): boolean { - return category === CourseCategory.ALL_CLASS; + return !category; } - async search( - searchCourseNewDto: SearchCourseNewDto, - ): Promise { - const courseRepository = await this.courseService.getCourseRepository(); - const { keyword, cursorId, year, semester } = searchCourseNewDto; - - const LIMIT = PaginatedCoursesDto.LIMIT; - - let queryBuilder = courseRepository - .createQueryBuilder('course') - .leftJoinAndSelect('course.courseDetails', 'courseDetails') - .where('course.year = :year', { year }) - .andWhere('course.semester = :semester', { semester }); - - queryBuilder = queryBuilder.andWhere( - new Brackets((qb) => { - qb.where('course.courseName LIKE :keyword', { keyword: `%${keyword}%` }) - .orWhere('course.professorName LIKE :keyword', { - keyword: `%${keyword}%`, - }) - .orWhere('course.courseCode LIKE :keyword', { - keyword: `%${keyword}%`, - }); - }), - ); - - if (cursorId) { - queryBuilder = queryBuilder.andWhere('course.id > :cursorId', { - cursorId, - }); - } - - queryBuilder = queryBuilder.orderBy('course.id', 'ASC').take(LIMIT); - - const courses = await queryBuilder.getMany(); - return await this.courseService.mappingCourseDetailsToCourses(courses); + async buildQuery( + queryBuilder: SelectQueryBuilder, + searchCourseNewDto?: SearchCourseNewDto, + ): Promise> { + return queryBuilder; } } diff --git a/src/course/strategy/course-search-strategy.ts b/src/course/strategy/course-search-strategy.ts index e7963d73..0cab2a0a 100644 --- a/src/course/strategy/course-search-strategy.ts +++ b/src/course/strategy/course-search-strategy.ts @@ -1,9 +1,13 @@ import { CourseCategory } from 'src/enums/course-category.enum'; -import { PaginatedCoursesDto } from '../dto/paginated-courses.dto'; import { SearchCourseNewDto } from '../dto/search-course-new.dto'; +import { SelectQueryBuilder } from 'typeorm'; +import { CourseEntity } from 'src/entities/course.entity'; export interface CourseSearchStrategy { supports(category: CourseCategory): boolean; - search(searchCourseNewDto: SearchCourseNewDto): Promise; + buildQuery( + queryBuilder: SelectQueryBuilder, + searchCourseNewDto?: SearchCourseNewDto, + ): Promise>; } diff --git a/src/course/strategy/general-search-strategy.ts b/src/course/strategy/general-search-strategy.ts index a7c786fa..026e6b4c 100644 --- a/src/course/strategy/general-search-strategy.ts +++ b/src/course/strategy/general-search-strategy.ts @@ -1,57 +1,20 @@ import { CourseCategory } from 'src/enums/course-category.enum'; import { Injectable } from '@nestjs/common'; import { CourseSearchStrategy } from './course-search-strategy'; -import { SearchCourseNewDto } from '../dto/search-course-new.dto'; -import { PaginatedCoursesDto } from '../dto/paginated-courses.dto'; -import { CourseService } from '../course.service'; -import { Brackets } from 'typeorm'; +import { SelectQueryBuilder } from 'typeorm'; +import { CourseEntity } from 'src/entities/course.entity'; @Injectable() export class GeneralSearchStrategy implements CourseSearchStrategy { - constructor(private readonly courseService: CourseService) {} - supports(category: CourseCategory): boolean { return category === CourseCategory.GENERAL_STUDIES; } - async search( - searchCourseNewDto: SearchCourseNewDto, - ): Promise { - const courseRepository = await this.courseService.getCourseRepository(); - const { keyword, cursorId, year, semester } = searchCourseNewDto; - - const LIMIT = PaginatedCoursesDto.LIMIT; - - let queryBuilder = courseRepository - .createQueryBuilder('course') - .leftJoinAndSelect('course.courseDetails', 'courseDetails') - .where('course.year = :year', { year }) - .andWhere('course.semester = :semester', { semester }) - .andWhere('course.category = :category', { - category: CourseCategory.GENERAL_STUDIES, - }); - - queryBuilder = queryBuilder.andWhere( - new Brackets((qb) => { - qb.where('course.courseName LIKE :keyword', { keyword: `%${keyword}%` }) - .orWhere('course.professorName LIKE :keyword', { - keyword: `%${keyword}%`, - }) - .orWhere('course.courseCode LIKE :keyword', { - keyword: `%${keyword}%`, - }); - }), - ); - - if (cursorId) { - queryBuilder = queryBuilder.andWhere('course.id > :cursorId', { - cursorId, - }); - } - - queryBuilder = queryBuilder.orderBy('course.id', 'ASC').take(LIMIT); - - const courses = await queryBuilder.getMany(); - return await this.courseService.mappingCourseDetailsToCourses(courses); + async buildQuery( + queryBuilder: SelectQueryBuilder, + ): Promise> { + return queryBuilder.andWhere('course.category = :category', { + category: CourseCategory.GENERAL_STUDIES, + }); } } diff --git a/src/course/strategy/major-search-strategy.ts b/src/course/strategy/major-search-strategy.ts index 0b9520c6..9d4643bf 100644 --- a/src/course/strategy/major-search-strategy.ts +++ b/src/course/strategy/major-search-strategy.ts @@ -2,62 +2,30 @@ import { Injectable } from '@nestjs/common'; import { CourseCategory } from 'src/enums/course-category.enum'; import { CourseSearchStrategy } from './course-search-strategy'; import { SearchCourseNewDto } from '../dto/search-course-new.dto'; -import { PaginatedCoursesDto } from '../dto/paginated-courses.dto'; -import { CourseService } from '../course.service'; import { throwKukeyException } from 'src/utils/exception.util'; -import { Brackets } from 'typeorm'; +import { SelectQueryBuilder } from 'typeorm'; +import { CourseEntity } from 'src/entities/course.entity'; @Injectable() export class MajorSearchStrategy implements CourseSearchStrategy { - constructor(private readonly courseService: CourseService) {} supports(category: CourseCategory): boolean { return category === CourseCategory.MAJOR; } - async search( + async buildQuery( + queryBuilder: SelectQueryBuilder, searchCourseNewDto: SearchCourseNewDto, - ): Promise { + ): Promise> { if (!searchCourseNewDto.classification) { throwKukeyException('MAJOR_REQUIRED'); } - const courseRepository = await this.courseService.getCourseRepository(); - const { keyword, cursorId, year, semester } = searchCourseNewDto; - const LIMIT = PaginatedCoursesDto.LIMIT; + const { classification } = searchCourseNewDto; - let queryBuilder = courseRepository - .createQueryBuilder('course') - .leftJoinAndSelect('course.courseDetails', 'courseDetails') - .where('course.year = :year', { year }) - .andWhere('course.semester = :semester', { semester }) + return queryBuilder .andWhere('course.category = :category', { category: CourseCategory.MAJOR, }) - .andWhere('course.major = :major', { - major: searchCourseNewDto.classification, - }); - - queryBuilder = queryBuilder.andWhere( - new Brackets((qb) => { - qb.where('course.courseName LIKE :keyword', { keyword: `%${keyword}%` }) - .orWhere('course.professorName LIKE :keyword', { - keyword: `%${keyword}%`, - }) - .orWhere('course.courseCode LIKE :keyword', { - keyword: `%${keyword}%`, - }); - }), - ); - - if (cursorId) { - queryBuilder = queryBuilder.andWhere('course.id > :cursorId', { - cursorId, - }); - } - - queryBuilder = queryBuilder.orderBy('course.id', 'ASC').take(LIMIT); - - const courses = await queryBuilder.getMany(); - return await this.courseService.mappingCourseDetailsToCourses(courses); + .andWhere('course.major = :major', { major: classification }); } } From 423fad8d7459ae28f2fcbeee9cd1765f2c4295d6 Mon Sep 17 00:00:00 2001 From: Devheun <86945971+Devheun@users.noreply.github.com> Date: Sun, 2 Feb 2025 15:48:25 +0900 Subject: [PATCH 24/48] =?UTF-8?q?refactor::=20=EC=84=B1=ED=98=84's=20revie?= =?UTF-8?q?w=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/course/course.service.ts | 5 ----- src/course/dto/search-course-new.dto.ts | 4 ++-- src/decorators/docs/course.decorator.ts | 2 ++ 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/course/course.service.ts b/src/course/course.service.ts index c27268da..f7517a4b 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -18,11 +18,6 @@ export class CourseService { @Inject('CourseSearchStrategy') private readonly strategies: CourseSearchStrategy[], ) {} - - async getCourseRepository(): Promise { - return this.courseRepository; - } - async getCourse(courseId: number): Promise { const course = await this.courseRepository.findOne({ where: { id: courseId }, diff --git a/src/course/dto/search-course-new.dto.ts b/src/course/dto/search-course-new.dto.ts index a6ec3b74..f67effb5 100644 --- a/src/course/dto/search-course-new.dto.ts +++ b/src/course/dto/search-course-new.dto.ts @@ -12,12 +12,12 @@ export class SearchCourseNewDto { @ApiProperty({ description: '연도' }) @IsString() - @Length(4) + @Length(4, 4) year: string; @ApiProperty({ description: '학기' }) @IsString() - @Length(1) + @Length(1, 1) semester: string; @ApiPropertyOptional({ diff --git a/src/decorators/docs/course.decorator.ts b/src/decorators/docs/course.decorator.ts index 49c335ee..5ec3d7c0 100644 --- a/src/decorators/docs/course.decorator.ts +++ b/src/decorators/docs/course.decorator.ts @@ -8,6 +8,7 @@ import { MethodNames } from 'src/common/types/method'; import { CourseController } from 'src/course/course.controller'; import { PaginatedCoursesDto } from 'src/course/dto/paginated-courses.dto'; import { ApiKukeyExceptionResponse } from '../api-kukey-exception-response'; +import { CourseCategory } from 'src/enums/course-category.enum'; type CourseEndPoints = MethodNames; @@ -37,6 +38,7 @@ const CourseDocsMap: Record = { name: 'category', required: true, type: 'enum', + enum: CourseCategory, }), ApiQuery({ name: 'keyword', From 7d0f2ca74100cc5fbcdf35b1f79068bdabcc6395 Mon Sep 17 00:00:00 2001 From: Devheun <86945971+Devheun@users.noreply.github.com> Date: Sun, 2 Feb 2025 15:54:32 +0900 Subject: [PATCH 25/48] =?UTF-8?q?fix::=20swagger=20=EB=AA=85=EC=84=B8=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 - category는 안 넘겨줄때도 있어서 false로 수정 --- src/decorators/docs/course.decorator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/decorators/docs/course.decorator.ts b/src/decorators/docs/course.decorator.ts index 5ec3d7c0..4cf13492 100644 --- a/src/decorators/docs/course.decorator.ts +++ b/src/decorators/docs/course.decorator.ts @@ -36,7 +36,7 @@ const CourseDocsMap: Record = { }), ApiQuery({ name: 'category', - required: true, + required: false, type: 'enum', enum: CourseCategory, }), From d0d127b2dd152a58b8a18352d278db4ad70a8029 Mon Sep 17 00:00:00 2001 From: Kim SeongHyeon Date: Sun, 2 Feb 2025 16:19:37 +0900 Subject: [PATCH 26/48] =?UTF-8?q?rename:=20=ED=8C=8C=EC=9D=BC,=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EB=84=A4=EC=9D=B4=EB=B0=8D=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/decorators/docs/banner.decorator.ts | 4 ++-- src/home/banner/banner.controller.ts | 4 ++-- ...reateBannerRequest.dto.ts => create-banner-request.dto.ts} | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) rename src/home/banner/dto/{createBannerRequest.dto.ts => create-banner-request.dto.ts} (88%) diff --git a/src/decorators/docs/banner.decorator.ts b/src/decorators/docs/banner.decorator.ts index 560066c8..53c3e2de 100644 --- a/src/decorators/docs/banner.decorator.ts +++ b/src/decorators/docs/banner.decorator.ts @@ -9,7 +9,7 @@ import { import { MethodNames } from 'src/common/types/method'; import { BannerController } from 'src/home/banner/banner.controller'; import { bannerDto } from 'src/home/banner/dto/banner.dto'; -import { createBannerRequestDto } from 'src/home/banner/dto/createBannerRequest.dto'; +import { CreateBannerRequestDto } from 'src/home/banner/dto/create-banner-request.dto'; type BannerEndPoints = MethodNames; @@ -34,7 +34,7 @@ const BannerDocsMap: Record = { }), ApiConsumes('multipart/form-data'), ApiBody({ - type: createBannerRequestDto, + type: CreateBannerRequestDto, }), ApiResponse({ status: 201, diff --git a/src/home/banner/banner.controller.ts b/src/home/banner/banner.controller.ts index 4971cd04..cf20c28e 100644 --- a/src/home/banner/banner.controller.ts +++ b/src/home/banner/banner.controller.ts @@ -18,7 +18,7 @@ import { Role } from 'src/enums/role.enum'; import { FileInterceptor } from '@nestjs/platform-express'; import { ApiTags } from '@nestjs/swagger'; import { BannerDocs } from 'src/decorators/docs/banner.decorator'; -import { createBannerRequestDto } from 'src/home/banner/dto/createBannerRequest.dto'; +import { CreateBannerRequestDto } from 'src/home/banner/dto/create-banner-request.dto'; @Controller('banner') @ApiTags('banner') @@ -37,7 +37,7 @@ export class BannerController { @Post() async createBannerImage( @UploadedFile() image: Express.Multer.File, - @Body() body: createBannerRequestDto, + @Body() body: CreateBannerRequestDto, ): Promise { return await this.bannerService.createBannerImage(image, body.title); } diff --git a/src/home/banner/dto/createBannerRequest.dto.ts b/src/home/banner/dto/create-banner-request.dto.ts similarity index 88% rename from src/home/banner/dto/createBannerRequest.dto.ts rename to src/home/banner/dto/create-banner-request.dto.ts index 8804450f..de89ad82 100644 --- a/src/home/banner/dto/createBannerRequest.dto.ts +++ b/src/home/banner/dto/create-banner-request.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsNotEmpty } from 'class-validator'; -export class createBannerRequestDto { +export class CreateBannerRequestDto { @ApiProperty({ description: '배너 이미지 파일', type: 'string', From 605b6db6a1010964c06d1d2d9e48c886357b8f84 Mon Sep 17 00:00:00 2001 From: Devheun <86945971+Devheun@users.noreply.github.com> Date: Tue, 4 Feb 2025 23:53:41 +0900 Subject: [PATCH 27/48] =?UTF-8?q?fix:=20=EC=98=A4=ED=83=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20swagger=20=EB=AA=85=EC=84=B8=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 --- src/course/dto/search-course-new.dto.ts | 2 +- src/decorators/docs/course.decorator.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/course/dto/search-course-new.dto.ts b/src/course/dto/search-course-new.dto.ts index f67effb5..f4fb7bae 100644 --- a/src/course/dto/search-course-new.dto.ts +++ b/src/course/dto/search-course-new.dto.ts @@ -39,7 +39,7 @@ export class SearchCourseNewDto { @ApiPropertyOptional({ description: - 'cateogry가 Major일때 특정 과를, category가 Academic Foundation일 때 특정 단과대를 넣어주세요.', + 'category가 Major일때 특정 과를, category가 Academic Foundation일 때 특정 단과대를 넣어주세요.', }) @IsString() @IsOptional() diff --git a/src/decorators/docs/course.decorator.ts b/src/decorators/docs/course.decorator.ts index 4cf13492..16f74d3a 100644 --- a/src/decorators/docs/course.decorator.ts +++ b/src/decorators/docs/course.decorator.ts @@ -55,8 +55,7 @@ const CourseDocsMap: Record = { description: '강의 검색 성공 시', type: PaginatedCoursesDto, }), - ApiKukeyExceptionResponse(['MAJOR_REQUIRED']), - ApiKukeyExceptionResponse(['COLLEGE_REQUIRED']), + ApiKukeyExceptionResponse(['MAJOR_REQUIRED', 'COLLEGE_REQUIRED']), ], }; From 2c92974ffbcf3fd30456010648effed2b991f399 Mon Sep 17 00:00:00 2001 From: Kim SeongHyeon Date: Sun, 9 Feb 2025 11:14:59 +0900 Subject: [PATCH 28/48] =?UTF-8?q?fix:=20=EA=B0=80=EC=9E=85=EC=95=88?= =?UTF-8?q?=EB=90=9C=20=EC=9C=A0=EC=A0=80=EC=97=90=20=EB=8C=80=ED=95=9C=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/auth/auth.service.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index cc551884..86ff7c3b 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -359,6 +359,11 @@ export class AuthService { email: string, ): Promise { const user = await this.userService.findUserByEmail(email); + + if (!user) { + throwKukeyException('USER_NOT_FOUND'); + } + const tempPassword = this.generateRandomString(10); const isUpdated = await this.userService.updatePassword( From 81e773c1e70b602ba7f50c066fe0e5bb87a94703 Mon Sep 17 00:00:00 2001 From: JeongYeonSeung Date: Sun, 9 Feb 2025 13:22:27 +0900 Subject: [PATCH 29/48] =?UTF-8?q?feat::=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=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 --- src/decorators/docs/club.decorator.ts | 22 +++++++ src/home/club/club.controller.ts | 11 ++++ src/home/club/club.service.ts | 29 ++++++++++ .../club/dto/get-club-detail-request.dto.ts | 16 ++++++ .../club/dto/get-club-detail-response.dto.ts | 57 +++++++++++++++++++ 5 files changed, 135 insertions(+) create mode 100644 src/home/club/dto/get-club-detail-request.dto.ts create mode 100644 src/home/club/dto/get-club-detail-response.dto.ts diff --git a/src/decorators/docs/club.decorator.ts b/src/decorators/docs/club.decorator.ts index bcc39425..b21bb049 100644 --- a/src/decorators/docs/club.decorator.ts +++ b/src/decorators/docs/club.decorator.ts @@ -19,6 +19,7 @@ import { GetRecommendClubResponseDto } from 'src/home/club/dto/get-recommend-clu import { UpdateClubRequestDto } from 'src/home/club/dto/update-club-request-dto'; import { UpdateClubResponseDto } from 'src/home/club/dto/update-club-response-dto'; import { ApiKukeyExceptionResponse } from '../api-kukey-exception-response'; +import { GetClubDetailResponseDto } from 'src/home/club/dto/get-club-detail-response.dto'; type ClubEndPoints = MethodNames; @@ -61,6 +62,27 @@ const ClubDocsMap: Record = { }), ApiKukeyExceptionResponse(['LOGIN_REQUIRED']), ], + getClubDetail: [ + ApiOperation({ + summary: '동아리 상세 조회', + description: '동아리 상세 정보를 조회합니다.', + }), + ApiQuery({ + name: 'clubId', + description: 'club id', + required: true, + }), + ApiQuery({ + name: 'isLogin', + description: '로그인 여부', + required: true, + }), + ApiOkResponse({ + description: '동아리 상세 정보 반환', + type: GetClubDetailResponseDto, + }), + ApiKukeyExceptionResponse(['LOGIN_REQUIRED', 'CLUB_NOT_FOUND']), + ], toggleLikeClub: [ ApiOperation({ summary: '동아리 좋아요 등록/해제', diff --git a/src/home/club/club.controller.ts b/src/home/club/club.controller.ts index 663186fb..287fb5c8 100644 --- a/src/home/club/club.controller.ts +++ b/src/home/club/club.controller.ts @@ -34,6 +34,8 @@ import { CreateClubResponseDto } from './dto/create-club-response-dto'; import { UpdateClubRequestDto } from './dto/update-club-request-dto'; import { DeleteClubResponseDto } from './dto/delete-club-response-dto'; import { ClubDocs } from 'src/decorators/docs/club.decorator'; +import { GetClubDetailResponseDto } from './dto/get-club-detail-response.dto'; +import { GetClubDetailRequestDto } from './dto/get-club-detail-request.dto'; @Controller('club') @ApiTags('club') @@ -51,6 +53,15 @@ export class ClubController { return await this.clubService.getClubList(user, getClubRequestDto); } + @UseGuards(OptionalJwtAuthGuard) + @Get('/:clubId') + async getClubDetail( + @User() user: AuthorizedUserDto | null, + @Query() getClubDetailRequestDto: GetClubDetailRequestDto, + ): Promise { + return await this.clubService.getClubDetail(user, getClubDetailRequestDto); + } + @UseGuards(JwtAuthGuard) @UseInterceptors(TransactionInterceptor) @Post('/like/:clubId') diff --git a/src/home/club/club.service.ts b/src/home/club/club.service.ts index 1ff3acaf..a4e70a82 100644 --- a/src/home/club/club.service.ts +++ b/src/home/club/club.service.ts @@ -17,6 +17,8 @@ import { UpdateClubRequestDto } from './dto/update-club-request-dto'; import { UpdateClubResponseDto } from './dto/update-club-response-dto'; import { DeleteClubResponseDto } from './dto/delete-club-response-dto'; import { throwKukeyException } from 'src/utils/exception.util'; +import { GetClubDetailResponseDto } from './dto/get-club-detail-response.dto'; +import { GetClubDetailRequestDto } from './dto/get-club-detail-request.dto'; @Injectable() export class ClubService { @@ -64,6 +66,33 @@ export class ClubService { return clubList; } + async getClubDetail( + user: AuthorizedUserDto | null, + requetDto: GetClubDetailRequestDto, + ): Promise { + const { clubId, isLogin } = requetDto; + + // isLogin이 true이나 user가 없을 경우 refresh를 위해 401 던짐 + if (!user && isLogin) { + throwKukeyException('LOGIN_REQUIRED'); + } + + const club = await this.clubRepository.findOne({ + where: { id: clubId }, + relations: ['clubLikes', 'clubLikes.user'], + }); + + if (!club) { + throwKukeyException('CLUB_NOT_FOUND'); + } + + const isLiked = club.clubLikes.some((clubLike) => + user && isLogin && clubLike.user ? clubLike.user.id === user.id : false, + ); + + return new GetClubDetailResponseDto(club, isLiked); + } + async toggleLikeClub( transactionManager: EntityManager, userId: number, diff --git a/src/home/club/dto/get-club-detail-request.dto.ts b/src/home/club/dto/get-club-detail-request.dto.ts new file mode 100644 index 00000000..ac7ae441 --- /dev/null +++ b/src/home/club/dto/get-club-detail-request.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsBoolean, IsNotEmpty, IsNumber } from 'class-validator'; +import { ToBoolean } from 'src/decorators/to-boolean.decorator'; + +export class GetClubDetailRequestDto { + @IsNotEmpty() + @IsNumber() + @ApiProperty({ description: 'club id' }) + clubId: number; + + @IsNotEmpty() + @ToBoolean() + @IsBoolean() + @ApiPropertyOptional({ description: '로그인 여부' }) + isLogin: boolean; +} diff --git a/src/home/club/dto/get-club-detail-response.dto.ts b/src/home/club/dto/get-club-detail-response.dto.ts new file mode 100644 index 00000000..a8732521 --- /dev/null +++ b/src/home/club/dto/get-club-detail-response.dto.ts @@ -0,0 +1,57 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ClubCategory } from 'src/common/types/club-category-type'; +import { ClubEntity } from 'src/entities/club.entity'; + +export class GetClubDetailResponseDto { + @ApiProperty({ description: 'club table의 PK' }) + clubId: number; + + @ApiProperty({ description: '동아리명' }) + name: string; + + @ApiProperty({ description: '동아리 카테고리' }) + category: ClubCategory; + + @ApiProperty({ description: '동아리 요약' }) + summary: string; + + @ApiProperty({ description: '정기 모임' }) + regularMeeting: string; + + @ApiProperty({ description: '모집 기간' }) + recruitmentPeriod: string; + + @ApiProperty({ description: '동아리 설명' }) + description: string; + + @ApiProperty({ description: '동아리 사진 URL 목록' }) + imageUrl: string[]; + + @ApiProperty({ description: '좋아요 개수' }) + likeCount: number; + + @ApiPropertyOptional({ description: '인스타그램 링크' }) + instagramLink: string; + + @ApiPropertyOptional({ description: '유튜브 링크' }) + youtubeLink: string; + + @ApiProperty({ description: '좋아요 여부' }) + isLiked: boolean; + + constructor(club: ClubEntity, isLiked: boolean) { + this.clubId = club.id; + this.name = club.name; + this.category = club.category; + this.summary = club.summary; + this.regularMeeting = club.regularMeeting; + this.recruitmentPeriod = club.recruitmentPeriod; + this.description = club.description; + this.imageUrl = []; + this.imageUrl[0] = club.imageUrl; + this.likeCount = club.allLikes; + this.instagramLink = club.instagramLink; + this.youtubeLink = club.youtubeLink; + this.isLiked = isLiked; + } +} From c9b15d029a0ba1bb33936c6389a1c67d9e5dc5c3 Mon Sep 17 00:00:00 2001 From: Kim SeongHyeon Date: Sun, 9 Feb 2025 14:47:50 +0900 Subject: [PATCH 30/48] =?UTF-8?q?feat:=20=EC=8B=A0=EA=B3=A0=20=EC=88=98?= =?UTF-8?q?=EB=9D=BD(=EC=9C=A0=EC=A0=80=20=EC=A0=95=EC=A7=80)=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/community/report/dto/accept-report.dto.ts | 9 ++++ src/community/report/report.controller.ts | 22 ++++++-- src/community/report/report.repository.ts | 15 ++++++ src/community/report/report.service.ts | 21 ++++++++ src/decorators/docs/report.decorator.ts | 34 +++++++++++- src/entities/report.entity.ts | 3 ++ src/entities/user-ban.entity.ts | 34 ++++++++++++ src/entities/user.entity.ts | 6 +++ src/user/user-ban.service.ts | 53 +++++++++++++++++++ src/user/user.module.ts | 9 +++- 10 files changed, 199 insertions(+), 7 deletions(-) create mode 100644 src/community/report/dto/accept-report.dto.ts create mode 100644 src/entities/user-ban.entity.ts create mode 100644 src/user/user-ban.service.ts diff --git a/src/community/report/dto/accept-report.dto.ts b/src/community/report/dto/accept-report.dto.ts new file mode 100644 index 00000000..0ce7fab5 --- /dev/null +++ b/src/community/report/dto/accept-report.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsNumber } from 'class-validator'; + +export class AcceptReportRequestDto { + @ApiProperty({ description: '정지 일 수' }) + @IsNotEmpty() + @IsNumber() + banDays: number; +} diff --git a/src/community/report/report.controller.ts b/src/community/report/report.controller.ts index 930a4b3f..8636390d 100644 --- a/src/community/report/report.controller.ts +++ b/src/community/report/report.controller.ts @@ -1,32 +1,46 @@ -import { Controller, Get, Param, Post, UseGuards } from '@nestjs/common'; +import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common'; import { ReportService } from './report.service'; import { GetReportListResponseDto } from './dto/get-report-list.dto'; -import { ApiTags } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; import { GetReportResponseDto } from './dto/get-report.dto'; import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard'; import { RolesGuard } from 'src/auth/guards/role.guard'; import { Roles } from 'src/decorators/roles.decorator'; import { Role } from 'src/enums/role.enum'; import { ReportDocs } from 'src/decorators/docs/report.decorator'; +import { AcceptReportRequestDto } from 'src/community/report/dto/accept-report.dto'; @Controller('report') @UseGuards(JwtAuthGuard, RolesGuard) @ApiTags('report') @ReportDocs +@ApiBearerAuth('accessToken') +@Roles(Role.admin) export class ReportController { constructor(private readonly reportService: ReportService) {} - @Roles(Role.admin) @Get() async getReportList(): Promise { return await this.reportService.getReportList(); } - @Roles(Role.admin) @Post('/:reportId') async getReport( @Param('reportId') reportId: number, ): Promise { return await this.reportService.getReport(reportId); } + + @Post('/:reportId/accept') + async acceptReport( + @Param('reportId') reportId: number, + @Body() body: AcceptReportRequestDto, + ): Promise { + await this.reportService.acceptReport(reportId, body); + } + + @Post('/:reportId/reject') + async rejectReport(@Param('reportId') reportId: number): Promise { + await this.reportService.rejectReport(reportId); + } } diff --git a/src/community/report/report.repository.ts b/src/community/report/report.repository.ts index 9568d5e9..69bfd4a9 100644 --- a/src/community/report/report.repository.ts +++ b/src/community/report/report.repository.ts @@ -42,6 +42,9 @@ export class ReportRepository extends Repository { async getReportList(): Promise { const reports = await this.find({ + where: { + isAccepted: false, + }, order: { createdAt: 'DESC', }, @@ -83,4 +86,16 @@ export class ReportRepository extends Repository { }, }); } + + async acceptReport(reportId: number): Promise { + const report = await this.getReport(reportId); + if (report.commentId) { + await this.update({ commentId: report.commentId }, { isAccepted: true }); + } else { + await this.update( + { postId: report.postId, commentId: IsNull() }, + { isAccepted: true }, + ); + } + } } diff --git a/src/community/report/report.service.ts b/src/community/report/report.service.ts index bc3c5d95..1ee776bb 100644 --- a/src/community/report/report.service.ts +++ b/src/community/report/report.service.ts @@ -5,12 +5,15 @@ import { GetReportListResponseDto } from './dto/get-report-list.dto'; import { GetReportResponseDto } from './dto/get-report.dto'; import { FileService } from 'src/common/file.service'; import { throwKukeyException } from 'src/utils/exception.util'; +import { UserBanService } from 'src/user/user-ban.service'; +import { AcceptReportRequestDto } from 'src/community/report/dto/accept-report.dto'; @Injectable() export class ReportService { constructor( private readonly reportRepository: ReportRepository, private readonly fileService: FileService, + private readonly userBanService: UserBanService, ) {} async createReport( @@ -56,4 +59,22 @@ export class ReportService { return response; } + + async acceptReport( + reportId: number, + dto: AcceptReportRequestDto, + ): Promise { + const report = await this.reportRepository.getReport(reportId); + const userId = report.commentId + ? report.comment.userId + : report.post.userId; + if (userId) { + await this.userBanService.banUser(userId, report.reason, dto.banDays); + } + await this.reportRepository.acceptReport(reportId); + } + + async rejectReport(reportId: number): Promise { + await this.reportRepository.acceptReport(reportId); + } } diff --git a/src/decorators/docs/report.decorator.ts b/src/decorators/docs/report.decorator.ts index 47e60c77..8e7d524e 100644 --- a/src/decorators/docs/report.decorator.ts +++ b/src/decorators/docs/report.decorator.ts @@ -1,5 +1,6 @@ -import { ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; +import { ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; import { MethodNames } from 'src/common/types/method'; +import { AcceptReportRequestDto } from 'src/community/report/dto/accept-report.dto'; import { GetReportListResponseDto } from 'src/community/report/dto/get-report-list.dto'; import { GetReportResponseDto } from 'src/community/report/dto/get-report.dto'; import { ReportController } from 'src/community/report/report.controller'; @@ -33,6 +34,37 @@ const ReportDocsMap: Record = { type: GetReportResponseDto, }), ], + acceptReport: [ + ApiOperation({ + summary: '신고 승인', + description: '신고를 승인합니다.', + }), + ApiParam({ + name: 'reportId', + description: '신고 고유 ID', + }), + ApiBody({ + type: AcceptReportRequestDto, + }), + ApiResponse({ + status: 201, + description: '신고 승인 성공', + }), + ], + rejectReport: [ + ApiOperation({ + summary: '신고 거부', + description: '신고를 거부합니다.', + }), + ApiParam({ + name: 'reportId', + description: '신고 고유 ID', + }), + ApiResponse({ + status: 201, + description: '신고 거부 성공', + }), + ], }; export function ReportDocs(target: typeof ReportController) { diff --git a/src/entities/report.entity.ts b/src/entities/report.entity.ts index b7080f30..31512704 100644 --- a/src/entities/report.entity.ts +++ b/src/entities/report.entity.ts @@ -27,6 +27,9 @@ export class ReportEntity extends CommonEntity { @Column('varchar', { nullable: false }) reason: string; + @Column('boolean', { default: false }) + isAccepted: boolean; + @JoinColumn({ name: 'reporterId' }) @ManyToOne(() => UserEntity, (userEntity) => userEntity.reports, { onDelete: 'SET NULL', diff --git a/src/entities/user-ban.entity.ts b/src/entities/user-ban.entity.ts new file mode 100644 index 00000000..dbc9ad85 --- /dev/null +++ b/src/entities/user-ban.entity.ts @@ -0,0 +1,34 @@ +import { CommonEntity } from 'src/entities/common.entity'; +import { UserEntity } from 'src/entities/user.entity'; +import { + Column, + Entity, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; + +@Entity('user_ban') +export class UserBanEntity extends CommonEntity { + @PrimaryGeneratedColumn({ type: 'bigint' }) + id: number; + + @Column({ nullable: false }) + userId: number; + + @JoinColumn({ name: 'userId' }) + @ManyToOne(() => UserEntity, (userEntity) => userEntity.userBans, { + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + }) + user: UserEntity; + + @Column({ nullable: false }) + bannedAt: Date; + + @Column({ nullable: false }) + expiredAt: Date; + + @Column({ nullable: false }) + reason: string; +} diff --git a/src/entities/user.entity.ts b/src/entities/user.entity.ts index f2c19355..b42717f3 100644 --- a/src/entities/user.entity.ts +++ b/src/entities/user.entity.ts @@ -25,6 +25,7 @@ import { AttendanceCheckEntity } from './attendance-check.entity'; import { Role } from 'src/enums/role.enum'; import { CharacterEntity } from './character.entity'; import { UserLanguageEntity } from './user-language.entity'; +import { UserBanEntity } from 'src/entities/user-ban.entity'; @Entity('user') export class UserEntity extends CommonEntity { @@ -172,4 +173,9 @@ export class UserEntity extends CommonEntity { { cascade: true }, ) userLanguages: UserLanguageEntity[]; + + @OneToMany(() => UserBanEntity, (userBanEntity) => userBanEntity.user, { + cascade: true, + }) + userBans: UserBanEntity[]; } diff --git a/src/user/user-ban.service.ts b/src/user/user-ban.service.ts new file mode 100644 index 00000000..a6afa5d7 --- /dev/null +++ b/src/user/user-ban.service.ts @@ -0,0 +1,53 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { UserBanEntity } from 'src/entities/user-ban.entity'; +import { Notice } from 'src/notice/enum/notice.enum'; +import { NoticeService } from 'src/notice/notice.service'; +import { MoreThanOrEqual, Repository } from 'typeorm'; + +@Injectable() +export class UserBanService { + constructor( + @InjectRepository(UserBanEntity) + private readonly userBanRepository: Repository, + private readonly noticeService: NoticeService, + ) {} + + async banUser( + userId: number, + reason: string, + expireDays: number, + ): Promise { + const bannedAt = new Date(); + const expiredAt = new Date( + bannedAt.getTime() + expireDays * 24 * 60 * 60 * 1000, + ); + + await this.userBanRepository.save({ + userId, + bannedAt, + expiredAt, + reason, + }); + + // 알림 발송 + await this.noticeService.emitNotice( + userId, + 'You have been banned!', + Notice.ban, + null, + null, + ); + } + + async checkUserBan(userId: number): Promise { + const userBan = await this.userBanRepository.findOne({ + where: { + userId, + expiredAt: MoreThanOrEqual(new Date()), + }, + }); + + return !!userBan; + } +} diff --git a/src/user/user.module.ts b/src/user/user.module.ts index 45ef2a4a..562c57a5 100644 --- a/src/user/user.module.ts +++ b/src/user/user.module.ts @@ -8,6 +8,9 @@ import { PointHistoryEntity } from 'src/entities/point-history.entity'; import { CharacterEntity } from 'src/entities/character.entity'; import { PointService } from './point.service'; import { UserLanguageEntity } from 'src/entities/user-language.entity'; +import { UserBanEntity } from 'src/entities/user-ban.entity'; +import { NoticeModule } from 'src/notice/notice.module'; +import { UserBanService } from 'src/user/user-ban.service'; @Module({ imports: [ @@ -16,10 +19,12 @@ import { UserLanguageEntity } from 'src/entities/user-language.entity'; PointHistoryEntity, CharacterEntity, UserLanguageEntity, + UserBanEntity, ]), + NoticeModule, ], controllers: [UserController], - providers: [UserService, PointService, UserRepository], - exports: [UserService, PointService, UserRepository], + providers: [UserService, PointService, UserRepository, UserBanService], + exports: [UserService, PointService, UserRepository, UserBanService], }) export class UserModule {} From b91eb85985929da2fab0c00cdc2f71602bd2228d Mon Sep 17 00:00:00 2001 From: Kim SeongHyeon Date: Sun, 9 Feb 2025 15:00:08 +0900 Subject: [PATCH 31/48] =?UTF-8?q?feat:=20=EC=A0=95=EC=A7=80=EB=90=9C=20?= =?UTF-8?q?=EC=9C=A0=EC=A0=80=20=EC=BB=A4=EB=AE=A4=EB=8B=88=ED=8B=B0=20?= =?UTF-8?q?=ED=99=9C=EB=8F=99=20=EC=A0=9C=ED=95=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/community/comment/comment.module.ts | 2 ++ src/community/comment/comment.service.ts | 10 ++++++++++ src/community/post/post.service.ts | 10 ++++++++++ src/utils/exception.util.ts | 6 ++++++ 4 files changed, 28 insertions(+) diff --git a/src/community/comment/comment.module.ts b/src/community/comment/comment.module.ts index 25adcf55..56bc95e1 100644 --- a/src/community/comment/comment.module.ts +++ b/src/community/comment/comment.module.ts @@ -9,6 +9,7 @@ import { CommentLikeEntity } from 'src/entities/comment-like.entity'; import { CommentAnonymousNumberEntity } from 'src/entities/comment-anonymous-number.entity'; import { NoticeModule } from 'src/notice/notice.module'; import { ReportModule } from '../report/report.module'; +import { UserModule } from 'src/user/user.module'; @Module({ imports: [ @@ -20,6 +21,7 @@ import { ReportModule } from '../report/report.module'; PostModule, NoticeModule, ReportModule, + UserModule, ], controllers: [CommentController], providers: [CommentService, CommentRepository], diff --git a/src/community/comment/comment.service.ts b/src/community/comment/comment.service.ts index cf248be0..7447feec 100644 --- a/src/community/comment/comment.service.ts +++ b/src/community/comment/comment.service.ts @@ -19,6 +19,7 @@ import { CursorPageOptionsDto } from 'src/common/dto/CursorPageOptions.dto'; import { CursorPageMetaResponseDto } from 'src/common/dto/CursorPageResponse.dto'; import { GetMyCommentListResponseDto } from './dto/get-myComment-list.dto'; import { throwKukeyException } from 'src/utils/exception.util'; +import { UserBanService } from 'src/user/user-ban.service'; @Injectable() export class CommentService { @@ -28,6 +29,7 @@ export class CommentService { @InjectRepository(CommentAnonymousNumberEntity) private readonly commentAnonymousNumberRepository: Repository, private readonly noticeService: NoticeService, + private readonly userBanService: UserBanService, ) {} async getMyCommentList( @@ -62,6 +64,10 @@ export class CommentService { requestDto: CreateCommentRequestDto, parentCommentId?: number, ) { + if (await this.userBanService.checkUserBan(user.id)) { + throwKukeyException('USER_BANNED'); + } + const post = await this.postService.isExistingPostId(postId); if (!post) { throwKukeyException('POST_NOT_FOUND'); @@ -171,6 +177,10 @@ export class CommentService { commentId: number, requestDto: UpdateCommentRequestDto, ): Promise { + if (await this.userBanService.checkUserBan(user.id)) { + throwKukeyException('USER_BANNED'); + } + const comment = await this.commentRepository.getCommentByCommentId(commentId); if (!comment) { diff --git a/src/community/post/post.service.ts b/src/community/post/post.service.ts index e2e7fc00..d8b416bc 100644 --- a/src/community/post/post.service.ts +++ b/src/community/post/post.service.ts @@ -36,6 +36,7 @@ import { PostPreview, PostPreviewWithBoardName } from './dto/post-preview.dto'; import { InjectRepository } from '@nestjs/typeorm'; import { PointService } from 'src/user/point.service'; import { throwKukeyException } from 'src/utils/exception.util'; +import { UserBanService } from 'src/user/user-ban.service'; @Injectable() export class PostService { @@ -48,6 +49,7 @@ export class PostService { private readonly fileService: FileService, private readonly pointService: PointService, private readonly noticeService: NoticeService, + private readonly userBanService: UserBanService, @Inject(CACHE_MANAGER) private readonly cacheManager: Cache, ) {} @@ -131,6 +133,10 @@ export class PostService { images: Array, requestDto: CreatePostRequestDto, ): Promise { + if (await this.userBanService.checkUserBan(user.id)) { + throwKukeyException('USER_BANNED'); + } + for (const image of images) { if (!this.fileService.imagefilter(image)) { throwKukeyException('NOT_IMAGE_FILE'); @@ -195,6 +201,10 @@ export class PostService { images: Array, requestDto: UpdatePostRequestDto, ): Promise { + if (await this.userBanService.checkUserBan(user.id)) { + throwKukeyException('USER_BANNED'); + } + const post = await this.postRepository.getPostByPostId(postId); if (!post) { throwKukeyException('POST_NOT_FOUND'); diff --git a/src/utils/exception.util.ts b/src/utils/exception.util.ts index b85364e2..fc1a5bed 100644 --- a/src/utils/exception.util.ts +++ b/src/utils/exception.util.ts @@ -207,6 +207,12 @@ export const kukeyExceptions = createKukeyExceptions({ errorCode: 2016, statusCode: 500, }, + USER_BANNED: { + name: 'USER_BANNED', + message: 'User is banned.', + errorCode: 2017, + statusCode: 403, + }, // - 21xx : Point POINT_NOT_ENOUGH: { name: 'POINT_NOT_ENOUGH', From 0ee55e6adb9b692ccdaba9b1b088fe9631b34f13 Mon Sep 17 00:00:00 2001 From: Kim SeongHyeon Date: Sun, 9 Feb 2025 15:03:05 +0900 Subject: [PATCH 32/48] =?UTF-8?q?feat:=20swagger=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/decorators/docs/comment.decorator.ts | 2 ++ src/decorators/docs/post.decorator.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/decorators/docs/comment.decorator.ts b/src/decorators/docs/comment.decorator.ts index c009134f..e31dc469 100644 --- a/src/decorators/docs/comment.decorator.ts +++ b/src/decorators/docs/comment.decorator.ts @@ -60,6 +60,7 @@ const CommentDocsMap: Record = { 'INVALID_PARENT_COMMENT_REQUEST', 'REPLY_TO_DIFFERENT_POST', 'POST_UPDATE_FAILED', + 'USER_BANNED', ]), ], updateComment: [ @@ -84,6 +85,7 @@ const CommentDocsMap: Record = { 'COMMENT_OWNERSHIP_REQUIRED', 'COMMENT_IN_QUESTION_BOARD', 'COMMENT_UPDATE_FAILED', + 'USER_BANNED', ]), ], deleteComment: [ diff --git a/src/decorators/docs/post.decorator.ts b/src/decorators/docs/post.decorator.ts index 529202c1..57e41e9f 100644 --- a/src/decorators/docs/post.decorator.ts +++ b/src/decorators/docs/post.decorator.ts @@ -134,6 +134,7 @@ const PostDocsMap: Record = { 'TOO_MANY_IMAGES', 'BOARD_NOT_FOUND', 'FILE_UPLOAD_FAILED', + 'USER_BANNED', ]), ], updatePost: [ @@ -162,6 +163,7 @@ const PostDocsMap: Record = { 'TOO_MANY_IMAGES', 'FILE_UPLOAD_FAILED', 'FILE_DELETE_FAILED', + 'USER_BANNED', ]), ], deletePost: [ From 53184edfef0c23173ddf02978b90c682b02bfbe9 Mon Sep 17 00:00:00 2001 From: Kim SeongHyeon Date: Sun, 9 Feb 2025 16:01:18 +0900 Subject: [PATCH 33/48] =?UTF-8?q?feat:=20=EC=8B=A0=EA=B3=A0=EC=88=98?= =?UTF-8?q?=EB=9D=BD=EC=8B=9C=20=EA=B8=80=20=EC=82=AD=EC=A0=9C=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 순환종속성 해결 위해 엔드포인트 위치 수정 --- src/community/comment/comment.controller.ts | 28 +--------- src/community/comment/comment.module.ts | 3 +- src/community/post/post.controller.ts | 22 +------- src/community/post/post.module.ts | 2 - src/community/report/dto/create-report.dto.ts | 14 ++++- src/community/report/report.controller.ts | 43 +++++++++++++-- src/community/report/report.module.ts | 10 +++- src/community/report/report.service.ts | 52 +++++++++++++------ src/decorators/docs/comment.decorator.ts | 23 -------- src/decorators/docs/post.decorator.ts | 23 -------- src/decorators/docs/report.decorator.ts | 24 +++++++++ src/user/user-ban.service.ts | 7 +-- 12 files changed, 127 insertions(+), 124 deletions(-) diff --git a/src/community/comment/comment.controller.ts b/src/community/comment/comment.controller.ts index b2eb9917..239f8dc8 100644 --- a/src/community/comment/comment.controller.ts +++ b/src/community/comment/comment.controller.ts @@ -20,11 +20,6 @@ import { GetCommentResponseDto } from './dto/get-comment.dto'; import { UpdateCommentRequestDto } from './dto/update-comment.dto'; import { DeleteCommentResponseDto } from './dto/delete-comment.dto'; import { LikeCommentResponseDto } from './dto/like-comment.dto'; -import { - CreateReportRequestDto, - CreateReportResponseDto, -} from '../report/dto/create-report.dto'; -import { ReportService } from '../report/report.service'; import { CursorPageOptionsDto } from 'src/common/dto/CursorPageOptions.dto'; import { GetMyCommentListResponseDto } from './dto/get-myComment-list.dto'; import { TransactionInterceptor } from 'src/common/interceptors/transaction.interceptor'; @@ -39,10 +34,7 @@ import { CommentDocs } from 'src/decorators/docs/comment.decorator'; @ApiBearerAuth('accessToken') @CommentDocs export class CommentController { - constructor( - private readonly commentService: CommentService, - private readonly reportService: ReportService, - ) {} + constructor(private readonly commentService: CommentService) {} @Get('/my') async getMyCommentList( @@ -112,22 +104,4 @@ export class CommentController { commentId, ); } - - @Post('/:commentId/report') - async reportComment( - @User() user: AuthorizedUserDto, - @Param('commentId') commentId: number, - @Body() body: CreateReportRequestDto, - ): Promise { - const comment = await this.commentService.getComment(commentId); - if (!comment) { - throwKukeyException('COMMENT_NOT_FOUND'); - } - return await this.reportService.createReport( - user.id, - body.reason, - comment.postId, - commentId, - ); - } } diff --git a/src/community/comment/comment.module.ts b/src/community/comment/comment.module.ts index 56bc95e1..b157f256 100644 --- a/src/community/comment/comment.module.ts +++ b/src/community/comment/comment.module.ts @@ -8,7 +8,6 @@ import { PostModule } from '../post/post.module'; import { CommentLikeEntity } from 'src/entities/comment-like.entity'; import { CommentAnonymousNumberEntity } from 'src/entities/comment-anonymous-number.entity'; import { NoticeModule } from 'src/notice/notice.module'; -import { ReportModule } from '../report/report.module'; import { UserModule } from 'src/user/user.module'; @Module({ @@ -20,10 +19,10 @@ import { UserModule } from 'src/user/user.module'; ]), PostModule, NoticeModule, - ReportModule, UserModule, ], controllers: [CommentController], providers: [CommentService, CommentRepository], + exports: [CommentService], }) export class CommentModule {} diff --git a/src/community/post/post.controller.ts b/src/community/post/post.controller.ts index 2d46accd..e86ce49c 100644 --- a/src/community/post/post.controller.ts +++ b/src/community/post/post.controller.ts @@ -35,11 +35,6 @@ import { ReactPostRequestDto, ReactPostResponseDto, } from './dto/react-post.dto'; -import { - CreateReportRequestDto, - CreateReportResponseDto, -} from '../report/dto/create-report.dto'; -import { ReportService } from '../report/report.service'; import { TransactionInterceptor } from 'src/common/interceptors/transaction.interceptor'; import { TransactionManager } from 'src/decorators/manager.decorator'; import { EntityManager } from 'typeorm'; @@ -52,10 +47,7 @@ import { PostDocs } from 'src/decorators/docs/post.decorator'; @PostDocs @ApiBearerAuth('accessToken') export class PostController { - constructor( - private readonly postService: PostService, - private readonly reportService: ReportService, - ) {} + constructor(private readonly postService: PostService) {} @Get() async getPostList( @@ -190,16 +182,4 @@ export class PostController { body, ); } - - @Post('/:postId/report') - async reportPost( - @User() user: AuthorizedUserDto, - @Param('postId') postId: number, - @Body() body: CreateReportRequestDto, - ): Promise { - if (!(await this.postService.isExistingPostId(postId))) { - throwKukeyException('POST_NOT_FOUND'); - } - return await this.reportService.createReport(user.id, body.reason, postId); - } } diff --git a/src/community/post/post.module.ts b/src/community/post/post.module.ts index 5941280b..57257245 100644 --- a/src/community/post/post.module.ts +++ b/src/community/post/post.module.ts @@ -11,7 +11,6 @@ import { CommonModule } from 'src/common/common.module'; import { PostScrapRepository } from './post-scrap.repository'; import { PostScrapEntity } from 'src/entities/post-scrap.entity'; import { PostReactionEntity } from 'src/entities/post-reaction.entity'; -import { ReportModule } from '../report/report.module'; import { UserModule } from 'src/user/user.module'; import { NoticeModule } from 'src/notice/notice.module'; @@ -25,7 +24,6 @@ import { NoticeModule } from 'src/notice/notice.module'; ]), BoardModule, CommonModule, - ReportModule, UserModule, NoticeModule, ], diff --git a/src/community/report/dto/create-report.dto.ts b/src/community/report/dto/create-report.dto.ts index c9f53240..b61cb991 100644 --- a/src/community/report/dto/create-report.dto.ts +++ b/src/community/report/dto/create-report.dto.ts @@ -1,10 +1,20 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; export class CreateReportRequestDto { + @ApiProperty({ description: '게시글 ID' }) @IsNotEmpty() - @IsString() + postId: number; + + @ApiProperty({ + description: '댓글 ID (댓글 신고일 경우 존재, 게시글 신고일 경우 null', + }) + @IsOptional() + commentId?: number; + @ApiProperty({ description: '신고 사유' }) + @IsNotEmpty() + @IsString() reason: string; } diff --git a/src/community/report/report.controller.ts b/src/community/report/report.controller.ts index 8636390d..d096e62d 100644 --- a/src/community/report/report.controller.ts +++ b/src/community/report/report.controller.ts @@ -1,4 +1,12 @@ -import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common'; +import { + Body, + Controller, + Get, + Param, + Post, + UseGuards, + UseInterceptors, +} from '@nestjs/common'; import { ReportService } from './report.service'; import { GetReportListResponseDto } from './dto/get-report-list.dto'; import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; @@ -9,36 +17,63 @@ import { Roles } from 'src/decorators/roles.decorator'; import { Role } from 'src/enums/role.enum'; import { ReportDocs } from 'src/decorators/docs/report.decorator'; import { AcceptReportRequestDto } from 'src/community/report/dto/accept-report.dto'; +import { TransactionInterceptor } from 'src/common/interceptors/transaction.interceptor'; +import { TransactionManager } from 'src/decorators/manager.decorator'; +import { EntityManager } from 'typeorm'; +import { User } from 'src/decorators/user.decorator'; +import { AuthorizedUserDto } from 'src/auth/dto/authorized-user-dto'; +import { + CreateReportRequestDto, + CreateReportResponseDto, +} from 'src/community/report/dto/create-report.dto'; @Controller('report') @UseGuards(JwtAuthGuard, RolesGuard) @ApiTags('report') @ReportDocs @ApiBearerAuth('accessToken') -@Roles(Role.admin) export class ReportController { constructor(private readonly reportService: ReportService) {} + @Roles(Role.admin) @Get() async getReportList(): Promise { return await this.reportService.getReportList(); } - @Post('/:reportId') + @Roles(Role.admin) + @Get('/:reportId') async getReport( @Param('reportId') reportId: number, ): Promise { return await this.reportService.getReport(reportId); } + @Post() + async createReport( + @User() user: AuthorizedUserDto, + @Body() body: CreateReportRequestDto, + ): Promise { + return await this.reportService.createReport( + user.id, + body.reason, + body.postId, + body.commentId, + ); + } + + @Roles(Role.admin) @Post('/:reportId/accept') + @UseInterceptors(TransactionInterceptor) async acceptReport( + @TransactionManager() transactionManager: EntityManager, @Param('reportId') reportId: number, @Body() body: AcceptReportRequestDto, ): Promise { - await this.reportService.acceptReport(reportId, body); + await this.reportService.acceptReport(transactionManager, reportId, body); } + @Roles(Role.admin) @Post('/:reportId/reject') async rejectReport(@Param('reportId') reportId: number): Promise { await this.reportService.rejectReport(reportId); diff --git a/src/community/report/report.module.ts b/src/community/report/report.module.ts index 4f4c82d8..fd19054e 100644 --- a/src/community/report/report.module.ts +++ b/src/community/report/report.module.ts @@ -6,9 +6,17 @@ import { ReportRepository } from './report.repository'; import { ReportController } from './report.controller'; import { CommonModule } from 'src/common/common.module'; import { UserModule } from 'src/user/user.module'; +import { PostModule } from 'src/community/post/post.module'; +import { CommentModule } from 'src/community/comment/comment.module'; @Module({ - imports: [TypeOrmModule.forFeature([ReportEntity]), CommonModule, UserModule], + imports: [ + TypeOrmModule.forFeature([ReportEntity]), + CommonModule, + UserModule, + PostModule, + CommentModule, + ], controllers: [ReportController], providers: [ReportService, ReportRepository], exports: [ReportService], diff --git a/src/community/report/report.service.ts b/src/community/report/report.service.ts index 1ee776bb..4a488536 100644 --- a/src/community/report/report.service.ts +++ b/src/community/report/report.service.ts @@ -7,6 +7,9 @@ import { FileService } from 'src/common/file.service'; import { throwKukeyException } from 'src/utils/exception.util'; import { UserBanService } from 'src/user/user-ban.service'; import { AcceptReportRequestDto } from 'src/community/report/dto/accept-report.dto'; +import { PostService } from 'src/community/post/post.service'; +import { CommentService } from 'src/community/comment/comment.service'; +import { EntityManager } from 'typeorm'; @Injectable() export class ReportService { @@ -14,29 +17,29 @@ export class ReportService { private readonly reportRepository: ReportRepository, private readonly fileService: FileService, private readonly userBanService: UserBanService, + private readonly postService: PostService, + private readonly commentService: CommentService, ) {} async createReport( - reporterId: number, + userId: number, reason: string, postId: number, commentId?: number, ): Promise { if ( - await this.reportRepository.checkAlreadyReport( - reporterId, - postId, - commentId, - ) + await this.reportRepository.checkAlreadyReport(userId, postId, commentId) ) { throwKukeyException('ALREADY_REPORTED'); } - await this.reportRepository.createReport( - reporterId, - reason, - postId, - commentId, - ); + if (!(await this.postService.isExistingPostId(postId))) { + throwKukeyException('POST_NOT_FOUND'); + } + if (commentId && !(await this.commentService.getComment(commentId))) { + throwKukeyException('COMMENT_NOT_FOUND'); + } + + await this.reportRepository.createReport(userId, reason, postId, commentId); return new CreateReportResponseDto(true); } @@ -61,15 +64,32 @@ export class ReportService { } async acceptReport( + transactionManager: EntityManager, reportId: number, dto: AcceptReportRequestDto, ): Promise { const report = await this.reportRepository.getReport(reportId); - const userId = report.commentId - ? report.comment.userId - : report.post.userId; + const isComment = report.commentId ? true : false; + const userId = isComment ? report.comment.userId : report.post.userId; if (userId) { - await this.userBanService.banUser(userId, report.reason, dto.banDays); + await this.userBanService.banUser( + transactionManager, + userId, + report.reason, + dto.banDays, + ); + } + if (isComment) { + await this.commentService.deleteComment( + transactionManager, + { id: userId, username: '' }, + report.commentId, + ); + } else { + await this.postService.deletePost( + { id: userId, username: '' }, + report.postId, + ); } await this.reportRepository.acceptReport(reportId); } diff --git a/src/decorators/docs/comment.decorator.ts b/src/decorators/docs/comment.decorator.ts index e31dc469..cd76fa7d 100644 --- a/src/decorators/docs/comment.decorator.ts +++ b/src/decorators/docs/comment.decorator.ts @@ -13,10 +13,6 @@ import { GetCommentResponseDto } from 'src/community/comment/dto/get-comment.dto import { GetMyCommentListResponseDto } from 'src/community/comment/dto/get-myComment-list.dto'; import { LikeCommentResponseDto } from 'src/community/comment/dto/like-comment.dto'; import { UpdateCommentRequestDto } from 'src/community/comment/dto/update-comment.dto'; -import { - CreateReportRequestDto, - CreateReportResponseDto, -} from 'src/community/report/dto/create-report.dto'; import { ApiKukeyExceptionResponse } from '../api-kukey-exception-response'; type CommentEndPoints = MethodNames; @@ -130,25 +126,6 @@ const CommentDocsMap: Record = { 'COMMENT_LIKE_CANCEL_FAILED', ]), ], - reportComment: [ - ApiOperation({ - summary: '댓글 신고', - description: '댓글을 신고합니다', - }), - ApiParam({ - name: 'commentId', - description: '댓글의 고유 ID', - }), - ApiBody({ - type: CreateReportRequestDto, - }), - ApiResponse({ - status: 201, - description: '댓글 신고 성공', - type: CreateReportResponseDto, - }), - ApiKukeyExceptionResponse(['COMMENT_NOT_FOUND', 'ALREADY_REPORTED']), - ], }; export function CommentDocs(target: typeof CommentController) { diff --git a/src/decorators/docs/post.decorator.ts b/src/decorators/docs/post.decorator.ts index 57e41e9f..f6522460 100644 --- a/src/decorators/docs/post.decorator.ts +++ b/src/decorators/docs/post.decorator.ts @@ -19,10 +19,6 @@ import { import { ScrapPostResponseDto } from 'src/community/post/dto/scrap-post.dto'; import { UpdatePostRequestDto } from 'src/community/post/dto/update-post.dto'; import { PostController } from 'src/community/post/post.controller'; -import { - CreateReportRequestDto, - CreateReportResponseDto, -} from 'src/community/report/dto/create-report.dto'; import { ApiKukeyExceptionResponse } from '../api-kukey-exception-response'; type PostEndPoints = MethodNames; @@ -237,25 +233,6 @@ const PostDocsMap: Record = { 'USER_NOT_FOUND', ]), ], - reportPost: [ - ApiOperation({ - summary: '게시글 신고', - description: '게시글을 신고합니다', - }), - ApiParam({ - name: 'postId', - description: '게시글의 고유 ID', - }), - ApiBody({ - type: CreateReportRequestDto, - }), - ApiResponse({ - status: 201, - description: '게시글 신고 성공', - type: CreateReportResponseDto, - }), - ApiKukeyExceptionResponse(['POST_NOT_FOUND', 'ALREADY_REPORTED']), - ], }; export function PostDocs(target: typeof PostController) { diff --git a/src/decorators/docs/report.decorator.ts b/src/decorators/docs/report.decorator.ts index 8e7d524e..b22a7292 100644 --- a/src/decorators/docs/report.decorator.ts +++ b/src/decorators/docs/report.decorator.ts @@ -1,9 +1,14 @@ import { ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; import { MethodNames } from 'src/common/types/method'; import { AcceptReportRequestDto } from 'src/community/report/dto/accept-report.dto'; +import { + CreateReportRequestDto, + CreateReportResponseDto, +} from 'src/community/report/dto/create-report.dto'; import { GetReportListResponseDto } from 'src/community/report/dto/get-report-list.dto'; import { GetReportResponseDto } from 'src/community/report/dto/get-report.dto'; import { ReportController } from 'src/community/report/report.controller'; +import { ApiKukeyExceptionResponse } from 'src/decorators/api-kukey-exception-response'; type ReportEndPoints = MethodNames; @@ -65,6 +70,25 @@ const ReportDocsMap: Record = { description: '신고 거부 성공', }), ], + createReport: [ + ApiOperation({ + summary: '신고 생성', + description: '신고를 생성합니다.', + }), + ApiBody({ + type: CreateReportRequestDto, + }), + ApiResponse({ + status: 201, + description: '신고 생성 성공', + type: CreateReportResponseDto, + }), + ApiKukeyExceptionResponse([ + 'ALREADY_REPORTED', + 'POST_NOT_FOUND', + 'COMMENT_NOT_FOUND', + ]), + ], }; export function ReportDocs(target: typeof ReportController) { diff --git a/src/user/user-ban.service.ts b/src/user/user-ban.service.ts index a6afa5d7..f42f0bff 100644 --- a/src/user/user-ban.service.ts +++ b/src/user/user-ban.service.ts @@ -3,7 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { UserBanEntity } from 'src/entities/user-ban.entity'; import { Notice } from 'src/notice/enum/notice.enum'; import { NoticeService } from 'src/notice/notice.service'; -import { MoreThanOrEqual, Repository } from 'typeorm'; +import { EntityManager, MoreThanOrEqual, Repository } from 'typeorm'; @Injectable() export class UserBanService { @@ -14,6 +14,7 @@ export class UserBanService { ) {} async banUser( + transactionManager: EntityManager, userId: number, reason: string, expireDays: number, @@ -23,7 +24,7 @@ export class UserBanService { bannedAt.getTime() + expireDays * 24 * 60 * 60 * 1000, ); - await this.userBanRepository.save({ + await transactionManager.save(UserBanEntity, { userId, bannedAt, expiredAt, @@ -36,7 +37,7 @@ export class UserBanService { 'You have been banned!', Notice.ban, null, - null, + transactionManager, ); } From 4ea96377dbb591a75be3d7aeeec3a54c7f0e3a63 Mon Sep 17 00:00:00 2001 From: JeongYeonSeung Date: Sun, 9 Feb 2025 16:14:09 +0900 Subject: [PATCH 34/48] =?UTF-8?q?mod::=20=EB=B0=9B=EC=9D=80=20=EC=B9=9C?= =?UTF-8?q?=EA=B5=AC=20=EC=9A=94=EC=B2=AD=20=EA=B0=9C=EC=88=98=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EB=A1=9C=EC=A7=81=EC=9D=98=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?dto=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 가장 최근 요청 보낸 최대 2명의 유저 캐릭터 정보 가져오도록 변경 --- src/friendship/dto/friend-character.dto.ts | 16 +++++++++++++++ src/friendship/dto/get-friend-response.dto.ts | 20 +++---------------- ...t-received-friendship-request-count.dto.ts | 16 ++++++++++++++- src/friendship/friendship.service.ts | 18 ++++++++++++++++- 4 files changed, 51 insertions(+), 19 deletions(-) create mode 100644 src/friendship/dto/friend-character.dto.ts diff --git a/src/friendship/dto/friend-character.dto.ts b/src/friendship/dto/friend-character.dto.ts new file mode 100644 index 00000000..fd910eec --- /dev/null +++ b/src/friendship/dto/friend-character.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { CharacterEntity } from 'src/entities/character.entity'; +import { CharacterType } from 'src/enums/character-type.enum'; + +export class FriendCharacterDto { + @ApiProperty({ description: '캐릭터 종류', enum: CharacterType }) + type: CharacterType; + + @ApiProperty({ description: '캐릭터 레벨' }) + level: number; + + constructor(character: CharacterEntity) { + this.type = character.type; + this.level = character.selectedLevel ?? character.level; + } +} diff --git a/src/friendship/dto/get-friend-response.dto.ts b/src/friendship/dto/get-friend-response.dto.ts index a84add22..a66c5a77 100644 --- a/src/friendship/dto/get-friend-response.dto.ts +++ b/src/friendship/dto/get-friend-response.dto.ts @@ -1,20 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; -import { CharacterEntity } from 'src/entities/character.entity'; import { UserEntity } from 'src/entities/user.entity'; -import { CharacterType } from 'src/enums/character-type.enum'; - -export class Character { - @ApiProperty({ description: '캐릭터 종류', enum: CharacterType }) - type: CharacterType; - - @ApiProperty({ description: '캐릭터 레벨' }) - level: number; - - constructor(character: CharacterEntity) { - this.type = character.type; - this.level = character.selectedLevel ?? character.level; - } -} +import { FriendCharacterDto } from './friend-character.dto'; export class GetFriendResponseDto { @ApiProperty({ description: 'freindship table의 PK' }) @@ -39,7 +25,7 @@ export class GetFriendResponseDto { country: string; @ApiProperty({ description: '유저 캐릭터' }) - character: Character; + character: FriendCharacterDto; constructor(friendshipId: number, friend: UserEntity) { this.friendshipId = friendshipId; @@ -49,6 +35,6 @@ export class GetFriendResponseDto { this.homeUniversity = friend.homeUniversity; this.major = friend.major; this.country = friend.country; - this.character = new Character(friend.character); + this.character = new FriendCharacterDto(friend.character); } } diff --git a/src/friendship/dto/get-received-friendship-request-count.dto.ts b/src/friendship/dto/get-received-friendship-request-count.dto.ts index 85eb278a..fc5e6305 100644 --- a/src/friendship/dto/get-received-friendship-request-count.dto.ts +++ b/src/friendship/dto/get-received-friendship-request-count.dto.ts @@ -1,4 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; +import { FriendCharacterDto } from './friend-character.dto'; +import { CharacterEntity } from 'src/entities/character.entity'; export class GetReceivedFriendshipRequestCountDto { @ApiProperty({ description: '전체 받은 친구 요청 개수' }) @@ -7,8 +9,20 @@ export class GetReceivedFriendshipRequestCountDto { @ApiProperty({ description: '확인하지 않은 받은 친구 요청 개수' }) unreadCount: number; - constructor(totalCount: number, unreadCount: number) { + @ApiProperty({ + description: '가장 최근에 요청을 보낸 친구의 캐릭터 (최대 2개)', + }) + friendCharacters: FriendCharacterDto[]; + + constructor( + totalCount: number, + unreadCount: number, + friendChararcters: CharacterEntity[], + ) { this.totalCount = totalCount; this.unreadCount = unreadCount; + this.friendCharacters = friendChararcters.map((character) => { + return new FriendCharacterDto(character); + }); } } diff --git a/src/friendship/friendship.service.ts b/src/friendship/friendship.service.ts index 67275d8d..49f9e1cf 100644 --- a/src/friendship/friendship.service.ts +++ b/src/friendship/friendship.service.ts @@ -189,7 +189,23 @@ export class FriendshipService { const { totalCount, unreadCount } = await this.friendshipRepository.countReceivedFriendships(userId); - return new GetReceivedFriendshipRequestCountDto(totalCount, unreadCount); + const recentRequests = await this.friendshipRepository.find({ + where: { toUserId: userId, areWeFriend: false }, + relations: ['fromUser', 'fromUser.character'], + order: { createdAt: 'DESC' }, + select: ['fromUser'], + take: 2, + }); + + const recentCharacters = recentRequests.map((req) => { + return req.fromUser.character; + }); + + return new GetReceivedFriendshipRequestCountDto( + totalCount, + unreadCount, + recentCharacters, + ); } async getSentWaitingFriendList( From f8d3e3039f4544faff4f811f95e0b9679f48c246 Mon Sep 17 00:00:00 2001 From: JeongYeonSeung Date: Sun, 9 Feb 2025 16:16:33 +0900 Subject: [PATCH 35/48] =?UTF-8?q?fix::=20=EB=B3=B4=EB=82=B8/=EB=B0=9B?= =?UTF-8?q?=EC=9D=80=20=EC=B9=9C=EA=B5=AC=20=EC=9A=94=EC=B2=AD=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=8B=9C=20=EC=B5=9C=EC=8B=A0=20=EC=88=9C=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=A0=95=EB=A0=AC=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 --- src/friendship/friendship.repository.ts | 1 + src/friendship/friendship.service.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/friendship/friendship.repository.ts b/src/friendship/friendship.repository.ts index f9f0ac27..f22dcccb 100644 --- a/src/friendship/friendship.repository.ts +++ b/src/friendship/friendship.repository.ts @@ -96,6 +96,7 @@ export class FriendshipRepository extends Repository { 'fromUser.character', 'toUser.character', ], + order: { createdAt: 'DESC' }, }); } diff --git a/src/friendship/friendship.service.ts b/src/friendship/friendship.service.ts index 49f9e1cf..6d8bc999 100644 --- a/src/friendship/friendship.service.ts +++ b/src/friendship/friendship.service.ts @@ -165,6 +165,7 @@ export class FriendshipService { { where: { toUserId: userId, areWeFriend: false }, relations: ['fromUser', 'fromUser.character'], + order: { createdAt: 'DESC' }, }, ); From 0fcdc6507384efbd349eab7a72b243c46301d9de Mon Sep 17 00:00:00 2001 From: Kim SeongHyeon Date: Sun, 9 Feb 2025 16:38:55 +0900 Subject: [PATCH 36/48] =?UTF-8?q?fix:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=8B=A0=EA=B3=A0=EC=8B=9C=20=EA=B8=80=20=EC=82=AD=EC=A0=9C?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=ED=8A=B8=EB=9E=9C=EC=9E=AD=EC=85=98=20?= =?UTF-8?q?=ED=8F=AC=ED=95=A8=EB=90=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/community/post/post.controller.ts | 4 +++- src/community/post/post.service.ts | 14 ++++++++++++-- src/community/report/report.service.ts | 2 ++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/community/post/post.controller.ts b/src/community/post/post.controller.ts index e86ce49c..35cab2c8 100644 --- a/src/community/post/post.controller.ts +++ b/src/community/post/post.controller.ts @@ -150,11 +150,13 @@ export class PostController { } @Delete('/:postId') + @UseInterceptors(TransactionInterceptor) async deletePost( + @TransactionManager() transactionManager: EntityManager, @User() user: AuthorizedUserDto, @Param('postId') postId: number, ): Promise { - return await this.postService.deletePost(user, postId); + return await this.postService.deletePost(transactionManager, user, postId); } @Post('/:postId/scrap') diff --git a/src/community/post/post.service.ts b/src/community/post/post.service.ts index d8b416bc..2406e153 100644 --- a/src/community/post/post.service.ts +++ b/src/community/post/post.service.ts @@ -281,10 +281,20 @@ export class PostService { } async deletePost( + transactionManager: EntityManager, user: AuthorizedUserDto, postId: number, ): Promise { - const post = await this.postRepository.getPostByPostId(postId); + const post = await transactionManager.findOne(PostEntity, { + where: { id: postId }, + relations: [ + 'postImages', + 'comments.commentLikes', + 'postScraps', + 'postReactions', + 'commentAnonymousNumbers', + ], + }); if (!post) { throwKukeyException('POST_NOT_FOUND'); } @@ -300,7 +310,7 @@ export class PostService { await this.fileService.deleteFile(image.imgDir); } - const isDeleted = await this.postRepository.deletePost(postId); + const isDeleted = await transactionManager.softRemove(post); if (!isDeleted) { throwKukeyException('POST_DELETE_FAILED'); } diff --git a/src/community/report/report.service.ts b/src/community/report/report.service.ts index 4a488536..0b5a3b65 100644 --- a/src/community/report/report.service.ts +++ b/src/community/report/report.service.ts @@ -45,6 +45,7 @@ export class ReportService { async getReportList(): Promise { const reports = await this.reportRepository.getReportList(); + console.log(reports); return reports.map((report) => new GetReportListResponseDto(report)); } @@ -87,6 +88,7 @@ export class ReportService { ); } else { await this.postService.deletePost( + transactionManager, { id: userId, username: '' }, report.postId, ); From 1081667caf3c22097f69c111ae9f46be7467be31 Mon Sep 17 00:00:00 2001 From: Kim SeongHyeon Date: Sun, 9 Feb 2025 17:09:59 +0900 Subject: [PATCH 37/48] =?UTF-8?q?fix:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=EB=90=98=EC=96=B4=EB=8F=84=20=EB=8C=93?= =?UTF-8?q?=EA=B8=80=20=EB=82=A8=EA=B2=A8=EB=91=90=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 - 댓글에 대한 신고가 남아있을 경우 해당 신고 처리 필요 --- src/community/post/post.service.ts | 3 ++- src/entities/comment.entity.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/community/post/post.service.ts b/src/community/post/post.service.ts index 2406e153..e7b92756 100644 --- a/src/community/post/post.service.ts +++ b/src/community/post/post.service.ts @@ -289,7 +289,8 @@ export class PostService { where: { id: postId }, relations: [ 'postImages', - 'comments.commentLikes', + // 댓글에 대해 신고가 들어왔을때 대비해서 삭제x + // 'comments.commentLikes', 'postScraps', 'postReactions', 'commentAnonymousNumbers', diff --git a/src/entities/comment.entity.ts b/src/entities/comment.entity.ts index 6aa8b48b..87077267 100644 --- a/src/entities/comment.entity.ts +++ b/src/entities/comment.entity.ts @@ -43,7 +43,7 @@ export class CommentEntity extends CommonEntity { @JoinColumn({ name: 'postId' }) @ManyToOne(() => PostEntity, (postEntity) => postEntity.comments, { - onDelete: 'CASCADE', + onDelete: 'NO ACTION', }) post: PostEntity; From 8e016a99fa6563ab485f261b5f3deea449d1b74e Mon Sep 17 00:00:00 2001 From: Kim SeongHyeon Date: Sun, 9 Feb 2025 17:37:16 +0900 Subject: [PATCH 38/48] =?UTF-8?q?fix:=20=EA=B2=8C=EC=8B=9C=EA=B8=80?= =?UTF-8?q?=EC=9D=B4=20=EC=82=AD=EC=A0=9C=EB=90=9C=20=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/community/comment/comment.service.ts | 26 +++++++++++++----------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/community/comment/comment.service.ts b/src/community/comment/comment.service.ts index 7447feec..378f4e01 100644 --- a/src/community/comment/comment.service.ts +++ b/src/community/comment/comment.service.ts @@ -233,8 +233,20 @@ export class CommentService { } const post = await this.postService.isExistingPostId(comment.postId); - if (Number(post.boardId) === 2) { - throwKukeyException('COMMENT_IN_QUESTION_BOARD'); + if (post) { + if (Number(post.boardId) === 2) { + throwKukeyException('COMMENT_IN_QUESTION_BOARD'); + } + + const updateResult = await transactionManager.decrement( + PostEntity, + { id: comment.postId }, + 'commentCount', + 1, + ); + if (!updateResult.affected) { + throwKukeyException('POST_UPDATE_FAILED'); + } } const deleteResult = await transactionManager.softRemove( @@ -245,16 +257,6 @@ export class CommentService { throwKukeyException('COMMENT_DELETE_FAILED'); } - const updateResult = await transactionManager.decrement( - PostEntity, - { id: comment.postId }, - 'commentCount', - 1, - ); - if (!updateResult.affected) { - throwKukeyException('POST_UPDATE_FAILED'); - } - return new DeleteCommentResponseDto(true); } From ecdddba6a8611fc96d5b796c03aad326abd0cd69 Mon Sep 17 00:00:00 2001 From: Kim SeongHyeon Date: Sun, 9 Feb 2025 18:13:58 +0900 Subject: [PATCH 39/48] =?UTF-8?q?fix:=20=EC=8B=A0=EA=B3=A0=EB=90=9C=20?= =?UTF-8?q?=EA=B8=80=20=EC=82=AD=EC=A0=9C=20=EA=B6=8C=ED=95=9C=ED=99=95?= =?UTF-8?q?=EC=9D=B8=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 --- src/community/comment/comment.service.ts | 28 +++++++++++++++++------- src/community/post/post.service.ts | 18 ++++++++++----- src/community/report/report.service.ts | 4 ++-- 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/src/community/comment/comment.service.ts b/src/community/comment/comment.service.ts index 378f4e01..5eb97a2d 100644 --- a/src/community/comment/comment.service.ts +++ b/src/community/comment/comment.service.ts @@ -228,16 +228,11 @@ export class CommentService { if (!comment) { throwKukeyException('COMMENT_NOT_FOUND'); } - if (comment.userId !== user.id) { - throwKukeyException('COMMENT_OWNERSHIP_REQUIRED'); - } - const post = await this.postService.isExistingPostId(comment.postId); - if (post) { - if (Number(post.boardId) === 2) { - throwKukeyException('COMMENT_IN_QUESTION_BOARD'); - } + this.checkDeleteAuthority(comment, post, user); + + if (post) { const updateResult = await transactionManager.decrement( PostEntity, { id: comment.postId }, @@ -260,6 +255,23 @@ export class CommentService { return new DeleteCommentResponseDto(true); } + private checkDeleteAuthority( + comment: CommentEntity, + post: PostEntity, + user: AuthorizedUserDto, + ) { + if (user.id !== -1) { + if (comment.userId !== user.id) { + throwKukeyException('COMMENT_OWNERSHIP_REQUIRED'); + } + if (post) { + if (Number(post.boardId) === 2) { + throwKukeyException('COMMENT_IN_QUESTION_BOARD'); + } + } + } + } + async likeComment( tranasactionManager: EntityManager, user: AuthorizedUserDto, diff --git a/src/community/post/post.service.ts b/src/community/post/post.service.ts index e7b92756..b2369bc4 100644 --- a/src/community/post/post.service.ts +++ b/src/community/post/post.service.ts @@ -299,13 +299,8 @@ export class PostService { if (!post) { throwKukeyException('POST_NOT_FOUND'); } - if (post.userId !== user.id) { - throwKukeyException('POST_OWNERSHIP_REQUIRED'); - } - if (post.boardId == 2 && post.commentCount > 0) { - throwKukeyException('POST_IN_QUESTION_BOARD'); - } + this.checkDeleteAuthority(post, user); for (const image of post.postImages) { await this.fileService.deleteFile(image.imgDir); @@ -319,6 +314,17 @@ export class PostService { return new DeletePostResponseDto(true); } + private checkDeleteAuthority(post: PostEntity, user: AuthorizedUserDto) { + if (user.id !== -1) { + if (post.userId !== user.id) { + throwKukeyException('POST_OWNERSHIP_REQUIRED'); + } + if (post.boardId == 2 && post.commentCount > 0) { + throwKukeyException('POST_IN_QUESTION_BOARD'); + } + } + } + async scrapPost( transactionManager: EntityManager, user: AuthorizedUserDto, diff --git a/src/community/report/report.service.ts b/src/community/report/report.service.ts index 0b5a3b65..58b8c89a 100644 --- a/src/community/report/report.service.ts +++ b/src/community/report/report.service.ts @@ -83,13 +83,13 @@ export class ReportService { if (isComment) { await this.commentService.deleteComment( transactionManager, - { id: userId, username: '' }, + { id: -1, username: '' }, report.commentId, ); } else { await this.postService.deletePost( transactionManager, - { id: userId, username: '' }, + { id: -1, username: '' }, report.postId, ); } From bfa545e1537ea883342f1d18fff5c9182efea5b8 Mon Sep 17 00:00:00 2001 From: Kim SeongHyeon Date: Sun, 9 Feb 2025 18:20:51 +0900 Subject: [PATCH 40/48] =?UTF-8?q?fix:=20swagger=20=EB=AA=85=EC=84=B8=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 - console.log 삭제 --- src/community/report/dto/create-report.dto.ts | 8 +++++--- src/community/report/report.service.ts | 1 - 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/community/report/dto/create-report.dto.ts b/src/community/report/dto/create-report.dto.ts index b61cb991..d1233bf5 100644 --- a/src/community/report/dto/create-report.dto.ts +++ b/src/community/report/dto/create-report.dto.ts @@ -1,15 +1,17 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator'; export class CreateReportRequestDto { @ApiProperty({ description: '게시글 ID' }) @IsNotEmpty() + @IsNumber() postId: number; - @ApiProperty({ + @ApiPropertyOptional({ description: '댓글 ID (댓글 신고일 경우 존재, 게시글 신고일 경우 null', }) @IsOptional() + @IsNumber() commentId?: number; @ApiProperty({ description: '신고 사유' }) diff --git a/src/community/report/report.service.ts b/src/community/report/report.service.ts index 58b8c89a..87b1cfca 100644 --- a/src/community/report/report.service.ts +++ b/src/community/report/report.service.ts @@ -45,7 +45,6 @@ export class ReportService { async getReportList(): Promise { const reports = await this.reportRepository.getReportList(); - console.log(reports); return reports.map((report) => new GetReportListResponseDto(report)); } From 3d8a2f041bca9c69bef24fcf642995b98aa41822 Mon Sep 17 00:00:00 2001 From: Devheun <86945971+Devheun@users.noreply.github.com> Date: Wed, 12 Feb 2025 00:07:53 +0900 Subject: [PATCH 41/48] =?UTF-8?q?feat::=20=EC=98=A4=EB=8A=98=EC=9D=98=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=ED=91=9C=20=EB=B0=98=ED=99=98=20API=20?= =?UTF-8?q?=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 반환값은 아직 구체적으로 나온 게 없어서 임의로 지정해놓은 상태 --- src/decorators/docs/timetable.decorator.ts | 24 ++++++++ .../dto/get-today-timetable-response.dto.ts | 54 ++++++++++++++++++ src/timetable/timetable.controller.ts | 10 ++++ src/timetable/timetable.service.ts | 56 +++++++++++++++++++ 4 files changed, 144 insertions(+) create mode 100644 src/timetable/dto/get-today-timetable-response.dto.ts diff --git a/src/decorators/docs/timetable.decorator.ts b/src/decorators/docs/timetable.decorator.ts index eb756d75..47edf2ca 100644 --- a/src/decorators/docs/timetable.decorator.ts +++ b/src/decorators/docs/timetable.decorator.ts @@ -17,6 +17,7 @@ import { UpdateTimetableNameDto } from 'src/timetable/dto/update-timetable-name. import { GetTimetableByUserIdResponseDto } from 'src/timetable/dto/userId-timetable.dto'; import { TimetableController } from 'src/timetable/timetable.controller'; import { ApiKukeyExceptionResponse } from '../api-kukey-exception-response'; +import { GetTodayTimetableResponse } from 'src/timetable/dto/get-today-timetable-response.dto'; type TimetableEndPoints = MethodNames; @@ -104,6 +105,29 @@ const TimetableDocsMap: Record = { }), ApiKukeyExceptionResponse(['TIMETABLE_NOT_FOUND']), ], + getTodayTimetable: [ + ApiOperation({ + summary: '오늘의 시간표 조회', + description: '오늘의 시간표를 조회합니다.', + }), + ApiQuery({ + name: 'year', + type: 'string', + required: true, + description: '연도', + }), + ApiQuery({ + name: 'semester', + type: 'string', + required: true, + description: '학기', + }), + ApiResponse({ + status: 200, + description: '오늘의 시간표 조회 성공 시', + type: GetTodayTimetableResponse, + }), + ], getTimetableByTimetableId: [ ApiOperation({ summary: '시간표 ID로 시간표 관련 정보 조회', diff --git a/src/timetable/dto/get-today-timetable-response.dto.ts b/src/timetable/dto/get-today-timetable-response.dto.ts new file mode 100644 index 00000000..e98ef5dc --- /dev/null +++ b/src/timetable/dto/get-today-timetable-response.dto.ts @@ -0,0 +1,54 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { CourseEntity } from 'src/entities/course.entity'; +import { ScheduleEntity } from 'src/entities/schedule.entity'; + +// 대표시간표의 오늘 수업 + 일정 +export class TodayCourseDto { + @ApiProperty({ description: '강의 이름' }) + courseName: string; + + @ApiProperty({ description: '교수님 성함' }) + professorName: string; + + @ApiPropertyOptional({ description: '강의실' }) + classroom?: string; + + @ApiPropertyOptional({ description: '시작 시간' }) + startTime?: string; + + @ApiPropertyOptional({ description: '종료 시간' }) + endTime?: string; +} + +export class TodayScheduleDto { + @ApiProperty({ description: '일정 이름' }) + scheduleName: string; + + @ApiProperty({ description: '시작 시간' }) + startTime: string; + + @ApiProperty({ description: '종료 시간' }) + endTime: string; + + @ApiPropertyOptional({ description: '위치' }) + location?: string; +} + +export class GetTodayTimetableResponse { + @ApiPropertyOptional({ + type: [TodayCourseDto], + description: '오늘의 강의 목록', + }) + courses?: TodayCourseDto[]; + + @ApiPropertyOptional({ + type: [TodayScheduleDto], + description: '오늘의 일정 목록', + }) + schedules?: TodayScheduleDto[]; + + constructor(courses: TodayCourseDto[], schedules: TodayScheduleDto[]) { + this.courses = courses; + this.schedules = schedules; + } +} diff --git a/src/timetable/timetable.controller.ts b/src/timetable/timetable.controller.ts index c501cbdd..f39c5071 100644 --- a/src/timetable/timetable.controller.ts +++ b/src/timetable/timetable.controller.ts @@ -28,6 +28,7 @@ import { TransactionInterceptor } from 'src/common/interceptors/transaction.inte import { TransactionManager } from 'src/decorators/manager.decorator'; import { EntityManager } from 'typeorm'; import { TimetableDocs } from 'src/decorators/docs/timetable.decorator'; +import { GetTodayTimetableResponse } from './dto/get-today-timetable-response.dto'; @Controller('timetable') @ApiTags('timetable') @@ -83,6 +84,15 @@ export class TimetableController { return await this.timetableService.getTimetableByUserId(user.id); } + // 오늘 시간표 가져오기 + @Get('/today') + async getTodayTimetable( + @Query() timetableDto: TimetableDto, + @User() user: AuthorizedUserDto, + ): Promise { + return await this.timetableService.getTodayTimetable(timetableDto, user); + } + // 특정 시간표 가져오기 @Get('/:timetableId') async getTimetableByTimetableId( diff --git a/src/timetable/timetable.service.ts b/src/timetable/timetable.service.ts index 2fbd758b..c189922a 100644 --- a/src/timetable/timetable.service.ts +++ b/src/timetable/timetable.service.ts @@ -18,6 +18,7 @@ import { isConflictingTime } from 'src/utils/time-utils'; import { DayType } from 'src/common/types/day-type.utils'; import { throwKukeyException } from 'src/utils/exception.util'; import { DeleteTimetableResponseDto } from './dto/delete-timetable-response.dto'; +import { GetTodayTimetableResponse } from './dto/get-today-timetable-response.dto'; @Injectable() export class TimetableService { @@ -551,4 +552,59 @@ export class TimetableService { newMainTimetable.mainTimetable = true; return newMainTimetable; } + + async getTodayTimetable( + timetableDto: TimetableDto, + user: AuthorizedUserDto, + ): Promise { + const today = new Date().toLocaleDateString('en-US', { + weekday: 'short', + }) as DayType; + + const mainTimetable = await this.timetableRepository.findOne({ + where: { + userId: user.id, + year: timetableDto.year, + semester: timetableDto.semester, + mainTimetable: true, + }, + }); + + const todayCourses = await this.timetableCourseRepository + .createQueryBuilder('timetableCourse') + .leftJoinAndSelect('timetableCourse.course', 'course') + .leftJoinAndSelect('course.courseDetails', 'courseDetail') + .where('timetableCourse.timetableId = :timetableId', { + timetableId: mainTimetable.id, + }) + .andWhere('courseDetail.day = :today', { today }) + .getMany(); + + const schedules = await this.scheduleService.getScheduleByTimetableId( + mainTimetable.id, + ); + const todaySchedules = schedules.filter( + (schedule) => schedule.day === today, + ); + + const todayCoursesResponse = todayCourses.map((timetableCourse) => ({ + courseName: timetableCourse.course.courseName, + classroom: timetableCourse.course.courseDetails[0].classroom, + startTime: timetableCourse.course.courseDetails[0].startTime, + endTime: timetableCourse.course.courseDetails[0].endTime, + professorName: timetableCourse.course.professorName, + })); + + const todaySchedulesResponse = todaySchedules.map((schedule) => ({ + scheduleName: schedule.title, + startTime: schedule.startTime, + endTime: schedule.endTime, + location: schedule.location, + })); + + return new GetTodayTimetableResponse( + todayCoursesResponse, + todaySchedulesResponse, + ); + } } From 09a5bc2b4b95bf7a7b7263a2d5cd037f8d1becdd Mon Sep 17 00:00:00 2001 From: JeongYeonSeung Date: Wed, 12 Feb 2025 15:28:04 +0900 Subject: [PATCH 42/48] =?UTF-8?q?mod::=20hot=20club=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20=EC=8B=9C=205=EA=B0=9C=20=EB=B0=98=ED=99=98=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 함수명, 주석 등 자잘한 부분들 수정 --- src/decorators/docs/club.decorator.ts | 6 +++--- src/home/club/club-like.repository.ts | 4 ++-- src/home/club/club.controller.ts | 4 ++-- src/home/club/club.repository.ts | 4 ++-- src/home/club/club.service.ts | 10 +++++----- src/home/club/dto/get-hot-club-response.dto.ts | 2 ++ 6 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/decorators/docs/club.decorator.ts b/src/decorators/docs/club.decorator.ts index bcc39425..ebb64d86 100644 --- a/src/decorators/docs/club.decorator.ts +++ b/src/decorators/docs/club.decorator.ts @@ -78,14 +78,14 @@ const ClubDocsMap: Record = { }), ApiKukeyExceptionResponse(['CLUB_NOT_FOUND']), ], - getHotClubList: [ + getHotClubs: [ ApiOperation({ summary: 'Hot Club 목록 조회', description: - '최근 일주일 동안 좋아요 개수가 가장 많은 동아리 4개를 반환합니다.', + '최근 일주일 동안 좋아요 개수가 가장 많은 동아리 5개를 반환합니다.', }), ApiOkResponse({ - description: 'Hot Club 목록 4개 반환', + description: 'Hot Club 목록 5개 반환', isArray: true, type: GetHotClubResponseDto, }), diff --git a/src/home/club/club-like.repository.ts b/src/home/club/club-like.repository.ts index 3eef0a88..92c0f3b5 100644 --- a/src/home/club/club-like.repository.ts +++ b/src/home/club/club-like.repository.ts @@ -15,7 +15,7 @@ export class ClubLikeRepository extends Repository { const oneWeekAgo = new Date(); oneWeekAgo.setDate(oneWeekAgo.getDate() - 7); - // 일주일 간 좋아요 개수가 가장 많은 동아리 4개 반환, 좋아요 개수가 같은 경우 랜덤 선택 + // 일주일 간 좋아요 개수가 가장 많은 동아리 5개 반환, 좋아요 개수가 같은 경우 랜덤 선택 const topClubLikes = await this.createQueryBuilder('club_like') .select('club_like.clubId') .addSelect('COUNT(club_like.id)', 'likeCount') @@ -23,7 +23,7 @@ export class ClubLikeRepository extends Repository { .groupBy('club_like.clubId') .orderBy('likeCount', 'DESC') .addOrderBy('RAND()') - .limit(4) + .limit(5) .getRawMany(); return topClubLikes; diff --git a/src/home/club/club.controller.ts b/src/home/club/club.controller.ts index 663186fb..d537f548 100644 --- a/src/home/club/club.controller.ts +++ b/src/home/club/club.controller.ts @@ -68,8 +68,8 @@ export class ClubController { } @Get('hot') - async getHotClubList(): Promise { - return await this.clubService.getHotClubList(); + async getHotClubs(): Promise { + return await this.clubService.getHotClubs(); } @UseGuards(OptionalJwtAuthGuard) diff --git a/src/home/club/club.repository.ts b/src/home/club/club.repository.ts index 682470ad..65d3d4cc 100644 --- a/src/home/club/club.repository.ts +++ b/src/home/club/club.repository.ts @@ -60,11 +60,11 @@ export class ClubRepository extends Repository { } async findClubsByAllLikesAndRandom(): Promise { - // allLikes 순서대로 4개 반환, allLikes 같은 경우 랜덤으로 선택 + // allLikes 순서대로 5개 반환, allLikes 같은 경우 랜덤으로 선택 return await this.createQueryBuilder('club') .orderBy('club.allLikes', 'DESC') .addOrderBy('RAND()') - .limit(4) + .limit(5) .getMany(); } diff --git a/src/home/club/club.service.ts b/src/home/club/club.service.ts index 1ff3acaf..8ed833be 100644 --- a/src/home/club/club.service.ts +++ b/src/home/club/club.service.ts @@ -108,22 +108,22 @@ export class ClubService { } } - async getHotClubList(): Promise { + async getHotClubs(): Promise { const topLikedClubsInfo = await this.clubLikeRepository.findTopLikedClubsInfo(); const hotClubIds = topLikedClubsInfo.map((info) => info.clubId); const hotClubs = await this.clubRepository.findClubsByIdOrder(hotClubIds); - // hotClubs의 개수가 4개 미만인 경우, 전체 좋아요 개수 기준으로 높은 것부터 선택(좋아요 개수 같은 경우 랜덤 선택)하여 부족한 수를 채움 - const additionalClubsNeeded = 4 - hotClubs.length; + // hotClubs의 개수가 5개 미만인 경우, 전체 좋아요 개수 기준으로 높은 것부터 선택(좋아요 개수 같은 경우 랜덤 선택)하여 부족한 수를 채움 + const additionalCount = 5 - hotClubs.length; const allClubs = await this.clubRepository.findClubsByAllLikesAndRandom(); - // 전체 찜 개수 기준으로 가져온 동아리 중 hotClubs내에 이미 포함된 경우 제거 + // 전체 좋아요 개수 기준으로 가져온 동아리 중 hotClubs내에 이미 포함된 경우 제거 const existingClubIds = new Set(hotClubs.map((hc) => hc.id)); const additionalClubs = allClubs .filter((club) => !existingClubIds.has(club.id)) - .slice(0, additionalClubsNeeded); + .slice(0, additionalCount); const combinedClubs = [...hotClubs, ...additionalClubs]; let ranking = 1; diff --git a/src/home/club/dto/get-hot-club-response.dto.ts b/src/home/club/dto/get-hot-club-response.dto.ts index 70c4cdec..eba2041e 100644 --- a/src/home/club/dto/get-hot-club-response.dto.ts +++ b/src/home/club/dto/get-hot-club-response.dto.ts @@ -3,6 +3,7 @@ import { GetClubResponseDto } from './get-club-response.dto'; import { ClubEntity } from 'src/entities/club.entity'; export class GetHotClubResponseDto extends PickType(GetClubResponseDto, [ + 'clubId', 'name', 'summary', 'imageUrl', @@ -15,6 +16,7 @@ export class GetHotClubResponseDto extends PickType(GetClubResponseDto, [ constructor(club: ClubEntity, ranking: number) { super(); + this.clubId = club.id; this.name = club.name; this.summary = club.summary; this.imageUrl = club.imageUrl; From 87c14ca4df93dfd7ef93c64a4005563c0235b6bf Mon Sep 17 00:00:00 2001 From: JeongYeonSeung Date: Wed, 12 Feb 2025 16:20:32 +0900 Subject: [PATCH 43/48] =?UTF-8?q?mod::=20recommend=20club=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=8B=9C=205=EA=B0=9C=20=EB=B0=98=ED=99=98?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 함수명 및 주석 등 자잘한 부분들 수정 --- src/decorators/docs/club.decorator.ts | 4 ++-- src/home/club/club.controller.ts | 4 ++-- src/home/club/club.repository.ts | 4 ++-- src/home/club/club.service.ts | 16 ++++++++-------- .../club/dto/get-recommend-club-response.dto.ts | 2 ++ 5 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/decorators/docs/club.decorator.ts b/src/decorators/docs/club.decorator.ts index ebb64d86..76ec3017 100644 --- a/src/decorators/docs/club.decorator.ts +++ b/src/decorators/docs/club.decorator.ts @@ -90,7 +90,7 @@ const ClubDocsMap: Record = { type: GetHotClubResponseDto, }), ], - getRecommendClubList: [ + getRecommendClubs: [ ApiOperation({ summary: 'Recommend Club 목록 조회', description: @@ -102,7 +102,7 @@ const ClubDocsMap: Record = { required: true, }), ApiOkResponse({ - description: 'Recommend Club 목록 4개 반환', + description: 'Recommend Club 목록 5개 반환', isArray: true, type: GetRecommendClubResponseDto, }), diff --git a/src/home/club/club.controller.ts b/src/home/club/club.controller.ts index d537f548..38e6d880 100644 --- a/src/home/club/club.controller.ts +++ b/src/home/club/club.controller.ts @@ -74,11 +74,11 @@ export class ClubController { @UseGuards(OptionalJwtAuthGuard) @Get('recommend') - async getRecommendClubList( + async getRecommendClubs( @User() user: AuthorizedUserDto | null, @Query() getRecommendClubRequestDto: GetRecommendClubRequestDto, ): Promise { - return await this.clubService.getRecommendClubList( + return await this.clubService.getRecommendClubs( user, getRecommendClubRequestDto, ); diff --git a/src/home/club/club.repository.ts b/src/home/club/club.repository.ts index 65d3d4cc..a8646137 100644 --- a/src/home/club/club.repository.ts +++ b/src/home/club/club.repository.ts @@ -69,10 +69,10 @@ export class ClubRepository extends Repository { } async findClubsByRandom(): Promise { - // 랜덤 4개 반환 + // 랜덤 5개 반환 return await this.createQueryBuilder('club') .orderBy('RAND()') - .limit(4) + .limit(5) .getMany(); } diff --git a/src/home/club/club.service.ts b/src/home/club/club.service.ts index 8ed833be..6c179cca 100644 --- a/src/home/club/club.service.ts +++ b/src/home/club/club.service.ts @@ -133,7 +133,7 @@ export class ClubService { }); } - async getRecommendClubList( + async getRecommendClubs( user: AuthorizedUserDto | null, requestDto: GetRecommendClubRequestDto, ): Promise { @@ -153,7 +153,7 @@ export class ClubService { const likedClubCategories = await this.clubLikeRepository.findLikedClubCategories(userId); - // 좋아요 누른 동아리가 없을 경우 무작위로 4개 선정 + // 좋아요 누른 동아리가 없을 경우 무작위로 5개 선정 if (likedClubCategories.length === 0) { const recommendClubs = await this.clubRepository.findClubsByRandom(); return recommendClubs.map((club) => { @@ -162,10 +162,10 @@ export class ClubService { } const recommendClubList: GetRecommendClubResponseDto[] = []; - const clubsPerCategory = Math.ceil(4 / likedClubCategories.length); + const clubsPerCategory = Math.round(5 / likedClubCategories.length); const shuffledCategories = this.shuffleArray(likedClubCategories); - // 좋아요 누른 동아리의 카테고리 수에 따라 비율에 맞게 4개 선정 + // 좋아요 누른 동아리의 카테고리 수에 따라 비율에 맞게 5개 선정 for (const category of shuffledCategories) { const clubs = await this.clubRepository.findClubsByCategoryAndRandom( category, @@ -176,11 +176,11 @@ export class ClubService { }); recommendClubList.push(...recommendClubs); - if (recommendClubs.length >= 4) break; + if (recommendClubs.length >= 5) break; } // 부족한 경우, 랜덤으로 채움 - if (recommendClubList.length < 4) { + if (recommendClubList.length < 5) { const existingClubNames = new Set(recommendClubList.map((rc) => rc.name)); const randomClubs = await this.clubRepository.findClubsByRandom(); const additionalClubs = randomClubs @@ -191,8 +191,8 @@ export class ClubService { recommendClubList.push(...additionalClubs); } - // 앞에서부터 4개를 랜덤한 순서로 반환 - return this.shuffleArray(recommendClubList.slice(0, 4)); + // 앞에서부터 5개를 랜덤한 순서로 반환 + return this.shuffleArray(recommendClubList.slice(0, 5)); } async createClub( diff --git a/src/home/club/dto/get-recommend-club-response.dto.ts b/src/home/club/dto/get-recommend-club-response.dto.ts index 6cefa30c..d92e55ab 100644 --- a/src/home/club/dto/get-recommend-club-response.dto.ts +++ b/src/home/club/dto/get-recommend-club-response.dto.ts @@ -3,6 +3,7 @@ import { GetClubResponseDto } from './get-club-response.dto'; import { ClubEntity } from 'src/entities/club.entity'; export class GetRecommendClubResponseDto extends PickType(GetClubResponseDto, [ + 'clubId', 'name', 'summary', 'imageUrl', @@ -12,6 +13,7 @@ export class GetRecommendClubResponseDto extends PickType(GetClubResponseDto, [ constructor(club: ClubEntity) { super(); + this.clubId = club.id; this.name = club.name; this.summary = club.summary; this.imageUrl = club.imageUrl; From 12b5b60c8663c81543fc17b5bd4d24200871cbd5 Mon Sep 17 00:00:00 2001 From: JeongYeonSeung Date: Wed, 12 Feb 2025 16:27:45 +0900 Subject: [PATCH 44/48] =?UTF-8?q?rename::=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=EB=AA=85=EC=97=90=20=EC=9E=90=EB=A3=8C=ED=98=95=20=EB=84=A3?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - swagger 일부 수정 --- src/decorators/docs/club.decorator.ts | 2 +- src/home/club/club.controller.ts | 4 ++-- src/home/club/club.service.ts | 8 ++++---- src/home/club/dto/get-club-request.ts | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/decorators/docs/club.decorator.ts b/src/decorators/docs/club.decorator.ts index 76ec3017..709135d3 100644 --- a/src/decorators/docs/club.decorator.ts +++ b/src/decorators/docs/club.decorator.ts @@ -23,7 +23,7 @@ import { ApiKukeyExceptionResponse } from '../api-kukey-exception-response'; type ClubEndPoints = MethodNames; const ClubDocsMap: Record = { - getClubList: [ + getClubs: [ ApiOperation({ summary: '동아리 목록 조회', description: diff --git a/src/home/club/club.controller.ts b/src/home/club/club.controller.ts index 38e6d880..1fb53d98 100644 --- a/src/home/club/club.controller.ts +++ b/src/home/club/club.controller.ts @@ -44,11 +44,11 @@ export class ClubController { @UseGuards(OptionalJwtAuthGuard) @Get() - async getClubList( + async getClubs( @User() user: AuthorizedUserDto | null, @Query() getClubRequestDto: GetClubRequestDto, ): Promise { - return await this.clubService.getClubList(user, getClubRequestDto); + return await this.clubService.getClubs(user, getClubRequestDto); } @UseGuards(JwtAuthGuard) diff --git a/src/home/club/club.service.ts b/src/home/club/club.service.ts index 6c179cca..cc6fba06 100644 --- a/src/home/club/club.service.ts +++ b/src/home/club/club.service.ts @@ -26,7 +26,7 @@ export class ClubService { private readonly fileService: FileService, ) {} - async getClubList( + async getClubs( user: AuthorizedUserDto | null, requestDto: GetClubRequestDto, ): Promise { @@ -50,7 +50,7 @@ export class ClubService { } // 현재 접속 중인 유저의 각 동아리에 대한 좋아요 여부 함께 반환. 유저 존재하지 않을 시 false - let clubList = clubs.map((club) => { + let response = clubs.map((club) => { const isLiked = club.clubLikes.some((clubLike) => user && isLogin && clubLike.user ? clubLike.user.id === user.id : false, ); @@ -59,9 +59,9 @@ export class ClubService { // 내가 좋아요를 누른 동아리만 보기 (유저 존재한다면) if (user && isLogin && wishList) { - clubList = clubList.filter((club) => club.isLiked === true); + response = response.filter((club) => club.isLiked === true); } - return clubList; + return response; } async toggleLikeClub( diff --git a/src/home/club/dto/get-club-request.ts b/src/home/club/dto/get-club-request.ts index f2099d7e..de709072 100644 --- a/src/home/club/dto/get-club-request.ts +++ b/src/home/club/dto/get-club-request.ts @@ -1,4 +1,4 @@ -import { ApiPropertyOptional } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { IsBoolean, IsIn, @@ -36,6 +36,6 @@ export class GetClubRequestDto { @IsNotEmpty() @ToBoolean() @IsBoolean() - @ApiPropertyOptional({ description: '로그인 여부' }) + @ApiProperty({ description: '로그인 여부' }) isLogin: boolean; } From 99ef0513be10f60215d58080866b625e25caf1de Mon Sep 17 00:00:00 2001 From: JeongYeonSeung Date: Wed, 19 Feb 2025 00:25:48 +0900 Subject: [PATCH 45/48] =?UTF-8?q?feat::=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20DTO=EC=97=90=20=EB=A7=81=ED=81=AC=20?= =?UTF-8?q?=EA=B0=9C=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/home/club/club.service.ts | 4 +++- src/home/club/dto/get-club-detail-response.dto.ts | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/home/club/club.service.ts b/src/home/club/club.service.ts index a4e70a82..c3e972f6 100644 --- a/src/home/club/club.service.ts +++ b/src/home/club/club.service.ts @@ -90,7 +90,9 @@ export class ClubService { user && isLogin && clubLike.user ? clubLike.user.id === user.id : false, ); - return new GetClubDetailResponseDto(club, isLiked); + const linkCount = (club.instagramLink ? 1 : 0) + (club.youtubeLink ? 1 : 0); + + return new GetClubDetailResponseDto(club, isLiked, linkCount); } async toggleLikeClub( diff --git a/src/home/club/dto/get-club-detail-response.dto.ts b/src/home/club/dto/get-club-detail-response.dto.ts index a8732521..90ea0766 100644 --- a/src/home/club/dto/get-club-detail-response.dto.ts +++ b/src/home/club/dto/get-club-detail-response.dto.ts @@ -39,7 +39,10 @@ export class GetClubDetailResponseDto { @ApiProperty({ description: '좋아요 여부' }) isLiked: boolean; - constructor(club: ClubEntity, isLiked: boolean) { + @ApiProperty({ description: '링크 개수' }) + linkCount: number; + + constructor(club: ClubEntity, isLiked: boolean, linkCount: number) { this.clubId = club.id; this.name = club.name; this.category = club.category; @@ -53,5 +56,6 @@ export class GetClubDetailResponseDto { this.instagramLink = club.instagramLink; this.youtubeLink = club.youtubeLink; this.isLiked = isLiked; + this.linkCount = linkCount; } } From ddd44a1977fa896dbb659fbd479ce3a3b09e0086 Mon Sep 17 00:00:00 2001 From: Devheun <86945971+Devheun@users.noreply.github.com> Date: Mon, 24 Feb 2025 00:56:02 +0900 Subject: [PATCH 46/48] =?UTF-8?q?refactor:=20PR=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - getMainTimetable 메서드를 만들어놓고 깜빡하고 있었네요.. (Shout to 성현) --- src/timetable/dto/get-today-timetable-response.dto.ts | 2 -- src/timetable/timetable.service.ts | 9 +-------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/timetable/dto/get-today-timetable-response.dto.ts b/src/timetable/dto/get-today-timetable-response.dto.ts index e98ef5dc..18b682de 100644 --- a/src/timetable/dto/get-today-timetable-response.dto.ts +++ b/src/timetable/dto/get-today-timetable-response.dto.ts @@ -1,6 +1,4 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { CourseEntity } from 'src/entities/course.entity'; -import { ScheduleEntity } from 'src/entities/schedule.entity'; // 대표시간표의 오늘 수업 + 일정 export class TodayCourseDto { diff --git a/src/timetable/timetable.service.ts b/src/timetable/timetable.service.ts index c189922a..041cdecb 100644 --- a/src/timetable/timetable.service.ts +++ b/src/timetable/timetable.service.ts @@ -561,14 +561,7 @@ export class TimetableService { weekday: 'short', }) as DayType; - const mainTimetable = await this.timetableRepository.findOne({ - where: { - userId: user.id, - year: timetableDto.year, - semester: timetableDto.semester, - mainTimetable: true, - }, - }); + const mainTimetable = await this.getMainTimetable(timetableDto, user); const todayCourses = await this.timetableCourseRepository .createQueryBuilder('timetableCourse') From 4482988e94d27a911fd89ecad336e14f0ec33d6a Mon Sep 17 00:00:00 2001 From: JeongYeonSeung Date: Tue, 25 Feb 2025 00:37:14 +0900 Subject: [PATCH 47/48] =?UTF-8?q?mod::=20=EB=B0=98=ED=99=98=ED=95=A0=20clu?= =?UTF-8?q?b=20=EA=B0=9C=EC=88=98=20=EC=83=81=EC=88=98=EB=A1=9C=20?= =?UTF-8?q?=EB=BA=B4=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/constant/club-count.constant.ts | 1 + src/home/club/club-like.repository.ts | 3 ++- src/home/club/club.repository.ts | 5 +++-- src/home/club/club.service.ts | 13 ++++++++----- 4 files changed, 14 insertions(+), 8 deletions(-) create mode 100644 src/common/constant/club-count.constant.ts diff --git a/src/common/constant/club-count.constant.ts b/src/common/constant/club-count.constant.ts new file mode 100644 index 00000000..6a4d4f16 --- /dev/null +++ b/src/common/constant/club-count.constant.ts @@ -0,0 +1 @@ +export const CLUB_COUNT = 5; diff --git a/src/home/club/club-like.repository.ts b/src/home/club/club-like.repository.ts index 92c0f3b5..e933b9c4 100644 --- a/src/home/club/club-like.repository.ts +++ b/src/home/club/club-like.repository.ts @@ -1,4 +1,5 @@ import { Injectable } from '@nestjs/common'; +import { CLUB_COUNT } from 'src/common/constant/club-count.constant'; import { ClubLikeEntity } from 'src/entities/club-like.entity'; import { DataSource, Repository } from 'typeorm'; @@ -23,7 +24,7 @@ export class ClubLikeRepository extends Repository { .groupBy('club_like.clubId') .orderBy('likeCount', 'DESC') .addOrderBy('RAND()') - .limit(5) + .limit(CLUB_COUNT) .getRawMany(); return topClubLikes; diff --git a/src/home/club/club.repository.ts b/src/home/club/club.repository.ts index a8646137..f58c094b 100644 --- a/src/home/club/club.repository.ts +++ b/src/home/club/club.repository.ts @@ -1,4 +1,5 @@ import { Injectable } from '@nestjs/common'; +import { CLUB_COUNT } from 'src/common/constant/club-count.constant'; import { ClubCategory } from 'src/common/types/club-category-type'; import { ClubEntity } from 'src/entities/club.entity'; import { Brackets, DataSource, Repository } from 'typeorm'; @@ -64,7 +65,7 @@ export class ClubRepository extends Repository { return await this.createQueryBuilder('club') .orderBy('club.allLikes', 'DESC') .addOrderBy('RAND()') - .limit(5) + .limit(CLUB_COUNT) .getMany(); } @@ -72,7 +73,7 @@ export class ClubRepository extends Repository { // 랜덤 5개 반환 return await this.createQueryBuilder('club') .orderBy('RAND()') - .limit(5) + .limit(CLUB_COUNT) .getMany(); } diff --git a/src/home/club/club.service.ts b/src/home/club/club.service.ts index cc6fba06..ad49d4f8 100644 --- a/src/home/club/club.service.ts +++ b/src/home/club/club.service.ts @@ -17,6 +17,7 @@ import { UpdateClubRequestDto } from './dto/update-club-request-dto'; import { UpdateClubResponseDto } from './dto/update-club-response-dto'; import { DeleteClubResponseDto } from './dto/delete-club-response-dto'; import { throwKukeyException } from 'src/utils/exception.util'; +import { CLUB_COUNT } from 'src/common/constant/club-count.constant'; @Injectable() export class ClubService { @@ -116,7 +117,7 @@ export class ClubService { const hotClubs = await this.clubRepository.findClubsByIdOrder(hotClubIds); // hotClubs의 개수가 5개 미만인 경우, 전체 좋아요 개수 기준으로 높은 것부터 선택(좋아요 개수 같은 경우 랜덤 선택)하여 부족한 수를 채움 - const additionalCount = 5 - hotClubs.length; + const additionalCount = CLUB_COUNT - hotClubs.length; const allClubs = await this.clubRepository.findClubsByAllLikesAndRandom(); // 전체 좋아요 개수 기준으로 가져온 동아리 중 hotClubs내에 이미 포함된 경우 제거 @@ -162,7 +163,9 @@ export class ClubService { } const recommendClubList: GetRecommendClubResponseDto[] = []; - const clubsPerCategory = Math.round(5 / likedClubCategories.length); + const clubsPerCategory = Math.round( + CLUB_COUNT / likedClubCategories.length, + ); const shuffledCategories = this.shuffleArray(likedClubCategories); // 좋아요 누른 동아리의 카테고리 수에 따라 비율에 맞게 5개 선정 @@ -176,11 +179,11 @@ export class ClubService { }); recommendClubList.push(...recommendClubs); - if (recommendClubs.length >= 5) break; + if (recommendClubs.length >= CLUB_COUNT) break; } // 부족한 경우, 랜덤으로 채움 - if (recommendClubList.length < 5) { + if (recommendClubList.length < CLUB_COUNT) { const existingClubNames = new Set(recommendClubList.map((rc) => rc.name)); const randomClubs = await this.clubRepository.findClubsByRandom(); const additionalClubs = randomClubs @@ -192,7 +195,7 @@ export class ClubService { } // 앞에서부터 5개를 랜덤한 순서로 반환 - return this.shuffleArray(recommendClubList.slice(0, 5)); + return this.shuffleArray(recommendClubList.slice(0, CLUB_COUNT)); } async createClub( From d781633fbd60e45d2df1d8fee63b1199dd3fde31 Mon Sep 17 00:00:00 2001 From: JeongYeonSeung Date: Tue, 25 Feb 2025 22:23:17 +0900 Subject: [PATCH 48/48] =?UTF-8?q?fix::=20club=20controller=20=EB=9D=BC?= =?UTF-8?q?=EC=9A=B0=ED=8C=85=20=EC=88=9C=EC=84=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/home/club/club.controller.ts | 34 ++++++++++++++++---------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/home/club/club.controller.ts b/src/home/club/club.controller.ts index a5279a42..56b6ea97 100644 --- a/src/home/club/club.controller.ts +++ b/src/home/club/club.controller.ts @@ -53,6 +53,23 @@ export class ClubController { return await this.clubService.getClubs(user, getClubRequestDto); } + @Get('hot') + async getHotClubs(): Promise { + return await this.clubService.getHotClubs(); + } + + @UseGuards(OptionalJwtAuthGuard) + @Get('recommend') + async getRecommendClubs( + @User() user: AuthorizedUserDto | null, + @Query() getRecommendClubRequestDto: GetRecommendClubRequestDto, + ): Promise { + return await this.clubService.getRecommendClubs( + user, + getRecommendClubRequestDto, + ); + } + @UseGuards(OptionalJwtAuthGuard) @Get('/:clubId') async getClubDetail( @@ -78,23 +95,6 @@ export class ClubController { ); } - @Get('hot') - async getHotClubs(): Promise { - return await this.clubService.getHotClubs(); - } - - @UseGuards(OptionalJwtAuthGuard) - @Get('recommend') - async getRecommendClubs( - @User() user: AuthorizedUserDto | null, - @Query() getRecommendClubRequestDto: GetRecommendClubRequestDto, - ): Promise { - return await this.clubService.getRecommendClubs( - user, - getRecommendClubRequestDto, - ); - } - @UseGuards(JwtAuthGuard, RolesGuard) @Roles(Role.admin) @UseInterceptors(FileInterceptor('clubImage'))