diff --git a/bitgouel-api/src/main/kotlin/team/msg/domain/admin/presentation/AdminController.kt b/bitgouel-api/src/main/kotlin/team/msg/domain/admin/presentation/AdminController.kt index f5e40bf36..ce3e453ca 100644 --- a/bitgouel-api/src/main/kotlin/team/msg/domain/admin/presentation/AdminController.kt +++ b/bitgouel-api/src/main/kotlin/team/msg/domain/admin/presentation/AdminController.kt @@ -1,5 +1,6 @@ package team.msg.domain.admin.presentation +import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* import org.springframework.web.multipart.MultipartFile @@ -40,9 +41,15 @@ class AdminController( return ResponseEntity.noContent().build() } - @PostMapping("/excel") + @PostMapping("/student/excel") fun uploadStudentListExcel(@RequestPart file: MultipartFile): ResponseEntity { adminService.uploadStudentListExcel(file) - return ResponseEntity.ok().build() + return ResponseEntity.status(HttpStatus.CREATED).build() + } + + @PostMapping("/club/excel") + fun uploadClubListExcel(@RequestPart file: MultipartFile): ResponseEntity { + adminService.uploadClubListExcel(file) + return ResponseEntity.status(HttpStatus.CREATED).build() } } \ No newline at end of file diff --git a/bitgouel-api/src/main/kotlin/team/msg/domain/admin/service/AdminService.kt b/bitgouel-api/src/main/kotlin/team/msg/domain/admin/service/AdminService.kt index 4b1ea995b..2caadeb82 100644 --- a/bitgouel-api/src/main/kotlin/team/msg/domain/admin/service/AdminService.kt +++ b/bitgouel-api/src/main/kotlin/team/msg/domain/admin/service/AdminService.kt @@ -11,4 +11,5 @@ interface AdminService { fun rejectUsers(userIds: List) fun forceWithdraw(userIds: List) fun uploadStudentListExcel(file: MultipartFile) + fun uploadClubListExcel(file: MultipartFile) } \ No newline at end of file diff --git a/bitgouel-api/src/main/kotlin/team/msg/domain/admin/service/AdminServiceImpl.kt b/bitgouel-api/src/main/kotlin/team/msg/domain/admin/service/AdminServiceImpl.kt index 5c9c19529..70337bd2a 100644 --- a/bitgouel-api/src/main/kotlin/team/msg/domain/admin/service/AdminServiceImpl.kt +++ b/bitgouel-api/src/main/kotlin/team/msg/domain/admin/service/AdminServiceImpl.kt @@ -5,13 +5,18 @@ import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import org.springframework.web.multipart.MultipartFile import team.msg.common.enums.ApproveStatus +import team.msg.common.enums.Field import team.msg.common.util.StudentUtil import team.msg.common.util.UserUtil import team.msg.domain.admin.exception.InvalidCellTypeException import team.msg.domain.admin.presentation.data.request.QueryUsersRequest +import team.msg.domain.club.exception.AlreadyExistClubException import team.msg.domain.club.exception.ClubNotFoundException +import team.msg.domain.club.exception.InvalidFieldException import team.msg.domain.club.model.Club import team.msg.domain.club.repository.ClubRepository +import team.msg.domain.school.exception.SchoolNotFoundException +import team.msg.domain.school.repository.SchoolRepository import team.msg.domain.student.repository.StudentRepository import team.msg.domain.user.enums.Authority import team.msg.domain.user.exception.InvalidEmailException @@ -31,7 +36,8 @@ class AdminServiceImpl( private val userUtil: UserUtil, private val studentUtil: StudentUtil, private val clubRepository: ClubRepository, - private val studentRepository: StudentRepository + private val studentRepository: StudentRepository, + private val schoolRepository: SchoolRepository ) : AdminService { /** @@ -122,7 +128,7 @@ class AdminServiceImpl( } catch (e: IndexOutOfBoundsException) { throw InvalidCellTypeException("전화번호 셀 서식을 텍스트로 바꿔주세요.") } catch (e: Exception) { - throw InternalServerException("엑셀 파일 처리 중 문제가 발생했습니다. info : [ errorMessage = ${e.message}") + throw InternalServerException("엑셀 파일 처리 중 문제가 발생했습니다. info : [ errorMessage = ${e.message} ]") } val sheet = workbook.getSheetAt(0) @@ -156,6 +162,58 @@ class AdminServiceImpl( } } + /** + * 동아리 리스트 엑셀을 업로드 하는 비지니스 로직입니다 + * @param 동아리 리스트 엑셀 업로드를 위한 MultipartFile + */ + @Transactional(rollbackFor = [Exception::class]) + override fun uploadClubListExcel(file: MultipartFile) { + file.inputStream.use { + val workbook = try { + WorkbookFactory.create(file.inputStream) + } catch (e: Exception) { + throw InternalServerException("엑셀 파일 처리 중 문제가 발생했습니다. info : [ errorMessage = ${e.message} ]") + } + + val sheet = workbook.getSheetAt(0) + + sheet.forEachIndexed { index, row -> + if (index == 0) + return@forEachIndexed + + if (row.getCell(0).stringCellValue == "") + return + + val schoolName = row.getCell(0).stringCellValue + val clubName = row.getCell(1).stringCellValue + val field = row.getCell(2).stringCellValue + + val school = schoolRepository.findByName(schoolName) + ?: throw SchoolNotFoundException("존재하지 않는 학교입니다. info : [ schoolName = $schoolName ]") + + if (clubRepository.existsByName(clubName)) { + throw AlreadyExistClubException("이미 존재하는 동아리입니다. info : [ clubName = $clubName ]") + } + + val clubField = when (field) { + FUTURISTIC_TRANSPORTATION_EQUIPMENT -> Field.FUTURISTIC_TRANSPORTATION_EQUIPMENT + ENERGY -> Field.ENERGY + MEDICAL_HEALTHCARE -> Field.MEDICAL_HEALTHCARE + AI_CONVERGENCE -> Field.AI_CONVERGENCE + CULTURE -> Field.CULTURE + else -> throw InvalidFieldException("유효하지 않은 동아리 분야입니다. info : [ clubField = $field ]") + } + + val club = Club( + school = school, + name = clubName, + field = clubField + ) + clubRepository.save(club) + } + } + } + private fun validateExcelStudentData(email: String, phoneNumber: String, password: String) { val emailRegex = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}\$".toRegex() if (!email.matches(emailRegex)) @@ -173,4 +231,12 @@ class AdminServiceImpl( private infix fun ClubRepository.findByName(clubName: String): Club = this.findByName(clubName) ?: throw ClubNotFoundException("존재하지 않는 동아리입니다. info : [ clubName = $clubName ]") + + companion object { + const val FUTURISTIC_TRANSPORTATION_EQUIPMENT = "미래형 운송기기" + const val ENERGY = "에너지 산업" + const val MEDICAL_HEALTHCARE = "의료 헬스케어" + const val AI_CONVERGENCE = "AI 융복합" + const val CULTURE = "문화산업" + } } \ No newline at end of file diff --git a/bitgouel-api/src/main/kotlin/team/msg/domain/certification/service/CertificationServiceImpl.kt b/bitgouel-api/src/main/kotlin/team/msg/domain/certification/service/CertificationServiceImpl.kt index d70fd53a9..45962c8bf 100644 --- a/bitgouel-api/src/main/kotlin/team/msg/domain/certification/service/CertificationServiceImpl.kt +++ b/bitgouel-api/src/main/kotlin/team/msg/domain/certification/service/CertificationServiceImpl.kt @@ -118,7 +118,7 @@ class CertificationServiceImpl( val student = studentRepository findStudentById studentId if (student.club != club && club != null) - throw ForbiddenCertificationException("자격증을 조회할 권한이 없습니다. info : [ club = $club ]") + throw ForbiddenCertificationException("자격증을 조회할 권한이 없습니다. info : [ club = ${club.name} ]") val certifications = certificationRepository findAllByStudentIdOrderByAcquisitionDateDesc studentId diff --git a/bitgouel-api/src/main/kotlin/team/msg/domain/club/exception/InvalidFieldException.kt b/bitgouel-api/src/main/kotlin/team/msg/domain/club/exception/InvalidFieldException.kt new file mode 100644 index 000000000..bb819947d --- /dev/null +++ b/bitgouel-api/src/main/kotlin/team/msg/domain/club/exception/InvalidFieldException.kt @@ -0,0 +1,8 @@ +package team.msg.domain.club.exception + +import team.msg.domain.club.exception.constant.ClubErrorCode +import team.msg.global.error.exception.BitgouelException + +class InvalidFieldException( + message: String +) : BitgouelException(message, ClubErrorCode.INVALID_FIELD.status) \ No newline at end of file diff --git a/bitgouel-api/src/main/kotlin/team/msg/domain/club/exception/constant/ClubErrorCode.kt b/bitgouel-api/src/main/kotlin/team/msg/domain/club/exception/constant/ClubErrorCode.kt index 6e668b646..a00047f86 100644 --- a/bitgouel-api/src/main/kotlin/team/msg/domain/club/exception/constant/ClubErrorCode.kt +++ b/bitgouel-api/src/main/kotlin/team/msg/domain/club/exception/constant/ClubErrorCode.kt @@ -4,6 +4,7 @@ enum class ClubErrorCode( val status: Int ) { NOT_EMPTY_CLUB(400), + INVALID_FIELD(400), CLUB_NOT_FOUND(404), ALREADY_EXIST_CLUB(409) } \ No newline at end of file diff --git a/bitgouel-api/src/main/kotlin/team/msg/domain/inquiry/service/InquiryServiceImpl.kt b/bitgouel-api/src/main/kotlin/team/msg/domain/inquiry/service/InquiryServiceImpl.kt index 4361ba2c6..c54c317a9 100644 --- a/bitgouel-api/src/main/kotlin/team/msg/domain/inquiry/service/InquiryServiceImpl.kt +++ b/bitgouel-api/src/main/kotlin/team/msg/domain/inquiry/service/InquiryServiceImpl.kt @@ -116,7 +116,7 @@ class InquiryServiceImpl( throw ForbiddenCommandInquiryException("문의사항을 삭제할 권한이 없습니다. info : [ userId = ${currentUser.id}, inquiryId = $id ]") if(inquiry.answerStatus == AnswerStatus.ANSWERED) { - val inquiryAnswer = inquiryAnswerRepository findByInquiryId id + val inquiryAnswer = inquiryAnswerRepository findByInquiryId id inquiryAnswerRepository.delete(inquiryAnswer) } diff --git a/bitgouel-api/src/main/kotlin/team/msg/global/security/SecurityConfig.kt b/bitgouel-api/src/main/kotlin/team/msg/global/security/SecurityConfig.kt index c7ce9fd5c..e9a1fd71f 100644 --- a/bitgouel-api/src/main/kotlin/team/msg/global/security/SecurityConfig.kt +++ b/bitgouel-api/src/main/kotlin/team/msg/global/security/SecurityConfig.kt @@ -133,7 +133,8 @@ class SecurityConfig( .mvcMatchers(HttpMethod.DELETE, "/admin/reject").hasRole(ADMIN) .mvcMatchers(HttpMethod.GET, "/admin/{user_id}").hasRole(ADMIN) .mvcMatchers(HttpMethod.DELETE, "/admin/withdraw").hasRole(ADMIN) - .mvcMatchers(HttpMethod.POST, "/admin/excel").hasRole(ADMIN) + .mvcMatchers(HttpMethod.POST, "/admin/student/excel").hasRole(ADMIN) + .mvcMatchers(HttpMethod.POST, "/admin/club/excel").hasRole(ADMIN) // inquiry .mvcMatchers(HttpMethod.POST, "/inquiry").authenticated() diff --git a/bitgouel-api/src/test/kotlin/team/msg/domain/admin/service/AdminServiceImplTest.kt b/bitgouel-api/src/test/kotlin/team/msg/domain/admin/service/AdminServiceImplTest.kt index e7d75391d..58e17cd2f 100644 --- a/bitgouel-api/src/test/kotlin/team/msg/domain/admin/service/AdminServiceImplTest.kt +++ b/bitgouel-api/src/test/kotlin/team/msg/domain/admin/service/AdminServiceImplTest.kt @@ -5,12 +5,17 @@ import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.IsolationMode import io.kotest.core.spec.style.BehaviorSpec import io.kotest.matchers.shouldBe -import io.mockk.* +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.verify import team.msg.common.enums.ApproveStatus import team.msg.common.util.StudentUtil import team.msg.common.util.UserUtil import team.msg.domain.admin.presentation.data.request.QueryUsersRequest import team.msg.domain.club.repository.ClubRepository +import team.msg.domain.school.repository.SchoolRepository import team.msg.domain.student.model.Student import team.msg.domain.student.repository.StudentRepository import team.msg.domain.user.enums.Authority @@ -30,12 +35,14 @@ class AdminServiceImplTest : BehaviorSpec({ val studentUtil = mockk() val clubRepository = mockk() val studentRepository = mockk() + val schoolRepository = mockk() val adminServiceImpl = AdminServiceImpl( userRepository = userRepository, userUtil = userUtil, studentUtil = studentUtil, clubRepository = clubRepository, - studentRepository = studentRepository + studentRepository = studentRepository, + schoolRepository = schoolRepository ) // queryUsers 테스트 코드 diff --git a/bitgouel-domain/src/main/kotlin/team/msg/domain/lecture/model/Lecture.kt b/bitgouel-domain/src/main/kotlin/team/msg/domain/lecture/model/Lecture.kt index 9f83dbbcc..fadbb0c39 100644 --- a/bitgouel-domain/src/main/kotlin/team/msg/domain/lecture/model/Lecture.kt +++ b/bitgouel-domain/src/main/kotlin/team/msg/domain/lecture/model/Lecture.kt @@ -7,7 +7,6 @@ import javax.persistence.Enumerated import javax.persistence.FetchType import javax.persistence.JoinColumn import javax.persistence.ManyToOne -import org.hibernate.annotations.ColumnDefault import team.msg.common.entity.BaseUUIDEntity import team.msg.domain.lecture.enums.LectureStatus import team.msg.domain.lecture.enums.Semester