@uniai-fe/uds-templates 0.1.33 → 0.1.35
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/dist/styles.css +19 -15
- package/package.json +1 -1
- package/src/cctv/apis/server.ts +8 -6
- package/src/cctv/components/cam-list/Item.tsx +30 -49
- package/src/cctv/components/pagination/list/Item.tsx +20 -43
- package/src/cctv/components/video/Template.tsx +49 -0
- package/src/cctv/components/video/overlay/footer/Container.tsx +2 -7
- package/src/cctv/components/video/overlay/footer/OpenButton.tsx +2 -4
- package/src/cctv/components/video/overlay/header/Container.tsx +10 -18
- package/src/cctv/components/video/overlay/header/LiveState.tsx +3 -3
- package/src/cctv/components/viewer/desktop/Container.tsx +5 -2
- package/src/cctv/components/viewer/desktop/Video.tsx +32 -64
- package/src/cctv/hooks/useCompanyData.tsx +28 -3
- package/src/cctv/hooks/useContext.ts +42 -5
- package/src/cctv/hooks/useRtcStream.ts +50 -11
- package/src/cctv/styles/variables.scss +6 -6
- package/src/cctv/styles/video.scss +14 -9
- package/src/cctv/types/hook.ts +199 -0
- package/src/cctv/types/index.ts +3 -0
- package/src/cctv/types/list.ts +20 -7
- package/src/cctv/types/props.ts +100 -0
- package/src/cctv/types/video-state.ts +57 -0
- package/src/cctv/utils/video-state.ts +93 -0
package/dist/styles.css
CHANGED
|
@@ -55,12 +55,12 @@
|
|
|
55
55
|
rgba(0, 0, 0, 0.2) 100%
|
|
56
56
|
);
|
|
57
57
|
/* Live State */
|
|
58
|
-
--cctv-live-state-bg-
|
|
59
|
-
--cctv-live-state-dot-
|
|
60
|
-
--cctv-live-state-text-
|
|
61
|
-
--cctv-live-state-bg-
|
|
62
|
-
--cctv-live-state-dot-
|
|
63
|
-
--cctv-live-state-text-
|
|
58
|
+
--cctv-live-state-bg-on: var(--color-cool-gray-10);
|
|
59
|
+
--cctv-live-state-dot-on: var(--color-feedback-new);
|
|
60
|
+
--cctv-live-state-text-on: var(--color-common-99);
|
|
61
|
+
--cctv-live-state-bg-off: var(--color-cool-gray-55);
|
|
62
|
+
--cctv-live-state-dot-off: var(--color-border-strong);
|
|
63
|
+
--cctv-live-state-text-off: var(--color-label-disabled);
|
|
64
64
|
/* Error */
|
|
65
65
|
--cctv-error-text-color: var(--color-label-disabled);
|
|
66
66
|
--cctv-error-icon-color: var(--color-label-disabled);
|
|
@@ -1028,6 +1028,9 @@
|
|
|
1028
1028
|
width: 100%;
|
|
1029
1029
|
height: fit-content;
|
|
1030
1030
|
}
|
|
1031
|
+
.cctv-video-overlay-header.empty {
|
|
1032
|
+
height: calc(var(--font-heading-xsmall-size) * 1.5);
|
|
1033
|
+
}
|
|
1031
1034
|
|
|
1032
1035
|
.cctv-video-overlay-header-upper,
|
|
1033
1036
|
.cctv-video-overlay-header-lower {
|
|
@@ -1038,23 +1041,24 @@
|
|
|
1038
1041
|
|
|
1039
1042
|
.cctv-video-live-state {
|
|
1040
1043
|
--theme-badge-font-weight: 400;
|
|
1044
|
+
--badge-fill-bg-color: var(--cctv-live-state-bg-off);
|
|
1045
|
+
--badge-fill-label-color: var(--cctv-live-state-text-off);
|
|
1041
1046
|
margin-right: 4px;
|
|
1042
1047
|
}
|
|
1048
|
+
.cctv-video-live-state.on {
|
|
1049
|
+
--badge-fill-bg-color: var(--cctv-live-state-bg-on);
|
|
1050
|
+
--badge-fill-label-color: var(--cctv-live-state-text-on);
|
|
1051
|
+
}
|
|
1052
|
+
.cctv-video-live-state.on .cctv-video-live-state-dot {
|
|
1053
|
+
background: var(--cctv-live-state-dot-on);
|
|
1054
|
+
}
|
|
1043
1055
|
|
|
1044
1056
|
.cctv-video-live-state-dot {
|
|
1045
1057
|
width: 4px;
|
|
1046
1058
|
height: 4px;
|
|
1047
1059
|
margin-right: 4px;
|
|
1048
1060
|
border-radius: 4px;
|
|
1049
|
-
background: var(--cctv-live-state-dot-
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
.cctv-video-live-state:where(.disabled) {
|
|
1053
|
-
--badge-fill-bg-color: var(--cctv-live-state-bg-disabled);
|
|
1054
|
-
--badge-fill-label-color: var(--cctv-live-state-text-disabled);
|
|
1055
|
-
}
|
|
1056
|
-
.cctv-video-live-state:where(.disabled) .cctv-video-live-state-dot {
|
|
1057
|
-
background: var(--cctv-live-state-dot-disabled);
|
|
1061
|
+
background: var(--cctv-live-state-dot-off);
|
|
1058
1062
|
}
|
|
1059
1063
|
|
|
1060
1064
|
.cctv-video-close-button {
|
package/package.json
CHANGED
package/src/cctv/apis/server.ts
CHANGED
|
@@ -163,12 +163,14 @@ export async function getServerCompanyList({
|
|
|
163
163
|
// searchParams,
|
|
164
164
|
};
|
|
165
165
|
|
|
166
|
-
if (!queryUrl && !username)
|
|
166
|
+
if (!queryUrl && !username) {
|
|
167
|
+
nextAPILog("GET", routeUrl, query_url, { searchParams });
|
|
167
168
|
return {
|
|
168
169
|
res: alternateResponse,
|
|
169
170
|
...API_OPTION,
|
|
170
171
|
options: { status: 400, statusText: "유저 아이디를 확인할 수 없습니다." },
|
|
171
172
|
};
|
|
173
|
+
}
|
|
172
174
|
|
|
173
175
|
// 요청 URL 구성
|
|
174
176
|
const url = generateBackendQueryUrl_GET({
|
|
@@ -242,6 +244,7 @@ export async function getServerCctvToken({
|
|
|
242
244
|
};
|
|
243
245
|
|
|
244
246
|
if (!reqBody || !reqBody.username || !reqBody.company_id || !reqBody.cam_id) {
|
|
247
|
+
nextAPILog("POST", routeUrl, query_url, { reqBody });
|
|
245
248
|
return {
|
|
246
249
|
res: alternateResponse,
|
|
247
250
|
...API_OPTION,
|
|
@@ -270,11 +273,10 @@ export async function getServerCctvToken({
|
|
|
270
273
|
path,
|
|
271
274
|
};
|
|
272
275
|
|
|
276
|
+
nextAPILog("POST", routeUrl, query_url, { bodyData });
|
|
277
|
+
|
|
273
278
|
try {
|
|
274
|
-
const res = await fetchWithBody<
|
|
275
|
-
API_Req_CctvRtcTokenOrigin,
|
|
276
|
-
API_Res_CctvRtcToken
|
|
277
|
-
>({
|
|
279
|
+
const res = await fetchWithBody<string, API_Res_CctvRtcToken>({
|
|
278
280
|
method: "POST",
|
|
279
281
|
routeUrl,
|
|
280
282
|
...API_OPTION,
|
|
@@ -283,7 +285,7 @@ export async function getServerCctvToken({
|
|
|
283
285
|
"Content-Type": "application/json",
|
|
284
286
|
...(headers || {}),
|
|
285
287
|
},
|
|
286
|
-
bodyData,
|
|
288
|
+
body: JSON.stringify(bodyData),
|
|
287
289
|
alternateResponse,
|
|
288
290
|
});
|
|
289
291
|
|
|
@@ -1,45 +1,31 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
import clsx from "clsx";
|
|
2
|
-
import type {
|
|
3
|
-
import CCTVVideoContainer from "../video/Container";
|
|
4
|
-
import CCTVVideoOverlayBody from "../video/overlay/body/Container";
|
|
5
|
-
import CCTVVideoError from "../video/overlay/body/Error";
|
|
6
|
-
import CCTVVideoOverlayContainer from "../video/overlay/Container";
|
|
7
|
-
import CCTVVideoOverlayFooter from "../video/overlay/footer/Container";
|
|
8
|
-
import CCTVVideoOverlayHeader from "../video/overlay/header/Container";
|
|
9
|
-
import CCTVVideoContents from "../video/Video";
|
|
4
|
+
import type { CctvCompanyCameraData } from "../../types";
|
|
10
5
|
import { useCctvRtcStream } from "../../hooks";
|
|
11
6
|
import { useMemo } from "react";
|
|
7
|
+
import CCTVVideoTemplate from "../video/Template";
|
|
8
|
+
import {
|
|
9
|
+
getOverlayMessage,
|
|
10
|
+
getIsLive,
|
|
11
|
+
getIsError,
|
|
12
|
+
} from "../../utils/video-state";
|
|
12
13
|
|
|
13
14
|
export default function CCTVCamListItem({
|
|
14
15
|
className,
|
|
15
16
|
...cam
|
|
16
|
-
}: {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
companyId: cam.company_id,
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
const overlayMessage = (() => {
|
|
24
|
-
if (!cam.cam_online) return "CCTV 연결 오류";
|
|
25
|
-
if (isTokenLoading || isStreaming) return "스트림을 준비하고 있습니다.";
|
|
26
|
-
if (isTokenError) return "토큰을 발급하지 못했습니다.";
|
|
27
|
-
if (streamError) return streamError;
|
|
28
|
-
return null;
|
|
29
|
-
})();
|
|
17
|
+
}: {
|
|
18
|
+
className?: string;
|
|
19
|
+
} & CctvCompanyCameraData) {
|
|
20
|
+
const { videoRef, ...rtcCtx } = useCctvRtcStream({ cam });
|
|
30
21
|
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
22
|
+
const isLive = useMemo(() => getIsLive({ cam, ...rtcCtx }), [cam, rtcCtx]);
|
|
23
|
+
const isError = useMemo(() => getIsError({ cam, ...rtcCtx }), [cam, rtcCtx]);
|
|
24
|
+
const overlayMessage = useMemo(
|
|
25
|
+
() => getOverlayMessage({ cam, hasCamProp: true, ...rtcCtx }),
|
|
26
|
+
[cam, rtcCtx],
|
|
34
27
|
);
|
|
35
28
|
|
|
36
|
-
const disabledLiveState = useMemo(() => {
|
|
37
|
-
if (!cam.cam_online) return true;
|
|
38
|
-
if (isTokenLoading || isTokenError) return true;
|
|
39
|
-
if (streamError) return true;
|
|
40
|
-
return false;
|
|
41
|
-
}, [cam.cam_online, isTokenLoading, isTokenError, streamError]);
|
|
42
|
-
|
|
43
29
|
return (
|
|
44
30
|
<li
|
|
45
31
|
className={clsx(
|
|
@@ -49,23 +35,18 @@ export default function CCTVCamListItem({
|
|
|
49
35
|
isError && "is-error",
|
|
50
36
|
)}
|
|
51
37
|
>
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
) : null}
|
|
65
|
-
</CCTVVideoOverlayBody>
|
|
66
|
-
<CCTVVideoOverlayFooter activeTitle activeOpenButton cam={cam} />
|
|
67
|
-
</CCTVVideoOverlayContainer>
|
|
68
|
-
</CCTVVideoContainer>
|
|
38
|
+
<CCTVVideoTemplate
|
|
39
|
+
ref={videoRef}
|
|
40
|
+
className="cctv-cam-list-video-container"
|
|
41
|
+
headerOptions={{
|
|
42
|
+
activeLiveState: true,
|
|
43
|
+
activeTitle: false,
|
|
44
|
+
isLive,
|
|
45
|
+
title: cam.cam_name,
|
|
46
|
+
}}
|
|
47
|
+
footerOptions={{ activeTitle: true, activeOpenButton: true, cam }}
|
|
48
|
+
{...{ isError, overlayMessage, isLive }}
|
|
49
|
+
/>
|
|
69
50
|
</li>
|
|
70
51
|
);
|
|
71
52
|
}
|
|
@@ -1,48 +1,31 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import clsx from "clsx";
|
|
4
|
-
import
|
|
5
|
-
import CCTVVideoContents from "../../video/Video";
|
|
6
|
-
import CCTVVideoOverlayContainer from "../../video/overlay/Container";
|
|
7
|
-
import CCTVVideoOverlayFooter from "../../video/overlay/footer/Container";
|
|
8
|
-
import CCTVVideoOverlayBody from "../../video/overlay/body/Container";
|
|
9
|
-
import CCTVVideoError from "../../video/overlay/body/Error";
|
|
10
|
-
import type { CctvCompanyCameraList } from "../../../types";
|
|
4
|
+
import type { CctvCompanyCameraData } from "../../../types";
|
|
11
5
|
import { useCallback, useMemo } from "react";
|
|
12
6
|
import { useCctvRtcStream } from "../../../hooks";
|
|
13
|
-
import
|
|
7
|
+
import CCTVVideoTemplate from "../../video/Template";
|
|
8
|
+
import {
|
|
9
|
+
getOverlayMessage,
|
|
10
|
+
getIsLive,
|
|
11
|
+
getIsError,
|
|
12
|
+
} from "../../../utils/video-state";
|
|
14
13
|
|
|
15
14
|
export default function CCTVPaginationListItem({
|
|
16
15
|
className,
|
|
17
16
|
...cam
|
|
18
17
|
}: {
|
|
19
18
|
className?: string;
|
|
20
|
-
} &
|
|
21
|
-
const { videoRef,
|
|
22
|
-
useCctvRtcStream({
|
|
23
|
-
cam,
|
|
24
|
-
companyId: cam.company_id,
|
|
25
|
-
});
|
|
19
|
+
} & CctvCompanyCameraData) {
|
|
20
|
+
const { videoRef, ...rtcCtx } = useCctvRtcStream({ cam });
|
|
26
21
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return null;
|
|
33
|
-
})();
|
|
34
|
-
|
|
35
|
-
const isError = useMemo(
|
|
36
|
-
() => !cam.cam_online || isTokenError || streamError,
|
|
37
|
-
[cam.cam_online, isTokenError, streamError],
|
|
22
|
+
const isLive = useMemo(() => getIsLive({ cam, ...rtcCtx }), [cam, rtcCtx]);
|
|
23
|
+
const isError = useMemo(() => getIsError({ cam, ...rtcCtx }), [cam, rtcCtx]);
|
|
24
|
+
const overlayMessage = useMemo(
|
|
25
|
+
() => getOverlayMessage({ cam, hasCamProp: true, ...rtcCtx }),
|
|
26
|
+
[cam, rtcCtx],
|
|
38
27
|
);
|
|
39
28
|
|
|
40
|
-
const disabledLiveState = useMemo(() => {
|
|
41
|
-
if (isTokenLoading || isTokenError) return true;
|
|
42
|
-
if (streamError) return true;
|
|
43
|
-
return false;
|
|
44
|
-
}, [isTokenLoading, isTokenError, streamError]);
|
|
45
|
-
|
|
46
29
|
const handleSelect = useCallback(() => {
|
|
47
30
|
if (typeof cam.onSelect === "function") cam.onSelect();
|
|
48
31
|
}, [cam]);
|
|
@@ -63,18 +46,12 @@ export default function CCTVPaginationListItem({
|
|
|
63
46
|
aria-pressed={cam.selected}
|
|
64
47
|
aria-label={`${cam.cam_name} 선택`}
|
|
65
48
|
>
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
<CCTVVideoError>{overlayMessage}</CCTVVideoError>
|
|
73
|
-
) : null}
|
|
74
|
-
</CCTVVideoOverlayBody>
|
|
75
|
-
<CCTVVideoOverlayFooter cam={cam} />
|
|
76
|
-
</CCTVVideoOverlayContainer>
|
|
77
|
-
</CCTVVideoContainer>
|
|
49
|
+
<CCTVVideoTemplate
|
|
50
|
+
ref={videoRef}
|
|
51
|
+
className="cctv-pagination-list-video-container"
|
|
52
|
+
footerOptions={{ cam }}
|
|
53
|
+
{...{ isError, overlayMessage, isLive }}
|
|
54
|
+
/>
|
|
78
55
|
</button>
|
|
79
56
|
</li>
|
|
80
57
|
);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
import CCTVVideoContainer from "./Container";
|
|
3
|
+
import CCTVVideoContents from "./Video";
|
|
4
|
+
import CCTVVideoOverlayContainer from "./overlay/Container";
|
|
5
|
+
import CCTVVideoOverlayHeader from "./overlay/header/Container";
|
|
6
|
+
import CCTVVideoOverlayBody from "./overlay/body/Container";
|
|
7
|
+
import CCTVVideoError from "./overlay/body/Error";
|
|
8
|
+
import CCTVVideoOverlayFooter from "./overlay/footer/Container";
|
|
9
|
+
import type { CctvVideoTemplateProps } from "../../types/props";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* CCTV; Video Template
|
|
13
|
+
* @component
|
|
14
|
+
* @property {string} [className]
|
|
15
|
+
* @property {CctvVideoOverlayHeaderProps} [headerOptions]
|
|
16
|
+
* @property {CctvVideoOverlayFooterProps} [footerOptions]
|
|
17
|
+
* @property {boolean} [isError]
|
|
18
|
+
* @property {string | null} [overlayMessage]
|
|
19
|
+
* @property {boolean} [isLive]
|
|
20
|
+
* @property {React.Ref<HTMLVideoElement>} ref
|
|
21
|
+
*/
|
|
22
|
+
const CCTVVideoTemplate = forwardRef(
|
|
23
|
+
(
|
|
24
|
+
{
|
|
25
|
+
className,
|
|
26
|
+
headerOptions,
|
|
27
|
+
footerOptions,
|
|
28
|
+
isError,
|
|
29
|
+
isLive,
|
|
30
|
+
overlayMessage,
|
|
31
|
+
}: CctvVideoTemplateProps,
|
|
32
|
+
ref: React.Ref<HTMLVideoElement | null>,
|
|
33
|
+
) => {
|
|
34
|
+
return (
|
|
35
|
+
<CCTVVideoContainer className={className}>
|
|
36
|
+
<CCTVVideoContents ref={ref} muted />
|
|
37
|
+
<CCTVVideoOverlayContainer>
|
|
38
|
+
<CCTVVideoOverlayHeader {...headerOptions} />
|
|
39
|
+
<CCTVVideoOverlayBody className={isError ? "is-error" : undefined}>
|
|
40
|
+
{!isLive ? <CCTVVideoError>{overlayMessage}</CCTVVideoError> : null}
|
|
41
|
+
</CCTVVideoOverlayBody>
|
|
42
|
+
<CCTVVideoOverlayFooter {...footerOptions} />
|
|
43
|
+
</CCTVVideoOverlayContainer>
|
|
44
|
+
</CCTVVideoContainer>
|
|
45
|
+
);
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
export default CCTVVideoTemplate;
|
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
import clsx from "clsx";
|
|
2
2
|
import CCTVVideoOpenButton from "./OpenButton";
|
|
3
3
|
import CCTVVideoOverlayTitle from "../Title";
|
|
4
|
-
import type {
|
|
4
|
+
import type { CctvVideoOverlayFooterProps } from "../../../../types";
|
|
5
5
|
|
|
6
6
|
export default function CCTVVideoOverlayFooter({
|
|
7
7
|
className,
|
|
8
8
|
activeTitle = true,
|
|
9
9
|
activeOpenButton,
|
|
10
10
|
cam,
|
|
11
|
-
}: {
|
|
12
|
-
className?: string;
|
|
13
|
-
activeTitle?: boolean;
|
|
14
|
-
activeOpenButton?: boolean;
|
|
15
|
-
cam?: Omit<CctvCompanyCameraList, "renderKey">;
|
|
16
|
-
}) {
|
|
11
|
+
}: CctvVideoOverlayFooterProps) {
|
|
17
12
|
// console.log(cam);
|
|
18
13
|
return (
|
|
19
14
|
<footer className={clsx("cctv-video-overlay-footer", className)}>
|
|
@@ -2,11 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { useCallback } from "react";
|
|
4
4
|
import ViewerOpenIcon from "../../../../img/viewer-open.svg";
|
|
5
|
-
import type {
|
|
5
|
+
import type { CctvCompanyCameraData } from "../../../../types";
|
|
6
6
|
|
|
7
|
-
export default function CCTVVideoOpenButton({
|
|
8
|
-
...cam
|
|
9
|
-
}: Omit<CctvCompanyCameraList, "renderKey">) {
|
|
7
|
+
export default function CCTVVideoOpenButton({ ...cam }: CctvCompanyCameraData) {
|
|
10
8
|
const onOpen = useCallback(() => {
|
|
11
9
|
if (typeof cam.onSelect === "function") cam.onSelect();
|
|
12
10
|
}, [cam]);
|
|
@@ -2,6 +2,7 @@ import clsx from "clsx";
|
|
|
2
2
|
import CCTVVideoLiveState from "./LiveState";
|
|
3
3
|
import CCTVVideoCloseButton from "./CloseButton";
|
|
4
4
|
import CCTVVideoOverlayTitle from "../Title";
|
|
5
|
+
import type { CctvVideoOverlayHeaderProps } from "../../../../types/props";
|
|
5
6
|
|
|
6
7
|
export default function CCTVVideoOverlayHeader({
|
|
7
8
|
className,
|
|
@@ -9,29 +10,20 @@ export default function CCTVVideoOverlayHeader({
|
|
|
9
10
|
activeCloseButton,
|
|
10
11
|
activeTitle,
|
|
11
12
|
title,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}: {
|
|
15
|
-
className?: string;
|
|
16
|
-
activeLiveState?: boolean;
|
|
17
|
-
activeCloseButton?: boolean;
|
|
18
|
-
activeTitle?: boolean;
|
|
19
|
-
title?: React.ReactNode;
|
|
20
|
-
activeTime?: boolean;
|
|
21
|
-
liveStateDisabled?: boolean;
|
|
22
|
-
}) {
|
|
13
|
+
isLive,
|
|
14
|
+
}: CctvVideoOverlayHeaderProps) {
|
|
23
15
|
const showUpper = activeLiveState || activeCloseButton;
|
|
24
|
-
const showLower = activeTitle
|
|
25
|
-
|
|
26
|
-
if (!showUpper && !showLower) return null;
|
|
16
|
+
const showLower = activeTitle;
|
|
27
17
|
|
|
28
18
|
return (
|
|
29
|
-
<header
|
|
19
|
+
<header
|
|
20
|
+
className={clsx("cctv-video-overlay-header", className, {
|
|
21
|
+
empty: !showUpper && !showLower,
|
|
22
|
+
})}
|
|
23
|
+
>
|
|
30
24
|
{showUpper && (
|
|
31
25
|
<div className="cctv-video-overlay-header-upper">
|
|
32
|
-
{activeLiveState &&
|
|
33
|
-
<CCTVVideoLiveState disabled={liveStateDisabled} />
|
|
34
|
-
)}
|
|
26
|
+
{activeLiveState && <CCTVVideoLiveState isLive={isLive} />}
|
|
35
27
|
{activeCloseButton && <CCTVVideoCloseButton />}
|
|
36
28
|
</div>
|
|
37
29
|
)}
|
|
@@ -4,15 +4,15 @@ import clsx from "clsx";
|
|
|
4
4
|
import { Badge } from "@uniai-fe/uds-primitives";
|
|
5
5
|
|
|
6
6
|
export default function CCTVVideoLiveState({
|
|
7
|
-
|
|
7
|
+
isLive = false,
|
|
8
8
|
}: {
|
|
9
|
-
|
|
9
|
+
isLive?: boolean;
|
|
10
10
|
}) {
|
|
11
11
|
return (
|
|
12
12
|
<Badge
|
|
13
13
|
size="xsmall"
|
|
14
14
|
intent="tertiary"
|
|
15
|
-
className={clsx("cctv-video-live-state", {
|
|
15
|
+
className={clsx("cctv-video-live-state", { on: isLive == true })}
|
|
16
16
|
>
|
|
17
17
|
<figure className="cctv-video-live-state-dot"></figure>
|
|
18
18
|
<span>Live</span>
|
|
@@ -2,7 +2,10 @@ import clsx from "clsx";
|
|
|
2
2
|
import CCTVViewerContainer from "../Container";
|
|
3
3
|
import CCTVViewerDesktopPagination from "./Pagination";
|
|
4
4
|
import CCTVViewerDesktopVideo from "./Video";
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
CctvCompanyCameraData,
|
|
7
|
+
CctvCompanyCameraList,
|
|
8
|
+
} from "../../../types";
|
|
6
9
|
|
|
7
10
|
export default function CCTVViewerDesktopContainer({
|
|
8
11
|
className,
|
|
@@ -12,7 +15,7 @@ export default function CCTVViewerDesktopContainer({
|
|
|
12
15
|
}: {
|
|
13
16
|
className?: string;
|
|
14
17
|
children?: React.ReactNode;
|
|
15
|
-
selectedCam?:
|
|
18
|
+
selectedCam?: CctvCompanyCameraData;
|
|
16
19
|
camList?: CctvCompanyCameraList[];
|
|
17
20
|
}) {
|
|
18
21
|
return (
|
|
@@ -3,81 +3,49 @@
|
|
|
3
3
|
import { useMemo } from "react";
|
|
4
4
|
|
|
5
5
|
import { useCctvContext, useCctvRtcStream } from "../../../hooks";
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
import CCTVViewerDesktopPlaceholder from "./Placeholder";
|
|
14
|
-
import type { CctvCompanyCameraList } from "../../../types";
|
|
6
|
+
import type { CctvCompanyCameraData } from "../../../types";
|
|
7
|
+
import CCTVVideoTemplate from "../../video/Template";
|
|
8
|
+
import {
|
|
9
|
+
getOverlayMessage,
|
|
10
|
+
getIsLive,
|
|
11
|
+
getIsError,
|
|
12
|
+
} from "../../../utils/video-state";
|
|
15
13
|
|
|
16
14
|
export default function CCTVViewerDesktopVideo({
|
|
17
|
-
cam,
|
|
15
|
+
cam: propsCam,
|
|
18
16
|
}: {
|
|
19
|
-
cam?:
|
|
17
|
+
cam?: CctvCompanyCameraData;
|
|
20
18
|
}) {
|
|
21
|
-
const { selectedCam,
|
|
22
|
-
useCctvContext();
|
|
19
|
+
const { selectedCam, isFetching } = useCctvContext();
|
|
23
20
|
|
|
24
21
|
// props의 cam이 들어오면 우선 적용
|
|
25
22
|
// cam이 부여되어있으면 selectedCam 무시
|
|
26
23
|
// cam이 없다면 selectedCam 적용
|
|
27
|
-
const
|
|
28
|
-
// console.log("CCTVViewerDesktopVideo camData:", camData);
|
|
24
|
+
const cam = useMemo(() => propsCam ?? selectedCam, [propsCam, selectedCam]);
|
|
29
25
|
|
|
30
|
-
const
|
|
26
|
+
const { videoRef, ...rtcCtx } = useCctvRtcStream({ cam });
|
|
31
27
|
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
const overlayMessage = useMemo(() => {
|
|
42
|
-
if (!camData) return null;
|
|
43
|
-
if (!camData.cam_online) return "CCTV 연결 오류";
|
|
44
|
-
if (isTokenLoading || isStreaming) return "스트림을 준비하고 있습니다.";
|
|
45
|
-
if (isTokenError) return "토큰을 발급하지 못했습니다.";
|
|
46
|
-
if (streamError) return streamError;
|
|
47
|
-
return null;
|
|
48
|
-
}, [camData, isTokenError, isTokenLoading, isStreaming, streamError]);
|
|
49
|
-
|
|
50
|
-
if (!cam && isFetching && !camData) {
|
|
51
|
-
return (
|
|
52
|
-
<CCTVViewerDesktopPlaceholder message="CCTV 데이터를 불러오는 중입니다." />
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (!camData && !cam) {
|
|
57
|
-
return (
|
|
58
|
-
<CCTVViewerDesktopPlaceholder message="좌측 목록에서 카메라를 선택하세요." />
|
|
59
|
-
);
|
|
60
|
-
}
|
|
28
|
+
const isLive = useMemo(() => getIsLive({ cam, ...rtcCtx }), [cam, rtcCtx]);
|
|
29
|
+
const isError = useMemo(() => getIsError({ cam, ...rtcCtx }), [cam, rtcCtx]);
|
|
30
|
+
const overlayMessage = useMemo(
|
|
31
|
+
() =>
|
|
32
|
+
getOverlayMessage({ cam, hasCamProp: !!propsCam, isFetching, ...rtcCtx }),
|
|
33
|
+
[propsCam, cam, isFetching, rtcCtx],
|
|
34
|
+
);
|
|
61
35
|
|
|
62
36
|
return (
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
{isCamOffline ? <CCTVVideoError /> : overlayMessage}
|
|
77
|
-
</CCTVVideoOverlayBody>
|
|
78
|
-
) : null}
|
|
79
|
-
<CCTVVideoOverlayFooter activeTitle={false} />
|
|
80
|
-
</CCTVVideoOverlayContainer>
|
|
81
|
-
</CCTVVideoContainer>
|
|
37
|
+
<CCTVVideoTemplate
|
|
38
|
+
ref={videoRef}
|
|
39
|
+
className="cctv-viewer-desktop-video-container"
|
|
40
|
+
headerOptions={{
|
|
41
|
+
activeLiveState: true,
|
|
42
|
+
activeTitle: true,
|
|
43
|
+
activeCloseButton: true,
|
|
44
|
+
isLive,
|
|
45
|
+
title: cam?.cam_name,
|
|
46
|
+
}}
|
|
47
|
+
footerOptions={{ activeTitle: false }}
|
|
48
|
+
{...{ isError, overlayMessage, isLive }}
|
|
49
|
+
/>
|
|
82
50
|
);
|
|
83
51
|
}
|
|
@@ -4,28 +4,48 @@ import { useEffect, useMemo } from "react";
|
|
|
4
4
|
import { useQueryCctvCompanyList } from "../apis";
|
|
5
5
|
import { useCctvApiUrl } from "../components/Provider";
|
|
6
6
|
import useCctvContext from "./useContext";
|
|
7
|
-
|
|
7
|
+
import type { UseCctvCompanyDataReturn } from "../types";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* CCTV 회사 리스트 API를 호출해 데이터를 가져오는 훅.
|
|
11
|
+
* @hook
|
|
12
|
+
* @param {UseCctvCompanyDataParams} [params] 훅 파라미터
|
|
13
|
+
* @property {string} [username] 사용자 계정 아이디 (Provider에서 주입된 기본값 대신 사용)
|
|
14
|
+
* @property {string} [url] 회사 리스트 API URL (Provider에서 주입된 기본값 대신 사용)
|
|
15
|
+
* @return {UseCctvCompanyDataReturn} 회사 리스트 쿼리 반환값
|
|
16
|
+
* @desc
|
|
17
|
+
* return {
|
|
18
|
+
* data, // 회사 리스트 API 응답 데이터
|
|
19
|
+
* isFetching, // 데이터 로딩 중 상태
|
|
20
|
+
* isError, // 데이터 로딩 에러 상태
|
|
21
|
+
* ...rest, // react-query useQuery 반환값
|
|
22
|
+
* }
|
|
23
|
+
*/
|
|
8
24
|
export default function useCctvCompanyData(params?: {
|
|
9
25
|
username?: string;
|
|
10
26
|
url?: string;
|
|
11
|
-
}) {
|
|
27
|
+
}): UseCctvCompanyDataReturn {
|
|
28
|
+
// 외부에서 username/url을 덮어쓸 수 있도록 받고, 없으면 Provider 값을 사용한다.
|
|
12
29
|
const { username, url } = params || {};
|
|
13
30
|
|
|
14
31
|
const { listUrl } = useCctvApiUrl();
|
|
15
32
|
const resolvedUrl = useMemo(() => url ?? listUrl, [url, listUrl]);
|
|
16
33
|
const { username: contextUsername, setValue } = useCctvContext();
|
|
17
34
|
|
|
35
|
+
// 사용자/URL에 맞춰 회사 리스트 API를 호출한다.
|
|
18
36
|
const { data, isFetching, isError, ...rest } = useQueryCctvCompanyList({
|
|
19
37
|
username: username ?? contextUsername,
|
|
20
38
|
url: resolvedUrl,
|
|
21
39
|
});
|
|
22
40
|
|
|
41
|
+
// 응답 데이터를 react-hook-form rawData 필드로 반영한다.
|
|
23
42
|
useEffect(() => {
|
|
24
43
|
if (data?.data) {
|
|
25
44
|
setValue("rawData", data?.data);
|
|
26
45
|
}
|
|
27
46
|
}, [data, setValue]);
|
|
28
47
|
|
|
48
|
+
// 로딩/에러 상태를 form state로도 전달해 다른 훅에서 활용할 수 있게 한다.
|
|
29
49
|
useEffect(() => {
|
|
30
50
|
setValue("isFetching", Boolean(isFetching));
|
|
31
51
|
}, [isFetching, setValue]);
|
|
@@ -34,5 +54,10 @@ export default function useCctvCompanyData(params?: {
|
|
|
34
54
|
setValue("isError", Boolean(isError));
|
|
35
55
|
}, [isError, setValue]);
|
|
36
56
|
|
|
37
|
-
return {
|
|
57
|
+
return {
|
|
58
|
+
data,
|
|
59
|
+
isFetching,
|
|
60
|
+
isError: Boolean(isError),
|
|
61
|
+
...rest,
|
|
62
|
+
};
|
|
38
63
|
}
|