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 @@ -12,15 +12,20 @@
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;
import lombok.extern.log4j.Log4j2;
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;
Expand All @@ -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<ApiResponse<PaymentSuccessResponse>> requestSuccess(
@AuthenticationPrincipal Jwt jwt,
Expand All @@ -50,7 +63,14 @@ public ResponseEntity<ApiResponse<PaymentSuccessResponse>> requestSuccess(
return ok(successWithDataOnly(PaymentSuccessResponse.from(response)));
}

@Operation(summary = "결제 요청 실패", description = "결제 요청에 실패하였습니다.")
@Operation(
summary = "결제 요청 실패",
description = """
결제 요청이 실패했을 때 호출됩니다.
서버에 실패 로그를 기록하고, 주문과 결제의 상태를 변경합니다.
"""
)
@PreAuthorize("isAuthenticated()")
@PostMapping("/fail")
public ResponseEntity<ApiResponse<Void>> requestFail(
@AuthenticationPrincipal Jwt jwt,
Expand All @@ -62,17 +82,35 @@ public ResponseEntity<ApiResponse<Void>> requestFail(
return ok(success());
}

@Operation(summary = "결제 조회", description = "주문ID에 대한 결제 내역을 조회합니다.")
@Operation(
summary = "결제 단건 조회",
description = "특정 주문 ID에 대한 결제 내역을 조회합니다."
)
@PreAuthorize("isAuthenticated()")
@GetMapping("/{orderId}")
public ResponseEntity<ApiResponse<PaymentInfoResponse>> 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<ApiResponse<Page<PaymentInfoResponse>>> getPaymentByOrderId(
@AuthenticationPrincipal Jwt jwt,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public ResponseEntity<ApiResponse<StoreRegisterResponse>> register(@RequestBody
}

@PreAuthorize("hasAnyRole('CUSTOMER', 'OWNER', 'MANAGER', 'MASTER')")
@GetMapping("/v1/reviews")
@GetMapping("/v1/stores")
public ResponseEntity<ApiResponse<Page<StoreSearchResponse>>> search(
@AuthenticationPrincipal Jwt jwt,
@RequestParam UUID ownerId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<ApiResponse<Page<TransactionInfoResponse>>> getTransactions (
@AuthenticationPrincipal Jwt jwt,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -27,41 +29,70 @@
@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<ApiResponse<Page<AdminUserResponse>>> search(
@AuthenticationPrincipal Jwt jwt,
@Parameter(description = "회원 검색 조건 (username, nickname, role)")
UserSearchRequest searchRequest,
@Parameter(description = "페이지네이션 정보 (page, size, sort)")
@PageableDefault Pageable pageable
) {
Page<AdminUserResponse> response = adminApplication.search(UUID.fromString(jwt.getSubject()), searchRequest, pageable);

return ok(successWithDataOnly(response));
}

@Operation(
summary = "특정 회원 정보 조회",
description = "userId로 등록된 회원의 모든 정보를 조회합니다."
)
@PreAuthorize("hasAnyRole('MANAGER', 'MASTER')")
@Operation(summary = "특정 회원 정보 조회", description = "userId로 등록된 회원의 모든 정보를 조회합니다.")
@GetMapping("/{userId}")
public ResponseEntity<ApiResponse<AdminUserResponse>> 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);

return ok(successWithDataOnly(response));
}

@Operation(
summary = "특정 회원 역할 수정",
description = """
UserId로 특정 회원의 역할을 수정합니다.
MASTER만 접근할 수 있습니다.
"""
)
@PreAuthorize("hasRole('MASTER')")
@Operation(summary = "특정 회원 역할 수정", description = "userId로 등록된 회원의 역할을 수정합니다.")
@PutMapping("/{userId}/role")
public ResponseEntity<ApiResponse<Void>> changeUserRole(
@AuthenticationPrincipal Jwt jwt,
@Parameter(
name = "userId",
description = "회원 ID (UUID)",
in = ParameterIn.PATH,
required = true
)
@PathVariable UUID userId,
@Valid @RequestBody UserRoleChangeRequest request
) {
Expand All @@ -70,11 +101,23 @@ public ResponseEntity<ApiResponse<Void>> changeUserRole(
return ok(success());
}

@Operation(
summary = "회원 강제 탈퇴",
description = """
등록된 회원을 관리자가 강제로 삭제합니다.
MANAGER/MASTER만 접근 가능합니다.
"""
)
@PreAuthorize("hasAnyRole('MANAGER', 'MASTER')")
@Operation(summary = "회원탈퇴", description = "등록된 회원을 강제로 삭제합니다.")
@DeleteMapping("/{userId}")
public ResponseEntity<ApiResponse<Void>> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;
Expand All @@ -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<ApiResponse<Void>> signup(@Valid @RequestBody SignupReqeust request) {
Expand All @@ -61,7 +69,10 @@ public ResponseEntity<ApiResponse<Void>> signup(@Valid @RequestBody SignupReqeus
return ok(success());
}

@Operation(summary = "인증 토큰 발급", description = "username, password 인증을 통해서 승인된 회원이 접근할 수 있는 토큰을 발급합니다.")
@Operation(
summary = "로그인",
description = "username, password 인증을 통해서 승인된 회원이 접근할 수 있는 토큰을 발급합니다."
)
@PostMapping("/login")
public ResponseEntity<ApiResponse<TokenResponse>> generateToken(@Valid @RequestBody TokenRequest request) {
TokenInfo token = tokenService.generate(request.username(), request.password());
Expand All @@ -76,32 +87,62 @@ public ResponseEntity<ApiResponse<TokenResponse>> generateToken(@Valid @RequestB
return ok(successWithDataOnly(response));
}

@Operation(summary = "로그아웃", description = "발급된 토큰을 무효화. 즉, 로그아웃합니다.")
@Operation(
summary = "로그아웃",
description = """
Keycloak 회원 세션에서 로그아웃 처리하며, RefreshToken을 무효화합니다.
이미 발급된 AccessToken은 블랙리스트에 저장하여 Security Filter로 검증합니다.
"""
)
@PreAuthorize("isAuthenticated()")
@PostMapping("/logout")
public ResponseEntity<ApiResponse<Void>> logout(@AuthenticationPrincipal Jwt jwt) {
userApplication.logout(UUID.fromString(jwt.getSubject()), jwt.getTokenValue(), jwt.getExpiresAt());

return ok(success());
}

@Operation(summary = "로그인한 회원 정보 조회", description = "로그인한 회원의 정보를 조회합니다.")
@Operation(
summary = "로그인한 회원 정보 조회",
description = "JWT를 기반으로 현재 로그인한 회원의 정보를 반환합니다."
)
@PreAuthorize("isAuthenticated()")
@GetMapping()
public ResponseEntity<ApiResponse<LoginUserInfoResponse>> getMyInfo(@AuthenticationPrincipal Jwt jwt) {
User response = userApplication.getLoginUser(UUID.fromString(jwt.getSubject()));

return ok(successWithDataOnly(LoginUserInfoResponse.from(response)));
}

@Operation(summary = "특정 회원 정보 조회", description = "UserId로 다른 회원의 정보를 조회합니다.")
@Operation(
summary = "특정 회원 정보 조회",
description = """
UserId로 특정 회원의 정보를 조회합니다.
username, userPhone, currentAddress 같은 민감한 정보는 제외됩니다.
"""
)
@PreAuthorize("isAuthenticated()")
@GetMapping("/{userId}")
public ResponseEntity<ApiResponse<UserInfoResponse>> getUserInfo(@PathVariable UUID userId) {
public ResponseEntity<ApiResponse<UserInfoResponse>> 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<ApiResponse<Void>> changeMyInfo(
@AuthenticationPrincipal Jwt jwt,
@Valid @RequestBody UserInfoChangeRequest request
Expand All @@ -117,7 +158,14 @@ public ResponseEntity<ApiResponse<Void>> changeMyInfo(
return ok(success());
}

@Operation(summary = "회원탈퇴", description = "로그인한 회원을 삭제합니다.")
@Operation(
summary = "회원탈퇴",
description = """
로그인한 회원의 계정을 삭제합니다.
Keycloak에서 삭제되고, DB에서는 논리 삭제 됩니다.
"""
)
@PreAuthorize("isAuthenticated()")
@DeleteMapping("/{userId}")
public ResponseEntity<ApiResponse<Void>> unsubscribe(@AuthenticationPrincipal Jwt jwt) {
userApplication.delete(UUID.fromString(jwt.getSubject()));
Expand Down