Merge remote-tracking branch 'origin/develop' into release
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-07-06 17:50:00 +09:00
2 changed files with 30 additions and 10 deletions
@@ -74,6 +74,16 @@ export function DeploymentManagementPage() {
() => releases.filter((release) => !selectedAppId || release.appId === selectedAppId),
[releases, selectedAppId]
);
const deployedReleaseIds = useMemo(
() => new Set(deployments
.filter((deployment) => deployment.status === 'SUCCEEDED' && deployment.releaseId)
.map((deployment) => deployment.releaseId!)),
[deployments]
);
const deployableReleases = useMemo(
() => filteredReleases.filter((release) => !deployedReleaseIds.has(release.id)),
[filteredReleases, deployedReleaseIds]
);
const filteredDeployments = useMemo(
() => deployments
.filter((deployment) => !selectedAppId || deployment.appId === selectedAppId)
@@ -110,11 +120,11 @@ export function DeploymentManagementPage() {
}, [selectedRelease?.id]);
useEffect(() => {
setReleaseId((prev) => filteredReleases.some((release) => release.id === prev) ? prev : filteredReleases[0]?.id || '');
setNamespace(filteredReleases[0]?.app?.namespace || '');
}, [filteredReleases]);
setReleaseId((prev) => deployableReleases.some((release) => release.id === prev) ? prev : deployableReleases[0]?.id || '');
setNamespace(deployableReleases[0]?.app?.namespace || '');
}, [deployableReleases]);
const deployableCards = useMemo(() => filteredReleases.slice(0, 8), [filteredReleases]);
const deployableCards = useMemo(() => deployableReleases.slice(0, 8), [deployableReleases]);
const openDeployModal = (release?: AppRelease) => {
if (release) {
@@ -145,7 +155,7 @@ export function DeploymentManagementPage() {
title="배포 관리"
description="생성된 릴리즈를 운영 환경에 배포하고 재배포와 롤백을 관리합니다."
actions={
<Button className="h-8 px-3 text-[13px]" disabled={filteredReleases.length === 0} onClick={() => openDeployModal()}>
<Button className="h-8 px-3 text-[13px]" disabled={deployableReleases.length === 0} onClick={() => openDeployModal()}>
<Plus className="size-4" />
배포 실행
</Button>
@@ -236,7 +246,7 @@ export function DeploymentManagementPage() {
</Card>
<Card className="min-w-0 overflow-hidden">
<CardHeader title="배포 가능 릴리즈" description="성공 상태의 릴리즈만 배포 대상으로 사용할 수 있습니다." />
<CardHeader title="배포 가능 릴리즈" description="아직 성공 배포되지 않은 릴리즈만 표시합니다." />
<div className="space-y-3 p-4">
{deployableCards.length === 0 ? (
<div className="rounded-md border border-border px-3 py-4 text-center text-sm text-muted-foreground">
@@ -269,7 +279,7 @@ export function DeploymentManagementPage() {
setNamespace(release?.app.namespace || '');
}}>
<option value="">릴리즈 선택</option>
{filteredReleases.map((release) => <option key={release.id} value={release.id}>{release.app.name} {release.version}</option>)}
{deployableReleases.map((release) => <option key={release.id} value={release.id}>{release.app.name} {release.version}</option>)}
</select>
<input className="h-9 w-full rounded-md border border-input bg-background px-3 text-sm outline-none" value={namespace} onChange={(event) => setNamespace(event.target.value)} placeholder="stdo-lms" />
<label className="flex items-center gap-2 rounded-md border border-border bg-muted/20 px-3 py-2 text-sm text-foreground">
+13 -3
View File
@@ -209,9 +209,11 @@ export function RuntimeConfigPage() {
const addVariable = () => {
if (!selectedProfile || !variableComponentId || !variableKey.trim() || !variableValue.trim()) return;
const key = variableKey.trim();
setError('');
createRuntimeVariable(selectedProfile.id, {
componentId: variableComponentId,
key: variableKey.trim(),
key,
value: variableValue.trim(),
isSecret: variableIsSecret
})
@@ -219,9 +221,17 @@ export function RuntimeConfigPage() {
setVariableKey('');
setVariableValue('');
setVariableIsSecret(false);
toast({
type: 'success',
title: '설정값 등록',
message: `${key} 설정값을 추가했습니다. 실행 중인 파드에는 적용 버튼을 눌러야 반영됩니다.`
});
refreshProfiles(appId, selectedProfile.id);
})
.catch((createError: Error) => setError(createError.message));
.catch((createError: Error) => {
setError(createError.message);
toast({ type: 'error', title: '설정값 등록 실패', message: createError.message });
});
};
const removeVariable = (variableId: string) => {
@@ -497,7 +507,7 @@ export function RuntimeConfigPage() {
<label className="block">
<span className="text-xs font-medium text-muted-foreground">Key</span>
<div className="mt-2 flex items-center gap-2">
<input className="h-9 min-w-0 flex-1 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={variableKey} onChange={(event) => setVariableKey(event.target.value)} placeholder="DATABASE_URL" />
<input className="h-9 min-w-0 flex-1 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={variableKey} onChange={(event) => setVariableKey(event.target.value)} placeholder="키 입력" />
<label className="flex shrink-0 items-center gap-2 text-xs text-muted-foreground">
<input type="checkbox" className="size-4 accent-primary" checked={variableIsSecret} onChange={(event) => setVariableIsSecret(event.target.checked)} />
Secret