@uniai-fe/uds-templates 0.1.20 → 0.1.22
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 +2 -2
- package/package.json +8 -4
- package/src/auth/login/jotai/user.ts +13 -0
- package/src/auth/login/types/api.ts +229 -0
- package/src/auth/login/types/form.ts +1 -0
- package/src/auth/login/types/index.ts +4 -0
- package/src/auth/signup/markup/VerificationForm.tsx +3 -2
- package/src/cctv/apis/client.ts +61 -0
- package/src/cctv/apis/index.ts +2 -0
- package/src/cctv/apis/server.ts +188 -0
- package/src/cctv/components/Provider.tsx +47 -0
- package/src/cctv/components/__viewer.tsx +99 -0
- package/src/cctv/components/cam-list/Container.tsx +36 -0
- package/src/cctv/components/cam-list/Item.tsx +71 -0
- package/src/cctv/components/cam-list/index.tsx +7 -0
- package/src/cctv/components/index.tsx +13 -0
- package/src/cctv/components/pagination/Container.tsx +26 -0
- package/src/cctv/components/pagination/Control.tsx +29 -0
- package/src/cctv/components/pagination/Provider.tsx +204 -0
- package/src/cctv/components/pagination/buttons/Base.tsx +56 -0
- package/src/cctv/components/pagination/buttons/Next.tsx +34 -0
- package/src/cctv/components/pagination/buttons/Prev.tsx +34 -0
- package/src/cctv/components/pagination/index.tsx +25 -0
- package/src/cctv/components/pagination/list/Carousel.tsx +26 -0
- package/src/cctv/components/pagination/list/Container.tsx +30 -0
- package/src/cctv/components/pagination/list/Item.tsx +81 -0
- package/src/cctv/components/video/Container.tsx +13 -0
- package/src/cctv/components/video/Video.tsx +34 -0
- package/src/cctv/components/video/index.tsx +9 -0
- package/src/cctv/components/video/overlay/Container.tsx +15 -0
- package/src/cctv/components/video/overlay/Title.tsx +28 -0
- package/src/cctv/components/video/overlay/body/Container.tsx +13 -0
- package/src/cctv/components/video/overlay/body/Error.tsx +16 -0
- package/src/cctv/components/video/overlay/footer/Container.tsx +30 -0
- package/src/cctv/components/video/overlay/footer/OpenButton.tsx +19 -0
- package/src/cctv/components/video/overlay/header/CloseButton.tsx +14 -0
- package/src/cctv/components/video/overlay/header/Container.tsx +50 -0
- package/src/cctv/components/video/overlay/header/LiveState.tsx +21 -0
- package/src/cctv/components/video/overlay/index.tsx +24 -0
- package/src/cctv/components/viewer/Container.tsx +13 -0
- package/src/cctv/components/viewer/desktop/Container.tsx +38 -0
- package/src/cctv/components/viewer/desktop/Pagination.tsx +20 -0
- package/src/cctv/components/viewer/desktop/Placeholder.tsx +18 -0
- package/src/cctv/components/viewer/desktop/Video.tsx +83 -0
- package/src/cctv/components/viewer/index.tsx +12 -0
- package/src/cctv/components/viewer/mobile/Container.tsx +13 -0
- package/src/cctv/data/context.ts +22 -0
- package/src/cctv/data/index.ts +1 -0
- package/src/cctv/hooks/index.tsx +5 -0
- package/src/cctv/hooks/useCompanyData.tsx +39 -0
- package/src/cctv/hooks/useContext.ts +150 -0
- package/src/cctv/hooks/useRtcStream.ts +94 -0
- package/src/cctv/img/chevron-left.svg +3 -0
- package/src/cctv/img/chevron-right.svg +3 -0
- package/src/cctv/img/error.svg +4 -0
- package/src/cctv/img/viewer-close.svg +3 -0
- package/src/cctv/img/viewer-open.svg +6 -0
- package/src/cctv/index.scss +1 -0
- package/src/cctv/index.tsx +9 -0
- package/src/cctv/jotai/context.ts +9 -0
- package/src/cctv/jotai/index.ts +1 -0
- package/src/cctv/styles/cam-list.scss +32 -0
- package/src/cctv/styles/index.scss +5 -0
- package/src/cctv/styles/pagination.scss +77 -0
- package/src/cctv/styles/variables.scss +38 -0
- package/src/cctv/styles/video.scss +142 -0
- package/src/cctv/styles/viewer.scss +7 -0
- package/src/cctv/types/api.ts +166 -0
- package/src/cctv/types/carousel.ts +24 -0
- package/src/cctv/types/context.ts +68 -0
- package/src/cctv/types/index.ts +4 -0
- package/src/cctv/types/list.ts +94 -0
- package/src/cctv/utils/data.ts +40 -0
- package/src/cctv/utils/select.ts +62 -0
- package/src/index.tsx +3 -0
- package/src/modal/styles/base.scss +2 -2
- package/src/types/api.ts +43 -0
- package/src/types/index.ts +1 -0
- package/src/auth/login/types.ts +0 -2
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo } from "react";
|
|
4
|
+
import { useFormContext } from "react-hook-form";
|
|
5
|
+
import { useQueryCctvCompanyList } from "../apis";
|
|
6
|
+
import { useCctvEndpoints } from "../components/Provider";
|
|
7
|
+
import type { CctvContext } from "../types";
|
|
8
|
+
|
|
9
|
+
export default function useCctvCompanyData({
|
|
10
|
+
username,
|
|
11
|
+
url,
|
|
12
|
+
}: {
|
|
13
|
+
username: string;
|
|
14
|
+
url?: string;
|
|
15
|
+
}) {
|
|
16
|
+
const { listUrl } = useCctvEndpoints();
|
|
17
|
+
const resolvedUrl = useMemo(() => url ?? listUrl, [url, listUrl]);
|
|
18
|
+
const { setValue } = useFormContext<CctvContext>();
|
|
19
|
+
const { data, isFetching, isError, ...rest } = useQueryCctvCompanyList({
|
|
20
|
+
username,
|
|
21
|
+
url: resolvedUrl,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (data?.data) {
|
|
26
|
+
setValue("rawData", data.data);
|
|
27
|
+
}
|
|
28
|
+
}, [data, setValue]);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
setValue("isFetching", Boolean(isFetching));
|
|
32
|
+
}, [isFetching, setValue]);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
setValue("isError", Boolean(isError));
|
|
36
|
+
}, [isError, setValue]);
|
|
37
|
+
|
|
38
|
+
return { data, isFetching, isError, ...rest };
|
|
39
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useMemo } from "react";
|
|
4
|
+
import { useSetAtom } from "jotai";
|
|
5
|
+
import { useFormContext, useWatch } from "react-hook-form";
|
|
6
|
+
|
|
7
|
+
import { cctvSelectedContext } from "../jotai/context";
|
|
8
|
+
import type {
|
|
9
|
+
CctvCompanyCameraList,
|
|
10
|
+
CctvCompanyGroup,
|
|
11
|
+
CctvCompanyGroupItems,
|
|
12
|
+
CctvContext,
|
|
13
|
+
} from "../types";
|
|
14
|
+
import { initCompanyList } from "../utils/data";
|
|
15
|
+
import { getCam, getCamList, getCompany } from "../utils/select";
|
|
16
|
+
|
|
17
|
+
export default function useCctvContext(params?: { pageCode?: string }) {
|
|
18
|
+
const { pageCode } = params ?? {};
|
|
19
|
+
|
|
20
|
+
// const username = useCctvUsername();
|
|
21
|
+
const setCtx = useSetAtom(cctvSelectedContext);
|
|
22
|
+
const { control, setValue, ...context } = useFormContext<CctvContext>();
|
|
23
|
+
|
|
24
|
+
const rawData = useWatch({ control, name: "rawData" });
|
|
25
|
+
// console.log("useCctvContext rawData:", rawData);
|
|
26
|
+
const isFetching = useWatch({ control, name: "isFetching" });
|
|
27
|
+
const isError = useWatch({ control, name: "isError" });
|
|
28
|
+
|
|
29
|
+
const companyList = useMemo(
|
|
30
|
+
() => initCompanyList({ data: rawData, pageCode }),
|
|
31
|
+
[rawData, pageCode],
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// console.log("useCctvContext companyList:", companyList);
|
|
35
|
+
|
|
36
|
+
const searchedKeyword = useWatch({ control, name: "filter" });
|
|
37
|
+
const selectedCompanyId = useWatch({ control, name: "company_id" });
|
|
38
|
+
const selectedCamId = useWatch({ control, name: "cam_id" });
|
|
39
|
+
|
|
40
|
+
const onSelectCompany = useCallback(
|
|
41
|
+
(company_id: string) => {
|
|
42
|
+
if (selectedCompanyId === company_id) return;
|
|
43
|
+
|
|
44
|
+
setValue("company_id", company_id);
|
|
45
|
+
setValue("cam_id", "");
|
|
46
|
+
|
|
47
|
+
setCtx({ company_id, cam_id: "" });
|
|
48
|
+
},
|
|
49
|
+
[selectedCompanyId, setValue, setCtx],
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const onToggleCamera = useCallback(
|
|
53
|
+
(cam_id: string) => {
|
|
54
|
+
setValue("cam_id", selectedCamId === cam_id ? "" : cam_id);
|
|
55
|
+
setCtx(prev => ({
|
|
56
|
+
...prev,
|
|
57
|
+
cam_id: selectedCamId === cam_id ? "" : cam_id,
|
|
58
|
+
}));
|
|
59
|
+
},
|
|
60
|
+
[selectedCamId, setValue, setCtx],
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const onOpenCamera = useCallback(
|
|
64
|
+
(cam_id: string) => {
|
|
65
|
+
if (selectedCamId === cam_id) return;
|
|
66
|
+
setValue("cam_id", cam_id);
|
|
67
|
+
setCtx(prev => ({ ...prev, cam_id: cam_id }));
|
|
68
|
+
},
|
|
69
|
+
[selectedCamId, setValue, setCtx],
|
|
70
|
+
);
|
|
71
|
+
const onCloseCamera = useCallback(
|
|
72
|
+
(cam_id: string) => {
|
|
73
|
+
if (selectedCamId !== cam_id) return;
|
|
74
|
+
setValue("cam_id", "");
|
|
75
|
+
setCtx(prev => ({ ...prev, cam_id: "" }));
|
|
76
|
+
},
|
|
77
|
+
[selectedCamId, setValue, setCtx],
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const groups = useMemo(
|
|
81
|
+
(): CctvCompanyGroup[] =>
|
|
82
|
+
companyList.map(group => ({
|
|
83
|
+
...group,
|
|
84
|
+
list: group.list.map((d: CctvCompanyGroupItems) => ({
|
|
85
|
+
...d,
|
|
86
|
+
selected: d.company_id === selectedCompanyId,
|
|
87
|
+
onSelect: () => onSelectCompany(d.company_id),
|
|
88
|
+
cam_list: d.cam_list.map((cam: CctvCompanyCameraList) => ({
|
|
89
|
+
...cam,
|
|
90
|
+
selected: cam.cam_id === selectedCamId,
|
|
91
|
+
onSelect: () => onOpenCamera(cam.cam_id),
|
|
92
|
+
})),
|
|
93
|
+
})),
|
|
94
|
+
})),
|
|
95
|
+
[
|
|
96
|
+
companyList,
|
|
97
|
+
onSelectCompany,
|
|
98
|
+
onOpenCamera,
|
|
99
|
+
selectedCamId,
|
|
100
|
+
selectedCompanyId,
|
|
101
|
+
],
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const selectedCompany = useMemo(
|
|
105
|
+
(): CctvCompanyGroupItems | undefined =>
|
|
106
|
+
getCompany(groups, selectedCompanyId),
|
|
107
|
+
[selectedCompanyId, groups],
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const cams = useMemo(
|
|
111
|
+
(): CctvCompanyCameraList[] => getCamList(groups, selectedCompanyId),
|
|
112
|
+
[selectedCompanyId, groups],
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const selectedCam = useMemo(
|
|
116
|
+
(): Omit<CctvCompanyCameraList, "renderKey"> | undefined =>
|
|
117
|
+
getCam(groups, selectedCompanyId, selectedCamId),
|
|
118
|
+
[selectedCamId, selectedCompanyId, groups],
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const companyValidList = useMemo(
|
|
122
|
+
() =>
|
|
123
|
+
groups
|
|
124
|
+
.flatMap(({ list }) => list)
|
|
125
|
+
.filter(
|
|
126
|
+
({ cam_list }) => !cam_list.every(({ cam_online }) => !cam_online),
|
|
127
|
+
)
|
|
128
|
+
.map(({ company_id }) => company_id),
|
|
129
|
+
[groups],
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
isFetching,
|
|
134
|
+
isError,
|
|
135
|
+
searchedKeyword,
|
|
136
|
+
selectedCompanyId,
|
|
137
|
+
selectedCamId,
|
|
138
|
+
selectedCompany,
|
|
139
|
+
selectedCam,
|
|
140
|
+
groups,
|
|
141
|
+
companyValidList,
|
|
142
|
+
cams,
|
|
143
|
+
control,
|
|
144
|
+
setValue,
|
|
145
|
+
...context,
|
|
146
|
+
onToggleCamera,
|
|
147
|
+
onOpenCamera,
|
|
148
|
+
onCloseCamera,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { startWhepStream, type WhepStreamHandle } from "@uniai-fe/util-rtc";
|
|
5
|
+
|
|
6
|
+
import { useQueryCctvRtcToken } from "../apis/client";
|
|
7
|
+
import { useCctvEndpoints } from "../components/Provider";
|
|
8
|
+
import type { CctvCompanyCameraList } from "../types";
|
|
9
|
+
import { useFormContext, useWatch } from "react-hook-form";
|
|
10
|
+
|
|
11
|
+
interface UseCctvRtcStreamParams {
|
|
12
|
+
cam?: Omit<CctvCompanyCameraList, "renderKey">;
|
|
13
|
+
companyId?: string;
|
|
14
|
+
tokenUrl?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function useCctvRtcStream({
|
|
18
|
+
cam,
|
|
19
|
+
companyId,
|
|
20
|
+
tokenUrl,
|
|
21
|
+
}: UseCctvRtcStreamParams) {
|
|
22
|
+
const { tokenUrl: contextTokenUrl } = useCctvEndpoints();
|
|
23
|
+
const videoRef = useRef<HTMLVideoElement | null>(null);
|
|
24
|
+
const [connectionState, setConnectionState] =
|
|
25
|
+
useState<RTCPeerConnectionState>("new");
|
|
26
|
+
const [streamError, setStreamError] = useState<string | null>(null);
|
|
27
|
+
const [isStreaming, setStreaming] = useState(false);
|
|
28
|
+
|
|
29
|
+
const { control } = useFormContext();
|
|
30
|
+
const username = useWatch({ control, name: "username" });
|
|
31
|
+
|
|
32
|
+
const tokenQuery = useQueryCctvRtcToken({
|
|
33
|
+
company_id: companyId ?? "",
|
|
34
|
+
cam_id: cam?.cam_id ?? "",
|
|
35
|
+
username,
|
|
36
|
+
url: tokenUrl ?? contextTokenUrl,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const endpoint = useMemo(() => {
|
|
40
|
+
if (!cam?.cam_rtc) return undefined;
|
|
41
|
+
const query = username ? `?username=${encodeURIComponent(username)}` : "";
|
|
42
|
+
return `${cam.cam_rtc.replace(/\/$/, "")}/whep${query}`;
|
|
43
|
+
}, [cam?.cam_rtc, username]);
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
const currentVideo = videoRef.current;
|
|
47
|
+
// 현재 렌더에서 할당된 video 요소를 캡처해 cleanup 시 안전하게 참조한다.
|
|
48
|
+
if (!tokenQuery.data?.token || !endpoint || !currentVideo) return;
|
|
49
|
+
|
|
50
|
+
let handle: WhepStreamHandle | null = null;
|
|
51
|
+
const controller = new AbortController();
|
|
52
|
+
setStreamError(null);
|
|
53
|
+
setStreaming(true);
|
|
54
|
+
setConnectionState("connecting");
|
|
55
|
+
|
|
56
|
+
startWhepStream({
|
|
57
|
+
endpoint,
|
|
58
|
+
token: tokenQuery.data.token,
|
|
59
|
+
video: currentVideo,
|
|
60
|
+
signal: controller.signal,
|
|
61
|
+
onConnectionStateChange: state => setConnectionState(state),
|
|
62
|
+
})
|
|
63
|
+
.then(streamHandle => {
|
|
64
|
+
handle = streamHandle;
|
|
65
|
+
})
|
|
66
|
+
.catch(error => {
|
|
67
|
+
setStreamError(
|
|
68
|
+
error instanceof Error
|
|
69
|
+
? error.message
|
|
70
|
+
: "스트림 연결에 실패했습니다.",
|
|
71
|
+
);
|
|
72
|
+
})
|
|
73
|
+
.finally(() => {
|
|
74
|
+
setStreaming(false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return () => {
|
|
78
|
+
controller.abort();
|
|
79
|
+
handle?.close();
|
|
80
|
+
handle = null;
|
|
81
|
+
currentVideo.srcObject = null;
|
|
82
|
+
};
|
|
83
|
+
}, [endpoint, tokenQuery.data?.token, cam?.cam_id]);
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
videoRef,
|
|
87
|
+
connectionState,
|
|
88
|
+
streamError,
|
|
89
|
+
isStreaming,
|
|
90
|
+
isTokenLoading: tokenQuery.isFetching,
|
|
91
|
+
isTokenError: tokenQuery.isError,
|
|
92
|
+
refetchToken: tokenQuery.refetch,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M1.43463 4.43463C1.74705 4.12221 2.25307 4.12221 2.56549 4.43463L16.5655 18.4346C16.8779 18.7471 16.8779 19.2531 16.5655 19.5655C16.2531 19.8779 15.7471 19.8778 15.4346 19.5655L14.6016 18.7325C14.4079 18.7757 14.2069 18.7999 14.0001 18.7999H5.00006C3.45368 18.7999 2.20029 17.5464 2.20026 16.0001V8.00006C2.20026 7.51792 2.32348 7.06501 2.53815 6.66901L1.43463 5.56549C1.12221 5.25307 1.12221 4.74705 1.43463 4.43463Z" fill="#CACBCE"/>
|
|
3
|
+
<path d="M14.0001 5.20026C15.5464 5.20029 16.7999 6.45368 16.7999 8.00006V8.81744L19.3311 7.80573C20.5133 7.33288 21.7995 8.20346 21.7999 9.47662V14.5235C21.7995 15.7966 20.5133 16.6672 19.3311 16.1944L16.7999 15.1817V16.0001C16.7999 16.4067 16.7106 16.792 16.5547 17.1407L4.63971 5.22565C4.75777 5.21047 4.87787 5.20026 5.00006 5.20026H14.0001Z" fill="#CACBCE"/>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M24.5009 9.65186C24.9695 9.18323 25.7285 9.18323 26.1972 9.65186C26.6658 10.1205 26.6658 10.8795 26.1972 11.3481L19.62 17.9238L26.1972 24.501C26.6658 24.9696 26.6658 25.7286 26.1972 26.1973C25.7285 26.6659 24.9695 26.6659 24.5009 26.1973L17.9237 19.6201L11.3481 26.1973C10.8794 26.6659 10.1204 26.6659 9.65176 26.1973C9.18314 25.7286 9.18314 24.9696 9.65176 24.501L16.2274 17.9238L9.65176 11.3481C9.18314 10.8795 9.18314 10.1205 9.65176 9.65186C10.1204 9.18323 10.8794 9.18323 11.3481 9.65186L17.9237 16.2275L24.5009 9.65186Z" fill="white"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M4 9.5V6.5C4 5.39543 4.89543 4.5 6 4.5H9" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
|
|
3
|
+
<path d="M15 4.5L18 4.5C19.1046 4.5 20 5.39543 20 6.5L20 9.5" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
|
|
4
|
+
<path d="M9 19.5L6 19.5C4.89543 19.5 4 18.6046 4 17.5L4 14.5" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
|
|
5
|
+
<path d="M20 14.5L20 17.5C20 18.6046 19.1046 19.5 18 19.5L15 19.5" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
|
|
6
|
+
</svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@use "./styles/index.scss";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./context";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
.cctv-cam-list-container {
|
|
2
|
+
width: 100%;
|
|
3
|
+
height: 100%;
|
|
4
|
+
|
|
5
|
+
overflow-x: hidden;
|
|
6
|
+
overflow-y: scroll;
|
|
7
|
+
|
|
8
|
+
scroll-snap-type: y proximity;
|
|
9
|
+
scrollbar-width: none;
|
|
10
|
+
-ms-overflow-style: none;
|
|
11
|
+
|
|
12
|
+
&::-webkit-scrollbar {
|
|
13
|
+
display: none;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.cctv-cam-list-track {
|
|
18
|
+
display: grid;
|
|
19
|
+
width: 100%;
|
|
20
|
+
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
|
21
|
+
gap: var(--cctv-list-gap);
|
|
22
|
+
padding: 0;
|
|
23
|
+
margin: 0;
|
|
24
|
+
scroll-behavior: smooth;
|
|
25
|
+
& > * {
|
|
26
|
+
scroll-snap-align: start;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.cctv-cam-list-item .cctv-video-container {
|
|
31
|
+
max-height: 240px;
|
|
32
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
.cctv-pagination-container {
|
|
2
|
+
width: 100%;
|
|
3
|
+
display: flex;
|
|
4
|
+
align-items: center;
|
|
5
|
+
gap: var(--cctv-carousel-gap);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.cctv-pagination-list-container {
|
|
9
|
+
width: calc(
|
|
10
|
+
100% - (var(--cctv-pagination-move-button-size) * 2) -
|
|
11
|
+
(var(--cctv-carousel-gap) * 2)
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.cctv-pagination-viewport {
|
|
16
|
+
overflow-x: auto;
|
|
17
|
+
overflow-y: hidden;
|
|
18
|
+
width: 100%;
|
|
19
|
+
scroll-snap-type: x proximity;
|
|
20
|
+
scrollbar-width: none;
|
|
21
|
+
-ms-overflow-style: none;
|
|
22
|
+
|
|
23
|
+
&::-webkit-scrollbar {
|
|
24
|
+
display: none;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.cctv-pagination-track {
|
|
29
|
+
// Carousel 트랙은 flex로 구성하고 gap으로 간격을 맞춘다.
|
|
30
|
+
display: flex;
|
|
31
|
+
gap: var(--cctv-carousel-gap);
|
|
32
|
+
margin: 0;
|
|
33
|
+
padding: 0;
|
|
34
|
+
list-style: none;
|
|
35
|
+
scroll-behavior: smooth;
|
|
36
|
+
& > * {
|
|
37
|
+
scroll-snap-align: start;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.cctv-pagination-track > li {
|
|
42
|
+
// panel 너비를 고정해 viewport 이동 시 균등하게 보이도록 한다.
|
|
43
|
+
flex: 0 0 minmax(150px, 1fr);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.cctv-pagination-list-item {
|
|
47
|
+
cursor: pointer;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.cctv-pagination-list-item .cctv-video-container {
|
|
51
|
+
max-height: 152px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.cctv-pagination-list-thumbnail {
|
|
55
|
+
width: 100%;
|
|
56
|
+
height: 100%;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.cctv-pagination-move-button {
|
|
60
|
+
width: var(--cctv-pagination-move-button-size);
|
|
61
|
+
height: var(--cctv-pagination-move-button-size);
|
|
62
|
+
border-radius: var(--cctv-pagination-move-button-size);
|
|
63
|
+
|
|
64
|
+
display: flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
justify-content: center;
|
|
67
|
+
|
|
68
|
+
cursor: pointer;
|
|
69
|
+
|
|
70
|
+
background: var(--cctv-pagination-move-button-active);
|
|
71
|
+
&.disabled {
|
|
72
|
+
background: var(--cctv-pagination-move-button-disabled);
|
|
73
|
+
}
|
|
74
|
+
&:disabled {
|
|
75
|
+
background: var(--cctv-pagination-move-button-disabled);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
/* Card layout */
|
|
3
|
+
--cctv-video-radius: 12px;
|
|
4
|
+
--cctv-list-gap: var(--spacing-gap-5);
|
|
5
|
+
|
|
6
|
+
--cctv-overlay-padding-x: var(--spacing-padding-8);
|
|
7
|
+
--cctv-overlay-padding-y: var(--spacing-padding-6);
|
|
8
|
+
|
|
9
|
+
--cctv-video-bg: #5f5f5f;
|
|
10
|
+
--cctv-overlay-bg: linear-gradient(
|
|
11
|
+
180deg,
|
|
12
|
+
rgba(0, 0, 0, 0) 0%,
|
|
13
|
+
rgba(0, 0, 0, 0.2) 100%
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
/* Live State */
|
|
17
|
+
--cctv-live-state-bg-active: var(--color-cool-gray-10);
|
|
18
|
+
--cctv-live-state-dot-active: var(--color-feedback-new);
|
|
19
|
+
--cctv-live-state-text-active: var(--color-common-99);
|
|
20
|
+
--cctv-live-state-bg-disabled: var(--color-cool-gray-55);
|
|
21
|
+
--cctv-live-state-dot-disabled: var(--color-border-strong);
|
|
22
|
+
--cctv-live-state-text-disabled: var(--color-label-disabled);
|
|
23
|
+
|
|
24
|
+
/* Error */
|
|
25
|
+
--cctv-error-text-color: var(--color-label-disabled);
|
|
26
|
+
--cctv-error-icon-color: var(--color-label-disabled);
|
|
27
|
+
|
|
28
|
+
/* Pagination */
|
|
29
|
+
--cctv-carousel-gap: var(--spacing-gap-5);
|
|
30
|
+
--cctv-pagination-move-button-size: 48px;
|
|
31
|
+
--cctv-pagination-move-button-active: var(--color-cool-gray-20);
|
|
32
|
+
--cctv-pagination-move-button-disabled: var(--color-cool-gray-85);
|
|
33
|
+
|
|
34
|
+
/* Controls */
|
|
35
|
+
--cctv-open-button-bg: rgba(0, 0, 0, 0.55);
|
|
36
|
+
--cctv-open-button-color: #ffffff;
|
|
37
|
+
--cctv-close-button-bg: rgba(0, 0, 0, 0.45);
|
|
38
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
.cctv-video-container {
|
|
2
|
+
position: relative;
|
|
3
|
+
width: 100%;
|
|
4
|
+
aspect-ratio: 4 / 3;
|
|
5
|
+
min-height: 240px;
|
|
6
|
+
border-radius: var(--cctv-video-radius);
|
|
7
|
+
|
|
8
|
+
background: var(--cctv-video-bg);
|
|
9
|
+
// overflow: hidden;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.cctv-video-box {
|
|
13
|
+
width: 100%;
|
|
14
|
+
height: 100%;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.cctv-video-box video {
|
|
18
|
+
width: 100%;
|
|
19
|
+
height: 100%;
|
|
20
|
+
margin: 0;
|
|
21
|
+
display: block;
|
|
22
|
+
object-fit: contain;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.cctv-video-overlay-container {
|
|
26
|
+
padding: var(--cctv-overlay-padding-y) var(--cctv-overlay-padding-x);
|
|
27
|
+
|
|
28
|
+
position: absolute;
|
|
29
|
+
inset: 0;
|
|
30
|
+
z-index: 1;
|
|
31
|
+
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-direction: column;
|
|
34
|
+
|
|
35
|
+
background: var(--cctv-overlay-bg);
|
|
36
|
+
}
|
|
37
|
+
.cctv-video-overlay-header {
|
|
38
|
+
width: 100%;
|
|
39
|
+
height: fit-content;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.cctv-video-overlay-header-upper,
|
|
43
|
+
.cctv-video-overlay-header-lower {
|
|
44
|
+
display: flex;
|
|
45
|
+
align-items: center;
|
|
46
|
+
justify-content: space-between;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.cctv-video-live-state {
|
|
50
|
+
--theme-badge-font-weight: 400;
|
|
51
|
+
|
|
52
|
+
margin-right: 4px;
|
|
53
|
+
}
|
|
54
|
+
.cctv-video-live-state-dot {
|
|
55
|
+
width: 4px;
|
|
56
|
+
height: 4px;
|
|
57
|
+
margin-right: 4px;
|
|
58
|
+
border-radius: 4px;
|
|
59
|
+
background: var(--cctv-live-state-dot-active);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.cctv-video-live-state:where(.disabled) {
|
|
63
|
+
--badge-fill-bg-color: var(--cctv-live-state-bg-disabled);
|
|
64
|
+
--badge-fill-label-color: var(--cctv-live-state-text-disabled);
|
|
65
|
+
.cctv-video-live-state-dot {
|
|
66
|
+
background: var(--cctv-live-state-dot-disabled);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.cctv-video-close-button {
|
|
71
|
+
width: 36px;
|
|
72
|
+
height: 36px;
|
|
73
|
+
border-radius: 50%;
|
|
74
|
+
font-size: 0;
|
|
75
|
+
border: none;
|
|
76
|
+
|
|
77
|
+
display: flex;
|
|
78
|
+
align-items: center;
|
|
79
|
+
justify-content: center;
|
|
80
|
+
cursor: pointer;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.cctv-video-overlay-title {
|
|
84
|
+
font-weight: 600;
|
|
85
|
+
color: var(--color-common-99);
|
|
86
|
+
line-height: 1.5em;
|
|
87
|
+
}
|
|
88
|
+
.cctv-video-header-title {
|
|
89
|
+
font-size: var(--font-heading-large-size);
|
|
90
|
+
}
|
|
91
|
+
.cctv-video-footer-title {
|
|
92
|
+
font-size: var(--font-heading-xsmall-size);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.cctv-video-overlay-body {
|
|
96
|
+
width: 100%;
|
|
97
|
+
flex: 1;
|
|
98
|
+
|
|
99
|
+
// pointer-events: none;
|
|
100
|
+
|
|
101
|
+
display: flex;
|
|
102
|
+
flex-direction: column;
|
|
103
|
+
align-items: center;
|
|
104
|
+
justify-content: center;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.cctv-video-error {
|
|
108
|
+
display: flex;
|
|
109
|
+
flex-direction: column;
|
|
110
|
+
align-items: center;
|
|
111
|
+
justify-content: center;
|
|
112
|
+
}
|
|
113
|
+
.cctv-video-error-icon {
|
|
114
|
+
margin-bottom: 2px;
|
|
115
|
+
fill: var(--cctv-error-icon-color);
|
|
116
|
+
}
|
|
117
|
+
.cctv-video-error-message {
|
|
118
|
+
font-size: 13px;
|
|
119
|
+
color: var(--cctv-error-text-color);
|
|
120
|
+
line-height: 1.5em;
|
|
121
|
+
font-weight: 600;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.cctv-video-status-text {
|
|
125
|
+
font-size: 12px;
|
|
126
|
+
color: var(--cctv-text-muted);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.cctv-video-overlay-footer {
|
|
130
|
+
display: flex;
|
|
131
|
+
align-items: center;
|
|
132
|
+
justify-content: space-between;
|
|
133
|
+
gap: 8px;
|
|
134
|
+
margin-top: auto;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.cctv-video-open-button {
|
|
138
|
+
display: flex;
|
|
139
|
+
align-items: center;
|
|
140
|
+
justify-content: center;
|
|
141
|
+
cursor: pointer;
|
|
142
|
+
}
|