diff --git a/src/pages/Login/KakaoLoginButton/KakaoLoginButton.stories.tsx b/src/pages/Login/KakaoLoginButton/KakaoLoginButton.stories.tsx new file mode 100644 index 0000000..4ab259e --- /dev/null +++ b/src/pages/Login/KakaoLoginButton/KakaoLoginButton.stories.tsx @@ -0,0 +1,46 @@ +// KakaoLoginButton.stories.tsx +import type { Meta, StoryObj } from '@storybook/react'; +import { KakaoLoginButton } from './KakaoLoginButton'; + +const meta = { + title: 'Pages/Login/KakaoLoginButton', + component: KakaoLoginButton, + parameters: { + layout: 'centered', + docs: { + description: { + component: '카카오 로그인을 위한 버튼 컴포넌트입니다. 왼쪽에 카카오톡 아이콘이 고정되어 있습니다.', + }, + }, + }, + argTypes: { + text: { + description: '버튼에 표시될 텍스트입니다.', + control: 'text', + table: { + type: { summary: 'string' }, + defaultValue: { summary: '카카오 로그인' }, + }, + }, + onClick: { + description: '버튼 클릭 시 실행될 콜백 함수입니다.', + action: 'clicked', + table: { + type: { summary: '() => void' }, + }, + }, + }, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// 기본 스토리 +export const Default: Story = { + args: { + text: '카카오 로그인', + onClick: () => alert('카카오 로그인 버튼 클릭'), + }, +}; + diff --git a/src/pages/Login/KakaoLoginButton/KakaoLoginButton.tsx b/src/pages/Login/KakaoLoginButton/KakaoLoginButton.tsx new file mode 100644 index 0000000..ecb18c4 --- /dev/null +++ b/src/pages/Login/KakaoLoginButton/KakaoLoginButton.tsx @@ -0,0 +1,57 @@ +import theme from '@/styles/theme'; +import { Button } from '@chakra-ui/react'; +import styled from '@emotion/styled'; +import { RiKakaoTalkFill } from 'react-icons/ri'; + +const STYLES = { + BUTTON : { + MAX_WIDTH : '23.5625rem', //377px + HEIGHT : '2.875rem', //46px + FONT_SIZE : '1rem', + BORDER_RADIUS : '0.9375rem' //15px + } +} as const + + + +const StyledButton = styled(Button)` + width: 100%; + max-width: ${STYLES.BUTTON.MAX_WIDTH}; + height: ${STYLES.BUTTON.HEIGHT}; + font: ${theme.typography.fontFamily}; + border: none; + font-size: ${STYLES.BUTTON.FONT_SIZE}; + font-weight : normal; + border-radius: ${STYLES.BUTTON.BORDER_RADIUS}; + display: flex; + justify-content: center; + align-items: center; +`; + +type KakaoLoginButtonProps = { + text?: string; + onClick?: () => void; +}; + +export const KakaoLoginButton = ({ text='카카오 로그인', onClick }: KakaoLoginButtonProps) => { + return ( + } + backgroundColor={theme.colors.kakaoYellow} + _hover={{ + backgroundColor: theme.colors.kakaoYellow, + opacity: 0.7, + color : theme.colors.black + }} + sx={{ + '.chakra-button__icon': { + position: 'absolute', + left: '1rem' + } + }} + onClick={onClick} + > + {text} + + ); +}; \ No newline at end of file diff --git a/src/pages/Login/LoginButton/LoginButton.stories.tsx b/src/pages/Login/LoginButton/LoginButton.stories.tsx new file mode 100644 index 0000000..9ed59e1 --- /dev/null +++ b/src/pages/Login/LoginButton/LoginButton.stories.tsx @@ -0,0 +1,92 @@ +// LoginButton.stories.tsx +import type { Meta, StoryObj } from '@storybook/react'; +import { LoginButton } from './LoginButton'; +const meta = { + title: 'Pages/Login/LoginButton', + component: LoginButton, + parameters: { + layout: 'centered', + docs: { + description: { + component: '로그인 페이지에서 사용되는 버튼 컴포넌트입니다.', + }, + }, + }, + argTypes: { + text: { + description: '버튼에 표시될 텍스트입니다.', + control: 'text', + table: { + type: { summary: 'string' }, + defaultValue: { summary: '로그인' }, + }, + }, + width: { + description: '버튼의 너비를 지정합니다 (CSS width 값)', + control: 'text', + table: { + type: { summary: 'string' }, + defaultValue: { summary: '100%' }, + }, + }, + onClick: { + description: '버튼 클릭 시 실행될 콜백 함수입니다.', + action: 'clicked', + table: { + type: { summary: '() => void' }, + }, + }, + }, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// 기본 스토리 +export const Default: Story = { + args: { + text: '로그인', + onClick: () => alert('로그인 버튼 클릭'), + }, + parameters: { + docs: { + description: { + story: '기본적인 형태의 로그인 버튼입니다. 너비가 100%인 상태로 표시됩니다.', + }, + } + } +}; + +// 사이즈 지정 +export const WithFixedWidth: Story = { + args: { + text: '로그인', + width : '200px', + onClick: () => alert('로그인 버튼 클릭'), + }, + parameters: { + docs: { + description: { + story: 'String 형식으로 너비 값을 지정할 수 있습니다', + }, + } + } +}; + + + +// 다른 텍스트를 가진 버튼 +export const WithDifferentText: Story = { + args: { + text: '카카오 로그인', + onClick: () => alert('카카오 로그인 버튼 클릭'), + }, + parameters: { + docs: { + description: { + story: '버튼에 들어갈 텍스트 문구를 지정할 수 있습니다', + }, + } + } +}; diff --git a/src/pages/Login/LoginButton/LoginButton.tsx b/src/pages/Login/LoginButton/LoginButton.tsx new file mode 100644 index 0000000..6c3fa1b --- /dev/null +++ b/src/pages/Login/LoginButton/LoginButton.tsx @@ -0,0 +1,46 @@ +import styled from '@emotion/styled'; +import { Button } from '@chakra-ui/react'; +import theme from '@/styles/theme'; + +const STYLES = { + BUTTON : { + HEIGHT : '2.875rem', //46px + FONT_SIZE : '1rem', //16px + BORDER_RADIUS : '0.9375rem', //15px + MARGIN_BOTTOM : '1rem', + } +} + + +const StyledButton = styled(Button)<{width : string}>` + width : ${props =>props.width}; + height : ${STYLES.BUTTON.HEIGHT}; + font : ${theme.typography.fontFamily}; + font-size : ${STYLES.BUTTON.FONT_SIZE}; + font-weight : normal; + color : ${theme.colors.white}; + background : ${theme.colors.primary}; + border-radius : ${STYLES.BUTTON.BORDER_RADIUS}; + margin-bottom : ${STYLES.BUTTON.MARGIN_BOTTOM}; +`; + +type LoginButtonProps = { + text : string; + width?: string; + onClick?:()=>void; +}; + +export const LoginButton = ({ text, width="100%" , onClick }: LoginButtonProps) => { + return ( + {text} + ); +}; + diff --git a/src/pages/Login/LoginForm/LoginForm.stories.tsx b/src/pages/Login/LoginForm/LoginForm.stories.tsx new file mode 100644 index 0000000..c5c021f --- /dev/null +++ b/src/pages/Login/LoginForm/LoginForm.stories.tsx @@ -0,0 +1,43 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { LoginForm } from './LoginForm'; +import { ChakraProvider } from '@chakra-ui/react'; +import theme from '@/styles/theme'; +import { action } from '@storybook/addon-actions'; + +const meta = { + title: 'Pages/Login/LoginForm', + component: LoginForm, + parameters: { + layout: 'centered', + docs: { + description: { + component: '로그인 폼 컴포넌트입니다. 이메일과 비밀번호 입력, 유효성 검사 기능을 포함합니다.', + }, + }, + }, + argTypes: { + onSubmit: { + description: '로그인 폼 제출 시 호출되는 콜백 함수입니다. 이메일과 비밀번호 데이터를 파라미터로 전달받습니다.', + } + }, + decorators: [ + (Story) => ( + + + + ), + ], + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// 모든 스토리에서 공통으로 사용할 onSubmit 함수 +const mockSubmit = action('onSubmit'); + +export const Default: Story = { + args: { + onSubmit: mockSubmit + }, +}; \ No newline at end of file diff --git a/src/pages/Login/LoginForm/LoginForm.tsx b/src/pages/Login/LoginForm/LoginForm.tsx new file mode 100644 index 0000000..c9d1582 --- /dev/null +++ b/src/pages/Login/LoginForm/LoginForm.tsx @@ -0,0 +1,145 @@ +import { LoginButton } from '@/pages/Login/LoginButton/LoginButton'; +import theme from '@/styles/theme'; +import { FormControl, FormErrorMessage, FormLabel, Input, Stack } from '@chakra-ui/react'; +import styled from '@emotion/styled'; +import { SubmitHandler, useForm } from 'react-hook-form'; + +const STYLES = { + FORM: { + MAX_WIDTH: '26.5625rem', // 425px + MIN_HEIGHT : '11.625rem', // 170px + CONTROL_HEIGHT: '6.5rem', // 72px ===> 6.5rem(104px)으로 바꾸면 가능 + MARGIN_BOTTOM: '1rem', // 16px + }, + BUTTON: { + HEIGHT: '2.875rem', // 46px + BORDER_RADIUS: '0.9375rem' // 15px + }, + IPUT : { + MAX_WIDTH : '23.5625rem', // 377px + MIN_HEIGHT: '3rem', // 48px + PADDING : '1rem' + }, + LOGINBUTTON: { + MARGIN_TOP :'1rem' //16px + } +} as const; + + +const FormWrapper = styled.form` + width: 100%; + max-width: ${STYLES.FORM.MAX_WIDTH}; + min-height : ${STYLES.FORM.MIN_HEIGHT}; + object-fit : fill; +`; + +const StyledInput = styled(Input)` + width : 100%; + max-width: ${STYLES.IPUT.MAX_WIDTH}; + min-height: ${STYLES.IPUT.MIN_HEIGHT}; + border-radius: ${STYLES.BUTTON.BORDER_RADIUS}; + color: ${theme.colors.gray[300]}; + background: ${theme.colors.white}; + padding: 0 ${STYLES.IPUT.PADDING}; +`; + + +const StyledStack = styled(Stack)` + align: flex-start; + width: 100%; + max-width: ${STYLES.FORM.MAX_WIDTH}; +`; + +const StyledFormControl = styled(FormControl)` + height: ${STYLES.FORM.CONTROL_HEIGHT}; + position: relative; + margin-bottom: ${STYLES.FORM.MARGIN_BOTTOM}; +`; + +const StyledFormErrorMessage = styled(FormErrorMessage)` + position: absolute; + bottom: 0; +`; + +const StyledLoginButtonDiv = styled.div` + margin-top : ${STYLES.LOGINBUTTON.MARGIN_TOP}; +`; + +export interface LoginFormValues { + email: string; + password: string; +} + +interface LoginFormProps { + onSubmit: (data: LoginFormValues) => void; +} +export const LoginForm = ({ onSubmit }: LoginFormProps) => { + const { + register, + handleSubmit, + formState: { errors }, + } = useForm(); + + const handleFormSubmit: SubmitHandler = (data) => { + onSubmit(data); + }; + + return ( + + + + 이메일 + + + {errors.email?.message} + + + + + 비밀번호 + + + {errors.password?.message} + + + + + + + + + + + ); +}; \ No newline at end of file diff --git a/src/pages/Login/index.tsx b/src/pages/Login/index.tsx index 7f4f2ae..5428f4e 100644 --- a/src/pages/Login/index.tsx +++ b/src/pages/Login/index.tsx @@ -1,163 +1,109 @@ import styled from '@emotion/styled'; +import { HStack } from '@chakra-ui/react'; import { Logo } from "@/components/ui/Logo/Logo"; -import { Button, Input, FormControl, FormLabel, FormErrorMessage, Stack, HStack } from "@chakra-ui/react" -import { useForm, SubmitHandler } from "react-hook-form" -import { RiKakaoTalkFill } from "react-icons/ri"; - -const LAYOUT = { - HEADER_HEIGHT: '0.9375rem', - MIN_HEIGHT: '48.3125rem', - PADDING: '2rem', - GAP: '1rem', -} as const; +import theme from '@/styles/theme'; +import { LoginForm, LoginFormValues } from '@/pages/Login/LoginForm/LoginForm'; +import { KakaoLoginButton } from '@/pages/Login/KakaoLoginButton/KakaoLoginButton'; + + +const STYLES = { + LAYOUT : { + HEADER_HEIGHT: '0.9375rem', //15px + MIN_HEIGHT: '48.3125rem', //773px + PADDING: '1.5rem', //24px + GAP: '1rem', + }, + LOGO : { + WIDTH: '5.375rem', //86px + HEIGHT: '4.5625rem' //73px + }, -const FORM = { - MAX_WIDTH: '26.5625rem', - CONTROL_HEIGHT: '100px', - MARGIN_BOTTOM: '1rem', } as const; -const LOGO = { - SIZE: '30%', -} as const; -const Container = styled.div` - height :calc(100vh- ${LAYOUT.HEADER_HEIGHT}); - min-height : ${LAYOUT.MIN_HEIGHT}; +const LogoContainer = styled.div` display: flex; flex-direction: column; align-items: center; - padding: ${LAYOUT.PADDING}; - gap : ${LAYOUT.GAP}; -`; - -const FormWrapper = styled.form` - width: 100%; - max-width : ${FORM.MAX_WIDTH}; -`; - -const StyledStack = styled(Stack)` - align :flex-start; - width : 100%; `; -const StyledButton = styled(Button)` - width : 100%; -`; - - -const StyledFormControl = styled(FormControl)` - height: ${FORM.CONTROL_HEIGHT}; - position: relative; - margin-bottom: ${FORM.MARGIN_BOTTOM}; +const Container = styled.div` + height: calc(100vh - ${STYLES.LAYOUT.HEADER_HEIGHT}); + min-height: ${STYLES.LAYOUT.MIN_HEIGHT}; + display: flex; + flex-direction: column; + align-items: center; + padding: ${STYLES.LAYOUT.PADDING}; + gap: ${STYLES.LAYOUT.GAP}; + background: ${theme.colors.background}; `; -const StyledFormErrorMessage = styled(FormErrorMessage)` - position: absolute; - bottom: 0; +const StyledStack = styled.div` + display: flex; + flex-direction: column; + align-items: center; + width: 100%; `; const StyledHStack = styled(HStack)` - align-items : center; - justify-content : center; - width : 100%; + align-items: center; + justify-content: center; + width: 100%; `; +const Title = styled.h1` + font-size: ${theme.typography.title1.size}; + line-height: ${theme.typography.title1.lineHeight}; + font-weight: ${theme.typography.title1.weight}; + text-align: center; + color: ${theme.colors.primary}; + margin-top : 40px; +`; -const TextButton = styled(Button)` - background:transparent; +const TextButton = styled.button` + color: ${theme.colors.gray[300]}; + background: none; + border: none; + cursor: pointer; &:hover { - background:transparent; - color : gray; + text-decoration: underline; } - `; +export const Login = () => { + const handleLoginSubmit = (data: LoginFormValues) => { + console.log('로그인 시도:', data); + // 로그인 로직 처리 -interface LoginFormValues { - email: string; - password: string; -} - -const handleClickLogin = ()=>{ - console.log("로그인 클릭"); - -} - -const handleFindEmail = () =>{ - console.log('이메일찾기 버튼 클릭'); -} + }; -const handleRegister = () =>{ - console.log('회원가입 버튼 클릭'); + const handleKakaoLogin = () =>{ + // 카카오 로그인 로직 + console.log('카카오'); -} + }; + const handleFindEmail = () => { + // 이메일/비밀번호 찾기 로직 + }; -export const Login = () => { - const { - register, - handleSubmit, - formState: { errors }, - } = useForm(); - - const onSubmit: SubmitHandler = (data) => { - console.log(data); + const handleRegister = () => { + // 회원가입 로직 }; return ( - -

일상의 질문으로 연결되는 우리들의 이야기(가제)

- - - - - 이메일 - - - - - {errors.email?.message} - - - - - 비밀번호 - - - {errors.password?.message} - - - - 로그인 - - + + + + 나의 취향 메이트 찾기 + - } colorScheme='yellow' > - 카카오 로그인 - + + diff --git a/src/styles/theme.ts b/src/styles/theme.ts index 93a071b..efdf0f0 100644 --- a/src/styles/theme.ts +++ b/src/styles/theme.ts @@ -6,6 +6,7 @@ const theme = { blue: '#59B3F8', yellow: '#FCF4D0', textYellow: '#C1A830', + kakaoYellow : '#FEE500', background: '#F3EBE0', white: '#ffffff', black: '#121212',