JAVA & JSP
CREATE TABLE gallery(
num NUMBER PRIMARY KEY,
title VARCHAR2(100) NOT NULL, -- 제목
writer VARCHAR2(20) NOT NULL, -- 업로더
content CLOB, -- 내용 설명
createdAt DATE DEFAULT SYSDATE
);
-- gallery 테이블은 board_seq 를 공유
CREATE TABLE gallery_image(
num NUMBER PRIMARY KEY,
galleryNum NUMBER REFERENCES gallery(num),
saveFileName VARCHAR2(100) NOT NULL, --업로드된 사진의 저장된 파일명
-- 만일 사진이 여러개라면 동일한 gallery_num 으로 여러개의 row 가 저장된다
createdAt DATE DEFAULT SYSDATE
);
CREATE SEQUENCE gallery_image_seq;;
<li class="nav-item">
<a class="nav-link <%=thisPage.equals("gallery") ? "active":""%>"
href="${pageContext.request.contextPath }/gallery/list.jsp">Gallery/a>
</li>
"/gallery/list.jsp"
<jsp:include page="/WEB-INF/include/resource.jsp"></jsp:include>
<jsp:include page="/WEB-INF/include/navbar.jsp">
<jsp:param value="gallery" name="thisPage"/>
</jsp:include>
<div class="container">
<h1>Gallery 목록</h1>
</div>
public class GalleryImageDto {
private int num;
// gallery 테이블의 PK 참조
private int galleryNum;
private String saveFileName;
private String createdAt;
}
public class GalleryDto {
private int num;
private String title;
private String writer;
private String content;
private String createdAt;
// 하나의 갤러리 글에 업로드된 사진이 여러 개일 수도 있다
// 사진 하나당 GalleryImageDto 객체 하나
// 사진이 여러 개 이니깐 List<GalleryImageDto> type 의 필드가 필요하다
private List<GalleryImageDto> imageList;
// setter & getter
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getWriter() {
return writer;
}
public void setWriter(String writer) {
this.writer = writer;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getCreatedAt() {
return createdAt;
}
public void setCreatedAt(String createdAt) {
this.createdAt = createdAt;
}
public List<GalleryImageDto> getImageList() {
return imageList;
}
public void setImageList(List<GalleryImageDto> imageList) {
this.imageList = imageList;
}
}
public class GalleryDao {
private static GalleryDao dao;
private GalleryDao() {}
public static GalleryDao getInstance() {
if (dao == null) {
dao = new GalleryDao();
}
return dao;
}
}
public List<GalleryDto> getListWithImages() {
List<GalleryDto> list = new ArrayList<>();
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
String sql = """
SELECT num, title, writer, TO_CHAR(createdAt, 'YYYY-MM-DD HH24:MI:SS') AS createdAt
FROM gallery
ORDER BY num DESC
""";
try {
conn = new DbcpBean().getConn();
pstmt = conn.prepareStatement(sql);
rs = pstmt.executeQuery();
while (rs.next()) {
GalleryDto dto = new GalleryDto();
int galleryNum = rs.getInt("num");
dto.setNum(galleryNum);
dto.setTitle(rs.getString("title"));
dto.setWriter(rs.getString("writer"));
dto.setCreatedAt(rs.getString("createdAt")); // String 처리
// 이미지 목록도 dao 호출로 포함
List<GalleryImageDto> imageList = getImageList(galleryNum);
dto.setImageList(imageList);
list.add(dto);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
if (rs != null) rs.close();
if (pstmt != null) pstmt.close();
if (conn != null) conn.close();
} catch (Exception e) {}
}
return list;
}
public boolean insert(GalleryDto dto) {
Connection conn = null;
PreparedStatement pstmt = null;
boolean isSuccess = false;
String sql = """
INSERT INTO gallery (num, title, writer, content)
VALUES (?, ?, ?, ?)
""";
try {
conn = new DbcpBean().getConn();
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, dto.getNum()); // 시퀀스를 dto에 미리 넣어서 전달
pstmt.setString(2, dto.getTitle());
pstmt.setString(3, dto.getWriter());
pstmt.setString(4, dto.getContent());
int rowCount = pstmt.executeUpdate();
isSuccess = rowCount > 0;
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
if (pstmt != null) pstmt.close();
if (conn != null) conn.close();
} catch (Exception e) {}
}
return isSuccess;
}
public boolean insertImage(GalleryImageDto dto) {
Connection conn = null;
PreparedStatement pstmt = null;
boolean isSuccess = false;
String sql = """
INSERT INTO gallery_image (num, galleryNum, saveFileName)
VALUES (gallery_image_seq.NEXTVAL, ?, ?)
""";
try {
conn = new DbcpBean().getConn();
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, dto.getGalleryNum());
pstmt.setString(2, dto.getSaveFileName());
int rowCount = pstmt.executeUpdate();
isSuccess = rowCount > 0;
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
if (pstmt != null) pstmt.close();
if (conn != null) conn.close();
} catch (Exception e) {}
}
return isSuccess;
}
public List<GalleryDto> getList() {
List<GalleryDto> list = new ArrayList<>();
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
String sql = """
SELECT num, title, writer, TO_CHAR(createdAt, 'YYYY-MM-DD HH24:MI:SS') AS createdAt
FROM gallery
ORDER BY num DESC
""";
try {
conn = new DbcpBean().getConn();
pstmt = conn.prepareStatement(sql);
rs = pstmt.executeQuery();
while (rs.next()) {
GalleryDto dto = new GalleryDto();
dto.setNum(rs.getInt("num"));
dto.setTitle(rs.getString("title"));
dto.setWriter(rs.getString("writer"));
dto.setCreatedAt(rs.getString("createdAt")); // string 처리
list.add(dto);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
if (rs != null) rs.close();
if (pstmt != null) pstmt.close();
if (conn != null) conn.close();
} catch (Exception e) {}
}
return list;
}
public GalleryDto getData(int num) {
GalleryDto dto = null;
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
String sql = """
SELECT num, title, writer, content, TO_CHAR(createdAt, 'YYYY-MM-DD HH24:MI:SS') AS createdAt
FROM gallery
WHERE num = ?
""";
try {
conn = new DbcpBean().getConn();
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, num);
rs = pstmt.executeQuery();
if (rs.next()) {
dto = new GalleryDto();
dto.setNum(rs.getInt("num"));
dto.setTitle(rs.getString("title"));
dto.setWriter(rs.getString("writer"));
dto.setContent(rs.getString("content"));
dto.setCreatedAt(rs.getString("createdAt")); // string 처리
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
if (rs != null) rs.close();
if (pstmt != null) pstmt.close();
if (conn != null) conn.close();
} catch (Exception e) {}
}
return dto;
}
public List<GalleryImageDto> getImageList(int galleryNum) {
List<GalleryImageDto> list = new ArrayList<>();
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
String sql = """
SELECT num, saveFileName, TO_CHAR(createdAt, 'YYYY-MM-DD HH24:MI:SS') AS createdAt
FROM gallery_image
WHERE galleryNum = ?
ORDER BY num ASC
""";
try {
conn = new DbcpBean().getConn();
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, galleryNum);
rs = pstmt.executeQuery();
while (rs.next()) {
GalleryImageDto dto = new GalleryImageDto();
dto.setNum(rs.getInt("num"));
dto.setGalleryNum(galleryNum);
dto.setSaveFileName(rs.getString("saveFileName"));
dto.setCreatedAt(rs.getString("createdAt")); // string 처리
list.add(dto);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
if (rs != null) rs.close();
if (pstmt != null) pstmt.close();
if (conn != null) conn.close();
} catch (Exception e) {}
}
return list;
}
public int getSequence() {
int num = 0;
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = new DbcpBean().getConn();
String sql = """
SELECT board_seq.NEXTVAL AS num FROM dual
""";
pstmt = conn.prepareStatement(sql);
rs = pstmt.executeQuery();
if (rs.next()) {
num = rs.getInt("num");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (rs != null)
rs.close();
if (pstmt != null)
pstmt.close();
if (conn != null)
conn.close();
} catch (Exception e) {}
}
return num;
}
<a class="btn btn-outline-primary btn-sm mt-2" href="new-form.jsp">
<i class="bi bi-image"></i>
<i class="bi bi-plus"></i>
<span class="visually-hidden">겔러리 새글 작성</span>
</a>
.drop-zone {
border: 2px dashed #0d6efd;
cursor: pointer;
border-radius: 10px;
background-color: #f8f9fa;
display: flex;
align-items: center;
justify-content: center;
height: 200px;
}
.drop-zone.dragover {
background-color: #e9ecef;
}
.preview img {
max-width: 120px;
margin: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
.preview-item {
position: relative;
display: inline-block;
}
.remove-btn {
position: absolute;
top: 11px;
right: 11px;
background-color: #ffffffcc; /* 반투명 흰색 */
color: #333;
border: none;
border-radius: 4px;
width: 20px;
height: 20px;
font-size: 16px;
cursor: pointer;
text-align: center;
transition: background-color 0.2s, color 0.2s;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
display: flex;
align-items: center;
justify-content: center;
}
.remove-btn:hover {
background-color: #dc3545;
color: white;
}
<jsp:include page="/WEB-INF/include/navbar.jsp">
<jsp:param name="thisPage" value="gallery"/>
</jsp:include>
<div class="container mt-4">
<h2><i class="bi bi-image"></i> 새 갤러리 작성</h2>
<form action="${pageContext.request.contextPath }/gallery/save" method="post" enctype="multipart/form-data">
<div class="mb-3">
<label for="title" class="form-label">제목</label>
<input type="text" class="form-control" id="title" name="title" required>
</div>
<div class="mb-3">
<label for="content" class="form-label">내용</label>
<textarea class="form-control" id="content" name="content" rows="6" required></textarea>
</div>
<div class="mb-3">
<label class="form-label">이미지 업로드 (Drag & Drop 또는 클릭)</label>
<div class="drop-zone" id="dropZone">
이곳에 이미지를 끌어다 놓거나 클릭하여 선택하세요.
<input type="file" name="images" id="fileInput" multiple hidden>
</div>
<div class="preview d-flex flex-wrap mt-3" id="preview"></div>
</div>
<button type="submit" class="btn btn-primary">등록</button>
</form>
</div>
const dropZone = document.querySelector("#dropZone");
const fileInput = document.querySelector("#fileInput");
const preview = document.querySelector("#preview");
let selectedFiles = [];
// drop zone 을 클릭했을 때 input type="file" 을 강제 클릭해서 파일 선택창이 열리도록 한다
dropZone.addEventListener("click", () => fileInput.click());
// drop zone 에 마우스가 드레그래서 들어갔을 때
dropZone.addEventListener("dragover", (e) => {
e.preventDefault();
// dragover 클래스를 추가해서 회색으로 변경되도록 한다
dropZone.classList.add("dragover");
});
// drop zone 에 마우스가 드래그해서 나갔을 때
dropZone.addEventListener("dragleave", () => {
// dragover 클래스를 제거해서 색상이 원래 색으로 돌아가도록 한다
dropZone.classList.remove("dragover");
});
// 파일을 끌어다가 drop 했을 때
dropZone.addEventListener("drop", (e) => {
e.preventDefault();
dropZone.classList.remove("dragover");
// 선택된 파일 객체가 들어있는 배열을 기능을 모두 쓸 수 있는 배열로 변경한 다음
const files = Array.from(e.dataTransfer.files);
// 기존에 선택한 파일이 들어있는 배열에 합친다
selectedFiles = [...selectedFiles, ...files]; // concat 과 동일한 동작
// preview 를 업데이트 한다 (미리보기 가능)
updatePreview();
});
// drap drop 이 아니고 파일을 직접 선택했을 때 input type="file" 에 change 이벤트가 발생한다
fileInput.addEventListener("change", () => {
// 위와 동일한 동작을 한다
const files = Array.from(fileInput.files);
selectedFiles = selectedFiles.concat(files);
updatePreview();
});
// preview 을 업데이트 하는 함수 (내부에서 await 이 있는 비동기 함수이기 때문에 async 키워드를 붙여서 만들었다)
async function updatePreview() {
// 이미 출력된 이미지를 모두 없애고
preview.innerHTML = "";
// input type="file" 에 value 를 넣어줄 정보를 구성하기 위한 객체
const dataTransfer = new DataTransfer();
// 반복문 돌면서 선택된 파일 개체를 순서대로 참조하면서
for (let i = 0; i < selectedFiles.length; i++) {
// i 번째 파일 객체를 불러와서
const file = selectedFiles[i];
// 만일 이미지 파일이 아니라면 continue (for 문을 계속 실행한다 continue 아래의 코드를 실행하지 않고)
if (!file.type.startsWith("image/")) continue;
try {
// 선택한 이미지 파일을 읽어 들여서 결과를 얻어낸다
const imageUrl = await readFileAsDataURL(file);
const container = document.createElement("div");
container.classList.add("preview-item");
const img = document.createElement("img");
img.setAttribute("src", imageUrl);
const btn = document.createElement("button");
btn.classList.add("remove-btn");
btn.innerText = "×";
btn.addEventListener("click", () => {
// selectedFiles 배열에서 i 번째 방으로 부터 1개의 item 삭제
selectedFiles.splice(i, 1);
// 삭제 후 preview 가 다시 update 되도록 함수를 다시 호출해 준다
updatePreview();
});
container.insertAdjacentElement("beforeend", img);
container.insertAdjacentElement("beforeend", btn);
preview.insertAdjacentElement("beforeend", container);
// DataTransfer 객체에 파일 객체를 추가
dataTransfer.items.add(file);
} catch (err) {
console.error("이미지 로딩 실패:", err);
}
}
// 선택한 파일(이미지 파일만 선별) 객체가 들어있는 배열을 input type="file" 의 value 로 넣어준다
fileInput.files = dataTransfer.files;
}
// 전달된 파일을 읽어들이고 Promise 객체를 리턴해주는 함수
function readFileAsDataURL(file) {
return new Promise((resolve, reject) => {
// FileReader 객체를 이용해서 파일을 읽어들이고
const reader = new FileReader();
// 모두 다 읽었을 때 실행할 함수 등록
reader.onload = () => {
// 읽은 data url 문자열을 resolve() 함수를 호출하면서 전달한다 (Promise 가 해결된다)
resolve(reader.result);
};
reader.readAsDataURL(file);
});
}
@WebServlet("/gallery/save")
@MultipartConfig(
fileSizeThreshold = 1024 * 1024 * 10,
maxFileSize = 1024 * 1024 * 50,
maxRequestSize = 1024 * 1024 * 100
)
public class GallerySaveServlet extends HttpServlet {
private String fileLocation;
@Override
public void init() throws ServletException {
ServletContext context = getServletContext();
fileLocation = context.getInitParameter("fileLocation");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
//글 작성자는 세션 객체로 부터 얻어낸다.
String writer=(String)req.getSession().getAttribute("userName");
String title = req.getParameter("title");
String content = req.getParameter("content");
// DAO 인스턴스 준비
GalleryDao dao = GalleryDao.getInstance();
// 시퀀스 미리 발급
int galleryNum = dao.getSequence();
// gallery 테이블에 저장
GalleryDto dto = new GalleryDto();
dto.setNum(galleryNum);
dto.setTitle(title);
dto.setWriter(writer);
dto.setContent(content);
dao.insert(dto);
// 파일이 여러 개이기 때문에 getParts() 메소드를 호출해서 리턴되는 Part 목록을 사용한다
Collection<Part> parts = req.getParts();
// 이미지 저장
for (Part part : req.getParts()) {
/*
* <input type="file" name="images"> 의 name 속성의 값이 images 이기 때문에
* if 문에서 확인한다
* part.getName() 메소드가 리턴하는 문자열을 확인하면 어떤 input type="file" 요소의 데이터인지
* 구별할 수 있다
* */
if (part.getName().equals("images") && part.getSize() > 0) {
String orgFileName = part.getSubmittedFileName();
String uuid = UUID.randomUUID().toString();
String saveFileName = uuid + "_" + orgFileName;
String filePath = fileLocation + "/" + saveFileName;
try (InputStream is = part.getInputStream()) {
Files.copy(is, Paths.get(filePath));
}
// gallery_image 테이블에 저장
GalleryImageDto imageDto = new GalleryImageDto();
imageDto.setGalleryNum(galleryNum);
imageDto.setSaveFileName(saveFileName);
dao.insertImage(imageDto);
}
}
// 성공 후 목록으로 리디렉션
resp.sendRedirect(req.getContextPath() + "/gallery/list.jsp");
}
}
<%
List<GalleryDto> list = GalleryDao.getInstance().getListWithImages();
%>
<div class="row">
<% for (GalleryDto tmp : list) {
// 0번째 대표 이미지의 저장된 파일명
String name = tmp.getImageList().get(0).getSaveFileName();
// 전체 이미지의 갯수
int count = tmp.getImageList().size();
%>
<div class="col-sm-6 col-md-4 mb-4">
<div class="card">
<img class="card-img-top" src="${pageContext.request.contextPath }/upload/<%=name %>" />
<div class="card-body">
<h4><%=tmp.getTitle() %></h4>
<p><%=tmp.getContent() %></p>
<a class="btn btn-sm btn-success" href="view.jsp?num=<%=tmp.getNum()%>">
<%=count %> 개의 이미지
</a>
</div>
</div>
</div>
<% } %>
</div>