Skip to content

[FEAT] 어드민 기능 추가#350

Merged
unifolio0 merged 2 commits into
developfrom
feat/#349
Apr 7, 2026
Merged

[FEAT] 어드민 기능 추가#350
unifolio0 merged 2 commits into
developfrom
feat/#349

Conversation

@unifolio0

Copy link
Copy Markdown
Contributor

closed #349

작업 내용

스크린샷

참고 사항

@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

본 PR은 시스템 관리자가 결제 내역을 확인하고 필요시 결제를 취소할 수 있도록 하는 어드민 기능을 도입합니다. 결제 상태 및 기간별 필터링 기능을 포함하여 관리 효율성을 높였으며, 관련 DTO와 서비스 로직을 신규로 구성하여 기존 결제 시스템과 연동하였습니다.

Highlights

  • 어드민 결제 관리 기능 추가: 결제 목록 조회(전체 및 회원별)와 결제 취소 기능을 수행하는 어드민용 컨트롤러 및 서비스 계층을 구현했습니다.
  • 데이터 조회 최적화: 결제 정보와 결제 결과를 효율적으로 조회하기 위해 리포지토리에 필터링 및 ID 목록 기반 조회 쿼리를 추가했습니다.
  • 테스트 코드 작성: 새로 추가된 어드민 결제 API에 대한 컨트롤러 테스트와 서비스 로직 검증을 위한 단위 테스트를 작성했습니다.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.


결제 내역 한눈에 보고, 취소 요청도 빠르게 처리. 관리자의 손끝에서, 시스템은 더욱 견고해지네.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai

coderabbitai Bot commented Apr 6, 2026

Copy link
Copy Markdown

Caution

Review failed

Pull request was closed or merged during review

Summary by CodeRabbit

  • 새로운 기능
    • 관리자용 결제 관리 API 추가: 전체/회원별 결제 목록 조회
    • 결제 취소 기능 추가(관리자 입력 취소 사유)
  • 개선
    • 결제 목록 필터링: 회원, 상태, 생성일 범위 및 페이징/정렬 지원
    • 응답에 결제 결과(승인/취소 메타데이터) 포함
  • 테스트
    • 관련 엔드투엔드 및 컨트롤러 테스트 추가 (API 동작 검증 및 문서화 검증 포함)

Walkthrough

관리자용 결제 관리 기능이 추가되었습니다. 결제 목록 조회(필터링·페이징, 선택적 memberId/state/startDate/endDate)와 결제 취소 API가 컨트롤러, 서비스, DTO, 리포지토리 및 통합 테스트와 함께 도입되었습니다.

Changes

Cohort / File(s) Summary
Admin Controller
src/main/java/com/samhap/kokomen/admin/controller/AdminPaymentController.java
/api/v1/admin/payments 조회(옵션: memberId, state, startDate, endDate, pageable) 및 POST /api/v1/admin/payments/{paymentId}/cancel 엔드포인트 추가.
Admin Service
src/main/java/com/samhap/kokomen/admin/service/AdminPaymentService.java
결제 조회(findPayments) 및 취소(cancelPayment) 로직 추가. 결과 일괄 조회 후 DTO 매핑 및 존재하지 않으면 NotFound 예외 처리. PaymentFacadeService로 취소 위임.
Admin DTOs
src/main/java/com/samhap/kokomen/admin/service/dto/AdminCancelPaymentRequest.java, .../AdminPaymentPageResponse.java, .../AdminPaymentResponse.java, .../AdminPaymentResultResponse.java
관리자용 취소 요청 DTO(검증 포함), 페이지 응답 및 결제/결과 응답 DTO(팩토리 메서드 포함) 추가.
Payment Repositories
src/main/java/com/samhap/kokomen/payment/repository/TosspaymentsPaymentRepository.java, .../TosspaymentsPaymentResultRepository.java
필터링·페이징 조회용 findPaymentsWithFilters(...) 및 다중 결제 ID로 결과 조회하는 findByTosspaymentsPaymentIdIn(...) 메서드 추가.
Tests
src/test/java/com/samhap/kokomen/admin/controller/AdminPaymentControllerTest.java, src/test/java/com/samhap/kokomen/admin/service/AdminPaymentServiceTest.java
컨트롤러(MockMvc) 및 서비스 레이어 통합 테스트 추가. 결제 목록 조회(페이징/필터)와 취소 흐름, 예외 케이스 검증 및 REST Docs 생성 포함.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Controller as AdminPaymentController
    participant Service as AdminPaymentService
    participant RepoP as TosspaymentsPaymentRepository
    participant RepoR as TosspaymentsPaymentResultRepository
    participant Facade as PaymentFacadeService
    participant External as TosspaymentsClient

    Client->>Controller: GET /api/v1/admin/payments?memberId=&state=&startDate=&endDate=&page...
    Controller->>Service: findPayments(memberId,state,startDate,endDate,pageable)
    Service->>RepoP: findPaymentsWithFilters(...)
    RepoP-->>Service: Page<TosspaymentsPayment>
    Service->>RepoR: findByTosspaymentsPaymentIdIn(paymentIds)
    RepoR-->>Service: List<TosspaymentsPaymentResult>
    Service-->>Controller: AdminPaymentPageResponse
    Controller-->>Client: 200 + JSON

    Client->>Controller: POST /api/v1/admin/payments/{paymentId}/cancel {cancelReason}
    Controller->>Service: cancelPayment(paymentId, request)
    Service->>RepoP: findById(paymentId)
    RepoP-->>Service: TosspaymentsPayment
    Service->>Facade: cancelPayment(cancelRequest)
    Facade->>External: cancelPayment(paymentKey, ...)
    External-->>Facade: TosspaymentsPaymentResponse
    Facade-->>Service: (void)
    Service-->>Controller: (void)
    Controller-->>Client: 200
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • [REFACTOR] 코드 통합 #332: 관련: TossPayments 도메인/서비스/리포지토리 변경을 포함하는 PR로, 본 PR의 리포지토리/DTO/페이사드 연동과 강한 코드 레벨 연관성이 있음.

Suggested reviewers

  • nak-honest
  • kargowild

Poem

🐰
관리자 창에 결제들이 줄지어,
필터로 골라보고 페이지로 넘기네.
취소 요청도 깔끔히 전송하고,
테스트로 안심하며 귀갓길 재촉하네.
당근 한 입 축하해요! 🥕✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Linked Issues check ❓ Inconclusive The linked issue #349 contains only placeholder TODO lists with no concrete coding requirements, making it impossible to validate whether the implementation meets specific objectives. The linked issue #349 requires detailed acceptance criteria and specific implementation requirements to properly validate code changes.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title '[FEAT] 어드민 기능 추가' accurately describes the main change: adding admin payment management functionality including list retrieval and cancellation endpoints.
Description check ✅ Passed The description mentions closing issue #349 and contains placeholders ('작업 내용', '스크린샷', '참고 사항') but this relates to the linked issue's objectives.
Out of Scope Changes check ✅ Passed All code changes (admin payment controller, service, DTOs, repository methods, and tests) appear related to admin payment functionality without obvious out-of-scope additions.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#349

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@unifolio0 unifolio0 self-assigned this Apr 6, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces administrative functionality for managing payments, including endpoints for listing all payments, filtering by member ID, and canceling specific payments. It includes new controller and service layers, along with necessary DTOs and repository extensions for filtered pagination. Feedback focuses on optimizing API response payloads by excluding large metadata fields from lists, improving query performance by replacing static JPQL with dynamic query tools like Querydsl, and reducing redundant database lookups during the payment cancellation process.

String orderId,
String orderName,
Long totalAmount,
String metadata,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

metadata 필드는 대용량 JSON 데이터를 포함할 수 있어, 목록 조회 API 응답에 매번 포함될 경우 네트워크 페이로드와 메모리 사용량에 부담을 줄 수 있습니다. 목록 조회 시에는 이 필드를 제외하고, 상세 정보가 필요한 경우에만 제공하는 방식을 권장합니다.

Comment on lines +31 to +37
@Query("""
SELECT p FROM TosspaymentsPayment p
WHERE (:memberId IS NULL OR p.memberId = :memberId)
AND (:state IS NULL OR p.state = :state)
AND (:startDate IS NULL OR p.createdAt >= :startDate)
AND (:endDate IS NULL OR p.createdAt <= :endDate)
""")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

(:param IS NULL OR p.field = :param) 형태의 JPQL 쿼리는 데이터 양이 많아질 경우 인덱스를 효율적으로 활용하지 못해 성능 저하를 유발할 수 있습니다. 또한, 필터링과 정렬에 사용되는 state 및 createdAt 필드에 인덱스가 없다면 성능 문제가 더욱 심각해질 수 있습니다. Querydsl 등을 활용한 동적 쿼리 최적화와 해당 필드들에 대한 인덱스 추가를 검토해 주세요.


public void cancelPayment(Long paymentId, AdminCancelPaymentRequest request) {
TosspaymentsPayment payment = readPaymentById(paymentId);
paymentFacadeService.cancelPayment(request.toCancelRequest(payment.getPaymentKey()));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

AdminPaymentService에서 이미 엔티티를 조회했음에도 불구하고, 호출되는 paymentFacadeService.cancelPayment 내부에서 동일한 엔티티를 다시 조회하는 중복이 발생합니다. facade 메서드가 엔티티를 직접 인자로 받도록 개선하여 불필요한 데이터베이스 접근을 줄이는 것이 좋습니다.

@github-actions

github-actions Bot commented Apr 6, 2026

Copy link
Copy Markdown

Test Results

 50 files   50 suites   1m 45s ⏱️
279 tests 278 ✅ 1 💤 0 ❌
281 runs  280 ✅ 1 💤 0 ❌

Results for commit 9697f04.

♻️ This comment has been updated with latest results.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/main/java/com/samhap/kokomen/admin/controller/AdminPaymentController.java`:
- Around line 24-63: Add explicit ADMIN authorization to the admin endpoints:
annotate AdminPaymentController (or each method findPayments,
findPaymentsByMemberId, cancelPayment) with a method-security annotation like
`@PreAuthorize`("hasRole('ADMIN')") and ensure Spring Security is present in
build.gradle; enable method security in your security configuration (e.g., add
`@EnableGlobalMethodSecurity`(prePostEnabled = true) on SecurityConfig) and/or
configure the HttpSecurity to restrict the path /api/v1/admin/** to users with
role ADMIN. Also ensure authentication principal is available where needed by
accepting an Authentication or `@CurrentUser` parameter in service calls if
authorization decisions require user context.

In `@src/main/java/com/samhap/kokomen/admin/service/AdminPaymentService.java`:
- Around line 32-57: The service methods findPayments and findPaymentsByMemberId
currently allow inverted date ranges; add a validation at the start of each (or
extract a private validateDateRange(LocalDateTime start, LocalDateTime end) and
call it) that checks if startDate and endDate are both non-null and
startDate.isAfter(endDate), and if so throw a BadRequestException with a clear
message (e.g., "startDate must be before or equal to endDate"); this enforces
service-layer business validation before calling
tosspaymentsPaymentRepository.findPaymentsWithFilters and returning
toPageResponse.

In
`@src/test/java/com/samhap/kokomen/admin/controller/AdminPaymentControllerTest.java`:
- Around line 89-117: Duplicate responseFields definitions in
AdminPaymentControllerTest cause maintenance pain; extract the repeated
FieldDescriptor array into a single private static final FieldDescriptor[]
(e.g., PAYMENT_PAGE_RESPONSE_FIELDS) defined in the AdminPaymentControllerTest
class and replace both inline responseFields(...) usages in the 전체_결제목록_조회_API
and 유저별_결제목록_조회_API tests with responseFields(PAYMENT_PAGE_RESPONSE_FIELDS) so
both tests reference the shared constant.
- Around line 220-224: The test in AdminPaymentControllerTest only asserts
status.isOk(); extend it to verify the cancellation effect by (1) asserting the
response body contains cancellation info using mockMvc.andExpect(jsonPath(...))
for the expected fields (e.g., "status":"CANCELED" or cancellationTimestamp) and
(2) reloading the Payment from the DB via the PaymentRepository (or the
repository used in the test) using payment.getId() and asserting the entity's
status == CANCELED; update the test method that calls mockMvc.perform(post(...))
to include these additional assertions and any necessary repository
autowiring/cleanup.

In `@src/test/java/com/samhap/kokomen/admin/service/AdminPaymentServiceTest.java`:
- Around line 212-254: The helper method createCancelResponse duplicates test
fixtures present in AdminPaymentControllerTest; extract this into a shared
fixture builder or utility: create a TosspaymentsPaymentResponseFixtureBuilder
(or a common test util class) that can build TosspaymentsPaymentResponse
instances for cancellations, move the logic currently in createCancelResponse
into that builder (exposing a method like
buildCancelledResponse(TosspaymentsPayment) or withPayment(...)), and replace
usages of createCancelResponse in AdminPaymentServiceTest and
AdminPaymentControllerTest to use the new fixture builder to eliminate
duplication.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d425e573-93a7-4017-8bd7-520bb3a83767

📥 Commits

Reviewing files that changed from the base of the PR and between bafead1 and 3eb4652.

📒 Files selected for processing (10)
  • src/main/java/com/samhap/kokomen/admin/controller/AdminPaymentController.java
  • src/main/java/com/samhap/kokomen/admin/service/AdminPaymentService.java
  • src/main/java/com/samhap/kokomen/admin/service/dto/AdminCancelPaymentRequest.java
  • src/main/java/com/samhap/kokomen/admin/service/dto/AdminPaymentPageResponse.java
  • src/main/java/com/samhap/kokomen/admin/service/dto/AdminPaymentResponse.java
  • src/main/java/com/samhap/kokomen/admin/service/dto/AdminPaymentResultResponse.java
  • src/main/java/com/samhap/kokomen/payment/repository/TosspaymentsPaymentRepository.java
  • src/main/java/com/samhap/kokomen/payment/repository/TosspaymentsPaymentResultRepository.java
  • src/test/java/com/samhap/kokomen/admin/controller/AdminPaymentControllerTest.java
  • src/test/java/com/samhap/kokomen/admin/service/AdminPaymentServiceTest.java

Comment on lines +24 to +63
@RequiredArgsConstructor
@RequestMapping("/api/v1/admin/payments")
@RestController
public class AdminPaymentController {

private final AdminPaymentService adminPaymentService;

@GetMapping
public ResponseEntity<AdminPaymentPageResponse> findPayments(
@RequestParam(required = false) PaymentState state,
@RequestParam(required = false) @DateTimeFormat(iso = ISO.DATE_TIME) LocalDateTime startDate,
@RequestParam(required = false) @DateTimeFormat(iso = ISO.DATE_TIME) LocalDateTime endDate,
@PageableDefault(size = 10, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable
) {
AdminPaymentPageResponse response = adminPaymentService.findPayments(state, startDate, endDate, pageable);
return ResponseEntity.ok(response);
}

@GetMapping("/members/{memberId}")
public ResponseEntity<AdminPaymentPageResponse> findPaymentsByMemberId(
@PathVariable Long memberId,
@RequestParam(required = false) PaymentState state,
@RequestParam(required = false) @DateTimeFormat(iso = ISO.DATE_TIME) LocalDateTime startDate,
@RequestParam(required = false) @DateTimeFormat(iso = ISO.DATE_TIME) LocalDateTime endDate,
@PageableDefault(size = 10, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable
) {
AdminPaymentPageResponse response = adminPaymentService.findPaymentsByMemberId(
memberId, state, startDate, endDate, pageable
);
return ResponseEntity.ok(response);
}

@PostMapping("/{paymentId}/cancel")
public ResponseEntity<Void> cancelPayment(
@PathVariable Long paymentId,
@RequestBody @Valid AdminCancelPaymentRequest request
) {
adminPaymentService.cancelPayment(paymentId, request);
return ResponseEntity.ok().build();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) 보안 설정 클래스 후보 확인
fd -i 'security.*\.java' src/main/java

# 2) 관리자 경로 인가 규칙 확인
rg -n --type=java -C3 '/api/v1/admin|authorizeHttpRequests|requestMatchers|antMatchers|hasRole|hasAuthority' src/main/java

# 3) 컨트롤러 레벨 권한 애너테이션 확인
rg -n --type=java -C2 '@PreAuthorize|@Secured|@RolesAllowed' src/main/java/com/samhap/kokomen/admin/controller/AdminPaymentController.java

Repository: samhap-soft/kokomen-backend

Length of output: 1487


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Find all security configuration classes
echo "=== Security Config Classes ==="
fd -e java 'Config|Security' src/main/java | grep -i security | head -20

echo ""
echo "=== All Java files in config directory ==="
fd -e java src/main/java/com/samhap/kokomen/config 2>/dev/null || echo "No config directory found"

echo ""
echo "=== WebSecurityConfiguration or similar ==="
find src/main/java -name '*Security*.java' -o -name '*Config*.java' | grep -i security | head -20

Repository: samhap-soft/kokomen-backend

Length of output: 102


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 2) Search for authorization rules targeting /api/v1/admin
echo "=== Authorization rules for admin paths ==="
rg -n --type=java 'api/v1/admin|hasRole.*ADMIN|hasAuthority.*ADMIN' src/main/java | head -30

echo ""
echo "=== SecurityFilterChain or HttpSecurity configuration ==="
rg -n --type=java -B2 -A5 'SecurityFilterChain|HttpSecurity|authorizeRequests|authorizeHttpRequests' src/main/java | head -50

Repository: samhap-soft/kokomen-backend

Length of output: 403


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 3) Check AdminPaymentController for any authorization annotations
echo "=== AdminPaymentController full content (first 100 lines) ==="
head -100 src/main/java/com/samhap/kokomen/admin/controller/AdminPaymentController.java

echo ""
echo "=== AdminController for comparison ==="
head -50 src/main/java/com/samhap/kokomen/admin/controller/AdminController.java

Repository: samhap-soft/kokomen-backend

Length of output: 4618


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) List all Java files in src/main/java to understand structure
echo "=== Directory structure ==="
find src/main/java -type d -name "*config*" -o -type d -name "*security*" | head -20

echo ""
echo "=== All Java files with 'Config' in name ==="
find src/main/java -name '*Config.java' | head -20

echo ""
echo "=== All Java files with 'Security' in name ==="
find src/main/java -name '*Security*.java' | head -20

Repository: samhap-soft/kokomen-backend

Length of output: 703


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 2) Search for class names that indicate security configuration
echo "=== Search for SecurityConfiguration class definition ==="
rg -n --type=java 'class.*Security.*Config|class.*WebSecurity|class.*ConfigurationAdapter|@EnableWebSecurity' src/main/java

echo ""
echo "=== Search for any `@Bean` methods related to security ==="
rg -n --type=java -B2 '@Bean.*Security|SecurityFilterChain|AuthenticationManager|PasswordEncoder' src/main/java | head -50

Repository: samhap-soft/kokomen-backend

Length of output: 128


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 3) Check if Spring Security is in dependencies and look for pom/gradle
echo "=== Check for Spring Security in pom.xml ==="
cat pom.xml 2>/dev/null | grep -i security || echo "pom.xml not found or no security dependency"

echo ""
echo "=== Check gradle files ==="
find . -name "build.gradle*" -exec grep -l security {} \; 2>/dev/null || echo "No gradle files with security"

echo ""
echo "=== Look for application.properties or application.yml ==="
find src/main/resources -name "application*" -exec echo "--- {} ---" \; -exec cat {} \; 2>/dev/null | head -100

Repository: samhap-soft/kokomen-backend

Length of output: 2298


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 4) Check entire src/main directory structure
echo "=== Main source directories ==="
ls -la src/main/java/com/samhap/kokomen/

echo ""
echo "=== All Java files in entire codebase ==="
find src/main/java -name '*.java' -type f | wc -l

echo ""
echo "=== Look for any interceptor or filter implementations ==="
rg -n --type=java 'implements.*Filter|implements.*Interceptor|extends.*Filter|extends.*Interceptor' src/main/java | head -20

Repository: samhap-soft/kokomen-backend

Length of output: 1328


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== WebConfig.java content ==="
cat src/main/java/com/samhap/kokomen/global/config/WebConfig.java

echo ""
echo "=== Check pom.xml for Spring Security dependency ==="
cat pom.xml | grep -A5 -B5 'security\|spring-boot-starter' || echo "pom.xml content not found"

Repository: samhap-soft/kokomen-backend

Length of output: 1638


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Auth module structure ==="
find src/main/java/com/samhap/kokomen/auth -type f -name '*.java'

echo ""
echo "=== Check for JWT or token-based authentication ==="
rg -n --type=java 'jwt|token|authenticate|authorization|principal' src/main/java/com/samhap/kokomen/auth | head -30

Repository: samhap-soft/kokomen-backend

Length of output: 2600


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== MemberAuthArgumentResolver.java ==="
cat src/main/java/com/samhap/kokomen/global/infrastructure/MemberAuthArgumentResolver.java

echo ""
echo "=== Search for build configuration files ==="
find . -maxdepth 2 -name "build.gradle*" -o -name "pom.xml" -o -name "settings.gradle"

echo ""
echo "=== Check if build.gradle exists ==="
ls -la | grep -E 'build|gradle|pom'

Repository: samhap-soft/kokomen-backend

Length of output: 3236


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check for any Interceptor implementations ==="
find src/main/java -name '*Interceptor*.java' -exec cat {} \;

echo ""
echo "=== Search for `@MemberAuth` or custom security annotations ==="
rg -n --type=java '@MemberAuth|@AdminAuth|@RequireAdmin|@RequireRole' src/main/java | head -20

Repository: samhap-soft/kokomen-backend

Length of output: 183


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check AdminPaymentService for authorization logic ==="
cat src/main/java/com/samhap/kokomen/admin/service/AdminPaymentService.java 2>/dev/null | head -50 || echo "File not found or error"

echo ""
echo "=== Check for any validation in admin methods ==="
rg -n --type=java -B3 -A3 'admin|ADMIN|Admin' src/main/java/com/samhap/kokomen/admin/service/AdminPaymentService.java 2>/dev/null | head -40 || echo "Not found"

Repository: samhap-soft/kokomen-backend

Length of output: 3785


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== build.gradle content ==="
cat build.gradle

echo ""
echo "=== Search for spring-security in gradle ==="
grep -i "security" build.gradle || echo "No security dependency found"

Repository: samhap-soft/kokomen-backend

Length of output: 2862


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check if AdminPaymentController methods have `@Authentication` ==="
rg -n --type=java -B2 -A10 '@GetMapping|@PostMapping' src/main/java/com/samhap/kokomen/admin/controller/AdminPaymentController.java

echo ""
echo "=== Check global exception handling for admin authorization ==="
rg -n --type=java 'admin|Admin|ADMIN' src/main/java/com/samhap/kokomen/global --type=java | grep -i 'exception\|interceptor\|filter' | head -20

Repository: samhap-soft/kokomen-backend

Length of output: 2048


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== List all files in global directory ==="
find src/main/java/com/samhap/kokomen/global -type f -name '*.java' | sort

echo ""
echo "=== Check annotation definitions ==="
cat src/main/java/com/samhap/kokomen/global/annotation/Authentication.java 2>/dev/null || echo "Not found"

Repository: samhap-soft/kokomen-backend

Length of output: 3509


🏁 Script executed:

#!/bin/bash
set -euo pipefixpipe

echo "=== Check if any other admin controllers have authorization ==="
find src/main/java/com/samhap/kokomen -name '*AdminController.java' -exec echo "--- {} ---" \; -exec grep -n '@Authentication\|@PreAuthorize\|@Secured' {} \;

echo ""
echo "=== Check MemberAuth class to see if it has role information ==="
cat src/main/java/com/samhap/kokomen/global/dto/MemberAuth.java

Repository: samhap-soft/kokomen-backend

Length of output: 127


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check if any other admin controllers have authorization ==="
find src/main/java/com/samhap/kokomen -name '*AdminController.java' -exec echo "--- {} ---" \; -exec grep -n '@Authentication\|@PreAuthorize\|@Secured' {} \;

echo ""
echo "=== Check MemberAuth class to see if it has role information ==="
cat src/main/java/com/samhap/kokomen/global/dto/MemberAuth.java

Repository: samhap-soft/kokomen-backend

Length of output: 622


어드민 엔드포인트에 ADMIN 권한 검증이 완전히 누락되어 있습니다.

AdminPaymentController의 모든 메서드에서 권한 제어 메커니즘이 없습니다:

  • Spring Security 의존성이 없음 (build.gradle 확인)
  • @PreAuthorize, @Secured, @RolesAllowed 등 인가 애너테이션 부재
  • 메서드 호출 시 @Authentication 파라미터 없음 (인증도 강제되지 않음)
  • MemberAuth는 memberId만 보유하며 역할 정보 없음
  • 전역 보안 필터나 인터셉터로 /api/v1/admin/** 보호 안 됨

현재 상태에서는 /api/v1/admin/payments 경로가 인증되지 않은 사용자 또는 일반 사용자도 접근할 수 있습니다. 민감한 관리자 기능(결제 조회, 취소)을 보호하기 위해 ADMIN 권한 검증이 필수입니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/samhap/kokomen/admin/controller/AdminPaymentController.java`
around lines 24 - 63, Add explicit ADMIN authorization to the admin endpoints:
annotate AdminPaymentController (or each method findPayments,
findPaymentsByMemberId, cancelPayment) with a method-security annotation like
`@PreAuthorize`("hasRole('ADMIN')") and ensure Spring Security is present in
build.gradle; enable method security in your security configuration (e.g., add
`@EnableGlobalMethodSecurity`(prePostEnabled = true) on SecurityConfig) and/or
configure the HttpSecurity to restrict the path /api/v1/admin/** to users with
role ADMIN. Also ensure authentication principal is available where needed by
accepting an Authentication or `@CurrentUser` parameter in service calls if
authorization decisions require user context.

Comment on lines +32 to +57
@Transactional(readOnly = true)
public AdminPaymentPageResponse findPayments(
PaymentState state,
LocalDateTime startDate,
LocalDateTime endDate,
Pageable pageable
) {
Page<TosspaymentsPayment> page = tosspaymentsPaymentRepository.findPaymentsWithFilters(
null, state, startDate, endDate, pageable
);
return toPageResponse(page);
}

@Transactional(readOnly = true)
public AdminPaymentPageResponse findPaymentsByMemberId(
Long memberId,
PaymentState state,
LocalDateTime startDate,
LocalDateTime endDate,
Pageable pageable
) {
Page<TosspaymentsPayment> page = tosspaymentsPaymentRepository.findPaymentsWithFilters(
memberId, state, startDate, endDate, pageable
);
return toPageResponse(page);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

날짜 범위 역전 입력에 대한 서비스 레벨 검증이 필요합니다.

startDateendDate보다 이후인 경우를 현재 허용하고 있어, 잘못된 요청이 정상 응답(빈 목록)으로 처리됩니다. 서비스에서 즉시 BadRequestException으로 차단해 주세요.

🔧 제안 수정안
 import com.samhap.kokomen.admin.service.dto.AdminCancelPaymentRequest;
 import com.samhap.kokomen.admin.service.dto.AdminPaymentPageResponse;
 import com.samhap.kokomen.admin.service.dto.AdminPaymentResponse;
+import com.samhap.kokomen.global.exception.BadRequestException;
 import com.samhap.kokomen.global.exception.NotFoundException;
@@
     public AdminPaymentPageResponse findPayments(
             PaymentState state,
             LocalDateTime startDate,
             LocalDateTime endDate,
             Pageable pageable
     ) {
+        validateDateRange(startDate, endDate);
         Page<TosspaymentsPayment> page = tosspaymentsPaymentRepository.findPaymentsWithFilters(
                 null, state, startDate, endDate, pageable
         );
         return toPageResponse(page);
     }
@@
     public AdminPaymentPageResponse findPaymentsByMemberId(
             Long memberId,
             PaymentState state,
             LocalDateTime startDate,
             LocalDateTime endDate,
             Pageable pageable
     ) {
+        validateDateRange(startDate, endDate);
         Page<TosspaymentsPayment> page = tosspaymentsPaymentRepository.findPaymentsWithFilters(
                 memberId, state, startDate, endDate, pageable
         );
         return toPageResponse(page);
     }
+
+    private void validateDateRange(LocalDateTime startDate, LocalDateTime endDate) {
+        if (startDate != null && endDate != null && startDate.isAfter(endDate)) {
+            throw new BadRequestException("startDate는 endDate보다 이후일 수 없습니다.");
+        }
+    }

As per coding guidelines "Use @Valid annotation in DTOs for validation, entity-level validation in constructors, business validation in service layer".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Transactional(readOnly = true)
public AdminPaymentPageResponse findPayments(
PaymentState state,
LocalDateTime startDate,
LocalDateTime endDate,
Pageable pageable
) {
Page<TosspaymentsPayment> page = tosspaymentsPaymentRepository.findPaymentsWithFilters(
null, state, startDate, endDate, pageable
);
return toPageResponse(page);
}
@Transactional(readOnly = true)
public AdminPaymentPageResponse findPaymentsByMemberId(
Long memberId,
PaymentState state,
LocalDateTime startDate,
LocalDateTime endDate,
Pageable pageable
) {
Page<TosspaymentsPayment> page = tosspaymentsPaymentRepository.findPaymentsWithFilters(
memberId, state, startDate, endDate, pageable
);
return toPageResponse(page);
}
`@Transactional`(readOnly = true)
public AdminPaymentPageResponse findPayments(
PaymentState state,
LocalDateTime startDate,
LocalDateTime endDate,
Pageable pageable
) {
validateDateRange(startDate, endDate);
Page<TosspaymentsPayment> page = tosspaymentsPaymentRepository.findPaymentsWithFilters(
null, state, startDate, endDate, pageable
);
return toPageResponse(page);
}
`@Transactional`(readOnly = true)
public AdminPaymentPageResponse findPaymentsByMemberId(
Long memberId,
PaymentState state,
LocalDateTime startDate,
LocalDateTime endDate,
Pageable pageable
) {
validateDateRange(startDate, endDate);
Page<TosspaymentsPayment> page = tosspaymentsPaymentRepository.findPaymentsWithFilters(
memberId, state, startDate, endDate, pageable
);
return toPageResponse(page);
}
private void validateDateRange(LocalDateTime startDate, LocalDateTime endDate) {
if (startDate != null && endDate != null && startDate.isAfter(endDate)) {
throw new BadRequestException("startDate는 endDate보다 이후일 수 없습니다.");
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/samhap/kokomen/admin/service/AdminPaymentService.java`
around lines 32 - 57, The service methods findPayments and
findPaymentsByMemberId currently allow inverted date ranges; add a validation at
the start of each (or extract a private validateDateRange(LocalDateTime start,
LocalDateTime end) and call it) that checks if startDate and endDate are both
non-null and startDate.isAfter(endDate), and if so throw a BadRequestException
with a clear message (e.g., "startDate must be before or equal to endDate");
this enforces service-layer business validation before calling
tosspaymentsPaymentRepository.findPaymentsWithFilters and returning
toPageResponse.

Comment on lines +89 to +117
responseFields(
fieldWithPath("data").type(JsonFieldType.ARRAY).description("결제 목록"),
fieldWithPath("data[].id").type(JsonFieldType.NUMBER).description("결제 ID"),
fieldWithPath("data[].payment_key").type(JsonFieldType.STRING).description("결제 키"),
fieldWithPath("data[].member_id").type(JsonFieldType.NUMBER).description("회원 ID"),
fieldWithPath("data[].order_id").type(JsonFieldType.STRING).description("주문 ID"),
fieldWithPath("data[].order_name").type(JsonFieldType.STRING).description("주문명"),
fieldWithPath("data[].total_amount").type(JsonFieldType.NUMBER).description("결제 금액"),
fieldWithPath("data[].metadata").type(JsonFieldType.STRING).description("메타데이터 (JSON)"),
fieldWithPath("data[].state").type(JsonFieldType.STRING).description("결제 상태"),
fieldWithPath("data[].service_type").type(JsonFieldType.STRING).description("서비스 타입"),
fieldWithPath("data[].created_at").type(JsonFieldType.STRING).description("생성일시"),
fieldWithPath("data[].updated_at").type(JsonFieldType.STRING).description("수정일시"),
fieldWithPath("data[].result").type(JsonFieldType.OBJECT).description("결제 결과 상세").optional(),
fieldWithPath("data[].result.method").type(JsonFieldType.STRING).description("결제 수단").optional(),
fieldWithPath("data[].result.balance_amount").type(JsonFieldType.NUMBER).description("잔액").optional(),
fieldWithPath("data[].result.tosspayments_status").type(JsonFieldType.STRING).description("토스페이먼츠 상태").optional(),
fieldWithPath("data[].result.requested_at").type(JsonFieldType.STRING).description("요청일시").optional(),
fieldWithPath("data[].result.approved_at").type(JsonFieldType.STRING).description("승인일시").optional(),
fieldWithPath("data[].result.cancel_reason").type(JsonFieldType.STRING).description("취소 사유").optional(),
fieldWithPath("data[].result.canceled_at").type(JsonFieldType.STRING).description("취소일시").optional(),
fieldWithPath("data[].result.cancel_status").type(JsonFieldType.STRING).description("취소 상태").optional(),
fieldWithPath("data[].result.receipt_url").type(JsonFieldType.STRING).description("영수증 URL").optional(),
fieldWithPath("data[].result.easy_pay_provider").type(JsonFieldType.STRING).description("간편결제 제공자").optional(),
fieldWithPath("current_page").type(JsonFieldType.NUMBER).description("현재 페이지"),
fieldWithPath("total_count").type(JsonFieldType.NUMBER).description("전체 건수"),
fieldWithPath("total_pages").type(JsonFieldType.NUMBER).description("전체 페이지 수"),
fieldWithPath("has_next").type(JsonFieldType.BOOLEAN).description("다음 페이지 존재 여부")
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

RestDocs 응답 필드 정의가 중복됩니다.

전체_결제목록_조회_API유저별_결제목록_조회_APIresponseFields 정의가 완전히 동일합니다(Lines 89-117, 160-188). 응답 구조 변경 시 두 곳을 수정해야 하므로 유지보수가 어려워집니다.

♻️ 공통 응답 필드를 상수로 추출하는 예시
private static final FieldDescriptor[] PAYMENT_PAGE_RESPONSE_FIELDS = {
    fieldWithPath("data").type(JsonFieldType.ARRAY).description("결제 목록"),
    fieldWithPath("data[].id").type(JsonFieldType.NUMBER).description("결제 ID"),
    // ... 나머지 필드들
    fieldWithPath("has_next").type(JsonFieldType.BOOLEAN).description("다음 페이지 존재 여부")
};

// 테스트에서 사용
.andDo(document("admin-findPayments",
        queryParameters(...),
        responseFields(PAYMENT_PAGE_RESPONSE_FIELDS)
));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/com/samhap/kokomen/admin/controller/AdminPaymentControllerTest.java`
around lines 89 - 117, Duplicate responseFields definitions in
AdminPaymentControllerTest cause maintenance pain; extract the repeated
FieldDescriptor array into a single private static final FieldDescriptor[]
(e.g., PAYMENT_PAGE_RESPONSE_FIELDS) defined in the AdminPaymentControllerTest
class and replace both inline responseFields(...) usages in the 전체_결제목록_조회_API
and 유저별_결제목록_조회_API tests with responseFields(PAYMENT_PAGE_RESPONSE_FIELDS) so
both tests reference the shared constant.

Comment on lines +220 to +224
// when & then
mockMvc.perform(post("/api/v1/admin/payments/{paymentId}/cancel", payment.getId())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

취소 결과에 대한 검증이 부족합니다.

현재 테스트는 status().isOk()만 확인하고 있습니다. 취소 API 호출 후 DB에서 결제 상태가 CANCELED로 변경되었는지, 또는 응답 본문에 취소 정보가 포함되어 있는지 추가 검증을 고려해 주세요.

🧪 상태 변경 검증 추가 예시
         // when & then
         mockMvc.perform(post("/api/v1/admin/payments/{paymentId}/cancel", payment.getId())
                         .contentType(MediaType.APPLICATION_JSON)
                         .content(objectMapper.writeValueAsString(request)))
                 .andExpect(status().isOk())
                 .andDo(document("admin-cancelPayment",
                         ...
                 ));
+
+        // verify state change
+        TosspaymentsPayment canceledPayment = tosspaymentsPaymentRepository.findById(payment.getId()).orElseThrow();
+        assertThat(canceledPayment.getState()).isEqualTo(PaymentState.CANCELED);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/com/samhap/kokomen/admin/controller/AdminPaymentControllerTest.java`
around lines 220 - 224, The test in AdminPaymentControllerTest only asserts
status.isOk(); extend it to verify the cancellation effect by (1) asserting the
response body contains cancellation info using mockMvc.andExpect(jsonPath(...))
for the expected fields (e.g., "status":"CANCELED" or cancellationTimestamp) and
(2) reloading the Payment from the DB via the PaymentRepository (or the
repository used in the test) using payment.getId() and asserting the entity's
status == CANCELED; update the test method that calls mockMvc.perform(post(...))
to include these additional assertions and any necessary repository
autowiring/cleanup.

Comment on lines +212 to +254
private TosspaymentsPaymentResponse createCancelResponse(TosspaymentsPayment payment) {
return new TosspaymentsPaymentResponse(
payment.getPaymentKey(),
PaymentType.NORMAL,
payment.getOrderId(),
payment.getOrderName(),
"mId",
"KRW",
"카드",
payment.getTotalAmount(),
0L,
TosspaymentsStatus.CANCELED,
LocalDateTime.now(),
LocalDateTime.now(),
"transaction_key",
9091L,
909L,
0L,
0L,
true,
null,
null,
null,
null,
"KR",
null,
Collections.singletonList(
new TosspaymentsCancel(
"cancel_transaction_key",
"테스트 환불",
0L,
LocalDateTime.now(),
0L,
"receipt_key",
payment.getTotalAmount(),
0L,
0L,
"DONE",
null
)
)
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

createCancelResponse 헬퍼가 컨트롤러 테스트와 중복됩니다.

AdminPaymentControllerTest.java에도 동일한 헬퍼 메서드가 존재합니다. TosspaymentsPaymentFixtureBuilder와 같은 방식으로 취소 응답용 픽스처 빌더(TosspaymentsPaymentResponseFixtureBuilder)를 생성하거나, 공통 테스트 유틸리티 클래스로 추출하는 것을 권장합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/com/samhap/kokomen/admin/service/AdminPaymentServiceTest.java`
around lines 212 - 254, The helper method createCancelResponse duplicates test
fixtures present in AdminPaymentControllerTest; extract this into a shared
fixture builder or utility: create a TosspaymentsPaymentResponseFixtureBuilder
(or a common test util class) that can build TosspaymentsPaymentResponse
instances for cancellations, move the logic currently in createCancelResponse
into that builder (exposing a method like
buildCancelledResponse(TosspaymentsPayment) or withPayment(...)), and replace
usages of createCancelResponse in AdminPaymentServiceTest and
AdminPaymentControllerTest to use the new fixture builder to eliminate
duplication.

@unifolio0 unifolio0 merged commit 3b14cc3 into develop Apr 7, 2026
3 of 4 checks passed
@unifolio0 unifolio0 deleted the feat/#349 branch April 7, 2026 14:51
unifolio0 added a commit that referenced this pull request Jun 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] 어드민 기능 추가

2 participants