feat: 허브 홈페이지 구축 및 URL 구조 개편 (2026-05)

- hub/ 신규 추가: cyanburu.com/ 허브 홈페이지 (에디토리얼+사이버펑크 디자인)
- hub/index.html: /portal/api/homepage/cards 동적 카드 로딩
- k8s/00-hub.yaml: hub 네임스페이스 + Deployment + Service
- k8s/13-ingress-hub.yaml: cyanburu.com/ → hub 라우팅
- k8s/08-ingress.yaml: cyanburu.com/ → cyanburu.com/portal 경로 변경
- backend/main.py: homepage_cards CRUD API 추가, root_path=/portal 설정
- frontend/index.html: API 경로 /portal/api 수정, 홈 카드 관리 탭 추가
- README.md: 2026-05 변경 이력 추가
This commit is contained in:
qorgh529
2026-06-10 18:18:53 +09:00
parent 72689d8647
commit 54877f8e96
7 changed files with 1019 additions and 17 deletions

View File

@@ -210,6 +210,7 @@
<button class="nav-btn admin-only" onclick="showPage('admin-pages')" style="display:none">⚙️ 페이지 관리</button>
<button class="nav-btn admin-only" onclick="showPage('admin-users')" style="display:none">👥 사용자 관리</button>
<button class="nav-btn admin-only" onclick="showPage('admin-notify')" style="display:none">🔔 알림 설정</button>
<button class="nav-btn admin-only" onclick="showPage('admin-hub-cards')" style="display:none">🏠 홈 카드 관리</button>
</nav>
<main>
@@ -447,10 +448,60 @@
</div>
</div>
<div id="page-admin-hub-cards" class="page">
<div class="page-header">
<div><h2>🏠 홈 카드 관리</h2><p>cyanburu.com 메인 허브 홈페이지에 표시될 카드를 관리합니다.</p></div>
<button class="btn btn-sm" onclick="openHubCardModal()">+ 카드 추가</button>
</div>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th style="width:50px">순서</th>
<th>제목</th>
<th>부제목</th>
<th>URL</th>
<th style="width:70px">태그</th>
<th style="width:70px">공개</th>
<th style="width:140px">관리</th>
</tr>
</thead>
<tbody id="hub-cards-table-body"></tbody>
</table>
</div>
</div>
</main>
</div>
<!-- MODALS -->
<div id="hub-card-modal" class="modal-overlay">
<div class="modal">
<h3 id="hub-card-modal-title">카드 추가</h3>
<div class="form-group"><label>제목 *</label><input type="text" id="hcm-title" placeholder="예: King's Cup"/></div>
<div class="form-group"><label>부제목</label><input type="text" id="hcm-subtitle" placeholder="예: 멀티플레이 술게임"/></div>
<div class="form-group"><label>설명</label><textarea id="hcm-desc" style="width:100%;padding:10px;border:2px solid var(--border);border-radius:8px;resize:vertical;min-height:80px;font-family:inherit;font-size:.9rem" placeholder="카드 설명을 입력하세요"></textarea></div>
<div class="form-group"><label>URL *</label><input type="text" id="hcm-url" placeholder="예: /kingscup 또는 https://gitea.cyanburu.com"/></div>
<div class="form-group">
<label>태그</label>
<select id="hcm-tag" style="width:100%;padding:10px;border:2px solid var(--border);border-radius:8px;font-size:.9rem">
<option value="LIVE">LIVE</option>
<option value="NEW">NEW</option>
<option value="WIP">WIP</option>
<option value="DEV">DEV</option>
</select>
</div>
<div class="form-group"><label>정렬 순서 (숫자가 작을수록 앞에 표시)</label><input type="number" id="hcm-sort" value="0" min="0" style="width:100%;padding:10px;border:2px solid var(--border);border-radius:8px;font-size:.9rem"/></div>
<div class="form-group" style="display:flex;align-items:center;gap:10px">
<input type="checkbox" id="hcm-visible" checked style="width:16px;height:16px;accent-color:var(--primary)"/>
<label for="hcm-visible" style="margin:0;font-size:.9rem">홈페이지에 공개</label>
</div>
<div class="modal-footer">
<button class="btn btn-sm btn-gray" onclick="closeModal('hub-card-modal')">취소</button>
<button class="btn btn-sm" onclick="saveHubCard()">저장</button>
</div>
</div>
</div>
<div id="page-modal" class="modal-overlay">
<div class="modal">
<h3 id="page-modal-title">페이지 추가</h3>
@@ -512,7 +563,7 @@
</div>
<script>
const API='/api';
const API='/portal/api';
let token=localStorage.getItem('portal_token');
let currentUser=null;
let editingPageId=null;
@@ -539,6 +590,7 @@ function showPage(name){
if(name==='admin-pages')loadAdminPages();
if(name==='admin-users')loadAdminUsers();
if(name==='admin-notify')loadNotifyConfig();
if(name==='admin-hub-cards')loadHubCards();
}
function closeModal(id){document.getElementById(id).classList.remove('open');}
@@ -1043,6 +1095,8 @@ function renderNotifyChannelList(){
<button class="btn btn-sm btn-danger" style="padding:5px 10px;" onclick="event.stopPropagation();deleteNotifyChannel(${ch.id})">🗑️</button>
</div>
</div>
<!-- 홈 카드 관리 (관리자) -->
<!-- 홈 카드 추가/수정 모달 -->
`).join('');
}
@@ -1149,6 +1203,73 @@ function fmtDate(s){
}catch{doLogout();}
}
})();
// ── 홈 카드 관리 ──────────────────────────────────────────────────────────
let editingHubCardId = null;
async function loadHubCards() {
const cards = await apiFetch('/admin/homepage-cards');
const tbody = document.getElementById('hub-cards-table-body');
if (!cards || cards.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;color:var(--muted);padding:40px">등록된 카드가 없습니다.</td></tr>';
return;
}
tbody.innerHTML = cards.map(c => `
<tr>
<td style="text-align:center;color:var(--muted)">${c.sort_order}</td>
<td><strong>${escHtml(c.title)}</strong></td>
<td style="color:var(--muted)">${escHtml(c.subtitle || '-')}</td>
<td><a href="${escHtml(c.url)}" target="_blank" style="color:var(--primary);font-size:.8rem">${escHtml(c.url)}</a></td>
<td><span class="tag ${c.tag === 'LIVE' ? 'tag-ok' : c.tag === 'NEW' ? 'tag-warning' : 'tag-user'}">${escHtml(c.tag || '')}</span></td>
<td style="text-align:center">${c.visible ? '✅' : '🔒'}</td>
<td><div class="actions">
<button class="btn btn-sm btn-gray" onclick="openHubCardModal(${c.id},'${escJs(c.title)}','${escJs(c.subtitle||'')}','${escJs(c.description||'')}','${escJs(c.url)}','${escJs(c.tag||'LIVE')}',${c.sort_order},${c.visible})">수정</button>
<button class="btn btn-sm btn-danger" onclick="deleteHubCard(${c.id})">삭제</button>
</div></td>
</tr>`).join('');
}
function openHubCardModal(id, title, subtitle, desc, url, tag, sort, visible) {
editingHubCardId = id || null;
document.getElementById('hub-card-modal-title').textContent = id ? '카드 수정' : '카드 추가';
document.getElementById('hcm-title').value = title || '';
document.getElementById('hcm-subtitle').value = subtitle || '';
document.getElementById('hcm-desc').value = desc || '';
document.getElementById('hcm-url').value = url || '';
document.getElementById('hcm-tag').value = tag || 'LIVE';
document.getElementById('hcm-sort').value = sort !== undefined ? sort : 0;
document.getElementById('hcm-visible').checked = visible !== undefined ? visible : true;
openModal('hub-card-modal');
}
async function saveHubCard() {
const body = {
title: document.getElementById('hcm-title').value.trim(),
subtitle: document.getElementById('hcm-subtitle').value.trim(),
description: document.getElementById('hcm-desc').value.trim(),
url: document.getElementById('hcm-url').value.trim(),
tag: document.getElementById('hcm-tag').value,
sort_order: parseInt(document.getElementById('hcm-sort').value) || 0,
visible: document.getElementById('hcm-visible').checked,
};
if (!body.title || !body.url) { alert('제목과 URL은 필수입니다.'); return; }
try {
if (editingHubCardId) {
await apiFetch('/admin/homepage-cards/' + editingHubCardId, { method: 'PUT', body: JSON.stringify(body) });
} else {
await apiFetch('/admin/homepage-cards', { method: 'POST', body: JSON.stringify(body) });
}
closeModal('hub-card-modal');
loadHubCards();
} catch(e) { alert(e.message); }
}
async function deleteHubCard(id) {
if (!confirm('이 카드를 삭제하시겠습니까?')) return;
try {
await apiFetch('/admin/homepage-cards/' + id, { method: 'DELETE' });
loadHubCards();
} catch(e) { alert(e.message); }
}
</script>
</body>
</html>