Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
basePackages = {
"io.github.petty.users.repository",
"io.github.petty.community.repository",
"io.github.petty.vision.repository"
},
entityManagerFactoryRef = "supabaseEntityManagerFactory",
transactionManagerRef = "supabaseTransactionManager"
Expand Down Expand Up @@ -74,7 +75,8 @@ public LocalContainerEntityManagerFactoryBean supabaseEntityManagerFactory(
return builder.dataSource(dataSource)
.packages(
"io.github.petty.users.entity",
"io.github.petty.community.entity"
"io.github.petty.community.entity",
"io.github.petty.vision.entity"
).persistenceUnit("supabase") // 중복 X
.properties(jpaProperties) // 대소문자 구분 X
.build();
Expand Down
373 changes: 295 additions & 78 deletions src/main/java/io/github/petty/vision/adapter/in/VisionController.java

Large diffs are not rendered by default.

66 changes: 66 additions & 0 deletions src/main/java/io/github/petty/vision/entity/VisionUsage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package io.github.petty.vision.entity;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.CreationTimestamp;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.UUID;

@Entity
@Table(name = "vision_usage",
uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "usage_date"}))
@Getter
@Setter
@NoArgsConstructor
public class VisionUsage {

@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;

@Column(name = "user_id", nullable = false)
private UUID userId;

@Column(name = "usage_date", nullable = false)
private LocalDate usageDate;

@Column(name = "usage_count", nullable = false)
private Integer usageCount = 0;

@Column(name = "daily_limit", nullable = false)
private Integer dailyLimit = 3; // 기본값 3회

@CreationTimestamp
@Column(name = "created_at")
private LocalDateTime createdAt;

@Column(name = "updated_at")
private LocalDateTime updatedAt;

public VisionUsage(UUID userId, LocalDate usageDate, Integer dailyLimit) {
this.userId = userId;
this.usageDate = usageDate;
this.dailyLimit = dailyLimit;
this.usageCount = 0;
}

// 사용량 증가
public void incrementUsage() {
this.usageCount++;
this.updatedAt = LocalDateTime.now();
}

// 사용 가능 여부 확인
public boolean canUse() {
return this.usageCount < this.dailyLimit;
}

// 남은 사용량
public int getRemainingUsage() {
return Math.max(0, this.dailyLimit - this.usageCount);
}
}
66 changes: 49 additions & 17 deletions src/main/java/io/github/petty/vision/helper/ImageValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,16 @@ public class ImageValidator {
);
private static final long MIN_FILE_SIZE = 10 * 1024; // 10KB
private static final long MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
private static final int MIN_WIDTH = 200;
private static final int MIN_HEIGHT = 200;
private static final float MIN_ANIMAL_CONFIDENCE = 70.0f;

// 🔥 해상도 제한 대폭 완화
private static final int MIN_WIDTH = 50; // 200 → 50으로 완화
private static final int MIN_HEIGHT = 50; // 200 → 50으로 완화
Comment on lines +36 to +38
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.


// 🔥 동물 감지 요구사항 완화
private static final float MIN_ANIMAL_CONFIDENCE = 50.0f; // 70 → 50으로 완화
private static final Set<String> ANIMAL_LABELS = new HashSet<>(
Arrays.asList("Animal", "Pet", "Dog", "Cat", "Mammal", "Canine", "Feline")
Arrays.asList("Animal", "Pet", "Dog", "Cat", "Mammal", "Canine", "Feline",
"Bird", "Fish", "Reptile", "Amphibian", "Insect")
);

public ValidationResult validate(MultipartFile file) {
Expand Down Expand Up @@ -68,39 +73,66 @@ public ValidationResult validate(MultipartFile file) {
if (image == null) {
return ValidationResult.invalid("유효한 이미지 파일이 아닙니다.");
}
if (image.getWidth() < MIN_WIDTH || image.getHeight() < MIN_HEIGHT) {
return ValidationResult.invalid("이미지 해상도가 너무 낮습니다. 최소 200×200 이상이어야 합니다.");

int width = image.getWidth();
int height = image.getHeight();

log.info("📏 이미지 해상도 확인: {}×{} (최소 요구: {}×{})",
width, height, MIN_WIDTH, MIN_HEIGHT);

// 🔥 아주 관대한 해상도 체크
if (width < MIN_WIDTH || height < MIN_HEIGHT) {
log.warn("⚠️ 해상도 낮음: {}×{}, 하지만 분석 진행", width, height);
// return ValidationResult.invalid() 대신 경고만 하고 통과
}
return validateAnimalContent(bytes);

// 🔥 동물 감지도 선택적으로 (실패해도 무조건 통과)
return validateAnimalContentOptional(bytes);

} catch (IOException e) {
log.error("이미지 처리 중 오류 발생", e);
return ValidationResult.invalid("이미지 파일을 처리하는 중 오류가 발생했습니다.");
}
}

private ValidationResult validateAnimalContent(byte[] bytes) {
/**
* 🔥 동물 콘텐츠 검증 (선택적 - 무조건 통과)
*/
private ValidationResult validateAnimalContentOptional(byte[] bytes) {
try {
DetectLabelsRequest request = DetectLabelsRequest.builder()
.image(Image.builder().bytes(SdkBytes.fromByteArray(bytes)).build())
.maxLabels(10)
.minConfidence(50.0f)
.maxLabels(20) // 10 → 20으로 증가
.minConfidence(30.0f) // 50 → 30으로 낮춤
.build();
DetectLabelsResponse response = rekognitionClient.detectLabels(request);

boolean animalDetected = false;
for (Label label : response.labels()) {
log.debug("🔍 감지된 라벨: {} (신뢰도: {}%)", label.name(), label.confidence());

if (ANIMAL_LABELS.contains(label.name()) && label.confidence() >= MIN_ANIMAL_CONFIDENCE) {
log.info("동물 감지됨: {}, 신뢰도: {}", label.name(), label.confidence());
return ValidationResult.valid();
log.info("✅ 동물 감지됨: {}, 신뢰도: {}%", label.name(), label.confidence());
animalDetected = true;
break;
}
}
return ValidationResult.invalid("반려동물이 감지되지 않았습니다.");

if (!animalDetected) {
log.warn("⚠️ 동물이 명확히 감지되지 않았지만 분석을 진행합니다.");
}

// 🔥 동물이 감지되든 안 되든 무조건 통과
return ValidationResult.valid();

} catch (Exception e) {
log.error("Rekognition 오류", e);
return ValidationResult.invalid("이미지 분석 중 오류가 발생했습니다.");
log.warn("⚠️ Rekognition 검증 실패, 기본 검증만 수행: {}", e.getMessage());
// 🔥 Rekognition 실패해도 무조건 통과
return ValidationResult.valid();
}
}

// Magic Number Signatures
// Magic Number Signatures (변경 없음)
private static final byte[] JPG_SIG = new byte[]{(byte) 0xFF, (byte) 0xD8, (byte) 0xFF};
private static final byte[] PNG_SIG = new byte[]{(byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A};
private static final byte[] BMP_SIG = new byte[]{0x42, 0x4D};
Expand Down Expand Up @@ -136,4 +168,4 @@ public static ValidationResult invalid(String message) {
return new ValidationResult(false, message);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package io.github.petty.vision.repository;

import io.github.petty.vision.entity.VisionUsage;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.time.LocalDate;
import java.util.Optional;
import java.util.UUID;

@Repository
public interface VisionUsageRepository extends JpaRepository<VisionUsage, UUID> {

/**
* 특정 사용자의 특정 날짜 사용량 조회
*/
Optional<VisionUsage> findByUserIdAndUsageDate(UUID userId, LocalDate usageDate);

/**
* 오늘 사용량 조회 (편의 메서드)
*/
@Query("SELECT v FROM VisionUsage v WHERE v.userId = :userId AND v.usageDate = CURRENT_DATE")
Optional<VisionUsage> findTodayUsage(@Param("userId") UUID userId);

/**
* 특정 사용자의 총 사용량 조회
*/
@Query("SELECT COALESCE(SUM(v.usageCount), 0) FROM VisionUsage v WHERE v.userId = :userId")
Long getTotalUsageByUser(@Param("userId") UUID userId);

/**
* 오래된 사용량 기록 삭제 (30일 이전)
*/
@Modifying
@Query("DELETE FROM VisionUsage v WHERE v.usageDate < :cutoffDate")
void deleteOldUsageRecords(@Param("cutoffDate") LocalDate cutoffDate);

/**
* 특정 날짜 이후 사용량 기록 조회
*/
@Query("SELECT v FROM VisionUsage v WHERE v.userId = :userId AND v.usageDate >= :fromDate ORDER BY v.usageDate DESC")
java.util.List<VisionUsage> findUsageHistory(@Param("userId") UUID userId, @Param("fromDate") LocalDate fromDate);
}
128 changes: 128 additions & 0 deletions src/main/java/io/github/petty/vision/service/VisionUsageService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package io.github.petty.vision.service;

import io.github.petty.vision.entity.VisionUsage;
import io.github.petty.vision.repository.VisionUsageRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.util.List;
import java.util.UUID;

@Service
@RequiredArgsConstructor
@Slf4j
@Transactional(transactionManager = "supabaseTransactionManager")
public class VisionUsageService {

private final VisionUsageRepository visionUsageRepository;

@Value("${vision.daily-limit:3}")
private int defaultDailyLimit;

/**
* 오늘 사용량 조회 (없으면 새로 생성)
*/
public VisionUsage getTodayUsage(UUID userId) {
LocalDate today = LocalDate.now();
return visionUsageRepository.findByUserIdAndUsageDate(userId, today)
.orElseGet(() -> createNewUsageRecord(userId, today));
}
Comment on lines +30 to +34
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.


/**
* 사용 가능 여부 확인
*/
public boolean canUseToday(UUID userId) {
VisionUsage usage = getTodayUsage(userId);
return usage.canUse();
}

/**
* 사용량 증가 (분석 완료 시 호출)
*/
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;
}
Comment on lines +47 to +64
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.


/**
* 남은 사용량 조회
*/
public int getRemainingUsage(UUID userId) {
VisionUsage usage = getTodayUsage(userId);
return usage.getRemainingUsage();
}

/**
* 사용량 기록 생성
*/
private VisionUsage createNewUsageRecord(UUID userId, LocalDate date) {
VisionUsage newUsage = new VisionUsage(userId, date, defaultDailyLimit);
VisionUsage saved = visionUsageRepository.save(newUsage);

log.debug("새로운 Vision 사용량 기록 생성: 사용자={}, 날짜={}, 한도={}",
userId, date, defaultDailyLimit);

return saved;
}

/**
* 사용자 사용량 히스토리 조회 (최근 7일)
*/
public List<VisionUsage> getRecentUsageHistory(UUID userId) {
LocalDate fromDate = LocalDate.now().minusDays(7);
return visionUsageRepository.findUsageHistory(userId, fromDate);
}

/**
* 총 사용량 조회
*/
public Long getTotalUsage(UUID userId) {
return visionUsageRepository.getTotalUsageByUser(userId);
}

/**
* 오래된 기록 정리 (매일 새벽 2시)
*/
@Scheduled(cron = "0 0 2 * * *")
public void cleanupOldRecords() {
LocalDate cutoffDate = LocalDate.now().minusDays(30);
visionUsageRepository.deleteOldUsageRecords(cutoffDate);
log.info("Vision 사용량 기록 정리 완료: {} 이전 데이터 삭제", cutoffDate);
}
Comment on lines +105 to +110
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.


/**
* 관리자용: 사용자 일일 한도 조정
*/
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);
}
Comment on lines +115 to +127
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.

}
8 changes: 7 additions & 1 deletion src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,10 @@ logging:
com.google.cloud: ERROR

server:
forward-headers-strategy: FRAMEWORK
forward-headers-strategy: FRAMEWORK

vision:
daily-limit: 3 # 기본 일일 한도
cleanup:
enabled: true
retention-days: 30 # 30일 이전 데이터 삭제
Loading