@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.
Files changed (79) hide show
  1. package/dist/styles.css +2 -2
  2. package/package.json +8 -4
  3. package/src/auth/login/jotai/user.ts +13 -0
  4. package/src/auth/login/types/api.ts +229 -0
  5. package/src/auth/login/types/form.ts +1 -0
  6. package/src/auth/login/types/index.ts +4 -0
  7. package/src/auth/signup/markup/VerificationForm.tsx +3 -2
  8. package/src/cctv/apis/client.ts +61 -0
  9. package/src/cctv/apis/index.ts +2 -0
  10. package/src/cctv/apis/server.ts +188 -0
  11. package/src/cctv/components/Provider.tsx +47 -0
  12. package/src/cctv/components/__viewer.tsx +99 -0
  13. package/src/cctv/components/cam-list/Container.tsx +36 -0
  14. package/src/cctv/components/cam-list/Item.tsx +71 -0
  15. package/src/cctv/components/cam-list/index.tsx +7 -0
  16. package/src/cctv/components/index.tsx +13 -0
  17. package/src/cctv/components/pagination/Container.tsx +26 -0
  18. package/src/cctv/components/pagination/Control.tsx +29 -0
  19. package/src/cctv/components/pagination/Provider.tsx +204 -0
  20. package/src/cctv/components/pagination/buttons/Base.tsx +56 -0
  21. package/src/cctv/components/pagination/buttons/Next.tsx +34 -0
  22. package/src/cctv/components/pagination/buttons/Prev.tsx +34 -0
  23. package/src/cctv/components/pagination/index.tsx +25 -0
  24. package/src/cctv/components/pagination/list/Carousel.tsx +26 -0
  25. package/src/cctv/components/pagination/list/Container.tsx +30 -0
  26. package/src/cctv/components/pagination/list/Item.tsx +81 -0
  27. package/src/cctv/components/video/Container.tsx +13 -0
  28. package/src/cctv/components/video/Video.tsx +34 -0
  29. package/src/cctv/components/video/index.tsx +9 -0
  30. package/src/cctv/components/video/overlay/Container.tsx +15 -0
  31. package/src/cctv/components/video/overlay/Title.tsx +28 -0
  32. package/src/cctv/components/video/overlay/body/Container.tsx +13 -0
  33. package/src/cctv/components/video/overlay/body/Error.tsx +16 -0
  34. package/src/cctv/components/video/overlay/footer/Container.tsx +30 -0
  35. package/src/cctv/components/video/overlay/footer/OpenButton.tsx +19 -0
  36. package/src/cctv/components/video/overlay/header/CloseButton.tsx +14 -0
  37. package/src/cctv/components/video/overlay/header/Container.tsx +50 -0
  38. package/src/cctv/components/video/overlay/header/LiveState.tsx +21 -0
  39. package/src/cctv/components/video/overlay/index.tsx +24 -0
  40. package/src/cctv/components/viewer/Container.tsx +13 -0
  41. package/src/cctv/components/viewer/desktop/Container.tsx +38 -0
  42. package/src/cctv/components/viewer/desktop/Pagination.tsx +20 -0
  43. package/src/cctv/components/viewer/desktop/Placeholder.tsx +18 -0
  44. package/src/cctv/components/viewer/desktop/Video.tsx +83 -0
  45. package/src/cctv/components/viewer/index.tsx +12 -0
  46. package/src/cctv/components/viewer/mobile/Container.tsx +13 -0
  47. package/src/cctv/data/context.ts +22 -0
  48. package/src/cctv/data/index.ts +1 -0
  49. package/src/cctv/hooks/index.tsx +5 -0
  50. package/src/cctv/hooks/useCompanyData.tsx +39 -0
  51. package/src/cctv/hooks/useContext.ts +150 -0
  52. package/src/cctv/hooks/useRtcStream.ts +94 -0
  53. package/src/cctv/img/chevron-left.svg +3 -0
  54. package/src/cctv/img/chevron-right.svg +3 -0
  55. package/src/cctv/img/error.svg +4 -0
  56. package/src/cctv/img/viewer-close.svg +3 -0
  57. package/src/cctv/img/viewer-open.svg +6 -0
  58. package/src/cctv/index.scss +1 -0
  59. package/src/cctv/index.tsx +9 -0
  60. package/src/cctv/jotai/context.ts +9 -0
  61. package/src/cctv/jotai/index.ts +1 -0
  62. package/src/cctv/styles/cam-list.scss +32 -0
  63. package/src/cctv/styles/index.scss +5 -0
  64. package/src/cctv/styles/pagination.scss +77 -0
  65. package/src/cctv/styles/variables.scss +38 -0
  66. package/src/cctv/styles/video.scss +142 -0
  67. package/src/cctv/styles/viewer.scss +7 -0
  68. package/src/cctv/types/api.ts +166 -0
  69. package/src/cctv/types/carousel.ts +24 -0
  70. package/src/cctv/types/context.ts +68 -0
  71. package/src/cctv/types/index.ts +4 -0
  72. package/src/cctv/types/list.ts +94 -0
  73. package/src/cctv/utils/data.ts +40 -0
  74. package/src/cctv/utils/select.ts +62 -0
  75. package/src/index.tsx +3 -0
  76. package/src/modal/styles/base.scss +2 -2
  77. package/src/types/api.ts +43 -0
  78. package/src/types/index.ts +1 -0
  79. package/src/auth/login/types.ts +0 -2
@@ -0,0 +1,30 @@
1
+ import clsx from "clsx";
2
+ import CCTVPaginationListItem from "./Item";
3
+ import type { CctvCompanyCameraList } from "../../../types";
4
+ import CCTVPaginationListCarousel from "./Carousel";
5
+ import { useCctvContext } from "../../../hooks";
6
+ import { useMemo } from "react";
7
+
8
+ export default function CCTVPaginationListContainer({
9
+ list,
10
+ }: {
11
+ list?: CctvCompanyCameraList[];
12
+ }) {
13
+ const { cams } = useCctvContext();
14
+
15
+ const camList = useMemo(() => list ?? cams, [list, cams]);
16
+
17
+ return (
18
+ <div className={clsx("cctv-pagination-list-container")}>
19
+ {camList.length === 0 ? (
20
+ <div>데이터 없음</div>
21
+ ) : (
22
+ <CCTVPaginationListCarousel>
23
+ {camList.map(({ renderKey, ...d }, i) => (
24
+ <CCTVPaginationListItem key={`${renderKey}/${i}`} {...d} />
25
+ ))}
26
+ </CCTVPaginationListCarousel>
27
+ )}
28
+ </div>
29
+ );
30
+ }
@@ -0,0 +1,81 @@
1
+ "use client";
2
+
3
+ import clsx from "clsx";
4
+ import CCTVVideoContainer from "../../video/Container";
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";
11
+ import { useCallback, useMemo } from "react";
12
+ import { useCctvRtcStream } from "../../../hooks";
13
+ import CCTVVideoOverlayHeader from "../../video/overlay/header/Container";
14
+
15
+ export default function CCTVPaginationListItem({
16
+ className,
17
+ ...cam
18
+ }: {
19
+ className?: string;
20
+ } & Omit<CctvCompanyCameraList, "renderKey">) {
21
+ const { videoRef, isTokenLoading, isTokenError, isStreaming, streamError } =
22
+ useCctvRtcStream({
23
+ cam,
24
+ companyId: cam.company_id,
25
+ });
26
+
27
+ const overlayMessage = (() => {
28
+ if (!cam.cam_online) return "CCTV 연결 오류";
29
+ if (isTokenLoading || isStreaming) return "스트림을 준비하고 있습니다.";
30
+ if (isTokenError) return "토큰을 발급하지 못했습니다.";
31
+ if (streamError) return streamError;
32
+ return null;
33
+ })();
34
+
35
+ const isError = useMemo(
36
+ () => !cam.cam_online || isTokenError || streamError,
37
+ [cam.cam_online, isTokenError, streamError],
38
+ );
39
+
40
+ const disabledLiveState = useMemo(() => {
41
+ if (isTokenLoading || isTokenError) return true;
42
+ if (streamError) return true;
43
+ return false;
44
+ }, [isTokenLoading, isTokenError, streamError]);
45
+
46
+ const handleSelect = useCallback(() => {
47
+ if (typeof cam.onSelect === "function") cam.onSelect();
48
+ }, [cam]);
49
+
50
+ return (
51
+ <li
52
+ className={clsx(
53
+ "cctv-pagination-list-item",
54
+ className,
55
+ cam.selected && "is-selected",
56
+ isError && "is-error",
57
+ )}
58
+ >
59
+ <button
60
+ type="button"
61
+ className="cctv-pagination-list-thumbnail"
62
+ onClick={handleSelect}
63
+ aria-pressed={cam.selected}
64
+ aria-label={`${cam.cam_name} 선택`}
65
+ >
66
+ <CCTVVideoContainer>
67
+ <CCTVVideoContents ref={videoRef} muted />
68
+ <CCTVVideoOverlayContainer>
69
+ <CCTVVideoOverlayHeader />
70
+ <CCTVVideoOverlayBody className={isError ? "is-error" : undefined}>
71
+ {disabledLiveState ? (
72
+ <CCTVVideoError>{overlayMessage}</CCTVVideoError>
73
+ ) : null}
74
+ </CCTVVideoOverlayBody>
75
+ <CCTVVideoOverlayFooter cam={cam} />
76
+ </CCTVVideoOverlayContainer>
77
+ </CCTVVideoContainer>
78
+ </button>
79
+ </li>
80
+ );
81
+ }
@@ -0,0 +1,13 @@
1
+ import clsx from "clsx";
2
+
3
+ export default function CCTVVideoContainer({
4
+ className,
5
+ children,
6
+ }: {
7
+ className?: string;
8
+ children: React.ReactNode;
9
+ }) {
10
+ return (
11
+ <div className={clsx("cctv-video-container", className)}>{children}</div>
12
+ );
13
+ }
@@ -0,0 +1,34 @@
1
+ "use client";
2
+
3
+ import { forwardRef } from "react";
4
+ import clsx from "clsx";
5
+
6
+ type CCTVVideoContentsProps = {
7
+ className?: string;
8
+ muted?: boolean;
9
+ autoPlay?: boolean;
10
+ controls?: boolean;
11
+ poster?: string;
12
+ };
13
+
14
+ const CCTVVideoContents = forwardRef<HTMLVideoElement, CCTVVideoContentsProps>(
15
+ (
16
+ { className, muted = true, autoPlay = true, controls = false, poster },
17
+ ref,
18
+ ) => (
19
+ <figure className={clsx("cctv-video-box", className)}>
20
+ <video
21
+ ref={ref}
22
+ muted={muted}
23
+ autoPlay={autoPlay}
24
+ playsInline
25
+ controls={controls}
26
+ poster={poster}
27
+ />
28
+ </figure>
29
+ ),
30
+ );
31
+
32
+ CCTVVideoContents.displayName = "CCTVVideoContents";
33
+
34
+ export default CCTVVideoContents;
@@ -0,0 +1,9 @@
1
+ import CCTVVideoContainer from "./Container";
2
+ import { CCTVVideoOverlay } from "./overlay";
3
+ import CCTVVideoContents from "./Video";
4
+
5
+ export const CCTVVideo = {
6
+ Container: CCTVVideoContainer,
7
+ Contents: CCTVVideoContents,
8
+ Overlay: CCTVVideoOverlay,
9
+ };
@@ -0,0 +1,15 @@
1
+ import clsx from "clsx";
2
+
3
+ export default function CCTVVideoOverlayContainer({
4
+ className,
5
+ children,
6
+ }: {
7
+ className?: string;
8
+ children: React.ReactNode;
9
+ }) {
10
+ return (
11
+ <div className={clsx("cctv-video-overlay-container", className)}>
12
+ {children}
13
+ </div>
14
+ );
15
+ }
@@ -0,0 +1,28 @@
1
+ "use client";
2
+
3
+ import clsx from "clsx";
4
+ import { useMemo } from "react";
5
+ import { useCctvContext } from "../../../hooks";
6
+
7
+ export default function CCTVVideoOverlayTitle({
8
+ className,
9
+ title,
10
+ }: {
11
+ className?: string;
12
+ title?: React.ReactNode;
13
+ }) {
14
+ const { selectedCam } = useCctvContext();
15
+
16
+ // props의 cam이 들어오면 우선 적용
17
+ // cam이 부여되어있으면 selectedCam 무시
18
+ // cam이 없다면 selectedCam 적용
19
+ const camTitle = useMemo(
20
+ () => (title ?? selectedCam?.cam_name) || "-",
21
+ [title, selectedCam],
22
+ );
23
+ return (
24
+ <h4 className={clsx("cctv-video-overlay-title", className)}>
25
+ <span>{camTitle}</span>
26
+ </h4>
27
+ );
28
+ }
@@ -0,0 +1,13 @@
1
+ import clsx from "clsx";
2
+
3
+ export default function CCTVVideoOverlayBody({
4
+ className,
5
+ children,
6
+ }: {
7
+ className?: string;
8
+ children?: React.ReactNode;
9
+ }) {
10
+ return (
11
+ <div className={clsx("cctv-video-overlay-body", className)}>{children}</div>
12
+ );
13
+ }
@@ -0,0 +1,16 @@
1
+ import CCTVErrorIcon from "../../../../img/error.svg";
2
+
3
+ export default function CCTVVideoError({
4
+ children,
5
+ }: {
6
+ children?: React.ReactNode;
7
+ }) {
8
+ return (
9
+ <div className="cctv-video-error">
10
+ <figure className="cctv-video-error-icon">
11
+ <CCTVErrorIcon width={24} height={24} alt="CCTV 연결 오류" />
12
+ </figure>
13
+ <p className="cctv-video-error-message">{children ?? "CCTV 연결 오류"}</p>
14
+ </div>
15
+ );
16
+ }
@@ -0,0 +1,30 @@
1
+ import clsx from "clsx";
2
+ import CCTVVideoOpenButton from "./OpenButton";
3
+ import CCTVVideoOverlayTitle from "../Title";
4
+ import type { CctvCompanyCameraList } from "../../../../types";
5
+
6
+ export default function CCTVVideoOverlayFooter({
7
+ className,
8
+ activeTitle = true,
9
+ activeOpenButton,
10
+ cam,
11
+ }: {
12
+ className?: string;
13
+ activeTitle?: boolean;
14
+ activeOpenButton?: boolean;
15
+ cam?: Omit<CctvCompanyCameraList, "renderKey">;
16
+ }) {
17
+ // console.log(cam);
18
+ return (
19
+ <footer className={clsx("cctv-video-overlay-footer", className)}>
20
+ {activeTitle && (
21
+ <CCTVVideoOverlayTitle
22
+ className={clsx("cctv-video-footer-title", className)}
23
+ // Carousel/Card 등 외부에서 전달한 타이틀을 그대로 노출한다.
24
+ title={cam?.cam_name ?? "-"}
25
+ />
26
+ )}
27
+ {activeOpenButton && cam && <CCTVVideoOpenButton {...cam} />}
28
+ </footer>
29
+ );
30
+ }
@@ -0,0 +1,19 @@
1
+ "use client";
2
+
3
+ import { useCallback } from "react";
4
+ import ViewerOpenIcon from "../../../../img/viewer-open.svg";
5
+ import type { CctvCompanyCameraList } from "../../../../types";
6
+
7
+ export default function CCTVVideoOpenButton({
8
+ ...cam
9
+ }: Omit<CctvCompanyCameraList, "renderKey">) {
10
+ const onOpen = useCallback(() => {
11
+ if (typeof cam.onSelect === "function") cam.onSelect();
12
+ }, [cam]);
13
+
14
+ return (
15
+ <button type="button" className="cctv-video-open-button" onClick={onOpen}>
16
+ <ViewerOpenIcon width={24} height={24} alt="크게 보기" />
17
+ </button>
18
+ );
19
+ }
@@ -0,0 +1,14 @@
1
+ "use client";
2
+
3
+ import { useCallback } from "react";
4
+ import ViewerCloseIcon from "../../../../img/viewer-close.svg";
5
+
6
+ export default function CCTVVideoCloseButton() {
7
+ const onClose = useCallback(() => {}, []);
8
+
9
+ return (
10
+ <button type="button" className="cctv-video-close-button" onClick={onClose}>
11
+ <ViewerCloseIcon width={36} height={36} alt="닫기" />
12
+ </button>
13
+ );
14
+ }
@@ -0,0 +1,50 @@
1
+ import clsx from "clsx";
2
+ import CCTVVideoLiveState from "./LiveState";
3
+ import CCTVVideoCloseButton from "./CloseButton";
4
+ import CCTVVideoOverlayTitle from "../Title";
5
+
6
+ export default function CCTVVideoOverlayHeader({
7
+ className,
8
+ activeLiveState,
9
+ activeCloseButton,
10
+ activeTitle,
11
+ title,
12
+ activeTime,
13
+ liveStateDisabled,
14
+ }: {
15
+ className?: string;
16
+ activeLiveState?: boolean;
17
+ activeCloseButton?: boolean;
18
+ activeTitle?: boolean;
19
+ title?: React.ReactNode;
20
+ activeTime?: boolean;
21
+ liveStateDisabled?: boolean;
22
+ }) {
23
+ const showUpper = activeLiveState || activeCloseButton;
24
+ const showLower = activeTitle || activeTime;
25
+
26
+ if (!showUpper && !showLower) return null;
27
+
28
+ return (
29
+ <header className={clsx("cctv-video-overlay-header", className)}>
30
+ {showUpper && (
31
+ <div className="cctv-video-overlay-header-upper">
32
+ {activeLiveState && (
33
+ <CCTVVideoLiveState disabled={liveStateDisabled} />
34
+ )}
35
+ {activeCloseButton && <CCTVVideoCloseButton />}
36
+ </div>
37
+ )}
38
+ {showLower && (
39
+ <div className="cctv-video-overlay-header-lower">
40
+ {activeTitle && (
41
+ <CCTVVideoOverlayTitle
42
+ className={clsx("cctv-video-header-title", className)}
43
+ title={title}
44
+ />
45
+ )}
46
+ </div>
47
+ )}
48
+ </header>
49
+ );
50
+ }
@@ -0,0 +1,21 @@
1
+ "use client";
2
+
3
+ import clsx from "clsx";
4
+ import { Badge } from "@uniai-fe/uds-primitives";
5
+
6
+ export default function CCTVVideoLiveState({
7
+ disabled = false,
8
+ }: {
9
+ disabled?: boolean;
10
+ }) {
11
+ return (
12
+ <Badge
13
+ size="xsmall"
14
+ intent="tertiary"
15
+ className={clsx("cctv-video-live-state", { disabled })}
16
+ >
17
+ <figure className="cctv-video-live-state-dot"></figure>
18
+ <span>Live</span>
19
+ </Badge>
20
+ );
21
+ }
@@ -0,0 +1,24 @@
1
+ import CCTVVideoOverlayBody from "./body/Container";
2
+ import CCTVVideoError from "./body/Error";
3
+ import CCTVVideoOverlayContainer from "./Container";
4
+ import CCTVVideoOverlayFooter from "./footer/Container";
5
+ import CCTVVideoOpenButton from "./footer/OpenButton";
6
+ import CCTVVideoCloseButton from "./header/CloseButton";
7
+ import CCTVVideoOverlayHeader from "./header/Container";
8
+ import CCTVVideoLiveState from "./header/LiveState";
9
+ import CCTVVideoOverlayTitle from "./Title";
10
+
11
+ export const CCTVVideoOverlay = {
12
+ Container: CCTVVideoOverlayContainer,
13
+ Title: CCTVVideoOverlayTitle,
14
+ Header: {
15
+ Container: CCTVVideoOverlayHeader,
16
+ LiveState: CCTVVideoLiveState,
17
+ CloseButton: CCTVVideoCloseButton,
18
+ },
19
+ Body: { Container: CCTVVideoOverlayBody, Error: CCTVVideoError },
20
+ Footer: {
21
+ Container: CCTVVideoOverlayFooter,
22
+ OpenButton: CCTVVideoOpenButton,
23
+ },
24
+ };
@@ -0,0 +1,13 @@
1
+ import clsx from "clsx";
2
+
3
+ export default function CCTVViewerContainer({
4
+ className,
5
+ children,
6
+ }: {
7
+ className?: string;
8
+ children: React.ReactNode;
9
+ }) {
10
+ return (
11
+ <div className={clsx("cctv-viewer-container", className)}>{children}</div>
12
+ );
13
+ }
@@ -0,0 +1,38 @@
1
+ import clsx from "clsx";
2
+ import CCTVViewerContainer from "../Container";
3
+ import CCTVViewerDesktopPagination from "./Pagination";
4
+ import CCTVViewerDesktopVideo from "./Video";
5
+ import type { CctvCompanyCameraList } from "../../../types";
6
+
7
+ export default function CCTVViewerDesktopContainer({
8
+ className,
9
+ children,
10
+ selectedCam,
11
+ camList,
12
+ }: {
13
+ className?: string;
14
+ children?: React.ReactNode;
15
+ selectedCam?: Omit<CctvCompanyCameraList, "renderKey">;
16
+ camList?: CctvCompanyCameraList[];
17
+ }) {
18
+ return (
19
+ <CCTVViewerContainer
20
+ className={clsx("cctv-viewer-desktop-container", className)}
21
+ >
22
+ {typeof children !== "undefined" ? (
23
+ children
24
+ ) : (
25
+ <>
26
+ <CCTVViewerDesktopVideo
27
+ {...(typeof selectedCam !== "undefined"
28
+ ? { cam: selectedCam }
29
+ : {})}
30
+ />
31
+ <CCTVViewerDesktopPagination
32
+ {...(typeof camList !== "undefined" ? { list: camList } : {})}
33
+ />
34
+ </>
35
+ )}
36
+ </CCTVViewerContainer>
37
+ );
38
+ }
@@ -0,0 +1,20 @@
1
+ "use client";
2
+
3
+ import { useCctvContext } from "../../../hooks";
4
+ import type { CctvCompanyCameraList } from "../../../types";
5
+ import CCTVPaginationContainer from "../../pagination/Container";
6
+ import CCTVPaginationListContainer from "../../pagination/list/Container";
7
+
8
+ export default function CCTVViewerDesktopPagination({
9
+ list,
10
+ }: {
11
+ list?: CctvCompanyCameraList[];
12
+ }) {
13
+ const { cams } = useCctvContext();
14
+
15
+ return (
16
+ <CCTVPaginationContainer className="cctv-viewer-desktop-pagination-container">
17
+ <CCTVPaginationListContainer list={list ?? cams} />
18
+ </CCTVPaginationContainer>
19
+ );
20
+ }
@@ -0,0 +1,18 @@
1
+ import clsx from "clsx";
2
+
3
+ /**
4
+ * Viewer Placeholder
5
+ * @desc
6
+ * - 데이터가 없거나 로딩 중일 때 사용자에게 상황을 전달한다.
7
+ */
8
+ export default function CCTVViewerDesktopPlaceholder({
9
+ className,
10
+ message,
11
+ }: {
12
+ className?: string;
13
+ message: string;
14
+ }) {
15
+ return (
16
+ <div className={clsx("cctv-viewer-placeholder", className)}>{message}</div>
17
+ );
18
+ }
@@ -0,0 +1,83 @@
1
+ "use client";
2
+
3
+ import { useMemo } from "react";
4
+
5
+ import { useCctvContext, useCctvRtcStream } from "../../../hooks";
6
+ import CCTVVideoContainer from "../../video/Container";
7
+ import CCTVVideoOverlayBody from "../../video/overlay/body/Container";
8
+ import CCTVVideoError from "../../video/overlay/body/Error";
9
+ import CCTVVideoOverlayContainer from "../../video/overlay/Container";
10
+ import CCTVVideoOverlayFooter from "../../video/overlay/footer/Container";
11
+ import CCTVVideoOverlayHeader from "../../video/overlay/header/Container";
12
+ import CCTVVideoContents from "../../video/Video";
13
+ import CCTVViewerDesktopPlaceholder from "./Placeholder";
14
+ import type { CctvCompanyCameraList } from "../../../types";
15
+
16
+ export default function CCTVViewerDesktopVideo({
17
+ cam,
18
+ }: {
19
+ cam?: Omit<CctvCompanyCameraList, "renderKey">;
20
+ }) {
21
+ const { selectedCam, selectedCompany, selectedCompanyId, isFetching } =
22
+ useCctvContext();
23
+
24
+ // props의 cam이 들어오면 우선 적용
25
+ // cam이 부여되어있으면 selectedCam 무시
26
+ // cam이 없다면 selectedCam 적용
27
+ const camData = useMemo(() => cam ?? selectedCam, [cam, selectedCam]);
28
+ // console.log("CCTVViewerDesktopVideo camData:", camData);
29
+
30
+ const isCamOffline = useMemo(() => !camData?.cam_online, [camData]);
31
+
32
+ const activeCompanyId =
33
+ camData?.company_id ?? selectedCompany?.company_id ?? selectedCompanyId;
34
+
35
+ const { videoRef, isTokenLoading, isTokenError, isStreaming, streamError } =
36
+ useCctvRtcStream({
37
+ cam: camData,
38
+ companyId: activeCompanyId,
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
+ }
61
+
62
+ return (
63
+ <CCTVVideoContainer className="cctv-viewer-desktop-video-container">
64
+ <CCTVVideoContents ref={videoRef} />
65
+ <CCTVVideoOverlayContainer>
66
+ <CCTVVideoOverlayHeader
67
+ activeLiveState
68
+ activeTitle
69
+ activeCloseButton
70
+ liveStateDisabled={isCamOffline}
71
+ />
72
+ {overlayMessage ? (
73
+ <CCTVVideoOverlayBody
74
+ className={isCamOffline ? "is-error" : undefined}
75
+ >
76
+ {isCamOffline ? <CCTVVideoError /> : overlayMessage}
77
+ </CCTVVideoOverlayBody>
78
+ ) : null}
79
+ <CCTVVideoOverlayFooter activeTitle={false} />
80
+ </CCTVVideoOverlayContainer>
81
+ </CCTVVideoContainer>
82
+ );
83
+ }
@@ -0,0 +1,12 @@
1
+ import CCTVViewerDesktopContainer from "./desktop/Container";
2
+ import CCTVViewerDesktopPagination from "./desktop/Pagination";
3
+ import CCTVViewerDesktopVideo from "./desktop/Video";
4
+
5
+ export const CCTVViewer = {
6
+ Desktop: {
7
+ Container: CCTVViewerDesktopContainer,
8
+ Video: CCTVViewerDesktopVideo,
9
+ Pagination: CCTVViewerDesktopPagination,
10
+ },
11
+ Mobile: {},
12
+ };
@@ -0,0 +1,13 @@
1
+ import CCTVViewerContainer from "../Container";
2
+
3
+ export default function CCTVViewerMobileContainer({
4
+ children,
5
+ }: {
6
+ children: React.ReactNode;
7
+ }) {
8
+ return (
9
+ <CCTVViewerContainer className="cctv-mobile-viewer-container">
10
+ {children}
11
+ </CCTVViewerContainer>
12
+ );
13
+ }
@@ -0,0 +1,22 @@
1
+ import type { CctvContext } from "../types/context";
2
+
3
+ /**
4
+ * CCTV; CCTV 컨텍스트 기본값
5
+ * - company_id: 선택된 업체 id코드
6
+ * - cam_id: 선택된 카메라 id코드
7
+ * - search: 검색 키워드 (입력값)
8
+ * - filter: 검색 키워드 (필터값)
9
+ * - rawData: 원본 데이터 배열
10
+ * - isFetching: 데이터 로딩 상태
11
+ * - isError: 데이터 에러 상태
12
+ */
13
+ export const CCTV_CONTEXT_DEFAULT_VALUES: CctvContext = {
14
+ username: "",
15
+ company_id: "",
16
+ cam_id: "",
17
+ search: "",
18
+ filter: "",
19
+ rawData: [],
20
+ isFetching: false,
21
+ isError: false,
22
+ };
@@ -0,0 +1 @@
1
+ export * from "./context";
@@ -0,0 +1,5 @@
1
+ import useCctvCompanyData from "./useCompanyData";
2
+ import useCctvContext from "./useContext";
3
+ import { useCctvRtcStream } from "./useRtcStream";
4
+
5
+ export { useCctvContext, useCctvCompanyData, useCctvRtcStream };