From f080eb113d76f06eb5df38dc6b869c631ee62869 Mon Sep 17 00:00:00 2001 From: hwinkr Date: Sat, 19 Oct 2024 11:38:29 +0900 Subject: [PATCH 01/21] =?UTF-8?q?chore:=20ios=20=EA=B8=B0=EA=B8=B0?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=B2=84=ED=8A=BC=20=ED=85=8D=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=83=89=EC=83=81=EC=9D=B4=20=ED=8C=8C=EB=9E=80?= =?UTF-8?q?=EC=83=89=EC=9C=BC=EB=A1=9C=20=EB=B3=B4=EC=98=80=EB=8D=98=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/styles/global.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontend/src/styles/global.ts b/frontend/src/styles/global.ts index 25ade3c1..1bf81472 100644 --- a/frontend/src/styles/global.ts +++ b/frontend/src/styles/global.ts @@ -162,8 +162,13 @@ const globalStyles = css` button { cursor: pointer; + padding: 0; + font: inherit; + color: inherit; + + appearance: none; /* IOS 디바이스의 버튼 색상이 파란색으로 보이는 문제 해결(@해리) */ border: none; } `; From ae229162f9ce30855f6dd78201473032c4d105a0 Mon Sep 17 00:00:00 2001 From: hwinkr Date: Sat, 19 Oct 2024 11:38:57 +0900 Subject: [PATCH 02/21] =?UTF-8?q?chore:=20BottomFixedButton=20=EB=86=92?= =?UTF-8?q?=EC=9D=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Buttons/BottomFixedButton/BottomFixedButton.styles.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/_common/Buttons/BottomFixedButton/BottomFixedButton.styles.ts b/frontend/src/components/_common/Buttons/BottomFixedButton/BottomFixedButton.styles.ts index 319ff735..664282be 100644 --- a/frontend/src/components/_common/Buttons/BottomFixedButton/BottomFixedButton.styles.ts +++ b/frontend/src/components/_common/Buttons/BottomFixedButton/BottomFixedButton.styles.ts @@ -6,8 +6,10 @@ export const s_bottomFixedStyles = css` /* 버튼 컴포넌트의 full variants를 사용하려고 했으나 6rem보다 height값이 작아 직접 높이를 정의했어요(@해리) full 버튼에 이미 의존하고 있는 컴포넌트들이 많아서 높이를 full 스타일을 변경할 수는 없었습니다. + + 버튼의 높이가 너무 높다는 피드백을 반영하기 위해서 높이 수정 5.2rem(@해리) */ - height: 6rem; + height: 5.2rem; box-shadow: 0 -4px 4px rgb(0 0 0 / 25%); `; From 0a1d4e657c87c37e26fb6c1fae76f4012cc8f17e Mon Sep 17 00:00:00 2001 From: hwinkr Date: Sat, 19 Oct 2024 11:41:23 +0900 Subject: [PATCH 03/21] =?UTF-8?q?feat:=20floating=20=EC=8A=A4=ED=83=80?= =?UTF-8?q?=EC=9D=BC,=20=ED=83=80=EC=9E=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/_common/Input/Input.styles.ts | 15 ++++++++++++--- frontend/src/components/_common/Input/index.tsx | 5 +++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/_common/Input/Input.styles.ts b/frontend/src/components/_common/Input/Input.styles.ts index 884d2ffd..efcc50a0 100644 --- a/frontend/src/components/_common/Input/Input.styles.ts +++ b/frontend/src/components/_common/Input/Input.styles.ts @@ -4,9 +4,8 @@ import theme from '@styles/theme'; const baseInputStyle = css` width: 100%; - height: 4.4rem; - padding: 0.8rem; - outline-color: ${theme.colors.primary}; + height: 4.8rem; + outline: none; ${theme.typography.bodyMedium} `; @@ -19,4 +18,14 @@ export const s_input = { border: none; outline: none; `, + floating: css` + padding-top: 1.6rem; /* 텍스트를 아래로 내리기 위해 top padding을 더 줌 (@해리) */ + padding-left: 1.2rem; + + color: #71717a; + + border: none; + ${baseInputStyle} + border-radius: 1.2rem; + `, }; diff --git a/frontend/src/components/_common/Input/index.tsx b/frontend/src/components/_common/Input/index.tsx index d39e7941..865437d3 100644 --- a/frontend/src/components/_common/Input/index.tsx +++ b/frontend/src/components/_common/Input/index.tsx @@ -2,12 +2,13 @@ import type { InputHTMLAttributes } from 'react'; import { s_input } from './Input.styles'; -export type InputVariant = 'default' | 'transparent'; +export type InputVariant = 'default' | 'transparent' | 'floating'; export interface InputProps extends InputHTMLAttributes { variant?: InputVariant; + isError?: boolean; } export default function Input({ variant = 'default', type = 'text', ...props }: InputProps) { - return ; + return ; } From 87084c9220f28282521c668e82245aedc38ef023 Mon Sep 17 00:00:00 2001 From: hwinkr Date: Sat, 19 Oct 2024 11:43:51 +0900 Subject: [PATCH 04/21] =?UTF-8?q?feat:=20FloatingInput=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - label이 인풋 태그 내부에 위치하도록 relative, absolute 활용 - focus 상태를 관리 --- .../FloatingInput/FloatingInput.stories.tsx | 24 ++++++++++ .../FloatingInput/FloatingInput.styles.ts | 47 +++++++++++++++++++ .../src/components/FloatingInput/index.tsx | 43 +++++++++++++++++ 3 files changed, 114 insertions(+) create mode 100644 frontend/src/components/FloatingInput/FloatingInput.stories.tsx create mode 100644 frontend/src/components/FloatingInput/FloatingInput.styles.ts create mode 100644 frontend/src/components/FloatingInput/index.tsx diff --git a/frontend/src/components/FloatingInput/FloatingInput.stories.tsx b/frontend/src/components/FloatingInput/FloatingInput.stories.tsx new file mode 100644 index 00000000..3697a372 --- /dev/null +++ b/frontend/src/components/FloatingInput/FloatingInput.stories.tsx @@ -0,0 +1,24 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import FloatingInput from '.'; + +const meta = { + title: 'Components/Inputs/FloatingInput', + component: FloatingInput, + argTypes: { + label: { control: 'text' }, + isError: { control: 'boolean' }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + label: '낙타해리빙봉', + placeholder: '송재석최현웅김윤경', + isError: false, + }, +}; diff --git a/frontend/src/components/FloatingInput/FloatingInput.styles.ts b/frontend/src/components/FloatingInput/FloatingInput.styles.ts new file mode 100644 index 00000000..177ac99f --- /dev/null +++ b/frontend/src/components/FloatingInput/FloatingInput.styles.ts @@ -0,0 +1,47 @@ +import { css } from '@emotion/react'; + +import theme from '@styles/theme'; + +export const s_floatingLabelContainer = css` + position: relative; + display: inline-block; + width: 100%; +`; + +export const s_floatingLabelInput = (isError: boolean) => css` + appearance: none; + box-shadow: ${isError ? `0 0 0 0.1rem #EB1E1E` : `0 0 0 0.1rem #71717a`}; + transition: box-shadow 0.3s; + + &::placeholder { + color: #71717a; + } + + &:focus { + box-shadow: ${isError + ? `0 0 0 0.1rem #EB1E1E` + : `0 0 0 0.1rem ${theme.colors.pink.mediumLight}`}; + } +`; + +const getLabelTextColor = (isFocused: boolean, isError: boolean): string => { + if (isError) return '#EB1E1E'; + + if (isFocused) return theme.colors.pink.medium; + + return '#71717a'; +}; + +export const s_floatingLabel = (isFocused: boolean, isError: boolean) => css` + position: absolute; + top: 0.4rem; + left: 1em; + + ${theme.typography.captionMedium}; + + color: ${getLabelTextColor(isFocused, isError)}; + + background: transparent; + + transition: color 0.3s; +`; diff --git a/frontend/src/components/FloatingInput/index.tsx b/frontend/src/components/FloatingInput/index.tsx new file mode 100644 index 00000000..c80a0051 --- /dev/null +++ b/frontend/src/components/FloatingInput/index.tsx @@ -0,0 +1,43 @@ +import { useState } from 'react'; + +import type { InputProps } from '@components/_common/Input'; +import Input from '@components/_common/Input'; + +import { + s_floatingLabel, + s_floatingLabelContainer, + s_floatingLabelInput, +} from './FloatingInput.styles'; + +interface FloatingInputProps extends InputProps { + label: string; + isError: boolean; +} +export default function FloatingInput({ + label, + placeholder, + isError, + ...props +}: FloatingInputProps) { + const [isFocused, setIsFocused] = useState(false); + + const focus = () => setIsFocused(true); + const blur = () => setIsFocused(false); + + return ( +
+ + +
+ ); +} From 94ee0cbf6bdd53563daf86534f02fea632fd084a Mon Sep 17 00:00:00 2001 From: hwinkr Date: Sat, 19 Oct 2024 11:45:30 +0900 Subject: [PATCH 05/21] =?UTF-8?q?feat:=20=EC=96=B4=EB=96=A4=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=20Field=20=EC=9D=B8=EC=A7=80=20=EB=82=98=ED=83=80?= =?UTF-8?q?=EB=82=B4=EB=8A=94=20Title=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - label과는 다른 역할을 한다는 생각을 바탕으로, FieldLabel과 마크업과 스타일이 동일하지만 추가로 하나의 컴포넌트를 추가로 생성 --- .../src/components/_common/Field/Field.styles.ts | 1 + frontend/src/components/_common/Field/index.tsx | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/frontend/src/components/_common/Field/Field.styles.ts b/frontend/src/components/_common/Field/Field.styles.ts index 389b8687..b2abed5a 100644 --- a/frontend/src/components/_common/Field/Field.styles.ts +++ b/frontend/src/components/_common/Field/Field.styles.ts @@ -7,6 +7,7 @@ export const s_field = css` display: flex; flex-direction: column; gap: 0.8rem; + margin-bottom: 1.2rem; `; export const s_label = css` diff --git a/frontend/src/components/_common/Field/index.tsx b/frontend/src/components/_common/Field/index.tsx index 74fc3a0f..5dea6d49 100644 --- a/frontend/src/components/_common/Field/index.tsx +++ b/frontend/src/components/_common/Field/index.tsx @@ -1,5 +1,8 @@ import type { ReactNode } from 'react'; +import FloatingInput from '@components/FloatingInput'; + +import Text from '../Text'; import { s_description, s_errorMessage, @@ -16,6 +19,14 @@ const Field = ({ children }: FieldProps) => { return
{children}
; }; +interface FieldTitleProps { + title: string; +} + +const FieldTitle = ({ title }: FieldTitleProps) => { + return {title}; +}; + interface FieldLabelProps { id: string; labelText: string; @@ -29,6 +40,7 @@ const FieldLabel = ({ id, labelText }: FieldLabelProps) => ( interface FieldDescriptionProps { description?: string; + accentText?: string; } const FieldDescription = ({ description }: FieldDescriptionProps) => @@ -44,8 +56,10 @@ const FieldErrorMessage = ({ errorMessage }: FieldErrorMessageProps) => ( ); +Field.Title = FieldTitle; Field.Label = FieldLabel; Field.Description = FieldDescription; Field.ErrorMessage = FieldErrorMessage; +Field.FloatingInput = FloatingInput; export default Field; From f6dd55cfaec9f4665ed78aa470f3dea19a260323 Mon Sep 17 00:00:00 2001 From: hwinkr Date: Sat, 19 Oct 2024 11:48:06 +0900 Subject: [PATCH 06/21] =?UTF-8?q?refactor:=20FloatingInput=EC=9D=84=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=B4=EC=84=9C=20=EC=95=BD=EC=86=8D?= =?UTF-8?q?=EC=9D=84=20=EC=83=9D=EC=84=B1=ED=95=98=EB=8A=94=20=EA=B2=83?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/constants/button.ts | 5 ++ frontend/src/constants/inputFields.ts | 29 ++++++++++- .../components/MeetingDateTime/index.tsx | 11 ++-- .../components/MeetingHostInfo/index.tsx | 51 +++++++++++-------- .../components/MeetingName/index.tsx | 21 ++++---- 5 files changed, 82 insertions(+), 35 deletions(-) create mode 100644 frontend/src/constants/button.ts diff --git a/frontend/src/constants/button.ts b/frontend/src/constants/button.ts new file mode 100644 index 00000000..9fcd9f91 --- /dev/null +++ b/frontend/src/constants/button.ts @@ -0,0 +1,5 @@ +export const MEETING_BUTTON_TEXTS = { + create: '약속 생성하기', + next: '다음', + register: '등록하러 가기', +}; diff --git a/frontend/src/constants/inputFields.ts b/frontend/src/constants/inputFields.ts index 63c56361..8e1c37bb 100644 --- a/frontend/src/constants/inputFields.ts +++ b/frontend/src/constants/inputFields.ts @@ -9,8 +9,35 @@ export const INPUT_RULES = { }; export const FIELD_DESCRIPTIONS = { + meetingName: '약속 이름은 1~10자 사이로 입력해 주세요.', + nickname: '1~5자 사이로 입력해 주세요.', + password: '4자리 숫자로 입력해 주세요.', + date: '날짜를 하나씩 클릭해 여러 날짜를 선택하거나\n시작일과 종료일을 클릭해 사이의 모든 날짜를 선택해 보세요', +}; + +export const FIELD_TITLES = { + meetingName: '약속 정보 입력', + meetingHostInfo: '약속 주최자 정보 입력', + meetingDateTime: '약속 후보 날짜 선택', + meetingTimeRange: '약속 시간 범위 선택', + attendeeLogin: '내 정보 입력', +}; + +export const FIELD_LABELS = { + meetingName: '약속 이름', + nickname: '닉네임', + password: '비밀번호', + onlyDate: '날짜만 선택할래요', +}; + +export const FIELD_PLACEHOLDERS = { + meetingName: '10자 이내의 약속 이름 입력', + nickname: '5자 이내의 약속 닉네임 입력', + password: '4자리 숫자 입력', +}; + +export const FIELD_ERROR_MESSAGES = { meetingName: '약속 이름은 1~10자 사이로 입력해 주세요.', nickname: '닉네임은 1~5자 사이로 입력해 주세요.', password: '비밀번호는 4자리 숫자로 입력해 주세요.', - date: '날짜를 하나씩 클릭해 여러 날짜를 선택하거나\n시작일과 종료일을 클릭해 사이의 모든 날짜를 선택해 보세요', }; diff --git a/frontend/src/pages/CreateMeetingPage/components/MeetingDateTime/index.tsx b/frontend/src/pages/CreateMeetingPage/components/MeetingDateTime/index.tsx index 9983c6a5..8c18c9cf 100644 --- a/frontend/src/pages/CreateMeetingPage/components/MeetingDateTime/index.tsx +++ b/frontend/src/pages/CreateMeetingPage/components/MeetingDateTime/index.tsx @@ -14,7 +14,8 @@ import type useDateSelect from '@hooks/useDateSelect/useDateSelect'; import type useMeetingType from '@hooks/useMeetingType/useMeetingType'; import type { UseTimeRangeDropdownReturn } from '@hooks/useTimeRangeDropdown/useTimeRangeDropdown'; -import { FIELD_DESCRIPTIONS } from '@constants/inputFields'; +import { MEETING_BUTTON_TEXTS } from '@constants/button'; +import { FIELD_DESCRIPTIONS, FIELD_LABELS, FIELD_TITLES } from '@constants/inputFields'; import { s_container, s_dateCandidateSelector } from './MeetingDateTime.styles'; @@ -74,7 +75,7 @@ export default function MeetingDateTime({
- +
{!isChecked && ( - + - 약속 생성하기 + {MEETING_BUTTON_TEXTS.create}
); diff --git a/frontend/src/pages/CreateMeetingPage/components/MeetingHostInfo/index.tsx b/frontend/src/pages/CreateMeetingPage/components/MeetingHostInfo/index.tsx index 77493d6c..e1ba3f52 100644 --- a/frontend/src/pages/CreateMeetingPage/components/MeetingHostInfo/index.tsx +++ b/frontend/src/pages/CreateMeetingPage/components/MeetingHostInfo/index.tsx @@ -1,11 +1,13 @@ +import ScrollBlock from '@components/ScrollBlock'; import BottomFixedButton from '@components/_common/Buttons/BottomFixedButton'; import Field from '@components/_common/Field'; -import Input from '@components/_common/Input'; +import Text from '@components/_common/Text'; import useButtonOnKeyboard from '@hooks/useButtonOnKeyboard/useButtonOnKeyboard'; import type { UseInputReturn } from '@hooks/useInput/useInput'; -import { FIELD_DESCRIPTIONS } from '@constants/inputFields'; +import { MEETING_BUTTON_TEXTS } from '@constants/button'; +import { FIELD_LABELS, FIELD_PLACEHOLDERS, FIELD_TITLES } from '@constants/inputFields'; interface MeetingHostInfoProps { hostNickNameInput: UseInputReturn; @@ -32,36 +34,45 @@ export default function MeetingHostInfo({ } = hostPasswordInput; const resizedButtonHeight = useButtonOnKeyboard(); + const isHostNickNameError = hostNickNameErrorMessage !== null; + const isHostPasswordError = hostPasswordErrorMessage !== null; return ( - <> + - - - - - - - - - - + + 약속을 생성하면 + + 돼요 + + + - + - 다음 + {MEETING_BUTTON_TEXTS.next} - + ); } diff --git a/frontend/src/pages/CreateMeetingPage/components/MeetingName/index.tsx b/frontend/src/pages/CreateMeetingPage/components/MeetingName/index.tsx index 98ec588b..3542d518 100644 --- a/frontend/src/pages/CreateMeetingPage/components/MeetingName/index.tsx +++ b/frontend/src/pages/CreateMeetingPage/components/MeetingName/index.tsx @@ -1,11 +1,12 @@ +import ScrollBlock from '@components/ScrollBlock'; import BottomFixedButton from '@components/_common/Buttons/BottomFixedButton'; import Field from '@components/_common/Field'; -import Input from '@components/_common/Input'; import useButtonOnKeyboard from '@hooks/useButtonOnKeyboard/useButtonOnKeyboard'; import type { UseInputReturn } from '@hooks/useInput/useInput'; -import { FIELD_DESCRIPTIONS } from '@constants/inputFields'; +import { MEETING_BUTTON_TEXTS } from '@constants/button'; +import { FIELD_LABELS, FIELD_PLACEHOLDERS, FIELD_TITLES } from '@constants/inputFields'; interface MeetingNameProps { meetingNameInput: UseInputReturn; @@ -24,18 +25,20 @@ export default function MeetingName({ errorMessage: meetingNameErrorMessage, } = meetingNameInput; + const isTextError = meetingNameErrorMessage !== null; const resizedButtonHeight = useButtonOnKeyboard(); return ( - <> + - - - + @@ -45,8 +48,8 @@ export default function MeetingName({ disabled={isMeetingNameInvalid} height={resizedButtonHeight} > - 다음 + {MEETING_BUTTON_TEXTS.next} - + ); } From d7d0c13d67795dea2df39fe7ede004ed132ce060 Mon Sep 17 00:00:00 2001 From: hwinkr Date: Sat, 19 Oct 2024 11:48:55 +0900 Subject: [PATCH 07/21] =?UTF-8?q?chore:=20DESCRIPTION,=20ERROR=5FMESSAGE?= =?UTF-8?q?=EB=A5=BC=20=EA=B5=AC=EB=B6=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/hooks/useCreateMeeting/useCreateMeeting.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/hooks/useCreateMeeting/useCreateMeeting.ts b/frontend/src/hooks/useCreateMeeting/useCreateMeeting.ts index 7ffdc65f..6a34fb91 100644 --- a/frontend/src/hooks/useCreateMeeting/useCreateMeeting.ts +++ b/frontend/src/hooks/useCreateMeeting/useCreateMeeting.ts @@ -6,7 +6,7 @@ import useTimeRangeDropdown from '@hooks/useTimeRangeDropdown/useTimeRangeDropdo import { usePostMeetingMutation } from '@stores/servers/meeting/mutations'; -import { FIELD_DESCRIPTIONS, INPUT_FIELD_PATTERN, INPUT_RULES } from '@constants/inputFields'; +import { FIELD_ERROR_MESSAGES, INPUT_FIELD_PATTERN, INPUT_RULES } from '@constants/inputFields'; const checkInputInvalid = (value: string, errorMessage: string | null) => value.length < INPUT_RULES.minimumLength || errorMessage !== null; @@ -14,7 +14,7 @@ const checkInputInvalid = (value: string, errorMessage: string | null) => const useCreateMeeting = () => { const meetingNameInput = useInput({ pattern: INPUT_FIELD_PATTERN.meetingName, - errorMessage: FIELD_DESCRIPTIONS.meetingName, + errorMessage: FIELD_ERROR_MESSAGES.meetingName, }); const isMeetingNameInvalid = checkInputInvalid( meetingNameInput.value, @@ -23,11 +23,11 @@ const useCreateMeeting = () => { const hostNickNameInput = useInput({ pattern: INPUT_FIELD_PATTERN.nickname, - errorMessage: FIELD_DESCRIPTIONS.nickname, + errorMessage: FIELD_ERROR_MESSAGES.nickname, }); const hostPasswordInput = useInput({ pattern: INPUT_FIELD_PATTERN.password, - errorMessage: FIELD_DESCRIPTIONS.password, + errorMessage: FIELD_ERROR_MESSAGES.password, }); const isHostInfoInvalid = checkInputInvalid(hostNickNameInput.value, hostNickNameInput.errorMessage) || From 8e8beb76a2009ab671320407ea36b81160b5a345 Mon Sep 17 00:00:00 2001 From: hwinkr Date: Sat, 19 Oct 2024 11:50:06 +0900 Subject: [PATCH 08/21] =?UTF-8?q?feat:=20=EC=8A=A4=ED=81=AC=EB=A1=A4?= =?UTF-8?q?=EC=9D=84=20=EB=A7=89=EB=8A=94=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 모바일 환경에서만 스크롤을 막으면 되기 때문에, 터치 이벤트로 인한 스크롤만 막도록 구현 --- frontend/src/components/ScrollBlock/index.tsx | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 frontend/src/components/ScrollBlock/index.tsx diff --git a/frontend/src/components/ScrollBlock/index.tsx b/frontend/src/components/ScrollBlock/index.tsx new file mode 100644 index 00000000..614db67e --- /dev/null +++ b/frontend/src/components/ScrollBlock/index.tsx @@ -0,0 +1,21 @@ +import type { PropsWithChildren } from 'react'; +import { useEffect, useRef } from 'react'; + +export default function ScrollBlock({ children }: PropsWithChildren) { + const contentRef = useRef(null); + + useEffect(() => { + const handleTouchMove = (e: TouchEvent) => { + e.preventDefault(); + }; + + // 터치 이벤트를 사용해서 스크롤을 할 경우, 해당 스크롤을 막는다는 것을 브라우저에게 명시적으로 알려주기 위해서 passive 속성 추가(@해리) + document.addEventListener('touchmove', handleTouchMove, { passive: false }); + + return () => { + document.removeEventListener('touchmove', handleTouchMove); + }; + }, []); + + return
{children}
; +} From e92fa4a7b6a7c9a58998fb647fa8d8dd2bc79055 Mon Sep 17 00:00:00 2001 From: hwinkr Date: Sat, 19 Oct 2024 11:51:48 +0900 Subject: [PATCH 09/21] =?UTF-8?q?refactor:=20=EC=95=BD=EC=86=8D=20?= =?UTF-8?q?=EC=B0=B8=EC=97=AC=EC=9E=90=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=A1=9C=EC=A7=81,=20=EC=8A=A4?= =?UTF-8?q?=ED=83=80=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FloatingInput으로 수정 - useAttendeeLogin 커스텀 훅 활용 - 필드가 페이지 상단에 위치하도록 변경 --- .../useAttendeeLogin/useAttendeeLogin.ts | 63 +++++++++ .../AttendeeLoginPage.styles.ts | 15 +- .../src/pages/AttendeeLoginPage/index.tsx | 129 +++++++----------- 3 files changed, 118 insertions(+), 89 deletions(-) create mode 100644 frontend/src/hooks/useAttendeeLogin/useAttendeeLogin.ts diff --git a/frontend/src/hooks/useAttendeeLogin/useAttendeeLogin.ts b/frontend/src/hooks/useAttendeeLogin/useAttendeeLogin.ts new file mode 100644 index 00000000..28f38883 --- /dev/null +++ b/frontend/src/hooks/useAttendeeLogin/useAttendeeLogin.ts @@ -0,0 +1,63 @@ +import { useContext } from 'react'; + +import { UuidContext } from '@contexts/UuidProvider'; + +import useInput from '@hooks/useInput/useInput'; + +import { usePostLoginMutation } from '@stores/servers/user/mutations'; + +import { FIELD_ERROR_MESSAGES, INPUT_FIELD_PATTERN } from '@constants/inputFields'; + +const useAttendeeLogin = () => { + const { uuid } = useContext(UuidContext); + const { mutate: postLoginMutate } = usePostLoginMutation(); + + const attendeeNameInput = useInput({ + pattern: INPUT_FIELD_PATTERN.nickname, + errorMessage: FIELD_ERROR_MESSAGES.nickname, + }); + const isAttendeeNameError = attendeeNameInput.errorMessage !== null; + const attendeeNameField = { ...attendeeNameInput, isError: isAttendeeNameError }; + + const attendeePasswordInput = useInput({ + pattern: INPUT_FIELD_PATTERN.password, + errorMessage: FIELD_ERROR_MESSAGES.password, + }); + const isAttendeePasswordError = attendeePasswordInput.errorMessage !== null; + const attendeePasswordField = { ...attendeePasswordInput, isError: isAttendeePasswordError }; + + const isFormValid = () => { + const hasLoginFormError = isAttendeeNameError || isAttendeePasswordError; + + if (hasLoginFormError) { + return false; + } + + const requiredFields = [attendeeNameInput.value, attendeePasswordInput.value]; + const isAllFieldsFilled = requiredFields.every((field) => field !== ''); + + return isAllFieldsFilled; + }; + + const handleLoginButtonClick = async () => { + if (!uuid) { + console.error('UUID is missing'); + return; + } + + postLoginMutate({ + uuid, + request: { attendeeName: attendeeNameInput.value, password: attendeePasswordInput.value }, + }); + }; + + return { + attendeeNameField, + attendeePasswordField, + isFormValid, + handleLoginButtonClick, + uuid, + }; +}; + +export default useAttendeeLogin; diff --git a/frontend/src/pages/AttendeeLoginPage/AttendeeLoginPage.styles.ts b/frontend/src/pages/AttendeeLoginPage/AttendeeLoginPage.styles.ts index e86bad38..1c60d902 100644 --- a/frontend/src/pages/AttendeeLoginPage/AttendeeLoginPage.styles.ts +++ b/frontend/src/pages/AttendeeLoginPage/AttendeeLoginPage.styles.ts @@ -3,22 +3,13 @@ import { css } from '@emotion/react'; export const s_container = css` display: flex; flex-direction: column; - gap: 2rem; - align-items: center; - justify-content: center; - - height: calc(100vh - 8.4rem); - padding: 0 2rem; + row-gap: 0.4rem; + height: 100%; `; export const s_inputContainer = css` display: flex; flex-direction: column; - gap: 1rem; - + justify-content: center; width: 100%; - padding: 1.6rem; - - background-color: #f7dacb; - border-radius: 0.5rem; `; diff --git a/frontend/src/pages/AttendeeLoginPage/index.tsx b/frontend/src/pages/AttendeeLoginPage/index.tsx index def8a836..39f5f13d 100644 --- a/frontend/src/pages/AttendeeLoginPage/index.tsx +++ b/frontend/src/pages/AttendeeLoginPage/index.tsx @@ -1,66 +1,37 @@ -import { useContext } from 'react'; - import ContentLayout from '@layouts/ContentLayout'; -import { UuidContext } from '@contexts/UuidProvider'; - +import FloatingLabelInput from '@components/FloatingInput'; +import ScrollBlock from '@components/ScrollBlock'; import BackButton from '@components/_common/Buttons/BackButton'; import { Button } from '@components/_common/Buttons/Button'; import Field from '@components/_common/Field'; import Header from '@components/_common/Header'; -import Input from '@components/_common/Input'; - -import useInput from '@hooks/useInput/useInput'; +import Text from '@components/_common/Text'; -import { usePostLoginMutation } from '@stores/servers/user/mutations'; +import useAttendeeLogin from '@hooks/useAttendeeLogin/useAttendeeLogin'; -import { FIELD_DESCRIPTIONS, INPUT_FIELD_PATTERN } from '@constants/inputFields'; +import { MEETING_BUTTON_TEXTS } from '@constants/button'; +import { FIELD_LABELS, FIELD_PLACEHOLDERS, FIELD_TITLES } from '@constants/inputFields'; import { s_container, s_inputContainer } from './AttendeeLoginPage.styles'; export default function AttendeeLoginPage() { - const { uuid } = useContext(UuidContext); - - const { mutate: postLoginMutate } = usePostLoginMutation(); + const { attendeeNameField, attendeePasswordField, handleLoginButtonClick, isFormValid, uuid } = + useAttendeeLogin(); const { value: attendeeName, onValueChange: handleAttendeeNameChange, errorMessage: attendeeNameErrorMessage, - } = useInput({ - pattern: INPUT_FIELD_PATTERN.nickname, - errorMessage: FIELD_DESCRIPTIONS.nickname, - }); + isError: isAttendeeNameError, + } = attendeeNameField; const { value: attendeePassword, onValueChange: handleAttendeePasswordChange, errorMessage: attendeePasswordErrorMessage, - } = useInput({ - pattern: INPUT_FIELD_PATTERN.password, - errorMessage: FIELD_DESCRIPTIONS.password, - }); - - const isFormValid = () => { - const errorMessages = [attendeeNameErrorMessage, attendeePasswordErrorMessage]; - const hasErrors = errorMessages.some((errorMessage) => errorMessage !== null); - - if (hasErrors) { - return false; - } - - const requiredFields = [attendeeName, attendeePassword]; - const isAllFieldsFilled = requiredFields.every((field) => field !== ''); - - return isAllFieldsFilled; - }; - - const handleLoginButtonClick = async () => { - postLoginMutate({ - uuid, - request: { attendeeName, password: attendeePassword }, - }); - }; + isError: isAttendeePasswordError, + } = attendeePasswordField; return ( <> @@ -69,43 +40,47 @@ export default function AttendeeLoginPage() { -
-
- - - - - - - - - - - - - + +
+
+ + + + 약속에서 사용할 를 입력해 주세요 + + + + + +
+
- -
+ ); From 4f5acaeb50e3c643186b240b193af5e584f47b35 Mon Sep 17 00:00:00 2001 From: hwinkr Date: Sat, 19 Oct 2024 11:52:16 +0900 Subject: [PATCH 10/21] =?UTF-8?q?refactor:=20=EB=8F=84=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=20=EC=A0=95=EC=B1=85=20=EB=B3=80=EA=B2=BD=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B8=ED=95=9C,=201=EB=85=84=20=EB=92=A4=EC=9D=98=20?= =?UTF-8?q?=EC=95=BD=EC=86=8D=EC=9D=84=20=EC=83=9D=EC=84=B1=ED=95=98?= =?UTF-8?q?=EB=A0=A4=EA=B3=A0=20=ED=95=98=EB=8A=94=20=EA=B2=BD=EC=9A=B0=20?= =?UTF-8?q?=ED=86=A0=EC=8A=A4=ED=8A=B8=20=EB=A9=94=EC=8B=9C=EC=A7=80?= =?UTF-8?q?=EB=A1=9C=20=EC=95=8C=EB=A0=A4=EC=A3=BC=EB=8A=94=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/constants/toasts.ts | 3 +++ frontend/src/hooks/useCalendar/useCalendar.ts | 19 ++++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 frontend/src/constants/toasts.ts diff --git a/frontend/src/constants/toasts.ts b/frontend/src/constants/toasts.ts new file mode 100644 index 00000000..af28336f --- /dev/null +++ b/frontend/src/constants/toasts.ts @@ -0,0 +1,3 @@ +export const TOAST_MESSAGES = { + OUT_OF_ONE_YEAR_RANGE: '최대 1년뒤의 약속만 생성할 수 있어요', +}; diff --git a/frontend/src/hooks/useCalendar/useCalendar.ts b/frontend/src/hooks/useCalendar/useCalendar.ts index 17448f55..a2eee7d4 100644 --- a/frontend/src/hooks/useCalendar/useCalendar.ts +++ b/frontend/src/hooks/useCalendar/useCalendar.ts @@ -1,7 +1,11 @@ import { useState } from 'react'; import type { MonthlyDays } from 'types/calendar'; -import { getMonth, getYear } from '@utils/date'; +import useToast from '@hooks/useToast/useToast'; + +import { getFullDate, getMonth, getYear } from '@utils/date'; + +import { TOAST_MESSAGES } from '@constants/toasts'; import { getMonthlyCalendarDate } from './useCalendar.utils'; @@ -23,9 +27,11 @@ interface useCalendarReturn { } const TODAY = new Date(); +const ONE_YEAR_LATER = getFullDate(new Date(getYear(TODAY) + 1, getMonth(TODAY))); const useCalendar = (): useCalendarReturn => { const [currentFullDate, setCurrentFullDate] = useState(new Date()); + const { addToast } = useToast(); const currentYear = getYear(currentFullDate); const currentMonth = getMonth(currentFullDate); @@ -36,6 +42,17 @@ const useCalendar = (): useCalendarReturn => { }; const moveToNextMonth = () => { + const fullDate = getFullDate(currentFullDate); + + if (fullDate >= ONE_YEAR_LATER) { + addToast({ + message: TOAST_MESSAGES.OUT_OF_ONE_YEAR_RANGE, + type: 'warning', + duration: 2000, + }); + return; + } + setCurrentFullDate(new Date(currentYear, currentMonth + 1)); }; From 80d1286cf34c7b42455c831c0747f9bedabc4c74 Mon Sep 17 00:00:00 2001 From: hwinkr Date: Sat, 19 Oct 2024 11:52:35 +0900 Subject: [PATCH 11/21] =?UTF-8?q?design:=20css=20=EC=BB=A8=EB=B2=A4?= =?UTF-8?q?=EC=85=98=EC=9D=84=20=EB=A7=9E=EC=B6=94=EA=B8=B0=20=EC=9C=84?= =?UTF-8?q?=ED=95=B4=20rem=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/MeetingCalendar/Date/Date.styles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/MeetingCalendar/Date/Date.styles.ts b/frontend/src/components/MeetingCalendar/Date/Date.styles.ts index 4499c3b6..4a704d6d 100644 --- a/frontend/src/components/MeetingCalendar/Date/Date.styles.ts +++ b/frontend/src/components/MeetingCalendar/Date/Date.styles.ts @@ -121,7 +121,7 @@ export const s_rangeStart = (isAllRangeSelected: boolean) => css` position: absolute; top: 0; - right: 0.4px; + right: 0.02rem; bottom: 0; width: 20%; From 490ddeb45b5acfd63a1c2c72e77d90d362eebdbe Mon Sep 17 00:00:00 2001 From: hwinkr Date: Tue, 22 Oct 2024 17:03:36 +0900 Subject: [PATCH 12/21] =?UTF-8?q?refactor:=20FloatingLabelInput=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=EB=AA=85=20?= =?UTF-8?q?=EC=88=98=EC=A0=95,=20=EB=B6=88=ED=95=84=EC=9A=94=ED=96=94=20is?= =?UTF-8?q?Focused=20=EC=83=81=ED=83=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ies.tsx => FloatingLabelInput.stories.tsx} | 8 +++---- ...styles.ts => FloatingLabelInput.styles.ts} | 19 +++++++---------- .../src/components/FloatingInput/index.tsx | 21 ++++++------------- .../components/_common/Input/Input.styles.ts | 3 --- 4 files changed, 17 insertions(+), 34 deletions(-) rename frontend/src/components/FloatingInput/{FloatingInput.stories.tsx => FloatingLabelInput.stories.tsx} (69%) rename frontend/src/components/FloatingInput/{FloatingInput.styles.ts => FloatingLabelInput.styles.ts} (64%) diff --git a/frontend/src/components/FloatingInput/FloatingInput.stories.tsx b/frontend/src/components/FloatingInput/FloatingLabelInput.stories.tsx similarity index 69% rename from frontend/src/components/FloatingInput/FloatingInput.stories.tsx rename to frontend/src/components/FloatingInput/FloatingLabelInput.stories.tsx index 3697a372..802c75d7 100644 --- a/frontend/src/components/FloatingInput/FloatingInput.stories.tsx +++ b/frontend/src/components/FloatingInput/FloatingLabelInput.stories.tsx @@ -1,15 +1,15 @@ import type { Meta, StoryObj } from '@storybook/react'; -import FloatingInput from '.'; +import FloatingLabelInput from '.'; const meta = { - title: 'Components/Inputs/FloatingInput', - component: FloatingInput, + title: 'Components/Inputs/FloatingLabelInput', + component: FloatingLabelInput, argTypes: { label: { control: 'text' }, isError: { control: 'boolean' }, }, -} satisfies Meta; +} satisfies Meta; export default meta; diff --git a/frontend/src/components/FloatingInput/FloatingInput.styles.ts b/frontend/src/components/FloatingInput/FloatingLabelInput.styles.ts similarity index 64% rename from frontend/src/components/FloatingInput/FloatingInput.styles.ts rename to frontend/src/components/FloatingInput/FloatingLabelInput.styles.ts index 177ac99f..5ec720e9 100644 --- a/frontend/src/components/FloatingInput/FloatingInput.styles.ts +++ b/frontend/src/components/FloatingInput/FloatingLabelInput.styles.ts @@ -2,10 +2,15 @@ import { css } from '@emotion/react'; import theme from '@styles/theme'; -export const s_floatingLabelContainer = css` +export const s_floatingLabelContainer = (isError: boolean) => css` position: relative; display: inline-block; width: 100%; + color: ${isError ? '#EB1E1E' : '#71717a'}; + + &:focus-within label { + color: ${isError ? '#EB1E1E' : theme.colors.pink.medium}; + } `; export const s_floatingLabelInput = (isError: boolean) => css` @@ -24,23 +29,13 @@ export const s_floatingLabelInput = (isError: boolean) => css` } `; -const getLabelTextColor = (isFocused: boolean, isError: boolean): string => { - if (isError) return '#EB1E1E'; - - if (isFocused) return theme.colors.pink.medium; - - return '#71717a'; -}; - -export const s_floatingLabel = (isFocused: boolean, isError: boolean) => css` +export const s_floatingLabel = () => css` position: absolute; top: 0.4rem; left: 1em; ${theme.typography.captionMedium}; - color: ${getLabelTextColor(isFocused, isError)}; - background: transparent; transition: color 0.3s; diff --git a/frontend/src/components/FloatingInput/index.tsx b/frontend/src/components/FloatingInput/index.tsx index c80a0051..b7748fbc 100644 --- a/frontend/src/components/FloatingInput/index.tsx +++ b/frontend/src/components/FloatingInput/index.tsx @@ -1,5 +1,3 @@ -import { useState } from 'react'; - import type { InputProps } from '@components/_common/Input'; import Input from '@components/_common/Input'; @@ -7,26 +5,21 @@ import { s_floatingLabel, s_floatingLabelContainer, s_floatingLabelInput, -} from './FloatingInput.styles'; +} from './FloatingLabelInput.styles'; -interface FloatingInputProps extends InputProps { +interface FloatingLabelInputProps extends InputProps { label: string; isError: boolean; } -export default function FloatingInput({ +export default function FloatingLabelInput({ label, placeholder, isError, ...props -}: FloatingInputProps) { - const [isFocused, setIsFocused] = useState(false); - - const focus = () => setIsFocused(true); - const blur = () => setIsFocused(false); - +}: FloatingLabelInputProps) { return ( -
-