From e8a85be36cff5b146fc7dbc0f3492f56cef27438 Mon Sep 17 00:00:00 2001 From: king Date: Sun, 24 May 2026 05:04:06 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=A3=BC=EB=AC=B8=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EB=A5=BC=20/orderlist/=20=EB=A6=AC=EB=B2=84=EC=8A=A4?= =?UTF-8?q?=20=ED=94=84=EB=A1=9D=EC=8B=9C=20=EA=B2=BD=EB=A1=9C=EB=A1=9C=20?= =?UTF-8?q?=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 메뉴 카드 새 탭 열기 + /orderlist/health/db 헬스 배지 (온라인/오프라인) 추가 - CUSTOMER_ORDER_LIST_URL 기본값을 /orderlist/ 로 (NPM 같은 도메인 하위 경로) - orderlist 측 compose 정리 스크립트 추가 (ports 제거, npm_default 외부 네트워크 attach) --- .env.example | 3 +- app/main.py | 4 +- app/static/styles.css | 34 +++++++ app/templates/main.html | 31 ++++++- scripts/fix-orderlist-compose.sh | 150 +++++++++++++++++++++++++++++++ 5 files changed, 218 insertions(+), 4 deletions(-) create mode 100644 scripts/fix-orderlist-compose.sh diff --git a/.env.example b/.env.example index 36a3e9a..e0ab2f0 100644 --- a/.env.example +++ b/.env.example @@ -4,4 +4,5 @@ SESSION_SECRET_KEY=replace-with-a-long-random-secret SESSION_COOKIE_SECURE=true PUBLIC_BASE_URL=https://dbx.no1king.freeddns.org CS_ORDER_URL=https://cs.example.com -CUSTOMER_ORDER_LIST_URL=https://orders.example.com +# 비워두면 같은 도메인의 /orderlist/ 경로(NPM 리버스 프록시)로 자동 연결됨 +CUSTOMER_ORDER_LIST_URL=/orderlist/ diff --git a/app/main.py b/app/main.py index f2a678c..5a24b41 100644 --- a/app/main.py +++ b/app/main.py @@ -113,11 +113,13 @@ async def home(request: Request) -> HTMLResponse: "title": "CS 발주 업무", "description": "CS 발주 업무 페이지로 이동", "url": env("CS_ORDER_URL", "#"), + "health_url": "", }, { "title": "고객 주문리스트 프로그램", "description": "고객 주문리스트 프로그램으로 이동", - "url": env("CUSTOMER_ORDER_LIST_URL", "#"), + "url": env("CUSTOMER_ORDER_LIST_URL", "/orderlist/"), + "health_url": "/orderlist/health/db", }, ] return render_template( diff --git a/app/static/styles.css b/app/static/styles.css index 40a7536..8e74114 100644 --- a/app/static/styles.css +++ b/app/static/styles.css @@ -789,3 +789,37 @@ a { color: inherit; text-decoration: none; } .header-portal { display: none; } .btn-logout span { display: none; } } + +/* ── 상태 배지 ─────────────────────────────────── */ +.status-badge { + display: inline-flex; + align-items: center; + gap: 6px; + margin-left: 8px; + padding: 2px 8px; + font-size: 11px; + font-weight: 600; + border-radius: 999px; + vertical-align: middle; + line-height: 1.4; + background: #eef2f7; + color: var(--muted); + border: 1px solid var(--border); +} +.status-badge::before { + content: ""; + width: 6px; + height: 6px; + border-radius: 50%; + background: currentColor; +} +.status-badge[data-status="online"] { + background: #e7f6ee; + color: var(--success); + border-color: #b6e0c6; +} +.status-badge[data-status="offline"] { + background: #fdecec; + color: var(--danger); + border-color: #f1c2c2; +} diff --git a/app/templates/main.html b/app/templates/main.html index ec30fb7..e25ffb3 100644 --- a/app/templates/main.html +++ b/app/templates/main.html @@ -55,7 +55,8 @@
{% for item in menu_items %} - +
{% if loop.index == 1 %} @@ -98,7 +99,12 @@
-

{{ item.title }}

+

+ {{ item.title }} + {% if item.health_url %} + 확인 중 + {% endif %} +

{{ item.description }}

@@ -123,6 +129,27 @@ const days = ["일요일","월요일","화요일","수요일","목요일","금요일","토요일"]; document.getElementById("today-date").textContent = d.getFullYear() + "년 " + (d.getMonth()+1) + "월 " + d.getDate() + "일 " + days[d.getDay()]; + + // 헬스 배지 — data-health-url 이 있는 카드만 갱신 + async function refreshHealthBadges() { + const cards = document.querySelectorAll(".menu-card[data-health-url]"); + await Promise.all(Array.from(cards).map(async (card) => { + const url = card.getAttribute("data-health-url"); + const badge = card.querySelector(".status-badge"); + if (!badge) return; + try { + const res = await fetch(url, { cache: "no-store", credentials: "omit" }); + const ok = res.ok; + badge.dataset.status = ok ? "online" : "offline"; + badge.textContent = ok ? "온라인" : "오프라인"; + } catch { + badge.dataset.status = "offline"; + badge.textContent = "오프라인"; + } + })); + } + refreshHealthBadges(); + setInterval(refreshHealthBadges, 30000); diff --git a/scripts/fix-orderlist-compose.sh b/scripts/fix-orderlist-compose.sh new file mode 100644 index 0000000..e9daaa9 --- /dev/null +++ b/scripts/fix-orderlist-compose.sh @@ -0,0 +1,150 @@ +#!/usr/bin/env bash +# /opt/dbx-orderlist/docker-compose.yml 를 안전하게 교체하고 재기동·검증한다. +# +# 변경 요지: +# - ports: "8000:8000" 제거 (호스트 외부 노출 차단) +# - expose: "8000" 추가 (도커 네트워크 내부에서만 접근) +# - 외부 네트워크 npm_default 에 영구 attach (NPM 리버스 프록시용) +# +# 사용법: +# sudo bash fix-orderlist-compose.sh +# +# 멱등(idempotent): 여러 번 실행해도 결과는 동일. + +set -euo pipefail + +COMPOSE_DIR="/opt/dbx-orderlist" +COMPOSE_FILE="${COMPOSE_DIR}/docker-compose.yml" +BACKUP_FILE="${COMPOSE_FILE}.bak.$(date +%Y%m%d-%H%M%S)" + +red() { printf '\033[31m%s\033[0m\n' "$*"; } +green() { printf '\033[32m%s\033[0m\n' "$*"; } +yellow() { printf '\033[33m%s\033[0m\n' "$*"; } +blue() { printf '\033[34m%s\033[0m\n' "$*"; } + +# ── 0. 사전 검사 ──────────────────────────────────────────────── +if [[ ! -f "${COMPOSE_FILE}" ]]; then + red "ERROR: ${COMPOSE_FILE} 를 찾을 수 없습니다." + exit 1 +fi + +if ! docker network inspect npm_default >/dev/null 2>&1; then + red "ERROR: npm_default 네트워크가 존재하지 않습니다. NPM 컨테이너가 실행 중인지 확인하세요." + exit 1 +fi + +# ── 1. 백업 ───────────────────────────────────────────────────── +blue "[1/5] 기존 compose 파일 백업 → ${BACKUP_FILE}" +cp -a "${COMPOSE_FILE}" "${BACKUP_FILE}" + +# ── 2. 새 compose 파일 작성 ──────────────────────────────────── +blue "[2/5] 새 docker-compose.yml 작성" +cat > "${COMPOSE_FILE}" <<'YAML' +services: + app: + build: . + image: orderlist-app:latest + container_name: orderlist-app + restart: unless-stopped + env_file: + - .env + environment: + # .env에 적힌 값이 컨테이너에서 그대로 보입니다. + # 컨테이너 내부에서 DB_HOST는 반드시 postgres-db 여야 합니다. + DB_HOST: ${DB_HOST:-postgres-db} + # 호스트 포트 바인딩 없음. NPM 리버스 프록시(npm_default 네트워크)로만 접근. + expose: + - "8000" + networks: + - postgres_net + - npm_net + - default + healthcheck: + test: ["CMD", "curl", "-fsS", "http://localhost:8000/health/db"] + interval: 30s + timeout: 5s + retries: 5 + start_period: 20s + +networks: + # 이미 운영 중인 postgres-db 컨테이너가 속해 있는 외부 네트워크 + postgres_net: + external: true + name: ${POSTGRES_NETWORK:-postgres-net} + # NPM(Nginx Proxy Manager) 컨테이너가 속해 있는 외부 네트워크 + npm_net: + external: true + name: npm_default + default: + driver: bridge +YAML + +# ── 3. 재기동 ─────────────────────────────────────────────────── +blue "[3/5] docker compose up -d" +cd "${COMPOSE_DIR}" +docker compose up -d + +# ── 4. 검증 ───────────────────────────────────────────────────── +blue "[4/5] 검증" + +echo +yellow "── 호스트 포트 바인딩 (8000 외부 노출 여부) ──" +PORT_LINE="$(docker ps --format '{{.Names}}\t{{.Ports}}' | grep '^orderlist-app' || true)" +echo "${PORT_LINE}" +if echo "${PORT_LINE}" | grep -q '0\.0\.0\.0:8000'; then + red "FAIL: 0.0.0.0:8000 매핑이 아직 남아 있습니다." + exit 1 +else + green "OK: 호스트 0.0.0.0:8000 매핑 제거됨." +fi + +echo +yellow "── orderlist-app 의 네트워크 ──" +NETS="$(docker inspect orderlist-app --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}')" +echo "${NETS}" +for required in npm_default; do + if ! echo " ${NETS} " | grep -q " ${required} "; then + red "FAIL: ${required} 네트워크에 연결되지 않았습니다." + exit 1 + fi +done +green "OK: npm_default 네트워크 연결 확인." + +# 재시도 helper — uvicorn 기동 + DB 연결까지 최대 ${1}초 대기 +wait_until_200() { + local desc="$1"; shift + local cmd="$*" + local code="" + local i + for i in $(seq 1 60); do + code="$(eval "${cmd}" 2>/dev/null || true)" + if [[ "${code}" == "200" ]]; then + echo "HTTP ${code} (try ${i})" + green "OK: ${desc}" + return 0 + fi + sleep 1 + done + red "FAIL: ${desc} — 최종 HTTP ${code:-ERR} (60초 대기 후에도 200 미수신)" + echo + yellow "── orderlist-app 최근 로그 (디버그용) ──" + docker logs --tail 40 orderlist-app || true + return 1 +} + +echo +yellow "── NPM 컨테이너에서 orderlist-app 호출 (최대 60초 대기) ──" +wait_until_200 "NPM 컨테이너에서 200 응답" \ + "docker exec npm-app sh -lc \"curl -fsS -o /dev/null -w '%{http_code}' http://orderlist-app:8000/health/db\"" + +echo +yellow "── 외부 도메인 헬스 체크 (최대 60초 대기) ──" +wait_until_200 "https://dbx.no1king.freeddns.org/orderlist/health/db → 200" \ + "curl -fsS -o /dev/null -w '%{http_code}' https://dbx.no1king.freeddns.org/orderlist/health/db" + +# ── 5. 완료 ───────────────────────────────────────────────────── +echo +green "[5/5] 완료. 백업: ${BACKUP_FILE}" +echo +yellow "남은 수동 점검 (다른 머신/서버 공인 IP에서):" +echo " curl --max-time 5 http://<서버공인IP>:8000/ # → Connection refused 여야 정상"