Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
479aa24
feat(aws): S3 presigned URL 및 AWS SDK v2 적용 기능 추가 DP-73
projectmiluju Jul 23, 2025
fe3654f
feat(aws): S3 클라이언트 설정을 위한 설정 클래스 추가 DP-73
projectmiluju Jul 23, 2025
f7ce0a0
feat(sandbox): S3Service 클래스 추가 및 파일 업로드 기능 구현 DP-73
projectmiluju Jul 23, 2025
92d03df
feat(sandbox): 파일 실행을 위한 SandboxController 및 SandboxService 추가 DP-73
projectmiluju Jul 23, 2025
4020e98
Merge branch 'develop' into feature/DP-73-sandbox-controller
projectmiluju Jul 23, 2025
b43f0fe
feat(dependencies): AWS SDK S3 version 2.31.78으로 변경 DP-73
projectmiluju Jul 23, 2025
868e28e
feat(security): 허용된 경로에 /api/projects/** 추가 DP-73
projectmiluju Jul 23, 2025
4c181f1
refactor(controller): 사용하지 않는 import문 제거 DP-73
projectmiluju Jul 23, 2025
00f35d9
feat(controller): 샌드박스 로그 확인을 위한 컨트롤러 구현 DP-73
projectmiluju Jul 23, 2025
10ba374
Merge branch 'develop' into feature/DP-73-sandbox-controller
projectmiluju Jul 29, 2025
8b8453c
feat(s3): 중복된 기능의 s3config 삭제 DP-73
projectmiluju Jul 29, 2025
f7c9f83
feat(build): spring-test dependency 추가 DP-73
projectmiluju Jul 29, 2025
4648591
feat(build): Apache Commons IO dependency 추가 DP-73
projectmiluju Jul 29, 2025
14877ae
feat(port): port getter 적용 DP-73
projectmiluju Jul 29, 2025
9564053
feat(repository): Repository와 status로 PortRegistry 찾는 메서드 추가 DP-73
projectmiluju Jul 29, 2025
a721580
feat(error): 추가된 에러 코드로 레포지토리 관련 오류 처리 개선 DP-73
projectmiluju Jul 29, 2025
5d57ae6
feat(exception): SandboxException을 위한 handler 생성 DP-73
projectmiluju Jul 29, 2025
c1c7fd5
feat(port): 레포지토리 할당 리팩토링 및 해제 메서드 추가 DP-73
projectmiluju Jul 29, 2025
c280b9f
feat(sandbox): 샌드박스 실행 요청 및 응답 DTO 추가 DP-73
projectmiluju Jul 29, 2025
a5e1dc9
feat(repository): assignToRepository 메서드를 사용하도록 사용 가능한 포트 할당 로직 리팩터링 …
projectmiluju Jul 29, 2025
f839aab
feat(sandbox): 프로젝트 실행 API 요청·응답 구조를 DTO로 개선 DP-73
projectmiluju Jul 29, 2025
c9058a9
feat(exception): 샌드박스 관련 예외 처리를 위한 SandboxException 클래스 생성 DP-73
projectmiluju Jul 29, 2025
2e2dd33
feat(config): RestTemplate 설정을 위한 RestTemplateConfig 클래스 추가 DP-73
projectmiluju Jul 29, 2025
3b6e788
feat(repository): 레포지토리 실행을 위한 RepositoryRunController 및 RepositoryRu…
projectmiluju Jul 29, 2025
a023652
Merge branch 'develop' into feature/DP-73-sandbox-controller
projectmiluju Jul 30, 2025
d2c77fe
feat(config): 타임아웃 시간 변경 DP-73
projectmiluju Jul 30, 2025
b9de046
Merge branch 'develop' into feature/DP-73-sandbox-controller
projectmiluju Jul 30, 2025
7603f9a
feat(repository): 실행 중인 컨테이너 추적을 위한 RunningContainer 생성 DP-73
projectmiluju Jul 30, 2025
1b9ac74
feat(repository): 레포지토리 실행, 중지, 상태 조회를 위한 제어 엔드포인트 추가 DP-73
projectmiluju Jul 30, 2025
50517d3
feat(repository): 레포지토리 중지 요청 응답 DTO DP-73
projectmiluju Jul 30, 2025
7ac0a9c
feat(repository): 레포지토리 중지 요청 응답 DTO 개선 DP-73
projectmiluju Jul 30, 2025
ac54220
feat(repository): 레포지토리 상태 요청 응답 DTO 추가 DP-73
projectmiluju Jul 30, 2025
0b12e54
feat(repository): 레포지토리 상태 조회 응답 DTO 개선 DP-73
projectmiluju Jul 30, 2025
0ef16d0
chore(repository): 사용하지 않는 import문 제거 DP-73
projectmiluju Jul 30, 2025
f0ef197
Merge branch 'develop' into feature/DP-73-sandbox-controller
projectmiluju Jul 30, 2025
a6bc2cf
feat(repository): 실행 중 로그 요청 기능 구현 DP-73
projectmiluju Jul 30, 2025
7d90a6a
feat(repository): 개선된 레포지토리 로그 조회 응답 및 오류 처리 DP-73
projectmiluju Jul 30, 2025
231d370
feat(repository): Swagger Operation 추가 DP-73
projectmiluju Jul 30, 2025
5995a02
feature(chat): 채팅 읽음 처리용 Controller 구현 DP-157
sunsetkk Jul 30, 2025
4d074b8
refactor(chat): ChatReadOffset 엔티티 수정 DP-157
sunsetkk Jul 30, 2025
7bdaa0a
chore: ErrorCode에 채팅 읽음 처리용 예외 항목 추가 DP-157
sunsetkk Jul 30, 2025
11ca39c
feature(chat): ChatReadOffsetRepository 생성 DP-157
sunsetkk Jul 30, 2025
6c360e2
feature(chat): ChatReadOffsetService 구현 DP-157
sunsetkk Jul 30, 2025
0b224de
feature(chat): 읽음 처리 요청 DTO 추가 및 수정 DP-157
sunsetkk Jul 30, 2025
c56ef29
fix(chat): 읽음 처리 API HTTP Method를 PUT → PATCH로 수정 DP-157
sunsetkk Jul 30, 2025
9eaa062
Merge pull request #161 from DeepDirect/feature/DP-73-sandbox-controller
projectmiluju Jul 30, 2025
b030320
Merge pull request #163 from DeepDirect/feature/DP-157-chat-read-offset
projectmiluju Jul 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,11 @@ dependencies {
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'

// AWS S3 (파일 업로드 및 다운로드를 위한 라이브러리)
// AWS SDK v2 - S3 + presigned URL
implementation 'software.amazon.awssdk:s3:2.31.78'
implementation 'com.amazonaws:aws-java-sdk-s3:1.12.673'


// 이메일 발송 기능
implementation 'org.springframework.boot:spring-boot-starter-mail'

Expand All @@ -72,7 +75,11 @@ 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'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

@PatchMapping("/read-offset")
public ResponseEntity<ApiResponseDto<Void>> 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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -26,15 +27,38 @@ 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
@PreUpdate
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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
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<ChatReadOffset, Long> {

Optional<ChatReadOffset> findByRepositoryIdAndUserId(Long repositoryId, Long userId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
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);
}
);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.deepdirect.deepwebide_be.global.exception;

import lombok.Getter;
import org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties;
import org.springframework.http.HttpStatus;

@Getter
Expand Down Expand Up @@ -38,6 +37,10 @@ public enum ErrorCode {
HISTORY_NOT_FOUND(HttpStatus.BAD_REQUEST, "해당 히스토리를 찾을 수 없습니다."),
MISSING_WS_PARAMS(HttpStatus.BAD_REQUEST, "WebSocket 연결에 필요한 Param이 누락되었습니다."),
INVALID_INPUT(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, "인증되지 않았습니다."),
Expand All @@ -57,6 +60,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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -75,6 +76,13 @@ public ResponseEntity<ApiResponseDto<?>> handleOtherExceptions(Exception ex, Htt
.body(ApiResponseDto.error(500, responseMessage));
}

@ExceptionHandler(SandboxException.class)
public ResponseEntity<ApiResponseDto<Void>> 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()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.cors.CorsConfiguration;
Expand Down Expand Up @@ -91,9 +90,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
new AntPathRequestMatcher("/redis-test"),
new AntPathRequestMatcher("/test/**"), // Sentry 테스트용
new AntPathRequestMatcher("/ws/**"),
new AntPathRequestMatcher("/test/**"), // Sentry 테스트용
new AntPathRequestMatcher("/login/oauth2/**"),
new AntPathRequestMatcher("/oauth2/**")
new AntPathRequestMatcher("/oauth2/**"),
new AntPathRequestMatcher("/api/projects/**")
).permitAll()
.requestMatchers(new AntPathRequestMatcher("/api/auth/signout")).authenticated()
.anyRequest().authenticated()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
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")
@Tag(name = "RUN", description = "레포지토리 실행, 로그 반환 등 기능 API")
public class RepositoryRunController {

private final RepositoryRunService repositoryRunService;
private final RepositoryRepository repositoryRepository;

@PostMapping("/{repositoryId}/execute")
@Operation(summary = "레포지토리 실행", description = "레포지토리를 실행하고 실행 결과를 반환합니다.")
public ResponseEntity<ApiResponseDto<RepositoryExecuteResponse>> executeRepository(
@AuthenticationPrincipal CustomUserDetails userDetails,
@PathVariable Long repositoryId
) {
RepositoryExecuteResponse resp = repositoryRunService.executeRepository(repositoryId, userDetails.getId());
return ResponseEntity.ok(ApiResponseDto.of(200, "레포지토리 실행 요청 완료", resp));
}

/**
* 레포지토리 중지
*/
@DeleteMapping("/{repositoryId}/stop")
@Operation(summary = "레포지토리 중지", description = "레포지토리를 중지하고 중지 결과를 반환합니다.")
public ResponseEntity<ApiResponseDto<RepositoryStopResponse>> stopRepository(
@AuthenticationPrincipal CustomUserDetails userDetails,
@PathVariable Long repositoryId
) {
boolean success = repositoryRunService.stopRepository(repositoryId, userDetails.getId());

RepositoryStopResponse result = RepositoryStopResponse.builder()
.repositoryId(repositoryId)
.stopped(success)
.message(success ? "레포지토리가 중지되었습니다." : "중지할 컨테이너가 없습니다.")
.build();

return ResponseEntity.ok(ApiResponseDto.of(200, "레포지토리 중지 요청 완료", result));
}

/**
* 레포지토리 실행 상태 조회
*/
@GetMapping("/{repositoryId}/status")
@Operation(summary = "레포지토리 상태 조회", description = "레포지토리의 실행 상태를 조회합니다.")
public ResponseEntity<ApiResponseDto<RepositoryStatusResponse>> getRepositoryStatus(
@AuthenticationPrincipal CustomUserDetails userDetails,
@PathVariable Long repositoryId
) {
RepositoryStatusResponse status = repositoryRunService.getRepositoryStatus(repositoryId, userDetails.getId());
return ResponseEntity.ok(ApiResponseDto.of(200, "레포지토리 상태 조회 완료", status));
}

/**
* 레포지토리 실행 로그 조회
*/
@GetMapping("/{repositoryId}/logs")
@Operation(summary = "레포지토리 로그 조회", description = "레포지토리의 실행 로그를 조회합니다.")
public ResponseEntity<ApiResponseDto<Map<String, Object>>> getRepositoryLogs(
@AuthenticationPrincipal CustomUserDetails userDetails,
@PathVariable Long repositoryId,
@RequestParam(defaultValue = "50") int lines,
@RequestParam(defaultValue = "5m") String since) {

Map<String, Object> logs = repositoryRunService.getRepositoryLogs(repositoryId, userDetails.getId(), lines, since);
return ResponseEntity.ok(ApiResponseDto.of(200, "레포지토리 로그 조회 완료", logs));
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,26 @@ 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() {
return this.port;
}
}

Loading