[HTML/CSS/JS 입문] AI로 만드는 ‘완성형 실무 대시보드’ (GPT vs Cursor vs AntiGravity) | Part 10

지난 시간에 우리는 AI 3총사를 이용해서 실시간 데이터를 활용한 페이징 UI를 만들어 보았습니다. 그리고 3개의 AI에 같은 프롬프트를 입력해서 나오는 결과를 비교해 보았었습니다. 어떠신가요? 이제 조금 알 것 같지 않나요? AI를 어떻게 써야 하는지. 어떻게 효율을 낼 수 있는지 말이죠.

저도 AI 비교 시리즈를 진행하면서 정말 많은 부분을 또 다시 느끼게 되었습니다. 저 스스로가 어떤 AI가 잘 맞는지도 다시 한번 체감할 수 있었고 또 어떤 상황에 어떤 AI를 쓰는게 맞는가 라는 부분에 대해서도 더 디테일 한 기준을 잡게 되었습니다. 여러분들은 어떠셨을지 모르겠습니다.

이런 이야기를 하는 이유는 이제 이번 시리즈의 마지막으로 실무에서 바로 쓰는 수준의 ‘완성형 대시보드’ 편이기 때문입니다.

앞서 Part 1 부터 진행해 오면서 처음 HTML 페이지 만들기부터 Part 8~9까지는 검색·필터·페이징·정렬·실시간 데이터 UI까지 기본 기능을 완성했습니다. 이제 여러분은 왠만한 프론트엔드 개발자 혹은 AI 부업으로 랜딩페이지를 만들어 납품할 정도의 기본적인 실력은 충분히 갖추게 되었습니다.

Part 10에서는 여기에 차트(Chart.js) + KPI 박스 + 실시간 데이터 연동까지 적용해 기업/서비스에서 바로 쓰는 수준의 대시보드 화면을 완성합니다. 이번 시간에도 함께 나아가 봅시다!

GPT vs Cursor vs AntiGravity 를 AI를 활용하여 시각화 한 이미지 자료입니다. 
본 이미지의 저작권은 블로그 주인 제로에게 있습니다.
GPT vs Cursor vs AntiGravity 를 AI를 활용하여 시각화 한 이미지 자료입니다.
본 이미지의 저작권은 블로그 주인 제로에게 있습니다.

외부링크(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에 입력한 프롬프트(무료버전)
이해를 돕기 위해 직접 찍은 스크린샷을 제공합니다. 본 이미지의 저작권은 블로그 주인 제로에게 있습니다.
GPT에 입력한 프롬프트(무료버전)
이해를 돕기 위해 직접 찍은 스크린샷을 제공합니다. 본 이미지의 저작권은 블로그 주인 제로에게 있습니다.
Cursor에 입력한 프롬프트(무료버전)
이해를 돕기 위해 직접 찍은 스크린샷을 제공합니다. 본 이미지의 저작권은 블로그 주인 제로에게 있습니다.
Cursor에 입력한 프롬프트(무료버전)
이해를 돕기 위해 직접 찍은 스크린샷을 제공합니다. 본 이미지의 저작권은 블로그 주인 제로에게 있습니다.
AntiGravity에 입력한 프롬프트(무료버전)
이해를 돕기 위해 직접 찍은 스크린샷을 제공합니다. 본 이미지의 저작권은 블로그 주인 제로에게 있습니다.
AntiGravity에 입력한 프롬프트(무료버전)
이해를 돕기 위해 직접 찍은 스크린샷을 제공합니다. 본 이미지의 저작권은 블로그 주인 제로에게 있습니다.

자 이제 결과를 알아 볼까요?


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가 생성한 실시간 대시보드 페이지.
이해를 돕기 위해 본인의 화면을 캡쳐하여 제공합니다. 본 이미지의 저작권은 블로그 주인 제로에게 있습니다.
GPT가 생성한 실시간 대시보드 페이지.
이해를 돕기 위해 본인의 화면을 캡쳐하여 제공합니다. 본 이미지의 저작권은 블로그 주인 제로에게 있습니다.
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가 생성한 실시간 대시보드 페이지.
이해를 돕기 위해 본인의 화면을 캡쳐하여 제공합니다. 본 이미지의 저작권은 블로그 주인 제로에게 있습니다.
Cursor가 생성한 실시간 대시보드 페이지.
이해를 돕기 위해 본인의 화면을 캡쳐하여 제공합니다. 본 이미지의 저작권은 블로그 주인 제로에게 있습니다.
Cursor가 생성한 실시간 대시보드 모바일 페이지.
이해를 돕기 위해 본인의 화면을 캡쳐하여 제공합니다. 
본 이미지의 저작권은 블로그 주인 제로에게 있습니다.
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>< Prev</button>
                        <span id="pageInfo">Page 1 of 1</span>
                        <button id="nextBtn" disabled>Next ></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가 생성한 실시간 대시보드 페이지.
이해를 돕기 위해 본인의 화면을 캡쳐하여 제공합니다. 본 이미지의 저작권은 블로그 주인 제로에게 있습니다.
AntiGravity가 생성한 실시간 대시보드 페이지.
이해를 돕기 위해 본인의 화면을 캡쳐하여 제공합니다. 본 이미지의 저작권은 블로그 주인 제로에게 있습니다.
AntiGravity가 생성한 실시간 대시보드 모바일 페이지.
이해를 돕기 위해 본인의 화면을 캡쳐하여 제공합니다. 
본 이미지의 저작권은 블로그 주인 제로에게 있습니다.
AntiGravity가 생성한 실시간 대시보드 모바일 페이지.
이해를 돕기 위해 본인의 화면을 캡쳐하여 제공합니다.
본 이미지의 저작권은 블로그 주인 제로에게 있습니다.

AntiGravity 의 대표적인 특징은 다음과 같습니다.

✔ 실행만 되면 되는 단순 로직
✔ 코드 간결함
✔ 빠르게 UI만 만들고 싶을 때 적합

하지만

  • 검색/정렬/페이징 중 일부가 누락되거나 오류 있음
  • 차트 업데이트 로직이 불안정
  • CSS 통일성 부족

실무 기준으로는 참고용 스케치 정도이긴 하지만 UI가 대체적으로 굉장히 세련되어 있습니다.
최근 가장 감각적인 UI 디자인을 보여주고 있습니다.


🧩 마무리하며

마지막 시간이었던 Part 10에서는 단순한 UI가 아닌 **기업 서비스 수준의 “완성형 대시보드”**를 구축했습니다.

✔ 실시간 KPI
✔ 실시간 차트(Line Chart)
✔ 실시간 테이블
✔ 검색
✔ 필터
✔ 정렬
✔ 페이징
✔ 반응형 스타일

그리고 GPT · Cursor · AntiGravity의 차이도 실무 기준으로 다시 한 번 비교했습니다.

어떠신가요. 이제 여러분들은 적절한 AI를 효율적으로 사용할 수 있고 또 그에 맞는 결과를 만들어 낼 수 있는 사람이 되었습니다.
지금까지 AI 3종 비교 및 AI로 반응형 페이지 만들기 시리즈였습니다.

감사합니다.

이전편 다시보기

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

위로 스크롤