---
code: "INT-23"
id: "c-counter"
tab: "dynamic-2"
kind: "interactive"
title: "Counter Tiles · 카운터 타일"
library: "DOM"
deps: ["dom"]
load: "med"
pasteKind: "custom-mount"
family: "kpi-dashboard"
dataShape: "animated counters in tile layout"
tasks: ["explore data", "filter subsets", "monitor change", "surface headline KPI", "surface headline metrics"]
tags: ["interactive", "exploration", "linked view", "kpi", "progress", "counter", "tiles", "카운터", "타일"]
gallery: "https://graph.heal7.com/#INT-23"
js: "/sku/INT-23.js"
---

# [INT-23] Counter Tiles · 카운터 타일

> AI 붙여넣기 카드. 갤러리 런타임(`main.js`)이 아니라 이 파일을 가져와라.

## 언제 가져가나
0부터 목표값까지 부드럽게 카운트업 — 대시보드 헤드라인 KPI.

**응용** 가입자 수, 매출 합계, 처리 건수 강조.

## 데이터 모양
- family: `kpi-dashboard`
- dataShape: animated counters in tile layout
- tasks: explore data, filter subsets, monitor change, surface headline KPI, surface headline metrics
- tags: interactive, exploration, linked view, kpi, progress, counter, tiles, 카운터, 타일


## 의존
dom · load `med` · library `DOM`

## 붙여넣는 법
1. 라이브러리 없이 host 엘리먼트에 직접 그린다
2. 함수 전체를 가져간다
3. `getElementById('원본id')` 만 대상 노드로 바꾼다

```js
const T = {
  "bg": "transparent",
  "fg": "#ece8f5",
  "sub": "#a59bbd",
  "grid": "rgba(255,255,255,.08)",
  "gold": "#e8c069",
  "royal": "#6b8cff",
  "rose": "#e07a9f",
  "teal": "#6dd2c4",
  "plum": "#b487e8"
};
const isDark = () => document.documentElement.getAttribute('data-theme') !== 'light';
const host = document.querySelector('#chart');
const factory = function(){
  const host = document.getElementById('c-counter'); if(!host) return;
  const tiles = [
    {t:'신규 가입',  v:1284, suf:''},
    {t:'일일 활성', v:8742, suf:''},
    {t:'매출(만원)', v:6520, suf:''},
    {t:'피드백',     v:312,  suf:''}
  ];
  host.innerHTML = '<div style="display:grid;grid-template-columns:repeat(2,1fr);grid-template-rows:repeat(2,1fr);gap:8px;height:100%;padding:4px 0">' +
    tiles.map(x=>`<div style="background:rgba(255,255,255,.04);border:1px solid var(--line2);border-radius:10px;padding:10px 14px;display:flex;flex-direction:column;justify-content:center;gap:4px">
      <div style="font-size:10px;color:var(--sub);letter-spacing:.04em">${x.t}</div>
      <div class="cnt" data-target="${x.v}" style="font-size:22px;font-weight:700;color:var(--gold);font-feature-settings:'tnum'">0</div>
    </div>`).join('') + '</div>';
  function run(){
    host.querySelectorAll('.cnt').forEach(el=>{
      const tg = +el.dataset.target;
      const dur = 1800, start = performance.now();
      function tick(now){
        const t = Math.min(1, (now-start)/dur);
        const eased = 1 - Math.pow(1-t, 3);
        el.textContent = Math.round(tg*eased).toLocaleString();
        if(t<1) requestAnimationFrame(tick);
      }
      requestAnimationFrame(tick);
    });
  }
  run();
  charts.push({resize: run});  // 탭 전환 시 재카운트
};
factory();
```

## 원본 factory (갤러리 정본 발췌)
- file: `frontend/js/graph-gallery/main.js`
- lines: 2303-2331
- gallery: https://graph.heal7.com/#INT-23
- raw js: https://graph.heal7.com/sku/INT-23.js

```js
function(){
  const host = document.getElementById('c-counter'); if(!host) return;
  const tiles = [
    {t:'신규 가입',  v:1284, suf:''},
    {t:'일일 활성', v:8742, suf:''},
    {t:'매출(만원)', v:6520, suf:''},
    {t:'피드백',     v:312,  suf:''}
  ];
  host.innerHTML = '<div style="display:grid;grid-template-columns:repeat(2,1fr);grid-template-rows:repeat(2,1fr);gap:8px;height:100%;padding:4px 0">' +
    tiles.map(x=>`<div style="background:rgba(255,255,255,.04);border:1px solid var(--line2);border-radius:10px;padding:10px 14px;display:flex;flex-direction:column;justify-content:center;gap:4px">
      <div style="font-size:10px;color:var(--sub);letter-spacing:.04em">${x.t}</div>
      <div class="cnt" data-target="${x.v}" style="font-size:22px;font-weight:700;color:var(--gold);font-feature-settings:'tnum'">0</div>
    </div>`).join('') + '</div>';
  function run(){
    host.querySelectorAll('.cnt').forEach(el=>{
      const tg = +el.dataset.target;
      const dur = 1800, start = performance.now();
      function tick(now){
        const t = Math.min(1, (now-start)/dur);
        const eased = 1 - Math.pow(1-t, 3);
        el.textContent = Math.round(tg*eased).toLocaleString();
        if(t<1) requestAnimationFrame(tick);
      }
      requestAnimationFrame(tick);
    });
  }
  run();
  charts.push({resize: run});  // 탭 전환 시 재카운트
}
```

