프로그래밍/생각나는대로 프로젝트

[프로젝트] 02. 팀 소개 프로젝트 Brain-8

Churnobyl 2023. 3. 19. 00:53
728x90
반응형

팀소개 프로젝트 Brain-8 메인 화면

개요: 본인의 뇌구조라는 컨셉으로 만든 팀 소개 프로젝트. 뇌의 각 부위를 클릭하면 각자가 작성한 내용을 볼 수 있다

 

 


프로젝트 정보


  • 프로젝트: Brain-8 프로젝트 (내일배움캠프 개강 첫 미니 프로젝트)
  • 개발기간: 2023.03.13 - 2023.03.16 ( 4일 )
  • 역할
    • 팀장
    • Layout
    • css
    • 뇌 컨텐츠
    • 개발 후 Merge
    • 코드 최적화
  • 사용 언어: HTML, JavaScript(Vanilla), Python
  • 사용 라이브러리:
    • HTML, JavaScript
      1. Jquery - 전체적인 동적 웹페이지 구현
      2. Bootstrap(프레임워크) -  각 개체들 구현
      3. Google Font (Gamja Flower) - 웹페이지 폰트 변경
      4. css animation cheet sheet - css animation 구현
    • Python
      1. Flask - 웹 개발 프레임워크 (import Flask, render_template, request, jsonify)
      2. pymongo - MongoDB로 DB구성
      3. bson - MongoDB Primary Key인 _id값(bson형식) 변환
        더보기
        from bson.json_util import dumps // from bson.objectid import ObjectId
      4. ytmusicapi - 유튜브 비공식 api, 크롤링에 사용

 

 


프로젝트 구성


★ 파일구조

기본적인 구조는 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 ( 움직이는 이미지 구현 )

 

 

★ 화면구성 및 코드

Brain-8 메인페이지 구성( 한 화면에 담기 위해 화면 크기를 줄였다 )
머리를 눌렀을 때 나오는 모달창

 

뇌 구조 중 '음악'을 눌렀을 때 나오는 유튜브 실행 모달창

 


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-functionease-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()함수를 사용한다

 

 

JSON 문자열 형식 (data['result'])

 

JSON형식 (JSON.parse(data['result']))

 

 

그리고 각 카드를 출력할 때 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 기본 구문

위와 같은 ajax 기본 구문을 이용해 json을 리턴받아 머리를 클릭 시에 위 데이터를 각 위치에 삽입한다

 


4. 뇌 컨텐츠

뇌 컨텐츠

내가 맡은 기능이다.

특징은 뇌의 각 부분에 hover를 했을 때 scale이 5% 커지면서 포커싱된다

그리고 클릭 했을 때는 DB에서 불러온 내용으로 텍스트가 바뀌면서 까맣게 변해 클릭 전후를 알 수 있게 했다

블로그를 눌렀을 때는 블로그 주소가 클립보드에 저장되어 관심있다면 블로그도 방문할 수 있도록 했다

 

개발 시에 가장 고생했던 부분은 뇌의 각 부분의 위치를 잡는 것이었다

처음에 포토샵으로 각각 패스를 따서 뇌의 각 부분을 구현하고나서 html에서 다시 짜맞출 생각을 하니 아찔했다

그래서 png파일의 위치정보를 그대로 가져와서 겹쳐볼까 생각해 그대로 export해서 적용하니, hover할 때 가장 위의 레이어만 선택되는 것이었다

위치정보까지 저장하는 법
'레이어를 파일로' 옵션으로 export할 때
그냥 export할 때

 

어떻게 보면 당연하다. 가장 위에 있는 이미지가 머리 전체를 덮고 있기 때문에.

그래서 어쩔 수 없이 그냥 export해서 뇌 각 부분을 html에서 css로 노가다하는 방법으로 뇌를 구현했다

 

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가 생기지 않겠다는 생각을 했다.

처음으로 프로젝트 발표회도 했고 재밌는 경험이었다

반응형