From 41c4634be1e90d8c75fe44b6308dc7fc464ded34 Mon Sep 17 00:00:00 2001 From: ownue Date: Sat, 8 Aug 2026 18:53:41 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=ED=9E=88=EC=8A=A4=ED=86=A0=EB=A6=AC?= =?UTF-8?q?=20=EC=83=81=EC=84=B8=20=EC=9D=91=EB=8B=B5=EC=9D=98=20totalBars?= =?UTF-8?q?=EC=99=80=20estimatedSeconds=20=EA=B3=84=EC=82=B0=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=20(#167)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/res/HistoryDetailResponseDTO.java | 21 ++++++--- .../history/service/HistoryService.java | 14 +++++- .../history/service/HistoryServiceTest.java | 45 ++++++++++++++++++- 3 files changed, 73 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/mr/domain/history/dto/res/HistoryDetailResponseDTO.java b/src/main/java/com/mr/domain/history/dto/res/HistoryDetailResponseDTO.java index dc3569ee..c45168f8 100644 --- a/src/main/java/com/mr/domain/history/dto/res/HistoryDetailResponseDTO.java +++ b/src/main/java/com/mr/domain/history/dto/res/HistoryDetailResponseDTO.java @@ -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; @@ -29,7 +30,7 @@ public record HistoryDetailResponseDTO( List analyses ) { - public static HistoryDetailResponseDTO from(Playing playing, List analyses) { + public static HistoryDetailResponseDTO from(Playing playing, List analyses, BarMetrics barMetrics) { BackingTrack backingTrack = playing.getBackingTrack(); return new HistoryDetailResponseDTO( @@ -46,8 +47,8 @@ public static HistoryDetailResponseDTO from(Playing playing, List 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() ); } @@ -85,7 +86,7 @@ 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(), @@ -93,9 +94,19 @@ public static AnalysisSummary from(Analysis analysis) { 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); + } } } diff --git a/src/main/java/com/mr/domain/history/service/HistoryService.java b/src/main/java/com/mr/domain/history/service/HistoryService.java index 91069076..c71581e2 100644 --- a/src/main/java/com/mr/domain/history/service/HistoryService.java +++ b/src/main/java/com/mr/domain/history/service/HistoryService.java @@ -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; @@ -37,6 +39,7 @@ public class HistoryService { private final PlayingRepository playingRepository; private final AnalysisRepository analysisRepository; + private final AnalysisBarCalculator analysisBarCalculator; public HistoryListResponseDTO getHistories(Long userId, int page, int size, HistoryPeriod period) { validatePaging(page, size); @@ -69,7 +72,16 @@ public HistoryDetailResponseDTO getHistoryDetail(Long userId, Long playingId) { List analyses = analysisRepository.findByPlayingIdAndUserIdOrderByStartBarAscIdAsc(playingId, userId); - return HistoryDetailResponseDTO.from(playing, analyses); + return HistoryDetailResponseDTO.from(playing, analyses, resolveBarMetrics(playing)); + } + + // 백킹트랙 정보 불완전 시 조회 실패 대신 마디 관련 필드만 null 처리 + private BarMetrics resolveBarMetrics(Playing playing) { + try { + return analysisBarCalculator.calculate(playing); + } catch (GeneralException exception) { + return null; + } } private List buildItems( diff --git a/src/test/java/com/mr/domain/history/service/HistoryServiceTest.java b/src/test/java/com/mr/domain/history/service/HistoryServiceTest.java index bc2c6e75..81b7d3a9 100644 --- a/src/test/java/com/mr/domain/history/service/HistoryServiceTest.java +++ b/src/test/java/com/mr/domain/history/service/HistoryServiceTest.java @@ -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; @@ -53,7 +54,7 @@ class HistoryServiceTest { @BeforeEach void setUp() { - historyService = new HistoryService(playingRepository, analysisRepository); + historyService = new HistoryService(playingRepository, analysisRepository, new AnalysisBarCalculator()); } private Playing mockPlaying(Long playingId, Long userId, PlayingStatus status, LocalDateTime endedAt) { @@ -276,6 +277,8 @@ void getHistoryDetail_success_returnsAllAnalysesRegardlessOfStatus() { given(playing.getRecordingFileUrl()).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); @@ -293,4 +296,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(); + } } From 58170f5582db1ff11840b9de825dec41330c970a Mon Sep 17 00:00:00 2001 From: ownue Date: Sun, 9 Aug 2026 00:47:30 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20application=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EB=B3=B5=EC=9B=90=20(#167)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.yml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 56dc1945..d3256ff4 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -47,7 +47,10 @@ jwt: # 소셜 로그인 API 연동 oauth: - frontend-redirect-uri: ${OAUTH_FRONTEND_REDIRECT_URI:http://localhost:5173/oauth/callback} + frontend-redirect-uris: + - http://localhost:5173/oauth/callback + - https://musereview-sigma.vercel.app/oauth/callback + temp-code-ttl: ${OAUTH_TEMP_CODE_TTL:2m} cookie: secure: ${OAUTH_COOKIE_SECURE:false} @@ -71,8 +74,20 @@ external: aws: s3: + credentials: + access-key: ${AWS_ACCESS_KEY_ID} + secret-key: ${AWS_SECRET_ACCESS_KEY} bucket: ${AWS_S3_BUCKET:} region: ${AWS_REGION:ap-northeast-2} + presigned-url-expiration: 10m + max-file-size: 31457280 # 30MB + key-prefix: recordings + allowed-content-types: + - audio/mpeg + - audio/wav + - audio/x-wav + - audio/webm + - audio/ogg # 내부 AI 분석 서버 설정