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 @@ -3,6 +3,7 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.mr.domain.analysis.entity.Analysis;
import com.mr.domain.analysis.entity.enums.AnalysisStatus;
import com.mr.domain.analysis.service.AnalysisBarCalculator.BarMetrics;
import com.mr.domain.backingtrack.entity.BackingTrack;
import com.mr.domain.playing.entity.MidiEventData;
import com.mr.domain.playing.entity.Playing;
Expand All @@ -29,7 +30,8 @@ public record HistoryDetailResponseDTO(
List<AnalysisSummary> analyses
) {

public static HistoryDetailResponseDTO from(Playing playing, List<Analysis> analyses, String recordingFileUrl) {
public static HistoryDetailResponseDTO from(
Playing playing, List<Analysis> analyses, String recordingFileUrl, BarMetrics barMetrics) {
BackingTrack backingTrack = playing.getBackingTrack();

return new HistoryDetailResponseDTO(
Expand All @@ -46,8 +48,8 @@ public static HistoryDetailResponseDTO from(Playing playing, List<Analysis> anal
backingTrack != null ? backingTrack.getAudioFileUrl() : null,
playing.getMidiData().stream().map(MidiEvent::from).toList(),
backingTrack != null ? backingTrack.getMidiData() : null,
null, // TODO: totalBars - playtimeSec/bpm 기반 추정 가능, 정확도 논의 후 추가
analyses.stream().map(AnalysisSummary::from).toList()
barMetrics != null ? barMetrics.totalBars() : null,
analyses.stream().map(analysis -> AnalysisSummary.from(analysis, barMetrics)).toList()
);
}

Expand Down Expand Up @@ -85,17 +87,27 @@ public record AnalysisSummary(
LocalDateTime createdAt
) {

public static AnalysisSummary from(Analysis analysis) {
public static AnalysisSummary from(Analysis analysis, BarMetrics barMetrics) {
return new AnalysisSummary(
analysis.getId(),
analysis.getStartBar(),
analysis.getEndBar(),
analysis.getStartBar() + "마디-" + analysis.getEndBar() + "마디 분석 리포트",
analysis.getSummary(),
analysis.getStatus(),
null, // TODO: estimatedSeconds 소스 없음
toEstimatedSeconds(analysis, barMetrics),
analysis.getCreatedAt()
);
}

// 분석 구간(startBar~endBar)의 재생 길이 산정
private static Integer toEstimatedSeconds(Analysis analysis, BarMetrics barMetrics) {
if (barMetrics == null) {
return null;
}

int barCount = analysis.getEndBar() - analysis.getStartBar() + 1;
return (int) Math.round(barCount * barMetrics.barDurationMs() / 1_000D);
}
}
}
15 changes: 14 additions & 1 deletion src/main/java/com/mr/domain/history/service/HistoryService.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import com.mr.domain.analysis.entity.Analysis;
import com.mr.domain.analysis.entity.enums.AnalysisStatus;
import com.mr.domain.analysis.repository.AnalysisRepository;
import com.mr.domain.analysis.service.AnalysisBarCalculator;
import com.mr.domain.analysis.service.AnalysisBarCalculator.BarMetrics;
import com.mr.domain.history.dto.req.HistoryPeriod;
import com.mr.domain.history.dto.res.HistoryDetailResponseDTO;
import com.mr.domain.history.dto.res.HistoryListResponseDTO;
Expand Down Expand Up @@ -39,6 +41,7 @@ public class HistoryService {
private final PlayingRepository playingRepository;
private final AnalysisRepository analysisRepository;
private final S3FileService s3FileService;
private final AnalysisBarCalculator analysisBarCalculator;

public HistoryListResponseDTO getHistories(Long userId, int page, int size, HistoryPeriod period) {
validatePaging(page, size);
Expand Down Expand Up @@ -77,7 +80,17 @@ public HistoryDetailResponseDTO getHistoryDetail(Long userId, Long playingId) {
playing.getRecordingObjectKey()
);

return HistoryDetailResponseDTO.from(playing, analyses, recordingFileUrl);
return HistoryDetailResponseDTO.from(
playing, analyses, recordingFileUrl, resolveBarMetrics(playing));
}

// 백킹트랙 정보 불완전 시 조회 실패 대신 마디 관련 필드만 null 처리
private BarMetrics resolveBarMetrics(Playing playing) {
try {
return analysisBarCalculator.calculate(playing);
} catch (GeneralException exception) {
return null;
}
}

private List<Item> buildItems(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import com.mr.domain.analysis.entity.enums.AnalysisGrade;
import com.mr.domain.analysis.entity.enums.AnalysisStatus;
import com.mr.domain.analysis.repository.AnalysisRepository;
import com.mr.domain.analysis.service.AnalysisBarCalculator;
import com.mr.domain.backingtrack.entity.BackingTrack;
import com.mr.domain.history.dto.req.HistoryPeriod;
import com.mr.domain.history.dto.res.HistoryDetailResponseDTO;
Expand Down Expand Up @@ -57,7 +58,8 @@ class HistoryServiceTest {

@BeforeEach
void setUp() {
historyService = new HistoryService(playingRepository, analysisRepository, s3FileService);
historyService = new HistoryService(
playingRepository, analysisRepository, s3FileService, new AnalysisBarCalculator());
}

private Playing mockPlaying(Long playingId, Long userId, PlayingStatus status, LocalDateTime endedAt) {
Expand Down Expand Up @@ -282,6 +284,8 @@ void getHistoryDetail_success_returnsAllAnalysesRegardlessOfStatus() {
given(s3FileService.createPresignedDownload(1L, recordingObjectKey)).willReturn(recordingFileUrl);
given(playing.getBackingTrack()).willReturn(backingTrack);
given(backingTrack.getAudioFileUrl()).willReturn(backingTrackAudioFileUrl);
given(backingTrack.getTimeSignature()).willReturn("4/4");
given(backingTrack.getPlaytimeSec()).willReturn(120);
given(playingRepository.findByIdWithBackingTrack(1L)).willReturn(Optional.of(playing));

Analysis completed = completedAnalysis(1L, 90);
Expand All @@ -301,4 +305,44 @@ void getHistoryDetail_success_returnsAllAnalysesRegardlessOfStatus() {
assertThat(response.analyses()).hasSize(2);
assertThat(response.analyses().get(1).status()).isEqualTo(AnalysisStatus.PENDING);
}

@Test
@DisplayName("getHistoryDetail - BPM/박자/재생시간이 있으면 totalBars와 구간별 estimatedSeconds를 계산한다")
void getHistoryDetail_success_calculatesBarMetrics() {
Playing playing = mockPlaying(1L, 1L, PlayingStatus.COMPLETED, LocalDateTime.now());
BackingTrack backingTrack = mock(BackingTrack.class);
given(playing.getBackingTrack()).willReturn(backingTrack);
given(backingTrack.getTimeSignature()).willReturn("4/4");
given(backingTrack.getPlaytimeSec()).willReturn(120);
given(playingRepository.findByIdWithBackingTrack(1L)).willReturn(Optional.of(playing));

Analysis completed = completedAnalysis(1L, 90);
given(analysisRepository.findByPlayingIdAndUserIdOrderByStartBarAscIdAsc(1L, 1L))
.willReturn(List.of(completed));

HistoryDetailResponseDTO response = historyService.getHistoryDetail(1L, 1L);

// BPM 120, 4/4 -> 마디당 2초, 재생 120초 -> 60마디 / 1~8마디 분석 -> 16초
assertThat(response.totalBars()).isEqualTo(60);
assertThat(response.analyses().get(0).estimatedSeconds()).isEqualTo(16);
}

@Test
@DisplayName("getHistoryDetail - 마디 계산 정보가 없으면 조회는 성공하고 마디 관련 필드만 null이다")
void getHistoryDetail_missingBarMetrics_returnsNullBarFields() {
Playing playing = mockPlaying(1L, 1L, PlayingStatus.COMPLETED, LocalDateTime.now());
BackingTrack backingTrack = mock(BackingTrack.class);
given(playing.getBackingTrack()).willReturn(backingTrack);
given(backingTrack.getPlaytimeSec()).willReturn(null);
given(playingRepository.findByIdWithBackingTrack(1L)).willReturn(Optional.of(playing));

Analysis completed = completedAnalysis(1L, 90);
given(analysisRepository.findByPlayingIdAndUserIdOrderByStartBarAscIdAsc(1L, 1L))
.willReturn(List.of(completed));

HistoryDetailResponseDTO response = historyService.getHistoryDetail(1L, 1L);

assertThat(response.totalBars()).isNull();
assertThat(response.analyses().get(0).estimatedSeconds()).isNull();
}
}
Loading