50일차 내용 정리

채공부·2025년 8월 1일

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;;
  • navbar 에 코드 추가
<li class="nav-item">
	<a class="nav-link <%=thisPage.equals("gallery") ? "active":""%>" 
		href="${pageContext.request.contextPath }/gallery/list.jsp">Gallery/a>
</li>
  • SecurityFilter 에 화이트 리스트 추가
"/gallery/list.jsp"
  • webapp 폴더 안에 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>
  • GalleryImageDto 생성
public class GalleryImageDto {
	private int num;
	// gallery 테이블의 PK 참조
	private int galleryNum;
	private String saveFileName;
	private String createdAt;
}
  • GalleryDto 생성
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;
	}
}
  • GalleryDao 생성
public class GalleryDao {
	private static GalleryDao dao;

    private GalleryDao() {}

    public static GalleryDao getInstance() {
        if (dao == null) {
            dao = new GalleryDao();
        }
        return dao;
    }
}
  • 이미지 목록도 포함하여 gallery 목록 반환 메소드
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;
}
  • list.jsp 코드 추가
<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>
  • new-form.jsp 생성
.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);
    });
}
  • GallerySaveServlet 생성
@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.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>
profile
학원 공부 내용 정리

0개의 댓글