ADD : 배경 일람 페이지 추가
This commit is contained in:
parent
7bbdb45202
commit
f368cb2f6a
@ -20,7 +20,10 @@
|
|||||||
"Bash(ipconfig)",
|
"Bash(ipconfig)",
|
||||||
"Bash(ping:*)",
|
"Bash(ping:*)",
|
||||||
"Bash(dir:*)",
|
"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 <noreply@anthropic.com>\nEOF\n\\)\")",
|
||||||
|
"Bash(git push)"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|||||||
126
backgrounds.html
Normal file
126
backgrounds.html
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>배경 씬 라이브러리 - 밍글 스튜디오</title>
|
||||||
|
|
||||||
|
<!-- 파비콘 -->
|
||||||
|
<link rel="icon" type="image/x-icon" href="/mingle-logo.ico">
|
||||||
|
<link rel="shortcut icon" href="/mingle-logo.ico">
|
||||||
|
|
||||||
|
<!-- Theme Color -->
|
||||||
|
<meta name="theme-color" content="#ff8800">
|
||||||
|
|
||||||
|
<!-- SEO -->
|
||||||
|
<meta name="description" content="밍글 스튜디오 스트리밍글 서비스용 배경 씬 라이브러리">
|
||||||
|
<meta name="robots" content="noindex, nofollow">
|
||||||
|
|
||||||
|
<!-- 폰트 -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;700&display=swap" rel="stylesheet">
|
||||||
|
<link href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- CSS -->
|
||||||
|
<link rel="stylesheet" href="css/common.css">
|
||||||
|
<link rel="stylesheet" href="css/backgrounds.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- 헤더 -->
|
||||||
|
<div id="header-placeholder"></div>
|
||||||
|
|
||||||
|
<!-- 메인 콘텐츠 -->
|
||||||
|
<main class="backgrounds-page">
|
||||||
|
<div class="container">
|
||||||
|
<!-- 페이지 헤더 -->
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>배경 씬 라이브러리</h1>
|
||||||
|
<p class="page-description">스트리밍글 서비스에서 사용 가능한 배경 씬 목록입니다</p>
|
||||||
|
<div class="last-updated" id="lastUpdated"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 필터 & 검색 -->
|
||||||
|
<div class="filter-section">
|
||||||
|
<div class="search-box">
|
||||||
|
<input type="text" id="searchInput" placeholder="배경 이름으로 검색..." class="search-input">
|
||||||
|
<span class="search-icon">🔍</span>
|
||||||
|
</div>
|
||||||
|
<div class="filter-tags" id="filterTags">
|
||||||
|
<button class="filter-tag active" data-tag="all">전체</button>
|
||||||
|
<!-- 태그 버튼들이 동적으로 추가됨 -->
|
||||||
|
</div>
|
||||||
|
<div class="view-options">
|
||||||
|
<button class="view-btn active" data-view="grid" title="그리드 보기">
|
||||||
|
<span>▦</span>
|
||||||
|
</button>
|
||||||
|
<button class="view-btn" data-view="list" title="리스트 보기">
|
||||||
|
<span>☰</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 통계 -->
|
||||||
|
<div class="stats-bar">
|
||||||
|
<span class="stat-item">
|
||||||
|
<strong id="totalCount">0</strong>개 배경
|
||||||
|
</span>
|
||||||
|
<span class="stat-item">
|
||||||
|
<strong id="filteredCount">0</strong>개 표시 중
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 배경 그리드 -->
|
||||||
|
<div class="backgrounds-grid" id="backgroundsGrid">
|
||||||
|
<!-- 배경 카드들이 동적으로 추가됨 -->
|
||||||
|
<div class="loading-placeholder">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
<p>배경 데이터를 불러오는 중...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 데이터 없음 메시지 -->
|
||||||
|
<div class="no-data" id="noData" style="display: none;">
|
||||||
|
<div class="no-data-icon">📭</div>
|
||||||
|
<h3>배경 데이터가 없습니다</h3>
|
||||||
|
<p>Unity에서 배경 데이터를 업로드해 주세요.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 검색 결과 없음 -->
|
||||||
|
<div class="no-results" id="noResults" style="display: none;">
|
||||||
|
<div class="no-results-icon">🔍</div>
|
||||||
|
<h3>검색 결과가 없습니다</h3>
|
||||||
|
<p>다른 검색어나 필터를 시도해 보세요.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- 이미지 모달 -->
|
||||||
|
<div class="image-modal" id="imageModal">
|
||||||
|
<div class="modal-overlay"></div>
|
||||||
|
<div class="modal-content">
|
||||||
|
<button class="modal-close">×</button>
|
||||||
|
<img src="" alt="" class="modal-image" id="modalImage">
|
||||||
|
<div class="modal-info">
|
||||||
|
<h3 id="modalTitle"></h3>
|
||||||
|
<p id="modalCategory"></p>
|
||||||
|
<div class="modal-tags" id="modalTags"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 푸터 -->
|
||||||
|
<div id="footer-placeholder"></div>
|
||||||
|
|
||||||
|
<!-- 백업 푸터 -->
|
||||||
|
<footer class="section" style="background:#222;color:#fff;padding:2.5rem 0 1.2rem;">
|
||||||
|
<div class="container" style="text-align:center;">
|
||||||
|
<div style="color:#bbb;font-size:0.98rem;">© 2025 밍글 스튜디오. All rights reserved.</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- JavaScript -->
|
||||||
|
<script src="js/common.js"></script>
|
||||||
|
<script src="js/backgrounds.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
441
css/backgrounds.css
Normal file
441
css/backgrounds.css
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
4
data/backgrounds.json
Normal file
4
data/backgrounds.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"lastUpdated": "2025-01-08T12:00:00",
|
||||||
|
"backgrounds": []
|
||||||
|
}
|
||||||
349
js/backgrounds.js
Normal file
349
js/backgrounds.js
Normal file
@ -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
|
||||||
|
? `<img src="${escapeHtml(bg.thumbnailUrl)}" alt="${escapeHtml(bg.sceneName)}" loading="lazy">`
|
||||||
|
: `<div class="no-thumbnail">🏞️</div>`;
|
||||||
|
|
||||||
|
const tagsHtml = (bg.tags || [])
|
||||||
|
.map(tag => `<span class="card-tag">${escapeHtml(tag)}</span>`)
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
const category = bg.category || extractCategory(bg.categoryName);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="background-card" data-scene="${escapeHtml(bg.sceneName)}">
|
||||||
|
<div class="card-thumbnail">
|
||||||
|
${thumbnailHtml}
|
||||||
|
<span class="card-category">${escapeHtml(category)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<h3 class="card-title">${escapeHtml(bg.sceneName)}</h3>
|
||||||
|
<div class="card-tags">${tagsHtml}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 뷰 모드 업데이트
|
||||||
|
*/
|
||||||
|
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 => `<span class="card-tag">${escapeHtml(tag)}</span>`)
|
||||||
|
.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);
|
||||||
|
})();
|
||||||
136
server.py
136
server.py
@ -1,8 +1,8 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
밍글 스튜디오 웹사이트 로컬 개발 서버
|
밍글 스튜디오 웹사이트 개발 서버
|
||||||
Python 기반 HTTP 서버로 개발 및 테스트 지원
|
Python 기반 HTTP 서버로 개발, 테스트 및 API 지원
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import http.server
|
import http.server
|
||||||
@ -11,7 +11,10 @@ import webbrowser
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import socket
|
import socket
|
||||||
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
from urllib.parse import parse_qs, urlparse
|
||||||
|
|
||||||
# 한글 출력을 위한 인코딩 설정
|
# 한글 출력을 위한 인코딩 설정
|
||||||
if os.name == 'nt': # Windows
|
if os.name == 'nt': # Windows
|
||||||
@ -32,34 +35,142 @@ HOST = '0.0.0.0' # 모든 인터페이스에서 접근 가능
|
|||||||
|
|
||||||
class CustomHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
|
class CustomHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||||
"""커스텀 HTTP 요청 핸들러"""
|
"""커스텀 HTTP 요청 핸들러"""
|
||||||
|
|
||||||
|
# API 데이터 파일 경로
|
||||||
|
BACKGROUNDS_DATA_FILE = 'data/backgrounds.json'
|
||||||
|
|
||||||
def end_headers(self):
|
def end_headers(self):
|
||||||
# CORS 헤더 추가 (개발용)
|
# CORS 헤더 추가 (개발용)
|
||||||
self.send_header('Access-Control-Allow-Origin', '*')
|
self.send_header('Access-Control-Allow-Origin', '*')
|
||||||
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
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('Cache-Control', 'no-cache, no-store, must-revalidate')
|
||||||
self.send_header('Pragma', 'no-cache')
|
self.send_header('Pragma', 'no-cache')
|
||||||
self.send_header('Expires', '0')
|
self.send_header('Expires', '0')
|
||||||
super().end_headers()
|
super().end_headers()
|
||||||
|
|
||||||
|
def do_OPTIONS(self):
|
||||||
|
"""OPTIONS 요청 처리 (CORS preflight)"""
|
||||||
|
self.send_response(200)
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
"""GET 요청 처리"""
|
"""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 == '/':
|
if self.path == '/':
|
||||||
self.path = '/index.html'
|
self.path = '/index.html'
|
||||||
|
|
||||||
# .html 확장자 없이 접근 시 자동으로 .html 추가
|
# .html 확장자 없이 접근 시 자동으로 .html 추가
|
||||||
elif not self.path.endswith('/') and '.' not in os.path.basename(self.path):
|
elif not self.path.endswith('/') and '.' not in os.path.basename(self.path):
|
||||||
html_path = self.path + '.html'
|
html_path = self.path + '.html'
|
||||||
if os.path.exists(html_path.lstrip('/')):
|
if os.path.exists(html_path.lstrip('/')):
|
||||||
self.path = html_path
|
self.path = html_path
|
||||||
|
|
||||||
super().do_GET()
|
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):
|
def log_message(self, format, *args):
|
||||||
"""로그 메시지 포맷팅"""
|
"""로그 메시지 포맷팅"""
|
||||||
print(f"[{self.log_date_time_string()}] {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" Gallery: http://localhost:{available_port}/gallery")
|
||||||
print(f" Contact: http://localhost:{available_port}/contact")
|
print(f" Contact: http://localhost:{available_port}/contact")
|
||||||
print(f" Q&A: http://localhost:{available_port}/qna")
|
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("="*60)
|
||||||
print("External Access Setup:")
|
print("External Access Setup:")
|
||||||
print(f" 1. Setup port forwarding for port {available_port} on router")
|
print(f" 1. Setup port forwarding for port {available_port} on router")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user