TIL

[250219TIL] 댓글 답글 기능 구현

도원좀비 2025. 2. 19. 20:49

 

    <script type="module">
        import { initializeApp } from "https://www.gstatic.com/firebasejs/9.22.0/firebase-app.js";
        import { getFirestore } from "https://www.gstatic.com/firebasejs/9.22.0/firebase-firestore.js";
        import { collection, addDoc } from "https://www.gstatic.com/firebasejs/9.22.0/firebase-firestore.js";
        import { getDocs, limit, getDoc, doc, deleteDoc, onSnapshot, query, where, orderBy, updateDoc } from "https://www.gstatic.com/firebasejs/9.22.0/firebase-firestore.js";

        const firebaseConfig = {
            apiKey: "---------------------------",
            authDomain: "---------------------",
            projectId: -------------",
            storageBucket: "------------------------",
            messagingSenderId: "--------------------------------",
            appId: "-----------------------------"
        };

        const app = initializeApp(firebaseConfig);
        const db = getFirestore(app);

        const params = new URLSearchParams(window.location.search);
        const id = params.get("id");

        async function loadMember() {
            let docSnap = await getDoc(doc(db, "members", id));
            let row = docSnap.data();

            let image = row['image'];
            let name = row['name'];
            let advantage = row['advantage'];
            let blog = row['blog'];
            let mbti = row['mbti'];
            let style = row['style'];

            let temp_html = `
                <div>
                    <figure class="figure">
                        <img src="${image}" class="figure-img" id="image" alt="...">
                        <p class="name" id="name">${name}</p>
                    </figure>
                    <div class="container bg-light text-dark rounded p-3 mb-2">
                    <div class="row mb-3">
                        <label for="inputstyle" class="label">MBTI</label>
                        <label for="inputblogaddr" class="label2">|</label>
                        <p class="list_text" id="mbti">${mbti}</p>
                    </div>
                    <div class="row mb-3">
                        <label for="inputtag" class="label">장점</label>
                        <label for="inputblogaddr" class="label2">|</label>
                        <p class="list_text" id="advantage">${advantage}</p>
                    </div>
                    <div class="row mb-3">
                        <label for="inputblogaddr" class="label">협업스타일</label>
                        <label for="inputblogaddr" class="label2">|</label>
                        <p class="list_text" id="style">${style}</p>
                    </div>
                    <div class="row mb-3">
                        <label for="inputgoods" class="label">blog</label>
                        <label for="inputblogaddr" class="label2">|</label>
                        <p class="list_text" id="blog">${blog}</p>
                    </div>
                  </div>
                </div>`;
            $('#group').append(temp_html);
        }

        async function deleteMember() {
            if (!confirm("정말 삭제하시겠습니까?")) return;
            try {
                await deleteDoc(doc(db, "members", id));
                alert("삭제되었습니다.");
                window.location.href = "index.html"; // 삭제 후 메인 페이지로 이동
            } catch (error) {
                console.error("삭제 실패:", error);
                alert("삭제 실패!");
            }
        }

        $("#commentbtn").click(async function () {
            const urlParams = new URL(location.href).searchParams;
            const id = urlParams.get('id');

            let author = $("#inputAuthor").val().trim();
            let content = $("#inputComment").val().trim();
            let profileImg = $("#inputProfileImg").val();

            if (!author || !content) {
                alert("작성자와 내용을 입력하세요.");
                return;
            }

            try {
                await addDoc(collection(db, "comments"), {
                    postId: id,
                    author: author,
                    content: content,
                    profileImg: profileImg,
                    timestamp: new Date()
                })
                alert("댓글이 작성되었습니다.");
                $("#inputAuthor").val("");
                $("#inputComment").val("");
                $("#inputProfileImg").val("");
            } catch (error) {
                console.error("댓글 작성 실패:", error);
                alert("댓글 작성 중 오류가 발생했습니다.");
            }
        });

        function loadReplies(commentId, replyContainer) {
            const q = query(
                collection(db, "replies"),
                where("commentId", "==", commentId),
                orderBy("timestamp") //desc 삭제
            );

            onSnapshot(q, (snapshot) => {
                replyContainer.empty();

            snapshot.forEach((doc) => {
                let reply = doc.data();
                let id = doc.id;
                let timestamp = reply.timestamp ? new Date(reply.timestamp.toDate()).toLocaleString() : "방금 전";
                let replyItem = `
                    <div class="border p-2 ms-4 bg-light rounded" id="${id}">
                        <strong>${reply.author}</strong>
                        <small class="text-muted">${timestamp}</small>
                        <button id="replydelbtn" type="button" class="commentdelbtn">x</button>
                        <button class="btn btn-sm btn-outline-primary edit-reply-btn" data-id="${id}">수정</button>
                        <p>${reply.content}</p>
                    </div>
                `;
                    replyContainer.append(replyItem);
                });
            });
        }

        function loadComments(postId) {
            const commentsContainer = $(".container.bg-light.text-dark.rounded.p-3.mb-2 div");
            commentsContainer.empty();

            const q = query(
                collection(db, "comments"),
                where("postId", "==", postId),
                orderBy("timestamp") // desc 삭제
            );

            onSnapshot(q, (snapshot) => {
                commentsContainer.empty();

                snapshot.forEach((doc) => {
                    let comment = doc.data();
                    let commentId = doc.id;
                    let timestamp = comment.timestamp ? new Date(comment.timestamp.toDate()).toLocaleString() : "방금 전";
                    let img = comment.profileImg || "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSM9V2kmxoDbx1pTAk_UONvWOR9vQFCkITkmw&s";

                let replyContainer = $(`<div class="ms-4"></div>`);  // 고유 id 추가

                    let commentItem = `
                    <div class="border p-3 mb-2 bg-white rounded" id="${commentId}">
                    <img src="${img}" style="width: 30px; height: 30px; border-radius: 100px" onerror="this.onerror=null; this.src='https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSM9V2kmxoDbx1pTAk_UONvWOR9vQFCkITkmw&s';">
                    <strong>${comment.author}</strong>
                    <small class="text-muted">${timestamp}</small>
                    <button id="comdelbtn" type="button" class="commentdelbtn">x</button>
                    <button class="btn btn-sm btn-outline-primary edit-btn" data-id="${commentId}">수정</button>
                    <p>${comment.content}</p>
                    <button class="btn btn-sm btn-outline-secondary reply-btn" data-id="${commentId}">답글</button>
                    <div class="reply-form" id="replyForm-${commentId}" style="display: none;">
                        <input type="text" id="replyAuthor-${commentId}" placeholder="작성자" class="form-control mb-1">
                        <input type="text" id="replyContent-${commentId}" placeholder="답글 작성" class="form-control mb-1">
                        <button class="btn btn-sm btn-secondary" id="replyBtn-${commentId}">작성</button>
                    </div>
                    </div>
                    `;
                    commentsContainer.append(commentItem);
                    commentsContainer.append(replyContainer);

                    loadReplies(commentId, replyContainer);  // 답글 불러오기

                    // 답글 폼 토글
                    document.querySelector(`.reply-btn[data-id='${commentId}']`).addEventListener('click', function () {
                        let replyForm = document.querySelector(`#replyForm-${commentId}`);
                        replyForm.style.display = (replyForm.style.display === 'block') ? 'none' : 'block';
                    });

                    // 답글 작성 버튼 클릭
                    document.querySelector(`#replyBtn-${commentId}`).addEventListener('click', function () {
                        addReply(commentId);
                    });
                });
            });
        }

        $(document).on("click", ".edit-btn", function () {
            let commentId = $(this).data("id");
            let commentDiv = $(this).closest(".border.p-3.mb-2.bg-white.rounded");
            let commentText = commentDiv.find("p").text();

            let inputField = $(`<input type='text' class='form-control edit-input' value='${commentText}'>`);
            let saveButton = $(`<button class='btn btn-sm btn-secondary save-btn'>저장</button>`); // 여기서 btn-success를 btn-secondary로 변경
            let editButton = $(this);

            commentDiv.find("p").replaceWith(inputField);
            editButton.replaceWith(saveButton);

            saveButton.click(async function () {
                let updatedText = inputField.val().trim();
                if (!updatedText) {
                    alert("내용을 입력해주세요.");
                    return;
                }

                try {
                    await updateDoc(doc(db, "comments", commentId), {
                        content: updatedText
                    });

                    let commentContent = $(`<p class='comment-content'>${updatedText}</p>`);
                    inputField.replaceWith(commentContent);
                    saveButton.replaceWith(editButton);
                } catch (error) {
                    console.error("댓글 수정 실패:", error);
                    alert("댓글 수정 중 오류가 발생했습니다.");
                }
            });
        });

        $(document).on("click", ".edit-reply-btn", function () {
            let replyId = $(this).data("id");
            let replyDiv = $(this).closest(".border.p-2.ms-4.bg-light.rounded");
            let replyText = replyDiv.find("p").text();

            let inputField = $(`<input type='text' class='form-control edit-input' value='${replyText}'>`);
            let saveButton = $(`<button class='btn btn-sm btn-secondary save-reply-btn'>저장</button>`);
            let editButton = $(this);

            replyDiv.find("p").replaceWith(inputField);
            editButton.replaceWith(saveButton);

            saveButton.click(async function () {
                let updatedText = inputField.val().trim();
                if (!updatedText) {
                    alert("내용을 입력해주세요.");
                    return;
                }

                try {
                    await updateDoc(doc(db, "replies", replyId), {
                        content: updatedText
                    });

                    let replyContent = $(`<p class='reply-content'>${updatedText}</p>`);
                    inputField.replaceWith(replyContent);
                    saveButton.replaceWith(editButton);
                } catch (error) {
                    console.error("답글 수정 실패:", error);
                    alert("답글 수정 중 오류가 발생했습니다.");
                }
            });
        });


        async function addReply(commentId) {
            const urlParams = new URL(location.href).searchParams;
            const id = urlParams.get('id');

            let author = $(`#replyAuthor-${commentId}`).val().trim();
            let content = $(`#replyContent-${commentId}`).val().trim();

            if (!author || !content) {
                alert("작성자와 내용을 입력하세요.");
                return;
            }

            try {
                await addDoc(collection(db, "replies"), {
                    commentId: commentId,
                    author: author,
                    content: content,
                    timestamp: new Date()
                });

                alert("답글이 작성되었습니다.");
                $(`#replyAuthor-${commentId}`).val("");
                $(`#replyContent-${commentId}`).val("");

                let replyContainer = $(`#replyContainer-${commentId}`);
                loadReplies(commentId, replyContainer);
            } catch (error) {
                console.error("답글 작성 실패:", error);
                alert("답글 작성 중 오류가 발생했습니다.");
            }
        }

        loadMember();
        document.querySelector("#btn").addEventListener("click", function () {
            window.location.href = "index.html"; // 메인 페이지로 이동
        });

        document.querySelector("#delete-btn").addEventListener("click", deleteMember);

        document.querySelector("#edit-btn").addEventListener("click", function () {
            window.location.href = `create.html?id=${id}`; // 수정 페이지로 이동
        });

        $(document).ready(function () {
            const urlParams = new URL(location.href).searchParams;
            const id = urlParams.get('id');
            loadComments(id);
        })


        //댓글 삭제
        $(document).ready(function () {
            $(document).on("click", '#comdelbtn', async function () {
                let id = $(this).parent().attr("id"); // "#comdelbtn" 대신에 this 넣음(가장 위에 위치한 댓글만 삭제되는 오류 해결)
                await deleteDoc(doc(db, "comments", id));
                console.log(id)
                alert('댓글이 삭제되었습니다');
                window.location.reload();
            })
            $(document).on("click", '#replydelbtn', async function () {
              let id = $(this).parent().attr("id"); // "#replydelbtn" 대신에 this 넣음(가장 위에 위치한 댓글만 삭제되는 오류 해결)
              await deleteDoc(doc(db, "replies", id));
              alert('댓글이 삭제되었습니다');
              window.location.reload();
            })
        })


    </script>
</head>

<body class="main text-dark">
    <div class="inputgroup" id="group">
    </div>

    <div class="mybtn">
        <button id="btn" type="button" class="btn btn-secondary">목록</button>
        <button id="edit-btn" type="button" class="btn btn-secondary">수정</button>
        <button id="delete-btn" type="button" class="btn btn-secondary">삭제</button>
    </div>

    <div class="container py-5">
        <div class="container bg-light text-dark rounded p-3 mb-2">
            <h4 class="mb-4">댓글 목록</h4>
            <div>
            </div>
        </div>
        <div class="comment container bg-light text-dark rounded p-3">
            <form class="row g-3">
                <h4 class="mb-4">댓글 작성</h4>
                <div style="display: flex;">
                    <div>
                        <label for="inputAuthor" class="form-label">작성자</label>
                        <input type="text" class="form-control" id="inputAuthor" placeholder="이름">
                    </div>
                    <div style="width: 100%; margin-left: 5px">
                        <label for="inputProfileImg" class="form-label">프로필 사진</label>
                        <input type="text" class="form-control" id="inputProfileImg" placeholder="URL">
                    </div>
                </div>
                <div class="col-12">
                    <label for="inputComment" class="form-label">내용</label>
                    <input type="text" class="form-control" id="inputComment" placeholder="댓글 작성">
                </div>
                <div class="col-12">
                    <button id="commentbtn" type="button" class="btn btn-secondary">작성</button>
                </div>
            </form>
        </div>
    </div>

</body>

</html>

구현 사진

댓글 코드

 $("#commentbtn").click(async function () {
            const urlParams = new URL(location.href).searchParams;
            const id = urlParams.get('id');

            let author = $("#inputAuthor").val().trim();
            let content = $("#inputComment").val().trim();
            let profileImg = $("#inputProfileImg").val();

            if (!author || !content) {
                alert("작성자와 내용을 입력하세요.");
                return;
            }

            try {
                await addDoc(collection(db, "comments"), {
                    postId: id,
                    author: author,
                    content: content,
                    profileImg: profileImg,
                    timestamp: new Date()
                })
                alert("댓글이 작성되었습니다.");
                $("#inputAuthor").val("");
                $("#inputComment").val("");
                $("#inputProfileImg").val("");
            } catch (error) {
                console.error("댓글 작성 실패:", error);
                alert("댓글 작성 중 오류가 발생했습니다.");
            }
        });

댓글을 작성해서 파이어 베이스로 보내고 해당 테이블이 없으면 생성까지 하는 코드

 

답글 코드

async function addReply(commentId) {
            const urlParams = new URL(location.href).searchParams;
            const id = urlParams.get('id');

            let author = $(`#replyAuthor-${commentId}`).val().trim();
            let content = $(`#replyContent-${commentId}`).val().trim();

            if (!author || !content) {
                alert("작성자와 내용을 입력하세요.");
                return;
            }

            try {
                await addDoc(collection(db, "replies"), {
                    commentId: commentId,
                    author: author,
                    content: content,
                    timestamp: new Date()
                });

                alert("답글이 작성되었습니다.");
                $(`#replyAuthor-${commentId}`).val("");
                $(`#replyContent-${commentId}`).val("");

                let replyContainer = $(`#replyContainer-${commentId}`);
                loadReplies(commentId, replyContainer);
            } catch (error) {
                console.error("답글 작성 실패:", error);
                alert("답글 작성 중 오류가 발생했습니다.");
            }
        }

댓글 아래에 답글을 작성 하는 코드

 

댓글과 답글을 출력하는 코드

        function loadReplies(commentId, replyContainer) {
            const q = query(
                collection(db, "replies"),
                where("commentId", "==", commentId),
                orderBy("timestamp") //desc 삭제
            );

            onSnapshot(q, (snapshot) => {
                replyContainer.empty();

            snapshot.forEach((doc) => {
                let reply = doc.data();
                let id = doc.id;
                let timestamp = reply.timestamp ? new Date(reply.timestamp.toDate()).toLocaleString() : "방금 전";
                let replyItem = `
                    <div class="border p-2 ms-4 bg-light rounded" id="${id}">
                        <strong>${reply.author}</strong>
                        <small class="text-muted">${timestamp}</small>
                        <button id="replydelbtn" type="button" class="commentdelbtn">x</button>
                        <button class="btn btn-sm btn-outline-primary edit-reply-btn" data-id="${id}">수정</button>
                        <p>${reply.content}</p>
                    </div>
                `;
                    replyContainer.append(replyItem);
                });
            });
        }

        function loadComments(postId) {
            const commentsContainer = $(".container.bg-light.text-dark.rounded.p-3.mb-2 div");
            commentsContainer.empty();

            const q = query(
                collection(db, "comments"),
                where("postId", "==", postId),
                orderBy("timestamp") // desc 삭제
            );

            onSnapshot(q, (snapshot) => {
                commentsContainer.empty();

                snapshot.forEach((doc) => {
                    let comment = doc.data();
                    let commentId = doc.id;
                    let timestamp = comment.timestamp ? new Date(comment.timestamp.toDate()).toLocaleString() : "방금 전";
                    let img = comment.profileImg || "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSM9V2kmxoDbx1pTAk_UONvWOR9vQFCkITkmw&s";

                let replyContainer = $(`<div class="ms-4"></div>`);  // 고유 id 추가

                    let commentItem = `
                    <div class="border p-3 mb-2 bg-white rounded" id="${commentId}">
                    <img src="${img}" style="width: 30px; height: 30px; border-radius: 100px" onerror="this.onerror=null; this.src='https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSM9V2kmxoDbx1pTAk_UONvWOR9vQFCkITkmw&s';">
                    <strong>${comment.author}</strong>
                    <small class="text-muted">${timestamp}</small>
                    <button id="comdelbtn" type="button" class="commentdelbtn">x</button>
                    <button class="btn btn-sm btn-outline-primary edit-btn" data-id="${commentId}">수정</button>
                    <p>${comment.content}</p>
                    <button class="btn btn-sm btn-outline-secondary reply-btn" data-id="${commentId}">답글</button>
                    <div class="reply-form" id="replyForm-${commentId}" style="display: none;">
                        <input type="text" id="replyAuthor-${commentId}" placeholder="작성자" class="form-control mb-1">
                        <input type="text" id="replyContent-${commentId}" placeholder="답글 작성" class="form-control mb-1">
                        <button class="btn btn-sm btn-secondary" id="replyBtn-${commentId}">작성</button>
                    </div>
                    </div>
                    `;
                    commentsContainer.append(commentItem);
                    commentsContainer.append(replyContainer);

                    loadReplies(commentId, replyContainer);  // 답글 불러오기

                    // 답글 폼 토글
                    document.querySelector(`.reply-btn[data-id='${commentId}']`).addEventListener('click', function () {
                        let replyForm = document.querySelector(`#replyForm-${commentId}`);
                        replyForm.style.display = (replyForm.style.display === 'block') ? 'none' : 'block';
                    });

                    // 답글 작성 버튼 클릭
                    document.querySelector(`#replyBtn-${commentId}`).addEventListener('click', function () {
                        addReply(commentId);
                    });
                });
            });
        }

loadReplies로 답글을 출력하고 폼으로 변환 한후에 loadComments 안에 replyContainer로 넣어서 구현 했다.

 

답글 구현중에 막히는 부분이 있었는데 파이어베이스에서 색인을 추가 해줘야 했다.

개발자도구에서 에러를 확인했지만 해당 오류가 아닌 줄 알고 넘어간게 패착이었다.