feat: 주문리스트를 /orderlist/ 리버스 프록시 경로로 통합
- 메뉴 카드 새 탭 열기 + /orderlist/health/db 헬스 배지 (온라인/오프라인) 추가 - CUSTOMER_ORDER_LIST_URL 기본값을 /orderlist/ 로 (NPM 같은 도메인 하위 경로) - orderlist 측 compose 정리 스크립트 추가 (ports 제거, npm_default 외부 네트워크 attach)
This commit is contained in:
+2
-1
@@ -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
@@ -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(
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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 여야 정상"
|
||||||
Reference in New Issue
Block a user