21 KiB
외부 서비스 조직선택기 통합 가이드
외부 서비스에서 조직·사용자를 선택하는 두 가지 연동 방법입니다. 이 문서에서 RP(Relying Party)는 Baron과 연동하는 외부 서비스를 뜻합니다. 현재 저장소 구현을 기준으로 합니다.
| 필요한 방식 | 데이터 조회 | 선택 UI | 읽을 절 |
|---|---|---|---|
| Integration API로 받은 데이터를 자체 화면에서 선택 | RP Backend가 Integration Credential로 조회 | 독립 SDK의 renderOrgPicker() |
0절 |
| OrgFront 선택기 화면을 그대로 임베딩 | iframe 안의 OrgFront가 조회 | /embed/picker와 postMessage |
1~5절 |
0. Integration API 데이터 + Picker SDK 방식
이 방식은 RP Backend가 조직 데이터를 조회하고, RP 브라우저는 선택 UI만 렌더링합니다. OrgFront iframe이나 OrgFront 사용자 로그인 세션을 사용하지 않습니다. RP 자체 로그인과 권한 검사는 계속 필요합니다.
RP 브라우저 → RP Backend → GET /api/v1/integrations/org-context
│ Integration Credential
└─ 승인된 v2 응답 → buildOrgChartModel → renderOrgPicker
Credential과 조회 API
- Directory API 가이드의 절차로 AdminFront에서 Integration Credential을 발급합니다. OperationID는
baron.org-context.read, 범위는 필요한 Tenant subtree, 사용자 선택에 필요한 exposure는members로 승인받습니다. - key ID와 secret을 RP Backend에 보관합니다. 브라우저 JavaScript, iframe URL,
VITE_*환경변수에 넣지 않습니다. - RP Backend가 사용자의 로그인과 요청한 조직에 대한 접근 권한을 확인한 뒤 아래 API를 호출합니다.
tenantId또는tenantSlug중 하나만 전달합니다.
GET /api/v1/integrations/org-context?tenantId=<tenant-uuid>&exposure=members
X-Baron-Key-ID: <integration-key-id>
X-Baron-Key-Secret: <integration-key-secret>
Accept: application/json
응답은 schemaVersion: "baron.org-context.v2"이며 scope, tree, tenants를 포함합니다. 원본 계약을 유지한 채 브라우저가 사용할 수 있는 응답으로 전달합니다. 이 예제처럼 사용자 이름을 표시하려면 members를 사용합니다. structure는 기본 exposure로 조직 구조만 조회하며 사용자 이름을 표시하는 다중선택 예제에는 적합하지 않습니다. 정확한 필드와 오류·cache 처리는 Org Context API 가이드를 따릅니다.
Credential은 서비스에 부여된 범위입니다. RP Backend는 이 범위 전체를 모든 RP 사용자에게 공개하지 말고, 해당 사용자가 조회해도 되는 subtree인지 별도로 확인해야 합니다. 401/403/조회 실패를 빈 조직도 응답으로 바꾸지 않습니다.
SDK의 createOrgContextClient({ baseUrl, credentials: { keyId, keySecret } })와 fetchOrgContext({ tenantId, exposure: "members" })를 Backend 조회에 사용할 수도 있습니다. 이때도 secret을 사용하는 코드는 Backend에서만 실행합니다.
SDK 파일 준비
SDK는 저장소에서 ES module과 UMD 파일로 빌드합니다.
cd orgfront
pnpm build:org-context-chart
orgfront/dist/org-context-chart/에 boc-<build-id>.js, boc-<build-id>.min.js, UMD 파일 등이 생성됩니다. 필요한 ES module 파일 한 개를 RP의 정적 파일로 배포합니다. 아래 예제는 그 파일을 /vendor/baron-org-context-chart.js라는 이름으로 배포했다고 가정합니다. 이 주소는 RP가 정하는 경로이며, Baron의 고정 CDN 주소나 npm 패키지명이 아닙니다.
이미 받은 데이터로 사용자 선택기만 렌더링
아래 /api/org-picker-context는 RP가 구현하는 예시 endpoint입니다. 앞의 인증·권한 검사를 수행한 뒤 Baron v2 응답을 반환하도록 구현합니다. 브라우저는 Integration Credential을 알 필요가 없습니다.
<div id="api-org-picker"></div>
<pre id="api-org-picker-result" aria-live="polite"></pre>
import {
buildOrgChartModel,
renderOrgPicker,
} from "/vendor/baron-org-context-chart.js";
// RP 사용자 세션으로 자신의 Backend를 호출합니다.
const response = await fetch("/api/org-picker-context", {
credentials: "same-origin",
headers: { Accept: "application/json" },
});
if (!response.ok) throw new Error(`조직 조회 실패: ${response.status}`);
const payload = await response.json();
if (payload.schemaVersion !== "baron.org-context.v2") {
throw new Error("지원하지 않는 조직 데이터 버전입니다.");
}
// 이미 API 응답을 보유하고 있다면 위 fetch 없이 payload부터 사용합니다.
const model = buildOrgChartModel(payload);
const host = document.getElementById("api-org-picker");
const result = document.getElementById("api-org-picker-result");
let picker;
picker = renderOrgPicker(host, model, {
variant: "orgfront",
mode: "multiple",
selectable: "user",
includeDescendants: true,
showDescendantToggle: true,
onChange(selection) {
result.textContent = `${selection.length}명 선택 중`;
},
onConfirm(selection) {
const userIds = [...new Set(selection
.filter(item => item.type === "user")
.map(item => item.id))];
// 이 UUID들을 RP Backend에 제출합니다. 저장 권한은 Backend가 재검사합니다.
result.textContent = JSON.stringify(userIds, null, 2);
picker.destroy();
},
onCancel() {
result.textContent = "선택을 취소했습니다.";
picker.destroy();
},
});
// 부모 모달을 닫거나 컴포넌트를 해제할 때도 picker.destroy() 호출
예제는 ES module로 실행합니다. HTML에 직접 넣을 때는 JavaScript 코드를 <script type="module"> 안에 넣습니다. renderOrgChart()는 호출하지 않아도 됩니다. renderOrgPicker()는 넘겨받은 model로 검색·선택 UI를 만들며 직접 API를 조회하지 않습니다. 기본 스타일은 자동 삽입됩니다. 스타일을 직접 제공할 때만 injectStyles: false를 사용합니다.
SDK 옵션은 selectable: "tenant" | "user" | "both"입니다. iframe query의 select와 이름이 다릅니다. variant: "orgfront"는 OrgFront 형태의 UI preset이며, /embed/picker 페이지 자체를 실행하는 옵션은 아닙니다.
콜백의 selection은 { type, id, name }[]이고 user.id는 canonical identity UUID입니다. 같은 정보를 DOM 이벤트 baron-org-picker-change/baron-org-picker-confirm의 event.detail.selection으로도 받을 수 있습니다. SDK 방식에는 iframe의 orgfront:picker:*, context, nonce 검증을 적용하지 않습니다.
반환된 controller는 getSelection(), confirm(), cancel(), destroy()를 제공합니다. confirm()/cancel()은 이벤트를 발생시키고, 화면 제거는 destroy()로 처리합니다. 데이터가 변경되면 기존 picker를 destroy()한 뒤 새 응답으로 model과 picker를 다시 만듭니다.
1. iframe 방식 연동 준비
운영 담당자에게 OrgFront 공개 URL과 사용자의 조직 조회 권한을 확인합니다. 아래 예제의 https://org.example.com은 실제 주소로 바꿉니다. iframe을 넣을 서비스도 HTTPS로 제공합니다. 로컬 검증에는 HTTP localhost를 사용할 수 있습니다.
| 경로 | 용도 |
|---|---|
/embed/picker |
외부 화면에 넣는 선택기 |
/login?auto=1&returnTo=... |
OrgFront 인증을 거쳐 선택기로 진입 |
/picker |
선택 모드와 대상을 바꿔 보는 화면 |
/embed-preview |
iframe URL과 선택 메시지를 확인하는 화면 |
부모 서비스의 로그인 상태만으로 OrgFront 로그인이 완료되지는 않습니다. OrgFront 세션이 있으면 선택기로 이동하고, 없으면 auto=1 진입이 OIDC 자동 로그인을 시도합니다. 자동 인증이 실패하면 대화형 로그인 재시도 또는 로그인 화면으로 이어질 수 있습니다. iframe 안에서 추가 로그인 없이 항상 열린다고 가정하지 마세요.
실제 외부 도메인에서 로그인 진입을 확인합니다. iframe이 차단되면 부모의 frame-src, OrgFront·로그인 화면의 frame-ancestors/X-Frame-Options, 브라우저의 쿠키 제한을 확인합니다. 로그인 화면을 임베딩할 수 없는 배포에서는 먼저 OrgFront를 최상위 창에서 열어 로그인한 뒤 선택기를 다시 엽니다. 이 절차만으로 브라우저의 쿠키 제한이 해제되는 것은 아닙니다.
현재 인증·세션 운영 정책은 Authentication and Login Flow를 따릅니다.
2. 선택기 URL 만들기
사용자 다중선택의 기본 조합은 다음과 같습니다. URL은 문자열을 직접 이어 붙이지 말고 URLSearchParams로 만듭니다.
const orgOrigin = new URL("https://org.example.com").origin;
const rootTenantId = "all";
const nonce = crypto.randomUUID(); // 열 때마다 새로 생성
const params = new URLSearchParams({
mode: "multiple",
select: "user",
rootTenantId,
parentOrigin: window.location.origin,
requestNonce: nonce,
includeDescendants: "true",
showDescendantToggle: "true",
});
const pickerPath = `/embed/picker?${params}`;
const loginUrl = new URL("/login", orgOrigin);
loginUrl.search = new URLSearchParams({
auto: "1",
returnTo: pickerPath,
}).toString();
returnTo는 OrgFront 내부 상대 경로입니다. RP callback 주소를 넣지 않습니다. parentOrigin은 선택기를 담는 부모 페이지의 origin이며 경로나 마지막 /를 붙이지 않습니다. 예: https://app.example.com.
URL 옵션
아래 기본값은 /embed/picker에 직접 접근했을 때 기준입니다. /picker와 /embed-preview의 URL 생성기는 모드를 명시하므로 외부에서도 mode와 select를 명시하세요.
| 옵션 | 값과 현재 동작 |
|---|---|
mode |
single 또는 multiple. 직접 접근 시 생략하면 single |
select |
tenant, user, both. 생략하면 both |
rootTenantId |
루트 Tenant ID 또는 slug. all은 조회된 트리의 루트들을 표시하며 독립 USER_GROUP 루트는 제외. 특정 범위는 아래 allowlist 설명 참고 |
parentOrigin |
부모의 정확한 origin. 생략하거나 잘못된 값이면 현재 구현은 *로 전송하므로 반드시 명시 |
requestNonce |
이번 열기 요청을 식별하는 임의 문자열. rootTenantId와 함께 있어야 응답 context가 생성됨 |
tenantSlug |
선택한 루트 안에서 표시할 하위 Tenant. tenantId, companyTenantId도 호환 별칭으로 수용하며 이 순서로 우선 적용 |
allowedRootTenantIds |
표시할 관리 루트 ID를 쉼표로 구분. 지정하면 rootTenantId/하위 Tenant 선택보다 우선. 빈 값 또는 __none__은 빈 트리 |
allowedTenantIds |
선택 가능한 Tenant ID의 allowlist. 빈 값 또는 __none__은 빈 목록. 표시 트리도 필터링하므로 사용자 표시 범위에 영향을 줄 수 있음 |
includeInternal |
true이면 내부 Tenant 포함. 기본 false |
includePrivate |
true이면 비공개 Tenant 포함. 기본 false |
includeDescendants |
다중선택에서 하위 항목도 선택. 기본 true |
showDescendantToggle |
하위 선택 체크박스 표시. 기본 true |
token |
이미 발급된 공유 조직도 토큰으로 공개 조회 경로 사용. 아래 공유 모드 참고 |
rootTenantId를 생략하거나 찾지 못하면 현재 구현은 hanmac-family 회사그룹, 첫 회사그룹, 부모 없는 Tenant 순으로 기본 루트를 찾습니다. 특정 범위를 반드시 지정해야 하면 Backend가 승인한 allowedRootTenantIds를 전달하고, 결과 저장 시 Backend에서 다시 권한을 검사합니다. rootTenantId=all도 서버 조회 권한을 늘리지는 않습니다.
tenantSlug에 의한 하위 범위 지정은 rootTenantId=all 또는 allowedRootTenantIds로 루트를 구성하는 경우에는 적용되지 않습니다. 한 번에 한 가지 범위 지정 방식을 사용하세요.
width/height는 검증 화면의 URL 생성 옵션에도 보이지만, 실제 iframe 크기는 부모의 CSS로 지정합니다. 선택기 내부는 iframe의 화면 높이를 사용합니다.
3. 선택 결과 수신
선택기는 부모에게 window.parent.postMessage를 보냅니다. 부모는 origin, iframe 창, nonce, rootTenantId, payload 형식을 검사한 뒤 사용합니다. origin만 확인하면 같은 OrgFront의 다른 iframe에서 보낸 메시지도 받아들일 수 있습니다.
메시지 type |
의미 |
|---|---|
orgfront:picker:ready |
선택기 컴포넌트가 마운트됨. 조직 데이터 조회 완료나 인증 성공 보증이 아님 |
orgfront:picker:confirm |
사용자가 선택 완료를 누름. payload.mode, payload.selections 포함 |
orgfront:picker:cancel |
사용자가 선택기 내부 취소를 누름. 부모가 iframe/모달을 닫아야 함 |
orgfront:picker:error |
조직 데이터 조회 실패. error: "org_picker_load_failed" |
requestNonce와 rootTenantId를 함께 보낸 경우 위 메시지에 다음 context가 포함됩니다.
{
"type": "orgfront:picker:confirm",
"context": { "nonce": "이번 요청의 nonce", "rootTenantId": "all" },
"payload": {
"mode": "multiple",
"selections": [
{
"type": "user",
"id": "11111111-1111-4111-8111-111111111111",
"name": "김하나 책임",
"email": "one@example.com",
"rootTenantName": "개발본부",
"leafTenantName": "개발팀"
}
]
}
}
항목의 필수 필드는 type, id, name입니다. type은 tenant 또는 user이며, 사용자 항목에는 이메일과 조직 표시명이 추가될 수 있습니다. 이름에는 직급 등 표시용 텍스트가 포함될 수 있으므로 이름이나 이메일로 사용자를 식별하지 않습니다. 사용자 연결·저장에는 id의 Baron canonical identity UUID를 사용합니다.
선택 완료는 조회 결과 전달입니다. RP 사용자 등록이나 whitelist 저장 API를 자동 호출하지 않습니다. 부모가 ID를 자신의 Backend에 제출하고, Backend는 대상의 활성 상태·소속·처리 권한을 다시 검사해야 합니다. URL allowlist나 nonce는 권한 증명서가 아닙니다.
4. 부모 페이지 예제
2절 코드를 먼저 실행한 뒤 아래 예제를 이어 붙입니다. org-picker-host와 org-picker-result 요소가 있는 부모 페이지에서 동작합니다. 사용자 다중선택 전용이며, 조직 선택으로 바꿀 때는 select, 응답 type, ID 검증도 함께 바꿉니다.
<div id="org-picker-host"></div>
<pre id="org-picker-result" aria-live="polite"></pre>
const host = document.getElementById("org-picker-host");
const result = document.getElementById("org-picker-result");
const frame = document.createElement("iframe");
frame.title = "사용자 다중선택";
frame.style.cssText = "width:100%;height:480px;border:0";
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
let closed = false;
function closePicker() {
closed = true;
clearTimeout(readyTimer);
window.removeEventListener("message", receive);
frame.remove();
}
function receive(event) {
if (closed || event.origin !== orgOrigin || event.source !== frame.contentWindow) return;
const data = event.data;
if (!data || typeof data !== "object" ||
data.context?.nonce !== nonce || data.context?.rootTenantId !== rootTenantId) return;
switch (data.type) {
case "orgfront:picker:ready":
clearTimeout(readyTimer);
result.textContent = "선택기가 열렸습니다. 데이터 로딩 상태는 선택기에서 확인하세요.";
break;
case "orgfront:picker:cancel":
result.textContent = "선택을 취소했습니다.";
closePicker();
break;
case "orgfront:picker:error":
result.textContent = "조직을 불러오지 못했습니다. 조회 권한과 연결 상태를 확인하세요.";
closePicker();
break;
case "orgfront:picker:confirm": {
const items = data.payload?.selections;
if (data.payload?.mode !== "multiple" || !Array.isArray(items) || !items.length) return;
if (!items.every(item => item && item.type === "user" &&
typeof item.id === "string" && uuid.test(item.id) && typeof item.name === "string")) return;
const selected = [...new Map(items.map(item => [item.id, { id: item.id, name: item.name }])).values()];
// 이 ID 목록을 자체 Backend에 제출하고 별도로 저장 결과를 처리합니다.
result.textContent = JSON.stringify(selected, null, 2);
closePicker();
break;
}
}
}
window.addEventListener("message", receive); // iframe을 열기 전에 등록
const readyTimer = setTimeout(() => {
result.textContent = "선택기 응답이 없습니다. OrgFront 로그인과 iframe 허용 설정을 확인하세요.";
closePicker();
}, 20000); // 예제의 대기 시간이며 서비스 응답 조건에 맞게 조정
frame.src = loginUrl.toString();
host.append(frame);
// 부모 모달을 닫거나 페이지를 떠날 때도 closePicker() 호출
예제의 20초 대기 시간에는 OrgFront 로그인 시간도 포함됩니다. 대화형 로그인 중에도 시간이 지나면 iframe이 닫히므로, 실제 서비스에서는 로그인 절차를 먼저 완료하거나 로그인에 필요한 대기 시간으로 조정합니다.
선택기 내부에는 검색과 선택 완료 UI가 있습니다. 부모에서 별도 저장 확인이 필요하면 선택 결과를 보관해 확인 화면을 보여준 뒤 저장합니다. 반복해서 여는 모달에 적용할 때는 2·4절 JavaScript를 하나의 열기 함수 안에서 실행하고, 해당 함수에서 closePicker를 반환해 부모의 닫기 동작에 연결합니다. 다시 열 때는 이전 함수가 반환한 닫기 함수를 호출하고 새 nonce로 URL을 생성합니다. 현재 선택기에는 초기 선택 ID 주입이나 부모가 보내는 선택 변경 메시지 계약이 없습니다.
5. 공유 모드와 문제 확인
이미 발급된 공유 조직도 토큰을 사용하는 경우 /embed/picker?token=...에 나머지 옵션을 함께 전달합니다. 이 경로는 공개 조직도 조회를 사용하며 includeInternal과 includePrivate는 적용하지 않습니다. 공유 토큰을 RP access token이나 refresh token으로 대체하지 않습니다. 임베딩 토큰 설계에 나오는 권장 발급 API는 설계 제안이므로 이 가이드의 구현된 발급 절차로 취급하지 않습니다.
| 증상 | 확인할 항목 |
|---|---|
| 로그인 화면 또는 빈 iframe | 공개 URL, OrgFront 로그인, iframe 허용 헤더, 브라우저 쿠키 제한 |
ready 이후 오류 |
OrgFront 조직도 API 응답과 현재 사용자의 조회 권한 |
| 부모가 선택 결과를 무시함 | 정확한 origin, event.source, 이번 nonce, rootTenantId, payload.mode |
| 조직·사용자가 보이지 않음 | Backend 조회 범위, root/allowlist 조합, 내부·비공개 표시 옵션, 사용자 활성 상태 |
| 취소를 눌러도 모달이 남음 | 부모의 orgfront:picker:cancel 처리와 닫기 함수 연결 |
| 테스트 iframe에서 한글이 깨짐 | 테스트 HTML의 <meta charset="utf-8">와 응답 인코딩 |
6. 구현과 검증 참고
-
실제 선택기 브라우저 통합 테스트:
cd devfront && pnpm exec playwright test --config playwright.picker.config.ts --workers=1
이 테스트는 두 Frontend의 실제 화면과 선택 메시지를 사용하고, 인증 상태와 Backend 응답에는 fixture를 사용합니다. 실제 외부 도메인의 SSO·iframe 허용 여부는 대상 배포에서 별도로 확인합니다.