@uniai-fe/uds-templates 0.1.19 → 0.1.21

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 (104) hide show
  1. package/dist/styles.css +58 -9
  2. package/package.json +7 -3
  3. package/src/auth/common/container/header/StageHeader.tsx +2 -2
  4. package/src/auth/login/jotai/user.ts +13 -0
  5. package/src/auth/login/types/api.ts +229 -0
  6. package/src/auth/login/types/form.ts +1 -0
  7. package/src/auth/login/types/index.ts +4 -0
  8. package/src/auth/signup/markup/VerificationForm.tsx +3 -2
  9. package/src/cctv/apis/client.ts +61 -0
  10. package/src/cctv/apis/index.ts +2 -0
  11. package/src/cctv/apis/server.ts +188 -0
  12. package/src/cctv/components/Provider.tsx +47 -0
  13. package/src/cctv/components/__viewer.tsx +99 -0
  14. package/src/cctv/components/cam-list/Container.tsx +36 -0
  15. package/src/cctv/components/cam-list/Item.tsx +71 -0
  16. package/src/cctv/components/cam-list/index.tsx +7 -0
  17. package/src/cctv/components/index.tsx +13 -0
  18. package/src/cctv/components/pagination/Container.tsx +26 -0
  19. package/src/cctv/components/pagination/Control.tsx +29 -0
  20. package/src/cctv/components/pagination/Provider.tsx +204 -0
  21. package/src/cctv/components/pagination/buttons/Base.tsx +56 -0
  22. package/src/cctv/components/pagination/buttons/Next.tsx +34 -0
  23. package/src/cctv/components/pagination/buttons/Prev.tsx +34 -0
  24. package/src/cctv/components/pagination/index.tsx +25 -0
  25. package/src/cctv/components/pagination/list/Carousel.tsx +26 -0
  26. package/src/cctv/components/pagination/list/Container.tsx +30 -0
  27. package/src/cctv/components/pagination/list/Item.tsx +81 -0
  28. package/src/cctv/components/video/Container.tsx +13 -0
  29. package/src/cctv/components/video/Video.tsx +34 -0
  30. package/src/cctv/components/video/index.tsx +9 -0
  31. package/src/cctv/components/video/overlay/Container.tsx +15 -0
  32. package/src/cctv/components/video/overlay/Title.tsx +28 -0
  33. package/src/cctv/components/video/overlay/body/Container.tsx +13 -0
  34. package/src/cctv/components/video/overlay/body/Error.tsx +16 -0
  35. package/src/cctv/components/video/overlay/footer/Container.tsx +30 -0
  36. package/src/cctv/components/video/overlay/footer/OpenButton.tsx +19 -0
  37. package/src/cctv/components/video/overlay/header/CloseButton.tsx +14 -0
  38. package/src/cctv/components/video/overlay/header/Container.tsx +50 -0
  39. package/src/cctv/components/video/overlay/header/LiveState.tsx +21 -0
  40. package/src/cctv/components/video/overlay/index.tsx +24 -0
  41. package/src/cctv/components/viewer/Container.tsx +13 -0
  42. package/src/cctv/components/viewer/desktop/Container.tsx +38 -0
  43. package/src/cctv/components/viewer/desktop/Pagination.tsx +20 -0
  44. package/src/cctv/components/viewer/desktop/Placeholder.tsx +18 -0
  45. package/src/cctv/components/viewer/desktop/Video.tsx +83 -0
  46. package/src/cctv/components/viewer/index.tsx +12 -0
  47. package/src/cctv/components/viewer/mobile/Container.tsx +13 -0
  48. package/src/cctv/data/context.ts +22 -0
  49. package/src/cctv/data/index.ts +1 -0
  50. package/src/cctv/hooks/index.tsx +5 -0
  51. package/src/cctv/hooks/useCompanyData.tsx +39 -0
  52. package/src/cctv/hooks/useContext.ts +150 -0
  53. package/src/cctv/hooks/useRtcStream.ts +94 -0
  54. package/src/cctv/img/chevron-left.svg +3 -0
  55. package/src/cctv/img/chevron-right.svg +3 -0
  56. package/src/cctv/img/error.svg +4 -0
  57. package/src/cctv/img/viewer-close.svg +3 -0
  58. package/src/cctv/img/viewer-open.svg +6 -0
  59. package/src/cctv/index.scss +1 -0
  60. package/src/cctv/index.tsx +9 -0
  61. package/src/cctv/jotai/context.ts +9 -0
  62. package/src/cctv/jotai/index.ts +1 -0
  63. package/src/cctv/styles/cam-list.scss +32 -0
  64. package/src/cctv/styles/index.scss +5 -0
  65. package/src/cctv/styles/pagination.scss +77 -0
  66. package/src/cctv/styles/variables.scss +38 -0
  67. package/src/cctv/styles/video.scss +142 -0
  68. package/src/cctv/styles/viewer.scss +7 -0
  69. package/src/cctv/types/api.ts +166 -0
  70. package/src/cctv/types/carousel.ts +24 -0
  71. package/src/cctv/types/context.ts +68 -0
  72. package/src/cctv/types/index.ts +4 -0
  73. package/src/cctv/types/list.ts +94 -0
  74. package/src/cctv/utils/data.ts +40 -0
  75. package/src/cctv/utils/select.ts +62 -0
  76. package/src/index.scss +0 -2
  77. package/src/index.tsx +3 -0
  78. package/src/modal/styles/base.scss +2 -2
  79. package/src/page-frame/desktop/index.tsx +4 -1
  80. package/src/page-frame/index.tsx +8 -2
  81. package/src/page-frame/mobile/{header/PageFrameMobileHeader.tsx → components/header/Header.tsx} +6 -6
  82. package/src/page-frame/mobile/components/header/index.ts +9 -0
  83. package/src/page-frame/mobile/components/index.tsx +14 -0
  84. package/src/page-frame/{navigation/PageFrameNavigation.tsx → mobile/components/navigation/Navigation.tsx} +3 -3
  85. package/src/page-frame/mobile/components/navigation/index.tsx +8 -0
  86. package/src/page-frame/mobile/components/page/Container.tsx +29 -0
  87. package/src/page-frame/mobile/{PageFrameMobile.tsx → components/page/Frame.tsx} +12 -9
  88. package/src/page-frame/mobile/components/page/index.tsx +11 -0
  89. package/src/page-frame/mobile/index.scss +3 -17
  90. package/src/page-frame/mobile/index.tsx +5 -2
  91. package/src/page-frame/{container/index.scss → mobile/styles/mobile.scss} +25 -7
  92. package/src/page-frame/mobile/types/index.ts +1 -0
  93. package/src/page-frame/mobile/types/page.ts +16 -0
  94. package/src/types/api.ts +43 -0
  95. package/src/types/index.ts +1 -0
  96. package/src/auth/login/types.ts +0 -2
  97. package/src/page-frame/container/PageFrameContainer.tsx +0 -24
  98. package/src/page-frame/container/index.tsx +0 -4
  99. package/src/page-frame/container/types.ts +0 -11
  100. package/src/page-frame/mobile/header/index.ts +0 -4
  101. package/src/page-frame/mobile/types.ts +0 -6
  102. package/src/page-frame/navigation/index.tsx +0 -4
  103. /package/src/page-frame/mobile/{header/page-frame-mobile-header.scss → styles/header.scss} +0 -0
  104. /package/src/page-frame/{navigation/index.scss → mobile/styles/navigation.scss} +0 -0
package/dist/styles.css CHANGED
@@ -19,7 +19,7 @@
19
19
  --modal-panel-width: 360px;
20
20
  --modal-panel-max-width: calc(100vw - var(--spacing-padding-10, 32px) * 2);
21
21
  --modal-panel-max-height: calc(100vh - var(--spacing-padding-10, 32px) * 2);
22
- --modal-panel-bg: var(--color-bg-surface-static-white);
22
+ --modal-panel-bg: var(--color-surface-static-white);
23
23
  --modal-panel-radius: var(--theme-radius-large-1);
24
24
  --modal-panel-shadow: 0px 18px 40px rgba(8, 11, 30, 0.18);
25
25
  --modal-border-color: var(--color-border-standard-cool-gray, #e4e5e7);
@@ -50,7 +50,7 @@
50
50
  /* namespace 충돌을 막기 위해 모든 @use 경로에 고유 alias를 부여한다. */
51
51
 
52
52
 
53
- .page-frame-container {
53
+ .page-frame-mobile-shell {
54
54
  display: grid;
55
55
  grid-template-rows: auto 1fr auto;
56
56
  width: min(100%, var(--uds-page-frame-max-width));
@@ -60,22 +60,22 @@
60
60
  color: inherit;
61
61
  }
62
62
 
63
- .page-frame-header,
64
- .page-frame-body,
65
- .page-frame-footer {
63
+ .page-frame-mobile-shell-header,
64
+ .page-frame-mobile-shell-body,
65
+ .page-frame-mobile-shell-footer {
66
66
  width: 100%;
67
67
  }
68
68
 
69
- .page-frame-header {
69
+ .page-frame-mobile-shell-header {
70
70
  padding-top: var(--uds-page-frame-header-padding-top);
71
71
  }
72
72
 
73
- .page-frame-body {
73
+ .page-frame-mobile-shell-body {
74
74
  min-height: 0;
75
75
  overflow-y: auto;
76
76
  }
77
77
 
78
- .page-frame-footer {
78
+ .page-frame-mobile-shell-footer {
79
79
  padding-bottom: var(--uds-page-frame-footer-safe-area);
80
80
  }
81
81
 
@@ -97,6 +97,55 @@
97
97
  background-color: inherit;
98
98
  }
99
99
 
100
+ .page-frame-mobile-header {
101
+ position: relative;
102
+ display: flex;
103
+ align-items: center;
104
+ justify-content: space-between;
105
+ gap: var(--spacing-padding-2, 8px);
106
+ min-height: 40px;
107
+ }
108
+
109
+ .page-frame-mobile-header__left,
110
+ .page-frame-mobile-header__right {
111
+ width: 64px;
112
+ height: 40px;
113
+ display: inline-flex;
114
+ align-items: center;
115
+ }
116
+
117
+ .page-frame-mobile-header__back {
118
+ border: none;
119
+ background: none;
120
+ display: inline-flex;
121
+ align-items: center;
122
+ gap: var(--spacing-padding-1, 4px);
123
+ padding: var(--spacing-padding-1, 4px);
124
+ font-size: 14px;
125
+ color: var(--color-label-standard);
126
+ cursor: pointer;
127
+ }
128
+
129
+ .page-frame-mobile-header__back-icon {
130
+ font-size: 18px;
131
+ line-height: 1;
132
+ }
133
+
134
+ .page-frame-mobile-header__back-placeholder {
135
+ width: 100%;
136
+ height: 100%;
137
+ }
138
+
139
+ .page-frame-mobile-header__title {
140
+ position: absolute;
141
+ left: 50%;
142
+ transform: translateX(-50%);
143
+ margin: 0;
144
+ font-size: 16px;
145
+ font-weight: 600;
146
+ color: var(--color-label-standard);
147
+ }
148
+
100
149
  .page-frame-navigation {
101
150
  width: 100%;
102
151
  }
@@ -120,7 +169,7 @@
120
169
  width: 100%;
121
170
  max-width: min(var(--modal-panel-width, 360px), var(--modal-panel-max-width, calc(100vw - var(--spacing-padding-10, 32px) * 2)));
122
171
  max-height: var(--modal-panel-max-height, calc(100vh - var(--spacing-padding-10, 32px) * 2));
123
- background-color: var(--modal-panel-bg, var(--color-bg-surface-static-white));
172
+ background-color: var(--modal-panel-bg, var(--color-surface-static-white));
124
173
  border-radius: var(--modal-panel-radius, var(--theme-radius-large-1));
125
174
  box-shadow: var(--modal-panel-shadow, 0px 18px 40px rgba(8, 11, 30, 0.18));
126
175
  pointer-events: auto;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniai-fe/uds-templates",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "description": "UNIAI Design System; UI Templates Package",
5
5
  "type": "module",
6
6
  "private": false,
@@ -48,11 +48,13 @@
48
48
  "design-templates:dev": "pnpm run dev"
49
49
  },
50
50
  "peerDependencies": {
51
+ "@tanstack/react-query": "^5",
51
52
  "@uniai-fe/uds-foundation": "^0.1.0",
52
53
  "@uniai-fe/uds-primitives": "^0.1.0",
53
54
  "@uniai-fe/util-functions": "^0.2.0",
55
+ "@uniai-fe/util-jotai": "^0.1.5",
54
56
  "@uniai-fe/util-next": "^0.2.0",
55
- "@tanstack/react-query": "^5",
57
+ "@uniai-fe/util-rtc": "^0.1.0",
56
58
  "jotai": "^2",
57
59
  "next": "^15",
58
60
  "react": "^19",
@@ -65,6 +67,7 @@
65
67
  },
66
68
  "devDependencies": {
67
69
  "@svgr/webpack": "^8.1.0",
70
+ "@tanstack/react-query": "^5.90.16",
68
71
  "@types/node": "^24.10.2",
69
72
  "@types/react": "^19.2.8",
70
73
  "@types/react-dom": "^19.2.3",
@@ -75,8 +78,9 @@
75
78
  "@uniai-fe/uds-foundation": "workspace:*",
76
79
  "@uniai-fe/uds-primitives": "workspace:*",
77
80
  "@uniai-fe/util-functions": "workspace:*",
81
+ "@uniai-fe/util-jotai": "workspace:*",
78
82
  "@uniai-fe/util-next": "workspace:*",
79
- "@tanstack/react-query": "^5.90.16",
83
+ "@uniai-fe/util-rtc": "workspace:*",
80
84
  "eslint": "^9.39.2",
81
85
  "jotai": "^2.16.1",
82
86
  "next": "^15.5.9",
@@ -1,7 +1,7 @@
1
1
  import clsx from "clsx";
2
2
  import type { ReactNode } from "react";
3
3
  import { PaginationCarousel } from "@uniai-fe/uds-primitives";
4
- import { PageFrameMobileHeader } from "../../../../page-frame/mobile/header";
4
+ import { MobileFrameHeader } from "../../../../page-frame/mobile/components/header";
5
5
 
6
6
  import "./stage-header.scss";
7
7
 
@@ -35,7 +35,7 @@ export function AuthStageHeader({
35
35
  }: StageHeaderProps) {
36
36
  return (
37
37
  <div className={clsx("auth-stage-header", className)}>
38
- <PageFrameMobileHeader
38
+ <MobileFrameHeader
39
39
  title={navigationTitle}
40
40
  backIcon={backIcon}
41
41
  onBack={onBack}
@@ -0,0 +1,13 @@
1
+ import { jotaiStorage } from "@uniai-fe/util-jotai";
2
+ import { atomWithStorage } from "jotai/utils";
3
+ import type { API_Res_LoginData } from "../types";
4
+
5
+ /**
6
+ * 로그인 사용자 정보 세션스토리지로 관리
7
+ * @state
8
+ */
9
+ export const userDataAtom = atomWithStorage<API_Res_LoginData | null>(
10
+ "userData",
11
+ null,
12
+ jotaiStorage.session<API_Res_LoginData | null>(),
13
+ );
@@ -0,0 +1,229 @@
1
+ import type { API_Res_Base } from "../../../types/api";
2
+
3
+ /**
4
+ * 로그인 API; 요청
5
+ * @route /auth/user/login
6
+ * @property {string} username - 사용자 ID
7
+ * @property {string} password - 비밀번호
8
+ */
9
+ export interface API_Req_Login {
10
+ /**
11
+ * 사용자 ID
12
+ */
13
+ username: string;
14
+ /**
15
+ * 비밀번호
16
+ */
17
+ password: string;
18
+ }
19
+
20
+ /**
21
+ * 로그인 API; 응답 - 유저 정보
22
+ * @route /auth/user/login
23
+ * @property {number} id - 사용자 인덱스
24
+ * @property {string} username - 사용자 아이디
25
+ * @property {string} full_name - 사용자 이름
26
+ * @property {number} gender - 사용자 성별
27
+ * - 0: 남
28
+ * - 1: 여
29
+ * @property {string} birth_date - 사용자 생년월일
30
+ * - yyyy-MM-dd
31
+ * @property {string} email - 사용자 이메일
32
+ * @property {number | string} phone_number - 사용자 전화번호
33
+ * @property {string} joined_at - 가입일
34
+ * - yyyy-MM-ddTHH:mm:ss
35
+ * @property {string | null} closed_at - 사용자 탈퇴일
36
+ * @property {number} use_sms - SMS 수신 여부
37
+ * - 0: 수신 안함
38
+ * - 1: 수신
39
+ * @property {number} user_type - 사용자 유형
40
+ * - 0: 일반 사용자
41
+ * ...
42
+ * - 99: 관리자
43
+ */
44
+ export interface API_Res_LoginUserInfo {
45
+ /**
46
+ * 사용자 인덱스
47
+ */
48
+ id: number;
49
+ /**
50
+ * 사용자 아이디
51
+ */
52
+ username: string;
53
+ /**
54
+ * 사용자 이름
55
+ */
56
+ full_name: string;
57
+ /**
58
+ * 사용자 성별
59
+ * - 0: 남
60
+ * - 1: 여
61
+ */
62
+ gender: number;
63
+ /**
64
+ * 사용자 생년월일
65
+ * - yyyy-MM-dd
66
+ */
67
+ birth_date: string;
68
+ /**
69
+ * 사용자 이메일
70
+ */
71
+ email: string;
72
+ /**
73
+ * 사용자 전화번호
74
+ */
75
+ phone_number: number | string;
76
+ /**
77
+ * 가입일
78
+ * - yyyy-MM-ddTHH:mm:ss
79
+ */
80
+ joined_at: string;
81
+ /**
82
+ * 사용자 탈퇴일
83
+ */
84
+ closed_at: string | null;
85
+ /**
86
+ * SMS 수신 여부
87
+ * - 0: 수신 안함
88
+ * - 1: 수신
89
+ */
90
+ use_sms: number;
91
+ /**
92
+ * 사용자 유형
93
+ * - 0: 일반 사용자
94
+ * ...
95
+ * - 99: 관리자
96
+ */
97
+ user_type: number;
98
+ }
99
+
100
+ /**
101
+ * 로그인 API; 응답 - 소속 그룹
102
+ * @route /auth/user/login
103
+ * @property {number} id - 그룹 인덱스
104
+ * @property {number} parent_id - 상위 그룹 인덱스
105
+ * @property {string} name - 그룹명
106
+ * @property {number} group_type - 그룹 유형
107
+ * - 0: 일반 그룹 (유니아이, 유통사 포함)
108
+ * - 1: 농장 그룹
109
+ * - 2: 기타 그룹
110
+ * @property {string} description - 그룹 설명
111
+ * @property {string} created_at - 그룹 생성일
112
+ * - yyyy-MM-ddTHH:mm:ss
113
+ */
114
+ export interface API_Res_LoginGroup {
115
+ /**
116
+ * 그룹 인덱스
117
+ */
118
+ id: number;
119
+ /**
120
+ * 상위 그룹 인덱스
121
+ */
122
+ parent_id: number;
123
+ /**
124
+ * 그룹명
125
+ */
126
+ name: string;
127
+ /**
128
+ * 그룹 유형
129
+ * - 0: 일반 그룹 (유니아이, 유통사 포함)
130
+ * - 1: 농장 그룹
131
+ * - 2: 기타 그룹
132
+ */
133
+ group_type: number;
134
+ /**
135
+ * 그룹 설명
136
+ */
137
+ description: string;
138
+ /**
139
+ * 그룹 생성일
140
+ * - yyyy-MM-ddTHH:mm:ss
141
+ */
142
+ created_at: string;
143
+ flag: number;
144
+ status: number;
145
+ }
146
+
147
+ /**
148
+ * 로그인 API; 응답 - 역할 정보
149
+ * @route /auth/user/login
150
+ * @property {number} id - 역할 인덱스
151
+ * @property {string} name - 역할 이름
152
+ * @property {string} description - 역할 설명
153
+ * @property {number} create_userid - 역할을 생성한 사용자 ID
154
+ * @property {number} group_id - 역할이 속한 그룹 ID
155
+ * @property {string} created_at - 역할 생성일
156
+ * - yyyy-MM-ddTHH:mm:ss
157
+ * @property {Array<unknown>} rules - 역할 권한 목록
158
+ */
159
+ export interface API_Res_LoginRole {
160
+ /**
161
+ * 역할 인덱스
162
+ */
163
+ id: number;
164
+ /**
165
+ * 역할 이름
166
+ */
167
+ name: string;
168
+ /**
169
+ * 역할 설명
170
+ */
171
+ description: string;
172
+ /**
173
+ * 역할을 생성한 사용자 ID
174
+ */
175
+ create_userid: number;
176
+ /**
177
+ * 역할이 속한 그룹 ID
178
+ */
179
+ group_id: number;
180
+ /**
181
+ * 역할 생성일
182
+ * - yyyy-MM-ddTHH:mm:ss
183
+ */
184
+ created_at: string;
185
+ rules: [];
186
+ }
187
+
188
+ /**
189
+ * 로그인 API; 응답 성공 데이터
190
+ * @route /auth/user/login
191
+ * @property {string} access_token - 엑세스 토큰
192
+ * @property {string} token_type - 엑세스 토큰 타입
193
+ * - bearer
194
+ * @property {number} expires_in - 토큰 유효기간
195
+ * - 초 단위
196
+ * @property {API_Res_LoginUserInfo} user_info - 로그인 유저 정보
197
+ * @property {API_Res_LoginGroup[]} groups - 소속 그룹 목록
198
+ * @property {API_Res_LoginGroup[]} ancestor_groups - 상위 그룹 목록
199
+ * @property {API_Res_LoginRole[]} roles - 역할 목록
200
+ */
201
+ export interface API_Res_LoginData {
202
+ /**
203
+ * 엑세스 토큰
204
+ */
205
+ access_token: string;
206
+ /**
207
+ * 엑세스 토큰 타입
208
+ * - bearer
209
+ */
210
+ token_type: string;
211
+ /**
212
+ * 토큰 유효기간
213
+ * - 초 단위
214
+ */
215
+ expires_in: number;
216
+ /**
217
+ * 로그인 유저 정보
218
+ */
219
+ user_info: API_Res_LoginUserInfo;
220
+ groups: API_Res_LoginGroup[];
221
+ ancestor_groups: API_Res_LoginGroup[];
222
+ roles: API_Res_LoginRole[];
223
+ }
224
+
225
+ /**
226
+ * 로그인 API; 응답
227
+ * @route /auth/user/login
228
+ */
229
+ export type API_Res_Login = API_Res_LoginData | API_Res_Base<null>;
@@ -0,0 +1 @@
1
+ export type {};
@@ -0,0 +1,4 @@
1
+ export type * from "./props";
2
+ export type * from "./hooks";
3
+ export type * from "./form";
4
+ export type * from "./api";
@@ -16,7 +16,7 @@ import {
16
16
  EmailInput,
17
17
  type EmailInputProps,
18
18
  } from "@uniai-fe/uds-primitives";
19
- import type { FormEvent } from "react";
19
+ import type { SubmitEvent } from "react";
20
20
  import type {
21
21
  AuthSignupAgreementOption,
22
22
  AuthSignupVerificationProps,
@@ -234,7 +234,8 @@ export function AuthSignupVerificationForm({
234
234
  const ctaDisabled =
235
235
  disabled || Boolean(submitDisabled) || !resolvedVerificationReady;
236
236
 
237
- const handleFormSubmit = (event: FormEvent<HTMLFormElement>) => {
237
+ // React 19에서는 onSubmit이 SubmitEvent를 요구하므로 이벤트 타입을 맞춰준다.
238
+ const handleFormSubmit = (event: SubmitEvent<HTMLFormElement>) => {
238
239
  formAttr?.onSubmit?.(event);
239
240
  if (event.defaultPrevented) {
240
241
  return;
@@ -0,0 +1,61 @@
1
+ import { useQuery, type UseQueryResult } from "@tanstack/react-query";
2
+ import type {
3
+ API_Req_CctvRtcToken,
4
+ API_Res_CctvCompanyGroup,
5
+ API_Res_CctvRtcToken,
6
+ } from "../types";
7
+ import { getQueryString } from "@uniai-fe/util-functions";
8
+
9
+ export const getCctvCompanyList = async ({
10
+ username,
11
+ url,
12
+ }: {
13
+ username: string;
14
+ url?: string;
15
+ }) =>
16
+ await (
17
+ await fetch(url ?? `/api/cctv/company-list${getQueryString(username)}`)
18
+ ).json();
19
+
20
+ export const useQueryCctvCompanyList = ({
21
+ username,
22
+ url,
23
+ }: {
24
+ username: string;
25
+ url?: string;
26
+ }): UseQueryResult<{ data: API_Res_CctvCompanyGroup[]; total_count: number }> =>
27
+ useQuery({
28
+ queryKey: ["cctv_company_list", username, url],
29
+ queryFn: () => getCctvCompanyList({ username, url }),
30
+ enabled: Boolean(username),
31
+ });
32
+
33
+ export const postCctvRtcToken = async ({
34
+ company_id,
35
+ cam_id,
36
+ username,
37
+ url,
38
+ }: API_Req_CctvRtcToken & { url?: string }): Promise<API_Res_CctvRtcToken> =>
39
+ await (
40
+ await fetch(url ?? `/api/cctv/token`, {
41
+ method: "POST",
42
+ headers: {
43
+ "Content-Type": "application/json",
44
+ },
45
+ body: JSON.stringify({ companyId: company_id, camId: cam_id, username }),
46
+ })
47
+ ).json();
48
+
49
+ export const useQueryCctvRtcToken = ({
50
+ company_id,
51
+ cam_id,
52
+ username,
53
+ url,
54
+ }: API_Req_CctvRtcToken & {
55
+ url?: string;
56
+ }): UseQueryResult<API_Res_CctvRtcToken> =>
57
+ useQuery({
58
+ queryKey: ["cctv_rtc_token", username, company_id, cam_id, url],
59
+ queryFn: () => postCctvRtcToken({ company_id, cam_id, username, url }),
60
+ enabled: Boolean(company_id && cam_id && username),
61
+ });
@@ -0,0 +1,2 @@
1
+ export * from "./server";
2
+ export * from "./client";
@@ -0,0 +1,188 @@
1
+ import {
2
+ generateBackendQueryUrl_GET,
3
+ nextAPILog,
4
+ } from "@uniai-fe/util-functions";
5
+ import type {
6
+ API_Req_GetCompanyListParams,
7
+ API_Res_CctvCompanyGroup,
8
+ } from "../types";
9
+
10
+ export const GROUP_PRESET: API_Res_CctvCompanyGroup[] = [
11
+ {
12
+ group_code: "C",
13
+ group_name: "사육",
14
+ list: [], //meat,
15
+ },
16
+ {
17
+ group_code: "PS-Growing",
18
+ group_name: "종계(육성)",
19
+ list: [], //breeder,
20
+ },
21
+ {
22
+ group_code: "PS-Laying",
23
+ group_name: "종계(산란)",
24
+ list: [], //laying,
25
+ },
26
+ {
27
+ group_code: "GPS-Growing",
28
+ group_name: "원종계(육성)",
29
+ list: [], //breeder,
30
+ },
31
+ {
32
+ group_code: "GPS-Laying",
33
+ group_name: "원종계(산란)",
34
+ list: [], //laying,
35
+ },
36
+ {
37
+ group_code: "PS-Hatching",
38
+ group_name: "부화",
39
+ list: [], //hatchery,
40
+ },
41
+ {
42
+ group_code: "FeedFactory",
43
+ group_name: "사료공장",
44
+ list: [], //feedFactory,
45
+ },
46
+ ];
47
+
48
+ export function getMatchedGroupList(
49
+ data: API_Res_CctvCompanyGroup[] = [],
50
+ { code, method }: { code: string; method: string },
51
+ ) {
52
+ if (typeof data === "undefined" || !Array.isArray(data)) return [];
53
+ return data
54
+ .filter(g => {
55
+ const groupCode = g.group_code;
56
+ if (!method || method === "includes") return groupCode.includes(code);
57
+ if (method === "startsWith") return groupCode.startsWith(code);
58
+ if (method === "endsWith") return groupCode.endsWith(code);
59
+ return groupCode === code;
60
+ })
61
+ .flatMap(g => g.list);
62
+ }
63
+
64
+ export function classifyGroups(
65
+ data: API_Res_CctvCompanyGroup[] = [],
66
+ ): API_Res_CctvCompanyGroup[] {
67
+ // 사육(육계)
68
+ const meatGroupList = getMatchedGroupList(data, {
69
+ code: "C-Growing",
70
+ method: "startsWith",
71
+ });
72
+ // 종계 - 육성
73
+ const PS_breederGroupList = getMatchedGroupList(data, {
74
+ code: "PS-Growing",
75
+ method: "startsWith",
76
+ });
77
+ // 종계 - 산란
78
+ const PS_layingGroupList = getMatchedGroupList(data, {
79
+ code: "PS-Laying",
80
+ method: "startsWith",
81
+ });
82
+ // 원종계 - 육성
83
+ const GPS_breederGroupList = getMatchedGroupList(data, {
84
+ code: "GPS-Growing",
85
+ method: "startsWith",
86
+ });
87
+ // 원종계 - 산란
88
+ const GPS_layingGroupList = getMatchedGroupList(data, {
89
+ code: "GPS-Laying",
90
+ method: "startsWith",
91
+ });
92
+ // 부화
93
+ const hatcheryGroupList = getMatchedGroupList(data, {
94
+ code: "Hatching",
95
+ method: "includes",
96
+ });
97
+ // 사료공장
98
+ const feedFactoryGroupList = getMatchedGroupList(data, {
99
+ code: "FeedFactory",
100
+ method: "includes",
101
+ });
102
+
103
+ const res = GROUP_PRESET.map(g => {
104
+ switch (g.group_code) {
105
+ case "C":
106
+ return { ...g, list: meatGroupList };
107
+ case "PS-Growing":
108
+ return { ...g, list: PS_breederGroupList };
109
+ case "PS-Laying":
110
+ return { ...g, list: PS_layingGroupList };
111
+ case "GPS-Growing":
112
+ return { ...g, list: GPS_breederGroupList };
113
+ case "GPS-Laying":
114
+ return { ...g, list: GPS_layingGroupList };
115
+ case "PS-Hatching":
116
+ return { ...g, list: hatcheryGroupList };
117
+ case "FeedFactory":
118
+ return { ...g, list: feedFactoryGroupList };
119
+ default:
120
+ return { ...g, list: [] };
121
+ }
122
+ });
123
+
124
+ return res;
125
+ }
126
+
127
+ /**
128
+ * CCTV; API route.ts 로직
129
+ * @api
130
+ * @route /v1/users/{username}/cctvs
131
+ * @param {API_Req_GetCompanyListParams} params
132
+ * @property {string} domain API 요청 도메인(서버)
133
+ * @property {string} routeUrl Next.js app/api 이하 경로 url
134
+ * @property {string} [queryUrl] 백엔드 요청 url
135
+ * @property {URLSearchParams | object} searchParams 요청 searchParams
136
+ */
137
+ export async function getCompanyList({
138
+ domain,
139
+ routeUrl,
140
+ queryUrl,
141
+ searchParams,
142
+ }: API_Req_GetCompanyListParams): Promise<{
143
+ res: API_Res_CctvCompanyGroup[];
144
+ domain: string;
145
+ queryUrl: string;
146
+ options?: ResponseInit;
147
+ }> {
148
+ const username = searchParams.get("username") || "";
149
+ const query_url = queryUrl || `/v1/users/${username}/cctvs`;
150
+
151
+ const API_OPTION = {
152
+ domain,
153
+ queryUrl: query_url,
154
+ // searchParams,
155
+ };
156
+
157
+ if (!queryUrl && !username)
158
+ return {
159
+ res: [],
160
+ ...API_OPTION,
161
+ options: { status: 400, statusText: "유저 아이디를 확인할 수 없습니다." },
162
+ };
163
+
164
+ // 요청 URL 구성
165
+ const url = generateBackendQueryUrl_GET({
166
+ routeUrl,
167
+ ...API_OPTION,
168
+ });
169
+
170
+ try {
171
+ const originRes: { data: API_Res_CctvCompanyGroup[] } = await (
172
+ await fetch(url)
173
+ ).json();
174
+
175
+ // 응답 그룹/농장
176
+ const resGroups = originRes?.data || [];
177
+ const res = classifyGroups(resGroups);
178
+
179
+ return { res, ...API_OPTION };
180
+ } catch (err) {
181
+ nextAPILog("GET", routeUrl, url, { err });
182
+ return {
183
+ res: [],
184
+ ...API_OPTION,
185
+ options: { status: 500 },
186
+ };
187
+ }
188
+ }