개발 환경
언어: java
Spring Boot ver : 2.3.1.RELEASE
Mybatis : 2.3.0
IDE: intelliJ
SDK: JDK 17
의존성 관리툴: Maven
DB: MySQL 8.0.12
구조도
<?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>
# [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
<?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 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
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;
}
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;
}
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;
}
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;
}
package org.study.board.dto;
import lombok.Data;
@Data
public class LoginForm {
private String loginId;
private String password;
}
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;
}
}
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);
}
}
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);
}
}
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();
}
<?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>
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);
}
}
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);
}
}
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);
}
<?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>
게시판 메인 페이지
<%@ 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>
글 상세/작성/수정 페이지 모두 동일 페이지
<%@ 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>
사용자 목록
<%@ 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>
사용자 상세 정보 보기
<%@ 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>
회원가입 페이지
<%@ 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>
로그인 페이지
<%@ 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>
로그아웃 페이지
<%@ 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 테이블 간의 제약 조건을 어떻게 설정할 지에 대한 고민이 있었다.
file의 idx(파일 번호)는 auto-increment로 각 파일의 index를 저장하는 컬럼으로만 사용하였고
board의 PRIMARY KEY인 bno(글 번호)를 file의 외래키로 제약 조건을 만들어 두 테이블을 연결하는 방법을 선택했다.
후에 게시판의 개수를 늘린다면 기존 file 테이블에 board_type 컬럼을 추가해 (ex : 1이면 공지게시판, 2이면 자유게시판, 3이면 질문게시판) 사용할 계획이다.