diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx
index b742f8222..793a2f0a2 100644
--- a/src/components/Modal/Modal.tsx
+++ b/src/components/Modal/Modal.tsx
@@ -1,6 +1,6 @@
import { createPortal } from 'react-dom';
import * as S from './Modal.style';
-import { MouseEvent, ReactNode, useEffect, useState } from 'react';
+import { MouseEvent, ReactNode, useCallback, useEffect, useState } from 'react';
import Image from 'next/image';
interface Props {
@@ -8,7 +8,7 @@ interface Props {
children: ReactNode;
}
-export default function Modal({ close, children }: Props) {
+const Modal = ({ close, children }: Props) => {
const [modalRoot, setModalRoot] = useState
(null);
useEffect(() => {
@@ -23,13 +23,17 @@ export default function Modal({ close, children }: Props) {
};
}, []);
+ const stopPropagation = useCallback((e: MouseEvent) => {
+ e.stopPropagation();
+ }, []);
+
if (!modalRoot) return null;
return createPortal(
- e.stopPropagation()}>
+
,
modalRoot
);
-}
+};
+
+export default Modal;
diff --git a/src/components/Modal/ModalAddFolder.tsx b/src/components/Modal/ModalAddFolder.tsx
index 77bd38ae4..a9e688139 100644
--- a/src/components/Modal/ModalAddFolder.tsx
+++ b/src/components/Modal/ModalAddFolder.tsx
@@ -1,6 +1,6 @@
import * as S from './Modal.style';
-function ModalAddFolder() {
+const ModalAddFolder = () => {
return (
<>
@@ -12,6 +12,6 @@ function ModalAddFolder() {
>
);
-}
+};
export default ModalAddFolder;
diff --git a/src/components/Modal/ModalAddLink.tsx b/src/components/Modal/ModalAddLink.tsx
index aa9f89092..57bc32ab4 100644
--- a/src/components/Modal/ModalAddLink.tsx
+++ b/src/components/Modal/ModalAddLink.tsx
@@ -6,7 +6,7 @@ interface Props {
url: string;
}
-export function ModalAddLink({ folders, url }: Props) {
+const ModalAddLink = ({ folders, url }: Props) => {
return (
<>
@@ -14,17 +14,18 @@ export function ModalAddLink({ folders, url }: Props) {
{url}
- {folders &&
- folders.map((folder) => (
-
- {folder.name}
- {folder.link.count}개 링크
-
- ))}
+ {folders
+ ? folders.map((folder) => (
+
+ {folder.name}
+ {folder.link.count}개 링크
+
+ ))
+ : null}
추가하기
>
);
-}
+};
export default ModalAddLink;
diff --git a/src/components/Modal/ModalDelete.tsx b/src/components/Modal/ModalDelete.tsx
index e295641df..20d9f6281 100644
--- a/src/components/Modal/ModalDelete.tsx
+++ b/src/components/Modal/ModalDelete.tsx
@@ -4,7 +4,7 @@ interface Props {
folderName: string;
}
-function ModalDelete({ folderName }: Props) {
+const ModalDelete = ({ folderName }: Props) => {
return (
<>
@@ -14,6 +14,6 @@ function ModalDelete({ folderName }: Props) {
삭제하기
>
);
-}
+};
export default ModalDelete;
diff --git a/src/components/Modal/ModalDeleteLink.tsx b/src/components/Modal/ModalDeleteLink.tsx
index 635ef9c4e..f98eb44b0 100644
--- a/src/components/Modal/ModalDeleteLink.tsx
+++ b/src/components/Modal/ModalDeleteLink.tsx
@@ -4,7 +4,7 @@ interface Props {
url: string;
}
-function ModalDeleteLink({ url }: Props) {
+const ModalDeleteLink = ({ url }: Props) => {
return (
<>
@@ -14,6 +14,6 @@ function ModalDeleteLink({ url }: Props) {
삭제하기
>
);
-}
+};
export default ModalDeleteLink;
diff --git a/src/components/Modal/ModalEdit.tsx b/src/components/Modal/ModalEdit.tsx
index 8feaa4767..0fbff428f 100644
--- a/src/components/Modal/ModalEdit.tsx
+++ b/src/components/Modal/ModalEdit.tsx
@@ -4,7 +4,7 @@ interface Props {
folderName: string;
}
-function ModalEdit({ folderName }: Props) {
+const ModalEdit = ({ folderName }: Props) => {
return (
<>
@@ -16,6 +16,6 @@ function ModalEdit({ folderName }: Props) {
>
);
-}
+};
export default ModalEdit;
diff --git a/src/components/Modal/ModalShare.tsx b/src/components/Modal/ModalShare.tsx
index f278a30a5..b54b8e82d 100644
--- a/src/components/Modal/ModalShare.tsx
+++ b/src/components/Modal/ModalShare.tsx
@@ -12,7 +12,7 @@ interface Props {
folderId: string | undefined;
}
-function ModalShare({ folderName, folderId }: Props) {
+const ModalShare = ({ folderName, folderId }: Props) => {
const handleShareKakao = () => {
window.Kakao.Share.sendDefault({
objectType: 'text',
@@ -72,6 +72,6 @@ function ModalShare({ folderName, folderId }: Props) {
>
);
-}
+};
export default ModalShare;
diff --git a/src/components/Nav/Nav.tsx b/src/components/Nav/Nav.tsx
index 448d61e79..f6d068ec8 100644
--- a/src/components/Nav/Nav.tsx
+++ b/src/components/Nav/Nav.tsx
@@ -1,80 +1,24 @@
-import { useEffect, useState } from 'react';
import Profile from '@components/Profile';
import * as S from './Nav.style';
-import useRequest from '@hooks/useRequest';
import Link from 'next/link';
import { useRouter } from 'next/router';
+import { SampleUserProfile } from '@pages/shared';
+import { UserProfile } from '@pages/folder';
-interface SampleUser {
- id: number;
- name: string;
- email: string;
- profileImageSource: string;
+interface Props {
+ profile?: SampleUserProfile | UserProfile;
}
-interface User {
- data: Data[];
-}
-
-interface Data {
- id: number;
- created_at: string;
- name: string;
- image_source: string;
- email: string;
- auth_id: string;
-}
-
-function Nav() {
- const [profile, setProfile] = useState({
- name: '',
- email: '',
- profileImageSource: '',
- image_source: '',
- });
- const [hasProfile, setHasProfile] = useState(false);
+const Nav = ({ profile }: Props) => {
const router = useRouter();
- const url = router.pathname === '/shared' ? '/sample/user' : '/users/1';
- const { fetch: fetchLoad } = useRequest({
- options: { url: url },
- });
-
- useEffect(() => {
- const loadProfile = async () => {
- const { data } = await fetchLoad();
- if (!data) {
- setHasProfile(false);
- return;
- }
- const typedData = data as SampleUser | User;
- if ('data' in typedData) {
- setProfile({
- name: typedData.data[0].name,
- email: typedData.data[0].email,
- profileImageSource: '',
- image_source: typedData.data[0].image_source,
- });
- } else {
- setProfile({
- name: typedData.name,
- email: typedData.email,
- profileImageSource: typedData.profileImageSource,
- image_source: '',
- });
- }
- setHasProfile(true);
- };
-
- loadProfile();
- }, []);
return (
- {hasProfile ? (
-
+ {profile ? (
+
) : (
로그인
@@ -82,6 +26,6 @@ function Nav() {
)}
);
-}
+};
export default Nav;
diff --git a/src/components/Profile/Profile.tsx b/src/components/Profile/Profile.tsx
index 929a8c2d6..d8bac79cf 100644
--- a/src/components/Profile/Profile.tsx
+++ b/src/components/Profile/Profile.tsx
@@ -1,18 +1,18 @@
import * as S from './Profile.style';
interface Props {
- data?: {
+ profile?: {
name: string;
email: string;
- profileImageSource: string;
- image_source: string;
+ profileImageSource?: string;
+ image_source?: string;
};
}
-function Profile({ data }: Props) {
- if (!data) return null;
+const Profile = ({ profile }: Props) => {
+ if (!profile) return null;
- const { name, email, profileImageSource, image_source } = data;
+ const { name, email, profileImageSource, image_source } = profile;
return (
@@ -24,6 +24,6 @@ function Profile({ data }: Props) {
{email}
);
-}
+};
export default Profile;
diff --git a/src/components/SampleCardList/SampleCard/SampleCard.tsx b/src/components/SampleCardList/SampleCard/SampleCard.tsx
index a10009fd6..c8c6dee6d 100644
--- a/src/components/SampleCardList/SampleCard/SampleCard.tsx
+++ b/src/components/SampleCardList/SampleCard/SampleCard.tsx
@@ -11,7 +11,7 @@ interface Props {
};
}
-function SampleCard({ item }: Props) {
+const SampleCard = ({ item }: Props) => {
if (!item) return null;
const { imageSource, createdAt, title, description, url } = item;
@@ -39,6 +39,6 @@ function SampleCard({ item }: Props) {
);
-}
+};
export default SampleCard;
diff --git a/src/components/SampleCardList/SampleCardList.tsx b/src/components/SampleCardList/SampleCardList.tsx
index 6753763e1..0bb3796a5 100644
--- a/src/components/SampleCardList/SampleCardList.tsx
+++ b/src/components/SampleCardList/SampleCardList.tsx
@@ -8,7 +8,7 @@ interface Props {
items: Link[];
}
-function SampleCardList({ searchKeyword, items }: Props) {
+const SampleCardList = ({ searchKeyword, items }: Props) => {
const lowerCaseKeyword = searchKeyword.toLowerCase();
const filteredItems = useMemo(
@@ -25,7 +25,7 @@ function SampleCardList({ searchKeyword, items }: Props) {
return (
<>
- {filteredItems && (
+ {filteredItems ? (
{filteredItems.map((item) => (
@@ -33,9 +33,9 @@ function SampleCardList({ searchKeyword, items }: Props) {
))}
- )}
+ ) : null}
>
);
-}
+};
export default SampleCardList;
diff --git a/src/components/SearchBar/SearchBar.tsx b/src/components/SearchBar/SearchBar.tsx
index d62e6e329..92d10b6e2 100644
--- a/src/components/SearchBar/SearchBar.tsx
+++ b/src/components/SearchBar/SearchBar.tsx
@@ -6,7 +6,7 @@ interface Props {
setSearchKeyword: (value: string) => void;
}
-function SearchBar({ setSearchKeyword }: Props) {
+const SearchBar = ({ setSearchKeyword }: Props) => {
const [value, setValue] = useState('');
const handleValueChange = (e: ChangeEvent) => {
@@ -28,7 +28,7 @@ function SearchBar({ setSearchKeyword }: Props) {
placeholder='링크를 검색해 보세요.'
/>
- {value && (
+ {value ? (
- )}
+ ) : null}
- {value && (
+ {value ? (
{value}으로 검색한 결과입니다.
- )}
+ ) : null}
);
-}
+};
export default SearchBar;
diff --git a/src/components/SignPageLayout/SignPageLayout.style.ts b/src/components/SignPageLayout/SignPageLayout.style.ts
new file mode 100644
index 000000000..79b8eaea5
--- /dev/null
+++ b/src/components/SignPageLayout/SignPageLayout.style.ts
@@ -0,0 +1,45 @@
+import { COLORS } from '@styles/color';
+import Link from 'next/link';
+import styled from 'styled-components';
+
+export const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 3rem;
+ padding-top: 12rem;
+ background-color: ${COLORS.GRAY_0};
+ min-height: 100vh;
+`;
+
+export const Header = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 1.6rem;
+`;
+
+export const Logo = styled.img`
+ height: 3.8rem;
+`;
+
+export const Recommendation = styled.div`
+ font-size: 1.6rem;
+ line-height: 2.4rem;
+ display: flex;
+ gap: 0.8rem;
+`;
+
+export const RecommendationLink = styled(Link)`
+ color: ${COLORS.PRIMARY};
+ font-weight: 600;
+ text-decoration: underline;
+`;
+
+export const Main = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ row-gap: 3.2rem;
+ width: 40rem;
+`;
diff --git a/src/components/SignPageLayout/SignPageLayout.tsx b/src/components/SignPageLayout/SignPageLayout.tsx
new file mode 100644
index 000000000..8d1f7dd2a
--- /dev/null
+++ b/src/components/SignPageLayout/SignPageLayout.tsx
@@ -0,0 +1,47 @@
+import { ReactNode } from 'react';
+import * as S from './SignPageLayout.style';
+import Link from 'next/link';
+import SocialBox from '@components/SocialBox';
+
+interface Props {
+ children?: ReactNode;
+ page: 'signin' | 'signup';
+}
+
+const SignPageLayout = ({ children, page }: Props) => {
+ return (
+
+
+
+
+
+
+ {page === 'signin' ? (
+ <>
+ 회원이 아니신가요?
+
+ 회원 가입하기
+
+ >
+ ) : (
+ <>
+ 이미 회원이신가요?
+
+ 로그인 하기
+
+ >
+ )}
+
+
+
+ {children}
+
+
+
+ );
+};
+
+export default SignPageLayout;
diff --git a/src/components/SignPageLayout/index.js b/src/components/SignPageLayout/index.js
new file mode 100644
index 000000000..5ddb9d810
--- /dev/null
+++ b/src/components/SignPageLayout/index.js
@@ -0,0 +1 @@
+export { default } from './SignPageLayout';
diff --git a/src/components/SigninForm/SigninForm.style.ts b/src/components/SigninForm/SigninForm.style.ts
new file mode 100644
index 000000000..f075c1d1b
--- /dev/null
+++ b/src/components/SigninForm/SigninForm.style.ts
@@ -0,0 +1,34 @@
+import { COLORS } from '@styles/color';
+import styled from 'styled-components';
+
+export const Form = styled.form`
+ display: flex;
+ flex-direction: column;
+ gap: 3rem;
+ width: 100%;
+`;
+
+export const InputWrapper = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 1.2rem;
+`;
+
+export const Label = styled.label`
+ font-size: 1.4rem;
+`;
+
+export const Button = styled.button`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: 5.4rem;
+ cursor: pointer;
+ background-image: linear-gradient(135deg, ${COLORS.PRIMARY} 0%, #6ae3fe 100%);
+ border: none;
+ border-radius: 0.8rem;
+ color: #f5f5f5;
+ font-size: 1.8rem;
+ font-weight: 600;
+`;
diff --git a/src/components/SigninForm/SigninForm.tsx b/src/components/SigninForm/SigninForm.tsx
new file mode 100644
index 000000000..db9526ff7
--- /dev/null
+++ b/src/components/SigninForm/SigninForm.tsx
@@ -0,0 +1,72 @@
+import apiRequest from '@api/apiRequest';
+import * as S from './SigninForm.style';
+import Input from '@components/Input';
+import { FormProvider, useForm } from 'react-hook-form';
+import { useRouter } from 'next/router';
+import { EMAIL_REGEX } from '@utils/regex';
+
+interface IFormInput {
+ email: string;
+ password: string;
+}
+
+const SigninForm = () => {
+ const { ...methods } = useForm({ mode: 'onBlur' });
+ const { register, handleSubmit, setError } = methods;
+ const router = useRouter();
+
+ const submitForm = async (data: IFormInput) => {
+ try {
+ const response = await apiRequest({
+ url: 'sign-in',
+ method: 'POST',
+ data,
+ });
+
+ if (response.status === 200) {
+ localStorage.setItem('accessToken', response.data.accessToken);
+ router.push('/folder');
+ }
+ } catch (error) {
+ setError('email', { message: '이메일을 확인해주세요.' });
+ setError('password', { message: '비밀번호를 확인해주세요.' });
+ }
+ };
+
+ const onSubmit = (data: IFormInput) => {
+ submitForm(data);
+ };
+
+ return (
+
+
+
+ 이메일
+
+
+
+ 비밀번호
+
+
+ 로그인
+
+
+ );
+};
+
+export default SigninForm;
diff --git a/src/components/SigninForm/index.js b/src/components/SigninForm/index.js
new file mode 100644
index 000000000..0be1d3255
--- /dev/null
+++ b/src/components/SigninForm/index.js
@@ -0,0 +1 @@
+export { default } from './SigninForm';
diff --git a/src/components/SignupForm/SignupForm.style.ts b/src/components/SignupForm/SignupForm.style.ts
new file mode 100644
index 000000000..f075c1d1b
--- /dev/null
+++ b/src/components/SignupForm/SignupForm.style.ts
@@ -0,0 +1,34 @@
+import { COLORS } from '@styles/color';
+import styled from 'styled-components';
+
+export const Form = styled.form`
+ display: flex;
+ flex-direction: column;
+ gap: 3rem;
+ width: 100%;
+`;
+
+export const InputWrapper = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 1.2rem;
+`;
+
+export const Label = styled.label`
+ font-size: 1.4rem;
+`;
+
+export const Button = styled.button`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: 5.4rem;
+ cursor: pointer;
+ background-image: linear-gradient(135deg, ${COLORS.PRIMARY} 0%, #6ae3fe 100%);
+ border: none;
+ border-radius: 0.8rem;
+ color: #f5f5f5;
+ font-size: 1.8rem;
+ font-weight: 600;
+`;
diff --git a/src/components/SignupForm/SignupForm.tsx b/src/components/SignupForm/SignupForm.tsx
new file mode 100644
index 000000000..175e52b5e
--- /dev/null
+++ b/src/components/SignupForm/SignupForm.tsx
@@ -0,0 +1,128 @@
+import apiRequest from '@api/apiRequest';
+import * as S from './SignupForm.style';
+import Input from '@components/Input';
+import { FormProvider, useForm } from 'react-hook-form';
+import { useRouter } from 'next/router';
+import { useEffect } from 'react';
+import { EMAIL_REGEX, PASSWORD_REGEX } from '@utils/regex';
+
+interface IFormInput {
+ email: string;
+ password: string;
+ passwordConfirm: string;
+}
+
+const SignupForm = () => {
+ const { ...methods } = useForm({ mode: 'onBlur' });
+ const { register, handleSubmit, watch, setError, clearErrors } = methods;
+ const router = useRouter();
+ const password = watch('password');
+ const passwordConfirm = watch('passwordConfirm');
+
+ const checkEmail = async (email: string) => {
+ try {
+ const response = await apiRequest({
+ url: 'check-email',
+ method: 'POST',
+ data: { email },
+ });
+
+ if (response.status !== 200) {
+ throw new Error();
+ }
+ } catch (error) {
+ setError('email', { message: '이미 존재하는 이메일입니다.' });
+ return false;
+ }
+ return true;
+ };
+
+ const submitForm = async (data: IFormInput) => {
+ const isEmailValid = await checkEmail(data.email);
+
+ if (!isEmailValid) {
+ return;
+ }
+
+ try {
+ const response = await apiRequest({
+ url: 'sign-up',
+ method: 'POST',
+ data,
+ });
+
+ if (response.status === 200) {
+ localStorage.setItem('accessToken', response.data.accessToken);
+ router.push('/folder');
+ }
+ } catch (error) {
+ console.log(error);
+ }
+ };
+
+ const onSubmit = (data: IFormInput) => {
+ submitForm(data);
+ };
+
+ useEffect(() => {
+ if (passwordConfirm && password !== passwordConfirm) {
+ setError('passwordConfirm', {
+ message: '비밀번호가 일치하지 않아요.',
+ });
+ } else {
+ clearErrors('passwordConfirm');
+ }
+ }, [passwordConfirm]);
+
+ return (
+
+
+
+ 이메일
+
+
+
+ 비밀번호
+
+
+
+ 비밀번호 확인
+
+ value === password || '비밀번호가 일치하지 않아요.',
+ })}
+ />
+
+ 회원가입
+
+
+ );
+};
+
+export default SignupForm;
diff --git a/src/components/SignupForm/index.js b/src/components/SignupForm/index.js
new file mode 100644
index 000000000..ace491221
--- /dev/null
+++ b/src/components/SignupForm/index.js
@@ -0,0 +1 @@
+export { default } from './SignupForm';
diff --git a/src/components/SocialBox/SocialBox.style.ts b/src/components/SocialBox/SocialBox.style.ts
new file mode 100644
index 000000000..30ef5174c
--- /dev/null
+++ b/src/components/SocialBox/SocialBox.style.ts
@@ -0,0 +1,23 @@
+import { COLORS } from '@styles/color';
+import styled from 'styled-components';
+
+export const Container = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+ padding: 1.2rem 2.4rem;
+ border-radius: 0.8rem;
+ border: 0.1rem solid ${COLORS.GRAY_20};
+ background: ${COLORS.GRAY_10};
+`;
+
+export const P = styled.p`
+ font-size: 1.4rem;
+ color: ${COLORS.GRAY_100};
+`;
+
+export const Links = styled.div`
+ display: flex;
+ gap: 1.6rem;
+`;
diff --git a/src/components/SocialBox/SocialBox.tsx b/src/components/SocialBox/SocialBox.tsx
new file mode 100644
index 000000000..a17fb3b75
--- /dev/null
+++ b/src/components/SocialBox/SocialBox.tsx
@@ -0,0 +1,36 @@
+import Link from 'next/link';
+import * as S from './SocialBox.style';
+
+interface Props {
+ page: 'signin' | 'signup';
+}
+
+const SocialBox = ({ page }: Props) => {
+ return (
+
+ {page === 'signin' ? '소셜 로그인' : '다른 방식으로 가입하기'}
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default SocialBox;
diff --git a/src/components/SocialBox/index.js b/src/components/SocialBox/index.js
new file mode 100644
index 000000000..07e4e8c27
--- /dev/null
+++ b/src/components/SocialBox/index.js
@@ -0,0 +1 @@
+export { default } from './SocialBox';
diff --git a/src/hooks/useRequest.ts b/src/hooks/useRequest.ts
index d2d714aaa..349c70079 100644
--- a/src/hooks/useRequest.ts
+++ b/src/hooks/useRequest.ts
@@ -1,5 +1,5 @@
import { useEffect, useState, useCallback, DependencyList } from 'react';
-import fetch from '@api/fetch';
+import apiRequest from '@api/apiRequest';
import { AxiosRequestConfig } from 'axios';
interface UseRequestOptions {
@@ -15,11 +15,11 @@ interface UseRequestReturn {
fetch: (args?: AxiosRequestConfig) => Promise<{ data?: T; error?: Error }>;
}
-function useRequest({
+const useRequest = ({
deps = [],
skip = false,
options,
-}: UseRequestOptions): UseRequestReturn {
+}: UseRequestOptions): UseRequestReturn => {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
@@ -30,7 +30,7 @@ function useRequest({
setError(null);
try {
- const { data: fetchedData } = await fetch({ ...options, ...args });
+ const { data: fetchedData } = await apiRequest({ ...options, ...args });
setData(() => fetchedData);
return { data: fetchedData };
} catch (err) {
@@ -49,6 +49,6 @@ function useRequest({
}, deps);
return { data, isLoading, error, fetch: refetch };
-}
+};
export default useRequest;
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
index bd54a80a6..ebfb06d8e 100644
--- a/src/pages/_app.tsx
+++ b/src/pages/_app.tsx
@@ -2,7 +2,7 @@ import type { AppProps } from 'next/app';
import Head from 'next/head';
import GlobalStyle from '@styles/GlobalStyle';
-export default function App({ Component, pageProps }: AppProps) {
+const App = ({ Component, pageProps }: AppProps) => {
return (
<>
@@ -12,4 +12,6 @@ export default function App({ Component, pageProps }: AppProps) {
>
);
-}
+};
+
+export default App;
diff --git a/src/pages/folder.tsx b/src/pages/folder.tsx
index b588c8b56..55857ea10 100644
--- a/src/pages/folder.tsx
+++ b/src/pages/folder.tsx
@@ -5,7 +5,7 @@ import FolderList from '@components/FolderList';
import SearchBar from '@components/SearchBar';
import { useEffect, useRef, useState } from 'react';
import { MainDiv } from '@styles/MainDiv';
-import fetch from '@api/fetch';
+import apiRequest from '@api/apiRequest';
import { GetServerSidePropsContext } from 'next';
import Layout from '@components/Layout/Layout';
@@ -25,28 +25,44 @@ interface Link {
count: number;
}
+export interface UserProfile {
+ id: number;
+ created_at: string;
+ name: string;
+ image_source: string;
+ email: string;
+ auth_id: string;
+}
+
interface Props {
cards: Cards;
folders: Folder[];
+ profile: UserProfile;
}
export async function getServerSideProps(context: GetServerSidePropsContext) {
const folderId = context.query.folderId || '';
- const response = await fetch({ url: `/users/1/links?folderId=${folderId}` });
+ const response = await apiRequest({
+ url: `/users/1/links?folderId=${folderId}`,
+ });
const cards = response.data;
- const response2 = await fetch({ url: '/users/1/folders' });
+ const response2 = await apiRequest({ url: '/users/1/folders' });
const folders = response2.data.data;
+ const response3 = await apiRequest({ url: '/users/1' });
+ const profile = response3.data.data[0];
+
return {
props: {
cards: cards,
folders: folders,
+ profile: profile,
},
};
}
-export default function Folder({ cards, folders }: Props) {
+const Folder = ({ cards, folders, profile }: Props) => {
const [searchKeyword, setSearchKeyword] = useState('');
const [isScrolled, setIsScrolled] = useState(false);
const ref = useRef(null);
@@ -54,11 +70,7 @@ export default function Folder({ cards, folders }: Props) {
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
- if (!entry.isIntersecting) {
- setIsScrolled(true);
- } else {
- setIsScrolled(false);
- }
+ setIsScrolled(!entry.isIntersecting);
},
{ threshold: 0 }
);
@@ -77,7 +89,7 @@ export default function Folder({ cards, folders }: Props) {
}, []);
return (
-
+
@@ -92,4 +104,6 @@ export default function Folder({ cards, folders }: Props) {
);
-}
+};
+
+export default Folder;
diff --git a/src/pages/index.style.ts b/src/pages/index.style.ts
new file mode 100644
index 000000000..a88dc1421
--- /dev/null
+++ b/src/pages/index.style.ts
@@ -0,0 +1,231 @@
+import { onMobile, onTablet } from '@styles/mediaQuery';
+import Link from 'next/link';
+import styled, { css } from 'styled-components';
+
+export const HeroHeader = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ row-gap: 4rem;
+ padding-top: 7rem;
+ background-color: #edf7ff;
+
+ ${onTablet} {
+ padding-top: 3.9rem;
+ padding-left: 3.2rem;
+ padding-right: 3.2rem;
+
+ br {
+ display: none;
+ }
+ }
+
+ ${onMobile} {
+ row-gap: 2.4rem;
+ padding-top: 2.8rem;
+ }
+`;
+
+export const HeroImage = styled.div`
+ width: 120rem;
+ height: 59rem;
+
+ ${onTablet} {
+ width: 69.8rem;
+ height: 34.3rem;
+ }
+
+ ${onMobile} {
+ width: 100%;
+ height: auto;
+ }
+`;
+
+export const Cta = styled(Link)`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 5.4rem;
+ cursor: pointer;
+ background-image: linear-gradient(135deg, #6d6afe 0%, #6ae3fe 100%);
+ border-radius: 0.8rem;
+ color: #f5f5f5;
+ font-size: 1.8rem;
+ font-weight: 600;
+ width: 35rem;
+
+ ${onMobile} {
+ height: 3.7rem;
+ font-size: 1.4rem;
+ width: 20rem;
+ }
+`;
+
+export const Slogan = styled.h1`
+ text-align: center;
+ font-size: 6.4rem;
+ font-weight: 700;
+ line-height: 8rem;
+
+ ${onMobile} {
+ font-size: 3.2rem;
+ line-height: 4.2rem;
+ }
+`;
+
+const BackgroundClipText = css`
+ background-clip: text;
+ -webkit-background-clip: text;
+ color: transparent;
+`;
+
+export const SloganGradient = styled.span`
+ background-image: linear-gradient(119deg, #6d6afe 0%, #ff9f9f 100%);
+ ${BackgroundClipText}
+`;
+
+export const Article = styled.article`
+ padding-top: 7rem;
+ padding-bottom: 12rem;
+
+ ${onTablet} {
+ padding-top: 3rem;
+ padding-bottom: 12rem;
+
+ br {
+ display: none;
+ }
+ }
+
+ ${onMobile} {
+ padding: 0;
+ }
+`;
+
+export const Section = styled.section`
+ display: grid;
+ justify-content: center;
+ column-gap: 15.7rem;
+ row-gap: 1rem;
+ width: 100%;
+ height: 55rem;
+ padding: 5rem 0;
+
+ &:nth-of-type(odd) {
+ grid-template:
+ '. image'
+ 'title image'
+ 'description image'
+ '. image'
+ /29.1rem 55rem;
+ }
+
+ &:nth-of-type(even) {
+ grid-template:
+ 'image .'
+ 'image title'
+ 'image description'
+ 'image .'
+ /55rem 29.1rem;
+ }
+
+ ${onTablet} {
+ height: 41.5rem;
+ column-gap: 5.1rem;
+
+ &:nth-of-type(odd) {
+ grid-template:
+ '. image'
+ 'title image'
+ 'description image'
+ '. image'
+ /26.2rem 38.5rem;
+ }
+
+ &:nth-of-type(even) {
+ grid-template:
+ 'image .'
+ 'image title'
+ 'image description'
+ 'image .'
+ /38.5rem 26.2rem;
+ }
+ }
+
+ ${onMobile} {
+ padding: 4rem 3.2rem;
+ height: auto;
+ row-gap: 0;
+
+ &:nth-of-type(odd),
+ &:nth-of-type(even) {
+ grid-template:
+ 'title'
+ 'image'
+ 'description';
+ }
+ }
+`;
+
+export const Title = styled.h2`
+ grid-area: title;
+ font-size: 4.8rem;
+ font-weight: 700;
+ line-height: 5.8rem;
+ letter-spacing: -0.03rem;
+
+ ${onMobile} {
+ font-size: 2.4rem;
+ line-height: 2.9rem;
+ margin-bottom: 2rem;
+ }
+`;
+
+export const Title1Gradient = styled.span`
+ background-image: linear-gradient(117deg, #fe8a8a 2.29%, #a4ceff 100%);
+ ${BackgroundClipText}
+`;
+
+export const Title2Gradient = styled.span`
+ background-image: linear-gradient(304deg, #6fbaff 0%, #ffd88b 100%);
+ ${BackgroundClipText}
+`;
+
+export const Title3Gradient = styled.span`
+ background-image: linear-gradient(133deg, #2945c7 0%, #dbe1f8 100%);
+ ${BackgroundClipText}
+`;
+
+export const Title4Gradient = styled.span`
+ background-image: linear-gradient(310deg, #fe578f 0%, #68e8f9 100%);
+ ${BackgroundClipText}
+`;
+
+export const Description = styled.p`
+ grid-area: description;
+ font-size: 1.6rem;
+ font-weight: 500;
+ color: #6b6b6b;
+ line-height: 150%;
+
+ ${onMobile} {
+ margin-top: 1.6rem;
+ }
+`;
+
+export const ContentImage = styled.div`
+ position: relative;
+ grid-area: image;
+ width: 55rem;
+ height: 45rem;
+
+ ${onTablet} {
+ width: 38.5rem;
+ height: 31.5rem;
+ }
+
+ ${onMobile} {
+ width: 100%;
+ height: auto;
+ }
+`;
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index ce1a5dc1a..4bce3f426 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -1,9 +1,109 @@
-import Layout from '@components/Layout/Layout';
+import Layout from '@components/Layout';
+import * as S from './index.style';
+import Image from 'next/image';
-export default function Home() {
+const Home = () => {
return (
- <>
-
- >
+
+
+
+ 세상의 모든 정보를
+ 쉽게 저장하고 관리해 보세요
+
+
+ 링크 추가하기
+
+
+
+
+
+
+
+
+ 원하는 링크
+ 를
저장하세요
+
+
+ 나중에 읽고 싶은 글, 다시 보고 싶은 영상,
+ 사고 싶은 옷, 기억하고 싶은 모든 것을
한 공간에 저장하세요.
+
+
+
+
+
+
+
+ 링크를 폴더로
+ 관리
+ 하세요
+
+
+ 나만의 폴더를 무제한으로 만들고
+ 다양하게 활용할 수 있습니다.
+
+
+
+
+
+
+
+ 저장한 링크를
+ 공유해 보세요
+
+
+ 여러 링크를 폴더에 담고 공유할 수 있습니다.
+ 가족, 친구, 동료들에게 쉽고 빠르게 링크를
+ 공유해 보세요.
+
+
+
+
+
+
+
+ 저장한 링크를
+ 검색해 보세요
+
+
+ 중요한 정보들을 검색으로 쉽게 찾아보세요.
+
+
+
+
+
+
+
);
-}
+};
+
+export default Home;
diff --git a/src/pages/shared.tsx b/src/pages/shared.tsx
index 501a3ed7a..54ada348e 100644
--- a/src/pages/shared.tsx
+++ b/src/pages/shared.tsx
@@ -3,7 +3,7 @@ import SearchBar from '@components/SearchBar';
import SampleCardList from '@components/SampleCardList';
import { MainDiv } from '@styles/MainDiv';
import { useState } from 'react';
-import fetch from '@api/fetch';
+import apiRequest from '@api/apiRequest';
import Layout from '@components/Layout/Layout';
export interface Folder {
@@ -29,26 +29,38 @@ export interface Link {
imageSource?: string;
}
+export interface SampleUserProfile {
+ id: number;
+ name: string;
+ email: string;
+ profileImageSource: string;
+}
+
interface Props {
folder: Folder;
+ profile: SampleUserProfile;
}
export async function getStaticProps() {
- const response = await fetch({ url: '/sample/folder' });
- const folder = response.data?.folder;
+ const response1 = await apiRequest({ url: '/sample/folder' });
+ const folder = response1.data?.folder;
+
+ const response2 = await apiRequest({ url: '/sample/user' });
+ const profile = response2.data;
return {
props: {
folder: folder,
+ profile: profile,
},
};
}
-export default function Shared({ folder }: Props) {
+const Shared = ({ folder, profile }: Props) => {
const [searchKeyword, setSearchKeyword] = useState('');
return (
-
+
@@ -56,4 +68,6 @@ export default function Shared({ folder }: Props) {
);
-}
+};
+
+export default Shared;
diff --git a/src/pages/signin.tsx b/src/pages/signin.tsx
new file mode 100644
index 000000000..c8bb10e38
--- /dev/null
+++ b/src/pages/signin.tsx
@@ -0,0 +1,24 @@
+import SignPageLayout from '@components/SignPageLayout';
+import SigninForm from '@components/SigninForm';
+import { useRouter } from 'next/router';
+import { useEffect } from 'react';
+
+const Signin = () => {
+ const router = useRouter();
+
+ useEffect(() => {
+ const accessToken = localStorage.getItem('accessToken');
+
+ if (accessToken) {
+ router.push('/folder');
+ }
+ }, []);
+
+ return (
+
+
+
+ );
+};
+
+export default Signin;
diff --git a/src/pages/signup.tsx b/src/pages/signup.tsx
new file mode 100644
index 000000000..fdc51a50f
--- /dev/null
+++ b/src/pages/signup.tsx
@@ -0,0 +1,24 @@
+import SignPageLayout from '@components/SignPageLayout';
+import SignupForm from '@components/SignupForm';
+import { useRouter } from 'next/router';
+import { useEffect } from 'react';
+
+const Signup = () => {
+ const router = useRouter();
+
+ useEffect(() => {
+ const accessToken = localStorage.getItem('accessToken');
+
+ if (accessToken) {
+ router.push('/folder');
+ }
+ }, []);
+
+ return (
+
+
+
+ );
+};
+
+export default Signup;
diff --git a/src/styles/mediaQuery.ts b/src/styles/mediaQuery.ts
index ec7a3f508..8cc9168e4 100644
--- a/src/styles/mediaQuery.ts
+++ b/src/styles/mediaQuery.ts
@@ -1,2 +1,2 @@
-export const onTablet = '@media screen and (max-width: 1124px)';
+export const onTablet = '@media screen and (max-width: 1199px)';
export const onMobile = '@media screen and (max-width: 767px)';
diff --git a/src/utils/regex.ts b/src/utils/regex.ts
new file mode 100644
index 000000000..2dde0d8e4
--- /dev/null
+++ b/src/utils/regex.ts
@@ -0,0 +1,2 @@
+export const EMAIL_REGEX = /^[\w-]+(\.[\w-]+)*@([\w-]+\.)+[a-zA-Z]{2,7}$/;
+export const PASSWORD_REGEX = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/;