-
Notifications
You must be signed in to change notification settings - Fork 0
[feat] 사용자 액세스 토큰으로 참여중인 프로젝트 ID, VM이 속한 Zone 정보 조회 #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
fc0b4ab
1f499dc
abb3bed
3c30212
ab1b226
773cb7e
e035d58
49def4e
7a805a0
aacc7e8
2e65fc6
5bdbd55
6c4126f
8bfc0fb
c421654
7d7b9af
670c580
0d64cc6
b29e66d
b9f71f1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,7 +2,7 @@ name: CI / CD | |
|
|
||
| on: | ||
| push: | ||
| branches: [main] | ||
| branches: [feat/#11-get-projectid-zone] | ||
|
|
||
| jobs: | ||
| CI: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package com.gcp.domain.gcp.dto; | ||
|
|
||
| import lombok.Builder; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| @Builder | ||
| public record ProjectZoneDto( | ||
| String projectId, List<String> zoneList | ||
| ) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,13 +4,20 @@ | |
| import com.gcp.domain.gcp.entity.GcpProject; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
| import org.springframework.data.jpa.repository.Query; | ||
| import org.springframework.data.repository.query.Param; | ||
| import org.springframework.stereotype.Repository; | ||
|
|
||
| import java.util.List; | ||
| import java.util.Optional; | ||
|
|
||
| @Repository | ||
| public interface GcpProjectRepository extends JpaRepository<GcpProject, Long> { | ||
|
|
||
| @Query("SELECT p FROM GcpProject p WHERE p.discordUser = :discordUser") | ||
| Optional<GcpProject> findByDiscordUser(DiscordUser discordUser); | ||
|
|
||
| @Query("SELECT p.projectId FROM GcpProject p WHERE p.discordUser = :discordUser") | ||
| Optional<List<String>> findAllProjectIdsByDiscordUser(DiscordUser discordUser); | ||
|
Comment on lines
+19
to
+20
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chain쿼리 메서드 구현을 검토해주세요.
다음 스크립트로 이 메서드의 사용법을 확인하여 Optional 래핑이 필요한지 검증해보겠습니다: 🏁 Script executed: #!/bin/bash
# Description: GcpProjectRepository의 findAllProjectIdsByDiscordUser 메서드 사용 패턴 확인
# 서비스 레이어에서 이 메서드를 어떻게 사용하는지 확인
rg -A 10 "findAllProjectIdsByDiscordUser"Length of output: 1680 컬렉션 반환 시 Optional 제거 및 빈 리스트 처리 권장
수정이 필요한 위치:
예시 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 래핑과 예외 던짐을 제거하고, 조회 결과가 없을 때 사용자에게 친숙한 안내를 제공할 수 있습니다.
🤖 Prompt for AI Agents |
||
|
|
||
| boolean existsByProjectIdAndDiscordUser(String projectId, DiscordUser discordUser); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| package com.gcp.domain.gcp.service; | ||
|
|
||
| public interface GcpProjectCommandService { | ||
| void insertNewGcpProject(String userId, String guildId, String projectId); | ||
| } | ||
|
Comment on lines
+3
to
+5
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 인터페이스 설계가 깔끔하지만 반환 타입과 예외 처리를 고려해보세요. 현재 다음과 같은 개선을 제안합니다: 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 |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| package com.gcp.domain.gcp.service; | ||
|
|
||
| import com.gcp.domain.discord.entity.DiscordUser; | ||
| import com.gcp.domain.discord.repository.DiscordUserRepository; | ||
| import com.gcp.domain.gcp.entity.GcpProject; | ||
| import com.gcp.domain.gcp.repository.GcpProjectRepository; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class GcpProjectCommandServiceImpl implements GcpProjectCommandService{ | ||
|
|
||
| private final GcpProjectRepository gcpProjectRepository; | ||
| private final DiscordUserRepository discordUserRepository; | ||
|
|
||
| @Override | ||
| public void insertNewGcpProject(String userId, String guildId, String projectId) { | ||
| DiscordUser discordUser = discordUserRepository.findByUserIdAndGuildId(userId, guildId).orElseThrow( | ||
| () -> new RuntimeException("해당 사용자를 찾을 수 없습니다. /gcp init 명령어를 먼저 실행해주세요.") | ||
| ); | ||
|
|
||
| if(!gcpProjectRepository.existsByProjectIdAndDiscordUser(projectId, discordUser)){ | ||
| GcpProject gcpProject = GcpProject.create(projectId, discordUser); | ||
| gcpProjectRepository.save(gcpProject); | ||
| } else{ | ||
| throw new RuntimeException("이미 등록된 프로젝트 입니다."); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,10 +2,17 @@ | |
|
|
||
| import com.fasterxml.jackson.databind.JsonNode; | ||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import com.gcp.domain.discord.entity.DiscordUser; | ||
| import com.gcp.domain.discord.repository.DiscordUserRepository; | ||
| import com.gcp.domain.gcp.dto.ProjectZoneDto; | ||
| import com.gcp.domain.gcp.repository.GcpProjectRepository; | ||
| import com.google.cloud.compute.v1.Project; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.SneakyThrows; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.checkerframework.checker.units.qual.A; | ||
| import org.json.JSONArray; | ||
| import org.json.JSONObject; | ||
| import org.springframework.http.*; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.web.client.RestTemplate; | ||
|
|
@@ -20,12 +27,11 @@ public class GcpService { | |
|
|
||
| private final RestTemplate restTemplate = new RestTemplate(); | ||
| private final DiscordUserRepository discordUserRepository; | ||
| private final GcpProjectRepository gcpProjectRepository; | ||
| private static final String ZONE = "us-central1-f"; | ||
| private static final String PROJECT_ID = "sincere-elixir-464606-j1"; | ||
|
Comment on lines
31
to
32
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 1Length of output: 1816 하드코딩된 PROJECT_ID 및 ZONE 상수 제거 및 동적 조회 적용 필요 PR 목표에 따라 • 상수 선언
• VM 제어 메서드
• Billing API 호출
• Compute Engine 인스턴스 조회
• Machine Type 지정
위 모든 부분에서 신규 등록된 프로젝트 조회 로직과 zone 조회 메서드를 사용하도록 리팩터링해주세요. 🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
|
|
||
|
|
||
| public String startVM(String userId, String guildId, String vmName) { | ||
| try { | ||
| String url = String.format( | ||
|
|
@@ -194,6 +200,71 @@ public List<Map<String, String>> getVmList(String userId, String guildId) { | |
| return parseVmResponse(response.getBody()); | ||
| } | ||
|
|
||
| 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; | ||
| } | ||
|
Comment on lines
+203
to
+222
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chain오류 처리 개선 필요 이전 학습 내용에 따르면, Discord 봇 서비스에서는 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.javaLength of output: 2369 GcpService.java – 토큰 조회 Optional.isEmpty() 적용 및 전체 메서드 오류 처리 보강 GcpService 클래스 내 • getProjectIds (205–224) 예시 수정안: - 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 파싱 시 🤖 Prompt for AI Agents |
||
|
|
||
| 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) { | ||
| 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.getJSONObject("items"); | ||
|
|
||
| List<String> zoneNames = new ArrayList<>(); | ||
|
|
||
| for (String key : items.keySet()) { | ||
| JSONObject zoneInfo = items.getJSONObject(key); | ||
| if (zoneInfo.has("instances") && key.startsWith("zones/")) { | ||
| String zoneName = key.substring("zones/".length()); | ||
| zoneNames.add(zoneName); | ||
| } | ||
| } | ||
|
|
||
| ProjectZoneDto dto = ProjectZoneDto.builder() | ||
| .projectId(projectId) | ||
| .zoneList(zoneNames) | ||
| .build(); | ||
| activeZones.add(dto); | ||
|
|
||
| } catch (Exception e) { | ||
| log.warn("프로젝트 Zone 조회 실패 {}: {}", projectId, e.getMessage()); | ||
| } | ||
| } | ||
|
|
||
| return activeZones; | ||
| } | ||
|
|
||
|
|
||
| private static List<Map<String, String>> parseVmResponse(String json) throws IOException { | ||
| List<Map<String, String>> vmList = new ArrayList<>(); | ||
| ObjectMapper objectMapper = new ObjectMapper(); | ||
|
|
||
There was a problem hiding this comment.
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 파이프라인이 전체적으로 실행-되어 서버에 바로 배포까지 이뤄집니다.#문자가 포함돼 있어 YAML 파서가 오해하지는 않지만, 가독성과 안전성을 위해 따옴표로 감싸는 편이 일반적입니다.예시 수정안:
의도적으로 임시 테스트 서버에만 배포하도록 설계한 것인지, 아니면 실서버 배포 워크플로우를 따로 두고 싶은 것인지 확인 부탁드립니다.
🏁 Script executed:
Length of output: 353
🏁 Script executed:
Length of output: 3368
프로덕션 배포 워크플로우 트리거 설정 검토 및 수정 필요
.github/workflows/deploy.yml의on.push.branches가feat/#11-get-projectid-zone한정으로 설정되어 있어 해당 브랜치에 푸시될 때마다 CI·CD(빌드 → SSH 배포) 전체가 실행됩니다.이로 인해 다음과 같은 위험이 있습니다.
또한, 브랜치명에
#문자가 포함돼 있을 경우 YAML 가독성과 안전성을 위해 다음과 같이 따옴표로 감싸는 것을 권장드립니다.수정 예시:
▸ 의도하신 바가
인지 확인 후,
main/release/*전용으로 분리두 가지 중 하나로 설정을 명확히 분리해 주시기 바랍니다.
🤖 Prompt for AI Agents