diff --git a/build.gradle b/build.gradle index 089e5f4..d0cfbd2 100644 --- a/build.gradle +++ b/build.gradle @@ -138,3 +138,7 @@ tasks.withType(JavaCompile).configureEach { tasks.named('test') { useJUnitPlatform() } + +tasks.withType(JavaCompile).configureEach { + options.compilerArgs += ['-Xlint:deprecation', '-Xlint:unchecked'] +} diff --git a/src/main/java/com/teamEWSN/gitdeun/codereference/controller/CodeReferenceController.java b/src/main/java/com/teamEWSN/gitdeun/codereference/controller/CodeReferenceController.java index 6fe9d61..c135e4c 100644 --- a/src/main/java/com/teamEWSN/gitdeun/codereference/controller/CodeReferenceController.java +++ b/src/main/java/com/teamEWSN/gitdeun/codereference/controller/CodeReferenceController.java @@ -1,7 +1,7 @@ package com.teamEWSN.gitdeun.codereference.controller; import com.teamEWSN.gitdeun.codereference.dto.CodeReferenceResponseDtos.*; -import com.teamEWSN.gitdeun.codereference.dto.*; +import com.teamEWSN.gitdeun.codereference.dto.CodeReferenceRequestDtos.*; import com.teamEWSN.gitdeun.codereference.service.CodeReferenceService; import com.teamEWSN.gitdeun.common.jwt.CustomUserDetails; import jakarta.validation.Valid; @@ -21,31 +21,32 @@ public class CodeReferenceController { private final CodeReferenceService codeReferenceService; // 특정 노드에 코드 참조 생성 - @PostMapping("/nodes/{nodeId}/code-references") + @PostMapping("/nodes/{nodeKey}/code-references") public ResponseEntity createCodeReference( @PathVariable Long mapId, - @PathVariable String nodeId, - @Valid @RequestBody CodeReferenceRequestDtos.CreateRequest request, + @PathVariable String nodeKey, + @Valid @RequestBody CreateRequest request, @AuthenticationPrincipal CustomUserDetails userDetails) { - return ResponseEntity.status(HttpStatus.CREATED).body(codeReferenceService.createReference(mapId, nodeId, userDetails.getId(), request)); + return ResponseEntity.status(HttpStatus.CREATED).body(codeReferenceService.createReference(mapId, nodeKey, userDetails.getId(), request)); } // 특정 코드 참조 상세 조회 - @GetMapping("/code-references/{refId}") - public ResponseEntity getCodeReference( + @GetMapping("/code-references/{refId}/detail") + public ResponseEntity getCodeReferenceDetail( @PathVariable Long mapId, @PathVariable Long refId, - @AuthenticationPrincipal CustomUserDetails userDetails) { - return ResponseEntity.ok(codeReferenceService.getReference(mapId, refId, userDetails.getId())); + @AuthenticationPrincipal CustomUserDetails userDetails, + @RequestHeader("Authorization") String authorizationHeader) { + return ResponseEntity.ok(codeReferenceService.getReferenceDetail(mapId, refId, userDetails.getId(), authorizationHeader)); } // 특정 노드에 연결된 모든 코드 참조 목록 조회 - @GetMapping("/nodes/{nodeId}/code-references") + @GetMapping("/nodes/{nodeKey}/code-references") public ResponseEntity> getNodeCodeReferences( @PathVariable Long mapId, - @PathVariable String nodeId, + @PathVariable String nodeKey, @AuthenticationPrincipal CustomUserDetails userDetails) { - return ResponseEntity.ok(codeReferenceService.getReferencesForNode(mapId, nodeId, userDetails.getId())); + return ResponseEntity.ok(codeReferenceService.getReferencesForNode(mapId, nodeKey, userDetails.getId())); } // 코드 참조 정보 수정 @@ -53,7 +54,7 @@ public ResponseEntity> getNodeCodeReferences( public ResponseEntity updateCodeReference( @PathVariable Long mapId, @PathVariable Long refId, - @Valid @RequestBody CodeReferenceRequestDtos.CreateRequest request, + @Valid @RequestBody CreateRequest request, @AuthenticationPrincipal CustomUserDetails userDetails) { return ResponseEntity.ok(codeReferenceService.updateReference(mapId, refId, userDetails.getId(), request)); } 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 bdd183a..16b7450 100644 --- a/src/main/java/com/teamEWSN/gitdeun/codereference/dto/CodeReferenceResponseDtos.java +++ b/src/main/java/com/teamEWSN/gitdeun/codereference/dto/CodeReferenceResponseDtos.java @@ -10,10 +10,20 @@ public class CodeReferenceResponseDtos { @AllArgsConstructor(access = AccessLevel.PROTECTED) public static class ReferenceResponse { private Long referenceId; - private String nodeId; + private String nodeKey; private String filePath; private Integer startLine; private Integer endLine; } + @Getter + @Builder + public static class ReferenceDetailResponse { + private Long referenceId; + private String nodeKey; + private String filePath; + private Integer startLine; + private Integer endLine; + private String codeContent; // 실제 코드 내용을 담을 필드 + } } \ No newline at end of file diff --git a/src/main/java/com/teamEWSN/gitdeun/codereference/entity/CodeReference.java b/src/main/java/com/teamEWSN/gitdeun/codereference/entity/CodeReference.java index d13df82..027f58b 100644 --- a/src/main/java/com/teamEWSN/gitdeun/codereference/entity/CodeReference.java +++ b/src/main/java/com/teamEWSN/gitdeun/codereference/entity/CodeReference.java @@ -25,8 +25,8 @@ public class CodeReference extends AuditedEntity { @JoinColumn(name = "mindmap_id", nullable = false) private Mindmap mindmap; - @Column(name = "node_id", nullable = false) - private String nodeId; + @Column(name = "node_key", nullable = false) + private String nodeKey; @Column(name = "file_path", columnDefinition = "TEXT", nullable = false) private String filePath; 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 8239ddb..1642413 100644 --- a/src/main/java/com/teamEWSN/gitdeun/codereference/mapper/CodeReferenceMapper.java +++ b/src/main/java/com/teamEWSN/gitdeun/codereference/mapper/CodeReferenceMapper.java @@ -4,11 +4,32 @@ import com.teamEWSN.gitdeun.codereference.entity.CodeReference; import org.mapstruct.Mapper; import org.mapstruct.Mapping; +import org.mapstruct.Named; import org.mapstruct.ReportingPolicy; @Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE) public interface CodeReferenceMapper { + /** + * 기본 CodeReference 응답 매핑 + */ @Mapping(target = "referenceId", source = "id") ReferenceResponse toReferenceResponse(CodeReference codeReference); + + @Mapping(target = "referenceId", source = "codeReference.id") + @Mapping(target = "nodeKey", source = "codeReference.nodeKey") + @Mapping(target = "filePath", source = "codeReference.filePath") + @Mapping(target = "startLine", source = "codeReference.startLine") + @Mapping(target = "endLine", source = "codeReference.endLine") + @Mapping(target = "codeContent", expression = "java(codeContent)") + ReferenceDetailResponse toReferenceDetailResponse(CodeReference codeReference, String codeContent); + + /** + * 파일명 추출 헬퍼 메서드 + */ + @Named("extractFileName") + default String extractFileName(String filePath) { + if (filePath == null) return null; + return filePath.substring(filePath.lastIndexOf('/') + 1); + } } \ No newline at end of file diff --git a/src/main/java/com/teamEWSN/gitdeun/codereference/repository/CodeReferenceRepository.java b/src/main/java/com/teamEWSN/gitdeun/codereference/repository/CodeReferenceRepository.java index 1475b3b..d0d978c 100644 --- a/src/main/java/com/teamEWSN/gitdeun/codereference/repository/CodeReferenceRepository.java +++ b/src/main/java/com/teamEWSN/gitdeun/codereference/repository/CodeReferenceRepository.java @@ -10,7 +10,7 @@ @Repository public interface CodeReferenceRepository extends JpaRepository { - List findByMindmapIdAndNodeId(Long mindmapId, String nodeId); + List findByMindmapIdAndNodeKey(Long mindmapId, String nodeKey); Optional findByMindmapIdAndId(Long mindmapId, Long id); 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 1ed4f59..bffa089 100644 --- a/src/main/java/com/teamEWSN/gitdeun/codereference/service/CodeReferenceService.java +++ b/src/main/java/com/teamEWSN/gitdeun/codereference/service/CodeReferenceService.java @@ -8,6 +8,7 @@ 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.mindmap.entity.Mindmap; import com.teamEWSN.gitdeun.mindmap.repository.MindmapRepository; import com.teamEWSN.gitdeun.mindmapmember.service.MindmapAuthService; @@ -26,9 +27,10 @@ public class CodeReferenceService { private final MindmapRepository mindmapRepository; private final MindmapAuthService mindmapAuthService; private final CodeReferenceMapper codeReferenceMapper; + private final FastApiClient fastApiClient; @Transactional - public ReferenceResponse createReference(Long mapId, String nodeId, Long userId, CreateRequest request) { + public ReferenceResponse createReference(Long mapId, String nodeKey, Long userId, CreateRequest request) { // 1. 권한 확인 if (!mindmapAuthService.hasEdit(mapId, userId)) { throw new GlobalException(ErrorCode.FORBIDDEN_ACCESS); @@ -41,7 +43,7 @@ public ReferenceResponse createReference(Long mapId, String nodeId, Long userId, // 3. CodeReference 엔티티 생성 및 저장 CodeReference codeReference = CodeReference.builder() .mindmap(mindmap) - .nodeId(nodeId) + .nodeKey(nodeKey) .filePath(request.getFilePath()) .startLine(request.getStartLine()) .endLine(request.getEndLine()) @@ -54,29 +56,34 @@ public ReferenceResponse createReference(Long mapId, String nodeId, Long userId, } @Transactional(readOnly = true) - public ReferenceResponse getReference(Long mapId, Long refId, Long userId) { - // 1. 권한 확인 + public ReferenceDetailResponse getReferenceDetail(Long mapId, Long refId, Long userId, String authorizationHeader) { if (!mindmapAuthService.hasView(mapId, userId)) { throw new GlobalException(ErrorCode.FORBIDDEN_ACCESS); } - // 2. 해당 마인드맵에 속한 코드 참조인지 확인 CodeReference codeReference = codeReferenceRepository.findByMindmapIdAndId(mapId, refId) .orElseThrow(() -> new GlobalException(ErrorCode.CODE_REFERENCE_NOT_FOUND)); - // 3. DTO로 변환하여 반환 - return codeReferenceMapper.toReferenceResponse(codeReference); + Mindmap mindmap = codeReference.getMindmap(); + + // FastAPI를 통해 전체 파일 내용 가져오기 + String fullContent = fastApiClient.getFileRaw(mindmap.getRepo().getGithubRepoUrl(), codeReference.getFilePath(), authorizationHeader); + + // 특정 라인만 추출 (snippet) + String snippet = extractLines(fullContent, codeReference.getStartLine(), codeReference.getEndLine()); + + return codeReferenceMapper.toReferenceDetailResponse(codeReference, snippet); } @Transactional(readOnly = true) - public List getReferencesForNode(Long mapId, String nodeId, Long userId) { + public List getReferencesForNode(Long mapId, String nodeKey, Long userId) { // 1. 권한 확인 (읽기) if (!mindmapAuthService.hasView(mapId, userId)) { throw new GlobalException(ErrorCode.FORBIDDEN_ACCESS); } // 2. 특정 노드에 속한 모든 코드 참조 조회 - List references = codeReferenceRepository.findByMindmapIdAndNodeId(mapId, nodeId); + List references = codeReferenceRepository.findByMindmapIdAndNodeKey(mapId, nodeKey); // 3. DTO 리스트로 변환하여 반환 return references.stream() @@ -86,7 +93,7 @@ public List getReferencesForNode(Long mapId, String nodeId, L @Transactional public ReferenceResponse updateReference(Long mapId, Long refId, Long userId, CreateRequest request) { - // 1. 권한 확인 (쓰기) + // 1. 권한 확인 if (!mindmapAuthService.hasEdit(mapId, userId)) { throw new GlobalException(ErrorCode.FORBIDDEN_ACCESS); } @@ -117,4 +124,16 @@ public void deleteReference(Long mapId, Long refId, Long userId) { // 3. 코드 참조 삭제 codeReferenceRepository.deleteById(refId); } + + private String extractLines(String fullContent, Integer startLine, Integer endLine) { + if (fullContent == null) return ""; + if (startLine == null || endLine == null || startLine <= 0 || endLine < startLine) return fullContent; + + String[] lines = fullContent.split("\\r?\\n"); + StringBuilder sb = new StringBuilder(); + for (int i = startLine - 1; i < endLine && i < lines.length; i++) { + sb.append(lines[i]).append("\n"); + } + return sb.toString(); + } } diff --git a/src/main/java/com/teamEWSN/gitdeun/codereview/controller/CodeReviewController.java b/src/main/java/com/teamEWSN/gitdeun/codereview/controller/CodeReviewController.java index cdf3c01..e13dc29 100644 --- a/src/main/java/com/teamEWSN/gitdeun/codereview/controller/CodeReviewController.java +++ b/src/main/java/com/teamEWSN/gitdeun/codereview/controller/CodeReviewController.java @@ -27,14 +27,14 @@ public class CodeReviewController { private final CodeReviewService codeReviewService; // 노드에 대한 코드 리뷰 생성 - @PostMapping(value = "/mindmaps/{mapId}/nodes/{nodeId}/code-reviews", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @PostMapping(value = "/mindmaps/{mapId}/nodes/{nodeKey}/code-reviews", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity createNodeCodeReview( @PathVariable Long mapId, - @PathVariable String nodeId, + @PathVariable String nodeKey, @Valid @RequestPart("request") CreateRequest request, @RequestPart(value = "files", required = false) List files, @AuthenticationPrincipal CustomUserDetails userDetails) { - return ResponseEntity.status(HttpStatus.CREATED).body(codeReviewService.createNodeReview(mapId, nodeId, userDetails.getId(), request, files)); + return ResponseEntity.status(HttpStatus.CREATED).body(codeReviewService.createNodeReview(mapId, nodeKey, userDetails.getId(), request, files)); } // 코드 참조에 대한 코드 리뷰 생성 @@ -66,12 +66,12 @@ public ResponseEntity updateCodeReviewStatus( } // 특정 노드에 달린 코드 리뷰 목록 조회 - @GetMapping("/mindmaps/{mapId}/nodes/{nodeId}/code-reviews") + @GetMapping("/mindmaps/{mapId}/nodes/{nodeKey}/code-reviews") public ResponseEntity> getNodeCodeReviews( @PathVariable Long mapId, - @PathVariable String nodeId, + @PathVariable String nodeKey, @AuthenticationPrincipal CustomUserDetails userDetails, @PageableDefault(size = 10) Pageable pageable) { - return ResponseEntity.ok(codeReviewService.getReviewsForNode(mapId, nodeId, userDetails.getId(), pageable)); + return ResponseEntity.ok(codeReviewService.getReviewsForNode(mapId, nodeKey, userDetails.getId(), pageable)); } } \ No newline at end of file diff --git a/src/main/java/com/teamEWSN/gitdeun/codereview/entity/CodeReview.java b/src/main/java/com/teamEWSN/gitdeun/codereview/entity/CodeReview.java index aad1710..3f95e47 100644 --- a/src/main/java/com/teamEWSN/gitdeun/codereview/entity/CodeReview.java +++ b/src/main/java/com/teamEWSN/gitdeun/codereview/entity/CodeReview.java @@ -32,8 +32,8 @@ public class CodeReview extends AuditedEntity { private Mindmap mindmap; // 리뷰 대상: 노드 ID 또는 코드 참조 - @Column(name = "node_id") - private String nodeId; // 마인드맵 그래프의 노드 ID + @Column(name = "node_key") + private String nodeKey; // 마인드맵 그래프의 노드 key @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "code_reference_id") diff --git a/src/main/java/com/teamEWSN/gitdeun/codereview/repository/CodeReviewRepository.java b/src/main/java/com/teamEWSN/gitdeun/codereview/repository/CodeReviewRepository.java index 50d7ede..bb36009 100644 --- a/src/main/java/com/teamEWSN/gitdeun/codereview/repository/CodeReviewRepository.java +++ b/src/main/java/com/teamEWSN/gitdeun/codereview/repository/CodeReviewRepository.java @@ -8,5 +8,5 @@ @Repository public interface CodeReviewRepository extends JpaRepository { - Page findByMindmapIdAndNodeId(Long mindmapId, String nodeId, Pageable pageable); + Page findByMindmapIdAndNodeKey(Long mindmapId, String nodeKey, Pageable pageable); } \ No newline at end of file diff --git a/src/main/java/com/teamEWSN/gitdeun/codereview/service/CodeReviewService.java b/src/main/java/com/teamEWSN/gitdeun/codereview/service/CodeReviewService.java index 56a3d88..5c287fc 100644 --- a/src/main/java/com/teamEWSN/gitdeun/codereview/service/CodeReviewService.java +++ b/src/main/java/com/teamEWSN/gitdeun/codereview/service/CodeReviewService.java @@ -38,7 +38,7 @@ public class CodeReviewService { private final CodeReviewMapper codeReviewMapper; @Transactional - public ReviewResponse createNodeReview(Long mapId, String nodeId, Long userId, CreateRequest request, List files) { + public ReviewResponse createNodeReview(Long mapId, String nodeKey, Long userId, CreateRequest request, List files) { // 리뷰 작성 권한 확인 if (!mindmapAuthService.hasEdit(mapId, userId)) { throw new GlobalException(ErrorCode.FORBIDDEN_ACCESS); @@ -53,7 +53,7 @@ public ReviewResponse createNodeReview(Long mapId, String nodeId, Long userId, C CodeReview codeReview = CodeReview.builder() .mindmap(mindmap) .author(author) - .nodeId(nodeId) + .nodeKey(nodeKey) .status(CodeReviewStatus.PENDING) .build(); CodeReview savedReview = codeReviewRepository.save(codeReview); @@ -125,12 +125,12 @@ public void updateReviewStatus(Long reviewId, Long userId, CodeReviewStatus stat } @Transactional(readOnly = true) - public Page getReviewsForNode(Long mapId, String nodeId, Long userId, Pageable pageable) { + public Page getReviewsForNode(Long mapId, String nodeKey, Long userId, Pageable pageable) { if (!mindmapAuthService.hasView(mapId, userId)) { throw new GlobalException(ErrorCode.FORBIDDEN_ACCESS); } - Page reviews = codeReviewRepository.findByMindmapIdAndNodeId(mapId, nodeId, pageable); + Page reviews = codeReviewRepository.findByMindmapIdAndNodeKey(mapId, nodeKey, pageable); return reviews.map(codeReviewMapper::toReviewListResponse); } } diff --git a/src/main/java/com/teamEWSN/gitdeun/comment/entity/Comment.java b/src/main/java/com/teamEWSN/gitdeun/comment/entity/Comment.java index 976efaf..b5dcbcf 100644 --- a/src/main/java/com/teamEWSN/gitdeun/comment/entity/Comment.java +++ b/src/main/java/com/teamEWSN/gitdeun/comment/entity/Comment.java @@ -6,6 +6,7 @@ import jakarta.persistence.*; import lombok.*; import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; import org.hibernate.annotations.Where; import java.time.LocalDateTime; @@ -16,7 +17,7 @@ @Getter @Builder @SQLDelete(sql = "UPDATE comment SET deleted_at = CURRENT_TIMESTAMP WHERE id = ?") -@Where(clause = "deleted_at IS NULL") +@SQLRestriction("deleted_at IS NULL") @NoArgsConstructor @AllArgsConstructor @Table(name = "comment") diff --git a/src/main/java/com/teamEWSN/gitdeun/common/exception/ErrorCode.java b/src/main/java/com/teamEWSN/gitdeun/common/exception/ErrorCode.java index cfff642..4269304 100644 --- a/src/main/java/com/teamEWSN/gitdeun/common/exception/ErrorCode.java +++ b/src/main/java/com/teamEWSN/gitdeun/common/exception/ErrorCode.java @@ -45,6 +45,7 @@ public enum ErrorCode { // 마인드맵 관련 MINDMAP_NOT_FOUND(HttpStatus.NOT_FOUND, "MINDMAP-001", "요청한 마인드맵을 찾을 수 없습니다."), MINDMAP_CREATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "MINDMAP-002", "마인드맵 생성 중 오류가 발생했습니다."), + NODE_NOT_FOUND(HttpStatus.NOT_FOUND, "MINDMAP-003", "요청한 마인드맵 노드 정보를 찾을 수 없습니다."), // 마인드맵 요청 검증 관련 INVALID_USER_ID(HttpStatus.BAD_REQUEST, "VALIDATE-001", "유효하지 않은 사용자 ID입니다."), 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 d40a94a..0f40a48 100644 --- a/src/main/java/com/teamEWSN/gitdeun/common/fastapi/FastApiClient.java +++ b/src/main/java/com/teamEWSN/gitdeun/common/fastapi/FastApiClient.java @@ -9,11 +9,14 @@ import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.http.HttpStatusCode; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.util.UriComponentsBuilder; import reactor.core.publisher.Mono; +import java.time.Duration; import java.time.LocalDateTime; import java.util.HashMap; import java.util.List; @@ -246,6 +249,68 @@ public SuggestionAutoResponse createAutoSuggestions( } } + public String getFileRaw(String repoUrl, String filePath, String authHeader) { + return getFileRaw(repoUrl, filePath, null, null, null, authHeader); + } + + public String getFileRaw(String repoUrl, String filePath, Integer startLine, Integer endLine, String sha, String authHeader) { + String repoId = extractMapId(repoUrl); + try { + log.debug("FastAPI 파일 내용 조회 시작 - repoId: {}, filePath: {}", repoId, filePath); + + // URI 생성 (쿼리 파라미터 포함) + UriComponentsBuilder uriBuilder = UriComponentsBuilder + .fromPath("/content/file/raw") + .queryParam("repo_id", repoId) + .queryParam("path", filePath); + + if (startLine != null) { + uriBuilder.queryParam("start_line", startLine); + } + if (endLine != null) { + uriBuilder.queryParam("end_line", endLine); + } + if (sha != null && !sha.trim().isEmpty()) { + uriBuilder.queryParam("sha", sha); + } + + 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 오류 - repoId: {}, filePath: {}, status: {}", + repoId, filePath, clientResponse.statusCode()); + return clientResponse.bodyToMono(String.class) + .map(errorBody -> new RuntimeException("파일을 찾을 수 없습니다: " + errorBody)); + }) + .onStatus(HttpStatusCode::is5xxServerError, serverResponse -> { + log.error("FastAPI 파일 조회 5xx 오류 - repoId: {}, filePath: {}, status: {}", + repoId, filePath, serverResponse.statusCode()); + return Mono.error(new RuntimeException("FastAPI 서버 오류")); + }) + .bodyToMono(String.class) + .timeout(Duration.ofSeconds(30)) + .block(); + + log.debug("FastAPI 파일 내용 조회 완료 - repoId: {}, filePath: {}, 길이: {}", + repoId, filePath, response != null ? response.length() : 0); + + return response != null ? response : ""; + + } catch (Exception e) { + log.error("FastAPI 파일 내용 조회 실패 - repoId: {}, filePath: {}", repoId, filePath, e); + return ""; // 빈 문자열 반환 (null 대신) + } + } + // === Helper Methods === // 저장소명 추출 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 911b977..467230c 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 @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Getter; import java.util.List; +import java.util.stream.Collectors; @Getter public class NodeDto { @@ -11,22 +12,8 @@ public class NodeDto { private String label; @JsonProperty("related_files") - private List relatedFiles; + private List relatedFiles; @JsonProperty("node_type") private String nodeType; - - - // 편의 메서드 - 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/fastapi/dto/RelatedFileDto.java b/src/main/java/com/teamEWSN/gitdeun/common/fastapi/dto/RelatedFileDto.java new file mode 100644 index 0000000..4690f9b --- /dev/null +++ b/src/main/java/com/teamEWSN/gitdeun/common/fastapi/dto/RelatedFileDto.java @@ -0,0 +1,31 @@ +package com.teamEWSN.gitdeun.common.fastapi.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RelatedFileDto { + + /** + * 파일 경로 (CodeReference.filePath와 매핑) + * 예: "src/main/java/com/example/service/UserService.java" + */ + @JsonProperty("file_path") + private String filePath; + + /** + * 파일명만 추출 + * @return 파일명 (확장자 포함) + */ + public String getFileName() { + if (filePath == null) return null; + return filePath.substring(filePath.lastIndexOf('/') + 1); + } +} \ No newline at end of file diff --git a/src/main/java/com/teamEWSN/gitdeun/mindmap/controller/NodeController.java b/src/main/java/com/teamEWSN/gitdeun/mindmap/controller/NodeController.java new file mode 100644 index 0000000..121beea --- /dev/null +++ b/src/main/java/com/teamEWSN/gitdeun/mindmap/controller/NodeController.java @@ -0,0 +1,49 @@ +package com.teamEWSN.gitdeun.mindmap.controller; + +import com.teamEWSN.gitdeun.common.jwt.CustomUserDetails; +import com.teamEWSN.gitdeun.mindmap.dto.NodeCodeResponseDto; +import com.teamEWSN.gitdeun.mindmap.dto.NodeSimpleDto; +import com.teamEWSN.gitdeun.mindmap.service.NodeService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/mindmaps/{mapId}/nodes") +@RequiredArgsConstructor +public class NodeController { + + private final NodeService nodeService; + + /** + * 특정 노드의 상세 정보와 연결된 모든 파일의 전체 코드를 조회합니다. + * CodeReview 및 CodeReference 기능에 사용됩니다. + */ + @GetMapping("/{nodeKey}/code") + public ResponseEntity getNodeWithCode( + @PathVariable Long mapId, + @PathVariable String nodeKey, + @AuthenticationPrincipal CustomUserDetails userDetails, + @RequestHeader("Authorization") String authorizationHeader + ) { + // (인증/인가 로직은 Spring Security와 MindmapAuthService에서 처리) + NodeCodeResponseDto response = nodeService.getNodeDetailsWithCode(mapId, nodeKey, userDetails.getId(), authorizationHeader); + return ResponseEntity.ok(response); + } + + /** + * (테스트용) 마인드맵의 모든 노드 목록(key, label)을 조회합니다. + */ + @GetMapping("/list") + public ResponseEntity> getNodeList( + @PathVariable Long mapId, + @RequestHeader("Authorization") String authorizationHeader + ) { + List response = nodeService.getNodeList(mapId, authorizationHeader); + return ResponseEntity.ok(response); + } +} + diff --git a/src/main/java/com/teamEWSN/gitdeun/mindmap/dto/NodeCodeResponseDto.java b/src/main/java/com/teamEWSN/gitdeun/mindmap/dto/NodeCodeResponseDto.java new file mode 100644 index 0000000..78aef5c --- /dev/null +++ b/src/main/java/com/teamEWSN/gitdeun/mindmap/dto/NodeCodeResponseDto.java @@ -0,0 +1,17 @@ +package com.teamEWSN.gitdeun.mindmap.dto; + +import com.teamEWSN.gitdeun.common.fastapi.dto.RelatedFileDto; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; +import java.util.Map; + +@Getter +@AllArgsConstructor +public class NodeCodeResponseDto { + private String nodeKey; + private String nodeLabel; + private List filePaths; + private Map codeContents; // filePath -> full code content +} \ No newline at end of file diff --git a/src/main/java/com/teamEWSN/gitdeun/mindmap/dto/NodeSimpleDto.java b/src/main/java/com/teamEWSN/gitdeun/mindmap/dto/NodeSimpleDto.java new file mode 100644 index 0000000..5da0bc3 --- /dev/null +++ b/src/main/java/com/teamEWSN/gitdeun/mindmap/dto/NodeSimpleDto.java @@ -0,0 +1,11 @@ +package com.teamEWSN.gitdeun.mindmap.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class NodeSimpleDto { + private String key; + private String label; +} diff --git a/src/main/java/com/teamEWSN/gitdeun/mindmap/service/NodeService.java b/src/main/java/com/teamEWSN/gitdeun/mindmap/service/NodeService.java new file mode 100644 index 0000000..3c3389d --- /dev/null +++ b/src/main/java/com/teamEWSN/gitdeun/mindmap/service/NodeService.java @@ -0,0 +1,93 @@ +package com.teamEWSN.gitdeun.mindmap.service; + +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.MindmapGraphDto; +import com.teamEWSN.gitdeun.common.fastapi.dto.NodeDto; +import com.teamEWSN.gitdeun.common.fastapi.dto.RelatedFileDto; +import com.teamEWSN.gitdeun.mindmap.dto.MindmapGraphResponseDto; +import com.teamEWSN.gitdeun.mindmap.dto.NodeCodeResponseDto; +import com.teamEWSN.gitdeun.mindmap.dto.NodeSimpleDto; +import com.teamEWSN.gitdeun.mindmap.entity.Mindmap; +import com.teamEWSN.gitdeun.mindmap.repository.MindmapRepository; +import com.teamEWSN.gitdeun.mindmap.util.MindmapGraphCache; +import com.teamEWSN.gitdeun.mindmapmember.service.MindmapAuthService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class NodeService { + private final MindmapAuthService mindmapAuthService; + private final MindmapRepository mindmapRepository; + private final MindmapGraphCache mindmapGraphCache; + private final FastApiClient fastApiClient; + + /** + * (테스트용) 마인드맵의 모든 노드 목록을 간략하게 조회합니다. + */ + @Transactional(readOnly = true) + public List getNodeList(Long mapId, String authorizationHeader) { + Mindmap mindmap = mindmapRepository.findByIdAndDeletedAtIsNull(mapId) + .orElseThrow(() -> new GlobalException(ErrorCode.MINDMAP_NOT_FOUND)); + + String repoUrl = mindmap.getRepo().getGithubRepoUrl(); + + MindmapGraphResponseDto graphData = mindmapGraphCache.getGraphWithHybridCache(repoUrl, authorizationHeader); + if (graphData == null || graphData.getNodes() == null) { + return Collections.emptyList(); + } + + return graphData.getNodes().stream() + .map(node -> new NodeSimpleDto(node.getKey(), node.getLabel())) + .collect(Collectors.toList()); + } + + @Transactional(readOnly = true) + public NodeCodeResponseDto getNodeDetailsWithCode(Long mapId, String nodeKey, Long userId, String authorizationHeader) { + if (!mindmapAuthService.hasView(mapId, userId)) { + throw new GlobalException(ErrorCode.FORBIDDEN_ACCESS); + } + + Mindmap mindmap = mindmapRepository.findByIdAndDeletedAtIsNull(mapId) + .orElseThrow(() -> new GlobalException(ErrorCode.MINDMAP_NOT_FOUND)); + + String repoUrl = mindmap.getRepo().getGithubRepoUrl(); + + // 1. 캐시/API를 통해 그래프 데이터를 가져옵니다. + MindmapGraphResponseDto graphData = mindmapGraphCache.getGraphWithHybridCache(repoUrl, authorizationHeader); + if (graphData == null || !graphData.getSuccess()) { + throw new GlobalException(ErrorCode.MINDMAP_NOT_FOUND); + } + + // 2. 그래프에서 해당 nodeKey를 가진 노드를 찾습니다. + NodeDto targetNode = graphData.getNodes().stream() + .filter(node -> nodeKey.equals(node.getKey())) + .findFirst() + .orElseThrow(() -> new GlobalException(ErrorCode.NODE_NOT_FOUND)); + + List filePaths = targetNode.getRelatedFiles(); + + // 3. 파일 경로 목록을 사용하여 각 파일의 전체 코드를 가져옵니다. + Map codeContents = filePaths.parallelStream() + .collect(Collectors.toConcurrentMap( + filePath -> filePath, + filePath -> fastApiClient.getFileRaw(repoUrl, filePath.getFilePath(), authorizationHeader) + )); + + return new NodeCodeResponseDto( + targetNode.getKey(), + targetNode.getLabel(), + filePaths, + codeContents + ); + } +} +