Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[REFACTOR] Service 계층 책임 분리 및 Validator 구현 #162

Merged
merged 28 commits into from
Jan 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e1920d3
FEAT: ScoreCalculator 점수 계산 서비스 분리 및 생성
hyerin315 Nov 29, 2024
8b57196
REFACTOR: 컨벤션 적용
hyerin315 Nov 29, 2024
b78128e
MERGE: 병합 중 충돌 해결
hyerin315 Nov 29, 2024
7fbb0e1
FEAT: ScoreCalculator 생성으로 Service 계층 책임 분리
hyerin315 Dec 3, 2024
e30964e
REFACTOR: ScoreCalculator 생성으로 Service 계층 책임 분리
hyerin315 Dec 3, 2024
da67654
[FEAT] 관리자 - 강의 상세 조회, 수정 엔드포인트 구현 (#153)
Chan-GN Nov 29, 2024
0c23805
[FEAT] 공지사항 init data 추가 (#154)
12ka39 Dec 3, 2024
557d674
REFACTOR: 분리된 책임에 따른 Service 메서드 재배치
hyerin315 Dec 4, 2024
b47dfb1
REFACTOR: 점수 계산 로직을 ScoreCalculator로 이동
hyerin315 Dec 4, 2024
71488ae
FIX: ExamStatistics 생성자 추가
hyerin315 Dec 4, 2024
ad6030e
REFACTOR: Service 책임 분리에 따른 Controller 수정
hyerin315 Dec 4, 2024
847176c
FIX: ExamService와 ExamStatusService 순환 참조 문제 해결
hyerin315 Dec 5, 2024
9b69581
REFACTOR: 서비스 간 의존성을 줄이고 역할을 명확히 분리
hyerin315 Jan 2, 2025
125e8ae
FIX: Service간 순환참조를 해결하기 위해 유효성 검사를 각 기능별 Validator로 분리
hyerin315 Jan 2, 2025
8b88ad5
FIX: 권한 검증 구문 수정
hyerin315 Jan 2, 2025
d43727f
REFACTOR: 중복된 ErrorCode 정리 및 수정
hyerin315 Jan 2, 2025
6a19d1e
REFACTOR: 추후 확장 및 유지보수를 고려하여 Exception 코드 분리
hyerin315 Jan 2, 2025
1071b7a
REFACTOR: Grading 관련 클래스명 변경
hyerin315 Jan 2, 2025
e6cb2c2
REFACTOR: 코드 formatting 적용
hyerin315 Jan 2, 2025
3004fd4
REFACTOR: Repository에서 사용자 email을 통한 조회를 Id로 변경
hyerin315 Jan 2, 2025
c351ee3
REFACTOR: 시험 결과 확인 엔드포인트 구분
hyerin315 Jan 2, 2025
cc3ac38
FIX: 권한 검증 메서드명 변경
hyerin315 Jan 2, 2025
537f6b5
FEAT: 사용자 역할별 이메일 검증 추가
hyerin315 Jan 2, 2025
9eb5828
REFACTOR: Repository에서 사용자 조회 방식을 email에서 Id로 변경에 따른 Service 및 Contro…
hyerin315 Jan 2, 2025
69360a3
REFACTOR: 사용자 권한 검증 에러처리
hyerin315 Jan 2, 2025
2aaf39e
MERGE: 병합 중 충돌 해결
hyerin315 Jan 2, 2025
8dc65cf
MERGE: 병합 중 충돌 해결
hyerin315 Jan 2, 2025
7f5a40e
REFACTOR: CourseAccessValidator 로직 최적화 및 ExamSubmissionController에서 V…
hyerin315 Jan 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
package com.example.epari.assignment.controller;

import java.util.List;

import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.example.epari.assignment.dto.submission.GradeRequestDto;
import com.example.epari.assignment.dto.submission.SubmissionRequestDto;
import com.example.epari.assignment.dto.submission.SubmissionResponseDto;
import com.example.epari.assignment.service.SubmissionService;
import com.example.epari.course.repository.CourseRepository;
import com.example.epari.global.annotation.CurrentUserEmail;
import com.example.epari.global.exception.BusinessBaseException;
import com.example.epari.global.exception.ErrorCode;
import com.example.epari.global.validator.CourseAccessValidator;
import com.example.epari.user.domain.Instructor;
import com.example.epari.user.domain.Student;
import com.example.epari.user.repository.InstructorRepository;
import com.example.epari.user.repository.StudentRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
Expand All @@ -25,112 +39,87 @@ public class SubmissionController {

private final StudentRepository studentRepository;

private final CourseRepository courseRepository;
private final InstructorRepository instructorRepository;

private final CourseAccessValidator courseAccessValidator;

@PreAuthorize("hasRole('STUDENT')")
@PostMapping
public ResponseEntity<SubmissionResponseDto> addSubmission(
@PathVariable Long courseId,
@PathVariable Long assignmentId,
@ModelAttribute SubmissionRequestDto requestDto,
public ResponseEntity<SubmissionResponseDto> addSubmission(@PathVariable Long courseId,
@PathVariable Long assignmentId, @ModelAttribute SubmissionRequestDto requestDto,
@CurrentUserEmail String email) {
Student student = studentRepository.findByEmail(email)
.orElseThrow(() -> new IllegalArgumentException("학생 정보를 찾을 수 없습니다."));

return ResponseEntity.ok(
submissionService.addSubmission(courseId, assignmentId, requestDto, student.getId())
);
return ResponseEntity.ok(submissionService.addSubmission(courseId, assignmentId, requestDto, student.getId()));
}

@PreAuthorize("hasRole('STUDENT')")
@GetMapping("/{submissionId}")
public ResponseEntity<SubmissionResponseDto> getSubmission(
@PathVariable Long courseId,
@PathVariable Long assignmentId,
@PathVariable Long submissionId) {
return ResponseEntity.ok(
submissionService.getSubmissionById(courseId, assignmentId, submissionId)
);
public ResponseEntity<SubmissionResponseDto> getSubmission(@PathVariable Long courseId,
@PathVariable Long assignmentId, @PathVariable Long submissionId) {
return ResponseEntity.ok(submissionService.getSubmissionById(courseId, assignmentId, submissionId));
}

@PreAuthorize("hasRole('INSTRUCTOR')")
@GetMapping("/list")
public ResponseEntity<List<SubmissionResponseDto>> getAllSubmissions(
@PathVariable Long courseId,
@PathVariable Long assignmentId,
@CurrentUserEmail String email) { // 강사 권한 검증을 위해 추가
public ResponseEntity<List<SubmissionResponseDto>> getAllSubmissions(@PathVariable Long courseId,
@PathVariable Long assignmentId, @CurrentUserEmail String email) {

// 강사가 해당 강의의 담당자인지 확인
if (!courseRepository.existsByCourseIdAndInstructorEmail(courseId, email)) {
throw new IllegalArgumentException("해당 강의의 조회 권한이 없습니다.");
}
// email -> id 변환 후 권한 검증
Instructor instructor = instructorRepository.findByEmail(email)
.orElseThrow(() -> new BusinessBaseException(ErrorCode.INSTRUCTOR_NOT_FOUND));

// CourseAccessValidator를 통한 권한 검증
courseAccessValidator.validateInstructorAccess(courseId, instructor.getId());

List<SubmissionResponseDto> submissions = submissionService.getSubmissionsWithStudents(courseId, assignmentId);
return ResponseEntity.ok(submissions);
}

@PreAuthorize("hasRole('STUDENT')")
@GetMapping
public ResponseEntity<SubmissionResponseDto> getStudentSubmission(
@PathVariable Long courseId,
@PathVariable Long assignmentId,
@CurrentUserEmail String email) {
public ResponseEntity<SubmissionResponseDto> getStudentSubmission(@PathVariable Long courseId,
@PathVariable Long assignmentId, @CurrentUserEmail String email) {
Student student = studentRepository.findByEmail(email)
.orElseThrow(() -> new IllegalArgumentException("학생 정보를 찾을 수 없습니다."));

SubmissionResponseDto submission = submissionService.getStudentSubmission(
courseId, assignmentId, student.getId());
SubmissionResponseDto submission = submissionService.getStudentSubmission(courseId, assignmentId,
student.getId());

// 제출물이 없는 경우 404 대신 200 OK와 null을 반환
return ResponseEntity.ok(submission);
}

@PreAuthorize("hasRole('STUDENT')")
@PutMapping("/{submissionId}")
public ResponseEntity<SubmissionResponseDto> updateSubmission(
@PathVariable Long courseId,
@PathVariable Long assignmentId,
@PathVariable Long submissionId,
@ModelAttribute SubmissionRequestDto requestDto,
@CurrentUserEmail String email) {
public ResponseEntity<SubmissionResponseDto> updateSubmission(@PathVariable Long courseId,
@PathVariable Long assignmentId, @PathVariable Long submissionId,
@ModelAttribute SubmissionRequestDto requestDto, @CurrentUserEmail String email) {
Student student = studentRepository.findByEmail(email)
.orElseThrow(() -> new IllegalArgumentException("학생 정보를 찾을 수 없습니다."));

return ResponseEntity.ok(
submissionService.updateSubmission(courseId, assignmentId, submissionId,
requestDto, student.getId())
);
submissionService.updateSubmission(courseId, assignmentId, submissionId, requestDto, student.getId()));
}

@PreAuthorize("hasRole('INSTRUCTOR')")
@PutMapping("/{submissionId}/grade")
public ResponseEntity<SubmissionResponseDto> gradeSubmission(
@PathVariable Long courseId,
@PathVariable Long assignmentId,
@PathVariable Long submissionId,
@CurrentUserEmail String email,
public ResponseEntity<SubmissionResponseDto> gradeSubmission(@PathVariable Long courseId,
@PathVariable Long assignmentId, @PathVariable Long submissionId, @CurrentUserEmail String email,
@RequestBody GradeRequestDto gradeRequestDto) {
return ResponseEntity.ok(
submissionService.gradeSubmission(
submissionId,
gradeRequestDto,
email)
);
return ResponseEntity.ok(submissionService.gradeSubmission(submissionId, gradeRequestDto, email));
}

@PreAuthorize("hasRole('STUDENT')")
@DeleteMapping("/{submissionId}")
public ResponseEntity<Void> deleteSubmission(
@PathVariable Long courseId,
@PathVariable Long assignmentId,
@PathVariable Long submissionId,
@CurrentUserEmail String email) {
public ResponseEntity<Void> deleteSubmission(@PathVariable Long courseId, @PathVariable Long assignmentId,
@PathVariable Long submissionId, @CurrentUserEmail String email) {
Student student = studentRepository.findByEmail(email)
.orElseThrow(() -> new IllegalArgumentException("학생 정보를 찾을 수 없습니다."));

submissionService.deleteSubmission(courseId, assignmentId, submissionId, student.getId());
return ResponseEntity.ok().build();
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
import com.example.epari.course.dto.attendance.AttendanceUpdateDto;
import com.example.epari.course.service.AttendanceService;
import com.example.epari.global.annotation.CurrentUserEmail;
import com.example.epari.global.exception.BusinessBaseException;
import com.example.epari.global.exception.ErrorCode;
import com.example.epari.user.domain.Instructor;
import com.example.epari.user.repository.InstructorRepository;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
Expand All @@ -32,20 +36,19 @@ public class AttendanceController {

private final AttendanceService attendanceService;

private final InstructorRepository instructorRepository;

/**
* 특정 강의의 특정 날짜 출석 현황을 조회
*/
@GetMapping
public ResponseEntity<List<AttendanceResponseDto>> getAttendances(
@PathVariable Long courseId,
public ResponseEntity<List<AttendanceResponseDto>> getAttendances(@PathVariable Long courseId,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date,
@CurrentUserEmail String email
) {
List<AttendanceResponseDto> responses = attendanceService.getAttendances(
courseId,
email,
date
);
@CurrentUserEmail String email) {
Instructor instructor = instructorRepository.findByEmail(email)
.orElseThrow(() -> new BusinessBaseException(ErrorCode.INSTRUCTOR_NOT_FOUND));

List<AttendanceResponseDto> responses = attendanceService.getAttendances(courseId, instructor.getId(), date);

return ResponseEntity.ok(responses);
}
Expand All @@ -54,13 +57,13 @@ public ResponseEntity<List<AttendanceResponseDto>> getAttendances(
* 특정 날짜의 학생들 출석 상태를 일괄 수정
*/
@PatchMapping
public ResponseEntity<Void> updateAttendances(
@PathVariable Long courseId,
public ResponseEntity<Void> updateAttendances(@PathVariable Long courseId,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date,
@RequestBody @Valid List<AttendanceUpdateDto> request,
@CurrentUserEmail String email
) {
attendanceService.updateAttendances(courseId, email, date, request);
@RequestBody @Valid List<AttendanceUpdateDto> request, @CurrentUserEmail String email) {
Instructor instructor = instructorRepository.findByEmail(email)
.orElseThrow(() -> new BusinessBaseException(ErrorCode.INSTRUCTOR_NOT_FOUND));

attendanceService.updateAttendances(courseId, instructor.getId(), date, request);

return ResponseEntity.ok().build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,16 @@
import com.example.epari.course.dto.attendance.AttendanceStatResponseDto;
import com.example.epari.course.service.AttendanceStatisticsService;
import com.example.epari.global.annotation.CurrentUserEmail;
import com.example.epari.global.exception.BusinessBaseException;
import com.example.epari.global.exception.ErrorCode;
import com.example.epari.user.domain.Instructor;
import com.example.epari.user.repository.InstructorRepository;

import lombok.RequiredArgsConstructor;

/**
* 설명: 강의별 학생 출석 통계를 조회하는 REST API 컨트롤러 구현
*/
*/

@RestController
@RequestMapping("/api/courses/{courseId}/stats")
Expand All @@ -26,14 +30,18 @@ public class AttendanceStatisticsController {

private final AttendanceStatisticsService attendanceStatisticsService;

private final InstructorRepository instructorRepository;

@GetMapping
@PreAuthorize("hasRole('INSTRUCTOR')")
public ResponseEntity<List<AttendanceStatResponseDto>> getStudentAttendanceStats(
@PathVariable Long courseId,
public ResponseEntity<List<AttendanceStatResponseDto>> getStudentAttendanceStats(@PathVariable Long courseId,
@CurrentUserEmail String email) {

List<AttendanceStatResponseDto> stats =
attendanceStatisticsService.getStudentAttendanceStats(courseId, email);
Instructor instructor = instructorRepository.findByEmail(email)
.orElseThrow(() -> new BusinessBaseException(ErrorCode.INSTRUCTOR_NOT_FOUND));

List<AttendanceStatResponseDto> stats = attendanceStatisticsService.getStudentAttendanceStats(courseId,
instructor.getId());
return ResponseEntity.ok(stats);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,35 +35,16 @@ public interface CourseRepository extends JpaRepository<Course, Long> {
List<Course> findAllByInstructorId(@Param("instructorId") Long instructorId);

/**
* 주어진 강의 ID와 강사 이메일로 해당 강의가 해당 강사의 강의인지 확인합니다.
* 강사의 강의 접근 권한 확인
*/
@Query("""
SELECT EXISTS (
SELECT 1
FROM Course c
WHERE c.id = :courseId
AND c.instructor.email = :instructorEmail
)
""")
boolean existsByCourseIdAndInstructorEmail(
@Param("courseId") Long courseId,
@Param("instructorEmail") String instructorEmail);
@Query("SELECT EXISTS (SELECT 1 FROM Course c WHERE c.id = :courseId AND c.instructor.id = :instructorId)")
boolean existsByCourseIdAndInstructorId(@Param("courseId") Long courseId, @Param("instructorId") Long instructorId);

/**
* 주어진 강의 ID와 학생 이메일로 해당 강의를 수강중인 학생인지 확인합니다.
* 학생의 강의 접근 권한 확인
*/
@Query("""
SELECT EXISTS (
SELECT 1
FROM Course c
JOIN CourseStudent cs ON c.id = cs.course.id
WHERE c.id = :courseId
AND cs.student.email = :studentEmail
)
""")
boolean existsByCourseIdAndStudentEmail(
@Param("courseId") Long courseId,
@Param("studentEmail") String studentEmail);
@Query("SELECT EXISTS (SELECT 1 FROM Course c JOIN c.courseStudents cs WHERE c.id = :courseId AND cs.student.id = :studentId)")
boolean existsByCourseIdAndStudentId(@Param("courseId") Long courseId, @Param("studentId") Long studentId);

@Query("SELECT c FROM Course c " +
"INNER JOIN FETCH c.instructor i " +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ public class AttendanceService {
* 해당 날짜의 출석 데이터가 없는 경우, 수강생 전체의 출석 데이터를 새로 생성
*/
@Transactional
public List<AttendanceResponseDto> getAttendances(Long courseId, String instructorEmail, LocalDate date) {
courseAccessValidator.validateInstructorAccess(courseId, instructorEmail);
public List<AttendanceResponseDto> getAttendances(Long courseId, Long instructorId, LocalDate date) {
courseAccessValidator.validateInstructorAccess(courseId, instructorId);

List<Attendance> attendances = attendanceRepository.findAllByCourseIdAndDate(courseId, date);

Expand All @@ -57,12 +57,12 @@ public List<AttendanceResponseDto> getAttendances(Long courseId, String instruct
@Transactional
public void updateAttendances(
Long courseId,
String instructorEmail,
Long instructorId,
LocalDate date,
List<AttendanceUpdateDto> updates
) {
// 강사 권한 검증
courseAccessValidator.validateInstructorAccess(courseId, instructorEmail);
courseAccessValidator.validateInstructorAccess(courseId, instructorId);

// 수정할 학생 ID 목록 추출
List<Long> studentIds = updates.stream()
Expand Down
Loading