처음 시작은 Rq 객체 관리를 Spring이 하게끔 바꾸는 것 부터 시작했다. 솔직히 뭔소린지 이해는 안되는데 코드를 보면 약간은 알 것 같기도 하고.
@Controller
public class UsrMemberController {
@Autowired
private MemberService memberService;
@Autowired
private Rq rq;
@RequestMapping("/usr/member/join")
public String showJoin() {
return "/usr/member/join";
}
@RequestMapping("/usr/member/doJoin")
@ResponseBody
public String doJoin(HttpServletRequest req, String loginId, String loginPw,
String name, String nickname, String cellphoneNum, String email) {
rq = (Rq) req.getAttribute("rq");
if (Ut.isEmptyOrNull(loginId))
return Ut.jsHistoryBack("F-1", Ut.f("아이디를 입력해주세요."));
if (Ut.isEmptyOrNull(loginPw))
return Ut.jsHistoryBack("F-2", Ut.f("비밀번호를 입력해주세요."));
if (Ut.isEmptyOrNull(name))
return Ut.jsHistoryBack("F-3", Ut.f("이름을 입력해주세요."));
if (Ut.isEmptyOrNull(nickname))
return Ut.jsHistoryBack("F-4", Ut.f("닉네임를 입력해주세요."));
if (Ut.isEmptyOrNull(cellphoneNum))
return Ut.jsHistoryBack("F-5", Ut.f("전화번호를 입력해주세요."));
if (Ut.isEmptyOrNull(email))
return Ut.jsHistoryBack("F-6", Ut.f("이메일을 입력해주세요."));
ResultData doJoinRd = memberService.doJoin(loginId, loginPw, name, nickname, cellphoneNum, email);
if (doJoinRd.isFail()) {
return Ut.jsHistoryBack(doJoinRd.getResultCode(), doJoinRd.getMsg());
}
Member member = memberService.getMemberById((int) doJoinRd.getData1());
return Ut.jsReplace("S-1", "회원가입이 완료되었습니다.", "/usr/member/login");
}
@RequestMapping("/usr/member/login")
public String showLogin(HttpServletRequest req) {
return "/usr/member/login";
}
@RequestMapping("/usr/member/doLogin")
@ResponseBody
public String doLogin(HttpServletRequest req, String loginId, String loginPw) {
rq = (Rq) req.getAttribute("rq");
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("비밀번호가 틀렸습니다."));
}
rq.login(member);
return Ut.jsReplace("S-1", Ut.f("%s님 환영합니다", member.getNickname()), " / ");
}
@RequestMapping("/usr/member/doLogout")
@ResponseBody
public String doLogout(HttpServletRequest req) {
// 로그아웃 처리
rq.logout();
return Ut.jsReplace("S-1", Ut.f("로그아웃 성공"), " / ");
}
}
이런식으로 매번 Rq rq로 선언을 해줬는데 해당 기능을 적용하면서 그게 생략됐다.
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class Rq {
@Getter
private boolean isLogined = false;
@Getter
private int loginedMemberId = 0;
private HttpServletRequest req;
private HttpServletResponse resp;
private HttpSession session;
public Rq(HttpServletRequest req, HttpServletResponse resp) {
this.req = req;
this.resp = resp;
this.session = req.getSession();
HttpSession httpSession = req.getSession();
if (httpSession.getAttribute("loginedMemberId") != null) {
isLogined = true;
loginedMemberId = (int) httpSession.getAttribute("loginedMemberId");
}
this.req.setAttribute("rq", this);
}
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();
}
}
public void logout() {
session.removeAttribute("loginedMemberId");
}
public void login(Member member) {
session.setAttribute("loginedMemberId", member.getId());
}
public void initBeforeActionInterceptor() {
System.err.println("initBeforeActionInterceptor 실행");
}
}
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
해당 코드를 추가하고 initBeforeActionInterceptor 메서드를 추가해서 해당 기능을 돌렸다.
거의 모든 전체 코드에
@Autowired
private Rq rq;
를 전역 변수로 선언해줬다.
여기서 Autowired가 뭔지 기억이 잘 안나니 한번 확인해보자.
@Autowired 애너테이션을 사용하면, 스프링이 해당 필드나 생성자, 메서드에 필요한 의존성을 자동으로 주입해 줍니다. 라고 한다.
이후에 doJoin, modify 등을 JSP로 화면 구성을 하셨는데 나는 이미 다 만들어둬서 내 코드랑 비교하면서 강사님이 하시는 것을 보았다. 보다보니까 나는 detail에 updateDate가 누락되어있어서 추가했다.
<div class="detail-item">
<span class="label">수정 날짜:</span> ${article.updateDate}
</div>

간단한 것이다.
강사님은 화면 구성을 다 만드신 뒤에 head에 daisyUI를 추가하셔서 스타일을 적용시켰다. 나는 이미 css로 다 해둬서 해당 link 태그를 생성하니까 죄다 깨지길래 그냥 각주처리 해뒀다.
사용법은 해당 링크에서 복붙하면서 페이지에 적용되는 것을 확인하면 된다. 사이트 보면 해당 스타일들을 볼 수 있게 잘 만들어두었다.
데이지 UI 뿐 아니라 cdnjs에 다양한 UI들이 있으니 한번 참고해보는 것도 작업을 편하게 할 수 있게 해줄 것 같다.
다음으로 게시글 작성도 만드셨는데 마찬가지로 나는 이미 해 두어서 그냥 봤다. 나랑 비슷한데 차이점은 글 작성 버튼을 헤드에 두셨다는 거? 그거 말고는 근본적으로 다를게 없다.
어제 포기했었던 글 전체보기 같은 탭을 이제 만들 것 같다.
일단 게시물을 저장하는 게시판의 개념을 만든다. 따라서 DB에 게시판을 저장할 공간, 즉 테이블을 생성한다.
#게시판 테이블 생성
CREATE TABLE board(
id INT(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
regDate DATETIME NOT NULL,
updateDate DATETIME NOT NULL,
`code` CHAR(50) NOT NULL Unique comment '공지사항, QnA 등',
`name` CHAR(50) NOT NULL Unique comment '',
delStatus TINYINT(1) UNSIGNED NOT NULL DEFAULT 0 COMMENT '탈퇴여부 (0=탈퇴 전, 1=탈퇴 후)',
delDate DATETIME COMMENT '탈퇴 날짜'
);
게시판에 속한 게시물도 존재하니 게시물 테이블에 inner join할 수 있게 boardId를 추가해준다.
ALTER TABLE article ADD COLUMN boardId INT(10) UNSIGNED NOT NULL AFTER memberId;
이후에 테스트 데이터와 article에 boardId의 값을 채워준다.
-- 게시판 테스트 데이터.
INSERT INTO board
SET regDate = NOW(),
updateDate = NOW(),
`code` = 'notice',
`name` = '공지사항';
INSERT INTO board
SET regDate = NOW(),
updateDate = NOW(),
`code` = 'free',
`name` = '자유';
INSERT INTO board
SET regDate = NOW(),
updateDate = NOW(),
`code` = 'QnA',
`name` = '질의응답';
UPDATE article
SET boardId = 1
WHERE id IN (1,2, 3, 4);
UPDATE article
SET boardId = 2
WHERE id in (5, 6);
UPDATE article
SET boardId = 3
WHERE id = 7;
이제 준비는 다 됐으니 프로젝트에 손을 대보자.
//기존 list
@RequestMapping("/usr/article/list")
public String showList(Model model) {
List<Article> articles = articleService.getArticles();
model.addAttribute("articles", articles);
return "/usr/article/list";
}
// 게시판 적용.
@Autowired
private BoardService boardService;
public String showList(Model model, int boardId) {
Board board = boardService.getBoardById(boardId);
List<Article> articles = articleService.getArticles();
model.addAttribute("articles", articles);
model.addAttribute("board", board);
return "usr/article/list";
}
우린 아직 BoardService를 만들어준적이 없으니 만들어주자.
@Service
public class BoardService {
@Autowired
private BoardRepository boardRepository;
public BoardService(BoardRepository boardRepository) {
this.boardRepository = boardRepository;
}
public Board getBoardById(int boardId) {
return boardRepository.getBoardById(boardId);
}
}
BoardRepository도 마찬가지.
@Mapper
public interface BoardRepository {
@Select("""
SELECT *
FROM board
WHERE id = #{boardId}
AND delStatus = 0
""")
public Board getBoardById(int boardId);
}
생성자와 getter setter를 담아둘 Board 클래스까지 만들면,
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Board {
private int id;
private String regDate;
private String updateDate;
private String code;
private String name;
private boolean delStatus;
private String delDate;
}
이제 준비의 준비는 끝이 났다.
이제는 게시판을 분리해줘야된다.
@RequestMapping("/usr/article/list")
public String showList(Model model, int boardId) {
Board board = boardService.getBoardById(boardId);
// List<Article> articles = articleService.getArticles();
List<Article> bIdarticles = articleService.getBIdArticles(boardId);
model.addAttribute("articles", bIdarticles);
model.addAttribute("board", board);
return "usr/article/list";
}
이렇게 하나 새로 선언해줬고
public List<Article> getBIdArticles(int boardId) {
return articleRepository.getArticlesByBorderId(boardId);
}
@Select("""
SELECT a.*, m.nickname AS extra__writer
FROM article a
INNER JOIN `member` m
ON a.memberId = m.id
WHERE a.boardId = #{boardId}
ORDER BY
a.id DESC
""")
public List<Article> getArticlesByBorderId(int boardId);
이렇게 해당 게시판의 게시물만 가져올 수 있게 만들었다.
그리고 List를 메뉴화 시켜야하는데,
<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="" href="/">HOME</a></li>
<li><a class="" href="../article/list">LIST</a>
<ul class="sub-menu">
<li><a href="../article/list?boardId=1">Notice</a></li>
<li><a href="../article/list?boardId=2">Free</a></li>
<li><a href="../article/list?boardId=3">QnA</a></li>
</ul></li>
<c:if test="${!rq.isLogined()}">
<li><a class="" href="../member/login">LOGIN</a></li>
<li><a class="mr-4" href="../member/join">JOIN</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>
헤드에 이렇게 추가해줬다.
아까 게시물을 게시판별로 분류하는 코드를 만들어서 화면상 해당 게시판에 속한 게시물이 잘 나온다.

이렇게.
그리고 2차메뉴는 이렇게.
write시 게시판 번호를 DB로 넘겨주지 않으니까 문제가 생긴다. write 기능도 수정이 필요하고 detail에 게시판이 무엇인지 명시되지 않는 것도 수정이 필요하다.
일단 write에 매개변수로 String boardId를 받아 서비스로 또 리포지트리로 넘기게 했고 쿼리에 해당 데이터가 들어갈 수 있게 수정했다.
@RequestMapping("/usr/article/doWrite")
@ResponseBody
public String doWrite(HttpServletRequest req, String title, String body, String boardId) {
Rq rq = (Rq) req.getAttribute("rq");
if (Ut.isEmptyOrNull(title)) {
return Ut.jsHistoryBack("F-1", "제목을 입력해주세요");
}
if (Ut.isEmptyOrNull(body)) {
return Ut.jsHistoryBack("F-2", "내용을 입력해주세요");
}
if (Ut.isEmptyOrNull(boardId)) {
return Ut.jsHistoryBack("F-3", "게시판 선택해주세요.");
}
int boardIdInt = Integer.parseInt(boardId);
ResultData writeArticleRd = articleService.writeArticle(rq.getLoginedMemberId(), title, body, boardIdInt);
int id = (int) writeArticleRd.getData1();
Article article = articleService.getArticleById(id);
return Ut.jsReplace(writeArticleRd.getResultCode(), writeArticleRd.getMsg(),"/usr/article/detail?id=" + id);
}
////////////////
public ResultData writeArticle(int memberId, String title, String body, int boardId) {
articleRepository.writeArticle(memberId, title, body, boardId);
int id = articleRepository.getLastInsertId();
return ResultData.from("S-1", Ut.f("%d번 글이 등록되었습니다", id), "등록 된 게시글의 id", id);
}
////////////////
public void writeArticle(int memberId, String title, String body, int boardId);
//////////////
<insert id="writeArticle">
INSERT INTO article
SET regDate = NOW(),
updateDate = NOW(),
title = #{title},
`body` = #{body},
memberId = #{memberId},
boardId = #{boardId}
</insert>
이렇게.
그리고 write JSP에 해당 태그를 추가
<div class="form-group">
<label for="boardId">게시판 선택</label>
<select id="boardId" name="boardId" class="form-select" required>
<option value="">게시판을 선택하세요</option>
<option value="1">Notice</option>
<option value="2">Free</option>
<option value="3">QnA</option>
</select>
</div>
화면상으로는 이렇게 보인다.


detail은 간단하게 해결했다.
이제는 페이지네이션이다. 테스트 데이터를 대량으로 늘렸다.
@RequestMapping("/usr/article/list")
public String showList(Model model, Integer page, @RequestParam(defaultValue = "1") int boardId) {
if (page == null) {
page = 1; // 기본값 설정
}
Board board = boardService.getBoardById(boardId);
model.addAttribute("board", board);
int itemsPerPage = 10;
int totalItems = articleService.getTotalArticlesCount(boardId);
int totalPages = (int) Math.ceil((double) totalItems / itemsPerPage);
int offset = (page - 1) * itemsPerPage;
List<Article> articles = articleService.getArticlesByPage(boardId, offset, itemsPerPage);
model.addAttribute("articles", articles);
model.addAttribute("currentPage", page);
model.addAttribute("totalPages", totalPages);
model.addAttribute("boardId", boardId);
return "usr/article/list";
}
//페이지네이션
public List<Article> getArticlesByPage(int boardId, int offset, int limit) {
return articleRepository.getArticlesByPage(boardId, offset, limit);
}
public int getTotalArticlesCount(int boardId) {
return articleRepository.getTotalArticlesCount(boardId);
}
//페이지네이션
public List<Article> getArticlesByPage(int boardId, int offset, int limit);
public int getTotalArticlesCount(int boardId);
<!-- 페이지네이션 -->
<select id="getArticlesByPage" resultType="Article">
SELECT *
FROM article
WHERE boardId = #{boardId}
ORDER BY id DESC
LIMIT #{limit} OFFSET #{offset}
</select>
<select id="getTotalArticlesCount" resultType="int">
SELECT COUNT(*)
FROM article
WHERE boardId = #{boardId}
</select>
페이지네이션은 이렇게 처리했고

이렇게 작동한다. 근데 지금 보면 writer가 누락되어있는데 이게 이유를 모르겠다.
gpt한테 물어봐서 얻은 결과는
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.repository.ArticleRepository">
<!-- resultMap 정의 -->
<resultMap id="ArticleResultMap" type="com.example.demo.vo.Article">
<result property="id" column="id"/>
<result property="title" column="title"/>
<result property="body" column="body"/>
<result property="extra__writer" column="extra__writer"/>
<!-- 필요한 다른 필드들 추가 -->
</resultMap>
<update id="modifyArticle">
UPDATE article
<set>
<if test="title != null and title != ''">title = #{title},</if>
<if test="body != null and body != ''">`body` = #{body},</if>
updateDate = NOW()
</set>
WHERE id = #{id}
</update>
<insert id="writeArticle">
INSERT INTO article
SET regDate = NOW(),
updateDate = NOW(),
title = #{title},
`body` = #{body},
memberId = #{memberId},
boardId = #{boardId}
</insert>
<select id="getArticles" resultMap="ArticleResultMap">
SELECT a.*, m.nickname AS extra__writer
FROM article a
INNER JOIN `member` m ON a.memberId = m.id
ORDER BY a.id DESC
</select>
<select id="getArticleById" resultType="Article">
SELECT a.*
FROM article a
WHERE id = #{id}
</select>
<!-- 페이지네이션 -->
<select id="getArticlesByPage" resultMap="ArticleResultMap">
SELECT a.*, m.nickname AS extra__writer
FROM article a
INNER JOIN `member` m ON a.memberId = m.id
WHERE a.boardId = #{boardId}
ORDER BY a.id DESC
LIMIT #{limit} OFFSET #{offset}
</select>
<select id="getTotalArticlesCount" resultType="int">
SELECT COUNT(*)
FROM article
WHERE boardId = #{boardId}
</select>
</mapper>
<!-- resultMap 정의 -->
<resultMap id="ArticleResultMap" type="com.example.demo.vo.Article">
<result property="id" column="id"/>
<result property="title" column="title"/>
<result property="body" column="body"/>
<result property="extra__writer" column="extra__writer"/>
<!-- 필요한 다른 필드들 추가 -->
</resultMap>
해당 코드를 추가함으로써 해결했다.
resultMap을 추가하고 제대로 명시해주니까 데이터를 찾아왔는데, board필드가 추가되면서 sql에서 맵핑을 해오지 못해서 나온 문제였던 것 같다.
아무튼 여기까지.. 페이지네이션은 지우고 한번 더 혼자 해봐야될 것 같다.