diff --git a/next.config.js b/next.config.js index 549bdfeb9..1f664c94f 100644 --- a/next.config.js +++ b/next.config.js @@ -6,6 +6,8 @@ const nextConfig = { "sprint-fe-project.s3.ap-northeast-2.amazonaws.com", "images.samsung.com", "example.com", + "flexible.img.hani.co.kr", + "via.placeholder.com", ], }, }; diff --git a/package-lock.json b/package-lock.json index 051229431..b2c326333 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,8 @@ "axios": "^1.7.3", "next": "14.2.5", "react": "^18", - "react-dom": "^18" + "react-dom": "^18", + "react-hook-form": "^7.52.2" }, "devDependencies": { "@types/node": "^20", @@ -3429,6 +3430,21 @@ "react": "^18.3.1" } }, + "node_modules/react-hook-form": { + "version": "7.52.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.52.2.tgz", + "integrity": "sha512-pqfPEbERnxxiNMPd0bzmt1tuaPcVccywFDpyk2uV5xCIBphHV5T8SVnX9/o3kplPE1zzKt77+YIoq+EMwJp56A==", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/package.json b/package.json index b2efd2128..9564018df 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "axios": "^1.7.3", "next": "14.2.5", "react": "^18", - "react-dom": "^18" + "react-dom": "^18", + "react-hook-form": "^7.52.2" }, "devDependencies": { "@types/node": "^20", diff --git a/public/images/icon/ic_null_user_profile_image.png b/public/images/icon/ic_null_user_profile_image.png index 1a4d2ba0c..da7e85bd8 100644 Binary files a/public/images/icon/ic_null_user_profile_image.png and b/public/images/icon/ic_null_user_profile_image.png differ diff --git a/src/components/DetailBoard.tsx b/src/components/DetailBoard.tsx index 2648cc7d4..2b9588fd9 100644 --- a/src/components/DetailBoard.tsx +++ b/src/components/DetailBoard.tsx @@ -44,15 +44,15 @@ function Product() { const res = await axios.get(`/articles/${id}`); const nextProduct = res.data; setProduct(nextProduct); + setLoading(false); } catch (error) { - console.error(`유효하지 않은 주소입니다.`); + alert(`유효하지 않은 주소입니다.`); router.replace(`/board`); } } useEffect(() => { getProduct(id); - setLoading(false); }, [id]); if (loading) { diff --git a/src/components/DetailBoardComments.tsx b/src/components/DetailBoardComments.tsx index f7877ad9a..9514ae213 100644 --- a/src/components/DetailBoardComments.tsx +++ b/src/components/DetailBoardComments.tsx @@ -23,21 +23,24 @@ interface ItemsListType { function DetailBoardComments() { const [comments, setComments] = useState([]); const [loading, setLoading] = useState(true); - const [values, setValues] = useState(""); + const [commentsValues, setCommentsValues] = useState(""); const [pass, setPass] = useState(false); const router = useRouter(); const id = Number(router.query["id"]); // 게시판 댓글 데이터 가져오기 async function getProduct(id: number) { - const res = await axios.get(`/articles/${id}/comments?limit=50`); - const nextComments = res.data.list; - console.log(nextComments); - setComments(nextComments); + try { + const res = await axios.get(`/articles/${id}/comments?limit=50`); + const nextComments = res.data.list; + setComments(nextComments); + setLoading(false); + } catch (error) { + alert("댓글 데이터 불러오기 실패"); + } } useEffect(() => { getProduct(id); - setLoading(false); }, [id]); const onClickReturn = () => { @@ -47,13 +50,14 @@ function DetailBoardComments() { // 댓글 인풋의 입력값 파악 const handleInputChange = (e: ChangeEvent) => { const { value } = e.target; - setValues(value); + setCommentsValues(value); }; - // 테스트를 위해 추가한 동작 + // TODO: 스프린트 미션에 API POST 관련 기능 요구 시 추가 예정, 현재는 테스트를 위한 코드 const handleSubmit = (e: FormEvent) => { + if (commentsValues.length <= 0) return; e.preventDefault(); - console.log(values); + console.log(commentsValues); }; // 댓글 작성한 시간 변환 함수 @@ -78,16 +82,6 @@ function DetailBoardComments() { } }; - // 입력값 감지 후 조건 충족 시 등록 버튼 활성화 - useEffect(() => { - function validation() { - const valueCheck = values.length > 0; - return valueCheck; - } - const isValid = validation(); - setPass(isValid); - }, [values]); - if (loading) { return
Loading...
; } @@ -104,7 +98,7 @@ function DetailBoardComments() { />
- - 로그인 - + {isLoggedIn ? ( +
+ 유저 프로필 아이콘 + {isOpen ? ( +
+ 로그아웃 +
+ ) : ( + <> + )} +
+ ) : ( + + 로그인 + + )} ); diff --git a/src/components/ProductComments.tsx b/src/components/ProductComments.tsx index da93a16aa..66fd4f7e1 100644 --- a/src/components/ProductComments.tsx +++ b/src/components/ProductComments.tsx @@ -7,10 +7,6 @@ import S from "@/components/ProductComments.module.css"; const PLACEHOLDERTEXT = "개인정보를 공유 및 요청하거나, 명예 훼손, 무단 광고, 불법 정보 유포시 모니터링 후 삭제될 수 있으며, 이에 대한 민형사상 책임은 게시자에게 있습니다."; -interface ButtonProps { - $pass?: boolean; -} - interface ItemsListType { list: T[]; } @@ -33,10 +29,9 @@ function ProductComments() { setValues(value); }; - // 테스트를 위해 추가한 동작 + // TODO: 스프린트 미션에 API POST 관련 기능 요구 시 추가 예정, 현재는 테스트를 위한 코드 const handleSubmit = (e: FormEvent) => { e.preventDefault(); - console.log(values); }; // 댓글 작성한 시간 변환 함수 diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 347cf9c0a..a735b10c8 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,11 +1,14 @@ import NavBar from "@/components/NavBar"; import "@/styles/globals.css"; import type { AppProps } from "next/app"; +import { useRouter } from "next/router"; export default function App({ Component, pageProps }: AppProps) { + const router = useRouter(); + const noNavBarPages = ["/login", "/register"]; return ( <> - + {!noNavBarPages.includes(router.pathname) && } ); diff --git a/src/pages/addboard.tsx b/src/pages/addboard.tsx index d0aed2173..f5f9952db 100644 --- a/src/pages/addboard.tsx +++ b/src/pages/addboard.tsx @@ -52,7 +52,7 @@ function AddBoard() { } }; - // 등록 버튼 클릭 시 제출 + // TODO: 스프린트 미션에 API POST 관련 기능 요구 시 추가 예정, 현재는 테스트를 위한 코드 const handleSubmit = (e: FormEvent) => { e.preventDefault(); console.log(values); @@ -64,7 +64,6 @@ function AddBoard() { return; } const image = values.images; - console.log(image); if (!(image instanceof File)) { console.error("Expected a File object but got:", image); diff --git a/src/pages/additem.tsx b/src/pages/additem.tsx index 4cb90a1b7..ca3cc1389 100644 --- a/src/pages/additem.tsx +++ b/src/pages/additem.tsx @@ -23,7 +23,7 @@ function AddItem() { const [preview, setPreview] = useState(""); const [tagInput, setTagInput] = useState(""); const [pass, setPass] = useState(false); - console.log(values); + // 이미지 삭제 const onClickImageDelete = () => { setValues((prevValues) => ({ @@ -70,7 +70,7 @@ function AddItem() { setTagInput(e.target.value); }; - // 등록 버튼 클릭 시 제출 + // TODO: 스프린트 미션에 API POST 관련 기능 요구 시 추가 예정, 현재는 테스트를 위한 코드 const handleSubmit = (e: FormEvent) => { e.preventDefault(); console.log(values); @@ -103,7 +103,6 @@ function AddItem() { return; } const image = values.images; - console.log(image); if (!(image instanceof File)) { console.error("Expected a File object but got:", image); diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 806abbd14..1866a61c3 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -10,7 +10,6 @@ export default function Home() {
- {/* */}
diff --git a/src/pages/login.tsx b/src/pages/login.tsx index 2a91731fb..bd10e6ae6 100644 --- a/src/pages/login.tsx +++ b/src/pages/login.tsx @@ -1,111 +1,90 @@ -import Link from "next/link"; +import { useForm } from "react-hook-form"; +import { useRouter } from "next/router"; import Image from "next/image"; +import S from "@/styles/login.module.css"; +import axios from "@/pages/api/axios"; +import { useEffect } from "react"; + +interface FormData { + email: string; + password: string; +} function Login() { + const { + register, + handleSubmit, + formState: { isSubmitting, isSubmitted, errors }, + } = useForm(); + const router = useRouter(); + + async function getAccessToken(data: FormData) { + try { + const res = await axios.post("/auth/signIn", data); + const token = res.data.accessToken; + localStorage.setItem("accessToken", token); + router.replace("/"); + } catch (error) { + alert("토큰 가져오기 실패"); + console.error(error); + router.replace("/login"); + } + } + useEffect(() => { + const token = localStorage.getItem("accessToken"); + if (token) router.replace("/"); + }, [router]); + return ( - <> -
-
- - 판다마켓 로고 - -
-
-
-
-
-
- - -
이메일을 입력해주세요
-
잘못된 이메일 형식입니다.
-
-
- - -
닉네임을 입력해주세요
-
-
- - -
비밀번호를 입력해주세요
-
비밀번호를 8자 이상 입력해주세요.
- visibilty-on-off-icon -
-
- - -
비밀번호가 일치하지 않습니다.
- visibilty-on-off-icon -
-
- -
-
- 간편 로그인하기 -
- - google-icon - - - kakao-icon - -
-
-
-
-
-
-
- 이미 회원이신가요? 로그인 -
-
- +
+
+ 판다마켓 로고 +
+
+ + + {errors.email && ( + + {String(errors.email.message)} + + )} + + + {errors.password && ( + + {String(errors.password.message)} + + )} + +
+
); } diff --git a/src/pages/signup.tsx b/src/pages/signup.tsx new file mode 100644 index 000000000..13f9805d1 --- /dev/null +++ b/src/pages/signup.tsx @@ -0,0 +1,134 @@ +import { useForm } from "react-hook-form"; +import { useRouter } from "next/router"; +import Image from "next/image"; +import S from "@/styles/login.module.css"; +import axios from "@/pages/api/axios"; +import { useEffect } from "react"; + +interface FormData { + email: string; + nickname: string; + password: string; + passwordConfirmation: string; +} +function SignUp() { + const { + register, + handleSubmit, + formState: { isSubmitting, isSubmitted, errors }, + getValues, + } = useForm(); + const router = useRouter(); + + async function postSignUp(data: FormData) { + try { + await axios.post("/auth/signUp", data); + router.replace("/login"); + } catch (error) { + alert("회원가입 실패"); + console.error(error); + router.replace("/signup"); + } + } + useEffect(() => { + const token = localStorage.getItem("accessToken"); + if (token) router.replace("/"); + }, [router]); + return ( +
+
+ 판다마켓 로고 +
+
+ + + {errors.email && ( + + {String(errors.email.message)} + + )} + + + {errors.nickname && ( + + {String(errors.nickname.message)} + + )} + + + {errors.password && ( + + {String(errors.password.message)} + + )} + + { + if (getValues("password") !== val) { + return "비밀번호가 일치하지 않습니다."; + } + }, + }, + })} + aria-invalid={isSubmitted ? (errors.passwordConfirmation ? "true" : "false") : undefined} + /> + {errors.passwordConfirmation && ( + + {String(errors.passwordConfirmation.message)} + + )} + +
+
+ ); +} + +export default SignUp; diff --git a/src/styles/globals.css b/src/styles/globals.css index bc7d07a4a..a30ac78fc 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -7,6 +7,7 @@ --gray600: #4b5563; --gray500: #6b7280; --gray400: #9ca3af; + --gray300: #d1d5db; --gray200: #e5e7eb; --gray100: #f3f4f6; --gray50: #f9fafb; @@ -68,3 +69,14 @@ table { a { text-decoration: none; } +input, +input:focus, +textarea, +textarea:focus { + outline: none; + box-shadow: none; +} +input[aria-invalid="true"] { + border: 2px solid red; + border-color: red; +} diff --git a/src/styles/login.module.css b/src/styles/login.module.css new file mode 100644 index 000000000..dd4dde982 --- /dev/null +++ b/src/styles/login.module.css @@ -0,0 +1,43 @@ +.container { + width: 100%; + margin: 70px auto 0px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.LogoImageWrapper { + position: relative; + width: 198px; + height: 66px; +} + +.formContainer { + display: flex; + flex-direction: column; +} +.inputBox { + outline: none; +} +.inputBox :focus { + outline: 2px solid var(--blue) !important; +} +.errorMessage { + font-size: 14px; + font-weight: 600; + color: var(--red); +} +@media screen and (max-width: 1199px) and (min-width: 768px) { +} + +@media screen and (min-width: 1200px) { + .container { + max-width: 1200px; + margin: 70px auto 0px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } +}