@uniai-fe/uds-templates 0.10.1 → 0.10.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniai-fe/uds-templates",
3
- "version": "0.10.1",
3
+ "version": "0.10.3",
4
4
  "description": "UNIAI Design System; UI Templates Package",
5
5
  "type": "module",
6
6
  "private": false,
@@ -56,28 +56,28 @@
56
56
  },
57
57
  "devDependencies": {
58
58
  "@svgr/webpack": "^8.1.0",
59
- "@tanstack/react-query": "^5.101.1",
60
- "@types/node": "^24.12.3",
59
+ "@tanstack/react-query": "^5.101.2",
60
+ "@types/node": "^24.13.2",
61
61
  "@types/react": "^19.2.17",
62
62
  "@types/react-dom": "^19.2.3",
63
- "eslint": "^9.39.2",
64
- "jotai": "^2.20.1",
63
+ "eslint": "^9.39.4",
64
+ "jotai": "^2.20.2",
65
65
  "next": "16.2.10",
66
- "prettier": "^3.8.4",
67
- "react-hook-form": "^7.80.0",
66
+ "prettier": "^3.8.5",
67
+ "react-hook-form": "^7.81.0",
68
68
  "sass": "^1.101.0",
69
69
  "typescript": "6.0.3",
70
- "@uniai-fe/eslint-config": "0.4.0",
70
+ "@uniai-fe/eslint-config": "0.4.1",
71
71
  "@uniai-fe/next-devkit": "0.4.0",
72
- "@uniai-fe/react-hooks": "0.3.0",
73
72
  "@uniai-fe/tsconfig": "0.2.0",
73
+ "@uniai-fe/react-hooks": "0.3.0",
74
74
  "@uniai-fe/uds-foundation": "0.5.0",
75
+ "@uniai-fe/uds-primitives": "0.10.0",
76
+ "@uniai-fe/util-functions": "0.4.3",
75
77
  "@uniai-fe/util-api": "0.2.1",
76
- "@uniai-fe/util-functions": "0.4.1",
77
- "@uniai-fe/util-jotai": "0.3.0",
78
78
  "@uniai-fe/util-next": "0.5.0",
79
79
  "@uniai-fe/util-rtc": "0.2.0",
80
- "@uniai-fe/uds-primitives": "0.10.0"
80
+ "@uniai-fe/util-jotai": "0.3.0"
81
81
  },
82
82
  "scripts": {
83
83
  "check:pre-commit": "pnpm --dir ../../.. run check:pre-commit",
@@ -2,30 +2,106 @@
2
2
 
3
3
  import { startWhepStream, type WhepStreamHandle } from "@uniai-fe/util-rtc";
4
4
 
5
+ /**
6
+ * CCTV Stream Registry Types; 외부 구독 snapshot
7
+ * @property {RTCPeerConnectionState} connectionState PeerConnection 연결 상태
8
+ * @property {boolean} isStreaming WHEP 시작 promise 진행 여부
9
+ * @property {MediaStream | null} stream 현재 수신 중인 MediaStream
10
+ * @property {string | null} streamError 마지막 WHEP 시작 오류 메시지
11
+ */
5
12
  interface CctvRtcStreamSnapshot {
13
+ /**
14
+ * PeerConnection 연결 상태
15
+ */
6
16
  connectionState: RTCPeerConnectionState;
17
+ /**
18
+ * WHEP 시작 promise 진행 여부
19
+ */
7
20
  isStreaming: boolean;
21
+ /**
22
+ * 현재 수신 중인 MediaStream
23
+ */
8
24
  stream: MediaStream | null;
25
+ /**
26
+ * 마지막 WHEP 시작 오류 메시지
27
+ */
9
28
  streamError: string | null;
10
29
  }
11
30
 
31
+ /**
32
+ * CCTV Stream Registry Types; Provider scope stream entry
33
+ * @property {Set<HTMLVideoElement>} attachedVideos 같은 stream을 표시하는 video 집합
34
+ * @property {AbortController | null} controller 진행 중이거나 연결된 WHEP cleanup controller
35
+ * @property {WhepStreamHandle | null} handle 연결 완료 뒤 PeerConnection cleanup handle
36
+ * @property {string} identityKey camera와 endpoint 기준 stream family key
37
+ * @property {Set<(snapshot: CctvRtcStreamSnapshot) => void>} listeners snapshot 구독자 집합
38
+ * @property {string} streamKey token generation까지 포함한 stream key
39
+ */
12
40
  interface CctvRtcStreamEntry extends CctvRtcStreamSnapshot {
41
+ /**
42
+ * 같은 stream을 표시하는 video element 집합
43
+ */
13
44
  attachedVideos: Set<HTMLVideoElement>;
45
+ /**
46
+ * 진행 중이거나 연결된 WHEP를 중단하는 controller
47
+ */
14
48
  controller: AbortController | null;
49
+ /**
50
+ * 연결 완료 뒤 PeerConnection을 닫는 handle
51
+ */
15
52
  handle: WhepStreamHandle | null;
53
+ /**
54
+ * camera와 endpoint 기준 stream family key
55
+ */
16
56
  identityKey: string;
57
+ /**
58
+ * entry snapshot 구독자 집합
59
+ */
17
60
  listeners: Set<(snapshot: CctvRtcStreamSnapshot) => void>;
61
+ /**
62
+ * token generation까지 포함한 stream key
63
+ */
18
64
  streamKey: string;
19
65
  }
20
66
 
67
+ /**
68
+ * CCTV Stream Registry Types; RTC stream 시작 파라미터
69
+ * @property {string} endpoint token-specific WHEP 요청 endpoint
70
+ * @property {string} identityKey camera와 endpoint 기준 stream family key
71
+ * @property {string} streamKey token generation까지 포함한 stream key
72
+ * @property {string} token WHEP Bearer token
73
+ * @property {HTMLVideoElement} video 최초 stream attach 대상 video
74
+ */
21
75
  interface CctvRtcStreamStartParams {
76
+ /**
77
+ * token-specific WHEP 요청 endpoint
78
+ */
22
79
  endpoint: string;
80
+ /**
81
+ * camera와 endpoint 기준 stream family key
82
+ */
23
83
  identityKey: string;
84
+ /**
85
+ * token generation까지 포함한 stream key
86
+ */
24
87
  streamKey: string;
88
+ /**
89
+ * WHEP Bearer token
90
+ */
25
91
  token: string;
92
+ /**
93
+ * 최초 stream attach 대상 video element
94
+ */
26
95
  video: HTMLVideoElement;
27
96
  }
28
97
 
98
+ /**
99
+ * CCTV Stream Registry Util; 외부 구독용 immutable snapshot 생성
100
+ * @util
101
+ * @param {CctvRtcStreamEntry} [entry] snapshot을 만들 registry entry
102
+ * @desc Map에 entry가 없을 때는 새 연결 전 기본 상태를 반환한다.
103
+ * @return {CctvRtcStreamSnapshot} 현재 entry 상태 또는 기본 idle 상태
104
+ */
29
105
  const getEntrySnapshot = (
30
106
  entry?: CctvRtcStreamEntry,
31
107
  ): CctvRtcStreamSnapshot => ({
@@ -35,12 +111,26 @@ const getEntrySnapshot = (
35
111
  streamError: entry?.streamError ?? null,
36
112
  });
37
113
 
114
+ /**
115
+ * CCTV Stream Registry Util; video element에 RTC stream 연결
116
+ * @util
117
+ * @param {HTMLVideoElement} video stream attach 대상 video
118
+ * @param {MediaStream | null} stream 표시할 MediaStream 또는 초기화용 null
119
+ * @desc inline autoplay 속성과 srcObject를 함께 적용한다.
120
+ */
38
121
  const attachVideo = (video: HTMLVideoElement, stream: MediaStream | null) => {
39
122
  video.playsInline = true;
40
123
  video.autoplay = true;
41
124
  video.srcObject = stream;
42
125
  };
43
126
 
127
+ /**
128
+ * CCTV Stream Registry Util; 지정 stream의 video 연결 해제
129
+ * @util
130
+ * @param {HTMLVideoElement} video cleanup 대상 video
131
+ * @param {MediaStream | null} stream 제거 대상 MediaStream
132
+ * @desc replacement stream이 이미 붙은 video를 이전 entry cleanup이 비우지 않게 한다.
133
+ */
44
134
  const clearVideoIfAttachedStream = (
45
135
  video: HTMLVideoElement,
46
136
  stream: MediaStream | null,
@@ -50,12 +140,25 @@ const clearVideoIfAttachedStream = (
50
140
  }
51
141
  };
52
142
 
143
+ /**
144
+ * CCTV Stream Registry Util; entry snapshot 구독자 통지
145
+ * @util
146
+ * @param {CctvRtcStreamEntry} entry 변경된 registry entry
147
+ * @desc 현재 entry의 immutable snapshot을 생성해 등록된 listener에 전달한다.
148
+ */
53
149
  const notifyEntry = (entry: CctvRtcStreamEntry) => {
54
150
  const snapshot = getEntrySnapshot(entry);
55
151
  entry.listeners.forEach(listener => listener(snapshot));
56
152
  };
57
153
 
154
+ /**
155
+ * CCTV Stream Registry Util; 단일 registry entry resource 정리
156
+ * @util
157
+ * @param {CctvRtcStreamEntry} entry 종료할 registry entry
158
+ * @desc AbortController와 WHEP handle cleanup은 각 구현의 idempotent 계약을 재사용한다.
159
+ */
58
160
  const closeEntry = (entry: CctvRtcStreamEntry) => {
161
+ // pending WHEP와 연결 완료 handle을 모두 닫되 다음 close가 중복 실행되지 않게 참조를 비운다.
59
162
  entry.controller?.abort();
60
163
  entry.controller = null;
61
164
  entry.handle?.close();
@@ -67,41 +170,95 @@ const closeEntry = (entry: CctvRtcStreamEntry) => {
67
170
  clearVideoIfAttachedStream(video, entry.stream);
68
171
  });
69
172
 
173
+ // 마지막 closed snapshot을 전달한 뒤 entry가 외부 객체를 붙잡지 않도록 집합을 비운다.
70
174
  notifyEntry(entry);
71
175
  entry.attachedVideos.clear();
72
176
  entry.listeners.clear();
73
177
  };
74
178
 
179
+ /**
180
+ * CCTV Stream Registry Types; Provider scope registry 계약
181
+ * @property {(streamKey: string, video: HTMLVideoElement) => () => void} attach video attach와 detach cleanup 등록
182
+ * @property {() => void} closeAll Provider scope의 모든 stream 종료
183
+ * @property {(identityKey: string, exceptStreamKey?: string) => void} closeByIdentity 같은 camera family의 stream 종료
184
+ * @property {(streamKey: string) => CctvRtcStreamSnapshot} getSnapshot stream snapshot 조회
185
+ * @property {(params: CctvRtcStreamStartParams) => void} start streamKey 단위 WHEP 시작
186
+ * @property {(streamKey: string, listener: (snapshot: CctvRtcStreamSnapshot) => void) => () => void} subscribe stream snapshot 구독
187
+ */
75
188
  export interface CctvRtcStreamRegistry {
189
+ /**
190
+ * video attach와 detach cleanup 등록
191
+ */
76
192
  attach: (streamKey: string, video: HTMLVideoElement) => () => void;
193
+ /**
194
+ * Provider scope의 모든 stream 종료
195
+ */
77
196
  closeAll: () => void;
197
+ /**
198
+ * 같은 camera family의 stream을 optional 예외 key 외에는 모두 종료
199
+ */
78
200
  closeByIdentity: (identityKey: string, exceptStreamKey?: string) => void;
201
+ /**
202
+ * streamKey의 현재 외부 구독 snapshot 조회
203
+ */
79
204
  getSnapshot: (streamKey: string) => CctvRtcStreamSnapshot;
205
+ /**
206
+ * streamKey 단위 WHEP 시작
207
+ */
80
208
  start: (params: CctvRtcStreamStartParams) => void;
209
+ /**
210
+ * streamKey snapshot 구독과 unsubscribe cleanup 등록
211
+ */
81
212
  subscribe: (
82
213
  streamKey: string,
83
214
  listener: (snapshot: CctvRtcStreamSnapshot) => void,
84
215
  ) => () => void;
85
216
  }
86
217
 
218
+ /**
219
+ * CCTV Stream Registry Util; Provider scope stream registry 생성
220
+ * @util
221
+ * @desc 같은 streamKey의 중복 WHEP start를 막고 camera identity별 replacement cleanup을 관리한다.
222
+ * @return {CctvRtcStreamRegistry} 독립된 Provider scope stream registry
223
+ * @example
224
+ * const registry = createCctvRtcStreamRegistry();
225
+ * registry.start({ endpoint, identityKey, streamKey, token, video });
226
+ */
87
227
  export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
228
+ // Map은 token generation이 포함된 streamKey마다 현재 lifecycle owner를 하나만 유지한다.
88
229
  const entries = new Map<string, CctvRtcStreamEntry>();
89
230
 
231
+ /**
232
+ * CCTV Stream Registry Util; entry resource와 Map owner 제거
233
+ * @util
234
+ * @param {CctvRtcStreamEntry} entry 삭제할 registry entry
235
+ * @desc entry cleanup을 수행한 뒤 streamKey의 Map owner를 제거한다.
236
+ */
90
237
  const deleteEntry = (entry: CctvRtcStreamEntry) => {
91
238
  closeEntry(entry);
92
239
  entries.delete(entry.streamKey);
93
240
  };
94
241
 
95
242
  return {
243
+ /**
244
+ * CCTV Stream Registry Util; streamKey entry에 video 연결
245
+ * @util
246
+ * @param {string} streamKey attach할 stream key
247
+ * @param {HTMLVideoElement} video attach 대상 video
248
+ * @desc 현재 stream을 video에 연결하고 attach 집합에 등록한다.
249
+ * @return {() => void} 현재 video attach만 해제하는 cleanup
250
+ */
96
251
  attach(streamKey, video) {
97
252
  const entry = entries.get(streamKey);
98
253
  if (!entry) {
254
+ // 아직 시작되지 않은 key는 stale srcObject 없이 빈 video 상태로 정규화한다.
99
255
  attachVideo(video, null);
100
256
  return () => {
101
257
  if (video.srcObject) video.srcObject = null;
102
258
  };
103
259
  }
104
260
 
261
+ // 기존 MediaStream이 있으면 즉시 reattach하고 pending entry면 현재 화면을 보존한다.
105
262
  entry.attachedVideos.add(video);
106
263
  if (entry.stream || !video.srcObject) {
107
264
  attachVideo(video, entry.stream);
@@ -112,11 +269,25 @@ export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
112
269
  clearVideoIfAttachedStream(video, entry.stream);
113
270
  };
114
271
  },
272
+ /**
273
+ * CCTV Stream Registry Util; Provider scope의 모든 stream 정리
274
+ * @util
275
+ * @desc 모든 entry resource를 닫고 registry Map을 비운다.
276
+ */
115
277
  closeAll() {
278
+ // Provider unmount에서 모든 entry를 한 번 닫은 뒤 registry owner Map을 비운다.
116
279
  entries.forEach(entry => closeEntry(entry));
117
280
  entries.clear();
118
281
  },
282
+ /**
283
+ * CCTV Stream Registry Util; 같은 identity의 이전 stream 정리
284
+ * @util
285
+ * @param {string} identityKey cleanup할 camera stream family key
286
+ * @param {string} [exceptStreamKey] 유지할 replacement stream key
287
+ * @desc optional 예외 key를 제외한 같은 camera family entry를 닫는다.
288
+ */
119
289
  closeByIdentity(identityKey, exceptStreamKey) {
290
+ // replacement track이 붙은 뒤 같은 camera family의 이전 generation만 선택적으로 닫는다.
120
291
  entries.forEach(entry => {
121
292
  if (
122
293
  entry.identityKey === identityKey &&
@@ -126,15 +297,36 @@ export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
126
297
  }
127
298
  });
128
299
  },
300
+ /**
301
+ * CCTV Stream Registry Util; streamKey snapshot 조회
302
+ * @util
303
+ * @param {string} streamKey 조회할 stream key
304
+ * @desc 내부 mutable entry를 노출하지 않고 외부 구독용 snapshot만 반환한다.
305
+ * @return {CctvRtcStreamSnapshot} 현재 entry 상태 또는 기본 idle 상태
306
+ */
129
307
  getSnapshot(streamKey) {
308
+ // 외부 consumer가 내부 mutable entry를 직접 소유하지 않도록 snapshot만 반환한다.
130
309
  return getEntrySnapshot(entries.get(streamKey));
131
310
  },
311
+ /**
312
+ * CCTV Stream Registry Util; streamKey 단위 WHEP 연결 시작
313
+ * @util
314
+ * @param {CctvRtcStreamStartParams} params WHEP 시작 파라미터
315
+ * @property {string} params.endpoint token-specific WHEP 요청 endpoint
316
+ * @property {string} params.identityKey camera와 endpoint 기준 stream family key
317
+ * @property {string} params.streamKey token generation까지 포함한 stream key
318
+ * @property {string} params.token WHEP Bearer token
319
+ * @property {HTMLVideoElement} params.video 최초 stream attach 대상 video element
320
+ * @desc 같은 streamKey의 중복 start를 막고 현재 entry의 lifecycle만 갱신한다.
321
+ */
132
322
  start({ endpoint, identityKey, streamKey, token, video }) {
133
323
  const existingEntry = entries.get(streamKey);
134
324
  if (existingEntry) {
325
+ // 같은 token generation의 concurrent attach는 기존 WHEP start 하나를 공유한다.
135
326
  return;
136
327
  }
137
328
 
329
+ // WHEP promise보다 entry를 먼저 등록해 같은 tick의 중복 start를 차단한다.
138
330
  const controller = new AbortController();
139
331
  const entry: CctvRtcStreamEntry = {
140
332
  attachedVideos: new Set([video]),
@@ -150,7 +342,17 @@ export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
150
342
  };
151
343
 
152
344
  entries.set(streamKey, entry);
345
+ /**
346
+ * CCTV Stream Registry Util; 현재 entry의 lifecycle owner 여부 확인
347
+ * @util
348
+ * @desc Map owner 일치와 AbortSignal 상태를 함께 확인한다.
349
+ * @return {boolean} callback과 settlement 반영 가능 여부
350
+ */
351
+ const isCurrentEntry = () =>
352
+ entries.get(streamKey) === entry && !controller.signal.aborted;
353
+
153
354
  if (!video.srcObject) {
355
+ // cold start는 빈 video를 초기화하고 replacement는 기존 화면을 그대로 유지한다.
154
356
  attachVideo(video, null);
155
357
  } else {
156
358
  video.playsInline = true;
@@ -163,10 +365,16 @@ export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
163
365
  video,
164
366
  signal: controller.signal,
165
367
  onConnectionStateChange: state => {
368
+ // Provider cleanup 또는 같은 key owner 교체 뒤 늦은 state event는 반영하지 않는다.
369
+ if (!isCurrentEntry()) return;
370
+
166
371
  entry.connectionState = state;
167
372
  notifyEntry(entry);
168
373
  },
169
374
  onTrack: event => {
375
+ // 닫힌 entry의 late track이 video와 snapshot을 되살리지 않게 막는다.
376
+ if (!isCurrentEntry()) return;
377
+
170
378
  entry.stream = event.streams[0] ?? null;
171
379
  entry.attachedVideos.forEach(attachedVideo =>
172
380
  attachVideo(attachedVideo, entry.stream),
@@ -175,11 +383,22 @@ export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
175
383
  },
176
384
  })
177
385
  .then(handle => {
386
+ // pending 중 close된 start가 늦게 resolve되면 새 handle을 즉시 회수한다.
387
+ if (!isCurrentEntry()) {
388
+ handle.close();
389
+ return;
390
+ }
391
+
178
392
  entry.handle = handle;
179
393
  entry.isStreaming = false;
180
394
  notifyEntry(entry);
181
395
  })
182
396
  .catch(error => {
397
+ // abort/delete된 이전 owner의 rejection은 현재 entry 상태를 덮어쓰지 않는다.
398
+ if (entries.get(streamKey) !== entry) return;
399
+
400
+ // 실패 원인이 어느 negotiation 단계이든 util-rtc 내부 PeerConnection cleanup을 요청한다.
401
+ entry.controller?.abort();
183
402
  entry.controller = null;
184
403
  entry.handle = null;
185
404
  entry.isStreaming = false;
@@ -191,13 +410,24 @@ export function createCctvRtcStreamRegistry(): CctvRtcStreamRegistry {
191
410
  entry.attachedVideos.forEach(attachedVideo =>
192
411
  clearVideoIfAttachedStream(attachedVideo, entry.stream),
193
412
  );
413
+ // subscriber에는 terminal error를 전달하되 실패 entry는 다음 명시적 start를 막지 않는다.
194
414
  notifyEntry(entry);
415
+ entries.delete(streamKey);
195
416
  });
196
417
  },
418
+ /**
419
+ * CCTV Stream Registry Util; streamKey snapshot listener 등록
420
+ * @util
421
+ * @param {string} streamKey 구독할 stream key
422
+ * @param {(snapshot: CctvRtcStreamSnapshot) => void} listener snapshot listener
423
+ * @desc 등록 직후 현재 snapshot을 전달하고 이후 상태 변경을 구독한다.
424
+ * @return {() => void} 현재 listener만 제거하는 unsubscribe
425
+ */
197
426
  subscribe(streamKey, listener) {
198
427
  const entry = entries.get(streamKey);
199
428
  if (!entry) return () => undefined;
200
429
 
430
+ // 구독 직후 현재 snapshot을 전달해 registry reattach의 첫 render 지연을 줄인다.
201
431
  entry.listeners.add(listener);
202
432
  listener(getEntrySnapshot(entry));
203
433
 
@@ -35,7 +35,7 @@ import { cctvRtcLiveRegistryAtom } from "../jotai/rtc";
35
35
  * selectedCompany, // 선택된 회사 데이터
36
36
  * selectedCam, // 선택된 카메라 데이터
37
37
  * groups, // 회사 그룹/회사/카메라 리스트 구조 데이터
38
- * companyValidList, // 온라인 카메라가 하나라도 있는 회사 ID 리스트
38
+ * companyValidList, // 카메라가 하나라도 있는 회사 ID 리스트
39
39
  * cams, // 선택된 회사의 카메라 리스트
40
40
  * onToggleCamera(cam_id), // 카메라 선택 토글 함수
41
41
  * onOpenCamera(cam_id), // 카메라 선택 함수
@@ -159,14 +159,12 @@ export default function useCctvContext(): UseCctvContextReturn {
159
159
  [selectedCamId, selectedCompanyId, groups],
160
160
  );
161
161
 
162
- // 온라인 카메라가 하나라도 있는 company_id 필터링한다.
162
+ // cam_online 상태와 무관하게 카메라가 하나라도 있는 company_id 반환한다.
163
163
  const companyValidList = useMemo(
164
164
  () =>
165
165
  groups
166
166
  .flatMap(({ list }) => list)
167
- .filter(
168
- ({ cam_list }) => !cam_list.every(({ cam_online }) => !cam_online),
169
- )
167
+ .filter(({ cam_list }) => cam_list.length > 0)
170
168
  .map(({ company_id }) => company_id),
171
169
  [groups],
172
170
  );
@@ -144,7 +144,7 @@ export interface API_Res_CctvCameraData
144
144
  * CCTV: 업체에 설치된 카메라 목록
145
145
  * @property {string} cam_id 카메라 id코드
146
146
  * @property {string} cam_name 카메라 별칭
147
- * @property {boolean} cam_online 카메라 설치상태
147
+ * @property {boolean} cam_online 카메라 네트워크 연결 상태
148
148
  * @property {string} cam_rtc 카메라 스트리밍 WebRTC 경로
149
149
  * @property {string} cam_rtcp 카메라 RTCP 경로
150
150
  * @property {string} cam_rtsp 카메라 RTSP 경로
@@ -156,7 +156,7 @@ export interface API_Res_CctvCameraData
156
156
  export interface API_Res_CctvCompanyCameraList extends API_Res_CctvCameraData {
157
157
  // API 응답은 데이터-only 구조이므로 UI 전용 콜백은 포함하지 않는다.
158
158
  /**
159
- * 카메라 설치 상태
159
+ * 카메라 네트워크 연결 상태
160
160
  */
161
161
  cam_online: boolean;
162
162
  }
@@ -195,7 +195,7 @@ export interface UseCctvCompanyDataReturn extends Omit<
195
195
  * @property {CctvCompanyGroupItems | undefined} selectedCompany 선택된 업체 데이터
196
196
  * @property {CctvCompanyCameraData | undefined} selectedCam 선택된 카메라 데이터
197
197
  * @property {CctvCompanyGroup[]} groups 업체 그룹/업체/카메라 리스트 구조 데이터
198
- * @property {string[]} companyValidList 온라인 카메라가 하나라도 있는 업체 ID 리스트
198
+ * @property {string[]} companyValidList 카메라가 하나라도 있는 업체 ID 리스트
199
199
  * @property {CctvCompanyCameraList[]} cams 선택된 업체의 카메라 리스트
200
200
  * @property {number} liveCamCount 실시간 스트리밍 중인 카메라 수
201
201
  * @property {(cam_id: string) => void} onToggleCamera 카메라 선택 토글 함수
@@ -244,7 +244,7 @@ export interface UseCctvContextReturn extends UseFormReturn<CctvBaseContext> {
244
244
  */
245
245
  groups: CctvCompanyGroup[];
246
246
  /**
247
- * 스트리밍이 가능한 업체 목록
247
+ * 카메라가 하나라도 있는 업체 목록
248
248
  */
249
249
  companyValidList: string[];
250
250
  /**
@@ -10,7 +10,7 @@ import type {
10
10
  * @property {Function} [onSelect] 카메라 선택 콜백 이벤트
11
11
  * @property {string} cam_id 카메라 id코드
12
12
  * @property {string} cam_name 카메라 별칭
13
- * @property {boolean} cam_online 카메라 설치상태
13
+ * @property {boolean} cam_online 카메라 네트워크 연결 상태
14
14
  * @property {string} [cam_rtc] 카메라 스트리밍 WebRTC 경로
15
15
  * @property {string} [cam_rtcp] 카메라 RTCP 경로
16
16
  * @property {string} [cam_rtsp] 카메라 RTSP 경로
@@ -46,7 +46,7 @@ export interface CctvCompanyCameraData extends API_Res_CctvCompanyCameraList {
46
46
  * @property {Function} [onSelect] 카메라 선택 콜백 이벤트
47
47
  * @property {string} cam_id 카메라 id코드
48
48
  * @property {string} cam_name 카메라 별칭
49
- * @property {boolean} cam_online 카메라 설치상태
49
+ * @property {boolean} cam_online 카메라 네트워크 연결 상태
50
50
  * @property {string} [cam_rtc] 카메라 스트리밍 WebRTC 경로
51
51
  * @property {string} [cam_rtcp] 카메라 RTCP 경로
52
52
  * @property {string} [cam_rtsp] 카메라 RTSP 경로
@@ -13,6 +13,7 @@ export const CCTV_MESSAGE = {
13
13
  "장시간 미사용으로 연결이 종료되었습니다. 계속 확인하시려면 다시 연결해 주세요.",
14
14
  tokenError: "토큰을 발급하지 못했습니다.",
15
15
  offline: "CCTV 연결 오류",
16
+ networkDisconnected: "네트워크 연결이 끊어졌습니다.",
16
17
  } as const;
17
18
 
18
19
  const RTC_PREPARING_STATES = new Set<RTCPeerConnectionState>([
@@ -56,7 +57,7 @@ export function getOverlayMessage({
56
57
  return isFetching ? CCTV_MESSAGE.fetching : CCTV_MESSAGE.selectCam;
57
58
  }
58
59
 
59
- if (!cam.cam_online) return CCTV_MESSAGE.offline;
60
+ if (!cam.cam_online) return CCTV_MESSAGE.networkDisconnected;
60
61
  // 에러 상태가 준비 상태와 겹칠 때는 에러 문구가 최종 표시 계약을 우선한다.
61
62
  if (isTokenError) return CCTV_MESSAGE.tokenError;
62
63
  if (streamError) return streamError;