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
8 changes: 8 additions & 0 deletions src/main/java/com/mr/Application.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
package com.mr;

import jakarta.annotation.PostConstruct;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.scheduling.annotation.EnableScheduling;

import java.util.TimeZone;

@EnableRetry
@EnableScheduling
@EnableJpaAuditing
@SpringBootApplication
public class Application {

@PostConstruct
public void init() {
TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
}

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
package com.mr.domain.analysis.dto.res;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.mr.domain.analysis.entity.Analysis;
import com.mr.domain.analysis.entity.enums.AnalysisStatus;
import java.time.LocalDateTime;
import io.swagger.v3.oas.annotations.media.Schema;

import java.time.Instant;

public record AnalysisCreateResponseDTO(
Long analysisId,
Long playingId,
AnalysisStatus status,
Integer startBar,
Integer endBar,
LocalDateTime createdAt
@Schema(description = "분석 생성 일시 (KST 기준 응답)", example = "2026-08-11T18:00:00", type = "string")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "Asia/Seoul")
Instant createdAt
Comment thread
rkdehdrbs7885-oss marked this conversation as resolved.
) {
public static AnalysisCreateResponseDTO from(Analysis analysis) {
return new AnalysisCreateResponseDTO(analysis.getId(), analysis.getPlaying().getId(), analysis.getStatus(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.mr.domain.analysis.entity.enums.AnalysisStatus;
import com.mr.domain.backingtrack.entity.BackingTrack;
import com.mr.domain.playing.entity.Playing;
Expand All @@ -11,8 +12,10 @@
import com.mr.domain.analysis.entity.enums.ContentFormat;
import com.mr.domain.analysis.entity.enums.LlmStatus;
import com.mr.domain.analysis.entity.enums.ReportGenerationType;
import io.swagger.v3.oas.annotations.media.Schema;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.Instant;
import java.util.Locale;

public record AnalysisResultResponseDTO(
Expand All @@ -22,7 +25,11 @@ public record AnalysisResultResponseDTO(
String genre,
String key,
Integer bpm,
LocalDateTime playedAt,

@Schema(description = "연주 일시 (KST 기준 응답)", example = "2026-08-11T18:00:00", type = "string")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "Asia/Seoul")
Instant playedAt,

String recordingFileUrl,
String backingTrackAudioFileUrl,
AnalysisStatus status,
Expand All @@ -34,8 +41,14 @@ public record AnalysisResultResponseDTO(
DomainScores domainScores,
Report report,
@JsonProperty("result") JsonNode rawResult,
LocalDateTime createdAt,
LocalDateTime completedAt

@Schema(description = "생성 일시 (KST 기준 응답)", example = "2026-08-11T18:00:00", type = "string")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "Asia/Seoul")
Instant createdAt,

@Schema(description = "완료 일시 (KST 기준 응답)", example = "2026-08-11T18:00:00", type = "string")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "Asia/Seoul")
Instant completedAt
) {

public static AnalysisResultResponseDTO from(
Expand Down Expand Up @@ -107,8 +120,14 @@ public record Report(
String content,
String modelName,
String promptVersion,
LocalDateTime createdAt,
LocalDateTime updatedAt

@Schema(description = "리포트 생성 일시 (KST 기준 응답)", example = "2026-08-11T18:00:00", type = "string")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "Asia/Seoul")
Instant createdAt,

@Schema(description = "리포트 수정 일시 (KST 기준 응답)", example = "2026-08-11T18:00:00", type = "string")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "Asia/Seoul")
Instant updatedAt
) {

private static Report fromNullable(AnalysisReport analysisReport) {
Expand All @@ -129,4 +148,4 @@ private static Report fromNullable(AnalysisReport analysisReport) {
);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
package com.mr.domain.analysis.dto.res;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.mr.domain.analysis.entity.Analysis;
import com.mr.domain.analysis.entity.enums.AnalysisStatus;
import java.time.LocalDateTime;
import io.swagger.v3.oas.annotations.media.Schema;

import java.time.Instant;

public record AnalysisStatusResponseDTO(
Long analysisId,
AnalysisStatus status,
Integer progressRate,
String message,
LocalDateTime createdAt,
LocalDateTime completedAt

@Schema(description = "생성 일시 (KST 기준 응답)", example = "2026-08-11T18:00:00", type = "string")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "Asia/Seoul")
Instant createdAt,

@Schema(description = "완료 일시 (KST 기준 응답)", example = "2026-08-11T18:00:00", type = "string")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "Asia/Seoul")
Instant completedAt
) {

public static AnalysisStatusResponseDTO from(
Expand Down
22 changes: 11 additions & 11 deletions src/main/java/com/mr/domain/analysis/entity/Analysis.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import jakarta.persistence.*;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Objects;
import lombok.AccessLevel;
Expand Down Expand Up @@ -92,17 +92,17 @@ public class Analysis extends BaseCreatedEntity {
private String failedReason;

@Column(name = "processing_started_at")
private LocalDateTime processingStartedAt;
private Instant processingStartedAt;
Comment thread
rkdehdrbs7885-oss marked this conversation as resolved.

@Column(name = "completed_at")
private LocalDateTime completedAt;
private Instant completedAt;

@Builder(access = AccessLevel.PRIVATE)
private Analysis(User user, Playing playing, String analysisVersion, Integer startBar, Integer endBar,
AnalysisStatus status, Integer totalScore, AnalysisGrade grade, String summary,
String analysisRequestJson, BigDecimal scaleScore, BigDecimal tensionScore,
BigDecimal progressionScore, BigDecimal voiceLeadingScore, String rawResultJson,
String failedReason, LocalDateTime completedAt) {
String failedReason, Instant completedAt) {
validateUser(user);
validatePlaying(playing);
validateOwnerConsistency(user, playing);
Expand Down Expand Up @@ -172,7 +172,7 @@ private static void validateBarRange(Integer startBar, Integer endBar) {
}
}

public LocalDateTime startProcessing(LocalDateTime now) {
public Instant startProcessing(Instant now) {
if (this.status != AnalysisStatus.PENDING) {
throw new IllegalStateException("PENDING 상태의 분석만 PROCESSING으로 변경할 수 있습니다.");
}
Expand All @@ -181,21 +181,21 @@ public LocalDateTime startProcessing(LocalDateTime now) {
return this.processingStartedAt;
}

public LocalDateTime restartProcessing(LocalDateTime now) {
public Instant restartProcessing(Instant now) {
if (this.status != AnalysisStatus.PROCESSING) {
throw new IllegalStateException("PROCESSING 상태의 분석만 다시 시작할 수 있습니다.");
}
this.processingStartedAt = nextProcessingStartedAt(now);
return this.processingStartedAt;
}

public boolean isCurrentProcessing(LocalDateTime expectedProcessingStartedAt) {
public boolean isCurrentProcessing(Instant expectedProcessingStartedAt) {
return this.status == AnalysisStatus.PROCESSING
&& Objects.equals(this.processingStartedAt, expectedProcessingStartedAt);
}

private LocalDateTime nextProcessingStartedAt(LocalDateTime requestedAt) {
LocalDateTime next = Objects.requireNonNull(requestedAt).truncatedTo(ChronoUnit.MILLIS);
private Instant nextProcessingStartedAt(Instant requestedAt) {
Instant next = Objects.requireNonNull(requestedAt).truncatedTo(ChronoUnit.MILLIS);
if (this.processingStartedAt != null && !next.isAfter(this.processingStartedAt)) {
return this.processingStartedAt.plus(1, ChronoUnit.MILLIS);
}
Expand All @@ -204,7 +204,7 @@ private LocalDateTime nextProcessingStartedAt(LocalDateTime requestedAt) {

public void complete(Integer totalScore, AnalysisGrade grade, String summary, BigDecimal scaleScore,
BigDecimal tensionScore, BigDecimal progressionScore, BigDecimal voiceLeadingScore,
String rawResultJson, LocalDateTime completedAt) {
String rawResultJson, Instant completedAt) {
if (this.status != AnalysisStatus.PROCESSING) {
throw new IllegalStateException("PROCESSING 상태의 분석만 완료 처리할 수 있습니다.");
}
Expand All @@ -220,7 +220,7 @@ public void complete(Integer totalScore, AnalysisGrade grade, String summary, Bi
this.completedAt = Objects.requireNonNull(completedAt);
}

public void fail(String failedReason, LocalDateTime completedAt) {
public void fail(String failedReason, Instant completedAt) {
if (this.status == AnalysisStatus.COMPLETED || this.status == AnalysisStatus.FAILED) {
throw new IllegalStateException("이미 완료된 분석은 실패 처리할 수 없습니다.");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.mr.domain.analysis.model;

import java.time.LocalDateTime;
import java.time.Instant;

public record AnalysisProcessingClaim(
String requestJson,
LocalDateTime processingStartedAt
Instant processingStartedAt
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import com.mr.domain.analysis.entity.Analysis;
import com.mr.domain.analysis.entity.enums.AnalysisStatus;
import jakarta.persistence.LockModeType;
import java.time.LocalDateTime;

import java.time.Instant;
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Pageable;
Expand All @@ -28,7 +29,7 @@ public interface AnalysisRepository extends JpaRepository<Analysis, Long> {
""")
List<Long> findIdsByStatusAndCreatedAtBefore(
@Param("status") AnalysisStatus status,
@Param("cutoff") LocalDateTime cutoff,
@Param("cutoff") Instant cutoff,
Pageable pageable
);

Expand All @@ -39,7 +40,7 @@ List<Long> findIdsByStatusAndCreatedAtBefore(
""")
List<Long> findIdsByStatusAndProcessingStartedAtBefore(
@Param("status") AnalysisStatus status,
@Param("cutoff") LocalDateTime cutoff,
@Param("cutoff") Instant cutoff,
Pageable pageable
);

Expand Down Expand Up @@ -72,7 +73,7 @@ List<Analysis> findByPlayingIdAndUserIdOrderByStartBarAscIdAsc(
List<Analysis> findByUserAndStatusSince(
@Param("userId") Long userId,
@Param("status") AnalysisStatus status,
@Param("since") LocalDateTime since
@Param("since") Instant since
);

@Query("""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import com.mr.domain.analysis.repository.AnalysisRepository;
import java.time.Clock;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.Instant;
import java.util.List;

import com.mr.domain.analysis.service.AnalysisProcessingService;
Expand Down Expand Up @@ -51,15 +51,15 @@ public AnalysisRecoveryScheduler(
fixedDelayString = "${analysis.recovery.fixed-delay-ms:30000}"
)
public void recoverPendingAnalyses() {
LocalDateTime now = LocalDateTime.now(clock);
Instant now = Instant.now(clock);
List<Long> pendingIds = analysisRepository.findIdsByStatusAndCreatedAtBefore(
AnalysisStatus.PENDING,
now.minus(pendingThreshold),
PageRequest.of(0, batchSize)
);
submitPending(pendingIds);

LocalDateTime processingCutoff = now.minus(processingThreshold);
Instant processingCutoff = now.minus(processingThreshold);
Comment thread
rkdehdrbs7885-oss marked this conversation as resolved.
List<Long> processingIds = analysisRepository.findIdsByStatusAndProcessingStartedAtBefore(
AnalysisStatus.PROCESSING,
processingCutoff,
Expand All @@ -79,7 +79,7 @@ private void submitPending(List<Long> analysisIds) {
}
}

private void submitStaleProcessing(List<Long> analysisIds, LocalDateTime cutoff) {
private void submitStaleProcessing(List<Long> analysisIds, Instant cutoff) {
for (Long analysisId : analysisIds) {
try {
taskExecutor.execute(() -> analysisProcessingService.recoverStaleProcessing(analysisId, cutoff));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
import com.mr.domain.analysis.model.GeneratedAnalysisReport;
import com.mr.global.client.ai.AiAnalysisRequest;
import com.mr.global.client.ai.AiServerClient;
import java.time.LocalDateTime;

import java.time.Instant;
import java.util.Optional;
import java.util.function.Supplier;
import lombok.RequiredArgsConstructor;
Expand All @@ -29,7 +30,7 @@ public void process(Long analysisId) {
processClaimed(analysisId, () -> analysisStateService.startProcessing(analysisId));
}

public void recoverStaleProcessing(Long analysisId, LocalDateTime cutoff) {
public void recoverStaleProcessing(Long analysisId, Instant cutoff) {
processClaimed(analysisId, () -> analysisStateService.restartStaleProcessing(analysisId, cutoff));
}

Expand Down
Loading