diff --git a/src/main/java/com/whylog/server/domain/git/controller/GitController.java b/src/main/java/com/whylog/server/domain/git/controller/GitController.java index d75616d..4ce8747 100644 --- a/src/main/java/com/whylog/server/domain/git/controller/GitController.java +++ b/src/main/java/com/whylog/server/domain/git/controller/GitController.java @@ -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; @@ -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 registerGitHubToken( @Parameter(hidden = true) @CurrentMember Long memberId, @Valid @RequestBody GitRequest.GitHubTokenDTO request) { @@ -51,6 +60,10 @@ public ApiResponse registerGitHubToken( 1. 최근 동기화한 레포지토리 (동기화 시간 최신순) 2. 동기화된 적 없는 레포지토리 (추가한순) """) + @ApiErrorCodeExamples({ + @ApiErrorCodeExample(value = ErrorStatus.class, name = "_BAD_REQUEST"), + @ApiErrorCodeExample(value = TeamErrorCode.class, name = "TEAM_NOT_FOUND") + }) public ApiResponse> getRepositories( @PathVariable Long teamId) { return ApiResponse.onSuccess( @@ -64,6 +77,12 @@ public ApiResponse> 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 createRepository( @Parameter(hidden = true) @CurrentMember Long memberId, @PathVariable Long teamId, @@ -76,6 +95,12 @@ public ApiResponse 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 syncRepository( @Parameter(hidden = true) @CurrentMember Long memberId, @PathVariable Long repositoryId) { @@ -100,6 +125,10 @@ public ApiResponse syncRepository( - nextCursorId: 다음 요청에 사용할 커서 ID - isFirst: 첫 페이지 여부 """) + @ApiErrorCodeExamples({ + @ApiErrorCodeExample(value = ErrorStatus.class, name = "_BAD_REQUEST"), + @ApiErrorCodeExample(value = GitErrorCode.class, name = "REPOSITORY_NOT_FOUND") + }) public ApiResponse getCommitsCursor( @PathVariable Long repositoryId, @Parameter(description = "이전 조회의 마지막 커밋 ID (첫 요청 시 생략)") @@ -130,6 +159,11 @@ public ApiResponse 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 getCommitDetail( @Parameter(hidden = true) @CurrentMember Long memberId, @PathVariable Long repositoryId, diff --git a/src/main/java/com/whylog/server/domain/team/controller/TeamController.java b/src/main/java/com/whylog/server/domain/team/controller/TeamController.java index c063e2c..c76624d 100644 --- a/src/main/java/com/whylog/server/domain/team/controller/TeamController.java +++ b/src/main/java/com/whylog/server/domain/team/controller/TeamController.java @@ -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; @@ -46,17 +50,13 @@ public ApiResponse> 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 sendInvitation( @PathVariable Long teamId, @Valid @RequestBody TeamRequest.InvitationDTO request) { @@ -99,6 +99,10 @@ public ApiResponse sendInvitation( ) ) ) + @ApiErrorCodeExamples({ + @ApiErrorCodeExample(value = TeamErrorCode.class, name = "TEAM_NAME_ALREADY_EXISTS"), + @ApiErrorCodeExample(value = TeamErrorCode.class, name = "TEAM_NAME_LENGTH") + }) public ApiResponse createTeam( @Parameter(hidden = true) @CurrentMember Long memberId, @Parameter(hidden = true) @Valid @RequestPart("request") TeamRequest.TeamCreateDTO request, diff --git a/src/main/java/com/whylog/server/global/apiPayload/annotation/ApiErrorCodeExample.java b/src/main/java/com/whylog/server/global/apiPayload/annotation/ApiErrorCodeExample.java new file mode 100644 index 0000000..63442d7 --- /dev/null +++ b/src/main/java/com/whylog/server/global/apiPayload/annotation/ApiErrorCodeExample.java @@ -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> value(); + String name(); +} diff --git a/src/main/java/com/whylog/server/global/apiPayload/annotation/ApiErrorCodeExamples.java b/src/main/java/com/whylog/server/global/apiPayload/annotation/ApiErrorCodeExamples.java new file mode 100644 index 0000000..a285469 --- /dev/null +++ b/src/main/java/com/whylog/server/global/apiPayload/annotation/ApiErrorCodeExamples.java @@ -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(); +} diff --git a/src/main/java/com/whylog/server/global/config/SwaggerConfig.java b/src/main/java/com/whylog/server/global/config/SwaggerConfig.java index 2f6daae..19de30e 100644 --- a/src/main/java/com/whylog/server/global/config/SwaggerConfig.java +++ b/src/main/java/com/whylog/server/global/config/SwaggerConfig.java @@ -1,7 +1,16 @@ 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; @@ -9,6 +18,7 @@ import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.servers.Server; +@Slf4j @Configuration public class SwaggerConfig { @@ -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")); @@ -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> 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)); + } }