Skip to content

Commit 5a2c6d2

Browse files
committed
feat: 워크로드 쿼리 조회 기능 추가
1 parent 2fea7c6 commit 5a2c6d2

5 files changed

Lines changed: 181 additions & 23 deletions

File tree

backend/app/routers/admin.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
DBTuneResult,
1818
WorkloadCreate,
1919
WorkloadRead,
20+
WorkloadDetailRead,
2021
WorkloadExecutionRead,
2122
AdminDashboardStats,
2223
AdminLoginRequest,
@@ -68,7 +69,6 @@ async def tune_database(
6869
try:
6970
content = await file.read()
7071
config_data = json.loads(content)
71-
# Pass admin email for logging
7272
return apply_db_configuration(db, config_data, applied_by=current_user.email)
7373
except Exception as e:
7474
raise HTTPException(status_code=400, detail=str(e))
@@ -127,6 +127,26 @@ def list_workloads(
127127
) for w in workloads
128128
]
129129

130+
@router.get("/workloads/{workload_id}", response_model=WorkloadDetailRead)
131+
def get_workload_details(
132+
workload_id: uuid.UUID,
133+
current_user: User = Depends(get_current_user),
134+
db: Session = Depends(get_db),
135+
):
136+
check_admin(current_user)
137+
workload = db.query(Workload).filter(Workload.id == workload_id).first()
138+
if not workload:
139+
raise HTTPException(status_code=404, detail="Workload not found")
140+
141+
return WorkloadDetailRead(
142+
id=workload.id,
143+
name=workload.name,
144+
description=workload.description,
145+
query_count=len(workload.queries),
146+
created_at=workload.created_at,
147+
queries=workload.queries
148+
)
149+
130150
@router.post("/workloads/{workload_id}/execute", response_model=WorkloadExecutionRead)
131151
def run_workload(
132152
workload_id: uuid.UUID,

backend/app/schemas/admin.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ class WorkloadRead(BaseModel):
2727
class Config:
2828
orm_mode = True
2929

30+
class WorkloadDetailRead(WorkloadRead):
31+
queries: List[Dict[str, Any]]
32+
3033
class WorkloadExecutionRead(BaseModel):
3134
id: uuid.UUID
3235
workload_id: uuid.UUID

frontend/src/components/Shared.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
11
import React from 'react';
22

3-
export const Modal = ({ title, onClose, children }: { title: string, onClose: () => void, children: React.ReactNode }) => (
3+
interface ModalProps {
4+
title: string;
5+
onClose: () => void;
6+
children: React.ReactNode;
7+
width?: string | number;
8+
}
9+
10+
export const Modal = ({ title, onClose, children, width = 800 }: ModalProps) => (
411
<div className="modal__backdrop" onClick={onClose}>
5-
<div className="modal__content" onClick={e => e.stopPropagation()}>
12+
<div
13+
className="modal__content"
14+
onClick={e => e.stopPropagation()}
15+
style={{ maxWidth: width, width: '100%' }}
16+
>
617
<div className="modal__header">
718
<span>{title}</span>
819
<button className="btn--ghost" onClick={onClose} style={{ fontSize: '1.25rem' }}>&times;</button>

frontend/src/pages/AdminDashboard.tsx

Lines changed: 138 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const AdminDashboard: React.FC<Props> = ({ api, onLogout }) => {
3535
const [executions, setExecutions] = useState<WorkloadExecution[]>([]);
3636
const [wlForm, setWlForm] = useState({ name: '', description: '', count: 100 });
3737
const [wlLoading, setWlLoading] = useState(false);
38+
const [selectedWorkloadForQueries, setSelectedWorkloadForQueries] = useState<Workload | null>(null);
3839

3940
// --- Monitor ---
4041
const [selectedExecution, setSelectedExecution] = useState<WorkloadExecution | null>(null);
@@ -188,6 +189,20 @@ const AdminDashboard: React.FC<Props> = ({ api, onLogout }) => {
188189
}
189190
};
190191

192+
const handleViewQueries = async (id: string) => {
193+
try {
194+
const res = await api(`/admin/workloads/${id}`);
195+
if (res.ok) {
196+
const data = await res.json();
197+
setSelectedWorkloadForQueries(data);
198+
} else {
199+
Swal.fire("실패", "쿼리 정보를 불러오지 못했습니다.", "error");
200+
}
201+
} catch (e) {
202+
console.error(e);
203+
}
204+
};
205+
191206
// --- Charts ---
192207
const chartData: EquityPoint[] = useMemo(() => {
193208
return executions
@@ -205,6 +220,17 @@ const AdminDashboard: React.FC<Props> = ({ api, onLogout }) => {
205220
}));
206221
}, [executions, workloads]);
207222

223+
// --- Components ---
224+
const StatCard = ({ label, value, unit, color }: { label: string, value: string | number, unit?: string, color?: string }) => (
225+
<div style={{ background: '#f9fafb', borderRadius: 12, padding: '20px', border: '1px solid #e5e7eb', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
226+
<span style={{ fontSize: 13, color: '#6b7280', textTransform: 'uppercase', fontWeight: 600, letterSpacing: '0.025em', marginBottom: 8 }}>{label}</span>
227+
<div style={{ display: 'flex', alignItems: 'baseline', gap: 4 }}>
228+
<span style={{ fontSize: 28, fontWeight: 700, color: color || '#111827', fontFamily: 'var(--font-mono)' }}>{value}</span>
229+
{unit && <span style={{ fontSize: 14, color: '#9ca3af', fontWeight: 500 }}>{unit}</span>}
230+
</div>
231+
</div>
232+
);
233+
208234
return (
209235
<div className="app-shell">
210236
<header className="top-header" style={{background: '#1a1a1a', borderBottom: '1px solid #333'}}>
@@ -243,7 +269,6 @@ const AdminDashboard: React.FC<Props> = ({ api, onLogout }) => {
243269
</header>
244270

245271
<main className="main-content">
246-
247272
{/* 1. Dashboard Stats */}
248273
{activeTab === 'stats' && stats && (
249274
<>
@@ -374,7 +399,12 @@ const AdminDashboard: React.FC<Props> = ({ api, onLogout }) => {
374399
<tr key={w.id}>
375400
<td>{w.name}</td>
376401
<td>{w.query_count}</td>
377-
<td><button className="btn btn--secondary btn--sm" onClick={() => handleExecuteWorkload(w.id)}>▶ 실행</button></td>
402+
<td>
403+
<div style={{display:'flex', gap: 4}}>
404+
<button className="btn btn--secondary btn--sm" onClick={() => handleViewQueries(w.id)}>조회</button>
405+
<button className="btn btn--secondary btn--sm" onClick={() => handleExecuteWorkload(w.id)}>▶ 실행</button>
406+
</div>
407+
</td>
378408
</tr>
379409
))}
380410
</tbody>
@@ -422,28 +452,116 @@ const AdminDashboard: React.FC<Props> = ({ api, onLogout }) => {
422452
)}
423453
</main>
424454

425-
{/* Analysis Modal */}
455+
{/* Queries Modal */}
456+
{selectedWorkloadForQueries && (
457+
<Modal title={`워크로드 쿼리 목록 (${selectedWorkloadForQueries.name})`} onClose={() => setSelectedWorkloadForQueries(null)} width={900}>
458+
<div style={{ maxHeight: '60vh', overflowY: 'auto' }}>
459+
{selectedWorkloadForQueries.queries && selectedWorkloadForQueries.queries.length > 0 ? (
460+
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
461+
{selectedWorkloadForQueries.queries.map((q, idx) => (
462+
<div key={idx} style={{ background: '#f5f5f5', padding: 16, borderRadius: 8, border: '1px solid #e5e5e5' }}>
463+
<div style={{ fontSize: 12, fontWeight: 700, color: '#666', marginBottom: 8, display: 'flex', justifyContent: 'space-between' }}>
464+
<span>Query #{idx + 1}</span>
465+
</div>
466+
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', fontFamily: 'var(--font-mono)', fontSize: 13, color: '#2563eb', background: '#fff', padding: 12, borderRadius: 4, border: '1px solid #eaeaea' }}>
467+
{q.sql}
468+
</pre>
469+
<div style={{ marginTop: 12 }}>
470+
<span style={{ fontSize: 11, fontWeight: 600, color: '#888', textTransform: 'uppercase' }}>Parameters</span>
471+
<div style={{ marginTop: 4, fontSize: 12, fontFamily: 'var(--font-mono)', color: '#444' }}>
472+
{JSON.stringify(q.params)}
473+
</div>
474+
</div>
475+
</div>
476+
))}
477+
</div>
478+
) : (
479+
<div className="empty-state-small">생성된 쿼리 정보가 없습니다.</div>
480+
)}
481+
</div>
482+
</Modal>
483+
)}
484+
485+
{/* Analysis Modal (Improved UI) */}
426486
{selectedExecution && (
427-
<Modal title="성능 상세 분석" onClose={() => setSelectedExecution(null)}>
428-
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 24 }}>
487+
<Modal title="성능 상세 분석" onClose={() => setSelectedExecution(null)} width={1000}>
488+
<div style={{ display: 'flex', flexDirection: 'column', gap: 32 }}>
489+
{/* Header Section */}
490+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', paddingBottom: 24, borderBottom: '1px solid #eaeaea' }}>
491+
<div>
492+
<div style={{ fontSize: 14, color: '#6b7280', marginBottom: 4 }}>Workload Execution ID</div>
493+
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 14, fontWeight: 600 }}>{selectedExecution.id}</div>
494+
</div>
495+
<div style={{ textAlign: 'right' }}>
496+
<div style={{ fontSize: 14, color: '#6b7280', marginBottom: 4 }}>Executed At</div>
497+
<div style={{ fontWeight: 600 }}>{new Date(selectedExecution.created_at).toLocaleString()}</div>
498+
</div>
499+
</div>
500+
501+
{/* Metrics Grid */}
429502
<div>
430-
<h4 style={{marginBottom: 12}}>📊 상세 성능 지표</h4>
431-
{selectedExecution.extended_metrics ? (
432-
<div className="metric-grid-compact" style={{gridTemplateColumns: '1fr 1fr'}}>
433-
<div className="metric-box"><label>Buffer Hit Ratio</label><span>{selectedExecution.extended_metrics.buffer_hit_ratio}%</span></div>
434-
<div className="metric-box"><label>Disk Blocks Read</label><span>{selectedExecution.extended_metrics.blocks_read}</span></div>
435-
<div className="metric-box"><label>Buffer Blocks Hit</label><span>{selectedExecution.extended_metrics.blocks_hit}</span></div>
436-
<div className="metric-box"><label>Rows Returned</label><span>{selectedExecution.extended_metrics.tuples_returned}</span></div>
437-
<div className="metric-box"><label>Transactions</label><span>{selectedExecution.extended_metrics.transactions}</span></div>
438-
</div>
439-
) : (
440-
<div className="empty-state-small">추가 지표 없음</div>
441-
)}
503+
<h4 style={{ fontSize: 16, fontWeight: 700, marginBottom: 16, color: '#111827' }}>📊 Performance Metrics</h4>
504+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 16 }}>
505+
<StatCard
506+
label="Execution Time"
507+
value={selectedExecution.execution_time_ms.toFixed(2)}
508+
unit="ms"
509+
color="#2563eb"
510+
/>
511+
{selectedExecution.extended_metrics ? (
512+
<>
513+
<StatCard
514+
label="Buffer Hit Ratio"
515+
value={selectedExecution.extended_metrics.buffer_hit_ratio}
516+
unit="%"
517+
color={selectedExecution.extended_metrics.buffer_hit_ratio > 90 ? '#059669' : '#d97706'}
518+
/>
519+
<StatCard
520+
label="Disk Blocks Read"
521+
value={selectedExecution.extended_metrics.blocks_read}
522+
/>
523+
<StatCard
524+
label="Buffer Blocks Hit"
525+
value={selectedExecution.extended_metrics.blocks_hit}
526+
/>
527+
<StatCard
528+
label="Rows Returned"
529+
value={selectedExecution.extended_metrics.tuples_returned}
530+
/>
531+
<StatCard
532+
label="Rows Fetched"
533+
value={selectedExecution.extended_metrics.tuples_fetched}
534+
/>
535+
<StatCard
536+
label="Transactions"
537+
value={selectedExecution.extended_metrics.transactions}
538+
/>
539+
</>
540+
) : (
541+
<div style={{ gridColumn: 'span 3', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#9ca3af', background: '#f9fafb', borderRadius: 12 }}>
542+
No extended metrics available
543+
</div>
544+
)}
545+
</div>
442546
</div>
547+
548+
{/* Config Snapshot */}
443549
<div>
444-
<h4 style={{marginBottom: 12}}>⚙️ DB 파라미터 스냅샷</h4>
445-
<div style={{ maxHeight: '300px', overflowY: 'auto', background: '#f5f5f5', padding: 12, borderRadius: 6, fontSize: 12 }}>
446-
<pre style={{margin: 0}}>{JSON.stringify(selectedExecution.db_config_snapshot, null, 2)}</pre>
550+
<h4 style={{ fontSize: 16, fontWeight: 700, marginBottom: 16, color: '#111827' }}>⚙️ DB Parameter Snapshot</h4>
551+
<div style={{
552+
background: '#1e293b',
553+
color: '#e2e8f0',
554+
padding: '20px',
555+
borderRadius: 12,
556+
fontFamily: 'var(--font-mono)',
557+
fontSize: 13,
558+
maxHeight: '300px',
559+
overflowY: 'auto',
560+
boxShadow: 'inset 0 2px 4px rgba(0,0,0,0.1)'
561+
}}>
562+
<pre style={{ margin: 0 }}>
563+
{JSON.stringify(selectedExecution.db_config_snapshot, null, 2)}
564+
</pre>
447565
</div>
448566
</div>
449567
</div>

frontend/src/types/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,18 @@ export interface DBTuningLog {
7474
reverted_at?: string;
7575
}
7676

77+
export interface WorkloadQuery {
78+
sql: string;
79+
params: Record<string, any>;
80+
}
81+
7782
export interface Workload {
7883
id: string;
7984
name: string;
8085
description?: string;
8186
query_count: number;
8287
created_at: string;
88+
queries?: WorkloadQuery[];
8389
}
8490

8591
export interface WorkloadExecution {

0 commit comments

Comments
 (0)