diff --git a/.prettierrc.json b/.prettierrc.json index 55ef4156e..83ef0aa67 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -2,5 +2,21 @@ "tabWidth": 2, "singleQuote": true, "jsxSingleQuote": true, - "plugins": ["prettier-plugin-tailwindcss"] + "importOrder": [ + "^@/hooks/(.*)$", + "^@/utils/(.*)$", + "^@/services/(.*)$", + "^@/types/(.*)$", + "^@/pages/(.*)$", + "^@/containers/(.*)$", + "^@/components/(.*)$", + "^@/styles/(.*)$", + "^@/public/(.*)$", + "^[./]" + ], + "importOrderSortSpecifiers": true, + "plugins": [ + "@trivago/prettier-plugin-sort-imports", + "prettier-plugin-tailwindcss" + ] } diff --git a/README.md b/README.md index a75ac5248..e69de29bb 100644 --- a/README.md +++ b/README.md @@ -1,40 +0,0 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. - -[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. - -The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. - -This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/next.config.js b/next.config.js index c1bec96b6..19ebcd14c 100644 --- a/next.config.js +++ b/next.config.js @@ -22,6 +22,10 @@ const nextConfig = { protocol: 'https', hostname: '**', }, + { + protocol: 'http', + hostname: '**', + }, ], }, }; diff --git a/package-lock.json b/package-lock.json index 6992f9e86..e06340738 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,19 +12,22 @@ "next": "13.5.6", "react": "^18", "react-dom": "^18", + "react-hook-form": "^7.49.2", "react-timeago": "^7.2.0" }, "devDependencies": { "@svgr/webpack": "^8.1.0", + "@trivago/prettier-plugin-sort-imports": "^4.3.0", "@types/node": "^20", "@types/react": "^18", - "@types/react-dom": "^18", + "@types/react-dom": "^18.2.18", "@types/react-timeago": "^4.1.6", "autoprefixer": "^10.0.1", "eslint": "^8", "eslint-config-next": "13.5.6", + "eslint-config-prettier": "^9.1.0", "postcss": "^8", - "prettier": "^3.1.0", + "prettier": "^3.1.1", "prettier-plugin-tailwindcss": "^0.5.9", "tailwindcss": "^3.3.0", "typescript": "^5" @@ -2492,6 +2495,106 @@ "tslib": "^2.4.0" } }, + "node_modules/@trivago/prettier-plugin-sort-imports": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.3.0.tgz", + "integrity": "sha512-r3n0onD3BTOVUNPhR4lhVK4/pABGpbA7bW3eumZnYdKaHkf1qEC+Mag6DPbGNuuh0eG8AaYj+YqmVHSiGslaTQ==", + "dev": true, + "dependencies": { + "@babel/generator": "7.17.7", + "@babel/parser": "^7.20.5", + "@babel/traverse": "7.23.2", + "@babel/types": "7.17.0", + "javascript-natural-sort": "0.7.1", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "@vue/compiler-sfc": "3.x", + "prettier": "2.x - 3.x" + }, + "peerDependenciesMeta": { + "@vue/compiler-sfc": { + "optional": true + } + } + }, + "node_modules/@trivago/prettier-plugin-sort-imports/node_modules/@babel/generator": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.7.tgz", + "integrity": "sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.17.0", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@trivago/prettier-plugin-sort-imports/node_modules/@babel/traverse": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@trivago/prettier-plugin-sort-imports/node_modules/@babel/traverse/node_modules/@babel/generator": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.23.6", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@trivago/prettier-plugin-sort-imports/node_modules/@babel/traverse/node_modules/@babel/types": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@trivago/prettier-plugin-sort-imports/node_modules/@babel/types": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", + "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.16.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@trysound/sax": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", @@ -2534,9 +2637,9 @@ } }, "node_modules/@types/react-dom": { - "version": "18.2.17", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.17.tgz", - "integrity": "sha512-rvrT/M7Df5eykWFxn6MYt5Pem/Dbyc1N8Y0S9Mrkw2WFCRiqUgw9P7ul2NpwsXCSM1DVdENzdG9J5SreqfAIWg==", + "version": "18.2.18", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.18.tgz", + "integrity": "sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw==", "dev": true, "dependencies": { "@types/react": "*" @@ -3908,6 +4011,18 @@ } } }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, "node_modules/eslint-import-resolver-node": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", @@ -5199,6 +5314,12 @@ "set-function-name": "^2.0.1" } }, + "node_modules/javascript-natural-sort": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", + "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==", + "dev": true + }, "node_modules/jiti": { "version": "1.21.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", @@ -5358,6 +5479,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -6043,9 +6170,9 @@ } }, "node_modules/prettier": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz", - "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz", + "integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -6194,6 +6321,22 @@ "react": "^18.2.0" } }, + "node_modules/react-hook-form": { + "version": "7.49.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.49.2.tgz", + "integrity": "sha512-TZcnSc17+LPPVpMRIDNVITY6w20deMdNi6iehTFLV1x8SqThXGwu93HjlUVU09pzFgZH7qZOvLMM7UYf2ShAHA==", + "engines": { + "node": ">=18", + "pnpm": "8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -6552,6 +6695,15 @@ "tslib": "^2.0.3" } }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", diff --git a/package.json b/package.json index c33c654e5..a7d3589c2 100644 --- a/package.json +++ b/package.json @@ -13,19 +13,22 @@ "next": "13.5.6", "react": "^18", "react-dom": "^18", + "react-hook-form": "^7.49.2", "react-timeago": "^7.2.0" }, "devDependencies": { "@svgr/webpack": "^8.1.0", + "@trivago/prettier-plugin-sort-imports": "^4.3.0", "@types/node": "^20", "@types/react": "^18", - "@types/react-dom": "^18", + "@types/react-dom": "^18.2.18", "@types/react-timeago": "^4.1.6", "autoprefixer": "^10.0.1", "eslint": "^8", "eslint-config-next": "13.5.6", + "eslint-config-prettier": "^9.1.0", "postcss": "^8", - "prettier": "^3.1.0", + "prettier": "^3.1.1", "prettier-plugin-tailwindcss": "^0.5.9", "tailwindcss": "^3.3.0", "typescript": "^5" diff --git a/src/components/Input.tsx b/src/components/Input.tsx index ed14f5275..60d19c3c1 100644 --- a/src/components/Input.tsx +++ b/src/components/Input.tsx @@ -1,53 +1,84 @@ -import { InputHTMLAttributes, useState } from 'react'; +import { InputHTMLAttributes, ReactNode, forwardRef, useState } from 'react'; +import { + FieldValues, + UseControllerProps, + useController, +} from 'react-hook-form'; import { IconEyeOff, IconEyeOn } from '@/public/svgs'; -interface Props extends InputHTMLAttributes { - errorMessage?: string; - type?: string; +interface InputProps { + field: InputHTMLAttributes; + placeholder: string; + type: string; + error: boolean; } -function Input({ - id, - placeholder, - errorMessage, - onChange, - type: initialType = 'text', - onBlur, -}: Props) { - const [type, setType] = useState(initialType); +export const Input = forwardRef( + ({ field, placeholder, type: initialType, error = false }, ref) => { + const [type, setType] = useState(initialType); + + const toggleShow = () => { + setType((prevType) => (prevType === 'text' ? 'password' : 'text')); + }; + + const errorStyle = Boolean(error) ? 'border-red' : ''; + const focusStyle = !Boolean(error) ? 'focus:border-primary' : ''; + + return ( +
+ + {initialType === 'password' && ( + + )} +
+ ); + }, +); +Input.displayName = 'Input'; - const toggleShow = () => { - setType((prevType) => (prevType === 'text' ? 'password' : 'text')); - }; +interface InputContainerProps + extends UseControllerProps { + children: ReactNode; + placeholder?: string; + type?: string; +} - const errorStyle = Boolean(errorMessage) ? 'border-red' : ''; - const focusStyle = !Boolean(errorMessage) ? 'focus:border-primary' : ''; +export function InputContainer({ + children, + placeholder = '값을 입력하세요.', + type = 'text', + ...controls +}: InputContainerProps) { + const { field, fieldState } = useController(controls); return ( -
- + + - {initialType === 'password' && ( - - )} -
- {errorMessage} +
+ {fieldState?.error?.message}
); } - -export default Input; diff --git a/src/components/Navigator.tsx b/src/components/Navigator.tsx index 94aaede57..38ac9caad 100644 --- a/src/components/Navigator.tsx +++ b/src/components/Navigator.tsx @@ -1,6 +1,6 @@ -import { RefObject } from 'react'; -import Link from 'next/link'; import Image from 'next/image'; +import Link from 'next/link'; +import { RefObject, useEffect } from 'react'; import useRequest from '@/hooks/useRequest'; import Button from '@/components/Button'; import { IconLinkbrary } from '@/public/svgs'; @@ -25,14 +25,19 @@ interface UserInfo { } function Navigator({ isLoggedIn, userId, navRef }: Props) { - const { data } = useRequest({ + const { data, fetch } = useRequest({ skip: !isLoggedIn, options: { - url: `/users/${userId}`, + url: `/users`, method: 'get', }, }); + useEffect(() => { + const accessToken = localStorage.getItem('accessToken'); + fetch({ headers: { Authorization: `Bearer ${accessToken}` } }); + }, []); + const userInfo = data?.data?.[0]; return ( diff --git a/src/components/toasts/CopyToClipboard.tsx b/src/components/toasts/CopyToClipboard.tsx index d60c363d6..925e6bfb7 100644 --- a/src/components/toasts/CopyToClipboard.tsx +++ b/src/components/toasts/CopyToClipboard.tsx @@ -6,7 +6,7 @@ function CopyToClipboard({ show }: Props) { return ( <> {show && ( -
+
링크가 복사되었습니다.
)} diff --git a/src/containers/Auth/Signin.tsx b/src/containers/Auth/Signin.tsx index 295f975c6..1cfe75145 100644 --- a/src/containers/Auth/Signin.tsx +++ b/src/containers/Auth/Signin.tsx @@ -1,17 +1,17 @@ -import { - ChangeEvent, - FocusEvent, - SyntheticEvent, - useEffect, - useState, -} from 'react'; +import { useEffect } from 'react'; +import { FieldErrors, useForm } from 'react-hook-form'; import { useRouter } from 'next/router'; import useRequest from '@/hooks/useRequest'; import Button from '@/components/Button'; +import { InputContainer } from '@/components/Input'; import Header from './components/Header'; -import InputContainer from './components/InputContainer'; import Social from './components/Social'; -import { ERROR_MESSAGES, validateEmail, validatePassword } from './validation'; +import { EMAIL_REGEX, ERROR_MESSAGES } from './validation'; + +interface FormValues { + email: string; + password: string; +} interface Signin { data: { @@ -22,46 +22,36 @@ interface Signin { function Signin() { const router = useRouter(); - const [emailErrorMessage, setEmailErrorMessage] = useState(''); - const [passwordErrorMessage, setPasswordErrorMessage] = useState(''); - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - - const onEmailChange = (e: ChangeEvent) => { - const nextValue = e.target.value; - setEmail(nextValue); - }; - - const onPasswordChange = (e: ChangeEvent) => { - const nextValue = e.target.value; - setPassword(nextValue); - }; + const { handleSubmit, control, watch, setError } = useForm({ + defaultValues: { + email: '', + password: '', + }, + mode: 'onBlur', + }); const { fetch: signin } = useRequest({ skip: true, options: { url: `/sign-in`, method: 'post', - data: { email, password }, + data: { email: watch('email'), password: watch('password') }, }, }); - const onSignin = async (e: SyntheticEvent) => { - e.preventDefault(); - const newEmailErrorMessage = validateEmail(email); - const newPasswordErrorMessage = validatePassword(password, router.pathname); - if (newEmailErrorMessage || newPasswordErrorMessage) { - setEmailErrorMessage(newEmailErrorMessage); - setPasswordErrorMessage(newPasswordErrorMessage); - return; - } - + const onSignin = async () => { const { data, error } = await signin(); if (error) { - setEmailErrorMessage(ERROR_MESSAGES.email.invalidLogin); - setPasswordErrorMessage(ERROR_MESSAGES.password.invalidLogin); + setError('email', { + type: 'invalid', + message: ERROR_MESSAGES.email.invalidLogin, + }); + setError('password', { + type: 'invalid', + message: ERROR_MESSAGES.password.invalidLogin, + }); return; } @@ -70,14 +60,8 @@ function Signin() { router.push('/folder'); }; - const onEmailBlur = (e: FocusEvent) => { - const errorMessage = validateEmail(e.target.value); - setEmailErrorMessage(errorMessage); - }; - - const onPasswordBlur = (e: FocusEvent) => { - const errorMessage = validatePassword(e.target.value, router.pathname); - setPasswordErrorMessage(errorMessage); + const onError = (error: FieldErrors) => { + console.error(error); }; useEffect(() => { @@ -91,30 +75,37 @@ function Signin() {
- + control={control} + name='email' type='email' placeholder='codeit@codeit.com' - errorMessage={emailErrorMessage} - onChange={onEmailChange} - onBlur={onEmailBlur} + rules={{ + required: ERROR_MESSAGES.email.emptyInput, + pattern: { + value: EMAIL_REGEX, + message: ERROR_MESSAGES.email.invalidInput, + }, + }} > 이메일 - + control={control} + name='password' type='password' placeholder='• • • • • • • •' - errorMessage={passwordErrorMessage} - onChange={onPasswordChange} - onBlur={onPasswordBlur} + rules={{ + required: ERROR_MESSAGES.password.emptyInput, + }} > 비밀번호 + diff --git a/src/containers/Auth/Signup.tsx b/src/containers/Auth/Signup.tsx index 889df0f98..3ecb17011 100644 --- a/src/containers/Auth/Signup.tsx +++ b/src/containers/Auth/Signup.tsx @@ -1,22 +1,18 @@ -import { - ChangeEvent, - FocusEvent, - SyntheticEvent, - useEffect, - useState, -} from 'react'; +import { useEffect } from 'react'; +import { FieldErrors, useForm } from 'react-hook-form'; import { useRouter } from 'next/router'; import useRequest from '@/hooks/useRequest'; import Button from '@/components/Button'; +import { InputContainer } from '@/components/Input'; import Header from './components/Header'; -import InputContainer from './components/InputContainer'; import Social from './components/Social'; -import { - ERROR_MESSAGES, - validateEmail, - validatePassword, - validatePasswordCheck, -} from './validation'; +import { EMAIL_REGEX, ERROR_MESSAGES, PASSWORD_REGEX } from './validation'; + +interface FormValues { + email: string; + password: string; + passwordCheck: string; +} interface Signup { data: { @@ -33,36 +29,22 @@ interface CheckEmail { function Signup() { const router = useRouter(); - const [emailErrorMessage, setEmailErrorMessage] = useState(''); - const [passwordErrorMessage, setPasswordErrorMessage] = useState(''); - const [passwordCheckErrorMessage, setPasswordCheckErrorMessage] = - useState(''); - - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [passwordCheck, setPasswordCheck] = useState(''); - - const onEmailChange = (e: ChangeEvent) => { - const nextValue = e.target.value; - setEmail(nextValue); - }; - - const onPasswordChange = (e: ChangeEvent) => { - const nextValue = e.target.value; - setPassword(nextValue); - }; - const onPasswordCheckChange = (e: ChangeEvent) => { - const nextValue = e.target.value; - setPasswordCheck(nextValue); - }; + const { handleSubmit, control, watch, setError } = useForm({ + defaultValues: { + email: '', + password: '', + passwordCheck: '', + }, + mode: 'onBlur', + }); const { fetch: checkEmail } = useRequest({ skip: true, options: { url: `/check-email`, method: 'post', - data: { email }, + data: { email: watch('email') }, }, }); @@ -71,45 +53,28 @@ function Signup() { options: { url: `/sign-up`, method: 'post', - data: { email, password }, + data: { email: watch('email'), password: watch('password') }, }, }); - const onSignup = async (e: SyntheticEvent) => { - e.preventDefault(); - const newEmailErrorMessage = validateEmail(email); - const newPasswordErrorMessage = validatePassword(password, router.pathname); - const newPasswordCheckErrorMessage = validatePasswordCheck( - passwordCheck, - password, - ); - if ( - newEmailErrorMessage || - newPasswordErrorMessage || - newPasswordCheckErrorMessage - ) { - setEmailErrorMessage(newEmailErrorMessage); - setPasswordErrorMessage(newPasswordErrorMessage); - setPasswordCheckErrorMessage(newPasswordCheckErrorMessage); - } - - if (!newEmailErrorMessage) { - const { error: duplicateEmailError } = await checkEmail(); - if (duplicateEmailError) { - setEmailErrorMessage(ERROR_MESSAGES.email.unavailableEmail); - return; - } + const checkPasswordMatch = (value: string) => { + if (value !== watch('password')) { + return ERROR_MESSAGES.passwordCheck.invalidInput; } + return true; + }; - if ( - newEmailErrorMessage || - newPasswordErrorMessage || - newPasswordCheckErrorMessage - ) + const onSignup = async () => { + const { error: duplicateEmailError } = await checkEmail(); + if (duplicateEmailError) { + setError('email', { + type: 'unavailable', + message: ERROR_MESSAGES.email.unavailableEmail, + }); return; + } const { data, error: signupError } = await signup(); - if (signupError) { console.error('회원가입에 실패하였습니다.'); return; @@ -120,19 +85,8 @@ function Signup() { router.push('/folder'); }; - const onEmailBlur = (e: FocusEvent) => { - const errorMessage = validateEmail(e.target.value); - setEmailErrorMessage(errorMessage); - }; - - const onPasswordBlur = (e: FocusEvent) => { - const errorMessage = validatePassword(e.target.value, router.pathname); - setPasswordErrorMessage(errorMessage); - }; - - const onPasswordCheckBlur = (e: FocusEvent) => { - const errorMessage = validatePasswordCheck(e.target.value, password); - setPasswordCheckErrorMessage(errorMessage); + const onError = (error: FieldErrors) => { + console.error(error); }; useEffect(() => { @@ -146,40 +100,52 @@ function Signup() {
- + control={control} + name='email' type='email' placeholder='codeit@codeit.com' - errorMessage={emailErrorMessage} - onChange={onEmailChange} - onBlur={onEmailBlur} + rules={{ + required: ERROR_MESSAGES.email.emptyInput, + pattern: { + value: EMAIL_REGEX, + message: ERROR_MESSAGES.email.invalidInput, + }, + }} > 이메일 - + control={control} + name='password' type='password' placeholder='• • • • • • • •' - errorMessage={passwordErrorMessage} - onChange={onPasswordChange} - onBlur={onPasswordBlur} + rules={{ + required: ERROR_MESSAGES.password.emptyInput, + pattern: { + value: PASSWORD_REGEX, + message: ERROR_MESSAGES.password.invalidInput, + }, + }} > 비밀번호 - + control={control} + name='passwordCheck' type='password' placeholder='• • • • • • • •' - errorMessage={passwordCheckErrorMessage} - onChange={onPasswordCheckChange} - onBlur={onPasswordCheckBlur} + rules={{ + validate: checkPasswordMatch, + }} > 비밀번호 확인 + diff --git a/src/containers/Auth/components/InputContainer.tsx b/src/containers/Auth/components/InputContainer.tsx deleted file mode 100644 index 69576f44b..000000000 --- a/src/containers/Auth/components/InputContainer.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { InputHTMLAttributes, ReactNode } from 'react'; -import Input from '@/components/Input'; - -interface InputContainerProps extends InputHTMLAttributes { - id: string; - type: string; - placeholder: string; - errorMessage: string; - children: ReactNode; -} - -function InputContainer({ - id, - type, - placeholder, - errorMessage, - onChange, - onBlur, - children, -}: InputContainerProps) { - return ( -
- - -
- ); -} - -export default InputContainer; diff --git a/src/containers/Auth/validation.ts b/src/containers/Auth/validation.ts index 1f9ba2461..cac61bb0f 100644 --- a/src/containers/Auth/validation.ts +++ b/src/containers/Auth/validation.ts @@ -15,40 +15,7 @@ export const ERROR_MESSAGES = { }, }; -const EMAIL_REGEX = +export const EMAIL_REGEX = /^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i; -const PASSWORD_REGEX = /^(?=.*[a-zA-Z])(?=.*[0-9]).{8,25}$/; - -const validateEmailPattern = (email: string) => { - return EMAIL_REGEX.test(email); -}; - -const validatePasswordPattern = (password: string) => { - return PASSWORD_REGEX.test(password); -}; - -export const validateEmail = (value: string) => { - if (!value) { - return ERROR_MESSAGES.email.emptyInput; - } else if (!validateEmailPattern(value)) { - return ERROR_MESSAGES.email.invalidInput; - } - return ''; -}; - -export const validatePassword = (value: string, page: string) => { - if (!value) { - return ERROR_MESSAGES.password.emptyInput; - } else if (page === '/signup' && !validatePasswordPattern(value)) { - return ERROR_MESSAGES.password.invalidInput; - } - return ''; -}; - -export const validatePasswordCheck = (value: string, password: string) => { - if (value !== password) { - return ERROR_MESSAGES.passwordCheck.invalidInput; - } - return ''; -}; +export const PASSWORD_REGEX = /^(?=.*[a-zA-Z])(?=.*[0-9]).{8,25}$/; diff --git a/src/containers/Folder/components/FoldersContainer.tsx b/src/containers/Folder/components/FoldersContainer.tsx index 8b797fe9d..dc3b90de1 100644 --- a/src/containers/Folder/components/FoldersContainer.tsx +++ b/src/containers/Folder/components/FoldersContainer.tsx @@ -1,8 +1,8 @@ import { useEffect, useState } from 'react'; +import { DEFAULT_USER_ID } from '@/services/config/default'; +import { Folder } from '@/types/Folder.types'; import FoldersView from './FoldersView'; import InfoContainer from './InfoContainer'; -import { Folder } from '@/types/Folder.types'; -import { DEFAULT_USER_ID } from '@/services/config/default'; const DEFAULT_FOLDER = { id: 0, @@ -11,7 +11,7 @@ const DEFAULT_FOLDER = { interface Props { folders: Folder[]; - initialFolderId: number; + initialFolderId?: number; setFolderLinks: (nextFolderId: number) => void; } diff --git a/src/containers/Folder/index.tsx b/src/containers/Folder/index.tsx index bd7a030cc..92e9cac0f 100644 --- a/src/containers/Folder/index.tsx +++ b/src/containers/Folder/index.tsx @@ -1,36 +1,28 @@ -import { useEffect, useState, useRef } from 'react'; import { useRouter } from 'next/router'; -import { DEFAULT_USER_ID, DEFAULT_FOLDER_ID } from '@/services/config/default'; +import { useEffect, useRef, useState } from 'react'; import filterLinks from '@/utils/filterLinks'; +import { DEFAULT_FOLDER_ID, DEFAULT_USER_ID } from '@/services/config/default'; +import { Folder as IFolder, Link } from '@/types/Folder.types'; import Layout from '@/components/Layout'; import SearchBar from '@/components/SearchBar'; import CardsContainer from '@/components/cards/CardsContainer'; import AddLinkContainer from './components/AddLinkContainer'; import FoldersContainer from './components/FoldersContainer'; -import { Link, Folder } from '@/types/Folder.types'; interface Props { links: Link[]; - folders: Folder[]; + folders: IFolder[]; + folderId?: number; } -function Folder({ links, folders }: Props) { +function Folder({ links, folders, folderId }: Props) { const router = useRouter(); - const initialFolderId = Array.isArray(router.query.folderId) - ? router.query.folderId[0] - : router.query.folderId; const setFolderLinks = (nextFolderId: number) => { if (nextFolderId === DEFAULT_FOLDER_ID) { - router.push({ - pathname: router.pathname, - query: {}, - }); + router.push('/folder'); } else { - router.push({ - pathname: router.pathname, - query: { folderId: String(nextFolderId) }, - }); + router.push(`/folder/${nextFolderId}`); } }; @@ -95,7 +87,7 @@ function Folder({ links, folders }: Props) { diff --git a/src/containers/Shared/components/Banner.tsx b/src/containers/Shared/components/Banner.tsx index 9b06a6f04..5838af597 100644 --- a/src/containers/Shared/components/Banner.tsx +++ b/src/containers/Shared/components/Banner.tsx @@ -1,25 +1,22 @@ import Image from 'next/image'; +import { User } from '@/types/Folder.types'; const DEFAULT_PROFILE_IMAGE_SRC = '/images/default-profile-img.png'; interface Props { - name?: string; - owner?: { - id: number; - name: string; - profileImageSource: string; - }; + name: string; + owner: User; } function Banner({ name, owner }: Props) { return (
폴더 유저 프로필 이미지 -

{`@${owner?.name}`}

+

{`@${owner.name}`}

{name}

); diff --git a/src/containers/Shared/components/Folder.tsx b/src/containers/Shared/components/Folder.tsx index 0e33855ac..379e71f4c 100644 --- a/src/containers/Shared/components/Folder.tsx +++ b/src/containers/Shared/components/Folder.tsx @@ -1,15 +1,14 @@ import { useRouter } from 'next/router'; -import { DEFAULT_USER_ID } from '@/services/config/default'; import filterLinks from '@/utils/filterLinks'; +import { DEFAULT_USER_ID } from '@/services/config/default'; +import { SharedPageProps } from '@/pages/shared/[folderId]'; import SearchBar from '@/components/SearchBar'; import CardList from '@/components/cards/CardsContainer'; import Banner from './Banner'; -import { SharedFolder } from '@/types/Folder.types'; -function Folder({ folder }: SharedFolder) { - const name = folder?.name; - const owner = folder?.owner; - const links = folder?.links; +function Folder({ links, folderInfo, userInfo }: SharedPageProps) { + const name = folderInfo[0].name; + const owner = userInfo[0]; const router = useRouter(); const initialKeyword = Array.isArray(router.query.keyword) diff --git a/src/containers/Shared/index.tsx b/src/containers/Shared/index.tsx index cbf56ad89..2c943db4a 100644 --- a/src/containers/Shared/index.tsx +++ b/src/containers/Shared/index.tsx @@ -1,11 +1,11 @@ +import { SharedPageProps } from '@/pages/shared/[folderId]'; import Layout from '@/components/Layout'; import Folder from './components/Folder'; -import { SharedFolder } from '@/types/Folder.types'; -function Shared({ folder }: SharedFolder) { +function Shared({ links, folderInfo, userInfo }: SharedPageProps) { return ( - + ); } diff --git a/src/hooks/useRequest.ts b/src/hooks/useRequest.ts index f6f805cbb..7df44f5f9 100644 --- a/src/hooks/useRequest.ts +++ b/src/hooks/useRequest.ts @@ -14,7 +14,7 @@ function useRequest({ deps = [], skip = false, options }: Props) { const [error, setError] = useState(null); const refetch = useCallback( - async (...args: string[]) => { + async (args?: axiosOptions) => { setIsLoading(true); setError(null); @@ -29,7 +29,7 @@ function useRequest({ deps = [], skip = false, options }: Props) { setIsLoading(false); } }, - [options] + [options], ); useEffect(() => { diff --git a/src/pages/folder.tsx b/src/pages/folder.tsx deleted file mode 100644 index 10f91b521..000000000 --- a/src/pages/folder.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import Folder from '@/containers/Folder'; -import { Folder as IFolder, Link } from '@/types/Folder.types'; -import { getFoldersApi, getLinksApi } from '@/services/apis'; - -export async function getServerSideProps({ query }: { query: any }) { - const folderId = query?.folderId; - try { - const [{ data: links }, { data: folders }] = await Promise.all([ - getLinksApi(folderId), - getFoldersApi(), - ]); - - return { - props: { - links, - folders, - }, - }; - } catch { - return { - notFound: true, - }; - } -} - -interface Props { - links: Link[]; - folders: IFolder[]; -} - -function FolderPage({ links, folders }: Props) { - return ; -} - -export default FolderPage; diff --git a/src/pages/folder/[folderId].tsx b/src/pages/folder/[folderId].tsx new file mode 100644 index 000000000..4e5665f53 --- /dev/null +++ b/src/pages/folder/[folderId].tsx @@ -0,0 +1,53 @@ +import { useRouter } from 'next/router'; +import { useEffect } from 'react'; +import useRequest from '@/hooks/useRequest'; +import { Folder as IFolder, Link } from '@/types/Folder.types'; +import Folder from '@/containers/Folder'; + +function FolderPage() { + const router = useRouter(); + const folderId = Array.isArray(router.query.folderId) + ? router.query.folderId[0] + : router.query.folderId; + + const { data: folders, fetch: getFolders } = useRequest<{ + data: { folder: IFolder[] }; + }>({ + skip: true, + options: { + url: `folders`, + method: 'get', + }, + }); + const { data: links, fetch: getLinks } = useRequest<{ + data: { folder: Link[] }; + }>({ + skip: true, + options: { + url: `links`, + method: 'get', + params: { folderId: folderId ?? 1 }, + }, + }); + + useEffect(() => { + const accessToken = localStorage.getItem('accessToken'); + if (!accessToken) { + router.push('/signin'); + } + getFolders({ headers: { Authorization: `Bearer ${accessToken}` } }); + getLinks({ headers: { Authorization: `Bearer ${accessToken}` } }); + }, [folderId]); + + if (!folders || !links) return; + + return ( + + ); +} + +export default FolderPage; diff --git a/src/pages/folder/index.tsx b/src/pages/folder/index.tsx new file mode 100644 index 000000000..7bd52bd79 --- /dev/null +++ b/src/pages/folder/index.tsx @@ -0,0 +1,42 @@ +import { useRouter } from 'next/router'; +import { useEffect } from 'react'; +import useRequest from '@/hooks/useRequest'; +import { Folder as IFolder, Link } from '@/types/Folder.types'; +import Folder from '@/containers/Folder'; + +function FolderPage() { + const router = useRouter(); + const { data: folders, fetch: getFolders } = useRequest<{ + data: { folder: IFolder[] }; + }>({ + skip: true, + options: { + url: `folders`, + method: 'get', + }, + }); + const { data: links, fetch: getLinks } = useRequest<{ + data: { folder: Link[] }; + }>({ + skip: true, + options: { + url: `links`, + method: 'get', + }, + }); + + useEffect(() => { + const accessToken = localStorage.getItem('accessToken'); + if (!accessToken) { + router.push('/signin'); + } + getFolders({ headers: { Authorization: `Bearer ${accessToken}` } }); + getLinks({ headers: { Authorization: `Bearer ${accessToken}` } }); + }, []); + + if (!folders || !links) return; + + return ; +} + +export default FolderPage; diff --git a/src/pages/shared.tsx b/src/pages/shared.tsx deleted file mode 100644 index 22aa654bd..000000000 --- a/src/pages/shared.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import Shared from '@/containers/Shared'; -import { SharedFolder } from '@/types/Folder.types'; -import { getSharedFolderApi } from '@/services/apis'; - -export async function getStaticProps() { - const { folder } = await getSharedFolderApi(); - - return { - props: { - folder, - }, - }; -} - -function SharedPage({ folder }: SharedFolder) { - return ; -} - -export default SharedPage; diff --git a/src/pages/shared/[folderId].tsx b/src/pages/shared/[folderId].tsx new file mode 100644 index 000000000..0c692bdba --- /dev/null +++ b/src/pages/shared/[folderId].tsx @@ -0,0 +1,49 @@ +import { + getSharedFolderInfoApi, + getSharedLinksApi, + getSharedUserApi, +} from '@/services/apis'; +import { Folder, Link, User } from '@/types/Folder.types'; +import Shared from '@/containers/Shared'; + +export interface SharedPageProps { + links: Link[]; + folderInfo: Folder[]; + userInfo: User[]; +} + +interface SSGProps { + query: { + [q: string]: string; + }; +} + +export async function getServerSideProps({ query }: SSGProps) { + const { folderId } = query; + try { + const [{ data: links }, { data: folderInfo }, { data: userInfo }] = + await Promise.all([ + getSharedLinksApi(folderId), + getSharedFolderInfoApi(folderId), + getSharedUserApi(), + ]); + + return { + props: { + links, + folderInfo, + userInfo, + }, + }; + } catch { + return { + notFound: true, + }; + } +} + +function SharedPage({ links, folderInfo, userInfo }: SharedPageProps) { + return ; +} + +export default SharedPage; diff --git a/src/pages/test.tsx b/src/pages/test.tsx deleted file mode 100644 index 1d989e983..000000000 --- a/src/pages/test.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import Button from '@/components/Button'; - -function Home() { - return ( -
- - -
- HELLO -
-
- ); -} - -export default Home; diff --git a/src/services/apis.ts b/src/services/apis.ts index 03e0ea300..4a10cbb1a 100644 --- a/src/services/apis.ts +++ b/src/services/apis.ts @@ -1,22 +1,36 @@ -import api from './utils/api'; +import { Folder, Link, User } from '@/types/Folder.types'; import { DEFAULT_USER_ID } from './config/default'; -import { Folder, Link, SharedFolder } from '@/types/Folder.types'; +import api from './utils/api'; -export const getSharedFolderApi = () => - api({ - url: '/sample/folder', +export const getSharedFolderInfoApi = (folderId: string) => + api<{ data: Folder[] }>({ + url: `/folders/${folderId}`, method: 'get', }); -export const getLinksApi = (folderId: string) => +export const getSharedLinksApi = (folderId: string) => api<{ data: Link[] }>({ url: `/users/${DEFAULT_USER_ID}/links`, method: 'get', params: { folderId }, }); -export const getFoldersApi = () => +export const getSharedUserApi = () => + api<{ data: User[] }>({ + url: `/users/${DEFAULT_USER_ID}`, + method: 'get', + }); + +export const getFoldersApi = (accessToken: string) => api<{ data: Folder[] }>({ - url: `/users/${DEFAULT_USER_ID}/folders`, + url: `/folders`, method: 'get', + headers: { Authorization: `Bearer ${accessToken}` }, + }); + +export const getLinksApi = (accessToken: string, folderId: string) => + api<{ data: Link[] }>({ + url: `/links`, + method: 'get', + params: { folderId }, }); diff --git a/src/services/utils/api.ts b/src/services/utils/api.ts index 49707d5c4..1576ec104 100644 --- a/src/services/utils/api.ts +++ b/src/services/utils/api.ts @@ -1,14 +1,18 @@ +import { AxiosBasicCredentials } from 'axios'; import { defaultInstance } from '../config/default'; export interface axiosOptions { - url: string; - method: string; + url?: string; + method?: string; params?: { [param: string]: string | number | boolean; }; data?: { [data: string]: string | number | boolean; }; + headers?: { + Authorization?: string; + }; } const api = async (options: axiosOptions) => { diff --git a/src/types/Folder.types.ts b/src/types/Folder.types.ts index 7045797aa..f9161b667 100644 --- a/src/types/Folder.types.ts +++ b/src/types/Folder.types.ts @@ -11,20 +11,6 @@ export interface Link { updated_at?: string | null; } -export interface SharedFolder { - folder: { - id: number; - name: string; - count: number; - links: Link[]; - owner: { - id: number; - name: string; - profileImageSource: string; - }; - }; -} - export interface Folder { id: number; name: string; @@ -34,3 +20,12 @@ export interface Folder { count: number; }; } + +export interface User { + auth_id: string; + created_at: string; + email: string; + id: number; + image_source: string; + name: string; +}