방명록 만들기
✒️ 2025-05-28 11:47 내용 수정
실습 목표
- Spring을 사용하여 방명록 사이트를 만든다.
- 방명록 사이트에서 게시글 조회, 추가, 수정, 삭제를 수행한다.
실습 흐름
- DB와 Spring을 Mybatis로 연결한다.
- DB에 테이블을 추가한다.
- DTO와 DAO를 만들고, Context_3_dao에 DAO Bean을 추가한다.
- 조회, 수정, 삭제 기능을 DAO에 추가한다.
- DAO에 추가한 내용의 query문을 mapper에도 추가한다.
- JSP 페이지를 만들어 각 기능에 맞는 데이터를 보내거나 받을 수 있도록 작성한다.
- Controller를 생성하여 DAO를 호출하고, JSP에서 보낸 데이터를 이어준다.
- ServletContext에 Controller Bean을 추가한다.
흐름 내용
DB에 테이블 추가
-- 시퀀스
CREATE SEQUENCE SEQ_VISIT_IDX;
-- 테이블
CREATE TABLE VISIT(
IDX NUMBER(3) PRIMARY KEY,
NAME VARCHAR2(50),
CONTENT VARCHAR2(1000),
PWD VARCHAR2(50),
IP VARCHAR2(20),
REGDATE DATE
);
-- 샘플데이터
INSERT INTO VISIT VALUES(
SEQ_VISIT_IDX.nextVal,
'홍길동',
'첫 게시글을 작성함',
'1111',
'192.1.1.1',
SYSDATE
);
-- 데이터 커밋
COMMIT;
Spring 프로젝트 환경설정 및 전체 파일 위치
- Mybatis 연동하기#환경설정 참고.
- pom.xml 파일의 Artifact ID, Name 수정
- web.xml, root-context.xml, servlet-context.xml 삭제
- WebInitializer, ServletContext, RootContext(Context_1, 2, 3), mybatis-config.xml, mapper.xml 가져오기
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
<?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.VisitDTO" alias="visit"/>
</typeAliases>
<mappers>
<mapper resource="config/mybatis/mapper/visit.xml" />
</mappers>
</configuration>
- 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="v">
<!-- 방명록 조회하기 -->
<select id="visit_list" resultType="visit">
SELECT * FROM VISIT ORDER BY IDX DESC
</select>
<!-- 특정 방명록 조회하기 -->
<select id="visit_one" parameterType="int" resultType="visit">
SELECT * FROM VISIT WHERE IDX=#{idx}
</select>
<!-- 새 방명록 추가하기 -->
<insert id="visit_insert" parameterType="visit">
INSERT INTO VISIT VALUES(
seq_visit_idx.nextVal,
#{name},
#{content},
#{pwd},
#{ip},
sysdate
)
</insert>
<!-- 방명록 삭제하기 -->
<delete id="visit_delete" parameterType="java.util.HashMap">
DELETE FROM VISIT WHERE IDX=#{idx} AND PWD=#{pwd}
</delete>
<!-- 방명록 수정하기 -->
<update id="visit_modify" parameterType="visit">
UPDATE VISIT
SET CONTENT = #{content},
PWD = #{pwd},
IP = #{ip},
REGDATE = sysdate
WHERE IDX=#{idx}
</update>
</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
- dto
- lombok 라이브러리의 getter와 setter 사용
package dto;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class VisitDTO {
private int idx;
private String name, content, pwd, ip, regdate;
}
- dao
package dao;
import java.util.HashMap;
import java.util.List;
import org.apache.ibatis.session.SqlSession;
import dto.VisitDTO;
public class VisitDAO {
SqlSession sqlSession;
public VisitDAO(SqlSession sqlSession) {
this.sqlSession = sqlSession;
}
// 방명록 전체 조회하기
public List<VisitDTO> selectList() {
List<VisitDTO> list = sqlSession.selectList("v.visit_list");
return list;
}
// 특정 방명록 조회하기
public VisitDTO selectOne(int idx) {
VisitDTO dto = sqlSession.selectOne("v.visit_one", idx);
return dto;
}
// 새 방명록 추가하기
public int insert(VisitDTO dto) {
int res = sqlSession.insert("v.visit_insert", dto);
return res;
}
// 방명록 삭제하기
public int delete(HashMap<String, Object> map) {
int res = sqlSession.delete("v.visit_delete", map);
return res;
}
// 방명록 수정하기
public int modify(VisitDTO dto) {
int res = sqlSession.update("v.visit_modify", dto);
return res;
}
}
- Context_3_dao
- DAO Bean 객체를 등록
package context;
import org.apache.ibatis.session.SqlSession;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import dao.VisitDAO;
@Configuration
public class Context_3_dao {
@Bean
public VisitDAO visit_daoBean(SqlSession sqlSession) {
return new VisitDAO(sqlSession);
}
}
Controller
- VisitController
- JSP 실습 때 Servlet이 하던 역할을 담당함
package com.nogroup.visit;
import java.util.HashMap;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import dao.VisitDAO;
import dto.VisitDTO;
import util.MyCommon;
@Controller
public class VisitController {
VisitDAO visit_dao;
public VisitController(VisitDAO visit_dao) {
this.visit_dao = visit_dao;
}
// 게시글 조회 페이지로 mapping
@RequestMapping(value= {"/", "visit_list"})
public String select(Model model) {
List<VisitDTO> list = visit_dao.selectList();
model.addAttribute("list", list);
return MyCommon.VIEW_PATH+"visit_list.jsp";
}
// 게시글 추가 페이지로 mapping
@RequestMapping(value= "insert_form")
public String insert_form() {
return MyCommon.VIEW_PATH+"insert_form.jsp";
}
// 게시글 추가
@RequestMapping(value= "insert")
public String insert(VisitDTO dto, HttpServletRequest request) {
// insert?name=이름&content=내용&pwd=비밀번호
String ip = request.getRemoteAddr();
dto.setIp(ip);
int res = visit_dao.insert(dto);
// sendRedirect("visit_list")
return "redirect:visit_list";
}
// 게시글 삭제 mapping
@RequestMapping(value="delete")
@ResponseBody
public String delete(int idx, String pwd) {
HashMap<String, Object> map = new HashMap<String, Object>();
map.put("idx", idx);
map.put("pwd", pwd);
int res = visit_dao.delete(map);
String result = "no";
if (res == 1) {
result = "yes";
}
String finRes = String.format("[{'res':'%s'}]", result);
// return 값을 callback 함수로 돌아감을 표시하는 @ResponseBody
return finRes;
}
// 게시글 수정 페이지로 이동
@RequestMapping(value="modify_form")
public String modify_form(Model model, int idx) {
VisitDTO dto = visit_dao.selectOne(idx);
model.addAttribute("dto", dto);
return MyCommon.VIEW_PATH+"visit_modify_form.jsp";
}
// 게시글 수정하기
@RequestMapping(value="modify")
public String modify(VisitDTO dto, HttpServletRequest request) {
String ip = request.getRemoteAddr();
dto.setIp(ip);
int res = visit_dao.modify(dto);
return "redirect:visit_list";
}
}
- ServletContext
- VisitController 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.visit.VisitController;
import dao.VisitDAO;
@Configuration
@EnableWebMvc
//@ComponentScan("com.nogroup.visit")
public class ServletContext implements WebMvcConfigurer{
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**").addResourceLocations("/resources/");
}
@Bean
public VisitController visitController(VisitDAO visit_dao) {
return new VisitController(visit_dao);
}
}
util 패키지
- src/main/resources에 util 패키지를 생성하고, 필요한 클래스를 추가한다.
- 자주 사용하는 상수 등을 등록한다.
- MyCommon 클래스
- JSP 경로를 저장하기 위해 사용
package util;
public class MyCommon {
public static String VIEW_PATH = "/WEB-INF/views/visit/";
}
JSP
- src/main/webapp/WEB-INF/views 폴더에 visit 하위 폴더 생성하기
- 앞으로 만들 jsp는 모두 해당 디렉터리에 생성한다.
- 방명록 조회 jsp
- Spring에서는 jsp에서 jsp로 이동하지 못한다.
- callback 함수에서 eval() 대신
(new Function('return'+data))()을 사용한 이유- eval()은 문자로 표현된 Javascript 코드를 실행하는 함수이므로, 악영향을 줄 수 있는 문자열을 실행 시 프로그램에 문제가 발생할 수 있다.
- 또한 eval()이 호출된 위치의 스코프를 제 3자가 볼 수 있으며, Function()으로 실행할 수 없는 공격을 eval()로는 할 수 있기에 보안상의 이유로 function()을 사용하는 것이 좋다.
<%@ 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>
<link rel="stylesheet" href="${pageContext.request.contextPath}/resources/css/visit.css">
<script src="${pageContext.request.contextPath}/resources/js/HttpRequest.js"></script>
<script>
function del(f) {
let idx = f.idx.value;
let pwd = f.pwd.value.trim();
let ori_pwd = f.ori_pwd.value.trim();
if(pwd == '') {
alert('비밀번호를 입력하세요');
return;
}
if(pwd != ori_pwd) {
alert('비밀번호가 일치하지 않습니다');
return;
}
if(!confirm('삭제하시겠습니까?')) {
return;
}
let url = "delete";
let param = "idx="+idx+"&pwd="+encodeURIComponent(pwd);
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].res == "no") {
alert('삭제를 실패했습니다');
return;
}
alert('성공적으로 삭제했습니다');
location.href="visit_list";
}
}
function modify(f) {
let ori_pwd = f.ori_pwd.value.trim();
let pwd = f.pwd.value.trim();
if(pwd != ori_pwd) {
alert('비밀번호가 일치하지 않습니다');
return;
}
f.action = "modify_form";
f.method = "POST";
f.submit();
}
</script>
</head>
<body>
<div id="main_box">
<h1>방명록 리스트</h1>
<input type="button" value="글쓰기" onclick="location.href='insert_form'">
</div>
<c:forEach var="dto" items="${list}">
<div class="visit_box">
<div class="type_content"><pre>${dto.content}</pre></div>
<div class="type_name">작성자 : ${dto.name}(${dto.ip})</div>
<div class="type_regdate">작성일 : ${dto.regdate}</div>
<div>
<form>
<input type="hidden" name="idx" value="${dto.idx}">
<input type="hidden" name="ori_pwd" value="${dto.pwd}">
비밀번호 <input type="password" name="pwd">
<input type="button" value="수정" onclick="modify(this.form)">
<input type="button" value="삭제" onclick="del(this.form)">
</form>
</div>
</div>
</c:forEach>
</body>
</html>
- 방명록 추가 페이지
<%@ 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(f) {
let name = f.name.value;
let content = f.content.value;
let pwd = f.pwd.value;
if (name == '') {
alert('이름을 입력하세요');
return;
}
if (content == '') {
alert('내용을 한 글자 이상 입력하세요');
return;
}
if (pwd == '') {
alert('비밀번호를 입력하세요');
return;
}
f.action = "insert";
f.submit();
}
</script>
</head>
<body>
<form>
<table border="1" align="center">
<caption>::새 글 작성하기::</caption>
<tr>
<th>작성자</th>
<td><input name="name" style="width:250px;"></td>
</tr>
<tr>
<th>내용</th>
<td>
<textarea row="5" cols="50" name="content" style="resize:none; wrap:on"></textarea>
</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)">
<input type="button" value="목록으로" onclick="location.href='visit_list'">
</td>
</tr>
</table>
</form>
</body>
</html>
- 방명록 수정 페이지
<%@ 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 type="text/javascript">
function send(f) {
let content = f.content.value;
if (content == '') {
alert('내용을 한 글자 이상 입력하세요');
return;
}
f.action = "modify";
f.method = "POST";
f.submit();
}
</script>
</head>
<body>
<form>
<input type="hidden" name="idx" value="${dto.idx}">
<table border="1" align="center">
<caption>::방명록 수정하기::</caption>
<tr>
<th>작성자</th>
<td>${dto.name}</td>
</tr>
<tr>
<th>내용</th>
<td>
<textarea name="content" row="5" cols="50" style="resize:none;wrap=on">${dto.content}</textarea>
</td>
</tr>
<tr>
<th>비밀번호</th>
<td><input type="password" name="pwd" value="${dto.pwd}"></td>
</tr>
<tr>
<td colspan="2" align="center">
<input type="button" value="수정" onclick="send(this.form)">
<input type="button" value="취소" onclick="location.href='visit_list'">
</td>
</tr>
</table>
</form>
</body>
</html>
css
- src/main/webapp/resources 폴더에 css 하위 폴더를 생성하고 css 파일을 추가한다.
@charset "UTF-8";
*{margin:0; padding:0;}
#main_box{
width:330px;
margin:auto;
}
h1{
text-align:center;
margin-top:10px 0;
color:#0080ff;
text-shadow:2px 2px 2px black;
}
.visit_box{
margin:30px auto 0;
width:330px;
box-shadow:2px 2px 2px black;
border:1px solid blue;
}
.type_content{
min-heigth:100px;
height:auto;
background-color:#fcc;
}
.type_name{
background-color:#cfc;
}
.type_regdate{
background-color:#ccf;
}
완성된 모습
-
방명록 전체가 조회된다.
-
글쓰기 버튼을 누르면 새 글 작성 화면이 나오며, 새 글에 필요한 정보를 입력할 수 있다.
-
등록하기 버튼을 누르면 추가된 방명록을 조회 화면에서 볼 수 있다.
-
수정하려는 방명록에 맞는 비밀번호를 입력하면 방명록을 수정할 수 있다.
-
수정 버튼을 누르면 바뀐 내용이 반영된 모습을 확인할 수 있다.
-
방명록에 맞는 비밀번호를 입력하고 삭제를 누르면 방명록이 성공적으로 삭제되는 것을 볼 수 있다.