개요: 본인의 뇌구조라는 컨셉으로 만든 팀 소개 프로젝트. 뇌의 각 부위를 클릭하면 각자가 작성한 내용을 볼 수 있다
프로젝트 정보
- 프로젝트: Brain-8 프로젝트 (내일배움캠프 개강 첫 미니 프로젝트)
- 개발기간: 2023.03.13 - 2023.03.16 ( 4일 )
- 역할
- 팀장
- Layout
- css
- 뇌 컨텐츠
- 개발 후 Merge
- 코드 최적화
- 사용 언어: HTML, JavaScript(Vanilla), Python
- 사용 라이브러리:
- HTML, JavaScript
- Jquery - 전체적인 동적 웹페이지 구현
- Bootstrap(프레임워크) - 각 개체들 구현
- Google Font (Gamja Flower) - 웹페이지 폰트 변경
- css animation cheet sheet - css animation 구현
- Python
- Flask - 웹 개발 프레임워크 (import Flask, render_template, request, jsonify)
- pymongo - MongoDB로 DB구성
- bson - MongoDB Primary Key인 _id값(bson형식) 변환
더보기from bson.json_util import dumps // from bson.objectid import ObjectId
- ytmusicapi - 유튜브 비공식 api, 크롤링에 사용
- HTML, JavaScript
프로젝트 구성
★ 파일구조
기본적인 구조는 Flask가 제공하는 템플릿이다
Brain-8 ─┬─ app.py (백엔드)
├─ headers_auth.json (ytmusicapi의 유튜브 접속 json파일)
├─ venv (python 가상환경 폴더)
├─ template ─── index.html (프론트엔드)
└─ static ─┬─ img ─┬─ bg.jpg ( 파스텔톤 배경 )
│ ├─ brain.png ( 머리 이미지 )
│ ├─ hat1~hat5.jpg ( 모자 이미지 )
│ ├─ part1~part8.jpg ( 뇌 각 부분 이미지 )
│ └─ ytplay.png, ytpause.png, ytstop.png ( 유튜브 )
└─ css ─┬─ style.css ( index.html css )
└─ animations.css ( 움직이는 이미지 구현 )
★ 화면구성 및 코드
1. 머리 컨텐츠
각자의 데이터를 담고 있는 머리이다
git방식의 협업을 위해서는 각자 개발할 구역에 대한 구획이 우선되어야 개발에 바로 착수할 수 있을 것 같다는 확신이 들어서 전날에 위와 같은 머리 레이아웃을 만들어 두고 floating과 hover시 색이 바뀌는 filter를 적용해두었다.
이후에 디자인을 담당한 팀원이 저렇게 모자를 씌워서 완성해주었다
모자부분 html 코드를 보자
<!-- index.html -->
<div class="brain-hat-combine">
<div class="hat">
<img src="../static/img/hat1.png">
</div>
<a href="#modal" class="brain-btn-open">
<div class="brain">
<img src="../static/img/brain.png">
<p>성철민</p>
</div>
</a>
</div>
div.brain-hat-combine클래스가 div.hat과 div.brain을 감싸고 있는 형태다
div.brain-hat-combine클래스로 감싸준 이유는 hover했을 때
1. 색이 동시에 바뀌는 효과
2. floating이 동시에 적용되는 효과
를 위해서다
a태그에는 클릭 시 모달창이 뜰 수 있도록 href="#modal"과 class="brain-btn-open"을 걸어주었다
이 부분은 뒤에서 다시 설명하겠다
그럼 css를 보자
/* style.css */
.brain-hat-combine {
position:relative;
}
.brain-hat-combine:hover {
filter: invert(53%) /* black => 파스텔 레드 */
sepia(53%)
saturate(6363%)
hue-rotate(334deg)
brightness(104%)
contrast(96%);
}
.brain {
position: absolute;
width: 10rem;
height: 10rem;
filter: invert(17%) /* black => grey */
sepia(1%)
saturate(4290%)
hue-rotate(47deg)
brightness(102%)
contrast(77%);
z-index: 1;
}
.hat {
position: absolute;
width: 11rem;
height: 7rem;
top: -30px;
z-index: 2;
}
.brain-hat-combine클래스의 position:relative는 자식 요소들의 absolute를 받아주기 위해서 관례적으로 relative로 설정해주었다.
자식 요소들에는 position:absolute 속성을 걸어주고 hat에 top: -30px을 적용해서 머리와 모자의 높이를 조절해주었다
다음으로는 hover시 추가되는 floating animation css를 보자
/* animations.css */
.floating1{
animation-name: floating1;
animation-duration: 3s;
animation-iteration-count: infinite;
animation-timing-function: ease-in-out;
}
@keyframes floating1 {
0% {
transform: translateY(0%);
}
50% {
transform: translateY(-3%);
}
100% {
transform: translateY(0%);
}
}
.floating2{
animation-name: floating2;
animation-duration: 3s;
animation-iteration-count: infinite;
animation-timing-function: ease-in-out;
}
@keyframes floating2 {
0% {
transform: translateY(0%);
}
50% {
transform: translateY(3%);
}
100% {
transform: translateY(0%);
}
}
애니메이션 이름은 머리와 모자쪽 각각 floating1, floating2이며 3s동안 Y축 방향으로 3% 정도 움직였다 돌아오도록 설정했다
또한 animation-timing-function을 ease-in-out으로 설정해 현실성있는 부드러운 움직임을 주었다
이제 이 hover시 이 css를 적용시키는 Javascript를 보자
/* index.html */
/*brain-hat-combine content hovering jquery*/
$(function () {
$('.brain-hat-combine').hover(function () {
$(this).children("div.brain").addClass('floating1')
$(this).children("div.hat").addClass('floating2')
}, function () {
$(this).children("div.brain").removeClass('floating1')
$(this).children("div.hat").removeClass('floating2');
}
)
})
/*brain-hat-combine content hovering jquery end*/
기본적인 $(document).ready(function () {});구문
JQuery 3.0이후부터는 지원하지 않기 때문에 $(function () {});구문으로 써야 한다
아무튼 brain-hat-combine class에 hover를 하게 되면 이 class를 가진 자식 요소 중에 div.brain과 div.hat에 addClass로 각각 floating1과 floating2를 적용한다
그리고 마우스가 벗어나게 되면 다시 removeClass로 floating1과 floating2를 제거한다
2. 댓글
이 기능을 개발하는데 팀원분께서 마지막날까지 고생하셨다
마지막날에는 둘이 vscode 라이브쉐어 기능으로 틀린코드찾기를 한 끝에 에러를 해결해서 발표할 수 있었다
이부분의 HTML를 살펴보자
<div class="mypost" id="textarea">
<div class="form-floating mb-3">
<input type="text" class="form-control" id="name" placeholder="url" />
<label for="floatingInput">닉네임</label>
</div>
<div class="form-floating">
<textarea class="form-control" placeholder="Leave a comment here" id="comment"
style="height: 100px"></textarea>
<label for="floatingTextarea2">응원댓글</label>
</div>
<button onclick="save_comment()" type="button" class="btn btn-dark">
댓글 남기기
</button>
</div>
<div class="mycards" id="comment-list">
<div class="card">
<div class="card-body">
<blockquote class="blockquote mb-0">
<p>새로운 앨범 너무 멋져요!</p>
<footer class="blockquote-footer">호빵맨</footer>
</blockquote>
<button onclick="edit_comment()" type="button" class="btn btn-dark">
수정
</button>
<button onclick="remove_comment()" type="button" class="btn btn-dark">
삭제
</button>
</div>
</div>
</div>
이 부분은 기본적으로 부트스트랩에서 들고오신 것으로 보이며 전체적으로 댓글쓰는 부분인 div.mypost와 댓글이 보여지는 div.mycards로 구분되어 있다
각각 버튼을 누르면 save_comment(), edit_comment(), remove_comment()함수가 실행된다
우선 페이지 로딩 시 현재 댓글 리스트를 불러오는 show_comment()함수를 보자
/* index.html */
/* 페이지 로딩 시 show_comment()함수 실행 */
$(function () {
show_comment();
});
/* show comment */
function show_comment() {
fetch('/comment', { method: "GET" })
.then((res) => res.json())
.then((data) => {
/* 리턴받은 data인 json문자열을 JSON.parse()함수로 JSON형식으로 변환 */
let rows = JSON.parse(data['result'])
/* 댓글 리스트 비우기 */
$('#comment-list').empty()
/* 최신순으로 반복문 출력 */
rows.reverse().forEach((a) => {
let name = a['name']
let comment = a['comment']
let _id = a['_id']['$oid']
let temp_html = `<div class="card" id='${_id}'>
<div class="card-body">
<blockquote class="blockquote mb-0">
<p>${comment}</p>
<footer class="blockquote-footer">${name}</footer>
</blockquote>
</div>
</div>
<button onclick="edit_comment('${_id}')" type="button" class="btn btn-dark">
수정
</button>
<button onclick="remove_comment('${_id}')" type="button" class="btn btn-dark">
삭제
</button>`
/* 하나씩 append해서 더해주기 */
$('#comment-list').append(temp_html)
});
});
};
/* show comment end */
나중에 뒤에서 설명하겠지만 app.py에서 get요청을 받고 데이터를 넘겨줄 때 아래와 같은 JSON문자열 형태로 넘겨주도록 설정했는데 그걸 JSON형식으로 변환하기 위해 JSON.parse()함수를 사용한다
그리고 각 카드를 출력할 때 id = '${_id}'로 각 카드 div에 _id값을 포함해서 출력하도록 했는데 이것은 나중에 수정이나 삭제를 할 때 primary key인 _id값을 기준으로 수정/삭제할 카드를 특정하기 위함이다
이제 백엔드인 app.py의 코드를 보자
# app.py
@app.route("/comment",methods=["GET"])
def comment_get():
all_comments = list(db.comments.find())
return jsonify({'result':dumps(all_comments)})
백엔드쪽은 단순하다
get요청을 받으면 DB의 comments에 있는 모든 데이터를 긁어와서 all_comments에 담고 dumps()함수로 감싼 뒤 jsonify로 다시 리턴해준다
우선 <DB명>.<Collection명>.find()는 무조건 pymongo의 cursor객체를 리턴한다
cursor객체는 쿼리의 결과 도큐먼트를 하나씩 읽을 수 있는 iterable한 객체이다
cursor객체로 뭔가를 더 사용하고 싶다면 여기를 참조
<pymongo.cursor.Cursor object at 0x000001BAA7CBDDF0>
cursor객체를 list로 감싸주면 비로소 아래와 같은 딕셔너리로 구성된 리스트를 얻을 수 있다
[{'_id': ObjectId('6413262faab51237a8517202'), 'name': '성철민', 'comment': '오예!!!!!!!'},
{'_id': ObjectId('6415b8fa8f5cf5954a964077'), 'name': '김치찌개', 'comment': '안녕하세요!!'}]
하지만 위 리스트를 바로 jsonify에 담아서 보내면 아래와 같은 오류가 뜰 것 이다
TypeError: Object of type ObjectId is not JSON serializable
ObjectId타입의 객체는 JSON 직렬화를 할 수 없다는 건데 이는 MongoDB의 Primary key인 _id의 타입인 ObjectId타입이 JSON 호환가능한 객체가 아니어서 생기는 문제이다. 즉 jsonify를 이용해 JSON으로 변환할 때 jsonify가 ObjectId 타입이 뭔지 몰라서 생기는 문제인 것이다. 당연하게도 _id를 제외한 다른 타입들을 담는다면 jsonify가 정상적으로 response객체를 생성한다
그렇다면 해결책은 무엇일까
1. dumps()함수로 감싸 모두 json문자열로 변환해준 뒤 보내서 JavaScript JSON.parse()함수로 다시 json형식으로 변환하기
2. dumps()함수에 ensure_ascii=False 속성을 적용해서 비리틴 문자열을 읽을 수 있는 형태 그대로 저장하기
3. _id값을 빼고 주기
우리가 사용한 코드에서는 1번 방법을 사용했지만 다음 번에는 2번 방법을 사용해보도록 하자
1,2번 방식 둘 다 잘 작동한다
이러한 방식으로 수정과 삭제 기능도 완성했다
python에서 JSON을 사용하는 자세한 방법에 대한 내용은 여기
3. 날씨
머리를 눌러서 모달창을 띄우면 왼쪽 위에 날씨가 뜬다
이 부분도 우리 팀원분께서 열심히 만들어 주셨다
기본적으로 OpenWeather 사이트의 api를 사용해서 DB에 저장된 각자의 경도, 위도 데이터를 집어넣어 위치와, 날씨와 기온을 리턴한 것이다
전체 코드가 너무 길어 일부만 발췌했다
/* index.html */
/* weather system ^~^ */
$.ajax({
type: "GET",
url: `https://api.openweathermap.org/data/2.5/weather?lat=${each_data['latitude']}&lon=${each_data['longitude']}&appid=<api>&units=metric`,
data: {},
success: function (response) {
let weather = response['weather']['0']['main']
let temp = response['main']['temp']
let point = response['name']
let temp_html = `<span>${weather}</span>`
let temp_html2 = `<span>${temp}도</span>`
$('#wth').append(temp_html)
$('#tp').append(temp_html2)
$('#point').text(point)
$('#points').text(point)
}
})
/* weather system end ^~^ */
ajax의 구문을 사용하셨다
${each_data['latutude']}와 ${each_data['longitude']}는 각각 DB에서 리턴받은 각자의 위도와 경도 데이터이다
위와 같은 ajax 기본 구문을 이용해 json을 리턴받아 머리를 클릭 시에 위 데이터를 각 위치에 삽입한다
4. 뇌 컨텐츠
내가 맡은 기능이다.
특징은 뇌의 각 부분에 hover를 했을 때 scale이 5% 커지면서 포커싱된다
그리고 클릭 했을 때는 DB에서 불러온 내용으로 텍스트가 바뀌면서 까맣게 변해 클릭 전후를 알 수 있게 했다
블로그를 눌렀을 때는 블로그 주소가 클립보드에 저장되어 관심있다면 블로그도 방문할 수 있도록 했다
개발 시에 가장 고생했던 부분은 뇌의 각 부분의 위치를 잡는 것이었다
처음에 포토샵으로 각각 패스를 따서 뇌의 각 부분을 구현하고나서 html에서 다시 짜맞출 생각을 하니 아찔했다
그래서 png파일의 위치정보를 그대로 가져와서 겹쳐볼까 생각해 그대로 export해서 적용하니, hover할 때 가장 위의 레이어만 선택되는 것이었다
어떻게 보면 당연하다. 가장 위에 있는 이미지가 머리 전체를 덮고 있기 때문에.
그래서 어쩔 수 없이 그냥 export해서 뇌 각 부분을 html에서 css로 노가다하는 방법으로 뇌를 구현했다
그리고 DB에서 각 컨텐츠를 불러오는 방식은
1. 머리를 클릭할 때 각 머리 아래에 있는 p태그의 '이름' 데이터를 읽은 뒤 post요청을 보냄 (이름 데이터는 모달창 아래에 재사용했다)
/* brain clicking */
$('.brain-btn-open').each(function () {
var imgTit = $(this).children('div.brain').children('p').text()
$(this).on('click', function (e) {
brainListing(imgTit) /* name기반 데이터 리턴 및 실행 함수 */
});
});
/* brain clicking end */
모달창과 함께 실행되는데 그 부분은 제외했다
2. 백엔드에서 이름에 일치하는 DB 데이터를 리턴받아 뇌의 각 부분 클릭 시 해당 텍스트를 리턴받은 변수로 대체한다
$('#mbti').click(function () {
/* mbti id의 class중에 brain-part 제거(회색으로 만들어주는 클래스) */
document.getElementById('mbti').classList.remove('brain-part');
/* mbti-p id를 가진 p태그의 text를 변수값으로 대체 */
$('#mbti-p').text(each_data['mbti']);
})
위와 같이 mbti라는 id를 가진 뇌의 부분을 클릭하면 brain-part 클래스를 제거해 그 부분을 까맣게 만들어주고 p태그의 텍스트를 변수값으로 대체해서 구현했다
$('#dream').click(function () {
document.getElementById('dream').classList.remove('brain-part');
$('#dream-p').text(each_data['dream']);
$('#dream-p').css('font-size', '1.2rem');
})
위와 같이 리턴한 텍스트 양이 너무 많을 경우에는 .css함수로 font-size를 조정하기도 했다
5. 유튜브 재생 컨텐츠
위의 뇌 각 부분 구현 시 전부 비슷하면 흥미가 떨어질 것 같아서 음악을 눌렀을 때는 각자 본인이 좋아하는 유튜브 음악을 자동재생시킬 수 있도록 구현했다
css 애니메이션도 넣어서 등장할 때 좀 더 생동감 있도록 만들었다
유튜브플레이어 get요청에 playerapiid=ytplayer를 추가해 iframe을 외부 버튼으로 제어할 수 있도록 하는 기능을 사용했다
우선 등장하는 코드를 보자
<!-- youtube Modal -->
<div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div id="yttransparent" style="display: none;">
</div>
</div>
<div class="ytPlay-text-cover">
<div>
<p>
제목:
<h1 id="songTitle"></h1>
</p>
</div>
<div>
<p>
아티스트:
<h2 id="songArtist"></h2>
</p>
</div>
</div>
<div id="ytPlayButton" class="ytPlay-cover">
<img src="../static/img/ytplay.png" class="ytbutton bounce play" id="play">
<img src="../static/img/ytpause.png" class="ytbutton bounce pause" id="pause">
<img src="../static/img/ytstop.png" class="ytbutton bounce stop" id="stop">
</div>
</div>
<!-- youtube Modal end -->
$('#music').click(function () {
document.getElementById('music').classList.remove('brain-part');
$('#yttransparent').empty();
$('#yttransparent').append(`
<iframe id='ytVideo' width='375' height='200'
src="https://www.youtube.com/embed/${each_data['favorite_music']}?autoplay=1&start=${each_data['start']}&mute=0&autohide='2'&modestbranding=1&enablejsapi=1&version=3&playerapiid=ytplayer"
frameBorder='0'
allow='accelerometer;
autoplay;
encrypted-media;
gyroscope;
picture-in-picture'
allowFullScreen></iframe>`)
$('#songTitle').text(each_data['music'])
$('#songArtist').text(each_data['artist'])
})
/* 유튜브 뮤직 버튼 */
$(".stop").on("click", function () {
document.getElementById('stop').classList.add('tossing');
$("iframe")[0].contentWindow.postMessage('{"event":"command","func":"' + 'stopVideo' + '","args":""}', '*');
console.log($("iframe")[0])
});
$(".pause").on("click", function () {
document.getElementById('pause').classList.add('tossing');
$("iframe")[0].contentWindow.postMessage('{"event":"command","func":"' + 'pauseVideo' + '","args":""}', '*');
console.log($("iframe")[0])
});
$(".play").on("click", function () {
document.getElementById('play').classList.add('tossing');
$("iframe")[0].contentWindow.postMessage('{"event":"command","func":"' + 'playVideo' + '","args":""}', '*');
console.log($("iframe")[0])
});
위와 같이 music이란 id를 가진 객체를 클릭했을 때 yttransparent 클래스를 가진 div의 내용을 비우고 유튜브 플레이어를 삽입한다. 이 div는 display:none;으로 설정해 눈에 보이지 않게 숨겼다
그리고 모달창 자체는 부트스트랩에서 들고 왔고 그 중에 백그라운드 부분만 따로 빼서 사용했다
프로젝트 완료 후의 감상
인생 두번째 팀장이었다
이번 팀원분들은 많이 차분하고 조용하셔서 나도 좀 차분하게 진행했다
좀 더 소통을 많이 할 수 있었는데 그러지 못했다는 후회가 남는다
프로젝트 자체는 별로 어렵지 않았지만 git을 사용해 프로젝트를 처음 했는데 git을 사용할 때는 무조건 프론트엔드 백엔드로 나누거나 각 구획이 정확하게 딱딱 나뉘어 있어야 나중에 merge할 때 conflict가 생기지 않겠다는 생각을 했다.
처음으로 프로젝트 발표회도 했고 재밌는 경험이었다
'프로그래밍 > 생각나는대로 프로젝트' 카테고리의 다른 글
[프로젝트] 06. DRF 경매 서비스 - 20세기 박물관 (0) | 2023.05.16 |
---|---|
[프로젝트] 05. Django SNS CaMu (0) | 2023.04.16 |
[프로젝트] 04. Django 무신사 재고 관리 시스템 (0) | 2023.04.10 |
[프로젝트] 03. Text RPG게임 냥이 키우기 (0) | 2023.04.03 |
[프로젝트] 01. 음악 추천 서비스 SMM (2) | 2023.03.11 |