@uniai-fe/uds-templates 0.5.15 → 0.5.16
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 +1 -0
- package/dist/styles.css +3 -0
- package/package.json +1 -1
- package/src/auth/login/markup/Container.tsx +7 -1
- package/src/auth/login/markup/FormField.tsx +18 -5
- package/src/auth/login/markup/LinkButtons.tsx +10 -11
- package/src/auth/login/types/props.ts +91 -0
- package/src/edge-case/components/NotFound.tsx +36 -14
- package/src/edge-case/types/index.ts +41 -0
- package/src/service-inquiry/components/Form.tsx +74 -34
- package/src/service-inquiry/components/OpenButton.tsx +3 -0
- package/src/service-inquiry/styles/form.scss +5 -0
- package/src/service-inquiry/types/form-context.ts +2 -6
- package/src/service-inquiry/types/props.ts +10 -0
package/README.md
CHANGED
|
@@ -156,6 +156,7 @@ Next.js 서비스에서 primitives와 동일한 방식으로 **Raw TypeScript**
|
|
|
156
156
|
5. 비로그인 기본 버튼은 `ServiceInquiry.OpenButton`, 로그인 후 page-frame 진입 버튼은 `ServiceInquiry.NavButton`, 그 외 커스텀 버튼은 `ServiceInquiry.useOpen`으로 모달 open을 연결한다.
|
|
157
157
|
6. modal footer confirm이 `ServiceInquiry.Form` submit 진입을 담당한다.
|
|
158
158
|
7. 실제 submit은 서비스 앱의 `useMutation + Next.js route handler + Modal.Alert` 조합으로 처리한다.
|
|
159
|
+
8. 표시 문구는 field별 `label`/`placeholder`/`helper`/`requiredMessage`, `inquiryTypeField.options`, `dialogOptions`, `NavButton.label`, `OpenButton.ariaLabel`로 필요한 항목만 주입한다.
|
|
159
160
|
|
|
160
161
|
`service-inquiry`는 구조와 request context까지만 제공하고, 네트워크 상태/재시도/성공·실패 피드백은 서비스 앱이 소유합니다.
|
|
161
162
|
|
package/dist/styles.css
CHANGED
|
@@ -1962,6 +1962,9 @@
|
|
|
1962
1962
|
font-weight: var(--service-inquiry-type-font-weight-default);
|
|
1963
1963
|
line-height: 1.4;
|
|
1964
1964
|
}
|
|
1965
|
+
.service-inquiry-type-option :where(.chip-label) {
|
|
1966
|
+
line-height: 1.4;
|
|
1967
|
+
}
|
|
1965
1968
|
|
|
1966
1969
|
.service-inquiry-type-option:where([data-selected=true]) {
|
|
1967
1970
|
background-color: var(--service-inquiry-type-bg-selected);
|
package/package.json
CHANGED
|
@@ -13,6 +13,7 @@ import type { AuthLoginProps } from "../types";
|
|
|
13
13
|
* @property {React.ReactNode} [props.footer] 하단 슬롯
|
|
14
14
|
* @property {AuthLoginFieldOptions} props.fieldOptions 입력 필드 옵션
|
|
15
15
|
* @property {AuthLoginLinkOptions} [props.linkOptions] 계정 찾기/회원가입 링크
|
|
16
|
+
* @property {AuthLoginTexts} [props.texts] 로그인 템플릿 기본 문구 옵션
|
|
16
17
|
* @property {() => void} [props.onFindPasswordLinkClick] 비밀번호 찾기 버튼 클릭 핸들러
|
|
17
18
|
*/
|
|
18
19
|
export default function AuthLoginContainer({
|
|
@@ -21,6 +22,7 @@ export default function AuthLoginContainer({
|
|
|
21
22
|
footer,
|
|
22
23
|
linkOptions,
|
|
23
24
|
fieldOptions,
|
|
25
|
+
texts,
|
|
24
26
|
onFindPasswordLinkClick,
|
|
25
27
|
}: AuthLoginProps) {
|
|
26
28
|
return (
|
|
@@ -29,10 +31,14 @@ export default function AuthLoginContainer({
|
|
|
29
31
|
header={header}
|
|
30
32
|
footer={footer}
|
|
31
33
|
>
|
|
32
|
-
<AuthLoginFormField
|
|
34
|
+
<AuthLoginFormField
|
|
35
|
+
{...fieldOptions}
|
|
36
|
+
texts={{ ...texts, ...fieldOptions.texts }}
|
|
37
|
+
/>
|
|
33
38
|
{/* 링크 옵션이 부분적으로만 넘어와도 내부에서 존재하는 항목만 렌더링한다. */}
|
|
34
39
|
<AuthLoginLinkButtons
|
|
35
40
|
linkOptions={linkOptions}
|
|
41
|
+
texts={texts}
|
|
36
42
|
onFindPasswordClick={onFindPasswordLinkClick}
|
|
37
43
|
/>
|
|
38
44
|
</AuthContainer>
|
|
@@ -14,12 +14,14 @@ import AuthLoginFormFieldPassword from "./Password";
|
|
|
14
14
|
* @param {AuthLoginFieldOptions} props
|
|
15
15
|
* @property {AuthLoginIdFieldOptions} [props.idField] 아이디 필드 옵션
|
|
16
16
|
* @property {AuthLoginPasswordFieldOptions} [props.passwordField] 비밀번호 필드 옵션
|
|
17
|
+
* @property {AuthLoginFieldTexts} [props.texts] 로그인 form 기본 문구 옵션
|
|
17
18
|
* @property {React.FormHTMLAttributes<HTMLFormElement>} [props.formAttr] <form> attr
|
|
18
19
|
* @property {SubmitHandler<AuthLoginFormValues>} props.onLogin 로그인 콜백
|
|
19
20
|
*/
|
|
20
21
|
export default function AuthLoginFormField({
|
|
21
22
|
idField,
|
|
22
23
|
passwordField,
|
|
24
|
+
texts,
|
|
23
25
|
formAttr,
|
|
24
26
|
onLogin,
|
|
25
27
|
}: AuthLoginFieldOptions) {
|
|
@@ -73,6 +75,10 @@ export default function AuthLoginFormField({
|
|
|
73
75
|
? { text: passwordHelperNode, state: helpers.password.state }
|
|
74
76
|
: undefined;
|
|
75
77
|
|
|
78
|
+
const idLabel = idField?.label ?? texts?.idLabel ?? "아이디";
|
|
79
|
+
const passwordLabel =
|
|
80
|
+
passwordField?.label ?? texts?.passwordLabel ?? "비밀번호";
|
|
81
|
+
|
|
76
82
|
return (
|
|
77
83
|
<form className="auth-login-form" {...formAttr} onSubmit={onSubmit}>
|
|
78
84
|
<div className="auth-login-fields">
|
|
@@ -80,10 +86,13 @@ export default function AuthLoginFormField({
|
|
|
80
86
|
<AuthLoginFormFieldId
|
|
81
87
|
register={register.id}
|
|
82
88
|
helper={idHelper}
|
|
83
|
-
label={
|
|
89
|
+
label={idLabel}
|
|
84
90
|
placeholder={
|
|
85
91
|
idField?.placeholder ??
|
|
86
|
-
|
|
92
|
+
texts?.idPlaceholder ??
|
|
93
|
+
(typeof idLabel === "string" || typeof idLabel === "number"
|
|
94
|
+
? `${idLabel}를 입력해 주세요`
|
|
95
|
+
: "아이디를 입력해 주세요")
|
|
87
96
|
}
|
|
88
97
|
inputProps={idField?.inputProps}
|
|
89
98
|
templateProps={idField?.templateProps}
|
|
@@ -92,8 +101,12 @@ export default function AuthLoginFormField({
|
|
|
92
101
|
<AuthLoginFormFieldPassword
|
|
93
102
|
register={register.password}
|
|
94
103
|
helper={passwordHelper}
|
|
95
|
-
label={
|
|
96
|
-
placeholder={
|
|
104
|
+
label={passwordLabel}
|
|
105
|
+
placeholder={
|
|
106
|
+
passwordField?.placeholder ??
|
|
107
|
+
texts?.passwordPlaceholder ??
|
|
108
|
+
"비밀번호를 입력해 주세요"
|
|
109
|
+
}
|
|
97
110
|
inputProps={passwordField?.inputProps}
|
|
98
111
|
templateProps={passwordField?.templateProps}
|
|
99
112
|
/>
|
|
@@ -105,7 +118,7 @@ export default function AuthLoginFormField({
|
|
|
105
118
|
block
|
|
106
119
|
disabled={disabled}
|
|
107
120
|
>
|
|
108
|
-
로그인
|
|
121
|
+
{texts?.submit ?? "로그인"}
|
|
109
122
|
</Button.Default>
|
|
110
123
|
</div>
|
|
111
124
|
</form>
|
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
import Link from "next/link";
|
|
2
2
|
import { Button, Divider } from "@uniai-fe/uds-primitives";
|
|
3
|
-
import type {
|
|
3
|
+
import type { AuthLoginLinkButtonsProps } from "../types";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* 로그인 링크 버튼 묶음; 전달된 링크만 선택적으로 렌더링한다.
|
|
7
7
|
* @component
|
|
8
|
-
* @param {
|
|
9
|
-
* @param {AuthLoginLinkOptions} [props.linkOptions]
|
|
8
|
+
* @param {AuthLoginLinkButtonsProps} props
|
|
9
|
+
* @param {AuthLoginLinkOptions} [props.linkOptions] 계정 관련 링크 옵션
|
|
10
|
+
* @param {AuthLoginLinkTexts} [props.texts] 링크/버튼 문구 옵션
|
|
10
11
|
* @param {() => void} [props.onFindPasswordClick] 비밀번호 찾기 커스텀 클릭 핸들러
|
|
11
12
|
*/
|
|
12
13
|
export default function AuthLoginLinkButtons({
|
|
13
14
|
linkOptions,
|
|
15
|
+
texts,
|
|
14
16
|
onFindPasswordClick,
|
|
15
|
-
}: {
|
|
16
|
-
linkOptions?: AuthLoginLinkOptions;
|
|
17
|
-
onFindPasswordClick?: () => void;
|
|
18
|
-
}) {
|
|
17
|
+
}: AuthLoginLinkButtonsProps) {
|
|
19
18
|
const hrefFindId = linkOptions?.find?.id;
|
|
20
19
|
const hrefFindPassword = linkOptions?.find?.password;
|
|
21
20
|
const hrefSignup = linkOptions?.signup;
|
|
@@ -33,7 +32,7 @@ export default function AuthLoginLinkButtons({
|
|
|
33
32
|
href={hrefFindId}
|
|
34
33
|
className="auth-login-find-account-button auth-find-id-button"
|
|
35
34
|
>
|
|
36
|
-
<span
|
|
35
|
+
<span>{texts?.findId ?? "아이디 찾기"}</span>
|
|
37
36
|
</Link>
|
|
38
37
|
)}
|
|
39
38
|
{hrefFindId && hrefFindPassword && <Divider />}
|
|
@@ -45,14 +44,14 @@ export default function AuthLoginLinkButtons({
|
|
|
45
44
|
className="auth-login-find-account-button auth-find-password-button"
|
|
46
45
|
onClick={onFindPasswordClick}
|
|
47
46
|
>
|
|
48
|
-
<span
|
|
47
|
+
<span>{texts?.findPassword ?? "비밀번호 찾기"}</span>
|
|
49
48
|
</button>
|
|
50
49
|
) : (
|
|
51
50
|
<Link
|
|
52
51
|
href={hrefFindPassword}
|
|
53
52
|
className="auth-login-find-account-button auth-find-password-button"
|
|
54
53
|
>
|
|
55
|
-
<span
|
|
54
|
+
<span>{texts?.findPassword ?? "비밀번호 찾기"}</span>
|
|
56
55
|
</Link>
|
|
57
56
|
))}
|
|
58
57
|
</div>
|
|
@@ -67,7 +66,7 @@ export default function AuthLoginLinkButtons({
|
|
|
67
66
|
priority="tertiary"
|
|
68
67
|
size="small"
|
|
69
68
|
>
|
|
70
|
-
회원가입
|
|
69
|
+
{texts?.signup ?? "회원가입"}
|
|
71
70
|
</Button.Rounded>
|
|
72
71
|
</div>
|
|
73
72
|
)}
|
|
@@ -216,10 +216,71 @@ export interface AuthLoginIdFieldOptions extends AuthLoginFieldProps<InputProps>
|
|
|
216
216
|
*/
|
|
217
217
|
export interface AuthLoginPasswordFieldOptions extends AuthLoginFieldProps<InputPasswordProps> {}
|
|
218
218
|
|
|
219
|
+
/**
|
|
220
|
+
* 로그인 필드 문구 옵션
|
|
221
|
+
* @property {ReactNode} [idLabel] 아이디 필드 기본 라벨
|
|
222
|
+
* @property {string} [idPlaceholder] 아이디 필드 기본 placeholder
|
|
223
|
+
* @property {ReactNode} [passwordLabel] 비밀번호 필드 기본 라벨
|
|
224
|
+
* @property {string} [passwordPlaceholder] 비밀번호 필드 기본 placeholder
|
|
225
|
+
* @property {ReactNode} [submit] submit 버튼 라벨
|
|
226
|
+
*/
|
|
227
|
+
export interface AuthLoginFieldTexts {
|
|
228
|
+
/**
|
|
229
|
+
* 아이디 필드 기본 라벨
|
|
230
|
+
*/
|
|
231
|
+
idLabel?: ReactNode;
|
|
232
|
+
/**
|
|
233
|
+
* 아이디 필드 기본 placeholder
|
|
234
|
+
*/
|
|
235
|
+
idPlaceholder?: string;
|
|
236
|
+
/**
|
|
237
|
+
* 비밀번호 필드 기본 라벨
|
|
238
|
+
*/
|
|
239
|
+
passwordLabel?: ReactNode;
|
|
240
|
+
/**
|
|
241
|
+
* 비밀번호 필드 기본 placeholder
|
|
242
|
+
*/
|
|
243
|
+
passwordPlaceholder?: string;
|
|
244
|
+
/**
|
|
245
|
+
* submit 버튼 라벨
|
|
246
|
+
*/
|
|
247
|
+
submit?: ReactNode;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* 로그인 링크 문구 옵션
|
|
252
|
+
* @property {ReactNode} [findId] 아이디 찾기 링크 라벨
|
|
253
|
+
* @property {ReactNode} [findPassword] 비밀번호 찾기 링크 라벨
|
|
254
|
+
* @property {ReactNode} [signup] 회원가입 버튼 라벨
|
|
255
|
+
*/
|
|
256
|
+
export interface AuthLoginLinkTexts {
|
|
257
|
+
/**
|
|
258
|
+
* 아이디 찾기 링크 라벨
|
|
259
|
+
*/
|
|
260
|
+
findId?: ReactNode;
|
|
261
|
+
/**
|
|
262
|
+
* 비밀번호 찾기 링크 라벨
|
|
263
|
+
*/
|
|
264
|
+
findPassword?: ReactNode;
|
|
265
|
+
/**
|
|
266
|
+
* 회원가입 버튼 라벨
|
|
267
|
+
*/
|
|
268
|
+
signup?: ReactNode;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* 로그인 템플릿 문구 옵션
|
|
273
|
+
* @extends AuthLoginFieldTexts
|
|
274
|
+
* @extends AuthLoginLinkTexts
|
|
275
|
+
*/
|
|
276
|
+
export interface AuthLoginTexts
|
|
277
|
+
extends AuthLoginFieldTexts, AuthLoginLinkTexts {}
|
|
278
|
+
|
|
219
279
|
/**
|
|
220
280
|
* 로그인 필드 옵션; form attr + 제출 핸들러 포함
|
|
221
281
|
* @property {AuthLoginIdFieldOptions} [idField] 로그인 아이디 필드 옵션
|
|
222
282
|
* @property {AuthLoginPasswordFieldOptions} [passwordField] 로그인 비밀번호 필드 옵션
|
|
283
|
+
* @property {AuthLoginFieldTexts} [texts] 로그인 form 기본 문구 옵션
|
|
223
284
|
* @property {React.FormHTMLAttributes<HTMLFormElement>} [formAttr] <form /> attributes
|
|
224
285
|
* @property {SubmitHandler<Record<string, string>>} onLogin 로그인 콜백 이벤트
|
|
225
286
|
*/
|
|
@@ -232,6 +293,10 @@ export interface AuthLoginFieldOptions {
|
|
|
232
293
|
* 비밀번호 필드 설정
|
|
233
294
|
*/
|
|
234
295
|
passwordField?: AuthLoginPasswordFieldOptions;
|
|
296
|
+
/**
|
|
297
|
+
* 로그인 form 기본 문구 옵션
|
|
298
|
+
*/
|
|
299
|
+
texts?: AuthLoginFieldTexts;
|
|
235
300
|
/**
|
|
236
301
|
* form attr
|
|
237
302
|
*/
|
|
@@ -271,6 +336,27 @@ export interface AuthLoginLinkOptions {
|
|
|
271
336
|
signup?: string;
|
|
272
337
|
}
|
|
273
338
|
|
|
339
|
+
/**
|
|
340
|
+
* 로그인 링크 버튼 props
|
|
341
|
+
* @property {AuthLoginLinkOptions} [linkOptions] 계정 관련 링크 옵션
|
|
342
|
+
* @property {AuthLoginLinkTexts} [texts] 링크/버튼 문구 옵션
|
|
343
|
+
* @property {() => void} [onFindPasswordClick] 비밀번호 찾기 링크버튼 클릭 콜백 이벤트
|
|
344
|
+
*/
|
|
345
|
+
export interface AuthLoginLinkButtonsProps {
|
|
346
|
+
/**
|
|
347
|
+
* 계정 관련 링크 옵션
|
|
348
|
+
*/
|
|
349
|
+
linkOptions?: AuthLoginLinkOptions;
|
|
350
|
+
/**
|
|
351
|
+
* 링크/버튼 문구 옵션
|
|
352
|
+
*/
|
|
353
|
+
texts?: AuthLoginLinkTexts;
|
|
354
|
+
/**
|
|
355
|
+
* 비밀번호 찾기 링크버튼 클릭 콜백 이벤트
|
|
356
|
+
*/
|
|
357
|
+
onFindPasswordClick?: () => void;
|
|
358
|
+
}
|
|
359
|
+
|
|
274
360
|
/**
|
|
275
361
|
* 로그인 템플릿 props; 컨테이너와 필드/링크 옵션을 조합한다.
|
|
276
362
|
* @template TFields
|
|
@@ -279,6 +365,7 @@ export interface AuthLoginLinkOptions {
|
|
|
279
365
|
* @property {ReactNode} [footer] 하단 콘텐츠
|
|
280
366
|
* @property {AuthLoginFieldOptions} [fieldOptions] 입력 필드 옵션
|
|
281
367
|
* @property {AuthLoginLinkOptions} [linkOptions] 계정관련 링크 옵션
|
|
368
|
+
* @property {AuthLoginTexts} [texts] 로그인 템플릿 기본 문구 옵션
|
|
282
369
|
* @property {() => void} [onFindPasswordLinkClick] 비밀번호 찾기 링크버튼 클릭 콜백 이벤트
|
|
283
370
|
*/
|
|
284
371
|
export interface AuthLoginProps extends Omit<AuthContainerProps, "children"> {
|
|
@@ -290,6 +377,10 @@ export interface AuthLoginProps extends Omit<AuthContainerProps, "children"> {
|
|
|
290
377
|
* 링크 옵션
|
|
291
378
|
*/
|
|
292
379
|
linkOptions?: AuthLoginLinkOptions;
|
|
380
|
+
/**
|
|
381
|
+
* 로그인 템플릿 기본 문구 옵션
|
|
382
|
+
*/
|
|
383
|
+
texts?: AuthLoginTexts;
|
|
293
384
|
/**
|
|
294
385
|
* 비밀번호 찾기 링크 클릭 핸들러
|
|
295
386
|
*/
|
|
@@ -3,13 +3,35 @@
|
|
|
3
3
|
import { clsx } from "clsx";
|
|
4
4
|
import { Alternate } from "@uniai-fe/uds-primitives";
|
|
5
5
|
import NotFoundIcon from "../img/404.svg";
|
|
6
|
-
import type { EdgeCaseNotFoundProps } from "../types";
|
|
6
|
+
import type { EdgeCaseNotFoundProps, EdgeCaseNotFoundTexts } from "../types";
|
|
7
|
+
|
|
8
|
+
const DEFAULT_NOT_FOUND_TEXTS: Required<EdgeCaseNotFoundTexts> = {
|
|
9
|
+
title: "이 페이지를 찾을 수 없습니다.",
|
|
10
|
+
description: (
|
|
11
|
+
<>
|
|
12
|
+
페이지가 이동되었거나 삭제되었을 수 있습니다.
|
|
13
|
+
<br />
|
|
14
|
+
페이지 주소를 다시 확인해 주세요.
|
|
15
|
+
</>
|
|
16
|
+
),
|
|
17
|
+
inquiryPrefix: "관련된 문의사항은 ",
|
|
18
|
+
inquiryAction: "문의하기",
|
|
19
|
+
inquirySuffix: (
|
|
20
|
+
<>
|
|
21
|
+
를 통해
|
|
22
|
+
<br />
|
|
23
|
+
접수해 주시길 바랍니다.
|
|
24
|
+
</>
|
|
25
|
+
),
|
|
26
|
+
prevAction: "이전 페이지로",
|
|
27
|
+
};
|
|
7
28
|
|
|
8
29
|
/**
|
|
9
30
|
* Edge Case Not Found; not-found 상태 화면 템플릿
|
|
10
31
|
* @component
|
|
11
32
|
* @param {EdgeCaseNotFoundProps} props
|
|
12
33
|
* @param {string} [props.className] 최상위 edge-case container className override다.
|
|
34
|
+
* @param {EdgeCaseNotFoundTexts} [props.texts] not-found 기본 문구 override다.
|
|
13
35
|
* @param {string} [props.inquiryHref] 문의 text action을 link로 렌더링할 href다.
|
|
14
36
|
* @param {() => void} [props.onInquiry] 문의 text action을 button으로 렌더링할 콜백이다.
|
|
15
37
|
* @param {string} [props.fallbackHref] onPrev가 없을 때 사용할 fallback href다.
|
|
@@ -19,11 +41,17 @@ import type { EdgeCaseNotFoundProps } from "../types";
|
|
|
19
41
|
*/
|
|
20
42
|
export default function EdgeCaseNotFound({
|
|
21
43
|
className,
|
|
44
|
+
texts,
|
|
22
45
|
inquiryHref,
|
|
23
46
|
onInquiry,
|
|
24
47
|
fallbackHref = "/",
|
|
25
48
|
onPrev,
|
|
26
49
|
}: EdgeCaseNotFoundProps) {
|
|
50
|
+
const resolvedTexts = {
|
|
51
|
+
...DEFAULT_NOT_FOUND_TEXTS,
|
|
52
|
+
...texts,
|
|
53
|
+
};
|
|
54
|
+
|
|
27
55
|
// 변경: data-edge-case forwarding이 없는 설치본에서도 NotFound preset 스타일이 적용되도록 class hook을 병행한다.
|
|
28
56
|
return (
|
|
29
57
|
<Alternate.Layout.Container
|
|
@@ -39,35 +67,29 @@ export default function EdgeCaseNotFound({
|
|
|
39
67
|
/>
|
|
40
68
|
</Alternate.Layout.Figure>
|
|
41
69
|
<Alternate.Layout.Title as="h1">
|
|
42
|
-
|
|
70
|
+
{resolvedTexts.title}
|
|
43
71
|
</Alternate.Layout.Title>
|
|
44
72
|
<Alternate.Layout.Contents>
|
|
45
|
-
<p>
|
|
46
|
-
페이지가 이동되었거나 삭제되었을 수 있습니다.
|
|
47
|
-
<br />
|
|
48
|
-
페이지 주소를 다시 확인해 주세요.
|
|
49
|
-
</p>
|
|
73
|
+
<p>{resolvedTexts.description}</p>
|
|
50
74
|
{/* 변경: NotFound 본문 줄바꿈은 폭 제한이 아니라 Figma fixed copy의 br로 고정한다. */}
|
|
51
75
|
{onInquiry || inquiryHref ? (
|
|
52
76
|
<p>
|
|
53
|
-
|
|
77
|
+
{resolvedTexts.inquiryPrefix}
|
|
54
78
|
<Alternate.Layout.TextButton href={inquiryHref} onClick={onInquiry}>
|
|
55
|
-
|
|
79
|
+
{resolvedTexts.inquiryAction}
|
|
56
80
|
</Alternate.Layout.TextButton>
|
|
57
|
-
|
|
58
|
-
<br />
|
|
59
|
-
접수해 주시길 바랍니다.
|
|
81
|
+
{resolvedTexts.inquirySuffix}
|
|
60
82
|
</p>
|
|
61
83
|
) : null}
|
|
62
84
|
</Alternate.Layout.Contents>
|
|
63
85
|
{/* 변경: router/history 판정은 내부에서 실행하지 않고 onPrev 또는 fallback href만 연결한다. */}
|
|
64
86
|
{onPrev ? (
|
|
65
87
|
<Alternate.Layout.Button onClick={onPrev}>
|
|
66
|
-
|
|
88
|
+
{resolvedTexts.prevAction}
|
|
67
89
|
</Alternate.Layout.Button>
|
|
68
90
|
) : (
|
|
69
91
|
<Alternate.Layout.Button as="a" href={fallbackHref}>
|
|
70
|
-
|
|
92
|
+
{resolvedTexts.prevAction}
|
|
71
93
|
</Alternate.Layout.Button>
|
|
72
94
|
)}
|
|
73
95
|
</Alternate.Layout.Container>
|
|
@@ -65,6 +65,7 @@ export interface EdgeCaseLoadingProps {
|
|
|
65
65
|
/**
|
|
66
66
|
* Edge Case Not Found Props; not-found 상태 화면 템플릿 props
|
|
67
67
|
* @property {string} [className] 최상위 edge-case container className override다.
|
|
68
|
+
* @property {EdgeCaseNotFoundTexts} [texts] not-found 기본 문구 override다.
|
|
68
69
|
* @property {string} [inquiryHref] 문의 text action을 link로 렌더링할 href다.
|
|
69
70
|
* @property {() => void} [onInquiry] 문의 text action을 button으로 렌더링할 콜백이다.
|
|
70
71
|
* @property {string} [fallbackHref] onPrev가 없을 때 사용할 fallback href다.
|
|
@@ -75,6 +76,10 @@ export interface EdgeCaseNotFoundProps {
|
|
|
75
76
|
* 최상위 edge-case container className override다.
|
|
76
77
|
*/
|
|
77
78
|
className?: string;
|
|
79
|
+
/**
|
|
80
|
+
* not-found 기본 문구 override다.
|
|
81
|
+
*/
|
|
82
|
+
texts?: EdgeCaseNotFoundTexts;
|
|
78
83
|
/**
|
|
79
84
|
* 문의 text action을 link로 렌더링할 href다.
|
|
80
85
|
*/
|
|
@@ -92,3 +97,39 @@ export interface EdgeCaseNotFoundProps {
|
|
|
92
97
|
*/
|
|
93
98
|
onPrev?: () => void;
|
|
94
99
|
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Edge Case Not Found Texts; not-found preset 문구
|
|
103
|
+
* @property {ReactNode} [title] 대표 제목 문구다.
|
|
104
|
+
* @property {ReactNode} [description] 설명 문구다.
|
|
105
|
+
* @property {ReactNode} [inquiryPrefix] 문의 action 앞 문구다.
|
|
106
|
+
* @property {ReactNode} [inquiryAction] 문의 text action 문구다.
|
|
107
|
+
* @property {ReactNode} [inquirySuffix] 문의 action 뒤 문구다.
|
|
108
|
+
* @property {ReactNode} [prevAction] 이전 페이지 action 문구다.
|
|
109
|
+
*/
|
|
110
|
+
export interface EdgeCaseNotFoundTexts {
|
|
111
|
+
/**
|
|
112
|
+
* 대표 제목 문구다.
|
|
113
|
+
*/
|
|
114
|
+
title?: ReactNode;
|
|
115
|
+
/**
|
|
116
|
+
* 설명 문구다.
|
|
117
|
+
*/
|
|
118
|
+
description?: ReactNode;
|
|
119
|
+
/**
|
|
120
|
+
* 문의 action 앞 문구다.
|
|
121
|
+
*/
|
|
122
|
+
inquiryPrefix?: ReactNode;
|
|
123
|
+
/**
|
|
124
|
+
* 문의 text action 문구다.
|
|
125
|
+
*/
|
|
126
|
+
inquiryAction?: ReactNode;
|
|
127
|
+
/**
|
|
128
|
+
* 문의 action 뒤 문구다.
|
|
129
|
+
*/
|
|
130
|
+
inquirySuffix?: ReactNode;
|
|
131
|
+
/**
|
|
132
|
+
* 이전 페이지 action 문구다.
|
|
133
|
+
*/
|
|
134
|
+
prevAction?: ReactNode;
|
|
135
|
+
}
|
|
@@ -39,17 +39,33 @@ const ServiceInquiryForm = ({
|
|
|
39
39
|
}: ServiceInquiryFormProps) => {
|
|
40
40
|
const form = useFormContext<ServiceInquiryFormValues>();
|
|
41
41
|
const selectedInquiryType = form.watch("inquiry_type");
|
|
42
|
+
const inquiryTypeOptions =
|
|
43
|
+
inquiryTypeField?.options ?? DEFAULT_INQUIRY_TYPE_OPTIONS;
|
|
44
|
+
const farmNameState = form.getFieldState("farm_name", form.formState);
|
|
45
|
+
const contactState = form.getFieldState("contact", form.formState);
|
|
46
|
+
const inquiryTypeState = form.getFieldState("inquiry_type", form.formState);
|
|
47
|
+
const textState = form.getFieldState("text", form.formState);
|
|
42
48
|
|
|
43
49
|
// 변경 설명: Modal.Dialog confirm 기본 submit과 연결되도록 form.handleSubmit 결과를 그대로 onSubmit에 바인딩한다.
|
|
44
50
|
const handleSubmit = form.handleSubmit(onSubmit);
|
|
45
51
|
|
|
46
52
|
useEffect(() => {
|
|
47
|
-
form.register("inquiry_type"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
form.register("inquiry_type", {
|
|
54
|
+
required:
|
|
55
|
+
inquiryTypeField?.required === false
|
|
56
|
+
? false
|
|
57
|
+
: (inquiryTypeField?.requiredMessage ?? "문의 유형을 선택해 주세요"),
|
|
58
|
+
});
|
|
59
|
+
if (!form.getValues("inquiry_type") && inquiryTypeOptions[0]) {
|
|
60
|
+
// 변경 설명: 문의 유형 값은 options에 주입된 표시값을 그대로 form value로 사용한다.
|
|
61
|
+
form.setValue("inquiry_type", inquiryTypeOptions[0]);
|
|
51
62
|
}
|
|
52
|
-
}, [
|
|
63
|
+
}, [
|
|
64
|
+
form,
|
|
65
|
+
inquiryTypeField?.required,
|
|
66
|
+
inquiryTypeField?.requiredMessage,
|
|
67
|
+
inquiryTypeOptions,
|
|
68
|
+
]);
|
|
53
69
|
|
|
54
70
|
return (
|
|
55
71
|
<form
|
|
@@ -64,6 +80,7 @@ const ServiceInquiryForm = ({
|
|
|
64
80
|
"service-inquiry-field-farm-name",
|
|
65
81
|
)}
|
|
66
82
|
width="full"
|
|
83
|
+
state={farmNameState.invalid ? "error" : undefined}
|
|
67
84
|
headerProps={{
|
|
68
85
|
required: farmNameField?.required ?? true,
|
|
69
86
|
...(typeof farmNameField?.label === "string"
|
|
@@ -77,14 +94,21 @@ const ServiceInquiryForm = ({
|
|
|
77
94
|
: farmNameField.label,
|
|
78
95
|
}),
|
|
79
96
|
}}
|
|
80
|
-
footer={farmNameField?.helper}
|
|
97
|
+
footer={farmNameState.error?.message ?? farmNameField?.helper}
|
|
81
98
|
>
|
|
82
99
|
<Input.Base
|
|
83
100
|
type="text"
|
|
84
101
|
block={true}
|
|
102
|
+
state={farmNameState.invalid ? "error" : undefined}
|
|
85
103
|
readOnly={farmNameField?.mode === "readonly"}
|
|
86
104
|
placeholder={farmNameField?.placeholder ?? "이름을 입력해 주세요"}
|
|
87
|
-
register={form.register("farm_name"
|
|
105
|
+
register={form.register("farm_name", {
|
|
106
|
+
required:
|
|
107
|
+
farmNameField?.required === false
|
|
108
|
+
? false
|
|
109
|
+
: (farmNameField?.requiredMessage ??
|
|
110
|
+
"이름을 입력해 주세요"),
|
|
111
|
+
})}
|
|
88
112
|
/>
|
|
89
113
|
</Form.Field.Template>
|
|
90
114
|
) : null}
|
|
@@ -96,6 +120,7 @@ const ServiceInquiryForm = ({
|
|
|
96
120
|
"service-inquiry-field-contact",
|
|
97
121
|
)}
|
|
98
122
|
width="full"
|
|
123
|
+
state={contactState.invalid ? "error" : undefined}
|
|
99
124
|
headerProps={{
|
|
100
125
|
required: contactField?.required ?? true,
|
|
101
126
|
...(typeof contactField?.label === "string"
|
|
@@ -110,6 +135,7 @@ const ServiceInquiryForm = ({
|
|
|
110
135
|
}),
|
|
111
136
|
}}
|
|
112
137
|
footer={
|
|
138
|
+
contactState.error?.message ??
|
|
113
139
|
contactField?.helper ??
|
|
114
140
|
"필요한 경우 입력하신 연락처로 연락 드립니다."
|
|
115
141
|
}
|
|
@@ -117,11 +143,18 @@ const ServiceInquiryForm = ({
|
|
|
117
143
|
<Input.Base
|
|
118
144
|
type="text"
|
|
119
145
|
block={true}
|
|
146
|
+
state={contactState.invalid ? "error" : undefined}
|
|
120
147
|
readOnly={contactField?.mode === "readonly"}
|
|
121
148
|
placeholder={
|
|
122
149
|
contactField?.placeholder ?? "연락처를 입력해 주세요"
|
|
123
150
|
}
|
|
124
|
-
register={form.register("contact"
|
|
151
|
+
register={form.register("contact", {
|
|
152
|
+
required:
|
|
153
|
+
contactField?.required === false
|
|
154
|
+
? false
|
|
155
|
+
: (contactField?.requiredMessage ??
|
|
156
|
+
"연락처를 입력해 주세요"),
|
|
157
|
+
})}
|
|
125
158
|
/>
|
|
126
159
|
</Form.Field.Template>
|
|
127
160
|
) : null}
|
|
@@ -133,6 +166,7 @@ const ServiceInquiryForm = ({
|
|
|
133
166
|
"service-inquiry-field-inquiry-type",
|
|
134
167
|
)}
|
|
135
168
|
width="full"
|
|
169
|
+
state={inquiryTypeState.invalid ? "error" : undefined}
|
|
136
170
|
headerProps={{
|
|
137
171
|
required: inquiryTypeField?.required ?? true,
|
|
138
172
|
...(typeof inquiryTypeField?.label === "string"
|
|
@@ -146,32 +180,30 @@ const ServiceInquiryForm = ({
|
|
|
146
180
|
: inquiryTypeField.label,
|
|
147
181
|
}),
|
|
148
182
|
}}
|
|
149
|
-
footer={inquiryTypeField?.helper}
|
|
183
|
+
footer={inquiryTypeState.error?.message ?? inquiryTypeField?.helper}
|
|
150
184
|
>
|
|
151
185
|
<div className="service-inquiry-type-grid">
|
|
152
|
-
{(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
},
|
|
174
|
-
)}
|
|
186
|
+
{inquiryTypeOptions.map(inquiryType => {
|
|
187
|
+
return (
|
|
188
|
+
<Chip.ClickableStyle
|
|
189
|
+
key={inquiryType}
|
|
190
|
+
className="service-inquiry-type-option"
|
|
191
|
+
chipStyle="filter"
|
|
192
|
+
fill="solid"
|
|
193
|
+
selected={selectedInquiryType === inquiryType}
|
|
194
|
+
// 변경 설명: inquiry_type은 변환 없이 선택된 label 원문을 form 값으로 유지한다.
|
|
195
|
+
onClick={() =>
|
|
196
|
+
form.setValue("inquiry_type", inquiryType, {
|
|
197
|
+
shouldDirty: true,
|
|
198
|
+
shouldTouch: true,
|
|
199
|
+
shouldValidate: true,
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
>
|
|
203
|
+
{inquiryType}
|
|
204
|
+
</Chip.ClickableStyle>
|
|
205
|
+
);
|
|
206
|
+
})}
|
|
175
207
|
</div>
|
|
176
208
|
</Form.Field.Template>
|
|
177
209
|
) : null}
|
|
@@ -183,6 +215,7 @@ const ServiceInquiryForm = ({
|
|
|
183
215
|
"service-inquiry-field-text",
|
|
184
216
|
)}
|
|
185
217
|
width="full"
|
|
218
|
+
state={textState.invalid ? "error" : undefined}
|
|
186
219
|
headerProps={{
|
|
187
220
|
required: textField?.required ?? true,
|
|
188
221
|
...(typeof textField?.label === "string"
|
|
@@ -196,17 +229,24 @@ const ServiceInquiryForm = ({
|
|
|
196
229
|
: textField.label,
|
|
197
230
|
}),
|
|
198
231
|
}}
|
|
199
|
-
footer={textField?.helper}
|
|
232
|
+
footer={textState.error?.message ?? textField?.helper}
|
|
200
233
|
>
|
|
201
234
|
<Input.TextArea
|
|
202
235
|
block={true}
|
|
236
|
+
state={textState.invalid ? "error" : undefined}
|
|
203
237
|
placeholder={
|
|
204
238
|
textField?.placeholder ??
|
|
205
239
|
"어떤 문제가 있었는지 적어주세요.\n예: 화면이 멈췄어요."
|
|
206
240
|
}
|
|
207
241
|
height={160}
|
|
208
242
|
maxLength={10000}
|
|
209
|
-
register={form.register("text"
|
|
243
|
+
register={form.register("text", {
|
|
244
|
+
required:
|
|
245
|
+
textField?.required === false
|
|
246
|
+
? false
|
|
247
|
+
: (textField?.requiredMessage ??
|
|
248
|
+
"문의 내용을 입력해 주세요"),
|
|
249
|
+
})}
|
|
210
250
|
/>
|
|
211
251
|
</Form.Field.Template>
|
|
212
252
|
) : null}
|
|
@@ -10,6 +10,7 @@ import clsx from "clsx";
|
|
|
10
10
|
* @component
|
|
11
11
|
* @param {UseOpenServiceInquiryOptions} props 문의 모달 열기 props
|
|
12
12
|
* @param {string} [props.className]
|
|
13
|
+
* @param {string} [props.ariaLabel] button aria-label
|
|
13
14
|
* @param {string} [props.stackKey] modal stack key
|
|
14
15
|
* @param {ServiceInquiryFormProps} props.formProps 문의 form props
|
|
15
16
|
* @param {Partial<DialogTemplateOptions<ServiceInquiryFormValues>>} [props.dialogOptions] 문의 모달 preset 위에 덮어쓸 dialog option
|
|
@@ -21,6 +22,7 @@ import clsx from "clsx";
|
|
|
21
22
|
*/
|
|
22
23
|
export default function ServiceInquiryOpenButton({
|
|
23
24
|
className,
|
|
25
|
+
ariaLabel = "문의하기",
|
|
24
26
|
stackKey,
|
|
25
27
|
formProps,
|
|
26
28
|
dialogOptions,
|
|
@@ -38,6 +40,7 @@ export default function ServiceInquiryOpenButton({
|
|
|
38
40
|
className={clsx("service-inquiry-open-button", className)}
|
|
39
41
|
priority="tertiary"
|
|
40
42
|
size="large"
|
|
43
|
+
aria-label={ariaLabel}
|
|
41
44
|
// 변경 설명: 기본 제공 버튼은 고정 원형 `?` 버튼 사양으로 렌더링한다.
|
|
42
45
|
onClick={openServiceInquiry}
|
|
43
46
|
>
|
|
@@ -36,6 +36,11 @@
|
|
|
36
36
|
font-size: var(--service-inquiry-type-font-size);
|
|
37
37
|
font-weight: var(--service-inquiry-type-font-weight-default);
|
|
38
38
|
line-height: 1.4;
|
|
39
|
+
|
|
40
|
+
:where(.chip-label) {
|
|
41
|
+
// 변경 설명: Chip 기본 label은 ellipsis를 위해 overflow hidden + line-height 1em을 갖기 때문에, service-inquiry의 큰 영문 라벨 descender가 잘리지 않도록 line-height만 모듈 범위에서 보정한다.
|
|
42
|
+
line-height: 1.4;
|
|
43
|
+
}
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
.service-inquiry-type-option:where([data-selected="true"]) {
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Service Inquiry Form; 문의 유형 값
|
|
3
|
-
* @typedef {
|
|
3
|
+
* @typedef {string} ServiceInquiryType
|
|
4
4
|
*/
|
|
5
|
-
export type ServiceInquiryType =
|
|
6
|
-
| "접속이 안 돼요"
|
|
7
|
-
| "화면이 멈춰요"
|
|
8
|
-
| "데이터가 안나와요"
|
|
9
|
-
| "기타";
|
|
5
|
+
export type ServiceInquiryType = string;
|
|
10
6
|
|
|
11
7
|
/**
|
|
12
8
|
* Service Inquiry Form; 사용자 입력값
|
|
@@ -26,6 +26,7 @@ export type ServiceInquiryFieldMode = "editable" | "readonly";
|
|
|
26
26
|
* @property {ReactNode} [label] field label
|
|
27
27
|
* @property {ReactNode} [helper] field helper
|
|
28
28
|
* @property {boolean} [required] required 표시 여부
|
|
29
|
+
* @property {string} [requiredMessage] required validation message
|
|
29
30
|
*/
|
|
30
31
|
export interface ServiceInquiryFieldBaseProps {
|
|
31
32
|
/**
|
|
@@ -40,6 +41,10 @@ export interface ServiceInquiryFieldBaseProps {
|
|
|
40
41
|
* required 표시 여부
|
|
41
42
|
*/
|
|
42
43
|
required?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* required validation message
|
|
46
|
+
*/
|
|
47
|
+
requiredMessage?: string;
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
/**
|
|
@@ -163,6 +168,7 @@ export interface UseOpenServiceInquiryOptions extends ServiceInquiryCreateModalO
|
|
|
163
168
|
/**
|
|
164
169
|
* Service Inquiry Button props; 버튼 옵션
|
|
165
170
|
* @property {string} [className]
|
|
171
|
+
* @property {string} [ariaLabel] button aria-label
|
|
166
172
|
* @property {string} [stackKey] modal stack key
|
|
167
173
|
* @property {ServiceInquiryFormProps} formProps 문의 form props
|
|
168
174
|
* @property {Partial<DialogTemplateOptions<ServiceInquiryFormValues>>} [dialogOptions] 문의 모달 preset 위에 덮어쓸 dialog option
|
|
@@ -173,6 +179,10 @@ export interface ServiceInquiryOpenButtonProps extends UseOpenServiceInquiryOpti
|
|
|
173
179
|
* button className
|
|
174
180
|
*/
|
|
175
181
|
className?: string;
|
|
182
|
+
/**
|
|
183
|
+
* button aria-label
|
|
184
|
+
*/
|
|
185
|
+
ariaLabel?: string;
|
|
176
186
|
}
|
|
177
187
|
|
|
178
188
|
/**
|