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 2f3b99f..9b83ead 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 @@ -27,6 +27,19 @@ public interface AttendanceRepository extends JpaRepository { // 1. 특정 출석 코드 ID에 해당하는 결석 데이터 조회 List findByAttendanceCodeIdAndStatusFalse(Integer attendanceCodeId); + // 이해도 체크 분모용 출석 인원. 세션 날짜 + 회차에서 실제 출석 완료(status=true)된 인원만 센다. + @Query(""" + SELECT COUNT(a) + FROM Attendance a + WHERE a.attendanceCode.attendanceDate = :attendanceDate + AND a.attendanceCode.attendanceOrder = :attendanceOrder + AND a.status = true + """) + long countAttendedByDateAndOrder( + @Param("attendanceDate") LocalDate attendanceDate, + @Param("attendanceOrder") String attendanceOrder + ); + // 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") LocalDate attendanceDate); @@ -47,5 +60,3 @@ Optional findByUserIdAndAttendanceCodeId( // 현재 만료되지 않은(활성화된) 출석 코드 목록을 가져오는 메서드 //List findByIsExpiredFalse(); } - - 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 989e3d1..15c211e 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 @@ -22,6 +22,7 @@ import com.example.Piroin.project.domain.assignment.entity.AssignmentItem; 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 java.time.LocalDate; @@ -116,6 +117,34 @@ public Optional getActiveAttendanceCode() { return attendanceCodeRepository.findFirstByIsExpiredFalseOrderByIdDesc(); } + // Q&A 이해도 체크 화면의 분모(13/29 중 29)를 계산한다. + @Transactional(readOnly = true) + public int countAttendedBySession(StudySession session) { + if (session == null) { + throw new IllegalArgumentException("세션 정보는 필수입니다."); + } + + String attendanceOrder = resolveAttendanceOrder(session.getDayPart()); + long attendedCount = attendanceRepository.countAttendedByDateAndOrder( + session.getSessionDate(), + attendanceOrder + ); + + return Math.toIntExact(attendedCount); + } + + // 현재 정책: 오전 세션은 1회차, 오후 세션은 2회차 출석 인원을 이해도 체크 분모로 사용한다. + private String resolveAttendanceOrder(SessionDayPart dayPart) { + if (dayPart == null) { + throw new IllegalArgumentException("세션 오전/오후 정보는 필수입니다."); + } + + return switch (dayPart) { + case AM -> "1"; + case PM -> "2"; + }; + } + // 3. 출석 체크 @Transactional public AttendanceMarkResponse markAttendance(Long userId, String inputCode) { @@ -304,4 +333,3 @@ public boolean updateAttendanceStatus(Long attendanceId, boolean status) { } */ - diff --git a/backend/src/main/java/com/example/Piroin/project/domain/question/dto/QuestionResDTO.java b/backend/src/main/java/com/example/Piroin/project/domain/question/dto/QuestionResDTO.java index be6196a..bbd774b 100644 --- a/backend/src/main/java/com/example/Piroin/project/domain/question/dto/QuestionResDTO.java +++ b/backend/src/main/java/com/example/Piroin/project/domain/question/dto/QuestionResDTO.java @@ -122,10 +122,17 @@ public record UnderstandingSliceResponse( ) { } + // 질문방 이해도 체크 바 응답. 프론트는 이 값들로 "이해했다 (13/29)"와 오른쪽 O/X 숫자를 그린다. public record UnderstandingCheckResponse( Long checkId, String content, + // 화면의 "13/29" 중 13: O 응답 수 + X 응답 수 + Integer respondedCount, + // 화면의 "13/29" 중 29: 해당 세션에 대응되는 출석 회차의 출석 인원 + Integer attendanceCount, + // 오른쪽 O 뱃지 숫자 Integer understoodCount, + // 오른쪽 X 뱃지 숫자 Integer notUnderstoodCount, LocalDateTime createdAt ) { @@ -173,18 +180,32 @@ public record CommentCreatedEvent( ) { } + // O/X 클릭 직후 응답. selectedChoice가 null이면 같은 선택지를 다시 눌러 취소된 상태다. public record UnderstandingResponseResult( Long checkId, UnderstandResChoice selectedChoice, + // 화면의 "13/29" 중 13: O 응답 수 + X 응답 수 + Integer respondedCount, + // 화면의 "13/29" 중 29: 해당 세션에 대응되는 출석 회차의 출석 인원 + Integer attendanceCount, + // 오른쪽 O 뱃지 숫자 Integer understoodCount, + // 오른쪽 X 뱃지 숫자 Integer notUnderstoodCount ) { } + // 운영진이 이해도 체크를 생성했을 때의 초기 응답. 생성 직후에는 O/X 응답자가 없어서 카운트가 0이다. public record UnderstandingCheckCreateResponse( Long checkId, String content, + // 생성 직후에는 0 + Integer respondedCount, + // 생성 응답에서는 질문방 조회 맥락이 아니므로 null로 내려간다. + Integer attendanceCount, + // 생성 직후에는 0 Integer understoodCount, + // 생성 직후에는 0 Integer notUnderstoodCount, LocalDateTime createdAt ) { diff --git a/backend/src/main/java/com/example/Piroin/project/domain/question/service/QuestionService.java b/backend/src/main/java/com/example/Piroin/project/domain/question/service/QuestionService.java index 07e65e9..3a4188f 100644 --- a/backend/src/main/java/com/example/Piroin/project/domain/question/service/QuestionService.java +++ b/backend/src/main/java/com/example/Piroin/project/domain/question/service/QuestionService.java @@ -1,5 +1,6 @@ package com.example.Piroin.project.domain.question.service; +import com.example.Piroin.project.domain.attendance.service.AttendanceService; import com.example.Piroin.project.domain.curriculum.entity.StudySession; import com.example.Piroin.project.domain.curriculum.repository.CurriculumRepository; import com.example.Piroin.project.domain.question.dto.QuestionReqDTO; @@ -44,6 +45,7 @@ public class QuestionService { private final CurriculumRepository curriculumRepository; private final UserRepository userRepository; private final QuestionEventService questionEventService; + private final AttendanceService attendanceService; // 질문 방 조회 @Transactional(readOnly = true) @@ -331,7 +333,7 @@ public QuestionResDTO.UnderstandingCheckCreateResponse createUnderstandingCheck( .build()); return new QuestionResDTO.UnderstandingCheckCreateResponse( - check.getId(), check.getTitle(), 0, 0, check.getCreatedAt() + check.getId(), check.getTitle(), 0, null, 0, 0, check.getCreatedAt() ); } @@ -349,7 +351,9 @@ public QuestionResDTO.UnderstandingResponseResult respondUnderstandingCheck( validateCheckBelongsToSession(check, session); UnderstandResChoice selectedChoice = applyUnderstandingResponse(check, loginUser, request.getChoice()); - return toUnderstandingResponseResult(check, selectedChoice); + // O/X 클릭 직후 프론트가 13/29와 O/X 뱃지를 바로 갱신할 수 있도록 최신 분모도 함께 내려준다. + int attendanceCount = attendanceService.countAttendedBySession(session); + return toUnderstandingResponseResult(check, selectedChoice, attendanceCount); } // 공통 헬퍼 메서드 @@ -416,6 +420,7 @@ private UnderstandResChoice applyUnderstandingResponse( if (response.hasChoice(requestedChoice)) { understandingResponseRepository.delete(response); + // 같은 O/X 버튼을 다시 누르면 인스타 좋아요 취소처럼 응답을 삭제하고 selectedChoice는 null로 내려간다. return null; } @@ -424,12 +429,22 @@ private UnderstandResChoice applyUnderstandingResponse( } private QuestionResDTO.UnderstandingResponseResult toUnderstandingResponseResult( - UnderstandingCheck check, UnderstandResChoice selectedChoice + UnderstandingCheck check, UnderstandResChoice selectedChoice, Integer attendanceCount ) { + // respondedCount는 프론트 화면의 "13/29" 중 13에 해당한다. + int understoodCount = understandingResponseRepository.countByCheckAndChoice( + check, UnderstandResChoice.UNDERSTOOD + ); + int notUnderstoodCount = understandingResponseRepository.countByCheckAndChoice( + check, UnderstandResChoice.NOT_UNDERSTOOD + ); + return new QuestionResDTO.UnderstandingResponseResult( check.getId(), selectedChoice, - understandingResponseRepository.countByCheckAndChoice(check, UnderstandResChoice.UNDERSTOOD), - understandingResponseRepository.countByCheckAndChoice(check, UnderstandResChoice.NOT_UNDERSTOOD) + understoodCount + notUnderstoodCount, + attendanceCount, + understoodCount, + notUnderstoodCount ); } @@ -454,17 +469,35 @@ private QuestionResDTO.UnderstandingSliceResponse getUnderstandingSlice(StudySes } UnderstandingCheck current = understandingPage.getContent().get(0); + // attendanceCount는 프론트 화면의 "13/29" 중 29에 해당한다. + int attendanceCount = attendanceService.countAttendedBySession(session); return new QuestionResDTO.UnderstandingSliceResponse( - toUnderstandingCheckResponse(current), understandingIndex, totalCount, + toUnderstandingCheckResponse(current, attendanceCount), understandingIndex, totalCount, understandingIndex < totalCount - 1, understandingIndex > 0 ); } private QuestionResDTO.UnderstandingCheckResponse toUnderstandingCheckResponse(UnderstandingCheck check) { + return toUnderstandingCheckResponse(check, null); + } + + private QuestionResDTO.UnderstandingCheckResponse toUnderstandingCheckResponse( + UnderstandingCheck check, Integer attendanceCount + ) { + // understoodCount/notUnderstoodCount는 오른쪽 O/X 뱃지 숫자로 그대로 사용한다. + int understoodCount = understandingResponseRepository.countByCheckAndChoice( + check, UnderstandResChoice.UNDERSTOOD + ); + int notUnderstoodCount = understandingResponseRepository.countByCheckAndChoice( + check, UnderstandResChoice.NOT_UNDERSTOOD + ); + return new QuestionResDTO.UnderstandingCheckResponse( check.getId(), check.getTitle(), - understandingResponseRepository.countByCheckAndChoice(check, UnderstandResChoice.UNDERSTOOD), - understandingResponseRepository.countByCheckAndChoice(check, UnderstandResChoice.NOT_UNDERSTOOD), + understoodCount + notUnderstoodCount, + attendanceCount, + understoodCount, + notUnderstoodCount, check.getCreatedAt() ); }