[VISION] 분석 일일 사용량 DB 관리 및 인증 UX 전면 개선#72
Conversation
Summary by CodeRabbit
Walkthrough이 변경사항은 Vision 기능에 대한 인증 처리, 사용량 추적, 오류 응답, 이미지 검증, 클라이언트-서버 상호작용을 대대적으로 개선합니다. Vision 사용량을 데이터베이스에 저장하고 조회하는 새로운 서비스와 엔티티, 레포지토리가 추가되었고, 프론트엔드 템플릿과 자바스크립트도 토큰 관리와 사용자 피드백 중심으로 리팩터링되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Browser
participant VisionController
participant VisionUsageService
participant VisionUsageRepository
participant DB
User->>Browser: 파일 업로드 및 분석 요청
Browser->>VisionController: /species 또는 /analyze (with token)
VisionController->>VisionUsageService: getTodayUsage(userId)
VisionUsageService->>VisionUsageRepository: findByUserIdAndUsageDate(userId, today)
VisionUsageRepository->>DB: SELECT vision_usage WHERE userId AND usageDate
DB-->>VisionUsageRepository: Usage Record or None
VisionUsageRepository-->>VisionUsageService: Usage Record
VisionUsageService-->>VisionController: Usage 정보 반환
alt 사용 가능
VisionController->>VisionUsageService: incrementUsage(userId)
VisionUsageService->>VisionUsageRepository: 저장/증가
VisionUsageRepository->>DB: UPDATE or INSERT
DB-->>VisionUsageRepository: OK
VisionUsageRepository-->>VisionUsageService: Updated Record
VisionUsageService-->>VisionController: Updated Usage
VisionController-->>Browser: 분석 결과/성공 응답
else 사용 초과
VisionController-->>Browser: 사용량 초과 오류 응답
end
Possibly related PRs
Poem
✨ Finishing Touches
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. 🪧 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 (
|
|
@CodeRabbit review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (6)
src/main/resources/application.yml (1)
21-25: Vision 설정이 적절하게 구성되었습니다.일일 한도 3회와 30일 데이터 보관 정책이 합리적입니다. 이 설정들이 VisionUsage 엔티티의 기본값과 일치하여 일관성이 유지됩니다.
파일 끝에 개행 문자가 누락되었습니다:
+ retention-days: 30 # 30일 이전 데이터 삭제 +🧰 Tools
🪛 YAMLlint (1.37.1)
[error] 25-25: no new line character at the end of file
(new-line-at-end-of-file)
src/main/java/io/github/petty/vision/entity/VisionUsage.java (1)
41-42: updatedAt 필드에 자동 관리 어노테이션 사용을 고려해보세요.수동으로
updatedAt을 설정하는 대신 Hibernate의@UpdateTimestamp어노테이션을 사용하면 더 일관성 있게 관리할 수 있습니다.+import org.hibernate.annotations.UpdateTimestamp; @Column(name = "updated_at") +@UpdateTimestamp private LocalDateTime updatedAt;src/main/java/io/github/petty/vision/helper/ImageValidator.java (1)
98-133: 동물 감지 검증이 사실상 무의미해짐모든 경우에
valid()를 반환하므로 이 메서드의 존재 의의가 없습니다. 비즈니스 요구사항을 재확인하고, 필요하다면 최소한의 검증 로직은 유지하는 것이 좋습니다.메서드를 제거하거나 최소한의 검증을 유지하는 두 가지 옵션:
옵션 1: 메서드 제거
-// 🔥 동물 감지도 선택적으로 (실패해도 무조건 통과) -return validateAnimalContentOptional(bytes); +// 동물 감지 검증을 완전히 제거 +return ValidationResult.valid();옵션 2: 경고 수준으로 유지
private ValidationResult validateAnimalContentOptional(byte[] bytes) { try { // ... 기존 코드 ... if (!animalDetected) { log.warn("⚠️ 동물이 명확히 감지되지 않았지만 분석을 진행합니다."); + // 메타데이터에 경고 추가 (UI에서 사용자에게 알림 가능) + return new ValidationResult(true, "동물이 명확히 감지되지 않았습니다."); }src/main/resources/templates/visionUpload.html (1)
534-565: 중복된 토큰 확인 로직 모듈화 필요페이지 포커스 시와 초기 로드 시 비슷한 토큰 확인 로직이 반복됩니다.
토큰 확인 로직을 통합:
+// 토큰 상태 확인 및 갱신 함수 +async function ensureAuthentication() { + if (isLoggedIn !== 'true') return; + + const status = await checkTokenStatus(); + if (!status.authenticated) { + console.log('🔄 [TOKEN] 인증 상태 없음, 토큰 갱신 시도'); + const refreshResult = await refreshAccessToken(); + if (!refreshResult.success) { + showTokenAlert('세션이 만료되었습니다. 다시 로그인해주세요.', 'danger'); + } + } else { + console.log('✅ [TOKEN] 인증 상태 확인됨'); + } +} + // 페이지 포커스 시 토큰 확인 document.addEventListener('visibilitychange', function() { - if (!document.hidden && isLoggedIn === 'true') { + if (!document.hidden) { console.log('👁️ [TOKEN] 페이지 포커스 감지, 토큰 상태 확인'); - checkTokenStatus(); + ensureAuthentication(); } }); // 페이지 로드 시 초기 토큰 상태 확인 -if (isLoggedIn === 'true') { - checkTokenStatus().then(status => { - if (status.authenticated) { - console.log('✅ [TOKEN] 초기 인증 상태 확인됨'); - } else { - console.log('🔄 [TOKEN] 초기 인증 상태 없음, 토큰 갱신 시도'); - refreshAccessToken(); - } - }); -} +ensureAuthentication();src/main/java/io/github/petty/vision/adapter/in/VisionController.java (2)
152-262: analyze 메서드의 복잡도를 줄이기 위한 리팩토링 제안현재 메서드가 110줄로 너무 길고 복잡합니다. 사용량 체크와 증가 로직을 별도 메서드로 분리하면 가독성과 유지보수성이 향상될 것입니다.
다음과 같이 리팩토링을 고려해보세요:
private boolean checkAndIncrementUsageWithFallback(UUID userId, HttpSession session) { // 사용량 체크 로직 boolean canUse = checkUsageWithFallback(userId, session); if (canUse) { // 사용량 증가 로직 incrementUsageWithFallback(userId, session); } return canUse; }
294-316: AuthenticationResult 클래스에 Lombok 사용 제안프로젝트에서 이미 Lombok을 사용하고 있으므로, 일관성과 간결성을 위해 Lombok 어노테이션 사용을 제안합니다.
- private static class AuthenticationResult { - private final boolean authenticated; - private final UUID userId; - private final String username; - - private AuthenticationResult(boolean authenticated, UUID userId, String username) { - this.authenticated = authenticated; - this.userId = userId; - this.username = username; - } - - public static AuthenticationResult authenticated(UUID userId, String username) { - return new AuthenticationResult(true, userId, username); - } - - public static AuthenticationResult unauthenticated() { - return new AuthenticationResult(false, null, null); - } - - public boolean isAuthenticated() { return authenticated; } - public UUID getUserId() { return userId; } - public String getUsername() { return username; } - } + @Getter + @AllArgsConstructor(access = AccessLevel.PRIVATE) + private static class AuthenticationResult { + private final boolean authenticated; + private final UUID userId; + private final String username; + + public static AuthenticationResult authenticated(UUID userId, String username) { + return new AuthenticationResult(true, userId, username); + } + + public static AuthenticationResult unauthenticated() { + return new AuthenticationResult(false, null, null); + } + }추가로 필요한 import:
import lombok.Getter; import lombok.AllArgsConstructor; import lombok.AccessLevel;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
src/main/java/io/github/petty/config/SupabaseDataSourceConfig.java(2 hunks)src/main/java/io/github/petty/vision/adapter/in/VisionController.java(3 hunks)src/main/java/io/github/petty/vision/entity/VisionUsage.java(1 hunks)src/main/java/io/github/petty/vision/helper/ImageValidator.java(3 hunks)src/main/java/io/github/petty/vision/repository/VisionUsageRepository.java(1 hunks)src/main/java/io/github/petty/vision/service/VisionUsageService.java(1 hunks)src/main/resources/application.yml(1 hunks)src/main/resources/templates/visionUpload.html(6 hunks)
🧰 Additional context used
🪛 YAMLlint (1.37.1)
src/main/resources/application.yml
[error] 25-25: no new line character at the end of file
(new-line-at-end-of-file)
🔇 Additional comments (11)
src/main/java/io/github/petty/config/SupabaseDataSourceConfig.java (1)
42-42: Vision 패키지 통합이 올바르게 구성되었습니다.새로운 Vision 관련 엔티티와 레포지토리를 Supabase 데이터 소스 구성에 올바르게 추가했습니다. 이를 통해 Spring Data JPA가 VisionUsage 엔티티와 VisionUsageRepository를 적절히 감지하고 관리할 수 있습니다.
Also applies to: 78-79
src/main/java/io/github/petty/vision/entity/VisionUsage.java (2)
52-55: 동시성 환경에서의 사용량 증가 처리를 검토해주세요.현재
incrementUsage()메서드는 동시 접근 시 경쟁 상태(race condition)가 발생할 수 있습니다. 여러 요청이 동시에 들어올 때 사용량이 정확히 증가하지 않을 수 있습니다.데이터베이스 레벨에서 원자적 업데이트를 사용하는 것을 고려해보세요:
// Repository에서 원자적 업데이트 쿼리 사용 @Modifying @Query("UPDATE VisionUsage v SET v.usageCount = v.usageCount + 1, v.updatedAt = CURRENT_TIMESTAMP WHERE v.id = :id") void incrementUsageCount(@Param("id") UUID id);
57-65: 비즈니스 로직 메서드들이 잘 구현되었습니다.
canUse()와getRemainingUsage()메서드들이 명확하고 올바르게 구현되었습니다.Math.max(0, ...)사용으로 음수 반환을 방지한 것도 좋습니다.src/main/java/io/github/petty/vision/repository/VisionUsageRepository.java (3)
25-26: CURRENT_DATE 함수 사용에 대한 검토가 필요합니다.JPQL의
CURRENT_DATE함수는 데이터베이스에 따라 시간대 처리가 다를 수 있습니다. 특히 서버와 데이터베이스의 시간대가 다른 경우 문제가 될 수 있습니다.더 명확한 날짜 처리를 위해 다음과 같이 수정하는 것을 고려해보세요:
@Query("SELECT v FROM VisionUsage v WHERE v.userId = :userId AND v.usageDate = :today") Optional<VisionUsage> findTodayUsage(@Param("userId") UUID userId, @Param("today") LocalDate today);
20-20: 레포지토리 메서드들이 잘 설계되었습니다.JPQL 쿼리들이 적절히 작성되었고, 파라미터 바인딩도 올바르게 사용되었습니다. 특히
COALESCE함수로 null 처리를 한 것과ORDER BY절을 사용한 것이 좋습니다.Also applies to: 31-32, 44-45
37-39:Details
❌ Incorrect review comment
@Modifying 쿼리에 @transactional 어노테이션이 필요합니다.
deleteOldUsageRecords메서드는 데이터를 수정하는 쿼리이므로, 이를 호출하는 서비스 메서드에@Transactional어노테이션이 있어야 합니다.VisionUsageService에서 이 메서드를 호출할 때 트랜잭션 처리가 올바르게 되어 있는지 확인해주세요:
🏁 Script executed:
#!/bin/bash # VisionUsageService에서 deleteOldUsageRecords 사용법 확인 ast-grep --pattern $'class VisionUsageService { $$$ $METHOD_NAME($$$) { $$$ deleteOldUsageRecords($$$); $$$ } $$$ }'Length of output: 137
VisionUsageService에서 deleteOldUsageRecords 호출 위치와 @transactional 적용 여부를 확인하기 위해 아래 스크립트를 실행해주세요:
🏁 Script executed:
#!/bin/bash # VisionUsageService 파일 경로 찾기 service_file=$(fd VisionUsageService.java) echo "=== Service 파일 ===" echo "$service_file" echo echo "=== deleteOldUsageRecords 호출부 ===" rg -n "deleteOldUsageRecords" "$service_file" echo echo "=== @Transactional 적용 여부 ===" rg -n "@Transactional" "$service_file"Length of output: 794
@transactional이 이미 적용되어 있습니다
src/main/java/io/github/petty/vision/service/VisionUsageService.java파일 상단(19줄)의@Transactional(transactionManager = "supabaseTransactionManager")어노테이션이 클래스 레벨에 선언되어 있어
deleteOldUsageRecords호출 시 이미 트랜잭션이 관리됩니다.
따라서 별도의@Transactional추가는 불필요합니다.Likely an incorrect or invalid review comment.
src/main/resources/templates/visionUpload.html (1)
175-218:⚠️ Potential issue
authenticatedFetch함수의 무한 재시도 가능성401 응답 시 토큰 갱신 후 재시도하는 로직이 좋지만, 토큰 갱신이 계속 실패할 경우 무한 루프에 빠질 수 있습니다.
재시도 횟수 제한 추가:
-async function authenticatedFetch(url, options = {}) { +async function authenticatedFetch(url, options = {}, retryCount = 0) { try { let response = await fetch(url, { ...options, credentials: 'include' }); - if (response.status === 401) { + if (response.status === 401 && retryCount < 1) { console.log('🔒 [TOKEN] 401 응답, 토큰 갱신 시도'); showTokenAlert('토큰 갱신 중...', 'warning'); const refreshResult = await refreshAccessToken(); if (refreshResult.success) { console.log('✅ [TOKEN] 토큰 갱신 성공, 요청 재시도'); hideTokenAlert(); - response = await fetch(url, { - ...options, - credentials: 'include' - }); + return authenticatedFetch(url, options, retryCount + 1); } else {Likely an incorrect or invalid review comment.
src/main/java/io/github/petty/vision/adapter/in/VisionController.java (4)
48-98: 사용량 조회 로직이 잘 구현되었습니다DB 우선, 세션 fallback 구조가 안정적으로 구현되었고, 디버그를 위한 dataSource 추가도 좋은 선택입니다.
100-150: 종 분석 엔드포인트의 인증 및 에러 처리 개선ResponseEntity를 사용한 구조화된 응답과 사용자 친화적 메시지 변환이 잘 구현되었습니다.
318-348: 사용량 조회 API가 안정적으로 구현됨DB 조회 실패 시 세션 fallback과 데이터 소스 표시가 잘 구현되었습니다.
380-401: 사용자 친화적 에러 메시지 변환이 훌륭합니다구체적인 해결 방법을 제시하고 친근한 톤을 유지하는 것이 사용자 경험 향상에 도움이 됩니다.
| public VisionUsage updateDailyLimit(UUID userId, int newLimit) { | ||
| if (newLimit < 0) { | ||
| throw new IllegalArgumentException("일일 한도는 0 이상이어야 합니다."); | ||
| } | ||
|
|
||
| VisionUsage usage = getTodayUsage(userId); | ||
| usage.setDailyLimit(newLimit); | ||
|
|
||
| log.info("Vision 일일 한도 변경: 사용자={}, 기존한도={}, 새한도={}", | ||
| userId, usage.getDailyLimit(), newLimit); | ||
|
|
||
| return visionUsageRepository.save(usage); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
관리자 권한 검증 및 한도 상한선 추가 필요
관리자 권한 검증과 일일 한도의 상한선 검증이 필요합니다.
+@PreAuthorize("hasRole('ADMIN')")
public VisionUsage updateDailyLimit(UUID userId, int newLimit) {
- if (newLimit < 0) {
- throw new IllegalArgumentException("일일 한도는 0 이상이어야 합니다.");
+ if (newLimit < 0 || newLimit > 100) {
+ throw new IllegalArgumentException("일일 한도는 0 이상 100 이하여야 합니다.");
}
VisionUsage usage = getTodayUsage(userId);
+ int oldLimit = usage.getDailyLimit();
usage.setDailyLimit(newLimit);
log.info("Vision 일일 한도 변경: 사용자={}, 기존한도={}, 새한도={}",
- userId, usage.getDailyLimit(), newLimit);
+ userId, oldLimit, newLimit);
return visionUsageRepository.save(usage);
}📝 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.
| public VisionUsage updateDailyLimit(UUID userId, int newLimit) { | |
| if (newLimit < 0) { | |
| throw new IllegalArgumentException("일일 한도는 0 이상이어야 합니다."); | |
| } | |
| VisionUsage usage = getTodayUsage(userId); | |
| usage.setDailyLimit(newLimit); | |
| log.info("Vision 일일 한도 변경: 사용자={}, 기존한도={}, 새한도={}", | |
| userId, usage.getDailyLimit(), newLimit); | |
| return visionUsageRepository.save(usage); | |
| } | |
| @PreAuthorize("hasRole('ADMIN')") | |
| public VisionUsage updateDailyLimit(UUID userId, int newLimit) { | |
| if (newLimit < 0 || newLimit > 100) { | |
| throw new IllegalArgumentException("일일 한도는 0 이상 100 이하여야 합니다."); | |
| } | |
| VisionUsage usage = getTodayUsage(userId); | |
| int oldLimit = usage.getDailyLimit(); | |
| usage.setDailyLimit(newLimit); | |
| log.info("Vision 일일 한도 변경: 사용자={}, 기존한도={}, 새한도={}", | |
| userId, oldLimit, newLimit); | |
| return visionUsageRepository.save(usage); | |
| } |
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/vision/service/VisionUsageService.java around
lines 115 to 127, the updateDailyLimit method lacks checks for administrator
privileges and an upper limit on the daily limit value. Add a verification step
to ensure the user has administrator rights before proceeding. Also, define and
enforce a maximum allowed daily limit, throwing an exception if newLimit exceeds
this upper bound. This will secure the method against unauthorized changes and
unreasonable limit values.
| public VisionUsage getTodayUsage(UUID userId) { | ||
| LocalDate today = LocalDate.now(); | ||
| return visionUsageRepository.findByUserIdAndUsageDate(userId, today) | ||
| .orElseGet(() -> createNewUsageRecord(userId, today)); | ||
| } |
There was a problem hiding this comment.
orElseGet 사용 시 트랜잭션 경계 고려 필요
getTodayUsage 메서드가 읽기 작업 중에 새 레코드를 생성할 수 있어 읽기 전용 트랜잭션에서 호출될 경우 문제가 발생할 수 있습니다.
+@Transactional(transactionManager = "supabaseTransactionManager", readOnly = false)
public VisionUsage getTodayUsage(UUID userId) {
LocalDate today = LocalDate.now();
return visionUsageRepository.findByUserIdAndUsageDate(userId, today)
.orElseGet(() -> createNewUsageRecord(userId, today));
}🤖 Prompt for AI Agents
In src/main/java/io/github/petty/vision/service/VisionUsageService.java around
lines 30 to 34, the getTodayUsage method uses orElseGet to create a new usage
record if none exists, which can cause issues if called within a read-only
transaction. To fix this, separate the read and write operations by first
attempting to find the usage record without creating one, and only create a new
record within a proper transactional context if none is found, ensuring that
record creation does not occur inside a read-only transaction.
| @Scheduled(cron = "0 0 2 * * *") | ||
| public void cleanupOldRecords() { | ||
| LocalDate cutoffDate = LocalDate.now().minusDays(30); | ||
| visionUsageRepository.deleteOldUsageRecords(cutoffDate); | ||
| log.info("Vision 사용량 기록 정리 완료: {} 이전 데이터 삭제", cutoffDate); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
스케줄러 메서드에 트랜잭션 및 에러 핸들링 추가 필요
대량 삭제 작업 시 트랜잭션 처리와 에러 핸들링이 필요합니다.
+@Transactional(transactionManager = "supabaseTransactionManager")
@Scheduled(cron = "0 0 2 * * *")
public void cleanupOldRecords() {
- LocalDate cutoffDate = LocalDate.now().minusDays(30);
- visionUsageRepository.deleteOldUsageRecords(cutoffDate);
- log.info("Vision 사용량 기록 정리 완료: {} 이전 데이터 삭제", cutoffDate);
+ try {
+ LocalDate cutoffDate = LocalDate.now().minusDays(30);
+ int deletedCount = visionUsageRepository.deleteOldUsageRecords(cutoffDate);
+ log.info("Vision 사용량 기록 정리 완료: {} 이전 데이터 {}건 삭제", cutoffDate, deletedCount);
+ } catch (Exception e) {
+ log.error("Vision 사용량 기록 정리 실패", 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.
| @Scheduled(cron = "0 0 2 * * *") | |
| public void cleanupOldRecords() { | |
| LocalDate cutoffDate = LocalDate.now().minusDays(30); | |
| visionUsageRepository.deleteOldUsageRecords(cutoffDate); | |
| log.info("Vision 사용량 기록 정리 완료: {} 이전 데이터 삭제", cutoffDate); | |
| } | |
| @Transactional(transactionManager = "supabaseTransactionManager") | |
| @Scheduled(cron = "0 0 2 * * *") | |
| public void cleanupOldRecords() { | |
| try { | |
| LocalDate cutoffDate = LocalDate.now().minusDays(30); | |
| int deletedCount = visionUsageRepository.deleteOldUsageRecords(cutoffDate); | |
| log.info("Vision 사용량 기록 정리 완료: {} 이전 데이터 {}건 삭제", cutoffDate, deletedCount); | |
| } catch (Exception e) { | |
| log.error("Vision 사용량 기록 정리 실패", e); | |
| // 모니터링 시스템에 알림 전송 고려 | |
| } | |
| } |
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/vision/service/VisionUsageService.java around
lines 105 to 110, the scheduled cleanupOldRecords method lacks transaction
management and error handling. Wrap the method body in a transaction by adding
the appropriate transactional annotation or programmatic transaction management.
Also, add try-catch blocks to handle potential exceptions during the deletion
process, logging errors appropriately to ensure failures are captured without
crashing the scheduler.
| public VisionUsage incrementUsage(UUID userId) { | ||
| VisionUsage usage = getTodayUsage(userId); | ||
|
|
||
| if (!usage.canUse()) { | ||
| throw new IllegalStateException( | ||
| String.format("일일 사용 한도(%d회)를 초과했습니다. 현재 사용량: %d회", | ||
| usage.getDailyLimit(), usage.getUsageCount()) | ||
| ); | ||
| } | ||
|
|
||
| usage.incrementUsage(); | ||
| VisionUsage saved = visionUsageRepository.save(usage); | ||
|
|
||
| log.info("Vision 사용량 증가: 사용자={}, 날짜={}, 사용량={}/{}", | ||
| userId, usage.getUsageDate(), saved.getUsageCount(), saved.getDailyLimit()); | ||
|
|
||
| return saved; | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
동시성 이슈: 사용량 증가 시 경합 상태 발생 가능
여러 요청이 동시에 사용량을 증가시킬 때 경합 상태가 발생할 수 있습니다. 데이터베이스 레벨에서 락을 사용하거나 낙관적 잠금을 적용하는 것을 고려하세요.
비관적 잠금을 사용한 해결 방법:
public VisionUsage incrementUsage(UUID userId) {
- VisionUsage usage = getTodayUsage(userId);
+ // 비관적 잠금을 사용하여 동시성 문제 해결
+ VisionUsage usage = visionUsageRepository.findByUserIdAndUsageDateWithLock(userId, LocalDate.now())
+ .orElseGet(() -> createNewUsageRecord(userId, LocalDate.now()));🏁 Script executed:
#!/bin/bash
# VisionUsage 엔티티에서 버전 필드나 락 메커니즘 확인
ast-grep --pattern $'class VisionUsage {
$$$
}'Length of output: 50
추가 정보 확인: VisionUsage 엔티티와 잠금(@Version) 필드 유무를 재검증합니다.
🏁 Script executed:
#!/bin/bash
# 1) VisionUsage 엔티티 클래스 위치 및 정의 확인
rg -n "class VisionUsage" -C 3 src/main/java
# 2) 낙관적 잠금을 위한 @Version 어노테이션 필드 확인
rg -n "@Version" -C 3 src/main/javaLength of output: 1357
저장소 인터페이스에서 잠금 관련 메서드 정의 여부를 확인해 주세요.
🏁 Script executed:
#!/bin/bash
# VisionUsageRepository 인터페이스 위치 및 메서드 정의 확인
rg -n "interface VisionUsageRepository" -C 5 src/main/java
# 잠금 어노테이션 또는 잠금 메서드 확인
rg -n "@Lock" -C 5 src/main/java
rg -n "findByUserIdAndUsageDate" -C 5 src/main/javaLength of output: 3636
동시성 보장: VisionUsage 증가 시 잠금 전략 적용 필요
현재 getTodayUsage()로 조회한 뒤 incrementUsage() → save() 로직은 여러 요청이 동시에 수행될 때 마지막 저장이 이전 처리를 덮어쓰면서 증가가 누락되는 경쟁 상태(race condition)가 발생할 수 있습니다. 아래 중 하나를 선택해 JPA 잠금 또는 Atomic 연산을 적용하세요.
• 엔티티에 낙관적 잠금 추가
VisionUsage에@Version필드 추가
• 비관적 잠금으로 조회 시점에 락 획득Repository메서드에@Lock(LockModeType.PESSIMISTIC_WRITE)적용
• Atomic 업데이트 쿼리 사용@Modifying @Query("UPDATE VisionUsage v SET v.usageCount = v.usageCount + 1 …")
수정 예시:
// src/main/java/io/github/petty/vision/entity/VisionUsage.java
public class VisionUsage {
+ @Version
+ private Long version;
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
// ...
}// src/main/java/io/github/petty/vision/repository/VisionUsageRepository.java
public interface VisionUsageRepository extends JpaRepository<VisionUsage, UUID> {
- Optional<VisionUsage> findByUserIdAndUsageDate(UUID userId, LocalDate usageDate);
+ @Lock(LockModeType.PESSIMISTIC_WRITE)
+ @Query("SELECT v FROM VisionUsage v WHERE v.userId = :userId AND v.usageDate = CURRENT_DATE")
+ Optional<VisionUsage> findByUserIdAndUsageDate(UUID userId, LocalDate usageDate);
}적용 후 동시성 문제가 해결되는지 확인해 주세요.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/vision/service/VisionUsageService.java lines 47
to 64, the incrementUsage method has a race condition due to concurrent updates
on VisionUsage. To fix this, apply a locking strategy: either add a @Version
field in the VisionUsage entity for optimistic locking, or modify the repository
method that fetches usage to use @Lock(LockModeType.PESSIMISTIC_WRITE) for
pessimistic locking, or implement an atomic update query with @Modifying and
@Query to increment usageCount directly in the database. After applying one of
these, verify concurrency issues are resolved.
| // 🔥 해상도 제한 대폭 완화 | ||
| private static final int MIN_WIDTH = 50; // 200 → 50으로 완화 | ||
| private static final int MIN_HEIGHT = 50; // 200 → 50으로 완화 |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
해상도 제한 완화로 인한 품질 문제 가능성
최소 해상도를 50x50으로 낮추면 분석 품질이 저하될 수 있습니다. Vision API의 최소 요구사항을 확인하세요.
🌐 Web query:
What is the minimum image resolution requirement for Amazon Rekognition and Google Vision API?
💡 Result:
Both Amazon Rekognition and Google Vision API have specific image resolution requirements to ensure optimal performance.
Amazon Rekognition:
-
Minimum Image Dimensions: The minimum image dimensions are 80 pixels for both height and width. (docs.aws.amazon.com)
-
Recommended Resolution: For best results, it's recommended to use images with a resolution of VGA (640x480) or higher. (aws.amazon.com)
-
Face Detection: To detect a face, it must be no smaller than 40x40 pixels in an image with a resolution of 1920x1080 pixels. (docs.aws.amazon.com)
Google Vision API:
-
Minimum Image Dimensions: Images should generally be a minimum of 640x480 pixels (about 300,000 pixels) to enable accurate detection. (cloud.google.com)
-
Recommended Sizes for Specific Features:
- Face Detection: 1600x1200 pixels
- Text Detection and Document Text Detection: 1024x768 pixels
- Other Features (e.g., Label Detection, Landmark Detection): 640x480 pixels
It's important to note that while images smaller than these recommended sizes may still work, they could result in lower accuracy. Additionally, images should not exceed 75 million pixels (length x width) for OCR analysis. (cloud.google.com)
In summary, both services accept images with a minimum resolution of 80x80 pixels, but for optimal performance, higher resolutions are recommended.
Citations:
- 1: https://docs.aws.amazon.com/rekognition/latest/dg/limits.html?utm_source=openai
- 2: https://aws.amazon.com/rekognition/faqs/?utm_source=openai
- 3: https://docs.aws.amazon.com/rekognition/latest/dg/limits.html?utm_source=openai
- 4: https://cloud.google.com/vision/docs/supported-files?utm_source=openai
- 5: https://cloud.google.com/vision/docs/supported-files?utm_source=openai
해상도 제한 완화로 인한 API 최소 요구사항 위반
현재 최소 해상도를 50×50으로 설정할 경우,
- Amazon Rekognition의 최소 이미지 크기(80×80 픽셀)[1]을 충족하지 못해 API 호출 시 오류가 발생할 수 있습니다.
- Google Vision API는 권장 최소 해상도 640×480 픽셀[2]을 기반으로 최적 성능을 제공하므로, 50×50 해상도에서는 인식 정확도가 크게 저하될 수 있습니다.
위 두 가지를 고려하여 비즈니스 요구사항과 분석 품질 저하 가능성을 재검토하고, 최소 해상도를 다음과 같이 상향 조정할 것을 제안합니다.
- 최소 해상도를 Amazon Rekognition 최소치(80×80) 이상으로 설정
- 필요 시 Google Vision API 권장 해상도(640×480) 수준으로 검토
- 변경에 따른 성능 및 품질 영향 테스트
참고 문서
- Amazon Rekognition 이미지 제한: https://docs.aws.amazon.com/rekognition/latest/dg/limits.html
- Google Vision API 권장 해상도: https://cloud.google.com/vision/docs/supported-files
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/vision/helper/ImageValidator.java around lines
36 to 38, the minimum image resolution constants are set to 50x50, which is
below the minimum requirements of Amazon Rekognition (80x80) and far below
Google Vision API's recommended 640x480. Update the MIN_WIDTH and MIN_HEIGHT
constants to at least 80 to meet Amazon Rekognition's minimum, and consider
increasing them to 640 and 480 respectively if aiming for Google Vision API's
recommended resolution. After adjusting, test the impact on performance and
analysis quality to ensure business requirements are met.
| // 리사이즈된 파일로 FormData 생성 | ||
| const originalFile = fileInput.files[0]; | ||
| if (!originalFile) { | ||
| alert('이미지를 선택해주세요.'); | ||
| return; | ||
| } | ||
|
|
||
| console.log(' [DEBUG] 이미지 리사이즈 확인 중...'); | ||
| const processedFile = await resizeImageIfNeeded(originalFile, 200, 200); | ||
|
|
||
| const fd = new FormData(); | ||
| fd.append('file', processedFile); | ||
| fd.append('petName', document.getElementById('petName').value); | ||
|
|
||
| // 이미지 표시 준비 | ||
| if (showImg) { | ||
| showImg.src = URL.createObjectURL(processedFile); | ||
| } | ||
|
|
There was a problem hiding this comment.
이미지 파일 검증 누락 및 메모리 관리 필요
이미지 표시용 ObjectURL 생성 후 해제하지 않고, 파일 타입 검증이 누락되었습니다.
// 리사이즈된 파일로 FormData 생성
const originalFile = fileInput.files[0];
if (!originalFile) {
alert('이미지를 선택해주세요.');
+ safeSetDisplay(mainFormContainer, 'block');
+ safeSetDisplay(spinnerContainer, 'none');
return;
}
+// 파일 타입 검증
+const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png'];
+if (!allowedTypes.includes(originalFile.type)) {
+ alert('JPEG 또는 PNG 이미지만 업로드 가능합니다.');
+ safeSetDisplay(mainFormContainer, 'block');
+ safeSetDisplay(spinnerContainer, 'none');
+ return;
+}
+
console.log('📷 [DEBUG] 이미지 리사이즈 확인 중...');
const processedFile = await resizeImageIfNeeded(originalFile, 200, 200);
const fd = new FormData();
fd.append('file', processedFile);
fd.append('petName', document.getElementById('petName').value);
// 이미지 표시 준비
if (showImg) {
- showImg.src = URL.createObjectURL(processedFile);
+ const previewURL = URL.createObjectURL(processedFile);
+ showImg.src = previewURL;
+ // 이미지 로드 후 URL 해제
+ showImg.onload = () => URL.revokeObjectURL(previewURL);
}📝 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.
| // 리사이즈된 파일로 FormData 생성 | |
| const originalFile = fileInput.files[0]; | |
| if (!originalFile) { | |
| alert('이미지를 선택해주세요.'); | |
| return; | |
| } | |
| console.log(' [DEBUG] 이미지 리사이즈 확인 중...'); | |
| const processedFile = await resizeImageIfNeeded(originalFile, 200, 200); | |
| const fd = new FormData(); | |
| fd.append('file', processedFile); | |
| fd.append('petName', document.getElementById('petName').value); | |
| // 이미지 표시 준비 | |
| if (showImg) { | |
| showImg.src = URL.createObjectURL(processedFile); | |
| } | |
| // 리사이즈된 파일로 FormData 생성 | |
| const originalFile = fileInput.files[0]; | |
| if (!originalFile) { | |
| alert('이미지를 선택해주세요.'); | |
| safeSetDisplay(mainFormContainer, 'block'); | |
| safeSetDisplay(spinnerContainer, 'none'); | |
| return; | |
| } | |
| // 파일 타입 검증 | |
| const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png']; | |
| if (!allowedTypes.includes(originalFile.type)) { | |
| alert('JPEG 또는 PNG 이미지만 업로드 가능합니다.'); | |
| safeSetDisplay(mainFormContainer, 'block'); | |
| safeSetDisplay(spinnerContainer, 'none'); | |
| return; | |
| } | |
| console.log('📷 [DEBUG] 이미지 리사이즈 확인 중...'); | |
| const processedFile = await resizeImageIfNeeded(originalFile, 200, 200); | |
| const fd = new FormData(); | |
| fd.append('file', processedFile); | |
| fd.append('petName', document.getElementById('petName').value); | |
| // 이미지 표시 준비 | |
| if (showImg) { | |
| const previewURL = URL.createObjectURL(processedFile); | |
| showImg.src = previewURL; | |
| // 이미지 로드 후 URL 해제 | |
| showImg.onload = () => URL.revokeObjectURL(previewURL); | |
| } |
🤖 Prompt for AI Agents
In src/main/resources/templates/visionUpload.html around lines 430 to 448, the
code lacks validation for the uploaded file type and does not release the
created ObjectURL, which can cause memory leaks. Add a check to verify that the
selected file is an image before processing it. After setting showImg.src to the
ObjectURL, store the URL in a variable and call URL.revokeObjectURL on it once
the image is loaded or no longer needed to properly release memory.
| function resizeImageIfNeeded(file, minWidth = 200, minHeight = 200) { | ||
| return new Promise((resolve) => { | ||
| const img = new Image(); | ||
| const canvas = document.createElement('canvas'); | ||
| const ctx = canvas.getContext('2d'); | ||
|
|
||
| img.onload = function() { | ||
| console.log(' [DEBUG] 원본 이미지 크기:', img.width, 'x', img.height); | ||
|
|
||
| if (img.width >= minWidth && img.height >= minHeight) { | ||
| console.log(' [DEBUG] 이미지 크기 충분함, 원본 사용'); | ||
| resolve(file); | ||
| return; | ||
| } | ||
| throw new Error('종 분석에 실패했습니다.'); | ||
|
|
||
| const scale = Math.max(minWidth / img.width, minHeight / img.height); | ||
| const newWidth = Math.round(img.width * scale); | ||
| const newHeight = Math.round(img.height * scale); | ||
|
|
||
| console.log(' [DEBUG] 이미지 리사이즈:', newWidth, 'x', newHeight, '(스케일:', scale, ')'); | ||
|
|
||
| canvas.width = newWidth; | ||
| canvas.height = newHeight; | ||
|
|
||
| ctx.imageSmoothingEnabled = true; | ||
| ctx.imageSmoothingQuality = 'high'; | ||
| ctx.drawImage(img, 0, 0, newWidth, newHeight); | ||
|
|
||
| canvas.toBlob((blob) => { | ||
| const resizedFile = new File([blob], file.name, { | ||
| type: file.type, | ||
| lastModified: Date.now() | ||
| }); | ||
| console.log(' [DEBUG] 이미지 리사이즈 완료'); | ||
| resolve(resizedFile); | ||
| }, file.type, 0.9); | ||
| }; | ||
|
|
||
| img.onerror = function() { | ||
| console.error(' [DEBUG] 이미지 로드 실패, 원본 반환'); | ||
| resolve(file); | ||
| }; | ||
|
|
||
| img.src = URL.createObjectURL(file); | ||
| }); | ||
| } |
There was a problem hiding this comment.
이미지 리사이즈 시 메모리 누수 방지 필요
URL.createObjectURL()로 생성한 객체 URL을 해제하지 않아 메모리 누수가 발생할 수 있습니다.
function resizeImageIfNeeded(file, minWidth = 200, minHeight = 200) {
return new Promise((resolve) => {
const img = new Image();
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
+ let objectURL = null;
img.onload = function() {
console.log('📐 [DEBUG] 원본 이미지 크기:', img.width, 'x', img.height);
if (img.width >= minWidth && img.height >= minHeight) {
console.log('✅ [DEBUG] 이미지 크기 충분함, 원본 사용');
+ if (objectURL) URL.revokeObjectURL(objectURL);
resolve(file);
return;
}
// ... 리사이즈 로직 ...
canvas.toBlob((blob) => {
const resizedFile = new File([blob], file.name, {
type: file.type,
lastModified: Date.now()
});
console.log('✅ [DEBUG] 이미지 리사이즈 완료');
+ if (objectURL) URL.revokeObjectURL(objectURL);
resolve(resizedFile);
}, file.type, 0.9);
};
img.onerror = function() {
console.error('❌ [DEBUG] 이미지 로드 실패, 원본 반환');
+ if (objectURL) URL.revokeObjectURL(objectURL);
resolve(file);
};
- img.src = URL.createObjectURL(file);
+ objectURL = URL.createObjectURL(file);
+ img.src = objectURL;
});
}📝 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.
| function resizeImageIfNeeded(file, minWidth = 200, minHeight = 200) { | |
| return new Promise((resolve) => { | |
| const img = new Image(); | |
| const canvas = document.createElement('canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| img.onload = function() { | |
| console.log(' [DEBUG] 원본 이미지 크기:', img.width, 'x', img.height); | |
| if (img.width >= minWidth && img.height >= minHeight) { | |
| console.log(' [DEBUG] 이미지 크기 충분함, 원본 사용'); | |
| resolve(file); | |
| return; | |
| } | |
| throw new Error('종 분석에 실패했습니다.'); | |
| const scale = Math.max(minWidth / img.width, minHeight / img.height); | |
| const newWidth = Math.round(img.width * scale); | |
| const newHeight = Math.round(img.height * scale); | |
| console.log(' [DEBUG] 이미지 리사이즈:', newWidth, 'x', newHeight, '(스케일:', scale, ')'); | |
| canvas.width = newWidth; | |
| canvas.height = newHeight; | |
| ctx.imageSmoothingEnabled = true; | |
| ctx.imageSmoothingQuality = 'high'; | |
| ctx.drawImage(img, 0, 0, newWidth, newHeight); | |
| canvas.toBlob((blob) => { | |
| const resizedFile = new File([blob], file.name, { | |
| type: file.type, | |
| lastModified: Date.now() | |
| }); | |
| console.log(' [DEBUG] 이미지 리사이즈 완료'); | |
| resolve(resizedFile); | |
| }, file.type, 0.9); | |
| }; | |
| img.onerror = function() { | |
| console.error(' [DEBUG] 이미지 로드 실패, 원본 반환'); | |
| resolve(file); | |
| }; | |
| img.src = URL.createObjectURL(file); | |
| }); | |
| } | |
| function resizeImageIfNeeded(file, minWidth = 200, minHeight = 200) { | |
| return new Promise((resolve) => { | |
| const img = new Image(); | |
| const canvas = document.createElement('canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| let objectURL = null; | |
| img.onload = function() { | |
| console.log(' [DEBUG] 원본 이미지 크기:', img.width, 'x', img.height); | |
| if (img.width >= minWidth && img.height >= minHeight) { | |
| console.log(' [DEBUG] 이미지 크기 충분함, 원본 사용'); | |
| if (objectURL) URL.revokeObjectURL(objectURL); | |
| resolve(file); | |
| return; | |
| } | |
| const scale = Math.max(minWidth / img.width, minHeight / img.height); | |
| const newWidth = Math.round(img.width * scale); | |
| const newHeight = Math.round(img.height * scale); | |
| console.log(' [DEBUG] 이미지 리사이즈:', newWidth, 'x', newHeight, '(스케일:', scale, ')'); | |
| canvas.width = newWidth; | |
| canvas.height = newHeight; | |
| ctx.imageSmoothingEnabled = true; | |
| ctx.imageSmoothingQuality = 'high'; | |
| ctx.drawImage(img, 0, 0, newWidth, newHeight); | |
| canvas.toBlob((blob) => { | |
| const resizedFile = new File([blob], file.name, { | |
| type: file.type, | |
| lastModified: Date.now() | |
| }); | |
| console.log(' [DEBUG] 이미지 리사이즈 완료'); | |
| if (objectURL) URL.revokeObjectURL(objectURL); | |
| resolve(resizedFile); | |
| }, file.type, 0.9); | |
| }; | |
| img.onerror = function() { | |
| console.error(' [DEBUG] 이미지 로드 실패, 원본 반환'); | |
| if (objectURL) URL.revokeObjectURL(objectURL); | |
| resolve(file); | |
| }; | |
| objectURL = URL.createObjectURL(file); | |
| img.src = objectURL; | |
| }); | |
| } |
🤖 Prompt for AI Agents
In src/main/resources/templates/visionUpload.html around lines 319 to 364, the
function resizeImageIfNeeded uses URL.createObjectURL to load the image but does
not release the created object URL, causing potential memory leaks. To fix this,
after the image has loaded or failed to load, call URL.revokeObjectURL with the
created object URL to properly release the memory. Ensure this revocation
happens in both the img.onload and img.onerror handlers.
|
|
||
| // 세션 키 상수 | ||
| // 세션 fallback용 상수 | ||
| private static final int DEFAULT_DAILY_LIMIT = 3; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
DEFAULT_DAILY_LIMIT을 설정 파일에서 주입받도록 변경 필요
현재 일일 한도가 하드코딩되어 있습니다. PR 목표에 따르면 이 값은 application.yml에서 관리되어야 합니다.
- private static final int DEFAULT_DAILY_LIMIT = 3;
+ @Value("${vision.daily-limit:3}")
+ private int defaultDailyLimit;추가로 클래스 상단에 @Value import가 필요합니다:
import org.springframework.beans.factory.annotation.Value;🤖 Prompt for AI Agents
In src/main/java/io/github/petty/vision/adapter/in/VisionController.java at line
44, the DEFAULT_DAILY_LIMIT is currently hardcoded. Change this to inject the
value from application.yml using the @Value annotation. Add the import statement
for org.springframework.beans.factory.annotation.Value at the top of the class.
Replace the static final int declaration with a private int field annotated with
@Value("${property.name}") to load the limit from configuration.
| private AuthenticationResult checkAuthenticationWithRefresh(HttpServletRequest request) { | ||
| Authentication auth = SecurityContextHolder.getContext().getAuthentication(); | ||
|
|
||
| // 기본 인증 확인 | ||
| if (auth == null || !auth.isAuthenticated() || auth instanceof AnonymousAuthenticationToken) { | ||
| log.debug(" 기본 인증 실패"); | ||
| return AuthenticationResult.unauthenticated(); | ||
| } | ||
|
|
||
| try { | ||
| UUID userId = userService.getCurrentUserId(auth.getPrincipal()); | ||
| String username = auth.getName(); | ||
|
|
||
| log.debug(" 인증 확인 성공: 사용자={}, userId={}", username, userId); | ||
|
|
||
| return AuthenticationResult.authenticated(userId, username); | ||
| } catch (Exception e) { | ||
| log.warn(" 사용자 정보 추출 실패: {}", e.getMessage()); | ||
| return AuthenticationResult.unauthenticated(); | ||
| } | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
checkAuthenticationWithRefresh 메서드명이 실제 동작과 불일치
메서드명은 refresh 토큰 처리를 암시하지만, 실제로는 기본 인증 확인만 수행합니다. PR 목표에서 언급한 "자동 토큰 갱신"이 프론트엔드에서 처리된다면, 메서드명을 checkAuthentication으로 변경하는 것이 명확할 것입니다.
🏁 Script executed:
#!/bin/bash
# Description: 프론트엔드 파일에서 실제 refresh 토큰 처리 로직 확인
# visionUpload.html에서 refreshAccessToken 함수 구현 확인
fd -e html visionUpload | xargs rg -A 10 "refreshAccessToken|authenticatedFetch"Length of output: 2830
메서드명 checkAuthenticationWithRefresh → checkAuthentication 재명명 제안
이 메서드는 실제로 토큰 갱신 로직을 수행하지 않고, SecurityContext에서 인증 정보만 확인합니다. 프론트엔드(authenticatedFetch/refreshAccessToken)에서 /api/auth/refresh로 토큰 갱신을 처리하고 있으므로, 메서드명을 변경해 혼동을 줄이는 것이 좋습니다.
- 파일:
src/main/java/io/github/petty/vision/adapter/in/VisionController.java - 라인: 269–289
- 제안된 수정:
- private AuthenticationResult checkAuthenticationWithRefresh(HttpServletRequest request) { + private AuthenticationResult checkAuthentication(HttpServletRequest request) {
📝 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.
| private AuthenticationResult checkAuthenticationWithRefresh(HttpServletRequest request) { | |
| Authentication auth = SecurityContextHolder.getContext().getAuthentication(); | |
| // 기본 인증 확인 | |
| if (auth == null || !auth.isAuthenticated() || auth instanceof AnonymousAuthenticationToken) { | |
| log.debug(" 기본 인증 실패"); | |
| return AuthenticationResult.unauthenticated(); | |
| } | |
| try { | |
| UUID userId = userService.getCurrentUserId(auth.getPrincipal()); | |
| String username = auth.getName(); | |
| log.debug(" 인증 확인 성공: 사용자={}, userId={}", username, userId); | |
| return AuthenticationResult.authenticated(userId, username); | |
| } catch (Exception e) { | |
| log.warn(" 사용자 정보 추출 실패: {}", e.getMessage()); | |
| return AuthenticationResult.unauthenticated(); | |
| } | |
| } | |
| --- a/src/main/java/io/github/petty/vision/adapter/in/VisionController.java | |
| +++ b/src/main/java/io/github/petty/vision/adapter/in/VisionController.java | |
| @@ -269,7 +269,7 @@ | |
| - private AuthenticationResult checkAuthenticationWithRefresh(HttpServletRequest request) { | |
| + private AuthenticationResult checkAuthentication(HttpServletRequest request) { | |
| Authentication auth = SecurityContextHolder.getContext().getAuthentication(); | |
| // 기본 인증 확인 | |
| if (auth == null || !auth.isAuthenticated() || auth instanceof AnonymousAuthenticationToken) { | |
| log.debug(" 기본 인증 실패"); | |
| return AuthenticationResult.unauthenticated(); | |
| } | |
| try { | |
| UUID userId = userService.getCurrentUserId(auth.getPrincipal()); | |
| String username = auth.getName(); | |
| log.debug(" 인증 확인 성공: 사용자={}, userId={}", username, userId); | |
| return AuthenticationResult.authenticated(userId, username); | |
| } catch (Exception e) { | |
| log.warn(" 사용자 정보 추출 실패: {}", e.getMessage()); | |
| return AuthenticationResult.unauthenticated(); | |
| } | |
| } |
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/vision/adapter/in/VisionController.java lines
269 to 289, rename the method checkAuthenticationWithRefresh to
checkAuthentication to accurately reflect its functionality, as it only verifies
basic authentication without handling token refresh. Update all references to
this method accordingly to avoid confusion about its purpose.
📜 PR 내용 요약
Vision 일일 분석 사용량을 DB로 관리하는 기능 도입 및 프론트엔드 인증/UX 전면 리팩토링
⚒️ 작업 및 변경 내용(상세하게)
VisionUsage엔티티: 사용자별, 날짜별 사용 횟수 기록VisionUsageRepository: JPA 쿼리 메소드 정의VisionUsageService: 사용량 조회/증가/초과 체크 로직 구현ImageValidator수정/보완SupabaseDataSourceConfig.java에서 외부 DB 커넥션 관리application.yml에서 관련 환경변수, 일일 한도 등 관리authenticatedFetch,refreshAccessToken함수 추가)📚 기타 참고 사항
/api/auth/refresh엔드포인트 점검 필요