diff --git a/src/main/java/com/teamEWSN/gitdeun/common/converter/IsoToLocalDateTimeDeserializer.java b/src/main/java/com/teamEWSN/gitdeun/common/converter/IsoToLocalDateTimeDeserializer.java new file mode 100644 index 0000000..b30761c --- /dev/null +++ b/src/main/java/com/teamEWSN/gitdeun/common/converter/IsoToLocalDateTimeDeserializer.java @@ -0,0 +1,28 @@ +package com.teamEWSN.gitdeun.common.converter; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.*; +import java.io.IOException; +import java.time.*; +import java.time.format.DateTimeParseException; + +public class IsoToLocalDateTimeDeserializer extends JsonDeserializer { + @Override + public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + String s = p.getText(); + if (s == null || s.isBlank()) return null; + + // 1) 오프셋/Z 포함 → UTC LDT로 + try { + return OffsetDateTime.parse(s).atZoneSameInstant(ZoneOffset.UTC).toLocalDateTime(); + } catch (DateTimeParseException ignored) {} + + // 2) Instant 순수 파싱 시도 + try { + return Instant.parse(s).atZone(ZoneOffset.UTC).toLocalDateTime(); + } catch (DateTimeParseException ignored) {} + + // 3) 오프셋 없는 LocalDateTime 포맷(레거시) 방어 + return LocalDateTime.parse(s); + } +} 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 263db94..558957d 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,8 @@ package com.teamEWSN.gitdeun.common.fastapi; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.teamEWSN.gitdeun.common.converter.IsoToLocalDateTimeDeserializer; import com.teamEWSN.gitdeun.common.fastapi.dto.AnalysisResultDto; import com.teamEWSN.gitdeun.common.fastapi.dto.MindmapGraphDto; import lombok.Getter; @@ -42,6 +45,8 @@ public AnalysisResultDto analyzeResult(String repoUrl, String prompt, String aut log.info("Fetch 완료 - 파일: {}, 파싱: {}", fetchResult.getFiles_saved(), fetchResult.getFiles_parsed()); + saveRepoInfo(mapId, authorizationHeader); + // Step 2: 마인드맵 기본 분석 (AI) AnalyzeResponse analyzeResult = analyzeAI(repoUrl, prompt, authorizationHeader); log.info("AI 분석 완료 - 디렉터리: {}", analyzeResult.getDirs_analyzed()); @@ -55,7 +60,7 @@ public AnalysisResultDto analyzeResult(String repoUrl, String prompt, String aut RepoInfoResponse repoInfo = getRepoInfo(mapId, authorizationHeader); // 모든 데이터를 종합하여 DTO 생성 - return buildAnalysisResultDto(mapId, fetchResult, analyzeResult, graphData, repoInfo); + return buildAnalysisResultDto(mapId, analyzeResult, graphData, repoInfo); } catch (Exception e) { log.error("저장소 분석 실패: {}", e.getMessage(), e); @@ -96,7 +101,7 @@ public AnalysisResultDto refreshMindmap(String repoUrl, String prompt, String au } // 저장소 파일 fetch - private FetchResponse fetchRepoInfo(String repoUrl, String authHeader) { + public FetchResponse fetchRepoInfo(String repoUrl, String authHeader) { Map request = new HashMap<>(); request.put("repo_url", repoUrl); @@ -124,14 +129,23 @@ public void saveRepoInfo(String repoUrl, String authHeader) { .block(); } - // 저장소 정보 조회 (TODO: FastAPI에 구현 필요) - private RepoInfoResponse getRepoInfo(String mapId, String authHeader) { - // TODO: FastAPI에 /repo/{mapId}/info 엔드포인트 구현 필요 - // 현재는 기본값 반환 - RepoInfoResponse info = new RepoInfoResponse(); - info.setDefault_branch("main"); - info.setLast_updated_at(LocalDateTime.now()); - return info; + // 저장소 정보 조회 + public RepoInfoResponse getRepoInfo(String mapId, String authHeader) { + try { + return webClient.get() + .uri("/repo/{mapId}/info", mapId) + .header("Authorization", authHeader) + .retrieve() + .bodyToMono(RepoInfoResponse.class) + .block(); + } catch (Exception e) { + log.warn("저장소 정보 조회 실패: {}, 기본값 사용", e.getMessage()); + // 조회 실패 시 기본값 반환 + RepoInfoResponse info = new RepoInfoResponse(); + info.setDefaultBranch("main"); + info.setLastCommit(LocalDateTime.now()); + return info; + } } /** @@ -158,7 +172,7 @@ public AnalyzeResponse analyzeAI(String repoUrl, String prompt, String authHeade /** * ArangoDB에서 마인드맵 그래프 데이터를 조회 */ - private MindmapGraphDto getGraph(String mapId, String authHeader) { + public MindmapGraphDto getGraph(String mapId, String authHeader) { return webClient.get() .uri("/mindmap/{mapId}/graph", mapId) .header("Authorization", authHeader) @@ -168,7 +182,7 @@ private MindmapGraphDto getGraph(String mapId, String authHeader) { } // 최신 변경사항 새로고침 - private RefreshResponse refreshLatest(String mapId, String prompt, String authHeader) { + public RefreshResponse refreshLatest(String mapId, String prompt, String authHeader) { Map request = new HashMap<>(); if (StringUtils.hasText(prompt)) { request.put("prompt", prompt); @@ -212,7 +226,6 @@ private String extractMapId(String repoUrl) { private AnalysisResultDto buildAnalysisResultDto( String mapId, - FetchResponse fetchResult, AnalyzeResponse analyzeResult, MindmapGraphDto graphData, RepoInfoResponse repoInfo @@ -221,13 +234,13 @@ private AnalysisResultDto buildAnalysisResultDto( String mapDataJson = convertGraphToJson(graphData); // AI가 생성한 제목 또는 기본 제목 + // TODO: 제목 생성 fastapi 메서드 필요 String title = String.format("%s 프로젝트 구조 분석 (디렉터리 %d개)", mapId, analyzeResult.getDirs_analyzed()); return AnalysisResultDto.builder() - .defaultBranch(repoInfo.getDefault_branch()) - .githubLastUpdatedAt(repoInfo.getLast_updated_at()) - .mapData(mapDataJson) + .defaultBranch(repoInfo.getDefaultBranch()) + .lastCommit(repoInfo.getLastCommit()) .title(title) .errorMessage(null) .build(); @@ -240,13 +253,14 @@ private AnalysisResultDto buildRefreshResultDto( RepoInfoResponse repoInfo ) { String mapDataJson = convertGraphToJson(graphData); + + // TODO: 제목 생성 fastapi 메서드 필요 String title = String.format("%s (변경 파일 %d개 반영)", mapId, refreshResult.getChanged_files()); return AnalysisResultDto.builder() - .defaultBranch(repoInfo.getDefault_branch()) - .githubLastUpdatedAt(repoInfo.getLast_updated_at()) - .mapData(mapDataJson) + .defaultBranch(repoInfo.getDefaultBranch()) + .lastCommit(repoInfo.getLastCommit()) .title(title) .errorMessage(null) .build(); @@ -295,8 +309,14 @@ public static class RefreshResponse { @Getter @Setter public static class RepoInfoResponse { - private String default_branch; - private LocalDateTime last_updated_at; + @JsonProperty("default_branch") + private String defaultBranch; + + @JsonProperty("last_commit") + @JsonDeserialize(using = IsoToLocalDateTimeDeserializer.class) + private LocalDateTime lastCommit; + + } @Getter @@ -308,4 +328,6 @@ public static class DeleteResponse { private Integer nodes_removed; private Integer recs_removed; } + + } \ No newline at end of file diff --git a/src/main/java/com/teamEWSN/gitdeun/common/fastapi/dto/AnalysisResultDto.java b/src/main/java/com/teamEWSN/gitdeun/common/fastapi/dto/AnalysisResultDto.java index d476c50..a68a057 100644 --- a/src/main/java/com/teamEWSN/gitdeun/common/fastapi/dto/AnalysisResultDto.java +++ b/src/main/java/com/teamEWSN/gitdeun/common/fastapi/dto/AnalysisResultDto.java @@ -10,10 +10,9 @@ public class AnalysisResultDto { // Repo 관련 정보 private String defaultBranch; - private LocalDateTime githubLastUpdatedAt; + private LocalDateTime lastCommit; // Mindmap 관련 정보 - private String mapData; // JSON 형태의 마인드맵 데이터 private String title; // 프롬프트 및 mindmap 정보 요약 private String errorMessage; // 실패 시 전달될 에러메세지 // TODO: FastAPI 응답에 맞춰 필드 정의 diff --git a/src/main/java/com/teamEWSN/gitdeun/common/fastapi/dto/EdgeDto.java b/src/main/java/com/teamEWSN/gitdeun/common/fastapi/dto/EdgeDto.java index e38af6d..cd8085f 100644 --- a/src/main/java/com/teamEWSN/gitdeun/common/fastapi/dto/EdgeDto.java +++ b/src/main/java/com/teamEWSN/gitdeun/common/fastapi/dto/EdgeDto.java @@ -1,9 +1,25 @@ package com.teamEWSN.gitdeun.common.fastapi.dto; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Getter; @Getter public class EdgeDto { - private String from; - private String to; + @JsonProperty("from") + private String fromKey; + + @JsonProperty("to") + private String toKey; + + @JsonProperty("edge_type") + private String edgeType; + + // 편의 메서드 + public boolean isContainmentEdge() { + return "contains".equals(edgeType) || edgeType == null; + } + + public boolean isSuggestionEdge() { + return "suggestion".equals(edgeType); + } } diff --git a/src/main/java/com/teamEWSN/gitdeun/common/fastapi/dto/MindmapGraphDto.java b/src/main/java/com/teamEWSN/gitdeun/common/fastapi/dto/MindmapGraphDto.java index 398f2ec..4b55393 100644 --- a/src/main/java/com/teamEWSN/gitdeun/common/fastapi/dto/MindmapGraphDto.java +++ b/src/main/java/com/teamEWSN/gitdeun/common/fastapi/dto/MindmapGraphDto.java @@ -1,11 +1,14 @@ package com.teamEWSN.gitdeun.common.fastapi.dto; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Getter; import java.util.List; @Getter public class MindmapGraphDto { - private int count; + @JsonProperty("map_id") + private String mapId; + private Integer count; private List nodes; private List edges; } diff --git a/src/main/java/com/teamEWSN/gitdeun/common/fastapi/dto/NodeDto.java b/src/main/java/com/teamEWSN/gitdeun/common/fastapi/dto/NodeDto.java index c8fd6cb..c313417 100644 --- a/src/main/java/com/teamEWSN/gitdeun/common/fastapi/dto/NodeDto.java +++ b/src/main/java/com/teamEWSN/gitdeun/common/fastapi/dto/NodeDto.java @@ -1,11 +1,33 @@ package com.teamEWSN.gitdeun.common.fastapi.dto; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Getter; import java.util.List; @Getter public class NodeDto { private String key; + private String label; - private List related_files; + + @JsonProperty("related_files") + private List relatedFiles; + + @JsonProperty("node_type") + private String nodeType; + + private String mode; + + // 편의 메서드 + public boolean isFileNode() { + return "file".equals(nodeType); + } + + public boolean isSuggestionNode() { + return "suggestion".equals(nodeType); + } + + public int getFileCount() { + return relatedFiles != null ? relatedFiles.size() : 0; + } } \ No newline at end of file diff --git a/src/main/java/com/teamEWSN/gitdeun/common/util/CacheType.java b/src/main/java/com/teamEWSN/gitdeun/common/util/CacheType.java index 122740c..31a1456 100644 --- a/src/main/java/com/teamEWSN/gitdeun/common/util/CacheType.java +++ b/src/main/java/com/teamEWSN/gitdeun/common/util/CacheType.java @@ -7,7 +7,9 @@ @RequiredArgsConstructor public enum CacheType { SERVICE_AUTOCOMPLETE("serviceAutocomplete", 2, 1200), // 자동완성 캐시 - USER_SKILLS("userSkills", 1, 500); // 사용자 개발기술 캐시 + USER_SKILLS("userSkills", 1, 500), // 사용자 개발기술 캐시 + MINDMAP_GRAPH_L1("mindmapGraphL1", 100, 1), // 30분, 최대 100개 + VISIT_HISTORY_SUMMARY("visitHistorySummary", 1000, 1); // 1시간, 최대 1000개 private final String cacheName; private final int expiredAfterWrite; // 시간(hour) 단위 diff --git a/src/main/java/com/teamEWSN/gitdeun/common/webhook/dto/WebhookUpdateDto.java b/src/main/java/com/teamEWSN/gitdeun/common/webhook/dto/WebhookUpdateDto.java index b1b7ba9..385750d 100644 --- a/src/main/java/com/teamEWSN/gitdeun/common/webhook/dto/WebhookUpdateDto.java +++ b/src/main/java/com/teamEWSN/gitdeun/common/webhook/dto/WebhookUpdateDto.java @@ -10,7 +10,6 @@ public class WebhookUpdateDto { // FastAPI 콜백 페이로드와 동일 private String repoUrl; - private String mapData; private String defaultBranch; - private LocalDateTime githubLastUpdatedAt; + private LocalDateTime lastCommit; } diff --git a/src/main/java/com/teamEWSN/gitdeun/mindmap/dto/MindmapDetailResponseDto.java b/src/main/java/com/teamEWSN/gitdeun/mindmap/dto/MindmapDetailResponseDto.java index 8251b1c..243f483 100644 --- a/src/main/java/com/teamEWSN/gitdeun/mindmap/dto/MindmapDetailResponseDto.java +++ b/src/main/java/com/teamEWSN/gitdeun/mindmap/dto/MindmapDetailResponseDto.java @@ -15,7 +15,10 @@ public class MindmapDetailResponseDto { private Long mindmapId; private String title; private String branch; - private String mapData; // 핵심 데이터인 마인드맵 JSON + + // 맵 데이터 + private MindmapGraphResponseDto mindmapGraph; + private LocalDateTime createdAt; private LocalDateTime updatedAt; diff --git a/src/main/java/com/teamEWSN/gitdeun/mindmap/dto/MindmapGraphResponseDto.java b/src/main/java/com/teamEWSN/gitdeun/mindmap/dto/MindmapGraphResponseDto.java new file mode 100644 index 0000000..9441a96 --- /dev/null +++ b/src/main/java/com/teamEWSN/gitdeun/mindmap/dto/MindmapGraphResponseDto.java @@ -0,0 +1,23 @@ +package com.teamEWSN.gitdeun.mindmap.dto; + +import com.teamEWSN.gitdeun.common.fastapi.dto.EdgeDto; +import com.teamEWSN.gitdeun.common.fastapi.dto.NodeDto; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +@Builder +public class MindmapGraphResponseDto { + private Boolean success; + private String error; + + // FastAPI 응답 데이터 그대로 전달 + private String graphMapId; + private Integer nodeCount; + private List nodes; + private List edges; +} \ No newline at end of file diff --git a/src/main/java/com/teamEWSN/gitdeun/mindmap/dto/prompt/PromptPreviewResponseDto.java b/src/main/java/com/teamEWSN/gitdeun/mindmap/dto/prompt/PromptPreviewResponseDto.java index 96e7de9..8fe5bbd 100644 --- a/src/main/java/com/teamEWSN/gitdeun/mindmap/dto/prompt/PromptPreviewResponseDto.java +++ b/src/main/java/com/teamEWSN/gitdeun/mindmap/dto/prompt/PromptPreviewResponseDto.java @@ -1,5 +1,6 @@ package com.teamEWSN.gitdeun.mindmap.dto.prompt; +import com.teamEWSN.gitdeun.mindmap.dto.MindmapGraphResponseDto; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -13,7 +14,10 @@ public class PromptPreviewResponseDto { private Long historyId; private String prompt; private String title; - private String previewMapData; // 미리보기용 맵 데이터 + + // TODO: 프롬프트 미리보기용 맵 데이터 + private MindmapGraphResponseDto mindmapGraph; + private LocalDateTime createdAt; private Boolean applied; // 현재 적용 상태 } \ No newline at end of file diff --git a/src/main/java/com/teamEWSN/gitdeun/mindmap/entity/Mindmap.java b/src/main/java/com/teamEWSN/gitdeun/mindmap/entity/Mindmap.java index 2170254..0db7322 100644 --- a/src/main/java/com/teamEWSN/gitdeun/mindmap/entity/Mindmap.java +++ b/src/main/java/com/teamEWSN/gitdeun/mindmap/entity/Mindmap.java @@ -49,9 +49,9 @@ public class Mindmap extends AuditedEntity { @Column(length = 50, nullable = false) private String title; - @JdbcTypeCode(SqlTypes.JSON) + /*@JdbcTypeCode(SqlTypes.JSON) @Column(name = "map_data", columnDefinition = "json", nullable = false) - private String mapData; + private String mapData;*/ // TODO: 멤버수 제한 기능 (유료?) @Builder.Default @@ -66,10 +66,6 @@ public class Mindmap extends AuditedEntity { private List promptHistories = new ArrayList<>(); - public void updateMapData(String newMapData) { - this.mapData = newMapData; - } - public void updateTitle(String newTitle) { this.title = newTitle; } @@ -97,7 +93,6 @@ public void applyPromptHistory(PromptHistory promptHistory) { // 새 프롬프트 적용 promptHistory.applyToMindmap(); - this.mapData = promptHistory.getMapData(); } diff --git a/src/main/java/com/teamEWSN/gitdeun/mindmap/entity/PromptHistory.java b/src/main/java/com/teamEWSN/gitdeun/mindmap/entity/PromptHistory.java index b1eec3f..da07c3d 100644 --- a/src/main/java/com/teamEWSN/gitdeun/mindmap/entity/PromptHistory.java +++ b/src/main/java/com/teamEWSN/gitdeun/mindmap/entity/PromptHistory.java @@ -29,10 +29,6 @@ public class PromptHistory extends CreatedEntity { @Column(length = 50) private String title; // 분석 결과 요약 (기록 제목) - @JdbcTypeCode(SqlTypes.JSON) - @Column(name = "map_data", columnDefinition = "json", nullable = false) - private String mapData; // 해당 프롬프트의 분석 결과 데이터 - @Builder.Default @Column(name = "applied", nullable = false) private Boolean applied = false; // 적용 확정 여부 diff --git a/src/main/java/com/teamEWSN/gitdeun/mindmap/mapper/PromptHistoryMapper.java b/src/main/java/com/teamEWSN/gitdeun/mindmap/mapper/PromptHistoryMapper.java index a79d155..f0838cc 100644 --- a/src/main/java/com/teamEWSN/gitdeun/mindmap/mapper/PromptHistoryMapper.java +++ b/src/main/java/com/teamEWSN/gitdeun/mindmap/mapper/PromptHistoryMapper.java @@ -22,7 +22,6 @@ public interface PromptHistoryMapper { * PromptHistory 엔티티 → PromptPreviewResponseDto 변환 */ @Mapping(source = "id", target = "historyId") - @Mapping(source = "mapData", target = "previewMapData") PromptPreviewResponseDto toPreviewResponseDto(PromptHistory promptHistory); /** diff --git a/src/main/java/com/teamEWSN/gitdeun/mindmap/service/MindmapService.java b/src/main/java/com/teamEWSN/gitdeun/mindmap/service/MindmapService.java index 7a3a1a9..6ff528a 100644 --- a/src/main/java/com/teamEWSN/gitdeun/mindmap/service/MindmapService.java +++ b/src/main/java/com/teamEWSN/gitdeun/mindmap/service/MindmapService.java @@ -58,13 +58,12 @@ public Mindmap saveMindmapFromAnalysis(AnalysisResultDto analysisResult, String .user(user) .branch(repo.getDefaultBranch()) .title(title) - .mapData(StringUtils.hasText(analysisResult.getMapData()) ? analysisResult.getMapData() : "{}") .build(); mindmapRepository.save(mindmap); // 4. 초기 프롬프트 히스토리 생성 (프롬프트가 있는 경우) if (StringUtils.hasText(prompt)) { - promptHistoryService.createInitialPromptHistory(mindmap, prompt, analysisResult.getMapData(), title); + promptHistoryService.createInitialPromptHistory(mindmap, prompt, title); } // 5. 마인드맵 소유자 멤버로 등록 @@ -116,7 +115,6 @@ public MindmapDetailResponseDto updateMindmapFromAnalysis(Long mapId, AnalysisRe .orElseThrow(() -> new GlobalException(ErrorCode.MINDMAP_NOT_FOUND)); mindmap.getRepo().updateWithAnalysis(analysisResult); - mindmap.updateMapData(analysisResult.getMapData()); MindmapDetailResponseDto responseDto = mindmapMapper.toDetailResponseDto(mindmap); mindmapSseService.broadcastUpdate(mapId, responseDto); @@ -159,7 +157,6 @@ public void updateMindmapFromWebhook(WebhookUpdateDto dto, String authorizationH repo.updateWithWebhookData(dto); for (Mindmap mindmap : mindmapsToUpdate) { - mindmap.updateMapData(dto.getMapData()); MindmapDetailResponseDto responseDto = mindmapMapper.toDetailResponseDto(mindmap); mindmapSseService.broadcastUpdate(mindmap.getId(), responseDto); log.info("Webhook으로 마인드맵 ID {} 업데이트 및 SSE 전송 완료", mindmap.getId()); @@ -178,7 +175,7 @@ private Repo processRepository(String repoUrl, AnalysisResultDto analysisResult) Repo newRepo = Repo.builder() .githubRepoUrl(repoUrl) .defaultBranch(analysisResult.getDefaultBranch()) - .githubLastUpdatedAt(analysisResult.getGithubLastUpdatedAt()) + .githubLastUpdatedAt(analysisResult.getLastCommit()) .build(); return repoRepository.save(newRepo); }); diff --git a/src/main/java/com/teamEWSN/gitdeun/mindmap/service/PromptHistoryService.java b/src/main/java/com/teamEWSN/gitdeun/mindmap/service/PromptHistoryService.java index 4597355..f59ca3e 100644 --- a/src/main/java/com/teamEWSN/gitdeun/mindmap/service/PromptHistoryService.java +++ b/src/main/java/com/teamEWSN/gitdeun/mindmap/service/PromptHistoryService.java @@ -49,7 +49,7 @@ public PromptPreviewResponseDto createPromptPreview(Long mapId, Long userId, Min String repoUrl = mindmap.getRepo().getGithubRepoUrl(); try { - AnalysisResultDto analysisResult = fastApiClient.analyzeWithPrompt(repoUrl, req.getPrompt(), authorizationHeader); + AnalysisResultDto analysisResult = fastApiClient.refreshMindmap(repoUrl, req.getPrompt(), authorizationHeader); // FastAPI로부터 받은 analysisSummary 사용 String summary = analysisResult.getTitle(); @@ -63,7 +63,6 @@ public PromptPreviewResponseDto createPromptPreview(Long mapId, Long userId, Min .mindmap(mindmap) .prompt(req.getPrompt()) .title(summary) - .mapData(analysisResult.getMapData()) .applied(false) .build(); @@ -184,13 +183,12 @@ public void deletePromptHistory(Long mapId, Long historyId, Long userId) { /** * 마인드맵 생성 시 초기 프롬프트 히스토리 생성 */ - public void createInitialPromptHistory(Mindmap mindmap, String prompt, String mapData, String promptTitle) { + public void createInitialPromptHistory(Mindmap mindmap, String prompt, String promptTitle) { if (prompt != null && !prompt.trim().isEmpty()) { PromptHistory history = PromptHistory.builder() .mindmap(mindmap) .prompt(prompt) .title(promptTitle) - .mapData(mapData) .applied(true) .build(); diff --git a/src/main/java/com/teamEWSN/gitdeun/mindmap/util/MindmapGraphCache.java b/src/main/java/com/teamEWSN/gitdeun/mindmap/util/MindmapGraphCache.java new file mode 100644 index 0000000..75b57cd --- /dev/null +++ b/src/main/java/com/teamEWSN/gitdeun/mindmap/util/MindmapGraphCache.java @@ -0,0 +1,119 @@ +package com.teamEWSN.gitdeun.mindmap.util; + +import com.teamEWSN.gitdeun.common.fastapi.FastApiClient; +import com.teamEWSN.gitdeun.common.fastapi.dto.MindmapGraphDto; +import com.teamEWSN.gitdeun.mindmap.dto.MindmapGraphResponseDto; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.time.Duration; + +@Slf4j +@Component +@RequiredArgsConstructor +public class MindmapGraphCache { + + private final RedisTemplate redisTemplate; + private final FastApiClient fastApiClient; + + // L1 캐시: Caffeine + @Cacheable(value = "MINDMAP_GRAPH_L1", key = "#mapId") + public MindmapGraphResponseDto getGraphFromL1Cache(String mapId) { + return null; // 캐시 미스 시 L2로 진행 + } + + // L2 캐시: Redis + public MindmapGraphResponseDto getGraphWithHybridCache(String repoUrl, String authHeader) { + String mapId = extractMapId(repoUrl); + + // 1. L1 캐시 확인 (Caffeine) + MindmapGraphResponseDto l1Result = getGraphFromL1Cache(mapId); + if (l1Result != null) { + log.debug("마인드맵 그래프 L1 캐시 히트 - mapId: {}", mapId); + return l1Result; + } + + // 2. L2 캐시 확인 (Redis) + String redisKey = "mindmap:graph:" + mapId; + try { + MindmapGraphResponseDto l2Result = (MindmapGraphResponseDto) redisTemplate.opsForValue().get(redisKey); + + if (l2Result != null) { + log.debug("마인드맵 그래프 L2 캐시 히트 - mapId: {}", mapId); + // L1 캐시에 다시 저장 + cacheToL1(mapId, l2Result); + return l2Result; + } + } catch (Exception e) { + log.warn("Redis 캐시 조회 실패 - mapId: {}, FastAPI 직접 호출로 진행", mapId, e); + } + + // 3. FastAPI 실시간 조회 + try { + log.debug("FastAPI 실시간 조회 시작 - mapId: {}", mapId); + MindmapGraphDto graphDto = fastApiClient.getGraph(mapId, authHeader); + MindmapGraphResponseDto responseDto = convertToResponseDto(graphDto); + + // 4. 양방향 캐싱 + try { + redisTemplate.opsForValue().set(redisKey, responseDto, Duration.ofHours(2)); // L2: 2시간 + log.debug("Redis 캐시 저장 완료 - mapId: {}", mapId); + } catch (Exception e) { + log.warn("Redis 캐시 저장 실패 - mapId: {}", mapId, e); + } + + cacheToL1(mapId, responseDto); // L1: 30분 (CacheType에서 정의) + + return responseDto; + + } catch (Exception e) { + log.error("FastAPI 그래프 조회 실패 - mapId: {}", mapId, e); + return MindmapGraphResponseDto.builder() + .success(false) + .error("그래프 데이터 조회 실패: " + e.getMessage()) + .graphMapId(mapId) + .nodeCount(0) + .build(); + } + } + + @CachePut(value = "MINDMAP_GRAPH_L1", key = "#mapId") + public MindmapGraphResponseDto cacheToL1(String mapId, MindmapGraphResponseDto data) { + return data; + } + + // 캐시 무효화 (마인드맵 새로고침 시) + public void evictCache(String repoUrl) { + String mapId = extractMapId(repoUrl); + String redisKey = "mindmap:graph:" + mapId; + + try { + redisTemplate.delete(redisKey); + log.info("마인드맵 그래프 캐시 무효화 완료 - mapId: {}", mapId); + } catch (Exception e) { + log.warn("캐시 무효화 실패 - mapId: {}", mapId, e); + } + } + + // === Helper Methods === + + private String extractMapId(String repoUrl) { + String[] segments = repoUrl.split("/"); + return segments[segments.length - 1].replaceAll("\\.git$", ""); + } + + private MindmapGraphResponseDto convertToResponseDto(MindmapGraphDto graphDto) { + return MindmapGraphResponseDto.builder() + .success(true) + .error(null) + .graphMapId(graphDto.getMapId()) + .nodeCount(graphDto.getCount()) + .nodes(graphDto.getNodes()) + .edges(graphDto.getEdges()) + .build(); + } +} diff --git a/src/main/java/com/teamEWSN/gitdeun/repo/entity/Repo.java b/src/main/java/com/teamEWSN/gitdeun/repo/entity/Repo.java index 074c526..6b2b828 100644 --- a/src/main/java/com/teamEWSN/gitdeun/repo/entity/Repo.java +++ b/src/main/java/com/teamEWSN/gitdeun/repo/entity/Repo.java @@ -32,8 +32,8 @@ public class Repo { @Column(name = "default_branch", length = 100) private String defaultBranch; // 기본 브랜치 - @Column(name = "github_last_updated_at") - private LocalDateTime githubLastUpdatedAt; // GitHub 브랜치 최신 커밋 시간 (commit.committer.date) + @Column(name = "last_commit") + private LocalDateTime lastCommit; // GitHub 브랜치 최신 커밋 시간 (commit.committer.date) @OneToMany(mappedBy = "repo", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) private List mindmaps = new ArrayList<>(); @@ -42,23 +42,23 @@ public class Repo { public Repo(String githubRepoUrl, String defaultBranch, LocalDateTime githubLastUpdatedAt) { this.githubRepoUrl = githubRepoUrl; this.defaultBranch = defaultBranch; - this.githubLastUpdatedAt = githubLastUpdatedAt; + this.lastCommit = githubLastUpdatedAt; } public void updateWithAnalysis(AnalysisResultDto result) { this.defaultBranch = result.getDefaultBranch(); - this.githubLastUpdatedAt = result.getGithubLastUpdatedAt(); + this.lastCommit = result.getLastCommit(); } public void updateWithWebhookData(WebhookUpdateDto dto) { this.defaultBranch = dto.getDefaultBranch(); - this.githubLastUpdatedAt = dto.getGithubLastUpdatedAt(); + this.lastCommit = dto.getLastCommit(); } // 마지막 커밋 시간 업데이트 public void updateLastCommitTime(LocalDateTime lastCommitTime) { if (lastCommitTime != null) { - this.githubLastUpdatedAt = lastCommitTime; + this.lastCommit = lastCommitTime; } } @@ -72,9 +72,9 @@ public void updateDefaultBranch(String defaultBranch) { // 저장소가 최신 상태인지 확인 public boolean isNewerThan(LocalDateTime comparisonTime) { - if (this.githubLastUpdatedAt == null || comparisonTime == null) { + if (this.lastCommit == null || comparisonTime == null) { return false; } - return this.githubLastUpdatedAt.isAfter(comparisonTime); + return this.lastCommit.isAfter(comparisonTime); } } \ No newline at end of file diff --git a/src/main/java/com/teamEWSN/gitdeun/repo/service/RepoService.java b/src/main/java/com/teamEWSN/gitdeun/repo/service/RepoService.java index 573bf38..935e547 100644 --- a/src/main/java/com/teamEWSN/gitdeun/repo/service/RepoService.java +++ b/src/main/java/com/teamEWSN/gitdeun/repo/service/RepoService.java @@ -1,6 +1,5 @@ package com.teamEWSN.gitdeun.repo.service; -import com.teamEWSN.gitdeun.common.fastapi.FastApiClient; import com.teamEWSN.gitdeun.common.exception.ErrorCode; import com.teamEWSN.gitdeun.common.exception.GlobalException; import com.teamEWSN.gitdeun.common.fastapi.dto.AnalysisResultDto; @@ -12,7 +11,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.LocalDateTime; import java.util.Optional; @Service diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index d69a7dd..7a81212 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -25,6 +25,7 @@ spring: order_updates: true jdbc: batch_size: 1000 + time_zone: UTC security: oauth2: client: