Java/Spring Boot

Spring Boot 기초(file) - AWS 풀스택 과정 79일차

awspspgh 2024. 11. 18. 09:28
목차
1. file

 

1. file

application.properties

spring.application.name=spring

server.port=8089

# 타임리프 캐싱 끄기. 새로고침 반영 설정
spring.thymeleaf.cache=false
spring.thymeleaf.enabled=true
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html

# DB 설정
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.jdbc-url=jdbc:mysql://localhost:3306/bootdb
spring.datasource.username=springUser
spring.datasource.password=mysql

# mybatis
mybatis.mapper-locations=classpath:/mappers/*.xml
mybatis.configuration.map-underscore-to-camel-case=true

#fileUpload 경로
spring.servlet.multipart.enabled=true
spring.servlet.multipart.max-file-size=20MB
spring.servlet.multipart.max-request-size=40MB
spring.servlet.multipart.location=D:/_myProject/_java/_fileUpload

 

BoardDTO.java

package com.ezen.spring.domain;

import lombok.*;

import java.util.List;

@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class BoardDTO {
    private BoardVO bvo;
    private List<FileVO> flist;
}

 

 

FileVO.java

package com.ezen.spring.domain;

import lombok.*;

@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class FileVO {
    private String uuid;
    private String saveDir;
    private String fileName;
    private int fileType;
    private long bno;
    private long fileSize;
    private String regDate;
}

 

 FileHandler.java

package com.ezen.spring.handler;

import com.ezen.spring.domain.FileVO;
import groovy.util.logging.Slf4j;
import net.coobird.thumbnailator.Thumbnails;
import org.apache.tika.Tika;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

@Slf4j
@Component
public class FileHandler {
    private String UP_DIR = "D:\\_myProject\\_java\\_fileUpload\\";

    public List<FileVO> uploadFiles(MultipartFile[] files) {
        List<FileVO> flist = new ArrayList<>();
        LocalDate date = LocalDate.now();
        // 2024-11-15 =? 2024\\11\\15
        String today = date.toString().replace("-", File.separator);

        // D:\_myProject\_java\_fileUpload\2024\11\15
        File folders = new File(UP_DIR, today);

        // mkdir = 1개의 폴더만 // mkdirs = 여러 개
        if(!folders.exists()){
            folders.mkdirs(); // 여러 개 생성
        }

        // FileVO 생성
        for(MultipartFile file : files){
            // file => name / size
            FileVO fvo = new FileVO();
            fvo.setSaveDir(today);
            fvo.setFileSize(file.getSize());

            // file.name => 경로를 포함하는 경우도 있음. /test/test.txt
            String originalFileName = file.getOriginalFilename();
            String onlyFileName = originalFileName.substring(originalFileName.lastIndexOf(File.separator)+1);
            fvo.setFileName(onlyFileName);

            UUID uuid = UUID.randomUUID();
            String uuidStr = uuid.toString();
            fvo.setUuid(uuidStr);

            // ----- fvo 설정 마무리
            // 디스크 저장
            String fileName = uuidStr + "_" + onlyFileName;
            File storeFile = new File(folders, fileName);

            // 저장
            try {
                file.transferTo(storeFile);
                // 파일 타입 : 그림파일만 썸네일 생성
                if(isImageFile(storeFile)){
                    fvo.setFileType(1);
                    File thumbnail = new File(folders, uuidStr+"_th_"+fileName);
                    Thumbnails.of(storeFile).size(100, 100).toFile(thumbnail);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            // for문 안
            flist.add(fvo);

        }

        return flist;
    }

    private boolean isImageFile(File file) throws IOException {
        String mimeType = new Tika().detect(file);
        return mimeType.startsWith("image");
    }
}

 

 FileRemoveHandler.java

package com.ezen.spring.handler;

import com.ezen.spring.domain.FileVO;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.io.File;

@Slf4j
@Component
public class FileRemoveHandler {
    private final String BASE_PATH = "D:\\_myProject\\_java\\_fileUpload\\";

    public boolean deleteFile(FileVO fvo){
        File delFile = new File(BASE_PATH, fvo.getSaveDir());
        boolean isDel = false;
        String delete = fvo.getUuid()+"_"+fvo.getFileName();
        try{
            File deleteFile = new File(delFile, delete);
            log.info("deleteFile {}", deleteFile);
            isDel = deleteFile.delete();
            if(fvo.getFileType()>0){
                String deleteThumb = fvo.getUuid()+"_th_"+fvo.getFileName();
                File deleteThumbFile = new File(delFile, deleteThumb);
                log.info("deleteThumb {}", deleteThumbFile);
                if(deleteThumbFile.exists()){
                    deleteThumbFile.delete();
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        }

        return isDel;
    }
}

 

 register.html

<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layout/layout}">

<div layout:fragment="content" class="container-md">
    <h1>Boot Register page</h1>
  <hr>

  <div>
  <form action="/board/register" method="post" enctype="multipart/form-data">
  <div class="mb-3">
    <label for="t" class="form-label">Title</label>
    <input type="text" class="form-control" name="title" id="t" placeholder="title...">
  </div>
  <div class="mb-3">
    <label for="w" class="form-label">Writer</label>
    <input type="text" class="form-control" name="writer" id="w" placeholder="writer...">
  </div>
  <div class="mb-3">
    <label for="c" class="form-label">Content</label>
    <textarea class="form-control" name="content" id="c" rows="3"></textarea>
  </div>
    <!-- file 추가 라인 -->
    <div class="mb-3">
      <label for="file" class="form-label">File</label>
      <input type="file" class="form-control" name="files" id="file" multiple style="display:none;">
    </div>
    <button type="button" id="trigger" class="btn btn-primary">File Upload</button>

    <!-- file 출력 라인 -->
    <div class="input-group mb-3" id="fileZone"></div>

      <button type="submit" class="btn btn-primary" id="regBtn">register</button>
    </form>
  <script th:src="@{/js/boardRegister.js}"></script>
  </div>
</div>

 

 detail.html

<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layout/layout}">

<div layout:fragment="content" class="container-md">
    <h1>Boot Detail page</h1>
    <hr>

    <th:block th:with="bvo=${bdto.bvo}">
    <form action="/board/modify" method="post" id="modForm" enctype="multipart/form-data">
        <div class="mb-3">
            <label for="r" class="form-label">Created At</label>
            <input type="text" class="form-control" name="regDate" id="r" th:value="${bvo.regDate}" readonly>
        </div><div class="mb-3">
            <input type="hidden" class="form-control" name="bno" id="n" th:value="${bvo.bno}">
        </div>
        <div class="mb-3">
            <label for="title" class="form-label">Title</label>
            <input type="text" class="form-control" name="title" id="title" th:value="${bvo.title}" readonly>
        </div>
        <div class="mb-3">
            <label for="writer" class="form-label">Writer</label>
            <input type="text" class="form-control" name="writer" id="writer" th:value="${bvo.writer}" readonly>
        </div>
        <div class="mb-3">
            <label for="content" class="form-label">Content</label>
            <textarea class="form-control" name="content" id="content" rows="3" readonly>[[${bvo.content}]]</textarea>
        </div>
        <!-- file print line -->
        <div class="mb-3">
            <ul class="list-group">
                <li th:each="fvo:${bdto.flist}" class="list-group-item">
                    <div th:if="${fvo.fileType > 0}" class="ms-2 me-auto">
                        <img th:src="@{|/upload/${fvo.saveDir}/${fvo.uuid}_${fvo.fileName}|}" alt="img" />
                    </div>
                    <div th:unless="${fvo.fileType > 0}" class="ms-2 me-auto">
                        <!-- icon -->
                        <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-paperclip" viewBox="0 0 16 16">
                            <path d="M4.5 3a2.5 2.5 0 0 1 5 0v9a1.5 1.5 0 0 1-3 0V5a.5.5 0 0 1 1 0v7a.5.5 0 0 0 1 0V3a1.5 1.5 0 1 0-3 0v9a2.5 2.5 0 0 0 5 0V5a.5.5 0 0 1 1 0v7a3.5 3.5 0 1 1-7 0z"/>
                        </svg>
                    </div>
                    <div class="ms-2 me-auto">
                        <div class="fw-bold">[[${fvo.fileName}]]</div>
                        [[${fvo.regDate}]]
                    </div>
                    <span class="badge text-bg-success rounded-pill">[[${fvo.fileSize}]]Bytes</span>
                    <button type="button" th:data-uuid="${fvo.uuid}" class="btn btn-outline-danger bnt-nm file-x" disabled>x</button>
                </li>
            </ul>
        </div>
        <!-- file 추가 라인 -->
        <div class="mb-3">
            <input type="file" class="form-control" name="files" id="file" multiple style="display:none;">
        </div>
        <button type="button" id="trigger" class="btn btn-primary" disabled>File Upload</button> <br>
        <button type="button" id="listBtn" class="btn btn-primary">List</button>
        <!-- de-tail page에서 modify 상태로 변경하기 위한 버튼 -->
        <button type="button" id="modBtn" class="btn btn-warning">Modify</button>
        <a th:href="@{/board/delete(bno=${bvo.bno})}">
            <button type="button" id="delBtn" class="btn btn-danger">Delete</button>
        </a>
    </form>
    </th:block>

    <script th:src="@{/js/boardDetail.js}"></script>
    <script th:src="@{/js/boardRegister.js}"></script>

</div>

 

 BoardController.java

package com.ezen.spring.controller;

import com.ezen.spring.domain.BoardDTO;
import com.ezen.spring.domain.BoardVO;
import com.ezen.spring.domain.FileVO;
import com.ezen.spring.domain.PagingVO;
import com.ezen.spring.handler.FileHandler;
import com.ezen.spring.handler.FileRemoveHandler;
import com.ezen.spring.handler.PagingHandler;
import com.ezen.spring.service.BoardService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import java.util.List;

@RequestMapping("/board/*")
@RequiredArgsConstructor
@Slf4j
@Controller
public class BoardController {
    private final BoardService bsv;
    private final FileHandler fh;

    @GetMapping("/register")
    public String register(){
        return"/board/register";
    }

    @PostMapping("/register")
    public String register(BoardVO boardVO, @RequestParam(name = "files", required = false)MultipartFile[] files){
        log.info(">>>> boardVO >>>> {}", boardVO);
        List<FileVO> flist = null;
        if(files[0].getSize() > 0 && files != null){
            flist = fh.uploadFiles(files);
            log.info(">>>> flist >>>> {}", flist);
        }
        int isOk = bsv.register(new BoardDTO(boardVO, flist));

        return "index";
    }

    @GetMapping("/list")
    public String list(Model m, PagingVO pgvo){
        // 전체 게시글 수 가져오기
        int totalCount = bsv.getTotalCount(pgvo);
        PagingHandler ph = new PagingHandler(pgvo, totalCount);
        m.addAttribute("list", bsv.getList(pgvo));
        m.addAttribute("ph", ph);
        return"/board/list";
    }

    @GetMapping("/detail")
    public String detail(@RequestParam("bno") long bno, Model m){
        m.addAttribute("bdto", bsv.getDetail(bno));
        return "/board/detail";
    }

    @PostMapping("/modify")
    public String modify(BoardVO bvo, RedirectAttributes redirectAttributes, @RequestParam(value = "files", required = false)MultipartFile[] files){
        List<FileVO> flist = null;
        if(files[0].getSize()>0){
            flist = fh.uploadFiles(files);
        }
        int isOk = bsv.update(new BoardDTO(bvo, flist));
        redirectAttributes.addAttribute("bno", bvo.getBno());
        return "redirect:/board/detail";
    }

    @GetMapping("/delete")
    public String delete(@RequestParam("bno") long bno){
        int isOk = bsv.delete(bno);
        return "redirect:/board/list";
    }

    @ResponseBody
    @DeleteMapping("/file/{uuid}")
    public String removeFile(@PathVariable("uuid") String uuid){
        FileVO fvo = bsv.getFile(uuid);
        int isOk = bsv.removeFile(uuid);
        // 파일 삭제
        FileRemoveHandler fr = new FileRemoveHandler();
        boolean isDel = fr.deleteFile(fvo);
        return (isOk > 0 && isDel) ? "1" : "0";
    }
}

 

 BoardService.java

package com.ezen.spring.service;

import com.ezen.spring.domain.BoardDTO;
import com.ezen.spring.domain.BoardVO;
import com.ezen.spring.domain.FileVO;
import com.ezen.spring.domain.PagingVO;

import java.util.List;

public interface BoardService {
//    int register(BoardDTO boardVO);
      int register(BoardDTO boardDTO);

    List<BoardVO> getList(PagingVO pgvo);

    BoardDTO getDetail(long bno);

    int update(BoardDTO boardDTO);

    int delete(long bno);

    int getTotalCount(PagingVO pgvo);

    int removeFile(String uuid);

    FileVO getFile(String uuid);
}

 

 BoardServiceImpl.java

package com.ezen.spring.service;

import com.ezen.spring.domain.BoardDTO;
import com.ezen.spring.domain.BoardVO;
import com.ezen.spring.domain.FileVO;
import com.ezen.spring.domain.PagingVO;
import com.ezen.spring.repository.BoardMapper;
import com.ezen.spring.repository.FileMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.List;

@Slf4j
@RequiredArgsConstructor
@Service
public class BoardServiceImpl implements  BoardService{
    private final BoardMapper boardMapper;
    private final FileMapper fileMapper;

    @Override
    public int register(BoardDTO boardDTO) {
        int isOk = boardMapper.register(boardDTO.getBvo());
        if(boardDTO.getFlist()==null){
            return isOk;
        }
        if(isOk > 0 && boardDTO.getFlist().size() > 0){
            // 파일저장
            // board의 bno 가져오기 => 가장 큰 bno
            long bno = boardMapper.getBno();
            for(FileVO fvo : boardDTO.getFlist()){
                fvo.setBno(bno);
                isOk *= fileMapper.insertFile(fvo);
            }
        }
        return isOk;
    }

    @Override
    public List<BoardVO> getList(PagingVO pgvo) {
        return boardMapper.getList(pgvo);
    }

    @Override
    public BoardDTO getDetail(long bno) {
        // fileList 가져와서 DTO 생성
        BoardDTO bdto = new BoardDTO(boardMapper.getDetail(bno), fileMapper.getFileList(bno));
        return bdto;
    }

    @Override
    public int update(BoardDTO boardDTO) {
        int isOk = boardMapper.update(boardDTO.getBvo());
        if(boardDTO.getFlist()==null){
            return isOk;
        }
        if(isOk > 0 && !boardDTO.getFlist().isEmpty()){
            for(FileVO fvo : boardDTO.getFlist()){
                fvo.setBno(boardDTO.getBvo().getBno());
                isOk *= fileMapper.insertFile(fvo);
            }
        }
        return isOk;
    }

    @Override
    public int delete(long bno)
    {
        return boardMapper.delete(bno);
    }

    @Override
    public int getTotalCount(PagingVO pgvo) {
        return boardMapper.getTotalCount(pgvo);
    }

    @Override
    public int removeFile(String uuid) {
        return fileMapper.removeFile(uuid);
    }

    @Override
    public FileVO getFile(String uuid) {
        return fileMapper.getFile(uuid);
    }
}

 

 BoardMapper.java

package com.ezen.spring.repository;

import com.ezen.spring.domain.BoardDTO;
import com.ezen.spring.domain.BoardVO;
import com.ezen.spring.domain.PagingVO;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface BoardMapper {
    int register(BoardDTO boardDTO);

    int register(BoardVO boardVO);

    List<BoardVO> getList(PagingVO pgvo);

    BoardVO getDetail(long bno);

    int update(BoardVO bvo);

    int delete(long bno);

    int getTotalCount(PagingVO pgvo);

    long getBno();

}

 

 boardMapper.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.ezen.spring.repository.BoardMapper">
    <insert id="register">
        insert into board(title, writer, content)
        values(#{title}, #{writer}, #{content})
    </insert>
    <select id="getBno" resultType="long">
        select max(bno) from board;
    </select>
    <select id="getList" resultType="com.ezen.spring.domain.BoardVO">
        select * from board 
        <include refid="search"></include>
        order by bno desc
        limit #{startIndex}, #{qty}
    </select>
    <select id="getDetail" resultType="com.ezen.spring.domain.BoardVO">
        select * from board where bno = #{bno}
    </select>
    <update id="update">
        update board set title = #{title}, content = #{content}, reg_date = now()
        where bno = #{bno}
    </update>
    <delete id="delete">
        delete from board where bno = #{bno}
    </delete>
    <select id="getTotalCount" resultType="int">
        select count(bno) from board
        <include refid="search"></include>
    </select>
    <sql id="search">
        <if test="type != null">
            <trim prefix="where (" suffix=")" suffixOverrides="or">
                <foreach collection="typeToArray" item="type">
                    <trim suffix="or">
                        <choose>
                            <when test="type=='t'.toString()">
                                title like concat('%', #{keyword}, '%')
                            </when>
                            <when test="type=='w'.toString()">
                                title like concat('%', #{keyword}, '%')
                            </when>
                            <when test="type=='c'.toString()">
                                title like concat('%', #{keyword}, '%')
                            </when>
                        </choose>
                    </trim>
                </foreach>
            </trim>
        </if>
    </sql>
</mapper>

 

 FileMapper.java

package com.ezen.spring.repository;

import com.ezen.spring.domain.FileVO;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface FileMapper {
    int insertFile(FileVO fvo);

    List<FileVO> getFileList(long bno);

    int removeFile(String uuid);

    FileVO getFile(String uuid);
}

 

 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.ezen.spring.repository.FileMapper">
    <insert id="insertFile">
        insert into file(uuid, save_dir, file_name, file_type, bno, file_size)
        values(#{uuid}, #{saveDir}, #{fileName}, #{fileType}, #{bno}, #{fileSize})
    </insert>
    <select id="getFileList" resultType="com.ezen.spring.domain.FileVO">
        select * from file where bno = #{bno}
    </select>
    <delete id="removeFile">
        delete from file where uuid = #{uuid}
    </delete>
    <select id="getFile">
        select * from file where uuid = #{uuid}
    </select>
</mapper>

 

 WebMvcConfig.java

package com.ezen.spring.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceChainRegistration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    String uploadPath = "file:///D:\\_myProject\\_java\\_fileUpload\\";

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/upload/**")
                .addResourceLocations(uploadPath);
    }

}

 

 boardDetail.js

console.log("boardDetail.js in!!");

document.getElementById('listBtn').addEventListener('click', () => {
    // 리스트로 이동
    location.href="/board/list";
});

document.getElementById('modBtn').addEventListener('click', ()=>{
// title, content의 readonly를 해지 readOnly = true / false
    document.getElementById('title').readOnly = false;
    document.getElementById('content').readOnly = false;

    // modBtn delBtn 삭제
    document.getElementById('modBtn').remove();
    document.getElementById('delBtn').remove();

    // modBtn => submit
    // <button></button>
    let modBtn = document.createElement('button');
    // <button type="submit"></button> 
    modBtn.setAttribute('type','submit'); 
    modBtn.setAttribute('id','regBtn'); 
    // class="btn btn-warning"
    modBtn.classList.add('btn','btn-outline-warning');
    // <button type="submit" class="btn btn-outline-warning">submit</button>
    modBtn.innerText="submit";

    // form 태그의 자식 요소로 추가 - form에 가장 마지막에 추가
    document.getElementById('modForm').appendChild(modBtn);

    // file-x 버튼 disabled 해지
    let fileDelBtn = document.querySelectorAll(".file-x");
    console.log(fileDelBtn);
    for(let delBtn of fileDelBtn){
        delBtn.disabled = false;
    }

    // 파일 업로드 버튼 disabled 해지
    document.getElementById('trigger').disabled = false;

});

document.addEventListener('click',(e)=>{
    if(e.target.classList.contains('file-x')){
        console.log(e.target);
        let uuid = e.target.dataset.uuid;
        fileRemoveToServer(uuid).then(result => {
            if(result > 0){
                alert("파일 삭제 성공");
                e.target.closest('li').remove();
            }
        })
    }
});

// 비동기 데이터 보내기
async function fileRemoveToServer(uuid) {
    try{
        const url = '/board/file/'+uuid;
        const config = {
            method:"delete"
        }
        const resp = await fetch(url, config);
        const result = await resp.text();
        return result;
    }catch(error){
        console.log(error);
    }
}

 

 boardRegister.js

console.log("boardRegister.js in!!");
document.getElementById('trigger').addEventListener('click',()=>{
 document.getElementById('file').click();
});

// 실행파일 막기 / 20MB 이상
const regExp = new RegExp("\.(exe|sh|bat|jar|dll|msi)$");
const maxSize = 1024*1024*20;

function fileValidation(fileName, fileSize){
    if(regExp.test(fileName)){
        return 0;
    }else if(fileSize > maxSize){
        return 0;
    }else{
        return 1;
    }
}

document.addEventListener('change',(e)=>{
    if(e.target.id == 'file'){
        const fileObject = document.getElementById('file').files;
        console.log(fileObject);

        document.getElementById('regBtn').disabled = false;

        const fileZone = document.getElementById('fileZone');
        // 이전에 추가한 파일 삭제
        fileZone.innerHTML = "";
        let ul = `<ul class="list-group list-group-flush">`;
        let isOk = 1; // 여러 파일에 대한 값을 확인하기 위해 1 *
        for(let file of fileObject){
            let vaild = fileValidation(file.name, file.size);
            isOk *= vaild;
            ul += `<li class="list-group-item">`;
            ul += `<div class="ms-2 me-auto">`;
            ul += `${vaild ? '<div class="fw-bold">업로드 가능</div>' : '<div class="fw-bold text-danger">업로드 불가능</div>'}`;
            ul += `${file.name}</div>`;
            ul += `<span class="badge text-bg-${vaild ? 'success' : 'danger'} rounded-pill">${file.size}Bytes</span></li>`;
        }
        ul += `</ul>`;
        fileZone.innerHTML = ul;

        if(isOk == 0){
            document.getElementById('regBtn').disabled = true;
        }
    }
});
/* <ul class="list-group list-group-flush">
<li class="list-group-item">An item</li>
</ul> */

 

 Application.java

package com.ezen.spring;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@EnableScheduling
@SpringBootApplication
public class Application {

	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}

}

 

출력