@makeitnow/jumpitnow 0.0.0-stage → 0.1.0-alpha.2

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +366 -2
  3. package/dist/cjs/index.d.ts +6 -0
  4. package/dist/cjs/index.js +29 -0
  5. package/dist/cjs/internal/errors.d.ts +15 -0
  6. package/dist/cjs/internal/errors.js +44 -0
  7. package/dist/cjs/internal/gateway.d.ts +41 -0
  8. package/dist/cjs/internal/gateway.js +586 -0
  9. package/dist/cjs/internal/link.d.ts +13 -0
  10. package/dist/cjs/internal/link.js +111 -0
  11. package/dist/cjs/internal/protocol.d.ts +29 -0
  12. package/dist/cjs/internal/protocol.js +38 -0
  13. package/dist/cjs/internal/simulator.d.ts +15 -0
  14. package/dist/cjs/internal/simulator.js +117 -0
  15. package/dist/cjs/package.json +1 -0
  16. package/dist/cjs/types.d.ts +182 -0
  17. package/dist/cjs/types.js +2 -0
  18. package/dist/esm/index.d.ts +6 -0
  19. package/dist/esm/index.js +22 -0
  20. package/dist/esm/internal/errors.d.ts +15 -0
  21. package/dist/esm/internal/errors.js +37 -0
  22. package/dist/esm/internal/gateway.d.ts +41 -0
  23. package/dist/esm/internal/gateway.js +582 -0
  24. package/dist/esm/internal/link.d.ts +13 -0
  25. package/dist/esm/internal/link.js +106 -0
  26. package/dist/esm/internal/protocol.d.ts +29 -0
  27. package/dist/esm/internal/protocol.js +32 -0
  28. package/dist/esm/internal/simulator.d.ts +15 -0
  29. package/dist/esm/internal/simulator.js +113 -0
  30. package/dist/esm/types.d.ts +182 -0
  31. package/dist/esm/types.js +1 -0
  32. package/docs/API.md +106 -0
  33. package/docs/COMPATIBILITY.md +11 -0
  34. package/docs/SECURITY.md +17 -0
  35. package/docs/index.html +30 -0
  36. package/examples/browser/app.js +104 -0
  37. package/examples/browser/index.html +37 -0
  38. package/examples/react/GatewayPanel.tsx +66 -0
  39. package/package.json +45 -4
package/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # 변경 안내
2
+
3
+ ## 0.1.0-alpha.2 · 2026-10-10
4
+
5
+ - 제품 이름을 반영해 npm 패키지 이름을 `@makeitnow/jumpitnow`로 정리했습니다.
6
+ - JavaScript 함수·입력값·결과값·예제 중심으로 교사용 사용 안내를 정리했습니다.
7
+ - README에 AI 코딩 도구용 요청문, 바로 실행하는 가상 장비 예제, 데이터 표시·저장 규칙을 추가했습니다.
8
+ - 설치 파일 생성과 설치 안내를 복구했습니다.
9
+ - 예제 화면에서 장비별 실행 결과를 읽기 쉬운 문장으로 표시합니다.
10
+
11
+ 연결·장비 선택·운동 설정·시작·데이터 수신·정지 기능을 제공합니다. 실제 장비 검증은 아직 진행하지 않았습니다.
package/README.md CHANGED
@@ -1,3 +1,367 @@
1
- # Temporary Holding Version
1
+ # Jumpitnow SDK
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **줄넘기 데이터를 나만의 수업 화면, 운동 기록, 게임으로.**
4
+
5
+ npm 패키지 이름: **`@makeitnow/jumpitnow`**
6
+
7
+ Jumpitnow 게이트웨이와 스마트 줄넘기를 JavaScript 함수로 사용하는 SDK입니다. 연결, 장비 선택, 운동 설정, 시작·정지, 데이터 수신을 앱에 붙일 수 있습니다. 장비가 없어도 가상 줄넘기로 먼저 개발할 수 있습니다.
8
+
9
+ 이 README를 AI 코딩 도구에 전달하고 만들고 싶은 서비스를 설명하세요. 아래 요청문과 공개 함수로 개발을 시작할 수 있습니다.
10
+
11
+ > **현재 버전: `0.1.0-alpha.2`**
12
+ > 개발 확인용입니다. 실제 장비 검증은 아직 진행하지 않았습니다. npm 공개가 완료된 버전 또는 전달받은 `.tgz` 설치 파일로 시작하세요. 가상 장비에서 동작한 결과가 실제 장비 호환성을 보장하지는 않습니다.
13
+
14
+ [AI에게 전달할 요청문](#ai에게-전달할-요청문) · [설치](#설치) · [바로 실행하기](#가상-줄넘기로-바로-실행하기) · [함수 안내](#공개-함수-한눈에-보기) · [데이터 저장](#운동-데이터를-표시하고-저장하기)
15
+
16
+ ## 어떤 서비스를 만들 수 있나요?
17
+
18
+ | 만들고 싶은 서비스 | SDK로 받는 기능 | 앱에서 만드는 기능 |
19
+ |---|---|---|
20
+ | 수업 현황판 | 여러 줄넘기의 횟수·경과 시간 수신 | 장비 선택, 카드 화면, 수업 진행 |
21
+ | 개인 운동 기록장 | 운동 시작·정지와 마지막 측정값 | 기록 저장, 날짜별 조회, CSV 내보내기 |
22
+ | 목표 달성 챌린지 | 목표 횟수·목표 시간 설정 | 진행률, 목표 달성 화면, 배지 |
23
+ | 줄넘기 게임 | 선택한 줄넘기의 운동 데이터 | 점수 계산, 캐릭터 움직임, 게임 규칙 |
24
+
25
+ SDK는 장비 기능을 제공합니다. 로그인, 학생 명단, 데이터베이스, 저장 API, 그래프와 게임 규칙은 앱에서 구현합니다. SDK가 학생 정보를 자동 수집하거나 기록을 외부 저장소에 자동 전송하지 않습니다.
26
+
27
+ 완성된 화면을 먼저 보려면 [줄넘기 기록실 예제](https://jumpitnow-records-example.kkongnamul11.workers.dev)를 열어 **가상 줄넘기 → 연결하고 검색 → 장비 선택 → 기록 시작 → 정지하고 저장**을 체험하세요. 이 예제는 브라우저별 기록 공간을 사용하며, 교사 계정 로그인이나 다른 기기와의 기록 동기화 기능은 없습니다.
28
+
29
+ ## AI에게 전달할 요청문
30
+
31
+ 아래 내용을 README와 함께 AI 코딩 도구에 붙여 넣으세요. `[만들 서비스]`만 원하는 내용으로 바꾸면 됩니다.
32
+
33
+ ```text
34
+ Jumpitnow SDK를 사용해서 [만들 서비스: 예를 들어 줄넘기 수업 기록장]을 만들어줘.
35
+ 첨부한 README와 docs/API.md를 함수 사용의 기준으로 삼아줘.
36
+
37
+ 1. 개발 방식
38
+ - 기존 프로젝트의 화면 구성과 프레임워크를 먼저 확인해줘.
39
+ - SDK는 @makeitnow/jumpitnow의 공개 함수로만 사용해줘.
40
+ - 문서에 없는 함수, 이벤트, 필드를 임의로 만들지 마.
41
+ - npm에서 해당 버전을 조회할 수 있으면 버전을 고정해 설치해줘.
42
+ 등록 전이거나 공개 처리 중이면 전달받은 .tgz로 설치해줘.
43
+ - 처음에는 createSimulatedGateway({ deviceCount: 3 })로 개발해줘.
44
+ - 화면에 '연습 모드'를 표시하고 가상 기록과 실제 기록을 구분해줘.
45
+ - 실제 장비 모드에서는 createGateway()를 사용하고,
46
+ 사용자가 연결 버튼을 눌렀을 때 connect()를 호출해줘.
47
+
48
+ 2. 화면과 운동 흐름
49
+ - 연결 → 검색 → 장비 직접 선택 → 운동 설정 → 시작 → 데이터 표시
50
+ → 정지 확인 → 기록 저장 → 연결 종료 순서로 만들어줘.
51
+ - 검색 결과 전체를 자동으로 운동 대상으로 선택하지 마.
52
+ - 자유 운동, 목표 횟수, 목표 시간 중 하나를 선택하게 해줘.
53
+ - configure()의 ok가 true일 때만 start()를 호출해줘.
54
+ - 시작에 전달한 장비 목록은 별도로 보관하고, 정지할 때도 사용해줘.
55
+ - 명령 처리 중에는 중복 클릭을 막고, 운동 중에는 대상과 모드 변경을 막아줘.
56
+ - 결과의 ok와 장비별 results를 모두 확인해줘.
57
+ - failed와 unknown, 일부 시작 실패 후 recovery 결과를 화면에 구분해줘.
58
+ - 정지 여부를 확인하지 못하면 현장 확인을 안내하고 자동 재시작하지 마.
59
+
60
+ 3. 데이터와 저장
61
+ - measurement 이벤트의 count는 누적값이므로 기존 횟수에 더하지 마.
62
+ - deviceId + sessionId + segmentId별로 측정값을 구분해줘.
63
+ - sessionId는 장비별 운동 식별값이므로 수업 전체에는 앱의 별도 기록 ID를 써줘.
64
+ - elapsedMs는 밀리초, receivedAt은 수신 시각이야. 둘을 혼동하지 마.
65
+ - 오래된 값과 연결 끊김을 표시하고 마지막 값을 최신처럼 표시하지 마.
66
+ - stop()의 정지 결과와 finalMeasurement 확보 여부를 따로 확인해줘.
67
+ - 저장은 앱에서 구현해줘. SDK에 save(), login() 같은 함수를 만들지 마.
68
+ - 저장 실패 시 기록을 유지하고 같은 기록 ID로 재시도해 중복을 막아줘.
69
+ - 연습용 데이터의 simulated 표시를 저장할 때도 유지해줘.
70
+
71
+ 4. 완료 확인
72
+ - 연결 취소, 선택한 장비 없음, 일부 시작 실패, 연결 끊김,
73
+ 마지막 측정값 없음, 저장 실패 상황을 확인해줘.
74
+ - 이벤트 구독을 중복 등록하지 말고 사용이 끝나면 해제해줘.
75
+ - 실행 방법, 변경한 파일, 확인한 동작과 실제 장비 검증이 남은 부분을 알려줘.
76
+ ```
77
+
78
+ 화면을 확장할 때는 다음처럼 덧붙일 수 있습니다.
79
+
80
+ | 원하는 화면 | 요청문에 추가할 내용 |
81
+ |---|---|
82
+ | 교실 현황판 | “선택한 줄넘기를 큰 카드로 보여주고 횟수와 경과 시간을 표시해줘.” |
83
+ | 기록장 | “종료한 운동을 저장하고 목록·상세 보기·CSV 다운로드를 만들어줘.” |
84
+ | 게임 | “현재 운동 구간에서 늘어난 횟수만 점수에 반영하고, 구간이 바뀌면 기준값을 새로 잡아줘.” |
85
+
86
+ ## 설치
87
+
88
+ JavaScript 또는 TypeScript 웹 프로젝트에 설치합니다. 패키지 설치·개발에는 Node.js 20 이상이 필요하며, 앱 개발 도구가 더 높은 버전을 요구하면 해당 조건을 따릅니다.
89
+
90
+ ### npm으로 설치
91
+
92
+ npm 공개가 완료된 버전은 앱 프로젝트 폴더에서 다음과 같이 설치합니다.
93
+
94
+ ```sh
95
+ npm install --save-exact @makeitnow/jumpitnow@0.1.0-alpha.2
96
+ ```
97
+
98
+ 등록 전이거나 발행 직후 아직 조회되지 않는다면 아래 설치 파일 방식을 사용하세요. 두 설치 방식 중 하나를 선택합니다.
99
+
100
+ ### 전달받은 파일로 설치
101
+
102
+ 프로젝트 안에 `vendor` 폴더를 만들고 전달받은 `makeitnow-jumpitnow-0.1.0-alpha.2.tgz` 파일을 넣은 뒤, 프로젝트 폴더에서 실행하세요.
103
+
104
+ ```sh
105
+ npm install ./vendor/makeitnow-jumpitnow-0.1.0-alpha.2.tgz
106
+ ```
107
+
108
+ 아직 프로젝트가 없다면 `mkdir my-jumprope-app`, `cd my-jumprope-app`, `npm init -y`를 차례로 실행한 뒤 위와 같이 설치합니다. 이 방법으로 아래 가상 장비 예제를 바로 실행할 수 있습니다.
109
+
110
+ JavaScript와 TypeScript에서 같은 방식으로 가져옵니다. 타입 정의가 포함되어 있어 별도의 타입 패키지는 필요하지 않습니다.
111
+
112
+ ```js
113
+ import { createGateway, createSimulatedGateway, getSupport } from '@makeitnow/jumpitnow';
114
+ ```
115
+
116
+ ## 가상 줄넘기로 바로 실행하기
117
+
118
+ 설치한 프로젝트에 아래 내용을 `practice.mjs`로 저장하고 `node practice.mjs`를 실행하세요. **가상 줄넘기 한 대**의 연결부터 수신·정지·연결 종료까지 확인합니다. 실제 장비는 움직이지 않으며 기록을 외부로 저장하지 않습니다.
119
+
120
+ ```js
121
+ import { createSimulatedGateway } from '@makeitnow/jumpitnow';
122
+
123
+ const gateway = createSimulatedGateway({ deviceCount: 1 });
124
+ const unsubscribe = gateway.on('measurement', value => {
125
+ console.log(`${value.count}회 / ${(value.elapsedMs / 1000).toFixed(1)}초`);
126
+ });
127
+ const unsubscribeError = gateway.on('error', error => {
128
+ console.error(error.code, error.message);
129
+ });
130
+
131
+ try {
132
+ await gateway.connect();
133
+ const { devices } = await gateway.scan();
134
+ // 이 예제에서만 가상 장비 한 대를 선택합니다.
135
+ // 실제 수업 앱에서는 사용자가 목록에서 직접 선택하게 만드세요.
136
+ const device = devices.find(item => item.state === 'available');
137
+ if (!device) throw new Error('사용할 줄넘기가 없습니다.');
138
+ const deviceIds = [device.deviceId];
139
+
140
+ const configured = await gateway.configure({ deviceIds, mode: { type: 'free' } });
141
+ if (!configured.ok) {
142
+ console.table(configured.results);
143
+ throw new Error('운동 설정 결과를 확인하세요.');
144
+ }
145
+
146
+ const started = await gateway.start({ deviceIds });
147
+ if (!started.ok) {
148
+ console.table(started.results);
149
+ throw new Error('시작 및 정지 복구 결과를 확인하세요.');
150
+ }
151
+
152
+ // 예제에서는 약 3초 동안 수신합니다. 앱에서는 사용자의 정지 버튼을 사용합니다.
153
+ await new Promise(resolve => setTimeout(resolve, 3000));
154
+
155
+ const stopped = await gateway.stop({ deviceIds });
156
+ console.table(stopped.results);
157
+ if (!stopped.ok) throw new Error('정지 여부를 확인하지 못한 장비가 있습니다.');
158
+
159
+ for (const result of stopped.results) {
160
+ const final = result.finalMeasurement;
161
+ if (final?.available && final.measurement) {
162
+ console.log('마지막 운동 값:', final.measurement);
163
+ } else {
164
+ console.log('정지는 확인했지만 마지막 운동 값은 확보하지 못했습니다.');
165
+ }
166
+ }
167
+ } finally {
168
+ try {
169
+ const closed = await gateway.disconnect();
170
+ if (!closed.closed || (closed.stopResults && !closed.stopResults.ok)) {
171
+ console.error('연결 종료 또는 장비 정지 상태를 확인하세요.', closed);
172
+ }
173
+ } finally {
174
+ unsubscribe();
175
+ unsubscribeError();
176
+ }
177
+ }
178
+ ```
179
+
180
+ 가상 장비는 앱 개발을 위한 연습 도구입니다. 화면 반응과 기록 흐름을 확인하는 데 사용하고, 수치의 증가 속도를 실제 운동 속도나 성능 측정 기준으로 사용하지 마세요.
181
+
182
+ ## 실제 장비를 사용하는 웹 화면
183
+
184
+ 컴퓨터용 Chrome 또는 Edge에서 HTTPS 페이지나 `localhost` 개발 환경을 사용합니다. 실제 장비 연결은 브라우저 화면에서 실행하며 서버의 API 처리 코드에서 실행하지 않습니다.
185
+
186
+ 먼저 `getSupport().supported`로 현재 브라우저의 연결 기능을 확인합니다. 이 값은 **브라우저 환경 확인**이며 실제 장비의 연결·호환성 검증 결과가 아닙니다.
187
+
188
+ 아래 코드는 웹 화면에 연결할 형태를 보여줍니다. `connectButton`, `showDevicePicker`, `showMessage`는 앱에서 구현하는 버튼과 화면 함수이며 SDK 함수가 아닙니다.
189
+
190
+ ```js
191
+ import { createGateway, getSupport } from '@makeitnow/jumpitnow';
192
+
193
+ const gateway = createGateway(); // 화면을 다시 그릴 때마다 새로 만들지 않습니다.
194
+
195
+ connectButton.onclick = async () => {
196
+ if (!getSupport().supported) {
197
+ showMessage('컴퓨터용 Chrome 또는 Edge에서 실행 환경을 확인해주세요.');
198
+ return;
199
+ }
200
+ connectButton.disabled = true;
201
+ try {
202
+ await gateway.connect(); // 사용자의 클릭에서 바로 호출합니다.
203
+ const { devices } = await gateway.scan();
204
+ showDevicePicker(devices); // 사용 가능한 장비를 보여주고 직접 선택받습니다.
205
+ } catch (error) {
206
+ showMessage(error.message);
207
+ } finally {
208
+ connectButton.disabled = false;
209
+ }
210
+ };
211
+ ```
212
+
213
+ 웹 앱에서는 운동 중 연결·검색·설정 버튼도 잠급니다. 가상/실제 모드를 바꿀 때는 기존 운동의 정지를 확인하고 연결 종료와 구독 해제를 마친 뒤 새 인스턴스를 만드세요.
214
+
215
+ 전체 화면 구성은 [브라우저 예제](examples/browser/app.js)와 [React 예제](examples/react/GatewayPanel.tsx)를 참고하세요. React에서는 SDK 인스턴스를 화면 수명 동안 유지하고 이벤트 구독을 해제합니다. 페이지를 닫거나 컴포넌트가 사라지는 순간에 비동기 정지가 완료될 것이라고 가정하지 말고, 명시적인 종료 버튼에서 정지 결과를 확인하세요.
216
+
217
+ ## 공개 함수 한눈에 보기
218
+
219
+ **앱에서 가져오는 기능**
220
+
221
+ | 함수·클래스 | 용도 |
222
+ |---|---|
223
+ | `createGateway()` | 실제 장비용 인스턴스 생성 |
224
+ | `createSimulatedGateway({ deviceCount: 3 })` | 가상 장비용 인스턴스 생성 |
225
+ | `getSupport()` | 현재 브라우저의 연결 기능 확인 |
226
+ | `SDKError` | 함수 호출 오류의 `code`, `message` 확인 |
227
+
228
+ **`gateway`에서 사용하는 함수**
229
+
230
+ | 함수 | 입력·결과 요약 |
231
+ |---|---|
232
+ | `connect()` | 연결 후 정보 반환. 실패하면 오류 발생 |
233
+ | `scan()` | `{ devices, status, windowMs }` 반환. 배열 자체를 반환하지 않음 |
234
+ | `getDevices()` | 현재 장비 목록 반환 |
235
+ | `configure({ deviceIds, mode })` | 선택한 장비의 운동 방식 설정 |
236
+ | `start({ deviceIds })` | 설정된 장비의 운동 시작 |
237
+ | `stop({ deviceIds })` | 운동 정지 및 장비별 마지막 값 확보 시도 |
238
+ | `on('measurement', handler)` | 운동 값 수신. 구독 해제 함수 반환 |
239
+ | `readMeasurement({ deviceId })` | 특정 장비의 현재 값 요청 |
240
+ | `getLastMeasurement({ deviceId })` | 마지막 값 또는 `null` 반환. 새 값을 요청하지 않음 |
241
+ | `readBattery({ deviceId })` | `{ kind: 'percent', value }` 또는 알 수 없음 표시 |
242
+ | `identify({ deviceId, signal })` | 해당 장비에서 활성화된 경우에만 위치 확인 신호 사용 |
243
+ | `getInfo()` / `getDiagnostics()` | 연결 정보 / 문제 확인용 요약 반환 |
244
+ | `disconnect()` | `{ closed, stopResults }` 반환 |
245
+
246
+ `connect`, `scan`, `configure`, `start`, `stop`, `readMeasurement`, `readBattery`, `identify`, `disconnect`는 비동기 함수입니다. `await`와 `try/catch`를 사용하세요. 장비 작업은 순서대로 실행하고 여러 장비의 개별 요청을 `Promise.all()`로 동시에 보내지 않습니다.
247
+
248
+ `deviceIds`에는 검색 결과에서 선택한 `deviceId`를 넣습니다. 빈 배열, 중복 ID, 학생 이름, 장비 표시 번호를 대신 넣지 마세요. `identify()`는 기본적으로 활성화되어 있지 않으므로 필수 수업 흐름에 넣지 않습니다.
249
+
250
+ ### 운동 모드
251
+
252
+ 아래 셋 중 **하나**를 `configure({ deviceIds, mode })`의 `mode`에 전달합니다.
253
+
254
+ ```js
255
+ const freeMode = { type: 'free' };
256
+ const countMode = { type: 'count', targetCount: 100 };
257
+ const timeMode = { type: 'time', durationSeconds: 60 };
258
+ ```
259
+
260
+ 목표값은 양의 정수입니다. 선택한 장비의 `capabilities`에 있는 `modes`, `maxTargetCount`, `maxDurationSeconds` 범위에서 입력받으세요. 목표 달성 표시와 앱의 기록 종료·저장 처리는 별도로 구현하며, 목표에 도달했다는 이유로 저장이 완료되었다고 표시하지 않습니다.
261
+
262
+ ### 명령 결과와 이벤트
263
+
264
+ `configure()`, `start()`, `stop()`, `identify()`는 `{ requestId, ok, results }`를 반환합니다. 오류가 발생하지 않았더라도 **`ok: false`일 수 있습니다.**
265
+
266
+ | 장비별 결과 | 앱에서 처리할 내용 |
267
+ |---|---|
268
+ | `status: 'acknowledged'` | 해당 요청이 확인됨 |
269
+ | `status: 'failed'` | 해당 요청이 처리되지 않음 |
270
+ | `status: 'unknown'` | 처리 여부 확인 필요. 성공으로 표시하지 않음 |
271
+ | `recovery` | 일부 시작 실패 후 수행한 정지 시도의 결과 확인 |
272
+ | `finalMeasurement` | 정지 시 마지막 값 확보 여부 확인 |
273
+
274
+ `start()` 일부 실패 시 처음 시작 요청의 `acknowledged`만 보고 운동 중으로 표시하지 마세요. `recovery`로 이미 정지를 시도했을 수 있습니다. `stop()`을 기다린 결과의 `ok`는 정지 확인 여부이고, 마지막 측정값 확보 여부와는 다릅니다. `disconnect()` 결과의 `closed`도 장비 정지 성공 여부와 별개입니다.
275
+
276
+ 지원하는 이벤트는 아래 네 가지입니다. 별도의 `ready`, `data`, `finished`, `saved` 이벤트는 없습니다.
277
+
278
+ | 이벤트 | 콜백에서 받는 값 |
279
+ |---|---|
280
+ | `measurement` | 측정값 객체 |
281
+ | `deviceChanged` | `{ device, previousState }` |
282
+ | `connectionChanged` | `{ state, previousState }` |
283
+ | `error` | `{ code, message, recoverable, ... }` |
284
+
285
+ 이벤트의 `error` 처리와 비동기 함수의 `try/catch`를 모두 구현합니다. `recoverable: true`를 자동 재시작 허용으로 해석하지 마세요.
286
+
287
+ ## 운동 데이터를 표시하고 저장하기
288
+
289
+ ### 측정값 읽기
290
+
291
+ | 필드 | 의미와 사용 규칙 |
292
+ |---|---|
293
+ | `deviceId` | 현재 SDK에서 장비를 구분하는 값. 문자열 내용을 해석하거나 학생의 영구 ID로 사용하지 않음 |
294
+ | `displayNumber` | 화면에 보여줄 장비 번호. 미확인 시 `null` |
295
+ | `sessionId` | 해당 장비의 운동 식별값. 수업 전체의 공통 ID가 아님 |
296
+ | `segmentId` | 운동 안의 구간 식별값. 달라지면 새로운 구간으로 구분 |
297
+ | `count` | 현재 구간의 누적 횟수 |
298
+ | `elapsedMs` | 현재 구간의 경과 시간. 초 표시 시 `1000`으로 나눔 |
299
+ | `pauseCount` | 멈춤 횟수. 알 수 없으면 `null`이며 0과 구분 |
300
+ | `receivedAt` | 앱이 값을 받은 시각. Unix 시간, 밀리초 단위 |
301
+ | `source` | `live`: 수신한 값 / `cache`: 마지막 보관 값 |
302
+ | `stale` | 오래되었거나 현재 상태 확인이 필요한 값이면 `true` |
303
+ | `simulated` | 연습용 가상 데이터이면 `true` |
304
+
305
+ 같은 구간에서 `10 → 12 → 15`를 받았다면 현재 횟수는 **15회**입니다. 합계 37회로 저장하지 마세요. 같은 `(deviceId, sessionId, segmentId)`의 기록은 마지막 누적값으로 갱신합니다. 여러 구간의 합계가 필요하면 구간마다 확정한 값을 한 번씩 합산하고, 누락된 마지막 값은 미확인 상태로 남깁니다.
306
+
307
+ 운동이 시작되면 SDK가 `measurement` 이벤트로 값을 전달하므로 기본 화면은 이벤트를 구독하면 됩니다. 별도로 반복 읽기를 중복 구현할 필요는 없습니다. `readMeasurement()`도 수신 이벤트를 발생시키므로 반환값과 이벤트를 각각 새 기록으로 추가하지 마세요.
308
+
309
+ 수신이 멈춘 경우 기존 객체의 `stale` 값이 저절로 바뀌지는 않습니다. 화면 갱신 시 `getLastMeasurement()`로 상태를 다시 확인하고 `connectionChanged`, `deviceChanged`도 함께 반영하세요.
310
+
311
+ ### 저장은 앱에서 구현하기
312
+
313
+ SDK에는 `save()`, `saveRecord()`, 로그인, 기록 조회 함수가 없습니다. 브라우저 임시 보관, 서버 저장, CSV 다운로드를 앱 기능으로 만듭니다.
314
+
315
+ 권장 저장 흐름은 다음과 같습니다.
316
+
317
+ 1. 시작 전에 앱의 운동 기록 ID를 만들고 선택한 장비와 모드를 보관합니다.
318
+ 2. 수신한 측정값을 장비·운동·구간별로 갱신합니다. 전체 추이를 저장하려면 저장 간격도 앱에서 정합니다.
319
+ 3. 처음 시작에 전달한 장비 목록으로 `stop()`을 호출합니다.
320
+ 4. 장비별 정지 결과를 기록하고, 확보한 `finalMeasurement.measurement`로 마지막 값을 갱신합니다. 값이 없으면 이전 값을 확정값으로 꾸미거나 0으로 채우지 않습니다.
321
+ 5. 앱의 저장 API에 전송합니다. 정지 미확인 기록은 그 상태를 보존합니다.
322
+ 6. 저장소의 성공 응답을 확인한 뒤 “저장 완료”를 표시합니다. 실패하면 임시 기록과 같은 기록 ID로 재시도하며, 서버도 같은 ID의 중복 저장을 막도록 구현합니다.
323
+
324
+ 다음은 **앱에서 정할 수 있는 저장 형식**입니다. SDK의 반환 형식은 아닙니다.
325
+
326
+ ```text
327
+ 운동 기록
328
+ ├─ recordId 앱이 만든 운동 기록 ID
329
+ ├─ startedAt / endedAt 앱에서 기록한 시작·종료 시각
330
+ ├─ mode 사용한 운동 모드와 목표
331
+ ├─ simulated 연습용 여부
332
+ └─ devices[] 선택한 장비별 기록
333
+ ├─ deviceId / displayNumber
334
+ ├─ stopStatus acknowledged / failed / unknown
335
+ ├─ finalMeasurementAvailable
336
+ └─ segments[] sessionId + segmentId별 마지막 값 또는 측정 이력
337
+ ```
338
+
339
+ 학교·교사 계정, 학생과 장비의 연결 관계, 조회 권한은 앱에서 관리하세요. 저장소 비밀키는 브라우저 코드에 넣지 않습니다. 연습 데이터는 실제 수업 통계에서 분리합니다.
340
+
341
+ ## 앱을 공유하기 전에 확인하기
342
+
343
+ - [ ] 장비 없이 연습 모드에서 연결·검색·선택·시작·수신·정지가 된다.
344
+ - [ ] 선택하지 않은 장비를 시작하지 않으며, 중복 클릭이 막힌다.
345
+ - [ ] 설정 실패, 일부 시작 실패, 정지 미확인을 각각 표시한다.
346
+ - [ ] 같은 구간의 누적값과 반복 수신을 중복 합산하지 않는다.
347
+ - [ ] 연결이 끊기면 오래된 데이터를 구분하고 자동으로 운동을 재시작하지 않는다.
348
+ - [ ] 마지막 값이 없거나 저장이 실패해도 기록을 성공으로 꾸미지 않는다.
349
+ - [ ] 저장 재시도 시 같은 운동 기록이 중복 생성되지 않는다.
350
+ - [ ] 화면을 다시 열어도 이벤트 구독이 중복되지 않는다.
351
+ - [ ] 실제 사용 환경에서 장비 연결·횟수·시간·정지 결과를 확인한다.
352
+
353
+ 가상 장비로 예외 화면을 확인할 때는 `createSimulatedGateway({ deviceCount: 3, startFailures: [2] })`로 2번 장비의 시작 실패를 만들거나, `gateway.simulation.dropConnection()`으로 연결 끊김을 재현할 수 있습니다. 이 연습 기능은 가상 장비 인스턴스에서만 사용합니다.
354
+
355
+ ## 더 알아보기
356
+
357
+ | 문서·예제 | 내용 |
358
+ |---|---|
359
+ | [함수 사용법](docs/API.md) | 입력값, 결과값, 이벤트 사용 예 |
360
+ | [그림으로 보는 사용 가이드](docs/index.html) | 브라우저에서 열어 보는 기능 안내 |
361
+ | [사용 환경](docs/COMPATIBILITY.md) | 실제 장비 사용 조건과 검증 상태 |
362
+ | [수업 운영과 기록 관리](docs/SECURITY.md) | 장비 선택, 정지 확인, 기록 관리 |
363
+ | [브라우저 예제](examples/browser/app.js) | 연결·선택·운동·수신 화면 구현 |
364
+ | [React 예제](examples/react/GatewayPanel.tsx) | React 화면에서 공개 함수 사용 |
365
+ | [변경 기록](CHANGELOG.md) | 버전별 변경 내용 |
366
+
367
+ SDK 이용 조건은 배포자가 안내하는 정책을 확인하세요. 현재 패키지의 라이선스 표기는 `UNLICENSED`이며, 교사 배포용 이용 조건은 아직 확정되지 않았습니다.
@@ -0,0 +1,6 @@
1
+ import type { Gateway, GatewayOptions, SimulatedGateway, SimulationOptions } from './types.js';
2
+ export { getSupport } from './internal/link.js';
3
+ export { SDKError } from './internal/errors.js';
4
+ export type * from './types.js';
5
+ export declare function createGateway(options?: GatewayOptions): Gateway;
6
+ export declare function createSimulatedGateway(options?: SimulationOptions): SimulatedGateway;
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SDKError = exports.getSupport = void 0;
4
+ exports.createGateway = createGateway;
5
+ exports.createSimulatedGateway = createSimulatedGateway;
6
+ const gateway_js_1 = require("./internal/gateway.js");
7
+ const link_js_1 = require("./internal/link.js");
8
+ const simulator_js_1 = require("./internal/simulator.js");
9
+ var link_js_2 = require("./internal/link.js");
10
+ Object.defineProperty(exports, "getSupport", { enumerable: true, get: function () { return link_js_2.getSupport; } });
11
+ var errors_js_1 = require("./internal/errors.js");
12
+ Object.defineProperty(exports, "SDKError", { enumerable: true, get: function () { return errors_js_1.SDKError; } });
13
+ function facade(engine) {
14
+ return {
15
+ connect: () => engine.connect(), disconnect: () => engine.disconnect(),
16
+ getInfo: () => engine.getInfo(), scan: options => engine.scan(options), getDevices: () => engine.getDevices(),
17
+ configure: options => engine.configure(options), start: options => engine.start(options), stop: options => engine.stop(options),
18
+ readMeasurement: options => engine.readMeasurement(options), getLastMeasurement: options => engine.getLastMeasurement(options),
19
+ readBattery: options => engine.readBattery(options), identify: options => engine.identify(options),
20
+ on: (event, handler) => engine.on(event, handler), getDiagnostics: () => engine.getDiagnostics()
21
+ };
22
+ }
23
+ function createGateway(options = {}) {
24
+ return Object.freeze(facade(new gateway_js_1.GatewayEngine(new link_js_1.BrowserLink(), options)));
25
+ }
26
+ function createSimulatedGateway(options = {}) {
27
+ const link = new simulator_js_1.SimulationLink(options);
28
+ return Object.freeze({ ...facade(new gateway_js_1.GatewayEngine(link, options, true)), simulation: link.controls() });
29
+ }
@@ -0,0 +1,15 @@
1
+ import type { ErrorCode, ErrorInfo } from '../types.js';
2
+ export declare class SDKError extends Error implements ErrorInfo {
3
+ readonly code: ErrorCode;
4
+ readonly recoverable: boolean;
5
+ readonly deviceId?: string;
6
+ readonly requestId?: string;
7
+ constructor(code: ErrorCode, context?: {
8
+ deviceId?: string;
9
+ requestId?: string;
10
+ });
11
+ toJSON(): ErrorInfo;
12
+ }
13
+ export declare const fail: (code: ErrorCode) => never;
14
+ export declare const asError: (error: unknown, fallback?: ErrorCode) => SDKError;
15
+ export declare function integer(value: unknown, min: number, max: number): number;
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.asError = exports.fail = exports.SDKError = void 0;
4
+ exports.integer = integer;
5
+ const messages = {
6
+ UNSUPPORTED_BROWSER: '이 브라우저는 USB 장비 연결을 지원하지 않습니다.',
7
+ INSECURE_CONTEXT: 'HTTPS 또는 localhost에서 실행해주세요.',
8
+ USER_CANCELLED: '포트 선택을 취소했습니다.', PORT_UNAVAILABLE: 'USB 포트를 열거나 해제할 수 없습니다.',
9
+ NOT_CONNECTED: '게이트웨이를 먼저 연결해주세요.', UNSUPPORTED_GATEWAY: '지원하는 게이트웨이 응답을 확인하지 못했습니다.',
10
+ UNSUPPORTED_FEATURE: '이 장비에서 활성화되지 않은 기능입니다.', INVALID_ARGUMENT: '입력값을 확인해주세요.',
11
+ INVALID_STATE: '현재 상태에서 실행할 수 없습니다. 장비 상태를 먼저 확인해주세요.',
12
+ DEVICE_OFFLINE: '장비의 응답을 확인할 수 없습니다.', AMBIGUOUS_DEVICE: '장비 번호가 중복되었거나 응답 대상을 확인할 수 없습니다.',
13
+ BUSY: '다른 장비 작업이 진행 중입니다.', TIMEOUT: '제한 시간 안에 응답을 받지 못했습니다.',
14
+ DEVICE_REJECTED: '장비 또는 게이트웨이가 요청을 거부했습니다.', CONNECTION_LOST: '게이트웨이 연결이 끊어졌습니다.',
15
+ PROTOCOL_ERROR: '지원하는 응답 형식이 아닙니다.'
16
+ };
17
+ class SDKError extends Error {
18
+ code;
19
+ recoverable;
20
+ deviceId;
21
+ requestId;
22
+ constructor(code, context = {}) {
23
+ super(messages[code]);
24
+ this.name = 'SDKError';
25
+ this.code = code;
26
+ this.recoverable = !['UNSUPPORTED_BROWSER', 'UNSUPPORTED_GATEWAY', 'UNSUPPORTED_FEATURE', 'INVALID_ARGUMENT'].includes(code);
27
+ this.deviceId = context.deviceId;
28
+ this.requestId = context.requestId;
29
+ }
30
+ toJSON() {
31
+ return { code: this.code, message: this.message, recoverable: this.recoverable,
32
+ ...(this.deviceId ? { deviceId: this.deviceId } : {}), ...(this.requestId ? { requestId: this.requestId } : {}) };
33
+ }
34
+ }
35
+ exports.SDKError = SDKError;
36
+ const fail = (code) => { throw new SDKError(code); };
37
+ exports.fail = fail;
38
+ const asError = (error, fallback = 'PROTOCOL_ERROR') => error instanceof SDKError ? error : new SDKError(fallback);
39
+ exports.asError = asError;
40
+ function integer(value, min, max) {
41
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < min || value > max)
42
+ (0, exports.fail)('INVALID_ARGUMENT');
43
+ return value;
44
+ }
@@ -0,0 +1,41 @@
1
+ import type { Battery, CommandResult, Device, Diagnostics, DisconnectResult, Events, Gateway, GatewayInfo, GatewayOptions, IdentifySignal, Measurement, Mode, ScanResult } from '../types.js';
2
+ import type { Link } from './link.js';
3
+ export declare const VERSION = "0.1.0-alpha.2";
4
+ export declare class GatewayEngine implements Gateway {
5
+ #private;
6
+ constructor(link: Link, options?: GatewayOptions, simulated?: boolean);
7
+ on<K extends keyof Events>(name: K, handler: (value: Events[K]) => void): () => void;
8
+ connect(): Promise<GatewayInfo>;
9
+ scan(options?: {
10
+ timeoutMs?: number;
11
+ }): Promise<ScanResult>;
12
+ configure(options: {
13
+ deviceIds: string[];
14
+ mode: Mode;
15
+ }): Promise<CommandResult>;
16
+ start(options: {
17
+ deviceIds: string[];
18
+ }): Promise<CommandResult>;
19
+ stop(options: {
20
+ deviceIds: string[];
21
+ }): Promise<CommandResult>;
22
+ readMeasurement(options: {
23
+ deviceId: string;
24
+ timeoutMs?: number;
25
+ }): Promise<Measurement>;
26
+ getLastMeasurement(options: {
27
+ deviceId: string;
28
+ }): Measurement | null;
29
+ readBattery(options: {
30
+ deviceId: string;
31
+ timeoutMs?: number;
32
+ }): Promise<Battery>;
33
+ identify(options: {
34
+ deviceId: string;
35
+ signal: IdentifySignal;
36
+ }): Promise<CommandResult>;
37
+ disconnect(): Promise<DisconnectResult>;
38
+ getDevices(): Device[];
39
+ getInfo(): GatewayInfo;
40
+ getDiagnostics(): Diagnostics;
41
+ }