Skip to content

[REFACTOR] 복수의 사용자가 같은 레포를 트래킹할 때의 시나리오 고려#75

Merged
coli-geonwoo merged 22 commits intodevelopfrom
refactor/#74
Aug 18, 2025
Merged

[REFACTOR] 복수의 사용자가 같은 레포를 트래킹할 때의 시나리오 고려#75
coli-geonwoo merged 22 commits intodevelopfrom
refactor/#74

Conversation

@coli-geonwoo
Copy link
Collaborator

@coli-geonwoo coli-geonwoo commented Aug 11, 2025

🚩 연관 JIRA 이슈

jira issue url:

🔂 변경 내역

🗣️ 리뷰 요구사항 (선택)

Summary by CodeRabbit

  • New Features
    • 중앙 서버에서 웹훅 등록/삭제 관리 기능 추가(사용자별 트래킹 카운트 포함)
  • Refactor
    • 웹훅 책임 전담 서비스로 이동 및 기존 웹훅 API 제거, 저장소 등록이 사용자 단위로 중복 방지되도록 변경
    • 도메인/영속성 패키지 대규모 재구성
  • Tests
    • 웹훅 등록·삭제 흐름 단위 테스트 추가, 기존 관련 테스트 정리/제거
  • Chores
    • 로컬 DB를 H2(in-memory)로 전환 및 H2 콘솔 활성화
    • 웹훅 관련 오류 코드 2건 추가

@coderabbitai
Copy link

coderabbitai bot commented Aug 11, 2025

Walkthrough

GitHub 도메인 패키지를 세분화(answer/pr/question/repo/token/webhook), 웹훅 관리를 WebHookService로 분리하고 관련 레포지토리·JPA·DTO·서비스·테스트 참조를 일괄 갱신했으며 로컬 DB 설정을 MySQL→H2로 변경하였다.

Changes

Cohort / File(s) Summary
웹훅 서비스·GitHubService 변경
gss-api-app/src/main/java/com/devoops/service/github/WebHookService.java, gss-api-app/src/main/java/com/devoops/service/GitHubService.java, gss-api-app/src/main/java/com/devoops/controller/GithubController.java
WebHookService 신설(registerWebhook/deleteWebhook, 레지스트리 카운트 연동). GitHubService에서 웹훅 관련 메서드 제거 및 getUserPullRequests 시그니처 변경. 기존 GithubController 삭제.
Facade·서비스 호출 교체
gss-api-app/src/main/java/com/devoops/service/facade/RepositoryFacadeService.java, gss-domain/src/main/java/com/devoops/service/repository/RepositoryService.java
Repository 저장/삭제 시 웹훅 작업 호출을 GitHubService → WebHookService로 교체. RepositoryService는 사용자 단위 exists 검사로 변경(@requiredargsconstructor 적용, 일부 의존성 제거).
MCP 웹훅 처리 흐름
gss-mcp-app/src/main/java/com/devoops/dto/request/GitHubWebhookEventRequest.java, gss-mcp-app/src/main/java/com/devoops/service/webhook/WebhookFacadeService.java, gss-mcp-app/src/main/java/com/devoops/controller/webhook/WebhookController.java
isMerged 반환형 Boolean→boolean 변경. WebhookFacadeService가 externalId+userId로 레포 조회해 isTracking 체크 후 처리하도록 흐름 변경(합병 여부/트래킹 여부에 따라 PR 저장·이벤트 발행).
도메인 엔티티 패키지 재구성
gss-domain/src/main/java/com/devoops/domain/entity/github/... (answer/, pr/, question/, repo/, token/, webhook/)
Answer/AnswerRanking/Answers, PullRequest/RecordStatus, Question, GithubRepository, GithubToken, GithubWebhook 등 다수 클래스 패키지 이동 및 GithubRepoRegistryCount 도메인 추가.
도메인 리포지토리 인터페이스 재배치 및 API 변경
gss-domain/src/main/java/com/devoops/domain/repository/github/... (repo/, token/, webhook/, answer/, pr/, question/)
리포지토리 인터페이스 서브패키지 이동. GithubRepoDomainRepository에 user-scoped 메서드 추가(existsByExternalIdAndUserId, findByExternalIdAndUserId). 토큰 리포지토리 재정의 및 웹훅 레지스트리 카운트 리포지토리 추가.
JPA 엔티티·JpaRepository 이동 및 웹훅 카운트 엔터티 추가
gss-domain/src/main/java/com/devoops/jpa/entity/github/..., gss-domain/src/main/java/com/devoops/jpa/repository/github/...
JPA 엔티티 및 JpaRepository들을 서브패키지로 이동. GithubRepoRegistryCountEntity 및 JpaRepository 추가(증감 JPQL 쿼리 포함). 웹훅 관련 JPA/리포지토리도 webhook 패키지로 이동.
DTO·컨트롤러·응답 타입 참조 갱신
gss-api-app/src/main/java/com/devoops/controller/..., gss-api-app/src/main/java/com/devoops/dto/response/*
여러 컨트롤러/DTO에서 도메인 타입 import 경로 갱신(예: PullRequest, RecordStatus, Answer 등). 일부 DTO의 공개 시그니처에서 참조되는 타입의 패키지 해석 변경.
테스트 변경 및 보강
gss-api-app/src/test/..., gss-domain/src/testFixtures/..., gss-domain/src/test/...
전반적 import 경로 갱신. WebHookServiceTest 추가(등록/삭제/카운트 케이스). 기존 GitHubServiceTest 삭제. WebhookGenerator.generate 시그니처에 trackingCount 파라미터 추가 등 테스트 보수.
예외코드 확장
gss-common/src/main/java/com/devoops/exception/errorcode/ErrorCode.java
GITHUB_REPOSITORY_REGISTRY_COUNT_NOT_FOUND 및 INVALID_GITHUB_REPOSITORY_REGISTRY_COUNT 2개 5XX 코드 추가.
로컬 DB 설정 변경
gss-domain/src/main/resources/application-db.yml
로컬 프로파일 DB를 MySQL→H2 메모리로 전환, H2 콘솔 활성화 및 JPA 설정(create-drop 등) 명시.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant RepoFacade as RepositoryFacadeService
  participant WebHookSvc as WebHookService
  participant GH as GitHubClient
  participant Domain as Domain Repositories

  User->>RepoFacade: save(repository)
  RepoFacade->>WebHookSvc: registerWebhook(user, repositoryId)
  WebHookSvc->>Domain: find repository by id and user
  alt registry exists
    WebHookSvc->>Domain: plusCount(externalId)
  else first time
    WebHookSvc->>Domain: get token by userId
    WebHookSvc->>GH: createWebhook(Bearer token, externalId, mcpWebhookUrl)
    WebHookSvc->>Domain: save GithubWebhook
    WebHookSvc->>Domain: save registryCount=1
  end
Loading
sequenceDiagram
  autonumber
  participant GH as GitHub
  participant MCP as WebhookController
  participant Facade as WebhookFacadeService
  participant RepoRepo as GithubRepoDomainRepository
  participant PRRepo as PullRequestDomainRepository
  participant EventBus as ApplicationEventPublisher

  GH-->>MCP: POST webhook event (payload)
  MCP->>Facade: createQuestionWithWebhookEvent(request, triggerUser)
  Facade->>RepoRepo: findByExternalIdAndUserId(request.repositoryId, user.id)
  alt repo.isTracking == true and request.isMerged == true
    Facade->>PRRepo: savePullRequest(repo.id, payload)
    Facade->>EventBus: publish QuestionCreateEvent(triggerUser.githubToken, PR)
  else
    Facade-->>MCP: ignore/no-op
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ 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/#74

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 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 Aug 11, 2025

Test Results

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

Results for commit 5b83e10.

♻️ This comment has been updated with latest results.

@github-actions
Copy link

github-actions bot commented Aug 11, 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: 6

🔭 Outside diff range comments (10)
gss-domain/src/main/java/com/devoops/domain/entity/github/question/Question.java (1)

14-18: boolean 필드 isAnswered의 Lombok 게터명 혼동 가능성

isAnswered 필드는 Lombok가 isIsAnswered() 또는 getIsAnswered()를 생성할 수 있어 호출부 혼동을 야기할 수 있습니다. 명시적 게터를 추가하거나 필드명을 answered로 변경하는 것을 권장합니다.

권장(보수적) 수정: 명시적 게터 추가

 public class Question {
@@
     private final boolean isAnswered;
@@
     public Question(long pullRequestId, String category, String content) {
         this(null, pullRequestId, category, content, false);
     }
+
+    public boolean isAnswered() {
+        return isAnswered;
+    }
 }
gss-api-app/src/main/java/com/devoops/dto/response/RepositorySaveResponse.java (1)

20-22: NPE 위험: repository.getId() 오토언박싱

GithubRepository.getId()Long이며, null일 경우 long으로 언박싱 시 NPE가 발생합니다. 저장 직후 응답이라면 null이 아니어야 하므로 명확히 검증하거나 DTO 필드를 Long으로 바꾸는 방법이 있습니다.

옵션 A(권장: 계약 명확화) — null 금지 검증 추가:

 package com.devoops.dto.response;

 import com.devoops.domain.entity.github.repo.GithubRepository;
+import java.util.Objects;
@@
     public RepositorySaveResponse(GithubRepository repository) {
-        this(repository.getId(), repository.getUserId(), repository.getName(), repository.getUrl());
+        this(
+            Objects.requireNonNull(repository.getId(), "repository.id must not be null"),
+            repository.getUserId(),
+            repository.getName(),
+            repository.getUrl()
+        );
     }

옵션 B — DTO의 idLong으로 변경(호출부 영향 가능):

-public record RepositorySaveResponse(
+public record RepositorySaveResponse(
         @Schema(description = "레포지토리 id", example = "1")
-        long id,
+        Long id,
@@
 ) {
gss-domain/src/main/java/com/devoops/command/request/PullRequestCreateCommand.java (1)

51-66: PullRequestCreateCommand.toDomainEntity()에서 recordStatus 반영 필요

toDomainEntity()가 항상 RecordStatus.PENDING을 사용해, 커맨드 객체의 recordStatus 필드 값이 도메인 엔티티로 전달되지 않고 있습니다. 다음과 같이 수정해주세요:

• 수정 대상 파일
gss-domain/src/main/java/com/devoops/command/request/PullRequestCreateCommand.java
(라인 51–66)

• 변경 예시

     public PullRequest toDomainEntity() {
-        return new PullRequest(
+        return new PullRequest(
             null,
             repositoryId,
             userId,
             title,
             description,
             summary,
             summaryDetail,
             pullRequestUrl,
             externalId,
-            RecordStatus.PENDING,
+            recordStatus,
             mergedAt,
             tag
         );
     }

• 추가 제안

  • PENDING 외 상태(예: MERGED, CLOSED 등)인 경우에도 올바르게 매핑되는지 단위 테스트를 작성해 검증하세요.
  • 커맨드 객체의 recordStatus가 널이 될 수 있는지, 기본값 설정이 필요한지 여부도 함께 확인 바랍니다.
gss-api-app/src/main/java/com/devoops/dto/response/RepositoryPullRequestResponse.java (1)

34-42: RepositoryPullRequestResponse 생성자에 mergedAt null 검사 추가 필요
PullRequest.getMergedAt()가 null일 경우 toLocalDate() 호출 시 NPE가 발생합니다. 다음 중 하나를 선택해 주세요:

  • DTO가 “병합 완료된 PR 전용”임을 보장하고, Objects.requireNonNull 등으로 사전조건 검증 추가
  • 미병합(PR 상태가 PENDING)도 포함된다면 DTO에서 @NotNull 제거 및 스키마에 nullable = true 명시

수정 예시 (사전조건 검증 추가):

--- a/gss-api-app/src/main/java/com/devoops/dto/response/RepositoryPullRequestResponse.java
+++ b/gss-api-app/src/main/java/com/devoops/dto/response/RepositoryPullRequestResponse.java
@@ public record RepositoryPullRequestResponse( 
-        pullRequest.getMergedAt().toLocalDate(),
+        Objects.requireNonNull(pullRequest.getMergedAt(), "mergedAt must not be null").toLocalDate(),
  • 파일: gss-api-app/src/main/java/com/devoops/dto/response/RepositoryPullRequestResponse.java
  • 위치: 생성자 내부 (34–42라인)
gss-domain/src/main/java/com/devoops/jpa/repository/user/UserDomainRepositoryImpl.java (2)

29-31: 일관된 도메인 예외로 교체 제안

RuntimeException 대신 기존에 사용 중인 GssException(ErrorCode.USER_NOT_FOUND) 등 도메인 예외로 통일하는 것이 좋습니다.

-                .orElseThrow(() -> new RuntimeException("EntityNotFound 공통 예외 처리 필요"))
+                .orElseThrow(() -> new GssException(ErrorCode.USER_NOT_FOUND))

51-53: 유저 토큰 중복 방지: user_id에 DB 레벨 유니크 제약 추가 필요

아래 사항을 반영해 주세요.

  • Entity 레벨:
    • gss-domain/src/main/java/com/devoops/jpa/entity/github/token/GithubTokenEntity.javauserId 필드에
      @Column(name = "user_id", unique = true, nullable = false)
      private long userId;
      와 같이 unique = true 제약을 추가
  • 마이그레이션 스크립트:
    • Flyway 또는 Liquibase 기반 SQL/DDL 스크립트로 기존 테이블에 ALTER TABLE github_tokens ADD CONSTRAINT … UNIQUE (user_id); 형태의 제약을 추가
    • 이미 중복된 레코드가 존재할 경우 삭제 또는 병합 로직을 포함하여 데이터 정합성 확보
  • 추가 고려 사항:
    • 중복 삽입 시 예외 발생 대신 업서트(upsert)/교체 로직을 구현할지 결정
    • 토큰 갱신 시점에 기존 레코드를 덮어쓰도록 서비스 레이어 로직 보완
gss-domain/src/main/java/com/devoops/jpa/entity/github/token/GithubTokenEntity.java (3)

27-31: DB 제약 강화: userId 유니크/NOT NULL, token 길이 확장/NOT NULL 권장

중복 토큰/NULL 저장 방지를 위해 필드 제약을 명시하는 것이 안전합니다. GitHub 토큰 길이가 50을 넘을 수 있으므로 여유 있게 확장하는 것을 권장합니다.

-    private long userId;
+    @Column(name = "user_id", nullable = false, unique = true)
+    private long userId;
 
-    @Nonnull
-    @Column(length = 50)
-    private String token;
+    @Nonnull
+    @Column(name = "token", length = 255, nullable = false)
+    private String token;

29-33: 민감정보(토큰) 암호화 저장 권장

토큰을 평문으로 저장하는 대신 JPA AttributeConverter 기반 필드 암복호화(예: Jasypt, 자체 AES/GCM) 적용을 권장합니다. 최소한 로깅 마스킹도 병행해 주세요.

예시(Converter 추가, 외부 파일로 구현):

import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;

@Converter
public class TokenEncryptConverter implements AttributeConverter<String, String> {
    @Override
    public String convertToDatabaseColumn(String attribute) {
        if (attribute == null) return null;
        // TODO: KMS/별도 키 관리 하에 암호화
        return Encryptor.encrypt(attribute);
    }
    @Override
    public String convertToEntityAttribute(String dbData) {
        if (dbData == null) return null;
        return Encryptor.decrypt(dbData);
    }
}

적용:

-    @Column(name = "token", length = 255, nullable = false)
+    @Column(name = "token", length = 255, nullable = false)
+    @Convert(converter = TokenEncryptConverter.class)
     private String token;

16-21: DB 참조 무결성(FK/ON DELETE CASCADE) 설정 검증 및 적용 필요

현재 코드베이스 내에서 github_tokens 테이블이 users(id)를 참조하는 외래키 제약이나 삭제 전파 설정이 확인되지 않습니다. 고아 토큰이 남지 않도록 아래를 검토·조치해주세요.

• 마이그레이션 스크립트(예: src/main/resources/db/migration/** 또는 Liquibase changelog)에서

  • github_tokens.user_idusers.id FOREIGN KEY 제약 추가
  • ON DELETE CASCADE 또는 애플리케이션 삭제 전파 정책 설정
    • JPA @JoinColumn(foreignKey = @ForeignKey(...)) 또는 DDL 스크립트를 통해 스키마에 반영

예시 SQL 스니펫:

ALTER TABLE github_tokens
  ADD CONSTRAINT fk_github_tokens_user
    FOREIGN KEY (user_id) REFERENCES users(id)
    ON DELETE CASCADE;
gss-domain/src/main/java/com/devoops/jpa/repository/github/question/QuestionDomainRepositoryImpl.java (1)

30-37: saveAll 메서드의 pullRequestId 파라미터 미사용
인터페이스(QuestionDomainRepository)와 구현체(QuestionDomainRepositoryImpl.saveAll)에 pullRequestId 매개변수가 선언되어 있으나, 구현부에서는 전혀 사용되지 않아 혼동을 초래하고 유지보수 비용을 증가시킬 수 있습니다.

옵션:

  • 메서드 시그니처에서 pullRequestId 삭제 (인터페이스, 구현체, 호출부 일괄 수정)
  • QuestionEntity.from(...) 호출 전에 pullRequestId를 주입하여 엔티티 생성 로직에 반영해 PR 연관관계를 보장

도메인 규칙에 맞는 방향을 선택해 주세요.

🧹 Nitpick comments (25)
gss-domain/src/main/resources/application-db.yml (3)

16-20: H2 URL 옵션 보강 제안: MySQL 호환 + 세션 유지성 확보

로컬에서 MySQL과의 호환성과 in-memory DB의 생명주기 이슈를 줄이기 위해 아래 옵션 추가를 권장합니다.

  • MODE=MySQL: MySQL 문법/타입 호환성
  • DB_CLOSE_DELAY=-1, DB_CLOSE_ON_EXIT=FALSE: 커넥션 풀/컨텍스트 종료 시 데이터 손실 방지
  • DATABASE_TO_UPPER=false: 테이블/컬럼명 케이스 보존

또한 driver-class-name/username/password는 부트가 자동 감지하므로 생략 가능(선택)합니다.

아래 패치를 적용해 보세요.

-    url: jdbc:h2:mem:database
+    url: jdbc:h2:mem:database;MODE=MySQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;DATABASE_TO_UPPER=false

21-24: H2 콘솔 접근 제한 명시화

로컬 프로필이더라도 원격 접근 차단을 명시하는 것을 권장합니다.

적용 예:

   h2:
     console:
       enabled: true
       path: /h2-console
+      settings:
+        web-allow-others: false

7-15: 주석 처리된 MySQL 설정 정리

로컬 프로필에서 H2로 전환이 확정이라면, 주석 블록은 제거하거나 git 히스토리에만 남기는 편이 유지보수에 유리합니다. dev/prod 프로필에 이미 MySQL 설정이 존재하므로 중복 정보는 최소화하세요.

gss-api-app/src/main/java/com/devoops/dto/response/RepositorySaveResponse.java (1)

10-11: 네이밍 불일치: ownerIdrepository.getUserId()

응답 필드는 ownerId지만 도메인 모델은 userId로 노출됩니다. 의미상 동일하다면 OK이나, 용어를 통일하면 가독성과 유지보수성이 좋아집니다.

Also applies to: 21-22

gss-domain/src/main/java/com/devoops/jpa/entity/user/UserEntity.java (1)

46-54: 방어적 코딩 제안: githubToken null 처리

toDomainEntity에서 githubToken이 필수라면 명시적으로 검증해 조기 실패하도록 하면 좋습니다.

 import com.devoops.domain.entity.user.User;
+import java.util.Objects;
@@
-    public User toDomainEntity(GithubToken githubToken) {
+    public User toDomainEntity(GithubToken githubToken) {
+        Objects.requireNonNull(githubToken, "githubToken must not be null");
         return new User(
                 this.id,
                 this.providerId,
                 this.nickname,
                 this.profileImageUrl,
                 githubToken
         );
     }
gss-mcp-app/src/main/java/com/devoops/event/QuestionEventListener.java (1)

54-63: 오타/네이밍 통일: pulLRequestId → pullRequestId

파라미터명에 대문자 L이 끼어 가독성이 떨어집니다. 일관된 카멜케이스로 정리해 주세요.

-            Long pulLRequestId
+            Long pullRequestId
@@
-                        .map(question -> new QuestionCreateCommand(pulLRequestId, taggedQuestion.category(), question))
+                        .map(question -> new QuestionCreateCommand(pullRequestId, taggedQuestion.category(), question))
gss-domain/src/testFixtures/java/com/devoops/generator/AnswerGenerator.java (1)

12-13: 생성자 주입으로 전환 권장(@Autowired 필드 주입 지양)

테스트 픽스처라도 불변성/명시성 측면에서 생성자 주입이 더 안전합니다.

-    @Autowired
-    private AnswerDomainRepository answerDomainRepository;
+    private final AnswerDomainRepository answerDomainRepository;
+
+    public AnswerGenerator(AnswerDomainRepository answerDomainRepository) {
+        this.answerDomainRepository = answerDomainRepository;
+    }
gss-api-app/src/main/java/com/devoops/service/GitHubService.java (1)

26-43: 페이지네이션 보강 제안: 조건 충족 PR이 3개 미만으로 수집될 수 있음

현재는 1페이지(최대 10건)만 조회 후 필터링하여 3개를 제한합니다. 첫 페이지에 조건(작성자=본인, mergedAt != null)을 만족하는 항목이 부족하면 항상 3개를 채우지 못합니다. 추가 페이지를 순회하며 모자란 수량만큼 수집하는 방식으로 보강을 권장합니다.

-    public List<GithubPrResponse> getUserPullRequests(
-            GithubRepoUrl repoUrl,
-            User user
-    ) {
-        GithubToken token = user.getGithubToken();
-        List<GithubPrResponse> closedPullRequests = gitHubClient.getPullRequests(
-                BEARER_PREFIX + token.getToken(),
-                repoUrl.getOwner(),
-                repoUrl.getRepoName(),
-                REQUEST_PULL_REQUEST_STATUS,
-                MAX_PER_PAGE_PULL_REQUEST,
-                1
-        );
-        return closedPullRequests.stream()
-                .filter(pr -> pr.isUserPr(user.getProviderId()) && pr.mergedAt() != null)
-                .limit(MAX_USER_PR_LIMIT)
-                .toList();
-    }
+    public List<GithubPrResponse> getUserPullRequests(GithubRepoUrl repoUrl, User user) {
+        GithubToken token = user.getGithubToken();
+        List<GithubPrResponse> collected = new java.util.ArrayList<>();
+        int page = 1;
+        while (collected.size() < MAX_USER_PR_LIMIT) {
+            List<GithubPrResponse> pageItems = gitHubClient.getPullRequests(
+                    BEARER_PREFIX + token.getToken(),
+                    repoUrl.getOwner(),
+                    repoUrl.getRepoName(),
+                    REQUEST_PULL_REQUEST_STATUS,
+                    MAX_PER_PAGE_PULL_REQUEST,
+                    page
+            );
+            if (pageItems == null || pageItems.isEmpty()) break;
+            pageItems.stream()
+                    .filter(pr -> pr.isUserPr(user.getProviderId()) && pr.mergedAt() != null)
+                    .forEach(collected::add);
+            page++;
+        }
+        return collected.stream().limit(MAX_USER_PR_LIMIT).toList();
+    }

추가로, 본 클래스에서 getUserPullRequests는 User를 받고 getRepositoryInfo는 GithubToken을 받습니다. API 일관성을 위해 동일한 입력 모델로 통일하는 것도 고려해 주세요.

gss-domain/src/testFixtures/java/com/devoops/generator/UserGenerator.java (1)

16-19: 테스트 데이터 ID 생성의 결정론 보장 제안

테스트 픽스처에서 ThreadLocalRandom으로 ID를 생성하면 드물게 중복/플레이키 가능성이 있습니다. 단조 증가 시퀀스로 교체하면 안정적입니다.

다음과 같이 간단히 개선할 수 있습니다:

 public class UserGenerator {
+    private static final java.util.concurrent.atomic.AtomicLong SEQ = new java.util.concurrent.atomic.AtomicLong(1);
@@
     public User generate(String name) {
-        GithubToken token = new GithubToken(name);
-        User user = new User(ThreadLocalRandom.current().nextLong(), name, "profileUrl", token);
+        GithubToken token = new GithubToken(name);
+        User user = new User(SEQ.getAndIncrement(), name, "profileUrl", token);
         return userRepository.saveUser(user);
     }
 }

원하시면 위 변경을 포함한 PR 보조 커밋 패치를 준비하겠습니다.

gss-domain/src/main/java/com/devoops/domain/entity/github/pr/PullRequests.java (1)

7-12: 내부 컬렉션 불변화로 캡슐화 강화 제안

현재 values 리스트가 외부에서 변경될 여지가 있습니다. 방어적 복사/불변화로 도메인 무결성을 높이는 것을 권장합니다.

다음과 같이 롬복 생성자 대신 명시적 생성자로 방어적 복사를 적용할 수 있습니다:

 @Getter
-@RequiredArgsConstructor
 public class PullRequests {
 
     private final List<PullRequest> values;
+
+    public PullRequests(List<PullRequest> values) {
+        this.values = java.util.List.copyOf(values);
+    }
 }

호환성 영향 범위를 함께 점검해 드릴 수 있습니다.

gss-api-app/src/main/java/com/devoops/dto/response/RepositoryPullRequestResponses.java (1)

15-19: from() 가독성/간결성 개선 제안

collectingAndThen 대신 toList()를 사용해 더 간결하게 작성할 수 있습니다(프로젝트 JDK가 지원한다면).

-    public static RepositoryPullRequestResponses from(PullRequests pullRequests) {
-        return pullRequests.getValues().stream()
-                .map(RepositoryPullRequestResponse::new)
-                .collect(Collectors.collectingAndThen(Collectors.toList(), RepositoryPullRequestResponses::new));
-    }
+    public static RepositoryPullRequestResponses from(PullRequests pullRequests) {
+        return new RepositoryPullRequestResponses(
+                pullRequests.getValues().stream()
+                        .map(RepositoryPullRequestResponse::new)
+                        .toList()
+        );
+    }
gss-api-app/src/main/java/com/devoops/controller/pullrequests/PullRequestController.java (1)

49-56: HTTP 상태 코드 정합성: 본문 없는 성공 응답은 204 No Content 권장

상태만 변경하는 PATCH에서 바디 없이 200을 반환하고 있습니다. 204가 의미적으로 더 적합합니다.

-        return ResponseEntity.ok().build();
+        return ResponseEntity.noContent().build();
gss-domain/src/main/java/com/devoops/jpa/repository/github/token/GithubTokenJpaRepository.java (1)

1-3: 패키지 이동에 따른 스캔/제약 검증 권장

  • 리포지토리 패키지가 ...github.token으로 이동했습니다. Spring Data JPA 스캔 대상 포함 여부를 확인해 주세요.
  • findByUserId가 단건 Optional을 반환하므로 DB 레벨에서 user_id 유니크 제약이 있는지 확인 권장합니다. 다중 사용자/레포 시나리오와 무관하게 토큰은 사용자 단위 1:1이 일반적입니다.
gss-domain/src/testFixtures/java/com/devoops/generator/GithubRepoGenerator.java (1)

13-15: 필드 주입 지양, 생성자 주입 권장 (테스트 픽스처도 동일)

불필요한 가변성을 줄이기 위해 생성자 주입으로 변경을 권장합니다.

-    @Autowired
-    private GithubRepoDomainRepository githubRepoDomainRepository;
+    private final GithubRepoDomainRepository githubRepoDomainRepository;
+
+    @Autowired
+    public GithubRepoGenerator(GithubRepoDomainRepository githubRepoDomainRepository) {
+        this.githubRepoDomainRepository = githubRepoDomainRepository;
+    }
gss-api-app/src/main/java/com/devoops/controller/question/QuestionController.java (1)

31-39: 생성 API의 상태 코드/헤더 개선 제안

리소스 생성(POST /api/questions/{questionId}/answer)은 201 Created와 Location 헤더를 반환하는 것이 REST 표준에 가깝습니다.

-        return ResponseEntity.ok(response);
+        // Location: /api/questions/answer/{answerId}
+        URI location = URI.create("/api/questions/answer/" + response.id());
+        return ResponseEntity.created(location).body(response);
gss-api-app/src/test/java/com/devoops/service/facade/QuestionFacadeServiceTest.java (1)

33-77: PR 목표 연계 테스트 보강 제안: 다중 사용자 동일 레포 트래킹 시나리오

이번 PR 목표(복수 사용자의 동일 레포 트래킹)에 맞춰 다음 케이스 추가를 제안합니다:

  • 서로 다른 사용자가 동일 레포의 동일 PR에 대해 답변 생성/삭제 시, PR 상태 전이가 사용자 독립적으로 일관되는지.
  • 사용자 A의 답변 상태 변화가 사용자 B의 접근/상태 조회에 부정적 영향이 없는지.

원하시면 테스트 스켈레톤을 제공하겠습니다.

gss-domain/src/main/java/com/devoops/jpa/repository/user/UserDomainRepositoryImpl.java (1)

26-31: 조회 순서 개선 제안: User 먼저, Token 다음

현재는 토큰을 먼저 조회합니다. 존재하지 않는 사용자 ID로 요청 시 불필요한 토큰 조회가 선행되며, 오류 의미도 모호해질 수 있습니다. User 존재 확인 후, 해당 userId로 토큰을 조회하는 편이 명확합니다.

-        GithubToken githubToken = githubTokenJpaRepository.findByUserId(id)
-                .orElseThrow(() -> new GssException(ErrorCode.GITHUB_TOKEN_NOT_FOUND))
-                .toDomainEntity();
-        return userJpaRepository.findById(id)
-                .orElseThrow(() -> new GssException(ErrorCode.USER_NOT_FOUND))
-                .toDomainEntity(githubToken);
+        UserEntity userEntity = userJpaRepository.findById(id)
+                .orElseThrow(() -> new GssException(ErrorCode.USER_NOT_FOUND));
+        GithubToken githubToken = githubTokenJpaRepository.findByUserId(userEntity.getId())
+                .orElseThrow(() -> new GssException(ErrorCode.GITHUB_TOKEN_NOT_FOUND))
+                .toDomainEntity();
+        return userEntity.toDomainEntity(githubToken);
gss-mcp-app/src/main/java/com/devoops/dto/request/GitHubWebhookEventRequest.java (1)

54-56: NPE 방지 가드 추가 제안(getPullRequestUrl)

다른 getter(getMergedAt, getPullRequestDiffUrl)와 달리 null 가드가 없어 NPE 가능성이 있습니다. 동일 패턴으로 가드 추가를 권장합니다.

-    public String getPullRequestUrl() {
-        return pullRequest.url;
-    }
+    public String getPullRequestUrl() {
+        if (pullRequest == null) {
+            return null;
+        }
+        return pullRequest.url;
+    }
gss-domain/src/main/java/com/devoops/domain/repository/github/question/QuestionDomainRepository.java (1)

18-18: 파라미터 명 명확성 개선 제안

repositoryPrs 대신 pullRequests 등 도메인 용어와 일치하는 이름을 권장합니다. 구현부/호출부 일괄 수정 필요.

-    List<Question> findAllByPullRequests(PullRequests repositoryPrs);
+    List<Question> findAllByPullRequests(PullRequests pullRequests);
gss-domain/src/testFixtures/java/com/devoops/generator/PullRequestGenerator.java (1)

12-16: 테스트 픽스처도 생성자 주입으로 정리 권장(일관성/테스트 용이성)

필드 주입 대신 생성자 주입을 사용하면 불변성/주입 명시성이 좋아집니다. Lombok 사용 시 간단히 전환 가능.

-import org.springframework.beans.factory.annotation.Autowired;
+import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Component;
 
 @Component
+@RequiredArgsConstructor
 public class PullRequestGenerator {
 
-    @Autowired
-    private PullRequestDomainRepository pullRequestDomainRepository;
+    private final PullRequestDomainRepository pullRequestDomainRepository;
gss-domain/src/main/java/com/devoops/jpa/repository/github/webhook/GithubRepoRegistryCountJpaRepository.java (1)

7-14: 새로운 레포지토리 인터페이스 구현이 적절함

웹훅 레지스트리 카운트 관리를 위한 JPA 리포지토리가 올바르게 구현되었습니다. externalId 기반의 CRUD 연산이 적절히 정의되어 있습니다.

다만 deleteByExternalId 메서드 사용 시 트랜잭션 안전성을 고려해야 합니다.

gss-domain/src/main/java/com/devoops/domain/entity/github/webhook/GithubRepoRegistryCount.java (1)

8-16: 도메인 값 객체가 적절하게 구현됨

불변성을 보장하는 값 객체로 잘 설계되었습니다. 다만 필드명과 메서드명의 명확성을 개선할 수 있습니다.

reposCount는 실제로는 특정 레포지토리의 등록 카운트를 의미하므로 registrationCount 또는 trackingCount가 더 명확할 것 같습니다. greaterThan 메서드도 hasMoreRegistrationsThan 등으로 구체화할 수 있습니다.

-    private final long reposCount;
+    private final long trackingCount;

-    public boolean greaterThan(long reposCount) {
-        return this.reposCount > reposCount;
+    public boolean hasMoreTrackingsThan(long count) {
+        return this.trackingCount > count;
gss-mcp-app/src/main/java/com/devoops/controller/webhook/WebhookController.java (1)

24-25: 주석의 URL 업데이트 필요

엔드포인트 경로가 변경되었지만 주석의 webhook URL이 업데이트되지 않았습니다.

-     * webhook url : https://mcp.dev-oops.kr/api/webhooks/github/pull-requests
+     * webhook url : https://mcp.dev-oops.kr/api/webhooks/github/pull-requests/{repositoryId}
gss-api-app/src/main/java/com/devoops/service/facade/RepositoryFacadeService.java (1)

67-67: 다중 사용자 시나리오 고려 사항이 적절히 식별되었습니다.

TODO 주석이 PR의 핵심 목적인 "복수의 사용자가 같은 레포를 트래킹할 때의 시나리오"를 정확히 반영하고 있습니다. 이는 중요한 비즈니스 로직 개선 포인트입니다.

다른 사용자가 등록한 웹훅을 고려한 로직 개선 방안을 제안해드릴 수 있습니다. 새로운 이슈로 추적하시겠습니까?

gss-mcp-app/src/main/java/com/devoops/service/webhook/WebhookFacadeService.java (1)

13-13: 사용하지 않는 import 제거 필요

java.util.Optional import가 코드에서 사용되지 않습니다.

-import java.util.Optional;
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 8bb5bd3 and dde44db.

📒 Files selected for processing (106)
  • gss-api-app/src/main/java/com/devoops/controller/GithubController.java (0 hunks)
  • gss-api-app/src/main/java/com/devoops/controller/docs/RepositoryControllerSwagger.java (0 hunks)
  • gss-api-app/src/main/java/com/devoops/controller/pullrequests/PullRequestController.java (1 hunks)
  • gss-api-app/src/main/java/com/devoops/controller/question/QuestionController.java (1 hunks)
  • gss-api-app/src/main/java/com/devoops/controller/repository/RepositoryController.java (1 hunks)
  • gss-api-app/src/main/java/com/devoops/dto/response/AnswerPutResponse.java (1 hunks)
  • gss-api-app/src/main/java/com/devoops/dto/response/AnswerPutResponses.java (1 hunks)
  • gss-api-app/src/main/java/com/devoops/dto/response/AnswerSaveResponse.java (1 hunks)
  • gss-api-app/src/main/java/com/devoops/dto/response/AnswerUpdateResponse.java (1 hunks)
  • gss-api-app/src/main/java/com/devoops/dto/response/MyRepositoriesResponse.java (1 hunks)
  • gss-api-app/src/main/java/com/devoops/dto/response/PullRequestDetailReadResponse.java (1 hunks)
  • gss-api-app/src/main/java/com/devoops/dto/response/PullRequestRankingResponse.java (1 hunks)
  • gss-api-app/src/main/java/com/devoops/dto/response/PullRequestRankingResponses.java (1 hunks)
  • gss-api-app/src/main/java/com/devoops/dto/response/PullRequestReadResponse.java (1 hunks)
  • gss-api-app/src/main/java/com/devoops/dto/response/RepositoryPullRequestResponse.java (1 hunks)
  • gss-api-app/src/main/java/com/devoops/dto/response/RepositoryPullRequestResponses.java (1 hunks)
  • gss-api-app/src/main/java/com/devoops/dto/response/RepositorySaveResponse.java (1 hunks)
  • gss-api-app/src/main/java/com/devoops/service/GitHubService.java (1 hunks)
  • gss-api-app/src/main/java/com/devoops/service/facade/AuthFacadeService.java (1 hunks)
  • gss-api-app/src/main/java/com/devoops/service/facade/PullRequestFacadeService.java (1 hunks)
  • gss-api-app/src/main/java/com/devoops/service/facade/QuestionFacadeService.java (1 hunks)
  • gss-api-app/src/main/java/com/devoops/service/facade/RepositoryFacadeService.java (3 hunks)
  • gss-api-app/src/main/java/com/devoops/service/github/WebHookService.java (1 hunks)
  • gss-api-app/src/test/java/com/devoops/controller/question/QuestionControllerTest.java (1 hunks)
  • gss-api-app/src/test/java/com/devoops/controller/repository/RepositoryControllerTest.java (1 hunks)
  • gss-api-app/src/test/java/com/devoops/service/GitHubServiceTest.java (0 hunks)
  • gss-api-app/src/test/java/com/devoops/service/answerranking/AnswerRankingServiceTest.java (1 hunks)
  • gss-api-app/src/test/java/com/devoops/service/facade/PullRequestFacadeServiceTest.java (1 hunks)
  • gss-api-app/src/test/java/com/devoops/service/facade/QuestionFacadeServiceTest.java (1 hunks)
  • gss-api-app/src/test/java/com/devoops/service/facade/RepositoryFacadeServiceTest.java (2 hunks)
  • gss-api-app/src/test/java/com/devoops/service/github/WebHookServiceTest.java (1 hunks)
  • gss-api-app/src/test/java/com/devoops/service/pullrequests/PullRequestServiceTest.java (1 hunks)
  • gss-api-app/src/test/java/com/devoops/service/question/QuestionServiceTest.java (1 hunks)
  • gss-api-app/src/test/java/com/devoops/service/repository/RepositoryServiceTest.java (1 hunks)
  • gss-api-app/src/test/java/com/devoops/service/user/UserServiceTest.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/command/request/PullRequestCreateCommand.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/command/request/QuestionCreateCommand.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/command/request/RepositoryCreateCommand.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/domain/entity/github/answer/Answer.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/domain/entity/github/answer/AnswerRanking.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/domain/entity/github/answer/AnswerRankings.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/domain/entity/github/answer/Answers.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/domain/entity/github/pr/PullRequest.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/domain/entity/github/pr/PullRequests.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/domain/entity/github/pr/RecordStatus.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/domain/entity/github/question/Question.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/domain/entity/github/repo/GithubRepository.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/domain/entity/github/token/GithubToken.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/domain/entity/github/webhook/GithubRepoRegistryCount.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/domain/entity/github/webhook/GithubWebhook.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/domain/entity/user/User.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/domain/repository/github/GithubTokenDomainRepository.java (0 hunks)
  • gss-domain/src/main/java/com/devoops/domain/repository/github/answer/AnswerDomainRepository.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/domain/repository/github/answer/AnswerRankingDomainRepository.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/domain/repository/github/pr/PullRequestDomainRepository.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/domain/repository/github/question/QuestionDomainRepository.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/domain/repository/github/repo/GithubRepoDomainRepository.java (2 hunks)
  • gss-domain/src/main/java/com/devoops/domain/repository/github/token/GithubTokenDomainRepository.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/domain/repository/github/webhook/GithubRepoRegistryCountRepository.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/domain/repository/github/webhook/GithubWebhookDomainRepository.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/jpa/entity/github/answer/AnswerEntity.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/jpa/entity/github/answer/AnswerRankingEntity.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/jpa/entity/github/pr/PullRequestEntity.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/jpa/entity/github/question/QuestionEntity.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/jpa/entity/github/repo/GithubRepositoryEntity.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/jpa/entity/github/token/GithubTokenEntity.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/jpa/entity/github/webhook/GithubRepoRegistryCountEntity.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/jpa/entity/github/webhook/GithubWebhookEntity.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/jpa/entity/user/UserEntity.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/jpa/repository/github/answer/AnswerDomainRepositoryImpl.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/jpa/repository/github/answer/AnswerJpaRepository.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/jpa/repository/github/answer/AnswerRankingDomainRepositoryImpl.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/jpa/repository/github/answer/AnswerRankingJpaRepository.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/jpa/repository/github/pr/PullRequestDomainRepositoryImpl.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/jpa/repository/github/pr/PullRequestJpaRepository.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/jpa/repository/github/question/QuestionDomainRepositoryImpl.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/jpa/repository/github/question/QuestionJpaRepository.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/jpa/repository/github/repo/GithubRepoDomainRepositoryImpl.java (3 hunks)
  • gss-domain/src/main/java/com/devoops/jpa/repository/github/repo/GithubRepoJpaRepository.java (2 hunks)
  • gss-domain/src/main/java/com/devoops/jpa/repository/github/token/GithubTokenDomainRepositoryImpl.java (2 hunks)
  • gss-domain/src/main/java/com/devoops/jpa/repository/github/token/GithubTokenJpaRepository.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/jpa/repository/github/webhook/GithubRepoRegistryCountJpaRepository.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/jpa/repository/github/webhook/GithubRepoRegistryCountRepositoryImpl.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/jpa/repository/github/webhook/GithubWebHookDomainRepositoryImpl.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/jpa/repository/github/webhook/GithubWebHookJpaRepository.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/jpa/repository/user/UserDomainRepositoryImpl.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/service/answer/AnswerService.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/service/answerranking/AnswerRankingService.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/service/pullrequest/PullRequestService.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/service/question/QuestionService.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/service/repository/RepositoryService.java (2 hunks)
  • gss-domain/src/main/resources/application-db.yml (1 hunks)
  • gss-domain/src/testFixtures/java/com/devoops/BaseRepositoryTest.java (1 hunks)
  • gss-domain/src/testFixtures/java/com/devoops/generator/AnswerGenerator.java (1 hunks)
  • gss-domain/src/testFixtures/java/com/devoops/generator/AnswerRankingGenerator.java (1 hunks)
  • gss-domain/src/testFixtures/java/com/devoops/generator/GithubRepoGenerator.java (1 hunks)
  • gss-domain/src/testFixtures/java/com/devoops/generator/PullRequestGenerator.java (1 hunks)
  • gss-domain/src/testFixtures/java/com/devoops/generator/QuestionGenerator.java (1 hunks)
  • gss-domain/src/testFixtures/java/com/devoops/generator/UserGenerator.java (1 hunks)
  • gss-domain/src/testFixtures/java/com/devoops/generator/WebhookGenerator.java (2 hunks)
  • gss-mcp-app/src/main/java/com/devoops/controller/webhook/WebhookController.java (2 hunks)
  • gss-mcp-app/src/main/java/com/devoops/dto/request/GitHubWebhookEventRequest.java (1 hunks)
  • gss-mcp-app/src/main/java/com/devoops/event/QuestionCreateEvent.java (1 hunks)
  • gss-mcp-app/src/main/java/com/devoops/event/QuestionEventListener.java (1 hunks)
  • gss-mcp-app/src/main/java/com/devoops/service/webhook/WebhookFacadeService.java (2 hunks)
  • gss-mcp-app/src/test/java/com/devoops/service/webhook/WebhookFacadeServiceTest.java (1 hunks)
💤 Files with no reviewable changes (4)
  • gss-domain/src/main/java/com/devoops/domain/repository/github/GithubTokenDomainRepository.java
  • gss-api-app/src/main/java/com/devoops/controller/docs/RepositoryControllerSwagger.java
  • gss-api-app/src/main/java/com/devoops/controller/GithubController.java
  • gss-api-app/src/test/java/com/devoops/service/GitHubServiceTest.java
🧰 Additional context used
🧬 Code Graph Analysis (1)
gss-domain/src/main/java/com/devoops/jpa/entity/github/webhook/GithubRepoRegistryCountEntity.java (1)
gss-domain/src/main/java/com/devoops/domain/entity/github/webhook/GithubRepoRegistryCount.java (1)
  • Getter (6-16)
🔇 Additional comments (112)
gss-domain/src/main/resources/application-db.yml (1)

25-32: DB 초기화 설정 충돌 없음 확인

리포지토리 내에

  • data.sql / schema.sql
  • Flyway 마이그레이션 디렉터리 또는 org.flywaydb 의존성
  • @ActiveProfiles("local") 통합 테스트

가 모두 존재하지 않아,
현재 설정(ddl-auto: create-drop, defer-datasource-initialization: false)은 충돌 없이 정상 동작합니다.

gss-domain/src/main/java/com/devoops/domain/entity/github/repo/GithubRepository.java (2)

1-1: 패키지 리팩터링 LGTM

의미 있는 서브패키지 분리로 응집도가 좋아졌습니다.


17-25: 잘못된 리뷰 코멘트: Lombok 게터 충돌 이슈 없음
rg 검색 결과, 호출부에서는 오직 isTracking()만 사용되고(getIsTracking()·isIsTracking() 호출 전무), 실제로 GithubRepository에 해당 메서드가 제공되고 있어 게터명 충돌 문제가 발생하지 않습니다.
따라서 본 리뷰는 해당 코드를 잘못 이해한 것이므로 무시하시면 됩니다.

Likely an incorrect or invalid review comment.

gss-domain/src/main/java/com/devoops/domain/entity/github/question/Question.java (1)

1-1: 패키지 리팩터링 LGTM

도메인 계층 구조 정리가 일관적입니다.

gss-domain/src/main/java/com/devoops/domain/entity/github/pr/PullRequest.java (1)

1-1: 패키지 리팩터링 LGTM

pr 서브패키지 이동이 일관적이며, 공개 API 변화가 없어 안전합니다.

gss-domain/src/main/java/com/devoops/domain/entity/github/token/GithubToken.java (1)

1-1: 패키지 리팩터링 LGTM

의존 모듈들의 임포트 경로와 일관적으로 정리되었습니다.

gss-domain/src/main/java/com/devoops/jpa/entity/user/UserEntity.java (1)

3-3: 임포트 경로 업데이트 LGTM

GithubToken 토큰 서브패키지로의 이동 반영이 정확합니다.

gss-mcp-app/src/main/java/com/devoops/event/QuestionCreateEvent.java (1)

3-4: 임포트 경로 업데이트 LGTM

이벤트 시그니처 안정적으로 유지되었습니다.

gss-domain/src/main/java/com/devoops/domain/entity/github/answer/Answer.java (1)

1-1: 패키지 리팩터링 LGTM

answer 서브패키지로의 이동이 다른 엔티티와 일관됩니다.

gss-domain/src/main/java/com/devoops/domain/entity/github/answer/AnswerRankings.java (1)

1-1: 패키지 이동 LGTM

answer 관련 도메인을 github.answer로 세분화한 방향과 일치합니다. 코드 로직 변화는 없고, 영향 범위는 import 경로 업데이트로 한정됩니다.

gss-domain/src/main/java/com/devoops/jpa/entity/github/pr/PullRequestEntity.java (1)

1-4: 검증 완료: 도메인 PullRequest 생성자/필드 시그니처 변경 없음

PullRequest 클래스는 @RequiredArgsConstructor 기반 생성자와 필드( id, repositoryId, userId, title, description, summary, summaryDetail, pullRequestUrl, externalId, recordStatus, mergedAt, tag )가 이전과 동일하게 유지되고 있음을 확인했습니다. 엔티티↔도메인 매핑(from/to) 시그니처에도 변화가 없어 기능상 영향이 없습니다.
LGTM.

gss-domain/src/main/java/com/devoops/domain/repository/github/answer/AnswerDomainRepository.java (1)

1-4: 패키지/임포트 경로 변경 LGTM

answer 전용 패키지로의 이동이 반영되었고, 레포지토리 계약은 변경 없습니다.

gss-domain/src/main/java/com/devoops/service/answer/AnswerService.java (1)

3-4: 임포트 경로 정리 LGTM

도메인/레포지토리의 answer 패키지 분리에 맞춘 조정입니다. 서비스 로직 영향은 없습니다.

gss-domain/src/testFixtures/java/com/devoops/generator/AnswerGenerator.java (1)

3-5: 임포트 경로 변경 LGTM

패키지 리팩터링 방향과 일치합니다. 테스트 픽스처 로직 영향은 없습니다.

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

4-4: 임포트 경로 변경 LGTM

token 하위 패키지 이동에 맞춘 정리로 보입니다. 동작 변화는 없습니다.

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

3-3: 임포트 경로 변경 LGTM

token 하위 패키지로의 이동 반영으로 보이며, 빌더/생성 로직 변화는 없습니다.

gss-api-app/src/main/java/com/devoops/dto/response/AnswerSaveResponse.java (1)

3-3: 패키지 경로 변경 정상 적용

Answer의 하위 패키지 분리에 맞춘 import 갱신으로 보이며, 나머지 로직 영향은 없습니다. LGTM.

gss-api-app/src/main/java/com/devoops/dto/response/MyRepositoriesResponse.java (1)

3-3: 도메인 리패키징 반영 OK

GithubRepository import 경로 변경만 있으며, 매핑 로직과 공개 API에는 영향이 없습니다. LGTM.

gss-domain/src/testFixtures/java/com/devoops/generator/UserGenerator.java (1)

3-3: 토큰 엔티티 리패키징 반영 OK

GithubTokengithub.token 하위 패키지 이동 반영입니다. 문제 없습니다.

gss-domain/src/main/java/com/devoops/domain/entity/github/pr/PullRequests.java (1)

1-1: 패키지 이동 일관성 확인

PullRequestsgithub.pr로 이동한 변경과 다른 파일들의 import 갱신이 일치합니다. 정상입니다.

gss-domain/src/main/java/com/devoops/command/request/PullRequestCreateCommand.java (1)

3-4: PR 도메인 타입 리패키징 반영 OK

PullRequest, RecordStatus import 경로 변경만 있으며, 공개 API에는 영향이 없습니다.

gss-api-app/src/test/java/com/devoops/controller/repository/RepositoryControllerTest.java (1)

4-6: 테스트 import 경로 갱신 확인

GithubRepository, PullRequest, RecordStatus의 새 패키지 경로로 정상 업데이트되었습니다. 테스트 시나리오에는 영향 없습니다.

gss-api-app/src/main/java/com/devoops/dto/response/RepositoryPullRequestResponses.java (1)

3-3: PR 컬렉션 타입 리패키징 반영 OK

PullRequests import 경로만 변경되었습니다. 동작 동일. LGTM.

gss-api-app/src/main/java/com/devoops/dto/response/PullRequestRankingResponse.java (1)

3-3: 도메인 AnswerRanking 리패키징 반영 OK

import 경로 갱신만 있으며, 생성자 위임 로직은 동일합니다. LGTM.

gss-api-app/src/main/java/com/devoops/dto/response/AnswerPutResponses.java (1)

3-3: 패키지 구조 개선이 올바르게 적용되었습니다.

Answers 클래스의 import 경로가 새로운 계층적 패키지 구조에 맞게 업데이트되었습니다.

gss-domain/src/main/java/com/devoops/domain/entity/github/answer/AnswerRanking.java (1)

1-1: 도메인 패키지 구조화가 적절합니다.

AnswerRanking 클래스가 답변 관련 엔티티들을 그룹화하는 answer 서브패키지로 이동되었습니다.

gss-domain/src/main/java/com/devoops/domain/entity/github/webhook/GithubWebhook.java (1)

1-1: 웹훅 관련 도메인 엔티티의 패키지 분리가 적절합니다.

웹훅 관련 엔티티들을 별도의 서브패키지로 그룹화하여 도메인 구조를 개선했습니다.

gss-api-app/src/test/java/com/devoops/service/user/UserServiceTest.java (1)

6-6: 테스트의 import 경로가 올바르게 업데이트되었습니다.

GithubToken의 새로운 패키지 구조에 맞게 import 경로가 수정되었습니다.

gss-domain/src/main/java/com/devoops/domain/entity/github/answer/Answers.java (1)

1-1: 답변 관련 엔티티의 패키지 이동이 적절합니다.

Answers 클래스가 답변 관련 도메인 엔티티들과 함께 answer 서브패키지로 올바르게 이동되었습니다.

gss-api-app/src/main/java/com/devoops/controller/repository/RepositoryController.java (1)

5-6: 컨트롤러의 import 경로가 새로운 패키지 구조에 맞게 업데이트되었습니다.

GithubRepositoryPullRequests가 각각 repopr 서브패키지로 이동된 것에 맞춰 import가 수정되었습니다.

gss-api-app/src/main/java/com/devoops/dto/response/AnswerUpdateResponse.java (1)

3-3: DTO의 import 경로가 올바르게 업데이트되었습니다.

Answer 엔티티의 새로운 패키지 경로에 맞춰 import가 수정되었습니다.

gss-domain/src/main/java/com/devoops/command/request/QuestionCreateCommand.java (1)

3-3: 커맨드 객체의 import 경로가 적절하게 업데이트되었습니다.

Question 엔티티의 새로운 패키지 구조에 맞게 import가 수정되었고, 도메인 변환 로직은 그대로 유지됩니다.

gss-api-app/src/main/java/com/devoops/dto/response/RepositoryPullRequestResponse.java (1)

3-4: 패키지 경로 리팩터 반영 OK

pr 서브패키지로의 이동이 일관되게 반영되었습니다.

gss-api-app/src/main/java/com/devoops/controller/pullrequests/PullRequestController.java (1)

5-5: 패키지 경로 리팩터 반영 OK

RecordStatus의 pr 패키지 이동 반영이 적절합니다.

gss-domain/src/main/java/com/devoops/jpa/repository/github/answer/AnswerJpaRepository.java (1)

1-3: 문제 없음: Spring Data JPA 패키지 스캔 및 엔티티 이름 기본값 정상

Spring Boot auto-configuration이 com.devoops 하위 전체 패키지를 자동 스캔하며,
AnswerEntity·QuestionEntity 모두 @Entity(name) 미지정 상태로 기본 simple class name을 사용하므로 JPQL 수정 필요가 없습니다.

gss-domain/src/main/java/com/devoops/domain/entity/github/pr/RecordStatus.java (1)

1-1: 패키지 이동 OK

열거형 값 변화 없이 경로만 변경되었습니다. JPA가 EnumType.STRING으로 매핑 중이라면 DB/JSON 표현은 영향 없습니다.

gss-domain/src/testFixtures/java/com/devoops/generator/GithubRepoGenerator.java (1)

3-6: 패키지 경로 리팩터 반영 OK

GithubRepository/GithubRepoDomainRepository의 repo 서브패키지 이동 반영이 적절합니다.

gss-api-app/src/main/java/com/devoops/controller/question/QuestionController.java (1)

5-6: 패키지 경로 리팩터 반영 OK

Answer/Answers의 github.answer 경로 반영이 적절합니다.

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

7-14: 패키지 경로 리팩터 반영 OK

테스트 import 경로 정리가 잘 반영되었습니다.

gss-domain/src/main/java/com/devoops/jpa/repository/user/UserDomainRepositoryImpl.java (1)

3-3: 임포트 경로 리팩토링 정합성 확인

token 서브패키지로의 이동이 일관되게 반영되었습니다. 추가 로직 변경은 없어 보이며 컴파일 타임 영향 최소화로 보입니다.

Also applies to: 9-9, 11-11

gss-domain/src/main/java/com/devoops/jpa/entity/github/token/GithubTokenEntity.java (1)

1-1: 임포트/패키지 이동 OK

도메인 토큰 타입과 경로가 일관되게 정리되었습니다.

Also applies to: 3-3

gss-api-app/src/main/java/com/devoops/dto/response/AnswerPutResponse.java (1)

3-3: 임포트 경로 리팩토링 반영 OK

레코드/생성자 로직 변경 없이 경로만 정리되어 영향도 낮습니다.

gss-mcp-app/src/main/java/com/devoops/dto/request/GitHubWebhookEventRequest.java (2)

92-102: Primitive 반환으로 NPE 리스크 제거 👍

boolean으로 전환 + null 가드가 있어 안전합니다. 동작 의미도 명확해졌습니다.


79-83: List.getFirst() 사용 호환성 문제 없음
프로젝트의 Gradle 설정에서 toolchain 및 source/targetCompatibility가 모두 Java 23으로 지정되어 있어 Java 21 이상에서 제공되는 List.getFirst() 메서드를 안전하게 사용할 수 있습니다.

  • build.gradle:9-10 – languageVersion = JavaLanguageVersion.of(23)
  • build.gradle:53-54 – sourceCompatibility = JavaVersion.VERSION_23, targetCompatibility = JavaVersion.VERSION_23
gss-domain/src/testFixtures/java/com/devoops/BaseRepositoryTest.java (1)

3-4: 임포트 경로 이동 반영 OK

테스트 픽스처의 @import 대상 클래스는 동일하며 경로만 변경되어 영향도 낮습니다.

gss-domain/src/main/java/com/devoops/domain/repository/github/question/QuestionDomainRepository.java (2)

1-5: 패키지 구조 분리 반영 OK

pr/question 서브도메인으로의 분리가 인터페이스에도 일관되게 반영되었습니다.


5-5: QuestionAnswer 클래스 패키지 위치 확인 요청

Question/Answers가 question/pr 서브패키지로 이동된 반면, QuestionAnswer.java는 여전히
com.devoops.domain.entity.github 패키지에 남아 있습니다.
의도된 구조인지 확인 부탁드립니다.

  • 파일: gss-domain/src/main/java/com/devoops/domain/entity/github/QuestionAnswer.java
gss-api-app/src/main/java/com/devoops/service/facade/QuestionFacadeService.java (1)

4-8: 임포트 경로 리팩토링 반영 OK

도메인 타입 경로만 변경되었고 로직 영향은 없습니다.

gss-domain/src/testFixtures/java/com/devoops/generator/PullRequestGenerator.java (1)

3-7: 임포트 경로 리팩토링 반영 OK

테스트 픽스처의 도메인/레포 경로가 일관되게 조정되었습니다.

gss-domain/src/main/java/com/devoops/service/question/QuestionService.java (1)

4-10: 패키지 구조 변경이 올바르게 적용되었습니다.

GitHub 도메인 엔티티들이 각각의 하위 패키지(answer, question)로 적절히 분리되었고, 관련 레포지토리 인터페이스도 일관성 있게 업데이트되었습니다.

gss-domain/src/main/java/com/devoops/service/pullrequest/PullRequestService.java (2)

4-7: 패키지 구조 변경이 일관성 있게 적용되었습니다.

PR 관련 도메인 엔티티들이 pr 하위 패키지로 적절히 분리되었습니다.


13-37: PullRequestService.findByRepositoryId 제거 영향 검증
현재 PullRequestService에서 findByRepositoryId(long repositoryId) 메소드를 삭제하셨습니다. 도메인 레포지토리(PullRequestDomainRepositoryPullRequestJpaRepository)에는 여전히 해당 메소드가 남아 있으므로, 서비스 레이어를 통해 호출하는 부분(컨트롤러, GraphQL 리졸버, 스케줄러 등)이 없는지 추가 검증이 필요합니다.

아래 스크립트로 호출 여부를 확인해 보세요:

rg -n "findByRepositoryId" --glob "gss-api-app/src/**/*.java"

• 호출 결과에 PullRequestService.findByRepositoryId(...) 가 없다면, 삭제를 그대로 진행해도 무방합니다.
• 호출이 발견된다면, 해당 위치에서 대체 로직을 적용하거나 메소드를 복원해야 합니다.

gss-api-app/src/main/java/com/devoops/dto/response/PullRequestRankingResponses.java (1)

3-3: 패키지 구조 변경이 적절히 반영되었습니다.

AnswerRankingsanswer 하위 패키지로 이동한 것이 올바르게 반영되었고, 미사용 import도 정리되어 코드가 더 깔끔해졌습니다.

gss-domain/src/main/java/com/devoops/domain/repository/github/webhook/GithubWebhookDomainRepository.java (1)

1-12: 웹훅 관련 코드의 패키지 분리가 적절합니다.

웹훅 관련 도메인 레포지토리가 webhook 하위 패키지로 이동하여 관심사 분리가 잘 이루어졌습니다.

gss-domain/src/main/java/com/devoops/jpa/entity/github/webhook/GithubWebhookEntity.java (1)

1-38: JPA 엔티티의 패키지 구조가 도메인과 일관성 있게 변경되었습니다.

JPA 엔티티와 도메인 엔티티 모두 webhook 하위 패키지로 이동하여 레이어 간 일관성이 유지되었습니다.

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

3-6: 퍼사드 서비스의 도메인 엔티티 import가 올바르게 업데이트되었습니다.

새로운 패키지 구조에 맞게 answerpr 하위 패키지에서 관련 엔티티들을 import하도록 일관성 있게 변경되었습니다.

gss-domain/src/main/java/com/devoops/domain/repository/github/token/GithubTokenDomainRepository.java (2)

1-11: 토큰 관리 기능의 분리가 적절합니다.

GitHub 토큰 관리 기능이 별도 패키지로 분리되어 관심사 분리가 잘 이루어졌습니다. 인터페이스 설계도 깔끔합니다.


6-11: GithubTokenDomainRepository의 WebHookService 연관성 확인

WebHookService에서 사용자별 토큰을 아래 위치에서 조회하여 Webhook 생성/삭제 시 사용하고 있습니다.

  • gss-api-app/src/main/java/com/devoops/service/github/WebHookService.java
    • 46행: githubTokenDomainRepository.getByUserId(user.getId())createWebhook(githubToken, …)
    • 71행: githubTokenDomainRepository.getByUserId(user.getId())tryDeleteWebhook(githubToken, …)

위와 같이 각 사용자별 토큰을 기반으로 Webhook을 관리하고 있음을 확인했습니다.
“복수의 사용자가 같은 레포를 트래킹할 때”에도 사용자별 토큰 관리가 반드시 필요한지 비즈니스 요구사항을 다시 검토해주세요.

gss-mcp-app/src/test/java/com/devoops/service/webhook/WebhookFacadeServiceTest.java (1)

7-12: 테스트 코드의 import가 새로운 패키지 구조에 맞게 업데이트되었습니다.

웹훅 관련 테스트에서 사용하는 도메인 엔티티와 레포지토리들의 import 경로가 새로운 하위 패키지 구조에 일관성 있게 반영되었습니다.

gss-domain/src/main/java/com/devoops/jpa/entity/github/repo/GithubRepositoryEntity.java (1)

1-3: 패키지 재구성이 올바르게 적용됨

GitHub 도메인 엔티티를 서브패키지로 분리하는 리팩터링이 정확하게 수행되었습니다. 패키지 선언과 import 문이 일관되게 업데이트되었습니다.

gss-api-app/src/test/java/com/devoops/service/pullrequests/PullRequestServiceTest.java (1)

6-8: 테스트 파일의 import 문이 올바르게 업데이트됨

도메인 패키지 재구성에 맞춰 import 문들이 정확하게 수정되었습니다. 테스트 로직에는 변경이 없어 기존 기능이 유지됩니다.

gss-domain/src/main/java/com/devoops/jpa/repository/github/answer/AnswerRankingJpaRepository.java (1)

1-3: 패키지 이동이 올바르게 수행됨

AnswerRankingJpaRepository의 패키지가 github.answer 서브패키지로 적절히 이동되었고, 관련 import도 정확하게 업데이트되었습니다.

gss-domain/src/main/java/com/devoops/jpa/repository/github/question/QuestionJpaRepository.java (1)

1-4: 질문 관련 리포지토리의 패키지 이동 완료

QuestionJpaRepositorygithub.question 서브패키지로 올바르게 이동되었고, QuestionEntity import도 적절히 업데이트되었습니다. QuestionAnswer는 공통 타입으로 기존 패키지에 유지되는 것으로 보입니다.

gss-api-app/src/test/java/com/devoops/controller/question/QuestionControllerTest.java (1)

7-11: 컨트롤러 테스트의 import 문 업데이트 완료

도메인 패키지 재구성에 맞춰 모든 import 문이 올바르게 업데이트되었습니다:

  • Answer/Answers → github.answer
  • GithubRepository → github.repo
  • PullRequest/RecordStatus → github.pr
  • Question → github.question

테스트 로직은 변경되지 않아 기존 기능이 그대로 유지됩니다.

gss-domain/src/main/java/com/devoops/domain/repository/github/pr/PullRequestDomainRepository.java (1)

1-5: 패키지 리팩터링이 적절히 수행되었습니다.

PR 관련 도메인 엔티티들을 pr 서브패키지로 이동한 것이 일관성 있게 적용되어 있습니다. 임포트 경로도 올바르게 업데이트되었습니다.

gss-domain/src/main/java/com/devoops/jpa/repository/github/webhook/GithubWebHookJpaRepository.java (1)

1-3: 웹훅 관련 컴포넌트의 패키지 분리가 잘 수행되었습니다.

웹훅 관련 JPA 리포지토리를 별도 패키지로 분리하여 도메인 구조를 명확하게 만들었습니다.

gss-api-app/src/test/java/com/devoops/service/facade/RepositoryFacadeServiceTest.java (1)

13-15: 임포트 경로 업데이트가 올바르게 적용되었습니다.

repo 서브패키지로 이동한 도메인 엔티티들의 임포트가 정확하게 업데이트되었습니다.

gss-domain/src/main/java/com/devoops/jpa/repository/github/pr/PullRequestJpaRepository.java (1)

1-3: JPA 리포지토리의 패키지 이동이 적절합니다.

Pull Request 관련 JPA 리포지토리를 pr 서브패키지로 이동하여 도메인별 구조화가 잘 되어 있습니다.

gss-api-app/src/main/java/com/devoops/dto/response/PullRequestReadResponse.java (1)

3-5: DTO의 임포트 업데이트가 올바르게 수행되었습니다.

PR 관련 도메인 타입들이 새로운 패키지 구조에 맞게 임포트되었습니다.

gss-api-app/src/main/java/com/devoops/dto/response/PullRequestDetailReadResponse.java (1)

3-5: 상세 응답 DTO의 임포트 경로가 정확하게 업데이트되었습니다.

패키지 리팩터링에 따른 임포트 변경이 일관성 있게 적용되었습니다.

gss-domain/src/main/java/com/devoops/jpa/repository/github/repo/GithubRepoJpaRepository.java (2)

1-3: 저장소 관련 컴포넌트의 패키지 분리가 적절합니다.

GitHub 저장소 관련 JPA 리포지토리를 repo 서브패키지로 이동하여 구조화가 잘 되어 있습니다.


16-20: API 변경 반영 확인 완료
모든 호출부가 새로운 시그니처(existsByGithubRepositoryIdAndUserId, findByGithubRepositoryIdAndUserId)로 업데이트되었으며, 단일 파라미터 메서드(findByGithubRepositoryId(long externalId))는 사용처가 없습니다. 추가 조치 불필요합니다.

gss-api-app/src/main/java/com/devoops/service/facade/RepositoryFacadeService.java (2)

6-7: 서비스 책임 분리와 패키지 구조 개선이 잘 수행되었습니다.

  • 도메인 엔티티 임포트가 새로운 서브패키지 구조에 맞게 업데이트되었습니다
  • WebHookService를 별도로 분리하여 웹훅 관리 책임을 명확하게 했습니다

Also applies to: 14-14, 28-28


35-35: 웹훅 서비스 분리가 적절하게 적용되었습니다.

웹훅 등록과 삭제 로직이 전용 WebHookService로 이동되어 단일 책임 원칙을 잘 준수하고 있습니다.

Also applies to: 69-69

gss-domain/src/main/java/com/devoops/jpa/entity/github/question/QuestionEntity.java (1)

1-3: 패키지 구조 개선이 적절합니다.

도메인 엔티티를 세분화된 패키지(github.question)로 이동하는 것은 코드 구조를 개선하고 관련성 있는 코드들을 그룹화하는 좋은 접근입니다.

gss-domain/src/main/java/com/devoops/command/request/RepositoryCreateCommand.java (1)

3-3: 패키지 구조 개선에 따른 적절한 import 경로 업데이트입니다.

GithubRepository가 github.repo 서브패키지로 이동한 것에 맞춰 import 경로가 올바르게 업데이트되었습니다.

gss-domain/src/testFixtures/java/com/devoops/generator/AnswerRankingGenerator.java (1)

3-8: 패키지 구조 개선에 따른 포괄적인 import 업데이트가 적절합니다.

모든 도메인 엔티티와 JPA 관련 클래스들이 새로운 서브패키지 구조(github.answer, github.pr, github.question)에 맞춰 일관성 있게 업데이트되었습니다.

gss-domain/src/main/java/com/devoops/jpa/repository/github/webhook/GithubWebHookDomainRepositoryImpl.java (1)

1-7: 웹훅 관련 컴포넌트의 패키지 분리가 적절합니다.

웹훅 관련 도메인 엔티티, 레포지토리, 구현체들을 별도의 webhook 서브패키지로 분리하여 관련 기능들을 명확히 그룹화했습니다.

gss-domain/src/main/java/com/devoops/jpa/entity/github/answer/AnswerEntity.java (1)

1-3: 답변 관련 엔티티의 패키지 분리가 적절합니다.

Answer 엔티티를 github.answer 서브패키지로 이동하여 답변 관련 도메인 요소들을 명확히 그룹화했습니다.

gss-domain/src/main/java/com/devoops/service/repository/RepositoryService.java (3)

4-8: 패키지 구조 개선에 따른 적절한 import 업데이트입니다.

도메인 엔티티와 레포지토리들이 새로운 서브패키지 구조(github.repo, github.pr)로 이동한 것에 맞춰 import가 올바르게 업데이트되었습니다.


24-24: 복수 사용자의 동일 레포 트래킹을 지원하는 중요한 비즈니스 로직 변경입니다.

existsByExternalId에서 existsByExternalIdAndUserId로 변경하여 동일한 외부 레포지토리를 여러 사용자가 독립적으로 트래킹할 수 있도록 지원합니다. 이는 PR 목표와 일치하는 핵심적인 개선사항입니다.


17-17: Lombok을 활용한 생성자 주입 개선이 좋습니다.

@RequiredArgsConstructor를 추가하여 final 필드들에 대한 생성자 기반 의존성 주입을 깔끔하게 처리했습니다.

gss-domain/src/main/java/com/devoops/jpa/entity/github/answer/AnswerRankingEntity.java (1)

1-4: 답변 랭킹 엔티티의 패키지 분리가 적절합니다.

AnswerRanking과 관련 엔티티들을 github.answer 서브패키지로 이동하고, QuestionEntity와의 관계를 위해 github.question 패키지를 import하여 도메인 간 관계를 명확히 했습니다.

gss-api-app/src/test/java/com/devoops/service/github/WebHookServiceTest.java (3)

27-40: WebHookService 테스트 구조가 잘 설계되었습니다.

테스트 클래스가 적절히 구성되어 있습니다:

  • GitHubClient Mock을 통한 외부 의존성 격리
  • 필요한 레포지토리들의 자동 주입
  • 명확한 테스트 범위 정의

58-74: 복수 사용자 트래킹 시나리오에 대한 핵심 테스트입니다.

이미 웹훅이 등록된 레포지토리에 대해 추가 사용자가 트래킹할 때 등록 카운트가 증가하고 중복 웹훅 생성을 방지하는 로직을 잘 검증합니다. PR의 주요 목표인 "복수의 사용자가 같은 레포를 트래킹할 때의 시나리오 고려"에 대한 핵심 테스트케이스입니다.


99-112: 레지스트리 카운트 감소 로직에 대한 중요한 테스트입니다.

여러 사용자가 동일 레포를 트래킹하는 상황에서 한 사용자가 트래킹을 중단했을 때, 웹훅을 완전히 삭제하지 않고 카운트만 감소시키는 로직을 검증합니다. 이는 다른 사용자의 트래킹에 영향을 주지 않도록 하는 중요한 기능입니다.

gss-domain/src/main/java/com/devoops/domain/entity/user/User.java (1)

3-3: 패키지 리팩터링 승인

GithubToken의 패키지 경로가 새로운 구조(com.devoops.domain.entity.github.token)로 올바르게 업데이트되었습니다.

gss-domain/src/main/java/com/devoops/domain/repository/github/answer/AnswerRankingDomainRepository.java (1)

1-6: 패키지 구조 개선 승인

레포지토리 인터페이스가 answer 서브패키지로 적절히 이동되었고, 관련 도메인 엔티티들의 임포트 경로가 새로운 패키지 구조에 맞게 올바르게 업데이트되었습니다.

gss-domain/src/main/java/com/devoops/service/answerranking/AnswerRankingService.java (1)

3-8: 임포트 경로 업데이트 승인

서비스의 모든 임포트가 새로운 도메인 패키지 구조에 맞게 올바르게 업데이트되었습니다. 기존 서비스 로직은 그대로 유지되어 안정성이 보장됩니다.

gss-domain/src/main/java/com/devoops/domain/repository/github/repo/GithubRepoDomainRepository.java (2)

1-3: 패키지 리팩터링 승인

GithubRepoDomainRepository가 repo 서브패키지로 이동하고 관련 임포트가 올바르게 업데이트되었습니다.


19-23: 다중 사용자 시나리오 지원 개선

existsByExternalIdAndUserIdfindByExternalIdAndUserId 메서드 추가로 여러 사용자가 동일한 레포지토리를 추적하는 시나리오를 적절히 지원하게 되었습니다. 이는 PR 목표와 완벽히 일치합니다.

gss-domain/src/main/java/com/devoops/jpa/repository/github/repo/GithubRepoDomainRepositoryImpl.java (2)

1-7: 패키지 구조 및 임포트 업데이트 승인

구현 클래스가 repo 서브패키지로 적절히 이동되었고, 모든 임포트 경로가 새로운 패키지 구조에 맞게 올바르게 업데이트되었습니다.


43-47: 사용자별 레포지토리 조회 구현 승인

existsByExternalIdAndUserIdfindByExternalIdAndUserId 메서드가 올바르게 구현되어 다중 사용자 시나리오를 지원합니다. 예외 처리도 적절히 구현되어 있습니다.

Also applies to: 78-83

gss-domain/src/main/java/com/devoops/domain/repository/github/webhook/GithubRepoRegistryCountRepository.java (1)

1-18: 새로운 레포지토리 카운팅 인터페이스 승인

GithubRepoRegistryCountRepository는 여러 사용자가 동일한 레포지토리를 트래킹하는 시나리오를 지원하기 위한 핵심 컴포넌트입니다. plusCountminusCount 메서드를 통해 레포지토리별 추적 카운트를 관리할 수 있어, 웹훅 관리에 필수적인 기능을 제공합니다.

gss-domain/src/testFixtures/java/com/devoops/generator/WebhookGenerator.java (2)

3-8: 패키지 구조 업데이트 승인

임포트 경로가 새로운 패키지 구조에 맞게 올바르게 업데이트되었고, 새로운 GithubRepoRegistryCountRepository 의존성이 추가되었습니다.


21-24: 트래킹 카운트 통합 기능 승인

웹훅 생성 시 trackingCount 매개변수를 받아 GithubRepoRegistryCount를 저장하는 기능이 추가되어, 다중 사용자 레포지토리 추적 시나리오를 효과적으로 지원합니다. 이는 PR 목표와 완벽히 일치하는 구현입니다.

gss-domain/src/main/java/com/devoops/jpa/repository/github/answer/AnswerDomainRepositoryImpl.java (1)

1-11: 패키지 리팩터링 및 임포트 정리 승인

구현 클래스가 answer 서브패키지로 적절히 이동되었고, 모든 임포트 경로가 새로운 패키지 구조에 맞게 업데이트되었습니다. 불필요한 임포트 제거와 필요한 임포트 추가도 올바르게 수행되었습니다.

gss-domain/src/testFixtures/java/com/devoops/generator/QuestionGenerator.java (1)

3-5: 패키지 리팩터링 반영 정상

새로운 도메인 패키지 경로로의 import 정리가 일관되고 정확합니다. 테스트 픽스처 동작에 영향 없어 보입니다.

gss-api-app/src/test/java/com/devoops/service/question/QuestionServiceTest.java (1)

8-11: 도메인 패키지 이동 반영 OK

엔티티/레포지토리의 새 네임스페이스로의 import 변경이 일관됩니다. 테스트 로직 변경 없음 확인했습니다.

Also applies to: 13-13, 15-15

gss-api-app/src/test/java/com/devoops/service/answerranking/AnswerRankingServiceTest.java (1)

7-16: 패키지 경로 정리 적절

Answer/Ranking/Repo/PR/Question/RecordStatus, 도메인 레포지토리 import가 새 구조에 맞게 잘 정리되었습니다.

gss-domain/src/main/java/com/devoops/jpa/repository/github/question/QuestionDomainRepositoryImpl.java (1)

1-1: 모듈/패키지 재구성 반영 LGTM

question 하위 패키지로의 이동 및 관련 엔티티/레포지토리 import 변경이 일관성 있게 적용되었습니다. 비즈니스 로직 변화는 없어 보입니다.

Also applies to: 3-5, 7-7, 10-10

gss-api-app/src/test/java/com/devoops/service/facade/PullRequestFacadeServiceTest.java (1)

7-11: 패키지 이동 반영 정상

PR/Repo/Answer/Question/RecordStatus의 새 패키지 경로로 import가 적절히 업데이트되었습니다. 테스트 시맨틱 변화 없음 확인했습니다.

gss-domain/src/main/java/com/devoops/jpa/repository/github/pr/PullRequestDomainRepositoryImpl.java (1)

1-1: PR 하위 모듈로 리팩터링 정합성 양호

패키지/타입 이동(import) 및 JPA 레포지토리 분리(Repo/Question) 적용이 일관적입니다. 기존 트랜잭션 경계와 동작 의미 변화도 없습니다.

Also applies to: 3-6, 9-13

gss-domain/src/main/java/com/devoops/jpa/entity/github/webhook/GithubRepoRegistryCountEntity.java (1)

29-39: 도메인-엔티티 매핑 확인

도메인 reposCount ↔ 엔티티 registryCount 매핑이 의도대로 보입니다. 팩토리/변환 메서드의 책임 분리가 명확합니다.

gss-domain/src/main/java/com/devoops/jpa/repository/github/token/GithubTokenDomainRepositoryImpl.java (1)

1-1: 토큰 모듈로의 패키지 분리 반영

도메인/JPA 토큰 타입 및 예외 코드 import 정리가 적절합니다. 저장 로직도 기존 의미를 유지합니다.

Also applies to: 3-3, 5-9

gss-mcp-app/src/main/java/com/devoops/service/webhook/WebhookFacadeService.java (2)

37-46: 저장소 추적 상태 확인 로직 승인

사용자별 저장소 조회와 추적 상태 확인을 통해 웹훅 이벤트를 조건부로 처리하는 로직이 올바르게 구현되었습니다. 이는 복수의 사용자가 같은 레포를 트래킹하는 시나리오를 적절히 고려한 개선사항입니다.


54-54: 저장소 조회 로직 개선 승인

findByExternalIdAndUserId를 사용하여 사용자별로 저장소를 조회하는 방식으로 변경된 것이 보안과 데이터 무결성 측면에서 올바른 접근입니다.

gss-api-app/src/main/java/com/devoops/service/github/WebHookService.java (2)

61-76: 웹훅 삭제 로직 승인

트랜잭션 경계가 적절히 설정되어 있고, 레지스트리 카운트를 확인하여 다른 사용자가 사용 중인 경우 웹훅을 유지하는 로직이 올바르게 구현되었습니다.


78-89: GitHub API 오류 처리 승인

GithubNotFoundException을 적절히 처리하여 GitHub에서 이미 삭제된 웹훅에 대한 오류를 무시하는 로직이 올바르게 구현되었습니다.

gss-domain/src/main/java/com/devoops/jpa/repository/github/webhook/GithubRepoRegistryCountRepositoryImpl.java (1)

18-64: 저장소 레지스트리 카운트 구현 승인

레지스트리 카운트 관리를 위한 저장소 구현이 올바르게 작성되었습니다:

  • 읽기 전용과 변경 작업에 대한 트랜잭션 경계가 적절히 설정됨
  • 일관된 오류 처리와 도메인 예외 사용
  • 도메인-엔티티 변환이 적절히 처리됨
  • 헬퍼 메서드를 통한 중복 코드 제거
gss-domain/src/main/java/com/devoops/jpa/repository/github/answer/AnswerRankingDomainRepositoryImpl.java (1)

1-91: 패키지 재구성 승인

GitHub 도메인 엔티티들의 패키지 구조를 서브패키지로 재구성한 변경사항이 일관되게 적용되었습니다. 로직 변경 없이 임포트 경로만 업데이트되어 안전한 리팩터링입니다.

gss-api-app/src/test/java/com/devoops/service/repository/RepositoryServiceTest.java (1)

8-16: 테스트 임포트 업데이트 승인

패키지 재구성에 맞춰 테스트 임포트 경로가 올바르게 업데이트되었습니다. 테스트 로직에는 변경이 없어 기존 기능을 그대로 검증할 수 있습니다.

User user = userGenerator.generate("김건우");
GithubRepository repo = repoGenerator.generate(user, "건우의 레포");
webhookGenerator.generate(user, repo);
webhookGenerator.generate(user, repo, 1L);
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

웹훅 생성 API 변경사항을 확인해주세요.

webhookGenerator.generate() 메서드가 3개의 파라미터를 받도록 변경되었습니다. 추가된 1L 파라미터가 웹훅 ID를 의미하는지 확인이 필요합니다.

다음 스크립트로 웹훅 생성기의 새로운 시그니처를 확인해보겠습니다:


🏁 Script executed:

#!/bin/bash
# 웹훅 생성기의 메서드 시그니처 변경사항 확인
ast-grep --pattern 'class WebhookGenerator {
  $$$
  generate($_, $_, $_) {
    $$$
  }
  $$$
}'

Length of output: 98


아래 스크립트를 통해 WebhookGenerator.java 파일 위치와 generate 메서드 시그니처를 다시 확인해 주세요:


🏁 Script executed:

#!/bin/bash
# WebhookGenerator.java 파일 위치 확인
echo "=== 파일 위치 ==="
fd WebhookGenerator.java

# WebhookGenerator 클래스 정의 및 generate 메서드 시그니처 조회
echo "=== 클래스 선언 확인 ==="
grep -n "class WebhookGenerator" $(fd WebhookGenerator.java)

echo "=== generate 메서드 시그니처 확인 ==="
grep -n "generate" $(fd WebhookGenerator.java)

Length of output: 650


WebhookGenerator.generate() 시그니처 변경 확인 및 테스트 수정 요청

WebhookGenerator.generate(User, GithubRepository, long)로 변경되어, 세 번째 파라미터는 웹훅 ID가 아닌 trackingCount를 의미합니다.
현재 테스트에서 리터럴 1L을 그대로 사용 중인데, 의미를 명확히 하기 위해 다음을 적용해주세요:

  • 테스트 코드에서 1L 대신 long trackingCount = 1L; 과 같이 의미 있는 변수명을 도입하거나
  • 상수(TEST_TRACKING_COUNT 등)로 선언하여 가독성을 높일 것

해당 변경은 아래 위치에서 적용 가능합니다:

  • 파일: gss-api-app/src/test/java/com/devoops/service/facade/RepositoryFacadeServiceTest.java
  • 라인: 91 (webhookGenerator.generate(user, repo, 1L);)
🤖 Prompt for AI Agents
In
gss-api-app/src/test/java/com/devoops/service/facade/RepositoryFacadeServiceTest.java
around line 91, the call webhookGenerator.generate(user, repo, 1L) uses a raw
literal but the third parameter is now trackingCount (not webhook ID); replace
the literal with a clearly named variable or constant (e.g., long trackingCount
= 1L; or a TEST_TRACKING_COUNT constant) and use that variable in the generate
call to improve readability and intent.

Comment on lines 41 to 47
public void plusCount() {
this.registryCount += 1;
}

public void minusCount() {
this.registryCount -= 1;
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

동시성 및 무결성 이슈: 카운트 증가/감소 로직

여러 사용자가 동일 레포를 동시에 트래킹/언트래킹할 수 있는 시나리오에서, 현재 in-memory 증가/감소는 다음 문제를 유발합니다.

  • Lost update: 동시에 로드 후 각각 ±1 처리 시 마지막 커밋이 값을 덮어쓸 수 있음.
  • 음수 허용: minusCount()가 0 이하로 내려갈 수 있음.

권장 조치:

  1. 낙관적 락 추가(@Version) 또는 원자적 업데이트 쿼리 사용
  • JPA 엔티티에 버전 필드를 추가해 동시 수정 감지 및 재시도 전략 적용.
  • 또는 JPA 레포지토리에서 update ... set registry_count = registry_count ± 1 where external_id = :id 형태의 단일 쿼리로 원자적 갱신.
  1. 하한선 검증
  • minusCount()에서 0 아래로 내려가는 것을 방지하고 도메인 예외를 발생.

아래는 최소 변경 예시입니다.

  • 음수 방지 가드 추가(현재 메서드 범위 내 diff):
 public void minusCount() {
-    this.registryCount -= 1;
+    if (this.registryCount == 0) {
+        throw new IllegalStateException("registryCount cannot be negative");
+    }
+    this.registryCount -= 1;
 }
  • 낙관적 락 도입(파일 상단 import 및 필드 추가 필요):
@@
-import jakarta.persistence.Table;
+import jakarta.persistence.Table;
+import jakarta.persistence.Version;
@@
     private long registryCount;
+
+    @Version
+    private Long version;

또는, 레포지토리 레벨 원자적 쿼리(별도 인터페이스/Impl에 추가 필요):

@Modifying
@Query("update GithubRepoRegistryCountEntity e set e.registryCount = e.registryCount + 1 where e.externalId = :externalId")
int incrementByExternalId(@Param("externalId") long externalId);

@Modifying
@Query("update GithubRepoRegistryCountEntity e set e.registryCount = e.registryCount - 1 where e.externalId = :externalId and e.registryCount > 0")
int decrementByExternalIdIfPositive(@Param("externalId") long externalId);
🤖 Prompt for AI Agents
In
gss-domain/src/main/java/com/devoops/jpa/entity/github/webhook/GithubRepoRegistryCountEntity.java
around lines 41-47, the current in-memory plusCount()/minusCount() can cause
lost updates under concurrency and allows negative counts; add concurrency and
integrity protections by (1) introducing optimistic locking via a @Version long
version field on the entity so JPA will detect concurrent modifications (add
import and field at the top of the class), and (2) making minusCount() enforce a
non-negative invariant by checking registryCount > 0 before decrementing and
throwing a domain-specific exception when attempting to go below zero;
alternatively, replace these in-memory mutators with atomic repository-level
update methods (Modifying @Query increment/decrement that operate like
registry_count = registry_count ± 1 and return affected rows, using a
conditional e.g. decrement only when > 0) and use their return value to decide
success/failure.

Comment on lines 27 to 31
@Transactional(readOnly = true)
public GithubToken getByUserId(long userId) {
return githubTokenJpaRepository.findByUserId(userId)
.map(GithubTokenEntity::toDomainEntity)
.orElseThrow(() -> new GssException(ErrorCode.NO_RESOURCE_FOUND));
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

예외 코드 및 상위 계층 처리 검증 권장

getByUserId에서 ErrorCode.NO_RESOURCE_FOUND를 던집니다. 토큰 미존재 상황에 대해 더 구체적인 에러 코드가 존재한다면(예: GITHUB_TOKEN_NOT_FOUND) 대체를 고려해 주세요. 또한 서비스/컨트롤러 계층에서 해당 예외를 적절한 HTTP 상태로 매핑하는지 확인 필요합니다.

아래 스크립트로 참조/매핑 현황을 점검할 수 있습니다.


🏁 Script executed:

#!/bin/bash
# 1) 토큰 리포지토리 사용처에서 getByUserId 호출부 확인
rg -n "getByUserId\\(" -A 3

# 2) ErrorCode 정의 중 토큰 관련 코드 존재 여부 확인
rg -n "GITHUB_TOKEN_NOT_FOUND|TOKEN_NOT_FOUND|NO_RESOURCE_FOUND" gss-domain/src/main/java

# 3) 예외 -> HTTP 상태 매핑(ControllerAdvice 등)에서 NO_RESOURCE_FOUND 핸들링 확인
fd -a ".*Advice.*\\.java" | xargs -I{} sh -c 'echo ">>> {}"; rg -n "NO_RESOURCE_FOUND|GssException|ErrorCode" "{}" -A 2 -B 2'

Length of output: 2982


예외 처리 코드 구체화 및 상위 계층 매핑 검증 필요

  • 파일 gss-domain/src/main/java/com/devoops/jpa/repository/github/token/GithubTokenDomainRepositoryImpl.java (라인 31)
    – 기존 ErrorCode.NO_RESOURCE_FOUND 대신 도메인에 맞는 ErrorCode.GITHUB_TOKEN_NOT_FOUND 사용 권장
  • 글로벌 예외 처리 핸들러(@ControllerAdvice 등)에서
    GssException(ErrorCode.GITHUB_TOKEN_NOT_FOUND)이 HTTP 404로 매핑되는지 확인 또는 핸들러 추가

제안하는 코드 변경 예시:

- .orElseThrow(() -> new GssException(ErrorCode.NO_RESOURCE_FOUND));
+ .orElseThrow(() -> new GssException(ErrorCode.GITHUB_TOKEN_NOT_FOUND));
🤖 Prompt for AI Agents
In
gss-domain/src/main/java/com/devoops/jpa/repository/github/token/GithubTokenDomainRepositoryImpl.java
around lines 27 to 31, the repository currently throws
GssException(ErrorCode.NO_RESOURCE_FOUND); replace that with
GssException(ErrorCode.GITHUB_TOKEN_NOT_FOUND) so the exception semantics match
the domain, and then verify the global exception handler (@ControllerAdvice)
maps ErrorCode.GITHUB_TOKEN_NOT_FOUND to HTTP 404 (add a specific mapping if
missing) to ensure the higher layer returns a 404 for missing GitHub tokens.

Comment on lines +6 to +8
import com.devoops.domain.entity.github.token.GithubToken;
import com.devoops.domain.entity.github.pr.PullRequest;
import com.devoops.domain.entity.github.question.Question;
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

패키지 리네이밍 반영 LGTM

새 패키지 경로(token/pr/question)로의 import 변경이 일관됩니다. 기능적 영향 없음으로 보입니다.

레거시 경로가 남아있는지 빠르게 점검하려면 아래 스크립트를 루트에서 실행해 주세요.


🏁 Script executed:

#!/bin/bash
# 레거시 import 경로 탐지
rg -n 'com\.devoops\.domain\.entity\.github\.GithubToken|com\.devoops\.domain\.entity\.github\.PullRequest|com\.devoops\.domain\.entity\.github\.Question'

Length of output: 2039


QuestionAnswer 패키지 리네이밍 반영 필요
현재 com.devoops.domain.entity.github.QuestionAnswer 레거시 import가 여전히 남아 있어, 새 패키지 경로(com.devoops.domain.entity.github.question.QuestionAnswer)로 일괄 변경해야 합니다.

아래 파일들에서 수정이 필요합니다:

  • gss-domain/src/main/java/com/devoops/service/question/QuestionService.java:7
  • gss-domain/src/main/java/com/devoops/domain/repository/github/question/QuestionDomainRepository.java:5
  • gss-domain/src/main/java/com/devoops/jpa/repository/github/question/QuestionJpaRepository.java:3, 16
  • gss-domain/src/main/java/com/devoops/jpa/repository/github/question/QuestionDomainRepositoryImpl.java:6
  • gss-mcp-app/src/test/java/com/devoops/service/webhook/WebhookFacadeServiceTest.java:9
  • gss-api-app/src/test/java/com/devoops/service/question/QuestionServiceTest.java:12
  • gss-api-app/src/main/java/com/devoops/dto/response/QuestionAnswerResponse.java:3
  • gss-api-app/src/main/java/com/devoops/dto/response/PullRequestReadResponse.java:4
  • gss-api-app/src/main/java/com/devoops/dto/response/PullRequestDetailReadResponse.java:4
  • gss-api-app/src/main/java/com/devoops/dto/response/QuestionResponse.java:3
  • gss-api-app/src/main/java/com/devoops/dto/response/QuestionBriefResponse.java:3
  • gss-api-app/src/main/java/com/devoops/service/facade/PullRequestFacadeService.java:5

남은 레거시 경로를 모두 찾으시려면 아래 스크립트를 실행하세요:

rg -n 'com\.devoops\.domain\.entity\.github\.QuestionAnswer'
🤖 Prompt for AI Agents
In gss-mcp-app/src/main/java/com/devoops/event/QuestionEventListener.java around
lines 6 to 8, the legacy import com.devoops.domain.entity.github.QuestionAnswer
must be replaced with the new package path
com.devoops.domain.entity.github.question.QuestionAnswer; apply the same
replacement in the other affected files listed
(gss-domain/src/main/java/com/devoops/service/question/QuestionService.java:7;
gss-domain/src/main/java/com/devoops/domain/repository/github/question/QuestionDomainRepository.java:5;
gss-domain/src/main/java/com/devoops/jpa/repository/github/question/QuestionJpaRepository.java:3,16;
gss-domain/src/main/java/com/devoops/jpa/repository/github/question/QuestionDomainRepositoryImpl.java:6;
gss-mcp-app/src/test/java/com/devoops/service/webhook/WebhookFacadeServiceTest.java:9;
gss-api-app/src/test/java/com/devoops/service/question/QuestionServiceTest.java:12;
gss-api-app/src/main/java/com/devoops/dto/response/QuestionAnswerResponse.java:3;
gss-api-app/src/main/java/com/devoops/dto/response/PullRequestReadResponse.java:4;
gss-api-app/src/main/java/com/devoops/dto/response/PullRequestDetailReadResponse.java:4;
gss-api-app/src/main/java/com/devoops/dto/response/QuestionResponse.java:3;
gss-api-app/src/main/java/com/devoops/dto/response/QuestionBriefResponse.java:3;
gss-api-app/src/main/java/com/devoops/service/facade/PullRequestFacadeService.java:5)
and then run rg -n 'com\.devoops\.domain\.entity\.github\.QuestionAnswer' to
confirm no legacy imports remain.

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 (4)
gss-common/src/main/java/com/devoops/exception/errorcode/ErrorCode.java (2)

29-30: 레포지토리 등록 카운트 관련 에러코드에 5xx 사용 재검토

도메인 자원 부재/검증 실패는 보통 4xx(404/400/409 등)에 가깝습니다. 기존에도 GitHub 계열 NotFound를 5xx로 두고 있어 일관성은 있지만, 대외 API/클라이언트 계약 관점에서는 오해 소지가 큽니다. 향후 마이그레이션이 가능하다면 아래처럼 재분류를 고려해 주세요.

  • NOT_FOUND 계열: 404
  • INVALID 상태: 400 또는 409

현 PR 범위에서 변경이 어렵다면, 컨트롤러 단에서의 상태 매핑 문서를 남기거나 API 스펙에 명시하는 것도 방법입니다.


30-30: 메시지 한글 띄어쓰기 수정 제안

"0이하" → "0 이하"로 띄어쓰기 보정하면 가독성이 좋아집니다.

적용 diff:

-    INVALID_GITHUB_REPOSITORY_REGISTRY_COUNT(500, "레포지토리 등록 카운트가 0이하입니다"),
+    INVALID_GITHUB_REPOSITORY_REGISTRY_COUNT(500, "레포지토리 등록 카운트가 0 이하입니다"),
gss-domain/src/main/java/com/devoops/jpa/repository/github/webhook/GithubRepoRegistryCountJpaRepository.java (2)

24-25: 삭제 결과 활용을 위해 deleteByExternalId 반환값 노출 고려

삭제 영향 행 수를 반환받으면 상위 레이어에서 존재하지 않는 키 삭제에 대한 로깅/보상 처리가 용이합니다. 필수는 아니나 운영 가시성 측면에서 유용합니다.

적용 diff(인터페이스):

-    void deleteByExternalId(long repoExternalId);
+    long deleteByExternalId(long repoExternalId);

구현부에서의 활용 예(다른 파일, 참고용):

long deleted = githubRepoRegistryCountJpaRepository.deleteByExternalId(repoExternalId);
if (deleted == 0) {
    // 필요 시 로그 또는 예외 처리
}

12-15: externalId 컬럼에 대한 유니크 인덱스/제약 추가 권장

findBy/exist/update/delete 모두 externalId 조건에 의존합니다. DB 레벨에서 unique + index를 부여해

  • 중복 데이터 방지
  • 조건 업데이트의 성능/락 효율 향상
    을 확보하는 것을 권장합니다. 엔티티 매핑(GithubRepoRegistryCountEntity)에 유니크 인덱스 설정을 고려해 주세요.
📜 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 settings in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between dde44db and 326ba96.

📒 Files selected for processing (7)
  • gss-api-app/src/main/java/com/devoops/service/github/WebHookService.java (1 hunks)
  • gss-common/src/main/java/com/devoops/exception/errorcode/ErrorCode.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/domain/repository/github/webhook/GithubRepoRegistryCountRepository.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/jpa/repository/github/token/GithubTokenDomainRepositoryImpl.java (2 hunks)
  • gss-domain/src/main/java/com/devoops/jpa/repository/github/webhook/GithubRepoRegistryCountJpaRepository.java (1 hunks)
  • gss-domain/src/main/java/com/devoops/jpa/repository/github/webhook/GithubRepoRegistryCountRepositoryImpl.java (1 hunks)
  • gss-mcp-app/src/main/java/com/devoops/controller/webhook/WebhookController.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • gss-domain/src/main/java/com/devoops/domain/repository/github/webhook/GithubRepoRegistryCountRepository.java
  • gss-mcp-app/src/main/java/com/devoops/controller/webhook/WebhookController.java
  • gss-domain/src/main/java/com/devoops/jpa/repository/github/token/GithubTokenDomainRepositoryImpl.java
  • gss-api-app/src/main/java/com/devoops/service/github/WebHookService.java
🧰 Additional context used
🧬 Code Graph Analysis (1)
gss-domain/src/main/java/com/devoops/jpa/repository/github/webhook/GithubRepoRegistryCountRepositoryImpl.java (1)
gss-domain/src/main/java/com/devoops/jpa/repository/github/token/GithubTokenDomainRepositoryImpl.java (1)
  • Repository (13-33)
🔇 Additional comments (3)
gss-domain/src/main/java/com/devoops/jpa/repository/github/webhook/GithubRepoRegistryCountRepositoryImpl.java (3)

18-23: save 매핑 및 반환 로직 적절

도메인 ↔ 엔티티 변환 후 저장/재변환 흐름이 일관적입니다.


38-42: 벌크 업데이트 사용 시 영속성 컨텍스트 정합성 주의

동일 트랜잭션에서 이미 로드된 GithubRepoRegistryCountEntity가 존재하면, JPQL 벌크 업데이트 결과와 1차 캐시 상태가 불일치할 수 있습니다. JPA 리포지토리 쿼리에 clearAutomatically/flushAutomatically 옵션을 적용하는 제안을 함께 반영해 주세요.


59-62: getByExternalId 예외 매핑 일관성 양호

도메인 리턴 및 NotFound 예외 매핑이 다른 리포지토리 구현들과 합치합니다.

Comment on lines +46 to +51
public void minusCount(long externalId) {
int updatedRow = githubRepoRegistryCountJpaRepository.decrementByExternalIdIfPositive(externalId);
if (updatedRow == 0) {
throw new GssException(ErrorCode.GITHUB_REPOSITORY_REGISTRY_COUNT_NOT_FOUND);
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

minusCount: NotFound와 Invalid 상태를 구분해 예외 매핑

현재 updatedRow == 0이면 모두 NotFound로 처리됩니다. 실제로는

  • 레코드는 존재하나 registryCount == 0인 경우: INVALID_GITHUB_REPOSITORY_REGISTRY_COUNT
  • 레코드 자체가 없음: GITHUB_REPOSITORY_REGISTRY_COUNT_NOT_FOUND
    로 분기하는 것이 정확합니다.

적용 diff:

     @Override
     @Transactional
     public void minusCount(long externalId) {
         int updatedRow = githubRepoRegistryCountJpaRepository.decrementByExternalIdIfPositive(externalId);
         if (updatedRow == 0) {
-            throw new GssException(ErrorCode.GITHUB_REPOSITORY_REGISTRY_COUNT_NOT_FOUND);
+            if (githubRepoRegistryCountJpaRepository.existsByExternalId(externalId)) {
+                throw new GssException(ErrorCode.INVALID_GITHUB_REPOSITORY_REGISTRY_COUNT);
+            }
+            throw new GssException(ErrorCode.GITHUB_REPOSITORY_REGISTRY_COUNT_NOT_FOUND);
         }
     }

참고: 본 분기 도입 시 ErrorCode.INVALID_GITHUB_REPOSITORY_REGISTRY_COUNT가 실사용됩니다.

📝 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 minusCount(long externalId) {
int updatedRow = githubRepoRegistryCountJpaRepository.decrementByExternalIdIfPositive(externalId);
if (updatedRow == 0) {
throw new GssException(ErrorCode.GITHUB_REPOSITORY_REGISTRY_COUNT_NOT_FOUND);
}
}
@Override
@Transactional
public void minusCount(long externalId) {
int updatedRow = githubRepoRegistryCountJpaRepository.decrementByExternalIdIfPositive(externalId);
if (updatedRow == 0) {
if (githubRepoRegistryCountJpaRepository.existsByExternalId(externalId)) {
throw new GssException(ErrorCode.INVALID_GITHUB_REPOSITORY_REGISTRY_COUNT);
}
throw new GssException(ErrorCode.GITHUB_REPOSITORY_REGISTRY_COUNT_NOT_FOUND);
}
}
🤖 Prompt for AI Agents
In
gss-domain/src/main/java/com/devoops/jpa/repository/github/webhook/GithubRepoRegistryCountRepositoryImpl.java
around lines 46 to 51, the method currently treats updatedRow == 0 as NotFound
for all cases; change it to distinguish between "record missing" and "record
exists but registryCount == 0": first query the repository for existence (e.g.,
findByExternalId or existsByExternalId), if not present throw
GssException(ErrorCode.GITHUB_REPOSITORY_REGISTRY_COUNT_NOT_FOUND); if present
read the current registryCount (or a tailored query that returns the count) and
if it is 0 throw
GssException(ErrorCode.INVALID_GITHUB_REPOSITORY_REGISTRY_COUNT); otherwise
perform the decrement and ensure the update affected a row (throw NotFound or
handle race condition if it returns 0). Ensure no new logging required and reuse
existing ErrorCode values.

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: 1

♻️ Duplicate comments (1)
gss-domain/src/main/java/com/devoops/jpa/entity/github/webhook/GithubRepoRegistryCountEntity.java (1)

4-13: 동시성 손실 업데이트 방지: @Version 필드 추가 권장

여러 사용자가 동일 레포를 동시에 트래킹/언트래킹하는 시나리오에서, 엔티티 기반 read-modify-write 갱신은 마지막 커밋이 값을 덮어써 증가/감소가 소실될 수 있습니다. 낙관적 락(@Version)으로 동시 수정 감지를 적용하고, 서비스 계층에서 재시도 전략을 두는 것을 권장합니다.

아래 최소 변경으로 @Version을 도입할 수 있습니다.

@@
 import jakarta.persistence.GeneratedValue;
 import jakarta.persistence.GenerationType;
 import jakarta.persistence.Id;
 import jakarta.persistence.Table;
+import jakarta.persistence.Version;
@@
     private long registryCount;
+
+    @Version
+    private Long version;

Also applies to: 27-29

🧹 Nitpick comments (2)
gss-domain/src/main/java/com/devoops/jpa/entity/github/webhook/GithubRepoRegistryCountEntity.java (2)

25-27: 명시적 컬럼 매핑 및 NOT NULL 지정으로 이식성 강화

H2/MySQL 물리 네이밍 전략 차이를 제거하고 스키마를 명확히 하기 위해 컬럼명을 명시하고 NOT NULL을 선언하는 것을 권장합니다.

@@
-import jakarta.persistence.Table;
+import jakarta.persistence.Table;
+import jakarta.persistence.Column;
@@
-    private long externalId;
+    @Column(name = "external_id", nullable = false)
+    private long externalId;
@@
-    private long registryCount;
+    @Column(name = "registry_count", nullable = false)
+    private long registryCount;

Also applies to: 4-13


14-16: 음수 방지 체크 제약(선택): registry_count >= 0

도메인 불변식(카운트는 음수가 될 수 없음)을 DB 차원에서도 보강하고 싶다면 체크 제약을 추가할 수 있습니다. Hibernate 벤더 확장임을 유의하세요(MySQL 버전에 따라 동작 차이 가능).

@@
-import jakarta.persistence.Table;
+import jakarta.persistence.Table;
+import org.hibernate.annotations.Check;
@@
-@Table(
+@Check(constraints = "registry_count >= 0")
+@Table(
     name = "github_repo_registry_count",
     uniqueConstraints = {
         @UniqueConstraint(
             name = "uk_github_repo_registry_count_external_id",
             columnNames = "external_id"
         )
     }
 )

Also applies to: 4-13

📜 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 f1fd798 and 5b83e10.

📒 Files selected for processing (1)
  • gss-domain/src/main/java/com/devoops/jpa/entity/github/webhook/GithubRepoRegistryCountEntity.java (1 hunks)
⏰ 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 (2)
gss-domain/src/main/java/com/devoops/jpa/entity/github/webhook/GithubRepoRegistryCountEntity.java (2)

29-39: 도메인 매핑(from/to) 구현은 명확하고 일관적입니다

  • from(): id null 위임, externalId/카운트 매핑 OK
  • toDomainEntity(): 도메인 생성자 인자 순서/의미 일치

1-40: 원자적 증감 쿼리 구현 확인

  • gss-domain/src/main/java/com/devoops/jpa/repository/github/webhook/GithubRepoRegistryCountJpaRepository.java
    • incrementByExternalId(@param("externalId") long)
    • decrementByExternalIdIfPositive(@param("externalId") long)
    이 두 메서드가 각각 UPDATE … SET registry_count = registry_count ± 1 쿼리를 사용해 DB 레벨에서 원자적 증감 처리를 보장합니다.
  • 별도로 @Version 기반 낙관적 락이나 재시도 로직이 없어도 동시성 안전성이 확보됩니다.

Comment on lines +14 to +16
@Table(name = "github_repo_registry_count")
@Entity
@Getter
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

데이터 무결성: externalId 중복을 차단하는 유니크 제약 추가

동일 externalId에 대해 중복 로우가 생성되면 카운트 일관성이 깨집니다. DB 레벨 유니크 제약으로 단일 행을 강제하세요.

@@
-import jakarta.persistence.Table;
+import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
@@
-@Table(name = "github_repo_registry_count")
+@Table(
+    name = "github_repo_registry_count",
+    uniqueConstraints = {
+        @UniqueConstraint(
+            name = "uk_github_repo_registry_count_external_id",
+            columnNames = "external_id"
+        )
+    }
+)
 @Entity

Also applies to: 4-13

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] 같은 레포 대상으로 웹훅이 겹치지 않도록 수정

1 participant