💡 학습 목표
|
💡 잠깐 개념 확인 객체지향 특징(4가지) 추상화 캡슐화 상속 다형성 객체 지향 프로그래밍 객체 지향 프로그래밍은 컴퓨터 프로그램 명령어의 목록으로 보는 시각에서 벗어나 여러개의 독립된 단위, 즉 객체들을 모임으로 파악하고자 하는 것이다. 각각의 객체는 메시지를 주고받고, 데이터를 처리할 수는 상호 협력 과정이다. |
프로젝트 기본 구성
1. 회원 관리 기능
- 회원가입: 사용자가 회원가입을 할 수 있는 기능
- 회원탈퇴: 사용자가 회원탈퇴를 할 수 있는 기능
- 로그인 및 로그아웃: 사용자가 로그인하고 로그아웃할 수 있는 기능
2. 게시판 기능
- 게시글 작성 : 수정, 삭제 : 사용자가 게시글을 작성, 수정, 삭제할 수 있는 기능
- 게시글 조회 :사용자가 게시글 목록을 조회하고 상세 내용을 볼 수 있는 기능
- 댓글 작성 및 삭제 : 게시글에 대한 댓글을 작성하고 삭제할 수 있는 기능
3. 페이징 처리
- 게시글 목록 페이징 : 게시글 목록을 페이지 단위로 나누어 볼 수 있는 기능
비기능적 요구사항
- Dynamic Web Project: 이클립스에서 Dynamic Web Project로 설정하여 개발(lib 직접 설정)
- 톰캣 10.1.25 환경: 톰캣 10.1.25 버전을 사용하여 애플리케이션 배포 및 실행
- MVC 패턴 사용: 모델, 뷰, 컨트롤러 구조를 따르는 코드 작성
- DAO 인터페이스 설계: 데이터베이스 접근을 위한 DAO 인터페이스와 구현 클래스 설계 및 구현
- DB 연결 풀링 (HikariCP): HikariCP를 사용하여 데이터베이스 연결 풀링 설정 및 사용
- JSTL 태그 사용: JSP 페이지에서 JSTL core 및 format 태그를 사용하여 코드 간결화
- 데이터베이스 정규화: 데이터베이스 테이블 설계 시 정규화 적용
- 캐스케이드 적용: 데이터베이스 외래 키 관계에서 캐스케이드 옵션 적용
- 게시글 조회 로직 수행시간 측정: 게시글 리스트 조회 시 로직 수행시간을 측정하여 출력
프로젝트 명 : jsp_mvc_board
패키지 명 : com.tenco.tboard
context root : t-board
JSP 파일 위치 : WEB-INF/views
프로젝트 기본 구조
테이블 설계
drop database if exists db_tboard;
create database db_tboard;
use db_tboard;
-- users 테이블 생성
create table users(
id int auto_increment primary key,
username varchar(50) not null unique,
password varchar(50) not null,
email varchar(50) not null unique,
created_at timestamp default current_timestamp
);
desc users;
-- board 테이블 생성
create table board(
id int primary key auto_increment,
user_id int,
title varchar(255) not null,
content text not null,
created_at timestamp default current_timestamp,
foreign key(user_id) references users(id) on delete cascade
);
desc board;
-- comments 테이블 생성
create table comments(
id int auto_increment primary key,
board_id int,
user_id int,
content text not null,
created_at timestamp default current_timestamp,
foreign key(board_id) references board(id) on delete cascade,
foreign key(user_id) references users(id) on delete cascade
);
desc comments;
-- 기존 데이터 삭제
-- truncate table comments;
-- truncate table board;
-- truncate table users;
-- users 테이블에 샘플 데이터 삽입
INSERT INTO users (username, password, email) VALUES ('user1', 'asd123', 'user1@example.com');
INSERT INTO users (username, password, email) VALUES ('user2', 'asd123', 'user2@example.com');
INSERT INTO users (username, password, email) VALUES ('user3', 'asd123', 'user3@example.com');
-- board 테이블에 샘플 데이터 삽입
INSERT INTO board (user_id, title, content) VALUES (1, '자바의 장점', '자바는 플랫폼 독립적이며, 풍부한 라이브러리를 제공합니다.');
INSERT INTO board (user_id, title, content) VALUES (2, '파이썬과 머신러닝', '파이썬은 머신러닝에 최적화된 언어로, 다양한 라이브러리를 지원합니다.');
INSERT INTO board (user_id, title, content) VALUES (3, '자바스크립트와 웹 개발', '자바스크립트는 웹 개발의 핵심 언어로, 프론트엔드와 백엔드 모두에서 사용됩니다.');
INSERT INTO board (user_id, title, content) VALUES (1, 'Spring 프레임워크', 'Spring은 자바 기반의 강력한 애플리케이션 프레임워크입니다.');
INSERT INTO board (user_id, title, content) VALUES (2, 'Django를 활용한 웹 개발', 'Django는 파이썬 기반의 웹 프레임워크로, 빠른 개발이 가능합니다.');
INSERT INTO board (user_id, title, content) VALUES (3, 'React로 인터랙티브 웹 만들기', 'React는 사용자 인터페이스 구축을 위한 자바스크립트 라이브러리입니다.');
INSERT INTO board (user_id, title, content) VALUES (1, 'JSP와 서블릿', 'JSP와 서블릿을 사용하여 동적인 웹 페이지를 개발할 수 있습니다.');
INSERT INTO board (user_id, title, content) VALUES (2, 'Flask의 장점', 'Flask는 가벼운 파이썬 웹 프레임워크로, 유연성과 확장성이 뛰어납니다.');
INSERT INTO board (user_id, title, content) VALUES (3, 'Node.js로 서버 개발', 'Node.js는 자바스크립트를 사용하여 서버 측 애플리케이션을 개발할 수 있습니다.');
INSERT INTO board (user_id, title, content) VALUES (1, '첫 게시글 작성', '첫 게시글을 작성했습니다. 앞으로 자주 올리겠습니다.');
INSERT INTO board (user_id, title, content) VALUES (2, '두 번째 게시글', '두 번째 게시글입니다. 댓글 많이 달아주세요.');
INSERT INTO board (user_id, title, content) VALUES (3, '안녕하세요', '안녕하세요, 오늘도 즐거운 하루 되세요.');
-- comments 테이블에 샘플 데이터 삽입
INSERT INTO comments (board_id, user_id, content) VALUES (1, 2, '자바의 장점에 대해 잘 읽었습니다.');
INSERT INTO comments (board_id, user_id, content) VALUES (1, 3, '좋은 글 감사합니다.');
INSERT INTO comments (board_id, user_id, content) VALUES (2, 1, '파이썬과 머신러닝에 관한 글 잘 봤습니다.');
INSERT INTO comments (board_id, user_id, content) VALUES (2, 3, '유익한 정보 감사합니다.');
INSERT INTO comments (board_id, user_id, content) VALUES (3, 1, '자바스크립트에 대해 더 알고 싶습니다.');
INSERT INTO comments (board_id, user_id, content) VALUES (3, 2, '도움이 많이 되었습니다.');
-- 데이터 확인
SELECT * FROM users;
SELECT * FROM board;
SELECT * FROM comments;
CASCADE는 데이터베이스 관리 시스템(DBMS)에서 외래 키 제약 조건을 설정할 때 사용되는 옵션 중 하나입니다. 이 옵션은 참조 무결성을 유지하기 위해 부모 테이블의 행이 변경될 때 자식 테이블의 행에 자동으로 동일한 변경을 적용하도록 합니다.
CASCADE 옵션의 종류
- ON DELETE CASCADE
- 부모 테이블에서 행이 삭제될 때, 해당 행을 참조하는 모든 자식 테이블의 행들도 자동으로 삭제됩니다.
- 예를 들어, posts 테이블에서 특정 게시글이 삭제되면, 그 게시글에 달린 모든 댓글(comments 테이블)도 함께 삭제됩니다.
- ON UPDATE CASCADE
- 부모 테이블의 기본 키 값이 업데이트될 때, 해당 기본 키를 참조하는 자식 테이블의 외래 키 값도 자동으로 업데이트됩니다.
작업중 (임시)
더보기
CommentRepositoryImpl
package com.tenco.tboard.repository;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
import com.tenco.tboard.model.Comment;
import com.tenco.tboard.repository.interfaces.CommnetRepository;
import com.tenco.tboard.util.DBUtil;
public class CommnetRepositoryImpl implements CommnetRepository {
private static final String INSERT_COMMNET_SQL = " INSERT INTO comments (board_id, user_id, content) VALUES (?, ? , ?) ";
private static final String DELETE_COMMNET_SQL = " DELETE FROM comments WHERE id = ? ";
private static final String SELECT_COMMNET_BY_ID = " SELECT * FROM comments WHERE id = ? ";
private static final String SELECT_COMMNETS_BY_BOARD_ID = " SELECT * FROM comments WHERE board_id = ? ORDER BY created_at DESC ";
@Override
public void addComment(Comment comment) {
try (Connection conn = DBUtil.getConnection()) {
conn.setAutoCommit(false);
try (PreparedStatement pstmt = conn.prepareStatement(INSERT_COMMNET_SQL)) {
pstmt.setInt(1, comment.getBoardId());
pstmt.setInt(2, comment.getUserId());
pstmt.setString(3, comment.getContnet());
pstmt.executeUpdate();
conn.commit();
} catch (Exception e) {
conn.rollback();
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void deleteComment(int id) {
try (Connection conn = DBUtil.getConnection()) {
conn.setAutoCommit(false);
try (PreparedStatement pstmt = conn.prepareStatement(DELETE_COMMNET_SQL)) {
pstmt.setInt(1, id);
pstmt.executeUpdate();
conn.commit();
} catch (Exception e) {
conn.rollback();
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public Comment getCommentById(int id) {
Comment comment = null;
try (Connection conn = DBUtil.getConnection();
PreparedStatement pstmt = conn.prepareStatement(SELECT_COMMNET_BY_ID)) {
pstmt.setInt(1, id);
try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
comment = new Comment(rs.getInt("id"), rs.getInt("board_id"), rs.getInt("user_id"),
rs.getString("content"), rs.getTimestamp("created_at"));
}
} catch (Exception e) {
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
return comment;
}
@Override
public List<Comment> getCommentsByBoardId(int boardId) {
List<Comment> commentList = new ArrayList<>();
try (Connection conn = DBUtil.getConnection();
PreparedStatement pstmt = conn.prepareStatement(SELECT_COMMNETS_BY_BOARD_ID)) {
pstmt.setInt(1, boardId);
try (ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
Comment comment = new Comment(rs.getInt("id"), rs.getInt("board_id"), rs.getInt("user_id"),
rs.getString("content"), rs.getTimestamp("created_at"));
commentList.add(comment);
}
} catch (Exception e) {
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
return commentList;
}
}
Comment
package com.tenco.tboard.model;
import java.sql.Timestamp;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
public class Comment {
private int id;
private int boardId;
private int userId;
private String content;
private Timestamp createdAt;
private String username;
}
webapp/views/board/view.jsp (게시글 상세 보기 화면)
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>${board.title}</title>
<style>
/* common styles */
body {
font-family: Arial, sans-serif;
background-color: #f8f9fa;
margin: 0;
padding: 0;
}
.container {
width: 80%;
margin: 0 auto;
background-color: #fff;
padding: 20px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
margin-top: 20px;
}
h2, h3 {
color: #343a40;
}
p {
color: #6c757d;
}
a {
text-decoration: none;
}
.btn {
display: inline-block;
padding: 10px 20px;
margin: 10px 5px;
border-radius: 5px;
color: #fff;
background-color: #007bff;
text-align: center;
}
.btn:hover {
background-color: #0056b3;
}
.btn-edit {
background-color: #28a745;
}
.btn-edit:hover {
background-color: #218838;
}
.btn-delete {
background-color: #dc3545;
}
.btn-delete:hover {
background-color: #c82333;
}
.btn-list {
background-color: #17a2b8;
}
.btn-list:hover {
background-color: #138496;
}
/* view specific styles */
h2 {
font-size: 2em;
margin-bottom: 20px;
}
p {
font-size: 1.2em;
line-height: 1.6;
}
p small {
display: block;
margin-top: 10px;
color: #6c757d;
}
.btn {
font-size: 1em;
}
h3 {
margin-top: 40px;
margin-bottom: 20px;
font-size: 1.5em;
}
/* comment form styles */
.comment-section {
margin-top: 30px;
}
.comment-list {
margin-bottom: 20px;
}
.comment {
padding: 10px;
border-bottom: 1px solid #ddd;
}
.comment .comment-author {
font-weight: bold;
color: #343a40;
}
.comment .comment-date {
font-size: 0.9em;
color: #6c757d;
}
.comment .comment-content {
margin-top: 10px;
font-size: 1.1em;
}
.comment-form {
margin-top: 20px;
display: flex;
flex-direction: column;
}
.comment-form textarea {
padding: 10px;
font-size: 1em;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 5px;
resize: vertical;
}
.comment-form button {
align-self: flex-start;
padding: 10px 20px;
border: none;
border-radius: 5px;
background-color: #28a745;
color: #fff;
font-size: 1em;
cursor: pointer;
}
.comment-form button:hover {
background-color: #218838;
}
</style>
</head>
<body>
<div class="container">
<h2>${board.title}</h2>
<p>${board.content}</p>
<p>
<fmt:formatDate value="${board.createdAt}" pattern="yyyy-MM-dd HH:mm" />
</p>
<c:if test="${board.userId == principal.id}">
<a class="btn btn-edit" href="${pageContext.request.contextPath}/board/edit?id=${board.id}">수정</a>
<a class="btn btn-delete" href="${pageContext.request.contextPath}/board/delete?id=${board.id}">삭제</a>
</c:if>
<a class="btn btn-list" href="${pageContext.request.contextPath}/board/list?page=1">목록으로 돌아가기</a>
</div>
<div class="container comment-section">
<h3>댓글</h3>
<!-- 댓글 리스트 -->
<div class="comment-list">
<c:forEach var="comment" items="${commentList}">
<div class="comment">
<div class="comment-author">${comment.username}</div>
<div class="comment-date">${comment.createdAt}</div>
<div class="comment-content">${comment.content}</div>
</div>
</c:forEach>
</div>
<!-- 댓글 작성 폼 -->
<form class="comment-form" action="${pageContext.request.contextPath}/comment/add" method="post">
<textarea name="content" rows="4" placeholder="댓글을 작성하세요..." required></textarea>
<button type="submit">댓글 작성</button>
</form>
</div>
</body>
</html>
Board (model or DTO )
package com.tenco.tboard.model;
import java.sql.Timestamp;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
public class Board {
private int id;
private int userId;
private String title;
private String content;
private Timestamp createdAt;
}
BoardRepository
package com.tenco.tboard.repository.interfaces;
import java.util.List;
import com.tenco.tboard.model.Comment;
public interface CommentRepository {
void addComment(Comment comment);
void deleteComment(int id);
Comment getCommentById(int id);
List<Comment> getCommentsByBoardId(int boardId);
}
BoardRepositoryImpl (DAO)
package com.tenco.tboard.repository;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
import com.tenco.tboard.model.Board;
import com.tenco.tboard.repository.interfaces.BoardRepository;
import com.tenco.tboard.util.DBUtil;
public class BoardRepositoryImpl implements BoardRepository {
private static final String SELECT_ALL_BOARDS = " select * from board order by created_at desc limit ? offset ? ";
private static final String COUNT_ALL_BOARDS = " select count(*) as count from board ";
private static final String INSERT_BOARD_SQL = " INSERT INTO board(user_id, title, content) values(?, ?, ?) ";
private static final String DELETE_BOARD_SQL = " DELETE FROM board WHERE id = ? ";
private static final String SELECT_BOARD_BY_ID = " SELECT * FROM board WHERE id = ? ";
private static final String UPDATE_BOARD_SQL = " UPDATE board SET title = ?, content = ? WHERE id = ? ";
@Override
public void addBoard(Board board) {
try (Connection conn = DBUtil.getConnection()) {
conn.setAutoCommit(false);
try (PreparedStatement pstmt = conn.prepareStatement(INSERT_BOARD_SQL)) {
pstmt.setInt(1, board.getUserId());
pstmt.setString(2, board.getTitle());
pstmt.setString(3, board.getContent());
pstmt.executeUpdate();
conn.commit();
} catch (Exception e) {
conn.rollback();
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void updateBoard(Board board) {
try (Connection conn = DBUtil.getConnection()) {
conn.setAutoCommit(false);
try (PreparedStatement pstmt = conn.prepareStatement(UPDATE_BOARD_SQL)) {
pstmt.setString(1, board.getTitle());
pstmt.setString(2, board.getContent());
pstmt.setInt(3, board.getId());
pstmt.executeUpdate();
conn.commit();
} catch (Exception e) {
conn.rollback();
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void deleteBoard(int id) {
try (Connection conn = DBUtil.getConnection()){
conn.setAutoCommit(false);
try (PreparedStatement pstmt = conn.prepareStatement(DELETE_BOARD_SQL)){
pstmt.setInt(1, id);
pstmt.executeUpdate();
conn.commit();
} catch (Exception e) {
conn.rollback();
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public Board getBoardById(int id) {
Board board = null;
try (Connection conn = DBUtil.getConnection();
PreparedStatement pstmt = conn.prepareStatement(SELECT_BOARD_BY_ID)){
pstmt.setInt(1, id);
try (ResultSet rs = pstmt.executeQuery()){
if (rs.next()) {
board = Board.builder()
.id(rs.getInt("id"))
.userId(rs.getInt("user_id"))
.title(rs.getString("title"))
.content(rs.getString("content"))
.createdAt(rs.getTimestamp("created_at"))
.build();
}
} catch (Exception e) {
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
return board;
}
@Override
public List<Board> getAllBoards(int limit, int offset) {
List<Board> boardList = new ArrayList<>();
try (Connection conn = DBUtil.getConnection();
PreparedStatement pstmt = conn.prepareStatement(SELECT_ALL_BOARDS)) {
pstmt.setInt(1, limit);
pstmt.setInt(2, offset);
ResultSet rs = pstmt.executeQuery();
while (rs.next()) {
boardList.add(
Board.builder().id(rs.getInt("id"))
.userId(rs.getInt("user_id")).title(rs.getString("title"))
.content(rs.getString("content"))
.createdAt(rs.getTimestamp("created_at"))
.build());
}
System.out.println("BoardRepositoryImpl - 로깅 : count " + boardList.size());
} catch (Exception e) {
e.printStackTrace();
}
return boardList;
}
@Override
public int getTotalBoardCount() {
int count = 0;
try (Connection conn = DBUtil.getConnection();
PreparedStatement pstmt = conn.prepareStatement(COUNT_ALL_BOARDS)){
ResultSet rs = pstmt.executeQuery();
if(rs.next()) {
count = rs.getInt("count");
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(" 로깅 totalCount : " + count);
return count;
}
}
BoardController
(package com.tenco.tboard.controller)
package com.tenco.tboard.controller;
import java.io.IOException;
import java.util.List;
import com.tenco.tboard.model.Board;
import com.tenco.tboard.model.Comment;
import com.tenco.tboard.model.User;
import com.tenco.tboard.repository.BoardRepositoryImpl;
import com.tenco.tboard.repository.CommentRepositoryImpl;
import com.tenco.tboard.repository.interfaces.BoardRepository;
import com.tenco.tboard.repository.interfaces.CommentRepository;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
@WebServlet("/board/*")
public class BoardController extends HttpServlet {
private static final long serialVersionUID = 1L;
private BoardRepository boardRepository;
private CommentRepository commentRepository;
@Override
public void init() throws ServletException {
boardRepository = new BoardRepositoryImpl();
commentRepository = new CommentRepositoryImpl();
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String action = request.getPathInfo();
HttpSession session = request.getSession(false);
if(session == null || session.getAttribute("principal") == null ) {
response.sendRedirect(request.getContextPath() + "/user/signin");
return;
}
switch (action) {
case "/delete":
handleDeleteBoard(request, response, session);
break;
case "/update":
showEditBoardForm(request, response, session);
break;
case "/create":
showCreateBoardForm(request, response, session);
break;
case "/list":
handleListBoards(request, response, session);
break;
case "/view":
showViewBoard(request, response, session);
break;
case "/deleteComment":
handleDeleteComment(request, response, session);
break;
default:
response.sendError(HttpServletResponse.SC_NOT_FOUND);
break;
}
}
/**
* 댓글 삭제 기능( GET 방식 처리)
* @param request
* @param response
* @param session
*/
private void handleDeleteComment(HttpServletRequest request, HttpServletResponse response, HttpSession session) {
// TODO Auto-generated method stub
}
/**
* 상세 보기 화면 이동(GET 방식 처리)
* @param request
* @param response
* @param session
*/
private void showViewBoard(HttpServletRequest request, HttpServletResponse response, HttpSession session) {
try {
int boardId = Integer.parseInt(request.getParameter("id"));
Board board = boardRepository.getBoardById(boardId);
if(board == null) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 현재 로그인한 사용자의 ID
User user = (User)session.getAttribute("principal");
if(user != null) {
request.setAttribute("userID", user.getId());
}
List<Comment> commentList = commentRepository.getCommentsByBoardId(boardId);
request.setAttribute("board", board);
request.setAttribute("commentList", commentList);
request.getRequestDispatcher("/WEB-INF/views/board/view.jsp").forward(request, response);
} catch (Exception e) {
// 잘못된 접근 입니다. back();
}
}
/**
* 수정 폼 화면 이동(인증 검사 반드시 처리)
* @param request
* @param response
* @param session
*/
private void showEditBoardForm(HttpServletRequest request, HttpServletResponse response, HttpSession session) {
// TODO Auto-generated method stub
}
/**
* 게시글 삭제 기능 만들기
* @param request
* @param response
* @param session
*/
private void handleDeleteBoard(HttpServletRequest request, HttpServletResponse response, HttpSession session) {
}
/**
* 게시글 생성 화면 이동
* @param request
* @param response
* @param session
* @throws IOException
* @throws ServletException
*/
private void showCreateBoardForm(HttpServletRequest request, HttpServletResponse response, HttpSession session) throws ServletException, IOException {
request.getRequestDispatcher("/WEB-INF/views/board/create.jsp").forward(request, response);
}
/**
* 페이징 처리 하기
* @param request
* @param response
* @throws IOException
* @throws ServletException
*/
private void handleListBoards(HttpServletRequest request, HttpServletResponse response, HttpSession session) throws ServletException, IOException {
int page = 1; // 기본 페이지 번호
int pageSize = 3; // 한 페이지당 보여질 게시글에 수
try {
String pageStr = request.getParameter("page");
if(pageStr != null ) {
page = Integer.parseInt(pageStr);
}
} catch (Exception e) {
page = 1;
}
int offset = (page - 1) * pageSize; // 시작 위치 계산( offset 값 계산)
List<Board> boardList = boardRepository.getAllBoards(pageSize, offset);
// 전체 게시글 수 조회
int totalBoards = boardRepository.getTotalBoardCount();
// 총 페이지 수 계산 --> [1][2][3][...]
int totalPages = (int) Math.ceil((double)totalBoards / pageSize);
request.setAttribute("boardList", boardList);
request.setAttribute("totalPages", totalPages);
request.setAttribute("currentPage", page);
// 현재 로그인한 사용자 ID 설정
if(session != null) {
User user = (User)session.getAttribute("principal");
if(user != null) {
request.setAttribute("userId", user.getId());
}
}
request.getRequestDispatcher("/WEB-INF/views/board/list.jsp").forward(request, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String action = request.getPathInfo();
HttpSession session = request.getSession(false);
if(session == null || session.getAttribute("principal") == null ) {
response.sendRedirect(request.getContextPath() + "/user/signin");
return;
}
switch (action) {
case "/create":
handleCreateBoard(request, response, session);
break;
case "/edit":
break;
case "/addComment":
break;
default:
response.sendError(HttpServletResponse.SC_NOT_FOUND);
break;
}
}
/**
* 게시글 생성 처리
* @param request
* @param response
* @param session
* @throws IOException
*/
private void handleCreateBoard(HttpServletRequest request, HttpServletResponse response, HttpSession session) throws IOException {
// 유효성 검사 생략
String title = request.getParameter("title");
String content = request.getParameter("content");
User user = (User)session.getAttribute("principal");
Board board = Board.builder()
.userId(user.getId())
.title(title)
.content(content)
.build();
boardRepository.addBoard(board);
response.sendRedirect(request.getContextPath() + "/board/list?page=1");
}
}
DB 접근 기술 -1
더보기
package com.tenco.tboard.util;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class DBUtilBasic {
private static final String DB_URL = "jdbc:mysql://localhost:3306/db_tboard?useSSL=false&serverTimezone=Asia/Seoul";
private static final String DB_USER = "root";
private static final String DB_PASSWORD = "asd123";
static {
try {
Class.forName("com.mysql.cj.Driver");
} catch (Exception e) {
System.out.println("DB 드라이버 로딩 실패");
e.printStackTrace();
}
}
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
}
}
- serverTimezone=Asia/Seoul
- serverTimezone=Asia/Seoul로 설정하면, JDBC 드라이버가 데이터베이스 서버의 시간대를 한국 시간(서울)으로 해석합니다. 이는 서버와 클라이언트의 시간대 차이로 인한 문제를 방지하기 위해 설정할 수 있습니다.
- useSSL=false
- useSSL 옵션은 데이터베이스 연결 시 SSL(Secure Sockets Layer)을 사용할지 여부를 설정합니다.
- useSSL=false로 설정하면, SSL을 사용하지 않고 데이터베이스에 연결합니다.
- SSL을 사용하지 않는 이유
- 로컬 개발 환경: 개발 환경에서는 보안이 덜 중요한 경우가 많아 SSL을 비활성화하여 연결 설정을 단순화할 수 있습니다.
- 성능: SSL을 사용하면 데이터 전송이 암호화되므로 성능에 약간의 영향을 줄 수 있습니다. 성능을 중시하는 경우 SSL을 비활성화할 수 있습니다.
- 주의: SSL을 비활성화하면 데이터 전송이 암호화되지 않기 때문에 보안에 취약해질 수 있습니다. 특히, 운영 환경에서는 SSL을 사용하는 것이 좋습니다.
DB 접근 기술 - 2
더보기
<?xml version="1.0" encoding="UTF-8"?>
<Context>
<Resource name="jdbc/tboard"
auth="Container"
type="javax.sql.DataSource"
factory="com.zaxxer.hikari.HikariJNDIFactory"
uniqueResourceName="MyTboard"
minimumIdle="5"
maximumPoolSize="10"
connectionTimeout="30000"
idleTimeout="600000"
maxLifetime="1800000"
jdbcUrl="jdbc:mysql://localhost:3306/db_tboard?serverTimezone=Asia/Seoul"
driverClassName="com.mysql.cj.jdbc.Driver"
username="root"
password="asd123"/>
</Context>
context.xml 파일은 톰캣 서버에서 JNDI(Java Naming and Directory Interface) 리소스를 설정하는 데 사용됩니다. 이 파일을 통해 데이터베이스 연결 풀을 정의하고 설정할 수 있습니다.
설정 항목 | 설명 |
<Resource> 태그 | JNDI 리소스를 정의합니다. 이 리소스를 통해 데이터베이스 연결을 관리합니다. |
name="jdbc/tboard" | JNDI 리소스의 이름을 지정합니다. 애플리케이션에서 이 이름으로 데이터베이스 연결을 조회할 수 있습니다. |
auth="Container" | 리소스의 인증을 컨테이너에서 관리하도록 설정합니다. |
type="javax.sql.DataSource" | 리소스의 타입을 지정합니다. 여기서는 javax.sql.DataSource 타입으로 설정하여 데이터베이스 연결 풀을 정의합니다. |
factory="com.zaxxer.hikari.HikariJNDIFactory" | HikariCP (High-Performance JDBC Connection Pool)에서 제공하는 JNDI 팩토리 클래스를 사용하여 연결 풀을 생성합니다. |
minimumIdle="5" maximumPoolSize="10" | 연결 풀의 설정입니다. 최소 유휴 연결 수를 5로, 최대 연결 수를 10으로 설정합니다. |
idleTimeout="30000" maxLifetime="1800000" | 연결의 유휴 타임아웃(30초) 및 최대 생존 시간(30분)을 설정합니다. |
dataSourceClassName="com.mysql.cj.jdbc.MysqlDataSource" | 사용할 데이터베이스 드라이버 클래스 이름을 지정합니다. 여기서는 MySQL 데이터 소스 클래스를 사용합니다. |
dataSource.url, dataSource.user, dataSource.password | 데이터베이스 연결 정보입니다. 데이터베이스 URL, 사용자 이름, 비밀번호를 설정합니다. |
package com.tenco.tboard.util;
import java.sql.Connection;
import java.sql.SQLException;
import javax.naming.InitialContext;
import javax.sql.DataSource;
public class DBUtil {
private static DataSource dataSource;
static {
try {
InitialContext ctx = new InitialContext();
dataSource = (DataSource)ctx.lookup("java:comp/env/jdbc/tboard");
} catch (Exception e) {
System.out.println("DBUtil 초기화 실패 ");
e.printStackTrace();
}
}
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
}
DBUtil 클래스는 JNDI를 통해 데이터베이스 연결 풀에서 연결을 가져오는 유틸리티 클래스 입니다.
- InitialContext ctx = new InitialContext();로 JNDI 초기 컨텍스트를 생성합니다.
- ctx.lookup("java:comp/env/jdbc/tboard");로 context.xml에 정의된 데이터 소스를 찾습니다. 여기서
"java:comp/env/jdbc/tboard"은 JNDI 리소스의 이름입니다.
index.jsp 파일 생성
더보기
webapp/index.jsp 파일 생성
http://localhost:8080/t-board/index.jsp 요청 주소 확인
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>JSP MVC 게시판</title>
<style type="text/css">
body{
font-family: arial, sans-serif;
background-color: #f4f4f4;
color: #333;
margin: 0;
padding: 0;
}
.nav-list {
list-style-type: none;
padding: 0;
}
.nav-list li {
margin: 10px;
display: inline-block;
}
.nav-list li a {
text-decoration: none;
padding: 10px 20px;
color: black;
}
.btn-primary {
background-color: #007bff;
}
.btn-secondary {
background-color: #6c757d;
}
</style>
</head>
<body>
<div class="container">
<h2>JSP MVC 게시판 테스트 페이지</h2>
<ul class="nav-list">
<li class="btn btn-primary"><a href="/t-board/user/signup">회원가입</a></li>
<li class="btn btn-primary"><a href="/t-board/user/signup">로그인</a></li>
<li class="btn btn-secondary"><a href="/t-board/user/signup">로그아웃</a></li>
<li class="btn btn-primary"><a href="/t-board/user/signup">게시판목록</a></li>
</ul>
</div>
</body>
</html>
회원 가입 및 로그인 기능 만들기
더보기
User
package com.tenco.tboard.model;
import java.sql.Timestamp;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
private int id;
private String username;
private String password;
private String email;
private Timestamp createdAt;
}
UserRepository
package com.tenco.tboard.repository;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
import com.tenco.tboard.model.User;
import com.tenco.tboard.repository.interfaces.UserRepository;
import com.tenco.tboard.util.DBUtil;
public class UserRepositoryImpl implements UserRepository {
private static final String INSERT_USER_SQL = " INSERT INTO users (username, password, email) VALUES(? , ? , ?) ";
private static final String DELETE_USER_SQL = " DELETE FROM users WHERE id = ? ";
private static final String SELECT_USER_BY_UESRNAME = " SELECT * FROM users WHERE username = ? ";
private static final String SELECT_USER_BY_UESRNAME_AND_PASSWORD = " SELECT * FROM users WHERE username = ? AND password = ? ";
private static final String SELECT_ALL_USERS = " SELECT * FROM users ";
@Override
public void addUser(User user) {
try (Connection conn = DBUtil.getConnection()){
conn.setAutoCommit(false);
// username 중복 확인 필요
// email 중복 확인 필요
try (PreparedStatement pstmt = conn.prepareStatement(INSERT_USER_SQL)){
pstmt.setString(1, user.getUsername());
pstmt.setString(2, user.getPassword());
pstmt.setString(3, user.getEmail());
pstmt.executeUpdate();
conn.commit();
} catch (Exception e) {
conn.rollback();
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void deleteUser(int id) {
try (Connection conn = DBUtil.getConnection()){
conn.setAutoCommit(false);
try (PreparedStatement pstmt = conn.prepareStatement(DELETE_USER_SQL)){
pstmt.setInt(1, id);
pstmt.executeUpdate();
conn.commit();
} catch (Exception e) {
conn.rollback();
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public User getUserByusername(String username) {
User user = null;
try (Connection conn = DBUtil.getConnection();
PreparedStatement pstmt = conn.prepareStatement(SELECT_USER_BY_UESRNAME)) {
pstmt.setString(1, username);
ResultSet rs = pstmt.executeQuery();
if(rs.next()) {
user = User.builder()
.id(rs.getInt("id"))
.username(rs.getString("username"))
.password(rs.getString("password"))
.email(rs.getString("email"))
.createdAt(rs.getTimestamp("created_at"))
.build();
}
} catch (Exception e) {
e.printStackTrace();
}
return user;
}
@Override
public User getUserByusernameAndPassword(String username, String password) {
User user = null;
try (Connection conn = DBUtil.getConnection();
PreparedStatement pstmt = conn.prepareStatement(SELECT_USER_BY_UESRNAME_AND_PASSWORD)) {
pstmt.setString(1, username);
pstmt.setString(2, password);
ResultSet rs = pstmt.executeQuery();
if(rs.next()) {
user = User.builder()
.id(rs.getInt("id"))
.username(rs.getString("username"))
.password(rs.getString("password"))
.email(rs.getString("email"))
.createdAt(rs.getTimestamp("created_at"))
.build();
}
} catch (Exception e) {
e.printStackTrace();
}
return user;
}
@Override
public List<User> getAllUsers() {
List<User> userList = new ArrayList<>();
try (Connection conn = DBUtil.getConnection();
PreparedStatement pstmt = conn.prepareStatement(SELECT_ALL_USERS)) {
ResultSet rs = pstmt.executeQuery();
while(rs.next()) {
User user = User.builder()
.id(rs.getInt("id"))
.username(rs.getString("username"))
.password(rs.getString("password"))
.email(rs.getString("email"))
.createdAt(rs.getTimestamp("created_at"))
.build();
userList.add(user);
}
} catch (Exception e) {
e.printStackTrace();
}
return userList;
}
}
signup JSP 파일 생성
- WEB-INF 폴더에 대한 이해
- 주소 설계 및 name 속성 반드시 확인
- 테스트 시 기본 값을 넣어 두자.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>회원가입</title>
<link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/resources/css/common.css">
<link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/resources/css/signup.css">
</head>
<body>
<div class="container">
<h2>회원가입</h2>
<form action="${pageContext.request.contextPath}/user/signup" method="post" >
<div class="form-group">
<label for="username">Username : </label>
<input type="text" id="username" name="username" value="고길동" required>
</div>
<div class="form-group">
<label for="password">Password : </label>
<input type="text" id="password" name="password" value="asd1234" required>
</div>
<div class="form-group">
<label for=""email"">Email : </label>
<input type="email" id="email" name="email" value="a@naver.com" required>
</div>
<div class="form-group">
<input class="btn btn-primary" type="submit" value="회원가입" >
</div>
</form>
</div>
</body>
</html>
WEB-INF 폴더란?
- WEB-INF 폴더는 클라이언트(웹 브라우저)에서 직접 접근할 수 없습니다.
- 서버 설정에 의해 보호되며, 외부에서 직접 요청할 수 없습니다. 이 폴더 내의 파일들은 서블릿이나 JSP 페이지를 통해서만 접근할 수 있습니다.
pageContext란?
- pageContext는 JSP 페이지에서 가장 상위에 있는 내장 객체로, 그 하위에 다양한 범위의 객체들(request, response, session, application 등)에 접근할 수 있는 기능을 포함하고 있음.
- pageContext 객체는 현재 JSP 페이지에서만 유효합니다. 다른 JSP 페이지나 서블릿으로 이동할 때, pageContext의 속성은 자동으로 전달되지 않습니다
Page Scope
- 현재 JSP 페이지 내에서만 유효합니다.
- 다른 JSP 페이지나 서블릿으로 이동하면 유효하지 않습니다.
- 설정 예) pageContext.setAttribute("attrName", "value");
- 접근 예:) pageContext.getAttribute("attrName");
signin JSP 파일 생성
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>로그인</title>
<link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/resources/css/common.css">
<link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/resources/css/signin.css">
</head>
<body>
<div class="container">
<h2>로그인</h2>
<c:if test="${not empty errorMessage }">
<p style="color: red;"> ${errorMessage} </p>
</c:if>
<form action="${pageContext.request.contextPath}/user/signin" method="post" >
<div class="form-group">
<label for="username">Username : </label>
<input type="text" id="username" name="username" value="고길동" required>
</div>
<div class="form-group">
<label for="password">Password : </label>
<input type="text" id="password" name="password" value="asd1234" required>
</div>
<div class="form-group">
<input class="btn btn-primary" type="submit" value="로그인" >
</div>
</form>
</div>
</body>
</html>
UserController
package com.tenco.tboard.controller;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import com.tenco.tboard.model.User;
import com.tenco.tboard.repository.UserRepositoryImpl;
import com.tenco.tboard.repository.interfaces.UserRepository;
@WebServlet("/user/*")
public class UserController extends HttpServlet {
private static final long serialVersionUID = 1L;
private UserRepository userRepository;
@Override
public void init() throws ServletException {
userRepository = new UserRepositoryImpl();
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String action = request.getPathInfo();
switch (action) {
case "/signup":
request.getRequestDispatcher("/WEB-INF/views/user/signup.jsp").forward(request, response);
break;
case "/signin":
request.getRequestDispatcher("/WEB-INF/views/user/signin.jsp").forward(request, response);
break;
case "/logout":
handleLogout(request, response);
break;
default:
response.sendError(HttpServletResponse.SC_NOT_FOUND);
break;
}
}
/**
* 로그아웃 기능 처리
* @throws IOException
* http://localhost:8080/t-board/user/logout
*/
private void handleLogout(HttpServletRequest request, HttpServletResponse response) throws IOException {
HttpSession session = request.getSession();
session.invalidate();
response.sendRedirect(request.getContextPath() + "/user/signin");
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String action = request.getPathInfo();
switch (action) {
case "/signup":
handleSignup(request, response);
break;
case "/signin":
handleSignin(request, response);
break;
default:
response.sendError(HttpServletResponse.SC_NOT_FOUND);
break;
}
}
private void handleSignin(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String username = request.getParameter("username");
String password = request.getParameter("password");
User principal = userRepository.getUserByusernameAndPassword(username, password);
if(principal != null && principal.getPassword().equals(password)) {
HttpSession session = request.getSession();
session.setAttribute("principal", principal);
response.sendRedirect(request.getContextPath() + "/board/list");
} else {
request.setAttribute("errorMessage", "잘못된 요청입니다.");
request.getRequestDispatcher("/WEB-INF/views/user/signin.jsp").forward(request, response);
}
}
private void handleSignup(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 데이터 추출
String username = request.getParameter("username") ;
String password = request.getParameter("password") ;
String email = request.getParameter("email");
// 데이터 유효성 검사 생략
User user = User.builder()
.username(username)
.password(password)
.email(email)
.build();
// TODO 삭제 예정
System.out.println(user);
int result = userRepository.addUser(user);
if(result != 0) {
response.sendRedirect(request.getContextPath() + "/user/signin");
} else {
response.setContentType("text/html; charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("<script> alert('잘못된 요청입니다'); history.back(); </script>");
}
}
}
게시판 기능 만들어 보기 -1
더보기
Board
package com.tenco.tboard.model;
import java.security.Timestamp;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
public class Board {
private int id;
private int userId;
private String title;
private String content;
private Timestamp createdAt;
}
BoardRepository
package com.tenco.tboard.repository.interfaces;
import java.util.List;
import com.tenco.tboard.model.Board;
public interface BoardRepository {
void addBoard(Board board);
void updateBoard(Board board, int principalId);
void deleteBoard(int id, int principalId);
Board getBoardById(int id);
List<Board> getAllBoards(int limit, int offset);
int getTotalBoardCount();
}
list.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>게시글 목록</title>
<link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/resources/css/common.css">
<link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/resources/css/list.css">
</head>
<body>
<h2>게시글 목록</h2>
<div class="action">
<a class="btn btn-create" href="${pageContext.request.contextPath}/board/create" >새글 작성하기</a>
<a class="btn btn-back" href="${pageContext.request.contextPath}/index.jsp" >홈 화면</a>
</div>
<c:forEach var="board" items="${boardList}">
<div class="board-item">
<h3><a href="#">${board.title}</a></h3>
<p>${board.content}</p>
<p> <fmt:formatDate value="${board.createdAt}" pattern="yyyy-MM-dd HH:mm" /></p>
<!-- 게시글에 작성자가 세션 유저와 동일하다면 수정, 삭제 버튼을 보여주자 -->
<c:if test="${board.userId == userId }">
<a class="btn btn-edit" href="#">수정</a>
<a class="btn btn-delete" href="#">삭제</a>
</c:if>
</div>
</c:forEach>
<br>
<div class="pagination">
<!-- index for -->
<c:forEach begin="1" end="${totalPages}" var="i" >
<c:choose>
<c:when test="${ i == currentPage }">
<span class="current-page" >${i}</span>
</c:when>
<c:otherwise>
<span><a href="${pageContext.request.contextPath}/board/list?page=${i}">${i}</a></span>
</c:otherwise>
</c:choose>
</c:forEach>
</div>
</body>
</html>
board.css
@charset "UTF-8";
h2 {
text-align: center;
color: #007bff;
}
.btn {
display: inline-block;
padding: 10px 15px;
margin-top: 10px;
text-decoration: none;
border-radius: 4px;
}
list.css
@charset "UTF-8";
@import url("board.css");
.action {
margin-bottom: 20px;
}
.action .btn {
margin-right: 10px;
}
.board-item {
margin-bottom: 20px;
padding: 15px;
border-bottom: 1px solid #ddd;
}
.board-item h3{
margin: 0;
}
.board-item p{
margin: 5px 0;
}
.btn-create {
background-color: #28a745;
color: white;
text-align: center;
}
.btn-create:hover {
background-color: #218838;
}
.btn-edit {
background-color: #007bff;
color: white;
text-align: center;
}
.btn-edit:hover {
background-color: #0056b3;
}
.btn-delete {
background-color: #dc3545;
color: white;
text-align: center;
}
.btn-delete:hover {
background-color: #c82333;
}
.btn-back {
background-color: #6c757d;
color: white;
}
.btn-back:hover {
background-color: #5a6268;
}
.pagination {
text-align: center;
margin-top: 20px;
margin-bottom: 40px;
}
.pagination a {
display: inline-block;
padding: 5px 10px;
margin: 0 5px;
border: 1px solid #ddd;
text-decoration: none;
color: #007bff;
}
.pagination a:hover {
background-color: #007bff;
color: white;
}
.current-page {
display: inline-block;
padding: 5px 10px;
background-color: #007bff;
border: 1px solid #007bff;
border-radius: 4px;
color: white;
}
common.css
@charset "UTF-8";
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f4f4f4;
color: #333;
}
create.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>새글 작성하기</title>
<link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/resources/css/create.css">
</head>
<body>
<div class="container">
<h2>새글 작성하기</h2>
<form action="${pageContext.request.contextPath}/board/create" method="post">
<div class="form-group">
<label for="title">제목</label>
<input type="text" id="title" name="title" value="코딩 테스트 연습">
</div>
<div class="form-group">
<label for="content">내용</label>
<input type="text" id="content" name="content" value="알고리즘, 생산성 향상">
</div>
<div class="form-group">
<input type="submit" value="작성하기" class="btn btn-submit">
<a href="${pageContext.request.contextPath}/board/list?page=1" class="btn btn-back">목록으로 돌아가기</a>
</div>
</form>
</div>
</body>
</html>
BoardController
package com.tenco.tboard.controller;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.util.List;
import com.tenco.tboard.model.Board;
import com.tenco.tboard.model.User;
import com.tenco.tboard.repository.BoardRepositoryImpl;
import com.tenco.tboard.repository.interfaces.BoardRepository;
import com.tenco.tboard.repository.interfaces.UserRepository;
@WebServlet("/board/*")
public class BoardController extends HttpServlet {
private static final long serialVersionUID = 1L;
private BoardRepository boardRepository;
@Override
public void init() throws ServletException {
boardRepository = new BoardRepositoryImpl();
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String action = request.getPathInfo();
HttpSession session = request.getSession(false);
if(session == null || session.getAttribute("principal") == null ) {
response.sendRedirect(request.getContextPath() + "/user/signin");
return;
}
switch (action) {
case "/delete":
handleDeleteBoard(request, response, session);
break;
case "/update":
showEditBoardForm(request, response, session);
break;
case "/create":
showCreateBoardForm(request, response, session);
break;
case "/list":
handleListBoards(request, response, session);
break;
case "/view":
showViewBoard(request, response, session);
break;
case "/deleteComment":
handleDeleteComment(request, response, session);
break;
default:
response.sendError(HttpServletResponse.SC_NOT_FOUND);
break;
}
}
/**
* 댓글 삭제 기능( GET 방식 처리)
* @param request
* @param response
* @param session
*/
private void handleDeleteComment(HttpServletRequest request, HttpServletResponse response, HttpSession session) {
// TODO Auto-generated method stub
}
/**
* 상세 보기 화면 이동(GET 방식 처리)
* @param request
* @param response
* @param session
*/
private void showViewBoard(HttpServletRequest request, HttpServletResponse response, HttpSession session) {
try {
int id = Integer.parseInt(request.getParameter("id"));
Board board = boardRepository.getBoardById(id);
if(board == null) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
request.setAttribute("board", board);
// 현재 로그인한 사용자의 ID
User user = (User)session.getAttribute("principal");
if(user != null) {
request.setAttribute("userID", user.getId());
}
// TODO - 댓글 조회
// 댓글 조회 및 권환 확인 추가 예정
request.getRequestDispatcher("/WEB-INF/views/board/view.jsp").forward(request, response);
} catch (Exception e) {
// 잘못된 접근 입니다. back();
}
}
/**
* 수정 폼 화면 이동(인증 검사 반드시 처리)
* @param request
* @param response
* @param session
*/
private void showEditBoardForm(HttpServletRequest request, HttpServletResponse response, HttpSession session) {
// TODO Auto-generated method stub
}
/**
* 게시글 삭제 기능 만들기
* @param request
* @param response
* @param session
*/
private void handleDeleteBoard(HttpServletRequest request, HttpServletResponse response, HttpSession session) {
}
/**
* 게시글 생성 화면 이동
* @param request
* @param response
* @param session
* @throws IOException
* @throws ServletException
*/
private void showCreateBoardForm(HttpServletRequest request, HttpServletResponse response, HttpSession session) throws ServletException, IOException {
request.getRequestDispatcher("/WEB-INF/views/board/create.jsp").forward(request, response);
}
/**
* 페이징 처리 하기
* @param request
* @param response
* @throws IOException
* @throws ServletException
*/
private void handleListBoards(HttpServletRequest request, HttpServletResponse response, HttpSession session) throws ServletException, IOException {
int page = 1; // 기본 페이지 번호
int pageSize = 3; // 한 페이지당 보여질 게시글에 수
try {
String pageStr = request.getParameter("page");
if(pageStr != null ) {
page = Integer.parseInt(pageStr);
}
} catch (Exception e) {
page = 1;
}
int offset = (page - 1) * pageSize; // 시작 위치 계산( offset 값 계산)
List<Board> boardList = boardRepository.getAllBoards(pageSize, offset);
// 전체 게시글 수 조회
int totalBoards = boardRepository.getTotalBoardCount();
// 총 페이지 수 계산 --> [1][2][3][...]
int totalPages = (int) Math.ceil((double)totalBoards / pageSize);
request.setAttribute("boardList", boardList);
request.setAttribute("totalPages", totalPages);
request.setAttribute("currentPage", page);
// 현재 로그인한 사용자 ID 설정
if(session != null) {
User user = (User)session.getAttribute("principal");
if(user != null) {
request.setAttribute("userId", user.getId());
}
}
request.getRequestDispatcher("/WEB-INF/views/board/list.jsp").forward(request, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String action = request.getPathInfo();
HttpSession session = request.getSession(false);
if(session == null || session.getAttribute("principal") == null ) {
response.sendRedirect(request.getContextPath() + "/user/signin");
return;
}
switch (action) {
case "/create":
handleCreateBoard(request, response, session);
break;
case "/edit":
break;
case "/addComment":
break;
default:
response.sendError(HttpServletResponse.SC_NOT_FOUND);
break;
}
}
/**
* 게시글 생성 처리
* @param request
* @param response
* @param session
* @throws IOException
*/
private void handleCreateBoard(HttpServletRequest request, HttpServletResponse response, HttpSession session) throws IOException {
// 유효성 검사 생략
String title = request.getParameter("title");
String content = request.getParameter("content");
User user = (User)session.getAttribute("principal");
Board board = Board.builder()
.userId(user.getId())
.title(title)
.content(content)
.build();
boardRepository.addBoard(board);
response.sendRedirect(request.getContextPath() + "/board/list?page=1");
}
}
BoardRepositoryImpl
package com.tenco.tboard.repository;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
import com.tenco.tboard.model.Board;
import com.tenco.tboard.repository.interfaces.BoardRepository;
import com.tenco.tboard.util.DBUtil;
public class BoardRepositoryImpl implements BoardRepository {
private static final String SELECT_ALL_BOARDS = " select * from board order by created_at desc limit ? offset ? ";
private static final String COUNT_ALL_BOARDS = " select count(*) as count from board ";
private static final String INSERT_BOARD_SQL = " INSERT INTO board(user_id, title, content) values(?, ?, ?) ";
private static final String DELETE_BOARD_SQL = " DELETE FROM board WHERE id = ? ";
private static final String SELECT_BOARD_BY_ID = " SELECT * FROM board WHERE id = ? ";
private static final String UPDATE_BOARD_SQL = " UPDATE board SET title = ?, content = ? WHERE id = ? ";
@Override
public void addBoard(Board board) {
try (Connection conn = DBUtil.getConnection()) {
conn.setAutoCommit(false);
try (PreparedStatement pstmt = conn.prepareStatement(INSERT_BOARD_SQL)) {
pstmt.setInt(1, board.getUserId());
pstmt.setString(2, board.getTitle());
pstmt.setString(3, board.getContent());
pstmt.executeUpdate();
conn.commit();
} catch (Exception e) {
conn.rollback();
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void updateBoard(Board board) {
try (Connection conn = DBUtil.getConnection()) {
conn.setAutoCommit(false);
try (PreparedStatement pstmt = conn.prepareStatement(UPDATE_BOARD_SQL)) {
pstmt.setString(1, board.getTitle());
pstmt.setString(2, board.getContent());
pstmt.setInt(3, board.getId());
pstmt.executeUpdate();
conn.commit();
} catch (Exception e) {
conn.rollback();
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void deleteBoard(int id) {
try (Connection conn = DBUtil.getConnection()){
conn.setAutoCommit(false);
try (PreparedStatement pstmt = conn.prepareStatement(DELETE_BOARD_SQL)){
pstmt.setInt(1, id);
pstmt.executeUpdate();
conn.commit();
} catch (Exception e) {
conn.rollback();
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public Board getBoardById(int id) {
Board board = null;
try (Connection conn = DBUtil.getConnection();
PreparedStatement pstmt = conn.prepareStatement(SELECT_BOARD_BY_ID)){
pstmt.setInt(1, id);
try (ResultSet rs = pstmt.executeQuery()){
if (rs.next()) {
board = Board.builder()
.id(rs.getInt("id"))
.userId(rs.getInt("user_id"))
.title(rs.getString("title"))
.content(rs.getString("content"))
.createdAt(rs.getTimestamp("created_at"))
.build();
}
} catch (Exception e) {
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
return board;
}
@Override
public List<Board> getAllBoards(int limit, int offset) {
List<Board> boardList = new ArrayList<>();
try (Connection conn = DBUtil.getConnection();
PreparedStatement pstmt = conn.prepareStatement(SELECT_ALL_BOARDS)) {
pstmt.setInt(1, limit);
pstmt.setInt(2, offset);
ResultSet rs = pstmt.executeQuery();
while (rs.next()) {
boardList.add(
Board.builder().id(rs.getInt("id"))
.userId(rs.getInt("user_id")).title(rs.getString("title"))
.content(rs.getString("content"))
.createdAt(rs.getTimestamp("created_at"))
.build());
}
System.out.println("BoardRepositoryImpl - 로깅 : count " + boardList.size());
} catch (Exception e) {
e.printStackTrace();
}
return boardList;
}
@Override
public int getTotalBoardCount() {
int count = 0;
try (Connection conn = DBUtil.getConnection();
PreparedStatement pstmt = conn.prepareStatement(COUNT_ALL_BOARDS)){
ResultSet rs = pstmt.executeQuery();
if(rs.next()) {
count = rs.getInt("count");
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(" 로깅 totalCount : " + count);
return count;
}
}
View.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>${board.title}</title>
<style>
/* common styles */
body {
font-family: Arial, sans-serif;
background-color: #f8f9fa;
margin: 0;
padding: 0;
}
.container {
width: 80%;
margin: 0 auto;
background-color: #fff;
padding: 20px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
margin-top: 20px;
}
h2, h3 {
color: #343a40;
}
p {
color: #6c757d;
}
a {
text-decoration: none;
}
.btn {
display: inline-block;
padding: 10px 20px;
margin: 10px 5px;
border-radius: 5px;
color: #fff;
background-color: #007bff;
text-align: center;
}
.btn:hover {
background-color: #0056b3;
}
.btn-edit {
background-color: #28a745;
}
.btn-edit:hover {
background-color: #218838;
}
.btn-delete {
background-color: #dc3545;
}
.btn-delete:hover {
background-color: #c82333;
}
.btn-list {
background-color: #17a2b8;
}
.btn-list:hover {
background-color: #138496;
}
/* view specific styles */
h2 {
font-size: 2em;
margin-bottom: 20px;
}
p {
font-size: 1.2em;
line-height: 1.6;
}
p small {
display: block;
margin-top: 10px;
color: #6c757d;
}
.btn {
font-size: 1em;
}
h3 {
margin-top: 40px;
margin-bottom: 20px;
font-size: 1.5em;
}
/* comment form styles */
.comment-section {
margin-top: 30px;
}
.comment-list {
margin-bottom: 20px;
}
.comment {
padding: 10px;
border-bottom: 1px solid #ddd;
}
.comment .comment-author {
font-weight: bold;
color: #343a40;
}
.comment .comment-date {
font-size: 0.9em;
color: #6c757d;
}
.comment .comment-content {
margin-top: 10px;
font-size: 1.1em;
}
.comment-form {
margin-top: 20px;
display: flex;
flex-direction: column;
}
.comment-form textarea {
padding: 10px;
font-size: 1em;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 5px;
resize: vertical;
}
.comment-form button {
align-self: flex-start;
padding: 10px 20px;
border: none;
border-radius: 5px;
background-color: #28a745;
color: #fff;
font-size: 1em;
cursor: pointer;
}
.comment-form button:hover {
background-color: #218838;
}
</style>
</head>
<body>
<div class="container">
<h2>${board.title}</h2>
<p>${board.content}</p>
<p>
<fmt:formatDate value="${board.createdAt}" pattern="yyyy-MM-dd HH:mm" />
</p>
<c:if test="${board.userId == principal.id}">
<a class="btn btn-edit" href="${pageContext.request.contextPath}/board/edit?id=${board.id}">수정</a>
<a class="btn btn-delete" href="${pageContext.request.contextPath}/board/delete?id=${board.id}">삭제</a>
</c:if>
<a class="btn btn-list" href="${pageContext.request.contextPath}/board/list?page=1">목록으로 돌아가기</a>
</div>
<div class="container comment-section">
<h3>댓글</h3>
<!-- 댓글 리스트 -->
<div class="comment-list">
<div class="comment">
<div class="comment-author">1</div>
<div class="comment-date"></div>
<div class="comment-content"></div>
</div>
</div>
<!-- 댓글 작성 폼 -->
<form class="comment-form" action="${pageContext.request.contextPath}/comment/add" method="post">
<textarea name="content" rows="4" placeholder="댓글을 작성하세요..." required></textarea>
<button type="submit">댓글 작성</button>
</form>
</div>
</body>
</html>
'JSP > CH04 - 서블릿과 JSP의 연동' 카테고리의 다른 글
04. 커스텀 태그(JSTL) 라이브러리 사용, (EL 표현식) (0) | 2024.07.15 |
---|---|
03. JSP와 MVC 패턴 Todo 프로젝트 (4) | 2024.07.10 |
02. 간단한 게시판 만들어 보기 (0) | 2024.07.10 |
01. 서블릿과 JSP의 개념과 차이점 (0) | 2024.07.05 |