기초단계/JSP&Servlet

2023.04.10 JSP

춘핑이 2023. 4. 10. 18:52

newlec JSP

73. getNextNotice 메소드의 SQL 쿼리 작성하기

getNotice(int id)의 SQL은 다음과 같다.
String sql = "SELECT * FROM NOTICE WHERE ID=?";

Next와 Prev 다음과 이전이 필요하기 때문에 id가필요한 서브쿼리가 필요하다.
게시물이 3번이고 12357 이있다면 3을 얻고 5와 2를 얻어오는 순서로 가야한다.
등록일자를 기준으로 역정렬되있는 상태이다. 다음글을 얻으려면 큰놈을 골라야한다. 기준점이 regdate가 될것이다.
id를 얻고 redate가 큰녀석을 골라야한다.

MYSQL은 고민해서 다음과 같이 만들어보았다.
정렬해서 특정 아이디보다 크거중 하나면 그 다음글이다.

SELECT ID FROM NOTICE WHERE ID > 3 ORDER BY REGDATE ASC LIMIT 1;

74. getPrevNotice 메소드의 SQL 쿼리 작성하기

반대로 역정렬해서 특정 아이디보다 작은거 중 하나면 그 이전글이다.
SELECT ID FROM NOTICE WHERE ID < 3 ORDER BY REGDATE DESC LIMIT 1;

75. getNoticeList의 JDBC 코드 구현하기

컨트롤러의 JDBC 문들을 다 가져오고 컨트롤러에는 뷰단에 포워드하는 내용만 남겨놓는다.
sql문에 110 11~20 이런식으로 나와야하는데 이부분을 어떻게 할까?
앞부분은 등차수열이다. an = a + (n - 1)d 그러니 an = 0+(page-1)*10 MYSQL은 0부터시작이기때문이다.

뒷부분은 10 , 20 ,30 ,40 이니 page * 10
mysql은 그냥 10페이지씩하면된다.

그리고 검색이 될 수 있도록 넣어줘야한다.

WHERE TITLE LIKE ? string filed가 title일때 String query %a%하면a가 포함되는 제목이 찾아진다.
https://gent.tistory.com/401 %%참조하기.

string filed도 ?로 해서 넣으면되지않을까 싶은데 sql에 들어갈때 문자열이라 sql에 'title'이런식으로 들어가서 검색이 안된다.
+field+그래서 덧셈연산자로 넣어줘야한다.

public List<Notice> getNoticeList(String field, String query, int page) {
    List<Notice> list = new ArrayList<>();

    int startNum = (page - 1) * 10;
    int lastNum = 10;

    try {
        Class.forName("com.mysql.cj.jdbc.Driver");
        Connection con = DriverManager.getConnection(dbURL, dbID, dbPwd);

        String sql = "SELECT * FROM NOTICE WHERE " + field + " LIKE ? ORDER BY REGDATE DESC LIMIT ?, ?";
        PreparedStatement pstmt = con.prepareStatement(sql);
        pstmt.setString(1, "%" + query + "%");
        pstmt.setInt(2, startNum);
        pstmt.setInt(3, lastNum);

        ResultSet rs = pstmt.executeQuery();

        while (rs.next()) {
            Notice notice = new Notice();
            notice.setId(rs.getInt("ID"));
            notice.setWriterId(rs.getString("WRITER_ID"));
            notice.setTitle(rs.getString("TITLE"));
            notice.setRegdate(rs.getDate("REGDATE"));
            notice.setHit(rs.getInt("HIT"));
            list.add(notice);
        }

        rs.close();
        pstmt.close();
        con.close();

    } catch (Exception e) {
        e.printStackTrace();
    }
    return list;
}

76. NoticeService 클래스 완성하기

getNoticeCount() 게시물목록을 가져오는데 검색을 하는것과 아닌것을 추가해야한다.
Count함수를 사용해서 집계를하면된다.

SELECT COUNT(ID) COUNT FROM 테이블
컬럼 ID의 개수를 세는것는 것이다.
개수를 얻어서 rs.getInt("count");해야하는데 인자가 COUNT(ID)를 넣을 수없으니 별칭으로 COUNT넣어주는 것이 좋다.

public int getNoticeCount(String field, String query) {
    int count = 0;
    try {
        Class.forName("com.mysql.cj.jdbc.Driver");
        Connection con = DriverManager.getConnection(dbURL, dbID, dbPwd);
        String sql = "SELECT COUNT(ID) COUNT FROM NOTICE WHERE " + field + " LIKE ? ORDER BY REGDATE DESC";
        PreparedStatement pstmt = con.prepareStatement(sql);
        pstmt.setString(1, "%" + query + "%");
        ResultSet rs = pstmt.executeQuery();
        count = rs.getInt("COUNT");

        rs.close();
        pstmt.close();
        con.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
    return count;
}

getNotice() id로 들어간 글 내용을 보여주는 것이다.
notice 객체를 만들어서 리턴하면된다.
next, prev 도 비슷하다.

public Notice getNotice(int id) {

    Notice notice = null;
    try {
        Class.forName("com.mysql.cj.jdbc.Driver");
        Connection con = DriverManager.getConnection(dbURL, dbID, dbPwd);

        String sql = "SELECT * FROM NOTICE WHERE ID=?";
        PreparedStatement pstmt = con.prepareStatement(sql);
        pstmt.setInt(1, id);
        ResultSet rs = pstmt.executeQuery();

        if (rs.next()) {
            notice = new Notice();
            notice.setId(rs.getInt("ID"));
            notice.setWriterId(rs.getString("WRITER_ID"));
            notice.setTitle(rs.getString("TITLE"));
            notice.setRegdate(rs.getDate("REGDATE"));
            notice.setHit(rs.getInt("HIT"));
        }

        rs.close();
        pstmt.close();
        con.close();

    } catch (Exception e) {
        e.printStackTrace();
    }
    return notice;
}

public Notice getNextNotice(int id) {
    Notice notice = null;
    try {
        Class.forName("com.mysql.cj.jdbc.Driver");
        Connection con = DriverManager.getConnection(dbURL, dbID, dbPwd);

        String sql = "SELECT ID FROM NOTICE WHERE ID > 3 ORDER BY REGDATE ASC LIMIT 1";
        PreparedStatement pstmt = con.prepareStatement(sql);
        pstmt.setInt(1, id);
        ResultSet rs = pstmt.executeQuery();

        if (rs.next()) {
            notice = new Notice();
            notice.setId(rs.getInt("ID"));
            notice.setWriterId(rs.getString("WRITER_ID"));
            notice.setTitle(rs.getString("TITLE"));
            notice.setRegdate(rs.getDate("REGDATE"));
            notice.setHit(rs.getInt("HIT"));
        }

        rs.close();
        pstmt.close();
        con.close();

    } catch (Exception e) {
        e.printStackTrace();
    }
    return notice;
}

public Notice getPrevNotice(int id) {
    Notice notice = null;
    try {
        Class.forName("com.mysql.cj.jdbc.Driver");
        Connection con = DriverManager.getConnection(dbURL, dbID, dbPwd);

        String sql = "SELECT ID FROM NOTICE WHERE ID < 3 ORDER BY REGDATE DESC LIMIT 1";
        PreparedStatement pstmt = con.prepareStatement(sql);
        pstmt.setInt(1, id);
        ResultSet rs = pstmt.executeQuery();

        if (rs.next()) {
            notice = new Notice();
            notice.setId(rs.getInt("ID"));
            notice.setWriterId(rs.getString("WRITER_ID"));
            notice.setTitle(rs.getString("TITLE"));
            notice.setRegdate(rs.getDate("REGDATE"));
            notice.setHit(rs.getInt("HIT"));
        }

        rs.close();
        pstmt.close();
        con.close();

    } catch (Exception e) {
        e.printStackTrace();
    }
    return notice;
}

77. 목록 페이지에서 검색 기능 구현하기

검색을 구현해보자. 검색 칸에 값을 넣고 검색하면 field값과query값을 받아올 것이다.
String field 에는 두가지 값을 받을건데 select박스의 value를 받게 된다.

<select name="f">
    <option value="title">제목</option>
    <option value="writerId">작성자</option>
</select>

옵션의 타이틀과 아이디 이것들이 f라는 키로 전달이 된다.

list?f=title&q=a 이런식의 쿼리를 만들어서 서버에 요청을 할 것이다.
검색은 필수가 아니라 선택이기 때문에 field와 쿼리에 null이 올수도잇어서 기본값을 만들어줘야한다.

String field = "title";
if (field_ != null) {
    field = field_;
}

String query = "";
if (query_ != null) {
    query = query_;
}

그런데 검색 이후에 필드에 검색어가 사라지는 경우가 있다.
검색후에 상태가 그대로 유지해줘야한다.
뷰단에 param.q를 넣으면된다. 제목 작성자도 유지해줘야한다.

<option ${(param.f == "title")? "selected" : ""} value="title">제목</option>
<option ${(param.f == "writer_id")? "selected" :""} value="writer_id">작성자</option>
<input type="text" name="q" value="${param.q}"/>

@WebServlet("/notice/list")
public class NoticeListController extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String field_ = request.getParameter("f");
        String query_ = request.getParameter("q");;

        String field = "title";
        if (field_ != null) {
            field = field_;
        }

        String query = "";
        if (query_ != null) {
            query = query_;
        }

        int page = 1;

        NoticeService service = new NoticeService();
        List<Notice> list = service.getNoticeList(field, query, page);
        request.setAttribute("notice", list);

        RequestDispatcher dis = request.getRequestDispatcher("/WEB-INF/view/notice/list.jsp");
        dis.forward(request, response);
    }
}

78. 목록에서 페이징 구현하기

번호를 누르면 번호에 대한 값이 나오도록 해야한다.
페이지넘어가면 다음 목록 불러오고 검색시에도 검색한 목록의 다음 페이지 나오게 해줘야한다.
http://localhost:8080/notice/list?p=2&t=&q=
타이틀과 쿼리가 없어져서 상태값이 유지가 안됫다.
사실 검색하면 검색과 맞물려서 옵션, 검색어, 페이징이 같이가야하는 것이다.

페이저를 누를때 같이 적용되서 넘어가도록 해주자.

<li><a class="-text- orange bold" href="?p=${startNum+i}&f=${param.f}&q=${param.q">${i + startNum}</a></li>

String page_ = request.getParameter("p");
int page = 1;
if (page_ != null) {
    page = Integer.parseInt(page_);
}

List<Notice> list = service.getNoticeList(field, query, page);

페이지 값을 받아서 주면된다.

빈문자열일경우도 고려해서 null조건에 추가해줘야한다!!

더 추가해야하는 내용은 다음과 같다.
1.데이터가 없는 페이지는 안가게하기
2.현재번호가 하이라이트되기
3.끝번호까지만 나오게하기

79. Pager에서 현재 페이지 번호 처리

현재 페이지만 클릭된 모습으로 보여줘야한다.

1/5의 페이징에 앞 번호는 파라미터를 받아서 넣게 해주자
빈문자열이거나 null이라면 1을 넣어주자. ${(empty param.p)? "1" : param.p}

선택된것만 오랜지를 넣고싶다면 파라미터와 현재 번호가 같을때 색을 넣어주면 된다 역시 삼항연산을 사용하면된다.
면 p값이 없을때 기본값을 넣기 위해 ${(page == (startNum + i)?'orange':'')}다음과 같이 해주자.

80. Pager에서 마지막 번호 처리하기

마지막번호로 가면 더이상 못가야한다.
먼저 필요한것은 마지막번호가 무엇이냐 이다. 총 몇페이지 가있느냐

db에 레코드가 몇개가 있느냐에 따라 마지막번호를 정할 수있다.
그래서 컨트롤러에서 서비스로 레코드 개수가 몇개인지 받아와야한다.

100개가있다하면 10개씩 보여주고 있으니 100/10이다.
검색상태에서도 글 갯수가 필요하니 getNoticeCount(field, query)함수를 사용해서 레코드 수가 총 몇개인지 가져오자.

그런데 단순하게 count/10 을 햇을때 나머지가 남으면 문제가생긴다.
그래서 Math클래스의 ceil을 사용해보자.
10.2->11 Math.ceil(10.2) / Math.floor 10.2->10
우리는 소수점없애고 위로 보낼것이니 실링하자.

정적메소드를 EL에서 사용할 수 있다. 또한 11.0이 되기때문에 소수점을 잘라내는 함수 사용해야한다.

그런데 함수를 사용하는 것은 별로 좋아보이지 않아서 나는 if문을 사용했다.
jsp에서 숫자를 나누면 정수가 아니기 때문에 숫자로 포맷해줘야한다.

<fmt:parseNumber var="result" value="${count / 10}" integerOnly="true"/>

<c:if test="${count / 10 == 0}">
    <c:set var="lastNum" value="${count / 10 }" />
</c:if>

<c:if test="${count / 10 != 0}">
    <c:set var="lastNum" value="${(count / 10) + 1}" />
</c:if>

(startNum+i) <= lastNum일때만 번호가 나오게해서 없으면 페이지 자체가 안나오게해버리자.

<ul class="-list- center">
    <c:forEach var="i" begin="0" end="4">
        <c:if test="${(startNum+i) <= lastNum }">
            <li><a class="-text- ${(page == (startNum + i)?'orange':'')} bold" href="?p=${startNum+i}&f=${param.f}&q=${param.q}">${i + startNum}</a></li>
        </c:if>
    </c:forEach>
</ul>

81. 자세한 페이지 수정하기

detail은 서비스레이어를 나눈게아니라 컨트롤러자체가 jdbc자체를 받아왓다
이번엔 컨트롤러에서 서비스레이어를 사용해보자.
NoticeDetailController에서 getNotice(id)를 사용해보자

그런데 사용자는 전혀달라진게 없는데 굳이 나눠야하나?
사용자를 위한게 아니라 개발자를 위한 방식이다. 더 편한 부분으로 만드는게 낫다. 더 좋은 부분으로 작성하자.
여러사람이 나눈다고 생각햇을때 이것을 다른사람이 만든다면? 자기 분야만 만들면되니 훨씬 편하게 될것이다.
소규모에 컨트롤러 몇개없으면 그냥 합치는게 낫다.

@WebServlet("/notice/detail")
public class NoticeDetailController extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        int id = Integer.parseInt(request.getParameter("id"));

        NoticeService service = new NoticeService();
        Notice notice = service.getNotice(id);

        request.setAttribute("notice", notice);
        request.getRequestDispatcher("/WEB-INF/view/notice/detail.jsp").forward(request, response);
    }
}

82. 목록에 댓글 수를 포함하려면?

여기까지는 가장 기본적인 스택이다.
제목53(3) 이런식으로 제목 옆에 댓글수를 달아주자.
먼저 디테일에 댓글을 해줘야한다.
하지만 이런 틀이 없으니 데이터베이스에다가만 추가해보자.
데이터베이스의 COMMENT 테이블에 댓글 추가하기

일반적으로 이렇게 테이블하나를 목록화하는것보다 묶어서 하는 것이 일반적이다.

83. 댓글 수를 포함하기 위한 쿼리 문제

notice와 comment가 보여져야하니 두개의 테이블의 join이 필요하다
select * from notice N
INNER JOIN "COMMENT" C ON N.ID = C.NOTICE_ID
order by N.regdate desc;
INNER JOIN 은 자식이 테이블이 기준이되서 자식이 필요로하는 부모테이블의 컬럼내용을 가져온다.

그래서 left outer join사용해야한다.
select * from notice N
left JOIN "COMMENT" C ON N.ID = C.NOTICE_ID
order by N.regdate desc;

그런데 자식때문에 부모가 반복되니까 noitce가 중점이 되서 나오길원한다.
자식은 집계할때만 사용하기때문에 모든게 나올필요가없다.

SELECT N.ID, N.TITLE, N.WRITER_ID, N.REGDATE, N.HIT, N.FILES, COUNT(C.ID) CMT_COUNT from notice N
LEFT JOIN "COMMENT" C ON N.ID = C.NOTICE_ID
GROUP BY N.ID, N.TITLE, N.WRITER_ID, N.REGDATE, N.HIT, N.FILES
order by N.regdate desc;

그루핑을 해보자. 부모테이블은 전부다나오게(컨텐츠는 용량이커서 다안나오고 join문에서 잘려서 빼줘야한다.)
자식테이블은 count()를 사용해서 갯수만 나오게 하자.

이 SQL문을 지난 getNoticeList()에 집어 넣었던 SQL문과 합쳐야하는데
매우 복잡해진 sql문이 되버린다. 이를해결해야한다.

84. 목록의 댓글 수를 위한 View 생성하기

서브쿼리가 여러번 들어가게 되면 매우 복잡해진다. 그래서 차선책을 사용해야한다.
그것은 바로 조인문을 결과집합으로 하는 NOTICE_VIEW를 만들어서 사용하면된다.
orderby는 본 sql문에서 하고있기 때문에 빼는게 좋다.

CREATE VIEW NOTICE_VIEW
AS
SELECT N.ID, N.TITLE, N.WRITER_ID, N.REGDATE, N.HIT, N.FILES, COUNT(C.ID) CMT_COUNT from notice N
LEFT JOIN COMMENT C ON N.ID = C.NOTICE_ID
GROUP BY N.ID, N.TITLE, N.WRITER_ID, N.REGDATE, N.HIT, N.FILES;

VIEW 는 테이블을 대신함으로써 데이터가 필터링된다거나 정렬된다고 할때 뷰에 포함안시키는 게 바람직하다
원본데이터를 추가하는 데 컬럼을 추가하는 것으로 만드는것이다.
뷰는 업무에 따라 여러 테이블들을 조인해서 만들어 놓는것이다.

String sql = "SELECT * FROM NOTICE_VIEW WHERE " + field + " LIKE ? ORDER BY REGDATE DESC LIMIT ?, ?";
를 해서 보여주게할 수 있다.

그냥 Notice생성자를 추가하는게아니라 NoticeView라는 클래스를 만들고 Notice를 상속받게하는 것이 좋다.
cmtCount만 게터세터를 추가한 새로운 것을 만들면된다.

public class NoticeView extends Notice {
    private int cmtCount;

    public NoticeView() {
    }

    public NoticeView(int id, String title, Date regdate, String writerId, int hit, String files, String content,
            int cmtCount) {
        super(id, title, regdate, writerId, hit, files, "");
        this.cmtCount = cmtCount;
    }

    public int getCmtCount() {
        return cmtCount;
    }

    public void setCmtCount(int cmtCount) {
        this.cmtCount = cmtCount;
    }
}

public List<NoticeView> getNoticeViewList(String field, String query, int page) {
    List<NoticeView> list = new ArrayList<>();

    int startNum = (page - 1) * 10;
    int lastNum = 10;

    try {
        Class.forName("com.mysql.cj.jdbc.Driver");
        Connection con = DriverManager.getConnection(dbURL, dbID, dbPwd);

        String sql = "SELECT * FROM NOTICE_VIEW WHERE " + field + " LIKE ? ORDER BY REGDATE DESC LIMIT ?, ?";
        PreparedStatement pstmt = con.prepareStatement(sql);
        pstmt.setString(1, "%" + query + "%");
        pstmt.setInt(2, startNum);
        pstmt.setInt(3, lastNum);

        ResultSet rs = pstmt.executeQuery();

        while (rs.next()) {
            NoticeView notice = new NoticeView();
            notice.setId(rs.getInt("ID"));
            notice.setWriterId(rs.getString("WRITER_ID"));
            notice.setTitle(rs.getString("TITLE"));
            notice.setRegdate(rs.getDate("REGDATE"));
            notice.setHit(rs.getInt("HIT"));
            notice.setCmtCount(rs.getInt("CMT_COUNT"));
            list.add(notice);
        }

        rs.close();
        pstmt.close();
        con.close();

    } catch (Exception e) {
        e.printStackTrace();
    }
    return list;
}

85. Index 페이지 추가하기

사용자가 보는 공지목록과 공지내용을 만들었다.
이제 관리자 페이지로 해야한다. 그전에 관리자든 일반유저든 기본으로 보는내용 = 인덱스페이지이다.

인덱스.jsp만들기
인덱스 컨트롤러가 컨트롤러 패키지에 다들어갔다.
사실 여러개의 컨트롤러가 다 만드는게 아니다.
controller.notice이런식으로 패키지를 세분화 해주자.

86. Admin 페이지를 위한 서비스 목록 추가하기

관리자를 윙한 admin 페이지를 위한 서비스 목록 추가해보자
공개를위한 버튼 삭제를 위한 버튼이 따로 있다.
1.일괄공개요청 pubNoitceAll(ids)
2.일괄삭제요청 removeNoticeAll(ids)
3.글쓰기-글등록 공지등록요청 insertNotice(notice)
4.자세한페이지 수정 삭제등 버튼 공지에 요청 deleteNotice(id)
5.공지수정요청 수정페이지에서 요청 updateNotice(noitce)
6.인덱스페이지에서의 페이지 요청 getNoticeNewestList()
insert delete등은 실행한 후에 반환값으로 몇개에 영향을 주었는지를 반환값으로 받도록 한다.

87. admin/index 페이지 추가하기

관리자 홈 공지사항 메뉴가있다. 일반적으로는 대시보드 형태로 집계된 데이터를 가져온다
프로젝트에 같은 컨트롤러가 있어서 이상하게 url이 입력이된다. 클래스자체가 요청된다.
정상적으로 url매핑을 못한다.
어쩔수 없이 직접 입력해야한다.

88. admin/notice/list 페이지 추가하기

일반유저가 봣던거랑 다른것은 관리를 위한 것들이 있다.

89. 다중 선택 값 POST 하기

관리자페이지 공개 삭제 등 다중값보내기다중값이 두종류잇을때 구분하는 방법을 알아야한다.
값을 전달하는 방법 24강 다중값보내기 참조하자

<td><input type="checkbox" name="open-id" value="${notice.id }"></td>
<td><input type="checkbox" name="del-id" value="${notice.id }"></td>

open-일괄공개 버튼 del-일괄삭제 버튼연동을 어떻게 하는가?
체크박스는 이름은 open-id로 가는데 전달되는 값이 없는데 전달할 값은 id로 선택해서 지우니 id로 하자.
del도 마찬가지로 값을 id로 받자.

<input type="submit" class="btn-text btn-default" value="일괄공개">
<input type="submit" class="btn-text btn-default" value="일괄삭제">

값이 전달되기 위해서는 submit버튼이 checkbox가 같은 폼안에 있어야한다.

<form action="list" method="post"> post요청을 하자.
같은 폼안에 있으니 어떤 것을 체크하던 submit버튼을 누르면 다 전달이된다.
그래서 어떤 값을 처리할것인지 구분해줘야한다.
개발자도구에서 이름이 잘 지정되고 있는지를 확인해주자.
체크를 하고 일괄삭제 버튼을 누르면 체크한 것들이 전달되는 것을 알 수 있다.

그런데 서버쪽에선 이것들을 어떻게 받을 것인가?
doPost메소드로 받으면된다.

String[] openIds = request.getParameterValues("open-id");
String[] delIds = request.getParameterValues("del-id");

90. 다중 submit 요청 구분하기

체크된내용들이 다 같게 들어갔다. 이를 어떻게 서버쪽에서 구분할까?
submit 밸류를 버튼의 값으로 사용한다. 일반적인 입력버튼과는 다르게 눌리지 않은 버튼은 가지 않는다.
그래서 키를 같게 name="cmd" 로해도 구분을 할 수 있게 된다.

<input type="submit" class="btn-text btn-default" name="cmd" value="일괄공개">
<input type="submit" class="btn-text btn-default" name="cmd" value="일괄삭제">

String cmd = request.getParameter("cmd");
switch(cmd){
case "일괄공개":
break;
case "일괄삭제":
break;
}

버튼 요청을 처리 하는데 실제로 공개/비공개를위한 컬럼이 필요하다.
이러면 db수정, entity수정, 몇가지 수정 스트레스 받기위해 알아보자.
먼저 컬럼을 추가해보자
PUB컬럼 추가 TRUE FALSE값을 받기위해 NUMBER 크기는 1 기본값은 0(FALSE)