@uniai-fe/uds-templates 0.4.11 → 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 +6 -2
- package/dist/styles.css +41 -1
- package/package.json +1 -1
- package/src/page-frame/desktop/components/nav/Container.tsx +2 -0
- package/src/page-frame/desktop/types/nav.ts +5 -0
- package/src/service-inquiry/components/Form.tsx +84 -11
- package/src/service-inquiry/components/NavButton.tsx +55 -0
- package/src/service-inquiry/components/OpenButton.tsx +4 -7
- package/src/service-inquiry/hooks/useOpen.ts +11 -3
- package/src/service-inquiry/index.tsx +3 -0
- package/src/service-inquiry/styles/form.scss +31 -1
- package/src/service-inquiry/styles/variables.scss +12 -0
- package/src/service-inquiry/types/api.ts +9 -1
- package/src/service-inquiry/types/form-context.ts +15 -0
- package/src/service-inquiry/types/props.ts +55 -6
- package/src/service-inquiry/utils/modal-option.tsx +19 -4
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(--
|
|
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
|
@@ -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,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";
|
|
5
|
-
import {
|
|
4
|
+
import { Chip, Form, Input } from "@uniai-fe/uds-primitives";
|
|
5
|
+
import { useEffect } from "react";
|
|
6
6
|
import type { ServiceInquiryFormProps } from "../types";
|
|
7
|
-
import type { ServiceInquiryFormValues } from "../types";
|
|
7
|
+
import type { ServiceInquiryFormValues, ServiceInquiryType } from "../types";
|
|
8
|
+
import { useFormContext } from "react-hook-form";
|
|
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={
|
|
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
|
-
|
|
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
|
-
|
|
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(--
|
|
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 {
|
|
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 {
|
|
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 =
|
|
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
|
|
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
|
-
|
|
51
|
+
position: "right",
|
|
52
|
+
width: "fit",
|
|
53
|
+
label: "문의하기",
|
|
39
54
|
...dialogOptions?.confirm,
|
|
40
55
|
},
|
|
41
56
|
});
|