배포 도메인 관리자 페이지 블랭크 수정 (vite preview allowedHosts)

vnet.18ka.net 공개 배포에서 vite preview 서버가 Host 검증으로 공개 도메인
요청을 "Blocked request. This host is not allowed." 403으로 막아 SPA 전체가
블랭크로 뜨던 문제를 수정. server.allowedHosts와 같은 목록을 preview에도
적용하고 기본 허용 호스트에 vnet.18ka.net/api-vnet.18ka.net/vignette.chanpaca.net 추가.

검증: curl -H "Host: vnet.18ka.net" 127.0.0.1:5174 가 수정 전 Blocked →
수정 후 200+정상 index, 로컬 dev·preview 일반/슈퍼관리자 전 페이지 정상 렌더,
npm run typecheck. 대시보드 104차 기록.
This commit is contained in:
Yun Chan 2026-06-30 12:16:51 +09:00
parent 4e6b0045e3
commit 09d3220f68
2 changed files with 17 additions and 1 deletions

View file

@ -9,7 +9,16 @@ const nodeEnv = (
process?: { env?: { VITE_ALLOWED_HOSTS?: string; VITE_API_PROXY_TARGET?: string } }; process?: { env?: { VITE_ALLOWED_HOSTS?: string; VITE_API_PROXY_TARGET?: string } };
} }
).process?.env; ).process?.env;
const defaultAllowedHosts = ["alpaca-home.taile93291.ts.net"]; // 공개 배포 호스트(cloudflared 터널 대상)도 기본 허용에 포함한다.
// vite 6 preview 서버는 Host 헤더를 allowedHosts로 검증하므로, 누락 시
// "Blocked request. This host is not allowed." 403이 반환되어 배포 도메인이
// 통째로 블랭크가 된다. server(dev)와 preview(배포) 모두 같은 목록을 쓴다.
const defaultAllowedHosts = [
"alpaca-home.taile93291.ts.net",
"vnet.18ka.net",
"api-vnet.18ka.net",
"vignette.chanpaca.net",
];
const allowedHosts = Array.from( const allowedHosts = Array.from(
new Set([ new Set([
...defaultAllowedHosts, ...defaultAllowedHosts,
@ -47,6 +56,12 @@ export default defineConfig({
}, },
}, },
}, },
preview: {
port: 5174,
// 배포는 `vite preview`를 cloudflared 터널로 공개 도메인에 연결한다.
// allowedHosts가 없으면 공개 Host가 차단되어 전체 화면이 블랭크가 된다.
...(allowedHosts.length ? { allowedHosts } : {}),
},
build: { build: {
outDir: "dist", outDir: "dist",
sourcemap: false, sourcemap: false,

View file

@ -536,6 +536,7 @@
<p class="dg-note">101차 적용(2026-06-30): 관리자(<code>role=admin</code>)가 학습자·교수자·관리자 워크스페이스를 모두 직접 오갈 수 있도록 권한 경계를 정리했다. 백엔드는 <code>Principal.can_access_role()</code><code>role=admin</code>이면 모든 역할 접근을 허용하고, <code>require_role()</code>은 super_admin뿐 아니라 admin도 허용 역할로 승격한다. 학습자 전용 경로(<code>/consent</code> accept/withdraw, <code>/voice/ws</code>)의 super_admin-only 승격 분기도 <code>can_access_role(Role.LEARNER)</code> 기준으로 바꿔 admin이 동일하게 통과한다. 프론트는 <code>auth.tsx</code><code>canAccessRole()</code>에 admin 전체 접근을 반영하고, 역할 전환 메뉴는 <code>accessibleRolesFor()</code> 단일 헬퍼가 소유하며, <code>Sidebar</code> admin 내비에 <code>운영 홈</code>·<code>교수 콘솔</code>·<code>학습자 홈</code>·<code>학습</code>·<code>기록</code> 진입을 추가했다. 외부 API 계약·DB schema·승인 게이트(<code>account_status</code>)·super_admin 권한 부여/회수 동작은 바꾸지 않았다. 검증: <code>npm run typecheck</code>, <code>npm run check:api-types</code>, <code>py -3.11 -X utf8 -B -m pytest app/ -q</code> 290 passed(RBAC/runtime 30 포함), admin 워크스페이스 내비 E2E 1 passed, layout 시각게이트 9/9, session-layout 4 passed.</p> <p class="dg-note">101차 적용(2026-06-30): 관리자(<code>role=admin</code>)가 학습자·교수자·관리자 워크스페이스를 모두 직접 오갈 수 있도록 권한 경계를 정리했다. 백엔드는 <code>Principal.can_access_role()</code><code>role=admin</code>이면 모든 역할 접근을 허용하고, <code>require_role()</code>은 super_admin뿐 아니라 admin도 허용 역할로 승격한다. 학습자 전용 경로(<code>/consent</code> accept/withdraw, <code>/voice/ws</code>)의 super_admin-only 승격 분기도 <code>can_access_role(Role.LEARNER)</code> 기준으로 바꿔 admin이 동일하게 통과한다. 프론트는 <code>auth.tsx</code><code>canAccessRole()</code>에 admin 전체 접근을 반영하고, 역할 전환 메뉴는 <code>accessibleRolesFor()</code> 단일 헬퍼가 소유하며, <code>Sidebar</code> admin 내비에 <code>운영 홈</code>·<code>교수 콘솔</code>·<code>학습자 홈</code>·<code>학습</code>·<code>기록</code> 진입을 추가했다. 외부 API 계약·DB schema·승인 게이트(<code>account_status</code>)·super_admin 권한 부여/회수 동작은 바꾸지 않았다. 검증: <code>npm run typecheck</code>, <code>npm run check:api-types</code>, <code>py -3.11 -X utf8 -B -m pytest app/ -q</code> 290 passed(RBAC/runtime 30 포함), admin 워크스페이스 내비 E2E 1 passed, layout 시각게이트 9/9, session-layout 4 passed.</p>
<p class="dg-note">102차 적용(2026-06-30): 소유자(윤찬) 결정 4건이 확정되어 owner 보드 결정 카드 7→3으로 줄었다. (1) <b>s2s 2차 PoC 유지</b> — 캐스케이드 1차를 운영 기준으로 두면서 <code>voice-s2s-poc.md</code> 비교 PoC를 계속 연다(최종 채택은 비교 리포트 이후). (2) <b>fast-loop 외부 폴백 허용</b> — 로컬 상주 분류기 기본 + 외부 Haiku/Solar 폴백 허용, 전송 시 데이터주권·보존·동의 가드 적용. (3) <b>재귀학습 fine-tuning 포함</b> — few-shot 자동갱신에 더해 fine-tuning까지 범위에 포함하되, 데이터셋은 동의·추가 축어록 수급 게이트가 닫힌 뒤 구성. (4) <b>서비스 음성 = 대체 TTS provider</b> — 상업 라이선스가 명확한 대체 provider 채택, Higgs synthetic·dev sample provider는 개발 플래그로만 유지, 실존 인물/성우 ref 금지 유지. 네 결정 모두 방향 확정이며 구현/실측은 후속 build 항목으로 남는다. 남은 owner 결정은 평가설계(H1 κ/ICC), 운영 보존·에스컬레이션, 추가 축어록 수급 3건이다. SSOT drift 게이트(<code>check-dev-dashboard-ssot.py</code>)의 기대 카운트도 결정 3·decision panel 3행으로 동기화했다.</p> <p class="dg-note">102차 적용(2026-06-30): 소유자(윤찬) 결정 4건이 확정되어 owner 보드 결정 카드 7→3으로 줄었다. (1) <b>s2s 2차 PoC 유지</b> — 캐스케이드 1차를 운영 기준으로 두면서 <code>voice-s2s-poc.md</code> 비교 PoC를 계속 연다(최종 채택은 비교 리포트 이후). (2) <b>fast-loop 외부 폴백 허용</b> — 로컬 상주 분류기 기본 + 외부 Haiku/Solar 폴백 허용, 전송 시 데이터주권·보존·동의 가드 적용. (3) <b>재귀학습 fine-tuning 포함</b> — few-shot 자동갱신에 더해 fine-tuning까지 범위에 포함하되, 데이터셋은 동의·추가 축어록 수급 게이트가 닫힌 뒤 구성. (4) <b>서비스 음성 = 대체 TTS provider</b> — 상업 라이선스가 명확한 대체 provider 채택, Higgs synthetic·dev sample provider는 개발 플래그로만 유지, 실존 인물/성우 ref 금지 유지. 네 결정 모두 방향 확정이며 구현/실측은 후속 build 항목으로 남는다. 남은 owner 결정은 평가설계(H1 κ/ICC), 운영 보존·에스컬레이션, 추가 축어록 수급 3건이다. SSOT drift 게이트(<code>check-dev-dashboard-ssot.py</code>)의 기대 카운트도 결정 3·decision panel 3행으로 동기화했다.</p>
<p class="dg-note">103차 적용(2026-06-30): 남은 소유자 결정 3건이 확정되어 owner 결정 7건이 전건 확정(결정 필요 3→0)됐다. (1) <b>평가설계 H1</b> — 평가자 신뢰도 κ≥0.70 & ICC≥0.75, 환각률 ≤3%, 독립표본 t-검정(α=0.05), 20명 실험/통제군 무작위 배정, 단회기 50분·3척도 pre-post 유지. (2) <b>운영 보존·에스컬레이션</b> — 헬스 raw 90일·rollup 365일, escalation 운영자 수동 승인, SLA 미표기(관측 샘플 이력 유지), 담당 그룹 자동 배정 미도입. (3) <b>추가 축어록</b> — 20건을 1개월 내 수급. 이 결정들은 설계·정책 확정이며, KPI threshold 반영·retention 기본값 적용·실제 수급/파일럿 evidence·통계해석은 후속 build/GATE로 남는다. 즉 owner 보드의 남은 항목은 모두 외부 증거·기관 의존(공개 OAuth 실 <code>/turn</code>, vnet DNS, 한신대 거버넌스, 임상팀 콘텐츠, 20명 파일럿·법무·물리 실측)이며, 운영 원칙상 E2E/외부 증거 없이는 DONE으로 체크하지 않는다. SSOT drift 게이트의 기대 카운트도 결정 0·decision panel 0행으로 동기화했다.</p> <p class="dg-note">103차 적용(2026-06-30): 남은 소유자 결정 3건이 확정되어 owner 결정 7건이 전건 확정(결정 필요 3→0)됐다. (1) <b>평가설계 H1</b> — 평가자 신뢰도 κ≥0.70 & ICC≥0.75, 환각률 ≤3%, 독립표본 t-검정(α=0.05), 20명 실험/통제군 무작위 배정, 단회기 50분·3척도 pre-post 유지. (2) <b>운영 보존·에스컬레이션</b> — 헬스 raw 90일·rollup 365일, escalation 운영자 수동 승인, SLA 미표기(관측 샘플 이력 유지), 담당 그룹 자동 배정 미도입. (3) <b>추가 축어록</b> — 20건을 1개월 내 수급. 이 결정들은 설계·정책 확정이며, KPI threshold 반영·retention 기본값 적용·실제 수급/파일럿 evidence·통계해석은 후속 build/GATE로 남는다. 즉 owner 보드의 남은 항목은 모두 외부 증거·기관 의존(공개 OAuth 실 <code>/turn</code>, vnet DNS, 한신대 거버넌스, 임상팀 콘텐츠, 20명 파일럿·법무·물리 실측)이며, 운영 원칙상 E2E/외부 증거 없이는 DONE으로 체크하지 않는다. SSOT drift 게이트의 기대 카운트도 결정 0·decision panel 0행으로 동기화했다.</p>
<p class="dg-note">104차 적용(2026-06-30): 공개 배포 도메인(<code>vnet.18ka.net</code>)에서 슈퍼관리자가 관리자 페이지 전체를 블랭크로 보던 문제를 고쳤다. 원인은 권한이 아니라 <code>vite preview</code> 서버의 host 검증이었다. 배포는 <code>npm run preview</code>를 cloudflared 터널로 공개 도메인에 연결하는데, <code>vite.config.ts</code><code>preview.allowedHosts</code>가 없어 공개 Host 요청이 <code>Blocked request. This host is not allowed.</code> 403 텍스트만 받아 SPA가 통째로 로드되지 못했다(에러 바운더리 부재로 전체 흰 화면). <code>server.allowedHosts</code>와 동일 목록을 <code>preview</code>에도 적용하고 기본 허용에 <code>vnet.18ka.net</code>/<code>api-vnet.18ka.net</code>/<code>vignette.chanpaca.net</code>를 추가했다. 검증: 재현(<code>curl -H "Host: vnet.18ka.net" 127.0.0.1:5174</code> → 수정 전 Blocked, 수정 후 <code>200</code>+정상 index), 로컬 dev·preview에서 일반/슈퍼 관리자 전 admin·teach·learn 페이지 정상 렌더(pageerror 0), <code>npm run typecheck</code>. 공개 OAuth 실 <code>/turn</code> 증명은 여전히 별도 BLOCK이다.</p>
<p class="dg-note">20차 적용(2026-06-28): 신규 Google/SAML 사용자는 <code>account_status=pending</code>으로 시작하고 승인 전에는 <code>/pending</code> 안내 화면만 본다. <code>yunchan@twentyoz.kr</code>는 슈퍼 관리자 allowlist로 admin+approved를 받으며, <code>/admin/users</code>는 가입 승인 탭에서 pending 계정을 승인 또는 보류 처리한다.</p> <p class="dg-note">20차 적용(2026-06-28): 신규 Google/SAML 사용자는 <code>account_status=pending</code>으로 시작하고 승인 전에는 <code>/pending</code> 안내 화면만 본다. <code>yunchan@twentyoz.kr</code>는 슈퍼 관리자 allowlist로 admin+approved를 받으며, <code>/admin/users</code>는 가입 승인 탭에서 pending 계정을 승인 또는 보류 처리한다.</p>
<p class="dg-note">21차 적용(2026-06-28): 관리자 페이지 진입권을 기본 역할과 분리해 <code>app_user.admin_access</code>로 저장한다. <code>AUTH_SUPER_ADMIN_EMAILS</code> 기본값은 <code>yunchan@twentyoz.kr</code>, <code>hoonjungkoo@hs.ac.kr</code>이며, 슈퍼 관리자는 학습자·교수자·관리자 공간 전환과 관리자 권한 부여/회수를 할 수 있다. 학생·교수 계정도 <code>admin_access=true</code>면 우측 상단 관리자 진입이 노출된다. 구성 슈퍼 관리자의 권한 회수와 계정 비활성화는 차단한다.</p> <p class="dg-note">21차 적용(2026-06-28): 관리자 페이지 진입권을 기본 역할과 분리해 <code>app_user.admin_access</code>로 저장한다. <code>AUTH_SUPER_ADMIN_EMAILS</code> 기본값은 <code>yunchan@twentyoz.kr</code>, <code>hoonjungkoo@hs.ac.kr</code>이며, 슈퍼 관리자는 학습자·교수자·관리자 공간 전환과 관리자 권한 부여/회수를 할 수 있다. 학생·교수 계정도 <code>admin_access=true</code>면 우측 상단 관리자 진입이 노출된다. 구성 슈퍼 관리자의 권한 회수와 계정 비활성화는 차단한다.</p>
<p class="dg-note">25차 적용(2026-06-28): <code>/admin/users</code>에서 허용 도메인 밖 이메일도 정확한 계정 단위로 강제 등록할 수 있다. Google/SAML/dev-login은 미리 등록된 이메일만 도메인 게이트 예외로 통과시키고, provider 로그인 시 기존 관리 row의 역할·코호트·승인 상태를 이어받는다. 미등록 외부 도메인 로그인은 계속 차단한다.</p> <p class="dg-note">25차 적용(2026-06-28): <code>/admin/users</code>에서 허용 도메인 밖 이메일도 정확한 계정 단위로 강제 등록할 수 있다. Google/SAML/dev-login은 미리 등록된 이메일만 도메인 게이트 예외로 통과시키고, provider 로그인 시 기존 관리 row의 역할·코호트·승인 상태를 이어받는다. 미등록 외부 도메인 로그인은 계속 차단한다.</p>