Merge pull request 'release: 2026-04-16.3 (6건 커밋) — Admin 디자인 시스템 Phase 1-A' (#55) from develop into main
All checks were successful
Build and Deploy KCG AI Monitoring (Frontend) / build-and-deploy (push) Successful in 15s
All checks were successful
Build and Deploy KCG AI Monitoring (Frontend) / build-and-deploy (push) Successful in 15s
This commit is contained in:
커밋
020b3b7643
@ -4,6 +4,13 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [2026-04-16.3]
|
||||||
|
|
||||||
|
### 변경
|
||||||
|
- **Admin 3개 페이지 디자인 시스템 준수 리팩토링 (Phase 1-A)** — PerformanceMonitoring/DataRetentionPolicy/DataModelVerification 자체 탭 네비 → `TabBar/TabButton` 공통 컴포넌트, 원시 `<button>` → `TabButton`, PerformanceMonitoring 정적 hex 9건 → `performanceStatus` 카탈로그 경유
|
||||||
|
- **신규 카탈로그** `shared/constants/performanceStatus.ts` — PerformanceStatus(good/warning/critical/running/passed/failed/active/scheduled/archived) → {intent, hex, label} + utilizationStatus(ratio) 헬퍼
|
||||||
|
- **RBAC skeleton** — 3개 페이지 최상단 `useAuth().hasPermission('admin:{resource}', 'OP')` 호출 배치 (Phase 3 액션 버튼 추가 시 가드로 연결)
|
||||||
|
|
||||||
## [2026-04-16.2]
|
## [2026-04-16.2]
|
||||||
|
|
||||||
### 추가
|
### 추가
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Card, CardContent } from '@shared/components/ui/card';
|
import { Card, CardContent } from '@shared/components/ui/card';
|
||||||
import { Badge } from '@shared/components/ui/badge';
|
import { Badge } from '@shared/components/ui/badge';
|
||||||
|
import { TabBar, TabButton } from '@shared/components/ui/tabs';
|
||||||
import { PageContainer, PageHeader } from '@shared/components/layout';
|
import { PageContainer, PageHeader } from '@shared/components/layout';
|
||||||
import { getStatusIntent } from '@shared/constants/statusIntent';
|
import { getStatusIntent } from '@shared/constants/statusIntent';
|
||||||
|
import { useAuth } from '@/app/auth/AuthContext';
|
||||||
import {
|
import {
|
||||||
Database, CheckCircle, AlertTriangle, FileText, Users,
|
Database, CheckCircle, AlertTriangle, FileText, Users,
|
||||||
Layers, Table2, Search, ChevronRight, GitBranch,
|
Layers, Table2, Search, ChevronRight, GitBranch,
|
||||||
@ -105,6 +107,18 @@ const VERIFICATION_HISTORY = [
|
|||||||
|
|
||||||
export function DataModelVerification() {
|
export function DataModelVerification() {
|
||||||
const [tab, setTab] = useState<Tab>('overview');
|
const [tab, setTab] = useState<Tab>('overview');
|
||||||
|
const { hasPermission } = useAuth();
|
||||||
|
// 향후 Phase 3 에서 검증 승인·이력 등록 버튼 추가 시 가드로 연결
|
||||||
|
void hasPermission('admin:data-model-verification', 'CREATE');
|
||||||
|
void hasPermission('admin:data-model-verification', 'UPDATE');
|
||||||
|
|
||||||
|
const TABS: Array<{ key: Tab; icon: typeof Eye; label: string }> = [
|
||||||
|
{ key: 'overview', icon: Eye, label: '검증 현황' },
|
||||||
|
{ key: 'logical', icon: GitBranch, label: '논리 모델 검증' },
|
||||||
|
{ key: 'physical', icon: Database, label: '물리 모델 검증' },
|
||||||
|
{ key: 'duplication', icon: Search, label: '중복·정합성 점검' },
|
||||||
|
{ key: 'history', icon: FileText, label: '검증 결과 이력' },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
@ -116,21 +130,14 @@ export function DataModelVerification() {
|
|||||||
demo
|
demo
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 탭 */}
|
<TabBar variant="underline">
|
||||||
<div className="flex gap-0 border-b border-border">
|
{TABS.map(t => (
|
||||||
{([
|
<TabButton key={t.key} variant="underline" active={tab === t.key}
|
||||||
{ key: 'overview' as Tab, icon: Eye, label: '검증 현황' },
|
icon={<t.icon className="w-3.5 h-3.5" />} onClick={() => setTab(t.key)}>
|
||||||
{ key: 'logical' as Tab, icon: GitBranch, label: '논리 모델 검증' },
|
{t.label}
|
||||||
{ key: 'physical' as Tab, icon: Database, label: '물리 모델 검증' },
|
</TabButton>
|
||||||
{ key: 'duplication' as Tab, icon: Search, label: '중복·정합성 점검' },
|
|
||||||
{ key: 'history' as Tab, icon: FileText, label: '검증 결과 이력' },
|
|
||||||
]).map(t => (
|
|
||||||
<button type="button" key={t.key} onClick={() => setTab(t.key)}
|
|
||||||
className={`flex items-center gap-1.5 px-4 py-2.5 text-[11px] font-medium border-b-2 transition-colors ${tab === t.key ? 'text-green-400 border-green-400' : 'text-hint border-transparent hover:text-label'}`}>
|
|
||||||
<t.icon className="w-3.5 h-3.5" />{t.label}
|
|
||||||
</button>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</TabBar>
|
||||||
|
|
||||||
{/* ── ① 검증 현황 ── */}
|
{/* ── ① 검증 현황 ── */}
|
||||||
{tab === 'overview' && (
|
{tab === 'overview' && (
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Card, CardContent } from '@shared/components/ui/card';
|
import { Card, CardContent } from '@shared/components/ui/card';
|
||||||
import { Badge } from '@shared/components/ui/badge';
|
import { Badge } from '@shared/components/ui/badge';
|
||||||
|
import { TabBar, TabButton } from '@shared/components/ui/tabs';
|
||||||
import { PageContainer, PageHeader } from '@shared/components/layout';
|
import { PageContainer, PageHeader } from '@shared/components/layout';
|
||||||
import { getStatusIntent } from '@shared/constants/statusIntent';
|
import { getStatusIntent } from '@shared/constants/statusIntent';
|
||||||
|
import { useAuth } from '@/app/auth/AuthContext';
|
||||||
import {
|
import {
|
||||||
Database, Clock, Trash2, ShieldCheck, FileText, AlertTriangle,
|
Database, Clock, Trash2, ShieldCheck, FileText, AlertTriangle,
|
||||||
CheckCircle, Archive, CalendarClock, UserCheck, Search,
|
CheckCircle, Archive, CalendarClock, UserCheck, Search,
|
||||||
@ -89,6 +91,18 @@ const STORAGE_ARCHITECTURE = [
|
|||||||
|
|
||||||
export function DataRetentionPolicy() {
|
export function DataRetentionPolicy() {
|
||||||
const [tab, setTab] = useState<Tab>('overview');
|
const [tab, setTab] = useState<Tab>('overview');
|
||||||
|
const { hasPermission } = useAuth();
|
||||||
|
// 향후 Phase 3 에서 파기 승인/예외 등록 시 disabled 가드로 활용
|
||||||
|
void hasPermission('admin:data-retention', 'UPDATE');
|
||||||
|
void hasPermission('admin:data-retention', 'DELETE');
|
||||||
|
|
||||||
|
const TABS: Array<{ key: Tab; icon: typeof Eye; label: string }> = [
|
||||||
|
{ key: 'overview', icon: Eye, label: '보관 현황' },
|
||||||
|
{ key: 'retention', icon: CalendarClock, label: '유형별 보관기간' },
|
||||||
|
{ key: 'disposal', icon: Trash2, label: '파기 절차' },
|
||||||
|
{ key: 'exception', icon: ShieldCheck, label: '예외·연장' },
|
||||||
|
{ key: 'audit', icon: FileText, label: '파기 감사 대장' },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
@ -100,21 +114,14 @@ export function DataRetentionPolicy() {
|
|||||||
demo
|
demo
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 탭 */}
|
<TabBar variant="underline">
|
||||||
<div className="flex gap-0 border-b border-border">
|
{TABS.map(t => (
|
||||||
{([
|
<TabButton key={t.key} variant="underline" active={tab === t.key}
|
||||||
{ key: 'overview' as Tab, icon: Eye, label: '보관 현황' },
|
icon={<t.icon className="w-3.5 h-3.5" />} onClick={() => setTab(t.key)}>
|
||||||
{ key: 'retention' as Tab, icon: CalendarClock, label: '유형별 보관기간' },
|
{t.label}
|
||||||
{ key: 'disposal' as Tab, icon: Trash2, label: '파기 절차' },
|
</TabButton>
|
||||||
{ key: 'exception' as Tab, icon: ShieldCheck, label: '예외·연장' },
|
|
||||||
{ key: 'audit' as Tab, icon: FileText, label: '파기 감사 대장' },
|
|
||||||
]).map(t => (
|
|
||||||
<button type="button" key={t.key} onClick={() => setTab(t.key)}
|
|
||||||
className={`flex items-center gap-1.5 px-4 py-2.5 text-[11px] font-medium border-b-2 transition-colors ${tab === t.key ? 'text-blue-400 border-blue-400' : 'text-hint border-transparent hover:text-label'}`}>
|
|
||||||
<t.icon className="w-3.5 h-3.5" />{t.label}
|
|
||||||
</button>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</TabBar>
|
||||||
|
|
||||||
{/* ── ① 보관 현황 ── */}
|
{/* ── ① 보관 현황 ── */}
|
||||||
{tab === 'overview' && (
|
{tab === 'overview' && (
|
||||||
|
|||||||
@ -1,7 +1,15 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Card, CardContent } from '@shared/components/ui/card';
|
import { Card, CardContent } from '@shared/components/ui/card';
|
||||||
import { Badge } from '@shared/components/ui/badge';
|
import { Badge } from '@shared/components/ui/badge';
|
||||||
|
import { TabBar, TabButton } from '@shared/components/ui/tabs';
|
||||||
import { PageContainer, PageHeader } from '@shared/components/layout';
|
import { PageContainer, PageHeader } from '@shared/components/layout';
|
||||||
|
import {
|
||||||
|
getPerformanceStatusHex,
|
||||||
|
getPerformanceStatusIntent,
|
||||||
|
utilizationStatus,
|
||||||
|
type PerformanceStatus,
|
||||||
|
} from '@shared/constants/performanceStatus';
|
||||||
|
import { useAuth } from '@/app/auth/AuthContext';
|
||||||
import {
|
import {
|
||||||
Activity, Gauge, Users, Database, Brain, Server,
|
Activity, Gauge, Users, Database, Brain, Server,
|
||||||
CheckCircle, AlertTriangle, TrendingUp, Clock,
|
CheckCircle, AlertTriangle, TrendingUp, Clock,
|
||||||
@ -21,13 +29,13 @@ import {
|
|||||||
type Tab = 'overview' | 'response' | 'capacity' | 'aiModel' | 'availability';
|
type Tab = 'overview' | 'response' | 'capacity' | 'aiModel' | 'availability';
|
||||||
|
|
||||||
// ─── 성능 KPI ──────────────────
|
// ─── 성능 KPI ──────────────────
|
||||||
const PERF_KPI = [
|
const PERF_KPI: Array<{ label: string; value: string; unit: string; icon: typeof Users; status: PerformanceStatus }> = [
|
||||||
{ label: '현재 동시접속', value: '342', unit: '명', icon: Users, color: '#3b82f6', status: 'normal' },
|
{ label: '현재 동시접속', value: '342', unit: '명', icon: Users, status: 'normal' },
|
||||||
{ label: '대시보드 p95', value: '1.8', unit: '초', icon: Gauge, color: '#10b981', status: 'good' },
|
{ label: '대시보드 p95', value: '1.8', unit: '초', icon: Gauge, status: 'good' },
|
||||||
{ label: '시스템 가동률', value: '99.87', unit: '%', icon: Shield, color: '#06b6d4', status: 'good' },
|
{ label: '시스템 가동률', value: '99.87', unit: '%', icon: Shield, status: 'good' },
|
||||||
{ label: 'AI 추론 p95', value: '1.4', unit: '초', icon: Brain, color: '#8b5cf6', status: 'good' },
|
{ label: 'AI 추론 p95', value: '1.4', unit: '초', icon: Brain, status: 'good' },
|
||||||
{ label: '배치 SLA 준수', value: '100', unit: '%', icon: CheckCircle, color: '#10b981', status: 'good' },
|
{ label: '배치 SLA 준수', value: '100', unit: '%', icon: CheckCircle, status: 'good' },
|
||||||
{ label: '이벤트 경보', value: '0', unit: '건', icon: AlertTriangle, color: '#f59e0b', status: 'normal' },
|
{ label: '이벤트 경보', value: '0', unit: '건', icon: AlertTriangle, status: 'normal' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// ─── SLO 적용 그룹 ──────────────────
|
// ─── SLO 적용 그룹 ──────────────────
|
||||||
@ -116,20 +124,30 @@ const IMPACT_REDUCTION = [
|
|||||||
{ strategy: 'HPA 자동 확장', target: 'CPU/메모리 70% 임계', effect: '피크 자동 대응', per: 'PER-02·06' },
|
{ strategy: 'HPA 자동 확장', target: 'CPU/메모리 70% 임계', effect: '피크 자동 대응', per: 'PER-02·06' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const statusIntent = (s: 'good' | 'warn' | 'critical' | 'success'): 'success' | 'warning' | 'critical' => {
|
// 로컬 status 문자열을 카탈로그 PerformanceStatus로 매핑
|
||||||
if (s === 'good' || s === 'success') return 'success';
|
const toStatus = (s: 'good' | 'warn' | 'critical' | 'success'): PerformanceStatus => {
|
||||||
|
if (s === 'good' || s === 'success') return 'good';
|
||||||
if (s === 'warn') return 'warning';
|
if (s === 'warn') return 'warning';
|
||||||
return 'critical';
|
return 'critical';
|
||||||
};
|
};
|
||||||
|
const statusIntent = (s: 'good' | 'warn' | 'critical' | 'success') =>
|
||||||
const barColor = (ratio: number): string => {
|
getPerformanceStatusIntent(toStatus(s));
|
||||||
if (ratio < 0.6) return '#10b981';
|
const barColor = (ratio: number): string =>
|
||||||
if (ratio < 0.8) return '#f59e0b';
|
getPerformanceStatusHex(utilizationStatus(ratio));
|
||||||
return '#ef4444';
|
|
||||||
};
|
|
||||||
|
|
||||||
export function PerformanceMonitoring() {
|
export function PerformanceMonitoring() {
|
||||||
const [tab, setTab] = useState<Tab>('overview');
|
const [tab, setTab] = useState<Tab>('overview');
|
||||||
|
const { hasPermission } = useAuth();
|
||||||
|
// 향후 Phase 3 에서 EXPORT 버튼 추가 시 disabled={!canExport} 로 연결
|
||||||
|
void hasPermission('admin:performance-monitoring', 'EXPORT');
|
||||||
|
|
||||||
|
const TABS: Array<{ key: Tab; icon: typeof BarChart3; label: string }> = [
|
||||||
|
{ key: 'overview', icon: BarChart3, label: '성능 현황' },
|
||||||
|
{ key: 'response', icon: Gauge, label: '응답성 (PER-01)' },
|
||||||
|
{ key: 'capacity', icon: Users, label: '처리용량 (PER-02·03)' },
|
||||||
|
{ key: 'aiModel', icon: Brain, label: 'AI 모델 (PER-04)' },
|
||||||
|
{ key: 'availability', icon: Shield, label: '가용성·확장성 (PER-05·06)' },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
@ -141,38 +159,34 @@ export function PerformanceMonitoring() {
|
|||||||
demo
|
demo
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 탭 */}
|
<TabBar variant="underline">
|
||||||
<div className="flex gap-0 border-b border-border">
|
{TABS.map(t => (
|
||||||
{([
|
<TabButton key={t.key} variant="underline" active={tab === t.key}
|
||||||
{ key: 'overview' as Tab, icon: BarChart3, label: '성능 현황' },
|
icon={<t.icon className="w-3.5 h-3.5" />} onClick={() => setTab(t.key)}>
|
||||||
{ key: 'response' as Tab, icon: Gauge, label: '응답성 (PER-01)' },
|
{t.label}
|
||||||
{ key: 'capacity' as Tab, icon: Users, label: '처리용량 (PER-02·03)' },
|
</TabButton>
|
||||||
{ key: 'aiModel' as Tab, icon: Brain, label: 'AI 모델 (PER-04)' },
|
|
||||||
{ key: 'availability' as Tab, icon: Shield, label: '가용성·확장성 (PER-05·06)' },
|
|
||||||
]).map(t => (
|
|
||||||
<button type="button" key={t.key} onClick={() => setTab(t.key)}
|
|
||||||
className={`flex items-center gap-1.5 px-4 py-2.5 text-[11px] font-medium border-b-2 transition-colors ${tab === t.key ? 'text-cyan-400 border-cyan-400' : 'text-hint border-transparent hover:text-label'}`}>
|
|
||||||
<t.icon className="w-3.5 h-3.5" />{t.label}
|
|
||||||
</button>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</TabBar>
|
||||||
|
|
||||||
{/* ── ① 성능 현황 ── */}
|
{/* ── ① 성능 현황 ── */}
|
||||||
{tab === 'overview' && (
|
{tab === 'overview' && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{/* KPI */}
|
{/* KPI */}
|
||||||
<div className="grid grid-cols-6 gap-2">
|
<div className="grid grid-cols-6 gap-2">
|
||||||
{PERF_KPI.map(k => (
|
{PERF_KPI.map(k => {
|
||||||
<div key={k.label} className="flex items-center gap-3 px-4 py-3 rounded-xl border border-border bg-card" style={{ borderLeftColor: k.color, borderLeftWidth: 3 }}>
|
const hex = getPerformanceStatusHex(k.status);
|
||||||
<k.icon className="w-5 h-5" style={{ color: k.color }} />
|
return (
|
||||||
<div>
|
<div key={k.label} className="flex items-center gap-3 px-4 py-3 rounded-xl border border-border bg-card" style={{ borderLeftColor: hex, borderLeftWidth: 3 }}>
|
||||||
<div className="text-lg font-bold" style={{ color: k.color }}>
|
<k.icon className="w-5 h-5" style={{ color: hex }} />
|
||||||
{k.value}<span className="text-[10px] ml-1 text-hint">{k.unit}</span>
|
<div>
|
||||||
|
<div className="text-lg font-bold" style={{ color: hex }}>
|
||||||
|
{k.value}<span className="text-[10px] ml-1 text-hint">{k.unit}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-[9px] text-hint">{k.label}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[9px] text-hint">{k.label}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
))}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 사용자 그룹별 SLO */}
|
{/* 사용자 그룹별 SLO */}
|
||||||
|
|||||||
@ -30,3 +30,4 @@ export * from './connectionStatuses';
|
|||||||
export * from './trainingZoneTypes';
|
export * from './trainingZoneTypes';
|
||||||
export * from './kpiUiMap';
|
export * from './kpiUiMap';
|
||||||
export * from './statusIntent';
|
export * from './statusIntent';
|
||||||
|
export * from './performanceStatus';
|
||||||
|
|||||||
64
frontend/src/shared/constants/performanceStatus.ts
Normal file
64
frontend/src/shared/constants/performanceStatus.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* 성능/시스템 상태 공통 카탈로그 (admin 성능·데이터 보관·모델 검증 페이지 공유)
|
||||||
|
*
|
||||||
|
* status 문자열 → BadgeIntent + 아이콘 hex.
|
||||||
|
* 인라인 hex/하드코딩 Tailwind 색상을 이 카탈로그로 치환한다.
|
||||||
|
*
|
||||||
|
* 사용처:
|
||||||
|
* - PerformanceMonitoring (KPI 카드, SLO 대시보드)
|
||||||
|
* - DataRetentionPolicy (정책 활성/보관 상태)
|
||||||
|
* - DataModelVerification (검증 passed/failed/running)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { BadgeIntent } from '@lib/theme/variants';
|
||||||
|
|
||||||
|
export type PerformanceStatus =
|
||||||
|
| 'good' // 정상/양호
|
||||||
|
| 'normal' // 일반 (기본)
|
||||||
|
| 'warning' // 주의
|
||||||
|
| 'critical' // 심각/경보
|
||||||
|
| 'running' // 실행 중
|
||||||
|
| 'passed' // 통과
|
||||||
|
| 'failed' // 실패
|
||||||
|
| 'active' // 활성
|
||||||
|
| 'scheduled' // 예약
|
||||||
|
| 'archived'; // 보관
|
||||||
|
|
||||||
|
export interface PerformanceStatusMeta {
|
||||||
|
intent: BadgeIntent;
|
||||||
|
/** 아이콘·바 차트용 hex (동적 데이터 기반 예외 허용) */
|
||||||
|
hex: string;
|
||||||
|
label: { ko: string; en: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PERFORMANCE_STATUS_META: Record<PerformanceStatus, PerformanceStatusMeta> = {
|
||||||
|
good: { intent: 'success', hex: '#10b981', label: { ko: '양호', en: 'Good' } },
|
||||||
|
normal: { intent: 'info', hex: '#3b82f6', label: { ko: '정상', en: 'Normal' } },
|
||||||
|
warning: { intent: 'warning', hex: '#f59e0b', label: { ko: '주의', en: 'Warning' } },
|
||||||
|
critical: { intent: 'critical', hex: '#ef4444', label: { ko: '심각', en: 'Critical' } },
|
||||||
|
running: { intent: 'info', hex: '#06b6d4', label: { ko: '실행 중', en: 'Running' } },
|
||||||
|
passed: { intent: 'success', hex: '#10b981', label: { ko: '통과', en: 'Passed' } },
|
||||||
|
failed: { intent: 'critical', hex: '#ef4444', label: { ko: '실패', en: 'Failed' } },
|
||||||
|
active: { intent: 'success', hex: '#10b981', label: { ko: '활성', en: 'Active' } },
|
||||||
|
scheduled: { intent: 'info', hex: '#8b5cf6', label: { ko: '예약', en: 'Scheduled' } },
|
||||||
|
archived: { intent: 'muted', hex: '#6b7280', label: { ko: '보관', en: 'Archived' } },
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 사용률(0~1) → 상태 분류 (KPI 게이지 바 등) */
|
||||||
|
export function utilizationStatus(ratio: number): PerformanceStatus {
|
||||||
|
if (ratio < 0.6) return 'good';
|
||||||
|
if (ratio < 0.8) return 'warning';
|
||||||
|
return 'critical';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPerformanceStatusMeta(status: PerformanceStatus): PerformanceStatusMeta {
|
||||||
|
return PERFORMANCE_STATUS_META[status] ?? PERFORMANCE_STATUS_META.normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPerformanceStatusIntent(status: PerformanceStatus): BadgeIntent {
|
||||||
|
return getPerformanceStatusMeta(status).intent;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPerformanceStatusHex(status: PerformanceStatus): string {
|
||||||
|
return getPerformanceStatusMeta(status).hex;
|
||||||
|
}
|
||||||
불러오는 중...
Reference in New Issue
Block a user