Skip to content

[VISION] 분석 일일 사용량 DB 관리 및 인증 UX 전면 개선#72

Merged
usn757 merged 2 commits intoPETTY-HUB:mainfrom
23MinL:feat/vision-config
Jun 8, 2025
Merged

[VISION] 분석 일일 사용량 DB 관리 및 인증 UX 전면 개선#72
usn757 merged 2 commits intoPETTY-HUB:mainfrom
23MinL:feat/vision-config

Conversation

@23MinL
Copy link
Contributor

@23MinL 23MinL commented Jun 8, 2025

📜 PR 내용 요약

Vision 일일 분석 사용량을 DB로 관리하는 기능 도입 및 프론트엔드 인증/UX 전면 리팩토링

  • Vision 분석 기능의 사용량 제한을 기존 세션/메모리 방식에서 DB 기반으로 변경
  • 리프레시 토큰 처리, 인증 자동화 등 UX/보안 대폭 강화
  • Supabase 설정, 서비스/컨트롤러/유효성 코드 및 프론트엔드 코드 전체 업데이트

⚒️ 작업 및 변경 내용(상세하게)

  1. Vision 일일 사용량 DB 관리 기능 추가
  • 엔티티/리포지토리/서비스 계층 추가
    • VisionUsage 엔티티: 사용자별, 날짜별 사용 횟수 기록
    • VisionUsageRepository: JPA 쿼리 메소드 정의
    • VisionUsageService: 사용량 조회/증가/초과 체크 로직 구현
  1. VisionController 비즈니스 로직 변경
  • 컨트롤러에서 로그인/사용량 초과/DB 체크로직 도입
  • 종분석/최종분석 모두 사용량 체크, 응답 메시지 개선
  • ImageValidator 수정/보완
  1. Supabase 연동 및 설정파일 강화
  • SupabaseDataSourceConfig.java에서 외부 DB 커넥션 관리
  • application.yml에서 관련 환경변수, 일일 한도 등 관리
  1. 프론트엔드 UX/보안 개선 (visionUpload.html)
  • 로그인 체크, 사용량 안내, 초과시 버튼 비활성화 등
  • 토큰 만료시 자동 리프레시(authenticatedFetch, refreshAccessToken 함수 추가)
  • UX 대폭 개선(로딩, 결과 표시, 에러 메시지, 접근 제한, 미리보기 등)
  • 모바일 대응 및 접근성 강화
  1. 기타
  • 코드 정리 및 스타일 일관성 유지
  • 기존 session 기반 사용량 체크 로직 완전 제거(DB 일원화)

📚 기타 참고 사항

  • DB 마이그레이션 필요: VisionUsage 테이블 생성 후 정상 작동
  • 배포 환경(application.yml)에서 일일 한도, DB URI, Supabase 키 등 반드시 점검
  • 테스트는 기본적으로 로컬 DB, 실제 배포시 supabase prod로 변경 필요
  • 프론트엔드 자동 토큰갱신(fetch) 동작 안할시, 서버 /api/auth/refresh 엔드포인트 점검 필요

@coderabbitai
Copy link

coderabbitai bot commented Jun 8, 2025

Summary by CodeRabbit

  • 신규 기능

    • 비전(이미지 분석) 기능에 대해 일일 사용량 추적 및 제한 기능이 추가되었습니다.
    • 사용자는 자신의 일일 사용량 및 남은 횟수를 웹 UI와 API를 통해 확인할 수 있습니다.
    • 관리자에 의한 사용자별 일일 사용 한도 설정 및 30일 이상된 기록 자동 정리 기능이 도입되었습니다.
  • 버그 수정

    • 이미지 업로드 시 해상도 및 동물 인식 기준이 완화되어 더 다양한 이미지를 허용합니다.
    • 인증 및 토큰 만료 시 자동 갱신 및 사용자 알림이 개선되었습니다.
  • UI 개선

    • 토큰 상태 및 오프라인 경고, 로딩 스피너, 이미지 미리보기, 상세 오류 메시지 등 사용자 경험이 대폭 향상되었습니다.
    • 분석 결과, 사용량, 로그인 상태 등의 안내가 더욱 명확해졌습니다.
  • 문서화

    • 환경설정 파일 및 템플릿에 새로운 설정 및 안내가 추가되었습니다.

Walkthrough

이 변경사항은 Vision 기능에 대한 인증 처리, 사용량 추적, 오류 응답, 이미지 검증, 클라이언트-서버 상호작용을 대대적으로 개선합니다. Vision 사용량을 데이터베이스에 저장하고 조회하는 새로운 서비스와 엔티티, 레포지토리가 추가되었고, 프론트엔드 템플릿과 자바스크립트도 토큰 관리와 사용자 피드백 중심으로 리팩터링되었습니다.

Changes

파일/경로 변경 요약
.../SupabaseDataSourceConfig.java Vision 관련 JPA 엔티티 및 레포지토리 패키지 스캔 경로 추가
.../VisionController.java 인증 처리, 사용량 추적, 오류 응답, 세션/DB fallback, 메소드 시그니처 및 내부 로직 대폭 리팩터링
.../entity/VisionUsage.java Vision 일일 사용량 추적용 JPA 엔티티 신설, 사용량 증가/조회/제한 메소드 포함
.../repository/VisionUsageRepository.java VisionUsage 엔티티용 JPA 레포지토리 신설, 일일/총 사용량 조회 및 기록 삭제 등 메소드 추가
.../service/VisionUsageService.java Vision 사용량 관리 서비스 신설, 일일 사용량 증가/조회, 사용 가능 여부, 기록 정리, 제한 변경 등 비즈니스 로직 구현
.../helper/ImageValidator.java 이미지 검증 기준 완화(해상도/동물 검출), 검증 실패시 경고만 로그, 검증 통과로 처리
.../application.yml Vision 일일 사용량 및 기록 정리 관련 신규 설정 추가(daily-limit, cleanup)
.../templates/visionUpload.html 토큰/오프라인 알림, 이미지 미리보기, 사용량/로그인 상태 UI, 인증 fetch, 이미지 리사이즈, JS 리팩터링

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
Loading

Possibly related PRs

Poem

🐰
오늘도 Vision이 말해요,
사용량은 DB가 기억해요.
인증은 더 똑똑하게,
이미지는 더 넓게 받아요.
프론트엔드도 토큰 걱정 끝!
토끼는 기뻐 폴짝,
데이터와 코드가 함께 춤춰요!

✨ Finishing Touches
  • 📝 Generate Docstrings

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need 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)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@23MinL
Copy link
Contributor Author

23MinL commented Jun 8, 2025

@CodeRabbit review

@coderabbitai
Copy link

coderabbitai bot commented Jun 8, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@usn757 usn757 merged commit 08d6f92 into PETTY-HUB:main Jun 8, 2025
2 checks passed
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 6e0e064 and 4ff03e5.

📒 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: 사용자 친화적 에러 메시지 변환이 훌륭합니다

구체적인 해결 방법을 제시하고 친근한 톤을 유지하는 것이 사용자 경험 향상에 도움이 됩니다.

Comment on lines +115 to +127
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);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

관리자 권한 검증 및 한도 상한선 추가 필요

관리자 권한 검증과 일일 한도의 상한선 검증이 필요합니다.

+@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.

Suggested change
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.

Comment on lines +30 to +34
public VisionUsage getTodayUsage(UUID userId) {
LocalDate today = LocalDate.now();
return visionUsageRepository.findByUserIdAndUsageDate(userId, today)
.orElseGet(() -> createNewUsageRecord(userId, today));
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Comment on lines +105 to +110
@Scheduled(cron = "0 0 2 * * *")
public void cleanupOldRecords() {
LocalDate cutoffDate = LocalDate.now().minusDays(30);
visionUsageRepository.deleteOldUsageRecords(cutoffDate);
log.info("Vision 사용량 기록 정리 완료: {} 이전 데이터 삭제", cutoffDate);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ 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.

Suggested change
@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.

Comment on lines +47 to +64
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;
}
Copy link

Choose a reason for hiding this comment

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

💡 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/java

Length 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/java

Length 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.

Comment on lines +36 to +38
// 🔥 해상도 제한 대폭 완화
private static final int MIN_WIDTH = 50; // 200 → 50으로 완화
private static final int MIN_HEIGHT = 50; // 200 → 50으로 완화
Copy link

Choose a reason for hiding this comment

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

💡 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:


해상도 제한 완화로 인한 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) 수준으로 검토
  • 변경에 따른 성능 및 품질 영향 테스트

참고 문서

  1. Amazon Rekognition 이미지 제한: https://docs.aws.amazon.com/rekognition/latest/dg/limits.html
  2. 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.

Comment on lines +430 to +448
// 리사이즈된 파일로 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);
}

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

이미지 파일 검증 누락 및 메모리 관리 필요

이미지 표시용 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.

Suggested change
// 리사이즈된 파일로 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.

Comment on lines +319 to +364
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);
});
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

이미지 리사이즈 시 메모리 누수 방지 필요

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.

Suggested change
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;
Copy link

Choose a reason for hiding this comment

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

🛠️ 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.

Comment on lines +269 to +289
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();
}
}
Copy link

Choose a reason for hiding this comment

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

💡 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


메서드명 checkAuthenticationWithRefreshcheckAuthentication 재명명 제안

이 메서드는 실제로 토큰 갱신 로직을 수행하지 않고, 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.

Suggested change
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.

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.

2 participants