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 @@ -8,6 +8,7 @@
import com.humix.api.global.apiPayload.exception.GeneralException;
import com.humix.api.global.security.dto.TokenDto;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseCookie;
Expand All @@ -30,7 +31,7 @@ public class AuthController implements AuthControllerDocs {
@PostMapping("/guest-login")
@Override
public ApiResponse<MemberDTO.MemberResponse> guestLogin(
@RequestBody MemberDTO.MemberRequest request,
@Valid @RequestBody MemberDTO.MemberRequest request,
HttpServletResponse response) { // HttpServletResponse 주입받기

// 토큰 발급
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ public interface AuthControllerDocs {
@Operation(summary = "게스트 로그인 API", description = "최초 접속 시 발급한 Device ID로 로그인하여 토큰을 발급받습니다.")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "성공"),
@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\":null}")))
examples = @ExampleObject(name = "검증 실패",
value = "{\"isSuccess\":false, \"code\":\"COMMON400\", \"message\":\"잘못된 요청입니다.\", \"result\":{\"device_id\":\"device_id는 필수입니다.\"}}")))
})
@PostMapping("/guest-login")
ApiResponse<MemberDTO.MemberResponse> guestLogin(@RequestBody MemberDTO.MemberRequest request,
Expand All @@ -34,7 +34,11 @@ ApiResponse<MemberDTO.MemberResponse> guestLogin(@RequestBody MemberDTO.MemberRe
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "인증 만료",
content = @Content(schema = @Schema(implementation = ApiResponse.class),
examples = @ExampleObject(name = "리프레시 토큰 만료",
value = "{\"isSuccess\":false, \"code\":\"AUTH401\", \"message\":\"리프레시 토큰이 만료되었습니다. 다시 로그인해주세요.\", \"result\":null}")))
value = "{\"isSuccess\":false, \"code\":\"AUTH4001\", \"message\":\"리프레시 토큰이 만료되었습니다. 다시 로그인해주세요.\", \"result\":null}"))),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "사용자 없음",
content = @Content(schema = @Schema(implementation = ApiResponse.class),
examples = @ExampleObject(name = "사용자 없음",
value = "{\"isSuccess\":false, \"code\":\"MEMBER4000\", \"message\":\"해당하는 사용자가 존재하지 않습니다.\", \"result\":null}")))
})
@PostMapping("/silent-refresh")
ApiResponse<MemberDTO.MemberResponse> silentRefresh(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.humix.api.global.apiPayload.ApiResponse;
import com.humix.api.global.apiPayload.code.GeneralSuccessCode;
import com.humix.api.global.security.userdetails.CustomUserDetails;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
Expand All @@ -31,7 +32,7 @@ public ApiResponse<MelodyScoreDTO.MelodyVectorResponse> convertHummingToVector(@
public ApiResponse<MelodyScoreDTO.MelodyVectorResponse> updateHummingVector(
@AuthenticationPrincipal CustomUserDetails userDetails,
@PathVariable("humming_id") Long hummingId,
@RequestBody MelodyScoreDTO.MelodyUpdateRequest request) {
@Valid @RequestBody MelodyScoreDTO.MelodyUpdateRequest request) {

MelodyScoreDTO.MelodyVectorResponse result = hummingService.updateHummingVector(hummingId, request);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,23 @@ public interface HummingControllerDocs {
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "오디오 데이터 없음",
content = @Content(schema = @Schema(implementation = ApiResponse.class),
examples = @ExampleObject(name = "허밍 없음",
value = "{\"isSuccess\":false, \"code\":\"HUMMING404\", \"message\":\"해당 허밍 데이터를 찾을 수 없습니다.\", \"result\":null}")))
value = "{\"isSuccess\":false, \"code\":\"HUMMING4000\", \"message\":\"해당하는 허밍 기록이 존재하지 않습니다.\", \"result\":null}")))
})
@PostMapping("/{humming_id}/vectors")
ApiResponse<MelodyScoreDTO.MelodyVectorResponse> convertHummingToVector(@AuthenticationPrincipal CustomUserDetails userDetails,
@PathVariable("humming_id") Long hummingId);

@Operation(summary = "사용자 수정 멜로디 벡터 저장 API", description = "사용자가 웹 에디터에서 가공한 멜로디 노트 데이터를 업데이트합니다.")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "성공")
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", 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\":{\"notes\":\"notes는 필수입니다.\"}}"))),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "멜로디 악보 없음",
content = @Content(schema = @Schema(implementation = ApiResponse.class),
examples = @ExampleObject(name = "악보 없음",
value = "{\"isSuccess\":false, \"code\":\"MELODY4000\", \"message\":\"해당 허밍의 멜로디 악보가 존재하지 않습니다.\", \"result\":null}")))
})
@PutMapping("/{humming_id}/vectors")
ApiResponse<MelodyScoreDTO.MelodyVectorResponse> updateHummingVector(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import com.humix.api.domain.humming.entity.Humming;
import com.humix.api.domain.member.entity.Member;
import jakarta.validation.constraints.NotBlank;
import java.time.LocalDateTime;

public class HummingDTO {
//허밍 정보 저장 요청 Request
public record HummingSaveRequest(
@NotBlank(message = "file_key는 필수입니다.")
@JsonProperty("file_key") String fileKey,
@JsonProperty("duration_seconds") int durationSeconds
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import com.humix.api.domain.melodyScore.dto.MelodyScoreDTO;
import com.humix.api.domain.melodyScore.entity.MelodyScore;
import com.humix.api.domain.melodyScore.repository.MelodyScoreRepository;
import com.humix.api.global.apiPayload.code.GeneralErrorCode;
import com.humix.api.global.apiPayload.exception.GeneralException;
import lombok.RequiredArgsConstructor;
import org.jspecify.annotations.NonNull;
import org.springframework.beans.factory.annotation.Value;
Expand Down Expand Up @@ -44,7 +46,7 @@ public class HummingService {
public MelodyScoreDTO.MelodyVectorResponse convertHummingToVector(Long hummingId) {
// 1. 데이터베이스로부터 허밍 엔티티 및 적재된 S3 오디오 주소를 로드합니다.
Humming humming = hummingRepository.findById(hummingId)
.orElseThrow(() -> new IllegalArgumentException("해당 허밍 기록이 존재하지 않습니다. ID: " + hummingId));
.orElseThrow(() -> new GeneralException(GeneralErrorCode.HUMMING_NOT_FOUND));

// 2. S3 URL을 가져와 비공개 버킷인 경우도 접근 가능하도록 임시 Presigned GET URL을 생성합니다.
String presignedGetUrl = generatePresignedGetUrl(humming.getS3FileUrl());
Expand Down Expand Up @@ -148,11 +150,7 @@ public MelodyScoreDTO.MelodyVectorResponse updateHummingVector(Long hummingId, M

// 1. 기존 데이터베이스에 존재하는 멜로디 악보 레코드를 식별 조회합니다.
MelodyScore existingMelodyScore = melodyScoreRepository.findByHummingId(hummingId)
.orElseThrow(() -> new IllegalArgumentException("해당 허밍의 추출된 멜로디 벡터(MelodyScore)가 존재하지 않습니다. ID: " + hummingId));

if (request == null || request.notes() == null) {
throw new IllegalArgumentException("수정할 노트(notes) 데이터가 비어있습니다.");
}
.orElseThrow(() -> new GeneralException(GeneralErrorCode.MELODY_SCORE_NOT_FOUND));

try {
// 2. 프론트에서 넘어온 수정 노트 리스트를 JSON 문자열로 가공합니다.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.humix.api.domain.humming.entity.Humming;
import com.humix.api.domain.melodyScore.entity.MelodyScore;
import jakarta.validation.constraints.NotNull;
import java.util.List;

public class MelodyScoreDTO {
Expand All @@ -21,6 +22,7 @@ public record NoteDto(
//멜로디 벡터 수정 요청 Request Body
public record MelodyUpdateRequest(
@JsonProperty("humming_id") Long hummingId,
@NotNull(message = "notes는 필수입니다.")
List<NoteDto> notes
) {
public MelodyScore from(Humming humming) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.humix.api.global.apiPayload.ApiResponse;
import com.humix.api.global.apiPayload.code.GeneralSuccessCode;
import com.humix.api.global.security.userdetails.CustomUserDetails;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
Expand All @@ -22,7 +23,7 @@ public class GenerationController implements GenerationControllerDocs {
@Override
public ApiResponse<MusicGenerationDTO.TaskAcceptedResponse> generateSong(
@AuthenticationPrincipal CustomUserDetails userDetails,
@RequestBody MusicGenerationDTO.SongCreateRequest request) {
@Valid @RequestBody MusicGenerationDTO.SongCreateRequest request) {

MusicGenerationDTO.TaskAcceptedResponse result = musicGenerationService.generateSong(userDetails, request);
return ApiResponse.onSuccess(GeneralSuccessCode.ACCEPTED, result);
Expand All @@ -49,7 +50,7 @@ public ApiResponse<MusicGenerationDTO.TaskCancelResponse> cancelTask(@PathVariab
public ApiResponse<MusicGenerationDTO.TaskAcceptedResponse> modifySongPrompt(
@AuthenticationPrincipal CustomUserDetails userDetails,
@PathVariable("song_id") Long songId,
@RequestBody MusicGenerationDTO.SongModificationRequest request) {
@Valid @RequestBody MusicGenerationDTO.SongModificationRequest request) {

MusicGenerationDTO.TaskAcceptedResponse result = musicGenerationService.modifySongPrompt(userDetails, songId, request);
return ApiResponse.onSuccess(GeneralSuccessCode.ACCEPTED, result);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,15 @@ public interface GenerationControllerDocs {
value = "{\"isSuccess\": false, \"code\": \"AUTH4000\", \"message\": \"로그인이 필요한 기능입니다.\", \"result\": null}"
)
)
)
),
@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는 필수입니다.\"}}"))),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "멜로디 악보 없음",
content = @Content(schema = @Schema(implementation = ApiResponse.class),
examples = @ExampleObject(name = "악보 없음",
value = "{\"isSuccess\":false, \"code\":\"MELODY4000\", \"message\":\"해당 허밍의 멜로디 악보가 존재하지 않습니다.\", \"result\":null}")))
})
@PostMapping("")
ApiResponse<MusicGenerationDTO.TaskAcceptedResponse> generateSong(
Expand All @@ -86,7 +94,11 @@ ApiResponse<MusicGenerationDTO.TaskAcceptedResponse> generateSong(
"**complete 이벤트 예시:** `{\"status\": \"COMPLETED\", \"result\": {\"task_id\": \"task_xxx\", \"song_id\": 42, \"audio_url\": \"https://...\", \"duration_seconds\": 30}}`"
)
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "스트림 연결 성공")
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "스트림 연결 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "태스크 없음",
content = @Content(schema = @Schema(implementation = ApiResponse.class),
examples = @ExampleObject(name = "태스크 없음",
value = "{\"isSuccess\":false, \"code\":\"TASK4000\", \"message\":\"해당 태스크 ID의 작업 정보가 존재하지 않습니다.\", \"result\":null}")))
})
@GetMapping(value = "/tasks/{task_id}/stream", produces = "text/event-stream")
SseEmitter subscribeTaskStream(@PathVariable("task_id") String taskId);
Expand Down Expand Up @@ -114,7 +126,11 @@ ApiResponse<MusicGenerationDTO.TaskAcceptedResponse> generateSong(
"}"
)
)
)
),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "태스크 없음",
content = @Content(schema = @Schema(implementation = ApiResponse.class),
examples = @ExampleObject(name = "태스크 없음",
value = "{\"isSuccess\":false, \"code\":\"TASK4000\", \"message\":\"해당 태스크 ID의 작업 정보가 존재하지 않습니다.\", \"result\":null}")))
})
@DeleteMapping("/tasks/{task_id}")
ApiResponse<MusicGenerationDTO.TaskCancelResponse> cancelTask(@PathVariable("task_id") String taskId);
Expand Down Expand Up @@ -166,7 +182,15 @@ ApiResponse<MusicGenerationDTO.TaskAcceptedResponse> generateSong(
value = "{\"isSuccess\": false, \"code\": \"AUTH4000\", \"message\": \"로그인이 필요한 기능입니다.\", \"result\": null}"
)
)
)
),
@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\":{\"prompt\":\"prompt는 필수입니다.\"}}"))),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "원본 곡 없음",
content = @Content(schema = @Schema(implementation = ApiResponse.class),
examples = @ExampleObject(name = "원본 곡 없음",
value = "{\"isSuccess\":false, \"code\":\"GENERATION4000\", \"message\":\"해당하는 생성곡이 존재하지 않습니다.\", \"result\":null}")))
})
@PostMapping("/{song_id}/modifications")
ApiResponse<MusicGenerationDTO.TaskAcceptedResponse> modifySongPrompt(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.humix.api.global.apiPayload.ApiResponse;
import com.humix.api.global.apiPayload.code.GeneralSuccessCode;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

Expand All @@ -19,7 +20,7 @@ public class InternalController {
@PostMapping("/{task_id}/completion")
public ApiResponse<Object> completeTask(
@PathVariable("task_id") String taskId,
@RequestBody MusicGenerationDTO.AiTaskCompletionRequest request) {
@Valid @RequestBody MusicGenerationDTO.AiTaskCompletionRequest request) {

musicGenerationService.completeTask(taskId, request);
return ApiResponse.onSuccess(GeneralSuccessCode.OK, null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import com.humix.api.domain.member.entity.Member;
import com.humix.api.domain.musicGeneration.entity.MusicGeneration;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

import java.time.LocalDateTime;
import java.util.List;
Expand All @@ -13,6 +15,7 @@ public class MusicGenerationDTO {
//노래 생성 요청 Request Body
public record SongCreateRequest(
@Schema(description = "허밍 ID", example = "1", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "humming_id는 필수입니다.")
@JsonProperty("humming_id") Long hummingId,

@Schema(description = "생성할 곡 제목", example = "나의 첫 번째 곡")
Expand Down Expand Up @@ -56,6 +59,7 @@ public record TaskCancelResponse(
public record SongModificationRequest(
@Schema(description = "수정 방향 프롬프트 (예: 더 밝게, 템포를 빠르게 등)", example = "더 밝고 경쾌하게 수정해줘",
requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "prompt는 필수입니다.")
String prompt
) {
//프롬프트 수정곡(수정 버전) 요청 시 새로운 MusicGeneration 엔티티를 생성하는 매핑 메서드
Expand Down Expand Up @@ -197,12 +201,7 @@ 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
) {
public AiTaskCompletionRequest {
if (generatedAudioUrl == null || generatedAudioUrl.isBlank()) {
throw new IllegalArgumentException("생성된 오디오 URL(generated_audio_url)은 필수입니다.");
}
}
}
) {}
}
Loading
Loading