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
11 changes: 9 additions & 2 deletions src/main/java/umc/codeplay/controller/AuthController.java
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,14 @@ public ApiResponse<MemberResponseDTO.LoginResultDTO> 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);
}
Expand Down Expand Up @@ -113,9 +118,11 @@ public ApiResponse<MemberResponseDTO.LoginResultDTO> 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);
}
Expand Down
12 changes: 2 additions & 10 deletions src/main/java/umc/codeplay/controller/FileController.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -30,15 +28,9 @@ public class FileController {
description = "업로드를 위한 Presigned URL 생성 - 유효시간 존재")
@PostMapping("/upload")
public ApiResponse<FileResponseDTO.UploadFile> 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));
}
}
19 changes: 15 additions & 4 deletions src/main/java/umc/codeplay/controller/TaskController.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -71,4 +69,17 @@ public ApiResponse<MemberResponseDTO.TaskProgressDTO> 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<MemberResponseDTO.TaskProgressDTO> waitTask(
@PathVariable Long taskId,
@RequestParam(defaultValue = "300000") long timeoutMillis // 기본 5분 대기시간 + 10초마다 작업상태 체크
) {
Task task = taskService.waitTask(taskId, timeoutMillis);
return ApiResponse.onSuccess(MemberConverter.toTaskProgressDTO(task));
}
}
3 changes: 2 additions & 1 deletion src/main/java/umc/codeplay/converter/MemberConverter.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/umc/codeplay/domain/Harmony.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/umc/codeplay/domain/Music.java
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/umc/codeplay/domain/Remix.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/umc/codeplay/domain/Track.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
38 changes: 14 additions & 24 deletions src/main/java/umc/codeplay/domain/enums/FileType.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
1 change: 1 addition & 0 deletions src/main/java/umc/codeplay/dto/MemberResponseDTO.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public static class JoinResultDTO {
@AllArgsConstructor
public static class LoginResultDTO {
String email;
String profileUrl;
String token;
String refreshToken;
}
Expand Down
101 changes: 55 additions & 46 deletions src/main/java/umc/codeplay/service/FileService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
}
}
}
7 changes: 7 additions & 0 deletions src/main/java/umc/codeplay/service/MemberService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down
26 changes: 26 additions & 0 deletions src/main/java/umc/codeplay/service/TaskService.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package umc.codeplay.service;

import java.time.Duration;
import java.time.Instant;

import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -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);
}
}
Loading