@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 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-active: var(--color-cool-gray-10);
59
- --cctv-live-state-dot-active: var(--color-feedback-new);
60
- --cctv-live-state-text-active: var(--color-common-99);
61
- --cctv-live-state-bg-disabled: var(--color-cool-gray-55);
62
- --cctv-live-state-dot-disabled: var(--color-border-strong);
63
- --cctv-live-state-text-disabled: var(--color-label-disabled);
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-active);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniai-fe/uds-templates",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
4
4
  "description": "UNIAI Design System; UI Templates Package",
5
5
  "type": "module",
6
6
  "private": false,
@@ -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 { CctvCompanyCameraList } from "../../types";
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
- }: { className?: string } & Omit<CctvCompanyCameraList, "renderKey">) {
17
- const { videoRef, isTokenLoading, isTokenError, isStreaming, streamError } =
18
- useCctvRtcStream({
19
- cam,
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 isError = useMemo(
32
- () => !cam.cam_online || isTokenError || streamError,
33
- [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],
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
- <CCTVVideoContainer>
53
- <CCTVVideoContents ref={videoRef} muted />
54
- <CCTVVideoOverlayContainer>
55
- <CCTVVideoOverlayHeader
56
- activeLiveState
57
- activeTitle={false}
58
- liveStateDisabled={disabledLiveState}
59
- title={cam.cam_name}
60
- />
61
- <CCTVVideoOverlayBody className={isError ? "is-error" : undefined}>
62
- {disabledLiveState ? (
63
- <CCTVVideoError>{overlayMessage}</CCTVVideoError>
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 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";
4
+ import type { CctvCompanyCameraData } from "../../../types";
11
5
  import { useCallback, useMemo } from "react";
12
6
  import { useCctvRtcStream } from "../../../hooks";
13
- import CCTVVideoOverlayHeader from "../../video/overlay/header/Container";
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
- } & Omit<CctvCompanyCameraList, "renderKey">) {
21
- const { videoRef, isTokenLoading, isTokenError, isStreaming, streamError } =
22
- useCctvRtcStream({
23
- cam,
24
- companyId: cam.company_id,
25
- });
19
+ } & CctvCompanyCameraData) {
20
+ const { videoRef, ...rtcCtx } = useCctvRtcStream({ cam });
26
21
 
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],
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
- <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>
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 { CctvCompanyCameraList } from "../../../../types";
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 { CctvCompanyCameraList } from "../../../../types";
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
- 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
- }) {
13
+ isLive,
14
+ }: CctvVideoOverlayHeaderProps) {
23
15
  const showUpper = activeLiveState || activeCloseButton;
24
- const showLower = activeTitle || activeTime;
25
-
26
- if (!showUpper && !showLower) return null;
16
+ const showLower = activeTitle;
27
17
 
28
18
  return (
29
- <header className={clsx("cctv-video-overlay-header", className)}>
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
- disabled = false,
7
+ isLive = false,
8
8
  }: {
9
- disabled?: boolean;
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", { disabled })}
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 { CctvCompanyCameraList } from "../../../types";
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?: Omit<CctvCompanyCameraList, "renderKey">;
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 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";
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?: Omit<CctvCompanyCameraList, "renderKey">;
17
+ cam?: CctvCompanyCameraData;
20
18
  }) {
21
- const { selectedCam, selectedCompany, selectedCompanyId, isFetching } =
22
- useCctvContext();
19
+ const { selectedCam, isFetching } = useCctvContext();
23
20
 
24
21
  // props의 cam이 들어오면 우선 적용
25
22
  // cam이 부여되어있으면 selectedCam 무시
26
23
  // cam이 없다면 selectedCam 적용
27
- const camData = useMemo(() => cam ?? selectedCam, [cam, selectedCam]);
28
- // console.log("CCTVViewerDesktopVideo camData:", camData);
24
+ const cam = useMemo(() => propsCam ?? selectedCam, [propsCam, selectedCam]);
29
25
 
30
- const isCamOffline = useMemo(() => !camData?.cam_online, [camData]);
26
+ const { videoRef, ...rtcCtx } = useCctvRtcStream({ cam });
31
27
 
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
- }
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
- <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>
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 { data, isFetching, isError, ...rest };
57
+ return {
58
+ data,
59
+ isFetching,
60
+ isError: Boolean(isError),
61
+ ...rest,
62
+ };
38
63
  }