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
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
Expand Down Expand Up @@ -99,6 +100,25 @@ public ApiResponse<ApplicationResponse.CommitConnectionResponseDTO> connectCommi
return ApiResponse.onSuccess(applicationCommandService.connectCommit(applicationId, request));
}

@DeleteMapping("/{applicationId}/commits")
@Operation(summary = "커밋 연결 해제 API", description = """
적용사항에 연결된 커밋을 해제하는 API입니다.

해제할 커밋 ID 하나를 `commit_id`로 전달합니다.
예시: `{ "commit_id": 1 }`
요청한 커밋이 연결되어 있지 않으면 요청이 실패합니다.
""")
@ApiErrorCodeExamples({
@ApiErrorCodeExample(value = ErrorStatus.class, name = "_BAD_REQUEST"),
@ApiErrorCodeExample(value = DecisionErrorCode.class, name = "APPLICATION_NOT_FOUND"),
@ApiErrorCodeExample(value = DecisionErrorCode.class, name = "APPLICATION_COMMIT_NOT_CONNECTED")
})
public ApiResponse<ApplicationResponse.CommitConnectionResponseDTO> disconnectCommit(
@PathVariable Long applicationId,
@Valid @RequestBody DecisionRequest.CommitDisconnectionDTO request) {
return ApiResponse.onSuccess(applicationCommandService.disconnectCommit(applicationId, request));
}

@PostMapping("/{decisionId}/recommendations")
@Operation(summary = "추천 결과 저장 API", description = "적용사항의 추천 결과를 저장하는 API입니다.")
@ApiErrorCodeExamples({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ public static class CommitConnectionDTO {
private List<@NotNull Long> commitIds;
}

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
@Schema(description = "커밋 연결 해제 요청")
public static class CommitDisconnectionDTO {

@Schema(description = "해제할 커밋 ID", example = "1")
@NotNull
private Long commitId;
}

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public enum DecisionErrorCode implements BaseErrorCode {
DECISION_NOT_FOUND(HttpStatus.NOT_FOUND, "DECISION_404", "존재하지 않는 결정사항입니다."),
APPLICATION_NOT_FOUND(HttpStatus.NOT_FOUND, "APPLICATION_404", "존재하지 않는 적용사항입니다."),
APPLICATION_COMMIT_ALREADY_CONNECTED(HttpStatus.CONFLICT, "APPLICATION_COMMIT_409", "이미 연결된 커밋입니다."),
APPLICATION_COMMIT_NOT_CONNECTED(HttpStatus.NOT_FOUND, "APPLICATION_COMMIT_404", "연결되지 않은 커밋입니다."),
;

private final HttpStatus httpStatus;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.whylog.server.domain.decision.entity.ApplicationCommitsId;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

Expand All @@ -19,4 +20,11 @@ public interface ApplicationCommitsRepository extends JpaRepository<ApplicationC
ORDER BY c.dateTime DESC, c.id DESC
""")
List<ApplicationCommits> findByApplicationId(@Param("applicationId") Long applicationId);

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("""
DELETE FROM ApplicationCommits ac
WHERE ac.decisionCommits.id IN :decisionCommitIds
""")
void deleteByDecisionCommitsIdIn(@Param("decisionCommitIds") List<Long> decisionCommitIds);
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
package com.whylog.server.domain.decision.repository;

import com.whylog.server.domain.decision.entity.DecisionCommits;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface DecisionCommitsRepository extends JpaRepository<DecisionCommits, Long> {

// 추천 결과 저장시 중복 방지용
Optional<DecisionCommits> findByDecisionIdAndCommitId(Long decisionId, Long commitId);

@Query("""
SELECT dc.id
FROM DecisionCommits dc
WHERE dc.commitId IN :commitIds
""")
List<Long> findIdsByCommitIdIn(@Param("commitIds") List<Long> commitIds);

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("""
DELETE FROM DecisionCommits dc
WHERE dc.commitId IN :commitIds
""")
void deleteByCommitIdIn(@Param("commitIds") List<Long> commitIds);
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,13 @@ public ApplicationResponse.CommitConnectionResponseDTO connectCommit(Long applic
}

// 요청한 커밋 중 하나라도 이미 연결되어 있으면 전체 요청을 실패 처리
if (commitIds.stream().anyMatch(commitConnectionRepository::existsByCommitId)) {
throw new ErrorHandler(DecisionErrorCode.APPLICATION_COMMIT_ALREADY_CONNECTED);
}

List<CommitConnectionId> commitConnectionIds = commitIds.stream()
.map(commitId -> new CommitConnectionId(applicationId, commitId))
.toList();
if (commitConnectionIds.stream().anyMatch(commitConnectionRepository::existsById)) {
throw new ErrorHandler(DecisionErrorCode.APPLICATION_COMMIT_ALREADY_CONNECTED);
}

// 신규 커밋 연결 정보를 저장
List<CommitConnection> commitConnections = commitConnectionIds.stream()
Expand All @@ -69,4 +70,25 @@ public ApplicationResponse.CommitConnectionResponseDTO connectCommit(Long applic
.commitIds(commitIds)
.build();
}

// 적용사항에 연결된 커밋 하나를 해제합니다.
@Transactional
public ApplicationResponse.CommitConnectionResponseDTO disconnectCommit(Long applicationId,
DecisionRequest.CommitDisconnectionDTO request) {
applicationRepository.findById(applicationId)
.orElseThrow(ApplicationNotFoundException::new);

Long commitId = request.getCommitId();
CommitConnectionId commitConnectionId = new CommitConnectionId(applicationId, commitId);
if (!commitConnectionRepository.existsById(commitConnectionId)) {
throw new ErrorHandler(DecisionErrorCode.APPLICATION_COMMIT_NOT_CONNECTED);
}

commitConnectionRepository.deleteById(commitConnectionId);

return ApplicationResponse.CommitConnectionResponseDTO.builder()
.applicationId(applicationId)
.commitIds(List.of(commitId))
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

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.exception.GitErrorCode;
import com.whylog.server.domain.git.service.GitCommandService;
import com.whylog.server.domain.git.service.GitQueryService;
Expand All @@ -17,7 +16,6 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Slice;
import org.springframework.web.bind.annotation.*;
import java.util.List;

Expand All @@ -42,6 +40,7 @@ public class GitController {
""")
@ApiErrorCodeExamples({
@ApiErrorCodeExample(value = ErrorStatus.class, name = "_BAD_REQUEST"),
@ApiErrorCodeExample(value = GitErrorCode.class, name = "GITHUB_TOKEN_INVALID"),
@ApiErrorCodeExample(value = ErrorStatus.class, name = "_INTERNAL_SERVER_ERROR")
})
public ApiResponse<GitResponse.GitHubTokenResponseDTO> registerGitHubToken(
Expand All @@ -50,6 +49,30 @@ public ApiResponse<GitResponse.GitHubTokenResponseDTO> registerGitHubToken(
return ApiResponse.onSuccess(gitCommandService.registerGitHubToken(memberId, request.getAccessToken()));
}

@DeleteMapping("/github/token")
@Operation(
summary = "GitHub Access Token 삭제 API",
description = "현재 로그인한 사용자의 GitHub Access Token을 삭제합니다.")
@ApiErrorCodeExamples({
@ApiErrorCodeExample(value = ErrorStatus.class, name = "_BAD_REQUEST")
})
public ApiResponse<GitResponse.GitHubTokenDeleteResponseDTO> deleteGitHubToken(
@Parameter(hidden = true) @CurrentMember Long memberId) {
return ApiResponse.onSuccess(gitCommandService.deleteGitHubToken(memberId));
}

@GetMapping("/github/token/status")
@Operation(
summary = "GitHub Access Token 등록 여부 조회 API",
description = "현재 로그인한 사용자의 GitHub Access Token 등록 여부를 true/false로 조회합니다.")
@ApiErrorCodeExamples({
@ApiErrorCodeExample(value = ErrorStatus.class, name = "_BAD_REQUEST")
})
public ApiResponse<GitResponse.GitHubTokenStatusResponseDTO> getGitHubTokenStatus(
@Parameter(hidden = true) @CurrentMember Long memberId) {
return ApiResponse.onSuccess(gitQueryService.getGitHubTokenStatus(memberId));
}

@GetMapping("/teams/{teamId}/repositories")
@Operation(
summary = "팀의 연동된 레포지토리 목록 조회",
Expand Down Expand Up @@ -109,12 +132,32 @@ public ApiResponse<GitResponse.RepositorySyncResponseDTO> syncRepository(
return ApiResponse.onSuccess(GitResponse.RepositorySyncResponseDTO.from(repositoryId));
}

@DeleteMapping("/repositories/{repositoryId}")
@Operation(
summary = "GitHub 레포지토리 삭제",
description = """
등록된 레포지토리를 삭제합니다.

레포지토리를 삭제하면 관련된 커밋, 커밋 분석, 연결/추천 커밋 데이터도 함께 삭제됩니다.
""")
@ApiErrorCodeExamples({
@ApiErrorCodeExample(value = ErrorStatus.class, name = "_BAD_REQUEST"),
@ApiErrorCodeExample(value = GitErrorCode.class, name = "REPOSITORY_NOT_FOUND")
})
public ApiResponse<GitResponse.RepositoryDeleteResponseDTO> deleteRepository(
@PathVariable Long repositoryId) {
return ApiResponse.onSuccess(gitCommandService.deleteRepository(repositoryId));
}

@GetMapping("/repositories/{repositoryId}/commits")
@Operation(
summary = "커밋 목록 조회 (커서 기반 무한스크롤)",
description = """
10개씩 커밋을 조회합니다.

각 커밋에는 기본 정보와 함께 연결된 적용사항 정보가 포함됩니다.
커밋이 적용사항에 연결되어 있지 않다면 `connectedApplication`은 null로 반환됩니다.

📌 사용 방법:
1. 첫 요청: cursor 파라미터 없음
2. 응답의 hasNext가 true면, nextCursorId를 cursor로 다시 요청
Expand All @@ -124,6 +167,7 @@ public ApiResponse<GitResponse.RepositorySyncResponseDTO> syncRepository(
- hasNext: 다음 페이지 존재 여부 (더 불러올 커밋이 있으면 true)
- nextCursorId: 다음 요청에 사용할 커서 ID
- isFirst: 첫 페이지 여부
- connectedApplication: 커밋에 연결된 적용사항 정보 (없으면 null)
""")
@ApiErrorCodeExamples({
@ApiErrorCodeExample(value = ErrorStatus.class, name = "_BAD_REQUEST"),
Expand All @@ -133,13 +177,7 @@ 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));
return ApiResponse.onSuccess(gitQueryService.getCommitListResponse(repositoryId, cursor));
}

@GetMapping("/repositories/{repositoryId}/commits/{commitHash}")
Expand Down Expand Up @@ -172,6 +210,3 @@ public ApiResponse<GitResponse.CommitDetailDTO> getCommitDetail(
return ApiResponse.onSuccess(commitDetail);
}
}



Loading
Loading