@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniai-fe/uds-templates",
3
- "version": "0.0.8",
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.25.0",
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
- "eslint": "^9.39.1",
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
- "sass": "^1.96.0",
73
- "react-hook-form": "^7.68.0",
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,4 @@
1
+ import "./index.scss";
2
+
3
+ export { AuthContainer } from "./AuthContainer";
4
+ export type { AuthContainerProps } from "./types";
@@ -0,0 +1,8 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ export type AuthContainerProps = {
4
+ className?: string;
5
+ header?: ReactNode;
6
+ children: ReactNode;
7
+ footer?: ReactNode;
8
+ };
@@ -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,8 @@
1
+ export const authLoginValidOptions = {
2
+ id: {
3
+ required: "아이디를 입력해 주세요.",
4
+ },
5
+ password: {
6
+ required: "비밀번호를 입력해 주세요.",
7
+ },
8
+ };
@@ -0,0 +1,6 @@
1
+ export { useAuthLoginForm } from "./useAuthLoginForm";
2
+ export type {
3
+ AuthLoginFormValues,
4
+ UseAuthLoginFormOptions,
5
+ UseAuthLoginFormReturn,
6
+ } from "../types/hooks";
@@ -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
+ };