diff --git a/src/main/java/umc/codeplay/controller/AuthController.java b/src/main/java/umc/codeplay/controller/AuthController.java index c9eade1..2b52c8a 100644 --- a/src/main/java/umc/codeplay/controller/AuthController.java +++ b/src/main/java/umc/codeplay/controller/AuthController.java @@ -68,9 +68,14 @@ public ApiResponse login( String token = jwtUtil.generateToken(authentication.getName(), authorities); String refreshToken = jwtUtil.generateRefreshToken(authentication.getName(), authorities); + String profileImage = memberService.getMemberProfileImage(request.getEmail()); + return ApiResponse.onSuccess( MemberConverter.toLoginResultDTO( - request.getEmail(), token, refreshToken)); // 예시로 토큰만 문자열로 반환 + request.getEmail(), + profileImage, + token, + refreshToken)); // 예시로 토큰만 문자열로 반환 } catch (Exception e) { throw new GeneralHandler(ErrorStatus.ID_OR_PASSWORD_WRONG); } @@ -113,9 +118,11 @@ public ApiResponse refresh( // 새로운 액세스 토큰 생성 String newAccessToken = jwtUtil.generateToken(usernameFromToken, authorities); + String profileImage = memberService.getMemberProfileImage(email); return ApiResponse.onSuccess( - MemberConverter.toLoginResultDTO(usernameFromToken, newAccessToken, null)); + MemberConverter.toLoginResultDTO( + usernameFromToken, profileImage, newAccessToken, null)); } else { throw new GeneralHandler(ErrorStatus.INVALID_REFRESH_TOKEN); } diff --git a/src/main/java/umc/codeplay/controller/FileController.java b/src/main/java/umc/codeplay/controller/FileController.java index d265b2b..c1d1ff3 100644 --- a/src/main/java/umc/codeplay/controller/FileController.java +++ b/src/main/java/umc/codeplay/controller/FileController.java @@ -15,8 +15,6 @@ import umc.codeplay.dto.FileResponseDTO; import umc.codeplay.service.FileService; -import static umc.codeplay.service.FileService.buildFilename; - @RestController @RequestMapping("/files") @RequiredArgsConstructor @@ -30,15 +28,9 @@ public class FileController { description = "업로드를 위한 Presigned URL 생성 - 유효시간 존재") @PostMapping("/upload") public ApiResponse generateUrl( - @RequestParam(value = "fileType") FileType fileType, - @RequestParam(value = "fileName") String fileName) { + @RequestParam FileType fileType, @RequestParam String fileName) { String username = SecurityContextHolder.getContext().getAuthentication().getName(); - String newFileName = fileType.getFolderName() + buildFilename(fileName); - - Long id = fileType.processUpload(fileService, newFileName, username); - String uploadUrl = fileService.generatePutPresignedUrl(newFileName); - - return ApiResponse.onSuccess(fileType.createResponse(uploadUrl, id)); + return ApiResponse.onSuccess(fileService.getUploadUrl(username, fileName, fileType)); } } diff --git a/src/main/java/umc/codeplay/controller/TaskController.java b/src/main/java/umc/codeplay/controller/TaskController.java index 6cb7da1..e8f1418 100644 --- a/src/main/java/umc/codeplay/controller/TaskController.java +++ b/src/main/java/umc/codeplay/controller/TaskController.java @@ -1,13 +1,11 @@ package umc.codeplay.controller; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import lombok.RequiredArgsConstructor; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import umc.codeplay.apiPayLoad.ApiResponse; @@ -71,4 +69,17 @@ public ApiResponse getTask( Task task = taskService.findById(request.getTaskId()); return ApiResponse.onSuccess(MemberConverter.toTaskProgressDTO(task)); } + + @Hidden // TODO: 기능 완성시 @Hidden 태그만 삭제해주세요! + @Operation( + summary = "작업 진행 상황 조회", + description = "작업 ID를 받아 완료될 때까지 기다린 후 결과를 반환합니다. 기본 대기시간 5분, 10초마다 작업 상태확인.") + @GetMapping("/wait/{taskId}") + public ApiResponse waitTask( + @PathVariable Long taskId, + @RequestParam(defaultValue = "300000") long timeoutMillis // 기본 5분 대기시간 + 10초마다 작업상태 체크 + ) { + Task task = taskService.waitTask(taskId, timeoutMillis); + return ApiResponse.onSuccess(MemberConverter.toTaskProgressDTO(task)); + } } diff --git a/src/main/java/umc/codeplay/converter/MemberConverter.java b/src/main/java/umc/codeplay/converter/MemberConverter.java index 9c27b5d..928c031 100644 --- a/src/main/java/umc/codeplay/converter/MemberConverter.java +++ b/src/main/java/umc/codeplay/converter/MemberConverter.java @@ -33,10 +33,11 @@ public static MemberResponseDTO.JoinResultDTO toJoinResultDTO(Member member) { } public static MemberResponseDTO.LoginResultDTO toLoginResultDTO( - String email, String token, String refreshToken) { + String email, String profileUrl, String token, String refreshToken) { return MemberResponseDTO.LoginResultDTO.builder() .email(email) + .profileUrl(profileUrl) .token(token) .refreshToken(refreshToken) .build(); diff --git a/src/main/java/umc/codeplay/domain/Harmony.java b/src/main/java/umc/codeplay/domain/Harmony.java index 05e4ff5..a4c99fb 100644 --- a/src/main/java/umc/codeplay/domain/Harmony.java +++ b/src/main/java/umc/codeplay/domain/Harmony.java @@ -41,7 +41,7 @@ public class Harmony extends BaseEntity { @Builder private Harmony(String scale, String genre, Integer bpm, String voiceColor, Music music) { - this.title = music.getTitle().split("-", 2)[1]; + this.title = music.getTitle() + "_화성분석 결과"; this.scale = scale; this.genre = genre; this.bpm = bpm; diff --git a/src/main/java/umc/codeplay/domain/Music.java b/src/main/java/umc/codeplay/domain/Music.java index 79e642b..d367885 100644 --- a/src/main/java/umc/codeplay/domain/Music.java +++ b/src/main/java/umc/codeplay/domain/Music.java @@ -26,7 +26,8 @@ public class Music extends BaseEntity { @Column(nullable = false, length = 100) private String title; - @Column(columnDefinition = "TEXT", nullable = false) + @Column(columnDefinition = "TEXT") + @Setter private String musicUrl; @ManyToOne(fetch = FetchType.LAZY) diff --git a/src/main/java/umc/codeplay/domain/Remix.java b/src/main/java/umc/codeplay/domain/Remix.java index 9c10ffa..1745899 100644 --- a/src/main/java/umc/codeplay/domain/Remix.java +++ b/src/main/java/umc/codeplay/domain/Remix.java @@ -69,7 +69,7 @@ public Remix( Boolean isChorusOn, String resultMusicUrl, Music music) { - this.title = music.getTitle().split("-", 2)[1]; + this.title = music.getTitle() + "_리믹스 결과"; this.scaleModulation = scaleModulation; this.tempoRatio = tempoRatio; this.reverbAmount = reverbAmount; diff --git a/src/main/java/umc/codeplay/domain/Track.java b/src/main/java/umc/codeplay/domain/Track.java index a34ca28..086e500 100644 --- a/src/main/java/umc/codeplay/domain/Track.java +++ b/src/main/java/umc/codeplay/domain/Track.java @@ -47,7 +47,7 @@ public class Track extends BaseEntity { @Builder public Track( String vocalUrl, String instrumentalUrl, String bassUrl, String drumsUrl, Music music) { - this.title = music.getTitle().split("-", 2)[1]; + this.title = music.getTitle() + "_스템분리 결과"; this.vocalUrl = vocalUrl; this.instrumentalUrl = instrumentalUrl; this.bassUrl = bassUrl; diff --git a/src/main/java/umc/codeplay/domain/enums/FileType.java b/src/main/java/umc/codeplay/domain/enums/FileType.java index 4bbff7f..6ac1c9e 100644 --- a/src/main/java/umc/codeplay/domain/enums/FileType.java +++ b/src/main/java/umc/codeplay/domain/enums/FileType.java @@ -1,39 +1,29 @@ package umc.codeplay.domain.enums; import umc.codeplay.dto.FileResponseDTO; -import umc.codeplay.service.FileService; public enum FileType { AUDIO { - public String getFolderName() { - return "requestFiles/"; - } - - public Long processUpload(FileService fileService, String fileName, String username) { - return fileService.uploadMusic(fileName, username); - } - - public FileResponseDTO.UploadFile createResponse(String uploadUrl, Long id) { - return new FileResponseDTO.UploadFile(uploadUrl, id, null); + @Override + public String buildStoragePath(Long id, String fileName) { + return String.format("%s%d/%s", BASE_AUDIO_PATH, id, fileName); } }, IMAGE { - public String getFolderName() { - return "profileImgs/"; - } - - public Long processUpload(FileService fileService, String fileName, String username) { - return fileService.uploadProfile(fileName, username); - } - - public FileResponseDTO.UploadFile createResponse(String uploadUrl, Long id) { - return new FileResponseDTO.UploadFile(uploadUrl, null, id); + @Override + public String buildStoragePath(Long id, String fileName) { + return String.format("%s%d/%s", BASE_IMAGE_PATH, id, fileName); } }; - public abstract String getFolderName(); + private static final String BASE_AUDIO_PATH = "requestFiles/"; + private static final String BASE_IMAGE_PATH = "profileImgs/"; - public abstract Long processUpload(FileService fileService, String fileName, String username); + public abstract String buildStoragePath(Long id, String fileName); - public abstract FileResponseDTO.UploadFile createResponse(String uploadUrl, Long id); + public FileResponseDTO.UploadFile createResponse(String uploadUrl, Long id) { + return this == AUDIO + ? new FileResponseDTO.UploadFile(uploadUrl, id, null) + : new FileResponseDTO.UploadFile(uploadUrl, null, id); + } } diff --git a/src/main/java/umc/codeplay/dto/MemberResponseDTO.java b/src/main/java/umc/codeplay/dto/MemberResponseDTO.java index 4388b35..c45fb7c 100644 --- a/src/main/java/umc/codeplay/dto/MemberResponseDTO.java +++ b/src/main/java/umc/codeplay/dto/MemberResponseDTO.java @@ -24,6 +24,7 @@ public static class JoinResultDTO { @AllArgsConstructor public static class LoginResultDTO { String email; + String profileUrl; String token; String refreshToken; } diff --git a/src/main/java/umc/codeplay/service/FileService.java b/src/main/java/umc/codeplay/service/FileService.java index 93a3af7..f10d924 100644 --- a/src/main/java/umc/codeplay/service/FileService.java +++ b/src/main/java/umc/codeplay/service/FileService.java @@ -5,24 +5,25 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.presigner.S3Presigner; -import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest; import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest; import umc.codeplay.apiPayLoad.code.status.ErrorStatus; import umc.codeplay.apiPayLoad.exception.handler.GeneralHandler; import umc.codeplay.domain.Member; import umc.codeplay.domain.Music; +import umc.codeplay.domain.enums.FileType; +import umc.codeplay.dto.FileResponseDTO; import umc.codeplay.repository.MemberRepository; import umc.codeplay.repository.MusicRepository; @Service @RequiredArgsConstructor public class FileService { - @Value("${s3.bucket}") private String bucketName; @@ -33,64 +34,72 @@ public class FileService { private final MusicRepository musicRepository; private final MemberRepository memberRepository; - // 타임스탬프_파일명 형식으로 파일 이름 저장 - public static String buildFilename(String filename) { - return String.format("%s_%s", System.currentTimeMillis(), sanitizeFileName(filename)); - } + @Transactional + public FileResponseDTO.UploadFile getUploadUrl( + String username, String fileName, FileType fileType) { + String sanitizedFileName = sanitizeFileName(fileName); + Long entityId = createFileEntity(username, sanitizedFileName, fileType); + String storagePath = fileType.buildStoragePath(entityId, sanitizedFileName); - // 특수 문자나 공백 등을 정리 - private static String sanitizeFileName(String fileName) { - String normalizedFileName = Normalizer.normalize(fileName, Normalizer.Form.NFC); - System.out.println(normalizedFileName); - return normalizedFileName.replaceAll("\\s+", "_").replaceAll("[^가-힣a-zA-Z0-9.\\-_]", "_"); + updateEntityUrl(entityId, storagePath, fileType); + String uploadUrl = generatePresignedUrl(storagePath); + return fileType.createResponse(uploadUrl, entityId); } - // S3에 파일을 업로드할 수 있는 Presigned URL 생성 - public String generatePutPresignedUrl(String fileName) { - try { - PutObjectRequest putObjectRequest = - PutObjectRequest.builder().bucket(bucketName).key(fileName).build(); - - PutObjectPresignRequest presignRequest = - PutObjectPresignRequest.builder() - .signatureDuration(Duration.ofMinutes(60)) - .putObjectRequest(putObjectRequest) - .build(); - - PresignedPutObjectRequest presignedRequest = - s3Presigner.presignPutObject(presignRequest); - return presignedRequest.url().toString(); - } catch (Exception e) { - throw new GeneralHandler(ErrorStatus.AWS_SERVICE_UNAVAILABLE); - } + private String sanitizeFileName(String fileName) { + return Normalizer.normalize(fileName, Normalizer.Form.NFC) + .replaceAll("[^가-힣a-zA-Z0-9.\\s\\-_]", "_"); } - // User 레포지토리에 업로드 - public Long uploadProfile(String newFileName, String userEmail) { + private Long createFileEntity(String userEmail, String fileName, FileType fileType) { Member member = memberRepository .findByEmail(userEmail) .orElseThrow(() -> new GeneralHandler(ErrorStatus.MEMBER_NOT_FOUND)); - String s3Url = - String.format("https://%s.s3.%s.amazonaws.com/%s", bucketName, region, newFileName); + if (fileType == FileType.IMAGE) { + return member.getId(); + } - member.setProfileUrl(s3Url); - return memberRepository.save(member).getId(); + Music newMusic = Music.builder().title(fileName).member(member).build(); + return musicRepository.save(newMusic).getId(); } - // music 레포지토리에 업로드 - public Long uploadMusic(String newFileName, String userEmail) { - Member member = - memberRepository - .findByEmail(userEmail) - .orElseThrow(() -> new GeneralHandler(ErrorStatus.MEMBER_NOT_FOUND)); + private void updateEntityUrl(Long id, String storagePath, FileType fileType) { + String url = + String.format("https://%s.s3.%s.amazonaws.com/%s", bucketName, region, storagePath); + + if (fileType == FileType.IMAGE) { + Member member = + memberRepository + .findById(id) + .orElseThrow(() -> new GeneralHandler(ErrorStatus.MEMBER_NOT_FOUND)); + member.setProfileUrl(url); + memberRepository.save(member); + } else { + Music music = + musicRepository + .findById(id) + .orElseThrow(() -> new GeneralHandler(ErrorStatus.MUSIC_NOT_FOUND)); + music.setMusicUrl(url); + musicRepository.save(music); + } + } - // 저장하는 url은 유효시간이 없는 public - String s3Url = - String.format("https://%s.s3.%s.amazonaws.com/%s", bucketName, region, newFileName); - Music newMusic = Music.builder().title(newFileName).musicUrl(s3Url).member(member).build(); + private String generatePresignedUrl(String storagePath) { + try { + PutObjectRequest objectRequest = + PutObjectRequest.builder().bucket(bucketName).key(storagePath).build(); - return musicRepository.save(newMusic).getId(); + PutObjectPresignRequest presignRequest = + PutObjectPresignRequest.builder() + .signatureDuration(Duration.ofMinutes(60)) + .putObjectRequest(objectRequest) + .build(); + + return s3Presigner.presignPutObject(presignRequest).url().toString(); + } catch (Exception e) { + throw new GeneralHandler(ErrorStatus.AWS_SERVICE_UNAVAILABLE); + } } } diff --git a/src/main/java/umc/codeplay/service/MemberService.java b/src/main/java/umc/codeplay/service/MemberService.java index 86a8a0f..3362bf2 100644 --- a/src/main/java/umc/codeplay/service/MemberService.java +++ b/src/main/java/umc/codeplay/service/MemberService.java @@ -41,6 +41,13 @@ public class MemberService { "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; private static final SecureRandom RANDOM = new SecureRandom(); + public String getMemberProfileImage(String email) { + return memberRepository + .findByEmail(email) + .orElseThrow(() -> new GeneralHandler(ErrorStatus.MEMBER_NOT_FOUND)) + .getProfileUrl(); + } + public Member joinMember(MemberRequestDTO.JoinDto request) { if (memberRepository.findByEmail(request.getEmail()).isPresent()) { diff --git a/src/main/java/umc/codeplay/service/TaskService.java b/src/main/java/umc/codeplay/service/TaskService.java index 833498e..1c05427 100644 --- a/src/main/java/umc/codeplay/service/TaskService.java +++ b/src/main/java/umc/codeplay/service/TaskService.java @@ -1,5 +1,8 @@ package umc.codeplay.service; +import java.time.Duration; +import java.time.Instant; + import org.springframework.stereotype.Service; import lombok.RequiredArgsConstructor; @@ -73,4 +76,27 @@ public Task addTask(Remix newRemix) { return taskRepository.save(task); } + + public Task waitTask(Long id, long timeoutMillis) { + Instant startTime = Instant.now(); + Task task = findById(id); + + while (Duration.between(startTime, Instant.now()).toMillis() < timeoutMillis) { + + // complete 면 응답 + if (task.getStatus().equals(ProcessStatus.COMPLETED)) { + return task; + } + + // 3초 대기이후 다시 작업 체크 + try { + Thread.sleep(10000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new GeneralException(ErrorStatus._INTERNAL_SERVER_ERROR); + } + } + + return findById(id); + } }