본문 바로가기
기초 및 언어/▶ Java&JSP

25. JSP_게시판

by 류딩이 2025. 9. 16.

✅ 작성순서

  1. DB 테이블/시퀀스 설계 (board 테이블, seq)
  2. DAO 클래스 작성 (insertBoard, getArticles, getArticle, updateBoard, deleteBoard 등)
  3. 화면 JSP 작성 (insertForm.jsp, list.jsp, detail.jsp 등)
  4. Proc JSP 작성 (insertProc.jsp, updateProc.jsp, deleteProc.jsp)
  5. 흐름 연결 & 테스트

글목록 조회 결과화면

 

 

🌈게시글 IP란에 나의 IP를 넣는방법 

상단메뉴 Run > Run Configuration > Tomcat Server선택 > Aguments > VM aguments 맨 아래칸에 작성

 

-Djava.net.preferIPv4Stack=true

 

 

 

 

(1) SQL Table 생성 

더보기
drop table board;

create table board( 
	num number not null primary key,
	writer varchar2(10) not null,
	email varchar2(10),
	subject varchar2(20) not null,
	passwd varchar2(12) not null,
	reg_date date not null,
	readcount int default 0,
	ref number not null,
	re_step number not null,
	re_level number not null,
	content varchar2(20) not null,
	ip varchar2(15) not null
);

drop sequence board_seq;
create sequence board_seq
  increment by 1
  start with 1
  minvalue 1
  maxvalue 10000
  nocache;
  

	
insert into board(num,writer,email,subject,passwd,reg_date,ref,re_step,re_level,content,ip)
values(board_seq.nextval,'a1','a1','a1','1234','2023-1-1',1,0,0,'내용1','127.0.0.1');


insert into board(num,writer,email,subject,passwd,reg_date,ref,re_step,re_level,content,ip)
values(board_seq.nextval,'a2','a2','a2','1234','2023-1-1',2,0,0,'내용2','127.0.0.1');

insert into board(num,writer,email,subject,passwd,reg_date,ref,re_step,re_level,content,ip)
values(board_seq.nextval,'a21','a21','a21','1234','2023-1-1',2,4,1,'내용21','127.0.0.1');

insert into board(num,writer,email,subject,passwd,reg_date,ref,re_step,re_level,content,ip)
values(board_seq.nextval,'a22','a22','a22','1234','2023-1-1',2,1,1,'내용22','127.0.0.1');

insert into board(num,writer,email,subject,passwd,reg_date,ref,re_step,re_level,content,ip)
values(board_seq.nextval,'a221','a221','a221','1234','2023-1-1',2,3,2,'내용221','127.0.0.1');

insert into board(num,writer,email,subject,passwd,reg_date,ref,re_step,re_level,content,ip)
values(board_seq.nextval,'a222','a222','a222','1234','2023-1-1',2,2,2,'내용222','127.0.0.1');



insert into board(num,writer,email,subject,passwd,reg_date,ref,re_step,re_level,content,ip)
values(board_seq.nextval,'a3','a3','a3','1234','2023-1-1',7,0,0,'내용3','127.0.0.1');

insert into board(num,writer,email,subject,passwd,reg_date,ref,re_step,re_level,content,ip)
values(board_seq.nextval,'a31','a31','a31','1234','2023-1-1',7,6,1,'내용31','127.0.0.1');

insert into board(num,writer,email,subject,passwd,reg_date,ref,re_step,re_level,content,ip)
values(board_seq.nextval,'a311','a311','a311','1234','2023-1-1',7,7,2,'내용311','127.0.0.1');

insert into board(num,writer,email,subject,passwd,reg_date,ref,re_step,re_level,content,ip)
values(board_seq.nextval,'a32','a32','a32','1234','2023-1-1',7,2,1,'내용32','127.0.0.1');

insert into board(num,writer,email,subject,passwd,reg_date,ref,re_step,re_level,content,ip)
values(board_seq.nextval,'a321','a321','a321','1234','2023-1-1',7,5,2,'내용321','127.0.0.1');

insert into board(num,writer,email,subject,passwd,reg_date,ref,re_step,re_level,content,ip)
values(board_seq.nextval,'a322','a322','a322','1234','2023-1-1',7,3,2,'내용322','127.0.0.1');

insert into board(num,writer,email,subject,passwd,reg_date,ref,re_step,re_level,content,ip)
values(board_seq.nextval,'a3221','a3221','a3221','1234','2023-1-1',7,4,3,'내용3221','127.0.0.1');

insert into board(num,writer,email,subject,passwd,reg_date,ref,re_step,re_level,content,ip)
values(board_seq.nextval,'a33','a33','a33','1234','2023-1-1',7,1,1,'내용33','127.0.0.1');


commit;

(2). BoardDao 계정연결 및 싱글톤패턴 생성코드 보기

더보기
public class BoardDao {
	String driver = "oracle.jdbc.driver.OracleDriver";
    String url = "jdbc:oracle:thin:@localhost:1521:orcl";
    String id = "sqlid";
    String pw = "sqlpw";
    Connection conn = null;
    PreparedStatement ps = null;
    ResultSet rs = null;
    
    // 🌈싱글톤패턴으로 객체생성(객체를 한번만 생성하기)
    private static BoardDao instance = new BoardDao();
    
    public static BoardDao getInstance() {
    	return instance;
    }// getInstance
    
    // 드라이버 접속
    private BoardDao() {
    	try {
			Class.forName(driver);
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
    }
    
    // 계정연결
    public void getConnection() {
    	try {
			conn = DriverManager.getConnection(url, id, pw);
		} catch (SQLException e) {
			e.printStackTrace();
		}
    }

 

 

🌈 싱글톤패턴

// 싱글톤패턴으로 객체생성(객체를 한번만 생성하기)
private static BoardDao instance = new BoardDao();
  • 싱글톤 패턴: 프로그램 전체에서 BoardDao 객체를 딱 하나만 만들고 공유.
  • new BoardDao()를 private static으로 선언 → 클래스가 로딩될 때 단 한 번 객체를 생성해둔다.

👉 리소스 절약 + 일관성 + 유지보수성 때문에 DAO에서 싱글톤을 많이 씀.

 

 

📌 <jsp:useBean>이랑 뭐가 다르냐?

더보기
  • JSP 페이지가 실행될 때마다 필요할 때 new 해서 객체 생성
  • JSP가 다르면 DAO 객체도 새로 만들어질 수 있음
  • 기본적으로 싱글톤 보장 안 됨

👉  Bean (BoardBean, MovieBean 등 데이터 담는 클래스): <jsp:useBean>으로 사용


1. 🌈게시판 만들기 - list.jsp 글목록 조회

 

1-1. BoardDao.getArticles (글 목록조회)

더보기
public ArrayList<BoardBean> getArticles(int start, int end) {
    ArrayList<BoardBean> list = new ArrayList<BoardBean>();
    getConnection();
    String sql = "select num, writer, email, subject, passwd, reg_date, readcount, ref, re_step, re_level, content, ip " ;
    sql += "from (select rownum as rank, num, writer, email, subject, passwd, reg_date, readcount, ref, re_step, re_level, content, ip ";
    sql += "from (select num, writer, email, subject, passwd, reg_date, readcount, ref, re_step, re_level, content, ip ";
    sql += "from board  ";
    sql += "order by ref desc, re_step asc )) ";
    sql += "where rank between ? and ? ";

    try {
        ps = conn.prepareStatement(sql);
        ps.setInt(1, start);
        ps.setInt(2, end);
        rs = ps.executeQuery();

        while(rs.next()) {
            int num = rs.getInt("num");
            String writer = rs.getString("writer");
            String email = rs.getString("email");
            String subject = rs.getString("subject");
            String passwd = rs.getString("passwd");
            Timestamp reg_date = rs.getTimestamp("reg_date");
            int readcount = rs.getInt("readcount");
            int ref = rs.getInt("ref");
            int re_step = rs.getInt("re_step");
            int re_level = rs.getInt("re_level");
            String content = rs.getString("content");
            String ip = rs.getString("ip");

            BoardBean bb = new BoardBean(num, writer, email, subject, passwd, 
                    reg_date, readcount, ref, re_step, re_level, content, ip);

            list.add(bb);

        }
    } catch (SQLException e) {
        e.printStackTrace();
    } finally {
        try {
            if(rs != null)
                rs.close();
            if(ps != null)
                ps.close();
            if(conn != null)
                conn.close();
        } catch(SQLException e) {
            e.printStackTrace();
        }
    }
    System.out.println("list: " + list.size());
    return list;
}// getArticles

1-2. BoardDao.getArticlesCount()  (글 개수 조회)

 

 

조회 메서드 작성 순서 & 설명

더보기

1. 기본 조회 (정렬까지)

SELECT num, writer, email, subject, passwd, reg_date, readcount, ref, re_step, re_level, content, ip
FROM board
ORDER BY ref DESC, re_step ASC;
  • 게시글 전체 조회
  • ORDER BY ref DESC, re_step ASC
    → ref(원글 기준 그룹번호) → 최신 글이 위로 오도록 내림차순
    → re_step(답글 순서) → 나중에 단 답글(re_step이 큰 값)이 아래로 보이도록 오름차순 

2. ROWNUM 붙이기

SELECT ROWNUM AS rank, num, writer, email, subject, passwd, reg_date, readcount, ref, re_step, re_level, content, ip
FROM ( ...정렬된 select... )​

 

  • Oracle에서 페이징 처리를 위해 ROWNUM 붙임
  • 단, ROWNUM은 ORDER BY보다 먼저 적용되므로 정렬된 결과를 서브쿼리로 감싸야 원하는 순서대로 번호(rank)를 매길 수 있음

3. 바깥쪽에서 원하는 구간만 자르기

SELECT num, writer, email, subject, passwd, reg_date, readcount, ref, re_step, re_level, content, ip
FROM ( ...ROWNUM 붙인 select... )
WHERE rank BETWEEN ? AND ?;

 

  • ? 바인딩 변수 사용 (JDBC PreparedStatement에서 값 넣음)
    • ? 첫 번째: 시작 글 번호 (예: 1, 11, 21 …)
    • ? 두 번째: 끝 글 번호 (예: 10, 20, 30 …)
  • 예: BETWEEN 11 AND 20 → 11번째부터 20번째까지 게시글 가져오기

✅ 요약 흐름

  1. 게시글 가져오기 + 정렬
    → 최신 글 순서대로 정렬
  2. ROWNUM 붙이기
    → 행 번호(rank)를 생성
  3. WHERE rank BETWEEN ? AND ?
    → 특정 페이지 범위(시작~끝)만 잘라서 가져오기

🔑 정리

  • 안쪽 SELECT: 실제 데이터 정렬
  • 중간 SELECT: ROWNUM 붙여서 순번 매김
  • 바깥 SELECT: 페이지 범위 추출 (페이징)

 

🔑 한 줄 정리

  • rownum =  "넣은순서"가 아니라 "조회 순서"
  • num = "글 고유번호"

🔍 예시

1. 정렬 안 한 경우

select rownum, num, subject from board;

ROWNUM | NUM | SUBJECT
-------+-----+----------
1      | 1   | 첫 번째 글
2      | 2   | 두 번째 글
3      | 3   | 세 번째 글
4      | 4   | 네 번째 글

👉 이 경우에는 rownum이 insert된 순서대로 보이는 것처럼 보임.
왜냐면 기본적으로 Oracle이 클러스터 인덱스 순으로 읽어오기 때

 

2. 정렬을 바꾼 경우

select rownum, num, subject 
from (select * from board order by num desc);

ROWNUM | NUM | SUBJECT
-------+-----+----------
1      | 4   | 네 번째 글
2      | 3   | 세 번째 글
3      | 2   | 두 번째 글
4      | 1   | 첫 번째 글

👉 이번엔 rownum이 num 컬럼 내림차순 정렬된 순서대로 다시 매겨짐.

 

✅ 차이 요약

  • num
    • 우리가 시퀀스로 넣은 글번호 (테이블에 저장된 실제 값)
    • 절대 변하지 않음
  • rownum
    • SELECT 실행 시점에 결과 집합에 붙는 "임시 순번"
    • 정렬 결과에 따라 계속 달라질 수 있음

 


2. list.form 전체코드

더보기
<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ page import="java.util.*"%>
<%@ page import="myPkg.*"%>
<%
	BoardDao bdao= BoardDao.getInstance();
%>

<%@ page import="java.text.SimpleDateFormat"%>
	<%
		// static 이므로 클래스이름으로 접근한다.
		// 객체가 한번만 만들어질 수 있도록 static으로 설정
		
	
		// 날짜 포맷 지정 (작성일 출력용)
		SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm");	
	
		int pageSize = 10; // 1페이지당 10개
		
		String pageNum = request.getParameter("pageNum");
		if(pageNum == null){ // 현재화면에서 실행 1페이지 시작
			pageNum = "1";
		}
		int currentPage = Integer.parseInt(pageNum);
		
		// 현재 페이지에서 가져올 레코드의 시작행/끝행 계산
		int startRow = (currentPage - 1) * pageSize + 1;
		int endRow = (currentPage * pageSize);
		
		// 전체 레코드 개수 구하기
		int count = bdao.getArticleCount();
		ArrayList<BoardBean> list = bdao.getArticles(startRow, endRow);
		// 현재 페이지에 처음 시작되는 번호
		int number = count - (currentPage - 1) * pageSize;
	%>

<style>
    table {
        border-collapse: collapse;
        margin: 10px auto;
    }
    td, th {
        border: 1px solid #232323;
        padding: 5px;
    }
    th{
    	background-color: cyan;
    }
    a {
        text-decoration: none;
        color: blue;
    }
   
    #page{
    	text-align: center;
    }
    
    #titlelist{
    	text-align:center;
    }
</style>

<div id="titlelist">
<b>글목록(전체 글: <%=count %>)</b>
</div>
<table width="700">
	<tr>
		<td><a href="writeForm.jsp">글쓰기</a></td>
	</tr>
</table>
<table width="700">
	<tr height="30">
		<td align="center" width="50">번 호</td>
		<td align="center" width="250">제 목</td>
		<td align="center" width="100">작성자</td>
		<td align="center" width="150">작성일</td>
		<td align="center" width="50">조 회</td>
		<td align="center" width="100">IP</td>
	</tr>
	<% 
    	for(int i=0; i<list.size(); i+=1){
    		BoardBean bb = list.get(i);
    %>
	<tr>
		<td><%=number-- %></td>
		<%-- <td><%=bb.getNum() %></td> --%>
		<td id="title">
				<%
					if(bb.getRe_level()>0){//답글:1 답답:2 답답답:3
						int wid = bb.getRe_level() * 15;
				%>
					<img src="./images/level.gif" width="<%=wid%>"> 
					<img src="./images/re.gif">
				
				<%				
					} //if				
				%>				
					<a href="content.jsp?num=<%=bb.getNum()%>&pageNum=<%=currentPage%>"><%=bb.getSubject()%></a> 
				
				<%
					if(bb.getReadcount() >= 10){
				%>
					<img src="./images/hot.gif" height="14"/>
				<%
					}
				%>
				
		</td>
		<td><%=bb.getWriter() %></td>
		<td><%=sdf.format(bb.getReg_date()) %></td>
		<td><%=bb.getReadcount() %></td>
		<td><%=bb.getIp() %></td>
	</tr>
	<% 
    	}
    %>
</table>
<div id="page">
<%
	if(count > 0){
		// 전체 페이지 수 = 전체글수 / 페이지크기 (+나머지 있으면 +1)
		int pageCount = count / pageSize + (count % pageSize ==0 ? 0 : 1);
		
		int pageBlock = 10;  // 한번에 보여줄 페이지 블록 수
		int startPage = ((currentPage-1) / pageBlock * pageBlock) + 1;
		int endPage = startPage + pageBlock - 1; // 끝 페이지
		
		if(endPage > pageCount){
			endPage = pageCount;  // 마지막 페이지 번호를 초과하지 않도록 보정
		}
		
		if(startPage>10){
		%>
		<a href="list.jsp?pageNum=<%=startPage-10%>">[이전]</a>
		<% 			
		}
		%>
		<%
		for(int i=startPage; i<=endPage; i+=1){
		%>
		<a href="list.jsp?pageNum=<%=i %>">[<%=i %>]</a>
		<%			
		}// for
		
		if(endPage < pageCount){
		%>
		<a href="list.jsp?pageNum=<%=startPage+10%>">[다음]</a>
		<%			
				}
			}
		%>
</div>

📌 전체 흐름

1. DAO 불러오기

<%
	BoardDao bdao= BoardDao.getInstance();
%>
  • DAO를 싱글톤 패턴으로 생성해서 가져옴.
  • 이제 이 페이지에서 DB 접근은 bdao가 맡음.

2. 페이지 번호 및 시작(startRow)·끝(endRow) 행 구하기

int pageSize = 10; // 1페이지당 10개
String pageNum = request.getParameter("pageNum");
if(pageNum == null) {
	pageNum = "1";
}
int currentPage = Integer.parseInt(pageNum);

int startRow = (currentPage - 1) * pageSize + 1;
int endRow = currentPage * pageSize;

 

⚫pageSize: 한 페이지당 글 개수 (10개).

 

⚫ pageNum: 현재 몇 번째 페이지인지 파라미터로 받음 (없으면 1페이지).

===> 처음 창을 열었을때는 null 이므로

===> pageNum =  1

===> int currentPage = 1

 

  • startRow: 이 페이지에서 가져올 첫 번째 글 번호
  • endRow: 이 페이지에서 가져올 마지막 글 번호
startRow = (선택한 페이지 번호 - 1) * 페이지당 글 개수 + 1
endRow = (선택한 페이지 번호) * 페이지당 글 개수

3. 게시판 글 개수 · 글 목록 조회 & 화면 번호 계산

// 게시판 글 개수
int count = bdao.getArticleCount();

//  글 목록 조회
ArrayList<BoardBean> list = bdao.getArticles(startRow, endRow);

// 화면에 출력할 페이지 번호
int number = count - (currentPage - 1) * pageSize;
  1. count: 전체 글 수 (DB에서 select count(*)).
  2. list: 현재 페이지 범위(startRow~endRow)의 글 목록.
  3. number: 화면에 출력할 글 번호

✅ number 예시:

  • 전체 글 수 = 95, 페이지 크기 = 10
  • 1페이지 → 95 - (1-1)*10 = 95 (맨 위 글 번호 95)
  • 2페이지 → 95 - (2-1)*10 = 85 (맨 위 글 번호 85)
  • 3페이지 → 95 - (3-1)*10 = 75

 


4. 게시판 목록을 <table>에 출력하는 부분 (글 목록 테이블) 

<tr>
	<td align="center" width="50">번 호</td>
	<td align="center" width="250">제 목</td>
	<td align="center" width="100">작성자</td>
	<td align="center" width="150">작성일</td>
	<td align="center" width="50">조 회</td>
	<td align="center" width="100">IP</td>
</tr>
<%
	for(int i=0; i<list.size(); i++){
		BoardBean bb = list.get(i);
%>
<tr>
	<td><%=number-- %></td>
	<td id="title">
		<% if(bb.getRe_level()>0){ %>
			<img src="./images/level.gif" width="<%=bb.getRe_level()*15%>">
			<img src="./images/re.gif">
		<% } %>
		<a href="content.jsp?num=<%=bb.getNum()%>&pageNum=<%=currentPage%>"><%=bb.getSubject()%></a>
		<% if(bb.getReadcount() >= 10){ %>
			<img src="./images/hot.gif" height="14"/>
		<% } %>
	</td>
	<td><%=bb.getWriter() %></td>
	<td><%=sdf.format(bb.getReg_date()) %></td>
	<td><%=bb.getReadcount() %></td>
	<td><%=bb.getIp() %></td>
</tr>
<%
	}
%>
    🌈 제목 처리
  • number--: 번호를 순차적으로 줄여나감.
  • re_level: 답글 들여쓰기 (답글일수록 왼쪽 여백 wid ↑).
  • hot.gif: 조회수 10 이상이면 🔥 표시.
  • sdf.format: 작성일 yyyy-MM-dd HH:mm 형태로 출력.

5. 페이지 블록 출력 (하단 페이지 번호)

int pageCount = count / pageSize + (count % pageSize == 0 ? 0 : 1);
int pageBlock = 10;
int startPage = ((currentPage-1) / pageBlock * pageBlock) + 1;
int endPage = startPage + pageBlock - 1;
if(endPage > pageCount) endPage = pageCount;
  • count: 전체 글 수
  • pageSize: 한 페이지에 보여줄 글 개수

👉 count / pageSize 로 몫 계산 → 기본 페이지 수
👉 나머지가 있으면 +1 페이지 더 추가

더보기
int startPage = ((currentPage-1) / pageBlock * pageBlock) + 1;

 

👉 현재 페이지(currentPage)가 속한 페이지 블록의 시작 번호를 구하는 공식

 

📌 블록 개념 먼저

  • pageBlock = 10 이라고 하면
    • [1][2][3]...[10] → 첫 번째 블록
    • [11][12]...[20] → 두 번째 블록
    • [21][22]...[30] → 세 번째 블록

즉, 10개씩 묶어서 페이지 번호를 보여주는 것 


예시:

currentPage = 7, pageBlock = 10

(currentPage - 1) = 6

6 / 10 = 0 (정수 나눗셈)

0 * 10 = 0

+ 1 → startPage = 1👉 즉, 7번 페이지는 1~10 블록에 속하므로 시작 번호는 1


6. 페이지 번호링크 출력

if(startPage > 10){
	<a href="list.jsp?pageNum=<%=startPage-10%>">[이전]</a>
}
for(int i=startPage; i<=endPage; i++){
	<a href="list.jsp?pageNum=<%=i %>">[<%=i %>]</a>
}
if(endPage < pageCount){
	<a href="list.jsp?pageNum=<%=startPage+10%>">[다음]</a>
}

 

👉 페이지 번호 링크 출력

 

  • [이전], [다음] 버튼으로 앞/뒤 블록 이동.
  • 현재 블록 내 페이지 번호를 반복문으로 출력

📌 정리

  • 이 페이지 역할: list.jsp = 게시판의 글 목록 + 페이징 처리.
  • 데이터 흐름:
    DB → DAO (getArticles) → ArrayList<BoardBean> → JSP에서 for문으로 테이블 출력.
  • 주요 기능:
    • 글 번호 출력
    • 답글 들여쓰기 (re_level)
    • 조회수 10 이상 시 hot 아이콘
    • 작성일 포맷팅
    • 페이지 번호 블록 표시