diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 80f5ed2..fd7a9a4 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -48,16 +48,29 @@ jobs: key: ${{ secrets.EC2_PRIVATE_KEY }} port: 22 script: | - set -eux + set -euo pipefail + # 대상 디렉터리 보장 + mkdir -p ${{ secrets.EC2_TARGET_PATH }} + + # 멀티라인 .env 안전 저장 - 옵션 A(Heredoc) 예시 + cat > ${{ secrets.EC2_TARGET_PATH }}/.env <<'ENV_EOF'${{ secrets.ENV_FILE }}ENV_EOF + chmod 600 ${{ secrets.EC2_TARGET_PATH }}/.env + # Docker Hub 로그인 echo "${{ secrets.DOCKERHUB_TOKEN }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin - # 최신 이미지 가져오기 + + # 네트워크 준비 docker network create gitdeun-network || true + # 최신 커밋 SHA로 배포(불변 태그) IMAGE="${{ secrets.DOCKER_USERNAME }}/gitdeun:${{ github.sha }}" docker pull "$IMAGE" + + # 기존 컨테이너 중지/삭제 docker stop gitdeun || true docker rm gitdeun || true + + # 실행 docker run -d \ --name gitdeun \ --restart unless-stopped \ diff --git a/src/main/java/com/teamEWSN/gitdeun/codereference/dto/CodeReferenceResponseDtos.java b/src/main/java/com/teamEWSN/gitdeun/codereference/dto/CodeReferenceResponseDtos.java index 16b7450..e82576f 100644 --- a/src/main/java/com/teamEWSN/gitdeun/codereference/dto/CodeReferenceResponseDtos.java +++ b/src/main/java/com/teamEWSN/gitdeun/codereference/dto/CodeReferenceResponseDtos.java @@ -2,6 +2,8 @@ import lombok.*; +import java.util.List; + public class CodeReferenceResponseDtos { @Getter @@ -25,5 +27,6 @@ public static class ReferenceDetailResponse { private Integer startLine; private Integer endLine; private String codeContent; // 실제 코드 내용을 담을 필드 + private List reviewIds; } } \ No newline at end of file diff --git a/src/main/java/com/teamEWSN/gitdeun/codereference/mapper/CodeReferenceMapper.java b/src/main/java/com/teamEWSN/gitdeun/codereference/mapper/CodeReferenceMapper.java index 1642413..deeaf01 100644 --- a/src/main/java/com/teamEWSN/gitdeun/codereference/mapper/CodeReferenceMapper.java +++ b/src/main/java/com/teamEWSN/gitdeun/codereference/mapper/CodeReferenceMapper.java @@ -2,11 +2,15 @@ import com.teamEWSN.gitdeun.codereference.dto.CodeReferenceResponseDtos.*; import com.teamEWSN.gitdeun.codereference.entity.CodeReference; +import com.teamEWSN.gitdeun.codereview.entity.CodeReview; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.Named; import org.mapstruct.ReportingPolicy; +import java.util.List; +import java.util.stream.Collectors; + @Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE) public interface CodeReferenceMapper { @@ -22,6 +26,7 @@ public interface CodeReferenceMapper { @Mapping(target = "startLine", source = "codeReference.startLine") @Mapping(target = "endLine", source = "codeReference.endLine") @Mapping(target = "codeContent", expression = "java(codeContent)") + @Mapping(target = "reviewIds", source = "codeReference.codeReviews", qualifiedByName = "reviewsToIds") ReferenceDetailResponse toReferenceDetailResponse(CodeReference codeReference, String codeContent); /** @@ -32,4 +37,12 @@ default String extractFileName(String filePath) { if (filePath == null) return null; return filePath.substring(filePath.lastIndexOf('/') + 1); } + + @Named("reviewsToIds") + default List reviewsToIds(List reviews) { + if (reviews == null) { + return null; + } + return reviews.stream().map(CodeReview::getId).collect(Collectors.toList()); + } } \ No newline at end of file diff --git a/src/main/java/com/teamEWSN/gitdeun/codereference/service/CodeReferenceService.java b/src/main/java/com/teamEWSN/gitdeun/codereference/service/CodeReferenceService.java index 1b8fd98..f664f66 100644 --- a/src/main/java/com/teamEWSN/gitdeun/codereference/service/CodeReferenceService.java +++ b/src/main/java/com/teamEWSN/gitdeun/codereference/service/CodeReferenceService.java @@ -8,7 +8,6 @@ import com.teamEWSN.gitdeun.codereference.repository.CodeReferenceRepository; import com.teamEWSN.gitdeun.common.exception.ErrorCode; import com.teamEWSN.gitdeun.common.exception.GlobalException; -import com.teamEWSN.gitdeun.common.fastapi.FastApiClient; import com.teamEWSN.gitdeun.common.fastapi.dto.NodeDto; import com.teamEWSN.gitdeun.mindmap.dto.MindmapGraphResponseDto; import com.teamEWSN.gitdeun.mindmap.entity.Mindmap; @@ -16,6 +15,7 @@ import com.teamEWSN.gitdeun.mindmap.util.FileContentCache; import com.teamEWSN.gitdeun.mindmap.util.MindmapGraphCache; import com.teamEWSN.gitdeun.mindmapmember.service.MindmapAuthService; +import com.teamEWSN.gitdeun.repo.entity.Repo; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -34,7 +34,6 @@ public class CodeReferenceService { private final CodeReferenceMapper codeReferenceMapper; private final MindmapGraphCache mindmapGraphCache; private final FileContentCache fileContentCache; - private final FastApiClient fastApiClient; @Transactional public ReferenceResponse createReference(Long mapId, String nodeKey, Long userId, CreateRequest request, String authorizationHeader) throws GlobalException { @@ -72,10 +71,18 @@ public ReferenceDetailResponse getReferenceDetail(Long mapId, Long refId, Long u CodeReference codeReference = codeReferenceRepository.findByMindmapIdAndId(mapId, refId) .orElseThrow(() -> new GlobalException(ErrorCode.CODE_REFERENCE_NOT_FOUND)); - Mindmap mindmap = codeReference.getMindmap(); + Repo repo = codeReference.getMindmap().getRepo(); + String repoUrl = repo.getGithubRepoUrl(); + LocalDateTime lastCommit = repo.getLastCommit(); - // FastAPI를 통해 전체 파일 내용 가져오기 - String fullContent = fastApiClient.getFileRaw(mindmap.getRepo().getGithubRepoUrl(), codeReference.getFilePath(), authorizationHeader); + // FastAPI를 통해 전체 파일 내용 가져오기 (수정된 메서드 호출) + String fullContent = fileContentCache.getFileContentWithCacheFromNode( + repoUrl, + codeReference.getNodeKey(), + codeReference.getFilePath(), + lastCommit, + authorizationHeader + ); // 특정 라인만 추출 (snippet) String snippet = extractLines(fullContent, codeReference.getStartLine(), codeReference.getEndLine()); @@ -166,7 +173,13 @@ private void validateCodeReferenceRequest(Mindmap mindmap, String nodeKey, Creat } // 파일의 전체 내용을 가져와 총 라인 수 계산 - String fileContent = fileContentCache.getFileContentWithCache(repoUrl, request.getFilePath(), lastCommit, authorizationHeader); + String fileContent = fileContentCache.getFileContentWithCacheFromNode( + repoUrl, + nodeKey, + request.getFilePath(), + lastCommit, + authorizationHeader + ); int totalLines = fileContent.split("\\r?\\n").length; // 요청된 라인 범위(startLine, endLine)가 유효한지 확인 diff --git a/src/main/java/com/teamEWSN/gitdeun/common/fastapi/FastApiClient.java b/src/main/java/com/teamEWSN/gitdeun/common/fastapi/FastApiClient.java index 8605715..3657712 100644 --- a/src/main/java/com/teamEWSN/gitdeun/common/fastapi/FastApiClient.java +++ b/src/main/java/com/teamEWSN/gitdeun/common/fastapi/FastApiClient.java @@ -311,6 +311,53 @@ public String getFileRaw(String repoUrl, String filePath, Integer startLine, Int } } + public String getFileRawFromNode(String nodeKey, String filePath, String authHeader) { + try { + log.debug("FastAPI 파일 내용 조회 시작 - nodeKey: {}, filePath: {}", nodeKey, filePath); + + // URI 생성 (쿼리 파라미터 포함) + UriComponentsBuilder uriBuilder = UriComponentsBuilder + .fromPath("/content/file/nodes") + .queryParam("node_key", nodeKey) + .queryParam("file_path", filePath); + + String uri = uriBuilder.build().toUriString(); + + String response = webClient.get() + .uri(uri) + .headers(headers -> { + if (authHeader != null && !authHeader.trim().isEmpty()) { + headers.set("Authorization", authHeader); + } + headers.set("Accept", "text/plain"); + }) + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError, clientResponse -> { + log.warn("FastAPI 파일 조회 4xx 오류 - nodeKey: {}, filePath: {}, status: {}", + nodeKey, filePath, clientResponse.statusCode()); + return clientResponse.bodyToMono(String.class) + .map(errorBody -> new RuntimeException("파일을 찾을 수 없습니다: " + errorBody)); + }) + .onStatus(HttpStatusCode::is5xxServerError, serverResponse -> { + log.error("FastAPI 파일 조회 5xx 오류 - nodeKey: {}, filePath: {}, status: {}", + nodeKey, filePath, serverResponse.statusCode()); + return Mono.error(new RuntimeException("FastAPI 서버 오류")); + }) + .bodyToMono(String.class) + .timeout(Duration.ofSeconds(30)) + .block(); + + log.debug("FastAPI 파일 내용 조회 완료 - nodeKey: {}, filePath: {}, 길이: {}", + nodeKey, filePath, response != null ? response.length() : 0); + + return response != null ? response : ""; + + } catch (Exception e) { + log.error("FastAPI 파일 내용 조회 실패 - nodeKey: {}, filePath: {}", nodeKey, filePath, e); + return ""; // 빈 문자열 반환 (null 대신) + } + } + // === Helper Methods === // 저장소명 추출 diff --git a/src/main/java/com/teamEWSN/gitdeun/mindmap/service/MindmapOrchestrationService.java b/src/main/java/com/teamEWSN/gitdeun/mindmap/service/MindmapOrchestrationService.java index 43aea6d..b7ba820 100644 --- a/src/main/java/com/teamEWSN/gitdeun/mindmap/service/MindmapOrchestrationService.java +++ b/src/main/java/com/teamEWSN/gitdeun/mindmap/service/MindmapOrchestrationService.java @@ -151,6 +151,9 @@ public void promptMindmap(Long mapId, String prompt, Long userId, String authHea String repoUrl = mindmap.getRepo().getGithubRepoUrl(); + // FastAPI 분석 요청 전, 관련된 모든 파일 캐시를 무효화 + fileContentCache.evictFileCacheForRepo(repoUrl); + // FastAPI에 자동 분석 요청 SuggestionAutoResponse suggestionResponse = fastApiClient.createAutoSuggestions(repoUrl, prompt, authHeader); diff --git a/src/main/java/com/teamEWSN/gitdeun/mindmap/service/NodeService.java b/src/main/java/com/teamEWSN/gitdeun/mindmap/service/NodeService.java index cb6fa82..c896dba 100644 --- a/src/main/java/com/teamEWSN/gitdeun/mindmap/service/NodeService.java +++ b/src/main/java/com/teamEWSN/gitdeun/mindmap/service/NodeService.java @@ -83,7 +83,7 @@ public NodeCodeResponseDto getNodeDetailsWithCode(Long mapId, String nodeKey, Lo Map codeContentsMap = filePaths.parallelStream() .collect(Collectors.toConcurrentMap( filePath -> filePath, - filePath -> fileContentCache.getFileContentWithCache(repoUrl, filePath.getFilePath(), lastCommit, authorizationHeader) + filePath -> fileContentCache.getFileContentWithCacheFromNode(repoUrl, nodeKey, filePath.getFilePath(), lastCommit, authorizationHeader) )); // 4. Map을 List 형태로 변환 diff --git a/src/main/java/com/teamEWSN/gitdeun/mindmap/util/FileContentCache.java b/src/main/java/com/teamEWSN/gitdeun/mindmap/util/FileContentCache.java index df6cd4b..0533137 100644 --- a/src/main/java/com/teamEWSN/gitdeun/mindmap/util/FileContentCache.java +++ b/src/main/java/com/teamEWSN/gitdeun/mindmap/util/FileContentCache.java @@ -44,8 +44,10 @@ public void evictL1Cache() { } } - public String getFileContentWithCache(String repoUrl, String filePath, LocalDateTime lastCommit, String authHeader) { - String cacheKey = "file-content:" + repoUrl + ":" + filePath + ":" + lastCommit.toString(); + + public String getFileContentWithCacheFromNode(String repoUrl, String nodeKey, String filePath, LocalDateTime lastCommit, String authHeader) { + // 캐시 키에 repoUrl을 포함시켜 어떤 리포지토리의 캐시인지 명확히 합니다. + String cacheKey = "file-content:" + repoUrl + ":node:" + nodeKey + ":" + filePath + ":" + lastCommit.toString(); // 1. L1 캐시 확인 String content = l1Cache.getFromL1Cache(cacheKey); @@ -67,7 +69,7 @@ public String getFileContentWithCache(String repoUrl, String filePath, LocalDate } // 3. FastAPI 실시간 조회 - content = fastApiClient.getFileRaw(repoUrl, filePath, authHeader); + content = fastApiClient.getFileRawFromNode(nodeKey, filePath, authHeader); // 4. L1, L2 캐시에 저장 if (content != null) { @@ -81,20 +83,19 @@ public String getFileContentWithCache(String repoUrl, String filePath, LocalDate return content; } + // 캐시 무효화 public void evictFileCacheForRepo(String repoUrl) { // L1 캐시는 전체 삭제 l1Cache.evictL1Cache(); + deleteRedisKeysByPattern("file-content:repo:" + repoUrl + ":*"); + } - // L2 캐시는 패턴으로 검색하여 삭제 - // L2 캐시(Redis)는 SCAN을 사용하여 안전하게 삭제 - String pattern = "file-content:" + repoUrl + ":*"; + private void deleteRedisKeysByPattern(String pattern) { try { Set keysToDelete = redisTemplate.execute((RedisCallback>) connection -> { Set keys = new HashSet<>(); - try (Cursor cursor = connection.keyCommands().scan(ScanOptions.scanOptions().match(pattern).count(100).build())) { - while (cursor.hasNext()) { keys.add(new String(cursor.next(), StandardCharsets.UTF_8)); }