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 @@ -41,7 +41,8 @@ public record AnalysisResultResponseDTO(
public static AnalysisResultResponseDTO from(
Analysis analysis,
AnalysisReport analysisReport,
JsonNode rawResult
JsonNode rawResult,
String recordingFileUrl
) {
Playing playing = analysis.getPlaying();
BackingTrack backingTrack = playing.getBackingTrack();
Expand All @@ -53,7 +54,7 @@ public static AnalysisResultResponseDTO from(
formatKey(backingTrack),
playing.getBpm(),
playing.getEndedAt(),
playing.getRecordingFileUrl(),
recordingFileUrl,
backingTrack != null ? backingTrack.getAudioFileUrl() : null,
analysis.getStatus(),
analysis.getStartBar(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.mr.domain.playing.exception.PlayingErrorStatus;
import com.mr.domain.playing.repository.PlayingRepository;
import com.mr.global.apipayload.exception.GeneralException;
import com.mr.global.file.s3.service.S3FileService;
import java.util.List;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
Expand All @@ -41,6 +42,7 @@ public class AnalysisService {
private final AnalysisRequestFactory analysisRequestFactory;
private final ApplicationEventPublisher eventPublisher;
private final ObjectMapper objectMapper;
private final S3FileService s3FileService;

@Transactional
public AnalysisCreateResponseDTO createAnalysis(
Expand Down Expand Up @@ -115,10 +117,17 @@ public AnalysisResultResponseDTO getAnalysisResult(
analysis.getRawResultJson()
);

String recordingFileUrl =
s3FileService.createPresignedDownload(
userId,
analysis.getPlaying().getRecordingObjectKey()
);

return AnalysisResultResponseDTO.from(
analysis,
analysisReport,
rawResult
rawResult,
recordingFileUrl
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public record HistoryDetailResponseDTO(
List<AnalysisSummary> analyses
) {

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

return new HistoryDetailResponseDTO(
Expand All @@ -42,7 +42,7 @@ public static HistoryDetailResponseDTO from(Playing playing, List<Analysis> anal
playing.getEndedAt(),
toDurationMinutes(playing.getDurationSec()),
playing.getDurationSec(),
playing.getRecordingFileUrl(),
recordingFileUrl,
backingTrack != null ? backingTrack.getAudioFileUrl() : null,
playing.getMidiData().stream().map(MidiEvent::from).toList(),
backingTrack != null ? backingTrack.getMidiData() : null,
Expand Down
10 changes: 9 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 @@ -12,6 +12,7 @@
import com.mr.domain.playing.entity.enums.PlayingStatus;
import com.mr.domain.playing.repository.PlayingRepository;
import com.mr.global.apipayload.exception.GeneralException;
import com.mr.global.file.s3.service.S3FileService;
import com.mr.global.util.RelativeDateFormatter;
import java.time.LocalDateTime;
import java.util.ArrayList;
Expand All @@ -37,6 +38,7 @@ public class HistoryService {

private final PlayingRepository playingRepository;
private final AnalysisRepository analysisRepository;
private final S3FileService s3FileService;

public HistoryListResponseDTO getHistories(Long userId, int page, int size, HistoryPeriod period) {
validatePaging(page, size);
Expand Down Expand Up @@ -69,7 +71,13 @@ public HistoryDetailResponseDTO getHistoryDetail(Long userId, Long playingId) {
List<Analysis> analyses =
analysisRepository.findByPlayingIdAndUserIdOrderByStartBarAscIdAsc(playingId, userId);

return HistoryDetailResponseDTO.from(playing, analyses);
String recordingFileUrl =
s3FileService.createPresignedDownload(
userId,
playing.getRecordingObjectKey()
);

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

private List<Item> buildItems(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public record AnalysisContextResponse(
Integer totalBars
) {

public static AnalysisContextResponse from(Playing playing, int totalBars) {
public static AnalysisContextResponse from(Playing playing, int totalBars, String recordingFileUrl) {
BackingTrack backingTrack = playing.getBackingTrack();

return new AnalysisContextResponse(
Expand All @@ -38,7 +38,7 @@ public static AnalysisContextResponse from(Playing playing, int totalBars) {
playing.getEndedAt(),
toDurationMinutes(playing.getDurationSec()),
playing.getDurationSec(),
playing.getRecordingFileUrl(),
recordingFileUrl,
backingTrack.getAudioFileUrl(),
playing.getMidiData().stream().map(MidiEvent::from).toList(),
backingTrack.getMidiData(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public record PlayingDetailResponse(
BackingTrackInfo backingTrack,
LocalDateTime createdAt
) {
public static PlayingDetailResponse from(Playing playing) {
public static PlayingDetailResponse from(Playing playing, String recordingFileUrl) {
return new PlayingDetailResponse(
playing.getId(),
playing.getStatus(),
Expand All @@ -30,7 +30,7 @@ public static PlayingDetailResponse from(Playing playing) {
playing.getEndedAt(),
playing.getDurationSec(),
playing.getBpm(),
playing.getRecordingFileUrl(),
recordingFileUrl,
playing.isPublic(),
BackingTrackInfo.from(playing.getBackingTrack()),
playing.getCreatedAt()
Expand Down
18 changes: 13 additions & 5 deletions src/main/java/com/mr/domain/playing/entity/Playing.java
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,8 @@ public class Playing extends BaseCreatedDeletedEntity {
@Column(name = "ended_at")
private LocalDateTime endedAt;

// 사용자가 연주한 녹음 파일 URL
@Column(name = "recording_file_url", length = 255)
private String recordingFileUrl;
@Column(name = "recording_object_key", length = 255)
private String recordingObjectKey;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@Column(name = "duration_sec")
private Integer durationSec;
Expand Down Expand Up @@ -190,7 +189,7 @@ public static Playing createBackingTrack(
// 연주 완료 시 전체 MIDI 데이터를 저장하고 완료 상태로 전환
public void completeWithMidiData(
List<MidiEventData> requestedMidiData,
String recordingFileUrl
String recordingObjectKey
) {
validateCompletableStatus();
validateMidiData(requestedMidiData);
Expand All @@ -200,12 +199,13 @@ public void completeWithMidiData(

List<MidiEventData> normalizedMidiData = normalizeMidiData(requestedMidiData, savedDurationMs);
validateNormalizedMidiData(normalizedMidiData);
validateRecordingObjectKey(recordingObjectKey);

this.midiData = new ArrayList<>(normalizedMidiData);
this.endedAt = this.startedAt.plusNanos(savedDurationMs * 1_000_000L);
this.durationSec = Math.toIntExact(
Duration.ofMillis(savedDurationMs).toSeconds());
this.recordingFileUrl = recordingFileUrl;
this.recordingObjectKey = recordingObjectKey;
this.status = PlayingStatus.COMPLETED;
}

Expand All @@ -215,6 +215,14 @@ public void validatePlayingOwner(Long userId) {
}
}

private void validateRecordingObjectKey(String recordingObjectKey) {
if (recordingObjectKey == null || recordingObjectKey.isBlank()) {
throw new GeneralException(
PlayingErrorStatus.INVALID_RECORDING_OBJECT_KEY
);
}
}

public void validateCompleted() {
if (this.status != PlayingStatus.COMPLETED) {
throw new GeneralException(PlayingErrorStatus.PLAYING_NOT_COMPLETED);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public enum PlayingErrorStatus implements BaseCode {
MISSING_PLAYING_STATUS(HttpStatus.BAD_REQUEST, "PLAYING_400_12", "연주 상태는 필수 입력값입니다."),
UNSUPPORTED_PLAYING_MODE(HttpStatus.BAD_REQUEST, "PLAYING_400_13", "현재 지원하지 않는 연주 모드입니다."),
INVALID_PLAYING_ID(HttpStatus.BAD_REQUEST, "PLAYING_400_14", "연주 ID가 올바르지 않습니다."),
INVALID_RECORDING_OBJECT_KEY(HttpStatus.BAD_REQUEST," PLAYING_400_15", "유효하지 않은 녹음 파일 Object Key입니다."),
PLAYING_ACCESS_DENIED(HttpStatus.FORBIDDEN, "PLAYING_403_01", "해당 연주에 대한 접근 권한이 없습니다."),
BACKING_TRACK_ACCESS_FORBIDDEN(HttpStatus.FORBIDDEN, "PLAYING_403_02", "해당 백킹트랙으로 연주를 시작할 수 없습니다."),
PLAYING_NOT_FOUND(HttpStatus.NOT_FOUND, "PLAYING_404_01", "연주 세션을 찾을 수 없습니다."),
Expand Down
18 changes: 15 additions & 3 deletions src/main/java/com/mr/domain/playing/service/PlayingService.java
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ public MidiEventSaveResponse saveMidiEvents(

playing.completeWithMidiData(
midiEvents,
recording.fileUrl()
recording.objectKey()
);

publishPracticeMilestoneNotification(userId, playing);
Expand All @@ -150,7 +150,13 @@ public PlayingDetailResponse getPlayingDetail(Long userId, Long playingId) {
playing.validatePlayingOwner(userId);
playing.validateCompleted();

return PlayingDetailResponse.from(playing);
String recordingFileUrl =
s3FileService.createPresignedDownload(
userId,
playing.getRecordingObjectKey()
);

return PlayingDetailResponse.from(playing, recordingFileUrl);
}

@Transactional(readOnly = true)
Expand All @@ -166,8 +172,14 @@ public AnalysisContextResponse getAnalysisContext(Long userId, Long playingId) {
throw new GeneralException(PlayingErrorStatus.BACKING_TRACK_NOT_FOUND);
}

String recordingFileUrl =
s3FileService.createPresignedDownload(
userId,
playing.getRecordingObjectKey()
);

int totalBars = analysisBarCalculator.calculate(playing).totalBars();
return AnalysisContextResponse.from(playing, totalBars);
return AnalysisContextResponse.from(playing, totalBars, recordingFileUrl);
}

@Transactional
Expand Down
1 change: 0 additions & 1 deletion src/main/java/com/mr/global/file/s3/dto/ValidatedFile.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
// S3 업로드 완료 검증 결과
public record ValidatedFile(
String objectKey,
String fileUrl,
Long fileSize,
String contentType
) {
Expand Down
47 changes: 0 additions & 47 deletions src/main/java/com/mr/global/file/s3/service/S3FileService.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest;
import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest;

import java.net.URI;
import java.time.Instant;
import java.util.Map;

Expand Down Expand Up @@ -141,7 +140,6 @@ public ValidatedFile validateUploadedFile(

return new ValidatedFile(
objectKey,
buildFileUrl(objectKey),
headObject.contentLength(),
normalizedContentType
);
Expand Down Expand Up @@ -218,43 +216,6 @@ public void deleteObject(String objectKey) {
}
}

public String extractObjectKey(
String storedFileValue
) {
if (storedFileValue == null
|| storedFileValue.isBlank()) {
throw new GeneralException(
S3ErrorStatus.INVALID_OBJECT_KEY
);
}

if (!storedFileValue.startsWith("http://")
&& !storedFileValue.startsWith("https://")) {
return storedFileValue;
}

try {
String path =
URI.create(storedFileValue)
.getPath();

if (path == null || path.isBlank()) {
throw new GeneralException(
S3ErrorStatus.INVALID_OBJECT_KEY
);
}

return path.startsWith("/")
? path.substring(1)
: path;

} catch (IllegalArgumentException exception) {
throw new GeneralException(
S3ErrorStatus.INVALID_OBJECT_KEY
);
}
}

private HeadObjectResponse getHeadObject(String objectKey) {
HeadObjectRequest request = HeadObjectRequest.builder()
.bucket(s3Properties.bucket())
Expand Down Expand Up @@ -346,12 +307,4 @@ private void validateObjectKey(
throw new GeneralException(S3ErrorStatus.INVALID_OBJECT_KEY);
}
}

private String buildFileUrl(String objectKey) {
return s3Client.utilities()
.getUrl(builder -> builder
.bucket(s3Properties.bucket())
.key(objectKey))
.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
-- 1. 컬럼명 변경
ALTER TABLE playing
RENAME COLUMN recording_file_url TO recording_object_key;

-- 2. URL에서 S3 Object Key만 추출
UPDATE playing
SET recording_object_key = regexp_replace(
recording_object_key,
'^(https?://[^/]+/)?([^?#]*).*$',
'\2'
)
WHERE recording_object_key IS NOT NULL
AND btrim(recording_object_key) <> '';

-- 3. 검증: 실패 시 예외 발생 → Flyway가 트랜잭션 롤백 처리
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM playing
WHERE recording_object_key IS NOT NULL
AND (
recording_object_key LIKE '%?%'
OR recording_object_key LIKE 'http%'
OR recording_object_key LIKE '%X-Amz%'
OR btrim(recording_object_key) = ''
)
) THEN
RAISE EXCEPTION 'recording_object_key migration validation failed';
END IF;
END
$$;
Loading
Loading