@makeitnow/jumpitnow 0.1.0-alpha.3 → 0.1.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +9 -5
- package/dist/cjs/internal/gateway.d.ts +1 -1
- package/dist/cjs/internal/gateway.js +1 -1
- package/dist/esm/internal/gateway.d.ts +1 -1
- package/dist/esm/internal/gateway.js +1 -1
- package/docs/API.md +1 -1
- package/docs/index.html +661 -25
- package/examples/browser/index.html +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# 변경 안내
|
|
2
2
|
|
|
3
|
+
## 0.1.0-alpha.4 · 2026-10-11
|
|
4
|
+
|
|
5
|
+
- 단일 HTML로 읽고 공유할 수 있는 SDK & API 가이드를 개편했습니다.
|
|
6
|
+
- 연결 관계도, 단계별 함수 호출, 검색 가능한 API 18개, 운동 모드별 코드, 측정값 해석과 저장 흐름을 시각적으로 설명합니다.
|
|
7
|
+
- AI 코딩 요청문·예제 복사, 모바일 목차와 인쇄용 화면을 제공합니다.
|
|
8
|
+
- npm 설치 파일에 `docs/index.html`을 함께 배포합니다. 공개 함수와 운동 데이터 형식은 alpha.3과 같습니다.
|
|
9
|
+
|
|
3
10
|
## 0.1.0-alpha.3 · 2026-10-10
|
|
4
11
|
|
|
5
12
|
- 버전 변경 시 SDK와 현재 사용 안내의 버전을 함께 갱신합니다.
|
package/README.md
CHANGED
|
@@ -8,7 +8,11 @@ Jumpitnow 게이트웨이와 스마트 줄넘기를 JavaScript 함수로 사용
|
|
|
8
8
|
|
|
9
9
|
이 README를 AI 코딩 도구에 전달하고 만들고 싶은 서비스를 설명하세요. 아래 요청문과 공개 함수로 개발을 시작할 수 있습니다.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
시각적으로 살펴보려면 [SDK & API HTML 가이드](docs/index.html)를 브라우저에서 여세요. 연결 흐름도, 검색 가능한 API 설명, 데이터 해석 예시와 복사 가능한 코드를 한 문서에 담았습니다. `docs/index.html`은 스타일·그림·동작을 포함한 단일 파일이라 별도로 전달할 수 있습니다.
|
|
12
|
+
|
|
13
|
+
npm 설치 후 가이드 위치는 `node_modules/@makeitnow/jumpitnow/docs/index.html`입니다. 이 파일을 복사해 공유하거나 프로젝트의 정적 파일 서버에서 열 수 있습니다. npm 웹사이트는 HTML을 가이드 화면으로 표시하지 않습니다.
|
|
14
|
+
|
|
15
|
+
> **현재 버전: `0.1.0-alpha.4`**
|
|
12
16
|
>
|
|
13
17
|
> 개발 확인용입니다. 실제 장비 검증은 아직 진행하지 않았습니다. npm 공개가 완료된 버전 또는 전달받은 `.tgz` 설치 파일로 시작하세요. 가상 장비에서 동작한 결과가 실제 장비 호환성을 보장하지는 않습니다.
|
|
14
18
|
|
|
@@ -93,17 +97,17 @@ JavaScript 또는 TypeScript 웹 프로젝트에 설치합니다. 패키지 설
|
|
|
93
97
|
npm 공개가 완료된 버전은 앱 프로젝트 폴더에서 다음과 같이 설치합니다.
|
|
94
98
|
|
|
95
99
|
```sh
|
|
96
|
-
npm install --save-exact @makeitnow/jumpitnow@0.1.0-alpha.
|
|
100
|
+
npm install --save-exact @makeitnow/jumpitnow@0.1.0-alpha.4
|
|
97
101
|
```
|
|
98
102
|
|
|
99
103
|
등록 전이거나 발행 직후 아직 조회되지 않는다면 아래 설치 파일 방식을 사용하세요. 두 설치 방식 중 하나를 선택합니다.
|
|
100
104
|
|
|
101
105
|
### 전달받은 파일로 설치
|
|
102
106
|
|
|
103
|
-
프로젝트 안에 `vendor` 폴더를 만들고 전달받은 `makeitnow-jumpitnow-0.1.0-alpha.
|
|
107
|
+
프로젝트 안에 `vendor` 폴더를 만들고 전달받은 `makeitnow-jumpitnow-0.1.0-alpha.4.tgz` 파일을 넣은 뒤, 프로젝트 폴더에서 실행하세요.
|
|
104
108
|
|
|
105
109
|
```sh
|
|
106
|
-
npm install ./vendor/makeitnow-jumpitnow-0.1.0-alpha.
|
|
110
|
+
npm install ./vendor/makeitnow-jumpitnow-0.1.0-alpha.4.tgz
|
|
107
111
|
```
|
|
108
112
|
|
|
109
113
|
아직 프로젝트가 없다면 `mkdir my-jumprope-app`, `cd my-jumprope-app`, `npm init -y`를 차례로 실행한 뒤 위와 같이 설치합니다. 이 방법으로 아래 가상 장비 예제를 바로 실행할 수 있습니다.
|
|
@@ -358,7 +362,7 @@ SDK에는 `save()`, `saveRecord()`, 로그인, 기록 조회 함수가 없습니
|
|
|
358
362
|
| 문서·예제 | 내용 |
|
|
359
363
|
|---|---|
|
|
360
364
|
| [함수 사용법](docs/API.md) | 입력값, 결과값, 이벤트 사용 예 |
|
|
361
|
-
| [
|
|
365
|
+
| [SDK & API HTML 가이드](docs/index.html) | 흐름도, API 검색, 데이터 예시, 코드 복사와 AI 요청문 |
|
|
362
366
|
| [사용 환경](docs/COMPATIBILITY.md) | 실제 장비 사용 조건과 검증 상태 |
|
|
363
367
|
| [수업 운영과 기록 관리](docs/SECURITY.md) | 장비 선택, 정지 확인, 기록 관리 |
|
|
364
368
|
| [브라우저 예제](examples/browser/app.js) | 연결·선택·운동·수신 화면 구현 |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Battery, CommandResult, Device, Diagnostics, DisconnectResult, Events, Gateway, GatewayInfo, GatewayOptions, IdentifySignal, Measurement, Mode, ScanResult } from '../types.js';
|
|
2
2
|
import type { Link } from './link.js';
|
|
3
|
-
export declare const VERSION = "0.1.0-alpha.
|
|
3
|
+
export declare const VERSION = "0.1.0-alpha.4";
|
|
4
4
|
export declare class GatewayEngine implements Gateway {
|
|
5
5
|
#private;
|
|
6
6
|
constructor(link: Link, options?: GatewayOptions, simulated?: boolean);
|
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.GatewayEngine = exports.VERSION = void 0;
|
|
4
4
|
const errors_js_1 = require("./errors.js");
|
|
5
5
|
const protocol_js_1 = require("./protocol.js");
|
|
6
|
-
exports.VERSION = '0.1.0-alpha.
|
|
6
|
+
exports.VERSION = '0.1.0-alpha.4';
|
|
7
7
|
const INTERFACE = '1';
|
|
8
8
|
const clone = (value) => {
|
|
9
9
|
try {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Battery, CommandResult, Device, Diagnostics, DisconnectResult, Events, Gateway, GatewayInfo, GatewayOptions, IdentifySignal, Measurement, Mode, ScanResult } from '../types.js';
|
|
2
2
|
import type { Link } from './link.js';
|
|
3
|
-
export declare const VERSION = "0.1.0-alpha.
|
|
3
|
+
export declare const VERSION = "0.1.0-alpha.4";
|
|
4
4
|
export declare class GatewayEngine implements Gateway {
|
|
5
5
|
#private;
|
|
6
6
|
constructor(link: Link, options?: GatewayOptions, simulated?: boolean);
|
package/docs/API.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Jumpitnow SDK · 함수 사용법
|
|
2
2
|
|
|
3
|
-
교사 앱에서 사용하는 JavaScript 함수와 입력값, 결과값을 설명합니다. 현재 버전은 `0.1.0-alpha.
|
|
3
|
+
교사 앱에서 사용하는 JavaScript 함수와 입력값, 결과값을 설명합니다. 현재 버전은 `0.1.0-alpha.4`입니다. 설치 방법은 [사용 안내](../README.md)를 참고하세요.
|
|
4
4
|
|
|
5
5
|
## 연결하고 장비 선택하기
|
|
6
6
|
|
package/docs/index.html
CHANGED
|
@@ -1,30 +1,666 @@
|
|
|
1
|
-
<!doctype html
|
|
2
|
-
<style>
|
|
3
|
-
:root{--ink:#183341;--muted:#5c7181;--blue:#245bda;--teal:#087f80;--line:#dce5ed;--paper:#f4f7fb}*{box-sizing:border-box}body{margin:0;color:var(--ink);background:var(--paper);font:15px/1.8 -apple-system,BlinkMacSystemFont,"Apple SD Gothic Neo","Malgun Gothic",sans-serif}a{color:var(--blue);text-underline-offset:4px}code,pre,.mono{font-family:ui-monospace,SFMono-Regular,Consolas,monospace}header{background:white;border-bottom:1px solid var(--line);padding:20px 5%;display:flex;justify-content:space-between;align-items:center;gap:20px}.brand{font-size:23px;font-weight:900;letter-spacing:-1px}.brand span{color:var(--blue)}header nav{display:flex;gap:24px;font-size:13px}.wrap{max-width:1180px;margin:auto;padding:65px 30px}.hero{display:grid;grid-template-columns:1.5fr 1fr;gap:70px;align-items:center;padding-bottom:60px}.eyebrow{font:11px ui-monospace,monospace;letter-spacing:.13em;color:var(--blue);font-weight:700}h1{font-size:clamp(34px,4vw,54px);line-height:1.28;letter-spacing:-2px;margin:20px 0}h1 em{font-style:normal;color:var(--blue)}p{color:var(--muted)}.tag{display:inline-block;background:#eaf0ff;color:var(--blue);padding:5px 9px;font-size:12px;border-radius:5px}.actions{display:flex;gap:12px;margin-top:26px;flex-wrap:wrap}.button{display:inline-block;text-decoration:none;border:1px solid var(--line);padding:10px 18px;border-radius:7px;background:white;font-size:14px;font-weight:700}.button.primary{background:var(--blue);color:white;border-color:var(--blue)}.sample{background:#163444;color:#d8e7f5;padding:24px;border-radius:10px;box-shadow:0 16px 32px #1537450d}.sample .mono{color:#8dcbda;font-size:11px}.sample pre{margin:20px 0;font-size:12px;line-height:2;white-space:pre-wrap;overflow-wrap:anywhere}.sample small{color:#a5becd}section{padding:42px 0;border-top:1px solid var(--line);scroll-margin-top:25px}section h2{font-size:26px;letter-spacing:-1px;margin:8px 0 22px}.flow{display:flex;align-items:center;gap:0;margin:26px 0}.node{flex:1;background:white;border:1px solid var(--line);border-radius:8px;padding:24px 15px;text-align:center}.node strong{display:block;font-size:16px}.node small{display:block;color:var(--muted);font-size:11px;margin-top:7px}.node.sdk{background:var(--blue);color:white;border-color:var(--blue)}.node.sdk small{color:#dfe8ff}.arrow{color:var(--teal);padding:0 13px;font-size:22px}.steps{display:grid;grid-template-columns:repeat(4,1fr);gap:24px}.step{border-top:2px solid var(--blue);padding-top:15px}.step .mono{color:var(--blue);font-size:12px}.step h3{font-size:16px;margin:10px 0 5px}.step p{font-size:13px;margin:0}.code{background:white;border:1px solid var(--line);border-radius:9px;padding:22px;font-size:12px;white-space:pre-wrap;overflow-wrap:anywhere}.table-wrap{overflow:auto;background:white;border:1px solid var(--line);border-radius:9px}table{border-collapse:collapse;width:100%;font-size:13px}th,td{padding:15px 20px;text-align:left;border-bottom:1px solid var(--line);vertical-align:top}th{background:#edf3fa;font-size:12px}td code{color:var(--blue);font-size:12px}tr:last-child td{border-bottom:0}.results{display:grid;grid-template-columns:repeat(3,1fr);gap:20px}.result{padding:23px;background:white;border:1px solid var(--line);border-top:3px solid var(--teal);border-radius:7px}.result:nth-child(2){border-top-color:#b84b40}.result:nth-child(3){border-top-color:#b88625}.result code{font-weight:700}.result p{font-size:13px;margin-bottom:0}.note{border-left:3px solid var(--blue);padding:10px 18px;background:#ebf1fc;margin-top:24px;font-size:13px}.docs{display:grid;grid-template-columns:repeat(3,1fr);gap:20px}.doc{background:white;border:1px solid var(--line);border-radius:9px;padding:22px;text-decoration:none;color:var(--ink)}.doc:hover{border-color:var(--blue)}.doc strong{display:block}.doc p{font-size:13px;margin:7px 0 12px}.doc span{font-size:12px;color:var(--blue)}footer{font-size:12px;color:var(--muted);border-top:1px solid var(--line);padding-top:24px}a:focus-visible{outline:3px solid #91b3ff;outline-offset:4px}@media(max-width:850px){.hero{grid-template-columns:1fr;gap:25px}.steps{grid-template-columns:repeat(2,1fr)}.wrap{padding-top:36px}.docs{grid-template-columns:1fr}}@media(max-width:600px){.wrap{padding:30px 20px}.flow{flex-direction:column}.node{width:100%}.arrow{transform:rotate(90deg);padding:8px}.results{grid-template-columns:1fr}header nav a:first-child{display:none}.steps{gap:22px 16px}th,td{padding:13px;min-width:135px}.hero{padding-bottom:35px}}@media print{header nav,.actions{display:none}.wrap{max-width:none;padding:20px}body{background:white}.hero{gap:30px}section{break-inside:avoid}a{color:inherit}.sample{box-shadow:none}}
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="ko"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="color-scheme" content="light"><meta name="description" content="Jumpitnow JavaScript SDK 가이드. 설치, 장비 연결, 운동 데이터, API와 AI 코딩 예제를 시각적으로 살펴보세요."><title>Jumpitnow · SDK & API Guide</title><style>
|
|
3
|
+
:root{--navy:#102a46;--ink:#263c51;--muted:#68798b;--blue:#2563eb;--mint:#29a994;--coral:#e96b52;--paper:#f5f8fc;--line:#e3eaf2;--mono:SFMono-Regular,Consolas,"Liberation Mono",monospace;--body:"Avenir Next","Apple SD Gothic Neo","Malgun Gothic",sans-serif;--header:72px;--sidebar:232px}*{box-sizing:border-box}html{scroll-behavior:smooth;scroll-padding-top:100px}body{margin:0;font:15px/1.75 var(--body);color:var(--ink);background:#fff}button,input,textarea{font:inherit}button,a,input,summary{ -webkit-tap-highlight-color:transparent}button{cursor:pointer}button:disabled{cursor:default}a{color:inherit;text-decoration:none}button,a,input,summary,textarea{outline-offset:4px} :focus-visible{outline:3px solid #77a4ff}::selection{background:#dce8ff;color:var(--navy)}code,pre,.mono{font-family:var(--mono)}code{font-size:.88em}p{margin:0 0 16px}h1,h2,h3,h4{color:var(--navy);line-height:1.35;margin:0;word-break:keep-all}h1{font-size:clamp(35px,3.6vw,55px);font-weight:800;letter-spacing:-2.6px}h2{font-size:30px;letter-spacing:-1.2px}h3{font-size:19px;letter-spacing:-.5px}small{font-size:12px}.skip{position:fixed;top:-100px;left:20px;z-index:100;background:white;padding:12px}.skip:focus{top:10px}.topbar{height:var(--header);display:flex;align-items:center;position:sticky;top:0;background:#ffffffee;backdrop-filter:blur(12px);border-bottom:1px solid var(--line);z-index:30;padding:0 32px;gap:32px}.brand{display:flex;align-items:center;gap:10px;font:800 22px/1 var(--body);letter-spacing:-1px;color:var(--navy)}.brand-mark{width:31px;height:31px}.brand-divider{width:1px;height:22px;background:var(--line);margin:0 5px}.top-title{font-size:13px;color:var(--muted);letter-spacing:.01em}.top-actions{margin-left:auto;display:flex;align-items:center;gap:24px;font-size:13px}.external{color:var(--muted)}.external:hover{color:var(--blue)}.version{display:flex;align-items:center;gap:7px;background:#edf4ff;color:#2761c8;font:11px var(--mono);padding:7px 10px;border-radius:6px}.dot{width:6px;height:6px;border-radius:50%;background:currentColor;display:inline-block}.print-btn{border:0;background:none;color:var(--muted);padding:6px}.mobile-menu{display:none;border:1px solid var(--line);border-radius:6px;background:white;padding:5px 9px}.sidebar{position:fixed;top:var(--header);left:0;bottom:0;width:var(--sidebar);background:#f9fbfe;border-right:1px solid var(--line);padding:26px 18px;overflow:auto;z-index:20}.side-caption{font:10px var(--mono);color:#8090a2;letter-spacing:.12em;padding:0 14px;margin:22px 0 8px}.side-caption:first-of-type{margin-top:0}.sidebar nav a{display:flex;align-items:center;gap:11px;padding:8px 13px;margin:3px 0;border-radius:7px;font-size:13px;color:#62748a;position:relative}.sidebar nav a.active{background:#e9f0fd;color:var(--blue);font-weight:700}.sidebar nav a.active:before{content:'';position:absolute;left:0;height:15px;width:3px;background:var(--blue);border-radius:2px}.nav-icon{width:15px;font-family:var(--mono);font-size:13px;text-align:center;opacity:.65}.side-footer{padding:20px 13px;border-top:1px solid var(--line);margin-top:25px;font-size:11px;line-height:1.8;color:var(--muted)}.side-footer strong{color:var(--navy);display:block;margin-bottom:5px;font-size:12px}.main{margin-left:var(--sidebar);padding:0 48px;max-width:1512px}.content{max-width:1100px;margin:0 auto}.breadcrumb{font-size:11px;color:#8493a4;display:flex;gap:10px;padding-top:32px}.breadcrumb b{font-weight:500;color:#4d6683}.hero{padding:40px 0 30px}.hero-grid{display:grid;grid-template-columns:1.12fr 1fr;align-items:center;gap:24px}.eyebrow{font:600 10px/1.5 var(--mono);letter-spacing:.13em;color:var(--blue);display:flex;align-items:center;gap:8px;margin-bottom:16px}.eyebrow:before{content:'';width:18px;height:2px;background:currentColor}.hero h1 em{font-style:normal;color:var(--blue)}.hero-copy{max-width:480px;color:var(--muted);font-size:15px;margin-top:22px;line-height:1.9;word-break:keep-all}.actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:25px}.btn{border:1px solid var(--line);background:white;border-radius:7px;padding:10px 17px;display:inline-flex;align-items:center;justify-content:center;gap:12px;font-size:13px;font-weight:600;line-height:1.6}.btn:hover{background:#f2f6fc;border-color:#bacbe4}.btn.primary{background:var(--blue);color:white;border-color:var(--blue);box-shadow:0 4px 9px #2563eb18}.btn.primary:hover{background:#174dcc}.hero-art{position:relative;min-width:0}.hero-art svg{width:100%;height:auto;display:block}.art-caption{font-size:10px;letter-spacing:.04em;text-align:center;color:#8a99aa;margin-top:-18px}.install-bar{display:flex;align-items:center;gap:18px;padding:16px 18px;border:1px solid var(--line);border-radius:9px;background:#f8fafd;margin-top:30px}.install-bar .label{font:11px var(--mono);color:#8290a1;border-right:1px solid var(--line);padding-right:18px}.install-bar code{font-size:12px;color:#365272;overflow-x:auto;white-space:nowrap;flex:1;min-width:0}.copy{font-size:11px;padding:5px 10px;border:1px solid #d4deeb;border-radius:5px;color:#61748a;background:#fff;white-space:nowrap}.copy:hover{border-color:var(--blue);color:var(--blue)}.alpha-note{display:flex;align-items:start;gap:10px;color:var(--muted);font-size:12px;margin:16px 0 8px}.alpha-note .tag{flex-shrink:0}.tag{font:10px/1.7 var(--mono);display:inline-block;border-radius:4px;background:#edf3fc;color:#45648e;padding:2px 7px}.doc-section{padding:58px 0;border-top:1px solid var(--line);scroll-margin-top:96px}.section-top{display:flex;align-items:start;justify-content:space-between;gap:24px;margin-bottom:28px}.section-top p{margin:12px 0 0;color:var(--muted);font-size:14px;max-width:680px;word-break:keep-all}.section-kicker{font:11px var(--mono);letter-spacing:.12em;color:#8b9db0;white-space:nowrap;padding-top:10px}.architecture{background:#f7faff;border:1px solid var(--line);border-radius:12px;padding:30px 24px}.diagram-labels{display:flex;justify-content:space-between;max-width:90%;margin:0 auto 13px;font:10px var(--mono);letter-spacing:.07em;color:#7a8da5}.arch-row{display:grid;grid-template-columns:1fr 26px 1fr 26px 1.15fr 26px 1fr;align-items:center}.arch-node{text-align:center;min-width:0}.arch-box{height:106px;display:flex;flex-direction:column;justify-content:center;align-items:center;gap:7px;border:1px solid #dce6f3;border-radius:8px;background:white;box-shadow:0 4px 8px #0b2c4810}.arch-box .pictogram{font-size:24px;line-height:1;color:#496c98}.arch-box strong{font-size:14px;color:var(--navy)}.arch-node small{display:block;color:#8091a5;margin-top:11px;font-size:10px}.arch-box.sdk{background:var(--blue);border-color:var(--blue);color:white;box-shadow:0 6px 20px #2563eb20}.arch-box.sdk strong,.arch-box.sdk .pictogram{color:#fff}.arch-arrow{text-align:center;color:#9ab4d9;font-size:18px}.boundary{display:grid;grid-template-columns:1fr 1fr;gap:25px;margin-top:26px}.boundary>div{padding-left:14px;border-left:3px solid #b6cae5}.boundary>div:first-child{border-color:var(--blue)}.boundary strong{font-size:12px;color:var(--navy)}.boundary p{font-size:12px;color:var(--muted);margin:5px 0 0}.tabs{display:flex;gap:4px;border-bottom:1px solid var(--line);margin-bottom:0}.tabs button{background:transparent;border:0;border-bottom:2px solid transparent;color:var(--muted);padding:12px 17px;font-size:13px}.tabs button[aria-selected=true]{border-color:var(--blue);color:var(--blue);font-weight:700}.quick-panel{padding-top:24px}.quick-meta{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--muted);flex-wrap:wrap;margin-bottom:15px}.quick-meta code{color:var(--blue);background:#edf4ff;padding:4px 8px;border-radius:5px}.code-card{border-radius:9px;overflow:hidden;background:#132b44;color:#d5e3f4;margin:14px 0;position:relative}.code-head{height:42px;display:flex;align-items:center;justify-content:space-between;gap:8px;padding:0 17px;background:#19344f;border-bottom:1px solid #304961;font:10px var(--mono);color:#99b3d3}.code-head .copy{background:#28435e;border-color:#3e5873;color:#bed0e6}.code-card pre{margin:0;padding:21px 23px;font:12px/1.95 var(--mono);overflow:auto;tab-size:2;white-space:pre}.code-card .comment{color:#7e9fb5}.code-card .string{color:#9cd7c5}.code-card .keyword{color:#9fbbff}.long-code pre{max-height:345px}.long-code.expanded pre{max-height:none}.expand-code{width:100%;border:0;border-top:1px solid #304961;background:#19344f;color:#a4c4ed;padding:10px;font-size:11px}.tip{display:flex;gap:12px;padding:16px 18px;background:#eff5ff;color:#47658a;border-radius:7px;font-size:12px;line-height:1.8;margin-top:16px}.tip strong{color:#2b538c}.tip-icon{font:700 13px var(--mono);padding-top:2px}.flow-steps{display:grid;grid-template-columns:repeat(6,1fr);gap:0;margin:8px 0 22px}.flow-step{position:relative;text-align:left;border:0;background:none;padding:0 8px 0 0;min-width:0;color:var(--muted)}.flow-step:before{content:'';position:absolute;top:16px;left:34px;right:0;border-top:1px solid var(--line)}.flow-step:last-child:before{display:none}.flow-step .num{display:flex;position:relative;align-items:center;justify-content:center;width:33px;height:33px;border:1px solid #d9e3f0;border-radius:50%;font:11px var(--mono);background:white;margin-bottom:12px;color:#8a9bb0}.flow-step.active .num{color:white;background:var(--blue);border-color:var(--blue);box-shadow:0 0 0 5px #ecf2ff}.flow-step b{font-size:12px;font-weight:600;display:block}.flow-step.active b{color:var(--blue)}.flow-step small{font:9px/1.8 var(--mono);display:block;margin-top:4px;color:#8c9eb2;overflow-wrap:anywhere}.flow-detail{display:grid;grid-template-columns:.9fr 1.1fr;gap:30px;padding:24px;border:1px solid var(--line);border-radius:9px;align-items:center}.flow-text p{font-size:13px;color:var(--muted);margin:12px 0}.flow-text .owner{font-size:11px;color:#557598;border-top:1px solid var(--line);padding-top:13px}.flow-detail .code-card{margin:0}.flow-detail pre{font-size:11px;padding:18px}.mode-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}.mode-option{background:white;border:1px solid var(--line);border-radius:9px;padding:22px;text-align:left;color:var(--ink)}.mode-option[aria-pressed=true]{border-color:var(--blue);background:#f2f6ff;box-shadow:inset 0 0 0 1px var(--blue)}.mode-icon{width:36px;height:36px;border-radius:9px;background:#edf3fc;color:#4d78b8;display:flex;align-items:center;justify-content:center;font:23px var(--mono);margin-bottom:14px}.mode-option[aria-pressed=true] .mode-icon{background:var(--blue);color:white}.mode-option strong{font-size:15px;display:block;color:var(--navy)}.mode-option p{font-size:12px;color:var(--muted);margin:6px 0 0}.mode-controls{display:flex;gap:15px;align-items:center;margin-top:18px;font-size:12px;flex-wrap:wrap}.mode-controls input{width:100px;padding:7px 10px;border:1px solid var(--line);border-radius:5px;color:var(--navy)}.mode-controls .invalid{color:#bc4736;font-size:12px}.mode-caption{color:var(--muted);font-size:12px;margin:0}.api-toolbar{display:flex;gap:14px;align-items:center;justify-content:space-between;margin-bottom:17px}.search-wrap{position:relative;flex:1;max-width:420px}.search-wrap>span{position:absolute;left:13px;top:7px;color:#90a0b2;font-size:20px}.search-wrap input{width:100%;padding:10px 42px 10px 38px;border:1px solid var(--line);border-radius:7px;background:#f9fbfe;color:var(--navy);font-size:12px}.search-wrap kbd{position:absolute;right:11px;top:10px;font:10px var(--mono);padding:1px 5px;border:1px solid var(--line);color:#8798a9;border-radius:3px;background:white}.api-count{font:11px var(--mono);color:#8a99aa}.chips{display:flex;gap:7px;flex-wrap:wrap;margin-bottom:18px}.chip{padding:5px 12px;font-size:11px;border:1px solid var(--line);border-radius:5px;background:white;color:var(--muted)}.chip[aria-pressed=true]{background:var(--navy);border-color:var(--navy);color:white}.api-entry{border:1px solid var(--line);border-radius:8px;margin-bottom:10px;overflow:hidden}.api-entry[open]{border-color:#b2c9eb;box-shadow:0 4px 12px #17418a06}.api-entry summary{display:grid;grid-template-columns:minmax(180px,1fr) auto 20px;align-items:center;gap:20px;padding:17px 20px;cursor:pointer;list-style:none}.api-entry summary::-webkit-details-marker{display:none}.api-entry summary:after{content:'+';font:18px var(--mono);color:#8d9eb1}.api-entry[open] summary:after{content:'−';color:var(--blue)}.api-entry[open] summary{background:#f7faff;border-bottom:1px solid var(--line)}.api-title{font:600 14px var(--mono);color:#31588a}.api-subtitle{font-size:11px;color:var(--muted);display:block;margin-top:5px;font-family:var(--body);font-weight:400}.api-kind{font:10px var(--mono);color:#62788c;background:#f1f5f9;padding:3px 7px;border-radius:4px}.api-kind.async{background:#edf5ff;color:#3970b3}.api-body{padding:22px}.api-body>p{font-size:13px;color:var(--muted)}.api-signature{font:12px var(--mono);color:var(--blue);overflow-wrap:anywhere;margin-bottom:12px}.return{font-size:11px;color:var(--muted);border-left:2px solid var(--mint);padding:7px 12px;margin:15px 0;background:#f4faf8;overflow-wrap:anywhere}.return code{color:#2c776d}.table-wrap{border:1px solid var(--line);border-radius:8px;overflow:auto;margin:17px 0}table{width:100%;border-collapse:collapse;font-size:12px;text-align:left}th{background:#f5f8fc;font-size:10px;font-weight:600;color:#7c8fa3}td,th{padding:13px 16px;border-bottom:1px solid var(--line);vertical-align:top}tr:last-child td{border-bottom:0}td code{font-size:11px;color:#3a679c;white-space:nowrap}td p{margin:4px 0 0;font-size:11px;color:var(--muted)}.api-entry td{font-size:11px}.api-note{color:#6b7c8f;background:#f8fafc;border-radius:6px;font-size:11px;padding:12px 15px;line-height:1.8}.empty{padding:35px 20px;border:1px dashed #c4d4e8;text-align:center;border-radius:8px;font-size:13px;color:var(--muted)}.advanced{margin-top:22px;border-top:1px solid var(--line);padding-top:15px}.advanced>summary{cursor:pointer;font-size:13px;color:#436085}.data-lab{display:grid;grid-template-columns:1fr 1.12fr;gap:22px}.score-panel{border:1px solid var(--line);border-radius:10px;padding:25px;background:#f8fbff}.score-top{display:flex;justify-content:space-between;align-items:center;font-size:12px;color:#6b7d91}.practice-badge{color:#368c7c;background:#e3f3ed;border-radius:4px;padding:2px 7px;font-size:10px}.score-big{display:flex;align-items:baseline;gap:8px;margin:12px 0 0}.score-big strong{font:600 68px/1.35 var(--body);letter-spacing:-3px;color:var(--navy);font-variant-numeric:tabular-nums}.score-big span{font-size:16px;color:#7d8fa5}.score-time{font:12px var(--mono);color:#6a88ab}.sample-track{display:flex;gap:7px;align-items:center;margin:24px 0 14px;font:12px var(--mono);color:#96a6b9}.sample-track span{padding:8px 12px;border-radius:6px;border:1px solid var(--line);background:white}.sample-track span.current{border-color:var(--blue);background:#eaf1ff;color:var(--blue)}.score-panel p{font-size:12px;color:var(--muted)}.lab-buttons{display:flex;gap:8px;flex-wrap:wrap}.lab-caption{font-size:10px;color:#91a0b0;margin:12px 0 0!important}.payload-panel .code-card{margin:0;height:100%}.payload-panel pre{font-size:11px;padding:18px;line-height:1.75}.data-rule{margin-top:20px;display:flex;align-items:center;justify-content:space-between;gap:14px;padding:16px 20px;border:1px solid #d6e9e3;background:#f4faf7;border-radius:8px;font-size:12px;color:#476f66}.data-rule strong{color:#267a67}.data-rule code{white-space:nowrap;color:#4c7b72}.field-table td:first-child{width:150px}.field-table tr{cursor:pointer}.field-table tbody tr:hover,.field-table tr.selected{background:#f1f6ff}.field-table th:nth-child(2){width:135px}.field-detail{padding:18px 20px;border:1px solid #cadbf4;border-radius:8px;background:#f4f8ff;font-size:12px;min-height:90px}.field-detail strong{color:var(--blue);font-family:var(--mono);display:block;margin-bottom:6px}.event-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}.event-card{border:1px solid var(--line);border-radius:8px;padding:20px}.event-card code{color:var(--blue);font-weight:600}.event-card p{font-size:12px;color:var(--muted);margin:8px 0 10px}.event-card .event-shape{font:10px var(--mono);color:#7b91a8;background:#f6f9fd;padding:7px 9px;border-radius:4px;overflow-wrap:anywhere}.results-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-bottom:22px}.result-card{padding:20px;border:1px solid var(--line);border-radius:8px;border-top:3px solid var(--mint)}.result-card:nth-child(2){border-top-color:var(--coral)}.result-card:nth-child(3){border-top-color:#d6a441}.result-card .status-label{font-size:11px;color:#488a7b}.result-card:nth-child(2) .status-label{color:#c56951}.result-card:nth-child(3) .status-label{color:#a88943}.result-card code{display:block;color:var(--navy);font-size:12px;font-weight:600;margin:9px 0}.result-card p{font-size:12px;color:var(--muted);margin:0}.result-switch{display:flex;gap:7px;align-items:center;flex-wrap:wrap;font-size:12px}.result-switch>span{color:var(--muted);margin-right:4px}.result-message{font-size:12px;color:var(--muted);padding:10px 0 0}.error-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:25px}.error-card{padding:18px;border-left:2px solid var(--line);background:#fbfcfe}.error-card strong{display:block;color:#4d6582;font-size:12px}.error-card code{display:block;font-size:9px;color:#8b9daf;margin:6px 0;overflow-wrap:anywhere}.error-card p{font-size:11px;margin:0;color:var(--muted)}.storage-flow{display:grid;grid-template-columns:repeat(4,1fr);border:1px solid var(--line);border-radius:9px;overflow:hidden;margin-bottom:23px}.storage-step{padding:20px;border-right:1px solid var(--line);position:relative}.storage-step:last-child{border-right:0;background:#f3faf7}.storage-step .mono{font-size:10px;color:#839ab6}.storage-step strong{display:block;font-size:13px;color:var(--navy);margin:7px 0}.storage-step p{font-size:11px;color:var(--muted);margin:0}.storage-split{display:grid;grid-template-columns:1fr 1fr;gap:24px}.storage-tree{background:#f7faff;border:1px solid var(--line);border-radius:8px;padding:22px}.tree-root{font:600 13px var(--mono);color:#365b89}.tree-branch{margin:13px 0 0 5px;padding-left:17px;border-left:1px solid #b6cce6}.tree-branch>div{font:11px/2 var(--mono);color:#5d7899}.tree-branch small{font:11px var(--body);color:#97a5b4;margin-left:6px}.storage-notes{font-size:12px;color:var(--muted)}.storage-notes ul{padding-left:18px;margin:5px 0}.storage-notes li{margin-bottom:12px;padding-left:3px}.storage-notes strong{color:#48627f}.ai-panel{background:#f7faff;border:1px solid #dce7f5;border-radius:10px;padding:26px}.ai-top{display:flex;align-items:center;justify-content:space-between;gap:15px;margin-bottom:15px}.ai-top p{font-size:12px;color:var(--muted);margin:6px 0 0}.prompt-text{width:100%;border:1px solid var(--line);background:white;color:#516b87;font:12px/1.85 var(--body);resize:vertical;min-height:255px;padding:18px;border-radius:7px}.recipe-buttons{display:flex;gap:7px;flex-wrap:wrap;margin-bottom:15px}.recipe-buttons button{border:1px solid #d3e0f0;background:white;border-radius:5px;padding:5px 10px;font-size:11px;color:#667e9c}.recipe-buttons button[aria-pressed=true]{border-color:var(--blue);color:var(--blue);background:#edf4ff}.checklist{list-style:none;padding:0;margin:20px 0 0;display:grid;grid-template-columns:1fr 1fr;gap:10px 24px}.checklist label{display:flex;align-items:start;gap:9px;font-size:12px;color:#6e8197;cursor:pointer}.checklist input{accent-color:var(--blue);margin-top:5px;flex-shrink:0}.checklist input:checked+span{color:#2c7d6c}.check-progress{color:#54769f;font:11px var(--mono);margin-top:15px}.finish{background:#f6f9ff;border:1px solid var(--line);border-radius:10px;display:flex;align-items:center;justify-content:space-between;padding:26px 30px;gap:18px}.finish h3{font-size:18px}.finish p{font-size:12px;color:var(--muted);margin:7px 0 0}.footer{padding:25px 0 35px;border-top:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;gap:20px;font-size:11px;color:#8e9db0}.footer strong{color:#637d99}.toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);z-index:90;padding:10px 18px;background:#19364f;color:white;border-radius:7px;box-shadow:0 6px 24px #102e4a20;font-size:12px}.top-progress{position:fixed;top:0;left:0;height:2px;background:var(--blue);width:0;z-index:100}.no-js{padding:15px;background:#fff4df;color:#826226}.no-js a{text-decoration:underline}[hidden]{display:none!important}
|
|
4
|
+
@media(min-width:1650px){.main{padding:0 70px}.hero{padding-top:50px}.hero-grid{gap:50px}}
|
|
5
|
+
@media(max-width:1200px){:root{--sidebar:205px}.main{padding:0 32px}.topbar{padding:0 23px}.top-actions{gap:16px}.hero-grid{grid-template-columns:1.1fr 1fr;gap:0}h1{font-size:39px}.hero-copy{font-size:14px}.hero-art{margin-right:-10px}.flow-detail{grid-template-columns:1fr;gap:13px}.flow-step small{font-size:8px}.section-kicker{display:none}.arch-box strong{font-size:12px}}
|
|
6
|
+
@media(max-width:920px){:root{--sidebar:0px}.sidebar{width:240px;transform:translateX(-100%);box-shadow:8px 0 25px #122f4b12;transition:transform .2s}.sidebar.open{transform:translateX(0)}.mobile-menu{display:block}.topbar{gap:16px}.main{padding:0 30px}.hero-grid{grid-template-columns:1.1fr 1fr}.hero-art{margin-right:0}.top-title,.brand-divider{display:none}.brand{font-size:20px}.top-actions{gap:12px}.architecture{padding:24px 18px}}
|
|
7
|
+
@media(max-width:640px){:root{--header:64px}.topbar{padding:0 16px;gap:10px}.brand{font-size:19px;gap:7px}.brand-mark{width:27px;height:27px}.top-actions .external,.print-btn{display:none}.version{font-size:9px;padding:6px}.top-actions{margin-left:auto}.main{padding:0 21px}.breadcrumb{padding-top:22px;font-size:10px}.hero{padding:30px 0}.hero-grid{grid-template-columns:1fr}.hero-art{max-width:390px;width:100%;margin:14px auto 0}.hero-copy{font-size:14px}h1{font-size:39px;letter-spacing:-1.8px}h2{font-size:25px}.hero .eyebrow{font-size:9px}.hero .actions{margin-top:19px}.install-bar{padding:13px;gap:8px;margin-top:21px}.install-bar .label{display:none}.install-bar code{font-size:10px}.alpha-note{font-size:11px}.doc-section{padding:37px 0}.section-top{margin-bottom:22px}.section-top p{font-size:13px}.arch-row{grid-template-columns:1fr 24px 1fr;gap:13px 0}.arch-arrow:nth-child(4){display:none}.arch-box{height:94px}.diagram-labels{display:none}.boundary{grid-template-columns:1fr;gap:18px;margin-top:22px}.flow-steps{grid-template-columns:repeat(3,1fr);gap:20px 4px}.flow-step:nth-child(3):before{display:none}.flow-step small{font-size:9px}.flow-detail{padding:18px}.flow-detail pre{font-size:10px;padding:14px}.mode-grid{gap:8px}.mode-option{padding:14px 10px}.mode-option strong{font-size:13px}.mode-option p{font-size:10px}.mode-icon{height:31px;width:31px;font-size:21px}.tabs button{padding:11px 12px;font-size:12px}.code-card pre{font-size:10px;padding:17px}.quick-meta{font-size:11px}.api-toolbar{gap:10px}.api-entry summary{gap:9px;padding:16px 13px;grid-template-columns:minmax(0,1fr) auto 12px}.api-title{font-size:12px}.api-subtitle{font-size:10px}.api-kind{font-size:9px}.api-body{padding:16px}.api-body table{min-width:410px}.api-signature{font-size:10px}.chips{gap:5px}.chip{padding:5px 9px;font-size:10px}.data-lab,.storage-split{grid-template-columns:1fr}.payload-panel pre{font-size:11px}.data-rule{align-items:start;flex-direction:column;padding:15px}.field-table{min-width:550px}.event-grid,.error-grid{grid-template-columns:1fr}.results-grid{grid-template-columns:1fr;gap:10px}.result-card{padding:16px}.result-card code{display:inline-block;margin:0 10px 7px}.storage-flow{grid-template-columns:1fr 1fr}.storage-step{padding:17px;border-bottom:1px solid var(--line)}.storage-step:nth-child(2){border-right:0}.storage-step:nth-child(n+3){border-bottom:0}.ai-panel{padding:18px}.ai-top{align-items:start}.ai-top .btn{padding:7px 11px;white-space:nowrap;font-size:11px}.checklist{grid-template-columns:1fr}.finish{padding:23px;display:block}.finish .btn{margin-top:17px}.footer{align-items:start;flex-direction:column;gap:5px}.toast{width:max-content;max-width:88vw;text-align:center}.tip{font-size:11px;padding:14px}.long-code pre{max-height:300px}}
|
|
8
|
+
@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}.sidebar{transition:none}}
|
|
9
|
+
@media print{html{scroll-behavior:auto}body{font-size:11px}.topbar,.sidebar,.top-progress,.copy,.print-btn,.mobile-menu,.toast,.actions,.api-toolbar,#api-empty,.chips,.expand-code,.lab-buttons,.recipe-buttons,.checklist,.check-progress{display:none!important}.main{margin:0;padding:0}.content{max-width:none}.hero{padding:15px 0}.hero-grid{grid-template-columns:1fr 1fr}.hero-art{max-width:320px}.doc-section{padding:25px 0}.code-card{background:#f7f9fc;color:#1e3956;border:1px solid #cdd8e7}.code-head{background:#e9eff7;color:#3a5774}.code-card pre{white-space:pre-wrap;overflow-wrap:anywhere;max-height:none!important;font-size:9px}.code-card .comment{color:#587486}.code-card .keyword,.code-card .string{color:#275792}.api-entry{break-inside:avoid}.api-body{display:block!important}.quick-panel[hidden]{display:block!important}.tabs,.flow-steps,.mode-grid{break-inside:avoid}.prompt-text{height:950px;resize:none;overflow:visible}.storage-flow,.architecture,.score-panel,.result-card,.event-card{break-inside:avoid}h2,h3{break-after:avoid}.hero-copy,.section-top p{font-size:12px}.footer{font-size:9px}.install-bar{background:#fff}.art-caption{margin-top:0}a{color:inherit}.field-table{min-width:0}section{scroll-margin-top:0}}
|
|
4
10
|
</style></head><body>
|
|
5
|
-
<
|
|
6
|
-
<
|
|
7
|
-
<
|
|
8
|
-
|
|
11
|
+
<a class="skip" href="#overview">본문으로 건너뛰기</a><div class="top-progress" aria-hidden="true"></div>
|
|
12
|
+
<header class="topbar"><button class="mobile-menu" type="button" aria-label="목차 열기" aria-expanded="false" aria-controls="sidebar">☰</button><a class="brand" href="#overview" aria-label="Jumpitnow 가이드 시작"><svg class="brand-mark" viewBox="0 0 32 32" fill="none" aria-hidden="true"><rect width="32" height="32" rx="9" fill="#2563eb"/><path d="M10 12v7c0 7 12 7 12 0v-7M10 15c-5-10 17-10 12 0" stroke="white" stroke-width="2.3" stroke-linecap="round"/><path d="M9 12l2 5M23 12l-2 5" stroke="#a6dce5" stroke-width="3.6" stroke-linecap="round"/></svg>jumpitnow</a><span class="brand-divider"></span><span class="top-title">Developer Guide</span><div class="top-actions"><a class="external" href="https://www.npmjs.com/package/@makeitnow/jumpitnow/v/0.1.0-alpha.4" target="_blank" rel="noopener noreferrer">npm 패키지 ↗</a><span class="version"><i class="dot"></i>v0.1.0-alpha.4</span><button class="print-btn" type="button" data-print>인쇄 / PDF ↗</button></div></header>
|
|
13
|
+
<aside class="sidebar" id="sidebar"><nav aria-label="가이드 목차"><div class="side-caption">시작하기</div><a href="#overview" class=active><span class="nav-icon" aria-hidden="true">⌂</span>SDK 소개</a><a href="#quickstart"><span class="nav-icon" aria-hidden="true">↗</span>빠르게 시작하기</a><a href="#flow"><span class="nav-icon" aria-hidden="true">⇄</span>연결과 운동 흐름</a><div class="side-caption">SDK 사용하기</div><a href="#modes"><span class="nav-icon" aria-hidden="true">◎</span>운동 모드</a><a href="#api"><span class="nav-icon" aria-hidden="true">⌘</span>API 레퍼런스</a><a href="#data"><span class="nav-icon" aria-hidden="true">▥</span>데이터 이해하기</a><a href="#events"><span class="nav-icon" aria-hidden="true">◇</span>이벤트 구독</a><a href="#results"><span class="nav-icon" aria-hidden="true">✓</span>결과와 오류</a><div class="side-caption">서비스 만들기</div><a href="#storage"><span class="nav-icon" aria-hidden="true">▤</span>기록 저장 설계</a><a href="#ai"><span class="nav-icon" aria-hidden="true">✧</span>AI 코딩 가이드</a><a href="#check"><span class="nav-icon" aria-hidden="true">☑</span>완성 전 체크</a></nav><div class="side-footer"><strong>JavaScript로 만드는<br>나만의 줄넘기 서비스</strong>프레임워크 독립 · 타입 정의 포함<br>이 문서는 공개 함수와 사용법을 안내합니다.</div></aside>
|
|
14
|
+
<main class="main"><div class="content"><div class="breadcrumb">문서 <span>/</span> SDK & API <span>/</span> <b>시작하기</b></div>
|
|
15
|
+
<section class="hero" id="overview" aria-labelledby="hero-title"><div class="hero-grid"><div><div class="eyebrow">JUMPITNOW / JAVASCRIPT SDK</div><h1 id="hero-title">한 번의 점프가,<br><em>하나의 데이터로.</em></h1><p class="hero-copy">줄넘기 연결부터 운동 데이터 수신까지.<br>익숙한 JavaScript 함수로 나만의 수업 화면,<br>운동 기록, 줄넘기 게임을 만들어 보세요.</p><div class="actions"><a href="#quickstart" class="btn primary">빠르게 시작하기 <span>↗</span></a><a href="#api" class="btn">API 살펴보기 <span>→</span></a></div></div><div class="hero-art"><svg viewBox="0 0 480 365" role="img" aria-labelledby="hero-svg-title hero-svg-desc"><title id="hero-svg-title">줄넘기에서 수업 데이터로</title><desc id="hero-svg-desc">줄넘기 모양의 곡선 안에 예시 운동 횟수 137회와 수신 데이터 카드를 표시합니다.</desc><defs><pattern id="dots" width="19" height="19" patternUnits="userSpaceOnUse"><circle cx="1" cy="1" r="1" fill="#dfe8f4"/></pattern></defs><rect x="32" y="19" width="418" height="312" rx="24" fill="#f4f8ff"/><rect x="32" y="19" width="418" height="312" rx="24" fill="url(#dots)"/><path d="M67 278V64H408V278Z" fill="none" stroke="#e0e9f7" stroke-width="1.5"/><path d="M76 82H405M76 268H405M240 64V278" stroke="#e3ecf8" fill="none"/><path d="M149 259C56 130 122 50 240 64S411 171 315 279" fill="none" stroke="#c5d9fb" stroke-width="11" stroke-linecap="round"/><path d="M149 259C56 130 122 50 240 64S411 171 315 279" fill="none" stroke="#4479ed" stroke-width="5" stroke-linecap="round"/><g transform="translate(141 248) rotate(-31)"><rect width="17" height="60" rx="8.5" fill="#193956"/><rect x="3" y="14" width="11" height="37" rx="5.5" fill="#e8775d"/><path d="M5 23H12M5 30H12M5 37H12" stroke="#f5b299" stroke-width="1.5"/></g><g transform="translate(305 264) rotate(25)"><rect width="17" height="60" rx="8.5" fill="#193956"/><rect x="3" y="14" width="11" height="37" rx="5.5" fill="#e8775d"/><path d="M5 23H12M5 30H12M5 37H12" stroke="#f5b299" stroke-width="1.5"/></g><rect x="150" y="120" width="167" height="125" rx="12" fill="#fff" stroke="#dce7f5"/><text x="171" y="145" fill="#8499b3" font-size="10" font-family="sans-serif" letter-spacing="1.5">JUMP COUNT</text><text x="169" y="203" fill="#173f68" font-size="54" font-family="sans-serif" font-weight="700" letter-spacing="-2">137<tspan font-size="13" fill="#8ba0b8" dx="7">회</tspan></text><circle cx="175" cy="227" r="3" fill="#34ad96"/><text x="185" y="231" fill="#728da8" font-size="10" font-family="sans-serif">measurement</text><rect x="16" y="209" width="114" height="57" rx="9" fill="#fff" stroke="#e0e8f2"/><circle cx="34" cy="226" r="3" fill="#36ab96"/><text x="44" y="230" font-size="10" font-family="sans-serif" fill="#6f879f">예시 데이터</text><text x="32" y="252" font-family="monospace" font-size="12" fill="#2b669d">count: 137</text><rect x="319" y="90" width="131" height="46" rx="9" fill="#2563eb"/><text x="334" y="109" font-family="monospace" font-size="9" fill="#adcbff">YOUR CLASSROOM</text><text x="334" y="125" font-family="sans-serif" font-size="11" fill="#fff">수업 화면에 연결됩니다 ↗</text><path d="M295 118l24-5M128 234l22-12" stroke="#9bbbea" stroke-dasharray="3 3"/><circle cx="98" cy="85" r="4" fill="#e97a5f"/><circle cx="391" cy="258" r="4" fill="#29a994"/></svg><div class="art-caption">SMART JUMP ROPE → YOUR NEXT IDEA</div></div></div>
|
|
16
|
+
<div class="install-bar"><span class="label">TERMINAL</span><code id="install-command">npm install --save-exact @makeitnow/jumpitnow@0.1.0-alpha.4</code><button class="copy" type="button" data-copy-target="install-command">복사</button></div><div class="alpha-note"><span class="tag">ALPHA</span><span>현재 버전은 개발 확인용입니다. 가상 장비로 먼저 개발할 수 있으며, 실제 장비 호환성 검증은 아직 진행하지 않았습니다.</span></div></section>
|
|
9
17
|
|
|
10
|
-
|
|
18
|
+
<section class="doc-section" id="concept"><div class="section-top"><div><div class="eyebrow">SDK AT A GLANCE</div><h2>줄넘기와 앱 사이, 필요한 함수만.</h2><p>장비의 운동 값을 받아서 어떤 경험을 만들지는 선생님의 앱에서 결정합니다.</p></div><span class="section-kicker">OVERVIEW</span></div>
|
|
19
|
+
<div class="architecture" role="group" aria-label="스마트 줄넘기, 게이트웨이, Jumpitnow SDK, 선생님의 앱이 순서대로 연결됩니다"><div class="diagram-labels"><span>운동을 측정하는 장비</span><span>연결 · 제어 · 측정값</span><span>선생님이 만드는 경험</span></div><div class="arch-row"><div class="arch-node"><div class="arch-box"><span class="pictogram" aria-hidden="true">∩</span><strong>스마트 줄넘기</strong></div><small>횟수 · 경과 시간</small></div><span class="arch-arrow" aria-hidden="true">↔</span><div class="arch-node"><div class="arch-box"><span class="pictogram" aria-hidden="true">▣</span><strong>게이트웨이</strong></div><small>사용할 장비 연결</small></div><span class="arch-arrow" aria-hidden="true">↔</span><div class="arch-node"><div class="arch-box sdk"><span class="pictogram mono" aria-hidden="true">{ }</span><strong>Jumpitnow SDK</strong></div><small>JavaScript 함수</small></div><span class="arch-arrow" aria-hidden="true">↔</span><div class="arch-node"><div class="arch-box"><span class="pictogram" aria-hidden="true">▤</span><strong>선생님의 앱</strong></div><small>수업 · 기록 · 게임</small></div></div><div class="boundary"><div><strong>SDK가 제공하는 것</strong><p>연결, 장비 검색, 운동 설정, 시작·정지, 측정값과 상태 알림.</p></div><div><strong>앱에서 만드는 것</strong><p>학생 배정, 로그인, 화면, 점수 계산, 기록 저장과 조회.</p></div></div></div></section>
|
|
11
20
|
|
|
12
|
-
|
|
13
|
-
|
|
21
|
+
<section class="doc-section" id="quickstart"><div class="section-top"><div><div class="eyebrow">QUICK START</div><h2>첫 연결을 만들어 보세요.</h2><p>앱 프로젝트에 패키지를 설치하세요. 장비가 없어도 가상 줄넘기로 전체 흐름을 실행할 수 있습니다.</p></div><span class="section-kicker">GET STARTED</span></div>
|
|
22
|
+
<div class="quick-meta"><span>설치·개발</span><code>Node.js 20+</code><span>실제 장비 사용</span><code>컴퓨터용 Chrome / Edge</code><code>HTTPS / localhost</code></div>
|
|
23
|
+
<div class="tabs" role="tablist" aria-label="시작 예제"><button type="button" role="tab" id="tab-practice" aria-controls="panel-practice" aria-selected="true" data-tab="practice">장비 없이 연습하기</button><button type="button" role="tab" id="tab-actual" aria-controls="panel-actual" aria-selected="false" tabindex="-1" data-tab="actual">실제 장비 연결하기</button></div><div class="quick-panel" id="panel-practice" role="tabpanel" aria-labelledby="tab-practice"><p class="mode-caption"><code>practice.mjs</code>로 저장한 다음 <code>node practice.mjs</code>를 실행하세요. 이 예제에서는 가상 장비 한 대를 사용합니다.</p><div class="code-card long-code" id=practice-code><div class="code-head"><span>practice.mjs · 실행 가능한 전체 예제</span><button type="button" class="copy" data-copy-code>복사</button></div><pre><code>import { createSimulatedGateway } from '@makeitnow/jumpitnow';
|
|
24
|
+
|
|
25
|
+
const gateway = createSimulatedGateway({ deviceCount: 1 });
|
|
26
|
+
const unsubscribe = gateway.on('measurement', value => {
|
|
27
|
+
console.log(`${value.count}회 / ${(value.elapsedMs / 1000).toFixed(1)}초`);
|
|
28
|
+
});
|
|
29
|
+
const unsubscribeError = gateway.on('error', error => {
|
|
30
|
+
console.error(error.code, error.message);
|
|
14
31
|
});
|
|
15
32
|
|
|
16
|
-
|
|
17
|
-
await gateway.connect();
|
|
18
|
-
const { devices } = await gateway.scan()
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
try {
|
|
34
|
+
await gateway.connect();
|
|
35
|
+
const { devices } = await gateway.scan();
|
|
36
|
+
// 이 예제에서만 가상 장비 한 대를 선택합니다.
|
|
37
|
+
// 실제 수업 앱에서는 사용자가 목록에서 직접 선택하게 만드세요.
|
|
38
|
+
const device = devices.find(item => item.state === 'available');
|
|
39
|
+
if (!device) throw new Error('사용할 줄넘기가 없습니다.');
|
|
40
|
+
const deviceIds = [device.deviceId];
|
|
41
|
+
|
|
42
|
+
const configured = await gateway.configure({ deviceIds, mode: { type: 'free' } });
|
|
43
|
+
if (!configured.ok) {
|
|
44
|
+
console.table(configured.results);
|
|
45
|
+
throw new Error('운동 설정 결과를 확인하세요.');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const started = await gateway.start({ deviceIds });
|
|
49
|
+
if (!started.ok) {
|
|
50
|
+
console.table(started.results);
|
|
51
|
+
throw new Error('시작 및 정지 복구 결과를 확인하세요.');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 예제에서는 약 3초 동안 수신합니다. 앱에서는 사용자의 정지 버튼을 사용합니다.
|
|
55
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
56
|
+
|
|
57
|
+
const stopped = await gateway.stop({ deviceIds });
|
|
58
|
+
console.table(stopped.results);
|
|
59
|
+
if (!stopped.ok) throw new Error('정지 여부를 확인하지 못한 장비가 있습니다.');
|
|
60
|
+
|
|
61
|
+
for (const result of stopped.results) {
|
|
62
|
+
const final = result.finalMeasurement;
|
|
63
|
+
if (final?.available && final.measurement) {
|
|
64
|
+
console.log('마지막 운동 값:', final.measurement);
|
|
65
|
+
} else {
|
|
66
|
+
console.log('정지는 확인했지만 마지막 운동 값은 확보하지 못했습니다.');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} finally {
|
|
70
|
+
try {
|
|
71
|
+
const closed = await gateway.disconnect();
|
|
72
|
+
if (!closed.closed || (closed.stopResults && !closed.stopResults.ok)) {
|
|
73
|
+
console.error('연결 종료 또는 장비 정지 상태를 확인하세요.', closed);
|
|
74
|
+
}
|
|
75
|
+
} finally {
|
|
76
|
+
unsubscribe();
|
|
77
|
+
unsubscribeError();
|
|
78
|
+
}
|
|
79
|
+
}</code></pre><button type=button class=expand-code aria-expanded=false>전체 코드 펼치기 ↓</button></div><div class="tip"><span class="tip-icon">i</span><div>가상 장비의 <code>simulated: true</code> 표시는 화면과 저장 데이터에 유지하세요. 실제 수업 기록과 구분하는 기준입니다.</div></div></div><div class="quick-panel" id="panel-actual" role="tabpanel" aria-labelledby="tab-actual" hidden><p class="mode-caption">아래는 웹앱의 버튼 연결 예시입니다. <code>connectButton</code>, <code>showDevicePicker</code>, <code>showMessage</code>는 앱에서 구현하는 화면 요소·함수입니다.</p><div class="code-card long-code"><div class="code-head"><span>앱의 연결 버튼에 붙이는 코드</span><button type="button" class="copy" data-copy-code>복사</button></div><pre><code>import { createGateway, getSupport } from '@makeitnow/jumpitnow';
|
|
80
|
+
|
|
81
|
+
const gateway = createGateway(); // 화면을 다시 그릴 때마다 새로 만들지 않습니다.
|
|
82
|
+
|
|
83
|
+
connectButton.onclick = async () => {
|
|
84
|
+
if (!getSupport().supported) {
|
|
85
|
+
showMessage('컴퓨터용 Chrome 또는 Edge에서 실행 환경을 확인해주세요.');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
connectButton.disabled = true;
|
|
89
|
+
try {
|
|
90
|
+
await gateway.connect(); // 사용자의 클릭에서 바로 호출합니다.
|
|
91
|
+
const { devices } = await gateway.scan();
|
|
92
|
+
showDevicePicker(devices); // 사용 가능한 장비를 보여주고 직접 선택받습니다.
|
|
93
|
+
} catch (error) {
|
|
94
|
+
showMessage(error.message);
|
|
95
|
+
} finally {
|
|
96
|
+
connectButton.disabled = false;
|
|
97
|
+
}
|
|
98
|
+
};</code></pre><button type=button class=expand-code aria-expanded=false>전체 코드 펼치기 ↓</button></div><div class="tip"><span class="tip-icon">i</span><div><strong>연결 버튼의 클릭 안에서 <code>connect()</code>를 호출하세요.</strong><br>검색 결과를 보여준 뒤 사용할 장비를 직접 선택받습니다. 연결·검색만으로 운동을 시작하지 않습니다.</div></div></div></section>
|
|
99
|
+
|
|
100
|
+
<section class="doc-section" id="flow"><div class="section-top"><div><div class="eyebrow">THE SESSION FLOW</div><h2>하나의 운동이 완성되는 순서.</h2><p>단계를 눌러 호출할 함수와 앱에서 처리할 일을 확인하세요. 비동기 작업은 순서대로 기다립니다.</p></div><span class="section-kicker">6 STEPS</span></div><div class="flow-steps" aria-label="운동 단계"><button class="flow-step active" type="button" data-step="0" aria-pressed="true"><span class="num">01</span><b>연결</b><small>connect()</small></button><button class="flow-step" type="button" data-step="1" aria-pressed="false"><span class="num">02</span><b>검색·선택</b><small>scan()</small></button><button class="flow-step" type="button" data-step="2" aria-pressed="false"><span class="num">03</span><b>운동 설정</b><small>configure()</small></button><button class="flow-step" type="button" data-step="3" aria-pressed="false"><span class="num">04</span><b>시작·수신</b><small>start() / on()</small></button><button class="flow-step" type="button" data-step="4" aria-pressed="false"><span class="num">05</span><b>정지·저장</b><small>stop()</small></button><button class="flow-step" type="button" data-step="5" aria-pressed="false"><span class="num">06</span><b>연결 종료</b><small>disconnect()</small></button></div><div class="flow-detail"><div class="flow-text"><h3 id="flow-title"></h3><p id="flow-desc"></p><div class="owner" id="flow-owner"></div></div><div class="code-card " id=flow-code><div class="code-head"><span>JavaScript · 호출 흐름</span><button type="button" class="copy" data-copy-code>복사</button></div><pre><code></code></pre></div></div><div class="tip"><span class="tip-icon">↳</span><div><strong>시작한 장비 목록을 보관하세요.</strong> 운동 중 화면의 선택이 달라져도 정지에는 처음 시작한 <code>deviceIds</code>를 사용합니다.</div></div></section>
|
|
101
|
+
|
|
102
|
+
<section class="doc-section" id="modes"><div class="section-top"><div><div class="eyebrow">EXERCISE MODES</div><h2>수업에 맞는 운동 방식.</h2><p>모드를 선택하면 configure()에 전달할 코드가 바뀝니다. 목표값은 장비의 지원 범위 안에서 정하세요.</p></div><span class="section-kicker">CONFIGURE</span></div>
|
|
103
|
+
<div class="mode-grid"><button class="mode-option" type="button" data-mode="free" aria-pressed="true"><span class="mode-icon">∞</span><strong>자유 운동</strong><p>목표 없이 자유롭게<br>횟수와 시간을 확인합니다.</p></button><button class="mode-option" type="button" data-mode="count" aria-pressed="false"><span class="mode-icon">#</span><strong>목표 횟수</strong><p>예: 100회 도전<br>정한 횟수를 목표로 합니다.</p></button><button class="mode-option" type="button" data-mode="time" aria-pressed="false"><span class="mode-icon">◷</span><strong>목표 시간</strong><p>예: 60초 운동<br>정한 시간을 목표로 합니다.</p></button></div><div class="mode-controls" id="mode-controls" hidden><label for="mode-value" id="mode-label">목표 횟수</label><input id="mode-value" type="number" min="1" step="1" value="100" inputmode="numeric" aria-describedby="mode-error"><span id="mode-unit">회</span><span class="invalid" id="mode-error" role="status"></span></div><div class="code-card " id=mode-code><div class="code-head"><span>JavaScript · mode</span><button type="button" class="copy" data-copy-code>복사</button></div><pre><code></code></pre></div><p class="mode-caption">지원 목록은 <code>capabilities.modes</code>, 상한은 <code>maxTargetCount</code>와 <code>maxDurationSeconds</code>로 확인합니다. 목표 달성 화면과 기록 종료·저장은 앱에서 구현합니다.</p></section>
|
|
104
|
+
|
|
105
|
+
<section class="doc-section" id="api"><div class="section-top"><div><div class="eyebrow">API REFERENCE</div><h2>필요한 함수를 바로 찾으세요.</h2><p>함수를 펼치면 입력값, 반환값, 사용 예시를 볼 수 있습니다. 검색은 함수명과 한국어 설명을 함께 찾습니다.</p></div><span class="section-kicker">18 ENTRIES</span></div>
|
|
106
|
+
<div class="api-toolbar"><div class="search-wrap"><span aria-hidden="true">⌕</span><input type="search" id="api-search" placeholder="함수 검색 · 예: start, 배터리, 측정값" aria-label="API 함수 검색"><kbd aria-hidden="true">/</kbd></div><span class="api-count" id="api-count" role="status"></span></div><div class="chips" aria-label="API 분류"><button class="chip" type="button" data-category="전체" aria-pressed="true">전체</button><button class="chip" type="button" data-category="준비" aria-pressed="false">준비</button><button class="chip" type="button" data-category="연결·장비" aria-pressed="false">연결·장비</button><button class="chip" type="button" data-category="운동" aria-pressed="false">운동</button><button class="chip" type="button" data-category="데이터" aria-pressed="false">데이터</button><button class="chip" type="button" data-category="진단" aria-pressed="false">진단</button></div><div id="api-list"></div><div class="empty" id="api-empty" hidden>일치하는 함수가 없습니다. 검색어를 바꾸거나 “전체” 분류를 선택하세요.</div><noscript><div class="no-js">API 목록의 검색·펼치기는 JavaScript가 필요합니다. <a href="API.md">함수 사용법 문서</a>를 확인하세요.</div></noscript>
|
|
107
|
+
<details class="advanced"><summary>인스턴스의 공통 옵션 보기 · GatewayOptions</summary><div class="table-wrap"><table><thead><tr><th>옵션</th><th>기본값 / 범위</th><th>설명</th></tr></thead><tbody><tr><td><code>requestTimeoutMs</code></td><td>1500 / 50~30000ms</td><td>요청 결과를 기다리는 시간</td></tr><tr><td><code>scanWindowMs</code></td><td>2000 / 50~30000ms</td><td>기본 검색 시간</td></tr><tr><td><code>pollIntervalMs</code></td><td>250 / 20~30000ms</td><td>운동 중 자동 조회 간격</td></tr><tr><td><code>staleAfterMs</code></td><td>5000 / 50~300000ms</td><td>오래된 값으로 판단하는 기준</td></tr><tr><td><code>identifySignals</code></td><td>[] · 기본 비활성</td><td>실제 제품에서 검증한 위치 확인 신호만 활성화</td></tr></tbody></table></div><p class="mode-caption">숫자 옵션은 정수입니다. 기본값부터 시작하고, 수업 환경과 실제 장비 확인 후 조정하세요.</p></details></section>
|
|
108
|
+
|
|
109
|
+
<section class="doc-section" id="data"><div class="section-top"><div><div class="eyebrow">UNDERSTAND YOUR DATA</div><h2>데이터 한 건을 읽는 방법.</h2><p>“다음 값 받기”를 눌러 같은 운동 구간의 누적값이 화면을 어떻게 바꾸는지 확인하세요.</p></div><span class="section-kicker">MEASUREMENT</span></div>
|
|
110
|
+
<div class="data-lab"><div class="score-panel"><div class="score-top"><span>줄넘기 01 · 현재 구간</span><span class="practice-badge">예시 데이터</span></div><div class="score-big"><strong id="lab-count">10</strong><span>회</span></div><div class="score-time" id="lab-time">5.0초 · elapsedMs / 1000</div><div class="sample-track" aria-label="예시 수신 순서"><span data-sample="0" class="current">10</span>→<span data-sample="1">12</span>→<span data-sample="2">15</span></div><p id="lab-explainer">첫 측정값입니다. 화면에 10회를 표시합니다.</p><div class="lab-buttons"><button type="button" class="btn primary" id="lab-next">다음 값 받기 →</button><button type="button" class="btn" id="lab-reset">처음부터</button></div><p class="lab-caption">읽는 방법을 보여주는 예시입니다. SDK·장비에 연결하지 않습니다.</p></div><div class="payload-panel"><div class="code-card " id=payload-code><div class="code-head"><span>measurement · 예시 객체</span><button type="button" class="copy" data-copy-code>복사</button></div><pre><code></code></pre></div></div></div><div class="data-rule"><span><strong>누적 횟수는 덧셈이 아니라 갱신입니다.</strong><br>같은 구간의 10 → 12 → 15는 최종 15회입니다.</span><code>currentCount = value.count</code></div><div class="table-wrap"><table class="field-table"><thead><tr><th>필드</th><th>타입 / 단위</th><th>의미 · 행을 눌러 자세히 보기</th></tr></thead><tbody><tr data-field="0" tabindex="0" role="button" aria-label="deviceId 설명 보기"><td><code>deviceId</code></td><td>string</td><td>장비 구분 키</td></tr><tr data-field="1" tabindex="0" role="button" aria-label="displayNumber 설명 보기"><td><code>displayNumber</code></td><td>number | null</td><td>화면에 보여줄 장비 번호</td></tr><tr data-field="2" tabindex="0" role="button" aria-label="sessionId 설명 보기"><td><code>sessionId</code></td><td>string</td><td>장비별 운동 식별값</td></tr><tr data-field="3" tabindex="0" role="button" aria-label="segmentId 설명 보기"><td><code>segmentId</code></td><td>string</td><td>운동 내 구간 식별값</td></tr><tr data-field="4" tabindex="0" role="button" aria-label="count 설명 보기"><td><code>count</code></td><td>number</td><td>현재 구간의 누적 횟수</td></tr><tr data-field="5" tabindex="0" role="button" aria-label="elapsedMs 설명 보기"><td><code>elapsedMs</code></td><td>number · ms</td><td>구간의 경과 시간</td></tr><tr data-field="6" tabindex="0" role="button" aria-label="pauseCount 설명 보기"><td><code>pauseCount</code></td><td>number | null</td><td>멈춤 횟수</td></tr><tr data-field="7" tabindex="0" role="button" aria-label="receivedAt 설명 보기"><td><code>receivedAt</code></td><td>number · Unix ms</td><td>앱이 수신한 시각</td></tr><tr data-field="8" tabindex="0" role="button" aria-label="source 설명 보기"><td><code>source</code></td><td>'live' | 'cache'</td><td>이번 수신값 또는 마지막 보관값</td></tr><tr data-field="9" tabindex="0" role="button" aria-label="stale 설명 보기"><td><code>stale</code></td><td>boolean</td><td>현재 상태를 다시 확인해야 하는 값</td></tr><tr data-field="10" tabindex="0" role="button" aria-label="simulated 설명 보기"><td><code>simulated</code></td><td>boolean</td><td>연습용 가상 데이터 여부</td></tr></tbody></table></div><div class="field-detail" id="field-detail" aria-live="polite"></div></section>
|
|
111
|
+
|
|
112
|
+
<section class="doc-section" id="events"><div class="section-top"><div><div class="eyebrow">EVENTS</div><h2>데이터와 상태를 함께 구독하세요.</h2><p>SDK는 네 가지 이벤트를 제공합니다. 화면을 다시 열 때 구독이 중복되지 않도록 사용 후 해제합니다.</p></div><span class="section-kicker">4 EVENTS</span></div>
|
|
113
|
+
<div class="event-grid"><article class="event-card"><code>measurement</code><p>측정값으로 횟수와 시간을 갱신합니다.</p><div class="event-shape">Measurement</div></article><article class="event-card"><code>deviceChanged</code><p>장비별 사용 가능·운동 중·연결 상태를 표시합니다.</p><div class="event-shape">{ device, previousState }</div></article><article class="event-card"><code>connectionChanged</code><p>연결이 바뀌거나 끊긴 상태를 화면에 보여줍니다.</p><div class="event-shape">{ state, previousState }</div></article><article class="event-card"><code>error</code><p>오류 코드와 메시지를 확인하고 다음 행동을 안내합니다.</p><div class="event-shape">{ code, message, recoverable, ... }</div></article></div><div class="code-card "><div class="code-head"><span>JavaScript · 구독과 정리</span><button type="button" class="copy" data-copy-code>복사</button></div><pre><code>const subscriptions = [
|
|
114
|
+
gateway.on('measurement', value => console.log(value.count)),
|
|
115
|
+
gateway.on('deviceChanged', ({ device }) => console.log(device.state)),
|
|
116
|
+
gateway.on('connectionChanged', ({ state }) => console.log(state)),
|
|
117
|
+
gateway.on('error', error => console.error(error.code, error.message))
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
// 명시적으로 운동·연결 종료를 처리한 뒤, 화면 사용을 마칠 때
|
|
121
|
+
subscriptions.forEach(unsubscribe => unsubscribe());</code></pre></div><p class="mode-caption">기본 화면은 <code>measurement</code>를 구독하면 됩니다. 별도의 반복 읽기를 중복 구현하지 마세요. 이벤트 오류 처리와 함수 호출의 <code>try/catch</code>는 둘 다 필요합니다.</p></section>
|
|
122
|
+
|
|
123
|
+
<section class="doc-section" id="results"><div class="section-top"><div><div class="eyebrow">RESULTS & RECOVERY</div><h2>호출이 끝났다면, 결과를 확인하세요.</h2><p>configure · start · stop · identify는 전체 ok와 장비별 results를 반환합니다. 예외가 없더라도 ok가 false일 수 있습니다.</p></div><span class="section-kicker">COMMAND RESULT</span></div>
|
|
124
|
+
<div class="results-grid"><article class="result-card"><span class="status-label">● 요청 확인</span><code>acknowledged</code><p>해당 장비의 요청이 확인됐습니다.</p></article><article class="result-card"><span class="status-label">● 처리 실패</span><code>failed</code><p>오류 정보와 입력값·장비 상태를 확인합니다.</p></article><article class="result-card"><span class="status-label">● 확인 필요</span><code>unknown</code><p>성공으로 표시하지 않고 장비를 확인합니다.</p></article></div><div class="result-switch"><span>응답 예시</span><button class="chip" type="button" data-result="success" aria-pressed="true">모두 확인</button><button class="chip" type="button" data-result="partial" aria-pressed="false">일부 시작 실패</button><button class="chip" type="button" data-result="unknown" aria-pressed="false">정지 결과 미확인</button></div><div class="code-card " id=result-code><div class="code-head"><span>CommandResult · 예시</span><button type="button" class="copy" data-copy-code>복사</button></div><pre><code></code></pre></div><p class="result-message" id="result-message" aria-live="polite"></p><div class="tip"><span class="tip-icon">!</span><div><strong>정지 확인 ≠ 마지막 값 확보 ≠ 기록 저장.</strong><br><code>ok</code>, <code>finalMeasurement.available</code>, 앱의 저장 결과를 각각 확인합니다. 마지막 값이 없으면 이전 값을 확정값으로 꾸미지 않습니다.</div></div><div class="error-grid"><article class="error-card"><strong>환경 확인</strong><code>UNSUPPORTED_BROWSER · INSECURE_CONTEXT</code><p>컴퓨터용 Chrome/Edge와 HTTPS 또는 localhost 환경을 확인하세요.</p></article><article class="error-card"><strong>연결 선택</strong><code>USER_CANCELLED · PORT_UNAVAILABLE</code><p>취소는 사용자의 선택으로 처리하고, 장비 사용 상태를 확인한 뒤 다시 연결하세요.</p></article><article class="error-card"><strong>입력·순서</strong><code>INVALID_ARGUMENT · INVALID_STATE · BUSY</code><p>값과 호출 순서를 확인하세요. 명령 처리 중에는 버튼을 잠그고 순서대로 await합니다.</p></article><article class="error-card"><strong>장비 확인</strong><code>DEVICE_OFFLINE · AMBIGUOUS_DEVICE</code><p>연결 상태와 중복 표시 번호를 확인하고, 수업 대상을 다시 선택하세요.</p></article><article class="error-card"><strong>기능 확인</strong><code>UNSUPPORTED_GATEWAY · UNSUPPORTED_FEATURE</code><p>사용 가능한 제품과 장비의 capabilities를 확인하세요.</p></article><article class="error-card"><strong>결과 미확인</strong><code>NOT_CONNECTED · TIMEOUT · CONNECTION_LOST · DEVICE_REJECTED · PROTOCOL_ERROR</code><p>장비 상태와 장비별 결과를 확인하세요. 시작을 자동으로 반복하지 않습니다.</p></article></div></section>
|
|
125
|
+
|
|
126
|
+
<section class="doc-section" id="storage"><div class="section-top"><div><div class="eyebrow">BUILD A RECORDING APP</div><h2>수신한 값이 수업 기록이 되기까지.</h2><p>SDK가 전달한 값을 앱의 기록 구조로 보관합니다. 로그인, 학생 명단, 데이터베이스와 저장 API는 앱에서 구현합니다.</p></div><span class="section-kicker">APP RESPONSIBILITY</span></div>
|
|
127
|
+
<div class="storage-flow"><div class="storage-step"><span class="mono">01 / PREPARE</span><strong>앱 기록 ID 생성</strong><p>시작 전 대상·모드와<br>고유 recordId를 보관합니다.</p></div><div class="storage-step"><span class="mono">02 / UPDATE</span><strong>구간별 값 갱신</strong><p>장비 + 운동 + 구간으로<br>마지막 누적값을 갱신합니다.</p></div><div class="storage-step"><span class="mono">03 / CONFIRM</span><strong>정지 결과 기록</strong><p>정지 상태와 마지막 값<br>확보 여부를 따로 남깁니다.</p></div><div class="storage-step"><span class="mono">04 / SAVE</span><strong>저장 성공 확인</strong><p>앱의 저장 응답을 받은 뒤<br>저장 완료를 표시합니다.</p></div></div><div class="storage-split"><div class="storage-tree"><div class="tree-root">record <small>· 앱에서 정하는 예시 구조</small></div><div class="tree-branch"><div>recordId</div><div>startedAt / endedAt</div><div>mode / simulated</div><div>devices[]</div><div class="tree-branch"><div>deviceId / displayNumber</div><div>stopStatus</div><div>finalMeasurementAvailable</div><div>segments[]</div><div class="tree-branch"><div>sessionId / segmentId</div><div>lastMeasurement</div></div></div></div></div><div class="storage-notes"><h3>기록이 틀어지지 않게</h3><ul><li><strong>같은 구간은 갱신합니다.</strong><br>deviceId + sessionId + segmentId로 구분하고, 누적 횟수를 반복 합산하지 않습니다.</li><li><strong>확인하지 못한 값도 상태로 남깁니다.</strong><br>정지 미확인·마지막 값 없음·오래된 값을 구분하세요.</li><li><strong>저장 재시도에는 같은 기록 ID를 씁니다.</strong><br>실패한 기록을 유지하고 서버에서도 중복 저장을 막습니다.</li><li><strong>학생 배정과 접근 권한은 앱에서 관리합니다.</strong><br>저장소 비밀키는 브라우저 코드에 넣지 않습니다.</li></ul></div></div></section>
|
|
128
|
+
|
|
129
|
+
<section class="doc-section" id="ai"><div class="section-top"><div><div class="eyebrow">BUILD WITH AI</div><h2>아이디어를 설명하면, 구현 기준은 여기서.</h2><p>아래 요청문과 이 가이드 또는 패키지의 README를 AI 코딩 도구에 함께 전달하세요. 원하는 서비스에 맞게 수정할 수 있습니다.</p></div><span class="section-kicker">VIBE CODING</span></div>
|
|
130
|
+
<div class="ai-panel"><div class="ai-top"><div><h3>나만의 서비스 만들기</h3><p>서비스 선택 → 요청문 복사 → AI 코딩 도구에 전달</p></div><button type="button" class="btn primary" id="copy-prompt">요청문 복사 ↗</button></div><div class="recipe-buttons" aria-label="만들 서비스 선택"><button type="button" data-recipe="줄넘기 수업 기록장" aria-pressed="true">운동 기록장</button><button type="button" data-recipe="여러 줄넘기의 횟수와 시간을 보여주는 교실 현황판" aria-pressed="false">교실 현황판</button><button type="button" data-recipe="현재 운동 구간의 횟수 증가를 점수로 바꾸는 줄넘기 게임" aria-pressed="false">줄넘기 게임</button></div><label class="quick-meta" for="ai-prompt">AI에게 전달할 요청문 · 직접 수정 가능</label><textarea class="prompt-text" id="ai-prompt" spellcheck="false">Jumpitnow SDK를 사용해서 줄넘기 수업 기록장을 만들어줘.
|
|
131
|
+
첨부한 README와 docs/API.md를 함수 사용의 기준으로 삼아줘.
|
|
132
|
+
|
|
133
|
+
1. 개발 방식
|
|
134
|
+
- 기존 프로젝트의 화면 구성과 프레임워크를 먼저 확인해줘.
|
|
135
|
+
- SDK는 @makeitnow/jumpitnow의 공개 함수로만 사용해줘.
|
|
136
|
+
- 문서에 없는 함수, 이벤트, 필드를 임의로 만들지 마.
|
|
137
|
+
- npm에서 해당 버전을 조회할 수 있으면 버전을 고정해 설치해줘.
|
|
138
|
+
등록 전이거나 공개 처리 중이면 전달받은 .tgz로 설치해줘.
|
|
139
|
+
- 처음에는 createSimulatedGateway({ deviceCount: 3 })로 개발해줘.
|
|
140
|
+
- 화면에 '연습 모드'를 표시하고 가상 기록과 실제 기록을 구분해줘.
|
|
141
|
+
- 실제 장비 모드에서는 createGateway()를 사용하고,
|
|
142
|
+
사용자가 연결 버튼을 눌렀을 때 connect()를 호출해줘.
|
|
143
|
+
|
|
144
|
+
2. 화면과 운동 흐름
|
|
145
|
+
- 연결 → 검색 → 장비 직접 선택 → 운동 설정 → 시작 → 데이터 표시
|
|
146
|
+
→ 정지 확인 → 기록 저장 → 연결 종료 순서로 만들어줘.
|
|
147
|
+
- 검색 결과 전체를 자동으로 운동 대상으로 선택하지 마.
|
|
148
|
+
- 자유 운동, 목표 횟수, 목표 시간 중 하나를 선택하게 해줘.
|
|
149
|
+
- configure()의 ok가 true일 때만 start()를 호출해줘.
|
|
150
|
+
- 시작에 전달한 장비 목록은 별도로 보관하고, 정지할 때도 사용해줘.
|
|
151
|
+
- 명령 처리 중에는 중복 클릭을 막고, 운동 중에는 대상과 모드 변경을 막아줘.
|
|
152
|
+
- 결과의 ok와 장비별 results를 모두 확인해줘.
|
|
153
|
+
- failed와 unknown, 일부 시작 실패 후 recovery 결과를 화면에 구분해줘.
|
|
154
|
+
- 정지 여부를 확인하지 못하면 현장 확인을 안내하고 자동 재시작하지 마.
|
|
155
|
+
|
|
156
|
+
3. 데이터와 저장
|
|
157
|
+
- measurement 이벤트의 count는 누적값이므로 기존 횟수에 더하지 마.
|
|
158
|
+
- deviceId + sessionId + segmentId별로 측정값을 구분해줘.
|
|
159
|
+
- sessionId는 장비별 운동 식별값이므로 수업 전체에는 앱의 별도 기록 ID를 써줘.
|
|
160
|
+
- elapsedMs는 밀리초, receivedAt은 수신 시각이야. 둘을 혼동하지 마.
|
|
161
|
+
- 오래된 값과 연결 끊김을 표시하고 마지막 값을 최신처럼 표시하지 마.
|
|
162
|
+
- stop()의 정지 결과와 finalMeasurement 확보 여부를 따로 확인해줘.
|
|
163
|
+
- 저장은 앱에서 구현해줘. SDK에 save(), login() 같은 함수를 만들지 마.
|
|
164
|
+
- 저장 실패 시 기록을 유지하고 같은 기록 ID로 재시도해 중복을 막아줘.
|
|
165
|
+
- 연습용 데이터의 simulated 표시를 저장할 때도 유지해줘.
|
|
166
|
+
|
|
167
|
+
4. 완료 확인
|
|
168
|
+
- 연결 취소, 선택한 장비 없음, 일부 시작 실패, 연결 끊김,
|
|
169
|
+
마지막 측정값 없음, 저장 실패 상황을 확인해줘.
|
|
170
|
+
- 이벤트 구독을 중복 등록하지 말고 사용이 끝나면 해제해줘.
|
|
171
|
+
- 실행 방법, 변경한 파일, 확인한 동작과 실제 장비 검증이 남은 부분을 알려줘.</textarea><p class="lab-caption">이 문서는 AI 서비스에 내용을 전송하지 않습니다. 복사한 요청문을 사용하는 도구에 직접 붙여 넣으세요.</p></div></section>
|
|
172
|
+
|
|
173
|
+
<section class="doc-section" id="check"><div class="section-top"><div><div class="eyebrow">BEFORE YOU SHARE</div><h2>수업에 가져가기 전, 마지막 점검.</h2><p>연습 모드로 화면을 확인한 뒤 실제 사용할 장비와 환경에서 확인하세요. 체크는 이 페이지를 열어 둔 동안만 유지됩니다.</p></div><span class="section-kicker">RELEASE CHECK</span></div>
|
|
174
|
+
<ul class="checklist"><li><label><input type="checkbox" class="check-item"><span>연습 모드에서 연결·검색·선택·시작·수신·정지가 됩니다.</span></label></li><li><label><input type="checkbox" class="check-item"><span>선택하지 않은 장비를 시작하지 않고 중복 클릭을 막습니다.</span></label></li><li><label><input type="checkbox" class="check-item"><span>설정 실패·일부 시작 실패·정지 미확인을 구분합니다.</span></label></li><li><label><input type="checkbox" class="check-item"><span>누적 횟수와 반복 수신값을 중복 합산하지 않습니다.</span></label></li><li><label><input type="checkbox" class="check-item"><span>연결이 끊기면 오래된 값을 표시하고 자동 재시작하지 않습니다.</span></label></li><li><label><input type="checkbox" class="check-item"><span>마지막 값 없음과 저장 실패를 성공으로 표시하지 않습니다.</span></label></li><li><label><input type="checkbox" class="check-item"><span>같은 기록 ID로 재시도해 저장 중복을 방지합니다.</span></label></li><li><label><input type="checkbox" class="check-item"><span>화면을 다시 열어도 이벤트 구독이 중복되지 않습니다.</span></label></li><li><label><input type="checkbox" class="check-item"><span>연습 기록을 실제 수업 기록과 구분합니다.</span></label></li><li><label><input type="checkbox" class="check-item"><span>실제 장비의 연결·횟수·시간·정지 결과를 확인합니다.</span></label></li></ul><div class="check-progress" id="check-progress" role="status">0 / 10 항목 확인</div></section>
|
|
175
|
+
<div class="finish"><div><h3>이제, 선생님의 아이디어를 연결하세요.</h3><p>가상 줄넘기로 시작해 화면과 기록 흐름을 먼저 만들어 보세요.</p></div><a class="btn primary" href="#quickstart">첫 예제로 돌아가기 ↗</a></div><footer class="footer"><div><strong>jumpitnow</strong> SDK & API Guide · 2026.10.11</div><div>기준 버전 0.1.0-alpha.4 · 이용 조건 UNLICENSED / 별도 정책 안내</div></footer></div></main><div class="toast" role="status" hidden></div>
|
|
176
|
+
<script>const api=[
|
|
177
|
+
{
|
|
178
|
+
"name": "createGateway",
|
|
179
|
+
"group": "준비",
|
|
180
|
+
"kind": "동기",
|
|
181
|
+
"sig": "createGateway(options?)",
|
|
182
|
+
"desc": "실제 장비와 함께 사용할 SDK 인스턴스를 만듭니다. 화면 수명 동안 하나를 유지하세요.",
|
|
183
|
+
"returns": "Gateway",
|
|
184
|
+
"params": [
|
|
185
|
+
[
|
|
186
|
+
"options",
|
|
187
|
+
"GatewayOptions · 선택",
|
|
188
|
+
"요청 대기, 검색 시간 등. 아래 공통 옵션 참조."
|
|
189
|
+
]
|
|
190
|
+
],
|
|
191
|
+
"example": "const gateway = createGateway();",
|
|
192
|
+
"note": "생성만으로 연결되지는 않습니다. 사용자의 연결 버튼에서 connect()를 호출하세요."
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"name": "createSimulatedGateway",
|
|
196
|
+
"group": "준비",
|
|
197
|
+
"kind": "동기",
|
|
198
|
+
"sig": "createSimulatedGateway(options?)",
|
|
199
|
+
"desc": "장비 없이 화면과 운동 흐름을 개발할 수 있는 연습용 인스턴스를 만듭니다.",
|
|
200
|
+
"returns": "SimulatedGateway",
|
|
201
|
+
"params": [
|
|
202
|
+
[
|
|
203
|
+
"deviceCount",
|
|
204
|
+
"number · 선택",
|
|
205
|
+
"가상 장비 수. 기본 3, 1~40."
|
|
206
|
+
],
|
|
207
|
+
[
|
|
208
|
+
"seed",
|
|
209
|
+
"number · 선택",
|
|
210
|
+
"가상 값 생성 기준. 기본 1."
|
|
211
|
+
],
|
|
212
|
+
[
|
|
213
|
+
"responseDelayMs",
|
|
214
|
+
"number · 선택",
|
|
215
|
+
"가상 응답 지연. 기본 10ms."
|
|
216
|
+
],
|
|
217
|
+
[
|
|
218
|
+
"startFailures",
|
|
219
|
+
"number[] · 선택",
|
|
220
|
+
"시작 실패를 재현할 가상 장비의 표시 번호."
|
|
221
|
+
]
|
|
222
|
+
],
|
|
223
|
+
"example": "const gateway = createSimulatedGateway({\n deviceCount: 3, startFailures: [2]\n});",
|
|
224
|
+
"note": "공통 옵션도 사용할 수 있습니다. simulated 표시를 화면과 저장 데이터에 유지하세요."
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
"name": "getSupport",
|
|
228
|
+
"group": "준비",
|
|
229
|
+
"kind": "동기",
|
|
230
|
+
"sig": "getSupport()",
|
|
231
|
+
"desc": "현재 브라우저에서 실제 장비 연결 기능을 사용할 수 있는지 확인합니다.",
|
|
232
|
+
"returns": "Support · supported, secureContext, reason 등",
|
|
233
|
+
"params": [],
|
|
234
|
+
"example": "const support = getSupport();\nif (!support.supported) {\n console.log(support.reason);\n}",
|
|
235
|
+
"note": "supported는 브라우저 환경 확인입니다. 장비의 실제 호환성 검증을 뜻하지 않습니다."
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"name": "SDKError",
|
|
239
|
+
"group": "준비",
|
|
240
|
+
"kind": "오류 클래스",
|
|
241
|
+
"sig": "SDKError",
|
|
242
|
+
"desc": "함수 호출에서 발생한 오류를 구분할 때 사용합니다.",
|
|
243
|
+
"returns": "ErrorInfo · code, message, recoverable, deviceId?, requestId?",
|
|
244
|
+
"params": [],
|
|
245
|
+
"example": "try {\n await gateway.scan();\n} catch (error) {\n if (error instanceof SDKError) {\n console.error(error.code, error.message);\n } else {\n throw error;\n }\n}",
|
|
246
|
+
"note": "recoverable: true를 자동 재시작 허용으로 해석하지 마세요. 앱에서도 발생할 수 있는 다른 오류를 처리하세요."
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
"name": "connect",
|
|
250
|
+
"group": "연결·장비",
|
|
251
|
+
"kind": "비동기",
|
|
252
|
+
"sig": "gateway.connect()",
|
|
253
|
+
"desc": "연결 선택과 연결 완료를 처리합니다.",
|
|
254
|
+
"returns": "Promise<GatewayInfo>",
|
|
255
|
+
"params": [],
|
|
256
|
+
"example": "await gateway.connect();\nconst info = gateway.getInfo();",
|
|
257
|
+
"note": "실제 장비는 사용자가 누른 연결 버튼에서 호출합니다. 연결 완료 후 검색하세요."
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
"name": "scan",
|
|
261
|
+
"group": "연결·장비",
|
|
262
|
+
"kind": "비동기",
|
|
263
|
+
"sig": "gateway.scan({ timeoutMs }?)",
|
|
264
|
+
"desc": "장비를 검색하고 사용할 수 있는 장비 목록을 받습니다.",
|
|
265
|
+
"returns": "Promise<{ devices: Device[], status: \"complete\", windowMs: number }>",
|
|
266
|
+
"params": [
|
|
267
|
+
[
|
|
268
|
+
"timeoutMs",
|
|
269
|
+
"number · 선택",
|
|
270
|
+
"검색 시간. 50~30000ms, 생략하면 scanWindowMs."
|
|
271
|
+
]
|
|
272
|
+
],
|
|
273
|
+
"example": "const { devices, status } = await gateway.scan();\n// devices를 화면에 표시하고 사용할 장비를 직접 선택받습니다.\nconsole.table(devices);",
|
|
274
|
+
"note": "배열 자체가 아니라 객체를 반환합니다. 검색 결과 전체를 자동 시작하지 않습니다."
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
"name": "getDevices",
|
|
278
|
+
"group": "연결·장비",
|
|
279
|
+
"kind": "동기",
|
|
280
|
+
"sig": "gateway.getDevices()",
|
|
281
|
+
"desc": "현재 SDK가 알고 있는 장비 목록을 가져옵니다.",
|
|
282
|
+
"returns": "Device[]",
|
|
283
|
+
"params": [],
|
|
284
|
+
"example": "const devices = gateway.getDevices();\nconst ready = devices.filter(d => d.state === 'available');",
|
|
285
|
+
"note": "새 검색을 실행하지 않습니다. 장비 선택에는 deviceId를 사용하며, displayNumber는 화면 표시용입니다."
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
"name": "getInfo",
|
|
289
|
+
"group": "연결·장비",
|
|
290
|
+
"kind": "동기",
|
|
291
|
+
"sig": "gateway.getInfo()",
|
|
292
|
+
"desc": "연결 상태, SDK 버전과 가상 장비 여부 등 현재 연결 정보를 읽습니다.",
|
|
293
|
+
"returns": "GatewayInfo · connection, sdkVersion, simulated, features 등",
|
|
294
|
+
"params": [],
|
|
295
|
+
"example": "const { connection, sdkVersion, simulated } = gateway.getInfo();",
|
|
296
|
+
"note": "연결 상태는 disconnected / connecting / connected / disconnecting / lost 중 하나입니다."
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
"name": "disconnect",
|
|
300
|
+
"group": "연결·장비",
|
|
301
|
+
"kind": "비동기",
|
|
302
|
+
"sig": "gateway.disconnect()",
|
|
303
|
+
"desc": "연결을 종료하고 종료 과정의 정지 결과를 받습니다.",
|
|
304
|
+
"returns": "Promise<{ closed: boolean, stopResults: CommandResult | null }>",
|
|
305
|
+
"params": [],
|
|
306
|
+
"example": "const result = await gateway.disconnect();\nif (!result.closed || (result.stopResults && !result.stopResults.ok)) {\n console.error('연결 종료 또는 장비 정지 상태를 확인하세요.', result);\n}",
|
|
307
|
+
"note": "closed는 연결 종료 여부입니다. 장비 정지 성공 여부와 구분하세요. 페이지 종료에 정지 처리를 맡기지 마세요."
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
"name": "configure",
|
|
311
|
+
"group": "운동",
|
|
312
|
+
"kind": "비동기",
|
|
313
|
+
"sig": "gateway.configure({ deviceIds, mode })",
|
|
314
|
+
"desc": "선택한 장비에 자유·목표 횟수·목표 시간 중 하나의 운동 방식을 설정합니다.",
|
|
315
|
+
"returns": "Promise<CommandResult>",
|
|
316
|
+
"params": [
|
|
317
|
+
[
|
|
318
|
+
"deviceIds",
|
|
319
|
+
"string[] · 필수",
|
|
320
|
+
"검색 결과에서 선택한 장비 키. 빈 배열·중복 불가."
|
|
321
|
+
],
|
|
322
|
+
[
|
|
323
|
+
"mode",
|
|
324
|
+
"Mode · 필수",
|
|
325
|
+
"free / count + targetCount / time + durationSeconds."
|
|
326
|
+
]
|
|
327
|
+
],
|
|
328
|
+
"example": "const result = await gateway.configure({\n deviceIds, mode: { type: 'count', targetCount: 100 }\n});\nif (!result.ok) console.table(result.results);",
|
|
329
|
+
"note": "설정의 ok가 true일 때만 시작하세요. 각 장비의 capabilities에 있는 모드와 목표 상한을 따릅니다."
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
"name": "start",
|
|
333
|
+
"group": "운동",
|
|
334
|
+
"kind": "비동기",
|
|
335
|
+
"sig": "gateway.start({ deviceIds })",
|
|
336
|
+
"desc": "운동 설정이 완료된 장비를 시작합니다.",
|
|
337
|
+
"returns": "Promise<CommandResult>",
|
|
338
|
+
"params": [
|
|
339
|
+
[
|
|
340
|
+
"deviceIds",
|
|
341
|
+
"string[] · 필수",
|
|
342
|
+
"이번 운동 대상으로 보관한 장비 키 목록."
|
|
343
|
+
]
|
|
344
|
+
],
|
|
345
|
+
"example": "const started = await gateway.start({ deviceIds });\nif (!started.ok) {\n console.table(started.results);\n}",
|
|
346
|
+
"note": "일부 실패 시 recovery의 정지 결과도 확인하세요. 처음 acknowledged였던 장비가 이미 정지됐을 수 있습니다."
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
"name": "stop",
|
|
350
|
+
"group": "운동",
|
|
351
|
+
"kind": "비동기",
|
|
352
|
+
"sig": "gateway.stop({ deviceIds })",
|
|
353
|
+
"desc": "시작했던 장비를 정지하고 마지막 측정값 확보를 시도합니다.",
|
|
354
|
+
"returns": "Promise<CommandResult>",
|
|
355
|
+
"params": [
|
|
356
|
+
[
|
|
357
|
+
"deviceIds",
|
|
358
|
+
"string[] · 필수",
|
|
359
|
+
"시작할 때 보관한 장비 키 목록."
|
|
360
|
+
]
|
|
361
|
+
],
|
|
362
|
+
"example": "const stopped = await gateway.stop({ deviceIds });\nfor (const result of stopped.results) {\n const final = result.finalMeasurement;\n console.log(result.status, final?.available, final?.measurement);\n}",
|
|
363
|
+
"note": "정지 확인과 마지막 측정값 확보는 별개입니다. 값이 없으면 미확인으로 남기고 0으로 대체하지 마세요."
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
"name": "identify",
|
|
367
|
+
"group": "운동",
|
|
368
|
+
"kind": "비동기",
|
|
369
|
+
"sig": "gateway.identify({ deviceId, signal })",
|
|
370
|
+
"desc": "지원·활성화된 위치 확인 신호로 특정 장비를 찾습니다.",
|
|
371
|
+
"returns": "Promise<CommandResult>",
|
|
372
|
+
"params": [
|
|
373
|
+
[
|
|
374
|
+
"deviceId",
|
|
375
|
+
"string · 필수",
|
|
376
|
+
"선택한 장비의 키."
|
|
377
|
+
],
|
|
378
|
+
[
|
|
379
|
+
"signal",
|
|
380
|
+
"IdentifySignal · 필수",
|
|
381
|
+
"vibrate / buzzer / displayId."
|
|
382
|
+
]
|
|
383
|
+
],
|
|
384
|
+
"example": "const signal = device.capabilities.identifySignals[0];\nif (signal) {\n const result = await gateway.identify({ deviceId: device.deviceId, signal });\n console.log(result.ok, result.results);\n}",
|
|
385
|
+
"note": "기본 활성화 신호는 없습니다. 실제 제품에서 검증된 신호만 인스턴스 옵션으로 활성화하고 지원 목록을 확인하세요."
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
"name": "on",
|
|
389
|
+
"group": "데이터",
|
|
390
|
+
"kind": "구독",
|
|
391
|
+
"sig": "gateway.on(event, handler)",
|
|
392
|
+
"desc": "운동 값·장비 상태·연결 상태·오류를 구독합니다.",
|
|
393
|
+
"returns": "() => void · 구독 해제 함수",
|
|
394
|
+
"params": [
|
|
395
|
+
[
|
|
396
|
+
"event",
|
|
397
|
+
"이벤트 이름 · 필수",
|
|
398
|
+
"measurement / deviceChanged / connectionChanged / error."
|
|
399
|
+
],
|
|
400
|
+
[
|
|
401
|
+
"handler",
|
|
402
|
+
"function · 필수",
|
|
403
|
+
"이벤트별 데이터 객체를 받는 함수."
|
|
404
|
+
]
|
|
405
|
+
],
|
|
406
|
+
"example": "const off = gateway.on('measurement', value => {\n console.log(value.deviceId, value.count);\n});\n// 화면 사용을 마칠 때\noff();",
|
|
407
|
+
"note": "이벤트를 중복 등록하지 마세요. 함수 호출의 try/catch와 error 이벤트 처리를 모두 구현하세요."
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
"name": "readMeasurement",
|
|
411
|
+
"group": "데이터",
|
|
412
|
+
"kind": "비동기",
|
|
413
|
+
"sig": "gateway.readMeasurement({ deviceId, timeoutMs? })",
|
|
414
|
+
"desc": "특정 장비의 현재 운동 값을 요청합니다.",
|
|
415
|
+
"returns": "Promise<Measurement>",
|
|
416
|
+
"params": [
|
|
417
|
+
[
|
|
418
|
+
"deviceId",
|
|
419
|
+
"string · 필수",
|
|
420
|
+
"조회할 장비의 키."
|
|
421
|
+
],
|
|
422
|
+
[
|
|
423
|
+
"timeoutMs",
|
|
424
|
+
"number · 선택",
|
|
425
|
+
"응답 대기 시간. 50~30000ms."
|
|
426
|
+
]
|
|
427
|
+
],
|
|
428
|
+
"example": "const value = await gateway.readMeasurement({ deviceId });\nconsole.log(value.count, value.elapsedMs);",
|
|
429
|
+
"note": "이 요청도 measurement 이벤트를 발생시킵니다. 반환값과 이벤트를 각각 새 기록으로 추가하지 마세요."
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
"name": "getLastMeasurement",
|
|
433
|
+
"group": "데이터",
|
|
434
|
+
"kind": "동기",
|
|
435
|
+
"sig": "gateway.getLastMeasurement({ deviceId })",
|
|
436
|
+
"desc": "마지막으로 보관한 측정값을 최신성 표시와 함께 가져옵니다.",
|
|
437
|
+
"returns": "Measurement | null",
|
|
438
|
+
"params": [
|
|
439
|
+
[
|
|
440
|
+
"deviceId",
|
|
441
|
+
"string · 필수",
|
|
442
|
+
"조회할 장비의 키."
|
|
443
|
+
]
|
|
444
|
+
],
|
|
445
|
+
"example": "const last = gateway.getLastMeasurement({ deviceId });\nif (!last) console.log('아직 받은 값 없음');\nelse if (last.stale) console.log('현재 값 확인 필요');",
|
|
446
|
+
"note": "새 값을 요청하지 않습니다. 기존 객체의 stale은 저절로 바뀌지 않으므로 화면 갱신 시 다시 조회하세요."
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
"name": "readBattery",
|
|
450
|
+
"group": "데이터",
|
|
451
|
+
"kind": "비동기",
|
|
452
|
+
"sig": "gateway.readBattery({ deviceId, timeoutMs? })",
|
|
453
|
+
"desc": "선택한 장비의 배터리 정보를 읽습니다.",
|
|
454
|
+
"returns": "Promise<{ kind: \"percent\", value: number } | { kind: \"unknown\" }>",
|
|
455
|
+
"params": [
|
|
456
|
+
[
|
|
457
|
+
"deviceId",
|
|
458
|
+
"string · 필수",
|
|
459
|
+
"조회할 장비의 키."
|
|
460
|
+
],
|
|
461
|
+
[
|
|
462
|
+
"timeoutMs",
|
|
463
|
+
"number · 선택",
|
|
464
|
+
"응답 대기 시간. 50~30000ms."
|
|
465
|
+
]
|
|
466
|
+
],
|
|
467
|
+
"example": "const battery = await gateway.readBattery({ deviceId });\nconsole.log(battery.kind === 'percent' ? `${battery.value}%` : '잔량 미확인');",
|
|
468
|
+
"note": "unknown을 배터리 0%로 표시하지 마세요."
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
"name": "getDiagnostics",
|
|
472
|
+
"group": "진단",
|
|
473
|
+
"kind": "동기",
|
|
474
|
+
"sig": "gateway.getDiagnostics()",
|
|
475
|
+
"desc": "연결 상태와 최근 요청의 결과·소요 시간 등 문제 확인용 요약을 받습니다.",
|
|
476
|
+
"returns": "Diagnostics · connection, deviceCount, recentRequests 등",
|
|
477
|
+
"params": [],
|
|
478
|
+
"example": "const summary = gateway.getDiagnostics();\nconsole.log(summary.connection, summary.deviceCount);",
|
|
479
|
+
"note": "운동 기록이나 학생 정보 저장 기능이 아닙니다. 장비 상태와 오류를 앱에서 확인할 때 사용하세요."
|
|
480
|
+
}
|
|
481
|
+
];
|
|
482
|
+
const flow=[
|
|
483
|
+
{
|
|
484
|
+
"label": "연결",
|
|
485
|
+
"fn": "connect()",
|
|
486
|
+
"title": "사용자의 클릭에서 연결합니다.",
|
|
487
|
+
"desc": "실제 장비에서는 연결 버튼으로 시작합니다. 연결 완료를 기다린 뒤 다음 작업을 실행하세요.",
|
|
488
|
+
"code": "await gateway.connect();",
|
|
489
|
+
"owner": "SDK: 연결 처리 · 앱: 연결 버튼과 상태 표시"
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
"label": "검색·선택",
|
|
493
|
+
"fn": "scan()",
|
|
494
|
+
"title": "발견된 장비에서 수업 대상을 고릅니다.",
|
|
495
|
+
"desc": "검색 결과의 deviceId를 보관합니다. 표시 번호나 학생 번호로 바꾸지 말고, 사용할 장비를 직접 선택받으세요.",
|
|
496
|
+
"code": "const { devices } = await gateway.scan();\n// 앱의 선택 화면에서 고른 장비 키를 보관합니다.\nconst deviceIds = selectedDevices.map(d => d.deviceId);",
|
|
497
|
+
"owner": "SDK: 장비 목록 · 앱: 선택 화면과 학생 배정"
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
"label": "운동 설정",
|
|
501
|
+
"fn": "configure()",
|
|
502
|
+
"title": "설정이 확인되면 시작할 수 있습니다.",
|
|
503
|
+
"desc": "자유, 목표 횟수, 목표 시간 중 하나를 설정합니다. 선택한 장비의 지원 범위와 결과의 ok를 확인하세요.",
|
|
504
|
+
"code": "const configured = await gateway.configure({\n deviceIds, mode: { type: 'free' }\n});\nif (!configured.ok) throw new Error('설정 결과 확인 필요');",
|
|
505
|
+
"owner": "SDK: 설정 요청과 결과 · 앱: 모드 선택"
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
"label": "시작·수신",
|
|
509
|
+
"fn": "start() / on()",
|
|
510
|
+
"title": "측정값으로 수업 화면을 갱신합니다.",
|
|
511
|
+
"desc": "시작 전에 이벤트를 구독합니다. count는 누적 횟수이므로 기존 값에 더하지 않고 새 값으로 갱신하세요.",
|
|
512
|
+
"code": "const off = gateway.on('measurement', value => {\n console.log(value.deviceId, value.count);\n});\nconst started = await gateway.start({ deviceIds });\nif (!started.ok) console.table(started.results);",
|
|
513
|
+
"owner": "SDK: 운동 데이터 · 앱: 화면, 그래프와 점수"
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
"label": "정지·저장",
|
|
517
|
+
"fn": "stop()",
|
|
518
|
+
"title": "정지 결과와 마지막 값을 따로 확인합니다.",
|
|
519
|
+
"desc": "처음 시작한 장비 목록으로 정지합니다. 정지 결과와 마지막 값 확보 여부를 함께 보관하고, 저장은 앱에서 구현하세요.",
|
|
520
|
+
"code": "const stopped = await gateway.stop({ deviceIds });\nfor (const result of stopped.results) {\n console.log(result.status, result.finalMeasurement);\n}\n// 앱에서 미확인 상태까지 보존해 기록을 저장합니다.",
|
|
521
|
+
"owner": "SDK: 정지 결과와 마지막 값 · 앱: 기록 저장"
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
"label": "연결 종료",
|
|
525
|
+
"fn": "disconnect()",
|
|
526
|
+
"title": "종료 결과를 확인하고 구독을 해제합니다.",
|
|
527
|
+
"desc": "운동 종료를 명시적으로 처리합니다. 화면을 닫거나 연결이 끊긴 것만으로 장비가 정지했다고 판단하지 마세요.",
|
|
528
|
+
"code": "try {\n const result = await gateway.disconnect();\n console.log(result.closed, result.stopResults);\n} finally {\n off();\n}",
|
|
529
|
+
"owner": "SDK: 연결 종료 · 앱: 종료 안내와 구독 정리"
|
|
530
|
+
}
|
|
531
|
+
];
|
|
532
|
+
const fields=[
|
|
533
|
+
[
|
|
534
|
+
"deviceId",
|
|
535
|
+
"string",
|
|
536
|
+
"장비 구분 키",
|
|
537
|
+
"문자열을 해석하거나 학생의 영구 ID로 사용하지 않습니다."
|
|
538
|
+
],
|
|
539
|
+
[
|
|
540
|
+
"displayNumber",
|
|
541
|
+
"number | null",
|
|
542
|
+
"화면에 보여줄 장비 번호",
|
|
543
|
+
"미확인 값은 null입니다. 제어에는 deviceId를 사용합니다."
|
|
544
|
+
],
|
|
545
|
+
[
|
|
546
|
+
"sessionId",
|
|
547
|
+
"string",
|
|
548
|
+
"장비별 운동 식별값",
|
|
549
|
+
"수업 전체 기록 ID는 앱에서 별도로 만듭니다."
|
|
550
|
+
],
|
|
551
|
+
[
|
|
552
|
+
"segmentId",
|
|
553
|
+
"string",
|
|
554
|
+
"운동 내 구간 식별값",
|
|
555
|
+
"값이 바뀌면 새로운 구간으로 구분합니다."
|
|
556
|
+
],
|
|
557
|
+
[
|
|
558
|
+
"count",
|
|
559
|
+
"number",
|
|
560
|
+
"현재 구간의 누적 횟수",
|
|
561
|
+
"10 → 12 → 15를 받았다면 현재 값은 15회입니다."
|
|
562
|
+
],
|
|
563
|
+
[
|
|
564
|
+
"elapsedMs",
|
|
565
|
+
"number · ms",
|
|
566
|
+
"구간의 경과 시간",
|
|
567
|
+
"초 단위 표시에는 1000으로 나눕니다."
|
|
568
|
+
],
|
|
569
|
+
[
|
|
570
|
+
"pauseCount",
|
|
571
|
+
"number | null",
|
|
572
|
+
"멈춤 횟수",
|
|
573
|
+
"알 수 없는 null과 0회를 구분합니다."
|
|
574
|
+
],
|
|
575
|
+
[
|
|
576
|
+
"receivedAt",
|
|
577
|
+
"number · Unix ms",
|
|
578
|
+
"앱이 수신한 시각",
|
|
579
|
+
"운동 시간과 다릅니다. 화면에서 오래된 값인지 확인할 때 참고합니다."
|
|
580
|
+
],
|
|
581
|
+
[
|
|
582
|
+
"source",
|
|
583
|
+
"'live' | 'cache'",
|
|
584
|
+
"이번 수신값 또는 마지막 보관값",
|
|
585
|
+
"cache를 새로 수신한 값처럼 표시하지 않습니다."
|
|
586
|
+
],
|
|
587
|
+
[
|
|
588
|
+
"stale",
|
|
589
|
+
"boolean",
|
|
590
|
+
"현재 상태를 다시 확인해야 하는 값",
|
|
591
|
+
"getLastMeasurement()로 최신성 상태를 다시 조회합니다."
|
|
592
|
+
],
|
|
593
|
+
[
|
|
594
|
+
"simulated",
|
|
595
|
+
"boolean",
|
|
596
|
+
"연습용 가상 데이터 여부",
|
|
597
|
+
"실제 기록과 구분하며 저장할 때도 유지합니다."
|
|
598
|
+
]
|
|
599
|
+
];
|
|
600
|
+
|
|
601
|
+
'use strict';
|
|
602
|
+
const $ = (s, r=document) => r.querySelector(s);
|
|
603
|
+
const $$ = (s, r=document) => [...r.querySelectorAll(s)];
|
|
604
|
+
const escape = s => String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
605
|
+
function highlight(code) {
|
|
606
|
+
return String(code).split(/(\/\/[^\n]*|'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*`)/g).map(token => {
|
|
607
|
+
if(token.startsWith('//')) return '<span class="comment">'+escape(token)+'</span>';
|
|
608
|
+
if(/^['"`]/.test(token)) return '<span class="string">'+escape(token)+'</span>';
|
|
609
|
+
return escape(token).replace(/\b(const|let|await|async|import|from|if|else|try|catch|finally|throw|new|return|for|of|true|false|null)\b/g,'<span class="keyword">$1</span>');
|
|
610
|
+
}).join('');
|
|
611
|
+
}
|
|
612
|
+
function setCode(selector, code) { const element=$(selector); element.textContent=code; element.innerHTML=highlight(code); }
|
|
613
|
+
$$('.code-card pre code').forEach(c=>c.innerHTML=highlight(c.textContent));
|
|
614
|
+
let toastTimer;
|
|
615
|
+
function toast(message){clearTimeout(toastTimer);$('.toast').textContent=message;$('.toast').hidden=false;toastTimer=setTimeout(()=>$('.toast').hidden=true,2500);}
|
|
616
|
+
async function copyText(value, button){
|
|
617
|
+
try {
|
|
618
|
+
if(navigator.clipboard && window.isSecureContext) await navigator.clipboard.writeText(value);
|
|
619
|
+
else {const el=document.createElement('textarea');el.value=value;el.style.cssText='position:fixed;top:0;left:-9999px';document.body.append(el);el.select();const success=document.execCommand('copy');el.remove();if(!success)throw new Error('copy');}
|
|
620
|
+
toast('복사했습니다.');
|
|
621
|
+
if(button){const old=button.textContent;button.textContent='복사 완료 ✓';setTimeout(()=>button.textContent=old,1800);}
|
|
622
|
+
} catch {toast('자동 복사가 지원되지 않습니다. 내용을 선택해 직접 복사하세요.');}
|
|
623
|
+
}
|
|
624
|
+
document.addEventListener('click',e=>{
|
|
625
|
+
const copy=e.target.closest('[data-copy-code]');if(copy)copyText($('pre',copy.closest('.code-card')).textContent,copy);
|
|
626
|
+
const target=e.target.closest('[data-copy-target]');if(target)copyText(document.getElementById(target.dataset.copyTarget).textContent,target);
|
|
627
|
+
const expand=e.target.closest('.expand-code');if(expand){const open=expand.closest('.code-card').classList.toggle('expanded');expand.setAttribute('aria-expanded',String(open));expand.textContent=open?'코드 접기 ↑':'전체 코드 펼치기 ↓';}
|
|
628
|
+
});
|
|
629
|
+
$('.mobile-menu').addEventListener('click',()=>{const open=$('.sidebar').classList.toggle('open');$('.mobile-menu').setAttribute('aria-expanded',String(open));$('.mobile-menu').setAttribute('aria-label',open?'목차 닫기':'목차 열기');syncMenuVisibility();});
|
|
630
|
+
function closeMenu(){ $('.sidebar').classList.remove('open');$('.mobile-menu').setAttribute('aria-expanded','false');$('.mobile-menu').setAttribute('aria-label','목차 열기');syncMenuVisibility(); }
|
|
631
|
+
function syncMenuVisibility(){ $('.sidebar').inert=matchMedia('(max-width:920px)').matches&&!$('.sidebar').classList.contains('open'); }
|
|
632
|
+
addEventListener('resize',syncMenuVisibility);syncMenuVisibility();
|
|
633
|
+
$$('.sidebar a').forEach(a=>a.addEventListener('click',closeMenu));
|
|
634
|
+
document.addEventListener('click',e=>{if(!e.target.closest('.sidebar,.mobile-menu'))closeMenu();});
|
|
635
|
+
document.addEventListener('keydown',e=>{if(e.key==='Escape')closeMenu();if(e.key==='/'&&!e.metaKey&&!e.ctrlKey&&!e.altKey&&!e.target.matches('input,textarea,[contenteditable]')){e.preventDefault();$('#api-search').focus();}});
|
|
636
|
+
const navLinks=$$('.sidebar a');
|
|
637
|
+
function scrollUI(){const pos=window.scrollY+155;let active=navLinks[0];navLinks.forEach(a=>{if($(a.getAttribute('href')).offsetTop<=pos)active=a;});navLinks.forEach(a=>{a.classList.toggle('active',a===active);if(a===active)a.setAttribute('aria-current','location');else a.removeAttribute('aria-current');});const max=document.documentElement.scrollHeight-innerHeight;$('.top-progress').style.width=(max>0?scrollY/max*100:0)+'%';}
|
|
638
|
+
let queued=false;addEventListener('scroll',()=>{if(!queued){queued=true;requestAnimationFrame(()=>{scrollUI();queued=false;});}},{passive:true});addEventListener('resize',scrollUI);scrollUI();
|
|
639
|
+
function activateTab(name,focus=false){$$('[data-tab]').forEach(button=>{const active=button.dataset.tab===name;button.setAttribute('aria-selected',String(active));button.tabIndex=active?0:-1;$('#panel-'+button.dataset.tab).hidden=!active;if(active&&focus)button.focus();});}
|
|
640
|
+
$$('[data-tab]').forEach(button=>{button.addEventListener('click',()=>activateTab(button.dataset.tab));button.addEventListener('keydown',e=>{if(['ArrowLeft','ArrowRight','Home','End'].includes(e.key)){e.preventDefault();activateTab(e.key==='Home'?'practice':e.key==='End'?'actual':button.dataset.tab==='practice'?'actual':'practice',true);}});});
|
|
641
|
+
function showStep(index){const step=flow[index];$('#flow-title').textContent=step.title;$('#flow-desc').textContent=step.desc;$('#flow-owner').textContent=step.owner;setCode('#flow-code pre code',step.code);$$('[data-step]').forEach(b=>{const selected=Number(b.dataset.step)===index;b.classList.toggle('active',selected);b.setAttribute('aria-pressed',String(selected));});}
|
|
642
|
+
$$('[data-step]').forEach(b=>b.addEventListener('click',()=>showStep(Number(b.dataset.step))));showStep(0);
|
|
643
|
+
let currentMode='free';
|
|
644
|
+
function renderMode(){const n=Number($('#mode-value').value);const valid=Number.isSafeInteger(n)&&n>0;$('#mode-controls').hidden=currentMode==='free';$('#mode-error').textContent=currentMode!=='free'&&!valid?'양의 정수를 입력하세요.':'';$('#mode-value').setAttribute('aria-invalid',String(currentMode!=='free'&&!valid));if(currentMode!=='free'&&!valid){setCode('#mode-code pre code','// 목표값에 양의 정수를 입력하세요.');return;}const mode=currentMode==='free'?"{ type: 'free' }":currentMode==='count'?`{ type: 'count', targetCount: ${n} }`:`{ type: 'time', durationSeconds: ${n} }`;setCode('#mode-code pre code',`const configured = await gateway.configure({\n deviceIds,\n mode: ${mode}\n});\nif (!configured.ok) {\n console.table(configured.results);\n // 결과를 확인하기 전에는 시작하지 않습니다.\n}`);}
|
|
645
|
+
$$('[data-mode]').forEach(b=>b.addEventListener('click',()=>{currentMode=b.dataset.mode;$$('[data-mode]').forEach(x=>x.setAttribute('aria-pressed',String(x===b)));$('#mode-value').value=currentMode==='time'?60:100;$('#mode-label').textContent=currentMode==='time'?'목표 시간':'목표 횟수';$('#mode-unit').textContent=currentMode==='time'?'초':'회';renderMode();}));$('#mode-value').addEventListener('input',renderMode);renderMode();
|
|
646
|
+
let category='전체';
|
|
647
|
+
const apiMarkup=api.map(item=>{const params=item.params.length?'<div class="table-wrap"><table><thead><tr><th>입력</th><th>타입</th><th>설명</th></tr></thead><tbody>'+item.params.map(p=>'<tr><td><code>'+escape(p[0])+'</code></td><td>'+escape(p[1])+'</td><td>'+escape(p[2])+'</td></tr>').join('')+'</tbody></table></div>':'';return '<details class="api-entry" id="api-'+item.name+'" data-group="'+item.group+'"><summary><div><span class="api-title">'+item.name+(item.name==='SDKError'?'':'()')+'</span><span class="api-subtitle">'+escape(item.desc)+'</span></div><span class="api-kind '+(item.kind==='비동기'?'async':'')+'">'+item.kind+'</span></summary><div class="api-body"><div class="api-signature">'+escape(item.sig)+'</div>'+params+'<div class="return">반환값 <code>'+escape(item.returns)+'</code></div><div class="code-card"><div class="code-head"><span>JavaScript · 사용 예시</span><button type="button" class="copy" data-copy-code>복사</button></div><pre><code>'+highlight(item.example)+'</code></pre></div><div class="api-note">'+escape(item.note)+'</div></div></details>';}).join('');
|
|
648
|
+
$('#api-list').innerHTML=apiMarkup;
|
|
649
|
+
function filterApi(){const q=$('#api-search').value.trim().toLowerCase();let count=0;api.forEach(item=>{const shown=(category==='전체'||item.group===category)&&JSON.stringify(item).toLowerCase().includes(q);$('#api-'+item.name).hidden=!shown;if(shown)count++;});$('#api-count').textContent=count+' / '+api.length+' API';$('#api-empty').hidden=count>0;}
|
|
650
|
+
$$('[data-category]').forEach(b=>b.addEventListener('click',()=>{category=b.dataset.category;$$('[data-category]').forEach(c=>c.setAttribute('aria-pressed',String(c===b)));filterApi();}));$('#api-search').addEventListener('input',filterApi);filterApi();
|
|
651
|
+
const counts=[10,12,15],times=[5000,6000,7500];let sampleIndex=0;
|
|
652
|
+
function renderSample(){const count=counts[sampleIndex];$('#lab-count').textContent=count;$('#lab-time').textContent=(times[sampleIndex]/1000).toFixed(1)+'초 · elapsedMs / 1000';$('#lab-explainer').textContent=['첫 측정값입니다. 화면에 10회를 표시합니다.','새 누적값은 12회입니다. 10 + 12로 더하지 않습니다.','최종 표시값은 15회입니다. 세 값을 더한 37회가 아닙니다.'][sampleIndex];$('#lab-next').disabled=sampleIndex===2;$('#lab-next').textContent=sampleIndex===2?'세 값 모두 확인 ✓':'다음 값 받기 →';$$('[data-sample]').forEach(s=>s.classList.toggle('current',Number(s.dataset.sample)===sampleIndex));const data={deviceId:'example-device-1',displayNumber:1,sessionId:'example-session',segmentId:'example-segment',count,elapsedMs:times[sampleIndex],pauseCount:0,receivedAt:1791676800000+times[sampleIndex],source:'live',stale:false,simulated:true};setCode('#payload-code pre code',JSON.stringify(data,null,2));}
|
|
653
|
+
$('#lab-next').addEventListener('click',()=>{if(sampleIndex<2){sampleIndex++;renderSample();}});$('#lab-reset').addEventListener('click',()=>{sampleIndex=0;renderSample();});renderSample();
|
|
654
|
+
function showField(index){const field=fields[index];$('#field-detail').innerHTML='<strong>'+escape(field[0])+'</strong>'+escape(field[3]);$$('[data-field]').forEach(row=>row.classList.toggle('selected',Number(row.dataset.field)===index));}
|
|
655
|
+
$$('[data-field]').forEach(row=>{row.addEventListener('click',()=>showField(Number(row.dataset.field)));row.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();showField(Number(row.dataset.field));}});});showField(4);
|
|
656
|
+
const responseExamples={success:{result:{requestId:'example-request',ok:true,results:[{deviceId:'example-device-1',status:'acknowledged'},{deviceId:'example-device-2',status:'acknowledged'}]},message:'선택한 모든 장비의 요청이 확인된 예시입니다. stop()에서는 마지막 값 확보 여부도 별도로 확인합니다.'},partial:{result:{requestId:'example-request',ok:false,results:[{deviceId:'example-device-1',status:'acknowledged',recovery:{action:'stop',status:'acknowledged'}},{deviceId:'example-device-2',status:'failed',errorCode:'DEVICE_REJECTED'}]},message:'첫 장비는 시작 후 복구 과정에서 정지됐습니다. 처음 status만 보고 계속 운동 중으로 표시하지 마세요.'},unknown:{result:{requestId:'example-request',ok:false,results:[{deviceId:'example-device-1',status:'unknown',errorCode:'TIMEOUT',finalMeasurement:{available:false,measurement:null}}]},message:'정지 결과와 마지막 값을 확인하지 못한 예시입니다. 장비를 현장에서 확인하고 미확인 상태를 기록합니다.'}};
|
|
657
|
+
function showResult(name){setCode('#result-code pre code',JSON.stringify(responseExamples[name].result,null,2));$('#result-message').textContent=responseExamples[name].message;$$('[data-result]').forEach(b=>b.setAttribute('aria-pressed',String(b.dataset.result===name)));}
|
|
658
|
+
$$('[data-result]').forEach(b=>b.addEventListener('click',()=>showResult(b.dataset.result)));showResult('success');
|
|
659
|
+
$('#copy-prompt').addEventListener('click',()=>copyText($('#ai-prompt').value,$('#copy-prompt')));
|
|
660
|
+
$$('[data-recipe]').forEach(b=>b.addEventListener('click',()=>{const input=$('#ai-prompt');input.value=input.value.replace(/^Jumpitnow SDK를 사용해서.*$/m,'Jumpitnow SDK를 사용해서 '+b.dataset.recipe+'을 만들어줘.');$$('[data-recipe]').forEach(x=>x.setAttribute('aria-pressed',String(x===b)));}));
|
|
661
|
+
$$('.check-item').forEach(i=>i.addEventListener('change',()=>$('#check-progress').textContent=$$('.check-item:checked').length+' / 10 항목 확인'));
|
|
662
|
+
let printState;
|
|
663
|
+
addEventListener('beforeprint',()=>{printState={details:$$('details').map(d=>[d,d.open]),hidden:$$('.api-entry').map(d=>[d,d.hidden])};$$('details').forEach(d=>d.open=true);$$('.api-entry').forEach(d=>d.hidden=false);});
|
|
664
|
+
addEventListener('afterprint',()=>{if(printState){printState.details.forEach(([d,open])=>d.open=open);printState.hidden.forEach(([d,hidden])=>d.hidden=hidden);printState=null;}});
|
|
665
|
+
$('[data-print]').addEventListener('click',()=>window.print());
|
|
666
|
+
</script></body></html>
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
</style>
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
|
-
<header><div class="bar"><div><div class="brand">jumpitnow<span> .</span></div><div class="version">TEACHER SDK / 0.1.0-alpha.
|
|
12
|
+
<header><div class="bar"><div><div class="brand">jumpitnow<span> .</span></div><div class="version">TEACHER SDK / 0.1.0-alpha.4</div></div><nav class="toplinks"><a href="../../docs/index.html">사용 가이드</a><a href="../../docs/API.md">함수 사용법</a></nav></div></header>
|
|
13
13
|
<main>
|
|
14
14
|
<div class="intro"><div><div class="eyebrow">INTERACTIVE EXAMPLE</div><h1>연결에서 첫 번째 점프까지.</h1><p>장비를 선택하고 운동을 시작해 보세요.<br>수신되는 누적 횟수를 SDK가 화면에 전달합니다.</p></div><span class="badge" id="modeBadge">가상 장비 · 실제 장비 제어 없음</span></div>
|
|
15
15
|
<div class="workspace">
|