feat: 주문리스트를 /orderlist/ 리버스 프록시 경로로 통합

- 메뉴 카드 새 탭 열기 + /orderlist/health/db 헬스 배지 (온라인/오프라인) 추가
- CUSTOMER_ORDER_LIST_URL 기본값을 /orderlist/ 로 (NPM 같은 도메인 하위 경로)
- orderlist 측 compose 정리 스크립트 추가 (ports 제거, npm_default 외부 네트워크 attach)
This commit is contained in:
2026-05-24 05:04:06 +09:00
parent 0c007e6432
commit e8a85be36c
5 changed files with 218 additions and 4 deletions
+2 -1
View File
@@ -4,4 +4,5 @@ SESSION_SECRET_KEY=replace-with-a-long-random-secret
SESSION_COOKIE_SECURE=true SESSION_COOKIE_SECURE=true
PUBLIC_BASE_URL=https://dbx.no1king.freeddns.org PUBLIC_BASE_URL=https://dbx.no1king.freeddns.org
CS_ORDER_URL=https://cs.example.com CS_ORDER_URL=https://cs.example.com
CUSTOMER_ORDER_LIST_URL=https://orders.example.com # 비워두면 같은 도메인의 /orderlist/ 경로(NPM 리버스 프록시)로 자동 연결됨
CUSTOMER_ORDER_LIST_URL=/orderlist/
+3 -1
View File
@@ -113,11 +113,13 @@ async def home(request: Request) -> HTMLResponse:
"title": "CS 발주 업무", "title": "CS 발주 업무",
"description": "CS 발주 업무 페이지로 이동", "description": "CS 발주 업무 페이지로 이동",
"url": env("CS_ORDER_URL", "#"), "url": env("CS_ORDER_URL", "#"),
"health_url": "",
}, },
{ {
"title": "고객 주문리스트 프로그램", "title": "고객 주문리스트 프로그램",
"description": "고객 주문리스트 프로그램으로 이동", "description": "고객 주문리스트 프로그램으로 이동",
"url": env("CUSTOMER_ORDER_LIST_URL", "#"), "url": env("CUSTOMER_ORDER_LIST_URL", "/orderlist/"),
"health_url": "/orderlist/health/db",
}, },
] ]
return render_template( return render_template(
+34
View File
@@ -789,3 +789,37 @@ a { color: inherit; text-decoration: none; }
.header-portal { display: none; } .header-portal { display: none; }
.btn-logout span { 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;
}
+29 -2
View File
@@ -55,7 +55,8 @@
<div class="card-grid"> <div class="card-grid">
{% for item in menu_items %} {% for item in menu_items %}
<a class="menu-card" href="{{ item.url }}"> <a class="menu-card" href="{{ item.url }}" target="_blank" rel="noopener noreferrer"
{% if item.health_url %}data-health-url="{{ item.health_url }}"{% endif %}>
<div class="card-icon"> <div class="card-icon">
{% if loop.index == 1 %} {% if loop.index == 1 %}
<!-- 클립보드 / 발주 아이콘 --> <!-- 클립보드 / 발주 아이콘 -->
@@ -98,7 +99,12 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<p class="card-title">{{ item.title }}</p> <p class="card-title">
{{ item.title }}
{% if item.health_url %}
<span class="status-badge" data-status="checking" aria-live="polite">확인 중</span>
{% endif %}
</p>
<p class="card-desc">{{ item.description }}</p> <p class="card-desc">{{ item.description }}</p>
</div> </div>
@@ -123,6 +129,27 @@
const days = ["일요일","월요일","화요일","수요일","목요일","금요일","토요일"]; const days = ["일요일","월요일","화요일","수요일","목요일","금요일","토요일"];
document.getElementById("today-date").textContent = document.getElementById("today-date").textContent =
d.getFullYear() + "년 " + (d.getMonth()+1) + "월 " + d.getDate() + "일 " + days[d.getDay()]; 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);
</script> </script>
</body> </body>
</html> </html>
+150
View File
@@ -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 여야 정상"