@uniai-fe/uds-templates 0.4.10 → 0.4.12

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/README.md CHANGED
@@ -58,6 +58,7 @@ Next.js 서비스에서 primitives와 동일한 방식으로 **Raw TypeScript**
58
58
  - `/service-inquiry`
59
59
  - `ServiceInquiry.Form`
60
60
  - `ServiceInquiry.OpenButton`
61
+ - `ServiceInquiry.NavButton`
61
62
  - `ServiceInquiry.useOpen`
62
63
  - `ServiceInquiry.useProvideContext`
63
64
  - `ServiceInquiry.useUserContext`
@@ -107,9 +108,12 @@ Next.js 서비스에서 primitives와 동일한 방식으로 **Raw TypeScript**
107
108
  - ui-legacy 스택 기반 모달 Provider/Root/Container + 템플릿(`Modal.Alert`, `Modal.Dialog`)
108
109
  - Storybook(`apps/design-storybook/src/stories/templates/modal`)에서 Alert/Confirm 케이스를 검증한다.
109
110
  - `/service-inquiry/**`
110
- - 문의 입력 전용 form, 기본 원형 `?` open button, 커스텀 trigger용 open hook, 페이지 context 등록 hook, request context 조립 hook, 네트워크 오류 수집 hook, modal preset factory를 제공한다.
111
+ - 문의 입력 전용 form, 기본 원형 `?` open button, page-frame nav button, 커스텀 trigger용 open hook, 페이지 context 등록 hook, request context 조립 hook, 네트워크 오류 수집 hook, modal preset factory를 제공한다.
112
+ - 현재 form 기본 구조는 `이름`, `연락처`, `문의 유형`, `자세한 문의 내용` 4필드이며, `사진 첨부`는 구현 범위에서 제외한다.
111
113
  - submit transport, React Query mutation, Next.js route handler, 에러 피드백(`Modal.Alert`)은 서비스 앱이 소유한다.
112
114
  - 모듈 내부 Jotai registry를 사용하므로, layout 고정 버튼 1개와 페이지별 context 등록 hook 조합으로 ready-to-use 구성이 가능하다.
115
+ - `stackKey`는 필요할 때만 override하고, `useOpen` 계열은 기본적으로 현재 pathname 기준 `${pathname}/inquiry`를 사용한다.
116
+ - modal description은 기본으로 `"사용 중 문제가 있나요? 아래 내용을 적어 문의해 주세요."`를 사용하고 특수 문구가 필요할 때만 `dialogOptions.description`으로 덮어쓴다.
113
117
  - 로그인 후 `farm_name`, `contact`를 auto-fill + readonly로 보여야 할 때는 `formContextOptions.defaultValues`와 `farmNameField.mode`, `contactField.mode`를 함께 전달한다.
114
118
  - `/weather/**`
115
119
  - page-frame header utility에 결합되는 weather header 템플릿과 weather data hook/mock 도구를 제공한다.
@@ -126,7 +130,7 @@ Next.js 서비스에서 primitives와 동일한 방식으로 **Raw TypeScript**
126
130
  2. layout 고정 버튼은 `ServiceInquiry.useUserContext()`로 모듈 내부 registry 기반 `requestContext`를 읽는다.
127
131
  3. 서비스 앱은 네트워크 오류가 발생했을 때 `ServiceInquiry.useNetworkError().reportNetworkError(...)`만 호출하고, 모듈이 이를 `user_context.network_errors`에 자동 병합한다.
128
132
  4. 각 페이지/폼은 `ServiceInquiry.useProvideContext({ labels, userContext })`로 `ServiceInquiryProvidedContext`를 등록한다.
129
- 5. 기본 버튼은 `ServiceInquiry.OpenButton`, 커스텀 버튼은 `ServiceInquiry.useOpen`으로 모달 open을 연결한다.
133
+ 5. 비로그인 기본 버튼은 `ServiceInquiry.OpenButton`, 로그인 후 page-frame 진입 버튼은 `ServiceInquiry.NavButton`, 그 외 커스텀 버튼은 `ServiceInquiry.useOpen`으로 모달 open을 연결한다.
130
134
  6. modal footer confirm이 `ServiceInquiry.Form` submit 진입을 담당한다.
131
135
  7. 실제 submit은 서비스 앱의 `useMutation + Next.js route handler + Modal.Alert` 조합으로 처리한다.
132
136
 
package/dist/styles.css CHANGED
@@ -99,6 +99,17 @@
99
99
  --service-inquiry-button-pos-left: var(--spacing-padding-10);
100
100
  --service-inquiry-button-pos-right: auto;
101
101
  --service-inquiry-button-pos-bottom: var(--spacing-padding-10);
102
+ --service-inquiry-field-gap: var(--spacing-padding-5);
103
+ --service-inquiry-type-gap: 6px;
104
+ --service-inquiry-type-height: 48px;
105
+ --service-inquiry-type-radius: 12px;
106
+ --service-inquiry-type-font-size: 17px;
107
+ --service-inquiry-type-font-weight-default: 500;
108
+ --service-inquiry-type-font-weight-selected: 700;
109
+ --service-inquiry-type-bg-default: var(--color-surface-standard);
110
+ --service-inquiry-type-bg-selected: var(--color-surface-static-blue);
111
+ --service-inquiry-type-color-default: var(--color-label-neutral);
112
+ --service-inquiry-type-color-selected: var(--color-primary-default);
102
113
  }
103
114
 
104
115
  /* templates styles는 foundation/primitives 순으로 import된 이후를 가정한다. */
@@ -1743,18 +1754,47 @@
1743
1754
  .service-inquiry-form {
1744
1755
  display: flex;
1745
1756
  flex-direction: column;
1757
+ text-align: left;
1746
1758
  }
1747
1759
 
1748
1760
  .service-inquiry-fields {
1749
1761
  display: flex;
1750
1762
  flex-direction: column;
1751
- gap: var(--spacing-padding-5);
1763
+ gap: var(--service-inquiry-field-gap);
1752
1764
  }
1753
1765
 
1754
1766
  .service-inquiry-field {
1755
1767
  display: flex;
1756
1768
  flex-direction: column;
1757
1769
  }
1770
+ .service-inquiry-field :where(.form-field-footer) {
1771
+ text-align: left;
1772
+ }
1773
+
1774
+ .service-inquiry-type-grid {
1775
+ display: grid;
1776
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1777
+ gap: var(--service-inquiry-type-gap);
1778
+ width: 100%;
1779
+ }
1780
+
1781
+ .service-inquiry-type-option {
1782
+ width: 100%;
1783
+ max-width: none;
1784
+ height: var(--service-inquiry-type-height);
1785
+ border-radius: var(--service-inquiry-type-radius);
1786
+ background-color: var(--service-inquiry-type-bg-default);
1787
+ color: var(--service-inquiry-type-color-default);
1788
+ font-size: var(--service-inquiry-type-font-size);
1789
+ font-weight: var(--service-inquiry-type-font-weight-default);
1790
+ line-height: 1.4;
1791
+ }
1792
+
1793
+ .service-inquiry-type-option:where([data-selected=true]) {
1794
+ background-color: var(--service-inquiry-type-bg-selected);
1795
+ color: var(--service-inquiry-type-color-selected);
1796
+ font-weight: var(--service-inquiry-type-font-weight-selected);
1797
+ }
1758
1798
 
1759
1799
  .service-inquiry-open-button {
1760
1800
  width: var(--service-inquiry-button-width);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniai-fe/uds-templates",
3
- "version": "0.4.10",
3
+ "version": "0.4.12",
4
4
  "description": "UNIAI Design System; UI Templates Package",
5
5
  "type": "module",
6
6
  "private": false,
@@ -15,6 +15,7 @@ export default function PageNavContainer({
15
15
  logo,
16
16
  sitemap,
17
17
  currentPath,
18
+ children,
18
19
  }: PageFrameDesktopNavProps) {
19
20
  const pathnameValue = usePathname();
20
21
  // currentPath를 명시하면 Next 라우터가 없는 환경에서도 선택 상태를 제어할 수 있다.
@@ -65,6 +66,7 @@ export default function PageNavContainer({
65
66
  ),
66
67
  )}
67
68
  </ul>
69
+ {children}
68
70
  </nav>
69
71
  );
70
72
  }
@@ -1,5 +1,6 @@
1
+ import PageNavButton from "./Button";
1
2
  import PageNavContainer from "./Container";
2
3
 
3
- const Nav = PageNavContainer;
4
+ const Nav = { Container: PageNavContainer, Button: PageNavButton };
4
5
 
5
6
  export default Nav;
@@ -1,3 +1,4 @@
1
+ import type { ReactNode } from "react";
1
2
  import type { SitemapDataType } from "../../types";
2
3
 
3
4
  /**
@@ -74,4 +75,8 @@ export interface PageFrameDesktopNavProps {
74
75
  * 현재 경로.
75
76
  */
76
77
  currentPath?: string;
78
+ /**
79
+ * extra 요소
80
+ */
81
+ children?: ReactNode;
77
82
  }
@@ -1,10 +1,18 @@
1
1
  "use client";
2
2
 
3
3
  import clsx from "clsx";
4
- import { Form, Input } from "@uniai-fe/uds-primitives";
4
+ import { Chip, Form, Input } from "@uniai-fe/uds-primitives";
5
+ import { useEffect } from "react";
6
+ import type { ServiceInquiryFormProps } from "../types";
7
+ import type { ServiceInquiryFormValues, ServiceInquiryType } from "../types";
5
8
  import { useFormContext } from "react-hook-form";
6
- import type { ServiceInquiryFieldKey, ServiceInquiryFormProps } from "../types";
7
- import type { ServiceInquiryFormValues } from "../types";
9
+
10
+ const DEFAULT_INQUIRY_TYPE_OPTIONS: ServiceInquiryType[] = [
11
+ "접속이 안 돼요",
12
+ "화면이 멈춰요",
13
+ "데이터가 안나와요",
14
+ "기타",
15
+ ];
8
16
 
9
17
  /**
10
18
  * Service Inquiry Form; 문의 입력 form
@@ -14,6 +22,7 @@ import type { ServiceInquiryFormValues } from "../types";
14
22
  * @param {ServiceInquiryFieldKey[]} [props.visibleFields] 노출 필드 목록
15
23
  * @param {ServiceInquiryInputFieldProps} [props.farmNameField] 농장명 필드 설정
16
24
  * @param {ServiceInquiryInputFieldProps} [props.contactField] 연락처 필드 설정
25
+ * @param {ServiceInquiryTypeFieldProps} [props.inquiryTypeField] 문의 유형 필드 설정
17
26
  * @param {ServiceInquiryTextAreaFieldProps} [props.textField] 문의 본문 필드 설정
18
27
  * @param {SubmitHandler<ServiceInquiryFormValues>} props.onSubmit 문의 제출 핸들러
19
28
  * @example
@@ -24,14 +33,24 @@ const ServiceInquiryForm = ({
24
33
  visibleFields,
25
34
  farmNameField,
26
35
  contactField,
36
+ inquiryTypeField,
27
37
  textField,
28
38
  onSubmit,
29
39
  }: ServiceInquiryFormProps) => {
30
40
  const form = useFormContext<ServiceInquiryFormValues>();
41
+ const selectedInquiryType = form.watch("inquiry_type");
31
42
 
32
43
  // 변경 설명: Modal.Dialog confirm 기본 submit과 연결되도록 form.handleSubmit 결과를 그대로 onSubmit에 바인딩한다.
33
44
  const handleSubmit = form.handleSubmit(onSubmit);
34
45
 
46
+ useEffect(() => {
47
+ form.register("inquiry_type");
48
+ if (!form.getValues("inquiry_type")) {
49
+ // 변경 설명: 문의 유형은 화면 label과 submit payload를 동일하게 유지하기 위해 한글 원문값을 그대로 기본값으로 사용한다.
50
+ form.setValue("inquiry_type", "접속이 안 돼요");
51
+ }
52
+ }, [form]);
53
+
35
54
  return (
36
55
  <form
37
56
  className={clsx("service-inquiry-form", className)}
@@ -46,7 +65,7 @@ const ServiceInquiryForm = ({
46
65
  )}
47
66
  width="full"
48
67
  headerProps={{
49
- required: farmNameField?.required,
68
+ required: farmNameField?.required ?? true,
50
69
  ...(typeof farmNameField?.label === "string"
51
70
  ? { label: farmNameField.label }
52
71
  : typeof farmNameField?.label === "number"
@@ -54,7 +73,7 @@ const ServiceInquiryForm = ({
54
73
  : {
55
74
  labelJsx:
56
75
  typeof farmNameField?.label === "undefined"
57
- ? "농장명"
76
+ ? "이름"
58
77
  : farmNameField.label,
59
78
  }),
60
79
  }}
@@ -64,7 +83,7 @@ const ServiceInquiryForm = ({
64
83
  type="text"
65
84
  block={true}
66
85
  readOnly={farmNameField?.mode === "readonly"}
67
- placeholder={farmNameField?.placeholder ?? "농장명 입력"}
86
+ placeholder={farmNameField?.placeholder ?? "이름을 입력해 주세요"}
68
87
  register={form.register("farm_name")}
69
88
  />
70
89
  </Form.Field.Template>
@@ -90,20 +109,73 @@ const ServiceInquiryForm = ({
90
109
  : contactField.label,
91
110
  }),
92
111
  }}
93
- footer={contactField?.helper}
112
+ footer={
113
+ contactField?.helper ??
114
+ "필요한 경우 입력하신 연락처로 연락 드립니다."
115
+ }
94
116
  >
95
117
  <Input.Base
96
118
  type="text"
97
119
  block={true}
98
120
  readOnly={contactField?.mode === "readonly"}
99
121
  placeholder={
100
- contactField?.placeholder ?? "이메일 또는 전화번호 입력"
122
+ contactField?.placeholder ?? "연락처를 입력해 주세요"
101
123
  }
102
124
  register={form.register("contact")}
103
125
  />
104
126
  </Form.Field.Template>
105
127
  ) : null}
106
128
 
129
+ {(visibleFields?.includes("inquiry_type") ?? true) ? (
130
+ <Form.Field.Template
131
+ className={clsx(
132
+ "service-inquiry-field",
133
+ "service-inquiry-field-inquiry-type",
134
+ )}
135
+ width="full"
136
+ headerProps={{
137
+ required: inquiryTypeField?.required ?? true,
138
+ ...(typeof inquiryTypeField?.label === "string"
139
+ ? { label: inquiryTypeField.label }
140
+ : typeof inquiryTypeField?.label === "number"
141
+ ? { label: String(inquiryTypeField.label) }
142
+ : {
143
+ labelJsx:
144
+ typeof inquiryTypeField?.label === "undefined"
145
+ ? "문의 유형"
146
+ : inquiryTypeField.label,
147
+ }),
148
+ }}
149
+ footer={inquiryTypeField?.helper}
150
+ >
151
+ <div className="service-inquiry-type-grid">
152
+ {(inquiryTypeField?.options ?? DEFAULT_INQUIRY_TYPE_OPTIONS).map(
153
+ inquiryType => {
154
+ return (
155
+ <Chip.ClickableStyle
156
+ key={inquiryType}
157
+ className="service-inquiry-type-option"
158
+ chipStyle="filter"
159
+ fill="solid"
160
+ selected={selectedInquiryType === inquiryType}
161
+ // 변경 설명: inquiry_type은 영문 key 변환 없이 선택된 한글 label 원문을 그대로 submit payload로 유지한다.
162
+ onClick={() =>
163
+ form.setValue("inquiry_type", inquiryType, {
164
+ shouldDirty: true,
165
+ shouldTouch: true,
166
+ shouldValidate: true,
167
+ })
168
+ }
169
+ >
170
+ {inquiryType}
171
+ </Chip.ClickableStyle>
172
+ );
173
+ },
174
+ )}
175
+ </div>
176
+ </Form.Field.Template>
177
+ ) : null}
178
+
107
179
  {(visibleFields?.includes("text") ?? true) ? (
108
180
  <Form.Field.Template
109
181
  className={clsx(
@@ -120,7 +192,7 @@ const ServiceInquiryForm = ({
120
192
  : {
121
193
  labelJsx:
122
194
  typeof textField?.label === "undefined"
123
- ? "문의 내용"
195
+ ? "자세한 문의 내용"
124
196
  : textField.label,
125
197
  }),
126
198
  }}
@@ -129,10 +201,11 @@ const ServiceInquiryForm = ({
129
201
  <Input.TextArea
130
202
  block={true}
131
203
  placeholder={
132
- textField?.placeholder ?? "문의 내용을 자세히 입력해 주세요."
204
+ textField?.placeholder ??
205
+ "어떤 문제가 있었는지 적어주세요.\n예: 화면이 멈췄어요."
133
206
  }
134
207
  height={160}
135
- length={10000}
208
+ maxLength={10000}
136
209
  register={form.register("text")}
137
210
  />
138
211
  </Form.Field.Template>
@@ -0,0 +1,55 @@
1
+ "use client";
2
+
3
+ import clsx from "clsx";
4
+ import { useOpenServiceInquiry } from "../hooks";
5
+ import Icon from "../img/inquiry.svg";
6
+ import type { ServiceInquiryNavButtonProps } from "../types";
7
+
8
+ /**
9
+ * Service Inquiry Nav Button; page-frame nav 문의 버튼
10
+ * @component
11
+ * @param {ServiceInquiryNavButtonProps} props page-frame nav 문의 버튼 props
12
+ * @param {string} [props.className] nav button className
13
+ * @param {string} [props.label] nav button label
14
+ * @param {string} [props.stackKey] modal stack key
15
+ * @param {ServiceInquiryFormProps} props.formProps 문의 form props
16
+ * @param {Partial<DialogTemplateOptions<ServiceInquiryFormValues>>} [props.dialogOptions] 문의 모달 preset 위에 덮어쓸 dialog option
17
+ * @param {() => void} [props.onOpen] 모달 open 직전 콜백
18
+ * @example
19
+ * <ServiceInquiryNavButton formProps={{ onSubmit: values => console.info(values) }} />
20
+ */
21
+ export default function ServiceInquiryNavButton({
22
+ className,
23
+ label = "문의하기",
24
+ stackKey,
25
+ formProps,
26
+ dialogOptions,
27
+ onOpen,
28
+ }: ServiceInquiryNavButtonProps) {
29
+ const { openServiceInquiry } = useOpenServiceInquiry({
30
+ stackKey,
31
+ formProps,
32
+ dialogOptions,
33
+ onOpen,
34
+ });
35
+
36
+ return (
37
+ <div className="page-frame-nav-item">
38
+ <button
39
+ type="button"
40
+ className={clsx(
41
+ "page-frame-nav-category",
42
+ "page-frame-inquiry-button",
43
+ className,
44
+ )}
45
+ // 변경 설명: page-frame children에 바로 주입해도 nav hover/spacing 규칙을 유지하도록 기존 nav class를 그대로 재사용한다.
46
+ onClick={openServiceInquiry}
47
+ >
48
+ <span className="page-frame-nav-category-icon">
49
+ <Icon width={24} height={24} viewBox="0 0 24 24" />
50
+ </span>
51
+ <span className="page-frame-nav-category-label">{label}</span>
52
+ </button>
53
+ </div>
54
+ );
55
+ }
@@ -10,23 +10,22 @@ import clsx from "clsx";
10
10
  * @component
11
11
  * @param {UseOpenServiceInquiryOptions} props 문의 모달 열기 props
12
12
  * @param {string} [props.className]
13
- * @param {string} props.stackKey modal stack key
13
+ * @param {string} [props.stackKey] modal stack key
14
14
  * @param {ServiceInquiryFormProps} props.formProps 문의 form props
15
15
  * @param {Partial<DialogTemplateOptions<ServiceInquiryFormValues>>} [props.dialogOptions] 문의 모달 preset 위에 덮어쓸 dialog option
16
16
  * @param {() => void} [props.onOpen] 모달 open 직전 콜백
17
17
  * @example
18
18
  * <ServiceInquiryOpenButton
19
- * stackKey="sample"
20
19
  * formProps={{ onSubmit: values => console.info(values) }}
21
20
  * />
22
21
  */
23
- const ServiceInquiryOpenButton = ({
22
+ export default function ServiceInquiryOpenButton({
24
23
  className,
25
24
  stackKey,
26
25
  formProps,
27
26
  dialogOptions,
28
27
  onOpen,
29
- }: ServiceInquiryOpenButtonProps) => {
28
+ }: ServiceInquiryOpenButtonProps) {
30
29
  const { openServiceInquiry } = useOpenServiceInquiry({
31
30
  stackKey,
32
31
  formProps,
@@ -45,6 +44,4 @@ const ServiceInquiryOpenButton = ({
45
44
  ?
46
45
  </Button.Rounded>
47
46
  );
48
- };
49
-
50
- export default ServiceInquiryOpenButton;
47
+ }
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { usePathname } from "next/navigation";
3
4
  import { Modal } from "../../modal";
4
5
  import { createServiceInquiryModal } from "../utils/modal-option";
5
6
  import type {
@@ -11,14 +12,13 @@ import type {
11
12
  * Service Inquiry Hook; 문의 모달 열기 Hook
12
13
  * @hook
13
14
  * @param {UseOpenServiceInquiryOptions} options 문의 모달 열기 훅 옵션
14
- * @param {string} options.stackKey modal stack key
15
+ * @param {string} [options.stackKey] modal stack key
15
16
  * @param {ServiceInquiryFormProps} options.formProps 문의 form props
16
17
  * @param {Partial<DialogTemplateOptions<ServiceInquiryFormValues>>} [options.dialogOptions] 문의 모달 preset 위에 덮어쓸 dialog option
17
18
  * @param {() => void} [options.onOpen] 모달 open 직전 콜백
18
19
  * @returns {UseOpenServiceInquiryReturn} 문의 모달 open 함수
19
20
  * @example
20
21
  * const { openServiceInquiry } = useOpenServiceInquiry({
21
- * stackKey: "sample",
22
22
  * formProps: { onSubmit: values => console.info(values) },
23
23
  * });
24
24
  */
@@ -29,13 +29,21 @@ export function useOpenServiceInquiry({
29
29
  onOpen,
30
30
  }: UseOpenServiceInquiryOptions): UseOpenServiceInquiryReturn {
31
31
  const { newModal } = Modal.useModal();
32
+ const pathname = usePathname();
32
33
 
33
34
  // 변경 설명: 커스텀 버튼은 render prop 대신 hook이 돌려주는 단일 open 함수로 연결한다.
34
35
  const openServiceInquiry = () => {
36
+ const resolvedPathname =
37
+ pathname ??
38
+ (typeof window === "undefined" ? "" : window.location.pathname);
39
+
35
40
  onOpen?.();
36
41
  newModal(
37
42
  createServiceInquiryModal({
38
- stackKey,
43
+ // 변경 설명: stackKey를 생략하면 현재 pathname 기준 문의 모달 키를 자동 생성한다.
44
+ stackKey:
45
+ stackKey ??
46
+ `${resolvedPathname === "/" ? "" : resolvedPathname}/inquiry`,
39
47
  formProps,
40
48
  dialogOptions,
41
49
  }),
@@ -1,5 +1,6 @@
1
1
  import "./index.scss";
2
2
  import ServiceInquiryForm from "./components/Form";
3
+ import ServiceInquiryNavButton from "./components/NavButton";
3
4
  import ServiceInquiryOpenButton from "./components/OpenButton";
4
5
  import {
5
6
  useServiceInquiryNetworkError,
@@ -20,6 +21,7 @@ import {
20
21
  export const ServiceInquiry = {
21
22
  Form: ServiceInquiryForm,
22
23
  OpenButton: ServiceInquiryOpenButton,
24
+ NavButton: ServiceInquiryNavButton,
23
25
  useOpen: useOpenServiceInquiry,
24
26
  useNetworkError: useServiceInquiryNetworkError,
25
27
  useProvideContext: useProvideServiceInquiryContext,
@@ -32,6 +34,7 @@ export const ServiceInquiry = {
32
34
  export {
33
35
  ServiceInquiryForm,
34
36
  ServiceInquiryOpenButton,
37
+ ServiceInquiryNavButton,
35
38
  useServiceInquiryNetworkError,
36
39
  useOpenServiceInquiry,
37
40
  useProvideServiceInquiryContext,
@@ -1,15 +1,45 @@
1
1
  .service-inquiry-form {
2
2
  display: flex;
3
3
  flex-direction: column;
4
+ text-align: left;
4
5
  }
5
6
 
6
7
  .service-inquiry-fields {
7
8
  display: flex;
8
9
  flex-direction: column;
9
- gap: var(--spacing-padding-5);
10
+ gap: var(--service-inquiry-field-gap);
10
11
  }
11
12
 
12
13
  .service-inquiry-field {
13
14
  display: flex;
14
15
  flex-direction: column;
16
+
17
+ :where(.form-field-footer) {
18
+ text-align: left;
19
+ }
20
+ }
21
+
22
+ .service-inquiry-type-grid {
23
+ display: grid;
24
+ grid-template-columns: repeat(2, minmax(0, 1fr));
25
+ gap: var(--service-inquiry-type-gap);
26
+ width: 100%;
27
+ }
28
+
29
+ .service-inquiry-type-option {
30
+ width: 100%;
31
+ max-width: none;
32
+ height: var(--service-inquiry-type-height);
33
+ border-radius: var(--service-inquiry-type-radius);
34
+ background-color: var(--service-inquiry-type-bg-default);
35
+ color: var(--service-inquiry-type-color-default);
36
+ font-size: var(--service-inquiry-type-font-size);
37
+ font-weight: var(--service-inquiry-type-font-weight-default);
38
+ line-height: 1.4;
39
+ }
40
+
41
+ .service-inquiry-type-option:where([data-selected="true"]) {
42
+ background-color: var(--service-inquiry-type-bg-selected);
43
+ color: var(--service-inquiry-type-color-selected);
44
+ font-weight: var(--service-inquiry-type-font-weight-selected);
15
45
  }
@@ -5,4 +5,16 @@
5
5
  --service-inquiry-button-pos-left: var(--spacing-padding-10);
6
6
  --service-inquiry-button-pos-right: auto;
7
7
  --service-inquiry-button-pos-bottom: var(--spacing-padding-10);
8
+
9
+ --service-inquiry-field-gap: var(--spacing-padding-5);
10
+ --service-inquiry-type-gap: 6px;
11
+ --service-inquiry-type-height: 48px;
12
+ --service-inquiry-type-radius: 12px;
13
+ --service-inquiry-type-font-size: 17px;
14
+ --service-inquiry-type-font-weight-default: 500;
15
+ --service-inquiry-type-font-weight-selected: 700;
16
+ --service-inquiry-type-bg-default: var(--color-surface-standard);
17
+ --service-inquiry-type-bg-selected: var(--color-surface-static-blue);
18
+ --service-inquiry-type-color-default: var(--color-label-neutral);
19
+ --service-inquiry-type-color-selected: var(--color-primary-default);
8
20
  }
@@ -1,9 +1,13 @@
1
- import type { ServiceInquiryUserContext } from "./form-context";
1
+ import type {
2
+ ServiceInquiryType,
3
+ ServiceInquiryUserContext,
4
+ } from "./form-context";
2
5
 
3
6
  /**
4
7
  * Service Inquiry API; 문의 등록 요청
5
8
  * @property {string} text 문의 본문
6
9
  * @property {string} farm_name 문의 대상 농장명 또는 빈 문자열
10
+ * @property {ServiceInquiryType} inquiry_type 문의 유형 label 원문
7
11
  * @property {string[]} labels JIRA label 목록
8
12
  * @property {string} contact 문의자 연락처(email 또는 phone number)
9
13
  * @property {string} page_path 문의하기를 연 현재 페이지 URL
@@ -18,6 +22,10 @@ export interface API_Req_ServiceInquiry {
18
22
  * 문의 대상 농장명 또는 빈 문자열
19
23
  */
20
24
  farm_name: string;
25
+ /**
26
+ * 문의 유형 label 원문
27
+ */
28
+ inquiry_type: ServiceInquiryType;
21
29
  /**
22
30
  * JIRA label 목록
23
31
  */
@@ -1,10 +1,25 @@
1
+ /**
2
+ * Service Inquiry Form; 문의 유형 값
3
+ * @typedef {"접속이 안 돼요" | "화면이 멈춰요" | "데이터가 안나와요" | "기타"} ServiceInquiryType
4
+ */
5
+ export type ServiceInquiryType =
6
+ | "접속이 안 돼요"
7
+ | "화면이 멈춰요"
8
+ | "데이터가 안나와요"
9
+ | "기타";
10
+
1
11
  /**
2
12
  * Service Inquiry Form; 사용자 입력값
13
+ * @property {ServiceInquiryType} inquiry_type 문의 유형
3
14
  * @property {string} text 문의 본문
4
15
  * @property {string} farm_name 농장명 또는 빈 문자열
5
16
  * @property {string} contact 문의자 연락처
6
17
  */
7
18
  export interface ServiceInquiryFormValues {
19
+ /**
20
+ * 문의 유형
21
+ */
22
+ inquiry_type: ServiceInquiryType;
8
23
  /**
9
24
  * 문의 본문
10
25
  */
@@ -1,12 +1,19 @@
1
1
  import type { ReactNode } from "react";
2
2
  import type { SubmitHandler } from "react-hook-form";
3
3
  import type { DialogTemplateOptions } from "../../modal/types";
4
- import type { ServiceInquiryFormValues } from "./form-context";
4
+ import type {
5
+ ServiceInquiryFormValues,
6
+ ServiceInquiryType,
7
+ } from "./form-context";
5
8
 
6
9
  /**
7
10
  * Service Inquiry; 노출 필드 키
8
11
  */
9
- export type ServiceInquiryFieldKey = "farm_name" | "contact" | "text";
12
+ export type ServiceInquiryFieldKey =
13
+ | "farm_name"
14
+ | "contact"
15
+ | "inquiry_type"
16
+ | "text";
10
17
 
11
18
  /**
12
19
  * Service Inquiry Form; field 입력 모드
@@ -62,12 +69,27 @@ export interface ServiceInquiryTextAreaFieldProps extends ServiceInquiryFieldBas
62
69
  placeholder?: string;
63
70
  }
64
71
 
72
+ /**
73
+ * Service Inquiry Form; 문의 유형 field 설정
74
+ * @property {ReactNode} [label] field label
75
+ * @property {ReactNode} [helper] field helper
76
+ * @property {boolean} [required] required 표시 여부
77
+ * @property {ServiceInquiryType[]} [options] 노출할 문의 유형 옵션 순서
78
+ */
79
+ export interface ServiceInquiryTypeFieldProps extends ServiceInquiryFieldBaseProps {
80
+ /**
81
+ * 노출할 문의 유형 옵션 순서
82
+ */
83
+ options?: ServiceInquiryType[];
84
+ }
85
+
65
86
  /**
66
87
  * Service Inquiry Form; 문의 입력 폼 props
67
88
  * @property {string} [className] form className
68
89
  * @property {ServiceInquiryFieldKey[]} [visibleFields] 노출 필드 목록
69
90
  * @property {ServiceInquiryInputFieldProps} [farmNameField] 농장명 필드 설정
70
91
  * @property {ServiceInquiryInputFieldProps} [contactField] 연락처 필드 설정
92
+ * @property {ServiceInquiryTypeFieldProps} [inquiryTypeField] 문의 유형 필드 설정
71
93
  * @property {ServiceInquiryTextAreaFieldProps} [textField] 문의 본문 필드 설정
72
94
  * @property {SubmitHandler<ServiceInquiryFormValues>} props.onSubmit 문의 제출 핸들러
73
95
  */
@@ -88,6 +110,10 @@ export interface ServiceInquiryFormProps {
88
110
  * 연락처 필드 설정
89
111
  */
90
112
  contactField?: ServiceInquiryInputFieldProps;
113
+ /**
114
+ * 문의 유형 필드 설정
115
+ */
116
+ inquiryTypeField?: ServiceInquiryTypeFieldProps;
91
117
  /**
92
118
  * 문의 본문 필드 설정
93
119
  */
@@ -100,7 +126,7 @@ export interface ServiceInquiryFormProps {
100
126
 
101
127
  /**
102
128
  * Service Inquiry Modal; 문의 모달 preset 생성 옵션
103
- * @property {string} stackKey modal stack key
129
+ * @property {string} [stackKey] modal stack key
104
130
  * @property {ServiceInquiryFormProps} formProps 문의 form props
105
131
  * @property {Partial<DialogTemplateOptions<ServiceInquiryFormValues>>} [dialogOptions] 문의 모달 preset 위에 덮어쓸 dialog option
106
132
  */
@@ -109,7 +135,7 @@ export interface ServiceInquiryCreateModalOptions {
109
135
  /**
110
136
  * modal stack key
111
137
  */
112
- stackKey: string;
138
+ stackKey?: string;
113
139
  /**
114
140
  * 문의 form props
115
141
  */
@@ -122,7 +148,7 @@ export interface ServiceInquiryCreateModalOptions {
122
148
 
123
149
  /**
124
150
  * Service Inquiry Hook; 문의 모달 열기 훅 옵션
125
- * @property {string} stackKey modal stack key
151
+ * @property {string} [stackKey] modal stack key
126
152
  * @property {ServiceInquiryFormProps} formProps 문의 form props
127
153
  * @property {Partial<DialogTemplateOptions<ServiceInquiryFormValues>>} [dialogOptions] 문의 모달 preset 위에 덮어쓸 dialog option
128
154
  * @property {() => void} [onOpen] 모달 open 직전 콜백
@@ -137,15 +163,38 @@ export interface UseOpenServiceInquiryOptions extends ServiceInquiryCreateModalO
137
163
  /**
138
164
  * Service Inquiry Button props; 버튼 옵션
139
165
  * @property {string} [className]
140
- * @property {string} stackKey modal stack key
166
+ * @property {string} [stackKey] modal stack key
141
167
  * @property {ServiceInquiryFormProps} formProps 문의 form props
142
168
  * @property {Partial<DialogTemplateOptions<ServiceInquiryFormValues>>} [dialogOptions] 문의 모달 preset 위에 덮어쓸 dialog option
143
169
  * @property {() => void} [onOpen] 모달 open 직전 콜백
144
170
  */
145
171
  export interface ServiceInquiryOpenButtonProps extends UseOpenServiceInquiryOptions {
172
+ /**
173
+ * button className
174
+ */
146
175
  className?: string;
147
176
  }
148
177
 
178
+ /**
179
+ * Service Inquiry Nav Button props; page-frame nav 문의 버튼 옵션
180
+ * @property {string} [className] nav button className
181
+ * @property {string} [label] nav button label
182
+ * @property {string} [stackKey] modal stack key
183
+ * @property {ServiceInquiryFormProps} formProps 문의 form props
184
+ * @property {Partial<DialogTemplateOptions<ServiceInquiryFormValues>>} [dialogOptions] 문의 모달 preset 위에 덮어쓸 dialog option
185
+ * @property {() => void} [onOpen] 모달 open 직전 콜백
186
+ */
187
+ export interface ServiceInquiryNavButtonProps extends UseOpenServiceInquiryOptions {
188
+ /**
189
+ * nav button className
190
+ */
191
+ className?: string;
192
+ /**
193
+ * nav button label
194
+ */
195
+ label?: string;
196
+ }
197
+
149
198
  /**
150
199
  * Service Inquiry Hook; 문의 모달 열기 훅 반환값
151
200
  * @property {() => void} openServiceInquiry 문의 모달 open 함수
@@ -9,17 +9,16 @@ import type {
9
9
  * Service Inquiry Modal; 문의 form이 포함된 Dialog option 생성기
10
10
  * @component
11
11
  * @param {ServiceInquiryCreateModalOptions} options 문의 모달 preset 생성 옵션
12
- * @param {string} options.stackKey modal stack key
12
+ * @param {string} [options.stackKey] modal stack key
13
13
  * @param {ServiceInquiryFormProps} options.formProps 문의 form props
14
14
  * @param {Partial<DialogTemplateOptions<ServiceInquiryFormValues>>} [options.dialogOptions] 문의 모달 preset 위에 덮어쓸 dialog option
15
15
  * @example
16
16
  * createServiceInquiryModal({
17
- * stackKey: "service-inquiry",
18
17
  * formProps: { onSubmit: values => console.info(values) },
19
18
  * })
20
19
  */
21
20
  export function createServiceInquiryModal({
22
- stackKey,
21
+ stackKey = "service-inquiry",
23
22
  formProps,
24
23
  dialogOptions,
25
24
  }: ServiceInquiryCreateModalOptions) {
@@ -28,14 +27,30 @@ export function createServiceInquiryModal({
28
27
  return createDialogModal<ServiceInquiryFormValues>({
29
28
  ...dialogOptions,
30
29
  stackKey,
30
+ // 변경 설명: service-inquiry 모달은 Figma popup 실측값(520px)을 기본 panel width로 사용한다.
31
+ width: dialogOptions?.width ?? 520,
31
32
  title: dialogOptions?.title ?? "문의하기",
33
+ // 변경 설명: 기본 안내 문구는 figma 기준 문의 모달 description으로 고정하고 필요한 경우에만 override한다.
34
+ description:
35
+ dialogOptions?.description ??
36
+ "사용 중 문제가 있나요? 아래 내용을 적어 문의해 주세요.",
32
37
  // 변경 설명: 문의 모달은 title과 안내 문구를 좌측 정렬로 읽히게 하기 위해 split header를 기본값으로 사용한다.
33
38
  headerLayout: dialogOptions?.headerLayout ?? "split",
39
+ // 변경 설명: Figma popup은 우측 닫기 아이콘이 기본 노출 상태라 service-inquiry preset도 close button을 기본 활성화한다.
40
+ activeCloseButton: dialogOptions?.activeCloseButton ?? true,
34
41
  // 변경 설명: ServiceInquiryForm은 useFormContext를 직접 읽으므로 Dialog 내부 Form.Provider를 항상 활성화한다.
35
42
  formContextOptions,
36
43
  content: <ServiceInquiryForm {...formProps} />,
44
+ cancel: {
45
+ label: "취소",
46
+ position: "right",
47
+ width: "fit",
48
+ ...dialogOptions?.cancel,
49
+ },
37
50
  confirm: {
38
- label: "문의 접수",
51
+ position: "right",
52
+ width: "fit",
53
+ label: "문의하기",
39
54
  ...dialogOptions?.confirm,
40
55
  },
41
56
  });