mingle-website/server.py
68893236+KINDNICK@users.noreply.github.com 5d2ff03d79 Add: 파트너 스트리머 모집 페이지 추가
- 파트너 스트리머 모집 페이지 (partner.html) 추가
- 선발 로드맵 섹션 구현 (2026.01.10 ~ 2026.12.31)
- 지원 조건 및 혜택 안내
- 지원서 양식 템플릿 및 복사 기능
- 이메일 클릭 시 클립보드 복사 기능
- 반응형 디자인 적용 (모바일 최적화)

Update: 팝업 시스템 개선
- index.html 팝업 이미지 및 링크 업데이트
- 쿠키 기반 "오늘 하루 보지 않기" 기능

Update: 네비게이션 메뉴
- 헤더에 Partner 링크 추가

Update: 서버 설정
- Caddy 리버스 프록시 설정 추가 (HTTPS 지원)
- Python 서버 HTTP 모드 강제 설정

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-10 02:04:29 +09:00

385 lines
15 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
밍글 스튜디오 웹사이트 개발 서버
Python 기반 HTTP 서버로 개발, 테스트 및 API 지원
"""
import http.server
import socketserver
import webbrowser
import os
import sys
import socket
import json
from pathlib import Path
from datetime import datetime
from urllib.parse import parse_qs, urlparse
# 한글 출력을 위한 인코딩 설정
if os.name == 'nt': # Windows
import locale
import codecs
# UTF-8 강제 설정
try:
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
except:
# Python 3.6 이하 버전 호환
sys.stdout = codecs.getwriter('utf-8')(sys.stdout.detach())
sys.stderr = codecs.getwriter('utf-8')(sys.stderr.detach())
# 서버 설정
PORT = 8001
HOST = '0.0.0.0' # 모든 인터페이스에서 접근 가능
class CustomHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
"""커스텀 HTTP 요청 핸들러"""
# API 데이터 파일 경로
BACKGROUNDS_DATA_FILE = 'data/backgrounds.json'
PROPS_DATA_FILE = 'data/props.json'
def end_headers(self):
# CORS 헤더 추가 (개발용)
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Content-Type, X-API-Key')
# 캐시 비활성화 (개발용)
self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate')
self.send_header('Pragma', 'no-cache')
self.send_header('Expires', '0')
super().end_headers()
def do_OPTIONS(self):
"""OPTIONS 요청 처리 (CORS preflight)"""
self.send_response(200)
self.end_headers()
def do_GET(self):
"""GET 요청 처리"""
parsed_path = urlparse(self.path)
# API 엔드포인트: GET /api/backgrounds
if parsed_path.path == '/api/backgrounds':
self.handle_get_backgrounds()
return
# API 엔드포인트: GET /api/props
if parsed_path.path == '/api/props':
self.handle_get_props()
return
# 기본 파일 처리
if self.path == '/':
self.path = '/index.html'
# .html 확장자 없이 접근 시 자동으로 .html 추가
elif not self.path.endswith('/') and '.' not in os.path.basename(self.path):
html_path = self.path + '.html'
if os.path.exists(html_path.lstrip('/')):
self.path = html_path
super().do_GET()
def do_POST(self):
"""POST 요청 처리"""
parsed_path = urlparse(self.path)
# API 엔드포인트: POST /api/backgrounds
if parsed_path.path == '/api/backgrounds':
self.handle_post_backgrounds()
return
# API 엔드포인트: POST /api/props
if parsed_path.path == '/api/props':
self.handle_post_props()
return
# 그 외 POST 요청은 405 반환
self.send_error(405, 'Method Not Allowed')
def handle_get_backgrounds(self):
"""배경 데이터 조회 API"""
try:
if os.path.exists(self.BACKGROUNDS_DATA_FILE):
with open(self.BACKGROUNDS_DATA_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
else:
data = {
'lastUpdated': datetime.now().isoformat(),
'backgrounds': []
}
self.send_response(200)
self.send_header('Content-Type', 'application/json; charset=utf-8')
self.end_headers()
self.wfile.write(json.dumps(data, ensure_ascii=False, indent=2).encode('utf-8'))
except Exception as e:
self.send_error_json(500, f'데이터 조회 실패: {str(e)}')
def handle_post_backgrounds(self):
"""배경 데이터 업데이트 API"""
try:
# Content-Length 확인
content_length = int(self.headers.get('Content-Length', 0))
if content_length == 0:
self.send_error_json(400, '요청 본문이 비어있습니다')
return
# JSON 데이터 파싱
post_data = self.rfile.read(content_length)
try:
data = json.loads(post_data.decode('utf-8'))
except json.JSONDecodeError as e:
self.send_error_json(400, f'잘못된 JSON 형식: {str(e)}')
return
# 데이터 유효성 검사
if 'backgrounds' not in data:
self.send_error_json(400, 'backgrounds 필드가 필요합니다')
return
# lastUpdated 자동 설정
data['lastUpdated'] = datetime.now().isoformat()
# 데이터 디렉토리 확인 및 생성
data_dir = os.path.dirname(self.BACKGROUNDS_DATA_FILE)
if data_dir and not os.path.exists(data_dir):
os.makedirs(data_dir)
# 파일 저장
with open(self.BACKGROUNDS_DATA_FILE, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
# 성공 응답
response = {
'success': True,
'message': f'{len(data["backgrounds"])}개의 배경이 저장되었습니다',
'lastUpdated': data['lastUpdated'],
'count': len(data['backgrounds'])
}
self.send_response(200)
self.send_header('Content-Type', 'application/json; charset=utf-8')
self.end_headers()
self.wfile.write(json.dumps(response, ensure_ascii=False).encode('utf-8'))
print(f"[API] 배경 데이터 업데이트: {len(data['backgrounds'])}개 항목")
except Exception as e:
self.send_error_json(500, f'데이터 저장 실패: {str(e)}')
def handle_get_props(self):
"""프랍 데이터 조회 API"""
try:
if os.path.exists(self.PROPS_DATA_FILE):
with open(self.PROPS_DATA_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
else:
data = {
'lastUpdated': datetime.now().isoformat(),
'props': []
}
self.send_response(200)
self.send_header('Content-Type', 'application/json; charset=utf-8')
self.end_headers()
self.wfile.write(json.dumps(data, ensure_ascii=False, indent=2).encode('utf-8'))
except Exception as e:
self.send_error_json(500, f'데이터 조회 실패: {str(e)}')
def handle_post_props(self):
"""프랍 데이터 업데이트 API"""
try:
# Content-Length 확인
content_length = int(self.headers.get('Content-Length', 0))
if content_length == 0:
self.send_error_json(400, '요청 본문이 비어있습니다')
return
# JSON 데이터 파싱
post_data = self.rfile.read(content_length)
try:
data = json.loads(post_data.decode('utf-8'))
except json.JSONDecodeError as e:
self.send_error_json(400, f'잘못된 JSON 형식: {str(e)}')
return
# 데이터 유효성 검사
if 'props' not in data:
self.send_error_json(400, 'props 필드가 필요합니다')
return
# lastUpdated 자동 설정
data['lastUpdated'] = datetime.now().isoformat()
# 데이터 디렉토리 확인 및 생성
data_dir = os.path.dirname(self.PROPS_DATA_FILE)
if data_dir and not os.path.exists(data_dir):
os.makedirs(data_dir)
# 파일 저장
with open(self.PROPS_DATA_FILE, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
# 성공 응답
response = {
'success': True,
'message': f'{len(data["props"])}개의 프랍이 저장되었습니다',
'lastUpdated': data['lastUpdated'],
'count': len(data['props'])
}
self.send_response(200)
self.send_header('Content-Type', 'application/json; charset=utf-8')
self.end_headers()
self.wfile.write(json.dumps(response, ensure_ascii=False).encode('utf-8'))
print(f"[API] 프랍 데이터 업데이트: {len(data['props'])}개 항목")
except Exception as e:
self.send_error_json(500, f'데이터 저장 실패: {str(e)}')
def send_error_json(self, code, message):
"""JSON 형식 에러 응답"""
self.send_response(code)
self.send_header('Content-Type', 'application/json; charset=utf-8')
self.end_headers()
error_response = {
'success': False,
'error': message
}
self.wfile.write(json.dumps(error_response, ensure_ascii=False).encode('utf-8'))
def log_message(self, format, *args):
"""로그 메시지 포맷팅"""
print(f"[{self.log_date_time_string()}] {format % args}")
def find_available_port(start_port=8001):
"""사용 가능한 포트 찾기"""
for port in range(start_port, start_port + 20): # 더 많은 포트 시도
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 주소 재사용
sock.bind(('localhost', port)) # 실제로 바인드 테스트
sock.close()
print(f"Available port found: {port}")
return port # 성공하면 해당 포트 반환
except OSError:
continue # 포트가 사용 중이면 다음 포트 시도
print(f"No available port found in range {start_port}-{start_port+19}")
return start_port # 찾지 못하면 기본값 반환
def create_self_signed_cert():
"""자체 서명 인증서 생성"""
cert_file = 'server.crt'
key_file = 'server.key'
# 인증서가 이미 존재하면 재사용
if os.path.exists(cert_file) and os.path.exists(key_file):
print(f"Using existing SSL certificate: {cert_file}")
return cert_file, key_file
print("Generating self-signed SSL certificate...")
try:
import subprocess
# OpenSSL을 사용하여 자체 서명 인증서 생성
subprocess.run([
'openssl', 'req', '-x509', '-newkey', 'rsa:4096',
'-keyout', key_file, '-out', cert_file,
'-days', '365', '-nodes',
'-subj', '/CN=localhost'
], check=True, capture_output=True)
print(f"SSL certificate created: {cert_file}, {key_file}")
return cert_file, key_file
except (subprocess.CalledProcessError, FileNotFoundError):
print("Warning: OpenSSL not found. Cannot create SSL certificate.")
print("Install OpenSSL or use the caddy server for HTTPS.")
return None, None
def main():
"""메인 서버 실행 함수"""
# 현재 디렉토리가 프로젝트 루트인지 확인
if not os.path.exists('index.html'):
print("Error: index.html file not found.")
print("Please run from project root directory.")
sys.exit(1)
# 사용 가능한 포트 찾기
available_port = find_available_port(PORT)
# Force HTTP mode (Caddy handles HTTPS)
use_https = False
protocol = "http"
try:
# 서버 시작
httpd = socketserver.TCPServer((HOST, available_port), CustomHTTPRequestHandler)
httpd.allow_reuse_address = True # 주소 재사용 허용
with httpd:
# 로컬 및 외부 접근 주소 표시
import socket
local_ip = socket.gethostbyname(socket.gethostname())
print("Mingle Studio Development Server Started!")
print("="*60)
print(f"Local Access: {protocol}://localhost:{available_port}")
print(f"Network Access: {protocol}://{local_ip}:{available_port}")
print(f"Root Directory: {os.getcwd()}")
if use_https:
print("⚠️ Using self-signed certificate - browser will show security warning")
print(" Click 'Advanced''Proceed' to continue")
print("="*60)
print("Main Pages:")
print(f" Home: {protocol}://localhost:{available_port}/")
print(f" About: {protocol}://localhost:{available_port}/about")
print(f" Services: {protocol}://localhost:{available_port}/services")
print(f" Portfolio: {protocol}://localhost:{available_port}/portfolio")
print(f" Gallery: {protocol}://localhost:{available_port}/gallery")
print(f" Contact: {protocol}://localhost:{available_port}/contact")
print(f" Q&A: {protocol}://localhost:{available_port}/qna")
print(f" Backgrounds: {protocol}://localhost:{available_port}/backgrounds")
print(f" Props: {protocol}://localhost:{available_port}/props")
print(f" Partner: {protocol}://localhost:{available_port}/partner")
print("="*60)
print("API Endpoints:")
print(f" GET /api/backgrounds - 배경 데이터 조회")
print(f" POST /api/backgrounds - 배경 데이터 업데이트")
print(f" GET /api/props - 프랍 데이터 조회")
print(f" POST /api/props - 프랍 데이터 업데이트")
print("="*60)
print("External Access Setup:")
print(f" 1. Setup port forwarding for port {available_port} on router")
print(f" 2. Access via public IP: {protocol}://[PUBLIC_IP]:{available_port}")
print(f" 3. Same network: {protocol}://{local_ip}:{available_port}")
print("="*60)
print("Tip: Press Ctrl+C to stop the server.")
print("Opening browser automatically...")
# 기본 브라우저에서 자동으로 열기 (로컬호스트로)
webbrowser.open(f"{protocol}://localhost:{available_port}")
# 서버 실행
httpd.serve_forever()
except KeyboardInterrupt:
print("\n\nServer stopped.")
print("Goodbye!")
except OSError as e:
if e.errno == 10048: # WinError 10048
print(f"\nError: Port {available_port} is already in use.")
print("Try different port or check the following:")
print("1. Check if another server is running")
print("2. Kill Python process in Task Manager")
print(f"3. Run in command prompt: netstat -ano | findstr :{available_port}")
else:
print(f"Server error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()