init studio admin frontend

This commit is contained in:
2026-07-01 17:34:15 +09:00
commit c50bd2b941
64 changed files with 22452 additions and 0 deletions
@@ -0,0 +1,111 @@
import { useEffect, useMemo, useState } from 'react';
import { Badge } from '../../components/ui';
import type { AppWithComponents, JobStatus } from '../../types/application';
import { listApps } from './application-api';
import { getStoredSelectedAppId, setStoredSelectedAppId } from '../../lib/selected-app';
import { formatDateTime } from '../../lib/date-format';
export function useApps() {
const [apps, setApps] = useState<AppWithComponents[]>([]);
const [selectedAppId, setSelectedAppId] = useState(() => getStoredSelectedAppId());
const [loading, setLoading] = useState(false);
const refreshApps = async () => {
setLoading(true);
try {
const nextApps = await listApps();
setApps(nextApps);
const storedAppId = getStoredSelectedAppId();
const validAppId = nextApps.some((app) => app.id === storedAppId) ? storedAppId : nextApps[0]?.id ?? '';
if (validAppId) {
setSelectedAppId(validAppId);
setStoredSelectedAppId(validAppId);
}
} finally {
setLoading(false);
}
};
useEffect(() => {
void refreshApps();
}, []);
const selectedApp = useMemo(
() => apps.find((app) => app.id === selectedAppId) ?? apps[0] ?? null,
[apps, selectedAppId]
);
const selectApp = (appId: string) => {
setSelectedAppId(appId);
setStoredSelectedAppId(appId);
};
return { apps, selectedApp, selectedAppId: selectedApp?.id ?? '', loading, refreshApps, selectApp };
}
export function AppSelector({
apps,
value,
onChange,
loading
}: {
apps: AppWithComponents[];
value: string;
onChange: (appId: string) => void;
loading?: boolean;
}) {
return (
<div className="mb-5 flex flex-col gap-2 rounded-lg border border-border bg-card px-5 py-4 md:flex-row md:items-center md:justify-between">
<div>
<p className="text-sm font-semibold text-foreground">앱 선택</p>
<p className="mt-1 text-xs text-muted-foreground">선택한 앱 기준으로 소스, 빌드, 릴리즈, 배포 데이터를 봅니다.</p>
</div>
<select
className="h-9 min-w-64 rounded-md border border-input bg-background px-3 text-sm text-foreground outline-none focus:ring-2 focus:ring-ring"
value={value}
disabled={loading || apps.length === 0}
onChange={(event) => onChange(event.target.value)}
>
{apps.length === 0 ? <option value="">등록된 앱을 선택해주세요</option> : null}
{apps.map((app) => (
<option key={app.id} value={app.id}>
{app.name}
</option>
))}
</select>
</div>
);
}
export function StatusBadge({ status }: { status: string }) {
const normalized = status.toUpperCase();
const label = {
SUCCEEDED: '성공',
ACTIVE: '활성',
READY: '준비',
RUNNING: '실행 중',
PENDING: '대기',
FAILED: '실패',
DISABLED: '비활성',
CANCELED: '취소'
}[normalized] ?? status;
if (normalized === 'SUCCEEDED' || normalized === 'ACTIVE' || normalized === 'READY' || normalized === 'RUNNING' || status === '성공' || status === '활성' || status === '준비' || status === '실행 중') {
return <Badge tone={normalized === 'RUNNING' || status === '실행 중' ? 'blue' : 'green'}>{label}</Badge>;
}
if (normalized === 'PENDING' || status === '대기') return <Badge tone="amber">{label}</Badge>;
if (normalized === 'FAILED' || normalized === 'DISABLED' || normalized === 'CANCELED' || status === '실패' || status === '비활성' || status === '취소') return <Badge tone="red">{label}</Badge>;
return <Badge>{label}</Badge>;
}
export function isBuildDone(status: JobStatus) {
return !['PENDING', 'RUNNING'].includes(status);
}
export function formatDate(value?: string | null) {
return formatDateTime(value);
}
export function EmptyState({ children }: { children: string }) {
return <div className="flex min-h-40 items-center justify-center text-sm text-muted-foreground">{children}</div>;
}