앨범 사이트 만들기
✒️ 2025-05-26 11:06 내용 수정
실습 목표
- Oracle 데이터베이스에 있는 Photo 테이블에 사진의 정보를 조회, 추가, 삭제, 다운로드한다.
- 데이터베이스에 사진 정보를 저장하고, 실제 사진을 서버에 전송하며, 서버에서 사진을 다운로드한다.
실습 흐름
- 데이터베이스에 테이블 및 시퀀스, 필요 시 샘플 데이터 추가
- 데이터베이스에 연결 : context.xml 파일, 라이브러리(JDBC)
- 테이블의 정보를 저장할 DTO 클래스 생성
- 데이터베이스에서 조회, 추가, 삭제를 수행하는 DAO 클래스 생성
- 사진을 등록, 삭제, 다운로드할 JSP, 사진 추가 형식의 JSP 생성
- DAO 클래스 객체를 생성하고, JSP와 연결할 Servlet 생성
- Servlet에서 코드 실행 시 결과 확인
DB에 테이블 추가
--시퀀스
CREATE SEQUENCE SEQ_PHOTO_IDX;
--테이블
CREATE TABLE PHOTO(
IDX NUMBER(3) PRIMARY KEY,
TITLE VARCHAR2(100),
FILENAME VARCHAR2(100),
PWD VARCHAR2(50),
IP VARCHAR2(20),
REGIDATE DATE
);
DB 연결
- context.xml
<?xml version="1.0" encoding="UTF-8"?>
<Context>
<Resource
auth="Container"
name="jdbc/oracle_test"
type="javax.sql.DataSource"
driverClassName="oracle.jdbc.driver.OracleDriver"
factory="org.apache.commons.dbcp.BasicDataSourceFactory"
url="jdbc:oracle:thin:@localhost:1521:xe"
username="계정명" password="비밀번호"
maxActive="20" maxIdle="10" maxWait="1"/>
</Context>
- 라이브러리
| 파일 |
|---|
| commons-collections-3.2.1.jar |
| commons-dbcp-1.2.2.jar |
| commons-pool-1.4.jar |
| ojdbc8-23.3.0.23.09.jar |
| cos.jar |
| lombok.jar |
- service용 DBService.java
package service;
import java.sql.Connection;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
public class DBConnection {
static DBConnection db = null;
DataSource ds;
// singleton pattern으로 생성
public static DBConnection getInstance() {
if(db == null) {
db = new DBConnection();
}
return db;
}
// 생성자에서 Context 객체와 DataSource 초기화
public DBConnection() {
try {
InitialContext ic = new InitialContext();
Context ctx = (Context)ic.lookup("java:comp/env");
ds = (DataSource)ctx.lookup("jdbc/oracle_test");
} catch (NamingException e) {
e.printStackTrace();
}
}
// 생성자에서 준비한 정보로 DB에 연결하여 Connection 객체 얻기
public Connection getConnection() {
Connection connec = null;
try {
connec = ds.getConnection();
} catch (Exception e) {
e.printStackTrace();
}
return connec;
}
}
Ajax
- HttpRequest.js
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
package dto;
public class PhotoDTO {
private int idx;
private String title;
private String filename;
private String pwd;
private String ip;
private String regidate;
public int getIdx() {
return idx;
}
public void setIdx(int idx) {
this.idx = idx;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getFilename() {
return filename;
}
public void setFilename(String filename) {
this.filename = filename;
}
public String getPwd() {
return pwd;
}
public void setPwd(String pwd) {
this.pwd = pwd;
}
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
public String getRegidate() {
return regidate;
}
public void setRegidate(String regidate) {
this.regidate = regidate;
}
}
- lombok을 사용하면 훨씬 간단해진다.
package dto;
import lombok.Data;
@Data
public class PhotoDTO {
private int idx;
private String title;
private String filename;
private String pwd;
private String ip;
private String date;
}
- dao
package dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
import dto.PhotoDTO;
import service.DBService;
public class PhotoDAO {
static PhotoDAO single = null;
// singleton pattern
public static PhotoDAO getInstance() {
if (single == null) {
single = new PhotoDAO();
}
return single;
}
// 사진 전체 조회
public List<PhotoDTO> selectList() {
List<PhotoDTO> list = new ArrayList<PhotoDTO>();
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
String sql = "SELECT * FROM PHOTO";
try {
// 1. Connection 객체를 얻어온다
con = DBService.getInstance().getConnection();
// 2. 명령 처리 객체 정보를 얻어온다
pstmt = con.prepareStatement(sql);
// 3. 결과행 처리 객체를 얻어온다
rs = pstmt.executeQuery();
while(rs.next()) {
PhotoDTO dto = new PhotoDTO();
dto.setIdx(rs.getInt("idx"));
dto.setTitle(rs.getString("title"));
dto.setFilename(rs.getString("filename"));
dto.setPwd(rs.getString("pwd"));
dto.setIp(rs.getString("ip"));
dto.setRegidate(rs.getString("regidate"));
list.add(dto);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if(rs != null) {
rs.close();
}
if(pstmt != null) {
pstmt.close();
}
if(con != null) {
con.close();
}
} catch (Exception e2) {
e2.printStackTrace();
}
}
return list;
}
// 사진 등록
public int insert(PhotoDTO dto) {
int res = 0;
Connection con = null;
PreparedStatement pstmt = null;
String sql = "INSERT INTO PHOTO VALUES(SEQ_PHOTO_IDX.nextVal, ?, ?, ?, ?, SYSDATE)";
try {
// 1. Connection 획득
con = DBService.getInstance().getConnection();
// 2. 명령 처리 객체 획득
pstmt = con.prepareStatement(sql);
// 3. pstmt에 parameter 채우기
pstmt.setString(1, dto.getTitle());
pstmt.setString(2, dto.getFilename());
pstmt.setString(3, dto.getPwd());
pstmt.setString(4, dto.getIp());
// 4. DB로 데이터 전송 (res : 처리된 행 수)
res = pstmt.executeUpdate();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if(pstmt != null) {
pstmt.close();
}
if(con != null) {
con.close();
}
} catch (Exception e2) {
e2.printStackTrace();
}
}
return res;
}
// 사진 제거
public int delete(int idx) {
int res = 0;
Connection con = null;
PreparedStatement pstmt = null;
String sql = "DELETE FROM PHOTO WHERE IDX=?";
try {
// 1. Connection 획득
con = DBService.getInstance().getConnection();
// 2. 명령 처리 객체 획득
pstmt = con.prepareStatement(sql);
// 3. pstmt에 parameter 채우기
pstmt.setInt(1, idx);
// 4. DB로 데이터 전송 (res : 처리된 행 수)
res = pstmt.executeUpdate();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if(pstmt != null) {
pstmt.close();
}
if(con != null) {
con.close();
}
} catch (Exception e2) {
e2.printStackTrace();
}
}
return res;
}
}
Servlet
- file list
package action;
import java.io.IOException;
import java.util.List;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import dao.PhotoDAO;
import dto.PhotoDTO;
@WebServlet("/list")
public class PhotoListAction extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// DAO로 DB에 접근해서 photo 전체 목록 조회
List<PhotoDTO> list = PhotoDAO.getInstance().selectList();
// list를 request 영역에 바인딩
request.setAttribute("list", list);
// request 영역에 바인딩 된 list를 어떤 jsp에서 사용할건지 포워딩
RequestDispatcher disp = request.getRequestDispatcher("photo_list.jsp");
disp.forward(request, response);
}
}
- file insert
package action;
import java.io.File;
import java.io.IOException;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.oreilly.servlet.MultipartRequest;
import com.oreilly.servlet.multipart.DefaultFileRenamePolicy;
import dao.PhotoDAO;
import dto.PhotoDTO;
@WebServlet("/insert")
public class PhotoInsertAction extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 경로 설정
String web_path = "/upload/";
ServletContext application = request.getServletContext();
String path = application.getRealPath(web_path);
System.out.println("real path : " + path);
// 파일 사이즈 설정 : 100mb
int max_size = 1024 * 1024 * 100;
// MultipartRequest 객체 생성
MultipartRequest mr = new MultipartRequest(request, path, max_size, "utf-8", new DefaultFileRenamePolicy());
// 파일 이름 설정
String fileName = "";
File f = mr.getFile("photo"); // 전달받은 파일 검색
if (f != null) {
fileName = f.getName();
}
String title = mr.getParameter("title");
String pwd = mr.getParameter("pwd");
String ip = request.getRemoteAddr();
// 객체에 정보 담기
PhotoDTO dto = new PhotoDTO();
dto.setTitle(title);
dto.setFilename(fileName);
dto.setPwd(pwd);
dto.setIp(ip);
int res = PhotoDAO.getInstance().insert(dto);
if (res > 0) {
response.sendRedirect("list");
}
}
}
- file delete
package action;
import java.io.File;
import java.io.IOException;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import dao.PhotoDAO;
@WebServlet("/delete")
public class PhotoDeleteAction extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// delete?idx=n&filename=n
request.setCharacterEncoding("utf-8");
int idx = Integer.parseInt(request.getParameter("idx"));
String fileName = request.getParameter("filename");
// 서버의 /upload/ 경로 가져오기
String web_path = "/upload/";
ServletContext application = request.getServletContext();
String path = application.getRealPath(web_path);
// DB에서 파일 정보 삭제
int res = PhotoDAO.getInstance().delete(idx);
String param = "no";
if(res > 0) {
File f = new File(path, fileName);
if (f.exists()) {
f.delete();
}
param = "yes";
}
// 결과값인 param을 json 구조로 포장하여 출력 텍스트 작성
String resultStr = String.format("[{'param':'%s'}]", param);
response.getWriter().print(resultStr);
}
}
- file download
package action;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URLEncoder;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/download")
public class PhotoDownloadAction extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 서버에 파일이 저장된 경로 가져오기
String dir = request.getParameter("dir");
String fullPath = request.getServletContext().getRealPath(dir);
String fileName = "";
fileName = request.getParameter("filename");
String fullPathName = String.format("%s%s", fullPath, fileName);
// 파일 가져오기
File file = new File(fullPathName);
byte[] b = new byte[1024*1024*100]; // 파일 최대 한도만큼 바이트 배열 설정
// 사용자 브라우저 타입 얻어오기
String strAgent = request.getHeader("User-Agent");
System.out.println("browser : " + strAgent);
String userCharset = request.getCharacterEncoding();
if (userCharset == null) {
userCharset = "utf-8";
}
String value = "";
// IE인 경우
// IE 지원이 종료되어 향후엔 IE 부분이 필요 없을지도 모른다.
if (strAgent.indexOf("MSIE") > -1) {
// IE 5.5 인 경우
if (strAgent.indexOf("MSIE 5.5") > -1) {
value = "filename="+fileName;
} else if (strAgent.indexOf("MSIE 7.0") > -1) { // IE 7.0인 경우
// 인코딩 타입 비교
if (userCharset.equalsIgnoreCase("UTF-8")) {
fileName = URLEncoder.encode(fileName, userCharset);
fileName = fileName.replaceAll("\\+", " ");
value = "attachment; filename=\""+fileName+"\"";
} else {
value = "attachment; filename=" + new String(fileName.getBytes(userCharset), "ISO-8859-1");
}
} else { // IE 8.0 이상인 경우 두 번 호출됨
// 인코딩 타입 비교
if (userCharset.equalsIgnoreCase("UTF-8")) {
fileName = URLEncoder.encode(fileName, userCharset);
fileName = fileName.replaceAll("\\+", " ");
value = "attachment; filename=\""+fileName+"\"";
} else {
value = "attachment; filename" + new String(fileName.getBytes(userCharset), "ISO-8859-1");
}
} // IE 확인 종료
} else if (strAgent.indexOf("Firefox") > -1) { // Firefox인 경우
// Firefox는 공백 문자 이후가 인식되지 않음
// 다만 기타 브라우저랑 코드는 같은데..
value = "attachment; filename=" + new String(fileName.getBytes(), "ISO-8859-1");
} else { // 그외 브라우저
value = "attachment; filename=" + new String(fileName.getBytes(), "ISO-8859-1");
}
// 브라우저가 캐싱하지 않도록 설정
response.setContentType("Pragma : no-cache");
// 전송 데이터가 stream 처리되도록 설정. 웹 상 전송 문자셋은 8859_1
response.setContentType("application/octect-stream;charset=8859_1;");
// 데이터 형식 성향 설정(attachment : 첨부파일)
response.setHeader("Content-Disposition", value);
// 내용물 인코딩 방식 결정
response.setHeader("Content-Transfer-Encoding", "binary");
// 파일 내보내기 - 정확하게는 이미지가 아닌 모든 파일임
if (file.isFile()) {
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream());
int i = 0;
try {
while((i=bis.read(b)) != -1) {
bos.write(b, 0, i);
}
} catch (Exception e) {
} finally {
if (bos != null) bos.close();
if (bis != null) bis.close();
}
}
}
}
JSP
- 앨범 리스트 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>
<link rel="stylesheet" href="css/photo.css">
<script src="js/HttpRequest.js"></script>
<script type="text/javascript">
function del(f) {
var idx = f.idx.value.trim();
var pwd = f.pwd.value.trim();
var pwd2 = f.pwd2.value.trim();
var filename = f.filename.value.trim();
// 유효성 검사
if (pwd2 == '') {
alert('비밀번호를 입력해주세요');
return;
}
if (pwd != pwd2) {
alert('비밀번호가 일치하지 않습니다');
return;
}
// 삭제 여부 체크
if(!confirm('삭제하시겠습니까?')) {
return;
}
var url = "delete";
var param = "idx="+idx+"&filename="+filename;
sendRequest(url, param, resultFn, "POST");
}
function resultFn() {
if(xhr.readyState == 4 && xhr.status == 200) {
var data = xhr.responseText;
var json = eval(data);
if(json[0].param == "no") {
alert('제거에 실패했습니다');
} else {
alert('성공적으로 제거했습니다');
}
location.href = "list";
}
}
function download(filename){
location.href = "download?dir=/upload/&filename="+encodeURIComponent(filename);
}
</script>
</head>
<body>
<div id="main_box">
<h1> ::: Photo Gallery ::: </h1>
<div align="center" style="margin:10px;">
<input type="button" value="사진등록" onclick="location.href='photo_insert.jsp'">
</div>
<div id="photo_content">
<c:forEach var="dto" items="${list}">
<div class="photo_box">
<img src="upload/${dto.filename}">
<div class="title">
<h2>${dto.title}</h2>
</div>
<form>
<input type="hidden" name="idx" value="${dto.idx}">
<input type="hidden" name="pwd" value="${dto.pwd}">
<input type="hidden" name="filename" value="${dto.filename}">
<div>
<input type="password" name="pwd2" size="5">
<input type="button" value="삭제" onclick="del(this.form)">
<input type="button" value="다운로드" onclick="download('${dto.filename}')">
</div>
</form>
</div>
</c:forEach>
</div>
</div>
</body>
</html>
- 사진 등록 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 type="text/javascript">
function send(f) {
var title = f.title.value;
var pwd = f.pwd.value;
var photo = f.photo.value;
// 유효성 검사
if (title == '') {
alert('제목을 입력하세요');
return;
}
if (pwd == '') {
alert('비밀번호를 입력하세요');
return;
}
if (photo == '') {
alert('파일을 업로드하세요');
return;
}
f.submit();
}
</script>
</head>
<body>
<form action="insert" method="POST" enctype="multipart/form-data">
<table border="1" align="center">
<caption>:: 사진 등록하기 ::</caption>
<tr>
<th>제목</th>
<td><input name="title"></td>
</tr>
<tr>
<th>비밀번호</th>
<td><input name="pwd" type="password"></td>
</tr>
<tr>
<th>등록할 사진</th>
<td><input name="photo" type="file"></td>
</tr>
<tr>
<td colspan="2" align="center">
<input type="button" value="등록하기" onclick="send(this.form)">
<input type="button" value="목록으로 돌아가기" onclick="location.href='list'">
</td>
</tr>
</table>
</form>
</body>
</html>
- CSS
@charset "UTF-8";
*{margin:0; padding:0;}
#main_box{width: 900px; margin:0 auto;}
#main_box h1{
text-align: center;
text-shadow: 3px 3px 5px gray;
color: ghostwhite;
}
#photo_content{
margin:20px auto; padding:10px;
width: 710px; height:665px;
border:1px solid blue;
overflow: auto;
box-shadow: 5px 5px 10px gray;
}
.photo_box{
margin:10px;
width: 200px; height: auto;
border: 1px solid green;
float: left;
}
.photo_box h2{text-align: center;}
.photo_box img{
display: block;
margin:auto;
width: 98%; height: 150px;
}
완성된 모습
-
ListServlet에서 실행하면 앨범 사이트 화면이 뜬다. 아직 아무 사진도 업로드하지 않아 비어있다.
-
사진 등록 버튼을 눌러 제목과 비밀번호, 파일을 선택해 업로드한다.
-
파일이 정상적으로 업로드되어 사이트에 뜨는 것을 확인할 수 있다.
-
서버(현재는 로컬 컴퓨터)의 실제 파일 경로에도 해당 파일이 있는 것을 확인할 수 있다.
-
비밀번호 없이 삭제 버튼을 누르면 비밀번호 입력을 요구한다.
-
비밀번호를 입력하고, 일치하는 비밀번호라면 정상적으로 삭제되었다는 메시지가 뜬다.
-
이미지가 삭제되어 화면에서 사라진다.
-
다운로드 버튼을 누르면 파일을 다운로드할 수 있다.
-
파일을 정상적으로 다운 받을 수 있다.