스프링부트(Spring Boot) MyBatis 게시판 만들기1 - CRUD 처리 [Mysql, DBeaver, IntelliJ, Maven]

예림·2024년 6월 5일
2

스프링부트 MyBatis 게시판 CRUD


개발 환경

  • 언어: java

  • Spring Boot ver : 2.3.1.RELEASE

  • Mybatis : 2.3.0

  • IDE: intelliJ

  • SDK: JDK 17

  • 의존성 관리툴: Maven

  • DB: MySQL 8.0.12

  • 구조도


Project Settings


  • pom.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    	<modelVersion>4.0.0</modelVersion>
    	<parent>
    		<groupId>org.springframework.boot</groupId>
    		<artifactId>spring-boot-starter-parent</artifactId>
    		<version>2.3.1.RELEASE</version>
    		<relativePath/> <!-- lookup parent from repository -->
    	</parent>
    	<groupId>org.study</groupId>
    	<artifactId>board</artifactId>
    	<version>0.0.1-SNAPSHOT</version>
    	<packaging>war</packaging>
    	<name>board</name>
    	<description>board</description>
    	<properties>
    		<java.version>1.8</java.version>
    	</properties>
    	<dependencies>
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-web</artifactId>
    		</dependency>
    		<dependency>
    			<groupId>org.mybatis.spring.boot</groupId>
    			<artifactId>mybatis-spring-boot-starter</artifactId>
    			<version>2.3.0</version>
    		</dependency>
    
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-devtools</artifactId>
    			<scope>runtime</scope>
    			<optional>true</optional>
    		</dependency>
    		<dependency>
    			<groupId>mysql</groupId>
    			<artifactId>mysql-connector-java</artifactId>
    			<version>8.0.12</version>
    		</dependency>
    		<dependency>
    			<groupId>org.projectlombok</groupId>
    			<artifactId>lombok</artifactId>
    			<optional>true</optional>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-tomcat</artifactId>
    			<scope>provided</scope>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-test</artifactId>
    			<scope>test</scope>
    		</dependency>
    		<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
    		<dependency>
    			<groupId>org.mybatis</groupId>
    			<artifactId>mybatis</artifactId>
    			<version>3.4.6</version>
    		</dependency>
    		<dependency>
    			<groupId>org.bgee.log4jdbc-log4j2</groupId>
    			<artifactId>log4jdbc-log4j2-jdbc4.1</artifactId>
    			<version>1.16</version>
    		</dependency>
    		<!-- https://mvnrepository.com/artifact/javax.servlet/jstl -->
    		<dependency>
    			<groupId>javax.servlet</groupId>
    			<artifactId>jstl</artifactId>
    			<version>1.2</version>
    		</dependency>
    		<dependency>
    			<groupId>org.apache.tomcat.embed</groupId>
    			<artifactId>tomcat-embed-jasper</artifactId>
    			<scope>provided</scope>
    		</dependency>
    		<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-thymeleaf -->
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-thymeleaf</artifactId>
    			<version>3.2.4</version>
    		</dependency>
    		<!-- https://mvnrepository.com/artifact/javax.validation/validation-api -->
    		<dependency>
    			<groupId>javax.validation</groupId>
    			<artifactId>validation-api</artifactId>
    			<version>2.0.0.Final</version>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework.security</groupId>
    			<artifactId>spring-security-crypto</artifactId>
    		</dependency>
    		<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
    		<!-- 대용량 파일 처리 -->
    		<dependency>
    			<groupId>commons-io</groupId>
    			<artifactId>commons-io</artifactId>
    			<version>2.6</version>
    		</dependency>
    
    		<!-- commons-fileupload -->
    		<!-- multipart등을 사용할수 있음 -->
    		<dependency>
    			<groupId>commons-fileupload</groupId>
    			<artifactId>commons-fileupload</artifactId>
    			<version>1.3.1</version>
    		</dependency>
    	</dependencies>
    
    	<build>
    		<plugins>
    			<plugin>
    				<groupId>org.springframework.boot</groupId>
    				<artifactId>spring-boot-maven-plugin</artifactId>
    				<configuration>
    					<excludes>
    						<exclude>
    							<groupId>org.projectlombok</groupId>
    							<artifactId>lombok</artifactId>
    						</exclude>
    					</excludes>
    				</configuration>
    			</plugin>
    		</plugins>
    	</build>
    
    </project>
    
  • application.properties
    # [DATABASE] - MySQL
    custom.db.type=mysql
    spring.datasource.url=jdbc:log4jdbc:mysql://58.151.212.83:33090/board_study?serverTimezone=UTC&characterEncoding=UTF-8
    spring.datasource.username=msync
    spring.datasource.password=msync9867
    spring.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
    
    spring.mvc.view.prefix=/WEB-INF/jsp/views/
    spring.mvc.view.suffix=.jsp
    
    # mybatis \uB9E4\uD551 type\uC744 \uC9E7\uAC8C \uC4F0\uAE30 \uC704\uD55C \uC124\uC815
    mybatis.type-aliases-package=org.study.board
    # mybatis mapper \uC704\uCE58
    mybatis.mapper-locations=classpath:mapper/*.xml
    # mybatis camelCase \uBCC0\uD658 \uC124\uC815
    mybatis.configuration.map-underscore-to-camel-case=true
    # mybatis \uC870\uD68C \uACB0\uACFC \uBAA8\uB4E0 \uCEEC\uB7FC\uC774 null\uC778 \uACBD\uC6B0 null \uBC18\uD658 \uBC29\uC9C0
    mybatis.configuration.return-instance-for-empty-row=true
    
    server.servlet.jsp.init-parameters.development=true
    
    spring.servlet.multipart.enabled=true
    spring.servlet.multipart.maxFileSize=20MB
    spring.servlet.multipart.maxRequestSize=100MB
    spring.servlet.multipart.location=D:\\image
    
    upload.path=D:/image
  • log4j2.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <!-- appender : where 로그의 출력 위치(파일, 콘솔, DB등)를 결정-->
    <!-- layout : how 어떤 형식으로 로그를 출력할지 결정 -->
    <!-- message : what -->
    <!-- logger : who 로깅 메시지를 appender에 전달. logger는 6단계의 레벨을 가지며, 레벨을 가지고 출력여부를 결정 -->
    <!-- logging event level : logger가 메시지를 기록할 지 여부를 결정하는 기준. -->
    <!-- debug -> info -> warn -> error -> fatal -->
    <!-- logger에 설정된 이벤트 등급 이상의 이벤트만 기록. -->
    
    <Configuration status="WARN">
        <Appenders>
            <Console name="Console" target="SYSTEM_OUT">
                <PatternLayout pattern="[%t] %-5level %logger{1.} - %msg%n"/>
            </Console>
        </Appenders>
        <Loggers>
            <Logger name="kr.or.ddit" level="info" additivity="false">
                <AppenderRef ref="Console"/>
            </Logger>
            <Logger name="jdbc.sqltiming" level="debug" additivity="false">
                <AppenderRef ref="Console"/>
            </Logger>
            <Logger name="jdbc.resultsettable" level="debug" additivity="false">
                <AppenderRef ref="Console"/>
            </Logger>
            <Root level="error">
                <AppenderRef ref="Console"/>
            </Root>
        </Loggers>
    </Configuration>
  • log4jdbc.log4j2.properties
    # log4jdbc log is processed through slf4j
    # log4jdbc spy의 로그 이벤트를 slf4j를 통해 처리한다
    log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator
    
    # log4jdbc log length is unlimited
    # 로그를 í‘œì‹œí•  줄의 ì œí•œ, 0은 ë¬´ì œí•œ
    log4jdbc.dump.sql.maxlinelength=0
    
    # log4jdbc driv class setting
    # log4jdbc의 드라이브 클래스 ì„¤ì •
    # com.mysql.cj.jdbc.Driver를 사용하도록 ëª…ì‹œí•˜ê³  드라이버를 자동으로 로드하지 않도록 false값을 부여
    log4jdbc.drivers=com.mysql.cj.jdbc.Driver
    log4jdbc.auto.load.popular.drivers=false

DTO


  • User.java
    package org.study.board.dto;
    
    import lombok.Data;
    
    import java.sql.Timestamp;
    
    @Data
    public class User {
    
        private Long idx;
        private String userId;
        private String password;
        private String username;
        private Timestamp regdate;
    
    }
    
  • Board.java
    package org.study.board.dto;
    
    import lombok.Data;
    
    import java.util.Date;
    import java.util.List;
    
    @Data
    public class Board {
    
        private Integer bno;
        private String title;
        private String content;
        private String writer;
        private Date regdate;
        private int hit;
        private boolean deleteYn;
        private boolean noticeYn;
        private Integer userIdx;
    
        /*첨부파일*/
        private List<FileDto> list;
        private String filename;
        private String[] uuids;
        private String[] filenames;
        private String[] contentTypes;
    
    }
    
  • FileDto.java
    package org.study.board.dto;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    public class FileDto {
        private String filename;
        private String uuid;
        private String contentType;
    
    }
    
  • JoinForm.java
    package org.study.board.dto;
    
    import lombok.Getter;
    import lombok.NoArgsConstructor;
    import lombok.Setter;
    
    import javax.validation.constraints.NotBlank;
    
    @Getter
    @Setter
    @NoArgsConstructor
    public class JoinForm {
    
        @NotBlank(message = "로그인 아이디가 비어있습니다.")
        private String loginId;
    
        @NotBlank(message = "비밀번호가 비어있습니다.")
        private String password;
        private String passwordCheck;
    
        @NotBlank(message = "닉네임이 비어있습니다.")
        private String username;
    
    }
  • LoginForm.java
    package org.study.board.dto;
    
    import lombok.Data;
    
    @Data
    public class LoginForm {
    
        private String loginId;
        private String password;
    }
    

Util


  • FileUtil.java
    package org.study.board.util;
    
    import org.springframework.web.multipart.MultipartFile;
    import org.study.board.dto.FileDto;
    
    import java.io.File;
    import java.io.IOException;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.UUID;
    
    public class FileUtil {
        public static List<FileDto> uploadFile(MultipartFile[] uploadFile){
            List<FileDto> list = new ArrayList<>();
    
            for (MultipartFile file : uploadFile) {
                if (!file.isEmpty()) {
                    // UUID를 이용해 unique한 파일 이름을 만들어준다.
                    FileDto dto = new FileDto();
                    dto.setUuid(UUID.randomUUID().toString());
                    dto.setFilename(file.getOriginalFilename());
                    dto.setContentType(file.getContentType());
    
                    list.add(dto);
    
                    File newFileName = new File(dto.getUuid() + "_" + dto.getFilename());
                    try {
                        file.transferTo(newFileName);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
    
            return list;
        }
    }

User


  • UserController.java
    package org.study.board.controller;
    
    import lombok.RequiredArgsConstructor;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.validation.BindingResult;
    import org.springframework.validation.FieldError;
    import org.springframework.web.bind.annotation.*;
    import org.study.board.dto.JoinForm;
    import org.study.board.dto.LoginForm;
    import org.study.board.dto.User;
    import org.study.board.service.UserService;
    
    import javax.servlet.http.Cookie;
    import javax.servlet.http.HttpServletResponse;
    import java.util.List;
    
    @RequiredArgsConstructor
    @Controller
    public class UserController {
    
        private static final Logger log = LoggerFactory.getLogger(UserController.class);
        @Autowired
        private UserService service;
    
        @GetMapping("/user/main")
        public String userList(Model model) {
            List<User> users = service.getAllUsers();
            model.addAttribute("users", users);
            return "user/main";
        }
    
        @GetMapping("/join")
        public String join(Model model) {
    
            model.addAttribute("JoinForm", new JoinForm());
            return "join";
        }
    
        @PostMapping("/join")
        public String join(@ModelAttribute("JoinForm") JoinForm form, BindingResult bindingResult) {
            // loginId 중복 체크
            if(service.checkLoginIdDuplicate(form.getLoginId())) {
                bindingResult.addError(new FieldError("joinForm", "loginId", "존재하는 아이디입니다."));
            }
    
            if (bindingResult.hasErrors()) {
                return "join";
            }
    
            if (!form.getPassword().equals(form.getPasswordCheck())) {
                bindingResult.rejectValue("passwordCheck", "passwordCheck", "비밀번호가 일치하지 않습니다.");
                return "join";
            }
    
            service.join(form);
            return "redirect:/login";
        }
    
        @GetMapping("/login")
        public String loginForm(Model model) {
    
            model.addAttribute("loginForm", new LoginForm());
            return "login";
        }
    
        @PostMapping("/login")
        public String login(@ModelAttribute("loginForm") LoginForm form, BindingResult bindingResult, HttpServletResponse response) {
    
            User loginUser = service.login(form.getLoginId(), form.getPassword());
            log.info("login? {}", loginUser);
    
            if (loginUser == null) {
                bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
            }
    
            if (bindingResult.hasErrors()) {
                return "login";
            }
    
            Cookie cookie = new Cookie("idx", String.valueOf(loginUser.getIdx()));
            cookie.setMaxAge(60 * 60);  // 쿠키 유효 시간 : 1시간
            response.addCookie(cookie);
    
            // 로그인 성공 처리
            return "redirect:/main";
        }
    
        @GetMapping("/logout")
        public String logout(HttpServletResponse response) {
    
            Cookie cookie = new Cookie("idx", null);
            cookie.setMaxAge(0); //쿠키 종료
            response.addCookie(cookie);
            return "redirect:/main";
        }
    
        @GetMapping("/user/info/{userId}")
        public String userInfo(@PathVariable String userId, Model model) {
            log.info("Fetching user info for userId: {}", userId);
            User user = service.getLoginUser(userId);
            if (user == null) {
                log.warn("User not found for userId: {}", userId);
                return "redirect:/login";
            }
            log.info("User found: {}", user);
            model.addAttribute("user", user);
            return "user/info";
        }
    
        @GetMapping("/check-username")
        @ResponseBody
        public boolean checkUsername(@RequestParam String loginId) {
            return service.checkLoginIdDuplicate(loginId);
        }
    
    }
    
  • UserService.java
    package org.study.board.service;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.stereotype.Service;
    import org.study.board.controller.UserController;
    import org.study.board.dto.JoinForm;
    import org.study.board.dto.User;
    import org.study.board.repository.UserMapper;
    
    import java.util.List;
    import java.util.Optional;
    
    @Service
    public class UserService {
        private static final Logger log = LoggerFactory.getLogger(UserService.class);
        @Autowired
        private UserMapper mapper;
    
        private PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
    
        public List<User> getAllUsers() {
            return mapper.findAllUsers();
        }
    
        public void join(JoinForm form) {
            User user = new User();
            user.setUserId(form.getLoginId());
            user.setUsername(form.getUsername());
            user.setPassword(passwordEncoder.encode(form.getPassword()));  // 비밀번호 암호화
            mapper.save(user);
        }
    
        public boolean checkLoginIdDuplicate(String loginId) {
            log.info("Checking loginId in service: {}", loginId);
            int count = mapper.existsByLoginId(loginId);
            log.info("Count result: {}", count);
            return count > 0;
        }
    
        public User login(String userId, String password) {
            Optional<User> userOptional = Optional.ofNullable(mapper.findByLoginId(userId));
            if (userOptional.isPresent()) {
                User user = userOptional.get();
                if (passwordEncoder.matches(password, user.getPassword())) {  // 비밀번호 확인
                    return user;
                } else {
                    throw new RuntimeException("비밀번호가 일치하지 않습니다.");
                }
            } else {
                throw new RuntimeException("사용자를 찾을 수 없습니다.");
            }
        }
    
        public User getLoginUser(String userId) {
            if(userId == null) return null;
    
            Optional<User> optionalUser = Optional.ofNullable(mapper.findByLoginId(userId));
            return optionalUser.orElse(null);
        }
    }
    
  • UserMapper.java
    package org.study.board.repository;
    
    import org.apache.ibatis.annotations.Mapper;
    import org.study.board.dto.JoinForm;
    import org.study.board.dto.User;
    
    import java.util.List;
    
    @Mapper
    public interface UserMapper {
        User findById(Long idx);
        User findByLoginId(String userId);
        void save(User user);
        int existsByLoginId(String loginId);
        List<User> findAllUsers();
    }
    
  • userMapper.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="org.study.board.repository.UserMapper">
    
        <select id="findById" resultType="org.study.board.dto.User"
                parameterType="long">
            SELECT *
            FROM user
            WHERE idx = #{idx}
        </select>
    
        <select id="findByLoginId" resultType="org.study.board.dto.User"
                parameterType="String">
            SELECT *
            FROM user
            WHERE userId = #{userId}
        </select>
    
        <insert id="save" parameterType="org.study.board.dto.User">
            INSERT INTO user (userId, password, username, regdate)
            VALUES (#{userId}, #{password}, #{username}, NOW())
        </insert>
    
        <select id="findAllUsers" resultType="org.study.board.dto.User">
            SELECT *
            FROM user
        </select>
    
        <select id="existsByLoginId" resultType="_int"
                                    parameterType="String">
            SELECT count(*)
            FROM user
            WHERE userId=#{loginId}
        </select>
    </mapper>

Board


  • BoardController.java
    package org.study.board.controller;
    
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.io.FileUtils;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.core.io.Resource;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.*;
    import org.springframework.web.multipart.MultipartFile;
    import org.study.board.dto.Board;
    import org.study.board.dto.FileDto;
    import org.study.board.dto.User;
    import org.study.board.repository.BoardMapper;
    import org.study.board.repository.UserMapper;
    import org.study.board.service.BoardService;
    import org.study.board.util.FileUtil;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletRequestWrapper;
    import java.io.File;
    import java.io.IOException;
    import java.io.InputStream;
    import java.util.List;
    import java.util.Map;
    import java.util.UUID;
    
    @Slf4j
    @Controller
    public class BoardController {
    
        @Autowired
        private BoardService boardService;
        @Autowired
        private UserMapper userMapper;
        @Autowired
        private HttpServletRequest request;
    
        @RequestMapping("/main")
        public String main(@CookieValue(name="idx", required = false) Long idx, Board board, Model model){
    
            List<Board> boardList = boardService.getBoardlist(board);
            if(idx == null){
                model.addAttribute("board", boardList);
                return "board/main";
            }
            //로그인
            User loginUser=userMapper.findById(idx);
            if(loginUser == null){
                return "login";
            }
            model.addAttribute("user", loginUser);
            model.addAttribute("board", boardList);
            return "board/main";
        }
    
        @GetMapping("/board/{bno}")
        public String boardDetail(@PathVariable Integer bno, Model model, Board board){
            Board boardDetail = boardService.getBoard(bno);
            List<FileDto> file = boardService.getFile(board);
    
            model.addAttribute("board", boardDetail);
            model.addAttribute("getFile", file);
            return "board/write";
        }
    
        @RequestMapping("/write")
        public String write(@CookieValue(name="idx", required = false) Long idx, Model model, Board board){
            User loginUser=userMapper.findById(idx);
            model.addAttribute("user", loginUser);
            if(board.getBno()==null){
                model.addAttribute("getBoard", board);
                model.addAttribute("getFile", boardService.getFile(board));
            }
            return "board/write";
        }
    
        @RequestMapping("/insertBoard")
        public String insertBoard(@ModelAttribute Board board, @CookieValue(name="idx", required = false) Long idx, Model model) {
            User loginUser=userMapper.findById(idx);
            board.setWriter(loginUser.getUsername());
            model.addAttribute("user", loginUser);
            boardService.insertBoard(board);
            return "redirect:/main";
        }
    
        @DeleteMapping("/delete/{bno}")
        public ResponseEntity<String> deleteBoard(@PathVariable Integer bno) {
            boolean deleted = boardService.deleteBoard(bno);
            if (deleted) {
                return ResponseEntity.ok("게시물이 성공적으로 삭제되었습니다.");
            } else {
                return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                        .body("게시물 삭제 중에 오류가 발생했습니다.");
            }
        }
    
        /*ajax로 첨부파일 처리*/
        @RequestMapping("/ajaxFile")
        @ResponseBody
        public List<FileDto> ajaxFile(@RequestParam("files") MultipartFile[] uploadFile) {
            // 파일 등록
            List<FileDto> fileList = FileUtil.uploadFile(uploadFile);
            return fileList;
        }
    
        /*파일 다운로드*/
        @RequestMapping("/downloadFile")
        public ResponseEntity<Resource> downloadFile(@ModelAttribute FileDto fileDto) throws IOException {
            return boardService.downloadFile(fileDto);
        }
    
    }
    
  • BoardService.java
    package org.study.board.service;
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.core.io.InputStreamResource;
    import org.springframework.core.io.Resource;
    import org.springframework.http.ContentDisposition;
    import org.springframework.http.HttpHeaders;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.stereotype.Service;
    import org.study.board.dto.Board;
    import org.study.board.dto.FileDto;
    import org.study.board.repository.BoardMapper;
    
    import java.io.IOException;
    import java.nio.charset.StandardCharsets;
    import java.nio.file.Files;
    import java.nio.file.Path;
    import java.nio.file.Paths;
    import java.util.ArrayList;
    import java.util.List;
    
    @Slf4j
    @Service
    public class BoardService {
    
        @Autowired
        private BoardMapper mapper;
    
        public List<Board> getBoardlist(Board board) {
            return mapper.getBoardList(board);
        }
    
        // 첨부파일 리스트
        public List<FileDto> getFile (Board board) {return mapper.getFile(board);}
    
        public Board getBoard(Integer bno){
            return mapper.getBoard(bno);
        }
    
        public void insertBoard(Board board){
            if (board.getBno() != null) {
                mapper.deleteFile(board);
            }
            if(board.getBno()!=null){
                mapper.updateBoard(board);
            }else{
                mapper.insertBoard(board);
            }
            // 파일 이름 유니크하게 생성
            List<FileDto> list = new ArrayList<>();
            String[] uuids = board.getUuids();
            String[] fileNames = board.getFilenames();
            String[] contentTypes = board.getContentTypes();
    
            if(uuids!=null){
                for(int i=0;i<uuids.length;i++){
                    FileDto fileDto = new FileDto();
                    fileDto.setFilename(fileNames[i]);
                    fileDto.setUuid(uuids[i]);
                    fileDto.setContentType(contentTypes[i]);
                    list.add(fileDto);
                }
            }
    
            // 첨부파일 등록 (게시글이 있을경우)
            if (!list.isEmpty()) {
                board.setList(list);
                mapper.insertFile(board);
            }
        }
    
        // 파일 다운로드
        public ResponseEntity<Resource> downloadFile(FileDto fileDto) throws IOException {
            // 파일 저장 경로 설정
            String filePath = "d:\\image";
            Path path = Paths.get(filePath + "/" + fileDto.getUuid() + "_" + fileDto.getFilename());
            String contentType = Files.probeContentType(path);
            // header를 통해서 다운로드 되는 파일의 정보를 설정한다.
            HttpHeaders headers = new HttpHeaders();
            headers.setContentDisposition(ContentDisposition.builder("attachment")
                    .filename(fileDto.getFilename(), StandardCharsets.UTF_8)
                    .build());
            headers.add(HttpHeaders.CONTENT_TYPE, contentType);
    
            Resource resource = new InputStreamResource(Files.newInputStream(path));
    
            return new ResponseEntity<>(resource, headers, HttpStatus.OK);
        }
    
        public boolean deleteBoard(Integer bno){
            return mapper.deleteBoard(bno);
        }
    
    }
    
  • BoardMapper.java
    package org.study.board.repository;
    
    import org.apache.ibatis.annotations.Mapper;
    import org.study.board.dto.Board;
    import org.study.board.dto.FileDto;
    
    import java.util.List;
    
    @Mapper
    public interface BoardMapper {
    
        List<Board> getBoardList(Board board);
        List<FileDto> getFile(Board board);
        Board getBoard(Integer bno);
        void insertBoard(Board board);
        void updateBoard(Board board);
        boolean deleteBoard(Integer bno);
        void insertFile(Board board);
        void deleteFile(Board board);
    }
    
  • 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="org.study.board.repository.BoardMapper">
    
        <select id="getBoardList" parameterType="org.study.board.dto.Board"
        resultType="org.study.board.dto.Board">
            SELECT *
            from board
            where delete_yn!=1
        </select>
    
        <select id="getBoard" parameterType="int"
                resultType="org.study.board.dto.Board">
            select *
            from board
            where delete_yn!=1 and bno=#{bno}
        </select>
    
        <!--첨부파일 리스트-->
        <select id="getFile" resultType="org.study.board.dto.FileDto">
            SELECT *
            FROM file
            WHERE delete_yn!=1 and bno=#{bno}
        </select>
    
        <insert id="insertBoard" parameterType="org.study.board.dto.Board">
            INSERT INTO board (title, content, writer, regdate, hit, delete_yn, user_idx)
            VALUES (#{title}, #{content}, #{writer}, NOW(), 0, 0, #{userIdx})
            <selectKey keyProperty="bno" resultType="int" order="AFTER">
                SELECT LAST_INSERT_ID()
            </selectKey>
        </insert>
    
        <update id="updateBoard" parameterType="org.study.board.dto.Board">
            UPDATE board
            SET title=#{title}, content=#{content},regdate=NOW(), hit = #{hit}, delete_yn=0, user_idx = #{userIdx}
            WHERE bno = #{bno}
        </update>
    
        <update id="deleteBoard" parameterType="int">
            update board
            SET delete_yn=1
            WHERE bno=#{bno}
        </update>
    
        <!-- 파일 정보를 삽입하는 쿼리 -->
        <insert id="insertFile" parameterType="org.study.board.dto.Board">
            INSERT INTO file (bno, uuid, filename, content_type)
            VALUES
            <foreach collection="list" separator="," index="index" item="item">
                (#{bno}, #{item.uuid}, #{item.filename}, #{item.contentType})
            </foreach>
        </insert>
    
        <update id="deleteFile" parameterType="org.study.board.dto.Board">
            UPDATE file
            SET delete_yn=1
            WHERE bno=#{bno} AND delete_yn=0
        </update>
    
    </mapper>

View (webapp/WEB-INF/jsp/views)


  • board
    • main.jsp
      • 게시판 메인 페이지

        <%@ page contentType="text/html;charset=UTF-8" language="java" %>
        <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
        <!DOCTYPE HTML>
        <head>
            <title>게시판 메인</title>
            <style>
                .button-container {
                    margin-top: 20px;
                }
                .button-container input {
                    margin-right: 10px;
                }
                table {
                    width: 100%;
                    border-collapse: collapse;
                }
                th, td {
                    border: 1px solid #ddd;
                    padding: 8px;
                }
                th {
                    background-color: #f2f2f2;
                }
            </style>
        </head>
        <body>
        <h1>게시글 목록</h1>
        <table>
            <thead>
            <tr>
                <th class="one wide">번호</th>
                <th class="ten wide">글제목</th>
                <th class="two wide">작성자</th>
                <th class="three wide">작성일</th>
            </tr>
            </thead>
        
            <tbody>
            <c:forEach var="board" items="${board}">
                <tr>
                    <td><span>${board.bno}</span></td>
                    <td><a href="/board/${board.bno}"><span>${board.title}</span></a></td>
                    <td><span>${board.writer}</span></td>
                    <td><span>${board.regdate}</span></td>
                </tr>
            </c:forEach>
            </tbody>
        </table>
        
            <%
            // 쿠키 배열 가져오기
            Cookie[] cookies = request.getCookies();
            boolean isLoggedIn = false;
        
            // 쿠키 배열을 순회하며 idx 쿠키의 존재 여부 확인
            if (cookies != null) {
                for (Cookie c : cookies) {
                    if (c.getName().equals("idx")) {
                        // idx 쿠키가 존재하면 로그인 상태로 설정
                        isLoggedIn = true;
                        break; // 로그인 상태이므로 더 이상 반복할 필요가 없음
                    }
                }
            }
        %>
        
        <div class="button-container">
            <%
                // 로그인 상태에 따라 버튼 표시
                if (isLoggedIn) {
            %>
            <input type="button" value="user 목록" onclick="location.href='/user/main'"><br/><br/>
            <input type="button" value="글 작성" onclick="location.href='/write'"><br/><br/>
            <input type="button" value="로그아웃" onclick="location.href='/logout'"><br/><br/>
            <%
            } else {
            %>
            <input type="button" value="로그인" onclick="location.href='/login'"><br/><br/>
            <input type="button" value="회원가입" onclick="location.href='/join'"><br/><br/>
            <%
                }
            %>
        </div>
        </body>
        </html>
        
    • write.jsp
      • 글 상세/작성/수정 페이지 모두 동일 페이지

        <%@ page contentType="text/html;charset=UTF-8" language="java" %>
        <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
        <!DOCTYPE HTML>
        <head>
            <title>글 상세</title>
            <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
        </head>
        <body>
        <h1>글 상세보기</h1>
        
        <form id="boardForm" method="post" action="/insertBoard">
            <table width="90%">
                <tr width="90%">
                    <td width="10%" align="center">작성자</td>
                    <c:if test="${not empty board.writer}">
                        <td width="50%">${board.writer}</td>
                    </c:if>
                    <td width="50%">${user.username}</td>
                </tr>
                <tr>
                    <td align="center">제목</td>
                    <td><input type="text" name="title" style="width: 95%;" value="${board.title}"></td>
                </tr>
                <tr>
                    <td align="center">내용</td>
                    <td><textarea name="content" style="width: 95%;height: 200px;">${board.content}</textarea></td>
                </tr>
                <tr>
                    <td align="center">첨부파일 등록</td>
                    <td>
                    <input type="file" name="files" id="files" multiple="multiple" onchange="uploadFile(this)">
                    <div id="uploadDiv">
                        <c:forEach items="${getFile}" var="file" varStatus="status">
                            <div class="uploadResult">
                                <span>${status.count}. </span><a href="/downloadFile?uuid=${file.uuid}&fileName=${file.filename}" download>${file.filename}</a>
                                <input type="hidden" name="uuids" value="${file.uuid}">
                                <input type="hidden" name="filenames" value="${file.filename}">
                                <input type="hidden" name="contentTypes" value="${file.contentType}">
                                <label class="delBtn">◀ 삭제</label>
                            </div>
                        </c:forEach>
                    </div>
                    </td>
                    <br><br><br>
        
                </tr>
                <tr>
                    <td colspan="2" align="center">
                        <input type="hidden" name="bno" value="${board.bno}">
                        <button type="submit">등록</button>
                        <button type="button" onclick="gotoMain();">목록</button>
                        <c:if test="${not empty board.bno}">
                            <button type="button" onclick="deleteBoard()">삭제</button>
                        </c:if>
                    </td>
                </tr>
            </table>
        </form>
        </body>
        <script>
        
            // delBtn 개별 삭제 버튼
            $(function () {
                delBtnFile();
            });
        
            function delBtnFile() {
                $(".delBtn").on("click",function () {
                    $(this).parent().remove();
                    stCnt();
                });
            }
        
            // status.count ajax사용시 다시 그려주기
            function stCnt() {
                $('.uploadResult').each(function(index, item){
                    $(this).children('span').html(index+1+'. ');
                });
            }
        
            // ajax 첨부파일 업로드
            function uploadFile(obj) {
                let files = obj.files;
                let formData = new FormData();
        
                for (var i = 0; i < files.length; i++){
                    formData.append("files", files[i]);
                }
        
                $.ajax({
                    type: 'post',
                    enctype: 'multipart/form-data',
                    url: '/ajaxFile',
                    data: formData,
                    processData: false,
                    contentType: false,
                    success: function(data) {
                        console.log(data);
                        let result = "";
                        let cnt = $('.uploadResult').length;
                        for (var i = 0; i < data.length; i++){
                            result += '<div class="uploadResult">';
                            result += '<span>' +(cnt + i + 1)  +  '. </span><a href="/downloadFile?uuid=' + data[i].uuid + '&filename=' + data[i].filename + '" download>' + data[i].filename + '</a>';
                            result += '<input type="hidden" name="uuids" value="' + data[i].uuid + '">';
                            result += '<input type="hidden" name="filenames" value="' + data[i].filename + '">';
                            result += '<input type="hidden" name="contentTypes" value="' + data[i].contentType + '">';
                            result += '<label type="button" class="delBtn"> ◀ 삭제</label>';
                            result += '</div>';
                        }
                        $('#uploadDiv').append(result);
                        delBtnFile();
                    }
                });
        
            }
        
            // 목록이동
            function gotoMain() {
                location.href = "/main";
            }
        
            // 글 삭제
            function deleteBoard() {
                const bno = document.getElementsByName("bno")[0].value;
                if (confirm("정말 삭제하시겠습니까?")) {
                    fetch(`/delete/${bno}`, {
                        method: 'DELETE'
                    })
                        .then(response => {
                            if (response.ok) {
                                alert("삭제되었습니다.");
                                location.href = "/main";
                            } else {
                                alert("삭제에 실패하였습니다.");
                            }
                        })
                        .catch(error => {
                            console.error("Error:", error);
                            alert("삭제 중 오류가 발생하였습니다.");
                        });
                }
            }
        
        </script>
        </html>
        
  • user
    • main.jsp
      • 사용자 목록

        <%@ page contentType="text/html;charset=UTF-8" language="java" %>
        <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
        <html>
        <head>
          <title>사용자 목록</title>
        </head>
        <body>
        <h1>사용자 목록</h1>
        <table border="1">
          <thead>
          <tr>
            <th>유저번호</th>
            <th>아이디</th>
            <th>이름</th>
            <th>가입일</th>
          </tr>
          </thead>
          <tbody>
          <c:forEach items="${users}" var="user">
            <tr>
              <td>${user.idx}</td>
              <td><a href="info/${user.userId}">${user.userId}</a></td>
              <td>${user.username}</td>
              <td>${user.regdate}</td>
            </tr>
          </c:forEach>
          </tbody>
        </table>
        </body>
        </html>
        
    • info.jsp
      • 사용자 상세 정보 보기

        <%@ page contentType="text/html;charset=UTF-8" language="java" %>
        <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
        <html>
        <head>
            <title>사용자 정보</title>
        </head>
        <body>
        <h1>사용자 정보</h1>
        <table>
            <tr>
                <td>아이디:</td>
                <td>${user.userId}</td>
            </tr>
            <tr>
                <td>비밀번호:</td>
                <td>${user.password}</td>
            </tr>
            <tr>
                <td>이름:</td>
                <td>${user.username}</td>
            </tr>
            <tr>
                <td>가입일:</td>
                <td>${user.regdate}</td>
            </tr>
        </table>
        </body>
        </html>
        
  • join.jsp
    • 회원가입 페이지

      <%@ page contentType="text/html;charset=UTF-8" language="java" %>
      <!DOCTYPE HTML>
      <head>
          <title>회원가입페이지</title>
          <style>
              .container {
                  max-width: 560px;
              }
              .field-error {
                  border-color: #dc3545;
                  color: #dc3545;
              }
          </style>
      </head>
      <body>
      <h1>
          회원가입
      </h1>
      
      <form action="/join" method="post">
          <div>
              <label for="loginId">아이디:</label>
              <input type="text" id="loginId" name="loginId" required>
              <button type="button" onclick="checkUsername()">중복 확인</button>
              <div id="usernameCheckResult"></div>
          </div>
          <div>
              <label for="password">비밀번호:</label>
              <input type="password" id="password" name="password" required>
          </div>
          <div>
              <label for="passwordCheck">비밀번호 확인:</label>
              <input type="password" id="passwordCheck" name="passwordCheck" required>
          </div>
          <div>
              <label for="username">이름:</label>
              <input type="text" id="username" name="username" required>
          </div>
          <div>
              <button type="submit">가입하기</button>
          </div>
      </form>
      </body>
      <script>
          function checkUsername() {
              const loginIdInput = document.getElementById('loginId');
              const loginId = loginIdInput.value;
              const resultDiv = document.getElementById('usernameCheckResult');
      
              fetch(`/check-username?loginId=`+loginId)
                  .then(response => response.json())
                  .then(data => {
                      if (data) {
                          resultDiv.textContent = '이미 사용 중인 아이디입니다.';
                          resultDiv.style.color = 'red';
                          loginIdInput.classList.add('field-error');
                      } else {
                          resultDiv.textContent = '사용 가능한 아이디입니다.';
                          resultDiv.style.color = 'green';
                          loginIdInput.classList.remove('field-error');
                      }
                  });
          }
      </script>
      </html>
      
  • login.jsp
    • 로그인 페이지

      <%@ page contentType="text/html;charset=UTF-8" language="java" %>
      <!DOCTYPE HTML>
      <head>
          <title>로그인페이지</title>
          <style>
              .container {
                  max-width: 560px;
              }
              .field-error {
                  border-color: #dc3545;
                  color: #dc3545;
              }
          </style>
      </head>
      <body>
      
      <div class="container">
      
          <div class="py-5 text-center">
              <h2>로그인</h2>
          </div>
      
          <form action="/login" method="post">
      
              <div>
                  <label for="loginId">로그인 ID</label>
                  <input type="text" id="loginId" name="loginId" value="${loginForm.loginId}" class="form-control"
                         th:errorclass="field-error">
              </div>
              <div>
                  <label for="password">비밀번호</label>
                  <input type="password" id="password" name="password" value="${loginForm.password}" class="form-control"
                         th:errorclass="field-error">
              </div>
      
              <hr class="my-4">
      
              <div class="row">
                  <div class="col">
                      <button class="w-100 btn btn-primary btn-lg" type="submit">로그인</button>
                  </div>
                  <div class="col">
                      <a href="/main" class="w-100 btn btn-secondary btn-lg">취소</a>
                      <%--<button class="w-100 btn btn-secondary btn-lg" onclick="window.location.href='/main'">취소</button>--%>
                  </div>
              </div>
      
          </form>
      
      </div> <!-- /container -->
      </body>
      </html>
  • logout.jsp
    • 로그아웃 페이지

      <%@ page contentType="text/html;charset=UTF-8" language="java" %>
      <html>
      <head>
          <title>Logout</title>
      </head>
      <body>
      <%
          Cookie cookie = new Cookie("idx",null);
          cookie.setMaxAge(0);
          response.addCookie(cookie);
      %>
      <script>
          if (!alert('로그아웃 되었습니다')) document.location = 'main.jsp'
      </script>
      </body>
      </html>

실행 화면


  • /main

    • 메인 페이지 - 로그아웃 상태

  • /check-username

    • 아이디 중복체크

  • /join

    • 회원가입

  • /login

    • 로그인

  • /main

    • 메인 페이지 - 로그인 상태

  • /write

    • 글 작성 페이지

  • /board/{bno}

    • 글 상세보기

  • /insertBoard

    • 글 작성/수정

  • /delete/{bno}

    • 글 삭제

  • /user/main

    • 사용자 목록

  • /user/info/{userId}

    • 사용자 상세정보

회고


미니 프로젝트를 시작하면서 기능 개발의 우선순위에 대한 감이 안 잡혔었는데, 모든 기능을 수행하기 전에 권한을 정하는 로직이 우선이라고 생각해서 user로그인 기능을 먼저 개발하기로 했던 것 같다.

로그인을 할 때 사용자의 데이터를 전송하는 방식엔 여러가지가 있지만 (cookie, session, jwt ㄷ등) 항상 session방식을 사용했었어서 이번 프로젝트는 cookie value로 받아오는 방식을 선택했다.

보안 때문에라도 스프링 프레임워크에서 제공하는 Spring Security를 적용해보려 했지만 스프링 버전이 업데이트 되면서 방식도 조금씩 바뀌었고 filterChain 등 나에게는 아직 사용하는 법이 어렵게 느껴져서 좀 더 공부한 후 다음 프로젝트 때 적용해볼 생각이다.

첨부 파일 업로드와 삭제하는 기능이 가장 막막했다.

db설계를 할 때 board와 file 테이블 간의 제약 조건을 어떻게 설정할 지에 대한 고민이 있었다.

  1. board테이블의 bno를 file테이블의 외래키로 할지,
  2. file테이블의 idx를 board테이블의 외래키로 할지

file의 idx(파일 번호)는 auto-increment로 각 파일의 index를 저장하는 컬럼으로만 사용하였고

board의 PRIMARY KEY인 bno(글 번호)를 file의 외래키로 제약 조건을 만들어 두 테이블을 연결하는 방법을 선택했다.

후에 게시판의 개수를 늘린다면 기존 file 테이블에 board_type 컬럼을 추가해 (ex : 1이면 공지게시판, 2이면 자유게시판, 3이면 질문게시판) 사용할 계획이다.

profile
백엔드 개발하는 사람

0개의 댓글