- {width >= 768 ? (
+ {width >= BREAKPOINTS.TABLET ? (

) : (

@@ -54,10 +56,10 @@ function Header() {

)}
{["/"].includes(location.pathname) && (
-
-
+
)}
diff --git a/React/panda-market/src/components/UI/DeleteButton.jsx b/React/panda-market/src/components/UI/DeleteButton.jsx
new file mode 100644
index 00000000..9ddeef25
--- /dev/null
+++ b/React/panda-market/src/components/UI/DeleteButton.jsx
@@ -0,0 +1,12 @@
+import DeleteIcon from "../../assets/icon/ic_delete.svg";
+import styles from "./DeleteButton.module.css";
+
+function DeleteButton({ className = "", onClick = () => {} }) {
+ return (
+
+ );
+}
+
+export default DeleteButton;
diff --git a/React/panda-market/src/components/UI/DeleteButton.module.css b/React/panda-market/src/components/UI/DeleteButton.module.css
new file mode 100644
index 00000000..cdb1ba40
--- /dev/null
+++ b/React/panda-market/src/components/UI/DeleteButton.module.css
@@ -0,0 +1,5 @@
+.deleteIcon {
+ padding: 0.5rem;
+ border-radius: 100%;
+ background-color: var(--gray400);
+}
diff --git a/React/panda-market/src/components/UI/MenuButton.jsx b/React/panda-market/src/components/UI/MenuButton.jsx
new file mode 100644
index 00000000..42a1cdad
--- /dev/null
+++ b/React/panda-market/src/components/UI/MenuButton.jsx
@@ -0,0 +1,16 @@
+import MenuIcon from "../../assets/icon/ic_menu.svg";
+import styles from "./MenuButton.module.css";
+
+function MenuButton({ className = "", onClick = () => {} }) {
+ return (
+
+ );
+}
+
+export default MenuButton;
diff --git a/React/panda-market/src/components/UI/MenuButton.module.css b/React/panda-market/src/components/UI/MenuButton.module.css
new file mode 100644
index 00000000..bc65a3ff
--- /dev/null
+++ b/React/panda-market/src/components/UI/MenuButton.module.css
@@ -0,0 +1,7 @@
+.menuButton {
+ border-radius: 100%;
+}
+
+.menuButton:hover {
+ background-color: var(--gray100);
+}
diff --git a/React/panda-market/src/components/UI/PrimaryButton.jsx b/React/panda-market/src/components/UI/PrimaryButton.jsx
new file mode 100644
index 00000000..bb825808
--- /dev/null
+++ b/React/panda-market/src/components/UI/PrimaryButton.jsx
@@ -0,0 +1,18 @@
+import React from "react";
+import styles from "./PrimaryButton.module.css";
+
+function PrimaryButton({
+ children,
+ className = "",
+ onClick = () => {},
+ disabled = false,
+}) {
+ const buttonClass = `${styles.button} ${className}`;
+ return (
+
+ );
+}
+
+export default PrimaryButton;
diff --git a/React/panda-market/src/components/UI/PrimaryButton.module.css b/React/panda-market/src/components/UI/PrimaryButton.module.css
new file mode 100644
index 00000000..6a5b8c9c
--- /dev/null
+++ b/React/panda-market/src/components/UI/PrimaryButton.module.css
@@ -0,0 +1,16 @@
+.button {
+ background-color: var(--blue);
+ color: var(--gray100);
+}
+
+.button:hover {
+ background-color: #1967d6;
+}
+
+.button:focus {
+ background-color: #1251aa;
+}
+
+.button:disabled {
+ background-color: var(--gray400);
+}
diff --git a/React/panda-market/src/hooks/useWindowSize.js b/React/panda-market/src/hooks/useWindowSize.js
index e6d59042..57b137f2 100644
--- a/React/panda-market/src/hooks/useWindowSize.js
+++ b/React/panda-market/src/hooks/useWindowSize.js
@@ -1,7 +1,7 @@
import { useState, useEffect } from "react";
import debounce from "lodash.debounce";
-function useWindowSize() {
+function useWindowSize(delay = 300) {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
@@ -13,7 +13,7 @@ function useWindowSize() {
width: window.innerWidth,
height: window.innerHeight,
});
- }, 300);
+ }, delay);
window.addEventListener("resize", handleResize);
return () => {
diff --git a/React/panda-market/src/pages/HomePage/HomePage.jsx b/React/panda-market/src/pages/HomePage/HomePage.jsx
index 4db9aad5..0432b2d2 100644
--- a/React/panda-market/src/pages/HomePage/HomePage.jsx
+++ b/React/panda-market/src/pages/HomePage/HomePage.jsx
@@ -1,4 +1,11 @@
+import { Link } from "react-router-dom";
import { Helmet } from "react-helmet";
+import PrimaryButton from "../../components/UI/PrimaryButton";
+import BannerTopImg from "../../assets/image/Img_home_top.svg";
+import BannerBottomImg from "../../assets/image/Img_home_bottom.svg";
+import HomeImg1 from "../../assets/image/Img_home_01.png";
+import HomeImg2 from "../../assets/image/Img_home_02.png";
+import HomeImg3 from "../../assets/image/Img_home_03.png";
import styles from "./HomePage.module.css";
function HomePage() {
@@ -7,6 +14,80 @@ function HomePage() {
판다마켓 - 일상의 모든 물건을 거래해 보세요
+
+
+
+
+
+ 일상의 모든 물건을
+
거래해 보세요
+
+
+
+ 구경하러 가기
+
+
+
+
+

+
+
+
+
+
+
+

+
+
Hot item
+
인기 상품을 확인해 보세요
+
+ 가장 HOT한 중고거래 물품을
+
+ 판다 마켓에서 확인해 보세요
+
+
+
+
+
+
Search
+
구매를 원하는 상품을 검색하세요
+
+ 구매하고 싶은 물품은 검색해서
+
+ 쉽게 찾아보세요
+
+
+

+
+
+

+
+
Register
+
판매를 원하는 상품을 등록하세요
+
+ 어떤 물건이든 판매하고 싶은 상품을
+
+ 쉽게 등록하세요
+
+
+
+
+
+
+
+
+
+ 믿을 수 있는
+
+ 판다마켓 중고 거래
+
+
+
+

+
+
+
+
>
);
}
diff --git a/React/panda-market/src/pages/HomePage/HomePage.module.css b/React/panda-market/src/pages/HomePage/HomePage.module.css
index e69de29b..3ba9586d 100644
--- a/React/panda-market/src/pages/HomePage/HomePage.module.css
+++ b/React/panda-market/src/pages/HomePage/HomePage.module.css
@@ -0,0 +1,276 @@
+.wrapper {
+ max-width: 120rem;
+ margin: 0 auto;
+ width: 100%;
+}
+
+.title {
+ font-weight: 700;
+ font-size: 4rem;
+ line-height: 5.6rem;
+ letter-spacing: 0.02em;
+ margin: 0;
+}
+
+.bold {
+ font-weight: 700;
+ font-size: 4rem;
+ line-height: 5.6rem;
+ letter-spacing: 0.02em;
+ margin: 0;
+}
+
+.loginLinkButton {
+ font-size: 1.6rem;
+ font-weight: 600;
+ border-radius: 0.8rem;
+ padding: 1.2rem 2.3rem;
+}
+
+.itemsLinkButton {
+ font-size: 2rem;
+ font-weight: 600;
+ border-radius: 4rem;
+ padding: 1.6rem 11.4rem;
+}
+
+.banner {
+ display: flex;
+ justify-content: center;
+ align-items: flex-end;
+ height: 54rem;
+ background-color: #cfe5ff;
+}
+
+.bannerContainer {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 1%;
+}
+
+.bannerLeft {
+ display: flex;
+ flex-direction: column;
+ gap: 3.2rem;
+ width: 35.7rem;
+ height: 26rem;
+ padding-bottom: 3.2rem;
+}
+
+.bannerRight {
+ display: flex;
+ justify-content: center;
+}
+
+.features {
+ display: flex;
+ flex-direction: column;
+ margin-bottom: 13.8rem;
+ gap: 0%;
+}
+
+.feature {
+ margin: 13.8rem 0;
+ padding-right: 5%;
+ display: flex;
+ align-items: center;
+ gap: 5%;
+ border-radius: 1.2rem;
+ background-color: #fcfcfc;
+}
+
+.feature:nth-child(2) {
+ padding-right: 0;
+ padding-left: 5%;
+ text-align: right;
+}
+
+/* 부모 요소인 feature의 남은 공간을 모두 차지하도록 설정 */
+.featureContent {
+ flex: 1;
+}
+
+.featureTag {
+ font-weight: 700;
+ font-size: 1.8rem;
+ line-height: 2.6rem;
+ color: var(--blue);
+}
+
+.featureDescription {
+ font-weight: 500;
+ font-size: 2.4rem;
+ line-height: 3.2rem;
+}
+
+.mobile {
+ display: none;
+}
+
+/* Tablet */
+@media (max-width: 1199px) {
+ .bold {
+ font-size: 3.2rem;
+ line-height: 4.2rem;
+ }
+
+ .banner.top {
+ height: 77.1rem;
+ }
+
+ .banner.bottom {
+ height: 92.7rem;
+ }
+
+ .bannerContainer {
+ display: flex;
+ flex-direction: column;
+ }
+
+ .bannerLeft {
+ display: flex;
+ flex-direction: column;
+ gap: 3.2rem;
+ width: auto;
+ text-align: center;
+ padding-bottom: 35rem;
+ }
+
+ .features {
+ margin-bottom: 8rem;
+ }
+
+ .feature {
+ margin: 3rem;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 2rem;
+ border-radius: 0;
+ }
+
+ .feature > img {
+ width: 100%;
+ }
+
+ .pc {
+ display: none;
+ }
+
+ .tablet {
+ display: flex;
+ }
+
+ .columnReverse {
+ flex-direction: column-reverse !important;
+ }
+
+ .feature:nth-child(2) {
+ padding: 0;
+ align-items: flex-end;
+ }
+
+ .featureDescription {
+ font-size: 1.8rem;
+ line-height: 2.6rem;
+ }
+}
+
+/* Mobile */
+@media (max-width: 767px) {
+ .title {
+ font-size: 3.2rem;
+ line-height: 4.5rem;
+ }
+
+ .bold {
+ font-size: 2.4rem;
+ line-height: 3.2rem;
+ }
+
+ .itemsLinkButton {
+ padding: 1.2rem 6.1rem;
+ font-size: 1.8rem;
+ }
+
+ .banner.top {
+ height: 54rem;
+ position: relative;
+ }
+
+ .banner.bottom {
+ height: 54rem;
+ position: relative;
+ }
+
+ .bannerContainer {
+ display: flex;
+ flex-direction: column;
+ }
+
+ .bannerLeft {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 3.2rem;
+ width: auto;
+ text-align: center;
+ position: absolute;
+ top: 4rem;
+ }
+
+ .top .bannerLeft {
+ padding: 0;
+ }
+
+ .bannerRight > img {
+ width: 100%;
+ }
+
+ .features {
+ margin-bottom: 8rem;
+ }
+
+ .feature {
+ margin: 4rem 1.5rem 0 1.5rem;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 1rem;
+ border-radius: 0;
+ }
+
+ .feature > img {
+ width: 100%;
+ }
+
+ .pc {
+ display: none;
+ }
+
+ .mobile {
+ display: flex;
+ }
+
+ .columnReverse {
+ flex-direction: column-reverse !important;
+ }
+
+ .feature:nth-child(2) {
+ padding: 0;
+ align-items: flex-end;
+ }
+
+ .featureTag {
+ font-size: 1.6rem;
+ }
+
+ .featureDescription {
+ font-size: 1.6rem;
+ line-height: 2.6rem;
+ letter-spacing: 0.08em;
+ }
+}
diff --git a/React/panda-market/src/pages/ItemPage/ItemPage.jsx b/React/panda-market/src/pages/ItemPage/ItemPage.jsx
index 81d18440..8597d7ce 100644
--- a/React/panda-market/src/pages/ItemPage/ItemPage.jsx
+++ b/React/panda-market/src/pages/ItemPage/ItemPage.jsx
@@ -1,7 +1,29 @@
+import { Link, useParams } from "react-router-dom";
+import ItemInfoSection from "./components/ItemInfoSection";
+import ItemCommentSection from "./components/ItemCommentSection";
+import PrimaryButton from "../../components/UI/PrimaryButton";
+import BackIcon from "../../assets/icon/ic_back.svg";
import styles from "./ItemPage.module.css";
function ItemPage() {
- return <>>;
+ const { productId } = useParams(null);
+
+ return (
+
+
+
+
+
+
+
+
+
+ 목록으로 돌아가기
+
+
+
+
+ );
}
export default ItemPage;
diff --git a/React/panda-market/src/pages/ItemPage/ItemPage.module.css b/React/panda-market/src/pages/ItemPage/ItemPage.module.css
index e69de29b..5e8f38e1 100644
--- a/React/panda-market/src/pages/ItemPage/ItemPage.module.css
+++ b/React/panda-market/src/pages/ItemPage/ItemPage.module.css
@@ -0,0 +1,57 @@
+.container {
+ max-width: 120rem;
+ margin: 2.4rem auto;
+ width: 100%;
+}
+
+.itemInfoContainer {
+ padding-bottom: 4rem;
+ border-bottom: 0.1rem solid var(--gray200);
+}
+
+.commentContainer {
+ padding-top: 4rem;
+ padding-bottom: 4.8rem;
+}
+
+.buttonContainer {
+ display: flex;
+ justify-content: center;
+}
+
+.backButton {
+ padding: 1.2rem 4.2rem;
+ border-radius: 4rem;
+ display: flex;
+ gap: 0.8rem;
+ font-weight: 600;
+ font-size: 1.8rem;
+ line-height: 2.6rem;
+}
+
+/* Tablet */
+@media (max-width: 1199px) {
+ .container {
+ padding: 0 2.4rem;
+ }
+
+ .itemInfoContainer {
+ padding-bottom: 3.2rem;
+ }
+}
+
+/* Mobile */
+@media (max-width: 767px) {
+ .container {
+ padding: 0 1.6rem;
+ margin: 1.6rem auto;
+ }
+
+ .itemInfoContainer {
+ padding-bottom: 2.4rem;
+ }
+
+ .commentContainer {
+ padding-top: 2.4rem;
+ }
+}
diff --git a/React/panda-market/src/pages/ItemPage/components/ItemCommentCard.jsx b/React/panda-market/src/pages/ItemPage/components/ItemCommentCard.jsx
new file mode 100644
index 00000000..e3c97c60
--- /dev/null
+++ b/React/panda-market/src/pages/ItemPage/components/ItemCommentCard.jsx
@@ -0,0 +1,121 @@
+import { useState, useEffect } from "react";
+import { getFormattedDate, getPassedTime } from "../../../utils/dateTimeUtils";
+import MenuButton from "../../../components/UI/MenuButton";
+import PrimaryButton from "../../../components/UI/PrimaryButton";
+import UserDefaultImg from "../../../assets/image/default-profile.png";
+import styles from "./ItemCommentCard.module.css";
+
+function ItemCommentCard({
+ comment: {
+ content,
+ updatedAt,
+ writer: { nickname, image },
+ },
+}) {
+ const [isMenuOpen, setIsMenuOpen] = useState(false);
+ const [isEditMode, setIsEditMode] = useState(false);
+ const [modifiedComment, setModifiedComment] = useState(content);
+ const [editAvailable, setEditAvailable] = useState(false);
+
+ const passedTime = getPassedTime(updatedAt);
+ const updatedDate = getFormattedDate(updatedAt);
+
+ const handleEditMode = () => {
+ setIsEditMode(true);
+ setIsMenuOpen(false);
+ };
+
+ const handleChange = (event) => {
+ setModifiedComment(event.target.value);
+ };
+
+ const handleEditCancel = () => {
+ setIsEditMode(false);
+ setModifiedComment(content);
+ };
+
+ const handleEditComplete = (event) => {
+ event.stopPropagation();
+ setIsEditMode(false);
+ // 나중에 수정한 코멘트 patch 함수 동작하도록 추가
+ setModifiedComment(content);
+ };
+
+ useEffect(() => {
+ if (modifiedComment !== content && modifiedComment) {
+ setEditAvailable(true);
+ } else {
+ setEditAvailable(false);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [modifiedComment]);
+
+ return (
+
+ {!isEditMode ? (
+
setIsMenuOpen((prev) => !prev)}
+ />
+ ) : (
+ ""
+ )}
+ {isMenuOpen && (
+
+
+
+
+ )}
+ {isEditMode ? (
+
+ ) : (
+ {content}
+ )}
+
+
+ {image === null ? (
+

+ ) : (
+

+ )}
+
+
{nickname}
+
+ {passedTime > 24 ? updatedDate : `${passedTime}시간 전`}
+
+
+
+ {isEditMode ? (
+
+ ) : (
+ ""
+ )}
+
+
+ );
+}
+
+export default ItemCommentCard;
diff --git a/React/panda-market/src/pages/ItemPage/components/ItemCommentCard.module.css b/React/panda-market/src/pages/ItemPage/components/ItemCommentCard.module.css
new file mode 100644
index 00000000..cab63ae2
--- /dev/null
+++ b/React/panda-market/src/pages/ItemPage/components/ItemCommentCard.module.css
@@ -0,0 +1,135 @@
+.container {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ gap: 2.4rem;
+ padding-bottom: 1.2rem;
+ border-bottom: 1px solid var(--gray200);
+}
+
+.menuButton {
+ position: absolute;
+ top: 0rem;
+ right: 0;
+}
+
+.commentMenuList {
+ display: flex;
+ flex-direction: column;
+ position: absolute;
+ top: 3rem;
+ right: 0;
+ width: 13.9rem;
+ background-color: #ffffff;
+ border-radius: 1.2rem;
+ border: 0.1rem solid var(--gray200);
+}
+
+.commentMenu {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 4.6rem;
+ font-weight: 400;
+ font-size: 1.6rem;
+ color: var(--gray500);
+}
+
+.commentMenu:hover:first-child,
+.commentMenu:focus:first-child {
+ border-top-left-radius: 1.2rem;
+ border-top-right-radius: 1.2rem;
+ background-color: var(--gray100);
+}
+
+.commentMenu:hover:nth-child(2),
+.commentMenu:focus:nth-child(2) {
+ border-bottom-left-radius: 1.2rem;
+ border-bottom-right-radius: 1.2rem;
+ background-color: var(--gray100);
+}
+
+.modifiedComment,
+.modifiedComment:focus {
+ height: 8rem;
+}
+
+.contentSection {
+ font-weight: 400;
+ font-size: 1.4rem;
+ line-height: 2.4rem;
+ color: var(--gray800);
+}
+
+.bottomSection {
+ display: flex;
+ justify-content: space-between;
+}
+
+.buttonContainer {
+ padding-bottom: 1.2rem;
+}
+
+.cancelButton,
+.editCompleteButton {
+ padding: 1.2rem 2.3rem;
+ border-radius: 0.8rem;
+ font-weight: 600;
+ font-size: 1.6rem;
+}
+
+.cancelButton {
+ color: #737373;
+}
+
+.cancelButton:hover,
+.cancelButton:focus {
+ color: var(--blue);
+}
+
+.infoSection {
+ display: flex;
+ gap: 0.8rem;
+}
+
+.userImg {
+ width: 3.2rem;
+ height: 3.2rem;
+ border-radius: 100%;
+}
+
+.userInfo {
+ display: flex;
+ flex-direction: column;
+ gap: 0.4rem;
+}
+
+.userNickname {
+ font-weight: 400;
+ font-size: 1.2rem;
+ line-height: 1.8rem;
+ color: var(--gray600);
+}
+
+.updatedAt {
+ font-weight: 400;
+ font-size: 1.2rem;
+ line-height: 1.8rem;
+ color: var(--gray400);
+}
+
+/* Tablet */
+@media (max-width: 1199px) {
+}
+
+/* Mobile */
+@media (max-width: 767px) {
+ .commentMenuList {
+ width: 10.2rem;
+ }
+
+ .commentMenu {
+ height: 4.5rem;
+ font-size: 1.4rem;
+ }
+}
diff --git a/React/panda-market/src/pages/ItemPage/components/ItemCommentSection.jsx b/React/panda-market/src/pages/ItemPage/components/ItemCommentSection.jsx
new file mode 100644
index 00000000..b7716e4f
--- /dev/null
+++ b/React/panda-market/src/pages/ItemPage/components/ItemCommentSection.jsx
@@ -0,0 +1,108 @@
+import { useEffect, useState, useRef } from "react";
+import { getItemComments } from "../../../apis/itemApi";
+import PrimaryButton from "../../../components/UI/PrimaryButton";
+import ItemCommentCard from "./ItemCommentCard";
+import EmptyCommentImg from "../../../assets/image/Img_inquiry_empty.svg";
+import styles from "./ItemCommentSection.module.css";
+
+function ItemCommentSection({ productId }) {
+ const [newComment, setNewComment] = useState("");
+ const [registerAvailable, setRegisterAvailable] = useState(false);
+ const [comments, setComments] = useState([]);
+ const [cursor, setCursor] = useState(0);
+ const observerRef = useRef(null);
+
+ const handleChange = (event) => {
+ setNewComment(event.target.value);
+ };
+
+ const handleLoad = async () => {
+ try {
+ const { list, nextCursor } = await getItemComments(productId, {
+ cursor,
+ });
+ setComments((prev) => [...prev, ...list]);
+ setCursor(nextCursor);
+ } catch (error) {
+ alert(error.message);
+ console.error("ERROR: ", error);
+ }
+ };
+
+ useEffect(() => {
+ handleLoad();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ useEffect(() => {
+ if (cursor === null) return;
+ const observer = new IntersectionObserver(
+ (entries) => {
+ if (entries[0].isIntersecting) handleLoad();
+ },
+ { threshold: 1 }
+ );
+ if (observerRef.current) observer.observe(observerRef.current);
+ return () => observer.disconnect();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [cursor]);
+
+ useEffect(() => {
+ if (newComment) {
+ setRegisterAvailable(true);
+ } else {
+ setRegisterAvailable(false);
+ }
+ }, [newComment]);
+
+ return (
+
+
+ {comments.length === 0 ? (
+
+
+

+
+
아직 문의가 없어요
+
+ ) : (
+
+ {comments.map((comment, index) => (
+
+
+
+ ))}
+
+ )}
+
+ );
+}
+
+export default ItemCommentSection;
diff --git a/React/panda-market/src/pages/ItemPage/components/ItemCommentSection.module.css b/React/panda-market/src/pages/ItemPage/components/ItemCommentSection.module.css
new file mode 100644
index 00000000..129ebc9a
--- /dev/null
+++ b/React/panda-market/src/pages/ItemPage/components/ItemCommentSection.module.css
@@ -0,0 +1,73 @@
+.container {
+ display: flex;
+ flex-direction: column;
+ gap: 2.4rem;
+}
+
+.newCommentSection {
+ display: flex;
+ flex-direction: column;
+ gap: 1.6rem;
+}
+
+.newComment,
+.newComment:focus {
+ height: 10.4rem;
+}
+
+.registerButtonContainer {
+ display: flex;
+ justify-content: flex-end;
+}
+
+.registerButton {
+ padding: 1.2rem 2.3rem;
+ border-radius: 0.8rem;
+ font-weight: 600;
+ font-size: 1.6rem;
+}
+
+.emptyComment {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.emptyCommentImg {
+ width: 19.6rem;
+}
+
+.emptyCommentText {
+ font-weight: 400;
+ font-size: 1.6rem;
+ color: var(--gray400);
+}
+
+.commentListSection {
+ display: flex;
+ flex-direction: column;
+ gap: 2.4rem;
+}
+
+/* Tablet */
+@media (max-width: 1199px) {
+ .container {
+ gap: 4rem;
+ }
+
+ .emptyCommentImg {
+ width: 14rem;
+ }
+}
+
+/* Mobile */
+@media (max-width: 767px) {
+ .newComment,
+ .newComment:focus {
+ height: 12.9rem;
+ }
+
+ .commentListSection {
+ gap: 1.6rem;
+ }
+}
diff --git a/React/panda-market/src/pages/ItemPage/components/ItemInfoSection.jsx b/React/panda-market/src/pages/ItemPage/components/ItemInfoSection.jsx
new file mode 100644
index 00000000..89e107e0
--- /dev/null
+++ b/React/panda-market/src/pages/ItemPage/components/ItemInfoSection.jsx
@@ -0,0 +1,95 @@
+import { useEffect, useState } from "react";
+import { getItemById } from "../../../apis/itemApi";
+import { getFormattedDate } from "../../../utils/dateTimeUtils";
+import MenuButton from "../../../components/UI/MenuButton";
+import SellerIcon from "../../../assets/image/default-profile.png";
+import HeartIcon from "../../../assets/icon/ic_heart.svg";
+import styles from "./ItemInfoSection.module.css";
+
+function ItemInfoSection({ productId }) {
+ const [item, setItem] = useState();
+
+ useEffect(() => {
+ const handleLoad = async () => {
+ try {
+ const data = await getItemById(productId);
+ setItem(data);
+ } catch (error) {
+ alert(error.message);
+ console.error("ERROR: ", error);
+ }
+ };
+ handleLoad();
+ }, [productId]);
+
+ if (!item) {
+ return;
+ }
+
+ const {
+ name,
+ description,
+ price,
+ tags,
+ images,
+ favoriteCount,
+ createdAt,
+ ownerNickname,
+ } = item;
+
+ const formattedRegisterDate = getFormattedDate(createdAt);
+
+ return (
+
+
+

+
+
+
+
+
{name}
+ {price.toLocaleString()}원
+
+
+
+
+
상품 소개
+
{description}
+
+
+
상품 태그
+
+ {tags.map((tag, index) => (
+
+ #{tag}
+
+ ))}
+
+
+
+
+
+
+
+

+
+
+
{ownerNickname}
+
{formattedRegisterDate}
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default ItemInfoSection;
diff --git a/React/panda-market/src/pages/ItemPage/components/ItemInfoSection.module.css b/React/panda-market/src/pages/ItemPage/components/ItemInfoSection.module.css
new file mode 100644
index 00000000..29fd14f0
--- /dev/null
+++ b/React/panda-market/src/pages/ItemPage/components/ItemInfoSection.module.css
@@ -0,0 +1,239 @@
+.container {
+ display: grid;
+ grid-template-columns: 2fr 3fr;
+ gap: 2.4rem;
+}
+
+.itemImgSection {
+ padding: 0.5rem 0;
+}
+
+.itemImg {
+ width: 100%;
+ border-radius: 1.6rem;
+ aspect-ratio: 1 / 1;
+ object-fit: cover;
+}
+
+.itemInfoSection {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ gap: 4rem;
+ max-width: 100%;
+ min-width: 0;
+}
+
+.itemInfoTopSection {
+ display: flex;
+ flex-direction: column;
+ gap: 2.4rem;
+}
+
+.infoTitle {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ gap: 1.6rem;
+ padding-bottom: 1.6rem;
+ border-bottom: 1px solid var(--gray200);
+}
+
+.menuButton {
+ position: absolute;
+ top: 0;
+ right: 0;
+}
+
+.itemName {
+ font-weight: 600;
+ font-size: 2.4rem;
+ color: var(--gray800);
+}
+
+.infoPrice {
+ font-weight: 600;
+ font-size: 4rem;
+ color: var(--gray800);
+}
+
+.subtitle {
+ font-weight: 600;
+ font-size: 1.6rem;
+ color: var(--gray600);
+}
+
+.infoDetailContainer {
+ display: flex;
+ flex-direction: column;
+ gap: 2.4rem;
+}
+
+.infoDetail {
+ display: flex;
+ flex-direction: column;
+ gap: 1.6rem;
+}
+
+.itemDescription {
+ min-height: 10.4rem;
+ max-height: 10.4rem;
+ overflow-y: auto;
+ font-weight: 400;
+ font-size: 1.6rem;
+ color: var(--gray600);
+}
+
+.itemTags {
+ display: flex;
+ gap: 0.8rem;
+ overflow-x: auto;
+ white-space: nowrap;
+}
+
+.itemDescription::-webkit-scrollbar,
+.itemTags::-webkit-scrollbar {
+ display: none;
+}
+
+.itemTag {
+ padding: 0.6rem 1.6rem;
+ border-radius: 2.6rem;
+ background-color: var(--gray100);
+ font-weight: 400;
+ font-size: 1.6rem;
+ color: var(--gray800);
+}
+
+.itemInfoBottomSection {
+ display: flex;
+ justify-content: space-between;
+}
+
+.AdditionalInfo {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 1.6rem;
+}
+
+.sellerIconContainer {
+ padding: 0.5rem 0;
+}
+
+.sellerIcon {
+ width: 4rem;
+ height: 4rem;
+}
+
+.sellerInfo {
+ display: flex;
+ flex-direction: column;
+ gap: 0.8rem;
+}
+
+.sellerName {
+ font-weight: 500;
+ font-size: 1.4rem;
+ color: var(--gray600);
+}
+
+.registerDate {
+ font-weight: 400;
+ font-size: 1.4rem;
+ color: var(--gray400);
+}
+
+.itemLikeInfo {
+ display: flex;
+ align-items: center;
+}
+
+.LikeButtonContainer {
+ display: flex;
+ justify-content: flex-end;
+ width: 10.3rem;
+ border-left: 0.1rem solid var(--gray200);
+}
+
+.likeButton {
+ display: flex;
+ gap: 0.4rem;
+ padding: 0.4rem 1.2rem;
+ border-radius: 3.5rem;
+ border: 0.1rem solid var(--gray200);
+}
+
+.likeButton:hover {
+ background-color: var(--gray100);
+}
+
+.likeIcon {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 2.4rem;
+}
+
+.likeCount {
+ font-weight: 500;
+ font-size: 1.6rem;
+ color: var(--gray500);
+}
+
+/* Tablet */
+@media (max-width: 1199px) {
+ .container {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 1.6rem;
+ }
+
+ .itemImgSection {
+ padding: 0;
+ }
+
+ .itemInfoSection {
+ justify-content: flex-start;
+ }
+
+ .infoTitle {
+ gap: 0.8rem;
+ }
+
+ .itemName {
+ font-size: 2rem;
+ }
+
+ .infoPrice {
+ font-size: 3.2rem;
+ }
+
+ .infoDetail {
+ gap: 0.8rem;
+ }
+
+ .subtitle {
+ font-size: 1.4rem;
+ }
+
+ .itemDescription {
+ max-height: 15.6rem;
+ }
+}
+
+/* Mobile */
+@media (max-width: 767px) {
+ .container {
+ display: flex;
+ flex-direction: column;
+ }
+
+ .itemName {
+ font-size: 1.6rem;
+ }
+
+ .infoPrice {
+ font-size: 2.4rem;
+ }
+}
diff --git a/React/panda-market/src/pages/LoginPage/LoginPage.jsx b/React/panda-market/src/pages/LoginPage/LoginPage.jsx
index 8073831a..b490a49a 100644
--- a/React/panda-market/src/pages/LoginPage/LoginPage.jsx
+++ b/React/panda-market/src/pages/LoginPage/LoginPage.jsx
@@ -1,7 +1,178 @@
+import { useEffect, useState } from "react";
+import { Link, useNavigate } from "react-router-dom";
+import { validEmail, validPassword } from "../../utils/validation";
+import PrimaryButton from "../../components/UI/PrimaryButton";
+import LogoImg from "../../assets/logo/panda-market-logo.svg";
+import VisibilityOff from "../../assets/icon/visibility_off.svg";
+import VisibilityOn from "../../assets/icon/visibility_on.svg";
+import GoogleIcon from "../../assets/icon/ic_google.svg";
+import KakaoIcon from "../../assets/icon/ic_kakao.svg";
import styles from "./LoginPage.module.css";
function LoginPage() {
- return <>>;
+ const [formData, setFormData] = useState({
+ email: "",
+ password: "",
+ });
+ const [isValid, setIsValid] = useState({
+ email: true,
+ password: true,
+ });
+ const [isFilled, setIsFilled] = useState({
+ email: true,
+ password: true,
+ });
+ const [isVisible, setIsVisible] = useState({
+ password: false,
+ });
+ const [isLoginAvailable, setIsLoginAvailable] = useState(false);
+ const navigate = useNavigate();
+
+ const handleChange = (event) => {
+ const { id, value } = event.target;
+ setFormData((prev) => ({
+ ...prev,
+ [id]: value,
+ }));
+
+ const updateValidationState = (id, value, validFunc) => {
+ setIsFilled((prev) => ({
+ ...prev,
+ [id]: value.length > 0,
+ }));
+ setIsValid((prev) => ({
+ ...prev,
+ [id]: value.length === 0 || validFunc(value),
+ }));
+ };
+
+ if (id === "email") {
+ updateValidationState("email", value, validEmail);
+ }
+
+ if (id === "password") {
+ updateValidationState("password", value, validPassword);
+ }
+ };
+
+ const handlePasswordVisible = (event) => {
+ const { name } = event.currentTarget;
+
+ setIsVisible((prev) => ({
+ ...prev,
+ [name]: !prev[name],
+ }));
+ };
+
+ const handleLogin = (e) => {
+ if (!isLoginAvailable) {
+ e.preventDefault();
+ return;
+ }
+ navigate("/items");
+ };
+
+ useEffect(() => {
+ setIsLoginAvailable(
+ validEmail(formData.email) && validPassword(formData.password)
+ );
+ }, [formData]);
+
+ return (
+
+
+

+
+
+
+ );
}
export default LoginPage;
diff --git a/React/panda-market/src/pages/LoginPage/LoginPage.module.css b/React/panda-market/src/pages/LoginPage/LoginPage.module.css
index e69de29b..4ad3e065 100644
--- a/React/panda-market/src/pages/LoginPage/LoginPage.module.css
+++ b/React/panda-market/src/pages/LoginPage/LoginPage.module.css
@@ -0,0 +1,122 @@
+.container {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ gap: 4rem;
+ min-height: 100vh;
+ max-width: 64rem;
+ margin: 0 auto;
+}
+
+.logoImg {
+ width: 39.6rem;
+}
+
+.formSection {
+ display: flex;
+ flex-direction: column;
+ gap: 2.4rem;
+ width: 100%;
+}
+
+.passwordInput {
+ position: relative;
+}
+
+.visibleIcon {
+ position: absolute;
+ top: 5.7rem;
+ right: 2rem;
+}
+
+.cautionInput {
+ border: 2px solid var(--red);
+}
+
+.cautionText {
+ color: var(--red);
+ font-weight: 600;
+ font-size: 1.4rem;
+ line-height: 2.4rem;
+ padding: 1rem 0 0 1.6rem;
+}
+
+.loginButton {
+ font-size: 2rem;
+ font-weight: 600;
+ width: 100%;
+ border-radius: 4rem;
+ padding: 1.6rem 12.4rem;
+}
+
+.socialLoginSection {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+ padding: 1.6rem 2.3rem;
+ border-radius: 0.8rem;
+ background-color: #e6f2ff;
+ color: var(--gray800);
+ font-weight: 500;
+}
+
+.socialLoginText {
+ font-weight: 500;
+ font-size: 1.6rem;
+ color: var(--gray800);
+}
+
+.socialLoginList {
+ display: flex;
+ align-items: center;
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ gap: 1.6rem;
+}
+
+.googleIcon {
+ background-color: #ffffff;
+ outline: 0.1rem solid var(--gray50);
+ padding: 0.8rem;
+ border-radius: 999rem;
+}
+
+.kakaoIcon {
+ background-color: #f5e14b;
+ outline: 0.1rem solid var(--gray50);
+ padding: 0.8rem;
+ border-radius: 999rem;
+}
+
+.signupSection {
+ display: flex;
+ justify-content: center;
+ gap: 0.4rem;
+ font-weight: 500;
+ font-size: 1.4rem;
+ color: var(--gray900);
+}
+
+.signupLink,
+.signupLink:visited {
+ color: var(--blue);
+ text-decoration: underline;
+}
+
+/* Tablet */
+@media (max-width: 1199px) {
+}
+
+/* Mobile */
+@media (max-width: 767px) {
+ .container {
+ padding: 1.6rem;
+ max-width: 40rem;
+ }
+ .logoImg {
+ width: 19.8rem;
+ }
+}
diff --git a/React/panda-market/src/pages/MarketPage/components/AllItemsSection.jsx b/React/panda-market/src/pages/MarketPage/components/AllItemsSection.jsx
index 7627444d..df8071c8 100644
--- a/React/panda-market/src/pages/MarketPage/components/AllItemsSection.jsx
+++ b/React/panda-market/src/pages/MarketPage/components/AllItemsSection.jsx
@@ -2,40 +2,51 @@ import { Link } from "react-router-dom";
import { useEffect, useState } from "react";
import { getItems } from "../../../apis/itemApi";
import useWindowSize from "../../../hooks/useWindowSize";
+import BREAKPOINTS from "../../../utils/breakpoints";
import ItemCard from "./ItemCard";
import SearchIcon from "../../../assets/icon/ic_search.svg";
import DownIcon from "../../../assets/icon/ic_arrow_down.svg";
import DropdownIcon from "../../../assets/icon/ic_sort.svg";
-import BackIcon from "../../../assets/icon/ic_back.svg";
+import BackIcon from "../../../assets/icon/ic_prev.svg";
import NextIcon from "../../../assets/icon/ic_next.svg";
import styles from "./AllItemsSection.module.css";
+import PrimaryButton from "../../../components/UI/PrimaryButton";
+
+const PAGE_ARRAY = [1, 2, 3, 4, 5];
+const PAGE_CHUNK_SIZE = 5;
function AllItems() {
const [order, setOrder] = useState("recent");
const [page, setPage] = useState(1);
- const [pageSize, setPageSize] = useState(10);
+ const [itemsPerPage, setItemsPerPage] = useState(10);
const [isOpen, setIsOpen] = useState(false);
const [totalItemCount, setTotalItemCount] = useState(0);
const [pageBound, setPageBound] = useState(0);
const [items, setItems] = useState([]);
- const MaxPageBound = Math.floor(totalItemCount / pageSize / 5);
- const pageArr = [1, 2, 3, 4, 5];
+
+ const MaxPageBound = Math.floor(
+ totalItemCount / itemsPerPage / PAGE_CHUNK_SIZE
+ );
const { width } = useWindowSize();
// 아이템 불러오기
- const handleLoad = async (query) => {
- try {
- const { list, totalCount } = await getItems(query);
- setItems(list);
- setTotalItemCount(totalCount);
- } catch (error) {
- return;
- }
- };
-
useEffect(() => {
- handleLoad({ page, pageSize, order });
- }, [page, pageSize, order]);
+ const handleLoad = async () => {
+ try {
+ const { list, totalCount } = await getItems({
+ page,
+ pageSize: itemsPerPage,
+ order,
+ });
+ setItems(list);
+ setTotalItemCount(totalCount);
+ } catch (error) {
+ alert(error.message);
+ console.error("ERROR: ", error);
+ }
+ };
+ handleLoad();
+ }, [page, itemsPerPage, order]);
// 아이템 정렬
const handleOrderChange = (event) => {
@@ -58,32 +69,32 @@ function AllItems() {
const plusPageBound = () => {
setPageBound(pageBound + 1);
- setPage(1 + 5 * (pageBound + 1));
+ setPage(1 + PAGE_CHUNK_SIZE * (pageBound + 1));
};
const minusPageBound = () => {
pageBound < 1 ? setPageBound(0) : setPageBound(pageBound - 1);
pageBound < 1
- ? setPage(1 + 5 * pageBound)
- : setPage(1 + 5 * (pageBound - 1));
+ ? setPage(1 + PAGE_CHUNK_SIZE * pageBound)
+ : setPage(1 + PAGE_CHUNK_SIZE * (pageBound - 1));
};
// 반응형
useEffect(() => {
let newPageSize;
- if (width > 1200) {
+ if (width > BREAKPOINTS.DESKTOP) {
newPageSize = 10; // PC
- } else if (width > 768) {
+ } else if (width > BREAKPOINTS.TABLET) {
newPageSize = 6; // Tablet
} else {
newPageSize = 4; // Mobile
}
- if (newPageSize !== pageSize) {
- setPageSize(newPageSize);
+ if (newPageSize !== itemsPerPage) {
+ setItemsPerPage(newPageSize);
}
- }, [width, pageSize]);
+ }, [width, itemsPerPage]);
return (
@@ -91,7 +102,7 @@ function AllItems() {
전체 상품
-
+
{/* 아이템 정렬 드롭다운*/}
diff --git a/React/panda-market/src/pages/MarketPage/components/AllItemsSection.module.css b/React/panda-market/src/pages/MarketPage/components/AllItemsSection.module.css
index e458c892..62e4014f 100644
--- a/React/panda-market/src/pages/MarketPage/components/AllItemsSection.module.css
+++ b/React/panda-market/src/pages/MarketPage/components/AllItemsSection.module.css
@@ -23,7 +23,8 @@ form {
position: relative;
}
-.searchBar {
+.searchBar,
+.searchBar:focus {
width: 32.5rem;
height: 4.2rem;
border: none;
diff --git a/React/panda-market/src/pages/MarketPage/components/BestItemsSection.jsx b/React/panda-market/src/pages/MarketPage/components/BestItemsSection.jsx
index 0f91f0b4..8e9dfb4d 100644
--- a/React/panda-market/src/pages/MarketPage/components/BestItemsSection.jsx
+++ b/React/panda-market/src/pages/MarketPage/components/BestItemsSection.jsx
@@ -1,6 +1,7 @@
import { useEffect, useState } from "react";
import { getItems } from "../../../apis/itemApi";
import useWindowSize from "../../../hooks/useWindowSize";
+import BREAKPOINTS from "../../../utils/breakpoints";
import ItemCard from "./ItemCard";
import styles from "./BestItemsSection.module.css";
@@ -10,22 +11,30 @@ function BestItems() {
const { width } = useWindowSize();
// 아이템 불러오기
- const handleLoad = async (query) => {
- const { list } = await getItems(query);
- setItems(list);
- };
-
useEffect(() => {
- handleLoad({ page: 1, pageSize, order: "favorite" });
+ const handleLoad = async () => {
+ try {
+ const { list } = await getItems({
+ page: 1,
+ pageSize,
+ order: "favorite",
+ });
+ setItems(list);
+ } catch (error) {
+ alert(error.message);
+ console.error("ERROR: ", error);
+ }
+ };
+ handleLoad();
}, [pageSize]);
// 반응형
useEffect(() => {
let newPageSize;
- if (width > 1200) {
+ if (width > BREAKPOINTS.DESKTOP) {
newPageSize = 4; // PC
- } else if (width > 768) {
+ } else if (width > BREAKPOINTS.TABLET) {
newPageSize = 2; // Tablet
} else {
newPageSize = 1; // Mobile
diff --git a/React/panda-market/src/pages/MarketPage/components/ItemCard.jsx b/React/panda-market/src/pages/MarketPage/components/ItemCard.jsx
index 99eb3a00..4d8f49dd 100644
--- a/React/panda-market/src/pages/MarketPage/components/ItemCard.jsx
+++ b/React/panda-market/src/pages/MarketPage/components/ItemCard.jsx
@@ -1,20 +1,34 @@
+import { useNavigate } from "react-router-dom";
import heartIcon from "../../../assets/icon/ic_heart.svg";
import styles from "./ItemCard.module.css";
function ItemCard({ item }) {
- const { images, name, price, favoriteCount } = item;
+ const { id, images, name, price, favoriteCount } = item;
+ const navigate = useNavigate();
+
+ const handleClick = () => {
+ navigate(`/items/${id}`);
+ };
return (
<>
-
+
-
+
- - {name}
+ -
+
+ {name}
+
+
- {price}원
-
-
+
{favoriteCount}
diff --git a/React/panda-market/src/pages/RegisterItemPage/RegisterItemPage.jsx b/React/panda-market/src/pages/RegisterItemPage/RegisterItemPage.jsx
index f6d07952..f51c0c6a 100644
--- a/React/panda-market/src/pages/RegisterItemPage/RegisterItemPage.jsx
+++ b/React/panda-market/src/pages/RegisterItemPage/RegisterItemPage.jsx
@@ -1,52 +1,24 @@
-import { useState, useEffect, useRef } from "react";
-import PrusIcon from "../../assets/icon/ic_plus.svg";
-import DeleteIcon from "../../assets/icon/ic_delete.svg";
+import { useState, useEffect } from "react";
+import PrimaryButton from "../../components/UI/PrimaryButton";
+import ImageUploader from "./components/ImageUploader";
+import DeleteButton from "../../components/UI/DeleteButton";
import styles from "./RegisterItemPage.module.css";
function RegisterItemPage() {
const [registerAvailable, setRegisterAvailable] = useState(false);
+ const [itemImg, setItemImg] = useState(null);
+ const [tagValues, setTagValues] = useState([]);
const [formData, setFormData] = useState({
name: "",
description: "",
price: "",
tag: "",
});
- const [tagValues, setTagValues] = useState([]);
- const [itemImg, setItemImg] = useState(null);
- const [fileError, setFileError] = useState("");
- const fileInputRef = useRef(null);
const handleRegister = async (event) => {
event.preventDefault();
};
- const handleFileButtonClick = () => {
- if (!fileInputRef.current) return;
- fileInputRef.current.click();
- };
-
- const handleFileChange = (event) => {
- const files = event.target.files;
-
- if (!files) {
- return;
- }
-
- if (files.length > 1) {
- setFileError("*이미지 등록은 최대 1개까지 가능합니다.");
- return;
- }
-
- setFileError("");
- setItemImg(URL.createObjectURL(files[0]));
- };
-
- const handleDeleteFile = () => {
- setItemImg(null);
- setFileError("");
- fileInputRef.current.value = "";
- };
-
const handleChange = (event) => {
const { id, value } = event.target;
setFormData((prev) => ({
@@ -71,67 +43,38 @@ function RegisterItemPage() {
useEffect(() => {
const { name, description, price } = formData;
- if (name && description && price && tagValues.length !== 0) {
+ if (
+ itemImg !== null &&
+ name &&
+ description &&
+ price &&
+ tagValues.length !== 0
+ ) {
setRegisterAvailable(true);
} else {
setRegisterAvailable(false);
}
- }, [formData, tagValues]);
+ }, [itemImg, tagValues, formData]);
return (
);
diff --git a/React/panda-market/src/pages/RegisterItemPage/RegisterItemPage.module.css b/React/panda-market/src/pages/RegisterItemPage/RegisterItemPage.module.css
index cbc37edf..e555d6ed 100644
--- a/React/panda-market/src/pages/RegisterItemPage/RegisterItemPage.module.css
+++ b/React/panda-market/src/pages/RegisterItemPage/RegisterItemPage.module.css
@@ -42,100 +42,11 @@ h1.title {
}
h2.title {
- margin: 0;
font-weight: 700;
font-size: 1.8rem;
color: var(--gray800);
}
-.imgSectionContainer {
- display: flex;
- gap: 2.4rem;
-}
-
-.imgAddButton,
-.imgAddButton:hover,
-.imgAddButton:focus,
-.imgAddButton:disabled {
- width: 28.2rem;
- height: 28.2rem;
- border-radius: 1.2rem;
- background-color: var(--gray100);
-}
-
-.imgAddButtonContent {
- display: flex;
- flex-direction: column;
- gap: 1.2rem;
-}
-
-.imgAddButtonText {
- font-weight: 400;
- font-size: 1.6rem;
- color: var(--gray400);
-}
-
-.imgPreview {
- position: relative;
- width: 28.2rem;
- height: 28.2rem;
-}
-
-.imgPreview > img {
- width: 100%;
- border-radius: 1.2rem;
- aspect-ratio: 1 / 1;
- object-fit: cover;
-}
-
-.imgPreview > button {
- all: unset;
- cursor: pointer;
- position: absolute;
- top: 1.2rem;
- right: 1.2rem;
-}
-
-.errorMessage {
- font-weight: 400;
- font-size: 1.6rem;
- color: var(--red);
-}
-
-input,
-input:focus {
- height: 5.6rem;
- padding: 1.6rem 2.4rem;
- border-radius: 1.2rem;
- border: none;
- background-color: var(--gray100);
- font-weight: 400;
- font-size: 1.6rem;
- line-height: 2.6rem;
- outline: none;
-}
-
-textarea,
-textarea:focus {
- height: 28.2rem;
- padding: 1.6rem 2.4rem;
- border-radius: 1.2rem;
- border: none;
- background-color: var(--gray100);
- font-weight: 400;
- font-size: 1.6rem;
- line-height: 2.6rem;
- resize: none;
- outline: none;
-}
-
-input::placeholder,
-textarea::placeholder {
- font-weight: 400;
- font-size: 1.6rem;
- color: var(--gray400);
-}
-
.tags {
display: flex;
gap: 1.2rem;
@@ -149,7 +60,6 @@ textarea::placeholder {
.tag {
display: inline-flex;
- align-items: center;
gap: 0.8rem;
padding: 0.6rem 1.2rem;
border-radius: 2.6rem;
@@ -159,15 +69,10 @@ textarea::placeholder {
color: var(--gray800);
}
-.deleteButton {
- padding: 0.5rem;
- border-radius: 100%;
- background-color: var(--gray400);
-}
-
-.tag > button {
- all: unset;
- cursor: pointer;
+.tagName {
+ display: flex;
+ justify-content: center;
+ align-items: center;
}
/* Tablet */
@@ -175,20 +80,6 @@ textarea::placeholder {
.container {
padding: 1.6rem 2.4rem;
}
-
- .imgSectionContainer {
- gap: 1rem;
- }
-
- .imgAddButton {
- width: 16.8rem;
- height: 16.8rem;
- }
-
- .imgPreview {
- width: 16.8rem;
- height: 16.8rem;
- }
}
/* Mobile */
@@ -196,18 +87,4 @@ textarea::placeholder {
.container {
padding: 2.4rem 1.5rem;
}
-
- .imgSectionContainer {
- gap: 1rem;
- }
-
- .imgAddButton {
- width: 16.8rem;
- height: 16.8rem;
- }
-
- .imgPreview {
- width: 16.8rem;
- height: 16.8rem;
- }
}
diff --git a/React/panda-market/src/pages/RegisterItemPage/components/ImageUploader.jsx b/React/panda-market/src/pages/RegisterItemPage/components/ImageUploader.jsx
new file mode 100644
index 00000000..25e34dc8
--- /dev/null
+++ b/React/panda-market/src/pages/RegisterItemPage/components/ImageUploader.jsx
@@ -0,0 +1,76 @@
+import { useState, useRef } from "react";
+import DeleteButton from "../../../components/UI/DeleteButton";
+import PrusIcon from "../../../assets/icon/ic_plus.svg";
+import styles from "./ImageUploader.module.css";
+
+// 나중에 등록 기능 구현 시 업로드한 이미지 파일 주소를 RegisterItemPage로 lifting 해줄 필요가 있어 보임
+function ImageUpload({ itemImg, setItemImg }) {
+ const [fileError, setFileError] = useState("");
+ const fileInputRef = useRef(null);
+
+ const handleFileButtonClick = () => {
+ if (!fileInputRef.current) return;
+ fileInputRef.current.click();
+ };
+
+ const handleFileChange = (event) => {
+ const files = event.target.files;
+
+ if (!files) {
+ return;
+ }
+
+ if (files.length > 1) {
+ setFileError("*이미지 등록은 최대 1개까지 가능합니다.");
+ return;
+ }
+
+ setFileError("");
+ setItemImg(URL.createObjectURL(files[0]));
+ };
+
+ const handleDeleteFile = () => {
+ setItemImg(null);
+ setFileError("");
+ fileInputRef.current.value = "";
+ };
+
+ return (
+
+
+
+
+
+
+

+
+
이미지 등록
+
+
+ {itemImg && (
+
+

+
handleDeleteFile()}
+ className={styles.deleteButton}
+ />
+
+ )}
+
+ {fileError &&
{fileError}
}
+
+ );
+}
+
+export default ImageUpload;
diff --git a/React/panda-market/src/pages/RegisterItemPage/components/ImageUploader.module.css b/React/panda-market/src/pages/RegisterItemPage/components/ImageUploader.module.css
new file mode 100644
index 00000000..9c8eff33
--- /dev/null
+++ b/React/panda-market/src/pages/RegisterItemPage/components/ImageUploader.module.css
@@ -0,0 +1,91 @@
+.container {
+ display: flex;
+ flex-direction: column;
+ gap: 1.6rem;
+}
+
+.imgSectionContainer {
+ display: flex;
+ gap: 2.4rem;
+}
+
+.imgAddButton,
+.imgAddButton:hover,
+.imgAddButton:focus,
+.imgAddButton:disabled {
+ width: 28.2rem;
+ height: 28.2rem;
+ border-radius: 1.2rem;
+ background-color: var(--gray100);
+}
+
+.imgAddButtonContent {
+ display: flex;
+ flex-direction: column;
+ gap: 1.2rem;
+}
+
+.imgAddButtonText {
+ font-weight: 400;
+ font-size: 1.6rem;
+ color: var(--gray400);
+}
+
+.imgPreview {
+ position: relative;
+ width: 28.2rem;
+ height: 28.2rem;
+}
+
+.imgPreview > img {
+ width: 100%;
+ border-radius: 1.2rem;
+ aspect-ratio: 1 / 1;
+ object-fit: cover;
+}
+
+.deleteButton {
+ position: absolute;
+ top: 1.2rem;
+ right: 1.2rem;
+}
+
+.errorMessage {
+ font-weight: 400;
+ font-size: 1.6rem;
+ color: var(--red);
+}
+
+/* Tablet */
+@media (max-width: 1199px) {
+ .imgSectionContainer {
+ gap: 1rem;
+ }
+
+ .imgAddButton {
+ width: 16.8rem;
+ height: 16.8rem;
+ }
+
+ .imgPreview {
+ width: 16.8rem;
+ height: 16.8rem;
+ }
+}
+
+/* Mobile */
+@media (max-width: 767px) {
+ .imgSectionContainer {
+ gap: 1rem;
+ }
+
+ .imgAddButton {
+ width: 16.8rem;
+ height: 16.8rem;
+ }
+
+ .imgPreview {
+ width: 16.8rem;
+ height: 16.8rem;
+ }
+}
diff --git a/React/panda-market/src/pages/SignupPage/SignupPage.jsx b/React/panda-market/src/pages/SignupPage/SignupPage.jsx
new file mode 100644
index 00000000..5a54fc58
--- /dev/null
+++ b/React/panda-market/src/pages/SignupPage/SignupPage.jsx
@@ -0,0 +1,248 @@
+import { useEffect, useState } from "react";
+import { Link, useNavigate } from "react-router-dom";
+import {
+ validEmail,
+ validPassword,
+ matchPassword,
+} from "../../utils/validation";
+import PrimaryButton from "../../components/UI/PrimaryButton";
+import LogoImg from "../../assets/logo/panda-market-logo.svg";
+import VisibilityOff from "../../assets/icon/visibility_off.svg";
+import VisibilityOn from "../../assets/icon/visibility_on.svg";
+import GoogleIcon from "../../assets/icon/ic_google.svg";
+import KakaoIcon from "../../assets/icon/ic_kakao.svg";
+import styles from "./SignupPage.module.css";
+
+function SignupPage() {
+ const [formData, setFormData] = useState({
+ email: "",
+ nickname: "",
+ password: "",
+ passwordCheck: "",
+ });
+ const [isValid, setIsValid] = useState({
+ email: true,
+ nickname: true,
+ password: true,
+ passwordCheck: true,
+ });
+ const [isFilled, setIsFilled] = useState({
+ email: true,
+ nickname: true,
+ password: true,
+ passwordCheck: true,
+ });
+ const [isVisible, setIsVisible] = useState({
+ password: false,
+ passwordCheck: false,
+ });
+ const [isLoginAvailable, setIsLoginAvailable] = useState(false);
+ const navigate = useNavigate();
+
+ const handleChange = (event) => {
+ const { id, value } = event.target;
+
+ setFormData((prev) => ({
+ ...prev,
+ [id]: value,
+ }));
+
+ const updateValidationState = (id, value, validFunc) => {
+ setIsFilled((prev) => ({
+ ...prev,
+ [id]: value.length > 0,
+ }));
+ setIsValid((prev) => ({
+ ...prev,
+ [id]: value.length === 0 || validFunc(value),
+ }));
+ };
+
+ if (id === "email") {
+ updateValidationState("email", value, validEmail);
+ }
+
+ if (id === "nickname") {
+ updateValidationState("nickname", value, () => true);
+ }
+
+ if (id === "password") {
+ updateValidationState("password", value, validPassword);
+ }
+
+ if (id === "passwordCheck") {
+ updateValidationState("passwordCheck", value, (value) =>
+ matchPassword(formData.password, value)
+ );
+ }
+ };
+
+ const handlePasswordVisible = (event) => {
+ const { name } = event.currentTarget;
+
+ setIsVisible((prev) => ({
+ ...prev,
+ [name]: !prev[name],
+ }));
+ };
+
+ const handleSignup = (e) => {
+ if (!isLoginAvailable) {
+ e.preventDefault();
+ return;
+ }
+ navigate("/login");
+ };
+
+ useEffect(() => {
+ setIsLoginAvailable(
+ validEmail(formData.email) &&
+ formData.nickname.length > 0 &&
+ validPassword(formData.password) &&
+ matchPassword(formData.password, formData.passwordCheck)
+ );
+ }, [formData]);
+
+ return (
+
+
+

+
+
+
+ );
+}
+
+export default SignupPage;
diff --git a/React/panda-market/src/pages/SignupPage/SignupPage.module.css b/React/panda-market/src/pages/SignupPage/SignupPage.module.css
new file mode 100644
index 00000000..ae9cea61
--- /dev/null
+++ b/React/panda-market/src/pages/SignupPage/SignupPage.module.css
@@ -0,0 +1,122 @@
+.container {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ gap: 4rem;
+ min-height: 100vh;
+ max-width: 64rem;
+ margin: 0 auto;
+}
+
+.logoImg {
+ width: 39.6rem;
+}
+
+.formSection {
+ display: flex;
+ flex-direction: column;
+ gap: 2.4rem;
+ width: 100%;
+}
+
+.passwordInput {
+ position: relative;
+}
+
+.visibleIcon {
+ position: absolute;
+ top: 5.7rem;
+ right: 2rem;
+}
+
+.cautionInput {
+ border: 2px solid var(--red);
+}
+
+.cautionText {
+ color: var(--red);
+ font-weight: 600;
+ font-size: 1.4rem;
+ line-height: 2.4rem;
+ padding: 1rem 0 0 1.6rem;
+}
+
+.loginButton {
+ font-size: 2rem;
+ font-weight: 600;
+ width: 100%;
+ border-radius: 4rem;
+ padding: 1.6rem 12.4rem;
+}
+
+.socialLoginSection {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+ padding: 1.6rem 2.3rem;
+ border-radius: 0.8rem;
+ background-color: #e6f2ff;
+ color: var(--gray800);
+ font-weight: 500;
+}
+
+.socialLoginText {
+ font-weight: 500;
+ font-size: 1.6rem;
+ color: var(--gray800);
+}
+
+.socialLoginList {
+ display: flex;
+ align-items: center;
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ gap: 1.6rem;
+}
+
+.googleIcon {
+ background-color: #ffffff;
+ outline: 0.1rem solid var(--gray50);
+ padding: 0.8rem;
+ border-radius: 999rem;
+}
+
+.kakaoIcon {
+ background-color: #f5e14b;
+ outline: 0.1rem solid var(--gray50);
+ padding: 0.8rem;
+ border-radius: 999rem;
+}
+
+.loginSection {
+ display: flex;
+ justify-content: center;
+ gap: 0.4rem;
+ font-weight: 500;
+ font-size: 1.4rem;
+ color: var(--gray900);
+}
+
+.loginLink,
+.loginLink:visited {
+ color: var(--blue);
+ text-decoration: underline;
+}
+
+/* Tablet */
+@media (max-width: 1199px) {
+}
+
+/* Mobile */
+@media (max-width: 767px) {
+ .container {
+ padding: 1.6rem;
+ max-width: 40rem;
+ }
+ .logoImg {
+ width: 19.8rem;
+ }
+}
diff --git a/React/panda-market/src/styles/App.font.css b/React/panda-market/src/styles/App.font.css
index f16ee090..44de1d45 100644
--- a/React/panda-market/src/styles/App.font.css
+++ b/React/panda-market/src/styles/App.font.css
@@ -14,3 +14,7 @@ input {
textarea {
font-family: Pretendard, sans-serif;
}
+
+button {
+ font-family: Pretendard, sans-serif;
+}
diff --git a/React/panda-market/src/styles/App.module.css b/React/panda-market/src/styles/App.module.css
index 892bf284..aef6f8a9 100644
--- a/React/panda-market/src/styles/App.module.css
+++ b/React/panda-market/src/styles/App.module.css
@@ -1,5 +1,3 @@
-@import-normalize;
-
* {
box-sizing: border-box;
word-break: keep-all;
@@ -16,6 +14,12 @@ body {
padding: 0;
}
+h1,
+h2,
+h3 {
+ margin: 0;
+}
+
a,
a:hover,
a:active,
@@ -25,28 +29,18 @@ a:visited {
}
button {
+ padding: 0;
background: none;
border: none;
outline: none;
box-shadow: none;
cursor: pointer;
- background-color: var(--blue);
- color: var(--gray100);
display: inline-flex;
align-items: center;
justify-content: center;
}
-button:hover {
- background-color: #1967d6;
-}
-
-button:focus {
- background-color: #1251aa;
-}
-
button:disabled {
- background-color: var(--gray400);
cursor: default;
pointer-events: none;
}
@@ -55,6 +49,56 @@ img {
vertical-align: bottom;
}
+label {
+ display: flex;
+ flex-direction: column;
+ font-weight: 700;
+ font-size: 1.8rem;
+ color: var(--gray800);
+}
+
+label > :first-child {
+ margin-top: 1.6rem;
+}
+
+input,
+input:focus {
+ height: 5.6rem;
+ padding: 1.6rem 2.4rem;
+ border-radius: 1.2rem;
+ border: none;
+ background-color: var(--gray100);
+ font-weight: 400;
+ font-size: 1.6rem;
+ line-height: 2.6rem;
+ outline-color: var(--blue);
+}
+
+textarea,
+textarea:focus {
+ height: 28.2rem;
+ padding: 1.6rem 2.4rem;
+ border-radius: 1.2rem;
+ border: none;
+ background-color: var(--gray100);
+ font-weight: 400;
+ font-size: 1.6rem;
+ line-height: 2.6rem;
+ resize: none;
+ outline-color: var(--blue);
+}
+
+input::placeholder,
+textarea::placeholder {
+ font-weight: 400;
+ font-size: 1.6rem;
+ color: var(--gray400);
+}
+
+textarea::-webkit-scrollbar {
+ display: none;
+}
+
:global(#root) {
display: flex;
flex-direction: column;
diff --git a/React/panda-market/src/utils/breakpoints.js b/React/panda-market/src/utils/breakpoints.js
new file mode 100644
index 00000000..2462d2f0
--- /dev/null
+++ b/React/panda-market/src/utils/breakpoints.js
@@ -0,0 +1,6 @@
+const BREAKPOINTS = {
+ DESKTOP: 1200,
+ TABLET: 768,
+};
+
+export default BREAKPOINTS;
diff --git a/React/panda-market/src/utils/dateTimeUtils.js b/React/panda-market/src/utils/dateTimeUtils.js
new file mode 100644
index 00000000..6e3ac322
--- /dev/null
+++ b/React/panda-market/src/utils/dateTimeUtils.js
@@ -0,0 +1,20 @@
+export const getFormattedDate = (dateString) => {
+ const date = new Date(dateString);
+
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, "0");
+ const day = String(date.getDate()).padStart(2, "0");
+
+ const formattedDate = `${year}. ${month}. ${day}`;
+
+ return formattedDate;
+};
+
+export const getPassedTime = (dateString) => {
+ const updatedTime = new Date(dateString);
+ const now = new Date();
+ const diffTime = now - updatedTime;
+ const passedTime = Math.floor(diffTime / (1000 * 60 * 60));
+
+ return passedTime;
+};
diff --git a/React/panda-market/src/utils/validation.js b/React/panda-market/src/utils/validation.js
new file mode 100644
index 00000000..f3df2cab
--- /dev/null
+++ b/React/panda-market/src/utils/validation.js
@@ -0,0 +1,12 @@
+export const validEmail = (email) => {
+ const email_regex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/i;
+ return email_regex.test(email);
+};
+
+export const validPassword = (password) => {
+ return !(password.length < 8);
+};
+
+export const matchPassword = (password, passwordCheck) => {
+ return password === passwordCheck;
+};