본문 바로가기
백엔드/Spring Framework

게시판 구현 / 3. 게시판 글 등록 및 카테고리

by maverick11471 2024. 7. 17.

[요약]

 - Member  테이블에 ROLE 컬럼을 추가하여 주인장과 일반 사용자의 기능을 구분한다.

 - JSTL을 활용하여 .jsp 를 구현한다.

 - ApplicationContext를 활용하여 BoardService의 구현체를 동적으로 가져온다.


1. Member 테이블에 ROLE 컬럼 추가 및 관련 클래스 수정

 

[MySQL Workbench 수정]

 - 컬럼 : ROLE 추가 / Default값 : 'USER'

 

 - bitcamp ROLE 수정 : 'ADMIN(주인장)'

 

[MemberDto 수정]

 - ROLE 추가 (getter, setter 포함)

public class MemberDto {

...
    private String role;
    
    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }
...

 

[Member-mapper 수정]

 - 로그인 시 ROLE 추가

<select id="login" parameterType="member" resultType="member">
    SELECT ID
         , USERNAME
         , PASSWORD
         , EMAIL
         , NICKNAME
         , TEL
         , ROLE
        FROM MEMBER
        WHERE USERNAME = #{username}
          AND PASSWORD = #{password}
</select>

 


2. 자유게시판 글 등록 추가 (로그인한 경우에만)

[free-list.jsp 수정]

 - JSTL 추가

<%@ taglib prefix="c" uri="jakarta.tags.core" %>
...
<body>
	// loginMember(ID, PW 포함) 이 Null이 아닐 때
    <c:if test="${loginMember ne null}">
        <div class="container mt-3 mb-5 w-50 d-flex justify-content-end">
            <button type="button" class="btn btn-outline-secondary" onclick="location.href='/board/post.do'">글 등록</button>
        </div>
    </c:if>
...

 

 

 - 참고(JSTL)

 

el 표기법과 JSTL

[요약] - el표기법 : 자바에서 받아온 데이터 표출 - JSTL : 자바문법을 사용하기 위한 기술 (.jsp 에서 사용) [el 표기법(Expression Language)] - JSP 에서 ${} 표기되는 기법 - Java에서 받아온 데이터를 표

maverick11471.tistory.com

 

(좌측) 로그인 안할 시 글 등록이 없음 / (우측) 로그인 시 글 등록버튼 생성

 


3. 공지사항 글 등록 추가 (주인장만)

[Notice-list.jsp 수정]

<%@ taglib prefix="c" uri="jakarta.tags.core" %>
...
<body>
	// loginMember가 null이 아니고 loginMember의 role이 ADMIN일때
    <c:if test="${loginMember ne null and loginMember.role eq 'ADMIN'}">
<%--        <c:if test="${loginMember != null && loginMember.role == 'ADMIN'}">    --%>
        <div class="container mt-3 mb-5 w-50 d-flex justify-content-end">
            <button type="button" class="btn btn-outline-secondary" onclick="location.href='/board/post.do'">글 등록</button>
        </div>
    </c:if>

 


4. 글 등록 - 카테고리 구분

    * 주인장은 카테고리로 자유게시판과 공지사항이 있어야 한다

[post.jsp 수정]

<%@ taglib prefix="c" uri="jakarta.tags.core" %>
...
// loginMember의 role이 ADMIN(주인장)일 때
<c:if test="${loginMember.role eq 'ADMIN'}">
    <div class="form-group">
        <label for="type">카테고리</label>
        <select class="form-select" name="type" id="type">
            <option value="free" selected>자유게시판</option>
            <option value="notice">공지사항</option>
        </select>
    </div>
</c:if>

 

(좌측) 주인장일 시 카테고리가 나옴 / (우측) 일반 사용자일 경우 카테고리가 안나옴


4-1. 글 등록 - 게시판 구분

 

글 등록을 할 수 있는 게시판은 2개가 있다.(공지사항, 자유게시판)

 

그런데 글 등록 후 '등록' 버튼을 누르면 이 게시글이 공지사항으로 갈지 자유게시판으로 갈지 지정해 두지 않았다.

 

이를 지정해주려 한다.

 

[요약]

type 변수를 생성하여 type에 따른 화면 조성

 

[BoardDto 수정]

 - type 변수 추가 

    * getter, setter 추가

public class BoardDto {
    private String type = "free";
    
    public String getType() {
    	return type;
    }

    public void setType(String type) {
        this.type = type;
    }

 

 

[BoardController 수정]

@Controller
@RequestMapping("/board")
public class BoardController {
	// boardService 객체 생성
    private BoardService boardService;
    // ApplicationContext 객체 생성
    private ApplicationContext applicationContext;

    @Autowired
    public BoardController(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }
    
        @PostMapping("/post.do")
        public String post(BoardDto boardDto) {
            // 게시판 타입에 따른 동적 의존성 주입
            if(boardDto.getType().equals("free")) {
            // 즉, 스프링 컨테이너에서 "freeBoardServiceImpl" 이름의 Bean을 찾아와서, 
            // 그 Bean이 BoardService 인터페이스의 구현체인지 확인하고 있습니다.

			// 이렇게 하면 boardService 변수에 BoardService 인터페이스의 구현체가 할당됩니다. 
            // 이를 통해 boardService를 BoardService 인터페이스의 메서드를 호출할 수 있게 됩니다.
                boardService = applicationContext.getBean("freeBoardServiceImpl", BoardService.class);
                // free-list 일 때
            } else {
            	// notice-list 일 때
                boardService = applicationContext.getBean("noticeServiceImpl", BoardService.class);
            }

            boardService.post(boardDto);

            if(boardDto.getType().equals("free")) {
                return "redirect:/board/free-list.do";
            } else {
                return "redirect:/board/notice-list.do";
            }
        }

 

 

Spring Framework / ApplicationContext 역할

ApplicationContext는 Spring 프레임워크에서 사용되는 핵심 인터페이스 중 하나입니다. 이 인터페이스는 Spring 애플리케이션의 컨테이너 역할을 하며, 다음과 같은 주요 기능을 제공합니

maverick11471.tistory.com

 

 

[post.jsp 수정]

 - loginMember는 memberdao.login으로 login 시 갖고있는 정보들이 있다.

 - readonly 수정이 안되고 읽는 것만 가능

...
// 자유게시판의 valuer값을 free로 지정
<select class="form-select" name="type" id="type">
    <option value="free" selected>자유게시판</option>
    <option value="notice">공지사항</option>
</select>
...

...
// 글 등록 시 작성자 칸에 nickname이 들어가도록 표현하며,
// 이는 readonly로 읽기만 가능하다.
<div class="form-group mt-3">
    <label for="nickname">작성자</label>
    <input type="text" class="form-control" id="nickname" name="nickname"
           value="${loginMember.nickname}" readonly required>
</div>
...

 

 

[freeboard-mapper / notice-mapper 수정]

Freeboard notice 컬럼에 nickname 없다.

그래서 member join 해야 한다.

<mapper namespace="FreeBoardDao">
    <insert id="post" parameterType="board">
        INSERT INTO FREEBOARD(
            TITLE,
            CONTENT,
            WRITER_ID
        ) VALUES(
            #{title},
            #{content},
            // 서브쿼리로 nickname 가져오기
            (
                SELECT M.ID
                    FROM MEMBER M
                    WHERE M.NICKNAME = #{nickname}
            )
        )
    </insert>
<mapper namespace="NoticeDao">
    <insert id="post" parameterType="board">
        INSERT INTO NOTICE(
            TITLE,
            CONTENT,
            WRITER_ID
        ) VALUES (
            #{title},
            #{content},
            (
                SELECT M.ID
                    FROM MEMBER M
                    WHERE M.NICKNAME = #{nickname}
            )
        )
    </insert>

 

 - 결과1. 자유게시판에 일반사용자가 글 등록

 - 결과2. 공지사항에 주인장이 글 등록

 

그런데 등록 새로고침을 해도 주소창에 post 나온다. (화면만 바뀌고, url 주소창은 그대로 post.do 유지)

이를 해결하기위해 redirect 사용한다.

[BoardController 수정]

@PostMapping("/post.do")
public String post(BoardDto boardDto) {
    // 게시판 타입에 따른 동적 의존성 주입
    if(boardDto.getType().equals("free")) {
        boardService = applicationContext.getBean("freeBoardServiceImpl", BoardService.class);
    } else {
        boardService = applicationContext.getBean("noticeServiceImpl", BoardService.class);
    }

    boardService.post(boardDto);

	// redirect로 수정(free면 free-list로, else면 notice-list로
    if(boardDto.getType().equals("free")) {
        return "redirect:/board/free-list.do";
    } else {
        return "redirect:/board/notice-list.do";
    }
}

 


 

그런데 또 다른 문제점이 발생했다.

 

로그인만 안해도 url주소에 /post.do 만 입력해도 글 등록이 된다.

 

이를 방지하는 방법은 아래와 같다.

 

1. MemberController에서 set으로 loginMember를 지정한다

@PostMapping("/login.do")
public String login(MemberDto memberDto, Model model, HttpSession session) {
    try {
    	// set으로 loginMember 지정. 어디든 갖다쓸 수 있음.
        MemberDto loginMember = memberService.login(memberDto);

        loginMember.setPassword("");

        session.setAttribute("loginMember", loginMember);

        return "redirect:/";
    } catch (Exception e) {
        model.addAttribute("loginFailMsg", e.getMessage());
        return "member/login";
    }
}

 

2. 이를 BoardController에서 get으로 loginMember를 가져온다.

@GetMapping("/post.do")
public String postView(HttpSession session) {
    MemberDto loginMember = (MemberDto) session.getAttribute("loginMember");

	// loginMember가 null이면 로그인창으로 이동
    if(loginMember == null) {
        return "redirect:/member/login.do";
    }

	// 아니면 post 게시판으로 이동한다.
    return "board/post";
}

 

 


2-1. 자유게시판 등록한 글이 계속해서 나오게 수정

[boardController 수정]

 - Model 객체에 BoardList를 넣는다. 이 때 키 값을 freeBoardList로 지정한다.

@GetMapping("/free-list.do")
public String freeListView(Model model) {
    boardService = applicationContext.getBean("freeBoardServiceImpl", BoardService.class);

    model.addAttribute("freeBoardList", boardService.getBoardList());

    return "board/free-list";
}

 

[free-list.jsp 수정]

 - Controller에서 지정한 freeBoardList를 item으로 삼아 한개씩 freeBoard로 꺼내 속성을 지정한다.

 - c:forEach로 하나씩 꺼내온다.

<tbody class="table-group-divider">
    <c:forEach items="${freeBoardList}" var="freeBoard">
    	// onclick 시 url을 지정하고 쿼리스트링 방식으로 클릭 시
        // url 주소창에 freeBoard.id가 지정될 수 있도록 한다.
        <tr class="board-tr" onclick="location.href='/board/free-detail.do?id=${freeBoard.id}'">
            <td>${freeBoard.id}</td>
            <td>${freeBoard.title}</td>
            <td>${freeBoard.nickname}</td>
            <td>
                <javatime:format value="${freeBoard.regdate}" pattern="yyyy-MM-dd"/>
            </td>
            <td>${freeBoard.cnt}</td>
        </tr>
    </c:forEach>
</tbody>

 

[쿼리스트링으로 구현 시 주소창에 id가 나온다]

 

[이전에 등록한 자유게시판 목록 전체가 나온다]


3-1. 공지사항 등록한 글이 계속해서 나오게 수정

자유게시판과 방법은 동일하다.

 

[boardController 수정]

@GetMapping("/notice-list.do")
public String noticeListView(Model model) {
    boardService = applicationContext.getBean("noticeServiceImpl", BoardService.class);

    model.addAttribute("noticeList", boardService.getBoardList());

    return "board/notice-list";
}

 

 

[notice-list.jsp 수정]

<div class="container mt-3 mb-5 w-75 card-wrapper">
    <c:forEach items="${noticeList}" var="notice">
        <div class="card" style="width: 18rem;">
            <svg class="bd-placeholder-img card-img-top" width="100%" height="180" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Placeholder: Image cap" preserveAspectRatio="xMidYMid slice" focusable="false"><title>Placeholder</title><rect width="100%" height="100%" fill="#868e96"></rect></svg>
            <div class="card-body">
                <h5 class="card-title">${notice.title}</h5>
                <p class="card-text">작성일:
                    <!--fmt:parseDate: LocalDateTime 타입을 Date 타입으로 변환-->
                    <fmt:parseDate value="${notice.regdate}" pattern="yyyy-MM-dd'T'HH:mm" var="parsedRegdate" type="both"/>
                    <!--fmt:formatDate: Date타입의 날짜를 형식 지정에 맞게 표출-->
                    <fmt:formatDate value="${parsedRegdate}" pattern="yyyy-MM-dd"></fmt:formatDate>
                </p>
                <a href="/board/notice-detail.do" class="btn btn-outline-secondary btn-sm">자세히 보기</a>
            </div>
        </div>
    </c:forEach>

 

 

위에서 fmt 라는 낯선 단어가 보일 것이다.

 

이는 왜 시행된 것일까?

 


부록. 시간타입 변경

 

공지사항과 자유게시판 모두 등록시간에 대문자 T가 들어가 있어 이상하게 나온다.

 

이를 수정하기 위해 아래와 같이 조치한다.

 

[notice-list 수정]

<%@ taglib prefix="fmt" uri="jakarta.tags.fmt" %>
...
    <p class="card-text">작성일:
        <!--fmt:parseDate: LocalDateTime 타입을 Date 타입으로 변환-->
        <fmt:parseDate value="${notice.regdate}" pattern="yyyy-MM-dd'T'HH:mm" var="parsedRegdate" type="both"/>
        <!--fmt:formatDate: Date타입의 날짜를 형식 지정에 맞게 표출-->
        <fmt:formatDate value="${parsedRegdate}" pattern="yyyy-MM-dd"></fmt:formatDate>
    </p>
...

 

 

 - parseDate를 지정해서 타입을 Date 타입으로 변환하고,

 - 이를 활용하여 formatDate로 날짜 형식에 맞게 표출한다.

 

그런데 이 과정이 너무 귀찮다..

 

그래서 자유게시판은 다르게 수정하였다.

 

<%@ taglib prefix="javatime" uri="http://sargue.net/jsptags/time" %>
...
    <c:forEach items="${freeBoardList}" var="freeBoard">
        <tr class="board-tr" onclick="location.href='/board/free-detail.do?id=${freeBoard.id}'">
            <td>${freeBoard.id}</td>
            <td>${freeBoard.title}</td>
            <td>${freeBoard.nickname}</td>
            <td>
                <javatime:format value="${freeBoard.regdate}" pattern="yyyy-MM-dd"/>
            </td>
            <td>${freeBoard.cnt}</td>
        </tr>
    </c:forEach>
...

이는 javatime이라는 라이브러리를 사용한 것인데

 

사용방법은 아래와 같다.

 

- 구글 검색

 

[pom.xml 주입]

<dependency>
    <groupId>net.sargue</groupId>
    <artifactId>java-time-jsptags</artifactId>
    <version>2.0.0</version>
</dependency>

 

 

시간 값 jstl 설정 시 유의사항

 

Moddate regdate 는 'yyyy-MM-ddTHH:mm:ss' 형태여야 하는데

Name 값을 빼버리면 이 값이 백엔드로 안넘어간다.

 

[BoardDto]

// 'yyyy-MM-ddTHH:mm:ss' => 이 형태의 값이 넘어와야 된다.
private LocalDateTime regdate;
private LocalDateTime moddate;

 

[freeboard-detail]

 - name 값이 있어야 input 시 내용이 넘어간다. 그런데 넘어갈 때 yyyy-MM-ddTHH:mm:ss 형식으로 넘어간다.

   하지만 front 화면단에는 위 형식으로 표현하면 이상하기 때문에 pattern="yyyy-MM-dd"를 지정해 주었었다.

   이대로만 하면 오류가 발생하게 된다.

   이를 방지하기 위해 script를 추가하여 형식에 맞게 값을 넘겨줘야 한다.

<div class="form-group mt-3">
    <label for="regdate">등록일</label>
    <input type="text" class="form-control" id="regdate" name="regdate"
           value="<javatime:format value="${notice.regdate}" pattern="yyyy-MM-dd"/>" required>
</div>
<div class="form-group mt-3">
    <label for="moddate">수정일</label>
    <input type="text" class="form-control" id="moddate" name="moddate"
           value="<javatime:format value="${notice.moddate}" pattern="yyyy-MM-dd"/>" required>
</div>


[freeboard-detail] 수정

    <script>
        $(() => {
            $("#modify-form").on("submit", (e) => {
                $("#regdate").val(`\${\$("#regdate").val()}T00:00:00`);
                $("#moddate").val(`\${\$("#moddate").val()}T00:00:00`);
            });
        });
    </script>