From 76a82b3bd2f2bf06766ccf9800477cb74d01cbd0 Mon Sep 17 00:00:00 2001 From: JOO Date: Thu, 29 Oct 2026 00:25:25 +0900 Subject: [PATCH 1/3] =?UTF-8?q?Feat:=20=ED=81=B4=EB=9E=98=EC=8A=A4=20?= =?UTF-8?q?=EB=A6=AC=ED=8F=AC=ED=8A=B8=20=EB=82=B4=20=ED=99=9C=EB=8F=99=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ClassResultController.java | 13 +++++ .../dto/response/ClassMyActivityResponse.java | 26 ++++++++++ .../classroom/service/ClassResultService.java | 3 ++ .../service/ClassResultServiceImpl.java | 47 +++++++++++++++++++ 4 files changed, 89 insertions(+) create mode 100644 src/main/java/com/swyp/picke/domain/classroom/dto/response/ClassMyActivityResponse.java diff --git a/src/main/java/com/swyp/picke/domain/classroom/controller/ClassResultController.java b/src/main/java/com/swyp/picke/domain/classroom/controller/ClassResultController.java index a81b5cca..88379df9 100644 --- a/src/main/java/com/swyp/picke/domain/classroom/controller/ClassResultController.java +++ b/src/main/java/com/swyp/picke/domain/classroom/controller/ClassResultController.java @@ -2,6 +2,7 @@ import com.swyp.picke.domain.classroom.dto.response.ClassInsightResponse; import com.swyp.picke.domain.classroom.dto.response.ClassMyReportResponse; +import com.swyp.picke.domain.classroom.dto.response.ClassMyActivityResponse; import com.swyp.picke.domain.classroom.dto.response.ClassResultResponse; import com.swyp.picke.domain.classroom.service.ClassResultService; import com.swyp.picke.global.common.response.ApiResponse; @@ -49,6 +50,18 @@ public ApiResponse getMyReport( return ApiResponse.onSuccess(classResultService.getMyReport(userId, classId)); } + @Operation(summary = "내 활동 (리포트 화면)", + description = "공개된 의견·댓글에 받은 좋아요 수와 내가 쓴 공개 댓글 목록을 커서 기반으로 조회합니다. 참여 멤버 전용이며, 기본 20건·최대 100건입니다.") + @GetMapping("/my-activity") + public ApiResponse getMyActivity( + @AuthenticationPrincipal Long userId, + @PathVariable Long classId, + @org.springframework.web.bind.annotation.RequestParam(required = false) String cursor, + @org.springframework.web.bind.annotation.RequestParam(required = false) Integer size + ) { + return ApiResponse.onSuccess(classResultService.getMyActivity(userId, classId, cursor, size)); + } + @Operation(summary = "클래스 인사이트 (운영자)", description = "공개 의견을 요약한 AI 인사이트와 근거·대표 의견. 운영자가 아니면 403 CLASS_403_OWNER. " + "작성자 3명 미만이면 LIMITED, 생성 실패면 FAILED + retryable.") diff --git a/src/main/java/com/swyp/picke/domain/classroom/dto/response/ClassMyActivityResponse.java b/src/main/java/com/swyp/picke/domain/classroom/dto/response/ClassMyActivityResponse.java new file mode 100644 index 00000000..985cd11a --- /dev/null +++ b/src/main/java/com/swyp/picke/domain/classroom/dto/response/ClassMyActivityResponse.java @@ -0,0 +1,26 @@ +package com.swyp.picke.domain.classroom.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "클래스 리포트 화면용 내 활동. 받은 좋아요는 공개된 의견·댓글에 받은 수의 합계") +public record ClassMyActivityResponse( + Long classId, + long receivedLikeCount, + long commentCount, + List comments, + String nextCursor, + boolean hasNext +) { + public record Comment( + Long commentId, + Long perspectiveId, + String perspectiveTitle, + String stance, + String content, + int likeCount, + LocalDateTime createdAt + ) { + } +} diff --git a/src/main/java/com/swyp/picke/domain/classroom/service/ClassResultService.java b/src/main/java/com/swyp/picke/domain/classroom/service/ClassResultService.java index 810fc837..d7382bc4 100644 --- a/src/main/java/com/swyp/picke/domain/classroom/service/ClassResultService.java +++ b/src/main/java/com/swyp/picke/domain/classroom/service/ClassResultService.java @@ -2,6 +2,7 @@ import com.swyp.picke.domain.classroom.dto.response.ClassInsightResponse; import com.swyp.picke.domain.classroom.dto.response.ClassMyReportResponse; +import com.swyp.picke.domain.classroom.dto.response.ClassMyActivityResponse; import com.swyp.picke.domain.classroom.dto.response.ClassResultResponse; /** @@ -14,6 +15,8 @@ public interface ClassResultService { ClassMyReportResponse getMyReport(Long userId, Long classId); + ClassMyActivityResponse getMyActivity(Long userId, Long classId, String cursor, Integer size); + ClassInsightResponse getInsight(Long userId, Long classId); ClassInsightResponse retryInsight(Long userId, Long classId); diff --git a/src/main/java/com/swyp/picke/domain/classroom/service/ClassResultServiceImpl.java b/src/main/java/com/swyp/picke/domain/classroom/service/ClassResultServiceImpl.java index 2778a025..9513cc2d 100644 --- a/src/main/java/com/swyp/picke/domain/classroom/service/ClassResultServiceImpl.java +++ b/src/main/java/com/swyp/picke/domain/classroom/service/ClassResultServiceImpl.java @@ -3,6 +3,7 @@ import com.swyp.picke.domain.battle.entity.BattleOption; import com.swyp.picke.domain.classroom.dto.response.ClassInsightResponse; import com.swyp.picke.domain.classroom.dto.response.ClassMyReportResponse; +import com.swyp.picke.domain.classroom.dto.response.ClassMyActivityResponse; import com.swyp.picke.domain.classroom.dto.response.ClassResultResponse; import com.swyp.picke.domain.classroom.entity.ClassMember; import com.swyp.picke.domain.classroom.entity.ClassReport; @@ -23,11 +24,14 @@ import com.swyp.picke.domain.perspective.entity.PerspectiveComment; import com.swyp.picke.domain.perspective.enums.PerspectiveStatus; import com.swyp.picke.domain.perspective.repository.PerspectiveCommentRepository; +import com.swyp.picke.domain.perspective.repository.CommentLikeRepository; +import com.swyp.picke.domain.perspective.repository.PerspectiveLikeRepository; import com.swyp.picke.domain.perspective.repository.PerspectiveRepository; import com.swyp.picke.domain.vote.entity.BattleVote; import com.swyp.picke.domain.vote.repository.BattleVoteRepository; import com.swyp.picke.global.common.exception.CustomException; import com.swyp.picke.global.common.exception.ErrorCode; +import com.swyp.picke.global.common.util.CursorParser; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.ArrayList; @@ -37,6 +41,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.data.domain.PageRequest; /** * 결과 화면 조회. 저장된 리포트 content(JSON)를 응답으로 바꾸고, 근거 글은 현재 상태로 다시 읽어 삭제·숨김된 글은 뺀다. @@ -58,6 +63,11 @@ public class ClassResultServiceImpl implements ClassResultService { private final BattleVoteRepository battleVoteRepository; private final PerspectiveRepository perspectiveRepository; private final PerspectiveCommentRepository perspectiveCommentRepository; + private final PerspectiveLikeRepository perspectiveLikeRepository; + private final CommentLikeRepository commentLikeRepository; + + private static final int DEFAULT_ACTIVITY_PAGE_SIZE = 20; + private static final int MAX_ACTIVITY_PAGE_SIZE = 100; @Override public ClassResultResponse getResult(Long userId, Long classId) { @@ -120,6 +130,43 @@ public ClassMyReportResponse getMyReport(Long userId, Long classId) { perspectiveCount, commentCount, feedback, generatedAt); } + @Override + public ClassMyActivityResponse getMyActivity(Long userId, Long classId, String cursor, Integer size) { + ClassAccess access = classAccessService.requireResultReadable(classId, userId, LocalDateTime.now(KST)); + if (access.member() == null) { + throw new CustomException(ErrorCode.CLASS_MEMBER_ONLY); + } + + int pageSize = size == null || size <= 0 + ? DEFAULT_ACTIVITY_PAGE_SIZE + : Math.min(size, MAX_ACTIVITY_PAGE_SIZE); + PageRequest pageable = PageRequest.of(0, pageSize); + LocalDateTime before = CursorParser.parseDateTime(cursor); + List comments = before == null + ? perspectiveCommentRepository.findVisibleByClassroomIdAndUserIdOrderByCreatedAtDesc(classId, userId, pageable) + : perspectiveCommentRepository.findVisibleByClassroomIdAndUserIdBeforeOrderByCreatedAtDesc( + classId, userId, before, pageable); + + List items = comments.stream() + .map(comment -> new ClassMyActivityResponse.Comment( + comment.getId(), + comment.getPerspective().getId(), + comment.getPerspective().getOption().getTitle(), + comment.getPerspective().getOption().getStance(), + comment.getContent(), + comment.getLikeCount(), + comment.getCreatedAt())) + .toList(); + String nextCursor = comments.size() == pageSize + ? comments.get(comments.size() - 1).getCreatedAt().toString() + : null; + long receivedLikeCount = perspectiveLikeRepository.countReceivedByUserInClass(classId, userId) + + commentLikeRepository.countReceivedByUserInClass(classId, userId); + long commentCount = perspectiveCommentRepository.countVisibleByClassroomIdAndUserId(classId, userId); + return new ClassMyActivityResponse(classId, receivedLikeCount, commentCount, items, + nextCursor, nextCursor != null); + } + @Override public ClassInsightResponse getInsight(Long userId, Long classId) { LocalDateTime now = LocalDateTime.now(KST); From a7456e793fa7c12ba1477c206522af45ef8a8f19 Mon Sep 17 00:00:00 2001 From: JOO Date: Thu, 29 Oct 2026 00:28:27 +0900 Subject: [PATCH 2/3] =?UTF-8?q?Feat:=20=EB=A6=AC=ED=8F=AC=ED=8A=B8=20?= =?UTF-8?q?=EC=A2=8B=EC=95=84=EC=9A=94=C2=B7=EB=8C=93=EA=B8=80=20N+1=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/CommentLikeRepository.java | 11 ++++++ .../PerspectiveCommentRepository.java | 35 +++++++++++++++++++ .../repository/PerspectiveLikeRepository.java | 10 ++++++ .../repository/PerspectiveRepository.java | 7 ++++ .../service/PerspectiveCommentService.java | 18 ++++++++-- .../service/PerspectiveService.java | 20 +++++++++-- 6 files changed, 96 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/swyp/picke/domain/perspective/repository/CommentLikeRepository.java b/src/main/java/com/swyp/picke/domain/perspective/repository/CommentLikeRepository.java index 0ea49a04..e350cf74 100644 --- a/src/main/java/com/swyp/picke/domain/perspective/repository/CommentLikeRepository.java +++ b/src/main/java/com/swyp/picke/domain/perspective/repository/CommentLikeRepository.java @@ -3,6 +3,8 @@ import com.swyp.picke.domain.perspective.entity.CommentLike; import com.swyp.picke.domain.perspective.entity.PerspectiveComment; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.List; import java.util.Optional; @@ -16,4 +18,13 @@ public interface CommentLikeRepository extends JpaRepository void deleteAllByCommentIn(List comments); void deleteAllByComment(PerspectiveComment comment); + + @Query(""" + select count(l) from CommentLike l + where l.comment.classroom.id = :classId + and l.comment.user.id = :userId + and l.comment.hidden = false + and l.comment.perspective.status = com.swyp.picke.domain.perspective.enums.PerspectiveStatus.PUBLISHED + """) + long countReceivedByUserInClass(@Param("classId") Long classId, @Param("userId") Long userId); } diff --git a/src/main/java/com/swyp/picke/domain/perspective/repository/PerspectiveCommentRepository.java b/src/main/java/com/swyp/picke/domain/perspective/repository/PerspectiveCommentRepository.java index a75e38e3..1c683268 100644 --- a/src/main/java/com/swyp/picke/domain/perspective/repository/PerspectiveCommentRepository.java +++ b/src/main/java/com/swyp/picke/domain/perspective/repository/PerspectiveCommentRepository.java @@ -4,6 +4,7 @@ import com.swyp.picke.domain.perspective.entity.PerspectiveComment; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -34,6 +35,40 @@ public interface PerspectiveCommentRepository extends JpaRepository findByClassroomIdAndUserIdAndHiddenFalseOrderByCreatedAtAsc(Long classroomId, Long userId); + @Modifying(flushAutomatically = true) + @Query("update PerspectiveComment c set c.hidden = true where c.user.id = :userId and c.hidden = false") + int hideAllByUserId(@Param("userId") Long userId); + + @Query(""" + select c from PerspectiveComment c + join fetch c.perspective p + where c.classroom.id = :classId and c.user.id = :userId + and c.hidden = false and p.status = com.swyp.picke.domain.perspective.enums.PerspectiveStatus.PUBLISHED + order by c.createdAt desc, c.id desc + """) + List findVisibleByClassroomIdAndUserIdOrderByCreatedAtDesc( + @Param("classId") Long classId, @Param("userId") Long userId, Pageable pageable); + + @Query(""" + select c from PerspectiveComment c + join fetch c.perspective p + where c.classroom.id = :classId and c.user.id = :userId + and c.hidden = false and p.status = com.swyp.picke.domain.perspective.enums.PerspectiveStatus.PUBLISHED + and c.createdAt < :cursor + order by c.createdAt desc, c.id desc + """) + List findVisibleByClassroomIdAndUserIdBeforeOrderByCreatedAtDesc( + @Param("classId") Long classId, @Param("userId") Long userId, + @Param("cursor") LocalDateTime cursor, Pageable pageable); + + @Query(""" + select count(c) from PerspectiveComment c + join c.perspective p + where c.classroom.id = :classId and c.user.id = :userId + and c.hidden = false and p.status = com.swyp.picke.domain.perspective.enums.PerspectiveStatus.PUBLISHED + """) + long countVisibleByClassroomIdAndUserId(@Param("classId") Long classId, @Param("userId") Long userId); + @Query("SELECT c FROM PerspectiveComment c WHERE c.perspective.battle.id = :battleId AND c.classroom IS NULL AND c.hidden = false AND c.likeCount >= :minLikeCount ORDER BY c.likeCount DESC") List findTopCommentsByBattleId(@Param("battleId") Long battleId, @Param("minLikeCount") int minLikeCount, Pageable pageable); } diff --git a/src/main/java/com/swyp/picke/domain/perspective/repository/PerspectiveLikeRepository.java b/src/main/java/com/swyp/picke/domain/perspective/repository/PerspectiveLikeRepository.java index 0262f529..9764b4a0 100644 --- a/src/main/java/com/swyp/picke/domain/perspective/repository/PerspectiveLikeRepository.java +++ b/src/main/java/com/swyp/picke/domain/perspective/repository/PerspectiveLikeRepository.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import java.util.List; @@ -23,5 +24,14 @@ public interface PerspectiveLikeRepository extends JpaRepository List findByClassroomIdAndUserIdAndStatusOrderByCreatedAtAsc(Long classroomId, Long userId, PerspectiveStatus status); + @Modifying(flushAutomatically = true) + @Query("update Perspective p set p.status = :hidden where p.user.id = :userId and p.status <> :hidden") + int hideAllByUserId(@Param("userId") Long userId, @Param("hidden") PerspectiveStatus hidden); + // 마이페이지: 일반 배틀 의견만 List findByUserIdAndClassroomIsNullOrderByCreatedAtDesc(Long userId, Pageable pageable); diff --git a/src/main/java/com/swyp/picke/domain/perspective/service/PerspectiveCommentService.java b/src/main/java/com/swyp/picke/domain/perspective/service/PerspectiveCommentService.java index a0c76e44..09b0e4dc 100644 --- a/src/main/java/com/swyp/picke/domain/perspective/service/PerspectiveCommentService.java +++ b/src/main/java/com/swyp/picke/domain/perspective/service/PerspectiveCommentService.java @@ -22,11 +22,13 @@ import com.swyp.picke.domain.user.entity.User; import com.swyp.picke.domain.user.repository.UserRepository; import com.swyp.picke.domain.user.enums.CharacterType; +import com.swyp.picke.domain.user.enums.UserStatus; import com.swyp.picke.domain.user.service.UserService; import com.swyp.picke.domain.vote.service.BattleVoteService; import com.swyp.picke.global.common.exception.CustomException; import com.swyp.picke.global.common.exception.ErrorCode; import com.swyp.picke.global.infra.s3.service.S3PresignedUrlService; +import com.swyp.picke.global.common.util.CursorParser; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; @@ -71,8 +73,11 @@ public CreateCommentResponse createComment(Long perspectiveId, Long userId, Crea Classroom classroom = perspective.getClassroom(); requireClassWritable(classroom, userId); requireVisibleAuthor(perspective, classroom); - User user = userRepository.findById(userId) + User user = userRepository.findByIdForUpdate(userId) .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + if (user.getStatus() != UserStatus.ACTIVE) { + throw new CustomException(ErrorCode.USER_NOT_FOUND); + } PerspectiveComment comment = PerspectiveComment.builder() .perspective(perspective) @@ -147,7 +152,7 @@ private CommentListResponse listComments(Long perspectiveId, Long userId, String List comments = cursor == null ? commentRepository.findByPerspectiveOrderByCreatedAtDesc(perspective, pageable) : commentRepository.findByPerspectiveAndCreatedAtBeforeOrderByCreatedAtDesc( - perspective, LocalDateTime.parse(cursor), pageable); + perspective, CursorParser.parseDateTime(cursor), pageable); Long battleId = perspective.getBattle().getId(); // 클래스 댓글이면 작성자를 클래스 표시 이름으로 보여주고, 나간·내보낸 멤버의 댓글은 뺀다 @@ -204,6 +209,7 @@ public void deleteComment(Long perspectiveId, Long commentId, Long userId) { @Transactional public UpdateCommentResponse updateComment(Long perspectiveId, Long commentId, Long userId, UpdateCommentRequest request) { + requireActiveUserForWrite(userId); Perspective perspective = findPerspectiveById(perspectiveId); PerspectiveComment comment = findCommentById(commentId); validateOwnership(comment, userId); @@ -213,6 +219,14 @@ public UpdateCommentResponse updateComment(Long perspectiveId, Long commentId, L return new UpdateCommentResponse(comment.getId(), comment.getContent(), comment.getUpdatedAt()); } + private void requireActiveUserForWrite(Long userId) { + User user = userRepository.findByIdForUpdate(userId) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + if (user.getStatus() != UserStatus.ACTIVE) { + throw new CustomException(ErrorCode.USER_NOT_FOUND); + } + } + /** 댓글 작성자의 사후투표 입장. 클래스 의견이면 같은 클래스 안의 투표만 본다 */ private String resolveStance(Long battleId, Long authorId, Classroom classroom) { Long postVoteOptionId = BattleVoteService.findPostVoteOptionId(battleId, authorId, classroom); diff --git a/src/main/java/com/swyp/picke/domain/perspective/service/PerspectiveService.java b/src/main/java/com/swyp/picke/domain/perspective/service/PerspectiveService.java index a682267a..b998e76b 100644 --- a/src/main/java/com/swyp/picke/domain/perspective/service/PerspectiveService.java +++ b/src/main/java/com/swyp/picke/domain/perspective/service/PerspectiveService.java @@ -9,6 +9,7 @@ import com.swyp.picke.domain.classroom.service.ClassDisplayNameResolver; import com.swyp.picke.domain.perspective.enums.PerspectiveStatus; import com.swyp.picke.domain.user.entity.User; +import com.swyp.picke.domain.user.enums.UserStatus; import com.swyp.picke.domain.user.repository.UserRepository; import com.swyp.picke.domain.perspective.dto.request.CreatePerspectiveRequest; import com.swyp.picke.domain.perspective.dto.request.UpdatePerspectiveRequest; @@ -31,6 +32,7 @@ import com.swyp.picke.domain.vote.service.BattleVoteService; import com.swyp.picke.global.common.exception.CustomException; import com.swyp.picke.global.common.exception.ErrorCode; +import com.swyp.picke.global.common.util.CursorParser; import com.swyp.picke.global.infra.s3.service.S3PresignedUrlService; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; @@ -112,8 +114,11 @@ public CreatePerspectiveResponse createPerspective(Long battleId, Long userId, C @Transactional public CreatePerspectiveResponse createPerspective(Long battleId, Long userId, Classroom classroom, CreatePerspectiveRequest request) { Battle battle = battleService.findById(battleId); - User user = userRepository.findById(userId) + User user = userRepository.findByIdForUpdate(userId) .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + if (user.getStatus() != UserStatus.ACTIVE) { + throw new CustomException(ErrorCode.USER_NOT_FOUND); + } Long postVoteOptionId = BattleVoteService.findPostVoteOptionId(battleId, userId, classroom); BattleOption option = battleService.findOptionById(postVoteOptionId); @@ -151,13 +156,13 @@ public PerspectiveListResponse getPerspectives(Long battleId, Long userId, Class ? perspectiveRepository.findByBattleIdAndClassroomAndOptionIdAndStatusOrderByLikeCountDescCreatedAtDesc(battleId, classroom, option.getId(), PerspectiveStatus.PUBLISHED, pageable) : cursor == null ? perspectiveRepository.findByBattleIdAndClassroomAndOptionIdAndStatusOrderByCreatedAtDesc(battleId, classroom, option.getId(), PerspectiveStatus.PUBLISHED, pageable) - : perspectiveRepository.findByBattleIdAndClassroomAndOptionIdAndStatusAndCreatedAtBeforeOrderByCreatedAtDesc(battleId, classroom, option.getId(), PerspectiveStatus.PUBLISHED, LocalDateTime.parse(cursor), pageable); + : perspectiveRepository.findByBattleIdAndClassroomAndOptionIdAndStatusAndCreatedAtBeforeOrderByCreatedAtDesc(battleId, classroom, option.getId(), PerspectiveStatus.PUBLISHED, CursorParser.parseDateTime(cursor), pageable); } else { perspectives = isPopular ? perspectiveRepository.findByBattleIdAndClassroomAndStatusOrderByLikeCountDescCreatedAtDesc(battleId, classroom, PerspectiveStatus.PUBLISHED, pageable) : cursor == null ? perspectiveRepository.findByBattleIdAndClassroomAndStatusOrderByCreatedAtDesc(battleId, classroom, PerspectiveStatus.PUBLISHED, pageable) - : perspectiveRepository.findByBattleIdAndClassroomAndStatusAndCreatedAtBeforeOrderByCreatedAtDesc(battleId, classroom, PerspectiveStatus.PUBLISHED, LocalDateTime.parse(cursor), pageable); + : perspectiveRepository.findByBattleIdAndClassroomAndStatusAndCreatedAtBeforeOrderByCreatedAtDesc(battleId, classroom, PerspectiveStatus.PUBLISHED, CursorParser.parseDateTime(cursor), pageable); } // 클래스 의견이면 작성자를 클래스 표시 이름으로 보여주고, 나간·내보낸 멤버의 의견은 뺀다 @@ -216,6 +221,7 @@ public void deletePerspective(Long perspectiveId, Long userId) { @Transactional public UpdatePerspectiveResponse updatePerspective(Long perspectiveId, Long userId, UpdatePerspectiveRequest request) { + requireActiveUserForWrite(userId); Perspective perspective = findPerspectiveById(perspectiveId); validateOwnership(perspective, userId); requireClassWritable(perspective, userId); @@ -224,6 +230,14 @@ public UpdatePerspectiveResponse updatePerspective(Long perspectiveId, Long user return new UpdatePerspectiveResponse(perspective.getId(), perspective.getContent(), perspective.getUpdatedAt()); } + private void requireActiveUserForWrite(Long userId) { + User user = userRepository.findByIdForUpdate(userId) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + if (user.getStatus() != UserStatus.ACTIVE) { + throw new CustomException(ErrorCode.USER_NOT_FOUND); + } + } + public List getMyPerspective(Long battleId, Long userId) { return getMyPerspective(battleId, userId, null); } From 695d3b81e8488e24eea55c9ea53495cc35c0bc64 Mon Sep 17 00:00:00 2001 From: JOO Date: Thu, 29 Oct 2026 00:28:48 +0900 Subject: [PATCH 3/3] =?UTF-8?q?Docs:=20=ED=81=B4=EB=9E=98=EC=8A=A4=20?= =?UTF-8?q?=EB=A6=AC=ED=8F=AC=ED=8A=B8=20API=20=EB=AA=85=EC=84=B8=20?= =?UTF-8?q?=EA=B0=B1=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/api-specs/class-api.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/api-specs/class-api.md b/docs/api-specs/class-api.md index cbbd378b..a41879e7 100644 --- a/docs/api-specs/class-api.md +++ b/docs/api-specs/class-api.md @@ -623,6 +623,8 @@ AI가 주제 후보를 제안하면 운영자가 하나를 고르거나 직접 - 나간 뒤에도 같은 코드로 다시 참여할 수 있다. - 투표·의견 등 활동 이력이 있으면 좌석은 반환되지 않는다. +계정 탈퇴 시에도 같은 좌석 규칙을 적용한다. 탈퇴자가 운영자인 ACTIVE 클래스는 소프트 삭제하고, 참여 중인 멤버십은 `LEFT`로 바꾼다. 탈퇴자가 쓴 의견·댓글은 숨김 처리하며 이력 참조를 위해 DB 행은 보존한다. + | 상황 | 응답 | |---|---| | 운영자 (나갈 수 없고 클래스를 삭제해야 함) | 403 `CLASS_403_OWNER_LEAVE` | @@ -1075,6 +1077,28 @@ AI가 주제 후보를 제안하면 운영자가 하나를 고르거나 직접 - `coverage=partial`은 분량 제한으로 일부 글을 빼고 분석했다는 뜻이다. 뺀 글 수는 `excludedCount`에 있다. - 참여하지 않는 운영자는 403 `CLASS_403_MEMBER`다. +### 8.2.1 내 활동 (리포트 화면) +- `GET /api/v1/classes/{classId}/result/my-activity?cursor={createdAt}&size=20` +- 참여 중인 멤버만 조회할 수 있다. 기본 페이지 크기는 20, 최대 100이다. +- `receivedLikeCount`는 공개된 내 의견·댓글에 받은 좋아요 수 합계다. `comments`는 공개 상태인 내 댓글 목록이며, 최신순 커서 페이지네이션을 사용한다. +- 잘못된 `cursor`는 400 `COMMON_400`이다. + +```json +{ + "statusCode": 200, + "data": { + "classId": 5, + "receivedLikeCount": 12, + "commentCount": 2, + "comments": [ + { "commentId": 61, "perspectiveId": 50, "perspectiveTitle": "창작이다", "stance": "...", "content": "...", "likeCount": 2, "createdAt": "2026-10-21T12:00:00" } + ], + "nextCursor": null, + "hasNext": false + } +} +``` + ### 8.3 클래스 인사이트 (운영자) - `GET /api/v1/classes/{classId}/result/insight` - 공개 의견을 요약한 AI 인사이트와 근거·대표 의견이다.