37 minute read

게시판

프로젝트 구조도

├─ BulletinBoard
├─ BulletinBoard.js
├─ BulletinBoardComment.js
├─ BulletinBoardHashTag.js
├─ BulletinBoardModal.js
├─ BulletinBoardPages.js
├─ BulletinBoardPagesEdit.js
├─ FormattedDate.js
├─ HashTagCustom.js
├─ HashtagCustom.css
├─ LikeHeartBtn.js
├─ UpdatePostCount.js
└─ css
│ ├─ BulletinBoard.css
│ ├─ BulletinBoardComment.css
│ ├─ BulletinBoardHashTag.css
│ └─ LikeHeartBtn.css

image

image

image

image

image

image

image

image

image

image

image

image

image

image

Front-end

서버에서 데이터 가져오기

// 백엔드단에서 리스트 객체를 가져오는 부분
  useEffect(() => {
    axios
      .get("/api/bulletinboard")
      .then((res) => {
        const sortedData = res.data.sort(
          (a, b) => new Date(b.postDate) - new Date(a.postDate)
        );
        setBulletinBoardPost(sortedData);
        message.success("데이터를 성공적으로 갱신하였습니다.");
      })
      .catch((error) =>
        message.error("데이터를 갱신하는 도중 에러가 발생하였습니다.")
      );
  }, []);
  1. useEffect 훅: 이 코드는 React 컴포넌트 내에서 useEffect 훅을 사용하고 있습니다. useEffect는 컴포넌트가 렌더링되고 난 후에 비동기 작업을 수행하기 위해 사용됩니다. 빈 배열 ([])을 두 번째 매개변수로 전달하여, 컴포넌트가 처음 렌더링될 때 한 번만 실행되도록 설정되어 있습니다. 즉, 컴포넌트가 처음으로 마운트될 때만 실행됩니다.
  2. Axios를 사용한 데이터 가져오기: Axios는 HTTP 요청을 보내고 받는 데 사용되는 라이브러리입니다. axios.get("/api/bulletinboard")를 통해 백엔드 API로 GET 요청을 보냅니다. 백엔드는 게시판 게시물 목록을 반환할 것으로 예상됩니다.
  3. 비동기 요청 처리: axios는 Promise를 반환하므로 .then().catch()를 사용하여 비동기 요청의 성공 및 실패를 처리합니다.
  • .then((res) => { ... }) : 성공한 경우, 백엔드에서 반환한 데이터는 res 매개변수에 들어옵니다. 이 데이터는 게시물 목록으로 예상되며, 날짜(postDate)를 기준으로 내림차순으로 정렬됩니다. 정렬된 데이터는 setBulletinBoardPost 함수를 사용하여 React 컴포넌트의 상태로 설정됩니다. 이로써 게시물 목록이 컴포넌트의 상태로 저장되어 화면에 표시됩니다.
  • .catch((error) => { ... }) : 실패한 경우, 오류 메시지를 화면에 표시하기 위해 message.error("데이터를 갱신하는 도중 에러가 발생하였습니다.") 가 호출됩니다.
  1. 성공 메시지 표시: 데이터 갱신이 성공하면 message.success("데이터를 성공적으로 갱신하였습니다.") 를 사용하여 성공 메시지가 화면에 표시됩니다.

이렇게 코드는 백엔드 API에서 데이터를 가져와서 정렬한 후 React 컴포넌트의 상태로 설정하고, 성공 또는 실패에 따라 메시지를 표시하는 간단한 데이터 로딩 및 처리 로직을 구현하고 있습니다.

게시판 카테고리

// 게시판 카테고리
const categoryOptions = [
  { label: "전체", key: "전체 게시판", value: "CategoryAll" },
  { label: "공지사항", key: "공지사항", value: "CategoryNotice" },
  { label: "사내게시판", key: "사내게시판", value: "Categoryemployee" },
  // ... 다른 카테고리 옵션 ...
];

// 선택한 카테고리의 label 값
const handleCategoryChange = (event) => {
  const selectedCategory = event.target.value;
  setPostCategory(selectedCategory);
  if (selectedCategory === "CategoryAll") {
    setCurrentPagePosts(bulletinBoardPost.slice(0, postsPerPage));
  } else {
    const filteredPosts = bulletinBoardPost.filter(
      (post) =>
        post.postCategory === selectedCategory ||
        selectedCategory === "전체 게시판"
    );
    setCurrentPagePosts(filteredPosts.slice(0, postsPerPage));
  }
};

이 코드는 게시판에서 게시물을 카테고리별로 필터링하고, 선택한 카테고리에 따라 현재 페이지에 보여질 게시물을 업데이트하는 부분을 구현한 것입니다. 코드를 상세하게 설명하겠습니다:

  1. categoryOptions: 이 배열은 게시판의 카테고리 옵션을 정의합니다. 각 항목은 객체로 표현되며, label은 화면에 표시될 카테고리명, key는 내부 식별자(예: 코드에서 사용) 및 value는 카테고리를 식별할 값을 나타냅니다. “전체” 카테고리를 나타내는 항목과 다른 게시판 카테고리도 나열되어 있습니다.
  2. handleCategoryChange 함수: 이 함수는 사용자가 카테고리 선택 상자를 변경할 때 호출됩니다. 이 함수는 선택한 카테고리를 받아와서 현재 선택한 카테고리를 setPostCategory 함수를 사용하여 React 컴포넌트의 상태로 설정합니다.
  3. 카테고리에 따른 게시물 필터링:
  • 만약 선택한 카테고리가 “전체 게시판”(CategoryAll)인 경우, 모든 게시물을 보여주기 위해 bulletinBoardPost 배열에서 처음부터 postsPerPage까지의 게시물을 setCurrentPagePosts 함수를 사용하여 현재 페이지 게시물로 설정합니다.
  • 그렇지 않은 경우, 선택한 카테고리에 해당하는 게시물만 필터링하여 filteredPosts 배열에 저장합니다. 이때 postCategory는 게시물 객체의 카테고리를 나타내며, selectedCategory는 사용자가 선택한 카테고리를 나타냅니다. “전체 게시판”을 선택한 경우에도 해당 항목을 포함한 게시물을 보여줍니다.
  • 마지막으로, setCurrentPagePosts 함수를 사용하여 현재 페이지에 보여질 게시물을 filteredPosts 배열에서 처음부터 postsPerPage까지의 게시물로 설정합니다.

이렇게 코드는 사용자가 카테고리를 선택할 때 선택한 카테고리에 따라 화면에 표시되는 게시물을 필터링하고 업데이트하는 데 사용됩니다.

카테고리 렌더링 방법

<CFormSelect
  aria-label="Default select example"
  options={categoryOptions.map((option) => ({
    label: option.label,
    value: option.key,
  }))}
  onChange={handleCategoryChange}
  value={postCategory}
/>

조회수 이벤트

// `updatePostCount`는 게시물의 조회수를 업데이트하는 비동기 함수
const updatePostCount = async (postNum, postCountNum) => {
  try {
    // 서버에 PUT 요청을 보내서 게시물의 조회수를 업데이트
    const response = await axios.put(`/api/bulletinboard/update/${postNum}`, {
      // 기존 조회수에 1을 더한 값을 요청 본문에 포함
      postCountNum: postCountNum + 1,
    });
    // 응답의 상태 코드가 200일 경우, 성공으로 판단
    if (response.status === 200) {
      console.log("조회수 업데이트 성공");

      // 클라이언트 측에서 현재 저장된 게시물 목록을 순회하며
      const updatedBulletinBoardPost = bulletinBoardPost.map((post) => {
        // 업데이트 대상 게시물을 찾았을 경우
        if (post.postNum === postNum) {
          // 해당 게시물의 조회수만 업데이트하고 나머지 정보는 그대로 유지
          return { ...post, postCountNum: postCountNum + 1 };
        } else {
          // 업데이트 대상이 아닌 게시물은 그대로 반환
          return post;
        }
      });
      // 업데이트된 게시물 목록을 콘솔에 출력
      console.log(updatedBulletinBoardPost);
      // 상태를 업데이트하여 리렌더링을 유발
      setBulletinBoardPost(updatedBulletinBoardPost);
    }
  } catch (error) {
    // 요청 중 오류가 발생하면 오류를 콘솔에 출력합니다.
    console.error("조회수 업데이트 실패", error);
  }
};
  1. 게시물 번호(postNum)와 현재 조회수(postCountNum)를 인자로 받는 함수입니다.
  2. axios를 이용하여 백엔드에 해당 게시물의 조회수를 1 증가시키라는 PUT 요청을 보냅니다.
  3. 만약 백엔드로부터 성공적인 응답(200 상태 코드)을 받으면, 클라이언트 측의 게시물 목록(bulletinBoardPost)에서 해당 게시물을 찾아 그 조회수를 1 증가시킵니다.
  4. 해당 게시물의 조회수가 업데이트 된 새로운 목록을 setBulletinBoardPost를 이용하여 상태를 업데이트하여 리렌더링을 유발시킵니다.
  5. 만약 중간에 어떤 오류가 발생하면, 오류 메시지를 콘솔에 출력합니다.

게시판 검색창

// `handleSearchTermChange` 함수는 검색어 입력 필드의 값이 변경될 때 호출
const handleSearchTermChange = (event) => {
  // 이벤트 객체로부터 대상의 값(검색어)을 가져와 `setSearchTerm`을 사용하여 상태를 업데이트
  setSearchTerm(event.target.value);
};

// `handleSearch` 함수는 검색 버튼 클릭 시나 다른 검색 트리거에 의해 호출
const handleSearch = () => {
  // `bulletinBoardPost` 배열을 순회하면서 게시물의 제목이 검색어를 포함하고 있는지 확인
  const searchedPosts = bulletinBoardPost.filter((post) =>
    post.postTitle.includes(searchTerm)
  );
  // 검색 결과에 해당하는 게시물들을 `setSearchedPosts`를 이용하여 상태를 업데이트
  setSearchedPosts(searchedPosts);
};

// `useEffect`는 React의 Side Effect Hook 이다!
useEffect(() => {
  // 게시물 목록(`bulletinBoardPost`)이 변경될 때마다 호출된다.

  // 게시물 목록이 변경되면 검색된 게시물 목록(`searchedPosts`)을
  // 전체 게시물 목록으로 리셋. 즉, 검색 필터링을 초기화한다.
  setSearchedPosts(bulletinBoardPost);

  // `bulletinBoardPost`의 변경을 감지하여 이 `useEffect`를 실행한다
}, [bulletinBoardPost]);

로직 요약:

  1. handleSearchTermChange: 사용자가 검색어 입력 필드의 값을 변경할 때마다 해당 값을 검색어 상태(searchTerm)로 설정합니다.
  2. handleSearch: 사용자가 검색을 실행하려고 할 때, 현재 게시물 목록(bulletinBoardPost)에서 검색어가 포함된 게시물을 필터링하여 검색된 게시물 상태(searchedPosts)를 업데이트합니다.
  3. useEffect: 게시물 목록(bulletinBoardPost)이 변경될 때마다 검색된 게시물 목록(searchedPosts)을 전체 게시물 목록으로 초기화하여 검색 필터링을 리셋합니다.

페이지네이션

// `onChange` 함수는 페이지가 변경될 때마다 호출됩니다.
const onChange = (page) => {
  // 현재 페이지 번호를 상태로 설정합니다.
  setCurrent(page);

  // 현재 페이지의 시작 인덱스를 계산합니다.
  const startIndex = (page - 1) * postsPerPage;
  // 현재 페이지의 끝 인덱스를 계산합니다.
  const endIndex = startIndex + postsPerPage;

  // `bulletinBoardPost` 배열을 순회하면서
  // 게시물의 제목이 검색어를 포함하고 있는지 확인하여 필터링합니다.
  const filteredPosts = bulletinBoardPost.filter((post) =>
    post.postTitle.includes(searchTerm)
  );

  // 필터링된 게시물 목록에서 현재 페이지에 해당하는 부분만 잘라냅니다.
  setCurrentPagePosts(filteredPosts.slice(startIndex, endIndex));
};

로직 요약:

  1. onChange 함수는 페이지 번호가 변경될 때 호출되며, 변경된 페이지 번호를 인자로 받습니다.
  2. 해당 페이지 번호를 기반으로 게시물 목록에서 시작 인덱스(startIndex)와 끝 인덱스(endIndex)를 계산합니다.
  3. 게시물 목록(bulletinBoardPost)을 순회하면서 검색어(searchTerm)가 포함된 게시물만 필터링합니다.
  4. 필터링된 게시물 목록에서 현재 페이지에 해당하는 부분을 잘라내어, 현재 페이지의 게시물 목록(currentPagePosts) 상태를 업데이트합니다.



게시물 상세페이지

// 유저 정보 받아오기
import Cookies from "js-cookie";

const BulletinBoardPages = (props) => {
  // 게시물의 ID를 URL로부터 얻기
  const { id } = useParams();

  // 게시물과 댓글 상태 설정
  const [post, setPost] = useState(null);
  const [comments, setComments] = useState([]);

  // 유저 정보 가져오기
  const staffInfo = JSON.parse(Cookies.get("staffInfo"));

  // 현재 게시물의 ID를 숫자로 변환
  const currentId = parseInt(id, 10);
  // 이전 및 다음 게시물의 ID 계산
  const prevPost = currentId - 1;
  const nextPost = currentId + 1;

  // 최대 및 최소 게시물 ID 상태 설정
  const [maxPostId, setMaxPostId] = useState(null);
  const [minPostId] = useState(1); // 게시물 ID가 1부터 시작한다고 가정

  // 유저 정보 로깅
  console.log(staffInfo);

  // 게시물에 대한 댓글 불러오기
  useEffect(() => {
    fetch(`/api/bulletinboard/BulletinBoardPages/${id}/comments`)
      .then((response) => response.json())
      .then((comment) => {
        console.log(id);
        setComments(comment); // 댓글 상태 설정
      });
  }, [id]);

  // 선택된 ID에 해당하는 게시물 데이터 불러오기
  useEffect(() => {
    fetch(`/api/bulletinboard/BulletinBoardPages/${id}`)
      .then((response) => response.json())
      .then((data) => {
        setPost(data); // 게시물 상태 설정
      });
  }, [id]);

  // 게시물 데이터가 아직 없으면 로딩 메시지 표시
  if (!post) return <div>Loading...</div>;

  // 게시물 삭제 기능
  const handleDelete = async () => {
    try {
      // 사용자 확인 요청
      if (!confirm("게시물을 삭제하시겠습니까?")) {
        alert("취소 되었습니다.");
        return;
      }

      // 게시물 삭제 API 호출
      await axios.delete(
        `/api/bulletinboard/BulletinBoardPages/${post.postNum}`
      );

      alert("삭제 되었습니다.");
      navigate("/BulletinBoard/BulletinBoard"); // 게시판 메인 페이지로 이동
    } catch (error) {
      console.error("삭제 불가", error);
    }
  };

  // ... 나머지 코드 ...
};
  1. 유저 정보: Cookies를 통해 로컬 쿠키에서 유저 정보인 staffInfo를 가져옵니다.
  2. 게시물 및 댓글 상태 설정: React의 useState를 사용하여 게시물과 댓글 정보를 저장하기 위한 상태를 초기화합니다.
  3. 게시물 ID 처리: 현재 페이지의 게시물 ID를 가져오며, 이를 기반으로 이전 및 다음 게시물의 ID를 계산합니다.
  4. 댓글 불러오기: useEffect를 사용하여 게시물의 ID가 변경될 때마다 해당 게시물의 댓글을 API 호출로 불러옵니다.
  5. 게시물 데이터 불러오기: 마찬가지로 useEffect를 사용하여 게시물의 ID가 변경될 때 해당 게시물의 데이터를 API 호출로 불러옵니다.
  6. 게시물 삭제: 게시물을 삭제하기 전 사용자에게 확인을 받고, 확인된 경우 해당 게시물을 삭제하는 API를 호출합니다. 삭제 후에는 게시판의 메인 페이지로 이동합니다.



// 게시물의 수정 및 삭제 버튼 영역
<div className="d-grid gap-2 d-md-flex justify-content-md-end BulletinBoardPages-AddDelBtn">
  {/* 현재 로그인한 사용자가 게시물 작성자와 동일할 경우 삭제 버튼을 표시 */}
  {staffInfo.empId === post.empId && (
    <CButton color="danger" role="button" onClick={handleDelete}>
      삭제
    </CButton>
  )}

  {/* 현재 로그인한 사용자가 게시물 작성자와 동일할 경우 수정 버튼을 표시 */}
  {staffInfo.empId === post.empId && (
    <CButton
      color="success"
      role="button"
      onClick={() => {
        // 수정 버튼 클릭 시, 게시물 수정 페이지로 이동
        navigate(`/BulletinBoard/BulletinBoardPagesEdit/${post.postNum}`);
      }}
    >
      수정
    </CButton>
  )}
</div>

설명:

  1. 버튼 영역의 스타일: 이 div는 게시물의 수정 및 삭제 버튼을 담고 있으며, Bootstrap의 유틸리티 클래스를 사용하여 버튼의 배치 및 간격을 조절하고 있습니다.
  2. 삭제 버튼 표시 조건: staffInfo.empId === post.empId 조건은 현재 로그인한 사용자의 ID와 게시물의 작성자 ID를 비교하여 일치할 경우에만 삭제 버튼을 표시하도록 합니다. 이로 인해 게시물의 작성자만 해당 게시물을 삭제할 수 있습니다.
  3. 수정 버튼 표시 조건: 삭제 버튼과 마찬가지로 현재 로그인한 사용자의 ID와 게시물의 작성자 ID가 일치할 경우에만 수정 버튼을 표시합니다.
  4. 수정 버튼 클릭 동작: 수정 버튼을 클릭하면 navigate 함수를 사용하여 게시물 수정 페이지로 이동합니다. 이 때, 게시물의 고유 번호 (post.postNum)를 URL에 포함하여 해당 게시물의 수정 페이지로 이동하게 됩니다.

이러한 방식으로 게시물의 작성자만 해당 게시물을 수정하거나 삭제할 수 있는 권한을 가지게 되며, 다른 사용자는 이러한 버튼을 볼 수조차 없게 됩니다.



게시물 수정

import "./css/BulletinBoard.css";
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useParams } from "react-router-dom";
import { Container, Col, Row } from "react-bootstrap";
import { CKEditor } from "@ckeditor/ckeditor5-react";
import ClassicEditor from "@ckeditor/ckeditor5-build-classic";
import BulletinBoardHashTag from "./BulletinBoardHashTag";

import axios from "axios";

import {
  CButton,
  CFormTextarea,
  CCard,
  CCol,
  CForm,
  CBadge,
  CFormSelect,
  CRow,
  CFormInput,
} from "@coreui/react";
import { Input, Pagination } from "antd";
import LikeHeartBtn from "./LikeHeartBtn"; // Make sure the path is correct

const BulletinBoardPagesEdit = (props) => {
  const [postTitle, setPostTitle] = useState("");
  const [postContent, setPostContent] = useState("");
  const [hashtagName, setHashtagName] = useState("");
  const [postCategory, setPostCategory] = useState("");
  const [selectedHashTags, setSelectedHashTags] = useState([]);

  //게시물 불러오기
  const { id } = useParams();
  const [post, setPost] = useState(null);
  const navigate = useNavigate();

  useEffect(() => {
    fetch(`/api/bulletinboard/BulletinBoardPages/${id}`)
      .then((response) => response.json())
      .then((data) => {
        setPost(data);
        setPostTitle(data.postTitle);
        setPostContent(data.postContent);
        setHashtagName(data.hashtagName);
        setPostCategory(data.postCategory);
        setSelectedHashTags(data.hashtagName.split(","));
      });
  }, [id]);

  const categoryOptions = [
    { label: "공지사항", key: "공지사항", value: "CategoryNotice" },
    { label: "사내게시판", key: "사내게시판", value: "Categoryemployee" },
  ];

  const handleCategoryChange = (event) => {
    const selectedLabel = event.target.value;
    setPostCategory(selectedLabel);
  };

  const handleHashTagChange = (newHashTags) => {
    setSelectedHashTags(newHashTags);
  };

  const handlePostUpdate = async () => {
    try {
      const updatedPost = {
        postTitle: postTitle,
        postContent: postContent,
        hashtagName: selectedHashTags.join(","),
        postCategory: postCategory,
        postDateEdit: new Date(),
      };

      // Send the updatedPost data to the backend API
      await axios.put(
        `/api/bulletinboard/BulletinBoardPagesEdit/update/${id}`,
        updatedPost
      );

      alert("게시물이 정상적으로 수정되었습니다.");

      navigate(`/BulletinBoard/BulletinBoardPages/${id}`);
    } catch (error) {
      console.error("There was an error updating the post!", error);
    }
  };

  return (
    <>
      <Container className="BulletinBoardPages-Container">
        <CCard className="BulletinBoardPages-CCard">
          <CRow className="BulletinBoardModalInputSize">
            <h2>게시글 수정</h2>
            <CCol md={3}>
              <CFormSelect
                aria-label="카테고리"
                options={categoryOptions.map((option) => ({
                  label: option.label,
                  value: option.label,
                }))}
                onChange={handleCategoryChange}
                value={postCategory}
              />
            </CCol>
            <CCol md={9}>
              <CFormInput
                className="title-input"
                type="text"
                placeholder="제목"
                name="postTitle"
                value={postTitle}
                onChange={(e) => setPostTitle(e.target.value)}
              />
            </CCol>
          </CRow>

          <CKEditor
            editor={ClassicEditor}
            data={postContent}
            value={postContent}
            onReady={(editor) => {
              console.log("에디터가 준비되었습니다!", editor);
            }}
            onChange={(event, editor) => {
              const data = editor.getData();
              setPostContent(data);
            }}
          />

          <BulletinBoardHashTag
            onHashTagChange={handleHashTagChange}
            value={hashtagName}
          />

          <div className="d-grid gap-2 d-md-flex justify-content-md-end BulletinBoardModalBtn">
            <button
              type="button"
              className="btn btn-outline-warning me-md-2"
              onClick={() => {
                navigate(`/BulletinBoard/BulletinBoardPages/${id}`);
              }}
            >
              취소하기
            </button>
            <button
              className="btn btn-outline-primary me-md-2"
              onClick={handlePostUpdate}
            >
              수정하기
            </button>
          </div>
        </CCard>
      </Container>
    </>
  );
};
export default BulletinBoardPagesEdit;

코드를 살펴보면, 주어진 BulletinBoardPagesEdit라는 컴포넌트는 사용자가 게시글을 수정할 수 있는 기능을 제공합니다. 다음은 주요 기능 및 코드 구성에 대한 설명입니다.

  1. Imports 및 초기 설정: 여러 라이브러리와 컴포넌트들이 가져옵니다. CKEditor를 포함하여 필요한 컴포넌트와 스타일을 import 합니다.
  2. State 초기화:

    const [postTitle, setPostTitle] = useState("");
    const [postContent, setPostContent] = useState("");
    const [hashtagName, setHashtagName] = useState("");
    const [postCategory, setPostCategory] = useState("");
    const [selectedHashTags, setSelectedHashTags] = useState([]);
    

    위 코드는 컴포넌트 내에서 사용될 상태 변수들을 초기화합니다

  3. 게시물 정보 불러오기:

    const { id } = useParams();
    useEffect(() => { ... });
    
    

    페이지 URL에서 게시물의 ID를 가져와서 해당 게시물의 데이터를 fetch하여 상태를 설정합니다.

  4. 카테고리 옵션 및 카테고리 변경 핸들러:

    javascriptCopy code
    const categoryOptions = [ ... ];
    const handleCategoryChange = (event) => { ... };
    
    

    카테고리 옵션을 정의하고, 선택된 카테고리에 따라 상태를 업데이트하는 함수를 정의합니다.

  5. 해시태그 변경 핸들러:

    javascriptCopy code
    const handleHashTagChange = (newHashTags) => { ... };
    
    

    사용자가 해시태그를 변경할 때 해당 해시태그 리스트를 업데이트하는 함수입니다.

  6. 게시물 업데이트 핸들러:

    const handlePostUpdate = async () => { ... };
    
    

    사용자가 수정 버튼을 클릭했을 때 게시물 정보를 업데이트하는 함수입니다. Axios를 사용하여 API 엔드포인트에 PUT 요청을 보내서 게시물 정보를 업데이트하고, 성공적으로 업데이트되면 해당 게시물 페이지로 리다이렉트됩니다.

  7. 렌더링:
    • 컴포넌트는 Bootstrap과 CoreUI 라이브러리를 사용하여 게시글 수정 UI를 렌더링합니다.
    • CKEditor를 사용하여 게시글의 내용을 편집합니다.
    • BulletinBoardHashTag 컴포넌트를 사용하여 해시태그를 수정할 수 있게 합니다. .
    • 취소 및 수정 버튼을 통해 사용자는 게시물을 수정하거나 수정을 취소할 수 있습니다

이 컴포넌트는 게시글의 정보를 불러와서 제목, 내용, 해시태그, 카테고리 등을 수정할 수 있게 합니다.

수정한 내용을 저장하면 백엔드 서버에 변경 사항이 업데이트되며, 사용자는 해당 게시물 페이지로 리다이렉트됩니다.

게시글 댓글

// 부모댓글 입력 로직
  ****const handleSubmitComment = async () => {
    try {
      const data = {
        commentDate: new Date(),
        postNum: postNum,
        empId: staffInfo.empId, // staffInfo에서 empId 가져옴
        empNum: staffInfo.empNum, // staffInfo에서 empNum 가져옴
        email: staffInfo.email, // staffInfo에서 email 가져옴
        commentContent: comment,
      };

      const response = await axios.post(
        `api/bulletinboard/BulletinBoardPages/addParentComment/${postNum}`,
        data
      );

      setComment("");
      alert("정상 등록 되었습니다.");
      location.reload();
    } catch (error) {
      console.error("에러 발생:", error);
    }
  };
 //자식 댓글
  **** const handleSubmit = async () => {
    try {
      const data = {
        commentDate: new Date(),
        postNum: postNum,
        parentCommentId: parentComment, // 수정된 부분
        empId: staffInfo.empId, // staffInfo에서 empId 가져옴
        empNum: staffInfo.empNum, // staffInfo에서 empNum 가져옴
        email: staffInfo.email, // staffInfo에서 email 가져옴
        commentContent: reply,
      };

      const response = await axios.post(
        `api/bulletinboard/BulletinBoardPages/addChildComment/${postNum}`,
        data
      );

      setReply("");
      alert("정상 등록 되었습니다.");
      location.reload();
    } catch (error) {
      console.error("에러 발생:", error);
      console.log(parentComment);
    }
  };
// CommentContent 컴포넌트 정의
// 댓글 내용을 표시하고 관리하는 React 함수 컴포넌트입니다.
const CommentContent = ({
  author, // 댓글 작성자
  time, // 댓글 작성 시간
  text, // 댓글 내용
  postNum, // 해당 댓글이 속한 게시글 번호
  commentId, // 댓글의 고유 ID
  empId, // 댓글 작성자의 직원 ID
  isChild = false, // 댓글이 부모 댓글인지 자식 댓글인지 판별하는 prop. 기본값은 false, 즉 부모 댓글입니다.
}) => {
  // 댓글 편집 상태를 관리하는 state. 기본값은 false.
  const [editing, setEditing] = useState(false);

  // 편집된 댓글 내용을 관리하는 state. 기본값은 전달된 text prop입니다.
  const [editedText, setEditedText] = useState(text);

  // 로그인한 사용자의 정보를 쿠키에서 가져옵니다.
  const staffInfo = JSON.parse(Cookies.get("staffInfo"));

  // 댓글 작성자의 empId와 현재 로그인한 사용자의 empId를 콘솔에 출력합니다.
  console.log("댓글 작성자 empId:", empId);
  console.log("현재 로그인한 사용자 empId:", staffInfo.empId);

  // 댓글 편집 입력 필드의 값을 state에 반영하는 함수입니다.
  const handleEditChange = (event) => {
    setEditedText(event.target.value);
  };

  // 댓글을 업데이트하는 함수입니다.
  const handleUpdate = async () => {
    // 현재 로그인한 사용자가 댓글의 작성자가 아닐 경우
    if (empId !== staffInfo.empId) {
      alert("댓글의 작성자만 수정할 수 있습니다.");
      return;
    }

    try {
      // 업데이트할 댓글 데이터를 정의합니다.
      const data = {
        commentContent: editedText,
        commentEditDate: new Date(),
      };

      // 부모 댓글과 자식 댓글에 대한 API 엔드포인트를 구분합니다.
      // isChild 값이 true면 자식 댓글의 엔드포인트를, false면 부모 댓글의 엔드포인트를 사용합니다.
      const endPoint = isChild
        ? `/api/bulletinboard/BulletinBoardPages/${postNum}/updateChildComment/${commentId}`
        : `/api/bulletinboard/BulletinBoardPages/${postNum}/updateParentComment/${commentId}`;

      // axios를 사용해 댓글 데이터를 서버에 업데이트 요청합니다.
      await axios.put(endPoint, data);

      // 성공적으로 댓글이 업데이트 되었을 경우, 사용자에게 알림 메시지를 표시합니다.
      alert("댓글이 성공적으로 수정되었습니다.");

      // (여기서부터 코드가 잘림. 이후의 작업이나 추가적인 동작은 제공된 코드에서 확인할 수 없습니다.)
    } catch (error) {
      // 오류가 발생한 경우, 오류 메시지나 관련 동작을 여기에서 처리할 수 있습니다.
      // 현재 코드에서는 해당 부분이 제공되지 않았습니다.
    }
  };

  // ... (컴포넌트의 나머지 부분)
};
          <CRow>
            {/* 부모 댓글 */}
            <CommentContent
              author={empId}
              time={<FormattedDate date={commentDate} />}
              text={commentContent}
              onUpdate={handleUpdateComment}
              postNum={postNum}
              commentId={commentId}
              empId={empId}
            />
            <ReplyForm postNum={postNum} parentComment={commentId} />
          </CRow>
        </div>
      </CForm>
      {/* 자식 댓글들 */}
      {childComments && childComments.length > 0 ? (
        <ul>
          {childComments.map((childComment, index) => (
            <li key={index} className="childText">
              <CRow>
                <CommentContent
                  author={childComment.empId}
                  time={<FormattedDate date={childComment.commentDate} />}
                  text={childComment.commentContent}
                  onUpdate={handleUpdateComment}
                  postNum={postNum}
                  commentId={childComment.commentId}
                  empId={childComment.empId}
                  isChild={true} // 자식 댓글임을 나타내는 prop을 추가합니다.
                />
              </CRow>

댓글 작성

import React, { useState, useEffect, Childre, useCallback } from "react";
import axios from "axios";
import { CRow, CCol, CButton, CFormTextarea, CForm } from "@coreui/react";
import { Input, Pagination } from "antd";
import FormattedDate from "./FormattedDate";
import Cookies from "js-cookie";
import "./css/BulletinBoardComment.css";

export const CommentInput = ({ postNum }) => {
  const [comment, setComment] = useState("");
  const staffInfo = JSON.parse(Cookies.get("staffInfo"));
  console.log(staffInfo);
  const handleCommentChange = (event) => {
    setComment(event.target.value);
  };

  // 부모댓글 입력 로직
  const handleSubmitComment = async () => {
    try {
      const data = {
        commentDate: new Date(),
        postNum: postNum,
        empId: staffInfo.empId, // staffInfo에서 empId 가져옴
        empNum: staffInfo.empNum, // staffInfo에서 empNum 가져옴
        email: staffInfo.email, // staffInfo에서 email 가져옴
        commentContent: comment,
      };

      const response = await axios.post(
        `api/bulletinboard/BulletinBoardPages/addParentComment/${postNum}`,
        data
      );

      setComment("");
      alert("정상 등록 되었습니다.");
      location.reload();
    } catch (error) {
      console.error("에러 발생:", error);
    }
  };

  return (
    <CRow className="BulletinBoardPages-MainComment">
      <CCol md={10} sm="auto">
        <input type="hidden" className="article-id" />
        <input type="hidden" className="parent-comment-id" />
        <CFormTextarea
          className="BulletinBoardPages-MainCommentBox"
          placeholder="댓글 작성시 상대방에 대한 매너를 지켜주세요. "
          rows="3"
          required
          value={comment}
          onChange={handleCommentChange}
        />
      </CCol>
      <CCol md={2} sm="auto">
        <CButton
          className="form-control mt-2"
          type="button"
          onClick={handleSubmitComment}
        >
          쓰기
        </CButton>
      </CCol>
    </CRow>
  );
};

// CommentContent 컴포넌트 내에서 수정 기능 추가
const CommentContent = ({
  author,
  time,
  text,
  postNum,
  commentId,
  empId,
  isChild = false, // isChild prop을 추가. 기본값은 false (부모 댓글)
}) => {
  const [editing, setEditing] = useState(false);
  const [editedText, setEditedText] = useState(text);
  const staffInfo = JSON.parse(Cookies.get("staffInfo"));
  console.log("댓글 작성자 empId:", empId);
  console.log("현재 로그인한 사용자 empId:", staffInfo.empId);

  const handleEditChange = (event) => {
    setEditedText(event.target.value);
  };

  const handleUpdate = async () => {
    if (empId !== staffInfo.empId) {
      alert("댓글의 작성자만 수정할 수 있습니다.");
      return;
    }

    try {
      const data = {
        commentContent: editedText,
        commentEditDate: new Date(),
      };

      // 부모 댓글과 자식 댓글에 대한 엔드포인트를 구분
      const endPoint = isChild
        ? `/api/bulletinboard/BulletinBoardPages/${postNum}/updateChildComment/${commentId}`
        : `/api/bulletinboard/BulletinBoardPages/${postNum}/updateParentComment/${commentId}`;

      await axios.put(endPoint, data);

      alert("댓글이 성공적으로 수정되었습니다.");
    } catch (error) {
      console.error("실패");

      if (
        error.response &&
        error.response.data &&
        error.response.data.message
      ) {
        alert("댓글 수정 중 오류: " + error.response.data.message);
        console.log("Server response:", error.response.data);
      } else {
        alert("댓글 수정 중 알 수 없는 오류가 발생했습니다.");
      }
    }

    setEditing(false);
  };

  return (
    <CCol md={10} xs="auto" className="textBoxBoard">
      <strong>{author} </strong>
      <small>
        <time>{time}</time>
      </small>
      {editing ? (
        <>
          <CFormTextarea value={editedText} onChange={handleEditChange} />
          <CButton
            className="BulletinBoardPages-DelBtn"
            color="success"
            variant="ghost"
            onClick={handleUpdate}
          >
            저장
          </CButton>
          <CButton
            className="BulletinBoardPages-DelBtn"
            color="secondary"
            variant="ghost"
            onClick={() => setEditing(false)}
          >
            취소
          </CButton>
        </>
      ) : (
        <>
          <p className="mb-1">{text}</p>
          {author === staffInfo.empId && (
            <CButton
              id="EditBtn"
              className="BulletinBoardPages-DelBtn"
              color="primary"
              variant="ghost"
              onClick={() => setEditing(true)}
            >
              수정
            </CButton>
          )}
          {!editing && author === staffInfo.empId && (
            <CButton
              className="BulletinBoardPages-DelBtn"
              color="danger"
              variant="ghost"
              onClick={() => {
                if (isChild) {
                  deleteChildComment(postNum, commentId);
                } else {
                  deleteParentComment(postNum, commentId);
                }
              }}
            >
              삭제
            </CButton>
          )}
        </>
      )}
    </CCol>
  );
};
// 부모 댓글 삭제
function deleteParentComment(postNum, commentId) {
  fetch(
    `api/bulletinboard/BulletinBoardPages/${postNum}/deleteParentComment/${commentId}`,
    {
      method: "DELETE",
    }
  )
    .then((response) => {
      if (response.ok) {
        alert("부모 댓글이 성공적으로 삭제되었습니다."); // 정상 삭제 알림
        location.reload();
        return;
      } else {
        throw new Error("Failed to delete the parent comment.");
      }
    })
    .then((data) => {
      if (data && !data.success) {
        alert("부모 댓글 삭제 중 오류가 발생했습니다: " + data.message);
        console.log("postNum", postNum, "commentId:", commentId);
      }
    })
    .catch((error) => {
      console.error("Error:", error);
      console.log("postNum", postNum, "commentId:", commentId);

      if (error.message !== "Failed to delete the parent comment.") {
        alert("댓글 삭제 중 오류가 발생했습니다.");
      }
    });
}

// ===================================

// 자식 댓글 삭제
function deleteChildComment(postNum, commentId) {
  console.log("Received commentId:", commentId); // 여기서 확인

  fetch(
    `api/bulletinboard/BulletinBoardPages/${postNum}/deleteChildComment/${commentId}`,
    {
      method: "DELETE",
    }
  )
    .then((response) => {
      if (response.ok) {
        alert("자식 댓글이 성공적으로 삭제되었습니다."); // 정상 삭제 알림
        location.reload();
        return;
      } else {
        throw new Error("Failed to delete the comment.");
      }
    })
    .then((data) => {
      if (data && !data.success) {
        alert("자식 댓글 삭제 중 오류가 발생했습니다: " + data.message);
      }
    })
    .catch((error) => {
      if (error.message !== "Failed to delete the comment.") {
        alert("댓글 삭제 중 오류가 발생했습니다.");
      }
    });
}
const handleUpdateComment = (updatedText) => {
  // 여기에서 API를 호출하여 댓글 내용을 업데이트
  console.log("Updated text:", updatedText);
  // 예를 들면, axios.put() 메서드를 사용하여 댓글을 업데이트
};

// 자식 댓글 등록 폼
const ReplyForm = ({ postNum, parentComment }) => {
  const [reply, setReply] = React.useState("");
  const staffInfo = JSON.parse(Cookies.get("staffInfo"));

  const handleReplyChange = (event) => {
    setReply(event.target.value);
  };

  const handleSubmit = async () => {
    try {
      const data = {
        commentDate: new Date(),
        postNum: postNum,
        parentCommentId: parentComment, // 수정된 부분
        empId: staffInfo.empId, // staffInfo에서 empId 가져옴
        empNum: staffInfo.empNum, // staffInfo에서 empNum 가져옴
        email: staffInfo.email, // staffInfo에서 email 가져옴
        commentContent: reply,
      };

      const response = await axios.post(
        `api/bulletinboard/BulletinBoardPages/addChildComment/${postNum}`,
        data
      );

      setReply("");
      alert("정상 등록 되었습니다.");
      location.reload();
    } catch (error) {
      console.error("에러 발생:", error);
      console.log(parentComment);
    }
  };

  // ... 나머지 코드 ...

  return (
    <li className="child-comment">
      <input type="hidden" className="article-id" />
      <div className="row">
        <CCol md={12}>
          <details>
            <summary className="comment-toggle">댓글 달기</summary>
            <CForm className="comment-form">
              <input type="hidden" className="article-id" />
              <input type="hidden" className="parent-comment-id" />
              <CRow>
                <CCol md={9}>
                  <CFormTextarea
                    className="comment-textbox"
                    placeholder="댓글 쓰기.."
                    rows="2"
                    required
                    value={reply}
                    onChange={handleReplyChange}
                  />
                </CCol>
                <CCol md={2}>
                  <CButton
                    className="form-control btn btn-primary mt-2"
                    color="info"
                    type="button"
                    onClick={handleSubmit}
                  >
                    쓰기
                  </CButton>
                </CCol>
              </CRow>
            </CForm>
          </details>
        </CCol>
      </div>
    </li>
  );
};

// 전체 댓글 폼
export const SingleComment = ({ comment, postNum }) => {
  const { commentContent, commentDate, empId, commentId, childComments } =
    comment;

  const handleUpdateComment = useCallback((updatedContent) => {
    console.log("Updated Content:", updatedContent);
    // 여기서 업데이트 로직을 구현합니다. (필요한 경우)
  }, []);

  return (
    <div className="comment">
      <CForm>
        <Input type="hidden" />
        <div className="comment-content">
          <CRow>
            {/* 부모 댓글 */}
            <CommentContent
              author={empId}
              time={<FormattedDate date={commentDate} />}
              text={commentContent}
              onUpdate={handleUpdateComment}
              postNum={postNum}
              commentId={commentId}
              empId={empId}
            />
            <ReplyForm postNum={postNum} parentComment={commentId} />
          </CRow>
        </div>
      </CForm>
      {/* 자식 댓글들 */}
      {childComments && childComments.length > 0 ? (
        <ul>
          {childComments.map((childComment, index) => (
            <li key={index} className="childText">
              <CRow>
                <CommentContent
                  author={childComment.empId}
                  time={<FormattedDate date={childComment.commentDate} />}
                  text={childComment.commentContent}
                  onUpdate={handleUpdateComment}
                  postNum={postNum}
                  commentId={childComment.commentId}
                  empId={childComment.empId}
                  isChild={true} // 자식 댓글임을 나타내는 prop을 추가합니다.
                />
              </CRow>
            </li>
          ))}
        </ul>
      ) : null}
    </div>
  );
};

CommentInput 컴포넌트

  • 부모 댓글을 입력하기 위한 폼이다.
  • Cookies.get("staffInfo")를 사용하여 현재 로그인된 사용자의 정보를 가져온다.
  • 댓글을 작성하고 “쓰기” 버튼을 클릭하면 handleSubmitComment 함수를 호출하여 API로 데이터를 전송하고 댓글을 등록한다.
  1. CommentContent 컴포넌트
    • 댓글의 내용을 표시하는 컴포넌트이다.
    • 댓글 내용, 작성자, 작성 시간을 표시한다.
    • 현재 로그인한 사용자가 댓글 작성자와 동일하면 댓글을 수정하거나 삭제할 수 있는 버튼이 표시된다.
    • 댓글 수정 시에는 텍스트 영역에 수정 내용을 입력하고 저장할 수 있다.
    • isChild prop은 댓글이 부모 댓글인지 자식 댓글인지를 나타낸다.
  2. deleteParentCommentdeleteChildComment 함수
    • 각각 부모 댓글과 자식 댓글을 삭제하는 기능이다.
    • fetch 함수를 사용하여 API에 DELETE 요청을 보내 댓글을 삭제한다.
  3. ReplyForm 컴포넌트
    • 부모 댓글에 대한 자식 댓글을 입력하는 폼이다.
    • 부모 댓글의 ID는 parentComment prop을 통해 전달된다.
    • “댓글 달기” 버튼을 클릭하면 텍스트 영역과 버튼이 표시된다.
    • 자식 댓글을 작성하고 “쓰기” 버튼을 클릭하면 handleSubmit 함수를 호출하여 API로 데이터를 전송하고 댓글을 등록한다.
  4. SingleComment 컴포넌트
    • 하나의 부모 댓글과 그에 속한 자식 댓글들을 표시하는 컴포넌트이다.
    • 부모 댓글 아래에는 자식 댓글 입력 폼(ReplyForm)이 있다.
    • 부모 댓글에 속한 자식 댓글들은 리스트 형태로 표시된다.

위의 설명은 제시된 코드의 주요 기능과 각 컴포넌트의 역할을 요약한 것이다. 코드는 댓글 관련 기능을 구현하기 위해 axios를 사용하여 API 요청을 보내는 로직, 상태 관리, UI 구성 등 다양한 요소들을 포함하고 있다.



게시글 페이지에서 렌더링 하는 화면

<CRow>
  <CCol md={12} lg={12} className="mb-4 BulletinBoardPages-Comment">
    <ul>
      {Array.isArray(comments) &&
        comments.map((comment) => (
          <SingleComment
            comment={comment}
            postNum={post.postNum}
            key={comment.parentCommentId}
          />
        ))}
    </ul>
  </CCol>
</CRow>



게시판 글 쓰기

import React, { useState } from "react";
import { CKEditor } from "@ckeditor/ckeditor5-react";
import ClassicEditor from "@ckeditor/ckeditor5-build-classic";
import {
  CButton,
  CModal,
  CModalHeader,
  CModalTitle,
  CModalBody,
  CCol,
  CFormInput,
  CRow,
  CFormSelect,
} from "@coreui/react";
import axios from "axios";
import BulletinBoardHashTag from "./BulletinBoardHashTag";
import Cookies from "js-cookie";

function BulletinBoardModal() {
  const [visibleLg, setVisibleLg] = useState(false);
  const [empNum, setEmpNum] = useState("");
  const [empId, setEmpId] = useState("");
  const [postContent, setPostContent] = useState("");
  const [HashtagName, setHashtagName] = useState("");
  const [postCountNum, setPostCountNum] = useState("");
  const [postTitle, setPostTitle] = useState("");
  const [postCategory, setPostCategory] = useState("");
  const [selectedHashTags, setSelectedHashTags] = useState([]);
  const [email, setEmail] = useState("");
  const [hashtagDate, setHashtagDate] = useState("");
  const staffInfo = JSON.parse(Cookies.get("staffInfo"));
  console.log(staffInfo);
  const handleBulletinBoardPostUpdate = async () => {
    try {
      const newPost = {
        empNum: staffInfo.empNum, // staffInfo에서 empNum 가져옴
        empId: staffInfo.empId, // staffInfo에서 empId 가져옴
        postTitle: postTitle,
        postContent: postContent,
        hashtagName: selectedHashTags.join(","), // Join selectedHashTags into a string
        postDate: new Date(),
        postCountNum: 0,
        postLike: 0,
        postRecommend: 0,
        postCategory: postCategory,
        email: staffInfo.email, // staffInfo에서 email 가져옴
        hashtagDate: new Date(),
      };
      console.log("등록하기 버튼 클릭됨");

      // Send the newPost data to the backend API
      const response = await axios.post("/api/bulletinboard/add", newPost);

      // Clear the form fields after successful submission
      setEmpNum("");
      setEmpId("");
      setEmail("");
      setPostTitle("");
      setPostContent("");
      setHashtagName("");
      setPostCountNum("");
      setPostCategory("");
      setHashtagDate("");

      alert("정상 등록 되었습니다.");

      // Close the modal
      setVisibleLg(false);

      console.log(newPost);
      window.location.reload();
      // Perform any other actions you need after successful submission
    } catch (error) {
      console.error("에러 발생:", error);
    }
  };

  const handleHashTagChange = (newHashTags) => {
    setSelectedHashTags(newHashTags);
    console.log("Selected Hash Tags:", newHashTags);
  };

  const categoryOptions = [
    { label: "전체", key: "전체 게시판", value: "CategoryAll" },
    { label: "공지사항", key: "공지사항", value: "CategoryNotice" },
    { label: "사내게시판", key: "사내게시판", value: "Categoryemployee" },
    // ... 다른 카테고리 옵션 ...
  ];

  const handleCategoryChange = (event) => {
    const selectedLabel = event.target.value;
    setPostCategory(selectedLabel);
  };

  return (
    <>
      <CCol className="BulletinBoardPostBtn">
        <CButton color="light" onClick={() => setVisibleLg(!visibleLg)}>
          게시글 작성하기
        </CButton>
      </CCol>
      <CModal size="lg" visible={visibleLg} onClose={() => setVisibleLg(false)}>
        <CModalHeader>
          <CModalTitle>글쓰기</CModalTitle>
        </CModalHeader>

        <CModalBody>
          <CRow className="BulletinBoardModalInputSize">
            <CCol md={3}>
              <CFormSelect
                aria-label="카테고리"
                options={categoryOptions.map((option) => ({
                  label: option.label,
                  value: option.label,
                }))}
                onChange={handleCategoryChange}
              />
            </CCol>
            <CCol md={9}>
              <CFormInput
                className="title-input"
                type="text"
                placeholder="제목"
                name="postTitle"
                value={postTitle}
                onChange={(e) => setPostTitle(e.target.value)}
              />
            </CCol>
          </CRow>

          <CKEditor
            editor={ClassicEditor}
            onReady={(editor) => {
              console.log("에디터가 준비되었습니다!", editor);
            }}
            onChange={(event, editor) => {
              const data = editor.getData();
              console.log("에디터의 내용을 가져옵니다", {
                event,
                editor,
                data,
              });
              setPostContent(data);
            }}
          />

          <BulletinBoardHashTag
            onHashTagChange={handleHashTagChange}
            value={HashtagName}
          />

          <div className="d-grid gap-2 d-md-flex justify-content-md-end BulletinBoardModalBtn">
            <button
              type="button"
              onClick={() => setVisibleLg(false)}
              className="btn btn-outline-warning me-md-2"
            >
              취소하기
            </button>
            <button
              type="button"
              onClick={handleBulletinBoardPostUpdate}
              className="btn btn-outline-primary me-md-2"
            >
              등록하기
            </button>
          </div>
        </CModalBody>
      </CModal>
    </>
  );
}

export default BulletinBoardModal;



해시태그

image

const baseTagifySettings = {
  blacklist: [],
  maxTags: 6,
  backspace: "edit",
  placeholder: "해시태그를 입력해보세요! 최대 6개 까지 가능합니다.",
  editTags: 1,
  dropdown: {
    maxItems: 20, // 드롭다운 메뉴에서 몇개 정도 항목을 보여줄지
    classname: "tags-look", // 드롭다운 메뉴 엘리먼트 클래스 이름. 이걸로 css 선택자로 쓰면 된다.
    enabled: 1, // 단어 몇글자 입력했을떄 추천 드롭다운 메뉴가 나타날지
    closeOnSelect: true, // 드롭다운 메뉴에서 태그 선택하면 자동으로 꺼지는지 안꺼지는지
  },
  callbacks: {},
};

function TagField({
  label,
  name,
  initialValue = [],
  suggestions = [],
  onChange,
}) {
  const handleChange = (e) => {
    const newTags = e.detail.tagify.value.map((item) => item.value);
    onChange(newTags);
  };

  const settings = {
    ...baseTagifySettings,
    whitelist: suggestions,
    callbacks: {
      add: handleChange,
      remove: handleChange,
      blur: handleChange,
      edit: handleChange,
      invalid: handleChange,
      click: handleChange,
      focus: handleChange,
      "edit:updated": handleChange,
      "edit:start": handleChange,
    },
  };

  return (
    <div className="form-group">
      <label htmlFor={"field-" + name}>{label}</label>
      <Tagify
        settings={settings}
        value={initialValue.join(",")}
        onAdd={handleChange}
        onRemove={handleChange}
        onBlur={handleChange}
        // ... other events ...
      />
    </div>
  );
}

function BulletinBoardHashTag({ onHashTagChange, value }) {
  const suggestions = [
    "급여",
    "연봉",
    "연차",
    "공지사항",
    "영업팀",
    "인사팀",
    // ... other suggestions ...
  ];

  const initialValue = value ? value.split(",") : [];

  return (
    <div className="BulletinBoardHashTag">
      <TagField
        initialValue={initialValue}
        suggestions={suggestions}
        onChange={onHashTagChange} // Pass the onChange function from parent
      />
    </div>
  );
}
  1. Imports:

    import React, { useRef, useState } from "react";
    import "@yaireo/tagify/dist/tagify.css";
    import Tagify from "@yaireo/tagify/dist/react.tagify";
    import {} from "@coreui/react";
    import "./css/BulletinBoardHashTag.css";
    
    • React 및 필요한 React hook을 가져온다.
    • Tagify 라이브러리 및 관련 스타일을 가져온다.
    • CoreUI의 React 컴포넌트들을 가져온다.
    • 해당 컴포넌트에 대한 CSS를 가져온다.
  2. Base Tagify Settings:

    javascriptCopy code
    const baseTagifySettings = { ... };
    
    
    • Tagify의 기본 설정입니다.
  3. TagField Component:

    function TagField({
      label,
      name,
      initialValue = [],
      suggestions = [],
      onChange,
    }) { ... }
    
    
    • 해시태그 입력 필드의 핵심 컴포넌트입니다.
    • label, name, initialValue, suggestions, onChange와 같은 props를 받습니다.
    • 여기에서 handleChange 함수를 사용하여 태그의 변경을 처리합니다.
  4. BulletinBoardHashTag Component:

    function BulletinBoardHashTag({ onHashTagChange, value }) { ... }
    
    
    • TagField 컴포넌트를 사용하는 래퍼 컴포넌트입니다.
    • 초기 해시태그 값과 해시태그가 변경될 때 호출되는 함수를 props로 받습니다.
  5. Export Statement:

    export default BulletinBoardHashTag;
    
    • 다른 React 파일에서 BulletinBoardHashTag 컴포넌트를 사용할 수 있게끔 export합니다.

세부적으로 나누어 설명하면 위와 같습니다. 이 코드는 사용자에게 해시태그 입력 인터페이스를 제공하며, 특정 해시태그 제안을 보여주고, 사용자의 해시태그 입력 변화를 상위 컴포넌트에 알려주는 역할을 하게 된다.



Back-End

package com.project.backend.controller;

import com.project.backend.dto.BulletinBoard.BulletinBoardDto;
import com.project.backend.dto.BulletinBoard.ChildCommentDto;
import com.project.backend.dto.BulletinBoard.ParentCommentDto;
import com.project.backend.service.BulletinBoard.BulletinBoardServicetotal;
import com.project.backend.service.BulletinBoard.CommentService;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/api/bulletinboard")
public class BulletinBoardController {

  private final BulletinBoardServicetotal bulletinBoardServicetotal;
  private final CommentService commentService;

  public BulletinBoardController(BulletinBoardServicetotal bulletinBoardServicetotal, CommentService commentService) {
    this.bulletinBoardServicetotal = bulletinBoardServicetotal;
    this.commentService = commentService;
  }

  @GetMapping
  public ResponseEntity<List<BulletinBoardDto>> getBulletinBoardData() {
    List<BulletinBoardDto> bulletinBoardData = bulletinBoardServicetotal.getBulletinBoardPost();
    return new ResponseEntity<>(bulletinBoardData, HttpStatus.OK);
  }

  // 게시물 등록
  @PostMapping("/add")
  public ResponseEntity<String> addBulletinBoardPost(@RequestBody BulletinBoardDto bulletinBoardDto) {
    try {
      bulletinBoardServicetotal.createBulletinBoardData(bulletinBoardDto);
      return ResponseEntity.ok("게시물이 등록되었습니다.");
    } catch (Exception e) {
      return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("게시물 등록 중 오류가 발생했습니다.");
    }
  }

  //게시물 페이지 이동 렌더링
  @GetMapping("/BulletinBoardPages/{postNum}")
  public ResponseEntity<BulletinBoardDto> getBulletinBoardPost(@PathVariable Long postNum) {
    BulletinBoardDto bulletinBoardPost = bulletinBoardServicetotal.getBulletinBoardPostByPostNum(postNum);
    return new ResponseEntity<>(bulletinBoardPost, HttpStatus.OK);
  }

  // 조회수 카운트
  @PutMapping("/update/{postNum}")
  public ResponseEntity<String> updateBulletinBoardPost(@PathVariable Long postNum, @RequestBody BulletinBoardDto bulletinBoardDto) {
    try {
      bulletinBoardDto.setPostNum(postNum);
      bulletinBoardServicetotal.updateBulletinBoardPost(bulletinBoardDto);
      return ResponseEntity.ok("게시물이 업데이트되었습니다.");
    } catch (Exception e) {
      return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("게시물 업데이트 중 오류가 발생했습니다.");
    }
  }

  //게시물 삭제
  @DeleteMapping("/BulletinBoardPages/{postNum}")
  public ResponseEntity<String> deleteBulletinBoard(@PathVariable Long postNum) {
    try {
      bulletinBoardServicetotal.deleteBulletinBoard(postNum);
      return ResponseEntity.ok("게시물이 성공적으로 삭제되었습니다.");
    } catch (Exception e) {
      return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("게시물을 삭제하는 도중 오류가 발생했습니다.");
    }
  }

  //게시물 수정
  @PutMapping("/BulletinBoardPagesEdit/update/{postNum}")
  public ResponseEntity<String> updatePost(@PathVariable Long postNum, @RequestBody BulletinBoardDto bulletinBoardDto) {
    try {
      bulletinBoardServicetotal.updateBulletinBoardPost(postNum, bulletinBoardDto);
      return ResponseEntity.ok("게시물이 업데이트되었습니다.");
    } catch (Exception e) {
      return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("게시물 업데이트 중 오류가 발생했습니다.");
    }
  }

  //부모 댓글 저장 로직
  @PostMapping("/BulletinBoardPages/addParentComment/{postNum}")
  public ResponseEntity<String> addParentComment(@PathVariable Long postNum, @RequestBody ParentCommentDto parentCommentDto) {
    try {
      commentService.addParentComment(postNum, parentCommentDto);
      return ResponseEntity.ok("부모 댓글이 등록되었습니다.");
    } catch (Exception e) {
      return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("부모 댓글 등록 중 오류가 발생했습니다.");
    }
  }

  //자식 댓글 저장 로직
  @PostMapping("/BulletinBoardPages/addChildComment/{postNum}")
  public ResponseEntity<String> addChildComment(
    @PathVariable Long postNum,
    @RequestBody ChildCommentDto childCommentDto
  ) {
    try {
      // 클라이언트에서 받아온 parentCommentId를 사용하여 자식 댓글에 연결
      Long parentCommentId = childCommentDto.getParentCommentId();
      if (parentCommentId == null) {
        return ResponseEntity.badRequest().body("부모 댓글 ID가 제공되지 않았습니다.");
      }

      commentService.addChildComment(postNum, parentCommentId, childCommentDto);
      return ResponseEntity.ok("자식 댓글이 등록되었습니다.");
    } catch (Exception e) {
      return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("자식 댓글 등록 중 오류가 발생했습니다.");
    }
  }

  //부모 댓글과 자식 댓글 보여주기
  @GetMapping("/BulletinBoardPages/{postNum}/comments")
  public ResponseEntity<List<ParentCommentDto>> getCommentsByBulletinBoardId(@PathVariable Long postNum) {
    List<ParentCommentDto> comments = commentService.getCommentsByBulletinBoardId(postNum);
    return ResponseEntity.ok(comments);
  }

  // 부모 댓글 삭제
  @DeleteMapping("/BulletinBoardPages/{postNum}/deleteParentComment/{commentId}")
  public ResponseEntity<String> deleteParentComment(@PathVariable Long postNum, @PathVariable Long commentId) {
    try {
      commentService.deleteParentComment(postNum, commentId);
      return ResponseEntity.ok("부모 댓글 및 관련된 자식 댓글들이 삭제되었습니다.");
    } catch (Exception e) {
      return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("부모 댓글 삭제 중 오류가 발생했습니다.");
    }
  }

  // 자식 댓글 삭제
  @DeleteMapping("/BulletinBoardPages/{postNum}/deleteChildComment/{commentId}")
  public ResponseEntity<String> deleteChildComment(@PathVariable Long postNum, @PathVariable Long commentId) {
    try {
      commentService.deleteChildComment(postNum, commentId);
      return ResponseEntity.ok("자식 댓글이 삭제되었습니다.");
    } catch (Exception e) {
      return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("자식 댓글 삭제 중 오류가 발생했습니다.");
    }
  }

  @PutMapping("/BulletinBoardPages/{postNum}/updateParentComment/{commentId}")
  public ResponseEntity<?> updateParentComment(@PathVariable Long postNum, @PathVariable Long commentId, @RequestBody ParentCommentDto request) {
    try {
      commentService.editParentComment(postNum, commentId, request);
      return ResponseEntity.ok().body("댓글이 성공적으로 수정되었습니다.");
    } catch (Exception e) {
      return ResponseEntity.badRequest().body("댓글 수정 중 오류가 발생했습니다.");
    }
  }

  @PutMapping("/BulletinBoardPages/{postNum}/updateChildComment/{commentId}")
  public ResponseEntity<?> updateChildComment(@PathVariable Long postNum, @PathVariable Long commentId, @RequestBody ChildCommentDto request) {
    try {
      commentService.editChildComment(postNum, commentId, request);
      return ResponseEntity.ok().body("자식 댓글이 성공적으로 수정되었습니다.");
    } catch (Exception e) {
      return ResponseEntity.badRequest().body("자식 댓글 수정 중 오류가 발생했습니다.");
    }
  }
}

Dto

package com.project.backend.dto.BulletinBoard;

import lombok.Getter;
import lombok.Setter;

import java.util.Date;

@Getter
@Setter
public class BulletinBoardDto {

  private Long postNum;
  private String empId;
  private String empNum;
  private String postTitle;
  private String postContent;
  private Date postDate;
  private int postCountNum;
  private int postLike;
  private int postRecommend;
  private String postCategory;
  private Date postDateEdit;
  private String hashtagName;
  private Date hashtagDate;
  private Date hashtagDateEdit;
  private String email;

  @Getter
  @Setter
  public class LikeToggleRequestDto {
    private Long postNum;  // 게시물 번호
    private String empId;  // 좋아요를 클릭한 직원의 ID
  }

  @Getter
  @Setter
  public class RecommendToggleRequestDto {
    private Long postNum;  // 게시물 번호
    private String empId;  // 추천을 클릭한 직원의 ID
  }

  @Getter
  @Setter
  public class LikeToggleResponseDto {
    private boolean success;
    private boolean likeStatus;  // 현재 좋아요 상태 (예: 좋아요가 활성화되었는지 여부)
    private int likeCount;  // 해당 게시물의 전체 좋아요 수
  }

  @Getter
  @Setter
  public class RecommendToggleResponseDto {
    private boolean success;
    private boolean recommendStatus;  // 현재 추천 상태 (예: 추천이 활성화되었는지 여부)
    private int recommendCount;  // 해당 게시물의 전체 추천 수
  }
}

지식 댓글

package com.project.backend.dto.BulletinBoard;

import java.util.Date;

public class ChildCommentDto {

  private String empId;
  private String empNum;
  private String email;
  private String commentContent;
  private Date commentDate;
  private Date commentEdit;
  private Date commentEditDate;

  private  Long commentId;

  private Long parentCommentId;

  public Long getParentCommentId() {
    return parentCommentId;
  }

  public void setParentCommentId(Long parentCommentId) {
    this.parentCommentId = parentCommentId;
  }
  // getters and setters

  public Long getCommentId() {
    return commentId;
  }

  public void setCommentId(Long commentId) {
    this.commentId = commentId;
  }

  public String getEmpId() {
    return empId;
  }

  public void setEmpId(String empId) {
    this.empId = empId;
  }

  public String getEmpNum() {
    return empNum;
  }

  public void setEmpNum(String empNum) {
    this.empNum = empNum;
  }

  public String getEmail() {
    return email;
  }

  public void setEmail(String email) {
    this.email = email;
  }

  public String getCommentContent() {
    return commentContent;
  }

  public void setCommentContent(String commentContent) {
    this.commentContent = commentContent;
  }

  public Date getCommentDate() {
    return commentDate;
  }

  public void setCommentDate(Date commentDate) {
    this.commentDate = commentDate;
  }

  public Date getCommentEdit() {
    return commentEdit;
  }

  public void setCommentEdit(Date commentEdit) {
    this.commentEdit = commentEdit;
  }

  public Date getCommentEditDate() {
    return commentEditDate;
  }

  public void setCommentEditDate(Date commentEditDate) {
    this.commentEditDate = commentEditDate;
  }

}

부모 댓글

package com.project.backend.dto.BulletinBoard;

import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;

public class ParentCommentDto {

  private Long commentId;
  private Long postNum;
  private String empId;
  private String empNum;
  private String email;
  private String commentContent;
  private Date commentDate;
  private Date commentEditDate; // 수정 댓글 저장 날짜a

//  자식 댓글 리스트
  private List<ChildCommentDto> childComments;

  public Long getCommentId() {
    return commentId;

  }

  public List<ChildCommentDto> getChildComments() {
    return childComments;
  }
  public void setChildComments(List<ChildCommentDto> childComments) {
    this.childComments = childComments;
  }

  public void setCommentId(Long commentId) {
    this.commentId = commentId;
  }

  public Long getPostNum() {
    return postNum;
  }

  public void setPostNum(Long postNum) {
    this.postNum = postNum;
  }

  public String getEmpId() {
    return empId;
  }

  public void setEmpId(String empId) {
    this.empId = empId;
  }

  public String getEmpNum() {
    return empNum;
  }

  public void setEmpNum(String empNum) {
    this.empNum = empNum;
  }

  public String getEmail() {
    return email;
  }

  public void setEmail(String email) {
    this.email = email;
  }

  public String getCommentContent() {
    return commentContent;
  }

  public void setCommentContent(String commentContent) {
    this.commentContent = commentContent;
  }

  public Date getCommentDate() {
    return commentDate;
  }

  public void setCommentDate(Date commentDate) {
    this.commentDate = commentDate;
  }

  public Date getCommentEditDate() {
    return commentEditDate;
  }

  public void setCommentEditDate(Date commentEditDate) {
    this.commentEditDate = commentEditDate;
  }

}

게시판 엔티티

package com.project.backend.entity.bulletinboard;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import java.util.*;

@Entity
@Table(name = "bulletinboard")
@Getter
@Setter
@ToString
public class BulletinBoard {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY )
  @Column(name = "post_Num")
  private Long postNum;

  @Column(name = "emp_Id")  //로그인 정보와 조인
  private String empId;

  @Column(name = "emp_Num") // 조그인 정보에서 받아와야함
  private String empNum;

  @Column(name = "post_Title", nullable = false)
  private String postTitle;

  @Column(name = "post_Content", nullable = false, length = 50000)
  private String postContent;

  @Column(name = "post_Date", nullable = false)
  private Date postDate;

  @Column(name = "post_Count_Num")
  private int postCountNum;

  @Column(name = "post_Like")
  private int postLike;

  @Column(name = "post_Recommend")
  private int postRecommend;

  @Column(name = "post_Category")
  private String postCategory;

  @Column(name = "post_Date_Edit")
  private Date postDateEdit;

  @Column(name = "hashtag_Name")
  private String hashtagName;

  //  nullable = false
  @Column(name = "hashtag_date")
  private Date hashtagDate;

  @Column(name = "hashtag_date_edit")
  private Date hashtagDateEdit;

  @Column(name = "email")
  private String email;

  @OneToMany(mappedBy = "bulletinBoard", cascade = CascadeType.ALL)
  private List<ParentComment> comments;

  // Constructors, other methods, if any
}

자식 엔티티

package com.project.backend.entity.bulletinboard;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

import java.util.Date;

@Entity
@Table(name = "child_comment")
@Getter
@Setter
public class ChildComment {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "comment_Id", nullable = false)
  private Long commentId;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "parent_comment_Id", nullable = false)
  private ParentComment parentComment;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "bulletin_board_Id")
  private BulletinBoard bulletinBoard;

  @Column(name = "emp_Id")
  private String empId;

  @Column(name = "emp_Num")
  private String empNum;

  @Column(name = "email")
  private String email;

  @Column(name = "comment_Content")
  private String commentContent;

  @Column(name = "comment_Date")
  private Date commentDate;

  @Column(name = "comment_Edit")
  private Date commentEdit;

  @Column(name = "comment_Edit_Date")
  private Date commentEditDate;

  // Constructors, other methods, if any
}

부모 엔티티

package com.project.backend.entity.bulletinboard;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Entity
@Table(name = "parent_comment")
@Getter
@Setter
public class ParentComment {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "comment_Id", nullable = false)
  private Long commentId;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "post_Num", nullable = false)
  private BulletinBoard bulletinBoard;

  @OneToMany(mappedBy = "parentComment", cascade = CascadeType.ALL)
  private List<ChildComment> childComments = new ArrayList<>();

  @Column(name = "emp_Id")
  private String empId;

  @Column(name = "emp_Num")
  private String empNum;

  @Column(name = "email")
  private String email;

  @Column(name = "comment_Content")
  private String commentContent;

  @Column(name = "comment_Date")
  private Date commentDate;

  @Column(name = "comment_Edit_Date")
  private Date commentEditDate;  // 수정 일자

  // Constructors, other methods, if any
}

댓글 서비스

package com.project.backend.service.BulletinBoard;

import com.project.backend.dto.BulletinBoard.ChildCommentDto;
import com.project.backend.dto.BulletinBoard.ParentCommentDto;
import com.project.backend.entity.bulletinboard.BulletinBoard;
import com.project.backend.entity.bulletinboard.ChildComment;
import com.project.backend.entity.bulletinboard.ParentComment;
import com.project.backend.repository.bulletinboard.BulletinBoardRepository;
import com.project.backend.repository.bulletinboard.ChildCommentRepository;
import com.project.backend.repository.bulletinboard.ParentCommentRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import java.time.LocalDateTime;

@Service
public class CommentService {

  @Autowired
  private BulletinBoardRepository bulletinBoardRepository;

  @Autowired
  private ParentCommentRepository parentCommentRepository;

  @Autowired
  private ChildCommentRepository childCommentRepository;

  public void addParentComment(Long postNum, ParentCommentDto parentCommentDto) {
    BulletinBoard bulletinBoard = bulletinBoardRepository.findById(postNum)
      .orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. id=" + postNum));
    ParentComment parentComment = new ParentComment();
    parentComment.setBulletinBoard(bulletinBoard);
    parentComment.setCommentContent(parentCommentDto.getCommentContent());
    parentComment.setEmpId(parentCommentDto.getEmpId());
    parentComment.setEmpNum(parentCommentDto.getEmpNum());
    parentComment.setEmail(parentCommentDto.getEmail());
    parentComment.setCommentDate(parentCommentDto.getCommentDate());
    parentComment.setCommentEditDate(parentCommentDto.getCommentEditDate());
    parentCommentRepository.save(parentComment);
  }

  public void addChildComment(Long postNum, Long parentCommentId, ChildCommentDto childCommentDto) {
    BulletinBoard bulletinBoard = bulletinBoardRepository.findById(postNum)
      .orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. id=" + postNum));
    ParentComment parentComment = parentCommentRepository.findById(parentCommentId)
      .orElseThrow(() -> new IllegalArgumentException("해당 부모 댓글이 없습니다. id=" + parentCommentId));
    ChildComment childComment = new ChildComment();
    childComment.setBulletinBoard(bulletinBoard);
    childComment.setParentComment(parentComment);
    // Set the rest of the fields from childCommentDto
    childComment.setEmpId(childCommentDto.getEmpId());
    childComment.setEmpNum(childCommentDto.getEmpNum());
    childComment.setEmail(childCommentDto.getEmail());
    childComment.setCommentContent(childCommentDto.getCommentContent());
    childComment.setCommentDate(childCommentDto.getCommentDate());
    childComment.setCommentEdit(childCommentDto.getCommentEdit());
    childComment.setCommentEditDate(childCommentDto.getCommentEditDate());

    childCommentRepository.save(childComment);
  }

  public List<ParentCommentDto> getCommentsByBulletinBoardId(Long postNum) {
    BulletinBoard bulletinBoard = bulletinBoardRepository.findById(postNum)
      .orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. id=" + postNum));
    List<ParentComment> parentComments = parentCommentRepository.findByBulletinBoard(bulletinBoard);

    return parentComments.stream()
      .map(parentComment -> {
        ParentCommentDto parentCommentDto = new ParentCommentDto();
        parentCommentDto.setCommentContent(parentComment.getCommentContent());
        parentCommentDto.setCommentId(parentComment.getCommentId());
        parentCommentDto.setEmpId(parentComment.getEmpId());
        parentCommentDto.setEmpNum(parentComment.getEmpNum());
        parentCommentDto.setEmail(parentComment.getEmail());
        parentCommentDto.setCommentDate(parentComment.getCommentDate());
        parentCommentDto.setCommentEditDate(parentComment.getCommentEditDate());

        // 자식 댓글 리스트를 가져와서 ParentCommentDto에 추가
        List<ChildComment> childCommentsForThisParent = childCommentRepository.findByParentComment(parentComment);
        List<ChildCommentDto> childCommentDtos = childCommentsForThisParent.stream().map(childComment -> {
          ChildCommentDto childCommentDto = new ChildCommentDto();
          childCommentDto.setCommentContent(childComment.getCommentContent());
          childCommentDto.setCommentId(childComment.getCommentId());
          childCommentDto.setEmpId(childComment.getEmpId());
          childCommentDto.setEmpNum(childComment.getEmpNum());
          childCommentDto.setEmail(childComment.getEmail());
          childCommentDto.setCommentDate(childComment.getCommentDate());
          childCommentDto.setCommentEdit(childComment.getCommentEdit());
          childCommentDto.setCommentEditDate(childComment.getCommentEditDate());
          return childCommentDto;
        }).collect(Collectors.toList());
        parentCommentDto.setChildComments(childCommentDtos);

        return parentCommentDto;
      })
      .collect(Collectors.toList());
  }

  // 부모 댓글 삭제
  public void deleteParentComment(Long postNum, Long commentId) {
    // 게시글의 존재 확인
    BulletinBoard bulletinBoard = bulletinBoardRepository.findById(postNum)
      .orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. id=" + postNum));

    // 댓글의 존재 확인
    ParentComment parentComment = parentCommentRepository.findById(commentId)
      .orElseThrow(() -> new IllegalArgumentException("해당 부모 댓글이 없습니다. id=" + commentId));

    // 댓글 삭제
    // CascadeType.ALL로 설정했기 때문에 부모 댓글 삭제 시 연관된 자식 댓글들도 모두 삭제됩니다.
    parentCommentRepository.delete(parentComment);
  }

  // 자식 댓글 삭제
  public void deleteChildComment(Long postNum, Long childCommentId) {
    // 게시글의 존재 확인
    BulletinBoard bulletinBoard = bulletinBoardRepository.findById(postNum)
      .orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. id=" + postNum));

    // 자식 댓글의 존재 확인
    ChildComment childComment = childCommentRepository.findById(childCommentId)
      .orElseThrow(() -> new IllegalArgumentException("해당 자식 댓글이 없습니다. id=" + childCommentId));

    // 자식 댓글 삭제
    childCommentRepository.delete(childComment);
  }

  // 부모 댓글 수정
  public void editParentComment(Long postNum, Long commentId, ParentCommentDto parentCommentDto) {
    // 게시글과 댓글을 찾습니다.
    ParentComment parentComment = parentCommentRepository.findById(commentId)
      .orElseThrow(() -> new IllegalArgumentException("해당 부모 댓글이 없습니다. id=" + commentId));

    // 댓글 내용과 수정일자만 업데이트 합니다.
    parentComment.setCommentContent(parentCommentDto.getCommentContent());
    parentComment.setCommentEditDate(new Date()); // 댓글 수정 시간을 현재 시간으로 업데이트

    parentCommentRepository.save(parentComment);
  }

  // 자식 댓글 수정
  public void editChildComment(Long postNum, Long childCommentId, ChildCommentDto childCommentDto) {
    ChildComment childComment = childCommentRepository.findById(childCommentId)
      .orElseThrow(() -> new IllegalArgumentException("해당 자식 댓글이 없습니다. id=" + childCommentId));

    // 댓글 내용과 수정일자만 업데이트 합니다.
    childComment.setCommentContent(childCommentDto.getCommentContent());
    childComment.setCommentEditDate(new Date()); // 댓글 수정 시간을 현재 시간으로 업데이트

    childCommentRepository.save(childComment);
  }

}

게시판 서비스 impl

package com.project.backend.service.BulletinBoard;

import com.project.backend.dto.BulletinBoard.BulletinBoardDto;
import com.project.backend.entity.bulletinboard.BulletinBoard;
import com.project.backend.repository.bulletinboard.BulletinBoardRepository;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;

@Transactional
@Service
public class BulletinBoardServiceImpl implements BulletinBoardService {

  private final BulletinBoardRepository bulletinBoardRepository;

  @Autowired
  public BulletinBoardServiceImpl(BulletinBoardRepository bulletinBoardRepository) {
    this.bulletinBoardRepository = bulletinBoardRepository;
  }

  @Override
  public List<BulletinBoardDto> getBulletinBoardPost() {
    List<BulletinBoard> bulletinBoards = bulletinBoardRepository.findAll();
    List<BulletinBoardDto> bulletinBoardDtos = new ArrayList<>();

    for (BulletinBoard bulletinBoard : bulletinBoards) {
      BulletinBoardDto bulletinBoardDto = new BulletinBoardDto();
      BeanUtils.copyProperties(bulletinBoard, bulletinBoardDto);
      bulletinBoardDtos.add(bulletinBoardDto);
    }

    return bulletinBoardDtos;
  }

  @Override
  public void createBulletinBoardData(BulletinBoardDto bulletinBoardDto) {
    BulletinBoard bulletinBoard = new BulletinBoard();
    BeanUtils.copyProperties(bulletinBoardDto, bulletinBoard);
    bulletinBoardRepository.save(bulletinBoard);
  }

  @Override
  public BulletinBoardDto getBulletinBoardPostByPostNum(Long postNum) {
    if(postNum == null) {
      throw new IllegalArgumentException("postNum은 null일 수 없습니다.");
    }
    BulletinBoard bulletinBoard = bulletinBoardRepository.findByPostNum(postNum)
      .orElseThrow(() -> new IllegalArgumentException("해당 postNum의 게시물이 존재하지 않습니다."));
    BulletinBoardDto bulletinBoardDto = new BulletinBoardDto();
    BeanUtils.copyProperties(bulletinBoard, bulletinBoardDto);
    return bulletinBoardDto;
  }
}

게시판 서비스

package com.project.backend.service.BulletinBoard;

import com.project.backend.dto.BulletinBoard.BulletinBoardDto;
import com.project.backend.entity.bulletinboard.BulletinBoard;
import com.project.backend.exception.bulletinboard.ResourceNotFoundException;
import com.project.backend.repository.bulletinboard.BulletinBoardRepository;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class BulletinBoardServicetotal {

  private  final BulletinBoardRepository bulletinBoardRepository;

  @Autowired
  public BulletinBoardServicetotal(BulletinBoardRepository bulletinBoardRepository) {
    this.bulletinBoardRepository = bulletinBoardRepository;

  }

  public List<BulletinBoardDto> getBulletinBoardPost() {
    List<BulletinBoard> bulletinBoards = bulletinBoardRepository.findAll();
    List<BulletinBoardDto> bulletinBoardDtos = new ArrayList<>();

    for (BulletinBoard bulletinBoard : bulletinBoards) {
      BulletinBoardDto bulletinBoardDto = new BulletinBoardDto();
      BeanUtils.copyProperties(bulletinBoard, bulletinBoardDto);
      bulletinBoardDtos.add(bulletinBoardDto);
    }

    return bulletinBoardDtos;
  }

  public void createBulletinBoardData(BulletinBoardDto bulletinBoardDto) {
    BulletinBoard bulletinBoard = new BulletinBoard();
    BeanUtils.copyProperties(bulletinBoardDto, bulletinBoard);
    bulletinBoardRepository.save(bulletinBoard);
  }

  public BulletinBoardDto getBulletinBoardPostByPostNum(Long postNum) {
    BulletinBoard bulletinBoard = bulletinBoardRepository.findByPostNum(postNum)
      .orElseThrow(() -> new ResourceNotFoundException("Post not found with postNum " + postNum));
    BulletinBoardDto bulletinBoardDto = new BulletinBoardDto();
    BeanUtils.copyProperties(bulletinBoard, bulletinBoardDto);
    return bulletinBoardDto;
  }

  public void updateBulletinBoardPost(BulletinBoardDto bulletinBoardPost) {
    BulletinBoard bulletinBoard = bulletinBoardRepository.findByPostNum(bulletinBoardPost.getPostNum())
      .orElseThrow(() -> new ResourceNotFoundException("Post not found with postNum " + bulletinBoardPost.getPostNum()));
    bulletinBoard.setPostCountNum(bulletinBoardPost.getPostCountNum());
    bulletinBoardRepository.save(bulletinBoard);
  }

//게시물 삭제
  public void deleteBulletinBoard(Long postNum) {
    bulletinBoardRepository.deleteById(postNum);
  }

  //게시물 수정

  public void updateBulletinBoardPost(Long postNum, BulletinBoardDto bulletinBoardDto) {
    BulletinBoard post = bulletinBoardRepository.findById(postNum)
      .orElseThrow(() -> new IllegalArgumentException("해당 게시물이 없습니다. id=" + postNum));

    post.setPostTitle(bulletinBoardDto.getPostTitle());
    post.setPostContent(bulletinBoardDto.getPostContent());
    post.setHashtagName(bulletinBoardDto.getHashtagName());
    post.setPostCategory(bulletinBoardDto.getPostCategory());
    post.setPostDateEdit(bulletinBoardDto.getPostDateEdit());

    bulletinBoardRepository.save(post);
  }
//===========================

}
@Entity
@Table(name = "bulletinboard")
@Getter
@Setter
@ToString
public class BulletinBoard {

  // ... (다른 필드들)

  // 게시판과 댓글 사이의 일대다 관계를 나타내는 필드입니다.
  // 게시판 한 개에 여러 개의 부모 댓글이 연결될 수 있기 때문에 List<ParentComment>로 정의하였습니다.
  // mappedBy 속성은 ParentComment 엔티티의 "bulletinBoard" 필드와 연결을 나타냅니다.
  // cascade = CascadeType.ALL은 게시판이 삭제되면 해당 게시판에 연결된 댓글도 모두 삭제됨을 의미합니다.
  @OneToMany(mappedBy = "bulletinBoard", cascade = CascadeType.ALL)
  private List<ParentComment> comments;

  // ... (나머지 생성자, 메서드 등)
}

ParentComment 엔티티

@Entity
@Table(name = "parent_comment")
@Getter
@Setter
public class ParentComment {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "comment_Id", nullable = false)
    private Long commentId;

    // ...

    // ParentComment와 ChildComment 사이의 일대다 관계를 표현합니다.
    // ParentComment 하나에 여러 ChildComment가 연결될 수 있기 때문입니다.
    // mappedBy 속성은 ChildComment 엔티티의 "parentComment" 필드와 연결을 나타냅니다.
    // cascade = CascadeType.ALL는 부모 댓글이 삭제되면 해당 부모 댓글에 연결된 자식 댓글들도 모두 삭제됨을 의미합니다.
    @OneToMany(mappedBy = "parentComment", cascade = CascadeType.ALL)
    private List<ChildComment> childComments = new ArrayList<>();

    // ...
}

ParentComment와 ChildComment 사이의 일대다 관계를 표현한다. ParentComment 하나에 여러 ChildComment가 연결될 수 있기 때문이다. mappedBy 속성은 ChildComment 엔티티의 “parentComment” 필드와 연결을 나타낸다. cascade = CascadeType.ALL는 부모 댓글이 삭제되면 해당 부모 댓글에 연결된 자식 댓글들도 모두 삭제됨을 의미한다.

ChildComment 엔티티:

@Entity
@Table(name = "child_comment")
@Getter
@Setter
public class ChildComment {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "comment_Id", nullable = false)
    private Long commentId;

    // ...

    // ChildComment와 ParentComment 사이의 다대일 관계를 표현합니다.
    // 여러 ChildComment가 한 ParentComment에 연결될 수 있기 때문입니다.
    // @JoinColumn은 해당 ChildComment 테이블의 "parent_comment_Id" 컬럼이 외래 키로 사용됨을 나타냅니다.
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "parent_comment_Id", nullable = false)
    private ParentComment parentComment;

    // ...
}

연관관계 설명:

  • ParentComment 엔티티는 여러 개의 ChildComment 엔티티를 가질 수 있습니다. 이 관계는 ParentComment@OneToMany 애노테이션을 통해 정의된다. 이 때, mappedBy 속성은 이 관계가 ChildCommentparentComment 필드에 의해 매핑되었음을 나타낸다.
  • 반대로, ChildComment 엔티티는 한 개의 ParentComment 엔티티에 속할 수 있다. 이 관계는 ChildComment@ManyToOne 애노테이션을 통해 정의되며, @JoinColumn 애노테이션은 이 관계가 데이터베이스 테이블의 어떤 컬럼에 의해 표현되는지를 나타낸다.
  • 추가로, cascade = CascadeType.ALL 옵션이 설정되어 있어, 부모 댓글(ParentComment)이 삭제될 경우 그에 연결된 모든 자식 댓글(ChildComment)도 함께 삭제된다.

이렇게 ParentCommentChildComment 사이에는 일대다, 다대일 관계가 설정되어 있다.

게시판 service

1. 게시글 전체 조회 (getBulletinBoardPost)

public List<BulletinBoardDto> getBulletinBoardPost() {
    List<BulletinBoard> bulletinBoards = bulletinBoardRepository.findAll();
    List<BulletinBoardDto> bulletinBoardDtos = new ArrayList<>();

    for (BulletinBoard bulletinBoard : bulletinBoards) {
        BulletinBoardDto bulletinBoardDto = new BulletinBoardDto();
        BeanUtils.copyProperties(bulletinBoard, bulletinBoardDto);
        bulletinBoardDtos.add(bulletinBoardDto);
    }
    return bulletinBoardDtos;
}
  • bulletinBoardRepository.findAll()를 통해 모든 게시글을 조회합니다.
  • 조회된 BulletinBoard 엔터티 목록을 BulletinBoardDto 목록으로 변환합니다.
  • 변환된 DTO 목록을 반환합니다.

2. 게시글 생성 (createBulletinBoardData)

public void createBulletinBoardData(BulletinBoardDto bulletinBoardDto) {
    BulletinBoard bulletinBoard = new BulletinBoard();
    BeanUtils.copyProperties(bulletinBoardDto, bulletinBoard);
    bulletinBoardRepository.save(bulletinBoard);
}
  • 전달받은 BulletinBoardDtoBulletinBoard 엔터티로 변환합니다.
  • 변환된 엔터티를 저장하여 DB에 새 게시글을 생성합니다.

3. 게시글 번호로 조회 (getBulletinBoardPostByPostNum)

public BulletinBoardDto getBulletinBoardPostByPostNum(Long postNum) {
    BulletinBoard bulletinBoard = bulletinBoardRepository.findByPostNum(postNum)
        .orElseThrow(() -> new ResourceNotFoundException("Post not found with postNum " + postNum));
    BulletinBoardDto bulletinBoardDto = new BulletinBoardDto();
    BeanUtils.copyProperties(bulletinBoard, bulletinBoardDto);
    return bulletinBoardDto;
}
  • bulletinBoardRepository.findByPostNum(postNum)를 사용하여 게시글 번호로 게시글을 조회합니다.
  • 조회된 BulletinBoard 엔터티를 BulletinBoardDto로 변환하여 반환합니다.

4. 게시글 수정 (조회수 업데이트: updateBulletinBoardPost)

public void updateBulletinBoardPost(BulletinBoardDto bulletinBoardPost) {
    BulletinBoard bulletinBoard = bulletinBoardRepository.findByPostNum(bulletinBoardPost.getPostNum())
        .orElseThrow(() -> new ResourceNotFoundException("Post not found with postNum " + bulletinBoardPost.getPostNum()));
    bulletinBoard.setPostCountNum(bulletinBoardPost.getPostCountNum());
    bulletinBoardRepository.save(bulletinBoard);
}
  • 게시글 번호를 기반으로 게시글을 조회합니다.
  • 조회된 게시글의 조회수를 업데이트하고, DB에 저장합니다.

5. 게시글 삭제 (deleteBulletinBoard)

public void deleteBulletinBoard(Long postNum) {
    bulletinBoardRepository.deleteById(postNum);
}
  • 게시글 번호를 기반으로 게시글을 삭제합니다.

6. 게시글 수정 (updateBulletinBoardPost - 오버로딩 버전)

public void updateBulletinBoardPost(Long postNum, BulletinBoardDto bulletinBoardDto) {
    BulletinBoard post = bulletinBoardRepository.findById(postNum)
        .orElseThrow(() -> new IllegalArgumentException("해당 게시물이 없습니다. id=" + postNum));

    post.setPostTitle(bulletinBoardDto.getPostTitle());
    post.setPostContent(bulletinBoardDto.getPostContent());
    post.setHashtagName(bulletinBoardDto.getHashtagName());
    post.setPostCategory(bulletinBoardDto.getPostCategory());
    post.setPostDateEdit(bulletinBoardDto.getPostDateEdit());

    bulletinBoardRepository.save(post);
}

  • 게시글 번호와 수정할 내용이 담긴 DTO를 전달받습니다.
  • 게시글 번호로 게시글을 조회하고, 해당 게시글의 내용을 전달받은 DTO의 내용으로 업데이트합니다.
  • 수정된 게시글을 DB에 저장합니다.

이 클래스는 게시판에 대한 핵심 CRUD 작업을 수행합니다: 생성, 읽기, 업데이트, 삭제. 코드 내에서 예외 처리도 이루어지며, 게시글을 찾을 수 없는 경우 등에 대해 적절한 예외 메시지를 반환합니다.

import { jsPDF } from "jspdf"; //라이브러리 import
const doc = new jsPDF(); // 새로운 PDF 파일을 생성
doc.text("Hello world!", 10, 10); // 내용을 입력
doc.save("sample.pdf"); //PDF의 이름 설정

deleteById vs delete

deleteById를 사용하면, 서비스 로직에서 메서드 하나만 사용해도 조회와 삭제가 모두 가능하다. 또한, 내부적으로 id에 대한 null체크도 해주기 때문에 의도치않은 NullPointerException도 예방할 수 있다.

하지만, deleteById는 내부적인 findById 조회시에 데이터가 없을 경우, EmptyResultDataAccessException이 고정으로 발생한다.

반면에, findById + delete를 사용한다면, 직접 예외처리를 커스텀할 수 있다는 장점이 있다.

개인 공부 기록용 블로그입니다.
본문에 오류가 포함되어 있을 수도 있습니다.
댓글 또는 메일 주시면 수정하도록 하겠습니다.🍀

맨 위로 이동하기

Leave a comment