From 05c0aff457161b14e53f5c2f02255b06fbdfe9a1 Mon Sep 17 00:00:00 2001 From: Shin-Yu-1 Date: Sun, 3 Aug 2025 11:15:42 +0900 Subject: [PATCH 1/4] =?UTF-8?q?fix(controller):=20=EB=B8=8C=EB=9D=BC?= =?UTF-8?q?=EC=9A=B0=EC=A0=80=EC=97=90=EC=84=9C=20=EB=9D=84=EC=9A=B8=20pos?= =?UTF-8?q?tMessage()=EC=97=90=20url=EC=9D=B4=20localhost=EB=A1=9C=20?= =?UTF-8?q?=EB=90=98=EC=96=B4=EC=9E=88=EB=8A=94=20=EA=B2=83=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8=ED=95=98=EC=97=AC=20=EC=88=98=EC=A0=95=ED=95=A8.=20|?= =?UTF-8?q?=20DP-208?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../deepwebide_be/member/controller/OAuthController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/deepdirect/deepwebide_be/member/controller/OAuthController.java b/src/main/java/com/deepdirect/deepwebide_be/member/controller/OAuthController.java index 5e1c02a0..fb991ced 100644 --- a/src/main/java/com/deepdirect/deepwebide_be/member/controller/OAuthController.java +++ b/src/main/java/com/deepdirect/deepwebide_be/member/controller/OAuthController.java @@ -47,7 +47,7 @@ private void sendSuccessResponse(HttpServletResponse response, SignInResponse da window.opener.postMessage({ type: 'GITHUB_LOGIN_SUCCESS', response: %s - }, 'http://localhost:5173'); + }, 'https://www.deepdirect.site'); window.close(); } @@ -70,7 +70,7 @@ private void sendErrorResponse(HttpServletResponse response, String errorMessage window.opener.postMessage({ type: 'GITHUB_LOGIN_ERROR', error: '%s' - }, 'http://localhost:5173'); + }, 'https://www.deepdirect.site'); window.close(); } From b7b69f74703739f51d6cde3a1f41ff654068e5e4 Mon Sep 17 00:00:00 2001 From: hyejin kwon <97959501+sunsetkk@users.noreply.github.com> Date: Sun, 3 Aug 2025 14:05:15 +0900 Subject: [PATCH 2/4] Update README.md --- README.md | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a6a9b181..141031e9 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,66 @@ -DeepWebIDE -api.deep +# Sprinf Boot + Java 17 + Gradle +This project provides a bakend implementation for DeepDirect using Spring Boot, JWT security, WebSocket (STOMP), Redis pub/sub, and MySQL. + +## Tech Stack +- Language : Java 17 +- Library & Framework : Spring Boot 3.4.7, Spring Security, Spring Web, Spring WebSocket, Spring Data JPA, Spring Mail +- Build Tool : Gradle +- Database : MySQL +- ORM : JPA(Hibernate) +- Cache / Message Broker : Redis(Lettuce, pub/sub) +- Authentication : JWT, OAuth2 (GitHub), Spring Security +- Dev Tools : Lombok, Swagger, Actuator +- Message Protocol : WebSocket + STOMP +- Third-Party API : Coolsms API +- Monitoring : Sentry +- Cloud & Storage : AWS EC2, S3, Route53 +- Deploy : Docker + Nginx, GitHub Action (CI/CD) + +--- + +## Environment Variables (.env) +These environment variables are required for local development and production deployments. +Sensitive values should never be exposed publicly--make sure they are securely managed through a `.env` file or a secure environment configuration system. + + +```env +# ✅ Redis +SPRING_REDIS_HOST=localhost +SPRING_REDIS_PORT=6379 +SPRING_REDIS_PASSWORD=your_redis_password + +# ✅ JWT +JWT_SECRET=dEVzVDFAM1NlQ3JFdCEyI2tFeTQzMjE= +JWT_ACCESS_TOKEN_EXPIRATION=86400000 # 24 hours (ms) +JWT_REFRESH_TOKEN_EXPIRATION=1209600000 # 14 days (ms) + +# ✅ Gmail SMTP +SPRING_MAIL_USERNAME=your_email@gmail.com +SPRING_MAIL_PASSWORD=your_gmail_app_password + +# ✅ Coolsms API +COOLSMS_KEY=your_coolsms_key +COOLSMS_SECRET=your_coolsms_secret +COOLSMS_NUMBER=010xxxxxxxx + +# ✅ AWS +AWS_ACCESS_KEY=your_aws_access_key +AWS_SECRET_KEY=your_aws_secret_key +AWS_REGION=ap-northeast-2 +AWS_S3_BUCKET=my-app-bucket + +# ✅ GitHub OAuth +GIT_CLIENT_ID=your_github_client_id +GIT_CLIENT_SECRET=your_github_client_secret + +# ✅ Sentry (Monitoring) +SENTRY_DSN=https://xxxxxxx.ingest.us.sentry.io/xxxxxxxxxx +SENTRY_ENVIRONMENT=development +SENTRY_RELEASE=0 +SENTRY_TRACES_SAMPLE_RATE=0.1 +SENTRY_DEBUG=false + +# ✅ Sandbox API +SANDBOX_URL=http://localhost:9090 +``` + From b8f77a9b13ede25f52b968152b68a323f9f31f13 Mon Sep 17 00:00:00 2001 From: hyejin kwon <97959501+sunsetkk@users.noreply.github.com> Date: Sun, 3 Aug 2025 14:06:21 +0900 Subject: [PATCH 3/4] Update README.md --- README.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 141031e9..acf323b0 100644 --- a/README.md +++ b/README.md @@ -2,19 +2,19 @@ This project provides a bakend implementation for DeepDirect using Spring Boot, JWT security, WebSocket (STOMP), Redis pub/sub, and MySQL. ## Tech Stack -- Language : Java 17 -- Library & Framework : Spring Boot 3.4.7, Spring Security, Spring Web, Spring WebSocket, Spring Data JPA, Spring Mail -- Build Tool : Gradle -- Database : MySQL -- ORM : JPA(Hibernate) -- Cache / Message Broker : Redis(Lettuce, pub/sub) -- Authentication : JWT, OAuth2 (GitHub), Spring Security -- Dev Tools : Lombok, Swagger, Actuator -- Message Protocol : WebSocket + STOMP -- Third-Party API : Coolsms API -- Monitoring : Sentry -- Cloud & Storage : AWS EC2, S3, Route53 -- Deploy : Docker + Nginx, GitHub Action (CI/CD) +- **Language** : Java 17 +- **Library & Framework** : Spring Boot 3.4.7, Spring Security, Spring Web, Spring WebSocket, Spring Data JPA, Spring Mail +- **Build Tool** : Gradle +- **Database** : MySQL +- **ORM** : JPA(Hibernate) +- **Cache** / Message Broker : Redis(Lettuce, pub/sub) +- **Authentication** : JWT, OAuth2 (GitHub), Spring Security +- **Dev Tools** : Lombok, Swagger, Actuator +- **Message Protocol** : WebSocket + STOMP +- **Third-Party API** : Coolsms API +- **Monitoring** : Sentry +- **Cloud & Storage** : AWS EC2, S3, Route53 +- **Deploy** : Docker + Nginx, GitHub Action (CI/CD) --- From 18a8b84e2e60c38872b520af19b5bd1291ffa382 Mon Sep 17 00:00:00 2001 From: WON YONG CHONG <108574909+projectmiluju@users.noreply.github.com> Date: Mon, 4 Aug 2025 02:14:55 +0900 Subject: [PATCH 4/4] Revert "Feature/dp 200 run" --- .../controller/RepositoryRunController.java | 3 + .../repository/domain/PortRegistry.java | 14 ++- .../repository/PortRegistryRepository.java | 5 - .../RunningContainerRepository.java | 3 - .../service/RepositoryRunService.java | 104 +++++------------- 5 files changed, 40 insertions(+), 89 deletions(-) diff --git a/src/main/java/com/deepdirect/deepwebide_be/repository/controller/RepositoryRunController.java b/src/main/java/com/deepdirect/deepwebide_be/repository/controller/RepositoryRunController.java index b761ba45..7f27f54e 100644 --- a/src/main/java/com/deepdirect/deepwebide_be/repository/controller/RepositoryRunController.java +++ b/src/main/java/com/deepdirect/deepwebide_be/repository/controller/RepositoryRunController.java @@ -1,6 +1,8 @@ package com.deepdirect.deepwebide_be.repository.controller; import com.deepdirect.deepwebide_be.global.dto.ApiResponseDto; +import com.deepdirect.deepwebide_be.global.exception.ErrorCode; +import com.deepdirect.deepwebide_be.global.exception.GlobalException; import com.deepdirect.deepwebide_be.global.security.CustomUserDetails; import com.deepdirect.deepwebide_be.repository.dto.response.RepositoryExecuteResponse; import com.deepdirect.deepwebide_be.repository.dto.response.RepositoryStatusResponse; @@ -15,6 +17,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import java.util.Map; diff --git a/src/main/java/com/deepdirect/deepwebide_be/repository/domain/PortRegistry.java b/src/main/java/com/deepdirect/deepwebide_be/repository/domain/PortRegistry.java index 64815840..807b07e3 100644 --- a/src/main/java/com/deepdirect/deepwebide_be/repository/domain/PortRegistry.java +++ b/src/main/java/com/deepdirect/deepwebide_be/repository/domain/PortRegistry.java @@ -1,9 +1,7 @@ package com.deepdirect.deepwebide_be.repository.domain; import jakarta.persistence.*; -import lombok.Getter; -@Getter @Entity @Table(name = "port_registry") public class PortRegistry { @@ -19,7 +17,13 @@ public class PortRegistry { @OneToOne private Repository repository; - public PortRegistry() {} // JPA 기본 생성자 +// public void setStatus(PortStatus status) { +// this.status = status; +// } +// +// public void setRepository(Repository repository) { +// this.repository = repository; +// } public void assignToRepository(Repository repository) { this.status = PortStatus.IN_USE; @@ -31,4 +35,8 @@ public void release() { this.repository = null; } + public Integer getPort() { + return this.port; + } } + diff --git a/src/main/java/com/deepdirect/deepwebide_be/repository/repository/PortRegistryRepository.java b/src/main/java/com/deepdirect/deepwebide_be/repository/repository/PortRegistryRepository.java index 726373fb..f2b8c769 100644 --- a/src/main/java/com/deepdirect/deepwebide_be/repository/repository/PortRegistryRepository.java +++ b/src/main/java/com/deepdirect/deepwebide_be/repository/repository/PortRegistryRepository.java @@ -5,7 +5,6 @@ import com.deepdirect.deepwebide_be.repository.domain.Repository; import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; import java.util.Optional; public interface PortRegistryRepository extends JpaRepository { @@ -14,8 +13,4 @@ public interface PortRegistryRepository extends JpaRepository findByRepository(Repository repository); Optional findFirstByStatus(PortStatus status); - - Optional findByPort(Integer port); - List findAllByStatus(PortStatus status); - } diff --git a/src/main/java/com/deepdirect/deepwebide_be/repository/repository/RunningContainerRepository.java b/src/main/java/com/deepdirect/deepwebide_be/repository/repository/RunningContainerRepository.java index ee0d3748..3705099a 100644 --- a/src/main/java/com/deepdirect/deepwebide_be/repository/repository/RunningContainerRepository.java +++ b/src/main/java/com/deepdirect/deepwebide_be/repository/repository/RunningContainerRepository.java @@ -29,7 +29,4 @@ public interface RunningContainerRepository extends JpaRepository findAllByStatusAndCreatedAtBefore(String status, LocalDateTime dateTime); - } diff --git a/src/main/java/com/deepdirect/deepwebide_be/repository/service/RepositoryRunService.java b/src/main/java/com/deepdirect/deepwebide_be/repository/service/RepositoryRunService.java index a70e4d22..268811fe 100644 --- a/src/main/java/com/deepdirect/deepwebide_be/repository/service/RepositoryRunService.java +++ b/src/main/java/com/deepdirect/deepwebide_be/repository/service/RepositoryRunService.java @@ -20,18 +20,18 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FileUtils; import org.springframework.beans.factory.annotation.Value; -import org.springframework.scheduling.annotation.Async; -import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.http.HttpMethod; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.mock.web.MockMultipartFile; import org.springframework.web.client.RestTemplate; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; + import java.io.*; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.time.LocalDateTime; import java.util.*; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -53,26 +53,33 @@ public class RepositoryRunService { @Value("${sandbox.api.base-url}") private String sandboxBaseUrl; - // === 컨테이너 실행 === @Transactional public RepositoryExecuteResponse executeRepository(Long repositoryId, Long userId) { log.info("Starting repository execution - repositoryId: {}, userId: {}", repositoryId, userId); File zipFile = null; try { + // 1. 기존 실행 중인 컨테이너 중지 요청 stopExistingContainer(repositoryId); + // 2. 권한 체크 & 레포지토리 조회 Repository repo = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId) .orElseThrow(() -> new GlobalException(ErrorCode.REPOSITORY_NOT_FOUND)); + // 3. framework, port 등 정보 추출 String framework = convertTypeToFramework(repo.getRepositoryType()); - Integer port = allocateOrGetPort(repo); // 랜덤 할당 + Integer port = allocateOrGetPort(repo); + // 4. 새로운 UUID 생성 String uuid = UUID.randomUUID().toString(); + // 5. 파일트리를 zip으로 변환 zipFile = fileTreeToZip(repositoryId, uuid); + + // 6. S3 업로드 String s3Url = uploadToS3(zipFile, uuid); + // 7. 샌드박스 실행 요청 SandboxExecutionRequest request = SandboxExecutionRequest.builder() .uuid(uuid) .url(s3Url) @@ -82,11 +89,9 @@ public RepositoryExecuteResponse executeRepository(Long repositoryId, Long userI SandboxExecutionResponse result = sandboxService.requestExecution(request); + // 8. 실행 중인 컨테이너 정보 저장 saveRunningContainer(repositoryId, uuid, "sandbox-" + uuid, port, framework, s3Url); - // **컨테이너 10분 후 자동 만료 비동기 스케줄** - scheduleAutoStopAndRelease(uuid, port); - log.info("Repository execution completed - repositoryId: {}, uuid: {}, port: {}", repositoryId, uuid, port); return RepositoryExecuteResponse.builder() @@ -112,46 +117,6 @@ public RepositoryExecuteResponse executeRepository(Long repositoryId, Long userI } } - // === 컨테이너 만료 비동기 스케줄 === - @Async - public void scheduleAutoStopAndRelease(String uuid, Integer port) { - try { - Thread.sleep(600_000); // 10분 대기 - - boolean stopped = sandboxService.stopContainer(uuid); - if (stopped) { - RunningContainer container = runningContainerRepository.findByUuid(uuid).orElse(null); - if (container != null) { - container.stop(); - runningContainerRepository.save(container); - - PortRegistry portReg = portRegistryRepository.findByPort(port).orElse(null); - if (portReg != null) { - portReg.release(); - portRegistryRepository.save(portReg); - } - } - log.info("컨테이너 {}가 만료되어 중지 및 포트 {} 반납 완료", uuid, port); - } - } catch (Exception e) { - log.error("컨테이너 만료 자동 중지 실패 - uuid: {}", uuid, e); - } - } - - // === 만료 컨테이너 1분마다 백업성 스케줄 === - @Scheduled(fixedDelay = 60_000) - public void autoCleanupExpiredContainers() { - LocalDateTime now = LocalDateTime.now(); - List expired = runningContainerRepository - .findAllByStatusAndCreatedAtBefore("RUNNING", now.minusMinutes(10)); - for (RunningContainer container : expired) { - log.info("자동정리: 10분 초과 컨테이너 발견 {}", container.getUuid()); - stopRepository(container.getRepositoryId(), null); - } - } - - - /** * 기존 실행 중인 컨테이너 중지 */ @@ -193,7 +158,8 @@ public void saveRunningContainer(Long repositoryId, String uuid, String containe Integer port, String framework, String s3Url) { try { RunningContainer container = RunningContainer.builder() - .repositoryId(repositoryId).uuid(uuid) + .repositoryId(repositoryId) + .uuid(uuid) .containerName(containerName) .port(port) .status("RUNNING") @@ -217,16 +183,9 @@ public void saveRunningContainer(Long repositoryId, String uuid, String containe @Transactional public boolean stopRepository(Long repositoryId, Long userId) { try { - // userId가 null이 아니면 권한 체크, null이면 skip - if (userId != null) { - repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId) - .orElseThrow(() -> new GlobalException(ErrorCode.REPOSITORY_NOT_FOUND)); - } else { - // 그냥 존재하는지 체크 (없으면 그냥 진행) - if (!repositoryRepository.findById(repositoryId).isPresent()) { - log.warn("stopRepository: repositoryId={} not found, but will cleanup container/port anyway.", repositoryId); - } - } + // 권한 체크 + repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId) + .orElseThrow(() -> new GlobalException(ErrorCode.REPOSITORY_NOT_FOUND)); Optional containerOpt = runningContainerRepository.findByRepositoryId(repositoryId); @@ -243,18 +202,10 @@ public boolean stopRepository(Long repositoryId, Long userId) { if (success) { container.stop(); runningContainerRepository.save(container); - - // 포트 반납도 여기서! - PortRegistry portReg = portRegistryRepository.findByPort(container.getPort()).orElse(null); - if (portReg != null) { - portReg.release(); - portRegistryRepository.save(portReg); - } - log.info("Successfully stopped repository: {} (uuid: {})", repositoryId, container.getUuid()); } - return true; + return success; } catch (Exception e) { log.error("Failed to stop repository: {}", repositoryId, e); @@ -313,7 +264,6 @@ private String convertTypeToFramework(RepositoryType type) { }; } - // === 랜덤 포트 할당 방식으로 수정 === private Integer allocateOrGetPort(Repository repo) { return portRegistryRepository.findByRepository(repo) .map(PortRegistry::getPort) @@ -323,18 +273,16 @@ private Integer allocateOrGetPort(Repository repo) { @Transactional public PortRegistry allocateNewPortForRepository(Repository repo) { log.debug("Allocating new port for repository: {}", repo.getId()); - List availablePorts = portRegistryRepository.findAllByStatus(PortStatus.AVAILABLE); - if (availablePorts.isEmpty()) { - throw new GlobalException(ErrorCode.NO_AVAILABLE_PORT); - } + // 1. 사용 가능한 포트 목록 조회 (status == AVAILABLE) + PortRegistry available = portRegistryRepository.findFirstByStatus(PortStatus.AVAILABLE) + .orElseThrow(() -> new GlobalException(ErrorCode.NO_AVAILABLE_PORT)); - Collections.shuffle(availablePorts); - PortRegistry selected = availablePorts.get(0); - selected.assignToRepository(repo); - PortRegistry savedRegistry = portRegistryRepository.save(selected); + // 2. 해당 포트 할당/저장 + available.assignToRepository(repo); + PortRegistry savedRegistry = portRegistryRepository.save(available); - log.info("Port {} allocated to repository {}", selected.getPort(), repo.getId()); + log.info("Port {} allocated to repository {}", available.getPort(), repo.getId()); return savedRegistry; }