Skip to content
Closed
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;
Comment on lines +41 to +42
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

updatedAt 필드의 자동 업데이트 개선을 고려해보세요.

현재 updatedAt 필드가 수동으로 관리되고 있습니다. 더 일관성 있는 타임스탬프 관리를 위해 @UpdateTimestamp 어노테이션 사용을 권장합니다.

+import org.hibernate.annotations.UpdateTimestamp;

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

그리고 incrementUsage() 메서드에서 수동 설정 제거:

 public void incrementUsage() {
     this.usageCount++;
-    this.updatedAt = LocalDateTime.now();
 }
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/vision/entity/VisionUsage.java at lines 41-42,
the updatedAt field is currently managed manually. To improve consistency, add
the @UpdateTimestamp annotation to the updatedAt field to enable automatic
timestamp updates on entity changes. Then, remove any manual setting of
updatedAt in the incrementUsage() method to avoid redundant or conflicting
updates.


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으로 완화

// 🔥 동물 감지 요구사항 완화
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);

Comment on lines +89 to +91
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

검증 로직이 실질적으로 무효화됨

모든 경로에서 ValidationResult.valid()를 반환하도록 변경되어 실질적인 검증이 이루어지지 않습니다. 이는:

  • 잘못된 이미지가 다운스트림 서비스로 전달될 수 있음
  • 오류 처리가 다른 레이어로 이동해야 함
  • 로깅만으로는 문제 추적이 어려울 수 있음

최소한의 검증 기준은 유지하면서 UX를 개선하는 균형점을 찾는 것이 좋겠습니다.

Also applies to: 126-126, 131-131

🤖 Prompt for AI Agents
In src/main/java/io/github/petty/vision/helper/ImageValidator.java around lines
89-91, 126, and 131, the current code returns ValidationResult.valid()
unconditionally, effectively disabling validation. To fix this, reinstate
meaningful validation logic that performs actual checks and returns appropriate
ValidationResult based on the image content. Ensure that failures are handled
properly, either by returning failure results or throwing exceptions as needed,
while still allowing optional paths like animal content validation to pass
gracefully but not bypass all validation. Add proper logging and error handling
to aid troubleshooting without compromising validation integrity.

} 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);
Comment on lines +25 to +26
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

CURRENT_DATE 사용에 대한 시간대 고려가 필요합니다.

CURRENT_DATE는 데이터베이스 서버의 시간대를 기준으로 작동합니다. 애플리케이션과 DB 서버의 시간대가 다를 경우 예상과 다른 결과가 나올 수 있습니다.

시간대 일관성을 확인하는 스크립트를 실행해보세요:


🏁 Script executed:

#!/bin/bash
# Description: 애플리케이션과 DB 시간대 설정 확인

# application.yml에서 시간대 설정 확인
rg -A 3 -B 3 "timezone|time-zone" --type yaml

# Java 코드에서 LocalDate.now() 사용 패턴 확인
rg "LocalDate\.now\(\)" --type java

Length of output: 577


타임존 일관성 확보 필요

현재 JPQL CURRENT_DATE는 DB 서버의 타임존을, LocalDate.now()는 애플리케이션 서버의 타임존을 기준으로 동작하기 때문에 불일치로 인한 데이터 조회 오류가 발생할 수 있습니다. 또한 application.yml에 타임존 설정이 누락되어 있습니다.

수정 제안:

  • 리포지토리 메서드 파라미터화
    CURRENT_DATE 대신 애플리케이션에서 계산한 날짜를 파라미터로 전달하도록 변경합니다.

    - @Query("SELECT v FROM VisionUsage v WHERE v.userId = :userId AND v.usageDate = CURRENT_DATE")
    - Optional<VisionUsage> findTodayUsage(@Param("userId") UUID userId);
    + @Query("SELECT v FROM VisionUsage v WHERE v.userId = :userId AND v.usageDate = :today")
    + Optional<VisionUsage> findUsageByDate(@Param("userId") UUID userId,
    +                                       @Param("today") LocalDate today);

    호출부에서는 LocalDate.now(ZoneId.of("UTC")) 등 애플리케이션 기준 타임존을 명시하여 전달하세요.

  • 애플리케이션 타임존 설정 추가
    src/main/resources/application.yml에 아래 설정을 추가해 애플리케이션과 DB 간 타임존을 통일합니다.

    spring:
      datasource:
        url: jdbc:mysql://<host>:<port>/<db>?serverTimezone=UTC
      jpa:
        properties:
          hibernate:
            jdbc:
              time_zone: UTC
      jackson:
        time-zone: UTC

점검 대상 파일:

  • src/main/java/io/github/petty/vision/repository/VisionUsageRepository.java
  • src/main/java/io/github/petty/vision/service/VisionUsageService.java
  • src/main/resources/application.yml

이와 같이 처리하여 날짜 조회 시 생성되는 타임존 차이를 방지하세요.

🤖 Prompt for AI Agents
In src/main/java/io/github/petty/vision/repository/VisionUsageRepository.java
around lines 25 to 26, replace the JPQL CURRENT_DATE usage with a method
parameter for the date to avoid timezone inconsistencies between the database
and application server. Modify the method signature to accept a LocalDate
parameter representing the date calculated in the application with a consistent
timezone (e.g., UTC). Update the query to use this parameter instead of
CURRENT_DATE. Additionally, ensure src/main/resources/application.yml includes
timezone settings to unify application and database timezones as specified.


/**
* 특정 사용자의 총 사용량 조회
*/
@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);
Comment on lines +37 to +39
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

@Modifying 쿼리에 트랜잭션 관리가 필요합니다.

deleteOldUsageRecords 메서드는 @Modifying 어노테이션을 사용하지만 트랜잭션 컨텍스트에서 실행되어야 합니다. 서비스 레이어에서 @Transactional로 감싸져 있는지 확인이 필요합니다.

서비스에서 이 메서드 호출 시 트랜잭션 처리를 확인해보세요:


🏁 Script executed:

#!/bin/bash
# Description: deleteOldUsageRecords 메서드 사용처에서 트랜잭션 처리 확인

# 해당 메서드를 호출하는 코드 검색
rg -A 5 -B 5 "deleteOldUsageRecords" --type java

# @Transactional 어노테이션과 함께 사용되는지 확인
ast-grep --pattern 'class $_ {
  $$$
  @Transactional
  $$$
  deleteOldUsageRecords($_)
  $$$
}'

Length of output: 2453


서비스 스케줄러 메서드에 트랜잭션 처리 추가 필요

@Modifying 쿼리는 반드시 트랜잭션 컨텍스트에서 실행되어야 합니다. 현재 cleanupOldRecords() 메서드는 @Scheduled만 붙어 있고, 트랜잭션 관리가 적용되지 않은 상태입니다. 아래 위치에 @Transactional 어노테이션을 추가해 주세요.

• 파일: src/main/java/io/github/petty/vision/service/VisionUsageService.java

  • 메서드 cleanupOldRecords()@Transactional 추가

예시 변경(diff):

    @Scheduled(cron = "0 0 2 * * *")
+   @Transactional
    public void cleanupOldRecords() {
        LocalDate cutoffDate = LocalDate.now().minusDays(30);
        visionUsageRepository.deleteOldUsageRecords(cutoffDate);
        log.info("Vision 사용량 기록 정리 완료: {} 이전 데이터 삭제", cutoffDate);
    }
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/vision/service/VisionUsageService.java around
the cleanupOldRecords() method, add the @Transactional annotation to ensure the
method runs within a transaction. This is necessary because the
deleteOldUsageRecords() method in VisionUsageRepository uses @Modifying and
requires a transactional context. Locate the cleanupOldRecords() method and
annotate it with @Transactional above the existing @Scheduled annotation or
method signature.


/**
* 특정 날짜 이후 사용량 기록 조회
*/
@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));
}

/**
* 사용 가능 여부 확인
*/
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.

🛠️ Refactor suggestion

동시성 환경에서의 race condition 방지가 필요합니다.

여러 요청이 동시에 들어올 때 같은 사용자가 한도를 초과하여 사용할 수 있는 race condition이 발생할 수 있습니다. 비관적 락이나 낙관적 락 적용을 고려해보세요.

비관적 락을 사용한 개선안:

 public VisionUsage incrementUsage(UUID userId) {
-    VisionUsage usage = getTodayUsage(userId);
+    // 비관적 락으로 동시성 제어
+    LocalDate today = LocalDate.now();
+    VisionUsage usage = visionUsageRepository.findByUserIdAndUsageDate(userId, today)
+            .orElseGet(() -> createNewUsageRecord(userId, today));
+    
+    // 락을 걸고 다시 조회하여 최신 상태 확인
+    usage = visionUsageRepository.findById(usage.getId())
+            .orElseThrow(() -> new IllegalStateException("사용량 기록을 찾을 수 없습니다."));

     if (!usage.canUse()) {
         throw new IllegalStateException(
                 String.format("일일 사용 한도(%d회)를 초과했습니다. 현재 사용량: %d회",
                         usage.getDailyLimit(), usage.getUsageCount())
         );
     }

또는 리포지토리에 @Lock(LockModeType.PESSIMISTIC_WRITE) 어노테이션을 추가한 메서드를 만들어 사용하세요.

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 around
lines 47 to 64, the incrementUsage method is vulnerable to race conditions in
concurrent environments, allowing usage limits to be exceeded. To fix this,
apply a pessimistic lock by creating a repository method annotated with
@Lock(LockModeType.PESSIMISTIC_WRITE) that fetches the VisionUsage entity with a
write lock. Use this locked method to retrieve the usage before checking and
incrementing usage, ensuring that concurrent requests are serialized and the
usage limit is enforced correctly.


/**
* 남은 사용량 조회
*/
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.

💡 Verification agent

🧩 Analysis chain

스케줄러 설정의 시간대 일관성을 확인하세요.

cron 표현식 "0 0 2 * * *"은 서버의 기본 시간대를 사용합니다. 애플리케이션의 시간대 설정과 일치하는지 확인이 필요합니다.

시간대 설정을 확인해보세요:

만약 특정 시간대를 명시해야 한다면:

-@Scheduled(cron = "0 0 2 * * *")
+@Scheduled(cron = "0 0 2 * * *", zone = "Asia/Seoul")

🏁 Script executed:

#!/bin/bash
# Description: 스케줄러 시간대 설정 확인

# application.yml에서 시간대 관련 설정 확인
rg -A 5 -B 5 "spring\.task\.scheduling|timezone|time-zone" --type yaml

# @Scheduled 어노테이션 사용 패턴 확인
rg -A 3 -B 1 "@Scheduled.*cron" --type java

Length of output: 1182


스케줄러 타임존 설정 일관성 보장 필요

현재 application.yml에는 스케줄러의 기본 시간대를 지정하는 설정이 없고, VisionUsageService@Scheduled에는 zone 옵션이 빠져 있어 다른 스케줄러(SyncService)와 동작 환경이 달라질 수 있습니다.
다음 중 하나를 적용해주세요:

  • 전역 설정을 추가
    spring:
      task:
        scheduling:
          time-zone: Asia/Seoul
  • 또는 해당 애노테이션에 zone 옵션 명시
    --- a/src/main/java/io/github/petty/vision/service/VisionUsageService.java
    +++ b/src/main/java/io/github/petty/vision/service/VisionUsageService.java
       @Scheduled(cron = "0 0 2 * * *", zone = "Asia/Seoul")
       public void cleanupOldRecords() {
           LocalDate cutoffDate = LocalDate.now().minusDays(30);
           visionUsageRepository.deleteOldUsageRecords(cutoffDate);
           log.info("Vision 사용량 기록 정리 완료: {} 이전 데이터 삭제", cutoffDate);
       }

• 대상 파일:

  • src/main/java/io/github/petty/vision/service/VisionUsageService.java
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/vision/service/VisionUsageService.java around
lines 105 to 110, the @Scheduled annotation for cleanupOldRecords lacks an
explicit time zone, which may cause inconsistency with the application's
intended scheduling time zone. Fix this by adding the 'zone' attribute to the
@Scheduled annotation, specifying the correct time zone (e.g., zone =
"Asia/Seoul") to ensure the scheduled task runs consistently with the
application's time zone settings.


/**
* 관리자용: 사용자 일일 한도 조정
*/
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);
}
}
10 changes: 8 additions & 2 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ spring:
import: "optional:classpath:application-secret.yml"

profiles:
active: prod
active: dev
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

프로덕션 프로파일이 dev로 변경됨

활성 프로파일이 prod에서 dev로 변경되었습니다. 이는 의도적인 변경인지 확인이 필요합니다:

  • 개발 환경 설정이 프로덕션에 배포될 위험
  • 디버그 정보 노출 가능성
  • 성능 최적화 미적용

프로덕션 배포 시에는 반드시 prod 프로파일을 사용해야 합니다.

🤖 Prompt for AI Agents
In src/main/resources/application.yml at line 9, the active profile is set to
'dev' instead of 'prod'. Verify if this change is intentional; if the
application is meant for production deployment, revert the active profile back
to 'prod' to avoid exposing debug information, prevent using development
configurations in production, and ensure performance optimizations are applied.


logging:
level:
Expand All @@ -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