From cda0401735b20aeba477beada5fb4d010c3d0620 Mon Sep 17 00:00:00 2001 From: yesjuhee Date: Fri, 18 Oct 2024 15:23:11 +0900 Subject: [PATCH] =?UTF-8?q?[FEAT]=20=EB=B0=95=EC=82=AC=EA=B3=BC=EC=A0=95?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=20(#150)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: user type PHD 추가 * feat: createPhD API 구현 * fix: 학생 엑셀 업로드 양식에 박사과정 학생 등록 방식 설명 추가 * feat: createPhDExcel API 구현 * feat: PHD UserType에 일부 API 권한 부여 * feat: 연구실적 서비스 로직에 PHD UserType 추가 * fix: 연구실적 DB 구조 수정 * fix: 연구실적 등록 시 지도교수 최대 2명 설정 가능 * fix: 연구실적 엑셀 다운로드 시 학위과정, 지도교수 이름 포함 --- prisma/schema.prisma | 47 +++-- ...353\241\235_\354\226\221\354\213\235.xlsx" | Bin 10258 -> 8345 bytes src/common/enums/user-type.enum.ts | 1 + .../achievements/achievements.controller.ts | 10 +- .../achievements/achievements.service.ts | 33 ++- .../dtos/create-achievements.dto.ts | 22 +- src/modules/students/dtos/create-phd.dto.ts | 43 ++++ src/modules/students/dtos/phd.dto.ts | 30 +++ src/modules/students/students.controller.ts | 69 ++++++- src/modules/students/students.service.ts | 191 ++++++++++++++++++ src/modules/users/users.controller.ts | 4 +- 11 files changed, 415 insertions(+), 35 deletions(-) create mode 100644 src/modules/students/dtos/create-phd.dto.ts create mode 100644 src/modules/students/dtos/phd.dto.ts diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e687efd..f8ce7e6 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -9,25 +9,27 @@ datasource db { /// 사용자 model User { - id Int @id @default(autoincrement()) - loginId String @unique @db.VarChar(255) - password String @db.VarChar(255) - name String @db.VarChar(255) - email String? @unique @db.VarChar(255) - phone String? @db.VarChar(255) - type UserType - createdAt DateTime @default(now()) - updatedAt DateTime @default(now()) @updatedAt - deletedAt DateTime? - deptId Int? - signId String? @unique - headReviewProcesses Process[] @relation("headReviewer") - studentProcess Process? @relation("student") - reviews Review[] - reviewers Reviewer[] - department Department? @relation(fields: [deptId], references: [id], onDelete: SetNull) - signFile File? @relation(fields: [signId], references: [uuid]) - Achievements Achievements[] + id Int @id @default(autoincrement()) + loginId String @unique @db.VarChar(255) + password String @db.VarChar(255) + name String @db.VarChar(255) + email String? @unique @db.VarChar(255) + phone String? @db.VarChar(255) + type UserType + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + deletedAt DateTime? + deptId Int? + signId String? @unique + headReviewProcesses Process[] @relation("headReviewer") + studentProcess Process? @relation("student") + reviews Review[] + reviewers Reviewer[] + department Department? @relation(fields: [deptId], references: [id], onDelete: SetNull) + signFile File? @relation(fields: [signId], references: [uuid]) + studentAchivements Achievements[] @relation("student") + profesosr1Achivements Achievements[] @relation("professor1") + profesor2Achivements Achievements[] @relation("professor2") @@index([deptId], map: "user_deptId_fkey") @@map("user") @@ -167,7 +169,11 @@ model Achievements { authorType AuthorType authorNumbers Int userId Int? - User User? @relation(fields: [userId], references: [id], onDelete: Cascade) + professorId1 Int? + professorId2 Int? + User User? @relation("student", fields: [userId], references: [id], onDelete: Cascade) + Professor1 User? @relation("professor1", fields: [professorId1], references: [id], onDelete: SetNull) + Professor2 User? @relation("professor2", fields: [professorId2], references: [id], onDelete: SetNull) @@index([userId], map: "achievements_userId_fkey") @@map("achievements") @@ -177,6 +183,7 @@ enum UserType { STUDENT PROFESSOR ADMIN + PHD } enum Stage { diff --git "a/resources/excel/\354\227\260\352\265\254\353\205\274\353\254\270\354\236\221\355\222\210\354\213\234\354\212\244\355\205\234_\355\225\231\354\203\235_\354\235\274\352\264\204\353\223\261\353\241\235_\354\226\221\354\213\235.xlsx" "b/resources/excel/\354\227\260\352\265\254\353\205\274\353\254\270\354\236\221\355\222\210\354\213\234\354\212\244\355\205\234_\355\225\231\354\203\235_\354\235\274\352\264\204\353\223\261\353\241\235_\354\226\221\354\213\235.xlsx" index e6d011392dc7e52252cc8eb9169e843961fc6320..c64d3bdce1d6f01d098ed7025ba9551402d32e1b 100644 GIT binary patch literal 8345 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 user.id); + const missingIds = professorIds.filter((id) => !foundIds.includes(id)); + if (missingIds.length !== 0) throw new BadRequestException(`ID:[${missingIds}]에 해당하는 교수가 없습니다.`); + } + return await this.prismaService.achievements.create({ data: { userId, @@ -33,6 +47,8 @@ export class AchievementsService { publicationDate, authorNumbers, authorType, + professorId1: professorIds ? professorIds[0] : undefined, + professorId2: professorIds && professorIds.length == 2 ? professorIds[1] : undefined, }, }); } @@ -44,7 +60,7 @@ export class AchievementsService { }, }); if (!foundUser) throw new BadRequestException("해당 논문실적은 존재하지 않습니다."); - if (user.type === UserType.STUDENT && foundUser.userId != user.id) + if ((user.type === UserType.STUDENT || user.type === UserType.PHD) && foundUser.userId != user.id) throw new BadRequestException("다른 학생의 논문실적은 수정할수 없습니다."); const { performance, paperTitle, journalName, ISSN, publicationDate, authorType, authorNumbers } = updateAchievementDto; @@ -69,7 +85,7 @@ export class AchievementsService { } async getAchievements(currentUser: User, achievementsQuery: AchievementsSearchQuery) { - if (currentUser.type == UserType.STUDENT) { + if (currentUser.type === UserType.STUDENT || currentUser.type === UserType.PHD) { const studentQuery = { where: { userId: currentUser.id, @@ -147,6 +163,8 @@ export class AchievementsService { department: true, }, }, + Professor1: true, + Professor2: true, }, }); if (!achievements) throw new BadRequestException("검색된 논문 실적이 없습니다."); @@ -158,6 +176,9 @@ export class AchievementsService { record["학번"] = student.loginId; record["이름"] = student.name; record["학과"] = dept.name; + record["학위과정"] = achievement.User.type === UserType.STUDENT ? "석사" : "박사"; + record["지도교수1"] = achievement.Professor1 ? achievement.Professor1.name : null; + record["지도교수2"] = achievement.Professor2 ? achievement.Professor2.name : null; record["실적 구분"] = this.PerformanceToFullname(achievement.performance); record["학술지 또는 학술대회명"] = achievement.journalName; @@ -236,7 +257,7 @@ export class AchievementsService { }, }); if (!achievement) throw new BadRequestException("해당 id의 논문실적이 존재하지 않습니다."); - if (user.type === UserType.STUDENT && achievement.userId !== user.id) + if ((user.type === UserType.STUDENT || user.type === UserType.PHD) && achievement.userId !== user.id) throw new UnauthorizedException("학생의 경우 본인 논문 실적만 조회가 가능합니다."); return achievement; } @@ -248,7 +269,7 @@ export class AchievementsService { }, }); if (!achievement) throw new BadRequestException("해당 id의 논문실적이 존재하지 않습니다."); - if (user.type === UserType.STUDENT && achievement.userId !== user.id) + if ((user.type === UserType.STUDENT || user.type === UserType.PHD) && achievement.userId !== user.id) throw new UnauthorizedException("학생의 경우 본인 논문 실적만 조회가 가능합니다."); try { await this.prismaService.achievements.delete({ diff --git a/src/modules/achievements/dtos/create-achievements.dto.ts b/src/modules/achievements/dtos/create-achievements.dto.ts index 327f1b9..3d13249 100644 --- a/src/modules/achievements/dtos/create-achievements.dto.ts +++ b/src/modules/achievements/dtos/create-achievements.dto.ts @@ -1,4 +1,15 @@ -import { IsDate, IsEnum, IsInt, IsNotEmpty, IsOptional, IsPositive, IsString } from "class-validator"; +import { + ArrayMaxSize, + ArrayMinSize, + IsArray, + IsDate, + IsEnum, + IsInt, + IsNotEmpty, + IsOptional, + IsPositive, + IsString, +} from "class-validator"; import { Performance } from "../../../common/enums/performance.enum"; import { Author } from "../../../common/enums/author.enum"; import { ApiProperty } from "@nestjs/swagger"; @@ -69,4 +80,13 @@ export class CreateAchievementsDto { @IsInt() @IsPositive() authorNumbers: number; + + @ApiProperty({ description: "지도교수 아이디 리스트", type: [Number], example: "[3, 4]" }) + @IsArray() + @Type(() => Number) + @IsInt({ each: true }) + @IsPositive({ each: true }) + @ArrayMinSize(0) + @ArrayMaxSize(2) + professorIds: number[]; } diff --git a/src/modules/students/dtos/create-phd.dto.ts b/src/modules/students/dtos/create-phd.dto.ts new file mode 100644 index 0000000..3469c2d --- /dev/null +++ b/src/modules/students/dtos/create-phd.dto.ts @@ -0,0 +1,43 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { Type } from "class-transformer"; +import { IsEmail, IsInt, IsNotEmpty, IsOptional, IsPositive, IsString } from "class-validator"; +import { IsKoreanPhoneNumber } from "src/common/decorators/is-kr-phone-number.decorator"; + +export class CreatePhDDto { + // 사용자 정보 + @ApiProperty({ description: "로그인 아이디(학번)", example: "2022000000" }) + @IsNotEmpty() + @IsString() + loginId: string; + + @ApiProperty({ description: "비밀번호(생년월일)", example: "010101" }) + @IsNotEmpty() + @IsString() + password: string; + + @ApiProperty({ description: "학생 이름", example: "홍길동" }) + @IsNotEmpty() + @IsString() + name: string; + + @ApiProperty({ description: "학생 이메일", example: "abc@gmail.com", required: false }) + @IsOptional() + @IsNotEmpty() + @IsString() + @IsEmail() + email: string; + + @ApiProperty({ description: "학생 전화번호", example: "010-1111-1222", required: false }) + @IsOptional() + @IsNotEmpty() + @IsString() + @IsKoreanPhoneNumber() + phone: string; + + @ApiProperty({ description: "학과 아이디", example: "1" }) + @IsNotEmpty() + @Type(() => Number) + @IsPositive() + @IsInt() + deptId: number; +} diff --git a/src/modules/students/dtos/phd.dto.ts b/src/modules/students/dtos/phd.dto.ts new file mode 100644 index 0000000..c21b119 --- /dev/null +++ b/src/modules/students/dtos/phd.dto.ts @@ -0,0 +1,30 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { Department, User, UserType } from "@prisma/client"; +import { DepartmentDto } from "src/modules/departments/dtos/department.dto"; + +export class PhDDto { + constructor(phdData: Partial & { department: Department }) { + this.id = phdData.id; + this.loginId = phdData.loginId; + this.name = phdData.name; + this.email = phdData.email; + this.phone = phdData.phone; + this.type = phdData.type; + this.department = phdData.department ? new DepartmentDto(phdData.department) : undefined; + } + + @ApiProperty({ description: "아이디" }) + id: number; + @ApiProperty({ description: "로그인 아이디" }) + loginId: string; + @ApiProperty({ description: "이름" }) + name: string; + @ApiProperty({ description: "이메일" }) + email?: string; + @ApiProperty({ description: "연락처" }) + phone?: string; + @ApiProperty({ description: "사용자 유형", enum: UserType, example: UserType.PHD }) + type: UserType; + @ApiProperty({ description: "학과" }) + department: DepartmentDto; +} diff --git a/src/modules/students/students.controller.ts b/src/modules/students/students.controller.ts index 7a4d709..61e81d2 100644 --- a/src/modules/students/students.controller.ts +++ b/src/modules/students/students.controller.ts @@ -1,3 +1,4 @@ +import { CreatePhDDto } from "./dtos/create-phd.dto"; import { StudentsService } from "./students.service"; import { Body, @@ -50,11 +51,12 @@ import { ReviewersDto } from "./dtos/reviewers.dto"; import { FileInterceptor } from "@nestjs/platform-express"; import { ExcelFilter } from "src/common/pipes/excel.filter"; import { UpdateReviewerQueryDto } from "./dtos/update-reviewer-query-dto"; +import { PhDDto } from "./dtos/phd.dto"; @Controller("students") @UseGuards(JwtGuard) @ApiTags("학생 API") -@ApiExtraModels(UserDto, CommonResponseDto, SystemDto, ThesisInfoDto, ReviewersDto) +@ApiExtraModels(UserDto, CommonResponseDto, SystemDto, ThesisInfoDto, ReviewersDto, PhDDto) @ApiInternalServerErrorResponse({ description: "서버 내부 오류" }) @ApiBearerAuth("access-token") export class StudentsController { @@ -82,6 +84,26 @@ export class StudentsController { return new CommonResponseDto(userDto); } + @Post("/phd") + @UseUserTypeGuard([UserType.ADMIN]) + @ApiOperation({ + summary: "박사과정 학생 생성 API", + description: "박사과정 학생의 기본 회원정보를 받아서 생성한다. 학생의 회원 가입 역할을 한다.", + }) + @ApiUnauthorizedResponse({ description: "[관리자] 로그인 후 접근 가능" }) + @ApiBadRequestResponse({ description: "요청 양식 오류" }) + @ApiCreatedResponse({ + description: "박사과정 학생 생성 성공", + schema: { + allOf: [{ $ref: getSchemaPath(CommonResponseDto) }, { $ref: getSchemaPath(PhDDto) }], + }, + }) + async createPhD(@Body() createPhdDto: CreatePhDDto) { + const phd = await this.studentsService.createPhD(createPhdDto); + const userDto = new PhDDto(phd); + return new CommonResponseDto(userDto); + } + @Post("/excel") @UseUserTypeGuard([UserType.ADMIN]) @UseInterceptors(FileInterceptor("file", { fileFilter: ExcelFilter })) @@ -127,6 +149,51 @@ export class StudentsController { return new CommonResponseDto(pageDto); } + @Post("/excel/phd") + @UseUserTypeGuard([UserType.ADMIN]) + @UseInterceptors(FileInterceptor("file", { fileFilter: ExcelFilter })) + @ApiConsumes("multipart/form-data") + @ApiBody({ + schema: { + type: "object", + properties: { + file: { + type: "string", + format: "binary", + }, + }, + }, + }) + @ApiOperation({ + summary: "박사과정 학생 엑셀 업로드 API", + description: "엑셀을 업로드하여 박사과정 학생을 생성한다. 학번 기준 기존 학생인 경우 업데이트를 진행한다.", + }) + @ApiUnauthorizedResponse({ description: "[관리자] 로그인 후 접근 가능" }) + @ApiBadRequestResponse({ description: "엑셀 양식 오류" }) + @ApiCreatedResponse({ + description: "생성 및 업데이트 성공", + schema: { + allOf: [ + { $ref: getSchemaPath(CommonResponseDto) }, + { $ref: getSchemaPath(PageDto) }, + { + properties: { + content: { + type: "array", + items: { $ref: getSchemaPath(PhDDto) }, + }, + }, + }, + ], + }, + }) + async createPhDExcel(@UploadedFile() excelFile: Express.Multer.File) { + const students = await this.studentsService.createPhDExcel(excelFile); + const contents = students.map((student) => new PhDDto(student)); + const pageDto = new PageDto(0, 0, contents.length, contents); + return new CommonResponseDto(pageDto); + } + // 학생 대량 조회 API @Get("/excel") @UseUserTypeGuard([UserType.ADMIN]) diff --git a/src/modules/students/students.service.ts b/src/modules/students/students.service.ts index 62b7a64..fe75832 100644 --- a/src/modules/students/students.service.ts +++ b/src/modules/students/students.service.ts @@ -24,6 +24,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 { CreatePhDDto } from "./dtos/create-phd.dto"; @Injectable() export class StudentsService { @@ -244,6 +245,56 @@ export class StudentsService { } } + async createPhD(createPhDDto: CreatePhDDto) { + const { loginId, password, name, email, phone, deptId } = createPhDDto; + + // 로그인 아이디, 이메일 존재 여부 확인 + const foundId = await this.prismaService.user.findUnique({ + where: { + loginId, + deletedAt: null, + }, + }); + if (foundId) + throw new BadRequestException("이미 존재하는 아이디입니다. 기존 학생 수정은 학생 수정 페이지를 이용해주세요."); + if (email) { + const foundEmail = await this.prismaService.user.findUnique({ + where: { + email, + deletedAt: null, + }, + }); + if (foundEmail) throw new BadRequestException("이미 존재하는 이메일입니다."); + } + + // deptId 올바른지 확인 + const foundDept = await this.prismaService.department.findUnique({ + where: { id: deptId }, + }); + if (!foundDept) throw new BadRequestException("해당하는 학과가 없습니다."); + + try { + return await this.prismaService.$transaction(async (tx) => { + // 사용자(user) 생성 + return await tx.user.create({ + data: { + deptId, + loginId, + email, + password: this.authService.createHash(password), + name, + phone, + type: UserType.PHD, + }, + include: { department: true }, + }); + }); + } catch (error) { + console.log(error); + throw new InternalServerErrorException("학생 생성 실패"); + } + } + async createStudentExcel(excelFile: Express.Multer.File) { if (!excelFile) throw new BadRequestException("파일을 업로드해주세요."); const workBook = XLSX.read(excelFile.buffer, { type: "buffer" }); @@ -1035,6 +1086,146 @@ export class StudentsService { ); } + async createPhDExcel(excelFile: Express.Multer.File) { + if (!excelFile) throw new BadRequestException("파일을 업로드해주세요."); + const workBook = XLSX.read(excelFile.buffer, { type: "buffer" }); + const sheetName = workBook.SheetNames[0]; + const sheet = workBook.Sheets[sheetName]; + const studentRecords = XLSX.utils.sheet_to_json(sheet); + + return await this.prismaService.$transaction( + async (tx) => { + const students = []; + for (const [index, studentRecord] of studentRecords.entries()) { + try { + // 모든 항목 값 받아오기 + const studentNumber = studentRecord["학번"]?.toString(); + const password = studentRecord["비밀번호"]?.toString(); + const name = studentRecord["이름"]; + const email = studentRecord["이메일"]; + const phone = studentRecord["연락처"]?.toString(); + const major = studentRecord["학과"]; + + // 학과 찾기 + let deptId: number, foundDept: Department; + if (major) { + foundDept = await this.prismaService.department.findFirst({ + where: { name: major }, + }); + if (!foundDept) throw new BadRequestException(`${index + 2} 행의 학과 명을 확인해주세요.`); + deptId = foundDept.id; + } + + // 학번으로 기존 학생 여부 판단 + if (!studentNumber) throw new BadRequestException(`${index + 2}번째 행의 [학번] 항목을 다시 확인해주세요.`); + const foundStudent = await this.prismaService.user.findUnique({ + where: { + loginId: studentNumber, + type: UserType.PHD, + deletedAt: null, + }, + }); + + // 학생 업데이트 + if (foundStudent) { + // 학생 기본 정보 업데이트 (학번, 비밀번호, 이름, 이메일, 연락처, 학과) + const updateStudentDto = new UpdateStudentDto(); + updateStudentDto.password = password; + updateStudentDto.name = name; + updateStudentDto.email = email; + updateStudentDto.phone = phone; + updateStudentDto.deptId = deptId; + // validation + const validationErrors = await validate(updateStudentDto); + if (validationErrors.length > 0) { + validationErrors.map((error) => console.log(error.constraints)); + throw new BadRequestException(`${index + 2} 행의 데이터 입력 형식이 잘못되었습니다.`); + } + + // 이메일 중복 여부 확인 + if (updateStudentDto.email) { + const foundEmail = await this.prismaService.user.findUnique({ + where: { email: updateStudentDto.email, deletedAt: null }, + }); + if (foundEmail && foundEmail.id !== foundStudent.id) + throw new BadRequestException(`${index + 2}행 : 이미 존재하는 이메일로는 변경할 수 없습니다.`); + } + + // 업데이트 + const updatePhD = await tx.user.update({ + where: { id: foundStudent.id, deletedAt: null }, + data: { + loginId: updateStudentDto.loginId, + password: updateStudentDto.password + ? this.authService.createHash(updateStudentDto.password) + : undefined, + name: updateStudentDto.name, + email: updateStudentDto.email, + phone: updateStudentDto.phone, + deptId: updateStudentDto.deptId, + }, + include: { department: true }, + }); + + students.push(updatePhD); + } + // 신규 학생 생성 + else { + const createPhDDto = new CreatePhDDto(); + createPhDDto.loginId = studentNumber; + createPhDDto.password = password; + createPhDDto.name = name; + createPhDDto.email = email; + createPhDDto.phone = phone; + createPhDDto.deptId = deptId; + + // DTO 이용한 validation + const validationErrors = await validate(createPhDDto); + if (validationErrors.length > 0) { + validationErrors.map((error) => console.log(error.constraints)); + throw new BadRequestException(`${index + 2} 행의 데이터 입력 형식이 잘못되었습니다.`); + } + + // 이메일 존재 여부 확인 + if (createPhDDto.email) { + const foundEmail = await this.prismaService.user.findUnique({ + where: { email: createPhDDto.email }, + }); + if (foundEmail) throw new BadRequestException(`${index + 2}행 : 이미 존재하는 이메일입니다.`); + } + + // 신규 학생 생성 + const newPhD = await tx.user.create({ + data: { + deptId: createPhDDto.deptId, + loginId: createPhDDto.loginId, + email: createPhDDto.email, + password: this.authService.createHash(createPhDDto.password), + name: createPhDDto.name, + phone: createPhDDto.phone, + type: UserType.PHD, + }, + include: { department: true }, + }); + + students.push(newPhD); + } + } catch (error) { + console.log(error); + if (error.status === 400) { + throw new BadRequestException(error.message); + } + throw new InternalServerErrorException(`${index + 2} 행에서 에러 발생`); + } + } + return students; + }, + { + timeout: 4000, + } + ); + } + // 학생 대량 조회 API async getStudentList(studentSearchPageQuery: StudentSearchPageQuery) { const { studentNumber, name, email, phone, departmentId } = studentSearchPageQuery; diff --git a/src/modules/users/users.controller.ts b/src/modules/users/users.controller.ts index 10f9e6b..d13c771 100644 --- a/src/modules/users/users.controller.ts +++ b/src/modules/users/users.controller.ts @@ -29,7 +29,7 @@ export class UsersController { constructor(private readonly usersService: UsersService) {} @Get("/me") - @UseUserTypeGuard([UserType.ADMIN, UserType.STUDENT, UserType.PROFESSOR]) + @UseUserTypeGuard([UserType.ADMIN, UserType.STUDENT, UserType.PROFESSOR, UserType.PHD]) @ApiOperation({ summary: "로그인 유저 정보 조회 API", description: "로그인된 유저의 회원 정보를 조회할 수 있다.", @@ -46,7 +46,7 @@ export class UsersController { } @Put("/me") - @UseUserTypeGuard([UserType.ADMIN, UserType.PROFESSOR, UserType.STUDENT]) + @UseUserTypeGuard([UserType.ADMIN, UserType.PROFESSOR, UserType.STUDENT, UserType.PHD]) @ApiOperation({ summary: "로그인 유저 정보 수정 API", description: "로그인된 유저의 회원 정보 중 '비밀번호', '연락처', '이메일' 필드를 수정할 수 있다.",