@uniai-fe/uds-templates 0.11.7 → 0.12.1
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/README.md +7 -0
- package/package.json +9 -9
- package/src/cctv/hooks/index.tsx +7 -1
- package/src/cctv/hooks/useFullscreen.ts +212 -0
- package/src/cctv/types/hook.ts +39 -0
package/README.md
CHANGED
|
@@ -39,6 +39,13 @@ import { Frame, Modal } from "@uniai-fe/uds-templates";
|
|
|
39
39
|
|
|
40
40
|
`@uniai-fe/uds-templates/src/**`와 export map에 없는 category path는 public API가 아니다.
|
|
41
41
|
|
|
42
|
+
### CCTV 전체화면
|
|
43
|
+
|
|
44
|
+
`useCctvFullscreen`은 서비스가 지정한 container를 전체화면으로 전환한다.
|
|
45
|
+
화면 방향 API를 지원하면 전체화면 동안 `landscape`를 요청하며, 종료·대상 제거 시
|
|
46
|
+
고정을 해제해 기본 방향 정책으로 돌아간다. 세로 방향 복귀를 강제하지 않는다.
|
|
47
|
+
방향 API 미지원·요청 거부는 전체화면 시청을 차단하지 않는다.
|
|
48
|
+
|
|
42
49
|
## Styles
|
|
43
50
|
|
|
44
51
|
CSS consumer는 dependency order대로 각 public entry를 로드한다.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniai-fe/uds-templates",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"description": "UNIAI Design System; UI Templates Package",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -67,17 +67,17 @@
|
|
|
67
67
|
"react-hook-form": "^7.84.0",
|
|
68
68
|
"sass": "^1.101.7",
|
|
69
69
|
"typescript": "6.0.3",
|
|
70
|
-
"@uniai-fe/
|
|
71
|
-
"@uniai-fe/tsconfig": "0.2.0",
|
|
72
|
-
"@uniai-fe/uds-primitives": "0.12.5",
|
|
73
|
-
"@uniai-fe/uds-foundation": "0.6.0",
|
|
74
|
-
"@uniai-fe/util-functions": "0.4.3",
|
|
70
|
+
"@uniai-fe/next-devkit": "0.4.0",
|
|
75
71
|
"@uniai-fe/eslint-config": "0.4.2",
|
|
72
|
+
"@uniai-fe/uds-primitives": "0.12.5",
|
|
76
73
|
"@uniai-fe/util-jotai": "0.3.0",
|
|
77
|
-
"@uniai-fe/
|
|
74
|
+
"@uniai-fe/react-hooks": "0.3.0",
|
|
75
|
+
"@uniai-fe/util-api": "0.2.1",
|
|
76
|
+
"@uniai-fe/tsconfig": "0.2.0",
|
|
78
77
|
"@uniai-fe/util-rtc": "0.2.1",
|
|
79
|
-
"@uniai-fe/
|
|
80
|
-
"@uniai-fe/
|
|
78
|
+
"@uniai-fe/util-functions": "0.4.3",
|
|
79
|
+
"@uniai-fe/uds-foundation": "0.6.0",
|
|
80
|
+
"@uniai-fe/util-next": "0.5.0"
|
|
81
81
|
},
|
|
82
82
|
"scripts": {
|
|
83
83
|
"check:pre-commit": "pnpm --dir ../../.. run check:pre-commit",
|
package/src/cctv/hooks/index.tsx
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import useCctvCompanyData from "./useCompanyData";
|
|
2
2
|
import useCctvContext from "./useContext";
|
|
3
3
|
import { useCctvRtcStream } from "./useRtcStream";
|
|
4
|
+
import { useCctvFullscreen } from "./useFullscreen";
|
|
4
5
|
|
|
5
|
-
export {
|
|
6
|
+
export {
|
|
7
|
+
useCctvContext,
|
|
8
|
+
useCctvCompanyData,
|
|
9
|
+
useCctvRtcStream,
|
|
10
|
+
useCctvFullscreen,
|
|
11
|
+
};
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
|
+
import type { UseCctvFullscreenReturn } from "../types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* CCTV; service-owned viewer container fullscreen state
|
|
8
|
+
* @hook
|
|
9
|
+
* @desc 대상 element의 표준 Fullscreen API 지원 여부와 실제 document 상태를 동기화한다.
|
|
10
|
+
* 요청 실패는 기존 viewer를 유지하며, 외부 종료 후에는 진입 전 focus를 복원한다.
|
|
11
|
+
* 지원 환경에서는 진입 후 가로 방향을 요청하고 종료·대상 제거 시 기본 방향 정책으로 돌려준다.
|
|
12
|
+
* 방향 요청 실패는 전체화면 성공에 영향을 주지 않으며 종료 후 세로 방향을 강제하지 않는다.
|
|
13
|
+
* @return {UseCctvFullscreenReturn} 대상 ref callback과 전체화면 상태·action
|
|
14
|
+
*/
|
|
15
|
+
export function useCctvFullscreen(): UseCctvFullscreenReturn {
|
|
16
|
+
const [targetElement, setTargetElement] = useState<HTMLElement | null>(null);
|
|
17
|
+
const [isFullscreen, setIsFullscreen] = useState(false);
|
|
18
|
+
const targetElementRef = useRef<HTMLElement | null>(null);
|
|
19
|
+
const entryFocusRef = useRef<HTMLElement | null>(null);
|
|
20
|
+
const wasFullscreenRef = useRef(false);
|
|
21
|
+
const unlockOrientationRef = useRef<(() => void) | null>(null);
|
|
22
|
+
|
|
23
|
+
const releaseOrientation = useCallback(() => {
|
|
24
|
+
const unlock = unlockOrientationRef.current;
|
|
25
|
+
unlockOrientationRef.current = null;
|
|
26
|
+
try {
|
|
27
|
+
unlock?.();
|
|
28
|
+
} catch {
|
|
29
|
+
// 숨겨진 document 등에서 해제가 거부돼도 전체화면 종료와 focus 복원을 유지한다.
|
|
30
|
+
}
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* service가 선택한 전체화면 대상 element를 hook lifecycle에 연결한다.
|
|
35
|
+
*/
|
|
36
|
+
const targetRef = useCallback(
|
|
37
|
+
(element: HTMLElement | null) => {
|
|
38
|
+
if (targetElementRef.current !== element) releaseOrientation();
|
|
39
|
+
targetElementRef.current = element;
|
|
40
|
+
setTargetElement(element);
|
|
41
|
+
|
|
42
|
+
if (element === null) {
|
|
43
|
+
setIsFullscreen(false);
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
[releaseOrientation],
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 외부 종료 후에도 사용자가 시작한 control로 keyboard focus를 돌려보낸다.
|
|
51
|
+
*/
|
|
52
|
+
const restoreEntryFocus = useCallback(() => {
|
|
53
|
+
const entryFocus = entryFocusRef.current;
|
|
54
|
+
entryFocusRef.current = null;
|
|
55
|
+
|
|
56
|
+
if (entryFocus?.isConnected) {
|
|
57
|
+
entryFocus.focus();
|
|
58
|
+
}
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (targetElement === null) return;
|
|
63
|
+
|
|
64
|
+
const ownerDocument = targetElement.ownerDocument;
|
|
65
|
+
|
|
66
|
+
// request Promise가 아니라 browser event를 실제 전체화면 상태의 기준으로 사용한다.
|
|
67
|
+
const synchronizeFullscreenState = () => {
|
|
68
|
+
const nextIsFullscreen =
|
|
69
|
+
ownerDocument.fullscreenElement === targetElement;
|
|
70
|
+
|
|
71
|
+
setIsFullscreen(nextIsFullscreen);
|
|
72
|
+
|
|
73
|
+
if (!nextIsFullscreen && wasFullscreenRef.current) {
|
|
74
|
+
releaseOrientation();
|
|
75
|
+
restoreEntryFocus();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
wasFullscreenRef.current = nextIsFullscreen;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
ownerDocument.addEventListener(
|
|
82
|
+
"fullscreenchange",
|
|
83
|
+
synchronizeFullscreenState,
|
|
84
|
+
);
|
|
85
|
+
ownerDocument.addEventListener(
|
|
86
|
+
"fullscreenerror",
|
|
87
|
+
synchronizeFullscreenState,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
return () => {
|
|
91
|
+
ownerDocument.removeEventListener(
|
|
92
|
+
"fullscreenchange",
|
|
93
|
+
synchronizeFullscreenState,
|
|
94
|
+
);
|
|
95
|
+
ownerDocument.removeEventListener(
|
|
96
|
+
"fullscreenerror",
|
|
97
|
+
synchronizeFullscreenState,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
releaseOrientation();
|
|
101
|
+
if (ownerDocument.fullscreenElement === targetElement) {
|
|
102
|
+
void ownerDocument.exitFullscreen().catch(() => undefined);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (wasFullscreenRef.current) {
|
|
106
|
+
restoreEntryFocus();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
wasFullscreenRef.current = false;
|
|
110
|
+
};
|
|
111
|
+
}, [releaseOrientation, restoreEntryFocus, targetElement]);
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 현재 service target의 표준 전체화면 진입을 요청한다.
|
|
115
|
+
*/
|
|
116
|
+
const enterFullscreen = useCallback(async () => {
|
|
117
|
+
const target = targetElementRef.current;
|
|
118
|
+
|
|
119
|
+
if (
|
|
120
|
+
target === null ||
|
|
121
|
+
!target.ownerDocument.fullscreenEnabled ||
|
|
122
|
+
typeof target.requestFullscreen !== "function"
|
|
123
|
+
) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (target.ownerDocument.fullscreenElement === target) {
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const activeElement = target.ownerDocument.activeElement;
|
|
132
|
+
entryFocusRef.current =
|
|
133
|
+
typeof HTMLElement !== "undefined" && activeElement instanceof HTMLElement
|
|
134
|
+
? activeElement
|
|
135
|
+
: null;
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
await target.requestFullscreen();
|
|
139
|
+
const orientation = target.ownerDocument.defaultView?.screen.orientation;
|
|
140
|
+
if (
|
|
141
|
+
targetElementRef.current === target &&
|
|
142
|
+
target.ownerDocument.fullscreenElement === target &&
|
|
143
|
+
typeof orientation?.lock === "function" &&
|
|
144
|
+
typeof orientation.unlock === "function"
|
|
145
|
+
) {
|
|
146
|
+
// 대기 중에도 종료가 pending lock을 취소할 수 있도록 먼저 소유권을 기록한다.
|
|
147
|
+
const unlock = () => orientation.unlock();
|
|
148
|
+
unlockOrientationRef.current = unlock;
|
|
149
|
+
try {
|
|
150
|
+
await orientation.lock("landscape");
|
|
151
|
+
} catch {
|
|
152
|
+
// 이전 요청의 거부가 이후 전체화면 세션의 해제 책임을 지우지 않도록 구분한다.
|
|
153
|
+
if (unlockOrientationRef.current === unlock) {
|
|
154
|
+
unlockOrientationRef.current = null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return true;
|
|
159
|
+
} catch {
|
|
160
|
+
entryFocusRef.current = null;
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}, []);
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 다른 element의 전체화면은 건드리지 않고 현재 target 소유 상태만 종료한다.
|
|
167
|
+
*/
|
|
168
|
+
const exitFullscreen = useCallback(async () => {
|
|
169
|
+
const target = targetElementRef.current;
|
|
170
|
+
|
|
171
|
+
if (
|
|
172
|
+
target === null ||
|
|
173
|
+
target.ownerDocument.fullscreenElement !== target ||
|
|
174
|
+
typeof target.ownerDocument.exitFullscreen !== "function"
|
|
175
|
+
) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
await target.ownerDocument.exitFullscreen();
|
|
181
|
+
releaseOrientation();
|
|
182
|
+
return true;
|
|
183
|
+
} catch {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}, [releaseOrientation]);
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 현재 document 소유 상태에 따라 target의 진입 또는 종료 action을 선택한다.
|
|
190
|
+
*/
|
|
191
|
+
const toggleFullscreen = useCallback(async () => {
|
|
192
|
+
const target = targetElementRef.current;
|
|
193
|
+
|
|
194
|
+
if (target?.ownerDocument.fullscreenElement === target) {
|
|
195
|
+
return exitFullscreen();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return enterFullscreen();
|
|
199
|
+
}, [enterFullscreen, exitFullscreen]);
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
targetRef,
|
|
203
|
+
isFullscreenSupported:
|
|
204
|
+
targetElement !== null &&
|
|
205
|
+
targetElement.ownerDocument.fullscreenEnabled &&
|
|
206
|
+
typeof targetElement.requestFullscreen === "function",
|
|
207
|
+
isFullscreen,
|
|
208
|
+
enterFullscreen,
|
|
209
|
+
exitFullscreen,
|
|
210
|
+
toggleFullscreen,
|
|
211
|
+
};
|
|
212
|
+
}
|
package/src/cctv/types/hook.ts
CHANGED
|
@@ -13,6 +13,45 @@ import type {
|
|
|
13
13
|
import type { UseFormReturn } from "react-hook-form";
|
|
14
14
|
import type { CctvBaseContext } from "./context";
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* CCTV; useCctvFullscreen return
|
|
18
|
+
* @property {React.RefCallback<HTMLElement>} targetRef 전체화면 대상 viewer container ref callback
|
|
19
|
+
* @property {boolean} isFullscreenSupported 대상과 브라우저의 표준 Fullscreen API 지원 여부
|
|
20
|
+
* @property {boolean} isFullscreen 현재 대상이 실제 전체화면인지 여부
|
|
21
|
+
* @property {() => Promise<boolean>} enterFullscreen 대상 전체화면 진입 action과 성공 여부
|
|
22
|
+
* @property {() => Promise<boolean>} exitFullscreen 현재 대상 소유 전체화면 종료 action과 성공 여부
|
|
23
|
+
* @property {() => Promise<boolean>} toggleFullscreen 현재 대상의 전체화면 진입·종료 action과 성공 여부
|
|
24
|
+
*/
|
|
25
|
+
export interface UseCctvFullscreenReturn {
|
|
26
|
+
/**
|
|
27
|
+
* 전체화면 대상 viewer container ref callback
|
|
28
|
+
*/
|
|
29
|
+
targetRef: React.RefCallback<HTMLElement>;
|
|
30
|
+
/**
|
|
31
|
+
* 대상과 브라우저의 표준 Fullscreen API 지원 여부
|
|
32
|
+
*/
|
|
33
|
+
isFullscreenSupported: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* 현재 대상이 실제 전체화면인지 여부
|
|
36
|
+
*/
|
|
37
|
+
isFullscreen: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* 대상 전체화면 진입 action
|
|
40
|
+
* @return 진입 요청 성공 여부
|
|
41
|
+
*/
|
|
42
|
+
enterFullscreen: () => Promise<boolean>;
|
|
43
|
+
/**
|
|
44
|
+
* 현재 대상 소유 전체화면 종료 action
|
|
45
|
+
* @return 종료 요청 성공 여부
|
|
46
|
+
*/
|
|
47
|
+
exitFullscreen: () => Promise<boolean>;
|
|
48
|
+
/**
|
|
49
|
+
* 현재 대상의 전체화면 진입·종료 action
|
|
50
|
+
* @return 요청 성공 여부
|
|
51
|
+
*/
|
|
52
|
+
toggleFullscreen: () => Promise<boolean>;
|
|
53
|
+
}
|
|
54
|
+
|
|
16
55
|
/**
|
|
17
56
|
* CCTV; RTC stream 재연결 trigger
|
|
18
57
|
*/
|