diff --git a/README.md b/README.md index a6a9b181..acf323b0 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 +``` + 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(); } 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; }