From 0cff7726caca89f5e62cced128c935b72ca5b326 Mon Sep 17 00:00:00 2001 From: Jisu Shin Date: Mon, 8 Sep 2025 00:07:30 +0900 Subject: [PATCH 01/30] =?UTF-8?q?refactor:=20BaseTextField=20=EB=8F=84?= =?UTF-8?q?=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/TextField/BaseTextField.tsx | 178 ++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 src/common/component/TextField/BaseTextField.tsx diff --git a/src/common/component/TextField/BaseTextField.tsx b/src/common/component/TextField/BaseTextField.tsx new file mode 100644 index 00000000..0209077e --- /dev/null +++ b/src/common/component/TextField/BaseTextField.tsx @@ -0,0 +1,178 @@ +import type { ReactNode, Ref } from 'react'; +import { useCallback, useMemo, useRef, useState } from 'react'; + +type CommitReason = 'enter' | 'blur'; + +export interface BaseTextFieldProps { + id?: string; + value: string; + onChange: (value: string) => void; + onCommit?: (value: string, reason: CommitReason) => void; + onClear?: () => void; + maxLength?: number; + locked?: boolean; + invalid?: boolean; + ref?: Ref; + children: (args: { + inputProps: React.ComponentPropsWithRef<'input'>; + hasValue: boolean; + isFocused: boolean; + isComposing: boolean; + length: number; + remainingLength?: number; + clear: () => void; + commit: (reason: CommitReason) => void; + }) => ReactNode; +} + +const BaseTextField = ({ + id, + value, + onChange, + onCommit, + onClear, + maxLength, + locked, + invalid, + ref: externalRef, + children, +}: BaseTextFieldProps) => { + const [isComposing, setIsComposing] = useState(false); + const [isFocused, setIsFocused] = useState(false); + const lastCommittedRef = useRef(null); + const skipBlurOnceRef = useRef(false); + const localRef = useRef(null); + + const isLocked = !!locked; + const trimToMaxLength = (v: string, max?: number) => + typeof max === 'number' && v.length > max ? v.slice(0, max) : v; + + const hasValue = Boolean(value); + const length = value.length; + const remainingLength = + typeof maxLength === 'number' ? Math.max(0, maxLength - length) : undefined; + + const commit = useCallback( + (reason: CommitReason) => { + if (lastCommittedRef.current === value) return; + onCommit?.(value, reason); + lastCommittedRef.current = value; + }, + [onCommit, value], + ); + + const clear = useCallback(() => { + onChange(''); + if (onClear) onClear(); + }, [onChange, onClear]); + + const handleChange = useCallback( + (e: React.ChangeEvent) => { + if (isLocked) return; + const raw = e.target.value; + const next = isComposing ? raw : trimToMaxLength(raw, maxLength); + onChange(next); + }, + [isLocked, isComposing, maxLength, onChange], + ); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + const nativeEvent = e.nativeEvent as unknown as { isComposing?: boolean }; + const nativeComposing = Boolean(nativeEvent?.isComposing); + if (e.key === 'Enter' && !isComposing && !nativeComposing) { + e.preventDefault(); + e.stopPropagation(); + skipBlurOnceRef.current = true; + commit('enter'); + e.currentTarget.blur?.(); + return; + } + }, + [isComposing, commit], + ); + + const handleFocus = useCallback(() => { + setIsFocused(true); + }, []); + + const handleBlur = useCallback(() => { + setIsFocused(false); + if (isComposing || skipBlurOnceRef.current) { + skipBlurOnceRef.current = false; + return; + } + commit('blur'); + }, [isComposing, commit]); + + const handleCompositionStart = useCallback(() => { + setIsComposing(true); + }, []); + + const handleCompositionEnd = useCallback(() => { + setIsComposing(false); + if (typeof maxLength === 'number') { + const current = localRef.current?.value ?? ''; + const trimmed = trimToMaxLength(current, maxLength); + if (trimmed !== current) { + onChange(trimmed); + } + } + }, [maxLength, onChange]); + + const inputProps = useMemo(() => { + const assignRef = (node: HTMLInputElement | null) => { + localRef.current = node; + if (typeof externalRef === 'function') externalRef(node); + else if (externalRef && typeof externalRef === 'object') { + (externalRef as { current: HTMLInputElement | null }).current = node; + } + }; + + const base: React.ComponentPropsWithRef<'input'> = { + ref: assignRef, + id, + value, + onChange: handleChange, + onKeyDown: handleKeyDown, + onFocus: handleFocus, + onBlur: handleBlur, + onCompositionStart: handleCompositionStart, + onCompositionEnd: handleCompositionEnd, + readOnly: isLocked, + 'aria-readonly': isLocked, + 'aria-disabled': undefined, + 'aria-invalid': invalid || undefined, + }; + return base; + }, [ + externalRef, + id, + value, + isLocked, + invalid, + handleChange, + handleKeyDown, + handleFocus, + handleBlur, + handleCompositionStart, + handleCompositionEnd, + ]); + + return ( + <> + {children({ + inputProps, + hasValue, + isFocused, + isComposing, + length, + remainingLength, + clear, + commit, + })} + + ); +}; + +export default BaseTextField; From 625eeded46c8c8f56ee2b2b89e2cdf336f0c573a Mon Sep 17 00:00:00 2001 From: Jisu Shin Date: Wed, 10 Sep 2025 23:20:13 +0900 Subject: [PATCH 02/30] =?UTF-8?q?fix:=20BaseTextField=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cursor/rules/rule.mdc | 24 +++ .../component/TextField/BaseTextField.tsx | 9 +- .../mandalart/MandalartTextField.css.ts | 189 ++++++++++++++++++ .../mandalart/MandalartTextField.stories.tsx | 88 ++++++++ .../mandalart/MandalartTextField.tsx | 124 ++++++++++++ .../TextField/mandalart/constants.ts | 9 + .../component/TextField/mandalart/index.ts | 3 + 7 files changed, 438 insertions(+), 8 deletions(-) create mode 100644 .cursor/rules/rule.mdc create mode 100644 src/common/component/TextField/mandalart/MandalartTextField.css.ts create mode 100644 src/common/component/TextField/mandalart/MandalartTextField.stories.tsx create mode 100644 src/common/component/TextField/mandalart/MandalartTextField.tsx create mode 100644 src/common/component/TextField/mandalart/constants.ts create mode 100644 src/common/component/TextField/mandalart/index.ts diff --git a/.cursor/rules/rule.mdc b/.cursor/rules/rule.mdc new file mode 100644 index 00000000..3e0942bf --- /dev/null +++ b/.cursor/rules/rule.mdc @@ -0,0 +1,24 @@ +--- +alwaysApply: true +--- + +# Your rule content + +You carefully provide accurate, factual, thoughtful answers, and are a genius at reasoning. + +## Basic Principles + +- Follow the user’s requirements carefully & to the letter. +- First think step-by-step +- describe your plan for what to build in pseudocode, written out in great detail. +- Confirm, then write code! +- Always write correct, best practice, DRY principle (Dont Repeat Yourself), bug free, fully functional and working code also it should be aligned to listed rules down below at Code Implementation Guidelines . +- Focus on easy, readability code and being performant. +- Fully implement all requested functionality. +- Leave NO todo’s, placeholders or missing pieces. +- Ensure code is complete! Verify thoroughly finalised. +- Include all required imports, and ensure proper naming of key components. +- Be concise Minimize any other prose. +- If you think there might not be a correct answer, you say so. +- If you do not know the answer, say so, instead of guessing. +- Use English for all code and Use Korean for documentation. diff --git a/src/common/component/TextField/BaseTextField.tsx b/src/common/component/TextField/BaseTextField.tsx index 0209077e..64cc0497 100644 --- a/src/common/component/TextField/BaseTextField.tsx +++ b/src/common/component/TextField/BaseTextField.tsx @@ -1,4 +1,4 @@ -import type { ReactNode, Ref } from 'react'; +import type { ReactNode } from 'react'; import { useCallback, useMemo, useRef, useState } from 'react'; type CommitReason = 'enter' | 'blur'; @@ -12,7 +12,6 @@ export interface BaseTextFieldProps { maxLength?: number; locked?: boolean; invalid?: boolean; - ref?: Ref; children: (args: { inputProps: React.ComponentPropsWithRef<'input'>; hasValue: boolean; @@ -34,7 +33,6 @@ const BaseTextField = ({ maxLength, locked, invalid, - ref: externalRef, children, }: BaseTextFieldProps) => { const [isComposing, setIsComposing] = useState(false); @@ -123,10 +121,6 @@ const BaseTextField = ({ const inputProps = useMemo(() => { const assignRef = (node: HTMLInputElement | null) => { localRef.current = node; - if (typeof externalRef === 'function') externalRef(node); - else if (externalRef && typeof externalRef === 'object') { - (externalRef as { current: HTMLInputElement | null }).current = node; - } }; const base: React.ComponentPropsWithRef<'input'> = { @@ -146,7 +140,6 @@ const BaseTextField = ({ }; return base; }, [ - externalRef, id, value, isLocked, diff --git a/src/common/component/TextField/mandalart/MandalartTextField.css.ts b/src/common/component/TextField/mandalart/MandalartTextField.css.ts new file mode 100644 index 00000000..690007ca --- /dev/null +++ b/src/common/component/TextField/mandalart/MandalartTextField.css.ts @@ -0,0 +1,189 @@ +import { style, styleVariants } from '@vanilla-extract/css'; + +import { colors, fonts } from '@/style/token'; + +const bigGoalBox = { + display: 'flex', + alignItems: 'center', + flexShrink: 0, + width: '57.1rem', + height: '8rem', + padding: '2rem 3rem', + borderRadius: '12px', +}; + +const subGoalBox = { + display: 'flex', + alignItems: 'center', + flexShrink: 0, + width: '57.1rem', + height: '5.6rem', + padding: '1.4rem 2rem', + borderRadius: '8px', +}; + +const todoBox = { + display: 'flex', + alignItems: 'center', + flexShrink: 0, + width: '43.6rem', + height: '5.6rem', + padding: '1.4rem 2rem', + borderRadius: '8px', +}; + +export const inputBase = style({ + display: 'block', + width: '100%', + height: '100%', + background: 'transparent', + border: 'none', + outline: 'none', + padding: 0, + color: 'inherit', +}); + +const makeInputFont = (font: Record) => + style({ + ...font, + }); + +export const inputFontVariants = styleVariants({ + bigGoal: [inputBase, makeInputFont(fonts.title01)], + subGoal: [inputBase, makeInputFont(fonts.subtitle03)], + todo: [inputBase, makeInputFont(fonts.subtitle03)], +}); + +type InputStateOptions = {}; + +const createInputStateVariants = (_opts?: InputStateOptions) => { + return styleVariants({ + default: { + color: colors.grey6, + textAlign: 'left', + }, + clicked: { + color: colors.grey6, + textAlign: 'left', + }, + typing: { + color: colors.grey10, + textAlign: 'left', + }, + filled: { + color: colors.grey10, + textAlign: 'left', + }, + hover: { + color: colors.grey6, + textAlign: 'left', + }, + }); +}; + +export const inputStateVariants = { + bigGoal: createInputStateVariants(), + subGoal: createInputStateVariants(), + todo: createInputStateVariants(), +}; + +type VariantOptions = { + borderWidth: string; + activeBorderColor: string; +}; + +const createVariants = (box: object, opts: VariantOptions) => { + const { borderWidth, activeBorderColor } = opts; + const baseDefault: Record = { + ...box, + border: `${borderWidth} solid transparent`, + background: colors.grey4, + color: colors.grey6, + }; + const baseActive: Record = { + ...box, + border: `${borderWidth} solid ${activeBorderColor}`, + background: colors.grey3, + }; + const baseHoverOrFilled: Record = { + ...box, + border: `${borderWidth} solid transparent`, + background: colors.grey4, + }; + + return { + default: { + ...baseDefault, + textAlign: 'left' as const, + }, + clicked: { + ...baseActive, + color: colors.grey6, + textAlign: 'left' as const, + }, + typing: { + ...baseActive, + color: colors.grey10, + textAlign: 'left' as const, + justifyContent: 'space-between' as const, + }, + filled: { + ...baseHoverOrFilled, + color: colors.grey10, + textAlign: 'left' as const, + }, + hover: { + ...box, + border: `${borderWidth} solid transparent`, + background: colors.grey3, + color: colors.grey6, + textAlign: 'left' as const, + }, + }; +}; + +export const bigGoalVariants = styleVariants( + createVariants(bigGoalBox, { + borderWidth: '3px', + activeBorderColor: colors.grey5, + }), +); + +export const subGoalVariants = styleVariants( + createVariants(subGoalBox, { + borderWidth: '2px', + activeBorderColor: colors.blue06, + }), +); + +export const todoVariants = styleVariants( + createVariants(todoBox, { + borderWidth: '2px', + activeBorderColor: colors.blue06, + }), +); + +export const clearButton = style({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: '3.2rem', + height: '3.2rem', + aspectRatio: '1/1', + background: 'none', + border: 'none', + padding: 0, + cursor: 'pointer', +}); + +export const clearButtonSmall = style({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: '2.4rem', + height: '2.4rem', + background: 'none', + border: 'none', + padding: 0, + cursor: 'pointer', +}); diff --git a/src/common/component/TextField/mandalart/MandalartTextField.stories.tsx b/src/common/component/TextField/mandalart/MandalartTextField.stories.tsx new file mode 100644 index 00000000..20ae9dc6 --- /dev/null +++ b/src/common/component/TextField/mandalart/MandalartTextField.stories.tsx @@ -0,0 +1,88 @@ +import { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { MandalartTextField } from './'; +import type { MandalartVariant } from './constants'; +import { colors } from '@/style/token'; + +const meta = { + title: 'Components/TextField/MandalartTextField', + component: MandalartTextField, + parameters: { + layout: 'centered', + backgrounds: { + default: 'dark', + }, + }, + tags: ['autodocs'], + argTypes: { + variant: { + control: 'select', + options: ['bigGoal', 'subGoal', 'todo'], + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const FieldPreview = ({ title, variant }: { title: string; variant: MandalartVariant }) => { + const [value, setValue] = useState(''); + return ( +
+

{title}

+
+ +
+
+ ); +}; + +export const Default: Story = { + render: () => ( +
+ + + +
+ ), +}; + +export const BigGoal: Story = { + args: { + variant: 'bigGoal', + value: '', + }, + render: ({ variant }) => { + const [value, setValue] = useState(''); + return ( + + ); + }, +}; + +export const SubGoal: Story = { + args: { + variant: 'subGoal', + value: '', + }, + render: ({ variant }) => { + const [value, setValue] = useState(''); + return ( + + ); + }, +}; + +export const Todo: Story = { + args: { + variant: 'todo', + value: '', + }, + render: ({ variant }) => { + const [value, setValue] = useState(''); + return ( + + ); + }, +}; diff --git a/src/common/component/TextField/mandalart/MandalartTextField.tsx b/src/common/component/TextField/mandalart/MandalartTextField.tsx new file mode 100644 index 00000000..dece9177 --- /dev/null +++ b/src/common/component/TextField/mandalart/MandalartTextField.tsx @@ -0,0 +1,124 @@ +import { useMemo, useState } from 'react'; +import clsx from 'clsx'; + +import BaseTextField from '../BaseTextField'; +import IcTextdelete from '@/assets/svg/IcTextdelete'; + +import type { MandalartVariant } from './constants'; +import { BIG_GOAL_MAX_LENGTH, DEFAULT_PLACEHOLDER } from './constants'; +import * as s from './MandalartTextField.css'; + +type FieldState = 'default' | 'clicked' | 'typing' | 'filled' | 'hover'; + +const pickMaxLength = (variant: MandalartVariant, maxLength?: number) => + variant === 'bigGoal' ? (maxLength ?? BIG_GOAL_MAX_LENGTH) : (maxLength ?? undefined); + +const pickPlaceholder = (variant: MandalartVariant, placeholder?: string) => + placeholder ?? DEFAULT_PLACEHOLDER[variant]; + +const computeFieldState = (args: { + hasValue: boolean; + isFocused: boolean; + isHovered: boolean; +}): FieldState => { + const { hasValue, isFocused, isHovered } = args; + if (isFocused) return hasValue ? 'typing' : 'clicked'; + if (hasValue) return 'filled'; + return isHovered ? 'hover' : 'default'; +}; + +const callBoth = + (a?: (e: E) => void, b?: (e: E) => void) => + (e: E) => { + a?.(e); + b?.(e); + }; + +export interface MandalartTextFieldProps { + variant?: MandalartVariant; + value: string; + onChange: (value: string) => void; + placeholder?: string; + maxLength?: number; + disabled?: boolean; + onKeyDown?: React.KeyboardEventHandler; + onBlur?: React.FocusEventHandler; + onCompositionStart?: React.CompositionEventHandler; + onCompositionEnd?: React.CompositionEventHandler; +} + +const MandalartTextField = ({ + variant = 'bigGoal', + value, + onChange, + placeholder, + maxLength, + disabled, + onKeyDown, + onBlur, + onCompositionStart, + onCompositionEnd, +}: MandalartTextFieldProps) => { + const [isHovered, setIsHovered] = useState(false); + + const effectiveMaxLength = pickMaxLength(variant, maxLength); + const effectivePlaceholder = pickPlaceholder(variant, placeholder); + + const wrapperVariants = useMemo(() => { + const map = { + bigGoal: s.bigGoalVariants, + subGoal: s.subGoalVariants, + todo: s.todoVariants, + } as const; + return map[variant]; + }, [variant]); + + const clearButtonClass = variant === 'bigGoal' ? s.clearButton : s.clearButtonSmall; + + return ( + + {({ inputProps, hasValue, isFocused, clear }) => { + const state = computeFieldState({ hasValue, isFocused, isHovered }); + const stateStylesByVariant = s.inputStateVariants[variant] ?? s.inputStateVariants.bigGoal; + const inputClass = clsx(s.inputFontVariants[variant], stateStylesByVariant[state]); + const wrapperClass = wrapperVariants[state]; + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + + {state === 'typing' && ( + + )} +
+ ); + }} +
+ ); +}; + +export default MandalartTextField; diff --git a/src/common/component/TextField/mandalart/constants.ts b/src/common/component/TextField/mandalart/constants.ts new file mode 100644 index 00000000..00b810eb --- /dev/null +++ b/src/common/component/TextField/mandalart/constants.ts @@ -0,0 +1,9 @@ +export type MandalartVariant = 'bigGoal' | 'subGoal' | 'todo'; + +export const DEFAULT_PLACEHOLDER: Readonly> = { + bigGoal: '이루고 싶은 목표를 작성하세요.', + subGoal: '세부 목표를 입력해주세요', + todo: '할 일을 입력해주세요', +}; + +export const BIG_GOAL_MAX_LENGTH = 30; diff --git a/src/common/component/TextField/mandalart/index.ts b/src/common/component/TextField/mandalart/index.ts new file mode 100644 index 00000000..98ecf83f --- /dev/null +++ b/src/common/component/TextField/mandalart/index.ts @@ -0,0 +1,3 @@ +export { default as MandalartTextField } from './MandalartTextField'; +export type { MandalartTextFieldProps } from './MandalartTextField'; +export type { MandalartVariant } from './constants'; From 9f2e04c7bde36f8ed250ccb24988c9ba9127c2f5 Mon Sep 17 00:00:00 2001 From: Jisu Shin Date: Wed, 10 Sep 2025 23:20:49 +0900 Subject: [PATCH 03/30] =?UTF-8?q?Revert=20"fix:=20BaseTextField=20?= =?UTF-8?q?=EC=88=98=EC=A0=95"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 625eeded46c8c8f56ee2b2b89e2cdf336f0c573a. --- .cursor/rules/rule.mdc | 24 --- .../component/TextField/BaseTextField.tsx | 9 +- .../mandalart/MandalartTextField.css.ts | 189 ------------------ .../mandalart/MandalartTextField.stories.tsx | 88 -------- .../mandalart/MandalartTextField.tsx | 124 ------------ .../TextField/mandalart/constants.ts | 9 - .../component/TextField/mandalart/index.ts | 3 - 7 files changed, 8 insertions(+), 438 deletions(-) delete mode 100644 .cursor/rules/rule.mdc delete mode 100644 src/common/component/TextField/mandalart/MandalartTextField.css.ts delete mode 100644 src/common/component/TextField/mandalart/MandalartTextField.stories.tsx delete mode 100644 src/common/component/TextField/mandalart/MandalartTextField.tsx delete mode 100644 src/common/component/TextField/mandalart/constants.ts delete mode 100644 src/common/component/TextField/mandalart/index.ts diff --git a/.cursor/rules/rule.mdc b/.cursor/rules/rule.mdc deleted file mode 100644 index 3e0942bf..00000000 --- a/.cursor/rules/rule.mdc +++ /dev/null @@ -1,24 +0,0 @@ ---- -alwaysApply: true ---- - -# Your rule content - -You carefully provide accurate, factual, thoughtful answers, and are a genius at reasoning. - -## Basic Principles - -- Follow the user’s requirements carefully & to the letter. -- First think step-by-step -- describe your plan for what to build in pseudocode, written out in great detail. -- Confirm, then write code! -- Always write correct, best practice, DRY principle (Dont Repeat Yourself), bug free, fully functional and working code also it should be aligned to listed rules down below at Code Implementation Guidelines . -- Focus on easy, readability code and being performant. -- Fully implement all requested functionality. -- Leave NO todo’s, placeholders or missing pieces. -- Ensure code is complete! Verify thoroughly finalised. -- Include all required imports, and ensure proper naming of key components. -- Be concise Minimize any other prose. -- If you think there might not be a correct answer, you say so. -- If you do not know the answer, say so, instead of guessing. -- Use English for all code and Use Korean for documentation. diff --git a/src/common/component/TextField/BaseTextField.tsx b/src/common/component/TextField/BaseTextField.tsx index 64cc0497..0209077e 100644 --- a/src/common/component/TextField/BaseTextField.tsx +++ b/src/common/component/TextField/BaseTextField.tsx @@ -1,4 +1,4 @@ -import type { ReactNode } from 'react'; +import type { ReactNode, Ref } from 'react'; import { useCallback, useMemo, useRef, useState } from 'react'; type CommitReason = 'enter' | 'blur'; @@ -12,6 +12,7 @@ export interface BaseTextFieldProps { maxLength?: number; locked?: boolean; invalid?: boolean; + ref?: Ref; children: (args: { inputProps: React.ComponentPropsWithRef<'input'>; hasValue: boolean; @@ -33,6 +34,7 @@ const BaseTextField = ({ maxLength, locked, invalid, + ref: externalRef, children, }: BaseTextFieldProps) => { const [isComposing, setIsComposing] = useState(false); @@ -121,6 +123,10 @@ const BaseTextField = ({ const inputProps = useMemo(() => { const assignRef = (node: HTMLInputElement | null) => { localRef.current = node; + if (typeof externalRef === 'function') externalRef(node); + else if (externalRef && typeof externalRef === 'object') { + (externalRef as { current: HTMLInputElement | null }).current = node; + } }; const base: React.ComponentPropsWithRef<'input'> = { @@ -140,6 +146,7 @@ const BaseTextField = ({ }; return base; }, [ + externalRef, id, value, isLocked, diff --git a/src/common/component/TextField/mandalart/MandalartTextField.css.ts b/src/common/component/TextField/mandalart/MandalartTextField.css.ts deleted file mode 100644 index 690007ca..00000000 --- a/src/common/component/TextField/mandalart/MandalartTextField.css.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { style, styleVariants } from '@vanilla-extract/css'; - -import { colors, fonts } from '@/style/token'; - -const bigGoalBox = { - display: 'flex', - alignItems: 'center', - flexShrink: 0, - width: '57.1rem', - height: '8rem', - padding: '2rem 3rem', - borderRadius: '12px', -}; - -const subGoalBox = { - display: 'flex', - alignItems: 'center', - flexShrink: 0, - width: '57.1rem', - height: '5.6rem', - padding: '1.4rem 2rem', - borderRadius: '8px', -}; - -const todoBox = { - display: 'flex', - alignItems: 'center', - flexShrink: 0, - width: '43.6rem', - height: '5.6rem', - padding: '1.4rem 2rem', - borderRadius: '8px', -}; - -export const inputBase = style({ - display: 'block', - width: '100%', - height: '100%', - background: 'transparent', - border: 'none', - outline: 'none', - padding: 0, - color: 'inherit', -}); - -const makeInputFont = (font: Record) => - style({ - ...font, - }); - -export const inputFontVariants = styleVariants({ - bigGoal: [inputBase, makeInputFont(fonts.title01)], - subGoal: [inputBase, makeInputFont(fonts.subtitle03)], - todo: [inputBase, makeInputFont(fonts.subtitle03)], -}); - -type InputStateOptions = {}; - -const createInputStateVariants = (_opts?: InputStateOptions) => { - return styleVariants({ - default: { - color: colors.grey6, - textAlign: 'left', - }, - clicked: { - color: colors.grey6, - textAlign: 'left', - }, - typing: { - color: colors.grey10, - textAlign: 'left', - }, - filled: { - color: colors.grey10, - textAlign: 'left', - }, - hover: { - color: colors.grey6, - textAlign: 'left', - }, - }); -}; - -export const inputStateVariants = { - bigGoal: createInputStateVariants(), - subGoal: createInputStateVariants(), - todo: createInputStateVariants(), -}; - -type VariantOptions = { - borderWidth: string; - activeBorderColor: string; -}; - -const createVariants = (box: object, opts: VariantOptions) => { - const { borderWidth, activeBorderColor } = opts; - const baseDefault: Record = { - ...box, - border: `${borderWidth} solid transparent`, - background: colors.grey4, - color: colors.grey6, - }; - const baseActive: Record = { - ...box, - border: `${borderWidth} solid ${activeBorderColor}`, - background: colors.grey3, - }; - const baseHoverOrFilled: Record = { - ...box, - border: `${borderWidth} solid transparent`, - background: colors.grey4, - }; - - return { - default: { - ...baseDefault, - textAlign: 'left' as const, - }, - clicked: { - ...baseActive, - color: colors.grey6, - textAlign: 'left' as const, - }, - typing: { - ...baseActive, - color: colors.grey10, - textAlign: 'left' as const, - justifyContent: 'space-between' as const, - }, - filled: { - ...baseHoverOrFilled, - color: colors.grey10, - textAlign: 'left' as const, - }, - hover: { - ...box, - border: `${borderWidth} solid transparent`, - background: colors.grey3, - color: colors.grey6, - textAlign: 'left' as const, - }, - }; -}; - -export const bigGoalVariants = styleVariants( - createVariants(bigGoalBox, { - borderWidth: '3px', - activeBorderColor: colors.grey5, - }), -); - -export const subGoalVariants = styleVariants( - createVariants(subGoalBox, { - borderWidth: '2px', - activeBorderColor: colors.blue06, - }), -); - -export const todoVariants = styleVariants( - createVariants(todoBox, { - borderWidth: '2px', - activeBorderColor: colors.blue06, - }), -); - -export const clearButton = style({ - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - width: '3.2rem', - height: '3.2rem', - aspectRatio: '1/1', - background: 'none', - border: 'none', - padding: 0, - cursor: 'pointer', -}); - -export const clearButtonSmall = style({ - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - width: '2.4rem', - height: '2.4rem', - background: 'none', - border: 'none', - padding: 0, - cursor: 'pointer', -}); diff --git a/src/common/component/TextField/mandalart/MandalartTextField.stories.tsx b/src/common/component/TextField/mandalart/MandalartTextField.stories.tsx deleted file mode 100644 index 20ae9dc6..00000000 --- a/src/common/component/TextField/mandalart/MandalartTextField.stories.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { useState } from 'react'; -import type { Meta, StoryObj } from '@storybook/react-vite'; - -import { MandalartTextField } from './'; -import type { MandalartVariant } from './constants'; -import { colors } from '@/style/token'; - -const meta = { - title: 'Components/TextField/MandalartTextField', - component: MandalartTextField, - parameters: { - layout: 'centered', - backgrounds: { - default: 'dark', - }, - }, - tags: ['autodocs'], - argTypes: { - variant: { - control: 'select', - options: ['bigGoal', 'subGoal', 'todo'], - }, - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -const FieldPreview = ({ title, variant }: { title: string; variant: MandalartVariant }) => { - const [value, setValue] = useState(''); - return ( -
-

{title}

-
- -
-
- ); -}; - -export const Default: Story = { - render: () => ( -
- - - -
- ), -}; - -export const BigGoal: Story = { - args: { - variant: 'bigGoal', - value: '', - }, - render: ({ variant }) => { - const [value, setValue] = useState(''); - return ( - - ); - }, -}; - -export const SubGoal: Story = { - args: { - variant: 'subGoal', - value: '', - }, - render: ({ variant }) => { - const [value, setValue] = useState(''); - return ( - - ); - }, -}; - -export const Todo: Story = { - args: { - variant: 'todo', - value: '', - }, - render: ({ variant }) => { - const [value, setValue] = useState(''); - return ( - - ); - }, -}; diff --git a/src/common/component/TextField/mandalart/MandalartTextField.tsx b/src/common/component/TextField/mandalart/MandalartTextField.tsx deleted file mode 100644 index dece9177..00000000 --- a/src/common/component/TextField/mandalart/MandalartTextField.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { useMemo, useState } from 'react'; -import clsx from 'clsx'; - -import BaseTextField from '../BaseTextField'; -import IcTextdelete from '@/assets/svg/IcTextdelete'; - -import type { MandalartVariant } from './constants'; -import { BIG_GOAL_MAX_LENGTH, DEFAULT_PLACEHOLDER } from './constants'; -import * as s from './MandalartTextField.css'; - -type FieldState = 'default' | 'clicked' | 'typing' | 'filled' | 'hover'; - -const pickMaxLength = (variant: MandalartVariant, maxLength?: number) => - variant === 'bigGoal' ? (maxLength ?? BIG_GOAL_MAX_LENGTH) : (maxLength ?? undefined); - -const pickPlaceholder = (variant: MandalartVariant, placeholder?: string) => - placeholder ?? DEFAULT_PLACEHOLDER[variant]; - -const computeFieldState = (args: { - hasValue: boolean; - isFocused: boolean; - isHovered: boolean; -}): FieldState => { - const { hasValue, isFocused, isHovered } = args; - if (isFocused) return hasValue ? 'typing' : 'clicked'; - if (hasValue) return 'filled'; - return isHovered ? 'hover' : 'default'; -}; - -const callBoth = - (a?: (e: E) => void, b?: (e: E) => void) => - (e: E) => { - a?.(e); - b?.(e); - }; - -export interface MandalartTextFieldProps { - variant?: MandalartVariant; - value: string; - onChange: (value: string) => void; - placeholder?: string; - maxLength?: number; - disabled?: boolean; - onKeyDown?: React.KeyboardEventHandler; - onBlur?: React.FocusEventHandler; - onCompositionStart?: React.CompositionEventHandler; - onCompositionEnd?: React.CompositionEventHandler; -} - -const MandalartTextField = ({ - variant = 'bigGoal', - value, - onChange, - placeholder, - maxLength, - disabled, - onKeyDown, - onBlur, - onCompositionStart, - onCompositionEnd, -}: MandalartTextFieldProps) => { - const [isHovered, setIsHovered] = useState(false); - - const effectiveMaxLength = pickMaxLength(variant, maxLength); - const effectivePlaceholder = pickPlaceholder(variant, placeholder); - - const wrapperVariants = useMemo(() => { - const map = { - bigGoal: s.bigGoalVariants, - subGoal: s.subGoalVariants, - todo: s.todoVariants, - } as const; - return map[variant]; - }, [variant]); - - const clearButtonClass = variant === 'bigGoal' ? s.clearButton : s.clearButtonSmall; - - return ( - - {({ inputProps, hasValue, isFocused, clear }) => { - const state = computeFieldState({ hasValue, isFocused, isHovered }); - const stateStylesByVariant = s.inputStateVariants[variant] ?? s.inputStateVariants.bigGoal; - const inputClass = clsx(s.inputFontVariants[variant], stateStylesByVariant[state]); - const wrapperClass = wrapperVariants[state]; - - return ( -
setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - > - - {state === 'typing' && ( - - )} -
- ); - }} -
- ); -}; - -export default MandalartTextField; diff --git a/src/common/component/TextField/mandalart/constants.ts b/src/common/component/TextField/mandalart/constants.ts deleted file mode 100644 index 00b810eb..00000000 --- a/src/common/component/TextField/mandalart/constants.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type MandalartVariant = 'bigGoal' | 'subGoal' | 'todo'; - -export const DEFAULT_PLACEHOLDER: Readonly> = { - bigGoal: '이루고 싶은 목표를 작성하세요.', - subGoal: '세부 목표를 입력해주세요', - todo: '할 일을 입력해주세요', -}; - -export const BIG_GOAL_MAX_LENGTH = 30; diff --git a/src/common/component/TextField/mandalart/index.ts b/src/common/component/TextField/mandalart/index.ts deleted file mode 100644 index 98ecf83f..00000000 --- a/src/common/component/TextField/mandalart/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { default as MandalartTextField } from './MandalartTextField'; -export type { MandalartTextFieldProps } from './MandalartTextField'; -export type { MandalartVariant } from './constants'; From 2fd80a71b613160bf83366696c0001861f3e01c5 Mon Sep 17 00:00:00 2001 From: Jisu Shin Date: Wed, 10 Sep 2025 23:28:17 +0900 Subject: [PATCH 04/30] =?UTF-8?q?fix:=20BaseTextField=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/component/TextField/BaseTextField.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/common/component/TextField/BaseTextField.tsx b/src/common/component/TextField/BaseTextField.tsx index 0209077e..64cc0497 100644 --- a/src/common/component/TextField/BaseTextField.tsx +++ b/src/common/component/TextField/BaseTextField.tsx @@ -1,4 +1,4 @@ -import type { ReactNode, Ref } from 'react'; +import type { ReactNode } from 'react'; import { useCallback, useMemo, useRef, useState } from 'react'; type CommitReason = 'enter' | 'blur'; @@ -12,7 +12,6 @@ export interface BaseTextFieldProps { maxLength?: number; locked?: boolean; invalid?: boolean; - ref?: Ref; children: (args: { inputProps: React.ComponentPropsWithRef<'input'>; hasValue: boolean; @@ -34,7 +33,6 @@ const BaseTextField = ({ maxLength, locked, invalid, - ref: externalRef, children, }: BaseTextFieldProps) => { const [isComposing, setIsComposing] = useState(false); @@ -123,10 +121,6 @@ const BaseTextField = ({ const inputProps = useMemo(() => { const assignRef = (node: HTMLInputElement | null) => { localRef.current = node; - if (typeof externalRef === 'function') externalRef(node); - else if (externalRef && typeof externalRef === 'object') { - (externalRef as { current: HTMLInputElement | null }).current = node; - } }; const base: React.ComponentPropsWithRef<'input'> = { @@ -146,7 +140,6 @@ const BaseTextField = ({ }; return base; }, [ - externalRef, id, value, isLocked, From 0ba4278d6de1092506ad437cd99a37ac516b1e16 Mon Sep 17 00:00:00 2001 From: Jisu Shin Date: Wed, 10 Sep 2025 23:50:53 +0900 Subject: [PATCH 05/30] =?UTF-8?q?fix:=20BaseTextField=20=EA=B8=80=EC=9E=90?= =?UTF-8?q?=20=EC=88=98=20=EC=A0=9C=ED=95=9C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/TextField/BaseTextField.tsx | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/src/common/component/TextField/BaseTextField.tsx b/src/common/component/TextField/BaseTextField.tsx index 64cc0497..f4dc7b80 100644 --- a/src/common/component/TextField/BaseTextField.tsx +++ b/src/common/component/TextField/BaseTextField.tsx @@ -39,11 +39,8 @@ const BaseTextField = ({ const [isFocused, setIsFocused] = useState(false); const lastCommittedRef = useRef(null); const skipBlurOnceRef = useRef(false); - const localRef = useRef(null); const isLocked = !!locked; - const trimToMaxLength = (v: string, max?: number) => - typeof max === 'number' && v.length > max ? v.slice(0, max) : v; const hasValue = Boolean(value); const length = value.length; @@ -68,8 +65,12 @@ const BaseTextField = ({ (e: React.ChangeEvent) => { if (isLocked) return; const raw = e.target.value; - const next = isComposing ? raw : trimToMaxLength(raw, maxLength); - onChange(next); + if (typeof maxLength === 'number' && raw.length > maxLength) { + if (isComposing) return; + onChange(raw.slice(0, maxLength)); + return; + } + onChange(raw); }, [isLocked, isComposing, maxLength, onChange], ); @@ -107,24 +108,21 @@ const BaseTextField = ({ setIsComposing(true); }, []); - const handleCompositionEnd = useCallback(() => { - setIsComposing(false); - if (typeof maxLength === 'number') { - const current = localRef.current?.value ?? ''; - const trimmed = trimToMaxLength(current, maxLength); - if (trimmed !== current) { - onChange(trimmed); + const handleCompositionEnd = useCallback( + (e: React.CompositionEvent) => { + setIsComposing(false); + if (typeof maxLength === 'number') { + const current = e.currentTarget.value ?? ''; + if (current.length > maxLength) { + onChange(current.slice(0, maxLength)); + } } - } - }, [maxLength, onChange]); + }, + [maxLength, onChange], + ); const inputProps = useMemo(() => { - const assignRef = (node: HTMLInputElement | null) => { - localRef.current = node; - }; - const base: React.ComponentPropsWithRef<'input'> = { - ref: assignRef, id, value, onChange: handleChange, @@ -133,6 +131,7 @@ const BaseTextField = ({ onBlur: handleBlur, onCompositionStart: handleCompositionStart, onCompositionEnd: handleCompositionEnd, + maxLength: typeof maxLength === 'number' ? maxLength : undefined, readOnly: isLocked, 'aria-readonly': isLocked, 'aria-disabled': undefined, From 0f1eb0a8539e2866f8fc7dd4f0daf09036ca14f3 Mon Sep 17 00:00:00 2001 From: Jisu Shin Date: Thu, 11 Sep 2025 00:04:34 +0900 Subject: [PATCH 06/30] =?UTF-8?q?refactor:=20MandalartTextField=EC=97=90?= =?UTF-8?q?=20BaseTextfield=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mandalart/MandalartTextField.css.ts | 149 ++++++++++++++++++ .../mandalart/MandalartTextField.stories.tsx | 40 +++++ .../mandalart/MandalartTextField.tsx | 98 ++++++++++++ .../TextField/mandalart/constants.ts | 9 ++ .../component/TextField/mandalart/index.ts | 3 + 5 files changed, 299 insertions(+) create mode 100644 src/common/component/TextField/mandalart/MandalartTextField.css.ts create mode 100644 src/common/component/TextField/mandalart/MandalartTextField.stories.tsx create mode 100644 src/common/component/TextField/mandalart/MandalartTextField.tsx create mode 100644 src/common/component/TextField/mandalart/constants.ts create mode 100644 src/common/component/TextField/mandalart/index.ts diff --git a/src/common/component/TextField/mandalart/MandalartTextField.css.ts b/src/common/component/TextField/mandalart/MandalartTextField.css.ts new file mode 100644 index 00000000..5825f1f7 --- /dev/null +++ b/src/common/component/TextField/mandalart/MandalartTextField.css.ts @@ -0,0 +1,149 @@ +import { style, styleVariants } from '@vanilla-extract/css'; + +import { colors, fonts } from '@/style/token'; + +const makeBox = (width: string, height: string, padding: string, borderRadius: string) => ({ + display: 'flex', + alignItems: 'center', + flexShrink: 0, + width, + height, + padding, + borderRadius, +}); + +const bigGoalBox = makeBox('57.1rem', '8rem', '2rem 3rem', '12px'); +const subGoalBox = makeBox('57.1rem', '5.6rem', '1.4rem 2rem', '8px'); +const todoBox = makeBox('43.6rem', '5.6rem', '1.4rem 2rem', '8px'); + +export const inputBase = style({ + display: 'block', + width: '100%', + height: '100%', + background: 'transparent', + border: 'none', + outline: 'none', + padding: 0, + color: 'inherit', + textAlign: 'left', +}); + +const makeInputFont = (font: Record) => + style({ + ...font, + }); + +export const inputFontVariants = styleVariants({ + bigGoal: [inputBase, makeInputFont(fonts.title01)], + subGoal: [inputBase, makeInputFont(fonts.subtitle03)], + todo: [inputBase, makeInputFont(fonts.subtitle03)], +}); + +const createInputStateVariants = () => { + return styleVariants({ + default: { + color: colors.grey6, + }, + clicked: { + color: colors.grey6, + }, + typing: { + color: colors.grey10, + }, + filled: { + color: colors.grey10, + }, + hover: { + color: colors.grey6, + }, + }); +}; + +const commonInputStateVariants = createInputStateVariants(); + +export const inputStateVariants = { + bigGoal: commonInputStateVariants, + subGoal: commonInputStateVariants, + todo: commonInputStateVariants, +}; + +type VariantOptions = { + borderWidth: string; + activeBorderColor: string; +}; + +const createVariants = (box: object, opts: VariantOptions) => { + const { borderWidth, activeBorderColor } = opts; + const baseDefault = { + ...box, + border: `${borderWidth} solid transparent`, + background: colors.grey4, + }; + const baseActive = { + ...box, + border: `${borderWidth} solid ${activeBorderColor}`, + background: colors.grey3, + }; + const baseHoverOrFilled = { + ...box, + border: `${borderWidth} solid transparent`, + background: colors.grey4, + }; + + return { + default: { + ...baseDefault, + }, + clicked: { + ...baseActive, + }, + typing: { + ...baseActive, + justifyContent: 'space-between', + }, + filled: { + ...baseHoverOrFilled, + }, + hover: { + ...baseHoverOrFilled, + background: colors.grey3, + }, + }; +}; + +export const bigGoalVariants = styleVariants( + createVariants(bigGoalBox, { + borderWidth: '3px', + activeBorderColor: colors.grey5, + }), +); + +export const subGoalVariants = styleVariants( + createVariants(subGoalBox, { + borderWidth: '2px', + activeBorderColor: colors.blue06, + }), +); + +export const todoVariants = styleVariants( + createVariants(todoBox, { + borderWidth: '2px', + activeBorderColor: colors.blue06, + }), +); + +const makeIconButton = (size: string) => + style({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: size, + height: size, + background: 'none', + border: 'none', + padding: 0, + cursor: 'pointer', + }); + +export const clearButton = makeIconButton('3.2rem'); +export const clearButtonSmall = makeIconButton('2.4rem'); diff --git a/src/common/component/TextField/mandalart/MandalartTextField.stories.tsx b/src/common/component/TextField/mandalart/MandalartTextField.stories.tsx new file mode 100644 index 00000000..b08a8127 --- /dev/null +++ b/src/common/component/TextField/mandalart/MandalartTextField.stories.tsx @@ -0,0 +1,40 @@ +import { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { MandalartTextField } from './'; + +const meta = { + title: 'Components/TextField/MandalartTextField', + component: MandalartTextField, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const BigGoal: Story = { + args: { variant: 'bigGoal', value: '', onChange: () => {} }, + render: () => { + const [value, setValue] = useState(''); + return ; + }, +}; + +export const SubGoal: Story = { + args: { variant: 'subGoal', value: '', onChange: () => {} }, + render: () => { + const [value, setValue] = useState(''); + return ; + }, +}; + +export const Todo: Story = { + args: { variant: 'todo', value: '', onChange: () => {} }, + render: () => { + const [value, setValue] = useState(''); + return ; + }, +}; diff --git a/src/common/component/TextField/mandalart/MandalartTextField.tsx b/src/common/component/TextField/mandalart/MandalartTextField.tsx new file mode 100644 index 00000000..db32199f --- /dev/null +++ b/src/common/component/TextField/mandalart/MandalartTextField.tsx @@ -0,0 +1,98 @@ +import { useState } from 'react'; + +import IcTextdelete from '@/assets/svg/IcTextdelete'; + +import { type MandalartVariant, BIG_GOAL_MAX_LENGTH, DEFAULT_PLACEHOLDER } from './constants'; +import * as s from './MandalartTextField.css'; + +import BaseTextField from '../BaseTextField'; + +type FieldState = 'default' | 'clicked' | 'typing' | 'filled' | 'hover'; + +const pickMaxLength = (variant: MandalartVariant, maxLength?: number) => + variant === 'bigGoal' ? (maxLength ?? BIG_GOAL_MAX_LENGTH) : (maxLength ?? undefined); + +const pickPlaceholder = (variant: MandalartVariant, placeholder?: string) => + placeholder ?? DEFAULT_PLACEHOLDER[variant]; + +const computeFieldState = (args: { + hasValue: boolean; + isFocused: boolean; + isHovered: boolean; +}): FieldState => { + const { hasValue, isFocused, isHovered } = args; + if (isFocused) return hasValue ? 'typing' : 'clicked'; + if (hasValue) return 'filled'; + return isHovered ? 'hover' : 'default'; +}; + +export interface MandalartTextFieldProps { + variant?: MandalartVariant; + value: string; + onChange: (value: string) => void; + placeholder?: string; + maxLength?: number; + disabled?: boolean; +} + +const MandalartTextField = ({ + variant = 'bigGoal', + value, + onChange, + placeholder, + maxLength, + disabled, +}: MandalartTextFieldProps) => { + const [isHovered, setIsHovered] = useState(false); + + const effectiveMaxLength = pickMaxLength(variant, maxLength); + const effectivePlaceholder = pickPlaceholder(variant, placeholder); + + const wrapperVariants = + variant === 'bigGoal' + ? s.bigGoalVariants + : variant === 'subGoal' + ? s.subGoalVariants + : s.todoVariants; + + const clearButtonClass = variant === 'bigGoal' ? s.clearButton : s.clearButtonSmall; + + return ( + + {({ inputProps, hasValue, isFocused, clear }) => { + const state = computeFieldState({ hasValue, isFocused, isHovered }); + const stateStylesByVariant = s.inputStateVariants[variant] ?? s.inputStateVariants.bigGoal; + const inputClass = `${s.inputFontVariants[variant]} ${stateStylesByVariant[state]}`; + const wrapperClass = wrapperVariants[state]; + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + + {state === 'typing' && ( + + )} +
+ ); + }} +
+ ); +}; + +export default MandalartTextField; diff --git a/src/common/component/TextField/mandalart/constants.ts b/src/common/component/TextField/mandalart/constants.ts new file mode 100644 index 00000000..a728dc1a --- /dev/null +++ b/src/common/component/TextField/mandalart/constants.ts @@ -0,0 +1,9 @@ +export type MandalartVariant = 'bigGoal' | 'subGoal' | 'todo'; + +export const DEFAULT_PLACEHOLDER = { + bigGoal: '이루고 싶은 목표를 작성하세요', + subGoal: '세부 목표를 입력해주세요', + todo: '할 일을 입력해주세요', +} as const; + +export const BIG_GOAL_MAX_LENGTH = 30; diff --git a/src/common/component/TextField/mandalart/index.ts b/src/common/component/TextField/mandalart/index.ts new file mode 100644 index 00000000..98ecf83f --- /dev/null +++ b/src/common/component/TextField/mandalart/index.ts @@ -0,0 +1,3 @@ +export { default as MandalartTextField } from './MandalartTextField'; +export type { MandalartTextFieldProps } from './MandalartTextField'; +export type { MandalartVariant } from './constants'; From 481cb67efe223745fb5214ce583358ff2d73d8f4 Mon Sep 17 00:00:00 2001 From: Jisu Shin Date: Thu, 11 Sep 2025 00:05:11 +0900 Subject: [PATCH 07/30] =?UTF-8?q?remove:=20=EA=B8=B0=EC=A1=B4=20MandalartT?= =?UTF-8?q?extField=20=ED=8F=B4=EB=8D=94=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MandalartTextField.css.ts | 188 ----------------- .../MandalartTextField.stories.tsx | 106 ---------- .../MandalartTextField/MandalartTextField.tsx | 195 ------------------ .../MandalartTextField.types.ts | 14 -- .../MandalartTextField/constant/constants.ts | 7 - .../component/MandalartTextField/index.ts | 2 - 6 files changed, 512 deletions(-) delete mode 100644 src/common/component/MandalartTextField/MandalartTextField.css.ts delete mode 100644 src/common/component/MandalartTextField/MandalartTextField.stories.tsx delete mode 100644 src/common/component/MandalartTextField/MandalartTextField.tsx delete mode 100644 src/common/component/MandalartTextField/MandalartTextField.types.ts delete mode 100644 src/common/component/MandalartTextField/constant/constants.ts delete mode 100644 src/common/component/MandalartTextField/index.ts diff --git a/src/common/component/MandalartTextField/MandalartTextField.css.ts b/src/common/component/MandalartTextField/MandalartTextField.css.ts deleted file mode 100644 index ffea2e9f..00000000 --- a/src/common/component/MandalartTextField/MandalartTextField.css.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { style, styleVariants } from '@vanilla-extract/css'; - -import { colors } from '@/style/token/color.css'; -import { fonts } from '@/style/token/typography.css'; - -// ====== 공통 base 스타일 ====== -const bigGoalBase = { - display: 'flex', - alignItems: 'center', - flexShrink: 0, - width: '57.1rem', - height: '8rem', - borderRadius: '12px', - padding: '2rem 3rem', - fontSize: fonts.title01.fontSize, - fontWeight: fonts.title01.fontWeight, - lineHeight: fonts.title01.lineHeight, - color: colors.grey10, -}; -const subGoalBase = { - display: 'flex', - alignItems: 'center', - flexShrink: 0, - width: '57.1rem', - height: '5.6rem', - borderRadius: '8px', - padding: '1.4rem 2rem', - fontSize: fonts.subtitle03.fontSize, - fontWeight: fonts.subtitle03.fontWeight, - lineHeight: fonts.subtitle03.lineHeight, - color: colors.grey10, -}; -const todoBase = { - display: 'flex', - alignItems: 'center', - flexShrink: 0, - width: '43.6rem', - height: '5.6rem', - borderRadius: '8px', - padding: '1.4rem 2rem', - fontSize: fonts.subtitle03.fontSize, - fontWeight: fonts.subtitle03.fontWeight, - lineHeight: fonts.subtitle03.lineHeight, - color: '#111111', -}; - -export const bigGoalBaseClass = style(bigGoalBase); -export const subGoalBaseClass = style(subGoalBase); -export const todoBaseClass = style(todoBase); - -export const inputBase = style({ - display: 'block', - width: '100%', - height: '100%', - background: 'transparent', - border: 'none', - outline: 'none', - padding: 0, - color: colors.grey10, -}); - -const makeInputStyle = (font: Record) => - style({ - fontFamily: 'Pretendard', - ...font, - '::placeholder': { - color: colors.grey6, - fontFamily: 'Pretendard', - ...font, - }, - }); -export const inputBigGoal = makeInputStyle(fonts.title01); -export const inputSubGoal = makeInputStyle(fonts.subtitle03); -export const inputTodo = makeInputStyle(fonts.subtitle03); - -export const inputVariants = styleVariants({ - bigGoal: [inputBase, inputBigGoal], - subGoal: [inputBase, inputSubGoal], - todo: [inputBase, inputTodo], -}); - -export const bigGoalVariants = styleVariants({ - default: { - ...bigGoalBase, - border: '0.3rem solid transparent', - background: colors.grey4, - color: colors.grey6, - textAlign: 'left', - }, - clicked: { - ...bigGoalBase, - border: `0.3rem solid ${colors.grey5}`, - background: colors.grey3, - color: colors.grey6, - textAlign: 'left', - }, - typing: { - ...bigGoalBase, - border: `0.3rem solid ${colors.grey5}`, - background: colors.grey3, - color: colors.grey10, - textAlign: 'left', - justifyContent: 'space-between', - }, - filled: { - ...bigGoalBase, - border: '0.3rem solid transparent', - background: colors.grey4, - color: colors.grey10, - textAlign: 'center', - }, - hover: { - ...bigGoalBase, - border: '0.3rem solid transparent', - background: colors.grey3, - color: colors.grey6, - textAlign: 'center', - }, -}); - -const createVariantStyles = (baseStyle: object) => ({ - default: { - ...baseStyle, - border: '2px solid transparent', - background: colors.grey4, - color: colors.grey6, - textAlign: 'center' as const, - }, - clicked: { - ...baseStyle, - border: `2px solid ${colors.blue06}`, - background: colors.grey3, - color: colors.grey6, - textAlign: 'center' as const, - }, - typing: { - ...baseStyle, - border: `2px solid ${colors.blue06}`, - background: colors.grey3, - color: colors.grey10, - textAlign: 'left' as const, - justifyContent: 'space-between' as const, - }, - filled: { - ...baseStyle, - border: '2px solid transparent', - background: colors.grey4, - color: colors.grey10, - textAlign: 'center' as const, - fontWeight: 600, - }, - hover: { - ...baseStyle, - border: '2px solid transparent', - background: colors.grey3, - color: colors.grey6, - textAlign: 'center' as const, - }, -}); - -export const subGoalVariants = styleVariants(createVariantStyles(subGoalBase)); -export const todoVariants = styleVariants(createVariantStyles(todoBase)); - -const CLEAR_BUTTON_SIZE = '3.2rem'; -const CLEAR_BUTTON_SMALL_SIZE = '2.4rem'; -export const clearButton = style({ - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - width: CLEAR_BUTTON_SIZE, - height: CLEAR_BUTTON_SIZE, - aspectRatio: '1/1', - background: 'none', - border: 'none', - padding: 0, - cursor: 'pointer', -}); -export const clearButtonSmall = style({ - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - width: CLEAR_BUTTON_SMALL_SIZE, - height: CLEAR_BUTTON_SMALL_SIZE, - background: 'none', - border: 'none', - padding: 0, - cursor: 'pointer', -}); diff --git a/src/common/component/MandalartTextField/MandalartTextField.stories.tsx b/src/common/component/MandalartTextField/MandalartTextField.stories.tsx deleted file mode 100644 index 247f8bda..00000000 --- a/src/common/component/MandalartTextField/MandalartTextField.stories.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { useState } from 'react'; -import type { Meta, StoryObj } from '@storybook/react-vite'; - -import MandalartTextField from './MandalartTextField'; - -const meta: Meta = { - title: 'Common/MandalartTextField', - component: MandalartTextField, - parameters: { - layout: 'centered', - docs: { - description: { - component: - '상위목표(bigGoal), 하위목표(subGoal), 할 일(todo) 등 다양한 텍스트필드 용도로 사용할 수 있습니다. variant별로 스타일/placeholder/최대글자수 등이 자동 적용됩니다.', - }, - }, - }, - tags: ['autodocs'], - argTypes: { - variant: { - control: 'radio', - options: ['bigGoal', 'subGoal', 'todo'], - description: '텍스트필드 타입 (상위목표/하위목표/할 일)', - }, - value: { control: 'text', description: '입력값' }, - placeholder: { control: 'text', description: 'placeholder (미입력 시 자동 적용)' }, - maxLength: { control: 'number', description: '최대 글자수 (상위목표만 30자 제한)' }, - disabled: { control: 'boolean', description: '비활성화 여부' }, - onChange: { action: 'changed', description: '입력값 변경 이벤트' }, - }, -}; - -export default meta; -type Story = StoryObj; - -export const Interactive: Story = { - args: { - variant: 'bigGoal', - value: '', - onChange: () => {}, - placeholder: '', - maxLength: 30, - disabled: false, - }, - render: (args) => { - const [value, setValue] = useState(''); - return ; - }, - parameters: { - docs: { - description: { - story: '실제 입력/클리어/포커스 등 모든 상태를 직접 테스트할 수 있습니다.', - }, - }, - }, -}; - -const variants = [ - { variant: 'bigGoal', label: '상위목표 (bigGoal)' }, - { variant: 'subGoal', label: '하위목표 (subGoal)' }, - { variant: 'todo', label: '할 일 (todo)' }, -] as const; - -export const Variants: Story = { - render: () => { - const [values, setValues] = useState(['', '', '']); - return ( -
- {variants.map(({ variant, label }, idx) => ( -
-
{label}
- setValues((vals) => vals.map((val, i) => (i === idx ? v : val)))} - /> -
- ))} -
- ); - }, - parameters: { - docs: { - description: { - story: 'variant별 스타일/placeholder/최대글자수 자동 적용 예시입니다.', - }, - }, - }, -}; - -export const Disabled: Story = { - args: { - variant: 'bigGoal', - value: '비활성화 예시', - onChange: () => {}, - disabled: true, - }, - render: (args) => , - parameters: { - docs: { - description: { - story: 'disabled=true 시 스타일/동작 예시입니다.', - }, - }, - }, -}; diff --git a/src/common/component/MandalartTextField/MandalartTextField.tsx b/src/common/component/MandalartTextField/MandalartTextField.tsx deleted file mode 100644 index 8a0cce25..00000000 --- a/src/common/component/MandalartTextField/MandalartTextField.tsx +++ /dev/null @@ -1,195 +0,0 @@ -import { useReducer, useRef } from 'react'; - -import type { TextFieldProps, TextFieldVariant } from './MandalartTextField.types'; -import * as styles from './MandalartTextField.css'; -import { DEFAULT_PLACEHOLDER, BIG_GOAL_MAX_LENGTH } from './constant/constants'; - -import IcTextdelete from '@/assets/svg/IcTextdelete'; - -type FieldState = 'default' | 'clicked' | 'typing' | 'filled' | 'hover'; - -type State = { - isFocused: boolean; - isHovered: boolean; - isComposing: boolean; -}; - -type Action = - | { type: 'FOCUS' } - | { type: 'BLUR' } - | { type: 'HOVER_ENTER' } - | { type: 'HOVER_LEAVE' } - | { type: 'COMPOSE_START' } - | { type: 'COMPOSE_END' }; - -const reducer = (state: State, action: Action): State => { - switch (action.type) { - case 'FOCUS': - return { ...state, isFocused: true }; - case 'BLUR': - return { ...state, isFocused: false }; - case 'HOVER_ENTER': - return { ...state, isHovered: true }; - case 'HOVER_LEAVE': - return { ...state, isHovered: false }; - case 'COMPOSE_START': - return { ...state, isComposing: true }; - case 'COMPOSE_END': - return { ...state, isComposing: false }; - default: - return state; - } -}; - -const getFieldState = (isFocused: boolean, isHovered: boolean, hasValue: boolean): FieldState => { - if (isFocused) { - return hasValue ? 'typing' : 'clicked'; - } - if (hasValue) { - return 'filled'; - } - if (isHovered) { - return 'hover'; - } - return 'default'; -}; - -const getWrapperClass = (variant: TextFieldVariant, state: FieldState) => { - const variantStyles = { - bigGoal: styles.bigGoalVariants, - subGoal: styles.subGoalVariants, - todo: styles.todoVariants, - }; - return variantStyles[variant][state]; -}; - -const getClearButtonClass = (variant: TextFieldVariant) => - variant === 'bigGoal' ? styles.clearButton : styles.clearButtonSmall; - -const getMaxLength = (variant: TextFieldVariant, maxLength?: number) => { - if (variant === 'bigGoal') { - return maxLength ?? BIG_GOAL_MAX_LENGTH; - } - if (variant === 'subGoal' || variant === 'todo') { - return 30; - } - return undefined; -}; - -const getPlaceholder = (variant: TextFieldVariant, placeholder?: string) => - placeholder ?? DEFAULT_PLACEHOLDER[variant]; - -const TextField = ({ - variant = 'bigGoal', - value, - onChange, - placeholder, - maxLength, - disabled = false, - onKeyDown, - onBlur, - onCompositionStart, - onCompositionEnd, -}: TextFieldProps) => { - const [state, dispatch] = useReducer(reducer, { - isFocused: false, - isHovered: false, - isComposing: false, - }); - const inputRef = useRef(null); - - const hasValue = Boolean(value); - const fieldState = getFieldState(state.isFocused, state.isHovered, hasValue); - - const wrapperClass = getWrapperClass(variant, fieldState); - const clearButtonClass = getClearButtonClass(variant); - const effectiveMaxLength = getMaxLength(variant, maxLength); - const effectivePlaceholder = getPlaceholder(variant, placeholder); - - const handleInputChange = (e: React.ChangeEvent) => { - onChange(e.target.value); - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && !state.isComposing) { - e.preventDefault(); - e.stopPropagation(); - e.currentTarget.blur(); - } - }; - - const handleClearClick = (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - onChange(''); - setTimeout(() => inputRef.current?.focus(), 0); - }; - - const handleContainerClick = () => { - if (!disabled) { - inputRef.current?.focus(); - } - }; - - const handleWrapperKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - e.preventDefault(); - handleContainerClick(); - } - }; - - return ( -
!disabled && dispatch({ type: 'HOVER_ENTER' })} - onMouseLeave={() => dispatch({ type: 'HOVER_LEAVE' })} - onClick={handleContainerClick} - onKeyDown={handleWrapperKeyDown} - role="button" - tabIndex={0} - > - dispatch({ type: 'FOCUS' })} - onBlur={(e) => { - dispatch({ type: 'BLUR' }); - if (onBlur) { - onBlur(e); - } - }} - onKeyDown={onKeyDown ?? handleKeyDown} - onCompositionStart={(e) => { - dispatch({ type: 'COMPOSE_START' }); - if (onCompositionStart) { - onCompositionStart(e); - } - }} - onCompositionEnd={(e) => { - dispatch({ type: 'COMPOSE_END' }); - if (onCompositionEnd) { - onCompositionEnd(e); - } - }} - maxLength={effectiveMaxLength} - /> - {fieldState === 'typing' && ( - - )} -
- ); -}; - -export default TextField; diff --git a/src/common/component/MandalartTextField/MandalartTextField.types.ts b/src/common/component/MandalartTextField/MandalartTextField.types.ts deleted file mode 100644 index 86b4ee1a..00000000 --- a/src/common/component/MandalartTextField/MandalartTextField.types.ts +++ /dev/null @@ -1,14 +0,0 @@ -export type TextFieldVariant = 'bigGoal' | 'subGoal' | 'todo'; - -export interface TextFieldProps { - variant?: TextFieldVariant; - value: string; - onChange: (value: string) => void; - placeholder?: string; - maxLength?: number; - disabled?: boolean; - onKeyDown?: React.KeyboardEventHandler; - onBlur?: React.FocusEventHandler; - onCompositionStart?: React.CompositionEventHandler; - onCompositionEnd?: React.CompositionEventHandler; -} diff --git a/src/common/component/MandalartTextField/constant/constants.ts b/src/common/component/MandalartTextField/constant/constants.ts deleted file mode 100644 index a0e9984e..00000000 --- a/src/common/component/MandalartTextField/constant/constants.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const DEFAULT_PLACEHOLDER = { - bigGoal: '이루고 싶은 목표를 작성하세요', - subGoal: '세부 목표를 입력해주세요', - todo: '할 일을 입력해주세요', -} as const; - -export const BIG_GOAL_MAX_LENGTH = 30; diff --git a/src/common/component/MandalartTextField/index.ts b/src/common/component/MandalartTextField/index.ts deleted file mode 100644 index a4f6c7b0..00000000 --- a/src/common/component/MandalartTextField/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './MandalartTextField'; -export * from './MandalartTextField.types'; From 0b898fdd988c42a88cb0dde4f9ec0bf358108535 Mon Sep 17 00:00:00 2001 From: Jisu Shin Date: Sat, 13 Sep 2025 14:52:06 +0900 Subject: [PATCH 08/30] =?UTF-8?q?refactor:=20SignupTextField=EC=97=90=20Ba?= =?UTF-8?q?seTextfield=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TextField/signup/SignupTextField.css.ts | 99 ++++++++++++++ .../signup/SignupTextField.stories.tsx | 48 +++++++ .../TextField/signup/SignupTextField.tsx | 123 ++++++++++++++++++ .../component/TextField/signup/constants.ts | 14 ++ .../component/TextField/signup/format.ts | 10 ++ .../component/TextField/signup/index.ts | 3 + .../component/TextField/signup/validation.ts | 62 +++++++++ 7 files changed, 359 insertions(+) create mode 100644 src/common/component/TextField/signup/SignupTextField.css.ts create mode 100644 src/common/component/TextField/signup/SignupTextField.stories.tsx create mode 100644 src/common/component/TextField/signup/SignupTextField.tsx create mode 100644 src/common/component/TextField/signup/constants.ts create mode 100644 src/common/component/TextField/signup/format.ts create mode 100644 src/common/component/TextField/signup/index.ts create mode 100644 src/common/component/TextField/signup/validation.ts diff --git a/src/common/component/TextField/signup/SignupTextField.css.ts b/src/common/component/TextField/signup/SignupTextField.css.ts new file mode 100644 index 00000000..ae6d1384 --- /dev/null +++ b/src/common/component/TextField/signup/SignupTextField.css.ts @@ -0,0 +1,99 @@ +import { style, styleVariants } from '@vanilla-extract/css'; + +import { colors, fonts } from '@/style/token'; + +const box = { + display: 'flex', + width: '52.2rem', + height: '5rem', + padding: '1.4rem 2rem', + alignItems: 'center', + flexShrink: 0, + borderRadius: '8px', +}; + +export const inputBase = style({ + display: 'block', + width: '100%', + height: '100%', + background: 'transparent', + border: 'none', + outline: 'none', + padding: 0, + color: 'inherit', + textAlign: 'left', +}); + +export const inputFont = style([inputBase, fonts.body03]); + +const createInputStateVariants = () => + styleVariants({ + default: { color: colors.grey6 }, + clicked: { color: colors.grey6 }, + typing: { color: colors.grey10 }, + filled: { color: colors.grey10 }, + hover: { color: colors.grey6 }, + locked: { color: colors.grey5 }, + }); + +export const inputState = createInputStateVariants(); + +const createBoxVariants = (activeBorderColor: string) => { + const baseDefault = { + ...box, + border: `2px solid transparent`, + background: colors.grey4, + } as const; + const baseActive = { + ...box, + border: `2px solid ${activeBorderColor}`, + background: colors.grey3, + } as const; + const baseHoverOrFilled = { + ...box, + border: `2px solid transparent`, + background: colors.grey4, + } as const; + + return styleVariants({ + default: baseDefault, + clicked: baseActive, + typing: { ...baseActive, justifyContent: 'space-between' }, + filled: baseHoverOrFilled, + hover: { ...baseHoverOrFilled, background: colors.grey3 }, + locked: { ...baseHoverOrFilled, justifyContent: 'space-between', pointerEvents: 'none' }, + }); +}; + +export const fieldBox = createBoxVariants(colors.blue06); + +export const clearButton = style({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: '2rem', + height: '2rem', + background: 'none', + border: 'none', + padding: 0, + cursor: 'pointer', +}); + +export const lockIcon = style({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: '2rem', + height: '2rem', + color: colors.grey5, +}); + +export const errorContainer = style({ + display: 'flex', + width: '52.2rem', + flexDirection: 'column', + alignItems: 'flex-start', + gap: '0.4rem', +}); + +export const errorText = style([fonts.caption02, { color: colors.error01, alignSelf: 'stretch' }]); diff --git a/src/common/component/TextField/signup/SignupTextField.stories.tsx b/src/common/component/TextField/signup/SignupTextField.stories.tsx new file mode 100644 index 00000000..e3d0ff12 --- /dev/null +++ b/src/common/component/TextField/signup/SignupTextField.stories.tsx @@ -0,0 +1,48 @@ +import { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { SignupTextField } from './'; + +const meta = { + title: 'Components/TextField/SignupTextField', + component: SignupTextField, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Name: Story = { + args: { variant: 'name', value: '', onChange: () => {} }, + render: () => { + const [value, setValue] = useState(''); + return ; + }, +}; + +export const EmailLocked: Story = { + args: { variant: 'email', value: '', onChange: () => {} }, + render: () => { + const [value] = useState('user@example.com'); + return {}} disabled />; + }, +}; + +export const Birth: Story = { + args: { variant: 'birth', value: '', onChange: () => {} }, + render: () => { + const [value, setValue] = useState(''); + return ; + }, +}; + +export const Job: Story = { + args: { variant: 'job', value: '', onChange: () => {} }, + render: () => { + const [value, setValue] = useState(''); + return ; + }, +}; diff --git a/src/common/component/TextField/signup/SignupTextField.tsx b/src/common/component/TextField/signup/SignupTextField.tsx new file mode 100644 index 00000000..d7a567ba --- /dev/null +++ b/src/common/component/TextField/signup/SignupTextField.tsx @@ -0,0 +1,123 @@ +import { useCallback, useState } from 'react'; + +import IcLock from '@/assets/svg/IcLock'; +import IcMediumTextdelete from '@/assets/svg/IcMediumTextdelete'; + +import BaseTextField from '../BaseTextField'; +import type { SignupVariant } from './constants'; +import { DEFAULT_PLACEHOLDER } from './constants'; +import * as s from './SignupTextField.css'; +import { formatBirthDate } from './format'; +import { validateSignupField } from './validation'; + +type FieldState = 'default' | 'clicked' | 'typing' | 'filled' | 'hover' | 'locked'; + +const pickPlaceholder = (variant: SignupVariant, placeholder?: string) => + placeholder ?? DEFAULT_PLACEHOLDER[variant]; + +const computeFieldState = (args: { + hasValue: boolean; + isFocused: boolean; + isHovered: boolean; + locked: boolean; +}): FieldState => { + const { hasValue, isFocused, isHovered, locked } = args; + if (locked) return 'locked'; + if (isFocused) return hasValue ? 'typing' : 'clicked'; + if (hasValue) return 'filled'; + return isHovered ? 'hover' : 'default'; +}; + +export interface SignupTextFieldProps { + variant: SignupVariant; + value: string; + onChange: (value: string) => void; + placeholder?: string; + disabled?: boolean; +} + +const SignupTextField = ({ + variant, + value, + onChange, + placeholder, + disabled, +}: SignupTextFieldProps) => { + const [isHovered, setIsHovered] = useState(false); + + const effectivePlaceholder = pickPlaceholder(variant, placeholder); + + const handleMouseEnter = useCallback(() => setIsHovered(true), []); + const handleMouseLeave = useCallback(() => setIsHovered(false), []); + + const handleChange = useCallback( + (nextValue: string) => { + if (variant === 'birth') { + onChange(formatBirthDate(nextValue)); + return; + } + onChange(nextValue); + }, + [onChange, variant], + ); + + return ( + + {({ inputProps, hasValue, isFocused, clear }) => { + const state = computeFieldState({ hasValue, isFocused, isHovered, locked: !!disabled }); + const wrapperClass = s.fieldBox[state]; + const inputClass = s.inputState[state]; + + let computedInvalid: boolean | undefined; + let computedError: string | undefined; + const signupType = variant === 'email' ? undefined : (variant as 'name' | 'birth' | 'job'); + if (signupType) { + const msg = validateSignupField(signupType, value); + if (msg) { + computedInvalid = true; + computedError = msg; + } + } + + return ( +
+
+ + {state === 'typing' && variant !== 'email' && ( + + )} + {state === 'locked' && ( + + + + )} +
+ {computedInvalid && ( +

+ {computedError || '유효하지 않은 입력입니다.'} +

+ )} +
+ ); + }} +
+ ); +}; + +export default SignupTextField; diff --git a/src/common/component/TextField/signup/constants.ts b/src/common/component/TextField/signup/constants.ts new file mode 100644 index 00000000..c2132e05 --- /dev/null +++ b/src/common/component/TextField/signup/constants.ts @@ -0,0 +1,14 @@ +export type SignupVariant = 'name' | 'email' | 'birth' | 'job'; + +export const DEFAULT_PLACEHOLDER = { + name: '이름을 입력해주세요', + email: '', + birth: '생년월일 8자를 입력해주세요', + job: '직업을 입력해주세요', +} as const; + +export const ERROR_MESSAGES = { + name: '한글/영문 10자 이하로 입력해주세요', + birth: '정확한 생년월일 8자를 입력해주세요', + job: '한글/영문 15자 이하로 입력해주세요', +} as const; diff --git a/src/common/component/TextField/signup/format.ts b/src/common/component/TextField/signup/format.ts new file mode 100644 index 00000000..671b1c6e --- /dev/null +++ b/src/common/component/TextField/signup/format.ts @@ -0,0 +1,10 @@ +export function formatBirthDate(value: string): string { + const digits = value.replace(/[^0-9]/g, '').slice(0, 8); + if (digits.length < 5) { + return digits; + } + if (digits.length < 7) { + return `${digits.slice(0, 4)}-${digits.slice(4)}`; + } + return `${digits.slice(0, 4)}-${digits.slice(4, 6)}-${digits.slice(6)}`; +} diff --git a/src/common/component/TextField/signup/index.ts b/src/common/component/TextField/signup/index.ts new file mode 100644 index 00000000..05ca3bbb --- /dev/null +++ b/src/common/component/TextField/signup/index.ts @@ -0,0 +1,3 @@ +export { default as SignupTextField } from './SignupTextField'; +export type { SignupTextFieldProps } from './SignupTextField'; +export type { SignupVariant } from './constants'; diff --git a/src/common/component/TextField/signup/validation.ts b/src/common/component/TextField/signup/validation.ts new file mode 100644 index 00000000..b588ba52 --- /dev/null +++ b/src/common/component/TextField/signup/validation.ts @@ -0,0 +1,62 @@ +import { ERROR_MESSAGES } from './constants'; + +export const NAME_REGEX = /^[A-Za-z가-힣ㄱ-ㅎㅏ-ㅣ\s]{1,10}$/; +export const JOB_REGEX = /^[A-Za-z가-힣ㄱ-ㅎㅏ-ㅣ\s]{1,15}$/; +export const BIRTH_REGEX = /^\d{4}-\d{2}-\d{2}$/; + +export function validateName(value: string): string | undefined { + const v = (value ?? '').trim(); + if (!v) return undefined; + if (!NAME_REGEX.test(v)) return ERROR_MESSAGES.name; + return undefined; +} + +export function validateBirth(value: string): string | undefined { + const v = (value ?? '').trim(); + if (!v) return undefined; + if (!BIRTH_REGEX.test(v)) return ERROR_MESSAGES.birth; + + const [yy, mm, dd] = v.split('-'); + const yearNum = parseInt(yy, 10); + const monthNum = parseInt(mm, 10); + const dayNum = parseInt(dd, 10); + if (monthNum < 1 || monthNum > 12) return ERROR_MESSAGES.birth; + if (dayNum < 1 || dayNum > 31) return ERROR_MESSAGES.birth; + + const exact = new Date(yearNum, monthNum - 1, dayNum); + if ( + exact.getFullYear() !== yearNum || + exact.getMonth() !== monthNum - 1 || + exact.getDate() !== dayNum + ) { + return ERROR_MESSAGES.birth; + } + + const now = new Date(); + const utc = now.getTime() + now.getTimezoneOffset() * 60000; + const koreaNow = new Date(utc + 9 * 60 * 60 * 1000); + const todayIso = koreaNow.toISOString().slice(0, 10); + const [ty, tm, td] = todayIso.split('-'); + const today = new Date(`${ty}-${tm}-${td}`); + const inputDate = new Date(`${yearNum}-${mm}-${dd}`); + if (inputDate.getTime() > today.getTime()) return ERROR_MESSAGES.birth; + + return undefined; +} + +export function validateJob(value: string): string | undefined { + const v = (value ?? '').trim(); + if (!v) return undefined; + if (!JOB_REGEX.test(v)) return ERROR_MESSAGES.job; + return undefined; +} + +export function validateSignupField( + type: 'name' | 'birth' | 'job', + value: string, +): string | undefined { + if (type === 'name') return validateName(value); + if (type === 'birth') return validateBirth(value); + if (type === 'job') return validateJob(value); + return undefined; +} From 5d072dd5b3e48caafb54573cf70c6d13f5977cf7 Mon Sep 17 00:00:00 2001 From: Jisu Shin Date: Sat, 13 Sep 2025 14:52:26 +0900 Subject: [PATCH 09/30] =?UTF-8?q?fix:=20aria-disabled=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/component/TextField/BaseTextField.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/common/component/TextField/BaseTextField.tsx b/src/common/component/TextField/BaseTextField.tsx index f4dc7b80..3acae808 100644 --- a/src/common/component/TextField/BaseTextField.tsx +++ b/src/common/component/TextField/BaseTextField.tsx @@ -134,7 +134,6 @@ const BaseTextField = ({ maxLength: typeof maxLength === 'number' ? maxLength : undefined, readOnly: isLocked, 'aria-readonly': isLocked, - 'aria-disabled': undefined, 'aria-invalid': invalid || undefined, }; return base; From 86bdb17c76e33dc37f9b174a2aa45bed8e8c43bf Mon Sep 17 00:00:00 2001 From: Jisu Shin Date: Sat, 13 Sep 2025 14:54:28 +0900 Subject: [PATCH 10/30] =?UTF-8?q?refactor:=20formatBirthDate=EB=A5=BC=20Te?= =?UTF-8?q?xtField/signup/validation.ts=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= =?UTF-8?q?=ED=95=98=EA=B3=A0=20common/util=EC=97=90=EC=84=9C=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/util/format.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/common/util/format.ts b/src/common/util/format.ts index 9a56edce..2ee6d00e 100644 --- a/src/common/util/format.ts +++ b/src/common/util/format.ts @@ -1,14 +1,3 @@ -export function formatBirthDate(value: string): string { - const digits = value.replace(/[^0-9]/g, '').slice(0, 8); - if (digits.length < 5) { - return digits; - } - if (digits.length < 7) { - return `${digits.slice(0, 4)}.${digits.slice(4)}`; - } - return `${digits.slice(0, 4)}.${digits.slice(4, 6)}.${digits.slice(6)}`; -} - export function formatDateDot(date: Date): string { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); From 8a6925719b29efbf12369d17ce17aa961e7fad8c Mon Sep 17 00:00:00 2001 From: Jisu Shin Date: Sat, 13 Sep 2025 15:01:37 +0900 Subject: [PATCH 11/30] =?UTF-8?q?chore:=20import=EB=AC=B8=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/component/TextField/signup/SignupTextField.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/common/component/TextField/signup/SignupTextField.tsx b/src/common/component/TextField/signup/SignupTextField.tsx index d7a567ba..561f8223 100644 --- a/src/common/component/TextField/signup/SignupTextField.tsx +++ b/src/common/component/TextField/signup/SignupTextField.tsx @@ -4,8 +4,7 @@ import IcLock from '@/assets/svg/IcLock'; import IcMediumTextdelete from '@/assets/svg/IcMediumTextdelete'; import BaseTextField from '../BaseTextField'; -import type { SignupVariant } from './constants'; -import { DEFAULT_PLACEHOLDER } from './constants'; +import { DEFAULT_PLACEHOLDER, type SignupVariant } from './constants'; import * as s from './SignupTextField.css'; import { formatBirthDate } from './format'; import { validateSignupField } from './validation'; From 4a25526e894a6ea1b04a80a4e7bc7b8acfebe4e9 Mon Sep 17 00:00:00 2001 From: Jisu Shin Date: Sat, 13 Sep 2025 17:39:18 +0900 Subject: [PATCH 12/30] =?UTF-8?q?refactor:=20ModifyTextField=EC=97=90=20Ba?= =?UTF-8?q?seTextfield=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TextField/modify/ModifyTextField.css.ts | 100 ++++++++++++++++++ .../modify/ModifyTextField.stories.tsx | 46 ++++++++ .../TextField/modify/ModifyTextField.tsx | 86 +++++++++++++++ .../component/TextField/modify/constants.ts | 6 ++ .../component/TextField/modify/index.ts | 3 + 5 files changed, 241 insertions(+) create mode 100644 src/common/component/TextField/modify/ModifyTextField.css.ts create mode 100644 src/common/component/TextField/modify/ModifyTextField.stories.tsx create mode 100644 src/common/component/TextField/modify/ModifyTextField.tsx create mode 100644 src/common/component/TextField/modify/constants.ts create mode 100644 src/common/component/TextField/modify/index.ts diff --git a/src/common/component/TextField/modify/ModifyTextField.css.ts b/src/common/component/TextField/modify/ModifyTextField.css.ts new file mode 100644 index 00000000..e861a96a --- /dev/null +++ b/src/common/component/TextField/modify/ModifyTextField.css.ts @@ -0,0 +1,100 @@ +import { style, styleVariants } from '@vanilla-extract/css'; + +import { colors, fonts } from '@/style/token'; + +export const inputBase = style({ + display: 'block', + width: '100%', + height: '100%', + background: 'transparent', + border: 'none', + outline: 'none', + padding: 0, + color: 'inherit', + textAlign: 'left', +}); + +const makeBox = (width: string, height: string, padding: string) => ({ + display: 'flex', + alignItems: 'center', + flexShrink: 0, + width, + height, + padding, + borderRadius: '8px', +}); + +const subGoalBoxBase = makeBox('62rem', '6.6rem', '1.6rem 2rem'); +const todoBoxBase = makeBox('48.5rem', '5.6rem', '1.4rem 2rem'); + +const subGoalBase = { + ...subGoalBoxBase, + border: `3px solid ${colors.gradient04}`, + background: colors.grey2, +}; + +export const subGoalBox = styleVariants({ + default: subGoalBase, + hover: { ...subGoalBase, cursor: 'pointer' }, + clicked: subGoalBase, + typing: subGoalBase, + filled: subGoalBase, +}); + +const createVariants = (box: object, opts: { borderWidth: string; activeBorderColor: string }) => { + const { borderWidth, activeBorderColor } = opts; + const baseDefault = { + ...box, + border: `${borderWidth} solid transparent`, + background: colors.grey4, + }; + const baseActive = { + ...box, + border: `${borderWidth} solid ${activeBorderColor}`, + background: colors.grey3, + }; + const baseHoverOrFilled = { + ...box, + border: `${borderWidth} solid transparent`, + background: colors.grey4, + }; + return { + default: baseDefault, + clicked: baseActive, + typing: { ...baseActive, justifyContent: 'space-between' }, + filled: baseHoverOrFilled, + hover: { ...baseHoverOrFilled, background: colors.grey3 }, + } as const; +}; + +export const todoBox = styleVariants( + createVariants(todoBoxBase, { borderWidth: '2px', activeBorderColor: colors.blue06 }), +); + +export const subGoalInput = styleVariants({ + default: [inputBase, fonts.subtitle03, { color: colors.grey6 }], + hover: [inputBase, fonts.subtitle03, { color: colors.grey11 }], + clicked: [inputBase, fonts.subtitle03, { color: colors.grey11 }], + typing: [inputBase, fonts.subtitle03, { color: colors.grey11 }], + filled: [inputBase, fonts.subtitle03, { color: colors.grey11 }], +}); + +export const todoInput = styleVariants({ + default: [inputBase, fonts.subtitle03, { color: colors.grey6 }], + hover: [inputBase, fonts.subtitle03, { color: colors.grey6 }], + clicked: [inputBase, fonts.subtitle03, { color: colors.grey6 }], + typing: [inputBase, fonts.subtitle03, { color: colors.grey10 }], + filled: [inputBase, fonts.subtitle02, { color: colors.grey10 }], +}); + +export const clearButton = style({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: '2.4rem', + height: '2.4rem', + background: 'none', + border: 'none', + padding: 0, + cursor: 'pointer', +}); diff --git a/src/common/component/TextField/modify/ModifyTextField.stories.tsx b/src/common/component/TextField/modify/ModifyTextField.stories.tsx new file mode 100644 index 00000000..7bfb9f0a --- /dev/null +++ b/src/common/component/TextField/modify/ModifyTextField.stories.tsx @@ -0,0 +1,46 @@ +import { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { ModifyTextField } from './'; + +const meta = { + title: 'Components/TextField/ModifyTextField', + component: ModifyTextField, + parameters: { layout: 'centered' }, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const SubGoalFilled: Story = { + args: { variant: 'subGoal', value: '', onChange: () => {} }, + render: () => { + const [value, setValue] = useState('나의 하위 목표'); + return ; + }, +}; + +export const SubGoalEmpty: Story = { + args: { variant: 'subGoal', value: '', onChange: () => {} }, + render: () => { + const [value, setValue] = useState(''); + return ; + }, +}; + +export const TodoEmpty: Story = { + args: { variant: 'todo', value: '', onChange: () => {} }, + render: () => { + const [value, setValue] = useState(''); + return ; + }, +}; + +export const TodoFilled: Story = { + args: { variant: 'todo', value: '', onChange: () => {} }, + render: () => { + const [value, setValue] = useState('완료된 할 일'); + return ; + }, +}; diff --git a/src/common/component/TextField/modify/ModifyTextField.tsx b/src/common/component/TextField/modify/ModifyTextField.tsx new file mode 100644 index 00000000..e9933450 --- /dev/null +++ b/src/common/component/TextField/modify/ModifyTextField.tsx @@ -0,0 +1,86 @@ +import { useCallback, useState } from 'react'; + +import IcTextdelete from '@/assets/svg/IcTextdelete'; + +import BaseTextField from '../BaseTextField'; +import { type ModifyVariant, DEFAULT_PLACEHOLDER } from './constants.ts'; +import * as s from './ModifyTextField.css.ts'; + +type FieldState = 'default' | 'hover' | 'clicked' | 'typing' | 'filled'; + +const pickPlaceholder = (variant: ModifyVariant, placeholder?: string) => + placeholder ?? DEFAULT_PLACEHOLDER[variant]; + +const computeFieldState = (args: { + hasValue: boolean; + isFocused: boolean; + isHovered: boolean; +}): FieldState => { + const { hasValue, isFocused, isHovered } = args; + if (isFocused) return hasValue ? 'typing' : 'clicked'; + if (hasValue) return 'filled'; + return isHovered ? 'hover' : 'default'; +}; + +export interface ModifyTextFieldProps { + variant: ModifyVariant; + value: string; + onChange: (value: string) => void; + placeholder?: string; + disabled?: boolean; +} + +const ModifyTextField = ({ + variant, + value, + onChange, + placeholder, + disabled, +}: ModifyTextFieldProps) => { + const [isHovered, setIsHovered] = useState(false); + + const effectivePlaceholder = pickPlaceholder(variant, placeholder); + + const handleMouseEnter = useCallback(() => setIsHovered(true), []); + const handleMouseLeave = useCallback(() => setIsHovered(false), []); + + const handleChange = useCallback( + (nextValue: string) => { + onChange(nextValue); + }, + [onChange], + ); + + const isTodo = variant === 'todo'; + + return ( + + {({ inputProps, hasValue, isFocused, clear }) => { + const state = computeFieldState({ hasValue, isFocused, isHovered }); + const wrapperClass = variant === 'subGoal' ? s.subGoalBox[state] : s.todoBox[state]; + const inputClass = variant === 'subGoal' ? s.subGoalInput[state] : s.todoInput[state]; + + return ( +
+
+ + {isTodo && state === 'typing' && ( + + )} +
+
+ ); + }} +
+ ); +}; + +export default ModifyTextField; diff --git a/src/common/component/TextField/modify/constants.ts b/src/common/component/TextField/modify/constants.ts new file mode 100644 index 00000000..802fcb67 --- /dev/null +++ b/src/common/component/TextField/modify/constants.ts @@ -0,0 +1,6 @@ +export type ModifyVariant = 'subGoal' | 'todo'; + +export const DEFAULT_PLACEHOLDER = { + subGoal: '세부 목표를 입력해주세요', + todo: '할 일을 입력해주세요', +} as const; diff --git a/src/common/component/TextField/modify/index.ts b/src/common/component/TextField/modify/index.ts new file mode 100644 index 00000000..f2fbf9c1 --- /dev/null +++ b/src/common/component/TextField/modify/index.ts @@ -0,0 +1,3 @@ +export { default as ModifyTextField } from './ModifyTextField'; +export type { ModifyTextFieldProps } from './ModifyTextField'; +export type { ModifyVariant } from './constants'; From c51c521e77b4b5ff5db99e01477390825e839068 Mon Sep 17 00:00:00 2001 From: Jisu Shin Date: Sat, 13 Sep 2025 17:39:33 +0900 Subject: [PATCH 13/30] =?UTF-8?q?remove:=20=EA=B8=B0=EC=A1=B4=20ModifyText?= =?UTF-8?q?Field=20=ED=8F=B4=EB=8D=94=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ModifyTextField/ModifyTextField.css.ts | 123 ------------ .../ModifyTextField/ModifyTextField.tsx | 190 ------------------ .../ModifyTextField/ModifyTextField.types.ts | 11 - .../component/ModifyTextField/index 2.ts | 2 - src/common/component/ModifyTextField/index.ts | 2 - .../useModifyTextFieldState.ts | 99 --------- 6 files changed, 427 deletions(-) delete mode 100644 src/common/component/ModifyTextField/ModifyTextField.css.ts delete mode 100644 src/common/component/ModifyTextField/ModifyTextField.tsx delete mode 100644 src/common/component/ModifyTextField/ModifyTextField.types.ts delete mode 100644 src/common/component/ModifyTextField/index 2.ts delete mode 100644 src/common/component/ModifyTextField/index.ts delete mode 100644 src/common/component/ModifyTextField/useModifyTextFieldState.ts diff --git a/src/common/component/ModifyTextField/ModifyTextField.css.ts b/src/common/component/ModifyTextField/ModifyTextField.css.ts deleted file mode 100644 index fc2a5b1d..00000000 --- a/src/common/component/ModifyTextField/ModifyTextField.css.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { style, styleVariants } from '@vanilla-extract/css'; - -import { colors, fonts } from '@/style/token'; - -const leftAligned = { textAlign: 'left' as const }; - -const subGoalBase = { - display: 'flex', - width: '62rem', - height: '6.6rem', - padding: '1.6rem 2rem', - alignItems: 'center', - flexShrink: 0, - borderRadius: '8px', - border: '3px solid transparent', - fontSize: '2.2rem', - fontWeight: 600, - lineHeight: '140%', - fontFamily: 'Pretendard', - ...leftAligned, -}; - -const todoBase = { - display: 'flex', - width: '48.5rem', - padding: '1.4rem 2rem', - alignItems: 'center', - borderRadius: '8px', - border: '2px solid transparent', - ...fonts.subtitle03, - ...leftAligned, -}; - -export const subGoalBaseClass = style({ ...subGoalBase }); -export const todoBaseClass = style({ ...todoBase }); - -export const inputBase = style({ - display: 'block', - width: '100%', - height: '100%', - background: 'transparent', - border: 'none', - outline: 'none', - padding: 0, - color: 'inherit', - textAlign: 'inherit', - '::placeholder': { - color: colors.grey6, - }, -}); - -export const subGoalVariants = styleVariants({ - filled: { - ...subGoalBase, - border: `3px solid ${colors.blue04}`, - background: colors.grey2, - color: colors.grey11, - cursor: 'pointer', - }, - empty: { - ...subGoalBase, - border: `3px solid ${colors.blue04}`, - background: colors.grey2, - color: colors.grey6, - cursor: 'pointer', - }, -}); - -export const todoVariants = styleVariants({ - modify_empty: { - ...todoBase, - background: colors.grey4, - color: colors.grey6, - cursor: 'pointer', - }, - modify_hover: { - ...todoBase, - background: colors.grey3, - color: colors.grey6, - cursor: 'pointer', - }, - modify_clicked: { - ...todoBase, - border: `2px solid ${colors.blue06}`, - background: colors.grey3, - color: colors.grey6, - cursor: 'pointer', - }, - modify_typing: { - ...todoBase, - border: `2px solid ${colors.blue06}`, - background: colors.grey3, - color: colors.grey10, - justifyContent: 'space-between', - }, - modify_filled: { - ...todoBase, - background: colors.grey4, - color: colors.grey10, - fontWeight: 600, - cursor: 'pointer', - }, -}); - -export const clearButton = style({ - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - width: '2.4rem', - height: '2.4rem', - background: 'none', - border: 'none', - padding: 0, - cursor: 'pointer', - flexShrink: 0, -}); - -export const inputContainer = style({ - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - flex: '1 0 0', -}); diff --git a/src/common/component/ModifyTextField/ModifyTextField.tsx b/src/common/component/ModifyTextField/ModifyTextField.tsx deleted file mode 100644 index 8a695987..00000000 --- a/src/common/component/ModifyTextField/ModifyTextField.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import type { ModifyTextFieldProps, ModifyTextFieldVariant } from './ModifyTextField.types'; -import * as styles from './ModifyTextField.css'; -import { useModifyTextFieldState, type Action } from './useModifyTextFieldState'; - -import { IcMediumTextdelete } from '@/assets/svg'; - -const DEFAULT_PLACEHOLDER: Record = { - subGoal: '세부 목표를 입력해주세요', - todo: '할 일을 입력해주세요', -}; - -// ====== 타입 정의 ====== -type SubGoalFieldState = 'filled' | 'empty'; -type TodoFieldState = - | 'modify_empty' - | 'modify_hover' - | 'modify_clicked' - | 'modify_typing' - | 'modify_filled'; - -// ====== 상태 결정 함수 분리 ====== -function getSubGoalFieldState(hasValue: boolean): SubGoalFieldState { - return hasValue ? 'filled' : 'empty'; -} - -function getTodoFieldState( - isFocused: boolean, - isHovered: boolean, - hasValue: boolean, -): TodoFieldState { - if (isFocused) { - return hasValue ? 'modify_typing' : 'modify_clicked'; - } - if (hasValue) { - return 'modify_filled'; - } - if (isHovered) { - return 'modify_hover'; - } - return 'modify_empty'; -} - -// ====== 스타일 결정 함수 ====== -function getWrapperClass( - variant: ModifyTextFieldVariant, - fieldState: SubGoalFieldState | TodoFieldState, -) { - if (variant === 'subGoal') { - return styles.subGoalVariants[fieldState as SubGoalFieldState]; - } - return styles.todoVariants[fieldState as TodoFieldState]; -} - -function getPlaceholder(variant: ModifyTextFieldVariant, placeholder?: string) { - return placeholder ?? DEFAULT_PLACEHOLDER[variant]; -} - -function getInputProps({ - value, - onChange, - onFocus, - onBlur, - handleKeyDown, - dispatch, - placeholder, - disabled, -}: { - value: string; - onChange: (value: string) => void; - onFocus?: () => void; - onBlur?: () => void; - handleKeyDown: (e: React.KeyboardEvent) => void; - dispatch: (action: Action) => void; - placeholder?: string; - disabled?: boolean; -}) { - return { - type: 'text', - value, - onChange: (e: React.ChangeEvent) => onChange(e.target.value), - onFocus: () => { - dispatch({ type: 'FOCUS' }); - onFocus?.(); - }, - onBlur: () => { - dispatch({ type: 'BLUR' }); - onBlur?.(); - }, - onKeyDown: handleKeyDown, - onCompositionStart: () => dispatch({ type: 'COMPOSE_START' }), - onCompositionEnd: () => dispatch({ type: 'COMPOSE_END' }), - placeholder, - disabled, - className: styles.inputBase, - }; -} - -function getWrapperProps({ - disabled, - handleWrapperClick, - handleWrapperKeyDown, - dispatch, - isFocused, -}: { - disabled?: boolean; - handleWrapperClick: () => void; - handleWrapperKeyDown: (e: React.KeyboardEvent) => void; - dispatch: (action: Action) => void; - isFocused: boolean; -}) { - return disabled - ? { tabIndex: -1 as const } - : { - role: 'button' as const, - tabIndex: 0 as const, - onClick: isFocused ? undefined : handleWrapperClick, - onKeyDown: handleWrapperKeyDown, - onMouseEnter: () => dispatch({ type: 'HOVER_ENTER' }), - onMouseLeave: () => dispatch({ type: 'HOVER_LEAVE' }), - }; -} - -function ClearButton({ onClick }: { onClick: (e: React.MouseEvent) => void }) { - return ( - - ); -} - -export default function ModifyTextField({ - variant = 'todo', - value, - onChange, - placeholder, - disabled = false, - onBlur, - onFocus, -}: ModifyTextFieldProps) { - const { - state, - dispatch, - inputRef, - handleKeyDown, - handleClearClick, - handleWrapperClick, - handleWrapperKeyDown, - } = useModifyTextFieldState({ onChange }); - - const hasValue = Boolean(value); - const fieldState = - variant === 'subGoal' - ? getSubGoalFieldState(hasValue) - : getTodoFieldState(state.isFocused, state.isHovered, hasValue); - const wrapperClass = getWrapperClass(variant, fieldState); - const effectivePlaceholder = getPlaceholder(variant, placeholder); - const inputProps = getInputProps({ - value, - onChange, - onFocus, - onBlur, - handleKeyDown, - dispatch, - placeholder: effectivePlaceholder, - disabled, - }); - const wrapperProps = getWrapperProps({ - disabled, - handleWrapperClick, - handleWrapperKeyDown, - dispatch, - isFocused: state.isFocused, - }); - const showClearButton = fieldState === 'modify_typing'; - - return ( -
-
- - {showClearButton && } -
-
- ); -} diff --git a/src/common/component/ModifyTextField/ModifyTextField.types.ts b/src/common/component/ModifyTextField/ModifyTextField.types.ts deleted file mode 100644 index 0dc6a5e0..00000000 --- a/src/common/component/ModifyTextField/ModifyTextField.types.ts +++ /dev/null @@ -1,11 +0,0 @@ -export type ModifyTextFieldVariant = 'subGoal' | 'todo'; - -export interface ModifyTextFieldProps { - variant?: ModifyTextFieldVariant; - value: string; - onChange: (value: string) => void; - placeholder?: string; - disabled?: boolean; - onBlur?: () => void; - onFocus?: () => void; -} diff --git a/src/common/component/ModifyTextField/index 2.ts b/src/common/component/ModifyTextField/index 2.ts deleted file mode 100644 index 15551a09..00000000 --- a/src/common/component/ModifyTextField/index 2.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './ModifyTextField'; -export type { ModifyTextFieldProps, ModifyTextFieldVariant } from './ModifyTextField.types'; diff --git a/src/common/component/ModifyTextField/index.ts b/src/common/component/ModifyTextField/index.ts deleted file mode 100644 index 15551a09..00000000 --- a/src/common/component/ModifyTextField/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './ModifyTextField'; -export type { ModifyTextFieldProps, ModifyTextFieldVariant } from './ModifyTextField.types'; diff --git a/src/common/component/ModifyTextField/useModifyTextFieldState.ts b/src/common/component/ModifyTextField/useModifyTextFieldState.ts deleted file mode 100644 index 05ac8ebd..00000000 --- a/src/common/component/ModifyTextField/useModifyTextFieldState.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { useReducer, useRef, useCallback } from 'react'; - -interface State { - isFocused: boolean; - isHovered: boolean; - isComposing: boolean; -} - -export type Action = - | { type: 'FOCUS' } - | { type: 'BLUR' } - | { type: 'HOVER_ENTER' } - | { type: 'HOVER_LEAVE' } - | { type: 'COMPOSE_START' } - | { type: 'COMPOSE_END' }; - -function reducer(state: State, action: Action): State { - switch (action.type) { - case 'FOCUS': { - return { ...state, isFocused: true }; - } - case 'BLUR': { - return { ...state, isFocused: false }; - } - case 'HOVER_ENTER': { - return { ...state, isHovered: true }; - } - case 'HOVER_LEAVE': { - return { ...state, isHovered: false }; - } - case 'COMPOSE_START': { - return { ...state, isComposing: true }; - } - case 'COMPOSE_END': { - return { ...state, isComposing: false }; - } - default: { - return state; - } - } -} - -export function useModifyTextFieldState({ onChange }: { onChange: (value: string) => void }) { - const [state, dispatch] = useReducer(reducer, { - isFocused: false, - isHovered: false, - isComposing: false, - }); - const inputRef = useRef(null); - - const handleKeyDown = useCallback( - (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && !state.isComposing) { - e.preventDefault(); - e.stopPropagation(); - e.currentTarget.blur(); - } - }, - [state.isComposing], - ); - - const handleClearClick = useCallback( - (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - onChange(''); - setTimeout(() => inputRef.current?.focus(), 0); - }, - [onChange], - ); - - const handleWrapperClick = useCallback(() => { - if (inputRef.current) { - inputRef.current.focus(); - const len = inputRef.current.value.length ?? 0; - inputRef.current.setSelectionRange(len, len); - } - }, []); - - const handleWrapperKeyDown = useCallback( - (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && !state.isFocused) { - e.preventDefault(); - handleWrapperClick(); - } - }, - [handleWrapperClick, state.isFocused], - ); - - return { - state, - dispatch, - inputRef, - handleKeyDown, - handleClearClick, - handleWrapperClick, - handleWrapperKeyDown, - }; -} From 0ed0cf711e36dcb494d260abc01bab662d39d975 Mon Sep 17 00:00:00 2001 From: Jisu Shin Date: Sat, 13 Sep 2025 17:46:00 +0900 Subject: [PATCH 14/30] =?UTF-8?q?fix:=20=EB=B9=8C=EB=93=9C=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/TextField/BaseTextField.tsx | 17 +++++-- .../mandalart/MandalartTextField.tsx | 8 +++- .../TextField/modify/ModifyTextField.tsx | 8 +++- .../TextField/signup/SignupTextField.tsx | 12 +++-- .../component/TextField/signup/validation.ts | 48 ++++++++++++++----- 5 files changed, 70 insertions(+), 23 deletions(-) diff --git a/src/common/component/TextField/BaseTextField.tsx b/src/common/component/TextField/BaseTextField.tsx index 3acae808..8305ca46 100644 --- a/src/common/component/TextField/BaseTextField.tsx +++ b/src/common/component/TextField/BaseTextField.tsx @@ -49,7 +49,9 @@ const BaseTextField = ({ const commit = useCallback( (reason: CommitReason) => { - if (lastCommittedRef.current === value) return; + if (lastCommittedRef.current === value) { + return; + } onCommit?.(value, reason); lastCommittedRef.current = value; }, @@ -58,15 +60,21 @@ const BaseTextField = ({ const clear = useCallback(() => { onChange(''); - if (onClear) onClear(); + if (onClear) { + onClear(); + } }, [onChange, onClear]); const handleChange = useCallback( (e: React.ChangeEvent) => { - if (isLocked) return; + if (isLocked) { + return; + } const raw = e.target.value; if (typeof maxLength === 'number' && raw.length > maxLength) { - if (isComposing) return; + if (isComposing) { + return; + } onChange(raw.slice(0, maxLength)); return; } @@ -148,6 +156,7 @@ const BaseTextField = ({ handleBlur, handleCompositionStart, handleCompositionEnd, + maxLength, ]); return ( diff --git a/src/common/component/TextField/mandalart/MandalartTextField.tsx b/src/common/component/TextField/mandalart/MandalartTextField.tsx index db32199f..0e2cce11 100644 --- a/src/common/component/TextField/mandalart/MandalartTextField.tsx +++ b/src/common/component/TextField/mandalart/MandalartTextField.tsx @@ -21,8 +21,12 @@ const computeFieldState = (args: { isHovered: boolean; }): FieldState => { const { hasValue, isFocused, isHovered } = args; - if (isFocused) return hasValue ? 'typing' : 'clicked'; - if (hasValue) return 'filled'; + if (isFocused) { + return hasValue ? 'typing' : 'clicked'; + } + if (hasValue) { + return 'filled'; + } return isHovered ? 'hover' : 'default'; }; diff --git a/src/common/component/TextField/modify/ModifyTextField.tsx b/src/common/component/TextField/modify/ModifyTextField.tsx index e9933450..a14f5e46 100644 --- a/src/common/component/TextField/modify/ModifyTextField.tsx +++ b/src/common/component/TextField/modify/ModifyTextField.tsx @@ -17,8 +17,12 @@ const computeFieldState = (args: { isHovered: boolean; }): FieldState => { const { hasValue, isFocused, isHovered } = args; - if (isFocused) return hasValue ? 'typing' : 'clicked'; - if (hasValue) return 'filled'; + if (isFocused) { + return hasValue ? 'typing' : 'clicked'; + } + if (hasValue) { + return 'filled'; + } return isHovered ? 'hover' : 'default'; }; diff --git a/src/common/component/TextField/signup/SignupTextField.tsx b/src/common/component/TextField/signup/SignupTextField.tsx index 561f8223..41b1fab0 100644 --- a/src/common/component/TextField/signup/SignupTextField.tsx +++ b/src/common/component/TextField/signup/SignupTextField.tsx @@ -21,9 +21,15 @@ const computeFieldState = (args: { locked: boolean; }): FieldState => { const { hasValue, isFocused, isHovered, locked } = args; - if (locked) return 'locked'; - if (isFocused) return hasValue ? 'typing' : 'clicked'; - if (hasValue) return 'filled'; + if (locked) { + return 'locked'; + } + if (isFocused) { + return hasValue ? 'typing' : 'clicked'; + } + if (hasValue) { + return 'filled'; + } return isHovered ? 'hover' : 'default'; }; diff --git a/src/common/component/TextField/signup/validation.ts b/src/common/component/TextField/signup/validation.ts index b588ba52..011ba92a 100644 --- a/src/common/component/TextField/signup/validation.ts +++ b/src/common/component/TextField/signup/validation.ts @@ -6,22 +6,34 @@ export const BIRTH_REGEX = /^\d{4}-\d{2}-\d{2}$/; export function validateName(value: string): string | undefined { const v = (value ?? '').trim(); - if (!v) return undefined; - if (!NAME_REGEX.test(v)) return ERROR_MESSAGES.name; + if (!v) { + return undefined; + } + if (!NAME_REGEX.test(v)) { + return ERROR_MESSAGES.name; + } return undefined; } export function validateBirth(value: string): string | undefined { const v = (value ?? '').trim(); - if (!v) return undefined; - if (!BIRTH_REGEX.test(v)) return ERROR_MESSAGES.birth; + if (!v) { + return undefined; + } + if (!BIRTH_REGEX.test(v)) { + return ERROR_MESSAGES.birth; + } const [yy, mm, dd] = v.split('-'); const yearNum = parseInt(yy, 10); const monthNum = parseInt(mm, 10); const dayNum = parseInt(dd, 10); - if (monthNum < 1 || monthNum > 12) return ERROR_MESSAGES.birth; - if (dayNum < 1 || dayNum > 31) return ERROR_MESSAGES.birth; + if (monthNum < 1 || monthNum > 12) { + return ERROR_MESSAGES.birth; + } + if (dayNum < 1 || dayNum > 31) { + return ERROR_MESSAGES.birth; + } const exact = new Date(yearNum, monthNum - 1, dayNum); if ( @@ -39,15 +51,21 @@ export function validateBirth(value: string): string | undefined { const [ty, tm, td] = todayIso.split('-'); const today = new Date(`${ty}-${tm}-${td}`); const inputDate = new Date(`${yearNum}-${mm}-${dd}`); - if (inputDate.getTime() > today.getTime()) return ERROR_MESSAGES.birth; + if (inputDate.getTime() > today.getTime()) { + return ERROR_MESSAGES.birth; + } return undefined; } export function validateJob(value: string): string | undefined { const v = (value ?? '').trim(); - if (!v) return undefined; - if (!JOB_REGEX.test(v)) return ERROR_MESSAGES.job; + if (!v) { + return undefined; + } + if (!JOB_REGEX.test(v)) { + return ERROR_MESSAGES.job; + } return undefined; } @@ -55,8 +73,14 @@ export function validateSignupField( type: 'name' | 'birth' | 'job', value: string, ): string | undefined { - if (type === 'name') return validateName(value); - if (type === 'birth') return validateBirth(value); - if (type === 'job') return validateJob(value); + if (type === 'name') { + return validateName(value); + } + if (type === 'birth') { + return validateBirth(value); + } + if (type === 'job') { + return validateJob(value); + } return undefined; } From 74fea9033707d52e3ebdb6a698cc98fa803cc29a Mon Sep 17 00:00:00 2001 From: Jisu Shin Date: Sat, 13 Sep 2025 18:20:23 +0900 Subject: [PATCH 15/30] =?UTF-8?q?remove:=20=EA=B8=B0=EC=A1=B4=20SignupText?= =?UTF-8?q?Field=20=ED=8F=B4=EB=8D=94=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SignupTextField/RenderInputContent.tsx | 54 ------- .../SignupTextField/SignupTextField.css.ts | 122 ---------------- .../SignupTextField/SignupTextField.mock 2.ts | 5 - .../SignupTextField/SignupTextField.mock.ts | 5 - .../SignupTextField/SignupTextField.tsx | 135 ------------------ .../SignupTextField/SignupTextField.types.ts | 14 -- .../component/SignupTextField/constants.ts | 5 - .../component/SignupTextField/index 2.ts | 2 - src/common/component/SignupTextField/index.ts | 2 - .../useSignupTextFieldState.ts | 98 ------------- .../component/SignupTextField/validation.ts | 51 ------- 11 files changed, 493 deletions(-) delete mode 100644 src/common/component/SignupTextField/RenderInputContent.tsx delete mode 100644 src/common/component/SignupTextField/SignupTextField.css.ts delete mode 100644 src/common/component/SignupTextField/SignupTextField.mock 2.ts delete mode 100644 src/common/component/SignupTextField/SignupTextField.mock.ts delete mode 100644 src/common/component/SignupTextField/SignupTextField.tsx delete mode 100644 src/common/component/SignupTextField/SignupTextField.types.ts delete mode 100644 src/common/component/SignupTextField/constants.ts delete mode 100644 src/common/component/SignupTextField/index 2.ts delete mode 100644 src/common/component/SignupTextField/index.ts delete mode 100644 src/common/component/SignupTextField/useSignupTextFieldState.ts delete mode 100644 src/common/component/SignupTextField/validation.ts diff --git a/src/common/component/SignupTextField/RenderInputContent.tsx b/src/common/component/SignupTextField/RenderInputContent.tsx deleted file mode 100644 index 8a79b9a6..00000000 --- a/src/common/component/SignupTextField/RenderInputContent.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React from 'react'; - -import IcSmallTextdelete from '@/assets/svg/IcSmallTextdelete'; -import IcLock from '@/assets/svg/IcLock'; - -interface RenderInputContentProps { - fieldState: string; - inputProps: React.InputHTMLAttributes; - value: string; - isLocked: boolean; - handleClearClick: (e: React.MouseEvent) => void; - styles: typeof import('./SignupTextField.css'); -} - -export function RenderInputContent({ - fieldState, - inputProps, - value, - isLocked, - handleClearClick, - styles, -}: RenderInputContentProps) { - if (fieldState === 'typing' || fieldState === 'typingError') { - return ( - <> - - {value && !isLocked && ( - - )} - - ); - } - if (fieldState === 'error') { - return ; - } - if (fieldState === 'locked') { - return ( -
- - -
- ); - } - return ; -} diff --git a/src/common/component/SignupTextField/SignupTextField.css.ts b/src/common/component/SignupTextField/SignupTextField.css.ts deleted file mode 100644 index de668c6d..00000000 --- a/src/common/component/SignupTextField/SignupTextField.css.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { styleVariants, style } from '@vanilla-extract/css'; - -import { colors } from '@/style/token/color.css'; -import { fonts } from '@/style/token/typography.css'; - -export const baseClass = style({ - display: 'flex', - width: '52.2rem', - height: '5rem', - padding: '1.4rem 2rem', - alignItems: 'center', - flexShrink: 0, - borderRadius: '8px', - border: '2px solid transparent', - ...fonts.body03, -}); - -export const fieldVariants = styleVariants({ - default: { - border: '2px solid transparent', - background: colors.grey4, - color: colors.grey6, - }, - clicked: { - border: `2px solid ${colors.blue06}`, - background: colors.grey3, - color: colors.grey6, - }, - typing: { - border: `2px solid ${colors.blue06}`, - background: colors.grey3, - color: colors.grey10, - }, - filled: { - border: '2px solid transparent', - background: colors.grey4, - color: colors.grey10, - }, - completed: { - border: '2px solid transparent', - background: colors.grey4, - color: colors.grey10, - }, - locked: { - border: '2px solid transparent', - background: colors.grey4, - color: colors.grey5, - }, - error: { - border: `2px solid ${colors.error01}`, - background: colors.grey4, - color: colors.grey10, - }, -}); - -export const inputContent = style({ - display: 'flex', - flex: 1, - alignItems: 'center', - justifyContent: 'space-between', - flexShrink: 0, -}); - -export const clearButton = style({ - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - width: '2rem', - height: '2rem', - background: 'none', - border: 'none', - padding: 0, - cursor: 'pointer', -}); - -export const iconClass = style({ - width: '2rem', - height: '2rem', - flexShrink: 0, -}); - -export const lockIconClass = style({ - width: '2rem', - height: '2rem', - flexShrink: 0, -}); - -export const errorMessageWrapper = style({ - display: 'flex', - width: '52.2rem', - flexDirection: 'column', - alignItems: 'flex-start', - gap: '0.4rem', -}); - -export const inputBase = style({ - display: 'block', - width: '100%', - height: '100%', - background: 'transparent', - border: 'none', - outline: 'none', - padding: 0, - color: 'inherit', -}); - -const makeInputStyle = (font: typeof fonts.body03) => - style({ - ...font, - '::placeholder': { - color: colors.grey6, - ...font, - }, - }); - -export const inputStyle = makeInputStyle(fonts.body03); - -export const errorMessage = style({ - alignSelf: 'stretch', - color: colors.error01, - ...fonts.caption02, -}); diff --git a/src/common/component/SignupTextField/SignupTextField.mock 2.ts b/src/common/component/SignupTextField/SignupTextField.mock 2.ts deleted file mode 100644 index 56ae2187..00000000 --- a/src/common/component/SignupTextField/SignupTextField.mock 2.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const MOCK_SIGNUP_DATA = { - name: '신지수?((#((3ㅓ우렁', - email: 'sjsz0811@hanyang.ac.kr', - birth: '2002-08-11', -}; diff --git a/src/common/component/SignupTextField/SignupTextField.mock.ts b/src/common/component/SignupTextField/SignupTextField.mock.ts deleted file mode 100644 index 56ae2187..00000000 --- a/src/common/component/SignupTextField/SignupTextField.mock.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const MOCK_SIGNUP_DATA = { - name: '신지수?((#((3ㅓ우렁', - email: 'sjsz0811@hanyang.ac.kr', - birth: '2002-08-11', -}; diff --git a/src/common/component/SignupTextField/SignupTextField.tsx b/src/common/component/SignupTextField/SignupTextField.tsx deleted file mode 100644 index 1c05a3f5..00000000 --- a/src/common/component/SignupTextField/SignupTextField.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import type { SignupTextFieldProps } from './SignupTextField.types'; -import * as styles from './SignupTextField.css'; -import { validateField } from './validation'; -import { useSignupTextFieldState } from './useSignupTextFieldState'; -import { RenderInputContent } from './RenderInputContent'; - -import { formatBirthDate } from '@/common/util/format'; - -function getFieldState( - isFocused: boolean, - hasValue: boolean, - error: boolean, - isLocked: boolean, -): keyof typeof styles.fieldVariants | 'typingError' { - if (isLocked) { - return 'locked'; - } - if (isFocused && hasValue && error) { - return 'typingError'; - } - if (error) { - return 'error'; - } - if (isFocused && hasValue) { - return 'typing'; - } - if (isFocused && !hasValue) { - return 'clicked'; - } - if (!isFocused && hasValue) { - return 'completed'; - } - return 'default'; -} - -export default function SignupTextField({ - id, - type, - value, - onChange, - placeholder, - error: externalError, - onBlur, - onFocus, -}: SignupTextFieldProps) { - const { - state, - dispatch, - inputRef, - handleKeyDown, - handleClearClick, - handleWrapperClick, - handleWrapperKeyDown, - } = useSignupTextFieldState({ onChange }); - - const isLocked = type === 'email'; - const error = - externalError || - (type === 'email' ? undefined : validateField(type as 'name' | 'birth' | 'job', value)); - const hasValue = Boolean(value); - const fieldState = getFieldState(state.isFocused, hasValue, !!error, isLocked); - - const wrapperProps = isLocked - ? { tabIndex: -1 as const } - : { - role: 'button' as const, - tabIndex: 0 as const, - onClick: handleWrapperClick, - onKeyDown: handleWrapperKeyDown, - onMouseEnter: () => dispatch({ type: 'HOVER_ENTER' }), - onMouseLeave: () => dispatch({ type: 'HOVER_LEAVE' }), - }; - - function createInputProps() { - return { - id, - ref: inputRef, - type: 'text' as const, - value, - onChange: (e: React.ChangeEvent) => { - if (!isLocked) { - if (type === 'birth') { - onChange(formatBirthDate(e.target.value)); - } else { - onChange(e.target.value); - } - } - }, - onFocus: () => { - dispatch({ type: 'FOCUS' }); - onFocus?.(); - }, - onBlur: () => { - dispatch({ type: 'BLUR' }); - onBlur?.(); - }, - onKeyDown: handleKeyDown, - onCompositionStart: () => dispatch({ type: 'COMPOSE_START' }), - onCompositionEnd: (e: React.CompositionEvent) => { - dispatch({ type: 'COMPOSE_END' }); - if (type === 'name' || type === 'job') { - const filtered = e.currentTarget.value.replace(/[^a-zA-Z가-힣ㄱ-ㅎㅏ-ㅣ\s]/g, ''); - onChange(filtered); - } - }, - placeholder: placeholder ?? (type === 'job' ? '정보를 입력해주세요' : placeholder), - disabled: isLocked, - className: [styles.inputBase, styles.inputStyle].join(' '), - }; - } - - const inputProps = createInputProps(); - - return ( -
-
- -
- {error &&
{error}
} -
- ); -} diff --git a/src/common/component/SignupTextField/SignupTextField.types.ts b/src/common/component/SignupTextField/SignupTextField.types.ts deleted file mode 100644 index 2de39a19..00000000 --- a/src/common/component/SignupTextField/SignupTextField.types.ts +++ /dev/null @@ -1,14 +0,0 @@ -export type SignupTextFieldType = 'name' | 'email' | 'birth' | 'job'; - -export interface SignupTextFieldProps { - id?: string; - type: SignupTextFieldType; - value: string; - onChange: (value: string) => void; - placeholder?: string; - error?: string; - disabled?: boolean; - onBlur?: () => void; - onFocus?: () => void; - maxLength?: number; -} diff --git a/src/common/component/SignupTextField/constants.ts b/src/common/component/SignupTextField/constants.ts deleted file mode 100644 index affdb88d..00000000 --- a/src/common/component/SignupTextField/constants.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const ERROR_MESSAGES = { - name: '한글/영문 10자 이하로 입력해주세요', - birth: '정확한 생년월일을 입력해주세요', - job: '한글/영문 15자 이하로 입력해주세요', -} as const; diff --git a/src/common/component/SignupTextField/index 2.ts b/src/common/component/SignupTextField/index 2.ts deleted file mode 100644 index 2d495fe6..00000000 --- a/src/common/component/SignupTextField/index 2.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './SignupTextField'; -export * from './SignupTextField.types'; diff --git a/src/common/component/SignupTextField/index.ts b/src/common/component/SignupTextField/index.ts deleted file mode 100644 index 2d495fe6..00000000 --- a/src/common/component/SignupTextField/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './SignupTextField'; -export * from './SignupTextField.types'; diff --git a/src/common/component/SignupTextField/useSignupTextFieldState.ts b/src/common/component/SignupTextField/useSignupTextFieldState.ts deleted file mode 100644 index 85aeafb2..00000000 --- a/src/common/component/SignupTextField/useSignupTextFieldState.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { useReducer, useRef, useCallback } from 'react'; - -interface State { - isFocused: boolean; - isHovered: boolean; - isComposing: boolean; -} -type Action = - | { type: 'FOCUS' } - | { type: 'BLUR' } - | { type: 'HOVER_ENTER' } - | { type: 'HOVER_LEAVE' } - | { type: 'COMPOSE_START' } - | { type: 'COMPOSE_END' }; - -function reducer(state: State, action: Action): State { - switch (action.type) { - case 'FOCUS': { - return { ...state, isFocused: true }; - } - case 'BLUR': { - return { ...state, isFocused: false }; - } - case 'HOVER_ENTER': { - return { ...state, isHovered: true }; - } - case 'HOVER_LEAVE': { - return { ...state, isHovered: false }; - } - case 'COMPOSE_START': { - return { ...state, isComposing: true }; - } - case 'COMPOSE_END': { - return { ...state, isComposing: false }; - } - default: { - return state; - } - } -} - -export function useSignupTextFieldState({ onChange }: { onChange: (value: string) => void }) { - const [state, dispatch] = useReducer(reducer, { - isFocused: false, - isHovered: false, - isComposing: false, - }); - const inputRef = useRef(null); - - const handleKeyDown = useCallback( - (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && !state.isComposing) { - e.preventDefault(); - e.stopPropagation(); - e.currentTarget.blur(); - } - }, - [state.isComposing], - ); - - const handleClearClick = useCallback( - (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - onChange(''); - setTimeout(() => inputRef.current?.focus(), 0); - }, - [onChange], - ); - - const handleWrapperClick = useCallback(() => { - if (inputRef.current) { - inputRef.current.focus(); - const len = inputRef.current.value.length ?? 0; - inputRef.current.setSelectionRange(len, len); - } - }, []); - - const handleWrapperKeyDown = useCallback( - (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - e.preventDefault(); - handleWrapperClick(); - } - }, - [handleWrapperClick], - ); - - return { - state, - dispatch, - inputRef, - handleKeyDown, - handleClearClick, - handleWrapperClick, - handleWrapperKeyDown, - }; -} diff --git a/src/common/component/SignupTextField/validation.ts b/src/common/component/SignupTextField/validation.ts deleted file mode 100644 index 745761fc..00000000 --- a/src/common/component/SignupTextField/validation.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { ERROR_MESSAGES } from './constants'; - -export const NAME_MAX_LENGTH = 10; -export const JOB_MAX_LENGTH = 15; -export const BIRTH_REGEX = /^\d{4}\.\d{2}\.\d{2}$/; -export const NAME_REGEX = /^[a-zA-Z가-힣ㄱ-ㅎㅏ-ㅣ\s]*$/; -export const JOB_REGEX = /^[a-zA-Z가-힣ㄱ-ㅎㅏ-ㅣ\s]*$/; - -export function validateField(type: 'name' | 'birth' | 'job', value: string): string | undefined { - if (type === 'name') { - if (!NAME_REGEX.test(value) || value.length > NAME_MAX_LENGTH) { - return ERROR_MESSAGES.name; - } - return undefined; - } - if (type === 'birth') { - if (!value) { - return undefined; - } - if (!BIRTH_REGEX.test(value)) { - return ERROR_MESSAGES.birth; - } - const [year, month, day] = value.split('.'); - const monthNum = parseInt(month, 10); - if (monthNum < 1 || monthNum > 12) { - return ERROR_MESSAGES.birth; - } - const dayNum = parseInt(day, 10); - if (dayNum < 1 || dayNum > 31) { - return ERROR_MESSAGES.birth; - } - const now = new Date(); - const utc = now.getTime() + now.getTimezoneOffset() * 60000; - const koreaNow = new Date(utc + 9 * 60 * 60 * 1000); - const todayStr = koreaNow.toISOString().slice(0, 10); - const [todayYear, todayMonth, todayDay] = todayStr.split('-'); - const today = new Date(`${todayYear}-${todayMonth}-${todayDay}`); - const inputDate = new Date(`${year}-${month}-${day}`); - if (inputDate.getTime() > today.getTime()) { - return ERROR_MESSAGES.birth; - } - return undefined; - } - if (type === 'job') { - if (!JOB_REGEX.test(value) || value.length > JOB_MAX_LENGTH) { - return ERROR_MESSAGES.job; - } - return undefined; - } - return undefined; -} From aca9498d1d9c544fb66e0315098c7623b51fb71a Mon Sep 17 00:00:00 2001 From: Jisu Shin Date: Tue, 16 Sep 2025 20:56:51 +0900 Subject: [PATCH 16/30] =?UTF-8?q?fix:=20MandalartTextField=EC=97=90=20onCo?= =?UTF-8?q?mmit=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/TextField/mandalart/MandalartTextField.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/common/component/TextField/mandalart/MandalartTextField.tsx b/src/common/component/TextField/mandalart/MandalartTextField.tsx index 0e2cce11..474be5ad 100644 --- a/src/common/component/TextField/mandalart/MandalartTextField.tsx +++ b/src/common/component/TextField/mandalart/MandalartTextField.tsx @@ -34,6 +34,7 @@ export interface MandalartTextFieldProps { variant?: MandalartVariant; value: string; onChange: (value: string) => void; + onCommit?: (value: string, reason: 'enter' | 'blur') => void; placeholder?: string; maxLength?: number; disabled?: boolean; @@ -43,6 +44,7 @@ const MandalartTextField = ({ variant = 'bigGoal', value, onChange, + onCommit, placeholder, maxLength, disabled, @@ -65,6 +67,7 @@ const MandalartTextField = ({ From 47338bf7ad4f3575a53810ec4cb5e6eb56f4e427 Mon Sep 17 00:00:00 2001 From: Jisu Shin Date: Tue, 16 Sep 2025 21:05:45 +0900 Subject: [PATCH 17/30] =?UTF-8?q?fix:=20MandalartTextField=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=B2=98=EB=A5=BC=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81=20=EB=B2=84=EC=A0=84=EC=9C=BC=EB=A1=9C=20=EA=B5=90?= =?UTF-8?q?=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/page/todo/entireTodo/Todo.tsx | 13 +++++----- .../todo/entireTodo/constant/constants.ts | 1 - .../todo/lowerTodo/component/TodoFields.tsx | 26 +++++++------------ .../upperTodo/component/SubGoalFields.tsx | 21 ++++++++------- 4 files changed, 26 insertions(+), 35 deletions(-) diff --git a/src/page/todo/entireTodo/Todo.tsx b/src/page/todo/entireTodo/Todo.tsx index 3e766c7e..f2f7500e 100644 --- a/src/page/todo/entireTodo/Todo.tsx +++ b/src/page/todo/entireTodo/Todo.tsx @@ -1,14 +1,14 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { FULL_TEXT, TYPING_DURATION, PLACEHOLDER_TEXT } from './constant/constants'; +import { FULL_TEXT, TYPING_DURATION } from './constant/constants'; import * as styles from './Todo.css'; import { useCreateOverallTodo } from '@/api/domain/entireTodo/hook/useCreateMandalart'; import useTypingEffect from '@/common/hook/useTypingEffect'; import GoButton from '@/common/component/GoButton/GoButton'; import GradientBackground from '@/common/component/Background/GradientBackground'; -import TextField from '@/common/component/MandalartTextField/MandalartTextField'; +import { MandalartTextField } from '@/common/component/TextField/mandalart'; import { PATH } from '@/route'; const Todo = () => { @@ -18,8 +18,8 @@ const Todo = () => { const { mutate } = useCreateOverallTodo(); - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { + const handleFieldCommit = (_value: string, reason: 'enter' | 'blur') => { + if (reason === 'enter') { handleGoNext(); } }; @@ -53,12 +53,11 @@ const Todo = () => {

{renderTextWithLineBreaks()}

- 0} onClick={handleGoNext} />
diff --git a/src/page/todo/entireTodo/constant/constants.ts b/src/page/todo/entireTodo/constant/constants.ts index ec50867d..75b86937 100644 --- a/src/page/todo/entireTodo/constant/constants.ts +++ b/src/page/todo/entireTodo/constant/constants.ts @@ -1,3 +1,2 @@ export const FULL_TEXT = '66일간 달성할 목표를 입력하고\n만다라트를 시작해보세요!'; export const TYPING_DURATION = 3000; -export const PLACEHOLDER_TEXT = '이루고 싶은 목표를 작성하세요.'; diff --git a/src/page/todo/lowerTodo/component/TodoFields.tsx b/src/page/todo/lowerTodo/component/TodoFields.tsx index 04ac79c7..9f2698ec 100644 --- a/src/page/todo/lowerTodo/component/TodoFields.tsx +++ b/src/page/todo/lowerTodo/component/TodoFields.tsx @@ -2,7 +2,7 @@ import { useRef, forwardRef, useImperativeHandle } from 'react'; import * as styles from '../LowerTodo.css'; -import TextField from '@/common/component/MandalartTextField/MandalartTextField'; +import { MandalartTextField } from '@/common/component/TextField/mandalart'; import CycleDropDown from '@/common/component/CycleDropDown/CycleDropDown'; interface TodoItem { @@ -43,13 +43,6 @@ const TodoFields = forwardRef(function TodoFields( isComposing: isComposingArr.current, })); - const handleCompositionStart = (index: number) => { - isComposingArr.current[index] = true; - }; - const handleCompositionEnd = (index: number) => { - isComposingArr.current[index] = false; - }; - const handleSave = (todo: TodoItem, index: number) => { if (isComposingArr.current[index]) { return; @@ -84,6 +77,12 @@ const TodoFields = forwardRef(function TodoFields( onChange(newValues); }; + const getHandleFieldCommit = (index: number) => (_value: string, reason: 'enter' | 'blur') => { + if (reason === 'enter') { + handleSave(values[index], index); + } + }; + return (
{values.map((item, index) => ( @@ -94,20 +93,13 @@ const TodoFields = forwardRef(function TodoFields( onChange={(label) => handleCycleChange(index, label as CycleLabel)} />
- handleTitleChange(index, newValue)} - placeholder="할 일을 입력해주세요" + onCommit={getHandleFieldCommit(index)} disabled={disabled} maxLength={30} - onKeyDown={(e) => { - if (e.key === 'Enter' && !isComposingArr.current[index]) { - handleSave(values[index], index); - } - }} - onCompositionStart={() => handleCompositionStart(index)} - onCompositionEnd={() => handleCompositionEnd(index)} /> ))} diff --git a/src/page/todo/upperTodo/component/SubGoalFields.tsx b/src/page/todo/upperTodo/component/SubGoalFields.tsx index c5e0fea9..28c37b34 100644 --- a/src/page/todo/upperTodo/component/SubGoalFields.tsx +++ b/src/page/todo/upperTodo/component/SubGoalFields.tsx @@ -1,7 +1,7 @@ import * as styles from '../UpperTodo.css'; -import { DEFAULT_PLACEHOLDER } from '@/common/component/MandalartTextField/constant/constants'; -import TextField from '@/common/component/MandalartTextField/MandalartTextField'; +import { DEFAULT_PLACEHOLDER } from '@/common/component/TextField/mandalart/constants'; +import { MandalartTextField } from '@/common/component/TextField/mandalart'; const ORDER_LABELS = [ '첫번째', @@ -37,6 +37,13 @@ const SubGoalFields = ({ onChange(newValues); }; + const getHandleFieldCommit = + (index: number, id?: number) => (value: string, reason: 'enter' | 'blur') => { + if (reason === 'enter' && onEnter) { + onEnter(index, value, id); + } + }; + const appliedValues = [...values]; if (aiResponseData) { aiResponseData.forEach(({ position, title }) => { @@ -47,19 +54,13 @@ const SubGoalFields = ({ return (
{appliedValues.map((value, index) => ( - handleChange(index, val)} + onCommit={getHandleFieldCommit(index, idPositions?.[index]?.id)} placeholder={`${ORDER_LABELS[index]} ${DEFAULT_PLACEHOLDER.subGoal}`} - data-id={idPositions?.[index]?.id?.toString()} - onKeyDown={(e) => { - if (e.key === 'Enter' && onEnter) { - e.preventDefault(); - onEnter(index, e.currentTarget.value, idPositions?.[index]?.id); - } - }} /> ))}
From 7666d3745ff62ce5b2bea17f692efaf815425278 Mon Sep 17 00:00:00 2001 From: Jisu Shin Date: Tue, 16 Sep 2025 21:16:41 +0900 Subject: [PATCH 18/30] =?UTF-8?q?fix:=20SignupTextField=EC=97=90=20id=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/component/TextField/signup/SignupTextField.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/common/component/TextField/signup/SignupTextField.tsx b/src/common/component/TextField/signup/SignupTextField.tsx index 41b1fab0..15c0085e 100644 --- a/src/common/component/TextField/signup/SignupTextField.tsx +++ b/src/common/component/TextField/signup/SignupTextField.tsx @@ -34,6 +34,7 @@ const computeFieldState = (args: { }; export interface SignupTextFieldProps { + id?: string; variant: SignupVariant; value: string; onChange: (value: string) => void; @@ -42,6 +43,7 @@ export interface SignupTextFieldProps { } const SignupTextField = ({ + id, variant, value, onChange, @@ -67,7 +69,7 @@ const SignupTextField = ({ ); return ( - + {({ inputProps, hasValue, isFocused, clear }) => { const state = computeFieldState({ hasValue, isFocused, isHovered, locked: !!disabled }); const wrapperClass = s.fieldBox[state]; From 57763ac970624d825c3120293bac59023f643b09 Mon Sep 17 00:00:00 2001 From: Jisu Shin Date: Tue, 16 Sep 2025 21:24:04 +0900 Subject: [PATCH 19/30] =?UTF-8?q?fix:=20SignupTextField=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=B2=98=EB=A5=BC=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81=20=EB=B2=84=EC=A0=84=EC=9C=BC=EB=A1=9C=20=EA=B5=90?= =?UTF-8?q?=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BasicInfoSection/BasicInfoSection.tsx | 20 +++++++++---------- .../component/JobDropDown/JobDropDown.tsx | 9 ++------- src/page/signup/hook/useSignUpForm.ts | 2 +- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/src/page/signup/BasicInfoSection/BasicInfoSection.tsx b/src/page/signup/BasicInfoSection/BasicInfoSection.tsx index 338f6b22..cd3b6a39 100644 --- a/src/page/signup/BasicInfoSection/BasicInfoSection.tsx +++ b/src/page/signup/BasicInfoSection/BasicInfoSection.tsx @@ -1,4 +1,4 @@ -import SignupTextField from '@/common/component/SignupTextField'; +import { SignupTextField } from '@/common/component/TextField/signup'; import JobDropDown from '@/page/signup/component/JobDropDown/JobDropDown'; import * as styles from '@/page/signup/BasicInfoSection/BasicInfoSection.css'; import LabeledField from '@/page/signup/component/LabelField/LabelField'; @@ -34,7 +34,7 @@ const BasicInfoSection = ({ - + - + diff --git a/src/page/signup/component/JobDropDown/JobDropDown.tsx b/src/page/signup/component/JobDropDown/JobDropDown.tsx index 363250b5..3d0fb2c3 100644 --- a/src/page/signup/component/JobDropDown/JobDropDown.tsx +++ b/src/page/signup/component/JobDropDown/JobDropDown.tsx @@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from 'react'; import { IcDropdown } from '@/assets/svg'; import * as styles from '@/page/signup/component/JobDropDown/JobDropDown.css'; import JobList from '@/page/signup/component/JobDropDown/JobList'; -import SignupTextField from '@/common/component/SignupTextField'; +import { SignupTextField } from '@/common/component/TextField/signup'; import type { JobItem } from '@/page/signup/component/JobDropDown/type/JobItem'; import type { JobValue } from '@/page/signup/component/JobDropDown/type/JobValue'; import { useGetJobList } from '@/api/domain/signup/hook/useGetJobList'; @@ -72,12 +72,7 @@ const JobDropDown = ({ {!isPlaceHolder && selectedJob.id === jobList[jobList.length - 1].id && (
- +
)} diff --git a/src/page/signup/hook/useSignUpForm.ts b/src/page/signup/hook/useSignUpForm.ts index 8c0b1071..bd0d9d51 100644 --- a/src/page/signup/hook/useSignUpForm.ts +++ b/src/page/signup/hook/useSignUpForm.ts @@ -1,6 +1,6 @@ import { useState } from 'react'; -import { validateField } from '@/common/component/SignupTextField/validation'; +import { validateSignupField as validateField } from '@/common/component/TextField/signup/validation'; import type { JobValue } from '@/page/signup/component/JobDropDown/type/JobValue'; const PLACE_HOLDER = '직업을 선택하세요'; From c555aa1097484ad89beee405974dd2cfebcd7fe4 Mon Sep 17 00:00:00 2001 From: Jisu Shin Date: Tue, 16 Sep 2025 21:35:07 +0900 Subject: [PATCH 20/30] =?UTF-8?q?fix:=20HoverContent.tsx=EC=99=80=20?= =?UTF-8?q?=EB=8F=99=EC=9D=BC=ED=95=98=EA=B2=8C=20ModifyTextfield=20?= =?UTF-8?q?=EC=83=81=EC=88=98=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/component/TextField/modify/constants.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/component/TextField/modify/constants.ts b/src/common/component/TextField/modify/constants.ts index 802fcb67..51cfbcf0 100644 --- a/src/common/component/TextField/modify/constants.ts +++ b/src/common/component/TextField/modify/constants.ts @@ -1,6 +1,6 @@ export type ModifyVariant = 'subGoal' | 'todo'; export const DEFAULT_PLACEHOLDER = { - subGoal: '세부 목표를 입력해주세요', - todo: '할 일을 입력해주세요', + subGoal: '수정할 목표를 입력해주세요.', + todo: '목표를 입력해주세요.', } as const; From 6d745a10d0e28244c32a6af2b450dd2d08a983cf Mon Sep 17 00:00:00 2001 From: Jisu Shin Date: Tue, 16 Sep 2025 21:36:10 +0900 Subject: [PATCH 21/30] =?UTF-8?q?fix:=20ModifyTextField=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=B2=98=EB=A5=BC=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81=20=EB=B2=84=EC=A0=84=EC=9C=BC=EB=A1=9C=20=EA=B5=90?= =?UTF-8?q?=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../edit/component/HoverContent/HoverContent.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/page/edit/component/HoverContent/HoverContent.tsx b/src/page/edit/component/HoverContent/HoverContent.tsx index 46259c54..93541068 100644 --- a/src/page/edit/component/HoverContent/HoverContent.tsx +++ b/src/page/edit/component/HoverContent/HoverContent.tsx @@ -3,7 +3,8 @@ import { useState, useEffect } from 'react'; import * as styles from './HoverContent.css'; import Mandalart from '@/common/component/Mandalart/Mandalart'; -import ModifyTextField from '@/common/component/ModifyTextField'; +import { ModifyTextField } from '@/common/component/TextField/modify'; +import { DEFAULT_PLACEHOLDER as MODIFY_PLACEHOLDER } from '@/common/component/TextField/modify/constants'; import CycleDropDown from '@/common/component/CycleDropDown/CycleDropDown'; import { useUpdateSubGoal } from '@/api/domain/edit/hook'; import type { SubGoal, CoreGoal } from '@/page/mandal/types/mandal'; @@ -117,12 +118,7 @@ const HoverContent = ({
e.stopPropagation()}>
e.preventDefault()}> - +
    {subGoals.map((subGoal, index) => (
  • @@ -134,7 +130,7 @@ const HoverContent = ({ variant="todo" value={subGoal.title} onChange={(value) => handleTodoChange(index, value)} - placeholder={`${index + 1}번째 목표를 입력해주세요.`} + placeholder={`${index + 1}번째 ${MODIFY_PLACEHOLDER.todo}`} />
  • ))} From 26e8d229b69e1b24b47d0cb8848c031a93f0ebfe Mon Sep 17 00:00:00 2001 From: Jisu Shin Date: Fri, 19 Sep 2025 03:45:03 +0900 Subject: [PATCH 22/30] =?UTF-8?q?fix:=20=EC=88=98=EB=8F=99=20=EC=A0=88?= =?UTF-8?q?=EB=8B=A8=20=EC=A0=9C=EA=B1=B0=ED=95=98=EA=B3=A0=20input=20maxL?= =?UTF-8?q?ength=EB=A1=9C=20=EA=B8=B8=EC=9D=B4=20=EC=A0=9C=ED=95=9C=20?= =?UTF-8?q?=EB=8B=A8=EC=88=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/TextField/BaseTextField.tsx | 24 ++++--------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/src/common/component/TextField/BaseTextField.tsx b/src/common/component/TextField/BaseTextField.tsx index 8305ca46..7fa76a6a 100644 --- a/src/common/component/TextField/BaseTextField.tsx +++ b/src/common/component/TextField/BaseTextField.tsx @@ -71,16 +71,9 @@ const BaseTextField = ({ return; } const raw = e.target.value; - if (typeof maxLength === 'number' && raw.length > maxLength) { - if (isComposing) { - return; - } - onChange(raw.slice(0, maxLength)); - return; - } onChange(raw); }, - [isLocked, isComposing, maxLength, onChange], + [isLocked, onChange], ); const handleKeyDown = useCallback( @@ -116,18 +109,9 @@ const BaseTextField = ({ setIsComposing(true); }, []); - const handleCompositionEnd = useCallback( - (e: React.CompositionEvent) => { - setIsComposing(false); - if (typeof maxLength === 'number') { - const current = e.currentTarget.value ?? ''; - if (current.length > maxLength) { - onChange(current.slice(0, maxLength)); - } - } - }, - [maxLength, onChange], - ); + const handleCompositionEnd = useCallback((e: React.CompositionEvent) => { + setIsComposing(false); + }, []); const inputProps = useMemo(() => { const base: React.ComponentPropsWithRef<'input'> = { From ddcb7c98e36fc4ebb3089d7bd1fc6bf49dd176a6 Mon Sep 17 00:00:00 2001 From: Jisu Shin Date: Fri, 19 Sep 2025 03:46:09 +0900 Subject: [PATCH 23/30] =?UTF-8?q?fix:=20Mandalart=20createVariants?= =?UTF-8?q?=EC=97=90=EC=84=9C=20base=20=EB=B3=80=EC=88=98=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mandalart/MandalartTextField.css.ts | 35 ++++++++----------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/src/common/component/TextField/mandalart/MandalartTextField.css.ts b/src/common/component/TextField/mandalart/MandalartTextField.css.ts index 5825f1f7..737f2eb7 100644 --- a/src/common/component/TextField/mandalart/MandalartTextField.css.ts +++ b/src/common/component/TextField/mandalart/MandalartTextField.css.ts @@ -74,38 +74,31 @@ type VariantOptions = { const createVariants = (box: object, opts: VariantOptions) => { const { borderWidth, activeBorderColor } = opts; - const baseDefault = { - ...box, - border: `${borderWidth} solid transparent`, - background: colors.grey4, - }; - const baseActive = { - ...box, - border: `${borderWidth} solid ${activeBorderColor}`, - background: colors.grey3, - }; - const baseHoverOrFilled = { - ...box, - border: `${borderWidth} solid transparent`, - background: colors.grey4, - }; - return { default: { - ...baseDefault, + ...box, + border: `${borderWidth} solid transparent`, + background: colors.grey4, }, clicked: { - ...baseActive, + ...box, + border: `${borderWidth} solid ${activeBorderColor}`, + background: colors.grey3, }, typing: { - ...baseActive, + ...box, + border: `${borderWidth} solid ${activeBorderColor}`, + background: colors.grey3, justifyContent: 'space-between', }, filled: { - ...baseHoverOrFilled, + ...box, + border: `${borderWidth} solid transparent`, + background: colors.grey4, }, hover: { - ...baseHoverOrFilled, + ...box, + border: `${borderWidth} solid transparent`, background: colors.grey3, }, }; From e5f74c5697b8d246665f1816f26564d629d789ac Mon Sep 17 00:00:00 2001 From: Jisu Shin Date: Fri, 19 Sep 2025 03:52:21 +0900 Subject: [PATCH 24/30] =?UTF-8?q?fix:=20BaseTextField=EC=9D=98=20locked?= =?UTF-8?q?=EB=A5=BC=20disabled=EB=A1=9C=20=ED=86=B5=EC=9D=BC=ED=95=98?= =?UTF-8?q?=EA=B3=A0=20=EC=82=AC=EC=9A=A9=EC=B2=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/component/TextField/BaseTextField.tsx | 16 ++++++++-------- .../TextField/mandalart/MandalartTextField.tsx | 2 +- .../TextField/modify/ModifyTextField.tsx | 2 +- .../TextField/signup/SignupTextField.css.ts | 4 ++-- .../TextField/signup/SignupTextField.stories.tsx | 2 +- .../TextField/signup/SignupTextField.tsx | 16 ++++++++-------- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/common/component/TextField/BaseTextField.tsx b/src/common/component/TextField/BaseTextField.tsx index 7fa76a6a..4c8e0dac 100644 --- a/src/common/component/TextField/BaseTextField.tsx +++ b/src/common/component/TextField/BaseTextField.tsx @@ -10,7 +10,7 @@ export interface BaseTextFieldProps { onCommit?: (value: string, reason: CommitReason) => void; onClear?: () => void; maxLength?: number; - locked?: boolean; + disabled?: boolean; invalid?: boolean; children: (args: { inputProps: React.ComponentPropsWithRef<'input'>; @@ -31,7 +31,7 @@ const BaseTextField = ({ onCommit, onClear, maxLength, - locked, + disabled, invalid, children, }: BaseTextFieldProps) => { @@ -40,7 +40,7 @@ const BaseTextField = ({ const lastCommittedRef = useRef(null); const skipBlurOnceRef = useRef(false); - const isLocked = !!locked; + const isDisabled = !!disabled; const hasValue = Boolean(value); const length = value.length; @@ -67,13 +67,13 @@ const BaseTextField = ({ const handleChange = useCallback( (e: React.ChangeEvent) => { - if (isLocked) { + if (isDisabled) { return; } const raw = e.target.value; onChange(raw); }, - [isLocked, onChange], + [isDisabled, onChange], ); const handleKeyDown = useCallback( @@ -124,15 +124,15 @@ const BaseTextField = ({ onCompositionStart: handleCompositionStart, onCompositionEnd: handleCompositionEnd, maxLength: typeof maxLength === 'number' ? maxLength : undefined, - readOnly: isLocked, - 'aria-readonly': isLocked, + readOnly: isDisabled, + 'aria-readonly': isDisabled, 'aria-invalid': invalid || undefined, }; return base; }, [ id, value, - isLocked, + isDisabled, invalid, handleChange, handleKeyDown, diff --git a/src/common/component/TextField/mandalart/MandalartTextField.tsx b/src/common/component/TextField/mandalart/MandalartTextField.tsx index 474be5ad..405541fc 100644 --- a/src/common/component/TextField/mandalart/MandalartTextField.tsx +++ b/src/common/component/TextField/mandalart/MandalartTextField.tsx @@ -69,7 +69,7 @@ const MandalartTextField = ({ onChange={onChange} onCommit={onCommit} maxLength={effectiveMaxLength} - locked={disabled} + disabled={disabled} > {({ inputProps, hasValue, isFocused, clear }) => { const state = computeFieldState({ hasValue, isFocused, isHovered }); diff --git a/src/common/component/TextField/modify/ModifyTextField.tsx b/src/common/component/TextField/modify/ModifyTextField.tsx index a14f5e46..0e2e8ef8 100644 --- a/src/common/component/TextField/modify/ModifyTextField.tsx +++ b/src/common/component/TextField/modify/ModifyTextField.tsx @@ -58,7 +58,7 @@ const ModifyTextField = ({ const isTodo = variant === 'todo'; return ( - + {({ inputProps, hasValue, isFocused, clear }) => { const state = computeFieldState({ hasValue, isFocused, isHovered }); const wrapperClass = variant === 'subGoal' ? s.subGoalBox[state] : s.todoBox[state]; diff --git a/src/common/component/TextField/signup/SignupTextField.css.ts b/src/common/component/TextField/signup/SignupTextField.css.ts index ae6d1384..8404fe90 100644 --- a/src/common/component/TextField/signup/SignupTextField.css.ts +++ b/src/common/component/TextField/signup/SignupTextField.css.ts @@ -33,7 +33,7 @@ const createInputStateVariants = () => typing: { color: colors.grey10 }, filled: { color: colors.grey10 }, hover: { color: colors.grey6 }, - locked: { color: colors.grey5 }, + disabled: { color: colors.grey5 }, }); export const inputState = createInputStateVariants(); @@ -61,7 +61,7 @@ const createBoxVariants = (activeBorderColor: string) => { typing: { ...baseActive, justifyContent: 'space-between' }, filled: baseHoverOrFilled, hover: { ...baseHoverOrFilled, background: colors.grey3 }, - locked: { ...baseHoverOrFilled, justifyContent: 'space-between', pointerEvents: 'none' }, + disabled: { ...baseHoverOrFilled, justifyContent: 'space-between', pointerEvents: 'none' }, }); }; diff --git a/src/common/component/TextField/signup/SignupTextField.stories.tsx b/src/common/component/TextField/signup/SignupTextField.stories.tsx index e3d0ff12..7e55fe0b 100644 --- a/src/common/component/TextField/signup/SignupTextField.stories.tsx +++ b/src/common/component/TextField/signup/SignupTextField.stories.tsx @@ -23,7 +23,7 @@ export const Name: Story = { }, }; -export const EmailLocked: Story = { +export const EmailDisabled: Story = { args: { variant: 'email', value: '', onChange: () => {} }, render: () => { const [value] = useState('user@example.com'); diff --git a/src/common/component/TextField/signup/SignupTextField.tsx b/src/common/component/TextField/signup/SignupTextField.tsx index 15c0085e..09ccac6f 100644 --- a/src/common/component/TextField/signup/SignupTextField.tsx +++ b/src/common/component/TextField/signup/SignupTextField.tsx @@ -9,7 +9,7 @@ import * as s from './SignupTextField.css'; import { formatBirthDate } from './format'; import { validateSignupField } from './validation'; -type FieldState = 'default' | 'clicked' | 'typing' | 'filled' | 'hover' | 'locked'; +type FieldState = 'default' | 'clicked' | 'typing' | 'filled' | 'hover' | 'disabled'; const pickPlaceholder = (variant: SignupVariant, placeholder?: string) => placeholder ?? DEFAULT_PLACEHOLDER[variant]; @@ -18,11 +18,11 @@ const computeFieldState = (args: { hasValue: boolean; isFocused: boolean; isHovered: boolean; - locked: boolean; + disabled: boolean; }): FieldState => { - const { hasValue, isFocused, isHovered, locked } = args; - if (locked) { - return 'locked'; + const { hasValue, isFocused, isHovered, disabled } = args; + if (disabled) { + return 'disabled'; } if (isFocused) { return hasValue ? 'typing' : 'clicked'; @@ -69,9 +69,9 @@ const SignupTextField = ({ ); return ( - + {({ inputProps, hasValue, isFocused, clear }) => { - const state = computeFieldState({ hasValue, isFocused, isHovered, locked: !!disabled }); + const state = computeFieldState({ hasValue, isFocused, isHovered, disabled: !!disabled }); const wrapperClass = s.fieldBox[state]; const inputClass = s.inputState[state]; @@ -109,7 +109,7 @@ const SignupTextField = ({ )} - {state === 'locked' && ( + {state === 'disabled' && ( From 0db4f3c558e0e7ec5f86dad77a0eedd4a8de5f02 Mon Sep 17 00:00:00 2001 From: Jisu Shin Date: Fri, 19 Sep 2025 03:52:54 +0900 Subject: [PATCH 25/30] =?UTF-8?q?fix:=20=EB=A6=B0=ED=8A=B8=20=EA=B2=BD?= =?UTF-8?q?=EA=B3=A0=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/component/TextField/BaseTextField.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/component/TextField/BaseTextField.tsx b/src/common/component/TextField/BaseTextField.tsx index 4c8e0dac..b5871bcc 100644 --- a/src/common/component/TextField/BaseTextField.tsx +++ b/src/common/component/TextField/BaseTextField.tsx @@ -109,7 +109,7 @@ const BaseTextField = ({ setIsComposing(true); }, []); - const handleCompositionEnd = useCallback((e: React.CompositionEvent) => { + const handleCompositionEnd = useCallback(() => { setIsComposing(false); }, []); From ec2370fae3d0fa95e49e704ef474c1b7617a95f4 Mon Sep 17 00:00:00 2001 From: Jisu Shin Date: Fri, 19 Sep 2025 03:58:11 +0900 Subject: [PATCH 26/30] =?UTF-8?q?fix:=20subGoalBox=EB=A5=BC=20=EB=8B=A8?= =?UTF-8?q?=EC=9D=BC=20=EC=8A=A4=ED=83=80=EC=9D=BC=EB=A1=9C=20=EB=8B=A8?= =?UTF-8?q?=EC=88=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/TextField/modify/ModifyTextField.css.ts | 11 +++++------ .../component/TextField/modify/ModifyTextField.tsx | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/common/component/TextField/modify/ModifyTextField.css.ts b/src/common/component/TextField/modify/ModifyTextField.css.ts index e861a96a..0e91917c 100644 --- a/src/common/component/TextField/modify/ModifyTextField.css.ts +++ b/src/common/component/TextField/modify/ModifyTextField.css.ts @@ -33,12 +33,11 @@ const subGoalBase = { background: colors.grey2, }; -export const subGoalBox = styleVariants({ - default: subGoalBase, - hover: { ...subGoalBase, cursor: 'pointer' }, - clicked: subGoalBase, - typing: subGoalBase, - filled: subGoalBase, +export const subGoalBox = style({ + ...subGoalBase, + ':hover': { + cursor: 'pointer', + }, }); const createVariants = (box: object, opts: { borderWidth: string; activeBorderColor: string }) => { diff --git a/src/common/component/TextField/modify/ModifyTextField.tsx b/src/common/component/TextField/modify/ModifyTextField.tsx index 0e2e8ef8..1343389a 100644 --- a/src/common/component/TextField/modify/ModifyTextField.tsx +++ b/src/common/component/TextField/modify/ModifyTextField.tsx @@ -61,7 +61,7 @@ const ModifyTextField = ({ {({ inputProps, hasValue, isFocused, clear }) => { const state = computeFieldState({ hasValue, isFocused, isHovered }); - const wrapperClass = variant === 'subGoal' ? s.subGoalBox[state] : s.todoBox[state]; + const wrapperClass = variant === 'subGoal' ? s.subGoalBox : s.todoBox[state]; const inputClass = variant === 'subGoal' ? s.subGoalInput[state] : s.todoInput[state]; return ( From c4ea119189e60cbb1f878653fd4bef597dec4916 Mon Sep 17 00:00:00 2001 From: Jisu Shin Date: Fri, 19 Sep 2025 04:08:06 +0900 Subject: [PATCH 27/30] =?UTF-8?q?fix:=20ModifyTextField=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EC=A4=91=EC=B2=A9=20div=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TextField/modify/ModifyTextField.tsx | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/common/component/TextField/modify/ModifyTextField.tsx b/src/common/component/TextField/modify/ModifyTextField.tsx index 1343389a..74c3f718 100644 --- a/src/common/component/TextField/modify/ModifyTextField.tsx +++ b/src/common/component/TextField/modify/ModifyTextField.tsx @@ -65,21 +65,23 @@ const ModifyTextField = ({ const inputClass = variant === 'subGoal' ? s.subGoalInput[state] : s.todoInput[state]; return ( -
    -
    - - {isTodo && state === 'typing' && ( - - )} -
    +
    + + {isTodo && state === 'typing' && ( + + )}
    ); }} From 3f4192d3391ccff0db7db5598a0ebebf9b965038 Mon Sep 17 00:00:00 2001 From: Jisu Shin Date: Fri, 19 Sep 2025 04:09:08 +0900 Subject: [PATCH 28/30] =?UTF-8?q?fix:=20ModifyTextField=EC=97=90=EC=84=9C?= =?UTF-8?q?=20onChange=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/TextField/modify/ModifyTextField.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/common/component/TextField/modify/ModifyTextField.tsx b/src/common/component/TextField/modify/ModifyTextField.tsx index 74c3f718..7f7c26f0 100644 --- a/src/common/component/TextField/modify/ModifyTextField.tsx +++ b/src/common/component/TextField/modify/ModifyTextField.tsx @@ -48,17 +48,10 @@ const ModifyTextField = ({ const handleMouseEnter = useCallback(() => setIsHovered(true), []); const handleMouseLeave = useCallback(() => setIsHovered(false), []); - const handleChange = useCallback( - (nextValue: string) => { - onChange(nextValue); - }, - [onChange], - ); - const isTodo = variant === 'todo'; return ( - + {({ inputProps, hasValue, isFocused, clear }) => { const state = computeFieldState({ hasValue, isFocused, isHovered }); const wrapperClass = variant === 'subGoal' ? s.subGoalBox : s.todoBox[state]; From 6e29cb5227ebb8e85776c2a227821890ef263f3c Mon Sep 17 00:00:00 2001 From: Jisu Shin Date: Fri, 19 Sep 2025 04:10:05 +0900 Subject: [PATCH 29/30] =?UTF-8?q?fix:=20SignupTextField=20createBoxVariant?= =?UTF-8?q?s=EC=97=90=EC=84=9C=20base=20=EB=B3=80=EC=88=98=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TextField/signup/SignupTextField.css.ts | 55 +++++++++++-------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/src/common/component/TextField/signup/SignupTextField.css.ts b/src/common/component/TextField/signup/SignupTextField.css.ts index 8404fe90..6c14ec07 100644 --- a/src/common/component/TextField/signup/SignupTextField.css.ts +++ b/src/common/component/TextField/signup/SignupTextField.css.ts @@ -39,29 +39,40 @@ const createInputStateVariants = () => export const inputState = createInputStateVariants(); const createBoxVariants = (activeBorderColor: string) => { - const baseDefault = { - ...box, - border: `2px solid transparent`, - background: colors.grey4, - } as const; - const baseActive = { - ...box, - border: `2px solid ${activeBorderColor}`, - background: colors.grey3, - } as const; - const baseHoverOrFilled = { - ...box, - border: `2px solid transparent`, - background: colors.grey4, - } as const; - return styleVariants({ - default: baseDefault, - clicked: baseActive, - typing: { ...baseActive, justifyContent: 'space-between' }, - filled: baseHoverOrFilled, - hover: { ...baseHoverOrFilled, background: colors.grey3 }, - disabled: { ...baseHoverOrFilled, justifyContent: 'space-between', pointerEvents: 'none' }, + default: { + ...box, + border: `2px solid transparent`, + background: colors.grey4, + }, + clicked: { + ...box, + border: `2px solid ${activeBorderColor}`, + background: colors.grey3, + }, + typing: { + ...box, + border: `2px solid ${activeBorderColor}`, + background: colors.grey3, + justifyContent: 'space-between', + }, + filled: { + ...box, + border: `2px solid transparent`, + background: colors.grey4, + }, + hover: { + ...box, + border: `2px solid transparent`, + background: colors.grey3, + }, + disabled: { + ...box, + border: `2px solid transparent`, + background: colors.grey4, + justifyContent: 'space-between', + pointerEvents: 'none', + }, }); }; From e34ce533329d35036c55827ee3e180062f680042 Mon Sep 17 00:00:00 2001 From: Jisu Shin Date: Fri, 19 Sep 2025 04:12:02 +0900 Subject: [PATCH 30/30] =?UTF-8?q?fix:=20SignupTextField=EC=9D=98=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=EC=9D=84=20=EB=A0=8C?= =?UTF-8?q?=EB=8D=94=20=EC=99=B8=EB=B6=80=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TextField/signup/SignupTextField.tsx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/common/component/TextField/signup/SignupTextField.tsx b/src/common/component/TextField/signup/SignupTextField.tsx index 09ccac6f..5c274a0f 100644 --- a/src/common/component/TextField/signup/SignupTextField.tsx +++ b/src/common/component/TextField/signup/SignupTextField.tsx @@ -68,6 +68,17 @@ const SignupTextField = ({ [onChange, variant], ); + const signupType = variant === 'email' ? undefined : (variant as 'name' | 'birth' | 'job'); + let computedInvalid: boolean | undefined; + let computedError: string | undefined; + if (signupType) { + const msg = validateSignupField(signupType, value); + if (msg) { + computedInvalid = true; + computedError = msg; + } + } + return ( {({ inputProps, hasValue, isFocused, clear }) => { @@ -75,17 +86,6 @@ const SignupTextField = ({ const wrapperClass = s.fieldBox[state]; const inputClass = s.inputState[state]; - let computedInvalid: boolean | undefined; - let computedError: string | undefined; - const signupType = variant === 'email' ? undefined : (variant as 'name' | 'birth' | 'job'); - if (signupType) { - const msg = validateSignupField(signupType, value); - if (msg) { - computedInvalid = true; - computedError = msg; - } - } - return (