diff --git a/backend/src/main/java/com/example/Piroin/project/domain/assignment/controller/AssignmentController.java b/backend/src/main/java/com/example/Piroin/project/domain/assignment/controller/AssignmentController.java index 136d34d..0ab1b60 100644 --- a/backend/src/main/java/com/example/Piroin/project/domain/assignment/controller/AssignmentController.java +++ b/backend/src/main/java/com/example/Piroin/project/domain/assignment/controller/AssignmentController.java @@ -32,11 +32,10 @@ public CreateAssignmentResponse createAssignment( // 2. 과제 수정 @Operation(summary = "과제 수정", description = "과제에 대한 과제명/주차/날짜를 운영진이 수정합니다.") @PatchMapping("/modify/{assignmentId}") - @ResponseStatus(HttpStatus.OK) public ModifyAssignmentResponse modifyAssignment( @PathVariable Integer assignmentId, @RequestBody ModifyAssignmentRequest request - ){ + ) { return assignmentService.modifyAssignment(assignmentId, request); } 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 a08d685..70e6ebd 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 @@ -2,6 +2,7 @@ import lombok.Getter; +import java.time.DayOfWeek; import java.time.LocalDate; @Getter @@ -12,5 +13,5 @@ public class CreateAssignmentRequest { private String week; - private LocalDate sessionDate; + private DayOfWeek day; } diff --git a/backend/src/main/java/com/example/Piroin/project/domain/assignment/dto/ModifyAssignmentRequest.java b/backend/src/main/java/com/example/Piroin/project/domain/assignment/dto/ModifyAssignmentRequest.java index 0595cec..b13024a 100644 --- a/backend/src/main/java/com/example/Piroin/project/domain/assignment/dto/ModifyAssignmentRequest.java +++ b/backend/src/main/java/com/example/Piroin/project/domain/assignment/dto/ModifyAssignmentRequest.java @@ -1,6 +1,8 @@ package com.example.Piroin.project.domain.assignment.dto; import lombok.Getter; + +import java.time.DayOfWeek; import java.time.LocalDate; @Getter @@ -9,5 +11,5 @@ public class ModifyAssignmentRequest { private String week; - private LocalDate sessionDate; + private DayOfWeek day; } 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 acb02d2..ceb82c8 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 @@ -3,6 +3,7 @@ import com.example.Piroin.project.domain.assignment.entity.AssignmentItem; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; import java.util.Optional; public interface AssignmentItemRepository extends JpaRepository { @@ -10,4 +11,11 @@ public interface AssignmentItemRepository extends JpaRepository findByUserIdAndAssignmentId(Long userId, Integer assignmentId); + + List findByUserIdAndAssignmentWeek( + Long userId, + String week + ); + + Optional findById(Integer id); } diff --git a/backend/src/main/java/com/example/Piroin/project/domain/assignment/repository/AssignmentRepository.java b/backend/src/main/java/com/example/Piroin/project/domain/assignment/repository/AssignmentRepository.java index 16c5917..19f69bc 100644 --- a/backend/src/main/java/com/example/Piroin/project/domain/assignment/repository/AssignmentRepository.java +++ b/backend/src/main/java/com/example/Piroin/project/domain/assignment/repository/AssignmentRepository.java @@ -3,9 +3,12 @@ import com.example.Piroin.project.domain.assignment.entity.Assignment; import org.springframework.data.jpa.repository.JpaRepository; +import java.time.LocalDate; import java.util.List; public interface AssignmentRepository extends JpaRepository { List findByWeekOrderBySessionDateAsc(String week); + + List findBySessionDate(LocalDate sessionDate); } diff --git a/backend/src/main/java/com/example/Piroin/project/domain/assignment/service/AssignmentService.java b/backend/src/main/java/com/example/Piroin/project/domain/assignment/service/AssignmentService.java index 524bc60..6c0af51 100644 --- a/backend/src/main/java/com/example/Piroin/project/domain/assignment/service/AssignmentService.java +++ b/backend/src/main/java/com/example/Piroin/project/domain/assignment/service/AssignmentService.java @@ -9,13 +9,27 @@ import com.example.Piroin.project.domain.assignment.exception.code.AssignmentErrorCode; 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.entity.AttendanceCode; +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.entity.StudySession; +import com.example.Piroin.project.domain.curriculum.exception.CurriculumException; +import com.example.Piroin.project.domain.curriculum.exception.code.CurriculumErrorCode; +import com.example.Piroin.project.domain.curriculum.repository.CurriculumRepository; +import com.example.Piroin.project.domain.curriculum.service.CurriculumService; +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; import com.example.Piroin.project.domain.user.repository.UserRepository; -import jakarta.transaction.Transactional; +import com.example.Piroin.project.domain.user.service.UserService; +import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import java.time.DayOfWeek; +import java.time.LocalDate; +import java.util.ArrayList; import java.util.List; @Service @@ -26,19 +40,52 @@ public class AssignmentService { private final AssignmentRepository assignmentRepository; private final AssignmentItemRepository assignmentItemRepository; private final UserRepository userRepository; + private final AttendanceRepository attendanceRepository; + private final CurriculumRepository curriculumRepository; + private final AttendanceCodeRepository attendanceCodeRepository; // 1. 과제 생성 - public CreateAssignmentResponse createAssignment(CreateAssignmentRequest request) { + @Transactional + public CreateAssignmentResponse createAssignment( + CreateAssignmentRequest request + ) { + + // week 문자열 -> Long 변환 + Long week = Long.valueOf(request.getWeek()); + + // 해당 주차 세션들 조회 + List sessions = + curriculumRepository.findByWeek(week); + + // 요청한 요일과 일치하는 세션 찾기 + StudySession matchedSession = sessions.stream() + .filter(session -> + session.getSessionDate().getDayOfWeek() + == request.getDay() + ) + .findFirst() + .orElseThrow(() -> + new CurriculumException( + CurriculumErrorCode.SESSION_DATE_NOT_FOUND + )); + + // 실제 날짜 추출 + LocalDate sessionDate = matchedSession.getSessionDate(); + // Assignment 생성 Assignment assignment = Assignment.builder() .title(request.getTitle()) .week(request.getWeek()) - .sessionDate(request.getSessionDate()) + .sessionDate(sessionDate) .build(); assignmentRepository.save(assignment); - List users = userRepository.findAll(); + + // 생성한 과제로 모든 부원에게 assignmentItem 생성하기 + // ADMIN 제외 MEMBER만 조회 추천 + List users = + userRepository.findByRole(Role.MEMBER); List assignmentItems = users.stream() .map(user -> AssignmentItem.builder() @@ -53,23 +100,65 @@ public CreateAssignmentResponse createAssignment(CreateAssignmentRequest request return new CreateAssignmentResponse(assignment.getId()); } + // 2. 과제 수정 - public ModifyAssignmentResponse modifyAssignment(Integer assignmentId, - ModifyAssignmentRequest request) { + @Transactional + public ModifyAssignmentResponse modifyAssignment( + Integer assignmentId, + ModifyAssignmentRequest request + ) { Assignment assignment = assignmentRepository.findById(assignmentId) .orElseThrow(() -> new AssignmentException( AssignmentErrorCode.ASSIGNMENT_NOT_FOUND )); + /* + 1. 최종적으로 사용할 week 결정 + - request에 있으면 그 값 사용 + - 없으면 기존 assignment 값 사용 + */ + String finalWeek = request.getWeek() != null + ? request.getWeek() + : assignment.getWeek(); + + /* + 2. 최종적으로 사용할 day 결정 + - request에 있으면 그 값 사용 + - 없으면 기존 sessionDate의 요일 사용 + */ + DayOfWeek finalDay = request.getDay() != null + ? request.getDay() + : assignment.getSessionDate().getDayOfWeek(); + + /* + 3. week/day 조합으로 StudySession 조회해서 + 새로운 sessionDate 계산 + */ + + // request에서 보낸 주차에 해당하는 세션들을 전부 찾아 리스트에 저장. + List weekSessions = curriculumRepository.findByWeek(Long.parseLong(finalWeek)); + + // request에서 보낸 요일에 해당하는 세션을 찾음. + StudySession studySession = weekSessions.stream() + .filter(s -> s.getSessionDate().getDayOfWeek() == finalDay) // 자바끼리 요일 비교 + .findFirst() + .orElseThrow(() -> new CurriculumException( + CurriculumErrorCode.STUDY_SESSION_NOT_FOUND + )); + + // 그 세션의 날짜를 추출. + LocalDate newSessionDate = studySession.getSessionDate(); + + /* + 4. 수정 적용 + */ assignment.update( request.getTitle(), - request.getWeek(), - request.getSessionDate() + finalWeek, + newSessionDate ); - assignmentRepository.save(assignment); - return new ModifyAssignmentResponse(assignment.getId()); } @@ -147,4 +236,106 @@ private String convertDay(DayOfWeek dayOfWeek) { }; } + + // 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() + .assignmentItemId( + item != null ? item.getId() : null + ) + .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() + .attendanceId( + attendance != null ? attendance.getId() : null + ) + .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(); + } } 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 7416663..72d9204 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 @@ -36,14 +36,12 @@ public class AdminAttendanceController { @PostMapping("/admin/attendance/start") public AttendanceCodeResponse startAttendance() { - // 1. 오늘 날짜 구하기 (LocalDate 활용) + // 오늘 날짜 LocalDate today = LocalDate.now(); - // 2. 서비스가 원하는 "yyyy-MM-dd" 형식의 문자열로 포맷팅 - String todayStr = today.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); - - // 3. 포맷팅된 오늘 날짜 문자열을 서비스에 넘겨줍니다. - AttendanceCode code = attendanceService.generateCodeAndCreateAttendances(todayStr); + // 서비스 호출 + AttendanceCode code = + attendanceService.generateCodeAndCreateAttendances(today); return AttendanceCodeResponse.from(code); } diff --git a/backend/src/main/java/com/example/Piroin/project/domain/attendance/entity/AttendanceCode.java b/backend/src/main/java/com/example/Piroin/project/domain/attendance/entity/AttendanceCode.java index efe7f82..2986adb 100644 --- a/backend/src/main/java/com/example/Piroin/project/domain/attendance/entity/AttendanceCode.java +++ b/backend/src/main/java/com/example/Piroin/project/domain/attendance/entity/AttendanceCode.java @@ -3,6 +3,8 @@ import jakarta.persistence.*; import lombok.*; +import java.time.LocalDate; + @Entity @Table(name = "attendance_code") @Getter @@ -16,7 +18,7 @@ public class AttendanceCode { private Integer id; // SERIAL 타입에 매칭 (Long -> Integer) @Column(name = "attendance_date") - private String attendanceDate; + private LocalDate attendanceDate; @Column(name = "attendance_order") private String attendanceOrder; // '1, 2, 3' 코멘트 항목 diff --git a/backend/src/main/java/com/example/Piroin/project/domain/attendance/repository/AttendanceCodeRepository.java b/backend/src/main/java/com/example/Piroin/project/domain/attendance/repository/AttendanceCodeRepository.java index 6002531..b407419 100644 --- a/backend/src/main/java/com/example/Piroin/project/domain/attendance/repository/AttendanceCodeRepository.java +++ b/backend/src/main/java/com/example/Piroin/project/domain/attendance/repository/AttendanceCodeRepository.java @@ -24,12 +24,14 @@ public interface AttendanceCodeRepository extends JpaRepository findByCodeAndStudySessionId(String code, Long studySessionId); // 특정 날짜에 발급된 코드 개수 조회 - long countByAttendanceDate(String attendanceDate); + long countByAttendanceDate(LocalDate attendanceDate); // 만료되지 않은 코드 목록 조회 List findByIsExpiredFalse(); Optional findByCode(String code); + + List findByAttendanceDate(LocalDate attendanceDate); } 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 f027731..c386e4b 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 @@ -14,6 +14,8 @@ public interface AttendanceRepository extends JpaRepository { + Optional findById(Integer id); + // List findByUserId(Long userId); //Optional findByUserIdAndStudySessionId(Integer userId, Long studySessionId); @@ -31,11 +33,16 @@ public interface AttendanceRepository extends JpaRepository { // 2. 특정 유저 ID와 출석 코드의 날짜 조건으로 조회 (엔티티 그래프 참조: attendanceCode.attendanceDate) @Query("SELECT a FROM Attendance a WHERE a.user.id = :userId AND a.attendanceCode.attendanceDate = :attendanceDate") - List findByUserIdAndDate(@Param("userId") Integer userId, @Param("attendanceDate") String attendanceDate); + List findByUserIdAndDate(@Param("userId") Integer userId, @Param("attendanceDate") LocalDate attendanceDate); // 3. 특정 유저의 모든 출석 데이터 조회 List findByUserId(Long userId); + Optional findByUserIdAndAttendanceCodeId( + Long userId, + Integer attendanceCodeId + ); + // 특정 날짜에 발급된 출석 코드의 개수를 세는 메서드 //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 fb1f923..1f07b2b 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 @@ -49,12 +49,12 @@ public class AttendanceService { // 1. 출석 시작 코드 (출석코드 생성 함수) @Transactional - public AttendanceCode generateCodeAndCreateAttendances(String dateStr) { // [수정] 세션 ID 대신 날짜를 직접 받음 + public AttendanceCode generateCodeAndCreateAttendances(LocalDate date) { // [수정] 세션 ID 대신 날짜를 직접 받음 // 1. [삭제] 더 이상 세션을 조회해서 날짜를 파싱할 필요가 없습니다. (curriculumRepository 조회 제거) // 2. 해당 날짜에 생성된 출석 코드 개수 조회 - long codeCountOfDay = attendanceCodeRepository.countByAttendanceDate(dateStr); + long codeCountOfDay = attendanceCodeRepository.countByAttendanceDate(date); if (codeCountOfDay >= 3) { throw new IllegalStateException("하루에 최대 3회까지만 출석 코드를 생성할 수 있습니다."); @@ -72,7 +72,7 @@ public AttendanceCode generateCodeAndCreateAttendances(String dateStr) { // [수 // 5. 새로운 AttendanceCode 생성 및 저장 AttendanceCode attendanceCode = AttendanceCode.builder() - .attendanceDate(dateStr) // [수정] 파라미터로 받은 날짜 주입 + .attendanceDate(date) // [수정] 파라미터로 받은 날짜 주입 .attendanceOrder(attendanceOrder) .code(code) .isExpired(false) @@ -181,7 +181,7 @@ public List findByUserIdAndDate(Integer userId, LocalDate dat // 변경된 구조: User ID와 AttendanceCode의 날짜 조건으로 조회 List attendances = - attendanceRepository.findByUserIdAndDate(userId, dateStr); + attendanceRepository.findByUserIdAndDate(userId, date); return attendances.stream() .map(attendance -> new AttendanceSlotRes( @@ -193,19 +193,21 @@ public List findByUserIdAndDate(Integer userId, LocalDate dat } // 6. 유저의 전체 출석 현황을 날짜별로 묶어서 조회하는 함수 - public List findByUserId(Integer userId) { // Long -> Integer - List attendances = attendanceRepository.findByUserId(Long.valueOf(userId)); + public List findByUserId(Integer userId) { - // 변경된 구조: AttendanceCode에 저장된 String 날짜를 기준으로 그룹화(groupingBy) - Map> grouped = attendances.stream() + List attendances = + attendanceRepository.findByUserId(Long.valueOf(userId)); + + // LocalDate 기준으로 그룹화 + Map> grouped = attendances.stream() .collect(Collectors.groupingBy( attendance -> attendance.getAttendanceCode().getAttendanceDate() )); return grouped.entrySet().stream() .map(entry -> { - // String으로 정렬/그룹화된 키를 다시 LocalDate 객체로 변환하여 DTO에 주입 - LocalDate date = LocalDate.parse(entry.getKey()); + + LocalDate date = entry.getKey(); List slots = entry.getValue().stream() .map(attendance -> new AttendanceSlotRes( @@ -221,7 +223,7 @@ public List findByUserId(Integer userId) { // Long -> Integ return dto; }) - .sorted(Comparator.comparing(AttendanceStatusRes::getDate).reversed()) // 최신날짜 순 정렬 + .sorted(Comparator.comparing(AttendanceStatusRes::getDate).reversed()) .toList(); } diff --git a/backend/src/main/java/com/example/Piroin/project/domain/curriculum/exception/CurriculumException.java b/backend/src/main/java/com/example/Piroin/project/domain/curriculum/exception/CurriculumException.java index 7e6b6e8..6e7d7db 100644 --- a/backend/src/main/java/com/example/Piroin/project/domain/curriculum/exception/CurriculumException.java +++ b/backend/src/main/java/com/example/Piroin/project/domain/curriculum/exception/CurriculumException.java @@ -1,5 +1,6 @@ package com.example.Piroin.project.domain.curriculum.exception; +import com.example.Piroin.project.domain.curriculum.exception.code.CurriculumErrorCode; import lombok.Getter; import org.springframework.http.HttpStatus; @@ -7,9 +8,17 @@ public class CurriculumException extends RuntimeException { private final HttpStatus status; + private final String code; public CurriculumException(HttpStatus status, String message) { super(message); this.status = status; + this.code = null; } -} + + public CurriculumException(CurriculumErrorCode curriculumErrorCode) { + super(curriculumErrorCode.getMessage()); + this.status = curriculumErrorCode.getStatus(); + this.code = curriculumErrorCode.getCode(); + } +} \ No newline at end of file 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 f4fdf2b..b7b07ec 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 @@ -1,6 +1,38 @@ package com.example.Piroin.project.domain.curriculum.exception.code; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor public enum CurriculumErrorCode { - USER_NOT_FOUND, - SESSION_NOT_FOUND -} + + USER_NOT_FOUND( + HttpStatus.NOT_FOUND, + "CURRICULUM401", + "사용자를 찾을 수 없습니다." + ), + + SESSION_NOT_FOUND( + HttpStatus.NOT_FOUND, + "CURRICULUM402", + "세션을 찾을 수 없습니다." + ), + + STUDY_SESSION_NOT_FOUND( + HttpStatus.NOT_FOUND, + "CURRICULUM404", + "해당 스터디 세션이 존재하지 않습니다." + ), + + SESSION_DATE_NOT_FOUND( + HttpStatus.BAD_REQUEST, + "CURRICULUM405", + "해당 날짜의 세션이 없습니다. 세션을 먼저 생성해주세요." + ); + + private final HttpStatus status; + private final String code; + private final String message; +} \ No newline at end of file diff --git a/backend/src/main/java/com/example/Piroin/project/domain/curriculum/repository/CurriculumRepository.java b/backend/src/main/java/com/example/Piroin/project/domain/curriculum/repository/CurriculumRepository.java index 8989cd9..fa17314 100644 --- a/backend/src/main/java/com/example/Piroin/project/domain/curriculum/repository/CurriculumRepository.java +++ b/backend/src/main/java/com/example/Piroin/project/domain/curriculum/repository/CurriculumRepository.java @@ -3,8 +3,12 @@ import com.example.Piroin.project.domain.curriculum.entity.StudySession; import com.example.Piroin.project.domain.curriculum.enums.SessionStatus; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import java.time.DayOfWeek; import java.util.List; +import java.util.Optional; /* StudySession(세션) DB 접근 인터페이스 @@ -15,4 +19,17 @@ public interface CurriculumRepository extends JpaRepository List findByStatusOrderBySessionDateAscDayPartAsc(SessionStatus status); List findByStatusOrderBySessionDateDescDayPartDesc(SessionStatus status); + + List findByWeek(Long week); + +// @Query(""" +// SELECT s +// FROM StudySession s +// WHERE s.week = :week +// AND FUNCTION('DAY_OF_WEEK', s.sessionDate) = :dayValue +// """) +// Optional findByWeekAndDay( +// @Param("week") Long week, +// @Param("dayValue") DayOfWeek dayValue +// ); } diff --git a/backend/src/main/java/com/example/Piroin/project/domain/user/controller/AdminUserController.java b/backend/src/main/java/com/example/Piroin/project/domain/user/controller/AdminUserController.java index 305487b..5d2dda7 100644 --- a/backend/src/main/java/com/example/Piroin/project/domain/user/controller/AdminUserController.java +++ b/backend/src/main/java/com/example/Piroin/project/domain/user/controller/AdminUserController.java @@ -1,8 +1,10 @@ package com.example.Piroin.project.domain.user.controller; -import com.example.Piroin.project.domain.user.dto.StudentListResponse; -import com.example.Piroin.project.domain.user.dto.StudentListResponse; +import com.example.Piroin.project.domain.assignment.service.AssignmentService; +import com.example.Piroin.project.domain.attendance.dto.ApiResponse; +import com.example.Piroin.project.domain.user.dto.*; import com.example.Piroin.project.domain.user.service.AdminUserService; +import com.example.Piroin.project.domain.user.service.UserService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; @@ -17,8 +19,9 @@ public class AdminUserController { private final AdminUserService adminUserService; + private final AssignmentService assignmentService; - // 전체 부원 목록 조회 + // 1. 전체 부원 목록 조회 @Operation(summary = "전체 부원 이름 목록 조회", description = "운영진이 전체 부원의 이름 목록을 조회합니다.") @GetMapping("/studentlist") public List getStudentList() { @@ -34,6 +37,44 @@ public List searchStudents( return adminUserService.searchStudents(name); } + // 2. 특정 부원의 과제/출석 정보 조회 + @Operation( + summary = "특정 학생 주간 과제/출석 조회", + description = "운영진이 특정 학생의 주차별 과제 및 출석 상태를 조회합니다." + ) + @GetMapping("/admin/student/{userId}/status/{week}") + public ApiResponse getStudentWeeklyStatus( + @PathVariable Long userId, + @PathVariable Long week + ) { + + return ApiResponse.success( + assignmentService.getStudentWeeklyStatus( + userId, + week + ) + ); + } + + // 3. 특정 부원의 과제/출석 상태 수정 (운영진) + @Operation( + summary = "특정 학생 주간 과제/출석 수정", + description = "운영진이 특정 학생의 주차별 과제 및 출석 상태를 수정합니다." + ) + @PatchMapping("/users/{userId}/weeks/{week}") + public UpdateStudentStatusResponse updateStudentWeekStatus( + @PathVariable Long userId, + @PathVariable Long week, + @RequestBody UpdateStudentStatusRequest request + ) { + + return adminUserService.updateStudentWeekStatus( + userId, + week, + request + ); + } + } \ No newline at end of file diff --git a/backend/src/main/java/com/example/Piroin/project/domain/user/dto/AssignmentStatusResponse.java b/backend/src/main/java/com/example/Piroin/project/domain/user/dto/AssignmentStatusResponse.java new file mode 100644 index 0000000..22d6300 --- /dev/null +++ b/backend/src/main/java/com/example/Piroin/project/domain/user/dto/AssignmentStatusResponse.java @@ -0,0 +1,15 @@ +package com.example.Piroin.project.domain.user.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@Builder +public class AssignmentStatusResponse { + private Integer assignmentItemId; + private Integer assignmentId; + private String title; + private String submitted; +} diff --git a/backend/src/main/java/com/example/Piroin/project/domain/user/dto/AttendanceStatusResponse.java b/backend/src/main/java/com/example/Piroin/project/domain/user/dto/AttendanceStatusResponse.java new file mode 100644 index 0000000..31c6f04 --- /dev/null +++ b/backend/src/main/java/com/example/Piroin/project/domain/user/dto/AttendanceStatusResponse.java @@ -0,0 +1,19 @@ +package com.example.Piroin.project.domain.user.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@Builder +public class AttendanceStatusResponse { + private Integer attendanceId; + + private Integer attendanceCodeId; + + private String attendanceOrder; + + private Boolean attended; + +} diff --git a/backend/src/main/java/com/example/Piroin/project/domain/user/dto/DayStatusResponse.java b/backend/src/main/java/com/example/Piroin/project/domain/user/dto/DayStatusResponse.java new file mode 100644 index 0000000..1d96a62 --- /dev/null +++ b/backend/src/main/java/com/example/Piroin/project/domain/user/dto/DayStatusResponse.java @@ -0,0 +1,22 @@ +package com.example.Piroin.project.domain.user.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDate; +import java.util.List; + +@Getter +@AllArgsConstructor +@Builder +public class DayStatusResponse { + + private String day; + + private LocalDate sessionDate; + + private List assignments; + + private List attendances; +} diff --git a/backend/src/main/java/com/example/Piroin/project/domain/user/dto/StudentAssignmentResponse.java b/backend/src/main/java/com/example/Piroin/project/domain/user/dto/StudentAssignmentResponse.java new file mode 100644 index 0000000..58f6d32 --- /dev/null +++ b/backend/src/main/java/com/example/Piroin/project/domain/user/dto/StudentAssignmentResponse.java @@ -0,0 +1,18 @@ +package com.example.Piroin.project.domain.user.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; + +// 학생이 본인의 과제 조회 +@Builder +@AllArgsConstructor +public class StudentAssignmentResponse { + + private Integer assignmentId; + + private String title; + + private String day; + + private String submitted; +} diff --git a/backend/src/main/java/com/example/Piroin/project/domain/user/dto/StudentAttendanceResponse.java b/backend/src/main/java/com/example/Piroin/project/domain/user/dto/StudentAttendanceResponse.java new file mode 100644 index 0000000..61c6dfd --- /dev/null +++ b/backend/src/main/java/com/example/Piroin/project/domain/user/dto/StudentAttendanceResponse.java @@ -0,0 +1,17 @@ +package com.example.Piroin.project.domain.user.dto; + + +import lombok.AllArgsConstructor; +import lombok.Builder; + +@Builder +@AllArgsConstructor +public class StudentAttendanceResponse { + private Integer attendanceId; + + private String day; + + private String attendanceOrder; + + private Boolean attended; +} diff --git a/backend/src/main/java/com/example/Piroin/project/domain/user/dto/StudentStatusResponse.java b/backend/src/main/java/com/example/Piroin/project/domain/user/dto/StudentStatusResponse.java new file mode 100644 index 0000000..860d8a8 --- /dev/null +++ b/backend/src/main/java/com/example/Piroin/project/domain/user/dto/StudentStatusResponse.java @@ -0,0 +1,20 @@ +package com.example.Piroin.project.domain.user.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; + +import java.util.List; + + +@Builder +@AllArgsConstructor +public class StudentStatusResponse { + + private String week; + + private String studentName; + + private List assignments; + + private List attendances; +} diff --git a/backend/src/main/java/com/example/Piroin/project/domain/user/dto/StudentWeeklyStatusResponse.java b/backend/src/main/java/com/example/Piroin/project/domain/user/dto/StudentWeeklyStatusResponse.java new file mode 100644 index 0000000..65a7711 --- /dev/null +++ b/backend/src/main/java/com/example/Piroin/project/domain/user/dto/StudentWeeklyStatusResponse.java @@ -0,0 +1,17 @@ +package com.example.Piroin.project.domain.user.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Getter +@AllArgsConstructor +@Builder +public class StudentWeeklyStatusResponse { + + private Long week; + + private List days; +} diff --git a/backend/src/main/java/com/example/Piroin/project/domain/user/dto/UpdateStudentStatusRequest.java b/backend/src/main/java/com/example/Piroin/project/domain/user/dto/UpdateStudentStatusRequest.java new file mode 100644 index 0000000..08841a0 --- /dev/null +++ b/backend/src/main/java/com/example/Piroin/project/domain/user/dto/UpdateStudentStatusRequest.java @@ -0,0 +1,30 @@ +package com.example.Piroin.project.domain.user.dto; + +import com.example.Piroin.project.domain.assignment.enums.AssignmentStatus; +import lombok.Getter; + +import java.util.List; + +@Getter +public class UpdateStudentStatusRequest { + + private List assignments; + + private List attendances; + + @Getter + public static class AssignmentStatusRequest { + + private Integer assignmentItemId; + + private AssignmentStatus submitted; + } + + @Getter + public static class AttendanceStatusRequest { + + private Integer attendanceId; + + private Boolean status; + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/example/Piroin/project/domain/user/dto/UpdateStudentStatusResponse.java b/backend/src/main/java/com/example/Piroin/project/domain/user/dto/UpdateStudentStatusResponse.java new file mode 100644 index 0000000..f626a12 --- /dev/null +++ b/backend/src/main/java/com/example/Piroin/project/domain/user/dto/UpdateStudentStatusResponse.java @@ -0,0 +1,15 @@ +package com.example.Piroin.project.domain.user.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class UpdateStudentStatusResponse { + + private Long userId; + + private Long week; + + private String message; +} 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 a980ace..6cfa315 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 @@ -1,12 +1,24 @@ package com.example.Piroin.project.domain.user.service; -import com.example.Piroin.project.domain.user.dto.StudentListResponse; +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.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.user.dto.*; import com.example.Piroin.project.domain.user.entity.User; import com.example.Piroin.project.domain.user.enums.Role; import com.example.Piroin.project.domain.user.repository.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import com.example.Piroin.project.domain.user.dto.UpdateStudentStatusRequest; +import com.example.Piroin.project.domain.user.dto.UpdateStudentStatusResponse; +import org.springframework.transaction.annotation.Transactional; + import java.util.List; @Service @@ -14,8 +26,14 @@ public class AdminUserService { private final UserRepository userRepository; + private final UserService userService; + private final AssignmentItemRepository assignmentItemRepository; + private final AttendanceRepository attendanceRepository; + private final CurriculumRepository curriculumRepository; + private final AssignmentRepository assignmentRepository; + private final AttendanceCodeRepository attendanceCodeRepository; - // 전체 부원 목록 조회 + // 1. 전체 부원 목록 조회 public List getStudentList() { List students = userRepository.findByRole(Role.MEMBER); @@ -28,7 +46,7 @@ public List getStudentList() { .toList(); } - // 부원 이름 검색 + // 2. 부원 이름 검색 public List searchStudents(String name) { List students = @@ -41,4 +59,168 @@ public List searchStudents(String name) { )) .toList(); } + + // 3. 특정 부원의 과제/출석 상태 수정 (운영진) + @Transactional + public UpdateStudentStatusResponse updateStudentWeekStatus( + Long userId, + Long week, + UpdateStudentStatusRequest request + ) { + + User user = userRepository.findById(userId) + .orElseThrow(() -> + new RuntimeException("사용자가 존재하지 않습니다.") + ); + + // 과제 상태 수정 + if (request.getAssignments() != null) { + + for (UpdateStudentStatusRequest.AssignmentStatusRequest dto + : request.getAssignments()) { + + AssignmentItem assignmentItem = + assignmentItemRepository.findById(dto.getAssignmentItemId()) + .orElseThrow(() -> + new RuntimeException("과제 정보가 존재하지 않습니다.") + ); + + // 본인 데이터 검증 + 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()) { + + Attendance attendance = + attendanceRepository.findById(dto.getAttendanceId()) + .orElseThrow(() -> + new RuntimeException("출석 정보가 존재하지 않습니다.") + ); + + // 본인 데이터 검증 + if (!attendance.getUser().getId().equals(userId)) { + throw new RuntimeException("해당 유저의 출석이 아닙니다."); + } + + // PATCH 방식 + if (dto.getStatus() != null) { + attendance.updateStatus(dto.getStatus()); + } + } + } + + return new UpdateStudentStatusResponse( + userId, + week, + "부원의 과제/출석 상태가 수정되었습니다." + ); + } + +// // 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(); +// } } \ No newline at end of file diff --git a/backend/src/main/resources/db/migration/V3__hange_attendance_date_to_date.sql b/backend/src/main/resources/db/migration/V3__hange_attendance_date_to_date.sql new file mode 100644 index 0000000..13d6bdb --- /dev/null +++ b/backend/src/main/resources/db/migration/V3__hange_attendance_date_to_date.sql @@ -0,0 +1,3 @@ +ALTER TABLE attendance_code +ALTER COLUMN attendance_date TYPE DATE +USING attendance_date::DATE; \ No newline at end of file