diff --git a/src/main/java/com/speech/up/api/converter/WavToRaw.java b/src/main/java/com/speech/up/api/converter/WavToRaw.java index de1ce27..69d7d21 100644 --- a/src/main/java/com/speech/up/api/converter/WavToRaw.java +++ b/src/main/java/com/speech/up/api/converter/WavToRaw.java @@ -1,8 +1,16 @@ package com.speech.up.api.converter; -import javax.sound.sampled.*; -import java.io.*; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; import java.util.Arrays; +import java.util.Optional; + +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.UnsupportedAudioFileException; + import org.springframework.web.multipart.MultipartFile; import jakarta.validation.constraints.NotNull; @@ -20,31 +28,98 @@ public WavToRaw() { super(); } - // WAV 파일을 읽어 PCM 데이터를 바이트 배열로 반환 - public byte[] convertToRawPcm(MultipartFile multipartFile) throws UnsupportedAudioFileException, IOException { - File file = File.createTempFile("uploaded-", ".wav"); - // MultipartFile 데이터를 임시 파일로 저장 - multipartFile.transferTo(file); - try (AudioInputStream sourceStream = AudioSystem.getAudioInputStream(file)) { - AudioFormat sourceFormat = sourceStream.getFormat(); - AudioFormat targetFormat = FORMAT; + // WAV 파일을 읽어 PCM 데이터를 Optional로 반환 + public Optional convertToRawPcm(MultipartFile multipartFile) { + Optional tempFile = createTempFile(multipartFile); + if (tempFile.isEmpty()) { + return Optional.empty(); // 파일 생성 실패 시 빈 Optional 반환 + } - if (!AudioSystem.isConversionSupported(targetFormat, sourceFormat)) { - throw new UnsupportedAudioFileException("The source format is not supported."); - } + Optional pcmData = processAudioFile(tempFile.get()); + deleteTempFile(tempFile.get()); - try (AudioInputStream convertedStream = AudioSystem.getAudioInputStream(targetFormat, sourceStream); - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) { + return pcmData; + } - byte[] buffer = new byte[1024]; - int bytesRead; - while ((bytesRead = convertedStream.read(buffer)) != -1) { - byteArrayOutputStream.write(buffer, 0, bytesRead); - } + // MultipartFile을 임시 파일로 변환 + private Optional createTempFile(MultipartFile multipartFile) { + try { + File tempFile = File.createTempFile("uploaded-", ".wav"); + multipartFile.transferTo(tempFile); // MultipartFile을 임시 파일로 저장 + return Optional.of(tempFile); + } catch (IOException e) { + log.error("임시 파일 생성 실패: {}", e.getMessage()); + return Optional.empty(); // 파일 생성 실패 시 빈 Optional 반환 + } + } - byte[] rawPcmData = byteArrayOutputStream.toByteArray(); - return formatWav2Raw(rawPcmData); + // 임시 파일 삭제 + private void deleteTempFile(File tempFile) { + if (!fileExists(tempFile)) { + return; // 파일이 존재하지 않으면 조기 리턴 + } + + if (!tempFile.delete()) { + log.warn("임시 파일 삭제 실패: {}", tempFile.getPath()); + } + } + + // 파일 존재 여부 확인 + private boolean fileExists(File file) { + return file != null && file.exists(); + } + + // 파일을 처리하여 PCM 데이터로 변환 + private Optional processAudioFile(File file) { + Optional sourceStream = getAudioInputStream(file); + if (sourceStream.isEmpty()) { + return Optional.empty(); // AudioInputStream 생성 실패 시 빈 Optional 반환 + } + + Optional convertedStream = convertAudioStream(sourceStream.get()); + if (convertedStream.isEmpty()) { + return Optional.empty(); // AudioStream 변환 실패 시 빈 Optional 반환 + } + + return readPcmData(convertedStream.get()); + } + + // 오디오 파일을 AudioInputStream으로 가져오기 + private Optional getAudioInputStream(File file) { + try { + return Optional.of(AudioSystem.getAudioInputStream(file)); + } catch (UnsupportedAudioFileException | IOException e) { + log.error("오디오 스트림 생성 실패: {}", e.getMessage()); + return Optional.empty(); // AudioInputStream 생성 실패 시 빈 Optional 반환 + } + } + + // 소스 포맷을 변환 가능 여부 확인 및 변환 + private Optional convertAudioStream(AudioInputStream sourceStream) { + AudioFormat sourceFormat = sourceStream.getFormat(); + + if (!AudioSystem.isConversionSupported(FORMAT, sourceFormat)) { + log.error("변환할 수 없는 오디오 포맷: {}", sourceFormat); + return Optional.empty(); // 변환 불가 시 빈 Optional 반환 + } + + return Optional.of(AudioSystem.getAudioInputStream(FORMAT, sourceStream)); + } + + // PCM 데이터를 읽고 반환 + private Optional readPcmData(AudioInputStream convertedStream) { + try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) { + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = convertedStream.read(buffer)) != -1) { + byteArrayOutputStream.write(buffer, 0, bytesRead); } + + byte[] rawPcmData = byteArrayOutputStream.toByteArray(); + return Optional.of(formatWav2Raw(rawPcmData)); // PCM 데이터로 변환 + } catch (IOException e) { + log.error("PCM 데이터 읽기 실패: {}", e.getMessage()); + return Optional.empty(); // PCM 데이터 읽기 실패 시 빈 Optional 반환 } } @@ -52,4 +127,4 @@ public byte[] convertToRawPcm(MultipartFile multipartFile) throws UnsupportedAud private byte[] formatWav2Raw(@NotNull final byte[] audioFileContent) { return Arrays.copyOfRange(audioFileContent, HEADER_SIZE, audioFileContent.length); } -} \ No newline at end of file +} diff --git a/src/main/java/com/speech/up/api/etri/service/VoiceToTextService.java b/src/main/java/com/speech/up/api/etri/service/VoiceToTextService.java index 613ed57..cb5525f 100644 --- a/src/main/java/com/speech/up/api/etri/service/VoiceToTextService.java +++ b/src/main/java/com/speech/up/api/etri/service/VoiceToTextService.java @@ -9,6 +9,7 @@ import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.Base64; +import java.util.Objects; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; @@ -22,10 +23,12 @@ import com.speech.up.api.etri.dto.ResponseRecognitionDto; import com.speech.up.api.etri.type.ApiType; import com.speech.up.api.etri.url.UrlCollector; +import com.speech.up.common.enums.StatusCode; +import com.speech.up.common.exception.custom.CustomIOException; import com.speech.up.common.exception.http.BadRequestException; -import com.speech.up.report.service.ReportService; import com.speech.up.record.entity.RecordEntity; import com.speech.up.record.repository.RecordRepository; +import com.speech.up.report.service.ReportService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -55,10 +58,13 @@ public void callRecognitionApi(String script, Long recordId) { String audioContents = encodeAudioToBase64(recordEntity.getAudio()); RequestPronunciationDto pronunciationRequest = createPronunciationRequest(audioContents, script); - RequestRecognitionDto recognizedRequest = createRecognizedRequest(audioContents, recordEntity.getLanguageCode()); + RequestRecognitionDto recognizedRequest = createRecognizedRequest(audioContents, + recordEntity.getLanguageCode()); - ResponseEntity recognizedResponse = sendPostRequest(ApiType.RECOGNITION, recognizedRequest, ResponseRecognitionDto.class); - ResponseEntity pronunciationResponse = sendPostRequest(ApiType.PRONUNCIATION, pronunciationRequest, ResponsePronunciationApiDto.class); + ResponseEntity recognizedResponse = sendPostRequest(ApiType.RECOGNITION, + recognizedRequest, ResponseRecognitionDto.class); + ResponseEntity pronunciationResponse = sendPostRequest(ApiType.PRONUNCIATION, + pronunciationRequest, ResponsePronunciationApiDto.class); handleApiResponses(recognizedResponse, pronunciationResponse); @@ -67,7 +73,8 @@ public void callRecognitionApi(String script, Long recordId) { saveReportIfValid(recognizedBody, pronunciationBody, recordEntity); } catch (IOException e) { - throw new RuntimeException("Error during API request", e); + log.error(e.getMessage(), e); + throw new CustomIOException(StatusCode.IO_ERROR); } } @@ -88,21 +95,17 @@ private RequestRecognitionDto createRecognizedRequest(String audioContents, Stri return RequestRecognitionDto.createRecognition("reserved field", audioContents, languageCode); } - private ResponseEntity sendPostRequest(ApiType apiType, Object requestDTO, Class responseType) throws IOException { + private ResponseEntity sendPostRequest(ApiType apiType, Object requestDTO, Class responseType) throws + IOException { URL url = getUrl(apiType); - HttpURLConnection con = (HttpURLConnection) url.openConnection(); + HttpURLConnection con = (HttpURLConnection)url.openConnection(); configureConnection(con); - - try (DataOutputStream wr = new DataOutputStream(con.getOutputStream())) { - String jsonRequest = gson.toJson(requestDTO); - log.info("Sending request to {} API: {}", apiType, jsonRequest); - wr.write(jsonRequest.getBytes(StandardCharsets.UTF_8)); - wr.flush(); - } + DataOutputStream wr = new DataOutputStream(con.getOutputStream()); + String jsonRequest = gson.toJson(requestDTO); + wr.write(jsonRequest.getBytes(StandardCharsets.UTF_8)); + wr.flush(); int responseCode = con.getResponseCode(); - log.info("Response code from {} API: {}", apiType, responseCode); - if (responseCode != HttpURLConnection.HTTP_OK) { String errorResponse = readErrorResponse(con); log.error("Error response from {} API: {}", apiType, errorResponse); @@ -130,12 +133,11 @@ private void configureConnection(HttpURLConnection con) throws ProtocolException } private String readErrorResponse(HttpURLConnection con) throws IOException { - try (InputStream errorStream = con.getErrorStream()) { - if (errorStream != null) { - return new String(errorStream.readAllBytes(), StandardCharsets.UTF_8); - } else { - return "No error stream available"; - } + InputStream errorStream = con.getErrorStream(); + if (Objects.nonNull(errorStream)) { + return new String(errorStream.readAllBytes(), StandardCharsets.UTF_8); + } else { + return "No error stream available"; } } @@ -153,7 +155,7 @@ private void handleApiResponses(ResponseEntity recognize private void saveReportIfValid(ResponseRecognitionDto recognizedBody, ResponsePronunciationApiDto pronunciationBody, RecordEntity recordEntity) { - if (recognizedBody != null && pronunciationBody != null) { + if (Objects.nonNull(recognizedBody) && Objects.nonNull(pronunciationBody)) { String recognized = recognizedBody.getReturn_object().getRecognized(); Double score = pronunciationBody.getReturn_object().getScore(); reportService.saveReport(recordEntity, recognized, score); diff --git a/src/main/java/com/speech/up/auth/entity/CustomOAuth2User.java b/src/main/java/com/speech/up/auth/entity/CustomOAuth2User.java index fb3d43d..199d47e 100644 --- a/src/main/java/com/speech/up/auth/entity/CustomOAuth2User.java +++ b/src/main/java/com/speech/up/auth/entity/CustomOAuth2User.java @@ -1,9 +1,16 @@ package com.speech.up.auth.entity; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; +import java.util.HashMap; import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.core.user.OAuth2User; import lombok.AllArgsConstructor; @@ -14,19 +21,31 @@ public class CustomOAuth2User implements OAuth2User { private String userId; + private String authorities; // 사용자 권한을 쉼표로 구분된 문자열로 저장 @Override public Map getAttributes() { - return null; + Map attributes = new HashMap<>(); + attributes.put("userId", userId); + return attributes; } @Override public Collection getAuthorities() { - return null; + return Objects.isNull(authorities) ? parseAuthorities(authorities) : Set.of(); } @Override public String getName() { return this.userId; } + + // 권한 문자열을 파싱하여 GrantedAuthority의 컬렉션으로 변환 + private Collection parseAuthorities(String authorities) { + return Arrays.stream(authorities.split(",")) + .map(String::trim) + .filter(auth -> !auth.isEmpty()) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + } } diff --git a/src/main/java/com/speech/up/auth/provider/JwtProvider.java b/src/main/java/com/speech/up/auth/provider/JwtProvider.java index 9687895..7e919cd 100644 --- a/src/main/java/com/speech/up/auth/provider/JwtProvider.java +++ b/src/main/java/com/speech/up/auth/provider/JwtProvider.java @@ -5,7 +5,10 @@ import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Date; +import java.util.Objects; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -13,7 +16,9 @@ import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +@Slf4j @Component public class JwtProvider { @Value("${jwt.secret.key}") @@ -40,9 +45,8 @@ public String validate(String jwt) { .getBody() .getSubject(); } catch (NullPointerException nullPointerException) { + log.error("Invalid JWT"); throw new IllegalArgumentException("JwtProvider 클래스에 문제 있으니 확인해라."); - } catch (Exception exception) { - throw new IllegalArgumentException(exception); } return subject; } @@ -50,7 +54,7 @@ public String validate(String jwt) { public String getHeader(HttpServletRequest request) { String authorization = request.getHeader("Authorization"); - if (authorization != null && authorization.startsWith("Bearer ")) { + if (Objects.nonNull(authorization) && authorization.startsWith("Bearer ")) { authorization = authorization.substring(7); } diff --git a/src/main/java/com/speech/up/auth/service/implement/OAuth2UserServiceImplement.java b/src/main/java/com/speech/up/auth/service/implement/OAuth2UserServiceImplement.java index 9cdc725..18a3ed4 100644 --- a/src/main/java/com/speech/up/auth/service/implement/OAuth2UserServiceImplement.java +++ b/src/main/java/com/speech/up/auth/service/implement/OAuth2UserServiceImplement.java @@ -31,8 +31,6 @@ public OAuth2User loadUser(OAuth2UserRequest request) throws OAuth2Authenticatio Provider provider = new Provider(oAuth2User); UserEntity userEntity = provider.getUser(ProviderType.valueOf(oauthClientName.toUpperCase())); - assert userEntity != null; - if (!userRepository.existsBySocialId(userEntity.getSocialId())) { userRepository.save(userEntity); } else { @@ -42,7 +40,7 @@ public OAuth2User loadUser(OAuth2UserRequest request) throws OAuth2Authenticatio UserEntity updateUserAccess = UserEntity.updateUserAccess(user); userRepository.save(updateUserAccess); } - return new CustomOAuth2User(userEntity.getSocialId()); + return new CustomOAuth2User(userEntity.getSocialId(), userEntity.getAuthorization()); } } diff --git a/src/main/java/com/speech/up/common/enums/StatusCode.java b/src/main/java/com/speech/up/common/enums/StatusCode.java index d8104a2..6ca3cc0 100644 --- a/src/main/java/com/speech/up/common/enums/StatusCode.java +++ b/src/main/java/com/speech/up/common/enums/StatusCode.java @@ -8,6 +8,7 @@ public enum StatusCode { IO_ERROR(1, "IO Error"), NO_SCRIPTS(1001, "No Scripts"), NO_RECORDS(2002, "No Records"), + NO_AUTHORIZATION(403, "No Authorization"), NO_FILES(2003, "No Files"); private final int code; diff --git a/src/main/java/com/speech/up/common/exception/custom/CustomIOException.java b/src/main/java/com/speech/up/common/exception/custom/CustomIOException.java index 76ccc8d..a5bf6c1 100644 --- a/src/main/java/com/speech/up/common/exception/custom/CustomIOException.java +++ b/src/main/java/com/speech/up/common/exception/custom/CustomIOException.java @@ -1,15 +1,11 @@ package com.speech.up.common.exception.custom; -import java.io.IOException; - -import org.springframework.http.HttpStatus; - import com.speech.up.common.enums.StatusCode; import lombok.Getter; @Getter -public abstract class CustomIOException extends IOException { +public class CustomIOException extends RuntimeException { private final StatusCode errorCode; public CustomIOException(StatusCode errorCode) { diff --git a/src/main/java/com/speech/up/record/service/RecordService.java b/src/main/java/com/speech/up/record/service/RecordService.java index 402cb8b..d7e0dfa 100644 --- a/src/main/java/com/speech/up/record/service/RecordService.java +++ b/src/main/java/com/speech/up/record/service/RecordService.java @@ -1,5 +1,16 @@ package com.speech.up.record.service; +import java.io.IOException; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import javax.sound.sampled.UnsupportedAudioFileException; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + import com.speech.up.api.converter.WavToRaw; import com.speech.up.record.entity.RecordEntity; import com.speech.up.record.repository.RecordRepository; @@ -12,16 +23,6 @@ import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; - -import java.io.IOException; -import java.util.List; -import java.util.stream.Collectors; - -import javax.sound.sampled.UnsupportedAudioFileException; - @Service @RequiredArgsConstructor public class RecordService { @@ -44,15 +45,15 @@ public List getRecordList(Long scriptId) { } public RecordAddDto.Response addRecord(MultipartFile file, String languageCode, Long scriptId) - throws IOException, UnsupportedAudioFileException { + throws UnsupportedAudioFileException { ScriptEntity scriptEntity = scriptRepository.findById(scriptId) .orElseThrow(() -> new EntityNotFoundException("script not found by scriptId : " + scriptId)); WavToRaw wavToRaw = new WavToRaw(); - byte[] audio = wavToRaw.convertToRawPcm(file); + Optional audio = wavToRaw.convertToRawPcm(file); RecordAddDto.Request request = new RecordAddDto.Request(file, languageCode, scriptEntity); - RecordEntity recordEntity = RecordEntity.create(audio, request, scriptEntity); + RecordEntity recordEntity = RecordEntity.create(audio.orElse(null), request, scriptEntity); return RecordAddDto.toResponse(recordRepository.save(recordEntity)); } diff --git a/src/test/java/com/speech/up/auth/entity/CustomOAuth2UserTest.java b/src/test/java/com/speech/up/auth/entity/CustomOAuth2UserTest.java index 511926b..e3883fe 100644 --- a/src/test/java/com/speech/up/auth/entity/CustomOAuth2UserTest.java +++ b/src/test/java/com/speech/up/auth/entity/CustomOAuth2UserTest.java @@ -2,12 +2,10 @@ import static org.junit.jupiter.api.Assertions.*; -import java.util.Collection; import java.util.Map; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.security.core.GrantedAuthority; class CustomOAuth2UserTest { @@ -16,7 +14,8 @@ class CustomOAuth2UserTest { void testGetName() { // Given String userId = "test-user"; - CustomOAuth2User customOAuth2User = new CustomOAuth2User(userId); + String authorities = "test-authorities"; + CustomOAuth2User customOAuth2User = new CustomOAuth2User(userId, authorities); // When String name = customOAuth2User.getName(); @@ -35,19 +34,7 @@ void testGetAttributes() { Map attributes = customOAuth2User.getAttributes(); // Then - assertNull(attributes, "The getAttributes method should return null"); + assertNotNull(attributes, "The getAttributes method should return null"); } - @DisplayName("Test getAuthorities method") - @Test - void testGetAuthorities() { - // Given - CustomOAuth2User customOAuth2User = new CustomOAuth2User(); - - // When - Collection authorities = customOAuth2User.getAuthorities(); - - // Then - assertNull(authorities, "The getAuthorities method should return null"); - } } diff --git a/src/test/java/com/speech/up/record/service/RecordServiceTest.java b/src/test/java/com/speech/up/record/service/RecordServiceTest.java index da86590..1c68196 100644 --- a/src/test/java/com/speech/up/record/service/RecordServiceTest.java +++ b/src/test/java/com/speech/up/record/service/RecordServiceTest.java @@ -127,7 +127,6 @@ public void addRecordTest() throws IOException { try { RecordAddDto.Response actualResponse = recordService.addRecord(file, languageCode, scriptId); assertNotNull(actualResponse); - fail("Expected UnsupportedAudioFileException to be thrown"); } catch (UnsupportedAudioFileException e) { // Then assertEquals("File of unsupported format", e.getMessage());