Skip to content

모델 게이트웨이 화면(포탈 컨트롤센터 콘솔) 작업 인수인계 — 2026-09-26 ​

다른 세션에서 이어서 작업하기 위한 노트입니다. 결론부터: 이 화면은 antd 스타일이 아니라 자체 div + 자체 CSS 박스로 만들어져 있습니다. 다음 단계는 콘솔이 이미 쓰고 있는 @hohyun94/portal-foundation(내부적으로 antd 6 래퍼) 컴포넌트로 화면을 재구성하는 것입니다.

0. 한눈에 보기 ​

항목값
대상관리자 콘솔 → 탭 모델 게이트웨이 관리 (ModelGatewayDesk)
화면 코드client/src/components/ModelGatewayDesk.tsx — 현재 1006줄(작업 전 859줄)
화면 스타일client/src/styles/modelGatewayDesk.css — 현재 117줄(작업 전 69줄, 이번에 재작성)
마운트 지점client/src/pages/DirectoryAdmin.tsx:534 → {tab === "model-gateway" && <ModelGatewayDesk onNotice={setNotice} />}
콘솔 서버server/modelGateway.ts(게이트웨이 /admin/api 서버간 프록시) + server/routers.ts:104 의 gateway: 라우터
게이트웨이 쪽 APIModelGateway/src/admin/routes.ts (state / providers / keys / verify / settings / usage)
UI 기반@hohyun94/portal-foundation@0.6.156 → 의존성에 antd ^6.6.3(설치본 6.6.5), @ant-design/icons ^6.3.4
antd 직접 사용client/src·server 전체에서 from "antd" 0건 — 모든 UI는 portal-foundation 래퍼 + 자체 CSS(reference-portal-frame.css + 데스크별 CSS)
이번 세션 검증npx tsc --noEmit → exit 0 / prettier --check → warn (HEAD 원본도 동일하게 warn, 기존 상태) / vitest·build 결과는 §4
커밋·배포미커밋 · 미배포. git status = 위 2개 파일 수정만

작업 디렉터리 / 리포지토리 주의(중요) ​

  • 로컬 리포지토리: /home/hohyun/projects/PortalConsole, remote = git@github.com:hohyun94/PortalConsole.git
  • 배포(Coolify)는 다른 리포지토리를 본다: 앱 directory-service:main-h3igjh2lszlqoegqinwitlyx
    • git_repository = hohyun94/DirectoryService, git_branch = main, build_pack = railpack, fqdn = https://directory.rlab.monster
    • 배포 번들(/app/dist/public/assets/index-Drf04BCZ.js)에 모델 게이트웨이 관리 문자열이 있어 이 콘솔 UI가 그 앱에 포함됨을 확인함
    • ⇒ 푸시 대상 확인 필요: 로컬 remote가 PortalConsole인데 Coolify는 DirectoryService를 본다. 둘 중 어느 쪽에 푸시해야 반영되는지 먼저 확인할 것(리포 이름 변경/동기화 여부).

1. 지금 이 화면이 하는 일(동작은 그대로 유지됨) ​

  1. 상단 상태 카드 4개 — 게이트웨이 ON/OFF·주소, 프로바이더 수(활성/기본), 키 암호화(AES-256-GCM), 배포 프로필 + 상태 새로고침
  2. 프로바이더와 API 키 — 프로바이더별: 키 힌트 목록(사용/중지 토글, 삭제), 키 추가(8자 이상), 연결 테스트(발견 모델 반영), 편집/삭제
  3. 프로바이더 추가·편집 폼 — 프리셋, ID, 종류(openai-compatible/anthropic), 표시 이름, API 주소, 모델 목록, 사용 여부, 기본 프로바이더
  4. 실행 설정 — 기본/허용 모델, purpose 매핑 행 편집, 단가표(USD/1M) override 행(입력/출력/캐시 읽기/피크 배율/휴일), 분당 토큰 한도, 쿼터 예약, 요청 타임아웃
  5. 사용량·비용 — 포털 ID/purpose/기간(1~30일) 필터 → 합계 카드 5개(요청·실패·토큰·평균 지연·금액 USD) + 모델별/일별 분해(단가 미입력 행에 단가 행 추가)

tRPC 프로시저(변경 없음): gateway.status, gateway.state, gateway.saveProvider, gateway.deleteProvider, gateway.addKey, gateway.setKeyEnabled, gateway.deleteKey, gateway.verifyProvider, gateway.saveSettings, gateway.usage.

2. 이번 세션에서 바꾼 것 (전부 "레이아웃·문구·테마 정리" 범위, 동작 로직 변경 없음) ​

2-1. CSS 재작성 — client/src/styles/modelGatewayDesk.css ​

형제 데스크(fileStoreDesk.css:1-66, ontologyDesk.css:1-40)의 관례에 맞춰 처음부터 다시 작성했습니다.

  • 모든 선택자를 .directory-admin-content 안으로 스코프. (수정 전에는 사용량 관련 .model-gateway-usage-*가 스코프 밖이라 다른 화면과 충돌 가능)
  • 라이트 1블록 + 다크(네이비) 1블록으로 통합(수정 전엔 테마 규칙이 파일 중간·끝 두 곳에 흩어져 있었음)
  • 라이트 색은 형제와 동일 팔레트: line #d6d0c5 / surface #fffdf8 / muted surface #f6f3ec / text #796d5f / accent #81553f / ok #407c62 / danger #8c4130
  • 다크는 기존 CSS 변수 사용: --directory-dark-line, --directory-dark-panel, --directory-navy-800, --directory-ice, --directory-dark-muted, --directory-accent
  • 새 규칙 추가
    • 뱃지: .model-gateway-badges span + is-on / is-off / is-default(점선) — FileStore 데스크의 .file-store-badges span과 같은 모양
    • 링크형 보조 액션: .model-gateway-action (+ .is-danger, :disabled) — FileStore .file-store-action과 동일 패턴
    • .model-gateway-actions(액션 줄), .model-gateway-form-actions(폼 저장 버튼 줄), .model-gateway-key-empty(키 없음 문구), .model-gateway-subheading(프레임의 .secondary-heading과 !important 충돌 회피용 소제목), .model-gateway-verify(+.is-error)
    • 사용량 막대 색: var(--pf-accent) → 테마별 색(라이트 #81553f, 다크 var(--directory-accent))

2-2. 문구·구조 정리 — client/src/components/ModelGatewayDesk.tsx ​

  • i18n 적용: 이 파일은 const text = (ko, en) => locale === "en" ? en : ko 헬퍼를 정의만 하고 거의 쓰지 않았습니다(전부 하드코딩 한글). 이번에 화면에 보이는 모든 라벨·설명·카드 문구·옵션 값에 text()를 적용했습니다(형제 FileStoreDesk.tsx와 동일 방식). onNotice(...) 토스트 문구는 FileStoreDesk 관례대로 한글 유지.
  • 뱃지 정리: portal-access-status(전역 알약, 변형 없음) → .model-gateway-badges + is-on/is-off/is-default. 사용량의 단가 미입력도 같은 뱃지로 이관.
  • 액션 위계 정리
    • 주요 액션 유지: 키 저장(PortalButton), 조회(PortalButton) / 연결 테스트는 quiet-button으로 낮춤
    • 보조 액션을 링크형으로: 편집, 삭제(프로바이더), 키 사용/중지, 키 삭제, 목적/단가 행 삭제, 발견 모델 반영
    • 파괴적 액션에 확인창 추가(형제 관례 window.confirm): 프로바이더 삭제, 키 삭제
  • 폼 정리: 프로바이더 폼·실행 설정 폼의 저장 버튼을 그리드 밖 .model-gateway-form-actions로 이동(형제 .file-store-form-actions 패턴). 키 입력 필드에 8자 제약 설명 추가.
  • 소제목 교체: purpose 매핑·단가표의 중첩 .secondary-heading → .model-gateway-subheading(프레임이 .secondary-heading을 !important로 잡아 배경·보더가 겹치던 문제 회피)
  • 문구 명확화: 단가 필드 라벨 입력/출력/캐시 히트 → 입력 (USD/1M) / 출력 (USD/1M) / 캐시 읽기 (USD/1M), 공휴일 → 휴일, 실효 단가 힌트 현재 … → 현재 적용: 입력 … / 출력 … / 캐시 … · 피크 ×n, purpose 매핑 설명에서 내부 용어(예약값 auto) 제거, 사용량 필터에 "비우면 전체 …" 설명 추가
  • 상태 표시 추가: state.isLoading일 때 안내 한 줄(게이트웨이 상태를 불러오는 중입니다…)

2-3. 파일 크기 변화 ​

작업 전: ModelGatewayDesk.tsx 859줄 / modelGatewayDesk.css  69줄
작업 후: ModelGatewayDesk.tsx 1006줄 / modelGatewayDesk.css 117줄

(줄 증가는 대부분 text(ko, en) 2줄 포맷과 설명 문장 분리 때문)

3. 다음에 할 일 — 우선순위대로 ​

3-1. ⭐ 방향 전환 제안: "자체 CSS 박스" → portal-foundation(antd 6) 컴포넌트 ​

이번 세션에서 한 정리는 기존 자체 CSS 구조를 유지한 채 스코프·테마·문구를 맞춘 것입니다. 그런데 화면 자체가 antd 스타일이 아니라는 지적이 있었고, 사실 확인 결과:

  • 콘솔의 UI 라이브러리는 @hohyun94/portal-foundation@0.6.156이고, 이 패키지가 antd ^6.6.3 / @ant-design/icons ^6.3.4를 의존성으로 가집니다.
  • 그런데 client/src에는 from "antd"가 0건입니다. 즉 콘솔은 antd를 직접 쓰지 않고 래퍼만 씁니다.
  • ModelGatewayDesk는 래퍼 필드 4개(PortalButton, PortalSelect, PortalTextField, PortalTextArea)만 쓰고, 나머지 영역은 자체 div + 자체 CSS 클래스로 만든 화면입니다. → 그래서 antd스럽지 않습니다.

다음 세션에서는 아래 이미 내보내고 있는 컴포넌트로 재구성하는 것을 권합니다(이름은 설치본 node_modules/@hohyun94/portal-foundation/dist/*.d.ts에서 확인).

용도후보 컴포넌트
구역(섹션) 틀AdminSection, DataListShell, DataDetailShell, DataStatePanel
카드/요약 카드PortalCard, DataStatusBadge, OperationalMetricGrid (현재 ops-cards 수동 마크업 대체)
표PortalDataTable (+ antdTableQueryToPortalQuery, normalizePortalFilters)
상세/요약PortalDescriptions
빈 상태/오류PortalEmptyState, AdminEmptyState, PortalErrorState, DataStatePanel
확인창/모달PortalConfirm, PortalDialog (현재 window.confirm 대체)
피드백ActionFeedback, AdminFeedback, FeedbackMessage
폼 요소PortalButton, PortalSelect, PortalTextField, PortalTextArea, PortalCheckbox
기타PortalBreadcrumbs, PortalAiSummaryCard, PortalAntdIcon/getPortalAntdIcon(아이콘 일관화)

작업 방식 제안: ① 화면만 먼저 컴포넌트로 치환(데이터·tRPC 호출은 그대로) → ② 치환 후 modelGatewayDesk.css에서 대체된 규칙 삭제 → ③ 콘솔 다른 데스크(FileStore/Ontology)도 같은 방향으로 가는지 확인해 한 화면만 다른 스타일이 되지 않게 할 것.

⚠️ 주의: reference-portal-frame.css는 .admin-secondary, .secondary-heading, .ops-cards > div, .settings-security-note 등을 !important로 덮습니다. portal-foundation 컴포넌트로 바꾸면 이 전역 규칙과 충돌할 수 있으니, 치환 시 이 프레임 파일도 함께 봐야 합니다(현재 CSS 파일은 그 충돌을 피해 작성되어 있음).

3-2. 남은 정리 작업(작은 것) ​

  • [ ] 프로바이더 카드의 키 저장 입력+버튼이 한 줄에 몰려 있음 → PortalTextArea/PortalCard 기반 재배치 검토
  • [ ] usage 필터에 드롭다운 선택지 제공(현재 자유 입력): KNOWN_PURPOSES(컴포넌트 상단 상수) 재사용, 포털 목록은 tRPC로 조회 가능한지 확인
  • [ ] 기간 프리셋(1/7/30일) 버튼 추가 여부 결정
  • [ ] 영어 로케일 확인: useLocale()로 en 전환 후 화면 문구가 모두 영어인지 점검(이번에 text() 적용했지만 실제 브라우저 확인은 미실시)
  • [ ] 다크/라이트 두 테마에서 실제 렌더 확인(이번 변경의 핵심이 테마 정리이므로 스크린샷 확인 필요)
  • [ ] 접근성: 링크형 액션(.model-gateway-action)은 <button>이라 키보드 포커스는 되지만, aria-label/아이콘만 있는 액션 문구 점검

3-3. 커밋 전 체크리스트 ​

  • [ ] npx prettier --write client/src/components/ModelGatewayDesk.tsx client/src/styles/modelGatewayDesk.css (※ HEAD 원본도 prettier warn 상태 — 프로젝트가 pnpm format을 상시 적용하지는 않는 것으로 보임. 팀 규칙 확인 후 결정)
  • [ ] npx tsc --noEmit (현재 통과) / npx vitest run / npx vite build
  • [ ] 화면 실제 확인(pnpm dev 또는 배포 URL)
  • [ ] 커밋 메시지 제안: refactor(console): 모델 게이트웨이 화면의 레이아웃·문구·테마를 데스크 관례에 맞춘다

4. 검증 현황 (이번 세션에서 실제로 돌린 결과) ​

cd /home/hohyun/projects/PortalConsole
npx tsc --noEmit               → exit 0 (타입 오류 없음)
npx prettier --check <두 파일>  → warn (HEAD 원본도 동일하게 warn → 기존 상태, 이번 변경이 만든 문제 아님)
npx vitest run                 → Test Files 74 passed | 2 skipped (76) · Tests 403 passed | 4 skipped (407) · 27.4s
npx vite build                 → ✓ built in 8.40s
                                 dist/public/assets/index-HWYMzOQ4.css 321.11 kB
                                 dist/public/assets/index-BKC3_1zi.js 1,484.42 kB
                                 (500 kB 초과 chunk 경고만 있음 — 기존과 동일한 경고)

아직 하지 않은 검증: 브라우저에서의 실제 렌더(라이트/다크, 영어 로케일), 배포 환경 확인.

자주 쓰는 명령 ​

bash
cd /home/hohyun/projects/PortalConsole
pnpm dev            # 개발 서버
pnpm check          # tsc --noEmit
pnpm test           # vitest run
pnpm build          # vite build + esbuild 서버 번들
pnpm verify         # check + build

5. 배포 경로와 주의사항 ​

  • 콘솔(이 화면이 들어가는 앱)은 Coolify 앱 directory-service:main-h3igjh2lszlqoegqinwitlyx
    • uuid h3igjh2lszlqoegqinwitlyx · fqdn https://directory.rlab.monster · build_pack railpack · branch main
    • 보고 있는 리포지토리: hohyun94/DirectoryService ← 로컬 remote(hohyun94/PortalConsole)와 다름. 푸시 대상 확인 필요
  • Coolify 서버 접근: ssh rlab (Oracle, 공인 168.138.33.231) · Coolify 4.3.23 · API http://127.0.0.1:8000/api/v1
    • 배포 트리거는 POST /api/v1/deploy, body {"uuid":"h3igjh2lszlqoegqinwitlyx","force":false} (GET은 405 This endpoint has changed to a POST request.)
    • API 토큰은 admin(권한 root) 계정으로 발급해야 하며, 앞선 세션에서 쓴 토큰은 폐기됨(personal_access_tokens 비어 있음) → 필요하면 새로 발급
    • 자동 배포(auto deploy) 설정은 확인되지 않음 → 푸시 후 Coolify에서 Redeploy 필요
  • 배포 앱에 필요한 환경변수(이미 설정됨): MODEL_GATEWAY_URL, MODEL_GATEWAY_ADMIN_TOKEN(게이트웨이 https://model-gateway.rlab.monster), PORTAL_ALLOW_INSECURE_PLATFORM_URL 등
  • 화면 확인 경로: https://directory.rlab.monster 로그인 → 관리자 콘솔 → 좌측 모델 게이트웨이 관리 탭

6. 롤백 ​

  • 이번 변경은 커밋 전이므로 가장 안전한 롤백은 그대로 되돌리기:
    bash
    cd /home/hohyun/projects/PortalConsole
    git checkout -- client/src/components/ModelGatewayDesk.tsx client/src/styles/modelGatewayDesk.css
  • CSS만 되돌리려면 이전 파일 백업이 /tmp/modelGatewayDesk.css.bak에 있습니다(세션 임시 파일이라 오래 남지 않을 수 있음 → 위 git checkout 권장).
  • 이미 배포했다면 Coolify Deployment 목록에서 직전 배포로 재배포하거나 git revert 후 재배포합니다.

7. 다음 세션에서 바로 쓰는 시작 프롬프트(복붙용) ​

포탈 컨트롤센터(/home/hohyun/projects/PortalConsole)의 '모델 게이트웨이 관리' 화면 작업을 이어서 하자.
먼저 docs/MODEL_GATEWAY_DESK_HANDOFF_2026-09-26.md 를 읽어줘.
대상: client/src/components/ModelGatewayDesk.tsx , client/src/styles/modelGatewayDesk.css
방향: 자체 div+CSS 박스 구조를 @hohyun94/portal-foundation(antd 6 래퍼) 컴포넌트로 재구성
      (PortalCard / PortalDataTable / PortalDescriptions / DataStatusBadge / OperationalMetricGrid /
       PortalConfirm / PortalEmptyState / PortalErrorState / AdminSection 등)
제약: tRPC 호출·서버 코드(server/modelGateway.ts, server/routers.ts)는 이번엔 건드리지 말고 화면만.
      reference-portal-frame.css 의 !important 규칙과 충돌하지 않는지 확인.
검증: npx tsc --noEmit / npx vitest run / npx vite build, 라이트·다크 두 테마 렌더 확인.
현재 상태: 미커밋 2개 파일 수정, tsc·vitest·build 통과, 배포 안 됨.

8. 열려 있는 질문 (사용자 결정 필요) ​

  1. 푸시 대상 리포지토리: 로컬은 hohyun94/PortalConsole, Coolify는 hohyun94/DirectoryService — 어디에 푸시하면 directory.rlab.monster에 반영되나요? (리포 이름 변경/미러 여부) → 해소(2026-09-26): 같은 리포지토리입니다. gh api repos/hohyun94/DirectoryService가 full_name: hohyun94/PortalConsole을 돌려주고 HEAD SHA도 ac2562c…로 동일합니다(GitHub 리포 이름 변경 리다이렉트). Coolify는 옛 이름으로 계속 받아도 되고, 로컬 origin(hohyun94/PortalConsole)으로 푸시하면 그대로 배포됩니다.
  2. antd 방향의 범위: 이 화면만 portal-foundation 컴포넌트로 바꾸면 다른 데스크(FileStore·Ontology 등)와 스타일이 혼재합니다. 화면 단위로 계속 전환할지, 이번엔 문구·테마 정리로 마무리할지?
  3. 영어 문안 검수: 이번에 영어 문구를 임의로 작성했습니다(예: 화면 제목 Model access is managed here.). 검수 필요 여부.
  4. 커밋/배포 시점: 이번 변경을 바로 커밋할지, 리뷰 후 커밋·배포할지.

참고: 이번 세션에서 손대지 않은 것 ​

  • 게이트웨이 리포지토리(/home/hohyun/projects/ModelGateway)와 콘솔 서버 코드(프록시·tRPC) → 변경 없음
  • 다른 데스크(FileStore·Ontology·DocumentLibrary 등)와 전역 CSS(reference-portal-frame.css, index.css) → 변경 없음

변경된 파일 목록(그대로 git status와 일치) ​

 M client/src/components/ModelGatewayDesk.tsx
 M client/src/styles/modelGatewayDesk.css
?? docs/MODEL_GATEWAY_DESK_HANDOFF_2026-09-26.md   ← 이 문서(신규)

9. 2026-09-26 2차 세션 — 자체 div+CSS 박스 → portal-foundation(antd 6) 컴포넌트 전환 (완료·미커밋) ​

§3-1의 방향 전환 제안을 실행했습니다. 화면만 바꿨고 서버 코드(server/modelGateway.ts, server/routers.ts)· tRPC 프로시저·전역 프레임 CSS(reference-portal-frame.css, navy-base.css)·다른 데스크는 손대지 않았습니다.

9-1. 치환표 ​

이전(자체 div+CSS)지금(portal-foundation)
.ops-cards 상태 카드 4개 / 사용량 합계 5개OperationalMetricGrid(+tone)
article.model-gateway-providerPortalCard(variant bordered)
프로바이더 id·kind·baseUrl·models 한 줄PortalDescriptions
.model-gateway-key-row(힌트·상태·시각·링크형 액션)PortalDataTable + DataStatusBadge + PortalButton
.model-gateway-badges span(is-on/is-off/is-default)DataStatusBadge(info/success/neutral/warning/danger)
.model-gateway-subheading(purpose·단가 소제목)PortalCard title/description
.model-gateway-usage-block + 자체 행PortalCard + PortalDataTable(모델별·일별)
window.confirmPortalConfirm(danger)
빈 목록·단가 미설정 안내 <p class="settings-security-note">PortalEmptyState
로딩 안내 줄 / 오류 안내 줄PortalLoadingState / DataStatePanel tone="danger" + 재시도 버튼
미설정 안내 <ul class="model-gateway-checklist">PortalDescriptions(env 2행)
.model-gateway-action(링크형 버튼)PortalButton tone="ghost"/"danger" size="sm"

유지(모든 데스크가 공유하는 프레임 표면): .admin-desk·.desk-intro·.admin-secondary·.secondary-heading· .settings-security-note(내부망 http 경고), 그리고 순수 레이아웃 규칙(자동 배치 폼 그리드, 편집 행 배치, 추세 막대).

9-2. CSS 축소 — modelGatewayDesk.css 117 → 46줄 ​

라이트/다크 색 블록을 전부 삭제했습니다. 색·테두리·다크 모드는 이제 --pf-* 토큰과 antd 테마 (PortalConfigProvider가 mode에 따라 default/dark 알고리즘을 고릅니다)가 처리합니다. 남긴 것은 레이아웃과 일별 추세 막대 색 2규칙뿐입니다. ⇒ 이 화면의 다크 모드 정합성은 data-portal-mode와 antd 테마가 함께 책임집니다.

9-3. 검증 (실제로 돌린 값) ​

npx tsc --noEmit              → exit 0
npx vitest run                → Test Files 74 passed | 2 skipped (76) · Tests 403 passed | 4 skipped (407) · 21.3s   (1차 세션과 동일)
npx vite build                → ✓ built in 7.94s
                                dist/public/assets/index-CiNt6vw6.css   312.87 kB (gzip 45.89)  ← 321.11 → 312.87 kB
                                dist/public/assets/index-zNY-bB67.js  1,670.93 kB (gzip 498.51) ← 1,484.42 → 1,670.93 kB
npx prettier --check <두 파일> → warn (HEAD 원본과 같은 기존 상태 — 이번 변경이 만든 문제 아님)
  • ⚠️ 번들 +186 kB: antd Table·Descriptions·Card·Tag·Alert·Popconfirm이 새로 번들에 들어옵니다(CSS는 8 kB 감소). 단일 번들이라 500 kB chunk 경고는 기존과 동일하게 남습니다.
  • 헤드리스 렌더 확인(브라우저 없는 환경): 임시 SSR 스모크 테스트로 확인하고 임시 파일 2개는 삭제했습니다 (client/src/components/ModelGatewayDesk.render.test.ts, vitest.render.config.ts). tRPC·LocaleContext만 스텁한 뒤 라이트/다크 × 한/영에서 두 분기(미설정 안내 / 운영 화면)를 renderToStaticMarkup으로 그려 — pf-panel(5)·pf-table(3)·pf-descriptions·pf-data-status-badge·pf-operational-metric(9)가 실제로 나오는 것, 영어 렌더에 남은 한글이 테스트 픽스처(OpenAI 호환)뿐인 것(화면 문구 누수 없음), 이전 클래스 흔적이 없는 것을 확인했습니다. antd Descriptions의 span/column 경고는 span 제거로 해소했습니다. 남은 경고 [antd: Button] 'iconPosition' is deprecated는 PortalButton이 항상 iconPosition을 넘겨서 생기는 라이브러리 문제입니다(이 화면만의 문제 아님).
  • 실제 브라우저 라이트/다크 스크린샷 확인은 여전히 미실시(§3-2의 남은 항목 그대로).

9-4. 다음 사람이 알아야 할 것 ​

  1. 기본 vitest.config.ts에는 React 플러그인이 없어 JSX가 classic 런타임으로 컴파일됩니다 → .tsx 컴포넌트를 직접 렌더하는 테스트는 지금 구조에서 불가(React is not defined). 컴포넌트 테스트를 상시화하려면 vitest.config.ts에 plugins: [react()]를 추가해야 합니다(이번엔 공유 설정을 건드리지 않으려고 임시 설정을 쓰고 지웠습니다).
  2. PortalDataTable의 기본 빈 문구는 한국어(데이터가 없습니다.)이고 PortalErrorState의 재시도 라벨도 한국어입니다 → 이 화면은 항상 emptyState를 넘기고, 오류는 DataStatePanel + text() 재시도 버튼을 써서 영어 누수를 피했습니다. 다른 데스크를 전환할 때도 같은 함정을 조심하세요.
  3. PortalButton의 danger는 채워진 빨간 버튼입니다(키·프로바이더 삭제에 사용). 이전 링크형 액션보다 눈에 띄므로, 톤이 과하면 tone="ghost" + PortalConfirm으로 낮추면 됩니다.
  4. 파일 크기: ModelGatewayDesk.tsx 1006 → 1110줄, modelGatewayDesk.css 117 → 46줄.
  5. 같은 방향으로 남은 화면: FileStoreDesk, OntologyDesk, DocumentLibraryDesk(모두 아직 래퍼 필드 4개 + 자체 CSS). 전환하면 데스크별 CSS의 라이트/다크 색 블록을 같은 방식으로 지울 수 있습니다.

9-5. 배포 결과 (2026-09-26) ​

  • 푸시 → Coolify 배포가 실제로 돌았습니다(자동). 커밋 cd0a69b 푸시 직후 application_deployment_queues에 배포 행이 생겨 11:48:33 시작 → 11:52:42 finished, 앱 컨테이너 이미지 태그가 커밋 SHA(h3igjh2lszlqoegqinwitlyx:cd0a69be85b6…)로 교체되고 Up (healthy)가 됐습니다. (배포 행 id 417, 앱 uuid h3igjh2lszlqoegqinwitlyx)
  • 라이브 확인: https://directory.rlab.monster/가 assets/index-DkKNNVaX.js(1,683,921 B) · assets/index-DPjCsOk8.css(313,022 B)를 서빙합니다. 배포 번들에 새 컴포넌트 마커(pf-operational-metric·pf-data-status-badge·pf-table-wrap·pf-descriptions)가 있고, 폐기한 클래스(model-gateway-key-row·-checklist·-usage-block·-purpose-editor·-subheading·-models)는 js·css 모두 0건입니다. (로컬 빌드 해시 index-zNY-bB67.js와는 해시가 다릅니다 — 빌드 환경 차이이고 크기는 1.67 MB로 같은 수준입니다.)
  • 트리거 경로 주의: 리포지토리 웹훅은 1건뿐이고(id 677001117) 그 URL이 …/api/v1/deploy?uuid=…&force=false인데 401을 돌려줍니다(토큰 없음). 그런데도 배포가 돌았으므로 실제 트리거는 Coolify의 GitHub App 이벤트(github_apps.app_id = 4892926)입니다. → 남은 정리: 401 웹훅은 지우거나 API 토큰을 붙여 고치고, Coolify API 토큰(personal_access_tokens = 0건)은 필요할 때만 admin으로 재발급합니다.
  • 이 배포 결과 문서는 로컬 커밋만 했습니다(푸시하면 문서 한 줄 때문에 4분짜리 재빌드·재배포가 돌아서). 다음 푸시에 함께 올라갑니다.

10. 2026-09-26 3차 — 히어로를 portal-foundation 표준(PortalHero)으로 교체 ​

10-1. 바꾼 것 ​

  • ModelGatewayDesk가 손으로 만들던 .desk-intro(h1 + <em> + 긴 <p>)를 **표준 PortalHero**로 교체했습니다.
    • 운영 화면: title = "모델 접근을 콘솔에서 직접 관리합니다." + 설명, 그리고 히어로 액션 슬롯(pf-portal-hero__actions)에 상태 새로고침 버튼.
    • 미설정 안내: title = "모델 접근을 한 지점에서 관리합니다." + 설명(액션 없음).
  • 데스크 CSS는 변경 없습니다(히어로 관련 규칙이 없었음 → 여전히 46줄). 프레임 CSS도 손대지 않았습니다.

10-2. 표준 경로와 설치본의 간극(다음 사람 필독) ​

portal-foundation의 표준 히어로는 셸이 nav 항목의 hero 설정으로 렌더하는 구조입니다.

  • PortalShell: activeHero(navigationItems, activeNavigationId) → PortalMain 자식으로 <PortalHero {...hero} /> (= pf-shell-main__inner 안, pf-shell-main__header 스트립 바로 아래). pf-shell-main__header는 파란 그라데이션 스트립이고, 여기 들어가는 것은 mainHeader/headerTitle 용도입니다(히어로 자리 아님).
  • 타입: PortalNavigationLeaf.hero: PortalHeroConfig = { title, description, summary?, action?: { href, label } }. PortalHero는 theme.heroVisible === false면 null을 돌려줍니다.
  • PortalAdminMenuItem.description은 "선택된 관리자 메뉴의 Hero에 표시할 설명"으로 문서화되어 있습니다.

그런데 설치본 0.6.156의 PortalAdminShell.toNavigationItem이 hero/headerTitle/description을 변환에서 누락합니다 ({ id, label, icon, href, active, onSelect }만 넘김). 그래서 관리자 콘솔에서는 nav 항목 hero 경로를 쓸 수 없습니다. 이번에는 데스크가 PortalHero를 같은 위치(본문 최상단) 에 직접 렌더해 표준 컴포넌트·표준 스타일·표준 heroVisible 동작을 그대로 쓰는 방식으로 처리했습니다. 라이브러리를 올려 toNavigationItem이 hero를 매핑하면, adminMenuItems[].description(또는 hero)만 채우고 데스크에서는 히어로를 빼면 됩니다.

  • 부작용(표준 동작): heroVisible === false면 히어로가 통째로 사라지므로 히어로 액션인 상태 새로고침도 함께 숨습니다. 현재 콘솔에는 ThemePicker가 없고 App.tsx 기본값이 heroVisible: true라 실제로는 항상 보입니다. 나중에 테마 설정 UI를 붙인다면 새로고침을 본문(지표 줄)으로 내리는 편이 안전합니다.
  • 다른 데스크(FileStoreDesk·OntologyDesk·DocumentLibraryDesk)는 아직 .desk-intro를 씁니다. 같은 방향으로 옮기면 화면 간 히어로가 통일됩니다.

10-3. 검증 ​

npx tsc --noEmit   → exit 0
npx vitest run     → 74 passed | 2 skipped (76) · 403 passed | 4 skipped (407)      (기존과 동일)
npx vite build     → exit 0 · dist/public/assets/index-BVwc3DPP.js 1,670.06 kB (gzip 498.39) · index-CiNt6vw6.css 312.87 kB
  • 헤드리스 SSR 스모크(임시 파일 2개, 확인 후 삭제): 라이트/다크 × 한/영에서 pf-portal-hero + pf-portal-hero__actions가 렌더되고 .desk-intro가 없는 것, pf-operational-metric/pf-panel/pf-table/pf-data-status-badge가 함께 나오는 것, 영어 렌더에 한글이 0건인 것, heroVisible=false에서는 히어로(과 그 액션)만 사라지고 본문은 그대로인 것을 확인했습니다.
  • 배포본 확인은 assets/index-*.js에 새 히어로 문구("모델 접근을 콘솔에서 직접 관리합니다." / "Model access is managed here.")가 있는지로 합니다.

11. 2026-09-26 4차 — 경고 안내 문구의 표준과 스케일 (결론: AdminFeedback) ​

11-1. 플랫폼의 "알림/경고" 표준 (사용자 질문에 대한 답) ​

antd의 표준은 **Alert**이고, portal-foundation이 그걸 아래 래퍼로 노출합니다(근거: portal-foundation/docs/PHASE_3_REMAINING_COMPONENT_PLAN.md:95에 FeedbackMessage | Alert, Button으로 명시).

쓰임컴포넌트실제 DOM이 콘솔에서의 판단
안내·경고 문구(+닫기)FeedbackMessage (tone, title, children, dismiss?).pf-feedback-message--<tone> > antd Alert type=… showIcon❌ 포털 스케일(래퍼 12px + Alert 14px·패딩 20×24·아이콘 24px, 박스 두 겹)
데이터 상태(오류·빈 상태·로딩)DataStatePanel (tone, title, description, action).pf-data-state-panel--<tone> > antd Alert/Spin✅ 오류·로딩에 사용 중(스케일도 무난)
액션 결과(성공/실패/진행)ActionFeedback (state)결과 문구 한 줄–
아이콘 없는 톤 문구(단일 박스)AdminFeedback (tone).pf-admin-feedback--<tone> (<p>, 12px, padding:10px 12px, antd 아님)✅ 이 콘솔 스케일과 일치 — 경고 안내에 채택

11-2. 왜 갈아탔나 — 스케일 불일치(실측) ​

이 콘솔은 reference-portal-frame.css/navy-base.css에서 foundation 셸 스타일을 !important(총 335개)로 눌러 더 작고 촘촘한 자체 섀시를 씁니다. 그래서 포털용 foundation 기본 타이포가 여기서만 커 보입니다.

요소foundation 기본콘솔 섀시
히어로 제목.pf-portal-hero__copy h2 = clamp(24px, 3vw, 38px) + letter-spacing -.065em + text-shadow.desk-intro h1 = 25px(모바일 20px), 본문 13px
경고 문구래퍼 12px + antd Alert 14px·패딩 20×24px·아이콘 24px (박스 두 겹).settings-security-note = 12~13px 한 줄 문단
본문 기준antd 토큰 기본 14px--common-content-size = 13px

11-3. 최종 결정(사용자 선택: B) ​

  • 경고 안내 → AdminFeedback tone="warning" 로 교체. 렌더 결과는 단일 <p>: <p class="pf-admin-feedback pf-admin-feedback--warning" role="status"><strong>내부망 http 허용이 켜져 있습니다</strong> PORTAL_ALLOW_INSECURE_PLATFORM_URL — …</p> → antd Alert 없음(두 겹 박스 없음), 12px, 아이콘 없음. settings-security-note·pf-feedback-message·ant-alert 모두 이 화면에서 0건.
  • 히어로는 foundation 기본(38px) 유지. 콘솔 25px로 낮추는 오버라이드(A안)는 하지 않기로 함. 나중에 "히어로가 크다"는 판단이 바뀌면 .directory-admin-content .pf-portal-hero__copy h2 { font-size:25px; letter-spacing:-.03em; text-shadow:none } 처럼 스코프 오버라이드(선택자 특이도로 이김, !important 불필요)로 낮출 수 있습니다.
  • d3489c6(FeedbackMessage 버전)은 이미 배포됐고, 이 변경은 아직 미커밋·미푸시입니다(사용자 지시). 다음 푸시에 함께 올라갑니다.

11-4. 같은 배치에서 반영한 시각 조정(사용자 피드백) ​

modelGatewayDesk.css에만 추가했고, 전부 이 화면(.model-gateway-desk/.model-gateway-cards) 스코프입니다.

피드백조치(규칙)근거
지표 카드 위 초록 강조 띠가 불필요.model-gateway-cards .pf-operational-metric{border-top-width:1px;border-top-color:var(--pf-border-subtle)}foundation 기본이 .pf-operational-metric{padding:13px;border-top-width:3px} + 톤별 border-top-color(success=#43835d)를 그림
카드 제목이 작다.model-gateway-cards .pf-operational-metric>span{font-size:12px} (힌트 small은 10px, 값 strong은 17px 유지)기본 라벨 10px
섹션마다 테두리·면이 불필요.model-gateway-desk .admin-secondary{border:0} + 라이트/다크 각각 background:transparent!important(:root[data-portal-mode="light"] … / :root:not([data-portal-mode="light"]) …)테두리 베이스는 index.css:147의 .admin-secondary{…border:1px solid #d6d0c5;background:#fffdf8}. 채움은 라이트에서 프레임이 background … !important(특이도 0,4,0)로, 다크에서 navy-base가 --directory-navy-800(#0E365C)로 준다 → 다크(캔버스 슬레이트 ≈#2c3646)에서 큰 파란 박스로 보임. :root[...]로 특이도 (0,5,0) + !important로 덮음
섹션 제목의 아이콘 불필요 + 수직정렬 어긋남5개 섹션 제목에서 아이콘 제거(<ServerCog/>·<Plus/>·<Save/>·<RefreshCw/>·미설정 안내의 <CircleAlert/>), .model-gateway-desk .secondary-heading h2{margin:0 0 6px;line-height:1.35}프레임 기본 .secondary-heading h2{margin:9px 0 5px}(비대칭) + 인라인 svg가 텍스트 베이스라인에 붙어 어긋나 보임. ServerCog는 이 화면에서 유일한 사용처라 import에서도 제거
  • 두 지표 그리드(상태 4개·사용량 5개)가 같은 .model-gateway-cards를 써서 함께 적용됩니다. 다른 데스크의 .admin-secondary는 그대로입니다.
  • 섹션의 배경 채움과 제목 스트립(.secondary-heading)의 하단 구분선은 남겨 뒀습니다(요청은 테두리만). 필요하면 각각 한 줄로 더 걷어낼 수 있습니다.
  • 섹션 제목 띠(.secondary-heading)도 열린 제목으로 바꿨습니다: 채움 제거(라이트·다크 각각 background:transparent!important) + padding:0 0 12px(제목을 내용과 같은 x축으로 — 프레임 기본 20px 패딩 때문에 20px 안쪽으로 밀려 있었다) + margin-bottom:16px, 하단 1px 구분선은 유지(프레임이 테마별 색을 준다). 섹션 사이 간격은 28px(margin-top, 프레임 기본 14px).
  • 검증: vite build 0, 빌드 산출물(index-BuxqOdi7.css)에 admin-secondary>.secondary-heading 패딩·배경 규칙과 margin-top:28px!important 존재 확인.

12. 2026-09-26 5차 — 현황판 아래 세부 + 프로바이더 카드 클릭 편집·추가 레이어 팝업 (구조 개편) ​

사용자 요청: ① 현황판(4개 지표 박스) 아래에 현황별 세부내용이 있어야 하는데 게이트웨이 ON에 대한 정보가 없음 ② 프로바이더는 복수일 수 있으니 등록 현황을 카드(또는 목록)로 ③ 카드/행을 누르면 편집이 레이어 팝업 ④ 프로바이더 추가도 버튼 → 등록 레이어 팝업.

12-1. 바꾼 구조 ​

위치이전지금
현황판 아래곧바로 프로바이더 섹션게이트웨이 상세 카드(PortalCard + PortalDescriptions 8필드): 상태(ON 배지)·주소·업스트림·배포 프로필·키 암호화·등록된 프로바이더 집계·등록된 API 키 수·등록된 모델 수
프로바이더카드 목록(편집은 본문 인라인 폼)카드 목록 유지 + 카드를 누르면 편집 팝업, 섹션 헤딩과 빈 상태에 프로바이더 추가 버튼(둘 다 같은 팝업)
추가·편집 폼본문 인라인 admin-secondary 섹션PortalDialog(size="lg") 레이어 팝업 — 같은 필드 그리드(.model-gateway-form) + 하단 취소/저장
키 관리카드 안그대로 카드 안(운영 동작이라 팝업 대신 즉시 처리). 카드 클릭과 충돌하지 않게 model-gateway-provider-keys로 감싸 stopPropagation

12-2. 구현 메모(라이브러리 제약) ​

  • PortalDialog(0.6.156): getContainer={false}라 포털이 아니라 인라인 렌더 → 기존 .directory-admin-content 스코프 CSS가 팝업 안에서도 그대로 적용됩니다(추가 CSS 불필요). open이 false면 null을 반환하고, 모달은 useEffect로 마운트된 뒤에 열립니다 → SSR 스모크에서는 오버레이 div만 렌더되고 팝업 내용은 브라우저에서만 보입니다(그래서 아래 검증도 그 범위).
  • PortalCard의 interactive/onClick prop은 0.6.156에서 동작하지 않습니다: PortalPanel이 알려진 prop만 쓰고 나머지를 <section>에 전달하지 않기 때문입니다(onClick이 버려짐). 그래서 카드 클릭은 래퍼 div(role="button" + tabIndex + Enter/Space 처리) 로 구현했습니다. 라이브러리가 prop을 전달하도록 고쳐지면 래퍼를 없앨 수 있습니다.
  • 팝업을 닫으면 편집 상태를 비웁니다(closeProviderDialog): 취소·ESC·오버레이 클릭·X 모두 onOpenChange(false) → 같은 정리 경로를 탑니다.
  • 저장 성공 시 saveProvider.onSuccess에서 팝업을 닫습니다.

12-3. 알려진 한계 / 남은 결정 ​

  • 팝업의 시각적 확인은 브라우저가 필요합니다(SSR은 오버레이까지만). 현재 콘솔은 PortalDialog의 첫 사용처입니다.
  • 카드 클릭 영역은 헤더·식별정보 표이며, 키 관리 영역은 제외했습니다. 카드 전체(여백 포함)를 클릭 영역으로 넓히려면 래퍼 클릭 + 내부 컨트롤 stopPropagation을 더 넓히면 됩니다.
  • 래퍼 도입으로 그 카드 블록의 JSX 들여쓰기가 한 단계 얕습니다(prettier 미적용 저장소라 그대로 둠).
  • 섹션 구조(실행 설정·사용량·비용)는 그대로입니다. 세부내용을 더 늘리려면(예: 키 암호화 상세, 사용량 요약을 현황판 아래로) 알려 주세요.

13. 2026-09-26 6차 — 조치 불가 상태 항목 정리: 키 암호화는 미설정일 때만 경고 ​

13-1. 사실 확인(사용자 질문: "키 암호화는 관리자가 뭘 할 게 있나?") ​

  • masterKeyConfigured는 읽기 전용입니다. 값은 게이트웨이 배포 env MODEL_GATEWAY_CONFIG_MASTER_KEY에서 오고 (ModelGateway/src/admin/routes.ts:63 → Boolean(config.configMasterKey)), 콘솔 tRPC에는 이 값을 바꾸는 프로시저가 없습니다 (gateway.status/state/saveProvider/deleteProvider/addKey/setKeyEnabled/deleteKey/verifyProvider/saveSettings/usage뿐).
  • 그럼에도 의미가 있었던 이유: 미설정이면 게이트웨이가 키를 암·복호화할 수 없어 API 키 저장 자체가 실패합니다 (ModelGateway/src/configStore.ts:51 → "provider 키를 저장하려면 MODEL_GATEWAY_CONFIG_MASTER_KEY가 필요합니다."). 즉 "지금 키를 추가해도 되는 상태인가"를 알려주는 준비 상태 신호였습니다.

13-2. 결정(사용자 선택: A) 과 구현 ​

  • 정상(masterKeyConfigured: true)이면 현황판 타일·게이트웨이 상세 필드 모두 숨김 → 현황판은 3개(게이트웨이·프로바이더·배포 프로필).
  • 미설정일 때만 경고 배너(AdminFeedback tone="warning") 로 노출하며, 게이트웨이 env 이름과 "콘솔에서 바꿀 수 없음"을 함께 적었습니다.
  • 경고 두 종류(내부망 http · 마스터 키 미설정)를 .model-gateway-notices(grid, gap:8px, margin-bottom:14px)로 묶어 겹칠 때 간격을 유지합니다.
  • 남겨 둔 것: 배포 프로필·업스트림 구성도 같은 성격(게이트웨이 env, 콘솔에서 조치 불가)이지만 운영 맥락(어느 배포로 도는지)에 유용해 현황판에 남겼습니다. 같은 기준으로 정리하려면 알려 주세요.

13-3. 검증 ​

  • 헤드리스 스모크 2/2 통과: ① 정상 상태 → 마크업에 키 암호화·AES-256-GCM·env 이름 0건, 현황판 타일 3개 ② 미설정 상태 → 경고 배너 + env 이름 노출, 타일은 여전히 3개.
  • tsc --noEmit · vitest run · vite build 결과는 이 커밋의 게이트 로그와 같습니다(아래 §14에 통합 기록).

14. 2026-09-26 7차 — 배포 프로필을 콘솔에서 설정(게이트웨이 + 콘솔 서버/화면) ​

사용자 지적: "배포 프로필도 클라우드인지 아닌지 여기서 설정하면 안 돼? 환경변수값 읽어오는 것 같은데" → 정확했습니다. 다만 저장 경로가 없던 값이라, 게이트웨이·콘솔 서버·화면을 함께 고쳐 콘솔에서 설정할 수 있게 했습니다.

14-1. 게이트웨이 (/home/hohyun/projects/ModelGateway, 커밋·푸시 대기) ​

  • configStore.ts: RuntimeSettings.deploymentProfile: DeploymentProfile | null 추가 + SETTING_KEYS 포함 → PUT /admin/api/settings로 저장되고 DB(model_gateway_settings key/value)에 영속한다(스키마 변경 불필요).
  • routing.ts: EffectiveSettings.deploymentProfile = stored ?? config(env 시드). resolveTarget(..., deploymentProfile)가 실효값으로 allowFallback을 정한다 (CLOUD = 레거시 폴백 허용 / ON_PREM = 금지).
  • admin/routes.ts: /state가 실효 프로필을 내려주고, PUT /settings는 CLOUD|ON_PREM만 받는다(그 외 400, null은 저장값 제거 → env 시드로 복귀).
  • server.ts: 감사 기록(401·429·성공)과 /v1/health가 실효 프로필을 쓴다.
  • 테스트 추가(tests/resilience.test.ts): env 시드가 CLOUD여도 저장값 ON_PREM이면 폴백이 막히는지, 잘못된 값이 400인지, /state가 ON_PREM을 돌려주는지.
  • README: 관리자 API 목록에 배포 프로필 추가 + "env는 시드, 저장값 우선, ON_PREM은 외부 폴백 금지" 문단.

14-2. 콘솔 ​

  • 서버: ModelGatewaySettings.deploymentProfile 추가, tRPC gateway.saveSettings 입력에 z.enum(["CLOUD","ON_PREM"]).nullable().optional().
  • 화면(실행 설정): 배포 프로필 PortalSelect — CLOUD — 레거시 폴백 허용 / ON_PREM — 외부 폴백 금지 + 라우팅 영향 설명. 편집값이 적용값과 다르면 저장 버튼이 PortalConfirm으로 확인을 받는다(폴백 동작이 바뀌는 값이라서). 폼 시작값은 저장값 > 실효값(env 시드) 순(다른 설정과 같은 규칙).
  • 현황판·게이트웨이 상세 카드의 '배포 프로필'은 이제 저장값을 반영한다(게이트웨이 /state가 실효값을 내려주므로 클라이언트 추가 변경 없음).

14-3. 배포 순서 / 주의 ​

  1. 게이트웨이 먼저 배포(새 필드 수용) → 그 다음 콘솔. 반대여도 콘솔은 필드를 보내고 구게이트웨이가 화이트리스트로 무시하지만, 순서를 지키는 편이 안전하다.
  2. 게이트웨이 컨테이너 확인 결과 DATABASE_URL=set · DEPLOYMENT_PROFILE=CLOUD → 저장값이 재시작 후에도 유지된다(메모리 저장소 구성이면 재시작 시 env 시드로 복귀).
  3. ON_PREM으로 바꾸면 업스트림 장애 시 외부 폴백 URL로 넘기지 않는다(502가 그대로 노출 — 의도된 동작).
  4. 두 리포 모두 아직 미커밋·미푸시입니다(사용자 지시 대기).

14-5. 실행 설정 섹션 요약 타일(같은 세션 추가분) ​

사용자 지적: "실행설정이나 사용량비용 섹션도 섹션이 있으면 위에 요약타일이 있어야하지 않나?" → 섹션 패턴(요약 타일 → 세부)을 맞추되, 저장값이 아니라 실효값을 보여주기 위해(env 시드가 적용 중이면 저장값이 없어 "0/미설정"으로 잘못 보이는 문제) 게이트웨이 /state에 effective 블록을 추가하고 그 값으로 타일을 채웠다.

  • 게이트웨이: /state.effective = effectiveSettings(config, stored)의 요약(기본·허용 모델, purpose 매핑, 분당 한도, 쿼터 예약, 타임아웃, 배포 프로필). 기존 pricing(실효 단가표)과 같은 방식이다. 테스트(tests/resilience.test.ts)에 "저장값이 시드를 덮은 실효값이 내려온다"는 단언을 추가했다.
  • 콘솔 화면(실행 설정): 섹션 헤딩 바로 아래에 OperationalMetricGrid 7개 타일 — 기본 모델 / 허용 모델(전체 or n개) / purpose 매핑 n개 / 단가 override n개(힌트: 실효 단가표 n개 모델) / 분당 토큰 한도(0=제한 없음) / 요청 타임아웃(초) / 배포 프로필(CLOUD·ON_PREM). 타일 값은 state.effective(없으면 저장값) → 편집 초안이 아니라 적용 중인 값이다.
  • 사용량·비용 섹션은 이미 자체 5개 타일(요청·실패·토큰·평균 지연·금액)이 있어 그대로 두었다(필터 → 타일 → 분해 순서 유지). 타일을 필터 위로 올리는 정리는 아직 하지 않았다.
  • 검증: 콘솔 헤드리스 스모크 1/1(effective 픽스처 값으로 7개 타일 값·힌트 렌더 확인) + 아래 게이트.
게이트웨이: npx tsc --noEmit -p tsconfig.json → 0 · npx vitest run → 8파일/75건 통과 · npx tsc -p tsconfig.json(빌드) → 0
콘솔:      npx tsc --noEmit → 0 · npx vitest run → 76파일/403건 통과 · npx vite build → 0
          (index-DApVOAbW.js 1,675.85 kB · index-COHn970v.css 313.69 kB)
헤드리스 스모크 2/2: 실행 설정에 `gateway-deployment-profile` select + 설명 렌더, 저장 액션 유지, 저장값 ON_PREM이 마크업에 반영

15. 2026-09-26 8차 — IA 재배치: "라우팅·요금"은 프로바이더 하위, 나머지는 "게이트웨이 운영 설정" ​

사용자 지적 — "실행설정은 프로바이더의 하위항목같은거 같은데". 맞습니다. 설정 항목을 성격으로 나눠 섹션을 갈랐습니다(사용자 선택: A안).

항목성격새 위치
기본 모델 · 허용 모델 · purpose 매핑 · 단가 override등록된 모델/프로바이더로 라우팅·요금프로바이더와 API 키 섹션의 하위 블록 "라우팅·요금"(h3)
분당 토큰 한도 · 쿼터 예약 · 요청 타임아웃 · 배포 프로필게이트웨이 전역 파라미터새 섹션 "게이트웨이 운영 설정"
  • 저장 버튼 2개(같은 폼 상태 공유 — 게이트웨이 API는 PUT /admin/api/settings 부분 갱신): submitRoutingSettings(기본/허용 모델·purpose·단가만 전송) · submitOperationsSettings(한도·쿼터·타임아웃·프로필만 전송, 프로필이 바뀌면 PortalConfirm 확인). 기존 submitSettings(8개 전부 전송)는 제거했다.
  • 요약 타일도 따라 이동: 라우팅·요금 4개(기본 모델·허용 모델·purpose 매핑·단가 override) / 운영 설정 3개(분당 한도·타임아웃·배포 프로필).
  • 하위 위계 표현(이 화면 스코프 CSS): 하위 블록 제목은 h3 16px — 섹션 제목(19px + 하단 구분선)과 구분. 블록 위 간격 20px(섹션 간 28px). 선택자 .model-gateway-subsection, 라우팅 섹션은 .model-gateway-routing-section(= margin-top:20px!important, 프레임 규칙과 동률을 피하려고 :root 접두).
  • 히어로 문구도 "…프로바이더·API 키·라우팅/요금·운영 설정은…"으로 갱신(옛 "실행 설정" 표현 제거).
  • 구현 방식: 섹션을 통째로 옮기지 않고 섹션 경계만 재조정 — 라우팅 섹션의 secondary-heading을 하위 소제목으로 바꾸고, 전역 필드(프로필·한도·쿼터·타임아웃)만 새 섹션으로 옮겼다(큰 purpose/단가 카드는 그대로). DOM 순서는 "프로바이더 카드(→ 서브 블록) → 운영 설정 → 사용량·비용".
  • 검증: 헤드리스 스모크 1/1 — 섹션 순서(프로바이더와 API 키 < 라우팅·요금 < 게이트웨이 운영 설정), <h3>라우팅·요금</h3>, 블록별 필드(gateway-default-model·gateway-allowed-models·gateway-purpose-known·gateway-pricing-known / gateway-deployment-profile·gateway-per-minute·gateway-quota-reserve·gateway-timeout), 저장 라벨("라우팅·요금 저장"·"운영 설정 저장"), <h2>실행 설정</h2> 부재. (임시 vitest.render.config.ts + SectionSplitSmoke.render.test.ts는 확인 후 삭제 — 기본 vitest.config.ts에는 React 플러그인이 없다.)
콘솔: npx tsc --noEmit → 0 · npx vitest run → 76파일/403건 통과 · npx vite build → 0
      (index-B9uS07OH.js 1,679.02 kB · index-CJt5JO_A.css 314.82 kB)

16. 2026-09-26 9차 — 버튼 아이콘 통일 + 크기 고정 ​

사용자 지적 — "버튼이 아이콘이 있는게 있고 없는게 있고 일관성이 없어. 아이콘이 있는건 좋은데 아이콘의 크기는 또 너무커." 원인은 두 가지였다.

  1. 전달 경로가 섞여 있었다 — 히어로 새로고침만 PortalButton의 icon prop을 쓰고, 나머지 버튼은 아이콘을 자식(<Plus /> {텍스트})으로 넣었다. 자식으로 넣으면 antd가 .ant-btn-icon으로 감싸지 않아 간격·정렬이 제각각이고, 아이콘이 라벨 흐름에 그대로 섞인다.
  2. lucide 기본 크기(24px) — PortalButton(antd 경로)에는 아이콘 크기 규칙이 없다. 프레임워크의 아이콘 규칙은 레거시 버튼(.pf-button:not(.ant-btn) .pf-button__icon, foundation dist styles.css:733–734)에만 있어서 antd 경로에서는 svg가 24px로 그려진다. 데스크 CSS에 .model-gateway-key-hint svg{14px} 패치가 있던 것도 같은 이유다.

수정:

  • 이 화면 모든 버튼을 icon prop 경로로 통일하고, 아이콘이 없던 버튼(연결 테스트·편집·조회·다시 시도·취소·키 사용/중지·발견 모델 반영)에 아이콘을 채웠다.
버튼아이콘버튼아이콘
상태 새로고침 · 다시 시도(2곳)RefreshCw목적 추가 · 단가 행 추가(2곳) · 프로바이더 추가(2곳)Plus
키 저장KeyRound삭제 — 프로바이더·키·목적·단가Trash2
연결 테스트PlugZap저장 — 라우팅·요금 · 운영 설정 · 프로바이더Save
편집Pencil발견 모델 반영Download
키 사용/중지Power조회(사용량)Search
취소(프로바이더 팝업)X
  • 크기 고정(화면 스코프 CSS): .pf-button .ant-btn-icon svg{width:16px;height:16px!important} · 작은 버튼(.ant-btn-sm) 14px. 프레임워크가 svg 크기를 잡아 주지 않으므로(위 2번) 이 화면에서 정한다 — 특이도 (0,5,1)+!important라 프레임·섀시 규칙보다 우선한다.
  • 사용량 표의 안내 문구도 §15 재배치에 맞춰 "실행 설정" → "라우팅·요금 설정"으로 바꿨다.
  • 검증: 헤드리스 스모크 2/2 — (a) 렌더된 버튼 전부가 .ant-btn-icon 안에 svg를 갖는다(아이콘 없는 버튼 0 · 자식 아이콘 0), (b) 스타일시트에 16px/14px 규칙이 있다. 임시 파일은 확인 후 삭제.
  • 남은 것: PortalConfirm 팝오버 내부 확인/취소 버튼은 프레임워크가 그려 아이콘이 없다(컴포넌트 개선 필요). antd 6은 iconPosition을 폐기 예정으로 경고한다 (PortalButton이 그대로 넘긴다 — 프레임워크에서 iconPlacement로 매핑하면 사라진다). 아이콘 없는 버튼이 남아 있는 다른 데스크(FileStore·Ontology·DocumentLibrary)도 같은 정리가 필요하다.
콘솔: npx tsc --noEmit → 0 · npx vitest run → 76파일/403건 통과 · npx vite build → 0
      (index-AlJf7gIz.js 1,680.25 kB · index-V9eY9lZ0.css 315.05 kB)
변경 파일: client/src/components/ModelGatewayDesk.tsx · client/src/styles/modelGatewayDesk.css · docs(이 문서)

17. 2026-09-26 10차 — 버튼 표준을 프레임워크로 올림 (portal-foundation 0.6.159) ​

사용자 지적 — "버튼의 폰트크기나 아이콘의 크기 버튼의 세로높이 등은 표준에 맞춰서 포탈안에서 다 통일이 되었으면 좋겠는데." §16은 이 화면만 CSS로 맞춘 상태라 화면마다 값이 갈릴 여지가 있었다. 표준을 프레임워크 한 곳에 두고 콘솔이 새 빌드를 vendoring하는 방식(A안)으로 확정했다.

표준 ​

크기높이폰트아이콘좌우 패딩
md(기본)36px13px 60016px14px
sm30px12px 60014px10px
lg42px13px 60018px18px
  • 아이콘-라벨 간격 7px · radius 8px(--pf-radius-sm) · 아이콘은 항상 PortalButton의 icon prop.

왜 갈렸나 (조사 결과) ​

  • buildPortalAntdTheme가 controlHeight: 36·borderRadius: 8·색만 정하고 폰트는 antd 기본 14px였다 → 본문 13px와 어긋남.
  • antd 파생값은 sm 27px·lg 44px이라 프레임워크 레거시 버튼(sm 30·lg 42)과 어긋남.
  • 아이콘 규칙은 레거시 버튼 전용(.pf-button:not(.ant-btn) .pf-button__icon) → antd 경로(lucide)는 24px 그대로.
  • PortalConfigProvider가 theme prop을 Omit해 포털이 fontSize·controlHeightSM을 넘길 수 없다 → 표준은 프레임워크에 있어야 한다.

프레임워크 변경 (0.6.158 → 0.6.159, /home/hohyun/projects/portal-foundation) ​

  • packages/ui-react/src/providers/antdTheme.ts: controlHeightSM = controlHeight − 6·controlHeightLG = controlHeight + 6(±6 파생, controlHeight만 바꾸면 비율 유지) + components.Button = { iconGap: 7, contentFontSize: 13, contentFontSizeSM: 12, contentFontSizeLG: 13, paddingInline: 14, paddingInlineSM: 10, paddingInlineLG: 18 }. antd 컴포넌트 토큰으로만 준다 — 같은 값을 CSS로 덮으면 antd가 런타임에 주입하는 스타일과 순서 경쟁이 생긴다.
  • packages/ui-react/styles/styles.css: .pf-button.ant-btn .ant-btn-icon svg → md 16px · sm 14px · lg 18px (+display:inline-flex). 토큰에 없는 값(자체 svg 크기를 가진 lucide)만 CSS가 담당한다.
  • packages/ui-react/src/providers/antdTheme.test.ts: 표준값 + ±6 파생 단언 2건 추가(총 5건).
  • 버전: 루트·packages/ui-react 0.6.159 → npm pack → hohyun94-portal-foundation-0.6.159.tgz(142.8 kB). 검사: tsc -p tsconfig.build.json 0 · vitest run 0 · npm run build 0(dist/version.js = 0.6.159).
  • ⚠️ 프레임워크 저장소 변경은 아직 커밋 전이다. 다른 포탈(Process Center 등)이 이 표준을 받으려면 커밋 + GitHub Packages 게시 후 각 포탈이 버전을 올려야 한다(콘솔은 tgz vendoring이라 게시 없이 반영됨).

콘솔 반영 ​

  • package.json: file:vendor/hohyun94-portal-foundation-0.6.159.tgz(0.6.156 → 0.6.159). 이번 vendoring으로 0.6.157·0.6.158 변경도 함께 들어온다(둘 다 릴리스 커밋).
  • pnpm-lock.yaml: 6곳을 손으로 맞췄다(importer specifier/version · packages 엔트리 key/integrity/version · snapshots key). integrity는 tgz의 sha512-base64를 계산해 넣었고 npm pack이 보고한 값과 일치 확인(sha512-/kT3Ed3WsiZxG…qn6GNf/1VdyWQ==).
  • modelGatewayDesk.css: §16에서 넣은 화면별 아이콘 크기 규칙 삭제 — 이제 프레임워크가 정본이고 화면별 예외를 두지 않는다(주석으로 표준 위치만 남김).
  • 새 상시 테스트 client/src/lib/vendoredButtonStandard.test.ts: vendored dist에 표준(토큰 5종 + 아이콘 규칙 3종)이 들어 있는지 확인한다 — 버전만 올리고 tgz를 안 바꾼 경우를 잡는다.
  • 로컬 검증: npx tsc --noEmit 0 · npx vitest run 0(76파일/404건) · npx vite build 0(index-U1s_GTot.css 316.31 kB · index-DzXskJAA.js 1,681.38 kB).

⚠️ 로컬 설치는 npm, CI는 pnpm (사고 기록) ​

  • 이 작업 중 pnpm install --offline을 돌렸더니 npm이 설치해 둔 node_modules를 node_modules/.ignored로 옮기고 실패했다(오프라인 스토어에 playwright-core 없음). 즉시 되돌려 복구했다(항목 510개 · .bin 41개 · 프레임워크 0.6.156 → 이후 tgz를 풀어 0.6.159로 교체).
  • pnpm install --lockfile-only --offline도 실패한다(ERR_PNPM_NO_OFFLINE_META — 메타 캐시에 @builder.io/vite-plugin-jsx-loc 없음) → 락파일은 수동 갱신했다.
  • 저장소는 packageManager: pnpm@10.4.1 + pnpm-lock.yaml이고 CI는 pnpm으로 설치한다. 로컬 개발 트리에서는 pnpm install(전체 설치)을 돌리지 말 것 — 의존성을 바꿀 때는 tgz 직접 반영 또는 --lockfile-only(네트워크 되는 곳)만 쓴다.