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에서 전역 데이터를 저장하는 좋은 방법은?
- 파일에 저장
- 딕셔너리 변수 사용
- 데이터베이스 사용
💡 정답 확인
정답: 모두 맞음!
상황에 따라 다른 방법을 사용해요:
- 개발/테스트: 딕셔너리 변수
- 소규모: 파일 저장
- 대규모: 데이터베이스
Q2. matplotlib 그래프를 웹에 표시하는 방법은?
- 파일로 저장 후 링크
- Base64로 인코딩
- 둘 다 가능
💡 정답 확인
정답: 3번
두 방법 모두 가능해요!
- 파일 저장: 간단하지만 파일 관리 필요
- Base64: 메모리 사용, 파일 관리 불필요
✅ Topic 4 마스터 체크리스트
🚀 Unit 11 완주 축하!
와! 정말 대단해요! 🎊
Unit 11을 완주하면서 여러분이 배운 것들:
✨ 기술 스택
- Flask: 웹 프레임워크 마스터
- HTML/CSS: 아름다운 UI 디자인
- Python: 백엔드 로직 구현
- Matplotlib: 데이터 시각화
- 웹 스크래핑: 실시간 데이터 수집
🛠️ 만든 프로젝트들
- 애니메이션 웹사이트 (Topic 1)
- 인터랙티브 폼 시스템 (Topic 2)
- 실시간 엔터테인먼트 정보 앱 (Topic 3)
- 개인 맞춤형 엔터테인먼트 허브 (Topic 4)
🎯 개발자 역량
- 풀스택 개발: 프론트엔드 + 백엔드
- 데이터 시각화: 차트와 그래프
- 웹 스크래핑: 실시간 엔터테인먼트 데이터 수집
- 사용자 경험: 직관적이고 재미있는 인터페이스
🌟 다음 단계
이제 여러분은:
- 포트폴리오에 넣을 멋진 엔터테인먼트 프로젝트가 있어요
- 친구들에게 자랑할 수 있는 개인 맞춤형 허브를 만들었어요
- 실제 회사에서 사용하는 기술들을 익혔어요
- 어떤 웹앱이든 만들 수 있는 자신감이 생겼어요!
진짜 개발자가 되신 걸 축하드려요! 🚀✨
Last updated on