지난 시간에 우리는 AI 3총사를 이용해서 실시간 데이터를 활용한 페이징 UI를 만들어 보았습니다. 그리고 3개의 AI에 같은 프롬프트를 입력해서 나오는 결과를 비교해 보았었습니다. 어떠신가요? 이제 조금 알 것 같지 않나요? AI를 어떻게 써야 하는지. 어떻게 효율을 낼 수 있는지 말이죠.
저도 AI 비교 시리즈를 진행하면서 정말 많은 부분을 또 다시 느끼게 되었습니다. 저 스스로가 어떤 AI가 잘 맞는지도 다시 한번 체감할 수 있었고 또 어떤 상황에 어떤 AI를 쓰는게 맞는가 라는 부분에 대해서도 더 디테일 한 기준을 잡게 되었습니다. 여러분들은 어떠셨을지 모르겠습니다.
이런 이야기를 하는 이유는 이제 이번 시리즈의 마지막으로 실무에서 바로 쓰는 수준의 ‘완성형 대시보드’ 편이기 때문입니다.
앞서 Part 1 부터 진행해 오면서 처음 HTML 페이지 만들기부터 Part 8~9까지는 검색·필터·페이징·정렬·실시간 데이터 UI까지 기본 기능을 완성했습니다. 이제 여러분은 왠만한 프론트엔드 개발자 혹은 AI 부업으로 랜딩페이지를 만들어 납품할 정도의 기본적인 실력은 충분히 갖추게 되었습니다.
Part 10에서는 여기에 차트(Chart.js) + KPI 박스 + 실시간 데이터 연동까지 적용해 기업/서비스에서 바로 쓰는 수준의 대시보드 화면을 완성합니다. 이번 시간에도 함께 나아가 봅시다!

본 이미지의 저작권은 블로그 주인 제로에게 있습니다.
외부링크(GPT / Cursor / AntiGravity)
핵심 요약: GPT, Cursor, AntiGravity는 각각 다른 특성과 활용 방식으로 UI 코딩을 지원합니다.
⭐ 이번 Part 10에서 완성하는 기능
| 구성 요소 | 설명 |
|---|---|
| ① KPI 요약 박스(Top Summary) | 매출, 주문수, 오늘 방문자, 오류로그 등 |
| ② 라인차트 / 바차트 (Chart.js) | 실시간 데이터 자동 업데이트 |
| ③ 검색 + 필터 + 페이징 | 사용자/상품/로그 리스트용 |
| ④ 실시간 데이터 연동 | 가격·수량·트래픽 등 자동으로 갱신 |
| ⑤ 3개 AI 비교(GPT·Cursor·AntiGravity) | 같은 프롬프트로 생성된 코드 품질 분석 |
👉 이번 편을 끝내면, ‘서비스에 바로 올릴 수 있는’ 기본 대시보드를 직접 구성할 수 있음.
1. 실무형 대시보드 레이아웃은 이렇게 구성한다
대시보드 화면 구조는 대부분 다음과 같습니다
[ KPI 박스 4개 ]
[ 실시간 라인 차트 ]
[ 실시간 테이블(검색/필터/정렬/페이징) ]
이 구성은 B2C 서비스(코인·쇼핑몰·게임)부터 B2B · 관리자 페이지까지 거의 공통입니다.
2. 동일한 프롬프트로 3개 AI에게 요청한 내용
HTML/CSS/JS로
1) KPI 요약 박스
2) Chart.js 기반 실시간 차트
3) 검색 + 필터 + 페이징 + 정렬 테이블
4) 실시간 데이터 3초마다 갱신
5) 주석 많은 버전
을 만들어줘. 입문자도 이해할 수 있게 코드 설명을 따로 포함해줘.

이해를 돕기 위해 직접 찍은 스크린샷을 제공합니다. 본 이미지의 저작권은 블로그 주인 제로에게 있습니다.

이해를 돕기 위해 직접 찍은 스크린샷을 제공합니다. 본 이미지의 저작권은 블로그 주인 제로에게 있습니다.

이해를 돕기 위해 직접 찍은 스크린샷을 제공합니다. 본 이미지의 저작권은 블로그 주인 제로에게 있습니다.
자 이제 결과를 알아 볼까요?
GPT – 가장 기본이자 틀을 잡을 수 있는 AI
GPT가 생성한 소스코드(통합본)
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>실시간 대시보드 예제 (HTML/CSS/JS)</title>
<!-- Chart.js CDN -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.3.0/dist/chart.umd.min.js"></script>
<style>
/* -----------------------------
기본 레이아웃 & 타이포그래피
----------------------------- */
:root{
--bg:#f5f7fb; --card:#ffffff; --muted:#6b7280; --accent:#2563eb;
--gap:16px;
}
*{box-sizing:border-box}
body{font-family:Inter,Segoe UI,Helvetica,Arial,sans-serif;background:var(--bg);color:#111;margin:0;padding:20px}
h1{margin:0 0 12px;font-size:20px}
.container{max-width:1200px;margin:0 auto}
/* -----------------------------
KPI 박스 스타일
----------------------------- */
.kpi-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:var(--gap);margin-bottom:18px}
.kpi{background:var(--card);padding:14px;border-radius:10px;box-shadow:0 1px 3px rgba(16,24,40,0.04);}
.kpi .label{font-size:12px;color:var(--muted)}
.kpi .value{font-size:22px;font-weight:600;margin-top:6px}
.kpi .small{font-size:12px;color:var(--muted);margin-top:8px}
/* -----------------------------
메인 레이아웃: 차트 + 테이블
----------------------------- */
.grid{display:grid;grid-template-columns:1fr;gap:var(--gap)}
@media(min-width:900px){ .grid{grid-template-columns:520px 1fr} }
.card{background:var(--card);border-radius:10px;padding:14px;box-shadow:0 1px 3px rgba(16,24,40,0.04)}
/* -----------------------------
차트 캔버스 반응형
----------------------------- */
.chart-wrap{height:320px}
canvas{width:100%!important;height:100%!important}
/* -----------------------------
테이블 + 컨트롤
----------------------------- */
.controls{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:12px;align-items:center}
.controls input,.controls select{padding:8px 10px;border-radius:6px;border:1px solid #e6e9ef}
table{width:100%;border-collapse:collapse;font-size:13px}
th,td{padding:8px 10px;text-align:left;border-bottom:1px solid #eef2f7}
th{cursor:pointer;user-select:none}
th.sort-asc::after{content:" ▲";font-size:10px}
th.sort-desc::after{content:" ▼";font-size:10px}
.badge{display:inline-block;padding:4px 8px;border-radius:999px;font-size:12px}
.badge.ok{background:#ecfdf5;color:#065f46}
.badge.warn{background:#fffbeb;color:#92400e}
.badge.err{background:#fff1f2;color:#9f1239}
.pagination{display:flex;gap:6px;align-items:center;justify-content:flex-end;margin-top:10px}
.page-btn{padding:6px 10px;border-radius:6px;border:1px solid #e6e9ef;background:white;cursor:pointer}
.page-btn.active{background:var(--accent);color:white;border-color:var(--accent)}
/* helper */
.muted{color:var(--muted)}
</style>
</head>
<body>
<div class="container">
<h1>실시간 대시보드 예제 (HTML/CSS/JS) - 3초마다 갱신</h1>
<!-- KPI 박스 -->
<div class="kpi-row">
<div class="kpi">
<div class="label">항목 수 (Total items)</div>
<div id="kpi-total" class="value">0</div>
<div class="small">현재 데이터 수</div>
</div>
<div class="kpi">
<div class="label">평균 값 (Average value)</div>
<div id="kpi-avg" class="value">0</div>
<div class="small">최신 값 기준</div>
</div>
<div class="kpi">
<div class="label">상승 항목 (Positive trend)</div>
<div id="kpi-up" class="value">0</div>
<div class="small">값이 직전보다 오른 항목 수</div>
</div>
<div class="kpi">
<div class="label">최종 갱신 (Last update)</div>
<div id="kpi-last" class="value">-</div>
<div class="small">서버/시뮬레이터 업데이트 시각</div>
</div>
</div>
<div class="grid">
<!-- 차트 카드 -->
<div class="card">
<h2 style="margin:0 0 10px;font-size:16px">실시간 차트 (Chart.js)</h2>
<div class="chart-wrap">
<canvas id="realtimeChart"></canvas>
</div>
<div style="margin-top:8px" class="muted">차트는 마지막 20 포인트를 표시합니다. 3초마다 데이터가 추가됩니다.</div>
</div>
<!-- 테이블 카드 -->
<div class="card">
<h2 style="margin:0 0 10px;font-size:16px">데이터 테이블</h2>
<!-- 검색/필터/페이징/정렬 컨트롤 -->
<div class="controls">
<input id="searchInput" placeholder="검색: 이름 포함" />
<select id="statusFilter">
<option value="all">모두</option>
<option value="ok">OK</option>
<option value="warn">WARN</option>
<option value="err">ERR</option>
</select>
<label class="muted">페이지 크기:</label>
<select id="pageSizeSelect">
<option>5</option>
<option selected>10</option>
<option>20</option>
</select>
<div style="flex:1"></div>
<button id="refreshBtn" class="page-btn">즉시 갱신</button>
</div>
<div style="overflow:auto">
<table id="dataTable" aria-label="데이터 테이블">
<thead>
<tr>
<th data-key="id">ID</th>
<th data-key="name">이름</th>
<th data-key="value">값</th>
<th data-key="status">상태</th>
<th data-key="ts">시각</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div class="pagination" id="pagination"></div>
</div>
</div>
</div>
</body>
</html>
GPT가 생성한 소스코드 설명(통합본)
1. HTML 구조
- KPI 박스 4개: 핵심 지표를 빠르게 보여줍니다.
- 차트: Chart.js 라인 차트로 마지막 20개 데이터를 실시간으로 표시합니다.
- 테이블: 검색, 필터, 정렬, 페이징 기능을 제공합니다.
2. CSS
- 그리드와 카드 스타일을 사용해 반응형 레이아웃을 구성했습니다.
- 화면이 넓을 경우 차트와 테이블이 가로로 배치됩니다.
3. JavaScript 주요 개념
state객체: 화면 상태(데이터, 정렬, 페이징 등)를 관리합니다.- 차트 초기화: Chart.js를 사용해 라인 차트를 만듭니다.
- 데이터 갱신:
simulateFetchData()함수는 서버 대신 랜덤 데이터를 생성합니다. 실제로는fetch()로 API를 호출하면 됩니다. - UI 갱신: KPI, 차트, 테이블 렌더링 함수가 각각 담당하며 데이터가 바뀌면 해당 함수들을 호출합니다.
- 이벤트 처리: 검색 입력, 필터 선택, 페이지 크기 변경, 테이블 헤더 클릭(정렬) 등 이벤트가 연결되어 있습니다.
- 주기적 갱신:
setInterval(doFetchAndRender, 3000)로 3초마다 데이터가 자동 갱신됩니다.
GPT가 생성한 실시간 대시보드 페이지

이해를 돕기 위해 본인의 화면을 캡쳐하여 제공합니다. 본 이미지의 저작권은 블로그 주인 제로에게 있습니다.

이해를 돕기 위해 본인의 화면을 캡쳐하여 제공합니다.
본 이미지의 저작권은 블로그 주인 제로에게 있습니다.
GPT는 다음과 같은 특징을 가집니다.
✔ KPI · 차트 · 테이블 모듈 분리
✔ Chart.js 설정이 정확하고 안정적
✔ 실시간 데이터 fetch 로직이 일반화되어 있음
✔ 검색/필터/페이징과 차트 상태 관리가 깔끔함
✔ 초보자용 주석 풍부
GPT는 “가장 기본적인 틀”을 만들어줍니다. 그대로 복붙해도 큰 문제 없이 작동합니다만
그래도 맹신하지 말고 에러가 없는지 다시 한번 확인해보세요.
Cursor – 개발자 친화적, 입문자 난이도는 올라감
Cursor 가 생성한 소스코드(통합본)
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>실시간 대시보드</title>
<!-- Chart.js 라이브러리 CDN -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<!-- CSS 파일 연결 -->
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- 헤더 섹션 -->
<header>
<h1>📊 실시간 대시보드</h1>
<p class="last-update">마지막 업데이트: <span id="lastUpdateTime">-</span></p>
</header>
<!-- 메인 컨테이너 -->
<main class="container">
<!-- 1. KPI 요약 박스 섹션 -->
<section class="kpi-section">
<h2>주요 지표 (KPI)</h2>
<div class="kpi-grid">
<!-- KPI 박스 1: 총 매출 -->
<div class="kpi-box">
<div class="kpi-icon">💰</div>
<div class="kpi-content">
<h3>총 매출</h3>
<p class="kpi-value" id="kpiRevenue">₩0</p>
<p class="kpi-change positive" id="kpiRevenueChange">+0%</p>
</div>
</div>
<!-- KPI 박스 2: 총 주문 -->
<div class="kpi-box">
<div class="kpi-icon">📦</div>
<div class="kpi-content">
<h3>총 주문</h3>
<p class="kpi-value" id="kpiOrders">0</p>
<p class="kpi-change positive" id="kpiOrdersChange">+0%</p>
</div>
</div>
<!-- KPI 박스 3: 활성 사용자 -->
<div class="kpi-box">
<div class="kpi-icon">👥</div>
<div class="kpi-content">
<h3>활성 사용자</h3>
<p class="kpi-value" id="kpiUsers">0</p>
<p class="kpi-change positive" id="kpiUsersChange">+0%</p>
</div>
</div>
<!-- KPI 박스 4: 평균 주문 금액 -->
<div class="kpi-box">
<div class="kpi-icon">📈</div>
<div class="kpi-content">
<h3>평균 주문 금액</h3>
<p class="kpi-value" id="kpiAvgOrder">₩0</p>
<p class="kpi-change positive" id="kpiAvgOrderChange">+0%</p>
</div>
</div>
</div>
</section>
<!-- 2. Chart.js 기반 실시간 차트 섹션 -->
<section class="chart-section">
<h2>실시간 매출 추이</h2>
<div class="chart-container">
<!-- Chart.js가 이 canvas 요소에 차트를 그립니다 -->
<canvas id="revenueChart"></canvas>
</div>
</section>
<!-- 3. 검색 + 필터 + 페이징 + 정렬 테이블 섹션 -->
<section class="table-section">
<h2>주문 내역</h2>
<!-- 검색 및 필터 컨트롤 -->
<div class="table-controls">
<!-- 검색 입력창 -->
<div class="search-box">
<input
type="text"
id="searchInput"
placeholder="주문 ID, 고객명, 상품명으로 검색..."
class="search-input"
>
<button id="searchBtn" class="btn btn-primary">검색</button>
</div>
<!-- 필터 드롭다운 -->
<div class="filter-box">
<label for="statusFilter">상태 필터:</label>
<select id="statusFilter" class="filter-select">
<option value="all">전체</option>
<option value="완료">완료</option>
<option value="처리중">처리중</option>
<option value="취소">취소</option>
</select>
</div>
</div>
<!-- 테이블 -->
<div class="table-wrapper">
<table id="ordersTable">
<thead>
<tr>
<!-- 정렬 가능한 헤더 (클릭 시 정렬) -->
<th class="sortable" data-column="id">
주문 ID <span class="sort-icon">⇅</span>
</th>
<th class="sortable" data-column="customer">
고객명 <span class="sort-icon">⇅</span>
</th>
<th class="sortable" data-column="product">
상품명 <span class="sort-icon">⇅</span>
</th>
<th class="sortable" data-column="amount">
금액 <span class="sort-icon">⇅</span>
</th>
<th class="sortable" data-column="status">
상태 <span class="sort-icon">⇅</span>
</th>
<th class="sortable" data-column="date">
주문일시 <span class="sort-icon">⇅</span>
</th>
</tr>
</thead>
<tbody id="tableBody">
<!-- JavaScript로 동적으로 데이터가 채워집니다 -->
<tr>
<td colspan="6" class="loading">데이터를 불러오는 중...</td>
</tr>
</tbody>
</table>
</div>
<!-- 페이징 컨트롤 -->
<div class="pagination">
<button id="prevBtn" class="btn btn-secondary">이전</button>
<span class="page-info">
페이지 <span id="currentPage">1</span> / <span id="totalPages">1</span>
(총 <span id="totalRecords">0</span>건)
</span>
<button id="nextBtn" class="btn btn-secondary">다음</button>
</div>
</section>
</main>
<!-- JavaScript 파일 연결 -->
<script src="script.js"></script>
</body>
</html>
/* ============================================
전체 스타일 설정
============================================ */
/* 모든 요소의 기본 여백과 패딩을 0으로 설정 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* body 스타일: 배경색, 폰트 설정 */
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
color: #333;
}
/* ============================================
헤더 스타일
============================================ */
header {
text-align: center;
color: white;
margin-bottom: 30px;
}
header h1 {
font-size: 2.5rem;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}
.last-update {
font-size: 0.9rem;
opacity: 0.9;
}
/* ============================================
메인 컨테이너
============================================ */
.container {
max-width: 1400px;
margin: 0 auto;
}
/* 각 섹션에 공통 스타일 적용 */
section {
background: white;
border-radius: 12px;
padding: 25px;
margin-bottom: 30px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
section h2 {
color: #667eea;
margin-bottom: 20px;
font-size: 1.5rem;
border-bottom: 2px solid #667eea;
padding-bottom: 10px;
}
/* ============================================
KPI 요약 박스 스타일
============================================ */
.kpi-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
.kpi-box {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 10px;
padding: 20px;
display: flex;
align-items: center;
gap: 15px;
color: white;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
/* 마우스 오버 시 약간 확대되는 효과 */
.kpi-box:hover {
transform: translateY(-5px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
}
.kpi-icon {
font-size: 3rem;
opacity: 0.9;
}
.kpi-content {
flex: 1;
}
.kpi-content h3 {
font-size: 0.9rem;
opacity: 0.9;
margin-bottom: 8px;
font-weight: normal;
}
.kpi-value {
font-size: 2rem;
font-weight: bold;
margin-bottom: 5px;
}
.kpi-change {
font-size: 0.85rem;
padding: 3px 8px;
border-radius: 12px;
display: inline-block;
}
/* 증가/감소 색상 */
.kpi-change.positive {
background: rgba(76, 175, 80, 0.3);
}
.kpi-change.negative {
background: rgba(244, 67, 54, 0.3);
}
/* ============================================
차트 섹션 스타일
============================================ */
.chart-container {
position: relative;
height: 400px;
margin-top: 20px;
}
/* Chart.js 캔버스가 차트를 그리는 영역 */
#revenueChart {
max-height: 400px;
}
/* ============================================
테이블 섹션 스타일
============================================ */
/* 검색 및 필터 컨트롤 */
.table-controls {
display: flex;
gap: 15px;
margin-bottom: 20px;
flex-wrap: wrap;
align-items: center;
}
.search-box {
flex: 1;
min-width: 300px;
display: flex;
gap: 10px;
}
.search-input {
flex: 1;
padding: 10px 15px;
border: 2px solid #ddd;
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.3s ease;
}
/* 입력창에 포커스가 갔을 때 */
.search-input:focus {
outline: none;
border-color: #667eea;
}
.filter-box {
display: flex;
align-items: center;
gap: 10px;
}
.filter-box label {
font-weight: 500;
color: #555;
}
.filter-select {
padding: 10px 15px;
border: 2px solid #ddd;
border-radius: 8px;
font-size: 1rem;
background: white;
cursor: pointer;
transition: border-color 0.3s ease;
}
.filter-select:focus {
outline: none;
border-color: #667eea;
}
/* 버튼 공통 스타일 */
.btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s ease;
font-weight: 500;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #5568d3;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(102, 126, 234, 0.3);
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
}
.btn-secondary:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
}
/* 테이블 래퍼 (스크롤 가능하게) */
.table-wrapper {
overflow-x: auto;
margin-bottom: 20px;
}
/* 테이블 스타일 */
table {
width: 100%;
border-collapse: collapse;
font-size: 0.95rem;
}
/* 테이블 헤더 */
thead {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
th {
padding: 15px;
text-align: left;
font-weight: 600;
position: relative;
}
/* 정렬 가능한 헤더 스타일 */
.sortable {
cursor: pointer;
user-select: none;
transition: background-color 0.3s ease;
}
.sortable:hover {
background: rgba(255, 255, 255, 0.1);
}
.sort-icon {
font-size: 0.8rem;
margin-left: 5px;
opacity: 0.7;
}
/* 정렬 방향 표시 */
.sort-asc .sort-icon::after {
content: ' ↑';
}
.sort-desc .sort-icon::after {
content: ' ↓';
}
/* 테이블 바디 */
tbody tr {
border-bottom: 1px solid #eee;
transition: background-color 0.2s ease;
}
/* 행에 마우스 오버 시 */
tbody tr:hover {
background-color: #f5f5f5;
}
td {
padding: 12px 15px;
}
/* 상태 배지 스타일 */
.status-badge {
padding: 5px 12px;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 500;
display: inline-block;
}
.status-complete {
background: #d4edda;
color: #155724;
}
.status-processing {
background: #fff3cd;
color: #856404;
}
.status-cancelled {
background: #f8d7da;
color: #721c24;
}
/* 로딩 메시지 */
.loading {
text-align: center;
padding: 40px;
color: #999;
font-style: italic;
}
/* ============================================
페이징 스타일
============================================ */
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
margin-top: 20px;
}
.page-info {
font-size: 1rem;
color: #555;
font-weight: 500;
}
/* ============================================
반응형 디자인 (모바일 대응)
============================================ */
@media (max-width: 768px) {
body {
padding: 10px;
}
header h1 {
font-size: 1.8rem;
}
.kpi-grid {
grid-template-columns: 1fr;
}
.table-controls {
flex-direction: column;
}
.search-box {
width: 100%;
min-width: auto;
}
.table-wrapper {
overflow-x: scroll;
}
table {
font-size: 0.85rem;
}
th, td {
padding: 8px;
}
}
/* ============================================
실시간 대시보드 JavaScript
============================================
이 파일은 대시보드의 모든 동적 기능을 담당합니다.
- KPI 데이터 표시
- Chart.js를 사용한 실시간 차트
- 검색, 필터, 페이징, 정렬 기능
- 3초마다 자동 데이터 갱신
============================================ */
// ============================================
// 전역 변수 선언
// ============================================
// Chart.js 차트 객체 (나중에 초기화됨)
let revenueChart = null;
// 테이블 관련 변수
let allOrders = []; // 모든 주문 데이터
let filteredOrders = []; // 필터링된 주문 데이터
let currentPage = 1; // 현재 페이지 번호
let itemsPerPage = 10; // 페이지당 표시할 항목 수
let currentSort = { // 현재 정렬 상태
column: null, // 정렬할 컬럼
direction: 'asc' // 정렬 방향 ('asc' 또는 'desc')
};
// 이전 KPI 값 저장 (변화율 계산용)
let previousKPI = {
revenue: 0,
orders: 0,
users: 0,
avgOrder: 0
};
// ============================================
// 페이지 로드 시 초기화
// ============================================
// DOMContentLoaded 이벤트: HTML이 완전히 로드된 후 실행
document.addEventListener('DOMContentLoaded', function() {
console.log('대시보드 초기화 시작...');
// 차트 초기화
initChart();
// 초기 데이터 로드
updateDashboard();
// 이벤트 리스너 등록
setupEventListeners();
// 3초마다 자동 갱신 설정
setInterval(updateDashboard, 3000);
console.log('대시보드 초기화 완료!');
});
// ============================================
// Chart.js 차트 초기화
// ============================================
function initChart() {
// canvas 요소 가져오기
const ctx = document.getElementById('revenueChart');
// Chart.js로 차트 생성
revenueChart = new Chart(ctx, {
type: 'line', // 선 그래프 타입
data: {
labels: [], // X축 레이블 (시간)
datasets: [{
label: '매출 (원)', // 데이터셋 이름
data: [], // Y축 데이터 (매출 값)
borderColor: 'rgb(102, 126, 234)', // 선 색상
backgroundColor: 'rgba(102, 126, 234, 0.1)', // 영역 배경색
borderWidth: 2, // 선 두께
fill: true, // 영역 채우기
tension: 0.4 // 곡선 부드러움 (0~1)
}]
},
options: {
responsive: true, // 반응형 차트
maintainAspectRatio: false, // 비율 유지 안 함 (높이 조절 가능)
plugins: {
legend: {
display: true, // 범례 표시
position: 'top' // 범례 위치
},
tooltip: {
enabled: true // 툴팁 표시
}
},
scales: {
y: {
beginAtZero: true, // Y축 0부터 시작
ticks: {
// Y축 값 포맷팅 (원 단위로 표시)
callback: function(value) {
return '₩' + value.toLocaleString();
}
}
}
},
animation: {
duration: 750 // 애니메이션 지속 시간 (밀리초)
}
}
});
}
// ============================================
// 대시보드 데이터 업데이트 (메인 함수)
// ============================================
function updateDashboard() {
// 랜덤 데이터 생성 (실제로는 서버에서 가져옴)
const newData = generateRandomData();
// KPI 업데이트
updateKPI(newData);
// 차트 업데이트
updateChart(newData);
// 테이블 데이터 업데이트
updateTableData(newData.orders);
// 마지막 업데이트 시간 표시
updateLastUpdateTime();
}
// ============================================
// 랜덤 데이터 생성 함수
// ============================================
// 실제 환경에서는 이 부분을 서버 API 호출로 대체합니다.
function generateRandomData() {
// 랜덤 KPI 값 생성
const revenue = Math.floor(Math.random() * 50000000) + 10000000; // 1천만~6천만원
const orders = Math.floor(Math.random() * 500) + 100; // 100~600건
const users = Math.floor(Math.random() * 1000) + 200; // 200~1200명
const avgOrder = Math.floor(revenue / orders); // 평균 주문 금액
// 주문 데이터 생성
const ordersList = [];
const customers = ['김철수', '이영희', '박민수', '최지영', '정수진', '한동훈', '윤서연', '강민호'];
const products = ['노트북', '스마트폰', '태블릿', '이어폰', '키보드', '마우스', '모니터', '웹캠'];
const statuses = ['완료', '처리중', '취소'];
// 50개의 랜덤 주문 생성
for (let i = 0; i < 50; i++) {
const orderId = 'ORD-' + String(Math.floor(Math.random() * 10000)).padStart(5, '0');
const customer = customers[Math.floor(Math.random() * customers.length)];
const product = products[Math.floor(Math.random() * products.length)];
const amount = Math.floor(Math.random() * 500000) + 50000;
const status = statuses[Math.floor(Math.random() * statuses.length)];
const date = new Date(Date.now() - Math.random() * 7 * 24 * 60 * 60 * 1000); // 최근 7일 내
ordersList.push({
id: orderId,
customer: customer,
product: product,
amount: amount,
status: status,
date: date
});
}
return {
revenue: revenue,
orders: orders,
users: users,
avgOrder: avgOrder,
ordersList: ordersList
};
}
// ============================================
// KPI 박스 업데이트
// ============================================
function updateKPI(data) {
// 총 매출 업데이트
const revenueElement = document.getElementById('kpiRevenue');
const revenueChangeElement = document.getElementById('kpiRevenueChange');
updateKPIBox(revenueElement, revenueChangeElement, data.revenue, previousKPI.revenue, true);
previousKPI.revenue = data.revenue;
// 총 주문 업데이트
const ordersElement = document.getElementById('kpiOrders');
const ordersChangeElement = document.getElementById('kpiOrdersChange');
updateKPIBox(ordersElement, ordersChangeElement, data.orders, previousKPI.orders, false);
previousKPI.orders = data.orders;
// 활성 사용자 업데이트
const usersElement = document.getElementById('kpiUsers');
const usersChangeElement = document.getElementById('kpiUsersChange');
updateKPIBox(usersElement, usersChangeElement, data.users, previousKPI.users, false);
previousKPI.users = data.users;
// 평균 주문 금액 업데이트
const avgOrderElement = document.getElementById('kpiAvgOrder');
const avgOrderChangeElement = document.getElementById('kpiAvgOrderChange');
updateKPIBox(avgOrderElement, avgOrderChangeElement, data.avgOrder, previousKPI.avgOrder, true);
previousKPI.avgOrder = data.avgOrder;
}
// KPI 박스 개별 업데이트 함수
function updateKPIBox(valueElement, changeElement, currentValue, previousValue, isCurrency) {
// 값 포맷팅
let formattedValue;
if (isCurrency) {
formattedValue = '₩' + currentValue.toLocaleString();
} else {
formattedValue = currentValue.toLocaleString();
}
// 값 업데이트
valueElement.textContent = formattedValue;
// 변화율 계산
if (previousValue === 0) {
changeElement.textContent = '+0%';
changeElement.className = 'kpi-change positive';
} else {
const changePercent = ((currentValue - previousValue) / previousValue * 100).toFixed(1);
const isPositive = changePercent >= 0;
changeElement.textContent = (isPositive ? '+' : '') + changePercent + '%';
changeElement.className = 'kpi-change ' + (isPositive ? 'positive' : 'negative');
}
}
// ============================================
// 차트 업데이트
// ============================================
function updateChart(data) {
// 현재 시간을 레이블로 추가
const now = new Date();
const timeLabel = now.getHours() + ':' + String(now.getMinutes()).padStart(2, '0') + ':' + String(now.getSeconds()).padStart(2, '0');
// 차트 데이터에 새 데이터 추가
revenueChart.data.labels.push(timeLabel);
revenueChart.data.datasets[0].data.push(data.revenue);
// 최대 20개의 데이터 포인트만 유지 (오래된 데이터 제거)
if (revenueChart.data.labels.length > 20) {
revenueChart.data.labels.shift(); // 첫 번째 요소 제거
revenueChart.data.datasets[0].data.shift();
}
// 차트 업데이트 (애니메이션과 함께)
revenueChart.update('active');
}
// ============================================
// 테이블 데이터 업데이트
// ============================================
function updateTableData(orders) {
// 전체 주문 데이터 저장
allOrders = orders;
// 필터링 및 검색 적용
applyFiltersAndSearch();
}
// ============================================
// 필터 및 검색 적용
// ============================================
function applyFiltersAndSearch() {
// 1. 검색어 가져오기
const searchTerm = document.getElementById('searchInput').value.toLowerCase().trim();
// 2. 상태 필터 가져오기
const statusFilter = document.getElementById('statusFilter').value;
// 3. 필터링 적용
filteredOrders = allOrders.filter(order => {
// 검색어 필터: 주문 ID, 고객명, 상품명에서 검색
const matchesSearch = searchTerm === '' ||
order.id.toLowerCase().includes(searchTerm) ||
order.customer.toLowerCase().includes(searchTerm) ||
order.product.toLowerCase().includes(searchTerm);
// 상태 필터
const matchesStatus = statusFilter === 'all' || order.status === statusFilter;
// 두 조건 모두 만족해야 함
return matchesSearch && matchesStatus;
});
// 4. 정렬 적용
if (currentSort.column) {
sortOrders(currentSort.column, currentSort.direction);
}
// 5. 페이지 1로 리셋
currentPage = 1;
// 6. 테이블 렌더링
renderTable();
}
// ============================================
// 테이블 정렬 함수
// ============================================
function sortOrders(column, direction) {
filteredOrders.sort((a, b) => {
let aValue = a[column];
let bValue = b[column];
// 날짜 컬럼인 경우 Date 객체로 변환
if (column === 'date') {
aValue = new Date(aValue);
bValue = new Date(bValue);
}
// 숫자 컬럼인 경우 숫자로 변환
if (column === 'amount' || column === 'id') {
aValue = column === 'amount' ? aValue : parseInt(aValue.replace('ORD-', ''));
bValue = column === 'amount' ? bValue : parseInt(bValue.replace('ORD-', ''));
}
// 문자열 컬럼인 경우 소문자로 변환하여 비교
if (typeof aValue === 'string') {
aValue = aValue.toLowerCase();
bValue = bValue.toLowerCase();
}
// 정렬 방향에 따라 비교
if (direction === 'asc') {
return aValue > bValue ? 1 : aValue < bValue ? -1 : 0;
} else {
return aValue < bValue ? 1 : aValue > bValue ? -1 : 0;
}
});
}
// ============================================
// 테이블 렌더링 함수
// ============================================
function renderTable() {
const tbody = document.getElementById('tableBody');
// 페이징 계산
const totalPages = Math.ceil(filteredOrders.length / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const pageOrders = filteredOrders.slice(startIndex, endIndex);
// 테이블 바디 비우기
tbody.innerHTML = '';
// 데이터가 없는 경우
if (pageOrders.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="loading">검색 결과가 없습니다.</td></tr>';
updatePagination(0, 1);
return;
}
// 각 주문을 테이블 행으로 변환
pageOrders.forEach(order => {
const row = document.createElement('tr');
// 날짜 포맷팅
const dateStr = order.date.toLocaleString('ko-KR', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
// 상태 배지 클래스 결정
let statusClass = 'status-complete';
if (order.status === '처리중') statusClass = 'status-processing';
if (order.status === '취소') statusClass = 'status-cancelled';
// 행 HTML 생성
row.innerHTML = `
<td>${order.id}</td>
<td>${order.customer}</td>
<td>${order.product}</td>
<td>₩${order.amount.toLocaleString()}</td>
<td><span class="status-badge ${statusClass}">${order.status}</span></td>
<td>${dateStr}</td>
`;
tbody.appendChild(row);
});
// 페이징 정보 업데이트
updatePagination(filteredOrders.length, totalPages);
}
// ============================================
// 페이징 정보 업데이트
// ============================================
function updatePagination(totalRecords, totalPages) {
document.getElementById('currentPage').textContent = currentPage;
document.getElementById('totalPages').textContent = totalPages;
document.getElementById('totalRecords').textContent = totalRecords;
// 이전/다음 버튼 활성화/비활성화
document.getElementById('prevBtn').disabled = currentPage === 1;
document.getElementById('nextBtn').disabled = currentPage === totalPages || totalPages === 0;
}
// ============================================
// 마지막 업데이트 시간 표시
// ============================================
function updateLastUpdateTime() {
const now = new Date();
const timeStr = now.toLocaleTimeString('ko-KR');
document.getElementById('lastUpdateTime').textContent = timeStr;
}
// ============================================
// 이벤트 리스너 설정
// ============================================
function setupEventListeners() {
// 검색 버튼 클릭
document.getElementById('searchBtn').addEventListener('click', function() {
applyFiltersAndSearch();
});
// 검색 입력창에서 Enter 키 입력
document.getElementById('searchInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
applyFiltersAndSearch();
}
});
// 상태 필터 변경
document.getElementById('statusFilter').addEventListener('change', function() {
applyFiltersAndSearch();
});
// 이전 페이지 버튼
document.getElementById('prevBtn').addEventListener('click', function() {
if (currentPage > 1) {
currentPage--;
renderTable();
}
});
// 다음 페이지 버튼
document.getElementById('nextBtn').addEventListener('click', function() {
const totalPages = Math.ceil(filteredOrders.length / itemsPerPage);
if (currentPage < totalPages) {
currentPage++;
renderTable();
}
});
// 테이블 헤더 클릭 (정렬)
document.querySelectorAll('.sortable').forEach(header => {
header.addEventListener('click', function() {
const column = this.getAttribute('data-column');
// 같은 컬럼을 클릭하면 정렬 방향 토글
if (currentSort.column === column) {
currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
} else {
currentSort.column = column;
currentSort.direction = 'asc';
}
// 정렬 아이콘 업데이트
updateSortIcons(column, currentSort.direction);
// 정렬 적용
sortOrders(column, currentSort.direction);
renderTable();
});
});
}
// ============================================
// 정렬 아이콘 업데이트
// ============================================
function updateSortIcons(activeColumn, direction) {
// 모든 정렬 아이콘 초기화
document.querySelectorAll('.sortable').forEach(header => {
header.classList.remove('sort-asc', 'sort-desc');
});
// 활성 컬럼에 정렬 방향 클래스 추가
const activeHeader = document.querySelector(`[data-column="${activeColumn}"]`);
if (activeHeader) {
activeHeader.classList.add(direction === 'asc' ? 'sort-asc' : 'sort-desc');
}
}
Cursor 가 생성한 소스코드 해설(통합본)
index.html – HTML 구조
- KPI 요약 박스 4개
- Chart.js 차트 영역
- 검색/필터/페이징/정렬 테이블
style.css – 스타일
- 그라데이션 배경
- 반응형 레이아웃
- 호버 효과
script.js – JavaScript 로직
- Chart.js 실시간 차트
- 3초마다 자동 데이터 갱신
- 검색, 필터, 페이징, 정렬 기능
- 상세한 주석 포함
Cursor가 생성한 대시보드 페이지

이해를 돕기 위해 본인의 화면을 캡쳐하여 제공합니다. 본 이미지의 저작권은 블로그 주인 제로에게 있습니다.

이해를 돕기 위해 본인의 화면을 캡쳐하여 제공합니다.
본 이미지의 저작권은 블로그 주인 제로에게 있습니다.
Cursor는 다음과 같은 특징을 보여줍니다.
✔ 폴더 구조까지 제안 (예: /components /services 등)
✔ 함수형·모듈형 코드가 깔끔하게 분리
✔ TypeScript 기반 추천도 해줌
✔ 실무형이지만 난이도가 높음
하지만. 다음과 같은 단점을 가집니다.
- 입문자는 이해 난이도 높음
- Chart.js 옵션을 간략화해 내용을 생략하기도 함
“코드 품질은 최고지만 초보자용은 아님”
AntiGravity – 빠르지만 매우 단순한 버전 생성
AntiGravity 가 생성한 소스코드(통합본)
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>실시간 데이터 대시보드</title>
<!-- Google Fonts: 세련된 폰트 사용 (Outfit) -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&display=swap" rel="stylesheet">
<!-- 스타일시트 연결 -->
<link rel="stylesheet" href="css/style.css">
<!-- Chart.js 라이브러리 (CDN) -->
<!-- 차트를 그리기 위해 필요한 외부 라이브러리입니다. -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
<div class="app-container">
<!-- 헤더 영역 -->
<header class="header">
<h1>📊 Real-time Dashboard</h1>
<p>실시간 데이터 모니터링 시스템 (3초 갱신)</p>
</header>
<!-- 메인 콘텐츠 영역 -->
<main class="main-content">
<!-- 1. KPI (핵심 성과 지표) 요약 박스 영역 -->
<section class="kpi-section">
<!-- 총 방문자 수 카드 -->
<div class="kpi-card">
<h3>Total Visitors</h3>
<p class="kpi-value" id="kpi-visitors">0</p>
<span class="kpi-trend positive">▲ 12%</span>
</div>
<!-- 활성 사용자 수 카드 -->
<div class="kpi-card">
<h3>Active Users</h3>
<p class="kpi-value" id="kpi-active">0</p>
<span class="kpi-trend neutral">- 0%</span>
</div>
<!-- 총 매출 카드 -->
<div class="kpi-card">
<h3>Revenue</h3>
<p class="kpi-value" id="kpi-revenue">$0</p>
<span class="kpi-trend negative">▼ 3%</span>
</div>
<!-- 서버 상태 카드 -->
<div class="kpi-card">
<h3>Server Status</h3>
<p class="kpi-value status-ok" id="kpi-status">Normal</p>
<span class="kpi-sub">Latency: <span id="kpi-latency">0ms</span></span>
</div>
</section>
<!-- 2. 차트 영역 -->
<section class="chart-section">
<div class="card">
<div class="card-header">
<h2>Real-time Traffic</h2>
<div class="live-indicator"><span class="blink">●</span> LIVE</div>
</div>
<div class="chart-container">
<!-- Chart.js가 이 canvas 태그에 차트를 그립니다 -->
<canvas id="realtimeChart"></canvas>
</div>
</div>
</section>
<!-- 3. 데이터 테이블 영역 -->
<section class="table-section">
<div class="card">
<div class="card-header">
<h2>Transaction History</h2>
</div>
<!-- 테이블 컨트롤 바 (검색, 필터) -->
<div class="table-controls">
<div class="control-group">
<label for="statusFilter">Status:</label>
<select id="statusFilter">
<option value="all">All</option>
<option value="completed">Completed</option>
<option value="pending">Pending</option>
<option value="failed">Failed</option>
</select>
</div>
<div class="control-group">
<input type="text" id="searchInput" placeholder="Search user or ID...">
</div>
</div>
<!-- 데이터 테이블 -->
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<!-- 클릭 시 정렬 기능을 위한 data-sort 속성 -->
<th data-sort="id">ID ↕</th>
<th data-sort="user">User ↕</th>
<th data-sort="amount">Amount ↕</th>
<th data-sort="status">Status ↕</th>
<th data-sort="date">Date ↕</th>
</tr>
</thead>
<tbody id="tableBody">
<!-- 자바스크립트로 데이터가 여기에 추가됩니다 -->
</tbody>
</table>
</div>
<!-- 페이지네이션 (페이징) -->
<div class="pagination">
<button id="prevBtn" disabled>&lt; Prev</button>
<span id="pageInfo">Page 1 of 1</span>
<button id="nextBtn" disabled>Next &gt;</button>
</div>
</div>
</section>
</main>
</div>
<!-- 로직 스크립트 연결 -->
<script src="js/script.js"></script>
</body>
</html>
/*
* 실시간 대시보드 스타일 시트
* 설명: 전체적인 레이아웃, 색상 테마, 반응형 디자인을 정의합니다.
*/
:root {
/* 프리미엄 컬러 팔레트 (다크/모던 테마) */
--bg-color: #f0f2f5;
--text-primary: #333333;
--text-secondary: #666666;
--card-bg: #ffffff;
--accent-color: #4f46e5;
/* 인디고 블루 */
--success-color: #10b981;
--warning-color: #f59e0b;
--danger-color: #ef4444;
--border-color: #e5e7eb;
/* 그림자 효과 */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
/* 기본 초기화 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Outfit', sans-serif;
background-color: --bg-color;
color: var(--text-primary);
line-height: 1.6;
}
/* 레이아웃 컨테이너 */
.app-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
/* 헤더 스타일 */
.header {
margin-bottom: 30px;
text-align: center;
}
.header h1 {
font-weight: 700;
font-size: 2rem;
color: var(--text-primary);
}
.header p {
color: var(--text-secondary);
font-size: 0.9rem;
}
/* 메인 콘텐츠 그리드 */
/* 모바일 First 접근: 기본은 1열, PC에서 2열 등으로 확장 */
.main-content {
display: flex;
flex-direction: column;
gap: 24px;
}
/* 카드 공통 스타일 */
.card,
.kpi-card {
background-color: var(--card-bg);
border-radius: 12px;
box-shadow: var(--shadow-md);
padding: 24px;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.card:hover,
.kpi-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
/* 1. KPI 섹션 스타일 */
/* 그리드 레이아웃: 4개의 카드를 나란히 배치 */
.kpi-section {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.kpi-card h3 {
font-size: 0.9rem;
color: var(--text-secondary);
margin-bottom: 10px;
font-weight: 600;
}
.kpi-value {
font-size: 2rem;
font-weight: 700;
margin-bottom: 5px;
color: var(--text-primary);
}
.kpi-trend {
font-size: 0.85rem;
font-weight: 600;
}
.kpi-trend.positive {
color: var(--success-color);
}
.kpi-trend.negative {
color: var(--danger-color);
}
.kpi-trend.neutral {
color: var(--text-secondary);
}
.kpi-sub {
font-size: 0.8rem;
color: var(--text-secondary);
}
.status-ok {
color: var(--success-color);
}
/* 2. 차트 섹션 스타일 */
.chart-section .card {
min-height: 400px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.card-header h2 {
font-size: 1.25rem;
font-weight: 700;
}
.live-indicator {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.85rem;
font-weight: 600;
color: var(--danger-color);
}
.blink {
animation: blinker 1.5s linear infinite;
}
@keyframes blinker {
50% {
opacity: 0;
}
}
.chart-container {
position: relative;
height: 300px;
/* 차트 높이 고정 */
width: 100%;
}
/* 3. 테이블 섹션 스타일 */
.table-controls {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin-bottom: 20px;
justify-content: space-between;
}
.control-group {
display: flex;
align-items: center;
gap: 8px;
}
.control-group input,
.control-group select {
padding: 8px 12px;
border: 1px solid var(--border-color);
border-radius: 6px;
font-family: inherit;
outline: none;
}
.control-group input:focus,
.control-group select:focus {
border-color: var(--accent-color);
box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.1);
}
/* 테이블 디자인 */
.table-responsive {
overflow-x: auto;
/* 모바일에서 가로 스크롤 허용 */
}
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 0.95rem;
}
.data-table th,
.data-table td {
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
.data-table th {
background-color: #f9fafb;
font-weight: 600;
color: var(--text-secondary);
cursor: pointer;
user-select: none;
}
.data-table th:hover {
background-color: #f3f4f6;
}
.data-table tr:hover {
background-color: #f9fafb;
}
/* 상태 뱃지 스타일 */
.status-badge {
padding: 4px 8px;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 600;
}
.status-badge.completed {
background-color: #d1fae5;
color: #065f46;
}
.status-badge.pending {
background-color: #fef3c7;
color: #92400e;
}
.status-badge.failed {
background-color: #fee2e2;
color: #991b1b;
}
/* 페이지네이션 */
.pagination {
display: flex;
justify-content: center;
align-items: center;
margin-top: 20px;
gap: 16px;
}
.pagination button {
padding: 8px 16px;
border: 1px solid var(--border-color);
background-color: white;
border-radius: 6px;
cursor: pointer;
font-family: inherit;
transition: all 0.2s;
}
.pagination button:hover:not(:disabled) {
background-color: #f3f4f6;
border-color: #d1d5db;
}
.pagination button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 반응형 미디어 쿼리 */
@media (max-width: 768px) {
.header h1 {
font-size: 1.5rem;
}
.kpi-section {
grid-template-columns: 1fr 1fr;
/* 모바일에서 2열 */
}
.table-controls {
flex-direction: column;
align-items: stretch;
}
.control-group {
flex-direction: column;
align-items: stretch;
}
}
@media (max-width: 480px) {
.kpi-section {
grid-template-columns: 1fr;
/* 아주 작은 화면 1열 */
}
}
/**
* 실시간 대시보드 메인 로직 스크립트
* 설명: 데이터 생성, 차트 업데이트, 테이블 관리 등을 담당합니다.
* 초보자를 위해 상세한 주석을 포함하고 있습니다.
*/
document.addEventListener('DOMContentLoaded', () => {
// ==========================================
// 1. 데이터 관리 및 상태 (Global State)
// ==========================================
// 차트 데이터 (초기값)
const chartData = {
labels: [], // 시간 라벨 (예: 10:00:00)
datasets: [{
label: '실시간 트래픽',
data: [], // 실제 데이터 값
borderColor: '#4f46e5', // 선 색상 (인디고)
backgroundColor: 'rgba(79, 70, 229, 0.1)', // 채우기 색상
borderWidth: 2,
tension: 0.4, // 곡선 부드러움 정도
fill: true // 아래 영역 채우기
}]
};
// 테이블 데이터 (거래 내역)
let transactionData = [];
// 테이블 상태 관리 (정렬, 페이징, 필터)
let tableState = {
sortKey: 'date', // 현재 정렬 기준 (id, user, amount, status, date)
sortOrder: 'desc', // 정렬 순서 (asc: 오름차순, desc: 내림차순)
currentPage: 1, // 현재 페이지
itemsPerPage: 5, // 페이지 당 항목 수
filterStatus: 'all', // 상태 필터 (all, completed, pending, failed)
searchQuery: '' // 검색어
};
// KPI 데이터 (초기값)
let kpiData = {
visitors: 1250,
active: 45,
revenue: 120500, // 달러 단위
latency: 24
};
// ==========================================
// 2. Chart.js 차트 초기화
// ==========================================
const ctx = document.getElementById('realtimeChart').getContext('2d');
const myChart = new Chart(ctx, {
type: 'line', // 선 그래프
data: chartData,
options: {
responsive: true,
maintainAspectRatio: false, // 컨테이너에 맞춰 크기 조절
animation: {
duration: 1000, // 애니메이션 1초
easing: 'linear' // 부드러운 연결
},
scales: {
y: {
beginAtZero: true,
grid: {
color: '#f3f4f6' // 그리드 선 색상 연하게
}
},
x: {
grid: {
display: false // x축 그리드 숨김
}
}
},
plugins: {
legend: {
display: false // 범례 숨김 (타이틀이 있으므로)
},
tooltip: {
mode: 'index',
intersect: false
}
},
interaction: {
mode: 'nearest',
axis: 'x',
intersect: false
}
}
});
// 초기 차트 데이터 채우기 (과거 10개 시점)
const now = new Date();
for (let i = 10; i >= 0; i--) {
const pastTime = new Date(now.getTime() - i * 3000);
addChartData(pastTime, Math.floor(Math.random() * 50) + 20);
}
// ==========================================
// 3. 유틸리티 함수 & 데이터 생성기
// ==========================================
/**
* 숫자를 포맷팅합니다 (예: 1200 -> 1,200)
*/
function formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
/**
* 날짜를 시간 문자열로 변환 (HH:mm:ss)
*/
function formatTime(date) {
return date.toTimeString().split(' ')[0];
}
/**
* 랜덤 거래 내역 생성기
*/
function generateTransaction() {
const id = Math.floor(10000 + Math.random() * 90000); // 5자리 랜덤 ID
const users = ['Alice', 'Bob', 'Charlie', 'David', 'Eve', 'Frank'];
const user = users[Math.floor(Math.random() * users.length)];
const amount = Math.floor(Math.random() * 500) + 10;
const statuses = ['completed', 'pending', 'failed'];
// 확률 조작: completed(70%), pending(20%), failed(10%)
const rand = Math.random();
let status = 'completed';
if (rand > 0.7) status = 'pending';
if (rand > 0.9) status = 'failed';
return {
id: `#${id}`,
user: user,
amount: amount,
status: status,
date: new Date()
};
}
// ==========================================
// 4. 로직 함수 (차트, 테이블, KPI 업데이트)
// ==========================================
/**
* 차트에 데이터를 추가하고 오래된 데이터를 제거하는 함수
*/
function addChartData(date, value) {
const label = formatTime(date);
// 라벨과 데이터 배열에 추가
myChart.data.labels.push(label);
myChart.data.datasets[0].data.push(value);
// 데이터가 20개를 넘어가면 가장 오래된 것 삭제 (메모리 관리)
if (myChart.data.labels.length > 20) {
myChart.data.labels.shift();
myChart.data.datasets[0].data.shift();
}
// 차트 업데이트 명령
myChart.update('none'); // 'none' 모드는 전체 다시 그리기 방지(성능 최적화)
}
/**
* 테이블 렌더링 함수 (필터, 검색, 정렬 포함)
*/
function renderTable() {
const tbody = document.getElementById('tableBody');
tbody.innerHTML = ''; // 기존 내용 비우기
// 1. 필터링 & 검색
let filteredData = transactionData.filter(item => {
const matchStatus = tableState.filterStatus === 'all' || item.status === tableState.filterStatus;
const searchTerm = tableState.searchQuery.toLowerCase();
const matchSearch = item.user.toLowerCase().includes(searchTerm) || item.id.toLowerCase().includes(searchTerm);
return matchStatus && matchSearch;
});
// 2. 정렬
filteredData.sort((a, b) => {
let valA = a[tableState.sortKey];
let valB = b[tableState.sortKey];
if (typeof valA === 'string') valA = valA.toLowerCase();
if (typeof valB === 'string') valB = valB.toLowerCase();
if (valA < valB) return tableState.sortOrder === 'asc' ? -1 : 1;
if (valA > valB) return tableState.sortOrder === 'asc' ? 1 : -1;
return 0;
});
// 3. 페이지네이션 계산
const totalItems = filteredData.length;
const totalPages = Math.max(1, Math.ceil(totalItems / tableState.itemsPerPage));
// 페이지 범위 보정
if (tableState.currentPage > totalPages) tableState.currentPage = totalPages;
const startIdx = (tableState.currentPage - 1) * tableState.itemsPerPage;
const endIdx = startIdx + tableState.itemsPerPage;
const pageData = filteredData.slice(startIdx, endIdx);
// 4. HTML 생성
pageData.forEach(item => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${item.id}</td>
<td>${item.user}</td>
<td>$${item.amount}</td>
<td><span class="status-badge ${item.status}">${item.status}</span></td>
<td>${formatTime(item.date)}</td>
`;
tbody.appendChild(row);
});
// 5. 페이지네이션 UI 업데이트
document.getElementById('pageInfo').textContent = `Page ${tableState.currentPage} of ${totalPages}`;
document.getElementById('prevBtn').disabled = tableState.currentPage === 1;
document.getElementById('nextBtn').disabled = tableState.currentPage === totalPages;
}
/**
* KPI 값 랜덤 업데이트
*/
function updateKPI() {
// 방문자 수 약간 변동
kpiData.visitors += Math.floor(Math.random() * 10) - 2;
// 활성 사용자 변동
kpiData.active = Math.floor(Math.random() * 20) + 30;
// 매출 증가
kpiData.revenue += Math.floor(Math.random() * 100);
// 레이턴시 변동
kpiData.latency = Math.floor(Math.random() * 40) + 10;
// 화면 갱신
document.getElementById('kpi-visitors').textContent = formatNumber(kpiData.visitors);
document.getElementById('kpi-active').textContent = kpiData.active;
document.getElementById('kpi-revenue').textContent = '$' + formatNumber(kpiData.revenue);
document.getElementById('kpi-latency').textContent = kpiData.latency + 'ms';
// 레이턴시에 따른 상태 표시
const statusEl = document.getElementById('kpi-status');
if (kpiData.latency > 50) {
statusEl.textContent = 'Slow';
statusEl.className = 'kpi-value text-warning'; // CSS 클래스 필요하다면 추가
statusEl.style.color = '#f59e0b';
} else {
statusEl.textContent = 'Normal';
statusEl.className = 'kpi-value status-ok';
statusEl.style.color = '#10b981';
}
}
/**
* 메인 업데이트 함수 (3초마다 실행될 내용)
*/
function tick() {
const now = new Date();
// 1. 차트 업데이트 (랜덤 트래픽 데이터)
const traffic = Math.floor(Math.random() * 80) + 20; // 20~100 사이 랜덤
addChartData(now, traffic);
// 2. 새로운 거래 내역 추가
// 새로운 데이터가 맨 앞에 오도록 unshift 사용
transactionData.unshift(generateTransaction());
// 데이터가 너무 많아지지 않게 100개로 제한
if (transactionData.length > 100) transactionData.pop();
// 3. KPI 업데이트
updateKPI();
// 4. 테이블 다시 그리기
renderTable();
}
// ==========================================
// 5. 이벤트 리스너 (사용자 입력 처리)
// ==========================================
// 검색 입력
document.getElementById('searchInput').addEventListener('input', (e) => {
tableState.searchQuery = e.target.value;
tableState.currentPage = 1; // 검색 시 첫 페이지로 이동
renderTable();
});
// 상태 필터 변경
document.getElementById('statusFilter').addEventListener('change', (e) => {
tableState.filterStatus = e.target.value;
tableState.currentPage = 1;
renderTable();
});
// 헤더 클릭 시 정렬 (이벤트 위임)
document.querySelectorAll('th[data-sort]').forEach(th => {
th.addEventListener('click', () => {
const key = th.dataset.sort;
if (tableState.sortKey === key) {
// 같은 키를 누르면 정렬 순서 반전
tableState.sortOrder = tableState.sortOrder === 'asc' ? 'desc' : 'asc';
} else {
tableState.sortKey = key;
tableState.sortOrder = 'desc'; // 새 키는 내림차순 기본
}
renderTable();
});
});
// 페이지 이동 버튼
document.getElementById('prevBtn').addEventListener('click', () => {
if (tableState.currentPage > 1) {
tableState.currentPage--;
renderTable();
}
});
document.getElementById('nextBtn').addEventListener('click', () => {
tableState.currentPage++; // renderTable 내에서 최대 페이지 체크함
renderTable();
});
// ==========================================
// 6. 실행 시작
// ==========================================
// 초기 데이터 몇 개 생성
for (let i = 0; i < 15; i++) {
transactionData.push(generateTransaction());
}
// 초기 렌더링
renderTable();
updateKPI();
// 3초마다 tick 함수 실행 (실시간 업데이트)
setInterval(tick, 3000);
});
AntiGravity가 생성한 소스코드 해설(통합본)
1. 뼈대: index.html
웹페이지의 전체적인 구조를 담당합니다.
- CDN 연결:
<script src="...chart.js">로 차트 라이브러리를 가져왔습니다. - 구역 나누기:
.kpi-section: 맨 위 숫자 4개가 나오는 요약 박스 영역입니다..chart-section: 그래프가 그려질<canvas>태그가 있는 영역입니다..table-section: 검색창, 필터, 그리고 데이터 테이블이 들어있는 영역입니다.
2. 꾸미기: css/style.css
디자인과 배치를 담당합니다.
- 변수 사용 (
:root): 색상이나 그림자 등을 변수(--bg-color등)로 만들어, 나중에 한 곳만 바꾸면 전체 테마를 쉽게 바꿀 수 있게 했습니다. - 반응형 레이아웃 (
Grid&Flex):Grid: KPI 카드 4개를 깔끔하게 바둑판처럼 배치합니다. 화면이 좁아지면 알아서 2줄, 4줄로 바뀝니다.Flexbox: 검색창과 필터 박스 등을 옆으로 나란히 정렬할 때 사용했습니다.
- 카드 디자인:
.card클래스에 흰 배경과 그림자를 주어 내용이 붕 떠 있는 듯한 깔끔한 느낌을 줬습니다.
3. 두뇌: js/script.js
실제 움직임을 담당하는 가장 중요한 파일입니다.
- 가짜 데이터 공장: generateTransaction() 함수가 랜덤한 아이디, 이름, 금액 등을 계속 만들어냅니다.
- Chart.js 연동:
new Chart(...)로 차트를 생성하고,- tick() 함수가 3초마다 실행되면서 새로운 데이터를 차트에 넣고(
push), 너무 오래된 데이터는 뺍니다(shift).
- 만능 테이블 (renderTable):
- 데이터가 바뀌거나, 검색하거나, 정렬할 때마다 테이블을 싹 지우고 다시 그립니다.
- 과정:
전체 데이터->필터(검색어)->정렬(오름/내림차순)->페이징(5개씩 자르기)->화면에 표시순서로 작동합니다.
- 무한 반복:
setInterval(tick, 3000)코드가 3초마다 모든 데이터(차트, 표, KPI)를 자동으로 갱신시켜 줍니다.
AntiGravity가 생성한 대시보드 페이지

이해를 돕기 위해 본인의 화면을 캡쳐하여 제공합니다. 본 이미지의 저작권은 블로그 주인 제로에게 있습니다.

이해를 돕기 위해 본인의 화면을 캡쳐하여 제공합니다.
본 이미지의 저작권은 블로그 주인 제로에게 있습니다.
AntiGravity 의 대표적인 특징은 다음과 같습니다.
✔ 실행만 되면 되는 단순 로직
✔ 코드 간결함
✔ 빠르게 UI만 만들고 싶을 때 적합
하지만
- 검색/정렬/페이징 중 일부가 누락되거나 오류 있음
- 차트 업데이트 로직이 불안정
- CSS 통일성 부족
실무 기준으로는 참고용 스케치 정도이긴 하지만 UI가 대체적으로 굉장히 세련되어 있습니다.
최근 가장 감각적인 UI 디자인을 보여주고 있습니다.
🧩 마무리하며
마지막 시간이었던 Part 10에서는 단순한 UI가 아닌 **기업 서비스 수준의 “완성형 대시보드”**를 구축했습니다.
✔ 실시간 KPI
✔ 실시간 차트(Line Chart)
✔ 실시간 테이블
✔ 검색
✔ 필터
✔ 정렬
✔ 페이징
✔ 반응형 스타일
그리고 GPT · Cursor · AntiGravity의 차이도 실무 기준으로 다시 한 번 비교했습니다.
어떠신가요. 이제 여러분들은 적절한 AI를 효율적으로 사용할 수 있고 또 그에 맞는 결과를 만들어 낼 수 있는 사람이 되었습니다.
지금까지 AI 3종 비교 및 AI로 반응형 페이지 만들기 시리즈였습니다.
감사합니다.
이전편 다시보기
- AI 코딩 시리즈 다시보기
[HTML/CSS 입문] AI로 로그인 페이지 만들어보기 (GPT vs Cursor vs AntiGravity) | Part 1
[HTML/CSS 입문] AI로 반응형 로그인 페이지 개선하기 (GPT vs Cursor vs AntiGravity) | Part 2
[HTML/CSS 입문] AI로 반응형 로그인 페이지 확장하기 (GPT vs Cursor vs AntiGravity) | Part 3
[HTML/CSS 입문] AI로 로그인 페이지 레이아웃(탭 메뉴) 만들어보기(GPT vs Cursor vs AntiGravity) | Part 4
[HTML/CSS 입문] AI로 반응형 UI 만들어보기 (GPT vs Cursor vs AntiGravity) – Part 5
[HTML/CSS 입문] AI로 만드는 헤더·푸터·레이아웃 비교(Gpt vs Cursor vs AntiGravity) | Part 6
[HTML/CSS 입문] AI로 로그인 이후 ‘대시보드 UI’ 만들기 (GPT vs Cursor vs AntiGravity) | Part 7
[HTML/CSS 입문] AI로 대시보드 차트 UI 만들기(GPT vs Cursor vs AntiGravity) | Part 8
[HTML/CSS 입문] AI로 실시간 데이터·검색/필터·테이블/페이징 UI 만들기 (GPT vs Cursor vs AntiGravity) | Part 9 - AI활용 가이드편 다시보기
[AI] 비전공자·초급 개발자를 위한 AI 활용 가이드
[AI] 입문자 및 비전공자를 위한 필수 AI(인공지능) 용어 소개 25선 - 그 외 관련 포스팅 다시보기
[입문] HTML/CSS란? (프론트엔드 1편) – AI 시대, 기본을 아는 사람의 경쟁력
[개발환경 입문] VS Code란? 입문자가 알아야 할 핵심 기능과 장점