Tiny Bunny
본문 바로가기

벽돌깨기

[SpringBoot] Axios post 요청

728x90

나는 DB를 다뤄본 적도 없고 네트워크나 서버 등의 배경 지식이 없을 뿐더러... 이번 프로젝트를 하면서 처음으로 백엔드를 맡았는데, DB 관리와 더불어 백엔드 <-> 프론트 간의 요청 및 응답까지 하게 되어서 막막했다.

 

내가 구현하고자 하는 건

① 프론트단에서 녹음 파일의 경로를 POST 요청으로 보내고

② 백단에서 해당 POST 요청을 처리하여 파일 경로를 DB에 저장하는 기능이다.

 

아주 단순한 기능임에도 불구하고 백 <-> 프론트 데이터 전달 과정도 몰랐던 나는 405 에러에게 지고야 말았고... 결국 생애 첫(?) 트러블슈팅기록을 올리기로 했다. 많이 부끄럽지만....

 

 

 


 

 

 

삽질 과정 1

 

다른 팀원이 Method.java 파일에 녹음된 음성 파일 경로를 임의로 저장해뒀다.

public class Method {

    public String stt() {
        String openApiURL = " ";
        String accessKey = " "; // 발급받은 API Key
        String languageCode = "korean";
        String audioFilePath = "C:\\Users\\skgud\\Downloads\\output.wav"; // 녹음된 음성 파일 경로
        String audioContents = null;

        Gson gson = new Gson();

        Map<String, Object> request = new HashMap<>();
        Map<String, String> argument = new HashMap<>();

        try {
            Path path = Paths.get(audioFilePath);
            byte[] audioBytes = Files.readAllBytes(path);
            audioContents = Base64.getEncoder().encodeToString(audioBytes);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

 

① index.html에서 버튼을 클릭하면 해당 음성파일 경로를 POST 요청으로 보내는 savePathToDB() 함수가 실행되도록 onClick 처리를 함

<!DOCTYPE html>
<html>
  <head>
    <title>MP3 to Text Converter</title>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  </head>
  <body>
    <h1>MP3 to Text Converter</h1>
    <form action="/upload" method="post" enctype="multipart/form-data">
      <input type="file" name="file" accept=".mp3" />
      <button type="submit">업로드 및 변환</button>
    </form>
    <button onclick="savePathToDB()">경로 db에 저장</button>
    <!-- Axios 요청 스크립트 -->
    <script type="text/javascript">
      function savePathToDB() {
        // "/saveAudioFilePath" 경로로 POST 요청을 보냄.
        // Spring Boot 컨트롤러에서 해당 경로를 처리 (AudioController.java)
        axios
          .post("/saveAudioFilePath", {
            audioFilePath: "C:\\Users\\82105\\Desktop\\SAMPLE_1.mp3",
          })
          .then(function (response) {
            alert("Audio file path saved successfully!");
          })
          .catch(function (error) {
            alert("Failed to save audio file path!");
          });
      }
    </script>
  </body>
</html>

 

② POST 요청을 보냈으니 이를 처리할 스프링부트 컨트롤러인 AudioController.java 파일을 생성함

index.html에서 "/saveAudioFilePath" 경로로 POST 요청을 보냈으니 AudioController.java에서 @PostMapping ("/saveAudioFilePath")을 해주면 끝나는 줄 알았다.

파일 경로를 audioFilePath 변수에 저장하고 userMapper.saveAudioFilePath(audioFilePath)를 호출하여 데이터베이스에 파일 경로를 저장하면 되지 않을까?

package com.gmovie.gmovie.controller;

import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import com.gmovie.gmovie.mapper.UserMapper;

@RestController
@CrossOrigin(origins = "http://localhost:8801") // Cross-Origin Resource Sharing(CORS) 허용
public class AudioController {

    @Autowired
    private UserMapper userMapper;

    @PostMapping("/saveAudioFilePath") // "/saveAudioFilePath" 경로로 들어오는 POST 요청을 처리하도록 설정
    // 요청 본문에서 데이터를 추출. 요청에서 받은 JSON 데이터는 Map<String, String> 형태로 파싱
    public ResponseEntity<String> saveAudioFilePath(@RequestBody Map<String, String> payload) {
        String audioFilePath = payload.get("audioFilePath"); // 파일 경로를 audioFilePath 변수에 저장하고
        // userMapper.saveAudioFilePath(audioFilePath)를 호출하여 데이터베이스에 파일 경로를 저장
        userMapper.saveAudioFilePath(audioFilePath);

        return new ResponseEntity<>("File path saved successfully.", HttpStatus.OK);
    }
}

 

③ UserMapper.java를 생성하고 saveAudioFilePath가 호출되면 INSERT문으로 DB에 저장되도록!......

이렇게 하면 되지 않을까? 했으나... 전혀 안 되죠?

@Mapper
public interface UserMapper {
  // 오디오 파일 경로(audioFilePath)를 받아 SUMMARY 테이블의 REDORDING 컬럼에 경로 저장
  @Insert("INSERT INTO SUMMARY (REDORDING) VALUES (#{audioFilePath})")
  public int saveAudioFilePath(String audioFilePath);
}

 

음성 파일을 선택하고 업로드 및 변환 버튼을 클릭하면 DB에 해당 경로가 저장되는 게 나의 목표였으나...

Failed to load resource: the server responded with a status of 405 ()     chrome-error://chromewebdata/:1

 

아래 포스팅을 참고했다.

 

405 Not Allowed 탐방기

405 에러, 어디가 문제인가?

velog.io

 

가능성

1. 405 Not Allowed :: 프론트에서 POST로 요청을 했는데 백에서 해당 요청을 GET으로 받거나 혹은 그 반대의 경우

나는 보다시피 버튼을 클릭하면 POST 요청을 보냈고 컨트롤러에서 @PostMapping이 제대로 설정되어 있어 이부분은 문제될 게 없어보인다. 프론트에서 POST 요청을 제대로 하고 있고 백에서 PostMapping을 통해 제대로 받고 있기 때문이다.

 

2. 백엔드가 문제인가?

그렇다고 하기엔 너무나도 명확하게 @PostMapping이 적혀있는 걸...?

 

3. 프론트가 문제인가?

해당 글을 읽어보니 form 태그가 문제가 될 수도 있겠다 싶었다. 왜냐하면 index.html에서 from 태그를 썼기 때문.

이전에 JavaScript로 유효성 검사를 하는 회원가입 페이지를 만들 때 form 태그의 action 속성을 통해 submit 기능을 수행하도록 만들었었어서 습관적으로 form 태그를 사용했다.

 

글을 읽다가 문득 드는 생각.

위의 글을 작성한 사람은 form 태그 안에 action으로 경로 설정을 해주지 않아 현재 url을 post 요청을 하고 있다고 한다.

때문에 nginx에서 정적 데이터를 post 요청해? 하고 405 에러를 던져줬다고 했다.

 

팀원이 준 index.html에서 action 경로가 /upload로 설정되어 있는데 확인해보니 해당 경로를 가진 파일이 없었다. 당연히 될 리가 없다. 경로를 /upload로 설정했는데 정작 해당 경로의 파일이 존재하지 않으니 말이다.

action="/upload" method="post"
action="/upload" method="get"
action="/upload" method="get"

 

 

 

임의로 upload.html 생성 후

action="/upload" method="post"
action="/upload" method="post"

 

 

 

action="/upload" method="get"
action="/upload" method="get"

 

method를 get으로 변경하니 200 코드와 함께 정상적으로 작동했다. 근데 이건 내가 원하는 게 아닌데..? 헛수고를 했다.

 

 

 


 

 

 

삽질과정 2

 

기존에 팀원에게 받은 코드에서 필요하지 않은 것들을 모두 빼고 다시 수정했다. 이제 될까?

 

① form 태그의 action 속성을 제거하고 필요 없는 버튼 삭제

② event.preventDefault()를 사용하여 form의 기본 동작인 페이지 새로 고침(submit)을 막음

③사용자가 선택한 파일과 그 경로를 서버에 업로드 하는 uploadAndSavePath()라는 함수를 만듦

④ FormData 객체를 생성하여 'file'과 'audioFilePath' 데이터를 추가하고

⑤ axios.post 메소드를 호출하여 '/saveAudioFilePath' URL에 POST 요청을 보내도록 함

=> 사용자가 MP3 파일과 그 파일 경로 정보(audioFilePath)를 선택하면 해당 정보들이 서버('/saveAudioFilePath') 쪽으로 전송됨

 

* FormData 객체는 웹 양식의 데이터를 생성하고 서버로 보내기 위한 키-값 쌍을 쉽게 만들 수 있는 방법을 제공하는 JavaScript의 내장 객체이다. 이 객체는 주로 XMLHttpRequest 또는 fetch API를 사용하여 데이터를 비동기적으로 서버에 전송할 때 사용된다. FormData 객체는 주로 파일 업로드와 같은 작업에서 유용하며, 이 경우 폼 데이터를 multipart/form-data MIME 형식으로 인코딩한다.

 

filePathSave.html

<!DOCTYPE html>
<html>
  <head>
    <title>mp3 file path save</title>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  </head>
  <body>
    <h1>mp3 file path save</h1>
    <form onsubmit="event.preventDefault(); uploadAndSavePath();">
      <input type="file" id="file" name="file" accept=".mp3" />
      <button type="submit">경로 저장</button>
    </form>
    <script type="text/javascript">
      function uploadAndSavePath() {
        var formData = new FormData();
        var audioFilePath = document.getElementById("file").value; // 사용자가 선택한 파일 경로

        formData.append("file", document.getElementById("file").files[0]);
        formData.append("audioFilePath", audioFilePath);

        axios
          .post("/saveAudioFilePath", formData)
          .then(function (response) {
            alert("File uploaded and path saved successfully!");
          })
          .catch(function (error) {
            alert("Failed to upload file or save path!");
          });
      }
    </script>
  </body>
</html>

 

 

 

filePathSaveController.java 생성

동적페이지의 경우 스프링부트에서는 src/main/resources/templates 디렉토리에 있는 HTML 파일들이 템플릿 엔진(예: Thymeleaf, FreeMarker 등)을 통해 렌더링되기 때문에 파일에 직접적으로 접근할 수 없다. 내가 만든 filePathSave는 템플릿 엔진을 사용하는 동적페이지이기 때문에 해당 페이지를 반환하는 컨트롤러 메서드가 필요하다.

package com.gmovie.gmovie.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class filePathSaveController {
    @GetMapping("/filePathSave")
    public String filePathSave() {
        return "filePathSave";
    }
}

 

 


AudioController.java

① 클라이언트에서 보낸 MultipartFile 객체와 audioFilePath 문자열을 파라미터로 받는 메서드 생성

 

* 내부로직

① 파일(file)이 비어있지 않다면, jdbcTemplate를 이용하여 audioFilePath 값을 DB에 저장하고 성공 메시지와 함께 HTTP 상태 코드 200(OK)를 반환
② 만약 파일 업로드 중 오류가 발생하면, 오류 메시지와 함께 HTTP 상태 코드 500(Internal Server Error)를 반환
③ 만약 파일(file)이 비어있다면, "File is empty."라는 메시지와 함께 HTTP 상태 코드 400(Bad Request)를 반환

package com.gmovie.gmovie.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
@CrossOrigin(origins = "http://localhost:8801") // Cross-Origin Resource Sharing(CORS) 허용
public class AudioController {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @PostMapping("/saveAudioFilePath")
    public ResponseEntity<String> postAudioPath(@RequestParam("file") MultipartFile file) {
        // 업로드된 파일을 처리하는 로직
        if (!file.isEmpty()) {
            try {
                // DB에 파일 경로 저장하는 로직
                jdbcTemplate.update("INSERT INTO SUMMARY (RECORDING) VALUES (?)",
                        "C:\\Users\\skgud\\Downloads\\output.wav");

                return new ResponseEntity<>("Path saved successfully.", HttpStatus.OK);
            } catch (Exception e) {
                return new ResponseEntity<>("Failed to upload file: " + e.getMessage(),
                        HttpStatus.INTERNAL_SERVER_ERROR);
            }
        } else {
            return new ResponseEntity<>("File is empty.", HttpStatus.BAD_REQUEST);
        }
    }
}

 

 

 

그리고 원래는 회의룸ID와 사용자 고유 번호인 no을 외래키로 참조하여 NOT NULL 제약조건을 걸어뒀었으나.. 아직 다른 팀원으로부터 코드를 받지 못해서 해당 제약 조건을 NULL로 변경하고 DB에 경로가 저장되는지만 테스트해봤다.

파일 경로를 저장할 테이블

 

파일을 선택하고
경로 저장 버튼을 누르면
DB에 해당 경로가 저장되는 것 확인

 

 

 

삽질과정 3

 

뭔가 이상함을 느꼈다. 파일 경로가 실제 파일 경로가 아니라 중간에 fakepath가 껴있는 것이다...

찾아보니까 웹 보안상의 이유로, 파일 입력 필드에서 선택된 파일의 실제 파일 시스템 경로를 JS나 다른 클라이언트 사이드 코드에 노출시키지 않는다고 한다. 그래서 C:\fakepath\가 나타나는 것이라고... 웹 표준에서 정한 동작 방식이다.

 

따라서 서버 사이드에서 실제 파일의 경로를 알 필요가 있다면 보통은 서버 내부에 별도의 저장소나 AWS S3 등 외부 스토리지 서비스에 저장하는 방식을 권장한다.

 

그러나.. 맨 처음에도 얘기했듯이 나는 백엔드가 처음이고 DB를 처음 만져(?)보고 서버쪽 지식이 전혀 없다. 마감까지 앞으로 단 5일. 다른 기능도 구현해야하기 때문에 여기서 뭘 더 새롭게 공부해서 할 시간은 없을 것이라는 판단을 내렸고.. 일단 급한대로 사용자가 업로드한 파일이 서버의 files/ 디렉토리에 저장되고 그 상대 경로가 DB에 저장되는 방법을 쓰기로 했다.

 

 

 

filePathSave.html

<!DOCTYPE html>
<html>
  <head>
    <title>mp3 file path save</title>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  </head>
  <body>
    <h1>mp3 file path save</h1>
    <form onsubmit="event.preventDefault(); uploadAndSavePath();">
      <input type="file" id="file" name="file" accept=".mp3" />
      <button type="submit">경로 저장</button>
    </form>
    <script type="text/javascript">
      function uploadAndSavePath() {
        var formData = new FormData();
        formData.append("file", document.getElementById("file").files[0]);

        axios
          .post("/saveAudioFilePath", formData)
          .then(function (response) {
            alert("File uploaded and path saved successfully!");
          })
          .catch(function (error) {
            alert("Failed to upload file or save path!");
          });
      }
    </script>
  </body>
</html>

 

 

 

saveAudioFilePath.java

package com.gmovie.gmovie.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.Files;

@RestController
@CrossOrigin(origins = "http://localhost:8801")
public class saveAudioFilePath {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @PostMapping("/saveAudioFilePath")
    public ResponseEntity<String> postAudioPath(@RequestParam("file") MultipartFile file) {
        // 업로드된 파일을 처리하는 로직
        if (!file.isEmpty()) {
            try {
                // files 폴더는 프로젝트 루트 디렉토리 아래에 위치
                Path filePath = Paths.get("files/" + file.getOriginalFilename());

                if (!Files.exists(filePath.getParent())) {
                    Files.createDirectories(filePath.getParent());
                }

                Files.write(filePath, file.getBytes());

                // DB에 파일 경로 저장하는 로직 (상대 경로 저장)
                jdbcTemplate.update(
                        "INSERT INTO SUMMARY (RECORDING) VALUES (?)",
                        filePath.toString());

                return new ResponseEntity<>("Path saved successfully.", HttpStatus.OK);
            } catch (Exception e) {
                return new ResponseEntity<>("Failed to upload file: " + e.getMessage(),
                        HttpStatus.INTERNAL_SERVER_ERROR);
            }
        } else {
            return new ResponseEntity<>("File is empty.", HttpStatus.BAD_REQUEST);
        }
    }
}

 

 

 

이제 사용자가 업로드한 파일을 서버의 files 디렉토리에 저장하고 해당 파일의 상대경로를 DB에 저장하게 된다.

 

 

 

이걸로 5일을 날리다니... POST 요청을 보내고 PostMapping으로 처리했는데 왜...? 대체 뭐가 문제야....? 이 굴레에서 3일을 못 빠져나왔다. 결국 마음을 비우고 처음부터 다시 만들었지만 ㅎㅎ... 일단 오늘은 여기까지 하고 자고 일어나서는 사용자가 회의 시작하기 버튼을 눌렀을 때 UUID로 회의룸ID를 생성하고 회의룸ID + 사용자 고유 no + 회의 요약본 + 파일경로를 DB에 저장하는 걸 해야겠다.

728x90