diff --git a/src/main/java/com/whylog/server/domain/decision/controller/ApplicationController.java b/src/main/java/com/whylog/server/domain/decision/controller/ApplicationController.java index 622c406..986e319 100644 --- a/src/main/java/com/whylog/server/domain/decision/controller/ApplicationController.java +++ b/src/main/java/com/whylog/server/domain/decision/controller/ApplicationController.java @@ -3,7 +3,9 @@ import com.whylog.server.domain.decision.dto.ApplicationResponse; import com.whylog.server.domain.decision.dto.DecisionRequest; import com.whylog.server.domain.decision.exception.DecisionErrorCode; +import com.whylog.server.domain.decision.service.ApplicationCommandService; import com.whylog.server.domain.decision.service.ApplicationQueryService; +import com.whylog.server.domain.git.exception.GitErrorCode; import com.whylog.server.global.apiPayload.ApiResponse; import com.whylog.server.global.apiPayload.annotation.ApiErrorCodeExample; import com.whylog.server.global.apiPayload.annotation.ApiErrorCodeExamples; @@ -27,6 +29,7 @@ @Tag(name = "Application", description = "적용사항 관련 API") public class ApplicationController { + private final ApplicationCommandService applicationCommandService; private final ApplicationQueryService applicationQueryService; @@ -42,34 +45,71 @@ public ApiResponse getApplication( } @GetMapping("/{applicationId}/recommended-commits") - @Operation(summary = "추천 커밋 조회 API", description = "특정 적용사항의 추천 커밋 목록을 조회하는 API입니다.") + @Operation(summary = "추천 커밋 조회 API", description = "특정 적용사항의 추천 커밋 목록을 조회하는 API입니다.최신순으로 조회, 페이징 없습니다.") + @ApiErrorCodeExamples({ + @ApiErrorCodeExample(value = ErrorStatus.class, name = "_BAD_REQUEST"), + @ApiErrorCodeExample(value = DecisionErrorCode.class, name = "APPLICATION_NOT_FOUND"), + @ApiErrorCodeExample(value = GitErrorCode.class, name = "COMMIT_NOT_FOUND") + }) public ApiResponse> getRecommendedCommits( @PathVariable Long applicationId) { - return ApiResponse.onSuccess(null); + return ApiResponse.onSuccess(applicationQueryService.getRecommendedCommits(applicationId)); } @GetMapping("/{applicationId}/connected-commits") - @Operation(summary = "연결된 커밋 조회 API", description = "특정 적용사항에 연결된 커밋 목록을 조회하는 API입니다.") - public ApiResponse> getConnectedCommits( + @Operation(summary = "연결된 커밋 조회 API", description = "특정 적용사항에 연결된 커밋 목록을 조회하는 API입니다.최신순으로 조회, 페이징 없습니다.") + @ApiErrorCodeExamples({ + @ApiErrorCodeExample(value = ErrorStatus.class, name = "_BAD_REQUEST"), + @ApiErrorCodeExample(value = DecisionErrorCode.class, name = "APPLICATION_NOT_FOUND"), + }) + public ApiResponse getConnectedCommits( @PathVariable Long applicationId) { - return ApiResponse.onSuccess(null); + return ApiResponse.onSuccess(applicationQueryService.getConnectedCommits(applicationId)); + } + + @GetMapping("/{applicationId}/status") + @Operation(summary = "적용현황 조회 API", description = "특정 적용사항에 연결된 커밋 해시, 커밋 메시지, 연결된 커밋 개수를 조회하는 API입니다.") + @ApiErrorCodeExamples({ + @ApiErrorCodeExample(value = ErrorStatus.class, name = "_BAD_REQUEST"), + @ApiErrorCodeExample(value = DecisionErrorCode.class, name = "APPLICATION_NOT_FOUND") + }) + public ApiResponse getApplicationStatus( + @PathVariable Long applicationId) { + return ApiResponse.onSuccess(applicationQueryService.getApplicationStatus(applicationId)); } @PostMapping("/{applicationId}/commits") - @Operation(summary = "커밋 연결 API", description = "적용사항에 커밋을 연결하는 API입니다.") + @Operation(summary = "커밋 연결 API", description = """ + 적용사항에 커밋을 연결하는 API입니다. + + 단일 연결과 다중 연결 모두 `commit_ids` 배열로 전달합니다. + 단건 연결 예시: `{ "commit_ids": [1] }` + 다건 연결 예시: `{ "commit_ids": [1, 2, 3] }` + 요청한 커밋 중 하나라도 이미 연결되어 있으면 전체 요청이 실패합니다. + """) + @ApiErrorCodeExamples({ + @ApiErrorCodeExample(value = ErrorStatus.class, name = "_BAD_REQUEST"), + @ApiErrorCodeExample(value = DecisionErrorCode.class, name = "APPLICATION_NOT_FOUND"), + @ApiErrorCodeExample(value = GitErrorCode.class, name = "COMMIT_NOT_FOUND"), + @ApiErrorCodeExample(value = DecisionErrorCode.class, name = "APPLICATION_COMMIT_ALREADY_CONNECTED") + }) public ApiResponse connectCommit( @PathVariable Long applicationId, @Valid @RequestBody DecisionRequest.CommitConnectionDTO request) { - return ApiResponse.onSuccess(null); + return ApiResponse.onSuccess(applicationCommandService.connectCommit(applicationId, request)); } @PostMapping("/{decisionId}/recommendations") @Operation(summary = "추천 결과 저장 API", description = "적용사항의 추천 결과를 저장하는 API입니다.") + @ApiErrorCodeExamples({ + @ApiErrorCodeExample(value = ErrorStatus.class, name = "_BAD_REQUEST"), + @ApiErrorCodeExample(value = DecisionErrorCode.class, name = "DECISION_NOT_FOUND"), + @ApiErrorCodeExample(value = GitErrorCode.class, name = "COMMIT_NOT_FOUND") + }) public ApiResponse saveRecommendation( @PathVariable Long decisionId, @Valid @RequestBody DecisionRequest.RecommendationDTO request) { return ApiResponse.onSuccess(null); } - // TODO: 적용현황 조회 api 추가 } diff --git a/src/main/java/com/whylog/server/domain/decision/controller/DecisionController.java b/src/main/java/com/whylog/server/domain/decision/controller/DecisionController.java index cb64fef..0ab3aaf 100644 --- a/src/main/java/com/whylog/server/domain/decision/controller/DecisionController.java +++ b/src/main/java/com/whylog/server/domain/decision/controller/DecisionController.java @@ -1,8 +1,12 @@ package com.whylog.server.domain.decision.controller; -import com.whylog.server.domain.decision.dto.ApplicationResponse; import com.whylog.server.domain.decision.dto.DecisionResponse; +import com.whylog.server.domain.decision.exception.DecisionErrorCode; +import com.whylog.server.domain.decision.service.DecisionQueryService; import com.whylog.server.global.apiPayload.ApiResponse; +import com.whylog.server.global.apiPayload.annotation.ApiErrorCodeExample; +import com.whylog.server.global.apiPayload.annotation.ApiErrorCodeExamples; +import com.whylog.server.global.apiPayload.code.status.ErrorStatus; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; @@ -11,31 +15,22 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.util.List; - @RestController @RequestMapping("/api/decisions") @RequiredArgsConstructor @Tag(name = "Decision", description = "결정사항 관련 API") public class DecisionController { + private final DecisionQueryService decisionQueryService; + @GetMapping("/{decisionId}/reliability") @Operation(summary = "신뢰도 조회 API", description = "특정 결정사항의 신뢰도 정보를 조회하는 API입니다.") + @ApiErrorCodeExamples({ + @ApiErrorCodeExample(value = ErrorStatus.class, name = "_BAD_REQUEST"), + @ApiErrorCodeExample(value = DecisionErrorCode.class, name = "DECISION_NOT_FOUND") + }) public ApiResponse getReliability( @PathVariable Long decisionId) { - return ApiResponse.onSuccess(null); - } - - @Operation(summary = "추천 커밋 조회 API", description = "특정 적용사항의 추천 커밋 목록을 조회하는 API입니다.") - public ApiResponse> getRecommendedCommits( - @PathVariable Long applicationId) { - return ApiResponse.onSuccess(null); - } - - @GetMapping("/{applicationId}/connected-commits") - @Operation(summary = "연결된 커밋 조회 API", description = "특정 적용사항에 연결된 커밋 목록을 조회하는 API입니다.") - public ApiResponse> getConnectedCommits( - @PathVariable Long applicationId) { - return ApiResponse.onSuccess(null); + return ApiResponse.onSuccess(decisionQueryService.getReliability(decisionId)); } } diff --git a/src/main/java/com/whylog/server/domain/decision/dto/ApplicationResponse.java b/src/main/java/com/whylog/server/domain/decision/dto/ApplicationResponse.java index 97b8089..66a16a0 100644 --- a/src/main/java/com/whylog/server/domain/decision/dto/ApplicationResponse.java +++ b/src/main/java/com/whylog/server/domain/decision/dto/ApplicationResponse.java @@ -113,7 +113,21 @@ public static class DecisionReasonItemDTO { @NoArgsConstructor @AllArgsConstructor @Builder - @Schema(description = "적용현황 항목") + @Schema(description = "적용현황 조회 응답") + public static class ApplicationStatusDTO { + + @Schema(description = "연결된 커밋 개수", example = "3") + private Integer commitCount; + + @Schema(description = "적용현황 커밋 목록") + private List commits; + } + + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + @Schema(description = "적용현황 커밋 항목") public static class ApplicationBaseItemDTO { @Schema(description = "커밋 해시", example = "b8fd9ad") @@ -121,9 +135,6 @@ public static class ApplicationBaseItemDTO { @Schema(description = "커밋 메시지", example = "feat: API 구현") private String commitMessage; - - @Schema(description = "세부 사항 목록", example = "SessionRepository 추상화 도입") - private List details; //ai 분석 응답 } @Getter @@ -133,9 +144,6 @@ public static class ApplicationBaseItemDTO { @Schema(description = "추천 커밋 응답") public static class RecommendedCommitDTO { - @Schema(description = "적용사항 ID", example = "1") - private Long applicationId; - @Schema(description = "저장소 이름", example = "whyLog-Backend") private String repositoryName; @@ -152,6 +160,20 @@ public static class RecommendedCommitDTO { private String reason; } + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + @Schema(description = "연결된 커밋 목록 조회 응답") + public static class ConnectedCommitListDTO { + + @Schema(description = "연결된 커밋 개수", example = "3") + private Integer commitCount; + + @Schema(description = "연결된 커밋 목록") + private List commits; + } + @Getter @NoArgsConstructor @AllArgsConstructor @@ -182,9 +204,8 @@ public static class CommitConnectionResponseDTO { @Schema(description = "적용사항 ID", example = "1") private Long applicationId; - @Schema(description = "커밋 ID", example = "abc123def456") - private String commitId; - + @Schema(description = "연결된 커밋 ID 목록", example = "[1, 2, 3]") + private List commitIds; } } diff --git a/src/main/java/com/whylog/server/domain/decision/dto/DecisionRequest.java b/src/main/java/com/whylog/server/domain/decision/dto/DecisionRequest.java index 002b748..7b1dbc9 100644 --- a/src/main/java/com/whylog/server/domain/decision/dto/DecisionRequest.java +++ b/src/main/java/com/whylog/server/domain/decision/dto/DecisionRequest.java @@ -1,7 +1,9 @@ package com.whylog.server.domain.decision.dto; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import java.util.List; import lombok.*; public class DecisionRequest { @@ -13,9 +15,9 @@ public class DecisionRequest { @Schema(description = "커밋 연결 요청") public static class CommitConnectionDTO { - @Schema(description = "커밋 ID", example = "1") - @NotBlank - private Long commitId; + @Schema(description = "연결할 커밋 ID 목록. 단건 연결도 배열로 전달합니다.", example = "[1, 2, 3]") + @NotEmpty + private List<@NotNull Long> commitIds; } @Getter @@ -26,7 +28,7 @@ public static class CommitConnectionDTO { public static class RecommendationDTO { @Schema(description = "추천 커밋 ID", example = "1") - @NotBlank + @NotNull private Long commitId; @Schema(description = "추천 이유", example = "이 커밋은 관련된 이슈를 해결하는 커밋입니다.") diff --git a/src/main/java/com/whylog/server/domain/decision/entity/Decision.java b/src/main/java/com/whylog/server/domain/decision/entity/Decision.java index 6e7f507..e38dd50 100644 --- a/src/main/java/com/whylog/server/domain/decision/entity/Decision.java +++ b/src/main/java/com/whylog/server/domain/decision/entity/Decision.java @@ -37,6 +37,9 @@ public class Decision extends BaseEntity { @Column(name = "is_created") private Boolean isCreated; + @Column(name = "reliability_score") + private Integer reliabilityScore; + @OneToMany(mappedBy = "decision", cascade = CascadeType.ALL, orphanRemoval = true) private final List applications = new ArrayList<>(); // @@ -45,15 +48,6 @@ public class Decision extends BaseEntity { // // @OneToMany(mappedBy = "decision", cascade = CascadeType.ALL, orphanRemoval = true) // private final List decisionBases = new ArrayList<>(); -// -// @OneToMany(mappedBy = "decision", cascade = CascadeType.ALL, orphanRemoval = true) -// private final List decisionCommits = new ArrayList<>(); -// -// @OneToMany(mappedBy = "decision", cascade = CascadeType.ALL, orphanRemoval = true) -// private final List commitConnections = new ArrayList<>(); -// -// @OneToMany(mappedBy = "decision", cascade = CascadeType.ALL, orphanRemoval = true) -// private final List effectRatios = new ArrayList<>(); public static Decision create(Meeting meeting, boolean isCreated) { Decision decision = new Decision(); @@ -61,4 +55,8 @@ public static Decision create(Meeting meeting, boolean isCreated) { decision.isCreated = isCreated; return decision; } + + public void updateReliabilityScore(Integer reliabilityScore) { + this.reliabilityScore = reliabilityScore; + } } diff --git a/src/main/java/com/whylog/server/domain/decision/entity/DecisionCommits.java b/src/main/java/com/whylog/server/domain/decision/entity/DecisionCommits.java index 40f5c00..8182026 100644 --- a/src/main/java/com/whylog/server/domain/decision/entity/DecisionCommits.java +++ b/src/main/java/com/whylog/server/domain/decision/entity/DecisionCommits.java @@ -1,6 +1,5 @@ package com.whylog.server.domain.decision.entity; -import com.whylog.server.domain.git.entity.Commit; import com.whylog.server.global.entity.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -11,13 +10,20 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @Entity @Getter -@Table(name = "Decision_Commits") +@Table( + name = "Decision_Commits", + uniqueConstraints = @UniqueConstraint( + name = "uk_decision_commits_decision_commit", + columnNames = {"decision_id", "commit_id"} + ) +) @NoArgsConstructor(access = AccessLevel.PROTECTED) public class DecisionCommits extends BaseEntity { @@ -32,4 +38,20 @@ public class DecisionCommits extends BaseEntity { @Column(name = "commit_id", nullable = false) private Long commitId; // 매핑 X, + + @Column(name = "reason", columnDefinition = "TEXT") + private String reason; + + public static DecisionCommits create(Decision decision, Long commitId, String reason) { + DecisionCommits decisionCommits = new DecisionCommits(); + decisionCommits.decision = decision; + decisionCommits.commitId = commitId; + decisionCommits.reason = reason; + return decisionCommits; + } + + // 저장시 + public void updateReason(String reason) { + this.reason = reason; + } } diff --git a/src/main/java/com/whylog/server/domain/decision/entity/EffectRatio.java b/src/main/java/com/whylog/server/domain/decision/entity/EffectRatio.java deleted file mode 100644 index 54ceb26..0000000 --- a/src/main/java/com/whylog/server/domain/decision/entity/EffectRatio.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.whylog.server.domain.decision.entity; - -import com.whylog.server.global.entity.BaseEntity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@Getter -@Table(name = "Effect_Ratio") -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class EffectRatio extends BaseEntity { - - @Id - @Column(name = "effect_ratio_id") - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "decision_id", nullable = false) - private Decision decision; - - @Column(name = "repository_name", length = 100) - private String repositoryName; - - private Integer ratio; -} diff --git a/src/main/java/com/whylog/server/domain/decision/exception/DecisionErrorCode.java b/src/main/java/com/whylog/server/domain/decision/exception/DecisionErrorCode.java index 3787764..1709e84 100644 --- a/src/main/java/com/whylog/server/domain/decision/exception/DecisionErrorCode.java +++ b/src/main/java/com/whylog/server/domain/decision/exception/DecisionErrorCode.java @@ -12,6 +12,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", "이미 연결된 커밋입니다."), ; private final HttpStatus httpStatus; diff --git a/src/main/java/com/whylog/server/domain/decision/repository/ApplicationBaseRepository.java b/src/main/java/com/whylog/server/domain/decision/repository/ApplicationBaseRepository.java index c9309d6..afeeb9c 100644 --- a/src/main/java/com/whylog/server/domain/decision/repository/ApplicationBaseRepository.java +++ b/src/main/java/com/whylog/server/domain/decision/repository/ApplicationBaseRepository.java @@ -3,6 +3,7 @@ import com.whylog.server.domain.decision.entity.ApplicationBase; import com.whylog.server.domain.decision.entity.ApplicationBaseId; 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; @@ -19,4 +20,12 @@ public interface ApplicationBaseRepository extends JpaRepository findByApplicationId(@Param("applicationId") Long applicationId); + + @Modifying + @Query("DELETE FROM ApplicationBase ab WHERE ab.application.decision.meeting.team.id = :teamId") + void deleteByTeamId(@Param("teamId") Long teamId); + + @Modifying + @Query("DELETE FROM ApplicationBase ab WHERE ab.application.decision.meeting.id = :meetingId") + void deleteByMeetingId(@Param("meetingId") Long meetingId); } diff --git a/src/main/java/com/whylog/server/domain/decision/repository/ApplicationCommitsRepository.java b/src/main/java/com/whylog/server/domain/decision/repository/ApplicationCommitsRepository.java new file mode 100644 index 0000000..2fab734 --- /dev/null +++ b/src/main/java/com/whylog/server/domain/decision/repository/ApplicationCommitsRepository.java @@ -0,0 +1,22 @@ +package com.whylog.server.domain.decision.repository; + +import com.whylog.server.domain.decision.entity.ApplicationCommits; +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.Query; +import org.springframework.data.repository.query.Param; + +public interface ApplicationCommitsRepository extends JpaRepository { + + // 적용사항에 추천된 커밋 연결 목록을 최신순으로 조회합니다. + @Query(""" + SELECT ac + FROM ApplicationCommits ac + JOIN FETCH ac.decisionCommits dc + JOIN Commit c ON c.id = dc.commitId + WHERE ac.application.id = :applicationId + ORDER BY c.dateTime DESC, c.id DESC + """) + List findByApplicationId(@Param("applicationId") Long applicationId); +} diff --git a/src/main/java/com/whylog/server/domain/decision/repository/ApplicationTimelineRepository.java b/src/main/java/com/whylog/server/domain/decision/repository/ApplicationTimelineRepository.java index 7cb8950..ac5b53b 100644 --- a/src/main/java/com/whylog/server/domain/decision/repository/ApplicationTimelineRepository.java +++ b/src/main/java/com/whylog/server/domain/decision/repository/ApplicationTimelineRepository.java @@ -3,6 +3,7 @@ import com.whylog.server.domain.decision.entity.ApplicationTimeline; import com.whylog.server.domain.decision.entity.ApplicationTimelineId; 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; @@ -19,4 +20,12 @@ public interface ApplicationTimelineRepository extends JpaRepository findByApplicationId(@Param("applicationId") Long applicationId); + + @Modifying + @Query("DELETE FROM ApplicationTimeline at WHERE at.application.decision.meeting.team.id = :teamId") + void deleteByTeamId(@Param("teamId") Long teamId); + + @Modifying + @Query("DELETE FROM ApplicationTimeline at WHERE at.application.decision.meeting.id = :meetingId") + void deleteByMeetingId(@Param("meetingId") Long meetingId); } diff --git a/src/main/java/com/whylog/server/domain/decision/repository/DecisionBaseRepository.java b/src/main/java/com/whylog/server/domain/decision/repository/DecisionBaseRepository.java index 4b5c554..c1eebef 100644 --- a/src/main/java/com/whylog/server/domain/decision/repository/DecisionBaseRepository.java +++ b/src/main/java/com/whylog/server/domain/decision/repository/DecisionBaseRepository.java @@ -2,6 +2,17 @@ import com.whylog.server.domain.decision.entity.DecisionBase; 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 DecisionBaseRepository extends JpaRepository { + + @Modifying + @Query("DELETE FROM DecisionBase db WHERE db.decision.meeting.team.id = :teamId") + void deleteByTeamId(@Param("teamId") Long teamId); + + @Modifying + @Query("DELETE FROM DecisionBase db WHERE db.decision.meeting.id = :meetingId") + void deleteByMeetingId(@Param("meetingId") Long meetingId); } diff --git a/src/main/java/com/whylog/server/domain/decision/repository/DecisionCommitsRepository.java b/src/main/java/com/whylog/server/domain/decision/repository/DecisionCommitsRepository.java new file mode 100644 index 0000000..8a09443 --- /dev/null +++ b/src/main/java/com/whylog/server/domain/decision/repository/DecisionCommitsRepository.java @@ -0,0 +1,11 @@ +package com.whylog.server.domain.decision.repository; + +import com.whylog.server.domain.decision.entity.DecisionCommits; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface DecisionCommitsRepository extends JpaRepository { + + // 추천 결과 저장시 중복 방지용 + Optional findByDecisionIdAndCommitId(Long decisionId, Long commitId); +} diff --git a/src/main/java/com/whylog/server/domain/decision/repository/DecisionTimelineRepository.java b/src/main/java/com/whylog/server/domain/decision/repository/DecisionTimelineRepository.java index 488ed0d..45e87ed 100644 --- a/src/main/java/com/whylog/server/domain/decision/repository/DecisionTimelineRepository.java +++ b/src/main/java/com/whylog/server/domain/decision/repository/DecisionTimelineRepository.java @@ -2,6 +2,17 @@ import com.whylog.server.domain.decision.entity.DecisionTimeline; 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 DecisionTimelineRepository extends JpaRepository { + + @Modifying + @Query("DELETE FROM DecisionTimeline dt WHERE dt.decision.meeting.team.id = :teamId") + void deleteByTeamId(@Param("teamId") Long teamId); + + @Modifying + @Query("DELETE FROM DecisionTimeline dt WHERE dt.decision.meeting.id = :meetingId") + void deleteByMeetingId(@Param("meetingId") Long meetingId); } diff --git a/src/main/java/com/whylog/server/domain/decision/service/ApplicationCommandService.java b/src/main/java/com/whylog/server/domain/decision/service/ApplicationCommandService.java new file mode 100644 index 0000000..a8efa61 --- /dev/null +++ b/src/main/java/com/whylog/server/domain/decision/service/ApplicationCommandService.java @@ -0,0 +1,72 @@ +package com.whylog.server.domain.decision.service; + +import com.whylog.server.domain.decision.dto.ApplicationResponse; +import com.whylog.server.domain.decision.dto.DecisionRequest; +import com.whylog.server.domain.decision.entity.Application; +import com.whylog.server.domain.decision.exception.DecisionErrorCode; +import com.whylog.server.domain.decision.exception.ApplicationNotFoundException; +import com.whylog.server.domain.decision.repository.ApplicationRepository; +import com.whylog.server.domain.git.entity.Commit; +import com.whylog.server.domain.git.entity.CommitConnection; +import com.whylog.server.domain.git.entity.CommitConnectionId; +import com.whylog.server.domain.git.exception.GitErrorCode; +import com.whylog.server.domain.git.repository.CommitConnectionRepository; +import com.whylog.server.domain.git.repository.CommitRepository; +import com.whylog.server.global.apiPayload.exception.handler.ErrorHandler; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class ApplicationCommandService { + + private final ApplicationRepository applicationRepository; + private final CommitRepository commitRepository; + private final CommitConnectionRepository commitConnectionRepository; + + // 적용사항에 하나 이상의 커밋을 연결합니다. + @Transactional + public ApplicationResponse.CommitConnectionResponseDTO connectCommit(Long applicationId, + DecisionRequest.CommitConnectionDTO request) { + // 적용사항 존재 여부 검증 + Application application = applicationRepository.findById(applicationId) + .orElseThrow(ApplicationNotFoundException::new); + + // 요청으로 전달된 커밋 ID 목록을 사용 + List commitIds = request.getCommitIds(); + + // 요청된 커밋 ID에 해당하는 커밋을 조회 + Map commitsById = commitRepository.findAllById(commitIds).stream() + .collect(Collectors.toMap(Commit::getId, Function.identity())); + + // 요청한 커밋 중 존재하지 않는 커밋이 있으면 예외 처리 + if (commitsById.size() != commitIds.size()) { + throw new ErrorHandler(GitErrorCode.COMMIT_NOT_FOUND); + } + + // 요청한 커밋 중 하나라도 이미 연결되어 있으면 전체 요청을 실패 처리 + List 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 commitConnections = commitConnectionIds.stream() + .map(commitConnectionId -> CommitConnection.create(application, commitsById.get(commitConnectionId.getCommitId()))) + .toList(); + + commitConnectionRepository.saveAll(commitConnections); + + return ApplicationResponse.CommitConnectionResponseDTO.builder() + .applicationId(applicationId) + .commitIds(commitIds) + .build(); + } +} diff --git a/src/main/java/com/whylog/server/domain/decision/service/ApplicationQueryService.java b/src/main/java/com/whylog/server/domain/decision/service/ApplicationQueryService.java index 8ac93a0..413497d 100644 --- a/src/main/java/com/whylog/server/domain/decision/service/ApplicationQueryService.java +++ b/src/main/java/com/whylog/server/domain/decision/service/ApplicationQueryService.java @@ -3,15 +3,23 @@ import com.whylog.server.domain.decision.dto.ApplicationResponse; import com.whylog.server.domain.decision.entity.Application; import com.whylog.server.domain.decision.entity.ApplicationBase; +import com.whylog.server.domain.decision.entity.ApplicationCommits; import com.whylog.server.domain.decision.entity.ApplicationTimeline; import com.whylog.server.domain.decision.exception.ApplicationNotFoundException; import com.whylog.server.domain.decision.repository.ApplicationBaseRepository; +import com.whylog.server.domain.decision.repository.ApplicationCommitsRepository; import com.whylog.server.domain.decision.repository.ApplicationRepository; import com.whylog.server.domain.decision.repository.ApplicationTimelineRepository; +import com.whylog.server.domain.git.entity.Commit; +import com.whylog.server.domain.git.entity.CommitConnection; +import com.whylog.server.domain.git.exception.GitErrorCode; +import com.whylog.server.domain.git.repository.CommitConnectionRepository; +import com.whylog.server.domain.git.repository.CommitRepository; import com.whylog.server.domain.user.entity.Member; import com.whylog.server.domain.user.service.MemberUseCase; -import java.util.Map; +import com.whylog.server.global.apiPayload.exception.handler.ErrorHandler; import java.util.List; +import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; @@ -20,15 +28,18 @@ @Service @RequiredArgsConstructor +@Transactional(readOnly = true) public class ApplicationQueryService { private final ApplicationRepository applicationRepository; private final ApplicationBaseRepository applicationBaseRepository; private final ApplicationTimelineRepository applicationTimelineRepository; + private final ApplicationCommitsRepository applicationCommitsRepository; + private final CommitRepository commitRepository; + private final CommitConnectionRepository commitConnectionRepository; private final MemberUseCase memberUseCase; // 적용사항 상세 조회에 필요한 제목, 타임라인, 원문 맥락, 결정근거를 조회 - @Transactional(readOnly = true) public ApplicationResponse.ApplicationDetailDTO getApplicationDetail(Long applicationId) { Application application = applicationRepository.findById(applicationId) .orElseThrow(ApplicationNotFoundException::new); @@ -48,6 +59,110 @@ public ApplicationResponse.ApplicationDetailDTO getApplicationDetail(Long applic .build(); } + // 적용사항에 연결된 커밋 목록을 조회 + public ApplicationResponse.ConnectedCommitListDTO getConnectedCommits(Long applicationId) { + // 적용사항 존재 여부검증 + applicationRepository.findById(applicationId) + .orElseThrow(ApplicationNotFoundException::new); + + // 적용사항에 사용자가 연결한 커밋 목록을 조회 + List commitConnections = commitConnectionRepository.findByApplicationId(applicationId); + + // 연결된 커밋 엔티티를 응답 DTO로 변환 + List commits = commitConnections.stream() + .map(commitConnection -> ApplicationResponse.ConnectedCommitDTO.builder() + .repositoryName(commitConnection.getCommit().getRepository().getName()) + .commitHash(commitConnection.getCommit().getHash()) + .message(commitConnection.getCommit().getMessage()) + .committedDate(commitConnection.getCommit().getDateTime()) + .build()) + .toList(); + + return ApplicationResponse.ConnectedCommitListDTO.builder() + .commitCount(commits.size()) + .commits(commits) + .build(); + } + + // 적용사항의 적용현황 요약 정보를 조회 + public ApplicationResponse.ApplicationStatusDTO getApplicationStatus(Long applicationId) { + // 적용사항 존재 여부 검증 + applicationRepository.findById(applicationId) + .orElseThrow(ApplicationNotFoundException::new); + + // 적용사항에 사용자가 연결한 커밋 목록을 조회 + List commitConnections = commitConnectionRepository.findByApplicationId(applicationId); + + // 연결된 커밋 목록을 적용현황 응답 형식으로 변환 + List commits = commitConnections.stream() + .map(commitConnection -> ApplicationResponse.ApplicationBaseItemDTO.builder() + .commitHash(commitConnection.getCommit().getHash()) + .commitMessage(commitConnection.getCommit().getMessage()) + .build()) + .toList(); + + return ApplicationResponse.ApplicationStatusDTO.builder() + .commitCount(commits.size()) + .commits(commits) + .build(); + } + + // 적용사항에 추천된 커밋 목록을 조회 + public List getRecommendedCommits(Long applicationId) { + // 적용사항 존재 여부 검증 + applicationRepository.findById(applicationId) + .orElseThrow(ApplicationNotFoundException::new); + + // 적용사항과 연결된 추천 커밋 원본 정보를 조회 + List applicationCommits = applicationCommitsRepository.findByApplicationId(applicationId); + + // 추천 원본이 들고 있는 commitId 목록으로 실제 커밋 정보를 조회 + Map commitsById = findCommitsById(applicationCommits); + + // 추천 원본과 커밋 정보를 합쳐 응답 DTO로 변환 + return applicationCommits.stream() + .filter(applicationCommit -> commitsById.containsKey(applicationCommit.getDecisionCommits().getCommitId())) + .map(applicationCommit -> toRecommendedCommitDTO(applicationCommit, commitsById)) + .toList(); + } + + // 추천 커밋 ID 목록에 해당하는 커밋 정보를 조회 + private Map findCommitsById(List applicationCommits) { + // 추천 커밋 원본에서 커밋 ID 목록을 추출 + List commitIds = applicationCommits.stream() + .map(applicationCommit -> applicationCommit.getDecisionCommits().getCommitId()) + .toList(); + + if (commitIds.isEmpty()) { + return Map.of(); + } + + //응답에 필요한 커밋 정보와 레포 이름을 함께 조회 + Map commitsById = commitRepository.findAllWithRepositoryByIdIn(commitIds).stream() + .collect(Collectors.toMap(Commit::getId, Function.identity())); + + // 추천 원본이 존재하지 않는 커밋을 참조하는 경우 + if (commitsById.size() != commitIds.size()) { + throw new ErrorHandler(GitErrorCode.COMMIT_NOT_FOUND); + } + + return commitsById; + } + + // 추천 커밋 연결 정보를 응답 DTO로 변환 + private ApplicationResponse.RecommendedCommitDTO toRecommendedCommitDTO(ApplicationCommits applicationCommit, + Map commitsById) { + Commit commit = commitsById.get(applicationCommit.getDecisionCommits().getCommitId()); + + return ApplicationResponse.RecommendedCommitDTO.builder() + .repositoryName(commit.getRepository().getName()) + .commitId(String.valueOf(commit.getId())) + .commitHash(commit.getHash()) + .message(commit.getMessage()) + .reason(applicationCommit.getDecisionCommits().getReason()) + .build(); + } + // 연결 테이블을 따라 적용사항에 속한 결정근거 목록을 응답 DTO로 변환 private List toDecisionReasonItems(List applicationBases) { return applicationBases.stream() diff --git a/src/main/java/com/whylog/server/domain/decision/service/DecisionQueryService.java b/src/main/java/com/whylog/server/domain/decision/service/DecisionQueryService.java new file mode 100644 index 0000000..4d9c935 --- /dev/null +++ b/src/main/java/com/whylog/server/domain/decision/service/DecisionQueryService.java @@ -0,0 +1,27 @@ +package com.whylog.server.domain.decision.service; + +import com.whylog.server.domain.decision.dto.DecisionResponse; +import com.whylog.server.domain.decision.entity.Decision; +import com.whylog.server.domain.decision.exception.DecisionNotFoundException; +import com.whylog.server.domain.decision.repository.DecisionRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class DecisionQueryService { + + private final DecisionRepository decisionRepository; + + // 결정사항의 신뢰도 조회 + public DecisionResponse.ReliabilityDTO getReliability(Long decisionId) { + Decision decision = decisionRepository.findById(decisionId) + .orElseThrow(DecisionNotFoundException::new); + + return DecisionResponse.ReliabilityDTO.builder() + .score(decision.getReliabilityScore()) + .build(); + } +} diff --git a/src/main/java/com/whylog/server/domain/git/entity/CommitConnection.java b/src/main/java/com/whylog/server/domain/git/entity/CommitConnection.java index ecf0566..6a18f2c 100644 --- a/src/main/java/com/whylog/server/domain/git/entity/CommitConnection.java +++ b/src/main/java/com/whylog/server/domain/git/entity/CommitConnection.java @@ -1,6 +1,6 @@ package com.whylog.server.domain.git.entity; -import com.whylog.server.domain.decision.entity.Decision; +import com.whylog.server.domain.decision.entity.Application; import com.whylog.server.global.entity.BaseEntity; import jakarta.persistence.EmbeddedId; import jakarta.persistence.Entity; @@ -15,20 +15,28 @@ @Entity @Getter -@Table(name = "Commit_Connection") +@Table(name = "commit_connection") @NoArgsConstructor(access = AccessLevel.PROTECTED) public class CommitConnection extends BaseEntity { @EmbeddedId private CommitConnectionId id; - @MapsId("decisionId") + @MapsId("applicationId") @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "decision_id", nullable = false) - private Decision decision; + @JoinColumn(name = "application_id", nullable = false) + private Application application; @MapsId("commitId") @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "commit_id", nullable = false) private Commit commit; + + public static CommitConnection create(Application application, Commit commit) { + CommitConnection commitConnection = new CommitConnection(); + commitConnection.id = new CommitConnectionId(application.getId(), commit.getId()); + commitConnection.application = application; + commitConnection.commit = commit; + return commitConnection; + } } diff --git a/src/main/java/com/whylog/server/domain/git/entity/CommitConnectionId.java b/src/main/java/com/whylog/server/domain/git/entity/CommitConnectionId.java index ad6aae9..e3f8dc9 100644 --- a/src/main/java/com/whylog/server/domain/git/entity/CommitConnectionId.java +++ b/src/main/java/com/whylog/server/domain/git/entity/CommitConnectionId.java @@ -4,6 +4,7 @@ import jakarta.persistence.Embeddable; import java.io.Serializable; import lombok.AccessLevel; +import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; @@ -11,11 +12,12 @@ @Getter @Embeddable @EqualsAndHashCode +@AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED) public class CommitConnectionId implements Serializable { - @Column(name = "decision_id") - private Long decisionId; + @Column(name = "application_id") + private Long applicationId; @Column(name = "commit_id") private Long commitId; diff --git a/src/main/java/com/whylog/server/domain/git/repository/CommitConnectionRepository.java b/src/main/java/com/whylog/server/domain/git/repository/CommitConnectionRepository.java new file mode 100644 index 0000000..6ec1447 --- /dev/null +++ b/src/main/java/com/whylog/server/domain/git/repository/CommitConnectionRepository.java @@ -0,0 +1,24 @@ +package com.whylog.server.domain.git.repository; + +import com.whylog.server.domain.git.entity.CommitConnection; +import com.whylog.server.domain.git.entity.CommitConnectionId; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +@Repository +public interface CommitConnectionRepository extends JpaRepository { + + // 적용사항에 연결된 커밋 목록을 최신순으로 조회합니다. + @Query(""" + SELECT cc + FROM CommitConnection cc + JOIN FETCH cc.commit c + JOIN FETCH c.repository r + WHERE cc.application.id = :applicationId + ORDER BY c.dateTime DESC, c.id DESC + """) + List findByApplicationId(@Param("applicationId") Long applicationId); +} diff --git a/src/main/java/com/whylog/server/domain/git/repository/CommitRepository.java b/src/main/java/com/whylog/server/domain/git/repository/CommitRepository.java index 5db73b6..380227b 100644 --- a/src/main/java/com/whylog/server/domain/git/repository/CommitRepository.java +++ b/src/main/java/com/whylog/server/domain/git/repository/CommitRepository.java @@ -27,6 +27,15 @@ Set findExistingHashes( @Param("hashes") List hashes ); + // 커밋 ID 목록에 해당하는 커밋과 저장소 정보를 함께 조회 + @Query(""" + SELECT c + FROM Commit c + JOIN FETCH c.repository + WHERE c.id IN :commitIds + """) + List findAllWithRepositoryByIdIn(@Param("commitIds") List commitIds); + // 커서 기반 무한스크롤 - 커밋 목록 조회 @Query("SELECT c FROM Commit c " + "WHERE c.repository.id = :repositoryId " + diff --git a/src/main/java/com/whylog/server/domain/meeting/service/MeetingCleanupService.java b/src/main/java/com/whylog/server/domain/meeting/service/MeetingCleanupService.java index ac190c5..5d7d517 100644 --- a/src/main/java/com/whylog/server/domain/meeting/service/MeetingCleanupService.java +++ b/src/main/java/com/whylog/server/domain/meeting/service/MeetingCleanupService.java @@ -1,7 +1,11 @@ package com.whylog.server.domain.meeting.service; import com.whylog.server.domain.decision.repository.ApplicationRepository; +import com.whylog.server.domain.decision.repository.ApplicationBaseRepository; +import com.whylog.server.domain.decision.repository.ApplicationTimelineRepository; +import com.whylog.server.domain.decision.repository.DecisionBaseRepository; import com.whylog.server.domain.decision.repository.DecisionRepository; +import com.whylog.server.domain.decision.repository.DecisionTimelineRepository; import com.whylog.server.domain.meeting.repository.DialogueRepository; import com.whylog.server.domain.meeting.repository.MeetingAnalysisRepository; import com.whylog.server.domain.meeting.repository.MeetingMemberRepository; @@ -16,7 +20,11 @@ public class MeetingCleanupService { private final ApplicationRepository applicationRepository; + private final ApplicationBaseRepository applicationBaseRepository; + private final ApplicationTimelineRepository applicationTimelineRepository; + private final DecisionBaseRepository decisionBaseRepository; private final DecisionRepository decisionRepository; + private final DecisionTimelineRepository decisionTimelineRepository; private final MeetingAnalysisRepository meetingAnalysisRepository; private final DialogueRepository dialogueRepository; private final MeetingMemberRepository meetingMemberRepository; @@ -37,6 +45,10 @@ public void deleteByMeetingId(Long meetingId) { } private void deleteChildrenByTeamId(Long teamId, List meetingIds) { + applicationTimelineRepository.deleteByTeamId(teamId); + applicationBaseRepository.deleteByTeamId(teamId); + decisionTimelineRepository.deleteByTeamId(teamId); + decisionBaseRepository.deleteByTeamId(teamId); applicationRepository.deleteByTeamId(teamId); decisionRepository.deleteByTeamId(teamId); meetingAnalysisRepository.deleteByTeamId(teamId); @@ -45,6 +57,10 @@ private void deleteChildrenByTeamId(Long teamId, List meetingIds) { } private void deleteChildrenByMeetingId(Long meetingId) { + applicationTimelineRepository.deleteByMeetingId(meetingId); + applicationBaseRepository.deleteByMeetingId(meetingId); + decisionTimelineRepository.deleteByMeetingId(meetingId); + decisionBaseRepository.deleteByMeetingId(meetingId); applicationRepository.deleteByMeetingId(meetingId); decisionRepository.deleteByMeetingId(meetingId); meetingAnalysisRepository.deleteByMeetingId(meetingId);