diff --git a/backend/src/main/java/com/example/Piroin/project/domain/assignment/dto/CreateAssignmentRequest.java b/backend/src/main/java/com/example/Piroin/project/domain/assignment/dto/CreateAssignmentRequest.java index 70e6ebd..adec845 100644 --- a/backend/src/main/java/com/example/Piroin/project/domain/assignment/dto/CreateAssignmentRequest.java +++ b/backend/src/main/java/com/example/Piroin/project/domain/assignment/dto/CreateAssignmentRequest.java @@ -7,7 +7,6 @@ @Getter public class CreateAssignmentRequest { - private Integer assignmentId; private String title; diff --git a/backend/src/main/java/com/example/Piroin/project/domain/assignment/enums/AssignmentStatus.java b/backend/src/main/java/com/example/Piroin/project/domain/assignment/enums/AssignmentStatus.java index 49315aa..e3902db 100644 --- a/backend/src/main/java/com/example/Piroin/project/domain/assignment/enums/AssignmentStatus.java +++ b/backend/src/main/java/com/example/Piroin/project/domain/assignment/enums/AssignmentStatus.java @@ -1,8 +1,14 @@ package com.example.Piroin.project.domain.assignment.enums; public enum AssignmentStatus { - SUCCESS, - INSUFFICIENT, - FAILURE, - PENDING + + SUCCESS, // 정상 제출 (0원) + + INSUFFICIENT_MINOR, // 경미한 불충분 (-10000) + + INSUFFICIENT_MAJOR, // 심각한 불충분 (-20000) + + FAILURE, // 미제출 (-20000) + + PENDING // 아직 채점 안 됨 } diff --git a/backend/src/main/java/com/example/Piroin/project/domain/assignment/repository/AssignmentItemRepository.java b/backend/src/main/java/com/example/Piroin/project/domain/assignment/repository/AssignmentItemRepository.java index ceb82c8..9b2be1e 100644 --- a/backend/src/main/java/com/example/Piroin/project/domain/assignment/repository/AssignmentItemRepository.java +++ b/backend/src/main/java/com/example/Piroin/project/domain/assignment/repository/AssignmentItemRepository.java @@ -18,4 +18,6 @@ List findByUserIdAndAssignmentWeek( ); Optional findById(Integer id); + + List findByUserId(Long userId); } diff --git a/backend/src/main/java/com/example/Piroin/project/domain/attendance/controller/AdminAttendanceController.java b/backend/src/main/java/com/example/Piroin/project/domain/attendance/controller/AdminAttendanceController.java index 72d9204..bb1f97c 100644 --- a/backend/src/main/java/com/example/Piroin/project/domain/attendance/controller/AdminAttendanceController.java +++ b/backend/src/main/java/com/example/Piroin/project/domain/attendance/controller/AdminAttendanceController.java @@ -67,21 +67,20 @@ public String expireActiveAttendance() { return attendanceService.expireActiveAttendanceCode(); } - // 4. 출석 상태 변경 (관리자 전용) - // 현재는 출석만 변경되지만 나중에 출석 & 과제 변경으로 바꿀 예정 - @Operation(summary = "출석 상태 변경", description = "관리자가 특정 사용자의 출석 상태를 변경합니다.") - @ApiResponses(value = { - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "출석 상태 변경 성공"), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "잘못된 요청"), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "출석 기록을 찾을 수 없음") - }) - - @PutMapping("/admin/users/{userId}/status") - public boolean updateUserStatus( - @Parameter(description = "사용자 ID", example = "1") - @PathVariable Integer userId, - @RequestBody UpdateUserStatusReq req) { - return attendanceService.updateUserStatus(userId, req); - } +// // 4. 출석 상태 변경 (관리자 전용) +// // 현재는 출석만 변경되지만 나중에 출석 & 과제 변경으로 바꿀 예정 +// @Operation(summary = "출석 상태 변경", description = "관리자가 특정 사용자의 출석 상태를 변경합니다.") +// @ApiResponses(value = { +// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "출석 상태 변경 성공"), +// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "잘못된 요청"), +// @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "출석 기록을 찾을 수 없음") +// }) +// @PutMapping("/admin/users/{userId}/status") +// public boolean updateUserStatus( +// @Parameter(description = "사용자 ID", example = "1") +// @PathVariable Integer userId, +// @RequestBody UpdateUserStatusReq req) { +// return attendanceService.updateUserStatus(userId, req); +// } } diff --git a/backend/src/main/java/com/example/Piroin/project/domain/attendance/repository/AttendanceRepository.java b/backend/src/main/java/com/example/Piroin/project/domain/attendance/repository/AttendanceRepository.java index c386e4b..2f3b99f 100644 --- a/backend/src/main/java/com/example/Piroin/project/domain/attendance/repository/AttendanceRepository.java +++ b/backend/src/main/java/com/example/Piroin/project/domain/attendance/repository/AttendanceRepository.java @@ -16,10 +16,6 @@ public interface AttendanceRepository extends JpaRepository { Optional findById(Integer id); - // List findByUserId(Long userId); - - //Optional findByUserIdAndStudySessionId(Integer userId, Long studySessionId); - // 연관관계 필드명이 attendanceCode 라면 내부 ID인 Id를 조합하여 명명 Optional findByUserIdAndAttendanceCodeId(Long userId, Long attendanceCodeId); @@ -43,6 +39,8 @@ Optional findByUserIdAndAttendanceCodeId( Integer attendanceCodeId ); + List findByAttendanceCodeId(Integer id); + // 특정 날짜에 발급된 출석 코드의 개수를 세는 메서드 //long countByAttendanceDate(String attendanceDate); diff --git a/backend/src/main/java/com/example/Piroin/project/domain/attendance/service/AttendanceService.java b/backend/src/main/java/com/example/Piroin/project/domain/attendance/service/AttendanceService.java index 1f07b2b..989e3d1 100644 --- a/backend/src/main/java/com/example/Piroin/project/domain/attendance/service/AttendanceService.java +++ b/backend/src/main/java/com/example/Piroin/project/domain/attendance/service/AttendanceService.java @@ -66,6 +66,18 @@ public AttendanceCode generateCodeAndCreateAttendances(LocalDate date) { // [수 activeCode.expire(); } + for (AttendanceCode activeCode : activeCodes) { + activeCode.expire(); + + List attendances = + attendanceRepository.findByAttendanceCodeId(activeCode.getId()); + + for (Attendance attendance : attendances) { + depositService.recalculateDeposit(attendance.getUser().getId()); + } + } + + // 4. 4자리 랜덤 코드 생성 및 차수(Order) 계산 String code = String.valueOf(ThreadLocalRandom.current().nextInt(1000, 10000)); String attendanceOrder = String.valueOf(codeCountOfDay + 1); // 1회차, 2회차, 3회차 @@ -226,46 +238,46 @@ public List findByUserId(Integer userId) { .sorted(Comparator.comparing(AttendanceStatusRes::getDate).reversed()) .toList(); } - - // 6. 유저 상태 변경 (관리자) - // 컨트롤러 부분은 출석만 받는데 여기는 출석&과제 둘 다 받아서 추후에 수정 예정 - @Transactional - public boolean updateUserStatus(Integer userId, UpdateUserStatusReq req) { - boolean updated = false; - - // 출석 상태 변경 코드 - if (req.getAttendanceId() != null && req.getAttendanceStatus() != null) { - Attendance attendance = attendanceRepository.findById(req.getAttendanceId()) - .orElseThrow(() -> new IllegalArgumentException("출석 기록을 찾을 수 없습니다.")); - - if (!attendance.getUser().getId().equals(userId)) { - throw new IllegalArgumentException("요청된 사용자와 출석 기록의 사용자가 일치하지 않습니다."); - } - - attendance.updateStatus(req.getAttendanceStatus()); - updated = true; - } - - // 과제 상태 변경 코드 - if (req.getAssignmentItemId() != null && req.getAssignmentStatus() != null) { - AssignmentItem assignmentItem = assignmentItemRepository.findById(Math.toIntExact(req.getAssignmentItemId())) - .orElseThrow(() -> new IllegalArgumentException("과제 기록을 찾을 수 없습니다.")); - - if (!assignmentItem.getUser().getId().equals(userId)) { - throw new IllegalArgumentException("요청된 사용자와 과제 기록의 사용자가 일치하지 않습니다."); - } - - assignmentItem.updateSubmitted(req.getAssignmentStatus()); - updated = true; - } - - // 출석 변경 → 보증금 재계산 (과제 변경도 포함이 되어 있나..?) - if (updated) { - depositService.recalculateDeposit(Long.valueOf(userId)); - } - - return updated; - } +// +// // 6. 유저 상태 변경 (관리자) +// // 컨트롤러 부분은 출석만 받는데 여기는 출석&과제 둘 다 받아서 추후에 수정 예정 +// @Transactional +// public boolean updateUserStatus(Integer userId, UpdateUserStatusReq req) { +// boolean updated = false; +// +// // 출석 상태 변경 코드 +// if (req.getAttendanceId() != null && req.getAttendanceStatus() != null) { +// Attendance attendance = attendanceRepository.findById(req.getAttendanceId()) +// .orElseThrow(() -> new IllegalArgumentException("출석 기록을 찾을 수 없습니다.")); +// +// if (!attendance.getUser().getId().equals(userId)) { +// throw new IllegalArgumentException("요청된 사용자와 출석 기록의 사용자가 일치하지 않습니다."); +// } +// +// attendance.updateStatus(req.getAttendanceStatus()); +// updated = true; +// } +// +// // 과제 상태 변경 코드 +// if (req.getAssignmentItemId() != null && req.getAssignmentStatus() != null) { +// AssignmentItem assignmentItem = assignmentItemRepository.findById(Math.toIntExact(req.getAssignmentItemId())) +// .orElseThrow(() -> new IllegalArgumentException("과제 기록을 찾을 수 없습니다.")); +// +// if (!assignmentItem.getUser().getId().equals(userId)) { +// throw new IllegalArgumentException("요청된 사용자와 과제 기록의 사용자가 일치하지 않습니다."); +// } +// +// assignmentItem.updateSubmitted(req.getAssignmentStatus()); +// updated = true; +// } +// +// // 출석 변경 → 보증금 재계산 (과제 변경도 포함이 되어 있나..?) +// if (updated) { +// depositService.recalculateDeposit(Long.valueOf(userId)); +// } +// +// return updated; +// } } diff --git a/backend/src/main/java/com/example/Piroin/project/domain/curriculum/exception/code/CurriculumErrorCode.java b/backend/src/main/java/com/example/Piroin/project/domain/curriculum/exception/code/CurriculumErrorCode.java index b7b07ec..07258ef 100644 --- a/backend/src/main/java/com/example/Piroin/project/domain/curriculum/exception/code/CurriculumErrorCode.java +++ b/backend/src/main/java/com/example/Piroin/project/domain/curriculum/exception/code/CurriculumErrorCode.java @@ -29,7 +29,7 @@ public enum CurriculumErrorCode { SESSION_DATE_NOT_FOUND( HttpStatus.BAD_REQUEST, "CURRICULUM405", - "해당 날짜의 세션이 없습니다. 세션을 먼저 생성해주세요." + "해당 주차/요일의 세션이 존재하지 않습니다. 세션을 먼저 생성해주세요." ); private final HttpStatus status; diff --git a/backend/src/main/java/com/example/Piroin/project/domain/deposit/entity/Deposit.java b/backend/src/main/java/com/example/Piroin/project/domain/deposit/entity/Deposit.java index 2fee4c3..38a958d 100644 --- a/backend/src/main/java/com/example/Piroin/project/domain/deposit/entity/Deposit.java +++ b/backend/src/main/java/com/example/Piroin/project/domain/deposit/entity/Deposit.java @@ -40,6 +40,7 @@ public class Deposit { @Column(name = "ascent_defence", nullable = false) private Integer ascentDefence; + // 출석 차감액만 새로 계산할 때 사용 public void updateAttendanceAmount(Integer descentAttendance) { this.descentAttendance = descentAttendance; @@ -51,6 +52,22 @@ public void updateAttendanceAmount(Integer descentAttendance) { + this.ascentDefence; } + // 과제 차감 + 출석 차감을 한 번에 재계산할 때 사용 + public void updateDepositAmount( + Integer descentAssignment, + Integer descentAttendance + ) { + this.descentAssignment = descentAssignment; + this.descentAttendance = descentAttendance; + + int baseAmount = 100_000; + + this.amount = baseAmount + - this.descentAssignment + - this.descentAttendance + + this.ascentDefence; + } + } diff --git a/backend/src/main/java/com/example/Piroin/project/domain/deposit/service/DepositService.java b/backend/src/main/java/com/example/Piroin/project/domain/deposit/service/DepositService.java index 404b437..b620b11 100644 --- a/backend/src/main/java/com/example/Piroin/project/domain/deposit/service/DepositService.java +++ b/backend/src/main/java/com/example/Piroin/project/domain/deposit/service/DepositService.java @@ -22,7 +22,7 @@ public class DepositService { private final UserRepository userRepository; private final AttendanceRepository attendanceRepository; - // 보증금 재계산 로직 (운영진이 출석/과제 여부 수정 시) + // 1. 보증금 재계산 로직 (운영진이 출석/과제 여부 수정 시, 출석코드 만료 시) @Transactional public void recalculateDeposit(Long userId) { User user = userRepository.findById(userId) @@ -37,7 +37,7 @@ public void recalculateDeposit(Long userId) { deposit.updateAttendanceAmount(descentAttendance); } - // 보증금 조회 로직 + // 2. 보증금 조회 로직 public DepositResponse getMyDeposit(Long userId) { Deposit deposit = depositRepository.findByUserId(userId) diff --git a/backend/src/main/java/com/example/Piroin/project/domain/user/service/AdminUserService.java b/backend/src/main/java/com/example/Piroin/project/domain/user/service/AdminUserService.java index 6cfa315..4e923a8 100644 --- a/backend/src/main/java/com/example/Piroin/project/domain/user/service/AdminUserService.java +++ b/backend/src/main/java/com/example/Piroin/project/domain/user/service/AdminUserService.java @@ -2,12 +2,15 @@ import com.example.Piroin.project.domain.assignment.entity.Assignment; import com.example.Piroin.project.domain.assignment.entity.AssignmentItem; +import com.example.Piroin.project.domain.assignment.enums.AssignmentStatus; import com.example.Piroin.project.domain.assignment.repository.AssignmentItemRepository; import com.example.Piroin.project.domain.assignment.repository.AssignmentRepository; import com.example.Piroin.project.domain.attendance.entity.Attendance; import com.example.Piroin.project.domain.attendance.repository.AttendanceCodeRepository; import com.example.Piroin.project.domain.attendance.repository.AttendanceRepository; import com.example.Piroin.project.domain.curriculum.repository.CurriculumRepository; +import com.example.Piroin.project.domain.deposit.entity.Deposit; +import com.example.Piroin.project.domain.deposit.repository.DepositRepository; import com.example.Piroin.project.domain.user.dto.*; import com.example.Piroin.project.domain.user.entity.User; import com.example.Piroin.project.domain.user.enums.Role; @@ -29,6 +32,7 @@ public class AdminUserService { private final UserService userService; private final AssignmentItemRepository assignmentItemRepository; private final AttendanceRepository attendanceRepository; + private final DepositRepository depositRepository; private final CurriculumRepository curriculumRepository; private final AssignmentRepository assignmentRepository; private final AttendanceCodeRepository attendanceCodeRepository; @@ -69,37 +73,31 @@ public UpdateStudentStatusResponse updateStudentWeekStatus( ) { User user = userRepository.findById(userId) - .orElseThrow(() -> - new RuntimeException("사용자가 존재하지 않습니다.") - ); + .orElseThrow(() -> new RuntimeException("사용자가 존재하지 않습니다.")); - // 과제 상태 수정 if (request.getAssignments() != null) { - for (UpdateStudentStatusRequest.AssignmentStatusRequest dto : request.getAssignments()) { + // 해당 assignmentItem 존재하지 않을 때 AssignmentItem assignmentItem = assignmentItemRepository.findById(dto.getAssignmentItemId()) .orElseThrow(() -> new RuntimeException("과제 정보가 존재하지 않습니다.") ); - // 본인 데이터 검증 + // assignmentItem가 userId의 과제가 아닐 경우 if (!assignmentItem.getUser().getId().equals(userId)) { throw new RuntimeException("해당 유저의 과제가 아닙니다."); } - // PATCH 방식 -> null이면 수정 안 함 if (dto.getSubmitted() != null) { assignmentItem.updateSubmitted(dto.getSubmitted()); } } } - // 출석 상태 수정 if (request.getAttendances() != null) { - for (UpdateStudentStatusRequest.AttendanceStatusRequest dto : request.getAttendances()) { @@ -109,18 +107,19 @@ public UpdateStudentStatusResponse updateStudentWeekStatus( new RuntimeException("출석 정보가 존재하지 않습니다.") ); - // 본인 데이터 검증 + // assignmentId가 userId의 것이 아닐 때 if (!attendance.getUser().getId().equals(userId)) { throw new RuntimeException("해당 유저의 출석이 아닙니다."); } - // PATCH 방식 if (dto.getStatus() != null) { attendance.updateStatus(dto.getStatus()); } } } + recalculateDeposit(userId); + return new UpdateStudentStatusResponse( userId, week, @@ -128,99 +127,49 @@ public UpdateStudentStatusResponse updateStudentWeekStatus( ); } -// // 5. 부원 출석/과제 상태 조회 -// @Transactional(readOnly = true) -// public StudentWeeklyStatusResponse getStudentWeeklyStatus( -// Long userId, -// Long week -// ) { -// -// List sessions = -// curriculumRepository.findByWeek(week); -// -// List dayResponses = new ArrayList<>(); -// -// for (StudySession session : sessions) { -// -// LocalDate sessionDate = session.getSessionDate(); -// -// String day = -// sessionDate.getDayOfWeek().toString(); -// -// /* -// * 과제 조회 -// */ -// List assignments = -// assignmentRepository.findBySessionDate(sessionDate); -// -// List assignmentResponses = -// assignments.stream() -// .map(assignment -> { -// -// AssignmentItem item = -// assignmentItemRepository -// .findByUserIdAndAssignmentId( -// userId, -// assignment.getId() -// ) -// .orElse(null); -// -// String submitted = -// item == null -// ? "PENDING" -// : item.getSubmitted().name(); -// -// return AssignmentStatusResponse.builder() -// .assignmentId(assignment.getId()) -// .title(assignment.getTitle()) -// .submitted(submitted) -// .build(); -// }) -// .toList(); -// -// /* -// * 출석 조회 -// */ -// List attendanceCodes = -// attendanceCodeRepository.findByAttendanceDate(sessionDate); -// -// List attendanceResponses = -// attendanceCodes.stream() -// .map(code -> { -// -// Attendance attendance = -// attendanceRepository -// .findByUserIdAndAttendanceCodeId( -// userId, -// code.getId() -// ) -// .orElse(null); -// -// boolean attended = -// attendance != null && -// attendance.getStatus(); -// -// return AttendanceStatusResponse.builder() -// .attendanceCodeId(code.getId()) -// .attendanceOrder(code.getAttendanceOrder()) -// .attended(attended) -// .build(); -// }) -// .toList(); -// -// dayResponses.add( -// DayStatusResponse.builder() -// .day(day) -// .sessionDate(sessionDate) -// .assignments(assignmentResponses) -// .attendances(attendanceResponses) -// .build() -// ); -// } -// -// return StudentWeeklyStatusResponse.builder() -// .week(week) -// .days(dayResponses) -// .build(); -// } + + // 4. 보증금 재계산 메소드(출석 & 과제 공통) + private void recalculateDeposit(Long userId) { + + Deposit deposit = depositRepository.findByUserId(userId) + .orElseThrow(() -> new RuntimeException("보증금 정보가 존재하지 않습니다.")); + + List assignmentItems = + assignmentItemRepository.findByUserId(userId); + + int assignmentPenalty = assignmentItems.stream() + .mapToInt(item -> calculateAssignmentPenalty(item.getSubmitted())) + .sum(); + + List attendances = + attendanceRepository.findByUserId(userId); + + int attendancePenalty = attendances.stream() + .mapToInt(attendance -> attendance.getStatus() ? 0 : 10_000) + .sum(); + + deposit.updateDepositAmount( + assignmentPenalty, + attendancePenalty + ); + } + + // 5. 과제에 대한 보증금 계산 로직 + private int calculateAssignmentPenalty(AssignmentStatus status) { + + return switch (status) { + + case SUCCESS, PENDING -> 0; + + case INSUFFICIENT_MINOR -> 10_000; + + case INSUFFICIENT_MAJOR -> 20_000; + + case FAILURE -> 30_000; + }; + } + + // 6. 출석에 대한 보증금 계산 로직 + // (AttendanceService에 있음!!) + } \ No newline at end of file diff --git a/backend/src/main/resources/db/migration/V4__add_assignment_status.sql b/backend/src/main/resources/db/migration/V4__add_assignment_status.sql new file mode 100644 index 0000000..dc1b61a --- /dev/null +++ b/backend/src/main/resources/db/migration/V4__add_assignment_status.sql @@ -0,0 +1,14 @@ +ALTER TABLE assignment_item + DROP CONSTRAINT chk_assignment_item_submitted; + +ALTER TABLE assignment_item + ADD CONSTRAINT chk_assignment_item_submitted + CHECK ( + submitted IN ( + 'SUCCESS', + 'INSUFFICIENT_MINOR', + 'INSUFFICIENT_MAJOR', + 'FAILURE', + 'PENDING' + ) + ); \ No newline at end of file