Skip to content

Conversation

@kamillcream
Copy link
Contributor

@kamillcream kamillcream commented Aug 5, 2025

📌 PR 개요

  • /gcp project-list(프로젝트 ID 조회) , /gcp project-register (채널에 프로젝트 등록), /gcp zone-list ( register된 프로젝트에 존재하는 instance들의 zone 정보 조회 )

✅ 변경사항

  • /gcp project-list, /gcp project-register, /gcp zone-list 명령어 등록
  • /gcp register -> /gcp login, /gcp explore -> /gcp project-list 명령어 변경
  • GcpProject 엔티티 수정, 레포지토리 , 서비스 클래스 생성.

🔍 체크리스트

  • PR 제목은 명확한가요?
  • 관련 이슈가 있다면 연결했나요?
  • 로컬 테스트는 통과했나요?
  • 코드에 불필요한 부분은 없나요?

📎 관련 이슈

Closes #11


💬 기타 참고사항

Summary by CodeRabbit

  • 신규 기능

    • Discord 봇에 GCP 프로젝트 ID 목록 조회, 프로젝트 등록, VM Zone 목록 조회 명령어가 추가되었습니다.
    • Google 계정 연동 명령어가 "register"에서 "login"으로 변경되었습니다.
  • 버그 수정

    • 사용자 등록 로직이 개선되어 중복 등록이 방지됩니다.
  • 기타

    • Google Cloud 관련 라이브러리 의존성이 추가되었습니다.
    • 프로젝트 및 VM Zone 정보를 효율적으로 관리하기 위한 내부 구조가 개선되었습니다.

@kamillcream kamillcream requested a review from 7ijin01 August 5, 2025 17:40
@kamillcream kamillcream self-assigned this Aug 5, 2025
@kamillcream kamillcream added the in: api API 관련 label Aug 5, 2025
@kamillcream kamillcream added status: feedback provided 피드백이 반영되었거나 제공된 상태입니다. priority: high 긴급하며 우선적으로 해결해야 하는 작업입니다. type: feature 새로운 기능을 추가하거나 구현하는 작업입니다. labels Aug 5, 2025
@coderabbitai
Copy link

coderabbitai bot commented Aug 5, 2025

Walkthrough

Discord 봇에서 GCP 프로젝트 ID 및 VM Zone 정보를 사용자 액세스 토큰을 활용해 조회하고, 프로젝트 등록 및 목록 조회, Zone 목록 조회 명령어를 추가하는 기능이 구현되었습니다. 이를 위해 서비스, 레포지토리, DTO, 명령어, 워크플로우 등 여러 계층에서 새로운 메서드와 클래스를 도입하고 기존 구조를 확장하였습니다.

Changes

Cohort / File(s) Change Summary
Discord 명령어 및 서비스 확장
src/main/java/com/gcp/domain/discord/service/GcpBotService.java, src/main/java/com/gcp/domain/discord/service/DiscordUserService.java, src/main/java/com/gcp/global/config/DiscordBotConfig.java
GCP 관련 slash 명령어(project-list, project-register, zone-list) 추가 및 기존 명령어 로직 확장/수정, 관련 서비스 메서드 호출 및 예외 처리 추가
GCP 프로젝트 및 Zone 조회 기능
src/main/java/com/gcp/domain/gcp/service/GcpService.java, src/main/java/com/gcp/domain/gcp/dto/ProjectZoneDto.java
GCP 프로젝트 ID 목록 및 각 프로젝트별 활성 VM Zone 목록을 조회하는 신규 메서드 및 DTO 추가
GCP 프로젝트 등록 서비스 및 레포지토리
src/main/java/com/gcp/domain/gcp/service/GcpProjectCommandService.java, src/main/java/com/gcp/domain/gcp/service/GcpProjectCommandServiceImpl.java, src/main/java/com/gcp/domain/gcp/repository/GcpProjectRepository.java
GCP 프로젝트 등록 서비스 인터페이스/구현체 및 프로젝트 ID 목록 조회용 레포지토리 메서드 추가
도메인 엔티티/레포지토리/유저 관련
src/main/java/com/gcp/domain/gcp/entity/GcpProject.java, src/main/java/com/gcp/domain/discord/repository/DiscordUserRepository.java
GcpProject 엔티티에서 zone/credentialsJson 필드 삭제, 빌더 패턴/팩토리 메서드 도입, DiscordUserRepository에 existsByUserIdAndGuildId 메서드 추가
의존성 및 워크플로우
build.gradle, .github/workflows/deploy.yml
Google Cloud 관련 라이브러리 의존성 추가, CI/CD 워크플로우 트리거 브랜치 변경
기타 포맷팅
src/main/java/com/gcp/domain/oauth2/handler/OAuth2AuthenticationSuccessHandler.java
코드 정렬 및 포맷팅만 변경, 기능 변화 없음

Sequence Diagram(s)

sequenceDiagram
    participant User as Discord User
    participant Bot as Discord Bot
    participant GcpService
    participant GcpProjectCommandService
    participant GcpProjectRepository
    participant DiscordUserRepository
    participant GoogleAPI as Google Cloud API

    User->>Bot: /gcp project-list
    Bot->>GcpService: getProjectIds(userId, guildId)
    GcpService->>GoogleAPI: GET /v1/projects (accessToken)
    GoogleAPI-->>GcpService: project list
    GcpService-->>Bot: List<String> projectIds
    Bot-->>User: 프로젝트 ID 목록 응답

    User->>Bot: /gcp project-register project_id
    Bot->>GcpProjectCommandService: insertNewGcpProject(userId, guildId, projectId)
    GcpProjectCommandService->>DiscordUserRepository: findByUserIdAndGuildId(userId, guildId)
    DiscordUserRepository-->>GcpProjectCommandService: DiscordUser
    GcpProjectCommandService->>GcpProjectRepository: save(GcpProject)
    GcpProjectRepository-->>GcpProjectCommandService: saved
    GcpProjectCommandService-->>Bot: 완료
    Bot-->>User: 등록 성공/실패 메시지

    User->>Bot: /gcp zone-list
    Bot->>GcpService: getActiveInstanceZones(userId, guildId)
    GcpService->>GcpProjectRepository: findAllProjectIdsByDiscordUser(DiscordUser)
    GcpProjectRepository-->>GcpService: List<String> projectIds
    loop for each projectId
        GcpService->>GoogleAPI: GET /v1/projects/{projectId}/aggregated/instances (accessToken)
        GoogleAPI-->>GcpService: instance/zone info
    end
    GcpService-->>Bot: List<ProjectZoneDto>
    Bot-->>User: 프로젝트별 Zone 목록 응답
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Assessment against linked issues

Objective Addressed Explanation
사용자 액세스 토큰으로 참여중인 프로젝트 ID, VM이 속한 Zone 정보 조회 기능 구현 (#11)
명령어 추가 (project-list, project-register, zone-list 등) (#11)
application.yml에 scope 추가 (#11) application.yml 등 설정 관련 변경은 본 PR에서 확인되지 않음.

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
워크플로우 트리거 브랜치 변경 (.github/workflows/deploy.yml) CI/CD 트리거 브랜치 변경은 본 이슈의 GCP 프로젝트/Zone 조회 기능과 직접적인 관련이 없음.
Google Cloud Logging 등 일부 의존성 추가 (build.gradle) logging 등 일부 라이브러리 추가는 본 이슈의 명령어/조회 기능과 직접 연관이 없어 보임.

Possibly related PRs

Suggested labels

in: infra, priority: medium

Suggested reviewers

  • 7ijin01

Poem

🐇
구름 위 프로젝트를 찾아,
토끼 봇이 명령어를 외치네!
"프로젝트 목록, Zone도 보여줘~"
깡총깡총 API를 누비며
새 기능 품고 돌아왔네.
GCP와 Discord, 오늘도 찰떡궁합!
☁️✨

Note

⚡️ Unit Test Generation is now available in beta!

Learn more here, or try it out under "Finishing Touches" below.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#11-get-projectid-zone

🪧 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.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @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 comments)

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

Other keywords and placeholders

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

CodeRabbit Configuration File (.coderabbit.yaml)

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

Documentation and Community

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🔭 Outside diff range comments (1)
src/main/java/com/gcp/domain/discord/service/DiscordUserService.java (1)

14-21: 조건부 로직이 반대로 구현되어 있습니다.

현재 로직은 사용자가 이미 존재할 때 새로운 엔티티를 생성하고 저장하려고 시도합니다. 이는 데이터베이스 제약 조건 위반을 일으킬 수 있으며, 메서드명 insertDiscordUser와 일치하지 않습니다.

다음과 같이 수정해야 합니다:

public boolean insertDiscordUser(String userId, String userName, String guildId, String guildName){
-    if (discordUserRepository.existsByUserIdAndGuildId(userId, guildId)) {
+    if (!discordUserRepository.existsByUserIdAndGuildId(userId, guildId)) {
        DiscordUser discordUser = new DiscordUser(userId, userName, guildId, guildName);
        discordUserRepository.save(discordUser);
        return true;
    }
    return false;
}
🧹 Nitpick comments (3)
src/main/java/com/gcp/domain/gcp/dto/ProjectZoneDto.java (1)

7-11: Record 기반 DTO 설계가 적절하지만 null 안전성을 고려해보세요.

Java record와 @Builder 사용이 적절합니다. 불변성과 간결성을 제공합니다.

null 안전성 개선을 위한 제안:

 @Builder
 public record ProjectZoneDto(
-        String projectId, List<String> zoneList
+        @NonNull String projectId, 
+        @NonNull List<String> zoneList
 ) {
 }

또는 기본값 제공:

@Builder
public record ProjectZoneDto(
        String projectId, 
        @Builder.Default List<String> zoneList = new ArrayList<>()
) {
}
src/main/java/com/gcp/domain/discord/service/GcpBotService.java (1)

85-93: 성공 메시지에 프로젝트 ID 포함 권장

프로젝트 등록 성공 메시지에 등록된 프로젝트 ID를 포함하면 사용자가 확인하기 더 좋을 것 같습니다.

 case "project-register" -> {
     try{
         String projectId = getRequiredOption(event, "project_id");
         gcpProjectCommandService.insertNewGcpProject(userId, guildId, projectId);
-        event.reply("프로젝트가 등록되었습니다.").queue();
+        event.reply(String.format("✅ 프로젝트 `%s`가 성공적으로 등록되었습니다.", projectId)).queue();
     } catch (RuntimeException e){
         event.reply(e.getMessage()).queue();
     }
 }
src/main/java/com/gcp/domain/gcp/service/GcpService.java (1)

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

org.checkerframework.checker.units.qual.A import가 사용되지 않고 있습니다.

-import org.checkerframework.checker.units.qual.A;
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 9e9c3a7 and c421654.

📒 Files selected for processing (13)
  • .github/workflows/deploy.yml (1 hunks)
  • build.gradle (1 hunks)
  • src/main/java/com/gcp/domain/discord/repository/DiscordUserRepository.java (1 hunks)
  • src/main/java/com/gcp/domain/discord/service/DiscordUserService.java (1 hunks)
  • src/main/java/com/gcp/domain/discord/service/GcpBotService.java (4 hunks)
  • src/main/java/com/gcp/domain/gcp/dto/ProjectZoneDto.java (1 hunks)
  • src/main/java/com/gcp/domain/gcp/entity/GcpProject.java (1 hunks)
  • src/main/java/com/gcp/domain/gcp/repository/GcpProjectRepository.java (1 hunks)
  • src/main/java/com/gcp/domain/gcp/service/GcpProjectCommandService.java (1 hunks)
  • src/main/java/com/gcp/domain/gcp/service/GcpProjectCommandServiceImpl.java (1 hunks)
  • src/main/java/com/gcp/domain/gcp/service/GcpService.java (3 hunks)
  • src/main/java/com/gcp/domain/oauth2/handler/OAuth2AuthenticationSuccessHandler.java (1 hunks)
  • src/main/java/com/gcp/global/config/DiscordBotConfig.java (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: kamillcream
PR: Cloudible/GCP#8
File: src/main/java/com/gcp/domain/gcp/service/GcpService.java:38-38
Timestamp: 2025-08-01T19:45:32.212Z
Learning: Discord 봇 서비스에서는 orElseThrow()로 예외를 던지는 것보다 Optional.isEmpty() 체크 후 사용자 친화적인 메시지를 직접 반환하는 것을 선호함. 특히 GcpService.java에서 Google OAuth 토큰 조회 실패 시 예외 대신 안내 메시지 반환이 더 적절함.
📚 Learning: discord 봇 서비스에서는 orelsethrow()로 예외를 던지는 것보다 optional.isempty() 체크 후 사용자 친화적인 메시지를 직접 반환하는 것을 선호함. 특히...
Learnt from: kamillcream
PR: Cloudible/GCP#8
File: src/main/java/com/gcp/domain/gcp/service/GcpService.java:38-38
Timestamp: 2025-08-01T19:45:32.212Z
Learning: Discord 봇 서비스에서는 orElseThrow()로 예외를 던지는 것보다 Optional.isEmpty() 체크 후 사용자 친화적인 메시지를 직접 반환하는 것을 선호함. 특히 GcpService.java에서 Google OAuth 토큰 조회 실패 시 예외 대신 안내 메시지 반환이 더 적절함.

Applied to files:

  • src/main/java/com/gcp/domain/oauth2/handler/OAuth2AuthenticationSuccessHandler.java
  • src/main/java/com/gcp/domain/discord/service/GcpBotService.java
  • src/main/java/com/gcp/domain/gcp/service/GcpService.java
🔇 Additional comments (7)
src/main/java/com/gcp/domain/oauth2/handler/OAuth2AuthenticationSuccessHandler.java (1)

81-82: 코드 포매팅이 개선되었지만 예외 처리 전략을 재검토해보세요.

인덴테이션 개선은 좋습니다. 하지만 이전 학습 내용에 따르면, Discord 봇 서비스에서는 orElseThrow()보다는 Optional.isEmpty() 체크 후 사용자 친화적인 메시지 반환을 선호합니다.

OAuth2 인증 컨텍스트에서도 동일한 패턴을 적용할지 확인해보세요:

if (discordUserOptional.isEmpty()) {
    log.error("DiscordUser를 찾을 수 없습니다: userId={}, guildId={}", userId, guildId);
    // 적절한 오류 응답 처리
    return;
}
DiscordUser discordUser = discordUserOptional.get();
src/main/java/com/gcp/domain/discord/repository/DiscordUserRepository.java (1)

20-20: 존재 여부 확인 메서드 추가가 적절합니다.

Spring Data JPA 네이밍 컨벤션을 따라 자동으로 쿼리가 생성되며, 단순 존재 여부 확인이 필요한 경우 findByUserIdAndGuildId()보다 효율적입니다.

src/main/java/com/gcp/domain/gcp/repository/GcpProjectRepository.java (1)

19-20: JPQL 쿼리 구현이 올바릅니다.

메서드 구현이 적절하며 Spring Data JPA 규칙을 잘 따르고 있습니다. 프로젝트 ID만 선택적으로 조회하는 효율적인 쿼리입니다.

src/main/java/com/gcp/domain/gcp/entity/GcpProject.java (2)

12-12: Builder 패턴 추가가 적절합니다.

Lombok의 @builder 어노테이션 추가로 객체 생성이 더 유연해졌습니다.


25-30: 정적 팩토리 메서드 구현이 깔끔합니다.

create 메서드가 필수 필드만으로 객체를 생성하는 명확한 방법을 제공합니다. Builder 패턴과 잘 조합되어 있습니다.

src/main/java/com/gcp/global/config/DiscordBotConfig.java (1)

42-46: 새로운 서브커맨드 구성이 적절합니다.

"register"를 "login"으로 변경하고 새로운 프로젝트 관리 명령어들을 추가한 것이 PR 목표와 잘 일치합니다. "project-register" 명령어에 필수 매개변수 설정도 올바릅니다.

src/main/java/com/gcp/domain/discord/service/GcpBotService.java (1)

95-116: zone-list 명령어 구현 양호

프로젝트별 Zone 목록을 보기 좋은 형식으로 출력하고, 적절한 오류 처리가 구현되어 있습니다.

Comment on lines 3 to 6
on:
push:
branches: [main]
branches: [feat/#11-get-projectid-zone]

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

프로덕션 배포가 기능 브랜치 푸시마다 자동 실행됩니다 – 의도 및 위험도 확인 필요
현재 push 트리거를 feat/#11-get-projectid-zone 한정으로 변경하면서, 해당 브랜치에 커밋이 푸시될 때마다 CI·CD 파이프라인이 전체적으로 실행-되어 서버에 바로 배포까지 이뤄집니다.

  1. 기능 브랜치가 아직 PR 머지 전이라면, 검증되지 않은 코드가 실서버에 배포될 수 있는 위험이 있습니다.
  2. 브랜치명에 # 문자가 포함돼 있어 YAML 파서가 오해하지는 않지만, 가독성과 안전성을 위해 따옴표로 감싸는 편이 일반적입니다.

예시 수정안:

on:
  push:
    branches: ['feat/#11-get-projectid-zone']   # 또는 main / release/* 등으로 조정

의도적으로 임시 테스트 서버에만 배포하도록 설계한 것인지, 아니면 실서버 배포 워크플로우를 따로 두고 싶은 것인지 확인 부탁드립니다.


🏁 Script executed:

#!/bin/bash
# Show the beginning of the deploy workflow to verify trigger settings
sed -n '1,20p' .github/workflows/deploy.yml

Length of output: 353


🏁 Script executed:

#!/bin/bash
# Display full workflow file to inspect deploy steps
sed -n '1,200p' .github/workflows/deploy.yml

Length of output: 3368


프로덕션 배포 워크플로우 트리거 설정 검토 및 수정 필요

.github/workflows/deploy.ymlon.push.branchesfeat/#11-get-projectid-zone 한정으로 설정되어 있어 해당 브랜치에 푸시될 때마다 CI·CD(빌드 → SSH 배포) 전체가 실행됩니다.
이로 인해 다음과 같은 위험이 있습니다.

  • 아직 PR 머지 전인 기능 브랜치의 검증되지 않은 코드가 실서버에 즉시 배포될 수 있음
  • 향후 다른 기능 브랜치에도 동일한 실수로 배포 트리거가 설정될 우려

또한, 브랜치명에 # 문자가 포함돼 있을 경우 YAML 가독성과 안전성을 위해 다음과 같이 따옴표로 감싸는 것을 권장드립니다.

수정 예시:

 on:
   push:
-    branches: [feat/#11-get-projectid-zone]
+    branches: ['feat/#11-get-projectid-zone']   # 또는 ['main'], ['release/*'] 등으로 변경

▸ 의도하신 바가

  1. 기능 브랜치 푸시 시 실서버 배포인가,
  2. 임시 테스트 환경용 워크플로우인가
    인지 확인 후,
  • 프로덕션 배포는 main/release/* 전용으로 분리
  • 기능 브랜치는 테스트 전용 워크플로우 생성

두 가지 중 하나로 설정을 명확히 분리해 주시기 바랍니다.

🤖 Prompt for AI Agents
In .github/workflows/deploy.yml around lines 3 to 6, the push trigger is set
only for the feature branch 'feat/#11-get-projectid-zone', which risks deploying
unverified code to production. To fix this, confirm whether this workflow is for
production deployment or testing. If for production, change the trigger branches
to only 'main' and 'release/*' branches and remove the feature branch. If for
testing, create a separate workflow for feature branches. Also, enclose branch
names containing '#' in quotes for YAML safety and readability.

Comment on lines 25 to 27
implementation "com.google.cloud:google-cloud-resourcemanager"
implementation "com.google.cloud:google-cloud-compute"
implementation "com.google.cloud:google-cloud-logging"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

중복된 의존성을 제거해야 합니다.

다음 의존성들이 중복되어 있습니다:

  • google-cloud-compute: 22번 라인(버전 1.14.0)과 26번 라인(버전 없음)
  • google-cloud-logging: 27번 라인(버전 없음)과 47번 라인(버전 3.21.3)

중복 제거를 위한 수정:

 	implementation "com.google.cloud:google-cloud-resourcemanager"
-	implementation "com.google.cloud:google-cloud-compute"
-	implementation "com.google.cloud:google-cloud-logging"

기존 의존성들(22번, 47번 라인)이 이미 존재하므로 새로 추가된 중복 항목들을 제거하세요. BOM(26.26.0)이 버전을 관리하므로 google-cloud-resourcemanager만 추가하면 됩니다.

📝 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
implementation "com.google.cloud:google-cloud-resourcemanager"
implementation "com.google.cloud:google-cloud-compute"
implementation "com.google.cloud:google-cloud-logging"
implementation "com.google.cloud:google-cloud-resourcemanager"
🤖 Prompt for AI Agents
In build.gradle around lines 25 to 27, there are duplicate dependencies for
google-cloud-compute and google-cloud-logging already declared at lines 22 and
47 with versions. Remove the duplicate entries at lines 26 and 27, keeping only
the unique dependency google-cloud-resourcemanager here, since the BOM manages
versions for these libraries.

Comment on lines +3 to +5
public interface GcpProjectCommandService {
void insertNewGcpProject(String userId, String guildId, String projectId);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

인터페이스 설계가 깔끔하지만 반환 타입과 예외 처리를 고려해보세요.

현재 void 반환 타입은 작업 성공/실패 여부를 호출자가 알 수 없게 합니다. Discord 봇 서비스에서는 사용자에게 적절한 피드백을 제공해야 하므로, 작업 결과를 나타내는 반환 타입을 고려해보세요.

다음과 같은 개선을 제안합니다:

 public interface GcpProjectCommandService {
-    void insertNewGcpProject(String userId, String guildId, String projectId);
+    boolean insertNewGcpProject(String userId, String guildId, String projectId);
 }

또는 더 명확한 결과 타입:

public interface GcpProjectCommandService {
    ProjectRegistrationResult registerProject(String userId, String guildId, String projectId);
}
🤖 Prompt for AI Agents
In src/main/java/com/gcp/domain/gcp/service/GcpProjectCommandService.java around
lines 3 to 5, the interface method currently returns void, which does not allow
the caller to know if the operation succeeded or failed. Change the method to
return a result type that clearly indicates success or failure, such as a custom
ProjectRegistrationResult class. Also, consider adding appropriate exception
handling or declaring exceptions in the method signature to handle error cases
explicitly.

Comment on lines 19 to 21
DiscordUser discordUser = discordUserRepository.findByUserIdAndGuildId(userId, guildId).orElseThrow(
() -> new RuntimeException("이미 등록된 프로젝트 입니다.")
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

오류 메시지가 부정확하고 예외 처리 방식을 개선해야 합니다.

현재 사용자를 찾을 수 없을 때 "이미 등록된 프로젝트 입니다."라는 잘못된 메시지를 표시합니다. 또한 검색된 학습 내용에 따르면, 이 Discord 봇 서비스에서는 예외를 던지는 것보다 사용자 친화적인 메시지를 직접 반환하는 것을 선호합니다.

다음과 같이 개선하는 것을 권장합니다:

@Override
-public void insertNewGcpProject(String userId, String guildId, String projectId) {
-    DiscordUser discordUser = discordUserRepository.findByUserIdAndGuildId(userId, guildId).orElseThrow(
-            () -> new RuntimeException("이미 등록된 프로젝트 입니다.")
-    );
+public String insertNewGcpProject(String userId, String guildId, String projectId) {
+    Optional<DiscordUser> discordUserOpt = discordUserRepository.findByUserIdAndGuildId(userId, guildId);
+    if (discordUserOpt.isEmpty()) {
+        return "등록된 사용자를 찾을 수 없습니다. 먼저 /gcp init 명령어를 사용해주세요.";
+    }
+
+    DiscordUser discordUser = discordUserOpt.get();
    GcpProject gcpProject = GcpProject.create(projectId, discordUser);
    gcpProjectRepository.save(gcpProject);
+    return "프로젝트가 성공적으로 등록되었습니다.";
}
🤖 Prompt for AI Agents
In src/main/java/com/gcp/domain/gcp/service/GcpProjectCommandServiceImpl.java
around lines 19 to 21, the exception message "이미 등록된 프로젝트 입니다." is inaccurate
for the case when the user is not found, and throwing a RuntimeException is
discouraged. Instead, modify the code to check if the user exists and return a
user-friendly message directly without throwing an exception, ensuring the
message correctly reflects the situation of the user not being found.

Comment on lines 31 to 32
private static final String ZONE = "us-central1-f";
private static final String PROJECT_ID = "sincere-elixir-464606-j1";
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

하드코딩된 PROJECT_ID와 ZONE 상수 제거 필요

PR 목적에 따르면 하드코딩된 값을 동적 조회로 대체해야 하는데, 여전히 PROJECT_ID와 ZONE 상수가 여러 메서드에서 사용되고 있습니다. 새로 추가된 프로젝트 등록 및 zone 조회 기능을 활용하도록 수정이 필요합니다.


🏁 Script executed:

#!/bin/bash
# PROJECT_ID와 ZONE 상수를 사용하는 메서드 확인
echo "=== PROJECT_ID 사용 위치 ==="
rg "PROJECT_ID" src/main/java/com/gcp/domain/gcp/service/GcpService.java -A 1

echo -e "\n=== ZONE 사용 위치 ==="
rg "ZONE" src/main/java/com/gcp/domain/gcp/service/GcpService.java -A 1

Length of output: 1816


하드코딩된 PROJECT_ID 및 ZONE 상수 제거 및 동적 조회 적용 필요

PR 목표에 따라 PROJECT_IDZONE 상수를 직접 사용하는 부분을 모두 새로 추가된 동적 조회 기능으로 대체해야 합니다. 아래 위치를 점검하고, 프로젝트 등록 서비스 및 zone 조회 로직을 활용해 상수를 제거해 주세요.

• 상수 선언

  • src/main/java/com/gcp/domain/gcp/service/GcpService.java:
    private static final String PROJECT_ID = "...";
    private static final String ZONE = "...";

• VM 제어 메서드

  • startVM/stopVM: PROJECT_ID, ZONE, vmName 파라미터 전달
  • getInstanceId 호출 시 ZONE 하드코드

• Billing API 호출

  • URL: https://cloudbilling.googleapis.com/v1/projects/%s/billingInfo (PROJECT_ID 하드코드)

• Compute Engine 인스턴스 조회

  • URL: https://compute.googleapis.com/compute/v1/projects/%s/zones/%s/instances (PROJECT_ID, ZONE 하드코드)

• Machine Type 지정

  • String.format("zones/%s/machineTypes/%s", ZONE, machineType)

위 모든 부분에서 신규 등록된 프로젝트 조회 로직과 zone 조회 메서드를 사용하도록 리팩터링해주세요.

🤖 Prompt for AI Agents
In src/main/java/com/gcp/domain/gcp/service/GcpService.java around lines 31-32,
remove the hardcoded PROJECT_ID and ZONE constants and replace all their usages
with calls to the newly added dynamic project and zone retrieval methods.
Refactor startVM, stopVM, and getInstanceId methods to accept project ID and
zone dynamically instead of using constants. Update all API URLs and machine
type strings to use these dynamic values by invoking the project registration
service and zone lookup logic instead of hardcoded strings.

Comment on lines +205 to +224
public List<String> getProjectIds(String userId, String guildId) {
String url = "https://cloudresourcemanager.googleapis.com/v1/projects";
String accessToken = discordUserRepository.findAccessTokenByUserIdAndGuildId(userId, guildId).orElseThrow();

HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(accessToken);
headers.setContentType(MediaType.APPLICATION_JSON);

HttpEntity<String> entity = new HttpEntity<>(null, headers);
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);

JSONObject json = new JSONObject(response.getBody());
JSONArray projects = json.getJSONArray("projects");

List<String> projectIds = new ArrayList<>();
for (int i = 0; i < projects.length(); i++) {
projectIds.add(projects.getJSONObject(i).getString("projectId"));
}
return projectIds;
}
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

오류 처리 개선 필요

이전 학습 내용에 따르면, Discord 봇 서비스에서는 orElseThrow() 대신 Optional.isEmpty() 체크 후 사용자 친화적인 메시지 반환을 선호합니다. 또한 API 호출 실패나 JSON 파싱 오류에 대한 처리가 필요합니다.

 public List<String> getProjectIds(String userId, String guildId) {
-    String url = "https://cloudresourcemanager.googleapis.com/v1/projects";
-    String accessToken = discordUserRepository.findAccessTokenByUserIdAndGuildId(userId, guildId).orElseThrow();
-
-    HttpHeaders headers = new HttpHeaders();
-    headers.setBearerAuth(accessToken);
-    headers.setContentType(MediaType.APPLICATION_JSON);
-
-    HttpEntity<String> entity = new HttpEntity<>(null, headers);
-    ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
-
-    JSONObject json = new JSONObject(response.getBody());
-    JSONArray projects = json.getJSONArray("projects");
-
-    List<String> projectIds = new ArrayList<>();
-    for (int i = 0; i < projects.length(); i++) {
-        projectIds.add(projects.getJSONObject(i).getString("projectId"));
+    try {
+        String url = "https://cloudresourcemanager.googleapis.com/v1/projects";
+        Optional<String> accessTokenOpt = discordUserRepository.findAccessTokenByUserIdAndGuildId(userId, guildId);
+        if (accessTokenOpt.isEmpty()) {
+            log.warn("Access token not found for user: {} in guild: {}", userId, guildId);
+            return Collections.emptyList();
+        }
+        
+        String accessToken = accessTokenOpt.get();
+        HttpHeaders headers = new HttpHeaders();
+        headers.setBearerAuth(accessToken);
+        headers.setContentType(MediaType.APPLICATION_JSON);
+
+        HttpEntity<String> entity = new HttpEntity<>(null, headers);
+        ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
+
+        JSONObject json = new JSONObject(response.getBody());
+        JSONArray projects = json.optJSONArray("projects");
+        
+        if (projects == null) {
+            return Collections.emptyList();
+        }
+
+        List<String> projectIds = new ArrayList<>();
+        for (int i = 0; i < projects.length(); i++) {
+            projectIds.add(projects.getJSONObject(i).getString("projectId"));
+        }
+        return projectIds;
+    } catch (Exception e) {
+        log.error("Failed to get project IDs for user: {} in guild: {}", userId, guildId, e);
+        return Collections.emptyList();
     }
-    return projectIds;
 }

다음 스크립트로 다른 메서드들의 오류 처리 패턴을 확인해보겠습니다:


🏁 Script executed:

#!/bin/bash
# GcpService 클래스에서 orElseThrow() 사용 패턴 확인
rg -A 3 "orElseThrow\(\)" src/main/java/com/gcp/domain/gcp/service/GcpService.java

Length of output: 2369


GcpService.java – 토큰 조회 Optional.isEmpty() 적용 및 전체 메서드 오류 처리 보강

GcpService 클래스 내
discordUserRepository.findAccessTokenByUserIdAndGuildId(userId, guildId).orElseThrow()
호출부(총 7곳)에서 Optional.isEmpty() 체크 후 사용자 친화적 반환/경고 로직으로 변경해야 합니다. 또한 모든 HTTP 호출부(RestTemplate.exchange 등)에 try/catch를 적용해 예외 발생 시 빈 리스트 반환 또는 적절한 로그/메시지 처리로 보강하세요.

• getProjectIds (205–224)
• Compute 인스턴스 조회·조작 메서드
• RestTemplate 사용 메서드 일체

예시 수정안:

- String accessToken = discordUserRepository.findAccessTokenByUserIdAndGuildId(userId, guildId).orElseThrow();
+ Optional<String> accessTokenOpt = discordUserRepository.findAccessTokenByUserIdAndGuildId(userId, guildId);
+ if (accessTokenOpt.isEmpty()) {
+     log.warn("Access token 미발견: userId={}, guildId={}", userId, guildId);
+     return Collections.emptyList();
+ }
+ String accessToken = accessTokenOpt.get();

추가로 JSON 파싱 시 optJSONArray 사용 및 전체 API 호출부에 try/catch로 예외를 안전하게 처리해 주세요.

🤖 Prompt for AI Agents
In src/main/java/com/gcp/domain/gcp/service/GcpService.java lines 205 to 224,
update the getProjectIds method to first check if the Optional from
discordUserRepository.findAccessTokenByUserIdAndGuildId is empty using
isEmpty(), and handle this case by returning a user-friendly response or logging
a warning instead of directly calling orElseThrow. Wrap the entire HTTP call
using restTemplate.exchange and subsequent JSON parsing in a try/catch block to
catch exceptions, log errors appropriately, and return an empty list if any
exception occurs. Replace the use of getJSONArray with optJSONArray to safely
handle missing or null JSON arrays during parsing. Apply similar Optional checks
and try/catch exception handling to all other methods using RestTemplate calls
in this class.

Comment on lines 226 to 265
public List<ProjectZoneDto> getActiveInstanceZones(String userId, String guildId) {
String accessToken = discordUserRepository.findAccessTokenByUserIdAndGuildId(userId, guildId).orElseThrow();
DiscordUser discordUser = discordUserRepository.findByUserIdAndGuildId(userId, guildId).orElseThrow();
List<String> projectIds = gcpProjectRepository.findAllProjectIdsByDiscordUser(discordUser).orElseThrow();

List<ProjectZoneDto> activeZones = new ArrayList<>();

for (String projectId : projectIds) {
String url = String.format("https://compute.googleapis.com/compute/v1/projects/%s/aggregated/instances", projectId);

HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(accessToken);
HttpEntity<Void> entity = new HttpEntity<>(headers);

ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
JSONObject json = new JSONObject(response.getBody());
JSONObject items = json.getJSONObject("items");

List<String> zoneNames = new ArrayList<>();

for (String key : items.keySet()) {
JSONObject zoneInfo = items.getJSONObject(key);
if (zoneInfo.has("instances")) {
// 키 형식: "zones/us-central1-a" → "us-central1-a" 추출
if (key.startsWith("zones/")) {
String zoneName = key.substring("zones/".length());
zoneNames.add(zoneName);
}
}
}

ProjectZoneDto dto = ProjectZoneDto.builder()
.projectId(projectId)
.zoneList(zoneNames)
.build();
activeZones.add(dto);
}

return activeZones;
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

오류 처리 및 복원력 개선 필요

여러 프로젝트를 처리할 때 하나의 프로젝트에서 오류가 발생해도 다른 프로젝트 처리를 계속할 수 있도록 개선이 필요합니다. 또한 orElseThrow() 사용 대신 Optional 체크를 권장합니다.

 public List<ProjectZoneDto> getActiveInstanceZones(String userId, String guildId) {
-    String accessToken = discordUserRepository.findAccessTokenByUserIdAndGuildId(userId, guildId).orElseThrow();
-    DiscordUser discordUser = discordUserRepository.findByUserIdAndGuildId(userId, guildId).orElseThrow();
-    List<String> projectIds = gcpProjectRepository.findAllProjectIdsByDiscordUser(discordUser).orElseThrow();
+    Optional<String> accessTokenOpt = discordUserRepository.findAccessTokenByUserIdAndGuildId(userId, guildId);
+    if (accessTokenOpt.isEmpty()) {
+        log.warn("Access token not found for user: {} in guild: {}", userId, guildId);
+        return Collections.emptyList();
+    }
+    
+    Optional<DiscordUser> discordUserOpt = discordUserRepository.findByUserIdAndGuildId(userId, guildId);
+    if (discordUserOpt.isEmpty()) {
+        log.warn("Discord user not found: {} in guild: {}", userId, guildId);
+        return Collections.emptyList();
+    }
+    
+    List<String> projectIds = gcpProjectRepository.findAllProjectIdsByDiscordUser(discordUserOpt.get())
+        .orElse(Collections.emptyList());
+    
+    String accessToken = accessTokenOpt.get();

     List<ProjectZoneDto> activeZones = new ArrayList<>();

     for (String projectId : projectIds) {
-        String url = String.format("https://compute.googleapis.com/compute/v1/projects/%s/aggregated/instances", projectId);
-
-        HttpHeaders headers = new HttpHeaders();
-        headers.setBearerAuth(accessToken);
-        HttpEntity<Void> entity = new HttpEntity<>(headers);
-
-        ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
-        JSONObject json = new JSONObject(response.getBody());
-        JSONObject items = json.getJSONObject("items");
-
-        List<String> zoneNames = new ArrayList<>();
-
-        for (String key : items.keySet()) {
-            JSONObject zoneInfo = items.getJSONObject(key);
-            if (zoneInfo.has("instances")) {
-                // 키 형식: "zones/us-central1-a" → "us-central1-a" 추출
-                if (key.startsWith("zones/")) {
-                    String zoneName = key.substring("zones/".length());
-                    zoneNames.add(zoneName);
+        try {
+            String url = String.format("https://compute.googleapis.com/compute/v1/projects/%s/aggregated/instances", projectId);
+
+            HttpHeaders headers = new HttpHeaders();
+            headers.setBearerAuth(accessToken);
+            HttpEntity<Void> entity = new HttpEntity<>(headers);
+
+            ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
+            JSONObject json = new JSONObject(response.getBody());
+            JSONObject items = json.optJSONObject("items");
+            
+            if (items == null) {
+                continue;
+            }
+
+            List<String> zoneNames = new ArrayList<>();
+
+            for (String key : items.keySet()) {
+                JSONObject zoneInfo = items.getJSONObject(key);
+                if (zoneInfo.has("instances")) {
+                    // 키 형식: "zones/us-central1-a" → "us-central1-a" 추출
+                    if (key.startsWith("zones/")) {
+                        String zoneName = key.substring("zones/".length());
+                        zoneNames.add(zoneName);
+                    }
                 }
             }
-        }
 
-        ProjectZoneDto dto = ProjectZoneDto.builder()
-                .projectId(projectId)
-                .zoneList(zoneNames)
-                .build();
-        activeZones.add(dto);
+            ProjectZoneDto dto = ProjectZoneDto.builder()
+                    .projectId(projectId)
+                    .zoneList(zoneNames)
+                    .build();
+            activeZones.add(dto);
+        } catch (Exception e) {
+            log.error("Failed to get zones for project: {}", projectId, e);
+            // 오류가 발생한 프로젝트는 빈 zone 목록으로 추가
+            activeZones.add(ProjectZoneDto.builder()
+                    .projectId(projectId)
+                    .zoneList(Collections.emptyList())
+                    .build());
+        }
     }

     return activeZones;
 }
📝 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 List<ProjectZoneDto> getActiveInstanceZones(String userId, String guildId) {
String accessToken = discordUserRepository.findAccessTokenByUserIdAndGuildId(userId, guildId).orElseThrow();
DiscordUser discordUser = discordUserRepository.findByUserIdAndGuildId(userId, guildId).orElseThrow();
List<String> projectIds = gcpProjectRepository.findAllProjectIdsByDiscordUser(discordUser).orElseThrow();
List<ProjectZoneDto> activeZones = new ArrayList<>();
for (String projectId : projectIds) {
String url = String.format("https://compute.googleapis.com/compute/v1/projects/%s/aggregated/instances", projectId);
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(accessToken);
HttpEntity<Void> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
JSONObject json = new JSONObject(response.getBody());
JSONObject items = json.getJSONObject("items");
List<String> zoneNames = new ArrayList<>();
for (String key : items.keySet()) {
JSONObject zoneInfo = items.getJSONObject(key);
if (zoneInfo.has("instances")) {
// 키 형식: "zones/us-central1-a" → "us-central1-a" 추출
if (key.startsWith("zones/")) {
String zoneName = key.substring("zones/".length());
zoneNames.add(zoneName);
}
}
}
ProjectZoneDto dto = ProjectZoneDto.builder()
.projectId(projectId)
.zoneList(zoneNames)
.build();
activeZones.add(dto);
}
return activeZones;
}
public List<ProjectZoneDto> getActiveInstanceZones(String userId, String guildId) {
Optional<String> accessTokenOpt = discordUserRepository.findAccessTokenByUserIdAndGuildId(userId, guildId);
if (accessTokenOpt.isEmpty()) {
log.warn("Access token not found for user: {} in guild: {}", userId, guildId);
return Collections.emptyList();
}
Optional<DiscordUser> discordUserOpt = discordUserRepository.findByUserIdAndGuildId(userId, guildId);
if (discordUserOpt.isEmpty()) {
log.warn("Discord user not found: {} in guild: {}", userId, guildId);
return Collections.emptyList();
}
List<String> projectIds = gcpProjectRepository
.findAllProjectIdsByDiscordUser(discordUserOpt.get())
.orElse(Collections.emptyList());
String accessToken = accessTokenOpt.get();
List<ProjectZoneDto> activeZones = new ArrayList<>();
for (String projectId : projectIds) {
try {
String url = String.format(
"https://compute.googleapis.com/compute/v1/projects/%s/aggregated/instances",
projectId
);
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(accessToken);
HttpEntity<Void> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(
url, HttpMethod.GET, entity, String.class
);
JSONObject json = new JSONObject(response.getBody());
JSONObject items = json.optJSONObject("items");
if (items == null) {
continue;
}
List<String> zoneNames = new ArrayList<>();
for (String key : items.keySet()) {
JSONObject zoneInfo = items.getJSONObject(key);
if (zoneInfo.has("instances") && key.startsWith("zones/")) {
zoneNames.add(key.substring("zones/".length()));
}
}
activeZones.add(ProjectZoneDto.builder()
.projectId(projectId)
.zoneList(zoneNames)
.build()
);
} catch (Exception e) {
log.error("Failed to get zones for project: {}", projectId, e);
activeZones.add(ProjectZoneDto.builder()
.projectId(projectId)
.zoneList(Collections.emptyList())
.build()
);
}
}
return activeZones;
}
🤖 Prompt for AI Agents
In src/main/java/com/gcp/domain/gcp/service/GcpService.java around lines 226 to
265, the method getActiveInstanceZones currently uses orElseThrow() which throws
exceptions immediately and stops processing if any project or user data is
missing. To improve resilience, replace orElseThrow() with Optional checks and
handle missing data gracefully. Additionally, wrap the per-project processing in
try-catch blocks so that if an error occurs for one project, it logs the error
and continues processing the remaining projects instead of failing the entire
method.

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

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between c421654 and b9f71f1.

📒 Files selected for processing (5)
  • build.gradle (1 hunks)
  • src/main/java/com/gcp/domain/discord/service/GcpBotService.java (4 hunks)
  • src/main/java/com/gcp/domain/gcp/repository/GcpProjectRepository.java (1 hunks)
  • src/main/java/com/gcp/domain/gcp/service/GcpProjectCommandServiceImpl.java (1 hunks)
  • src/main/java/com/gcp/domain/gcp/service/GcpService.java (3 hunks)
✅ Files skipped from review due to trivial changes (1)
  • build.gradle
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/main/java/com/gcp/domain/gcp/service/GcpProjectCommandServiceImpl.java
  • src/main/java/com/gcp/domain/gcp/service/GcpService.java
  • src/main/java/com/gcp/domain/discord/service/GcpBotService.java
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: kamillcream
PR: Cloudible/GCP#8
File: src/main/java/com/gcp/domain/gcp/service/GcpService.java:38-38
Timestamp: 2025-08-01T19:45:32.212Z
Learning: Discord 봇 서비스에서는 orElseThrow()로 예외를 던지는 것보다 Optional.isEmpty() 체크 후 사용자 친화적인 메시지를 직접 반환하는 것을 선호함. 특히 GcpService.java에서 Google OAuth 토큰 조회 실패 시 예외 대신 안내 메시지 반환이 더 적절함.
🔇 Additional comments (3)
src/main/java/com/gcp/domain/gcp/repository/GcpProjectRepository.java (3)

7-7: Import 추가가 적절합니다.

@Param 어노테이션 import가 새로운 쿼리 메서드의 명명된 파라미터를 위해 올바르게 추가되었습니다.


10-10: Import 추가가 적절합니다.

List import가 새로운 메서드의 반환 타입을 위해 올바르게 추가되었습니다.


22-22: 존재 여부 확인 메서드가 올바르게 구현되었습니다.

existsByProjectIdAndDiscordUser 메서드는 Spring Data JPA의 네이밍 컨벤션을 따라 복합 키 기반 존재 여부 확인을 위해 적절하게 구현되었습니다.

Comment on lines +19 to +20
@Query("SELECT p.projectId FROM GcpProject p WHERE p.discordUser = :discordUser")
Optional<List<String>> findAllProjectIdsByDiscordUser(DiscordUser discordUser);
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

쿼리 메서드 구현을 검토해주세요.

findAllProjectIdsByDiscordUser 메서드에서 Optional<List<String>> 반환 타입이 적절한지 확인이 필요합니다. 일반적으로 컬렉션 반환 시에는 빈 리스트를 반환하는 것이 Optional보다 더 적절할 수 있습니다.

다음 스크립트로 이 메서드의 사용법을 확인하여 Optional 래핑이 필요한지 검증해보겠습니다:


🏁 Script executed:

#!/bin/bash
# Description: GcpProjectRepository의 findAllProjectIdsByDiscordUser 메서드 사용 패턴 확인

# 서비스 레이어에서 이 메서드를 어떻게 사용하는지 확인
rg -A 10 "findAllProjectIdsByDiscordUser"

Length of output: 1680


컬렉션 반환 시 Optional 제거 및 빈 리스트 처리 권장

findAllProjectIdsByDiscordUserOptional<List<String>>을 반환하도록 구현되어 있는데, JPA 쿼리 메서드는 기본적으로 결과가 없을 때 빈 컬렉션을 반환하므로 Optional로 래핑할 필요가 없습니다. 또한, 서비스 레이어에서 .orElseThrow()로 예외를 던지는 대신 빈 리스트 여부를 확인하고 사용자 친화적인 메시지를 반환하는 편이 이 프로젝트의 정책과도 일치합니다.

수정이 필요한 위치:

  • src/main/java/com/gcp/domain/gcp/repository/GcpProjectRepository.java
    • 메서드 시그니처를 Optional<List<String>>List<String> 으로 변경
  • src/main/java/com/gcp/domain/gcp/service/GcpService.java
    findAllProjectIdsByDiscordUser(...).orElseThrow() 호출을 findAllProjectIdsByDiscordUser(...)로 변경
    • 반환된 리스트가 비었을 때 사용자 안내 메시지를 직접 반환하도록 로직 추가

예시 diff:

--- a/src/main/java/com/gcp/domain/gcp/repository/GcpProjectRepository.java
+++ b/src/main/java/com/gcp/domain/gcp/repository/GcpProjectRepository.java
-    @Query("SELECT p.projectId FROM GcpProject p WHERE p.discordUser = :discordUser")
-    Optional<List<String>> findAllProjectIdsByDiscordUser(DiscordUser discordUser);
+    @Query("SELECT p.projectId FROM GcpProject p WHERE p.discordUser = :discordUser")
+    List<String> findAllProjectIdsByDiscordUser(DiscordUser discordUser);
--- a/src/main/java/com/gcp/domain/gcp/service/GcpService.java
+++ b/src/main/java/com/gcp/domain/gcp/service/GcpService.java
-        List<String> projectIds = gcpProjectRepository.findAllProjectIdsByDiscordUser(discordUser).orElseThrow();
+        List<String> projectIds = gcpProjectRepository.findAllProjectIdsByDiscordUser(discordUser);
+        if (projectIds.isEmpty()) {
+            return new ResponseEntity<>("등록된 프로젝트가 없습니다. 먼저 프로젝트를 등록해주세요.", HttpStatus.OK);
+        }

이렇게 변경하면 불필요한 Optional 래핑과 예외 던짐을 제거하고, 조회 결과가 없을 때 사용자에게 친숙한 안내를 제공할 수 있습니다.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/java/com/gcp/domain/gcp/repository/GcpProjectRepository.java around
lines 19-20, change the method signature of findAllProjectIdsByDiscordUser from
Optional<List<String>> to just List<String> because JPA returns an empty list
when no results are found, so Optional is unnecessary. Then, in
src/main/java/com/gcp/domain/gcp/service/GcpService.java, update calls to this
method by removing .orElseThrow() and instead add logic to check if the returned
list is empty and return a user-friendly message accordingly. This removes
redundant Optional wrapping and exception throwing while improving user feedback
on empty results.

@kamillcream kamillcream merged commit 35f5b84 into main Aug 6, 2025
3 checks passed
@kamillcream kamillcream deleted the feat/#11-get-projectid-zone branch August 6, 2025 05:45
@kamillcream kamillcream restored the feat/#11-get-projectid-zone branch August 6, 2025 05:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

in: api API 관련 priority: high 긴급하며 우선적으로 해결해야 하는 작업입니다. status: feedback provided 피드백이 반영되었거나 제공된 상태입니다. type: feature 새로운 기능을 추가하거나 구현하는 작업입니다.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] 사용자 액세스 토큰으로 참여중인 프로젝트 ID, VM이 속한 Zone 정보 조회

3 participants