diff --git a/.eslintrc.json b/.eslintrc.json index b21aa6c7c..434a1f222 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -7,11 +7,11 @@ "createDefaultProgram": true }, "env": { - "browser": true, + "browser": true, "node": true, "es6": true }, - "ignorePatterns": ["node_modules/"], + "ignorePatterns": ["node_modules/"], "extends": [ "airbnb", "airbnb-typescript", @@ -19,11 +19,12 @@ "next/core-web-vitals", "plugin:@typescript-eslint/recommended", "plugin:prettier/recommended", - "prettier" + "prettier" ], "rules": { + "jsx-quotes": ["error", "prefer-single"], "react/react-in-jsx-scope": "off", "react/jsx-filename-extension": ["warn", { "extensions": [".ts", ".tsx"] }], - "no-useless-catch": "off" + "no-useless-catch": "off" } } diff --git a/.prettierrc b/.prettierrc index 863885b97..7b8dbf4d9 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,6 +1,7 @@ { "semi": true, "singleQuote": true, + "jsxSingleQuote": true, "tabWidth": 2, "trailingComma": "all" } diff --git a/components/common/Card.tsx b/components/common/Card.tsx index 731b77ee5..5ae51adb3 100644 --- a/components/common/Card.tsx +++ b/components/common/Card.tsx @@ -8,18 +8,16 @@ import NoImg from '@/public/assets/icons/card/card_no-img.svg'; import kebab from '@/public/assets/icons/card/kebab.svg'; import styles from '@/styles/card/card.module.css'; -interface Link { - link: { - id: string; - createdAt: string; - url: string; - title: string; - description: string; - imageSource: string; - }; +interface LinkProps { + id: string; + createdAt: string; + url: string; + title: string; + description: string; + imageSource: string; } -export default function Card({ link }: Link) { +export default function Card({ link }: { link: LinkProps }) { const { id, createdAt, url, title, description, imageSource } = link; const [linkInfo, setLinkInfo] = useState({ id: '', @@ -43,7 +41,7 @@ export default function Card({ link }: Link) { }); }; - const getCreatedAt = useMemo(() => { + const yearMonthDay = useMemo(() => { const [year, month, day] = calCreatedDates(createdAt); setMins(calCreatedAt(year, month, day)); return { year, month, day }; @@ -57,7 +55,7 @@ export default function Card({ link }: Link) {
- + {imageSource ? (
diff --git a/components/common/Header.tsx b/components/common/Header.tsx index 1bf53d483..f67f64b01 100644 --- a/components/common/Header.tsx +++ b/components/common/Header.tsx @@ -3,23 +3,17 @@ import LoginButton from '@/components/common/LoginButton'; import Logo from '@/components/common/Logo'; import styles from '@/styles/header/header.module.css'; -interface Props { - profileData: { - id: number; - created_at?: string; - name: string; - image_source: string; - email: string; - auth_id?: string; - }; +interface HeaderProps { + id: number; + created_at?: string; + name: string; + image_source: string; + email: string; + auth_id?: string; } -export default function Header({ profileData }: Props) { - const { - name = 'defaultName', - image_source = 'https://images.unsplash.com/photo-1701600713610-0f724c65168d?q=80&w=1074&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', - email = 'default@email.com', - } = profileData; +export default function Header({ profileData }: { profileData: HeaderProps }) { + const { id, created_at, name, image_source, email, auth_id } = profileData; return (
diff --git a/components/common/SearchBar.tsx b/components/common/SearchBar.tsx index bb4e0bc76..7afa37fb8 100644 --- a/components/common/SearchBar.tsx +++ b/components/common/SearchBar.tsx @@ -14,10 +14,10 @@ export default function SearchBar() { const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Backspace' && router.query.keyword === '') { - setKeyword(router.query); + setKeyword(router.query.keyword); } if (e.key === 'Enter') { - setKeyword(router.query); + setKeyword(router.query.keyword); if (!router.query.keyword) return; setIsSearchResultShowed(true); } @@ -59,19 +59,19 @@ export default function SearchBar() { className={`${styles.searchbarInput} ${ isSearchActive ? styles.active : styles.inactive }`} - name="keyword" - type="text" - placeholder="링크를 검색해 보세요." + name='keyword' + type='text' + placeholder='링크를 검색해 보세요.' onChange={(e) => handleKeywordChange(e)} onKeyDown={(e) => handleKeyDown(e)} />
diff --git a/components/common/SearchResult.tsx b/components/common/SearchResult.tsx index ab0642979..c20e31df2 100644 --- a/components/common/SearchResult.tsx +++ b/components/common/SearchResult.tsx @@ -1,16 +1,14 @@ -import { useRouter } from 'next/router'; +import { useContext } from 'react'; +import FolderContext from '@/contexts/FolderContext'; import styles from '@/styles/search/searchResult.module.css'; export default function SearchResult() { - const router = useRouter(); - const search = router.query.keyword; + const { keyword } = useContext(FolderContext); return ( - <> - - {search} - 으로 검색한 결과입니다. - - + + {keyword} + 으로 검색한 결과입니다. + ); } diff --git a/components/common/auth/Input.tsx b/components/common/auth/Input.tsx index cc077265b..3dec72aa6 100644 --- a/components/common/auth/Input.tsx +++ b/components/common/auth/Input.tsx @@ -4,27 +4,28 @@ import styles from '@/styles/auth/auth.module.css'; export default function Input({ type, - name, placeholder, - onClick, onBlurInput, errorMsg, }: { type: string; - name?: string; placeholder: string; - onClick?: any; onBlurInput: (value: string) => any; errorMsg: string; }) { - const [isEyeClicked, setIsEyeClicked] = useState(false); + const [isPasswordVisible, setIsPasswordVisible] = useState(false); + const [inputValue, setInputValue] = useState(''); const handleEyeClick = () => { - setIsEyeClicked((prev) => !prev); + setIsPasswordVisible((prev) => !prev); }; - const handleOnBlur = (e: React.FocusEvent) => { - onBlurInput(e.target.value); + const handleOnChange = (e: React.ChangeEvent) => { + setInputValue(e.target.value); + }; + + const handleOnBlur = () => { + onBlurInput(inputValue); }; return ( @@ -32,9 +33,10 @@ export default function Input({
handleOnBlur(e)} + onChange={(e) => handleOnChange(e)} + onBlur={handleOnBlur} name={type} - type={type === 'password' && isEyeClicked ? 'text' : type} + type={type === 'password' && isPasswordVisible ? 'text' : type} className={ errorMsg ? `${styles.textfieldInput} ${styles.errorInput}` @@ -43,23 +45,23 @@ export default function Input({ /> {type === 'password' && ( diff --git a/components/common/folderPage/Dropdown.tsx b/components/common/folderPage/Dropdown.tsx index dc63d43d6..791a66b01 100644 --- a/components/common/folderPage/Dropdown.tsx +++ b/components/common/folderPage/Dropdown.tsx @@ -25,7 +25,7 @@ export default function Dropdown({
{clickedKebabOption.delete && ( + >; + addedLink: string; + setAddedLink: Dispatch>; + folderList: {}[]; + setFolderList: Dispatch>; + keyword: string; + setKeyword: Dispatch>; + filteredLinks: { + id: string; + createdAt: string; + url: string; + title: string; + description: string; + }[]; + setFilteredLinks: Dispatch< + SetStateAction< + { + id: string; + createdAt: string; + url: string; + title: string; + description: string; + }[] + > + >; + isSearchResultShowed: boolean; + setIsSearchResultShowed: Dispatch>; +} + +const FolderContextTest = createContext( + undefined, +); + +export function FolderContextTestProvider({ children }: any) { + const [folderList, setFolderList] = useState<{}[]>([]); + const [clickedOption, setClickedOption] = useState({ + addFolderLink: false, + shareFolder: false, + editFolderName: false, + deleteFolder: false, + addNewFolder: false, + }); + const [addedLink, setAddedLink] = useState(''); + const [keyword, setKeyword] = useState(''); + const [filteredLinks, setFilteredLinks] = useState< + { + id: string; + createdAt: string; + url: string; + title: string; + description: string; + }[] + >([]); + const [isSearchResultShowed, setIsSearchResultShowed] = useState(false); + + return ( + + {children} + + ); +} + +export default FolderContextTest; diff --git a/contexts/KebabContext.jsx b/contexts/KebabContext.jsx index 7b4c863fc..e92fb5408 100644 --- a/contexts/KebabContext.jsx +++ b/contexts/KebabContext.jsx @@ -1,4 +1,4 @@ -import { createContext, useState } from 'react'; +import { Dispatch, SetStateAction, createContext, useState } from 'react'; const KebabContext = createContext(); diff --git a/contexts/KebabContextTest.tsx b/contexts/KebabContextTest.tsx new file mode 100644 index 000000000..70fb6f888 --- /dev/null +++ b/contexts/KebabContextTest.tsx @@ -0,0 +1,44 @@ +import { Dispatch, SetStateAction, createContext, useState } from 'react'; + +interface KebabContextTestProps { + clickedKebabOption: { + delete: boolean; + addToFolder: boolean; + }; + setClickedKebabOption: Dispatch< + SetStateAction<{ + delete: boolean; + addToFolder: boolean; + }> + >; + closeKebab: () => void; +} + +const KebabContextTest = createContext( + undefined, +); + +export function KebabContextTestProvider({ children }: any) { + const [clickedKebabOption, setClickedKebabOption] = useState({ + delete: false, + addToFolder: false, + }); + + const closeKebab = () => { + setClickedKebabOption({ delete: false, addToFolder: false }); + }; + + return ( + + {children} + + ); +} + +export default KebabContextTest; diff --git a/next.config.js b/next.config.js index 957e20aa7..2230eab2b 100644 --- a/next.config.js +++ b/next.config.js @@ -75,6 +75,18 @@ const nextConfig = { port: '', pathname: '/**', }, + { + protocol: 'https', + hostname: 'avatars.githubusercontent.com', + port: '', + pathname: '/**', + }, + { + protocol: 'https', + hostname: 'images.unsplash.com', + port: '', + pathname: '/**', + }, ], }, }; diff --git a/package-lock.json b/package-lock.json index 809fc2338..b83b3e120 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,11 @@ "name": "ts-nextjs-weekly", "version": "0.1.0", "dependencies": { + "axios": "^1.6.5", "next": "14.0.4", "react": "^18", "react-dom": "^18", + "react-hook-form": "^7.49.3", "styled-components": "^6.1.8" }, "devDependencies": { @@ -19,7 +21,6 @@ "@types/react-dom": "^18", "@typescript-eslint/eslint-plugin": "^6.18.1", "@typescript-eslint/parser": "^6.18.1", - "axios": "^1.6.5", "eslint": "^8.2.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb-typescript": "^17.1.0", @@ -878,8 +879,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/available-typed-arrays": { "version": "1.0.5", @@ -906,7 +906,6 @@ "version": "1.6.5", "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==", - "dev": true, "dependencies": { "follow-redirects": "^1.15.4", "form-data": "^4.0.0", @@ -1051,7 +1050,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -1183,7 +1181,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -2199,7 +2196,6 @@ "version": "1.15.4", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==", - "dev": true, "funding": [ { "type": "individual", @@ -2228,7 +2224,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -3080,7 +3075,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -3089,7 +3083,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -3490,8 +3483,7 @@ "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, "node_modules/punycode": { "version": "2.3.1", @@ -3545,6 +3537,22 @@ "react": "^18.2.0" } }, + "node_modules/react-hook-form": { + "version": "7.49.3", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.49.3.tgz", + "integrity": "sha512-foD6r3juidAT1cOZzpmD/gOKt7fRsDhXXZ0y28+Al1CHgX+AY1qIN9VSIIItXRq1dN68QrRwl1ORFlwjBaAqeQ==", + "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", diff --git a/package.json b/package.json index 0c37440fa..27602f6a5 100644 --- a/package.json +++ b/package.json @@ -9,11 +9,12 @@ "lint": "next lint" }, "dependencies": { + "axios": "^1.6.5", "next": "14.0.4", "react": "^18", "react-dom": "^18", - "styled-components": "^6.1.8", - "axios": "^1.6.5" + "react-hook-form": "^7.49.3", + "styled-components": "^6.1.8" }, "devDependencies": { "@types/node": "^20", diff --git a/pages/folder/index.tsx b/pages/folder/index.tsx index 6b22aaac6..4e5bd865d 100644 --- a/pages/folder/index.tsx +++ b/pages/folder/index.tsx @@ -1,5 +1,11 @@ import { useContext, useEffect, useState } from 'react'; -import { getData } from '@/utils/api'; +import { + getAllFolders, + getFolders, + getLinks, + getLinksByFolder, + getUsers, +} from '@/utils/api'; import { filterByKeyword } from '@/utils/searchUtils'; import FolderContext from '@/contexts/FolderContext'; import Modal from '@/components/common/folderPage/modal/Modal'; @@ -12,8 +18,19 @@ import Nolinks from '@/components/common/folderPage/NoLinks'; import CardWrapper from '@/components/common/CardWrapper'; import FolderAddButton from '@/components/common/folderPage/FolderAddButton'; import styles from '@/styles/card/cardWrapper.module.css'; +import { getToken, setToken } from '@/utils/auth'; +import { useRouter } from 'next/router'; export default function Folder() { + const router = useRouter(); + const [profileData, setProfileData] = useState({ + id: 0, + created_at: '', + name: '', + image_source: '', + email: '', + auth_id: '', + }); const { addedLink, clickedOption, @@ -23,34 +40,24 @@ export default function Folder() { filteredLinks, setFilteredLinks, } = useContext(FolderContext); - const [profileData, setProfileData] = useState({ - id: 0, - created_at: '', - name: '', - image_source: '', - email: '', - auth_id: '', - }); const [links, setLinks] = useState([]); const [currentFolder, setCurrentFolder] = useState<{ - id: string; + id: number; name?: string; - }>({ id: '', name: '전체' }); + }>({ id: 0, name: '전체' }); const getUserData = async () => { try { - const result = await getData('users/1'); - const { id, created_at, name, image_source, email, auth_id } = - result.data[0]; - setProfileData((prevProfileData) => ({ - ...prevProfileData, + const result = await getUsers(); + const { id, created_at, name, image_source, email, auth_id } = result; + setProfileData({ id, created_at, name, image_source, email, auth_id, - })); + }); } catch (e) { throw new Error(`Folderpage getUserData ${e}`); } @@ -58,9 +65,9 @@ export default function Folder() { const getTotalLinksData = async () => { try { - const result = await getData('users/1/links'); - const { data } = result; - const links = data.map((link: any) => ({ + const result = await getLinks(profileData.id); + console.log(result, 'dd'); + const links = result.map((link: any) => ({ ...link, createdAt: link.created_at, imageSource: link.image_source, @@ -73,10 +80,9 @@ export default function Folder() { const getFolderList = async () => { try { - const result = await getData('users/1/folders'); - const { data } = result; - data.unshift({ name: '전체' }); - setFolderList(data); + const res = await getAllFolders(profileData.id); + res.unshift({ name: '전체' }); + setFolderList(res); } catch (e) { throw new Error(`Folderpage의 getFolderList에서 ${e} 발생`); } @@ -84,16 +90,13 @@ export default function Folder() { const getFolder = async () => { try { - const result = await getData( - `users/1/links?folderId=${currentFolder?.id}`, - ); - const { data } = result; - const datas = data.map((link: any) => ({ + const res = await getLinksByFolder(profileData.id, currentFolder.id); + const data = res.map((link: any) => ({ ...link, createdAt: link.created_at, imageSource: link.image_source, })); - setLinks(datas); + setLinks(data); } catch (e) { throw new Error( `Folderpage의 handleFolderClick의 getFolder에서 ${e} 발생`, @@ -101,26 +104,32 @@ export default function Folder() { } }; - const handleFolderClick = ( - e: React.MouseEvent, - ) => { - const textContent = e.currentTarget.textContent; - setCurrentFolder((prev) => ({ ...prev, name: textContent?.toString() })); - if (textContent === '전체') { + const handleFolderClick = (folderId: number) => { + if (!folderId) { + setCurrentFolder((prev) => ({ ...prev, name: '전체' })); getTotalLinksData(); return; } const clikedFolder: any = folderList.filter( - (folder: any) => folder.name === textContent, + (folder: any) => folder.id === folderId, ); setCurrentFolder(clikedFolder[0]); + if (folderId) { + router.push(`/folder/${folderId}`); + } }; useEffect(() => { + if (!getToken()) { + router.replace('/signin'); + } getUserData(); + }, []); + + useEffect(() => { getTotalLinksData(); getFolderList(); - }, []); + }, [profileData.id]); useEffect(() => { getFolder(); @@ -134,18 +143,18 @@ export default function Folder() { <> {clickedOption.addFolderLink && addedLink && ( )} {clickedOption.shareFolder && ( - + )} {clickedOption.editFolderName && ( )} {clickedOption.addNewFolder && ( @@ -179,7 +188,7 @@ export default function Folder() { return (
handleFolderClick(e)} + onClick={() => handleFolderClick(folder.id)} > + )}
@@ -213,7 +222,7 @@ export default function Folder() { )}
)} - {!folderList.length && } + {!folderList.length && }
); diff --git a/pages/index.tsx b/pages/index.tsx index 41df6ddca..412fc300e 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -78,17 +78,17 @@ export default function Home() { <> Linkbrary - - + + - +
diff --git a/pages/shared.tsx b/pages/shared/index.tsx similarity index 93% rename from pages/shared.tsx rename to pages/shared/index.tsx index 2f2a49acd..039e3d57b 100644 --- a/pages/shared.tsx +++ b/pages/shared/index.tsx @@ -9,8 +9,11 @@ import CardWrapper from '@/components/common/CardWrapper'; import styles from '@/styles/header/mainHeader.module.css'; export default function Shared() { - const { keyword, filteredLinks, setFilteredLinks } = - useContext(FolderContext); + const folderContext = useContext(FolderContext); + if (!folderContext) { + return null; + } + const { keyword, filteredLinks, setFilteredLinks } = folderContext; const [profileData, setProfileData] = useState({ id: 0, name: '', diff --git a/pages/signin/index.tsx b/pages/signin/index.tsx index 1d8faefd3..4e7ff7c7b 100644 --- a/pages/signin/index.tsx +++ b/pages/signin/index.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; import { useRouter } from 'next/router'; -import { postSignin } from '@/utils/api'; +import { signin } from '@/utils/api'; import { getToken, setToken } from '@/utils/auth'; import { validateEmail } from '@/utils/validation'; import AuthButton from '@/components/common/auth/AuthButton'; @@ -11,71 +11,63 @@ import styles from '@/styles/auth/auth.module.css'; export default function Signin() { const router = useRouter(); - const [authInfo, setAuthInfo] = useState({ - email: '', - password: '', - }); - const [errorMsg, setErrorMsg] = useState({ - email: '', - password: '', + const [form, setForm] = useState({ + email: { + value: '', + errorMsg: '', + }, + password: { + value: '', + errorMsg: '', + }, }); const emailOnBlurInput = (value: string): void => { if (!value) { - setErrorMsg((prev) => ({ + setForm((prev) => ({ ...prev, - email: '이메일을 입력해주세요', + email: { value: '', errorMsg: '이메일을 입력해주세요' }, })); - setAuthInfo((prev) => ({ ...prev, email: '' })); return; } if (!validateEmail(value)) { - setErrorMsg((prev) => ({ + setForm((prev) => ({ ...prev, - email: '올바른 이메일 주소가 아닙니다.', + email: { value: '', errorMsg: '올바른 이메일 주소가 아닙니다.' }, })); - setAuthInfo((prev) => ({ ...prev, email: '' })); return; } - setErrorMsg((prev) => ({ + setForm((prev) => ({ ...prev, - email: '', + email: { value: value, errorMsg: '' }, })); - setAuthInfo((prev) => ({ ...prev, email: value })); }; const passwordOnBlurInput = (value: string): void => { if (!value) { - setErrorMsg((prev) => ({ + setForm((prev) => ({ ...prev, - password: '비밀번호를 입력해주세요', + password: { ...prev.password, errorMsg: '비밀번호를 입력해주세요.' }, })); return; } - setErrorMsg((prev) => ({ + setForm((prev) => ({ ...prev, - password: '', + password: { value, errorMsg: '' }, })); - setAuthInfo((prev) => ({ ...prev, password: value })); + return; }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - if (!authInfo.email || !authInfo.password) return; - try { - const res = await postSignin({ - email: authInfo.email, - password: authInfo.password, - }); - console.log(res); - if (res.accessToken) { - setToken(res.accessToken); - router.replace('/folder'); - } else { - return; - } - } catch (e: any) { - throw new Error(`signup handleSubmit: ${e}`); + if (!form.email.value || !form.password.value) return; + const res = await signin({ + email: form.email.value, + password: form.password.value, + }); + if (res.accessToken) { + setToken(res.accessToken); + router.replace('/folder'); } }; @@ -90,21 +82,21 @@ export default function Signin() {
handleSubmit(e)}> - - + +
); diff --git a/pages/signup/index.tsx b/pages/signup/index.tsx index 2c659f9d9..177392b06 100644 --- a/pages/signup/index.tsx +++ b/pages/signup/index.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { useRouter } from 'next/router'; import { validateEmail, validatePassword } from '@/utils/validation'; -import { checkDuplicateEmail, postSignup } from '@/utils/api'; +import { checkDuplicateEmail, signup } from '@/utils/api'; import { getToken, setToken } from '@/utils/auth'; import TextField from '@/components/common/auth/TextField'; import AuthHeader from '@/components/common/auth/AuthHeader'; @@ -11,34 +11,34 @@ import styles from '@/styles/auth/auth.module.css'; export default function Signup() { const router = useRouter(); - const [authInfo, setAuthInfo] = useState({ - email: '', - password: '', - isPasswordConfirmed: false, - }); - const [errorMsg, setErrorMsg] = useState({ - email: '', - password: '', - passwordConfirmation: '', + const [form, setForm] = useState({ + email: { + value: '', + errorMsg: '', + }, + password: { + value: '', + errorMsg: '', + }, + passwordConfirmation: { + isConfirmed: false, + errorMsg: '', + }, }); const getEmailErrorMessage = async (value: string) => { - if (!authInfo.email) return; + if (!form.email.value) return; try { const res = await checkDuplicateEmail(value); if (res.error) { - setErrorMsg((prev) => ({ - ...prev, - email: res.error.message.toString(), - })); - setAuthInfo((prev) => ({ + setForm((prev) => ({ ...prev, - email: '', + email: { value: '', errorMsg: res.error.message.toString() }, })); } else { - setErrorMsg((prev) => ({ + setForm((prev) => ({ ...prev, - email: '', + email: { ...prev.email, errorMsg: '' }, })); } } catch (e: any) { @@ -48,105 +48,97 @@ export default function Signup() { const emailOnBlurInput = (value: string): void => { if (!value) { - setErrorMsg((prev) => ({ + setForm((prev) => ({ ...prev, - email: '이메일을 입력해주세요', + email: { value: '', errorMsg: '이메일을 입력하세요' }, })); - setAuthInfo((prev) => ({ ...prev, email: '' })); return; } if (!validateEmail(value)) { - setErrorMsg((prev) => ({ + setForm((prev) => ({ ...prev, - email: '올바른 이메일이 아닙니다.', + email: { value: '', errorMsg: '올바른 이메일이 아닙니다.' }, })); - setAuthInfo((prev) => ({ ...prev, email: '' })); return; } - setErrorMsg((prev) => ({ + setForm((prev) => ({ ...prev, - email: '', + email: { value: value, errorMsg: '' }, })); - setAuthInfo((prev) => ({ ...prev, email: value })); }; const passwordOnBlurInput = (value: string): void => { if (!value) { - setErrorMsg((prev) => ({ + setForm((prev) => ({ ...prev, - password: '비밀번호를 입력해주세요', + password: { ...prev.password, errorMsg: '비밀번호를 입력하세요.' }, })); - return; } if (!validatePassword(value)) { - setErrorMsg((prev) => ({ + setForm((prev) => ({ ...prev, - password: '비밀번호는 영문, 숫자 조합 8자 이상 입력해 주세요.', + password: { + ...prev.password, + errorMsg: '비밀번호는 영문, 숫자 조합 8자 이상 입력해 주세요.', + }, })); return; } - setErrorMsg((prev) => ({ + setForm((prev) => ({ ...prev, - password: '', + password: { + value, + errorMsg: '', + }, })); - setAuthInfo((prev) => ({ ...prev, password: value })); }; const passwordConfirmOnBlurInput = (value: string): void => { if (!value) { - setErrorMsg((prev) => ({ + setForm((prev) => ({ ...prev, - passwordConfirmation: '비밀번호를 입력해주세요', - })); - setAuthInfo((prev) => ({ - ...prev, - isPasswordConfirmed: false, + passwordConfirmation: { + isConfirmed: false, + errorMsg: '비밀번호를 입력해주세요', + }, })); return; } - if (value !== authInfo.password) { - setErrorMsg((prev) => ({ - ...prev, - passwordConfirmation: '비밀번호가 일치하지 않아요.', - })); - setAuthInfo((prev) => ({ + if (value !== form.password.value) { + setForm((prev) => ({ ...prev, - isPasswordConfirmed: false, + passwordConfirmation: { + isConfirmed: false, + errorMsg: '비밀번호가 일치하지 않아요.', + }, })); return; } - setErrorMsg((prev) => ({ - ...prev, - passwordConfirmation: '', - })); - setAuthInfo((prev) => ({ + setForm((prev) => ({ ...prev, - isPasswordConfirmed: true, + passwordConfirmation: { + isConfirmed: true, + errorMsg: '', + }, })); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - if (!authInfo.isPasswordConfirmed || !authInfo.email) return; - try { - const res = await postSignup({ - email: authInfo.email, - password: authInfo.password, - }); - if (res.accessToken) { - setToken(res.accessToken); - router.replace('/folder'); - } else { - return; - } - } catch (e: any) { - throw new Error(`signup handleSubmit: ${e}`); + if (!form.passwordConfirmation.isConfirmed || !form.email.value) return; + const res = await signup({ + email: form.email.value, + password: form.password.value, + }); + if (res.accessToken) { + setToken(res.accessToken); + router.replace('/folder'); } }; useEffect(() => { - getEmailErrorMessage(authInfo.email); - }, [authInfo.email]); + getEmailErrorMessage(form.email.value); + }, [form.email]); useEffect(() => { const accessToken = getToken(); @@ -159,28 +151,28 @@ export default function Signup() {
handleSubmit(e)}> - - + + ); diff --git a/utils/api.ts b/utils/api.ts index 0434c21e1..d6809a890 100644 --- a/utils/api.ts +++ b/utils/api.ts @@ -1,10 +1,67 @@ import axios from 'axios'; +import { getToken } from './auth'; -axios.defaults.baseURL = 'https://bootcamp-api.codeit.kr/api'; +const accessToken = getToken(); + +const instance = axios.create({ + baseURL: 'https://bootcamp-api.codeit.kr/api', + timeout: 5000, + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, +}); + +export async function getFolders() { + try { + const res = await instance.get('/folders'); + return res; + } catch (e) { + throw new Error(`getFolders ${e}`); + } +} + +export async function getUsers() { + try { + const res = await instance.get('/users'); + return res.data.data[0]; + } catch (e) { + throw new Error(`getUsers ${e}`); + } +} + +export async function getLinks(userId: number) { + try { + const res = await instance.get(`/users/${userId}/links`); + return res.data.data; + } catch (e) { + throw new Error(`getLinks ${e}`); + } +} + +export async function getAllFolders(userId: number) { + try { + const res = await instance.get(`/users/${userId}/folders`); + return res.data.data; + } catch (e) { + throw new Error(`getAllFolders ${e}`); + } +} + +export async function getLinksByFolder(userId: number, folderId: number) { + try { + const res = await instance.get( + `/users/${userId}/links?folderId=${folderId}`, + ); + return res.data.data; + } catch (e) { + throw new Error(`getLinksByFolder ${e}`); + } +} export async function getData(url: string) { try { - const response = await axios.get(`${url}`); + const response = await instance.get(`${url}`); return response.data; } catch (e) { throw new Error(`getData에서 ${e} 발생`); @@ -13,7 +70,7 @@ export async function getData(url: string) { export async function checkDuplicateEmail(email: string) { try { - const res = await axios.post('/check-email', { + const res = await instance.post('/check-email', { email: email, }); return res.data; @@ -22,20 +79,28 @@ export async function checkDuplicateEmail(email: string) { } } -export async function postSignup(data: { email: string; password: string }) { +export async function signup(data: { email: string; password: string }) { try { - const res = await axios.post('/sign-up', data); + const res = await instance.post('/sign-up', data); return res.data.data; } catch (e: any) { - if (e.response) return e.response.data; + if (!e.response) { + alert('회원가입에 실패했습니다.'); + } else { + return e.response.data; + } } } -export async function postSignin(data: { email: string; password: string }) { +export async function signin(data: { email: string; password: string }) { try { - const res = await axios.post('/sign-in', data); + const res = await instance.post('/sign-in', data); return res.data.data; } catch (e: any) { - if (e.response) return e.response.data; + if (!e.response) { + alert('로그인에 실패했습니다.'); + } else { + return e.response.data; + } } } diff --git a/utils/auth.ts b/utils/auth.ts index e42722146..8252cdc8c 100644 --- a/utils/auth.ts +++ b/utils/auth.ts @@ -1,5 +1,9 @@ export const getToken = () => { - return localStorage.getItem('accessToken'); + const accessToken = + typeof localStorage !== 'undefined' + ? localStorage.getItem('accessToken') + : null; + return accessToken; }; export const setToken = (token: string) => {