From feb8e2f9264f7fdf768bea96e9f5ac739529dcc6 Mon Sep 17 00:00:00 2001 From: issuejong Date: Thu, 28 May 2026 18:19:44 +0900 Subject: [PATCH 1/5] =?UTF-8?q?[Feat]=20=EC=9D=B4=ED=95=B4=EB=8F=84=20?= =?UTF-8?q?=EC=B2=B4=ED=81=AC=20=EC=9D=91=EB=8B=B5=20DTO=20=ED=99=95?= =?UTF-8?q?=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/question/dto/QuestionResDTO.java | 4 ++++ .../question/service/QuestionService.java | 21 ++++++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) 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..951bcd3 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 @@ -125,6 +125,8 @@ public record UnderstandingSliceResponse( public record UnderstandingCheckResponse( Long checkId, String content, + Integer respondedCount, + Integer attendanceCount, Integer understoodCount, Integer notUnderstoodCount, LocalDateTime createdAt @@ -184,6 +186,8 @@ public record UnderstandingResponseResult( public record UnderstandingCheckCreateResponse( Long checkId, String content, + Integer respondedCount, + Integer attendanceCount, Integer understoodCount, 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..7eb07ef 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 @@ -331,7 +331,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() ); } @@ -461,10 +461,25 @@ private QuestionResDTO.UnderstandingSliceResponse getUnderstandingSlice(StudySes } private QuestionResDTO.UnderstandingCheckResponse toUnderstandingCheckResponse(UnderstandingCheck check) { + return toUnderstandingCheckResponse(check, null); + } + + private QuestionResDTO.UnderstandingCheckResponse toUnderstandingCheckResponse( + UnderstandingCheck check, Integer attendanceCount + ) { + 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() ); } From 0706b035961cd6b1c0cb50dd68ce37e142b6d5fc Mon Sep 17 00:00:00 2001 From: issuejong Date: Thu, 28 May 2026 18:27:38 +0900 Subject: [PATCH 2/5] =?UTF-8?q?[Feat]=20=EC=84=B8=EC=85=98=20=EA=B8=B0?= =?UTF-8?q?=EC=A4=80=20=EC=B6=9C=EC=84=9D=20=EC=9D=B8=EC=9B=90=20=EA=B3=84?= =?UTF-8?q?=EC=82=B0=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/AttendanceRepository.java | 13 ++++++++- .../attendance/service/AttendanceService.java | 28 ++++++++++++++++++- 2 files changed, 39 insertions(+), 2 deletions(-) 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..e25cce5 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,18 @@ public interface AttendanceRepository extends JpaRepository { // 1. 특정 출석 코드 ID에 해당하는 결석 데이터 조회 List findByAttendanceCodeIdAndStatusFalse(Integer attendanceCodeId); + @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); @@ -48,4 +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..4906e77 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,32 @@ public Optional getActiveAttendanceCode() { return attendanceCodeRepository.findFirstByIsExpiredFalseOrderByIdDesc(); } + @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); + } + + 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 +331,3 @@ public boolean updateAttendanceStatus(Long attendanceId, boolean status) { } */ - From 143d4d0c40359ee245ee7b29934de08ade06baa4 Mon Sep 17 00:00:00 2001 From: issuejong Date: Thu, 28 May 2026 18:37:37 +0900 Subject: [PATCH 3/5] =?UTF-8?q?[Feat]=20=EC=A7=88=EB=AC=B8=EB=B0=A9/?= =?UTF-8?q?=EC=9D=B4=ED=95=B4=EB=8F=84=20=EC=A1=B0=ED=9A=8C=20API=EC=97=90?= =?UTF-8?q?=20=EC=B6=9C=EC=84=9D=20=EC=9D=B8=EC=9B=90=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project/domain/question/service/QuestionService.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 7eb07ef..5ac746e 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) @@ -454,8 +456,10 @@ private QuestionResDTO.UnderstandingSliceResponse getUnderstandingSlice(StudySes } UnderstandingCheck current = understandingPage.getContent().get(0); + // 이해도 체크 분모는 세션에 대응되는 출석 회차의 실제 출석 인원을 사용한다. + int attendanceCount = attendanceService.countAttendedBySession(session); return new QuestionResDTO.UnderstandingSliceResponse( - toUnderstandingCheckResponse(current), understandingIndex, totalCount, + toUnderstandingCheckResponse(current, attendanceCount), understandingIndex, totalCount, understandingIndex < totalCount - 1, understandingIndex > 0 ); } From d7731ba18b25079ffa32b740b29954820c5b5d13 Mon Sep 17 00:00:00 2001 From: issuejong Date: Thu, 28 May 2026 18:45:39 +0900 Subject: [PATCH 4/5] =?UTF-8?q?[Feat]=20=EC=9D=B4=ED=95=B4=EB=8F=84=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=EC=A0=9C=EC=B6=9C=20API=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=ED=98=95=EC=8B=9D=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/question/dto/QuestionResDTO.java | 2 ++ .../question/service/QuestionService.java | 18 ++++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) 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 951bcd3..398100b 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 @@ -178,6 +178,8 @@ public record CommentCreatedEvent( public record UnderstandingResponseResult( Long checkId, UnderstandResChoice selectedChoice, + Integer respondedCount, + Integer attendanceCount, Integer understoodCount, Integer notUnderstoodCount ) { 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 5ac746e..9c50947 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 @@ -351,7 +351,8 @@ public QuestionResDTO.UnderstandingResponseResult respondUnderstandingCheck( validateCheckBelongsToSession(check, session); UnderstandResChoice selectedChoice = applyUnderstandingResponse(check, loginUser, request.getChoice()); - return toUnderstandingResponseResult(check, selectedChoice); + int attendanceCount = attendanceService.countAttendedBySession(session); + return toUnderstandingResponseResult(check, selectedChoice, attendanceCount); } // 공통 헬퍼 메서드 @@ -426,12 +427,21 @@ private UnderstandResChoice applyUnderstandingResponse( } private QuestionResDTO.UnderstandingResponseResult toUnderstandingResponseResult( - UnderstandingCheck check, UnderstandResChoice selectedChoice + UnderstandingCheck check, UnderstandResChoice selectedChoice, Integer attendanceCount ) { + 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 ); } From ffc7676087a169738f238d77123953c482815c14 Mon Sep 17 00:00:00 2001 From: issuejong Date: Thu, 28 May 2026 18:49:13 +0900 Subject: [PATCH 5/5] =?UTF-8?q?[Docs]=20=EC=A3=BC=EC=84=9D=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/AttendanceRepository.java | 2 +- .../attendance/service/AttendanceService.java | 2 ++ .../domain/question/dto/QuestionResDTO.java | 15 +++++++++++++++ .../domain/question/service/QuestionService.java | 6 +++++- 4 files changed, 23 insertions(+), 2 deletions(-) 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 e25cce5..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,7 @@ public interface AttendanceRepository extends JpaRepository { // 1. 특정 출석 코드 ID에 해당하는 결석 데이터 조회 List findByAttendanceCodeIdAndStatusFalse(Integer attendanceCodeId); + // 이해도 체크 분모용 출석 인원. 세션 날짜 + 회차에서 실제 출석 완료(status=true)된 인원만 센다. @Query(""" SELECT COUNT(a) FROM Attendance a @@ -59,4 +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 4906e77..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 @@ -117,6 +117,7 @@ public Optional getActiveAttendanceCode() { return attendanceCodeRepository.findFirstByIsExpiredFalseOrderByIdDesc(); } + // Q&A 이해도 체크 화면의 분모(13/29 중 29)를 계산한다. @Transactional(readOnly = true) public int countAttendedBySession(StudySession session) { if (session == null) { @@ -132,6 +133,7 @@ public int countAttendedBySession(StudySession session) { return Math.toIntExact(attendedCount); } + // 현재 정책: 오전 세션은 1회차, 오후 세션은 2회차 출석 인원을 이해도 체크 분모로 사용한다. private String resolveAttendanceOrder(SessionDayPart dayPart) { if (dayPart == null) { throw new IllegalArgumentException("세션 오전/오후 정보는 필수입니다."); 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 398100b..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,12 +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 ) { @@ -175,22 +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 9c50947..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 @@ -351,6 +351,7 @@ public QuestionResDTO.UnderstandingResponseResult respondUnderstandingCheck( validateCheckBelongsToSession(check, session); UnderstandResChoice selectedChoice = applyUnderstandingResponse(check, loginUser, request.getChoice()); + // O/X 클릭 직후 프론트가 13/29와 O/X 뱃지를 바로 갱신할 수 있도록 최신 분모도 함께 내려준다. int attendanceCount = attendanceService.countAttendedBySession(session); return toUnderstandingResponseResult(check, selectedChoice, attendanceCount); } @@ -419,6 +420,7 @@ private UnderstandResChoice applyUnderstandingResponse( if (response.hasChoice(requestedChoice)) { understandingResponseRepository.delete(response); + // 같은 O/X 버튼을 다시 누르면 인스타 좋아요 취소처럼 응답을 삭제하고 selectedChoice는 null로 내려간다. return null; } @@ -429,6 +431,7 @@ private UnderstandResChoice applyUnderstandingResponse( private QuestionResDTO.UnderstandingResponseResult toUnderstandingResponseResult( UnderstandingCheck check, UnderstandResChoice selectedChoice, Integer attendanceCount ) { + // respondedCount는 프론트 화면의 "13/29" 중 13에 해당한다. int understoodCount = understandingResponseRepository.countByCheckAndChoice( check, UnderstandResChoice.UNDERSTOOD ); @@ -466,7 +469,7 @@ 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, attendanceCount), understandingIndex, totalCount, @@ -481,6 +484,7 @@ private QuestionResDTO.UnderstandingCheckResponse toUnderstandingCheckResponse(U private QuestionResDTO.UnderstandingCheckResponse toUnderstandingCheckResponse( UnderstandingCheck check, Integer attendanceCount ) { + // understoodCount/notUnderstoodCount는 오른쪽 O/X 뱃지 숫자로 그대로 사용한다. int understoodCount = understandingResponseRepository.countByCheckAndChoice( check, UnderstandResChoice.UNDERSTOOD );