Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ dependencies {
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

// GitHub API
implementation 'org.kohsuke:github-api:1.321'

}

tasks.named('test') {
Expand Down
133 changes: 108 additions & 25 deletions src/main/java/com/whylog/server/domain/git/controller/GitController.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@

import com.whylog.server.domain.git.dto.GitRequest;
import com.whylog.server.domain.git.dto.GitResponse;
import com.whylog.server.domain.git.entity.Commit;
import com.whylog.server.domain.git.service.GitCommandService;
import com.whylog.server.domain.git.service.GitQueryService;
import com.whylog.server.global.apiPayload.ApiResponse;
import com.whylog.server.global.auth.annotation.CurrentMember;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import org.springframework.data.domain.Slice;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
Expand All @@ -22,39 +22,122 @@
@Tag(name = "Git", description = "깃 관련 API")
public class GitController {

private final GitCommandService gitCommandService;
private final GitQueryService gitQueryService;

@PostMapping("/github/token")
@Operation(
summary = "GitHub Access Token 등록 API",
description = """
GitHub API를 사용하기 위한 Personal Access Token을 등록하는 API입니다.
- 사용자가 GitHub에서 발급한 Personal Access Token을 저장합니다.
- 저장된 토큰은 이후 레포지토리 동기화 시 자동으로 사용됩니다.
- GitHub Personal Access Token 발급 필요 (repo 권한 포함)
- GitHub Settings > Developer settings > Personal access tokens에서 발급
""")
public ApiResponse<GitResponse.GitHubTokenResponseDTO> registerGitHubToken(
@Parameter(hidden = true) @CurrentMember Long memberId,
@Valid @RequestBody GitRequest.GitHubTokenDTO request) {
return ApiResponse.onSuccess(gitCommandService.registerGitHubToken(memberId, request.getAccessToken()));
}

@GetMapping("/teams/{teamId}/repositories")
@Operation(summary = "레포 목록 조회 API", description = "특정 팀의 레포 목록을 조회하는 API입니다.")
@Operation(
summary = "팀의 연동된 레포지토리 목록 조회",
description = """
팀에 연동된 GitHub 레포지토리 목록을 조회합니다.(페이징 없음)

정렬 순서:
1. 최근 동기화한 레포지토리 (동기화 시간 최신순)
2. 동기화된 적 없는 레포지토리 (추가한순)
""")
public ApiResponse<List<GitResponse.RepositoryDTO>> getRepositories(
@PathVariable Long teamId) {
return ApiResponse.onSuccess(null);
return ApiResponse.onSuccess(
gitQueryService.getRepositories(teamId).stream()
.map(GitResponse.RepositoryDTO::from)
.toList()
);
}

@PostMapping("/teams/{teamId}/repositories")
@Operation(summary = "레포 추가 API", description = "팀에 새로운 레포를 추가하는 API입니다.")
@Operation(
summary = "GitHub 레포지토리 추가",
description = "GitHub 레포지토리를 팀에 연동합니다. 등록 시에는 레포 정보만 저장되며, 커밋은 동기화 API를 호출할 때 수집됩니다.")
public ApiResponse<GitResponse.RepositoryCreateResponseDTO> createRepository(
@Parameter(hidden = true) @CurrentMember Long memberId,
@PathVariable Long teamId,
@Valid @RequestBody GitRequest.RepositoryCreateDTO request) {
return ApiResponse.onSuccess(null);
var repository = gitCommandService.createRepository(memberId, teamId, request);
return ApiResponse.onSuccess(GitResponse.RepositoryCreateResponseDTO.from(repository));
}

@GetMapping("/repositories/{repositoryId}/commits")
@Operation(summary = "커밋 목록 조회 API", description = "특정 레포의 커밋 목록을 조회하는 API입니다.")
public ApiResponse<List<GitResponse.CommitDTO>> getCommits(
@PostMapping("/repositories/{repositoryId}/sync")
@Operation(
summary = "GitHub 레포지토리 동기화",
description = "등록된 레포지토리의 최신 커밋을 DB에 저장합니다. 마지막 동기화 이후의 새 커밋만 저장되며 Merge 커밋은 제외됩니다.")
public ApiResponse<GitResponse.RepositorySyncResponseDTO> syncRepository(
@Parameter(hidden = true) @CurrentMember Long memberId,
@PathVariable Long repositoryId) {
return ApiResponse.onSuccess(null);

gitCommandService.syncRepository(memberId, repositoryId);
return ApiResponse.onSuccess(GitResponse.RepositorySyncResponseDTO.from(repositoryId));
}

@GetMapping("/commits/{commitId}")
@Operation(summary = "커밋 상세 조회 API", description = "특정 커밋의 상세 정보를 조회하는 API입니다.")
public ApiResponse<GitResponse.CommitDetailDTO> getCommitDetail(
@PathVariable Long commitId) {
return ApiResponse.onSuccess(null);
@GetMapping("/repositories/{repositoryId}/commits")
@Operation(
summary = "커밋 목록 조회 (커서 기반 무한스크롤)",
description = """
10개씩 커밋을 조회합니다.

📌 사용 방법:
1. 첫 요청: cursor 파라미터 없음
2. 응답의 hasNext가 true면, nextCursorId를 cursor로 다시 요청
3. hasNext가 false가 나올 때까지 반복

💡 응답 필드:
- hasNext: 다음 페이지 존재 여부 (더 불러올 커밋이 있으면 true)
- nextCursorId: 다음 요청에 사용할 커서 ID
- isFirst: 첫 페이지 여부
""")
public ApiResponse<GitResponse.CommitListResponseDTO> getCommitsCursor(
@PathVariable Long repositoryId,
@Parameter(description = "이전 조회의 마지막 커밋 ID (첫 요청 시 생략)")
@RequestParam(required = false) Long cursor) {

Slice<Commit> commitSlice = gitQueryService.getCommitsByRepository(
repositoryId,
cursor
);

return ApiResponse.onSuccess(GitResponse.CommitListResponseDTO.from(commitSlice, cursor));
}

@PostMapping("/repositories/{repositoryId}/sync")
@Operation(summary = "레포 동기화 API", description = "레포를 동기화하여 최신 커밋 정보를 가져오는 API입니다.")
public ApiResponse<GitResponse.RepositorySyncResponseDTO> syncRepository(
@PathVariable Long repositoryId) {
return ApiResponse.onSuccess(null);
@GetMapping("/repositories/{repositoryId}/commits/{commitHash}")
@Operation(
summary = "커밋 상세 조회",
description = """
커밋의 전체 정보와 변경된 파일 목록을 조회합니다.

- DB에서 가져오는 정보: hash, message, authorName, authorEmail, authorProfileImage, dateTime, description, changedFileCount
- (참고) authorName, authorEmail, authorProfileImage는 깃허브에서 가져와서 저장한 정보입니다.
- GitHub API에서 실시간 조회: changedFileList (변경된 파일 및 코드)

⚙️ 처리 방식:
- GitHub API 호출 완료 후 응답 (비동기 아님)
- GitHub API 호출 실패 시 변경 파일 리스트는 빈 배열로 반환

💡 프론트 구현시 참고 사항:
- changedCode를 react-diff-viewer-continued 라이브러리 사용하면 될거같으니 참고해주세요!
""")
public ApiResponse<GitResponse.CommitDetailDTO> getCommitDetail(
@Parameter(hidden = true) @CurrentMember Long memberId,
@PathVariable Long repositoryId,
@PathVariable String commitHash) {
GitResponse.CommitDetailDTO commitDetail = gitQueryService.getCommitByHash(memberId, repositoryId, commitHash);
return ApiResponse.onSuccess(commitDetail);
}
}



47 changes: 47 additions & 0 deletions src/main/java/com/whylog/server/domain/git/dto/GitRequest.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package com.whylog.server.domain.git.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.*;
import org.hibernate.validator.constraints.URL;

import java.time.LocalDateTime;

public class GitRequest {

@Getter
Expand All @@ -23,5 +26,49 @@ public static class RepositoryCreateDTO {
private String url;
}

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
@Schema(description = "GitHub Access Token 등록 요청")
public static class GitHubTokenDTO {

@Schema(description = "GitHub Access Token", example = "ghp_jv******")
@NotBlank
@JsonProperty("access_token")
private String accessToken;
}

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
@Schema(description = "커밋 생성 DTO")
public static class CommitCreateDTO {

@Schema(description = "커밋 해시", example = "abc123def456")
private String hash;

@Schema(description = "커밋 메시지", example = "[feat] 사용자 인증 기능 추가")
private String message;

@Schema(description = "작성자 이름", example = "김준용")
private String authorName;

@Schema(description = "작성자 이메일", example = "user@example.com")
private String authorEmail;

@Schema(description = "작성자 프로필 이미지", example = "https://avatars.githubusercontent.com/u/123?v=4")
private String authorProfileImage;

@Schema(description = "커밋 날짜", example = "2026-03-24T10:30:00")
private LocalDateTime dateTime;

@Schema(description = "추가된 줄 수", example = "45")
private Integer addedLines;

@Schema(description = "삭제된 줄 수", example = "12")
private Integer deletedLines;
}
}

Loading
Loading