From 479aa24a5ed683e3219931fcb409d126db0b0125 Mon Sep 17 00:00:00 2001 From: projectmiluju Date: Wed, 23 Jul 2025 20:38:39 +0900 Subject: [PATCH 01/40] =?UTF-8?q?feat(aws):=20S3=20presigned=20URL=20?= =?UTF-8?q?=EB=B0=8F=20AWS=20SDK=20v2=20=EC=A0=81=EC=9A=A9=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80=20DP-73?= 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 4fefc5ba..3256f9b3 100644 --- a/build.gradle +++ b/build.gradle @@ -49,8 +49,13 @@ dependencies { implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0' // AWS S3 (파일 업로드 및 다운로드를 위한 라이브러리) + // AWS SDK v2 - S3 + presigned URL + implementation platform('software.amazon.awssdk:bom:2.25.3') + implementation 'software.amazon.awssdk:s3' + implementation 'software.amazon.awssdk:s3-presigner' implementation 'com.amazonaws:aws-java-sdk-s3:1.12.673' + // 이메일 발송 기능 implementation 'org.springframework.boot:spring-boot-starter-mail' From fe3654f1784cefcc135f18cc82ce5fc4292c0467 Mon Sep 17 00:00:00 2001 From: projectmiluju Date: Wed, 23 Jul 2025 20:39:10 +0900 Subject: [PATCH 02/40] =?UTF-8?q?feat(aws):=20S3=20=ED=81=B4=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EC=96=B8=ED=8A=B8=20=EC=84=A4=EC=A0=95=EC=9D=84=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20=EC=84=A4=EC=A0=95=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EC=B6=94=EA=B0=80=20DP-73?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sandbox/config/AwsS3Config.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/main/java/com/deepdirect/deepwebide_be/sandbox/config/AwsS3Config.java diff --git a/src/main/java/com/deepdirect/deepwebide_be/sandbox/config/AwsS3Config.java b/src/main/java/com/deepdirect/deepwebide_be/sandbox/config/AwsS3Config.java new file mode 100644 index 00000000..29929d78 --- /dev/null +++ b/src/main/java/com/deepdirect/deepwebide_be/sandbox/config/AwsS3Config.java @@ -0,0 +1,35 @@ +package com.deepdirect.deepwebide_be.sandbox.config; + +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableConfigurationProperties +public class AwsS3Config { + + @Value("${cloud.aws.credentials.access-key}") + private String accessKey; + + @Value("${cloud.aws.credentials.secret-key}") + private String secretKey; + + @Value("${cloud.aws.region.static}") + private String region; + + @Bean + public AmazonS3 amazonS3() { + AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey); + return AmazonS3ClientBuilder.standard() + .withCredentials(new AWSStaticCredentialsProvider(credentials)) + .withRegion(region) + .build(); + } +} + From f7ce0a067264a48fa3e554e607ba58f410b7aee4 Mon Sep 17 00:00:00 2001 From: projectmiluju Date: Wed, 23 Jul 2025 20:39:22 +0900 Subject: [PATCH 03/40] =?UTF-8?q?feat(sandbox):=20S3Service=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=97=85=EB=A1=9C=EB=93=9C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20DP-73?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sandbox/service/S3Service.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/main/java/com/deepdirect/deepwebide_be/sandbox/service/S3Service.java diff --git a/src/main/java/com/deepdirect/deepwebide_be/sandbox/service/S3Service.java b/src/main/java/com/deepdirect/deepwebide_be/sandbox/service/S3Service.java new file mode 100644 index 00000000..4854059d --- /dev/null +++ b/src/main/java/com/deepdirect/deepwebide_be/sandbox/service/S3Service.java @@ -0,0 +1,30 @@ +package com.deepdirect.deepwebide_be.sandbox.service; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.ObjectMetadata; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; + +@Service +@RequiredArgsConstructor +public class S3Service { + + private final AmazonS3 amazonS3; + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + public String upload(MultipartFile file, String uuid) throws IOException { + String key = "sandbox-projects/" + uuid + ".zip"; + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentLength(file.getSize()); + + amazonS3.putObject(bucket, key, file.getInputStream(), metadata); + return amazonS3.getUrl(bucket, key).toString(); + } +} + From 92d03dfe55afbaa648146540e463a173d1517456 Mon Sep 17 00:00:00 2001 From: projectmiluju Date: Wed, 23 Jul 2025 20:39:51 +0900 Subject: [PATCH 04/40] =?UTF-8?q?feat(sandbox):=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=8B=A4=ED=96=89=EC=9D=84=20=EC=9C=84=ED=95=9C=20SandboxContr?= =?UTF-8?q?oller=20=EB=B0=8F=20SandboxService=20=EC=B6=94=EA=B0=80=20DP-73?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sandbox/controller/SandboxController.java | 50 +++++++++++++++++++ .../sandbox/service/SandboxService.java | 30 +++++++++++ 2 files changed, 80 insertions(+) create mode 100644 src/main/java/com/deepdirect/deepwebide_be/sandbox/controller/SandboxController.java create mode 100644 src/main/java/com/deepdirect/deepwebide_be/sandbox/service/SandboxService.java diff --git a/src/main/java/com/deepdirect/deepwebide_be/sandbox/controller/SandboxController.java b/src/main/java/com/deepdirect/deepwebide_be/sandbox/controller/SandboxController.java new file mode 100644 index 00000000..d7197758 --- /dev/null +++ b/src/main/java/com/deepdirect/deepwebide_be/sandbox/controller/SandboxController.java @@ -0,0 +1,50 @@ +package com.deepdirect.deepwebide_be.sandbox.controller; + +import com.deepdirect.deepwebide_be.sandbox.service.S3Service; +import com.deepdirect.deepwebide_be.sandbox.service.SandboxService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.*; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.multipart.MultipartFile; + +import java.util.Map; +import java.util.UUID; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/projects") +public class SandboxController { + + private final S3Service s3Service; + private final SandboxService sandboxService; + + @PostMapping("/execute") + public ResponseEntity uploadAndRun( + @RequestParam("file") MultipartFile file, + @RequestParam("framework") String framework, + @RequestParam("port") int port + ) { + try { + String uuid = UUID.randomUUID().toString(); + String s3Url = s3Service.upload(file, uuid); + + Map body = Map.of( + "uuid", uuid, + "url", s3Url, + "framework", framework, + "port", port + ); + + String sandboxResponse = sandboxService.requestExecution(body); + return ResponseEntity.ok("요청 완료: " + sandboxResponse); + + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("실행 실패: " + e.getMessage()); + } + } +} diff --git a/src/main/java/com/deepdirect/deepwebide_be/sandbox/service/SandboxService.java b/src/main/java/com/deepdirect/deepwebide_be/sandbox/service/SandboxService.java new file mode 100644 index 00000000..57bf0a6f --- /dev/null +++ b/src/main/java/com/deepdirect/deepwebide_be/sandbox/service/SandboxService.java @@ -0,0 +1,30 @@ +package com.deepdirect.deepwebide_be.sandbox.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class SandboxService { + + private final RestTemplate restTemplate = new RestTemplate(); + + public String requestExecution(Map body) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity> request = new HttpEntity<>(body, headers); + + ResponseEntity response = restTemplate.postForEntity( + "http://localhost:9090/api/sandbox/run", request, String.class + ); + + return response.getBody(); + } +} From b43f0feb79fbc833c8730b488ab132dbdad28a89 Mon Sep 17 00:00:00 2001 From: projectmiluju Date: Thu, 24 Jul 2025 02:30:46 +0900 Subject: [PATCH 05/40] =?UTF-8?q?feat(dependencies):=20=20AWS=20SDK=20S3?= =?UTF-8?q?=20version=202.31.78=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20D?= =?UTF-8?q?P-73?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 3256f9b3..f094b040 100644 --- a/build.gradle +++ b/build.gradle @@ -50,9 +50,7 @@ dependencies { // AWS S3 (파일 업로드 및 다운로드를 위한 라이브러리) // AWS SDK v2 - S3 + presigned URL - implementation platform('software.amazon.awssdk:bom:2.25.3') - implementation 'software.amazon.awssdk:s3' - implementation 'software.amazon.awssdk:s3-presigner' + implementation 'software.amazon.awssdk:s3:2.31.78' implementation 'com.amazonaws:aws-java-sdk-s3:1.12.673' From 868e28e52b8a62bf3ebc99c2be7609ab60ca9511 Mon Sep 17 00:00:00 2001 From: projectmiluju Date: Thu, 24 Jul 2025 02:30:58 +0900 Subject: [PATCH 06/40] =?UTF-8?q?feat(security):=20=ED=97=88=EC=9A=A9?= =?UTF-8?q?=EB=90=9C=20=EA=B2=BD=EB=A1=9C=EC=97=90=20/api/projects/**=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20DP-73?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../deepwebide_be/global/security/SecurityConfiguration.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/deepdirect/deepwebide_be/global/security/SecurityConfiguration.java b/src/main/java/com/deepdirect/deepwebide_be/global/security/SecurityConfiguration.java index 57fda07f..da643c09 100644 --- a/src/main/java/com/deepdirect/deepwebide_be/global/security/SecurityConfiguration.java +++ b/src/main/java/com/deepdirect/deepwebide_be/global/security/SecurityConfiguration.java @@ -56,7 +56,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti new AntPathRequestMatcher("/swagger-resources/**"), new AntPathRequestMatcher("/webjars/**"), new AntPathRequestMatcher("/api/auth/**"), - new AntPathRequestMatcher("/h2-console/**") + new AntPathRequestMatcher("/h2-console/**"), + new AntPathRequestMatcher("/api/projects/**") ).permitAll() .requestMatchers(new AntPathRequestMatcher("/api/auth/signout")).authenticated() .anyRequest().authenticated() From 4c181f1945191e564bfff7f697fd7c1f260b68cf Mon Sep 17 00:00:00 2001 From: projectmiluju Date: Thu, 24 Jul 2025 02:31:29 +0900 Subject: [PATCH 07/40] =?UTF-8?q?refactor(controller):=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20import?= =?UTF-8?q?=EB=AC=B8=20=EC=A0=9C=EA=B1=B0=20DP-73?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../deepwebide_be/sandbox/controller/SandboxController.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/deepdirect/deepwebide_be/sandbox/controller/SandboxController.java b/src/main/java/com/deepdirect/deepwebide_be/sandbox/controller/SandboxController.java index d7197758..070fcab9 100644 --- a/src/main/java/com/deepdirect/deepwebide_be/sandbox/controller/SandboxController.java +++ b/src/main/java/com/deepdirect/deepwebide_be/sandbox/controller/SandboxController.java @@ -8,7 +8,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.client.RestTemplate; import org.springframework.web.multipart.MultipartFile; import java.util.Map; From 00f35d9dae29753e34922ea2a011673d1f46bf8d Mon Sep 17 00:00:00 2001 From: projectmiluju Date: Thu, 24 Jul 2025 02:50:26 +0900 Subject: [PATCH 08/40] =?UTF-8?q?feat(controller):=20=EC=83=8C=EB=93=9C?= =?UTF-8?q?=EB=B0=95=EC=8A=A4=20=EB=A1=9C=EA=B7=B8=20=ED=99=95=EC=9D=B8?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EC=BB=A8=ED=8A=B8=EB=A1=A4?= =?UTF-8?q?=EB=9F=AC=20=EA=B5=AC=ED=98=84=20DP-73?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/SandboxLogController.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/main/java/com/deepdirect/deepwebide_be/sandbox/controller/SandboxLogController.java diff --git a/src/main/java/com/deepdirect/deepwebide_be/sandbox/controller/SandboxLogController.java b/src/main/java/com/deepdirect/deepwebide_be/sandbox/controller/SandboxLogController.java new file mode 100644 index 00000000..d9ff10fd --- /dev/null +++ b/src/main/java/com/deepdirect/deepwebide_be/sandbox/controller/SandboxLogController.java @@ -0,0 +1,32 @@ +package com.deepdirect.deepwebide_be.sandbox.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/projects") +public class SandboxLogController { + + private final RestTemplate restTemplate = new RestTemplate(); + + @GetMapping("/logs/{uuid}") + public ResponseEntity getContainerLogs(@PathVariable String uuid) { + String containerId = "sandbox-" + uuid; + String sandboxUrl = "http://localhost:9090/api/sandbox/logs/" + containerId; + + try { + ResponseEntity response = restTemplate.getForEntity(sandboxUrl, String.class); + return ResponseEntity.ok(response.getBody()); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("샌드박스 로그 조회 실패: " + e.getMessage()); + } + } +} From 8b8453ce0899adbee5fda25d81fbef671cb28654 Mon Sep 17 00:00:00 2001 From: projectmiluju Date: Tue, 29 Jul 2025 11:44:32 +0900 Subject: [PATCH 09/40] =?UTF-8?q?feat(s3):=20=EC=A4=91=EB=B3=B5=EB=90=9C?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=EC=9D=98=20s3config=20=EC=82=AD=EC=A0=9C?= =?UTF-8?q?=20DP-73?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sandbox/config/AwsS3Config.java | 35 ------------------- 1 file changed, 35 deletions(-) delete mode 100644 src/main/java/com/deepdirect/deepwebide_be/sandbox/config/AwsS3Config.java diff --git a/src/main/java/com/deepdirect/deepwebide_be/sandbox/config/AwsS3Config.java b/src/main/java/com/deepdirect/deepwebide_be/sandbox/config/AwsS3Config.java deleted file mode 100644 index 29929d78..00000000 --- a/src/main/java/com/deepdirect/deepwebide_be/sandbox/config/AwsS3Config.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.deepdirect.deepwebide_be.sandbox.config; - -import com.amazonaws.auth.AWSCredentials; -import com.amazonaws.auth.AWSStaticCredentialsProvider; -import com.amazonaws.auth.BasicAWSCredentials; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.AmazonS3ClientBuilder; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -@EnableConfigurationProperties -public class AwsS3Config { - - @Value("${cloud.aws.credentials.access-key}") - private String accessKey; - - @Value("${cloud.aws.credentials.secret-key}") - private String secretKey; - - @Value("${cloud.aws.region.static}") - private String region; - - @Bean - public AmazonS3 amazonS3() { - AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey); - return AmazonS3ClientBuilder.standard() - .withCredentials(new AWSStaticCredentialsProvider(credentials)) - .withRegion(region) - .build(); - } -} - From f7c9f83165e9b2ba5a1ad6511bce441077925f8b Mon Sep 17 00:00:00 2001 From: projectmiluju Date: Tue, 29 Jul 2025 12:07:33 +0900 Subject: [PATCH 10/40] =?UTF-8?q?feat(build):=20spring-test=20dependency?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20DP-73?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 9f5e7302..1fe8d3aa 100644 --- a/build.gradle +++ b/build.gradle @@ -76,6 +76,7 @@ dependencies { implementation 'io.sentry:sentry-logback:8.17.0' // 테스트 관련 라이브러리 + implementation 'org.springframework:spring-test' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' From 464859137e39d6d1cfe409401764c14d715f1135 Mon Sep 17 00:00:00 2001 From: projectmiluju Date: Tue, 29 Jul 2025 12:11:14 +0900 Subject: [PATCH 11/40] =?UTF-8?q?feat(build):=20Apache=20Commons=20IO=20de?= =?UTF-8?q?pendency=20=EC=B6=94=EA=B0=80=20DP-73?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.gradle b/build.gradle index 1fe8d3aa..2974f6c5 100644 --- a/build.gradle +++ b/build.gradle @@ -75,6 +75,9 @@ dependencies { implementation 'io.sentry:sentry-spring-boot-starter:8.17.0' implementation 'io.sentry:sentry-logback:8.17.0' + // Apache Commons IO (파일 처리 등 유틸리티) + implementation 'commons-io:commons-io:2.16.1' + // 테스트 관련 라이브러리 implementation 'org.springframework:spring-test' testImplementation 'org.springframework.boot:spring-boot-starter-test' From 14877ae1f90df92ee071f2a8d1e8037667f04ec1 Mon Sep 17 00:00:00 2001 From: projectmiluju Date: Tue, 29 Jul 2025 12:13:45 +0900 Subject: [PATCH 12/40] =?UTF-8?q?feat(port):=20port=20getter=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20DP-73?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../deepwebide_be/repository/domain/PortRegistry.java | 4 ++++ 1 file changed, 4 insertions(+) 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 28a94ac3..2174b394 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 @@ -24,5 +24,9 @@ public void setStatus(PortStatus status) { public void setRepository(Repository repository) { this.repository = repository; } + + public Integer getPort() { + return this.port; + } } From 956405319a80c96bbdd39ca7e5ad98c1011d436e Mon Sep 17 00:00:00 2001 From: projectmiluju Date: Tue, 29 Jul 2025 12:14:33 +0900 Subject: [PATCH 13/40] =?UTF-8?q?feat(repository):=20Repository=EC=99=80?= =?UTF-8?q?=20status=EB=A1=9C=20PortRegistry=20=EC=B0=BE=EB=8A=94=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80=20DP-73?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/repository/PortRegistryRepository.java | 5 +++++ 1 file changed, 5 insertions(+) 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 dcca9b65..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 @@ -2,10 +2,15 @@ import com.deepdirect.deepwebide_be.repository.domain.PortRegistry; import com.deepdirect.deepwebide_be.repository.domain.PortStatus; +import com.deepdirect.deepwebide_be.repository.domain.Repository; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; public interface PortRegistryRepository extends JpaRepository { + Optional findFirstByStatusOrderByPortAsc(PortStatus status); + + Optional findByRepository(Repository repository); + Optional findFirstByStatus(PortStatus status); } From a7215801c4bf9fc634267954fb04c00183292afd Mon Sep 17 00:00:00 2001 From: projectmiluju Date: Tue, 29 Jul 2025 14:34:42 +0900 Subject: [PATCH 14/40] =?UTF-8?q?feat(error):=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=EB=90=9C=20=EC=97=90=EB=9F=AC=20=EC=BD=94=EB=93=9C=EB=A1=9C=20?= =?UTF-8?q?=EB=A0=88=ED=8F=AC=EC=A7=80=ED=86=A0=EB=A6=AC=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=98=A4=EB=A5=98=20=EC=B2=98=EB=A6=AC=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=20DP-73?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../deepdirect/deepwebide_be/global/exception/ErrorCode.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/com/deepdirect/deepwebide_be/global/exception/ErrorCode.java b/src/main/java/com/deepdirect/deepwebide_be/global/exception/ErrorCode.java index 97295597..7518a1ae 100644 --- a/src/main/java/com/deepdirect/deepwebide_be/global/exception/ErrorCode.java +++ b/src/main/java/com/deepdirect/deepwebide_be/global/exception/ErrorCode.java @@ -32,6 +32,10 @@ public enum ErrorCode { FILE_CONTENT_NOT_FOUND(HttpStatus.BAD_REQUEST, "파일 내용이 존재하지 않습니다."), CANNOT_SAVE_FOLDER(HttpStatus.BAD_REQUEST, "폴더는 저장할 수 없습니다."), HISTORY_NOT_FOUND(HttpStatus.BAD_REQUEST, "해당 히스토리를 찾을 수 없습니다."), + REPOSITORY_EXECUTION_FAILED(HttpStatus.BAD_REQUEST, "레포지토리를 찾을 수 없습니다."), + UNSUPPORTED_REPOSITORY_TYPE(HttpStatus.BAD_REQUEST, "지원하지 않는 레포지토리 타입입니다."), + REPOSITORY_FILES_NOT_FOUND(HttpStatus.BAD_REQUEST, "레포지토리 파일을 찾을 수 없습니다."), + FILE_TREE_CONVERSION_FAILED(HttpStatus.BAD_REQUEST, "파일 트리 변환에 실패했습니다."), // 401 UNAUTHORIZED UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "인증되지 않았습니다."), From 5d57ae6c5ebc47bfd3161974f324191e6746bba1 Mon Sep 17 00:00:00 2001 From: projectmiluju Date: Tue, 29 Jul 2025 14:41:49 +0900 Subject: [PATCH 15/40] =?UTF-8?q?feat(exception):=20SandboxException?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9C=20handler=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=20DP-73?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/exception/GlobalExceptionHandler.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/com/deepdirect/deepwebide_be/global/exception/GlobalExceptionHandler.java b/src/main/java/com/deepdirect/deepwebide_be/global/exception/GlobalExceptionHandler.java index 58881b2a..69f63b45 100644 --- a/src/main/java/com/deepdirect/deepwebide_be/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/deepdirect/deepwebide_be/global/exception/GlobalExceptionHandler.java @@ -1,6 +1,7 @@ package com.deepdirect.deepwebide_be.global.exception; import com.deepdirect.deepwebide_be.global.dto.ApiResponseDto; +import com.deepdirect.deepwebide_be.sandbox.exception.SandboxException; import io.sentry.Sentry; import io.sentry.SentryLevel; import io.sentry.protocol.SentryId; @@ -75,6 +76,13 @@ public ResponseEntity> handleOtherExceptions(Exception ex, Htt .body(ApiResponseDto.error(500, responseMessage)); } + @ExceptionHandler(SandboxException.class) + public ResponseEntity> handleSandboxException(SandboxException e) { + log.error("Sandbox execution error", e); + return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE) + .body(ApiResponseDto.of(503, "코드 실행 서비스가 일시적으로 사용할 수 없습니다.", null)); + } + /** Sentry에 예외 정보 전송 */ private SentryId sendToSentry(Exception ex, String errorType, SentryLevel level, HttpServletRequest request) { if (!Sentry.isEnabled()) { From c1c7fd505fc54d50651d3b61c1b08b616a1c635a Mon Sep 17 00:00:00 2001 From: projectmiluju Date: Tue, 29 Jul 2025 14:42:37 +0900 Subject: [PATCH 16/40] =?UTF-8?q?feat(port):=20=EB=A0=88=ED=8F=AC=EC=A7=80?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20=ED=95=A0=EB=8B=B9=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81=20=EB=B0=8F=20=ED=95=B4=EC=A0=9C=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80=20DP-73?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/domain/PortRegistry.java | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) 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 2174b394..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 @@ -17,12 +17,22 @@ public class PortRegistry { @OneToOne private Repository repository; - public void setStatus(PortStatus status) { - this.status = status; +// 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; + this.repository = repository; } - public void setRepository(Repository repository) { - this.repository = repository; + public void release() { + this.status = PortStatus.AVAILABLE; + this.repository = null; } public Integer getPort() { From c280b9f78d443fb302ef0f020be07779933148fb Mon Sep 17 00:00:00 2001 From: projectmiluju Date: Tue, 29 Jul 2025 15:01:21 +0900 Subject: [PATCH 17/40] =?UTF-8?q?feat(sandbox):=20=EC=83=8C=EB=93=9C?= =?UTF-8?q?=EB=B0=95=EC=8A=A4=20=EC=8B=A4=ED=96=89=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9D=91=EB=8B=B5=20DTO=20=EC=B6=94=EA=B0=80=20DP-?= =?UTF-8?q?73?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../response/RepositoryExecuteResponse.java | 39 +++++++++++++++++++ .../dto/request/SandboxExecutionRequest.java | 17 ++++++++ .../response/SandboxExecutionResponse.java | 17 ++++++++ 3 files changed, 73 insertions(+) create mode 100644 src/main/java/com/deepdirect/deepwebide_be/repository/dto/response/RepositoryExecuteResponse.java create mode 100644 src/main/java/com/deepdirect/deepwebide_be/sandbox/dto/request/SandboxExecutionRequest.java create mode 100644 src/main/java/com/deepdirect/deepwebide_be/sandbox/dto/response/SandboxExecutionResponse.java diff --git a/src/main/java/com/deepdirect/deepwebide_be/repository/dto/response/RepositoryExecuteResponse.java b/src/main/java/com/deepdirect/deepwebide_be/repository/dto/response/RepositoryExecuteResponse.java new file mode 100644 index 00000000..909c582f --- /dev/null +++ b/src/main/java/com/deepdirect/deepwebide_be/repository/dto/response/RepositoryExecuteResponse.java @@ -0,0 +1,39 @@ +package com.deepdirect.deepwebide_be.repository.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@Schema(description = "레포지토리 실행 결과 응답 DTO") +public class RepositoryExecuteResponse { + + @Schema(description = "레포지토리 UUID", example = "123e4567-e89b-12d3-a456-426614174000") + private String uuid; + + @Schema(description = "S3 URL", example = "https://s3.amazonaws.com/bucket/123e4567-e89b-12d3-a456-426614174000.zip") + private String s3Url; + + @Schema(description = "레포지토리 포트 번호", example = "8080") + private Integer port; + + @Schema(description = "실행 결과 메시지", example = "레포지토리 실행이 성공적으로 완료되었습니다.") + private String message; + + // 추가된 필드들 + @Schema(description = "실행 ID", example = "exec-123456") + private String executionId; + + @Schema(description = "실행 상태", example = "RUNNING", allowableValues = {"PENDING", "RUNNING", "SUCCESS", "FAILED"}) + private String status; + + @Schema(description = "실행 출력", example = "Application started successfully on port 8080") + private String output; + + @Schema(description = "실행 에러", example = "") + private String error; + + @Schema(description = "실행 시간 (밀리초)", example = "5000") + private Long executionTime; +} diff --git a/src/main/java/com/deepdirect/deepwebide_be/sandbox/dto/request/SandboxExecutionRequest.java b/src/main/java/com/deepdirect/deepwebide_be/sandbox/dto/request/SandboxExecutionRequest.java new file mode 100644 index 00000000..82f64380 --- /dev/null +++ b/src/main/java/com/deepdirect/deepwebide_be/sandbox/dto/request/SandboxExecutionRequest.java @@ -0,0 +1,17 @@ +package com.deepdirect.deepwebide_be.sandbox.dto.request; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class SandboxExecutionRequest { + private String uuid; + private String url; + private String framework; + private Integer port; +} diff --git a/src/main/java/com/deepdirect/deepwebide_be/sandbox/dto/response/SandboxExecutionResponse.java b/src/main/java/com/deepdirect/deepwebide_be/sandbox/dto/response/SandboxExecutionResponse.java new file mode 100644 index 00000000..64752972 --- /dev/null +++ b/src/main/java/com/deepdirect/deepwebide_be/sandbox/dto/response/SandboxExecutionResponse.java @@ -0,0 +1,17 @@ +package com.deepdirect.deepwebide_be.sandbox.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class SandboxExecutionResponse { + private String executionId; + private String status; + private String output; + private String error; + private Long executionTime; + private String message; +} \ No newline at end of file From a5e1dc970bb04c50983391c34004626591036b94 Mon Sep 17 00:00:00 2001 From: projectmiluju Date: Tue, 29 Jul 2025 15:01:46 +0900 Subject: [PATCH 18/40] =?UTF-8?q?feat(repository):=20assignToRepository=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=EB=A5=BC=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=82=AC=EC=9A=A9=20=EA=B0=80?= =?UTF-8?q?=EB=8A=A5=ED=95=9C=20=ED=8F=AC=ED=8A=B8=20=ED=95=A0=EB=8B=B9=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EB=A6=AC=ED=8C=A9=ED=84=B0=EB=A7=81=20DP-?= =?UTF-8?q?73?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../deepwebide_be/repository/service/RepositoryService.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/com/deepdirect/deepwebide_be/repository/service/RepositoryService.java b/src/main/java/com/deepdirect/deepwebide_be/repository/service/RepositoryService.java index c8d6ff7c..872a42f0 100644 --- a/src/main/java/com/deepdirect/deepwebide_be/repository/service/RepositoryService.java +++ b/src/main/java/com/deepdirect/deepwebide_be/repository/service/RepositoryService.java @@ -56,8 +56,7 @@ public RepositoryCreateResponse createRepository(RepositoryCreateRequest request .orElseThrow(() -> new GlobalException( ErrorCode.NO_AVAILABLE_PORT )); - availablePort.setStatus(PortStatus.IN_USE); - availablePort.setRepository(savedRepository); + availablePort.assignToRepository(savedRepository); portRegistryRepository.save(availablePort); RepositoryMember ownerMember = RepositoryMember.builder() From f839aab6947f049b846cac02cabbe93f44afdc34 Mon Sep 17 00:00:00 2001 From: projectmiluju Date: Tue, 29 Jul 2025 15:02:18 +0900 Subject: [PATCH 19/40] =?UTF-8?q?feat(sandbox):=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=EC=A0=9D=ED=8A=B8=20=EC=8B=A4=ED=96=89=20API=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=C2=B7=EC=9D=91=EB=8B=B5=20=EA=B5=AC=EC=A1=B0=EB=A5=BC?= =?UTF-8?q?=20DTO=EB=A1=9C=20=EA=B0=9C=EC=84=A0=20DP-73?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sandbox/controller/SandboxController.java | 51 ++++++++++++---- .../sandbox/service/SandboxService.java | 60 ++++++++++++++----- 2 files changed, 84 insertions(+), 27 deletions(-) diff --git a/src/main/java/com/deepdirect/deepwebide_be/sandbox/controller/SandboxController.java b/src/main/java/com/deepdirect/deepwebide_be/sandbox/controller/SandboxController.java index 070fcab9..42fcf601 100644 --- a/src/main/java/com/deepdirect/deepwebide_be/sandbox/controller/SandboxController.java +++ b/src/main/java/com/deepdirect/deepwebide_be/sandbox/controller/SandboxController.java @@ -1,8 +1,15 @@ package com.deepdirect.deepwebide_be.sandbox.controller; +import com.deepdirect.deepwebide_be.global.dto.ApiResponseDto; +import com.deepdirect.deepwebide_be.sandbox.dto.request.SandboxExecutionRequest; +import com.deepdirect.deepwebide_be.sandbox.dto.response.SandboxExecutionResponse; import com.deepdirect.deepwebide_be.sandbox.service.S3Service; import com.deepdirect.deepwebide_be.sandbox.service.SandboxService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.*; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -13,37 +20,59 @@ import java.util.Map; import java.util.UUID; +@Slf4j @RestController @RequiredArgsConstructor @RequestMapping("/api/projects") +@Tag(name = "SANDBOX", description = "샌드박스 실행 API") public class SandboxController { private final S3Service s3Service; private final SandboxService sandboxService; @PostMapping("/execute") - public ResponseEntity uploadAndRun( + @Operation(summary = "프로젝트 업로드 및 실행", description = "zip 파일을 업로드하고 샌드박스에서 실행합니다.") + public ResponseEntity> uploadAndRun( + @Parameter(description = "업로드할 zip 파일", required = true) @RequestParam("file") MultipartFile file, + + @Parameter(description = "프레임워크 타입", example = "spring", required = true) @RequestParam("framework") String framework, - @RequestParam("port") int port + + @Parameter(description = "포트 번호", example = "8080", required = true) + @RequestParam("port") Integer port ) { try { + log.info("Starting project execution - framework: {}, port: {}", framework, port); + + // 1. UUID 생성 String uuid = UUID.randomUUID().toString(); + + // 2. S3에 파일 업로드 String s3Url = s3Service.upload(file, uuid); + log.debug("File uploaded to S3 - url: {}", s3Url); - Map body = Map.of( - "uuid", uuid, - "url", s3Url, - "framework", framework, - "port", port - ); + // 3. 샌드박스 실행 요청 생성 + SandboxExecutionRequest request = SandboxExecutionRequest.builder() + .uuid(uuid) + .url(s3Url) + .framework(framework) + .port(port) + .build(); + + // 4. 샌드박스 서비스 호출 + SandboxExecutionResponse response = sandboxService.requestExecution(request); - String sandboxResponse = sandboxService.requestExecution(body); - return ResponseEntity.ok("요청 완료: " + sandboxResponse); + log.info("Project execution completed - uuid: {}, status: {}", uuid, response.getStatus()); + + return ResponseEntity.ok( + ApiResponseDto.of(200, "프로젝트 실행 요청이 완료되었습니다.", response) + ); } catch (Exception e) { + log.error("Project execution failed - framework: {}, port: {}", framework, port, e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body("실행 실패: " + e.getMessage()); + .body(ApiResponseDto.of(500, "실행 실패: " + e.getMessage(), null)); } } } diff --git a/src/main/java/com/deepdirect/deepwebide_be/sandbox/service/SandboxService.java b/src/main/java/com/deepdirect/deepwebide_be/sandbox/service/SandboxService.java index 57bf0a6f..b9e05ae8 100644 --- a/src/main/java/com/deepdirect/deepwebide_be/sandbox/service/SandboxService.java +++ b/src/main/java/com/deepdirect/deepwebide_be/sandbox/service/SandboxService.java @@ -1,30 +1,58 @@ package com.deepdirect.deepwebide_be.sandbox.service; +import com.deepdirect.deepwebide_be.sandbox.dto.request.SandboxExecutionRequest; +import com.deepdirect.deepwebide_be.sandbox.dto.response.SandboxExecutionResponse; +import com.deepdirect.deepwebide_be.sandbox.exception.SandboxException; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; -import java.util.Map; - +@Slf4j @Service @RequiredArgsConstructor public class SandboxService { - private final RestTemplate restTemplate = new RestTemplate(); + private final RestTemplate restTemplate; - public String requestExecution(Map body) { - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); - HttpEntity> request = new HttpEntity<>(body, headers); + @Value("${sandbox.api.base-url:http://localhost:9090}") + private String sandboxBaseUrl; + + public SandboxExecutionResponse requestExecution(SandboxExecutionRequest request) { + try { + HttpHeaders headers = createHeaders(); + HttpEntity httpEntity = new HttpEntity<>(request, headers); + + String url = sandboxBaseUrl + "/api/sandbox/run"; + log.info("Requesting sandbox execution: {} with request: {}", url, request); - ResponseEntity response = restTemplate.postForEntity( - "http://localhost:9090/api/sandbox/run", request, String.class - ); + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.POST, + httpEntity, + SandboxExecutionResponse.class + ); - return response.getBody(); + if (response.getStatusCode().is2xxSuccessful()) { + log.info("Sandbox execution completed successfully"); + return response.getBody(); + } else { + throw new SandboxException("Sandbox execution failed with status: " + response.getStatusCode()); + } + + } catch (RestClientException e) { + log.error("Failed to communicate with sandbox service", e); + throw new SandboxException("Sandbox service communication error", e); + } + } + + private HttpHeaders createHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.add("User-Agent", "DeepWebIDE-Backend"); + return headers; } -} +} \ No newline at end of file From c9058a9c606229860efcfc8cd2a3e3ef1b702e02 Mon Sep 17 00:00:00 2001 From: projectmiluju Date: Tue, 29 Jul 2025 15:02:43 +0900 Subject: [PATCH 20/40] =?UTF-8?q?feat(exception):=20=EC=83=8C=EB=93=9C?= =?UTF-8?q?=EB=B0=95=EC=8A=A4=20=EA=B4=80=EB=A0=A8=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=EB=A5=BC=20=EC=9C=84=ED=95=9C=20SandboxExcep?= =?UTF-8?q?tion=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=83=9D=EC=84=B1=20DP-73?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sandbox/exception/SandboxException.java | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/main/java/com/deepdirect/deepwebide_be/sandbox/exception/SandboxException.java diff --git a/src/main/java/com/deepdirect/deepwebide_be/sandbox/exception/SandboxException.java b/src/main/java/com/deepdirect/deepwebide_be/sandbox/exception/SandboxException.java new file mode 100644 index 00000000..b98c3f5b --- /dev/null +++ b/src/main/java/com/deepdirect/deepwebide_be/sandbox/exception/SandboxException.java @@ -0,0 +1,11 @@ +package com.deepdirect.deepwebide_be.sandbox.exception; + +public class SandboxException extends RuntimeException { + public SandboxException(String message) { + super(message); + } + + public SandboxException(String message, Throwable cause) { + super(message, cause); + } +} From 2e2dd33711e3a16079e7f86b5baa0a575350f0d3 Mon Sep 17 00:00:00 2001 From: projectmiluju Date: Tue, 29 Jul 2025 15:03:05 +0900 Subject: [PATCH 21/40] =?UTF-8?q?feat(config):=20RestTemplate=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=EC=9D=84=20=EC=9C=84=ED=95=9C=20RestTemplateConfig=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=B6=94=EA=B0=80=20DP-73?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sandbox/config/RestTemplateConfig.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/main/java/com/deepdirect/deepwebide_be/sandbox/config/RestTemplateConfig.java diff --git a/src/main/java/com/deepdirect/deepwebide_be/sandbox/config/RestTemplateConfig.java b/src/main/java/com/deepdirect/deepwebide_be/sandbox/config/RestTemplateConfig.java new file mode 100644 index 00000000..0374b360 --- /dev/null +++ b/src/main/java/com/deepdirect/deepwebide_be/sandbox/config/RestTemplateConfig.java @@ -0,0 +1,20 @@ +package com.deepdirect.deepwebide_be.sandbox.config; + +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +import java.time.Duration; + +@Configuration +public class RestTemplateConfig { + + @Bean + public RestTemplate restTemplate(RestTemplateBuilder builder) { + return builder + .connectTimeout(Duration.ofSeconds(10)) + .readTimeout(Duration.ofSeconds(30)) + .build(); + } +} \ No newline at end of file From 3b6e7881262f38f41a13651d30f1483348180d84 Mon Sep 17 00:00:00 2001 From: projectmiluju Date: Tue, 29 Jul 2025 15:03:34 +0900 Subject: [PATCH 22/40] =?UTF-8?q?feat(repository):=20=EB=A0=88=ED=8F=AC?= =?UTF-8?q?=EC=A7=80=ED=86=A0=EB=A6=AC=20=EC=8B=A4=ED=96=89=EC=9D=84=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20RepositoryRunController=20=EB=B0=8F=20Repo?= =?UTF-8?q?sitoryRunService=20=EC=B6=94=EA=B0=80=20DP-73?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/RepositoryRunController.java | 33 ++ .../service/RepositoryRunService.java | 287 ++++++++++++++++++ 2 files changed, 320 insertions(+) create mode 100644 src/main/java/com/deepdirect/deepwebide_be/repository/controller/RepositoryRunController.java create mode 100644 src/main/java/com/deepdirect/deepwebide_be/repository/service/RepositoryRunService.java 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 new file mode 100644 index 00000000..c48a62c3 --- /dev/null +++ b/src/main/java/com/deepdirect/deepwebide_be/repository/controller/RepositoryRunController.java @@ -0,0 +1,33 @@ +package com.deepdirect.deepwebide_be.repository.controller; + +import com.deepdirect.deepwebide_be.global.dto.ApiResponseDto; +import com.deepdirect.deepwebide_be.global.security.CustomUserDetails; +import com.deepdirect.deepwebide_be.repository.dto.response.RepositoryExecuteResponse; +import com.deepdirect.deepwebide_be.repository.service.RepositoryRunService; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; + +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/repositories") +@Tag(name = "RUN", description = "레포지토리 실행, 로그 반환 등 기능 API") +public class RepositoryRunController { + + private final RepositoryRunService repositoryRunService; + + @PostMapping("/{repositoryId}/execute") + public ResponseEntity> executeRepository( + @AuthenticationPrincipal CustomUserDetails userDetails, + @PathVariable Long repositoryId + ) { + RepositoryExecuteResponse resp = repositoryRunService.executeRepository(repositoryId, userDetails.getId()); + return ResponseEntity.ok(ApiResponseDto.of(200, "레포지토리 실행 요청 완료", resp)); + } + +} + 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 new file mode 100644 index 00000000..f46575d5 --- /dev/null +++ b/src/main/java/com/deepdirect/deepwebide_be/repository/service/RepositoryRunService.java @@ -0,0 +1,287 @@ +package com.deepdirect.deepwebide_be.repository.service; + +import com.deepdirect.deepwebide_be.file.domain.FileContent; +import com.deepdirect.deepwebide_be.file.domain.FileNode; +import com.deepdirect.deepwebide_be.file.repository.FileContentRepository; +import com.deepdirect.deepwebide_be.file.repository.FileNodeRepository; +import com.deepdirect.deepwebide_be.global.exception.ErrorCode; +import com.deepdirect.deepwebide_be.global.exception.GlobalException; +import com.deepdirect.deepwebide_be.repository.domain.PortRegistry; +import com.deepdirect.deepwebide_be.repository.domain.PortStatus; +import com.deepdirect.deepwebide_be.repository.domain.Repository; +import com.deepdirect.deepwebide_be.repository.domain.RepositoryType; +import com.deepdirect.deepwebide_be.repository.dto.response.RepositoryExecuteResponse; +import com.deepdirect.deepwebide_be.repository.repository.PortRegistryRepository; +import com.deepdirect.deepwebide_be.repository.repository.RepositoryRepository; +import com.deepdirect.deepwebide_be.sandbox.dto.request.SandboxExecutionRequest; +import com.deepdirect.deepwebide_be.sandbox.dto.response.SandboxExecutionResponse; +import com.deepdirect.deepwebide_be.sandbox.service.S3Service; +import com.deepdirect.deepwebide_be.sandbox.service.SandboxService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FileUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.mock.web.MockMultipartFile; + + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.UUID; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +@Slf4j +@Service +@RequiredArgsConstructor +public class RepositoryRunService { + + private final RepositoryRepository repositoryRepository; + private final S3Service s3Service; + private final SandboxService sandboxService; + private final PortRegistryRepository portRegistryRepository; + private final FileNodeRepository fileNodeRepository; + private final FileContentRepository fileContentRepository; + + @Transactional + public RepositoryExecuteResponse executeRepository(Long repositoryId, Long userId) { + log.info("Starting repository execution - repositoryId: {}, userId: {}", repositoryId, userId); + + File zipFile = null; + try { + // 1. 권한 체크 & 레포지토리 조회 + Repository repo = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId) + .orElseThrow(() -> new GlobalException(ErrorCode.REPOSITORY_NOT_FOUND)); + + // 2. framework, port 등 정보 추출 + String framework = convertTypeToFramework(repo.getRepositoryType()); + Integer port = allocateOrGetPort(repo); + + // 3. 파일트리를 zip으로 변환 + String uuid = UUID.randomUUID().toString(); + zipFile = fileTreeToZip(repositoryId, uuid); + + // 4. S3 업로드 + String s3Url = uploadToS3(zipFile, uuid); + + // 5. 샌드박스 실행 요청 + SandboxExecutionRequest request = SandboxExecutionRequest.builder() + .uuid(uuid) + .url(s3Url) + .framework(framework) + .port(port) + .build(); + + SandboxExecutionResponse result = sandboxService.requestExecution(request); + + log.info("Repository execution completed - uuid: {}, port: {}", uuid, port); + + return RepositoryExecuteResponse.builder() + .uuid(uuid) + .s3Url(s3Url) + .port(port) + .message(result.getMessage()) + .executionId(result.getExecutionId()) + .status(result.getStatus()) + .output(result.getOutput()) + .error(result.getError()) + .executionTime(result.getExecutionTime()) + .build(); + + } catch (GlobalException e) { + log.error("Repository execution failed - repositoryId: {}, userId: {}", repositoryId, userId, e); + throw e; + } catch (Exception e) { + log.error("Repository execution failed - repositoryId: {}, userId: {}", repositoryId, userId, e); + throw new GlobalException(ErrorCode.REPOSITORY_EXECUTION_FAILED); + } finally { + // 임시 zip 파일 정리 + cleanupTempFile(zipFile); + } + } + + private String convertTypeToFramework(RepositoryType type) { + return switch (type) { + case SPRING_BOOT -> "spring"; + case REACT -> "react"; + case FAST_API -> "fastapi"; + default -> throw new GlobalException(ErrorCode.UNSUPPORTED_REPOSITORY_TYPE); + }; + } + + private Integer allocateOrGetPort(Repository repo) { + return portRegistryRepository.findByRepository(repo) + .map(PortRegistry::getPort) + .orElseGet(() -> allocateNewPortForRepository(repo).getPort()); + } + + @Transactional + public PortRegistry allocateNewPortForRepository(Repository repo) { + log.debug("Allocating new port for repository: {}", repo.getId()); + + // 1. 사용 가능한 포트 목록 조회 (status == AVAILABLE) + PortRegistry available = portRegistryRepository.findFirstByStatus(PortStatus.AVAILABLE) + .orElseThrow(() -> new GlobalException(ErrorCode.NO_AVAILABLE_PORT)); + + // 2. 해당 포트 할당/저장 + available.assignToRepository(repo); + PortRegistry savedRegistry = portRegistryRepository.save(available); + + log.info("Port {} allocated to repository {}", available.getPort(), repo.getId()); + return savedRegistry; + } + + private String uploadToS3(File zipFile, String uuid) throws IOException { + log.debug("Uploading zip file to S3 - uuid: {}", uuid); + + try (FileInputStream fis = new FileInputStream(zipFile)) { + String s3Url = s3Service.upload( + new MockMultipartFile("file", zipFile.getName(), "application/zip", fis), + uuid + ); + log.debug("S3 upload completed - url: {}", s3Url); + return s3Url; + } + } + + public File fileTreeToZip(Long repositoryId, String uuid) { + Path tempDir = null; + Path zipPath = null; + + try { + log.debug("Converting file tree to zip - repositoryId: {}, uuid: {}", repositoryId, uuid); + + // 1. 임시 디렉토리 생성 + tempDir = Files.createTempDirectory("repo-" + uuid); + + // 2. 파일트리/내용 복원 + List nodes = fileNodeRepository.findAllByRepositoryId(repositoryId); + if (nodes.isEmpty()) { + log.warn("No files found for repository: {}", repositoryId); + throw new GlobalException(ErrorCode.REPOSITORY_FILES_NOT_FOUND); + } + + restoreFileTree(nodes, tempDir); + + // 3. zip 압축 + zipPath = createZipFromDirectory(tempDir); + + log.debug("File tree conversion completed - zipPath: {}", zipPath); + return zipPath.toFile(); + + } catch (Exception e) { + log.error("Failed to convert file tree to zip - repositoryId: {}", repositoryId, e); + // 실패 시 생성된 임시 파일들 정리 + cleanupTempResources(tempDir, zipPath); + throw new GlobalException(ErrorCode.FILE_TREE_CONVERSION_FAILED); + } finally { + // 임시 디렉토리 정리 (zip 파일은 유지) + cleanupTempDirectory(tempDir); + } + } + + private void restoreFileTree(List nodes, Path tempDir) throws IOException { + log.debug("Restoring file tree - nodes count: {}", nodes.size()); + + // 폴더 먼저 생성 + nodes.stream() + .filter(FileNode::isFolder) + .forEach(node -> { + try { + Path nodePath = tempDir.resolve(node.getPath()); + Files.createDirectories(nodePath); + log.trace("Created directory: {}", nodePath); + } catch (IOException e) { + throw new UncheckedIOException("Failed to create directory: " + node.getPath(), e); + } + }); + + // 파일 생성 + for (FileNode node : nodes) { + if (!node.isFolder()) { + Path nodePath = tempDir.resolve(node.getPath()); + + // 부모 디렉토리 먼저 생성 + if (nodePath.getParent() != null) { + Files.createDirectories(nodePath.getParent()); + } + + // 파일 내용 작성 + FileContent content = fileContentRepository.findByFileNode(node) + .orElseThrow(() -> new GlobalException(ErrorCode.FILE_CONTENT_NOT_FOUND)); + + Files.write(nodePath, content.getContent()); + log.trace("Created file: {} (size: {} bytes)", nodePath, content.getContent().length); + } + } + } + + private Path createZipFromDirectory(Path tempDir) throws IOException { + Path zipPath = Paths.get(tempDir.toString() + ".zip"); + log.debug("Creating zip file: {}", zipPath); + + try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(zipPath))) { + Files.walk(tempDir) + .filter(Files::isRegularFile) + .forEach(path -> addToZip(zos, tempDir, path)); + } + + long zipSize = Files.size(zipPath); + log.debug("Zip file created successfully - size: {} bytes", zipSize); + + return zipPath; + } + + private void addToZip(ZipOutputStream zos, Path baseDir, Path filePath) { + try { + String entryName = baseDir.relativize(filePath).toString().replace('\\', '/'); + ZipEntry entry = new ZipEntry(entryName); + zos.putNextEntry(entry); + zos.write(Files.readAllBytes(filePath)); + zos.closeEntry(); + log.trace("Added to zip: {}", entryName); + } catch (IOException e) { + throw new UncheckedIOException("Failed to add file to zip: " + filePath, e); + } + } + + private void cleanupTempFile(File file) { + if (file != null && file.exists()) { + try { + Files.deleteIfExists(file.toPath()); + log.debug("Cleaned up temp file: {}", file.getPath()); + } catch (IOException e) { + log.warn("Failed to cleanup temp file: {}", file.getPath(), e); + } + } + } + + private void cleanupTempDirectory(Path tempDir) { + if (tempDir != null && Files.exists(tempDir)) { + try { + FileUtils.deleteDirectory(tempDir.toFile()); + log.debug("Cleaned up temp directory: {}", tempDir); + } catch (IOException e) { + log.warn("Failed to cleanup temp directory: {}", tempDir, e); + } + } + } + + private void cleanupTempResources(Path tempDir, Path zipPath) { + cleanupTempDirectory(tempDir); + if (zipPath != null) { + try { + Files.deleteIfExists(zipPath); + log.debug("Cleaned up temp zip file: {}", zipPath); + } catch (IOException e) { + log.warn("Failed to cleanup zip file: {}", zipPath, e); + } + } + } +} From d2c77feb62eb783e78667328acae6c02e67cf9ed Mon Sep 17 00:00:00 2001 From: projectmiluju Date: Wed, 30 Jul 2025 15:06:30 +0900 Subject: [PATCH 23/40] =?UTF-8?q?feat(config):=20=ED=83=80=EC=9E=84?= =?UTF-8?q?=EC=95=84=EC=9B=83=20=EC=8B=9C=EA=B0=84=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?DP-73?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../deepwebide_be/sandbox/config/RestTemplateConfig.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/deepdirect/deepwebide_be/sandbox/config/RestTemplateConfig.java b/src/main/java/com/deepdirect/deepwebide_be/sandbox/config/RestTemplateConfig.java index 0374b360..86a71101 100644 --- a/src/main/java/com/deepdirect/deepwebide_be/sandbox/config/RestTemplateConfig.java +++ b/src/main/java/com/deepdirect/deepwebide_be/sandbox/config/RestTemplateConfig.java @@ -13,8 +13,8 @@ public class RestTemplateConfig { @Bean public RestTemplate restTemplate(RestTemplateBuilder builder) { return builder - .connectTimeout(Duration.ofSeconds(10)) - .readTimeout(Duration.ofSeconds(30)) + .connectTimeout(Duration.ofSeconds(10)) // 연결 시도 10초 + .readTimeout(Duration.ofMinutes(10)) // 응답 대기 10분 .build(); } } \ No newline at end of file From 7603f9a6342f360013959266571a219fd8074900 Mon Sep 17 00:00:00 2001 From: projectmiluju Date: Wed, 30 Jul 2025 20:07:49 +0900 Subject: [PATCH 24/40] =?UTF-8?q?feat(repository):=20=EC=8B=A4=ED=96=89=20?= =?UTF-8?q?=EC=A4=91=EC=9D=B8=20=EC=BB=A8=ED=85=8C=EC=9D=B4=EB=84=88=20?= =?UTF-8?q?=EC=B6=94=EC=A0=81=EC=9D=84=20=EC=9C=84=ED=95=9C=20RunningConta?= =?UTF-8?q?iner=20=EC=83=9D=EC=84=B1=20DP-73?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/domain/RunningContainer.java | 77 +++++++++++++++++++ .../RunningContainerRepository.java | 32 ++++++++ 2 files changed, 109 insertions(+) create mode 100644 src/main/java/com/deepdirect/deepwebide_be/repository/domain/RunningContainer.java create mode 100644 src/main/java/com/deepdirect/deepwebide_be/repository/repository/RunningContainerRepository.java diff --git a/src/main/java/com/deepdirect/deepwebide_be/repository/domain/RunningContainer.java b/src/main/java/com/deepdirect/deepwebide_be/repository/domain/RunningContainer.java new file mode 100644 index 00000000..fba3d2aa --- /dev/null +++ b/src/main/java/com/deepdirect/deepwebide_be/repository/domain/RunningContainer.java @@ -0,0 +1,77 @@ +package com.deepdirect.deepwebide_be.repository.domain; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "running_containers") +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RunningContainer { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private Long repositoryId; + + @Column(nullable = false) + private String uuid; + + @Column(nullable = false) + private String containerName; + + @Column(nullable = false) + private Integer port; + + @Column(nullable = false) + private String status; + + @Column(nullable = false) + private String framework; + + @Column + private String s3Url; + + @Column(nullable = false) + private LocalDateTime createdAt; + + @Column + private LocalDateTime stoppedAt; + + // PrePersist로 createdAt 자동 설정 + @PrePersist + protected void onCreate() { + if (createdAt == null) { + createdAt = LocalDateTime.now(); + } + } + + // 상태 업데이트 메서드 + public void stop() { + this.status = "STOPPED"; + this.stoppedAt = LocalDateTime.now(); + } + + public void updateStatus(String status) { + this.status = status; + } + + // Builder 패턴을 위한 커스텀 빌더 클래스 + public static class RunningContainerBuilder { + private LocalDateTime createdAt = LocalDateTime.now(); // 기본값 설정 + + public RunningContainerBuilder createdAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + return this; + } + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..3705099a --- /dev/null +++ b/src/main/java/com/deepdirect/deepwebide_be/repository/repository/RunningContainerRepository.java @@ -0,0 +1,32 @@ +package com.deepdirect.deepwebide_be.repository.repository; + +import com.deepdirect.deepwebide_be.repository.domain.RunningContainer; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +@Repository +public interface RunningContainerRepository extends JpaRepository { + + Optional findByRepositoryId(Long repositoryId); + + @Modifying + @Query("DELETE FROM RunningContainer rc WHERE rc.repositoryId = :repositoryId") + void deleteByRepositoryId(@Param("repositoryId") Long repositoryId); + + List findByStatus(String status); + + Optional findByUuid(String uuid); + + @Query("SELECT rc FROM RunningContainer rc WHERE rc.status = 'RUNNING' AND rc.createdAt < :before") + List findStaleContainers(@Param("before") LocalDateTime before); + + @Query("SELECT COUNT(rc) FROM RunningContainer rc WHERE rc.status = 'RUNNING'") + long countRunningContainers(); +} From 1b9ac747325e338c1fc962ace2d9887dc01dbc9d Mon Sep 17 00:00:00 2001 From: projectmiluju Date: Wed, 30 Jul 2025 20:37:53 +0900 Subject: [PATCH 25/40] =?UTF-8?q?feat(repository):=20=EB=A0=88=ED=8F=AC?= =?UTF-8?q?=EC=A7=80=ED=86=A0=EB=A6=AC=20=EC=8B=A4=ED=96=89,=20=EC=A4=91?= =?UTF-8?q?=EC=A7=80,=20=EC=83=81=ED=83=9C=20=EC=A1=B0=ED=9A=8C=EB=A5=BC?= =?UTF-8?q?=20=EC=9C=84=ED=95=9C=20=EC=A0=9C=EC=96=B4=20=EC=97=94=EB=93=9C?= =?UTF-8?q?=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EC=B6=94=EA=B0=80=20DP-73?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/RepositoryRunController.java | 36 ++++ .../service/RepositoryRunService.java | 171 ++++++++++++++++-- .../sandbox/service/SandboxService.java | 51 ++++++ 3 files changed, 247 insertions(+), 11 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 c48a62c3..fb23e2ea 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 @@ -4,6 +4,7 @@ import com.deepdirect.deepwebide_be.global.security.CustomUserDetails; import com.deepdirect.deepwebide_be.repository.dto.response.RepositoryExecuteResponse; import com.deepdirect.deepwebide_be.repository.service.RepositoryRunService; +import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; @@ -11,6 +12,8 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; +import java.util.Map; + @RestController @RequiredArgsConstructor @@ -21,6 +24,7 @@ public class RepositoryRunController { private final RepositoryRunService repositoryRunService; @PostMapping("/{repositoryId}/execute") + @Operation(summary = "레포지토리 실행", description = "레포지토리를 실행하고 실행 결과를 반환합니다.") public ResponseEntity> executeRepository( @AuthenticationPrincipal CustomUserDetails userDetails, @PathVariable Long repositoryId @@ -29,5 +33,37 @@ public ResponseEntity> executeReposito return ResponseEntity.ok(ApiResponseDto.of(200, "레포지토리 실행 요청 완료", resp)); } + /** + * 레포지토리 중지 + */ + @DeleteMapping("/{repositoryId}/stop") + @Operation(summary = "레포지토리 중지", description = "레포지토리를 중지하고 중지 결과를 반환합니다.") + public ResponseEntity>> stopRepository( + @AuthenticationPrincipal CustomUserDetails userDetails, + @PathVariable Long repositoryId + ) { + boolean success = repositoryRunService.stopRepository(repositoryId, userDetails.getId()); + + Map result = Map.of( + "repositoryId", repositoryId, + "stopped", success, + "message", success ? "레포지토리가 중지되었습니다." : "중지할 컨테이너가 없습니다." + ); + + return ResponseEntity.ok(ApiResponseDto.of(200, "레포지토리 중지 요청 완료", result)); + } + + /** + * 레포지토리 실행 상태 조회 + */ + @GetMapping("/{repositoryId}/status") + @Operation(summary = "레포지토리 상태 조회", description = "레포지토리의 실행 상태를 조회합니다.") + public ResponseEntity>> getRepositoryStatus( + @AuthenticationPrincipal CustomUserDetails userDetails, + @PathVariable Long repositoryId + ) { + Map status = repositoryRunService.getRepositoryStatus(repositoryId, userDetails.getId()); + return ResponseEntity.ok(ApiResponseDto.of(200, "레포지토리 상태 조회 완료", status)); + } } 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 f46575d5..1e35612f 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 @@ -6,13 +6,11 @@ import com.deepdirect.deepwebide_be.file.repository.FileNodeRepository; import com.deepdirect.deepwebide_be.global.exception.ErrorCode; import com.deepdirect.deepwebide_be.global.exception.GlobalException; -import com.deepdirect.deepwebide_be.repository.domain.PortRegistry; -import com.deepdirect.deepwebide_be.repository.domain.PortStatus; -import com.deepdirect.deepwebide_be.repository.domain.Repository; -import com.deepdirect.deepwebide_be.repository.domain.RepositoryType; +import com.deepdirect.deepwebide_be.repository.domain.*; import com.deepdirect.deepwebide_be.repository.dto.response.RepositoryExecuteResponse; import com.deepdirect.deepwebide_be.repository.repository.PortRegistryRepository; import com.deepdirect.deepwebide_be.repository.repository.RepositoryRepository; +import com.deepdirect.deepwebide_be.repository.repository.RunningContainerRepository; import com.deepdirect.deepwebide_be.sandbox.dto.request.SandboxExecutionRequest; import com.deepdirect.deepwebide_be.sandbox.dto.response.SandboxExecutionResponse; import com.deepdirect.deepwebide_be.sandbox.service.S3Service; @@ -33,6 +31,8 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.UUID; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -48,6 +48,7 @@ public class RepositoryRunService { private final PortRegistryRepository portRegistryRepository; private final FileNodeRepository fileNodeRepository; private final FileContentRepository fileContentRepository; + private final RunningContainerRepository runningContainerRepository; @Transactional public RepositoryExecuteResponse executeRepository(Long repositoryId, Long userId) { @@ -55,22 +56,27 @@ public RepositoryExecuteResponse executeRepository(Long repositoryId, Long userI File zipFile = null; try { - // 1. 권한 체크 & 레포지토리 조회 + // 1. 기존 실행 중인 컨테이너 중지 요청 + stopExistingContainer(repositoryId); + + // 2. 권한 체크 & 레포지토리 조회 Repository repo = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId) .orElseThrow(() -> new GlobalException(ErrorCode.REPOSITORY_NOT_FOUND)); - // 2. framework, port 등 정보 추출 + // 3. framework, port 등 정보 추출 String framework = convertTypeToFramework(repo.getRepositoryType()); Integer port = allocateOrGetPort(repo); - // 3. 파일트리를 zip으로 변환 + // 4. 새로운 UUID 생성 String uuid = UUID.randomUUID().toString(); + + // 5. 파일트리를 zip으로 변환 zipFile = fileTreeToZip(repositoryId, uuid); - // 4. S3 업로드 + // 6. S3 업로드 String s3Url = uploadToS3(zipFile, uuid); - // 5. 샌드박스 실행 요청 + // 7. 샌드박스 실행 요청 SandboxExecutionRequest request = SandboxExecutionRequest.builder() .uuid(uuid) .url(s3Url) @@ -80,7 +86,10 @@ public RepositoryExecuteResponse executeRepository(Long repositoryId, Long userI SandboxExecutionResponse result = sandboxService.requestExecution(request); - log.info("Repository execution completed - uuid: {}, port: {}", uuid, port); + // 8. 실행 중인 컨테이너 정보 저장 + saveRunningContainer(repositoryId, uuid, "sandbox-" + uuid, port, framework, s3Url); + + log.info("Repository execution completed - repositoryId: {}, uuid: {}, port: {}", repositoryId, uuid, port); return RepositoryExecuteResponse.builder() .uuid(uuid) @@ -101,11 +110,151 @@ public RepositoryExecuteResponse executeRepository(Long repositoryId, Long userI log.error("Repository execution failed - repositoryId: {}, userId: {}", repositoryId, userId, e); throw new GlobalException(ErrorCode.REPOSITORY_EXECUTION_FAILED); } finally { - // 임시 zip 파일 정리 cleanupTempFile(zipFile); } } + /** + * 기존 실행 중인 컨테이너 중지 + */ + private void stopExistingContainer(Long repositoryId) { + runningContainerRepository.findByRepositoryId(repositoryId) + .ifPresent(container -> { + try { + log.info("Stopping existing container for repository {}: {}", repositoryId, container.getUuid()); + + // 샌드박스 서버에 중지 요청 + boolean success = sandboxService.stopContainer(container.getUuid()); + + if (success) { + // 상태 업데이트 + container.stop(); + runningContainerRepository.save(container); + + log.info("Successfully stopped existing container: {}", container.getUuid()); + } else { + log.warn("Failed to stop existing container: {}", container.getUuid()); + } + + // 성공 여부와 관계없이 DB에서 제거 (새로운 컨테이너가 실행될 예정이므로) + runningContainerRepository.deleteByRepositoryId(repositoryId); + + } catch (Exception e) { + log.error("Error while stopping existing container: {}", container.getUuid(), e); + // 에러가 발생해도 DB에서는 제거 + runningContainerRepository.deleteByRepositoryId(repositoryId); + } + }); + } + + /** + * 실행 중인 컨테이너 정보 저장 + */ + @Transactional + public void saveRunningContainer(Long repositoryId, String uuid, String containerName, + Integer port, String framework, String s3Url) { + try { + RunningContainer container = RunningContainer.builder() + .repositoryId(repositoryId) + .uuid(uuid) + .containerName(containerName) + .port(port) + .status("RUNNING") + .framework(framework) + .s3Url(s3Url) + .build(); + + runningContainerRepository.save(container); + log.info("Saved running container info - repositoryId: {}, uuid: {}, port: {}", + repositoryId, uuid, port); + + } catch (Exception e) { + log.error("Failed to save running container info - repositoryId: {}, uuid: {}", + repositoryId, uuid, e); + } + } + + /** + * 레포지토리 중지 (수동) + */ + @Transactional + public boolean stopRepository(Long repositoryId, Long userId) { + try { + // 권한 체크 + repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId) + .orElseThrow(() -> new GlobalException(ErrorCode.REPOSITORY_NOT_FOUND)); + + Optional containerOpt = runningContainerRepository.findByRepositoryId(repositoryId); + + if (containerOpt.isEmpty()) { + log.info("No running container found for repository: {}", repositoryId); + return false; + } + + RunningContainer container = containerOpt.get(); + + // 샌드박스 서버에 중지 요청 + boolean success = sandboxService.stopContainer(container.getUuid()); + + if (success) { + container.stop(); + runningContainerRepository.save(container); + log.info("Successfully stopped repository: {} (uuid: {})", repositoryId, container.getUuid()); + } + + return success; + + } catch (Exception e) { + log.error("Failed to stop repository: {}", repositoryId, e); + return false; + } + } + + /** + * 레포지토리 상태 조회 + */ + public Map getRepositoryStatus(Long repositoryId, Long userId) { + try { + // 권한 체크 + repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId) + .orElseThrow(() -> new GlobalException(ErrorCode.REPOSITORY_NOT_FOUND)); + + Optional containerOpt = runningContainerRepository.findByRepositoryId(repositoryId); + + if (containerOpt.isEmpty()) { + return Map.of( + "repositoryId", repositoryId, + "status", "NOT_RUNNING", + "message", "No running container found" + ); + } + + RunningContainer container = containerOpt.get(); + + // 샌드박스 서버에서 실제 상태 조회 + Map sandboxStatus = sandboxService.getContainerStatus(container.getUuid()); + + return Map.of( + "repositoryId", repositoryId, + "uuid", container.getUuid(), + "containerName", container.getContainerName(), + "port", container.getPort(), + "framework", container.getFramework(), + "createdAt", container.getCreatedAt(), + "dbStatus", container.getStatus(), + "sandboxStatus", sandboxStatus + ); + + } catch (Exception e) { + log.error("Failed to get repository status: {}", repositoryId, e); + return Map.of( + "repositoryId", repositoryId, + "status", "ERROR", + "error", e.getMessage() + ); + } + } + private String convertTypeToFramework(RepositoryType type) { return switch (type) { case SPRING_BOOT -> "spring"; diff --git a/src/main/java/com/deepdirect/deepwebide_be/sandbox/service/SandboxService.java b/src/main/java/com/deepdirect/deepwebide_be/sandbox/service/SandboxService.java index b9e05ae8..6cd94052 100644 --- a/src/main/java/com/deepdirect/deepwebide_be/sandbox/service/SandboxService.java +++ b/src/main/java/com/deepdirect/deepwebide_be/sandbox/service/SandboxService.java @@ -11,6 +11,8 @@ import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; +import java.util.Map; + @Slf4j @Service @RequiredArgsConstructor @@ -55,4 +57,53 @@ private HttpHeaders createHeaders() { headers.add("User-Agent", "DeepWebIDE-Backend"); return headers; } + + + /** + * 컨테이너 중지 요청 + */ + public boolean stopContainer(String uuid) { + try { + String url = sandboxBaseUrl + "/api/sandbox/stop/" + uuid; + + log.info("Sending stop request to sandbox server - uuid: {}, url: {}", uuid, url); + + // DELETE 요청으로 컨테이너 중지 + restTemplate.delete(url); + + log.info("Successfully sent stop request for container: {}", uuid); + return true; + + } catch (RestClientException e) { + log.error("Failed to send stop request for container: {}", uuid, e); + return false; + } catch (Exception e) { + log.error("Unexpected error while stopping container: {}", uuid, e); + return false; + } + } + + /** + * 컨테이너 상태 조회 + */ + public Map getContainerStatus(String uuid) { + try { + String url = sandboxBaseUrl + "/api/sandbox/status/" + uuid; + + log.debug("Checking container status - uuid: {}, url: {}", uuid, url); + + Map response = restTemplate.getForObject(url, Map.class); + + log.debug("Container status response: {}", response); + return response; + + } catch (RestClientException e) { + log.error("Failed to get container status: {}", uuid, e); + return Map.of( + "uuid", uuid, + "status", "ERROR", + "error", e.getMessage() + ); + } + } } \ No newline at end of file From 50517d3cbcad882400c1e2705cefa688946555ef Mon Sep 17 00:00:00 2001 From: projectmiluju Date: Wed, 30 Jul 2025 21:06:30 +0900 Subject: [PATCH 26/40] =?UTF-8?q?feat(repository):=20=EB=A0=88=ED=8F=AC?= =?UTF-8?q?=EC=A7=80=ED=86=A0=EB=A6=AC=20=EC=A4=91=EC=A7=80=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=20=EC=9D=91=EB=8B=B5=20DTO=20DP-73?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/RepositoryStopResponse.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/main/java/com/deepdirect/deepwebide_be/repository/dto/response/RepositoryStopResponse.java diff --git a/src/main/java/com/deepdirect/deepwebide_be/repository/dto/response/RepositoryStopResponse.java b/src/main/java/com/deepdirect/deepwebide_be/repository/dto/response/RepositoryStopResponse.java new file mode 100644 index 00000000..ad765023 --- /dev/null +++ b/src/main/java/com/deepdirect/deepwebide_be/repository/dto/response/RepositoryStopResponse.java @@ -0,0 +1,20 @@ +package com.deepdirect.deepwebide_be.repository.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@Schema(description = "레포지토리 중지 응답 DTO") +public class RepositoryStopResponse { + + @Schema(description = "레포지토리 ID", example = "1") + private Long repositoryId; + + @Schema(description = "중지 성공 여부", example = "true") + private boolean stopped; + + @Schema(description = "중지 메시지", example = "레포지토리가 중지되었습니다.") + private String message; +} \ No newline at end of file From 7ac0a9c7bdec3c0849a1fc118772e3f4d99ddbfb Mon Sep 17 00:00:00 2001 From: projectmiluju Date: Wed, 30 Jul 2025 21:06:57 +0900 Subject: [PATCH 27/40] =?UTF-8?q?feat(repository):=20=EB=A0=88=ED=8F=AC?= =?UTF-8?q?=EC=A7=80=ED=86=A0=EB=A6=AC=20=EC=A4=91=EC=A7=80=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=20=EC=9D=91=EB=8B=B5=20DTO=20=EA=B0=9C=EC=84=A0=20DP-?= =?UTF-8?q?73?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/RepositoryRunController.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 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 fb23e2ea..4583c699 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 @@ -3,6 +3,7 @@ import com.deepdirect.deepwebide_be.global.dto.ApiResponseDto; 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.RepositoryStopResponse; import com.deepdirect.deepwebide_be.repository.service.RepositoryRunService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -38,17 +39,17 @@ public ResponseEntity> executeReposito */ @DeleteMapping("/{repositoryId}/stop") @Operation(summary = "레포지토리 중지", description = "레포지토리를 중지하고 중지 결과를 반환합니다.") - public ResponseEntity>> stopRepository( + public ResponseEntity> stopRepository( @AuthenticationPrincipal CustomUserDetails userDetails, @PathVariable Long repositoryId ) { boolean success = repositoryRunService.stopRepository(repositoryId, userDetails.getId()); - Map result = Map.of( - "repositoryId", repositoryId, - "stopped", success, - "message", success ? "레포지토리가 중지되었습니다." : "중지할 컨테이너가 없습니다." - ); + RepositoryStopResponse result = RepositoryStopResponse.builder() + .repositoryId(repositoryId) + .stopped(success) + .message(success ? "레포지토리가 중지되었습니다." : "중지할 컨테이너가 없습니다.") + .build(); return ResponseEntity.ok(ApiResponseDto.of(200, "레포지토리 중지 요청 완료", result)); } From ac5422096d5e402c6076b3777e2db35f3aa40e14 Mon Sep 17 00:00:00 2001 From: projectmiluju Date: Wed, 30 Jul 2025 21:24:26 +0900 Subject: [PATCH 28/40] =?UTF-8?q?feat(repository):=20=EB=A0=88=ED=8F=AC?= =?UTF-8?q?=EC=A7=80=ED=86=A0=EB=A6=AC=20=EC=83=81=ED=83=9C=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=20=EC=9D=91=EB=8B=B5=20DTO=20=EC=B6=94=EA=B0=80=20DP-?= =?UTF-8?q?73?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../response/RepositoryStatusResponse.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/main/java/com/deepdirect/deepwebide_be/repository/dto/response/RepositoryStatusResponse.java diff --git a/src/main/java/com/deepdirect/deepwebide_be/repository/dto/response/RepositoryStatusResponse.java b/src/main/java/com/deepdirect/deepwebide_be/repository/dto/response/RepositoryStatusResponse.java new file mode 100644 index 00000000..7496c044 --- /dev/null +++ b/src/main/java/com/deepdirect/deepwebide_be/repository/dto/response/RepositoryStatusResponse.java @@ -0,0 +1,38 @@ +package com.deepdirect.deepwebide_be.repository.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; +import java.util.Map; + +@Getter +@Builder +@Schema(description = "레포지토리 실행 상태 응답 DTO") +public class RepositoryStatusResponse { + + @Schema(description = "레포지토리 ID", example = "1") + private Long repositoryId; + + @Schema(description = "실행 중인 컨테이너 UUID", example = "sandbox-1a2b3c4d") + private String uuid; + + @Schema(description = "도커 컨테이너 이름", example = "sandbox-1a2b3c4d") + private String containerName; + + @Schema(description = "할당된 실행 포트", example = "43210") + private Integer port; + + @Schema(description = "레포지토리 프레임워크", example = "spring") + private String framework; + + @Schema(description = "컨테이너 생성 시각", example = "2025-07-30T19:00:00") + private LocalDateTime createdAt; + + @Schema(description = "DB에 기록된 상태 (예: RUNNING, STOPPED)", example = "RUNNING") + private String dbStatus; + + @Schema(description = "샌드박스 서버에서 반환한 상태 정보", example = "{\"state\": \"healthy\", \"uptime\": \"5m\"}") + private Map sandboxStatus; +} \ No newline at end of file From 0b12e545e266cd169b33d4a8b55e9e2976c7ec44 Mon Sep 17 00:00:00 2001 From: projectmiluju Date: Wed, 30 Jul 2025 21:24:36 +0900 Subject: [PATCH 29/40] =?UTF-8?q?feat(repository):=20=EB=A0=88=ED=8F=AC?= =?UTF-8?q?=EC=A7=80=ED=86=A0=EB=A6=AC=20=EC=83=81=ED=83=9C=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=9D=91=EB=8B=B5=20DTO=20=EA=B0=9C=EC=84=A0=20DP-?= =?UTF-8?q?73?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/RepositoryRunController.java | 5 +- .../service/RepositoryRunService.java | 46 +++++++++---------- 2 files changed, 25 insertions(+), 26 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 4583c699..003d058d 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 @@ -3,6 +3,7 @@ import com.deepdirect.deepwebide_be.global.dto.ApiResponseDto; 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; import com.deepdirect.deepwebide_be.repository.dto.response.RepositoryStopResponse; import com.deepdirect.deepwebide_be.repository.service.RepositoryRunService; import io.swagger.v3.oas.annotations.Operation; @@ -59,11 +60,11 @@ public ResponseEntity> stopRepository( */ @GetMapping("/{repositoryId}/status") @Operation(summary = "레포지토리 상태 조회", description = "레포지토리의 실행 상태를 조회합니다.") - public ResponseEntity>> getRepositoryStatus( + public ResponseEntity> getRepositoryStatus( @AuthenticationPrincipal CustomUserDetails userDetails, @PathVariable Long repositoryId ) { - Map status = repositoryRunService.getRepositoryStatus(repositoryId, userDetails.getId()); + RepositoryStatusResponse status = repositoryRunService.getRepositoryStatus(repositoryId, userDetails.getId()); return ResponseEntity.ok(ApiResponseDto.of(200, "레포지토리 상태 조회 완료", status)); } } 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 1e35612f..62bd4640 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 @@ -8,6 +8,7 @@ import com.deepdirect.deepwebide_be.global.exception.GlobalException; import com.deepdirect.deepwebide_be.repository.domain.*; import com.deepdirect.deepwebide_be.repository.dto.response.RepositoryExecuteResponse; +import com.deepdirect.deepwebide_be.repository.dto.response.RepositoryStatusResponse; import com.deepdirect.deepwebide_be.repository.repository.PortRegistryRepository; import com.deepdirect.deepwebide_be.repository.repository.RepositoryRepository; import com.deepdirect.deepwebide_be.repository.repository.RunningContainerRepository; @@ -213,45 +214,42 @@ public boolean stopRepository(Long repositoryId, Long userId) { /** * 레포지토리 상태 조회 */ - public Map getRepositoryStatus(Long repositoryId, Long userId) { + public RepositoryStatusResponse getRepositoryStatus(Long repositoryId, Long userId) { try { - // 권한 체크 repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId) .orElseThrow(() -> new GlobalException(ErrorCode.REPOSITORY_NOT_FOUND)); Optional containerOpt = runningContainerRepository.findByRepositoryId(repositoryId); if (containerOpt.isEmpty()) { - return Map.of( - "repositoryId", repositoryId, - "status", "NOT_RUNNING", - "message", "No running container found" - ); + return RepositoryStatusResponse.builder() + .repositoryId(repositoryId) + .dbStatus("NOT_RUNNING") + .sandboxStatus(Map.of("message", "No running container found")) + .build(); } RunningContainer container = containerOpt.get(); - - // 샌드박스 서버에서 실제 상태 조회 Map sandboxStatus = sandboxService.getContainerStatus(container.getUuid()); - return Map.of( - "repositoryId", repositoryId, - "uuid", container.getUuid(), - "containerName", container.getContainerName(), - "port", container.getPort(), - "framework", container.getFramework(), - "createdAt", container.getCreatedAt(), - "dbStatus", container.getStatus(), - "sandboxStatus", sandboxStatus - ); + return RepositoryStatusResponse.builder() + .repositoryId(repositoryId) + .uuid(container.getUuid()) + .containerName(container.getContainerName()) + .port(container.getPort()) + .framework(container.getFramework()) + .createdAt(container.getCreatedAt()) + .dbStatus(container.getStatus()) + .sandboxStatus(sandboxStatus) + .build(); } catch (Exception e) { log.error("Failed to get repository status: {}", repositoryId, e); - return Map.of( - "repositoryId", repositoryId, - "status", "ERROR", - "error", e.getMessage() - ); + return RepositoryStatusResponse.builder() + .repositoryId(repositoryId) + .dbStatus("ERROR") + .sandboxStatus(Map.of("error", e.getMessage())) + .build(); } } From 0ef16d04f5d85bd0b5259425b3a3880987ee66eb Mon Sep 17 00:00:00 2001 From: projectmiluju Date: Wed, 30 Jul 2025 21:27:57 +0900 Subject: [PATCH 30/40] =?UTF-8?q?chore(repository):=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20import=EB=AC=B8=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20DP-73?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/controller/RepositoryRunController.java | 2 -- 1 file changed, 2 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 003d058d..cf002e6b 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 @@ -14,8 +14,6 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; -import java.util.Map; - @RestController @RequiredArgsConstructor From a6bc2cf1664b8c3807e0fd5c863a3f48f3cdfc9d Mon Sep 17 00:00:00 2001 From: projectmiluju Date: Wed, 30 Jul 2025 22:57:37 +0900 Subject: [PATCH 31/40] =?UTF-8?q?feat(repository):=20=EC=8B=A4=ED=96=89=20?= =?UTF-8?q?=EC=A4=91=20=EB=A1=9C=EA=B7=B8=20=EC=9A=94=EC=B2=AD=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84=20DP-73?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/RepositoryRunController.java | 23 ++++ .../service/RepositoryRunService.java | 125 ++++++++++++++++-- 2 files changed, 140 insertions(+), 8 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 cf002e6b..b032aae3 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,20 +1,28 @@ 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; import com.deepdirect.deepwebide_be.repository.dto.response.RepositoryStopResponse; +import com.deepdirect.deepwebide_be.repository.repository.RepositoryRepository; import com.deepdirect.deepwebide_be.repository.service.RepositoryRunService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; 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; + +@Slf4j @RestController @RequiredArgsConstructor @RequestMapping("/api/repositories") @@ -22,6 +30,7 @@ public class RepositoryRunController { private final RepositoryRunService repositoryRunService; + private final RepositoryRepository repositoryRepository; @PostMapping("/{repositoryId}/execute") @Operation(summary = "레포지토리 실행", description = "레포지토리를 실행하고 실행 결과를 반환합니다.") @@ -65,5 +74,19 @@ public ResponseEntity> getRepositorySta RepositoryStatusResponse status = repositoryRunService.getRepositoryStatus(repositoryId, userDetails.getId()); return ResponseEntity.ok(ApiResponseDto.of(200, "레포지토리 상태 조회 완료", status)); } + + /** + * 레포지토리 실행 로그 조회 + */ + @GetMapping("/{repositoryId}/logs") + public ResponseEntity>> getRepositoryLogs( + @AuthenticationPrincipal CustomUserDetails userDetails, + @PathVariable Long repositoryId, + @RequestParam(defaultValue = "50") int lines, + @RequestParam(defaultValue = "5m") String since) { + + Map logs = repositoryRunService.getRepositoryLogs(repositoryId, userDetails.getId(), lines, since); + return ResponseEntity.ok(ApiResponseDto.of(200, "레포지토리 로그 조회 완료", logs)); + } } 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 62bd4640..3c3d3430 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 @@ -19,22 +19,19 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FileUtils; +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.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.UncheckedIOException; +import java.io.*; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; +import java.util.*; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -50,6 +47,8 @@ public class RepositoryRunService { private final FileNodeRepository fileNodeRepository; private final FileContentRepository fileContentRepository; private final RunningContainerRepository runningContainerRepository; + private final String sandboxBaseUrl = "http://localhost:9090"; + private final RestTemplate restTemplate; @Transactional public RepositoryExecuteResponse executeRepository(Long repositoryId, Long userId) { @@ -431,4 +430,114 @@ private void cleanupTempResources(Path tempDir, Path zipPath) { } } } + + /** + * 레포지토리 로그 조회 + */ + /** + * 레포지토리 로그 조회 (상태 자동 동기화) + */ + public Map getRepositoryLogs(Long repositoryId, Long userId, int lines, String since) { + try { + log.info("Getting repository logs - repositoryId: {}, userId: {}", repositoryId, userId); + + repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId) + .orElseThrow(() -> new GlobalException(ErrorCode.REPOSITORY_NOT_FOUND)); + + Optional containerOpt = runningContainerRepository.findByRepositoryId(repositoryId); + + if (containerOpt.isEmpty()) { + return createNoContainerResponse(repositoryId); + } + + RunningContainer container = containerOpt.get(); + log.info("Found container - uuid: {}, dbStatus: {}", container.getUuid(), container.getStatus()); + + String url = String.format("%s/api/sandbox/logs/%s?lines=%d&since=%s", + sandboxBaseUrl, container.getUuid(), lines, since); + + try { + Map response = restTemplate.getForObject(url, Map.class); + + if (response != null) { + String status = (String) response.get("status"); + + // 컨테이너를 찾을 수 없는 경우 DB 상태 업데이트 + if ("CONTAINER_NOT_FOUND".equals(status)) { + log.warn("Container {} not found, updating DB status", container.getUuid()); + + // DB 상태 자동 동기화 + container.stop(); + runningContainerRepository.save(container); + + Map result = new HashMap<>(response); + result.put("repositoryId", repositoryId); + result.put("dbStatusUpdated", true); + result.put("message", "컨테이너가 존재하지 않아 DB 상태를 업데이트했습니다."); + result.put("containerInfo", createContainerInfo(container, "STOPPED")); + + return result; + } + + // 정상 응답 처리 + Map result = new HashMap<>(response); + result.put("repositoryId", repositoryId); + result.put("requestedUrl", url); + result.put("containerInfo", createContainerInfo(container, container.getStatus())); + + return result; + } + + return createErrorResponse(repositoryId, "Empty response from sandbox server"); + + } catch (Exception httpEx) { + log.error("HTTP request failed - url: {}", url, httpEx); + + // HTTP 오류 시에도 컨테이너 상태 확인 + if (httpEx.getMessage().contains("404") || httpEx.getMessage().contains("Not Found")) { + container.stop(); + runningContainerRepository.save(container); + } + + return createErrorResponse(repositoryId, httpEx.getMessage()); + } + + } catch (Exception e) { + log.error("Failed to get repository logs: {}", repositoryId, e); + return createErrorResponse(repositoryId, e.getMessage()); + } + } + + private Map createContainerInfo(RunningContainer container, String actualStatus) { + return Map.of( + "uuid", container.getUuid(), + "containerName", container.getContainerName(), + "port", container.getPort(), + "framework", container.getFramework(), + "createdAt", container.getCreatedAt(), + "dbStatus", actualStatus + ); + } + + private Map createNoContainerResponse(Long repositoryId) { + return Map.of( + "repositoryId", repositoryId, + "status", "NO_CONTAINER", + "message", "실행 중인 컨테이너 정보가 없습니다.", + "stdout", "", + "stderr", "", + "logs", "" + ); + } + + private Map createErrorResponse(Long repositoryId, String errorMessage) { + return Map.of( + "repositoryId", repositoryId, + "status", "ERROR", + "error", errorMessage, + "stdout", "", + "stderr", "", + "logs", "" + ); + } } From 7d90a6a3fc5dc3ccd0841a7874e15aa17b785c73 Mon Sep 17 00:00:00 2001 From: projectmiluju Date: Wed, 30 Jul 2025 22:59:29 +0900 Subject: [PATCH 32/40] =?UTF-8?q?feat(repository):=20=EA=B0=9C=EC=84=A0?= =?UTF-8?q?=EB=90=9C=20=EB=A0=88=ED=8F=AC=EC=A7=80=ED=86=A0=EB=A6=AC=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EC=A1=B0=ED=9A=8C=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=EB=B0=8F=20=EC=98=A4=EB=A5=98=20=EC=B2=98=EB=A6=AC=20DP-73?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/RepositoryRunService.java | 69 +++++++++++++------ 1 file changed, 47 insertions(+), 22 deletions(-) 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 3c3d3430..d551690b 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 @@ -434,9 +434,6 @@ private void cleanupTempResources(Path tempDir, Path zipPath) { /** * 레포지토리 로그 조회 */ - /** - * 레포지토리 로그 조회 (상태 자동 동기화) - */ public Map getRepositoryLogs(Long repositoryId, Long userId, int lines, String since) { try { log.info("Getting repository logs - repositoryId: {}, userId: {}", repositoryId, userId); @@ -447,7 +444,10 @@ public Map getRepositoryLogs(Long repositoryId, Long userId, int Optional containerOpt = runningContainerRepository.findByRepositoryId(repositoryId); if (containerOpt.isEmpty()) { - return createNoContainerResponse(repositoryId); + return Map.of( + "port", null, + "logs", "실행 중인 컨테이너가 없습니다." + ); } RunningContainer container = containerOpt.get(); @@ -466,29 +466,27 @@ public Map getRepositoryLogs(Long repositoryId, Long userId, int if ("CONTAINER_NOT_FOUND".equals(status)) { log.warn("Container {} not found, updating DB status", container.getUuid()); - // DB 상태 자동 동기화 container.stop(); runningContainerRepository.save(container); - Map result = new HashMap<>(response); - result.put("repositoryId", repositoryId); - result.put("dbStatusUpdated", true); - result.put("message", "컨테이너가 존재하지 않아 DB 상태를 업데이트했습니다."); - result.put("containerInfo", createContainerInfo(container, "STOPPED")); - - return result; + return Map.of( + "port", null, + "logs", "컨테이너가 존재하지 않아 중지되었습니다." + ); } - // 정상 응답 처리 - Map result = new HashMap<>(response); - result.put("repositoryId", repositoryId); - result.put("requestedUrl", url); - result.put("containerInfo", createContainerInfo(container, container.getStatus())); - - return result; + // 정상 응답 - 포트와 로그만 반환 + String logs = extractLogs(response); + return Map.of( + "port", container.getPort(), + "logs", logs + ); } - return createErrorResponse(repositoryId, "Empty response from sandbox server"); + return Map.of( + "port", container.getPort(), + "logs", "로그를 가져올 수 없습니다." + ); } catch (Exception httpEx) { log.error("HTTP request failed - url: {}", url, httpEx); @@ -499,13 +497,40 @@ public Map getRepositoryLogs(Long repositoryId, Long userId, int runningContainerRepository.save(container); } - return createErrorResponse(repositoryId, httpEx.getMessage()); + return Map.of( + "port", container.getPort(), + "logs", "로그 조회 중 오류가 발생했습니다: " + httpEx.getMessage() + ); } } catch (Exception e) { log.error("Failed to get repository logs: {}", repositoryId, e); - return createErrorResponse(repositoryId, e.getMessage()); + return Map.of( + "port", null, + "logs", "오류가 발생했습니다: " + e.getMessage() + ); + } + } + + private String extractLogs(Map response) { + StringBuilder combinedLogs = new StringBuilder(); + + String stdout = (String) response.get("stdout"); + String stderr = (String) response.get("stderr"); + + if (stderr != null && !stderr.trim().isEmpty()) { + combinedLogs.append(stderr.trim()); } + + if (stdout != null && !stdout.trim().isEmpty()) { + if (combinedLogs.length() > 0) { + combinedLogs.append("\n"); + } + combinedLogs.append(stdout.trim()); + } + + String result = combinedLogs.toString(); + return result.isEmpty() ? "로그가 없습니다." : result; } private Map createContainerInfo(RunningContainer container, String actualStatus) { From 231d370d63d95c22d0df159456dbcfdf92f4dda9 Mon Sep 17 00:00:00 2001 From: projectmiluju Date: Wed, 30 Jul 2025 23:33:58 +0900 Subject: [PATCH 33/40] =?UTF-8?q?feat(repository):=20Swagger=20Operation?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20DP-73?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/controller/RepositoryRunController.java | 1 + 1 file changed, 1 insertion(+) 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 b032aae3..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 @@ -79,6 +79,7 @@ public ResponseEntity> getRepositorySta * 레포지토리 실행 로그 조회 */ @GetMapping("/{repositoryId}/logs") + @Operation(summary = "레포지토리 로그 조회", description = "레포지토리의 실행 로그를 조회합니다.") public ResponseEntity>> getRepositoryLogs( @AuthenticationPrincipal CustomUserDetails userDetails, @PathVariable Long repositoryId, From 5995a02aea645cbf415b5fa630b85566bd4963d7 Mon Sep 17 00:00:00 2001 From: sunsetkk Date: Thu, 31 Jul 2025 01:36:35 +0900 Subject: [PATCH 34/40] =?UTF-8?q?feature(chat):=20=EC=B1=84=ED=8C=85=20?= =?UTF-8?q?=EC=9D=BD=EC=9D=8C=20=EC=B2=98=EB=A6=AC=EC=9A=A9=20Controller?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20DP-157?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ChatReadOffsetController.java | 29 +++++++++++++++++++ .../dto/request/ChatReadOffsetRequest.java | 2 ++ .../repository/ChatReadOffsetRepository.java | 2 ++ .../chat/service/ChatReadOffsetService.java | 2 ++ 4 files changed, 35 insertions(+) create mode 100644 src/main/java/com/deepdirect/deepwebide_be/chat/controller/ChatReadOffsetController.java create mode 100644 src/main/java/com/deepdirect/deepwebide_be/chat/dto/request/ChatReadOffsetRequest.java create mode 100644 src/main/java/com/deepdirect/deepwebide_be/chat/repository/ChatReadOffsetRepository.java create mode 100644 src/main/java/com/deepdirect/deepwebide_be/chat/service/ChatReadOffsetService.java diff --git a/src/main/java/com/deepdirect/deepwebide_be/chat/controller/ChatReadOffsetController.java b/src/main/java/com/deepdirect/deepwebide_be/chat/controller/ChatReadOffsetController.java new file mode 100644 index 00000000..118c67a0 --- /dev/null +++ b/src/main/java/com/deepdirect/deepwebide_be/chat/controller/ChatReadOffsetController.java @@ -0,0 +1,29 @@ +package com.deepdirect.deepwebide_be.chat.controller; + +import com.deepdirect.deepwebide_be.chat.dto.request.ChatReadOffsetRequest; +import com.deepdirect.deepwebide_be.chat.service.ChatReadOffsetService; +import com.deepdirect.deepwebide_be.global.dto.ApiResponseDto; +import com.deepdirect.deepwebide_be.global.security.CustomUserDetails; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/repositories/{repositoryId}/chat") +public class ChatReadOffsetController { + + private final ChatReadOffsetService chatReadOffsetService; + + @PutMapping("/read-offset") + public ResponseEntity> saveReadOffset( + @PathVariable Long repositoryId, + @Valid @RequestBody ChatReadOffsetRequest request, + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + chatReadOffsetService.saveOffset(repositoryId, userDetails.getId(), request.getLastReadMessageId()); + return ResponseEntity.ok(ApiResponseDto.of(200, "읽음 처리 완료", null)); + } +} diff --git a/src/main/java/com/deepdirect/deepwebide_be/chat/dto/request/ChatReadOffsetRequest.java b/src/main/java/com/deepdirect/deepwebide_be/chat/dto/request/ChatReadOffsetRequest.java new file mode 100644 index 00000000..15ce1130 --- /dev/null +++ b/src/main/java/com/deepdirect/deepwebide_be/chat/dto/request/ChatReadOffsetRequest.java @@ -0,0 +1,2 @@ +package com.deepdirect.deepwebide_be.chat.dto.request;public class ChatReadOffsetRequest { +} diff --git a/src/main/java/com/deepdirect/deepwebide_be/chat/repository/ChatReadOffsetRepository.java b/src/main/java/com/deepdirect/deepwebide_be/chat/repository/ChatReadOffsetRepository.java new file mode 100644 index 00000000..f7f92256 --- /dev/null +++ b/src/main/java/com/deepdirect/deepwebide_be/chat/repository/ChatReadOffsetRepository.java @@ -0,0 +1,2 @@ +package com.deepdirect.deepwebide_be.chat.repository;public class ChatReadOffsetRepository { +} diff --git a/src/main/java/com/deepdirect/deepwebide_be/chat/service/ChatReadOffsetService.java b/src/main/java/com/deepdirect/deepwebide_be/chat/service/ChatReadOffsetService.java new file mode 100644 index 00000000..dd5d9f0d --- /dev/null +++ b/src/main/java/com/deepdirect/deepwebide_be/chat/service/ChatReadOffsetService.java @@ -0,0 +1,2 @@ +package com.deepdirect.deepwebide_be.chat.service;public class ChatReadOffsetService { +} From 4d074b80bdf9ddd20d3a7fb62e6a831ebbd7873b Mon Sep 17 00:00:00 2001 From: sunsetkk Date: Thu, 31 Jul 2025 01:37:49 +0900 Subject: [PATCH 35/40] =?UTF-8?q?refactor(chat):=20ChatReadOffset=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=88=98=EC=A0=95=20DP-157?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/domain/ChatReadOffset.java | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/deepdirect/deepwebide_be/chat/domain/ChatReadOffset.java b/src/main/java/com/deepdirect/deepwebide_be/chat/domain/ChatReadOffset.java index b83a9fcf..3d57846a 100644 --- a/src/main/java/com/deepdirect/deepwebide_be/chat/domain/ChatReadOffset.java +++ b/src/main/java/com/deepdirect/deepwebide_be/chat/domain/ChatReadOffset.java @@ -4,6 +4,7 @@ import com.deepdirect.deepwebide_be.repository.domain.Repository; import jakarta.persistence.*; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -26,10 +27,11 @@ public class ChatReadOffset { @JoinColumn(name = "user_id", nullable = false) private User user; - @Column(name = "last_read_message_id", nullable = false) - private Long lastReadMessageId; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "last_read_message_id", nullable = false) + private ChatMessage lastReadMessage; - @Column(name = "updated_at") + @Column(name = "updated_at", nullable = false) private LocalDateTime updatedAt; @PrePersist @@ -37,4 +39,26 @@ public class ChatReadOffset { public void setUpdatedAt() { this.updatedAt = LocalDateTime.now(); } + + + @Builder + private ChatReadOffset(Repository repository, User user, ChatMessage lastReadMessage) { + this.repository = repository; + this.user = user; + this.lastReadMessage = lastReadMessage; + this.updatedAt = LocalDateTime.now(); + } + + public static ChatReadOffset of(Repository repository, User user, ChatMessage message) { + return ChatReadOffset.builder() + .repository(repository) + .user(user) + .lastReadMessage(message) + .build(); + } + + public void update(ChatMessage newMessage) { + this.lastReadMessage = newMessage; + this.updatedAt = LocalDateTime.now(); + } } From 7bdaa0a66d361f58020a1dda44930a01bebfe827 Mon Sep 17 00:00:00 2001 From: sunsetkk Date: Thu, 31 Jul 2025 01:38:13 +0900 Subject: [PATCH 36/40] =?UTF-8?q?chore:=20ErrorCode=EC=97=90=20=EC=B1=84?= =?UTF-8?q?=ED=8C=85=20=EC=9D=BD=EC=9D=8C=20=EC=B2=98=EB=A6=AC=EC=9A=A9=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=ED=95=AD=EB=AA=A9=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?DP-157?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/deepdirect/deepwebide_be/global/exception/ErrorCode.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/deepdirect/deepwebide_be/global/exception/ErrorCode.java b/src/main/java/com/deepdirect/deepwebide_be/global/exception/ErrorCode.java index f92846cc..e7d748a9 100644 --- a/src/main/java/com/deepdirect/deepwebide_be/global/exception/ErrorCode.java +++ b/src/main/java/com/deepdirect/deepwebide_be/global/exception/ErrorCode.java @@ -57,6 +57,7 @@ public enum ErrorCode { VERIFICATION_NOT_FOUND(HttpStatus.NOT_FOUND, "인증 요청 기록이 없습니다."), REPOSITORY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않은 레포지토리 입니다."), ENTRY_CODE_NOT_FOUND(HttpStatus.NOT_FOUND, "입장 코드를 찾을 수 없습니다."), + CHAT_MESSAGE_NOT_FOUND(HttpStatus.NOT_FOUND, "채팅 메시지를 찾을 수 없습니다."), // 409 CONFLICT From 11ca39c07bec9c2fceb37b499d64a6eda15ba497 Mon Sep 17 00:00:00 2001 From: sunsetkk Date: Thu, 31 Jul 2025 01:39:35 +0900 Subject: [PATCH 37/40] =?UTF-8?q?feature(chat):=20ChatReadOffsetRepository?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=20DP-157?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/repository/ChatReadOffsetRepository.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/deepdirect/deepwebide_be/chat/repository/ChatReadOffsetRepository.java b/src/main/java/com/deepdirect/deepwebide_be/chat/repository/ChatReadOffsetRepository.java index f7f92256..9239fd97 100644 --- a/src/main/java/com/deepdirect/deepwebide_be/chat/repository/ChatReadOffsetRepository.java +++ b/src/main/java/com/deepdirect/deepwebide_be/chat/repository/ChatReadOffsetRepository.java @@ -1,2 +1,11 @@ -package com.deepdirect.deepwebide_be.chat.repository;public class ChatReadOffsetRepository { +package com.deepdirect.deepwebide_be.chat.repository; + +import com.deepdirect.deepwebide_be.chat.domain.ChatReadOffset; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface ChatReadOffsetRepository extends JpaRepository { + + Optional findByRepositoryIdAndUserId(Long repositoryId, Long userId); } From 6c360e2916fb1e602078244512eac8a0ff55b977 Mon Sep 17 00:00:00 2001 From: sunsetkk Date: Thu, 31 Jul 2025 01:39:57 +0900 Subject: [PATCH 38/40] =?UTF-8?q?feature(chat):=20ChatReadOffsetService=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20DP-157?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/service/ChatReadOffsetService.java | 50 ++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/deepdirect/deepwebide_be/chat/service/ChatReadOffsetService.java b/src/main/java/com/deepdirect/deepwebide_be/chat/service/ChatReadOffsetService.java index dd5d9f0d..d6e41e89 100644 --- a/src/main/java/com/deepdirect/deepwebide_be/chat/service/ChatReadOffsetService.java +++ b/src/main/java/com/deepdirect/deepwebide_be/chat/service/ChatReadOffsetService.java @@ -1,2 +1,50 @@ -package com.deepdirect.deepwebide_be.chat.service;public class ChatReadOffsetService { +package com.deepdirect.deepwebide_be.chat.service; + +import com.deepdirect.deepwebide_be.chat.domain.ChatMessage; +import com.deepdirect.deepwebide_be.chat.domain.ChatReadOffset; +import com.deepdirect.deepwebide_be.chat.repository.ChatMessageRepository; +import com.deepdirect.deepwebide_be.chat.repository.ChatReadOffsetRepository; +import com.deepdirect.deepwebide_be.global.exception.ErrorCode; +import com.deepdirect.deepwebide_be.global.exception.GlobalException; +import com.deepdirect.deepwebide_be.member.domain.User; +import com.deepdirect.deepwebide_be.member.repository.UserRepository; +import com.deepdirect.deepwebide_be.repository.domain.Repository; +import com.deepdirect.deepwebide_be.repository.repository.RepositoryRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class ChatReadOffsetService { + + private final ChatReadOffsetRepository chatReadOffsetRepository; + private final RepositoryRepository repositoryRepository; + private final UserRepository userRepository; + private final ChatMessageRepository chatMessageRepository; + + @Transactional + public void saveOffset(Long repositoryId, Long userId, Long messageId) { + Repository repository = repositoryRepository.findById(repositoryId) + .orElseThrow(() -> new GlobalException(ErrorCode.REPOSITORY_NOT_FOUND)); + + User user = userRepository.findById(userId) + .orElseThrow(() -> new GlobalException(ErrorCode.USER_NOT_FOUND)); + + ChatMessage message = chatMessageRepository.findById(messageId) + .orElseThrow(() -> new GlobalException(ErrorCode.CHAT_MESSAGE_NOT_FOUND)); + + chatReadOffsetRepository.findByRepositoryIdAndUserId(repositoryId, userId) + .ifPresentOrElse( + offset -> { + if (message.getId() > offset.getLastReadMessage().getId()) { + offset.update(message); + } + }, + () -> { + ChatReadOffset newOffset = ChatReadOffset.of(repository, user, message); + chatReadOffsetRepository.save(newOffset); + } + ); + } } From 0b224de02daf0a70a99850ce32b7f23212f031d4 Mon Sep 17 00:00:00 2001 From: sunsetkk Date: Thu, 31 Jul 2025 01:40:43 +0900 Subject: [PATCH 39/40] =?UTF-8?q?feature(chat):=20=EC=9D=BD=EC=9D=8C=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EC=9A=94=EC=B2=AD=20DTO=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EC=88=98=EC=A0=95=20DP-157?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/ChatReadOffsetRequest.java | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/deepdirect/deepwebide_be/chat/dto/request/ChatReadOffsetRequest.java b/src/main/java/com/deepdirect/deepwebide_be/chat/dto/request/ChatReadOffsetRequest.java index 15ce1130..c2860601 100644 --- a/src/main/java/com/deepdirect/deepwebide_be/chat/dto/request/ChatReadOffsetRequest.java +++ b/src/main/java/com/deepdirect/deepwebide_be/chat/dto/request/ChatReadOffsetRequest.java @@ -1,2 +1,22 @@ -package com.deepdirect.deepwebide_be.chat.dto.request;public class ChatReadOffsetRequest { +package com.deepdirect.deepwebide_be.chat.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@Schema(description = "채팅 읽음 위치 저장 요청") +public class ChatReadOffsetRequest { + + @NotNull + @Schema(description = "마지막으로 읽은 메시지 ID", example = "152") + private Long lastReadMessageId; + + @Builder + public ChatReadOffsetRequest(Long lastReadMessageId) { + this.lastReadMessageId = lastReadMessageId; + } } From c56ef2937d317eaf83f671290bfc7bbefbde243b Mon Sep 17 00:00:00 2001 From: sunsetkk Date: Thu, 31 Jul 2025 01:46:22 +0900 Subject: [PATCH 40/40] =?UTF-8?q?fix(chat):=20=EC=9D=BD=EC=9D=8C=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20API=20HTTP=20Method=EB=A5=BC=20PUT=20?= =?UTF-8?q?=E2=86=92=20PATCH=EB=A1=9C=20=EC=88=98=EC=A0=95=20DP-157?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../deepwebide_be/chat/controller/ChatReadOffsetController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/deepdirect/deepwebide_be/chat/controller/ChatReadOffsetController.java b/src/main/java/com/deepdirect/deepwebide_be/chat/controller/ChatReadOffsetController.java index 118c67a0..850710e3 100644 --- a/src/main/java/com/deepdirect/deepwebide_be/chat/controller/ChatReadOffsetController.java +++ b/src/main/java/com/deepdirect/deepwebide_be/chat/controller/ChatReadOffsetController.java @@ -17,7 +17,7 @@ public class ChatReadOffsetController { private final ChatReadOffsetService chatReadOffsetService; - @PutMapping("/read-offset") + @PatchMapping("/read-offset") public ResponseEntity> saveReadOffset( @PathVariable Long repositoryId, @Valid @RequestBody ChatReadOffsetRequest request,