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
$(#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의 필드 안에 자동 주입해준다.
728x90

체크박스에 체크하고 삭제하는 글관리 구현 중 전체 체크박스를 체크하는 기능을 구현한다.

 


1. html 코드

                <table class="table caption-top table-bordered table-hover">
                    <caption>List</caption>
                    <thead>
                        <tr>
                            <th class="text-center" width="50" scope="col">
                                <input type="checkbox" onclick="selectAll(this)">
                            </th>
                            <th class="text-center" width="50" scope="col">No</th>
                            <th class="text-center" width="950" scope="col">제목</th>
                            <th class="text-center" width="200" scope="col">작성일</th>
                            <th class="text-center" width="180" scope="col">작성자</th>
                        </tr>
                    </thead>

                    <tbody>
                        <tr th:each="board : ${boardList}">
                            <td class="mt-5 text-center" scope="row">
                                <div class="checkbox">
                                    <input type="checkbox" name="boardIdList" th:value="${board.id}">
                                </div>
                            </td>
                            <td class="mt-5 text-center" scope="row" th:text="${board.id}">1</td>
                            <td><a th:text="${board.title}" th:href="@{/board/post(boardId=${board.id})}"
                                    style="text-decoration:none; color:black;">제목</a></td>
                            <td class="text-center" th:text="${#dates.format(board.createDate, 'yyyy/MM/dd HH:mm')}">작성일
                            </td>
                            <td class="text-center" th:text="${board.writer}">작성자</td>
                        </tr>
                    </tbody>
                </table>
  • 체크박스에 onclick 이벤트를 달아준다.

2. 스크립트 코드

    <script>
        function selectAll(selectAll) {
            var checkboxs = document.querySelectorAll(['input[type="checkbox"]'])

            checkboxs.forEach((checkbox) => {
                checkbox.checked = selectAll.checked
            })
        }
    </script>
  • document.querySelectorAll : 모든 체크박스 쿼리를 가져옴
  • this로 체크박스를 받았기 때문에 this체크박스가 체크되면 모든 체크박스가 체크되도록 코드 작성

* 출처 : https://hianna.tistory.com/432

728x90

휴지통 삭제, 복원을 구현하는 방법으로 ajax 통신을 사용하기로 한다.

이 때 체크박스 체크된 게시글만 삭제, 복원 가능하도록 하려고한다.

 -> 리스트로 boardId를 서버로 보내서 처리하도록 한다!


1. Controller

    // 휴지통 게시글 복원
    @PatchMapping("/trash")
    @ResponseBody
    public void restoreBoards(@RequestParam(name = "boardIdList[]", required = false) List<String> boardIdList) {
        for(int i = 0; i < boardIdList.size(); i ++) {
            boardService.restore(Long.parseLong(boardIdList.get(i)));
        }
    }

    // 휴지통 게시글 영구 삭제
    @DeleteMapping("/trash")
    @ResponseBody
    public void clearBoards(@RequestParam(name = "boardIdList[]", required = false) List<String> boardIdList) {
        for(int i = 0; i < boardIdList.size(); i ++) {
            boardService.clear(Long.parseLong(boardIdList.get(i)));
        }
    }
  • @RequestParam(name = "boardIdList[]") : 배열을 List로 받을 것이기 때문에 변수 끝에 대괄호([])를 붙여준다.

2. View

            <!-- contents -->
            <form class="row g-3 justify-content-end">
                <table class="table caption-top table-bordered table-hover">
                    <caption>List</caption>
                    <thead>
                        <tr>
                            <th class="text-center" width="50" scope="col"></th>
                            <th class="text-center" width="50" scope="col">No</th>
                            <th class="text-center" width="950" scope="col">제목</th>
                            <th class="text-center" width="200" scope="col">작성일</th>
                            <th class="text-center" width="180" scope="col">작성자</th>
                        </tr>
                    </thead>

                    <tbody>
                        <tr th:each="board : ${boardList}">
                            <td class="mt-5 text-center" scope="row">
                                <div class="checkbox">
                                    <input type="checkbox" name="boardIdList" th:value="${board.id}">
                                </div>
                            </td>
                            <td class="mt-5 text-center" scope="row" th:text="${board.id}">1</td>
                            <td><a th:text="${board.title}" th:href="@{/board/post(boardId=${board.id})}"
                                    style="text-decoration:none; color:black;">제목</a></td>
                            <td class="text-center" th:text="${#dates.format(board.createDate, 'yyyy/MM/dd HH:mm')}">작성일
                            </td>
                            <td class="text-center" th:text="${board.writer}">작성자</td>
                        </tr>
                    </tbody>
                </table>
                <div class="nav justify-content-end">
                    <button type="submit" class="btn btn-primary me-2" onclick="restoreBoards()">복원하기</button>
                    <button type="submit" class="btn btn-danger" onclick="deleteBoards()">삭제하기</button>
                </div>
            </form>
  • form 태그안에 버튼을 생성하고 각 버튼의 onclick을 등록한다.

3. ajax(script코드)

    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script th:inline="javascript">
        // 게시글 복원
        function restoreBoards() {
            var boardIdList = []

            $('input[name="boardIdList"]:checked').each(function (i) {
                boardIdList.push($(this).val());
            });

            if (!isEmptyArr(boardIdList)) {
                var param = { "boardIdList": boardIdList }

                $.ajax({
                    url: "/trash",
                    type: "PATCH",
                    data: param,
                    async: false,
                    success: function (response) {
                        alert('복원 됐습니다.')
                        location.reload()
                    },
                    error: function (response) {
                        alert('복원에 실패했습니다.')
                    }

                })
            } else {
                alert('복원할 게시글을 선택해주세요.')
            }
        }

        // 게시글 삭제
        function deleteBoards() {
            var boardIdList = []

            $('input[name="boardIdList"]:checked').each(function (i) {
                boardIdList.push($(this).val());
            });

            if (!isEmptyArr(boardIdList)) {
                var param = { "boardIdList": boardIdList }

                $.ajax({
                    url: "/trash",
                    type: "DELETE",
                    data: param,
                    async: false,
                    success: function (response) {
                        alert('영구 삭제 됐습니다.')
                        location.reload()
                    },
                    error: function (response) {
                        alert('영구 삭제 실패했습니다.')
                    }

                })
            } else {
                alert('삭제할 게시글을 선택해주세요.')
            }

        }

        // 빈배열 체크
        function isEmptyArr(arr) {
            if (Array.isArray(arr) && arr.length === 0) {
                return true;
            }

            return false;
        }
    </script>
  • var boardIdList = [] : 자바스크립트 배열 선언
  • $('input[name="boardIdList"]:checked').each(function (i) {  boardIdList.push($(this).val());  }); 
    • 체크박스에 체크된 개수만큼 each로 반복.
    • push를 통해 값을 배열에 초기화
  • isEmptyArr : 배열이 비었는지 체크

4. 결과

시연

 

+ 3번 스크립트 코드 수정!

만약 다중 게시글을 삭제하는데 팝업이 뜨지 않는다면? 당연하다.. 비동기적 통신으로 ajax를 실행해서 그렇다.

이를 해결하려면 동기적으로 ajax 통신을 사용해야하는데, async: false를 ajax 코드에 추가해주면된다.


* ajax 사용하기 : https://black-mint.tistory.com/35

 

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

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

black-mint.tistory.com

 

728x90
            var boardIdList = []

            $('input[name="boardIdList"]:checked').each(function (i) {
                boardIdList.push($(this).val());
            });
  • 이름이 boardIdList인 input태그의 체크된 값을 가져옴.
  • each로 반복하여 체크 개수만큼 push

* 출처 https://hanke-r.tistory.com/24

 

JavaScript - 체크박스 다중선택 value값

체크박스 다중선택 value값 가져오기 $("input[name=tblChk]:checked").each(function(){ var test = $(this).val(); console.log("체크된 값 : " + test); }); 체크박스 다중선택 value값 배열에 담기 var arTest..

hanke-r.tistory.com

 

+ Recent posts