diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 000000000..c9867ccd2
Binary files /dev/null and b/.DS_Store differ
diff --git a/.env b/.env
new file mode 100644
index 000000000..a33403f17
--- /dev/null
+++ b/.env
@@ -0,0 +1 @@
+REACT_APP_KAKAO_KEY = "b95afa4b872322082f58d1e2ca1623f2"
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index baa2b6655..f4561d625 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,6 +16,7 @@
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
+ "clsx": "^2.0.0",
"eslint": "^8",
"eslint-config-next": "13.5.6",
"typescript": "^5"
@@ -837,6 +838,15 @@
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
},
+ "node_modules/clsx": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz",
+ "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
diff --git a/package.json b/package.json
index 1ce24924f..83b8305ad 100644
--- a/package.json
+++ b/package.json
@@ -9,16 +9,17 @@
"lint": "next lint"
},
"dependencies": {
+ "next": "13.5.6",
"react": "^18",
- "react-dom": "^18",
- "next": "13.5.6"
+ "react-dom": "^18"
},
"devDependencies": {
- "typescript": "^5",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
+ "clsx": "^2.0.0",
"eslint": "^8",
- "eslint-config-next": "13.5.6"
+ "eslint-config-next": "13.5.6",
+ "typescript": "^5"
}
}
diff --git a/pages/_app.tsx b/pages/_app.tsx
index 021681f4d..2b4e1d3c5 100644
--- a/pages/_app.tsx
+++ b/pages/_app.tsx
@@ -1,6 +1,14 @@
-import '@/styles/globals.css'
-import type { AppProps } from 'next/app'
+import "@/src/assets/global.css";
+import type { AppProps } from "next/app";
+import Head from "next/head";
export default function App({ Component, pageProps }: AppProps) {
- return
+ return (
+ <>
+
+ linkbrary
+
+ ;
+ >
+ );
}
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.module.css b/pages/folder.module.css
new file mode 100644
index 000000000..14145798c
--- /dev/null
+++ b/pages/folder.module.css
@@ -0,0 +1,42 @@
+.root {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 4rem 3.2rem;
+}
+.addLink {
+ display: flex;
+ justify-content: center;
+ padding: 6rem 3.2rem 9rem 3.2rem;
+ background: var(--linkbrary-bg, #f0f6ff);
+
+ @media (max-width: 767px) {
+ padding-top: 2.4rem;
+ padding-bottom: 4rem;
+ }
+}
+.section {
+ display: flex;
+ flex-direction: column;
+ align-items: start;
+ gap: 4rem;
+}
+.folderSection {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 2.4rem;
+}
+.emptyLink {
+ width: 106rem;
+ padding: 4.1rem 0 3.5rem;
+ text-align: center;
+ font-size: 1.6rem;
+ @media (max-width: 1124px) {
+ width: 100%;
+ }
+ @media (max-width: 767px) {
+ width: 100%;
+ font-size: 1.4rem;
+ }
+}
diff --git a/pages/folder.tsx b/pages/folder.tsx
new file mode 100644
index 000000000..0c455ec57
--- /dev/null
+++ b/pages/folder.tsx
@@ -0,0 +1,69 @@
+import { createContext, useEffect, useState } from "react";
+import AddLink from "@/src/components/AddLink/AddLink";
+import FolderPageCards from "@/src/components/Cards/FolderPageCards";
+import FolderList from "@/src/components/FolderList/FolderList";
+import Search from "@/src/components/Search/Search";
+// import useAsync from "../hooks/useAsync";
+import { Nav, Footer } from "@/src/containers";
+import { Link, getLink } from "@/src/api/getLink";
+import { Folder, getFolder } from "@/src/api/getFolder";
+import style from "./folder.module.css";
+import { useSearchParams } from "next/navigation";
+import CurrentFolder from "@/src/components/CurrentFolder/CurrentFolder";
+
+export const FolderPageContext = createContext([]);
+
+function FolderPage() {
+ const [links, setLinks] = useState ([]);
+ const [folders, setFolders] = useState([]);
+ // const { wrappedFunction:getLinkAsync} = useAsync(getLink);
+ // const {wrappedFunction:getFolderAsync} = useAsync(getFolder);
+ const searchParams = useSearchParams();
+ const folderParam = searchParams.get("folderId");
+
+ useEffect(() => {
+ const handleLinkLoad = async () => {
+ const links = await getLink({
+ id: 1,
+ folderId: folderParam || "",
+ });
+ setLinks([...links]);
+ };
+ handleLinkLoad();
+ }, [folderParam]);
+
+ useEffect(() => {
+ const handleFolderLoad = async () => {
+ const folders = await getFolder({ id: 1 });
+ setFolders([...folders]);
+ };
+ handleFolderLoad();
+ }, []);
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ {links.length ? (
+
+ ) : (
+ 저장된 링크가 없습니다.
+ )}
+
+
+
+
+
+ >
+ );
+}
+
+export default FolderPage;
diff --git a/pages/index.tsx b/pages/index.tsx
index 02c4dee04..264fead19 100644
--- a/pages/index.tsx
+++ b/pages/index.tsx
@@ -1,114 +1,120 @@
-import Head from 'next/head'
-import Image from 'next/image'
-import { Inter } from 'next/font/google'
-import styles from '@/styles/Home.module.css'
+function HomePage() {
+ return HomePage ;
+}
-const inter = Inter({ subsets: ['latin'] })
+export default HomePage;
-export default function Home() {
- return (
- <>
-
- Create Next App
-
-
-
-
-
-
-
- Get started by editing
- pages/index.tsx
-
-
-
+// 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'] })
-
+//
+// >
+// )
+// }
diff --git a/pages/shared.jsx b/pages/shared.jsx
new file mode 100644
index 000000000..55750546f
--- /dev/null
+++ b/pages/shared.jsx
@@ -0,0 +1,51 @@
+import { useState, useEffect } from "react";
+import { getSampleFolder } from "@/src/api/sampleFolder";
+import Search from "@/src/components/Search/Search";
+import SharedPageCards from "@/src/components/Cards/SharedPageCards";
+import style from "./shared.module.css";
+import FolderInfo from "@/src/components/FolderInfo/FolderInfo";
+import { Nav, Footer } from "@/src/containers";
+
+function SharedPage() {
+ const [folderInfo, setFolderInfo] = useState({});
+ const [cards, setCards] = useState([]);
+ // const [isLoading, loadingError, getSampleFolderAsync] =
+ // useAsync(getSampleFolder);
+
+ useEffect(() => {
+ const loadLink = async () => {
+ const {
+ folder,
+ folder: { links },
+ } = await getSampleFolder();
+
+ setCards(() => {
+ return [...links];
+ });
+ setFolderInfo(() => ({ ...folder }));
+ };
+ loadLink();
+ }, []);
+
+ return (
+ <>
+
+
+
+
+
+ >
+ );
+}
+export default SharedPage;
diff --git a/pages/shared.module.css b/pages/shared.module.css
new file mode 100644
index 000000000..c343311dd
--- /dev/null
+++ b/pages/shared.module.css
@@ -0,0 +1,21 @@
+.root {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 4rem 3.2rem;
+}
+.section {
+ display: flex;
+ flex-direction: column;
+ /* justify-content: center; */
+ gap: 4rem;
+ padding: 0rem 3.2rem;
+ margin: 4rem auto;
+ align-items: center;
+}
+.folderSection {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 2.4rem;
+}
diff --git a/pages/sign.module.css b/pages/sign.module.css
new file mode 100644
index 000000000..5b0f7d825
--- /dev/null
+++ b/pages/sign.module.css
@@ -0,0 +1,70 @@
+.root {
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+ background-color: #f0f6ff;
+ font-size: 1.6rem;
+ padding-top: 16rem;
+ height: 100%;
+ padding-bottom: 11.6rem;
+}
+.container {
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+}
+.form {
+ width: 40rem;
+ display: flex;
+ flex-direction: column;
+ gap: 2.4rem;
+ margin: 3rem 0 3.2rem;
+}
+.label {
+ font-size: 1.4rem;
+ margin-bottom: 1.2rem;
+}
+.input {
+ padding: 1.8rem 3.5rem 1.8rem 1.5rem;
+ border: 0.1rem solid var(--linkbrary-gray-20);
+ border-radius: 0.8rem;
+ width: 100%;
+ margin-bottom: 0.6rem;
+ margin-top: 1.2rem;
+ font-size: 1.6rem;
+ background: var(--linkbrary-white, #fff);
+}
+.input:focus {
+ border: 0.1rem solid var(--linkbrary-primary-color, #6d6afe);
+}
+.inputWrapper {
+ position: relative;
+}
+.pwdEye {
+ position: absolute;
+ right: 1.6rem;
+ bottom: 50%;
+ transform: translateY(50%);
+ fill: white;
+ cursor: pointer;
+}
+
+.errorMessage {
+ font-size: 1.4rem;
+ color: red;
+ margin-top: 0.6rem;
+}
+
+.formButton {
+ padding: 1.6rem 2rem;
+ border: none;
+ background: var(--gra-purpleblue-to-skyblue);
+ color: var(--grey-light);
+ border-radius: 0.8rem;
+ font-size: 1.8rem;
+ cursor: pointer;
+}
+
+.redBox {
+ border-color: red;
+}
diff --git a/pages/signin.tsx b/pages/signin.tsx
new file mode 100644
index 000000000..e77a913d9
--- /dev/null
+++ b/pages/signin.tsx
@@ -0,0 +1,118 @@
+import SignHeader from "@/src/components/Sign/SignHeader";
+import SocialSign from "@/src/components/Sign/SocialSign";
+import style from "@/pages/sign.module.css";
+import { checkEmailError, checkPwdError } from "@/src/util/handleSignError";
+import { FormEvent, useRef, useState } from "react";
+import { postSignIn } from "@/src/api/postSignIn";
+import Image from "next/image";
+import EyeOffIcon from "@/src/assets/img/eye-off.svg";
+import EyeOnIcon from "@/src/assets/img/eye-on.svg";
+import clsx from "clsx";
+
+function SignInPage() {
+ const [emailErrorMessage, setEmailErrorMessage] = useState("");
+ const [pwdErrorMessage, setPwdErrorMessage] = useState("");
+ const [pwdType, setPwdType] = useState("password");
+ const emailInput = useRef("");
+ const pwdInput = useRef("");
+
+ const handleButtonClick = async (e: FormEvent) => {
+ e.preventDefault();
+
+ if (handleEmailError() && handlePwdError()) {
+ //이메일, 비밀번호가 유효한 값일 때만 api호출
+ const result = postSignIn({
+ email: emailInput.current,
+ password: pwdInput.current,
+ });
+ const data = await result;
+ if (data) {
+ setEmailErrorMessage("이메일을 확인해주세요");
+ setPwdErrorMessage("비밀번호를 확인해주세요");
+ }
+ }
+ };
+
+ const handleInputChange = (
+ e: React.ChangeEvent,
+ inputRef: React.MutableRefObject
+ ) => {
+ inputRef.current = e.target.value;
+ };
+
+ const handleEmailError = () => {
+ const errorMessage = checkEmailError(emailInput.current);
+ setEmailErrorMessage(errorMessage);
+ return !Boolean(errorMessage);
+ };
+
+ const handlePwdError = () => {
+ const errorMessage = checkPwdError(pwdInput.current);
+ setPwdErrorMessage(errorMessage);
+ return !Boolean(errorMessage);
+ };
+
+ const handlePwdEyeClick = () => {
+ setPwdType(pwdType === "email" ? "password" : "email");
+ };
+ return (
+
+ );
+}
+
+export default SignInPage;
diff --git a/pages/signup.tsx b/pages/signup.tsx
new file mode 100644
index 000000000..56c46cb55
--- /dev/null
+++ b/pages/signup.tsx
@@ -0,0 +1,165 @@
+import SignHeader from "@/src/components/Sign/SignHeader";
+import SocialSign from "@/src/components/Sign/SocialSign";
+import style from "@/pages/sign.module.css";
+import { checkEmailError, checkPwdError } from "@/src/util/handleSignError";
+import { FormEvent, useRef, useState } from "react";
+import { postSignUp } from "@/src/api/postSignUp";
+import Image from "next/image";
+import EyeOffIcon from "@/src/assets/img/eye-off.svg";
+import EyeOnIcon from "@/src/assets/img/eye-on.svg";
+import { isEmailUnique } from "@/src/api/isEmailUnique";
+import clsx from "clsx";
+
+function SignUpPage() {
+ const [emailErrorMessage, setEmailErrorMessage] = useState("");
+ const [pwdErrorMessage, setPwdErrorMessage] = useState("");
+ const [pwdCheckErrorMessage, setPwdCheckErrorMessage] = useState("");
+ const [pwdType, setPwdType] = useState("password");
+ const [pwdCheckType, setPwdCheckType] = useState("password");
+ const emailInput = useRef("");
+ const pwdInput = useRef("");
+ const pwdCheckInput = useRef("");
+
+ const handleButtonClick = async (e: FormEvent) => {
+ e.preventDefault();
+
+ if (
+ (await handleEmailError()) &&
+ handlePwdError() &&
+ handlePwdCheckError()
+ ) {
+ //이메일, 비밀번호가 유효한 값일 때만 api호출
+ postSignUp({
+ email: emailInput.current,
+ password: pwdInput.current,
+ });
+ }
+ };
+
+ const handleInputChange = (
+ e: React.ChangeEvent,
+ inputRef: React.MutableRefObject
+ ) => {
+ inputRef.current = e.target.value;
+ };
+
+ const handleEmailError = async () => {
+ let isValid = false;
+ const errorMessage = checkEmailError(emailInput.current);
+ setEmailErrorMessage(errorMessage);
+ if (!errorMessage) {
+ //이메일이 빈값이 아니고 올바른 형태일 때 중복체크, 그 전에는 하지 않음
+ if (await isEmailUnique(emailInput.current)) {
+ setEmailErrorMessage("");
+ isValid = true;
+ } else setEmailErrorMessage("이미 사용 중인 이메일입니다.");
+ }
+
+ return isValid;
+ };
+
+ const handlePwdError = () => {
+ handlePwdCheckError();
+ const errorMessage = checkPwdError(pwdInput.current);
+ setPwdErrorMessage(errorMessage);
+ return !Boolean(errorMessage);
+ };
+ const handlePwdCheckError = () => {
+ if (pwdCheckInput.current != pwdInput.current) {
+ setPwdCheckErrorMessage("비밀번호가 일치하지 않습니다.");
+ return false;
+ }
+ setPwdCheckErrorMessage("");
+ return true;
+ };
+ const handlePwdEyeClick = (
+ type: string,
+ setType: React.Dispatch>
+ ) => {
+ setType(type === "email" ? "password" : "email");
+ };
+ return (
+
+ );
+}
+
+export default SignUpPage;
diff --git a/public/favicon.ico b/public/favicon.ico
deleted file mode 100644
index 718d6fea4..000000000
Binary files a/public/favicon.ico and /dev/null differ
diff --git a/public/next.svg b/public/next.svg
deleted file mode 100644
index 5174b28c5..000000000
--- a/public/next.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/public/vercel.svg b/public/vercel.svg
deleted file mode 100644
index d2f842227..000000000
--- a/public/vercel.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/api/getFolder.ts b/src/api/getFolder.ts
new file mode 100644
index 000000000..8eca196dc
--- /dev/null
+++ b/src/api/getFolder.ts
@@ -0,0 +1,18 @@
+import { API } from "../config";
+
+export interface Folder {
+ id: number;
+ created_at: string;
+ name: string;
+ user_id: number;
+ link: {
+ count: number;
+ };
+}
+
+export async function getFolder({ id }: { id: number }) {
+ const res = await fetch(`${API.baseURL}/users/${id}/folders`);
+ const folders = await res.json();
+ if (res?.status === 200) return folders.data as Folder[] | [];
+ return [];
+}
diff --git a/src/api/getLink.ts b/src/api/getLink.ts
new file mode 100644
index 000000000..0cdfcb480
--- /dev/null
+++ b/src/api/getLink.ts
@@ -0,0 +1,26 @@
+import { API } from "../config";
+
+interface GetLinkRequest {
+ id: number;
+ folderId?: string;
+}
+
+export interface Link {
+ id: number;
+ created_at: string;
+ updated_at: null;
+ url: string;
+ title: string;
+ description: string;
+ image_source: string;
+ folder_id: number;
+}
+
+export async function getLink({ id, folderId }: GetLinkRequest) {
+ const res = await fetch(
+ `${API.baseURL}/users/${id}/links?folderId=${folderId}`
+ );
+ const links = await res.json();
+ if (res?.status === 200) return (links?.data || []) as Link[] | [];
+ return [];
+}
diff --git a/src/api/getUser.ts b/src/api/getUser.ts
new file mode 100644
index 000000000..3bed0e477
--- /dev/null
+++ b/src/api/getUser.ts
@@ -0,0 +1,17 @@
+import { API } from "../config";
+
+type GetUserResponse = {
+ id: number;
+ created_at: string;
+ name: string;
+ image_source: string;
+ email: string;
+ auth_id: string;
+};
+
+export async function getUser({ id }: { id: number }) {
+ const userRes = await fetch(`${API.users}/${id}`);
+ const userInfo = await userRes.json();
+ if (userRes?.status === 200) return userInfo.data[0] as GetUserResponse;
+ return null;
+}
diff --git a/src/api/isEmailUnique.ts b/src/api/isEmailUnique.ts
new file mode 100644
index 000000000..ef71edcf9
--- /dev/null
+++ b/src/api/isEmailUnique.ts
@@ -0,0 +1,19 @@
+import { API } from "../config";
+
+export const isEmailUnique = async (email: string) => {
+ try {
+ const response = await fetch(`${API["check-email"]}`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ email: email }),
+ });
+ if (response.status == 409) {
+ return false;
+ } else if (response.status == 200) return true;
+ else throw new Error(`${response.status}`);
+ } catch (err) {
+ console.log(err);
+ }
+};
diff --git a/src/api/postSignIn.ts b/src/api/postSignIn.ts
new file mode 100644
index 000000000..46e9accf9
--- /dev/null
+++ b/src/api/postSignIn.ts
@@ -0,0 +1,34 @@
+import { API } from "../config";
+
+interface postSignInProps {
+ email: string;
+ password: string;
+}
+
+export const postSignIn = async ({ email, password }: postSignInProps) => {
+ try {
+ const user = {
+ email: email,
+ password: password,
+ };
+ const response = await fetch(`${API["sign-in"]}`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(user),
+ });
+ console.log(response);
+ const signinResponse = await response.json();
+ if (response.status == 200) {
+ localStorage.setItem("accessToken", signinResponse.data.accessToken);
+ location.href = "/folder";
+ } else if (response.status == 400) {
+ return "이메일, 비밀번호 확인 요망";
+
+ // showErrorMessage($pwd,$pwdErrorMessage,"비밀번호를 확인해주세요.");
+ } else throw new Error(`${response.status}`);
+ } catch (err) {
+ console.log(err);
+ }
+};
diff --git a/src/api/postSignUp.ts b/src/api/postSignUp.ts
new file mode 100644
index 000000000..0bfb4fef9
--- /dev/null
+++ b/src/api/postSignUp.ts
@@ -0,0 +1,33 @@
+import { API } from "../config";
+
+interface postSignUpProps {
+ email: string;
+ password: string;
+}
+
+export const postSignUp = async ({ email, password }: postSignUpProps) => {
+ try {
+ const user = {
+ email: email,
+ password: password,
+ };
+ const response = await fetch(`${API["sign-up"]}`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(user),
+ });
+ const signinResponse = await response.json();
+ if (response.status == 200) {
+ localStorage.setItem("accessToken", signinResponse.data.accessToken);
+ location.href = "/folder";
+ } else if (response.status == 400) {
+ console.log(response);
+
+ // showErrorMessage($pwd,$pwdErrorMessage,"비밀번호를 확인해주세요.");
+ } else throw new Error(`${response.status}`);
+ } catch (err) {
+ console.log(err);
+ }
+};
diff --git a/src/api/sampleFolder.ts b/src/api/sampleFolder.ts
new file mode 100644
index 000000000..b64796685
--- /dev/null
+++ b/src/api/sampleFolder.ts
@@ -0,0 +1,29 @@
+import { API } from "../config";
+
+export interface Link {
+ id: number;
+ createdAt: string;
+ url: string;
+ title: string;
+ description: string;
+ imageSource: string;
+}
+
+export interface GetSampleFolderResponse {
+ folder: {
+ id: number;
+ name: string;
+ owner: {
+ id: string;
+ name: string;
+ profileImageSource: string;
+ };
+ links: Link[];
+ };
+}
+export async function getSampleFolder() {
+ const res = await fetch(API.sampleFolder);
+ const userFolder = await res.json();
+ if (res?.status === 200) return userFolder as GetSampleFolderResponse;
+ return [];
+}
diff --git a/src/api/sampleUser.ts b/src/api/sampleUser.ts
new file mode 100644
index 000000000..ac3237971
--- /dev/null
+++ b/src/api/sampleUser.ts
@@ -0,0 +1,9 @@
+import { API } from "../config";
+
+export async function getSampleUser() {
+ const userRes = await fetch(API.sampleUser);
+ const userInfo = await userRes.json();
+ if (userRes?.status === 200)
+ return userInfo;
+ return null;
+}
diff --git a/src/assets/color.css b/src/assets/color.css
new file mode 100644
index 000000000..fca502ec6
--- /dev/null
+++ b/src/assets/color.css
@@ -0,0 +1,14 @@
+:root {
+ --linkbrary-bg: #f0f6ff;
+ --linkbrary-primary-color: #6d6afe;
+ --gra-purpleblue-to-skyblue: linear-gradient(
+ 91deg,
+ #6d6afe 0.12%,
+ #6ae3fe 101.84%
+ );
+ --grey-light: #f5f5f5;
+ --linkbrary-gray-10: #e7effb;
+ --linkbrary-gray-20: #ccd5e3;
+ --linkbrary-gray-10: #e7effb;
+ --linkbrary-gray-100: #373740;
+}
diff --git a/src/assets/global.css b/src/assets/global.css
new file mode 100644
index 000000000..a05663d4e
--- /dev/null
+++ b/src/assets/global.css
@@ -0,0 +1,3 @@
+@import "./color.css";
+@import "./reset.css";
+@import "./index.css";
diff --git a/src/assets/img/Youtube.svg b/src/assets/img/Youtube.svg
new file mode 100644
index 000000000..f5feb870e
--- /dev/null
+++ b/src/assets/img/Youtube.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/assets/img/add.svg b/src/assets/img/add.svg
new file mode 100644
index 000000000..978ec0d02
--- /dev/null
+++ b/src/assets/img/add.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/assets/img/check.svg b/src/assets/img/check.svg
new file mode 100644
index 000000000..2ab2949b7
--- /dev/null
+++ b/src/assets/img/check.svg
@@ -0,0 +1,5 @@
+
+
+
diff --git a/src/assets/img/checkImg.svg b/src/assets/img/checkImg.svg
new file mode 100644
index 000000000..f7e725ab2
--- /dev/null
+++ b/src/assets/img/checkImg.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/assets/img/closeIcon.svg b/src/assets/img/closeIcon.svg
new file mode 100644
index 000000000..f8b85091e
--- /dev/null
+++ b/src/assets/img/closeIcon.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/assets/img/eye-off.svg b/src/assets/img/eye-off.svg
new file mode 100644
index 000000000..bec50d66f
--- /dev/null
+++ b/src/assets/img/eye-off.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/src/assets/img/eye-on.svg b/src/assets/img/eye-on.svg
new file mode 100644
index 000000000..61afee898
--- /dev/null
+++ b/src/assets/img/eye-on.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/src/assets/img/facebook.svg b/src/assets/img/facebook.svg
new file mode 100644
index 000000000..b9c9d4939
--- /dev/null
+++ b/src/assets/img/facebook.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/assets/img/google.png b/src/assets/img/google.png
new file mode 100644
index 000000000..cc94bf47d
Binary files /dev/null and b/src/assets/img/google.png differ
diff --git a/src/assets/img/instagram.svg b/src/assets/img/instagram.svg
new file mode 100644
index 000000000..0b9337b07
--- /dev/null
+++ b/src/assets/img/instagram.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/assets/img/kebab.png b/src/assets/img/kebab.png
new file mode 100644
index 000000000..88cc9bec6
Binary files /dev/null and b/src/assets/img/kebab.png differ
diff --git a/src/assets/img/link.svg b/src/assets/img/link.svg
new file mode 100644
index 000000000..0b9ab2e53
--- /dev/null
+++ b/src/assets/img/link.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/src/assets/img/logo.svg b/src/assets/img/logo.svg
new file mode 100644
index 000000000..282022090
--- /dev/null
+++ b/src/assets/img/logo.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/assets/img/modal-close.svg b/src/assets/img/modal-close.svg
new file mode 100644
index 000000000..b1d931f0b
--- /dev/null
+++ b/src/assets/img/modal-close.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/assets/img/modal-facebook.svg b/src/assets/img/modal-facebook.svg
new file mode 100644
index 000000000..9176716bd
--- /dev/null
+++ b/src/assets/img/modal-facebook.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/assets/img/modal-kakao.svg b/src/assets/img/modal-kakao.svg
new file mode 100644
index 000000000..a69c4ad01
--- /dev/null
+++ b/src/assets/img/modal-kakao.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/assets/img/modal-link-copy.svg b/src/assets/img/modal-link-copy.svg
new file mode 100644
index 000000000..16c8c60b1
--- /dev/null
+++ b/src/assets/img/modal-link-copy.svg
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/assets/img/no-image.svg b/src/assets/img/no-image.svg
new file mode 100644
index 000000000..4085188d2
--- /dev/null
+++ b/src/assets/img/no-image.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/assets/img/pacMan.gif b/src/assets/img/pacMan.gif
new file mode 100644
index 000000000..81e672433
Binary files /dev/null and b/src/assets/img/pacMan.gif differ
diff --git a/src/assets/img/pen.svg b/src/assets/img/pen.svg
new file mode 100644
index 000000000..08d895a5d
--- /dev/null
+++ b/src/assets/img/pen.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/src/assets/img/search.svg b/src/assets/img/search.svg
new file mode 100644
index 000000000..0406766fd
--- /dev/null
+++ b/src/assets/img/search.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/src/assets/img/share.svg b/src/assets/img/share.svg
new file mode 100644
index 000000000..b8bb338a0
--- /dev/null
+++ b/src/assets/img/share.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/assets/img/star.svg b/src/assets/img/star.svg
new file mode 100644
index 000000000..efd46768a
--- /dev/null
+++ b/src/assets/img/star.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/assets/img/trash.svg b/src/assets/img/trash.svg
new file mode 100644
index 000000000..710c317fa
--- /dev/null
+++ b/src/assets/img/trash.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/assets/img/twitter.svg b/src/assets/img/twitter.svg
new file mode 100644
index 000000000..14a6069a1
--- /dev/null
+++ b/src/assets/img/twitter.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/assets/index.css b/src/assets/index.css
new file mode 100644
index 000000000..d56dfe0ef
--- /dev/null
+++ b/src/assets/index.css
@@ -0,0 +1,6 @@
+html {
+ font-size: 62.5%;
+}
+body {
+ margin: 0;
+}
diff --git a/src/assets/reset.css b/src/assets/reset.css
new file mode 100644
index 000000000..ba5b5b45c
--- /dev/null
+++ b/src/assets/reset.css
@@ -0,0 +1,40 @@
+/* user agent stylesheet 초기화 */
+
+* {
+ box-sizing: border-box;
+ margin: 0;
+ font-family: "Pretendard";
+ word-break: keep-all;
+}
+
+html,
+body {
+ font-size: 62.5%;
+}
+
+a {
+ color: inherit;
+ text-decoration: none;
+ cursor: pointer;
+}
+
+input {
+ border: none;
+ padding: none;
+}
+input:focus {
+ outline: none;
+}
+input[type="search"]::-webkit-search-decoration,
+input[type="search"]::-webkit-search-cancel-button,
+input[type="search"]::-webkit-search-results-button,
+input[type="search"]::-webkit-search-results-decoration {
+ display: none;
+}
+
+button {
+ border: none;
+ padding: unset;
+ background-color: unset;
+ cursor: pointer;
+}
diff --git a/src/components/AddLink/AddLink.module.css b/src/components/AddLink/AddLink.module.css
new file mode 100644
index 000000000..b10a7e04f
--- /dev/null
+++ b/src/components/AddLink/AddLink.module.css
@@ -0,0 +1,43 @@
+.root {
+ display: flex;
+ width: 80rem;
+ height: 6.9rem;
+ padding: 1.6rem 2rem;
+ gap: 1rem;
+ border-radius: 1.5rem;
+ border: 0.1rem solid var(--linkbrary-primary-color, #6d6afe);
+ justify-content: space-between;
+ background-color: #fff;
+ @media (max-width: 1199px) {
+ width: 100%;
+ }
+ @media (max-width: 767px) {
+ width: 100%;
+ }
+}
+.input {
+ border: none;
+ background-color: #fff;
+ width: 100%;
+}
+.input:focus {
+ outline: none;
+}
+
+.button {
+ width: 8rem;
+ padding: 1rem 1.6rem;
+ border-radius: 0.8rem;
+ background: var(
+ --gra-purpleblue-to-skyblue,
+ linear-gradient(91deg, #6d6afe 0.12%, #6ae3fe 101.84%)
+ );
+ font-weight: 600;
+ color: var(--grey-light, #f5f5f5);
+}
+.section {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ gap: 1.2rem;
+}
diff --git a/src/components/AddLink/AddLink.tsx b/src/components/AddLink/AddLink.tsx
new file mode 100644
index 000000000..0c8dfd3af
--- /dev/null
+++ b/src/components/AddLink/AddLink.tsx
@@ -0,0 +1,47 @@
+import { ChangeEvent, FormEventHandler, useState } from "react";
+import style from "./AddLink.module.css";
+import linkIcon from "../../assets/img/link.svg";
+import AddLinkModal from "../Modal/AddLinkModal";
+import { Folder } from "../Modal/AddLinkModal";
+import Image from "next/image";
+
+function AddLink({ folders }: { folders: Folder[] }) {
+ const [url, setUrl] = useState("");
+ const [isModal, setIsModal] = useState(false);
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ setIsModal(true);
+ };
+
+ const handleChange = (e: ChangeEvent) => {
+ setUrl(e.target.value);
+ };
+
+ const handleExitClick = () => {
+ setIsModal(false);
+ };
+ return (
+ <>
+
+ {isModal && (
+
+ )}
+ >
+ );
+}
+export default AddLink;
diff --git a/src/components/Card/Card.module.css b/src/components/Card/Card.module.css
new file mode 100644
index 000000000..7ab5ae011
--- /dev/null
+++ b/src/components/Card/Card.module.css
@@ -0,0 +1,76 @@
+.card {
+ text-decoration: none;
+ color: black;
+}
+.container {
+ width: 34rem;
+ height: 33.4rem;
+ border-radius: 1.5rem;
+ box-shadow: 0 0.5rem 2.5rem 0 rgba(0, 0, 0, 0.08);
+}
+
+.image {
+ width: 100%;
+ height: 20rem;
+ border-top-right-radius: 1.5rem;
+ border-top-left-radius: 1.5rem;
+ background-position: center;
+ background-size: cover;
+}
+
+.hoverImage {
+ background-size: 130%;
+}
+
+.hoverBgColor {
+ background-color: #f0f6ff;
+}
+
+.explanation {
+ display: flex;
+ flex-direction: column;
+ padding: 1.5rem 2rem;
+ gap: 1rem;
+}
+
+.header {
+ display: flex;
+ font-size: 1.3rem;
+ color: #666;
+ justify-content: space-between;
+}
+
+.text {
+ font-size: 1.6rem;
+ width: 30rem;
+ line-height: 2.4rem;
+ height: 4.9rem;
+}
+
+.text div {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: 1.6rem;
+}
+
+.footer {
+ height: 1.9rem;
+ font-size: 1.4rem;
+}
+
+@media (max-width: 767px) {
+ .container {
+ width: 32.5rem;
+ height: 32.7rem;
+ }
+}
+.kebabContainer {
+ position: relative;
+}
+
+.kebab {
+ position: absolute;
+ top: 100%;
+ width: 10rem;
+}
diff --git a/src/components/Card/Card.tsx b/src/components/Card/Card.tsx
new file mode 100644
index 000000000..04d6b3225
--- /dev/null
+++ b/src/components/Card/Card.tsx
@@ -0,0 +1,100 @@
+import { ReactNode, useContext, useState } from "react";
+import { formatDate, prettyFormatTimeDiff } from "../../util/dateUtil";
+import kebabImg from "../../assets/img/kebab.png";
+import noImg from "../../assets/img/no-image.svg";
+import style from "./Card.module.css";
+import clsx from "clsx";
+import Kebab from "../Kebab/Kebab";
+import Modal from "../Modal/Modal";
+import AddLinkModal, { Folder } from "../Modal/AddLinkModal";
+import { FolderPageContext } from "@/pages/folder";
+import Image from "next/image";
+
+interface CardProps {
+ title: string;
+ description: string;
+ url: string;
+ imageSource: string;
+ createdAt: string;
+}
+function Card({ title, description, url, imageSource, createdAt }: CardProps) {
+ const [hover, setHover] = useState(false);
+ const [isShowKebab, setIsShowKebab] = useState(false);
+ const [modal, setModal] = useState(null);
+ const folders: Folder[] | null = useContext(FolderPageContext);
+
+ const handleMouseOver = () => {
+ setHover(true);
+ };
+ const handleMouseOut = () => {
+ setHover(false);
+ };
+ const handleKebabClick = (e: React.MouseEvent) => {
+ e.preventDefault();
+ isShowKebab ? setIsShowKebab(false) : setIsShowKebab(true);
+ };
+ const handleKebabBlur = () => {
+ setIsShowKebab(false);
+ };
+ const handleExitClick = () => {
+ setModal(null);
+ };
+ const handleKebabDeleteClick = () => {
+ setModal(
+
+ );
+ };
+ const handleKebabAddClick = () => {
+ setModal(
+
+ );
+ };
+
+ return (
+
+ );
+}
+
+export default Card;
diff --git a/src/components/Cards/Cards.module.css b/src/components/Cards/Cards.module.css
new file mode 100644
index 000000000..183f0baaf
--- /dev/null
+++ b/src/components/Cards/Cards.module.css
@@ -0,0 +1,20 @@
+.root {
+ display: grid;
+ width: 100%;
+ grid-template: auto / repeat(3, 34rem);
+ justify-content: center;
+ row-gap: 2.5rem;
+ column-gap: 2rem;
+}
+
+@media (max-width: 1124px) {
+ .root {
+ grid-template: auto / repeat(2, 34rem);
+ }
+}
+
+@media (max-width: 767px) {
+ .root {
+ grid-template: auto / repeat(1, 34rem);
+ }
+}
diff --git a/src/components/Cards/FolderPageCards.tsx b/src/components/Cards/FolderPageCards.tsx
new file mode 100644
index 000000000..c1a19b86e
--- /dev/null
+++ b/src/components/Cards/FolderPageCards.tsx
@@ -0,0 +1,34 @@
+import style from "./Cards.module.css";
+import Card from "../Card/Card";
+
+interface Card {
+ title: string;
+ description: string;
+ url: string;
+ image_source: string;
+ created_at: string;
+ id: number;
+}
+
+interface CardsProps {
+ cards: Card[];
+}
+function FolderPageCards({ cards }: CardsProps) {
+ return (
+
+ {cards.map((card) => {
+ return (
+
+ );
+ })}
+
+ );
+}
+export default FolderPageCards;
diff --git a/src/components/Cards/SharedPageCards.tsx b/src/components/Cards/SharedPageCards.tsx
new file mode 100644
index 000000000..f79a3e005
--- /dev/null
+++ b/src/components/Cards/SharedPageCards.tsx
@@ -0,0 +1,34 @@
+import style from "./Cards.module.css";
+import Card from "../Card/Card";
+
+interface SharePageCardsProps {
+ cards: SharePageCard[];
+}
+interface SharePageCard {
+ title: string;
+ description: string;
+ url: string;
+ imageSource: string;
+ createdAt: string;
+ id: number;
+}
+
+function SharedPageCards({ cards }: SharePageCardsProps) {
+ return (
+
+ {cards.map((card) => {
+ return (
+
+ );
+ })}
+
+ );
+}
+export default SharedPageCards;
diff --git a/src/components/CurrentFolder/CurrentFolder.module.css b/src/components/CurrentFolder/CurrentFolder.module.css
new file mode 100644
index 000000000..14b52089e
--- /dev/null
+++ b/src/components/CurrentFolder/CurrentFolder.module.css
@@ -0,0 +1,30 @@
+.root {
+ display: flex;
+ justify-content: space-between;
+ width: 100%;
+ @media (max-width: 767px) {
+ flex-direction: column;
+ gap: 1.2rem;
+ }
+}
+.folderName {
+ font-size: 2.4rem;
+ font-family: 600;
+ @media (max-width: 767px) {
+ font-size: 2rem;
+ }
+}
+.buttons button {
+ display: flex;
+ align-items: center;
+ color: var(--linkbrary-gray-60, #9fa6b2);
+ font-size: 1.4rem;
+ gap: 0.4rem;
+}
+.buttons {
+ display: flex;
+ gap: 1.2rem;
+}
+.hidden {
+ display: none;
+}
diff --git a/src/components/CurrentFolder/CurrentFolder.tsx b/src/components/CurrentFolder/CurrentFolder.tsx
new file mode 100644
index 000000000..6b768b507
--- /dev/null
+++ b/src/components/CurrentFolder/CurrentFolder.tsx
@@ -0,0 +1,81 @@
+import trashIcon from "../../assets/img/trash.svg";
+import shareIcon from "../../assets/img/share.svg";
+import penIcon from "../../assets/img/pen.svg";
+import style from "./CurrentFolder.module.css";
+import clsx from "clsx";
+import { useState } from "react";
+import { useScript } from "../../hooks/useScript";
+import Modal from "../Modal/Modal";
+import ShareModal from "../Modal/ShareModal";
+import { Folder } from "../Modal/AddLinkModal";
+import Image from "next/image";
+
+interface CurrentFolderProps {
+ folderId: string | null;
+ folders: Pick[];
+}
+
+function CurrentFolder({ folderId, folders }: CurrentFolderProps) {
+ useScript("https://developers.kakao.com/sdk/js/kakao.js");
+ const [isChangeNameModal, setIsChanageNameModal] = useState(false);
+ const [isDeleteModal, setIsDeleteNameModal] = useState(false);
+ const [isShareModal, setIsShareModal] = useState(false);
+
+ const handleChangeNameClick = () => {
+ setIsChanageNameModal(true);
+ };
+ const handleDeleteClick = () => {
+ setIsDeleteNameModal(true);
+ };
+ const handleShareClick = () => {
+ setIsShareModal(true);
+ };
+ const handleExitClick = () => {
+ setIsChanageNameModal(false);
+ setIsDeleteNameModal(false);
+ setIsShareModal(false);
+ };
+ const folderName =
+ folders.find((folder) => folder.id == folderId)?.name ?? "전체";
+ return (
+
+
{folderName}
+
+
+
+ 공유
+
+
+
+ 이름변경
+
+
+
+ 삭제
+
+
+ {isChangeNameModal && (
+
+ )}
+ {isDeleteModal && (
+
+ )}
+ {isShareModal && (
+
+ )}
+
+ );
+}
+export default CurrentFolder;
diff --git a/src/components/FolderInfo/FolderInfo.module.css b/src/components/FolderInfo/FolderInfo.module.css
new file mode 100644
index 000000000..4ac57eddc
--- /dev/null
+++ b/src/components/FolderInfo/FolderInfo.module.css
@@ -0,0 +1,43 @@
+.root {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ gap: 2rem;
+ padding-top: 2rem;
+ padding-bottom: 6rem;
+ background-color: var(--linkbrary-bg);
+}
+.root button {
+ width: 20rem;
+}
+.img {
+ width: 6rem;
+ height: 6rem;
+ border-radius: 4.7rem;
+}
+.folderName {
+ font-size: 4rem;
+ font-weight: 600;
+}
+.userName {
+ font-size: 1.6rem;
+}
+@media (max-width: 767px) {
+ .root {
+ gap: 1rem;
+ padding-top: 1rem;
+ padding-bottom: 4rem;
+ }
+ .img {
+ width: 4rem;
+ height: 4rem;
+ }
+ .userName {
+ font-size: 1.4rem;
+ }
+ .folderName {
+ font-size: 3.2rem;
+ font-weight: 400;
+ }
+}
diff --git a/src/components/FolderInfo/FolderInfo.tsx b/src/components/FolderInfo/FolderInfo.tsx
new file mode 100644
index 000000000..6120506fa
--- /dev/null
+++ b/src/components/FolderInfo/FolderInfo.tsx
@@ -0,0 +1,19 @@
+import style from "./FolderInfo.module.css";
+
+interface Props {
+ userName: string;
+ folderName: string;
+ profileImageSource: string;
+}
+
+function FolderInfo({ userName, folderName, profileImageSource }: Props) {
+ return (
+
+
+
@{userName}
+
{folderName}
+
+ );
+}
+
+export default FolderInfo;
diff --git a/src/components/FolderItem/FolderItem.module.css b/src/components/FolderItem/FolderItem.module.css
new file mode 100644
index 000000000..ccd80db16
--- /dev/null
+++ b/src/components/FolderItem/FolderItem.module.css
@@ -0,0 +1,21 @@
+.button {
+ padding: 0.8rem 1.2rem;
+ border-radius: 0.5rem;
+ border: 0.1rem solid var(--linkbrary-primary-color, #6d6afe);
+ background: #fff;
+ height: 3.6rem;
+ font-size: 1.6rem;
+ @media (max-width: 767px) {
+ padding: 0.6rem, 1rem;
+ font-size: 1.4rem;
+ }
+}
+
+.button:hover {
+ background: var(--linkbrary-gray-10, #e7effb);
+}
+
+.buttonClicked {
+ background: var(--linkbrary-primary-color, #6d6afe);
+ color: white;
+}
diff --git a/src/components/FolderItem/FolderItem.tsx b/src/components/FolderItem/FolderItem.tsx
new file mode 100644
index 000000000..df2bdc01f
--- /dev/null
+++ b/src/components/FolderItem/FolderItem.tsx
@@ -0,0 +1,29 @@
+import { useRouter } from "next/router";
+import { useSearchParams } from "next/navigation";
+import style from "./FolderItem.module.css";
+import clsx from "clsx";
+import Link from "next/link";
+
+interface Props {
+ id: string;
+ name: string;
+}
+function FolderItem({ id, name }: Props) {
+ const searchParams = useSearchParams();
+ const folderIdParam = searchParams.get("folderId");
+ const router = useRouter();
+
+ //const FOLDERID_QUERY = "?folderId=";
+ return (
+
+ {name}
+
+ );
+}
+export default FolderItem;
diff --git a/src/components/FolderList/FolderList.module.css b/src/components/FolderList/FolderList.module.css
new file mode 100644
index 000000000..a2ff19d36
--- /dev/null
+++ b/src/components/FolderList/FolderList.module.css
@@ -0,0 +1,35 @@
+.root {
+ display: flex;
+ width: 100%;
+ justify-content: space-between;
+ align-items: center;
+}
+.folders {
+ display: flex;
+ gap: 0.8rem;
+ flex-wrap: wrap;
+ align-items: center;
+}
+
+.addFolderBtn {
+ display: flex;
+ align-items: center;
+ gap: 0.4rem;
+ color: #6d6afe;
+ font-weight: 500;
+ @media (max-width: 767px) {
+ position: fixed;
+ bottom: 10.1rem;
+ left: 50%;
+ transform: translateX(-50%);
+ border-radius: 2rem;
+ border: 0.1rem solid var(--linkbrary-white, #fff);
+ background: var(--linkbrary-primary-color, #6d6afe);
+ color: white;
+ padding: 0.8rem 2.4rem;
+ }
+}
+/* .addFolderBtn img {
+ width: 1.6rem;
+ height: 1.6rem;
+} */
diff --git a/src/components/FolderList/FolderList.tsx b/src/components/FolderList/FolderList.tsx
new file mode 100644
index 000000000..33f19f82a
--- /dev/null
+++ b/src/components/FolderList/FolderList.tsx
@@ -0,0 +1,47 @@
+import style from "./FolderList.module.css";
+import FolderItem from "../FolderItem/FolderItem";
+import Modal from "../Modal/Modal";
+import { useState } from "react";
+import { Folder } from "../Modal/AddLinkModal";
+
+interface FolderListProps {
+ folders: Pick[];
+}
+
+function FolderList({ folders }: FolderListProps) {
+ const [isModal, setIsModal] = useState(false);
+ const entireFolder = [{ id: "", name: "전체" }, ...folders];
+ const handleClick = () => {
+ setIsModal(true);
+ };
+ const handleExitClick = () => {
+ setIsModal(false);
+ };
+ return (
+
+
+ {entireFolder.map((folder) => (
+
+ ))}
+
+
+
+
+ 폴더 추가 +
+
+
+ {isModal && (
+
+ )}
+
+ );
+}
+export default FolderList;
diff --git a/src/components/Kebab/Kebab.module.css b/src/components/Kebab/Kebab.module.css
new file mode 100644
index 000000000..a591836f4
--- /dev/null
+++ b/src/components/Kebab/Kebab.module.css
@@ -0,0 +1,16 @@
+.root {
+ display: flex;
+ justify-content: start;
+ flex-direction: column;
+ background: var(--gray-light-gray-00, #fff);
+ list-style: none;
+ align-self: stretch;
+ padding-left: 0;
+}
+.li {
+ padding: 0.7rem 1.2rem;
+}
+.li:hover {
+ background: var(--linkbrary-gray-10, #e7effb);
+ color: var(--linkbrary-primary-color, #6d6afe);
+}
diff --git a/src/components/Kebab/Kebab.tsx b/src/components/Kebab/Kebab.tsx
new file mode 100644
index 000000000..d31e03cb3
--- /dev/null
+++ b/src/components/Kebab/Kebab.tsx
@@ -0,0 +1,7 @@
+import { ReactNode } from "react";
+import style from "./Kebab.module.css";
+
+function Kebab({ children }: { children: ReactNode }) {
+ return ;
+}
+export default Kebab;
diff --git a/src/components/LinkButton/LinkButton.module.css b/src/components/LinkButton/LinkButton.module.css
new file mode 100644
index 000000000..09927e887
--- /dev/null
+++ b/src/components/LinkButton/LinkButton.module.css
@@ -0,0 +1,12 @@
+.blue {
+ display: block;
+ width: 8.8rem;
+ padding: 1.6rem 2rem;
+ border: none;
+ color: white;
+ background: linear-gradient(91deg, #6d6afe 0.12%, #6ae3fe 101.84%);
+ border-radius: 0.8rem;
+ font-size: 1.8rem;
+ font-weight: 600;
+ cursor: pointer;
+}
diff --git a/src/components/LinkButton/LinkButton.tsx b/src/components/LinkButton/LinkButton.tsx
new file mode 100644
index 000000000..beb74842c
--- /dev/null
+++ b/src/components/LinkButton/LinkButton.tsx
@@ -0,0 +1,15 @@
+import css from "./LinkButton.module.css";
+import Link from "next/link";
+
+interface Props {
+ url: string;
+ type: string;
+ text: string;
+}
+export function LinkButton({ url, type, text }: Props) {
+ return (
+
+ {text}
+
+ );
+}
diff --git a/src/components/Modal/AddLinkModal.module.css b/src/components/Modal/AddLinkModal.module.css
new file mode 100644
index 000000000..bfa6bec12
--- /dev/null
+++ b/src/components/Modal/AddLinkModal.module.css
@@ -0,0 +1,108 @@
+.modalWrapper {
+ position: fixed;
+ top: 0;
+ bottom: 0;
+ right: 0;
+ left: 0;
+ background-color: rgba(0, 0, 0, 0.4);
+ z-index: 1;
+}
+.root {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 2.4rem;
+ position: fixed;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ border-radius: 1.5rem;
+ border: 0.1rem solid var(--stroke-light, #dee2e6);
+ background: var(--gray-white, #fff);
+ padding: 3.2rem 4rem;
+ background-color: rgba(255, 255, 255, 1);
+}
+
+.closeModalButton {
+ position: absolute;
+ top: 2rem;
+ right: 2rem;
+ width: 2.4rem;
+}
+
+.title {
+ font-size: 2rem;
+ font-weight: 600;
+}
+.url {
+ color: var(--linkbrary-gray-60, #9fa6b2);
+ text-align: center;
+ font-size: 1.4rem;
+ line-height: 2.2rem; /* 157.143% */
+ padding-top: 0.8rem;
+}
+
+.folders {
+ display: flex;
+ flex-direction: column;
+ gap: 0.4rem;
+ align-items: start;
+ width: 25.8rem;
+}
+.folder {
+ padding: 0.8rem;
+ display: flex;
+ justify-content: space-between;
+ width: 100%;
+ cursor: pointer;
+}
+.folder:hover {
+ border-radius: 0.8rem;
+ background: var(--linkbrary-bg, #f0f6ff);
+}
+.folderContainer {
+ display: flex;
+ align-items: center;
+ gap: 0.8rem;
+}
+.folderName {
+ font-size: 1.6rem;
+ font-weight: 400;
+}
+.checkFolderName {
+ color: var(--linkbrary-primary-color, #6d6afe);
+}
+.folderCount {
+ color: var(--linkbrary-gray-60, #9fa6b2);
+ font-size: 1.4rem;
+ text-align: start;
+ width: auto;
+}
+.checkFolder {
+ border-radius: 0.8rem;
+ background: var(--linkbrary-bg, #f0f6ff);
+}
+.unCheckFolder {
+ background-color: white;
+}
+.checkImg {
+ display: none;
+ width: 1.4rem;
+ height: 1.4rem;
+}
+
+.showCheck {
+ display: block;
+}
+
+.addButton {
+ padding: 1.6rem 2rem;
+ border-radius: 0.8rem;
+ background: var(
+ --gra-purpleblue-to-skyblue,
+ linear-gradient(91deg, #6d6afe 0.12%, #6ae3fe 101.84%)
+ );
+ color: white;
+ width: 100%;
+ font-size: 1.6rem;
+}
diff --git a/src/components/Modal/AddLinkModal.tsx b/src/components/Modal/AddLinkModal.tsx
new file mode 100644
index 000000000..0a78fe755
--- /dev/null
+++ b/src/components/Modal/AddLinkModal.tsx
@@ -0,0 +1,101 @@
+import style from "./AddLinkModal.module.css";
+import closeIcon from "../../assets/img/modal-close.svg";
+import checkIcon from "../../assets/img/check.svg";
+import { useState } from "react";
+import clsx from "clsx";
+import Image from "next/image";
+
+interface FolderDataProps {
+ folderName: string;
+ folderCount: number;
+}
+
+interface ModalProps {
+ url: string;
+ folders: Folder[] | [] | null;
+ onExitClick: () => void;
+}
+
+export type Folder = {
+ id: string | number;
+ link: {
+ count: number;
+ };
+ name: string;
+};
+function FolderData({ folderName, folderCount }: FolderDataProps) {
+ const [isClick, setIsClick] = useState(false);
+ const handleClick = () => {
+ setIsClick(!isClick);
+ };
+ return (
+
+
+
+ {folderName}
+
+
{folderCount}개 링크
+
+
+
+
+ );
+}
+
+function AddLinkModal({ url, folders, onExitClick }: ModalProps) {
+ let newFolders: Folder[] = [];
+ if (folders) {
+ newFolders = [...folders];
+ newFolders.shift();
+ }
+
+ const handleExitClick = () => {
+ onExitClick();
+ };
+ return (
+
+
+
+
+
+
+
+
+ {newFolders.map((folder) => {
+ return (
+
+ );
+ })}
+
+
추가하기
+
+
+ );
+}
+
+export default AddLinkModal;
diff --git a/src/components/Modal/Modal.module.css b/src/components/Modal/Modal.module.css
new file mode 100644
index 000000000..e21ebe7e9
--- /dev/null
+++ b/src/components/Modal/Modal.module.css
@@ -0,0 +1,66 @@
+.modalWrapper {
+ position: fixed;
+ top: 0;
+ bottom: 0;
+ right: 0;
+ left: 0;
+ background-color: rgba(0, 0, 0, 0.4);
+}
+.root {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 2.4rem;
+ position: fixed;
+ left: 50%;
+ top: 50%;
+ transform: translateX(-50%) translateY(-50%);
+ border-radius: 1.5rem;
+ border: 0.1rem solid var(--stroke-light, #dee2e6);
+ background: var(--gray-white, #fff);
+ padding: 3.2rem 4rem;
+ background-color: rgba(255, 255, 255, 1);
+ z-index: 1;
+}
+
+.title {
+ font-size: 2rem;
+ font-weight: 600;
+}
+
+.closeModal img {
+ float: right;
+ width: 2.4rem;
+ position: absolute;
+ top: 2rem;
+ right: 2rem;
+}
+.p {
+ width: 28rem;
+ text-align: center;
+ font-size: 1.4rem;
+}
+.modalInput {
+ width: 28rem;
+ padding: 1.8rem 1.5rem;
+ border-radius: 0.8rem;
+ border: 0.1rem solid var(--linkbrary-gray-20, #ccd5e3);
+ background: var(--linkbrary-white, #fff);
+}
+.button {
+ padding: 1.6rem 2rem;
+ border-radius: 0.8rem;
+ color: white;
+ width: 100%;
+ font-size: 1.6rem;
+}
+.redButton {
+ background: var(--linkbrary-red, #ff5b56);
+}
+
+.blueButton {
+ background: var(
+ --gra-purpleblue-to-skyblue,
+ linear-gradient(91deg, #6d6afe 0.12%, #6ae3fe 101.84%)
+ );
+}
diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx
new file mode 100644
index 000000000..b87b6ef67
--- /dev/null
+++ b/src/components/Modal/Modal.tsx
@@ -0,0 +1,62 @@
+import style from "./Modal.module.css";
+import closeIcon from "../../assets/img/modal-close.svg";
+import clsx from "clsx";
+import Image from "next/image";
+
+// interface ModalProp {
+// color: string;
+// type: string;
+// buttonTitle: string;
+// }
+
+interface ModalType {
+ [key: string]: { color: string; type: string; buttonTitle: string };
+ // "폴더 이름 변경": ModalProp;
+ // "폴더 추가": ModalProp;
+ // "폴더 삭제": ModalProp;
+ // "링크 삭제": ModalProp;
+}
+
+const MODAL_TYPE: ModalType = {
+ "폴더 이름 변경": { color: "blue", type: "input", buttonTitle: "변경하기" },
+ "폴더 추가": { color: "blue", type: "input", buttonTitle: "추가하기" },
+ "폴더 삭제": { color: "red", type: "data", buttonTitle: "삭제하기" },
+ "링크 삭제": { color: "red", type: "data", buttonTitle: "삭제하기" },
+};
+
+interface Props {
+ title: string;
+ data: string;
+ modalFunc?: () => void;
+ onExitClick: () => void;
+}
+
+function Modal({ title, data, modalFunc, onExitClick }: Props) {
+ const handleExitClick = () => {
+ onExitClick();
+ };
+ return (
+
+
+
+
+
+
{title}
+ {MODAL_TYPE[title].type === "input" && (
+
+ )}
+ {MODAL_TYPE[title].type === "data" &&
{data}
}
+
+ {MODAL_TYPE[title].buttonTitle}
+
+
+
+ );
+}
+export default Modal;
diff --git a/src/components/Modal/ShareModal.module.css b/src/components/Modal/ShareModal.module.css
new file mode 100644
index 000000000..e539c7418
--- /dev/null
+++ b/src/components/Modal/ShareModal.module.css
@@ -0,0 +1,67 @@
+.modalWrapper {
+ position: fixed;
+ top: 0;
+ bottom: 0;
+ right: 0;
+ left: 0;
+ background-color: rgba(0, 0, 0, 0.4);
+ z-index: 1;
+}
+.root {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 2.4rem;
+ position: fixed;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ border-radius: 1.5rem;
+ border: 0.1rem solid var(--stroke-light, #dee2e6);
+ background: var(--gray-white, #fff);
+ padding: 3.2rem 4rem;
+ background-color: rgba(255, 255, 255, 1);
+}
+
+.closeModalButton {
+ position: absolute;
+ top: 2rem;
+ right: 2rem;
+ width: 2.4rem;
+}
+
+.title {
+ font-size: 2rem;
+ font-weight: 600;
+}
+.folderName {
+ color: var(--linkbrary-gray-60, #9fa6b2);
+ text-align: center;
+ font-size: 1.4rem;
+ line-height: 2.2rem; /* 157.143% */
+ padding-top: 0.8rem;
+}
+
+.shareLinks {
+ display: flex;
+ gap: 3.2rem;
+}
+.shareLinks p {
+ width: auto;
+}
+.shareLinks button {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+.toast {
+ position: fixed;
+ left: 50%;
+ top: 30%;
+ transform: translate(-50%, -50%);
+ font-size: 1.5rem;
+ background-color: var(--linkbrary-primary-color);
+ color: white;
+ padding: 1rem 1.5rem;
+ border-radius: 0.8rem;
+}
diff --git a/src/components/Modal/ShareModal.tsx b/src/components/Modal/ShareModal.tsx
new file mode 100644
index 000000000..f45d18ff2
--- /dev/null
+++ b/src/components/Modal/ShareModal.tsx
@@ -0,0 +1,49 @@
+import style from "./ShareModal.module.css";
+import closeIcon from "../../assets/img/modal-close.svg";
+import linkIcon from "../../assets/img/modal-link-copy.svg";
+import KakaoShare from "../SocialShare/KakaoShare";
+import MetaShare from "../SocialShare/MetaShare";
+import { copyClipBoard } from "../../util/copyClipBoard";
+import { useState } from "react";
+import Toast from "../Toast/Toast";
+import { useToast } from "@/src/hooks/useToast";
+import Image from "next/image";
+
+interface ShareModalProp {
+ folderName: string;
+ onExitClick: () => void;
+}
+
+function ShareModal({ folderName, onExitClick }: ShareModalProp) {
+ const { isToastPop, openToast, closeToast } = useToast();
+ const handleExitClick = () => {
+ onExitClick();
+ };
+ const handleLinkCopyClick = () => {
+ openToast();
+ copyClipBoard(window.location.href);
+ };
+ return (
+
+
+ {isToastPop &&
URL이 복사되었습니다 }
+
+ );
+}
+export default ShareModal;
diff --git a/src/components/NavProfile/NavProfile.module.css b/src/components/NavProfile/NavProfile.module.css
new file mode 100644
index 000000000..864455e67
--- /dev/null
+++ b/src/components/NavProfile/NavProfile.module.css
@@ -0,0 +1,20 @@
+.profile {
+ display: flex;
+ font-size: 1.4rem;
+ align-items: center;
+ gap: 0.9rem;
+}
+.profileImg {
+ width: 2.4rem;
+ height: 2.4rem;
+ border-radius: 4.7rem;
+}
+@media (max-width: 767px) {
+ .profileImg {
+ width: 2.8rem;
+ height: 2.8rem;
+ }
+ .email {
+ display: none;
+ }
+}
diff --git a/src/components/NavProfile/NavProfile.tsx b/src/components/NavProfile/NavProfile.tsx
new file mode 100644
index 000000000..d29b1db6b
--- /dev/null
+++ b/src/components/NavProfile/NavProfile.tsx
@@ -0,0 +1,44 @@
+import { useEffect, useState } from "react";
+import { LinkButton } from "../LinkButton/LinkButton";
+import style from "./NavProfile.module.css";
+// import useAsync from "../../hooks/useAsync";
+import { getUser } from "../../api/getUser";
+// import ErrorPage from "../../pages/ErrorPage";
+
+type GetUserResponse = {
+ id: number;
+ created_at: string;
+ name: string;
+ image_source: string;
+ email: string;
+ auth_id: string;
+};
+function NavProfile() {
+ const [userInfo, setUserInfo] = useState(null);
+ // const { isLoading, error, wrappedFunction: getUserAsync } = useAsync(getUser);
+
+ useEffect(() => {
+ const loadUser = async () => {
+ const userInfo: GetUserResponse | null = await getUser({ id: 1 });
+ setUserInfo(userInfo);
+ };
+ loadUser();
+ }, []);
+
+ // if (isLoading) return loading ;
+ // if (error) return ;
+ if (userInfo)
+ return (
+
+
+
{userInfo.email}
+
+ );
+ return ;
+}
+
+export default NavProfile;
diff --git a/src/components/Search/Search.module.css b/src/components/Search/Search.module.css
new file mode 100644
index 000000000..4b8de6e19
--- /dev/null
+++ b/src/components/Search/Search.module.css
@@ -0,0 +1,16 @@
+.root {
+ display: flex;
+ width: 100%;
+ height: 4.3rem;
+ padding: 1.5rem 1.6rem;
+ gap: 1rem;
+ border-radius: 1rem;
+ background-color: #f5f5f5;
+}
+.root input {
+ border: none;
+ background-color: #f5f5f5;
+}
+.root input:focus {
+ outline: none;
+}
diff --git a/src/components/Search/Search.tsx b/src/components/Search/Search.tsx
new file mode 100644
index 000000000..f99619059
--- /dev/null
+++ b/src/components/Search/Search.tsx
@@ -0,0 +1,12 @@
+import searchIcon from "../../assets/img/search.svg";
+import style from "./Search.module.css";
+import Image from "next/image";
+function Search() {
+ return (
+
+
+
+
+ );
+}
+export default Search;
diff --git a/src/components/Sign/SignHeader.module.css b/src/components/Sign/SignHeader.module.css
new file mode 100644
index 000000000..59c30f1a4
--- /dev/null
+++ b/src/components/Sign/SignHeader.module.css
@@ -0,0 +1,19 @@
+.root {
+ display: flex;
+ flex-direction: column;
+ gap: 1.6rem;
+}
+.logo {
+ width: 21rem;
+ height: 4rem;
+}
+
+.phrase {
+ font-weight: 400;
+}
+
+.link {
+ font-size: 600;
+ color: var(--linkbrary-primary-color);
+ text-decoration: underline;
+}
diff --git a/src/components/Sign/SignHeader.tsx b/src/components/Sign/SignHeader.tsx
new file mode 100644
index 000000000..7dd952bdd
--- /dev/null
+++ b/src/components/Sign/SignHeader.tsx
@@ -0,0 +1,27 @@
+import Link from "next/link";
+import Image from "next/image";
+import logoImg from "../../assets/img/logo.svg";
+import style from "./SignHeader.module.css";
+
+interface SignHeader {
+ message: string;
+ href: string;
+ linkMessage: string;
+}
+const SignHeader = ({ message, href, linkMessage }: SignHeader) => {
+ return (
+
+
+
+
+
+ {message}
+
+ {linkMessage}
+
+
+
+ );
+};
+
+export default SignHeader;
diff --git a/src/components/Sign/SignInput.module.css b/src/components/Sign/SignInput.module.css
new file mode 100644
index 000000000..88b3a44be
--- /dev/null
+++ b/src/components/Sign/SignInput.module.css
@@ -0,0 +1,32 @@
+.root {
+ display: flex;
+ flex-direction: column;
+}
+.label {
+ font-size: 1.4rem;
+ margin-bottom: 1.2rem;
+}
+
+.input {
+ padding: 1.8rem 1.5rem;
+ border: 0.1rem solid var(--linkbrary-gray-20);
+ border-radius: 0.8rem;
+ width: 100%;
+ margin-bottom: 0.6rem;
+}
+.inputWrapper {
+ position: relative;
+}
+.pwdEye {
+ position: absolute;
+ right: 1.6rem;
+ bottom: 50%;
+ transform: translateY(50%);
+ fill: white;
+ cursor: pointer;
+}
+
+.errorMessage {
+ font-size: 1.4rem;
+ color: red;
+}
diff --git a/src/components/Sign/SignInput.tsx b/src/components/Sign/SignInput.tsx
new file mode 100644
index 000000000..a7733d29b
--- /dev/null
+++ b/src/components/Sign/SignInput.tsx
@@ -0,0 +1,74 @@
+// import { ChangeEvent, useEffect, useState } from "react";
+// import style from "@/src/components/Sign/SignInput.module.css";
+// import Image from "next/image";
+// import EyeOffIcon from "@/src/assets/img/eye-off.svg";
+// import EyeOnIcon from "@/src/assets/img/eye-on.svg";
+
+// interface SignInputProps {
+// type: string;
+// label: string;
+// htmlFor: string;
+// isPrivate: boolean;
+// handleError: (param: string, checkVerify: boolean) => string | null;
+// inputRef: any;
+
+// }
+
+// const SignIpnut = ({
+// type = "email",
+// label,
+// htmlFor,
+// isPrivate,
+// handleError,
+
+// inputRef,
+// }: SignInputProps) => {
+// const [isError, setIsError] = useState(false);
+// const [errorMessage, setErrorMessage] = useState(null);
+// const [inputValue, setInputValue] = useState("");
+// const [inputType, setInputType] = useState(type);
+
+// const checkInputValue = () => {
+// setErrorMessage(handleError(inputValue, checkVerify));
+// if (!errorMessage) setIsError(true);
+// };
+// useEffect(() => {
+// if (checkVerify) checkInputValue();
+// }, [checkVerify]);
+
+// const handleOnChange = (e: ChangeEvent) => {
+// setInputValue(e.target.value);
+// };
+
+// const handlePwdEyeClick = () => {
+// setInputType(inputType === "email" ? "password" : "email");
+// };
+// return (
+//
+//
+// {label}
+//
+//
+//
+// {isPrivate && (
+//
+// )}
+//
+// {isError &&
{errorMessage}
}
+//
+// );
+// };
+
+// export default SignIpnut;
diff --git a/src/components/Sign/SocialSign.module.css b/src/components/Sign/SocialSign.module.css
new file mode 100644
index 000000000..9c3f73b69
--- /dev/null
+++ b/src/components/Sign/SocialSign.module.css
@@ -0,0 +1,22 @@
+.root {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ padding: 1.2rem 2.4rem;
+ border: 0.1rem solid var(--linkbrary-gray-20, #ccd5e3);
+ background: var(--linkbrary-gray-10, #e7effb);
+ color: var(--linkbrary-gray-100, #373740);
+ border-radius: 0.8rem;
+ font-size: 1.4rem;
+ font-weight: 400;
+}
+
+.links {
+ display: flex;
+ gap: 1.6rem;
+}
+.img {
+ width: 4.2rem;
+ height: 4.2rem;
+}
diff --git a/src/components/Sign/SocialSign.tsx b/src/components/Sign/SocialSign.tsx
new file mode 100644
index 000000000..1eccfa6b3
--- /dev/null
+++ b/src/components/Sign/SocialSign.tsx
@@ -0,0 +1,23 @@
+import Link from "next/link";
+import Image from "next/image";
+import googleIcon from "@/src/assets/img/google.png";
+import kakaoIcon from "@/src/assets/img/modal-kakao.svg";
+import style from "./SocialSign.module.css";
+
+const SocialSign = ({ message }: { message: string }) => {
+ return (
+
+
{message}
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default SocialSign;
diff --git a/src/components/SocialLink.tsx b/src/components/SocialLink.tsx
new file mode 100644
index 000000000..2f499018b
--- /dev/null
+++ b/src/components/SocialLink.tsx
@@ -0,0 +1,21 @@
+import Link from "next/link";
+import Image from "next/image";
+
+interface SocailLinkProp {
+ href: string;
+ iconUrl: string;
+ description: string;
+}
+
+function SocialLink({
+ link: { href, iconUrl, description },
+}: {
+ link: SocailLinkProp;
+}) {
+ return (
+
+
+
+ );
+}
+export default SocialLink;
diff --git a/src/components/SocialShare/KakaoShare.js b/src/components/SocialShare/KakaoShare.js
new file mode 100644
index 000000000..1ec51136f
--- /dev/null
+++ b/src/components/SocialShare/KakaoShare.js
@@ -0,0 +1,38 @@
+import kakaoIcon from "../../assets/img/modal-kakao.svg";
+function KakaoShare() {
+ const Kakao = window.Kakao;
+ if (!Kakao.isInitialized()) {
+ Kakao.init(process.env.REACT_APP_KAKAO_KEY);
+ }
+ const handleShareClick = () => {
+ Kakao.Share.sendDefault({
+ objectType: "feed",
+ content: {
+ title: "링크브러리",
+ description: "링크 폴더를 공유합니다.",
+ imageUrl:
+ "https://cdn.newspenguin.com/news/photo/201912/877_1419_234.jpg",
+ link: {
+ mobileWebUrl: window.location.href,
+ },
+ },
+ buttons: [
+ {
+ title: "링크브러리 방문하기",
+ link: {
+ mobileWebUrl: window.location.href,
+ },
+ },
+ ],
+ });
+ };
+
+ return (
+
+
+ 카카오톡
+
+ );
+}
+
+export default KakaoShare;
diff --git a/src/components/SocialShare/MetaShare.tsx b/src/components/SocialShare/MetaShare.tsx
new file mode 100644
index 000000000..f785090a2
--- /dev/null
+++ b/src/components/SocialShare/MetaShare.tsx
@@ -0,0 +1,17 @@
+import facebookIcon from "../../assets/img/modal-facebook.svg";
+function MetaShare() {
+ const handleShareClick = () => {
+ window.open(
+ `https://www.facebook.com/sharer/sharer.php?u=${window.location.href}`,
+ "페이스북 공유하기",
+ "width=600,height=800,location=no,status=no,scrollbars=yes"
+ );
+ };
+ return (
+
+
+ 페이스북
+
+ );
+}
+export default MetaShare;
diff --git a/src/components/Toast/Toast.module.css b/src/components/Toast/Toast.module.css
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/components/Toast/Toast.tsx b/src/components/Toast/Toast.tsx
new file mode 100644
index 000000000..7d0b348ed
--- /dev/null
+++ b/src/components/Toast/Toast.tsx
@@ -0,0 +1,24 @@
+// import { useContext } from "react";
+import style from "./Toast.module.css";
+import checkImg from "../../assets/img/checkImg.svg";
+import closeIcon from "../../assets/img/closeIcon.svg";
+import { ReactNode } from "react";
+
+interface ToastProp {
+ children: ReactNode;
+ onClick: () => void;
+}
+
+const Toast = ({ children, onClick }: ToastProp) => {
+ return (
+
+
+
{children}
+
+
+
+
+
+ );
+};
+export default Toast;
diff --git a/src/config.ts b/src/config.ts
new file mode 100644
index 000000000..77d7f8d59
--- /dev/null
+++ b/src/config.ts
@@ -0,0 +1,11 @@
+const BASE_URL = "https://bootcamp-api.codeit.kr/api";
+
+export const API = {
+ baseURL: `${BASE_URL}`,
+ sampleUser: `${BASE_URL}/sample/user`,
+ sampleFolder: `${BASE_URL}/sample/folder`,
+ users: `${BASE_URL}/users`,
+ "sign-in": `${BASE_URL}/sign-in`,
+ "sign-up": `${BASE_URL}/sign-up`,
+ "check-email": `${BASE_URL}/check-email`,
+};
diff --git a/src/containers/Footer/Footer.module.css b/src/containers/Footer/Footer.module.css
new file mode 100644
index 000000000..048c7af68
--- /dev/null
+++ b/src/containers/Footer/Footer.module.css
@@ -0,0 +1,39 @@
+.root {
+ display: flex;
+ padding: 3.2rem 1.4rem 6.4rem;
+ background-color: black;
+ justify-content: space-around;
+ align-items: center;
+ margin-top: 6rem;
+}
+.since {
+ color: #676767;
+ font-size: 1.6rem;
+ grid-area: since;
+}
+.extraSites {
+ display: flex;
+ gap: 1.3rem;
+ grid-area: extraSites;
+}
+.extraSites a {
+ color: #cfcfcf;
+ text-decoration: none;
+ font-size: 1.6rem;
+}
+.icons {
+ display: flex;
+ gap: 1.2rem;
+ grid-area: icons;
+}
+
+@media (max-width: 767px) {
+ .root {
+ display: grid;
+ margin-top: 4rem;
+ grid-template-areas:
+ "extraSites icons"
+ "since .";
+ gap: 3rem;
+ }
+}
diff --git a/src/containers/Footer/Footer.tsx b/src/containers/Footer/Footer.tsx
new file mode 100644
index 000000000..dd8aad617
--- /dev/null
+++ b/src/containers/Footer/Footer.tsx
@@ -0,0 +1,45 @@
+import css from "./Footer.module.css";
+import youtubeImg from "../../assets/img/Youtube.svg";
+import instagramImg from "../../assets/img/instagram.svg";
+import twitterImg from "../../assets/img/twitter.svg";
+import facebookImg from "../../assets/img/facebook.svg";
+import SocialLink from "../../components/SocialLink";
+import Link from "next/link";
+
+const SOCIAL_LINKS = [
+ {
+ href: "https://www.facebook.com/",
+ iconUrl: facebookImg,
+ description: "facebook icon",
+ },
+ {
+ href: "https://twitter.com/",
+ iconUrl: twitterImg,
+ description: "twiiter icon",
+ },
+ {
+ href: "https://www.youtube.com/",
+ iconUrl: youtubeImg,
+ description: "youtube icon",
+ },
+ {
+ href: "https://www.instagram.com/",
+ iconUrl: instagramImg,
+ description: "instagram icon",
+ },
+];
+const socialLinks = SOCIAL_LINKS.map((link) => {
+ return ;
+});
+export function Footer() {
+ return (
+
+
©codeit - 2023
+
+ Privacy Policy
+ FAQ
+
+
{socialLinks}
+
+ );
+}
diff --git a/src/containers/Nav/Nav.module.css b/src/containers/Nav/Nav.module.css
new file mode 100644
index 000000000..5c52808f0
--- /dev/null
+++ b/src/containers/Nav/Nav.module.css
@@ -0,0 +1,40 @@
+.root {
+ position: sticky;
+ top: 0;
+ display: flex;
+ justify-content: space-between;
+ background-color: var(--linkbrary-bg);
+ padding: 2rem 10rem;
+}
+.folder {
+ position: static;
+}
+.logo {
+ width: 13.3rem;
+ height: 2.4rem;
+}
+.profile {
+ display: flex;
+ font-size: 1.4rem;
+ align-items: center;
+ gap: 0.9rem;
+}
+.profileImg {
+ width: 2.4rem;
+ height: 2.4rem;
+ border-radius: 4.7rem;
+}
+
+@media (max-width: 767px) {
+ .root {
+ padding: 1.8rem 3.2rem 1.7rem;
+ }
+
+ .profileImg {
+ width: 2.8rem;
+ height: 2.8rem;
+ }
+ .email {
+ display: none;
+ }
+}
diff --git a/src/containers/Nav/Nav.tsx b/src/containers/Nav/Nav.tsx
new file mode 100644
index 000000000..983bc0d00
--- /dev/null
+++ b/src/containers/Nav/Nav.tsx
@@ -0,0 +1,20 @@
+import logoImg from "../../assets/img/logo.svg";
+import style from "./Nav.module.css";
+import NavProfile from "../../components/NavProfile/NavProfile";
+import { useRouter } from "next/router";
+import clsx from "clsx";
+import Link from "next/link";
+import Image from "next/image";
+
+export function Nav() {
+ const location = useRouter();
+ const isFolder = location.pathname === "/folder";
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/containers/index.js b/src/containers/index.js
new file mode 100644
index 000000000..3288dec56
--- /dev/null
+++ b/src/containers/index.js
@@ -0,0 +1,2 @@
+export * from "./Nav/Nav";
+export * from "./Footer/Footer";
diff --git a/src/hooks/useAsync.tsx b/src/hooks/useAsync.tsx
new file mode 100644
index 000000000..fb2d7f054
--- /dev/null
+++ b/src/hooks/useAsync.tsx
@@ -0,0 +1,31 @@
+import { useState } from "react";
+
+interface UseAsyncResponse {
+ wrappedFunction: (param?: object) => object;
+ error: Error | null;
+ isLoading: boolean;
+}
+
+const useAsync = (
+ asyncFunction: (param?: object) => Promise
+): UseAsyncResponse => {
+ const [isLoading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const wrappedFunction = async (param?: object) => {
+ setLoading(true);
+ setError(null);
+ try {
+ const res = await asyncFunction(param);
+ return res;
+ } catch (error) {
+ if (error instanceof Error) setError(error);
+ } finally {
+ setLoading(false);
+ return undefined;
+ }
+ };
+ return { isLoading, error, wrappedFunction };
+};
+
+export default useAsync;
diff --git a/src/hooks/useScript.tsx b/src/hooks/useScript.tsx
new file mode 100644
index 000000000..427d12ee1
--- /dev/null
+++ b/src/hooks/useScript.tsx
@@ -0,0 +1,15 @@
+import { useEffect } from "react";
+export function useScript(url: string) {
+ useEffect(() => {
+ const script = document.createElement("script");
+
+ script.src = url;
+ script.async = true;
+
+ document.body.appendChild(script);
+
+ return () => {
+ document.body.removeChild(script);
+ };
+ }, [url]);
+}
diff --git a/src/hooks/useToast.tsx b/src/hooks/useToast.tsx
new file mode 100644
index 000000000..91c32aa25
--- /dev/null
+++ b/src/hooks/useToast.tsx
@@ -0,0 +1,26 @@
+import { useState, useEffect } from "react";
+
+const useToast = () => {
+ const [isToastPop, setIsToastPop] = useState(false);
+
+ const openToast = () => {
+ setIsToastPop(true);
+ };
+
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ setIsToastPop(false);
+ }, 5000);
+ return () => {
+ clearTimeout(timer);
+ };
+ }, [isToastPop]);
+
+ const closeToast = () => {
+ setIsToastPop(false);
+ };
+
+ return { isToastPop, openToast, closeToast };
+};
+
+export { useToast };
diff --git a/src/util/copyClipBoard.ts b/src/util/copyClipBoard.ts
new file mode 100644
index 000000000..dd8bc35d0
--- /dev/null
+++ b/src/util/copyClipBoard.ts
@@ -0,0 +1,7 @@
+export async function copyClipBoard(text: string) {
+ try {
+ await navigator.clipboard.writeText(text);
+ } catch (error) {
+ alert("복사 실패!");
+ }
+}
diff --git a/src/util/dateUtil.ts b/src/util/dateUtil.ts
new file mode 100644
index 000000000..ce4db260f
--- /dev/null
+++ b/src/util/dateUtil.ts
@@ -0,0 +1,24 @@
+export function formatDate(date: Date) {
+ return `${date.getFullYear()}. ${date.getMonth() + 1}. ${date.getDate()}`;
+}
+function getTimeDiff(createdAt: Date) {
+ const now: Date = new Date();
+ return Math.floor(
+ (now.getMilliseconds() - createdAt.getMilliseconds()) / (1000 * 60)
+ ); //분단위
+}
+export function prettyFormatTimeDiff(createdAt: Date) {
+ const timeDiff = getTimeDiff(createdAt);
+ if (timeDiff < 2) return "1 minute ago";
+ if (timeDiff <= 59) return `${timeDiff} minutes ago`;
+ if (timeDiff < 2 * 60) return `1 hour ago`;
+ if (timeDiff <= 60 * 23) return `${Math.floor(timeDiff / 60)} hours ago`;
+ if (timeDiff < 60 * 25) return `1 day ago`;
+ if (timeDiff <= 60 * 24 * 30)
+ return `${Math.floor(timeDiff / (60 * 24))} days ago`;
+ if (timeDiff < 60 * 24 * 32) return `1 month ago`;
+ if (timeDiff <= 60 * 24 * 31 * 11)
+ return `${Math.floor(timeDiff / (60 * 24 * 31))} months ago`;
+ if (timeDiff < 60 * 24 * 31 * 13) return `1 year ago`;
+ return `${Math.floor(timeDiff / (60 * 24 * 31 * 12))} years ago`;
+}
diff --git a/src/util/handleSignError.ts b/src/util/handleSignError.ts
new file mode 100644
index 000000000..e9feb6461
--- /dev/null
+++ b/src/util/handleSignError.ts
@@ -0,0 +1,15 @@
+const REGEMAIL = /^[A-Za-z0-9\-]+@[A-Za-z0-9]+\.[a-z]/;
+const REGPWD = /(?=.*[a-zA-Z])(?=.*[0-9])[a-zA-Z0-9]{8,}/;
+
+export function checkEmailError(value: string) {
+ if (value == "") return "이메일을 입력해주세요.";
+ else if (!REGEMAIL.test(value)) return "올바른 이메일 주소가 아닙니다.";
+ return "";
+}
+
+export function checkPwdError(value: string) {
+ if (value === "") return "비밀번호를 입력해주세요.";
+ else if (!REGPWD.test(value))
+ return "비밀번호는 영문, 숫자 조합 8자 이상 입력해주세요.";
+ return "";
+}
diff --git a/styles/Home.module.css b/styles/Home.module.css
deleted file mode 100644
index 6676d2c66..000000000
--- a/styles/Home.module.css
+++ /dev/null
@@ -1,229 +0,0 @@
-.main {
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- align-items: center;
- padding: 6rem;
- min-height: 100vh;
-}
-
-.description {
- display: inherit;
- justify-content: inherit;
- align-items: inherit;
- font-size: 0.85rem;
- max-width: var(--max-width);
- width: 100%;
- z-index: 2;
- font-family: var(--font-mono);
-}
-
-.description a {
- display: flex;
- justify-content: center;
- align-items: center;
- gap: 0.5rem;
-}
-
-.description p {
- position: relative;
- margin: 0;
- padding: 1rem;
- background-color: rgba(var(--callout-rgb), 0.5);
- border: 1px solid rgba(var(--callout-border-rgb), 0.3);
- border-radius: var(--border-radius);
-}
-
-.code {
- font-weight: 700;
- font-family: var(--font-mono);
-}
-
-.grid {
- display: grid;
- grid-template-columns: repeat(4, minmax(25%, auto));
- max-width: 100%;
- width: var(--max-width);
-}
-
-.card {
- padding: 1rem 1.2rem;
- border-radius: var(--border-radius);
- background: rgba(var(--card-rgb), 0);
- border: 1px solid rgba(var(--card-border-rgb), 0);
- transition: background 200ms, border 200ms;
-}
-
-.card span {
- display: inline-block;
- transition: transform 200ms;
-}
-
-.card h2 {
- font-weight: 600;
- margin-bottom: 0.7rem;
-}
-
-.card p {
- margin: 0;
- opacity: 0.6;
- font-size: 0.9rem;
- line-height: 1.5;
- max-width: 30ch;
-}
-
-.center {
- display: flex;
- justify-content: center;
- align-items: center;
- position: relative;
- padding: 4rem 0;
-}
-
-.center::before {
- background: var(--secondary-glow);
- border-radius: 50%;
- width: 480px;
- height: 360px;
- margin-left: -400px;
-}
-
-.center::after {
- background: var(--primary-glow);
- width: 240px;
- height: 180px;
- z-index: -1;
-}
-
-.center::before,
-.center::after {
- content: '';
- left: 50%;
- position: absolute;
- filter: blur(45px);
- transform: translateZ(0);
-}
-
-.logo {
- position: relative;
-}
-/* Enable hover only on non-touch devices */
-@media (hover: hover) and (pointer: fine) {
- .card:hover {
- background: rgba(var(--card-rgb), 0.1);
- border: 1px solid rgba(var(--card-border-rgb), 0.15);
- }
-
- .card:hover span {
- transform: translateX(4px);
- }
-}
-
-@media (prefers-reduced-motion) {
- .card:hover span {
- transform: none;
- }
-}
-
-/* Mobile */
-@media (max-width: 700px) {
- .content {
- padding: 4rem;
- }
-
- .grid {
- grid-template-columns: 1fr;
- margin-bottom: 120px;
- max-width: 320px;
- text-align: center;
- }
-
- .card {
- padding: 1rem 2.5rem;
- }
-
- .card h2 {
- margin-bottom: 0.5rem;
- }
-
- .center {
- padding: 8rem 0 6rem;
- }
-
- .center::before {
- transform: none;
- height: 300px;
- }
-
- .description {
- font-size: 0.8rem;
- }
-
- .description a {
- padding: 1rem;
- }
-
- .description p,
- .description div {
- display: flex;
- justify-content: center;
- position: fixed;
- width: 100%;
- }
-
- .description p {
- align-items: center;
- inset: 0 0 auto;
- padding: 2rem 1rem 1.4rem;
- border-radius: 0;
- border: none;
- border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
- background: linear-gradient(
- to bottom,
- rgba(var(--background-start-rgb), 1),
- rgba(var(--callout-rgb), 0.5)
- );
- background-clip: padding-box;
- backdrop-filter: blur(24px);
- }
-
- .description div {
- align-items: flex-end;
- pointer-events: none;
- inset: auto 0 0;
- padding: 2rem;
- height: 200px;
- background: linear-gradient(
- to bottom,
- transparent 0%,
- rgb(var(--background-end-rgb)) 40%
- );
- z-index: 1;
- }
-}
-
-/* Tablet and Smaller Desktop */
-@media (min-width: 701px) and (max-width: 1120px) {
- .grid {
- grid-template-columns: repeat(2, 50%);
- }
-}
-
-@media (prefers-color-scheme: dark) {
- .vercelLogo {
- filter: invert(1);
- }
-
- .logo {
- filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);
- }
-}
-
-@keyframes rotate {
- from {
- transform: rotate(360deg);
- }
- to {
- transform: rotate(0deg);
- }
-}
diff --git a/styles/globals.css b/styles/globals.css
deleted file mode 100644
index d4f491e15..000000000
--- a/styles/globals.css
+++ /dev/null
@@ -1,107 +0,0 @@
-:root {
- --max-width: 1100px;
- --border-radius: 12px;
- --font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono',
- 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro',
- 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace;
-
- --foreground-rgb: 0, 0, 0;
- --background-start-rgb: 214, 219, 220;
- --background-end-rgb: 255, 255, 255;
-
- --primary-glow: conic-gradient(
- from 180deg at 50% 50%,
- #16abff33 0deg,
- #0885ff33 55deg,
- #54d6ff33 120deg,
- #0071ff33 160deg,
- transparent 360deg
- );
- --secondary-glow: radial-gradient(
- rgba(255, 255, 255, 1),
- rgba(255, 255, 255, 0)
- );
-
- --tile-start-rgb: 239, 245, 249;
- --tile-end-rgb: 228, 232, 233;
- --tile-border: conic-gradient(
- #00000080,
- #00000040,
- #00000030,
- #00000020,
- #00000010,
- #00000010,
- #00000080
- );
-
- --callout-rgb: 238, 240, 241;
- --callout-border-rgb: 172, 175, 176;
- --card-rgb: 180, 185, 188;
- --card-border-rgb: 131, 134, 135;
-}
-
-@media (prefers-color-scheme: dark) {
- :root {
- --foreground-rgb: 255, 255, 255;
- --background-start-rgb: 0, 0, 0;
- --background-end-rgb: 0, 0, 0;
-
- --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
- --secondary-glow: linear-gradient(
- to bottom right,
- rgba(1, 65, 255, 0),
- rgba(1, 65, 255, 0),
- rgba(1, 65, 255, 0.3)
- );
-
- --tile-start-rgb: 2, 13, 46;
- --tile-end-rgb: 2, 5, 19;
- --tile-border: conic-gradient(
- #ffffff80,
- #ffffff40,
- #ffffff30,
- #ffffff20,
- #ffffff10,
- #ffffff10,
- #ffffff80
- );
-
- --callout-rgb: 20, 20, 20;
- --callout-border-rgb: 108, 108, 108;
- --card-rgb: 100, 100, 100;
- --card-border-rgb: 200, 200, 200;
- }
-}
-
-* {
- box-sizing: border-box;
- padding: 0;
- margin: 0;
-}
-
-html,
-body {
- max-width: 100vw;
- overflow-x: hidden;
-}
-
-body {
- color: rgb(var(--foreground-rgb));
- background: linear-gradient(
- to bottom,
- transparent,
- rgb(var(--background-end-rgb))
- )
- rgb(var(--background-start-rgb));
-}
-
-a {
- color: inherit;
- text-decoration: none;
-}
-
-@media (prefers-color-scheme: dark) {
- html {
- color-scheme: dark;
- }
-}
diff --git a/tsconfig.json b/tsconfig.json
index 670224f3e..b75a7e816 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -17,6 +17,6 @@
"@/*": ["./*"]
}
},
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
+ "include": ["next-env.d.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}