diff --git a/apps/zoopi-web/.gitignore b/apps/zoopi-web/.gitignore index 26b4fb0..6826199 100644 --- a/apps/zoopi-web/.gitignore +++ b/apps/zoopi-web/.gitignore @@ -1,2 +1,3 @@ .storybook/dist -*.tsbuildinfo \ No newline at end of file +*.tsbuildinfo +.env \ No newline at end of file diff --git a/apps/zoopi-web/components/ModalLogin/ModalLogin.tsx b/apps/zoopi-web/components/ModalLogin/ModalLogin.tsx index 427024b..449dac0 100644 --- a/apps/zoopi-web/components/ModalLogin/ModalLogin.tsx +++ b/apps/zoopi-web/components/ModalLogin/ModalLogin.tsx @@ -4,21 +4,14 @@ import { useCallback, FormEventHandler, useRef, - forwardRef, - MutableRefObject, - ElementType, } from 'react'; import { Button } from '@web/components/Button'; import { Icon } from '@web/components/Icon'; import { Logo } from '@web/components/Logo'; import { Modal } from '@web/components/Modal'; -import { TextInput, TextInputProps } from '@web/components/TextInput'; -import { - TextInputPassword, - TextInputPasswordProps, -} from '@web/components/TextInputPassword'; - -const BUTTON_WIDTH = 400; +import { TextInput } from '@web/components/TextInput'; +import { useSignin } from '@web/hooks'; +import { validateId, validatePassword } from '@web/utils'; export type ModalLoginProps = { onClose: () => void; @@ -26,40 +19,44 @@ export type ModalLoginProps = { export const ModalLogin = ({ onClose }: ModalLoginProps) => { const theme = useTheme(); - const emailRef = useRef(null); - const passwordRef = useRef(null); + const { mutate } = useSignin(); - const handleSubmitLogin: FormEventHandler = useCallback((event) => { - event.preventDefault(); + const idRef = useRef(null); + const passwordRef = useRef(null); + const BUTTON_WIDTH = 400; - const email = emailRef?.current?.value; - const password = passwordRef?.current?.value; + const handleSubmitLogin: FormEventHandler = useCallback( + (event) => { + event.preventDefault(); - // eslint-disable-next-line no-console - console.log(email, password); - }, []); + const id = idRef?.current?.value; + const password = passwordRef?.current?.value; - const ForwardedComponent = ( - Component: ElementType, - props: T, - displayName: string - ) => { - const forwardedRef = (_, ref: MutableRefObject) => ( - - ); - forwardedRef.displayName = displayName; - return forwardRef(forwardedRef); - }; + if(!validateId(id)) { + alert('아이디는 6글자 이상 영문자(대/소)+숫자 조합이어야 합니다.'); + return; + } + if(!validatePassword(password)){ + // eslint-disable-next-line + alert(`비밀번호는 영문자(대/소)+숫자+특수문자 3가지 조합 10자리 이상이어야 합니다. ${"\n"}사용 가능한 특수문자: # $ % & ' ( ) * + , - . / : ; < = > ? @ [ ${"] ^ _ ` { | } ~ \ "}`); + return; + } - const EmailField = ForwardedComponent( - TextInput, - { label: '이메일', placeholder: 'sample@example.co.kr', type: 'email' }, - 'EmailInput' - ); - const PasswordField = ForwardedComponent( - TextInputPassword, - {}, - 'PasswordInput' + mutate( + { username: id, password }, + { + onSettled: (data)=>{ + const { code } = data; + if(code==='R-M006'){ + onClose(); + }else{ + alert(`${data.message}`) + } + } + } + ); + }, + [mutate,onClose] ); return ( @@ -104,13 +101,18 @@ export const ModalLogin = ({ onClose }: ModalLoginProps) => { }} >
- +
- +
- diff --git a/apps/zoopi-web/components/TextInput/TextInput.stories.tsx b/apps/zoopi-web/components/TextInput/TextInput.stories.tsx index 140c7bd..1d5c4bd 100644 --- a/apps/zoopi-web/components/TextInput/TextInput.stories.tsx +++ b/apps/zoopi-web/components/TextInput/TextInput.stories.tsx @@ -1,6 +1,6 @@ import { ComponentStory, ComponentMeta } from '@storybook/react'; -import { TextInput } from './TextInput'; +import { TextInput } from './index'; export default { title: 'atoms/TextInput', diff --git a/apps/zoopi-web/components/TextInput/TextInput.tsx b/apps/zoopi-web/components/TextInput/TextInput.tsx index befbadf..bed5d8b 100644 --- a/apps/zoopi-web/components/TextInput/TextInput.tsx +++ b/apps/zoopi-web/components/TextInput/TextInput.tsx @@ -8,6 +8,8 @@ import { useMemo, useId, useState, + MutableRefObject, + forwardRef, } from 'react'; import { Icon } from '@web/components/Icon'; import { Css, CssObject } from '@web/styles/theme'; @@ -23,22 +25,23 @@ export type TextInputProps = { type?: 'email' | 'password' | 'text'; clearDisabled?: boolean; right?: ReactNode; - forwardedRef?: string; }; -export const TextInput = ({ - children, - onChange, - onBlur, - onFocus, - value, - label, - clearDisabled = false, - right, - type = 'text', - forwardedRef, - ...rest -}: TextInputProps) => { +const TextInput = ( + { + children, + onChange, + onBlur, + onFocus, + value, + label, + clearDisabled = false, + right, + type = 'text', + ...rest + }: TextInputProps, + ref?: MutableRefObject +) => { const theme = useTheme(); const componentId = useId(); @@ -120,7 +123,7 @@ export const TextInput = ({ onChange={handleChange} onBlur={handleBlur} onFocus={handleFocus} - ref={forwardedRef} + ref={ref} css={css} />
@@ -143,3 +146,5 @@ export const TextInput = ({ ); }; + +export default forwardRef(TextInput) \ No newline at end of file diff --git a/apps/zoopi-web/components/TextInput/index.ts b/apps/zoopi-web/components/TextInput/index.ts index a7fcf6f..e857546 100644 --- a/apps/zoopi-web/components/TextInput/index.ts +++ b/apps/zoopi-web/components/TextInput/index.ts @@ -1 +1,2 @@ export * from './TextInput'; +export { default as TextInput } from './TextInput'; diff --git a/apps/zoopi-web/hooks/index.ts b/apps/zoopi-web/hooks/index.ts new file mode 100644 index 0000000..9a0e19f --- /dev/null +++ b/apps/zoopi-web/hooks/index.ts @@ -0,0 +1,2 @@ +export * from './useSignin'; +export * from './useRequest'; diff --git a/apps/zoopi-web/hooks/useRequest.tsx b/apps/zoopi-web/hooks/useRequest.tsx new file mode 100644 index 0000000..a640772 --- /dev/null +++ b/apps/zoopi-web/hooks/useRequest.tsx @@ -0,0 +1,14 @@ +import { useMutation, useQuery } from '@tanstack/react-query'; + +type Options = { + refetchInterval?: number; +}; + +export const useGetRequest = ( + queryName: string[], + queryFn: () => void, + options?: Options +) => useQuery(queryName, queryFn, { ...options }); + +export const usePostRequest = (queryFn: () => Promise) => + useMutation(queryFn); diff --git a/apps/zoopi-web/hooks/useSignin.tsx b/apps/zoopi-web/hooks/useSignin.tsx new file mode 100644 index 0000000..117fe50 --- /dev/null +++ b/apps/zoopi-web/hooks/useSignin.tsx @@ -0,0 +1,4 @@ +import { useMutation } from '@tanstack/react-query'; +import { AuthService } from '@web/services'; + +export const useSignin = () => useMutation(AuthService.signin) diff --git a/apps/zoopi-web/pages/_app.page.tsx b/apps/zoopi-web/pages/_app.page.tsx index a4d77e8..d0c4cd9 100644 --- a/apps/zoopi-web/pages/_app.page.tsx +++ b/apps/zoopi-web/pages/_app.page.tsx @@ -1,11 +1,16 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { AppProps } from 'next/app'; import { ThemeProvider, GlobalStyle } from '@web/styles'; +const queryClient = new QueryClient(); + const CustomApp = ({ Component, pageProps }: AppProps) => ( - - - - + + + + + + ); export default CustomApp; diff --git a/apps/zoopi-web/services/auth.service.ts b/apps/zoopi-web/services/auth.service.ts new file mode 100644 index 0000000..62ac4fb --- /dev/null +++ b/apps/zoopi-web/services/auth.service.ts @@ -0,0 +1,55 @@ +import { RequestService } from './request.service'; +import { Token } from '@web/utils'; + +class AuthService { + static async signin(user: { username: string; password: string }) { + const { data } = await RequestService.postRequest< + typeof user, + { + data:{ + code:string, + message: string, + status: string, + data: { + accessToken: string; + refreshToken: string; + } + } + } + >('/auth/signin', user); + + Token.setAccessToken(data.data.accessToken); + Token.setRefreshToken(data.data.refreshToken); + + return data; + } + + static async signup(authenticationKey, password, phone, username) { + await RequestService.postRequest< + { + authenticationKey: string; + password: string; + passwordCheck: string; + phone: string; + username: string; + }, + { + data: { + data: { + accessToken: string; + refreshToken: string; + }}; + } + >('auth/signup', { + authenticationKey, + password, + passwordCheck: password, + phone, + username, + }); + + // TODO: signup 명세서 업데이트되면 수정 예정 + } +} + +export default AuthService; diff --git a/apps/zoopi-web/services/index.ts b/apps/zoopi-web/services/index.ts new file mode 100644 index 0000000..b5803ac --- /dev/null +++ b/apps/zoopi-web/services/index.ts @@ -0,0 +1,2 @@ +export { default as AuthService } from './auth.service'; +export { default as RequestService } from './request.service'; diff --git a/apps/zoopi-web/services/request.service.ts b/apps/zoopi-web/services/request.service.ts new file mode 100644 index 0000000..7676036 --- /dev/null +++ b/apps/zoopi-web/services/request.service.ts @@ -0,0 +1,47 @@ +import axios, { AxiosRequestConfig } from 'axios'; + +export class RequestService { + static baseUrl = process.env.NEXT_PUBLIC_API_HOST; + + constructor(baseUrl?: string) { + if (baseUrl) RequestService.baseUrl = baseUrl; + } + + static async getRequest( + route: string, + token?: string, + customHeader?: AxiosRequestConfig['headers'] + ) { + const res = await axios.get( + this.baseUrl + route, + { + headers: { + ...customHeader, + Authorization: `Bearer ${token}`, + }, + } + ); + return res; + } + + static async postRequest( + route: string, + body: any, + token?: string, + customHeader?: AxiosRequestConfig['headers'] + ) { + const res = await axios.post( + this.baseUrl + route, + body, + { + headers: { + ...customHeader, + Authorization: `Bearer ${token}`, + }, + } + ); + return res; + } +} + +export default RequestService; diff --git a/apps/zoopi-web/utils/index.ts b/apps/zoopi-web/utils/index.ts new file mode 100644 index 0000000..c265375 --- /dev/null +++ b/apps/zoopi-web/utils/index.ts @@ -0,0 +1,2 @@ +export { default as Token } from './token.util'; +export * from './validate' \ No newline at end of file diff --git a/apps/zoopi-web/utils/token.util.ts b/apps/zoopi-web/utils/token.util.ts new file mode 100644 index 0000000..64a2691 --- /dev/null +++ b/apps/zoopi-web/utils/token.util.ts @@ -0,0 +1,25 @@ +import cookies from 'js-cookie'; + +class Token { + static ACCESS = 'access'; + + static REFRESH = 'refresh'; + + static getAccessToken(): string | undefined { + return cookies.get(this.ACCESS); + } + + static getRefreshToken(): string | undefined { + return cookies.get(this.REFRESH); + } + + static setAccessToken(access: string): void { + cookies.set(this.ACCESS, access, { expires: 1 }); + } + + static setRefreshToken(refresh: string): void { + cookies.set(this.REFRESH, refresh, { expires: 7 }); + } +} + +export default Token; diff --git a/apps/zoopi-web/utils/validate.ts b/apps/zoopi-web/utils/validate.ts new file mode 100644 index 0000000..92d3c98 --- /dev/null +++ b/apps/zoopi-web/utils/validate.ts @@ -0,0 +1,10 @@ + +export const validateId = (id:string) => { + const isValid = id.match(/^[A-Za-z0-9]{6,20}$/); + return isValid; +} + +export const validatePassword = (password:string) => { + const isValid = password.match(/^.(?=^.{10,20}$)(?=.\d)(?=.[a-zA-Z])(?=.[`~!@#$%^&()+=]).$/); + return isValid; +} \ No newline at end of file diff --git a/package.json b/package.json index abf509b..e7c325e 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,10 @@ "@emotion/server": "11.4.0", "@emotion/styled": "11.8.1", "@nrwl/next": "14.1.9", + "@tanstack/react-query": "^4.2.1", "core-js": "^3.6.5", "emotion-reset": "^3.0.1", + "js-cookie": "^3.0.1", "next": "12.1.5", "react": "18.1.0", "react-dom": "18.1.0", @@ -55,6 +57,7 @@ "@storybook/testing-library": "^0.0.12", "@testing-library/react": "13.1.1", "@types/jest": "27.4.1", + "@types/js-cookie": "^3.0.2", "@types/node": "16.11.7", "@types/react": "18.0.8", "@types/react-dom": "18.0.3", diff --git a/yarn.lock b/yarn.lock index b4d03cb..8b15bd1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3371,6 +3371,20 @@ content-type "^1.0.4" tslib "^2.4.0" +"@tanstack/query-core@^4.0.0-beta.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.2.1.tgz#21ff3a33f27bf038c990ea53af89cf7c7e8078fc" + integrity sha512-UOyOhHKLS/5i9qG2iUnZNVV3R9riJJmG9eG+hnMFIPT/oRh5UzAfjxCtBneNgPQZLDuP8y6YtRYs/n4qVAD5Ng== + +"@tanstack/react-query@^4.2.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.2.1.tgz#1f00f03573b35a353e62fa64f904bbb0286a1808" + integrity sha512-w02oTOYpoxoBzD/onAGRQNeLAvggLn7WZjS811cT05WAE/4Q3br0PTp388M7tnmyYGbgOOhFq0MkhH0wIfAKqA== + dependencies: + "@tanstack/query-core" "^4.0.0-beta.1" + "@types/use-sync-external-store" "^0.0.3" + use-sync-external-store "^1.2.0" + "@testing-library/dom@^8.3.0", "@testing-library/dom@^8.5.0": version "8.13.0" resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.13.0.tgz#bc00bdd64c7d8b40841e27a70211399ad3af46f5" @@ -3597,6 +3611,11 @@ jest-matcher-utils "^27.0.0" pretty-format "^27.0.0" +"@types/js-cookie@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-3.0.2.tgz#451eaeece64c6bdac8b2dde0caab23b085899e0d" + integrity sha512-6+0ekgfusHftJNYpihfkMu8BWdeHs9EOJuGcSofErjstGPfPGEu9yTu4t460lTzzAMl2cM5zngQJqPMHbbnvYA== + "@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.11" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" @@ -3817,6 +3836,11 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== +"@types/use-sync-external-store@^0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43" + integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== + "@types/webpack-env@^1.16.0", "@types/webpack-env@^1.17.0": version "1.17.0" resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.17.0.tgz#f99ce359f1bfd87da90cc4a57cab0a18f34a48d0" @@ -10070,6 +10094,11 @@ jest@27.5.1: import-local "^3.0.2" jest-cli "^27.5.1" +js-cookie@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.1.tgz#9e39b4c6c2f56563708d7d31f6f5f21873a92414" + integrity sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw== + js-string-escape@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/js-string-escape/-/js-string-escape-1.0.1.tgz#e2625badbc0d67c7533e9edc1068c587ae4137ef" @@ -14925,6 +14954,11 @@ url@^0.11.0: punycode "1.3.2" querystring "0.2.0" +use-sync-external-store@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" + integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== + use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"