@uniai-fe/uds-templates 0.4.1 → 0.4.3

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 (40) hide show
  1. package/README.md +23 -0
  2. package/dist/styles.css +129 -0
  3. package/package.json +5 -5
  4. package/src/auth/common/container/index.scss +2 -0
  5. package/src/auth/common/container/index.tsx +0 -1
  6. package/src/auth/find-id/markup/StepComplete.tsx +1 -1
  7. package/src/auth/find-password/index.scss +2 -0
  8. package/src/auth/find-password/markup/StepComplete.tsx +1 -1
  9. package/src/auth/index.scss +7 -0
  10. package/src/auth/index.tsx +1 -1
  11. package/src/auth/login/types/api.ts +65 -0
  12. package/src/cctv/index.tsx +0 -2
  13. package/src/index.scss +3 -8
  14. package/src/index.tsx +4 -0
  15. package/src/modal/index.tsx +0 -2
  16. package/src/page-frame/desktop/components/header/util/setting/Button.tsx +3 -2
  17. package/src/page-frame/desktop/index.tsx +0 -2
  18. package/src/page-frame/index.scss +2 -0
  19. package/src/page-frame/index.tsx +2 -0
  20. package/src/page-frame/mobile/components/header/Header.tsx +0 -1
  21. package/src/page-frame/mobile/components/header/index.ts +2 -0
  22. package/src/page-frame/mobile/index.tsx +0 -2
  23. package/src/service-inquiry/components/Form.tsx +157 -0
  24. package/src/service-inquiry/components/OpenButton.tsx +46 -0
  25. package/src/service-inquiry/hooks/index.ts +2 -0
  26. package/src/service-inquiry/hooks/useOpenServiceInquiry.ts +48 -0
  27. package/src/service-inquiry/hooks/useServiceInquiryUserContext.ts +89 -0
  28. package/src/service-inquiry/index.scss +1 -0
  29. package/src/service-inquiry/index.tsx +26 -0
  30. package/src/service-inquiry/styles/form.scss +15 -0
  31. package/src/service-inquiry/styles/index.scss +2 -0
  32. package/src/service-inquiry/styles/open-button.scss +15 -0
  33. package/src/service-inquiry/types/api.ts +68 -0
  34. package/src/service-inquiry/types/form-context.ts +48 -0
  35. package/src/service-inquiry/types/hooks.ts +83 -0
  36. package/src/service-inquiry/types/index.ts +4 -0
  37. package/src/service-inquiry/types/props.ts +171 -0
  38. package/src/service-inquiry/utils/modal-option.tsx +38 -0
  39. package/src/weather/components/icon/Weather.tsx +4 -0
  40. package/src/weather/index.tsx +2 -2
package/README.md CHANGED
@@ -55,6 +55,15 @@ Next.js 서비스에서 primitives와 동일한 방식으로 **Raw TypeScript**
55
55
  - `weatherCoordinate`
56
56
  - `useWeatherKorea`
57
57
  - `useOpenWeatherMap`
58
+ - `/service-inquiry`
59
+ - `ServiceInquiry.Form`
60
+ - `ServiceInquiry.OpenButton`
61
+ - `ServiceInquiry.useOpen`
62
+ - `ServiceInquiry.useUserContext`
63
+ - `ServiceInquiry.createModal`
64
+ - `ServiceInquiryFieldMode`
65
+ - `ServiceInquiryFormProps`
66
+ - `ServiceInquiryFormValues`
58
67
  - `/cctv`
59
68
  - `CCTV.Provider`
60
69
  - `CCTV.CamList.Container`
@@ -95,6 +104,10 @@ Next.js 서비스에서 primitives와 동일한 방식으로 **Raw TypeScript**
95
104
  - `/modal/**`
96
105
  - ui-legacy 스택 기반 모달 Provider/Root/Container + 템플릿(`Modal.Alert`, `Modal.Dialog`)
97
106
  - Storybook(`apps/design-storybook/src/stories/templates/modal`)에서 Alert/Confirm 케이스를 검증한다.
107
+ - `/service-inquiry/**`
108
+ - 문의 입력 전용 form, 기본 원형 `?` open button, 커스텀 trigger용 open hook, request context 조립 hook, modal preset factory를 제공한다.
109
+ - submit transport, React Query mutation, Next.js route handler, 에러 피드백(`Modal.Alert`)은 서비스 앱이 소유한다.
110
+ - 로그인 후 `farm_name`, `contact`를 auto-fill + readonly로 보여야 할 때는 `formContextOptions.defaultValues`와 `farmNameField.mode`, `contactField.mode`를 함께 전달한다.
98
111
  - `/weather/**`
99
112
  - page-frame header utility에 결합되는 weather header 템플릿과 weather data hook/mock 도구를 제공한다.
100
113
  - `/cctv/**`
@@ -104,6 +117,16 @@ Next.js 서비스에서 primitives와 동일한 방식으로 **Raw TypeScript**
104
117
 
105
118
  각 템플릿의 상세한 범위와 의사결정은 `CONTEXT-*.md` 문서에서 관리합니다.
106
119
 
120
+ ### Service Inquiry 도입 흐름
121
+
122
+ 1. 서비스 앱이 `react-hook-form`으로 `defaultValues`와 `onSubmit`을 준비한다.
123
+ 2. 필요하면 `ServiceInquiry.useUserContext`로 `labels`, `page_path`, 추가 `user_context`를 조립한다.
124
+ 3. 기본 버튼은 `ServiceInquiry.OpenButton`, 커스텀 버튼은 `ServiceInquiry.useOpen`으로 모달 open을 연결한다.
125
+ 4. modal footer confirm이 `ServiceInquiry.Form` submit 진입을 담당한다.
126
+ 5. 실제 submit은 서비스 앱의 `useMutation + Next.js route handler + Modal.Alert` 조합으로 처리한다.
127
+
128
+ `service-inquiry`는 구조와 request context까지만 제공하고, 네트워크 상태/재시도/성공·실패 피드백은 서비스 앱이 소유합니다.
129
+
107
130
  ### 회원가입 Step 구조
108
131
 
109
132
  1. **Step1 — User Info (name + phone)**: 기본 정보 입력. PhoneInput은 인증 UI 없이 마스킹만 제공한다.
package/dist/styles.css CHANGED
@@ -746,6 +746,46 @@
746
746
  transition: none;
747
747
  }
748
748
  }
749
+ .auth-stage-header {
750
+ display: flex;
751
+ flex-direction: column;
752
+ gap: var(--spacing-padding-5, 20px);
753
+ padding: 0 var(--spacing-padding-1, 4px);
754
+ }
755
+
756
+ .auth-stage-step {
757
+ display: flex;
758
+ justify-content: flex-start;
759
+ }
760
+
761
+ .auth-stage-step-pagination {
762
+ --pagination-carousel-gap: 6px;
763
+ }
764
+
765
+ .auth-stage-headline {
766
+ display: flex;
767
+ flex-direction: column;
768
+ gap: var(--spacing-padding-1, 4px);
769
+ }
770
+
771
+ .auth-stage-headline-text {
772
+ margin: 0;
773
+ font-size: 24px;
774
+ font-weight: 600;
775
+ line-height: 1.4;
776
+ letter-spacing: 0.2px;
777
+ color: var(--color-label-standard);
778
+ font-family: "Pretendard JP Variable", "Pretendard", system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
779
+ }
780
+
781
+ .auth-stage-headline-description {
782
+ margin: 0;
783
+ font-size: 14px;
784
+ line-height: 1.4;
785
+ color: var(--color-label-assistive);
786
+ font-family: "Pretendard", system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
787
+ }
788
+
749
789
 
750
790
 
751
791
  .auth-container {
@@ -826,6 +866,10 @@
826
866
  margin-top: var(--spacing-padding-4, 16px);
827
867
  }
828
868
 
869
+ .auth-code-field {
870
+ margin-top: var(--spacing-gap-6);
871
+ }
872
+
829
873
  .auth-login-form {
830
874
  display: flex;
831
875
  flex-direction: column;
@@ -1067,6 +1111,59 @@
1067
1111
  margin: 0;
1068
1112
  }
1069
1113
 
1114
+ .password-confirm-field {
1115
+ margin-top: var(--spacing-gap-6);
1116
+ }
1117
+
1118
+ .auth-password-helper {
1119
+ display: flex;
1120
+ flex-wrap: wrap;
1121
+ align-items: center;
1122
+ gap: var(--spacing-gap-3);
1123
+ margin-top: var(--spacing-gap-2);
1124
+ }
1125
+
1126
+ .auth-password-helper-column {
1127
+ flex-direction: column;
1128
+ align-items: flex-start;
1129
+ gap: var(--spacing-gap-1);
1130
+ }
1131
+
1132
+ .auth-set-password-icon {
1133
+ display: flex;
1134
+ align-items: center;
1135
+ justify-content: center;
1136
+ font-size: 0;
1137
+ }
1138
+ .auth-set-password-icon svg path {
1139
+ fill: var(--color-label-assistive);
1140
+ }
1141
+
1142
+ .auth-password-helper-text {
1143
+ font-size: var(--font-label-small-size);
1144
+ line-height: var(--font-label-small-line-height);
1145
+ font-weight: var(--font-label-small-weight);
1146
+ color: var(--color-label-assistive);
1147
+ }
1148
+
1149
+ .auth-password-helper-item {
1150
+ display: flex;
1151
+ align-items: center;
1152
+ gap: var(--spacing-gap-1);
1153
+ }
1154
+ .auth-password-helper-item[data-state=complete] .auth-password-helper-text {
1155
+ color: var(--color-success);
1156
+ }
1157
+ .auth-password-helper-item[data-state=complete] .auth-set-password-icon svg path {
1158
+ fill: var(--color-success);
1159
+ }
1160
+ .auth-password-helper-item[data-state=error] .auth-password-helper-text {
1161
+ color: var(--color-error);
1162
+ }
1163
+ .auth-password-helper-item[data-state=error] .auth-set-password-icon svg path {
1164
+ fill: var(--color-error);
1165
+ }
1166
+
1070
1167
  .auth-signup-form {
1071
1168
  display: flex;
1072
1169
  flex-direction: column;
@@ -1635,3 +1732,35 @@
1635
1732
  .cctv-viewer-desktop-pagination-container {
1636
1733
  margin-top: var(--spacing-gap-8);
1637
1734
  }
1735
+
1736
+ .service-inquiry-form {
1737
+ display: flex;
1738
+ flex-direction: column;
1739
+ }
1740
+
1741
+ .service-inquiry-fields {
1742
+ display: flex;
1743
+ flex-direction: column;
1744
+ gap: var(--spacing-padding-5);
1745
+ }
1746
+
1747
+ .service-inquiry-field {
1748
+ display: flex;
1749
+ flex-direction: column;
1750
+ }
1751
+
1752
+ .service-inquiry-open-button {
1753
+ width: 40px;
1754
+ height: 40px;
1755
+ border-radius: 20px;
1756
+ border: 1px solid var(--color-border-standard-cool-gray);
1757
+ background: var(--color_100);
1758
+ color: var(--color-label-neutral);
1759
+ font-size: 24px;
1760
+ font-weight: 400;
1761
+ display: flex;
1762
+ align-items: center;
1763
+ justify-content: center;
1764
+ padding: 0;
1765
+ cursor: pointer;
1766
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniai-fe/uds-templates",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "UNIAI Design System; UI Templates Package",
5
5
  "type": "module",
6
6
  "private": false,
@@ -12,7 +12,7 @@
12
12
  "publishConfig": {
13
13
  "access": "public"
14
14
  },
15
- "packageManager": "pnpm@10.32.1",
15
+ "packageManager": "pnpm@10.33.0",
16
16
  "engines": {
17
17
  "node": ">=24",
18
18
  "pnpm": ">=10"
@@ -70,7 +70,7 @@
70
70
  },
71
71
  "devDependencies": {
72
72
  "@svgr/webpack": "^8.1.0",
73
- "@tanstack/react-query": "^5.91.2",
73
+ "@tanstack/react-query": "^5.95.2",
74
74
  "@types/node": "^24.10.2",
75
75
  "@types/react": "^19.2.14",
76
76
  "@types/react-dom": "^19.2.3",
@@ -86,10 +86,10 @@
86
86
  "@uniai-fe/util-next": "workspace:*",
87
87
  "@uniai-fe/util-rtc": "workspace:*",
88
88
  "eslint": "^9.39.2",
89
- "jotai": "^2.18.1",
89
+ "jotai": "^2.19.0",
90
90
  "next": "^15.5.11",
91
91
  "prettier": "^3.8.1",
92
- "react-hook-form": "^7.71.2",
92
+ "react-hook-form": "^7.72.0",
93
93
  "sass": "^1.98.0",
94
94
  "typescript": "~5.9.3"
95
95
  }
@@ -1,3 +1,5 @@
1
+ @use "./header/stage-header.scss";
2
+
1
3
  :root {
2
4
  --auth-container-max-width: 335px;
3
5
  --auth-container-gap: var(--spacing-padding-7, 28px);
@@ -1,5 +1,4 @@
1
1
  import "./index.scss";
2
- import "./header/stage-header.scss";
3
2
 
4
3
  export { AuthStageHeader } from "./header";
5
4
  export { AuthContainer } from "./AuthContainer";
@@ -24,7 +24,7 @@ export default function FindIdStepComplete({
24
24
  title={title ?? "아이디 찾기 완료"}
25
25
  description={description ?? "로그인 페이지로 이동하여 로그인합니다."}
26
26
  cta={{
27
- label: cta?.label ?? "로그인하기",
27
+ label: cta?.label ?? "확인",
28
28
  buttonProps: cta?.buttonProps,
29
29
  }}
30
30
  >
@@ -1 +1,3 @@
1
1
  @use "../common/find/styles/find-account.scss";
2
+ // StepReset password helper는 외부 앱의 `@uniai-fe/uds-templates/css` 엔트리에도 포함돼야 한다.
3
+ @use "../common/password/styles/set-password.scss";
@@ -20,7 +20,7 @@ export default function FindPasswordStepComplete({
20
20
  title={title ?? "비밀번호 변경 완료"}
21
21
  description={description ?? "로그인 페이지로 이동하여 로그인해 주세요."}
22
22
  cta={{
23
- label: cta?.label ?? "로그인하기",
23
+ label: cta?.label ?? "확인",
24
24
  buttonProps: cta?.buttonProps,
25
25
  }}
26
26
  />
@@ -0,0 +1,7 @@
1
+ @use "./common/container/index.scss" as authCommonContainer;
2
+ @use "./common/complete/index.scss" as authCommonComplete;
3
+ @use "./common/auth-code/styles/index.scss" as authCommonAuthCode;
4
+ @use "./login/index.scss" as authLogin;
5
+ @use "./find-id/index.scss" as authFindId;
6
+ @use "./find-password/index.scss" as authFindPassword;
7
+ @use "./signup/styles/signup.scss" as authSignup;
@@ -1,4 +1,4 @@
1
- import "./login/index.scss";
1
+ import "./index.scss";
2
2
 
3
3
  import {
4
4
  AuthContainer,
@@ -51,6 +51,58 @@ export interface API_Res_LoginUserFarm {
51
51
  user_access_role: string;
52
52
  }
53
53
 
54
+ /**
55
+ * 로그인 API; 응답 - 소속 부서 정보
56
+ * @route /auth/user/login
57
+ * @property {number} org_id 소속 조직 인덱스
58
+ * @property {string} org_name 소속 조직명
59
+ * @property {string} org_type 소속 조직 유형 코드
60
+ * @property {number} parent_org_id 상위 조직 인덱스
61
+ * @property {string} parent_org_name 상위 조직명
62
+ * @property {number} root_org_id 최상위 조직 인덱스
63
+ * @property {string} root_org_name 최상위 조직명
64
+ * @property {number} position_id 직책 인덱스
65
+ * @property {string} position_name 직책명
66
+ */
67
+ export interface API_Res_LoginUserAuth {
68
+ /**
69
+ * 소속 조직 인덱스
70
+ */
71
+ org_id: number;
72
+ /**
73
+ * 소속 조직명
74
+ */
75
+ org_name: string;
76
+ /**
77
+ * 소속 조직 유형 코드
78
+ */
79
+ org_type: string;
80
+ /**
81
+ * 상위 조직 인덱스
82
+ */
83
+ parent_org_id: number;
84
+ /**
85
+ * 상위 조직명
86
+ */
87
+ parent_org_name: string;
88
+ /**
89
+ * 최상위 조직 인덱스
90
+ */
91
+ root_org_id: number;
92
+ /**
93
+ * 최상위 조직명
94
+ */
95
+ root_org_name: string;
96
+ /**
97
+ * 직책 인덱스
98
+ */
99
+ position_id: number;
100
+ /**
101
+ * 직책명
102
+ */
103
+ position_name: string;
104
+ }
105
+
54
106
  /**
55
107
  * 로그인 API; 응답 - 유저 정보
56
108
  * @route /auth/user/login
@@ -120,6 +172,19 @@ export interface API_Res_LoginUserInfo {
120
172
  * @property {string} user_access_role 계정 권한/직책 코드 (OWNER: 농장주, EMPLOYEE: 농장직원, REGIONAL_MANAGER: 지역소장)
121
173
  */
122
174
  user_farms: API_Res_LoginUserFarm[];
175
+ /**
176
+ * (avic 전용) 소속부서 정보
177
+ * @property {number} org_id 소속 조직 인덱스
178
+ * @property {string} org_name 소속 조직명
179
+ * @property {string} org_type 소속 조직 유형 코드
180
+ * @property {number} parent_org_id 상위 조직 인덱스
181
+ * @property {string} parent_org_name 상위 조직명
182
+ * @property {number} root_org_id 최상위 조직 인덱스
183
+ * @property {string} root_org_name 최상위 조직명
184
+ * @property {number} position_id 직책 인덱스
185
+ * @property {string} position_name 직책명
186
+ */
187
+ avic_organizations: API_Res_LoginUserAuth[];
123
188
  /**
124
189
  * 가입일 (UTC)
125
190
  * - yyyy-MM-ddTHH:mm:ss
@@ -1,5 +1,3 @@
1
- import "./index.scss";
2
-
3
1
  /**
4
2
  * CCTV; viewer, cam-list, pagination, api/hook/jotai 도구를 함께 제공하는 엔트리
5
3
  */
package/src/index.scss CHANGED
@@ -3,15 +3,10 @@
3
3
 
4
4
  /* 템플릿 레벨 스타일을 통합해 서비스 앱이 단일 엔트리만 import하도록 구성한다. */
5
5
  /* namespace 충돌을 막기 위해 모든 @use 경로에 고유 alias를 부여한다. */
6
- @use "./page-frame/desktop/index.scss" as pageFrameDesktop;
7
- @use "./page-frame/mobile/index.scss" as pageFrameMobile;
6
+ @use "./page-frame/index.scss" as pageFrameStyles;
8
7
  @use "./modal/index.scss" as modalStyles;
9
8
 
10
- @use "./auth/common/container/index.scss" as authCommonContainer;
11
- @use "./auth/common/complete/index.scss" as authCommonComplete;
12
- @use "./auth/login/index.scss" as authLogin;
13
- @use "./auth/find-id/index.scss" as authFindId;
14
- @use "./auth/find-password/index.scss" as authFindPassword;
15
- @use "./auth/signup/styles/signup.scss" as authSignup;
9
+ @use "./auth/index.scss" as authStyles;
16
10
  @use "./weather/index.scss" as weatherStyles;
17
11
  @use "./cctv/index.scss" as cctvStyles;
12
+ @use "./service-inquiry/index.scss" as serviceInquiryStyles;
package/src/index.tsx CHANGED
@@ -1,7 +1,11 @@
1
+ import "./modal/index.scss";
2
+ import "./cctv/index.scss";
3
+
1
4
  export * from "./page-frame";
2
5
  export * from "./auth";
3
6
  export * from "./modal";
4
7
  export * from "./weather";
5
8
  export * from "./cctv";
9
+ export * from "./service-inquiry";
6
10
 
7
11
  export type * from "./types";
@@ -1,5 +1,3 @@
1
- import "./index.scss";
2
-
3
1
  import { modalStackAtom } from "./jotai/atoms";
4
2
  import {
5
3
  ModalNamespace,
@@ -64,8 +64,9 @@ export default function PageHeaderSettingButton({
64
64
  }, [menuItems]);
65
65
 
66
66
  const isMatchRouteGroup = useMemo(
67
- () => pathname.startsWith(commonRoute),
68
- [pathname, commonRoute],
67
+ // Storybook의 next/navigation mock에서는 pathname이 null일 수 있어 fallback 경로를 재사용한다.
68
+ () => resolvedPath.startsWith(commonRoute),
69
+ [resolvedPath, commonRoute],
69
70
  );
70
71
 
71
72
  const dropdownItems: DropdownTemplateItem[] = useMemo(
@@ -1,5 +1,3 @@
1
- import "./index.scss";
2
-
3
1
  import Frame from "./components";
4
2
 
5
3
  /**
@@ -0,0 +1,2 @@
1
+ @use "./desktop/index.scss" as pageFrameDesktop;
2
+ @use "./mobile/index.scss" as pageFrameMobile;
@@ -1,3 +1,5 @@
1
+ import "./index.scss";
2
+
1
3
  import { DesktopFrame } from "./desktop";
2
4
  import { MobileFrame } from "./mobile";
3
5
 
@@ -2,7 +2,6 @@ import clsx from "clsx";
2
2
  import type { ReactNode } from "react";
3
3
 
4
4
  import IconBackward from "../../img/chevron-backward.svg";
5
- import "../../styles/header.scss";
6
5
 
7
6
  export interface MobileFrameHeaderProps {
8
7
  className?: string;
@@ -1,3 +1,5 @@
1
+ // MobileFrameHeader direct barrel 소비에서도 header 스타일을 함께 로드한다.
2
+ import "../../styles/header.scss";
1
3
  import { MobileFrameHeader } from "./Header";
2
4
 
3
5
  const Header = {
@@ -1,5 +1,3 @@
1
- import "./index.scss";
2
-
3
1
  import { MobileFrame } from "./components/page/Frame";
4
2
 
5
3
  /**
@@ -0,0 +1,157 @@
1
+ "use client";
2
+
3
+ import clsx from "clsx";
4
+ import { Form, Input } from "@uniai-fe/uds-primitives";
5
+ import { useFormContext } from "react-hook-form";
6
+ import type { ServiceInquiryFieldKey, ServiceInquiryFormProps } from "../types";
7
+ import type { ServiceInquiryFormValues } from "../types";
8
+
9
+ /**
10
+ * Service Inquiry Form; 문의 입력 form
11
+ * @component
12
+ * @param {ServiceInquiryFormProps} props 문의 form props
13
+ * @param {string} [props.className] form className
14
+ * @param {ServiceInquiryFieldKey[]} [props.visibleFields] 노출 필드 목록
15
+ * @param {ServiceInquiryInputFieldProps} [props.farmNameField] 농장명 필드 설정
16
+ * @param {ServiceInquiryInputFieldProps} [props.contactField] 연락처 필드 설정
17
+ * @param {ServiceInquiryTextAreaFieldProps} [props.textField] 문의 본문 필드 설정
18
+ * @param {SubmitHandler<ServiceInquiryFormValues>} props.onSubmit 문의 제출 핸들러
19
+ * @example
20
+ * <ServiceInquiryForm onSubmit={values => console.info(values)} />
21
+ */
22
+ const ServiceInquiryForm = ({
23
+ className,
24
+ visibleFields,
25
+ farmNameField,
26
+ contactField,
27
+ textField,
28
+ onSubmit,
29
+ }: ServiceInquiryFormProps) => {
30
+ const form = useFormContext<ServiceInquiryFormValues>();
31
+
32
+ // 변경 설명: Modal.Dialog confirm 기본 submit과 연결되도록 form.handleSubmit 결과를 그대로 onSubmit에 바인딩한다.
33
+ const handleSubmit = form.handleSubmit(onSubmit);
34
+
35
+ return (
36
+ <form
37
+ className={clsx("service-inquiry-form", className)}
38
+ onSubmit={handleSubmit}
39
+ >
40
+ <div className="service-inquiry-fields">
41
+ {(visibleFields?.includes("farm_name") ?? true) ? (
42
+ <Form.Field.Template
43
+ className={clsx(
44
+ "service-inquiry-field",
45
+ "service-inquiry-field-farm-name",
46
+ farmNameField?.className,
47
+ )}
48
+ width={farmNameField?.inputProps?.width ?? "full"}
49
+ headerProps={{
50
+ required: farmNameField?.required,
51
+ ...(typeof farmNameField?.label === "string"
52
+ ? { label: farmNameField.label }
53
+ : typeof farmNameField?.label === "number"
54
+ ? { label: String(farmNameField.label) }
55
+ : {
56
+ labelJsx:
57
+ typeof farmNameField?.label === "undefined"
58
+ ? "농장명"
59
+ : farmNameField.label,
60
+ }),
61
+ }}
62
+ footer={farmNameField?.helper}
63
+ >
64
+ <Input.Base
65
+ type="text"
66
+ block={farmNameField?.inputProps?.block ?? true}
67
+ readOnly={
68
+ farmNameField?.mode === "readonly" ||
69
+ farmNameField?.inputProps?.readOnly === true
70
+ }
71
+ placeholder={farmNameField?.placeholder ?? "농장명 입력"}
72
+ {...farmNameField?.inputProps}
73
+ register={form.register("farm_name")}
74
+ />
75
+ </Form.Field.Template>
76
+ ) : null}
77
+
78
+ {(visibleFields?.includes("contact") ?? true) ? (
79
+ <Form.Field.Template
80
+ className={clsx(
81
+ "service-inquiry-field",
82
+ "service-inquiry-field-contact",
83
+ contactField?.className,
84
+ )}
85
+ width={contactField?.inputProps?.width ?? "full"}
86
+ headerProps={{
87
+ required: contactField?.required ?? true,
88
+ ...(typeof contactField?.label === "string"
89
+ ? { label: contactField.label }
90
+ : typeof contactField?.label === "number"
91
+ ? { label: String(contactField.label) }
92
+ : {
93
+ labelJsx:
94
+ typeof contactField?.label === "undefined"
95
+ ? "연락처"
96
+ : contactField.label,
97
+ }),
98
+ }}
99
+ footer={contactField?.helper}
100
+ >
101
+ <Input.Base
102
+ type="text"
103
+ block={contactField?.inputProps?.block ?? true}
104
+ readOnly={
105
+ contactField?.mode === "readonly" ||
106
+ contactField?.inputProps?.readOnly === true
107
+ }
108
+ placeholder={
109
+ contactField?.placeholder ?? "이메일 또는 전화번호 입력"
110
+ }
111
+ {...contactField?.inputProps}
112
+ register={form.register("contact")}
113
+ />
114
+ </Form.Field.Template>
115
+ ) : null}
116
+
117
+ {(visibleFields?.includes("text") ?? true) ? (
118
+ <Form.Field.Template
119
+ className={clsx(
120
+ "service-inquiry-field",
121
+ "service-inquiry-field-text",
122
+ textField?.className,
123
+ )}
124
+ width={textField?.inputProps?.width ?? "full"}
125
+ headerProps={{
126
+ required: textField?.required ?? true,
127
+ ...(typeof textField?.label === "string"
128
+ ? { label: textField.label }
129
+ : typeof textField?.label === "number"
130
+ ? { label: String(textField.label) }
131
+ : {
132
+ labelJsx:
133
+ typeof textField?.label === "undefined"
134
+ ? "문의 내용"
135
+ : textField.label,
136
+ }),
137
+ }}
138
+ footer={textField?.helper}
139
+ >
140
+ <Input.TextArea
141
+ block={textField?.inputProps?.block ?? true}
142
+ placeholder={
143
+ textField?.placeholder ?? "문의 내용을 자세히 입력해 주세요."
144
+ }
145
+ height={textField?.inputProps?.height ?? 160}
146
+ length={textField?.inputProps?.length ?? 10000}
147
+ {...textField?.inputProps}
148
+ register={form.register("text")}
149
+ />
150
+ </Form.Field.Template>
151
+ ) : null}
152
+ </div>
153
+ </form>
154
+ );
155
+ };
156
+
157
+ export default ServiceInquiryForm;
@@ -0,0 +1,46 @@
1
+ "use client";
2
+
3
+ import { useOpenServiceInquiry } from "../hooks";
4
+ import type { ServiceInquiryOpenButtonProps } from "../types";
5
+
6
+ /**
7
+ * Service Inquiry Open Button; 문의 모달 trigger adapter
8
+ * @component
9
+ * @param {ServiceInquiryOpenButtonProps} props 문의 모달 열기 props
10
+ * @param {string} props.stackKey modal stack key
11
+ * @param {ServiceInquiryFormProps} props.formProps 문의 form props
12
+ * @param {Partial<DialogTemplateOptions<ServiceInquiryFormValues>>} [props.dialogOptions] 문의 모달 preset 위에 덮어쓸 dialog option
13
+ * @param {() => void} [props.onOpen] 모달 open 직전 콜백
14
+ * @example
15
+ * <ServiceInquiryOpenButton
16
+ * stackKey="sample"
17
+ * formProps={{ onSubmit: values => console.info(values) }}
18
+ * />
19
+ */
20
+ const ServiceInquiryOpenButton = ({
21
+ stackKey,
22
+ formProps,
23
+ dialogOptions,
24
+ onOpen,
25
+ }: ServiceInquiryOpenButtonProps) => {
26
+ const { openServiceInquiry } = useOpenServiceInquiry({
27
+ stackKey,
28
+ formProps,
29
+ dialogOptions,
30
+ onOpen,
31
+ });
32
+
33
+ return (
34
+ <button
35
+ type="button"
36
+ className="service-inquiry-open-button"
37
+ aria-label="문의하기"
38
+ // 변경 설명: 기본 제공 버튼은 고정 원형 `?` 버튼 사양으로 렌더링한다.
39
+ onClick={openServiceInquiry}
40
+ >
41
+ ?
42
+ </button>
43
+ );
44
+ };
45
+
46
+ export default ServiceInquiryOpenButton;
@@ -0,0 +1,2 @@
1
+ export * from "./useOpenServiceInquiry";
2
+ export * from "./useServiceInquiryUserContext";