From 7eb4995f85ca61205dfe3a812308225c3efb22dc Mon Sep 17 00:00:00 2001 From: yesjuhee Date: Wed, 1 May 2024 21:26:09 +0900 Subject: [PATCH 1/7] =?UTF-8?q?fix:=20schema.prisma=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/schema.prisma | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b971d49..b2b0873 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -53,9 +53,9 @@ model Process { studentId Int? @unique headReviewerId Int? phaseId Int? - headReviewer User? @relation("headReviewer", fields: [headReviewerId], references: [id]) + headReviewer User? @relation("headReviewer", fields: [headReviewerId], references: [id], onDelete: SetNull) phase Phase? @relation(fields: [phaseId], references: [id]) - student User? @relation("student", fields: [studentId], references: [id]) + student User? @relation("student", fields: [studentId], references: [id], onDelete: Cascade) reviewers Reviewer[] thesisInfos ThesisInfo[] @@ -87,7 +87,7 @@ model ThesisInfo { processId Int? reviews Review[] thesisFiles ThesisFile[] - process Process? @relation(fields: [processId], references: [id]) + process Process? @relation(fields: [processId], references: [id], onDelete: Cascade) @@index([processId], map: "thesis_info_processId_fkey") @@map("thesis_info") @@ -99,8 +99,8 @@ model Reviewer { reviewerId Int? processId Int? role Role - process Process? @relation(fields: [processId], references: [id]) - reviewer User? @relation(fields: [reviewerId], references: [id]) + process Process? @relation(fields: [processId], references: [id], onDelete: Cascade) + reviewer User? @relation(fields: [reviewerId], references: [id], onDelete: Cascade) @@index([processId], map: "reviewer_processId_fkey") @@index([reviewerId], map: "reviewer_reviewerId_fkey") @@ -120,8 +120,8 @@ model Review { thesisInfoId Int? fileId String? @unique file File? @relation(fields: [fileId], references: [uuid]) - reviewer User? @relation(fields: [reviewerId], references: [id]) - thesisInfo ThesisInfo? @relation(fields: [thesisInfoId], references: [id]) + reviewer User? @relation(fields: [reviewerId], references: [id], onDelete: Cascade) + thesisInfo ThesisInfo? @relation(fields: [thesisInfoId], references: [id], onDelete: Cascade) @@index([reviewerId], map: "review_reviewerId_fkey") @@index([thesisInfoId], map: "review_thesisInfoId_fkey") @@ -151,7 +151,7 @@ model ThesisFile { thesisInfoId Int? fileId String? @unique file File? @relation(fields: [fileId], references: [uuid]) - ThesisInfo ThesisInfo? @relation(fields: [thesisInfoId], references: [id]) + ThesisInfo ThesisInfo? @relation(fields: [thesisInfoId], references: [id], onDelete: Cascade) @@index([thesisInfoId], map: "thesis_file_thesisInfoId_fkey") @@map("thesis_file") @@ -167,7 +167,7 @@ model Achievements { authorType AuthorType authorNumbers Int userId Int? - User User? @relation(fields: [userId], references: [id]) + User User? @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId], map: "achievements_userId_fkey") @@map("achievements") From ad3941dc513342f9ee70a16c9c476e6b80fb18ec Mon Sep 17 00:00:00 2001 From: yesjuhee Date: Wed, 1 May 2024 22:29:52 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20=ED=95=99=EC=83=9D=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/files/files.module.ts | 1 + src/modules/students/students.controller.ts | 14 +++++++ src/modules/students/students.module.ts | 3 +- src/modules/students/students.service.ts | 45 ++++++++++++++++++++- 4 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/modules/files/files.module.ts b/src/modules/files/files.module.ts index 39b548b..89415b2 100644 --- a/src/modules/files/files.module.ts +++ b/src/modules/files/files.module.ts @@ -5,5 +5,6 @@ import { FilesService } from "./files.service"; @Module({ controllers: [FilesController], providers: [FilesService], + exports: [FilesService], }) export class FilesModule {} diff --git a/src/modules/students/students.controller.ts b/src/modules/students/students.controller.ts index 75ad21a..7a4d709 100644 --- a/src/modules/students/students.controller.ts +++ b/src/modules/students/students.controller.ts @@ -395,6 +395,7 @@ export class StudentsController { return new CommonResponseDto(reviewersDto); } + // 삭제 @Delete("") @ApiOperation({ summary: "학생 목록 삭제", @@ -408,4 +409,17 @@ export class StudentsController { return new CommonResponseDto(); } + + @Delete("/:id") + @ApiOperation({ + summary: "학생 삭제 API", + description: "아이디에 해당하는 학생을 DB에서 삭제하고, 관련 데이터와 파일도 모두 삭제한다.", + }) + @UseUserTypeGuard([UserType.ADMIN]) + @ApiUnauthorizedResponse({ description: "[관리자] 로그인 후 접근 가능" }) + @ApiInternalServerErrorResponse({ description: "서버 내부 오류" }) + async deleteStudent(@Param("id", PositiveIntPipe) studentId: number) { + await this.studentsService.deleteStudent(studentId); + return new CommonResponseDto(); + } } diff --git a/src/modules/students/students.module.ts b/src/modules/students/students.module.ts index 159c870..586f837 100644 --- a/src/modules/students/students.module.ts +++ b/src/modules/students/students.module.ts @@ -2,10 +2,11 @@ import { Module } from "@nestjs/common"; import { StudentsController } from "./students.controller"; import { StudentsService } from "./students.service"; import { AuthModule } from "../auth/auth.module"; +import { FilesModule } from "../files/files.module"; @Module({ controllers: [StudentsController], providers: [StudentsService], - imports: [AuthModule], + imports: [AuthModule, FilesModule], }) export class StudentsModule {} diff --git a/src/modules/students/students.service.ts b/src/modules/students/students.service.ts index 062a596..fb89e01 100644 --- a/src/modules/students/students.service.ts +++ b/src/modules/students/students.service.ts @@ -1,3 +1,4 @@ +import { FilesService } from "./../files/files.service"; import { ReviewerRoleQuery, UpdateReviewerQueryDto } from "./dtos/update-reviewer-query-dto"; import { ThesisInfoQueryDto, ThesisQueryType } from "./dtos/thesis-info-query.dto"; import { @@ -29,7 +30,8 @@ import { UpdateSystemDto } from "./dtos/update-system.dto"; export class StudentsService { constructor( private readonly prismaService: PrismaService, - private readonly authService: AuthService + private readonly authService: AuthService, + private readonly fileService: FilesService ) {} // 학생 생성 API @@ -2052,4 +2054,45 @@ export class StudentsService { throw new InternalServerErrorException("학생 목록 삭제에 실패했습니다."); } } + + async deleteStudent(studentId: number) { + // 학생 아이디 확인 + const student = await this.prismaService.user.findUnique({ + where: { + id: studentId, + type: UserType.STUDENT, + deletedAt: null, + }, + include: { studentProcess: { include: { thesisInfos: { include: { thesisFiles: true, reviews: true } } } } }, + }); + if (!student) { + throw new BadRequestException("해당하는 학생을 찾을 수 없습니다."); + } + + // 파일 데이터 확인 -> 미리 삭제 (Cascade로 삭제 안됨) + const thesisFiles = student.studentProcess.thesisInfos.flatMap((thesisInfo) => thesisInfo.thesisFiles); + const reviews = student.studentProcess.thesisInfos.flatMap((thesisInfo) => thesisInfo.reviews); + const thesisFileUUIDs = thesisFiles.map((thesisFile) => thesisFile.fileId).filter((uuid) => uuid !== null); // 학생 논문, 논문 발표, 수정 지시사항 관련 파일 uuid + const reviewFileUUIDs = reviews.map((review) => review.fileId).filter((uuid) => uuid !== null); // 교수 논문 심사 관련 파일 uuid + const fileUUIDs = [...thesisFileUUIDs, ...reviewFileUUIDs]; // 학생과 관련 있는 모든 파일 uuid + // minIO, file 테이블에서 삭제 (FileService 이용) + for (const uuid of fileUUIDs) { + try { + await this.fileService.deleteFile(uuid); + } catch (error) { + console.log(error); + throw new InternalServerErrorException("파일 삭제 오류 발생"); + } + } + // 학생 데이터 모두 삭제 (Hard delete) + // cascade로 삭제되는 연관된 데이터들 : Achivements, Process, Reviewers, ThesisInfos, Reviews, ThesisFiles + try { + await this.prismaService.user.delete({ + where: { id: studentId }, + }); + } catch (error) { + console.log(error); + throw new InternalServerErrorException("학생 삭제 오류 발생"); + } + } } From 7155d07c284118e6450cc190d7e046c33f502ca0 Mon Sep 17 00:00:00 2001 From: yesjuhee Date: Wed, 1 May 2024 23:01:43 +0900 Subject: [PATCH 3/7] =?UTF-8?q?fix:=20=ED=95=99=EC=83=9D=20=EC=9D=BC?= =?UTF-8?q?=EA=B4=84=20=EC=82=AD=EC=A0=9C=20API=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/students/students.service.ts | 39 +++++++++++++++++------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/src/modules/students/students.service.ts b/src/modules/students/students.service.ts index fb89e01..91de3a6 100644 --- a/src/modules/students/students.service.ts +++ b/src/modules/students/students.service.ts @@ -25,6 +25,7 @@ import { validate } from "class-validator"; import { UpdateThesisInfoDto } from "./dtos/update-thesis-info.dto"; import { Readable } from "stream"; import { UpdateSystemDto } from "./dtos/update-system.dto"; +import { file } from "jszip"; @Injectable() export class StudentsService { @@ -2040,18 +2041,31 @@ export class StudentsService { } async deleteStudentsList() { + // 논문 파일, 서명 파일 가져오기 + const files = await this.prismaService.file.findMany({ + where: {}, + include: { review: true, thesisFile: true, professor: true }, + }); + const fileUUIDs = files.filter((file) => file.thesisFile !== null || file.review !== null).map((file) => file.uuid); // 모든 논문 파일과 리뷰 파일 + // minIO, file 테이블에서 삭제 (FileService 이용) + for (const uuid of fileUUIDs) { + try { + await this.fileService.deleteFile(uuid); + } catch (error) { + console.log(error); + throw new InternalServerErrorException("파일 삭제 오류 발생"); + } + } + + // 학생 데이터 모두 삭제 (Hard delete) + // cascade로 삭제되는 연관된 데이터들 : Achivements, Process, Reviewers, ThesisInfos, Reviews, ThesisFiles try { - return await this.prismaService.user.updateMany({ - where: { - type: UserType.STUDENT, - deletedAt: null, - }, - data: { - deletedAt: DateUtil.getCurrentTime().fullDateTime, - }, + await this.prismaService.user.deleteMany({ + where: { type: UserType.STUDENT }, }); - } catch (e) { - throw new InternalServerErrorException("학생 목록 삭제에 실패했습니다."); + } catch (error) { + console.log(error); + throw new InternalServerErrorException("학생 삭제 오류 발생"); } } @@ -2088,7 +2102,10 @@ export class StudentsService { // cascade로 삭제되는 연관된 데이터들 : Achivements, Process, Reviewers, ThesisInfos, Reviews, ThesisFiles try { await this.prismaService.user.delete({ - where: { id: studentId }, + where: { + id: studentId, + type: UserType.STUDENT, + }, }); } catch (error) { console.log(error); From 6be93be7691798f140a21201a2f6f50badd5c5c6 Mon Sep 17 00:00:00 2001 From: yesjuhee Date: Wed, 1 May 2024 23:23:25 +0900 Subject: [PATCH 4/7] =?UTF-8?q?fix:=20=ED=95=99=EA=B3=BC=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20API=20=EC=82=AC=EC=9A=A9=20=EC=8B=9C=20=EC=86=8C?= =?UTF-8?q?=EC=86=8D=20=ED=95=99=EC=83=9D=20=EC=82=AD=EC=A0=9C=EB=90=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../departments/departments.controller.ts | 2 +- src/modules/departments/departments.module.ts | 2 ++ .../departments/departments.service.ts | 21 +++++++++++++++++-- src/modules/students/students.module.ts | 1 + src/modules/students/students.service.ts | 1 - 5 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/modules/departments/departments.controller.ts b/src/modules/departments/departments.controller.ts index ca6e086..ceb4f18 100644 --- a/src/modules/departments/departments.controller.ts +++ b/src/modules/departments/departments.controller.ts @@ -63,7 +63,7 @@ export class DepartmentsController { @Delete(":id") @ApiOperation({ summary: "학과 삭제", - description: "학과 삭제", + description: "학과 삭제, 해당 학과 소속 학생들은 Cascade로 삭제됩니다.", }) @UseUserTypeGuard([UserType.ADMIN]) @ApiUnauthorizedResponse({ description: "[관리자] 로그인 후 접근 가능" }) diff --git a/src/modules/departments/departments.module.ts b/src/modules/departments/departments.module.ts index cbe56a1..32fcc41 100644 --- a/src/modules/departments/departments.module.ts +++ b/src/modules/departments/departments.module.ts @@ -1,9 +1,11 @@ import { Module } from "@nestjs/common"; import { DepartmentsService } from "./departments.service"; import { DepartmentsController } from "./departments.controller"; +import { StudentsModule } from "../students/students.module"; @Module({ controllers: [DepartmentsController], providers: [DepartmentsService], + imports: [StudentsModule], }) export class DepartmentsModule {} diff --git a/src/modules/departments/departments.service.ts b/src/modules/departments/departments.service.ts index 2cc4095..8f9fab2 100644 --- a/src/modules/departments/departments.service.ts +++ b/src/modules/departments/departments.service.ts @@ -2,10 +2,15 @@ import { PrismaService } from "src/config/database/prisma.service"; import { BadRequestException, Injectable, InternalServerErrorException } from "@nestjs/common"; import { CreateDepartmentDto } from "./dtos/create-department.dto"; import { UpdateDepartmentQuery } from "./dtos/update-department-query.dto"; +import { UserType } from "@prisma/client"; +import { StudentsService } from "../students/students.service"; @Injectable() export class DepartmentsService { - constructor(private readonly prismaService: PrismaService) {} + constructor( + private readonly prismaService: PrismaService, + private readonly studentService: StudentsService + ) {} async getAllDepartments() { const departments = await this.prismaService.department.findMany({ include: { @@ -39,12 +44,24 @@ export class DepartmentsService { async deleteDepartment(id: number) { const department = await this.prismaService.department.findUnique({ where: { id }, + include: { users: true }, }); - if (!department) { throw new BadRequestException("존재하지 않는 학과입니다."); } + // 소속 학생 삭제 + const studentIds = department.users.filter((user) => user.type === UserType.STUDENT).map((student) => student.id); + for (const studentId of studentIds) { + try { + await this.studentService.deleteStudent(studentId); + } catch (error) { + console.log(error); + throw new InternalServerErrorException("해당 학과의 학생을 삭제하는 과정에서 오류 발생"); + } + } + + // 학과 삭제 return this.prismaService.department.delete({ where: { id }, }); diff --git a/src/modules/students/students.module.ts b/src/modules/students/students.module.ts index 586f837..9fcc5a0 100644 --- a/src/modules/students/students.module.ts +++ b/src/modules/students/students.module.ts @@ -8,5 +8,6 @@ import { FilesModule } from "../files/files.module"; controllers: [StudentsController], providers: [StudentsService], imports: [AuthModule, FilesModule], + exports: [StudentsService], }) export class StudentsModule {} diff --git a/src/modules/students/students.service.ts b/src/modules/students/students.service.ts index 91de3a6..5686e4d 100644 --- a/src/modules/students/students.service.ts +++ b/src/modules/students/students.service.ts @@ -25,7 +25,6 @@ import { validate } from "class-validator"; import { UpdateThesisInfoDto } from "./dtos/update-thesis-info.dto"; import { Readable } from "stream"; import { UpdateSystemDto } from "./dtos/update-system.dto"; -import { file } from "jszip"; @Injectable() export class StudentsService { From 67747681a6b379dfb33317c60ca69ad4b1153c65 Mon Sep 17 00:00:00 2001 From: yesjuhee Date: Wed, 1 May 2024 23:48:03 +0900 Subject: [PATCH 5/7] =?UTF-8?q?fix:=20=EA=B5=90=EC=88=98=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=EC=8B=9C=20review=20SetNull=EB=A1=9C=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 --- prisma/schema.prisma | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b2b0873..f63c336 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -120,7 +120,7 @@ model Review { thesisInfoId Int? fileId String? @unique file File? @relation(fields: [fileId], references: [uuid]) - reviewer User? @relation(fields: [reviewerId], references: [id], onDelete: Cascade) + reviewer User? @relation(fields: [reviewerId], references: [id], onDelete: SetNull) thesisInfo ThesisInfo? @relation(fields: [thesisInfoId], references: [id], onDelete: Cascade) @@index([reviewerId], map: "review_reviewerId_fkey") From 559b46e6c4e3b4929b5b704d67989c79dde0d137 Mon Sep 17 00:00:00 2001 From: yesjuhee Date: Thu, 2 May 2024 21:14:12 +0900 Subject: [PATCH 6/7] =?UTF-8?q?fix:=20minIO=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EB=B6=80=EB=B6=84=20=EB=A1=A4=EB=B0=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/students/students.service.ts | 30 ------------------------ 1 file changed, 30 deletions(-) diff --git a/src/modules/students/students.service.ts b/src/modules/students/students.service.ts index 5686e4d..47fbe67 100644 --- a/src/modules/students/students.service.ts +++ b/src/modules/students/students.service.ts @@ -2041,20 +2041,6 @@ export class StudentsService { async deleteStudentsList() { // 논문 파일, 서명 파일 가져오기 - const files = await this.prismaService.file.findMany({ - where: {}, - include: { review: true, thesisFile: true, professor: true }, - }); - const fileUUIDs = files.filter((file) => file.thesisFile !== null || file.review !== null).map((file) => file.uuid); // 모든 논문 파일과 리뷰 파일 - // minIO, file 테이블에서 삭제 (FileService 이용) - for (const uuid of fileUUIDs) { - try { - await this.fileService.deleteFile(uuid); - } catch (error) { - console.log(error); - throw new InternalServerErrorException("파일 삭제 오류 발생"); - } - } // 학생 데이터 모두 삭제 (Hard delete) // cascade로 삭제되는 연관된 데이터들 : Achivements, Process, Reviewers, ThesisInfos, Reviews, ThesisFiles @@ -2076,27 +2062,11 @@ export class StudentsService { type: UserType.STUDENT, deletedAt: null, }, - include: { studentProcess: { include: { thesisInfos: { include: { thesisFiles: true, reviews: true } } } } }, }); if (!student) { throw new BadRequestException("해당하는 학생을 찾을 수 없습니다."); } - // 파일 데이터 확인 -> 미리 삭제 (Cascade로 삭제 안됨) - const thesisFiles = student.studentProcess.thesisInfos.flatMap((thesisInfo) => thesisInfo.thesisFiles); - const reviews = student.studentProcess.thesisInfos.flatMap((thesisInfo) => thesisInfo.reviews); - const thesisFileUUIDs = thesisFiles.map((thesisFile) => thesisFile.fileId).filter((uuid) => uuid !== null); // 학생 논문, 논문 발표, 수정 지시사항 관련 파일 uuid - const reviewFileUUIDs = reviews.map((review) => review.fileId).filter((uuid) => uuid !== null); // 교수 논문 심사 관련 파일 uuid - const fileUUIDs = [...thesisFileUUIDs, ...reviewFileUUIDs]; // 학생과 관련 있는 모든 파일 uuid - // minIO, file 테이블에서 삭제 (FileService 이용) - for (const uuid of fileUUIDs) { - try { - await this.fileService.deleteFile(uuid); - } catch (error) { - console.log(error); - throw new InternalServerErrorException("파일 삭제 오류 발생"); - } - } // 학생 데이터 모두 삭제 (Hard delete) // cascade로 삭제되는 연관된 데이터들 : Achivements, Process, Reviewers, ThesisInfos, Reviews, ThesisFiles try { From 117bcc031af4ab126752ee9144a9d163ace06bc0 Mon Sep 17 00:00:00 2001 From: yesjuhee Date: Thu, 2 May 2024 21:16:18 +0900 Subject: [PATCH 7/7] =?UTF-8?q?fix:=20StudentModule=20FilesModuls=20DI=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/students/students.module.ts | 4 +--- src/modules/students/students.service.ts | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/modules/students/students.module.ts b/src/modules/students/students.module.ts index 9fcc5a0..3f4fef6 100644 --- a/src/modules/students/students.module.ts +++ b/src/modules/students/students.module.ts @@ -2,12 +2,10 @@ import { Module } from "@nestjs/common"; import { StudentsController } from "./students.controller"; import { StudentsService } from "./students.service"; import { AuthModule } from "../auth/auth.module"; -import { FilesModule } from "../files/files.module"; - @Module({ controllers: [StudentsController], providers: [StudentsService], - imports: [AuthModule, FilesModule], + imports: [AuthModule], exports: [StudentsService], }) export class StudentsModule {} diff --git a/src/modules/students/students.service.ts b/src/modules/students/students.service.ts index 47fbe67..89f5573 100644 --- a/src/modules/students/students.service.ts +++ b/src/modules/students/students.service.ts @@ -1,4 +1,3 @@ -import { FilesService } from "./../files/files.service"; import { ReviewerRoleQuery, UpdateReviewerQueryDto } from "./dtos/update-reviewer-query-dto"; import { ThesisInfoQueryDto, ThesisQueryType } from "./dtos/thesis-info-query.dto"; import { @@ -30,8 +29,7 @@ import { UpdateSystemDto } from "./dtos/update-system.dto"; export class StudentsService { constructor( private readonly prismaService: PrismaService, - private readonly authService: AuthService, - private readonly fileService: FilesService + private readonly authService: AuthService ) {} // 학생 생성 API