댓글 기능을 추가하면서
문득 앞선 글( https://blog.naver.com/mythee1/223531843633)에서 소개한 파이어 베이스를 이용해 만들었던 다음과 같은 제품 정보 상세 보기 페이지에 해당 제품에 대한 댓글 기능을 추가하면 좋겠다는 생각이 들었다.
다른 사람들의 웹페이지를 보더라도, 특정 제품이나 특정 기사, 특정 주제에 대해서는 사용자마다 자신만의 의견을 댓글로 남기는 경우가 많고, 또한 댓글을 통해서 다른 사람들의 생각을 알 수도 있을 것 같아서, 여러모로 유용하리란 생각이 들었기 때문이다.
댓글 표시 형태
댓글은 위에서 이미 만들어 두었던 제품 정보 상세 페이지에 다음과 같은 방식으로 댓글을 쓰는 부분과, 동시에 해당 상품과 연관이 있는 기존 댓글만을 읽어와 화면에 표시하는 형태로 댓글 기능을 추가하기로 했다.
댓글의 특성상 세세한 정보보다는 댓글 그 자체와, 작성일 정도만 표시하도록 했고, 작성자는 필요할 것 같기도 하고, 또 한편으로는 굳이 필요할까 싶기도 해서 일단은 필드를 만들어 두기는 하되, 별도의 값을 입력받지 않고 익명(anonymous)으로 할당해 일괄 등록하기로 했다. 추후 필요하다면, 작성자의 이름이나 별칭, ID를 입력받아 대체하기 쉽도록 하려는 의도였다.
이 때문에, 실제 댓글 내용을 입력받는 부분은 다음과 같이 댓글 본문만 입력받으면 되는 간단한 형태로 구성되었다.
또한, 댓글을 일단 쓰고 난 후, 추후 해당 댓글을 수정하거나, 또는 삭제하고 싶은 경우도 있을듯했기에, 댓글을 삭제하거나 수정하는 기능도 필요할 듯했다. 이를 위해, 각각의 댓글 끝에 “E”와 “D” 버튼을 추가하고 이를 통해 삭제 또는 수정 기능이 연결되도록 구성하기로 했다. 삭제와 편집 기능은 각가 버튼에 대한 핸들러 형식으로 처리하되, 최대한 간단한 코드로 구성하려고 했다.
댓글 입력 및 표시 포맷
우선 댓글을 입력받는 부분의 폼은…..
다음과 같이 기존의 상세 정보를 표시하던 바로 아랫부분에 textarea와 입력 버튼 한 개의 아주 간단한 코드로 구현했으며,
이 부분은 화면에서 다음과 같은 형태로 표시되게 된다.
실제 기존의 댓글을 표시하는 부분의 폼은 ….
댓글 입력받는 폼에 이어서, 다음과 같이 작성자, 내용, 작성일만 표시되도록 구성했다. 각 표에 부의 넓이는 원하는 대로 조정하면 된다. (참고 : 댓글 기능의 구현은 기존에 게시판 작성 시 사용했던 원리를 가져다 활용했다.)
이렇게 하면 다음과 같은 형태로 댓글을 입력하고, 표시할 수 있는 틀이 완성된다.
댓글 등록하기
우선 댓글을 등록하는 것은…..
본질적으로 파이어 스토어의 컬렉션에 입력받은 내용을 업로드하는 것이므로, db.collection (“컬렉션 이름”).add({ 메서드를 이용해 구현할 수 있다. (이의 상세한 내용에 대해서는 이전 글 https://blog.naver.com/mythee1/223493076303 참고하면 도움이 될듯하다.)
실제로는 댓글 본문을 입력받은 변수를 content 필드에 할당하면서, 작성 시간(createdAt), 일련번호(messageNumber), 작성자 이름(author), 상품정보와의 연결을 위한 정보인 상품 연관 번호(id), 추후 삭제 또는 편집 시 확인을 위한 용도의 비밀번호(editpwd) 등을 함께 등록해 준다.
이때 내용이 없는 댓글이 등록되는 것을 막기 위해, 업로드 직전 다음과 같이 댓글 내용이 실제로 있는지를 확인하는 단계를 거치도록 했다.
또한 댓글과 함께 등록할 보조 정보들이 몇가지 있는데….
가장 중요한 것은 개별상품과 연관된 댓글을 식별하는 데 사용하는 필드인 id 값으로, 이미 상품목록에서 제품상세 페이지로 이동할때 제품을 구분하기 위해 다음과 같은 코드로 상품등록번호를 id값으로 할당한 후, URL의 일부로 포함시켜 댓글기능을 구현하려고 하는 상세보기 페이지로 이동한 상태였다.
상세보기 페이지에서는 다음과 같은 방식으로 id를 추출했는데, id 변수값에 해당되는 상품등록번호는 숫자형 값인 반면, URL을 통해 전달되어 추출된 id는 string의 형태로 전달되므로, 이를 숫자형 값으로 변환시키고, 전역변수로 선언해 둔 상태였다.
댓글 기능에서는 상품과 연관된 댓글만을 식별하는 용도로 상품등록번호를 활용해야 하는데, 이에 해당되는 값인 id 변수가 이미 전역변수로 선언되어 있는 상태이므로 별도의 추가 선언없이 그대로 사용하였다.
댓글을 추후 삭제 또는 편집 시 권한 확인에 사용하기 위한 editpwd는 최대한 간단한 코드로 만들기 위해, 업로드 단계에서 다음과 같이 1줄의 코드로 가능한 prompt 명령으로 화면에서 직접 입력받아 할당하는 방식을 사용했다.
또한 revision 번호는 일괄 0으로, 작성자에 해당하는 author는 변수를 이용해 미리 “anonymous”로 일괄 지정해 두었다. 작성 시간을 나타내는 createdAt은 timestamp를 이용해 시간을 할당했다.
댓글 일련번호인 messageNumber는 파이어 베이스가 자체적으로 카운터 기능을 제공하지 않음을 고려해서 별도의 counterDB라는 이름의 컬렉션을 만들어 댓글 번호를 저장하고,
매번 이를 불러와 값을 1 증가시켜 사용하고, 또한 수정된 값을 update메서드로 다시 저장하도록 했다.
그리고, 이렇게 댓글이 등록되면, 다음 코드를 이용해 댓글을 입력받았던 폼(=HTML의 ID가 newComment)를 공란으로 지우도록 했으며, 댓글을 표시하는 부분이 새로 업로드한 댓글을 포함하여 갱신되도록 별도 함수 readComments() 함수를 호출하도록 했다.
결과적으로 댓글을 등록하는데 부분의 전체 코드는 다음과 같은 형태로 완성했다.
document.getElementById('submitComment').addEventListener('click', function(event) {
event.preventDefault(); // 버튼의 기본 동작을 막습니다.
var newComment = document.getElementById('newComment').value;
var author = 'anonymous';
var createdAt = firebase.firestore.Timestamp.now();
// 댓글 내용이 비어있는지 확인합니다.
if (newComment.trim() === '') {
console.log("Comment content is empty. Please write a comment.");
return; // 댓글 내용이 비어있으면 함수를 종료합니다.
}
// prompt로 비밀번호를 입력받습니다.
var editpwd = prompt("댓글 삭제/수정을 위한 비밀번호를 입력하세요:");
// counterDB 컬렉션의 msgcounter 문서의 messageNumber 필드 값을 읽어옵니다.
db.collection('counterDB').doc('msgcounter').get().then((doc) => {
if (doc.exists) {
var messageNumber = doc.data().messageNumber;
messageNumber++;
db.collection('counterDB').doc('msgcounter').update({
messageNumber: messageNumber
}).then(() => {
console.log("Message number successfully updated!");
}).catch((error) => {
console.error("Error updating message number: ", error);
});
db.collection('comment').add({
author: author,
content: newComment,
revision: 0,
상품연관번호: id,
editpwd: editpwd,
messageNumber: messageNumber,
createdAt: createdAt
}).then(() => {
console.log("Comment successfully written!");
document.getElementById('newComment').value = ''; // 댓글 입력 필드를 비웁니다.
readComments(); // 댓글을 다시 읽어와 표시합니다.
}).catch((error) => {
console.error("Error writing comment: ", error);
});
} else {
console.log("댓글 Counter DB값을 찾을수 없습니다!");
}
}).catch((error) => {
console.error("Error reading document: ", error);
});
});
댓글 읽어와 표시하기
댓글을 읽어와 표시하는 것은 …..
기본적으로는 다음과 db.collection( ).where(조건 ) 메서드를 이용하여 구현하였는데,
위의 쿼리로 불려온 데이터는 다음 코드를 이용해 테이블화되어
다음과 같이 표시되게 된다.
이때 시간 표시가 위와 같이 2줄로 나누어 24시간 포맷으로 표시되도록 하기 위해 시간을 다시 날짜와 시간으로 나누어 각각의 변수에 저장한 후 <br> 코드와 함께 재조합하여 사용하였다.
댓글 기능은 본질적으로는 파이어 스토어 Database에 내용을 업로드하는 것이므로, db.collection (“컬렉션 이름”).add({ 메서드를 이용해 구현할 수 있다. (이의 상세한 내용에 대해서는 이전 글 https://blog.naver.com/mythee1/223493076303 참고하면 도움이 될듯하다.)
실제로는 댓글 본문을 입력받은 변수를 content 필드에 할당하면서, 작성 시간(createdAt), 일련번호(messageNumber), 작성자 이름(author), 상품정보와의 연결을 위한 정보인 상품 연관 번호(id), 추후 삭제 또는 편집 시 확인을 위한 용도의 비밀번호(editpwd) 등을 함께 등록해 준다.
한편 표시된 댓글에 대해 삭제와 수정 기능을 구현하기 위해, 다음과 같이 각각 15px에 해당되는 칼럼을 만들고
다음과 같이 먼저 “D” 버튼 변수를 만들고, 연속해 이 버튼이 눌리면 db.collection( ).doc(id).dekete() 명령이 실행되도록 했다.
단, 실제 삭제 권한 여부를 확인하기 위해, db.collection( ).doc(id).delete() 실행 명령자에 비밀번호를 prompt 방식으로 입력하여 if() 문으로 비교하고, 비밀번호가 일치할 때만 삭제 동작이 진행되도록 했다.
수정 기능을 위한 E 버튼의 경우도 D 버튼과 동일하게, prompt로 비밀번호를 먼저 확인한 후, 일치하는 경우에 진행하게 되는데, db.collection( ).doc(id).delete ( ) 대신 db.collection( ).doc(id).update() 메서드를 이용한다는 점이 다르며,
수정해서 대체할 새로운 댓글을 입력받아야 하는 점이 다른데, 본 에제에서는 별도로 HTML을 만드는 대신, 최대한 간단한 코드로 변경된 댓글을 입력받기 위해 다음과 같이 prompt 명령을 활용하여 1줄로 해결했다.
그리고 이렇게 입력받은 새로운 댓글을 다음 코드를 이용해 update 해주면서, 시간 정보와, revision 정보를 변경해 주면 완성이다.
결과적으로 이렇게 댓글을 읽어와 표시하는 기능은 삭제 및 수정 부분을 포함하는 하나의 함수 형태로 구현되었는데, 전체 코드는 다음과 같다.
function readComments() {
// 새로운 코드: "comment" 컬렉션에서 데이터를 가져옵니다.
var commentsTable = document.getElementById('comments'); // 댓글을 표시할 테이블 요소를 가져옵니다.
db.collection('comment')
.where('상품연관번호', '==', id) // '상품연관번호' 필드가 'id'와 같은 문서만 가져옵니다.
.orderBy('createdAt', 'desc') // 'createdAt' 필드를 기준으로 내림차순으로 정렬합니다
.get()
.then((querySnapshot) => {
// 기존에 표시된 댓글을 모두 삭제합니다.
while (commentsTable.rows.length > 1) {
commentsTable.deleteRow(1);
}
querySnapshot.forEach((doc) => {
var data = doc.data();
// 필요한 필드를 가져옵니다.
var content = data.content;
var author = data.author;
var createdAt = data.createdAt;
// Firestore timestamp를 JavaScript Date 객체로 변환합니다.
var date = new Date(doc.data().createdAt.toDate());
// 원하는 형식으로 날짜를 변환합니다.
var datePart = date.toLocaleDateString('ko-KR', { year: '2-digit', month: '2-digit', day: '2-digit' });
// 원하는 형식으로 시간을 변환합니다. hour12 옵션을 false로 설정하여 24시간 형식으로 표시합니다.
var timePart = date.toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit', hour12: false });
// 변환된 날짜와 시간을 합칩니다.
var formattedDate = datePart + '<br>' + timePart;
// 테이블 로우를 생성합니다.
var row = commentsTable.insertRow();
var authorCell = row.insertCell();
var contentCell = row.insertCell();
var createdAtCell = row.insertCell();
var editCell = row.insertCell(); // 수정 버튼을 위한 셀을 추가합니다.
var deleteCell = row.insertCell(); // 삭제 버튼을 위한 셀을 추가합니다.
// 셀의 너비를 설정합니다.
editCell.style.width = "15px";
deleteCell.style.width = "15px";
authorCell.textContent = author;
contentCell.textContent = content;
createdAtCell.innerHTML = formattedDate; // 수정된 부분
createdAtCell.style.textAlign = "center"; // 추가된 부분
// "E" 버튼을 생성하고, 클릭 이벤트 핸들러를 연결합니다.
var editButton = document.createElement("button");
editButton.textContent = "E";
editButton.style.border = "2px solid #4CAF50"; // 녹색 외곽선을 추가합니다.
editButton.addEventListener('click', function() {
// 사용자에게 비밀번호를 입력하라는 메시지를 표시합니다.
var password = prompt("댓글 작성시 입력한 비밀번호는..?");
if (password == data.editpwd) {
// 비밀번호가 일치하면, 사용자에게 수정할 댓글을 입력하라는 메시지를 표시합니다.
var newContent = prompt("수정할 댓글을 입력하세요:");
db.collection('comment').doc(doc.id).update({
content: newContent, // 입력내용으로 'content'업데이트
revision: firebase.firestore.FieldValue.increment(1),
createdAt: firebase.firestore.Timestamp.now()
}).then(() => {
console.log("Comment successfully updated!");
contentCell.textContent = newContent;
var updateDate = new Date();
var updateDatePart = updateDate.toLocaleDateString('ko-KR', { year: '2-digit', month: '2-digit', day: '2-digit' });
var updateTimePart = updateDate.toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit', hour12: false });
var formattedUpdateDate = updateDatePart + '<br>' + updateTimePart;
createdAtCell.innerHTML = formattedUpdateDate;
}).catch((error) => {
console.error("Error updating comment: ", error);
});
} else {
alert("비밀번호가 일치하지 않습니다. 댓글이 수정되지 않았습니다.");
}
});
editCell.appendChild(editButton);
// "D" 버튼을 생성하고, 클릭 이벤트 핸들러를 연결합니다.
var deleteButton = document.createElement("button");
deleteButton.textContent = "D";
deleteButton.style.border = "2px solid #f44336";
deleteButton.addEventListener('click', function() {
var password = prompt("댓글 작성시 입력한 비밀번호는..?");
if (password == data.editpwd) {
db.collection('comment').doc(doc.id).delete().then(() => {
console.log("Comment successfully deleted!");
commentsTable.deleteRow(row.rowIndex);
}).catch((error) => {
console.error("Error removing comment: ", error);
});
} else {
alert("Incorrect password. The comment was not deleted.");
}
});
deleteCell.appendChild(deleteButton);
});
}).catch((error) => {
console.error("Error reading comments: ", error);
});
}
참고로 이렇게 페이지가 처음 보였을 때, 기존 댓글이 표시되기 위해서는 위에서 만들 댓글 읽기 함수가 자동으로 실행되도록 다음과 같은 함수를 추가해 주어야 한다.
참고 – 전체 페이지의 코드는 다음에 첨부해 둔다.

