{client.name ||
diff --git a/devfront/src/features/clients/components/ClientLogo.tsx b/devfront/src/features/clients/components/ClientLogo.tsx
new file mode 100644
index 00000000..397a1d99
--- /dev/null
+++ b/devfront/src/features/clients/components/ClientLogo.tsx
@@ -0,0 +1,59 @@
+import { ServerCog, ShieldHalf } from "lucide-react";
+import { useMemo, useState } from "react";
+import {
+ Avatar,
+ AvatarFallback,
+ AvatarImage,
+} from "../../../components/ui/avatar";
+import type { ClientSummary, ClientType } from "../../../lib/devApi";
+import { t } from "../../../lib/i18n";
+
+type ClientLogoProps = {
+ client: Pick
host 존재 필요
+ Backend->>Hydra: client metadata update
+ Hydra-->>Backend: 저장 완료
+ Backend-->>DevFront: 저장 성공
+ DevFront-->>Admin: 저장 완료 표시
+```
+
+### 2. userfront에서 자동 로그인 시작
+
+```mermaid
+sequenceDiagram
+ participant User as 사용자
+ participant UserFront as UserFront
+ participant Backend as Baron Backend
+ participant RP as RP 로그인 시작 URL
+ participant Baron as Baron OIDC/Hydra
+
+ User->>UserFront: 연동 앱 카드 클릭
+ UserFront->>Backend: linked RP 목록/상태 조회
+ Backend-->>UserFront: auto_login_supported, auto_login_url 반환
+
+ alt auto_login_supported = true
+ UserFront->>RP: GET auto_login_url
+ Note over RP: RP가 state, nonce,
PKCE verifier/challenge 생성
+ RP-->>UserFront: 302 Baron authorize endpoint
+ UserFront->>Baron: GET /oidc/oauth2/auth
+ else auto_login_supported = false
+ UserFront->>UserFront: 일반 url 또는 init_url 사용
+ end
+```
+
+### 3. 로컬 3333 RP 예시 흐름
+
+```mermaid
+sequenceDiagram
+ participant User as 사용자 브라우저
+ participant UserFront as UserFront
+ participant RP as localhost:3333 RP
+ participant Baron as Baron OIDC
+
+ User->>UserFront: RP 카드 클릭
+ UserFront->>RP: GET http://localhost:3333/login
+ RP-->>User: 302 /oidc/oauth2/auth?...&client_id=f5cdd938-a3ae-4e47-ab83-4c13e59949f5
+ User->>Baron: GET authorize endpoint
+ Baron-->>User: 로그인/동의 또는 기존 세션 진행
+ Baron-->>RP: callback redirect
+ RP-->>User: RP 세션 생성 후 앱 화면 진입
+```
+
+## 로직 요약
+
+1. DevFront는 `auto_login_supported`, `auto_login_url`을 RP metadata로 저장합니다.
+2. Backend는 `auto_login_supported=true`일 때만 `auto_login_url`을 linked RP 응답에 포함시켜 userfront가 사용할 수 있게 합니다.
+3. UserFront는 카드 클릭 시 이 URL로 직접 이동합니다.
+4. RP는 그 진입점에서 OIDC 요청을 "직접" 생성해야 합니다.
+5. Callback 검증에 필요한 `state`, `nonce`, PKCE 상태는 RP 저장소가 소유해야 합니다.
+6. 그래서 Baron이 RP 대신 authorize URL을 만들어 주는 구조로 바꾸면 안 됩니다.
+
## 검증 체크리스트
RP 등록자는 다음을 확인해야 합니다.
@@ -101,6 +217,7 @@ npm run test -- tests/orgfront-auto-login.spec.ts --project=chromium
| RP 로그인 화면에 머무름 | RP가 `auto=1` 쿼리를 읽어 자동으로 `signinRedirect` 또는 동일한 OIDC 시작 함수를 호출하는지 확인합니다. |
| callback에서 state 오류 발생 | userfront나 backend가 만든 `/oauth2/auth?...` URL을 직접 쓰지 말고 RP 자체 로그인 시작 URL에서 OIDC 요청을 생성해야 합니다. |
| 등록 저장이 실패함 | `auto_login_supported=true`일 때 `auto_login_url`이 비어 있거나 `http/https` URL이 아닌지 확인합니다. |
+| `http:localhost:3333/login` 같은 값이 브라우저에서는 열림 | 브라우저 보정에 기대지 말고 `http://localhost:3333/login`처럼 완전한 절대 URL로 저장합니다. |
## 구현 예시
diff --git a/locales/en.toml b/locales/en.toml
index 2da95a9f..10e89f32 100644
--- a/locales/en.toml
+++ b/locales/en.toml
@@ -131,12 +131,17 @@ empty = "No organization units have been registered yet."
import_error = "Import Error"
import_success = "Import Success"
loading = "Loading..."
+no_results = "No groups found."
subtitle = "Manage departments and teams under the current tenant."
[msg.admin.groups.members]
+add_modal_desc = "Search and select members to add from users in this tenant."
add_success = "Member added successfully."
+all_added = "All tenant members are already in this group."
count = "{{count}} members loaded."
empty = "No members are assigned to this organization unit."
+move_modal_desc = "Select the target group to move the selected member."
+move_success = "Member moved successfully."
remove_confirm = "Are you sure you want to remove this member?"
remove_success = "Member removed successfully."
title = "Member Management"
@@ -229,6 +234,9 @@ subtitle = "Set the basic tenant profile information."
desc = "View the list of users belonging to this organization."
empty = "No members found."
limit_notice = "Showing members from the first 10 descendant organizations due to size limits."
+remove_confirm = "Are you sure you want to exclude '{{name}}' from this organization?"
+remove_error = "An error occurred while excluding from organization."
+remove_success = "Successfully excluded from organization."
[msg.admin.tenants.owners]
add_success = "Owner added successfully."
@@ -255,6 +263,7 @@ empty = "No child tenants are connected."
subtitle = "Review and manage child tenants linked under this tenant."
[msg.admin.users]
+confirm_remove_org = "Do you want to remove this user from the organization?"
export_error = "Failed to export users."
status_error = "Failed to update user status."
@@ -1026,8 +1035,11 @@ unit_level_placeholder = "Unit Level Placeholder"
title = "User Groups"
[ui.admin.groups.members]
+add_modal_title = "Add Member to Group"
+move_modal_title = "Move Department"
[ui.admin.groups.members.table]
+actions = "ACTIONS"
email = "Email"
name = "Name"
remove = "Remove"
@@ -1100,6 +1112,10 @@ seed_badge = "Seed"
title = "Tenant Registry"
view_org_chart = "View Full Org Chart"
+[ui.admin.tenants.view]
+hierarchy = "Hierarchy"
+list = "List"
+
[ui.admin.tenants.domain_conflict]
description = ""
title = "Domain conflict"
@@ -1217,13 +1233,17 @@ self_delete_blocked = "You cannot delete your own account."
title = "API Key Registry"
[ui.admin.tenants.members]
+add_existing = "Assign Existing Member"
+create_new = "Create New Member"
delete_selected = "Delete Selected"
+remove = "Exclude from Organization"
view_org_chart = "View Full Org Chart"
direct_label = "Direct"
list_title = "Member Management"
title = "Tenant Members ({{count}})"
total = "Total"
total_label = "Total"
+view_profile = "View Profile"
[ui.admin.tenants.import_preview]
candidates = "Candidates"
@@ -1235,6 +1255,7 @@ no_candidates = "No matching tenants found."
title = "Import Preview"
[ui.admin.tenants.members.table]
+actions = "ACTIONS"
email = "EMAIL"
name = "NAME"
role = "ROLE"
diff --git a/locales/ko.toml b/locales/ko.toml
index fe5e2203..8a9f1f15 100644
--- a/locales/ko.toml
+++ b/locales/ko.toml
@@ -612,12 +612,17 @@ empty = "테넌트에 등록된 조직 단위가 없습니다."
import_error = "가져오기 실패"
import_success = "조직도가 임포트되었습니다."
loading = "로딩 중..."
+no_results = "그룹이 없습니다."
subtitle = "이 테넌트에 정의된 사용자 그룹 목록입니다."
[msg.admin.groups.members]
+add_modal_desc = "이 테넌트에 속한 사용자 중 추가할 멤버를 검색하여 선택하세요."
add_success = "구성원이 추가되었습니다."
+all_added = "모든 테넌트 멤버가 이미 이 그룹에 속해 있습니다."
count = "{{count}} 명"
empty = "멤버가 없습니다."
+move_modal_desc = "선택한 멤버를 이동할 대상 그룹을 선택하세요."
+move_success = "멤버가 이동되었습니다."
remove_confirm = "제거하시겠습니까?"
remove_success = "구성원이 제외되었습니다."
title = "[{{name}}] 멤버 관리"
@@ -710,6 +715,9 @@ subtitle = "필수 정보만 입력해도 생성 가능합니다. Slug는 없으
desc = "조직에 소속된 사용자 목록을 확인합니다."
empty = "소속된 사용자가 없습니다."
limit_notice = "하위 조직이 많아 상위 10개 조직의 멤버만 표시됩니다."
+remove_confirm = "'{{name}}'님을 이 조직에서 제외하시겠습니까?"
+remove_error = "조직에서 제외하는 중 오류가 발생했습니다."
+remove_success = "조직에서 제외되었습니다."
[msg.admin.tenants.owners]
add_success = "소유자가 추가되었습니다."
@@ -736,6 +744,7 @@ empty = "하위 테넌트가 없습니다."
subtitle = "현재 테넌트 하위에 생성된 조직입니다."
[msg.admin.users]
+confirm_remove_org = "이 조직에서 사용자를 제외하시겠습니까?"
export_error = "사용자 내보내기에 실패했습니다."
status_error = "사용자 상태 변경에 실패했습니다."
@@ -1505,8 +1514,11 @@ unit_level_placeholder = "예: 본부, 팀"
title = "User Groups"
[ui.admin.groups.members]
+add_modal_title = "그룹에 멤버 추가"
+move_modal_title = "부서 이동"
[ui.admin.groups.members.table]
+actions = "ACTIONS"
email = "이메일"
name = "이름"
remove = "제거"
@@ -1575,6 +1587,10 @@ seed_badge = "초기 설정"
title = "테넌트 목록"
view_org_chart = "전체 조직도 보기"
+[ui.admin.tenants.view]
+hierarchy = "계층 구조"
+list = "평면 목록"
+
[ui.admin.tenants.admins]
add_button = "관리자 추가"
already_admin = "이미 관리자"
@@ -1679,13 +1695,17 @@ status_error = "사용자 상태 변경에 실패했습니다: {{error}}"
title = "API Key Registry"
[ui.admin.tenants.members]
+add_existing = "기존 멤버 배정"
+create_new = "신규 멤버 생성"
delete_selected = "선택 삭제"
+remove = "조직에서 제외"
view_org_chart = "전체 조직도 보기"
direct_label = "직속"
list_title = "구성원 관리"
title = "테넌트 구성원 ({{count}})"
total = "전체"
total_label = "전체"
+view_profile = "상세 정보"
[ui.admin.tenants.import_preview]
candidates = "후보"
@@ -1697,6 +1717,7 @@ no_candidates = "매칭 가능한 테넌트가 없습니다."
title = "임포트 미리보기"
[ui.admin.tenants.members.table]
+actions = "ACTIONS"
email = "EMAIL"
name = "NAME"
role = "ROLE"
diff --git a/locales/template.toml b/locales/template.toml
index 0e24f3ec..5c5df6b4 100644
--- a/locales/template.toml
+++ b/locales/template.toml
@@ -481,12 +481,17 @@ empty = ""
import_error = ""
import_success = ""
loading = ""
+no_results = ""
subtitle = ""
[msg.admin.groups.members]
+add_modal_desc = ""
add_success = ""
+all_added = ""
count = ""
empty = ""
+move_modal_desc = ""
+move_success = ""
remove_confirm = ""
remove_success = ""
title = ""
@@ -579,6 +584,9 @@ subtitle = ""
desc = ""
empty = ""
limit_notice = ""
+remove_confirm = ""
+remove_error = ""
+remove_success = ""
[msg.admin.tenants.owners]
add_success = ""
@@ -605,6 +613,7 @@ empty = ""
subtitle = ""
[msg.admin.users]
+confirm_remove_org = ""
export_error = ""
status_error = ""
@@ -1374,8 +1383,11 @@ unit_level_placeholder = ""
title = ""
[ui.admin.groups.members]
+add_modal_title = ""
+move_modal_title = ""
[ui.admin.groups.members.table]
+actions = ""
email = ""
name = ""
remove = ""
@@ -1444,6 +1456,10 @@ seed_badge = ""
title = ""
view_org_chart = ""
+[ui.admin.tenants.view]
+hierarchy = ""
+list = ""
+
[ui.admin.tenants.admins]
add_button = ""
already_admin = ""
@@ -1521,13 +1537,17 @@ search_placeholder = ""
select_placeholder = ""
[ui.admin.tenants.members]
+add_existing = ""
+create_new = ""
descendants = ""
direct = ""
direct_label = ""
list_title = ""
+remove = ""
title = ""
total = ""
total_label = ""
+view_profile = ""
[msg.admin.apikeys.registry]
count = ""
@@ -1554,15 +1574,20 @@ status_error = ""
title = ""
[ui.admin.tenants.members]
+add_existing = ""
+create_new = ""
delete_selected = ""
+remove = ""
view_org_chart = ""
direct_label = ""
list_title = ""
title = ""
total = ""
total_label = ""
+view_profile = ""
[ui.admin.tenants.members.table]
+actions = ""
email = ""
name = ""
role = ""
diff --git a/scripts/run_adminfront_ci_tests.sh b/scripts/run_adminfront_ci_tests.sh
index 3b6549e3..27491756 100755
--- a/scripts/run_adminfront_ci_tests.sh
+++ b/scripts/run_adminfront_ci_tests.sh
@@ -2,10 +2,36 @@
set -euo pipefail
job_name="${1:-adminfront-tests}"
+repo_root="$(pwd)"
+tmp_dir=""
+
+cleanup() {
+ if [ -n "${tmp_dir:-}" ] && [ -d "$tmp_dir" ]; then
+ rm -rf "$tmp_dir"
+ fi
+}
+
+trap cleanup EXIT INT TERM
mkdir -p reports
rm -rf adminfront/node_modules
+tmp_dir="$(mktemp -d /tmp/baron-sso-adminfront-tests.XXXXXX)"
+playwright_browsers_path="$tmp_dir/ms-playwright"
+
+if command -v rsync >/dev/null 2>&1; then
+ rsync -rlptD --delete \
+ --exclude 'node_modules' \
+ --exclude 'playwright-report' \
+ --exclude 'test-results' \
+ "$repo_root/adminfront/" "$tmp_dir/adminfront/"
+else
+ cp -R "$repo_root/adminfront" "$tmp_dir/adminfront"
+ rm -rf "$tmp_dir/adminfront/node_modules" \
+ "$tmp_dir/adminfront/playwright-report" \
+ "$tmp_dir/adminfront/test-results"
+fi
+
is_port_available() {
local port="$1"
node -e '
@@ -43,7 +69,7 @@ fi
set +e
(
- cd adminfront
+ cd "$tmp_dir/adminfront"
npm ci --ignore-scripts
) 2>&1 | tee reports/adminfront-install.log
install_exit_code=${PIPESTATUS[0]}
@@ -71,8 +97,8 @@ fi
set +e
(
- cd adminfront
- "${playwright_install_cmd[@]}"
+ cd "$tmp_dir/adminfront"
+ PLAYWRIGHT_BROWSERS_PATH="$playwright_browsers_path" "${playwright_install_cmd[@]}"
) 2>&1 | tee reports/adminfront-provision.log
provision_exit_code=${PIPESTATUS[0]}
set -e
@@ -106,13 +132,16 @@ if ! is_port_available "$port"; then
fi
echo "==> adminfront using PORT=$port"
(
- cd adminfront
- PORT="$port" PLAYWRIGHT_WORKERS="${PLAYWRIGHT_WORKERS:-1}" \
+ cd "$tmp_dir/adminfront"
+ PORT="$port" PLAYWRIGHT_WORKERS="${PLAYWRIGHT_WORKERS:-1}" PLAYWRIGHT_BROWSERS_PATH="$playwright_browsers_path" \
node ./node_modules/playwright/cli.js test
) 2>&1 | tee reports/adminfront-test.log
test_exit_code=${PIPESTATUS[0]}
set -e
+[ -d "$tmp_dir/adminfront/playwright-report" ] && rm -rf reports/adminfront-playwright-report && cp -R "$tmp_dir/adminfront/playwright-report" reports/adminfront-playwright-report || true
+[ -d "$tmp_dir/adminfront/test-results" ] && rm -rf reports/adminfront-test-results && cp -R "$tmp_dir/adminfront/test-results" reports/adminfront-test-results || true
+
if [ "$test_exit_code" -ne 0 ]; then
{
echo "# Adminfront Test Failure Report"