Post

멀티보드 문의게시판 CRUD 구현: 비밀글 설정과 비밀번호 해싱

문의 게시판 등록/수정/삭제

화면

image 문의 게시글 등록 화면입니다.

비밀글 체크박스에 체크 후 비밀번호를 입력하여 문의 게시글을 등록하면 비밀글로 설정됩니다.


image

게시글 수정 화면입니다.

기존 저장된 문의 게시글 정보가 나타납니다.


Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
/**
 * board > BoardController.java
 */

/**
 * 문의 게시글을 저장하고 결과를 반환합니다.
 *
 * @param request  HttpServletRequest 객체
 * @param boardDTO 문의 게시글 정보 DTO
 * @return 게시글 저장 결과를 담은 API 응답 객체
 * @throws Exception 예외 발생 시
 */
@PostMapping("/api/boards/inquiry")
ResponseEntity<APIResponse> saveInquiryBoardInfo(HttpServletRequest request, @Valid @RequestBody BoardInquiryDTO boardDTO) throws Exception {

APIResponse apiResponse;

int seqId = AuthUtil.getSeqIdFromRequest(request);

if (seqId == 0) {
    apiResponse = ResponseBuilder.ErrorWithoutData("로그인되지 않았습니다.");
    return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(apiResponse);
}
boardService.saveInquiryBoardInfo(seqId, boardDTO);

apiResponse = ResponseBuilder.SuccessWithoutData("게시글 저장에 성공하였습니다.");
return ResponseEntity.status(HttpStatus.OK).body(apiResponse);
}
/**
 * 특정 문의 게시글을 삭제합니다.
 *
 * @param request HttpServletRequest 객체
 * @param boardId 게시글 ID
 * @return 게시글 삭제 결과를 담은 API 응답 객체
 */
@DeleteMapping("/api/boards/inquiry/{boardId}")
public ResponseEntity<APIResponse> deleteInquiryBoard(HttpServletRequest request, @PathVariable int boardId) {
//BearerAuthInterceptor에서 JWT에 따른 userId를 포함한 Request를 전달
APIResponse apiResponse;
int seqId = AuthUtil.getSeqIdFromRequest(request);

if (seqId == 0) {
    apiResponse = ResponseBuilder.ErrorWithoutData("로그인되지 않았습니다.");
    return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(apiResponse);
}

// 미 답변일 경우에만 삭제 가능
boardService.deleteInquiryBoard(seqId, boardId);

apiResponse = ResponseBuilder.SuccessWithoutData("게시글 삭제에 성공하였습니다.");
return ResponseEntity.status(HttpStatus.OK).body(apiResponse);
}

/**
 * 특정 문의 게시글을 수정하고 결과를 반환합니다.
 *
 * @param request  HttpServletRequest 객체
 * @param boardId  수정할 게시글 ID
 * @param boardDTO 수정할 문의 게시글 정보
 * @return 게시글 수정 결과를 담은 API 응답 객체
 * @throws Exception 예외 발생 시
 */
@PutMapping("/api/boards/inquiry/{boardId}")
public ResponseEntity<APIResponse> updateInquiryBoardInfo(HttpServletRequest request, @PathVariable int boardId,
                                                        @Valid @RequestBody BoardInquiryDTO boardDTO) throws Exception {
//BearerAuthInterceptor 에서 Request에 추출한 JWT로부터 추출한 seqId 포함하여 전달
APIResponse apiResponse;
int seqId = AuthUtil.getSeqIdFromRequest(request);

if (seqId == 0) {
    apiResponse = ResponseBuilder.ErrorWithoutData("로그인되지 않았습니다.");
    return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(apiResponse);
}

boardDTO.setBoardId(boardId);
boardService.updateInquiryBoardInfo(seqId, boardDTO);

apiResponse = ResponseBuilder.SuccessWithoutData("게시글 수정에 성공하였습니다.");
return ResponseEntity.status(HttpStatus.OK).body(apiResponse);
}

Controller에서 문의게시글 save, update, delete 입니다. delete에서 관리자가 문의게시글에 답변을 하였을 경우엔 삭제가 불가합니다. 그 외엔 자유게시글, 갤러리 게시글과 유사하기 때문에 넘어가겠습니다.


Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/**
 * service > BoardService.java
 */

/**
 * 문의 게시글 정보를 저장합니다.
 *
 * @param seqId    사용자 식별자 아이디
 * @param boardDTO 문의 게시글 정보 DTO
 * @throws Exception 예외 발생 시 처리
 */
public void saveInquiryBoardInfo(int seqId, BoardInquiryDTO boardDTO) throws Exception {

if (seqId <= 0) {
    throw new AppException(ErrorCode.USER_NOT_FOUND, "유효한 사용자가 아닙니다.");
}

if (boardDTO.getIsSecret() == 1 && boardDTO.getPassword().length() < 4) {
    throw new AppException(ErrorCode.BAD_REQUEST, "게시글 비밀번호는 4자 이상입니다.");
}
String hashedPassword = AuthUtil.hashPassword(boardDTO.getPassword());
boardDTO.setPassword(hashedPassword);
boardRepository.saveInquiryBoardInfo(boardDTO);
}

/**
 * 문의 게시판 정보를 수정합니다.
 *
 * @param seqId    사용자 식별자 아이디
 * @param boardDTO 문의 게시판 정보 DTO
 * @throws Exception 예외 발생 시 처리
 */
public void updateInquiryBoardInfo(int seqId, BoardInquiryDTO boardDTO) throws Exception {
    //현재 userSeqId와 게시글 정보에 저장된 userSeqId와 비교
    int getUserSeqId = boardRepository.getInquiryBoardDetail(boardDTO.getBoardId()).getUserSeqId();
    if (seqId != getUserSeqId) {
        throw new AppException(ErrorCode.INVALID_PERMISSION, "수정 권한이 없습니다.");
    }
    String enteredPassword = boardDTO.getPassword();
    if (!StringUtils.isEmpty(enteredPassword)){
        String hashedPassword = AuthUtil.hashPassword(enteredPassword);
        boardDTO.setPassword(hashedPassword);
    }

    boardRepository.updateInquiryBoardInfo(boardDTO);
}

/**
 * 문의 게시판을 삭제합니다.
 *
 * @param seqId   사용자 식별자 아이디
 * @param boardId 게시글 ID
 * @throws AppException 삭제 권한이 없거나 남아있는 답변이 있을 경우 발생하는 예외
 */
public void deleteInquiryBoard(int seqId, int boardId) {
//현재 userSeqId와 게시글 정보에 저장된 userSeqId와 비교

int getUserSeqId = boardRepository.getInquiryBoardDetail(boardId).getUserSeqId();
if (seqId != getUserSeqId) {
    throw new AppException(ErrorCode.INVALID_PERMISSION, "삭제 권한이 없습니다.");
}

if (replyRepository.countRepliesByBoardId(boardId) > 0) {
    throw new AppException(ErrorCode.REMAIN_REPLY, "답변 남아있어서 게시글 삭제가 불가합니다.");
}
boardRepository.deleteInquiryBoard(boardId);
}


save 시에는 유저 회원가입에서 활용한 패스워드 해싱 메소드 AuthUtil.hashPassword을 사용하여 평문 비밀번호를 해싱하여 저장합니다.

update에서는 사용자가 비밀번호를 입력하였을 경우, 해당 비밀번호를 해시하여 업데이트합니다.

delete에서는 관리자 답변이 남아있을 경우 REMAIN_REPLY Exception을 발생합니다. 커스텀 예외인 REMAIN_REPLY은 아래와 같이 사용 하고 있습니다.

1
2
3
4
5
6
7
8
9
@Getter
@AllArgsConstructor
public enum ErrorCode {

    /**
     * 409 CONFLICT : Resource 의 현재 상태와 충돌
     */
    ...
    REMAIN_REPLY(HttpStatus.CONFLICT, "댓글이 남아있어 삭제가 불가합니다"),

Repository & Mapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
 * repository > BoardRepository.java
 */

/**
 * 새로운 문의 게시글을 저장
 *
 * @param boardInquiryDTO 문의 게시글 정보 DTO
 */
void saveInquiryBoardInfo(BoardInquiryDTO boardInquiryDTO);

/**
 * 문의 게시글의 조회수를 1 증가
 *
 * @param boardId 게시글 ID
 */
void updateInquiryBoardVisitCount(int boardId);

/**
 * 문의 게시글을 삭제합니다.
 *
 * @param boardId 게시글 ID
 */
void deleteInquiryBoard(int boardId);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<!-- 문의 게시글 저장 -->
<insert id="saveInquiryBoardInfo" parameterType="BoardInquiryDTO" useGeneratedKeys="true" keyProperty="boardId">
    INSERT INTO inquiry_board
    (title, content, user_seq_id, created_at, visit_count, is_secret, is_answered, password, child_code_value)
    VALUES (#{title},
            #{content},
            (SELECT seq_id FROM users WHERE user_id = #{userId}),
            now(),
            0,
            #{isSecret},
            0,
            #{password},
            #{categoryValue})
</insert>

<!-- 문의 게시글 정보를 업데이트하는 쿼리 -->
<update id="updateInquiryBoardInfo" parameterType="BoardInquiryDTO">
    UPDATE inquiry_board
    SET title            = #{title},
        content          = #{content},
        child_code_value = #{categoryValue}
        <if test="password != null">
            , password = #{password}
            , is_secret = 1
        </if>
        <if test="password == null">
            , is_secret = 0
        </if>
    WHERE board_id = #{boardId}
</update>

<!-- 문의 게시글 삭제하는 쿼리 -->
<delete id="deleteInquiryBoard">
    DELETE
    FROM inquiry_board
    WHERE board_id = #{boardId}
</delete>

게시글 저장 시엔 앞선 게시글과 동일하게 String userId에 해당하는 user sequence id를 서브쿼리로 가져와 해당 값을 user_seq_id에 넣습니다. 만약 게시글 수정 시 비밀글 체크를하지 않았다면 is_secret 값을 0으로 업데이트합니다.


다음으로

계획했던 공지게시판, 자유게시판, 갤러리게시판, 문의게시판 구현을 완료하였습니다. 다음으로 각 게시판 최근 게시글을 볼 수 있는 대시보드 화면을 간단히 살펴보고 프로젝트의 미비점과 개선사항을 점검해보겠습니다.

This post is licensed under CC BY 4.0 by the author.