Skip to content

Conversation

@sunohkim
Copy link
Collaborator

@sunohkim sunohkim commented Feb 19, 2025

[BE] 질문 체크 기능 구현

#️⃣ 연관된 이슈

#182

📝 작업 내용

학생과 교수 모두 질문을 체크하고, 이를 바탕으로 SSE 메시지를 보내는 기능

  • 질문 체크 API 요청
  • Question 테이블 내 isComplete 업데이트
  • 비동기 방식으로 상대방에게 SSE 메시지를 통해 질문 체크 알림 전송

💬 리뷰 요구사항(선택)

학생이 체크 버튼을 눌렀을 때와 교수가 체크 버튼을 눌렀을 때 database 접근 방식을 조금 다르게 구성했습니다.

  • 학생의 경우 요청 시 questionId와 함께 쿠키 안에 studentId, courseId를 가지고 있기 때문에 update 1회만 수행하도록 설계했습니다.
  • 교수의 경우 요청 시 questionId만 알고 있기 때문에 먼저 questionId를 이용해 course와 studentId를 가져온 뒤 question update를 수행하고, studentId를 바탕으로 SSE 메시지 요청을 보내도록 설계했습니다.
    더 좋은 방식이 있으면 제안해 주셔도 됩니다.

Summary by CodeRabbit

  • New Features

    • Introduced targeted real-time messaging that now supports communications tailored for both courses and individual students.
    • Added endpoints enabling professors and students to check question statuses and receive immediate notifications.
    • Updated the course registration flow with revised redirection for a more intuitive navigation experience.
    • Introduced a new service for managing question-related functionalities, including status checks and updates.
  • Improvements

    • Enhanced system logging and messaging consistency for optimal performance and reliability.

@sunohkim sunohkim added ✨ Feat 새로운 기능 추가 (기능 개발 및 신규 기능 구현) 🌱 BE 백엔드 관련 labels Feb 19, 2025
@sunohkim sunohkim self-assigned this Feb 19, 2025
@coderabbitai
Copy link

coderabbitai bot commented Feb 19, 2025

Walkthrough

The changes in this pull request involve refactoring the SSE messaging system, including renaming methods and updating their parameters from Long to String. New functionality has been introduced for checking questions by both professors and students, with corresponding endpoints and service methods. The Question entity has also been updated to allow marking questions as complete. Additionally, minor adjustments have been made to HTTP response headers and logging messages across various controllers and services.

Changes

File(s) Change Summary
.../domain/sse/controller/SseMessageController.java,
.../domain/sse/service/SseService.java,
.../global/sse/SseMessageSender.java,
.../domain/reaction/StudentReactionService.java,
.../domain/request/StudentRequestService.java
Renamed and updated message sending methods; changed parameter type from Long to String and updated logging statements for improved consistency in SSE message handling.
.../domain/course/StudentCourseController.java Modified the course registration response header from "students/classroom" to "student/course".
.../domain/question/ProfessorQuestionController.java,
.../domain/question/ProfessorQuestionService.java
Added new professor question check functionality with an endpoint at /professors/questions/check/{questionId} that processes question checks, marks questions complete, and sends an SSE message.
.../domain/question/StudentQuestionController.java,
.../domain/question/StudentQuestionService.java
Introduced a new endpoint for students to check questions (POST /check/{questionId}) that verifies and updates the question status, triggering an SSE message.
.../domain/question/Question.java Updated the Question entity by adding a setter for isComplete, consolidating lombok imports, and initializing isComplete to false in the builder.
.../domain/question/QuestionRepository.java Added a new method updateQuestion to update the isComplete status using a custom JPQL query.
.../domain/question/dto/QuestionCheckSseRequest.java Introduced a new DTO for wrapping question check SSE requests.
.../global/exception/code/QuestionErrorCode.java Added a new enum to represent the QUESTION_NOT_FOUND error with corresponding HTTP status and message.

Sequence Diagram(s)

Professor Question Check Flow

sequenceDiagram
    participant ProfCtrl as ProfessorQuestionController
    participant ProfService as ProfessorQuestionService
    participant Repo as QuestionRepository
    participant SSE as SseMessageSender

    ProfCtrl->>ProfService: checkQuestion(questionId)
    ProfService->>Repo: getQuestion(questionId)
    Repo-->>ProfService: Question
    ProfService->>Repo: update question status (mark complete)
    ProfService->>SSE: sendMessage(QuestionCheckSseRequest)
    ProfService-->>ProfCtrl: HTTP 204 No Content
Loading

Student Question Check Flow

sequenceDiagram
    participant StudCtrl as StudentQuestionController
    participant StudService as StudentQuestionService
    participant Repo as QuestionRepository
    participant SSE as SseMessageSender

    StudCtrl->>StudService: checkQuestion(questionId, request)
    StudService->>Repo: updateQuestion(studentId, courseId, questionId)
    alt update successful
        StudService->>SSE: sendMessage(QuestionCheckSseRequest)
    else update failed
        StudService-->>StudCtrl: error (Question not found)
    end
    StudService-->>StudCtrl: HTTP 204 No Content
Loading

Possibly related PRs

Suggested reviewers

  • uri010

Poem

I'm a rabbit hopping through the code,
Changes abound in each secret node.
SSE messages now use strings so neat,
Questions get checked with an updated beat.
With endpoints fresh and logs that gleam,
My playful hops code a brighter dream!
🐰✨

✨ Finishing Touches
  • 📝 Generate Docstrings (Beta)

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (14)
back-end/reacton/src/main/java/com/softeer/reacton/domain/reaction/StudentReactionService.java (1)

23-35: Consider enhancing error handling for SSE message delivery.

While the code handles course-related errors well, it might benefit from handling SSE message delivery failures to ensure notifications are reliably delivered.

Consider:

  1. Adding a retry mechanism for failed SSE message deliveries
  2. Implementing a fallback notification method (e.g., WebSocket or polling)
  3. Monitoring SSE connection health
  4. Adding metrics for tracking message delivery success/failure rates

Would you like me to provide an example implementation of these enhancements?

back-end/reacton/src/main/java/com/softeer/reacton/domain/question/dto/QuestionCheckSseRequest.java (1)

8-10: Improve field naming and add validation.

The field name id is ambiguous. Consider renaming it to be more specific (e.g., questionId). Also, since this is a request DTO, consider adding validation annotations.

 public class QuestionCheckSseRequest {
-    private Long id;
+    @NotNull(message = "Question ID cannot be null")
+    private Long questionId;
 }
back-end/reacton/src/main/java/com/softeer/reacton/global/exception/code/QuestionErrorCode.java (1)

9-11: Consider adding more specific error codes.

Given the question-checking feature's requirements, consider adding more specific error codes for scenarios like:

  • Question already checked
  • Invalid course or student ID
  • Unauthorized access to question
 public enum QuestionErrorCode implements ErrorCode {
-    QUESTION_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 질문 정보를 찾을 수 없습니다.");
+    QUESTION_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 질문 정보를 찾을 수 없습니다."),
+    QUESTION_ALREADY_CHECKED(HttpStatus.BAD_REQUEST, "이미 확인된 질문입니다."),
+    INVALID_COURSE_OR_STUDENT(HttpStatus.BAD_REQUEST, "유효하지 않은 수업 또는 학생 정보입니다."),
+    UNAUTHORIZED_QUESTION_ACCESS(HttpStatus.FORBIDDEN, "질문에 대한 접근 권한이 없습니다.");
back-end/reacton/src/main/java/com/softeer/reacton/domain/question/QuestionRepository.java (1)

14-17: Consider using JPA's built-in methods instead of custom query.

The custom update query could be replaced with JPA's built-in methods for better maintainability:

  1. Use findByStudentIdAndCourseAndId to fetch the question
  2. Update the entity using the setter
  3. Save using save method

This approach would:

  • Leverage JPA's entity management
  • Trigger entity lifecycle events
  • Maintain audit trails (if using @EntityListeners)
-    @Modifying
-    @Query("UPDATE Question q SET q.isComplete = true " +
-            "WHERE q.studentId = :studentId AND q.course = :course AND q.id = :questionId")
-    int updateQuestion(@Param("studentId") String studentId, @Param("course") Course course, @Param("questionId") Long questionId);
+    Optional<Question> findByStudentIdAndCourseAndId(String studentId, Course course, Long id);
back-end/reacton/src/main/java/com/softeer/reacton/domain/question/Question.java (3)

24-26: Use primitive boolean for non-null field.

Since the field is marked as nullable=false, use primitive boolean instead of wrapper Boolean.

     @Setter
     @Column(nullable = false)
-    private Boolean isComplete;
+    private boolean isComplete;

30-30: Translate Korean comment to English.

For consistency and better international collaboration, consider translating the Korean comment to English.

-    private Course course; // 수업 정보 (외래 키)
+    private Course course; // Course information (foreign key)

24-24: Consider using a more controlled approach to state changes.

Instead of using @Setter, consider using a more explicit method to control state changes.

-    @Setter
     @Column(nullable = false)
     private boolean isComplete;
+
+    public void markAsComplete() {
+        this.isComplete = true;
+    }
back-end/reacton/src/main/java/com/softeer/reacton/domain/question/ProfessorQuestionController.java (1)

36-47: Consider adding request validation.

While the implementation is correct, consider adding validation for the questionId parameter to ensure it's positive and within valid range.

 public ResponseEntity<Void> checkQuestion(
-            @PathVariable("questionId") Long questionId) {
+            @PathVariable("questionId") @Positive @NotNull Long questionId) {
back-end/reacton/src/main/java/com/softeer/reacton/domain/question/StudentQuestionController.java (2)

81-92: Enhance API documentation with more specific error descriptions.

The API responses could be more descriptive to help API consumers better understand and handle errors:

             responses = {
-                    @ApiResponse(responseCode = "200", description = "성공적으로 전송했습니다."),
-                    @ApiResponse(responseCode = "404", description = "수업을 찾을 수 없습니다."),
-                    @ApiResponse(responseCode = "404", description = "질문을 찾을 수 없습니다."),
-                    @ApiResponse(responseCode = "409", description = "아직 수업이 시작되지 않았습니다."),
-                    @ApiResponse(responseCode = "500", description = "서버와의 연결에 실패했습니다.")
+                    @ApiResponse(responseCode = "204", description = "질문이 성공적으로 체크되었습니다."),
+                    @ApiResponse(responseCode = "404", description = "Course not found: 해당 수업을 찾을 수 없습니다."),
+                    @ApiResponse(responseCode = "404", description = "Question not found: 해당 질문을 찾을 수 없거나 접근 권한이 없습니다."),
+                    @ApiResponse(responseCode = "409", description = "Course not active: 수업이 아직 시작되지 않았습니다."),
+                    @ApiResponse(responseCode = "500", description = "Internal server error: 서버 처리 중 오류가 발생했습니다.")

93-108: Add validation for questionId path parameter.

Consider adding validation to ensure questionId is positive and not null.

     public ResponseEntity<Void> checkQuestion(
-            @PathVariable("questionId") Long questionId,
+            @PathVariable("questionId") @Min(1) @NotNull Long questionId,
             HttpServletRequest request) {

Also, the success response code should be 204 (NO_CONTENT) in the API documentation since that's what the implementation returns.

back-end/reacton/src/main/java/com/softeer/reacton/domain/question/StudentQuestionService.java (2)

73-92: Add unit tests for the new sendQuestionCheck method.

The new method requires test coverage to verify:

  • Successful question check update
  • Error handling for non-existent questions
  • SSE message sending

Would you like me to generate the unit test cases for this new functionality?


79-83: Consider adding more context to error messages.

When the question update fails, include more details in the error message to aid debugging.

         if (updatedRows == 0) {
-            log.debug("질문 체크를 처리하는 과정에서 발생한 에러입니다. : Question does not exist.");
+            log.debug("질문 체크를 처리하는 과정에서 발생한 에러입니다. : Question not found or access denied. questionId={}, studentId={}, courseId={}", questionId, studentId, courseId);
             throw new BaseException(QuestionErrorCode.QUESTION_NOT_FOUND);
         }
back-end/reacton-classroom/src/main/java/com/softeer/reacton_classroom/domain/sse/service/SseService.java (2)

46-59: Document the parameter type change and enhance error messages.

The method signature change from courseId to id needs better documentation and error handling.

+    /**
+     * Sends a message to a specific recipient identified by their ID.
+     * @param id The recipient's identifier (can be either courseId or studentId)
+     * @param message The message to be sent
+     * @throws BaseException if the recipient is not found or message sending fails
+     */
     public void sendMessage(String id, MessageResponse<?> message) {
         Sinks.Many<MessageResponse<?>> sink = sinks.get(id);
         if (sink == null) {
-            log.debug("전송 대상을 찾지 못했습니다. : Receiver not found.");
+            log.debug("전송 대상을 찾지 못했습니다. : Receiver not found for id={}", id);
             throw new BaseException(SseErrorCode.USER_NOT_FOUND);
         }

         Sinks.EmitResult result = sink.tryEmitNext(message);
         if (result.isFailure()) {
-            log.info("메시지 전송에 실패했습니다.");
+            log.info("메시지 전송에 실패했습니다. : Failed to send message to id={}, result={}", id, result);
             throw new BaseException(SseErrorCode.MESSAGE_SEND_FAILURE);
         }
-        log.info("메시지 전송에 성공했습니다.");
+        log.info("메시지 전송에 성공했습니다. : Successfully sent message to id={}", id);
     }

66-67: Consider implementing auto-reconnect functionality.

The TODO comment indicates a need for auto-reconnect functionality on the frontend.

Would you like me to help create an issue to track the implementation of auto-reconnect functionality for SSE connections?

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cf156fa and 4357d62.

📒 Files selected for processing (14)
  • back-end/reacton-classroom/src/main/java/com/softeer/reacton_classroom/domain/sse/controller/SseMessageController.java (1 hunks)
  • back-end/reacton-classroom/src/main/java/com/softeer/reacton_classroom/domain/sse/service/SseService.java (1 hunks)
  • back-end/reacton/src/main/java/com/softeer/reacton/domain/course/StudentCourseController.java (1 hunks)
  • back-end/reacton/src/main/java/com/softeer/reacton/domain/question/ProfessorQuestionController.java (1 hunks)
  • back-end/reacton/src/main/java/com/softeer/reacton/domain/question/ProfessorQuestionService.java (1 hunks)
  • back-end/reacton/src/main/java/com/softeer/reacton/domain/question/Question.java (2 hunks)
  • back-end/reacton/src/main/java/com/softeer/reacton/domain/question/QuestionRepository.java (1 hunks)
  • back-end/reacton/src/main/java/com/softeer/reacton/domain/question/StudentQuestionController.java (1 hunks)
  • back-end/reacton/src/main/java/com/softeer/reacton/domain/question/StudentQuestionService.java (2 hunks)
  • back-end/reacton/src/main/java/com/softeer/reacton/domain/question/dto/QuestionCheckSseRequest.java (1 hunks)
  • back-end/reacton/src/main/java/com/softeer/reacton/domain/reaction/StudentReactionService.java (1 hunks)
  • back-end/reacton/src/main/java/com/softeer/reacton/domain/request/StudentRequestService.java (1 hunks)
  • back-end/reacton/src/main/java/com/softeer/reacton/global/exception/code/QuestionErrorCode.java (1 hunks)
  • back-end/reacton/src/main/java/com/softeer/reacton/global/sse/SseMessageSender.java (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
back-end/reacton/src/main/java/com/softeer/reacton/domain/question/QuestionRepository.java (1)
Learnt from: sunohkim
PR: softeer5th/Team3-PowerPenguin#149
File: back-end/reacton/src/main/java/com/softeer/reacton/domain/course/CourseRepository.java:29-29
Timestamp: 2025-02-18T03:09:12.819Z
Learning: In Spring Boot applications with JPA, prefer using built-in methods from JpaRepository (e.g. findById, save, delete) instead of creating custom methods with identical functionality to avoid redundancy.
back-end/reacton/src/main/java/com/softeer/reacton/domain/question/Question.java (2)
Learnt from: sunohkim
PR: softeer5th/Team3-PowerPenguin#149
File: back-end/reacton/src/main/java/com/softeer/reacton/domain/course/CourseRepository.java:29-29
Timestamp: 2025-02-18T03:09:12.819Z
Learning: In Spring Boot applications with JPA, prefer using built-in methods from JpaRepository (e.g. findById, save, delete) instead of creating custom methods with identical functionality to avoid redundancy.
Learnt from: uri010
PR: softeer5th/Team3-PowerPenguin#69
File: back-end/reacton/src/main/java/com/softeer/reacton/domain/course/Course.java:97-104
Timestamp: 2025-02-07T00:57:09.147Z
Learning: The project uses Java 17, which supports modern language features like Stream's `.toList()` method.
🔇 Additional comments (10)
back-end/reacton/src/main/java/com/softeer/reacton/domain/reaction/StudentReactionService.java (1)

34-34: LGTM! The update aligns with the new SSE messaging API.

The change correctly adapts to the new sendMessage API by converting the courseId to a string.

back-end/reacton/src/main/java/com/softeer/reacton/domain/course/StudentCourseController.java (1)

83-83:

Details

✅ Verification successful

Verify frontend route compatibility for the new redirect URL.

The Location header URL has been changed from "students/classroom" to "student/course". This is a breaking change that requires corresponding updates in frontend routing.

Also note that while the controller's base path uses plural form ("/students/courses"), the redirect URL uses singular form ("student/course"). Consider maintaining consistent URL patterns across the application.

Run the following script to check for any other occurrences of these URL patterns and ensure consistency:


🏁 Script executed:

#!/bin/bash
# Search for URL patterns in both frontend and backend code
echo "Searching for URL patterns in the codebase..."
rg -g '!*.{class,jar}' -i "student/course|students/classroom"

# Check frontend route definitions
echo "Checking frontend route definitions..."
fd -e ts -e tsx -e js -e jsx --exec rg -l "Route|router.push|navigate|redirect"

Length of output: 1105


Frontend routing compatibility confirmed.

The new redirect URL "student/course" is correctly aligned with the front-end route definitions, as confirmed in the following:

  • Backend: The Location header now sets the URL to FRONTEND_BASE_URL + "student/course" in StudentCourseController.java.
  • Frontend: The route corresponding to /student/course is used consistently (e.g., in front-end/src/pages/student/home/StudentHome.tsx and the import in front-end/src/App.tsx).

Although the controller's base path uses the plural form ("/students/courses"), the update to a singular redirect URL appears intentional given the front-end usage. Please ensure that this naming difference is deliberate and that all team members are aware of the convention to avoid potential confusion in the future.

back-end/reacton/src/main/java/com/softeer/reacton/domain/request/StudentRequestService.java (1)

45-45:

Details

✅ Verification successful

LGTM! Consider adding null checks.

The change to use a more generic sendMessage method with string identifiers aligns well with the PR objectives for implementing SSE notifications for both students and professors.

Consider adding null checks for courseId before conversion to string:

-        sseMessageSender.sendMessage(courseId.toString(), sseMessage);
+        if (courseId != null) {
+            sseMessageSender.sendMessage(courseId.toString(), sseMessage);
+        } else {
+            log.error("courseId is null");
+            throw new BaseException(CourseErrorCode.COURSE_NOT_FOUND);
+        }

Let's verify that this change is consistently applied across the codebase:


🏁 Script executed:

#!/bin/bash
# Search for all usages of sendMessage and sendMessageToProfessor
rg -A 2 'sendMessage(To(Professor|Student)?)?\(' --type java

Length of output: 4295


SSE Messaging Update: Approved with a Minor Suggestion

The refactor to use the generic sendMessage method with string IDs is consistent with the refactored pattern seen in other parts of the codebase and aligns with the SSE notification objectives. The usage is uniform (e.g., in StudentQuestionService and StudentReactionService), which confirms the consistency of the change.

  • Verified Usage:

    • StudentRequestService.java now converts courseId to a string, similar to patterns found in other modules.
    • Other related files (e.g., StudentQuestionService.java, StudentReactionService.java) follow the same approach.
  • Suggestion for Improvement:
    Consider adding a null check for courseId before conversion. This will safeguard against potential null pointer issues. For example:

    -        sseMessageSender.sendMessage(courseId.toString(), sseMessage);
    +        if (courseId != null) {
    +            sseMessageSender.sendMessage(courseId.toString(), sseMessage);
    +        } else {
    +            log.error("courseId is null");
    +            throw new BaseException(CourseErrorCode.COURSE_NOT_FOUND);
    +        }

Overall, the change is LGTM. Please review the null-safety suggestion to ensure robust error handling.

back-end/reacton-classroom/src/main/java/com/softeer/reacton_classroom/domain/sse/controller/SseMessageController.java (2)

20-27: LGTM! Method rename improves clarity.

The rename from sendMessage to sendMessageToCourse better reflects the method's purpose and target recipient.


29-36: LGTM! New method for student messaging.

The new sendMessageToStudent method follows consistent patterns with the course messaging and properly handles student-specific message routing.

back-end/reacton/src/main/java/com/softeer/reacton/domain/question/ProfessorQuestionController.java (1)

24-35: LGTM! Well-documented API endpoint.

The API documentation is thorough and clearly describes all possible response scenarios.

back-end/reacton/src/main/java/com/softeer/reacton/domain/question/ProfessorQuestionService.java (2)

44-47: LGTM! Clean error handling.

The getQuestion method properly handles the not-found scenario with a custom exception.


49-53: LGTM! Clear course status validation.

The checkIfOpen method effectively validates course status before proceeding with question check.

back-end/reacton/src/main/java/com/softeer/reacton/global/sse/SseMessageSender.java (1)

36-46: LGTM! Robust retry mechanism.

The retry logic with exponential backoff is well implemented and properly handles WebClient exceptions.

back-end/reacton/src/main/java/com/softeer/reacton/domain/question/StudentQuestionService.java (1)

66-66:

Details

❓ Verification inconclusive

Verify the impact of the sendMessage signature change.

The method call has been updated to use toString() on courseId. Ensure all consumers of this API are updated to handle String IDs.


🏁 Script executed:

#!/bin/bash
# Search for other usages of sendMessage to verify they've been updated
rg -l "sendMessage.*courseId" --type java

Length of output: 445


Action Required: Verify Consistent Handling of String IDs in SSE API

Please ensure that the change of converting courseId to a string using toString() is reflected consistently across all usages of the sendMessage API. In addition to the update in StudentQuestionService.java, verify that the following files also correctly handle the new String type for the courseId argument:

  • back-end/reacton-classroom/src/main/java/com/softeer/reacton_classroom/domain/sse/controller/SseMessageController.java
  • back-end/reacton/src/main/java/com/softeer/reacton/domain/reaction/StudentReactionService.java
  • back-end/reacton/src/main/java/com/softeer/reacton/domain/request/StudentRequestService.java

Double-check that each consumer converts the course ID to a string if needed, and update the unit tests accordingly.

@sunohkim sunohkim requested a review from uri010 February 20, 2025 01:36
Copy link
Collaborator

@uri010 uri010 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

리뷰 드렸습니다!

- '질문 체크 데이터를 database에 저장하는 과정'만을 트랜잭션 처리했고, 비동기 방식으로 SSE 요청을 보내는 기능은 트랜잭션에서 제외
@softeer5th softeer5th deleted a comment from coderabbitai bot Feb 20, 2025
@softeer5th softeer5th deleted a comment from coderabbitai bot Feb 20, 2025
@sunohkim
Copy link
Collaborator Author

📝 추가 작업 내용

  • SseMessageSender에 불필요한 예외 핸들러가 구현되어 있었습니다. SSE 요청 간 발생하는 에러는 onErrorResume에서 처리하는데, 불필요한 try-catch문을 사용한 것이 확인되어 try-catch문을 삭제했습니다.

@sunohkim sunohkim requested a review from uri010 February 20, 2025 05:15
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
back-end/reacton/src/main/java/com/softeer/reacton/domain/question/ProfessorQuestionService.java (1)

23-41: 🛠️ Refactor suggestion

Consider separating DB operations into a dedicated transaction.

The current implementation includes both DB update and SSE message sending within the same transaction boundary. This could lead to unnecessary transaction rollbacks if SSE message sending fails. Consider extracting the DB operations into a separate transactional method:

-    public void sendQuestionCheck(Long questionId) {
+    public void sendQuestionCheck(Long questionId) {
+        Question question = updateQuestionStatus(questionId);
+        sendSseMessage(question);
+    }
+
+    @Transactional
+    private Question updateQuestionStatus(Long questionId) {
         log.debug("질문 체크 처리를 시작합니다. : questionId = {}", questionId);
 
         Question question = getQuestion(questionId);
         Course course = question.getCourse();
         checkIfOpen(course);
-        String studentId = question.getStudentId();
 
         question.setIsComplete(true);
-        questionService.save(question);
+        return questionService.save(question);
+    }
 
+    private void sendSseMessage(Question question) {
+        String studentId = question.getStudentId();
         QuestionCheckSseRequest questionCheckSseRequest = new QuestionCheckSseRequest(questionId);
 
         log.debug("SSE 서버에 질문 체크 전송을 요청합니다.");
         SseMessage<QuestionCheckSseRequest> sseMessage = new SseMessage<>("QUESTION_CHECK", questionCheckSseRequest);
         sseMessageSender.sendMessage(studentId, sseMessage);
 
         log.debug("질문 체크 처리가 완료되었습니다.");
-    }
🧹 Nitpick comments (3)
back-end/reacton/src/main/java/com/softeer/reacton/domain/question/QuestionService.java (2)

3-3: Consider using Spring's @transactional annotation.

Replace jakarta.transaction.Transactional with org.springframework.transaction.annotation.Transactional for better integration with Spring's transaction management and access to additional features.

-import jakarta.transaction.Transactional;
+import org.springframework.transaction.annotation.Transactional;

13-16: Enhance method robustness with validation and logging.

The save method could benefit from:

  1. Input validation to ensure the Question object is valid
  2. Exception handling for database operations
  3. Logging for tracking save operations
 @Transactional
 public void save(Question question) {
+    if (question == null) {
+        throw new IllegalArgumentException("Question cannot be null");
+    }
+    log.info("Saving question: {}", question.getId());
+    try {
         questionRepository.save(question);
+        log.debug("Successfully saved question: {}", question.getId());
+    } catch (Exception e) {
+        log.error("Failed to save question: {}", question.getId(), e);
+        throw e;
+    }
 }

Don't forget to add the lombok @slf4j annotation to the class:

 @Service
 @RequiredArgsConstructor
+@Slf4j
 public class QuestionService {
back-end/reacton/src/main/java/com/softeer/reacton/domain/question/ProfessorQuestionService.java (1)

31-32: Consider adding optimistic locking for concurrent updates.

The current implementation might face race conditions if multiple professors attempt to check the same question simultaneously. Consider adding version-based optimistic locking to prevent concurrent modifications:

 question.setIsComplete(true);
+// Add @Version field to Question entity to enable optimistic locking
 questionService.save(question);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4357d62 and e17363b.

📒 Files selected for processing (5)
  • back-end/reacton/src/main/java/com/softeer/reacton/domain/question/ProfessorQuestionService.java (1 hunks)
  • back-end/reacton/src/main/java/com/softeer/reacton/domain/question/QuestionRepository.java (1 hunks)
  • back-end/reacton/src/main/java/com/softeer/reacton/domain/question/QuestionService.java (1 hunks)
  • back-end/reacton/src/main/java/com/softeer/reacton/domain/question/StudentQuestionService.java (2 hunks)
  • back-end/reacton/src/main/java/com/softeer/reacton/global/sse/SseMessageSender.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • back-end/reacton/src/main/java/com/softeer/reacton/global/sse/SseMessageSender.java
🧰 Additional context used
🧠 Learnings (1)
back-end/reacton/src/main/java/com/softeer/reacton/domain/question/QuestionRepository.java (1)
Learnt from: sunohkim
PR: softeer5th/Team3-PowerPenguin#149
File: back-end/reacton/src/main/java/com/softeer/reacton/domain/course/CourseRepository.java:29-29
Timestamp: 2025-02-18T03:09:12.819Z
Learning: In Spring Boot applications with JPA, prefer using built-in methods from JpaRepository (e.g. findById, save, delete) instead of creating custom methods with identical functionality to avoid redundancy.
🔇 Additional comments (2)
back-end/reacton/src/main/java/com/softeer/reacton/domain/question/QuestionService.java (1)

7-8: LGTM! Good use of Lombok for constructor injection.

The combination of @Service and @RequiredArgsConstructor follows Spring best practices for dependency injection.

back-end/reacton/src/main/java/com/softeer/reacton/domain/question/QuestionRepository.java (1)

15-19: LGTM! Well-structured update query with proper error handling.

The implementation correctly uses @Modifying and @Transactional for the update operation, and returns the number of affected rows to handle the case where no matching question is found.

Comment on lines 9 to +11
public class QuestionService {

private final QuestionRepository questionRepository;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider adding methods for question checking functionality.

Based on the PR objectives, this service should handle question checking for both students and professors. Consider adding methods like:

  • checkQuestionByStudent(Long questionId, Long studentId, Long courseId)
  • checkQuestionByProfessor(Long questionId)

These methods would encapsulate the different database access patterns mentioned in the PR description.

log.debug("SSE 서버에 질문 전송을 요청합니다.");
SseMessage<CourseQuestionResponse> sseMessage = new SseMessage<>("QUESTION", courseQuestionResponse);
sseMessageSender.sendMessageToProfessor(courseId, sseMessage);
sseMessageSender.sendMessage(courseId.toString(), sseMessage);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify the impact of type change in sendMessage.

The parameter type for sseMessageSender.sendMessage has changed from Long to String. Ensure that all callers of this method have been updated accordingly.


🏁 Script executed:

#!/bin/bash
# Search for all calls to sendMessage to verify type consistency
rg -A 2 'sseMessageSender\.sendMessage\(' 

Length of output: 1962


Action Required: Ensure consistent type conversion for sendMessage calls

The updated sendMessage method now expects a String rather than a Long. In the StudentQuestionService (and in the StudentRequestService and StudentReactionService), the course ID is correctly converted to a String using toString(). However, notice that in ProfessorQuestionService the call still passes studentId without conversion. Please verify if studentId should also be converted to a String and update the call accordingly to maintain consistency across all invocations.

Comment on lines +72 to +91
public void sendQuestionCheck(String studentId, Long courseId, Long questionId) {
log.debug("질문 체크 처리를 시작합니다. : questionId = {}", questionId);

Course course = getCourse(courseId);
checkIfOpen(course);

int updatedRows = questionRepository.updateQuestion(studentId, course, questionId);
if (updatedRows == 0) {
log.debug("질문 체크를 처리하는 과정에서 발생한 에러입니다. : Question does not exist.");
throw new BaseException(QuestionErrorCode.QUESTION_NOT_FOUND);
}

QuestionCheckSseRequest questionCheckSseRequest = new QuestionCheckSseRequest(questionId);

log.debug("SSE 서버에 질문 체크 전송을 요청합니다.");
SseMessage<QuestionCheckSseRequest> sseMessage = new SseMessage<>("QUESTION_CHECK", questionCheckSseRequest);
sseMessageSender.sendMessage(courseId.toString(), sseMessage);

log.debug("질문 체크 처리가 완료되었습니다.");
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add transaction boundary for DB operations.

The sendQuestionCheck method should be annotated with @Transactional to ensure that the DB operations are executed within a transaction:

+    @Transactional
     public void sendQuestionCheck(String studentId, Long courseId, Long questionId) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public void sendQuestionCheck(String studentId, Long courseId, Long questionId) {
log.debug("질문 체크 처리를 시작합니다. : questionId = {}", questionId);
Course course = getCourse(courseId);
checkIfOpen(course);
int updatedRows = questionRepository.updateQuestion(studentId, course, questionId);
if (updatedRows == 0) {
log.debug("질문 체크를 처리하는 과정에서 발생한 에러입니다. : Question does not exist.");
throw new BaseException(QuestionErrorCode.QUESTION_NOT_FOUND);
}
QuestionCheckSseRequest questionCheckSseRequest = new QuestionCheckSseRequest(questionId);
log.debug("SSE 서버에 질문 체크 전송을 요청합니다.");
SseMessage<QuestionCheckSseRequest> sseMessage = new SseMessage<>("QUESTION_CHECK", questionCheckSseRequest);
sseMessageSender.sendMessage(courseId.toString(), sseMessage);
log.debug("질문 체크 처리가 완료되었습니다.");
}
@Transactional
public void sendQuestionCheck(String studentId, Long courseId, Long questionId) {
log.debug("질문 체크 처리를 시작합니다. : questionId = {}", questionId);
Course course = getCourse(courseId);
checkIfOpen(course);
int updatedRows = questionRepository.updateQuestion(studentId, course, questionId);
if (updatedRows == 0) {
log.debug("질문 체크를 처리하는 과정에서 발생한 에러입니다. : Question does not exist.");
throw new BaseException(QuestionErrorCode.QUESTION_NOT_FOUND);
}
QuestionCheckSseRequest questionCheckSseRequest = new QuestionCheckSseRequest(questionId);
log.debug("SSE 서버에 질문 체크 전송을 요청합니다.");
SseMessage<QuestionCheckSseRequest> sseMessage = new SseMessage<>("QUESTION_CHECK", questionCheckSseRequest);
sseMessageSender.sendMessage(courseId.toString(), sseMessage);
log.debug("질문 체크 처리가 완료되었습니다.");
}

Copy link
Collaborator

@uri010 uri010 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

조회와 업데이트를 한 트랜잭션에서 관리하지 않아도 되는 기능일 수도 있으나 같이 고민해봤으면 좋겠어서 리뷰로 남겨놨습니다. 선오님 의견도 공유해주세요~!

Comment on lines +30 to +34

question.setIsComplete(true);
questionService.save(question);

QuestionCheckSseRequest questionCheckSseRequest = new QuestionCheckSseRequest(questionId);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 트랜잭션 범위 수정이 된건가요? 조회랑 업데이트 부분이 같이 한 트랜잭션으로 묶여야 하지 않나요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

생각해보니 그렇네요. 단순히 '업데이트'만을 하나의 트랜잭션으로 생각했는데, 조회 후 업데이트할 경우에는 이 과정을 모두 트랜잭션으로 묶어야 할 필요가 있었습니다.

Comment on lines +75 to +82
Course course = getCourse(courseId);
checkIfOpen(course);

int updatedRows = questionRepository.updateQuestion(studentId, course, questionId);
if (updatedRows == 0) {
log.debug("질문 체크를 처리하는 과정에서 발생한 에러입니다. : Question does not exist.");
throw new BaseException(QuestionErrorCode.QUESTION_NOT_FOUND);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분도 업데이트는 트랜잭션이 보장될 거 같은데 조회랑 같이 한 트랜잭션으로 묶어주지 않아도 괜찮을까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋은 생각입니다!

Copy link
Collaborator

@uri010 uri010 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

프론트분들이 테스트 먼저 하고 싶다고 하셔서 Approve 드립니다!

@sunohkim sunohkim merged commit 533443a into dev Feb 20, 2025
1 check passed
@sunohkim sunohkim deleted the feat/be/question-check branch February 20, 2025 05:58
@sunohkim
Copy link
Collaborator Author

트랜잭션 관련 기능에 대해 코드를 보완했습니다.
#218 을 참고해 주시면 됩니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🌱 BE 백엔드 관련 ✨ Feat 새로운 기능 추가 (기능 개발 및 신규 기능 구현)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants