Skip to content

Conversation

@coli-geonwoo
Copy link
Collaborator

@coli-geonwoo coli-geonwoo commented Sep 4, 2025

🚩 연관 이슈

close #90

🔂 변경 내역

Summary by CodeRabbit

  • 신기능
    • 답변 랭킹에 더 긴 질문 콘텐츠 저장을 지원해 표시/저장 한계를 완화했습니다.
  • 버그 수정
    • 빈(공백) 답변은 랭킹에 반영되지 않도록 처리했습니다.
  • 리팩터
    • 답변 업데이트 처리를 이벤트 기반으로 전환해 업데이트 후 작업을 비동기/트랜잭션 완료 시점에 수행합니다.
    • PR 분석 퍼블리싱 경로 단순화로 전송 안정성을 개선했습니다.
  • 테스트
    • 빈 답변 미반영 및 답변 업데이트 시 이벤트 발행에 대한 테스트를 추가했습니다.
  • 잡무
    • PR 프롬프트 가이드에 질문 길이 255자 제한을 명시했습니다.

@coli-geonwoo coli-geonwoo self-assigned this Sep 4, 2025
@coli-geonwoo coli-geonwoo linked an issue Sep 4, 2025 that may be closed by this pull request
@coderabbitai
Copy link

coderabbitai bot commented Sep 4, 2025

Walkthrough

답변 랭킹 처리를 트랜잭션 종료 후 비동기 이벤트 기반으로 분리하고, 빈(Blank) 답변 무시 로직을 추가했다. AnswerRankingEntity의 content 컬럼을 TEXT로 변경했다. PR 분석 관련 불필요 타입 삭제와 Redis 퍼블리싱 직렬화 제거를 포함하며, 관련 테스트가 추가되었다.

Changes

Cohort / File(s) Summary
Answer 업데이트 이벤트 드리븐 전환
gss-api-app/src/main/java/com/devoops/event/UpdateAnswerEvent.java, gss-api-app/src/main/java/com/devoops/event/listener/AnswerEventListener.java, gss-api-app/src/main/java/com/devoops/service/facade/QuestionFacadeService.java, gss-api-app/src/test/java/com/devoops/service/facade/QuestionFacadeServiceTest.java
Answer 업데이트 시 ApplicationEventPublisher로 UpdateAnswerEvent 발행, @TransactionalEventListener(AFTER_COMMIT) + @async 리스너에서 AnswerRankingService.push 호출로 이전 직접 호출 제거. 테스트로 이벤트 발행 검증 추가.
답변/랭킹 도메인 보강
gss-domain/src/main/java/com/devoops/domain/entity/github/answer/Answer.java, gss-domain/src/main/java/com/devoops/service/answerranking/AnswerRankingService.java, gss-api-app/src/test/java/com/devoops/service/answerranking/AnswerRankingServiceTest.java
Answer에 isBlank() 추가. AnswerRankingService.push에 blank 조기 반환 가드 추가. 빈 컨텐츠가 랭킹에 반영되지 않음을 검증하는 테스트 추가.
JPA 스키마 변경
gss-domain/src/main/java/com/devoops/jpa/entity/github/answer/AnswerRankingEntity.java
questionContent 컬럼을 TEXT로 매핑 변경(columnDefinition = "TEXT").
래포지토리 API 정리
gss-domain/src/main/java/com/devoops/domain/repository/github/answer/AnswerRankingDomainRepository.java, gss-domain/src/main/java/com/devoops/jpa/repository/github/answer/AnswerRankingDomainRepositoryImpl.java
deleteAllInPullRequests(...) 메서드 및 구현 제거.
PR 분석 퍼블리셔 단순화
gss-api-app/src/main/java/com/devoops/event/publisher/PrAnalysisPublisher.java
ObjectMapper 기반 JSON 직렬화 제거, Redis에 객체 리스트를 직접 publish로 단순화. 예외 처리 코드 및 의존성 정리.
타입 제거
gss-api-app/src/main/java/com/devoops/event/AnalyzePrEvent.java
AnalyzePrEvent 레코드 삭제.
클라이언트 설정 가이드라인
gss-client/gss-mcp-client/src/main/resources/application-mcp-client.yml
PR 프롬프트 “question” 길이 255자 제한 가이드 문구 추가(기능 영향 없음).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Controller
  participant QuestionFacadeService as Service
  participant Tx as DB Tx
  participant SpringEvents as AppEventPublisher
  participant AnswerEventListener as Listener
  participant AnswerRankingService as Ranking

  User->>Controller: Update Answer request
  Controller->>Service: updateAnswer(answerId, content, userId)
  activate Service
  Service->>Tx: save/update Answer (within @Transactional)
  Service->>SpringEvents: publish UpdateAnswerEvent(answer, userId)
  deactivate Service
  note over Tx: Commit
  SpringEvents-->>AnswerEventListener: UpdateAnswerEvent (AFTER_COMMIT, async)
  activate AnswerEventListener
  AnswerEventListener->>Ranking: push(answer, userId)
  deactivate AnswerEventListener

  opt Blank content
    note over Ranking: if answer.isBlank() return
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Assessment against linked issues

Objective Addressed Explanation
content 컬럼을 TEXT로 변경 [#90]
유저 답변 저장 트랜잭션 커밋 후 랭킹 커밋(TransactionalEventListener 사용) [#90]
랭킹 처리 비동기 분리로 본 저장 트랜잭션 영향 제거 [#90]

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
PR 분석 이벤트 타입 제거 (gss-api-app/src/main/java/com/devoops/event/AnalyzePrEvent.java) 이슈 #90과 무관한 PR 분석 도메인 타입 삭제임.
Redis 퍼블리셔에서 JSON 직렬화 제거 (gss-api-app/src/main/java/com/devoops/event/publisher/PrAnalysisPublisher.java) 이슈 #90의 답변/랭킹 리팩터링 범위를 벗어나는 메시징 경로 변경.
AnswerRankingDomainRepository.deleteAllInPullRequests 제거 (gss-domain/.../AnswerRankingDomainRepository.java, ...Impl.java) 이슈 #90의 목표와 직접적 연관이 확인되지 않음.
MCP 클라이언트 YAML에 질문 길이 가이드 추가 (gss-client/gss-mcp-client/.../application-mcp-client.yml) 이슈 #90과 관련 없는 클라이언트 텍스트 가이드 변경.

Possibly related PRs

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/#90

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ 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.
    • 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.
  • 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 the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit 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

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • 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.

@github-actions
Copy link

github-actions bot commented Sep 4, 2025

Test Results

0 tests   0 ✅  0s ⏱️
0 suites  0 💤
0 files    0 ❌

Results for commit ad72ca2.

@github-actions
Copy link

github-actions bot commented Sep 4, 2025

📝 Test Coverage Report

Overall Project NaN% NaN% 🍏

There is no coverage information present for the Files changed

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

🧹 Nitpick comments (10)
gss-domain/src/main/java/com/devoops/domain/entity/github/answer/Answer.java (2)

21-23: Micrometer StringUtils 대신 표준 API로 대체하세요.

도메인 계층에서 io.micrometer.common.util.StringUtils 의존은 과합니다. String#isBlank()로 충분하며, null 가능성까지 고려하려면 content == null || content.isBlank()로 처리하면 됩니다.

적용 diff:

-    public boolean isBlank() {
-        return StringUtils.isBlank(content);
-    }
+    public boolean isBlank() {
+        return content == null || content.isBlank();
+    }

3-3: 불필요한 유틸 import 제거.

적용 diff:

-import io.micrometer.common.util.StringUtils;
gss-api-app/src/test/java/com/devoops/service/answerranking/AnswerRankingServiceTest.java (2)

21-23: 미사용 imports 정리 또는 파라미터라이즈드 테스트로 전환.

@ParameterizedTest, @NullAndEmptySource를 사용하지 않습니다. 제거하거나, 아래 제안처럼 공백 케이스까지 커버하는 파라미터 테스트로 바꿔주세요.

불필요 import 제거 diff:

-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.NullAndEmptySource;

(선택) 공백/탭까지 커버하는 파라미터 테스트 예시:

@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {" ", "\t", "\n"})
void 비어있거나_공백이면_랭킹에_반영하지_않는다(String body) { ... }

52-65: 빈 답변 미반영 테스트 추가는 적절합니다.

시나리오가 명확하고, 랭킹이 비어 있는지 검증이 정확합니다. 공백 전용 케이스(예: " ", "\t")도 추가되면 더 견고해집니다.

gss-api-app/src/test/java/com/devoops/service/facade/QuestionFacadeServiceTest.java (2)

121-147: 단일 이벤트 단정식을 더 간결하게 리팩터링하세요.

동일 스트림에 대해 allMatch를 두 번 사용하는 대신 단일 요소 단정으로 의도를 명확히 할 수 있습니다.

-            assertThat(applicationEvents.stream(UpdateAnswerEvent.class))
-                    .hasSize(1)
-                    .allMatch(event -> event.getAnswer().getId().equals(answer1.getId()))
-                    .allMatch(event -> event.getUserId() == user.getId());
+            assertThat(applicationEvents.stream(UpdateAnswerEvent.class))
+                    .singleElement()
+                    .satisfies(event -> {
+                        assertThat(event.getAnswer().getId()).isEqualTo(answer1.getId());
+                        assertThat(event.getUserId()).isEqualTo(user.getId());
+                    });

133-147: AFTER_COMMIT 리스너 동작 보장 여부를 별도 통합 테스트로 검증 필요.

본 테스트는 “발행”만 확인합니다. @TransactionalEventListener(phase = AFTER_COMMIT)@Async가 실제 커밋 이후에 호출되는지(롤백 시 미호출 포함) SpyBean/CountDownLatch 등을 이용해 통합 테스트로 한 번 더 검증해 주세요.

원하시면 통합 테스트 스캐폴드를 드리겠습니다.

gss-api-app/src/main/java/com/devoops/event/listener/AnswerEventListener.java (2)

18-24: 비동기 예외 로깅/관측 가능성을 확보하세요.

@Async에서는 예외가 호출자에게 전파되지 않습니다. 최소한 로깅을 추가해 실패를 관측 가능하게 하거나, 커스텀 AsyncUncaughtExceptionHandler를 등록하세요.

-import lombok.RequiredArgsConstructor;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
@@
-@RequiredArgsConstructor
+@RequiredArgsConstructor
+@Slf4j
 public class AnswerEventListener {
@@
-        answerRankingService.push(answer, userId);
+        try {
+            answerRankingService.push(answer, userId);
+        } catch (Exception e) {
+            log.error("Failed to push answer ranking. answerId={}, userId={}", answer.getId(), userId, e);
+        }

21-23: 이벤트 페이로드를 “엔티티” 대신 식별자 위주로 경량화 고려.

지연 로딩/분리(detached) 엔티티 접근 리스크를 줄이기 위해 answerId만 전달하고, 리스너에서 조회해 사용하는 방식을 검토해 주세요. 성능/일관성 요구사항에 따라 선택입니다.

gss-api-app/src/main/java/com/devoops/event/UpdateAnswerEvent.java (1)

7-18: 현대식 이벤트 스타일로 단순화 가능합니다.

Spring 4.2+에서는 ApplicationEvent 상속이 필수 아닙니다. 단순 POJO/record 이벤트로 전환하면 보일러플레이트가 줄고 테스트도 용이합니다.

-package com.devoops.event;
-
-import com.devoops.domain.entity.github.answer.Answer;
-import lombok.Getter;
-import org.springframework.context.ApplicationEvent;
-
-@Getter
-public class UpdateAnswerEvent extends ApplicationEvent {
-
-    private final Answer answer;
-    private final long userId;
-
-    public UpdateAnswerEvent(Object source, Answer answer, long userId) {
-        super(source);
-        this.answer = answer;
-        this.userId = userId;
-    }
-}
+package com.devoops.event;
+
+import com.devoops.domain.entity.github.answer.Answer;
+
+public record UpdateAnswerEvent(Answer answer, long userId) {}

퍼블리셔 호출부도 다음처럼 간단해집니다(참고):

eventPublisher.publishEvent(new UpdateAnswerEvent(answer, userId));
gss-api-app/src/main/java/com/devoops/service/facade/QuestionFacadeService.java (1)

45-52: 대량 업데이트 시 랭킹 이벤트 미발행 — 의도인지 확인 필요.

updateAllAnswers에서는 이벤트를 발행하지 않아 랭킹 갱신이 누락됩니다. 의도된 동작인지 확인 부탁드립니다. 만약 갱신이 필요하다면:

  • AnswerPutRequestuserId를 포함하거나,
  • 현재 사용자 컨텍스트(SecurityContext 등)에서 userId를 취득하여, 결과 Answers를 순회하며 이벤트를 발행하세요.

원하시면 두 방식에 대한 구체 패치 제안을 드리겠습니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f1ff8c7 and ad72ca2.

📒 Files selected for processing (13)
  • gss-api-app/src/main/java/com/devoops/event/AnalyzePrEvent.java (0 hunks)
  • gss-api-app/src/main/java/com/devoops/event/UpdateAnswerEvent.java (1 hunks)
  • gss-api-app/src/main/java/com/devoops/event/listener/AnswerEventListener.java (1 hunks)
  • gss-api-app/src/main/java/com/devoops/event/publisher/PrAnalysisPublisher.java (1 hunks)
  • gss-api-app/src/main/java/com/devoops/service/facade/QuestionFacadeService.java (3 hunks)
  • gss-api-app/src/test/java/com/devoops/service/answerranking/AnswerRankingServiceTest.java (2 hunks)
  • gss-api-app/src/test/java/com/devoops/service/facade/QuestionFacadeServiceTest.java (2 hunks)
  • gss-client/gss-mcp-client/src/main/resources/application-mcp-client.yml (1 hunks)
  • gss-domain/src/main/java/com/devoops/domain/entity/github/answer/Answer.java (2 hunks)
  • gss-domain/src/main/java/com/devoops/domain/repository/github/answer/AnswerRankingDomainRepository.java (0 hunks)
  • gss-domain/src/main/java/com/devoops/jpa/entity/github/answer/AnswerRankingEntity.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/jpa/repository/github/answer/AnswerRankingDomainRepositoryImpl.java (0 hunks)
  • gss-domain/src/main/java/com/devoops/service/answerranking/AnswerRankingService.java (1 hunks)
💤 Files with no reviewable changes (3)
  • gss-domain/src/main/java/com/devoops/domain/repository/github/answer/AnswerRankingDomainRepository.java
  • gss-domain/src/main/java/com/devoops/jpa/repository/github/answer/AnswerRankingDomainRepositoryImpl.java
  • gss-api-app/src/main/java/com/devoops/event/AnalyzePrEvent.java
🧰 Additional context used
🧬 Code graph analysis (1)
gss-api-app/src/main/java/com/devoops/service/facade/QuestionFacadeService.java (3)
gss-domain/src/main/java/com/devoops/service/answerranking/AnswerRankingService.java (1)
  • Service (13-46)
gss-api-app/src/main/java/com/devoops/service/facade/RepositoryFacadeService.java (1)
  • Service (25-93)
gss-api-app/src/main/java/com/devoops/service/facade/PullRequestFacadeService.java (1)
  • Service (17-54)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-and-push
🔇 Additional comments (4)
gss-api-app/src/main/java/com/devoops/event/publisher/PrAnalysisPublisher.java (1)

19-19: RedisTemplate value serializer 설정 검증 필요

  • gss-domain/src/main/java/com/devoops/config/RedisConfig.java에서 GenericJackson2JsonRedisSerializer가 value serializer로 설정된 것이 확인되었습니다; 구독자(리스너) 측에서도 동일 설정인지 반드시 검증하세요.
  • 타입 불일치로 인한 역직렬화 예외 방지를 위해 계약 테스트 추가 또는 JSON 문자열 회귀 옵션 준비를 권장합니다.
gss-domain/src/main/java/com/devoops/service/answerranking/AnswerRankingService.java (1)

25-27: 로깅 및 트랜잭션 경계 추가 검토

  • answer.isBlank() 블럭에 호출 스킵 로그 남기도록
    log.debug("Skip ranking push: blank answer. userId={}, questionId={}", userId, answer.getQuestionId());
  • 내부 삭제→저장 2단계 쓰기를 하나의 트랜잭션으로 묶기 위해 클래스 또는 push()@Transactional 추가 검토
  • 이벤트 리스너가 기본 phase(AFTER_COMMIT)로 동작하는지 직접 확인

예시:

 @Service
 @RequiredArgsConstructor
 @Slf4j
+@Transactional
 public class AnswerRankingService {
     public void push(Answer answer, long userId) {
         if (answer.isBlank()) {
-            return;
+            log.debug("Skip ranking push: blank answer. userId={}, questionId={}", userId, answer.getQuestionId());
+            return;
         }
         // …
gss-api-app/src/test/java/com/devoops/service/facade/QuestionFacadeServiceTest.java (1)

17-17: 이벤트 캡처 설정 적절합니다.

UpdateAnswerEventRecordApplicationEvents/ApplicationEvents를 통한 캡처 구성이 테스트 목적에 부합합니다.

Also applies to: 20-20, 24-26

gss-api-app/src/main/java/com/devoops/service/facade/QuestionFacadeService.java (1)

38-43: 트랜잭션 내 업데이트 후 이벤트 발행 흐름 적절합니다.

@Transactional + AFTER_COMMIT 조합으로 랭킹 저장이 본 저장을 방해하지 않도록 잘 분리됐습니다.

Comment on lines +18 to +24
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void updateAnswer(UpdateAnswerEvent updateAnswerEvent) {
Answer answer = updateAnswerEvent.getAnswer();
long userId = updateAnswerEvent.getUserId();
answerRankingService.push(answer, userId);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

리스너에 트랜잭션 경계를 추가하세요 (특히 @async 환경).

커밋 후 다른 스레드에서 실행되므로, JPA/MyBatis를 사용한다면 쓰기 작업에 트랜잭션이 없어 실패(예: TransactionRequiredException)할 수 있습니다. 리스너 메서드에 REQUIRES_NEW 트랜잭션을 부여해 랭킹 저장을 독립적으로 보장하세요.

 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
 import org.springframework.transaction.event.TransactionPhase;
 import org.springframework.transaction.event.TransactionalEventListener;
@@
     @Async
     @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
+    @Transactional(propagation = Propagation.REQUIRES_NEW)
     public void updateAnswer(UpdateAnswerEvent updateAnswerEvent) {
         Answer answer = updateAnswerEvent.getAnswer();
         long userId = updateAnswerEvent.getUserId();
         answerRankingService.push(answer, userId);
     }

대안: AnswerRankingService.push(...) 자체를 @Transactional로 감싸도 됩니다(서비스 계층 표준화).

📝 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
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void updateAnswer(UpdateAnswerEvent updateAnswerEvent) {
Answer answer = updateAnswerEvent.getAnswer();
long userId = updateAnswerEvent.getUserId();
answerRankingService.push(answer, userId);
}
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
@Component
public class AnswerEventListener {
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateAnswer(UpdateAnswerEvent updateAnswerEvent) {
Answer answer = updateAnswerEvent.getAnswer();
long userId = updateAnswerEvent.getUserId();
answerRankingService.push(answer, userId);
}
}
🤖 Prompt for AI Agents
In gss-api-app/src/main/java/com/devoops/event/listener/AnswerEventListener.java
around lines 18 to 24, the @Async listener runs after commit on another thread
and currently has no transactional boundary, which can cause
TransactionRequiredException when performing writes; add a new transaction
boundary (e.g., annotate the listener method with a propagation of REQUIRES_NEW)
so the ranking save runs in its own transaction, or alternatively make
AnswerRankingService.push(...) @Transactional to ensure the write executes
within a transaction.

- "summaryDetail"은 변경 내용을 항목별로 요약한 제목(title) + 설명(description) 쌍으로 구성해.
- "category"는 기술적인 관점에서 PR 코드 변경 내용을 반영하여 선택해 (예: 성능, 보안, 확장성, 유지보수성, 테스트 등)
- "question"은 각 category에 대해 기술 면접에서 사용할 수 있는 질문이어야 해.
- "question"은 가능한 255자가 넘지 않도록 짧고 명료하게 질문해.
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

프롬프트 지시만으로는 255자 제한을 보장하지 못합니다 — 스키마/후처리로 강제하세요

LLM 프롬프트에만 의존하면 간헐적으로 255자를 초과할 수 있어 동일 계열의 제약 위반이 재발할 수 있습니다. JSON 스키마에 maxLength=255를 명시하고(format-message가 주입하는 스키마에 반영), 파서 단계에서 초과 문항은 안전하게 잘라내는 후처리를 추가하는 것을 권장합니다. 라인 문구도 “가능한”이 아닌 “반드시/초과 시 절단”으로 바꿔 의도를 강화해 주세요.

Apply this diff to tighten the instruction:

-      - "question"은 가능한 255자가 넘지 않도록 짧고 명료하게 질문해.
+      - "question"은 반드시 255자 이내로 작성하고, 초과 시 앞 252자만 남기고 '...'로 절단해.

다음 스크립트로 레포 내 강제 수단이 있는지 확인해 주세요(스키마 maxLength, DTO @SiZe, DB 컬럼 등):

필요하시면: (1) 스키마에 maxLength 추가, (2) 파서 후처리 유틸(초과 시 안전 절단), (3) 경계 테스트(254/255/256자) 작성까지 패치 드리겠습니다.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== JSON 스키마에 maxLength 존재 여부 체크 =="
rg -n -C2 -i --type=json --type=yaml 'question"|questions"|prompt.*question|maxLength' || true

echo "== DTO/검증 애노테이션(@Size<=255) 존재 여부 체크 =="
rg -n -C2 -i --type=java --type=kt '@Size\s*\(\s*max\s*=\s*255' || true
rg -n -C2 -i --type=java --type=kt '\bString\s+question\b|\bvar\s+question\b|\bval\s+question\b' || true

echo "== DB 제약(컬럼 길이) 존재 여부 체크 =="
rg -n -C2 -i --type=sql 'question.*varchar\s*\(\s*255\s*\)|varchar\s*\(\s*255\s*\).*question' || true

Length of output: 2225


프롬프트 문구 및 제약 강제 적용

  • gss-client/gss-mcp-client/src/main/resources/application-mcp-client.yml:11을
    -      - "question"은 가능한 255자가 넘지 않도록 짧고 명료하게 질문해.
    +      - "question"은 반드시 255자 이내로 작성하고, 초과 시 앞 252자만 남기고 '...'로 절단해.
  • 프로젝트 전반 JSON 스키마(application-mcp-client.yml 등)에 question 필드에 maxLength: 255 명시
  • 응답 파서 단계에 255자 초과 시 앞 252자만 남기고 ‘...’로 절단하는 후처리 로직 추가
📝 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
- "question"가능한 255자가 넘지 않도록 짧고 명료하게 질문해.
- "question"반드시 255자 이내로 작성하고, 초과 시 앞 252자만 남기고 '...'로 절단해.
🤖 Prompt for AI Agents
gss-client/gss-mcp-client/src/main/resources/application-mcp-client.yml around
line 11: the project requires enforcing the question length constraint and
truncation behavior; add "maxLength: 255" to the question field in the JSON
schema(s) (application-mcp-client.yml and any other schema files used across the
project) so validators reject >255 characters, and implement a response-parser
post-processing step that, when the parsed question exceeds 255 characters,
truncates it to the first 252 characters and appends "..." before
returning/storing the value.

Comment on lines +29 to 30
@Column(name = "content", columnDefinition = "TEXT")
private String questionContent;
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

DB 컬럼을 TEXT로 고정하면 벤더 종속이 증가합니다. @lob로 매핑하고 마이그레이션을 동반하세요.

  • JPA 이식성: @Lob(+ @Column(name="content")) 조합이 벤더 중립적입니다.
  • 운영 필수: 엔티티 변경만으로는 프로덕션 스키마가 바뀌지 않습니다. Flyway/Liquibase 마이그레이션이 반드시 필요합니다.

선호 diff:

-    @NotBlank
-    @Column(name = "content", columnDefinition = "TEXT")
+    @NotBlank
+    @Lob
+    @Column(name = "content")
     private String questionContent;

필요 import:

import jakarta.persistence.Lob;

마이그레이션 존재 여부 확인 스크립트:

주의:

  • 이슈([REFACTOR] ANSWER_RANKING_ENTITY 로직 분리 #90) 원인은 “사용자 답변(content)” 255 제한입니다. 현재 변경은 Ranking Entity의 content입니다. 실제 사용자 답변 테이블(예: AnswerEntity)의 컬럼 타입도 TEXT/CLOB로 변경되었는지 반드시 확인해주세요. 필요시 마이그레이션 초안 드리겠습니다.

🏁 Script executed:

#!/bin/bash
# 1) Answer(사용자 답변) 및 Ranking의 content 컬럼 타입 변경 반영 여부 점검

# AnswerEntity 매핑에서 content 필드와 컬럼 정의 확인
rg -nP -C2 '(class\s+AnswerEntity\b)|(@Column\([^)]*name\s*=\s*"content"[^)]*\))' --g 'gss-**/*.java'

# "TEXT/CLOB" 정의가 어디에 쓰였는지 검색
rg -nP 'columnDefinition\s*=\s*"TEXT"|@Lob' --g 'gss-**/*'

# 마이그레이션 SQL에 content 타입 변경이 있는지 검색
rg -nP -C2 'ALTER\s+TABLE.+content.+(TEXT|CLOB)|MODIFY\s+COLUMN\s+`?content`?.+(TEXT|LONGTEXT)|TYPE\s+TO\s+(TEXT|CLOB)' --g '**/*.sql'

Length of output: 1519


🏁 Script executed:

#!/bin/bash
# 1) AnswerEntity 소스 파일 경로 찾기
rg -lP 'class\s+AnswerEntity' --glob '*.java'

# 2) AnswerEntity 및 AnswerRankingEntity에서 content 컬럼 어노테이션 확인
rg -n '@Column\([^)]*name\s*=\s*"content"[^)]*\)' --glob '*.java'

# 3) @Lob 어노테이션 사용 여부 확인
rg -n '@Lob' --glob '*.java'

# 4) SQL 마이그레이션에서 content 컬럼 타입 변경 반영 여부 확인
rg -nP -C2 'ALTER\s+TABLE.+content.+(TEXT|CLOB)|MODIFY\s+COLUMN.+content.+(TEXT|LONGTEXT)|TYPE\s+TO\s+(TEXT|CLOB)' --glob '*.sql'

Length of output: 652


🏁 Script executed:

#!/bin/bash
# 1) AnswerEntity content 필드 매핑 확인
rg -nP -C5 'private\s+String\s+content' gss-domain/src/main/java/com/devoops/jpa/entity/github/answer/AnswerEntity.java

# 2) 프로젝트 내 SQL 파일 목록
find gss-domain -type f -name "*.sql"

# 3) SQL 마이그레이션에서 content 컬럼 변경 반영 검색
rg -nP 'content' --glob 'gss-domain/**/*.sql' -C2

Length of output: 578


🏁 Script executed:

#!/bin/bash
# 전체 리포지토리에서 SQL 마이그레이션 파일 확인
find . -type f -name '*.sql'

# SQL 파일에서 content 컬럼 변경 반영 여부 검색
rg -nP 'content' --glob '*.sql' -C2

Length of output: 215


@lob 매핑 적용 및 마이그레이션 스크립트 추가 필요

  • gss-domain/src/main/java/com/devoops/jpa/entity/github/answer/AnswerRankingEntity.java (29–31):
    -    @Column(name = "content", columnDefinition = "TEXT")
    +    @Lob
    +    @Column(name = "content")
  • gss-domain/src/main/java/com/devoops/jpa/entity/github/answer/AnswerEntity.java (33–34):
    -    @Column(columnDefinition = "TEXT")
    +    @Lob
    +    @Column(name = "content")
  • Flyway/Liquibase 마이그레이션 SQL 파일이 확인되지 않습니다. 기존 content 컬럼의 VARCHAR(255) → TEXT/CLOB 변경 스크립트를 반드시 추가하세요.
  • 필요한 import:
    import jakarta.persistence.Lob;

@coli-geonwoo coli-geonwoo merged commit 6f4614c into develop Sep 4, 2025
5 checks passed
@coli-geonwoo coli-geonwoo deleted the refactor/#90 branch September 4, 2025 13:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[REFACTOR] ANSWER_RANKING_ENTITY 로직 분리

2 participants