#!/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()