diff --git a/docs/admin-fe-architecture-and-flow-v0.1.0.md b/docs/admin-fe-architecture-and-flow-v0.1.0.md new file mode 100644 index 0000000..fbe5fbf --- /dev/null +++ b/docs/admin-fe-architecture-and-flow-v0.1.0.md @@ -0,0 +1,374 @@ +# Studio Admin FE 구성과 운영 흐름 + +문서 버전: 0.1.0 +기준 앱 버전: 0.1.0 +기준일: 2026-07-03 + +## 개요 + +Studio Admin FE는 스튜디오 플랫폼 운영 콘솔이다. 런처, 어플리케이션, 실행 설정, 배포, 마이그레이션, 사용자/라이센스/공지 관리를 한 화면에서 처리한다. + +프론트엔드는 Vite + React 기반이며, API 호출은 `VITE_API_BASE_URL` 기준의 Admin BE로 전달된다. 인증 토큰은 `localStorage`의 `studio-admin-token`을 사용하고, 401 응답이 오면 세션을 지우고 로그인 만료 이벤트를 발생시킨다. + +## 주요 환경변수 + +```text +VITE_API_BASE_URL Admin BE API 주소 +VITE_GITEA_BASE_URL Gitea 웹 주소 +VITE_GITEA_SSH_BASE_URL Gitea SSH clone 주소 +VITE_GITEA_APP_ORG 앱 저장소 기본 조직 +VITE_R2_PUBLIC_BASE_URL R2 공개 다운로드 base URL +VITE_R2_ARTIFACT_PREFIX R2 artifact prefix, 기본 studio-artifacts +``` + +## 라우팅과 메뉴 + +메뉴 정의는 `src/layout/navigation.ts`에 있다. 실제 화면 매핑은 `src/App.tsx`의 `pathByView`와 `viewByPath`에서 관리한다. + +주요 메뉴: + +```text +대시보드 + +런처 +- 소스 관리 +- 빌드 관리 +- 버전 관리 +- 구성요소 +- 공통 설정 +- 앱 연결 + +어플리케이션 +- 앱 관리 +- 소스 관리 +- 빌드 관리 +- 릴리즈 관리 +- 실행 설정 +- 배포 관리 +- 마이그레이션 +- 로그/이벤트 + +운영 +- 관리자 관리 +- 사용자/세션 +- 라이센스 +- 공지사항 +``` + +## API 호출 구조 + +공통 API 유틸은 `src/lib/api.ts`에 있다. + +```text +apiBaseUrl = VITE_API_BASE_URL || http://localhost:4500/api +apiRequest(path) -> `${apiBaseUrl}${path}` +Authorization -> localStorage studio-admin-token +401 -> clearAdminSession + studio-admin-auth-expired event +``` + +도메인별 API 래퍼: + +```text +src/pages/launcher/launcher-api.ts 런처 소스/빌드/버전/앱 연결/공통 설정 +src/pages/applications/application-api.ts 앱/구성요소/소스/빌드/릴리즈/배포/마이그레이션 +src/pages/runtime/runtime-api.ts 실행 설정, namespace 리소스, 로그, scale/restart +src/pages/operations/operations-api.ts 관리자, 사용자 세션, 라이센스, 공지사항 +``` + +## 런처 관리 흐름 + +### 소스 관리 + +런처 클라이언트와 백엔드 소스 저장소를 나눠 관리한다. + +```text +클라이언트 저장소 -> Electron 런처 .deb 빌드 +백엔드 저장소 -> 런처 백엔드 컨테이너 이미지 빌드 +``` + +관련 API: + +```text +GET /launcher/source +PATCH /launcher/source +GET /gitea/repos?owner=platform +GET /gitea/repos?owner=studio +``` + +### 빌드 관리 + +런처 빌드는 클라이언트/백엔드로 나뉜다. + +클라이언트 빌드: + +```text +1. 저장소 clone +2. branch/commit checkout +3. npm ci && npm run build:linux +4. 생성된 .deb 탐색 +5. package.json version과 실제 .deb 파일명 기준으로 R2 key/download URL 확정 +6. R2 업로드 +7. latest deb/install.sh 갱신 +``` + +백엔드 빌드: + +```text +1. 저장소 clone +2. branch/commit checkout +3. package.json version 확인 +4. docker build +5. docker push +``` + +현재 정책상 런처 클라이언트 버전은 화면 입력값이 아니라 소스의 `package.json` 버전을 최종 기준으로 사용한다. + +### 버전 관리 + +런처 앱 업데이트 API가 바라보는 최신 런처 버전 정보를 관리한다. + +관리 값: + +```text +version +minimumVersion +artifact +downloadUrl +releaseNotes +repository +branch +commitSha +isActive +``` + +### 앱 연결 + +Admin 앱/릴리즈를 런처 앱 목록에 연결한다. 런처에서 사용자가 보는 앱 목록, 표시 순서, 실행 경로, 동영상, 아이콘 키, 활성 여부를 관리한다. + +주요 값: + +```text +appId +releaseId +displayOrder +isEnabled +displayName +description +subtitle +detailUrl +videoPath +iconKey +launchUrl +devAppPath +execPath +artifact +``` + +## 어플리케이션 관리 흐름 + +### 앱 관리 + +앱과 구성요소를 등록한다. + +앱 주요 값: + +```text +key +name +description +namespace +status +``` + +구성요소 주요 값: + +```text +name +type +repository +branch +buildTarget +imageSource +externalImage +workloadType +replicas +serviceType +storageEnabled +ports +``` + +구성요소는 빌드 대상과 런타임 배포 대상의 기본 단위다. + +### 소스 관리 + +구성요소별 Gitea 저장소와 브랜치를 연결한다. + +```text +앱 구성요소 -> repository/branch/buildTarget 연결 +``` + +### 빌드 관리 + +구성요소별 빌드 작업을 만든다. + +빌드 타입은 구성요소 설정에 따라 달라진다. + +```text +container-image -> docker build/push +artifact -> AppImage/deb 등 산출물 빌드 후 R2 업로드 +migration -> 마이그레이션 이미지/작업에 사용 +``` + +앱 artifact 빌드는 런처와 달리 빌드 후 `package.json` version을 읽어 R2 경로에 반영한다. + +### 릴리즈 관리 + +빌드 결과를 묶어 앱 릴리즈를 생성한다. + +```text +앱 선택 +버전 입력 +브랜치 선택 +빌드 결과 선택 +릴리즈 생성 +``` + +최근 수정 기준: + +```text +이미 릴리즈에 사용된 빌드 결과는 후보에서 제외 +최근 빌드 결과 위주로 기본 선택 +빌드 산출물에서 버전 정보 표시 +``` + +### 실행 설정 + +런타임 프로필을 만든다. 실제 배포 환경 차이는 이 프로필에 많이 담긴다. + +프로필 기본 값: + +```text +appId +name +environment: dev 또는 prod +namespace +``` + +프로필에 포함되는 값: + +```text +환경변수 +시크릿 +Ingress rule +componentId +host +path +servicePort +stripPrefix +tlsEnabled +tlsSecretName +``` + +`적용`을 누르면 Admin BE가 Kubernetes 리소스를 생성/갱신한다. + +### 배포 관리 + +생성된 릴리즈를 namespace에 배포한다. + +입력 값: + +```text +releaseId +environment: dev 또는 prod +namespace +updateLauncherAppVersion +``` + +현재 `environment`는 주로 배포 이력의 분류 라벨이다. 실제 리소스 차이는 namespace, 실행 설정, env/secret, ingress, image/artifact로 결정된다. + +같은 namespace와 같은 구성요소에 다른 버전을 배포하면 기존 Pod/리소스는 새 버전으로 교체된다. 다른 namespace에 배포하면 버전별/환경별 병행 운영이 가능하다. + +### 마이그레이션 + +릴리즈와 마이그레이션 구성요소를 기준으로 Kubernetes Job을 실행한다. + +주요 입력: + +```text +releaseId +migrationComponentId +imageComponentName +namespace +command +envSecretName +``` + +## 운영 관리 흐름 + +### 관리자 관리 + +관리자 계정, 역할, 활성화 상태, 임시 비밀번호, SSH 키를 관리한다. + +### 사용자/세션 + +런처/공통 인증 사용자 세션을 조회하고, 사용자 잠금/잠금 해제와 세션 종료를 수행한다. + +### 라이센스 + +라이센스 플랜과 사용자 앱 권한을 관리한다. 런처에서 앱 목록이 사용자별로 다르게 보이는 근거가 된다. + +### 공지사항 + +런처 로그인 또는 앱별 공지사항을 관리한다. + +공지 주요 값: + +```text +appKey +title +body +linkUrl +isActive +startsAt +endsAt +priority +``` + +## 현재 운영상 주의점 + +### 빌드 버전 + +런처 클라이언트와 앱 artifact는 `package.json` 버전을 기준으로 산출물 버전/R2 경로를 맞추는 방향이 맞다. 배포 대상 변경 시 package version도 같이 올려야 한다. + +### 배포 환경 + +`dev/prod` 셀렉트는 현재 분류값이다. 실제 분리는 namespace와 실행 설정 프로필로 한다. + +추천 예: + +```text +dev -> stdo-lms-dev +prod -> stdo-lms +``` + +### 같은 태그 이미지 + +같은 Docker 태그를 다시 push하면 로컬/레지스트리에는 이전 이미지 레이어가 남을 수 있다. Kubernetes도 같은 태그만으로는 항상 Pod를 재시작하지 않는다. 명확한 재배포가 필요하면 rollout restart 또는 버전 태그 변경이 필요하다. + +### 런처 업데이트 + +런처 업데이트 후에는 기존 프로세스를 완전히 종료하고 새 프로세스로 재실행해야 한다. 그렇지 않으면 업데이트 직후 AppImage 실행 환경이 꼬일 수 있다. + +## 관련 파일 + +```text +src/App.tsx +src/layout/navigation.ts +src/lib/api.ts +src/pages/launcher/launcher-api.ts +src/pages/applications/application-api.ts +src/pages/runtime/runtime-api.ts +src/pages/operations/operations-api.ts +src/types/application.ts +src/types/runtime.ts +``` diff --git a/src/pages/launcher/LauncherBuildPage.tsx b/src/pages/launcher/LauncherBuildPage.tsx index 82f1fae..64b20d7 100644 --- a/src/pages/launcher/LauncherBuildPage.tsx +++ b/src/pages/launcher/LauncherBuildPage.tsx @@ -81,6 +81,7 @@ export function LauncherBuildPage() { const [commitSha, setCommitSha] = useState(''); const [jobs, setJobs] = useState([]); const [selectedJob, setSelectedJob] = useState(null); + const [confirmOpen, setConfirmOpen] = useState(false); const [loading, setLoading] = useState(true); const [page, setPage] = useState(1); const loadingVisible = useMinimumLoading(loading); @@ -90,7 +91,6 @@ export function LauncherBuildPage() { const backendSource = source?.backend; const clientSource = source?.client; const artifactName = `autonomy-studio-launcher-${version || activeVersion?.version || '0.0.0'}.deb`; - const clientArtifactPreview = 'package.json 버전과 실제 .deb 파일명 기준으로 빌드 후 자동 확정'; const backendImageName = backendSource?.image || 'localhost:5000/studio-launcher-be'; const backendImageTag = `${backendImageName.replace(/\/$/, '')}:${version || '0.1.0'}`; const backendDockerfile = backendSource?.dockerfile || 'Dockerfile'; @@ -158,6 +158,7 @@ export function LauncherBuildPage() { }, [jobs, selectedJob]); const createJob = () => { + setConfirmOpen(false); setLoading(true); createLauncherBuildJob({ target, @@ -188,7 +189,7 @@ export function LauncherBuildPage() { title="빌드 관리" description="런처 클라이언트 설치 파일과 백엔드 컨테이너 이미지를 구성요소별로 빌드합니다." actions={ - @@ -231,14 +232,14 @@ export function LauncherBuildPage() { Commit SHA setCommitSha(event.target.value)} placeholder="비워두면 브랜치 최신 커밋" /> -
-

{isBackend ? '이미지' : '산출물'}

-

{isBackend ? backendImageTag : clientArtifactPreview}

-

{isBackend ? 'Dockerfile' : 'R2 Object Key'}

-

{isBackend ? backendDockerfile : clientArtifactPreview}

-

{isBackend ? 'Repository' : '다운로드 URL'}

-

{isBackend ? backendSource?.repository || '-' : clientArtifactPreview}

-
+ {isBackend ? ( +
+

이미지

+

{backendImageTag}

+

Repository

+

{backendSource?.repository || '-'}

+
+ ) : null} @@ -331,6 +332,55 @@ export function LauncherBuildPage() { {selectedJob?.logs || '작업을 선택하면 로그가 표시됩니다.'} + setConfirmOpen(false)} + footer={ + <> + + + + } + > +
+
+

{isBackend ? '런처 백엔드 이미지 빌드' : '런처 클라이언트 설치 파일 빌드'}를 실행합니다.

+

+ 실행 후 빌드 작업이 생성되고, 작업 목록에서 진행 상태와 로그를 확인할 수 있습니다. +

+
+
+
+ 대상 + {isBackend ? '백엔드' : '클라이언트'} +
+
+ 저장소 + + {isBackend ? backendSource?.repository || '-' : clientSource?.repository || activeVersion?.repository || '-'} + +
+
+ 브랜치 + {branch} +
+
+ 커밋 + {commitSha.trim() || '브랜치 최신'} +
+ {isBackend ? ( +
+ 이미지 + {backendImageTag} +
+ ) : null} +
+
+
); diff --git a/src/pages/launcher/LauncherSourcePage.tsx b/src/pages/launcher/LauncherSourcePage.tsx index e6fb45b..e0c510a 100644 --- a/src/pages/launcher/LauncherSourcePage.tsx +++ b/src/pages/launcher/LauncherSourcePage.tsx @@ -25,10 +25,7 @@ type SourceTarget = 'launcher-client' | 'launcher-backend'; type SourceForm = { repository: string; - branch: string; - commitSha: string; image: string; - dockerfile: string; }; const targetLabels: Record = { @@ -74,10 +71,7 @@ function sshCloneUrl(repository?: string | null, fallbackOrg?: string) { function emptyForm(): SourceForm { return { repository: '', - branch: 'release', - commitSha: '', - image: 'localhost:5000/studio-launcher-be', - dockerfile: 'Dockerfile' + image: 'localhost:5000/studio-launcher-be' }; } @@ -135,6 +129,7 @@ export function LauncherSourcePage() { const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const loadingVisible = useMinimumLoading(loading); + const sourceBranch = 'develop'; const activeVersion = useMemo(() => versions.find((version) => version.isActive) ?? versions[0], [versions]); const activeForm = forms[activeTarget]; @@ -164,17 +159,11 @@ export function LauncherSourcePage() { setForms({ 'launcher-client': { repository: clientRepository, - branch: sourceResult.source.client?.branch ?? sourceResult.source.branch ?? nextVersion?.branch ?? 'release', - commitSha: sourceResult.source.client?.commitSha ?? sourceResult.source.commitSha ?? nextVersion?.commitSha ?? '', - image: '', - dockerfile: '' + image: '' }, 'launcher-backend': { repository: backendRepository, - branch: sourceResult.source.backend?.branch ?? 'release', - commitSha: sourceResult.source.backend?.commitSha ?? '', - image: sourceResult.source.backend?.image ?? 'localhost:5000/studio-launcher-be', - dockerfile: sourceResult.source.backend?.dockerfile ?? 'Dockerfile' + image: sourceResult.source.backend?.image ?? 'localhost:5000/studio-launcher-be' } }); }) @@ -199,7 +188,7 @@ export function LauncherSourcePage() { let canceled = false; setCommitsLoading(true); setCommitsError(''); - listGiteaCommits(repository.owner, repository.repo, activeForm.branch || 'develop', 5) + listGiteaCommits(repository.owner, repository.repo, sourceBranch, 5) .then((nextCommits) => { if (!canceled) setCommits(nextCommits); }) @@ -216,7 +205,7 @@ export function LauncherSourcePage() { return () => { canceled = true; }; - }, [activeForm.repository, activeForm.branch, activeTargetMeta.defaultOrg]); + }, [activeForm.repository, activeTargetMeta.defaultOrg, sourceBranch]); const save = () => { if (!activeForm.repository.trim()) return; @@ -224,10 +213,10 @@ export function LauncherSourcePage() { updateLauncherSource({ target: activeTarget, repository: activeForm.repository.trim(), - branch: activeForm.branch.trim() || undefined, - commitSha: activeForm.commitSha.trim() || undefined, + branch: sourceBranch, + commitSha: '', image: activeTarget === 'launcher-backend' ? activeForm.image.trim() || undefined : undefined, - dockerfile: activeTarget === 'launcher-backend' ? activeForm.dockerfile.trim() || undefined : undefined + dockerfile: activeTarget === 'launcher-backend' ? 'Dockerfile' : undefined }) .then(() => { toast({ type: 'success', title: '소스 연결', message: `${activeTargetMeta.title} 소스 연결을 저장했습니다.` }); @@ -287,30 +276,13 @@ export function LauncherSourcePage() { {repos.map((repo) =>