diff --git a/README.md b/README.md index a8511b670..699e91ea5 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,29 @@ -## 스프린트 6미션 +## 스프린트 7미션 - site 확인: https://pandamarket-momo.netlify.app/ -### 템플릿 코드 확인 - -기본 요구사항 +### 기본 요구사항 - [x] React를 사용합니다 - [x] 피그마 디자인에 맞게 페이지를 만들어 주세요. - [x] Github에 PR(Pull Request)을 만들어서 미션을 제출합니다. 상품 등록 -- [ ] 페이지 주소가 “/additem” 일때 상단네비게이션바의 '중고마켓' 버튼의 색상은 “3692FF”입니다. -- [x] 상품 이미지는 최대 한개 업로드가 가능합니다. -- [x] 이미지를 제외하고 input 에 모든 값을 입력하면 ‘등록' 버튼이 활성화 됩니다. -- [x] API를 통한 상품 등록은 추후 미션에서 적용합니다. -- [x] 각 input의 placeholder 값을 정확히 입력해주세요. -- [x] 상품 등록 페이지 주소는 “/additem” 입니다. - 체크리스트 [심화] - 상품 등록 -- [x] 추가된 태그 안의 X 버튼을 누르면 해당 태그는 삭제됩니다. -- [x] 이미지 안의 X 버튼을 누르면 이미지가 삭제됩니다. + +### 상품 상세 + +- [x] response 로 받은 아래의 데이터로 화면을 구현합니다. +- [x] 상품 상세 페이지 주소는 “/items/{productId}” 입니다. + => favoriteCount : 하트 개수 + => images : 상품 이미지 + => tags : 상품태그 + => name : 상품 이름 + => description : 상품 설명 +- [x] 목록으로 돌아가기 버튼을 클릭하면 중고마켓 페이지 주소인 “/items” 으로 이동합니다 + 상품 문의 댓글 +- [x] response 로 받은 아래의 데이터로 화면을 구현합니다 +- [x] 문의하기에 내용을 입력하면 등록 버튼의 색상은 “3692FF”로 변합니다. + => image : 작성자 이미지 + => nickname : 작성자 닉네임 + => content : 작성자가 남긴 문구 + => description : 상품 설명 + => updatedAt : 문의글 마지막 업데이트 시간 diff --git a/package.json b/package.json index 9407baf12..3a8b7b817 100644 --- a/package.json +++ b/package.json @@ -37,8 +37,6 @@ "@eslint/compat": "^1.1.0", "@eslint/js": "^9.6.0", "eslint": "^9.6.0", - "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-react": "^7.34.3", "globals": "^15.7.0", "prettier": "^3.3.2" } diff --git a/src/App.js b/src/App.js index 9c97a2c1b..e7d2e04af 100644 --- a/src/App.js +++ b/src/App.js @@ -1,20 +1,22 @@ import { BrowserRouter, Routes, Route } from 'react-router-dom'; -import AddItem from './pages/AddItem'; -import Items from './pages/Items'; -import Board from './pages/Board'; -import Login from './pages/Login'; -import Notfound from './pages/Notfound'; +import Home from './pages/Home/Home'; +import AddItem from './pages/Item/AddItem'; +import Items from './pages/Item/Items'; +import ItemsPage from './pages/Item/ItemsPage'; +import Board from './pages/Board/Board'; +import Login from './pages/Auth/Login'; +import Notfound from './pages/Error/Notfound'; import './assets/css/style.css'; function App() { return ( - {/* 일시적 확인용 메인으로 addItems 세팅 */} - } /> - }> + {/* 일시적 확인용 메인으로 Items 세팅 */} + } /> + } /> - {/* } /> */} + } /> } /> } /> diff --git a/src/assets/css/style.css b/src/assets/css/style.css index 3518e47f5..78446d582 100644 --- a/src/assets/css/style.css +++ b/src/assets/css/style.css @@ -544,6 +544,7 @@ footer { /* ==== button ==== */ .btn-sm { display: flex; + width: auto; min-width: 88px; height: 48px; font-size: 16px; @@ -581,6 +582,35 @@ footer { color: var(--white); } +.btn-profile { + width: 40px; + height: 40px; + border-radius: 50%; + overflow: hidden; + background: url("../img/profile.png") no-repeat; +} + +.btn-favorite { + display: inline-flex; + width: fit-content; + align-items: center; + gap: 8px; + border: 1px solid var(--gray200); + background: var(--white); + border-radius: 2em; + padding: 0.5em 1em; + font-size: 16px; + color: var(--gray500); + transition: 0.2s ease; +} +.btn-favorite:hover { + background: var(--gray100); + color: var(--gray600); +} +.btn-favorite.on .ic_heart { + background-image: url("../img/ic_heart_fill.svg"); +} + .product-wrap { margin: 25px auto 40px; width: 100%; @@ -1061,11 +1091,19 @@ i.icon-sm { } /* ==== loadingbar ==== */ +.bg-dark { + background: var(--gray100); + width: 100%; + height: 100vh; + display: flex; + align-items: center; +} + .load-wrapp { width: 100px; height: 100px; - margin: 60px auto; - padding: 20px 20px 20px; + margin: 0 auto; + padding: 80px 20px 140px; text-align: center; } .load-wrapp .load { @@ -1189,7 +1227,6 @@ i.icon-sm { height: 100%; object-fit: cover; } - .image-add-wrap .image-add-box .ic_remove { position: absolute; right: 10px; diff --git a/src/assets/css/style.css.map b/src/assets/css/style.css.map index 382481a2c..27d4248c3 100644 --- a/src/assets/css/style.css.map +++ b/src/assets/css/style.css.map @@ -1 +1 @@ -{"version":3,"sourceRoot":"","sources":["../scss/base/_fonts.scss","../scss/base/_reset.scss","../scss/base/_variables.scss","../scss/layout/_header.scss","../scss/layout/_main.scss","../scss/layout/_footer.scss","../scss/layout/_responsive.scss","../scss/components/_button.scss","../scss/components/_card.scss","../scss/components/_form.scss","../scss/components/_icon.scss","../scss/components/_dropdown.scss","../scss/components/_pagination.scss","../scss/components/_loadingbar.scss","../scss/components/_tag.scss","../scss/components/_input-file.scss"],"names":[],"mappings":";AAAA;AAEA;EACE;EACA;EACA;;AAIF;EACE;EACA;EACA;;AAIF;EACE;EACA;EACA;;AAIF;EACE;EACA;EACA;;AC1BF;AAEA;AACA;AAAA;AAAA;EAGE;EACA;EACA;;;AAGF;AACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EA0BE;EACA;;;AAGF;AACA;AAAA;AAAA;AAAA;AAAA;AAAA;EAME;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;AACA;AAAA;AAAA;EAGE;EACA;;;AAGF;AACA;AAAA;AAAA;AAAA;AAAA;AAAA;EAME;;;AAGF;EACE;;;AAGF;AAAA;EAEE;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAIF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;ACjHF;AAGA;EACE;EAEA;EAEA;EACA;EACA;EACA;EAEA;EACA;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;AAEA;EACA;EACA;EACA;EACA;AAEA;EACA;;;AClCF;AAEA;EACE;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAEA;EACE;;;AAMR;EACE;EACA;EACA;EACA;EACA;;;AChDF;AAEA;EACE;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAIF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AC5HF;AAEA;EACE;EACA;EACA;;;AAIF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AC3CF;AAGA;EACE;IACE;IACA;;EAEA;IACE;IACA;;EAGF;IACE;IACA;IACA;IACA;;EAGE;IACE;;EAIJ;IACE;IACA;IACA;;EAKN;IACE;;EAEA;IACE;IACA;IACA;IACA;;EAGE;IACE;;EAEA;IACE;;EAIJ;IACE;;EAIJ;IACE;IACA;;EAKN;IACE;;EAEA;IACE;IACA;;EAGF;IACE;IACA;IACA;;EAEA;IACE;;EAKN;IACE;IACA;IACA;;;AAKJ;EAGI;IACE;;EACA;IACE;IACA;;EAKN;IACE;IACA;IACA;;EAGF;IACE;IACA;;EAEA;IACE;IACA;;EAGF;IACE;IACA;IACA;IACA;;EAEA;IACE;IACA;;EAEA;IACE;;EAIJ;IACE;IACA;IACA;IACA;;EAKN;IACE;;EAEA;IACE;;EAGE;IACE;;EAGF;IACE;;EAGF;IACE;;EAMR;IACE;;EAEA;IACE;IACA;;EAGF;IACE;IACA;IACA;;EAEA;IACE;IACA;;EAKN;IACE;IACA;IACA;IACA,qBACE;IAEF;;EAEA;IACE;IACA;;EAGF;IACE;IACA;;EAGF;IACE;;;AAKN;EACE;IACE;IACA;IACA;;;ACpNJ;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;ACtCF;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EAbF;IAcI;;;AAIJ;EACE;EACA;;AAEA;EAJF;IAKI;;;AAIJ;EACE;EACA;;AAGF;EA9CF;IA+CI;IACA;IACA;IACA,qBACE;;;AAKN;EACE;EACA;;AAEA;EAJF;IAKI;;;AAGF;EARF;IASI;;;AAIJ;EACE;;AAEA;EAHF;IAII;;;AAGF;EAPF;IAQI;;;AAGJ;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAEA;EACE;EACA;;AAEA;EACE;EACA;;AAEF;EACE;EACA;EACA;EACA;EACA;;AACA;EACE;;AAIJ;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AC1IV;AACA;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EARF;IASI;IACA;;;;AAIJ;AAAA;EAEE;EACA;EACA;EACA;EACA;EACA;;AAEA;EATF;AAAA;IAUI;IACA;IACA;;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EARF;IASI;;;;AAIJ;EACE;EACA;;AAEA;AAAA;EAEE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;AAAA;EACE;;AAEF;AAAA;EACE;;AAGJ;EAEE;;AAGF;EACE;;AAGF;EAhCF;IAiCI;;;AAEF;EACE;EACA;EACA;EACA;EACA;;AAEA;EAPF;IAQI;IACA;;;AAIJ;EACE;EACA;EACA;;;AAIJ;EACE;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;;;ACrJF;AACA;EACE;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AC1EF;AACA;EACE;;AACA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EAXF;IAYI;IACA;;EAEA;IACE;;;AAKN;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AACA;EAbF;IAcI;IACA;IACA;;;AAGF;EACE;;AAIA;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAIJ;EACE;;;AC7DR;AACA;EACE;EACA;EACA;EACA;EACA;EACA;;AAGE;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAGF;EACE;;AAGF;EACE;EACA;;AAGF;EACE;EACA;EACA;;AAEF;EACE;EACA;;AAGF;EACE;;;AC/CR;AACA;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAEA;EACE;EACA;EACA;EACA;;AACA;EACE;;AAEF;EACE;;AAEF;EACE;;AAIN;EACE;EACA;;;AAGJ;EACE;IACE;;EAEF;IACE;;EAEF;IACE;;;ACzCJ;EACE;EACA;EACA;EACA;;AACA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AACA;EACE;;AAEF;EACE;;AACA;EACE;;;AAKR;EACE;EACA;;AACA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;ACnCJ;EACE;EACA;;AACA;EAHF;IAII;;;AAEF;EANF;IAOI;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AACA;EACE;;AAIJ;EACE;EACA;EACA;EACA;EACA;;AACA;EACE;EACA;EACA;;AAEF;EACE;EACA;EACA;EACA;EACA;;AACA;EACE","file":"style.css"} \ No newline at end of file +{"version":3,"sourceRoot":"","sources":["../scss/base/_fonts.scss","../scss/base/_reset.scss","../scss/base/_variables.scss","../scss/layout/_header.scss","../scss/layout/_main.scss","../scss/layout/_footer.scss","../scss/layout/_responsive.scss","../scss/components/_button.scss","../scss/components/_card.scss","../scss/components/_form.scss","../scss/components/_icon.scss","../scss/components/_dropdown.scss","../scss/components/_pagination.scss","../scss/components/_loadingbar.scss","../scss/components/_tag.scss","../scss/components/_input-file.scss"],"names":[],"mappings":";AAAA;AAEA;EACE;EACA;EACA;;AAIF;EACE;EACA;EACA;;AAIF;EACE;EACA;EACA;;AAIF;EACE;EACA;EACA;;AC1BF;AAEA;AACA;AAAA;AAAA;EAGE;EACA;EACA;;;AAGF;AACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EA0BE;EACA;;;AAGF;AACA;AAAA;AAAA;AAAA;AAAA;AAAA;EAME;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;AACA;AAAA;AAAA;EAGE;EACA;;;AAGF;AACA;AAAA;AAAA;AAAA;AAAA;AAAA;EAME;;;AAGF;EACE;;;AAGF;AAAA;EAEE;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAIF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;ACjHF;AAGA;EACE;EAEA;EAEA;EACA;EACA;EACA;EAEA;EACA;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;AAEA;EACA;EACA;EACA;EACA;AAEA;EACA;;;AClCF;AAEA;EACE;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAEA;EACE;;;AAMR;EACE;EACA;EACA;EACA;EACA;;;AChDF;AAEA;EACE;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAIF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AC5HF;AAEA;EACE;EACA;EACA;;;AAIF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AC3CF;AAGA;EACE;IACE;IACA;;EAEA;IACE;IACA;;EAGF;IACE;IACA;IACA;IACA;;EAGE;IACE;;EAIJ;IACE;IACA;IACA;;EAKN;IACE;;EAEA;IACE;IACA;IACA;IACA;;EAGE;IACE;;EAEA;IACE;;EAIJ;IACE;;EAIJ;IACE;IACA;;EAKN;IACE;;EAEA;IACE;IACA;;EAGF;IACE;IACA;IACA;;EAEA;IACE;;EAKN;IACE;IACA;IACA;;;AAKJ;EAGI;IACE;;EACA;IACE;IACA;;EAKN;IACE;IACA;IACA;;EAGF;IACE;IACA;;EAEA;IACE;IACA;;EAGF;IACE;IACA;IACA;IACA;;EAEA;IACE;IACA;;EAEA;IACE;;EAIJ;IACE;IACA;IACA;IACA;;EAKN;IACE;;EAEA;IACE;;EAGE;IACE;;EAGF;IACE;;EAGF;IACE;;EAMR;IACE;;EAEA;IACE;IACA;;EAGF;IACE;IACA;IACA;;EAEA;IACE;IACA;;EAKN;IACE;IACA;IACA;IACA,qBACE;IAEF;;EAEA;IACE;IACA;;EAGF;IACE;IACA;;EAGF;IACE;;;AAKN;EACE;IACE;IACA;IACA;;;ACpNJ;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AACA;EACE;EACA;;AAGA;EACE;;;ACpEN;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EAbF;IAcI;;;AAIJ;EACE;EACA;;AAEA;EAJF;IAKI;;;AAIJ;EACE;EACA;;AAGF;EA9CF;IA+CI;IACA;IACA;IACA,qBACE;;;AAKN;EACE;EACA;;AAEA;EAJF;IAKI;;;AAGF;EARF;IASI;;;AAIJ;EACE;;AAEA;EAHF;IAII;;;AAGF;EAPF;IAQI;;;AAGJ;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAEA;EACE;EACA;;AAEA;EACE;EACA;;AAEF;EACE;EACA;EACA;EACA;EACA;;AACA;EACE;;AAIJ;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AC1IV;AACA;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EARF;IASI;IACA;;;;AAIJ;AAAA;EAEE;EACA;EACA;EACA;EACA;EACA;;AAEA;EATF;AAAA;IAUI;IACA;IACA;;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EARF;IASI;;;;AAIJ;EACE;EACA;;AAEA;AAAA;EAEE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;AAAA;EACE;;AAEF;AAAA;EACE;;AAGJ;EAEE;;AAGF;EACE;;AAGF;EAhCF;IAiCI;;;AAEF;EACE;EACA;EACA;EACA;EACA;;AAEA;EAPF;IAQI;IACA;;;AAIJ;EACE;EACA;EACA;;;AAIJ;EACE;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;;;ACrJF;AACA;EACE;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AC1EF;AACA;EACE;;AACA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EAXF;IAYI;IACA;;EAEA;IACE;;;AAKN;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AACA;EAbF;IAcI;IACA;IACA;;;AAGF;EACE;;AAIA;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAIJ;EACE;;;AC7DR;AACA;EACE;EACA;EACA;EACA;EACA;EACA;;AAGE;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAGF;EACE;;AAGF;EACE;EACA;;AAGF;EACE;EACA;EACA;;AAEF;EACE;EACA;;AAGF;EACE;;;AC/CR;AACA;EACE;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAEA;EACE;EACA;EACA;EACA;;AACA;EACE;;AAEF;EACE;;AAEF;EACE;;AAIN;EACE;EACA;;;AAGJ;EACE;IACE;;EAEF;IACE;;EAEF;IACE;;;AChDJ;EACE;EACA;EACA;EACA;;AACA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AACA;EACE;;AAEF;EACE;;AACA;EACE;;;AAKR;EACE;EACA;;AACA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;ACnCJ;EACE;EACA;;AACA;EAHF;IAII;;;AAEF;EANF;IAOI;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AACA;EACE;;AAIJ;EACE;EACA;EACA;EACA;EACA;;AACA;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;AACA;EACE","file":"style.css"} \ No newline at end of file diff --git a/src/assets/img/ic_heart_fill.svg b/src/assets/img/ic_heart_fill.svg new file mode 100644 index 000000000..691369bf2 --- /dev/null +++ b/src/assets/img/ic_heart_fill.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/scss/components/_button.scss b/src/assets/scss/components/_button.scss index 4e23709f1..e77f24104 100644 --- a/src/assets/scss/components/_button.scss +++ b/src/assets/scss/components/_button.scss @@ -2,6 +2,7 @@ .btn-sm { display: flex; + width: auto; min-width: 88px; height: 48px; font-size: 16px; @@ -38,3 +39,34 @@ background: var(--gray400); color: var(--white); } + +.btn-profile { + width: 40px; + height: 40px; + border-radius: 50%; + overflow: hidden; + background: url('../img/profile.png') no-repeat; +} + +.btn-favorite { + display: inline-flex; + width: fit-content; + align-items: center; + gap: 8px; + border: 1px solid var(--gray200); + background: var(--white); + border-radius: 2em; + padding: 0.5em 1em; + font-size: 16px; + color: var(--gray500); + transition: 0.2s ease; + &:hover { + background: var(--gray100); + color: var(--gray600); + } + &.on { + .ic_heart { + background-image: url('../img/ic_heart_fill.svg'); + } + } +} diff --git a/src/assets/scss/components/_loadingbar.scss b/src/assets/scss/components/_loadingbar.scss index 38829b764..5abe69fcf 100644 --- a/src/assets/scss/components/_loadingbar.scss +++ b/src/assets/scss/components/_loadingbar.scss @@ -1,9 +1,16 @@ /* ==== loadingbar ==== */ +.bg-dark { + background: var(--gray100); + width: 100%; + height: 100vh; + display: flex; + align-items: center; +} .load-wrapp { width: 100px; height: 100px; - margin: 60px auto; - padding: 20px 20px 20px; + margin: 0 auto; + padding: 80px 20px 140px; text-align: center; .load { diff --git a/src/components/AllProductList.js b/src/components/AllProductList.js deleted file mode 100644 index bb9d21528..000000000 --- a/src/components/AllProductList.js +++ /dev/null @@ -1,105 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import ProductCard from './ProductCard'; -import Pagination from './Pagination'; -import Loadingbar from './Loadingbar'; -import Dropdown from './Dropdown'; -import { getProducts } from '../services/api'; - -const getPageSize = () => { - const width = window.innerWidth; - switch (true) { - case width < 768: // Mobile viewport - return 4; - case width < 1200: // Tablet viewport - return 6; - default: // Desktop viewport - return 10; - } -}; - -const SORT_MENU_INFO = { - RECENT: { - label: '최신순', - orderLabel: 'recent', - }, - FAVORITE: { - label: '좋아요순', - orderLabel: 'favorite', - }, -}; - -function AllProductList() { - const [orderBy, setOrderBy] = useState('recent'); - const [products, setProducts] = useState([]); - const [page, setPage] = useState(1); - const [pageSize, setPageSize] = useState(getPageSize()); - const [totalPageNum, setTotalPageNum] = useState(0); - const [loading, setLoading] = useState(false); - - const handleLoad = async ({ orderBy, page, pageSize }) => { - setLoading(true); - try { - const products = await getProducts({ orderBy, page, pageSize }); - setProducts(products.list); - setTotalPageNum(Math.ceil(products.totalCount / pageSize)); - } catch (error) { - console.error('Failed to load products:', error); - setProducts([]); - } finally { - setLoading(false); - } - }; - - const onPageChange = pageNumber => { - if (!pageNumber || pageNumber > totalPageNum) return; - setPage(pageNumber); - }; - - useEffect(() => { - const handleResize = () => { - setPageSize(getPageSize()); - }; - window.addEventListener('resize', handleResize); - - // Initial load - handleLoad({ orderBy, page, pageSize }); - - return () => { - window.removeEventListener('resize', handleResize); - }; - }, [orderBy, page, pageSize]); - - const ProductList = - products.length > 0 ? ( -
    - {products.map(product => ( -
  • - -
  • - ))} -
- ) : ( -

상품이 없습니다.

- ); - - return ( - <> -
-

판매 중인 상품

- - - 상품 등록하기 - - setOrderBy(option.orderLabel)} - /> -
- {loading ? : ProductList} - {totalPageNum > 1 && } - - ); -} - -export default AllProductList; diff --git a/src/components/BestProductList.js b/src/components/BestProductList.js deleted file mode 100644 index fdc58b6ee..000000000 --- a/src/components/BestProductList.js +++ /dev/null @@ -1,63 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import ProductCard from './ProductCard'; -import { getProducts } from '../services/api'; - -const getPageSize = () => { - const width = window.innerWidth; - switch (true) { - case width < 768: // Mobile viewport - return 1; - case width < 1200: // Tablet viewport - return 2; - default: // Desktop viewport - return 4; - } -}; - -function BestProductList() { - const orderBy = 'favorite'; - const [products, setProducts] = useState([]); - const [pageSize, setPageSize] = useState(getPageSize()); - - const handleLoad = async ({ orderBy, pageSize }) => { - try { - const products = await getProducts({ orderBy, pageSize }); - setProducts(products.list); - } catch (error) { - console.error('Failed to load products:', error); - setProducts([]); - } - }; - - useEffect(() => { - handleLoad({ orderBy, pageSize }); - const handleResize = () => { - setPageSize(getPageSize()); - }; - window.addEventListener('resize', handleResize); - return () => { - window.removeEventListener('resize', handleResize); - }; - }, [orderBy, pageSize]); - - return ( - <> -
-

베스트 상품

-
-
    - {products.length > 0 ? ( - products.map(product => ( -
  • - -
  • - )) - ) : ( -

    상품이 없습니다.

    - )} -
- - ); -} - -export default BestProductList; diff --git a/src/components/Nav.js b/src/components/Nav.js deleted file mode 100644 index 0ffee8e55..000000000 --- a/src/components/Nav.js +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import { NavLink } from 'react-router-dom'; - -function getLinkStyle({ isActive }) { - return isActive ? 'nav-link active' : 'nav-link'; -} - -function Nav() { - return ( -
    -
  • - - 자유게시판 - -
  • -
  • - - 중고마켓 - -
  • -
- ); -} - -export default Nav; diff --git a/src/components/TagInput.js b/src/components/TagInput.js deleted file mode 100644 index f72585ff8..000000000 --- a/src/components/TagInput.js +++ /dev/null @@ -1,64 +0,0 @@ - -import React, { useState, useEffect, useCallback } from 'react'; - -const TagInput = ({ onTagListChange, reset }) => { - const [tagInputValue, setTagInputValue] = useState(''); - const [tagList, setTagList] = useState([]); - - const handleKeyPress = useCallback( - e => { - if (e.key === 'Enter' && tagInputValue.trim()) { - e.preventDefault(); - - const newTagList = [...tagList, tagInputValue.trim()]; - setTagList(newTagList); - setTagInputValue(''); - onTagListChange(newTagList); - } - }, - [tagInputValue, tagList, onTagListChange], - ); - - const handleTagRemove = useCallback( - tagToRemove => { - const updatedList = tagList.filter(tag => tag !== tagToRemove); - setTagList(updatedList); - onTagListChange(updatedList); - }, - [tagList, onTagListChange], - ); - - - useEffect(() => { - if (reset) { - setTagList([]); - onTagListChange([]); - } - }, [reset, onTagListChange]); - - - return ( -
- setTagInputValue(e.target.value)} - onKeyUp={handleKeyPress} - /> - {tagList.length > 0 && ( -
- {tagList.map((tag, index) => ( - - {tag} handleTagRemove(tag)}> - - ))} -
- )} -
- ); -}; - -export default TagInput; diff --git a/src/components/FileInput.js b/src/components/form/FileInput.js similarity index 55% rename from src/components/FileInput.js rename to src/components/form/FileInput.js index 586290626..8474de276 100644 --- a/src/components/FileInput.js +++ b/src/components/form/FileInput.js @@ -1,7 +1,7 @@ import { useEffect, useRef, useState } from 'react'; -import inputImg from '../assets/img/product/sample2.png'; +import inputImg from '../../assets/img/product/sample2.png'; -function FileInput({ name, value, onChange }) { +function FileInput({ label, name, value, onChange }) { const [preview, setPreview] = useState(value || inputImg); const inputRef = useRef(); const [isBoxVisible, setIsBoxVisible] = useState(null); @@ -38,7 +38,6 @@ function FileInput({ name, value, onChange }) { setPreview(inputImg); setIsBoxVisible(false); } - }, [value]); const handleImageError = () => { @@ -46,18 +45,21 @@ function FileInput({ name, value, onChange }) { }; return ( -
- - {isBoxVisible && ( -
- 상품 이미지 - -
- )} +
+ +
+ + {isBoxVisible && ( +
+ 상품 이미지 + +
+ )} +
); } diff --git a/src/components/form/NumberInput.js b/src/components/form/NumberInput.js new file mode 100644 index 000000000..1f6113a2f --- /dev/null +++ b/src/components/form/NumberInput.js @@ -0,0 +1,10 @@ +import React from 'react'; + +const NumberInput = ({ label, name, value, onChange, placeholder }) => ( +
+ + +
+); + +export default NumberInput; diff --git a/src/components/form/TagInput.js b/src/components/form/TagInput.js new file mode 100644 index 000000000..7bba0fead --- /dev/null +++ b/src/components/form/TagInput.js @@ -0,0 +1,61 @@ +import React, { useState, useEffect, useCallback } from 'react'; + +const TagInput = ({ label, onTagListChange }) => { + const [tagInputValue, setTagInputValue] = useState(''); + const [tagList, setTagList] = useState([]); + + const handleKeyPress = useCallback( + e => { + if (e.key === 'Enter' && tagInputValue.trim()) { + e.preventDefault(); + const newTagName = tagInputValue.trim(); + const newTag = { + id: Date.now(), + name: newTagName, + }; + const newTagList = [...tagList, newTag]; + setTagList(newTagList); + onTagListChange(newTagList); + setTagInputValue(''); + } + }, + [tagInputValue, tagList, onTagListChange], + ); + + const handleTagRemove = useCallback( + tagIdToRemove => { + setTagList(prevTagList => { + const updatedList = prevTagList.filter(tag => tag.id !== tagIdToRemove); + onTagListChange(updatedList); + return updatedList; + }); + }, + [onTagListChange], + ); + + return ( +
+ +
+ setTagInputValue(e.target.value)} + onKeyUp={handleKeyPress} + /> + {tagList.length > 0 && ( +
+ {tagList.map(tag => ( + + {tag.name} handleTagRemove(tag.id)}> + + ))} +
+ )} +
+
+ ); +}; + +export default TagInput; diff --git a/src/components/form/TextArea.js b/src/components/form/TextArea.js new file mode 100644 index 000000000..f140f7ee7 --- /dev/null +++ b/src/components/form/TextArea.js @@ -0,0 +1,10 @@ +import React from 'react'; + +const TextArea = ({ label, name, value, onChange, placeholder, rows }) => ( +
+ + +
+); + +export default TextArea; diff --git a/src/components/form/TextInput.js b/src/components/form/TextInput.js new file mode 100644 index 000000000..cf6c1d9c0 --- /dev/null +++ b/src/components/form/TextInput.js @@ -0,0 +1,10 @@ +import React from 'react'; + +const TextInput = ({ label, name, value, onChange, placeholder }) => ( +
+ + +
+); + +export default TextInput; diff --git a/src/components/hooks/useAsync.js b/src/components/hooks/useAsync.js deleted file mode 100644 index 5546d0f2a..000000000 --- a/src/components/hooks/useAsync.js +++ /dev/null @@ -1,22 +0,0 @@ -import { useState } from 'react'; - -function useAsync(asyncFunction) { - const [pending, setPending] = useState(false); - const [error, setError] = useState(null); - - const wrappedFunction = async (...args) => { - setPending(true); - setError(null); - try { - return await asyncFunction(...args); - } catch (error) { - setError(error); - } finally { - setPending(false); - } - }; - - return [pending, error, wrappedFunction]; -} - -export default useAsync; diff --git a/src/components/Dropdown.js b/src/components/ui/Dropdown.js similarity index 100% rename from src/components/Dropdown.js rename to src/components/ui/Dropdown.js diff --git a/src/components/Loadingbar.js b/src/components/ui/Loadingbar.js similarity index 100% rename from src/components/Loadingbar.js rename to src/components/ui/Loadingbar.js diff --git a/src/components/ui/Nav.js b/src/components/ui/Nav.js new file mode 100644 index 000000000..5788a7a5c --- /dev/null +++ b/src/components/ui/Nav.js @@ -0,0 +1,28 @@ +import React from 'react'; +import { NavLink, useLocation } from 'react-router-dom'; + +const getLinkStyle = (navProps, pathname) => { + const isAddItemPage = pathname === '/additem'; + return navProps.isActive || isAddItemPage ? 'nav-link active' : 'nav-link'; +}; + +function Nav() { + const location = useLocation(); + + return ( +
    +
  • + + 자유게시판 + +
  • +
  • + getLinkStyle(navProps, location.pathname)}> + 중고마켓 + +
  • +
+ ); +} + +export default Nav; diff --git a/src/components/Pagination.js b/src/components/ui/Pagination.js similarity index 86% rename from src/components/Pagination.js rename to src/components/ui/Pagination.js index 12ff4ef05..d6533a2c0 100644 --- a/src/components/Pagination.js +++ b/src/components/ui/Pagination.js @@ -25,11 +25,8 @@ function Pagination({ totalPageNum, activePageNum, onPageChange }) { title="이전"> {pages.map(page => ( -
  • -
  • diff --git a/src/components/ProductCard.js b/src/components/ui/ProductCard.js similarity index 68% rename from src/components/ProductCard.js rename to src/components/ui/ProductCard.js index c609e18e7..967d0493c 100644 --- a/src/components/ProductCard.js +++ b/src/components/ui/ProductCard.js @@ -1,10 +1,5 @@ import React from 'react'; -import imgDefault from '../assets/img/img-default.png'; - -function formatDate(value) { - const date = new Date(value); - return `${date.getFullYear()}. ${date.getMonth() + 1}. ${date.getDate()}.`; -} +import imgDefault from '../../assets/img/img-default.png'; function ProductCard({ product }) { if (!product) { @@ -13,7 +8,7 @@ function ProductCard({ product }) { const handleError = event => { event.target.src = imgDefault; }; - const { images, name, price, favoriteCount, createdAt } = product; + const { images, name, price, favoriteCount } = product; return (
    @@ -27,7 +22,6 @@ function ProductCard({ product }) { {favoriteCount}

    -

    {formatDate(createdAt)}

    ); diff --git a/src/hooks/useInquery.js b/src/hooks/useInquery.js new file mode 100644 index 000000000..38d2b6289 --- /dev/null +++ b/src/hooks/useInquery.js @@ -0,0 +1,28 @@ +import { useState, useEffect } from 'react'; +import { getInquery } from './../services/api'; + +const useInquery = productId => { + const [inquiries, setInquiries] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchInquiries = async () => { + try { + setIsLoading(true); + const fetchedInquiries = await getInquery(productId); + setInquiries(fetchedInquiries.list); + } catch (err) { + setError(err); + } finally { + setIsLoading(false); + } + }; + + fetchInquiries(); + }, [productId]); + + return { inquiries, isLoading, error }; +}; + +export default useInquery; diff --git a/src/hooks/useProductId.js b/src/hooks/useProductId.js new file mode 100644 index 000000000..45f039e5a --- /dev/null +++ b/src/hooks/useProductId.js @@ -0,0 +1,28 @@ +import { useState, useEffect } from 'react'; +import { getProductById } from './../services/api'; + +const useProductId = productId => { + const [product, setProduct] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchProduct = async () => { + try { + setIsLoading(true); + const fetchedProduct = await getProductById(productId); + setProduct(fetchedProduct); + } catch (err) { + setError(err); + } finally { + setIsLoading(false); + } + }; + + fetchProduct(); + }, [productId]); + + return { product, isLoading, error }; +}; + +export default useProductId; diff --git a/src/hooks/useProductList.js b/src/hooks/useProductList.js new file mode 100644 index 000000000..b3ca625a3 --- /dev/null +++ b/src/hooks/useProductList.js @@ -0,0 +1,51 @@ +import { useState, useEffect } from 'react'; +import { getProducts } from '../services/api'; + +const useProducts = (initialOrderBy, getPageSize) => { + const [orderBy, setOrderBy] = useState(initialOrderBy); + const [products, setProducts] = useState([]); + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(getPageSize()); + const [totalPageNum, setTotalPageNum] = useState(0); + const [loading, setLoading] = useState(false); + + const handleLoad = async ({ orderBy, page, pageSize }) => { + setLoading(true); + try { + const products = await getProducts({ orderBy, page, pageSize }); + setProducts(products.list); + setTotalPageNum(Math.ceil(products.totalCount / pageSize)); + } catch (error) { + console.error('Failed to load products:', error); + setProducts([]); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + const handleResize = () => { + setPageSize(getPageSize()); + }; + window.addEventListener('resize', handleResize); + + handleLoad({ orderBy, page, pageSize }); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, [orderBy, page, pageSize]); + + return { + orderBy, + setOrderBy, + products, + page, + setPage, + pageSize, + totalPageNum, + loading, + }; +}; + +export default useProducts; diff --git a/src/layout/Header.js b/src/layout/Header.js index 8903c3dc7..b22c487b1 100644 --- a/src/layout/Header.js +++ b/src/layout/Header.js @@ -1,4 +1,4 @@ -import Nav from './../components/Nav'; +import Nav from '../components/ui/Nav'; import { Link } from 'react-router-dom'; function Header() { diff --git a/src/pages/AddItem.js b/src/pages/AddItem.js deleted file mode 100644 index eaa3c73cc..000000000 --- a/src/pages/AddItem.js +++ /dev/null @@ -1,117 +0,0 @@ -import { Helmet } from 'react-helmet'; -import { useState, useMemo, useCallback } from 'react'; -import './AddItem.scss'; -import Header from '../layout/Header'; -import FileInput from '../components/FileInput'; -import TagInput from '../components/TagInput'; - -const INITIAL_VALUES = { - imgFile: null, - product: '', - content: '', - price: 0, - tag: [], -}; - -const AddItem = () => { - const [values, setValues] = useState(INITIAL_VALUES); - const [resetTagInput, setResetTagInput] = useState(false); - - const resetForm = () => { - setValues(INITIAL_VALUES); - setResetTagInput(true); - }; - - const isFormValid = useMemo(() => { - const { product, content, price, tag } = values; - return product.trim() !== '' && content.trim() !== '' && price !== 0 && tag.length > 0; - }, [values]); - - const handleChange = useCallback((name, value) => { - setValues(prevValues => ({ - ...prevValues, - [name]: value, - })); - }, []); - - const handleInputChange = e => { - const { name, value, type } = e.target; - handleChange(name, type === 'file' ? e.target.files[0] : value); - }; - - const handleTagListChange = useCallback( - tagList => { - handleChange('tag', tagList); - }, - [handleChange], - ); - - const handleSubmit = e => { - e.preventDefault(); - e.stopPropagation(); - alert('등록되었습니다'); - console.log(values); - resetForm(); - }; - - return ( - <> - - 판다마켓 - 상품 등록 - -
    -
    -
    -
    -
    -

    상품 등록하기

    - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    -
    -
    - - ); -}; - - -export default AddItem; diff --git a/src/pages/Login.js b/src/pages/Auth/Login.js similarity index 84% rename from src/pages/Login.js rename to src/pages/Auth/Login.js index 46e4fd01b..5986db9db 100644 --- a/src/pages/Login.js +++ b/src/pages/Auth/Login.js @@ -1,5 +1,5 @@ import { Helmet } from 'react-helmet'; -import Header from '../layout/Header'; +import Header from '../../layout/Header'; import './Login.scss'; function Login() { diff --git a/src/pages/Login.scss b/src/pages/Auth/Login.scss similarity index 100% rename from src/pages/Login.scss rename to src/pages/Auth/Login.scss diff --git a/src/pages/Signup.js b/src/pages/Auth/Signup.js similarity index 86% rename from src/pages/Signup.js rename to src/pages/Auth/Signup.js index 2fd919c34..855d03229 100644 --- a/src/pages/Signup.js +++ b/src/pages/Auth/Signup.js @@ -1,6 +1,6 @@ import React from 'react'; import { Helmet } from 'react-helmet'; -import Header from '../layout/Header'; +import Header from '../../layout/Header'; import './Signup.scss'; function Signup() { diff --git a/src/pages/Signup.scss b/src/pages/Auth/Signup.scss similarity index 100% rename from src/pages/Signup.scss rename to src/pages/Auth/Signup.scss diff --git a/src/pages/AddBoard.js b/src/pages/Board/AddBoard.js similarity index 85% rename from src/pages/AddBoard.js rename to src/pages/Board/AddBoard.js index 35a9efdc5..ca1703fd4 100644 --- a/src/pages/AddBoard.js +++ b/src/pages/Board/AddBoard.js @@ -1,5 +1,5 @@ import { Helmet } from 'react-helmet'; -import Header from '../layout/Header'; +import Header from '../../layout/Header'; import './AddBoard.scss'; function AddBoard() { diff --git a/src/pages/AddBoard.scss b/src/pages/Board/AddBoard.scss similarity index 100% rename from src/pages/AddBoard.scss rename to src/pages/Board/AddBoard.scss diff --git a/src/pages/Board.js b/src/pages/Board/Board.js similarity index 85% rename from src/pages/Board.js rename to src/pages/Board/Board.js index 1e1526911..e12ceb495 100644 --- a/src/pages/Board.js +++ b/src/pages/Board/Board.js @@ -1,5 +1,5 @@ import { Helmet } from 'react-helmet'; -import Header from '../layout/Header'; +import Header from '../../layout/Header'; import './Board.scss'; function Board() { diff --git a/src/pages/Board.scss b/src/pages/Board/Board.scss similarity index 100% rename from src/pages/Board.scss rename to src/pages/Board/Board.scss diff --git a/src/pages/Notfound.js b/src/pages/Error/Notfound.js similarity index 100% rename from src/pages/Notfound.js rename to src/pages/Error/Notfound.js diff --git a/src/pages/Notfound.scss b/src/pages/Error/Notfound.scss similarity index 100% rename from src/pages/Notfound.scss rename to src/pages/Error/Notfound.scss diff --git a/src/pages/Home.js b/src/pages/Home/Home.js similarity index 73% rename from src/pages/Home.js rename to src/pages/Home/Home.js index e8dade7d2..90b0f53d6 100644 --- a/src/pages/Home.js +++ b/src/pages/Home/Home.js @@ -1,6 +1,6 @@ import { Helmet } from 'react-helmet'; -import Header from '../layout/Header'; -import Footer from '../layout/Footer'; +import Header from '../../layout/Header'; +import Footer from '../../layout/Footer'; import './Home.scss'; function Home() { diff --git a/src/pages/Home.scss b/src/pages/Home/Home.scss similarity index 100% rename from src/pages/Home.scss rename to src/pages/Home/Home.scss diff --git a/src/pages/Item/AddItem.js b/src/pages/Item/AddItem.js new file mode 100644 index 000000000..3c405c1a9 --- /dev/null +++ b/src/pages/Item/AddItem.js @@ -0,0 +1,113 @@ +import { Helmet } from 'react-helmet'; +import { useState, useMemo, useCallback } from 'react'; +import './AddItem.scss'; +import Header from '../../layout/Header'; +import FileInput from '../../components/form/FileInput'; +import TagInput from '../../components/form/TagInput'; +import TextInput from '../../components/form/TextInput'; +import TextArea from '../../components/form/TextArea'; +import NumberInput from '../../components/form/NumberInput'; + +const INITIAL_VALUES = { + imgFile: null, + product: '', + content: '', + price: 0, + tag: [], +}; + +const AddItem = () => { + const [values, setValues] = useState(INITIAL_VALUES); + + const isFormValid = useMemo(() => { + const { product, content, price, tag } = values; + return product.trim() !== '' && content.trim() !== '' && price !== 0 && tag.length > 0; + }, [values]); + + const handleChange = useCallback((name, value) => { + setValues(prevValues => ({ + ...prevValues, + [name]: value, + })); + }, []); + + const handleInputChange = e => { + const { name, value, type } = e.target; + handleChange(name, type === 'file' ? e.target.files[0] : value); + }; + + const handleTagListChange = useCallback( + tagList => { + handleChange('tag', tagList); + }, + [handleChange], + ); + + const handleFormKeyDown = event => { + // textarea 안에서는 Enter 키를 허용 + if (event.key === 'Enter' && event.target.tagName !== 'TEXTAREA') { + event.preventDefault(); + } + }; + + const handleSubmit = e => { + e.preventDefault(); + e.stopPropagation(); + alert('등록되었습니다'); + console.log(values); + //추후 READ 해당파일로 이동 + window.location.reload(); + }; + + return ( + <> + + 판다마켓 - 상품 등록 + +
    +
    +
    +
    +
    +

    상품 등록하기

    + +
    + + + + + + + ); +} + +export default InqueryForm; diff --git a/src/pages/Item/components/InqueryList.js b/src/pages/Item/components/InqueryList.js new file mode 100644 index 000000000..6e9f72c0b --- /dev/null +++ b/src/pages/Item/components/InqueryList.js @@ -0,0 +1,43 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import Img_inquiry_empty from '../../../assets/img/Img_inquiry_empty.png'; +import timeAgo from '../../../utils/timeAgo'; + +function InquiryList({ inquiries }) { + const InqueryNone = ( +
    + 이미지 +

    아직 문의가 없습니다.

    +
    + ); + + return ( +
    + {inquiries.length === 0 ? ( + InqueryNone + ) : ( +
      + {inquiries.map(inquiry => ( +
    • +
      {inquiry.content}
      +
      +
      + 프로필 이미지 +
      +
      +
      {inquiry.writer.nickname}
      +
      {timeAgo(inquiry.createdAt)}
      {/* 시간 변환 함수 사용 */} +
      +
      +
    • + ))} +
    + )} + + 목록으로 돌아가기   + +
    + ); +} + +export default InquiryList; diff --git a/src/pages/Item/components/ProductInfo.js b/src/pages/Item/components/ProductInfo.js new file mode 100644 index 000000000..92fdfbb88 --- /dev/null +++ b/src/pages/Item/components/ProductInfo.js @@ -0,0 +1,47 @@ +import React from 'react'; +import { useState } from 'react'; +import ImgDefault from '../../../assets/img/img-default.png'; + +function ProductInfo({ product }) { + const { images, name, price, description, tags, favoriteCount } = product; + const [isFavorite, setIsFavorite] = useState(false); + + const toggleFavorite = () => { + setIsFavorite(prevState => !prevState); + }; + const ImgError = event => { + event.target.src = ImgDefault; + }; + + return ( +
    +
    + {name} +
    +
    +
    +
    {name}
    +
    +
    {price.toLocaleString()}원
    +
    +
    +
    + +

    {description}

    + +
    + {tags.map((tag, index) => ( + #{tag} + ))} +
    +
    + +
    +
    + ); +} + +export default ProductInfo; diff --git a/src/pages/Faq.js b/src/pages/Term/Faq.js similarity index 84% rename from src/pages/Faq.js rename to src/pages/Term/Faq.js index 911cb2789..3dc33f5e9 100644 --- a/src/pages/Faq.js +++ b/src/pages/Term/Faq.js @@ -1,5 +1,5 @@ import { Helmet } from 'react-helmet'; -import Header from '../layout/Header'; +import Header from '../../layout/Header'; import './Faq.scss'; function Faq() { diff --git a/src/pages/Faq.scss b/src/pages/Term/Faq.scss similarity index 100% rename from src/pages/Faq.scss rename to src/pages/Term/Faq.scss diff --git a/src/pages/Privacy.js b/src/pages/Term/Privacy.js similarity index 85% rename from src/pages/Privacy.js rename to src/pages/Term/Privacy.js index 192873a04..fa01d3583 100644 --- a/src/pages/Privacy.js +++ b/src/pages/Term/Privacy.js @@ -1,5 +1,5 @@ import { Helmet } from 'react-helmet'; -import Header from '../layout/Header'; +import Header from '../../layout/Header'; import './Privacy.scss'; function Privacy() { diff --git a/src/pages/privacy.scss b/src/pages/Term/privacy.scss similarity index 100% rename from src/pages/privacy.scss rename to src/pages/Term/privacy.scss diff --git a/src/services/api.js b/src/services/api.js index 406c7ffb3..87876ccac 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -1,17 +1,46 @@ -export async function getProducts(params = {}) { +const API_BASE_URL = 'https://panda-market-api.vercel.app'; + +export async function getProducts(params = {}) { const query = new URLSearchParams(params).toString(); try { - const response = await fetch( - `https://panda-market-api.vercel.app/products?${query}` - ); + const response = await fetch(`${API_BASE_URL}/products?${query}`); + if (!response.ok) { + throw new Error(`HTTP error: ${response.status}`); + } + const body = await response.json(); + return body; + } catch (error) { + console.error('Failed to fetch products:', error); + throw error; + } +} + +export async function getProductById(productId) { + try { + const response = await fetch(`${API_BASE_URL}/products/${productId}`); if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } const body = await response.json(); return body; } catch (error) { - console.error("Failed to fetch products:", error); + console.error('Failed to fetch product:', error); + throw error; + } +} + +export async function getInquery(productId) { + try { + const response = await fetch(`${API_BASE_URL}/products/${productId}/comments?limit=100`); + if (!response.ok) { + const errorBody = await response.json(); + throw new Error(`HTTP error: ${response.status}, ${errorBody.message}`); + } + const body = await response.json(); + return body; + } catch (error) { + console.error('Failed to fetch comments:', error); throw error; } } diff --git a/src/utils/timeAgo.js b/src/utils/timeAgo.js new file mode 100644 index 000000000..d7b7119f1 --- /dev/null +++ b/src/utils/timeAgo.js @@ -0,0 +1,24 @@ +function timeAgo(date) { + const now = new Date(); + const past = new Date(date); + const diffInSeconds = Math.floor((now - past) / 1000); + + const units = [ + { name: '년', seconds: 60 * 60 * 24 * 365 }, + { name: '개월', seconds: 60 * 60 * 24 * 30 }, + { name: '일', seconds: 60 * 60 * 24 }, + { name: '시간', seconds: 60 * 60 }, + { name: '분', seconds: 60 }, + { name: '초', seconds: 1 }, + ]; + + for (const unit of units) { + const value = Math.floor(diffInSeconds / unit.seconds); + if (value > 0) { + return `${value}${unit.name} 전`; + } + } + return '방금 전'; +} + +export default timeAgo;