Skip to Content
💻 코리아IT아카데미 신촌 - 프로그래밍 학습 자료
Python 프로그래밍Unit 11: 나의 첫 웹 앱 만들기Topic 4: 나만의 엔터테인먼트 대시보드 🎯

Topic 4: 나만의 엔터테인먼트 허브 완성하기 🎬🎮🎵

목표: 지금까지 배운 모든 것을 합쳐서 개인 맞춤형 엔터테인먼트 대시보드를 만들어요!

🎯 학습 목표

  • 모든 기능 통합: 영화, 게임, 음악, 투표, 플레이리스트를 한 페이지에!
  • 아름다운 디자인: 프로처럼 멋진 엔터테인먼트 허브
  • 개인화: 나만의 취향이 담긴 엔터테인먼트 홈페이지
  • 완성의 기쁨: 친구들에게 자랑할 수 있는 작품! ✨

🌟 대작 프로젝트: 엔터테인먼트 허브

지금까지 배운 모든 것을 합쳐서 나만의 엔터테인먼트 슈퍼 대시보드를 만들어봐요!

준비하기

pip install flask matplotlib requests beautifulsoup4

슈퍼 대시보드 메인 코드

app.py - 모든 기능을 합친 완전체:

from flask import Flask, render_template, request, redirect, url_for import matplotlib.pyplot as plt import matplotlib matplotlib.use('Agg') # GUI 없는 환경에서 사용 import io import base64 import random import requests from bs4 import BeautifulSoup from datetime import datetime import numpy as np app = Flask(__name__) # 전역 데이터 저장소 dashboard_data = { 'name': '나만의 엔터테인먼트 허브', 'entertainment_votes': {'🎬 영화': 0, '🎮 게임': 0, '🎵 음악': 0, '📺 드라마': 0, '🎪 예능': 0}, 'watchlist': [], 'entertainment_stats': {'영화 시청': 85, '게임 플레이': 75, '음악 감상': 90, '드라마 정주행': 60} } def get_entertainment_quote(): """엔터테인먼트 관련 명언 가져오기""" quotes = [ {"text": "영화는 꿈을 보는 예술이다.", "author": "영화감독"}, {"text": "게임은 또 다른 세상으로의 문이다.", "author": "게임 개발자"}, {"text": "음악은 영혼의 언어다.", "author": "음악가"}, {"text": "좋은 콘텐츠는 시간을 멈춘다.", "author": "크리에이터"}, {"text": "엔터테인먼트는 삶에 색깔을 칠한다.", "author": "예술가"} ] return random.choice(quotes) def get_trending_content(): """트렌딩 엔터테인먼트 컨텐츠 가져오기 (가상 데이터)""" trending_items = [ {"title": "오징어 게임 시즌 2", "type": "드라마", "icon": "📺", "rating": "9.2"}, {"title": "젤다의 전설: 왕국의 눈물", "type": "게임", "icon": "🎮", "rating": "9.8"}, {"title": "뉴진스 - Super Shy", "type": "음악", "icon": "🎵", "rating": "9.5"}, {"title": "가디언즈 오브 갤럭시 3", "type": "영화", "icon": "🎬", "rating": "8.8"}, {"title": "SNL 코리아", "type": "예능", "icon": "🎪", "rating": "8.5"}, ] trending = random.choice(trending_items) trending['time'] = datetime.now().strftime("%H:%M") return trending def create_entertainment_stats_chart(): """엔터테인먼트 활동 통계 차트 생성""" activities = list(dashboard_data['entertainment_stats'].keys()) progress = list(dashboard_data['entertainment_stats'].values()) fig, ax = plt.subplots(figsize=(10, 6)) colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96E6B3'] bars = ax.bar(activities, progress, color=colors) # 진도 퍼센트 표시 for bar, prog in zip(bars, progress): ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1, f'{prog}%', ha='center', va='bottom', fontweight='bold') ax.set_ylim(0, 100) ax.set_ylabel('활동 점수 (%)', fontsize=12) ax.set_title('🎭 나의 엔터테인먼트 활동', fontsize=16, fontweight='bold', pad=20) ax.grid(True, axis='y', alpha=0.3) # 스타일링 ax.spines['top'].set_visible(False) ax.spines['right'].set_visible(False) # Base64로 변환 img = io.BytesIO() plt.savefig(img, format='png', bbox_inches='tight', dpi=150) img.seek(0) plt.close() return base64.b64encode(img.getvalue()).decode() def create_entertainment_preference_chart(): """엔터테인먼트 선호도 파이 차트 생성""" preferences = list(dashboard_data['entertainment_votes'].keys()) votes = list(dashboard_data['entertainment_votes'].values()) # 투표가 있는 것만 표시 filtered_data = [(pref, vote) for pref, vote in zip(preferences, votes) if vote > 0] if not filtered_data: # 기본 데이터 filtered_data = [('🎬 영화', 1)] preferences, votes = zip(*filtered_data) fig, ax = plt.subplots(figsize=(8, 8)) colors = ['#FF9999', '#66B2FF', '#99FF99', '#FFCC99', '#FF99CC'] wedges, texts, autotexts = ax.pie(votes, labels=preferences, colors=colors[:len(preferences)], autopct='%1.1f%%', startangle=90, textprops={'fontsize': 12}) ax.set_title('🎭 나의 엔터테인먼트 선호도', fontsize=16, fontweight='bold', pad=20) # Base64로 변환 img = io.BytesIO() plt.savefig(img, format='png', bbox_inches='tight', dpi=150) img.seek(0) plt.close() return base64.b64encode(img.getvalue()).decode() @app.route('/', methods=['GET', 'POST']) def dashboard(): if request.method == 'POST': # 엔터테인먼트 선호도 투표 처리 if 'entertainment' in request.form: entertainment = request.form.get('entertainment') if entertainment in dashboard_data['entertainment_votes']: dashboard_data['entertainment_votes'][entertainment] += 1 # 관람/플레이 목록 추가 elif 'watchlist_item' in request.form: item = request.form.get('watchlist_item') if item.strip(): dashboard_data['watchlist'].append({ 'text': item, 'completed': False, 'id': len(dashboard_data['watchlist']) }) # 이름 변경 elif 'dashboard_name' in request.form: new_name = request.form.get('dashboard_name') if new_name.strip(): dashboard_data['name'] = new_name return redirect(url_for('dashboard')) # 데이터 준비 quote = get_entertainment_quote() trending = get_trending_content() entertainment_chart = create_entertainment_stats_chart() preference_chart = create_entertainment_preference_chart() # 통계 계산 total_votes = sum(dashboard_data['entertainment_votes'].values()) avg_progress = sum(dashboard_data['entertainment_stats'].values()) / len(dashboard_data['entertainment_stats']) return render_template('entertainment_dashboard.html', dashboard_name=dashboard_data['name'], quote=quote, trending=trending, entertainment_chart=f'data:image/png;base64,{entertainment_chart}', preference_chart=f'data:image/png;base64,{preference_chart}', entertainment_votes=dashboard_data['entertainment_votes'], watchlist=dashboard_data['watchlist'], total_votes=total_votes, avg_progress=f'{avg_progress:.1f}') @app.route('/toggle_watchlist/<int:item_id>') def toggle_watchlist(item_id): """관람목록 완료/미완료 토글""" for item in dashboard_data['watchlist']: if item['id'] == item_id: item['completed'] = not item['completed'] break return redirect(url_for('dashboard')) @app.route('/delete_watchlist/<int:item_id>') def delete_watchlist(item_id): """관람목록 삭제""" dashboard_data['watchlist'] = [item for item in dashboard_data['watchlist'] if item['id'] != item_id] return redirect(url_for('dashboard')) if __name__ == '__main__': app.run(debug=True)

엔터테인먼트 허브 템플릿

templates/entertainment_dashboard.html - 모든 것이 담긴 완전체:

<!DOCTYPE html> <html> <head> <title>{{ dashboard_name }} 🚀</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Apple SD Gothic Neo', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; padding: 20px; color: #333; } .header { text-align: center; color: white; margin-bottom: 30px; } .dashboard-title { font-size: 2.5em; margin-bottom: 10px; text-shadow: 2px 2px 4px rgba(0,0,0,0.3); } .dashboard-subtitle { font-size: 1.2em; opacity: 0.9; } .dashboard-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 25px; max-width: 1400px; margin: 0 auto; } .card { background: white; border-radius: 20px; padding: 25px; box-shadow: 0 15px 35px rgba(0,0,0,0.1); backdrop-filter: blur(10px); transition: transform 0.3s ease; } .card:hover { transform: translateY(-5px); } .card-title { font-size: 1.4em; font-weight: bold; margin-bottom: 20px; color: #333; display: flex; align-items: center; gap: 10px; } .weather-card { background: linear-gradient(135deg, #74b9ff, #0984e3); color: white; } .weather-main { display: flex; align-items: center; justify-content: space-between; margin-bottom: 15px; } .weather-icon { font-size: 3em; } .weather-temp { font-size: 2.5em; font-weight: 300; } .weather-condition { font-size: 1.2em; opacity: 0.9; } .weather-time { font-size: 0.9em; opacity: 0.8; text-align: right; } .quote-card { background: linear-gradient(135deg, #a29bfe, #6c5ce7); color: white; text-align: center; } .quote-text { font-size: 1.3em; font-style: italic; margin-bottom: 15px; line-height: 1.6; } .quote-author { font-size: 1em; opacity: 0.9; } .quote-author::before { content: "— "; } .chart-container { text-align: center; } .chart-container img { max-width: 100%; height: auto; border-radius: 10px; } .stats-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-top: 20px; } .stat-item { background: #f8f9fa; padding: 15px; border-radius: 10px; text-align: center; } .stat-number { font-size: 2em; font-weight: bold; color: #667eea; } .stat-label { font-size: 0.9em; color: #666; margin-top: 5px; } .mood-voting { display: grid; grid-template-columns: repeat(auto-fit, minmax(80px, 1fr)); gap: 10px; margin-bottom: 20px; } .mood-btn { padding: 15px 10px; border: 2px solid #ddd; border-radius: 15px; background: white; cursor: pointer; transition: all 0.3s; text-align: center; font-size: 1.1em; } .mood-btn:hover { border-color: #667eea; transform: scale(1.05); } .todo-form { display: flex; gap: 10px; margin-bottom: 20px; } .todo-input { flex: 1; padding: 12px; border: 2px solid #ddd; border-radius: 10px; font-size: 16px; } .add-btn { padding: 12px 20px; background: #667eea; color: white; border: none; border-radius: 10px; cursor: pointer; font-size: 16px; transition: background 0.3s; } .add-btn:hover { background: #764ba2; } .todo-list { list-style: none; } .todo-item { display: flex; align-items: center; justify-content: space-between; padding: 12px; margin-bottom: 8px; background: #f8f9fa; border-radius: 10px; transition: all 0.3s; } .todo-item.done { opacity: 0.6; text-decoration: line-through; } .todo-text { flex: 1; margin-left: 10px; } .todo-actions { display: flex; gap: 5px; } .btn-sm { padding: 5px 10px; border: none; border-radius: 5px; cursor: pointer; font-size: 12px; transition: all 0.3s; } .btn-toggle { background: #28a745; color: white; } .btn-delete { background: #dc3545; color: white; } .settings-form { margin-bottom: 20px; } .settings-input { width: 100%; padding: 12px; border: 2px solid #ddd; border-radius: 10px; font-size: 16px; margin-bottom: 10px; } .settings-btn { width: 100%; padding: 12px; background: #17a2b8; color: white; border: none; border-radius: 10px; cursor: pointer; font-size: 16px; } .footer { text-align: center; color: white; margin-top: 40px; opacity: 0.8; } @media (max-width: 768px) { .dashboard-grid { grid-template-columns: 1fr; } .dashboard-title { font-size: 2em; } } </style> </head> <body> <div class="header"> <h1 class="dashboard-title">{{ dashboard_name }}</h1> <p class="dashboard-subtitle">나만의 개인 맞춤형 대시보드</p> </div> <div class="dashboard-grid"> <!-- 날씨 카드 --> <div class="card weather-card"> <h2 class="card-title">🌤️ 날씨 정보</h2> <div class="weather-main"> <div> <div class="weather-icon">{{ weather.icon }}</div> <div class="weather-condition">{{ weather.condition }}</div> </div> <div> <div class="weather-temp">{{ weather.temp }}°</div> <div class="weather-time">{{ weather.time }} 업데이트</div> </div> </div> </div> <!-- 오늘의 명언 --> <div class="card quote-card"> <h2 class="card-title">✨ 오늘의 명언</h2> <div class="quote-text">"{{ quote.text }}"</div> <div class="quote-author">{{ quote.author }}</div> </div> <!-- 학습 진도 --> <div class="card"> <h2 class="card-title">📊 학습 진도</h2> <div class="chart-container"> <img src="{{ study_chart }}" alt="학습 진도 차트"> </div> <div class="stats-grid"> <div class="stat-item"> <div class="stat-number">{{ avg_progress }}%</div> <div class="stat-label">평균 진도</div> </div> <div class="stat-item"> <div class="stat-number">4</div> <div class="stat-label">학습 과목</div> </div> </div> </div> <!-- 기분 투표 --> <div class="card"> <h2 class="card-title">🎭 오늘의 기분</h2> <form method="POST"> <div class="mood-voting"> {% for mood in mood_votes.keys() %} <button type="submit" name="mood" value="{{ mood }}" class="mood-btn"> {{ mood }} <div style="font-size: 0.8em; color: #666;">{{ mood_votes[mood] }}</div> </button> {% endfor %} </div> </form> <div class="chart-container"> <img src="{{ mood_chart }}" alt="기분 투표 차트" style="max-height: 200px;"> </div> <div class="stats-grid"> <div class="stat-item"> <div class="stat-number">{{ total_votes }}</div> <div class="stat-label">총 투표수</div> </div> <div class="stat-item"> <div class="stat-number">5</div> <div class="stat-label">기분 종류</div> </div> </div> </div> <!-- 할 일 목록 --> <div class="card"> <h2 class="card-title">📝 할 일 목록</h2> <form method="POST" class="todo-form"> <input type="text" name="todo_item" class="todo-input" placeholder="새로운 할 일을 입력하세요..." required> <button type="submit" class="add-btn">추가</button> </form> <ul class="todo-list"> {% for todo in todo_list %} <li class="todo-item {% if todo.done %}done{% endif %}"> <span class="todo-text">{{ todo.text }}</span> <div class="todo-actions"> <a href="/toggle_todo/{{ todo.id }}" class="btn-sm btn-toggle"> {% if todo.done %}취소{% else %}완료{% endif %} </a> <a href="/delete_todo/{{ todo.id }}" class="btn-sm btn-delete">삭제</a> </div> </li> {% endfor %} {% if not todo_list %} <li class="todo-item"> <span class="todo-text" style="color: #999; font-style: italic;">할 일이 없습니다. 새로운 할 일을 추가해보세요!</span> </li> {% endif %} </ul> </div> <!-- 설정 --> <div class="card"> <h2 class="card-title">⚙️ 설정</h2> <form method="POST" class="settings-form"> <input type="text" name="dashboard_name" class="settings-input" placeholder="대시보드 이름 변경" value="{{ dashboard_name }}"> <button type="submit" class="settings-btn">이름 변경</button> </form> <div class="stats-grid"> <div class="stat-item"> <div class="stat-number">6</div> <div class="stat-label">활성 위젯</div> </div> <div class="stat-item"> <div class="stat-number">{{ todo_list|length }}</div> <div class="stat-label">할 일 개수</div> </div> </div> </div> </div> <div class="footer"> <p>🎉 Python Flask로 만든 나만의 대시보드! Unit 11 완주를 축하해요! 🎉</p> </div> </body> </html>

💡 Flask 대시보드 아키텍처 이해

이 대시보드의 핵심 구조와 작동 방식:

기능Flask 구현
데이터 저장전역 dashboard_data 딕셔너리
데이터 처리Flask 라우트에서 처리
데이터 전달Jinja2 템플릿 변수 {{ variable }}
사용자 상호작용HTML 폼 POST 요청

🎯 도전 과제: 더 멋지게!

1. 실시간 업데이트 추가하기

# Flask에서 데이터 업데이트 간격 조절 @app.route('/refresh') def refresh_data(): # 데이터 새로고침 로직 dashboard_data['last_update'] = datetime.now().strftime('%H:%M:%S') return redirect(url_for('dashboard'))

2. 데이터 영구 저장

import json from datetime import datetime # 대시보드 데이터를 JSON 파일로 저장 def save_dashboard_data(): data_to_save = dashboard_data.copy() data_to_save['last_saved'] = datetime.now().isoformat() with open('entertainment_dashboard.json', 'w', encoding='utf-8') as f: json.dump(data_to_save, f, ensure_ascii=False, indent=2) # 데이터 불러오기 def load_dashboard_data(): try: with open('entertainment_dashboard.json', 'r', encoding='utf-8') as f: return json.load(f) except FileNotFoundError: return dashboard_data # 기본 데이터 반환

3. 엔터테인먼트 기능 확장하기

  • 🎥 영화 리뷰 시스템
  • 🎵 음악 추천 엔진
  • 📺 YouTube 트렌딩 영상
  • 🎮 게임 점수 추적기

💡 퀴즈: 대시보드 이해도 체크

Q1. Flask에서 전역 데이터를 저장하는 좋은 방법은?

  1. 파일에 저장
  2. 딕셔너리 변수 사용
  3. 데이터베이스 사용

💡 정답 확인

정답: 모두 맞음!

상황에 따라 다른 방법을 사용해요:

  • 개발/테스트: 딕셔너리 변수
  • 소규모: 파일 저장
  • 대규모: 데이터베이스

Q2. matplotlib 그래프를 웹에 표시하는 방법은?

  1. 파일로 저장 후 링크
  2. Base64로 인코딩
  3. 둘 다 가능

💡 정답 확인

정답: 3번

두 방법 모두 가능해요!

  • 파일 저장: 간단하지만 파일 관리 필요
  • Base64: 메모리 사용, 파일 관리 불필요

✅ Topic 4 마스터 체크리스트

🚀 Unit 11 완주 축하!

와! 정말 대단해요! 🎊

Unit 11을 완주하면서 여러분이 배운 것들:

✨ 기술 스택

  • Flask: 웹 프레임워크 마스터
  • HTML/CSS: 아름다운 UI 디자인
  • Python: 백엔드 로직 구현
  • Matplotlib: 데이터 시각화
  • 웹 스크래핑: 실시간 데이터 수집

🛠️ 만든 프로젝트들

  1. 애니메이션 웹사이트 (Topic 1)
  2. 인터랙티브 폼 시스템 (Topic 2)
  3. 실시간 엔터테인먼트 정보 앱 (Topic 3)
  4. 개인 맞춤형 엔터테인먼트 허브 (Topic 4)

🎯 개발자 역량

  • 풀스택 개발: 프론트엔드 + 백엔드
  • 데이터 시각화: 차트와 그래프
  • 웹 스크래핑: 실시간 엔터테인먼트 데이터 수집
  • 사용자 경험: 직관적이고 재미있는 인터페이스

🌟 다음 단계

이제 여러분은:

  • 포트폴리오에 넣을 멋진 엔터테인먼트 프로젝트가 있어요
  • 친구들에게 자랑할 수 있는 개인 맞춤형 허브를 만들었어요
  • 실제 회사에서 사용하는 기술들을 익혔어요
  • 어떤 웹앱이든 만들 수 있는 자신감이 생겼어요!

진짜 개발자가 되신 걸 축하드려요! 🚀✨

Last updated on