From f368cb2f6a48e8f26f1a8ccb400ae6dfd210bd25 Mon Sep 17 00:00:00 2001 From: "68893236+KINDNICK@users.noreply.github.com" <68893236+KINDNICK@users.noreply.github.com> Date: Thu, 8 Jan 2026 02:02:24 +0900 Subject: [PATCH] =?UTF-8?q?ADD=20:=20=EB=B0=B0=EA=B2=BD=20=EC=9D=BC?= =?UTF-8?q?=EB=9E=8C=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 5 +- backgrounds.html | 126 +++++++++++ css/backgrounds.css | 441 ++++++++++++++++++++++++++++++++++++ data/backgrounds.json | 4 + js/backgrounds.js | 349 ++++++++++++++++++++++++++++ server.py | 136 ++++++++++- 6 files changed, 1050 insertions(+), 11 deletions(-) create mode 100644 backgrounds.html create mode 100644 css/backgrounds.css create mode 100644 data/backgrounds.json create mode 100644 js/backgrounds.js diff --git a/.claude/settings.local.json b/.claude/settings.local.json index f2181de..a3c4d5f 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -20,7 +20,10 @@ "Bash(ipconfig)", "Bash(ping:*)", "Bash(dir:*)", - "Bash(nul)" + "Bash(nul)", + "Bash(git add:*)", + "Bash(git commit -m \"$\\(cat <<''EOF''\nAdd : 포트폴리오 영상 추가 및 수정\n\n- Merry & Happy \\(트와이스\\) 치요x마늘 커버 영상 추가\n- 첫사랑 설명 수정\n- 이무지 생방송 링크 삭제\n- 크리스마스 모션캡쳐 합방 영상 추가\n- 춤짱자매즈 스트리머 이름 수정 \\(흰콕 & 호발\\)\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")", + "Bash(git push)" ], "deny": [], "ask": [] diff --git a/backgrounds.html b/backgrounds.html new file mode 100644 index 0000000..23be8f8 --- /dev/null +++ b/backgrounds.html @@ -0,0 +1,126 @@ + + + + + + 배경 씬 라이브러리 - 밍글 스튜디오 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + + +
+ +
+ + +
+
+ + +
+
+ + +
+ + 0개 배경 + + + 0개 표시 중 + +
+ + +
+ +
+
+

배경 데이터를 불러오는 중...

+
+
+ + + + + + +
+
+ + +
+ + +
+ + + + + + + + + + + + diff --git a/css/backgrounds.css b/css/backgrounds.css new file mode 100644 index 0000000..5fb70f7 --- /dev/null +++ b/css/backgrounds.css @@ -0,0 +1,441 @@ +/* ======================================== + 배경 씬 라이브러리 페이지 스타일 + ======================================== */ + +/* 페이지 헤더 */ +.backgrounds-page { + min-height: calc(100vh - var(--navbar-height)); + padding: var(--spacing-2xl) 0; +} + +.page-header { + text-align: center; + margin-bottom: var(--spacing-2xl); +} + +.page-header h1 { + font-size: var(--font-4xl); + font-weight: 700; + color: var(--text-primary); + margin-bottom: var(--spacing-sm); +} + +.page-description { + font-size: var(--font-lg); + color: var(--text-secondary); + margin-bottom: var(--spacing-md); +} + +.last-updated { + font-size: var(--font-sm); + color: var(--text-light); +} + +/* 필터 섹션 */ +.filter-section { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-md); + align-items: center; + margin-bottom: var(--spacing-lg); + padding: var(--spacing-lg); + background: var(--bg-white); + border-radius: var(--border-radius); + box-shadow: var(--box-shadow); +} + +.search-box { + position: relative; + flex: 1; + min-width: 250px; +} + +.search-input { + width: 100%; + padding: var(--spacing-md) var(--spacing-lg); + padding-left: 3rem; + border: 2px solid #eee; + border-radius: var(--border-radius-full); + font-size: var(--font-base); + transition: var(--transition); +} + +.search-input:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(255, 136, 0, 0.1); +} + +.search-icon { + position: absolute; + left: var(--spacing-md); + top: 50%; + transform: translateY(-50%); + font-size: 1.2rem; +} + +.filter-tags { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-sm); + flex: 2; +} + +.filter-tag { + padding: var(--spacing-sm) var(--spacing-md); + border: 2px solid #eee; + border-radius: var(--border-radius-full); + background: var(--bg-white); + cursor: pointer; + font-size: var(--font-sm); + font-weight: 500; + transition: var(--transition); +} + +.filter-tag:hover { + border-color: var(--primary-color); + color: var(--primary-color); +} + +.filter-tag.active { + background: var(--gradient-main); + color: white; + border-color: transparent; +} + +.view-options { + display: flex; + gap: var(--spacing-xs); +} + +.view-btn { + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + border: 2px solid #eee; + border-radius: var(--border-radius-sm); + background: var(--bg-white); + cursor: pointer; + font-size: 1.2rem; + transition: var(--transition); +} + +.view-btn:hover { + border-color: var(--primary-color); +} + +.view-btn.active { + background: var(--primary-color); + color: white; + border-color: transparent; +} + +/* 통계 바 */ +.stats-bar { + display: flex; + gap: var(--spacing-lg); + margin-bottom: var(--spacing-lg); + font-size: var(--font-sm); + color: var(--text-secondary); +} + +.stat-item strong { + color: var(--primary-color); + font-weight: 700; +} + +/* 배경 그리드 */ +.backgrounds-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: var(--spacing-lg); +} + +.backgrounds-grid.list-view { + grid-template-columns: 1fr; +} + +/* 배경 카드 */ +.background-card { + background: var(--bg-white); + border-radius: var(--border-radius); + overflow: hidden; + box-shadow: var(--box-shadow); + transition: var(--transition); + cursor: pointer; +} + +.background-card:hover { + transform: translateY(-5px); + box-shadow: var(--box-shadow-lg); +} + +.card-thumbnail { + position: relative; + aspect-ratio: 16 / 9; + overflow: hidden; + background: #f0f0f0; +} + +.card-thumbnail img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.5s ease; +} + +.background-card:hover .card-thumbnail img { + transform: scale(1.05); +} + +.card-thumbnail .no-thumbnail { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #e0e0e0 0%, #f5f5f5 100%); + color: var(--text-light); + font-size: 3rem; +} + +.card-category { + position: absolute; + top: var(--spacing-sm); + left: var(--spacing-sm); + padding: var(--spacing-xs) var(--spacing-sm); + background: rgba(0, 0, 0, 0.7); + color: white; + font-size: var(--font-xs); + font-weight: 500; + border-radius: var(--border-radius-sm); +} + +.card-content { + padding: var(--spacing-md); +} + +.card-title { + font-size: var(--font-lg); + font-weight: 600; + color: var(--text-primary); + margin-bottom: var(--spacing-sm); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.card-tags { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-xs); +} + +.card-tag { + padding: 2px 8px; + background: var(--bg-gray); + color: var(--text-secondary); + font-size: var(--font-xs); + border-radius: var(--border-radius-sm); +} + +/* 리스트 뷰 카드 */ +.backgrounds-grid.list-view .background-card { + display: flex; + flex-direction: row; +} + +.backgrounds-grid.list-view .card-thumbnail { + width: 200px; + min-width: 200px; + aspect-ratio: auto; + height: 112px; +} + +.backgrounds-grid.list-view .card-content { + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; +} + +/* 로딩 */ +.loading-placeholder { + grid-column: 1 / -1; + text-align: center; + padding: var(--spacing-3xl); + color: var(--text-secondary); +} + +.loading-spinner { + width: 50px; + height: 50px; + border: 4px solid #f0f0f0; + border-top-color: var(--primary-color); + border-radius: 50%; + animation: spin 1s linear infinite; + margin: 0 auto var(--spacing-md); +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* 데이터 없음 / 검색 결과 없음 */ +.no-data, +.no-results { + text-align: center; + padding: var(--spacing-3xl); + color: var(--text-secondary); +} + +.no-data-icon, +.no-results-icon { + font-size: 4rem; + margin-bottom: var(--spacing-md); +} + +.no-data h3, +.no-results h3 { + font-size: var(--font-2xl); + color: var(--text-primary); + margin-bottom: var(--spacing-sm); +} + +/* 모달 */ +.image-modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 2000; + display: none; + align-items: center; + justify-content: center; +} + +.image-modal.active { + display: flex; +} + +.modal-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.9); + cursor: pointer; +} + +.modal-content { + position: relative; + max-width: 90%; + max-height: 90%; + z-index: 1; +} + +.modal-close { + position: absolute; + top: -40px; + right: 0; + width: 40px; + height: 40px; + background: transparent; + border: none; + color: white; + font-size: 2rem; + cursor: pointer; + transition: var(--transition); +} + +.modal-close:hover { + color: var(--primary-color); +} + +.modal-image { + max-width: 100%; + max-height: 70vh; + border-radius: var(--border-radius); + box-shadow: var(--box-shadow-lg); +} + +.modal-info { + background: var(--bg-white); + padding: var(--spacing-lg); + border-radius: 0 0 var(--border-radius) var(--border-radius); + margin-top: -5px; +} + +.modal-info h3 { + font-size: var(--font-xl); + color: var(--text-primary); + margin-bottom: var(--spacing-xs); +} + +.modal-info p { + color: var(--text-secondary); + margin-bottom: var(--spacing-sm); +} + +.modal-tags { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-xs); +} + +.modal-tags .card-tag { + background: var(--primary-color); + color: white; +} + +/* 반응형 */ +@media (max-width: 768px) { + .page-header h1 { + font-size: var(--font-3xl); + } + + .filter-section { + flex-direction: column; + align-items: stretch; + } + + .search-box { + min-width: 100%; + } + + .filter-tags { + justify-content: center; + } + + .view-options { + justify-content: center; + } + + .backgrounds-grid { + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); + } + + .backgrounds-grid.list-view .background-card { + flex-direction: column; + } + + .backgrounds-grid.list-view .card-thumbnail { + width: 100%; + height: auto; + aspect-ratio: 16 / 9; + } + + .stats-bar { + justify-content: center; + } +} + +@media (max-width: 480px) { + .backgrounds-grid { + grid-template-columns: 1fr; + } +} diff --git a/data/backgrounds.json b/data/backgrounds.json new file mode 100644 index 0000000..330d01a --- /dev/null +++ b/data/backgrounds.json @@ -0,0 +1,4 @@ +{ + "lastUpdated": "2025-01-08T12:00:00", + "backgrounds": [] +} diff --git a/js/backgrounds.js b/js/backgrounds.js new file mode 100644 index 0000000..a14b8e7 --- /dev/null +++ b/js/backgrounds.js @@ -0,0 +1,349 @@ +/** + * 배경 씬 라이브러리 JavaScript + * Unity에서 업로드된 JSON 데이터를 기반으로 배경 목록을 표시 + */ + +(function() { + 'use strict'; + + // 데이터 파일 경로 + const DATA_PATH = 'data/backgrounds.json'; + + // 상태 + let backgroundsData = []; + let filteredData = []; + let currentTag = 'all'; + let currentView = 'grid'; + let searchQuery = ''; + + // DOM 요소 + const elements = { + grid: document.getElementById('backgroundsGrid'), + filterTags: document.getElementById('filterTags'), + searchInput: document.getElementById('searchInput'), + totalCount: document.getElementById('totalCount'), + filteredCount: document.getElementById('filteredCount'), + lastUpdated: document.getElementById('lastUpdated'), + noData: document.getElementById('noData'), + noResults: document.getElementById('noResults'), + imageModal: document.getElementById('imageModal'), + modalImage: document.getElementById('modalImage'), + modalTitle: document.getElementById('modalTitle'), + modalCategory: document.getElementById('modalCategory'), + modalTags: document.getElementById('modalTags') + }; + + /** + * 초기화 + */ + async function init() { + await loadData(); + setupEventListeners(); + } + + /** + * 데이터 로드 + */ + async function loadData() { + try { + const response = await fetch(DATA_PATH + '?t=' + Date.now()); + + if (!response.ok) { + throw new Error('데이터를 찾을 수 없습니다'); + } + + const data = await response.json(); + backgroundsData = data.backgrounds || []; + + // 마지막 업데이트 시간 표시 + if (data.lastUpdated) { + const date = new Date(data.lastUpdated); + elements.lastUpdated.textContent = `마지막 업데이트: ${formatDate(date)}`; + } + + // 태그 필터 생성 + createTagFilters(); + + // 통계 업데이트 + elements.totalCount.textContent = backgroundsData.length; + + // 데이터 렌더링 + filterAndRender(); + + } catch (error) { + console.error('데이터 로드 실패:', error); + showNoData(); + } + } + + /** + * 태그 필터 버튼 생성 + */ + function createTagFilters() { + // 모든 태그 수집 + const allTags = new Set(); + backgroundsData.forEach(bg => { + if (bg.tags && Array.isArray(bg.tags)) { + bg.tags.forEach(tag => allTags.add(tag)); + } + }); + + // 기존 태그 버튼 제거 (전체 버튼 제외) + const existingTags = elements.filterTags.querySelectorAll('.filter-tag:not([data-tag="all"])'); + existingTags.forEach(tag => tag.remove()); + + // 태그 버튼 생성 + allTags.forEach(tag => { + const btn = document.createElement('button'); + btn.className = 'filter-tag'; + btn.dataset.tag = tag; + btn.textContent = tag; + btn.addEventListener('click', () => setTagFilter(tag)); + elements.filterTags.appendChild(btn); + }); + } + + /** + * 이벤트 리스너 설정 + */ + function setupEventListeners() { + // 검색 + elements.searchInput.addEventListener('input', debounce((e) => { + searchQuery = e.target.value.toLowerCase(); + filterAndRender(); + }, 300)); + + // 전체 태그 필터 + const allTagBtn = elements.filterTags.querySelector('[data-tag="all"]'); + if (allTagBtn) { + allTagBtn.addEventListener('click', () => setTagFilter('all')); + } + + // 뷰 전환 + document.querySelectorAll('.view-btn').forEach(btn => { + btn.addEventListener('click', () => { + document.querySelectorAll('.view-btn').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + currentView = btn.dataset.view; + updateViewMode(); + }); + }); + + // 모달 닫기 + elements.imageModal.querySelector('.modal-overlay').addEventListener('click', closeModal); + elements.imageModal.querySelector('.modal-close').addEventListener('click', closeModal); + + // ESC 키로 모달 닫기 + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') closeModal(); + }); + } + + /** + * 태그 필터 설정 + */ + function setTagFilter(tag) { + currentTag = tag; + + // 버튼 활성화 상태 업데이트 + document.querySelectorAll('.filter-tag').forEach(btn => { + btn.classList.toggle('active', btn.dataset.tag === tag); + }); + + filterAndRender(); + } + + /** + * 필터링 및 렌더링 + */ + function filterAndRender() { + // 필터링 + filteredData = backgroundsData.filter(bg => { + // 태그 필터 + if (currentTag !== 'all') { + if (!bg.tags || !bg.tags.includes(currentTag)) { + return false; + } + } + + // 검색 필터 + if (searchQuery) { + const searchTarget = `${bg.sceneName} ${bg.categoryName} ${(bg.tags || []).join(' ')}`.toLowerCase(); + if (!searchTarget.includes(searchQuery)) { + return false; + } + } + + return true; + }); + + // 통계 업데이트 + elements.filteredCount.textContent = filteredData.length; + + // 렌더링 + render(); + } + + /** + * 카드 렌더링 + */ + function render() { + // 로딩/에러 상태 숨기기 + const loadingPlaceholder = elements.grid.querySelector('.loading-placeholder'); + if (loadingPlaceholder) { + loadingPlaceholder.remove(); + } + + elements.noData.style.display = 'none'; + elements.noResults.style.display = 'none'; + + // 데이터 없음 + if (backgroundsData.length === 0) { + elements.grid.innerHTML = ''; + showNoData(); + return; + } + + // 검색 결과 없음 + if (filteredData.length === 0) { + elements.grid.innerHTML = ''; + elements.noResults.style.display = 'block'; + return; + } + + // 카드 생성 + elements.grid.innerHTML = filteredData.map(bg => createCard(bg)).join(''); + + // 카드 클릭 이벤트 + elements.grid.querySelectorAll('.background-card').forEach((card, index) => { + card.addEventListener('click', () => openModal(filteredData[index])); + }); + + // 뷰 모드 적용 + updateViewMode(); + } + + /** + * 카드 HTML 생성 + */ + function createCard(bg) { + const thumbnailHtml = bg.thumbnailUrl + ? `${escapeHtml(bg.sceneName)}` + : `
🏞️
`; + + const tagsHtml = (bg.tags || []) + .map(tag => `${escapeHtml(tag)}`) + .join(''); + + const category = bg.category || extractCategory(bg.categoryName); + + return ` +
+
+ ${thumbnailHtml} + ${escapeHtml(category)} +
+
+

${escapeHtml(bg.sceneName)}

+
${tagsHtml}
+
+
+ `; + } + + /** + * 뷰 모드 업데이트 + */ + function updateViewMode() { + elements.grid.classList.toggle('list-view', currentView === 'list'); + } + + /** + * 모달 열기 + */ + function openModal(bg) { + if (bg.thumbnailUrl) { + elements.modalImage.src = bg.thumbnailUrl; + elements.modalImage.alt = bg.sceneName; + } else { + elements.modalImage.src = ''; + elements.modalImage.alt = ''; + } + + elements.modalTitle.textContent = bg.sceneName; + elements.modalCategory.textContent = bg.categoryName; + + elements.modalTags.innerHTML = (bg.tags || []) + .map(tag => `${escapeHtml(tag)}`) + .join(''); + + elements.imageModal.classList.add('active'); + document.body.style.overflow = 'hidden'; + } + + /** + * 모달 닫기 + */ + function closeModal() { + elements.imageModal.classList.remove('active'); + document.body.style.overflow = ''; + } + + /** + * 데이터 없음 표시 + */ + function showNoData() { + elements.grid.innerHTML = ''; + elements.noData.style.display = 'block'; + } + + /** + * 카테고리 추출 (폴더명에서) + */ + function extractCategory(folderName) { + if (!folderName) return '기타'; + const match = folderName.match(/\[([^\]]+)\]/); + return match ? match[1] : folderName; + } + + /** + * 날짜 포맷 + */ + function formatDate(date) { + return date.toLocaleString('ko-KR', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + }); + } + + /** + * HTML 이스케이프 + */ + function escapeHtml(str) { + if (!str) return ''; + const div = document.createElement('div'); + div.textContent = str; + return div.innerHTML; + } + + /** + * 디바운스 + */ + function debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; + } + + // 페이지 로드 시 초기화 + document.addEventListener('DOMContentLoaded', init); +})(); diff --git a/server.py b/server.py index fd97b38..8aa2f44 100644 --- a/server.py +++ b/server.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ -밍글 스튜디오 웹사이트 로컬 개발 서버 -Python 기반 HTTP 서버로 개발 및 테스트 지원 +밍글 스튜디오 웹사이트 개발 서버 +Python 기반 HTTP 서버로 개발, 테스트 및 API 지원 """ import http.server @@ -11,7 +11,10 @@ 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 @@ -32,34 +35,142 @@ HOST = '0.0.0.0' # 모든 인터페이스에서 접근 가능 class CustomHTTPRequestHandler(http.server.SimpleHTTPRequestHandler): """커스텀 HTTP 요청 핸들러""" - + + # API 데이터 파일 경로 + BACKGROUNDS_DATA_FILE = 'data/backgrounds.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') + 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 요청 처리""" - original_path = self.path - + parsed_path = urlparse(self.path) + + # API 엔드포인트: GET /api/backgrounds + if parsed_path.path == '/api/backgrounds': + self.handle_get_backgrounds() + 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 + + # 그 외 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 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}") @@ -114,6 +225,11 @@ def main(): print(f" Gallery: http://localhost:{available_port}/gallery") print(f" Contact: http://localhost:{available_port}/contact") print(f" Q&A: http://localhost:{available_port}/qna") + print(f" Backgrounds: http://localhost:{available_port}/backgrounds") + print("="*60) + print("API Endpoints:") + print(f" GET /api/backgrounds - 배경 데이터 조회") + print(f" POST /api/backgrounds - 배경 데이터 업데이트") print("="*60) print("External Access Setup:") print(f" 1. Setup port forwarding for port {available_port} on router")