본문 바로가기
05_Server (04. JSP 프로그래밍 구현)

[2-2] 공지사항서비스 목록조회요청

by moca7 2024. 9. 10.

 

 

 

ㅁ 공지사항서비스
- 공지사항서비스_목록조회요청
- 공지사항서비스_작성요청
- 공지사항서비스_수정요청
- 공지사항서비스_삭제요청

 

 

 

 

 

ㅁ header.jsp

 

- header.jsp에 있는 공지사항이라는 메뉴 클릭시 공지사항 목록 페이지가 뜨게끔 한다.

 

 
<%@ page import="com.br.web.member.model.vo.Member" %>
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
   
<!-- Bootstrap 사용을 위한 CDN -->
<!-- ------------------------- -->
   
<style>
  header{height: 150px}
  header a{color:black;}
</style>

<%
  String contextPath = request.getContextPath(); // "/web"
 
  Member loginUser = (Member)session.getAttribute("loginUser");
  // 해당 구문이 실행되는 시점
  // 로그인 요청 전 페이지 로드시 : null
  // 로그인 성공 후 페이지 로드시 : 조회된 데이터가 담겨있는 Member객체
 
  String alertMsg = (String)session.getAttribute("alertMsg");
  // 해당 구문이 실행되는 시점
  // 특정 서비스 요청 전 페이지 로드시 : null
  // 특정 서비스 요청 성공 후 페이지 로드시 : alert로 띄워줄 메세지
%>

<% if(alertMsg != null) { %>
<script>
  alert('<%=alertMsg%>');
</script>
<% session.removeAttribute("alertMsg"); } %>

<header class="row m-3">
  <div class="col-3 d-flex justify-content-center align-items-center">
    <a href="<%=contextPath%>"><img src="<%= contextPath %>/assets/image/goodee_logo.png"></a>
  </div>
  <div class="col-6"></div>
  <div class="col-3 d-flex justify-content-center align-items-center">

    <% if(loginUser == null) { %>
    <!-- case1. 로그인전 -->
    <form action="<%= contextPath %>/login.me" method="post">
      <table>
        <tr>
          <th>ID</th>
          <td><input type="text" name="userId" class="form-control form-control-sm" placeholder="Enter Your ID" required></td>
        </tr>
        <tr>
          <th>PWD</th>
          <td><input type="password" name="userPwd" class="form-control form-control-sm" placeholder="Enter Your PWD" required></td>
        </tr>
        <tr>
          <td colspan="2" align="center">
            <button type="submit" class="btn btn-secondary btn-sm">로그인</button>
            <button type="button" class="btn btn-secondary btn-sm" id="signup-btn">회원가입</button>
            <script>
              document.getElementById("signup-btn").addEventListener("click", () => {
                location.href = "<%=contextPath%>/signup.me";
              })
            </script>
          </td>
        </tr>
      </table>
    </form>
    <% }else { %>
    <!-- case2. 로그인후 -->
    <div>
      <b><%= loginUser.getUserName() %></b>님 환영합니다. <br><br>

      <a href="<%=contextPath%>/myinfo.me">마이페이지</a>
      <a href="<%=contextPath%>/logout.me">로그아웃</a>
    </div>
    <% } %>

  </div>
</header>
<nav class="navbar m-3 navbar-expand-sm bg-dark navbar-dark d-flex justify-content-center">
  <ul class="navbar-nav">
    <li class="nav-item">
      <a class="nav-link" href="<%=contextPath%>">Home</a>
    </li>
    <li class="nav-item">
      <a class="nav-link" href="<%= contextPath %>/list.no">공지사항</a>
    </li>
    <li class="nav-item">
      <a class="nav-link" href="#">일반게시판</a>
    </li>
    <li class="nav-item">
      <a class="nav-link" href="#">사진게시판</a>
    </li>
  </ul>
</nav>
 

 

 

- 'Home' 링크를 클릭하면 메인페이지로 이동하게끔 한다. 

- '공지사항' 링크를 클릭하면 "/list.no"라는 url mapping 값을 가지는 서블릿 클래스가 호출되게끔 한다.

 

 

 

 

 

ㅁ src/main/webapp/views/notice에 noticeList.jsp를 만든다. 

 

 

 

 

 

 

 

- "공지사항목록페이지.html"에서 body 태그 안을 복사해서 붙여넣는다.

 

 

 

- 아래는 noticeList.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>
</head>
<body>

  <div class="container p-3">

   <!-- Header, Nav start -->
   <%@ include file="/views/common/header.jsp" %>
   <!-- Header, Nav end -->
 

   <!-- Section start -->
   <section class="row m-3" style="min-height: 500px">

    <div class="container border p-5 m-4 rounded">

      <h2 class="m-4">공지사항 목록</h2>

      <!-- 현재 로그인한 회원이 관리자 권한일 경우 보여지는 요소 -->
      <div align="right">
        <button type="button" class="btn btn-secondary btn-sm">등록하기</button>
      </div>

      <br>
      <table id="notice-list" class="table">
        <thead>
          <tr>
            <th width="100px">번호</th>
            <th width="600px">글제목</th>
            <th width="120px">작성자</th>
            <th>작성일</th>
          </tr>
        </thead>
        <tbody>
          <!-- case1. 조회된 공지글이 없을 경우
          <tr>
            <td colspan="4" style="text-align:center">존재하는 공지사항글이 없습니다.</td>
          </tr>
          -->

          <!-- case2. 조회된 공지글이 있을 경우 -->
          <tr class="board-title" data-toggle="collapse" data-target="#notice3">
            <td>3</td>
            <td>글제목입니다.</td>
            <td>admin</td>
            <td>2024-08-21</td>
          </tr>
          <tr class="board-content collapse" id="notice3">
            <td colspan="4">
              <p class="border rounded p-3 w-75 mx-auto" style="min-height: 150px;">3번 게시글의 내용입니다.</p>
             
              <!-- 로그인한 회원이 관리자 권한일 경우 보여지는 요소 -->
              <div align="center">
                <button type="button" class="btn btn-secondary btn-sm">수정하기</button>
                <button type="button" class="btn btn-danger btn-sm">삭제하기</button>
              </div>
            </td>
          </tr>

          <tr class="board-title" data-toggle="collapse" data-target="#notice2">
            <td>2</td>
            <td>글제목입니다.</td>
            <td>admin</td>
            <td>2024-08-21</td>
          </tr>
          <tr class="board-content collapse" id="notice2">
            <td colspan="4">
              <p class="border rounded p-3 w-75 mx-auto" style="min-height: 150px;">2번 게시글의 내용입니다.</p>
            </td>
          </tr>

          <tr class="board-title" data-toggle="collapse" data-target="#notice1">
            <td>1</td>
            <td>글제목입니다.</td>
            <td>admin</td>
            <td>2024-08-21</td>
          </tr>
          <tr class="board-content collapse" id="notice1">
            <td colspan="4">
              <p class="border rounded p-3 w-75 mx-auto" style="min-height: 150px;">1번 게시글의 내용입니다.</p>
            </td>
          </tr>

        </tbody>
      </table>
     
     
    </div>

   </section>
   <!-- Section end -->
 

   <!-- Footer start -->
   <%@ include file="/views/common/footer.jsp" %>
   <!-- Footer end -->

 </div>

</body>
</html>
 

 

 

- 페이지명은 항상 소문자로 시작해야 클래스들과 헷갈리지 않는다.

클래스는 다 대문자로 시작, jsp 파일은 다 소문자로 시작.

 

 

 

 

 

 /webApp/src/main/java/com/br/web/notice/controller에 NoticeListController.java라는 서블릿 클래스를 만든다. 

 

- 공지사항 서비스는 ".no"라는 패턴을 붙이기로 한다.

- "/list.no"라는 url mapping 값을 가지는 서블릿 클래스를 만든다.

 

 
package com.br.web.notice.controller;

import java.io.IOException;
import java.util.List;

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.br.web.notice.model.service.NoticeService;
import com.br.web.notice.model.vo.Notice;


@WebServlet("/list.no")
public class NoticeListController extends HttpServlet {

    private static final long serialVersionUID = 1L;

    public NoticeListController() {
        super();
    }


    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
 
        // 1. 요청
        //    응답데이터 조회
        List<Notice> list = new NoticeService().selectNoticeList();
 
       
        // 2. 응답
        //    ㄴ 응답페이지 : 공지사항 목록페이지 (/web/views/notice/noticeList.jsp)
        //    ㄴ 응답데이터 : db로 부터 조회된 데이터 (응답페이지에서만 필요)
       
        request.setAttribute("list", list);
        request.getRequestDispatcher("/views/notice/noticeList.jsp").forward(request, response);
       
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }

}
 

 

 

 

- 메뉴바에 있는 메뉴 클릭시 요청되고 딱히 전달되는 데이터는 없다.

request 객체에서 딱히 데이터를 뽑아낼 게 없다. 바로 쿼리를 실행한다.

- NOTICE 테이블을 전체 조회하면 Notice 객체가 여러개 나온다. (여러행의 데이터가 조회됨)

List<Notice>에 담는다. 

 

- 응답데이터는 응답페이지에서만 필요하다. 공지사항 목록 데이터는 다른 페이지로 이동하고 나서는 필요 없다. 

- 공지사항 목록 페이지(/web/views/notice/noticeList.jsp)는 처음으로 이동한다.

이 페이지를 응답하는 기존의 서블릿 클래스는 없다. 여기가 최초이다. 여기서 포워딩으로 이동한다.

- 포워딩으로 이동하면 포워딩시 전달되는 request에 데이터를 담아서 전달하면 된다.

 

 

 

 

ㅁ NoticeService 클래스에 selectNoticeList() 메소드를 만든다.

 

 
package com.br.web.notice.model.service;

import java.sql.Connection;
import java.util.List;

import com.br.web.notice.model.dao.NoticeDao;
import com.br.web.notice.model.vo.Notice;

import static com.br.web.common.template.JDBCTemplate.getConnection;
import static com.br.web.common.template.JDBCTemplate.close;
import static com.br.web.common.template.JDBCTemplate.commit;
import static com.br.web.common.template.JDBCTemplate.rollback;

public class NoticeService {
   
    private NoticeDao nDao = new NoticeDao();
   
    public List<Notice> selectNoticeList(){
 
        Connection conn = getConnection();
        List<Notice> list = nDao.selectNoticeList(conn);
 
        close(conn);
        return list;
 
    }

}
 

 

- service에서는 항상 Connection 객체를 생성해서 넘어간다. JDBCTemplate을 import static한다.

- dao 클래스의 selectNoticeList 메소드에 db와 연결된 Connection 객체를 넘기면서 실행한다.

 

 

 

 

 

ㅁ NoticeDao 클래스에 selectNoticeList() 메소드를 만든다.

 

 
package com.br.web.notice.model.dao;

import java.io.FileInputStream;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

import com.br.web.notice.model.vo.Notice;

import static com.br.web.common.template.JDBCTemplate.close;
 

public class NoticeDao {
   
    private Properties prop = new Properties();
 
   
    public NoticeDao() {
 
        try {
            prop.loadFromXML(new FileInputStream(NoticeDao.class.getResource("/db/mappers/notice-mapper.xml").getPath()));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
 
   
    public List<Notice> selectNoticeList(Connection conn){

        // select => ResultSet (여러행, 여러Notice객체) => List<Notice>
        List<Notice> list = new ArrayList<>();
        PreparedStatement pstmt = null;
        ResultSet rset = null;
 
        String sql = prop.getProperty("selectNoticeList");
       
        try {
            pstmt = conn.prepareStatement(sql);
            rset = pstmt.executeQuery();
           
            while(rset.next()) {
                list.add(new Notice(rset.getInt("notice_no")
                                  , rset.getString("notice_title")
                                  , rset.getString("notice_content")
                                  , rset.getString("user_id")
                                  , rset.getDate("regist_date")));
            }
           
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            close(rset);
            close(pstmt);
        }
       
        return list;
       
    }
   
}
 

 

 

- 최종 결과를 담을 List는 선언할 때 null로 초기화가 아니라 new로 생성을 해준다.

- List가 비어있냐 비어있지 않느냐로 조회 결과가 있는지 없는지를 판별한다. 

 

- 이번에 담은 sql문은 물음표가 없는 완성된 형태의 sql문이지만 그냥 Statement 객체가 아닌 PreparedStatement 객체에 담는다. 

- 5개 필드짜리 생성자를 Notice 객체에 만든다. 

- rset.getString("user_id")에는 noticeWriter가 아니라 db에 존재하는 컬럼명인 "USER_ID"를 작성해야 한다.

 

 

 

 

 

- VO객체인 Notice의 noticeWriter 필드에 insert할 때는 회원번호를 담아서 넘기지만, 

select로 조회해올 때는 조인해서 회원의 아이디를 담아서 가져온다.

 

 

 

 

 

ㅁ NoticeListController.java(서블릿 클래스)로 돌아온다.

 

 
package com.br.web.notice.controller;

import java.io.IOException;
import java.util.List;

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.br.web.notice.model.service.NoticeService;
import com.br.web.notice.model.vo.Notice;


@WebServlet("/list.no")
public class NoticeListController extends HttpServlet {

    private static final long serialVersionUID = 1L;

    public NoticeListController() {
        super();
    }


    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
 
        // 1. 요청
        //    응답데이터 조회
        List<Notice> list = new NoticeService().selectNoticeList();
       
        // 2. 응답
        //    ㄴ 응답페이지 : 공지사항 목록페이지 (/web/views/notice/noticeList.jsp)
        //    ㄴ 응답데이터 : db로 부터 조회된 데이터 (응답페이지에서만 필요)
       
        request.setAttribute("list", list);
        request.getRequestDispatcher("/views/notice/noticeList.jsp").forward(request, response);
       
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }

}
 

 

 

- 응답데이터는 응답페이지에서만 필요하다. 공지사항 목록 데이터는 다른 페이지로 이동하고 나서는 필요 없다. 

- 공지사항 목록 페이지(/web/views/notice/noticeList.jsp)는 처음으로 이동한다.

이 페이지를 응답하는 기존의 서블릿 클래스는 없다. 여기가 최초이다. 여기서 포워딩으로 이동한다.

- 포워딩으로 이동하면 포워딩시 전달되는 request에 데이터를 담아서 전달하면 된다.

 

- 조회결과가 있든지 없든지 무조건 공지사항 목록페이지로 이동시킨다. 조회 결과가 없어도. 

- request에 "list"라는 key값으로 Notice 객체가 담긴 list를 담는다.

 

 

 

 

ㅁ /webApp/src/main/webapp/views/notice의 noticeList.jsp로 돌아온다.

 

 
<%@ page import="java.util.List" %>
<%@ page import="com.br.web.notice.model.vo.Notice" %>
 
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
 
<%
  List<Notice> list = (List<Notice>)request.getAttribute("list");
%>
 
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>

  <div class="container p-3">

   <!-- Header, Nav start -->
   <%@ include file="/views/common/header.jsp" %>
   <!-- Header, Nav end -->

   <!-- Section start -->
   <section class="row m-3" style="min-height: 500px">

    <div class="container border p-5 m-4 rounded">

      <h2 class="m-4">공지사항 목록</h2>

      <% if(loginUser != null && loginUser.getStatus().equals("A")) { %>
      <div align="right">
        <a href="<%= contextPath %>/write.no" class="btn btn-secondary btn-sm">등록하기</a>
      </div>
      <% } %>

      <br>
      <table id="notice-list" class="table">
        <thead>
          <tr>
            <th width="100px">번호</th>
            <th width="600px">글제목</th>
            <th width="120px">작성자</th>
            <th>작성일</th>
          </tr>
        </thead>
        <tbody>
 
          <% if(list.isEmpty()) { %>
          <!-- case1. 조회된 공지글이 없을 경우 -->
          <tr>
            <td colspan="4" style="text-align:center">존재하는 공지사항글이 없습니다.</td>
          </tr>
          <% } else { %>
 
            <!-- case2. 조회된 공지글이 있을 경우 -->
            <% for(Notice n : list) { %>
            <tr class="board-title" data-toggle="collapse" data-target="#notice<%=n.getNoticeNo()%>">
              <td><%= n.getNoticeNo() %></td>
              <td><%= n.getNoticeTitle() %></td>
              <td><%= n.getNoticeWriter() %></td>
              <td><%= n.getRegistDt() %></td>
            </tr>
            <tr class="board-content collapse" id="notice<%=n.getNoticeNo()%>">
              <td colspan="4">
                <p class="border rounded p-3 w-75 mx-auto" style="min-height: 150px; white-space:pre;"><%= n.getNoticeContent() %></p>
               
                <% if(loginUser != null && loginUser.getUserId().equals(n.getNoticeWriter())) { %>
                <div align="center">
                  <a href="<%= contextPath %>/modify.no?no=<%= n.getNoticeNo() %>" class="btn btn-secondary btn-sm">수정하기</a>
                  <a href="<%= contextPath %>/delete.no?no=<%= n.getNoticeNo() %>" class="btn btn-danger btn-sm">삭제하기</a>
                </div>
                <%--
                  * 공지사항 삭제하기 과제 *
                    삭제하기 버튼 클릭시 /delete.no 요청하도록
                    이때 삭제할 글번호 넘기기
                   
                    미리작성해둔 쿼리 실행 후
                   
                    성공일 경우 다시 목록페이지가 보여지도록, alert메세지로 성공메세지
                    실패일 경우 에러페이지 보여지도록, 에러메세지 출력되도록
                --%>
               
                <% } %>
              </td>
            </tr>
            <% } %>
 
          <% } %>

        </tbody>
      </table>
     
     
    </div>

   </section>
   <!-- Section end -->

   <!-- Footer start -->
   <%@ include file="/views/common/footer.jsp" %>
   <!-- Footer end -->

 </div>

</body>
</html>
 

 

 

- 상단에 스크립틀릿으로 List<Notice> list = (List<Notice>)request.getAttribute("list");를 작성한다.

데이터를 꺼내는 구문은 항상 최상단에 써주는게 좋다. HTML 구문이 실행되기 전에 작성했다.

- 그에 맞춰 List와 Notice 클래스도 import 한다. jsp에서는 import 지시어를 사용해야 한다.

 

- noticeList.jsp도 header.jsp를 include하고 있다.

header.jsp에서 이미 현재 로그인한 유저 정보를 loginUser라는 변수에 담아놔서 noticeList.jsp에서도 쓸 수 있다.

- if(loginUser != null && loginUser.getStatus().equals("A"))으로

현재 로그인되어 있는지를 체크하고 또 로그인한 유저의 STATUS가 'A'(관리자)라면,

공지사항 목록페이지에 '등록하기' 버튼을 화면에 노출한다.

 

 

- 조회된 공지글이 없을 경우, 조건문 if(list.isEmpty()){} else{} 문으로 화면에 보여줄 내용을 작성한다.

- 조회된 공지글이 있을 경우, 반복문 for(Notice n : list)으로 한 Notice 객체당 2개의 행(제목행, 내용행)이 만들어진다.

조회된 공지글(Notice 객체)이 몇 개일진 모른다. 

몇 개일진 모르지만 그 전부를 순차적으로 접근한다.

 

- 부트스트랩의 collapse 기능을 이용했다.

data-toggle 속성값으로 "collapse"를 가지는 행을 클릭하는 순간

그 요소의 data-target 속성에 적혀있는 아이디를 가지는 요소가 보여진다.

- 내용행의 id로 화면구현할 때는 정적으로 "notice3"을 줬었다. id는 겹쳐서는 안 된다.

내용행의 id로 각 Notice 객체의 글번호를 뽑아오기로 한다. 글번호는 pk라 중복되지 않는다. 

 

 

- 조회된 공지글이 있는 경우, 로그인되어 있고 '현재 로그인한 유저의 아이디'와 '공지사항 게시글 작성한 유저의 아이디'가 같으면 내용행에 '수정하기', '삭제하기' 버튼을 노출시킨다.

- "loginUser != null"라는 선행 조건이 없으면 비회원의 경우 null.getUserId().equals(n.getNoticeWriter())이 되어서 null pointer exception이 발생한다.

(앞의 선행조건이 거짓이면 &&는 뒤의 조건을 검사하지 않는다)

 

- '등록하기' 버튼도 먼저 "loginUser != null"로 선행조건이 있어야 null pointer exception이 발생하지 않는다.

 

 

 

 

 

 

 

- 서버 재시작, 새로고침 후 관리자로 로그인하고 공지사항 게시판을 가면 비회원이나 일반회원일 때와 달리

'등록하기' 버튼이 보여진다.

- 공지사항 작성자 id와 현재 로그인한 id가 같다면 '수정하기', '삭제하기' 버튼도 그 글에 보여진다.

- url은 현재 "localhost:8888/web/list.no"이다.