✅ "COMMENTOON" 은 국내 최대 웹툰 사이트인 ‘NAVER WEBTOON’을 크롤링하여 사용자들의 의견을 교류할 수 있는 사이트입니다.
✅ 웹툰을 읽는 도중, 혹은 읽은 직후에 자신이 느낀 감정을 다른 이들과 함께 댓글로 교류할 수 있습니다!
BACK-END | FRONT-END | BACK-END | FRONT-END | FRONT-END |
---|---|---|---|---|
로그인 + 회원가입 + 즐겨찾기 기능 구현 | 레이아웃 수정 + 프론트 엔드 작업 | 검색 기능 구현 | 레이아웃 구성 + 프론트 엔드 작업 + 댓글 / 인기순 / 장르별 / 즐겨찾기 기능 구현 | 크롤링 기능 구현 |
사전 강의(웹개발 종합반)을 수강하고 임의로 편성된 조에서 진행을 했습니다.
저는 이 프로젝트에서 크롤링해온 MongoDB 데이터를 이용하여 프론트엔드 전반적인 작업을 html, css, javascript(jQuery)를 이용하여 구현하였습니다.
기본적인 뼈대는 빠른 구현을 위해 bootstrap을 사용하였습니다.
'use strict';
/*************************
* db에서 크롤링해온 데이터 받아오기
**************************/
$(document).ready(function () {
listing();
});
function listing() {
$.ajax({
type: 'GET',
url: '/webtoons',
data: {},
success: function (response) {
let rows = response['webtoons'];
for (let i = 0; i < 30; i++) {
let title = rows[i]['title'];
let body = rows[i]['body'].replace(/\"/gi, "'"); // Change double quotes to single quotes
let img = rows[i]['img'];
let writer = rows[i]['writer'];
let url = rows[i]['url'];
let star = rows[i]['star'];
let genre = rows[i]['genre'];
let temp_html = `<button
type="button"
class="thumbnail"
data-bs-toggle="modal"
data-bs-target="#exampleModal"
data-bs-whatever="${title}"
data-writer="${writer}"
data-body="${body}"
data-url="${url}"
data-star="⭐${star}"
data-genre="${genre}"
data-img="${img}"
>
<div class="col">
<div class="card shadow-sm">
<img
src="${img}"
width="100%"
height="100%"
title="${title}"
alt="${title}"
/>
<div class="card-body">
<p class="thunmbnail__title card-text">${title}</p>
</div>
</div>
</div>
</button>`;
$('#thumbnail-box').append(temp_html);
}
readTitle();
viewComments();
},
});
}
웹개발 종합반 강의를 기반으로 ajax 호출을 진행하고 메인화면에 썸네일을 띄워주는 기능을 구현하였습니다.
/*************************
* Put information received from db in modal window
**************************/
const exampleModal = document.getElementById('exampleModal');
exampleModal.addEventListener('show.bs.modal', function (event) {
// Clean input
clearValue();
// Button that triggered the modal
const button = event.relatedTarget;
// Extract info from data-bs-* attributes
const recipient = button.getAttribute('data-bs-whatever');
const thumbImg = button.getAttribute('data-img');
const thumbBody = button.getAttribute('data-body');
const thumbStar = button.getAttribute('data-star');
const thumbWriter = button.getAttribute('data-writer');
const thumbGenre = button.getAttribute('data-genre');
const thumbUrl = button.getAttribute('data-url');
// Update the modal's content.
const modalTitle = exampleModal.querySelector('.modal-title');
const modalImg = document.getElementById('modal-img');
const modalBody = exampleModal.querySelector('.modal-desc');
const modalStar = exampleModal.querySelector('.modal-star');
const modalWriter = exampleModal.querySelector('.modal-writer');
const modalGenre = exampleModal.querySelector('.modal-genre');
const modalUrl = exampleModal.querySelector('.modal-url');
modalTitle.textContent = recipient;
modalImg.src = thumbImg;
modalBody.textContent = thumbBody;
modalStar.textContent = thumbStar;
modalWriter.textContent = thumbWriter;
modalGenre.textContent = thumbGenre;
modalUrl.href = thumbUrl;
});
썸네일(버튼)을 클릭하면 클릭한 thumbnail의 data-* 정보를 받아 modal 창에 보여주는 방식을 javascript로 구현하였습니다.
/*************************
* Leave a comment function
**************************/
// readTitle() 값을 저장해줄 변수 선언
let titleBucket = '';
// 댓글 갯수 저장을 위한 변수 선언
let commentCount = 0;
// thunmbnail의 title을 읽어오는 함수 입니다.
function readTitle() {
// title 저장을 위한 변수 선언
const thumbnails = document.querySelectorAll('.thumbnail');
thumbnails.forEach(function (thumbnail) {
thumbnail.addEventListener('click', clickThumb);
});
function clickThumb(e) {
let title = e.currentTarget.getAttribute('data-bs-whatever');
// titleBucket에 title값을 넣어줍니다
titleBucket = title;
}
}
// input, textarea를 비워주기 위한 함수
function clearValue() {
let name = document.getElementById('recipient-name');
let comment = document.getElementById('message-text');
if (name.value !== '') {
name.value = name.getAttribute('value');
comment.value = null;
} else {
name.value = null;
comment.value = null;
}
}
//
$('#commentBtn').on('click', save_comment);
// comment 저장 함수
function save_comment() {
let name = $('#recipient-name').val();
let comment = $('#message-text').val();
let title = titleBucket;
$.ajax({
type: 'POST',
url: '/toon',
data: {
name_give: name,
comment_give: comment,
title_give: title,
time_give: timeString,
},
success: function (response) {
alert(response['msg']);
// 댓글을 등록 후에 읽어온다
$.ajax({
type: 'POST',
url: '/toon/comment',
// title이 뭔지 data를 보내줘야합니다.
data: { title_give: title },
success: function (response) {
// 댓글을 등록할때는 1개 등록
let name = response['comment'][commentCount]['name'];
let comment = response['comment'][commentCount]['comment'];
let timeNow = response['comment'][commentCount]['time'];
let temp_html = `<div class="row comments">
<div class="col-3 user-name">${name}</div>
<div class="col-9">${comment}</div>
<div class="comment__btn">
<div class="comment-time">${timeNow}</div>
</div>
</div>`;
$('#comment_box').prepend(temp_html);
// 댓글이 하나 늘었습니다.
commentCount += 1;
clearValue();
commentsNumberView();
},
});
},
});
}
// comment 보는 함수
function viewComments() {
// 모든 썸네일 버튼 클릭이벤트 생성
const thumbs = document.querySelectorAll('.thumbnail');
thumbs.forEach(function (thumbnail) {
thumbnail.addEventListener('click', show_comment);
});
// 댓글을 보여주는 함수
function show_comment() {
let title = titleBucket;
// 이미 생성된 댓글을 깨끗하게 지워줍니다.
$('.comments').remove();
// 댓글 갯수를 초기화해줍니다.
commentCount = 0;
$.ajax({
type: 'POST',
url: '/toon/comment',
data: { title_give: title },
success: function (response) {
let rows = response['comment'];
for (let i = commentCount; i < rows.length; i++) {
let name = rows[i]['name'];
let comment = rows[i]['comment'];
let timeNow = rows[i]['time'];
let temp_html = `<div class="row comments">
<div class="col-3 user-name">${name}</div>
<div class="col-9">${comment}</div>
<div class="comment__btn">
<div class="comment-time">${timeNow}</div>
</div>
</div>`;
$('#comment_box').prepend(temp_html);
}
// show_comment 선언 후 commentCount에 댓글 갯수저장
commentCount = rows.length;
commentsNumberView();
},
});
}
}
댓글 작성을 맡은 팀원 분께서 어려움을 겪어, 제가 맡게 되었습니다. CRUD(Create, Read, Update, Delete) 기능을 모두 구현하고싶었으나, 제대로 된 설계를 처음부터 진행을 하지 못하여 아쉬움이 남는 부분입니다.
/*************************
* Comment box validation
**************************/
const modalNickname = document.querySelector('#recipient-name');
const modalCommentBox = document.querySelector('#message-text');
const modalCommentBtn = document.querySelector('#commentBtn');
modalCommentBtn.disabled = true;
modalNickname.addEventListener('change', noFunction);
modalCommentBox.addEventListener('change', noFunction);
function noFunction() {
if (modalNickname.value === '' || modalCommentBox.value === '') {
modalCommentBtn.disabled = true;
} else {
modalCommentBtn.disabled = false;
}
}
// textarea 엔터키 적용
modalCommentBox.addEventListener('keydown', function (e) {
if (e.keyCode == 13) {
e.preventDefault();
modalCommentBtn.click();
}
});
// comment창에 키가 입력 될때마다 noFunction을 동작 시킵니다
modalNickname.addEventListener('keyup', noFunction);
modalCommentBox.addEventListener('keyup', noFunction);
/*************************
* Display comment registration time
**************************/
const commentToday = new Date();
const year = String(commentToday.getFullYear()).slice(-2);
const month = ('0' + (commentToday.getMonth() + 1)).slice(-2);
const day = ('0' + commentToday.getDate()).slice(-2);
const hours = ('0' + commentToday.getHours()).slice(-2);
const minutes = ('0' + commentToday.getMinutes()).slice(-2);
const timeString = year + '.' + month + '.' + day + ' ' + hours + ':' + minutes;
/*************************
* Limit the number of comments
**************************/
function length_check() {
const desc = $('#message-text').val();
const nick = $('#recipient-name').val();
if (desc.length > 100) {
alert('댓글은 100자를 초과할 수 없습니다.');
$('#message-text').val(desc.substring(0, 100));
}
if (nick.length > 8) {
alert('닉네임는 8자를 초과할 수 없습니다.');
$('#recipient-name').val(nick.substring(0, 8));
}
}
modalCommentBox.addEventListener('keyup', length_check);
modalNickname.addEventListener('keyup', length_check);
/*************************
* Show the number of comments
**************************/
function commentsNumberView() {
const commentsNumber = document.querySelector('.comment__count');
if (commentCount == 0) {
commentsNumber.innerHTML =
'웹툰에 대한' + '<br />' + '의견을 남겨주세요 😍';
} else {
commentsNumber.innerHTML = `댓글 수: ${commentCount}개 👍`;
}
}
유효성 검사 및 UX를 개선하였습니다.
'use strict';
/*************************
* favorite 저장함수
**************************/
const favoriteOff = document.querySelector('#favorites-off');
favoriteOff.addEventListener('click', save_favorites);
function save_favorites() {
let title = titleBucket;
let name = document.querySelector('#useremail').innerHTML;
$.ajax({
type: 'GET',
url: '/favoritelist',
data: {},
success: function (response) {
let rows = response['favorites'];
const arrayNew = [{ name: name, title: title }];
const newArr = [...rows, ...arrayNew];
// 중복 확인
const newArrSet = [...new Set(newArr.map(JSON.stringify))].map(
JSON.parse
);
rows.length === newArrSet.length
? alert('이미 즐겨찾기 되어있습니다!')
: $.ajax({
type: 'POST',
url: '/favorites',
data: {
name_give: name,
title_give: title,
},
success: function (response) {
console.log(response);
alert(response['msg']);
},
});
},
});
}
'use strict';
const favoriteOn = document.querySelector('#favorites-on');
function save_favorites() {
let title = titleBucket;
let name = document.querySelector('#useremail').innerHTML;
$.ajax({
type: 'POST',
url: '/favorites',
data: {
name_give: name,
title_give: title,
},
success: function (response) {
alert(response['msg']);
},
});
}
// 이메일, 타이틀 삭제
favoriteOn.addEventListener('click', save_delete);
// comment 저장 함수
function save_delete() {
let title = titleBucket;
let name = document.querySelector('#useremail').innerHTML;
$.ajax({
type: 'POST',
url: '/favorites/delete',
data: {
name_give: name,
title_give: title,
},
success: function (response) {
location.reload();
alert(response['msg']);
},
});
}
즐겨찾기를 맡은 팀원 분께서 어려움을 겪어, 제가 맡게 되었습니다. 초기 설계의 중요성을 느낀 부분중 하나였습니다. 기능 구현(보여주는 것)에 집중하였습니다.
'use strict';
// genre를 담아줄 변수 선언
let genreBucket = '';
// 장르를 구하기위한 변수 선언
const genres = document.querySelectorAll('.genre__content--btn');
genres.forEach(function (genre) {
genre.addEventListener('click', clickGenreBtn);
genre.addEventListener('click', genre_listing);
//기존 moreBtn 기능 삭제
genre.addEventListener('click', function () {
moreBtn.removeEventListener('click', morebtn);
});
//장르별 moreBtn 기능 추가
genre.addEventListener('click', function () {
moreBtn.addEventListener('click', genre_listing_morebtn);
});
});
function clickGenreBtn(e) {
let genre = e.currentTarget.getAttribute('data-genre');
// genreBucket에 genre값을 넣어줍니다
genreBucket = genre;
}
$.ajax({
type: 'POST',
url: '/webtoons/genre',
// genre가 뭔지 data를 보내줘야합니다.
data: { genre_give: genre },
success: function (response) {
let rows = response['webtoons'];
if (rows.length > 30) {
for (let i = 0; i < 30; i++) {
...
@app.route('/searchToons', methods=['POST'])
def search():
receice_keywords = request.form["give_keyword"]
searched_webtoons = list(dbc.webtoons.find({'title': {'$regex': '.*' + receice_keywords + '.*'}},{'_id': False}))
searched_webtoons_edit = list({editlist['title']: editlist for editlist in searched_webtoons}.values())
return jsonify({'msg': ' 저장 ','searched_webtoons':searched_webtoons_edit,'receice_keywords':receice_keywords})
크롤링을 받아올때, 데이터가 중복으로 받아졌습니다. 그래서 팀원분이 구현하신 검색 기능에서 썸네일이 중복으로 나오는 문제가 있었습니다. dictionary의 특성인 key값을 중복으로 설정할 수 없는 것을 이용하여 중복을 제거하였습니다.
2월 중순경 '항해99 시작 전 사전 스터디가 진행되었고, 바로 다음 주 월요일(22년 2월 21일)부터 시작을 하게 되습니다. 그 이전의 팀 프로젝트는 웹 퍼블리셔 수업(21년 6월경)에서 한번 진행해봤지만(강사님 서버에서 php 기반으로 진행), 일면식도 없는 사람들과 git을 이용해서 토이 프로젝트를 진행하는 것은 처음이었습니다.
토의를 통해 주제를 선정했고, 항해99 사전 스터디 강의인 웹 개발 종합반
을 기반으로 진행하기로 하였습니다.
기존에 저는 git을 개인적으로 조금 공부를 하였으나, 다른 팀원분들은 개발 지식이 전무하거나, 배우셨어도 git에 대한 개념이 잡혀 있지 않았던 터라 git을 공부하는 시간을 가졌었습니다.
프로젝트를 진행하는 도중 merge conflict가 자주 발생했고, 많은 문제가 있었으나 부족한 부분의 공부를 통해 팀원분들은 본인의 branch에 커밋을 하시면 제가 'dev' branch에 merge를 하고 merge conflict를 해결한 뒤, main에 다시 merge하는 방식으로 진행을 하게 되었습니다.
짧은 시간동안 협업을 진행하는 방식, git을 사용하는 방법, javascript에 대한 얕은 지식(알고 있다고 착각한)을 확실하게 하는 등 정말 많은 것을 배웠습니다.
아쉬운 점이 있다면 프로젝트를 진행하며 욕심에 기능을 계속 추가하게 되면서 당장 기능만 작동하게 하는 코드를 작성하게 되었고, 리팩토링 또한 하지 못하였습니다. 시작하기 전 설계가 매우 중요하다는 것들 느끼게되었습니다.
너무 다행스럽게도 전부 좋은 분들을 만나 정보를 공유하고 같이 문제를 해결해나가는 과정에서 협업의 즐거움 또한 알게 되었습니다.
사전 프로젝트를 진행하면서 지난 공부했던 내용들이 끼워 맞춰지는 기회였습니다.
다음 주부터 진행되는 미니프로젝트에서는 ES6 문법을 최대한 활용하고, clean code를 작성하려 노력하겠습니다.
사전 스터디 임에도 불구하고 매일 20시에 gather로 회의를 진행했던 열정적인 팀원분들에게 다시 한번 감사드리고, 성장한 모습으로 다시 만나 뵙게 되었으면 좋겠습니다.