게시판 만들기
✒️ 2025-05-28 11:46 내용 수정
실습 목표
- JSP로 만들었던 게시판을 이번엔 Spring을 사용해서 제작한다.
- DB와 연결하여 게시글 내용을 조회, 추가, 수정, 삭제한다.
- 회원가입 기능과 아이디 중복 체크, 로그인 세션 유지 기능을 구현한다.
실습 흐름
- DB와 Spring을 Mybatis로 연결한다.
- DB에 테이블을 추가한다.
- DTO와 DAO를 만들고, Context_3_dao에 DAO Bean을 추가한다.
- 조회, 수정, 삭제 기능을 DAO에 추가한다.
- DAO에 추가한 내용의 query문을 mapper에도 추가한다.
- JSP 페이지를 만들어 각 기능에 맞는 데이터를 보내거나 받을 수 있도록 작성한다.
- Controller를 생성하여 DAO를 호출하고, JSP에서 보낸 데이터를 이어준다.
- ServletContext에 Controller Bean을 추가한다.
- 게시판의 기본 기능을 구현하고 나서 로그인(아이디 중복 체크 포함), 회원가입, 로그아웃 기능을 추가한다.
흐름 내용
DB에 테이블 추가
- 게시판 만들기 에서 사용한 DB 테이블을 사용하거나, 테이블이 없다면 추가한다.
```sql
--시퀀스
CREATE SEQUENCE SEQ_BOARD_IDX;
--테이블
CREATE TABLE BOARD(
IDX NUMBER(3) PRIMARY KEY, --번호
NAME VARCHAR2(100) NOT NULL, --작성자
SUBJECT VARCHAR2(255) NOT NULL, --게시글 이름
CONTENT CLOB, --게시글 내용
PWD VARCHAR2(100), --비밀번호
IP VARCHAR2(100), --IP
REGDATE DATE, --작성일
READHIT NUMBER(3) DEFAULT 0, --조회수
REF INT, --기준글번호(댓글의 메인글 번호)
STEP INT, --댓글순서
DEPTH INT, --대댓글
DEL_INFO NUMBER(2) --글 삭제여부
);
-- 샘플 데이터 추가
INSERT INTO BOARD VALUES(
SEQ_BOARD_IDX.nextVal,
'에이스',
'게시판 첫 글',
'게시판 첫 번째 글은 내가 작성했다',
'1234',
'192.0.0.2',
SYSDATE,
0,
SEQ_BOARD_IDX.currval,
0,
0,
0
);
-- 댓글 샘플 데이터
INSERT INTO BOARD VALUES(
SEQ_BOARD_IDX.nextVal,
'브라보',
'오늘 날씨',
'오늘 날씨 비 오고 눈 내림',
'1234',
'192.0.0.2',
SYSDATE,
0,
1,
1,
1,
0
);
-- 댓글의 댓글 샘플 데이터
INSERT INTO BOARD VALUES(
SEQ_BOARD_IDX.nextVal,
'칼리',
'대댓글',
'대대대대댓글',
'1234',
'192.0.0.3',
SYSDATE,
0,
1,
2,
2,
0
);
프로젝트 기본 설정
- Annotation 기반 설정 파일#프로젝트 설정, Mybatis 연동하기#환경설정를 참고하여 프로젝트 기본 설정을 해둔다.
DB에 연결
- Context_1_dataSource
package context;
import javax.sql.DataSource;
import org.apache.commons.dbcp.BasicDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class Context_1_dataSource {
@Bean
public DataSource ds() {
BasicDataSource ds = new BasicDataSource();
ds.setDriverClassName("oracle.jdbc.OracleDriver");
ds.setUrl("jdbc:oracle:thin:@localhost:1521:xe");
ds.setUsername("계정명");
ds.setPassword("비밀번호");
return ds;
}
}
- Context_2_myBatis
package context;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import lombok.RequiredArgsConstructor;
@Configuration
@RequiredArgsConstructor
public class Context_2_myBatis {
final DataSource ds;
@Bean
public SqlSessionFactory factoryBean() throws Exception{
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(ds);
// mapper를 알고있는 mybatis-config.xml 파일의 위치를 알려줘야 함
factoryBean.setConfigLocation(new ClassPathResource("config/mybatis/mybatis-config.xml"));
return factoryBean.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionBean() throws Exception {
return new SqlSessionTemplate(factoryBean());
}
}
- mybatis-config.xml
- 게시판의 정보와 회원 정보를 가져오기 위해 mapper를 2개 사용해야 한다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "HTTP://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="cacheEnabled" value="false" />
<setting name="useGeneratedKeys" value="true" />
<setting name="defaultExecutorType" value="REUSE" />
</settings>
<typeAliases>
<typeAlias type="dto.BoardDTO" alias="board"/>
<typeAlias type="dto.MemberDTO" alias="member"/>
</typeAliases>
<mappers>
<mapper resource="config/mybatis/mapper/board.xml" />
<mapper resource="config/mybatis/mapper/member.xml" />
</mappers>
</configuration>
- board_mapper.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="b">
<!-- 페이지별 게시글 조회 -->
<select id="board_list" parameterType="java.util.HashMap" resultType="board">
SELECT * FROM (SELECT RANK() OVER(ORDER BY REF DESC, STEP) no, b.* FROM BOARD b)
WHERE no BETWEEN #{start} AND #{end}
</select>
<!-- 전체 게시물 수 조회 -->
<select id="board_count" resultType="int">
SELECT COUNT(*) FROM BOARD
</select>
<!-- 게시글 상세보기 -->
<select id="board_one" parameterType="int" resultType="board">
SELECT * FROM BOARD WHERE IDX = #{idx}
</select>
<!-- 조회수 증가 -->
<update id="board_update_readhit" parameterType="int">
UPDATE BOARD
SET READHIT = READHIT + 1
WHERE IDX = #{idx}
</update>
<!-- 게시글 등록 -->
<insert id="board_insert" parameterType="board">
INSERT INTO BOARD VALUES(
SEQ_BOARD_IDX.nextVal,
#{name},
#{subject},
#{content},
#{pwd},
#{ip},
sysdate,
0,
SEQ_BOARD_IDX.currVal,
0,
0,
0
)
</insert>
<!-- 글 삭제하기 -->
<update id="board_delete" parameterType="board">
UPDATE BOARD
SET SUBJECT = #{subject}, NAME = #{name}, DEL_INFO = -1
WHERE IDX=#{idx}
</update>
<!-- 댓글 등록을 위한 step + 1 -->
<update id="board_update_step" parameterType="board">
UPDATE BOARD
SET STEP = STEP + 1
WHERE REF = #{ref} AND STEP > #{step}
</update>
<!-- 댓글 등록 -->
<insert id="board_reply" parameterType="board">
INSERT INTO BOARD VALUES(
SEQ_BOARD_IDX.nextVal,
#{name},
#{subject},
#{content},
#{pwd},
#{ip},
sysdate,
0,
#{ref},
#{step},
#{depth},
0
)
</insert>
</mapper>
- member_mapper.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="m">
<!-- 로그인 체크 -->
<select id="login_check" parameterType="String" resultType="member">
SELECT * FROM MEMBER WHERE ID = #{id}
</select>
<!-- 회원가입 -->
<insert id="member_insert" parameterType="member">
INSERT INTO MEMBER VALUES(
SEQ_MEMBER_IDX.nextVal,
#{name},
#{id},
#{pwd},
#{email}
)
</insert>
</mapper>
Ajax
var xhr = null;
function createRequest() {
if (xhr != null) {
return;
}
if (window.ActiveXObject) {
xhr = new ActiveXObject("Microsoft.XMLHTTP"); // IE 환경
} else {
xhr = new XMLHttpRequest(); // 기타 브라우저 환경
}
}
function sendRequest(url, param, callback, method) {
// HttpRequest 생성
createRequest();
// 전송 타입 구분
var httpMethod = (method != 'POST' && method != 'post') ? 'GET' : 'POST';
// 파라미터 구분
var httpParam = (param == null || param == '') ? null : param;
// 접근 url
var httpURL = url;
// 요청 방식이 GET이고 전달할 파라미터가 있다면 새 url 경로 제작
if (httpMethod == 'GET' && httpParam != null) {
httpURL = httpURL+'?'+httParam;
}
// 서버로 보낼 Ajax 요청 형식
xhr.open(httpMethod, httpURL, true);
// requestHeader 설정 : Content-Type 지정
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
// 작업이 완료된 후 호출할 callback 메소드 지정
xhr.onreadystatechange = callback;
// Ajax 요청을 서버로 전달
xhr.send(httpMethod == 'POST' ? httpParam : null);
}
DTO와 DAO
- BoardDTO : 게시판 정보를 저장하는 DTO
- lombok 라이브러리의 @Data 사용
package dto;
import lombok.Data;
@Data
public class BoardDTO {
private int idx;
private int readhit;
private int ref;
private int step;
private int depth;
private int del_info;
private String name;
private String subject;
private String content;
private String pwd;
private String ip;
private String regdate;
}
- MemberDTO : 회원 정보를 저장하는 DTO
- lombok 라이브러리의 @Getter와 @Setter 사용
package dto;
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
public class MemberDTO {
private int idx;
private String name;
private String id;
private String pwd;
private String email;
}
- BoardDAO
- SqlSession을 통해 DB와 연결해서 게시판와 연관된 기능을 수행할 메소드들을 정리
package dao;
import java.util.HashMap;
import java.util.List;
import org.apache.ibatis.session.SqlSession;
import dto.BoardDTO;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class BoardDAO {
final SqlSession sqlSession;
// 페이지별 게시글 조회
public List<BoardDTO> selectList(HashMap<String, Integer> map) {
return sqlSession.selectList("b.board_list", map);
}
// 전체 게시물 수 조회
public int getRowTotal() {
return sqlSession.selectOne("b.board_count");
}
// 게시글 하나 상세보기
public BoardDTO selectOne(int idx) {
return sqlSession.selectOne("b.board_one", idx);
}
// 조회수 증가
public int update_readhit(int idx) {
return sqlSession.update("b.board_update_readhit", idx);
}
// 새 글 등록
public int insert(BoardDTO dto) {
return sqlSession.insert("b.board_insert", dto);
}
// 글 삭제
public int delete_update(BoardDTO dto) {
return sqlSession.update("b.board_delete", dto);
}
// 댓글 추가를 위한 step + 1
public int update_step(BoardDTO dto) {
return sqlSession.update("b.board_update_step", dto);
}
// 댓글 등록
public int reply(BoardDTO dto) {
return sqlSession.insert("b.board_reply", dto);
}
}
- MemberDAO
- SqlSession을 통해 DB와 연결해서 회원정보 및 로그인과 연관된 기능을 수행할 메소드들을 정리
package dao;
import org.apache.ibatis.session.SqlSession;
import dto.MemberDTO;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class MemberDAO {
final SqlSession sqlSession;
// 로그인 체크
public MemberDTO selectOne(String id) {
return sqlSession.selectOne("m.login_check", id);
}
// 회원가입
public int insert(MemberDTO dto) {
return sqlSession.insert("m.member_insert", dto);
}
}
- Context_3_dao
- BoardDAO와 MemberDAO Bean 객체를 등록
package context;
import org.apache.ibatis.session.SqlSession;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import dao.BoardDAO;
import dao.MemberDAO;
import dao.MemberDAO;
import dao.MemberDAO;
import dao.MemberDAO;
@Configuration
public class Context_3_dao {
@Bean
public BoardDAO boardDAO(SqlSession sqlSession) {
return new BoardDAO(sqlSession);
}
@Bean
public MemberDAO memberDAO(SqlSession sqlSession) {
return new MemberDAO(sqlSession);
}
}
Controller
- BoardController
- BoardDAO와 MemberDAO를 생성자 주입하고, HttpSession 객체는 필드 주입한다.
- 게시판 전체 기능과 회원가입 및 로그인 기능들을 담당한다.
package com.nogroup.board;
import java.util.HashMap;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import dao.BoardDAO;
import dao.MemberDAO;
import dto.BoardDTO;
import dto.MemberDTO;
import lombok.RequiredArgsConstructor;
import util.Common;
import util.Paging;
@Controller
@RequiredArgsConstructor
public class BoardController {
final BoardDAO board_dao;
final MemberDAO member_dao;
@Autowired
HttpServletRequest request;
@Autowired
HttpSession session;
// 게시글 전체를 페이지별로
@RequestMapping(value= {"/", "board_list"})
public String list(Model model, @RequestParam(required=false, defaultValue="1") int page) {
int start = (page - 1) * Common.Board.BLOCKLIST + 1;
int end = start + Common.Board.BLOCKLIST - 1;
HashMap<String, Integer> map = new HashMap<String, Integer>();
map.put("start", start);
map.put("end", end);
// 페이지 번호에 따른 전체 게시글 조회
List<BoardDTO> list = board_dao.selectList(map);
// 전체 게시글 수 조회
int rowTotal = board_dao.getRowTotal();
// 페이지 메뉴 생성하기
String pageMenu = Paging.getPaging("board_list",
page,
rowTotal,
Common.Board.BLOCKLIST,
Common.Board.BLOCKPAGE);
request.getSession().removeAttribute("show");
model.addAttribute("list", list);
model.addAttribute("pageMenu", pageMenu);
return Common.Board.VIEW_PATH+"board_list.jsp?page="+page;
}
// 게시글 상세보기
@RequestMapping("view")
public String view(Model model, int idx, int page) {
BoardDTO dto = board_dao.selectOne(idx);
// 조회수 증가
HttpSession session = request.getSession();
String show = (String)session.getAttribute("show");
if (show == null) {
int res = board_dao.update_readhit(idx);
session.setAttribute("show", "0");
}
model.addAttribute("dto", dto);
return Common.Board.VIEW_PATH+"board_view.jsp?page="+page;
}
// 글 등록
@RequestMapping("insert_form")
public String insert_form(int page) {
MemberDTO show = (MemberDTO)session.getAttribute("id");
if (show == null) {
return Common.Board.VIEW_PATH + "login_form.jsp";
}
return Common.Board.VIEW_PATH+"insert_form.jsp?page="+page+"&name="+show.getName();
}
@RequestMapping("insert")
public String insert(BoardDTO dto, int page) {
String ip = request.getRemoteAddr();
dto.setIp(ip);
int res = board_dao.insert(dto);
if (res > 0) {
return "redirect:board_list?page="+page;
}
return null;
}
// 글 삭제
@RequestMapping("board_delete")
@ResponseBody
public String delete(int idx) {
BoardDTO ori_dto = board_dao.selectOne(idx);
ori_dto.setSubject("삭제된 글 입니다");
ori_dto.setName("unknown");
int res = board_dao.delete_update(ori_dto);
if (res == 1) {
return "[{'result':'yes'}]";
} else {
return "[{'result':'no'}]";
}
}
// 댓글 추가
@RequestMapping("reply_form")
public String reply_form(int idx, int page) {
MemberDTO show = (MemberDTO)session.getAttribute("id");
if(show == null) {
return Common.Board.VIEW_PATH + "login_form.jsp";
}
return Common.Board.VIEW_PATH + "reply_form.jsp?idx="+idx+"&page="+page;
}
@RequestMapping("reply")
public String reply(BoardDTO dto, int idx, int page) {
// Controller에서 dao를 3번이나 접근하기에 이후에 service에서 reply()메소드를 생성해서
// dao 접근 및 댓글 추가 작업을 끝내는 기능을 추가하고
// Controller에서 service.reply()를 한 번만 호출해서 모든 기능을 수행하도록 변경할 수 있다.
String ip = request.getRemoteAddr();
BoardDTO ori_dto = board_dao.selectOne(idx);
int res = board_dao.update_step(ori_dto);
dto.setIp(ip);
// 댓글이 들어갈 위치 선정
dto.setRef(ori_dto.getRef());
dto.setStep(ori_dto.getStep()+1);
dto.setDepth(ori_dto.getDepth()+1);
int res2 = board_dao.reply(dto);
if (res2 > 0) {
return "redirect:board_list?page="+page;
}
return null;
}
//로그인
@RequestMapping("login")
@ResponseBody
public String login(String id, String pwd) {
MemberDTO dto = member_dao.selectOne(id);
// dto가 null일 경우 id가 존재하지 않음
if(dto == null) {
return "[{'param':'no_id'}]";
}
// 우리가 입력받은 pwd와 DB에 저장된 비밀번호를 비교
if(!pwd.equals(dto.getPwd())) {
return "[{'param':'no_pwd'}]";
}
// 아이디와 비밀번호에 문제 없으므로 로그인 가능
// 세션에 바인딩
session.setAttribute("id", dto);
// 로그인에 성공한 경우
return "[{'param':'clear'}]";
}
// 로그인 페이지로 이동
@RequestMapping("login_form")
public String login_form() {
return Common.Board.VIEW_PATH + "login_form.jsp";
}
// 로그아웃
@RequestMapping("logout")
public String logout() {
session.removeAttribute("id");
return "redirect:board_list";
}
// 회원가입
@RequestMapping("member_insert_form")
public String member_insert_form() {
return Common.Board.VIEW_PATH + "member_insert_form.jsp";
}
// 중복체크
@RequestMapping("check_id")
@ResponseBody
public String check_id(String id) {
MemberDTO dto = member_dao.selectOne(id);
if (dto == null) {
return "[{'res':'yes'}]";
} else {
return "[{'res':'no'}]";
}
}
// 회원가입 DB에 전달
@RequestMapping("member_insert")
@ResponseBody
public String member_insert(MemberDTO dto) {
int res = member_dao.insert(dto);
if (res > 0) {
return "[{'regi':'yes'}]";
} else {
return "[{'regi':'no'}]";
}
}
}
- ServletContext
- BoardController Bean 객체를 등록
package mvc;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import com.nogroup.board.BoardController;
import dao.BoardDAO;
import dao.MemberDAO;
@Configuration
@EnableWebMvc
//@ComponentScan("com.nogroup.board") // 자동 생성을 할 때 사용
public class ServletContext implements WebMvcConfigurer{
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**").addResourceLocations("/resources/");
}
@Bean
public BoardController boardController(BoardDAO boardDAO, MemberDAO memberDAO) {
return new BoardController(boardDAO, memberDAO);
}
}
util 패키지
- Common
- JSP 파일의 경로를 저장할 상수와 페이징 처리에 필요한 상수를 저장한다.
package util;
public class Common {
public static class Board{
public final static String VIEW_PATH = "/WEB-INF/views/board/";
// 한 페이지에 보여줄 게시물 수
public final static int BLOCKLIST = 10;
// 페이지 메뉴 수
public final static int BLOCKPAGE = 3;
}
}
- Paging
- 게시판의 페이징 처리를 담당하며, 게시판 만들기의 페이징 처리와 동일한 코드다.
package util;
public class Paging {
public static String getPaging(String pageURL, int nowPage, int rowTotal, int blockList, int blockPage) {
int totalPage, startPage, endPage;
boolean isPrevPage, isNextPage;
StringBuffer sb;
isPrevPage = isNextPage = false;
totalPage = (int)(rowTotal / blockList);
if (rowTotal % blockList != 0) totalPage++;
if (nowPage > totalPage) nowPage = totalPage;
startPage = (int)(((nowPage - 1)/blockPage)*blockPage+1);
endPage = startPage + blockPage -1;
if (endPage > totalPage) endPage = totalPage;
if (endPage < totalPage) isNextPage = true;
if (startPage > 1) isPrevPage = true;
sb = new StringBuffer();
if(isPrevPage) {
sb.append("<a href='"+pageURL+"?page=");
sb.append(startPage-1);
sb.append("'><img src='resources/img/btn_prev.gif'></a>");
} else {
sb.append("<img src='resources/img/btn_prev.gif'>");
}
sb.append("");
for (int i = startPage; i <= endPage; i++) {
if(i > totalPage) break;
if(i == nowPage) {
sb.append(" <b><font color='#ff0000'>");
sb.append(i);
sb.append("</font></b>");
} else {
sb.append(" <a href='"+pageURL+"?page=");
sb.append(i);
sb.append("'>");
sb.append(i);
sb.append("</a>");
}
}
sb.append(" ");
if(isNextPage) {
sb.append("<a href='"+pageURL+"?page=");
sb.append(endPage+1);
sb.append("'><img src='resources/img/btn_next.gif'></a>");
} else {
sb.append("<img src='resources/img/btn_next.gif'>");
}
return sb.toString();
}
}
JSP
- board_list.jsp
- 게시판 메인 페이지로, 모든 게시글을 조회할 수 있다.
- 로그인을 해야만 새 글 작성, 글 삭제, 댓글 추가를 할 수 있다.
- 회원 정보가 없다면 새로 회원가입할 수 있다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<style>
a{text-decoration:none;}
table{border-collapse:collapse; width:700px;}
</style>
</head>
<body>
<table border="1" align="center">
<tr>
<!-- 세션에 값이 있으면 로그아웃 버튼이, 세션이 없으면 로그인, 회원가입 버튼이 보이게 설정 -->
<td colspan="5" align="right">
<c:choose>
<c:when test="${empty id}">
<input type="button" value="로그인" onclick="location.href='login_form'">
<input type="button" value="회원가입" onclick="location.href='member_insert_form'">
</c:when>
<c:otherwise>
<input type="button" value="로그아웃" onclick="location.href='logout'">
</c:otherwise>
</c:choose>
</td>
</tr>
<tr>
<td colspan="5"><img src="resources/img/title_04.gif"></td>
</tr>
<tr>
<th>번호</th>
<th>제목</th>
<th>작성자</th>
<th>작성일</th>
<th>조회수</th>
</tr>
<c:forEach var="dto" items="${list}">
<tr>
<td align="center">${dto.idx}</td>
<!-- 댓글일 경우 들여쓰기 -->
<td>
<c:forEach begin="1" end="${dto.depth}"> </c:forEach>
<!-- 댓글기호 -->
<c:if test="${dto.depth ne 0 }">ㄴ</c:if>
<!-- 삭제되지 않은 글이라면 출력 가능 -->
<c:if test="${dto.del_info ne -1}">
<a href="view?idx=${dto.idx}&page=${param.page}">
<font color="black">${dto.subject}</font>
</a>
</c:if>
<!-- 삭제된 글은 클릭 불가 -->
<c:if test="${dto.del_info eq -1}">
<font color="gray">${dto.subject}</font>
</c:if>
</td>
<td>${dto.name}</td>
<c:if test="${dto.del_info ne -1}">
<td>${fn:split(dto.regdate, ' ')[0]}</td>
</c:if>
<c:if test="${dto.del_info eq -1}">
<td>unknown</td>
</c:if>
<td>${dto.readhit}</td>
</tr>
</c:forEach>
<tr>
<td colspan="5" align="center">
${pageMenu}
</td>
</tr>
<tr>
<td colspan="5" align="right">
<img src="resources/img/btn_reg.gif" onclick="location.href='insert_form?page=${param.page}'" style="cursor:pointer;">
</td>
</tr>
</table>
</body>
</html>
- board_view.jsp
- 게시글의 내용을 상세하게 볼 수 있으며, 댓글 작성, 글 삭제를 할 수 있다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<script src="resources/js/HttpRequest.js"></script>
<script>
function del() {
let pwd = ${dto.pwd};
let c_pwd = document.getElementById("c_pwd").value;
if (!confirm("삭제하시겠습니까?")) {
return;
}
if (c_pwd == '') {
alert("비밀번호를 입력해주세요");
return;
}
if (c_pwd != pwd) {
alert("비밀번호가 일치하지 않습니다")
return;
}
let url = "board_delete";
let param = "idx=${dto.idx}";
sendRequest(url, param, resultFn, "POST");
}
function resultFn() {
if(xhr.readyState == 4 && xhr.status == 200) {
let data = xhr.responseText;
let json = (new Function('return' + data))();
if(json[0].result == 'yes') {
alert("게시물이 성공적으로 삭제되었습니다");
location.href = "board_list?page=${param.page}";
} else {
alert("게시물 삭제에 실패했습니다");
}
}
}
function reply() {
location.href = "reply_form?idx=${dto.idx}&page=${param.page}";
}
</script>
</head>
<body>
<table border="1" align="center">
<caption>::게시글 상세보기</caption>
<tr>
<th>제목</th>
<td>${dto.subject}</td>
</tr>
<tr>
<th>작성자</th>
<td>${dto.name}</td>
</tr>
<tr>
<th>작성일</th>
<td>${dto.regdate}</td>
</tr>
<tr>
<th>ip</th>
<td>${dto.ip}</td>
</tr>
<tr>
<th>내용</th>
<td width="500px" height="200px"><pre>${dto.content}</pre></td>
</tr>
<tr>
<th>비밀번호</th>
<td><input type="password" id="c_pwd"></td>
</tr>
<tr>
<td colspan="2">
<img src="resources/img/btn_list.gif" onclick="location.href='board_list?page=${param.page}'" style="cursor:pointer">
<!-- 답글 -->
<c:if test="${dto.depth lt 1}">
<img src="resources/img/btn_reply.gif" onclick="reply()" style="cursor:pointer">
</c:if>
<img src="resources/img/btn_delete.gif" onclick="del()" style="cursor:pointer">
</td>
</tr>
</table>
</body>
</html>
- insert_form.jsp
- 새 글을 추가하는 페이지
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<script>
function send_check() {
let f = document.f;
f.submit();
}
</script>
</head>
<body>
<form action="insert" name="f" method="POST">
<input type="hidden" name="page" value="${param.page}">
<table border="1" align="center">
<caption>:::새 글 쓰기:::</caption>
<tr>
<th>제목</th>
<td><input name="subject"></td>
</tr>
<tr>
<th>작성자</th>
<td><input name="name" value="${param.name}"></td>
</tr>
<tr>
<th>내용</th>
<td><textarea name="content" rows="10" cols="50" style="resize:none;"></textarea></td>
</tr>
<tr>
<th>비밀번호</th>
<td><input name="pwd" type="password"></td>
</tr>
<tr>
<td colspan="2">
<img src="resources/img/btn_reg.gif" onclick="send_check()" style="cursor:pointer;">
<img src="resources/img/btn_back.gif" onclick="location.href='board_list?page=${param.page}'" style="cursor:pointer;">
</td>
</tr>
</table>
</form>
</body>
</html>
- reply_form.jsp
- 댓글을 추가하는 페이지로, 새 글을 추가하는 insert_form.jsp와 형태가 비슷하다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<script>
function send_check() {
let f = document.f;
f.submit();
}
</script>
</head>
<body>
<form action="reply" name="f" method="POST">
<input type="hidden" name="page" value="${param.page}">
<input type="hidden" name="idx" value="${param.idx}">
<table border="1" align="center">
<caption>:::댓글 쓰기:::</caption>
<tr>
<th>제목</th>
<td><input name="subject"></td>
</tr>
<tr>
<th>작성자</th>
<td><input name="name"></td>
</tr>
<tr>
<th>내용</th>
<td><textarea name="content" rows="10" cols="50" style="resize:none;"></textarea></td>
</tr>
<tr>
<th>비밀번호</th>
<td><input name="pwd" type="password"></td>
</tr>
<tr>
<td colspan="2">
<img src="resources/img/btn_reg.gif" onclick="send_check()" style="cursor:pointer;">
<img src="resources/img/btn_back.gif" onclick="location.href='board_list?page=${param.page}'" style="cursor:pointer;">
</td>
</tr>
</table>
</form>
</body>
</html>
- login_form.jsp
- 로그인 페이지로, 정보를 입력하면 DB에서 해당 회원 정보가 존재하는지 확인하고, 존재하면 로그인을, 없으면 로그인 실패를 띄운다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<script src="resources/js/HttpRequest.js"></script>
<script>
function send(f) {
let id = f.id.value.trim();
let pwd = f.pwd.value.trim();
if(id == '') {
alert('아이디를 입력해주세요');
return;
}
if(pwd == '') {
alert('비밀번호를 입력해주세요');
return;
}
let url = "login";
let param = "id="+id+"&pwd="+encodeURIComponent(pwd);
sendRequest(url, param, myCheck, "POST");
}
function myCheck() {
if(xhr.readyState == 4 && xhr.status == 200) {
let data = xhr.responseText;
let json = (new Function('return' + data))();
if(json[0].param == "no_id") {
alert("아이디가 존재하지 않습니다");
} else if (json[0].param == "no_pwd") {
alert("아이디와 비밀번호를 다시 확인해주세요")
} else{
alert("로그인 성공");
location.href="board_list";
}
}
}
</script>
</head>
<body>
<form>
<table border="1" align="center">
<caption>::로그인::</caption>
<tr>
<th>아이디</th>
<td><input name="id"></td>
</tr>
<tr>
<th>비밀번호</th>
<td><input name="pwd" type="password"></td>
</tr>
<tr>
<td colspan="2" align="center">
<input type="button" value="로그인" onclick="send(this.form)">
</td>
</tr>
</table>
</form>
</body>
</html>
- member_insert_form.jsp
- 회원가입 페이지로, 입력 받은 id가 DB에 존재하는지 확인하여 id 중복 조회를 수행하고 중복된 id가 없는 경우에 회원 가입을 처리한다.
- 이메일 형식을 정규 표현식으로 체크했으며, 회원가입이 성공적으로 되었다면 Ajax를 통해 화면에 알림창을 띄우고 게시판 페이지로 이동한다.
- 회원 가입을 실패했다면 현재 페이지에서 회원 가입을 다시 시도하라는 안내창을 띄운다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<script src="resources/js/HttpRequest.js"></script>
<script>
let idCheck = false;
// 아이디 중복체크
function check_id() {
let id = document.getElementById("id").value.trim();
if (id == null) {
alert("아이디를 입력하세요");
return;
}
let url = "check_id";
let param = "id="+id;
sendRequest(url, param, resultFn, "POST");
}
function che() {
idCheck = false;
}
function resultFn() {
if(xhr.readyState == 4 && xhr.status == 200) {
let data = xhr.responseText;
let json = (new Function('return' + data))();
if (json[0].res == "no") {
alert("중복된 아이디입니다.");
return;
} else {
alert("사용 가능한 아이디입니다.")
idCheck = true;
}
}
}
// 회원가입
function send(f) {
let id = f.id.value.trim();
let name = f.name.value.trim();
let pwd = f.pwd.value.trim();
let email = f.email.value.trim();
// 유효성 검사
if (id == '') {
alert('아이디를 입력해주세요');
return;
}
if (name == '') {
alert('이름를 입력해주세요');
return;
}
if (pwd == '') {
alert('비밀번호를 입력해주세요');
return;
}
if (email == '') {
alert('이메일을 입력해주세요');
return;
}
let regex = /[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*.[a-zA-Z]$/i;
if (!regex.test(email)) {
alert("이메일 형식이 일치하지 않습니다");
return;
}
if (!idCheck) {
alert("아이디 중복 체크가 필요합니다");
return;
}
let url = "member_insert";
let param = "id="+id+"&name="+name+"&pwd="+encodeURIComponent(pwd)+"&email="+email;
sendRequest(url, param, registerFn, "POST");
}
function registerFn() {
if(xhr.readyState == 4 && xhr.status == 200) {
let data = xhr.responseText;
let json = (new Function('return' + data))();
if (json[0].regi == "yes") {
alert("회원 가입이 완료되었습니다");
location.href="board_list";
} else {
alert("회원 가입에 실패했습니다. 다시 시도해주세요");
return;
}
}
}
</script>
</head>
<body>
<form>
<table border="1">
<caption>:::회원가입:::</caption>
<tr>
<th>아이디</th>
<td>
<input name="id" id="id" onchange="che()">
<input type="button" value="중복체크" onclick="check_id()">
</td>
</tr>
<tr>
<th>이름</th>
<td>
<input name="name">
</td>
</tr>
<tr>
<th>비밀번호</th>
<td>
<input name="pwd" type="password">
</td>
</tr>
<tr>
<th>이메일</th>
<td>
<input name="email">
</td>
</tr>
<tr>
<td colspan="2" align="center">
<input type="button" value="가입" onclick="send(this.form)">
<input type="button" value="취소" onclick="location.href='board_list'">
</td>
</tr>
</table>
</form>
</body>
</html>
완성된 모습
- 게시판 페이지에서 게시글들의 정보와 버튼들을 확인할 수 있는데, 로그인을 안 한 상태라면 오른쪽에 로그인과 회원가입 버튼이 뜨도록 설정했다.
- 로그인을 안 한 상태여도 게시글을 누르면 상세하게 볼 수 있다. 해당 글의 idx 정보가 url에도 페이지 정보와 함께 표시되는 것을 확인할 수 있다.
- 로그인 버튼을 누르거나, 로그인을 안 한 상태에서 게시글 추가하기 및 답변 달기를 누르면 로그인 페이지로 넘어간다. 알맞는 회원 정보를 입력하면 로그인 성공이 뜨며 메인 게시판 페이지로 이동한다.
- 로그인 상태라면 오른쪽에 로그아웃 버튼만 보인다.
- 새 글을 작성할 때 작성자 이름에 현재 로그인한 계정의 이름을 넣었으며, 아직 input으로 넘기는 것 외에 다른 형태로 수정하지 않아 input 내의 value를 변경할 수 있는 상태다.
- 모든 정보를 입력 해야만 등록하도록 설정되어 있다.
- 새 글을 추가하면 1페이지 가장 위에 업데이트 된다.
- 특정 게시글을 상세보기 한 후 답변 작성 버튼을 누르면 댓글을 작성한다.
- 작성자 이름은 현재 로그인한 계정쪽에서 받아오도록 수정하지 않아 임의로 변경 가능한 상태다.
- 모든 정보를 입력해야만 답변을 등록할 수 있다.
- url에 원본 글의 idx와 페이지 정보도 함께 있는 것을 확인할 수 있다.
- 답변을 작성하면 답변 표시로 글이 추가된다.
- 첫 테스트 때 step과 depth에 +1을 추가하는 것을 잊어서 수정하다 보니 원본 글의 조회수가 증가했다.
- 다른 페이지로 이동하면 url에도 해당 페이지 정보가 함께 반영된다.
- 로그아웃을 한 후 회원가입 버튼을 누르면 회원가입 페이지가 뜬다.
- 중복 체크 시 이미 DB에 존재하는 id라면 중복된 아이디라는 알림이 뜬다.
- 중복 체크 시 이미 DB에 존재하는 id라면 중복된 아이디라는 알림이 뜬다.
- DB에 없는 id라면 사용 가능한 id라는 알림이 뜬다.
- 이메일 정규식을 통과하지 못하면(잘못된 이메일 형식이라면) 이메일 형식 불일치 알림이 뜨면서 가입을 할 수 없도록 설정했다.
- id 중복 체크 없이 가입하려 한다면 id 중복 체크를 요구하는 알림이 뜬다.
- 모든 정보를 입력하고, id 중복 체크를 통과하고, 이메일 형식까지 일치하면 회원 가입이 완료되었다는 알림이 뜨면서 메인 게시판 페이지로 이동한다.