-
Notifications
You must be signed in to change notification settings - Fork 3
인풋 공톰 컴포넌트 구현 #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
인풋 공톰 컴포넌트 구현 #13
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import Input from './Input'; | ||
| import { AccountInputProps } from './types/types'; | ||
| import styles from './styles/AccountInput.module.css'; | ||
|
|
||
| /** | ||
| * 프로필 페이지용 계정 정보 표시 컴포넌트. | ||
| * 이메일과 비밀번호 모두 읽기 전용으로 표시한다. | ||
| * @param email 등록된 이메일 주소 | ||
| * @param children 하단 영역에 렌더링할 요소 (수정하기 버튼 등) | ||
| */ | ||
| export default function AccountInput({ email, children }: AccountInputProps) { | ||
| return ( | ||
| <div className={styles.container}> | ||
| <div className={styles.field}> | ||
| <label className={styles.label}>이메일</label> | ||
| <Input type="email" value={email} disabled className={styles.readOnly} /> | ||
| </div> | ||
| <div className={styles.field}> | ||
| <label className={styles.label}>비밀번호</label> | ||
| <Input type="password" value="••••••••" disabled className={styles.readOnly} /> | ||
| </div> | ||
| {children && <div className={styles.buttonArea}>{children}</div>} | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| 'use client'; | ||
|
|
||
| import { useCallback, useRef, useState } from 'react'; | ||
| import Image from 'next/image'; | ||
| import clsx from 'clsx'; | ||
| import TextArea from './TextArea'; | ||
| import { ActionTextAreaProps } from './types/types'; | ||
| import arrowActive from '@/assets/buttons/arrow/arrowUpActivedButton.svg'; | ||
| import arrowInactive from '@/assets/buttons/arrow/arrowUpNonActivedButton.svg'; | ||
| import styles from './styles/ActionTextArea.module.css'; | ||
|
|
||
| /** | ||
| * 전송 버튼이 포함된 텍스트 입력 기본 컴포넌트. | ||
| * 입력값이 있으면 전송 버튼이 활성화된다. | ||
| * @param onSubmit 전송 버튼 클릭 시 호출되는 콜백 | ||
| * @param wrapperClassName wrapper div에 적용할 추가 CSS 클래스 | ||
| * @param className TextArea에 적용할 추가 CSS 클래스 | ||
| * @param props 네이티브 textarea의 모든 속성 | ||
| */ | ||
| export default function ActionTextArea({ | ||
| onSubmit, | ||
| wrapperClassName, | ||
| className, | ||
| onChange, | ||
| ...props | ||
| }: ActionTextAreaProps) { | ||
| const [hasValue, setHasValue] = useState(false); | ||
| const textareaRef = useRef<HTMLTextAreaElement>(null); | ||
|
|
||
| const autoResize = useCallback(() => { | ||
| const el = textareaRef.current; | ||
| if (!el) return; | ||
| el.style.height = 'auto'; | ||
| el.style.height = `${el.scrollHeight}px`; | ||
| }, []); | ||
|
|
||
| return ( | ||
| <div className={clsx(styles.wrapper, wrapperClassName)}> | ||
| <TextArea | ||
| ref={textareaRef} | ||
| rows={1} | ||
| className={clsx(styles.textarea, className)} | ||
| onChange={(e) => { | ||
| setHasValue(e.target.value.length > 0); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| autoResize(); | ||
| onChange?.(e); | ||
| }} | ||
| {...props} | ||
| /> | ||
| <button | ||
| type="button" | ||
| className={styles.action} | ||
| onClick={onSubmit} | ||
| disabled={!hasValue} | ||
| aria-label="전송" | ||
| > | ||
| <Image src={hasValue ? arrowActive : arrowInactive} alt="" width={24} height={24} /> | ||
| </button> | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| import Input from './Input'; | ||
| import { ChangePasswordProps } from './types/types'; | ||
| import styles from './styles/ChangePassword.module.css'; | ||
|
|
||
| /** | ||
| * 비밀번호 변경 컴포넌트. | ||
| * @param isEditing 편집 모드 여부 (false면 인풋 비활성화, 기본값 false) | ||
| * @param newPasswordProps 새 비밀번호 인풋에 전달할 props | ||
| * @param confirmPasswordProps 새 비밀번호 확인 인풋에 전달할 props | ||
| * @param children 버튼 등 하단 영역에 렌더링할 요소 | ||
| */ | ||
| export default function ChangePassword({ | ||
| isEditing = false, | ||
| newPasswordProps, | ||
| confirmPasswordProps, | ||
| children, | ||
| }: ChangePasswordProps) { | ||
| return ( | ||
| <div className={styles.container}> | ||
| <div className={styles.field}> | ||
| <label className={styles.label}>새 비밀번호</label> | ||
| <Input | ||
| type="password" | ||
| placeholder="새 비밀번호를 입력해 주세요." | ||
| disabled={!isEditing} | ||
| {...newPasswordProps} | ||
| /> | ||
| </div> | ||
| <div className={styles.field}> | ||
| <label className={styles.label}>새 비밀번호 확인</label> | ||
| <Input | ||
| type="password" | ||
| placeholder="새 비밀번호를 다시 한 번 입력해 주세요." | ||
| disabled={!isEditing} | ||
| {...confirmPasswordProps} | ||
| /> | ||
| </div> | ||
| <div className={styles.buttonArea}>{children}</div> | ||
| </div> | ||
| ); | ||
| } | ||
|
Comment on lines
+1
to
+41
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import clsx from 'clsx'; | ||
| import ActionTextArea from './ActionTextArea'; | ||
| import { CommentInputProps } from './types/types'; | ||
| import styles from './styles/CommentInput.module.css'; | ||
|
|
||
| /** | ||
| * 댓글 입력 컴포넌트. | ||
| * ActionTextArea를 위아래 보더 스타일로 감싼다. | ||
| * @param className TextArea에 적용할 추가 CSS 클래스 | ||
| * @param props ActionTextArea의 모든 속성 | ||
| */ | ||
| export default function CommentInput({ className, ...props }: CommentInputProps) { | ||
| return ( | ||
| <ActionTextArea | ||
| wrapperClassName={styles.wrapper} | ||
| className={clsx(styles.textarea, className)} | ||
| {...props} | ||
| /> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,12 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import clsx from 'clsx'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { InputProps } from './types/types'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import styles from './styles/Input.module.css'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 공통 Input 컴포넌트. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param className 추가 CSS 클래스 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param props 네이티브 input의 모든 속성(placeholder, type, onChange 등) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export default function Input({ className, ...props }: InputProps) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return <input className={clsx(styles.input, className)} {...props} />; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1
to
+12
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 함수 컴포넌트에서
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| 'use client'; | ||
|
|
||
| import { useState } from 'react'; | ||
| import Image from 'next/image'; | ||
| import clsx from 'clsx'; | ||
| import Input from './Input'; | ||
| import { PasswordInputProps } from './types/types'; | ||
| import visibilityTrue from '@/assets/icons/visibility/visibillityTrue.svg'; | ||
| import visibilityFalse from '@/assets/icons/visibility/visibillityFalse.svg'; | ||
| import styles from './styles/PasswordInput.module.css'; | ||
|
|
||
| /** | ||
| * 비밀번호 Input 컴포넌트. | ||
| * @param className 추가 CSS 클래스 | ||
| * @param props 네이티브 input의 모든 속성(type 제외) | ||
| */ | ||
| export default function PasswordInput({ className, ...props }: PasswordInputProps) { | ||
| const [showPassword, setShowPassword] = useState(false); | ||
|
|
||
| return ( | ||
| <div className={styles.wrapper}> | ||
| <Input | ||
| type={showPassword ? 'text' : 'password'} | ||
| className={clsx(styles.input, className)} | ||
| {...props} | ||
| /> | ||
| <button | ||
| type="button" | ||
| className={styles.toggleButton} | ||
| onClick={() => setShowPassword((prev) => !prev)} | ||
| aria-label={showPassword ? '비밀번호 숨기기' : '비밀번호 보기'} | ||
| > | ||
| <Image | ||
| src={showPassword ? visibilityTrue : visibilityFalse} | ||
| alt="" | ||
| width={24} | ||
| height={24} | ||
| /> | ||
| </button> | ||
| </div> | ||
| ); | ||
| } | ||
|
Comment on lines
+1
to
+42
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 부모 컴포넌트로부터 |
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,12 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import clsx from 'clsx'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { TextAreaProps } from './types/types'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import styles from './styles/TextArea.module.css'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 멀티라인 입력 컴포넌트. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param className 추가 CSS 클래스 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param props 네이티브 textarea의 모든 속성(placeholder, rows, onChange 등) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export default function TextArea({ className, ref, ...props }: TextAreaProps) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return <textarea ref={ref} className={clsx(styles.textarea, className)} {...props} />; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1
to
+12
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 함수 컴포넌트에서
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| export { default as Input } from './Input'; | ||
| export { default as PasswordInput } from './PasswordInput'; | ||
| export { default as AccountInput } from './AccountInput'; | ||
| export { default as ChangePassword } from './ChangePassword'; | ||
| export { default as TextArea } from './TextArea'; | ||
| export { default as ActionTextArea } from './ActionTextArea'; | ||
| export { default as CommentInput } from './CommentInput'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| .container { | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: 24px; | ||
| width: 100%; | ||
| } | ||
|
|
||
| .field { | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: 8px; | ||
| } | ||
|
|
||
| .label { | ||
| font-family: var(--font-pretendard), 'Pretendard', sans-serif; | ||
| font-weight: 500; | ||
| font-size: 14px; | ||
| line-height: 17px; | ||
| color: #0f172a; | ||
| } | ||
|
|
||
| .readOnly { | ||
| background: #f1f5f9; | ||
| color: #94a3b8; | ||
| } | ||
|
|
||
| .buttonArea { | ||
| display: flex; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| .wrapper { | ||
| position: relative; | ||
| width: 100%; | ||
| } | ||
|
|
||
| .textarea { | ||
| min-height: auto; | ||
| padding-right: 56px; | ||
| overflow-y: hidden; | ||
| } | ||
|
|
||
| .action { | ||
| position: absolute; | ||
| right: 16px; | ||
| bottom: 12px; | ||
| display: flex; | ||
| align-items: center; | ||
| background: none; | ||
| border: none; | ||
| padding: 0; | ||
| cursor: pointer; | ||
| } | ||
|
|
||
| .action:disabled { | ||
| cursor: default; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| .container { | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: 24px; | ||
| width: 100%; | ||
| } | ||
|
|
||
| .field { | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: 8px; | ||
| } | ||
|
|
||
| .label { | ||
| font-family: var(--font-pretendard), 'Pretendard', sans-serif; | ||
| font-weight: 500; | ||
| font-size: 14px; | ||
| line-height: 17px; | ||
| color: #0f172a; | ||
| } | ||
|
|
||
| .buttonArea { | ||
| display: flex; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| .wrapper { | ||
| border-top: 1px solid #e2e8f0; | ||
| border-bottom: 1px solid #e2e8f0; | ||
| } | ||
|
|
||
| .textarea { | ||
| border: none; | ||
| border-radius: 0; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| .input { | ||
| width: 100%; | ||
| height: 48px; | ||
| border-radius: 12px; | ||
| border: 1px solid #e2e8f0; | ||
| padding: 16px; | ||
| background: #ffffff; | ||
| font-family: var(--font-pretendard), 'Pretendard', sans-serif; | ||
| font-weight: 400; | ||
| font-size: 16px; | ||
| line-height: 19px; | ||
| color: #0f172a; | ||
| outline: none; | ||
| } | ||
|
|
||
| .input::placeholder { | ||
| color: #64748b; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| .wrapper { | ||
| position: relative; | ||
| width: 100%; | ||
| } | ||
|
|
||
| .input { | ||
| padding-right: 48px; | ||
| } | ||
|
|
||
| .toggleButton { | ||
| position: absolute; | ||
| right: 16px; | ||
| top: 50%; | ||
| transform: translateY(-50%); | ||
| background: none; | ||
| border: none; | ||
| padding: 0; | ||
| cursor: pointer; | ||
| display: flex; | ||
| align-items: center; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| .textarea { | ||
| width: 100%; | ||
| min-height: 75px; | ||
| border-radius: 12px; | ||
| border: 1px solid #e2e8f0; | ||
| padding: 12px 16px; | ||
| background: #ffffff; | ||
| font-family: var(--font-pretendard), 'Pretendard', sans-serif; | ||
| font-weight: 400; | ||
| font-size: 16px; | ||
| line-height: 19px; | ||
| color: #0f172a; | ||
| outline: none; | ||
| resize: none; | ||
| overflow-y: auto; | ||
| } | ||
|
|
||
| .textarea::placeholder { | ||
| color: #64748b; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
두 가지 개선점을 제안합니다:
<label>의htmlFor속성과<input>의id속성을 연결하여 접근성을 향상시켜야 합니다.emailprop이undefined일 경우, React에서 제어되지 않는(uncontrolled) input이 제어되는(controlled) input으로 변경된다는 경고가 발생할 수 있습니다.value={email ?? ''}와 같이 기본값을 제공하여 이를 방지할 수 있습니다.