Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
51afcd6
refactor: 리액트 쿼리를 적용하여 리팩토링 진행중
byeolee1221 Nov 12, 2024
f1e6f81
Merge branch 'Next-문창기-sprint11' into Next-문창기-sprint12
byeolee1221 Nov 12, 2024
8a0fd79
fix: 빌드 시 layout 파일 오류 해결
byeolee1221 Nov 12, 2024
2c33462
fix: 빌드 시 NavBar 경로 관련 문제 수정
byeolee1221 Nov 12, 2024
bda9b0d
chore: pagination 좌우 버튼 비활성화되도록 수정
byeolee1221 Nov 13, 2024
9a03f49
feat: item 상세페이지 댓글 form 제외하고 구현
byeolee1221 Nov 13, 2024
4105b50
feat: item 댓글리스트 무한스크롤 적용
byeolee1221 Nov 13, 2024
b951b59
chore: 가독성 좋게 빈 줄 추가
byeolee1221 Nov 13, 2024
918e6a5
feat: 댓글 수정 및 삭제 구현, 삭제 시 확인모달 구현
byeolee1221 Nov 14, 2024
f474079
chore: bestItems isPending, isError 활용한 조건부 렌더링 수정
byeolee1221 Nov 14, 2024
dccb03a
feat: boards 페이지 bsetPosts까지 구현
byeolee1221 Nov 14, 2024
05ee557
feat: boards bestPosts 및 검색기능 구현 (server actions 활용)
byeolee1221 Nov 15, 2024
254ce19
feat: boards allPost 구현 (server actions 활용)
byeolee1221 Nov 15, 2024
51c8f5b
refactor: items 로직에도 server actions 적용하여 성능 향상
byeolee1221 Nov 15, 2024
64dd0e4
fix: tanstack query의 페이지네이션 기능 활용을 위해 allPostList 코드 변경
byeolee1221 Nov 15, 2024
d226b0c
feat: boards 개별페이지 구현
byeolee1221 Nov 15, 2024
17576aa
refactor: comment 컴포넌트 공용컴포넌트화, board 및 item에서 공동 사용하는 요청코드 공용컴포넌트화 및…
byeolee1221 Nov 16, 2024
51813c7
chore: boardsAtom 타입 빌드오류 해결
byeolee1221 Nov 16, 2024
9ff8ead
docs: readme 보완
byeolee1221 Nov 16, 2024
cf5c09b
chore: navbar 폴더명을 nav로 변경
byeolee1221 Nov 16, 2024
a1eefe2
chore: readme 보완, 베스트게시글 revalidate 삭제
byeolee1221 Nov 16, 2024
b6d142d
chore: 속도 문제로 vercel로 배포경로 변경
byeolee1221 Nov 16, 2024
b8c374c
chore: middleware 경로 수정
byeolee1221 Nov 16, 2024
5290ab6
fix: fetchComment return 값 수정
byeolee1221 Nov 16, 2024
53df744
feat: loading, error, global-error, not-found 추가
byeolee1221 Nov 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
{
"extends": ["next/core-web-vitals", "next/typescript"]
"extends": ["next/core-web-vitals", "next/typescript"],
"rules": {
"no-unused-vars": "off"
}
}

12 changes: 8 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage
Expand All @@ -25,8 +29,8 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local
# env files (can opt-in for commiting if needed)
.env*

# vercel
.vercel
Expand Down
51 changes: 31 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,48 +1,59 @@
# 스프린트 미션 11
# 스프린트 미션 12

## 요구사항

- Javascript
- React 18
- Next.js 14.2.13
- React 19.0.0-rc
- Next.js 15.0.2
- Tailwind CSS 3.4.1
- TanStack Query 5.19.0
- axios 1.7.7
- react-hook-form 7.53.0
- react-hook-form 7.53.2
- zod 3.23.8
- react-hot-toast 2.4.1
- typescript 5
- Jotai 2.10.1
- js-cookie 2.2.1
- framer-motion 11.11.11

### 배포 웹사이트: https://codeit-nextjs-mission.netlify.app/
### 배포 웹사이트: https://codeit-nextjs-mission.vercel.app/

### 기본

- [x] 유효한 정보를 입력하고 스웨거 명세된 “/auth/signUp”으로 POST 요청해서 성공 응답을 받으면 회원가입이 완료됩니다.
- [x] 회원가입이 완료되면 “/login”로 이동합니다.
- [x] 회원가입 페이지에 접근시 로컬 스토리지에 accessToken이 있는 경우 ‘/’ 페이지로 이동합니다.
- [x] 회원가입을 성공한 정보를 입력하고 스웨거 명세된 “/auth/signIp”으로 POST 요청을 하면 로그인이 완료됩니다.
- [x] 로그인이 완료되면 로컬 스토리지에 accessToken을 저장하고 “/” 로 이동합니다.
- [x] 로그인/회원가입 페이지에 접근시 로컬 스토리지에 accessToken이 있는 경우 ‘/’ 페이지로 이동합니다.
- [x] 로컬 스토리지에 accessToken이 있는 경우 상단바 ‘로그인’ 버튼이 판다 이미지로 바뀝니다.
- [x] ‘상품 등록하기’ 버튼을 누르면 “/additem” 로 이동합니다.
- [x] 각 상품 클릭 시 상품 상세 페이지로 이동합니다.
- [x] 상품 상세 페이지 주소는 “/items/{productId}” 입니다.
- [x] 내가 등록한 상품일 경우 상품 수정, 삭제가 가능합니다.
- [x] 문의하기 input창에 값을 입력 후 ‘등록’ 버튼을 누르면 댓글이 등록됩니다.
- [x] 내가 등록한 댓글은 수정, 삭제가 가능합니다.
- [x] 이미지를 제외하고 input 에 모든 값을 입력하면 ‘등록' 버튼이 활성화 됩니다.
- [x] 활성화된 ‘등록' 버튼을 누르면 상품 등록이 완료됩니다.
- [x] 등록이 완료되면 해당 상품 상세 페이지로 이동합니다.

### 심화

- [x] 로그인, 회원가입 기능에 react-hook-form을 활용해봅니다.
- [x] api 요청에 TanStack React Query를 활용해 주세요.

### 변경사항

- 스프린트 미션 10에서의 개선사항 일부를 반영하였습니다.
- 스프린트 미션 11에서의 개선사항을 반영하였습니다.
- js-cookie를 사용하여 로그인 상태를 유지하였습니다.
- 로그인이 필요한 POST, PATCH, DELETE 요청 시 토큰이 만료되어 있을 때 axios의 인터셉터를 통해 토큰을 재발급 받아 요청을 보내도록 했습니다.
- useQuery를 사용해서 클라이언트에서 GET 요청을 보내는 대신, server action을 사용하여 서버에서 데이터를 가져오도록 했습니다.
- server action을 사용할 때 fetch 메서드를 적용하여 next.js의 캐싱 기능을 사용했습니다.
- 사용자와의 상호작용이 필요한 GET 요청에서는 클라이언트단에서 tanstack query를 사용하였습니다.
- 미들웨어를 사용해서 로그인이 필요한 페이지에 접근할 때 로그인 상태를 확인하도록 했습니다.

## 스크린샷

| 메인 페이지 (데스크탑) | 로그인페이지 (데스크탑) |
| 중고마켓 페이지 (데스크탑) | 개별 상품 페이지 (데스크탑) |
| :--------------------------------------------------------------------: | :-------------------------------------------------------------------: |
| <img src="/public/images/mainPage.png" width="400" height="400"> | <img src="/public/images/signIn.png" width="400" height="400"> |
| 회원가입페이지 (데스크탑) |
| <img src="/public/images/signup.png" width="400" height="400"> |
| <img src="/public/images/itemPage.png" width="400" height="400"> | <img src="/public/images/itemDetail.png" width="400" height="400"> |
| 아이템 등록 페이지 (데스크탑) |
| <img src="/public/images/addItem.png" width="400" height="400"> |


## 멘토에게

- 감사합니다.
- 제출기간(토요일)까지 발생한 문제해결 및 모든 개선사항의 반영이 포함하기 어려워져서 일부 문제 및 개선사항이 그대로 있는 상태입니다.
- 추가적인 작업을 해서 문제해결 및 개선사항 반영을 해보겠습니다.
- 아직 추상화, server actions 전환 작업, 게시글 수정 및 삭제 작업이 덜 되어 있는 컴포넌트가 있습니다. 올려놓고 진행해보겠습니다.
16 changes: 0 additions & 16 deletions app/(auth)/authTypeShare.ts

This file was deleted.

33 changes: 0 additions & 33 deletions app/(auth)/error.tsx

This file was deleted.

44 changes: 22 additions & 22 deletions app/(auth)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"use client"
"use client";

import Image from "next/image";
import Link from "next/link";
Expand All @@ -13,28 +13,28 @@ const AuthLayout = ({ children }: { children: React.ReactNode }) => {
];

return (
<div className="flex flex-col space-y-6 px-4 items-center my-20 md:w-[640px] md:min-h-screen md:justify-center md:mt-0 md:m-auto">
<Link href="/" className="flex items-center space-x-3">
<Image src="/images/logo.png" alt="로고" width={51} height={51} className="md:w-[103px]" />
<h2 className="font-ROKAFSans text-4xl md:text-6xl text-[--color-theme]">판다마켓</h2>
</Link>
{children}
<div className="flex items-center justify-between bg-[--color-bg-skyblue] rounded-lg px-6 py-4 w-full">
<h2>간편 로그인하기</h2>
<div className="flex items-center space-x-4">
{footerArr.map((link, i) => (
<Link key={i} href={link.href} target="_blank" rel="noopener noreferrer">
<Image src={link.imgSrc} alt={link.imgAlt} width={42} height={42} />
</Link>
))}
<div className="flex flex-col space-y-6 px-4 items-center my-40 md:w-[640px] md:min-h-svh md:justify-center md:mt-0 md:m-auto">
<Link href="/" className="flex items-center space-x-3">
<Image src="/icons/logo.png" alt="판다마켓 로고" width={51} height={51} className="md:w-[103px]" />
<h2 className="font-ROKAFSans text-4xl md:text-6xl text-panda-theme">판다마켓</h2>
</Link>
{children}
<div className="flex items-center justify-between bg-panda-bg-skyblue rounded-lg px-6 py-4 w-full">
<h3>간편 로그인하기</h3>
<div className="flex items-center space-x-4">
{footerArr.map((link) => (
<Link href={link.href} key={link.imgAlt} target="_blank" rel="noopener noreferrer">
<Image src={link.imgSrc} alt={link.imgAlt} width={42} height={42} />
</Link>
))}
</div>
</div>
<div className="flex items-center space-x-1 text-sm">
<h3 className="font-medium">{pathname === "/signin" ? "판다마켓이 처음이신가요?" : "이미 회원이신가요?"}</h3>
<Link href={pathname === "/signin" ? "/signup" : "/signin"} className="font-bold text-panda-theme">{pathname === "/signin" ? "회원가입" : "로그인"}</Link>
</div>
</div>
<div className="flex items-center space-x-1 text-sm">
<h3 className="font-medium">{pathname === "/signin" ? "판다마켓이 처음이신가요?" : "이미 회원이신가요?"}</h3>
{pathname === "/signin" ? <Link href="/signup" className="underline text-[--color-theme]">회원가입</Link> : <Link href="/signin" className="underline text-[--color-theme]">로그인</Link>}
</div>
</div>
);
}
};

export default AuthLayout;
export default AuthLayout;
34 changes: 34 additions & 0 deletions app/(auth)/signin/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"use client";

import Image from "next/image";
import { useRouter } from "next/navigation";
import { startTransition } from "react";

const Error = ({ reset }: { reset: () => void }) => {
const router = useRouter();

return (
<div className="w-full min-h-screen flex flex-col items-center justify-center space-y-5">
<div className="flex items-center space-x-3">
<Image src="/images/globalError.png" alt="에러 아이콘" width={60} height={60} />
<div className="flex flex-col space-y-1">
<h1 className="font-bold">웹사이트에서 알 수 없는 오류가 발생했습니다.</h1>
<p className="text-sm">아래 버튼을 눌러 새로고침해보세요!</p>
</div>
</div>
<button
onClick={() => {
startTransition(() => {
router.refresh();
reset();
});
}}
className="bg-panda-theme hover:bg-panda-theme-hover text-white px-5 py-2 rounded-md text-center transition-colors"
>
다시 시도
</button>
</div>
);
};

export default Error;
118 changes: 94 additions & 24 deletions app/(auth)/signin/page.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,109 @@
"use client"
"use client";

import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { SigninSchema } from "./signinConstants";
import { signinSchema } from "./zodSchema/signinSchema";
import { zodResolver } from "@hookform/resolvers/zod";
import SigninForm from "@/components/auth/SigninForm";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { z } from "zod";
import { useState } from "react";
import Image from "next/image";
import toast from "react-hot-toast";
import axios from "axios";
import useToken from "@/hooks/useToken";

const Signin = () => {
const SigninPage = () => {
const router = useRouter();
const context = useToken();

useEffect(() => {
if (context?.session) {
router.push("/");
}
}, [router, context?.session]);

const form = useForm<z.infer<typeof SigninSchema>>({
resolver: zodResolver(SigninSchema),
mode: "all",
const { setTokens } = useToken();
const {
register,
handleSubmit,
reset,
formState: { errors, isSubmitting, isValid },
} = useForm<z.infer<typeof signinSchema>>({
resolver: zodResolver(signinSchema),
mode: "onChange",
defaultValues: {
userEmail: "",
userPassword: "",
},
});
const [visiblePassword, setVisiblePassword] = useState(false);

const handleVisiblePassword = () => {
setVisiblePassword((prev) => !prev);
};

const isLoading = form.formState.isSubmitting;
const error = form.formState.errors;
const onSubmit = async (data: z.infer<typeof signinSchema>) => {
try {
const response = await axios.post("/api/auth/signin", {
email: data.userEmail,
password: data.userPassword,
});

if (response.status === 200) {
setTokens(response.data.accessToken, response.data.refreshToken);
toast.success("로그인이 완료되었습니다.");
reset();
router.push("/");
}
} catch (error) {
if (axios.isAxiosError(error)) {
toast.error(error.response?.data || "로그인에 실패하였습니다.");
}
}
};

return (
<SigninForm form={form} isLoading={isLoading} error={error} />
)
}
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col space-y-10 w-full">
<div className="flex flex-col space-y-3">
<label htmlFor="userEmail" className="text-sm font-bold md:text-lg">
이메일
</label>
<input
{...register("userEmail")}
type="email"
id="userEmail"
name="userEmail"
placeholder="이메일을 입력해주세요."
className="px-6 py-4 bg-panda-gray100 rounded-xl focus:outline-none form-ring transition-all"
/>
{errors.userEmail && <span className="error-text-start">{errors.userEmail.message}</span>}
</div>
<div className="flex flex-col space-y-3">
<label htmlFor="userPassword" className="text-sm font-bold md:text-lg">
비밀번호
</label>
<div className="flex items-center justify-between px-6 py-4 bg-panda-gray100 rounded-xl form-ring transition-all">
<input
{...register("userPassword")}
type={visiblePassword ? "text" : "password"}
id="userPassword"
name="userPassword"
placeholder="비밀번호를 입력해주세요."
className="bg-panda-gray100 w-full focus:outline-none"
/>
<Image
src={visiblePassword ? "/icons/btn_visibility_off.svg" : "/icons/btn_visibility_on.svg"}
alt="보이기 버튼"
width={24}
height={24}
onClick={handleVisiblePassword}
className="cursor-pointer"
/>
</div>
{errors.userPassword && (
<span className="error-text-start">{errors.userPassword.message}</span>
)}
</div>
<button
type="submit"
disabled={!isValid || isSubmitting}
className="px-32 py-4 bg-panda-theme hover:bg-panda-theme-hover disabled:bg-panda-gray400 rounded-full text-white text-lg font-semibold transition-colors w-full"
>
{!isSubmitting ? "로그인" : "잠시만 기다려주세요."}
</button>
</form>
);
};

export default Signin;
export default SigninPage;
Loading
Loading