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 3657712..b9d9390 100644 --- a/src/main/java/com/teamEWSN/gitdeun/common/fastapi/FastApiClient.java +++ b/src/main/java/com/teamEWSN/gitdeun/common/fastapi/FastApiClient.java @@ -1,5 +1,6 @@ package com.teamEWSN.gitdeun.common.fastapi; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.teamEWSN.gitdeun.common.converter.IsoToLocalDateTimeDeserializer; @@ -249,7 +250,7 @@ public SuggestionAutoResponse createAutoSuggestions( } } - public String getFileRaw(String repoUrl, String filePath, String authHeader) { +/* public String getFileRaw(String repoUrl, String filePath, String authHeader) { return getFileRaw(repoUrl, filePath, null, null, null, authHeader); } @@ -309,52 +310,50 @@ public String getFileRaw(String repoUrl, String filePath, Integer startLine, Int log.error("FastAPI 파일 내용 조회 실패 - repoId: {}, filePath: {}", repoId, filePath, e); return ""; // 빈 문자열 반환 (null 대신) } - } + }*/ - public String getFileRawFromNode(String nodeKey, String filePath, String authHeader) { + public String getCodeFromNode(String nodeKey, String filePath, String authHeader) { + String fileName = extractFileName(filePath); try { - log.debug("FastAPI 파일 내용 조회 시작 - nodeKey: {}, filePath: {}", nodeKey, filePath); + log.debug("FastAPI 노드 기반 코드 조회 시작 - nodeKey: {}, filePath: {}", nodeKey, fileName); - // URI 생성 (쿼리 파라미터 포함) UriComponentsBuilder uriBuilder = UriComponentsBuilder - .fromPath("/content/file/nodes") + .fromPath("/content/file/by-node") // FastAPI의 해당 엔드포인트 .queryParam("node_key", nodeKey) - .queryParam("file_path", filePath); + .queryParam("file_path", fileName); + // 'prefer' 파라미터는 생략하여 FastAPI의 기본값(auto)을 따르도록 함 String uri = uriBuilder.build().toUriString(); - String response = webClient.get() + NodeCodeResponse 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) + .onStatus(HttpStatusCode::is4xxClientError, clientResponse -> + clientResponse.bodyToMono(String.class) + .map(errorBody -> { + log.warn("FastAPI 노드 코드 조회 4xx 오류 - nodeKey: {}, fileName: {}, status: {}, body: {}", + nodeKey, fileName, clientResponse.statusCode(), errorBody); + return new RuntimeException("FastAPI 클라이언트 오류: " + errorBody); + }) + ) + .bodyToMono(NodeCodeResponse.class) .timeout(Duration.ofSeconds(30)) .block(); - log.debug("FastAPI 파일 내용 조회 완료 - nodeKey: {}, filePath: {}, 길이: {}", - nodeKey, filePath, response != null ? response.length() : 0); + String codeContent = (response != null) ? response.getCode() : ""; + log.debug("FastAPI 노드 기반 코드 조회 성공 - nodeKey: {}, fileName: {}, 길이: {}", + nodeKey, fileName, codeContent != null ? codeContent.length() : 0); - return response != null ? response : ""; + return codeContent; } catch (Exception e) { - log.error("FastAPI 파일 내용 조회 실패 - nodeKey: {}, filePath: {}", nodeKey, filePath, e); - return ""; // 빈 문자열 반환 (null 대신) + log.error("FastAPI 노드 기반 코드 조회 실패 - nodeKey: {}, fileName: {}", nodeKey, fileName, e); + return ""; // 예외 발생 시 빈 문자열 반환 } } @@ -366,6 +365,11 @@ private String extractMapId(String repoUrl) { return segments[segments.length - 1].replaceAll("\\.git$", ""); } + // 파일명 추출 + public String extractFileName(String filePath) { + return filePath.substring(filePath.lastIndexOf('/') + 1); + } + private AnalysisResultDto buildAnalysisResultDto( RepoInfoResponse repoInfo ) { @@ -388,6 +392,13 @@ private AnalysisResultDto buildRefreshResultDto( // === Response DTOs === + @Getter + @Setter + @JsonIgnoreProperties(ignoreUnknown = true) // code 필드 외 다른 필드는 무시 + private static class NodeCodeResponse { + private String code; + } + @Getter @Setter public static class FetchResponse { 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 0533137..6367c1c 100644 --- a/src/main/java/com/teamEWSN/gitdeun/mindmap/util/FileContentCache.java +++ b/src/main/java/com/teamEWSN/gitdeun/mindmap/util/FileContentCache.java @@ -29,12 +29,14 @@ public class FileContentCache { @Component public static class FileContentL1Cache { - @Cacheable(value = "FILE_CONTENT_L1", key = "#key") + @Cacheable(value = "FILE_CONTENT_L1", key = "#key", + unless = "#result == null || #result.isEmpty()") public String getFromL1Cache(String key) { - return null; + return null; // 미스면 null } - @CachePut(value = "FILE_CONTENT_L1", key = "#key") + @CachePut(value = "FILE_CONTENT_L1", key = "#key", + condition = "#content != null && !#content.isBlank()") public String cacheToL1(String key, String content) { return content; } @@ -46,7 +48,6 @@ public void evictL1Cache() { 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 캐시 확인 @@ -59,23 +60,27 @@ public String getFileContentWithCacheFromNode(String repoUrl, String nodeKey, St // 2. L2 캐시 확인 try { content = (String) redisTemplate.opsForValue().get(cacheKey); - if (content != null) { + if (content != null && !content.isBlank()) { log.debug("파일 내용 L2 캐시 히트 - key: {}", cacheKey); l1Cache.cacheToL1(cacheKey, content); // L1에 저장 return content; + } else if (content != null && content.isBlank()) { + log.warn("L2 캐시에서 빈 문자열 발견 - key: {} (전파/재적재 하지 않음)", cacheKey); + // 캐시에 남겨두지 말고 즉시 삭제 + try { redisTemplate.delete(cacheKey); } catch (Exception ignore) {} } } catch (Exception e) { log.warn("Redis 조회 실패, API 직접 호출 - key: {}", cacheKey, e); } - // 3. FastAPI 실시간 조회 - content = fastApiClient.getFileRawFromNode(nodeKey, filePath, authHeader); + // 3. FastAPI 실시간 조회 (새로 만든 메서드를 호출하도록 변경) + content = fastApiClient.getCodeFromNode(nodeKey, filePath, authHeader); // 4. L1, L2 캐시에 저장 - if (content != null) { + if (content != null && !content.isBlank()) { l1Cache.cacheToL1(cacheKey, content); try { - redisTemplate.opsForValue().set(cacheKey, content, Duration.ofHours(2)); // L2: 2시간 + redisTemplate.opsForValue().set(cacheKey, content, Duration.ofHours(2)); } catch (Exception e) { log.warn("Redis 저장 실패 - key: {}", cacheKey, e); } @@ -88,7 +93,7 @@ public String getFileContentWithCacheFromNode(String repoUrl, String nodeKey, St public void evictFileCacheForRepo(String repoUrl) { // L1 캐시는 전체 삭제 l1Cache.evictL1Cache(); - deleteRedisKeysByPattern("file-content:repo:" + repoUrl + ":*"); + deleteRedisKeysByPattern("file-content:" + repoUrl + ":*"); } private void deleteRedisKeysByPattern(String pattern) {