diff --git a/build.gradle b/build.gradle index e1a22e7..399ab4b 100644 --- a/build.gradle +++ b/build.gradle @@ -36,6 +36,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-log4j2' implementation 'io.jsonwebtoken:jjwt-api:0.12.6' implementation 'io.minio:minio:9.0.1' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.6' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' implementation 'org.flywaydb:flyway-core' diff --git a/src/main/java/com/bop/youthpick/admin/board/controller/AdminCommunityCommentController.java b/src/main/java/com/bop/youthpick/admin/board/controller/AdminCommunityCommentController.java index a0dbf7d..cabb96e 100644 --- a/src/main/java/com/bop/youthpick/admin/board/controller/AdminCommunityCommentController.java +++ b/src/main/java/com/bop/youthpick/admin/board/controller/AdminCommunityCommentController.java @@ -3,12 +3,15 @@ import com.bop.youthpick.admin.board.dto.AdminCommunityCommentResponse; import com.bop.youthpick.admin.board.service.AdminCommunityService; import com.bop.youthpick.global.common.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "관리자 - 커뮤니티 댓글") @RestController @RequestMapping("/api/v1/admin/community-comments") @RequiredArgsConstructor @@ -16,6 +19,7 @@ public class AdminCommunityCommentController { private final AdminCommunityService adminCommunityService; + @Operation(summary = "커뮤니티 댓글 삭제", description = "관리자가 커뮤니티 댓글을 삭제합니다.") @DeleteMapping("/{commentId}") public ApiResponse delete(@PathVariable Long commentId) { return ApiResponse.ok(adminCommunityService.deleteComment(commentId)); diff --git a/src/main/java/com/bop/youthpick/admin/board/controller/AdminCommunityPostController.java b/src/main/java/com/bop/youthpick/admin/board/controller/AdminCommunityPostController.java index 9db29e6..6947241 100644 --- a/src/main/java/com/bop/youthpick/admin/board/controller/AdminCommunityPostController.java +++ b/src/main/java/com/bop/youthpick/admin/board/controller/AdminCommunityPostController.java @@ -5,6 +5,8 @@ import com.bop.youthpick.admin.board.dto.AdminCommunityPostResponse; import com.bop.youthpick.admin.board.service.AdminCommunityService; import com.bop.youthpick.global.common.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.constraints.Pattern; import java.time.LocalDate; import java.util.List; @@ -21,6 +23,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "관리자 - 커뮤니티 게시글") @RestController @RequestMapping("/api/v1/admin/community-posts") @RequiredArgsConstructor @@ -29,6 +32,9 @@ public class AdminCommunityPostController { private final AdminCommunityService adminCommunityService; + @Operation( + summary = "커뮤니티 게시글 목록 조회", + description = "카테고리, 작성자, 작성일 범위로 커뮤니티 게시글을 검색해 페이지 단위로 조회합니다.") @GetMapping public ApiResponse> list( @RequestParam(required = false) @@ -47,16 +53,19 @@ public ApiResponse> list( return ApiResponse.ok(page.getContent(), page); } + @Operation(summary = "게시글 댓글 목록 조회", description = "지정한 게시글에 달린 댓글 목록을 조회합니다.") @GetMapping("/{postId}/comments") public ApiResponse> getComments(@PathVariable Long postId) { return ApiResponse.ok(adminCommunityService.getComments(postId)); } + @Operation(summary = "게시글 첨부파일 목록 조회", description = "지정한 게시글에 첨부된 파일 목록을 조회합니다.") @GetMapping("/{postId}/attachments") public ApiResponse> getAttachments(@PathVariable Long postId) { return ApiResponse.ok(adminCommunityService.getAttachments(postId)); } + @Operation(summary = "커뮤니티 게시글 삭제", description = "관리자가 커뮤니티 게시글을 삭제합니다.") @DeleteMapping("/{postId}") public ApiResponse delete(@PathVariable Long postId) { return ApiResponse.ok(adminCommunityService.deletePost(postId)); diff --git a/src/main/java/com/bop/youthpick/admin/log/controller/AdminAppLogController.java b/src/main/java/com/bop/youthpick/admin/log/controller/AdminAppLogController.java index f7cc19c..e4c2558 100644 --- a/src/main/java/com/bop/youthpick/admin/log/controller/AdminAppLogController.java +++ b/src/main/java/com/bop/youthpick/admin/log/controller/AdminAppLogController.java @@ -3,6 +3,8 @@ import com.bop.youthpick.admin.log.dto.ApplicationLogResponse; import com.bop.youthpick.admin.log.service.AdminAppLogService; import com.bop.youthpick.global.common.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.constraints.Pattern; import java.time.LocalDate; import java.util.List; @@ -17,6 +19,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "관리자 - 애플리케이션 로그") @RestController @RequestMapping("/api/v1/admin/application-logs") @RequiredArgsConstructor @@ -25,6 +28,9 @@ public class AdminAppLogController { private final AdminAppLogService adminAppLogService; + @Operation( + summary = "애플리케이션 로그 목록 조회", + description = "로그 레벨, 키워드, 기간으로 애플리케이션 로그를 검색해 페이지 단위로 조회합니다.") @GetMapping public ApiResponse> list( @RequestParam(required = false) diff --git a/src/main/java/com/bop/youthpick/admin/log/controller/AdminSearchLogController.java b/src/main/java/com/bop/youthpick/admin/log/controller/AdminSearchLogController.java index 5500df7..6888255 100644 --- a/src/main/java/com/bop/youthpick/admin/log/controller/AdminSearchLogController.java +++ b/src/main/java/com/bop/youthpick/admin/log/controller/AdminSearchLogController.java @@ -3,6 +3,8 @@ import com.bop.youthpick.admin.log.dto.SearchLogResponse; import com.bop.youthpick.admin.log.service.AdminSearchLogService; import com.bop.youthpick.global.common.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import java.time.LocalDate; import java.util.List; import lombok.RequiredArgsConstructor; @@ -15,6 +17,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "관리자 - 검색 로그") @RestController @RequestMapping("/api/v1/admin/search-logs") @RequiredArgsConstructor @@ -22,6 +25,7 @@ public class AdminSearchLogController { private final AdminSearchLogService adminSearchLogService; + @Operation(summary = "검색 로그 목록 조회", description = "키워드와 기간으로 검색 로그를 조회해 페이지 단위로 조회합니다.") @GetMapping public ApiResponse> list( @RequestParam(required = false) String keyword, diff --git a/src/main/java/com/bop/youthpick/admin/policy/controller/AdminPolicyApplicationController.java b/src/main/java/com/bop/youthpick/admin/policy/controller/AdminPolicyApplicationController.java index 94c9659..c96f561 100644 --- a/src/main/java/com/bop/youthpick/admin/policy/controller/AdminPolicyApplicationController.java +++ b/src/main/java/com/bop/youthpick/admin/policy/controller/AdminPolicyApplicationController.java @@ -5,6 +5,8 @@ import com.bop.youthpick.admin.policy.dto.ApplicationChecklistItemResponse; import com.bop.youthpick.admin.policy.service.AdminPolicyApplicationService; import com.bop.youthpick.global.common.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import jakarta.validation.constraints.Pattern; import java.time.LocalDate; @@ -23,6 +25,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "관리자 - 정책 신청관리") @RestController @RequestMapping("/api/v1/admin/policy-applications") @RequiredArgsConstructor @@ -31,6 +34,9 @@ public class AdminPolicyApplicationController { private final AdminPolicyApplicationService adminPolicyApplicationService; + @Operation( + summary = "정책 신청 목록 조회", + description = "사용자, 정책명, 상태, 마감일 범위로 정책 신청 목록을 검색하여 페이지 단위로 조회한다.") @GetMapping public ApiResponse> list( @RequestParam(required = false) Long userId, @@ -51,12 +57,14 @@ public ApiResponse> list( return ApiResponse.ok(page.getContent(), page); } + @Operation(summary = "신청 체크리스트 조회", description = "특정 정책 신청 건의 체크리스트 항목 목록을 조회한다.") @GetMapping("/{applicationId}/checklist") public ApiResponse> getChecklist( @PathVariable Long applicationId) { return ApiResponse.ok(adminPolicyApplicationService.getChecklist(applicationId)); } + @Operation(summary = "신청 상태 변경", description = "특정 정책 신청 건의 상태를 변경한다.") @PatchMapping("/{applicationId}/status") public ApiResponse updateStatus( @PathVariable Long applicationId, diff --git a/src/main/java/com/bop/youthpick/admin/policy/controller/AdminPolicyController.java b/src/main/java/com/bop/youthpick/admin/policy/controller/AdminPolicyController.java index 12e5205..d22d7a4 100644 --- a/src/main/java/com/bop/youthpick/admin/policy/controller/AdminPolicyController.java +++ b/src/main/java/com/bop/youthpick/admin/policy/controller/AdminPolicyController.java @@ -5,6 +5,8 @@ import com.bop.youthpick.admin.policy.dto.AdminPolicyVisibilityUpdateRequest; import com.bop.youthpick.admin.policy.service.AdminPolicyService; import com.bop.youthpick.global.common.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import jakarta.validation.constraints.Pattern; import java.time.LocalDate; @@ -25,6 +27,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "관리자 - 정책") @RestController @RequestMapping("/api/v1/admin/policies") @RequiredArgsConstructor @@ -33,6 +36,7 @@ public class AdminPolicyController { private final AdminPolicyService adminPolicyService; + @Operation(summary = "정책 목록 조회", description = "카테고리, 노출 상태, 기간으로 정책 목록을 검색하여 페이지 단위로 조회한다.") @GetMapping public ApiResponse> list( @RequestParam(required = false) String category, @@ -51,12 +55,14 @@ public ApiResponse> list( return ApiResponse.ok(page.getContent(), page); } + @Operation(summary = "정책 정보 수정", description = "특정 정책의 정보를 수정한다.") @PutMapping("/{policyId}") public ApiResponse update( @PathVariable Long policyId, @Valid @RequestBody AdminPolicyUpdateRequest request) { return ApiResponse.ok(adminPolicyService.update(policyId, request)); } + @Operation(summary = "정책 노출 상태 변경", description = "특정 정책의 노출 상태(VISIBLE/HIDDEN)를 변경한다.") @PatchMapping("/{policyId}/visibility") public ApiResponse updateVisibility( @PathVariable Long policyId, @@ -65,6 +71,7 @@ public ApiResponse updateVisibility( adminPolicyService.updateVisibility(policyId, request.visibilityStatus())); } + @Operation(summary = "정책 삭제", description = "특정 정책을 소프트 삭제 처리한다.") @DeleteMapping("/{policyId}") public ApiResponse delete(@PathVariable Long policyId) { return ApiResponse.ok(adminPolicyService.softDelete(policyId)); diff --git a/src/main/java/com/bop/youthpick/admin/policy/controller/AdminRegionController.java b/src/main/java/com/bop/youthpick/admin/policy/controller/AdminRegionController.java index 9a5229f..a9896a6 100644 --- a/src/main/java/com/bop/youthpick/admin/policy/controller/AdminRegionController.java +++ b/src/main/java/com/bop/youthpick/admin/policy/controller/AdminRegionController.java @@ -3,12 +3,15 @@ import com.bop.youthpick.admin.policy.service.AdminRegionService; import com.bop.youthpick.global.common.ApiResponse; import com.bop.youthpick.policy.dto.RegionResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "관리자 - 지역") @RestController @RequestMapping("/api/v1/admin/regions") @RequiredArgsConstructor @@ -16,6 +19,7 @@ public class AdminRegionController { private final AdminRegionService adminRegionService; + @Operation(summary = "지역 목록 조회", description = "전체 지역 목록을 조회한다.") @GetMapping public ApiResponse> list() { return ApiResponse.ok(adminRegionService.findAll()); diff --git a/src/main/java/com/bop/youthpick/admin/sync/controller/AdminBatchJobLogController.java b/src/main/java/com/bop/youthpick/admin/sync/controller/AdminBatchJobLogController.java index 11d4693..3b24e71 100644 --- a/src/main/java/com/bop/youthpick/admin/sync/controller/AdminBatchJobLogController.java +++ b/src/main/java/com/bop/youthpick/admin/sync/controller/AdminBatchJobLogController.java @@ -3,6 +3,8 @@ import com.bop.youthpick.admin.sync.dto.BatchJobLogResponse; import com.bop.youthpick.admin.sync.service.AdminBatchJobLogService; import com.bop.youthpick.global.common.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.constraints.Pattern; import java.time.LocalDate; import java.util.List; @@ -17,6 +19,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "관리자 - 배치 작업 이력") @RestController @RequestMapping("/api/v1/admin/batch-job-logs") @RequiredArgsConstructor @@ -25,6 +28,7 @@ public class AdminBatchJobLogController { private final AdminBatchJobLogService adminBatchJobLogService; + @Operation(summary = "배치 작업 이력 조회", description = "상태, 기간으로 배치 작업 이력 목록을 검색하여 페이지 단위로 조회한다.") @GetMapping public ApiResponse> list( @RequestParam(required = false) diff --git a/src/main/java/com/bop/youthpick/admin/sync/controller/AdminPolicySyncController.java b/src/main/java/com/bop/youthpick/admin/sync/controller/AdminPolicySyncController.java index 742e753..35278e0 100644 --- a/src/main/java/com/bop/youthpick/admin/sync/controller/AdminPolicySyncController.java +++ b/src/main/java/com/bop/youthpick/admin/sync/controller/AdminPolicySyncController.java @@ -2,12 +2,15 @@ import com.bop.youthpick.global.common.ApiResponse; import com.bop.youthpick.sync.service.PolicySyncService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "관리자 - 정책 동기화") @RestController @RequestMapping("/api/v1/admin/batch/policy-sync") @RequiredArgsConstructor @@ -19,6 +22,11 @@ public class AdminPolicySyncController { * 정책 수집을 비동기로 시작시키고 즉시 202를 반환한다. 이미 실행 중이면 {@code SY001} 409 (GlobalExceptionHandler 변환). 결과 * 확인은 {@code GET /api/v1/admin/batch-job-logs}(#57)로 한다. */ + @Operation( + summary = "정책 동기화 트리거", + description = + "정책 수집을 비동기로 시작시키고 즉시 202를 반환한다. 이미 실행 중이면 SY001 409(GlobalExceptionHandler 변환). 결과 확인은 GET" + + " /api/v1/admin/batch-job-logs로 한다.") @PostMapping public ResponseEntity> trigger() { policySyncService.startFullSyncAsync(); diff --git a/src/main/java/com/bop/youthpick/admin/sync/controller/AdminPolicySyncJobController.java b/src/main/java/com/bop/youthpick/admin/sync/controller/AdminPolicySyncJobController.java index 4d65aaa..13d40c3 100644 --- a/src/main/java/com/bop/youthpick/admin/sync/controller/AdminPolicySyncJobController.java +++ b/src/main/java/com/bop/youthpick/admin/sync/controller/AdminPolicySyncJobController.java @@ -3,11 +3,14 @@ import com.bop.youthpick.admin.sync.dto.PolicySyncJobSummaryResponse; import com.bop.youthpick.admin.sync.service.AdminPolicySyncJobService; import com.bop.youthpick.global.common.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "관리자 - 정책 동기화 작업") @RestController @RequestMapping("/api/v1/admin/policy-sync-jobs") @RequiredArgsConstructor @@ -15,6 +18,7 @@ public class AdminPolicySyncJobController { private final AdminPolicySyncJobService adminPolicySyncJobService; + @Operation(summary = "정책 동기화 작업 요약 조회", description = "정책 동기화 작업의 현황 요약 정보를 조회한다.") @GetMapping("/summary") public ApiResponse summary() { return ApiResponse.ok(adminPolicySyncJobService.getSummary()); diff --git a/src/main/java/com/bop/youthpick/admin/user/controller/AdminLoginHistoryController.java b/src/main/java/com/bop/youthpick/admin/user/controller/AdminLoginHistoryController.java index b022d48..7afbda4 100644 --- a/src/main/java/com/bop/youthpick/admin/user/controller/AdminLoginHistoryController.java +++ b/src/main/java/com/bop/youthpick/admin/user/controller/AdminLoginHistoryController.java @@ -3,6 +3,8 @@ import com.bop.youthpick.admin.user.dto.LoginHistoryResponse; import com.bop.youthpick.admin.user.service.AdminLoginHistoryService; import com.bop.youthpick.global.common.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import java.time.LocalDate; import java.util.List; import lombok.RequiredArgsConstructor; @@ -15,6 +17,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "관리자 - 로그인 이력") @RestController @RequestMapping("/api/v1/admin/login-histories") @RequiredArgsConstructor @@ -22,6 +25,7 @@ public class AdminLoginHistoryController { private final AdminLoginHistoryService adminLoginHistoryService; + @Operation(summary = "로그인 이력 목록 조회", description = "회원과 기간으로 로그인 이력을 검색해 페이지 단위로 조회합니다.") @GetMapping public ApiResponse> list( @RequestParam(required = false) Long userId, diff --git a/src/main/java/com/bop/youthpick/admin/user/controller/AdminUserController.java b/src/main/java/com/bop/youthpick/admin/user/controller/AdminUserController.java index ad30ce7..fa7c430 100644 --- a/src/main/java/com/bop/youthpick/admin/user/controller/AdminUserController.java +++ b/src/main/java/com/bop/youthpick/admin/user/controller/AdminUserController.java @@ -5,6 +5,8 @@ import com.bop.youthpick.admin.user.dto.AdminUserRoleUpdateRequest; import com.bop.youthpick.admin.user.service.AdminUserService; import com.bop.youthpick.global.common.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import jakarta.validation.constraints.Pattern; import java.util.List; @@ -22,6 +24,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "관리자 - 회원") @RestController @RequestMapping("/api/v1/admin/users") @RequiredArgsConstructor @@ -30,6 +33,7 @@ public class AdminUserController { private final AdminUserService adminUserService; + @Operation(summary = "회원 목록 조회", description = "권한, 계정 상태, 가입 경로로 회원을 검색해 페이지 단위로 조회합니다.") @GetMapping public ApiResponse> list( @RequestParam(required = false) @@ -47,17 +51,20 @@ public ApiResponse> list( return ApiResponse.ok(page.getContent(), page); } + @Operation(summary = "회원 프로필 조회", description = "지정한 회원의 상세 프로필을 조회합니다.") @GetMapping("/{userId}/profile") public ApiResponse getProfile(@PathVariable Long userId) { return ApiResponse.ok(adminUserService.getProfile(userId)); } + @Operation(summary = "회원 권한 변경", description = "관리자가 지정한 회원의 권한(role)을 변경합니다.") @PatchMapping("/{userId}/role") public ApiResponse updateRole( @PathVariable Long userId, @Valid @RequestBody AdminUserRoleUpdateRequest request) { return ApiResponse.ok(adminUserService.updateRole(userId, request.role())); } + @Operation(summary = "회원 강제 탈퇴", description = "관리자가 지정한 회원을 소프트 삭제(강제 탈퇴) 처리합니다.") @DeleteMapping("/{userId}") public ApiResponse delete(@PathVariable Long userId) { return ApiResponse.ok(adminUserService.softDelete(userId)); diff --git a/src/main/java/com/bop/youthpick/auth/controller/AuthController.java b/src/main/java/com/bop/youthpick/auth/controller/AuthController.java index 2a241b0..52dcfef 100644 --- a/src/main/java/com/bop/youthpick/auth/controller/AuthController.java +++ b/src/main/java/com/bop/youthpick/auth/controller/AuthController.java @@ -13,6 +13,8 @@ import com.bop.youthpick.global.common.ApiResponse; import com.bop.youthpick.global.config.CorsProperties; import com.bop.youthpick.user.entity.User; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -29,6 +31,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "인증") @RestController @RequestMapping("/api/v1/auth") @RequiredArgsConstructor @@ -38,6 +41,9 @@ public class AuthController { private final RefreshTokenCookieSupport refreshTokenCookieSupport; private final CorsProperties corsProperties; + @Operation( + summary = "소셜 로그인 URL 발급", + description = "지정한 OAuth provider(google/naver/kakao)의 인가 URL을 생성해 반환한다.") @GetMapping("/oauth/{provider}/authorization-url") public ApiResponse authorizationUrl( @PathVariable String provider) { @@ -45,6 +51,10 @@ public ApiResponse authorizationUrl( return ApiResponse.ok(new OAuthAuthorizationUrlResponse(url)); } + @Operation( + summary = "소셜 로그인 콜백", + description = + "OAuth provider의 인가 코드로 로그인을 처리하고, access token은 응답 body로, refresh token은 HttpOnly 쿠키로 내려준다.") @PostMapping("/oauth/{provider}/callback") public ResponseEntity> callback( @PathVariable String provider, @Valid @RequestBody OAuthCallbackRequest request) { @@ -58,6 +68,10 @@ public ResponseEntity> callback( * refresh token 자체는 재발급(rotate)하지 않고 발급 시점의 만료 기간까지 그대로 재사용하므로, access token만 새로 내려주고 쿠키는 다시 * 설정하지 않는다. */ + @Operation( + summary = "액세스 토큰 재발급", + description = + "refresh token은 body가 아니라 HttpOnly 쿠키로 전달받는다. Origin 헤더가 있으면 허용 목록에 포함되는지 검증하고, refresh token 자체는 재발급(rotate)하지 않은 채 access token만 새로 내려준다.") @PostMapping("/token/refresh") public ApiResponse refresh( @CookieValue(name = RefreshTokenCookieSupport.COOKIE_NAME, required = false) @@ -71,6 +85,10 @@ public ApiResponse refresh( return ApiResponse.ok(AccessTokenResponse.from(tokens)); } + @Operation( + summary = "로그아웃", + description = + "인증된 사용자의 refresh token을 Redis에서 삭제하고, HttpOnly 쿠키로 전달되던 refresh token 쿠키도 함께 만료시킨다.") @PostMapping("/logout") public ResponseEntity logout(@CurrentUser Long userId, HttpServletResponse response) { authService.logout(userId); @@ -78,6 +96,7 @@ public ResponseEntity logout(@CurrentUser Long userId, HttpServletResponse return ResponseEntity.noContent().build(); } + @Operation(summary = "내 정보 조회", description = "인증된 사용자의 기본 정보를 조회해 반환한다.") @GetMapping("/me") public ApiResponse me(@CurrentUser Long userId) { User user = authService.getCurrentUser(userId); diff --git a/src/main/java/com/bop/youthpick/comment/controller/CommentController.java b/src/main/java/com/bop/youthpick/comment/controller/CommentController.java index cce7fb2..3e8ba1f 100644 --- a/src/main/java/com/bop/youthpick/comment/controller/CommentController.java +++ b/src/main/java/com/bop/youthpick/comment/controller/CommentController.java @@ -6,6 +6,8 @@ import com.bop.youthpick.comment.dto.CommentUpdateRequest; import com.bop.youthpick.comment.service.CommentService; import com.bop.youthpick.global.common.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import java.util.List; import lombok.RequiredArgsConstructor; @@ -20,6 +22,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "댓글") @RestController @RequestMapping("/api/v1/posts/{postId}/comments") @RequiredArgsConstructor @@ -27,6 +30,7 @@ public class CommentController { private final CommentService commentService; + @Operation(summary = "댓글 등록", description = "특정 게시글에 새로운 댓글을 작성하여 등록한다.") @PostMapping public ResponseEntity> create( @CurrentUser Long userId, @@ -37,12 +41,14 @@ public ResponseEntity> create( return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.ok(res)); } + @Operation(summary = "댓글 목록 조회", description = "특정 게시글에 달린 댓글 전체 목록을 조회한다.") @GetMapping public ApiResponse> findAll( @PathVariable Long postId, @CurrentUser(required = false) Long userId) { return commentService.getAllByPostId(postId); } + @Operation(summary = "댓글 수정", description = "댓글 ID로 대상 댓글의 내용을 수정한다.") @PatchMapping("/{commentId}") public ApiResponse update( @CurrentUser Long userId, @@ -52,6 +58,9 @@ public ApiResponse update( return ApiResponse.ok(commentService.update(userId, commentId, request)); } + @Operation( + summary = "댓글 삭제", + description = "댓글 ID로 대상 댓글을 삭제한다. 작성자 본인만 삭제할 수 있는지는 Service에서 검사한다.") @DeleteMapping("/{commentId}") public ApiResponse delete( // [추가] @CurrentUser: JWT 토큰에서 로그인한 사용자의 id를 꺼내 주입해 준다. diff --git a/src/main/java/com/bop/youthpick/file/controller/FileController.java b/src/main/java/com/bop/youthpick/file/controller/FileController.java index ddb0457..f256744 100644 --- a/src/main/java/com/bop/youthpick/file/controller/FileController.java +++ b/src/main/java/com/bop/youthpick/file/controller/FileController.java @@ -5,6 +5,8 @@ import com.bop.youthpick.file.dto.FileUploadResponse; import com.bop.youthpick.file.service.FileService; import com.bop.youthpick.global.common.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.UUID; @@ -24,6 +26,7 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; +@Tag(name = "파일") @RestController @RequestMapping("/api/v1/files") @RequiredArgsConstructor @@ -33,6 +36,7 @@ public class FileController { private final FileService fileService; + @Operation(summary = "파일 업로드", description = "인증된 사용자가 파일을 업로드하고 저장된 파일 정보를 반환한다.") @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity> upload( @CurrentUser Long userId, @RequestPart("file") MultipartFile file) { @@ -40,6 +44,7 @@ public ResponseEntity> upload( return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.ok(response)); } + @Operation(summary = "파일 다운로드", description = "파일 ID로 저장된 파일을 조회해 스트림으로 반환한다.") @GetMapping("/{fileId}") public ResponseEntity download(@PathVariable UUID fileId) { FileDownload file = fileService.download(fileId); diff --git a/src/main/java/com/bop/youthpick/global/config/OpenApiConfig.java b/src/main/java/com/bop/youthpick/global/config/OpenApiConfig.java new file mode 100644 index 0000000..e9d78cc --- /dev/null +++ b/src/main/java/com/bop/youthpick/global/config/OpenApiConfig.java @@ -0,0 +1,39 @@ +package com.bop.youthpick.global.config; + +import io.swagger.v3.oas.models.Components; +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.security.SecurityScheme; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class OpenApiConfig { + + private static final String BEARER_AUTH_SCHEME = "bearerAuth"; + + @Bean + OpenAPI openAPI() { + return new OpenAPI() + .info(apiInfo()) + .addSecurityItem(new SecurityRequirement().addList(BEARER_AUTH_SCHEME)) + .components( + new Components() + .addSecuritySchemes(BEARER_AUTH_SCHEME, bearerAuthScheme())); + } + + private Info apiInfo() { + return new Info() + .title("YouthPick API") + .description("청년 정책 추천 서비스 YouthPick 백엔드 API 문서") + .version("v0.0.1"); + } + + private SecurityScheme bearerAuthScheme() { + return new SecurityScheme() + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT"); + } +} diff --git a/src/main/java/com/bop/youthpick/policy/controller/PolicyApplicationChecklistController.java b/src/main/java/com/bop/youthpick/policy/controller/PolicyApplicationChecklistController.java index 0c3871a..eb4dd00 100644 --- a/src/main/java/com/bop/youthpick/policy/controller/PolicyApplicationChecklistController.java +++ b/src/main/java/com/bop/youthpick/policy/controller/PolicyApplicationChecklistController.java @@ -8,6 +8,8 @@ import com.bop.youthpick.policy.dto.PolicyApplicationUpdateChecklistRequest; import com.bop.youthpick.policy.entity.PolicyApplicationChecklist; import com.bop.youthpick.policy.service.PolicyApplicationChecklistService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import java.util.List; import lombok.RequiredArgsConstructor; @@ -35,6 +37,7 @@ * {@code @Valid @RequestBody} DTO(Create/UpdateChecklistRequest)로만 이루어지고, {@code @RequestParam}에 직접 * 붙는 제약(예: {@code @Pattern})이 없기 때문이다({@code @Validated}는 그 경우에만 필요하다). */ +@Tag(name = "정책 신청 체크리스트") @RestController @RequestMapping("/api/v1/policy-application-checklists") @RequiredArgsConstructor @@ -48,6 +51,13 @@ public class PolicyApplicationChecklistController { * 넘어가고, 서비스 내부에서 {@code PolicyApplicationChecklist.create()}가 이걸 content 필드에 담는다 — DTO는 * "message", 엔티티/리포지토리는 "content"로 이름이 다르니 헷갈리지 않도록 주의. */ + @Operation( + summary = "체크리스트 항목 추가", + description = + "POST /api/v1/policy-application-checklists — 특정 신청관리(applicationId)에 체크리스트 항목 추가." + + " request.message()가 PolicyApplicationChecklistService.add의 message 인자로 그대로 넘어가고, 서비스" + + " 내부에서 PolicyApplicationChecklist.create()가 이걸 content 필드에 담는다 — DTO는 \"message\"," + + " 엔티티/리포지토리는 \"content\"로 이름이 다르니 헷갈리지 않도록 주의.") @PostMapping public ResponseEntity> add( @CurrentUser Long userId, @@ -63,6 +73,12 @@ public ResponseEntity> add( * 체크리스트 목록. 서비스가 먼저 부모 신청관리의 존재/소유권을 확인한 뒤(쿼리 1회), 체크리스트 목록을 id 오름차순(등록 순서)으로 조회한다(쿼리 1회) — 총 * 2쿼리. */ + @Operation( + summary = "신청별 체크리스트 목록 조회", + description = + "GET /api/v1/policy-application-checklists/application/{applicationId} — 특정 신청관리에 달린 체크리스트" + + " 목록. 서비스가 먼저 부모 신청관리의 존재/소유권을 확인한 뒤(쿼리 1회), 체크리스트 목록을 id 오름차순(등록 순서)으로 조회한다(쿼리 1회) — 총" + + " 2쿼리.") @GetMapping("/application/{applicationId}") public ApiResponse> getByApplication( @CurrentUser Long userId, @@ -74,6 +90,10 @@ public ApiResponse> getByApplication( } /** {@code PATCH /api/v1/policy-application-checklists/{id}} — 체크리스트 내용(content) 수정. */ + @Operation( + summary = "체크리스트 내용 수정", + description = + "PATCH /api/v1/policy-application-checklists/{id} — 체크리스트 내용(content) 수정.") @PatchMapping("/{id}") public ApiResponse update( @CurrentUser Long userId, @@ -90,6 +110,11 @@ public ApiResponse update( * PolicyApplicationController#delete}가 {@code ApiResponse.ok(null)}로 데이터 없음을 표현하는 것과 다른 스타일 — * 여긴 메시지 body를 쓴다). */ + @Operation( + summary = "체크리스트 항목 체크", + description = + "PATCH /api/v1/policy-application-checklists/{id}/check — 체크 표시. body 없이 상태만 토글하므로 반환 데이터가" + + " 없고, PolicyApplicationChecklistMessageResponse로 성공 메시지만 내려준다.") @PatchMapping("/{id}/check") public ApiResponse check( @CurrentUser Long userId, @PathVariable Long id) { @@ -98,6 +123,10 @@ public ApiResponse check( } /** {@code PATCH /api/v1/policy-application-checklists/{id}/uncheck} — {@link #check}의 반대. */ + @Operation( + summary = "체크리스트 항목 체크 해제", + description = + "PATCH /api/v1/policy-application-checklists/{id}/uncheck — check의 반대. 체크 표시를 해제한다.") @PatchMapping("/{id}/uncheck") public ApiResponse uncheck( @CurrentUser Long userId, @PathVariable Long id) { @@ -111,6 +140,13 @@ public ApiResponse uncheck( * PolicyApplicationChecklistRepository.softDeleteAllByApplicationId()}이고, 그건 이 컨트롤러가 아니라 {@code * PolicyApplicationService.create()}의 reactivate 분기에서 호출된다(다른 도메인 흐름과의 연결점). */ + @Operation( + summary = "체크리스트 항목 삭제", + description = + "DELETE /api/v1/policy-application-checklists/{id} — 체크리스트 항목 하나만 소프트 삭제. 부모 신청관리 전체가" + + " 삭제/재등록될 때 체크리스트를 한 번에 정리하는 경로는 여기가 아니라" + + " PolicyApplicationChecklistRepository.softDeleteAllByApplicationId()이고, 그건 이 컨트롤러가 아니라" + + " PolicyApplicationService.create()의 reactivate 분기에서 호출된다.") @DeleteMapping("/{id}") public ApiResponse delete( @CurrentUser Long userId, @PathVariable Long id) { diff --git a/src/main/java/com/bop/youthpick/policy/controller/PolicyApplicationController.java b/src/main/java/com/bop/youthpick/policy/controller/PolicyApplicationController.java index d23bee8..15c02da 100644 --- a/src/main/java/com/bop/youthpick/policy/controller/PolicyApplicationController.java +++ b/src/main/java/com/bop/youthpick/policy/controller/PolicyApplicationController.java @@ -11,6 +11,8 @@ import com.bop.youthpick.policy.entity.PolicyApplication; import com.bop.youthpick.policy.exception.PolicyErrorCode; import com.bop.youthpick.policy.service.PolicyApplicationService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; @@ -38,6 +40,7 @@ * "Controller는 얇게" 규칙). */ @Validated +@Tag(name = "정책 신청관리") @RestController @RequestMapping("/api/v1/policy-applications") @RequiredArgsConstructor @@ -52,6 +55,13 @@ public class PolicyApplicationController { * 대신 깔끔한 400(P007)으로 막아준다. 신규 등록 vs soft-delete 재활성화 vs 중복 예외 판단은 {@link * PolicyApplicationService#create}가 전담한다. */ + @Operation( + summary = "정책 신청 관심 등록", + description = + "POST /api/v1/policy-applications — \"관심 등록\" 액션의 진입점. status 문자열 → ApplicationStatus 변환은" + + " parseStatus를 거친다 — 평소엔 DTO의 @Pattern(=ApplicationStatus.VALUES_PATTERN)이 걸러준 값만 들어오지만, 만에 하나" + + " 어긋날 경우를 대비해 parseStatus가 500 대신 깔끔한 400(P007)으로 막아준다. 신규 등록 vs soft-delete 재활성화 vs 중복 예외" + + " 판단은 PolicyApplicationService.create가 전담한다.") @PostMapping public ResponseEntity> create( @CurrentUser Long userId, @Valid @RequestBody PolicyApplicationCreateRequest request) { @@ -72,6 +82,12 @@ public ResponseEntity> create( * PolicyApplicationResponse}로 변환된 {@code Page}를 돌려주므로, 여기선 그 Page를 (content, meta) 형태로 {@link * ApiResponse}에 담기만 한다(api-design.md의 Pageable 규칙). */ + @Operation( + summary = "정책 신청 목록 조회", + description = + "GET /api/v1/policy-applications — 내 정책 신청 목록 페이지 조회. page/size/totalPages는 컨트롤러가 직접" + + " 계산하지 않는다 — PolicyApplicationService.getApplications가 이미 PolicyApplicationResponse로 변환된" + + " Page를 돌려주므로, 여기선 그 Page를 (content, meta) 형태로 ApiResponse에 담기만 한다.") @GetMapping public ApiResponse> getApplications( @CurrentUser Long userId, @PageableDefault(size = 20) Pageable pageable) { @@ -86,6 +102,13 @@ public ApiResponse> getApplications( * 동일하게 유지). 소유권 검증은 여기가 아니라 {@link PolicyApplicationService#changeStatus} 안의 {@code * verifyOwner()}에서 한다 — id로 조회한 신청이 진짜 이 userId 소유인지는 서비스 계층 책임이다. */ + @Operation( + summary = "정책 신청 상태 변경", + description = + "PATCH /api/v1/policy-applications/{id}/status — 상태만 단독으로 바꾸는 엔드포인트. create의 DTO와 같은" + + " ApplicationStatus.VALUES_PATTERN 화이트리스트를 공유한다(parseStatus가 방어선 역할은 동일하게 유지). 소유권 검증은 여기가" + + " 아니라 PolicyApplicationService.changeStatus 안의 verifyOwner()에서 한다 — id로 조회한 신청이 진짜 이 userId" + + " 소유인지는 서비스 계층 책임이다.") @PatchMapping("/{id}/status") public ApiResponse changeStatus( @CurrentUser Long userId, @@ -103,6 +126,11 @@ public ApiResponse changeStatus( * {@code PATCH /api/v1/policy-applications/{id}/memo} — 메모만 단독 수정. 컨트롤러는 {@code @Size}로 DTO 필드 * 길이를 검증하고, 빈 문자열/공백을 null로 통일하는 정규화는 서비스 계층이 담당한다. */ + @Operation( + summary = "정책 신청 메모 수정", + description = + "PATCH /api/v1/policy-applications/{id}/memo — 메모만 단독 수정. 컨트롤러는 @Size로 DTO 필드 길이를" + + " 검증하고, 빈 문자열/공백을 null로 통일하는 정규화는 서비스 계층이 담당한다.") @PatchMapping("/{id}/memo") public ApiResponse updateMemo( @CurrentUser Long userId, @@ -114,6 +142,9 @@ public ApiResponse updateMemo( } /** {@code PATCH /api/v1/policy-applications/{id}/end-at} — 마감일 단독 수정. */ + @Operation( + summary = "정책 신청 마감일 수정", + description = "PATCH /api/v1/policy-applications/{id}/end-at — 마감일 단독 수정.") @PatchMapping("/{id}/end-at") public ApiResponse updateEndAt( @CurrentUser Long userId, @@ -129,6 +160,11 @@ public ApiResponse updateEndAt( * deletedAt}만 세팅한다(재등록 시 reactivate로 되살아남). 반환할 데이터가 없어 {@code ApiResponse.ok(null)} — data는 * null, meta 없음. */ + @Operation( + summary = "정책 신청 관심 해제", + description = + "DELETE /api/v1/policy-applications/{id} — 소프트 삭제(관심 해제). 물리 삭제가 아니라 deletedAt만 세팅한다(재등록" + + " 시 reactivate로 되살아남). 반환할 데이터가 없어 ApiResponse.ok(null) — data는 null, meta 없음.") @DeleteMapping("/{id}") public ApiResponse delete(@CurrentUser Long userId, @PathVariable Long id) { policyApplicationService.delete(id, userId); diff --git a/src/main/java/com/bop/youthpick/policy/controller/PolicyChatController.java b/src/main/java/com/bop/youthpick/policy/controller/PolicyChatController.java index b2d19e0..2ff9706 100644 --- a/src/main/java/com/bop/youthpick/policy/controller/PolicyChatController.java +++ b/src/main/java/com/bop/youthpick/policy/controller/PolicyChatController.java @@ -4,6 +4,8 @@ import com.bop.youthpick.global.common.ApiResponse; import com.bop.youthpick.policy.dto.PolicyChatMessagesResponse; import com.bop.youthpick.policy.service.PolicyChatService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.constraints.Min; import lombok.RequiredArgsConstructor; import org.springframework.validation.annotation.Validated; @@ -13,6 +15,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "정책 채팅") @RestController @RequestMapping("/api/v1/policies/{policyId}/chat/messages") @RequiredArgsConstructor @@ -21,6 +24,9 @@ public class PolicyChatController { private final PolicyChatService policyChatService; + @Operation( + summary = "정책 채팅 메시지 조회", + description = "특정 정책 채팅방의 메시지 목록을 afterId 기준으로 이후 메시지만 조회한다(회원 전용).") @GetMapping public ApiResponse getMessages( @CurrentUser Long userId, diff --git a/src/main/java/com/bop/youthpick/policy/controller/PolicyComparisonController.java b/src/main/java/com/bop/youthpick/policy/controller/PolicyComparisonController.java index 4eb1e6a..a3c14a5 100644 --- a/src/main/java/com/bop/youthpick/policy/controller/PolicyComparisonController.java +++ b/src/main/java/com/bop/youthpick/policy/controller/PolicyComparisonController.java @@ -3,6 +3,8 @@ import com.bop.youthpick.global.common.ApiResponse; import com.bop.youthpick.policy.dto.PolicyComparisonItemResponse; import com.bop.youthpick.policy.service.PolicyComparisonService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.Size; import java.util.List; @@ -17,6 +19,7 @@ * 정책 비교 (비회원 허용). 비교 결과는 저장하지 않고 요청받은 정책들을 그때그때 조회해 내려주는 순수 조회다 — 그래서 생성(POST) + 식별자 재조회가 아니라 쿼리 * 파라미터를 받는 단일 GET이다. 공유가 필요하면 이 요청 URL 자체가 공유 링크가 된다. */ +@Tag(name = "정책 비교") @Validated @RestController @RequestMapping("/api/v1/policy-comparisons") @@ -25,6 +28,11 @@ public class PolicyComparisonController { private final PolicyComparisonService policyComparisonService; + @Operation( + summary = "정책 비교 조회", + description = + "정책 비교(비회원 허용). 비교 결과는 저장하지 않고 요청받은 정책들을 그때그때 조회해 내려주는 순수 조회다 — 그래서 생성(POST) + 식별자" + + " 재조회가 아니라 쿼리 파라미터를 받는 단일 GET이다. 공유가 필요하면 이 요청 URL 자체가 공유 링크가 된다.") @GetMapping public ApiResponse> compare( @RequestParam diff --git a/src/main/java/com/bop/youthpick/policy/controller/PolicyController.java b/src/main/java/com/bop/youthpick/policy/controller/PolicyController.java index c566f11..435a8f0 100644 --- a/src/main/java/com/bop/youthpick/policy/controller/PolicyController.java +++ b/src/main/java/com/bop/youthpick/policy/controller/PolicyController.java @@ -5,6 +5,8 @@ import com.bop.youthpick.policy.dto.PolicyCardResponse; import com.bop.youthpick.policy.dto.PolicyDetailResponse; import com.bop.youthpick.policy.service.PolicyService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -16,6 +18,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "정책") @RestController @RequestMapping("/api/v1/policies") @RequiredArgsConstructor @@ -29,6 +32,13 @@ public class PolicyController { * ageMin/ageMax는 자격 구간과의 겹침, jobCode는 온통청년 취업상태 코드(예: 0013001 재직자) — 모두 선택. meta에 * page/totalCount/totalPages. */ + @Operation( + summary = "정책 목록 조회", + description = + "정책 목록(카드) 조회(비회원 허용). 기본 최신순이되 region·jobCode 필터가 걸리면 조건 없는 정책(전국/취업상태 제한없음)을 뒤로 민다." + + " category(표준 5분류) exact match, keyword는 부분일치 검색, region은 시도명('전국'은 지역 무관이라 필터" + + " 미적용), ageMin/ageMax는 자격 구간과의 겹침, jobCode는 온통청년 취업상태 코드(예: 0013001 재직자) — 모두 선택." + + " meta에 page/totalCount/totalPages.") @GetMapping public ApiResponse> getCards( @RequestParam(required = false) String category, @@ -45,6 +55,7 @@ public ApiResponse> getCards( } /** 정책 상세 조회 (비회원 허용). 로그인 사용자의 조회는 최근 본 정책으로 기록된다. */ + @Operation(summary = "정책 상세 조회", description = "정책 상세 조회(비회원 허용). 로그인 사용자의 조회는 최근 본 정책으로 기록된다.") @GetMapping("/{policyId}") public ApiResponse getDetail( @PathVariable Long policyId, @CurrentUser(required = false) Long userId) { diff --git a/src/main/java/com/bop/youthpick/policy/controller/PolicyRecentViewController.java b/src/main/java/com/bop/youthpick/policy/controller/PolicyRecentViewController.java index ad6b1c9..2c7ba4b 100644 --- a/src/main/java/com/bop/youthpick/policy/controller/PolicyRecentViewController.java +++ b/src/main/java/com/bop/youthpick/policy/controller/PolicyRecentViewController.java @@ -4,6 +4,8 @@ import com.bop.youthpick.global.common.ApiResponse; import com.bop.youthpick.policy.dto.PolicyRecentViewResponse; import com.bop.youthpick.policy.service.PolicyRecentViewService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -13,6 +15,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "최근 본 정책") @RestController @RequestMapping("/api/v1/policy-recent-views") @RequiredArgsConstructor @@ -21,6 +24,7 @@ public class PolicyRecentViewController { private final PolicyRecentViewService policyRecentViewService; /** 최근 본 정책 목록 (회원 전용). 마지막 조회 시각 내림차순. */ + @Operation(summary = "최근 본 정책 목록 조회", description = "최근 본 정책 목록(회원 전용). 마지막 조회 시각 내림차순.") @GetMapping public ApiResponse> list( @CurrentUser Long userId, @PageableDefault(size = 20) Pageable pageable) { diff --git a/src/main/java/com/bop/youthpick/policy/controller/RecommendedPolicyController.java b/src/main/java/com/bop/youthpick/policy/controller/RecommendedPolicyController.java index 1553fc3..2b8af6c 100644 --- a/src/main/java/com/bop/youthpick/policy/controller/RecommendedPolicyController.java +++ b/src/main/java/com/bop/youthpick/policy/controller/RecommendedPolicyController.java @@ -4,6 +4,8 @@ import com.bop.youthpick.global.common.ApiResponse; import com.bop.youthpick.policy.dto.RecommendedPolicyResponse; import com.bop.youthpick.policy.service.RecommendedPolicyService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; @@ -11,6 +13,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "추천 정책") @RestController @RequestMapping("/api/v1/recommended-policies") @RequiredArgsConstructor @@ -19,6 +22,9 @@ public class RecommendedPolicyController { private final RecommendedPolicyService recommendedPolicyService; /** 맞춤정책 조회 (회원 전용). 로그인 사용자의 온보딩 프로필과 정책 자격조건을 매칭해 점수순으로 내린다. */ + @Operation( + summary = "맞춤 정책 추천 조회", + description = "맞춤정책 조회(회원 전용). 로그인 사용자의 온보딩 프로필과 정책 자격조건을 매칭해 점수순으로 내린다.") @GetMapping public ApiResponse> getRecommendations( @CurrentUser Long userId, diff --git a/src/main/java/com/bop/youthpick/policy/controller/RegionController.java b/src/main/java/com/bop/youthpick/policy/controller/RegionController.java index aa265a0..3c2c3a3 100644 --- a/src/main/java/com/bop/youthpick/policy/controller/RegionController.java +++ b/src/main/java/com/bop/youthpick/policy/controller/RegionController.java @@ -3,6 +3,8 @@ import com.bop.youthpick.global.common.ApiResponse; import com.bop.youthpick.policy.dto.RegionResponse; import com.bop.youthpick.policy.service.RegionService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; @@ -10,6 +12,7 @@ import org.springframework.web.bind.annotation.RestController; /** 지역 마스터 전체 목록 조회 (비회원 허용). 온보딩 프로필의 거주지역 선택지로 쓰인다. */ +@Tag(name = "지역") @RestController @RequestMapping("/api/v1/regions") @RequiredArgsConstructor @@ -17,6 +20,9 @@ public class RegionController { private final RegionService regionService; + @Operation( + summary = "지역 목록 조회", + description = "지역 마스터 전체 목록 조회(비회원 허용). 온보딩 프로필의 거주지역 선택지로 쓰인다.") @GetMapping public ApiResponse> list() { return ApiResponse.ok(regionService.getAllRegions()); diff --git a/src/main/java/com/bop/youthpick/post/controller/PostController.java b/src/main/java/com/bop/youthpick/post/controller/PostController.java index 8c7a912..01abc2f 100644 --- a/src/main/java/com/bop/youthpick/post/controller/PostController.java +++ b/src/main/java/com/bop/youthpick/post/controller/PostController.java @@ -8,6 +8,8 @@ import com.bop.youthpick.post.dto.PostUpdateRequest; import com.bop.youthpick.post.entity.PostCategory; import com.bop.youthpick.post.service.PostService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import java.util.List; @@ -21,6 +23,7 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +@Tag(name = "게시글") @RestController @RequestMapping("/api/v1/posts") @RequiredArgsConstructor @@ -31,6 +34,7 @@ public class PostController { private final PostService postService; + @Operation(summary = "게시글 등록", description = "새로운 게시글을 작성하여 등록한다.") @PostMapping public ResponseEntity> create( @CurrentUser Long userId, @Valid @RequestBody PostCreateRequest request) { @@ -39,6 +43,7 @@ public ResponseEntity> create( .body(ApiResponse.ok(response)); // 성공했을때 http status 201 } + @Operation(summary = "게시글 목록 조회", description = "카테고리/검색어로 게시글 목록을 페이지 단위로 조회한다.") @GetMapping // 리스폰스엔티티로 감싸야함 수정필요 public ApiResponse> findAll( @RequestParam(required = false) PostCategory category, @@ -51,6 +56,7 @@ public ApiResponse> findAll( return ApiResponse.ok(page.getContent(), page); } + @Operation(summary = "게시글 상세 조회", description = "게시글 ID로 상세 내용을 조회한다.") @GetMapping("/{postId}") public ApiResponse findById( @PathVariable Long postId, @@ -59,6 +65,7 @@ public ApiResponse findById( return ApiResponse.ok(postService.findById(postId, userId, request.getRemoteAddr())); } + @Operation(summary = "게시글 수정", description = "게시글 ID로 대상 게시글의 내용을 수정한다.") @PatchMapping("/{postId}") public ApiResponse update( @CurrentUser Long userId, @@ -67,6 +74,7 @@ public ApiResponse update( return ApiResponse.ok(postService.update(userId, postId, request)); } + @Operation(summary = "게시글 삭제", description = "게시글 ID로 대상 게시글을 삭제한다.") @DeleteMapping("/{postId}") public ApiResponse delete(@CurrentUser Long userId, @PathVariable Long postId) { postService.delete(userId, postId); diff --git a/src/main/java/com/bop/youthpick/user/controller/UserProfileController.java b/src/main/java/com/bop/youthpick/user/controller/UserProfileController.java index 620eb82..3717b0c 100644 --- a/src/main/java/com/bop/youthpick/user/controller/UserProfileController.java +++ b/src/main/java/com/bop/youthpick/user/controller/UserProfileController.java @@ -8,6 +8,8 @@ import com.bop.youthpick.user.dto.UserProfileResponse; import com.bop.youthpick.user.entity.UserProfile; import com.bop.youthpick.user.service.UserProfileService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -19,6 +21,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; +@Tag(name = "회원 프로필") @RestController @RequiredArgsConstructor public class UserProfileController { @@ -27,6 +30,10 @@ public class UserProfileController { // 경로의 userId는 프론트 호환을 위해 유지하되, 실제 대상은 인증 principal이다. // path와 principal이 다르면 타인 프로필 생성(IDOR)이므로 403으로 거부한다. + @Operation( + summary = "회원 프로필 등록", + description = + "경로의 userId는 프론트 호환을 위해 유지하되 실제 대상은 인증 principal이며, path와 principal이 다르면 타인 프로필 생성(IDOR)이므로 403으로 거부한다.") @PostMapping("/api/v1/users/{userId}/profile") public ResponseEntity> submit( @CurrentUser Long currentUserId, @@ -40,12 +47,16 @@ public ResponseEntity> submit( .body(ApiResponse.ok(UserProfileResponse.from(profile))); } + @Operation( + summary = "내 프로필 조회", + description = "인증된 사용자의 프로필을 조회해 반환한다. 등록된 프로필이 없으면 null을 반환한다.") @GetMapping("/api/v1/me/profile") public ApiResponse getMyProfile(@CurrentUser Long userId) { UserProfile profile = userProfileService.getMyProfile(userId); return ApiResponse.ok(profile == null ? null : UserProfileResponse.from(profile)); } + @Operation(summary = "내 프로필 수정", description = "인증된 사용자의 프로필 정보를 수정한다.") @PatchMapping("/api/v1/me/profile") public ApiResponse updateMyProfile( @CurrentUser Long userId, @Valid @RequestBody UserProfileRequest request) {