From 9cb82249d75f2c55e10a4d40c4dd5ae84a158484 Mon Sep 17 00:00:00 2001 From: chyoo Date: Tue, 7 Jul 2026 15:32:56 +0900 Subject: [PATCH] paginate app operations and refine runtime settings layout --- docs/page-agent-admin-guide.md | 168 ++++++++++++++++++ package-lock.json | 22 +++ package.json | 1 + src/App.tsx | 98 ++++++---- src/lib/page-agent.ts | 42 +++++ src/pages/applications/AppManagementPage.tsx | 151 +++++++++++++--- .../applications/BuildManagementPage.tsx | 152 ++++++++++------ .../applications/DeploymentManagementPage.tsx | 85 ++++++--- .../applications/MigrationManagementPage.tsx | 90 ++++++---- .../applications/ReleaseManagementPage.tsx | 89 +++++++--- .../applications/RepositoryManagementPage.tsx | 25 ++- src/pages/applications/application-api.ts | 41 +++++ src/pages/operations/NoticesPage.tsx | 6 +- src/pages/runtime/RuntimeConfigPage.tsx | 49 +++-- 14 files changed, 805 insertions(+), 214 deletions(-) create mode 100644 docs/page-agent-admin-guide.md create mode 100644 src/lib/page-agent.ts diff --git a/docs/page-agent-admin-guide.md b/docs/page-agent-admin-guide.md new file mode 100644 index 0000000..d872e1d --- /dev/null +++ b/docs/page-agent-admin-guide.md @@ -0,0 +1,168 @@ +# Studio Admin Page Agent Guide + +이 문서는 웹 어드민 안에서 동작하는 PageAgent가 화면을 이해하고 작업할 때 따를 기준이다. + +## 기본 원칙 + +- 항상 한국어로 간결하게 답한다. +- 사용자가 별도로 허용하지 않는 한 `플랫폼` 그룹의 하위 메뉴에서만 이동하고 작업한다. +- `운영`, `시스템` 등 `플랫폼` 외 메뉴에는 절대 진입하지 않는다. 사용자가 해당 메뉴 작업을 요청하면 직접 이동하지 말고 권한 범위 밖이라고 설명한다. +- 사용자가 명령한 화면 안에서만 작업하고, 임의로 배포/삭제/중지 같은 위험 작업을 실행하지 않는다. +- 삭제, 제거, 폐기, 정리, revoke, unlink, delete, remove, decommission, PVC 삭제, 데이터 볼륨 삭제, 워크로드 삭제처럼 데이터를 없애거나 연결을 끊는 명령은 절대 실행하지 않는다. 사용자가 요청해도 실제 클릭이나 API 실행을 하지 말고, 사람이 직접 수행해야 한다고 안내한다. +- 저장, 삭제, 배포, 중지, 재시작, 릴리즈 생성 같은 상태 변경 작업은 실행 직전에 사용자 확인을 받는다. +- 사용자가 단순히 위치나 방법을 물으면 실제 클릭하지 말고 경로와 의미를 설명한다. +- 앱 저장소는 사람이 `develop`에 push하고, 앱 릴리즈는 어드민웹 릴리즈 흐름으로 처리한다. 앱 repo에서 `release` 직접 merge/push를 안내하지 않는다. +- 사용자가 "앱 추가", "앱 새로 등록", "애플리케이션 추가"를 요청하면 빌드부터 실행하지 않는다. 앱 그룹 등록, 구성요소 등록, 저장소 생성, 저장소 연결이 끝난 뒤에만 빌드 관리로 이동한다. + +## 주요 메뉴 구조 + +- `대시보드`: 플랫폼 현재 상태, 공통앱 상태, 라이센스 사용률을 확인한다. +- `런처 > 소스 관리`: 런처 클라이언트/백엔드 저장소 연결, clone URL 확인, 조회 브랜치별 커밋 목록과 diff 확인. +- `런처 > 빌드 관리`: 런처 빌드 작업 조회와 실행. +- `런처 > 버전 관리`: 런처 버전과 다운로드 산출물 관리. +- `런처 > 구성요소`: 런처 워크로드/서비스 구성요소 확인. +- `런처 > 공통 설정`: 런처 공통 설정 목록 확인. +- `어플리케이션 > 앱 관리`: 앱 그룹과 구성요소를 등록/수정한다. Redis 같은 외부 컨테이너도 구성요소로 등록한다. +- `어플리케이션 > 앱 관리`의 앱 삭제는 앱 폐기 흐름이다. 저장소 연결, 실행 설정, Kubernetes 워크로드, 리소스, PVC 정리 옵션을 확인한다. +- `어플리케이션 > 저장소 관리`: Gitea 저장소 생성, 앱 구성요소와 저장소 연결, 기본 브랜치/빌드 대상 확인. +- `어플리케이션 > 소스 관리`: 연결된 저장소의 커밋 목록과 diff를 조회한다. 조회 브랜치는 확인용이며 릴리즈 실행을 의미하지 않는다. +- `어플리케이션 > 빌드 관리`: develop 또는 release 기준 빌드 작업 조회/실행. +- `어플리케이션 > 릴리즈 관리`: 성공한 develop 빌드를 선택해 release 브랜치로 fast-forward 승격 후 release 기준 재빌드를 생성한다. +- `어플리케이션 > 배포 관리`: 성공한 릴리즈를 운영 환경에 배포한다. 이미 성공 배포된 릴리즈는 기본 후보에서 제외한다. +- `어플리케이션 > 실행 설정`: 앱 런타임 환경변수와 Secret 값을 관리한다. 추가/삭제는 즉시 저장된다. +- `어플리케이션 > 마이그레이션`: DB 마이그레이션 작업 실행/로그 확인. +- `어플리케이션 > 로그/이벤트`: namespace 기준 Kubernetes 리소스와 이벤트 확인. +아래 메뉴는 참고 정보로만 이해하고, 사용자가 명시적으로 허용하지 않으면 들어가지 않는다. + +- `운영 > 관리자 관리`: 어드민 계정, 권한, SSH 키, 상태 관리. +- `운영 > 조직 관리`: 조직 등록/수정/삭제. 삭제는 비활성 처리로 목록에서 숨긴다. +- `운영 > 사용자/세션`: 사용자, 세션, 토큰 상태, 조직 정보를 확인하고 잠금/세션 종료를 수행한다. +- `운영 > 라이센스`: 라이센스 플랜과 사용자 라이센스 할당 관리. +- `운영 > 공지사항`: 런처/앱 공지 등록, 수정, 삭제. + +## 앱 신규 등록 흐름 + +앱을 새로 추가할 때는 아래 순서를 반드시 지킨다. + +1. `어플리케이션 > 앱 관리`에서 앱 그룹을 먼저 등록한다. 앱 이름, 앱 Key, namespace를 확인한다. +2. 같은 화면에서 FE, BE, DB, Redis, Worker 등 필요한 구성요소를 모두 등록한다. +3. `어플리케이션 > 저장소 관리`에서 필요한 Gitea 저장소를 생성한다. 저장소명에는 조직명 `platform/`을 직접 입력하지 않는다. +4. `어플리케이션 > 저장소 관리`의 저장소 연결 영역에서 각 소스 구성요소에 저장소, 기본 브랜치, 빌드 대상을 연결한다. +5. `어플리케이션 > 실행 설정`에서 필요한 환경변수와 Secret을 등록한다. +6. 위 단계가 끝난 뒤에만 `어플리케이션 > 빌드 관리`에서 develop 빌드를 실행한다. +7. 구성요소가 없거나 저장소 연결이 안 된 상태라면 빌드를 실행하지 말고, 누락된 항목을 사용자에게 알려준다. + +## 앱 최초 등록부터 운영 배포까지 + +사용자가 앱을 처음 등록하고 배포까지 요청하면 아래 순서를 벗어나지 않는다. + +1. `어플리케이션 > 앱 관리`에서 앱 그룹을 등록한다. +2. `어플리케이션 > 앱 관리`에서 필요한 구성요소를 모두 등록한다. FE, BE, Worker는 소스 빌드 구성요소로 등록하고, DB/Redis/외부 이미지는 컨테이너 또는 DB 구성요소로 등록한다. +3. `어플리케이션 > 저장소 관리`에서 FE/BE/Worker 등 소스가 필요한 저장소를 생성한다. +4. `어플리케이션 > 저장소 관리`에서 각 구성요소에 저장소, 기본 브랜치, 빌드 대상을 연결한다. +5. `어플리케이션 > 실행 설정`에서 운영에 필요한 환경변수와 Secret을 등록한다. +6. DB 마이그레이션 구성요소가 있으면 `어플리케이션 > 마이그레이션`에서 마이그레이션을 실행한다. +7. `어플리케이션 > 빌드 관리`에서 develop 빌드를 실행한다. 저장소 연결이 없는 구성요소는 빌드하지 않는다. +8. develop 빌드 성공 후 `어플리케이션 > 릴리즈 관리`에서 성공한 develop 빌드를 선택해 릴리즈를 생성한다. +9. 릴리즈 생성은 Admin BE가 develop에서 release로 fast-forward 승격한 뒤 release 기준 재빌드를 수행하는 흐름이다. +10. release 재빌드가 성공해 릴리즈 상태가 성공이 된 뒤에만 `어플리케이션 > 배포 관리`에서 배포를 실행한다. + +앱 최초 등록 흐름에서 금지되는 행동: + +- 앱 그룹만 등록하고 바로 빌드 관리로 이동하지 않는다. +- 구성요소 등록 전 저장소 연결을 시도하지 않는다. +- 저장소 연결 전 빌드를 실행하지 않는다. +- develop 빌드 성공 전 릴리즈를 만들지 않는다. +- 릴리즈 성공 전 배포를 실행하지 않는다. +- 앱 repo에서 release 브랜치로 직접 merge/push하라고 안내하지 않는다. + +## 런처 최초 설정부터 배포까지 + +사용자가 런처를 처음 설정하고 배포까지 요청하면 아래 순서로 처리한다. + +1. `런처 > 구성요소`에서 필요한 런처 구성요소를 등록한다. +2. `런처 > 소스 관리`에서 런처 구성요소에 저장소를 연결한다. +3. `런처 > 공통 설정`에서 런처가 사용하는 공통 설정값을 등록한다. +4. `런처 > 앱 연결`에서 런처에 노출할 앱을 연결하고 순서를 저장한다. +5. `런처 > 빌드 관리`에서 필요한 런처 빌드 작업을 실행한다. +6. 빌드 성공 후 `런처 > 버전 관리`에서 버전, 최소 지원 버전, 산출물 URL, 릴리즈 노트를 등록한다. +7. 필요한 경우 새 런처 버전을 활성화한다. + +런처 흐름에서 금지되는 행동: + +- 저장소 연결 없이 빌드를 실행하지 않는다. +- 빌드 성공 산출물이 없는데 버전만 먼저 활성화하지 않는다. +- 사용자가 명시적으로 요청하지 않은 앱 연결 순서 변경을 하지 않는다. +- 삭제, 제거, 폐기, unlink 계열 작업은 실행하지 않는다. + +## 런처 버전 업데이트 흐름 + +이미 등록된 런처의 새 버전을 반영할 때는 아래 순서를 따른다. + +1. 공통 설정 변경이 필요하면 `런처 > 공통 설정`에서 먼저 반영한다. +2. 런처 노출 앱이나 순서가 바뀌는 경우 `런처 > 앱 연결`에서 연결 상태를 저장한다. 사용자가 명시하지 않은 연결 변경은 하지 않는다. +3. `런처 > 빌드 관리`에서 새 버전에 필요한 빌드를 실행한다. +4. 빌드 작업이 성공하면 `런처 > 버전 관리`에서 새 버전, 최소 지원 버전, 다운로드 산출물, 릴리즈 노트를 등록하거나 갱신한다. +5. 필요한 경우 새 버전을 활성화한다. + +런처 버전 업데이트 흐름에서 금지되는 행동: + +- 빌드 성공 산출물이 없는 상태에서 버전만 활성화하지 않는다. +- 최소 지원 버전 변경은 사용자 요청 또는 정책 확인 없이 임의로 하지 않는다. +- 앱 연결 순서 변경은 사용자 요청 없이 하지 않는다. +- 삭제, 제거, 폐기, unlink 계열 작업은 실행하지 않는다. + +## 앱 릴리즈 흐름 + +1. `어플리케이션 > 빌드 관리`에서 develop 빌드를 실행한다. +2. develop 빌드가 성공하면 `어플리케이션 > 릴리즈 관리`에서 성공한 develop 빌드를 선택한다. +3. Admin BE가 develop에서 release로 fast-forward 승격한다. +4. release 브랜치 기준으로 재빌드하고 성공 산출물을 릴리즈로 저장한다. +5. `어플리케이션 > 배포 관리`에서 성공 릴리즈를 운영 배포한다. + +## 앱 버전 업데이트 흐름 + +이미 등록된 앱의 새 버전을 운영에 반영할 때는 아래 순서를 따른다. + +1. 환경변수나 Secret 변경이 필요한 경우 `어플리케이션 > 실행 설정`에서 먼저 반영한다. +2. DB 변경이 있는 경우 `어플리케이션 > 마이그레이션`에서 마이그레이션을 실행한다. +3. `어플리케이션 > 빌드 관리`에서 대상 구성요소의 develop 빌드를 실행한다. +4. develop 빌드가 성공하면 `어플리케이션 > 릴리즈 관리`에서 성공한 develop 빌드를 선택해 새 릴리즈 버전을 생성한다. +5. 릴리즈 생성 중 Admin BE가 develop에서 release로 fast-forward 승격하고 release 기준 재빌드를 실행한다. +6. release 재빌드가 성공하면 `어플리케이션 > 배포 관리`에서 성공 릴리즈를 운영 배포한다. + +앱 버전 업데이트 흐름에서 금지되는 행동: + +- 앱 repo의 release 브랜치로 직접 merge/push하라고 안내하지 않는다. +- develop 빌드 성공 전 릴리즈를 생성하지 않는다. +- 릴리즈 성공 전 배포를 실행하지 않는다. +- 이미 배포된 릴리즈를 다시 배포 후보로 선택하지 않는다. +- 삭제, 제거, 폐기, unlink 계열 작업은 실행하지 않는다. + +## 앱 폐기 흐름 + +사용자가 앱 삭제, 앱 제거, 앱 폐기를 요청하면 PageAgent는 실행하지 않는다. 아래 옵션의 의미만 설명하고, 실제 삭제/폐기 버튼 클릭은 사람이 직접 해야 한다. + +- `저장소 연결 정보 삭제`: 구성요소의 repository, branch, buildTarget 연결을 지운다. +- `실행 설정 삭제`: 앱 런타임 설정 정리를 의미한다. 기본 선택 대상이다. +- `Kubernetes 워크로드 중지`: Deployment/StatefulSet replicas를 0으로 내려 실행을 멈춘다. 기본 선택 대상이다. +- `Kubernetes 리소스 삭제`: 앱 구성요소 이름 기준의 Deployment, StatefulSet, Service, Ingress, ConfigMap, Secret을 삭제한다. 사용자가 명시적으로 원할 때만 선택한다. +- `PVC/데이터 볼륨 삭제`: DB/StatefulSet 데이터 삭제로 이어질 수 있다. 사용자가 데이터 삭제와 백업 확인을 명시한 경우에만 선택한다. + +기본적으로 `저장소 연결 정보 삭제`, `실행 설정 삭제`, `Kubernetes 워크로드 중지`만 선택한다. `Kubernetes 리소스 삭제`와 `PVC/데이터 볼륨 삭제`는 위험 작업이므로 임의로 선택하지 않는다. + +## 구성요소 등록 기준 + +- 소스가 있는 FE/BE/Worker는 저장소를 연결하고 빌드 대상으로 관리한다. +- Redis 같은 외부 이미지는 `CONTAINER` 유형과 `외부 이미지`로 등록한다. +- Redis 예시: 이름 `studio-auth-redis`, 이미지 `redis:7-alpine`, 포트 `6379`, targetPort `6379`, replicas `1`. +- Redis를 사용하는 BE에는 실행 설정에 `REDIS_URL=redis://studio-auth-redis:6379`처럼 등록한다. +- DB/MIGRATION 구성요소는 저장소 연결 목록에서 숨겨질 수 있다. + +## 용어 + +- `저장소 관리`: 저장소 생성과 구성요소 연결을 담당한다. +- `소스 관리`: 커밋 목록과 diff 조회를 담당한다. +- `조회 브랜치`: 커밋을 볼 브랜치일 뿐, 릴리즈나 배포 실행을 의미하지 않는다. +- `세션`: refresh token/session 식별자다. +- `상태`: 세션 또는 사용자 상태 판정값이다. 예: 활성, 만료, 종료, 잠김, 없음. diff --git a/package-lock.json b/package-lock.json index b25be5c..94b2029 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@tailwindcss/vite": "^4.3.0", "@vitejs/plugin-react": "^4.4.1", "lucide-react": "^1.14.0", + "page-agent": "file:../../page-agent/packages/page-agent", "react": "^19.1.0", "react-dom": "^19.1.0", "react-router-dom": "^7.18.0", @@ -23,6 +24,23 @@ "typescript": "^5.8.3" } }, + "../../page-agent/packages/page-agent": { + "version": "1.11.0", + "license": "MIT", + "dependencies": { + "@page-agent/core": "1.11.0", + "@page-agent/llms": "1.11.0", + "@page-agent/page-controller": "1.11.0", + "@page-agent/ui": "1.11.0", + "chalk": "^5.6.2" + }, + "devDependencies": { + "zod": "^4.4.3" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -2065,6 +2083,10 @@ "node": ">=18" } }, + "node_modules/page-agent": { + "resolved": "../../page-agent/packages/page-agent", + "link": true + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", diff --git a/package.json b/package.json index fd222b3..0fbd6e0 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@tailwindcss/vite": "^4.3.0", "@vitejs/plugin-react": "^4.4.1", "lucide-react": "^1.14.0", + "page-agent": "file:../../page-agent/packages/page-agent", "react": "^19.1.0", "react-dom": "^19.1.0", "react-router-dom": "^7.18.0", diff --git a/src/App.tsx b/src/App.tsx index 7ba3a1f..5bd96f8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState, type FormEvent, type ReactNode } from 'react'; +import { useEffect, useRef, useState, type FormEvent, type MouseEvent as ReactMouseEvent, type ReactNode } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { Activity, @@ -17,6 +17,7 @@ import { formatDateTime as formatDateTimeValue } from './lib/date-format'; import { apiBaseUrl, apiRequest, authExpiredEventName, clearAdminSession } from './lib/api'; import { filterAppsByCurrentAdmin } from './lib/admin-app-permissions'; import { cn } from './lib/utils'; +import { openPageAgent } from './lib/page-agent'; import { AppManagementPage as AppManagementApiPage } from './pages/applications/AppManagementPage'; import { BuildManagementPage } from './pages/applications/BuildManagementPage'; import { DeploymentManagementPage } from './pages/applications/DeploymentManagementPage'; @@ -446,6 +447,19 @@ function filterNavGroupsByRole(groups: typeof navGroups, role?: string | null) { .filter((group) => group.items.length > 0); } +function pageAgentAccessAttrs(allowed: boolean) { + return allowed + ? {} + : { + 'data-page-agent-ignore': 'true', + 'data-page-agent-not-interactive': 'true' + }; +} + +function isTrustedClick(event: ReactMouseEvent) { + return event.nativeEvent.isTrusted; +} + function Sidebar({ activeView, menuSearch, @@ -491,8 +505,11 @@ function Sidebar({