diff --git a/backend/src/main/java/com/example/Piroin/project/domain/attendance/dto/AttendanceDayStatusRes.java b/backend/src/main/java/com/example/Piroin/project/domain/attendance/dto/AttendanceDayStatusRes.java new file mode 100644 index 0000000..8f25d7f --- /dev/null +++ b/backend/src/main/java/com/example/Piroin/project/domain/attendance/dto/AttendanceDayStatusRes.java @@ -0,0 +1,23 @@ +package com.example.Piroin.project.domain.attendance.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDate; +import java.util.List; + +@Getter +@Setter +@Schema(description = "요일별 출석 상태") +public class AttendanceDayStatusRes { + + @Schema(description = "출석 날짜", example = "2026-06-23") + private LocalDate date; + + @Schema(description = "요일", example = "TUESDAY") + private String day; + + @Schema(description = "출석 차시별 상태 목록") + private List slots; +} diff --git a/backend/src/main/java/com/example/Piroin/project/domain/attendance/dto/AttendanceReqDTO.java b/backend/src/main/java/com/example/Piroin/project/domain/attendance/dto/AttendanceReqDTO.java deleted file mode 100644 index 47b76ec..0000000 --- a/backend/src/main/java/com/example/Piroin/project/domain/attendance/dto/AttendanceReqDTO.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.example.Piroin.project.domain.attendance.dto; - -public class AttendanceReqDTO { -} diff --git a/backend/src/main/java/com/example/Piroin/project/domain/attendance/dto/AttendanceResDTO.java b/backend/src/main/java/com/example/Piroin/project/domain/attendance/dto/AttendanceResDTO.java deleted file mode 100644 index bbafb19..0000000 --- a/backend/src/main/java/com/example/Piroin/project/domain/attendance/dto/AttendanceResDTO.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.example.Piroin.project.domain.attendance.dto; - -public class AttendanceResDTO { -} diff --git a/backend/src/main/java/com/example/Piroin/project/domain/attendance/dto/AttendanceStatusRes.java b/backend/src/main/java/com/example/Piroin/project/domain/attendance/dto/AttendanceStatusRes.java index 1bc1ed4..d7f990c 100644 --- a/backend/src/main/java/com/example/Piroin/project/domain/attendance/dto/AttendanceStatusRes.java +++ b/backend/src/main/java/com/example/Piroin/project/domain/attendance/dto/AttendanceStatusRes.java @@ -9,14 +9,12 @@ @Getter @Setter -@Schema(description = "사용자 출석 상태") +@Schema(description = "사용자 주차별 출석 상태") public class AttendanceStatusRes { - @Schema(description = "출석 날짜", example = "2025-06-24") - private LocalDate date; @Schema(description = "주차", example = "1") private int week; - @Schema(description = "출석 차시별 상태 목록") - private List slots; -} + @Schema(description = "요일별 출석 상태 목록") + private List days; +} \ No newline at end of file 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 9b83ead..5839457 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 @@ -1,6 +1,7 @@ package com.example.Piroin.project.domain.attendance.repository; import com.example.Piroin.project.domain.attendance.entity.AttendanceCode; +import com.example.Piroin.project.domain.curriculum.entity.StudySession; import com.example.Piroin.project.domain.user.entity.User; import com.example.Piroin.project.domain.attendance.entity.Attendance; import org.springframework.data.jpa.repository.JpaRepository; @@ -20,7 +21,6 @@ public interface AttendanceRepository extends JpaRepository { // 연관관계 필드명이 attendanceCode 라면 내부 ID인 Id를 조합하여 명명 Optional findByUserIdAndAttendanceCodeId(Long userId, Long attendanceCodeId); - //List findByUserIdAndStudySessionSessionDate(Integer userId, LocalDate date); int countByUserAndStatusFalse(User user); @@ -54,6 +54,7 @@ Optional findByUserIdAndAttendanceCodeId( 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 15c211e..2840c00 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 @@ -1,6 +1,9 @@ package com.example.Piroin.project.domain.attendance.service; +import com.example.Piroin.project.domain.assignment.repository.AssignmentRepository; 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.deposit.entity.Deposit; import com.example.Piroin.project.domain.deposit.repository.DepositRepository; @@ -23,13 +26,11 @@ import com.example.Piroin.project.domain.assignment.repository.AssignmentItemRepository; import com.example.Piroin.project.domain.attendance.dto.UpdateUserStatusReq; import com.example.Piroin.project.domain.curriculum.enums.SessionDayPart; +import com.example.Piroin.project.domain.attendance.dto.AttendanceDayStatusRes; import java.time.LocalDate; -import java.util.Comparator; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.concurrent.ThreadLocalRandom; import java.util.stream.Collectors; @@ -41,27 +42,29 @@ public class AttendanceService { private final AttendanceCodeRepository attendanceCodeRepository; private final UserRepository userRepository; private final DepositService depositService; - private final CurriculumRepository curriculumRepository; - private final AssignmentItemRepository assignmentItemRepository; - // 1. 출석 시작 코드 (출석코드 생성 함수) @Transactional public AttendanceCode generateCodeAndCreateAttendances(LocalDate date) { // [수정] 세션 ID 대신 날짜를 직접 받음 - // 1. [삭제] 더 이상 세션을 조회해서 날짜를 파싱할 필요가 없습니다. (curriculumRepository 조회 제거) + // 1-1) 해당 날짜에 커리큘럼이 있는지 확인 + if (!curriculumRepository.existsBySessionDate(date)) { + throw new CurriculumException( + CurriculumErrorCode.ATTENDANCE_DATE_NOT_AVAILABLE + ); + } - // 2. 해당 날짜에 생성된 출석 코드 개수 조회 + // 1-2) 해당 날짜에 생성된 출석 코드 개수 조회. long codeCountOfDay = attendanceCodeRepository.countByAttendanceDate(date); if (codeCountOfDay >= 3) { throw new IllegalStateException("하루에 최대 3회까지만 출석 코드를 생성할 수 있습니다."); } - // 3. 기존 활성화된 코드들 만료 처리 + // 1-3) 기존 활성화된 코드들 만료 처리 List activeCodes = attendanceCodeRepository.findByIsExpiredFalse(); for (AttendanceCode activeCode : activeCodes) { activeCode.expire(); @@ -79,11 +82,11 @@ public AttendanceCode generateCodeAndCreateAttendances(LocalDate date) { // [수 } - // 4. 4자리 랜덤 코드 생성 및 차수(Order) 계산 + // 1-4) 4자리 랜덤 코드 생성 및 차수(Order) 계산 String code = String.valueOf(ThreadLocalRandom.current().nextInt(1000, 10000)); String attendanceOrder = String.valueOf(codeCountOfDay + 1); // 1회차, 2회차, 3회차 - // 5. 새로운 AttendanceCode 생성 및 저장 + // 1-5) 새로운 AttendanceCode 생성 및 저장 AttendanceCode attendanceCode = AttendanceCode.builder() .attendanceDate(date) // [수정] 파라미터로 받은 날짜 주입 .attendanceOrder(attendanceOrder) @@ -93,11 +96,10 @@ public AttendanceCode generateCodeAndCreateAttendances(LocalDate date) { // [수 attendanceCodeRepository.save(attendanceCode); - // 6. 모든 MEMBER 유저에 대해 '현재 생성된 출석 코드' 기준 초기 출석 데이터 생성 + // 1-6) 모든 MEMBER 유저에 대해 '현재 생성된 출석 코드' 기준 초기 출석 데이터 생성 List users = userRepository.findByRole(Role.MEMBER); for (User user : users) { - // [확인] 이미 완벽하게 studySession 대신 attendanceCode를 주입하도록 잘 짜두셨습니다! Attendance attendance = Attendance.builder() .user(user) .attendanceCode(attendanceCode) @@ -233,103 +235,92 @@ public List findByUserIdAndDate(Integer userId, LocalDate dat .toList(); } - // 6. 유저의 전체 출석 현황을 날짜별로 묶어서 조회하는 함수 + // 6. 나의 전체 출석 현황 조회 서비스 public List findByUserId(Integer userId) { List attendances = attendanceRepository.findByUserId(Long.valueOf(userId)); - // LocalDate 기준으로 그룹화 - Map> grouped = attendances.stream() - .collect(Collectors.groupingBy( - attendance -> attendance.getAttendanceCode().getAttendanceDate() - )); + // 날짜별 그룹화 + Map> dateGrouped = + attendances.stream() + .collect(Collectors.groupingBy( + attendance -> + attendance.getAttendanceCode().getAttendanceDate() + )); + + // 주차별 그룹화 + Map> weekGrouped = + new HashMap<>(); + + for (Map.Entry> entry : dateGrouped.entrySet()) { + + LocalDate date = entry.getKey(); + + StudySession studySession = + curriculumRepository + .findFirstBySessionDate(date) + .orElseThrow(() -> + new RuntimeException("세션이 존재하지 않습니다.") + ); + + int week = studySession.getWeek().intValue(); + + List slots = + entry.getValue().stream() + .map(attendance -> + new AttendanceSlotRes( + attendance.getAttendanceCode().getId(), + attendance.getStatus() + ) + ) + .sorted( + Comparator.comparing( + AttendanceSlotRes::getAttendanceCodeId + ) + ) + .toList(); + + AttendanceDayStatusRes dayRes = new AttendanceDayStatusRes(); + dayRes.setDate(date); + dayRes.setDay(date.getDayOfWeek().toString()); + dayRes.setSlots(slots); - return grouped.entrySet().stream() + + weekGrouped + .computeIfAbsent(week, k -> new ArrayList<>()) + .add(dayRes); + } + + return weekGrouped.entrySet().stream() .map(entry -> { - LocalDate date = entry.getKey(); + AttendanceStatusRes dto = + new AttendanceStatusRes(); - List slots = entry.getValue().stream() - .map(attendance -> new AttendanceSlotRes( - attendance.getAttendanceCode().getId(), - attendance.getStatus() - )) - .sorted(Comparator.comparing(AttendanceSlotRes::getAttendanceCodeId)) - .toList(); + dto.setWeek(entry.getKey()); - AttendanceStatusRes dto = new AttendanceStatusRes(); - dto.setDate(date); - dto.setSlots(slots); + dto.setDays( + entry.getValue().stream() + .sorted( + Comparator.comparing( + AttendanceDayStatusRes::getDate + ) + ) + .toList() + ); return dto; }) - .sorted(Comparator.comparing(AttendanceStatusRes::getDate).reversed()) + .sorted( + Comparator.comparing( + AttendanceStatusRes::getWeek + ) + ) .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; -// } -} - -/* - // 관리자가 유저의 출석 상태를 변경하는 함수(나중에 과제까지 같이 변경되도록 수정할 것) - @Transactional - public boolean updateAttendanceStatus(Long attendanceId, boolean status) { - Optional attendanceOpt = attendanceRepository.findById(attendanceId); - - if (attendanceOpt.isEmpty()) { - return false; - } - // 출석 상태 변경 - Attendance attendance = attendanceOpt.get(); - attendance.setStatus(status); - attendanceRepository.save(attendance); - - // 출석 변경 → 보증금 재계산 - depositService.recalculateDeposit(attendance.getUser().getId()); - - return true; - } +} - */ 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 07258ef..21a760f 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 @@ -30,6 +30,12 @@ public enum CurriculumErrorCode { HttpStatus.BAD_REQUEST, "CURRICULUM405", "해당 주차/요일의 세션이 존재하지 않습니다. 세션을 먼저 생성해주세요." + ), + + ATTENDANCE_DATE_NOT_AVAILABLE( + HttpStatus.BAD_REQUEST, + "CURRICULUM406", + "해당 날짜는 세션 진행일이 아닙니다. 세션 일정 또는 커리큘럼을 확인해주세요." ); private final HttpStatus status; 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 35719a8..35e1501 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 @@ -6,6 +6,7 @@ import java.time.LocalDate; import java.util.List; +import java.util.Optional; /* StudySession(세션) DB 접근 인터페이스 @@ -25,6 +26,11 @@ public interface CurriculumRepository extends JpaRepository List findByWeekOrderBySessionDateAsc(Long week); + boolean existsBySessionDate(LocalDate sessionDate); + + Optional findFirstBySessionDate(LocalDate sessionDate); + + // @Query(""" // SELECT s // FROM StudySession s