diff --git a/src/main/java/com/example/Veco/domain/comment/controller/CommentController.java b/src/main/java/com/example/Veco/domain/comment/controller/CommentController.java index 55928782..85ed8931 100644 --- a/src/main/java/com/example/Veco/domain/comment/controller/CommentController.java +++ b/src/main/java/com/example/Veco/domain/comment/controller/CommentController.java @@ -60,18 +60,6 @@ public ApiResponse createComment( summary = "댓글 목록 조회 API", description = "특정 리소스(이슈, 목표, 외부 이슈)의 댓글 목록을 조회합니다. 최신순으로 정렬되어 반환됩니다." ) - @ApiResponses(value = { - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "200", - description = "댓글 목록 조회 성공", - content = @Content(schema = @Schema(implementation = CommentListResponseDTO.class)) - ), - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "404", - description = "대상 리소스를 찾을 수 없음", - content = @Content(schema = @Schema(implementation = ApiResponse.class)) - ) - }) @GetMapping public ApiResponse getAllComments( @Parameter(description = "댓글을 조회할 대상 리소스의 ID", required = true, example = "1") diff --git a/src/main/java/com/example/Veco/domain/external/controller/ExternalController.java b/src/main/java/com/example/Veco/domain/external/controller/ExternalController.java index 03ae7e49..a5a543c6 100644 --- a/src/main/java/com/example/Veco/domain/external/controller/ExternalController.java +++ b/src/main/java/com/example/Veco/domain/external/controller/ExternalController.java @@ -95,6 +95,7 @@ public ApiResponse> restoreGoals( @PatchMapping("/{externalId}") public ApiResponse modifyExternal( @PathVariable Long externalId, + @PathVariable Long teamId, @Valid @RequestBody ExternalRequestDTO.ExternalUpdateRequestDTO requestDTO) { return ApiResponse.onSuccess(externalService.updateExternal(externalId, requestDTO)); diff --git a/src/main/java/com/example/Veco/domain/external/controller/ExternalSwaggerDocs.java b/src/main/java/com/example/Veco/domain/external/controller/ExternalSwaggerDocs.java index b74cde05..f36d753a 100644 --- a/src/main/java/com/example/Veco/domain/external/controller/ExternalSwaggerDocs.java +++ b/src/main/java/com/example/Veco/domain/external/controller/ExternalSwaggerDocs.java @@ -147,6 +147,7 @@ ApiResponse createExternal( }) ApiResponse modifyExternal( @Parameter(description = "외부이슈 ID", required = true) @PathVariable Long externalId, + @Parameter(description = "팀 ID")@PathVariable Long teamId, @Valid @RequestBody ExternalRequestDTO.ExternalUpdateRequestDTO requestDTO); @Operation( diff --git a/src/main/java/com/example/Veco/domain/external/converter/ExternalConverter.java b/src/main/java/com/example/Veco/domain/external/converter/ExternalConverter.java index fb29d61d..7a0ad0a1 100644 --- a/src/main/java/com/example/Veco/domain/external/converter/ExternalConverter.java +++ b/src/main/java/com/example/Veco/domain/external/converter/ExternalConverter.java @@ -10,8 +10,6 @@ import com.example.Veco.domain.github.dto.webhook.GitHubWebhookPayload; import com.example.Veco.domain.external.entity.External; import com.example.Veco.domain.goal.entity.Goal; -import com.example.Veco.domain.goal.exception.GoalException; -import com.example.Veco.domain.goal.exception.code.GoalErrorCode; import com.example.Veco.domain.mapping.Assignment; import com.example.Veco.domain.member.entity.Member; import com.example.Veco.domain.team.entity.Team; @@ -141,14 +139,12 @@ public static ExternalResponseDTO.ExternalInfoDTO toExternalInfoDTO(External ext return ExternalResponseDTO.ExternalInfoDTO.builder() .id(external.getId()) .content(external.getDescription()) - .startDate(external.getStartDate()) - .endDate(external.getEndDate()) .content(external.getDescription()) .name(external.getName()) .priority(external.getPriority()) .goalId(external.getGoal()!= null ? external.getGoal().getId() : null) .goalTitle(external.getGoal()!= null ? external.getGoal().getTitle() : null) - .deadlines(deadline) + .deadline(deadline) .title(external.getTitle()) .state(external.getState()) .comments(commentResponseDTO) @@ -188,7 +184,7 @@ public static ExternalResponseDTO.ExternalDTO toExternalDTO(External external, L .priority(external.getPriority()) .goalId(external.getGoal() != null ? external.getGoal().getId() : null) .goalTitle(external.getGoal() != null ? external.getGoal().getTitle() : null) - .deadlines(deadline) + .deadline(deadline) .title(external.getTitle()) .state(external.getState()) .managers(assigneeResponseDTO) diff --git a/src/main/java/com/example/Veco/domain/external/dto/response/ExternalResponseDTO.java b/src/main/java/com/example/Veco/domain/external/dto/response/ExternalResponseDTO.java index 151592e8..a06a5249 100644 --- a/src/main/java/com/example/Veco/domain/external/dto/response/ExternalResponseDTO.java +++ b/src/main/java/com/example/Veco/domain/external/dto/response/ExternalResponseDTO.java @@ -56,12 +56,6 @@ public static class ExternalInfoDTO { @Schema(description = "이슈 상태", example = "TODO") private State state; - @Schema(description = "시작일", example = "2024-01-01") - private LocalDate startDate; - - @Schema(description = "마감일", example = "2024-01-31") - private LocalDate endDate; - @Schema(description = "목표 ID", example = "1") private Long goalId; @@ -74,7 +68,7 @@ public static class ExternalInfoDTO { private AssigneeResponseDTO managers; - private DeadlineResponseDTO deadlines; + private DeadlineResponseDTO deadline; private ExternalCommentResponseDTO comments; } @@ -108,12 +102,6 @@ public static class ExternalDTO { @Schema(description = "이슈 상태", example = "TODO") private State state; - @Schema(description = "시작일", example = "2024-01-01") - private LocalDate startDate; - - @Schema(description = "마감일", example = "2024-01-31") - private LocalDate endDate; - @Schema(description = "목표 ID", example = "1") private Long goalId; @@ -128,7 +116,7 @@ public static class ExternalDTO { private AssigneeResponseDTO managers; - private DeadlineResponseDTO deadlines; + private DeadlineResponseDTO deadline; } @Getter diff --git a/src/main/java/com/example/Veco/domain/external/entity/External.java b/src/main/java/com/example/Veco/domain/external/entity/External.java index f9dec9c0..1c9af182 100644 --- a/src/main/java/com/example/Veco/domain/external/entity/External.java +++ b/src/main/java/com/example/Veco/domain/external/entity/External.java @@ -40,7 +40,7 @@ public class External extends BaseEntity { @Column(name = "title", nullable = false) private String title; - @Column(name = "description") + @Column(name = "description", columnDefinition = "TEXT") @Builder.Default private String description = ""; diff --git a/src/main/java/com/example/Veco/domain/external/repository/ExternalCursorRepository.java b/src/main/java/com/example/Veco/domain/external/repository/ExternalCursorRepository.java index 0ba28d62..f42df068 100644 --- a/src/main/java/com/example/Veco/domain/external/repository/ExternalCursorRepository.java +++ b/src/main/java/com/example/Veco/domain/external/repository/ExternalCursorRepository.java @@ -572,10 +572,10 @@ private List convertToExternalItemDT .title(dto.getTitle()) .state(dto.getState()) .priority(dto.getPriority() != null ? dto.getPriority().name() : "없음") - .deadline(dto.getDeadlines() != null ? + .deadline(dto.getDeadline() != null ? ExternalGroupedResponseDTO.DeadlineDTO.builder() - .start(dto.getDeadlines().getStart() != null ? dto.getDeadlines().getStart().toString() : null) - .end(dto.getDeadlines().getEnd() != null ? dto.getDeadlines().getEnd().toString() : null) + .start(dto.getDeadline().getStart() != null ? dto.getDeadline().getStart().toString() : null) + .end(dto.getDeadline().getEnd() != null ? dto.getDeadline().getEnd().toString() : null) .build() : null) .managers(dto.getManagers() != null ? ExternalGroupedResponseDTO.ManagersDTO.builder() diff --git a/src/main/java/com/example/Veco/domain/external/service/ExternalService.java b/src/main/java/com/example/Veco/domain/external/service/ExternalService.java index 46d7ccd6..643fd789 100644 --- a/src/main/java/com/example/Veco/domain/external/service/ExternalService.java +++ b/src/main/java/com/example/Veco/domain/external/service/ExternalService.java @@ -95,13 +95,15 @@ public ExternalResponseDTO.CreateResponseDTO createExternal(Long teamId, List members = memberRepository.findAllByIdIn(request.getManagersId()); - if(request.getManagersId().size() != members.size()){ - throw new MemberHandler(MemberErrorStatus._MEMBER_NOT_FOUND); - } + if (request.getManagersId() != null) { + if(request.getManagersId().size() != members.size()){ + throw new MemberHandler(MemberErrorStatus._MEMBER_NOT_FOUND); + } - List memberTeamList = memberTeamRepository.findAllByMemberIdInAndTeamId(request.getManagersId(), teamId); - if (memberTeamList.size() != request.getManagersId().size()) { - throw new MemberHandler(MemberErrorStatus._FORBIDDEN); + List memberTeamList = memberTeamRepository.findAllByMemberIdInAndTeamId(request.getManagersId(), teamId); + if (memberTeamList.size() != request.getManagersId().size()) { + throw new MemberHandler(MemberErrorStatus._FORBIDDEN); + } } Goal goal = null; diff --git a/src/main/java/com/example/Veco/domain/github/controller/GitHubRestController.java b/src/main/java/com/example/Veco/domain/github/controller/GitHubRestController.java index 27c06ac9..ad7de8ef 100644 --- a/src/main/java/com/example/Veco/domain/github/controller/GitHubRestController.java +++ b/src/main/java/com/example/Veco/domain/github/controller/GitHubRestController.java @@ -8,6 +8,7 @@ import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -18,11 +19,12 @@ @RestController @RequiredArgsConstructor +@Slf4j public class GitHubRestController implements GitHubSwaggerDocs { private final GitHubService gitHubService; private final GitHubRepositoryService gitHubRepositoryService; - private static final String FRONTEND_URL = "http://localhost:5173"; + private static final String FRONTEND_URL = "https://web.vecoservice.shop"; @Hidden @GetMapping("/github/installation/callback") @@ -44,13 +46,27 @@ public ResponseEntity callbackInstallation( } @GetMapping("/api/teams/{teamId}/github/repositories") - public Mono>> getRepositories(@PathVariable("teamId") Long teamId) { + public ApiResponse> getRepositories(@PathVariable("teamId") Long teamId) { - // owner, repo 정보 추출해서 클라이언트에 제공 - return gitHubService.getInstallationIdAsync(teamId) // 비동기 DB 조회 - .flatMap(gitHubRepositoryService::getInstallationRepositories) - .map(ApiResponse::onSuccess) - .onErrorReturn(ApiResponse.onFailure("REPO_FETCH_FAILED", "레포지토리 조회 실패", null)); + try { + log.info("레포지토리 조회 시작: teamId={}", teamId); + + // 1. Installation ID 조회 (동기) + Long installationId = gitHubService.getInstallationIdSync(teamId); + log.info("Installation ID 조회 완료: {}", installationId); + + // 2. 레포지토리 목록 조회 (동기) + List repositories = + gitHubRepositoryService.getInstallationRepositoriesSync(installationId); + + log.info("레포지토리 조회 완료: {} 개", repositories.size()); + + return ApiResponse.onSuccess(repositories); + + } catch (Exception e) { + log.error("레포지토리 조회 실패: teamId={}", teamId, e); + return ApiResponse.onFailure("REPO_FETCH_FAILED", "레포지토리 조회 실패: " + e.getMessage(), null); + } } @GetMapping("/api/github/connect") diff --git a/src/main/java/com/example/Veco/domain/github/controller/GitHubSwaggerDocs.java b/src/main/java/com/example/Veco/domain/github/controller/GitHubSwaggerDocs.java index 3893e880..7b93fff4 100644 --- a/src/main/java/com/example/Veco/domain/github/controller/GitHubSwaggerDocs.java +++ b/src/main/java/com/example/Veco/domain/github/controller/GitHubSwaggerDocs.java @@ -18,7 +18,7 @@ public interface GitHubSwaggerDocs { summary = "GitHub App이 접근 가능한 레포지토리 목록 조회", description = "해당 팀이 설치한 GitHub App 이 접근 가능한 레포지토리 목록을 조회합니다." ) - Mono>> getRepositories(@PathVariable("teamId") Long teamId); + ApiResponse> getRepositories(@PathVariable("teamId") Long teamId); @Operation( summary = "GitHub 연동을 위한 App 설치 페이지 URL 조회", diff --git a/src/main/java/com/example/Veco/domain/github/service/GitHubRepositoryService.java b/src/main/java/com/example/Veco/domain/github/service/GitHubRepositoryService.java index 9abf2db4..3eab7b22 100644 --- a/src/main/java/com/example/Veco/domain/github/service/GitHubRepositoryService.java +++ b/src/main/java/com/example/Veco/domain/github/service/GitHubRepositoryService.java @@ -3,10 +3,12 @@ import com.example.Veco.domain.github.dto.response.GitHubApiResponseDTO; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatusCode; import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; +import java.time.Duration; import java.util.List; @Service @@ -24,6 +26,62 @@ public class GitHubRepositoryService { .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(1024 * 1024)) // 1MB .build(); + public List getInstallationRepositoriesSync(Long installationId) { + try { + log.info("레포지토리 조회 시작: installationId={}", installationId); + + // 1. Installation Token 조회 (동기) + String token = tokenService.getInstallationTokenSync(installationId); + log.info("Installation Token 조회 완료: installationId={}", installationId); + + // 2. 레포지토리 목록 조회 (동기) + return fetchRepositoriesSync(token, installationId); + + } catch (Exception e) { + log.error("레포지토리 조회 실패: installationId={}", installationId, e); + throw new RuntimeException("레포지토리 조회 실패", e); + } + } + + private List fetchRepositoriesSync(String token, Long installationId) { + try { + log.info("GitHub API 호출 시작: installationId={}", installationId); + + GitHubApiResponseDTO.GitHubRepositoryResponseDTO response = webClient.get() + .uri("/installation/repositories") + .header("Authorization", "Bearer " + token) + .retrieve() + .onStatus(HttpStatusCode::isError, clientResponse -> { + log.error("GitHub API 에러 응답: status={}", clientResponse.statusCode()); + return clientResponse.bodyToMono(String.class) + .flatMap(errorBody -> { + log.error("GitHub API 에러 바디: {}", errorBody); + return Mono.error(new RuntimeException( + "GitHub API 에러: " + clientResponse.statusCode() + ", body: " + errorBody)); + }); + }) + .bodyToMono(GitHubApiResponseDTO.GitHubRepositoryResponseDTO.class) + .timeout(Duration.ofSeconds(10)) + .doOnSuccess(result -> log.info("GitHub API 응답 성공: installationId={}, totalCount={}", + installationId, result != null ? result.getTotalCount() : 0)) + .doOnError(error -> log.error("GitHub API 호출 실패: installationId={}", installationId, error)) + .block(); // 동기 처리 + + if (response != null) { + log.info("설치된 레포지토리 조회 완료: installationId={}, count={}", + installationId, response.getTotalCount()); + log.info("repository : {}", response.getRepositories()); + return response.getRepositories(); + } else { + throw new RuntimeException("GitHub API 응답이 null입니다."); + } + + } catch (Exception e) { + log.error("fetchRepositoriesSync 실패: installationId={}", installationId, e); + throw new RuntimeException("GitHub 레포지토리 조회 실패", e); + } + } + public Mono> getInstallationRepositories(Long installationId) { return tokenService.getInstallationToken(installationId) .flatMap(token -> fetchRepositories(token, installationId)); diff --git a/src/main/java/com/example/Veco/domain/github/service/GitHubService.java b/src/main/java/com/example/Veco/domain/github/service/GitHubService.java index 11775d68..44e72a97 100644 --- a/src/main/java/com/example/Veco/domain/github/service/GitHubService.java +++ b/src/main/java/com/example/Veco/domain/github/service/GitHubService.java @@ -57,4 +57,8 @@ public Mono getInstallationIdAsync(Long teamId) { .subscribeOn(Schedulers.boundedElastic()); // DB 조회를 별도 스레드에서 실행 } + public Long getInstallationIdSync(Long teamId) { + return getInstallationId(teamId); + } + } diff --git a/src/main/java/com/example/Veco/domain/goal/entity/Goal.java b/src/main/java/com/example/Veco/domain/goal/entity/Goal.java index 4df463a8..6aa82969 100644 --- a/src/main/java/com/example/Veco/domain/goal/entity/Goal.java +++ b/src/main/java/com/example/Veco/domain/goal/entity/Goal.java @@ -36,7 +36,7 @@ public class Goal extends BaseEntity { @Column(name = "title", nullable = false) private String title; - @Column(name = "content") + @Column(name = "content", columnDefinition = "TEXT") @Builder.Default private String content = ""; diff --git a/src/main/java/com/example/Veco/domain/issue/entity/Issue.java b/src/main/java/com/example/Veco/domain/issue/entity/Issue.java index 0c93d6cc..25eeb4e8 100644 --- a/src/main/java/com/example/Veco/domain/issue/entity/Issue.java +++ b/src/main/java/com/example/Veco/domain/issue/entity/Issue.java @@ -33,7 +33,7 @@ public class Issue extends BaseEntity { @Column(name = "title", nullable = false) private String title; - @Column(name = "content") + @Column(name = "content", columnDefinition = "TEXT") @Builder.Default private String content = ""; diff --git a/src/main/java/com/example/Veco/domain/issue/repository/CustomIssueRepositoryImpl.java b/src/main/java/com/example/Veco/domain/issue/repository/CustomIssueRepositoryImpl.java index b0bb6436..8c44f902 100644 --- a/src/main/java/com/example/Veco/domain/issue/repository/CustomIssueRepositoryImpl.java +++ b/src/main/java/com/example/Veco/domain/issue/repository/CustomIssueRepositoryImpl.java @@ -36,11 +36,9 @@ public List findIssuesByTeamId(Predicate query, in List result = queryFactory .from(issue) .where(query) - .leftJoin(assignee).on(assignee.type.eq(Category.ISSUE).and(assignee.targetId.eq(issue.id))) - .leftJoin(member).on(member.id.eq(assignee.memberTeam.member.id)) .leftJoin(goal).on(goal.id.eq(issue.goal.id)) .orderBy(issue.id.desc()) - .groupBy(issue.id, assignee.id) + .groupBy(issue.id) .limit(size) .transform(GroupBy.groupBy(issue.id).list( Projections.constructor( diff --git a/src/main/java/com/example/Veco/domain/member/service/MemberCommandServiceImpl.java b/src/main/java/com/example/Veco/domain/member/service/MemberCommandServiceImpl.java index f0eed7bd..46926e89 100644 --- a/src/main/java/com/example/Veco/domain/member/service/MemberCommandServiceImpl.java +++ b/src/main/java/com/example/Veco/domain/member/service/MemberCommandServiceImpl.java @@ -15,17 +15,11 @@ import com.example.Veco.global.aws.util.S3Util; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.security.core.Authentication; -import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; -import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; -import java.util.List; -import java.util.Objects; - @Service @RequiredArgsConstructor @Slf4j @@ -54,7 +48,7 @@ public Member updateProfileImage(MultipartFile file, Member member) { } // S3 업로드 및 URL 생성 - String uploadedPath = s3Util.uploadFile(file, null); + String uploadedPath = s3Util.uploadFile(file, "profile/"); String imageUrl = s3Util.getImageUrl(uploadedPath); // DB 업데이트