From 11bf14d0452053f953616c0a840b2d51bd8c9bca Mon Sep 17 00:00:00 2001 From: ahcgnoej Date: Tue, 17 Feb 2026 03:39:41 +0900 Subject: [PATCH 1/4] =?UTF-8?q?[FEAT/#5]=20=ED=85=8D=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EA=B3=B5=ED=86=B5=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- babel.config.js | 13 +++ package.json | 3 + src/shared/ui/FormField/FormField.tsx | 89 +++++++++++++++++++++ src/shared/ui/FormField/Input.tsx | 110 ++++++++++++++++++++++++++ src/shared/ui/FormField/index.ts | 3 + src/shared/ui/FormField/types.ts | 54 +++++++++++++ yarn.lock | 22 ++++++ 7 files changed, 294 insertions(+) create mode 100644 src/shared/ui/FormField/FormField.tsx create mode 100644 src/shared/ui/FormField/Input.tsx create mode 100644 src/shared/ui/FormField/index.ts create mode 100644 src/shared/ui/FormField/types.ts diff --git a/babel.config.js b/babel.config.js index 23741e3..428f9fe 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,3 +1,5 @@ +const path = require("path"); + module.exports = (api) => { api.cache(true); return { @@ -5,5 +7,16 @@ module.exports = (api) => { ["babel-preset-expo", { jsxImportSource: "nativewind" }], "nativewind/babel", ], + plugins: [ + [ + "module-resolver", + { + root: [path.resolve(__dirname, "src")], + alias: { + "@": path.resolve(__dirname, "src"), + }, + }, + ], + ], }; }; diff --git a/package.json b/package.json index 8e97b55..6bf0fd8 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ }, "dependencies": { "@expo/vector-icons": "^15.0.3", + "@hookform/resolvers": "^5.2.2", "@react-navigation/bottom-tabs": "^7.4.0", "@react-navigation/elements": "^2.6.3", "@react-navigation/native": "^7.1.8", @@ -33,6 +34,7 @@ "nativewind": "^4.2.1", "react": "19.1.0", "react-dom": "19.1.0", + "react-hook-form": "^7.71.1", "react-native": "0.81.5", "react-native-gesture-handler": "~2.28.0", "react-native-reanimated": "~4.1.1", @@ -40,6 +42,7 @@ "react-native-screens": "~4.16.0", "react-native-web": "~0.21.0", "react-native-worklets": "0.5.1", + "zod": "^4.3.6", "zustand": "^5.0.11" }, "devDependencies": { diff --git a/src/shared/ui/FormField/FormField.tsx b/src/shared/ui/FormField/FormField.tsx new file mode 100644 index 0000000..364dcc9 --- /dev/null +++ b/src/shared/ui/FormField/FormField.tsx @@ -0,0 +1,89 @@ +import { colorTokens } from "@/shared/styles/token"; +import { useState } from "react"; +import { Controller, type FieldValues } from "react-hook-form"; +import { Text, View } from "react-native"; +import { FormFieldInput } from "./Input"; +import type { FormFieldProps, FormFieldStatus } from "./types"; + +/** + * 현재 상태를 계산하는 헬퍼 함수 + */ +function getStatus(hasError: boolean, isFocused: boolean): FormFieldStatus { + if (hasError) return "error"; // 에러 발생 시 status만 'error'로 전달 + if (isFocused) return "focus"; + return "default"; +} + +export function FormField({ + control, + name, + label, + appearance = "filled", + rightElement, + helperText, + labelColor, + helperTextColor, + ...inputProps +}: FormFieldProps) { + const [isFocused, setIsFocused] = useState(false); + + return ( + { + const hasError = Boolean(fieldState.error); + const status = getStatus(hasError, isFocused); + + return ( + + {label && ( + + {label} + + )} + + { + setIsFocused(true); + inputProps.onFocus?.(e); + }} + onBlur={(e) => { + setIsFocused(false); + field.onBlur(); + inputProps.onBlur?.(e); + }} + rightElement={rightElement} + /> + + {helperText && ( + + {helperText} + + )} + + ); + }} + /> + ); +} +//todo: helperText 로그인/회원가입 구현시 확장 필요 diff --git a/src/shared/ui/FormField/Input.tsx b/src/shared/ui/FormField/Input.tsx new file mode 100644 index 0000000..0964da3 --- /dev/null +++ b/src/shared/ui/FormField/Input.tsx @@ -0,0 +1,110 @@ +import { colorTokens } from "@/shared/styles/token"; +import { TextInput, View } from "react-native"; +import type { + FormFieldAppearance, + FormFieldInputProps, + FormFieldStatus, +} from "./types"; + +const APPEARANCE_STYLES: Record< + FormFieldAppearance, + { backgroundColor: string; borderColor: string } +> = { + filled: { + backgroundColor: colorTokens.neutral, // 회색 배경 + borderColor: colorTokens.neutral, + }, + outlined: { + backgroundColor: colorTokens.canvas, // 흰색 배경 (또는 transparent) + borderColor: colorTokens.contentSecondary, // 회색 테두리 + }, +}; + +/** + * 상태별 색상 정의 + */ +const STATUS_COLORS: Record< + FormFieldStatus, + { borderColor?: string; textColor: string } +> = { + default: { + borderColor: undefined, // appearance 따름 + textColor: colorTokens.contentPrimary, + }, + focus: { + borderColor: colorTokens.primary, + textColor: colorTokens.contentPrimary, + }, + error: { + borderColor: colorTokens.danger, + textColor: colorTokens.contentPrimary, + }, +}; + +export function FormFieldInput({ + appearance = "filled", + status, + fontSize = 17, + + inputBackgroundColor, + inputBorderColor, + inputTextColor, + + inputStyle, + rightElement, + + multiline, + ...props +}: FormFieldInputProps) { + const baseStyle = APPEARANCE_STYLES[appearance]; + const statusStyle = STATUS_COLORS[status]; + + const finalBackgroundColor = inputBackgroundColor + ? colorTokens[inputBackgroundColor] + : baseStyle.backgroundColor; + + const finalBorderColor = inputBorderColor + ? colorTokens[inputBorderColor] + : (statusStyle.borderColor ?? baseStyle.borderColor); + + const finalTextColor = inputTextColor + ? colorTokens[inputTextColor] + : statusStyle.textColor; + + return ( + + + + {rightElement && {rightElement}} + + ); +} +//todo: rightElement 로그인/회원가입 구현시 확장 필요 diff --git a/src/shared/ui/FormField/index.ts b/src/shared/ui/FormField/index.ts new file mode 100644 index 0000000..40eafce --- /dev/null +++ b/src/shared/ui/FormField/index.ts @@ -0,0 +1,3 @@ +export { FormField } from "./FormField"; +export { FormFieldInput } from "./Input"; +export type { FormFieldProps } from "./types"; diff --git a/src/shared/ui/FormField/types.ts b/src/shared/ui/FormField/types.ts new file mode 100644 index 0000000..576ff7e --- /dev/null +++ b/src/shared/ui/FormField/types.ts @@ -0,0 +1,54 @@ +import type { colorTokens } from "@/shared/styles/token"; +import type { ReactNode } from "react"; +import type { Control, FieldValues, Path } from "react-hook-form"; +import type { TextInputProps } from "react-native"; + +export type ColorTokenKey = keyof typeof colorTokens; + +/** + * 현재 크게 텍스트필드가 2가지 형태로 나뉨. + * filled: 배경색 있음, 테두리 없음 (기본) + * outlined: 배경색 흰색, 테두리 있음 + */ +export type FormFieldAppearance = "filled" | "outlined"; + +/** + * 로직 내부에서 계산되어 스타일을 덮어씀 + */ +export type FormFieldStatus = "default" | "focus" | "error"; + +export type FormFieldProps = TextInputProps & { + control: Control; + name: Path; + + label?: string; + helperText?: string; + + appearance?: FormFieldAppearance; + + labelColor?: ColorTokenKey; + helperTextColor?: ColorTokenKey; + + rightElement?: ReactNode; + + // 커스텀 오버라이드 (필요한 경우에만 사용) + inputBackgroundColor?: ColorTokenKey; + inputBorderColor?: ColorTokenKey; + inputTextColor?: ColorTokenKey; + placeholderColor?: ColorTokenKey; +}; + +// Input 컴포넌트 전용 Props +export type FormFieldInputProps = TextInputProps & { + appearance: FormFieldAppearance; + status: FormFieldStatus; + + fontSize?: number; + rightElement?: ReactNode; + + inputBackgroundColor?: ColorTokenKey; + inputBorderColor?: ColorTokenKey; + inputTextColor?: ColorTokenKey; + placeholderColor?: ColorTokenKey; + inputStyle?: TextInputProps["style"]; +}; diff --git a/yarn.lock b/yarn.lock index 6e70ff7..97582d6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1264,6 +1264,13 @@ chalk "^4.1.0" js-yaml "^4.1.0" +"@hookform/resolvers@^5.2.2": + version "5.2.2" + resolved "https://registry.yarnpkg.com/@hookform/resolvers/-/resolvers-5.2.2.tgz#5ac16cd89501ca31671e6e9f0f5c5d762a99aa12" + integrity sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA== + dependencies: + "@standard-schema/utils" "^0.3.0" + "@humanfs/core@^0.19.1": version "0.19.1" resolved "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz" @@ -1877,6 +1884,11 @@ dependencies: "@sinonjs/commons" "^3.0.0" +"@standard-schema/utils@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@standard-schema/utils/-/utils-0.3.0.tgz#3d5e608f16c2390c10528e98e59aef6bf73cae7b" + integrity sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g== + "@tanstack/query-core@5.90.20": version "5.90.20" resolved "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz" @@ -5933,6 +5945,11 @@ react-freeze@^1.0.0: resolved "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.4.tgz" integrity sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA== +react-hook-form@^7.71.1: + version "7.71.1" + resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.71.1.tgz#6a758958861682cf0eb22131eead684ba3618f66" + integrity sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w== + react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" @@ -7388,6 +7405,11 @@ yocto-queue@^0.1.0: resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== +zod@^4.3.6: + version "4.3.6" + resolved "https://registry.yarnpkg.com/zod/-/zod-4.3.6.tgz#89c56e0aa7d2b05107d894412227087885ab112a" + integrity sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg== + zustand@^5.0.11: version "5.0.11" resolved "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz" From e5762e8b3a353494e03d86e4049bd937af163743 Mon Sep 17 00:00:00 2001 From: ahcgnoej Date: Tue, 17 Feb 2026 16:52:45 +0900 Subject: [PATCH 2/4] =?UTF-8?q?[REFACTOR/#5]=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20=EC=BD=94=EB=93=9C=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/ui/FormField/FormField.tsx | 68 ++----- src/shared/ui/FormField/FormFieldHelper.tsx | 20 +++ src/shared/ui/FormField/FormFieldInput.tsx | 92 ++++++++++ src/shared/ui/FormField/FormFieldLabel.tsx | 21 +++ src/shared/ui/FormField/README.md | 188 ++++++++++++++++++++ src/shared/ui/FormField/index.ts | 4 +- src/shared/ui/FormField/types.ts | 21 +-- 7 files changed, 345 insertions(+), 69 deletions(-) create mode 100644 src/shared/ui/FormField/FormFieldHelper.tsx create mode 100644 src/shared/ui/FormField/FormFieldInput.tsx create mode 100644 src/shared/ui/FormField/FormFieldLabel.tsx create mode 100644 src/shared/ui/FormField/README.md diff --git a/src/shared/ui/FormField/FormField.tsx b/src/shared/ui/FormField/FormField.tsx index 364dcc9..e22fba3 100644 --- a/src/shared/ui/FormField/FormField.tsx +++ b/src/shared/ui/FormField/FormField.tsx @@ -1,18 +1,9 @@ -import { colorTokens } from "@/shared/styles/token"; -import { useState } from "react"; import { Controller, type FieldValues } from "react-hook-form"; -import { Text, View } from "react-native"; -import { FormFieldInput } from "./Input"; -import type { FormFieldProps, FormFieldStatus } from "./types"; - -/** - * 현재 상태를 계산하는 헬퍼 함수 - */ -function getStatus(hasError: boolean, isFocused: boolean): FormFieldStatus { - if (hasError) return "error"; // 에러 발생 시 status만 'error'로 전달 - if (isFocused) return "focus"; - return "default"; -} +import { View } from "react-native"; +import { FormFieldHelper } from "./FormFieldHelper"; +import { FormFieldInput } from "./FormFieldInput"; +import { FormFieldLabel } from "./FormFieldLabel"; +import type { FormFieldProps } from "./types"; export function FormField({ control, @@ -22,68 +13,37 @@ export function FormField({ rightElement, helperText, labelColor, - helperTextColor, ...inputProps }: FormFieldProps) { - const [isFocused, setIsFocused] = useState(false); - return ( { - const hasError = Boolean(fieldState.error); - const status = getStatus(hasError, isFocused); + //todo: 일단 에러메세지 추출만 함 + const errorMessage = fieldState.error?.message; + const message = helperText; return ( - + {label && ( - - {label} - + {label} )} { - setIsFocused(true); - inputProps.onFocus?.(e); - }} - onBlur={(e) => { - setIsFocused(false); - field.onBlur(); - inputProps.onBlur?.(e); - }} + onBlur={field.onBlur} + hasError={Boolean(errorMessage)} rightElement={rightElement} /> - {helperText && ( - - {helperText} - - )} + {message && {message}} ); }} /> ); } -//todo: helperText 로그인/회원가입 구현시 확장 필요 diff --git a/src/shared/ui/FormField/FormFieldHelper.tsx b/src/shared/ui/FormField/FormFieldHelper.tsx new file mode 100644 index 0000000..19d76a6 --- /dev/null +++ b/src/shared/ui/FormField/FormFieldHelper.tsx @@ -0,0 +1,20 @@ +import { colorTokens } from "@/shared/styles/token"; +import type { ReactNode } from "react"; +import { Text } from "react-native"; + +type Props = { + children: ReactNode; +}; + +export function FormFieldHelper({ children }: Props) { + return ( + + {children} + + ); +} diff --git a/src/shared/ui/FormField/FormFieldInput.tsx b/src/shared/ui/FormField/FormFieldInput.tsx new file mode 100644 index 0000000..0b3e243 --- /dev/null +++ b/src/shared/ui/FormField/FormFieldInput.tsx @@ -0,0 +1,92 @@ +import { colorTokens } from "@/shared/styles/token"; +import { useState } from "react"; +import { TextInput, View } from "react-native"; +import type { FormFieldAppearance, FormFieldInputProps } from "./types"; + +//formField가 크게 두가지 버전으로 존재함. +const APPEARANCE_STYLES: Record< + FormFieldAppearance, + { backgroundColor: string; borderColor: string } +> = { + filled: { + backgroundColor: colorTokens.neutral, + borderColor: colorTokens.neutral, + }, + outlined: { + backgroundColor: colorTokens.canvas, + borderColor: colorTokens.contentSecondary, + }, +}; + +export function FormFieldInput({ + appearance = "filled", + fontSize = 17, + inputBackgroundColor, + inputBorderColor, + inputTextColor, + inputStyle, + rightElement, + multiline, + hasError, + onFocus, + onBlur, + ...props +}: FormFieldInputProps) { + const [isFocused, setIsFocused] = useState(false); + + const baseStyle = APPEARANCE_STYLES[appearance]; + + const borderColor = hasError + ? colorTokens.danger + : isFocused + ? colorTokens.primary + : baseStyle.borderColor; + + const finalBackgroundColor = inputBackgroundColor + ? colorTokens[inputBackgroundColor] + : baseStyle.backgroundColor; + + const finalTextColor = inputTextColor + ? colorTokens[inputTextColor] + : colorTokens.contentPrimary; + + return ( + + { + setIsFocused(true); + onFocus?.(e); + }} + onBlur={(e) => { + setIsFocused(false); + onBlur?.(e); + }} + /> + + {rightElement && {rightElement}} + + ); +} diff --git a/src/shared/ui/FormField/FormFieldLabel.tsx b/src/shared/ui/FormField/FormFieldLabel.tsx new file mode 100644 index 0000000..99a17f3 --- /dev/null +++ b/src/shared/ui/FormField/FormFieldLabel.tsx @@ -0,0 +1,21 @@ +import { colorTokens } from "@/shared/styles/token"; +import { Text } from "react-native"; +import type { ColorTokenKey } from "./types"; + +type Props = { + children: string; + color?: ColorTokenKey; +}; + +export function FormFieldLabel({ children, color }: Props) { + return ( + + {children} + + ); +} diff --git a/src/shared/ui/FormField/README.md b/src/shared/ui/FormField/README.md new file mode 100644 index 0000000..03796a0 --- /dev/null +++ b/src/shared/ui/FormField/README.md @@ -0,0 +1,188 @@ +# FormField + +`FormField`는 **React Native 환경에서 폼 입력을 쉽게 만들기 위한 공용 입력 컴포넌트**입니다. + +- 폼 상태 관리 자동 연결 +- validation 에러 자동 표시 +- 디자인 시스템 스타일 일관성 유지 +- label / helper / right element 지원 + +👉 **React Hook Form** 기반으로 동작합니다. + +--- + +## 언제 사용하나요? + +다음과 같은 입력 UI에 사용합니다 + +- 로그인 / 회원가입 폼 +- 인증번호 입력 +- 설정 화면 +- 문의 작성 폼 + +👉 **텍스트 기반 입력 필드 전용 컴포넌트입니다.** + +--- + +## 빠른 시작 + +### 1. 폼 스키마 정의 + +보통 **Zod**와 함께 사용합니다. + +```ts +const schema = z.object({ + phone: z.string().min(10, "전화번호를 입력해주세요"), +}); +``` + +--- + +### 2. useForm 설정 + +```ts +const { control } = useForm({ + resolver: zodResolver(schema), + defaultValues: { phone: "" }, +}); +``` + +--- + +### 3. FormField 사용 + +```tsx + +``` + +--- + +## 기본 사용 패턴 + +### 라벨 + helper 메시지 + +```tsx + +``` + +--- + +### 오른쪽 버튼 추가 + +```tsx + + 인증번호 받기 + + } +/> +``` + +👉 인증 버튼, 아이콘 등에 사용합니다. + +--- + +### 스타일 변경 + +```tsx + +``` + +지원 스타일: + +- `"filled"` (기본값) +- `"outlined"` + +--- + +## Props 설명 + +### 필수 props + +| 이름 | 설명 | +| --------- | ----------------------------- | +| `control` | useForm에서 받은 control 객체 | +| `name` | 폼 필드 이름 | + +--- + +### 선택 props + +| 이름 | 설명 | +| -------------- | ---------------------------- | +| `label` | 입력 필드 라벨 | +| `helperText` | 보조 설명 메시지 | +| `appearance` | `"filled"` 또는 `"outlined"` | +| `rightElement` | 입력창 오른쪽 커스텀 요소 | +| `labelColor` | 라벨 색상 토큰 | + +--- + +### TextInput props + +`placeholder`, `keyboardType` 등 React Native TextInput props를 그대로 사용할 수 있습니다. + +```tsx + +``` + +--- + +## Validation 동작 방식 + +validation 에러가 발생하면: + +- 입력창 테두리가 빨간색으로 변경됨 +- helper 영역에 에러 메시지 표시됨 + +예: + +```ts +z.string().min(10, "10자리 이상 입력해주세요"); +``` + +--- + +## 권장 사용 방식 + +### ✅ 권장 + +- Zod + react-hook-form 조합 +- 기본 디자인 토큰 사용 +- validation 메시지 명확하게 작성 + +--- + +### ❌ 권장하지 않음 + +- 숫자/객체 값을 직접 바인딩 +- 복잡한 커스텀 UI를 input 안에 삽입 + +👉 이 컴포넌트는 **텍스트 입력 전용**입니다. + +--- + +## 예시 + +```tsx + +``` diff --git a/src/shared/ui/FormField/index.ts b/src/shared/ui/FormField/index.ts index 40eafce..6bb2e43 100644 --- a/src/shared/ui/FormField/index.ts +++ b/src/shared/ui/FormField/index.ts @@ -1,3 +1,5 @@ export { FormField } from "./FormField"; -export { FormFieldInput } from "./Input"; +export { FormFieldHelper } from "./FormFieldHelper"; +export { FormFieldInput } from "./FormFieldInput"; +export { FormFieldLabel } from "./FormFieldLabel"; export type { FormFieldProps } from "./types"; diff --git a/src/shared/ui/FormField/types.ts b/src/shared/ui/FormField/types.ts index 576ff7e..7d638c1 100644 --- a/src/shared/ui/FormField/types.ts +++ b/src/shared/ui/FormField/types.ts @@ -6,17 +6,10 @@ import type { TextInputProps } from "react-native"; export type ColorTokenKey = keyof typeof colorTokens; /** - * 현재 크게 텍스트필드가 2가지 형태로 나뉨. - * filled: 배경색 있음, 테두리 없음 (기본) - * outlined: 배경색 흰색, 테두리 있음 + * 텍스트필드 스타일 타입 (크게 두가지 있음) */ export type FormFieldAppearance = "filled" | "outlined"; -/** - * 로직 내부에서 계산되어 스타일을 덮어씀 - */ -export type FormFieldStatus = "default" | "focus" | "error"; - export type FormFieldProps = TextInputProps & { control: Control; name: Path; @@ -27,28 +20,28 @@ export type FormFieldProps = TextInputProps & { appearance?: FormFieldAppearance; labelColor?: ColorTokenKey; - helperTextColor?: ColorTokenKey; rightElement?: ReactNode; - // 커스텀 오버라이드 (필요한 경우에만 사용) + // 커스텀 스타일 오버라이드 inputBackgroundColor?: ColorTokenKey; inputBorderColor?: ColorTokenKey; inputTextColor?: ColorTokenKey; - placeholderColor?: ColorTokenKey; }; -// Input 컴포넌트 전용 Props +// Input Props export type FormFieldInputProps = TextInputProps & { appearance: FormFieldAppearance; - status: FormFieldStatus; fontSize?: number; rightElement?: ReactNode; + hasError?: boolean; + + // 커스텀 스타일 오버라이드 inputBackgroundColor?: ColorTokenKey; inputBorderColor?: ColorTokenKey; inputTextColor?: ColorTokenKey; - placeholderColor?: ColorTokenKey; + inputStyle?: TextInputProps["style"]; }; From 22bba069256a6431edc6e69c77e5f83378707fbb Mon Sep 17 00:00:00 2001 From: ahcgnoej Date: Tue, 17 Feb 2026 17:04:09 +0900 Subject: [PATCH 3/4] =?UTF-8?q?[CHORE/#5]=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=ED=8C=8C=EC=9D=BC=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/ui/FormField/Input.tsx | 110 ------------------------------ 1 file changed, 110 deletions(-) delete mode 100644 src/shared/ui/FormField/Input.tsx diff --git a/src/shared/ui/FormField/Input.tsx b/src/shared/ui/FormField/Input.tsx deleted file mode 100644 index 0964da3..0000000 --- a/src/shared/ui/FormField/Input.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { colorTokens } from "@/shared/styles/token"; -import { TextInput, View } from "react-native"; -import type { - FormFieldAppearance, - FormFieldInputProps, - FormFieldStatus, -} from "./types"; - -const APPEARANCE_STYLES: Record< - FormFieldAppearance, - { backgroundColor: string; borderColor: string } -> = { - filled: { - backgroundColor: colorTokens.neutral, // 회색 배경 - borderColor: colorTokens.neutral, - }, - outlined: { - backgroundColor: colorTokens.canvas, // 흰색 배경 (또는 transparent) - borderColor: colorTokens.contentSecondary, // 회색 테두리 - }, -}; - -/** - * 상태별 색상 정의 - */ -const STATUS_COLORS: Record< - FormFieldStatus, - { borderColor?: string; textColor: string } -> = { - default: { - borderColor: undefined, // appearance 따름 - textColor: colorTokens.contentPrimary, - }, - focus: { - borderColor: colorTokens.primary, - textColor: colorTokens.contentPrimary, - }, - error: { - borderColor: colorTokens.danger, - textColor: colorTokens.contentPrimary, - }, -}; - -export function FormFieldInput({ - appearance = "filled", - status, - fontSize = 17, - - inputBackgroundColor, - inputBorderColor, - inputTextColor, - - inputStyle, - rightElement, - - multiline, - ...props -}: FormFieldInputProps) { - const baseStyle = APPEARANCE_STYLES[appearance]; - const statusStyle = STATUS_COLORS[status]; - - const finalBackgroundColor = inputBackgroundColor - ? colorTokens[inputBackgroundColor] - : baseStyle.backgroundColor; - - const finalBorderColor = inputBorderColor - ? colorTokens[inputBorderColor] - : (statusStyle.borderColor ?? baseStyle.borderColor); - - const finalTextColor = inputTextColor - ? colorTokens[inputTextColor] - : statusStyle.textColor; - - return ( - - - - {rightElement && {rightElement}} - - ); -} -//todo: rightElement 로그인/회원가입 구현시 확장 필요 From 9620450fb70af4680aa87feeb5f6f85a75e1ea3e Mon Sep 17 00:00:00 2001 From: ahcgnoej Date: Sat, 21 Feb 2026 19:18:29 +0900 Subject: [PATCH 4/4] =?UTF-8?q?[FIX/#5]=20=ED=83=80=EC=9E=85=EC=97=90?= =?UTF-8?q?=EB=9F=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 116 +++++++-------- src/shared/styles/tokens.ts | 1 - src/shared/ui/FormField/FormField.tsx | 72 +++++----- src/shared/ui/FormField/FormFieldHelper.tsx | 24 ++-- src/shared/ui/FormField/FormFieldInput.tsx | 148 ++++++++++---------- src/shared/ui/FormField/FormFieldLabel.tsx | 26 ++-- src/shared/ui/FormField/types.ts | 42 +++--- src/shared/ui/index.ts | 1 - src/shared/ui/select/Select.tsx | 12 +- src/shared/ui/select/index.ts | 1 - src/shared/ui/select/types.ts | 1 - src/shared/utils/formatDate.ts | 4 +- tailwind.config.js | 4 +- tsconfig.json | 3 +- 14 files changed, 223 insertions(+), 232 deletions(-) diff --git a/package.json b/package.json index be62ad9..2d50cab 100644 --- a/package.json +++ b/package.json @@ -1,60 +1,60 @@ { - "name": "assu_fe_rn", - "main": "expo-router/entry", - "version": "1.0.0", - "scripts": { - "start": "expo start", - "android": "expo start --android", - "ios": "expo start --ios", - "web": "expo start --web", - "typecheck": "tsc --noEmit", - "biome:lint": "biome lint ./src", - "biome:fix": "biome check --write ./src", - "biome:format": "biome format ./src --write" - }, - "dependencies": { - "@expo/vector-icons": "^15.0.3", - "@hookform/resolvers": "^5.2.2", - "@react-navigation/bottom-tabs": "^7.4.0", - "@react-navigation/elements": "^2.6.3", - "@react-navigation/native": "^7.1.8", - "@tanstack/react-query": "^5.90.20", - "expo": "~54.0.33", - "expo-constants": "~18.0.13", - "expo-font": "~14.0.11", - "expo-haptics": "~15.0.8", - "expo-image": "~3.0.11", - "expo-linking": "~8.0.11", - "expo-router": "~6.0.23", - "expo-splash-screen": "~31.0.13", - "expo-status-bar": "~3.0.9", - "expo-symbols": "~1.0.8", - "expo-system-ui": "~6.0.9", - "expo-web-browser": "~15.0.10", - "nativewind": "^4.2.1", - "react": "19.1.0", - "react-dom": "19.1.0", - "react-hook-form": "^7.71.1", - "react-native": "0.81.5", - "react-native-element-dropdown": "^2.12.4", - "react-native-gesture-handler": "~2.28.0", - "react-native-reanimated": "~4.1.1", - "react-native-safe-area-context": "~5.6.0", - "react-native-screens": "~4.16.0", - "react-native-web": "~0.21.0", - "react-native-worklets": "0.5.1", - "zod": "^4.3.6", - "zustand": "^5.0.11" - }, - "devDependencies": { - "@biomejs/biome": "^2.3.14", - "@types/react": "~19.1.10", - "babel-plugin-module-resolver": "^5.0.2", - "eslint": "^9.25.0", - "eslint-config-expo": "~10.0.0", - "prettier-plugin-tailwindcss": "^0.7.2", - "tailwindcss": "3.4.17", - "typescript": "^5.9.3" - }, - "private": true + "name": "assu_fe_rn", + "main": "expo-router/entry", + "version": "1.0.0", + "scripts": { + "start": "expo start", + "android": "expo start --android", + "ios": "expo start --ios", + "web": "expo start --web", + "typecheck": "tsc --noEmit", + "biome:lint": "biome lint ./src", + "biome:fix": "biome check --write ./src", + "biome:format": "biome format ./src --write" + }, + "dependencies": { + "@expo/vector-icons": "^15.0.3", + "@hookform/resolvers": "^5.2.2", + "@react-navigation/bottom-tabs": "^7.4.0", + "@react-navigation/elements": "^2.6.3", + "@react-navigation/native": "^7.1.8", + "@tanstack/react-query": "^5.90.20", + "expo": "~54.0.33", + "expo-constants": "~18.0.13", + "expo-font": "~14.0.11", + "expo-haptics": "~15.0.8", + "expo-image": "~3.0.11", + "expo-linking": "~8.0.11", + "expo-router": "~6.0.23", + "expo-splash-screen": "~31.0.13", + "expo-status-bar": "~3.0.9", + "expo-symbols": "~1.0.8", + "expo-system-ui": "~6.0.9", + "expo-web-browser": "~15.0.10", + "nativewind": "^4.2.1", + "react": "19.1.0", + "react-dom": "19.1.0", + "react-hook-form": "^7.71.1", + "react-native": "0.81.5", + "react-native-element-dropdown": "^2.12.4", + "react-native-gesture-handler": "~2.28.0", + "react-native-reanimated": "~4.1.1", + "react-native-safe-area-context": "~5.6.0", + "react-native-screens": "~4.16.0", + "react-native-web": "~0.21.0", + "react-native-worklets": "0.5.1", + "zod": "^4.3.6", + "zustand": "^5.0.11" + }, + "devDependencies": { + "@biomejs/biome": "^2.3.14", + "@types/react": "~19.1.10", + "babel-plugin-module-resolver": "^5.0.2", + "eslint": "^9.25.0", + "eslint-config-expo": "~10.0.0", + "prettier-plugin-tailwindcss": "^0.7.2", + "tailwindcss": "3.4.17", + "typescript": "^5.9.3" + }, + "private": true } diff --git a/src/shared/styles/tokens.ts b/src/shared/styles/tokens.ts index 734cfcb..0af87b3 100644 --- a/src/shared/styles/tokens.ts +++ b/src/shared/styles/tokens.ts @@ -31,4 +31,3 @@ export const colorTokens = { /** global.styles.css: --color-content-inverse (blue-50) */ contentInverse: "#F4F6FE", } as const; - diff --git a/src/shared/ui/FormField/FormField.tsx b/src/shared/ui/FormField/FormField.tsx index e22fba3..cdcbd5a 100644 --- a/src/shared/ui/FormField/FormField.tsx +++ b/src/shared/ui/FormField/FormField.tsx @@ -6,44 +6,44 @@ import { FormFieldLabel } from "./FormFieldLabel"; import type { FormFieldProps } from "./types"; export function FormField({ - control, - name, - label, - appearance = "filled", - rightElement, - helperText, - labelColor, - ...inputProps + control, + name, + label, + appearance = "filled", + rightElement, + helperText, + labelColor, + ...inputProps }: FormFieldProps) { - return ( - { - //todo: 일단 에러메세지 추출만 함 - const errorMessage = fieldState.error?.message; - const message = helperText; + return ( + { + //todo: 일단 에러메세지 추출만 함 + const errorMessage = fieldState.error?.message; + const message = helperText; - return ( - - {label && ( - {label} - )} + return ( + + {label && ( + {label} + )} - + - {message && {message}} - - ); - }} - /> - ); + {message && {message}} + + ); + }} + /> + ); } diff --git a/src/shared/ui/FormField/FormFieldHelper.tsx b/src/shared/ui/FormField/FormFieldHelper.tsx index 19d76a6..9146ac9 100644 --- a/src/shared/ui/FormField/FormFieldHelper.tsx +++ b/src/shared/ui/FormField/FormFieldHelper.tsx @@ -1,20 +1,20 @@ -import { colorTokens } from "@/shared/styles/token"; import type { ReactNode } from "react"; import { Text } from "react-native"; +import { colorTokens } from "@/shared/styles/tokens"; type Props = { - children: ReactNode; + children: ReactNode; }; export function FormFieldHelper({ children }: Props) { - return ( - - {children} - - ); + return ( + + {children} + + ); } diff --git a/src/shared/ui/FormField/FormFieldInput.tsx b/src/shared/ui/FormField/FormFieldInput.tsx index 0b3e243..046cc55 100644 --- a/src/shared/ui/FormField/FormFieldInput.tsx +++ b/src/shared/ui/FormField/FormFieldInput.tsx @@ -1,92 +1,92 @@ -import { colorTokens } from "@/shared/styles/token"; import { useState } from "react"; import { TextInput, View } from "react-native"; +import { colorTokens } from "@/shared/styles/tokens"; import type { FormFieldAppearance, FormFieldInputProps } from "./types"; //formField가 크게 두가지 버전으로 존재함. const APPEARANCE_STYLES: Record< - FormFieldAppearance, - { backgroundColor: string; borderColor: string } + FormFieldAppearance, + { backgroundColor: string; borderColor: string } > = { - filled: { - backgroundColor: colorTokens.neutral, - borderColor: colorTokens.neutral, - }, - outlined: { - backgroundColor: colorTokens.canvas, - borderColor: colorTokens.contentSecondary, - }, + filled: { + backgroundColor: colorTokens.neutral, + borderColor: colorTokens.neutral, + }, + outlined: { + backgroundColor: colorTokens.canvas, + borderColor: colorTokens.contentSecondary, + }, }; export function FormFieldInput({ - appearance = "filled", - fontSize = 17, - inputBackgroundColor, - inputBorderColor, - inputTextColor, - inputStyle, - rightElement, - multiline, - hasError, - onFocus, - onBlur, - ...props + appearance = "filled", + fontSize = 17, + inputBackgroundColor, + inputBorderColor, + inputTextColor, + inputStyle, + rightElement, + multiline, + hasError, + onFocus, + onBlur, + ...props }: FormFieldInputProps) { - const [isFocused, setIsFocused] = useState(false); + const [isFocused, setIsFocused] = useState(false); - const baseStyle = APPEARANCE_STYLES[appearance]; + const baseStyle = APPEARANCE_STYLES[appearance]; - const borderColor = hasError - ? colorTokens.danger - : isFocused - ? colorTokens.primary - : baseStyle.borderColor; + const borderColor = hasError + ? colorTokens.danger + : isFocused + ? colorTokens.primary + : baseStyle.borderColor; - const finalBackgroundColor = inputBackgroundColor - ? colorTokens[inputBackgroundColor] - : baseStyle.backgroundColor; + const finalBackgroundColor = inputBackgroundColor + ? colorTokens[inputBackgroundColor] + : baseStyle.backgroundColor; - const finalTextColor = inputTextColor - ? colorTokens[inputTextColor] - : colorTokens.contentPrimary; + const finalTextColor = inputTextColor + ? colorTokens[inputTextColor] + : colorTokens.contentPrimary; - return ( - - { - setIsFocused(true); - onFocus?.(e); - }} - onBlur={(e) => { - setIsFocused(false); - onBlur?.(e); - }} - /> + return ( + + { + setIsFocused(true); + onFocus?.(e); + }} + onBlur={(e) => { + setIsFocused(false); + onBlur?.(e); + }} + /> - {rightElement && {rightElement}} - - ); + {rightElement && {rightElement}} + + ); } diff --git a/src/shared/ui/FormField/FormFieldLabel.tsx b/src/shared/ui/FormField/FormFieldLabel.tsx index 99a17f3..c260f37 100644 --- a/src/shared/ui/FormField/FormFieldLabel.tsx +++ b/src/shared/ui/FormField/FormFieldLabel.tsx @@ -1,21 +1,21 @@ -import { colorTokens } from "@/shared/styles/token"; import { Text } from "react-native"; +import { colorTokens } from "@/shared/styles/tokens"; import type { ColorTokenKey } from "./types"; type Props = { - children: string; - color?: ColorTokenKey; + children: string; + color?: ColorTokenKey; }; export function FormFieldLabel({ children, color }: Props) { - return ( - - {children} - - ); + return ( + + {children} + + ); } diff --git a/src/shared/ui/FormField/types.ts b/src/shared/ui/FormField/types.ts index 7d638c1..ede0eba 100644 --- a/src/shared/ui/FormField/types.ts +++ b/src/shared/ui/FormField/types.ts @@ -1,7 +1,7 @@ -import type { colorTokens } from "@/shared/styles/token"; import type { ReactNode } from "react"; import type { Control, FieldValues, Path } from "react-hook-form"; import type { TextInputProps } from "react-native"; +import type { colorTokens } from "@/shared/styles/tokens"; export type ColorTokenKey = keyof typeof colorTokens; @@ -11,37 +11,37 @@ export type ColorTokenKey = keyof typeof colorTokens; export type FormFieldAppearance = "filled" | "outlined"; export type FormFieldProps = TextInputProps & { - control: Control; - name: Path; + control: Control; + name: Path; - label?: string; - helperText?: string; + label?: string; + helperText?: string; - appearance?: FormFieldAppearance; + appearance?: FormFieldAppearance; - labelColor?: ColorTokenKey; + labelColor?: ColorTokenKey; - rightElement?: ReactNode; + rightElement?: ReactNode; - // 커스텀 스타일 오버라이드 - inputBackgroundColor?: ColorTokenKey; - inputBorderColor?: ColorTokenKey; - inputTextColor?: ColorTokenKey; + // 커스텀 스타일 오버라이드 + inputBackgroundColor?: ColorTokenKey; + inputBorderColor?: ColorTokenKey; + inputTextColor?: ColorTokenKey; }; // Input Props export type FormFieldInputProps = TextInputProps & { - appearance: FormFieldAppearance; + appearance: FormFieldAppearance; - fontSize?: number; - rightElement?: ReactNode; + fontSize?: number; + rightElement?: ReactNode; - hasError?: boolean; + hasError?: boolean; - // 커스텀 스타일 오버라이드 - inputBackgroundColor?: ColorTokenKey; - inputBorderColor?: ColorTokenKey; - inputTextColor?: ColorTokenKey; + // 커스텀 스타일 오버라이드 + inputBackgroundColor?: ColorTokenKey; + inputBorderColor?: ColorTokenKey; + inputTextColor?: ColorTokenKey; - inputStyle?: TextInputProps["style"]; + inputStyle?: TextInputProps["style"]; }; diff --git a/src/shared/ui/index.ts b/src/shared/ui/index.ts index e4c0223..e1856bb 100644 --- a/src/shared/ui/index.ts +++ b/src/shared/ui/index.ts @@ -1,2 +1 @@ export * from "./select"; - diff --git a/src/shared/ui/select/Select.tsx b/src/shared/ui/select/Select.tsx index 5a3faf4..1cc0ae7 100644 --- a/src/shared/ui/select/Select.tsx +++ b/src/shared/ui/select/Select.tsx @@ -1,11 +1,10 @@ +import { Ionicons } from "@expo/vector-icons"; import { useMemo, useState } from "react"; import { Text, View } from "react-native"; -import { Ionicons } from "@expo/vector-icons"; import { Dropdown } from "react-native-element-dropdown"; - -import type { SelectItem, SelectProps } from "./types"; -import { colorTokens } from "@/shared/styles/tokens"; import { shadows } from "@/shared/styles/shadows"; +import { colorTokens } from "@/shared/styles/tokens"; +import type { SelectItem, SelectProps } from "./types"; const SIZES = { sm: { @@ -118,9 +117,7 @@ export function Select({ const isSelected = item.value === value; return ( - + ); } - diff --git a/src/shared/ui/select/index.ts b/src/shared/ui/select/index.ts index 4ff96d8..87bf59c 100644 --- a/src/shared/ui/select/index.ts +++ b/src/shared/ui/select/index.ts @@ -1,3 +1,2 @@ export { Select } from "./Select"; export type { SelectItem, SelectProps, SelectSize } from "./types"; - diff --git a/src/shared/ui/select/types.ts b/src/shared/ui/select/types.ts index 131e413..779a2c1 100644 --- a/src/shared/ui/select/types.ts +++ b/src/shared/ui/select/types.ts @@ -37,4 +37,3 @@ export type SelectProps = { /** 테스트용 id */ testID?: string; }; - diff --git a/src/shared/utils/formatDate.ts b/src/shared/utils/formatDate.ts index 3c7077f..1cbcbb0 100644 --- a/src/shared/utils/formatDate.ts +++ b/src/shared/utils/formatDate.ts @@ -11,7 +11,7 @@ export function formatDate( options?: { locale?: string; separator?: "." | "-" | "/"; - } + }, ): string { const date = toDate(input); if (!date) return ""; @@ -32,7 +32,7 @@ export function formatDateTime( input: DateInput, options?: { separator?: "." | "-" | "/"; - } + }, ): string { const date = toDate(input); if (!date) return ""; diff --git a/tailwind.config.js b/tailwind.config.js index e643e37..918baee 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -32,9 +32,7 @@ module.exports = { gutter: "var(--layout-gutter)", }, fontFamily: { - sans: [ - "Pretendard-Regular", - ], + sans: ["Pretendard-Regular"], }, }, }, diff --git a/tsconfig.json b/tsconfig.json index 3cd2d19..a36bf33 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,8 +2,9 @@ "extends": "expo/tsconfig.base", "compilerOptions": { "strict": true, + "baseUrl": ".", "paths": { - "@/*": ["./src/*"] + "@/*": ["src/*"] } }, "include": [