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:
133
backend/main.py
133
backend/main.py
@@ -802,3 +802,136 @@ def delete_notice_reply(reply_id: int, token=Depends(verify_token), conn=Depends
|
||||
cur.execute("DELETE FROM notice_replies WHERE id = %s", (reply_id,))
|
||||
conn.commit()
|
||||
return {"ok": True}
|
||||
|
||||
# ─── Homepage Cards ──────────────────────────────────────────────────────────
|
||||
# 이 코드를 기존 main.py 맨 아래에 붙여넣으세요.
|
||||
# init_db() 함수 안의 cur.execute("""...""") 블록에도 아래 테이블을 추가해야 합니다.
|
||||
# (init_db 수정 방법은 아래 주석 참고)
|
||||
#
|
||||
# [init_db() 수정] 기존 CREATE TABLE ... 블록 마지막에 추가:
|
||||
#
|
||||
# CREATE TABLE IF NOT EXISTS homepage_cards (
|
||||
# id SERIAL PRIMARY KEY,
|
||||
# title VARCHAR(100) NOT NULL,
|
||||
# subtitle VARCHAR(200),
|
||||
# description TEXT,
|
||||
# url VARCHAR(500) NOT NULL,
|
||||
# tag VARCHAR(20) DEFAULT 'LIVE',
|
||||
# sort_order INTEGER DEFAULT 0,
|
||||
# visible BOOLEAN DEFAULT TRUE,
|
||||
# created_at TIMESTAMP DEFAULT NOW()
|
||||
# );
|
||||
#
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def init_homepage_cards_db():
|
||||
conn = psycopg2.connect(**DB_CONFIG)
|
||||
cur = conn.cursor()
|
||||
cur.execute("""
|
||||
CREATE TABLE IF NOT EXISTS homepage_cards (
|
||||
id SERIAL PRIMARY KEY,
|
||||
title VARCHAR(100) NOT NULL,
|
||||
subtitle VARCHAR(200),
|
||||
description TEXT,
|
||||
url VARCHAR(500) NOT NULL,
|
||||
tag VARCHAR(20) DEFAULT 'LIVE',
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
visible BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
""")
|
||||
# 기본 카드 초기 데이터 (최초 1회만)
|
||||
cur.execute("SELECT COUNT(*) FROM homepage_cards")
|
||||
count = cur.fetchone()[0]
|
||||
if count == 0:
|
||||
default_cards = [
|
||||
("Web Portal", "내부 서비스 관리", "로그인 후 할당된 웹 서비스 목록을 확인하고 접속할 수 있는 통합 포털.", "/portal", "LIVE", 1),
|
||||
("King's Cup", "멀티플레이 술게임", "디스코드 화면공유로 친구들과 함께 즐기는 킹컵 카드 게임. 실시간 WebSocket 기반.", "/kingscup", "NEW", 2),
|
||||
("Gitea", "셀프호스티드 Git", "GitHub 대신 자체 서버에서 운영하는 Git 저장소. Container Registry 포함.", "https://gitea.cyanburu.com", "LIVE", 3),
|
||||
("ArgoCD", "GitOps 배포 엔진", "Gitea 저장소를 감시해 K8s 클러스터에 자동으로 배포하는 GitOps 도구.", "https://argo.cyanburu.com", "LIVE", 4),
|
||||
]
|
||||
cur.executemany(
|
||||
"INSERT INTO homepage_cards (title, subtitle, description, url, tag, sort_order) VALUES (%s,%s,%s,%s,%s,%s)",
|
||||
default_cards
|
||||
)
|
||||
conn.commit()
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
@app.on_event("startup")
|
||||
def startup_homepage_cards():
|
||||
import time
|
||||
for _ in range(10):
|
||||
try:
|
||||
init_homepage_cards_db()
|
||||
print("Homepage cards DB initialized")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"Homepage cards DB not ready... {e}")
|
||||
time.sleep(3)
|
||||
|
||||
# ── Public API (인증 불필요 - 허브 홈페이지에서 호출) ──────────────────────
|
||||
@app.get("/api/homepage/cards")
|
||||
def get_homepage_cards(conn=Depends(get_db)):
|
||||
"""허브 홈페이지(cyanburu.com/)에서 카드 목록을 가져옵니다. 인증 불필요."""
|
||||
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
||||
cur.execute("""
|
||||
SELECT id, title, subtitle, description, url, tag, sort_order
|
||||
FROM homepage_cards
|
||||
WHERE visible = TRUE
|
||||
ORDER BY sort_order ASC, id ASC
|
||||
""")
|
||||
return cur.fetchall()
|
||||
|
||||
# ── Admin API (관리자 전용) ────────────────────────────────────────────────
|
||||
class HomepageCardCreate(BaseModel):
|
||||
title: str
|
||||
subtitle: Optional[str] = ""
|
||||
description: Optional[str] = ""
|
||||
url: str
|
||||
tag: Optional[str] = "LIVE"
|
||||
sort_order: Optional[int] = 0
|
||||
visible: Optional[bool] = True
|
||||
|
||||
@app.get("/api/admin/homepage-cards")
|
||||
def list_homepage_cards(token=Depends(require_admin), conn=Depends(get_db)):
|
||||
"""관리자: 전체 카드 목록 (비공개 포함)"""
|
||||
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
||||
cur.execute("SELECT * FROM homepage_cards ORDER BY sort_order ASC, id ASC")
|
||||
return cur.fetchall()
|
||||
|
||||
@app.post("/api/admin/homepage-cards")
|
||||
def create_homepage_card(data: HomepageCardCreate, token=Depends(require_admin), conn=Depends(get_db)):
|
||||
"""관리자: 카드 추가"""
|
||||
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
||||
cur.execute(
|
||||
"""INSERT INTO homepage_cards (title, subtitle, description, url, tag, sort_order, visible)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s) RETURNING *""",
|
||||
(data.title, data.subtitle, data.description, data.url, data.tag, data.sort_order, data.visible)
|
||||
)
|
||||
conn.commit()
|
||||
return cur.fetchone()
|
||||
|
||||
@app.put("/api/admin/homepage-cards/{card_id}")
|
||||
def update_homepage_card(card_id: int, data: HomepageCardCreate, token=Depends(require_admin), conn=Depends(get_db)):
|
||||
"""관리자: 카드 수정"""
|
||||
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
||||
cur.execute(
|
||||
"""UPDATE homepage_cards
|
||||
SET title=%s, subtitle=%s, description=%s, url=%s, tag=%s, sort_order=%s, visible=%s
|
||||
WHERE id=%s RETURNING *""",
|
||||
(data.title, data.subtitle, data.description, data.url, data.tag, data.sort_order, data.visible, card_id)
|
||||
)
|
||||
result = cur.fetchone()
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail="Card not found")
|
||||
conn.commit()
|
||||
return result
|
||||
|
||||
@app.delete("/api/admin/homepage-cards/{card_id}")
|
||||
def delete_homepage_card(card_id: int, token=Depends(require_admin), conn=Depends(get_db)):
|
||||
"""관리자: 카드 삭제"""
|
||||
cur = conn.cursor()
|
||||
cur.execute("DELETE FROM homepage_cards WHERE id=%s", (card_id,))
|
||||
conn.commit()
|
||||
return {"ok": True}
|
||||
Reference in New Issue
Block a user