Conversation
- 잔여 티켓 조회 기능 구현 - 티켓 소모 기능 구현 - 티켓 일마다 자동충전 기능 구현 - 기존 AI 요청에 티켓 추가
- Ticker -> Ticket
- 발급일이 보이는 오류를 갱신일로 수정
- 통합 테스트 - 서비스 테스트 - 컨트롤러 테스트
- 통합 테스트 - 서비스 테스트 - 컨트롤러 테스트
- 기존 로직 변경에 따른 테스트 변경
- 기존 로직 변경에 따른 테스트 변경
|
""" WalkthroughAI 이미지 생성 권한(퍼미션) 기반 결제 및 관리 기능이 삭제되고, AI 요청 티켓 기반의 결제 및 관리 시스템이 새롭게 도입되었습니다. 이에 따라 도메인, 서비스, 컨트롤러, DTO, 테스트 등 전반적인 코드가 티켓 중심으로 리팩터링 및 확장되었습니다. 유저 코인 잔액 조회 API도 추가되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant AiImageController
participant AiImageGenerationService
participant AiRequestTicketService
participant UserCoinService
participant PostEntityRepository
User->>AiImageController: POST /api/ai/images (useTicket)
AiImageController->>AiImageGenerationService: processImageGeneration(..., useTicket)
AiImageGenerationService->>PostEntityRepository: findById(postId)
alt useTicket == true
AiImageGenerationService->>AiRequestTicketService: useTicket(userId)
else
AiImageGenerationService->>UserCoinService: consumeCoin(userId, price)
end
AiImageGenerationService-->>AiImageController: 이미지 생성 결과 반환
AiImageController-->>User: 응답 반환
sequenceDiagram
actor User
participant AiRequestTicketController
participant AiRequestTicketService
User->>AiRequestTicketController: GET /api/ai/tickets/me
AiRequestTicketController->>AiRequestTicketService: getTicketInformation(userId)
AiRequestTicketService-->>AiRequestTicketController: 티켓 정보(잔여수, 리셋시각)
AiRequestTicketController-->>User: 응답 반환
Estimated code review effort4 (~90분) Poem
✨ Finishing Touches
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Summary of Changes
Hello @yooooonshine, 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!
This pull request introduces a ticket-based system for AI image generation, replacing the previous permission-based access. Users receive daily tickets for free generations and can use in-app coins for additional generations. The change includes infrastructure for managing tickets, ensuring data consistency through distributed locking, and providing new API endpoints for users to monitor their ticket and coin statuses. The aim is to complete core application functionalities by integrating a more flexible and controlled AI image generation economy.
Highlights
- AI Image Generation Ticket System: Implemented a ticket-based system for AI image generation, providing users with a daily quota of free tickets to generate AI images, replacing the previous permission-based system.
- Flexible Payment Integration: AI image generation requests can now be processed using either a free daily ticket or by consuming user coins, offering more flexibility. The
AiImageGenerationServicehandles this payment logic. - Distributed Locking for Ticket Operations: Integrated Redisson-based distributed locking within the
AiRequestTicketServiceandLockManagerto ensure thread-safe and consistent ticket consumption and renewal. - New API Endpoints for Tickets and Coins: Introduced new API endpoints:
/api/ai/tickets/meallows users to check their remaining AI image generation tickets and their next reset time, and/api/user/coinsenables users to query their current coin balance. - Removal of Legacy Permission System: The old
AiImagePermissionServiceandAiImagePermissionController, along with related error codes and entities, have been removed, streamlining the AI image generation authorization flow to the new ticket system. - Comprehensive Testing: Added unit and integration tests for the new
AiRequestTicketServiceandAiRequestTicketController, including concurrency tests to validate the distributed locking mechanism.
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 in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.
| 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 issue 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 or fill out our survey to provide feedback.
You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.
Footnotes
-
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. ↩
There was a problem hiding this comment.
Code Review
This pull request introduces a comprehensive ticket system for AI image generation and finalizes many core application features. The implementation includes entities, repositories, services, and controllers for ticket management, along with distributed locking for concurrency control. The changes are well-supported by a suite of unit and integration tests, which is excellent.
My review focuses on improving code clarity, correctness, and adherence to best practices. I've pointed out a few areas for refinement, such as removing a redundant method call, correcting a method name to follow Java conventions, and addressing potentially misleading transactional annotations due to self-invocation. I've also noted an unused dependency that can be cleaned up.
Overall, this is a substantial and well-executed feature implementation. Addressing these minor points will further enhance the quality and maintainability of the codebase.
src/main/java/hanium/modic/backend/domain/ai/service/AiRequestTicketService.java
Outdated
Show resolved
Hide resolved
src/main/java/hanium/modic/backend/common/redis/distributedLock/LockManager.java
Outdated
Show resolved
Hide resolved
src/main/java/hanium/modic/backend/domain/user/service/UserService.java
Outdated
Show resolved
Hide resolved
| } | ||
|
|
||
| public boolean isTicketExpired() { | ||
| return LocalDateTime.now().isAfter(this.lastIssuedAt.plusDays(1)); |
There was a problem hiding this comment.
The logic for checking ticket expiration is correct. However, LocalDateTime.now() can be problematic in unit tests as it's hard to control. For better testability, consider injecting a java.time.Clock instance and using LocalDateTime.now(clock) instead. This allows you to provide a fixed clock in your tests to simulate the passage of time.
This is not a critical change, but a good practice for future maintainability and testing.
src/main/java/hanium/modic/backend/domain/ai/enums/AiRequestTicketConstants.java
Outdated
Show resolved
Hide resolved
src/main/java/hanium/modic/backend/domain/ai/service/AiRequestTicketService.java
Outdated
Show resolved
Hide resolved
| // Reflection을 사용하여 lastIssuedAt을 어제로 설정 | ||
| try { | ||
| java.lang.reflect.Field field = expiredTicket.getClass().getDeclaredField("lastIssuedAt"); | ||
| field.setAccessible(true); | ||
| field.set(expiredTicket, LocalDateTime.now().minusDays(2)); | ||
| } catch (Exception e) { | ||
| throw new RuntimeException(e); | ||
| } |
There was a problem hiding this comment.
Using reflection in tests is a valid approach to modify private state, but it can be brittle if the field name changes. An alternative approach for better encapsulation and testability is to add a package-private or protected constructor to AiRequestTicketEntity that allows setting lastIssuedAt.
For example:
// In AiRequestTicketEntity.java
@Builder
private AiRequestTicketEntity(Long userId, LocalDateTime lastIssuedAt) {
this.userId = userId;
this.ticketCount = FREE_TICKET_COUNT_PER_DAY;
this.lastIssuedAt = lastIssuedAt != null ? lastIssuedAt : LocalDateTime.now();
}This is just a suggestion for future consideration to improve test design.
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (8)
src/main/java/hanium/modic/backend/domain/ai/enums/AiRequestTicketConstants.java (1)
3-8: 상수 클래스 구조 개선을 제안합니다.상수 클래스의 기본 구조는 좋지만, 다음과 같은 개선사항을 고려해보세요:
-public class AiRequestTicketConstants { +public final class AiRequestTicketConstants { + + private AiRequestTicketConstants() { + // 인스턴스화 방지 + } public static final int FREE_TICKET_COUNT_PER_DAY = 3; public static final int MINIMUM_TICKET_COUNT = 0; }
final클래스로 만들어 상속을 방지private생성자 추가로 인스턴스화 방지src/main/java/hanium/modic/backend/web/ai/dto/response/GetTicketInformationResponse.java (1)
7-21: 잘 설계된 응답 DTO이지만 nullable 타입을 검토해보세요.Record 패턴과 Swagger 문서화가 잘 적용되었습니다. 다만
ticketCount가Integer타입으로 선언되어 null 값을 허용하는데, 비즈니스 로직상 티켓 수가 null일 가능성이 없다면 primitiveint타입 사용을 고려해보세요.public record GetTicketInformationResponse( @Schema(description = "남은 티켓 수") - Integer ticketCount, + int ticketCount, @Schema(description = "다음 리셋까지 시간") LocalDateTime nextReset ) {src/main/java/hanium/modic/backend/web/ai/controller/AiRequestTicketController.java (1)
30-36: API 문서 응답 코드를 보완해야 합니다.현재 500 에러만 문서화되어 있는데, 인증 관련 에러(401, 403)도 추가하는 것이 좋겠습니다.
다음과 같이 응답 코드를 보완하는 것을 권장합니다:
@Operation( summary = "사용자 티켓 정보 조회", description = "현재 사용자의 AI 요청 티켓 정보를 조회합니다. 잔여 티켓 수와 다음 갱신까지의 시간을 포함합니다.", responses = { + @ApiResponse(responseCode = "200", description = "티켓 정보 조회 성공"), + @ApiResponse(responseCode = "401", description = "인증되지 않은 사용자"), @ApiResponse(responseCode = "500", description = "티켓 처리에 실패했습니다.[U-006]") } )src/main/java/hanium/modic/backend/domain/ai/domain/AiRequestTicketEntity.java (2)
29-30: userId에 인덱스 추가 필요
userId필드는unique = true제약 조건이 있지만, 조회 성능 향상을 위해 명시적으로 인덱스를 추가하는 것이 좋습니다.+@Table(name = "ai_request_tickets", indexes = { + @Index(name = "idx_user_id", columnList = "user_id") +}) @Entity
62-64: 만료 확인 로직 개선 제안현재 구현은 정확하지만, 테스트 가능성을 높이기 위해 시간을 주입받을 수 있도록 개선하면 좋겠습니다.
-public boolean isTicketExpired() { - return LocalDateTime.now().isAfter(this.lastIssuedAt.plusDays(1)); +public boolean isTicketExpired() { + return isTicketExpiredAt(LocalDateTime.now()); +} + +public boolean isTicketExpiredAt(LocalDateTime currentTime) { + return currentTime.isAfter(this.lastIssuedAt.plusDays(1)); }src/test/java/hanium/modic/backend/web/ai/controller/AiRequestTicketControllerIntegrationTest.java (1)
88-131: 리플렉션 사용에 대한 개선 제안리플렉션을 사용한 테스트는 유지보수가 어렵습니다. 테스트 전용 메서드나 TestFixture를 사용하는 것을 고려해보세요.
// 테스트 헬퍼 클래스 예시 public class AiRequestTicketTestHelper { public static AiRequestTicketEntity createExpiredTicket(Long userId) { AiRequestTicketEntity ticket = AiRequestTicketEntity.builder() .userId(userId) .build(); // 패키지 레벨 메서드나 테스트 전용 setter 사용 ticket.setLastIssuedAtForTesting(LocalDateTime.now().minusDays(2)); return ticket; } }src/main/java/hanium/modic/backend/domain/ai/service/AiRequestTicketService.java (1)
62-77: 불필요한 저장 작업 최적화티켓이 만료되지 않은 경우에도
save를 호출하고 있습니다. 변경사항이 있을 때만 저장하도록 개선하면 좋겠습니다.public AiRequestTicketEntity refreshTicketIfExpired(AiRequestTicketEntity userTicket) { try { lockManager.AiRequestTicketLock(userTicket.getUserId(), () -> { if (userTicket.isTicketExpired()) { userTicket.resetTickets(); + aiRequestTicketRepository.save(userTicket); } - aiRequestTicketRepository.save(userTicket); }); } catch (LockException e) { throw new AppException(AI_REQUEST_TICKET_PROCESSING_FAIL_EXCEPTION); } return userTicket; }src/test/java/hanium/modic/backend/domain/ai/service/AiRequestTicketServiceTest.java (1)
273-286: ExecutorService 종료 처리 개선 제안현재 구현도 좋지만, 로깅을 추가하면 디버깅에 도움이 됩니다.
private void shutdownExecutor(ExecutorService executor) { executor.shutdown(); try { if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { executor.shutdownNow(); if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { - System.err.println("Executor did not terminate"); + throw new IllegalStateException("Executor did not terminate within timeout"); } } } catch (InterruptedException e) { executor.shutdownNow(); Thread.currentThread().interrupt(); + throw new IllegalStateException("Thread interrupted while waiting for executor termination", e); } }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (22)
src/main/java/hanium/modic/backend/common/error/ErrorCode.java(1 hunks)src/main/java/hanium/modic/backend/common/redis/distributedLock/LockManager.java(2 hunks)src/main/java/hanium/modic/backend/domain/ai/domain/AiRequestTicketEntity.java(1 hunks)src/main/java/hanium/modic/backend/domain/ai/enums/AiRequestTicketConstants.java(1 hunks)src/main/java/hanium/modic/backend/domain/ai/repository/AiRequestTicketRepository.java(1 hunks)src/main/java/hanium/modic/backend/domain/ai/service/AiImageGenerationService.java(4 hunks)src/main/java/hanium/modic/backend/domain/ai/service/AiImagePermissionService.java(0 hunks)src/main/java/hanium/modic/backend/domain/ai/service/AiRequestTicketService.java(1 hunks)src/main/java/hanium/modic/backend/domain/user/service/UserCoinService.java(2 hunks)src/main/java/hanium/modic/backend/domain/user/service/UserService.java(2 hunks)src/main/java/hanium/modic/backend/web/ai/controller/AiImageController.java(1 hunks)src/main/java/hanium/modic/backend/web/ai/controller/AiImagePermissionController.java(0 hunks)src/main/java/hanium/modic/backend/web/ai/controller/AiRequestTicketController.java(1 hunks)src/main/java/hanium/modic/backend/web/ai/dto/request/AiImageGenerationRequest.java(1 hunks)src/main/java/hanium/modic/backend/web/ai/dto/response/GetTicketInformationResponse.java(1 hunks)src/main/java/hanium/modic/backend/web/user/controller/UserController.java(2 hunks)src/main/java/hanium/modic/backend/web/user/dto/response/GetCoinBalanceResponse.java(1 hunks)src/test/java/hanium/modic/backend/domain/ai/service/AiImageGenerationServiceTest.java(4 hunks)src/test/java/hanium/modic/backend/domain/ai/service/AiRequestTicketServiceTest.java(1 hunks)src/test/java/hanium/modic/backend/web/ai/controller/AiImageControllerTest.java(1 hunks)src/test/java/hanium/modic/backend/web/ai/controller/AiRequestTicketControllerIntegrationTest.java(1 hunks)src/test/java/hanium/modic/backend/web/ai/controller/AiRequestTicketControllerTest.java(1 hunks)
🧬 Code Graph Analysis (6)
src/main/java/hanium/modic/backend/domain/ai/domain/AiRequestTicketEntity.java (1)
src/main/java/hanium/modic/backend/domain/ai/enums/AiRequestTicketConstants.java (1)
AiRequestTicketConstants(3-8)
src/test/java/hanium/modic/backend/domain/ai/service/AiRequestTicketServiceTest.java (2)
src/main/java/hanium/modic/backend/domain/ai/enums/AiRequestTicketConstants.java (1)
AiRequestTicketConstants(3-8)src/test/java/hanium/modic/backend/base/login/ContextHolderUtil.java (1)
ContextHolderUtil(10-25)
src/test/java/hanium/modic/backend/web/ai/controller/AiRequestTicketControllerIntegrationTest.java (1)
src/test/java/hanium/modic/backend/domain/user/factory/UserFactory.java (1)
UserFactory(9-29)
src/test/java/hanium/modic/backend/web/ai/controller/AiRequestTicketControllerTest.java (1)
src/test/java/hanium/modic/backend/base/BaseControllerTest.java (1)
BaseControllerTest(13-49)
src/main/java/hanium/modic/backend/domain/ai/service/AiRequestTicketService.java (1)
src/main/java/hanium/modic/backend/common/error/exception/LockException.java (1)
LockException(3-8)
src/test/java/hanium/modic/backend/domain/ai/service/AiImageGenerationServiceTest.java (2)
src/test/java/hanium/modic/backend/domain/post/entityfactory/PostFactory.java (1)
PostFactory(10-36)src/test/java/hanium/modic/backend/domain/user/factory/UserFactory.java (1)
UserFactory(9-29)
💤 Files with no reviewable changes (2)
- src/main/java/hanium/modic/backend/web/ai/controller/AiImagePermissionController.java
- src/main/java/hanium/modic/backend/domain/ai/service/AiImagePermissionService.java
🧰 Additional context used
🧬 Code Graph Analysis (6)
src/main/java/hanium/modic/backend/domain/ai/domain/AiRequestTicketEntity.java (1)
src/main/java/hanium/modic/backend/domain/ai/enums/AiRequestTicketConstants.java (1)
AiRequestTicketConstants(3-8)
src/test/java/hanium/modic/backend/domain/ai/service/AiRequestTicketServiceTest.java (2)
src/main/java/hanium/modic/backend/domain/ai/enums/AiRequestTicketConstants.java (1)
AiRequestTicketConstants(3-8)src/test/java/hanium/modic/backend/base/login/ContextHolderUtil.java (1)
ContextHolderUtil(10-25)
src/test/java/hanium/modic/backend/web/ai/controller/AiRequestTicketControllerIntegrationTest.java (1)
src/test/java/hanium/modic/backend/domain/user/factory/UserFactory.java (1)
UserFactory(9-29)
src/test/java/hanium/modic/backend/web/ai/controller/AiRequestTicketControllerTest.java (1)
src/test/java/hanium/modic/backend/base/BaseControllerTest.java (1)
BaseControllerTest(13-49)
src/main/java/hanium/modic/backend/domain/ai/service/AiRequestTicketService.java (1)
src/main/java/hanium/modic/backend/common/error/exception/LockException.java (1)
LockException(3-8)
src/test/java/hanium/modic/backend/domain/ai/service/AiImageGenerationServiceTest.java (2)
src/test/java/hanium/modic/backend/domain/post/entityfactory/PostFactory.java (1)
PostFactory(10-36)src/test/java/hanium/modic/backend/domain/user/factory/UserFactory.java (1)
UserFactory(9-29)
🔇 Additional comments (24)
src/main/java/hanium/modic/backend/web/user/dto/response/GetCoinBalanceResponse.java (1)
3-6: 깔끔한 DTO 구현입니다.Java record를 사용한 간단하고 명확한 응답 DTO입니다. 단일 필드를 위한 구조로 적절하며, Long 타입도 코인 잔액을 표현하기에 적합합니다.
src/main/java/hanium/modic/backend/domain/user/service/UserCoinService.java (1)
26-31: 잘 구현된 코인 잔액 조회 메서드입니다.기존 클래스의 패턴을 잘 따르고 있으며, 적절한 예외 처리와 응답 DTO 사용이 좋습니다. 메서드명도 명확하고 구현이 간결합니다.
src/main/java/hanium/modic/backend/web/ai/dto/request/AiImageGenerationRequest.java (1)
25-27: 결제 방식 선택 필드가 잘 구현되었습니다.
useTicket필드 추가가 적절합니다:
@NotNull검증으로 필수 값 보장- 명확한 Swagger 문서화
- Boolean 타입으로 티켓/코인 선택을 직관적으로 표현
- 티켓 시스템 도입 목적에 부합하는 구현
src/main/java/hanium/modic/backend/domain/ai/repository/AiRequestTicketRepository.java (1)
9-12: 깔끔한 Repository 인터페이스 구현입니다.Spring Data JPA 컨벤션을 잘 따르고 있으며,
Optional반환 타입으로 null 안전성을 보장하고 있습니다. 사용자별 티켓 조회 메서드도 적절히 정의되었습니다.src/test/java/hanium/modic/backend/web/ai/controller/AiImageControllerTest.java (1)
86-111: 테스트 데이터 업데이트가 일관성 있게 적용되었습니다.새로 추가된
useTicket파라미터가 모든 테스트 케이스에 일관되게 적용되어 있으며, 기존 검증 로직은 그대로 유지되고 있습니다.src/main/java/hanium/modic/backend/web/user/controller/UserController.java (1)
124-133: 코인 잔액 조회 API가 잘 구현되었습니다.기존 패턴을 잘 따르고 있으며, 인증 처리(
@CurrentUser), Swagger 문서화, 응답 구조가 모두 적절합니다. 티켓 시스템과 함께 사용자에게 결제 수단에 대한 가시성을 제공하는 좋은 기능입니다.src/main/java/hanium/modic/backend/web/ai/controller/AiImageController.java (1)
87-94: 티켓 사용 옵션이 깔끔하게 전달되고 있습니다.새로운
useTicket파라미터가 적절히 서비스 레이어로 전달되고 있으며, 컨트롤러의 책임 범위를 벗어나지 않고 깔끔하게 구현되었습니다.src/test/java/hanium/modic/backend/domain/ai/service/AiImageGenerationServiceTest.java (4)
36-42: 새로운 의존성 import가 적절히 추가되었습니다.티켓 시스템 도입에 따른 필요한 의존성들이 올바르게 추가되었습니다.
66-69: 새로운 Mock 객체들이 적절히 선언되었습니다.PostEntityRepository와 AiRequestTicketService에 대한 Mock 객체가 추가되어 새로운 기능을 테스트할 수 있도록 구성되었습니다.
88-96: 테스트 데이터 설정이 개선되었습니다.Factory 패턴을 사용하여 테스트 데이터를 생성하고, 새로운 의존성에 대한 적절한 stubbing이 추가되었습니다.
115-117: 기존 실패 테스트가 올바르게 업데이트되었습니다.새로운 매개변수가 추가되어도 기존의 권한 검증 로직이 여전히 제대로 작동하는지 확인하는 테스트입니다.
src/main/java/hanium/modic/backend/common/error/ErrorCode.java (1)
77-78: 새로운 티켓 시스템 에러 코드가 적절히 추가되었습니다.에러 코드 네이밍과 HTTP 상태 코드가 적절하며, 기존 패턴을 잘 따르고 있습니다.
src/main/java/hanium/modic/backend/common/redis/distributedLock/LockManager.java (2)
33-33: 새로운 락 프리픽스가 적절히 추가되었습니다.티켓 전용 락을 위한 프리픽스가 기존 패턴을 따라 명확하게 정의되었습니다.
124-147: 티켓 락 메서드가 올바르게 구현되었습니다.기존 userLock 메서드와 동일한 패턴을 따르며, 적절한 타임아웃과 리스 시간을 사용하고 있습니다. 예외 처리와 락 해제 로직도 올바르게 구현되어 있습니다.
src/main/java/hanium/modic/backend/domain/ai/service/AiImageGenerationService.java (2)
25-29: 새로운 의존성들이 적절히 추가되었습니다.티켓 시스템과 코인 결제를 위한 필요한 의존성들이 올바르게 주입되었습니다.
Also applies to: 43-43, 50-51
54-61: 메서드 시그니처가 명확하게 확장되었습니다.
useTicket매개변수 추가로 결제 방식을 선택할 수 있도록 개선되었습니다.src/main/java/hanium/modic/backend/web/ai/controller/AiRequestTicketController.java (3)
1-44: 새로운 티켓 컨트롤러가 잘 구현되었습니다.REST API 설계 원칙을 잘 따르고 있으며, Swagger 문서화, 인증 처리, 응답 형식이 모두 적절합니다.
19-24: API 문서화가 상세하고 명확합니다.Swagger 태그와 컨트롤러 매핑이 적절하며, 검증 어노테이션도 올바르게 적용되었습니다.
37-43: 엔드포인트 구현이 간결하고 효율적입니다.인증된 사용자 정보를 올바르게 활용하고, 서비스 레이어와의 결합도가 적절합니다.
src/main/java/hanium/modic/backend/domain/ai/domain/AiRequestTicketEntity.java (1)
38-43: 빌더 구현이 적절합니다빌더 패턴을 통해 티켓 엔티티를 생성하며, 초기 티켓 수와 발급 시간을 적절히 설정하고 있습니다.
src/test/java/hanium/modic/backend/web/ai/controller/AiRequestTicketControllerIntegrationTest.java (1)
133-165: 사용자 격리 테스트가 잘 구현되었습니다다른 사용자의 티켓이 현재 사용자에게 영향을 주지 않는지 확인하는 중요한 테스트입니다. 구현이 명확하고 검증이 충분합니다.
src/main/java/hanium/modic/backend/domain/ai/service/AiRequestTicketService.java (1)
38-60: 재진입 가능한 RLock 사용으로 중첩 락 안전함
Redisson의 RLock은 동일 키에 대해 재진입이 가능하여, 이미 획득한 락을 같은 스레드에서 다시 요청해도 deadlock이 발생하지 않습니다. 따라서useTicket메서드 내에서refreshTicketIfExpired가 락을 재호출하더라도 안전하며, 기존 구현을 유지하셔도 됩니다.
- 확인 위치
- LockManager.AiRequestTicketLock(...)
(src/main/java/hanium/modic/backend/common/redis/distributedLock/LockManager.java)- RLock 사용부:
redissonClient.getLock(key).tryLock(...)Likely an incorrect or invalid review comment.
src/test/java/hanium/modic/backend/domain/ai/service/AiRequestTicketServiceTest.java (1)
190-271: 동시성 테스트가 잘 구현되었습니다동일 사용자와 다른 사용자 간의 동시 티켓 사용을 테스트하는 중요한 시나리오를 다루고 있습니다. ExecutorService의 적절한 종료 처리도 포함되어 있습니다.
src/test/java/hanium/modic/backend/web/ai/controller/AiRequestTicketControllerTest.java (1)
36-96: 컨트롤러 테스트가 잘 구성되었습니다다양한 시나리오(성공, 티켓 없음, 다른 사용자)를 커버하고 있으며, 서비스 계층을 적절히 모킹하여 컨트롤러 로직만 테스트하고 있습니다.
| AI_IMAGE_PERMISSION_NOT_FOUND(HttpStatus.FORBIDDEN, "AI-003", "AI 이미지 생성 권한이 없습니다."), | ||
| AI_IMAGE_PERMISSION_ALREADY_EXISTS(HttpStatus.CONFLICT, "AI-004", "이미 AI 이미지 생성 권한이 존재합니다."), | ||
| AI_IMAGE_PERMISSION_ENTITY_NOT_FOUND(HttpStatus.NOT_FOUND, "AI-005", "해당 AI 이미지 생성 권한을 찾을 수 없습니다."), | ||
| AI_REQUEST_TICKET_PROCESSING_FAIL_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, "U-006", "티켓 처리에 실패했습니다."), |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
에러 코드 중복 가능성을 확인해야 합니다.
AI_REQUEST_TICKET_PROCESSING_FAIL_EXCEPTION의 에러 코드가 "U-006"으로 설정되어 있는데, 이미 USER_COIN_TRANSFER_FAIL_EXCEPTION에서 동일한 코드를 사용하고 있습니다.
에러 코드 중복을 확인하기 위해 다음 스크립트를 실행해보겠습니다:
다음과 같이 수정해야 합니다:
- AI_REQUEST_TICKET_PROCESSING_FAIL_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, "U-006", "티켓 처리에 실패했습니다."),
+ AI_REQUEST_TICKET_PROCESSING_FAIL_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, "AI-007", "티켓 처리에 실패했습니다."),🏁 Script executed:
#!/bin/bash
# Description: ErrorCode enum에서 중복된 코드 값 찾기
# Expected: 중복된 에러 코드가 있다면 출력됨
echo "=== ErrorCode enum에서 중복된 코드 값 검색 ==="
rg -A 1 '"U-006"' src/main/java/hanium/modic/backend/common/error/ErrorCode.java
echo -e "\n=== 모든 에러 코드 값 추출 및 중복 확인 ==="
rg -o '"[A-Z]+-[0-9]+"' src/main/java/hanium/modic/backend/common/error/ErrorCode.java | sort | uniq -dLength of output: 767
에러 코드 “U-006” 중복 확인 및 수정 필요
AI_REQUEST_TICKET_PROCESSING_FAIL_EXCEPTION(src/main/java/hanium/modic/backend/common/error/ErrorCode.java:77)가 USER_COIN_TRANSFER_FAIL_EXCEPTION에서도 동일하게 “U-006”을 사용하고 있습니다. 전체 검사 결과 “U-006” 외에도 “A-001”, “A-002” 코드가 중복되어 있으니, 신규 항목은 고유한 AI 에러 코드(e.g. “AI-007”)로 변경해주세요.
제안된 수정:
- AI_REQUEST_TICKET_PROCESSING_FAIL_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, "U-006", "티켓 처리에 실패했습니다."),
+ AI_REQUEST_TICKET_PROCESSING_FAIL_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, "AI-007", "티켓 처리에 실패했습니다."),📝 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.
| AI_REQUEST_TICKET_PROCESSING_FAIL_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, "U-006", "티켓 처리에 실패했습니다."), | |
| AI_REQUEST_TICKET_PROCESSING_FAIL_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, "AI-007", "티켓 처리에 실패했습니다."), |
🤖 Prompt for AI Agents
In src/main/java/hanium/modic/backend/common/error/ErrorCode.java at line 77,
the error code "U-006" used for AI_REQUEST_TICKET_PROCESSING_FAIL_EXCEPTION is
duplicated in USER_COIN_TRANSFER_FAIL_EXCEPTION and other codes like "A-001" and
"A-002" are also duplicated. To fix this, assign a unique AI-specific error code
such as "AI-007" to this new error constant and ensure all new AI-related error
codes are unique and do not overlap with existing ones.
| } | ||
|
|
||
| // AI 요청 티켓 락 | ||
| public void AiRequestTicketLock( |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
메서드 네이밍 컨벤션을 개선해야 합니다.
메서드명 AiRequestTicketLock이 PascalCase로 되어있는데, Java 메서드 네이밍 컨벤션에 따라 camelCase를 사용해야 합니다.
다음과 같이 수정하는 것을 권장합니다:
- public void AiRequestTicketLock(
+ public void aiRequestTicketLock(해당 메서드를 사용하는 모든 곳에서도 동일하게 수정해야 합니다.
메서드 사용처를 확인하기 위해 다음 스크립트를 실행해보겠습니다:
🏁 Script executed:
#!/bin/bash
# Description: AiRequestTicketLock 메서드 사용처 찾기
# Expected: 메서드를 호출하는 모든 위치 출력
echo "=== AiRequestTicketLock 메서드 사용처 검색 ==="
rg -A 2 "AiRequestTicketLock\("Length of output: 1182
메서드명 camelCase로 수정 필요
Java 네이밍 컨벤션에 맞게 AiRequestTicketLock 메서드를 aiRequestTicketLock으로 변경하고, 호출부 2곳에서도 동일하게 수정하세요.
수정 대상:
- src/main/java/hanium/modic/backend/common/redis/distributedLock/LockManager.java
- src/main/java/hanium/modic/backend/domain/ai/service/AiRequestTicketService.java (2곳)
예시 diff:
--- a/src/main/java/hanium/modic/backend/common/redis/distributedLock/LockManager.java
+++ b/src/main/java/hanium/modic/backend/common/redis/distributedLock/LockManager.java
@@ -121,7 +121,7 @@ public class LockManager {
- public void AiRequestTicketLock(final Long userId, Runnable block) {
+ public void aiRequestTicketLock(final Long userId, Runnable block) {
// ...
}
--- a/src/main/java/hanium/modic/backend/domain/ai/service/AiRequestTicketService.java
+++ b/src/main/java/hanium/modic/backend/domain/ai/service/AiRequestTicketService.java
@@ -45,7 +45,7 @@ public class AiRequestTicketService {
- lockManager.AiRequestTicketLock(userId, () -> {
+ lockManager.aiRequestTicketLock(userId, () -> {
// ...
});
@@ -60,7 +60,7 @@ public class AiRequestTicketService {
- lockManager.AiRequestTicketLock(userTicket.getUserId(), () -> {
+ lockManager.aiRequestTicketLock(userTicket.getUserId(), () -> {
// ...
});🤖 Prompt for AI Agents
In
src/main/java/hanium/modic/backend/common/redis/distributedLock/LockManager.java
at line 124, rename the method AiRequestTicketLock to aiRequestTicketLock to
follow Java camelCase naming conventions. Also, update all calls to this method
in
src/main/java/hanium/modic/backend/domain/ai/service/AiRequestTicketService.java
at the two locations where it is invoked to use the new method name
aiRequestTicketLock.
src/main/java/hanium/modic/backend/domain/ai/domain/AiRequestTicketEntity.java
Show resolved
Hide resolved
| // 포스트 조회 | ||
| PostEntity post = postEntityRepository.findById(postId) | ||
| .orElseThrow(() -> new AppException(POST_NOT_FOUND_EXCEPTION)); | ||
|
|
||
| // 결제 처리 | ||
| if (useTicket) { | ||
| aiRequestTicketService.useTicket(userId); | ||
| } else { | ||
| // Todo : 상업용, 비상업용에 대해 로직 처리 필요 | ||
| userCoinService.consumeCoin(userId, post.getCommercialPrice()); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
결제 로직의 안전성을 개선해야 합니다.
현재 결제 로직에서 몇 가지 개선이 필요합니다:
- 결제 실패 시 롤백 처리가 필요합니다
- 티켓 사용과 코인 사용 모두에서 예외 발생 시 저장된 이미지 요청을 정리해야 합니다
- 상업용/비상업용 가격 로직이 구현되지 않았습니다
다음과 같이 개선하는 것을 권장합니다:
// 이미지 저장 및 ID 반환
AiRequestEntity aiRequestEntity = aiImageService.saveImage(imageUsagePurpose, fileName, imagePath, userId,
postId);
// 포스트 조회
PostEntity post = postEntityRepository.findById(postId)
.orElseThrow(() -> new AppException(POST_NOT_FOUND_EXCEPTION));
- // 결제 처리
- if (useTicket) {
- aiRequestTicketService.useTicket(userId);
- } else {
- // Todo : 상업용, 비상업용에 대해 로직 처리 필요
- userCoinService.consumeCoin(userId, post.getCommercialPrice());
- }
+ // 결제 처리 (트랜잭션 내에서 안전하게 처리)
+ try {
+ if (useTicket) {
+ aiRequestTicketService.useTicket(userId);
+ } else {
+ // 상업용 가격으로 우선 처리 (향후 비상업용 로직 추가 필요)
+ Long price = post.getCommercialPrice();
+ userCoinService.consumeCoin(userId, price);
+ }
+ } catch (Exception e) {
+ // 결제 실패 시 저장된 AI 요청 삭제 또는 상태 변경 필요
+ log.error("결제 처리 실패: userId={}, postId={}, useTicket={}", userId, postId, useTicket, e);
+ throw e;
+ }📝 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.
| // 포스트 조회 | |
| PostEntity post = postEntityRepository.findById(postId) | |
| .orElseThrow(() -> new AppException(POST_NOT_FOUND_EXCEPTION)); | |
| // 결제 처리 | |
| if (useTicket) { | |
| aiRequestTicketService.useTicket(userId); | |
| } else { | |
| // Todo : 상업용, 비상업용에 대해 로직 처리 필요 | |
| userCoinService.consumeCoin(userId, post.getCommercialPrice()); | |
| } | |
| // 이미지 저장 및 ID 반환 | |
| AiRequestEntity aiRequestEntity = aiImageService.saveImage(imageUsagePurpose, fileName, imagePath, userId, | |
| postId); | |
| // 포스트 조회 | |
| PostEntity post = postEntityRepository.findById(postId) | |
| .orElseThrow(() -> new AppException(POST_NOT_FOUND_EXCEPTION)); | |
| // 결제 처리 (트랜잭션 내에서 안전하게 처리) | |
| try { | |
| if (useTicket) { | |
| aiRequestTicketService.useTicket(userId); | |
| } else { | |
| // 상업용 가격으로 우선 처리 (향후 비상업용 로직 추가 필요) | |
| Long price = post.getCommercialPrice(); | |
| userCoinService.consumeCoin(userId, price); | |
| } | |
| } catch (Exception e) { | |
| // 결제 처리 실패 시 저장된 AI 요청 삭제 또는 상태 변경 필요 | |
| log.error("결제 처리 실패: userId={}, postId={}, useTicket={}", userId, postId, useTicket, e); | |
| throw e; | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/hanium/modic/backend/domain/ai/service/AiImageGenerationService.java
around lines 74 to 84, improve the payment logic by adding transaction
management to ensure rollback on failure, implement exception handling to clean
up any saved image requests if ticket or coin usage fails, and complete the
logic to differentiate between commercial and non-commercial pricing. Wrap the
payment steps in a try-catch block or use a transactional annotation to handle
rollback, ensure cleanup code runs on exceptions, and add the missing
commercial/non-commercial price handling before consuming coins.
| if (useTicket) { | ||
| aiRequestTicketService.useTicket(userId); | ||
| } else { | ||
| // Todo : 상업용, 비상업용에 대해 로직 처리 필요 |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
상업용/비상업용 로직 구현이 시급합니다.
TODO 주석으로 남겨진 상업용/비상업용 구분 로직이 현재 서비스에서 중요한 기능인지 확인이 필요합니다.
비상업용 가격 로직의 필요성을 확인하고, 필요하다면 구현을 도와드릴 수 있습니다. 다음 스크립트로 관련 코드를 찾아보겠습니다:
🏁 Script executed:
#!/bin/bash
# Description: 상업용/비상업용 가격 관련 코드 찾기
# Expected: PostEntity의 가격 필드 사용처와 관련 로직 확인
echo "=== PostEntity 가격 필드 사용처 검색 ==="
rg -A 3 -B 3 "(commercialPrice|nonCommercialPrice)"
echo -e "\n=== 상업용/비상업용 관련 주석이나 TODO 검색 ==="
rg -i "(commercial|non.commercial|상업|비상업)" --type javaLength of output: 28665
AiImageGenerationService에 상업용/비상업용 요금 분기 로직 구현 필요
현재 AiImageGenerationService.java(82행)에는 TODO 주석만 남겨져 있고, 실제로는 항상 post.getCommercialPrice()만 소비하고 있습니다. 비상업용 가격을 처리하려면 다음을 반영해주세요:
-
대상 파일 및 위치
- src/main/java/hanium/modic/backend/domain/ai/service/AiImageGenerationService.java
- 라인 82:
// Todo : 상업용, 비상업용에 대해 로직 처리 필요 - 바로 아래:
userCoinService.consumeCoin(userId, post.getCommercialPrice());
- 라인 82:
- src/main/java/hanium/modic/backend/domain/ai/service/AiImageGenerationService.java
-
구현 예시 (diff)
- // Todo : 상업용, 비상업용에 대해 로직 처리 필요 - userCoinService.consumeCoin(userId, post.getCommercialPrice()); + // 상업/비상업 구분에 따른 소비 코인 결정 + long price = (usageType == UsageType.COMMERCIAL) + ? post.getCommercialPrice() + : post.getNonCommercialPrice(); + userCoinService.consumeCoin(userId, price);
-
추가 작업
- Web/Controller 레이어의 요청 DTO(Create/UpdatePostRequest)에
usageType필드 추가 AiImageGenerationService.generate…메서드 시그니처에usageType파라미터 반영- 관련 테스트(PostControllerIntegrationTest 등)에도
usageType기반 로직 검증 추가
- Web/Controller 레이어의 요청 DTO(Create/UpdatePostRequest)에
위 항목을 반영해 비상업용 가격 로직을 완성해주세요.
📝 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.
| // Todo : 상업용, 비상업용에 대해 로직 처리 필요 | |
| // 상업/비상업 구분에 따른 소비 코인 결정 | |
| long price = (usageType == UsageType.COMMERCIAL) | |
| ? post.getCommercialPrice() | |
| : post.getNonCommercialPrice(); | |
| userCoinService.consumeCoin(userId, price); |
🤖 Prompt for AI Agents
In
src/main/java/hanium/modic/backend/domain/ai/service/AiImageGenerationService.java
at line 82, replace the TODO comment and the current unconditional call to
userCoinService.consumeCoin(userId, post.getCommercialPrice()) with logic that
checks the usageType parameter and consumes either the commercial or
non-commercial price accordingly. Also, add a usageType field to the request DTO
in the Web/Controller layer, update the generate method signature in
AiImageGenerationService to accept usageType, and extend related tests to verify
the new usageType-based pricing logic.
src/main/java/hanium/modic/backend/domain/user/service/UserService.java
Outdated
Show resolved
Hide resolved
| RequestAiImageGenerationResponse result = aiImageGenerationService.processImageGeneration( | ||
| imageUsagePurpose, TEST_FILE_NAME, TEST_IMAGE_PATH, TEST_POST_ID, TEST_USER_ID); | ||
| imageUsagePurpose, TEST_FILE_NAME, TEST_IMAGE_PATH, TEST_POST_ID, TEST_USER_ID, true); | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
티켓 사용 시나리오에 대한 테스트 커버리지를 확장해야 합니다.
현재 테스트는 useTicket=true만 테스트하고 있습니다. 코인 결제(useTicket=false) 시나리오와 PostEntity가 존재하지 않는 경우에 대한 테스트도 추가해야 합니다.
다음과 같은 테스트 케이스들을 추가하는 것을 권장합니다:
@Test
@DisplayName("processImageGeneration - 성공: 코인 결제 사용")
void processImageGeneration_Success_WithCoin() {
// given
ImagePrefix imageUsagePurpose = ImagePrefix.AI_REQUEST;
AiRequestEntity mockAiRequest = createTestAiRequestEntity();
List<PostImageEntity> mockPostImages = List.of(createTestPostImageEntity());
UserEntity user = UserFactory.createMockUser(TEST_USER_ID);
PostEntity mockPostEntity = PostFactory.createMockPost(user);
when(aiImagePermissionRepository.existsByUserIdAndPostId(TEST_USER_ID, TEST_POST_ID)).thenReturn(true);
when(postImageEntityRepository.findAllByPostId(TEST_POST_ID)).thenReturn(mockPostImages);
when(aiImageService.saveImage(imageUsagePurpose, TEST_FILE_NAME, TEST_IMAGE_PATH, TEST_USER_ID, TEST_POST_ID))
.thenReturn(mockAiRequest);
when(postEntityRepository.findById(TEST_POST_ID)).thenReturn(Optional.of(mockPostEntity));
doNothing().when(userCoinService).consumeCoin(TEST_USER_ID, mockPostEntity.getCommercialPrice());
// when
RequestAiImageGenerationResponse result = aiImageGenerationService.processImageGeneration(
imageUsagePurpose, TEST_FILE_NAME, TEST_IMAGE_PATH, TEST_POST_ID, TEST_USER_ID, false);
// then
assertEquals(TEST_IMAGE_ID, result.imageId());
verify(userCoinService).consumeCoin(TEST_USER_ID, mockPostEntity.getCommercialPrice());
verify(aiRequestTicketService, never()).useTicket(anyLong());
}
@Test
@DisplayName("processImageGeneration - 실패: 포스트를 찾을 수 없음")
void processImageGeneration_Fail_PostNotFound() {
// given
when(aiImagePermissionRepository.existsByUserIdAndPostId(TEST_USER_ID, TEST_POST_ID)).thenReturn(true);
when(postImageEntityRepository.findAllByPostId(TEST_POST_ID)).thenReturn(List.of(createTestPostImageEntity()));
when(aiImageService.saveImage(any(), any(), any(), any(), any())).thenReturn(createTestAiRequestEntity());
when(postEntityRepository.findById(TEST_POST_ID)).thenReturn(Optional.empty());
// when & then
AppException exception = assertThrows(AppException.class, () ->
aiImageGenerationService.processImageGeneration(
ImagePrefix.AI_REQUEST, TEST_FILE_NAME, TEST_IMAGE_PATH, TEST_POST_ID, TEST_USER_ID, true));
assertEquals(ErrorCode.POST_NOT_FOUND_EXCEPTION, exception.getErrorCode());
}🤖 Prompt for AI Agents
In
src/test/java/hanium/modic/backend/domain/ai/service/AiImageGenerationServiceTest.java
around lines 99 to 101, the current test only covers the scenario where
useTicket is true. You need to add additional test cases to cover the scenario
when useTicket is false (coin payment) and when the PostEntity is not found.
Implement one test method to verify successful image generation with coin
payment, mocking necessary dependencies and verifying coin consumption without
ticket usage. Implement another test method to simulate the failure case when
the post is not found, asserting that the appropriate exception with the correct
error code is thrown.
src/test/java/hanium/modic/backend/domain/ai/service/AiRequestTicketServiceTest.java
Show resolved
Hide resolved
| @Test | ||
| @WithCustomUser(email = "test@test.com") | ||
| @DisplayName("통합 테스트 - 티켓 정보 조회 성공") | ||
| void getUserTicketInformation_IntegrationTest_Success() throws Exception { | ||
| // when & then | ||
| ResultActions resultActions = mockMvc.perform(get("/api/ai/tickets/me")); | ||
|
|
||
| // 응답 형식 검증 | ||
| resultActions | ||
| .andExpect(jsonPath("$.data.ticketCount").exists()) | ||
| .andExpect(jsonPath("$.data.nextReset").exists()); | ||
|
|
||
| // 응답 값 검증 | ||
| GetTicketInformationResponse ticketInfo = testUtil.getResponseData( | ||
| resultActions, | ||
| GetTicketInformationResponse.class | ||
| ); | ||
|
|
||
| assertThat(ticketInfo.ticketCount()).isEqualTo(3); | ||
| assertThat(ticketInfo.nextReset()).isAfter(LocalDateTime.now()); | ||
| assertThat(ticketInfo.nextReset()).isBefore(LocalDateTime.now().plusDays(1).plusMinutes(1)); | ||
|
|
||
| // 데이터베이스에 저장되었는지 확인 | ||
| AiRequestTicketEntity savedTicket = aiRequestTicketRepository.findByUserId(1L).orElse(null); | ||
| assertThat(savedTicket).isNotNull(); | ||
| assertThat(savedTicket.getTicketCount()).isEqualTo(3); | ||
| assertThat(savedTicket.getUserId()).isEqualTo(1L); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
하드코딩된 사용자 ID 개선 필요
테스트가 사용자 ID가 1L이라고 가정하고 있습니다. 더 안정적인 테스트를 위해 실제 사용자 ID를 가져오는 것이 좋습니다.
@Test
@WithCustomUser(email = "test@test.com")
@DisplayName("통합 테스트 - 티켓 정보 조회 성공")
void getUserTicketInformation_IntegrationTest_Success() throws Exception {
+ // 현재 인증된 사용자 조회
+ UserEntity currentUser = userRepository.findByEmail("test@test.com")
+ .orElseThrow(() -> new AssertionError("테스트 사용자를 찾을 수 없습니다"));
+ Long userId = currentUser.getId();
+
// when & then
ResultActions resultActions = mockMvc.perform(get("/api/ai/tickets/me"));
// ... 중략 ...
// 데이터베이스에 저장되었는지 확인
- AiRequestTicketEntity savedTicket = aiRequestTicketRepository.findByUserId(1L).orElse(null);
+ AiRequestTicketEntity savedTicket = aiRequestTicketRepository.findByUserId(userId).orElse(null);
assertThat(savedTicket).isNotNull();
assertThat(savedTicket.getTicketCount()).isEqualTo(3);
- assertThat(savedTicket.getUserId()).isEqualTo(1L);
+ assertThat(savedTicket.getUserId()).isEqualTo(userId);
}📝 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.
| @Test | |
| @WithCustomUser(email = "test@test.com") | |
| @DisplayName("통합 테스트 - 티켓 정보 조회 성공") | |
| void getUserTicketInformation_IntegrationTest_Success() throws Exception { | |
| // when & then | |
| ResultActions resultActions = mockMvc.perform(get("/api/ai/tickets/me")); | |
| // 응답 형식 검증 | |
| resultActions | |
| .andExpect(jsonPath("$.data.ticketCount").exists()) | |
| .andExpect(jsonPath("$.data.nextReset").exists()); | |
| // 응답 값 검증 | |
| GetTicketInformationResponse ticketInfo = testUtil.getResponseData( | |
| resultActions, | |
| GetTicketInformationResponse.class | |
| ); | |
| assertThat(ticketInfo.ticketCount()).isEqualTo(3); | |
| assertThat(ticketInfo.nextReset()).isAfter(LocalDateTime.now()); | |
| assertThat(ticketInfo.nextReset()).isBefore(LocalDateTime.now().plusDays(1).plusMinutes(1)); | |
| // 데이터베이스에 저장되었는지 확인 | |
| AiRequestTicketEntity savedTicket = aiRequestTicketRepository.findByUserId(1L).orElse(null); | |
| assertThat(savedTicket).isNotNull(); | |
| assertThat(savedTicket.getTicketCount()).isEqualTo(3); | |
| assertThat(savedTicket.getUserId()).isEqualTo(1L); | |
| } | |
| @Test | |
| @WithCustomUser(email = "test@test.com") | |
| @DisplayName("통합 테스트 - 티켓 정보 조회 성공") | |
| void getUserTicketInformation_IntegrationTest_Success() throws Exception { | |
| + // 현재 인증된 사용자 조회 | |
| + UserEntity currentUser = userRepository.findByEmail("test@test.com") | |
| + .orElseThrow(() -> new AssertionError("테스트 사용자를 찾을 수 없습니다")); | |
| + Long userId = currentUser.getId(); | |
| // when & then | |
| ResultActions resultActions = mockMvc.perform(get("/api/ai/tickets/me")); | |
| // 응답 형식 검증 | |
| resultActions | |
| .andExpect(jsonPath("$.data.ticketCount").exists()) | |
| .andExpect(jsonPath("$.data.nextReset").exists()); | |
| // 응답 값 검증 | |
| GetTicketInformationResponse ticketInfo = testUtil.getResponseData( | |
| resultActions, | |
| GetTicketInformationResponse.class | |
| ); | |
| assertThat(ticketInfo.ticketCount()).isEqualTo(3); | |
| assertThat(ticketInfo.nextReset()).isAfter(LocalDateTime.now()); | |
| assertThat(ticketInfo.nextReset()).isBefore(LocalDateTime.now().plusDays(1).plusMinutes(1)); | |
| // 데이터베이스에 저장되었는지 확인 | |
| - AiRequestTicketEntity savedTicket = aiRequestTicketRepository.findByUserId(1L).orElse(null); | |
| + AiRequestTicketEntity savedTicket = aiRequestTicketRepository.findByUserId(userId).orElse(null); | |
| assertThat(savedTicket).isNotNull(); | |
| assertThat(savedTicket.getTicketCount()).isEqualTo(3); | |
| - assertThat(savedTicket.getUserId()).isEqualTo(1L); | |
| + assertThat(savedTicket.getUserId()).isEqualTo(userId); | |
| } |
🤖 Prompt for AI Agents
In
src/test/java/hanium/modic/backend/web/ai/controller/AiRequestTicketControllerIntegrationTest.java
between lines 35 and 62, the test hardcodes the user ID as 1L when verifying the
saved ticket entity. To improve test reliability, retrieve the actual user ID
dynamically from the authenticated test user context or the test setup instead
of using a fixed value. Replace the hardcoded 1L with this dynamic user ID when
querying the repository and asserting values.
…k/LockManager.java refactor: AiRequestTicketLock 을 camelCase에 맞게 수정 Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
…c-2025/modic_backend into feature/82-implement-ticket
Summary
티켓 시스템 기능을 구현했습니다. AI 이미지 생성과 연동된 티켓 검증 시스템을 통해 사용자의 AI 이미지 생성 횟수를 제한했습니다.
일 무료 3회 가능하게 했으며, 하루가 지나면 무료 생성 횟수를 리셋하도록 했습니다.
Problem
Solution
🎫 티켓 시스템 도입
Key Changes
🎫 티켓 시스템 (핵심 구현)
Test Results
✅ 단위 테스트 (100% 통과)
✅ 통합 테스트 (100% 통과)
📊 핵심 기능 검증 완료
🚀 성능 테스트
Summary by CodeRabbit
신규 기능
변경 사항
삭제
버그 수정
테스트