diff --git a/src/main/java/com/sparta/deliveryi/payment/presentation/PaymentApi.java b/src/main/java/com/sparta/deliveryi/payment/presentation/PaymentApi.java index 44d4cb0..c587159 100644 --- a/src/main/java/com/sparta/deliveryi/payment/presentation/PaymentApi.java +++ b/src/main/java/com/sparta/deliveryi/payment/presentation/PaymentApi.java @@ -12,6 +12,9 @@ import com.sparta.deliveryi.payment.presentation.dto.PaymentSuccessRequest; import com.sparta.deliveryi.payment.presentation.dto.PaymentSuccessResponse; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -19,8 +22,10 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.bind.annotation.*; import java.util.UUID; @@ -33,13 +38,21 @@ @RestController @RequiredArgsConstructor @RequestMapping("/v1/payments") -@Tag(name = "결제 API", description = "") +@Tag(name = "결제 API", description = "결제 승인, 실패, 조회 기능 제공") public class PaymentApi { private final PaymentApplication paymentApplication; - @Operation(summary = "결제 승인 요청", - description = "결제 요청이 성공하여, 결제 승인을 진행합니다.") + @Operation( + summary = "결제 승인 요청", + description = """ + 결제 요청이 성공하여, 결제 승인을 처리합니다. + - 결제 요청이 성공하면, Toss SDK는 Front-end로 `paymentKey`, `orderId`, `amount`등을 반환합니다. + - Front-end는 해당 정보를 포함해 `/v1/payments/success`를 호출하여 결제 승인을 요청합니다. + - 서버는 Toss 결제 승인 API를 호출해 결제를 최종 승인합니다. + """ + ) + @PreAuthorize("isAuthenticated()") @PostMapping("/success") public ResponseEntity> requestSuccess( @AuthenticationPrincipal Jwt jwt, @@ -50,7 +63,14 @@ public ResponseEntity> requestSuccess( return ok(successWithDataOnly(PaymentSuccessResponse.from(response))); } - @Operation(summary = "결제 요청 실패", description = "결제 요청에 실패하였습니다.") + @Operation( + summary = "결제 요청 실패", + description = """ + 결제 요청이 실패했을 때 호출됩니다. + 서버에 실패 로그를 기록하고, 주문과 결제의 상태를 변경합니다. + """ + ) + @PreAuthorize("isAuthenticated()") @PostMapping("/fail") public ResponseEntity> requestFail( @AuthenticationPrincipal Jwt jwt, @@ -62,17 +82,35 @@ public ResponseEntity> requestFail( return ok(success()); } - @Operation(summary = "결제 조회", description = "주문ID에 대한 결제 내역을 조회합니다.") + @Operation( + summary = "결제 단건 조회", + description = "특정 주문 ID에 대한 결제 내역을 조회합니다." + ) + @PreAuthorize("isAuthenticated()") @GetMapping("/{orderId}") public ResponseEntity> getPaymentByOrderId( @AuthenticationPrincipal Jwt jwt, + @Parameter( + name = "orderId", + description = "주문 ID (UUID)", + in = ParameterIn.PATH, + required = true + ) @PathVariable UUID orderId ) { Payment payment = paymentApplication.getPaymentByOrderId(UUID.fromString(jwt.getSubject()), orderId); return ok(successWithDataOnly(PaymentInfoResponse.from(payment))); } - @Operation(summary = "결제 목록 조회(관리자용)", description = "모든 결제 내역을 조회합니다.") + @Operation( + summary = "결제 목록 조회(관리자용)", + description = """ + 관리자가 전체 결제 내역을 검색 및 페이징 조회합니다. + 결제 상태: PENDING, APPROVED, REFUNDED, FAILED + MANAGER/MASTER만 접근 가능합니다. + """ + ) + @PreAuthorize("hasAnyRole('MANAGER','MASTER')") @GetMapping public ResponseEntity>> getPaymentByOrderId( @AuthenticationPrincipal Jwt jwt, diff --git a/src/main/java/com/sparta/deliveryi/store/presentation/webapi/StoreApi.java b/src/main/java/com/sparta/deliveryi/store/presentation/webapi/StoreApi.java index cdffa4f..4a59b5a 100644 --- a/src/main/java/com/sparta/deliveryi/store/presentation/webapi/StoreApi.java +++ b/src/main/java/com/sparta/deliveryi/store/presentation/webapi/StoreApi.java @@ -40,7 +40,7 @@ public ResponseEntity> register(@RequestBody } @PreAuthorize("hasAnyRole('CUSTOMER', 'OWNER', 'MANAGER', 'MASTER')") - @GetMapping("/v1/reviews") + @GetMapping("/v1/stores") public ResponseEntity>> search( @AuthenticationPrincipal Jwt jwt, @RequestParam UUID ownerId, diff --git a/src/main/java/com/sparta/deliveryi/transaction/presentation/TransactionApi.java b/src/main/java/com/sparta/deliveryi/transaction/presentation/TransactionApi.java index a694f64..c748a88 100644 --- a/src/main/java/com/sparta/deliveryi/transaction/presentation/TransactionApi.java +++ b/src/main/java/com/sparta/deliveryi/transaction/presentation/TransactionApi.java @@ -11,6 +11,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.web.bind.annotation.GetMapping; @@ -26,12 +27,24 @@ @RestController @RequiredArgsConstructor @RequestMapping("/v1/payments/transactions") -@Tag(name = "결제 트랜잭션 API", description = "") +@Tag(name = "결제 트랜잭션 API", description = "결제 관련 트랜잭션(승인, 취소, 실패 등) 내역 조회 기능 제공") public class TransactionApi { private final TransactionApplication transactionApplication; - @Operation(summary = "결제 트랜잭션 목록 조회(관리자용)", description = "모든 결제 트랜잭션 목록을 조회합니다.") + @Operation( + summary = "결제 트랜잭션 목록 조회(관리자용)", + description = """ + 전체 결제 트랜잭션 목록을 검색 및 페이징 조회합니다. + 검색 조건 + - 주문 ID (orderId) + - 거래 유형 (type: REQUEST/APPROVE/REFUND) + - 거래 상태 (status: SUCCESS/FAIL/PENDING) + - 사용자 아이디 (username) + MANAGER/MASTER만 접근 가능합니다. + """ + ) + @PreAuthorize("hasAnyRole('MANAGER', 'MASTER')") @GetMapping public ResponseEntity>> getTransactions ( @AuthenticationPrincipal Jwt jwt, diff --git a/src/main/java/com/sparta/deliveryi/user/presentation/webapi/AdminUserApi.java b/src/main/java/com/sparta/deliveryi/user/presentation/webapi/AdminUserApi.java index ec2566b..1508f54 100644 --- a/src/main/java/com/sparta/deliveryi/user/presentation/webapi/AdminUserApi.java +++ b/src/main/java/com/sparta/deliveryi/user/presentation/webapi/AdminUserApi.java @@ -6,6 +6,8 @@ import com.sparta.deliveryi.user.application.service.AdminApplication; import com.sparta.deliveryi.user.presentation.dto.UserRoleChangeRequest; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -27,17 +29,25 @@ @RestController @RequiredArgsConstructor @RequestMapping("/v1/admin/users") -@Tag(name = "회원 API(관리자용)", description = "관리자용 API로, 회원 정보 조회 시 모든 항목을 확인할 수 있습니다.") +@Tag(name = "회원 API(관리자용)", description = "관리자용 회원 정보 조회, 역할 수정, 탈퇴 기능 제공") public class AdminUserApi { private final AdminApplication adminApplication; + @Operation( + summary = "회원 목록 조회", + description = """ + 등록된 모든 회원 정보를 조회합니다. + 검색 조건과 페이징을 사용할 수 있습니다. + """ + ) @PreAuthorize("hasAnyRole('MANAGER', 'MASTER')") - @Operation(summary = "회원 목록 조회", description = "등록된 모든 회원 정보를 조회합니다.") @GetMapping("/all") public ResponseEntity>> search( @AuthenticationPrincipal Jwt jwt, + @Parameter(description = "회원 검색 조건 (username, nickname, role)") UserSearchRequest searchRequest, + @Parameter(description = "페이지네이션 정보 (page, size, sort)") @PageableDefault Pageable pageable ) { Page response = adminApplication.search(UUID.fromString(jwt.getSubject()), searchRequest, pageable); @@ -45,11 +55,20 @@ public ResponseEntity>> search( return ok(successWithDataOnly(response)); } + @Operation( + summary = "특정 회원 정보 조회", + description = "userId로 등록된 회원의 모든 정보를 조회합니다." + ) @PreAuthorize("hasAnyRole('MANAGER', 'MASTER')") - @Operation(summary = "특정 회원 정보 조회", description = "userId로 등록된 회원의 모든 정보를 조회합니다.") @GetMapping("/{userId}") public ResponseEntity> getMyInfo( @AuthenticationPrincipal Jwt jwt, + @Parameter( + name = "userId", + description = "회원 ID (UUID)", + in = ParameterIn.PATH, + required = true + ) @PathVariable UUID userId ) { AdminUserResponse response = adminApplication.getUserById(UUID.fromString(jwt.getSubject()), userId); @@ -57,11 +76,23 @@ public ResponseEntity> getMyInfo( return ok(successWithDataOnly(response)); } + @Operation( + summary = "특정 회원 역할 수정", + description = """ + UserId로 특정 회원의 역할을 수정합니다. + MASTER만 접근할 수 있습니다. + """ + ) @PreAuthorize("hasRole('MASTER')") - @Operation(summary = "특정 회원 역할 수정", description = "userId로 등록된 회원의 역할을 수정합니다.") @PutMapping("/{userId}/role") public ResponseEntity> changeUserRole( @AuthenticationPrincipal Jwt jwt, + @Parameter( + name = "userId", + description = "회원 ID (UUID)", + in = ParameterIn.PATH, + required = true + ) @PathVariable UUID userId, @Valid @RequestBody UserRoleChangeRequest request ) { @@ -70,11 +101,23 @@ public ResponseEntity> changeUserRole( return ok(success()); } + @Operation( + summary = "회원 강제 탈퇴", + description = """ + 등록된 회원을 관리자가 강제로 삭제합니다. + MANAGER/MASTER만 접근 가능합니다. + """ + ) @PreAuthorize("hasAnyRole('MANAGER', 'MASTER')") - @Operation(summary = "회원탈퇴", description = "등록된 회원을 강제로 삭제합니다.") @DeleteMapping("/{userId}") public ResponseEntity> unsubscribe( @AuthenticationPrincipal Jwt jwt, + @Parameter( + name = "userId", + description = "회원 ID (UUID)", + in = ParameterIn.PATH, + required = true + ) @PathVariable UUID userId ) { adminApplication.delete(UUID.fromString(jwt.getSubject()), userId); diff --git a/src/main/java/com/sparta/deliveryi/user/presentation/webapi/UserApi.java b/src/main/java/com/sparta/deliveryi/user/presentation/webapi/UserApi.java index 1e8737e..be9d34d 100644 --- a/src/main/java/com/sparta/deliveryi/user/presentation/webapi/UserApi.java +++ b/src/main/java/com/sparta/deliveryi/user/presentation/webapi/UserApi.java @@ -16,11 +16,15 @@ import com.sparta.deliveryi.user.presentation.dto.TokenResponse; import com.sparta.deliveryi.user.presentation.dto.UserInfoChangeRequest; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.web.bind.annotation.*; @@ -34,13 +38,17 @@ @RestController @RequiredArgsConstructor @RequestMapping("/v1/users") -@Tag(name = "회원 API", description = "") +@Tag(name = "회원 API", description = "회원 가입, 로그인, 정보조회 및 수정, 탈퇴 기능 제공") public class UserApi { private final TokenGenerateService tokenService; private final UserApplication userApplication; - @Operation(summary = "회원가입", description = "신규 회원을 등록합니다. 가입 시 기본 권한은 'CUSTOMER' 입니다.") + @Operation( + summary = "회원가입", + description = "신규 회원을 등록합니다. 가입 시 기본 권한은 'CUSTOMER' 입니다.", + security = @SecurityRequirement(name = "") + ) @ResponseStatus(HttpStatus.CREATED) @PostMapping("/signup") public ResponseEntity> signup(@Valid @RequestBody SignupReqeust request) { @@ -61,7 +69,10 @@ public ResponseEntity> signup(@Valid @RequestBody SignupReqeus return ok(success()); } - @Operation(summary = "인증 토큰 발급", description = "username, password 인증을 통해서 승인된 회원이 접근할 수 있는 토큰을 발급합니다.") + @Operation( + summary = "로그인", + description = "username, password 인증을 통해서 승인된 회원이 접근할 수 있는 토큰을 발급합니다." + ) @PostMapping("/login") public ResponseEntity> generateToken(@Valid @RequestBody TokenRequest request) { TokenInfo token = tokenService.generate(request.username(), request.password()); @@ -76,7 +87,14 @@ public ResponseEntity> generateToken(@Valid @RequestB return ok(successWithDataOnly(response)); } - @Operation(summary = "로그아웃", description = "발급된 토큰을 무효화. 즉, 로그아웃합니다.") + @Operation( + summary = "로그아웃", + description = """ + Keycloak 회원 세션에서 로그아웃 처리하며, RefreshToken을 무효화합니다. + 이미 발급된 AccessToken은 블랙리스트에 저장하여 Security Filter로 검증합니다. + """ + ) + @PreAuthorize("isAuthenticated()") @PostMapping("/logout") public ResponseEntity> logout(@AuthenticationPrincipal Jwt jwt) { userApplication.logout(UUID.fromString(jwt.getSubject()), jwt.getTokenValue(), jwt.getExpiresAt()); @@ -84,7 +102,11 @@ public ResponseEntity> logout(@AuthenticationPrincipal Jwt jwt return ok(success()); } - @Operation(summary = "로그인한 회원 정보 조회", description = "로그인한 회원의 정보를 조회합니다.") + @Operation( + summary = "로그인한 회원 정보 조회", + description = "JWT를 기반으로 현재 로그인한 회원의 정보를 반환합니다." + ) + @PreAuthorize("isAuthenticated()") @GetMapping() public ResponseEntity> getMyInfo(@AuthenticationPrincipal Jwt jwt) { User response = userApplication.getLoginUser(UUID.fromString(jwt.getSubject())); @@ -92,16 +114,35 @@ public ResponseEntity> getMyInfo(@Authenticat return ok(successWithDataOnly(LoginUserInfoResponse.from(response))); } - @Operation(summary = "특정 회원 정보 조회", description = "UserId로 다른 회원의 정보를 조회합니다.") + @Operation( + summary = "특정 회원 정보 조회", + description = """ + UserId로 특정 회원의 정보를 조회합니다. + username, userPhone, currentAddress 같은 민감한 정보는 제외됩니다. + """ + ) + @PreAuthorize("isAuthenticated()") @GetMapping("/{userId}") - public ResponseEntity> getUserInfo(@PathVariable UUID userId) { + public ResponseEntity> getUserInfo( + @Parameter( + name = "userId", + description = "조회할 회원의 ID (UUID)", + in = ParameterIn.PATH, + required = true + ) + @PathVariable UUID userId + ) { User response = userApplication.getUserById(userId); return ok(successWithDataOnly(UserInfoResponse.from(response))); } - @Operation(summary = "회원 정보 수정", description = "로그인한 회원의 정보를 수정합니다.") - @PutMapping() + @Operation( + summary = "회원 정보 수정", + description = "로그인한 회원의 nickname, userPhone, currentAddress를 수정합니다." + ) + @PreAuthorize("isAuthenticated()") + @PutMapping public ResponseEntity> changeMyInfo( @AuthenticationPrincipal Jwt jwt, @Valid @RequestBody UserInfoChangeRequest request @@ -117,7 +158,14 @@ public ResponseEntity> changeMyInfo( return ok(success()); } - @Operation(summary = "회원탈퇴", description = "로그인한 회원을 삭제합니다.") + @Operation( + summary = "회원탈퇴", + description = """ + 로그인한 회원의 계정을 삭제합니다. + Keycloak에서 삭제되고, DB에서는 논리 삭제 됩니다. + """ + ) + @PreAuthorize("isAuthenticated()") @DeleteMapping("/{userId}") public ResponseEntity> unsubscribe(@AuthenticationPrincipal Jwt jwt) { userApplication.delete(UUID.fromString(jwt.getSubject()));