Coding History

국비 지원 IT(웹앱개발) 취업반 강의 50일차 (Spring)

BlackBirdIT 2024. 8. 18. 15:02

하던 것을 이어서 modify 기능부터 보자.

    @RequestMapping("/usr/article/modify")
    public String doModify(Model model, HttpSession session, int id, String title, String body) {
        // 로그인 상태 확인
        boolean isLogined = false;
        int loginedMemberId = 0;

        if (session.getAttribute("loginedMemberId") != null) {
            isLogined = true;
            loginedMemberId = (int) session.getAttribute("loginedMemberId");
        }
        if (isLogined == false) {
//            return ResultData.from("F-A", "로그인 하고 써");
        }

        Article article = articleService.getArticleById(id);

        if (article == null) {
//            return ResultData.from("F-1", Ut.f("%d번 게시물은 없습니다.", id));
        }
        if (article.getMemberId() != loginedMemberId)
//            return ResultData.from("F-B", "게시물에 대한 권한이 없습니다.");

        articleService.modifyArticle(id, title, body);

        article = articleService.getArticleById(id);// 수정 후 데이터 새로 가져오기.
        return "/usr/article/modify";
    }

일단은 이렇게 고쳐서 jsp로 통신이 가능하게 만들었다.

modify와 delete에 로그인 체크와 권한 체크 중복코드가 보여서 service 패키지에 AuthService class를 선언해서 중복코드를 제거해줬다.

public class AuthService {

    public String checkLogin(HttpSession session, Model model) {
        if (session.getAttribute("loginedMemberId") == null) {
            model.addAttribute("errorMessage", "로그인 먼저 해주세요.");
            return "usr/member/login";
        }
        return null;
    }

    public String checkArticlePermission(HttpSession session, Model model, Article article) {
        int loginedMemberId = (int) session.getAttribute("loginedMemberId");
        if (article == null) {
            model.addAttribute("errorMessage", "해당 게시물은 없습니다.");
            return "usr/article/list";
        }
        if (article.getMemberId() != loginedMemberId) {
            model.addAttribute("errorMessage", "게시물에 대한 권한이 없습니다.");
            return "usr/article/list";
        }
        return null;
    }

}
    @RequestMapping("/usr/article/modify")
    public String doModify(Model model, HttpSession session, int id, String title, String body) {
        // 로그인 상태 확인
        String loginCheck = authService.checkLogin(session, model);
        if (loginCheck != null) {
            return loginCheck;
        }

        // 게시물 가져오기 및 권한 확인
        Article article = articleService.getArticleById(id);
        String permissionCheck = authService.checkArticlePermission(session, model, article);
        if (permissionCheck != null) {
            return permissionCheck;
        }

        articleService.modifyArticle(id, title, body);

        article = articleService.getArticleById(id);// 수정 후 데이터 새로 가져오기.
        return "/usr/article/modify";
    }

이렇게 하면서 강사님이 만드시는 걸 봤는데 그냥 강사님 코드를 따라서 해야될 것 같다. 내 능력밖으로 고쳐야되는게 산더미라 어디서부터 뭘 손대야할지 감도 잡히지 않는다.

일단 글로 나열하자면 디테일 페이지에서 수정과 삭제 버튼이 존재하는데 로그인한 유저의 권한이 있을 시에만 표시되게 하고, 나머지의 경우에는 버튼 자체가 보이지 않게 해야한다, 그걸 service에서 getForPrintArticle, updateForPrintData, userCanModify등을 생성해 처리하고 JSP에는 c:if문을 사용한다. 삭제도 같은 메커니즘. 이라 원래 내 코드로 어떻게든 비슷한 방식으로 써먹어보려고 했으나 약 두시간을 날리고 포기한게 현 상태다.

코드를 보자.

    @RequestMapping("/usr/article/doModify")
    @ResponseBody
    public ResultData<Article> doModify(HttpSession httpSession, int id, String title, String body) {

        boolean isLogined = false;
        int loginedMemberId = 0;

        if (httpSession.getAttribute("loginedMemberId") != null) {
            isLogined = true;
            loginedMemberId = (int) httpSession.getAttribute("loginedMemberId");
        }

        if (isLogined == false) {
            return ResultData.from("F-A", "로그인 하고 써");
        }

        Article article = articleService.getArticleById(id);

        if (article == null) {
            return ResultData.from("F-1", Ut.f("%d번 게시글은 없습니다", id));
        }

        ResultData userCanModifyRd = articleService.userCanModify(loginedMemberId, article);

        if (userCanModifyRd.isFail()) {
            return userCanModifyRd;
        }

        if (userCanModifyRd.isSuccess()) {
            articleService.modifyArticle(id, title, body);
        }

        article = articleService.getArticleById(id);

        return ResultData.from(userCanModifyRd.getResultCode(), userCanModifyRd.getMsg(), "수정 된 게시글", article);
    }

    @RequestMapping("/usr/article/doDelete")
    @ResponseBody
    public String doDelete(HttpSession httpSession, int id) {

        boolean isLogined = false;
        int loginedMemberId = 0;

        if (httpSession.getAttribute("loginedMemberId") != null) {
            isLogined = true;
            loginedMemberId = (int) httpSession.getAttribute("loginedMemberId");
        }

        if (isLogined == false) {
//            return ResultData.from("F-A", "로그인 하고 써");
            return Ut.jsReplace("F-A", "로그인 후 이용하세요", "../member/login");
        }

        Article article = articleService.getArticleById(id);

        if (article == null) {
//            return ResultData.from("F-1", Ut.f("%d번 게시글은 없습니다", id), "입력한 id", id);
            return Ut.jsHistoryBack("F-1", Ut.f("%d번 게시글은 없습니다", id));
        }

        ResultData userCanDeleteRd = articleService.userCanDelete(loginedMemberId, article);

        if (userCanDeleteRd.isFail()) {
            return Ut.jsHistoryBack(userCanDeleteRd.getResultCode(), userCanDeleteRd.getMsg());
        }

        if (userCanDeleteRd.isSuccess()) {
            articleService.deleteArticle(id);
        }

        return Ut.jsReplace(userCanDeleteRd.getResultCode(), userCanDeleteRd.getMsg(), "../article/list");

여기가 컨트롤러.

    public Article getForPrintArticle(int loginedMemberId, int id) {

        Article article = articleRepository.getForPrintArticle(id);

        controlForPrintData(loginedMemberId, article);

        return article;
    }


    private void controlForPrintData(int loginedMemberId, Article article) {
        if (article == null) {
            return;
        }
        ResultData userCanModifyRd = userCanModify(loginedMemberId, article);
        article.setUserCanModify(userCanModifyRd.isSuccess());

        ResultData userCanDeleteRd = userCanDelete(loginedMemberId, article);
        article.setUserCanDelete(userCanModifyRd.isSuccess());
    }

    public ResultData userCanDelete(int loginedMemberId, Article article) {
        if (article.getMemberId() != loginedMemberId) {
            return ResultData.from("F-2", Ut.f("%d번 게시글에 대한 삭제 권한이 없습니다", article.getId()));
        }
        return ResultData.from("S-1", Ut.f("%d번 게시글을 삭제했습니다", article.getId()));
    }

    public ResultData userCanModify(int loginedMemberId, Article article) {
        if (article.getMemberId() != loginedMemberId) {
            return ResultData.from("F-2", Ut.f("%d번 게시글에 대한 수정 권한이 없습니다", article.getId()));
        }
        return ResultData.from("S-1", Ut.f("%d번 게시글을 수정했습니다", article.getId()), "수정된 게시글", article);
    }

여기가 서비스.

    @Select("""
            SELECT a.* , m.nickname AS extra__writer
            FROM article AS a
            INNER JOIN `member` AS m
            ON a.memberId = m.id
            WHERE a.id = #{id}
                """)
    public Article getForPrintArticle(int id);

여기가 유틸.

일단 구현 된 것을 설명해보자면, 위의 서술한 내용이 구현되어있다. 방식이 어떻게 되어있냐면 유저의 로그인 상태를 구분하게끔, 또 유저가 해당 게시글의 권한을 갖고 있는지를 userCanModify, userCanDelete로 판단하고 getForPrintArticle을 활용해서 유저의 닉네임을 가져오고 있다.


list에서도 잘 가져오고,

detail에서도 잘 가져오는 모습

여기서 눈 여겨 봐야될 것이 뭐냐면 난 로그인을 한적이 없기 때문에 수정과 삭제 버튼이 존재하지 않는다는 것 로그인을 해서 권한이 있는 게시물로 가면 이런 모습이다.

김철수로 로그인 했고 수정과 삭제 버튼이 잘 보인다 JSP를 까보면

    <div class="actions">
        <c:if test="${article.userCanModify}">
            <a href="../article/modify?id=${article.id}" class="btn">게시물 수정</a>
        </c:if>
        <c:if test="${article.userCanDelete}">
        <a href="../article/doDelete?id=${article.id}" class="btn">게시물 삭제</a>
        </c:if>
    </div>

버튼에 <c:if>로 조건문을 감싸놓은 것을 볼 수 있다. 솔직히 말해서 이 때 까지는 왜 delete와 modify를 나눠서 하는지 몰랐지만 기능을 구분한 이유가 있었다.

delete쪽 컨트롤러 코드를 잘 살펴보면 Ut로 떡칠이 된 것을 볼 수 있는데, Ut을 까보자.

    public static String jsReplace(String resultCode, String msg, String replaceUri) {

        if (resultCode == null) {
            resultCode = "";
        }
        if (msg == null) {
            msg = "";
        }
        if (replaceUri == null) {
            replaceUri = "/";
        }

        String resultMsg = resultCode + "/" + msg;

        return Ut.f("""
                    <script>
                        let resultMsg = '%s'.trim();

                        if(resultMsg.length > 0){
                            alert(resultMsg);
                        }
                        location.replace('%s');
                    </script>
                """, resultMsg, replaceUri);
    }

    public static String jsHistoryBack(String resultCode, String msg) {
        if (resultCode == null) {
            resultCode = "";
        }
        if (msg == null) {
            msg = "";
        }

        String resultMsg = resultCode + "/" + msg;

        return Ut.f("""
                    <script>
                        let resultMsg = '%s'.trim();

                        if(resultMsg.length > 0){
                            alert(resultMsg);
                        }
                        history.back();
                    </script>
                """, resultMsg);
    }

해당하는 두개의 메서드를 만들어두었고 삭제와 수정 상황에서 받는 String 값이 다르기 때문이다.(오류 메세지 등등이) 잘 보면 스크립트 태그로 감싸서 JS코드로 상황에 맞게 알림창이 뜨게끔 만들었다.

이거 보고 감탄했다 JAVA안에서 별 다른 코드를 다 가져오는구나 싶었다.


실제로 삭제를 눌러보면 해당 메서드가 잘 작동하는 것을 볼 수 있다.

다음은 중복코드를 처리했다.

    @RequestMapping("/usr/article/detail")
    public String showDetail(HttpServletRequest req, Model model, int id) {

        Rq rq = new Rq(req);

        Article article = articleService.getForPrintArticle(rq.getLoginedMemberId(), id);

        model.addAttribute("article", article);

        return "usr/article/detail";
    }

    @RequestMapping("/usr/article/doModify")
    @ResponseBody
    public ResultData<Article> doModify(HttpServletRequest req, int id, String title, String body) {

        Rq rq = new Rq(req);

        Article article = articleService.getArticleById(id);

        if (article == null) {
            return ResultData.from("F-1", Ut.f("%d번 게시글은 없습니다", id));
        }

        ResultData userCanModifyRd = articleService.userCanModify(rq.getLoginedMemberId(), article);

        if (userCanModifyRd.isFail()) {
            return userCanModifyRd;
        }

        if (userCanModifyRd.isSuccess()) {
            articleService.modifyArticle(id, title, body);
        }

        article = articleService.getArticleById(id);

        return ResultData.from(userCanModifyRd.getResultCode(), userCanModifyRd.getMsg(), "수정 된 게시글", article);
    }

    @RequestMapping("/usr/article/doDelete")
    @ResponseBody
    public String doDelete(HttpServletRequest req, int id) {

        Rq rq = new Rq(req);

        if (rq.isLogined() == false) {
//            return ResultData.from("F-A", "로그인 하고 써");
            return Ut.jsReplace("F-A", "로그인 후 이용하세요", "../member/login");
        }

        Article article = articleService.getArticleById(id);

        if (article == null) {
//            return ResultData.from("F-1", Ut.f("%d번 게시글은 없습니다", id), "입력한 id", id);
            return Ut.jsHistoryBack("F-1", Ut.f("%d번 게시글은 없습니다", id));
        }

        ResultData userCanDeleteRd = articleService.userCanDelete(rq.getLoginedMemberId(), article);

        if (userCanDeleteRd.isFail()) {
            return Ut.jsHistoryBack(userCanDeleteRd.getResultCode(), userCanDeleteRd.getMsg());
        }

        if (userCanDeleteRd.isSuccess()) {
            articleService.deleteArticle(id);
        }

        return Ut.jsReplace(userCanDeleteRd.getResultCode(), userCanDeleteRd.getMsg(), "../article/list");
    }
    @RequestMapping("/usr/article/doWrite")
    @ResponseBody
    public ResultData doWrite(HttpServletRequest req, String title, String body) {

        Rq rq = new Rq(req);

        if (rq.isLogined() == false) {
            return ResultData.from("F-A", "로그인 하고 써");
        }

        if (Ut.isEmptyOrNull(title)) {
            return ResultData.from("F-1", "제목을 입력해주세요");
        }
        if (Ut.isEmptyOrNull(body)) {
            return ResultData.from("F-2", "내용을 입력해주세요");
        }

        ResultData writeArticleRd = articleService.writeArticle(rq.getLoginedMemberId(), title, body);

        int id = (int) writeArticleRd.getData1();

        Article article = articleService.getArticleById(id);

        return ResultData.from(writeArticleRd.getResultCode(), writeArticleRd.getMsg(), "생성된 게시글", article);
    }

Rq 클래스를 도입했고,

package com.example.demo.vo;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import lombok.Getter;

public class Rq {

    @Getter
    private boolean isLogined = false;
    @Getter
    private int loginedMemberId = 0;

    public Rq(HttpServletRequest req) {

        HttpSession httpSession = req.getSession();

        if (httpSession.getAttribute("loginedMemberId") != null) {
            isLogined = true;
            loginedMemberId = (int) httpSession.getAttribute("loginedMemberId");
        }
    }    
}

모양은 이렇다. 그런데 이러면 Rq가 또 중복이 된다고 새로운걸 도입한다고 하셨는데 인터셉터라는 것이다.
사전 체크 기능을 만든다고 보면 된다고 하셨다. 인터셉터는 컨트롤러에 들어오는 요청을 가로채서 체크 후에 돌려보내는 기능이라고 한다.

package com.example.demo.interceptor;

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@Component
public class BeforeActionInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
        return HandlerInterceptor.super.preHandle(req, resp, handler);
    }

}
package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import com.example.demo.interceptor.BeforeActionInterceptor;

@Configuration
public class MyWebMVCConfigurer implements WebMvcConfigurer {

    // BeforeActionInterceptor 불러오기(연결)
    @Autowired
    BeforeActionInterceptor beforeActionInterceptor;

    // 인터셉터 등록(적용)
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(beforeActionInterceptor).addPathPatterns("/**").excludePathPatterns("/resource/**")
                .excludePathPatterns("/error");
    }

}

그래서 이렇게 클래스 두개를 만들어줬다. 솔직히 이렇게 봐서는 뭐하는앤지 잘 모르겠다.

여튼 Rq를 써야하기 때문에 인터셉터에 넣어준다.

@Component
public class BeforeActionInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {

        Rq rq = new Rq(req);

        req.setAttribute("rq", rq);

        return HandlerInterceptor.super.preHandle(req, resp, handler);
    }

}
Rq rq = new Rq(req);

그리고 원래는 계속 new로 새로운 객체를 생성하면서 쓰던 코드를

Rq rq = (Rq) req.getAttribute("rq");

인터셉터에서 한번만 생성하도록 만든다.

다음으로 로그인 체크 기능이 중복이여서 다시 인터셉터를 사용해서 처리했다.

@Component
public class NeedLoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
        Rq rq = (Rq) req.getAttribute("rq");

        if (!rq.isLogined()) {
            System.err.println("==================로그인 하고 써====================");
//            resp.getWriter().append("<script>~~~~");

            rq.printHistoryBack("로그인 후 이용해주세요.");

            return false;

        }

        return HandlerInterceptor.super.preHandle(req, resp, handler);
    }
}
@Configuration
public class MyWebMVCConfigurer implements WebMvcConfigurer {

    // BeforeActionInterceptor 불러오기(연결)
    @Autowired
    BeforeActionInterceptor beforeActionInterceptor;
    // NeedLoginInterceptor 불러오기(연결)
    @Autowired
    NeedLoginInterceptor needLoginInterceptor;

    // 인터셉터 등록(적용)
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(beforeActionInterceptor).addPathPatterns("/**").excludePathPatterns("/resource/**")
                .excludePathPatterns("/error");

        registry.addInterceptor(needLoginInterceptor).addPathPatterns("/usr/article/write")
                .addPathPatterns("/usr/article/doWrite").addPathPatterns("/usr/article/modify")
                .addPathPatterns("/usr/article/doModify").addPathPatterns("/usr/article/doDelete")
                .addPathPatterns("/usr/member/doLogout");
    }

}
public class Rq {

    @Getter
    private boolean isLogined = false;
    @Getter
    private int loginedMemberId = 0;

    private HttpServletRequest req;
    private HttpServletResponse resp;

    public Rq(HttpServletRequest req, HttpServletResponse resp) {
        this.req = req;
        this.resp = resp;

        HttpSession httpSession = req.getSession();

        if (httpSession.getAttribute("loginedMemberId") != null) {
            isLogined = true;
            loginedMemberId = (int) httpSession.getAttribute("loginedMemberId");
        }
    }

    public void printHistoryBack(String msg) throws IOException {
        resp.setContentType("text/html; charset=UTF-8");
        println("<script>");
        if (!Ut.isEmpty(msg)) {
            println("alert('" + msg + "');");
        }
        println("history.back();");
        println("</script>");
    }

    private void println(String str) {
        print(str + "\n");
    }

    private void print(String str) {
        try {
            resp.getWriter().append(str);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

인터셉터로 다른 메서드를 만들고 경로 지정 후, Rq에서 메세지출력으로 처리. 솔직히 이해가 잘 안된다. 이건 시간이 좀 걸릴 것 같다.

암튼 이렇게 기반을 다져두고 로그인 로그아웃을 만들었다.

    @RequestMapping("/usr/member/login")
    public String showLogin() {
        return "/usr/member/login";
    }

    @RequestMapping("/usr/member/doLogin")
    @ResponseBody
    public String doLogin(HttpSession session, String loginId, String loginPw) {
        // 이미 로그인된 상태인지 확인
        if (session.getAttribute("loginUser") != null) {
            return Ut.jsHistoryBack("F-0", "이미 로그인된 상태입니다.");
        }

        if (Ut.isEmptyOrNull(loginId)) 
            return Ut.jsHistoryBack("F-1", "아이디를 입력해주세요.");


        if (Ut.isEmptyOrNull(loginPw)) 
            return Ut.jsHistoryBack("F-2", "비밀번호를 입력해주세요.");

        Member member = memberService.getMemberByLoginId(loginId);

        if (member == null) {
            return Ut.jsHistoryBack("F-3", Ut.f("%s는(은) 존재 하지않습니다.", loginId));
        }

        if (member.getLoginPw().equals(loginPw) == false) {
            return Ut.jsHistoryBack("F-4", Ut.f("비밀번호가 틀렸습니다."));
        }

        session.setAttribute("loginedMemberId", member.getId());

        return Ut.jsReplace("S-1", Ut.f("%s님 환영합니다", member.getNickname()), " / ");
    }

    @RequestMapping("/usr/member/doLogout")
    public String doLogout(HttpSession session, HttpServletRequest req) {
        // 로그인 상태 확인
        Rq rq = (Rq) req.getAttribute("rq");


        // 로그아웃 처리
        session.invalidate();

        return "redirect:/usr/home/main";
    }

기존 코드를 통신 가능하게 고쳐주고.

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>

<c:set var="pageTitle" value="로그인"></c:set>
<%@ include file="../common/head.jspf"%>

<hr />

<section class="">
    <div class="">
        <form action="../member/doLogin" method="POST">
        <table class="login-table">
            <tbody>
                <tr>
                    <th>아이디</th>
                    <td><input name="loginId" autocomplete="off" type="text"
                        placeholder="아이디를 입력해" /></td>
                </tr>
                <tr>
                    <th>비밀번호</th>
                    <td><input name="loginPw"
                        autocomplete="off" type="text" placeholder="비밀번호를 입력해" /></td>

                </tr>
                <tr>
                    <th></th>
                    <td><input type="submit"
                        value="로그인" /></td>

                </tr>
            </tbody>
        </table>
        </form>
    </div>
</section>

</body>
</html>

화면 구성을 만들고 css를 로그인 화면에 맞게 짜줬다.

/* 로그인 폼 스타일 */
form {
    width: 300px; 
    margin: 0 auto;
    padding: 20px;
    background-color: #1e1e1e;
    border-radius: 5px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
}

table.login-table {
    width: 100%;
}

.login-table th {
    text-align: left;
    padding-bottom: 10px;
    color: #c7c7c7;
}

.login-table td {
    padding-bottom: 10px;
}

input[type="text"], input[type="password"] {
    width: calc(100% - 10px); 
    padding: 8px; 
    border: 1px solid #333;
    border-radius: 3px;
    background-color: #2c2c2c;
    color: #e0e0e0;
}

input[type="submit"] {
    width: calc(100% - 10px); 
    padding: 8px; 
    background-color: #2a9d8f;
    color: #fff;
    border: none;
    border-radius: 3px;
    cursor: pointer;
    font-size: 16px;
}

input[type="submit"]:hover {
    background-color: #219080;
}

/* 섹션 스타일 */
section {
    margin-top: 20px;
    padding: 20px;
}

/* 폼 내부의 추가적인 스타일 */
form div {
    margin-bottom: 15px;
}

form label {
    color: #c7c7c7;
}

form input {
    padding: 8px; /* 패딩 조정 */
    width: 100%;
    box-sizing: border-box;
    border: 1px solid #444;
    background-color: #1e1e1e;
    color: #e0e0e0;
    border-radius: 4px;
}

form input[type="submit"] {
    background-color: #2a9d8f;
    color: #ffffff;
    cursor: pointer;
}

form input[type="submit"]:hover {
    background-color: #219080;
}

이건 그냥 기존에 만들어 둔 것을 토대로 chatGPT한테 시키고 내가 손 볼 것들만 조금씩 고쳐서 만들었다.

그래서 로그인 로그아웃을 해보면 경로 지정한 대로 잘 이동하고 잘 작동하는지 확인을 해보자.

아 참 헤드에 클릭해서 이동할 수 있게 만들었다. 이 코드부터 보고 가자.

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" type="text/css"
    href="${pageContext.request.contextPath}/resource/common.css">
<script src="/resource/common.js" defer="defer"></script>
<!-- 제이쿼리 -->
<script
    src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>

<!-- 폰트어썸 -->
<link rel="stylesheet"
    href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
<!-- 폰트어썸 FREE 아이콘 리스트 : https://fontawesome.com/v5.15/icons?d=gallery&p=2&m=free -->

<!-- 테일윈드 -->
<link rel="stylesheet"
    href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.1.4/tailwind.min.css">
<!-- 테일윈드 치트시트 : https://nerdcave.com/tailwind-cheat-sheet -->
<title>${pageTitle}</title>
</head>
<body>
    <header>
        <div class="flex h-20 mx-auto items-center text-3xl">
            <a href="/">LOGO</a>
            <div class="flex-grow"></div>
            <ul class="flex space-x-6">
                <li><a class="hover:underline" href="/">HOME</a></li>
                <li><a class="hover:underline" href="../article/list">LIST</a></li>
                <c:if test="${!rq.isLogined()}">
                    <li><a class="mr-4" href="../member/login">LOGIN</a></li>
                </c:if>
                <c:if test="${rq.isLogined()}">
                    <li><a onclick="if(confirm('로그아웃 하시겠습니까?') == false) return false;" class="mr-4"
                        href="../member/doLogout">LOGOUT</a></li>
                </c:if>
            </ul>
        </div>
    </header>

    <h1>${pageTitle}</h1>

그래서 화면이 어떻냐면

이렇고 로그인을 클릭하면 로그인 창으로 이동한다.

아이디나 비번이 틀리면 틀렸으니 js의 history.back();이 잘 먹고, 입력하지 않을 시에는 입력하라는 문구가 잘 뜬다.

로그인을 하면 로그아웃버튼으로 바뀌는 것도 잘 된다.

이렇게 한 와중에 강사님이 로그아웃 코드를 수정하셨다.

    @RequestMapping("/usr/member/doLogout")
    public String doLogout(HttpServletRequest req) {

        Rq rq = (Rq) req.getAttribute("rq");

        // 로그아웃 처리
        rq.logout();

        return Ut.jsReplace("S-1", Ut.f("로그아웃 성공"), " / ");
    }

계속 만들어둔 메서드를 쓴다는 걸 까먹는다.