diff --git a/.gitignore b/.gitignore
index 8f322f0d8..fd3dbb571 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@
/node_modules
/.pnp
.pnp.js
+.yarn/install-state.gz
# testing
/coverage
diff --git a/README.md b/README.md
index a75ac5248..d6bbf996f 100644
--- a/README.md
+++ b/README.md
@@ -1,40 +1,56 @@
-This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
-
-## Getting Started
-
-First, run the development server:
-
-```bash
-npm run dev
-# or
-yarn dev
-# or
-pnpm dev
-# or
-bun dev
-```
-
-Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
-
-You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
-
-[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
-
-The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
-
-This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
-
-## Learn More
-
-To learn more about Next.js, take a look at the following resources:
-
-- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
-- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
-
-You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
-
-## Deploy on Vercel
-
-The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
-
-Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
+## Styles
+
+### Week7
+
+1. 상단 네비게이션 바, 푸터를 랜딩 페이지와 동일한 스타일과 규칙으로 만듭니다. (week 1 ~ 3 요구사항 참고)
+2. Static, no image, Hover 상태 디자인을 보여주는 카드 컴포넌트를 만듭니다.
+3. Hover 상태에서 이미지가 기본 크기의 130%로 변합니다.
+4. 카드 컴포넌트를 클릭하면 해당 링크로 새로운 창을 띄워서 이동합니다.
+5. Tablet에서 카드 리스트가 화면의 너비 1124px를 기준으로 이상일 때는 3열로 작을 때는 2열로 배치됩니다.
+6. Tablet, Mobile에서 좌우 최소 여백은 32px 입니다.
+
+### Week8
+
+1. 반응형을 위한 스타일코드들을 리팩토링 합니다.
+
+## API & Logic
+
+### Week7
+
+1. 상단 네비게이션 바에 프로필 영역의 데이터는 https://bootcamp-api.codeit.kr/docs 에 명세된 “/api/sample/user”를 활용합니다.
+2. 상단 네비게이션 바에 프로필 영역의 데이터가 없는 경우 “로그인” 버튼이 보이도록 만듭니다.
+3. 폴더 소유자, 폴더 이름 영역, 링크들에 대한 데이터는 “/api/sample/folder”를 활용합니다.
+4. 커스텀 hook을 만들어 사용합니다.(선택)
+5. shared 페이지는 외부 유저에게 자신의 폴더 데이터 하나를 공유할 때 유저가 보게되는 화면 입니다.
+6. 카드 컴포넌트에서 createdAt 데이터 기준으로 현재 Date와 차이에 따라 아래와 같은 규칙으로 설정해 주세요.
+ - 2분 미만은 “1 minute ago”
+ - 59분 이하는 “OO minutes ago”
+ - 60분 이상은 “1 hour ago”
+ - 23시간 이하는 “OO hours ago”
+ - 24시간 이상은 “1 day ago”
+ - 30일 이하는 “OO days ago”
+ - 31일 이상은 “1 month ago”
+ - 11달 이하는 “OO months ago”
+ - 12달 이상은 “1 year ago”
+ - OO달 이상은 “{OO/12(소수점 버린 정수)} years ago”
+
+### Week8
+
+1. useEffect 훅의 내부로직과 디펜던스를 올바르게 수정합니다.
+2. api와 util함수들을 디렉토리를 따로 생성해 관리를 쉽게 합니다.
+3. GNB의 네이밍을 더 목적에 맞게 수정합니다.
+4. 시간경과 함수의 하드코딩이 된 부분을 더 명확하고 깔끔하게 수정합니다.
+5. 시간경과 함수의 조건문을 수정합니다.
+
+### Week13
+
+1. 기존의 프로젝트를 nextjs로 마이그레이션 합니다.
+2. 폴더페이지에서 아이디가 있는 폴더로 변경했을 때 전체폴더로 돌아갈 수 없는 문제를 해결합니다.
+3. 검색바의 기능을 구현합니다.
+4. 로그인, 회원가입에 필요한 인풋을 구현합니다.
+5. 기존의 react-router-dom으로 구현했던 쿼리파라미터 관련 로직을 next의 useRouter로 변경합니다.
+
+### Week14
+
+1. 소셜공유 로직을 분리합니다.
+2. 모달 컴포넌트를 더 명확하고 간단하게 리팩토링합니다.
diff --git a/components/Avatar/Avatar.module.css b/components/Avatar/Avatar.module.css
new file mode 100644
index 000000000..aa9b32c23
--- /dev/null
+++ b/components/Avatar/Avatar.module.css
@@ -0,0 +1,18 @@
+.avatar {
+ position: relative;
+ margin-right: 6px;
+}
+
+.avatar img {
+ border-radius: 100%;
+}
+
+.small {
+ width: 35px;
+ height: 35px;
+}
+
+.medium {
+ width: 100px;
+ height: 100px;
+}
diff --git a/components/Avatar/index.tsx b/components/Avatar/index.tsx
new file mode 100644
index 000000000..68f45553e
--- /dev/null
+++ b/components/Avatar/index.tsx
@@ -0,0 +1,23 @@
+import Image from "next/image";
+import classNames from "classnames";
+import styles from "@/components/Avatar/Avatar.module.css";
+
+interface AvatarProps {
+ src: string;
+ size: string;
+}
+
+function Avatar({ src, size }: AvatarProps) {
+ const avatarClass = classNames(styles.avatar, {
+ [styles.small]: size === "small",
+ [styles.medium]: size === "medium",
+ });
+
+ return (
+
+
+
+ );
+}
+
+export default Avatar;
diff --git a/components/FolderController/FoldersController.module.css b/components/FolderController/FoldersController.module.css
new file mode 100644
index 000000000..a49288f1d
--- /dev/null
+++ b/components/FolderController/FoldersController.module.css
@@ -0,0 +1,16 @@
+.container {
+ width: 1060px;
+ margin: 0 auto;
+}
+
+@media (min-width: 768px) and (max-width: 1199px) {
+ .container {
+ width: 705px;
+ }
+}
+
+@media (min-width: 375px) and (max-width: 767px) {
+ .container {
+ width: 325px;
+ }
+}
diff --git a/components/FolderController/index.tsx b/components/FolderController/index.tsx
new file mode 100644
index 000000000..393c0e12b
--- /dev/null
+++ b/components/FolderController/index.tsx
@@ -0,0 +1,74 @@
+import { useState } from "react";
+import { useRouter } from "next/router";
+import useAsync from "@/lib/useAsync";
+import { FolderData, LinkData, getLinksByUserIdAndFolderId } from "@/lib/api";
+import SearchBar from "@/components/SearchBar";
+import FoldersList from "@/components/FoldersList";
+import FolderLinkCards from "@/components/FolderLinkCards";
+import styles from "@/components/FolderController/FoldersController.module.css";
+
+interface FoldersControllerProps {
+ folders: FolderData[];
+ userId: number;
+}
+
+function FoldersController({ folders, userId }: FoldersControllerProps) {
+ const [searchedLinks, setSearchedLinks] = useState(null);
+ const [selectedFolderId, setSelectedFolderId] = useState(null);
+ const router = useRouter();
+ const {
+ value: links,
+ isLoading,
+ error,
+ } = useAsync(
+ getLinksByUserIdAndFolderId,
+ userId,
+ selectedFolderId
+ );
+
+ const handleClickFolder = (folderId: number | null) => {
+ router.push({
+ pathname: router.pathname,
+ query: { ...router.query, folderId },
+ });
+
+ if (!folderId) {
+ router.push({
+ pathname: router.pathname,
+ });
+ }
+
+ setSelectedFolderId(folderId);
+ };
+
+ const handleSearchByKeyword = (keyword: string) => {
+ if (!links) return console.log("링크가 존재하지 않습니다!");
+ const searchedLink = links?.filter((link) => link.title?.includes(keyword));
+ if (!searchedLink) return console.log("해당 링크가 존재하지 않습니다!");
+ setSearchedLinks(searchedLink);
+ };
+
+ return (
+
+
+ {isLoading ? (
+ Loading...
+ ) : error ? (
+ Error loading data.
+ ) : (
+ links && (
+ <>
+
+
+ >
+ )
+ )}
+
+ );
+}
+
+export default FoldersController;
diff --git a/components/FolderLinkCard/index.tsx b/components/FolderLinkCard/index.tsx
new file mode 100644
index 000000000..48375d90b
--- /dev/null
+++ b/components/FolderLinkCard/index.tsx
@@ -0,0 +1,94 @@
+import { ReactElement, useState } from "react";
+import Image from "next/image";
+import { LinkData } from "@/lib/api";
+import { displayCreatedTime, formatDateString } from "@/lib/dateUtils";
+import Modal from "@/components/Modal";
+import styles from "@/components/LinkCard.module.css";
+import { FaRegStar } from "react-icons/fa";
+import { GoKebabHorizontal } from "react-icons/go";
+import LinkDeleteForm from "../Modal/childrens/LinkDeleteForm";
+import LinkAddToFolderForm from "../Modal/childrens/LinkAddToFolderForm";
+
+interface FolderLinkCardProps {
+ link: LinkData;
+}
+
+interface ActionTypes {
+ [actionType: string]: ReactElement;
+}
+
+function FolderLinkCard({ link }: FolderLinkCardProps) {
+ const [onModal, setOnModal] = useState(false);
+ const [modalContent, setModalContent] = useState(null);
+
+ const handleClickModal = (actionType: string) => {
+ const actionTypes: ActionTypes = {
+ deleteLink: ,
+ addLink: ,
+ };
+
+ setModalContent(actionTypes[actionType]);
+ setOnModal(true);
+ };
+
+ const { url, description, title, created_at, image_source } = link;
+
+ const createdTime = displayCreatedTime(created_at);
+ const createdAtFormat = formatDateString(created_at);
+
+ const src = image_source || "/card-default.png";
+
+ const handleToggleDropDown = (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const button = e.currentTarget;
+ const linkInfo = button.closest(`.${styles.linkInfo}`);
+ const dropdown = linkInfo?.querySelector(`.${styles.dropdown}`);
+
+ dropdown?.classList.toggle(styles.hidden);
+ };
+
+ return (
+
+
+
+
+
+
+
{description}
+
{createdAtFormat}
+
+ handleClickModal("deleteLink")}>
+ 삭제하기
+
+ handleClickModal("addLink")}>
+ 폴더에 추가
+
+
+
+ {onModal && (
+
setOnModal(false)}>{modalContent}
+ )}
+
+ );
+}
+
+export default FolderLinkCard;
diff --git a/components/FolderLinkCards/index.tsx b/components/FolderLinkCards/index.tsx
new file mode 100644
index 000000000..2dd13c429
--- /dev/null
+++ b/components/FolderLinkCards/index.tsx
@@ -0,0 +1,32 @@
+import { LinkData } from "@/lib/api";
+import FolderLinkCard from "@/components/FolderLinkCard";
+import styles from "@/components/LinkCards.module.css";
+
+interface FolderLinkCardsProps {
+ links: LinkData[];
+ searchedLinks: LinkData[] | null;
+}
+
+function FolderLinkCards({ links, searchedLinks }: FolderLinkCardsProps) {
+ return (
+ <>
+ {links.length > 0 ? (
+
+
+ {(searchedLinks ? searchedLinks : links).map((link) => (
+
+
+
+ ))}
+
+
+ ) : (
+
+ 저장된 링크가 없습니다.
+
+ )}
+ >
+ );
+}
+
+export default FolderLinkCards;
diff --git a/components/FoldersList/FoldersList.module.css b/components/FoldersList/FoldersList.module.css
new file mode 100644
index 000000000..ef3d1ec49
--- /dev/null
+++ b/components/FoldersList/FoldersList.module.css
@@ -0,0 +1,86 @@
+.container {
+ display: flex;
+ flex-direction: column;
+ width: 1060px;
+}
+
+.listContainer {
+ display: flex;
+ width: 100%;
+ justify-content: space-between;
+ margin-bottom: 20px;
+ position: relative;
+}
+
+.folderButtonList {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 8px;
+}
+
+.folderButtonList button {
+ flex-shrink: 0;
+ background-color: var(--white);
+ border-radius: 8px;
+ border: 1px solid var(--primary);
+ padding: 8px 12px 8px 12px;
+ text-align: center;
+}
+
+.folderAddButton {
+ width: 75px;
+ border: none;
+ background: none;
+ color: var(--primary);
+}
+
+.controlContainer {
+ display: flex;
+ width: 100%;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+}
+
+.selectedFolderName {
+ font-size: 24px;
+ font-weight: 600;
+}
+
+.folderControl {
+ display: flex;
+ gap: 3px;
+}
+
+.folderControl button {
+ display: flex;
+ align-items: center;
+ background-color: transparent;
+ border: none;
+ gap: 2px;
+}
+
+@media (min-width: 768px) and (max-width: 1199px) {
+ .container {
+ width: 705px;
+ }
+}
+
+@media (min-width: 375px) and (max-width: 767px) {
+ .container {
+ width: 325px;
+ }
+
+ .folderAddButton {
+ width: 130px;
+ height: 35px;
+ padding: 8px 24px;
+ color: var(--white);
+ background-color: var(--primary);
+ position: absolute;
+ bottom: -500px;
+ left: 95px;
+ border-radius: 20px;
+ }
+}
diff --git a/components/FoldersList/index.tsx b/components/FoldersList/index.tsx
new file mode 100644
index 000000000..c91210f57
--- /dev/null
+++ b/components/FoldersList/index.tsx
@@ -0,0 +1,92 @@
+import { ReactElement, useState } from "react";
+import { FolderData } from "@/lib/api";
+import Modal from "@/components/Modal";
+import { FaPencilAlt, FaRegShareSquare, FaRegTrashAlt } from "react-icons/fa";
+import styles from "@/components/FoldersList/FoldersList.module.css";
+import FolderAddForm from "../Modal/childrens/FolderAddForm";
+import SocialShareBox from "../Modal/childrens/SocialShareBox";
+import FolderEditForm from "../Modal/childrens/FolderEditForm";
+import FolderDeleteForm from "../Modal/childrens/FolderDeleteForm";
+
+interface FoldersListProps {
+ handleClick: (folderId: number | null) => void;
+ folders: FolderData[];
+ selectedFolderId: number | null;
+}
+
+interface ActionTypes {
+ [actionType: string]: ReactElement;
+}
+
+function FoldersList({
+ handleClick,
+ folders,
+ selectedFolderId,
+}: FoldersListProps) {
+ const [modalContent, setModalContent] = useState(null);
+ const currentFolder = folders.find(
+ (folder) => folder.id === selectedFolderId
+ );
+
+ const handleClickModal = (actionType: string) => {
+ const actionTypes: ActionTypes = {
+ add: ,
+ share: ,
+ modify: ,
+ delete: ,
+ };
+
+ setModalContent(actionTypes[actionType]);
+ };
+
+ return (
+
+
+
+
+ handleClick(null)}>전체
+
+ {folders.map((folder) => (
+
+ handleClick(folder.id)}>
+ {folder.name}
+
+
+ ))}
+
+
handleClickModal("add")}
+ >
+ 폴더 추가 +
+
+
+
+
+ {currentFolder ? currentFolder.name : "전체"}
+
+ {selectedFolderId && (
+
+ handleClickModal("share")}>
+
+ 공유
+
+ handleClickModal("modify")}>
+
+ 수정
+
+ handleClickModal("delete")}>
+
+ 삭제
+
+
+ )}
+
+ {modalContent && (
+ setModalContent(null)}>{modalContent}
+ )}
+
+ );
+}
+
+export default FoldersList;
diff --git a/components/Footer/Footer.module.css b/components/Footer/Footer.module.css
new file mode 100644
index 000000000..883ee1c37
--- /dev/null
+++ b/components/Footer/Footer.module.css
@@ -0,0 +1,62 @@
+.container {
+ width: 100%;
+ display: grid;
+ grid-template: auto / 1fr 1fr 1fr;
+ padding: 40px 104px;
+ margin-top: 70px;
+ background-color: var(--black);
+ color: var(--white);
+}
+
+.container a {
+ display: inline-block;
+ text-decoration: none;
+ color: var(--white);
+}
+
+.companyInfo {
+ display: flex;
+ align-items: center;
+}
+
+.forUser {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 10px;
+}
+
+.socialMedia {
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+}
+
+@media (min-width: 375px) and (max-width: 767px) {
+ .container {
+ padding: 30px 60px;
+ grid-template:
+ "b c" 90px
+ "a ." 20px/ 1fr 1fr;
+ }
+
+ .forUser {
+ font-size: 14px;
+ grid-area: b;
+ justify-content: flex-start;
+ align-items: flex-start;
+ }
+
+ .companyInfo {
+ font-size: 14px;
+ grid-area: a;
+ }
+
+ .socialMedia {
+ grid-area: c;
+ }
+
+ .socialMedia a {
+ height: 16px;
+ }
+}
diff --git a/components/Footer/index.tsx b/components/Footer/index.tsx
new file mode 100644
index 000000000..64b497464
--- /dev/null
+++ b/components/Footer/index.tsx
@@ -0,0 +1,36 @@
+import { FaFacebook, FaInstagram, FaTwitter, FaYoutube } from "react-icons/fa";
+import styles from "@/components/Footer/Footer.module.css";
+
+function Footer() {
+ return (
+
+ );
+}
+
+export default Footer;
diff --git a/components/Header/Header.module.css b/components/Header/Header.module.css
new file mode 100644
index 000000000..17493e2e9
--- /dev/null
+++ b/components/Header/Header.module.css
@@ -0,0 +1,56 @@
+.container {
+ width: 100%;
+ height: 110px;
+ position: fixed;
+ top: 0;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 200px;
+ z-index: 1;
+}
+
+.logo {
+ width: 133px;
+ height: 24px;
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.loginBtn {
+ font-size: 22px;
+ font-weight: 600;
+}
+
+.profileContainer {
+ display: flex;
+ align-items: center;
+ position: relative;
+}
+
+.profileContainer span {
+ display: inline-block;
+ font-size: 16px;
+}
+
+@media (min-width: 768px) and (max-width: 1199px) {
+ .container {
+ padding: 0 32px;
+ }
+}
+
+@media (min-width: 375px) and (max-width: 767px) {
+ .container {
+ padding: 0 32px;
+ }
+
+ .logo {
+ width: 100px;
+ }
+
+ .profileContainer span {
+ display: none;
+ }
+}
diff --git a/components/Header/index.tsx b/components/Header/index.tsx
new file mode 100644
index 000000000..296944bcf
--- /dev/null
+++ b/components/Header/index.tsx
@@ -0,0 +1,40 @@
+import Image from "next/image";
+import Avatar from "@/components/Avatar";
+import styles from "@/components/Header/Header.module.css";
+
+interface HeaderProps {
+ userAvatarImage: string;
+ userProfileEmail: string;
+ userLogInSuccess: boolean;
+}
+
+function Header({
+ userAvatarImage,
+ userProfileEmail,
+ userLogInSuccess,
+}: HeaderProps) {
+ return (
+
+
+
+
+ {userLogInSuccess ? (
+
+ ) : (
+ 로그인
+ )}
+
+ );
+}
+
+export default Header;
diff --git a/components/Input/Input.module.css b/components/Input/Input.module.css
new file mode 100644
index 000000000..f1cf630ad
--- /dev/null
+++ b/components/Input/Input.module.css
@@ -0,0 +1,35 @@
+.container {
+ width: 350px;
+ position: relative;
+}
+
+.inputWrapper {
+ width: 100%;
+ padding: 18px 15px;
+ border: 1px solid var(--gray-500);
+ border-radius: 8px;
+}
+
+.inputWrapper:focus {
+ border: 1px solid var(--primary);
+}
+
+.eyeSlash {
+ border: none;
+ background: none;
+ position: absolute;
+ right: 16px;
+ top: 17px;
+}
+
+.errorBorder {
+ border: 1px solid var(--red);
+}
+
+.errorMessage {
+ display: inline-block;
+ font-size: 14px;
+ font-weight: 400;
+ color: var(--red);
+ margin-top: 5px;
+}
diff --git a/components/Input/index.tsx b/components/Input/index.tsx
new file mode 100644
index 000000000..96cb5a394
--- /dev/null
+++ b/components/Input/index.tsx
@@ -0,0 +1,41 @@
+import { HTMLInputTypeAttribute, useEffect, useState } from "react";
+import { FaEyeSlash } from "react-icons/fa";
+import styles from "@/components/Input/Input.module.css";
+
+interface InputProps {
+ type: HTMLInputTypeAttribute;
+}
+
+// 추후에 벨리데이션 로직과 컴포넌트에서 사용할 때 리팩토링 예정
+function Input({ type }: InputProps) {
+ const [isError, setIsError] = useState(true);
+ const inputBorder = document.querySelector(".inputWrapper");
+
+ return (
+
+ {isError ? (
+ <>
+
+ 내용을 다시 작성해주세요
+ >
+ ) : (
+
+ )}
+ {type === "password" && (
+
+
+
+ )}
+
+ );
+}
+
+export default Input;
diff --git a/components/LinkAddForm/LinkAddForm.module.css b/components/LinkAddForm/LinkAddForm.module.css
new file mode 100644
index 000000000..1e5a63823
--- /dev/null
+++ b/components/LinkAddForm/LinkAddForm.module.css
@@ -0,0 +1,55 @@
+.container {
+ width: 100%;
+ padding: 140px 0 80px;
+ background-color: var(--gray-300);
+ display: flex;
+ justify-content: center;
+}
+
+.form {
+ width: 800px;
+ height: 70px;
+ position: relative;
+ display: flex;
+}
+
+.linkIcon {
+ position: absolute;
+ top: 26px;
+ left: 15px;
+ color: var(--gray-600);
+}
+
+.input {
+ width: 100%;
+ padding: 16px 40px 16px 40px;
+ border: 1px solid var(--primary);
+ border-radius: 15px;
+ color: var(--gray-600);
+}
+
+.button {
+ width: 80px;
+ height: 35px;
+ position: absolute;
+ top: 17px;
+ right: 15px;
+ border-radius: 8px;
+ border: none;
+ background: var(--primary-gradient);
+ color: var(--white);
+ font-size: 14px;
+ font-weight: 600;
+}
+
+@media (min-width: 768px) and (max-width: 1199px) {
+ .form {
+ width: 705px;
+ }
+}
+
+@media (min-width: 375px) and (max-width: 767px) {
+ .form {
+ width: 325px;
+ }
+}
diff --git a/components/LinkAddForm/index.tsx b/components/LinkAddForm/index.tsx
new file mode 100644
index 000000000..c42fd0f9a
--- /dev/null
+++ b/components/LinkAddForm/index.tsx
@@ -0,0 +1,20 @@
+import { FaLink } from "react-icons/fa";
+import styles from "@/components/LinkAddForm/LinkAddForm.module.css";
+
+function LinkAddForm() {
+ return (
+
+ );
+}
+
+export default LinkAddForm;
diff --git a/components/LinkCard.module.css b/components/LinkCard.module.css
new file mode 100644
index 000000000..50c91055a
--- /dev/null
+++ b/components/LinkCard.module.css
@@ -0,0 +1,120 @@
+.linkContainer {
+ width: 340px;
+ border: none;
+ border-radius: 15px;
+ box-shadow: 0 0 15px 2px var(--gray-400);
+}
+
+.imageWrapper {
+ width: 100%;
+ height: 200px;
+ border-radius: 15px 15px 0 0;
+ position: relative;
+ overflow: hidden;
+}
+
+.linkImage {
+ border-radius: 15px 15px 0 0;
+ width: 340px;
+ height: 100%;
+ transition: transform 0.3s ease;
+ position: relative;
+}
+
+.linkImage:hover {
+ transform: scale(1.3);
+}
+
+.linkInfo {
+ padding: 15px;
+ height: 135px;
+ border-radius: 0 0 15px 15px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ transition: background-color 0.3s ease;
+ position: relative;
+}
+
+.linkContainer:hover .linkInfo {
+ background-color: var(--gray-400);
+}
+
+.linkInfoContent:first-child {
+ font-size: 13px;
+ font-weight: 400;
+ color: var(--gray-600);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.linkInfoContent:nth-child(2) {
+ font-size: 16px;
+ font-weight: 400;
+ color: var(--black);
+ margin: 10px 0 10px;
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 2;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ height: 48px;
+ line-height: 1.5;
+}
+
+.linkInfoContent:nth-child(3) {
+ font-size: 14px;
+ font-weight: 400;
+ color: var(--gray-700);
+}
+
+.starIcon {
+ color: var(--gray-600);
+ width: 30px;
+ height: 30px;
+ position: absolute;
+ right: 15px;
+ top: 15px;
+}
+
+.kebabButton {
+ background-color: transparent;
+ border: none;
+}
+
+.hidden {
+ display: none;
+}
+
+.dropdown {
+ width: 100px;
+ position: absolute;
+ right: -55px;
+ top: 35px;
+ z-index: 100;
+}
+
+.dropdown button {
+ padding: 7px 12px;
+ width: 100px;
+ border: none;
+ box-shadow: 0 0 15px 2px var(--gray-400);
+ cursor: pointer;
+}
+
+.dropdown button:first-child {
+ background-color: var(--white);
+}
+
+.dropdown button:nth-child(2) {
+ background-color: var(--gray-400);
+}
+
+@media (min-width: 375px) and (max-width: 767px) {
+ .container,
+ .linkImageAnchor,
+ .linkImage {
+ width: 325px;
+ }
+}
diff --git a/components/LinkCards.module.css b/components/LinkCards.module.css
new file mode 100644
index 000000000..8c6dc1cab
--- /dev/null
+++ b/components/LinkCards.module.css
@@ -0,0 +1,40 @@
+.container {
+ width: 1060px;
+ margin: 0 auto;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.linkList {
+ width: 100%;
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr;
+ gap: 20px;
+}
+
+.empty {
+ min-height: 400px;
+ display: flex;
+ justify-content: center;
+}
+
+@media (min-width: 768px) and (max-width: 1199px) {
+ .container {
+ width: 705px;
+ }
+
+ .linkList {
+ grid-template-columns: 1fr 1fr;
+ }
+}
+
+@media (min-width: 375px) and (max-width: 767px) {
+ .container {
+ width: 325px;
+ }
+
+ .linkList {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/components/Modal/Modal.module.css b/components/Modal/Modal.module.css
new file mode 100644
index 000000000..8139d4bff
--- /dev/null
+++ b/components/Modal/Modal.module.css
@@ -0,0 +1,36 @@
+.background {
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.4);
+ z-index: 1000;
+ top: 0;
+ left: 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ position: fixed;
+}
+
+.container {
+ padding: 28px;
+ background-color: var(--white);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 8px;
+ border-radius: 15px;
+ position: relative;
+}
+
+.exitButton {
+ width: 24px;
+ height: 24px;
+ position: absolute;
+ right: 16px;
+ top: 16px;
+ border: none;
+ border-radius: 9999px;
+ background-color: var(--gray-400);
+ color: var(--gray-600);
+ cursor: pointer;
+}
diff --git a/components/Modal/childrens/FolderAddForm/index.tsx b/components/Modal/childrens/FolderAddForm/index.tsx
new file mode 100644
index 000000000..d46671576
--- /dev/null
+++ b/components/Modal/childrens/FolderAddForm/index.tsx
@@ -0,0 +1,19 @@
+import styles from "@/components/Modal/childrens/ModalChildren.module.css";
+
+function FolderAddForm() {
+ return (
+ <>
+ 폴더 추가
+
+ >
+ );
+}
+
+export default FolderAddForm;
diff --git a/components/Modal/childrens/FolderDeleteForm/index.tsx b/components/Modal/childrens/FolderDeleteForm/index.tsx
new file mode 100644
index 000000000..6620fa446
--- /dev/null
+++ b/components/Modal/childrens/FolderDeleteForm/index.tsx
@@ -0,0 +1,17 @@
+import styles from "@/components/Modal/childrens/ModalChildren.module.css";
+
+function FolderDeleteForm() {
+ return (
+ <>
+ 폴더 삭제
+ 폴더명
+
+ >
+ );
+}
+
+export default FolderDeleteForm;
diff --git a/components/Modal/childrens/FolderEditForm/index.tsx b/components/Modal/childrens/FolderEditForm/index.tsx
new file mode 100644
index 000000000..a14794d4d
--- /dev/null
+++ b/components/Modal/childrens/FolderEditForm/index.tsx
@@ -0,0 +1,19 @@
+import styles from "@/components/Modal/childrens/ModalChildren.module.css";
+
+function FolderEditForm() {
+ return (
+ <>
+ 폴더 이름 변경
+
+ >
+ );
+}
+
+export default FolderEditForm;
diff --git a/components/Modal/childrens/LinkAddToFolderForm/index.tsx b/components/Modal/childrens/LinkAddToFolderForm/index.tsx
new file mode 100644
index 000000000..bf9877ff3
--- /dev/null
+++ b/components/Modal/childrens/LinkAddToFolderForm/index.tsx
@@ -0,0 +1,29 @@
+import styles from "@/components/Modal/childrens/ModalChildren.module.css";
+
+function LinkAddToFolderForm() {
+ return (
+ <>
+ 폴더에 추가
+ 링크 주소
+
+ >
+ );
+}
+
+export default LinkAddToFolderForm;
diff --git a/components/Modal/childrens/LinkDeleteForm/index.tsx b/components/Modal/childrens/LinkDeleteForm/index.tsx
new file mode 100644
index 000000000..07bfd6ef7
--- /dev/null
+++ b/components/Modal/childrens/LinkDeleteForm/index.tsx
@@ -0,0 +1,17 @@
+import styles from "@/components/Modal/childrens/ModalChildren.module.css";
+
+function LinkDeleteForm() {
+ return (
+ <>
+ 링크 삭제
+ url
+
+ >
+ );
+}
+
+export default LinkDeleteForm;
diff --git a/components/Modal/childrens/ModalChildren.module.css b/components/Modal/childrens/ModalChildren.module.css
new file mode 100644
index 000000000..befba1571
--- /dev/null
+++ b/components/Modal/childrens/ModalChildren.module.css
@@ -0,0 +1,92 @@
+.formContainer {
+ width: 280px;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ margin-top: 10px;
+}
+
+.formInput {
+ width: 100%;
+ padding: 15px 18px;
+ border: 1px solid var(--primary);
+ border-radius: 10px;
+ color: var(--black);
+ font-size: 16px;
+ font-weight: 400;
+}
+
+.formButton {
+ width: 100%;
+ padding: 16px 20px;
+ border-radius: 10px;
+ border: none;
+ background: var(--primary-gradient);
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--white);
+}
+
+.redBackground {
+ background: none;
+ background-color: var(--red);
+}
+
+.shareBox {
+ width: 280px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 32px;
+ margin-top: 10px;
+}
+
+.shareButtonWrapper {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 10px;
+ font-size: 13px;
+ font-weight: 400;
+ color: var(--black);
+}
+
+.shareButton {
+ background: none;
+ border: none;
+ border-radius: 100%;
+}
+
+.title {
+ font-size: 20px;
+ font-weight: 700;
+ color: var(--black);
+}
+
+.subTitle {
+ font-size: 14px;
+ font-weight: 400;
+ color: var(--gray-600);
+}
+
+.inputList {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.inputList button {
+ width: 100%;
+ text-align: left;
+ font-size: 16px;
+ font-weight: 400;
+ background-color: transparent;
+ border: none;
+ border-radius: 8px;
+ padding: 6px 15px;
+}
+
+.inputList button:active {
+ background-color: var(--gray-400);
+ color: var(--primary);
+}
diff --git a/components/Modal/childrens/SocialShareBox/index.tsx b/components/Modal/childrens/SocialShareBox/index.tsx
new file mode 100644
index 000000000..430a0c065
--- /dev/null
+++ b/components/Modal/childrens/SocialShareBox/index.tsx
@@ -0,0 +1,92 @@
+import Image from "next/image";
+import styles from "@/components/Modal/childrens/ModalChildren.module.css";
+
+interface SocialShareBoxProps {
+ title: string;
+}
+
+function SocialShareBox({ title }: SocialShareBoxProps) {
+ /* const location = useLocation();
+ const currentUrl =
+ window.location.origin + location.pathname + location.search;
+ const facebookShareUrl = `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(
+ currentUrl
+ )}`;
+
+ useEffect(() => {
+ if (window.Kakao && !window.Kakao.isInitialized()) {
+ window.Kakao.init(KAKAO_KEY);
+ }
+ }, []);
+
+ const handleCopyToClipBoard = async () => {
+ try {
+ await navigator.clipboard.writeText(currentUrl);
+ alert("클립보드에 복사되었습니다!");
+ } catch (error) {
+ console.error("클립보드 복사 실패:", error);
+ alert("클립보드 복사에 실패했습니다.");
+ }
+ };
+
+ const handleShareToKakao = (title: string) => {
+ window.Kakao.Link.sendDefault({
+ objectType: "feed",
+ content: {
+ title: title,
+ description: "유용한 링크를 모은 폴더를 공유합니다.",
+ imageUrl: defaultImg,
+ link: {
+ mobileWebUrl: window.location.href,
+ webUrl: window.location.href,
+ },
+ },
+ buttons: [
+ {
+ title: "웹으로 보기",
+ link: {
+ mobileWebUrl: window.location.href,
+ webUrl: window.location.href,
+ },
+ },
+ ],
+ });
+ }; */
+
+ return (
+ <>
+ 폴더 공유
+ 폴더명
+
+
+ handleShareToKakao(title)}
+ >
+
+
+ 카카오톡
+
+
+
+
+
+
+ 링크 복사
+
+
+ >
+ );
+}
+
+export default SocialShareBox;
diff --git a/components/Modal/index.tsx b/components/Modal/index.tsx
new file mode 100644
index 000000000..1264a58a9
--- /dev/null
+++ b/components/Modal/index.tsx
@@ -0,0 +1,21 @@
+import styles from "@/components/Modal/Modal.module.css";
+
+interface ModalProps {
+ children: React.ReactNode;
+ onClick: () => void;
+}
+
+function Modal({ children, onClick }: ModalProps) {
+ return (
+
+
+ {children}
+
+ X
+
+
+
+ );
+}
+
+export default Modal;
diff --git a/components/SearchBar/SearchBar.module.css b/components/SearchBar/SearchBar.module.css
new file mode 100644
index 000000000..d0cbe33f1
--- /dev/null
+++ b/components/SearchBar/SearchBar.module.css
@@ -0,0 +1,26 @@
+.wrapper {
+ width: 100%;
+ height: 54px;
+ padding: 15px 16px 15px 16px;
+ margin: 50px 0;
+ border-radius: 10px;
+ background-color: var(--gray-500);
+ display: flex;
+ align-items: center;
+}
+
+.button,
+.input {
+ border: none;
+ background-color: transparent;
+ color: var(--gray-700);
+ font-size: 16px;
+}
+
+.input {
+ width: 100%;
+}
+
+.input:focus {
+ outline: none;
+}
diff --git a/components/SearchBar/index.tsx b/components/SearchBar/index.tsx
new file mode 100644
index 000000000..df7c6c4e9
--- /dev/null
+++ b/components/SearchBar/index.tsx
@@ -0,0 +1,38 @@
+import { FaSearch } from "react-icons/fa";
+import styles from "@/components/SearchBar/SearchBar.module.css";
+import { ChangeEventHandler, FormEventHandler, useState } from "react";
+
+interface SearchBarProps {
+ onSearch: (keyword: string) => void;
+}
+
+function SearchBar({ onSearch }: SearchBarProps) {
+ const [inputValue, setInputValue] = useState("");
+
+ const handleChange: ChangeEventHandler = (e) => {
+ setInputValue(e.target.value);
+ };
+
+ const handleSubmit: FormEventHandler = (e) => {
+ e.preventDefault();
+
+ onSearch(inputValue);
+ };
+
+ return (
+
+ );
+}
+
+export default SearchBar;
diff --git a/components/SharedLinkCard/index.tsx b/components/SharedLinkCard/index.tsx
new file mode 100644
index 000000000..f752ec514
--- /dev/null
+++ b/components/SharedLinkCard/index.tsx
@@ -0,0 +1,48 @@
+import { SampleLink } from "@/lib/api";
+import { displayCreatedTime, formatDateString } from "@/lib/dateUtils";
+import styles from "@/components/LinkCard.module.css";
+import Image from "next/image";
+
+interface SharedLinkCardProps {
+ link: SampleLink;
+}
+
+function SharedLinkCard({ link }: SharedLinkCardProps) {
+ const { url, description, title, createdAt, imageSource } = link;
+
+ const createdTime = displayCreatedTime(createdAt);
+ const createdAtFormat = formatDateString(createdAt);
+
+ const src = imageSource || "/card-default.png";
+
+ return (
+
+
+
+
+
+
{createdTime}
+
{description}
+
{createdAtFormat}
+
+
+ );
+}
+
+export default SharedLinkCard;
diff --git a/components/SharedLinkCards/SharedLinkCards.tsx b/components/SharedLinkCards/SharedLinkCards.tsx
new file mode 100644
index 000000000..ba44a48d5
--- /dev/null
+++ b/components/SharedLinkCards/SharedLinkCards.tsx
@@ -0,0 +1,37 @@
+import { SampleLink } from "@/lib/api";
+import SearchBar from "@/components/SearchBar";
+import SharedLinkCard from "@/components/SharedLinkCard";
+import styles from "@/components/LinkCards.module.css";
+import { useState } from "react";
+
+interface SharedLinkCardsProps {
+ links: SampleLink[];
+}
+
+function SharedLinkCards({ links }: SharedLinkCardsProps) {
+ const [searchedLinks, setSearchedLinks] = useState(null);
+
+ const handleSearchByKeyword = (keyword: string) => {
+ if (!links) return console.log("링크가 존재하지 않습니다!");
+ const searchedLink = links?.filter((link) =>
+ link.description?.toLowerCase().includes(keyword.toLowerCase())
+ );
+ if (!searchedLink) return console.log("해당 링크가 존재하지 않습니다!");
+ setSearchedLinks(searchedLink);
+ };
+
+ return (
+
+
+
+ {(searchedLinks ? searchedLinks : links).map((link) => (
+
+
+
+ ))}
+
+
+ );
+}
+
+export default SharedLinkCards;
diff --git a/components/UserProfileAndTitle/UserProfileAndTitle.module.css b/components/UserProfileAndTitle/UserProfileAndTitle.module.css
new file mode 100644
index 000000000..b5b866129
--- /dev/null
+++ b/components/UserProfileAndTitle/UserProfileAndTitle.module.css
@@ -0,0 +1,18 @@
+.container {
+ width: 100%;
+ margin: 0 auto;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 20px;
+ background-color: var(--gray-300);
+ padding: 140px 0 80px;
+}
+
+.userName {
+ font-size: 16px;
+}
+
+.folderName {
+ font-size: 40px;
+}
diff --git a/components/UserProfileAndTitle/index.tsx b/components/UserProfileAndTitle/index.tsx
new file mode 100644
index 000000000..2b0c4f54a
--- /dev/null
+++ b/components/UserProfileAndTitle/index.tsx
@@ -0,0 +1,24 @@
+import Avatar from "@/components/Avatar";
+import styles from "@/components/UserProfileAndTitle/UserProfileAndTitle.module.css";
+
+interface UserProfileAndTitleProps {
+ userName: string;
+ folderName: string;
+ folderImage: string;
+}
+
+function UserProfileAndTitle({
+ userName,
+ folderName,
+ folderImage,
+}: UserProfileAndTitleProps) {
+ return (
+
+
+ @{userName}
+ {folderName}
+
+ );
+}
+
+export default UserProfileAndTitle;
diff --git a/lib/api.ts b/lib/api.ts
new file mode 100644
index 000000000..dce83d704
--- /dev/null
+++ b/lib/api.ts
@@ -0,0 +1,140 @@
+import { Params } from "./useAsync";
+
+const BASE_URL = "https://bootcamp-api.codeit.kr/api";
+
+export interface SampleUser {
+ id: number;
+ name: string;
+ email: string;
+ profileImageSource: string;
+}
+
+interface SampleFolderOwner {
+ id: number;
+ name: string;
+ profileImageSource: string;
+}
+
+export interface SampleLink {
+ id: number;
+ createdAt: string;
+ url: string;
+ title: string;
+ description: string;
+ imageSource: string;
+}
+
+export interface SampleFolder {
+ id: number;
+ name: string;
+ owner: SampleFolderOwner;
+ links?: SampleLink[];
+}
+
+interface SampleFolderResponse {
+ folder: SampleFolder;
+}
+
+export interface UserData {
+ id: number;
+ created_at: string;
+ name: string;
+ image_source: string;
+ email: string;
+ auth_id: string;
+}
+
+export interface FolderData {
+ id: number;
+ created_at: string;
+ name: string;
+ user_id: number;
+ favorite: boolean;
+ link: {
+ count: number;
+ };
+}
+
+export interface LinkData {
+ id: number;
+ created_at: string;
+ updated_at?: string;
+ url: string;
+ title: string;
+ description?: string;
+ image_source?: string;
+ folder_id: number;
+}
+
+interface Response {
+ data: Data[];
+}
+
+export async function getUser(): Promise {
+ const response = await fetch(`${BASE_URL}/sample/user`);
+ if (!response.ok) {
+ throw new Error("잘못된 요청입니다.");
+ }
+
+ const user: SampleUser = await response.json();
+
+ return user;
+}
+
+export async function getFolder(): Promise {
+ const response = await fetch(`${BASE_URL}/sample/folder`);
+ if (!response.ok) {
+ throw new Error("잘못된 요청입니다.");
+ }
+
+ const data: SampleFolderResponse = await response.json();
+ const { folder } = data;
+
+ return folder;
+}
+
+export async function getUserById({ userId }: Params): Promise {
+ const response = await fetch(`${BASE_URL}/users/${userId}`);
+ if (!response.ok) {
+ throw new Error("잘못된 요청입니다.");
+ }
+
+ const user: Response = await response.json();
+ const { data } = user;
+
+ return data[0];
+}
+
+export async function getFoldersByUserId({
+ userId,
+}: Params): Promise {
+ const response = await fetch(`${BASE_URL}/users/${userId}/folders`);
+ if (!response.ok) {
+ throw new Error("잘못된 요청입니다.");
+ }
+
+ const folders: Response = await response.json();
+ const { data } = folders;
+
+ return data;
+}
+
+export async function getLinksByUserIdAndFolderId({
+ userId,
+ folderId,
+}: Params): Promise {
+ let url = `${BASE_URL}/users/${userId}/links`;
+ if (folderId) {
+ url += `?folderId=${folderId}`;
+ }
+
+ const response = await fetch(url);
+ if (!response.ok) {
+ throw new Error("잘못된 요청입니다.");
+ }
+
+ const links: Response = await response.json();
+ const { data } = links;
+
+ return data;
+}
diff --git a/lib/constants.ts b/lib/constants.ts
new file mode 100644
index 000000000..3b6df320c
--- /dev/null
+++ b/lib/constants.ts
@@ -0,0 +1 @@
+export const KAKAO_KEY = "a841341da0099a5d1291638b48030745";
diff --git a/lib/dateUtils.ts b/lib/dateUtils.ts
new file mode 100644
index 000000000..f5e6f37a6
--- /dev/null
+++ b/lib/dateUtils.ts
@@ -0,0 +1,43 @@
+const MINUTE: number = 60 * 1000;
+const HOUR: number = 60 * MINUTE;
+const DAY: number = 24 * HOUR;
+const MONTH: number = 30 * DAY;
+const YEAR: number = 12 * MONTH;
+
+interface TimeFormat {
+ unit: number;
+ text: string;
+}
+
+export function displayCreatedTime(date: string) {
+ const nowDate = new Date();
+ const createdDate = new Date(date);
+ const diff: number = nowDate.getTime() - createdDate.getTime();
+
+ const times: TimeFormat[] = [
+ { unit: YEAR, text: "년 전" },
+ { unit: MONTH, text: "개월 전" },
+ { unit: DAY, text: "일 전" },
+ { unit: HOUR, text: "시간 전" },
+ { unit: MINUTE, text: "분 전" },
+ ];
+
+ for (const { unit, text } of times) {
+ const diffTime = Math.floor(diff / unit);
+ if (diffTime !== 0) {
+ return `${diffTime}${text}`;
+ }
+ }
+
+ return "방금 전";
+}
+
+export function formatDateString(date: string) {
+ const createdDate = new Date(date);
+
+ const year = createdDate.getFullYear();
+ const month = String(createdDate.getMonth() + 1).padStart(2, "0");
+ const day = String(createdDate.getDate()).padStart(2, "0");
+
+ return `${year}. ${month}. ${day}`;
+}
diff --git a/lib/useAsync.ts b/lib/useAsync.ts
new file mode 100644
index 000000000..61a0b6529
--- /dev/null
+++ b/lib/useAsync.ts
@@ -0,0 +1,50 @@
+import { useCallback, useEffect, useState } from "react";
+export interface CustomAsyncReturns {
+ isLoading: boolean;
+ value: T | null;
+ error: Error | null;
+}
+
+export interface Params {
+ userId?: number;
+ folderId?: number | null;
+}
+
+type AsyncFunc = (params: Params) => Promise;
+
+const useAsync = (
+ asyncFunc: AsyncFunc,
+ userId?: Params["userId"],
+ folderId?: Params["folderId"]
+): CustomAsyncReturns => {
+ const [isLoading, setIsLoading] = useState(false);
+ const [value, setValue] = useState(null);
+ const [error, setError] = useState(null);
+
+ const fetchData = useCallback(async () => {
+ setIsLoading(true);
+ try {
+ let result;
+ const params: Params = {};
+ if (userId !== undefined) params.userId = userId;
+ if (folderId !== undefined) params.folderId = folderId;
+
+ result = await asyncFunc(params);
+ setValue(result);
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ setError(error);
+ }
+ } finally {
+ setIsLoading(false);
+ }
+ }, [asyncFunc, userId, folderId]);
+
+ useEffect(() => {
+ fetchData();
+ }, [fetchData]);
+
+ return { value, error, isLoading };
+};
+
+export default useAsync;
diff --git a/next.config.mjs b/next.config.mjs
new file mode 100644
index 000000000..71ebab852
--- /dev/null
+++ b/next.config.mjs
@@ -0,0 +1,16 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+ reactStrictMode: true,
+ images: {
+ remotePatterns: [
+ {
+ protocol: "https",
+ hostname: "*",
+ port: "",
+ pathname: "/**",
+ },
+ ],
+ },
+};
+
+export default nextConfig;
diff --git a/package.json b/package.json
index 1ce24924f..2ede8346d 100644
--- a/package.json
+++ b/package.json
@@ -1,5 +1,5 @@
{
- "name": "fe-weekly-mission",
+ "name": "nextjs-migration",
"version": "0.1.0",
"private": true,
"scripts": {
@@ -11,7 +11,9 @@
"dependencies": {
"react": "^18",
"react-dom": "^18",
- "next": "13.5.6"
+ "next": "14.2.3",
+ "classnames": "^2.5.1",
+ "react-icons": "^5.0.1"
},
"devDependencies": {
"typescript": "^5",
@@ -19,6 +21,6 @@
"@types/react": "^18",
"@types/react-dom": "^18",
"eslint": "^8",
- "eslint-config-next": "13.5.6"
+ "eslint-config-next": "14.2.3"
}
}
diff --git a/pages/_app.tsx b/pages/_app.tsx
index 021681f4d..15f90f46b 100644
--- a/pages/_app.tsx
+++ b/pages/_app.tsx
@@ -1,6 +1,12 @@
-import '@/styles/globals.css'
-import type { AppProps } from 'next/app'
+import type { AppProps } from "next/app";
+import "@/styles/global.css";
+import Footer from "@/components/Footer";
export default function App({ Component, pageProps }: AppProps) {
- return
+ return (
+ <>
+
+
+ >
+ );
}
diff --git a/pages/_document.tsx b/pages/_document.tsx
index 54e8bf3e2..057c8a57b 100644
--- a/pages/_document.tsx
+++ b/pages/_document.tsx
@@ -1,13 +1,13 @@
-import { Html, Head, Main, NextScript } from 'next/document'
+import { Html, Head, Main, NextScript } from "next/document";
export default function Document() {
return (
-
+
- )
+ );
}
diff --git a/pages/folder.tsx b/pages/folder.tsx
new file mode 100644
index 000000000..e8b13f280
--- /dev/null
+++ b/pages/folder.tsx
@@ -0,0 +1,65 @@
+import { useEffect, useState } from "react";
+import useAsync from "@/lib/useAsync";
+import {
+ FolderData,
+ UserData,
+ getFoldersByUserId,
+ getUserById,
+} from "@/lib/api";
+import Header from "@/components/Header";
+import LinkAddForm from "@/components/LinkAddForm";
+import FoldersController from "@/components/FolderController";
+
+const SAMPLE_USER_ID = 1;
+
+function FolderPage() {
+ const [isUserLoggedIn, setIsUserLoggedIn] = useState(false);
+ const {
+ value: userProfileData,
+ isLoading: isLoadingUser,
+ error: userError,
+ } = useAsync(getUserById, SAMPLE_USER_ID);
+ const {
+ value: foldersData,
+ isLoading: isLoadingFolders,
+ error: foldersError,
+ } = useAsync(getFoldersByUserId, SAMPLE_USER_ID);
+
+ useEffect(() => {
+ if (!isLoadingUser && userProfileData) {
+ setIsUserLoggedIn(true);
+ }
+ }, [isLoadingUser, userProfileData]);
+
+ if (isLoadingUser || isLoadingFolders) {
+ return Loading...
;
+ }
+
+ if (userError || foldersError) {
+ return Error loading data.
;
+ }
+
+ if (!userProfileData || !foldersData) {
+ return No Data Available
;
+ }
+
+ return (
+ <>
+
+ {isUserLoggedIn ? (
+ <>
+
+
+ >
+ ) : (
+ 로그인해주세요.
+ )}
+ >
+ );
+}
+
+export default FolderPage;
diff --git a/pages/index.tsx b/pages/index.tsx
index 02c4dee04..15340008c 100644
--- a/pages/index.tsx
+++ b/pages/index.tsx
@@ -1,114 +1,3 @@
-import Head from 'next/head'
-import Image from 'next/image'
-import { Inter } from 'next/font/google'
-import styles from '@/styles/Home.module.css'
-
-const inter = Inter({ subsets: ['latin'] })
-
export default function Home() {
- return (
- <>
-
- Create Next App
-
-
-
-
-
-
-
- Get started by editing
- pages/index.tsx
-
-
-
-
-
-
-
-
-
-
- >
- )
+ return <>Hello>;
}
diff --git a/pages/shared.tsx b/pages/shared.tsx
new file mode 100644
index 000000000..3cc5aa4ff
--- /dev/null
+++ b/pages/shared.tsx
@@ -0,0 +1,62 @@
+import { useEffect, useState } from "react";
+import useAsync from "@/lib/useAsync";
+import { SampleFolder, SampleUser, getFolder, getUser } from "@/lib/api";
+import Header from "@/components/Header";
+import SharedLinkCards from "@/components/SharedLinkCards/SharedLinkCards";
+import UserProfileAndTitle from "@/components/UserProfileAndTitle";
+
+function SharedPage() {
+ const [isUserLoggedIn, setIsUserLoggedIn] = useState(false);
+ const {
+ value: userProfileData,
+ isLoading: isLoadingUser,
+ error: userError,
+ } = useAsync(getUser);
+ const {
+ value: folderData,
+ isLoading: isLoadingFolders,
+ error: foldersError,
+ } = useAsync(getFolder);
+
+ useEffect(() => {
+ if (!isLoadingUser && userProfileData) {
+ setIsUserLoggedIn(true);
+ }
+ }, [isLoadingUser, userProfileData]);
+
+ if (isLoadingUser || isLoadingFolders) {
+ return Loading...
;
+ }
+
+ if (userError || foldersError) {
+ return Error loading data.
;
+ }
+
+ if (!userProfileData || !folderData) {
+ return No Data Available
;
+ }
+
+ return (
+ <>
+
+ {isUserLoggedIn ? (
+ <>
+
+ {folderData.links && }
+ >
+ ) : (
+ 로그인해주세요.
+ )}
+ >
+ );
+}
+
+export default SharedPage;
diff --git a/public/Linkbrary.png b/public/Linkbrary.png
new file mode 100644
index 000000000..3d2c1e382
Binary files /dev/null and b/public/Linkbrary.png differ
diff --git a/public/card-default.png b/public/card-default.png
new file mode 100644
index 000000000..946084392
Binary files /dev/null and b/public/card-default.png differ
diff --git a/public/facebook.png b/public/facebook.png
new file mode 100644
index 000000000..dfa6671e6
Binary files /dev/null and b/public/facebook.png differ
diff --git a/public/kakaotalk.png b/public/kakaotalk.png
new file mode 100644
index 000000000..1d92c8da3
Binary files /dev/null and b/public/kakaotalk.png differ
diff --git a/public/share.png b/public/share.png
new file mode 100644
index 000000000..7031508ce
Binary files /dev/null and b/public/share.png differ
diff --git a/styles/global.css b/styles/global.css
new file mode 100644
index 000000000..698cbf86f
--- /dev/null
+++ b/styles/global.css
@@ -0,0 +1,150 @@
+* {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
+ Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
+}
+
+button {
+ cursor: pointer;
+}
+
+:root {
+ --primary: #6d5afe;
+ --red: #ff5b56;
+ --black: #111322;
+ --white: #ffffff;
+ --primary-gradient: linear-gradient(to right, var(--primary), #6ae3fe);
+ --gray-700: #3e3e43;
+ --gray-600: #9fa6b2;
+ --gray-500: #ccd5e3;
+ --gray-400: #e7effb;
+ --gray-300: #f0f6ff;
+}
+
+html,
+body,
+div,
+span,
+applet,
+object,
+iframe,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+p,
+blockquote,
+pre,
+a,
+abbr,
+acronym,
+address,
+big,
+cite,
+code,
+del,
+dfn,
+em,
+img,
+ins,
+kbd,
+q,
+s,
+samp,
+small,
+strike,
+strong,
+sub,
+sup,
+tt,
+var,
+b,
+u,
+i,
+center,
+dl,
+dt,
+dd,
+ol,
+ul,
+li,
+fieldset,
+form,
+label,
+legend,
+table,
+caption,
+tbody,
+tfoot,
+thead,
+tr,
+th,
+td,
+article,
+aside,
+canvas,
+details,
+embed,
+figure,
+figcaption,
+footer,
+header,
+hgroup,
+menu,
+nav,
+output,
+ruby,
+section,
+summary,
+time,
+mark,
+audio,
+video {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ font-size: 100%;
+ font: inherit;
+ vertical-align: baseline;
+}
+/* HTML5 display-role reset for older browsers */
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+menu,
+nav,
+section {
+ display: block;
+}
+body {
+ line-height: 1;
+}
+ol,
+ul {
+ list-style: none;
+}
+blockquote,
+q {
+ quotes: none;
+}
+blockquote:before,
+blockquote:after,
+q:before,
+q:after {
+ content: "";
+ content: none;
+}
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
diff --git a/tsconfig.json b/tsconfig.json
index 670224f3e..649790e5d 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,