나만의 페이스 만들기

HTML, CSS, JavaScript로 나만의 미터 페이스를 만드세요. 모든 페이스는 배터리부터 직접 보내는 숫자까지 모든 소스와 함께 쓸 수 있습니다.

빠른 시작

  1. 페이스 생성: 설정에서 페이스 생성…(Option-Command-N)을 클릭하세요. Multimeter가 버전 2 데이터를 이미 사용하는 기본 파일 폴더를 만듭니다.
  2. 파일 편집하기: Multimeter가 Finder에서 폴더를 보여 줍니다. 아무 코드 편집기에서나 파일을 수정하세요.
  3. 변경 사항 바로 보기: 저장할 때마다 Multimeter가 페이스를 다시 불러옵니다. 미터 추가…나 이 미터 사용자화…에서 여러 소스로 시험해 보세요.
  4. 공유하기: Finder에서 폴더를 압축하고 .zip의 이름을 .multimeter로 바꾼 다음 갤러리에 제출하세요. 누구나 가져오기…(Command-O)나 파일을 이중 클릭해 추가할 수 있습니다.

미터의 작동 방식

메뉴 막대의 미터는 페이스와 소스, 그리고 사용자의 설정으로 이루어집니다. 페이스는 여러분의 폴더입니다. Multimeter는 이 작은 웹 페이지를 높이 22포인트, 폭은 매니페스트의 width만큼인 투명한 뷰에 불러옵니다. 소스는 미터가 측정하는 대상으로, 사용자가 미터를 추가할 때 고릅니다.

무언가 바뀔 때마다 Multimeter는 window.meterData를 설정하고 meterUpdate 이벤트를 보냅니다. 페이스는 숫자가 어디서 오는지 몰라도 됩니다. level(0에서 1)을 그리고 formatted를 보여주면 CPU, 네트워크, 타이머, 배터리 모두에서 똑같이 동작합니다.

파일 구조

각 페이스는 다음을 담은 폴더입니다:

my-meter/
├── manifest.json    # name, size, which sources it's made for
├── index.html       # the page Multimeter loads
├── styles.css
└── meter.js         # listens for meterUpdate and draws

manifest.json

매니페스트는 페이스를 설명하고, 폭과 용도를 Multimeter에 알려줍니다:

{
  "name": "My Meter",
  "description": "What it shows, in one line.",
  "version": "1.0.0",
  "author": "Your Name",
  "apiVersion": 2,
  "sources": ["any"],
  "defaultSource": "cpu",
  "width": 60,
  "minWidth": 40,
  "maxWidth": 100,
  "supportsLightMode": true,
  "supportsDarkMode": true
}
키유형의미
namestring페이스 목록과 갤러리에 표시됩니다.
descriptionstring페이스가 무엇을 보여주는지 한 줄로.
versionstring버전 번호. 예: 1.0.0.
authorstring갤러리에 표시되는 이름.
apiVersion2이 페이지에서 설명하는 데이터를 쓰려면 2로 설정하세요.
sourcesstring[]어떤 소스든 그릴 수 있는 페이스는 ["any"], 아니면 ["battery"]나 ["battery", "countdown", "timer"] 같은 소스 ID 목록. Multimeter는 그 소스에서만 이 페이스를 보여줍니다.
defaultSourcestring누군가 이 페이스를 처음 추가할 때 선택되는 소스. 예: "cpu".
widthnumber포인트 단위의 폭. 높이는 항상 22입니다.
minWidth / maxWidthnumber사용자화에서 사용자가 정할 수 있는 최소 폭과 최대 폭.
supportsLightMode / supportsDarkModeboolean페이스가 밝은 메뉴 막대와 어두운 메뉴 막대에 맞게 디자인되었는지 여부.

apiVersion과 sources가 없는 매니페스트는 배터리 중심이지만 어떤 소스와도 동작하는 페이스로 취급되므로, 이전 버전용으로 만든 미터도 계속 동작합니다.

미터 데이터

Multimeter는 현재 데이터를 window.meterData에 넣고, 바뀔 때마다 다시 보냅니다. 담긴 내용은 다음과 같습니다(API 버전 2):

window.meterData = {
  apiVersion: 2,
  source: "cpu",            // what this meter measures
  sourceName: "CPU",        // that source's name, translated
  label: "CPU",             // the meter's label ("" if the person cleared it)
  value: 23.4,              // raw value in `unit` (null if unavailable)
  min: 0, max: 100,         // the range `level` is measured against
  level: 0.234,             // value placed in the range, 0 to 1
  formatted: "23%",         // ready to show, unit included, in the person's language
  unit: "%",                // "%", "W", "B/s", "rpm", "s", or your own
  state: "normal",          // "normal", "warning" or "critical"
  tint: null,               // "#30D158" if the person picked a color
  history: [0.21, 0.25],    // up to 60 recent levels, oldest first
  warningLevel: 0.7,        // alert levels on the same 0 to 1 scale (or null)
  criticalLevel: 0.9,
  alertDirection: "above",  // "above" or "below" (or null)
  available: true,          // false when the source can't be read right now
  colorScheme: "dark",      // the menu bar: "light" or "dark"
  reduceMotion: false,      // System Settings > Accessibility > Reduce motion
  strings: { timeLeft: "left" },
  battery: {                // always the Mac's battery, whatever the source
    level: 0.82, percentage: 82, isCharging: false, isPluggedIn: false,
    hasBattery: true, timeRemaining: 312, timeToFullCharge: null,
    formattedTime: "5:12", cycleCount: 212, health: 0.94, temperature: 30.5
  },
  // Version 1 fields, so battery meters keep working with any source:
  percentage: 23, isCharging: false, isPluggedIn: false,
  timeRemaining: null, timeToFullCharge: null, formattedTime: null,
  cycleCount: 212, health: 0.94, temperature: 30.5, hasBattery: true
}
필드의미
source소스 ID(아래 표 참고). sourceName은 번역된 이름입니다.
label미터의 레이블. 예: CPU, DOWN. 사용자가 이름을 바꾸거나 지울 수 있으므로 빈 문자열일 수도 있습니다.
valueunit 단위의 원래 값, 또는 null. min, max와 함께 보면 전체 그림이 나옵니다.
level값을 0에서 1 범위에 맞춘 것(범위를 벗어나지 않음). 대부분의 페이스는 이것만 있으면 됩니다.
formatted단위를 포함해 사용자의 언어에 맞게 형식화한 텍스트: 23%, 12.4 W, 1.2 MB/s, 4:12.
unit%, W, B/s, rpm, s(타이머와 카운트다운), 빈 문자열, 또는 사용자가 정한 단위.
state사용자가 정한 알림 수준에 따른 normal, warning, critical. 이 값에 맞춰 색을 바꾸세요.
tint사용자가 고른 #30D158 같은 색상, 또는 페이스 자체 색상을 쓰려면 null.
history그래프용으로 최근 레벨을 최대 60개(0에서 1), 오래된 것부터 담습니다.
warningLevelwarningLevel과 criticalLevel은 알림 수준을 level과 같은 0에서 1 척도로 나타내고, alertDirection은 그보다 높을 때(above) 알림이 울리는지 낮을 때(below) 울리는지 알려줍니다. 미터에 알림이 없으면 세 값 모두 null입니다.
available지금 소스를 읽을 수 없으면 false: 배터리가 없거나, 액세서리가 꺼져 있거나, 웹 주소에 접속이 안 될 때. 대시를 보여주세요.
colorScheme메뉴 막대의 화면 모드, light 또는 dark. 시스템 설정뿐 아니라 배경화면에 따라 바뀝니다.
reduceMotion동작 줄이기가 켜져 있으면 true. 애니메이션을 건너뛰세요.
batteryMac의 배터리. 미터가 무엇을 측정하든 항상 있습니다: 충전량, 충전 중 여부, 전원 연결 여부, 남은 시간, 완충까지 시간, 충전 횟수, 성능 상태, 온도.

업데이트 받기

window.meterData에 이미 있는 데이터로 한 번 그리고, meterUpdate가 올 때마다 다시 그리세요. 데이터를 받을 준비가 되면 window.meterReady = true로 설정하세요.

function render(data) {
  // draw the meter from data.level, data.formatted, data.state ...
}

window.addEventListener('meterUpdate', (e) => render(e.detail));
render(window.meterData || { level: 0.5, formatted: '50%', state: 'normal' });
window.meterReady = true;

소스

미터가 가질 수 있는 소스는 다음과 같습니다. 페이스는 source에서 ID를 받고 formatted는 이미 작성되어 오므로, 소스마다 다르게 처리할 일은 거의 없습니다.

소스sourceunitformatted 예시
배터리battery"%"82%
Bluetooth 액세서리bluetooth"%"64%
전력 소비power"W"12.4 W
CPUcpu"%"23%
메모리memory"%"71%
디스크 공간disk"%"58%
열 압력thermal""정상, 약간 높음, 높음, 위험
팬 속도*fan"rpm"2,400 rpm
네트워크network"B/s"1.2 MB/s, 340 KB/s
타이머timer"s"24:51
카운트다운countdown"s"3d 4h, 4:12, 12:05
기간 진행률progress"%"63%
보낸 값push""42
웹 주소url""1,284
파일file""12
스크립트*script""412

* 팬 속도와 스크립트는 Multimeter의 직접 다운로드 버전이 필요합니다. Mac App Store 버전은 팬 센서를 읽거나 스크립트를 실행할 수 없으니, 대신 보낸 값이나 파일을 쓰세요.

퍼센트는 항상 0에서 100입니다. 전력, 네트워크, 팬 속도는 사용자가 범위를 정하지 않으면 최근 최고값에 맞춰 눈금이 바뀝니다. 열 압력은 4단계라 level은 0, 0.33, 0.67, 1 중 하나입니다. 타이머와 카운트다운에서 level은 남은 시간의 비율이고 value는 남은 초입니다.

알림과 색상

각 미터에는 사용자가 정하는 경고 수준과 위험 수준이 있습니다(기본값은 배터리 20%와 10% 이하, CPU 70%와 90% 이상, 디스크 85%와 95% 이상). state는 Multimeter가 계산해 줍니다. 페이스는 normal일 때 차분하게, warning과 critical로 갈수록 눈에 띄게 보여야 하고, 사용자가 색상을 골랐다면 tint를 써야 합니다:

document.body.classList.remove('warning', 'critical');
if (data.state !== 'normal') document.body.classList.add(data.state);

// The person's color wins over the face's own
if (data.tint) document.body.style.setProperty('--accent', data.tint);
else document.body.style.removeProperty('--accent');
body.warning  { --accent: #ff9f0a; }
body.critical { --accent: #ff453a; }

빨간 끝이 있는 다이얼처럼 눈금에 알림 구간을 그리고 싶나요? warningLevel, criticalLevel, alertDirection을 쓰세요.

기록

history에는 최근 레벨이 최대 60개, 오래된 것부터, 이미 0에서 1 척도로 담겨 있습니다. 끝에 현재 level을 붙이면 그래프가 됩니다. 아래의 기록 그래프 예제가 이렇게 그립니다.

배터리 세부 정보

배터리는 여전히 특별합니다. data.battery는 미터가 무엇을 측정하든 항상 Mac의 배터리이므로, 어떤 페이스든 충전 번개 표시를 보여줄 수 있습니다. 배터리 전용 페이스는 매니페스트에 "sources": ["battery"]라고 적으세요. timeRemaining과 timeToFullCharge는 분 단위이며, macOS가 아직 계산 중이면 null입니다.

// A battery-only face: "sources": ["battery"] in manifest.json
const b = data.battery;
bolt.hidden = !b.isCharging;
time.textContent = b.isCharging
  ? (b.timeToFullCharge ? `${b.timeToFullCharge} min to full` : '')
  : (b.formattedTime ?? '');

버전 1용으로 작성된 페이스는 최상위의 percentage, isCharging, formattedTime을 읽습니다. 이 값들은 지금도 전송됩니다. 배터리 소스에서는 배터리 자체의 값이고, 다른 소스에서는 isCharging이 false, formattedTime은 타이머나 카운트다운의 남은 시간(h:mm)이며, 그 외에는 null입니다.

내 데이터

네 가지 소스는 직접 제공하는 숫자를 보여줍니다: 보낸 값, 웹 주소, 파일, 스크립트. 페이스는 이것들을 다른 소스와 똑같이 다룹니다.

링크로 값 보내기

multimeter://set 링크를 어디서든 여세요: Terminal, 셸 스크립트, 단축어(URL 열기), Raycast, cron 작업, 빌드 스크립트. 보낸 값 소스와 같은 키로 미터를 추가해 두면 링크가 열리는 순간 업데이트됩니다. open -g를 쓰면 Multimeter가 백그라운드에 머뭅니다.

# Set a value (the meter shows the "deploys" key)
open -g "multimeter://set?key=deploys&value=42"

# Range, label, unit and your own text ride along
open -g "multimeter://set?key=coffee&value=3&min=0&max=5&label=Coffee&unit=cups"
open -g "multimeter://set?key=build&value=0.8&max=1&formatted=80%25%20done"
매개변수의미
key어느 미터에 보낼지. 키는 대소문자를 구분하지 않습니다.
value숫자. 필수입니다.
min / max범위. 이걸로 level이 올바르게 나옵니다. 기본값은 0에서 100.
label미터 레이블 텍스트.
unit숫자 뒤에 붙는 단위. 예: cups. 퍼센트는 %를 쓰세요.
formatted숫자 대신 formatted에 쓸 직접 정한 텍스트.

보낸 값은 Mac App Store 버전에서도 동작합니다. Multimeter는 키마다 마지막 값을 기억합니다.

웹 주소, 파일, 스크립트

세 가지 모두 같은 출력을 읽습니다: 일반 숫자, 또는 value와 추가 항목을 담은 JSON. value를 제외한 모든 항목은 선택 사항입니다.

42
42.5
42%

{"value": 42, "min": 0, "max": 100, "label": "Deploys", "unit": "", "formatted": "42 left"}

API 응답처럼 더 큰 JSON을 가리키나요? 미터에 키 경로를 지정하면 숫자를 골라냅니다:

{"data": {"count": 17}}                → key path  data.count
{"items": [{"value": 9.5}]}             → key path  items.0.value
  • 웹 주소: 정한 간격으로 가져옵니다. 가장 짧게는 30초마다(바꾸지 않으면 5분마다).
  • 파일: 파일이 바뀔 때마다 다시 읽습니다. 파일을 쓸 수 있는 것이라면 무엇이든 편리하게 쓸 수 있습니다.
  • 스크립트: 정한 간격으로 zsh로 실행하는 셸 명령(바꾸지 않으면 30초마다). Multimeter는 출력된 숫자를 보여줍니다. Homebrew 폴더는 PATH에 들어 있으며, 10초보다 오래 걸리는 명령은 중단됩니다.
# Open pull requests waiting on you (direct download version)
gh pr list --search "review-requested:@me" --json number --jq length

# Free memory pressure as a percentage
memory_pressure | awk '/percentage/ {print 100 - $5}'

Mac App Store 버전은 웹 주소와 파일은 쓸 수 있지만 스크립트는 쓸 수 없습니다. 그곳에서는 스크립트가 링크를 보내거나 파일을 쓰게 하세요.

밝은 메뉴 막대와 어두운 메뉴 막대

메뉴 막대는 배경화면에 따라 밝거나 어둡습니다. Multimeter는 body에 light-mode 또는 dark-mode를 추가하고, 데이터에 colorScheme을 설정하며, prefers-color-scheme도 메뉴 막대에 맞춥니다. 편한 방법을 쓰세요:

/* Dark menu bar (the default) */
body { color: white; }

/* Light menu bar */
body.light-mode { color: black; }

/* Same thing with the media query, which follows the menu bar too */
@media (prefers-color-scheme: light) {
  body { color: black; }
}

동작 줄이기

동작 줄이기가 켜져 있으면 reduceMotion이 true가 되고 prefers-reduced-motion도 일치합니다. 미터는 읽기 쉽게 유지하고 튀기, 깜박임, 쓸기 같은 움직임은 빼세요:

body.reduce-motion * { transition: none !important; animation: none !important; }

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
}

완성된 예제

완성된 페이스 두 개가 Multimeter가 보내는 것과 같은 데이터로 이 페이지에서 실시간으로 동작합니다. 파일을 폴더에 복사해 가져오세요.

페이스 생성 기본 페이스

페이스 생성…이 만드는 그대로입니다. 레이블, 막대, 형식화된 값에 API를 설명하는 주석이 붙어 있습니다. state에 따라 색을 바꾸고, tint를 따르며, 메뉴 막대의 화면 모드를 따라갑니다.

starter/manifest.json

{
  "name": "My Face",
  "description": "A label, a bar and the value. Works with any source.",
  "version": "1.0.0",
  "author": "Your Name",
  "apiVersion": 2,
  "sources": ["any"],
  "defaultSource": "cpu",
  "width": 70,
  "minWidth": 50,
  "maxWidth": 120,
  "supportsLightMode": true,
  "supportsDarkMode": true
}

starter/index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <div id="meter">
        <span id="label"></span>
        <span id="track"><span id="fill"></span></span>
        <span id="value">--</span>
    </div>
    <script src="meter.js"></script>
</body>
</html>

starter/styles.css

* { margin: 0; padding: 0; box-sizing: border-box; }

/* The meter is 22 points tall and as wide as manifest.json says. */
body {
    font: 600 11px/1 -apple-system, BlinkMacSystemFont, sans-serif;
    font-variant-numeric: tabular-nums;
    color: white;                 /* dark menu bar */
    background: transparent;
    height: 22px;
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
    --accent: #30d158;            /* replaced by the meter's color, if one is set */
}

/* Light menu bar: the system setting, or the preview's light mode. */
#track { background: rgba(255, 255, 255, 0.25); }
@media (prefers-color-scheme: light) { body { color: black; } #track { background: rgba(0, 0, 0, 0.18); } }
body.light-mode { color: black; }
body.light-mode #track { background: rgba(0, 0, 0, 0.18); }
body.dark-mode { color: white; }
body.dark-mode #track { background: rgba(255, 255, 255, 0.25); }

#meter { display: flex; align-items: center; gap: 4px; }
#label { font-size: 8px; letter-spacing: 0.04em; opacity: 0.7; }
#track { width: 18px; height: 6px; border-radius: 3px; position: relative; overflow: hidden; }
#fill { position: absolute; inset: 0 auto 0 0; width: 0; background: var(--accent); border-radius: 3px; transition: width 0.4s ease; }

/* state is "normal", "warning" or "critical" (from the meter's alert levels). */
body.warning { --accent: #ffd60a; }
body.critical { --accent: #ff453a; }

@media (prefers-reduced-motion: reduce) { #fill { transition: none; } }

starter/meter.js

// Multimeter meter API, version 2. The app sets window.meterData and fires
// a 'meterUpdate' event whenever anything changes. The fields you'll use most:
//
//   level        0.0 to 1.0, the value within the meter's range
//   formatted    ready-to-show text with its unit: "23%", "1.2 MB/s", "4:12"
//   label        short label ("CPU"), may be "" if hidden
//   value, unit, min, max   the raw number and its range
//   state        "normal", "warning" or "critical"
//   tint         the color picked in Customize ("#30D158"), or null
//   history      up to 60 recent levels (0 to 1), oldest first, for graphs
//   source       what it measures: "battery", "cpu", "memory", "network", "timer"...
//   available    false when there's nothing to read (no battery, URL failing)
//   battery      the Mac's battery, always: level, isCharging, isPluggedIn,
//                hasBattery, timeRemaining, timeToFullCharge, health...
//   isCharging   true only when this meter shows the battery and it's charging
//   colorScheme  "light" or "dark"; reduceMotion  true if animations should stop
//
// Full reference: https://multimeterapp.com/creating-meters

class Meter {
    constructor() {
        this.label = document.getElementById('label');
        this.fill = document.getElementById('fill');
        this.value = document.getElementById('value');
        window.addEventListener('meterUpdate', (e) => this.render(e.detail));
        if (window.meterData) this.render(window.meterData);
        window.meterReady = true;
    }

    render(data) {
        const level = Math.max(0, Math.min(1, data.level || 0));
        this.label.textContent = data.label || '';
        this.label.style.display = data.label ? '' : 'none';
        this.value.textContent = data.available === false ? '\u2013' : (data.formatted || Math.round(level * 100) + '%');
        this.fill.style.width = (level * 100) + '%';

        document.body.classList.toggle('warning', data.state === 'warning');
        document.body.classList.toggle('critical', data.state === 'critical');
        if (data.tint) document.body.style.setProperty('--accent', data.tint);
        else document.body.style.removeProperty('--accent');
    }
}

if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', () => new Meter());
} else {
    new Meter();
}

기록 그래프

history로 최근 1분간의 선 그래프를 화면의 실제 해상도로 canvas에 그립니다.

history-graph/manifest.json

{
  "name": "History Graph",
  "description": "The last minute as a little line graph.",
  "version": "1.0.0",
  "author": "Your Name",
  "apiVersion": 2,
  "sources": ["any"],
  "defaultSource": "network",
  "width": 44,
  "minWidth": 30,
  "maxWidth": 80,
  "supportsLightMode": true,
  "supportsDarkMode": true
}

history-graph/index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <style>
    html, body { margin: 0; height: 22px; background: transparent; overflow: hidden; }
    canvas { display: block; width: 100%; height: 22px; }
  </style>
</head>
<body>
  <canvas id="graph"></canvas>
  <script src="meter.js"></script>
</body>
</html>

history-graph/meter.js

const canvas = document.getElementById('graph');
const ctx = canvas.getContext('2d');

function render(data) {
  // Draw at the screen's real resolution so the line stays sharp
  const scale = window.devicePixelRatio || 1;
  const w = canvas.clientWidth, h = 22;
  canvas.width = w * scale;
  canvas.height = h * scale;
  ctx.setTransform(scale, 0, 0, scale, 0, 0);
  ctx.clearRect(0, 0, w, h);

  const points = (data.history || []).concat(data.level ?? 0);
  if (points.length < 2) return;

  const dark = data.colorScheme !== 'light';
  const color = data.tint
    || (data.state === 'critical' ? '#ff453a' : data.state === 'warning' ? '#ff9f0a' : dark ? '#fff' : '#1d1d1f');
  const x = (i) => (i / (points.length - 1)) * (w - 2) + 1;
  const y = (v) => h - 3 - v * (h - 6);

  // Soft fill under the line, then the line
  ctx.beginPath();
  points.forEach((v, i) => (i ? ctx.lineTo(x(i), y(v)) : ctx.moveTo(x(i), y(v))));
  ctx.lineTo(x(points.length - 1), h);
  ctx.lineTo(x(0), h);
  ctx.closePath();
  ctx.globalAlpha = 0.22;
  ctx.fillStyle = color;
  ctx.fill();

  ctx.globalAlpha = 1;
  ctx.beginPath();
  points.forEach((v, i) => (i ? ctx.lineTo(x(i), y(v)) : ctx.moveTo(x(i), y(v))));
  ctx.strokeStyle = color;
  ctx.lineWidth = 1.5;
  ctx.lineJoin = 'round';
  ctx.stroke();
}

window.addEventListener('meterUpdate', (e) => render(e.detail));
render(window.meterData || { history: [0.2, 0.4, 0.3, 0.6, 0.5], level: 0.7 });
window.meterReady = true;

테스트 팁

  • 모든 요소를 높이 22포인트 안에 담고, minWidth와 maxWidth에서도 페이스를 확인하세요.
  • 시스템 서체(-apple-system)와 font-variant-numeric: tabular-nums를 쓰면 숫자가 흔들리지 않습니다.
  • 미터 추가…와 이 미터 사용자화…에서 여러 소스로 페이스를 시험해 보세요. 백분율, 네트워크 속도, 타이머, 사용할 수 없는 소스 등.
  • 밝은 메뉴 막대와 어두운 메뉴 막대, 색조, 경고와 위험 상태를 확인하세요.
  • JavaScript 오류는 Console.app에 나타납니다. index.html을 Safari에서 열고 콘솔에서 데이터를 보낼 수도 있습니다:
  • 페이스가 바로 그려지도록 파일은 작게 유지하고 네트워크 요청은 피하세요.
// Paste in Safari's console with your index.html open to try a value
window.meterData = { level: 0.92, formatted: '92%', label: 'CPU', state: 'critical',
  available: true, colorScheme: 'dark', history: [] };
window.dispatchEvent(new CustomEvent('meterUpdate', { detail: window.meterData }));

만들어 볼까요?

$19.99 한 번 결제. 구독 없음. 30일 환불.

$19.99에 구입