This commit is contained in:
@@ -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
|
||||
```
|
||||
@@ -81,6 +81,7 @@ export function LauncherBuildPage() {
|
||||
const [commitSha, setCommitSha] = useState('');
|
||||
const [jobs, setJobs] = useState<LauncherBuildJob[]>([]);
|
||||
const [selectedJob, setSelectedJob] = useState<LauncherBuildJob | null>(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={
|
||||
<Button className="h-8 px-3 text-[13px]" onClick={createJob} disabled={isBackend && !version.trim()}>
|
||||
<Button className="h-8 px-3 text-[13px]" onClick={() => setConfirmOpen(true)} disabled={isBackend && !version.trim()}>
|
||||
<Play className="size-4" />
|
||||
실행
|
||||
</Button>
|
||||
@@ -231,14 +232,14 @@ export function LauncherBuildPage() {
|
||||
<span className="text-xs font-medium text-muted-foreground">Commit SHA</span>
|
||||
<input className="mt-2 h-9 w-full rounded-md border border-input bg-background px-3 font-mono text-sm text-foreground outline-none placeholder:text-muted-foreground focus:ring-2 focus:ring-ring" value={commitSha} onChange={(event) => setCommitSha(event.target.value)} placeholder="비워두면 브랜치 최신 커밋" />
|
||||
</label>
|
||||
<div className="rounded-md border border-border bg-background p-3">
|
||||
<p className="text-xs text-muted-foreground">{isBackend ? '이미지' : '산출물'}</p>
|
||||
<p className="mt-2 break-all font-mono text-xs text-foreground">{isBackend ? backendImageTag : clientArtifactPreview}</p>
|
||||
<p className="mt-2 text-xs text-muted-foreground">{isBackend ? 'Dockerfile' : 'R2 Object Key'}</p>
|
||||
<p className="mt-1 break-all font-mono text-xs text-foreground">{isBackend ? backendDockerfile : clientArtifactPreview}</p>
|
||||
<p className="mt-2 text-xs text-muted-foreground">{isBackend ? 'Repository' : '다운로드 URL'}</p>
|
||||
<p className="mt-1 break-all font-mono text-xs text-foreground">{isBackend ? backendSource?.repository || '-' : clientArtifactPreview}</p>
|
||||
</div>
|
||||
{isBackend ? (
|
||||
<div className="rounded-md border border-border bg-background p-3">
|
||||
<p className="text-xs text-muted-foreground">이미지</p>
|
||||
<p className="mt-2 break-all font-mono text-xs text-foreground">{backendImageTag}</p>
|
||||
<p className="mt-2 text-xs text-muted-foreground">Repository</p>
|
||||
<p className="mt-1 break-all font-mono text-xs text-foreground">{backendSource?.repository || '-'}</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -331,6 +332,55 @@ export function LauncherBuildPage() {
|
||||
{selectedJob?.logs || '작업을 선택하면 로그가 표시됩니다.'}
|
||||
</pre>
|
||||
</Modal>
|
||||
<Modal
|
||||
open={confirmOpen}
|
||||
title="빌드 실행 확인"
|
||||
onClose={() => setConfirmOpen(false)}
|
||||
footer={
|
||||
<>
|
||||
<Button variant="outline" onClick={() => setConfirmOpen(false)}>취소</Button>
|
||||
<Button onClick={createJob} disabled={loading || (isBackend && !version.trim())}>
|
||||
<Play className="size-4" />
|
||||
실행
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="space-y-3 text-sm">
|
||||
<div className="rounded-md border border-border bg-muted/20 px-4 py-3">
|
||||
<p className="font-medium">{isBackend ? '런처 백엔드 이미지 빌드' : '런처 클라이언트 설치 파일 빌드'}를 실행합니다.</p>
|
||||
<p className="mt-1 text-xs leading-5 text-muted-foreground">
|
||||
실행 후 빌드 작업이 생성되고, 작업 목록에서 진행 상태와 로그를 확인할 수 있습니다.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-2 rounded-md border border-border bg-background p-4">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<span className="text-muted-foreground">대상</span>
|
||||
<span className="font-medium">{isBackend ? '백엔드' : '클라이언트'}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<span className="text-muted-foreground">저장소</span>
|
||||
<span className="truncate font-medium">
|
||||
{isBackend ? backendSource?.repository || '-' : clientSource?.repository || activeVersion?.repository || '-'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<span className="text-muted-foreground">브랜치</span>
|
||||
<span className="font-medium">{branch}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<span className="text-muted-foreground">커밋</span>
|
||||
<span className="font-mono text-xs font-medium">{commitSha.trim() || '브랜치 최신'}</span>
|
||||
</div>
|
||||
{isBackend ? (
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<span className="text-muted-foreground">이미지</span>
|
||||
<span className="truncate font-mono text-xs font-medium">{backendImageTag}</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<LoadingModal open={loadingVisible} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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<SourceTarget, { title: string; description: string; defaultOrg: string }> = {
|
||||
@@ -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) => <option key={repoFullName(repo)} value={repoFullName(repo)} />)}
|
||||
</datalist>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="text-xs font-medium text-muted-foreground">브랜치</span>
|
||||
<select
|
||||
className="mt-2 h-9 w-full rounded-md border border-input bg-background px-3 text-sm text-foreground outline-none focus:ring-2 focus:ring-ring"
|
||||
value={activeForm.branch}
|
||||
onChange={(event) => updateForm({ branch: event.target.value })}
|
||||
>
|
||||
<option value="develop">develop</option>
|
||||
<option value="release">release</option>
|
||||
<option value="main">main</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="block md:col-span-3">
|
||||
<span className="text-xs font-medium text-muted-foreground">Commit SHA</span>
|
||||
<input
|
||||
className="mt-2 h-9 w-full rounded-md border border-input bg-background px-3 font-mono text-sm text-foreground outline-none placeholder:text-muted-foreground focus:ring-2 focus:ring-ring"
|
||||
value={activeForm.commitSha}
|
||||
onChange={(event) => updateForm({ commitSha: event.target.value })}
|
||||
placeholder="비워두면 브랜치 최신 커밋 기준"
|
||||
/>
|
||||
</label>
|
||||
<div className="rounded-md border border-border bg-muted/20 px-3 py-2">
|
||||
<p className="text-xs font-medium text-muted-foreground">빌드 기준</p>
|
||||
<p className="mt-1 text-sm font-medium text-foreground">develop 최신 성공 빌드</p>
|
||||
</div>
|
||||
{activeTarget === 'launcher-backend' ? (
|
||||
<>
|
||||
<label className="block md:col-span-2">
|
||||
<label className="block md:col-span-3">
|
||||
<span className="text-xs font-medium text-muted-foreground">이미지 이름</span>
|
||||
<input
|
||||
className="mt-2 h-9 w-full rounded-md border border-input bg-background px-3 font-mono text-sm text-foreground outline-none placeholder:text-muted-foreground focus:ring-2 focus:ring-ring"
|
||||
@@ -319,21 +291,12 @@ export function LauncherSourcePage() {
|
||||
placeholder="localhost:5000/studio-launcher-be"
|
||||
/>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="text-xs font-medium text-muted-foreground">Dockerfile</span>
|
||||
<input
|
||||
className="mt-2 h-9 w-full rounded-md border border-input bg-background px-3 font-mono text-sm text-foreground outline-none placeholder:text-muted-foreground focus:ring-2 focus:ring-ring"
|
||||
value={activeForm.dockerfile}
|
||||
onChange={(event) => updateForm({ dockerfile: event.target.value })}
|
||||
placeholder="Dockerfile"
|
||||
/>
|
||||
</label>
|
||||
</>
|
||||
) : null}
|
||||
<div className="flex justify-end md:col-span-3">
|
||||
<Button onClick={save} disabled={!activeForm.repository.trim() || saving}>
|
||||
<Link2 className="size-4" />
|
||||
저장
|
||||
저장소 연결 저장
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -377,10 +340,7 @@ export function LauncherSourcePage() {
|
||||
</div>
|
||||
|
||||
<Card className="mt-4 min-w-0 overflow-hidden">
|
||||
<CardHeader
|
||||
title="최근 커밋"
|
||||
description={`${activeTargetMeta.title} 저장소의 ${activeForm.branch || 'develop'} 브랜치 기준 최근 5개 커밋입니다.`}
|
||||
/>
|
||||
<CardHeader title="최근 커밋" description={`${activeTargetMeta.title} 저장소의 develop 브랜치 기준 최근 5개 커밋입니다.`} />
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-[760px] w-full text-left text-sm">
|
||||
<thead className="border-y border-border bg-muted/30 text-xs text-muted-foreground">
|
||||
|
||||
@@ -174,7 +174,11 @@ export function LauncherVersionsPage() {
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader title="릴리즈 노트" description="최근 런처 배포 메모입니다." />
|
||||
<div className="p-5 text-sm leading-6 text-muted-foreground">{launcher.releaseNotes || '-'}</div>
|
||||
<div className="p-5">
|
||||
<div className="min-h-32 rounded-md border border-neutral-800 bg-neutral-950 px-4 py-3 text-sm leading-6 text-neutral-100">
|
||||
<p className="whitespace-pre-wrap">{launcher.releaseNotes || '-'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
Reference in New Issue
Block a user