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
6 changes: 4 additions & 2 deletions infra/db/ERD.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@ erDiagram
varchar(30) name
varchar(30) genre "nullable"
varchar(50) atmosphere "nullable"
varchar(500) prompt "nullable"
varchar(512) result_s3_url "nullable"
varchar(100) task_id UK "nullable"
int duration_seconds "nullable"
double duration_seconds "nullable"
varchar(20) status
datetime created_at
}
Expand Down Expand Up @@ -90,8 +91,9 @@ AI 음악 생성 결과. 수정(재생성)하면 원본을 바꾸지 않고 `par
| name | 곡 제목 (기본값 "나의 허밍곡") |
| genre | 장르 |
| atmosphere | 분위기 (API의 `mood`) |
| prompt | 사용자가 입력한 스타일 프롬프트 (선택, 최대 500자). 재생성 시 재사용 |
| result_s3_url | 생성된 오디오 S3 URL. 완료 전에는 NULL |
| task_id | 비동기 작업 ID (유니크) |
| duration_seconds | 곡 길이 (초) |
| duration_seconds | 곡 길이 (초). 요청 시에는 요청한 길이, AI 완료 콜백 이후에는 AI가 보고한 실제 길이 |
| status | `PROCESSING` / `COMPLETED` / `FAILED` / `CANCELED` |
| created_at | 요청 시각 |
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public ApiResponse<MusicGenerationDTO.SongListResponse> getMySongs(
List<MusicGenerationDTO.SongItemResponse> songItems = songPage.getContent().stream()
.map(song -> {
double hummingDuration = song.getMelodyScore().getHumming().getDurationSeconds();
int duration = song.getDurationSeconds() != null ? song.getDurationSeconds() : 30;
double duration = song.getDurationSeconds() != null ? song.getDurationSeconds() : 30.0;
return MusicGenerationDTO.SongItemResponse.of(song, hummingDuration, duration);
})
.collect(Collectors.toList());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ public interface GenerationControllerDocs {
"- `humming_id`: 벡터화가 완료된 허밍의 ID (필수)\n" +
"- `title`: 생성할 곡 제목 (생략 시 기본값 '나의 허밍곡' 사용)\n" +
"- `genre`: 원하는 장르 (예: pop, jazz, classical, hiphop)\n" +
"- `mood`: 원하는 분위기 (예: upbeat, calm, romantic, sad)")
"- `mood`: 원하는 분위기 (예: upbeat, calm, romantic, sad)\n" +
"- `prompt`: 원하는 스타일을 직접 서술하는 문장 (선택, 최대 500자)\n\n" +
"곡 길이는 서버 설정값(기본 30초)으로 고정되며, 멜로디가 이 길이를 넘으면 `HUMMING4001`(400)로 거부됩니다.")
@RequestBody(
required = true,
content = @Content(
Expand All @@ -34,7 +36,8 @@ public interface GenerationControllerDocs {
" \"humming_id\": 1,\n" +
" \"title\": \"나의 첫 번째 곡\",\n" +
" \"genre\": \"pop\",\n" +
" \"mood\": \"upbeat\"\n" +
" \"mood\": \"upbeat\",\n" +
" \"prompt\": \"잔잔한 피아노 위주로, 후반부에 스트링이 들어오는 느낌\"\n" +
"}"
)
)
Expand Down Expand Up @@ -69,10 +72,14 @@ public interface GenerationControllerDocs {
)
)
),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "요청 값 검증 실패",
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "요청 값 검증 실패 또는 허밍 길이 초과",
content = @Content(schema = @Schema(implementation = ApiResponse.class),
examples = @ExampleObject(name = "검증 실패",
value = "{\"isSuccess\":false, \"code\":\"COMMON400\", \"message\":\"잘못된 요청입니다.\", \"result\":{\"humming_id\":\"humming_id는 필수입니다.\"}}"))),
examples = {
@ExampleObject(name = "검증 실패",
value = "{\"isSuccess\":false, \"code\":\"COMMON400\", \"message\":\"잘못된 요청입니다.\", \"result\":{\"humming_id\":\"humming_id는 필수입니다.\"}}"),
@ExampleObject(name = "허밍 길이 초과",
value = "{\"isSuccess\":false, \"code\":\"HUMMING4001\", \"message\":\"허밍은 최대 30초까지 가능합니다.\", \"result\":null}")
})),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "멜로디 악보 없음",
content = @Content(schema = @Schema(implementation = ApiResponse.class),
examples = @ExampleObject(name = "악보 없음",
Expand All @@ -91,7 +98,7 @@ ApiResponse<MusicGenerationDTO.TaskAcceptedResponse> generateSong(
"- `progress`: 진행률 업데이트 (status: PROCESSING, progress: 0~90)\n" +
"- `complete`: 작업 완료 시 전송 (status: COMPLETED / FAILED)\n\n" +
"**progress 이벤트 예시:** `{\"task_id\": \"task_xxx\", \"status\": \"PROCESSING\", \"progress\": 50}`\n\n" +
"**complete 이벤트 예시:** `{\"status\": \"COMPLETED\", \"result\": {\"task_id\": \"task_xxx\", \"song_id\": 42, \"audio_url\": \"https://...\", \"duration_seconds\": 30}}`"
"**complete 이벤트 예시:** `{\"status\": \"COMPLETED\", \"result\": {\"task_id\": \"task_xxx\", \"song_id\": 42, \"audio_url\": \"https://...\", \"duration_seconds\": 30.0}}`"
)
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "스트림 연결 성공"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

import java.time.LocalDateTime;
import java.util.List;
Expand All @@ -25,7 +26,11 @@ public record SongCreateRequest(
String genre,

@Schema(description = "분위기 (예: upbeat, calm, romantic, sad 등)", example = "upbeat")
String mood
String mood,

@Schema(description = "원하는 스타일을 직접 서술하는 프롬프트 (선택, 최대 500자)", example = "잔잔한 피아노 위주로, 후반부에 스트링이 들어오는 느낌")
@Size(max = 500, message = "prompt는 최대 500자까지 입력할 수 있습니다.")
String prompt
) {
//DTO를 MusicGeneration 엔티티로 변환하는 매핑 메서드
public MusicGeneration from(Member member, MelodyScore melodyScore) {
Expand All @@ -36,6 +41,7 @@ public MusicGeneration from(Member member, MelodyScore melodyScore) {
.name(this.title)
.genre(this.genre)
.atmosphere(this.mood)
.prompt(this.prompt == null || this.prompt.isBlank() ? null : this.prompt)
.build();
}
}
Expand Down Expand Up @@ -72,6 +78,7 @@ public MusicGeneration from(Member member, MelodyScore melodyScore,
.name(parentGeneration.getName() + " (수정본)")
.genre(parentGeneration.getGenre())
.atmosphere(parentGeneration.getAtmosphere())
.prompt(parentGeneration.getPrompt())
.build();
}
}
Expand Down Expand Up @@ -99,8 +106,8 @@ public record CompletionResult(
@Schema(description = "생성된 오디오 S3 URL", example = "https://humix-bucket.s3.amazonaws.com/audio/abc123_variation.wav")
@JsonProperty("audio_url") String audioUrl,

@Schema(description = "생성된 오디오 재생 시간 (초)", example = "30")
@JsonProperty("duration_seconds") int durationSeconds
@Schema(description = "생성된 오디오 재생 시간 (초)", example = "30.0")
@JsonProperty("duration_seconds") double durationSeconds
) {}

//SSE Event: complete 데이터 구조
Expand Down Expand Up @@ -145,8 +152,8 @@ public record SongItemResponse(
@Schema(description = "원본 허밍 재생 시간 (초)", example = "12.5")
@JsonProperty("humming_duration_seconds") double hummingDurationSeconds,

@Schema(description = "생성된 오디오 재생 시간 (초)", example = "30")
@JsonProperty("duration_seconds") int durationSeconds,
@Schema(description = "생성된 오디오 재생 시간 (초)", example = "30.0")
@JsonProperty("duration_seconds") double durationSeconds,

@Schema(description = "생성된 오디오 S3 URL", example = "https://humix-bucket.s3.amazonaws.com/audio/abc123_variation.wav")
@JsonProperty("audio_url") String audioUrl,
Expand All @@ -156,7 +163,7 @@ public record SongItemResponse(
) {
//엔티티와 외부 조회 데이터를 조합하여 목록용 단일 객체로 변환하는 메서드
public static SongItemResponse of(MusicGeneration musicGeneration, double hummingDuration,
int durationSeconds) {
double durationSeconds) {
return new SongItemResponse(
musicGeneration.getId(),
musicGeneration.getName(), //엔티티의 name을 title 필드로 매핑
Expand Down Expand Up @@ -202,6 +209,9 @@ public static SongListResponse of(long totalCount, int currentPage,
//AI 서버 작업 완료 콜백 Request Body
public record AiTaskCompletionRequest(
@NotBlank(message = "generated_audio_url은 필수입니다.")
@JsonProperty("generated_audio_url") String generatedAudioUrl
@JsonProperty("generated_audio_url") String generatedAudioUrl,

// AI가 실제로 만든 오디오 길이(초). 실패 콜백이나 아직 이 필드를 보내지 않는 AI 서버에서는 null
@JsonProperty("duration_seconds") Double durationSeconds
) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,13 @@ public class MusicGeneration {
@Column(name = "task_id", length = 100, unique = true)
private String taskId;

// 사용자가 입력한 스타일 프롬프트 (선택). 재생성 시 재사용한다.
@Column(name = "prompt", length = 500)
private String prompt;

// 요청 시에는 요청한 길이, AI 완료 콜백 이후에는 AI가 보고한 실제 길이
@Column(name = "duration_seconds")
private Integer durationSeconds;
private Double durationSeconds;

@Enumerated(EnumType.STRING)
@Column(name = "status", length = 20, nullable = false)
Expand All @@ -63,13 +68,15 @@ public class MusicGeneration {

@Builder
public MusicGeneration(Member member, MelodyScore melodyScore, MusicGeneration parentGeneration,
String name, String genre, String atmosphere, String taskId, Integer durationSeconds) {
String name, String genre, String atmosphere, String prompt, String taskId,
Double durationSeconds) {
this.member = member;
this.melodyScore = melodyScore;
this.parentGeneration = parentGeneration;
this.name = name != null ? name : "나의 허밍곡"; // Default 값 설정
this.genre = genre;
this.atmosphere = atmosphere;
this.prompt = prompt;
this.taskId = taskId;
this.durationSeconds = durationSeconds;
this.status = GenerationStatus.PROCESSING; // 초기 상태 설정
Expand All @@ -92,7 +99,7 @@ public void updateTaskId(String taskId) {
}

// 재생 시간 업데이트 메서드
public void updateDuration(Integer durationSeconds) {
public void updateDuration(Double durationSeconds) {
this.durationSeconds = durationSeconds;
}
}
Loading
Loading