diff --git a/api/config.js b/api/config.js
new file mode 100644
index 000000000..5614c886c
--- /dev/null
+++ b/api/config.js
@@ -0,0 +1 @@
+export const BASE_URL = "https://bootcamp-api.codeit.kr/api";
diff --git a/components/EmailInput.tsx b/components/EmailInput.tsx
index fad90f919..8461ff54d 100644
--- a/components/EmailInput.tsx
+++ b/components/EmailInput.tsx
@@ -1,8 +1,8 @@
-// components/EmailInput.tsx
-
import React, { useState, useEffect } from "react";
import classNames from "classnames/bind";
-import styles from "@/styles/signin.module.scss";
+import styles from "@/styles/sign-in.module.scss";
+
+import { isEmailValid } from "@/utils/util";
interface EmailInputProps {
value: string;
@@ -10,13 +10,11 @@ interface EmailInputProps {
error?: string;
}
-export const emailCheck = (email: string) => {
- const emailForm =
- /^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*\.[a-zA-Z]{2,3}$/i;
- return emailForm.test(email);
-};
-
-export function EmailInput({ value, onChange, error }: EmailInputProps) {
+export default function EmailInput({
+ value,
+ onChange,
+ error,
+}: EmailInputProps) {
const cx = classNames.bind(styles);
const [isFocused, setIsFocused] = useState(false);
const [emailErrorText, setEmailErrorText] = useState("");
@@ -33,7 +31,7 @@ export function EmailInput({ value, onChange, error }: EmailInputProps) {
const handleBlur = () => {
setIsFocused(false);
if (value) {
- if (!emailCheck(value)) {
+ if (!isEmailValid(value)) {
setEmailErrorText("올바른 이메일 주소가 아닙니다.");
} else {
setEmailErrorText("");
@@ -51,9 +49,7 @@ export function EmailInput({ value, onChange, error }: EmailInputProps) {
return (
-
+
+
+
+
+ 추가하기
+
+
+ );
+}
+
+export default AddLinkBar;
diff --git a/components/Folder/Button.tsx b/components/Folder/Button.tsx
new file mode 100644
index 000000000..2d218caa3
--- /dev/null
+++ b/components/Folder/Button.tsx
@@ -0,0 +1,71 @@
+import { useState } from "react";
+import styled from "styled-components";
+
+interface FolderData {
+ id: number;
+ name: string;
+}
+
+interface StyledFolderButtonProps {
+ active: boolean;
+}
+
+const StyledFolderButton = styled.button
`
+ background-color: #ffffff;
+ width: auto;
+ height: 36px;
+ border-radius: 5px;
+ border: 1px solid #6d6afe;
+ font-size: 16px;
+ font-weight: 400;
+ margin-top: 6px;
+ margin-right: 10px;
+ text-align: center;
+
+ &:hover {
+ cursor: pointer;
+ }
+
+ ${({ active }) =>
+ active &&
+ `
+ background-color: #6D6AFE;
+ color: #ffffff;
+ `}
+`;
+
+interface ButtonProps {
+ folderData: FolderData[];
+ selectedFolderId: number | null;
+ onFolderClick: (buttonId: number | null) => void;
+}
+
+function Button({ folderData, selectedFolderId, onFolderClick }: ButtonProps) {
+ const [activeButton, setActiveButton] = useState(null);
+
+ const handleButtonClick = (buttonId: number | null) => {
+ setActiveButton(buttonId);
+ onFolderClick(buttonId);
+ };
+
+ return (
+ <>
+ handleButtonClick(null)}
+ active={activeButton === null}
+ >
+ 전체
+
+ {folderData.map((data) => (
+ handleButtonClick(data.id)}
+ active={activeButton === data.id}
+ >
+ {data.name}
+
+ ))}
+ >
+ );
+}
+export default Button;
diff --git a/components/Folder/Card.tsx b/components/Folder/Card.tsx
new file mode 100644
index 000000000..2c643a4ae
--- /dev/null
+++ b/components/Folder/Card.tsx
@@ -0,0 +1,96 @@
+import styled from "styled-components";
+import { formatDate, generateTimeText } from "@/utils/util";
+import Link from "next/link";
+
+const StyledCard = styled.div`
+ max-width: 340px;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px;
+ border-radius: 10px;
+
+ @media (max-width: 1124px) {
+ max-width: 340px;
+ }
+`;
+
+const StyledLink = styled(Link)`
+ display: block;
+ width: 100%;
+`;
+
+const CardImg = styled.img`
+ width: 100%;
+ height: 200px;
+ object-fit: cover;
+ border-top-left-radius: 10px;
+ border-top-right-radius: 10px;
+`;
+
+const CardTextSection = styled.section`
+ width: 100%;
+ height: 136px;
+ padding: 10px 20px;
+ display: grid;
+ grid-template-rows: 1fr 2fr 1fr;
+ grid-template-columns: 1fr;
+ grid-template-areas:
+ "time"
+ "description"
+ "date";
+`;
+
+const CardDescription = styled.p`
+ margin: 0;
+
+ width: 100%;
+ height: 50px;
+ display: -webkit-box;
+ -webkit-line-clamp: 3;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ grid-area: description;
+`;
+
+const TimeText = styled.p`
+ margin: 0;
+ grid-area: time;
+`;
+
+const DateText = styled.p`
+ margin: 0;
+
+ grid-area: date;
+`;
+export interface LinkData {
+ id: number;
+ created_at: string;
+ updated_at: string;
+ url: string;
+ title: string;
+ description: string;
+ image_source: string;
+ folder_id: number;
+}
+
+function Card({ linkData }: { linkData: LinkData }) {
+ return (
+
+
+
+
+
+ {generateTimeText(linkData.created_at)}
+ {linkData.description}
+ {formatDate(linkData.created_at)}
+
+
+ );
+}
+
+export default Card;
diff --git a/components/Folder/CardList.tsx b/components/Folder/CardList.tsx
new file mode 100644
index 000000000..d50f8d6bd
--- /dev/null
+++ b/components/Folder/CardList.tsx
@@ -0,0 +1,72 @@
+import { useEffect, useState } from "react";
+import axios from "axios";
+import Card, { LinkData } from "./Card";
+import { BASE_URL } from "@/api/config";
+import styled from "styled-components";
+
+const StyledCardContainer = styled.div`
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ column-gap: 20px;
+ row-gap: 25px;
+ margin: 0 auto;
+
+ @media (max-width: 1124px) {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ @media (max-width: 767px) {
+ grid-template-columns: repeat(1, 1fr);
+ }
+`;
+
+interface CardListProps {
+ selectedFolderId: number | null;
+}
+
+function CardList({ selectedFolderId }: CardListProps) {
+ const [linkData, setLinkData] = useState([]);
+
+ useEffect(() => {
+ const fetchLinkData = async () => {
+ const accessToken = localStorage.getItem("accessToken");
+
+ if (accessToken) {
+ try {
+ let url = `${BASE_URL}/links`;
+ if (selectedFolderId !== null) {
+ url += `?folderId=${selectedFolderId}`;
+ console.log("Id :", selectedFolderId);
+ }
+ const response = await axios.get(url, {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ },
+ });
+ if (selectedFolderId === null) {
+ setLinkData(response.data.data.folder);
+ } else {
+ setLinkData(response.data.data);
+ }
+ console.log("response : ", response.data.data);
+ } catch (error) {
+ console.error("에러 메세지 : ", error);
+ }
+ }
+ };
+
+ fetchLinkData();
+ }, [selectedFolderId]);
+
+ return (
+
+ {linkData.length > 0 ? (
+ linkData.map((data) => )
+ ) : (
+ 저장된 링크가 없습니다.
+ )}
+
+ );
+}
+
+export default CardList;
diff --git a/components/Folder/FolderList.tsx b/components/Folder/FolderList.tsx
new file mode 100644
index 000000000..a1b9ca019
--- /dev/null
+++ b/components/Folder/FolderList.tsx
@@ -0,0 +1,79 @@
+import { useEffect, useState } from "react";
+import axios from "axios";
+import styled from "styled-components";
+
+import { BASE_URL } from "@/api/config";
+import Button from "./Button";
+
+const ActiveFolderName = styled.p`
+ text-align: left;
+ width: 100%;
+ position: relative;
+ left: 50px;
+ font-size: 24px;
+ font-weight: 700;
+`;
+
+interface FolderListProps {
+ selectedFolderId: number | null;
+ onFolderClick: (folderId: number | null) => void;
+}
+interface FolderData {
+ id: number;
+ created_at: string;
+ name: string;
+ user_id: number;
+ favorite: boolean;
+ link: {
+ count: number;
+ };
+}
+
+function FolderList({ selectedFolderId, onFolderClick }: FolderListProps) {
+ const [folderData, setFolderData] = useState([]);
+
+ useEffect(() => {
+ const fetchFolderData = async () => {
+ const accessToken = localStorage.getItem("accessToken");
+
+ if (accessToken) {
+ try {
+ const response = await axios.get(`${BASE_URL}/folders`, {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ },
+ });
+ setFolderData(response.data.data.folder);
+ } catch (error) {
+ console.error("folderList 에러 : ", error);
+ }
+ }
+ };
+
+ fetchFolderData();
+ }, []);
+
+ return (
+
+ {folderData.length > 0 ? (
+ <>
+
+
+ {selectedFolderId
+ ? folderData.find((folder) => folder.id === selectedFolderId)
+ ?.name
+ : "전체"}
+
+ >
+ ) : (
+
저장된 폴더가 없습니다.
+ )}
+
+ );
+}
+
+export default FolderList;
diff --git a/components/Folder/Header.tsx b/components/Folder/Header.tsx
new file mode 100644
index 000000000..32037f6b8
--- /dev/null
+++ b/components/Folder/Header.tsx
@@ -0,0 +1,21 @@
+import styled from "styled-components";
+
+import Nav from "../Header-Nav";
+import AddLinkBar from "./AddLinkBar";
+
+const StyledHeader = styled.div`
+ background-color: #f0f6ff;
+ width: 100%;
+ height: 300px;
+`;
+
+function FolderHeader() {
+ return (
+
+
+
+
+ );
+}
+
+export default FolderHeader;
diff --git a/components/Folder/Main.tsx b/components/Folder/Main.tsx
new file mode 100644
index 000000000..de998dcc7
--- /dev/null
+++ b/components/Folder/Main.tsx
@@ -0,0 +1,42 @@
+import { useState, useEffect } from "react";
+import styled from "styled-components";
+import SearchBar from "../SearchBar";
+import FolderList from "./FolderList";
+import LinkList from "./CardList";
+
+const MainContainer = styled.main`
+ width: 100%;
+ height: auto;
+ padding: 20px 60px;
+ display: flex;
+ flex-direction: column;
+ gap: 40px;
+ align-items: center;
+`;
+
+interface MainProps {
+ folderId?: string;
+}
+
+function Main({ folderId }: MainProps) {
+ const [selectedFolderId, setSelectedFolderId] = useState(
+ folderId ? parseInt(folderId, 10) : null
+ );
+
+ useEffect(() => {
+ setSelectedFolderId(folderId ? parseInt(folderId, 10) : null);
+ }, [folderId]);
+
+ return (
+
+
+
+
+
+ );
+}
+
+export default Main;
diff --git a/components/Footer.tsx b/components/Footer.tsx
new file mode 100644
index 000000000..7151d17b6
--- /dev/null
+++ b/components/Footer.tsx
@@ -0,0 +1,65 @@
+import styled from "styled-components";
+import Link from "next/link";
+
+const FooterContainer = styled.footer`
+ background-color: #111322;
+ color: rgba(207, 207, 207, 1);
+ width: 100%;
+ height: 160px;
+ padding: 32px 104px 64px;
+`;
+const FooterContent = styled.div`
+ display: flex;
+ justify-content: space-between;
+ white-space: nowrap;
+`;
+const FooterYear = styled.div`
+ color: rgba(103, 103, 103, 1);
+`;
+
+const StyledLink = styled(Link)`
+ color: rgba(103, 103, 103, 1);
+`;
+
+const FooterLink = styled.div`
+ display: flex;
+ gap: 30px;
+`;
+
+const FooterSocial = styled.div`
+ display: flex;
+ gap: 10px;
+`;
+const SocialIcon = styled.img`
+ width: 20px;
+ height: 20px;
+`;
+function Footer() {
+ return (
+
+
+ ©codeit - 2023
+
+ Privacy Policy
+ FAQ
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default Footer;
diff --git a/components/Header-Nav.tsx b/components/Header-Nav.tsx
new file mode 100644
index 000000000..20bff82fa
--- /dev/null
+++ b/components/Header-Nav.tsx
@@ -0,0 +1,109 @@
+import Link from "next/link";
+import styled from "styled-components";
+import { BASE_URL } from "@/api/config";
+import Image from "next/image";
+import { useEffect, useState } from "react";
+import axios from "axios";
+
+interface StyledNavBarProps {
+ position?: string;
+}
+
+const StyledNavBar = styled.div`
+ background-color: #f0f6ff;
+ display: flex;
+ width: 100%;
+ height: 92px;
+ align-items: center;
+ justify-content: space-between;
+ padding: 32px 200px;
+ gap: 8px;
+ position: ${(props) => props.position || "static"};
+ z-index: 1;
+ @media (max-width: 1124px) {
+ width: 100%;
+ padding: 32px;
+ }
+ @media (max-width: 767px) {
+ width: 100%;
+ height: 63px;
+ padding: 18px 32px;
+ }
+`;
+
+const StyledUserProfile = styled.div`
+ display: flex;
+ gap: 6px;
+ align-items: center;
+`;
+
+const StyledUserProfileImg = styled.img`
+ border-radius: 50%;
+ width: 28px;
+ height: 28px;
+`;
+
+interface UserProfile {
+ id: number;
+ name: string;
+ email: string;
+ image_source: string;
+}
+
+interface HeaderNavProps {
+ position?: string;
+}
+
+function HeaderNav({ position = "static" }: HeaderNavProps) {
+ const [userProfile, setUserProfile] = useState(null);
+
+ useEffect(() => {
+ const fetchUserProfile = async () => {
+ const accessToken = localStorage.getItem("accessToken");
+
+ if (accessToken) {
+ try {
+ const response = await axios.get(`${BASE_URL}/users`, {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ },
+ });
+ setUserProfile(response.data.data[0]);
+ } catch (error) {
+ console.error("유저 정보 에러 :", error);
+ }
+ }
+ };
+ fetchUserProfile();
+ }, []);
+
+ return (
+ <>
+
+
+
+
+ {userProfile ? (
+
+
+ {userProfile.email}
+
+ ) : (
+
+ )}
+
+ >
+ );
+}
+
+export default HeaderNav;
diff --git a/components/PasswordInput.tsx b/components/PasswordInput.tsx
index 68027018f..13a1df7a8 100644
--- a/components/PasswordInput.tsx
+++ b/components/PasswordInput.tsx
@@ -1,6 +1,6 @@
import React, { useState } from "react";
import classNames from "classnames/bind";
-import styles from "@/styles/signin.module.scss";
+import styles from "@/styles/sign-in.module.scss";
interface PasswordInputProps {
value: string;
@@ -10,7 +10,7 @@ interface PasswordInputProps {
id?: string;
}
-export function PasswordInput({
+export default function PasswordInput({
value,
onChange,
onBlur,
@@ -61,7 +61,7 @@ export function PasswordInput({
/>
{error || errorText}
diff --git a/components/SearchBar.tsx b/components/SearchBar.tsx
new file mode 100644
index 000000000..30ff28f88
--- /dev/null
+++ b/components/SearchBar.tsx
@@ -0,0 +1,42 @@
+import styled from "styled-components";
+
+const SearchBarSection = styled.div`
+ position: relative;
+ width: 1060px;
+ margin: 0 auto;
+
+ @media (max-width: 1124px) {
+ width: 704px;
+ }
+ @media (max-width: 767px) {
+ width: 325px;
+ }
+`;
+
+const SearchInput = styled.input`
+ width: 100%;
+ height: 54px;
+ border: none;
+ border-radius: 10px;
+ padding: 15px 24px;
+ display: flex;
+ justify-content: center;
+ background-color: #f5f5f5;
+`;
+
+const SearchInputIcon = styled(SearchInput)`
+ background-image: url("/icon/search.svg");
+ background-position: 5px 50%;
+ background-repeat: no-repeat;
+ background-size: 16px;
+`;
+
+const SearchBar = () => {
+ return (
+
+
+
+ );
+};
+
+export default SearchBar;
diff --git a/components/Shared/Card.tsx b/components/Shared/Card.tsx
new file mode 100644
index 000000000..01580e3b1
--- /dev/null
+++ b/components/Shared/Card.tsx
@@ -0,0 +1,137 @@
+import styled from "styled-components";
+import Link from "next/link";
+
+import { BASE_URL } from "@/api/config";
+import { useFetch } from "@/utils/useFetch";
+import { formatDate, generateTimeText } from "@/utils/util";
+import { useEffect, useState } from "react";
+import axios from "axios";
+
+const StyledCardContainer = styled.article`
+ display: grid;
+ grid-template-columns: repeat(3, 340px);
+ grid-template-rows: 334px;
+ column-gap: 20px;
+ row-gap: 25px;
+ margin: 0 auto;
+ @media (max-width: 1124px) {
+ grid-template-columns: repeat(2, 1fr);
+ }
+ @media (max-width: 767px) {
+ grid-template-columns: repeat(1, 1fr);
+ }
+`;
+
+const StyledCardSection = styled.section`
+ width: 340px;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px;
+ border-radius: 10px;
+`;
+
+const StyledCardImg = styled.img`
+ width: 340px;
+ height: 200px;
+ object-fit: cover;
+ border-top-left-radius: 10px;
+ border-top-right-radius: 10px;
+ &:hover {
+ transform: scale(1.3);
+ transition: transform 0.3s ease;
+ }
+`;
+
+const StyledCardTextSection = styled.section`
+ width: 300px;
+ height: 136px;
+ padding: 15px 20px;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+`;
+
+const StyledCardCreatedAt = styled.div`
+ color: #666666;
+ font-size: 13px;
+ height: 17px;
+`;
+
+const StyledCardTextBody = styled.div`
+ font-size: 16px;
+ font-weight: 400;
+ overflow: hidden;
+ height: 49px;
+`;
+
+const StyledCardDate = styled.div`
+ font-size: 14px;
+`;
+
+interface LinkData {
+ id: number;
+ url: string;
+ title: string;
+ description: string;
+ image_source: string;
+ created_at: string;
+}
+
+interface CardProps {
+ folderId: string;
+ userId: number;
+}
+
+function Card({ folderId, userId }: CardProps) {
+ const [linkData, setLinkData] = useState([]);
+
+ useEffect(() => {
+ const fetchLinkData = async () => {
+ try {
+ const accessToken = localStorage.getItem("accessToken");
+ if (accessToken) {
+ const response = await axios.get(
+ `${BASE_URL}/users/${userId}/links?folderId=${folderId}`,
+ {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ },
+ }
+ );
+ setLinkData(response.data.data);
+ }
+ } catch (error) {
+ console.error("링크 데이터 가져오기 에러:", error);
+ }
+ };
+
+ fetchLinkData();
+ }, [folderId, userId]);
+
+ return (
+
+ {linkData.map((link) => (
+
+
+
+
+
+
+
+ {generateTimeText(link.created_at)}
+
+
+ {link.description}
+ {formatDate(link.created_at)}
+
+
+ ))}
+
+ );
+}
+
+export default Card;
diff --git a/components/Shared/Header-Content.tsx b/components/Shared/Header-Content.tsx
new file mode 100644
index 000000000..56cf4a203
--- /dev/null
+++ b/components/Shared/Header-Content.tsx
@@ -0,0 +1,109 @@
+import styled from "styled-components";
+import { useFetch } from "@/utils/useFetch";
+import { BASE_URL } from "@/api/config";
+import { useEffect, useState } from "react";
+import axios from "axios";
+
+const StyledFolderInfoContent = styled.div`
+ width: 100%;
+ height: 100%;
+ padding: 60px 200px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+`;
+
+const StyledFolderInfo = styled.div`
+ width: 188px;
+ height: 164px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ gap: 20px;
+`;
+
+const StyledFolderImg = styled.img`
+ width: 60px;
+ height: 60px;
+ border-radius: 50%;
+ object-fit: cover;
+`;
+
+const StyledFolderOwnerName = styled.p`
+ font-size: 16px;
+ font-weight: 400;
+`;
+
+const StyledFolderName = styled.p`
+ width: auto;
+ height: 48px;
+ font-weight: 600;
+ font-size: 32px;
+ line-height: 36px;
+ text-align: center;
+`;
+
+interface FolderData {
+ id: number;
+ name: string;
+ user_id: number;
+}
+
+interface FolderOwner {
+ id: number;
+ name: string;
+ image_source: string;
+}
+
+interface HeaderContentProps {
+ folderId: string;
+}
+
+function HeaderContent({ folderId }: HeaderContentProps) {
+ const [folderData, setFolderData] = useState(null);
+ const [folderOwner, setFolderOwner] = useState(null);
+
+ useEffect(() => {
+ const fetchFolderData = async () => {
+ try {
+ const response = await axios.get(`${BASE_URL}/folders/${folderId}`);
+ setFolderData(response.data.data);
+ } catch (error) {
+ console.error("폴더 정보 가져오기 에러:", error);
+ }
+ };
+
+ const fetchFolderOwner = async () => {
+ if (folderData) {
+ try {
+ const response = await axios.get(
+ `${BASE_URL}/users/${folderData.user_id}`
+ );
+ setFolderOwner(response.data.data);
+ } catch (error) {
+ console.error("폴더 소유자 정보 가져오기 에러:", error);
+ }
+ }
+ };
+
+ fetchFolderData();
+ if (folderData) {
+ fetchFolderOwner();
+ }
+ }, [folderId, folderData]);
+
+ return (
+
+ {folderData && folderOwner && (
+
+
+ {folderData.name}
+
+ )}
+
+ );
+}
+
+export default HeaderContent;
diff --git a/components/Shared/Main.tsx b/components/Shared/Main.tsx
new file mode 100644
index 000000000..db4e938e0
--- /dev/null
+++ b/components/Shared/Main.tsx
@@ -0,0 +1,30 @@
+import styled from "styled-components";
+import Card from "@/components/Shared/Card";
+import SearchBar from "@/components/SearchBar";
+
+const MainContainer = styled.main`
+ width: 100%;
+ height: auto;
+ padding: 20px 60px;
+ display: flex;
+ flex-direction: column;
+ gap: 40px;
+ align-items: center;
+ justify-content: center;
+`;
+interface MainProps {
+ folderId: string;
+}
+
+function Main({ folderId }: MainProps) {
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
+
+export default Main;
diff --git a/components/Shared/SharedHeader.tsx b/components/Shared/SharedHeader.tsx
new file mode 100644
index 000000000..c741b2751
--- /dev/null
+++ b/components/Shared/SharedHeader.tsx
@@ -0,0 +1,25 @@
+import styled from "styled-components";
+import Nav from "../Header-Nav";
+import Content from "./Header-Content";
+
+const StyledHeaderContainer = styled.div`
+ background-color: #f0f6ff;
+ width: 100%;
+ height: 336px;
+ position: relative;
+`;
+
+interface HeaderProps {
+ folderId: string;
+}
+
+function Header({ folderId }: HeaderProps) {
+ return (
+
+
+
+
+ );
+}
+
+export default Header;
diff --git a/components/SignInForm.tsx b/components/SignInForm.tsx
index a67a79674..7054dc5ef 100644
--- a/components/SignInForm.tsx
+++ b/components/SignInForm.tsx
@@ -1,29 +1,34 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
import axios from "axios";
-import { EmailInput } from "./EmailInput";
-import { PasswordInput } from "./PasswordInput";
-import styles from "@/styles/signin.module.scss";
import classNames from "classnames/bind";
+import EmailInput from "./EmailInput";
+import PasswordInput from "./PasswordInput";
+import { BASE_URL } from "@/api/config";
+import styles from "@/styles/sign-in.module.scss";
+
export function SignIn() {
const cx = classNames.bind(styles);
+ const router = useRouter();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [emailError, setEmailError] = useState("");
const [passwordError, setPasswordError] = useState("");
- const router = useRouter();
+
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setEmailError("");
setPasswordError("");
- const data = { email, password };
+
+ const body = { email, password };
+
try {
- const response = await axios.post(
- "https://bootcamp-api.codeit.kr/api/sign-in",
- data
- );
+ const response = await axios.post(`${BASE_URL}/sign-in`, body);
if (response.status === 200) {
+ const { accessToken } = response.data.data;
+ localStorage.setItem("accessToken", accessToken);
+ console.log("토큰 :", accessToken);
router.push("/folder");
}
} catch (error: any) {
@@ -35,6 +40,7 @@ export function SignIn() {
}
}
};
+
return (
diff --git a/components/SignUpForm.tsx b/components/SignUpForm.tsx
index 7cce3edfb..dbd1678c7 100644
--- a/components/SignUpForm.tsx
+++ b/components/SignUpForm.tsx
@@ -1,11 +1,14 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
import axios from "axios";
-import { EmailInput, emailCheck } from "./EmailInput";
-import { SignUpPassword, isValidPassword } from "./SignUpPassword";
-import styles from "@/styles/signup.module.scss";
import classNames from "classnames/bind";
+import EmailInput from "./EmailInput";
+import SignUpPassword from "./SignUpPassword";
+import { isEmailValid, isPasswordValid } from "@/utils/util";
+import { BASE_URL } from "@/api/config";
+import styles from "@/styles/sign-up.module.scss";
+
export function SignUp() {
const cx = classNames.bind(styles);
const [email, setEmail] = useState("");
@@ -16,41 +19,46 @@ export function SignUp() {
const signUp = async (e: React.FormEvent) => {
e.preventDefault();
- const emailChecked = emailCheck(email);
- const validPassword = isValidPassword(password);
+ const validateEmail = isEmailValid(email);
+ const validatePassword = isPasswordValid(password);
const passwordMatch = password === confirmPassword;
- if (emailChecked && validPassword && passwordMatch) {
- const checkEmailUrl = "https://bootcamp-api.codeit.kr/api/check-email";
+ if (validateEmail && validatePassword && passwordMatch) {
+ const checkEmailUrl = `${BASE_URL}/check-email`;
const checkEmailData = { email };
+
try {
const checkEmailResponse = await axios.post(
checkEmailUrl,
checkEmailData
);
- if (checkEmailResponse.status === 409) {
- setEmailError("이미 사용 중인 이메일입니다.");
- console.log("hello~");
- return;
- } else {
+ if (checkEmailResponse.status === 200) {
setEmailError("");
+ const signUpUrl = `${BASE_URL}/sign-up`;
+ const signUpData = { email, password };
+ try {
+ const signUpResponse = await axios.post(signUpUrl, signUpData);
+ if (signUpResponse.status === 200) {
+ const { accessToken } = signUpResponse.data;
+ localStorage.setItem("accessToken", accessToken);
+ router.push("/folder");
+ }
+ } catch (error) {
+ console.log("Error:", error);
+ }
}
-
- const signUpUrl = "https://bootcamp-api.codeit.kr/api/sign-up";
- const signUpData = { email, password };
- const signUpResponse = await axios.post(signUpUrl, signUpData);
- if (signUpResponse.status === 200) {
- router.push("/folder");
+ } catch (error: any) {
+ if (error.response && error.response.status === 409) {
+ setEmailError("이미 사용 중인 이메일입니다.");
+ } else {
+ console.log("Error:", error);
}
- } catch (error) {
- console.log("Error:", error);
- console.log("hello2");
}
}
};
return (
-
diff --git a/components/SignUpPassword.tsx b/components/SignUpPassword.tsx
index 359e344c9..fe3876364 100644
--- a/components/SignUpPassword.tsx
+++ b/components/SignUpPassword.tsx
@@ -1,5 +1,6 @@
import React, { useState } from "react";
-import { PasswordInput } from "./PasswordInput";
+import PasswordInput from "./PasswordInput";
+import { isPasswordValid } from "@/utils/util";
interface SignUpPasswordProps {
password: string;
@@ -9,12 +10,7 @@ interface SignUpPasswordProps {
onConfirmPasswordChange: (value: string) => void;
}
-export const isValidPassword = (password: string) => {
- const passwordForm = /(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/;
- return passwordForm.test(password);
-};
-
-export function SignUpPassword({
+export default function SignUpPassword({
password,
confirmPassword,
onPasswordChange,
@@ -24,7 +20,7 @@ export function SignUpPassword({
const [confirmPasswordError, setConfirmPasswordError] = useState("");
const validatePassword = () => {
- const validPassword = isValidPassword(password);
+ const validPassword = isPasswordValid(password);
if (!validPassword) {
setPasswordError("비밀번호는 영문,숫자 조합 8자 이상 입력해 주세요.");
} else {
diff --git a/components/passwordToggle.js b/components/passwordToggle.js
deleted file mode 100644
index 822a624eb..000000000
--- a/components/passwordToggle.js
+++ /dev/null
@@ -1,16 +0,0 @@
-$(document).ready(function () {
- $(".password-section i").on("click", function () {
- $("input").toggleClass("active");
- if ($("input").hasClass("active")) {
- $(this)
- .attr("class", "fa fa-eye-slash fa-lg")
- .prev("input")
- .attr("type", "text");
- } else {
- $(this)
- .attr("class", "fa fa-eye fa-lg")
- .prev("input")
- .attr("type", "password");
- }
- });
-});
diff --git a/next.config.js b/next.config.js
index a843cbee0..34241fe45 100644
--- a/next.config.js
+++ b/next.config.js
@@ -1,6 +1,9 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
-}
+ images: {
+ domains: ["codeit-front.s3.ap-northeast-2.amazonaws.com"],
+ },
+};
-module.exports = nextConfig
+module.exports = nextConfig;
diff --git a/package-lock.json b/package-lock.json
index 60b529143..e10843d0a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,10 +10,12 @@
"dependencies": {
"axios": "^1.6.8",
"classnames": "^2.5.1",
+ "moment": "^2.30.1",
"next": "13.5.6",
"react": "^18",
"react-dom": "^18",
- "react-router-dom": "^6.23.1"
+ "react-router-dom": "^6.23.1",
+ "styled-components": "^6.1.11"
},
"devDependencies": {
"@types/node": "^20",
@@ -26,9 +28,9 @@
}
},
"node_modules/@babel/runtime": {
- "version": "7.24.5",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz",
- "integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==",
+ "version": "7.24.6",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.6.tgz",
+ "integrity": "sha512-Ja18XcETdEl5mzzACGd+DKgaGJzPTCow7EglgwTmHdwokzDFYh/MHua6lU6DV/hjF2IaOJ4oX2nqnjG7RElKOw==",
"dev": true,
"dependencies": {
"regenerator-runtime": "^0.14.0"
@@ -37,6 +39,24 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@emotion/is-prop-valid": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz",
+ "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==",
+ "dependencies": {
+ "@emotion/memoize": "^0.8.1"
+ }
+ },
+ "node_modules/@emotion/memoize": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz",
+ "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA=="
+ },
+ "node_modules/@emotion/unitless": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz",
+ "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ=="
+ },
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
@@ -354,9 +374,9 @@
"dev": true
},
"node_modules/@types/react": {
- "version": "18.3.2",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.2.tgz",
- "integrity": "sha512-Btgg89dAnqD4vV7R3hlwOxgqobUQKgx3MmrQRi0yYbs/P0ym8XozIAlkqVilPqHQwXs4e9Tf63rrCgl58BcO4w==",
+ "version": "18.3.3",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz",
+ "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==",
"dev": true,
"dependencies": {
"@types/prop-types": "*",
@@ -372,6 +392,11 @@
"@types/react": "*"
}
},
+ "node_modules/@types/stylis": {
+ "version": "4.2.5",
+ "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz",
+ "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw=="
+ },
"node_modules/@typescript-eslint/parser": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz",
@@ -798,9 +823,9 @@
}
},
"node_modules/axios": {
- "version": "1.6.8",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz",
- "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==",
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
+ "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
@@ -845,12 +870,12 @@
}
},
"node_modules/braces": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
- "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"devOptional": true,
"dependencies": {
- "fill-range": "^7.0.1"
+ "fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
@@ -895,10 +920,18 @@
"node": ">=6"
}
},
+ "node_modules/camelize": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
+ "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/caniuse-lite": {
- "version": "1.0.30001620",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001620.tgz",
- "integrity": "sha512-WJvYsOjd1/BYUY6SNGUosK9DUidBPDTnOARHp3fSmFO1ekdxaY6nKRttEVrfMmYi80ctS0kz1wiWmm14fVc3ew==",
+ "version": "1.0.30001624",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001624.tgz",
+ "integrity": "sha512-0dWnQG87UevOCPYaOR49CBcLBwoZLpws+k6W37nLjWUhumP1Isusj0p2u+3KhjNloRWK9OKMgjBBzPujQHw4nA==",
"funding": [
{
"type": "opencollective",
@@ -1025,11 +1058,28 @@
"node": ">= 8"
}
},
+ "node_modules/css-color-keywords": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
+ "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/css-to-react-native": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
+ "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==",
+ "dependencies": {
+ "camelize": "^1.0.0",
+ "css-color-keywords": "^1.0.0",
+ "postcss-value-parser": "^4.0.2"
+ }
+ },
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
- "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
- "dev": true
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
@@ -1619,29 +1669,29 @@
}
},
"node_modules/eslint-plugin-react": {
- "version": "7.34.1",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.1.tgz",
- "integrity": "sha512-N97CxlouPT1AHt8Jn0mhhN2RrADlUAsk1/atcT2KyA/l9Q/E6ll7OIGwNumFmWfZ9skV3XXccYS19h80rHtgkw==",
+ "version": "7.34.2",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.2.tgz",
+ "integrity": "sha512-2HCmrU+/JNigDN6tg55cRDKCQWicYAPB38JGSFDQt95jDm8rrvSUo7YPkOIm5l6ts1j1zCvysNcasvfTMQzUOw==",
"dev": true,
"dependencies": {
- "array-includes": "^3.1.7",
- "array.prototype.findlast": "^1.2.4",
+ "array-includes": "^3.1.8",
+ "array.prototype.findlast": "^1.2.5",
"array.prototype.flatmap": "^1.3.2",
"array.prototype.toreversed": "^1.1.2",
"array.prototype.tosorted": "^1.1.3",
"doctrine": "^2.1.0",
- "es-iterator-helpers": "^1.0.17",
+ "es-iterator-helpers": "^1.0.19",
"estraverse": "^5.3.0",
"jsx-ast-utils": "^2.4.1 || ^3.0.0",
"minimatch": "^3.1.2",
- "object.entries": "^1.1.7",
- "object.fromentries": "^2.0.7",
- "object.hasown": "^1.1.3",
- "object.values": "^1.1.7",
+ "object.entries": "^1.1.8",
+ "object.fromentries": "^2.0.8",
+ "object.hasown": "^1.1.4",
+ "object.values": "^1.2.0",
"prop-types": "^15.8.1",
"resolve": "^2.0.0-next.5",
"semver": "^6.3.1",
- "string.prototype.matchall": "^4.0.10"
+ "string.prototype.matchall": "^4.0.11"
},
"engines": {
"node": ">=4"
@@ -1855,9 +1905,9 @@
}
},
"node_modules/fill-range": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
- "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"devOptional": true,
"dependencies": {
"to-regex-range": "^5.0.1"
@@ -2051,6 +2101,7 @@
"version": "7.1.7",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
"integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true,
"dependencies": {
"fs.realpath": "^1.0.0",
@@ -2283,6 +2334,7 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
"dev": true,
"dependencies": {
"once": "^1.3.0",
@@ -2764,9 +2816,9 @@
}
},
"node_modules/language-subtag-registry": {
- "version": "0.3.22",
- "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz",
- "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==",
+ "version": "0.3.23",
+ "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz",
+ "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==",
"dev": true
},
"node_modules/language-tags": {
@@ -2836,12 +2888,12 @@
}
},
"node_modules/micromatch": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
- "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
+ "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
"dev": true,
"dependencies": {
- "braces": "^3.0.2",
+ "braces": "^3.0.3",
"picomatch": "^2.3.1"
},
"engines": {
@@ -2888,6 +2940,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/moment": {
+ "version": "2.30.1",
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
+ "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -3259,6 +3319,11 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
+ },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -3478,6 +3543,7 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "deprecated": "Rimraf versions prior to v4 are no longer supported",
"dev": true,
"dependencies": {
"glob": "^7.1.3"
@@ -3616,6 +3682,11 @@
"node": ">= 0.4"
}
},
+ "node_modules/shallowequal": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
+ "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -3788,6 +3859,60 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/styled-components": {
+ "version": "6.1.11",
+ "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.11.tgz",
+ "integrity": "sha512-Ui0jXPzbp1phYij90h12ksljKGqF8ncGx+pjrNPsSPhbUUjWT2tD1FwGo2LF6USCnbrsIhNngDfodhxbegfEOA==",
+ "dependencies": {
+ "@emotion/is-prop-valid": "1.2.2",
+ "@emotion/unitless": "0.8.1",
+ "@types/stylis": "4.2.5",
+ "css-to-react-native": "3.2.0",
+ "csstype": "3.1.3",
+ "postcss": "8.4.38",
+ "shallowequal": "1.1.0",
+ "stylis": "4.3.2",
+ "tslib": "2.6.2"
+ },
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/styled-components"
+ },
+ "peerDependencies": {
+ "react": ">= 16.8.0",
+ "react-dom": ">= 16.8.0"
+ }
+ },
+ "node_modules/styled-components/node_modules/postcss": {
+ "version": "8.4.38",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
+ "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "nanoid": "^3.3.7",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.2.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
"node_modules/styled-jsx": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz",
@@ -3810,6 +3935,11 @@
}
}
},
+ "node_modules/stylis": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz",
+ "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg=="
+ },
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
diff --git a/package.json b/package.json
index 44e80a0be..7e6f23a72 100644
--- a/package.json
+++ b/package.json
@@ -11,10 +11,12 @@
"dependencies": {
"axios": "^1.6.8",
"classnames": "^2.5.1",
+ "moment": "^2.30.1",
"next": "13.5.6",
"react": "^18",
"react-dom": "^18",
- "react-router-dom": "^6.23.1"
+ "react-router-dom": "^6.23.1",
+ "styled-components": "^6.1.11"
},
"devDependencies": {
"@types/node": "^20",
diff --git a/pages/_document.tsx b/pages/_document.tsx
index 057c8a57b..61ab02f99 100644
--- a/pages/_document.tsx
+++ b/pages/_document.tsx
@@ -1,13 +1,48 @@
-import { Html, Head, Main, NextScript } from "next/document";
-
-export default function Document() {
- return (
-
-
-
-
-
-
-
- );
+import Document, {
+ DocumentContext,
+ Head,
+ Html,
+ Main,
+ NextScript,
+} from "next/document";
+import { ServerStyleSheet } from "styled-components";
+
+export default class MyDocument extends Document {
+ static async getInitialProps(ctx: DocumentContext) {
+ const sheet = new ServerStyleSheet();
+ const originalRenderPage = ctx.renderPage;
+
+ try {
+ ctx.renderPage = () =>
+ originalRenderPage({
+ enhanceApp: (App) => (props) =>
+ sheet.collectStyles(),
+ });
+
+ const initialProps = await Document.getInitialProps(ctx);
+ return {
+ ...initialProps,
+ styles: (
+ <>
+ {initialProps.styles}
+ {sheet.getStyleElement()}
+ >
+ ),
+ };
+ } finally {
+ sheet.seal();
+ }
+ }
+
+ render() {
+ return (
+
+
+
+
+
+
+
+ );
+ }
}
diff --git a/pages/folder.tsx b/pages/folder.tsx
deleted file mode 100644
index 89b62cb6e..000000000
--- a/pages/folder.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function folder() {
- return Login Success
;
-}
diff --git a/pages/folder/[[...folderId]].tsx b/pages/folder/[[...folderId]].tsx
new file mode 100644
index 000000000..a55654655
--- /dev/null
+++ b/pages/folder/[[...folderId]].tsx
@@ -0,0 +1,32 @@
+import { useRouter } from "next/router";
+import { useEffect } from "react";
+import Header from "@/components/Folder/Header";
+import Main from "@/components/Folder/Main";
+import Footer from "@/components/Footer";
+import styled from "styled-components";
+
+const StyledFolder = styled.div`
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+`;
+
+export default function FolderPage() {
+ const router = useRouter();
+ const { folderId } = router.query;
+
+ useEffect(() => {
+ const accessToken = localStorage.getItem("accessToken");
+ if (!accessToken) {
+ router.push("/signin");
+ }
+ }, []);
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/pages/index.tsx b/pages/index.tsx
index fd85e591b..ecd7dafdf 100644
--- a/pages/index.tsx
+++ b/pages/index.tsx
@@ -1,7 +1,8 @@
import Image from "next/image";
import Link from "next/link";
import classNames from "classnames/bind";
-import styles from "@/styles/Home.module.scss";
+import styles from "@/styles/index.module.scss";
+import Footer from "@/components/Footer";
export default function Home() {
const cx = classNames.bind(styles);
@@ -35,17 +36,17 @@ export default function Home() {
관리해 보세요
-
@@ -80,7 +81,7 @@ export default function Home() {
width={326}
height={266}
className={cx("section-img")}
- src="/img/manage.png"
+ src="/img/manage.svg"
alt="manage"
/>
@@ -117,7 +118,7 @@ export default function Home() {
width={326}
height={266}
className={cx("section-img")}
- src="/img/share.png"
+ src="/img/share.svg"
alt="share"
/>
@@ -128,7 +129,7 @@ export default function Home() {
width={326}
height={266}
className={cx("section-img")}
- src="/img/search.png"
+ src="/img/search.svg"
alt="search"
/>
@@ -144,49 +145,7 @@ export default function Home() {
-
+
>
);
}
diff --git a/pages/shared/[foderId].tsx b/pages/shared/[foderId].tsx
new file mode 100644
index 000000000..3db24f882
--- /dev/null
+++ b/pages/shared/[foderId].tsx
@@ -0,0 +1,17 @@
+import Header from "@/components/Shared/SharedHeader";
+import Main from "@/components/Shared/Main";
+import Footer from "@/components/Footer";
+import { useRouter } from "next/router";
+
+export default function SharedPage() {
+ const router = useRouter();
+ const { folderId } = router.query;
+
+ return (
+ <>
+
+
+
+ >
+ );
+}
diff --git a/pages/signin.tsx b/pages/signin.tsx
index 0d5e07df2..b8385be34 100644
--- a/pages/signin.tsx
+++ b/pages/signin.tsx
@@ -1,11 +1,11 @@
import React from "react";
import Image from "next/image";
-import styles from "@/styles/signin.module.scss";
+import styles from "@/styles/sign-in.module.scss";
import classNames from "classnames/bind";
import { SignIn } from "@/components/SignInForm";
import Link from "next/link";
-export default function Signin() {
+export default function SignInPage() {
const cx = classNames.bind(styles);
return (
@@ -13,7 +13,7 @@ export default function Signin() {
-
+
@@ -38,7 +38,7 @@ export default function Signin() {
@@ -46,7 +46,7 @@ export default function Signin() {
diff --git a/pages/signup.tsx b/pages/signup.tsx
index b650f4ccc..26ba58d3c 100644
--- a/pages/signup.tsx
+++ b/pages/signup.tsx
@@ -2,7 +2,7 @@ import React from "react";
import Image from "next/image";
import Link from "next/link";
import { SignUp } from "@/components/SignUpForm";
-import styles from "@/styles/signup.module.scss";
+import styles from "@/styles/sign-up.module.scss";
import classNames from "classnames/bind";
export default function SignUpPage() {
@@ -37,7 +37,7 @@ export default function SignUpPage() {
+
+
+
diff --git a/public/icon/eye-on.svg b/public/icon/eye-on.svg
new file mode 100644
index 000000000..61350f131
--- /dev/null
+++ b/public/icon/eye-on.svg
@@ -0,0 +1,4 @@
+
diff --git a/public/img/facebook.svg b/public/icon/facebook.svg
similarity index 100%
rename from public/img/facebook.svg
rename to public/icon/facebook.svg
diff --git a/public/img/google.svg b/public/icon/google.svg
similarity index 100%
rename from public/img/google.svg
rename to public/icon/google.svg
diff --git a/public/img/instagram.svg b/public/icon/instagram.svg
similarity index 100%
rename from public/img/instagram.svg
rename to public/icon/instagram.svg
diff --git a/public/img/kakao.svg b/public/icon/kakao.svg
similarity index 100%
rename from public/img/kakao.svg
rename to public/icon/kakao.svg
diff --git a/public/icon/link.svg b/public/icon/link.svg
new file mode 100644
index 000000000..ee600878d
--- /dev/null
+++ b/public/icon/link.svg
@@ -0,0 +1,4 @@
+
diff --git a/public/icon/search.svg b/public/icon/search.svg
new file mode 100644
index 000000000..0de52d7dc
--- /dev/null
+++ b/public/icon/search.svg
@@ -0,0 +1,4 @@
+
diff --git a/public/img/twitter.svg b/public/icon/twitter.svg
similarity index 100%
rename from public/img/twitter.svg
rename to public/icon/twitter.svg
diff --git a/public/img/youtube.svg b/public/icon/youtube.svg
similarity index 100%
rename from public/img/youtube.svg
rename to public/icon/youtube.svg
diff --git a/public/img/Linkbrary.jpg b/public/img/Linkbrary.jpg
deleted file mode 100644
index a292b3e23..000000000
Binary files a/public/img/Linkbrary.jpg and /dev/null differ
diff --git a/public/img/main.png b/public/img/main.png
deleted file mode 100644
index 97b9cba58..000000000
Binary files a/public/img/main.png and /dev/null differ
diff --git a/public/img/main.svg b/public/img/main.svg
new file mode 100644
index 000000000..223d5be45
--- /dev/null
+++ b/public/img/main.svg
@@ -0,0 +1,70 @@
+
diff --git a/public/img/manage.png b/public/img/manage.png
deleted file mode 100644
index 35f5fab4a..000000000
Binary files a/public/img/manage.png and /dev/null differ
diff --git a/public/img/manage.svg b/public/img/manage.svg
new file mode 100644
index 000000000..2c0b2f896
--- /dev/null
+++ b/public/img/manage.svg
@@ -0,0 +1,10 @@
+
diff --git a/public/img/meta.png b/public/img/meta.png
deleted file mode 100644
index bb0a891ce..000000000
Binary files a/public/img/meta.png and /dev/null differ
diff --git a/public/img/save.png b/public/img/save.png
deleted file mode 100644
index 8f2a2c97b..000000000
Binary files a/public/img/save.png and /dev/null differ
diff --git a/public/img/save.svg b/public/img/save.svg
new file mode 100644
index 000000000..e27ff00a2
--- /dev/null
+++ b/public/img/save.svg
@@ -0,0 +1,28 @@
+
diff --git a/public/img/search.png b/public/img/search.png
deleted file mode 100644
index cb76d8a3a..000000000
Binary files a/public/img/search.png and /dev/null differ
diff --git a/public/img/search.svg b/public/img/search.svg
new file mode 100644
index 000000000..09a92071b
--- /dev/null
+++ b/public/img/search.svg
@@ -0,0 +1,44 @@
+
diff --git a/public/img/share.png b/public/img/share.png
deleted file mode 100644
index 350f043d4..000000000
Binary files a/public/img/share.png and /dev/null differ
diff --git a/public/img/share.svg b/public/img/share.svg
new file mode 100644
index 000000000..62884e6a9
--- /dev/null
+++ b/public/img/share.svg
@@ -0,0 +1,44 @@
+
diff --git a/public/img/thumbnail.svg b/public/img/thumbnail.svg
new file mode 100644
index 000000000..bf37c269b
--- /dev/null
+++ b/public/img/thumbnail.svg
@@ -0,0 +1,14 @@
+
\ No newline at end of file
diff --git a/styles/Home.module.scss b/styles/index.module.scss
similarity index 99%
rename from styles/Home.module.scss
rename to styles/index.module.scss
index e8fb25f73..8f48e0af0 100644
--- a/styles/Home.module.scss
+++ b/styles/index.module.scss
@@ -50,11 +50,11 @@
color: transparent;
background-clip: text;
}
-.signup {
+.sign-up {
display: flex;
justify-content: center;
}
-.signup__link {
+.sign-up__link {
margin: 24px;
background: linear-gradient(90.99deg, #6d6afe 0.12%, #6ae3fe 101.84%);
color: #f5f5f5;
@@ -249,7 +249,7 @@
justify-content: center;
align-items: center;
}
- .signup__link {
+ .sign-up__link {
width: 200px;
height: 37px;
padding: 10px 16px;
diff --git a/styles/signin.module.scss b/styles/sign-in.module.scss
similarity index 99%
rename from styles/signin.module.scss
rename to styles/sign-in.module.scss
index 8f86da6c5..666f597ed 100644
--- a/styles/signin.module.scss
+++ b/styles/sign-in.module.scss
@@ -119,7 +119,7 @@
.button {
cursor: pointer;
}
-.signin {
+.sign-in__button {
font-family: Pretendard;
font-size: 18px;
font-weight: 600;
diff --git a/styles/signup.module.scss b/styles/sign-up.module.scss
similarity index 98%
rename from styles/signup.module.scss
rename to styles/sign-up.module.scss
index cdab47b14..547c8fbe7 100644
--- a/styles/signup.module.scss
+++ b/styles/sign-up.module.scss
@@ -71,7 +71,7 @@
letter-spacing: 0em;
text-align: left;
}
-.signup__form {
+.sign-up__form {
display: flex;
flex-direction: column;
justify-content: flex-start;
@@ -115,7 +115,7 @@
outline: none;
}
-.signup__button {
+.sign-up__button {
font-family: Pretendard;
font-size: 18px;
font-weight: 600;
diff --git a/utils/useFetch.ts b/utils/useFetch.ts
new file mode 100644
index 000000000..48415eb9a
--- /dev/null
+++ b/utils/useFetch.ts
@@ -0,0 +1,25 @@
+import { useCallback, useEffect, useState } from "react";
+
+export function useFetch(url: string): T | null {
+ const [data, setData] = useState(null);
+
+ const fetchData = useCallback(async () => {
+ try {
+ const response = await fetch(url);
+ if (response.ok) {
+ const data: T = await response.json();
+ setData(data);
+ } else {
+ setData(null);
+ }
+ } catch (error) {
+ console.error("fetch에 실패했습니다.");
+ setData(null);
+ }
+ }, [url]);
+
+ useEffect(() => {
+ fetchData();
+ }, [fetchData]);
+ return data;
+}
diff --git a/utils/util.ts b/utils/util.ts
new file mode 100644
index 000000000..96dba6def
--- /dev/null
+++ b/utils/util.ts
@@ -0,0 +1,42 @@
+import moment from "moment";
+
+export const isEmailValid = (email: string) => {
+ const emailForm =
+ /^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*\.[a-zA-Z]{2,3}$/i;
+ return emailForm.test(email);
+};
+
+export const isPasswordValid = (password: string) => {
+ const passwordForm = /(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/;
+ return passwordForm.test(password);
+};
+
+export const formatDate = (dateString: string): string => {
+ const date = moment(dateString);
+ return date.format("YYYY.MM.DD");
+};
+
+export const generateTimeText = (createdAt: string): string => {
+ const timeDiff = moment().diff(moment(createdAt), "minutes");
+
+ if (timeDiff < 2) {
+ return "1 minute ago";
+ }
+ if (timeDiff <= 59) {
+ return `${timeDiff} minutes ago`;
+ }
+ if (timeDiff < 60 * 24) {
+ const hours = Math.floor(timeDiff / 60);
+ return hours === 1 ? "1 hour ago" : `${hours} hours ago`;
+ }
+ if (timeDiff <= 60 * 24 * 30) {
+ const days = Math.floor(timeDiff / (60 * 24));
+ return days === 1 ? "1 day ago" : `${days} days ago`;
+ }
+ if (timeDiff <= 60 * 24 * 30 * 31) {
+ const months = Math.floor(timeDiff / (60 * 24 * 30));
+ return months === 1 ? "1 month ago" : `${months} months ago`;
+ }
+ const years = Math.floor(timeDiff / (60 * 24 * 30 * 12));
+ return years === 1 ? "1 year ago" : `${years} years ago`;
+};