diff --git a/src/main/java/in/koreatech/koin/domain/graduation/aop/GraduationAspect.java b/src/main/java/in/koreatech/koin/domain/graduation/aop/GraduationAspect.java new file mode 100644 index 000000000..201a886e1 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/graduation/aop/GraduationAspect.java @@ -0,0 +1,38 @@ +package in.koreatech.koin.domain.graduation.aop; + +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.stereotype.Component; + +import in.koreatech.koin.domain.graduation.repository.DetectGraduationCalculationRepository; +import in.koreatech.koin.global.auth.AuthContext; +import lombok.RequiredArgsConstructor; + +@Aspect +@Component +@RequiredArgsConstructor +public class GraduationAspect { + + private final AuthContext authContext; + private final DetectGraduationCalculationRepository detectGraduationCalculationRepository; + + /** + * 졸업 요건 계산 변경 여부를 업데이트할 controller 메서드. + */ + @AfterReturning(pointcut = "execution(* in.koreatech.koin.domain.timetable.controller.TimetableController.createTimetables(..)) || " + + "execution(* in.koreatech.koin.domain.timetable.controller.TimetableController.updateTimetable(..)) || " + + "execution(* in.koreatech.koin.domain.timetable.controller.TimetableController.deleteTimetableById(..)) || " + + "execution(* in.koreatech.koin.domain.timetableV2.controller.TimetableControllerV2.createTimetableLecture(..)) || " + + "execution(* in.koreatech.koin.domain.timetableV2.controller.TimetableControllerV2.updateTimetableLecture(..)) || " + + "execution(* in.koreatech.koin.domain.timetableV2.controller.TimetableControllerV2.deleteTimetableLecture(..)) || " + + "execution(* in.koreatech.koin.domain.timetableV2.controller.TimetableControllerV2.deleteTimetablesFrame(..))", + returning = "result") + public void afterTimetableLecture(JoinPoint joinPoint, Object result) { + Integer userId = authContext.getUserId(); + + detectGraduationCalculationRepository.findByUserId(userId).ifPresent(detectGraduationCalculation -> { + detectGraduationCalculation.updatedIsChanged(true); + }); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/graduation/controller/GraduationApi.java b/src/main/java/in/koreatech/koin/domain/graduation/controller/GraduationApi.java index 3445a6386..4d9068cac 100644 --- a/src/main/java/in/koreatech/koin/domain/graduation/controller/GraduationApi.java +++ b/src/main/java/in/koreatech/koin/domain/graduation/controller/GraduationApi.java @@ -18,6 +18,22 @@ @Tag(name = "(Normal) Graduation: 졸업학점 계산기", description = "졸업학점 계산기 정보를 관리한다") public interface GraduationApi { + + @ApiResponses( + value = { + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))) + } + ) + @Operation(summary = "졸업학점 계산 동의") + @SecurityRequirement(name = "Jwt Authentication") + @PostMapping("/graduation/agree") + ResponseEntity createStudentCourseCalculation( + @Auth(permit = {STUDENT}) Integer userId + ); + @ApiResponses( value = { @ApiResponse(responseCode = "200"), diff --git a/src/main/java/in/koreatech/koin/domain/graduation/controller/GraduationController.java b/src/main/java/in/koreatech/koin/domain/graduation/controller/GraduationController.java index dd28c7f58..ae552b4d4 100644 --- a/src/main/java/in/koreatech/koin/domain/graduation/controller/GraduationController.java +++ b/src/main/java/in/koreatech/koin/domain/graduation/controller/GraduationController.java @@ -1,13 +1,16 @@ package in.koreatech.koin.domain.graduation.controller; import java.io.IOException; +import static in.koreatech.koin.domain.user.model.UserType.STUDENT; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; +import in.koreatech.koin.domain.graduation.dto.GraduationCourseCalculationResponse; import in.koreatech.koin.domain.graduation.service.GraduationService; import in.koreatech.koin.domain.user.model.UserType; import in.koreatech.koin.global.auth.Auth; @@ -19,6 +22,14 @@ public class GraduationController implements GraduationApi { private final GraduationService graduationService; + @PostMapping("/graduation/agree") + public ResponseEntity createStudentCourseCalculation( + @Auth(permit = {STUDENT}) Integer userId) + { + graduationService.createStudentCourseCalculation(userId); + return ResponseEntity.ok().build(); + } + @PostMapping("/graduation/excel/upload") public ResponseEntity uploadStudentGradeExcelFile( @RequestParam(value = "file") MultipartFile file, @@ -31,4 +42,11 @@ public ResponseEntity uploadStudentGradeExcelFile( return ResponseEntity.badRequest().build(); } } + + @GetMapping("/graduation/course/calculation") + public ResponseEntity getGraduationCourseCalculation( + @Auth(permit = {STUDENT}) Integer userId) { + GraduationCourseCalculationResponse response = graduationService.getGraduationCourseCalculationResponse(userId); + return ResponseEntity.ok(response); + } } diff --git a/src/main/java/in/koreatech/koin/domain/graduation/dto/GraduationCourseCalculationResponse.java b/src/main/java/in/koreatech/koin/domain/graduation/dto/GraduationCourseCalculationResponse.java new file mode 100644 index 000000000..de865d0de --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/graduation/dto/GraduationCourseCalculationResponse.java @@ -0,0 +1,25 @@ +package in.koreatech.koin.domain.graduation.dto; + +import static com.fasterxml.jackson.databind.PropertyNamingStrategies.*; + +import java.util.List; + +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(value = SnakeCaseStrategy.class) +public record GraduationCourseCalculationResponse( + + List courseTypes +) { + + public record InnerCalculationResponse(String courseType, int requiredGrades, int grades) { + + public static InnerCalculationResponse of(String courseType, int requiredGrades, int grades) { + return new InnerCalculationResponse(courseType, requiredGrades, grades); + } + } + + public static GraduationCourseCalculationResponse of (List courseTypes) { + return new GraduationCourseCalculationResponse(courseTypes); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/graduation/model/DetectGraduationCalculation.java b/src/main/java/in/koreatech/koin/domain/graduation/model/DetectGraduationCalculation.java index 77e50fff2..ed21376e3 100644 --- a/src/main/java/in/koreatech/koin/domain/graduation/model/DetectGraduationCalculation.java +++ b/src/main/java/in/koreatech/koin/domain/graduation/model/DetectGraduationCalculation.java @@ -14,6 +14,7 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.OneToOne; import jakarta.persistence.Table; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -36,6 +37,15 @@ public class DetectGraduationCalculation { @Column(name = "is_changed", columnDefinition = "TINYINT") private boolean isChanged = false; + @Builder + private DetectGraduationCalculation( + User user, + boolean isChanged + ) { + this.user = user; + this.isChanged = isChanged; + } + public void updatedIsChanged(boolean isChanged) { this.isChanged = isChanged; } diff --git a/src/main/java/in/koreatech/koin/domain/graduation/repository/CatalogRepository.java b/src/main/java/in/koreatech/koin/domain/graduation/repository/CatalogRepository.java index 0faf67651..9cb9f4cb2 100644 --- a/src/main/java/in/koreatech/koin/domain/graduation/repository/CatalogRepository.java +++ b/src/main/java/in/koreatech/koin/domain/graduation/repository/CatalogRepository.java @@ -1,5 +1,6 @@ package in.koreatech.koin.domain.graduation.repository; +import java.util.List; import java.util.Optional; import org.springframework.data.repository.Repository; @@ -9,6 +10,7 @@ import in.koreatech.koin.domain.student.model.Department; public interface CatalogRepository extends Repository { + Optional findByYearAndDepartmentAndCode(String year, Department department, String code); default Catalog getByYearAndDepartmentAndCode(String year, Department department, String code) { @@ -16,4 +18,6 @@ default Catalog getByYearAndDepartmentAndCode(String year, Department department .orElseThrow(() -> CatalogNotFoundException.withDetail( "year: " + year + ", department: " + department + ", code: " + code)); } + + List findByLectureNameAndYearAndDepartment(String lectureName, String studentYear, Department department); } diff --git a/src/main/java/in/koreatech/koin/domain/graduation/repository/DetectGraduationCalculationRepository.java b/src/main/java/in/koreatech/koin/domain/graduation/repository/DetectGraduationCalculationRepository.java new file mode 100644 index 000000000..538a72114 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/graduation/repository/DetectGraduationCalculationRepository.java @@ -0,0 +1,15 @@ +package in.koreatech.koin.domain.graduation.repository; + +import java.util.Optional; + +import org.springframework.data.repository.Repository; + +import in.koreatech.koin.domain.graduation.model.DetectGraduationCalculation; + +public interface DetectGraduationCalculationRepository extends Repository { + + Optional findByUserId(Integer userId); + + void save(DetectGraduationCalculation detectGraduationCalculation); +} + diff --git a/src/main/java/in/koreatech/koin/domain/graduation/repository/StandardGraduationRequirementsRepository.java b/src/main/java/in/koreatech/koin/domain/graduation/repository/StandardGraduationRequirementsRepository.java new file mode 100644 index 000000000..b17ba74e4 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/graduation/repository/StandardGraduationRequirementsRepository.java @@ -0,0 +1,15 @@ +package in.koreatech.koin.domain.graduation.repository; + +import java.util.List; + +import org.springframework.data.repository.Repository; + +import in.koreatech.koin.domain.graduation.model.StandardGraduationRequirements; +import in.koreatech.koin.domain.student.model.Department; + +public interface StandardGraduationRequirementsRepository extends Repository { + + List findAllByDepartmentAndYear(Department department, String year); + + List findByDepartmentIdAndCourseTypeIdAndYear(Integer id, Integer id1, String studentYear); +} diff --git a/src/main/java/in/koreatech/koin/domain/graduation/repository/StudentCourseCalculationRepository.java b/src/main/java/in/koreatech/koin/domain/graduation/repository/StudentCourseCalculationRepository.java new file mode 100644 index 000000000..875fbbf4f --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/graduation/repository/StudentCourseCalculationRepository.java @@ -0,0 +1,20 @@ +package in.koreatech.koin.domain.graduation.repository; + +import java.util.Optional; + +import org.springframework.data.repository.Repository; + +import in.koreatech.koin.domain.graduation.model.StudentCourseCalculation; + +public interface StudentCourseCalculationRepository extends Repository { + + Optional findByUserId(Integer userId); + + void deleteAllByUserId(Integer userId); + + void save(StudentCourseCalculation studentCourseCalculation); + + StudentCourseCalculation findByUserIdAndStandardGraduationRequirementsId(Integer userId, Integer id); + + void delete(StudentCourseCalculation existingCalculation); +} diff --git a/src/main/java/in/koreatech/koin/domain/graduation/service/GraduationService.java b/src/main/java/in/koreatech/koin/domain/graduation/service/GraduationService.java index d894d8d28..a286b6dfd 100644 --- a/src/main/java/in/koreatech/koin/domain/graduation/service/GraduationService.java +++ b/src/main/java/in/koreatech/koin/domain/graduation/service/GraduationService.java @@ -2,10 +2,13 @@ import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Objects; import org.apache.poi.hssf.usermodel.HSSFWorkbook; -import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; @@ -13,6 +16,24 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; +import in.koreatech.koin.domain.graduation.dto.GraduationCourseCalculationResponse; +import in.koreatech.koin.domain.graduation.model.Catalog; +import in.koreatech.koin.domain.graduation.model.DetectGraduationCalculation; +import in.koreatech.koin.domain.graduation.model.StandardGraduationRequirements; +import in.koreatech.koin.domain.graduation.model.StudentCourseCalculation; +import in.koreatech.koin.domain.graduation.repository.CatalogRepository; +import in.koreatech.koin.domain.graduation.repository.DetectGraduationCalculationRepository; +import in.koreatech.koin.domain.graduation.repository.StandardGraduationRequirementsRepository; +import in.koreatech.koin.domain.graduation.repository.StudentCourseCalculationRepository; +import in.koreatech.koin.domain.student.exception.DepartmentNotFoundException; +import in.koreatech.koin.domain.student.exception.StudentNumberNotFoundException; +import in.koreatech.koin.domain.student.model.Department; +import in.koreatech.koin.domain.student.model.Student; +import in.koreatech.koin.domain.student.repository.StudentRepository; +import in.koreatech.koin.domain.student.util.StudentUtil; +import in.koreatech.koin.domain.timetableV2.model.TimetableLecture; +import in.koreatech.koin.domain.timetableV2.repository.TimetableFrameRepositoryV2; + import in.koreatech.koin.domain.graduation.model.GradeExcelData; import in.koreatech.koin.domain.graduation.model.CourseType; import in.koreatech.koin.domain.graduation.repository.CourseTypeRepository; @@ -21,10 +42,8 @@ import in.koreatech.koin.domain.timetable.model.Lecture; import in.koreatech.koin.domain.timetable.model.Semester; import in.koreatech.koin.domain.timetableV2.model.TimetableFrame; -import in.koreatech.koin.domain.timetableV2.model.TimetableLecture; import in.koreatech.koin.domain.timetableV2.repository.LectureRepositoryV2; import in.koreatech.koin.domain.timetableV2.repository.SemesterRepositoryV2; -import in.koreatech.koin.domain.timetableV2.repository.TimetableFrameRepositoryV2; import in.koreatech.koin.domain.timetableV2.repository.TimetableLectureRepositoryV2; import in.koreatech.koin.domain.user.model.User; import in.koreatech.koin.domain.user.repository.UserRepository; @@ -35,17 +54,87 @@ @Transactional(readOnly = true) public class GraduationService { - private static final String MIDDLE_TOTAL = "소 계"; - private static final String TOTAL = "합 계"; - private static final String RETAKE = "Y"; - private static final String UNSATISFACTORY = "U"; - + private final StudentRepository studentRepository; + private final StudentCourseCalculationRepository studentCourseCalculationRepository; + private final StandardGraduationRequirementsRepository standardGraduationRequirementsRepository; + private final TimetableFrameRepositoryV2 timetableFrameRepositoryV2; + private final DetectGraduationCalculationRepository detectGraduationCalculationRepository; private final CourseTypeRepository courseTypeRepository; private final UserRepository userRepository; private final SemesterRepositoryV2 semesterRepositoryV2; private final LectureRepositoryV2 lectureRepositoryV2; private final TimetableLectureRepositoryV2 timetableLectureRepositoryV2; - private final TimetableFrameRepositoryV2 timetableFrameRepositoryV2; + private final CatalogRepository catalogRepository; + + private static final String MIDDLE_TOTAL = "소 계"; + private static final String TOTAL = "합 계"; + private static final String RETAKE = "Y"; + private static final String UNSATISFACTORY = "U"; + + @Transactional + public void createStudentCourseCalculation(Integer userId) { + Student student = studentRepository.getById(userId); + + validateStudentField(student.getDepartment(), "학과를 추가하세요."); + validateStudentField(student.getStudentNumber(), "학번을 추가하세요."); + + initializeStudentCourseCalculation(student, student.getDepartment()); + + DetectGraduationCalculation detectGraduationCalculation = DetectGraduationCalculation.builder() + .user(student.getUser()) + .isChanged(false) + .build(); + detectGraduationCalculationRepository.save(detectGraduationCalculation); + } + + @Transactional + public void resetStudentCourseCalculation(Student student, Department newDepartment) { + // 기존 학생 졸업요건 계산 정보 삭제 + studentCourseCalculationRepository.findByUserId(student.getUser().getId()) + .ifPresent(studentCourseCalculation -> { + studentCourseCalculationRepository.deleteAllByUserId(student.getUser().getId()); + }); + + initializeStudentCourseCalculation(student, newDepartment); + + detectGraduationCalculationRepository.findByUserId(student.getUser().getId()) + .ifPresent(detectGraduationCalculation -> { + detectGraduationCalculation.updatedIsChanged(true); + }); + } + + @Transactional + public GraduationCourseCalculationResponse getGraduationCourseCalculationResponse(Integer userId) { + DetectGraduationCalculation detectGraduationCalculation = detectGraduationCalculationRepository.findByUserId(userId) + .orElseThrow(() -> new IllegalArgumentException("해당 사용자의 GraduationCalculation 정보가 존재하지 않습니다.")); + + if (!detectGraduationCalculation.isChanged()) { + return GraduationCourseCalculationResponse.of(List.of()); + } + + // 학생 정보와 학과 검증 + Student student = getValidatedStudent(userId); + String studentYear = StudentUtil.parseStudentNumberYearAsString(student.getStudentNumber()); + + // 시간표와 대학 요람 데이터 가져오기 + List catalogList = getCatalogListForStudent(student, studentYear); + + // courseTypeId와 학점 맵핑 + Map courseTypeCreditsMap = calculateCourseTypeCredits(catalogList); + + // GraduationRequirements 리스트 조회 + List graduationRequirements = getGraduationRequirements(catalogList, + studentYear); + + // 계산 로직 및 응답 생성 + List courseTypes = processGraduationCalculations( + userId, student, graduationRequirements, courseTypeCreditsMap + ); + + detectGraduationCalculation.updatedIsChanged(false); + + return GraduationCourseCalculationResponse.of(courseTypes); + } @Transactional public void readStudentGradeExcelFile(MultipartFile file, Integer userId) throws IOException { @@ -140,8 +229,8 @@ private void checkFiletype(MultipartFile file) { private boolean skipRow(GradeExcelData gradeExcelData) { return gradeExcelData.classTitle().equals(MIDDLE_TOTAL) || - gradeExcelData.retakeStatus().equals(RETAKE) || - gradeExcelData.grade().equals(UNSATISFACTORY); + gradeExcelData.retakeStatus().equals(RETAKE) || + gradeExcelData.grade().equals(UNSATISFACTORY); } private String getKoinSemester(String semester, String year) { @@ -152,4 +241,124 @@ private String getKoinSemester(String semester, String year) { } else return year + "-" + "여름"; } + + private void validateStudentField(Object field, String message) { + if (field == null) { + throw DepartmentNotFoundException.withDetail(message); + } + } + + private void initializeStudentCourseCalculation(Student student, Department department) { + // 학번에 맞는 이수요건 정보 조회 + List requirementsList = + standardGraduationRequirementsRepository.findAllByDepartmentAndYear( + department, student.getStudentNumber().substring(0, 4)); + + // 학생 졸업요건 계산 정보 초기화 + requirementsList.forEach(requirement -> + studentCourseCalculationRepository.save( + StudentCourseCalculation.builder() + .completedGrades(0) + .user(student.getUser()) + .standardGraduationRequirements(requirement) + .build() + ) + ); + } + + private Student getValidatedStudent(Integer userId) { + Student student = studentRepository.getById(userId); + + if (student.getDepartment() == null) { + throw new DepartmentNotFoundException("학과를 추가하세요."); + } + if (student.getStudentNumber() == null) { + throw new StudentNumberNotFoundException("학번을 추가하세요."); + } + return student; + } + + private List getCatalogListForStudent(Student student, String studentYear) { + List timetableLectures = timetableFrameRepositoryV2.getAllByUserId(student.getId()).stream() + .flatMap(frame -> frame.getTimetableLectures().stream()) + .toList(); + + List catalogList = new ArrayList<>(); + timetableLectures.forEach(timetableLecture -> { + String lectureName = timetableLecture.getLecture() != null + ? timetableLecture.getLecture().getName() + : timetableLecture.getClassTitle(); + + if (lectureName != null) { + List catalogs = catalogRepository.findByLectureNameAndYearAndDepartment( + lectureName, studentYear, student.getDepartment()); + catalogList.addAll(catalogs); + } + }); + return catalogList; + } + + private Map calculateCourseTypeCredits(List catalogList) { + Map courseTypeCreditsMap = new HashMap<>(); + for (Catalog catalog : catalogList) { + int courseTypeId = catalog.getCourseType().getId(); + courseTypeCreditsMap.put(courseTypeId, + courseTypeCreditsMap.getOrDefault(courseTypeId, 0) + catalog.getCredit()); + } + return courseTypeCreditsMap; + } + + private List getGraduationRequirements(List catalogList, + String studentYear) { + return catalogList.stream() + .map(catalog -> standardGraduationRequirementsRepository.findByDepartmentIdAndCourseTypeIdAndYear( + catalog.getDepartment().getId(), + catalog.getCourseType().getId(), + studentYear + )) + .filter(Objects::nonNull) + .flatMap(List::stream) + .distinct() + .toList(); + } + + private List processGraduationCalculations( + Integer userId, Student student, List graduationRequirements, + Map courseTypeCreditsMap) { + + return graduationRequirements.stream() + .map(requirement -> GraduationCourseCalculationResponse.InnerCalculationResponse.of( + requirement.getCourseType().getName(), + requirement.getRequiredGrades(), + updateStudentCourseCalculation(userId, student, requirement, courseTypeCreditsMap) + )) + .toList(); + } + + private int updateStudentCourseCalculation(Integer userId, Student student, + StandardGraduationRequirements requirement, + Map courseTypeCreditsMap) { + if (requirement.getCourseType() == null) { + return 0; + } + + int completedGrades = courseTypeCreditsMap.getOrDefault(requirement.getCourseType().getId(), 0); + + StudentCourseCalculation existingCalculation = studentCourseCalculationRepository + .findByUserIdAndStandardGraduationRequirementsId(userId, requirement.getId()); + + if (existingCalculation != null) { + completedGrades += existingCalculation.getCompletedGrades(); + studentCourseCalculationRepository.delete(existingCalculation); + } + + StudentCourseCalculation newCalculation = StudentCourseCalculation.builder() + .completedGrades(completedGrades) + .user(student.getUser()) + .standardGraduationRequirements(requirement) + .build(); + studentCourseCalculationRepository.save(newCalculation); + + return completedGrades; + } } diff --git a/src/main/java/in/koreatech/koin/domain/student/exception/DepartmentNotFoundException.java b/src/main/java/in/koreatech/koin/domain/student/exception/DepartmentNotFoundException.java index c3c531ed6..11d3fc166 100644 --- a/src/main/java/in/koreatech/koin/domain/student/exception/DepartmentNotFoundException.java +++ b/src/main/java/in/koreatech/koin/domain/student/exception/DepartmentNotFoundException.java @@ -6,7 +6,7 @@ public class DepartmentNotFoundException extends DataNotFoundException { private static final String DEFAULT_MESSAGE = "존재하지 않는 학과입니다."; - protected DepartmentNotFoundException(String message) { + public DepartmentNotFoundException(String message) { super(message); } diff --git a/src/main/java/in/koreatech/koin/domain/student/exception/StudentNumberNotFoundException.java b/src/main/java/in/koreatech/koin/domain/student/exception/StudentNumberNotFoundException.java new file mode 100644 index 000000000..641a6a56e --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/student/exception/StudentNumberNotFoundException.java @@ -0,0 +1,24 @@ +package in.koreatech.koin.domain.student.exception; + +import in.koreatech.koin.global.exception.DataNotFoundException; + +public class StudentNumberNotFoundException extends DataNotFoundException { + + private static final String DEFAULT_MESSAGE = "학번을 추가하세요."; + + public StudentNumberNotFoundException + (String message) { + super(message); + } + + protected StudentNumberNotFoundException + (String message, String detail) { + super(message, detail); + } + + public static StudentNumberNotFoundException + withDetail(String detail) { + return new StudentNumberNotFoundException + (DEFAULT_MESSAGE, detail); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/student/model/Student.java b/src/main/java/in/koreatech/koin/domain/student/model/Student.java index ca061b663..798542f1f 100644 --- a/src/main/java/in/koreatech/koin/domain/student/model/Student.java +++ b/src/main/java/in/koreatech/koin/domain/student/model/Student.java @@ -4,6 +4,7 @@ import in.koreatech.koin.domain.user.model.User; import in.koreatech.koin.domain.user.model.UserIdentity; +import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -39,7 +40,7 @@ public class Student { private String studentNumber; @JoinColumn(name = "department_id") - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) private Department department; @Column(name = "identity", columnDefinition = "SMALLINT") diff --git a/src/main/java/in/koreatech/koin/domain/student/repository/DepartmentRepository.java b/src/main/java/in/koreatech/koin/domain/student/repository/DepartmentRepository.java index 6a158d525..8dde58ef4 100644 --- a/src/main/java/in/koreatech/koin/domain/student/repository/DepartmentRepository.java +++ b/src/main/java/in/koreatech/koin/domain/student/repository/DepartmentRepository.java @@ -11,6 +11,8 @@ public interface DepartmentRepository extends Repository { Department save(Department department); + void saveAll(Iterable departments); + Optional findByName(String name); default Department getByName(String name) { diff --git a/src/main/java/in/koreatech/koin/domain/student/service/StudentService.java b/src/main/java/in/koreatech/koin/domain/student/service/StudentService.java index eaf8718fd..65083c8d5 100644 --- a/src/main/java/in/koreatech/koin/domain/student/service/StudentService.java +++ b/src/main/java/in/koreatech/koin/domain/student/service/StudentService.java @@ -1,8 +1,16 @@ package in.koreatech.koin.domain.student.service; + import java.time.Clock; +import java.util.List; import java.util.Optional; import java.util.UUID; +import in.koreatech.koin.domain.graduation.model.StandardGraduationRequirements; +import in.koreatech.koin.domain.graduation.model.StudentCourseCalculation; +import in.koreatech.koin.domain.graduation.repository.DetectGraduationCalculationRepository; +import in.koreatech.koin.domain.graduation.repository.StandardGraduationRequirementsRepository; +import in.koreatech.koin.domain.graduation.repository.StudentCourseCalculationRepository; +import in.koreatech.koin.domain.graduation.service.GraduationService; import in.koreatech.koin.domain.student.model.Department; import in.koreatech.koin.domain.student.model.Student; import in.koreatech.koin.domain.student.model.StudentEmailRequestEvent; @@ -18,6 +26,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.servlet.ModelAndView; + import in.koreatech.koin.domain.user.dto.AuthTokenRequest; import in.koreatech.koin.domain.user.dto.FindPasswordRequest; import in.koreatech.koin.domain.student.dto.StudentLoginRequest; @@ -54,6 +63,10 @@ public class StudentService { private final StudentRepository studentRepository; private final StudentRedisRepository studentRedisRepository; private final DepartmentRepository departmentRepository; + private final StudentCourseCalculationRepository studentCourseCalculationRepository; + private final StandardGraduationRequirementsRepository standardGraduationRequirementsRepository; + private final DetectGraduationCalculationRepository detectGraduationCalculationRepository; + private final GraduationService graduationService; private final PasswordEncoder passwordEncoder; private final ApplicationEventPublisher eventPublisher; private final Clock clock; @@ -91,7 +104,12 @@ public StudentUpdateResponse updateStudent(Integer userId, StudentUpdateRequest Student student = studentRepository.getById(userId); User user = student.getUser(); + Department oldDepartment = student.getDepartment(); Department newDepartment = departmentRepository.getByName(request.major()); + // 학부(학과) 변경 시 학생의 졸업 요건 계산 정보 초기화 + if (isChangedDepartment(oldDepartment, newDepartment) && student.getStudentNumber() != null) { + graduationService.resetStudentCourseCalculation(student, newDepartment); + } user.update(request.nickname(), request.name(), request.phoneNumber(), request.gender()); user.updateStudentPassword(passwordEncoder, request.password()); student.updateInfo(request.studentNumber(), newDepartment); @@ -150,4 +168,8 @@ public ModelAndView checkResetToken(String resetToken, String serverUrl) { modelAndView.addObject("resetToken", resetToken); return modelAndView; } + + private boolean isChangedDepartment(Department oldDepartment, Department newDepartment) { + return !oldDepartment.equals(newDepartment); + } } diff --git a/src/main/java/in/koreatech/koin/domain/student/util/StudentUtil.java b/src/main/java/in/koreatech/koin/domain/student/util/StudentUtil.java index e1379e14b..46d475b9c 100644 --- a/src/main/java/in/koreatech/koin/domain/student/util/StudentUtil.java +++ b/src/main/java/in/koreatech/koin/domain/student/util/StudentUtil.java @@ -8,4 +8,8 @@ public class StudentUtil { public static Integer parseStudentNumberYear(String studentNumber) { return Integer.parseInt(studentNumber.substring(0, 4)); } + + public static String parseStudentNumberYearAsString(String studentNumber) { + return studentNumber.substring(0, 4); + } } diff --git a/src/main/java/in/koreatech/koin/domain/timetableV2/factory/TimetableLectureCreator.java b/src/main/java/in/koreatech/koin/domain/timetableV2/factory/TimetableLectureCreator.java index 6326f693d..66066df63 100644 --- a/src/main/java/in/koreatech/koin/domain/timetableV2/factory/TimetableLectureCreator.java +++ b/src/main/java/in/koreatech/koin/domain/timetableV2/factory/TimetableLectureCreator.java @@ -1,5 +1,6 @@ package in.koreatech.koin.domain.timetableV2.factory; +import static in.koreatech.koin.domain.student.util.StudentUtil.parseStudentNumberYear; import static in.koreatech.koin.domain.timetableV2.dto.request.TimetableLectureCreateRequest.InnerTimeTableLectureRequest; import org.springframework.stereotype.Component; @@ -9,7 +10,6 @@ import in.koreatech.koin.domain.graduation.repository.CatalogRepository; import in.koreatech.koin.domain.student.model.Student; import in.koreatech.koin.domain.student.repository.StudentRepository; -import in.koreatech.koin.domain.student.util.StudentUtil; import in.koreatech.koin.domain.timetable.model.Lecture; import in.koreatech.koin.domain.timetableV2.dto.request.TimetableLectureCreateRequest; import in.koreatech.koin.domain.timetableV2.model.TimetableFrame; @@ -22,40 +22,36 @@ @RequiredArgsConstructor public class TimetableLectureCreator { - private final LectureRepositoryV2 lectureRepositoryV2; - private final TimetableLectureRepositoryV2 timetableLectureRepositoryV2; private final CatalogRepository catalogRepository; private final StudentRepository studentRepository; + private final LectureRepositoryV2 lectureRepositoryV2; + private final TimetableLectureRepositoryV2 timetableLectureRepositoryV2; - public void createTimetableLectures(TimetableLectureCreateRequest request, Integer userId, TimetableFrame frame) { + public void createTimetableLectures(TimetableLectureCreateRequest request, TimetableFrame frame) { for (InnerTimeTableLectureRequest lectureRequest : request.timetableLecture()) { Lecture lecture = determineLecture(lectureRequest.lectureId()); - CourseType courseType = determineCourseType(lecture, userId); + CourseType courseType = getCourseType(frame.getUser().getId(), lecture); TimetableLecture timetableLecture = lectureRequest.toTimetableLecture(frame, lecture, courseType); frame.addTimeTableLecture(timetableLecture); timetableLectureRepositoryV2.save(timetableLecture); } } - private CourseType determineCourseType(Lecture lecture, Integer userId) { - if (lecture != null) { - return getCourseType(userId, lecture); + private Lecture determineLecture(Integer lectureId) { + if (lectureId != null) { + return lectureRepositoryV2.getLectureById(lectureId); } return null; } private CourseType getCourseType(Integer userId, Lecture lecture) { + if (lecture == null) { + return null; + } Student student = studentRepository.getById(userId); - String year = StudentUtil.parseStudentNumberYear(student.getStudentNumber()).toString(); + String year = parseStudentNumberYear(student.getStudentNumber()).toString(); String code = lecture.getCode(); Catalog catalog = catalogRepository.getByYearAndDepartmentAndCode(year, student.getDepartment(), code); return catalog.getCourseType(); } - - private Lecture determineLecture(Integer lectureId) { - if (lectureId != null) { - return lectureRepositoryV2.getLectureById(lectureId); - } - return null; - } } diff --git a/src/main/java/in/koreatech/koin/domain/timetableV2/repository/TimetableFrameRepositoryV2.java b/src/main/java/in/koreatech/koin/domain/timetableV2/repository/TimetableFrameRepositoryV2.java index 4715310f7..9415f2c9f 100644 --- a/src/main/java/in/koreatech/koin/domain/timetableV2/repository/TimetableFrameRepositoryV2.java +++ b/src/main/java/in/koreatech/koin/domain/timetableV2/repository/TimetableFrameRepositoryV2.java @@ -1,5 +1,6 @@ package in.koreatech.koin.domain.timetableV2.repository; +import java.util.Arrays; import java.util.List; import java.util.Optional; @@ -94,7 +95,12 @@ default TimetableFrame getByIdWithDeleted(Integer id) { void deleteAllByUserAndSemester(User user, Semester semester); - List findAllByUserId(Integer userId); + Optional> findAllByUserId(Integer userId); boolean existsByUserAndSemester(User user, Semester semester); + + default List getAllByUserId(Integer userId) { + return findAllByUserId(userId) + .orElseThrow(() -> TimetableFrameNotFoundException.withDetail("userId: " + userId)); + }; } diff --git a/src/main/java/in/koreatech/koin/domain/timetableV2/service/TimetableFrameService.java b/src/main/java/in/koreatech/koin/domain/timetableV2/service/TimetableFrameService.java index 9fe8887ec..e90b67709 100644 --- a/src/main/java/in/koreatech/koin/domain/timetableV2/service/TimetableFrameService.java +++ b/src/main/java/in/koreatech/koin/domain/timetableV2/service/TimetableFrameService.java @@ -70,7 +70,7 @@ public Object getTimetablesFrame(Integer userId, String semesterRequest) { } public TimetableFramesResponse getAllTimetablesFrame(Integer userId) { - List timetableFrames = timetableFrameRepositoryV2.findAllByUserId(userId); + List timetableFrames = timetableFrameRepositoryV2.getAllByUserId(userId); return TimetableFramesResponse.from(timetableFrames); } diff --git a/src/main/java/in/koreatech/koin/domain/timetableV2/service/TimetableLectureService.java b/src/main/java/in/koreatech/koin/domain/timetableV2/service/TimetableLectureService.java index 29e932a02..e15c34d8c 100644 --- a/src/main/java/in/koreatech/koin/domain/timetableV2/service/TimetableLectureService.java +++ b/src/main/java/in/koreatech/koin/domain/timetableV2/service/TimetableLectureService.java @@ -41,7 +41,7 @@ public class TimetableLectureService { public TimetableLectureResponse createTimetableLectures(Integer userId, TimetableLectureCreateRequest request) { TimetableFrame frame = timetableFrameRepositoryV2.getById(request.timetableFrameId()); validateUserAuthorization(frame.getUser().getId(), userId); - timetableLectureCreator.createTimetableLectures(request, userId, frame); + timetableLectureCreator.createTimetableLectures(request, frame); return getTimetableLectureResponse(userId, frame); } diff --git a/src/test/java/in/koreatech/koin/acceptance/StudentApiTest.java b/src/test/java/in/koreatech/koin/acceptance/StudentApiTest.java index 2f6343e4b..313f36edf 100644 --- a/src/test/java/in/koreatech/koin/acceptance/StudentApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/StudentApiTest.java @@ -29,6 +29,7 @@ import in.koreatech.koin.domain.user.model.User; import in.koreatech.koin.domain.user.model.UserGender; import in.koreatech.koin.domain.user.repository.UserRepository; +import in.koreatech.koin.fixture.DepartmentFixture; import in.koreatech.koin.fixture.UserFixture; import in.koreatech.koin.global.auth.JwtProvider; @@ -54,6 +55,9 @@ public class StudentApiTest extends AcceptanceTest { @Autowired private UserFixture userFixture; + @Autowired + private DepartmentFixture departmentFixture; + @BeforeAll void setup() { clear(); @@ -134,6 +138,7 @@ void setup() { @Test void 학생이_정보를_수정한다() throws Exception { + departmentFixture.전체학부(); Student student = userFixture.준호_학생(); String token = userFixture.getToken(student.getUser()); @@ -341,6 +346,7 @@ void setup() { @Test void 이메일_요청을_확인_후_회원가입_이벤트가_발생하고_Redis에_저장된_정보가_삭제된다() throws Exception { + departmentFixture.전체학부(); mockMvc.perform( post("/user/student/register") .content(""" diff --git a/src/test/java/in/koreatech/koin/acceptance/TimetableLectureApiTest.java b/src/test/java/in/koreatech/koin/acceptance/TimetableLectureApiTest.java index cec1d887a..c4ff99a01 100644 --- a/src/test/java/in/koreatech/koin/acceptance/TimetableLectureApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/TimetableLectureApiTest.java @@ -80,7 +80,7 @@ void setup() { ], "professor" : "서정빈", "grades": "2", - "memo" : "메모", + "memo" : "메모" }, { "class_title": "커스텀생성2", @@ -448,7 +448,7 @@ void setup() { "target": "기공1", "professor": "박한수,최준호", "department": "기계공학부", - "course_type": "MSC 필수" + "course_type": "교양 필수" } ], "grades": 6, @@ -516,7 +516,7 @@ void setup() { "target": "기공1", "professor": "박한수,최준호", "department": "기계공학부", - "course_type": "MSC 필수" + "course_type": "교양 필수" } ], "grades": 6, diff --git a/src/test/java/in/koreatech/koin/fixture/DepartmentFixture.java b/src/test/java/in/koreatech/koin/fixture/DepartmentFixture.java index 73db29fba..cbe66c252 100644 --- a/src/test/java/in/koreatech/koin/fixture/DepartmentFixture.java +++ b/src/test/java/in/koreatech/koin/fixture/DepartmentFixture.java @@ -1,5 +1,7 @@ package in.koreatech.koin.fixture; +import java.util.List; + import org.springframework.stereotype.Component; import in.koreatech.koin.domain.student.model.Department; @@ -15,6 +17,25 @@ public DepartmentFixture(DepartmentRepository departmentRepository) { this.departmentRepository = departmentRepository; } + public void 전체학부() { + List departments = List.of( + Department.builder().name("건축공학부").build(), + Department.builder().name("고용서비스정책학과").build(), + Department.builder().name("기계공학부").build(), + Department.builder().name("디자인공학부").build(), + Department.builder().name("메카트로닉스공학부").build(), + Department.builder().name("산업경영학부").build(), + Department.builder().name("전기전자통신공학부").build(), + Department.builder().name("컴퓨터공학부").build(), + Department.builder().name("화학생명공학부").build(), + Department.builder().name("에너지신소재공학부").build(), + Department.builder().name("에너지신소재화학공학부").build(), + Department.builder().name("안전공학부").build() + ); + + departmentRepository.saveAll(departments); + } + public Department 컴퓨터공학부() { return departmentRepository.save( Department.builder()