diff --git a/8-sprint-fe/next.config.mjs b/8-sprint-fe/next.config.mjs
index 4678774e6..bb38a264d 100644
--- a/8-sprint-fe/next.config.mjs
+++ b/8-sprint-fe/next.config.mjs
@@ -1,4 +1,21 @@
/** @type {import('next').NextConfig} */
-const nextConfig = {};
+const nextConfig = {
+ images: {
+ remotePatterns: [
+ {
+ protocol: "https",
+ hostname: "sprint-fe-project.s3.ap-northeast-2.amazonaws.com",
+ },
+ {
+ protocol: "https",
+ hostname: "st.depositphotos.com",
+ },
+ {
+ protocol: "https",
+ hostname: "i.pinimg.com",
+ },
+ ],
+ },
+};
export default nextConfig;
diff --git a/8-sprint-fe/public/btn_visibility_off_24px.svg b/8-sprint-fe/public/btn_visibility_off_24px.svg
new file mode 100644
index 000000000..2b7b3b1c4
--- /dev/null
+++ b/8-sprint-fe/public/btn_visibility_off_24px.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/8-sprint-fe/public/btn_visibility_on_24px.svg b/8-sprint-fe/public/btn_visibility_on_24px.svg
new file mode 100644
index 000000000..35a75305e
--- /dev/null
+++ b/8-sprint-fe/public/btn_visibility_on_24px.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/8-sprint-fe/public/ic_google.svg b/8-sprint-fe/public/ic_google.svg
new file mode 100644
index 000000000..237b3a678
--- /dev/null
+++ b/8-sprint-fe/public/ic_google.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/8-sprint-fe/public/ic_kakao.svg b/8-sprint-fe/public/ic_kakao.svg
new file mode 100644
index 000000000..dfed416d0
--- /dev/null
+++ b/8-sprint-fe/public/ic_kakao.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/8-sprint-fe/src/api/fetchClient.js b/8-sprint-fe/src/api/fetchClient.js
new file mode 100644
index 000000000..694ed04b1
--- /dev/null
+++ b/8-sprint-fe/src/api/fetchClient.js
@@ -0,0 +1,70 @@
+const URL = `${process.env.NEXT_PUBLIC_CODEIT_URL}`;
+
+// 회원가입
+export async function fetchSignUp({
+ email,
+ nickname,
+ password,
+ passwordConfirmation,
+}) {
+ const response = await fetch(`${URL}/auth/signUp`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ cache: "force-cache",
+ body: JSON.stringify({
+ email,
+ nickname,
+ password,
+ passwordConfirmation,
+ }),
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text(); // JSON이 아닐 수도 있으니 text로
+ throw new Error(errorText);
+ }
+
+ return await response.json();
+}
+
+// 로그인
+export async function fetchLogin({ email, password }) {
+ const response = await fetch(`${URL}/auth/signIn`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ cache: "force-cache",
+ body: JSON.stringify({
+ email,
+ password,
+ }),
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error("로그인에 실패했습니다.");
+ }
+
+ return await response.json();
+}
+
+// get me
+export async function fetchGetMe(token) {
+ const response = await fetch(`${URL}/users/me`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error("로그인에 실패했습니다.");
+ }
+
+ return await response.json();
+}
diff --git a/8-sprint-fe/src/api/fetchProducts.js b/8-sprint-fe/src/api/fetchProducts.js
new file mode 100644
index 000000000..5793bf1e8
--- /dev/null
+++ b/8-sprint-fe/src/api/fetchProducts.js
@@ -0,0 +1,25 @@
+const URL = `${process.env.NEXT_PUBLIC_CODEIT_URL}/products`;
+
+// products list get
+export const fetchProducts = async ({
+ page = 1,
+ pageSize = 10,
+ orderBy = "recent",
+}) => {
+ const response = await fetch(
+ `${URL}?page=${page}&pageSize=${pageSize}&orderBy=${orderBy}`
+ );
+ if (!response.ok) {
+ throw new Error("서버에서 데이터를 가져오는데 실패했습니다.");
+ }
+ return await response.json();
+};
+
+// product get
+export const fetchProduct = async ({ id }) => {
+ const response = await fetch(`${URL}/${id}`);
+ if (!response.ok) {
+ throw new Error("서버에서 데이터를 가져오는데 실패했습니다.");
+ }
+ return await response.json();
+};
diff --git a/8-sprint-fe/src/api/userService.js b/8-sprint-fe/src/api/userService.js
new file mode 100644
index 000000000..aa9dfc236
--- /dev/null
+++ b/8-sprint-fe/src/api/userService.js
@@ -0,0 +1,50 @@
+import { cookieFetch } from "@/lib/fetchClient";
+
+// FormData 전용 fetch 함수 (Content-Type 헤더 없음)
+const formDataFetch = async (url, options = {}) => {
+ const baseURL = process.env.NEXT_PUBLIC_API_URL;
+ const defaultOptions = {
+ // 쿠키 전송을 위한 설정
+ credentials: "include",
+ // 서버 컴포넌트에서도 매번 재검증
+ cache: "no-store",
+ };
+
+ const mergedOptions = {
+ ...defaultOptions,
+ ...options,
+ };
+
+ const response = await fetch(`${baseURL}${url}`, mergedOptions);
+
+ if (!response.ok) {
+ throw new Error(`API error: ${response.status}`);
+ }
+
+ try {
+ return await response.json();
+ } catch (e) {
+ return { status: response.status, ok: response.ok };
+ }
+};
+
+export const userService = {
+ // 사용자 정보 요청
+ getMe: () => cookieFetch("/users/me"),
+
+ // 사용자 링크 요청
+ getMyLinks: () => cookieFetch("/users/me/links"),
+
+ // 사용자 정보 업데이트 (multipart/form-data)
+ updateMe: (formData) =>
+ formDataFetch("/users/me", {
+ method: "PATCH",
+ body: formData,
+ }),
+
+ // 링크 삭제
+ deleteLink: (linkId) =>
+ cookieFetch(`/users/me/links/${linkId}`, {
+ method: "DELETE",
+ }),
+};
diff --git a/8-sprint-fe/src/app/(auth)/_components/LoginPage.jsx b/8-sprint-fe/src/app/(auth)/_components/LoginPage.jsx
new file mode 100644
index 000000000..59d34864a
--- /dev/null
+++ b/8-sprint-fe/src/app/(auth)/_components/LoginPage.jsx
@@ -0,0 +1,108 @@
+"use client";
+import { AuthLogo } from "@/app/(components)/atoms/Logo";
+import OAuth from "@/app/(components)/atoms/OAuth";
+import PasswordInput from "@/app/(components)/atoms/PasswordInput";
+import TextInput from "@/app/(components)/atoms/TextInput";
+import Link from "next/link";
+import React, { useState } from "react";
+import { validateEmail, validatePassword } from "./validator";
+import { useAuth } from "@/providers/AuthProvider";
+import { useRouter } from "next/navigation";
+
+const LoginPage = () => {
+ const [value, setValue] = useState({
+ email: "",
+ password: "",
+ });
+ const [errors, setErrors] = useState({ email: "", password: "" });
+ const { login } = useAuth();
+ const router = useRouter();
+
+ const handleEmailChange = (e) => {
+ const val = e.target.value;
+ setValue((prev) => ({
+ ...prev,
+ email: val,
+ }));
+ setErrors((prev) => ({ ...prev, email: validateEmail(val) }));
+ };
+
+ const handlePasswordChange = (e) => {
+ const val = e.target.value;
+ setValue((prev) => ({
+ ...prev,
+ password: val,
+ }));
+ setErrors((prev) => ({ ...prev, password: validatePassword(val) }));
+ };
+
+ // 버튼 활성화 조건
+ const isFormValid =
+ value.email !== "" &&
+ value.password !== "" &&
+ !errors.email &&
+ !errors.password;
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ if (!isFormValid) {
+ console.log("invalidate");
+ return;
+ }
+
+ try {
+ await login(value);
+
+ alert("로그인에 성공했습니다.");
+ router.push("/items");
+ } catch (error) {
+ alert(error.message || "로그인에 실패했습니다.");
+ }
+ };
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+};
+
+export default LoginPage;
diff --git a/8-sprint-fe/src/app/(auth)/_components/SignUpPage.jsx b/8-sprint-fe/src/app/(auth)/_components/SignUpPage.jsx
new file mode 100644
index 000000000..a925dace4
--- /dev/null
+++ b/8-sprint-fe/src/app/(auth)/_components/SignUpPage.jsx
@@ -0,0 +1,167 @@
+"use client";
+import { AuthLogo } from "@/app/(components)/atoms/Logo";
+import OAuth from "@/app/(components)/atoms/OAuth";
+import PasswordInput from "@/app/(components)/atoms/PasswordInput";
+import TextInput from "@/app/(components)/atoms/TextInput";
+import Link from "next/link";
+import React, { useState } from "react";
+import {
+ validateEmail,
+ validateNickname,
+ validatePassword,
+ validatePasswordChecker,
+} from "./validator";
+import { useAuth } from "@/providers/AuthProvider";
+import { useRouter } from "next/navigation";
+
+const SignUpPage = () => {
+ const [user, setUser] = useState({
+ email: "",
+ nickname: "",
+ password: "",
+ passwordConfirmation: "",
+ });
+ const [errors, setErrors] = useState({
+ email: "",
+ nickname: "",
+ password: "",
+ passwordConfirmation: "",
+ });
+ const { signUp } = useAuth();
+ const router = useRouter();
+
+ const handleEmailChange = (e) => {
+ const value = e.target.value;
+ setUser((prev) => ({
+ ...prev,
+ email: value,
+ }));
+ setErrors((prev) => ({ ...prev, email: validateEmail(value) }));
+ };
+
+ const handleNicknameChange = (e) => {
+ const value = e.target.value;
+ setUser((prev) => ({
+ ...prev,
+ nickname: value,
+ }));
+ setErrors((prev) => ({ ...prev, nickname: validateNickname(value) }));
+ };
+
+ const handlePasswordChange = (e) => {
+ const value = e.target.value;
+ setUser((prev) => ({
+ ...prev,
+ password: value,
+ }));
+ setErrors((prev) => ({ ...prev, password: validatePassword(value) }));
+ };
+
+ const handlePasswordCheckerChange = (e) => {
+ const value = e.target.value;
+ setUser((prev) => ({
+ ...prev,
+ passwordConfirmation: value,
+ }));
+ setErrors((prev) => ({
+ ...prev,
+ passwordConfirmation: validatePasswordChecker(value, user.password),
+ }));
+ };
+
+ // 버튼 활성화 조건
+ const isFormValid =
+ user.email !== "" &&
+ user.password !== "" &&
+ user.nickname !== "" &&
+ user.passwordConfirmation !== "" &&
+ !errors.email &&
+ !errors.password &&
+ !errors.nickname &&
+ !errors.passwordConfirmation;
+
+ // 폼 제출
+ const handleSubmit = async (e) => {
+ e.preventDefault(); // 페이지 새로고침 방지
+
+ if (!isFormValid) {
+ console.log("invalidate");
+ return;
+ }
+
+ try {
+ await signUp(user);
+
+ // 회원가입 성공 후 처리
+ alert("회원가입에 성공했습니다.");
+ router.push("/items");
+ } catch (error) {
+ alert(error.message || "회원가입에 실패했습니다.");
+ }
+ };
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+};
+
+export default SignUpPage;
diff --git a/8-sprint-fe/src/app/(auth)/_components/validator.js b/8-sprint-fe/src/app/(auth)/_components/validator.js
new file mode 100644
index 000000000..f4027e7c0
--- /dev/null
+++ b/8-sprint-fe/src/app/(auth)/_components/validator.js
@@ -0,0 +1,44 @@
+const emailPattern = /^[A-Za-z0-9_\.\-]+@[A-Za-z0-9\-]+\.[A-za-z0-9\-]+/; // 이메일 패턴
+const MIN_PASSWORD_LENGTH = 8; // 비밀번호 최소길이
+
+export function validateEmail(val) {
+ if (!val) return "이메일을 입력해주세요.";
+ if (!emailPattern.test(val)) return "잘못된 이메일 형식입니다.";
+ return "";
+}
+
+export function validatePassword(val) {
+ if (!val) return "비밀번호를 입력해주세요.";
+ if (val.length < MIN_PASSWORD_LENGTH)
+ return "비밀번호는 8자 이상 입력해주세요.";
+ return "";
+}
+
+export function validateNickname(val) {
+ if (!val) return "닉네임을 입력해주세요.";
+ return "";
+}
+
+export function validatePasswordChecker(val, checker) {
+ if (!val) return "비밀번호가 일치하지 않습니다.";
+ if (val !== checker)
+ return "비밀번호가 일치하지 않습니다.";
+ return "";
+}
+
+export function validateProductName(val) {
+ if (!val || val.length > 10) return "10자 이내로 입력해주세요.";
+ return "";
+}
+export function validateDescription(val) {
+ if (!val || val.length < 10) return "10자 이상 입력해주세요.";
+ return "";
+}
+export function validatePrice(val) {
+ if (!val || Number.isFinite(val)) return "숫자로 입력해주세요.";
+ return "";
+}
+export function validateTag (val) {
+ if (val.length > 5) return "5자 이내롤 입력해주세요.";
+ return "";
+}
\ No newline at end of file
diff --git a/8-sprint-fe/src/app/(auth)/layout.jsx b/8-sprint-fe/src/app/(auth)/layout.jsx
new file mode 100644
index 000000000..eacc5708a
--- /dev/null
+++ b/8-sprint-fe/src/app/(auth)/layout.jsx
@@ -0,0 +1,10 @@
+import React from "react";
+import { AuthLayout } from "../(components)/Layout";
+
+export default function Authlayout({ children }) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/8-sprint-fe/src/app/(auth)/login/page.jsx b/8-sprint-fe/src/app/(auth)/login/page.jsx
new file mode 100644
index 000000000..b71ac2fb9
--- /dev/null
+++ b/8-sprint-fe/src/app/(auth)/login/page.jsx
@@ -0,0 +1,8 @@
+import React from "react";
+import LoginPage from "../_components/LoginPage";
+
+const Login = () => {
+ return ;
+};
+
+export default Login;
diff --git a/8-sprint-fe/src/app/(auth)/signup/page.jsx b/8-sprint-fe/src/app/(auth)/signup/page.jsx
new file mode 100644
index 000000000..ab73c4ff6
--- /dev/null
+++ b/8-sprint-fe/src/app/(auth)/signup/page.jsx
@@ -0,0 +1,8 @@
+import React from "react";
+import SignUpPage from "../_components/SignUpPage";
+
+const Register = () => {
+ return ;
+};
+
+export default Register;
diff --git a/8-sprint-fe/src/app/(components)/Header.jsx b/8-sprint-fe/src/app/(components)/Header.jsx
deleted file mode 100644
index c5f0d09c4..000000000
--- a/8-sprint-fe/src/app/(components)/Header.jsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import Link from "next/link";
-import React from "react";
-import Logo from "./atoms/Logo";
-import Button from "./atoms/Button";
-
-const Header = () => {
- return (
-
-
-
-
-
-
- 자유게시판
- 중고마켓
-
-
-
로그인
-
-
-
- );
-};
-
-export default Header;
diff --git a/8-sprint-fe/src/app/(components)/Layout.jsx b/8-sprint-fe/src/app/(components)/Layout.jsx
new file mode 100644
index 000000000..1dd85b6fc
--- /dev/null
+++ b/8-sprint-fe/src/app/(components)/Layout.jsx
@@ -0,0 +1,27 @@
+import React from "react";
+import Nav from "./Nav";
+import Footer from "./Footer";
+
+export function LandingLayout({ children }) {
+ return (
+ <>
+
+ {children}
+
+ >
+ );
+}
+
+export function MainLayout({ children }) {
+ return (
+ <>
+
+ {children}
+
+ >
+ );
+}
+
+export function AuthLayout({ children }) {
+ return <>{children}>;
+}
diff --git a/8-sprint-fe/src/app/(components)/Nav.jsx b/8-sprint-fe/src/app/(components)/Nav.jsx
new file mode 100644
index 000000000..4793c511c
--- /dev/null
+++ b/8-sprint-fe/src/app/(components)/Nav.jsx
@@ -0,0 +1,57 @@
+"use client";
+import Link from "next/link";
+import React from "react";
+import { Logo } from "./atoms/Logo";
+import Button from "./atoms/Button";
+import { usePathname } from "next/navigation";
+import { useAuth } from "@/providers/AuthProvider";
+import Image from "next/image";
+
+import ic_profile from "/public/ic_profile.svg";
+
+export default function Nav() {
+ const pathName = usePathname();
+ const { user } = useAuth();
+
+ return (
+
+ );
+}
diff --git a/8-sprint-fe/src/app/(components)/atoms/Logo.jsx b/8-sprint-fe/src/app/(components)/atoms/Logo.jsx
index 35eb483aa..312043337 100644
--- a/8-sprint-fe/src/app/(components)/atoms/Logo.jsx
+++ b/8-sprint-fe/src/app/(components)/atoms/Logo.jsx
@@ -2,13 +2,24 @@ import Image from "next/image";
import React from "react";
import pandaLogo from "/public/panda.svg";
-const Logo = () => {
+export function Logo() {
return (
- 판다마켓
+
+ 판다마켓
+
);
-};
+}
-export default Logo;
+export function AuthLogo() {
+ return (
+
+
+
+ 판다마켓
+
+
+ );
+}
diff --git a/8-sprint-fe/src/app/(components)/atoms/OAuth.jsx b/8-sprint-fe/src/app/(components)/atoms/OAuth.jsx
new file mode 100644
index 000000000..27288b2aa
--- /dev/null
+++ b/8-sprint-fe/src/app/(components)/atoms/OAuth.jsx
@@ -0,0 +1,28 @@
+import Image from "next/image";
+import React from "react";
+
+import ic_google from "/public/ic_google.svg";
+import ic_kakao from "/public/ic_kakao.svg";
+import Link from "next/link";
+
+const OAuth = () => {
+ return (
+
+ );
+};
+
+export default OAuth;
diff --git a/8-sprint-fe/src/app/(components)/atoms/PasswordInput.jsx b/8-sprint-fe/src/app/(components)/atoms/PasswordInput.jsx
new file mode 100644
index 000000000..a65e667f9
--- /dev/null
+++ b/8-sprint-fe/src/app/(components)/atoms/PasswordInput.jsx
@@ -0,0 +1,36 @@
+"use client";
+import React, { useState } from "react";
+
+import visibillity_on from "/public/btn_visibility_on_24px.svg";
+import visibillity_off from "/public/btn_visibility_off_24px.svg";
+import Image from "next/image";
+import TextInput from "./TextInput";
+
+const PasswordInput = ({ name, title, errmessage, isValid, ...props }) => {
+ const [isVisible, setIsVisible] = useState(false);
+
+ return (
+
+ setIsVisible((prev) => !prev)}
+ className="absolute top-15 right-6 cursor-pointer"
+ >
+ {isVisible ? (
+
+ ) : (
+
+ )}
+
+
+ );
+};
+
+export default PasswordInput;
diff --git a/8-sprint-fe/src/app/(components)/atoms/TextInput.jsx b/8-sprint-fe/src/app/(components)/atoms/TextInput.jsx
index 654779f7f..9f71750f2 100644
--- a/8-sprint-fe/src/app/(components)/atoms/TextInput.jsx
+++ b/8-sprint-fe/src/app/(components)/atoms/TextInput.jsx
@@ -1,12 +1,61 @@
-import React from 'react'
+"use client";
+import React, { useState } from "react";
+
+const TextInput = ({
+ name,
+ title,
+ errmessage,
+ type = "text",
+ isValid = true,
+ children,
+ ...props
+}) => {
+ const [isFocused, setIsFocused] = useState(false);
+ const [isTouched, setIsTouched] = useState(false);
+
+ const handleBlurOrEnter = () => {
+ // focus out
+ setIsFocused(false);
+ setIsTouched(true);
+ };
+
+ // 스타일 적용 여부 결정
+ const getInputClass = () => {
+ if (isFocused) return "border border-Primary-100"; // focus 중엔 항상 focused 스타일
+ if (!isTouched) return ""; // 처음 입력 전엔 스타일 없음
+ // focus out 이후에만 valid/invalid 스타일 적용
+ return isValid ? "border border-Primary-100" : "border border-error-red";
+ };
-const TextInput = ({name, title}) => {
return (
-
-
{title}
-
+
+
+ {title}
+
+
setIsFocused(true)}
+ onBlur={handleBlurOrEnter} // focus out 처리
+ onKeyDown={(e) => {
+ // enter 시 focus out
+ if (e.key === "Enter") {
+ handleBlurOrEnter(); // Enter 시 focus out과 동일 처리
+ e.currentTarget.blur(); // 실제로 input에서 focus 제거
+ }
+ }}
+ className={`w-full py-4 px-6 bg-gray-100 rounded-xl ${getInputClass()}`}
+ />
+ {children}
+ {!isFocused && !isValid && (
+
+ {errmessage}
+
+ )}
- )
-}
+ );
+};
-export default TextInput
\ No newline at end of file
+export default TextInput;
diff --git a/8-sprint-fe/src/app/board/[id]/page.jsx b/8-sprint-fe/src/app/(contents)/board/[id]/page.jsx
similarity index 100%
rename from 8-sprint-fe/src/app/board/[id]/page.jsx
rename to 8-sprint-fe/src/app/(contents)/board/[id]/page.jsx
diff --git a/8-sprint-fe/src/app/board/[id]/patch/page.jsx b/8-sprint-fe/src/app/(contents)/board/[id]/patch/page.jsx
similarity index 100%
rename from 8-sprint-fe/src/app/board/[id]/patch/page.jsx
rename to 8-sprint-fe/src/app/(contents)/board/[id]/patch/page.jsx
diff --git a/8-sprint-fe/src/app/board/_components/ArticleForm.jsx b/8-sprint-fe/src/app/(contents)/board/_components/ArticleForm.jsx
similarity index 100%
rename from 8-sprint-fe/src/app/board/_components/ArticleForm.jsx
rename to 8-sprint-fe/src/app/(contents)/board/_components/ArticleForm.jsx
diff --git a/8-sprint-fe/src/app/board/_components/BoardDetailPage.jsx b/8-sprint-fe/src/app/(contents)/board/_components/BoardDetailPage.jsx
similarity index 100%
rename from 8-sprint-fe/src/app/board/_components/BoardDetailPage.jsx
rename to 8-sprint-fe/src/app/(contents)/board/_components/BoardDetailPage.jsx
diff --git a/8-sprint-fe/src/app/board/_components/BoardPage.jsx b/8-sprint-fe/src/app/(contents)/board/_components/BoardPage.jsx
similarity index 100%
rename from 8-sprint-fe/src/app/board/_components/BoardPage.jsx
rename to 8-sprint-fe/src/app/(contents)/board/_components/BoardPage.jsx
diff --git a/8-sprint-fe/src/app/board/_components/BoardPatchPage.jsx b/8-sprint-fe/src/app/(contents)/board/_components/BoardPatchPage.jsx
similarity index 100%
rename from 8-sprint-fe/src/app/board/_components/BoardPatchPage.jsx
rename to 8-sprint-fe/src/app/(contents)/board/_components/BoardPatchPage.jsx
diff --git a/8-sprint-fe/src/app/board/_components/BoardPostPage.jsx b/8-sprint-fe/src/app/(contents)/board/_components/BoardPostPage.jsx
similarity index 100%
rename from 8-sprint-fe/src/app/board/_components/BoardPostPage.jsx
rename to 8-sprint-fe/src/app/(contents)/board/_components/BoardPostPage.jsx
diff --git a/8-sprint-fe/src/app/board/_components/Card.jsx b/8-sprint-fe/src/app/(contents)/board/_components/Card.jsx
similarity index 100%
rename from 8-sprint-fe/src/app/board/_components/Card.jsx
rename to 8-sprint-fe/src/app/(contents)/board/_components/Card.jsx
diff --git a/8-sprint-fe/src/app/board/_components/CommentList.jsx b/8-sprint-fe/src/app/(contents)/board/_components/CommentList.jsx
similarity index 100%
rename from 8-sprint-fe/src/app/board/_components/CommentList.jsx
rename to 8-sprint-fe/src/app/(contents)/board/_components/CommentList.jsx
diff --git a/8-sprint-fe/src/app/board/_components/ImageBox.jsx b/8-sprint-fe/src/app/(contents)/board/_components/ImageBox.jsx
similarity index 100%
rename from 8-sprint-fe/src/app/board/_components/ImageBox.jsx
rename to 8-sprint-fe/src/app/(contents)/board/_components/ImageBox.jsx
diff --git a/8-sprint-fe/src/app/board/_components/ListCommunity.jsx b/8-sprint-fe/src/app/(contents)/board/_components/ListCommunity.jsx
similarity index 87%
rename from 8-sprint-fe/src/app/board/_components/ListCommunity.jsx
rename to 8-sprint-fe/src/app/(contents)/board/_components/ListCommunity.jsx
index bb797ba53..108bb8add 100644
--- a/8-sprint-fe/src/app/board/_components/ListCommunity.jsx
+++ b/8-sprint-fe/src/app/(contents)/board/_components/ListCommunity.jsx
@@ -1,6 +1,6 @@
import React from "react";
-import ImageBox from "@/app/board/_components/ImageBox";
+import ImageBox from "@/app/(contents)/board/_components/ImageBox";
import Heart from "@/app/(components)/atoms/Heart";
import ProfileIcon from "@/app/(components)/atoms/ProfileIcon";
@@ -22,7 +22,7 @@ const ListCommunity = ({ title, createdAt }) => {
총명한 판다
{date}
-
+
);
diff --git a/8-sprint-fe/src/app/board/page.jsx b/8-sprint-fe/src/app/(contents)/board/page.jsx
similarity index 100%
rename from 8-sprint-fe/src/app/board/page.jsx
rename to 8-sprint-fe/src/app/(contents)/board/page.jsx
diff --git a/8-sprint-fe/src/app/board/post/page.jsx b/8-sprint-fe/src/app/(contents)/board/post/page.jsx
similarity index 100%
rename from 8-sprint-fe/src/app/board/post/page.jsx
rename to 8-sprint-fe/src/app/(contents)/board/post/page.jsx
diff --git a/8-sprint-fe/src/app/(contents)/items/[id]/page.jsx b/8-sprint-fe/src/app/(contents)/items/[id]/page.jsx
new file mode 100644
index 000000000..ef8734156
--- /dev/null
+++ b/8-sprint-fe/src/app/(contents)/items/[id]/page.jsx
@@ -0,0 +1,8 @@
+import React from "react";
+import ItemDetailPage from "../_components/ItemDetailPage";
+
+const ItemsDetail = () => {
+ return ;
+};
+
+export default ItemsDetail;
diff --git a/8-sprint-fe/src/app/(contents)/items/_components/BestProducts.jsx b/8-sprint-fe/src/app/(contents)/items/_components/BestProducts.jsx
new file mode 100644
index 000000000..7d875c922
--- /dev/null
+++ b/8-sprint-fe/src/app/(contents)/items/_components/BestProducts.jsx
@@ -0,0 +1,21 @@
+import React from "react";
+
+export default function BestProducts({ items }) {
+ return (
+
+
+
베스트 상품
+ {/*
+ {items.map((item) => {
+ // map을 이용해서 렌더링
+ return (
+
+
+
+ );
+ })}
+ */}
+
+
+ );
+}
diff --git a/8-sprint-fe/src/app/(contents)/items/_components/ItemDetailPage.jsx b/8-sprint-fe/src/app/(contents)/items/_components/ItemDetailPage.jsx
new file mode 100644
index 000000000..f0d5e61bd
--- /dev/null
+++ b/8-sprint-fe/src/app/(contents)/items/_components/ItemDetailPage.jsx
@@ -0,0 +1,17 @@
+"use client";
+import { fetchProduct } from "@/api/fetchProducts";
+import { useQuery } from "@tanstack/react-query";
+import React from "react";
+
+export default function ItemDetailPage({ id }) {
+ const {
+ data: product,
+ isPending,
+ error,
+ } = useQuery({
+ queryKey: ["produt", id],
+ queryFn: fetchProduct,
+ });
+
+ return ItemDetailPage
;
+}
diff --git a/8-sprint-fe/src/app/(contents)/items/_components/ItemsPage.jsx b/8-sprint-fe/src/app/(contents)/items/_components/ItemsPage.jsx
new file mode 100644
index 000000000..ac6c66978
--- /dev/null
+++ b/8-sprint-fe/src/app/(contents)/items/_components/ItemsPage.jsx
@@ -0,0 +1,15 @@
+"use client";
+import React from "react";
+import BestProducts from "./BestProducts";
+import ProductsList from "./ProductsList";
+
+const ItemsPage = () => {
+ return (
+
+ );
+};
+
+export default ItemsPage;
diff --git a/8-sprint-fe/src/app/(contents)/items/_components/ProductItem.jsx b/8-sprint-fe/src/app/(contents)/items/_components/ProductItem.jsx
new file mode 100644
index 000000000..b4ae1bae5
--- /dev/null
+++ b/8-sprint-fe/src/app/(contents)/items/_components/ProductItem.jsx
@@ -0,0 +1,26 @@
+import Heart from "@/app/(components)/atoms/Heart";
+import Image from "next/image";
+import React from "react";
+
+import img_defalt from "/public/img_default.svg";
+
+export default function ProductItem({ item }) {
+ const { images, name, price, favoriteCount } = item;
+
+ return (
+
+
+
+
+ );
+}
diff --git a/8-sprint-fe/src/app/(contents)/items/_components/ProductsList.jsx b/8-sprint-fe/src/app/(contents)/items/_components/ProductsList.jsx
new file mode 100644
index 000000000..dd2df4dba
--- /dev/null
+++ b/8-sprint-fe/src/app/(contents)/items/_components/ProductsList.jsx
@@ -0,0 +1,53 @@
+"use client";
+import { fetchProducts } from "@/api/fetchProducts";
+import { useQuery } from "@tanstack/react-query";
+import React, { useEffect, useState } from "react";
+import ProductItem from "./ProductItem";
+import Link from "next/link";
+
+const ProductsList = () => {
+ const [currentPage, setCurrentPage] = useState(1);
+ const [order, setOrder] = useState("recent");
+
+ const {
+ data: products,
+ isPending,
+ error,
+ } = useQuery({
+ queryKey: ["produts", currentPage, order],
+ queryFn: () => fetchProducts({ page: currentPage, orderBy: order }),
+ });
+
+ if (isPending) {
+ return (
+ 로딩 중...
+ );
+ }
+
+ if (error) {
+ return (
+
+ {error.message}
+
+ );
+ }
+
+ return (
+
+
판매 중인 상품
+
+ {products.list.map((product) => {
+ return (
+
+
+
+
+
+ );
+ })}
+
+
+ );
+};
+
+export default ProductsList;
diff --git a/8-sprint-fe/src/app/(contents)/items/page.jsx b/8-sprint-fe/src/app/(contents)/items/page.jsx
new file mode 100644
index 000000000..d73119623
--- /dev/null
+++ b/8-sprint-fe/src/app/(contents)/items/page.jsx
@@ -0,0 +1,8 @@
+import React from "react";
+import ItemsPage from "./_components/ItemsPage";
+
+const Items = () => {
+ return ;
+};
+
+export default Items;
diff --git a/8-sprint-fe/src/app/(contents)/layout.jsx b/8-sprint-fe/src/app/(contents)/layout.jsx
new file mode 100644
index 000000000..2ca15a77f
--- /dev/null
+++ b/8-sprint-fe/src/app/(contents)/layout.jsx
@@ -0,0 +1,6 @@
+import React from "react";
+import { MainLayout } from "../(components)/Layout";
+
+export default function Layout({ children }) {
+ return {children} ;
+}
diff --git a/8-sprint-fe/src/app/globals.css b/8-sprint-fe/src/app/globals.css
index 46c6009f2..fb8c5f728 100644
--- a/8-sprint-fe/src/app/globals.css
+++ b/8-sprint-fe/src/app/globals.css
@@ -43,7 +43,3 @@ section {
max-width: 1200px;
width: 100%;
}
-
-.basic_nav {
- @apply text-gray-600 text-lg font-bold;
-}
\ No newline at end of file
diff --git a/8-sprint-fe/src/app/layout.jsx b/8-sprint-fe/src/app/layout.jsx
index 5320085b1..85f850d57 100644
--- a/8-sprint-fe/src/app/layout.jsx
+++ b/8-sprint-fe/src/app/layout.jsx
@@ -1,9 +1,8 @@
import { Geist, Geist_Mono } from "next/font/google";
import localFont from "next/font/local";
import "./globals.css";
-import Header from "./(components)/Header";
-import Footer from "./(components)/Footer";
import TanstackProvider from "@/providers/TanstackProvider";
+import AuthProvider from "@/providers/AuthProvider";
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -54,9 +53,9 @@ export default function RootLayout({ children }) {
className={`antialiased ${rokaf.variable} ${pretender.variable}`}
>
-
- {children}
-
+
+ {children}
+