From fc0b4ab08c94fe6db813820f55387f89add23efe Mon Sep 17 00:00:00 2001 From: Jinyoung Date: Tue, 5 Aug 2025 06:06:11 +0900 Subject: [PATCH 01/20] =?UTF-8?q?feat:=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/build.gradle b/build.gradle index ed6f3ae..78a6d34 100644 --- a/build.gradle +++ b/build.gradle @@ -22,6 +22,11 @@ dependencies { implementation 'com.google.cloud:google-cloud-compute:1.14.0' // Compute Engine API implementation 'com.google.cloud:google-cloud-billing' // GCP Billing API implementation 'com.google.cloud:google-cloud-monitoring' // Cloud Monitoring API + implementation "com.google.cloud:google-cloud-resourcemanager" + implementation "com.google.cloud:google-cloud-compute" + implementation "com.google.cloud:google-cloud-logging" + + implementation 'com.google.api:gax-httpjson:0.107.0' implementation 'com.google.auth:google-auth-library-oauth2-http' // GCP 인증 라이브러리 implementation 'org.springframework.boot:spring-boot-starter-web' // Spring Boot 웹 서버 From 1f499dcf10f9fc20887a90da72c6adb51b0a0ee9 Mon Sep 17 00:00:00 2001 From: Jinyoung Date: Tue, 5 Aug 2025 06:06:24 +0900 Subject: [PATCH 02/20] =?UTF-8?q?feat:=20=EC=A1=B0=ED=9A=8C=20=EB=AA=85?= =?UTF-8?q?=EB=A0=B9=EC=96=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/gcp/domain/discord/service/GcpBotService.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/com/gcp/domain/discord/service/GcpBotService.java b/src/main/java/com/gcp/domain/discord/service/GcpBotService.java index 79963d1..268f0cb 100644 --- a/src/main/java/com/gcp/domain/discord/service/GcpBotService.java +++ b/src/main/java/com/gcp/domain/discord/service/GcpBotService.java @@ -46,6 +46,10 @@ public void onSlashCommandInteraction(SlashCommandInteractionEvent event) { : userName + "님은 " + guildName + "에 이미 등록되어 있습니다."; event.reply(responseMsg).queue(); } + case "explore" -> { + List userProjectIds = gcpService.getProjectIds(userId, guildId); + event.reply(String.valueOf(userProjectIds)).queue();; + } case "register" -> { String userProfile = Optional.ofNullable(author.getAvatarUrl()) .orElse(author.getDefaultAvatarUrl()); From abb3beda9ba827e0c9caadaeca58de0b53bbce18 Mon Sep 17 00:00:00 2001 From: Jinyoung Date: Tue, 5 Aug 2025 06:06:33 +0900 Subject: [PATCH 03/20] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20id=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gcp/domain/gcp/service/GcpService.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/main/java/com/gcp/domain/gcp/service/GcpService.java b/src/main/java/com/gcp/domain/gcp/service/GcpService.java index 6581f7b..354626b 100644 --- a/src/main/java/com/gcp/domain/gcp/service/GcpService.java +++ b/src/main/java/com/gcp/domain/gcp/service/GcpService.java @@ -6,6 +6,8 @@ import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.json.JSONArray; +import org.json.JSONObject; import org.springframework.http.*; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; @@ -194,6 +196,29 @@ public List> getVmList(String userId, String guildId) { return parseVmResponse(response.getBody()); } + public List 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 entity = new HttpEntity<>(null, headers); + ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class); + + JSONObject json = new JSONObject(response.getBody()); + JSONArray projects = json.getJSONArray("projects"); + + List projectIds = new ArrayList<>(); + for (int i = 0; i < projects.length(); i++) { + projectIds.add(projects.getJSONObject(i).getString("projectId")); + } + return projectIds; + } + + + private static List> parseVmResponse(String json) throws IOException { List> vmList = new ArrayList<>(); ObjectMapper objectMapper = new ObjectMapper(); From 3c302120736c5b4ec95f537027b8dc33430cbcf2 Mon Sep 17 00:00:00 2001 From: Jinyoung Date: Tue, 5 Aug 2025 06:06:40 +0900 Subject: [PATCH 04/20] =?UTF-8?q?feat:=20=EB=B0=B0=ED=8F=AC=20=EB=B8=8C?= =?UTF-8?q?=EB=9E=9C=EC=B9=98=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1d81a01..4fec902 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,7 +2,7 @@ name: CI / CD on: push: - branches: [main] + branches: [feat/#11-get-projectid-zone] jobs: CI: From ab1b226c444fdb534c385b7c4244feddb9679c22 Mon Sep 17 00:00:00 2001 From: Jinyoung Date: Tue, 5 Aug 2025 06:11:45 +0900 Subject: [PATCH 05/20] =?UTF-8?q?feat:=20=EB=AA=85=EB=A0=B9=EC=96=B4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/gcp/global/config/DiscordBotConfig.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/gcp/global/config/DiscordBotConfig.java b/src/main/java/com/gcp/global/config/DiscordBotConfig.java index cc659f6..2de29a4 100644 --- a/src/main/java/com/gcp/global/config/DiscordBotConfig.java +++ b/src/main/java/com/gcp/global/config/DiscordBotConfig.java @@ -40,6 +40,7 @@ public JDA jda() throws Exception { .addSubcommands( new SubcommandData("init", "디스코드 유저 등록"), new SubcommandData("register", "Google 계정 연동"), + new SubcommandData("explore", "소속 프로젝트 ID 목록 조회"), new SubcommandData("start", "VM 시작") .addOption(OptionType.STRING, "vm_name", "시작할 VM 이름", true), new SubcommandData("stop", "VM 정지") From 773cb7e9d0b51b389f2f663213d1ee3ee80ababc Mon Sep 17 00:00:00 2001 From: Jinyoung Date: Wed, 6 Aug 2025 01:56:50 +0900 Subject: [PATCH 06/20] =?UTF-8?q?feat:=20Gcp=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EB=93=B1=EB=A1=9D=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gcp/service/GcpProjectCommandService.java | 5 ++++ .../service/GcpProjectCommandServiceImpl.java | 26 +++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 src/main/java/com/gcp/domain/gcp/service/GcpProjectCommandService.java create mode 100644 src/main/java/com/gcp/domain/gcp/service/GcpProjectCommandServiceImpl.java diff --git a/src/main/java/com/gcp/domain/gcp/service/GcpProjectCommandService.java b/src/main/java/com/gcp/domain/gcp/service/GcpProjectCommandService.java new file mode 100644 index 0000000..f472d03 --- /dev/null +++ b/src/main/java/com/gcp/domain/gcp/service/GcpProjectCommandService.java @@ -0,0 +1,5 @@ +package com.gcp.domain.gcp.service; + +public interface GcpProjectCommandService { + void insertNewGcpProject(String userId, String guildId, String projectId); +} diff --git a/src/main/java/com/gcp/domain/gcp/service/GcpProjectCommandServiceImpl.java b/src/main/java/com/gcp/domain/gcp/service/GcpProjectCommandServiceImpl.java new file mode 100644 index 0000000..969a680 --- /dev/null +++ b/src/main/java/com/gcp/domain/gcp/service/GcpProjectCommandServiceImpl.java @@ -0,0 +1,26 @@ +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("이미 등록된 프로젝트 입니다.") + ); + + GcpProject gcpProject = GcpProject.create(projectId, discordUser); + gcpProjectRepository.save(gcpProject); + } +} From e035d58ced183caa1674b206f71037895f27158f Mon Sep 17 00:00:00 2001 From: Jinyoung Date: Wed, 6 Aug 2025 01:57:32 +0900 Subject: [PATCH 07/20] =?UTF-8?q?feat:=20Gcp=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EB=93=B1=EB=A1=9D=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../oauth2/handler/OAuth2AuthenticationSuccessHandler.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/gcp/domain/oauth2/handler/OAuth2AuthenticationSuccessHandler.java b/src/main/java/com/gcp/domain/oauth2/handler/OAuth2AuthenticationSuccessHandler.java index 3e41be8..eef2360 100644 --- a/src/main/java/com/gcp/domain/oauth2/handler/OAuth2AuthenticationSuccessHandler.java +++ b/src/main/java/com/gcp/domain/oauth2/handler/OAuth2AuthenticationSuccessHandler.java @@ -78,8 +78,8 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo } DiscordUser discordUser = discordUserRepository.findByUserIdAndGuildId(userId, guildId) - .orElseThrow(() -> new IllegalStateException( - String.format("DiscordUser를 찾을 수 없습니다: userId=%s, guildId=%s", userId, guildId))); + .orElseThrow(() -> new IllegalStateException( + String.format("DiscordUser를 찾을 수 없습니다: userId=%s, guildId=%s", userId, guildId))); String googleAccessToken = client.getAccessToken().getTokenValue(); String googleRefreshToken = client.getRefreshToken().getTokenValue(); From 49def4e9feec33d9a5ffccaa94e10647f6a34f0e Mon Sep 17 00:00:00 2001 From: Jinyoung Date: Wed, 6 Aug 2025 01:57:57 +0900 Subject: [PATCH 08/20] =?UTF-8?q?feat:=20=EB=AA=85=EB=A0=B9=EC=96=B4=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EB=93=B1=EB=A1=9D=20=EB=AA=85=EB=A0=B9=EC=96=B4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/gcp/global/config/DiscordBotConfig.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/gcp/global/config/DiscordBotConfig.java b/src/main/java/com/gcp/global/config/DiscordBotConfig.java index 2de29a4..38ce560 100644 --- a/src/main/java/com/gcp/global/config/DiscordBotConfig.java +++ b/src/main/java/com/gcp/global/config/DiscordBotConfig.java @@ -39,8 +39,10 @@ public JDA jda() throws Exception { Commands.slash("gcp", "GCP 관련 명령어") .addSubcommands( new SubcommandData("init", "디스코드 유저 등록"), - new SubcommandData("register", "Google 계정 연동"), - new SubcommandData("explore", "소속 프로젝트 ID 목록 조회"), + new SubcommandData("login", "Google 계정 연동"), + new SubcommandData("project-list", "소속 프로젝트 ID 목록 조회"), + new SubcommandData("project-register", "프로젝트 ID를 서버에 등록") + .addOption(OptionType.STRING, "project_id", "등록하고자 하는 프로젝트 ID", true), new SubcommandData("start", "VM 시작") .addOption(OptionType.STRING, "vm_name", "시작할 VM 이름", true), new SubcommandData("stop", "VM 정지") From 7a805a08017c91f256fdd5b7557b1c5e7bd359f6 Mon Sep 17 00:00:00 2001 From: Jinyoung Date: Wed, 6 Aug 2025 01:58:24 +0900 Subject: [PATCH 09/20] =?UTF-8?q?fix:=20=EB=94=94=EC=8A=A4=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EB=93=B1=EB=A1=9D=20?= =?UTF-8?q?=EC=9C=A0=EB=AC=B4=20=ED=8C=90=EB=B3=84=20=EB=B0=A9=EC=8B=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gcp/domain/discord/repository/DiscordUserRepository.java | 5 +++++ .../com/gcp/domain/discord/service/DiscordUserService.java | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/gcp/domain/discord/repository/DiscordUserRepository.java b/src/main/java/com/gcp/domain/discord/repository/DiscordUserRepository.java index a4135c2..fa35fb1 100644 --- a/src/main/java/com/gcp/domain/discord/repository/DiscordUserRepository.java +++ b/src/main/java/com/gcp/domain/discord/repository/DiscordUserRepository.java @@ -16,6 +16,11 @@ public interface DiscordUserRepository extends JpaRepository Optional findByUserIdAndGuildId(@Param("userId") String userId, @Param("guildId") String guildId); + + boolean existsByUserIdAndGuildId(String userId, String guildId); + + + @Query("SELECT u FROM DiscordUser u WHERE u.googleAccessToken = :googleAccessToken") Optional findByGoogleAccessToken(@Param("googleAccessToken") String googleAccessToken); diff --git a/src/main/java/com/gcp/domain/discord/service/DiscordUserService.java b/src/main/java/com/gcp/domain/discord/service/DiscordUserService.java index 5018f52..46c3af0 100644 --- a/src/main/java/com/gcp/domain/discord/service/DiscordUserService.java +++ b/src/main/java/com/gcp/domain/discord/service/DiscordUserService.java @@ -10,9 +10,9 @@ public class DiscordUserService { private final DiscordUserRepository discordUserRepository; - // + public boolean insertDiscordUser(String userId, String userName, String guildId, String guildName){ - if (discordUserRepository.findByUserIdAndGuildId(userId, guildId).isEmpty()) { + if (discordUserRepository.existsByUserIdAndGuildId(userId, guildId)) { DiscordUser discordUser = new DiscordUser(userId, userName, guildId, guildName); discordUserRepository.save(discordUser); return true; From aacc7e8660e4abc5fdd01ff22e34230550182c7b Mon Sep 17 00:00:00 2001 From: Jinyoung Date: Wed, 6 Aug 2025 01:58:36 +0900 Subject: [PATCH 10/20] =?UTF-8?q?feat:=20=EB=AA=85=EB=A0=B9=EC=96=B4=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EB=93=B1=EB=A1=9D=20=EB=AA=85=EB=A0=B9=EC=96=B4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/discord/service/GcpBotService.java | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/gcp/domain/discord/service/GcpBotService.java b/src/main/java/com/gcp/domain/discord/service/GcpBotService.java index 268f0cb..eea4a19 100644 --- a/src/main/java/com/gcp/domain/discord/service/GcpBotService.java +++ b/src/main/java/com/gcp/domain/discord/service/GcpBotService.java @@ -1,5 +1,6 @@ package com.gcp.domain.discord.service; +import com.gcp.domain.gcp.service.GcpProjectCommandService; import com.gcp.domain.gcp.service.GcpService; import lombok.RequiredArgsConstructor; import net.dv8tion.jda.api.entities.Guild; @@ -21,6 +22,7 @@ public class GcpBotService extends ListenerAdapter { private final GcpService gcpService; private final DiscordUserService discordUserService; + private final GcpProjectCommandService gcpProjectCommandService; @Override public void onSlashCommandInteraction(SlashCommandInteractionEvent event) { @@ -46,11 +48,13 @@ public void onSlashCommandInteraction(SlashCommandInteractionEvent event) { : userName + "님은 " + guildName + "에 이미 등록되어 있습니다."; event.reply(responseMsg).queue(); } - case "explore" -> { + + case "project-list" -> { List userProjectIds = gcpService.getProjectIds(userId, guildId); event.reply(String.valueOf(userProjectIds)).queue();; } - case "register" -> { + + case "login" -> { String userProfile = Optional.ofNullable(author.getAvatarUrl()) .orElse(author.getDefaultAvatarUrl()); @@ -76,6 +80,17 @@ public void onSlashCommandInteraction(SlashCommandInteractionEvent event) { event.reply("👇 아래 링크를 클릭해서 Google 계정을 연결해주세요:\n" + redirectUri).queue(); } + + case "project-register" -> { + try{ + String projectId = getRequiredOption(event, "project_id"); + gcpProjectCommandService.insertNewGcpProject(userId, guildId, projectId); + event.reply("프로젝트가 등록되었습니다.").queue(); + } catch (RuntimeException e){ + event.reply(e.getMessage()).queue(); + } + } + case "start" -> { String vmName = getRequiredOption(event, "vm_name"); event.reply(gcpService.startVM(userId, guildId, vmName)).queue(); From 2e65fc6ad3c8704b70cdaf6588008d405e560bff Mon Sep 17 00:00:00 2001 From: Jinyoung Date: Wed, 6 Aug 2025 01:58:50 +0900 Subject: [PATCH 11/20] =?UTF-8?q?feat:=20=EC=83=9D=EC=84=B1=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/gcp/domain/gcp/entity/GcpProject.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/gcp/domain/gcp/entity/GcpProject.java b/src/main/java/com/gcp/domain/gcp/entity/GcpProject.java index 25cfdbd..465a985 100644 --- a/src/main/java/com/gcp/domain/gcp/entity/GcpProject.java +++ b/src/main/java/com/gcp/domain/gcp/entity/GcpProject.java @@ -2,16 +2,13 @@ import com.gcp.domain.discord.entity.DiscordUser; import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.*; @Entity @Table(name = "gcp_project") @Getter -@Setter +@Builder @NoArgsConstructor @AllArgsConstructor public class GcpProject { @@ -19,12 +16,15 @@ public class GcpProject { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String projectId; - private String zone; - - @Column(columnDefinition = "TEXT") - private String credentialsJson; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id") private DiscordUser discordUser; + + public static GcpProject create(String projectId, DiscordUser discordUser) { + return GcpProject.builder() + .projectId(projectId) + .discordUser(discordUser) + .build(); + } } From 5bdbd554bf4511a08d8c981dd8eca0e576bd7f15 Mon Sep 17 00:00:00 2001 From: Jinyoung Date: Wed, 6 Aug 2025 02:01:52 +0900 Subject: [PATCH 12/20] =?UTF-8?q?feat:=20setter=20=EB=B3=B5=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/gcp/domain/gcp/entity/GcpProject.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/gcp/domain/gcp/entity/GcpProject.java b/src/main/java/com/gcp/domain/gcp/entity/GcpProject.java index 465a985..09810b1 100644 --- a/src/main/java/com/gcp/domain/gcp/entity/GcpProject.java +++ b/src/main/java/com/gcp/domain/gcp/entity/GcpProject.java @@ -8,6 +8,7 @@ @Entity @Table(name = "gcp_project") @Getter +@Setter @Builder @NoArgsConstructor @AllArgsConstructor From 6c4126f6ac445322e669ca42aaff60309d493f98 Mon Sep 17 00:00:00 2001 From: Jinyoung Date: Wed, 6 Aug 2025 02:18:06 +0900 Subject: [PATCH 13/20] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=EC=99=80=20zone=20=EC=A0=95=EB=B3=B4=EB=A5=BC=20?= =?UTF-8?q?=EB=8B=B4=EC=9D=80=20Dto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/gcp/domain/gcp/dto/ProjectZoneDto.java | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/main/java/com/gcp/domain/gcp/dto/ProjectZoneDto.java diff --git a/src/main/java/com/gcp/domain/gcp/dto/ProjectZoneDto.java b/src/main/java/com/gcp/domain/gcp/dto/ProjectZoneDto.java new file mode 100644 index 0000000..44cb9a7 --- /dev/null +++ b/src/main/java/com/gcp/domain/gcp/dto/ProjectZoneDto.java @@ -0,0 +1,11 @@ +package com.gcp.domain.gcp.dto; + +import lombok.Builder; + +import java.util.List; + +@Builder +public record ProjectZoneDto( + String projectId, List zoneList +) { +} From 8bfc0fb09835c52f3defbecdb242b1eb4f7fffd1 Mon Sep 17 00:00:00 2001 From: Jinyoung Date: Wed, 6 Aug 2025 02:18:20 +0900 Subject: [PATCH 14/20] =?UTF-8?q?feat:=20zone=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/discord/service/GcpBotService.java | 10 +++++ .../gcp/repository/GcpProjectRepository.java | 5 +++ .../gcp/domain/gcp/service/GcpService.java | 40 +++++++++++++++++++ .../gcp/global/config/DiscordBotConfig.java | 1 + 4 files changed, 56 insertions(+) diff --git a/src/main/java/com/gcp/domain/discord/service/GcpBotService.java b/src/main/java/com/gcp/domain/discord/service/GcpBotService.java index eea4a19..01cd91f 100644 --- a/src/main/java/com/gcp/domain/discord/service/GcpBotService.java +++ b/src/main/java/com/gcp/domain/discord/service/GcpBotService.java @@ -1,5 +1,6 @@ package com.gcp.domain.discord.service; +import com.gcp.domain.gcp.dto.ProjectZoneDto; import com.gcp.domain.gcp.service.GcpProjectCommandService; import com.gcp.domain.gcp.service.GcpService; import lombok.RequiredArgsConstructor; @@ -91,6 +92,15 @@ public void onSlashCommandInteraction(SlashCommandInteractionEvent event) { } } + case "zone-list" -> { + try { + List result = gcpService.getZones(userId, guildId); + event.reply(result.toString()).queue();; + } catch (Exception e) { + event.reply(e.getMessage()).queue(); + } + } + case "start" -> { String vmName = getRequiredOption(event, "vm_name"); event.reply(gcpService.startVM(userId, guildId, vmName)).queue(); diff --git a/src/main/java/com/gcp/domain/gcp/repository/GcpProjectRepository.java b/src/main/java/com/gcp/domain/gcp/repository/GcpProjectRepository.java index 71b37e4..dd4565e 100644 --- a/src/main/java/com/gcp/domain/gcp/repository/GcpProjectRepository.java +++ b/src/main/java/com/gcp/domain/gcp/repository/GcpProjectRepository.java @@ -4,8 +4,10 @@ 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 @@ -13,4 +15,7 @@ public interface GcpProjectRepository extends JpaRepository { @Query("SELECT p FROM GcpProject p WHERE p.discordUser = :discordUser") Optional findByDiscordUser(DiscordUser discordUser); + + @Query("SELECT p.projectId FROM GcpProject p WHERE p.discordUser = :discordUser") + Optional> findAllProjectIdsByDiscordUser(DiscordUser discordUser); } diff --git a/src/main/java/com/gcp/domain/gcp/service/GcpService.java b/src/main/java/com/gcp/domain/gcp/service/GcpService.java index 354626b..c4089e2 100644 --- a/src/main/java/com/gcp/domain/gcp/service/GcpService.java +++ b/src/main/java/com/gcp/domain/gcp/service/GcpService.java @@ -2,10 +2,15 @@ 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.*; @@ -22,6 +27,7 @@ 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"; @@ -217,6 +223,40 @@ public List getProjectIds(String userId, String guildId) { return projectIds; } + public List getZones(String userId, String guildId){ + String accessToken = discordUserRepository.findAccessTokenByUserIdAndGuildId(userId, guildId).orElseThrow(); + DiscordUser discordUser = discordUserRepository.findByUserIdAndGuildId(userId, guildId).orElseThrow(); + + List projectIds = gcpProjectRepository.findAllProjectIdsByDiscordUser(discordUser).orElseThrow(); + List everyZones = new ArrayList<>(); + + projectIds.forEach( + projectId -> { + String url = String.format("https://compute.googleapis.com/compute/v1/projects/%s/zones", projectId); + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(accessToken); + + HttpEntity entity = new HttpEntity<>(headers); + ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class); + + JSONObject json = new JSONObject(response.getBody()); + JSONArray items = json.getJSONArray("items"); + + List zones = new ArrayList<>(); + for (int i = 0; i < items.length(); i++) { + zones.add(items.getJSONObject(i).getString("name")); + } + ProjectZoneDto projectZoneDto = ProjectZoneDto.builder() + .projectId(projectId) + .zoneList(zones) + .build(); + everyZones.add(projectZoneDto); + } + ); + + + return everyZones; + } private static List> parseVmResponse(String json) throws IOException { diff --git a/src/main/java/com/gcp/global/config/DiscordBotConfig.java b/src/main/java/com/gcp/global/config/DiscordBotConfig.java index 38ce560..1e44a68 100644 --- a/src/main/java/com/gcp/global/config/DiscordBotConfig.java +++ b/src/main/java/com/gcp/global/config/DiscordBotConfig.java @@ -43,6 +43,7 @@ public JDA jda() throws Exception { new SubcommandData("project-list", "소속 프로젝트 ID 목록 조회"), new SubcommandData("project-register", "프로젝트 ID를 서버에 등록") .addOption(OptionType.STRING, "project_id", "등록하고자 하는 프로젝트 ID", true), + new SubcommandData("zone-list", "프로젝트 내 VM Zone 목록 조회"), new SubcommandData("start", "VM 시작") .addOption(OptionType.STRING, "vm_name", "시작할 VM 이름", true), new SubcommandData("stop", "VM 정지") From c421654ad1e582fa73574d9ceb49a8444e33e314 Mon Sep 17 00:00:00 2001 From: Jinyoung Date: Wed, 6 Aug 2025 02:34:29 +0900 Subject: [PATCH 15/20] =?UTF-8?q?feat:=20=EB=A9=94=EC=8B=9C=EC=A7=80=20?= =?UTF-8?q?=ED=98=95=EC=8B=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/discord/service/GcpBotService.java | 20 ++++++-- .../gcp/domain/gcp/service/GcpService.java | 50 +++++++++++-------- 2 files changed, 45 insertions(+), 25 deletions(-) diff --git a/src/main/java/com/gcp/domain/discord/service/GcpBotService.java b/src/main/java/com/gcp/domain/discord/service/GcpBotService.java index 01cd91f..5399470 100644 --- a/src/main/java/com/gcp/domain/discord/service/GcpBotService.java +++ b/src/main/java/com/gcp/domain/discord/service/GcpBotService.java @@ -94,10 +94,24 @@ public void onSlashCommandInteraction(SlashCommandInteractionEvent event) { case "zone-list" -> { try { - List result = gcpService.getZones(userId, guildId); - event.reply(result.toString()).queue();; + List result = gcpService.getActiveInstanceZones(userId, guildId); + + StringBuilder message = new StringBuilder("📦 **프로젝트별 인스턴스 활성 ZONE 목록**\n\n"); + for (ProjectZoneDto dto : result) { + message.append("🔹 **") + .append(dto.projectId()) + .append("**\n"); + + for (String zone : dto.zoneList()) { + message.append("↳ ").append(zone).append("\n"); + } + + message.append("\n"); + } + + event.reply(message.toString()).queue(); } catch (Exception e) { - event.reply(e.getMessage()).queue(); + event.reply("❌ 오류 발생: " + e.getMessage()).queue(); } } diff --git a/src/main/java/com/gcp/domain/gcp/service/GcpService.java b/src/main/java/com/gcp/domain/gcp/service/GcpService.java index c4089e2..835523a 100644 --- a/src/main/java/com/gcp/domain/gcp/service/GcpService.java +++ b/src/main/java/com/gcp/domain/gcp/service/GcpService.java @@ -223,39 +223,45 @@ public List getProjectIds(String userId, String guildId) { return projectIds; } - public List getZones(String userId, String guildId){ + public List getActiveInstanceZones(String userId, String guildId) { String accessToken = discordUserRepository.findAccessTokenByUserIdAndGuildId(userId, guildId).orElseThrow(); DiscordUser discordUser = discordUserRepository.findByUserIdAndGuildId(userId, guildId).orElseThrow(); - List projectIds = gcpProjectRepository.findAllProjectIdsByDiscordUser(discordUser).orElseThrow(); - List everyZones = new ArrayList<>(); - projectIds.forEach( - projectId -> { - String url = String.format("https://compute.googleapis.com/compute/v1/projects/%s/zones", projectId); - HttpHeaders headers = new HttpHeaders(); - headers.setBearerAuth(accessToken); + List activeZones = new ArrayList<>(); + + for (String projectId : projectIds) { + String url = String.format("https://compute.googleapis.com/compute/v1/projects/%s/aggregated/instances", projectId); - HttpEntity entity = new HttpEntity<>(headers); - ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class); + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(accessToken); + HttpEntity entity = new HttpEntity<>(headers); - JSONObject json = new JSONObject(response.getBody()); - JSONArray items = json.getJSONArray("items"); + ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class); + JSONObject json = new JSONObject(response.getBody()); + JSONObject items = json.getJSONObject("items"); - List zones = new ArrayList<>(); - for (int i = 0; i < items.length(); i++) { - zones.add(items.getJSONObject(i).getString("name")); + List 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 projectZoneDto = ProjectZoneDto.builder() - .projectId(projectId) - .zoneList(zones) - .build(); - everyZones.add(projectZoneDto); } - ); + } + ProjectZoneDto dto = ProjectZoneDto.builder() + .projectId(projectId) + .zoneList(zoneNames) + .build(); + activeZones.add(dto); + } - return everyZones; + return activeZones; } From 7d7b9af0c43747b3bf178e65c52f5816bf155028 Mon Sep 17 00:00:00 2001 From: Jinyoung Date: Wed, 6 Aug 2025 14:39:52 +0900 Subject: [PATCH 16/20] =?UTF-8?q?refactor:=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 -- 1 file changed, 2 deletions(-) diff --git a/build.gradle b/build.gradle index 78a6d34..3431855 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,6 @@ dependencies { implementation 'com.google.cloud:google-cloud-billing' // GCP Billing API implementation 'com.google.cloud:google-cloud-monitoring' // Cloud Monitoring API implementation "com.google.cloud:google-cloud-resourcemanager" - implementation "com.google.cloud:google-cloud-compute" implementation "com.google.cloud:google-cloud-logging" @@ -44,7 +43,6 @@ dependencies { implementation 'org.apache.httpcomponents:httpclient:4.5.13' - implementation 'com.google.cloud:google-cloud-logging:3.21.3' implementation 'com.google.code.gson:gson' // JSON 처리 From 670c580bf9d62a52068c239bdec36f4885e113eb Mon Sep 17 00:00:00 2001 From: Jinyoung Date: Wed, 6 Aug 2025 14:40:17 +0900 Subject: [PATCH 17/20] =?UTF-8?q?fix:=20project-list=EC=9D=98=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EC=B6=9C=EB=A0=A5=20=ED=98=95=EC=8B=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/gcp/domain/discord/service/GcpBotService.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/gcp/domain/discord/service/GcpBotService.java b/src/main/java/com/gcp/domain/discord/service/GcpBotService.java index 5399470..f157f0f 100644 --- a/src/main/java/com/gcp/domain/discord/service/GcpBotService.java +++ b/src/main/java/com/gcp/domain/discord/service/GcpBotService.java @@ -17,6 +17,7 @@ import java.util.Base64; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -52,7 +53,15 @@ public void onSlashCommandInteraction(SlashCommandInteractionEvent event) { case "project-list" -> { List userProjectIds = gcpService.getProjectIds(userId, guildId); - event.reply(String.valueOf(userProjectIds)).queue();; + if (userProjectIds.isEmpty()) { + event.reply("📭 참여 중인 프로젝트가 없습니다.").queue(); + } else { + String message = "📦 **참여 중인 프로젝트 목록**\n" + + userProjectIds.stream() + .map(id -> "• " + id) + .collect(Collectors.joining("\n")); + event.reply(message).queue(); + } } case "login" -> { From 0d64cc6f21b621d70786e9b536f8c4321c47afe3 Mon Sep 17 00:00:00 2001 From: Jinyoung Date: Wed, 6 Aug 2025 14:40:39 +0900 Subject: [PATCH 18/20] =?UTF-8?q?feat:=20Gcp=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=A4=91=EB=B3=B5=20=ED=99=95=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gcp/service/GcpProjectCommandServiceImpl.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/gcp/domain/gcp/service/GcpProjectCommandServiceImpl.java b/src/main/java/com/gcp/domain/gcp/service/GcpProjectCommandServiceImpl.java index 969a680..c60ede9 100644 --- a/src/main/java/com/gcp/domain/gcp/service/GcpProjectCommandServiceImpl.java +++ b/src/main/java/com/gcp/domain/gcp/service/GcpProjectCommandServiceImpl.java @@ -17,10 +17,14 @@ public class GcpProjectCommandServiceImpl implements GcpProjectCommandService{ @Override public void insertNewGcpProject(String userId, String guildId, String projectId) { DiscordUser discordUser = discordUserRepository.findByUserIdAndGuildId(userId, guildId).orElseThrow( - () -> new RuntimeException("이미 등록된 프로젝트 입니다.") + () -> new RuntimeException("해당 사용자를 찾을 수 없습니다. /gcp init 명령어를 먼저 실행해주세요.") ); - GcpProject gcpProject = GcpProject.create(projectId, discordUser); - gcpProjectRepository.save(gcpProject); + if(!gcpProjectRepository.existsByProjectIdAndDiscordUser(projectId, discordUser)){ + GcpProject gcpProject = GcpProject.create(projectId, discordUser); + gcpProjectRepository.save(gcpProject); + } else{ + throw new RuntimeException("이미 등록된 프로젝트 입니다."); + } } } From b29e66d174c106d03dcd742386a4154d35ed7610 Mon Sep 17 00:00:00 2001 From: Jinyoung Date: Wed, 6 Aug 2025 14:40:43 +0900 Subject: [PATCH 19/20] =?UTF-8?q?feat:=20Gcp=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=A4=91=EB=B3=B5=20=ED=99=95=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/gcp/domain/gcp/repository/GcpProjectRepository.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/gcp/domain/gcp/repository/GcpProjectRepository.java b/src/main/java/com/gcp/domain/gcp/repository/GcpProjectRepository.java index dd4565e..e4b7000 100644 --- a/src/main/java/com/gcp/domain/gcp/repository/GcpProjectRepository.java +++ b/src/main/java/com/gcp/domain/gcp/repository/GcpProjectRepository.java @@ -18,4 +18,6 @@ public interface GcpProjectRepository extends JpaRepository { @Query("SELECT p.projectId FROM GcpProject p WHERE p.discordUser = :discordUser") Optional> findAllProjectIdsByDiscordUser(DiscordUser discordUser); + + boolean existsByProjectIdAndDiscordUser(String projectId, DiscordUser discordUser); } From b9f71f1c5289ec6901b212d3ad393e6d93f69447 Mon Sep 17 00:00:00 2001 From: Jinyoung Date: Wed, 6 Aug 2025 14:41:15 +0900 Subject: [PATCH 20/20] =?UTF-8?q?fix:=20try-catch=EB=A1=9C=20zone=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EB=8F=84=EC=A4=91=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EB=B0=9C=EC=83=9D=ED=95=B4=EB=8F=84=20=EC=9D=B4=EC=96=B4?= =?UTF-8?q?=EC=84=9C=20=EC=8B=A4=ED=96=89=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gcp/domain/gcp/service/GcpService.java | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/gcp/domain/gcp/service/GcpService.java b/src/main/java/com/gcp/domain/gcp/service/GcpService.java index 835523a..c6bf046 100644 --- a/src/main/java/com/gcp/domain/gcp/service/GcpService.java +++ b/src/main/java/com/gcp/domain/gcp/service/GcpService.java @@ -32,8 +32,6 @@ public class GcpService { private static final String PROJECT_ID = "sincere-elixir-464606-j1"; - - public String startVM(String userId, String guildId, String vmName) { try { String url = String.format( @@ -231,34 +229,36 @@ public List getActiveInstanceZones(String userId, String guildId List activeZones = new ArrayList<>(); for (String projectId : projectIds) { - String url = String.format("https://compute.googleapis.com/compute/v1/projects/%s/aggregated/instances", projectId); + try { + String url = String.format("https://compute.googleapis.com/compute/v1/projects/%s/aggregated/instances", projectId); - HttpHeaders headers = new HttpHeaders(); - headers.setBearerAuth(accessToken); - HttpEntity entity = new HttpEntity<>(headers); + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(accessToken); + HttpEntity entity = new HttpEntity<>(headers); - ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class); - JSONObject json = new JSONObject(response.getBody()); - JSONObject items = json.getJSONObject("items"); + ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class); + JSONObject json = new JSONObject(response.getBody()); + JSONObject items = json.getJSONObject("items"); - List zoneNames = new ArrayList<>(); + List 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/")) { + 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); + ProjectZoneDto dto = ProjectZoneDto.builder() + .projectId(projectId) + .zoneList(zoneNames) + .build(); + activeZones.add(dto); + + } catch (Exception e) { + log.warn("프로젝트 Zone 조회 실패 {}: {}", projectId, e.getMessage()); + } } return activeZones;