통합 탐색·답변 온보딩 — 한 곳에서 켜는 체크리스트
목적: "모든 포털 상단 검색이 같은 검색·같은 답을 준다"를 배포에 켜는 데 필요한 등록·env·확인을 한 곳에 모은다. 관련 문서: docs/delegation-token-contract.md(위임 발급 계약) · ExploreGateway/docs/adapter-onboarding.md(어댑터 계약 §3-2) · ModelGateway/docs/PURPOSE_REGISTRY.md(rag.answer 목적).
0. 역할 (누가 무엇을 소유하는가)
| 서비스 | 역할 | 지켜야 할 것 |
|---|---|---|
| PortalConsole (원장) | 사용자·조직·등급, 포털 등록, 서비스/위임 토큰 발급 | 소비자 목록과 탐색 카탈로그의 정본 |
| ExploreGateway | 검색·답변 오케스트레이션(답을 만들지 않는다) | 카탈로그를 읽고, 각 포털 어댑터를 서비스 토큰으로 호출 |
RagService (rag-center) | 유일한 answer provider + 문서 색인 | 게이트웨이 서비스 토큰 + 사용자 위임 토큰으로 호출됨, RLS 직접 강제 |
| 소비 포털 (PC·AC·DL·GC) | 자기 색인 검색 + 상단 검색 UI | 답변은 게이트웨이에 위임(createExploreGateway), 검색은 자기 색인 |
| ModelGateway | 모델 접근 단일 창구 | RagService 가 x-purpose: rag.answer 로 경유 |
1. PortalConsole (원장)
1-1. 위임 소비자 등록 — PORTAL_CONSOLE_TOKEN_CONSUMERS_JSON
json
[
{ "clientId": "process-center", "clientSecret": "<PC 시크릿>", "audience": "ui-center-theme",
"scopes": ["theme:catalog:read", "theme:policy:read", "theme:policy:write"],
"delegationAudiences": ["rag-center", "explore-gateway", "directory-user"],
"delegationScopes": ["rag:read", "rag:sync", "rag:admin", "search:read"] },
{ "clientId": "agent-center", "clientSecret": "<AC 시크릿>", "audience": "ui-center-theme", "scopes": [],
"delegationAudiences": ["explore-gateway"], "delegationScopes": ["search:read"] },
{ "clientId": "document-lib", "clientSecret": "<DL 시크릿>", "audience": "ui-center-theme", "scopes": [],
"delegationAudiences": ["explore-gateway"], "delegationScopes": ["search:read"] },
{ "clientId": "guide-center", "clientSecret": "<GC 시크릿>", "audience": "ui-center-theme", "scopes": [],
"delegationAudiences": ["explore-gateway"], "delegationScopes": ["search:read"] }
]- 각
clientSecret은 그 포털 서버 env 와 같은 값이다:PROCESS_CENTER_SERVICE_CLIENT_SECRET·AGENT_CENTER_SERVICE_CLIENT_SECRET·DOCUMENT_LIBRARY_SERVICE_CLIENT_SECRET·GUIDE_CENTER_SERVICE_CLIENT_SECRET. delegationAudiences·delegationScopes밖의 요청은 거절된다 — 위임은 좁히기만 한다.- 개발 스택 값은
ProcessCenter/docker-compose.dev.yml의portal-console서비스에 있다(4개 소비자 등록 포함).
1-2. 탐색 카탈로그 — PORTAL_EXPLORE_CATALOG_JSON
json
{
"process-center": { "capabilities": ["search"], "endpoint": "/api/explore/v1/search", "timeoutMs": 1500 },
"agent-center": { "capabilities": ["search"], "endpoint": "/api/explore/v1/search", "timeoutMs": 1500 },
"document-library": { "capabilities": ["search"], "endpoint": "/api/explore/v1/search", "timeoutMs": 1500 },
"guide-center": { "capabilities": ["search"], "endpoint": "/api/explore/v1/search", "timeoutMs": 1500 },
"rag-center": { "capabilities": ["search", "answer"], "endpoint": "/api/explore/v1/search", "answerEndpoint": "/api/explore/v1/ask", "timeoutMs": 8000 }
}answer능력은rag-center하나만 갖는다 — provider 가 둘이면 질문마다 답이 달라진다.rag-center의search어댑터는 문서 결과를kind=document로 돌려줘 상단 검색 결과에 합류한다.
1-3. 포털 등록 — portal_configurations
- 소비 포털 4개 +
rag-center+explore-gateway를 등록한다. 각 포털의baseUrl은 게이트웨이가 실제로 호출하는 주소이므로 HTTPS(개발은 localhost 허용)로 열려 있어야 한다. explore-gateway는 UI 가 없으므로applicationType=PLATFORM_SERVICE로 등록한다(카탈로그portals[]에 나타나지 않게).- 게이트웨이가 컨트롤플레인을 읽으려면
directory:readscope 의 포털 API 키가 필요하다(아래CONTROL_PLANE_API_KEY).
2. ExploreGateway (오케스트레이터)
| env | 값 |
|---|---|
CONTROL_PLANE_ORIGIN | PortalConsole 오리진 |
CONTROL_PLANE_API_KEY | directory:read 포털 API 키 |
PORTAL_CONSOLE_JWT_SECRET | PortalConsole 과 같은 서명키 — 위임 토큰 검증 |
PORTAL_CONSOLE_JWT_AUDIENCE | explore-gateway (포털이 요청하는 audience 와 같아야 한다) |
PORTAL_SERVICE_TOKENS_JSON | 아래 |
PORTAL_DEFAULT_TIMEOUT_MS | 기본 어댑터 타임아웃(선택) |
json
{ "process-center": "<PC 어댑터 토큰>", "agent-center": "<AC 어댑터 토큰>",
"document-library": "<DL 어댑터 토큰>", "guide-center": "<GC 어댑터 토큰>",
"rag-center": "<RagService 어댑터 토큰>" }- 각 값은 그 포털의
EXPLORE_SERVICE_TOKEN/ RagService 의RAG_EXPLORE_GATEWAY_API_KEY와 같은 값이다. - 위임 audience 교정: 게이트웨이가 받는 토큰은 포털이 재위임한 것이라
aud=explore-gateway다(directory-user가 아니다).
3. RagService (rag-center)
| env | 값 |
|---|---|
RAG_EXPLORE_GATEWAY_API_KEY | 게이트웨이 PORTAL_SERVICE_TOKENS_JSON["rag-center"] 와 같은 값. 비우면 어댑터 경로가 503(정보 노출 방지) |
PORTAL_CONSOLE_JWT_AUDIENCE | rag-center,explore-gateway — 게이트웨이가 재위임한 토큰도 받아야 한다 |
PORTAL_CONSOLE_JWT_SIGNING_KEY · PORTAL_CONSOLE_JWT_ISSUER | PortalConsole 과 동일 |
MODEL_GATEWAY_URL · MODEL_GATEWAY_SERVICE_TOKEN | 설정하면 로컬 vLLM 대신 게이트웨이 경유(x-purpose: rag.answer). ModelGateway 에 rag.answer 모델 매핑 필요 |
ModelGateway rag.answer 매핑 등록(실측 절차)
매핑이 없으면 게이트웨이는 defaultModel 로 폴백한다 — 답변은 오지만 의도한 모델 고정이 아니고, 목적별 사용량·쿼터 집계가 rag.answer 로 잡히지 않는다. 저장된 설정(DB model_gateway_settings)이 env(MODEL_GATEWAY_PURPOSE_MODELS)보다 우선하므로, 이미 떠 있는 게이트웨이는 관리 API 로 저장한다.
bash
# 1) 매핑 저장 — PUT 은 실행 설정 '전체'를 교체한다. 기존 키(예: process.version-summary)를 함께 넣지 않으면 지워진다.
curl -X PUT -H "Authorization: Bearer $MODEL_GATEWAY_ADMIN_TOKEN" -H 'content-type: application/json' \
-d '{"defaultModel":"deepseek-flash","allowedModels":["deepseek-flash","deepseek-v4-pro"],
"purposeModels":{"rag.answer":"deepseek-flash","process.version-summary":"deepseek-flash"}}' \
https://model-gateway.rlab.monster/admin/api/settings
# 2) 확인 — purposeModels 에 rag.answer 가 보여야 한다(게이트웨이는 서비스 토큰으로 응답).
curl -H "Authorization: Bearer $MODEL_GATEWAY_SERVICE_TOKEN" https://model-gateway.rlab.monster/v1/models
# 3) 게이트웨이 단독 점검(선택) — 200 과 모델 이름이 돌아오면 업스트림(DeepSeek)까지 연결된 것이다.
curl -H "Authorization: Bearer $MODEL_GATEWAY_SERVICE_TOKEN" -H 'content-type: application/json' \
-H 'x-purpose: rag.answer' -H 'x-portal-id: rag-center' \
-d '{"model":"auto","messages":[{"role":"user","content":"한 문장으로 인사하세요"}]}' \
https://model-gateway.rlab.monster/v1/chat/completionsPortalConsole 화면(모델 게이트웨이 → 실행 설정 → purpose 매핑)에서도 같은 값을 관리할 수 있고, 제공자 키는 콘솔에서 암호화 저장한다(MODEL_GATEWAY_CONFIG_MASTER_KEY). 매핑·토큰이 준비되면 §5 의 /api/explore/v1/ask 점검이 근거 없음 폴백이 아닌 실제 답변 + citations 를 돌려준다.
4. 소비 포털
| 포털 | 필요한 env | 위임 주체 |
|---|---|---|
| ProcessCenter | EXPLORE_GATEWAY_URL, EXPLORE_GATEWAY_AUDIENCE, EXPLORE_GATEWAY_API_KEY(어댑터), PROCESS_CENTER_SERVICE_CLIENT_SECRET | openId(directory:<id>) |
| AgentCenter | EXPLORE_GATEWAY_URL, EXPLORE_GATEWAY_AUDIENCE, EXPLORE_SERVICE_TOKEN(어댑터), AGENT_CENTER_SERVICE_CLIENT_SECRET | 세션 subjectId |
| DocumentLibrary | 위와 동일 + DOCUMENT_LIBRARY_SERVICE_CLIENT_SECRET · DIRECTORY_SERVICE_URL | 로컬 원장의 directoryUserId |
| GuideCenter | 동일 + GUIDE_CENTER_SERVICE_CLIENT_SECRET · DIRECTORY_BASE_URL | 세션 directoryUserId |
- 미설정이면 질문 버튼을 켜지 않는다(
capabilities.answer=false) — 눌러도 답이 없는 버튼을 만들지 않는다. document-lib·guide-center는 directory 모드에서만 답변이 켜진다(자체 인증 배포에는 위임할 Directory 주체가 없다).
5. 확인 (순서대로)
bash
# 1) 원장이 소비자를 알고 있는가 — 잘못된 시크릿은 401
curl -sS -X POST "$PORTAL_CONSOLE/api/internal/delegation-token" -H 'content-type: application/json' \
-d '{"client_id":"rag-center","client_secret":"<시크릿>","subject":"<directoryUserId>","audience":"rag-center","scope":"rag:read"}'
# 2) 게이트웨이가 카탈로그를 읽는가 (어댑터 상태가 보인다)
curl -sS "$EXPLORE_GATEWAY/api/explore/v1/health"
# 3) 검색 — 사용자 위임 토큰이 필요하다
curl -sS -X POST "$EXPLORE_GATEWAY/api/explore/v1/search" -H "authorization: Bearer <위임토큰>" \
-H 'content-type: application/json' -d '{"query":"결재","limit":10}'
# 4) 답변 — providerId 가 rag-center 인지 확인
curl -sS -X POST "$EXPLORE_GATEWAY/api/explore/v1/ask" -H "authorization: Bearer <위임토큰>" \
-H 'content-type: application/json' -d '{"question":"결재선은 어떻게 되나요?"}'6. 흔한 실패와 원인
| 증상 | 원인 |
|---|---|
| 답변 버튼이 안 보인다 | 포털 capabilities.answer=false — EXPLORE_GATEWAY_URL 미설정 또는 위임 주체 없음(자체 인증) |
401 explore_gateway_auth_required | 게이트웨이 PORTAL_SERVICE_TOKENS_JSON 과 포털 EXPLORE_SERVICE_TOKEN 값 불일치 |
위임 발급 invalid_client | 소비자 목록의 clientSecret 과 포털 시크릿 불일치 |
scope_not_allowed/audience_not_allowed | delegationAudiences·delegationScopes 에 그 값이 없다(위임은 좁히기만 한다) |
답변이 비어 있다(partial: true) | 게이트웨이가 provider 를 못 찾았다 — 카탈로그의 rag-center 와 등록 상태를 확인 |
| RagService 어댑터가 503 | RAG_EXPLORE_GATEWAY_API_KEY 미설정(경로를 닫는다) |
| 검색에 그 포털 문서가 안 나온다 | 그 포털의 search 어댑터가 카탈로그에 없거나 서비스 토큰이 다르다 |
빌드가 file:vendor/... 를 못 찾는다 | vendor tarball 이 커밋되지 않았다 — git add -f vendor/<pkg>.tgz(.gitignore 의 *.tgz 규칙) |
7. 배포 순서 (의존성 순)
각 단계는 앞 단계가 준비된 뒤 배포한다. 소비 포털을 먼저 켜면 게이트웨이가 카탈로그를 못 읽어 검색이 조용히 빈다.
| # | 서비스 | 배포 전 확인 | 배포 후 확인 |
|---|---|---|---|
| 1 | PortalConsole | §1-1 소비자 JSON, §1-2 카탈로그 JSON, §1-3 등록(포털 4·rag-center·explore-gateway=PLATFORM_SERVICE) | 포털 API 키로 카탈로그가 조회되는가(directory:read) |
| 2 | RagService (rag-center) | §3 env 전부 + ModelGateway 의 rag.answer 모델 매핑 | POST /api/explore/v1/search 가 503 이 아닌가(서비스 토큰 유효), 답변이 오는가 |
| 3 | ExploreGateway | §2 env 전부(CONTROL_PLANE_*·PORTAL_SERVICE_TOKENS_JSON·PORTAL_CONSOLE_JWT_AUDIENCE=explore-gateway) | /api/explore/v1/health 의 adapters 에 등록한 포털이 보이는가 |
| 4 | 소비 포털 (PC → AC → DL → GC) | §4 env(포털별) | 상단 검색에서 ① 자기 색인 결과 ② 질문 버튼 ③ 답변 카드(출처 포함)가 순서대로 보이는가 — 한 곳씩 확인하고 다음으로 |
| 5 | 공통 확인 | §5 curl 4단계 | providerId 가 rag-center 인가 · partial:false 인가 |
롤백
- 포털 단위: 그 포털의
EXPLORE_GATEWAY_URL을 비우고 재배포 → 질문 버튼이 사라지고 자기 색인 검색만 남는다(장애 확산 차단). - 게이트웨이 단위: 카탈로그에서 문제 포털을 빼면 그 포털만 결과에서 빠진다(
unavailablePortals로 드러난다). - provider 단위:
rag-center를 카탈로그에서 빼면 답변이 멈추고 검색만 동작한다 — 답변을 되살릴 대안 provider 는 없다(정본 하나 원칙). - 코드 단위: 각 리포는
git revert <커밋>후 재배포. vendor tarball 변경은 되돌릴 때도git add -f가 필요하다.
배포 게이트 (이 저장소들이 쓰는 방식)
- 로컬 게이트가 기본:
pnpm check && pnpm build && pnpm test— 원격 CI 는 실행 횟수만큼 비용이 든다. - 신규 클론 검증(배포 이미지와 같은 경로):
git clone <repo> /tmp/x && cd /tmp/x && pnpm install --frozen-lockfile && pnpm build— vendor tarball 누락은 여기서만 드러난다. - 원격 검증 1회(수동):
gh workflow run verify.yml --repo hohyun94/<repo> --ref main- DocumentLibrary · GuideCenter 의
verify.yml은 PR 전용이라 수동 실행이 없다 — 그 두 리포는 PR 을 열거나 위 신규 클론 검증으로 대신한다.
- DocumentLibrary · GuideCenter 의