40일차 내용 정리

채공부·2025년 7월 18일

CSS

반응형 레이아웃

sm

  • device 의 width가 576px 이상일 때만 적용되는 css
  • small device 이상에서만 적용
<div class="container">
	<h3>반응형 레이아웃 테스트</h3>
	<div class="row">
		<div class="col-sm-6">
			<h4>제목</h4>
			<p>Lorem, ipsum dolor sit amet consectetur adipisicing elit. Autem laboriosam eos nam quos explicabo tempora assumenda magnam laborum velit odit? Non quam iusto repellat doloribus mollitia veritatis delectus consectetur nesciunt?</p>
		</div>
		<div class="col-sm-6">
			<h4>제목</h4>
			<p>Lorem ipsum dolor sit amet consectetur, adipisicing elit. Minima numquam ipsum nam, alias quos culpa deleniti eum? Saepe cumque culpa quia. Ratione nobis possimus totam temporibus laborum culpa iste illo.</p>
		</div>
	</div>
</div>
		  sm		   md	  lg		xl		    xxl
         576px	     768px	 992px	  1200px	  1400px
세로 Phone | 가로 phone | table | desktop |   wide   |	
		  |		       |       |         |  desktop |
		  |		       |       |         |          |         
		  |		       |       |         |          | 

img-fluid

  • css 상에서 max-width:100%;
<div class="row">
	<div class="col-md-6 col-lg-3">
 		<h4>나의 사진</h4>
		<img class="img-fluid" src="images/image1.png">
	</div>
	<div class="col-md-6 col-lg-3">
		<h4>너의 사진</h4>
		<img class="img-fluid" src="images/image2.png">
	</div>
</div>
  • 적용 우선순위
⭐ 가장 마지막에 적은 css가 적용   
<style>
	.col-6{
		width:50%;
	}
	.col-sm-6{
		width:25%
	}
	.col-md-1{
		width:8.333%;
	}
</style>

<div class="col-6 col-sm-6 col-md-1"></div>

➜ 항상 존재하지 않고 조건부로 존재하기에 해당 원리를 이용해서 반응형 페이지를 생성

JAVA & JSP

  • bootstrap 여백 추가
p
- `pt, pe, pb, ps`

m
- `mt, me, mb, ms`1-5 단계(4px,8px,16px,24px,48px)

게시판

  • 게시글 목록 링크 생성
<li><a href="${pageContext.request.contextPath}/board/list.jsp"></a></li>
  • SecurityFilter 코드 추가
Set<String> whiteList = Set.of(
	"/index.jsp",
	"/user/loginform.jsp", "/user/login.jsp",
	"/user/signup-form.jsp", "/user/signup.jsp",
	"/images/", "/upload/", "/board/list.jsp"
);
  • 테이블 & 시퀀스 생성
CREATE TABLE board(
	num NUMBER PRIMARY KEY,
	writer VARCHAR2(20) NOT NULL,
	title VARCHAR2(50) NOT NULL,
	content CLOB,
	viewCount NUMBER DEFAULT 0,
	createdAt DATE DEFAULT SYSDATE
);

CREATE SEQUENCE board_seq;

BoardDto 생성

public class BoardDto {
	private int num;
	private String writer;
	private String title;
	private String content;
	private int viewCount;
	private String createdAt;
	
	// setter, getter 
	public int getNum() {
		return num;
	}
	public void setNum(int num) {
		this.num = num;
	}
	public String getWriter() {
		return writer;
	}
	public void setWriter(String writer) {
		this.writer = writer;
	}
	public String getTitle() {
		return title;
	}
	public void setTitle(String title) {
		this.title = title;
	}
	public String getContent() {
		return content;
	}
	public void setContent(String content) {
		this.content = content;
	}
	public int getViewCount() {
		return viewCount;
	}
	public void setViewCount(int viewCount) {
		this.viewCount = viewCount;
	}
	public String getCreatedAt() {
		return createdAt;
	}
	public void setCreatedAt(String createdAt) {
		this.createdAt = createdAt;
	}
}

목록

  • list.jsp 작성
<jsp:include page="/WEB-INF/include/navbar.jsp">
	<jsp:param value="board" name="thisPage"/>
</jsp:include>
<div class="container pt-1">
	<a class="btn btn-outline-primary btn-sm" href="new-form.jsp">
		새 글 작성 <i class="bi bi-pencil-square"></i>
	</a>
	<h1>게시글 목록 입니다</h1>
	<table class="table table-bordered">
		<thead>
			<tr> 
				<th>글번호</th>
				<th>작성자</th>
				<th>제목</th>
				<th>조회수</th>
				<th>작성일</th>
			</tr>
		</thead>
		<tbody>
			
		</tbody>
	</table>
</div>

작성

  • 글 작성 링크 생성
<a class="btn btn-outline-primary btn-sm" href="new-form.jsp">
	새 글 작성 <i class="bi bi-pencil-square"></i>
</a>
  • new-form.jsp 생성
<jsp:include page="/WEB-INF/include/resource.jsp"></jsp:include>
<div class="container">
		<h1>게시글 작성 양식</h1>
		<form action="save.jsp" method="post">
			<div class="mb-2">
				<label class="form-label" for="title">제목</label>
				<input class="form-control" type="text" name="title" id="title" />
			</div>
			<div class="mb-2">
				<label class="form-label" for="content">내용</label>
				<textarea class="form-control" name="content" id="content" rows="10"></textarea>
			</div>
			<button class="btn btn-sm bg-primary-subtle text-white btn-sm" type="submit">작성하기</button>
		</form>
	</div>

BoardDao

  • 싱글톤 패턴
// 자신의 참조값을 저장할 static 필드
private static BoardDao dao;
// static 초기화 블럭에서 객체 생성해서 static 필드에 저장
static {
	dao = new BoardDao();
}
// 외부에서 객체 생성하지 못하도록 생성자의 접근 지정자를 private 로 지정
private BoardDao() {}
// 참조값을 리턴해주는 static 메소드 제공
public static BoardDao getInstance() {
	return dao;
}
  • 글 번호를 미리 select 해서 리턴해주는 메소드
// 글 번호를 미리 select 해서 리턴해주는 메소드
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();

		while (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;
}
  • 글 정보를 저장하는 메소드
public boolean insert(BoardDto dto) {
	Connection conn = null;
	PreparedStatement pstmt = null;
	int rowCount = 0;
	try {
		conn = new DbcpBean().getConn();
		String sql = """
			INSERT INTO board
			(num, writer, title, content)
			VALUES(?, ?, ?, ?)
			""";
		pstmt = conn.prepareStatement(sql);
        pstmt.setInt(1, dto.getNum());
		pstmt.setString(2, dto.getWriter());
		pstmt.setString(3, dto.getTitle());
		pstmt.setString(4, dto.getContent());
		rowCount = pstmt.executeUpdate();
	} catch (Exception e) {
		e.printStackTrace();
	} finally {
		try {
			if (pstmt != null)
				pstmt.close();
			if (conn != null)
				conn.close();
	} catch (Exception e) {
		}
	}
	if (rowCount > 0) {
		return true;
	} else {
		return false;
	}
}
⭐ 원래 Dao의 INSERT에서 NEXTVAL로 전달한다면 글번호 전달 불가능
    DB에 시퀀스 값을 select 하여 시퀀스 값을 바로 넘겨야 한다
  • save.jsp 생성
<%
// 폼 전송되는 title 과 content
String title = request.getParameter("title");
String content = request.getParameter("content");
// 글 작성자는 세션 객체로부터 얻어낸다
String writer = (String)session.getAttribute("userName");
// DB 에 저장하기
BoardDto dto = new BoardDto();
dto.setWriter(writer);
dto.setTitle(title);
dto.setContent(content);
// 글 번호를 미리 얻어낸다
int num = BoardDao.getInstance().getSequence();
// 글 번호도 dto 에 담는다
dto.setNum(num);
boolean isSuccess = BoardDao.getInstance().insert(dto);
// 응답하기
<div class="container">
	<%if(isSuccess) {%>
		<script>
			alert("저장 완료");
			location.href = "view.jsp?num=<%=num%>";
		</script>
	<%} else{%>
		<p>
			글 저장 실패
			<a href="new-form.jsp">다시 작성</a>
		</p>
	<%} %>
</div>
  • 글 하나의 정보를 리턴해주는 메소드
public BoardDto getByNum(int num) {
	BoardDto dto = null;
	Connection conn = null;
	PreparedStatement pstmt = null;
	ResultSet rs = null;
	try {
		conn = new DbcpBean().getConn();
		String sql = """
				SELECT writer, title, content, viewCount, createdAt
				FROM board
				WHERE num = ?
				""";
		pstmt = conn.prepareStatement(sql);
		pstmt.setInt(1, num);
		rs = pstmt.executeQuery();

		if (rs.next()) {
			dto = new BoardDto();
			dto.setNum(num);
			dto.setWriter(rs.getString("writer"));
			dto.setTitle(rs.getString("title"));
			dto.setContent(rs.getString("content"));
			dto.setViewCount(rs.getInt("viewCount"));
			dto.setCreatedAt(rs.getString("createdAt"));
		}
	} 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 dto;
}

상세보기

  • view.jsp 생성
<%
	// get 방식 파라미터로 전달되는 글 번호 얻어내기
	int num = Integer.parseInt(request.getParameter("num"));
	// DB 에서 해당 글의 자세한 정보를 얻어낸다
	BoardDto dto = BoardDao.getInstance().getByNum(num);
%>
<div class="container">
	<nav aria-label="breadcrumb">
		<ol class="breadcrumb">
		    <li class="breadcrumb-item">
		    	<a href="${pageContext.request.contextPath}/">Home</a>
		    </li>
		    <li class="breadcrumb-item">
		    	<a href="${pageContext.request.contextPath}/board/list.jsp">Board</a>
		    </li>
		    <li class="breadcrumb-item active">Detail</li>
		</ol>
	</nav>
	<h1>게시글 상세보기</h1>
	<table class="table table-striped">
		<colgroup>
			<col class="col-2" />
			<col class="col" />
		</colgroup>
		<tr>
			<th>글번호</th>
			<td><%=num%></td>
		</tr>
		<tr>
			<th>작성자</th>
			<td><%=dto.getWriter()%></td>
		</tr>
		<tr>
			<th>제목</th>
			<td><%=dto.getTitle()%></td>
		</tr>
		<tr>
			<th>조회수</th>
			<td><%=dto.getViewCount() %></td>
		</tr>
	</table>
	<div><pre><%=dto.getContent() %></pre></div>
	<div class="card mt-4">
=		<div class="card-header bg-light">
			<strong>본문 내용</strong>
		</div>
		<div class="card-body p-1">
			<pre class="mb-0" style="background-color: #f8f9fa; border-radius: 5px; padding: 1rem; white-space: pre-wrap; font-family: '맑은 고딕', 'Consolas', monospace;"><%= dto.getContent() %></pre>
		</div>
	</div>
</div>
  • 글 전체 목록을 리턴하는 메소드
public List<BoardDto> selectAll(){
	// 글 목록을 담을 객체 생성
	List<BoardDto> list = new ArrayList<>();
	Connection conn = null;
	PreparedStatement pstmt = null;
	ResultSet rs = null;
	try {
		conn = new DbcpBean().getConn();
		String sql = """
				SELECT num, writer, title, viewCount, createdAt
				FROM board
				ORDER BY num DESC
				""";
		pstmt = conn.prepareStatement(sql);

		rs = pstmt.executeQuery();

		while (rs.next()) {
			BoardDto dto = new BoardDto();
			dto.setNum(rs.getInt("num"));
			dto.setWriter(rs.getString("writer"));
			dto.setTitle(rs.getString("title"));
			dto.setViewCount(rs.getInt("viewCount"));
			dto.setCreatedAt(rs.getString("createdAt"));
			/* select 된 row 하나당 BoardDto 객체를 하나씩 생성해서
			   글 하나의 정보를 담고
			   BoardDto 객체의 참조값을 List 객체에 누적시키기*/
			list.add(dto);
		}
	} 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 list;
}
  • view.jsp 코드 추가
<%
	// get 방식 파라미터로 전달되는 글 번호 얻어내기
	int num = Integer.parseInt(request.getParameter("num"));
	// DB 에서 해당 글의 자세한 정보를 얻어낸다
	BoardDto dto = BoardDao.getInstance().getByNum(num);
%>
<div class="container pt-3">
		<nav aria-label="breadcrumb">
		  <ol class="breadcrumb">
		    <li class="breadcrumb-item">
		    	<a href="${pageContext.request.contextPath}/">Home</a>
		    </li>
		    <li class="breadcrumb-item">
		    	<a href="${pageContext.request.contextPath}/board/list.jsp">Board</a>
		    </li>
		    <li class="breadcrumb-item active">Datail</li>
		  </ol>
		</nav>
		<h1>게시글 상세보기</h1>
		<table class="table table-striped">
			<colgroup>
				<col class="col-2" />
				<col class="col" />
			</colgroup>
			<tr>
				<th>글번호</th>
				<td><%=num%></td>
			</tr>
			<tr>
				<th>작성자</th>
				<td><%=dto.getWriter()%></td>
			</tr>
			<tr>
				<th>제목</th>
				<td><%=dto.getTitle()%></td>
			</tr>
			<tr>
				<th>조회수</th>
				<td><%=dto.getViewCount() %></td>
			</tr>
		</table>
		<div><pre><%=dto.getContent() %></pre></div>
		<div class="card mt-4">
			<div class="card-header bg-light">
		    	<strong>본문 내용</strong>
			</div>
			<div class="card-body p-1">
				<pre class="mb-0" style="background-color: #f8f9fa; border-radius: 5px; padding: 1rem; white-space: pre-wrap; font-family: '맑은 고딕', 'Consolas', monospace;"><%= dto.getContent() %></pre>
			</div>
		</div>
	</div>

⚠️ 클라이언트가 작성한 글 제목이나 내용을 그대로 클라이언트에게 출력하는 것은
    javascript 주입 공격을 받을 수 있다
    따라서 해당 문자열을 escape 해서 출력하는 것이 안전하다

MVN

  • Apache Commons Text & Apache Commons Lang 의 jar 파일 다운로드 후 lib 폴더로 해당 파일들을 이동

  • 내용 출력 수정
<div><pre><%=StringEscapeUtils.escapeHtml4(dto.getContent())%></pre></div>
<div class="card mt-4">
	<div class="card-header bg-light">
		<strong>본문 내용</strong>
	</div>
	<div class="card-body p-1">
		<pre class="mb-0" style="background-color: #f8f9fa; border-radius: 5px; padding: 1rem; white-space: pre-wrap; font-family: '맑은 고딕', 'Consolas', monospace;"><%=StringEscapeUtils.escapeHtml4(dto.getContent())%></pre>
	</div>
</div>

삭제

  • 삭제 링크 생성
<%if(dto.getWriter().equals(userName)) {%>
	<div class="text-end pt-2">
		<a class="btn btn-danger btn-sm" href="delete.jsp?num=<%=dto.getNum()%>">Delete</a>
	</div>
<%} %>
  • 글을 삭제하는 메소드
public boolean deleteByNum(int num) {
	Connection conn = null;
	PreparedStatement pstmt = null;
	int rowCount = 0;
	try {
		conn = new DbcpBean().getConn();
		String sql = """
				DELETE
				FROM board
				WHERE num = ?
				""";
		pstmt = conn.prepareStatement(sql);
		pstmt.setInt(1, num);
		rowCount = pstmt.executeUpdate();
	} catch (Exception e) {
		e.printStackTrace();
	} finally {
		try {
			if (pstmt != null)
				pstmt.close();
			if (conn != null)
				conn.close();
		} catch (Exception e) {
		}
	}
	if (rowCount > 0) {
		return true;
	} else {
		return false;
	}
}
  • delete.jsp
<%
	// GET 방식 파라미터로 전달되는 글번호를 읽어와서
	int num = Integer.parseInt(request.getParameter("num"));
	// 글 작성자와 로그인된 userName 이 동일한지 비교해서 동일하지 않으면 에러를 응답
	String writer = BoardDao.getInstance().getByNum(num).getWriter(); // 삭제할 글 작성자
	String userName = (String)session.getAttribute("userName");
	// 만일 글 작성자와 로그인된 userName 하고 같지 않으면
	if(!writer.equals(userName)){
		// 에러 페이지 응답
		response.sendError(HttpServletResponse.SC_FORBIDDEN,"님의 글 지우기 불가능");
		return; // 메소드를 여기서 종료
	}
	// DB 에서 해당 글을 삭제하고
	BoardDao.getInstance().deleteByNum(num);
	// 응답한다
%>
<script>
	alert("삭제 완료");
	location.href = "${pageContext.request.contextPath}/board/list.jsp";
</script>

수정

  • 수정 링크 생성
<%if(dto.getWriter().equals(userName)) {%>
	<div class="text-end pt-2">
		<a class="btn btn-warning btn-sm" href="edit.jsp?num=<%=dto.getNum()%>">Edit</a>
	</div>
<%} %>
  • edit.jsp
<%
	int num = Integer.parseInt(request.getParameter("num"));
	BoardDto dto = BoardDao.getInstance().getByNum(num);
	
%>
<div class="container pt-3">
	<nav aria-label="breadcrumb">
		<ol class="breadcrumb">
			<li class="breadcrumb-item">
				<a href="${pageContext.request.contextPath}/">Home</a>
			</li>
			<li class="breadcrumb-item">
				<a href="${pageContext.request.contextPath}/board/list.jsp">Board</a>
		    </li>
		    <li class="breadcrumb-item active">Edit</li>
		</ol>
	</nav>
	<h1>글 수정 페이지</h1>
	<form action="update.jsp" method="post">
		<div>
			<label class="form-label" for="num">글번호</label>
			<input class="form-control" type="text" name="num" id="num" 
					value="<%=dto.getNum() %>" readonly/>
		</div>
		<div>
			<label class="form-label" for="writer">작성자</label>
			<input type="text" class="form-control" name="writer" id="writer"
					value="<%=dto.getWriter()%>" readonly/>
		</div>
		<div>
			<label class="form-label" for="title">제목</label>
			<input type="text" class="form-control" name="title" id="title"
					value="<%=dto.getTitle()%>"/>
		</div>
		<div>
			<label class="form-label" for="content">내용</label>
			<textarea class="form-control" name="content"
					id="content" rows="10"><%=dto.getContent()%></textarea>
		</div>
		<button class="btn btn-success btn-sm">수정</button>
		<button class="btn btn-danger btn-sm">Reset</button>
	</form>
</div>
  • 글을 수정하는 메소드
public boolean update(BoardDto dto) {
	Connection conn = null;
	PreparedStatement pstmt = null;
	int rowCount = 0;
	try {
		conn = new DbcpBean().getConn();
		String sql = """
				UPDATE board
				SET title=?, content=?
				WHERE num=?
				""";
		pstmt = conn.prepareStatement(sql);
		pstmt.setString(1, dto.getTitle());
		pstmt.setString(2, dto.getContent());
		pstmt.setInt(3, dto.getNum());
		rowCount = pstmt.executeUpdate();
	} catch (Exception e) {
		e.printStackTrace();
	} finally {
		try {
			if (pstmt != null)
				pstmt.close();
			if (conn != null)
				conn.close();
		} catch (Exception e) {
		}
	}
	if (rowCount > 0) {
		return true;
	} else {
		return false;
	}
}
  • update.jsp
<%
	// 폼 전송되는 내용을 읽어와서
	int num = Integer.parseInt(request.getParameter("num"));
	String title = request.getParameter("title");
	String content = request.getParameter("content");
	
	// 글 작성자와 로그인된 userName 이 동일한지 비교해서 동일하지 않으면 에러를 응답
	String writer = BoardDao.getInstance().getByNum(num).getWriter(); // 수정할 글 작성자
	String userName = (String)session.getAttribute("userName");
	// 만일 글 작성자와 로그인된 userName 하고 같지 않으면
	if(!writer.equals(userName)){
		// 에러 페이지 응답
		response.sendError(HttpServletResponse.SC_FORBIDDEN,"님의 글 수정 불가");
		return; // 메소드를 여기서 종료
	}
	
	// DB 에 수정 반영하고
	BoardDto dto = new BoardDto();
	dto.setNum(num);
	dto.setTitle(title);
	dto.setContent(content);
	BoardDao.getInstance().update(dto);
	// 응답한다
%>
<script>
	alert("수정 완료");
	location.href="${pageContext.request.contextPath}/board/view.jsp?num=<%=num%>";
</script>

navbar.jsp 코드 추가

<li class="nav-item">
	<a class="nav-link <%=thisPage.equals("board") ? "active":""%>" 
	href="${pageContext.request.contextPath }/board/list.jsp">Board</a>
</li>
profile
학원 공부 내용 정리

0개의 댓글