@uniai-fe/uds-templates 0.0.8 → 0.0.10
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/dist/styles.css +280 -327
- package/package.json +11 -8
- package/src/components/auth/container/AuthContainer.tsx +24 -0
- package/src/components/auth/container/index.scss +52 -0
- package/src/components/auth/container/index.tsx +4 -0
- package/src/components/auth/container/types.ts +8 -0
- package/src/components/auth/index.tsx +20 -0
- package/src/components/auth/login/data/valid-options.ts +8 -0
- package/src/components/auth/login/hooks/index.ts +6 -0
- package/src/components/auth/login/hooks/useAuthLoginForm.ts +80 -0
- package/src/components/auth/login/index.scss +1 -0
- package/src/components/auth/login/index.tsx +18 -0
- package/src/components/auth/login/markup/Container.tsx +34 -0
- package/src/components/auth/login/markup/FormField.tsx +115 -0
- package/src/components/auth/login/markup/LinkButtons.tsx +40 -0
- package/src/components/auth/login/styles/login.scss +52 -0
- package/src/components/auth/login/types/form.ts +0 -0
- package/src/components/auth/login/types/hooks.ts +82 -0
- package/src/components/auth/login/types/props.ts +95 -0
- package/src/components/auth/login/types.ts +2 -0
- package/src/index.tsx +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniai-fe/uds-templates",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.10",
|
|
4
4
|
"description": "UNIAI Design System; UI Templates Package",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"publishConfig": {
|
|
13
13
|
"access": "public"
|
|
14
14
|
},
|
|
15
|
-
"packageManager": "pnpm@10.
|
|
15
|
+
"packageManager": "pnpm@10.26.1",
|
|
16
16
|
"engines": {
|
|
17
17
|
"node": ">=24",
|
|
18
18
|
"pnpm": ">=10"
|
|
@@ -51,15 +51,15 @@
|
|
|
51
51
|
"@uniai-fe/uds-foundation": "^0.0.1",
|
|
52
52
|
"@uniai-fe/uds-primitives": "^0.0.2",
|
|
53
53
|
"react": ">= 19",
|
|
54
|
-
"react-dom": ">= 19"
|
|
54
|
+
"react-dom": ">= 19",
|
|
55
|
+
"react-hook-form": ">= 7",
|
|
56
|
+
"next": "^15"
|
|
55
57
|
},
|
|
56
58
|
"dependencies": {
|
|
57
59
|
"clsx": "^2.1.1",
|
|
58
60
|
"dayjs": "^1.11.19"
|
|
59
61
|
},
|
|
60
62
|
"devDependencies": {
|
|
61
|
-
"@uniai-fe/uds-foundation": "workspace:*",
|
|
62
|
-
"@uniai-fe/uds-primitives": "workspace:*",
|
|
63
63
|
"@svgr/webpack": "^8.1.0",
|
|
64
64
|
"@types/node": "^24.10.2",
|
|
65
65
|
"@types/react": "^19.2.7",
|
|
@@ -67,10 +67,13 @@
|
|
|
67
67
|
"@uniai-fe/eslint-config": "workspace:*",
|
|
68
68
|
"@uniai-fe/next-devkit": "workspace:*",
|
|
69
69
|
"@uniai-fe/tsconfig": "workspace:*",
|
|
70
|
-
"
|
|
70
|
+
"@uniai-fe/uds-foundation": "workspace:*",
|
|
71
|
+
"@uniai-fe/uds-primitives": "workspace:*",
|
|
72
|
+
"eslint": "^9.39.2",
|
|
73
|
+
"next": "^15.5.9",
|
|
71
74
|
"prettier": "^3.7.4",
|
|
72
|
-
"
|
|
73
|
-
"
|
|
75
|
+
"react-hook-form": "^7.69.0",
|
|
76
|
+
"sass": "^1.97.1",
|
|
74
77
|
"typescript": "~5.9.3"
|
|
75
78
|
}
|
|
76
79
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import clsx from "clsx";
|
|
2
|
+
import type { AuthContainerProps } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 로그인/회원가입 화면을 감싸는 공용 컨테이너
|
|
6
|
+
* @component
|
|
7
|
+
*/
|
|
8
|
+
export function AuthContainer({
|
|
9
|
+
className,
|
|
10
|
+
header,
|
|
11
|
+
children,
|
|
12
|
+
footer,
|
|
13
|
+
}: AuthContainerProps) {
|
|
14
|
+
// 단일 열 레이아웃을 강제해 모바일·PC 환경에서 동일한 시각적 구조를 보장한다.
|
|
15
|
+
return (
|
|
16
|
+
<section className={clsx("auth-container", className)}>
|
|
17
|
+
<div className="auth-container__inner">
|
|
18
|
+
{header && <header className="auth-container__header">{header}</header>}
|
|
19
|
+
<div className="auth-container__body">{children}</div>
|
|
20
|
+
{footer && <footer className="auth-container__footer">{footer}</footer>}
|
|
21
|
+
</div>
|
|
22
|
+
</section>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--auth-container-max-width: 335px;
|
|
3
|
+
--auth-container-gap: var(--spacing-padding-7, 28px);
|
|
4
|
+
--auth-container-padding-inline: var(--spacing-padding-6, 24px);
|
|
5
|
+
--auth-container-padding-top: calc(
|
|
6
|
+
var(--spacing-padding-9, 32px) + env(safe-area-inset-top, 0px)
|
|
7
|
+
);
|
|
8
|
+
--auth-container-padding-bottom: var(--spacing-padding-10, 40px);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.auth-container {
|
|
12
|
+
min-height: min(100svh, 100dvh);
|
|
13
|
+
padding: var(--auth-container-padding-top)
|
|
14
|
+
var(--auth-container-padding-inline)
|
|
15
|
+
calc(
|
|
16
|
+
var(--auth-container-padding-bottom) + env(safe-area-inset-bottom, 0px)
|
|
17
|
+
);
|
|
18
|
+
box-sizing: border-box;
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
align-items: center;
|
|
22
|
+
background-color: var(--color-common-100, #ffffff);
|
|
23
|
+
color: inherit;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.auth-container__inner {
|
|
27
|
+
width: min(100%, var(--auth-container-max-width, 335px));
|
|
28
|
+
display: flex;
|
|
29
|
+
flex-direction: column;
|
|
30
|
+
gap: var(--auth-container-gap);
|
|
31
|
+
margin-block: auto;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.auth-container__header {
|
|
35
|
+
width: 100%;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.auth-container__body {
|
|
39
|
+
display: flex;
|
|
40
|
+
flex-direction: column;
|
|
41
|
+
gap: var(--spacing-padding-6, 24px);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.auth-container__footer {
|
|
45
|
+
display: flex;
|
|
46
|
+
flex-direction: column;
|
|
47
|
+
gap: var(--spacing-padding-4, 16px);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.auth-container__footer {
|
|
51
|
+
margin-top: auto;
|
|
52
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import "./login/index.scss";
|
|
2
|
+
|
|
3
|
+
import { AuthContainer } from "./container";
|
|
4
|
+
import { AuthLogin } from "./login";
|
|
5
|
+
|
|
6
|
+
export const Auth = {
|
|
7
|
+
Container: AuthContainer,
|
|
8
|
+
Login: AuthLogin,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type {
|
|
12
|
+
AuthLoginProps,
|
|
13
|
+
AuthLoginFieldOptions,
|
|
14
|
+
AuthLoginFieldConfig,
|
|
15
|
+
AuthLoginFields,
|
|
16
|
+
AuthLoginLinkOptions,
|
|
17
|
+
AuthLoginFormValues,
|
|
18
|
+
UseAuthLoginFormOptions,
|
|
19
|
+
UseAuthLoginFormReturn,
|
|
20
|
+
} from "./login";
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { useWatch } from "react-hook-form";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import type {
|
|
5
|
+
AuthLoginFields,
|
|
6
|
+
AuthLoginFormValues,
|
|
7
|
+
UseAuthLoginFormOptions,
|
|
8
|
+
UseAuthLoginFormReturn,
|
|
9
|
+
} from "../types";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 로그인 폼 훅
|
|
13
|
+
* @hook
|
|
14
|
+
* @template TFields
|
|
15
|
+
* @param {UseAuthLoginFormOptions<TFields>} options 훅 옵션
|
|
16
|
+
* @desc
|
|
17
|
+
* - 1) form init → 2) register merge → 3) helper/state → 4) submit 순서
|
|
18
|
+
*/
|
|
19
|
+
export function useAuthLoginForm<
|
|
20
|
+
TFields extends AuthLoginFields = AuthLoginFields,
|
|
21
|
+
>({
|
|
22
|
+
fields,
|
|
23
|
+
form,
|
|
24
|
+
onLogin,
|
|
25
|
+
}: UseAuthLoginFormOptions<TFields>): UseAuthLoginFormReturn<TFields> {
|
|
26
|
+
/** 1) form init — useForm은 FormField에서 생성 후 주입된다. */
|
|
27
|
+
const values = useWatch({
|
|
28
|
+
control: form.control,
|
|
29
|
+
}) as AuthLoginFormValues | undefined;
|
|
30
|
+
|
|
31
|
+
/** 2) register merge — 필드별 register 함수를 생성한다. */
|
|
32
|
+
const register = useMemo(() => {
|
|
33
|
+
return (Object.keys(fields) as Array<keyof TFields>).reduce(
|
|
34
|
+
(acc, fieldKey) => {
|
|
35
|
+
const config = fields[fieldKey];
|
|
36
|
+
const fieldName = config.attr?.name ?? String(fieldKey);
|
|
37
|
+
acc[fieldKey] = form.register(fieldName);
|
|
38
|
+
return acc;
|
|
39
|
+
},
|
|
40
|
+
{} as UseAuthLoginFormReturn<TFields>["register"],
|
|
41
|
+
);
|
|
42
|
+
}, [fields, form]);
|
|
43
|
+
|
|
44
|
+
/** 3) helper/state — helper 메시지와 버튼 상태를 계산한다. */
|
|
45
|
+
const helpers = useMemo(() => {
|
|
46
|
+
return (Object.keys(fields) as Array<keyof TFields>).reduce(
|
|
47
|
+
(acc, fieldKey) => {
|
|
48
|
+
const config = fields[fieldKey];
|
|
49
|
+
const fieldName = config.attr?.name ?? String(fieldKey);
|
|
50
|
+
const state = form.getFieldState(fieldName);
|
|
51
|
+
const helperText =
|
|
52
|
+
(state.error?.message as ReactNode | undefined) ?? config.helper;
|
|
53
|
+
acc[fieldKey] = {
|
|
54
|
+
text: helperText,
|
|
55
|
+
state: state.invalid ? "error" : undefined,
|
|
56
|
+
};
|
|
57
|
+
return acc;
|
|
58
|
+
},
|
|
59
|
+
{} as UseAuthLoginFormReturn<TFields>["helpers"],
|
|
60
|
+
);
|
|
61
|
+
}, [fields, form]);
|
|
62
|
+
|
|
63
|
+
const trimmedFilled =
|
|
64
|
+
values &&
|
|
65
|
+
Object.values(values).every(value =>
|
|
66
|
+
typeof value === "string" ? value.trim().length > 0 : false,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const disabled = form.formState.isSubmitting || !trimmedFilled;
|
|
70
|
+
|
|
71
|
+
/** 4) submit — onLogin을 handleSubmit과 결합한다. */
|
|
72
|
+
const onSubmit = form.handleSubmit(onLogin);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
register,
|
|
76
|
+
onSubmit,
|
|
77
|
+
disabled,
|
|
78
|
+
helpers,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@use "./styles/login.scss";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import "./index.scss";
|
|
2
|
+
|
|
3
|
+
export type {
|
|
4
|
+
AuthLoginProps,
|
|
5
|
+
AuthLoginFieldOptions,
|
|
6
|
+
AuthLoginFieldConfig,
|
|
7
|
+
AuthLoginFields,
|
|
8
|
+
AuthLoginLinkOptions,
|
|
9
|
+
} from "./types";
|
|
10
|
+
export * from "./hooks";
|
|
11
|
+
|
|
12
|
+
import AuthLoginContainer from "./markup/Container";
|
|
13
|
+
import AuthLoginFormField from "./markup/FormField";
|
|
14
|
+
|
|
15
|
+
export const AuthLogin = {
|
|
16
|
+
Container: AuthLoginContainer,
|
|
17
|
+
FormField: AuthLoginFormField,
|
|
18
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import clsx from "clsx";
|
|
2
|
+
import { AuthContainer } from "../../container";
|
|
3
|
+
import AuthLoginFormField from "./FormField";
|
|
4
|
+
import AuthLoginLinkButtons from "./LinkButtons";
|
|
5
|
+
import type { AuthLoginProps } from "../types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 로그인 화면 템플릿 — AuthContainer 위에 아이디/비밀번호 입력과 CTA를 배치한다.
|
|
9
|
+
* @component
|
|
10
|
+
*/
|
|
11
|
+
export default function AuthLoginContainer({
|
|
12
|
+
className,
|
|
13
|
+
header,
|
|
14
|
+
footer,
|
|
15
|
+
linkOptions,
|
|
16
|
+
fieldOptions,
|
|
17
|
+
}: AuthLoginProps) {
|
|
18
|
+
return (
|
|
19
|
+
<>
|
|
20
|
+
<AuthContainer
|
|
21
|
+
className={clsx("auth-login-container", className)}
|
|
22
|
+
header={header}
|
|
23
|
+
footer={footer}
|
|
24
|
+
>
|
|
25
|
+
<AuthLoginFormField {...fieldOptions} />
|
|
26
|
+
<AuthLoginLinkButtons
|
|
27
|
+
hrefFindId={linkOptions.find.id}
|
|
28
|
+
hrefFindPassword={linkOptions.find.password}
|
|
29
|
+
hrefSignup={linkOptions.signup}
|
|
30
|
+
/>
|
|
31
|
+
</AuthContainer>
|
|
32
|
+
</>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import {
|
|
4
|
+
Button,
|
|
5
|
+
Input,
|
|
6
|
+
PasswordInput,
|
|
7
|
+
type InputPasswordProps,
|
|
8
|
+
type InputProps,
|
|
9
|
+
type InputState,
|
|
10
|
+
} from "@uniai-fe/uds-primitives";
|
|
11
|
+
import type {
|
|
12
|
+
AuthLoginFieldConfig,
|
|
13
|
+
AuthLoginFieldOptions,
|
|
14
|
+
AuthLoginFields,
|
|
15
|
+
AuthLoginFormValues,
|
|
16
|
+
} from "../types";
|
|
17
|
+
import { useAuthLoginForm } from "../hooks";
|
|
18
|
+
import { useForm } from "react-hook-form";
|
|
19
|
+
|
|
20
|
+
type HelperState = {
|
|
21
|
+
/**
|
|
22
|
+
* helper 텍스트
|
|
23
|
+
*/
|
|
24
|
+
text?: ReactNode;
|
|
25
|
+
/**
|
|
26
|
+
* helper 상태
|
|
27
|
+
*/
|
|
28
|
+
state?: InputState;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const composeFieldProps = <TProps extends InputProps>(
|
|
32
|
+
config: AuthLoginFieldConfig<TProps>,
|
|
33
|
+
helper?: HelperState,
|
|
34
|
+
): TProps => {
|
|
35
|
+
const baseProps = {
|
|
36
|
+
...(config.attr ?? {}),
|
|
37
|
+
...(config.props ?? ({} as TProps)),
|
|
38
|
+
} as TProps;
|
|
39
|
+
const mergedLabelProps = config.labelClassName
|
|
40
|
+
? {
|
|
41
|
+
...baseProps.labelProps,
|
|
42
|
+
className: config.labelClassName,
|
|
43
|
+
}
|
|
44
|
+
: baseProps.labelProps;
|
|
45
|
+
const mergedHelperProps = config.helperClassName
|
|
46
|
+
? {
|
|
47
|
+
...baseProps.helperProps,
|
|
48
|
+
className: config.helperClassName,
|
|
49
|
+
}
|
|
50
|
+
: baseProps.helperProps;
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
...baseProps,
|
|
54
|
+
label: config.label ?? baseProps.label,
|
|
55
|
+
helper: helper?.text ?? config.helper ?? baseProps.helper,
|
|
56
|
+
state: helper?.state ?? baseProps.state,
|
|
57
|
+
block: config.block ?? baseProps.block ?? true,
|
|
58
|
+
className: config.className ?? baseProps.className,
|
|
59
|
+
labelProps: mergedLabelProps,
|
|
60
|
+
helperProps: mergedHelperProps,
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export default function AuthLoginFormField<
|
|
65
|
+
TFields extends AuthLoginFields = AuthLoginFields,
|
|
66
|
+
>({ fields, formAttr, onLogin }: AuthLoginFieldOptions<TFields>) {
|
|
67
|
+
const defaultValues = useMemo(
|
|
68
|
+
() =>
|
|
69
|
+
(Object.keys(fields) as Array<keyof TFields>).reduce((acc, fieldKey) => {
|
|
70
|
+
const config = fields[fieldKey];
|
|
71
|
+
const fieldName = config.attr?.name ?? String(fieldKey);
|
|
72
|
+
acc[fieldName] = "";
|
|
73
|
+
return acc;
|
|
74
|
+
}, {} as AuthLoginFormValues),
|
|
75
|
+
[fields],
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const form = useForm<AuthLoginFormValues>({
|
|
79
|
+
mode: "onChange",
|
|
80
|
+
defaultValues,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const { register, helpers, onSubmit, disabled } = useAuthLoginForm<TFields>({
|
|
84
|
+
fields,
|
|
85
|
+
form,
|
|
86
|
+
onLogin,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<form className="auth-login-form" {...formAttr} onSubmit={onSubmit}>
|
|
91
|
+
<div className="auth-login-fields">
|
|
92
|
+
<Input
|
|
93
|
+
{...composeFieldProps<InputProps>(fields.id, helpers.id)}
|
|
94
|
+
register={register.id}
|
|
95
|
+
/>
|
|
96
|
+
<PasswordInput
|
|
97
|
+
{...composeFieldProps<InputPasswordProps>(
|
|
98
|
+
fields.password,
|
|
99
|
+
helpers.password,
|
|
100
|
+
)}
|
|
101
|
+
register={register.password}
|
|
102
|
+
/>
|
|
103
|
+
<Button.Default
|
|
104
|
+
type="submit"
|
|
105
|
+
scale="solid-xlarge"
|
|
106
|
+
priority="primary"
|
|
107
|
+
block
|
|
108
|
+
disabled={disabled}
|
|
109
|
+
>
|
|
110
|
+
로그인
|
|
111
|
+
</Button.Default>
|
|
112
|
+
</div>
|
|
113
|
+
</form>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { Button, Divider } from "@uniai-fe/uds-primitives";
|
|
3
|
+
|
|
4
|
+
export default function AuthLoginLinkButtons({
|
|
5
|
+
hrefFindId,
|
|
6
|
+
hrefFindPassword,
|
|
7
|
+
hrefSignup,
|
|
8
|
+
}: {
|
|
9
|
+
hrefFindId: string;
|
|
10
|
+
hrefFindPassword: string;
|
|
11
|
+
hrefSignup: string;
|
|
12
|
+
}) {
|
|
13
|
+
return (
|
|
14
|
+
<div className="auth-login-util-container">
|
|
15
|
+
<div className="auth-login-find-account">
|
|
16
|
+
<Link href={hrefFindId} className="auth-login-find-account-button">
|
|
17
|
+
<span>아이디 찾기</span>
|
|
18
|
+
</Link>
|
|
19
|
+
<Divider />
|
|
20
|
+
<Link
|
|
21
|
+
href={hrefFindPassword}
|
|
22
|
+
className="auth-login-find-account-button"
|
|
23
|
+
>
|
|
24
|
+
<span>비밀번호 찾기</span>
|
|
25
|
+
</Link>
|
|
26
|
+
</div>
|
|
27
|
+
<div className="auth-login-signup">
|
|
28
|
+
<Button.Rounded
|
|
29
|
+
as={Link}
|
|
30
|
+
href={hrefSignup}
|
|
31
|
+
className="auth-login-signup-button"
|
|
32
|
+
priority="tertiary"
|
|
33
|
+
size="small"
|
|
34
|
+
>
|
|
35
|
+
회원가입
|
|
36
|
+
</Button.Rounded>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
.auth-login-form {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: var(--spacing-padding-7, 28px);
|
|
5
|
+
margin-top: var(--spacing-padding-10, 40px);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.auth-login-fields {
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
gap: var(--spacing-padding-5, 20px);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.auth-login-util-container {
|
|
15
|
+
margin-top: 80px;
|
|
16
|
+
display: flex;
|
|
17
|
+
flex-direction: column;
|
|
18
|
+
gap: var(--spacing-padding-6, 24px);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.auth-login-find-account {
|
|
22
|
+
display: flex;
|
|
23
|
+
justify-content: center;
|
|
24
|
+
align-items: center;
|
|
25
|
+
|
|
26
|
+
--divider-height: 13px;
|
|
27
|
+
--divider-color: var(--color-label-neutral);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.auth-login-find-account-button {
|
|
31
|
+
color: var(--color-label-standard);
|
|
32
|
+
font-size: 13px;
|
|
33
|
+
line-height: 1em;
|
|
34
|
+
font-weight: 400;
|
|
35
|
+
padding-inline: var(--spacing-padding-1, 4px);
|
|
36
|
+
|
|
37
|
+
&:hover {
|
|
38
|
+
color: var(--color-primary-50, #2563eb);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.auth-login-signup {
|
|
43
|
+
display: flex;
|
|
44
|
+
justify-content: center;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.auth-login-signup-button {
|
|
48
|
+
text-decoration: none;
|
|
49
|
+
min-width: 160px;
|
|
50
|
+
justify-content: center;
|
|
51
|
+
--theme-button-font-label-medium-size: var(--font-caption-medium-size);
|
|
52
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SubmitHandler,
|
|
3
|
+
UseFormRegisterReturn,
|
|
4
|
+
UseFormReturn,
|
|
5
|
+
} from "react-hook-form";
|
|
6
|
+
import type { ReactNode } from "react";
|
|
7
|
+
import type { AuthLoginFields } from "./props";
|
|
8
|
+
import type { InputState } from "@uniai-fe/uds-primitives";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 로그인 폼 값 타입
|
|
12
|
+
* @typedef {AuthLoginFormValues}
|
|
13
|
+
* @desc
|
|
14
|
+
* - 모든 로그인 필드 입력값을 문자열 레코드로 취급한다.
|
|
15
|
+
*/
|
|
16
|
+
export type AuthLoginFormValues = Record<string, string>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 로그인 훅 옵션; useAuthLoginForm 설정
|
|
20
|
+
* @typedef {UseAuthLoginFormOptions}
|
|
21
|
+
* @desc
|
|
22
|
+
* - 필드 정의, useForm 반환 객체, 로그인 핸들러를 전달한다.
|
|
23
|
+
* - 필드 타입은 제네릭으로 확장 가능하다.
|
|
24
|
+
* @property {AuthLoginFields<TFields>} fields 로그인 입력 필드 설정
|
|
25
|
+
* @property {UseFormReturn<Record<string, string>>} form RHF useForm 반환 객체
|
|
26
|
+
* @property {SubmitHandler<Record<string, string>>} onLogin 제출 콜백
|
|
27
|
+
*/
|
|
28
|
+
export type UseAuthLoginFormOptions<
|
|
29
|
+
TFields extends AuthLoginFields = AuthLoginFields,
|
|
30
|
+
> = {
|
|
31
|
+
/**
|
|
32
|
+
* 로그인 필드 설정
|
|
33
|
+
* - 필수 id/password + 확장 필드를 포함한다.
|
|
34
|
+
*/
|
|
35
|
+
fields: TFields;
|
|
36
|
+
/**
|
|
37
|
+
* RHF useForm 반환 객체
|
|
38
|
+
* - register/onSubmit 등을 전달한다.
|
|
39
|
+
*/
|
|
40
|
+
form: UseFormReturn<AuthLoginFormValues>;
|
|
41
|
+
/**
|
|
42
|
+
* 로그인 제출 핸들러
|
|
43
|
+
* - form.handleSubmit으로 감싼 뒤 동작한다.
|
|
44
|
+
*/
|
|
45
|
+
onLogin: SubmitHandler<AuthLoginFormValues>;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 로그인 훅 반환타입; useAuthLoginForm 결과
|
|
50
|
+
* @typedef {UseAuthLoginFormReturn}
|
|
51
|
+
* @desc
|
|
52
|
+
* - register/onSubmit/disabled/helpers 네 축만 노출한다.
|
|
53
|
+
* - 필드는 제네릭 기반으로 key를 유지한다.
|
|
54
|
+
* @property {Record<keyof TFields, UseFormRegisterReturn>} register 필드별 register 함수
|
|
55
|
+
* @property {ReturnType<UseFormReturn["handleSubmit"]>} onSubmit 로그인 제출 핸들러
|
|
56
|
+
* @property {boolean} disabled 버튼 disabled 여부
|
|
57
|
+
* @property {Record<keyof TFields, { text?: string; state?: string }>} helpers 필드별 helper 상태
|
|
58
|
+
*/
|
|
59
|
+
export type UseAuthLoginFormReturn<
|
|
60
|
+
TFields extends AuthLoginFields = AuthLoginFields,
|
|
61
|
+
> = {
|
|
62
|
+
/**
|
|
63
|
+
* 필드별 register 함수
|
|
64
|
+
* - id/password 및 확장 필드를 동일 구조로 제공한다.
|
|
65
|
+
*/
|
|
66
|
+
register: Record<keyof TFields, UseFormRegisterReturn>;
|
|
67
|
+
/**
|
|
68
|
+
* 로그인 제출 핸들러
|
|
69
|
+
* - form.handleSubmit 결과를 그대로 노출한다.
|
|
70
|
+
*/
|
|
71
|
+
onSubmit: ReturnType<UseFormReturn["handleSubmit"]>;
|
|
72
|
+
/**
|
|
73
|
+
* 버튼 disabled 여부
|
|
74
|
+
* - trim 여부, isSubmitting 등 내부 로직으로 계산한다.
|
|
75
|
+
*/
|
|
76
|
+
disabled: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* 필드별 helper 상태
|
|
79
|
+
* - text/state 조합으로 error/info 메시지를 표현한다.
|
|
80
|
+
*/
|
|
81
|
+
helpers: Record<keyof TFields, { text?: ReactNode; state?: InputState }>;
|
|
82
|
+
};
|