diff --git a/pages/index.tsx b/pages/index.tsx index b390ee55..0271c9a6 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,5 +1,17 @@ -const HomePage = () => { - return <>Hello World>; +import type { NextPage } from 'next'; + +import WriteInput from '@/src/components/Input/WriteInput'; +import { useInput } from '@/src/hooks/useInput'; + +const HomePage: NextPage = () => { + const testInputProps = useInput({ + id: 'test', + defaultValue: '', + }); + + return ( + + ); }; export default HomePage; diff --git a/plopfile.mjs b/plopfile.mjs index d2e3eb12..3f197c12 100644 --- a/plopfile.mjs +++ b/plopfile.mjs @@ -1,4 +1,3 @@ -/* eslint-disable import/no-anonymous-default-export */ export default function (plop) { const getComponentName = { type: 'input', diff --git a/src/assets/icons/submit.svg b/src/assets/icons/submit.svg new file mode 100644 index 00000000..039b1dea --- /dev/null +++ b/src/assets/icons/submit.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/Input/WriteInput/index.tsx b/src/components/Input/WriteInput/index.tsx new file mode 100644 index 00000000..20c40fef --- /dev/null +++ b/src/components/Input/WriteInput/index.tsx @@ -0,0 +1,115 @@ +import type { ChangeEvent, HTMLAttributes, KeyboardEvent } from 'react'; +import { useMemo, useRef, useState } from 'react'; +import { assignInlineVars } from '@vanilla-extract/dynamic'; + +import { MAIN_INPUT_MAX_LENGTH } from '@/src/constants/config'; +import type { UseInputReturn } from '@/src/hooks/useInput'; +import { COLORS } from '@/src/styles/tokens'; + +import Icon from '../../SvgIcon'; +import * as style from './style.css'; + +interface WriteInputProps extends HTMLAttributes { + inputProps: UseInputReturn; + placeholder?: string; + maxLength?: number; +} + +const WriteInput = ({ + inputProps, + placeholder, + maxLength = MAIN_INPUT_MAX_LENGTH, +}: WriteInputProps) => { + const { id, value } = inputProps; + const inputRef = useRef(null); + const [textareaHeight, setTextareaHeight] = useState({ + row: 1, + lineBreak: {}, + }); + + const handleResize = (e: ChangeEvent) => { + const { scrollHeight, clientHeight, value } = e.target; + + if (value.length === 0) { + setTextareaHeight((prev) => ({ + row: 1, + lineBreak: { ...prev.lineBreak, [e.target.value.length]: false }, + })); + } + + if (scrollHeight > clientHeight) { + setTextareaHeight((prev) => ({ + row: prev.row + 1, + lineBreak: { ...prev.lineBreak, [value.length - 1]: true }, + })); + } + + if (textareaHeight.lineBreak[value.length]) { + setTextareaHeight((prev) => ({ + row: prev.row - 1, + lineBreak: { ...prev.lineBreak, [value.length]: false }, + })); + } + }; + + const handleKeydownEnter = (e: KeyboardEvent) => { + if (e.code === 'Enter') { + setTextareaHeight((prev) => ({ + row: prev.row + 1, + lineBreak: { ...prev.lineBreak, [value.length]: true }, + })); + } + }; + + const isValid = useMemo(() => value.length > 0, [value.length]); + + return ( + + + + + + + 1, + })} + > + + {value.length > 0 && ( + + {value.length} + / 500자 + + )} + + + + + + + + ); +}; + +export default WriteInput; diff --git a/src/components/Input/WriteInput/style.css.ts b/src/components/Input/WriteInput/style.css.ts new file mode 100644 index 00000000..1f2fe392 --- /dev/null +++ b/src/components/Input/WriteInput/style.css.ts @@ -0,0 +1,90 @@ +import { createVar, style } from '@vanilla-extract/css'; +import { recipe } from '@vanilla-extract/recipes'; + +import { COLORS } from '@/src/styles/tokens'; + +export const conatiner = style({ + display: 'flex', + justifyContent: 'space-between', + borderRadius: '16px', + width: '100%', + padding: '22px 12px 22px 24px', + border: `2px solid ${COLORS['Blue/Gradient']}`, +}); + +export const inputHeight = createVar(); +export const contentWrapper = style({ + display: 'flex', + alignItems: 'center', + width: '100%', + height: inputHeight, + minHeight: '27px', + maxHeight: '260px', +}); + +export const label = style({ + width: '100%', +}); + +export const input = style({ + padding: '0', + width: '100%', + maxHeight: '216px', + resize: 'none', + color: COLORS['Grey/900'], + fontSize: '17px', + lineHeight: '27px', + overflowWrap: 'break-word', + '::placeholder': { + color: COLORS['Grey/250'], + }, +}); + +export const submitWrapper = recipe({ + base: { + display: 'flex', + alignItems: 'flex-end', + paddingLeft: '20px', + }, + variants: { + multirow: { + true: { + height: '100%', + }, + }, + }, +}); + +export const submit = style({ + display: 'flex', + alignItems: 'center', + gap: '16px', + height: '48px', +}); + +export const textCount = style({ + color: COLORS['Grey/400'], + fontSize: '14px', + fontWeight: '400', + whiteSpace: 'nowrap', +}); + +export const currentTextCount = style({ + color: COLORS['Blue/Default'], + fontSize: '14px', + fontWeight: '700', +}); + +export const alert = style({ + display: 'flex', + alignItems: 'center', + marginTop: '12px', +}); + +export const alertMsg = style({ + marginLeft: '6px', + color: COLORS['Grey/600'], + fontSize: '13px', + fontWeight: '600', + lineHeight: '17px', +}); diff --git a/src/components/SvgIcon.tsx b/src/components/SvgIcon.tsx index 72283873..a8b23852 100644 --- a/src/components/SvgIcon.tsx +++ b/src/components/SvgIcon.tsx @@ -1,23 +1,16 @@ -import { Icon as icon } from '../constants/icon'; +import { iconFactory, type Icons } from '../constants/icon'; interface IconProps { - icon: keyof typeof icon; - fill?: string; - stroke?: string; + icon: Icons; + color?: string; width?: number; height?: number; } -const Icon = ({ - icon: iconKey, - fill, - stroke, - width = 24, - height = 24, -}: IconProps) => { - const SvgIcon = icon[iconKey]; +const Icon = ({ icon, color, width = 24, height = 24 }: IconProps) => { + const SvgIcon = iconFactory[icon]; - return ; + return ; }; export default Icon; diff --git a/src/constants/config.ts b/src/constants/config.ts new file mode 100644 index 00000000..1307be25 --- /dev/null +++ b/src/constants/config.ts @@ -0,0 +1,4 @@ +/** + * "끄적끄적" 최대 입력 글자 수 + */ +export const MAIN_INPUT_MAX_LENGTH = 500; diff --git a/src/constants/icon.ts b/src/constants/icon.ts index 50685d54..1d81236f 100644 --- a/src/constants/icon.ts +++ b/src/constants/icon.ts @@ -1,11 +1,9 @@ -import type { FC, SVGProps } from 'react'; - import Profle from '@/src/assets/icons/profile.svg'; +import Submit from '@/src/assets/icons/submit.svg'; -export type IconFactory = { - [key: string]: FC>; -}; - -export const Icon: IconFactory = { +export const iconFactory = { profile: Profle, + submit: Submit, }; + +export type Icons = keyof typeof iconFactory; diff --git a/src/hooks/useInput.ts b/src/hooks/useInput.ts new file mode 100644 index 00000000..4fe895de --- /dev/null +++ b/src/hooks/useInput.ts @@ -0,0 +1,19 @@ +import type { ChangeEvent } from 'react'; +import { useState } from 'react'; + +interface UseInputArgs { + id: string; + defaultValue?: string; +} + +export const useInput = ({ id, defaultValue = '' }: UseInputArgs) => { + const [value, setValue] = useState(defaultValue); + + const onChange = (e: ChangeEvent) => { + setValue(e.currentTarget.value); + }; + + return { id, value, onChange }; +}; + +export type UseInputReturn = ReturnType; diff --git a/tsconfig.json b/tsconfig.json index 19fff78a..91e21029 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "strict": true, + "strict": false, "target": "es5", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true,