From 5ea4f340d279495699b911c07e8010ed1127f7c8 Mon Sep 17 00:00:00 2001 From: 1022gusl <1022gusl@gmail.com> Date: Thu, 5 Dec 2024 06:19:32 +0900 Subject: [PATCH 01/13] =?UTF-8?q?refactor:=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=EC=82=AC=EC=9D=B4=EC=A6=88=ED=9B=85=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/hooks/usePagesize.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/hooks/usePagesize.ts b/app/hooks/usePagesize.ts index 114027a0e..2f49c6aeb 100644 --- a/app/hooks/usePagesize.ts +++ b/app/hooks/usePagesize.ts @@ -1,7 +1,9 @@ import { useEffect, useState } from "react"; -function usePageSize(): number { - const [pageSize, setPageSize] = useState(3); +type PageSizeType = "mobile" | "tablet" | "desktop"; + +function usePageSize(): PageSizeType { + const [pageSize, setPageSize] = useState("desktop"); useEffect(() => { if (typeof window === "undefined") return; @@ -10,11 +12,11 @@ function usePageSize(): number { const width = window.innerWidth; if (width >= 1280) { - setPageSize(3); + setPageSize("desktop"); } else if (width >= 768) { - setPageSize(2); + setPageSize("tablet"); } else { - setPageSize(1); + setPageSize("mobile"); } } From 49874c13d0d31188e11e5342a1928971aecbb74e Mon Sep 17 00:00:00 2001 From: 1022gusl <1022gusl@gmail.com> Date: Thu, 5 Dec 2024 06:23:23 +0900 Subject: [PATCH 02/13] =?UTF-8?q?refactor:=20=EB=B2=A0=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=ED=8F=AC=EC=8A=A4=ED=8A=B8=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=EC=97=90=EB=8F=84=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=EC=82=AC=EC=9D=B4=EC=A6=88=20=ED=83=80=EC=9E=85=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/boards/BestPost.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/components/boards/BestPost.tsx b/app/components/boards/BestPost.tsx index f389c1258..6e60ce7a1 100644 --- a/app/components/boards/BestPost.tsx +++ b/app/components/boards/BestPost.tsx @@ -9,7 +9,14 @@ import Link from "next/link"; export default function BestPost() { const [articles, setArticles] = useState([]); const [error, setError] = useState(null); - const pageSize: number = usePageSize(); + const pageSizeType: "mobile" | "tablet" | "desktop" = usePageSize(); + + const pageSizeMap: Record<"mobile" | "tablet" | "desktop", number> = { + mobile: 1, + tablet: 2, + desktop: 3, + }; + const pageSize = pageSizeMap[pageSizeType]; useEffect(() => { async function loadArticles() { From 01d03abea6c8d3d05fb2ce9f008b11b1e50dbd65 Mon Sep 17 00:00:00 2001 From: 1022gusl <1022gusl@gmail.com> Date: Thu, 5 Dec 2024 06:36:50 +0900 Subject: [PATCH 03/13] =?UTF-8?q?refactor:=20allpost=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=EB=84=A4?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/boards/AllPost.module.css | 26 ----------- app/components/boards/AllPost.tsx | 41 +++++------------ .../boards/AllPostPagination.module.css | 25 +++++++++++ app/components/boards/AllPostPagination.tsx | 44 +++++++++++++++++++ next.config.js | 10 +++++ 5 files changed, 90 insertions(+), 56 deletions(-) create mode 100644 app/components/boards/AllPostPagination.module.css create mode 100644 app/components/boards/AllPostPagination.tsx diff --git a/app/components/boards/AllPost.module.css b/app/components/boards/AllPost.module.css index fbfb427a5..36f88406b 100644 --- a/app/components/boards/AllPost.module.css +++ b/app/components/boards/AllPost.module.css @@ -114,29 +114,3 @@ line-height: 24px; color: #4b5563; } - -.pagination { - margin-bottom: 10px; - display: flex; - justify-content: center; - align-items: center; - gap: 15px; -} - -.paginationButton { - display: flex; - justify-content: center; - align-items: center; - cursor: pointer; - background-color: #24242c; - border: none; - border-radius: 10px; - width: 30px; - height: 25px; - font-size: 16px; -} - -.paginationButton:disabled { - cursor: not-allowed; - background-color: #d1d5db; -} diff --git a/app/components/boards/AllPost.tsx b/app/components/boards/AllPost.tsx index b1d05ac64..073b8ddd3 100644 --- a/app/components/boards/AllPost.tsx +++ b/app/components/boards/AllPost.tsx @@ -8,6 +8,7 @@ import SearchInput from "@/app/components/ui/SearchInput"; import { fetchArticles, Article } from "@/app/lib/api/api"; import Image from "next/image"; import Dropdown from "../ui/Dropdown"; +import Pagination from "./AllPostPagination"; export default function AllPost() { const router = useRouter(); @@ -42,7 +43,11 @@ export default function AllPost() { }; useEffect(() => { - const query = searchParams.get("q") || ""; + const getSearchQuery = (): string => { + return searchParams.get("q") || ""; + }; + + const query = getSearchQuery(); fetchArticlesFromApi(query); }, [searchParams]); @@ -147,35 +152,11 @@ export default function AllPost() { )} -
- - {currentPage} - -
+ ); } diff --git a/app/components/boards/AllPostPagination.module.css b/app/components/boards/AllPostPagination.module.css new file mode 100644 index 000000000..102ef2def --- /dev/null +++ b/app/components/boards/AllPostPagination.module.css @@ -0,0 +1,25 @@ +.pagination { + margin-bottom: 10px; + display: flex; + justify-content: center; + align-items: center; + gap: 15px; +} + +.paginationButton { + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + background-color: #24242c; + border: none; + border-radius: 10px; + width: 30px; + height: 25px; + font-size: 16px; +} + +.paginationButton:disabled { + cursor: not-allowed; + background-color: #d1d5db; +} diff --git a/app/components/boards/AllPostPagination.tsx b/app/components/boards/AllPostPagination.tsx new file mode 100644 index 000000000..57b66ca9b --- /dev/null +++ b/app/components/boards/AllPostPagination.tsx @@ -0,0 +1,44 @@ +import Image from "next/image"; +import styles from "./AllPostPagination.module.css"; + +export default function Pagination({ + currentPage, + totalArticles, + onPageChange, +}: { + currentPage: number; + totalArticles: number; + onPageChange: (page: number) => void; +}) { + const totalPages = Math.ceil(totalArticles / 10); + + return ( +
+ + {currentPage} + +
+ ); +} diff --git a/next.config.js b/next.config.js index 833dcdee4..52533a143 100644 --- a/next.config.js +++ b/next.config.js @@ -31,6 +31,16 @@ const nextConfig = { hostname: "flexible.img.hani.co.kr", pathname: "/**", }, + { + protocol: "https", + hostname: "mblogthumb-phinf.pstatic.net", + pathname: "/**", + }, + { + protocol: "https", + hostname: "ibb.co", + pathname: "/**", + }, ], }, }; From b398451488c36672dc0f2a4de00ad90010735216 Mon Sep 17 00:00:00 2001 From: 1022gusl <1022gusl@gmail.com> Date: Thu, 5 Dec 2024 06:45:04 +0900 Subject: [PATCH 04/13] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/boards/AllPost.tsx | 94 ++++++++++++++++--------------- 1 file changed, 49 insertions(+), 45 deletions(-) diff --git a/app/components/boards/AllPost.tsx b/app/components/boards/AllPost.tsx index 073b8ddd3..8fc16115b 100644 --- a/app/components/boards/AllPost.tsx +++ b/app/components/boards/AllPost.tsx @@ -78,6 +78,54 @@ export default function AllPost() { const paginatedArticles = getPaginatedArticles(); + const PostList = ({ articles }: { articles: Article[] }) => ( +
+ {articles.length > 0 ? ( + articles.map((article) => ( + +
+
+

{article.title}

+
+ {article.title +
+
+
+
+

{article.writer.nickname}

+
+ {new Date(article.createdAt).toLocaleDateString()} +
+
+
+ 하트 + {article.likeCount} +
+
+
+ + )) + ) : ( +

검색 결과가 없습니다.

+ )} +
+ ); + return (
@@ -106,51 +154,7 @@ export default function AllPost() { )}
-
- {paginatedArticles.length > 0 ? ( - paginatedArticles.map((article) => ( - -
-
-

{article.title}

-
- {article.title -
-
-
-
-

{article.writer.nickname}

-
- {new Date(article.createdAt).toLocaleDateString()} -
-
-
- 하트 - {article.likeCount} -
-
-
- - )) - ) : ( -

검색 결과가 없습니다.

- )} -
+ Date: Thu, 5 Dec 2024 06:48:33 +0900 Subject: [PATCH 05/13] =?UTF-8?q?feat:=20=EC=A3=BC=EC=84=9D=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/boards/page.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/boards/page.tsx b/app/boards/page.tsx index 6a84b34e7..78f1112b4 100644 --- a/app/boards/page.tsx +++ b/app/boards/page.tsx @@ -7,6 +7,10 @@ export default function boards() { <> 로딩 중
}> + {/* 이 부분에 suspense를 감싼 이유가 빌드 할 때 오류가 뜨더라고요...? + 멘토링 시간에 들었던 기억이 있어서 suspense가 어떤 역할인지는 알겠는데 + 오류를 해결하려면 suspense를 감싸달라고 해서 사용했습니다! + */} From bbed81e7a32f6d058dee45af4fe3c040a7339256 Mon Sep 17 00:00:00 2001 From: 1022gusl <1022gusl@gmail.com> Date: Sat, 7 Dec 2024 04:54:19 +0900 Subject: [PATCH 06/13] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=9E=84=EC=8B=9C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BeforeSprintReact/login.html | 146 +++++++++++------- app/components/ConditionalLayout.tsx | 23 +++ app/global.css | 13 ++ app/layout.tsx | 5 +- app/login/page.tsx | 137 ++++++++++++++++ app/signup/page.tsx | 3 + app/styles/sign.module.css | 121 +++++++++++++++ public/social/google.png | Bin 0 -> 1608 bytes public/social/kakao.png | Bin 0 -> 1439 bytes 9 files changed, 388 insertions(+), 60 deletions(-) create mode 100644 app/components/ConditionalLayout.tsx create mode 100644 app/login/page.tsx create mode 100644 app/signup/page.tsx create mode 100644 app/styles/sign.module.css create mode 100644 public/social/google.png create mode 100644 public/social/kakao.png diff --git a/app/beforemission/BeforeSprintReact/login.html b/app/beforemission/BeforeSprintReact/login.html index 7d78938e8..a84aa334b 100644 --- a/app/beforemission/BeforeSprintReact/login.html +++ b/app/beforemission/BeforeSprintReact/login.html @@ -1,60 +1,92 @@ - - - - - 판다마켓-로그인 - - - - - -
- -
-
-
- -
- - 이메일을 입력해 주세요 - 잘못된 이메일 형식입니다 -
+ + + + + 판다마켓-로그인 + + + + + +
+ +
+ +
+ +
+ + 이메일을 입력해 주세요 + 잘못된 이메일 형식입니다 +
-
- -
- - 보이게 - - 비밀번호를 입력해 주세요 - 비밀번호를 8자 이상 입력해주세요 -
- - -
- - -
- - - \ No newline at end of file +
+ +
+ + + 비밀번호를 입력해 주세요 + 비밀번호를 8자 이상 입력해주세요 +
+ + +
+ + +
+ + + diff --git a/app/components/ConditionalLayout.tsx b/app/components/ConditionalLayout.tsx new file mode 100644 index 000000000..5ed6108bb --- /dev/null +++ b/app/components/ConditionalLayout.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { usePathname } from "next/navigation"; +import Header from "./layout/Header"; + +export default function ConditionalLayout({ + children, +}: { + children: React.ReactNode; +}) { + const pathname = usePathname(); + + if (pathname === "/login" || pathname === "signup") { + return <>{children}; + } + + return ( + <> +
+ {children} + + ); +} diff --git a/app/global.css b/app/global.css index e635cdef1..f4d4916d6 100644 --- a/app/global.css +++ b/app/global.css @@ -15,6 +15,19 @@ a { color: inherit; } +input { + outline: none; + border: none; +} + +input:focus { + border: 2px solid var(--blue); +} + +input::placeholder { + color: var(--gray400); +} + @media (min-width: 375px) { .container { margin: 70px 20px 0; diff --git a/app/layout.tsx b/app/layout.tsx index 31fa732fe..72634d620 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,5 +1,5 @@ -import Header from "./components/layout/Header"; import "./global.css"; +import ConditionalLayout from "./components/ConditionalLayout"; export const metadata = { title: "판다마켓", @@ -14,8 +14,7 @@ export default function RootLayout({ return ( -
- {children} + {children} ); diff --git a/app/login/page.tsx b/app/login/page.tsx new file mode 100644 index 000000000..157fd11ef --- /dev/null +++ b/app/login/page.tsx @@ -0,0 +1,137 @@ +"use client"; + +import { useState, useEffect } from "react"; +import Link from "next/link"; +import Image from "next/image"; +import styles from "../styles/sign.module.css"; + +export default function LoginPage() { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [emailError, setEmailError] = useState(""); + const [passwordError, setPasswordError] = useState(""); + const [isFormValid, setIsFormValid] = useState(false); + + useEffect(() => { + const isEmailValid = + /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(email); + const isPasswordValid = password.length >= 8; + + setEmailError( + !email + ? "이메일을 입력해 주세요" + : isEmailValid + ? "" + : "잘못된 이메일 형식입니다" + ); + setPasswordError( + !password + ? "비밀번호를 입력해 주세요" + : isPasswordValid + ? "" + : "비밀번호를 8자 이상 입력해주세요" + ); + + setIsFormValid(isEmailValid && isPasswordValid); + }, [email, password]); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (isFormValid) { + // 로그인 후 이동 + window.location.href = "/items"; + } + }; + + return ( +
+
+ + 판다 + +
+
+
+
+ +
+ setEmail(e.target.value)} + required + /> + {emailError && ( + {emailError} + )} +
+
+ +
+ setPassword(e.target.value)} + required + /> + {passwordError && ( + {passwordError} + )} +
+ +
+
+
+ 간편 로그인하기 + +
+
+ 판다마켓이 처음이신가요? + + 회원가입 + +
+
+ ); +} diff --git a/app/signup/page.tsx b/app/signup/page.tsx new file mode 100644 index 000000000..05164df0b --- /dev/null +++ b/app/signup/page.tsx @@ -0,0 +1,3 @@ +export default function signup() { + return
회원가입 페이지
; +} diff --git a/app/styles/sign.module.css b/app/styles/sign.module.css new file mode 100644 index 000000000..393e04531 --- /dev/null +++ b/app/styles/sign.module.css @@ -0,0 +1,121 @@ +/* login.module.scss */ + +@media (min-width: 1200px) { + /* 추가 스타일 정의 가능 */ +} + +@media (min-width: 375px) and (max-width: 768px) { + .loginHome { + max-width: 400px; + margin-left: 16px; + margin-right: 16px; + } + + .loginLogo a { + display: flex; + justify-content: center; + } + + .loginLogo img { + max-width: 80%; + } +} + +.loginHome { + margin-top: 1.25em; + width: 40em; + display: flex; + flex-direction: column; + color: var(--gray800); + margin-bottom: 1.875em; +} + +.loginLogo { + display: flex; + justify-content: center; +} + +.info { + margin: 1.5625em 0 1.875em; + font-weight: 700; + font-size: 1.125rem; + line-height: 1.625rem; +} + +.email, +.password, +.passwordRepeat, +.username { + width: 100%; + height: 3.5em; + border-radius: 0.75rem; + font-size: 1rem; + font-weight: 400; + line-height: 1.625rem; + background-color: var(--gray100); + padding: 1em 1.5em; + margin: 0.75em 0; +} + +.passwordLabel, +.passwordRepeat { + position: relative; +} + +/* 비밀번호 눈모양 임시 */ +.seeBlind { + position: absolute; + left: 92%; + top: 3.457rem; +} + +.formError { + color: #f74747; + display: none; + font-size: 1rem; + font-weight: 600; +} + +.loginButton { + background-color: var(--gray400); + height: 3.111em; + border-radius: 2.5rem; + color: var(--gray100); + line-height: 2rem; + font-size: 1.25rem; + font-weight: 600; + display: flex; + justify-content: center; + align-items: center; +} + +.simpleLogin { + font-size: 1rem; + font-weight: 500; + line-height: 1.625rem; + display: flex; + justify-content: space-between; + align-items: center; + background-color: #e6f2ff; + padding: 1em 1.4375em; +} + +.signup { + font-size: 0.875rem; + line-height: 1.5rem; + font-weight: 500; + display: flex; + justify-content: center; + margin-top: 0.9375em; + gap: 4px; +} + +.textDecorationUnderline { + text-decoration: underline; +} + +/* 임시 */ +.signupButton { + color: var(--blue); + line-height: 1.0625rem; +} diff --git a/public/social/google.png b/public/social/google.png new file mode 100644 index 0000000000000000000000000000000000000000..64db8acb02398ab97b35c24be447d2f2358e8952 GIT binary patch literal 1608 zcmV-O2DkZ%P)H$RK~#7F-I;A{ zQ&kwp|L5G7t!%J50f%555ilshB7q2j(0!RijPF6kD3CZ~48~zhBz|G2QDWlD2R~>q zfe;M|2>}hpmk&6Qh!SNb1B`@(iXR+)_tD+Y*0C+uM`#^6zt= zKKDM)VWTsO)Xi{{%?imE1E`B|%~3E7@Pg)o%KfN92R7<*I*#05i~ z7p7vYIH~a7ymv^^)ZXa`_aIz%Mq?`FK)A9ajiIqL zN7gjgVYC<+Fk`fcW!av{N^zV);W^Xl%4W08s3xXqzDASVa*=9N5N=u4W>kutFIuo_ z6^uF}G&il0%UOGAO_T#|Q_i%{+IID=Iuf*lwC0UmgpFuf%jr@$YgQDFu|)nqAay5o zO<7PX43<=5+V=MG5KAbWLs3|>bdkM*29WcUKaDq}!7z;W>-G6uf_#Uo>RC-uDgKWu zbic7|8<(5<`*6q6pH+2b>KpMy;b)9|eYcE`6v*tRkvhBL6 zA}k`^kL)*lVE+Ca>_1OHn7QJr+Wh-b+qw#RY$5n<(-0OJrQ}PrL?T+JPcX$>`HSM5 zhx?;t_v}Ju=NE9L=o_I(v6080M&zkA2!nKI65XlmT1!!I!FR)0S7b7{`u3ZiRm0Hm z{wL5EEe$osw8)op(wFNKgXZLX9cSSz+y6mFXt8zAw}lotOv@%6(#$v|2%`6@yrq(56%zfisS=K=8I|}VGM-X zi+rYbZK6pJl~U$dHy&ncyiQZoYlRK@E$gA0P6rw`b>I;M{3!Y#e?w*tH=ttnIz8bLjrZEq`=imQD`u&I9$j#-m|zxJc`j%B#_)>*!OQz!YwoWCM`Vu`ua z6$Do-z+Il=nx-Ymz(FPR882Z&=Al8cc{UyS5dWroM;t?9U;onZwcU3oM^i(H_%B7) z)~X3kc@tG+5Y#QyC?1}>*4NaNzJN#ezk)4CKgA!tCttnZGRxQ;CWm|V;5+!W=Xl`$+@6fu^Djd+W@GJg4KFMYF4%Tl z`Lee}YHB(>g$iT)C|eD~)}?QtWya#b{ay7UGSlD1%4H`*i6vy^0#uKqYu^LBsw#07 zGC$gOVmJ1kJ%HZK#T)jUsMVou-fBEMXBEOAi!3tjzwQB^$Y!m$sxlIdu#kTqJ>QM* zl812Y!YLG@PK}~tUgK;mnHIwnvsyjTVWC7+mU7ldbbW_UFhN$9DN?-Bdc-9oB^Plq zCDMpW$Xhodt-GMm<`Ya6N=;{7ntV1M=PcjbBb%&i+cSsOl6|J6{9m|Yqa!ob@rE?W z;IJi7;hS7zEQ^5GV-J>KOO~qsAH_iG=sH%5g$TDzYx_p%kSQiI^aY;kqKSc)TS9S+=bn5;?h^gplIfv+)OUe|Qyr3mz zAIwCPKdm=%uQ5?gFckWNHJ70lfC=aJZF{OWxw2&>A0z-1cZIq^@FukZO!(`4yExsO z%vrv2eiwH)M-rVfw93N-wHI3ljy>$BUFmgUcXKS&Awzj;KS@9f3E2lm-wx;nsWd=$}5ub(84 zD08WxqS3O+H>m*>_8eM!{4eEyF4D%v3)1D=G3W3uKb9(`E z2e|;uFGWav>?gFi_*Z3&0D!5g514Oa@=ZNPN&<$4F4UMb9`C4Jk47ckxqFEQ2G1Qm zW8oS36QzXYYS!26c{Y!WLg6KC?;L1y@pz{mL*o;Z7t4e%YB`X=Zx_g+`W{Y8v)}N` zrJ_1GU93HNPQH-h11SPlqKzm2SH_DAf79JBf7j#X<$v@zT_%l(4SjUMZz`?Hcwd)} z_#J*p7dtzL6o44gQ&Sa^Yv$3hUaj@T#bg5N!`pla*tzq}~ zt##Y!}dN`O6e6RwRU?A;dg)*!#DP{4Z>PZxnx)ZuUl;S}!B}lpHE0r#4 z`8Adw7>rOKUx!&AvH!;VJ9Plvo88v;RpY}Uf8Qo-yNg>_zUO`URX?fh)RUT^Y9-)4 z3*Wu(E5pT7PCCkYN&)!n);mUi)C`cWf?zB45G&JCkl;xH&a@V>gq3P^%qTNjy3FfME?JJF?wj0!=!B_8xd3h!xEDT|3Xb#=xCK-? z5!y9ZGE%BlG6d4qPG83t4LWHX?at65FMTg_cr=mjD1E0h4XdDm?0uA8v^xy5e z|7!7q_xDbzId;~4Cn^)#65QE~)U|YuRZkE=qlyf2Ya4)6`AOu2_T!0V9=d5Di?^e* zgJ&lX@NMM64HS3wRGa%zUgoZbZ{ds^_nnTkX*OsEI=Z=Y7YXR)8-Hwoc5M-w=YCLz zS^)kgkW)%byG^hlH2nFd)V>xsg`{z6JTt%#?<=m@ObNPZm}oUnBXAolBZTe^#8}%2 tIwEiB-2=&W#WgR;@g{j)1bqGQ!Yhw{G}-7Ix!nK&002ovPDHLkV1nMxsz(3- literal 0 HcmV?d00001 From 88b5231b2bf0acd6db5393f7b24365a4c540de49 Mon Sep 17 00:00:00 2001 From: 1022gusl <1022gusl@gmail.com> Date: Sat, 7 Dec 2024 06:30:08 +0900 Subject: [PATCH 07/13] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/hooks/useSign.ts | 61 ++++++++++ app/login/page.tsx | 227 ++++++++++++++++++------------------- app/styles/sign.module.css | 79 ++++++------- public/logos/logo.png | Bin 0 -> 6319 bytes 4 files changed, 210 insertions(+), 157 deletions(-) create mode 100644 app/hooks/useSign.ts create mode 100644 public/logos/logo.png diff --git a/app/hooks/useSign.ts b/app/hooks/useSign.ts new file mode 100644 index 000000000..13cec3376 --- /dev/null +++ b/app/hooks/useSign.ts @@ -0,0 +1,61 @@ +"use client"; + +import { useState } from "react"; + +export default function useSign() { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [emailError, setEmailError] = useState(""); + const [passwordError, setPasswordError] = useState(""); + const [isFormValid, setIsFormValid] = useState(false); + + const validateEmail = (value: string) => { + const isValid = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test( + value + ); + setEmailError( + !value + ? "이메일을 입력해 주세요" + : isValid + ? "" + : "잘못된 이메일 형식입니다" + ); + return isValid; + }; + + const validatePassword = (value: string) => { + const isValid = value.length >= 8; + setPasswordError( + !value + ? "비밀번호를 입력해 주세요" + : isValid + ? "" + : "비밀번호를 8자 이상 입력해주세요" + ); + return isValid; + }; + + const handleEmailChange = (value: string) => { + setEmail(value); + const isEmailValid = validateEmail(value); + const isPasswordValid = validatePassword(password); + setIsFormValid(isEmailValid && isPasswordValid); + }; + + const handlePasswordChange = (value: string) => { + setPassword(value); + const isPasswordValid = validatePassword(value); + const isEmailValid = validateEmail(email); + setIsFormValid(isEmailValid && isPasswordValid); + }; + + return { + email, + setEmail: handleEmailChange, + password, + setPassword: handlePasswordChange, + emailError, + passwordError, + isFormValid, + }; +} diff --git a/app/login/page.tsx b/app/login/page.tsx index 157fd11ef..ab6abeac9 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -1,137 +1,134 @@ "use client"; -import { useState, useEffect } from "react"; import Link from "next/link"; import Image from "next/image"; import styles from "../styles/sign.module.css"; +import useSign from "../hooks/useSign"; export default function LoginPage() { - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const [emailError, setEmailError] = useState(""); - const [passwordError, setPasswordError] = useState(""); - const [isFormValid, setIsFormValid] = useState(false); - - useEffect(() => { - const isEmailValid = - /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(email); - const isPasswordValid = password.length >= 8; - - setEmailError( - !email - ? "이메일을 입력해 주세요" - : isEmailValid - ? "" - : "잘못된 이메일 형식입니다" - ); - setPasswordError( - !password - ? "비밀번호를 입력해 주세요" - : isPasswordValid - ? "" - : "비밀번호를 8자 이상 입력해주세요" - ); - - setIsFormValid(isEmailValid && isPasswordValid); - }, [email, password]); + const { + email, + setEmail, + password, + setPassword, + emailError, + passwordError, + isFormValid, + } = useSign(); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (isFormValid) { - // 로그인 후 이동 - window.location.href = "/items"; + window.location.href = "/items"; // 로그인 성공 시 이동 } }; return ( -
-
- - 판다 - -
-
-
-
- -
- setEmail(e.target.value)} - required - /> - {emailError && ( - {emailError} - )} -
-
- -
- setPassword(e.target.value)} - required - /> - {passwordError && ( - {passwordError} - )} -
- -
-
-
- 간편 로그인하기 -
- - 구글 - - +
+ -
- 판다마켓이 처음이신가요? - - 회원가입 - +
+
+
+
+ +
+ setEmail(e.target.value)} + required + className={styles.inputfield} + /> + {emailError && ( + {emailError} + )} +
+
+ +
+ setPassword(e.target.value)} + required + className={styles.inputfield} + /> + {passwordError && ( + {passwordError} + )} +
+ +
+
+
+ 간편 로그인하기 + +
+
+ 판다마켓이 처음이신가요? + + 회원가입 + +
-
+ ); } diff --git a/app/styles/sign.module.css b/app/styles/sign.module.css index 393e04531..ded091d8f 100644 --- a/app/styles/sign.module.css +++ b/app/styles/sign.module.css @@ -1,38 +1,25 @@ -/* login.module.scss */ - -@media (min-width: 1200px) { - /* 추가 스타일 정의 가능 */ +.header { + display: flex; + justify-content: center; + align-items: center; } -@media (min-width: 375px) and (max-width: 768px) { - .loginHome { - max-width: 400px; - margin-left: 16px; - margin-right: 16px; - } - - .loginLogo a { - display: flex; - justify-content: center; - } - - .loginLogo img { - max-width: 80%; - } +.logoContainer { + position: relative; + width: 396px; + height: 132px; } -.loginHome { - margin-top: 1.25em; - width: 40em; - display: flex; - flex-direction: column; - color: var(--gray800); - margin-bottom: 1.875em; +.loginLogo { + object-fit: contain; } -.loginLogo { +.content { display: flex; + flex-direction: column; justify-content: center; + max-width: 640px; + width: 100%; } .info { @@ -42,51 +29,52 @@ line-height: 1.625rem; } -.email, -.password, -.passwordRepeat, -.username { +.inputfield { width: 100%; height: 3.5em; border-radius: 0.75rem; font-size: 1rem; font-weight: 400; line-height: 1.625rem; - background-color: var(--gray100); + background-color: #f3f4f6; padding: 1em 1.5em; margin: 0.75em 0; } +.inputfield::placeholder { + color: #9ca3af; +} + .passwordLabel, .passwordRepeat { position: relative; } -/* 비밀번호 눈모양 임시 */ -.seeBlind { - position: absolute; - left: 92%; - top: 3.457rem; -} - .formError { color: #f74747; - display: none; font-size: 1rem; font-weight: 600; } .loginButton { - background-color: var(--gray400); + width: 100%; + background-color: #9ca3af; height: 3.111em; border-radius: 2.5rem; - color: var(--gray100); + border: none; + color: #f3f4f6; line-height: 2rem; font-size: 1.25rem; font-weight: 600; display: flex; justify-content: center; align-items: center; + cursor: not-allowed; +} + +.activeLoginButton { + background-color: #3692ff; + cursor: pointer; } .simpleLogin { @@ -119,3 +107,10 @@ color: var(--blue); line-height: 1.0625rem; } + +@media (max-width: 1200px) { + .logoContainer { + width: 266px; + height: 90px; + } +} diff --git a/public/logos/logo.png b/public/logos/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..5e01a0e201029ab5f36c186eaa211d1117b13772 GIT binary patch literal 6319 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 Date: Sat, 7 Dec 2024 07:26:08 +0900 Subject: [PATCH 08/13] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/ConditionalLayout.tsx | 2 +- app/components/sign/SocialLogin.tsx | 29 +++++ app/hooks/useSign.ts | 12 +- app/login/page.tsx | 31 +---- app/signup/page.tsx | 186 ++++++++++++++++++++++++++- app/styles/sign.module.css | 13 +- 6 files changed, 228 insertions(+), 45 deletions(-) create mode 100644 app/components/sign/SocialLogin.tsx diff --git a/app/components/ConditionalLayout.tsx b/app/components/ConditionalLayout.tsx index 5ed6108bb..0def7fd84 100644 --- a/app/components/ConditionalLayout.tsx +++ b/app/components/ConditionalLayout.tsx @@ -10,7 +10,7 @@ export default function ConditionalLayout({ }) { const pathname = usePathname(); - if (pathname === "/login" || pathname === "signup") { + if (pathname === "/login" || pathname === "/signup") { return <>{children}; } diff --git a/app/components/sign/SocialLogin.tsx b/app/components/sign/SocialLogin.tsx new file mode 100644 index 000000000..a753120b7 --- /dev/null +++ b/app/components/sign/SocialLogin.tsx @@ -0,0 +1,29 @@ +"use client"; + +import Image from "next/image"; +import styles from "../../styles/sign.module.css"; + +export default function SocialLogin() { + return ( +
+ 간편 로그인하기 +
+ + 구글 + + + + 카카오 + +
+
+ ); +} diff --git a/app/hooks/useSign.ts b/app/hooks/useSign.ts index 13cec3376..dfe57c1d7 100644 --- a/app/hooks/useSign.ts +++ b/app/hooks/useSign.ts @@ -7,7 +7,6 @@ export default function useSign() { const [password, setPassword] = useState(""); const [emailError, setEmailError] = useState(""); const [passwordError, setPasswordError] = useState(""); - const [isFormValid, setIsFormValid] = useState(false); const validateEmail = (value: string) => { const isValid = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test( @@ -37,18 +36,17 @@ export default function useSign() { const handleEmailChange = (value: string) => { setEmail(value); - const isEmailValid = validateEmail(value); - const isPasswordValid = validatePassword(password); - setIsFormValid(isEmailValid && isPasswordValid); + validateEmail(value); }; const handlePasswordChange = (value: string) => { setPassword(value); - const isPasswordValid = validatePassword(value); - const isEmailValid = validateEmail(email); - setIsFormValid(isEmailValid && isPasswordValid); + validatePassword(value); }; + const isFormValid = + emailError === "" && passwordError === "" && email && password; + return { email, setEmail: handleEmailChange, diff --git a/app/login/page.tsx b/app/login/page.tsx index ab6abeac9..6ab1cd1c4 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -4,6 +4,7 @@ import Link from "next/link"; import Image from "next/image"; import styles from "../styles/sign.module.css"; import useSign from "../hooks/useSign"; +import SocialLogin from "../components/sign/SocialLogin"; export default function LoginPage() { const { @@ -93,35 +94,7 @@ export default function LoginPage() { -
- 간편 로그인하기 - -
+
판다마켓이 처음이신가요? diff --git a/app/signup/page.tsx b/app/signup/page.tsx index 05164df0b..af0d3ba65 100644 --- a/app/signup/page.tsx +++ b/app/signup/page.tsx @@ -1,3 +1,185 @@ -export default function signup() { - return
회원가입 페이지
; +"use client"; + +import Link from "next/link"; +import Image from "next/image"; +import styles from "../styles/sign.module.css"; +import useSign from "../hooks/useSign"; +import { useState } from "react"; +import SocialLogin from "../components/sign/SocialLogin"; + +export default function SignupPage() { + const { + email, + setEmail, + password, + setPassword, + emailError, + passwordError, + isFormValid, + } = useSign(); + + const [username, setUsername] = useState(""); + const [usernameError, setUsernameError] = useState(""); + const [passwordRepeat, setPasswordRepeat] = useState(""); + const [passwordRepeatError, setPasswordRepeatError] = useState(""); + + const validateUsername = (value: string) => { + setUsernameError(!value ? "닉네임을 입력해 주세요" : ""); + return !!value; + }; + + const validatePasswordRepeat = (value: string) => { + const isValid = value === password; + setPasswordRepeatError( + !value + ? "비밀번호 확인을 입력해 주세요" + : isValid + ? "" + : "비밀번호가 일치하지 않습니다" + ); + return isValid; + }; + + const handleUsernameChange = (value: string) => { + setUsername(value); + validateUsername(value); + }; + + const handlePasswordRepeatChange = (value: string) => { + setPasswordRepeat(value); + validatePasswordRepeat(value); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if ( + validateUsername(username) && + validatePasswordRepeat(passwordRepeat) && + isFormValid + ) { + alert("회원가입 성공!"); + window.location.href = "/login"; + } + }; + + return ( +
+
+
+ + 판다 로고 + +
+
+
+
+
+
+ +
+ setEmail(e.target.value)} + required + className={styles.inputfield} + /> + {emailError && ( + {emailError} + )} +
+ +
+ +
+ handleUsernameChange(e.target.value)} + required + className={styles.inputfield} + /> + {usernameError && ( + {usernameError} + )} +
+ +
+ +
+ setPassword(e.target.value)} + required + className={styles.inputfield} + /> + {passwordError && ( + {passwordError} + )} +
+ +
+ +
+ handlePasswordRepeatChange(e.target.value)} + required + className={styles.inputfield} + /> + {passwordRepeatError && ( + {passwordRepeatError} + )} +
+ + +
+
+ +
+ 이미 회원이신가요? + + 로그인 + +
+
+
+ ); } diff --git a/app/styles/sign.module.css b/app/styles/sign.module.css index ded091d8f..fc92233a6 100644 --- a/app/styles/sign.module.css +++ b/app/styles/sign.module.css @@ -88,6 +88,13 @@ padding: 1em 1.4375em; } +.social { + display: flex; + justify-content: center; + align-items: center; + gap: 10px; +} + .signup { font-size: 0.875rem; line-height: 1.5rem; @@ -102,12 +109,6 @@ text-decoration: underline; } -/* 임시 */ -.signupButton { - color: var(--blue); - line-height: 1.0625rem; -} - @media (max-width: 1200px) { .logoContainer { width: 266px; From 0d849cdaf1c62f2c1dbf89b8df910d07b111ec2d Mon Sep 17 00:00:00 2001 From: 1022gusl <1022gusl@gmail.com> Date: Sat, 7 Dec 2024 07:41:01 +0900 Subject: [PATCH 09/13] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20post=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/lib/api/api.ts | 22 ++++++++++++++++++++++ app/signup/page.tsx | 14 +++++++++++--- app/styles/sign.module.css | 1 + test.http | 9 +++++++++ 4 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 test.http diff --git a/app/lib/api/api.ts b/app/lib/api/api.ts index 41a173a28..f905d2380 100644 --- a/app/lib/api/api.ts +++ b/app/lib/api/api.ts @@ -163,3 +163,25 @@ export async function postComment( throw error; } } + +export const signUp = async ( + email: string, + nickname: string, + password: string, + passwordConfirmation: string +) => { + const response = await fetch(`${BASE_URL}auth/signUp`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email, nickname, password, passwordConfirmation }), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || "회원가입 실패"); + } + + return response.json(); +}; diff --git a/app/signup/page.tsx b/app/signup/page.tsx index af0d3ba65..8ace840cd 100644 --- a/app/signup/page.tsx +++ b/app/signup/page.tsx @@ -6,6 +6,7 @@ import styles from "../styles/sign.module.css"; import useSign from "../hooks/useSign"; import { useState } from "react"; import SocialLogin from "../components/sign/SocialLogin"; +import { signUp } from "../lib/api/api"; export default function SignupPage() { const { @@ -50,15 +51,22 @@ export default function SignupPage() { validatePasswordRepeat(value); }; - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); + if ( validateUsername(username) && validatePasswordRepeat(passwordRepeat) && isFormValid ) { - alert("회원가입 성공!"); - window.location.href = "/login"; + try { + // API 호출 + await signUp(email, username, password, passwordRepeat); + alert("회원가입 성공!"); + window.location.href = "/login"; + } catch (error: any) { + alert(error.message || "회원가입 중 오류가 발생했습니다."); + } } }; diff --git a/app/styles/sign.module.css b/app/styles/sign.module.css index fc92233a6..11fa67d7e 100644 --- a/app/styles/sign.module.css +++ b/app/styles/sign.module.css @@ -107,6 +107,7 @@ .textDecorationUnderline { text-decoration: underline; + color: #3282f6; } @media (max-width: 1200px) { diff --git a/test.http b/test.http new file mode 100644 index 000000000..1652be9d4 --- /dev/null +++ b/test.http @@ -0,0 +1,9 @@ +POST https://panda-market-api.vercel.app/auth/signUp +Content-Type: application/json + +{ + "email": "test999@example.com", + "nickname": "testuser", + "password": "testpassword999", + "passwordConfirmation": "testpassword999" +} \ No newline at end of file From d07e69d4a149342f9c57d4e794dc273672db482f Mon Sep 17 00:00:00 2001 From: 1022gusl <1022gusl@gmail.com> Date: Sat, 7 Dec 2024 07:58:43 +0900 Subject: [PATCH 10/13] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8,=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20post=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/layout/Header.tsx | 2 +- app/lib/api/api.ts | 17 +++++++++++++++++ app/login/page.tsx | 25 ++++++++++++++++++++++--- app/signup/page.tsx | 12 ++++++++++-- public/logos/panda.png | Bin 2807 -> 0 bytes test.http | 9 +++++++++ 6 files changed, 59 insertions(+), 6 deletions(-) delete mode 100644 public/logos/panda.png diff --git a/app/components/layout/Header.tsx b/app/components/layout/Header.tsx index 04829c43e..db221db7d 100644 --- a/app/components/layout/Header.tsx +++ b/app/components/layout/Header.tsx @@ -21,7 +21,7 @@ function Header() { 판다마켓 { + const response = await fetch(`${BASE_URL}auth/signIn`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email, password }), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || "로그인 실패"); + } + + return response.json(); +}; diff --git a/app/login/page.tsx b/app/login/page.tsx index 6ab1cd1c4..c195dc684 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -1,10 +1,13 @@ "use client"; import Link from "next/link"; +import { useRouter } from "next/navigation"; import Image from "next/image"; import styles from "../styles/sign.module.css"; import useSign from "../hooks/useSign"; import SocialLogin from "../components/sign/SocialLogin"; +import { signIn } from "../lib/api/api"; +import { useEffect } from "react"; export default function LoginPage() { const { @@ -16,11 +19,27 @@ export default function LoginPage() { passwordError, isFormValid, } = useSign(); + const router = useRouter(); - const handleSubmit = (e: React.FormEvent) => { + useEffect(() => { + const token = localStorage.getItem("accessToken"); + if (token) { + router.push("/"); + } + }, [router]); + + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - if (isFormValid) { - window.location.href = "/items"; // 로그인 성공 시 이동 + + if (!isFormValid) return; + + try { + const { accessToken } = await signIn(email, password); + localStorage.setItem("accessToken", accessToken); + alert("로그인 성공!"); + router.push("/"); + } catch (error: any) { + alert(error.message || "로그인 중 오류가 발생했습니다."); } }; diff --git a/app/signup/page.tsx b/app/signup/page.tsx index 8ace840cd..332722acb 100644 --- a/app/signup/page.tsx +++ b/app/signup/page.tsx @@ -4,9 +4,10 @@ import Link from "next/link"; import Image from "next/image"; import styles from "../styles/sign.module.css"; import useSign from "../hooks/useSign"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import SocialLogin from "../components/sign/SocialLogin"; import { signUp } from "../lib/api/api"; +import { useRouter } from "next/navigation"; export default function SignupPage() { const { @@ -18,6 +19,14 @@ export default function SignupPage() { passwordError, isFormValid, } = useSign(); + const router = useRouter(); + + useEffect(() => { + const token = localStorage.getItem("accessToken"); + if (token) { + router.push("/"); + } + }, [router]); const [username, setUsername] = useState(""); const [usernameError, setUsernameError] = useState(""); @@ -60,7 +69,6 @@ export default function SignupPage() { isFormValid ) { try { - // API 호출 await signUp(email, username, password, passwordRepeat); alert("회원가입 성공!"); window.location.href = "/login"; diff --git a/public/logos/panda.png b/public/logos/panda.png deleted file mode 100644 index 85d6f64a4aad6bf4e402746f1778b38247091ca0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2807 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 JPDHLkV1m`oU-tk2 diff --git a/test.http b/test.http index 1652be9d4..c413577ab 100644 --- a/test.http +++ b/test.http @@ -6,4 +6,13 @@ Content-Type: application/json "nickname": "testuser", "password": "testpassword999", "passwordConfirmation": "testpassword999" +} + +### +POST https://panda-market-api.vercel.app/auth/signIn +Content-Type: application/json + +{ + "email": "test999@example.com", + "password": "testpassword999" } \ No newline at end of file From 1e0ca3dac5958bded18cd3b387cb97970b6d10be Mon Sep 17 00:00:00 2001 From: 1022gusl <1022gusl@gmail.com> Date: Sat, 7 Dec 2024 08:14:09 +0900 Subject: [PATCH 11/13] =?UTF-8?q?feat:=20=EA=B8=B0=EC=A1=B4=20=EB=AF=B8?= =?UTF-8?q?=EC=85=98=EC=97=90=20=ED=86=A0=ED=81=B0=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/addboard/page.tsx | 28 +++++++++++++-------- app/components/boards/id/CommentSection.tsx | 6 ----- app/components/ui/ImageUpload.tsx | 11 +++++--- app/lib/api/api.ts | 25 ++++++++++++++++++ test.http | 2 +- 5 files changed, 51 insertions(+), 21 deletions(-) diff --git a/app/addboard/page.tsx b/app/addboard/page.tsx index 36512450d..855927a58 100644 --- a/app/addboard/page.tsx +++ b/app/addboard/page.tsx @@ -3,24 +3,29 @@ import { useState } from "react"; import ImageUpload from "../components/ui/ImageUpload"; import InputField from "../components/ui/InputField"; +import { addArticle } from "../lib/api/api"; import styles from "./AddBoard.module.css"; export default function AddBoard() { const [title, setTitle] = useState(""); const [content, setContent] = useState(""); + const [image, setImage] = useState(null); // 이미지 파일 상태 추가 const isButtonDisabled = !title || !content; - const handleSubmit = () => { - alert("등록했습니다!"); - setTitle(""); - setContent(""); + const handleSubmit = async () => { + try { + const imageUrl = image ? URL.createObjectURL(image) : undefined; + await addArticle(title, content, imageUrl); + + alert("게시물이 성공적으로 등록되었습니다!"); + setTitle(""); + setContent(""); + setImage(null); + } catch (error: any) { + alert(error.message || "게시물 등록 중 오류가 발생했습니다."); + } }; - /*심화 요구사항에 회원가입, 로그인 api를 사용하여 받은 accessToken을 사용하여 게시물 등록을 합니다가 있는데 - 어떻게 해야 할 지 감이 안 잡힙니다... - 로그인 기능을 추가해야 하나요? 아니면 백엔드 서버에서 받아와야 하는건가요? - 일단 등록버튼을 클릭하면 인풋필드를 비우고 등록했다는 내용을 화면에 표시되도록 했습니다 - */ return (
@@ -50,7 +55,10 @@ export default function AddBoard() { value={content} onChange={(e) => setContent(e.target.value)} /> - + setImage(file)} // 이미지 파일 상태 관리 + />
); } diff --git a/app/components/boards/id/CommentSection.tsx b/app/components/boards/id/CommentSection.tsx index a5b1ac7bd..8d09b58b9 100644 --- a/app/components/boards/id/CommentSection.tsx +++ b/app/components/boards/id/CommentSection.tsx @@ -36,12 +36,6 @@ export default function CommentSection({ articleId }: CommentSectionProps) { const handleCommentSubmit = async () => { try { const token = localStorage.getItem("accessToken"); - /* 이 부분도 위 addboard 컴포넌트에서 주석으로 달았던 내용과 같습니다. - localStorage.setItem("accessToken", "abcd1234"); 처럼 임시로 해봤는데 - 유효한 토큰이 아니라고 하네요. 그래서 그런지 401 사용자 인증 에러가 뜹니다. - 제가 잘 이해하고 있는 지 모르겠어요. 구글링하면서 해보고 있는거라 ㅜㅜ - 백엔드에서 유효한 토큰을 어떻게 받아오나요? - */ if (!token) { alert("로그인이 필요합니다."); return; diff --git a/app/components/ui/ImageUpload.tsx b/app/components/ui/ImageUpload.tsx index 8388e0905..ccba6b7e3 100644 --- a/app/components/ui/ImageUpload.tsx +++ b/app/components/ui/ImageUpload.tsx @@ -7,11 +7,12 @@ import styles from "./ImageUpload.module.css"; interface ImageUploadProps { title: string; + onImageChange: (file: File | null) => void; // 상위 컴포넌트로 이미지 전달 } -function ImageUpload({ title }: ImageUploadProps) { - const [preview, setPreview] = useState(""); - const [errorMessage, setErrorMessage] = useState(""); +function ImageUpload({ title, onImageChange }: ImageUploadProps) { + const [preview, setPreview] = useState(""); // 미리보기 URL + const [errorMessage, setErrorMessage] = useState(""); // 에러 메시지 const fileInput = useRef(null); const inputId = "imageUpload"; @@ -27,9 +28,10 @@ function ImageUpload({ title }: ImageUploadProps) { const handleImageUpload = (e: ChangeEvent) => { const file = e.target.files?.[0]; if (file && file.type.startsWith("image/")) { - const prevUrl = URL.createObjectURL(file); + const prevUrl = URL.createObjectURL(file); // 미리보기 URL 생성 setPreview(prevUrl); setErrorMessage(""); + onImageChange(file); // 이미지 파일 전달 } else { alert("이미지 파일만 업로드 가능합니다."); } @@ -38,6 +40,7 @@ function ImageUpload({ title }: ImageUploadProps) { const handleImageDelete = () => { setPreview(""); setErrorMessage(""); + onImageChange(null); // 이미지 삭제 시 null 전달 if (fileInput.current) { fileInput.current.value = ""; } diff --git a/app/lib/api/api.ts b/app/lib/api/api.ts index c66349d79..514e9747a 100644 --- a/app/lib/api/api.ts +++ b/app/lib/api/api.ts @@ -202,3 +202,28 @@ export const signIn = async (email: string, password: string) => { return response.json(); }; + +export const addArticle = async ( + title: string, + content: string, + image?: string +): Promise => { + const token = localStorage.getItem("accessToken"); + if (!token) { + throw new Error("로그인이 필요합니다."); + } + + const response = await fetch(`${BASE_URL}articles`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ title, content, image }), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || "게시물 등록 실패"); + } +}; diff --git a/test.http b/test.http index c413577ab..7bb63a70b 100644 --- a/test.http +++ b/test.http @@ -15,4 +15,4 @@ Content-Type: application/json { "email": "test999@example.com", "password": "testpassword999" -} \ No newline at end of file +} From c7f66fab24708681a9d19f6c30f82d2cb829c2f2 Mon Sep 17 00:00:00 2001 From: 1022gusl <1022gusl@gmail.com> Date: Sat, 7 Dec 2024 08:35:18 +0900 Subject: [PATCH 12/13] =?UTF-8?q?refactor:=20=EB=A0=88=EC=9D=B4=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/addboard/page.tsx | 7 ++----- app/components/ui/ImageUpload.tsx | 12 ++++++------ app/components/ui/SearchInput.tsx | 2 +- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/app/addboard/page.tsx b/app/addboard/page.tsx index 855927a58..f59d59049 100644 --- a/app/addboard/page.tsx +++ b/app/addboard/page.tsx @@ -9,7 +9,7 @@ import styles from "./AddBoard.module.css"; export default function AddBoard() { const [title, setTitle] = useState(""); const [content, setContent] = useState(""); - const [image, setImage] = useState(null); // 이미지 파일 상태 추가 + const [image, setImage] = useState(null); const isButtonDisabled = !title || !content; @@ -55,10 +55,7 @@ export default function AddBoard() { value={content} onChange={(e) => setContent(e.target.value)} /> - setImage(file)} // 이미지 파일 상태 관리 - /> + setImage(file)} />
); } diff --git a/app/components/ui/ImageUpload.tsx b/app/components/ui/ImageUpload.tsx index ccba6b7e3..54ff3514a 100644 --- a/app/components/ui/ImageUpload.tsx +++ b/app/components/ui/ImageUpload.tsx @@ -7,12 +7,12 @@ import styles from "./ImageUpload.module.css"; interface ImageUploadProps { title: string; - onImageChange: (file: File | null) => void; // 상위 컴포넌트로 이미지 전달 + onImageChange: (file: File | null) => void; } function ImageUpload({ title, onImageChange }: ImageUploadProps) { - const [preview, setPreview] = useState(""); // 미리보기 URL - const [errorMessage, setErrorMessage] = useState(""); // 에러 메시지 + const [preview, setPreview] = useState(""); + const [errorMessage, setErrorMessage] = useState(""); const fileInput = useRef(null); const inputId = "imageUpload"; @@ -28,10 +28,10 @@ function ImageUpload({ title, onImageChange }: ImageUploadProps) { const handleImageUpload = (e: ChangeEvent) => { const file = e.target.files?.[0]; if (file && file.type.startsWith("image/")) { - const prevUrl = URL.createObjectURL(file); // 미리보기 URL 생성 + const prevUrl = URL.createObjectURL(file); setPreview(prevUrl); setErrorMessage(""); - onImageChange(file); // 이미지 파일 전달 + onImageChange(file); } else { alert("이미지 파일만 업로드 가능합니다."); } @@ -40,7 +40,7 @@ function ImageUpload({ title, onImageChange }: ImageUploadProps) { const handleImageDelete = () => { setPreview(""); setErrorMessage(""); - onImageChange(null); // 이미지 삭제 시 null 전달 + onImageChange(null); if (fileInput.current) { fileInput.current.value = ""; } diff --git a/app/components/ui/SearchInput.tsx b/app/components/ui/SearchInput.tsx index b30b911af..ace18a9e4 100644 --- a/app/components/ui/SearchInput.tsx +++ b/app/components/ui/SearchInput.tsx @@ -6,7 +6,7 @@ import Image from "next/image"; interface SearchInputProps { placeholder?: string; - onSearch: (value: string) => void; // 검색어를 상위로 전달 + onSearch: (value: string) => void; } const SearchInput: React.FC = ({ From 24a8a2b8c4cb1c6ef7419b5f5d4c5516fb6baaf5 Mon Sep 17 00:00:00 2001 From: 1022gusl <1022gusl@gmail.com> Date: Sat, 7 Dec 2024 09:19:00 +0900 Subject: [PATCH 13/13] =?UTF-8?q?feat:=20api=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/addboard/page.tsx | 4 +- app/lib/api/api.ts | 92 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 84 insertions(+), 12 deletions(-) diff --git a/app/addboard/page.tsx b/app/addboard/page.tsx index f59d59049..7f349a8b0 100644 --- a/app/addboard/page.tsx +++ b/app/addboard/page.tsx @@ -15,9 +15,7 @@ export default function AddBoard() { const handleSubmit = async () => { try { - const imageUrl = image ? URL.createObjectURL(image) : undefined; - await addArticle(title, content, imageUrl); - + await addArticle(title, content, image); alert("게시물이 성공적으로 등록되었습니다!"); setTitle(""); setContent(""); diff --git a/app/lib/api/api.ts b/app/lib/api/api.ts index 514e9747a..d34aa1355 100644 --- a/app/lib/api/api.ts +++ b/app/lib/api/api.ts @@ -203,27 +203,101 @@ export const signIn = async (email: string, password: string) => { return response.json(); }; -export const addArticle = async ( - title: string, - content: string, - image?: string -): Promise => { +export const refreshAccessToken = async (): Promise => { + const refreshToken = localStorage.getItem("refreshToken"); + if (!refreshToken) { + throw new Error("리프레시 토큰이 없습니다. 다시 로그인해주세요."); + } + + const response = await fetch(`${BASE_URL}auth/refresh-token`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ refreshToken }), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || "토큰 갱신 실패"); + } + + const data = await response.json(); + localStorage.setItem("accessToken", data.accessToken); + return data.accessToken; +}; + +export const uploadImage = async (image: File): Promise => { const token = localStorage.getItem("accessToken"); if (!token) { throw new Error("로그인이 필요합니다."); } - const response = await fetch(`${BASE_URL}articles`, { + const formData = new FormData(); + formData.append("file", image); + + const response = await fetch(`${BASE_URL}images/upload`, { method: "POST", headers: { - "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, - body: JSON.stringify({ title, content, image }), + body: formData, }); if (!response.ok) { const errorData = await response.json(); - throw new Error(errorData.message || "게시물 등록 실패"); + throw new Error(errorData.message || "이미지 업로드 실패"); + } + + const data = await response.json(); + return data.url; +}; + +export const addArticle = async ( + title: string, + content: string, + image?: File | null +): Promise => { + let token = localStorage.getItem("accessToken"); + if (!token) { + throw new Error("로그인이 필요합니다."); + } + + try { + const body = { title, content }; + + const response = await fetch(`${BASE_URL}articles`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + if (response.status === 401) { + const newToken = await refreshAccessToken(); + token = newToken; + + const retryResponse = await fetch(`${BASE_URL}articles`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(body), + }); + + if (!retryResponse.ok) { + throw new Error("게시물 등록 실패"); + } + } else { + const errorData = await response.json(); + throw new Error(errorData.message || "게시물 등록 실패"); + } + } + } catch (error) { + throw new Error(error instanceof Error ? error.message : "오류 발생"); } };