Skip to content
Merged
71 changes: 71 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
name: Deploy to EC2 with Docker hub

on:
push:
branches:
- main

jobs:
deploy:
runs-on: ubuntu-latest

steps:
# μ†ŒμŠ€μ½”λ“œ 체크아웃
- name: Checkout code
uses: actions/checkout@v5

# Gradle μ‹€ν–‰ κΆŒν•œ λΆ€μ—¬
- name: Make gradlew executable
run: chmod +x ./gradlew

# Docker hub 둜그인
- name: Log in to Docker Hub
uses: docker/[email protected]
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

# Dockerfile을 μ‚¬μš©ν•˜μ—¬ 이미지λ₯Ό λΉŒλ“œν•˜κ³  Docker Hub에 ν‘Έμ‹œν•©λ‹ˆλ‹€.
- name: Build and push Docker image
uses: docker/[email protected]
with:
context: .
file: ./Dockerfile
push: true
# Docker λ ˆμ΄μ–΄ μΊμ‹œ ν™œμ„±ν™”
cache-from: type=gha
cache-to: type=gha,mode=max
tags: |
${{ secrets.DOCKER_USERNAME }}/gitdeun:latest
${{ secrets.DOCKER_USERNAME }}/gitdeun:${{ github.sha }}

# 3. EC2μ—μ„œ ν™˜κ²½ λ³€μˆ˜λ₯Ό μ„€μ •ν•˜κ³  μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ‹€ν–‰
- name: Deploy via SSH
uses: appleboy/[email protected]
with:
host: ${{secrets.EC2_HOST}}
username: ${{secrets.EC2_USERNAME}}
key: ${{ secrets.EC2_PRIVATE_KEY }}
port: 22
script: |
set -eux
# 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 \
--env SPRING_PROFILES_ACTIVE=prod,s3Bucket \
--env-file ${{ secrets.EC2_TARGET_PATH }}/.env \
--network gitdeun-network \
-p 8080:8080 \
-v ${{ secrets.EC2_TARGET_PATH }}/logs:/app/logs \
"$IMAGE"
# 였래된 이미지 정리
docker image prune -f --filter "until=168h"
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ public ResponseEntity<ReferenceResponse> createCodeReference(
@PathVariable Long mapId,
@PathVariable String nodeKey,
@Valid @RequestBody CreateRequest request,
@RequestHeader("Authorization") String authorizationHeader,
@AuthenticationPrincipal CustomUserDetails userDetails) {
return ResponseEntity.status(HttpStatus.CREATED).body(codeReferenceService.createReference(mapId, nodeKey, userDetails.getId(), request));
return ResponseEntity.status(HttpStatus.CREATED).body(codeReferenceService.createReference(mapId, nodeKey, userDetails.getId(), request, authorizationHeader));
}

// νŠΉμ • μ½”λ“œ μ°Έμ‘° 상세 쑰회
Expand Down Expand Up @@ -55,8 +56,9 @@ public ResponseEntity<ReferenceResponse> updateCodeReference(
@PathVariable Long mapId,
@PathVariable Long refId,
@Valid @RequestBody CreateRequest request,
@RequestHeader("Authorization") String authorizationHeader,
@AuthenticationPrincipal CustomUserDetails userDetails) {
return ResponseEntity.ok(codeReferenceService.updateReference(mapId, refId, userDetails.getId(), request));
return ResponseEntity.ok(codeReferenceService.updateReference(mapId, refId, userDetails.getId(), request, authorizationHeader));
}

// μ½”λ“œ μ°Έμ‘° μ‚­μ œ
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,18 @@
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;
import com.teamEWSN.gitdeun.mindmap.repository.MindmapRepository;
import com.teamEWSN.gitdeun.mindmap.util.FileContentCache;
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.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;

Expand All @@ -27,10 +32,12 @@ public class CodeReferenceService {
private final MindmapRepository mindmapRepository;
private final MindmapAuthService mindmapAuthService;
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) {
public ReferenceResponse createReference(Long mapId, String nodeKey, Long userId, CreateRequest request, String authorizationHeader) throws GlobalException {
// 1. κΆŒν•œ 확인
if (!mindmapAuthService.hasEdit(mapId, userId)) {
throw new GlobalException(ErrorCode.FORBIDDEN_ACCESS);
Expand All @@ -40,6 +47,8 @@ public ReferenceResponse createReference(Long mapId, String nodeKey, Long userId
Mindmap mindmap = mindmapRepository.findById(mapId)
.orElseThrow(() -> new GlobalException(ErrorCode.MINDMAP_NOT_FOUND));

validateCodeReferenceRequest(mindmap, nodeKey, request, authorizationHeader);

// 3. CodeReference μ—”ν‹°ν‹° 생성 및 μ €μž₯
CodeReference codeReference = CodeReference.builder()
.mindmap(mindmap)
Expand All @@ -51,7 +60,6 @@ public ReferenceResponse createReference(Long mapId, String nodeKey, Long userId

CodeReference savedReference = codeReferenceRepository.save(codeReference);

// 4. DTO둜 λ³€ν™˜ν•˜μ—¬ λ°˜ν™˜
return codeReferenceMapper.toReferenceResponse(savedReference);
}

Expand All @@ -77,7 +85,6 @@ public ReferenceDetailResponse getReferenceDetail(Long mapId, Long refId, Long u

@Transactional(readOnly = true)
public List<ReferenceResponse> getReferencesForNode(Long mapId, String nodeKey, Long userId) {
// 1. κΆŒν•œ 확인 (읽기)
if (!mindmapAuthService.hasView(mapId, userId)) {
throw new GlobalException(ErrorCode.FORBIDDEN_ACCESS);
}
Expand All @@ -92,7 +99,7 @@ public List<ReferenceResponse> getReferencesForNode(Long mapId, String nodeKey,
}

@Transactional
public ReferenceResponse updateReference(Long mapId, Long refId, Long userId, CreateRequest request) {
public ReferenceResponse updateReference(Long mapId, Long refId, Long userId, CreateRequest request, String authorizationHeader) {
// 1. κΆŒν•œ 확인
if (!mindmapAuthService.hasEdit(mapId, userId)) {
throw new GlobalException(ErrorCode.FORBIDDEN_ACCESS);
Expand All @@ -102,6 +109,8 @@ public ReferenceResponse updateReference(Long mapId, Long refId, Long userId, Cr
CodeReference codeReference = codeReferenceRepository.findByMindmapIdAndId(mapId, refId)
.orElseThrow(() -> new GlobalException(ErrorCode.CODE_REFERENCE_NOT_FOUND));

validateCodeReferenceRequest(codeReference.getMindmap(), codeReference.getNodeKey(), request, authorizationHeader);

// 3. μ—”ν‹°ν‹° 정보 μ—…λ°μ΄νŠΈ
codeReference.update(request.getFilePath(), request.getStartLine(), request.getEndLine());

Expand Down Expand Up @@ -136,4 +145,36 @@ private String extractLines(String fullContent, Integer startLine, Integer endLi
}
return sb.toString();
}

// μ½”λ“œ μ°Έμ‘° 검증
private void validateCodeReferenceRequest(Mindmap mindmap, String nodeKey, CreateRequest request, String authorizationHeader) {
String repoUrl = mindmap.getRepo().getGithubRepoUrl();
LocalDateTime lastCommit = mindmap.getRepo().getLastCommit();

// κ·Έλž˜ν”„ λ°μ΄ν„°μ—μ„œ λ…Έλ“œ 정보 κ°€μ Έμ˜€κΈ°
MindmapGraphResponseDto graphData = mindmapGraphCache.getGraphWithHybridCache(repoUrl, lastCommit, authorizationHeader);
NodeDto targetNode = graphData.getNodes().stream()
.filter(node -> nodeKey.equals(node.getKey()))
.findFirst()
.orElseThrow(() -> new GlobalException(ErrorCode.NODE_NOT_FOUND));

// μš”μ²­λœ filePathκ°€ λ…Έλ“œμ— μ‹€μ œ ν¬ν•¨λœ νŒŒμΌμΈμ§€ 확인
boolean fileExists = targetNode.getRelatedFiles().stream()
.anyMatch(file -> request.getFilePath().equals(file.getFilePath()));
if (!fileExists) {
throw new GlobalException(ErrorCode.FILE_NOT_FOUND_IN_NODE);
}

// 파일의 전체 λ‚΄μš©μ„ 가져와 총 라인 수 계산
String fileContent = fileContentCache.getFileContentWithCache(repoUrl, request.getFilePath(), lastCommit, authorizationHeader);
int totalLines = fileContent.split("\\r?\\n").length;

// μš”μ²­λœ 라인 λ²”μœ„(startLine, endLine)κ°€ μœ νš¨ν•œμ§€ 확인
Integer start = request.getStartLine();
Integer end = request.getEndLine();
if (start <= 0 || start > end || end > totalLines) {
throw new GlobalException(ErrorCode.INVALID_LINE_RANGE);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public class CodeReviewController {
private final CodeReviewService codeReviewService;

// λ…Έλ“œμ— λŒ€ν•œ μ½”λ“œ 리뷰 생성
@PostMapping(value = "/mindmaps/{mapId}/nodes/{nodeKey}/code-reviews", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@PostMapping(value = "/mindmaps/{mapId}/nodes/{nodeKey}/code-reviews", consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE})
public ResponseEntity<ReviewResponse> createNodeCodeReview(
@PathVariable Long mapId,
@PathVariable String nodeKey,
Expand All @@ -38,7 +38,7 @@ public ResponseEntity<ReviewResponse> createNodeCodeReview(
}

// μ½”λ“œ 참쑰에 λŒ€ν•œ μ½”λ“œ 리뷰 생성
@PostMapping(value = "/references/{refId}/code-reviews", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@PostMapping(value = "/references/{refId}/code-reviews", consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE})
public ResponseEntity<ReviewResponse> createReferenceCodeReview(
@PathVariable Long refId,
@Valid @RequestPart("request") CreateRequest request,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public class CodeReviewRequestDtos {
@Getter
@NoArgsConstructor
public static class CreateRequest {
@NotEmpty(message = "첫 λŒ“κΈ€ λ‚΄μš©μ€ λΉ„μ›Œλ‘˜ 수 μ—†μŠ΅λ‹ˆλ‹€.")
@NotEmpty(message = "λŒ“κΈ€ λ‚΄μš©μ€ λΉ„μ›Œλ‘˜ 수 μ—†μŠ΅λ‹ˆλ‹€.")
private String content;
private EmojiType emojiType;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public class CommentController {
private final CommentService commentService;

// νŠΉμ • μ½”λ“œ 리뷰에 λŒ“κΈ€ λ˜λŠ” λŒ€λŒ“κΈ€ μž‘μ„±
@PostMapping(value = "/code-reviews/{reviewId}/comments", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@PostMapping(value = "/code-reviews/{reviewId}/comments", consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE})
public ResponseEntity<CommentResponse> createComment(
@PathVariable Long reviewId,
@Valid @RequestPart("request") CreateRequest request,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@Entity
@Getter
Expand Down Expand Up @@ -41,11 +43,11 @@ public class Comment extends AuditedEntity {

@Builder.Default
@OneToMany(mappedBy = "parentComment")
private List<Comment> replies = new ArrayList<>();
private Set<Comment> replies = new HashSet<>();

@Builder.Default
@OneToMany(mappedBy = "comment", cascade = CascadeType.ALL, orphanRemoval = true)
private List<CommentAttachment> attachments = new ArrayList<>();
private Set<CommentAttachment> attachments = new HashSet<>();

@Column(columnDefinition = "TEXT", nullable = false)
private String content;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

Expand Down Expand Up @@ -61,7 +62,7 @@ private AttachmentType determineAttachmentType(String mimeType) {
* μ²¨λΆ€νŒŒμΌ λͺ©λ‘μ„ S3와 DBμ—μ„œ λͺ¨λ‘ μ‚­μ œ (Hard Delete)
*/
@Transactional
public void deleteAttachments(List<CommentAttachment> attachments) {
public void deleteAttachments(Set<CommentAttachment> attachments) {
if (CollectionUtils.isEmpty(attachments)) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

@Service
Expand Down Expand Up @@ -108,7 +109,7 @@ public List<CommentResponse> getCommentsAsTree(Long reviewId) {
return topLevelComments;
}

@Transactional(readOnly = true)
@Transactional
public List<AttachmentResponse> addAttachmentsToComment(Long commentId, Long userId, List<MultipartFile> files) {
Comment comment = commentRepository.findById(commentId)
.orElseThrow(() -> new GlobalException(ErrorCode.COMMENT_NOT_FOUND));
Expand Down Expand Up @@ -170,7 +171,7 @@ public void deleteAttachment(Long attachmentId, Long userId) {
throw new GlobalException(ErrorCode.FORBIDDEN_ACCESS);
}

attachmentService.deleteAttachments(List.of(attachment));
attachmentService.deleteAttachments(Set.of(attachment));
}

@Transactional
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ public enum ErrorCode {
COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "REVIEW-003", "ν•΄λ‹Ή λŒ“κΈ€μ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."),
CANNOT_RESOLVE_REPLY(HttpStatus.BAD_REQUEST, "REVIEW-004", "λŒ€λŒ“κΈ€μ€ ν•΄κ²° μƒνƒœλ‘œ λ³€κ²½ν•  수 μ—†μŠ΅λ‹ˆλ‹€."),
EMOJI_ON_REPLY_NOT_ALLOWED(HttpStatus.BAD_REQUEST, "REVIEW-005", "이λͺ¨μ§€λŠ” μ΅œμƒμœ„ λŒ“κΈ€μ—λ§Œ μΆ”κ°€ν•  수 μžˆμŠ΅λ‹ˆλ‹€."),

FILE_NOT_FOUND_IN_NODE(HttpStatus.NOT_FOUND, "REVIEW-006", "ν•΄λ‹Ή λ…Έλ“œμ—μ„œ νŒŒμΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."),
INVALID_LINE_RANGE(HttpStatus.NOT_FOUND, "REVIEW-007", "μœ νš¨ν•œ μ½”λ“œ 블둝 λ²”μœ„κ°€ μ•„λ‹™λ‹ˆλ‹€."),

// 방문기둝 κ΄€λ ¨
HISTORY_NOT_FOUND(HttpStatus.NOT_FOUND, "VISITHISTORY-001", "λ°©λ¬Έ 기둝을 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,7 @@ public static class SuggestionAutoResponse {
private Integer total_target_files;
private Integer created;
private List<AutoScopeItem> items;
private String aggregate_node_key;
}

@Getter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ public enum CacheType {
SERVICE_AUTOCOMPLETE("serviceAutocomplete", 2, 1200), // μžλ™μ™„μ„± μΊμ‹œ
USER_SKILLS("userSkills", 1, 500), // μ‚¬μš©μž 개발기술 μΊμ‹œ
MINDMAP_GRAPH_L1("mindmapGraphL1", 100, 1), // 30λΆ„, μ΅œλŒ€ 100개
VISIT_HISTORY_SUMMARY("visitHistorySummary", 1000, 1); // 1μ‹œκ°„, μ΅œλŒ€ 1000개
VISIT_HISTORY_SUMMARY("visitHistorySummary", 1000, 1), // 1μ‹œκ°„, μ΅œλŒ€ 1000개
FILE_CONTENT_L1("fileContentL1", 500, 1);

private final String cacheName;
private final int expiredAfterWrite; // μ‹œκ°„(hour) λ‹¨μœ„
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.teamEWSN.gitdeun.mindmap.dto;

import com.teamEWSN.gitdeun.common.fastapi.dto.RelatedFileDto;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
public class FileWithCodeDto {
private final String fileName;
private final String filePath;
private final String codeContents;
private final int lineCount;

public FileWithCodeDto(RelatedFileDto relatedFile, String codeContents) {
this.fileName = relatedFile.getFileName();
this.filePath = relatedFile.getFilePath();
this.codeContents = codeContents;
this.lineCount = codeContents == null ? 0 : codeContents.split("\\r?\\n").length;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,5 @@
public class NodeCodeResponseDto {
private String nodeKey;
private String nodeLabel;
private List<RelatedFileDto> filePaths;
private Map<RelatedFileDto, String> codeContents; // filePath -> full code content
private List<FileWithCodeDto> files;
}
Loading