728x90

Spring Boot로 프로젝트 중 bootstrap 예시 템플릿을 사용하는 중 직접 만든 요소를 이쁘게 정렬하기 위해 가운데 정렬하기 위한 방법을 찾아봤습니다.


<div class="table-responsive mt-3" style="display: flex; justify-content: center;">
  • display: flax - 인라인 style속성을 이용해 가운데 정렬하고 싶은 태그 안에 넣어줍니다.
  • jstify-content: center - 태그 내의 요소를 가운데 정렬해줍니다.

Reference : https://deeplify.dev/front-end/markup/align-div-center

 

[HTML/CSS] div 가운데 정렬 하는 방법

CSS를 이용하여 div 등의 엘리먼트를 가운데 정렬하는 방법을 소개합니다.

deeplify.dev

 

'Web > CSS' 카테고리의 다른 글

[CSS] 하이퍼링크 밑줄 제거 (인라인 스타일 적용)  (0) 2021.12.18
Bootstrap 시작하기  (0) 2021.08.07
728x90

특정 버튼을 클릭했을 때 다른 태그의 내용을 보여주고자 한다. 자바스크립트로 태그 내용을 숨겨보자.

또, 숨겼던 태그를 나타내보자.

1. 태그 숨기기

document.getElementById('userList').style.display = "none"
  • document.getElementById로 숨기고자 하는 태그를 가져온다.
  • style.display = "none"으로 숨긴다.

2. 태그 나타내기

document.getElementById('userList').style.display = "block"
  • 위 내용과 동일하게 작성하고 none을 block으로 바꾸면 된다. 

3. 활용

계정생성 버튼을 선택하면 <div id="addDiv">가 나타나고 돌아가기를 클릭하면 <div id="userList>태그가 나타난다.

<div id="userList>
	<a type="button" class="btn btn-primary me-2" onclick="changeDiv()">계정생성</a>
</div>
<div id="addDiv">
	<a type="button" class="btn btn-primary me-2" onclick="changeDiv()">돌아가기</a>
</div>

 

        // 회원 리스트 폼 & 회원 생성 폼 전환
        function changeDiv() {
            var listDiv = document.getElementById('userList');
            var addDiv = document.getElementById('addUser');

            if(listDiv.style.display === 'none') {
                listDiv.style.display = 'block';
                addDiv.style.display = 'none';
            } else {
                listDiv.style.display = 'none';
                addDiv.style.display = 'block';
            }
        }
  • a태그의 타입을 button으로 지정하고 onclick으로 자바스크립트 함수를 호출해준다.
  • 버튼을 누르면 display 상태에 따라 특정 태그를 숨기고 나타낸다.
728x90

보통 input으로 글만 입력 받았다. 그러나 이제 파일을 입력받아보자!

<form action="#" th:action="@{/board/form}" th:object="${board}" method="post" enctype="multipart/form-data">
    <input id="uploadInput" type="file" class="btn btn-outline-primary" name="files" accept="image/*" multiple />
</form>
  • form의 enctype을 multipart/form-data로 지정함으로써 입력 파일을 서버에 보낼 수 있도록 한다.
  • type="file"을 통해 파일을 입력받을 수 있도록 한다.
  • accept를 통해 입력받을 수 있는 파일 기본값을 바꾼다. (모든파일을 받을 수 있긴 함)
  • multiple을 통해 여러 파일을 선택할 수 있다.

!주의 : 만약 Spring을 서버로 사용하는파일을 @RequestBody로 전달받을 경우 Content-Type이 multipart/form-data로 전달받기 때문에 오류를 일으킨다. 

'Web > HTML' 카테고리의 다른 글

[HTML] 링크 새 창으로 열기 (target)  (0) 2022.01.14
728x90

파일 첨부를 통해 이미지를 서버에 저장까지 진행했으니, 그 이미지를 게시글에 보여주기 작업을 진행하고 기록한다.

지난 글 : https://black-mint.tistory.com/62

 

[Spring Boot] 파일 첨부 (게시글 이미지 첨부)

게시글 작성에서 파일 첨부하는 방법을 기록한다. (본 글은 이미지 첨부 방법을 다룸) 1. Mysql - file 테이블 CREATE TABLE `tb_file` ( `id` int NOT NULL AUTO_INCREMENT, `board_id` int NOT NULL, `original_..

black-mint.tistory.com

 

게시글을 조회하면 content(내용) 밑에 등록했던 이미지를 행으로 나열할 것이다.


1. 구현 로직

  • 처음 지식이 전무한 상태에서 생각한 방법이 ajax 통신을 통해 이미지정보를 DB에서 List형태로 받아 이미지 경로를 View에 띄워주려했다. (실패)
  • 여러 정보를 탐색했지만 여러 이미지를 리스트 형태로 리턴하는 방법이 없었다. (1번 실패 이유)
  • img태그의 src에 이미지 리턴 주소를 넣어준다면..? (선택!)
  • 최종 로직
    1.  클라이언트에서 게시판ID(PK)를 서버로 전달
    2.  서버에서 해당 게시글에 등록된 첨부파일(이미지) 데이터를 반환
    3.  클라이언트는 서버로부터 받은 데이터를 이용해 img태그의 src속성에 이미지 api 주소를 저장하고 div태그 자식으로 추가!
    4. View에 이미지 보여주기

2. Controller

@RestController
public class FileController {

    @Autowired
    private FileService fileService;

    // 게시글에 첨부된 이미지 ID값 리스트로 반환
    @GetMapping(value = "/images/{boardId}")
    public List<FileDTO> getFileList(@PathVariable(value = "boardId") Long boardId) throws SQLException {
        List<FileDTO> files = fileService.getFileList(boardId);
        return files;
    }

    // 이미지 리턴
    @GetMapping(value = "/image/{imageId}", produces = MediaType.IMAGE_JPEG_VALUE)
    public ResponseEntity<Resource> getViewImage(@PathVariable(value = "imageId") Long imageId) throws SQLException {
        FileDTO fileDTO = fileService.getFile(imageId);
        Resource resource = new FileSystemResource(fileDTO.getPath());
        return new ResponseEntity<Resource>(resource, HttpStatus.OK);
    }
}
  • getFileList를 통해 게시글에 등록된 첨부파일(이미지)의 정보를 리턴받는다 (최종로직의 1, 2번에 해당)
  • getViewImage를 통해 이미지를 리턴 (최종로직의 3번의 일부에 해당)

3. Service

@Service
public class FileService {

    private final FileMapper fileRepository;

    @Autowired
    public FileService(FileMapper fileMapper) {
        this.fileRepository = fileMapper;
    }

    // BoardId로 File리스트 반환
    public List<FileDTO> getFileList(Long boardId) throws SQLException {
        List<FileDTO> fileList = fileRepository.selectByBoardId(boardId);
        return fileList;
    }

    // ImageID로 FileDTO 반환
    public FileDTO getFile(Long id) throws SQLException {
        FileDTO fileDTO = fileRepository.selectByFileId(id);
        return fileDTO;
    }
}
  • getFileList : FileDTO 타입의 List를 반환
  • getFile : 컨트롤러의 getViewImage에서 단일 이미지를 다루기 때문에 불러오고자 하는 이미지 데이터를 DB에서 조회하여 fileDTO객체 하나만을 리턴

4. View (상세 글 페이지 일부)

							.
							.
							.
                    <h2>제목 : <span th:text="${board.title}">글 제목</span></h2>
                </div>
                <hr />
                <div class="mb-3">
                    <textarea type="text" class="form-control" th:text="${board.content}" rows="13"
                        style="background-color: white;" readonly>내용</textarea>
                </div>

                <div id="image">
                </div>

                <!-- Comment -->
                <h4>댓글<span>(<span id="count"></span>)</span></h4>
                <div class="mb-5">
                    <div id="comment">
                    </div>
							.
							.
							.
  • 위는 글 제목, 내용을 나타내고 아래는 이전에 다뤘던 댓글이 나타나는 곳!
  • <div id="image"></div> 에 자바스크립트를 통해 등록된 이미지를 추가 할 예정

5. Script (이미지 로드 부분)

    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
    <script th:inline="javascript">
        $(document).ready(function () {
            getImgId() // 이미지 불러오기
            getComments() // 댓글 불러오기
        })

        // 해당 게시글에 등록된 이미지 ID를 리스트로 불러오기
        function getImgId() {
            var boardId = $('input[name=boardId]').val()
            var url = '/images/' + boardId
            var imgId = []

            $.ajax({
                type: 'GET',
                url: url,
                success: function (response) {
                    var idx = 0;

                    $.each(response, function (key, value) {
                        imgId[idx] = value.id
                        idx++
                    })
                    getImg(imgId)
                },
                error: function (response) {
                    console.log("이미지 ID 로딩 실패")
                }
            })
        }

        // 불러온 이미지 ID로 이미지 src요청
        function getImg(imgId) {
            var boardId = $('input[name=boardId]').val()
            var url = ''
            var a = ''

            for (var i = 0; i < imgId.length; i++) {
                url = '/image/' + imgId[i]
                a += '<img class="img-thumbnail mb-3 me-3" style="width: 200px; height: 200px" src="' + url + '" onclick="window.open(this.src)"/>'
            }
            $('#image').html(a)
        }
    </script>

페이지 로드가 완료되면 getImgId() 함수 실행

  • ajax 통신을 통해 서버의 getFileList를 실행
  • imgId 배열을 생성하고, ajax 통신에 성공하면 List의 FileDTO getId값을 배열에 초기화
  • value.id : 서버에서의 FileDTO.getId

getImgId()함수를 통해 배열에 이미지 id값이 모두 저장됐으면 getImg함수를 실행

  • img 태그를 생성하고 src에 서버의 getViewImage를 호출하는 url 값을 넣어준다.
  • 최종적으로 $('#image').html(a)를 통해 image태그 자식으로 img태그를 추가한다.

6. 결과

추가된 이미지

이 전 글에서 테스트로 추가한 이미지들이 보여진다. (귀엽다)

 

 

 

 

 

 

 

* [스프링에서 이미지를 다루는 방법] 참고 : https://www.baeldung.com/spring-mvc-image-media-data

 

728x90

게시글 작성에서 파일 첨부하는 방법을 기록한다. (본 글은 이미지 첨부 방법을 다룸)

 


1. Mysql - file 테이블

CREATE TABLE `tb_file` (
  `id` int NOT NULL AUTO_INCREMENT,
  `board_id` int NOT NULL,
  `original_file_name` varchar(100) NOT NULL,
  `size` int NOT NULL,
  `path` varchar(100) NOT NULL,
  `stored_file_name` varchar(100) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `board_id` (`board_id`),
  CONSTRAINT `tb_file_ibfk_1` FOREIGN KEY (`board_id`) REFERENCES `tb_board` (`id`) ON DELETE CASCADE
)
  • orginal_file_name : 사용자가 저장한 원본 파일이름
  • size : KB단위의 파일의 용량
  • path : 파일이 저장된 위치
  • stred_file_name : 서버에 저장된 uuid로 중복을 제거한 이름

2. FileDTO

@Data
public class FileDTO {
    private Long id;
    private Long boardId;
    private String originalFileName;
    private Long size;
    private String path;
    private String storedFileName;

    public FileDTO(Long boardId, String originalFileName, String storedFileName, Long size, String path) {
        this.boardId = boardId;
        this.originalFileName = originalFileName;
        this.storedFileName = storedFileName;
        this.size = size;
        this.path = path;
    }
}

3. 게시글 작성 Controller

    // 게시글 작성 & 수정
    @PostMapping("/form")
    public String boardSubmit(@Valid Board board, BindingResult bindingResult, Principal principal,
                              @RequestParam(value = "files", required = false) List<MultipartFile> files, Long id) throws IOException, SQLException {
        if (bindingResult.hasErrors() || files.size() > 7) {
            return "board/form";
        }

        String loginUsername = principal.getName();
        Long newBoardId = 0l;

        if (id == null) { // 새 글 작성
            newBoardId = boardService.save(board, loginUsername); // Insert

            // 첨부파일 있을 때
            if(!files.get(0).getOriginalFilename().isEmpty()) {
                for(int i = 0; i < files.size(); i ++) {
                    if(files.get(i).getContentType().contains("image/")) {
                        fileService.saveFile(files.get(i), newBoardId);
                    } else {
                        System.out.println("이미지 타입이 아닙니다");
                    }
                }
            }
        } else { // 기존 글 수정
            boardService.update(board, id); // Update
        }

        return "redirect:/board/list";
    }
  • @Valid : 게시글 작성 중에 제목과 내용의 제약조건(제목 2글자 이상 등)을 지키지 않으면 첫 줄의 if문을 통해 게시글 작성 홈페이지로 다시 돌아간다.
  • !files.get(0).getOriginalFilename().isEmpty() : MultipartFile 타입의 List의 첫 값을 통해 첨부파일의 존재 여부를 확인
  • getContentType을 통해 첨부파일의 형태가 IMAGE인지 확인 후 저장 로직 실행
  • @RequestParam(value = "files", required = false) : 값이 필수가 아닐 때 required = false 작성. (안적어주면 파일 첨부하지 않을 때 오류 Bad request 400 오류 발생)

4. Service

@Service
public class FileService {

    private final FileMapper fileRepository;

    @Autowired
    public FileService(FileMapper fileMapper) {
        this.fileRepository = fileMapper;
    }

    // 게시글 작성 & 수정에서 첨부파일 추가
    public void saveFile(MultipartFile file, Long boardId) throws IOException, SQLException {
        String uuid = UUID.randomUUID().toString(); // 파일명 중복 제거를 위한 uuid
        String originName = file.getOriginalFilename(); // 파일 원본명
        Long fileSize = file.getSize() / 1024; // kb

        String path = "C:/Temp/";
        String newName = "";

        if (originName.lastIndexOf(".") < 0) {
            newName = uuid + originName; // 확장자명이 없을 때
        } else {
            newName = uuid + StringUtils.substring(originName, originName.lastIndexOf(".")); //확장자명 포함
        }

        FileDTO fileDTO = new FileDTO(boardId, originName, newName, fileSize, path + newName);
        int result = fileRepository.insertFile(fileDTO);

        if (result > 0 && !file.getOriginalFilename().isEmpty()) {
            file.transferTo(new File(path + newName));
        }
    }
}
  • UUID를 이용해서 서버에 저장되는 파일명의 중복을 막는다.
  • StringUtils.substring을 이용해 확장자를 포함시켜 저장
  • result값을 받아서 insert가 바르게 진행 됐는지 확인 (바르게 저장 시 1 리턴)
  • MultipartFile.transferTo : if문 조건(result값과 MultipartFile의 null 체크)을 만족하면 서버로 첨부파일(이미지) 저장

5. View

    <main class="flex-shrink-0">
        <div class="container mt-3">
            <h2>게시판</h2>
            <form action="#" th:action="@{/board/form}" th:object="${board}" method="post"
                enctype="multipart/form-data">
                <input type="hidden" th:field="*{id}">
                <input type="hidden" th:field="*{writerId}">
                <div class="mb-3">
                    <label for="title" class="form-label">Title</label>
                    <input type="text" class="form-control"
                        th:classappend="${#fields.hasErrors('title')} ? 'is-invalid'" id="title" th:field="*{title}">
                    <div th:if="${#fields.hasErrors('title')}" th:errors="*{title}" id="validationServer03Feedback"
                        class="invalid-feedback">
                        Title Error
                    </div>
                </div>
                <div class="mb-3">
                    <label for="content" class="form-label">content</label>
                    <textarea class="form-control" id="content" rows="13"
                        th:classappend="${#fields.hasErrors('content')} ? 'is-invalid'"
                        th:field="*{content}"></textarea>
                    <div th:if="${#fields.hasErrors('content')}" th:errors="*{content}" id="validationServer03Feedback"
                        class="invalid-feedback">
                        Content Error
                    </div>
                </div>

                <div id="imageThumbnail">
                </div>

                <!-- button -->
                <div id="uploadForm" th:if="${param.boardId == null}">
                    <div id="uploadElement">
                        <input id="uploadInput" type="file" class="btn btn-outline-primary" name="files" accept="image/*"
                            onchange="setThumbnail(event);" multiple /> <span style="font-size: small;"> * jpeg / png 타입의
                            이미지를
                            7개까지
                            등록해주세요.</span>
                    </div>
                    <a id="reset" class="mt-3 btn btn-danger" onclick="resetImg()">Reset</a>
                </div>
                
                <div class="nav justify-content-end mb-5">
                    <button id="submit" type="submit" class="me-2 btn btn-primary">write</button>
                    <a type="button" class="btn btn-primary" th:href="@{/board/list}">exit</a>
                </div>
            </form>
        </div>
    </main>

    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
        crossorigin="anonymous"></script>
  • th:if를 이용해서 새로운 글 작성 할 때만 이미지 첨부를 가능하게 제약조건 추가 (글 수정으로 글 작성 form으로 진입하는 경우 첨부파일 버튼을 html에서 제거)
  • <form> : enctype 속성을 "multipart/form-data" 로 설정해줘야 파일을 컨트롤러로 전달 가능
  • <div> : id="imageThumbnail"  <-- 여기에 첨부된 파일의 미리보기를 불러올 예정 (썸네일)
  • <input type="file" accept="image/*" multiple/> : accept로 첨부파일 선택 시 모든파일 -> image타입의 파일을 선택하도록 유도. multiple을 사용해야 다중 파일 선택 가능

6. script

    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script>
        document.getElementById('submit').addEventListener('click', imageCounting)

        // 첨부 이미지 리셋 버튼
        function resetImg() {
            var fileUpload = document.getElementById('uploadInput')
            var img = document.querySelectorAll('img')
            fileUpload.value = null
            $('#imageThumbnail').children().remove()
        }

        // 이미지 개수 제한
        function imageCounting() {
            var fileUpload = $("input[type='file']")
            if (parseInt(fileUpload.get(0).files.length) > 7) {
                alert("이미지는 최대 7개까지 등록 가능합니다.")
            }
        }

        // 첨부 이미지 미리보기
        function setThumbnail(event) {
            for (var image of event.target.files) {
                var reader = new FileReader()

                reader.onload = function (event) {
                    var img = document.createElement("img")
                    img.className = "img-thumbnail mb-3 me-3"
                    img.name = "thumbnail"
                    img.style = "inline"
                    img.width = 200
                    img.height = 200
                    img.setAttribute("src", event.target.result)
                    document.querySelector("div#imageThumbnail").appendChild(img)
                }
                reader.readAsDataURL(image)
            }
        }
    </script>

 - resetImg (첨부 이미지 리셋, 첨부파일 리셋)

  • fileUpload.value = null 을 통해 지금까지 불러온 첨부파일 값을 null로 설정
  • $('#imageThumbnail').children().remove() : imageThumbnail 아이디의 태그 자식태그를 제거

 - imgCounting (첨부 개수제한)

  • $("input[type='file']") 를 이용해서 타입이 file인 input 태그를 가져와 저장

 - setThumbnail

  • FileReader를 이용해서 이미지 미리보기(썸네일) 정보 셋팅

7. 결과

테스트 전 file테이블 상태

 

 

시연

 

* 왜인지 모르겠으나 녹화할 때 파일 선택창이 안나와서 추가!

시연 후 MySQL 테이블
시연 후 추가된 파일들

8. 게시글 작성, 수정 (일부 코드 수정) : https://black-mint.tistory.com/70

 

[Spring Boot] 게시글 작성, 수정

이전 파일 업로드 글과 연관지어 게시글 작성의 구현을 마무리하고 이를 정리하겠습니다. 파일첨부에 대해서는 언급하지 않겠습니다! 이전 글 : https://black-mint.tistory.com/62?category=982467 [Spring Boot].

black-mint.tistory.com


6번 - setThumbnail 참고 : https://sinna94.tistory.com/entry/JavaScript-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C-%EB%AF%B8%EB%A6%AC%EB%B3%B4%EA%B8%B0-%EB%A7%8C%EB%93%A4%EA%B8%B0

728x90

1. 파일 삭제

File file = new File("C:/Temp/abc.jpg");

if(file.exists()) { //파일이 존재한다면
	file.delete(); // 삭제
	System.out.println(fileDTO.getStoredFileName() + " : 삭제 완료");
}
else {
	System.out.println("삭제할 파일이 없습니다.");
}
  • File 객체를 생성할 때 생성자에 존재하는 파일을 지정
  • File.delete()를 사용해서 삭제 가능
  • File.exists() : 파일의 존재 여부
728x90

Spring Boot프로젝트 중 게시글에 첨부된 이미지 데이터를 DAO를 통해 List로 반환하려한다.

이 때 반환받은 List가 Null인지 체크할 때 사용

  


1. Spring을 사용하는 경우 (CollectionUtils.isEmpty)

List<FileDTO> files = fileService.getFileList(boardId);

CollectionUtils.isEmpty(files)
  • FileDTO(이미지 파일 정보)를 DB에서 게시판 아이디로 조회하여 받아오는 코드이다.
  • CollectionUtils.isEmpty()를 이용해서 List가 null인지 체크 가능

2. Java

List<String> list = new ArrayList<>();

list.isEmpty();
  • isEmpty()를 이용해서 List가 null인지 체크 가능

 

* 참고 : https://jihyehwang09.github.io/2020/04/13/java-list-null-check/

 

Java- List의 Null을 체크하는 법

다양한 List의 Null 체크 방법들이 있는데, 어떤 방법이 효과적일지에 대해 정리해보도록 하자. TL;DRList의 Null Check를 할 때는Spring에서 Apach Commons 라이브러리의 CollectionUtils.isEmpty()를 사용하자. Null

JihyeHwang09.github.io

 

728x90
$(#comment).html('<spen>댓글</spen>');
  • id값이 comment 인 태그의 요소를 <span>댓글</spen>으로 바꿈

* 출처 : https://www.codingfactory.net/10324

 

jQuery / Method / .html() - 선택한 요소 안의 내용을 가져오거나, 다른 내용으로 바꾸는 메서드

.html() .html()은 선택한 요소 안의 내용을 가져오거나, 다른 내용으로 바꿉니다. .text()와 비슷하지만 태그의 처리가 다릅니다. 문법 1 .html() HTML 태그를 포함하여 선택한 요소 안의 내용을 가져옵니

www.codingfactory.net

 

* 사용 프로젝트 : https://black-mint.tistory.com/35

 

[Spring Boot] Ajax(비동기) 통신으로 댓글 구현 (+ Jquery 사용법)

게시판 댓글을 구현하는 도중 비동기 호출에 대해 알게 됐고, 이를 이용하려면 Ajax를 활용해야 한다는 정보를 얻었다. 비동기란? 비동기의 반대인 동기적 통신의 경우 절차적으로 일을 차례로

black-mint.tistory.com

 

728x90

게시판 첨부파일(이미지) 불러오기를 구현 중 만난 오류를 기록한다.

 


1. 테이블

CREATE TABLE `tb_file` (
  `id` int NOT NULL AUTO_INCREMENT,
  `board_id` int NOT NULL,
  `original_file_name` varchar(100) NOT NULL,
  `stored_file_name` varchar(100) NOT NULL,
  `size` int NOT NULL,
  `path` varchar(100) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `board_id` (`board_id`),
  CONSTRAINT `tb_file_ibfk_1` FOREIGN KEY (`board_id`) REFERENCES `tb_board` (`id`) ON DELETE CASCADE
)
  • id : 첨부파일 id
  • board_id : 첨부파일이 등록된 게시판 id
  • original_file_name : 첨부파일의 원래 이름 (확장자 포함)
  • stored_file_name : 첨부파일이 저장될 때 이름 (uuid로 중복제거한 이름)
  • size : 크기
  • path : 저장된 주소

2. FileMapper.xml

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.min.board.repository.mapper.FileMapper">

    <sql id="fileColumns">
        id
        ,board_id
        ,original_file_name
        ,stored_file_name
        ,size
        ,path
    </sql>

    <!-- 이미지 불러오기 -->
    <select id="selectByBoardId" resultType="FileDTO">
        SELECT
            <include refid="fileColumns"/>
        FROM
            tb_file
        WHERE
            board_id = #{boardId}
    </select>
</mapper>

3. FileDTO

@Data
public class FileDTO {
    private Long id;
    private Long boardId;
    private String originalFileName;
    private String storedFileName;
    private Long size;
    private String path;

    public FileDTO(Long boardId, String originalFileName, String storedFileName, Long size, String path) {
        this.boardId = boardId;
        this.originalFileName = originalFileName;
        this.storedFileName = storedFileName;
        this.size = size;
        this.path = path;
    }
}

4. 오류 내용

4.1 java.lang.NumberFormatException: For input string "컬럼데이터"

org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.executor.result.ResultMapException: Error attempting to get column 'stored_file_name' from result set.  Cause: java.lang.NumberFormatException: For input string: "a48e"

	at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:96)
	at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:441)
	at com.sun.proxy.$Proxy84.selectList(Unknown Source)
	at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:224)
	at org.apache.ibatis.binding.MapperMethod.executeForMany(MapperMethod.java:147)
	at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:80)
	at org.apache.ibatis.binding.MapperProxy$PlainMethodInvoker.invoke(MapperProxy.java:145)
	at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:86)
	at com.sun.proxy.$Proxy90.selectByBoardId(Unknown Source)
	at com.min.board.service.FileService.getFileList(FileService.java:50)
	at com.min.board.service.FileServiceTest.getFileListTest(FileServiceTest.java:23)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:688)
	at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
	at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
	at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$6(TestMethodTestDescriptor.java:210)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:206)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:131)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:65)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:143)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:143)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:108)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:96)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:75)
	at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:71)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
	at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:235)
	at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)
Caused by: org.apache.ibatis.executor.result.ResultMapException: Error attempting to get column 'stored_file_name' from result set.  Cause: java.lang.NumberFormatException: For input string: "a48e"
	at org.apache.ibatis.type.BaseTypeHandler.getResult(BaseTypeHandler.java:87)
	at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.createUsingConstructor(DefaultResultSetHandler.java:711)
	at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.createByConstructorSignature(DefaultResultSetHandler.java:694)
	at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.createResultObject(DefaultResultSetHandler.java:658)
	at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.createResultObject(DefaultResultSetHandler.java:631)
	at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.getRowValue(DefaultResultSetHandler.java:398)
	at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleRowValuesForSimpleResultMap(DefaultResultSetHandler.java:355)
	at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleRowValues(DefaultResultSetHandler.java:329)
	at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleResultSet(DefaultResultSetHandler.java:302)
	at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleResultSets(DefaultResultSetHandler.java:195)
	at org.apache.ibatis.executor.statement.PreparedStatementHandler.query(PreparedStatementHandler.java:65)
	at org.apache.ibatis.executor.statement.RoutingStatementHandler.query(RoutingStatementHandler.java:79)
	at org.apache.ibatis.executor.SimpleExecutor.doQuery(SimpleExecutor.java:63)
	at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:325)
	at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:156)
	at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:109)
	at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:89)
	at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:151)
	at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:145)
	at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:140)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:427)
	... 74 more
Caused by: java.lang.NumberFormatException: For input string: "a48e"
	at java.base/jdk.internal.math.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2054)
	at java.base/jdk.internal.math.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
	at java.base/java.lang.Double.parseDouble(Double.java:543)
	at com.mysql.cj.protocol.a.MysqlTextValueDecoder.getDouble(MysqlTextValueDecoder.java:249)
	at com.mysql.cj.result.AbstractNumericValueFactory.createFromBytes(AbstractNumericValueFactory.java:56)
	at com.mysql.cj.protocol.a.MysqlTextValueDecoder.decodeByteArray(MysqlTextValueDecoder.java:143)
	at com.mysql.cj.protocol.result.AbstractResultsetRow.decodeAndCreateReturnValue(AbstractResultsetRow.java:135)
	at com.mysql.cj.protocol.result.AbstractResultsetRow.getValueFromBytes(AbstractResultsetRow.java:243)
	at com.mysql.cj.protocol.a.result.ByteArrayRow.getValue(ByteArrayRow.java:91)
	at com.mysql.cj.jdbc.result.ResultSetImpl.getObject(ResultSetImpl.java:1321)
	at com.mysql.cj.jdbc.result.ResultSetImpl.getLong(ResultSetImpl.java:848)
	at com.mysql.cj.jdbc.result.ResultSetImpl.getLong(ResultSetImpl.java:854)
	at com.zaxxer.hikari.pool.HikariProxyResultSet.getLong(HikariProxyResultSet.java)
	at org.apache.ibatis.type.LongTypeHandler.getNullableResult(LongTypeHandler.java:37)
	at org.apache.ibatis.type.LongTypeHandler.getNullableResult(LongTypeHandler.java:26)
	at org.apache.ibatis.type.BaseTypeHandler.getResult(BaseTypeHandler.java:85)
	... 98 more

4.2 해결 방법을 시도하다가 만난 오류 - Cause: java.sql.SQLDataException: Cannot determine value type from string "컬럼 데이터" (캡처 못함)

5. 해결 시도

왜 String(JAVA) 타입에 VARCHAR 타입의 DB 데이터를 넣는데 NumberFormatException이 뜨는가..?>??!!?!?!

정말 여러 시도, 검색을 해봤지만 다른 오류(4.2)를 다시 가져올뿐이고, 검색에서 나오는 내용은 전혀 관련없는 Mapper.xml의 if문을 다루는 이야기 뿐이었다. (나는 if문을 쓰지 않았는데 말이다 ㅠ)

 

Mapper.xml의 resultType을 resultMap으로 바꿔봤다.

MySQL의 컬럼을 삭제하고 다시 생성도 해봤고, 컬럼명을 변경도 해봤다.

MySQL의 컬럼 type을 바꿔보기도 하고 사이즈 크기도 변경하는 등 여러 시도를 반복했지만 해결하지 못했다.

 

그러던중 혹시 original 이름과 uuid로 변형한 이름인 stored 이름의 컬럼을 떨어뜨려놔볼까? 라는 생각을 해봤다. (많은 시간을 오류와 싸우다 정신이 나간 듯)

6. 해결 방법

기존 [1, 2, 3]번의 코드 상태를 다음과 같이 바꿨다.

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.min.board.repository.mapper.FileMapper">

    <sql id="fileColumns">
        id
        ,board_id
        ,original_file_name
        ,size
        ,path
        ,stored_file_name
    </sql>

    <!-- 이미지 불러오기 -->
    <select id="selectByBoardId" resultType="FileDTO">
        SELECT
            <include refid="fileColumns"/>
        FROM
            tb_file
        WHERE
            board_id = #{boardId}
    </select>
</mapper>

 

 

@Data
public class FileDTO {
    private Long id;
    private Long boardId;
    private String originalFileName;
    private Long size;
    private String path;
    private String storedFileName;

    public FileDTO(Long boardId, String originalFileName, String storedFileName, Long size, String path) {
        this.boardId = boardId;
        this.originalFileName = originalFileName;
        this.storedFileName = storedFileName;
        this.size = size;
        this.path = path;
    }
}

 

 

CREATE TABLE `tb_file` (
  `id` int NOT NULL AUTO_INCREMENT,
  `board_id` int NOT NULL,
  `original_file_name` varchar(100) NOT NULL,
  `size` int NOT NULL,
  `path` varchar(100) NOT NULL,
  `stored_file_name` varchar(100) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `board_id` (`board_id`),
  CONSTRAINT `tb_file_ibfk_1` FOREIGN KEY (`board_id`) REFERENCES `tb_board` (`id`) ON DELETE CASCADE
)
  • 달라진 점 : 기존 코드는 original_file_name(originalFileName) 옆에 stored_file_name(storedFileName)가 위치해 있었다. 그러나 달라진 코드에서는 original_file_name에서 2칸 떨어진 위치에 stored_file_name이 위치해있다.

놀랍게도 이렇게 했더니 오류가 해결됐다!

728x90

첨부파일 관련 기능 구현 중 게시글 insert와 update 시 auto increment되는 게시글 번호를(PK) 가져와서 활용하려한다.

Mybatis에서 insert나 update 시 PK 값을 가져오는 방법을 기록한다.

 


1. Mapper 인터페이스

@Mapper
public interface BoardMapper {
	// 글 작성
	void insertBoard(Board board);
}
  • 파라미터로 받아진 Board 객체의 필드에 PK 값이 자동으로 들어간다. 때문에 void로 작성 가능

2. Mapper.xml

    <!--게시글 작성-->
    <insert id="insertBoard" parameterType="Board" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO
            tb_board (title, content, writer_id, writer, create_date)
        VALUES
            (#{title}, #{content}, #{writerId}, #{writer}, #{createDate})
    </insert>
  • useGeneratedKeys와 keyProperty (PK)를 추가해주면 insert, update 시 자동 증가하는 PK 값을 Board의 필드 안에 자동 주입해준다.

+ Recent posts