diff --git a/package-lock.json b/package-lock.json
index 58accf843..1b069c48a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,10 +11,15 @@
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
+ "@types/jest": "^29.5.14",
+ "@types/node": "^22.10.5",
+ "@types/react": "^19.0.3",
+ "@types/react-dom": "^19.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.28.0",
"react-scripts": "5.0.1",
+ "typescript": "^5.7.2",
"web-vitals": "^2.1.4"
}
},
@@ -3784,6 +3789,26 @@
"node": ">=12"
}
},
+ "node_modules/@testing-library/react/node_modules/@types/react": {
+ "version": "18.3.18",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz",
+ "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@testing-library/react/node_modules/@types/react-dom": {
+ "version": "18.3.5",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz",
+ "integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^18.0.0"
+ }
+ },
"node_modules/@testing-library/react/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@@ -4056,9 +4081,10 @@
}
},
"node_modules/@types/jest": {
- "version": "29.5.4",
- "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.4.tgz",
- "integrity": "sha512-PhglGmhWeD46FYOVLt3X7TiWjzwuVGW9wG/4qocPevXMjCmrIc5b6db9WjeGE4QYVpUAWMDv3v0IiBwObY289A==",
+ "version": "29.5.14",
+ "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz",
+ "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==",
+ "license": "MIT",
"dependencies": {
"expect": "^29.0.0",
"pretty-format": "^29.0.0"
@@ -4307,9 +4333,13 @@
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw=="
},
"node_modules/@types/node": {
- "version": "20.5.9",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.9.tgz",
- "integrity": "sha512-PcGNd//40kHAS3sTlzKB9C9XL4K0sTup8nbG5lC14kzEteTNuAFh9u5nA0o5TWnSG2r/JNPRXFVcHJIIeRlmqQ=="
+ "version": "22.10.5",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz",
+ "integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.20.0"
+ }
},
"node_modules/@types/parse-json": {
"version": "4.0.0",
@@ -4322,9 +4352,11 @@
"integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA=="
},
"node_modules/@types/prop-types": {
- "version": "15.7.5",
- "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
- "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w=="
+ "version": "15.7.14",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
+ "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
+ "license": "MIT",
+ "peer": true
},
"node_modules/@types/q": {
"version": "1.5.6",
@@ -4342,21 +4374,21 @@
"integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw=="
},
"node_modules/@types/react": {
- "version": "18.2.21",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.21.tgz",
- "integrity": "sha512-neFKG/sBAwGxHgXiIxnbm3/AAVQ/cMRS93hvBpg8xYRbeQSPVABp9U2bRnPf0iI4+Ucdv3plSxKK+3CW2ENJxA==",
+ "version": "19.0.3",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.3.tgz",
+ "integrity": "sha512-UavfHguIjnnuq9O67uXfgy/h3SRJbidAYvNjLceB+2RIKVRBzVsh0QO+Pw6BCSQqFS9xwzKfwstXx0m6AbAREA==",
+ "license": "MIT",
"dependencies": {
- "@types/prop-types": "*",
- "@types/scheduler": "*",
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-dom": {
- "version": "18.2.7",
- "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.7.tgz",
- "integrity": "sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==",
- "dependencies": {
- "@types/react": "*"
+ "version": "19.0.2",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.2.tgz",
+ "integrity": "sha512-c1s+7TKFaDRRxr1TxccIX2u7sfCnc3RxkVyBIUA2lCpyqCF+QoAwQ/CBg7bsMdVwP120HEH143VQezKtef5nCg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.0.0"
}
},
"node_modules/@types/resolve": {
@@ -4372,11 +4404,6 @@
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
"integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="
},
- "node_modules/@types/scheduler": {
- "version": "0.16.3",
- "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz",
- "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ=="
- },
"node_modules/@types/semver": {
"version": "7.5.1",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.1.tgz",
@@ -16666,16 +16693,16 @@
}
},
"node_modules/typescript": {
- "version": "4.9.5",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
- "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
- "peer": true,
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
+ "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
+ "license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
- "node": ">=4.2.0"
+ "node": ">=14.17"
}
},
"node_modules/unbox-primitive": {
@@ -16697,6 +16724,12 @@
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz",
"integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw=="
},
+ "node_modules/undici-types": {
+ "version": "6.20.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
+ "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
+ "license": "MIT"
+ },
"node_modules/unicode-canonical-property-names-ecmascript": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz",
diff --git a/package.json b/package.json
index 7b54ab4b6..b76fe328f 100644
--- a/package.json
+++ b/package.json
@@ -6,10 +6,15 @@
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
+ "@types/jest": "^29.5.14",
+ "@types/node": "^22.10.5",
+ "@types/react": "^19.0.3",
+ "@types/react-dom": "^19.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.28.0",
"react-scripts": "5.0.1",
+ "typescript": "^5.7.2",
"web-vitals": "^2.1.4"
},
"scripts": {
diff --git a/src/App.js b/src/App.tsx
similarity index 65%
rename from src/App.js
rename to src/App.tsx
index 74769b68d..3a9f82662 100644
--- a/src/App.js
+++ b/src/App.tsx
@@ -1,8 +1,9 @@
import { BrowserRouter, Route, Routes } from "react-router-dom";
import Header from "./component/Navigation/Header";
import MainPage from "./pages/MainPage";
-import UsedMarketPage from "./pages/UsedMarketPage";
import PostProductPage from "./pages/PostProductPage";
+import ProductDetail from "./pages/ProductDetail";
+import "./styles/global.css";
function App() {
return (
@@ -10,7 +11,10 @@ function App() {
} />
- } />
+
+ } />
+ } />
+
} />
diff --git a/src/api.js b/src/api.js
deleted file mode 100644
index 4813b3b7e..000000000
--- a/src/api.js
+++ /dev/null
@@ -1,20 +0,0 @@
-export async function getProduct({
- page = 1,
- pageSize = 4,
- orderBy = "favorite",
- keyword = "",
-}) {
- //기본 URL
- const baseURL = "https://panda-market-api.vercel.app/products";
- //URL 객체
- const url = new URL(baseURL);
- url.searchParams.set("page", page);
- url.searchParams.set("pageSize", pageSize);
- url.searchParams.set("orderBy", orderBy);
- url.searchParams.set("keyword", keyword);
- //API 호출
- console.log(url.toString());
- const response = await fetch(url.toString());
- const data = await response.json();
- return data;
-}
diff --git a/src/api.tsx b/src/api.tsx
new file mode 100644
index 000000000..7e28b6094
--- /dev/null
+++ b/src/api.tsx
@@ -0,0 +1,50 @@
+interface Product {
+ page?: number;
+ pageSize: number;
+ orderBy: string;
+ keyword?: string;
+}
+
+interface Comment {
+ productSlug: string;
+ limit: number;
+}
+
+export async function getProduct({
+ page = 1,
+ pageSize = 4,
+ orderBy = "favorite",
+ keyword = "",
+}: Product) {
+ //기본 URL
+ const baseURL = "https://panda-market-api.vercel.app/products";
+ //URL 객체
+ const url = new URL(baseURL);
+ url.searchParams.set("page", page.toString());
+ url.searchParams.set("pageSize", pageSize.toString());
+ url.searchParams.set("orderBy", orderBy);
+ url.searchParams.set("keyword", keyword);
+ //API 호출
+ console.log(url.toString());
+ const response = await fetch(url.toString());
+ const data = await response.json();
+ return data;
+}
+
+//제품 상세페이지
+export async function getProductDetail(productSlug: string) {
+ const response = await fetch(
+ `https://panda-market-api.vercel.app/products/${productSlug}`
+ );
+ const data = await response.json();
+ return data;
+}
+
+//제품 코멘트
+export async function getProductComment({ productSlug, limit }: Comment) {
+ const response = await fetch(
+ `https://panda-market-api.vercel.app/products/${productSlug}/comments?limit=${limit}`
+ );
+ const data = await response.json();
+ return data;
+}
diff --git a/src/asset/Img_inquiry_empty.png b/src/asset/Img_inquiry_empty.png
new file mode 100644
index 000000000..4c725d079
Binary files /dev/null and b/src/asset/Img_inquiry_empty.png differ
diff --git a/src/asset/fdec874e30ff90436580b2b8d751e9f5.png b/src/asset/fdec874e30ff90436580b2b8d751e9f5.png
new file mode 100644
index 000000000..a84b2160e
Binary files /dev/null and b/src/asset/fdec874e30ff90436580b2b8d751e9f5.png differ
diff --git a/src/asset/ic_back.png b/src/asset/ic_back.png
new file mode 100644
index 000000000..d0fecd482
Binary files /dev/null and b/src/asset/ic_back.png differ
diff --git a/src/asset/ic_kebab.png b/src/asset/ic_kebab.png
new file mode 100644
index 000000000..14f12b84a
Binary files /dev/null and b/src/asset/ic_kebab.png differ
diff --git a/src/component/AllProduct/AllProductItem.js b/src/component/AllProduct/AllProductItem.js
deleted file mode 100644
index 6f7c08735..000000000
--- a/src/component/AllProduct/AllProductItem.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import noPhotoImg from "../../asset/nophoto.png";
-import HeartIcon from "../../asset/ic_heart.png";
-import "./AllProductItem.css";
-
-function AllProductItem({ item }) {
- const onErrorImg = (e) => {
- e.target.src = noPhotoImg;
- };
- //이미지 링크가 잘못된 링크일 때, 기본 이미지 출력
- return (
-
- {item.images[0] ? (
-

- ) : (
-

- )}
-
{item.name}
-
{item.price.toLocaleString()}
-
-

-
{item.favoriteCount}
-
-
- );
-}
-
-export default AllProductItem;
diff --git a/src/component/AllProduct/AllProductItem.css b/src/component/AllProduct/AllProductItem.module.css
similarity index 57%
rename from src/component/AllProduct/AllProductItem.css
rename to src/component/AllProduct/AllProductItem.module.css
index 7ca52287f..032c802b9 100644
--- a/src/component/AllProduct/AllProductItem.css
+++ b/src/component/AllProduct/AllProductItem.module.css
@@ -3,19 +3,26 @@
margin: 0;
}
-.allproduct-img {
+.allproduct_img {
width: 221px;
height: 221px;
border-radius: 16px;
object-fit: cover;
}
-.allproduct-popularity {
+.allproduct_popularity {
width: 16px;
height: 16px;
+ color: black;
}
-.allproduct-title {
+.allproduct_title {
font-size: 14px;
font-weight: 500;
+ color: black;
+}
+
+.link {
+ text-decoration: none;
+ color: black;
}
diff --git a/src/component/AllProduct/AllProductItem.tsx b/src/component/AllProduct/AllProductItem.tsx
new file mode 100644
index 000000000..7ae4695b7
--- /dev/null
+++ b/src/component/AllProduct/AllProductItem.tsx
@@ -0,0 +1,57 @@
+import noPhotoImg from "../../asset/nophoto.png";
+import HeartIcon from "../../asset/ic_heart.png";
+import styles from "./AllProductItem.module.css";
+import { Link } from "react-router-dom";
+import { SyntheticEvent } from "react";
+
+interface Product {
+ id: number;
+ name: string;
+ price: number;
+ description: string;
+ tags: string[];
+ images: string[];
+ createdAt: string;
+ ownerNickname: string;
+ favoriteCount: number;
+}
+
+function AllProductItem({ item }: { item: Product }) {
+ const onErrorImg = (e: SyntheticEvent) => {
+ e.currentTarget.src = noPhotoImg;
+ };
+ //이미지 링크가 잘못된 링크일 때, 기본 이미지 출력
+
+ return (
+
+
+ {item.images[0] ? (
+

+ ) : (
+

+ )}
+
{item.name}
+
{item.price.toLocaleString()}
+
+

+
{item.favoriteCount}
+
+
+
+ );
+}
+
+export default AllProductItem;
diff --git a/src/component/AllProduct/AllProductList.css b/src/component/AllProduct/AllProductList.module.css
similarity index 88%
rename from src/component/AllProduct/AllProductList.css
rename to src/component/AllProduct/AllProductList.module.css
index 4d4972979..599266cc2 100644
--- a/src/component/AllProduct/AllProductList.css
+++ b/src/component/AllProduct/AllProductList.module.css
@@ -11,14 +11,14 @@
gap: 43px;
}
-.allsection-container {
+.allsection_container {
width: 1200px;
display: flex;
flex-direction: column;
gap: 24px;
}
-.allproduct-list {
+.allproduct_list {
display: flex;
flex-wrap: wrap;
gap: 23px;
diff --git a/src/component/AllProduct/AllProductList.js b/src/component/AllProduct/AllProductList.tsx
similarity index 63%
rename from src/component/AllProduct/AllProductList.js
rename to src/component/AllProduct/AllProductList.tsx
index 20ae89a5b..1e51eaa7d 100644
--- a/src/component/AllProduct/AllProductList.js
+++ b/src/component/AllProduct/AllProductList.tsx
@@ -1,19 +1,38 @@
import AllProductItem from "./AllProductItem";
-import "./AllProductList.css";
+import styles from "./AllProductList.module.css";
import { getProduct } from "../../api";
import { useEffect, useState } from "react";
import NavBar from "../Navigation/NavBar";
import PageButton from "../Pagination/PageButton";
+interface ProductValue {
+ page?: number;
+ pageSize: number;
+ orderBy: string;
+ keyword?: string;
+}
+
+interface Product {
+ id: number;
+ name: string;
+ price: number;
+ description: string;
+ tags: string[];
+ images: string[];
+ createdAt: string;
+ ownerNickname: string;
+ favoriteCount: number;
+}
+
function AllProductList() {
- const [items, setItems] = useState([]);
+ const [items, setItems] = useState([]);
const [orderBy, setOrderBy] = useState("recent");
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [keyword, setKeyword] = useState("");
const [maxPage, setMaxPage] = useState(5);
- const handleLoad = async (value) => {
+ const handleLoad = async (value: ProductValue) => {
const result = await getProduct(value);
const { list, totalCount } = result;
setItems(list);
@@ -25,25 +44,25 @@ function AllProductList() {
handleLoad({ page, pageSize, orderBy, keyword });
}, [page, pageSize, orderBy, keyword]);
- const handleOrder = (value) => {
+ const handleOrder = (value: string) => {
setOrderBy(value);
setPage(1);
};
- const handlePage = (value) => {
+ const handlePage = (value: number) => {
setPage(value);
};
- const handleKeyword = (value) => {
+ const handleKeyword = (value: string) => {
setKeyword(value);
setPage(1);
};
return (
-
-
+
+
-
+
{items.map((item) => {
return
;
})}
diff --git a/src/component/BestProduct/BestProductItem.js b/src/component/BestProduct/BestProductItem.js
deleted file mode 100644
index 1fbcb796a..000000000
--- a/src/component/BestProduct/BestProductItem.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import HeartIcon from "../../asset/ic_heart.png";
-import "./BestProductItem.css";
-
-function ProductItem({ item }) {
- return (
-
-

-
{item.name}
-
{item.price.toLocaleString()}
-
-

-
{item.favoriteCount}
-
-
- );
-}
-
-export default ProductItem;
diff --git a/src/component/BestProduct/BestProductItem.css b/src/component/BestProduct/BestProductItem.module.css
similarity index 63%
rename from src/component/BestProduct/BestProductItem.css
rename to src/component/BestProduct/BestProductItem.module.css
index 76b835023..90d242854 100644
--- a/src/component/BestProduct/BestProductItem.css
+++ b/src/component/BestProduct/BestProductItem.module.css
@@ -3,19 +3,24 @@
margin: 0;
}
-.bestproduct-img {
+.bestproduct_img {
width: 282px;
height: 282px;
border-radius: 16px;
object-fit: cover;
}
-.bestproduct-title {
+.bestproduct_title {
font-size: 14px;
font-weight: 500;
}
-.bestproduct-popularity {
+.bestproduct_popularity {
width: 16px;
height: 16px;
}
+
+.link {
+ text-decoration: none;
+ color: black;
+}
diff --git a/src/component/BestProduct/BestProductItem.tsx b/src/component/BestProduct/BestProductItem.tsx
new file mode 100644
index 000000000..9441f89e8
--- /dev/null
+++ b/src/component/BestProduct/BestProductItem.tsx
@@ -0,0 +1,41 @@
+import { Link } from "react-router-dom";
+import HeartIcon from "../../asset/ic_heart.png";
+import styles from "./BestProductItem.module.css";
+
+interface Product {
+ id: number;
+ name: string;
+ price: number;
+ description: string;
+ tags: string[];
+ images: string[];
+ createdAt: string;
+ ownerNickname: string;
+ favoriteCount: number;
+}
+
+function ProductItem({ item }: { item: Product }) {
+ return (
+
+
+

+
{item.name}
+
{item.price.toLocaleString()}
+
+

+
{item.favoriteCount}
+
+
+
+ );
+}
+
+export default ProductItem;
diff --git a/src/component/BestProduct/BestProductList.js b/src/component/BestProduct/BestProductList.js
deleted file mode 100644
index f01ca505d..000000000
--- a/src/component/BestProduct/BestProductList.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import BestProductItem from "./BestProductItem";
-import "./BestProductList.css";
-import { getProduct } from "../../api";
-import { useEffect, useState } from "react";
-
-function BestProductList() {
- const [items, setItems] = useState([]);
- const handleLoad = async (value) => {
- let result = await getProduct(value);
- const { list } = result;
- setItems(list);
- };
- useEffect(() => {
- handleLoad({ pageSize: 4, orderBy: "favorite" });
- }, []);
-
- return (
-
-
-
베스트 상품
-
- {items.map((item) => {
- return ;
- })}
-
-
-
- );
-}
-
-export default BestProductList;
diff --git a/src/component/BestProduct/BestProductList.css b/src/component/BestProduct/BestProductList.module.css
similarity index 81%
rename from src/component/BestProduct/BestProductList.css
rename to src/component/BestProduct/BestProductList.module.css
index b4b4ef8a8..d15f9f27b 100644
--- a/src/component/BestProduct/BestProductList.css
+++ b/src/component/BestProduct/BestProductList.module.css
@@ -9,11 +9,11 @@
align-items: center;
}
-.bestsection-container {
+.bestsection_container {
width: 1200px;
}
-.bestproduct-list {
+.bestproduct_list {
display: flex;
gap: 24px;
flex-wrap: wrap;
diff --git a/src/component/BestProduct/BestProductList.tsx b/src/component/BestProduct/BestProductList.tsx
new file mode 100644
index 000000000..f6ec97d6f
--- /dev/null
+++ b/src/component/BestProduct/BestProductList.tsx
@@ -0,0 +1,52 @@
+import BestProductItem from "./BestProductItem";
+import styles from "./BestProductList.module.css";
+import { getProduct } from "../../api";
+import { useEffect, useState } from "react";
+
+interface ProductValue {
+ page?: number;
+ pageSize: number;
+ orderBy: string;
+ keyword?: string;
+}
+
+interface Product {
+ id: number;
+ name: string;
+ price: number;
+ description: string;
+ tags: string[];
+ images: string[];
+ createdAt: string;
+ ownerNickname: string;
+ favoriteCount: number;
+}
+
+function BestProductList() {
+ const [items, setItems] = useState
([]);
+
+ const handleLoad = async (value: ProductValue) => {
+ let result = await getProduct(value);
+ const { list } = result;
+ setItems(list);
+ };
+
+ useEffect(() => {
+ handleLoad({ pageSize: 4, orderBy: "favorite" });
+ }, []);
+
+ return (
+
+
+
베스트 상품
+
+ {items.map((item) => {
+ return ;
+ })}
+
+
+
+ );
+}
+
+export default BestProductList;
diff --git a/src/component/Navigation/Header.js b/src/component/Navigation/Header.js
deleted file mode 100644
index 629c159e1..000000000
--- a/src/component/Navigation/Header.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import Logo from "../../asset/panda.png";
-import UserImg from "../../asset/userIcon.png";
-import "./Header.css";
-import { Link } from "react-router-dom";
-
-function Header() {
- return (
-
- );
-}
-
-export default Header;
diff --git a/src/component/Navigation/Header.css b/src/component/Navigation/Header.module.css
similarity index 82%
rename from src/component/Navigation/Header.css
rename to src/component/Navigation/Header.module.css
index 1cafd1d29..a8f722c1a 100644
--- a/src/component/Navigation/Header.css
+++ b/src/component/Navigation/Header.module.css
@@ -11,7 +11,7 @@
border-bottom: solid 1px #dfdfdf;
}
-.header-container {
+.headercontainer {
display: flex;
align-items: center;
width: 100%;
@@ -26,30 +26,30 @@
text-decoration: none;
}
-.header__logo-img {
+.header__logoimg {
width: 40px;
}
-.header__logo-title {
+.header__logotitle {
font-size: 25px;
font-weight: 700;
color: #3692ff;
}
-.header__Link-container {
+.header__Linkcontainer {
display: flex;
gap: 30px;
flex-grow: 1;
}
-.header__Link-title {
+.header__Linktitle {
font-size: 18px;
font-weight: 700;
color: #4b5563;
text-decoration: none;
}
-.header__profile-img {
+.header__profileimg {
width: 40px;
height: 40px;
}
diff --git a/src/component/Navigation/Header.tsx b/src/component/Navigation/Header.tsx
new file mode 100644
index 000000000..4389a8888
--- /dev/null
+++ b/src/component/Navigation/Header.tsx
@@ -0,0 +1,36 @@
+import Logo from "../../asset/panda.png";
+import UserImg from "../../asset/userIcon.png";
+import styles from "./Header.module.css";
+import { Link } from "react-router-dom";
+
+function Header() {
+ return (
+
+ );
+}
+
+export default Header;
diff --git a/src/component/Navigation/NavBar.js b/src/component/Navigation/NavBar.js
deleted file mode 100644
index 58002e4e7..000000000
--- a/src/component/Navigation/NavBar.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import { Link } from "react-router-dom";
-import "./NavBar.css";
-import { useState } from "react";
-
-function NavBar({ handleOrder, handleKeyword }) {
- const [searchInput, setSearchInput] = useState("");
-
- const onClickOption = (e) => {
- handleOrder(e.target.value);
- };
-
- const handleChangeInput = (e) => {
- setSearchInput(e.target.value);
- };
-
- //엔터키 입력시 상품 검색
- const onKeyDownEnter = (e) => {
- if (e.key === "Enter") {
- handleKeyword(searchInput);
- }
- };
-
- return (
-
- );
-}
-
-export default NavBar;
diff --git a/src/component/Navigation/NavBar.css b/src/component/Navigation/NavBar.module.css
similarity index 96%
rename from src/component/Navigation/NavBar.css
rename to src/component/Navigation/NavBar.module.css
index a53a938bc..c1478ff11 100644
--- a/src/component/Navigation/NavBar.css
+++ b/src/component/Navigation/NavBar.module.css
@@ -7,7 +7,7 @@
display: flex;
}
-.all-item__title {
+.allitem__title {
flex-grow: 1;
}
diff --git a/src/component/Navigation/NavBar.tsx b/src/component/Navigation/NavBar.tsx
new file mode 100644
index 000000000..5834fb503
--- /dev/null
+++ b/src/component/Navigation/NavBar.tsx
@@ -0,0 +1,50 @@
+import { Link } from "react-router-dom";
+import { ChangeEvent, KeyboardEvent, useState } from "react";
+import styles from "./NavBar.module.css";
+
+function NavBar({
+ handleOrder,
+ handleKeyword,
+}: {
+ handleOrder: (value: string) => void;
+ handleKeyword: (value: string) => void;
+}) {
+ const [searchInput, setSearchInput] = useState("");
+
+ const onClickOption = (e: ChangeEvent) => {
+ handleOrder(e.target.value);
+ };
+
+ const handleChangeInput = (e: ChangeEvent) => {
+ setSearchInput(e.target.value);
+ };
+
+ //엔터키 입력시 상품 검색
+ const onKeyDownEnter = (e: KeyboardEvent) => {
+ if (e.key === "Enter") {
+ handleKeyword(searchInput);
+ }
+ };
+
+ return (
+
+ );
+}
+
+export default NavBar;
diff --git a/src/component/Pagination/PageButton.js b/src/component/Pagination/PageButton.js
deleted file mode 100644
index efe5087fa..000000000
--- a/src/component/Pagination/PageButton.js
+++ /dev/null
@@ -1,60 +0,0 @@
-import LeftArrow from "../../asset/arrow_left.png";
-import RightArrow from "../../asset/arrow_right.png";
-import "./PageButton.css";
-
-function PageButton({ handlePage, page, maxPage }) {
- const ClickNumber = (e) => {
- handlePage(e.target.value);
- };
-
- const ClickIconLeft = () => {
- if (page > 1) {
- handlePage(page - 1);
- }
- };
-
- const ClickIconRight = () => {
- if (page < maxPage) {
- handlePage(page + 1);
- }
- };
-
- let pageIine;
- pageIine = Math.floor((page - 1) / 5);
- let PageGroup = [];
- if (pageIine === Math.floor(maxPage / 5)) {
- for (let i = 1; i <= maxPage % 5; i++) {
- PageGroup.push(i + 5 * pageIine);
- }
- } else {
- for (let i = 1; i <= 5; i++) {
- PageGroup.push(i + 5 * pageIine);
- }
- }
-
- function PageButtonNumber({ value }) {
- const className =
- page === value ? "pagebutton-number active" : "pagebutton-number";
- return (
-
- {value}
-
- );
- }
-
- return (
-
- -
-
-
- {PageGroup.map((value) => (
-
- ))}
- -
-
-
-
- );
-}
-
-export default PageButton;
diff --git a/src/component/Pagination/PageButton.css b/src/component/Pagination/PageButton.module.css
similarity index 78%
rename from src/component/Pagination/PageButton.css
rename to src/component/Pagination/PageButton.module.css
index 5bd5d72e6..0c79669f3 100644
--- a/src/component/Pagination/PageButton.css
+++ b/src/component/Pagination/PageButton.module.css
@@ -1,14 +1,14 @@
-.pagebutton-container {
+.pagebutton_container {
display: flex;
justify-content: center;
gap: 4px;
}
-.pagebutton-icon {
+.pagebutton_icon {
height: 16px;
}
-.pagebutton-number {
+.pagebutton_number {
width: 40px;
height: 40px;
border-radius: 40px;
@@ -21,7 +21,7 @@
cursor: pointer;
}
-.pagebutton-number.active {
+.pagebutton_number .active {
background-color: #2f80ed;
color: white;
}
diff --git a/src/component/Pagination/PageButton.tsx b/src/component/Pagination/PageButton.tsx
new file mode 100644
index 000000000..d3f95cbde
--- /dev/null
+++ b/src/component/Pagination/PageButton.tsx
@@ -0,0 +1,89 @@
+import LeftArrow from "../../asset/arrow_left.png";
+import RightArrow from "../../asset/arrow_right.png";
+import styles from "./PageButton.module.css";
+
+function PageButton({
+ handlePage,
+ page,
+ maxPage,
+}: {
+ handlePage: (value: number) => void;
+ page: number;
+ maxPage: number;
+}) {
+ const ClickNumber = (e: React.MouseEvent) => {
+ handlePage(Number(e.currentTarget.dataset.value));
+ };
+
+ const ClickIconLeft = () => {
+ if (page > 1) {
+ handlePage(page - 1);
+ }
+ };
+
+ const ClickIconRight = () => {
+ if (page < maxPage) {
+ handlePage(page + 1);
+ }
+ };
+
+ let pageIine;
+ pageIine = Math.floor((page - 1) / 5);
+ let PageGroup = [];
+ if (pageIine === Math.floor(maxPage / 5)) {
+ for (let i = 1; i <= maxPage % 5; i++) {
+ PageGroup.push(i + 5 * pageIine);
+ }
+ } else {
+ for (let i = 1; i <= 5; i++) {
+ PageGroup.push(i + 5 * pageIine);
+ }
+ }
+
+ let background = "";
+ let color = "";
+ function PageButtonNumber({ value }: { value: number }) {
+ if (page === value) {
+ background = "#2f80ed";
+ color = "white";
+ } else {
+ background = "";
+ color = "";
+ }
+
+ return (
+
+ {value}
+
+ );
+ }
+
+ return (
+
+ -
+
+
+ {PageGroup.map((value) => (
+
+ ))}
+ -
+
+
+
+ );
+}
+
+export default PageButton;
diff --git a/src/component/ProductComment/ProductComment.module.css b/src/component/ProductComment/ProductComment.module.css
new file mode 100644
index 000000000..c09fe8544
--- /dev/null
+++ b/src/component/ProductComment/ProductComment.module.css
@@ -0,0 +1,56 @@
+.comment_container {
+ display: flex;
+ gap: 24px;
+ flex-direction: column;
+ position: relative;
+ height: 100px;
+ border-bottom: 1px solid #e5e7eb;
+}
+
+.comment_content {
+ font-size: 14px;
+ font-weight: 400;
+ line-height: 24px;
+ color: #1f2937;
+}
+
+.user_wrap {
+ display: flex;
+ /* align-items: center; */
+ gap: 8px;
+}
+
+.user_img {
+ width: 32px;
+ height: 32px;
+ object-fit: cover;
+}
+
+.user_name_wrap {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.user_name {
+ font-size: 12px;
+ font-weight: 400;
+ line-height: 18px;
+ color: #4b5563;
+}
+
+.user_time {
+ font-size: 12px;
+ font-weight: 400;
+ line-height: 18px;
+ color: #9ca3af;
+}
+
+.kebabicon {
+ width: 24px;
+ height: 24px;
+ object-fit: cover;
+ position: absolute;
+ top: 0;
+ right: 0;
+}
diff --git a/src/component/ProductComment/ProductComment.tsx b/src/component/ProductComment/ProductComment.tsx
new file mode 100644
index 000000000..8e2d019c0
--- /dev/null
+++ b/src/component/ProductComment/ProductComment.tsx
@@ -0,0 +1,45 @@
+import userIcon from "../../asset/userIcon.png";
+import kebabIcon from "../../asset/ic_kebab.png";
+import styles from "./ProductComment.module.css";
+
+interface Value {
+ writer: {
+ image: string;
+ nickname: string;
+ id: number;
+ };
+ updatedAt: string;
+ createdAt: string;
+ content: string;
+ id: number;
+}
+
+function ProductComment({ id, value }: { id: number; value: Value }) {
+ //마지막 업데이트 시간 계산
+
+ // date1과 date2 문자열을 Date 객체로 변환합니다.
+ const startDate = new Date(value.updatedAt);
+ const endDate = new Date();
+ // 두 날짜 간의 시간 차이를 밀리초 단위로 계산합니다.
+ const diffTime = endDate.getTime() - startDate.getTime();
+ // 밀리초를 일 단위로 변환합니다.
+ const diffDays = diffTime / (1000 * 60 * 60 * 24);
+ // 일수 차이를 반환합니다.
+ console.log(Math.floor(diffDays));
+
+ return (
+
+
{value.content}
+
+

+
+
{value.writer.nickname}
+
{Math.floor(diffDays)}일 전
+
+
+

+
+ );
+}
+
+export default ProductComment;
diff --git a/src/component/ProductTag.js b/src/component/ProductTag.js
deleted file mode 100644
index 57a417d70..000000000
--- a/src/component/ProductTag.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import iconX from "../asset/ic_X.png";
-import styles from "./ProductTag.module.css";
-
-function ProductTag({ value, handlechangeTagList }) {
- const handleClickTag = () => {
- handlechangeTagList(value);
- };
- return (
-
-
#{value}
-

-
- );
-}
-
-export default ProductTag;
diff --git a/src/component/ProductTag.tsx b/src/component/ProductTag.tsx
new file mode 100644
index 000000000..7e5050a18
--- /dev/null
+++ b/src/component/ProductTag.tsx
@@ -0,0 +1,35 @@
+import iconX from "../asset/ic_X.png";
+import styles from "./ProductTag.module.css";
+
+function ProductTag({
+ value,
+ handlechangeTagList,
+ disable = true,
+}: {
+ value: string;
+ handlechangeTagList?: (id: string) => void;
+ disable?: boolean;
+}) {
+ const handleClickTag = () => {
+ if (handlechangeTagList) {
+ handlechangeTagList(value);
+ }
+ };
+ return (
+
+
#{value}
+ {disable ? (
+

+ ) : (
+ ""
+ )}
+
+ );
+}
+
+export default ProductTag;
diff --git a/src/global.d.ts b/src/global.d.ts
new file mode 100644
index 000000000..31ba36a60
--- /dev/null
+++ b/src/global.d.ts
@@ -0,0 +1,10 @@
+declare module "*.module.css" {
+ // module.css 로 끝나는 모든 파일을 module 로 인식
+ const content: { [key: string]: string }; // 해당 모듈파일의 타입 객체 생성
+ export = content; // 해당 타입 객체를 전역에 export
+}
+
+declare module "*.jpg";
+declare module "*.png";
+declare module "*.jpeg";
+declare module "*.gif";
diff --git a/src/pages/MainPage.js b/src/pages/MainPage.tsx
similarity index 100%
rename from src/pages/MainPage.js
rename to src/pages/MainPage.tsx
diff --git a/src/pages/PostProductPage.js b/src/pages/PostProductPage.tsx
similarity index 88%
rename from src/pages/PostProductPage.js
rename to src/pages/PostProductPage.tsx
index 85a14cd22..d0aca2cb8 100644
--- a/src/pages/PostProductPage.js
+++ b/src/pages/PostProductPage.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useRef, useState } from "react";
+import { ChangeEvent, KeyboardEvent, useEffect, useRef, useState } from "react";
import styles from "./PostProductPage.module.css";
import ProductTag from "../component/ProductTag";
import iconX from "../asset/ic_X.png";
@@ -6,25 +6,25 @@ import iconX from "../asset/ic_X.png";
function PostProductPage() {
const [preview, setPreview] = useState("");
const [imgError, setImgError] = useState(false);
- const [tagList, setTagList] = useState([]);
+ const [tagList, setTagList] = useState([]);
//value값들
const [nameInput, setnameInput] = useState("");
const [desInput, setDesInput] = useState("");
const [priceInput, setPriceInput] = useState("");
const [tagValue, setTagValue] = useState("");
- const inputRef = useRef();
+ const inputRef = useRef(null);
- const handleChangeNameInput = (e) => {
+ const handleChangeNameInput = (e: ChangeEvent) => {
setnameInput(e.target.value);
};
- const handleChangeDesInput = (e) => {
+ const handleChangeDesInput = (e: ChangeEvent) => {
setDesInput(e.target.value);
};
//가격 input 함수
- const handleChangePriceInput = (e) => {
+ const handleChangePriceInput = (e: ChangeEvent) => {
let number = e.target.value;
//숫자 이외에 입력은 공백으로 바꿔줌
number = number.replace(/[^0-9]/g, "");
@@ -40,7 +40,10 @@ function PostProductPage() {
: `${styles.post_button}`;
//파일 input 함수
- const handleChangeFile = (e) => {
+ const handleChangeFile = (e: ChangeEvent) => {
+ if (!e.target.files) {
+ return;
+ }
if (preview) {
setImgError(true);
return;
@@ -63,7 +66,7 @@ function PostProductPage() {
};
//태그 input 함수
- const handleChangeTag = (e) => {
+ const handleChangeTag = (e: KeyboardEvent) => {
const isValue = tagList.find((element) => element === tagValue);
if (e.keyCode === 13 && isValue) {
alert("이미 있는 태그입니다.");
@@ -74,18 +77,18 @@ function PostProductPage() {
}
};
- const handleChangeTagInput = (e) => {
+ const handleChangeTagInput = (e: ChangeEvent) => {
setTagValue(e.target.value);
};
- const handlechangeTagList = (id) => {
+ const handlechangeTagList = (id: string) => {
const nextTagList = tagList.filter((element) => element !== id);
setTagList(nextTagList);
};
//폼 제출기능 막기
- const preventSubmit = (e) => {
- // e.preventDefault();
+ const preventSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
};
useEffect(() => {
diff --git a/src/pages/ProductDetail.module.css b/src/pages/ProductDetail.module.css
new file mode 100644
index 000000000..8bb5acab0
--- /dev/null
+++ b/src/pages/ProductDetail.module.css
@@ -0,0 +1,247 @@
+.detail_section {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 1300px;
+}
+
+.detail_wrap {
+ max-width: 1200px;
+}
+
+/* 상품 영역 */
+.product_container {
+ display: flex;
+ gap: 24px;
+ align-items: center;
+}
+
+.product_img {
+ width: 486px;
+ height: 486px;
+ object-fit: cover;
+ border-radius: 16px;
+}
+
+.product_title_wrap,
+.product_des_wrap,
+.product_tag_wrap {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.product_title {
+ font-size: 24px;
+ font-weight: 600;
+ color: #1f2937;
+ line-height: 32px;
+}
+
+.product_price {
+ font-size: 40px;
+ font-weight: 600;
+ color: #1f2937;
+ line-height: 47px;
+}
+
+.product_line {
+ height: 1px;
+ background-color: #e5e7eb;
+}
+
+.product_description_title,
+.product_tag_title {
+ font-size: 16px;
+ font-weight: 600;
+ color: #4b5563;
+ line-height: 26px;
+}
+
+.product_tag_list {
+ display: flex;
+ gap: 8px;
+}
+
+.product_description {
+ font-size: 16px;
+ font-weight: 400;
+ color: #4b5563;
+ line-height: 26px;
+ flex-grow: 1;
+}
+
+.product_detail_section {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+}
+
+.product_wrap {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ width: 690px;
+ height: 496px;
+}
+
+.product_user_wrap {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+}
+
+.user_img {
+ object-fit: cover;
+ width: 40px;
+ height: 40px;
+}
+
+.user_name {
+ font-weight: 500;
+ font-size: 14px;
+ line-height: 24px;
+ color: #4b5563;
+}
+
+.user_date {
+ font-weight: 400;
+ font-size: 14px;
+ line-height: 24px;
+ color: #9ca3af;
+}
+
+/* 폼 영역 */
+
+.comment_input_container {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.comment_input_wrap {
+ display: flex;
+ flex-direction: column;
+ gap: 9px;
+}
+
+.comment_input_title {
+ font-weight: 600;
+ font-size: 16px;
+ line-height: 26px;
+ color: #111827;
+}
+
+.comment_input {
+ max-width: 1200px;
+ height: 104px;
+ background-color: #f3f4f6;
+ padding: 16px 24px;
+ border-radius: 12px;
+ border: none;
+ resize: none;
+ font-weight: 400;
+ font-size: 16px;
+ line-height: 26px;
+ font-family: "Pretendard Variable", Pretendard;
+}
+
+.comment_input::placeholder {
+ font-weight: 400;
+ font-size: 16px;
+ line-height: 26px;
+ color: #9ca3af;
+ font-family: "Pretendard Variable", Pretendard;
+}
+
+.comment_button {
+ width: 72px;
+ height: 42px;
+ background-color: #9ca3af;
+ border-radius: 8px;
+ color: white;
+ border: none;
+ margin-left: auto;
+}
+
+.comment_button_hover {
+ width: 72px;
+ height: 42px;
+ background-color: #3692ff;
+ border-radius: 8px;
+ color: white;
+ border: none;
+ margin-left: auto;
+}
+
+.detail_line {
+ height: 1px;
+ max-width: 1190px;
+ background-color: #e5e7eb;
+ margin: 40px 0;
+}
+
+.comment_list_container {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+ margin-top: 24px;
+}
+
+.heart {
+ width: 32px;
+ height: 32px;
+ object-fit: cover;
+}
+
+.button_container {
+ display: flex;
+ justify-content: center;
+ margin-top: 64px;
+}
+
+.button_to_list {
+ padding: 12px 64px;
+ border-radius: 40px;
+ border: none;
+ background-color: #3692ff;
+ color: white;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-weight: 400;
+ font-size: 18px;
+ line-height: 26px;
+ cursor: pointer;
+}
+
+.button_to_list:hover {
+ background-color: #8bbfff;
+}
+
+.button_backicon {
+ width: 24px;
+ height: 24px;
+ object-fit: cover;
+}
+
+.comment_no_wrap {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ gap: 8px;
+}
+
+.comment_no_img {
+ width: 196px;
+ height: 196px;
+ object-fit: cover;
+}
+
+.comment_no_des {
+ font-weight: 400px;
+ font-size: 14px;
+ line-height: 26px;
+ color: #9ca3af;
+}
diff --git a/src/pages/ProductDetail.tsx b/src/pages/ProductDetail.tsx
new file mode 100644
index 000000000..b3422cd8b
--- /dev/null
+++ b/src/pages/ProductDetail.tsx
@@ -0,0 +1,207 @@
+import { Link, useParams } from "react-router-dom";
+import styles from "./ProductDetail.module.css";
+import UserImg from "../asset/userIcon.png";
+import ProductTag from "../component/ProductTag";
+import ProductComment from "../component/ProductComment/ProductComment";
+import heartIcon from "../asset/ic_heart.png";
+import { SyntheticEvent, useEffect, useState } from "react";
+import { getProductComment, getProductDetail } from "../api";
+import noPhotoImg from "../asset/nophoto.png";
+import backIcon from "../asset/ic_back.png";
+import noCommentImg from "../asset/Img_inquiry_empty.png";
+
+interface Product {
+ id: number;
+ name: string;
+ price: number;
+ description: string;
+ tags: string[];
+ images: string[];
+ createdAt: string;
+ ownerNickname: string;
+ favoriteCount: number;
+}
+
+interface Comment {
+ writer: {
+ image: string;
+ nickname: string;
+ id: number;
+ };
+ updatedAt: string;
+ createdAt: string;
+ content: string;
+ id: number;
+}
+
+function ProductDetail() {
+ const [item, setItem] = useState();
+ const [comment, setComment] = useState([]);
+ const [input, setInput] = useState("");
+ const { productSlug } = useParams();
+
+ const handleChangeInput = (e: React.ChangeEvent) => {
+ setInput(e.target.value);
+ };
+
+ const handleLoad = async () => {
+ if (!productSlug) {
+ console.error("productSlug키 없음");
+ return;
+ }
+ try {
+ const result = await getProductDetail(productSlug);
+ const { list } = await getProductComment({ productSlug, limit: 3 });
+ if (!result) {
+ console.log("제품 정보를 찾을수 없음");
+ return;
+ }
+ setItem(result);
+ setComment(list || []);
+ } catch (error) {
+ console.log("데이터를 로드하는 중 오류 발생");
+ }
+ };
+
+ //이미지 링크가 잘못된 링크일 때, 기본 이미지 출력
+ const onErrorImg = (e: SyntheticEvent) => {
+ e.currentTarget.src = noPhotoImg;
+ };
+
+ //날짜
+ let date = new Date();
+ if (item) {
+ date = new Date(item.createdAt);
+ }
+
+ //등록 버튼 hover
+ let ButtonStyle = input
+ ? `${styles.comment_button_hover}`
+ : `${styles.comment_button}`;
+
+ useEffect(() => {
+ handleLoad();
+ }, []);
+ console.log(comment);
+
+ return (
+
+ {item ? (
+
+
+ {item.images[0] ? (
+

+ ) : (
+

+ )}
+
+
+
+
{item.name}
+
+ {item.price.toLocaleString()}
+
+
+
+
+
+ 상품 소개
+
+
+ {item.description}
+
+
+
+
상품 태그
+
+ {item.tags.map((value) => (
+
+ ))}
+
+
+
+
+

+
+
{item.ownerNickname}
+
+ {date.toLocaleDateString()}
+
+
+
+

+
+ {item.favoriteCount}
+
+
+
+
+
+
+
+
+ {!comment.length && (
+
+

+
+ 아직 문의가 없어요
+
+
+ )}
+ {comment.map((value) => (
+
+ ))}
+
+
+
+
+
+
+
+ ) : (
+ "로딩중..."
+ )}
+
+ );
+}
+
+export default ProductDetail;
diff --git a/src/pages/UsedMarketPage.js b/src/pages/UsedMarketPage.js
deleted file mode 100644
index 5a0a78e0e..000000000
--- a/src/pages/UsedMarketPage.js
+++ /dev/null
@@ -1,5 +0,0 @@
-function UsedMarketPage() {
- return <>>;
-}
-
-export default UsedMarketPage;
diff --git a/src/styles/global.css b/src/styles/global.css
new file mode 100644
index 000000000..6af651fe5
--- /dev/null
+++ b/src/styles/global.css
@@ -0,0 +1,13 @@
+@import url("https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable.min.css");
+
+* {
+ box-sizing: border-box;
+ margin: 0;
+}
+
+body {
+ font-family: "Pretendard Variable", Pretendard, -apple-system,
+ BlinkMacSystemFont, system-ui, Roboto, "Helvetica Neue", "Segoe UI",
+ "Apple SD Gothic Neo", "Noto Sans KR", "Malgun Gothic", "Apple Color Emoji",
+ "Segoe UI Emoji", "Segoe UI Symbol", sans-serif;
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 000000000..c9ae2f92f
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,112 @@
+{
+ "compilerOptions": {
+ /* Visit https://aka.ms/tsconfig to read more about this file */
+
+ /* Projects */
+ // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
+ // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
+ // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
+ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
+ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
+ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
+
+ /* Language and Environment */
+ "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
+ // "lib": [],
+ "jsx": "react-jsx" /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
+ // "jsx": "preserve", /* Specify what JSX code is generated. */
+ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
+ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
+ // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
+ // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
+ // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
+ // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
+ // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
+ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
+ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
+
+ /* Modules */
+ "module": "commonjs" /* Specify what module code is generated. */,
+ // "rootDir": "./", /* Specify the root folder within your source files. */
+ // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
+ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
+ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
+ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
+ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
+ // "types": [], /* Specify type package names to be included without being referenced in a source file. */
+ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
+ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
+ // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
+ // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
+ // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
+ // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
+ // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
+ // "noUncheckedSideEffectImports": true, /* Check side effect imports. */
+ // "resolveJsonModule": true, /* Enable importing .json files. */
+ // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
+ // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */
+
+ /* JavaScript Support */
+ // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
+ // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
+ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
+
+ /* Emit */
+ // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
+ // "declarationMap": true, /* Create sourcemaps for d.ts files. */
+ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
+ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
+ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
+ // "noEmit": true, /* Disable emitting files from a compilation. */
+ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
+ // "outDir": "./", /* Specify an output folder for all emitted files. */
+ // "removeComments": true, /* Disable emitting comments. */
+ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
+ // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
+ // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
+ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
+ // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
+ // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
+ // "newLine": "crlf", /* Set the newline character for emitting files. */
+ // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
+ // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
+ // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
+ // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
+ // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
+
+ /* Interop Constraints */
+ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
+ // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
+ // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
+ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
+ "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
+ // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
+ "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
+
+ /* Type Checking */
+ "strict": true /* Enable all strict type-checking options. */,
+ // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
+ // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
+ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
+ // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
+ // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
+ // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
+ // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
+ // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
+ // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
+ // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
+ // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
+ // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
+ // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
+ // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
+ // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
+ // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
+ // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
+ // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
+ // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
+
+ /* Completeness */
+ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
+ "skipLibCheck": true /* Skip type checking all .d.ts files. */
+ }
+}