TIL
[250219TIL] 댓글 답글 기능 구현
도원좀비
2025. 2. 19. 20:49
<script type="module">
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로 넣어서 구현 했다.
답글 구현중에 막히는 부분이 있었는데 파이어베이스에서 색인을 추가 해줘야 했다.

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