[Node.js] 커뮤니티 게시글, 댓글, 대댓글 구현하기

HoonDong_K·2023년 2월 6일
2

[project #1] movie-inner

목록 보기
9/10

커뮤니티 구상


Movie-inner 프로젝트 주요 기능 중 하나인 커뮤니티는 회원들이 게시글과 그에 대한 댓글, 대댓글을 통해 자유롭게 의견을 나눌 수 있게 해줍니다.

커뮤니티에서 주로 사용하는 기능은 다음과 같습니다.

  1. 게시글 작성, 수정, 삭제
  2. 댓글 작성, 수정, 삭제
  3. 대댓글 작성, 수정, 삭제
  4. 인기 게시글 + 조회수
  5. 페이지 별 게시글 목록

게시글, 댓글, 대댓글 작성, 수정, 삭제

1,2,3 의 경우, 게시글과 댓글 Table을 통해 관리하였습니다.

communitycomments
idxidx
user_idxcontent_idx
titleuser_idx
contentcomment
fileresponse_to
hitcreated_at
created_atupdated_at
updated_at

community table의 경우, primary key를 idx로 설정하여 게시글 번호를 부여하였고, 나머지 열의 경우 유저가 작성한 게시글 정보가 입력됩니다.

comments table의 경우, 동일하게 primary key로 댓글 idx를 관리하였습니다.댓글과 대댓글 모두 동일하게 comments table에서 관리하였는데

댓글과 대댓글이 달린 게시글은 content_idx로,
작성자는 user_idx로,
내용은 comment에 담깁니다.

댓글과 대댓글을 구분하는 방식은 response_to로 구분하였습니다. 만약 댓글의 경우, null값으로 저장되고 대댓글의 경우, 대댓글이 달리는 댓글(상위 댓글)의 idx 가 저장됩니다.

인기 게시글과 조회 수 관리

보통 다른 커뮤니티 사이트를 구경하면 게시글 목록만 있는 것이 아닌, 현재 어떤 게시글이 가장 인기가 있는 지 Top 5를 선정하여 보여줍니다.

Movie-inner 에서도 동일한 기능을 구현하기 위해 각 게시글 별 조회 수를 추가하였고 이를 통해 조회 수가 높은 상위 5개의 게시글을 커뮤니티 상단에 보여주도록 구현하였습니다.

조회 수는 유저가 게시글을 클릭하여 내용을 열람할 시, community 테이블의 hit 칼럼이 1 씩 증가하도록 update시키는 API 를 구현하였습니다.

페이지 별 게시글 목록

이 부분이 가장 오래 걸렸습니다.

커뮤니티에 등록된 게시글의 수는 한 페이지에서 보여주기에 그 양이 너무 많기에, 여러 페이지로 나누어 게시글을 보여주어야 합니다. 또한, 게시글은 작성만 하는 것이 아닌 수정과 삭제를 통해 유기적으로 변동되기에 게시글 번호, 또는 각 페이지에 해당하는 게시글의 목록이 변할 수 밖에 없습니다.

그렇기 때문에 우리는 한 페이지에 몇 개의 게시글을 보여줄 것인지, 그 페이지에 속한 게시글들은 매 순간 어떻게 업데이트되는 지, 게시글의 번호는 어떻게 달아줄 것인 지 고민하였습니다.

그런 부분들을 고려하여 getContentsPerPages 모듈을 제작하여 커뮤니티 뿐만 아니라 댓글, 검색 결과 등 각 페이지 별 contents를 제공해야하는 서비스에 사용할 수 있게 하였습니다.

실제 프로젝트 적용


게시글, 댓글, 대댓글 작성, 수정, 삭제

게시글의 경우, 작성은 INSERT, 수정은 해당 게시글 idx를 통해 UPDATE, 삭제 또한 idx를 통한 DELETE를 수행하여 간단하게 DB를 관리할 수 있습니다.

반면 댓글과 대댓글의 경우도 비슷하지만, 이 부분에서도 댓글과 대댓글을 구분할 수 있게 구현하였습니다.

const { contentIdx, userIdx, comment, responseTo } = params
let response: any = []
let idx = 0
try {
  // 만약 responseTo의 값이 없을 경우, 댓글로 구분
    if (responseTo === undefined) {
        response = await connection.run(
    	    `INSERT INTO comments(content_idx,user_idx,comment) VALUES (?,?,?)`,
            [contentIdx, userIdx, comment]
        )
        const { insertId } = response
        idx = insertId
      // 대댓글로 구분
    } else {
        response = await connection.run(
            `INSERT INTO comments(content_idx,user_idx,comment,response_to) VALUES (?,?,?,?)`,
            [contentIdx, userIdx, comment, responseTo]
        )
        const { insertId } = response
        idx = insertId
    }
 	// 추후 알람 기능 구현을 위해 댓글 작성 내용을 response 해주었습니다.
    const getResponse = await connection.run(
        `SELECT CMT.idx,CMT.content_idx,CMTY.user_idx AS content_writer_idx,CMT.user_idx as comment_writer_idx,INFO.nickname,CMT.comment,CMT.response_to,CMT.created_at FROM comments AS CMT INNER JOIN user_info AS INFO ON CMT.user_idx=INFO.idx INNER JOIN community AS CMTY ON CMT.content_idx=CMTY.idx WHERE CMT.idx=?`,
        [idx]
    )
} catch (e: any) {
  console.error(e)
}

API 요청 시 함께 보내온 매개변수 중 responseTo의 값에 따라 댓글과 대댓글을 구분하였고, 그에 맞게 DB에 INSERT 해주었습니다.
그 외, 수정과 삭제는 게시글과 동일하게 UPDATE, DELETE절을 사용하여 구현하였습니다.

인기 게시글과 조회 수 관리

인기 게시글의 경우, 모든 게시글의 조회 수 중 상위 5개의 게시글을 가져오도록 구현하였고, 조회 수는 유저가 게시글을 열람할 때마다 1씩 올라가도록 설정하였습니다.

조회 수 설정 >

const { idx } = params // 게시글 idx
try {
    const response = await connection.run(
        `SELECT hit FROM community WHERE idx=?`,
        [idx]
    )
    const { hit } = response[0]
    const increaseHit = hit + 1
    await connection.run(`UPDATE community SET hit=? WHERE idx=?`, [
        increaseHit,
        idx,
    ])
} catch (e: any) {
    console.error(e)
}

인기 게시글 >

try {
  // 조회 수 별 상위 5개의 게시글 정보 불러오기
    const response = await connection.run(
   `SELECT idx,title,created_at FROM community ORDER BY hit DESC LIMIT 5`
    )
    changeDbTimeForm(response)
    return {        
       status: 200,
       data: { top5Contents: response },
    }
} catch (e: any) {
    console.error(e)
}

페이지 별 게시글 목록

커뮤니티 페이지에 유저가 접속하였을 때, 게시글 목록을 보여주기 위해서 저희가 고려했던 사항은 다음과 같습니다.

  1. 게시글 번호
  2. 한 페이지 당 보여줄 게시글의 수
  3. 각 페이지 별 보여줄 게시글 목록

1번 게시글 번호의 경우,

communtiy Table에 저장되어 있는 게시글 번호인 idx와는 다른, 웹 페이지 상 보여줘야 하는 게시글 번호가 따로 더 필요했습니다. DB에 저장된 idx의 경우, 각 게시글의 Primary Key이기 때문에 다른 게시글이 삭제되어도 수정되지 않는 고유값으로 저장되어야 합니다.

하지만 웹 페이지 상의 게시글 번호는 게시글이 삭제되거나 추가될 때마다 변경되어야 하는, idx와는 다른 번호가 추가적으로 필요합니다.

//array - DB로부터 불러온 모든 게시글 배열
for (let i = 0; i < array.length; i++) {
    array[i]["number"] = i + 1
}

DB에서 불러온 모든 게시글 배열에서 각 게시글의 정보를 담은 객체에 number key값을 설정하고 반복문을 통해 숫자를 부여해주었습니다.

2번 한 페이지 당 보여줄 게시글의 수의 경우,

페이지마다 보여줄 게시글의 수를 설정하여야 합니다. 이번 프로젝트의 경우, 한 페이지 당 20개의 게시글을 보여주기로 결정하였고

총 게시글의 수가 𝑛 개일 경우에, 첫 페이지는 𝑛번부터 𝑛-19번의 게시글이 보여지고 두 번째 페이지는 𝑛-20번부터 𝑛-39번까지,, 마지막 페이지는 결국 1번으로 오게 됩니다.

3. 각 페이지 별 보여줄 게시글 목록의 경우,

각 페이지 별 게시글 목록을 보여주기 전, 저희는 총 게시글 수에 따른 총 페이지 수를 계산하여야 했습니다. 예를 들어, 총 20개의 게시글이 DB에 저장되어 있고, 한 페이지 당 20개의 게시글을 보여주기로 했다면 총 페이지 수는 1이 됩니다.

만약, 이 상황에서 유저가 하나의 게시글을 작성하여 총 게시글이 21개가 된다면 총 페이지 수는 2가 될 겁니다.

// 총 게시글 수
let contentsNumber: number = array.length

// 총 페이지 수
const pageNumber = Math.trunc(contentsNumber / contentsNumberPerPage)

const totalPage =
    contentsNumber % contentsNumberPerPage === 0
        ? pageNumber
        : pageNumber + 1

pageNumber는 총 게시글의 수에서 한 페이지에 보여줄 게시글 수를 나눈 몫이 됩니다.

하지만 만약 총 게시물이 21개라면, pageNumber가 1이 될텐데, 실제 페이지 수는 2가 되어야 합니다. 이를 위해서, 총 게시글의 수에서 한 페이지에 보여줄 게시글의 수를 나눈 나머지가 0일 경우, pageNumber가 총 페이지 수가 될 것이며, 그 외의 경우 pageNumber + 1 이 총 페이지 수가 될 것입니다.

//contentsNumber : 총 게시글 수 / contentsNumberPerPage : 페이지 당 게시글 수
let contents: any = {}
for (let i = 1; i < totalPage + 1; i++) {
    contents[i] = []
    for (
        let j = contentsNumber;
        contentsNumber - contentsNumberPerPage > 0
            ? j > contentsNumber - contentsNumberPerPage
            : j > 0;
        j--
    ) {
        contents[i].push(array[j - 1])
    }
    contentsNumber -= contentsNumberPerPage
}

contents라는 객체에 페이지 번호를 Key 값으로 주고, 그 페이지에 해당하는 게시글 목록을 Value 값으로 줄 계획입니다.

첫 번째 반복문은 contents의 Key 값을 만드는 과정이고, 두 번째 반복문은 Value인 게시글들을 push하는 과정입니다.

contentsNumber가 21이고 contentsNumberPerPage가 20이라고 가정해보겠습니다. (totalPage는 2가 될 겁니다.)

contents[1]의 반복문에서 j는 21부터 시작할 것이고 반복문의 조건문에서

contentsNumber - contentsNumberPerPage > 0

는 항상 참을 의미할테니 j가 1 (contentsNumber - contentsNumberPerPage) 초과일 경우, array[20]부터 array[1] 까지 contents[1]에 담길 것입니다.

j가 1일 경우, 조건문에 허용되지 않아 반복문을 빠져나오고

contentsNumber -= contentsNumberPerPage

에 의해 contentsNumber는 1이 될 것입니다.

이후, contents[2] 반복문이 시작되고 이번에는 두 번째 반복문의 j는 1부터 시작할 것입니다. 조건문에 의해 j가 0을 초과일 경우, contents[2]에 담기게 되기 때문에 array[0]은 2번째 페이지에 담기게 됩니다.

조금 복잡할 수 있지만 간단하게 생각한다면

contentsNumber - contentsNumberPerPage > 0
	// 총 게시글 수 > 페이지 당 게시글 수
	? j > contentsNumber - contentsNumberPerPage
	// 총 게시글 수 < 페이지 당 게시글 수
	: j > 0;

에 따라 조건이 분류되며 반복문이 지난 뒤

contentsNumber -= contentsNumberPerPage

총 게시글 수가 페이지 당 게시글 수만큼 차감되며 각 페이지에 담기는 원리입니다.

코드가 복잡하여 한 번에 이해하기 어렵지만 나름 뿌듯하게 작성했던 코드인 것 같습니다.

profile
개발을 잘하고 싶은 개발자

0개의 댓글