중앙정보기술인재개발원/Spring

[Spring] | final project 댓글 구현

soidev 2025. 7. 10. 14:11

오늘 할일

🌐 Spring MVC 웹 애플리케이션 흐름 정리


📌 1. RequestMapping & 파라미터 처리

✅ 요청 매핑

@RequestMapping("/process")
public String processForm(@ModelAttribute User user) {
    // form 데이터가 자동으로 User 객체에 매핑됨
}
  • @ModelAttribute: 폼에서 넘어온 여러 개의 파라미터를 객체로 받아 처리
  • DTO 객체와 필드명이 같으면 자동으로 매핑됨

✅ 단일 파라미터 처리

@RequestMapping("/login")
public String login(@RequestParam String accountName,
                    @RequestParam String password) {
    // 단일 파라미터를 직접 받아옴
}

📌 2. DB 연동 – MyBatis 사용 흐름

✅ MyBatis 구성 흐름

  1. 인터페이스 정의
public interface UserMapper {
    User selectByAccount(String accountName);
}

 

  2. XML 매핑 파일

<select id="selectByAccount" resultType="User">
    SELECT * FROM users WHERE account_name = #{accountName}
</select>
  1.  
  • 쿼리 1개 = 메서드 1개 = XML 태그 1개
  • SQL은 XML에, 메서드는 인터페이스에 정의

📌 3. 로그인 처리 흐름

  1. 로그인 요청 → @RequestParam으로 값 받기
  2. MyBatis로 DB에서 SELECT
  3. 로그인 성공 시 HttpSession에 사용자 정보 저장
  4. 이후 인증 체크는 세션 기반으로 처리
session.setAttribute("loginUser", user);

📌 4. Redirect의 의미

return "redirect:/home";
  • HTML을 바로 응답하는 게 아님
  • 브라우저에게 다시 요청하라고 지시
  • 즉, 2번 요청이 발생함 (POST-Redirect-GET 패턴)

📌 5. 게시판 & Thymeleaf 문법

✅ 로그인 여부에 따른 처리

<div th:if="${session.loginUser != null}">
    <p th:text="${session.loginUser.name} + '님 환영합니다'"></p>
    <a th:href="@{/logout}">로그아웃</a>
</div>
  • th:if: 조건부 렌더링
  • th:text: 텍스트 출력
  • th:href: 링크 처리

📌 6. 수정(Form) 화면에서 hidden 태그가 필요한 이유

<input type="hidden" name="id" th:value="${post.id}" />
  • 기본키(ID) 값을 숨겨서 전송하기 위함
  • 사용자는 수정 시 id 값을 볼 수 없지만, 서버는 어떤 게시물을 수정할지 알아야 함
  • 수정 처리는 보통 POST로 이루어지므로 id를 form에 포함해야 함

📌 7. List vs Map – 반복문 처리

  • List: 반복문(th:each) 돌릴 때 사용
  • Map: 특정 key로 객체를 꺼낼 때 사용
    (DTO와 비슷한 방식으로 속성 접근 가능)
<!-- List 예시 -->
<tr th:each="item : ${postList}">
    <td th:text="${item.title}"></td>
</tr>

<!-- Map 예시 -->
<p th:text="${userMap['admin'].name}"></p>

 

 

 

댓글 기능 구현

1.먼저 db에다 fp_comment 테이블 만들기

Create Table fp_comment(
id int primary key auto_increment,
articleId int,
userId int,
message VARCHAR(3000),
created_at DATETIME default NOW(),
FOREIGN KEY (articleId) REFERENCES fp_article(id) ON DELETE CASCADE,
FOREIGN KEY (userId) REFERENCES fp_user(id) ON DELETE CASCADE
);
 
  • foreign key 사용한 이유 :
  • 외래키를  댓글(CommentDto)을 등록할 때 이 댓글이 어떤 글(articleId)에 속하고, 누가(userId) 썼는지를 알아야 함
  • 두 테이블을 연결하는 다리역할(다른테이블의 기본키 참조)
  • 데이터 무결성을 위해
  • on delete cascade : foreign key로 테이블 간 관계를 연결해놨을 때, 부모 테이블의 행이 삭제되면 자식 테이블의 연관된 행도 자동으로 삭제되게 해주는 옵션

처음엔 외래키 설정 안 했을 시 → articleId넘기려고할때 자꾸 db든 서버든 articleId가 null값이 들어감

 1. articleId가 null로 저장되는 문제

  • 폼이나 DTO에서는 값이 들어왔는데, DB에는 null로 들어가는 경우 많음
  • 왜? 외래키 없으면 DB가 “이 값 진짜 존재하는 article인지” 검사 안 함
  • 실수로 빠지거나, 컨트롤러에서 안 넣어줘도 DB는 에러 없이 그냥 null 저장

 2. 존재하지 않는 게시글 ID로 댓글 저장 가능

  • 예: articleId = 999 이렇게 저장해도 DB는 에러 안 냄
  • 실제로는 그런 게시글이 없는데, 댓글만 DB에 저장됨 → 고아 데이터 생김

 3. JOIN 쿼리에서 에러 나거나 결과 없음

  • 댓글은 있는데 articleId가 null이거나, 존재하지 않는 게시글 ID라면?
  • 조인할 때 결과 안 뜨거나 null 뜸 → 프론트에서 에러 나기 딱 좋음

CommentDto 생성

package com.ca.finalproject.dto;

import java.time.LocalDateTime;

import lombok.Data;

@Data
public class CommentDto {
    private int id; // 댓글 고유 번호 (PK)
    private int articleId; // 어떤 게시글에 달린 댓글인지
    private int userId; // 댓글 작성자
    private String message; // 댓글 내용
    private LocalDateTime createdAt; // 작성 시각
}

readArticlePage.html에다 form 추가

<div class="row mb-4">
    <div class="col-md-12">
        <form action="/board/writeComment" method="post">
        <input type="hidden" name="articleId" th:value="${articleData.articleDto.id}" />
            <div class="input-group">
                <span class="input-group-text">
                	<i class="bi bi-chat-dots"></i>
                </span>

                <input type="text" name="message" class="form-control" placeholder="댓글을 입력하세요" required>

                <button type="submit" class="btn btn-primary">작성</button>
            </div>
        </form>
    </div>
</div>
  1. 사용자는 그냥 댓글만 입력해 (<input type="text" name="message" ...>).
  2. form이 submit될 때 articleId도 함께 서버로 전송됨 (숨겨진 상태로).
  3. 서버는 이 articleId를 보고 “이 댓글은 몇 번 게시글에 달린 거구나” 판단.
  4. 댓글 DB에 저장할 때 그걸 같이 넣음.

BoardController.java

@RequestMapping("writeComment")
public String writeComment(CommentDto commentDto,HttpSession session){
    UserDto sessionUser = (UserDto) session.getAttribute("sessionUserInfo");
    // System.out.println("sessionUser: " + sessionUser);

    if (sessionUser == null) {
        System.out.println("로그인 세션이 없습니다.");
        return "redirect:/user/loginPage"; // 로그인 안된 경우 로그인 페이지로 보냄
    }

    commentDto.setUserId(sessionUser.getId());
    boardService.writeComment(commentDto);
    return "redirect:/board/readArticlePage?id=" + commentDto.getArticleId();
}

BoardServiceImpl.java

public void writeComment(CommentDto commentDto){
	commentSqlMapper.insertComment(commentDto);
}

public List<Map<String, Object>> getCommentListByArticleId(int articleId){
    List<Map<String, Object>> resultList = new ArrayList<>();
    List<CommentDto> commentList = commentSqlMapper.findCommentsByArticleId(articleId);

    for(CommentDto commentDto : commentList){
        Map<String, Object> map = new HashMap<>();
        map.put("commentDto", commentDto);
        UserDto userDto = userSqlMapper.findUserById(commentDto.getUserId());
        map.put("userDto", userDto);
        resultList.add(map);
    }

    return resultList;
}

CommentSqlMapper.java

package com.ca.finalproject.board.mapper;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;

import com.ca.finalproject.dto.CommentDto;

@Mapper
public interface CommentSqlMapper {
    public void insertComment(CommentDto commentDto);
    public List<CommentDto> findCommentsByArticleId(int articleId);
    public int countCommentsByArticleId(int articleId);
}

CommentSqlMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.ca.finalproject.board.mapper.CommentSqlMapper">
<select id="findCommentsByArticleId" >
SELECT * FROM fp_comment WHERE articleId = #{articleId} ORDER BY created_at ASC
</select>

<insert id="insertComment">
INSERT INTO fp_comment (articleId,userId,message)
VALUES (#{articleId},#{userId},#{message});
</insert>

<select id="countCommentsByArticleId">
select count(*) from fp_comment where articleId=#{articleId}
</select>

</mapper>

 

근데 여기서 문제점 발견... db에단 commentCount가 연결안됨..

하지만 화면 출력,콘솔 출력은 잘됨 

 

뭐가 문제일까.. 

다시 해보쟈