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 @@ -3,9 +3,14 @@
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;
import com.whylog.server.domain.team.exception.TeamErrorCode;
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 com.whylog.server.global.auth.annotation.CurrentMember;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
Expand Down Expand Up @@ -35,6 +40,10 @@ public class GitController {
- GitHub Personal Access Token 발급 필요 (repo 권한 포함)
- GitHub Settings > Developer settings > Personal access tokens에서 발급
""")
@ApiErrorCodeExamples({
@ApiErrorCodeExample(value = ErrorStatus.class, name = "_BAD_REQUEST"),
@ApiErrorCodeExample(value = ErrorStatus.class, name = "_INTERNAL_SERVER_ERROR")
})
public ApiResponse<GitResponse.GitHubTokenResponseDTO> registerGitHubToken(
@Parameter(hidden = true) @CurrentMember Long memberId,
@Valid @RequestBody GitRequest.GitHubTokenDTO request) {
Expand All @@ -51,6 +60,10 @@ public ApiResponse<GitResponse.GitHubTokenResponseDTO> registerGitHubToken(
1. 최근 동기화한 레포지토리 (동기화 시간 최신순)
2. 동기화된 적 없는 레포지토리 (추가한순)
""")
@ApiErrorCodeExamples({
@ApiErrorCodeExample(value = ErrorStatus.class, name = "_BAD_REQUEST"),
@ApiErrorCodeExample(value = TeamErrorCode.class, name = "TEAM_NOT_FOUND")
})
public ApiResponse<List<GitResponse.RepositoryDTO>> getRepositories(
@PathVariable Long teamId) {
return ApiResponse.onSuccess(
Expand All @@ -64,6 +77,12 @@ public ApiResponse<List<GitResponse.RepositoryDTO>> getRepositories(
@Operation(
summary = "GitHub 레포지토리 추가",
description = "GitHub 레포지토리를 팀에 연동합니다. 등록 시에는 레포 정보만 저장되며, 커밋은 동기화 API를 호출할 때 수집됩니다.")
@ApiErrorCodeExamples({
@ApiErrorCodeExample(value = ErrorStatus.class, name = "_BAD_REQUEST"),
@ApiErrorCodeExample(value = GitErrorCode.class, name = "INVALID_GITHUB_URL"),
@ApiErrorCodeExample(value = GitErrorCode.class, name = "GITHUB_TOKEN_NOT_REGISTERED"),
@ApiErrorCodeExample(value = GitErrorCode.class, name = "GITHUB_TOKEN_EXPIRED")
})
public ApiResponse<GitResponse.RepositoryCreateResponseDTO> createRepository(
@Parameter(hidden = true) @CurrentMember Long memberId,
@PathVariable Long teamId,
Expand All @@ -76,6 +95,12 @@ public ApiResponse<GitResponse.RepositoryCreateResponseDTO> createRepository(
@Operation(
summary = "GitHub 레포지토리 동기화",
description = "등록된 레포지토리의 최신 커밋을 DB에 저장합니다. 마지막 동기화 이후의 새 커밋만 저장되며 Merge 커밋은 제외됩니다.")
@ApiErrorCodeExamples({
@ApiErrorCodeExample(value = ErrorStatus.class, name = "_BAD_REQUEST"),
@ApiErrorCodeExample(value = GitErrorCode.class, name = "REPOSITORY_NOT_FOUND"),
@ApiErrorCodeExample(value = GitErrorCode.class, name = "GITHUB_TOKEN_NOT_REGISTERED"),
@ApiErrorCodeExample(value = GitErrorCode.class, name = "GITHUB_TOKEN_EXPIRED")
})
public ApiResponse<GitResponse.RepositorySyncResponseDTO> syncRepository(
@Parameter(hidden = true) @CurrentMember Long memberId,
@PathVariable Long repositoryId) {
Expand All @@ -100,6 +125,10 @@ public ApiResponse<GitResponse.RepositorySyncResponseDTO> syncRepository(
- nextCursorId: 다음 요청에 사용할 커서 ID
- isFirst: 첫 페이지 여부
""")
@ApiErrorCodeExamples({
@ApiErrorCodeExample(value = ErrorStatus.class, name = "_BAD_REQUEST"),
@ApiErrorCodeExample(value = GitErrorCode.class, name = "REPOSITORY_NOT_FOUND")
})
public ApiResponse<GitResponse.CommitListResponseDTO> getCommitsCursor(
@PathVariable Long repositoryId,
@Parameter(description = "이전 조회의 마지막 커밋 ID (첫 요청 시 생략)")
Expand Down Expand Up @@ -130,6 +159,11 @@ public ApiResponse<GitResponse.CommitListResponseDTO> getCommitsCursor(
💡 프론트 구현시 참고 사항:
- changedCode를 react-diff-viewer-continued 라이브러리 사용하면 될거같으니 참고해주세요!
""")
@ApiErrorCodeExamples({
@ApiErrorCodeExample(value = ErrorStatus.class, name = "_BAD_REQUEST"),
@ApiErrorCodeExample(value = GitErrorCode.class, name = "COMMIT_NOT_FOUND"),
@ApiErrorCodeExample(value = GitErrorCode.class, name = "GITHUB_TOKEN_NOT_REGISTERED")
})
public ApiResponse<GitResponse.CommitDetailDTO> getCommitDetail(
@Parameter(hidden = true) @CurrentMember Long memberId,
@PathVariable Long repositoryId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
import com.whylog.server.domain.decision.dto.DecisionResponse;
import com.whylog.server.domain.team.dto.TeamRequest;
import com.whylog.server.domain.team.dto.TeamResponse;
import com.whylog.server.domain.team.exception.TeamErrorCode;
import com.whylog.server.domain.team.service.TeamCommandService;
import com.whylog.server.domain.team.service.TeamQueryService;
import com.whylog.server.domain.user.exception.MemberErrorStatus;
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.auth.annotation.CurrentMember;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Encoding;
Expand Down Expand Up @@ -46,17 +50,13 @@ public ApiResponse<List<DecisionResponse.DecisionListDTO>> getDecisions(

@PostMapping("/{teamId}/invitations")
@Operation(summary = "팀 초대 API", description = """

특정 사용자를 팀에 초대합니다. 팀 초대를 하면 상대는 즉시 팀원으로 추가됩니다.

| 상황 | HTTP Status | Code | Message |
| --- | --- | --- | --- |
| 성공 | 200 OK | COMMON200 | 성공입니다. |
| 이미 팀에 속한 사용자 | 409 Conflict | TEAM_409 | 이미 팀에 속한 사용자입니다. |
| 팀 없음 | 404 Not Found | TEAM_404 | 존재하지 않는 팀입니다. |
| 사용자 없음 | 404 Not Found | MEMBER_404 | 찾을 수 없는 유저입니다. |

""")
@ApiErrorCodeExamples({
@ApiErrorCodeExample(value = TeamErrorCode.class, name = "TEAM_NOT_FOUND"),
@ApiErrorCodeExample(value = MemberErrorStatus.class, name = "MEMBER_NOT_FOUND"),
@ApiErrorCodeExample(value = TeamErrorCode.class, name = "TEAM_MEMBER_ALREADY_EXISTS")
})
public ApiResponse<TeamResponse.InvitationResponseDTO> sendInvitation(
@PathVariable Long teamId,
@Valid @RequestBody TeamRequest.InvitationDTO request) {
Expand Down Expand Up @@ -99,6 +99,10 @@ public ApiResponse<TeamResponse.InvitationResponseDTO> sendInvitation(
)
)
)
@ApiErrorCodeExamples({
@ApiErrorCodeExample(value = TeamErrorCode.class, name = "TEAM_NAME_ALREADY_EXISTS"),
@ApiErrorCodeExample(value = TeamErrorCode.class, name = "TEAM_NAME_LENGTH")
})
public ApiResponse<TeamResponse.TeamCreateResponseDTO> createTeam(
@Parameter(hidden = true) @CurrentMember Long memberId,
@Parameter(hidden = true) @Valid @RequestPart("request") TeamRequest.TeamCreateDTO request,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.whylog.server.global.apiPayload.annotation;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(ApiErrorCodeExamples.class)
public @interface ApiErrorCodeExample {

Class<? extends Enum<?>> value();
String name();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.whylog.server.global.apiPayload.annotation;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiErrorCodeExamples {

ApiErrorCodeExample[] value();
}
70 changes: 67 additions & 3 deletions src/main/java/com/whylog/server/global/config/SwaggerConfig.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
package com.whylog.server.global.config;

import com.whylog.server.global.apiPayload.annotation.ApiErrorCodeExample;
import com.whylog.server.global.apiPayload.annotation.ApiErrorCodeExamples;
import com.whylog.server.global.apiPayload.code.BaseErrorCode;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.examples.Example;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.security.SecurityScheme;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.customizers.OperationCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.servers.Server;

@Slf4j
@Configuration
public class SwaggerConfig {

Expand All @@ -20,13 +30,11 @@ public OpenAPI FindCrimeAPI() {
.version("1.0.0");

String jwtSchemeName = "JWT TOKEN";
// API 요청헤더에 인증정보 포함
SecurityRequirement securityRequirement = new SecurityRequirement().addList(jwtSchemeName);
// SecuritySchemes 등록
Components components = new Components()
.addSecuritySchemes(jwtSchemeName, new SecurityScheme()
.name(jwtSchemeName)
.type(SecurityScheme.Type.HTTP) // HTTP 방식
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT"));

Expand All @@ -36,5 +44,61 @@ public OpenAPI FindCrimeAPI() {
.addSecurityItem(securityRequirement)
.components(components);
}

@Bean
public OperationCustomizer customize() {
return (operation, handlerMethod) -> {
// 여러 에러 코드가 적용된 경우
ApiErrorCodeExamples errorAnnotations = handlerMethod.getMethodAnnotation(ApiErrorCodeExamples.class);
if (errorAnnotations != null) {
for (ApiErrorCodeExample e : errorAnnotations.value()) {
handleErrorCode(operation, e.value(), e.name());
}
} else {
// 단일 에러 코드가 적용된 경우
ApiErrorCodeExample single = handlerMethod.getMethodAnnotation(ApiErrorCodeExample.class);
if (single != null) {
handleErrorCode(operation, single.value(), single.name());
}
}

return operation;
};
}

private void handleErrorCode(Operation operation, Class<? extends Enum<?>> enumClass, String name) {
for (Enum<?> constant : enumClass.getEnumConstants()) {
if (constant.name().equals(name) && constant instanceof BaseErrorCode) {
BaseErrorCode errorCode = (BaseErrorCode) constant;
generateSingleErrorCodeResponseExample(operation, errorCode);
break;
}
}
}

private void generateSingleErrorCodeResponseExample(Operation operation, BaseErrorCode errorCode) {
String httpStatusCode = String.valueOf(errorCode.getReasonHttpStatus().getHttpStatus().value());
String code = errorCode.getReasonHttpStatus().getCode();
String message = errorCode.getReasonHttpStatus().getMessage();

String exampleJson = String.format("""
{
"isSuccess": false,
"code": "%s",
"message": "%s"
}
""", code, message);

io.swagger.v3.oas.models.responses.ApiResponse apiResponse =
operation.getResponses().computeIfAbsent(httpStatusCode, statusCode ->
new io.swagger.v3.oas.models.responses.ApiResponse()
.description(message)
.content(new Content()));

MediaType mediaType = apiResponse.getContent()
.computeIfAbsent("application/json", k -> new MediaType());

mediaType.addExamples(code, new Example().value(exampleJson));
}
}

Loading