diff --git a/custom.d.ts b/custom.d.ts new file mode 100644 index 000000000..354e21e71 --- /dev/null +++ b/custom.d.ts @@ -0,0 +1,4 @@ +declare module "*.png"; +declare module "*.jpg"; +declare module "*.jpeg"; +declare module "*.svg"; diff --git a/package-lock.json b/package-lock.json index 3d6312751..047e57870 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.7", + "@types/react": "^19.0.7", + "@types/react-dom": "^19.0.3", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^7.0.1", "react-scripts": "5.0.1", + "typescript": "^5.7.3", "web-vitals": "^2.1.4" } }, @@ -3775,6 +3780,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", @@ -4052,9 +4077,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" @@ -4303,9 +4329,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.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz", + "integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } }, "node_modules/@types/parse-json": { "version": "4.0.0", @@ -4318,9 +4348,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", @@ -4338,21 +4370,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.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.7.tgz", + "integrity": "sha512-MoFsEJKkAtZCrC1r6CM8U22GzhG7u2Wir8ons/aCKH6MBdD1ibV24zOSSkdZVUKqN5i396zG5VKLYZ3yaUZdLA==", + "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.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.3.tgz", + "integrity": "sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" } }, "node_modules/@types/resolve": { @@ -4368,11 +4400,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", @@ -16686,16 +16713,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.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/unbox-primitive": { @@ -16717,6 +16744,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 30d5af03c..46f17c789 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.7", + "@types/react": "^19.0.7", + "@types/react-dom": "^19.0.3", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^7.0.1", "react-scripts": "5.0.1", + "typescript": "^5.7.3", "web-vitals": "^2.1.4" }, "scripts": { diff --git a/src/api.js b/src/api.js deleted file mode 100644 index a3cb0b55b..000000000 --- a/src/api.js +++ /dev/null @@ -1,47 +0,0 @@ -export async function getData({ - page = 1, - pageSize = 4, - orderBy = "recent", -} = {}) { - const response = await fetch( - `https://panda-market-api.vercel.app/products?page=${page}&pageSize=${pageSize}&orderBy=${orderBy}` - ); - const body = await response.json(); - return body; -} - -export async function getDataById(productId) { - const response = await fetch( - `https://panda-market-api.vercel.app/products/${productId}` - ); - const body = await response.json(); - return body; -} - -export async function getUserData({ productId, limit }) { - const response = await fetch( - `https://panda-market-api.vercel.app/products/${productId}/comments?limit=${limit}` - ); - const body = await response.json(); - return body; -} - -export async function postCommentData({ productId, editContent }) { - const token = - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTksInNjb3BlIjoiYWNjZXNzIiwiaWF0IjoxNzM3Mjc3NTIzLCJleHAiOjE3MzcyNzkzMjMsImlzcyI6InNwLXBhbmRhLW1hcmtldCJ9.PE7HgmQtdB0J1kQoYk4VieZfBs0CZFwedo2ttRBAHWY"; - - const response = await fetch( - `https://panda-market-api.vercel.app/products/${productId}/comments`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify({ - content: editContent, - }), - } - ); - return response; -} diff --git a/src/api.tsx b/src/api.tsx new file mode 100644 index 000000000..e662cffa5 --- /dev/null +++ b/src/api.tsx @@ -0,0 +1,114 @@ +export type OrderByType = "recent" | "favorite"; + +export interface Product { + createdAt: string; + favoriteCount: number; + ownerNickname: string; + ownerId: number; + images: string[]; + tags: string[]; + price: number; + description: string; + name: string; + id: number; +} + +interface ProductById extends Product { + updatedAt: string; + isFavorite: boolean; +} + +interface GetDataParams { + page?: number; + pageSize?: number; + orderBy?: OrderByType; +} + +export interface ProductResponse { + list: Product[]; + totalCount: number; +} + +interface Writer { + id: number; + nickname: string; + image: string | null; +} + +interface Comment { + id: number; + content: string; + createdAt: string; + updatedAt: string; + writer: Writer; +} + +interface CommentListResponse { + list: Comment[]; + nextCursor?: number; +} + +interface GetCommentDataParams { + productId?: string; + limit: number; + cursor?: number; +} + +interface PostCommentDataParams { + productId?: string; + editContent: string; +} + +export async function getData({ + page = 1, + pageSize = 4, + orderBy = "recent", +}: GetDataParams = {}): Promise { + const response = await fetch( + `https://panda-market-api.vercel.app/products?page=${page}&pageSize=${pageSize}&orderBy=${orderBy}` + ); + const body = await response.json(); + return body; +} + +export async function getDataById(productId?: string): Promise { + const response = await fetch( + `https://panda-market-api.vercel.app/products/${productId}` + ); + const body = await response.json(); + return body; +} + +export async function getCommentData({ + productId, + limit, +}: GetCommentDataParams): Promise { + const response = await fetch( + `https://panda-market-api.vercel.app/products/${productId}/comments?limit=${limit}` + ); + const body = await response.json(); + return body; +} + +export async function postCommentData({ + productId, + editContent, +}: PostCommentDataParams): Promise { + const token = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTksInNjb3BlIjoiYWNjZXNzIiwiaWF0IjoxNzM3Mjc3NTIzLCJleHAiOjE3MzcyNzkzMjMsImlzcyI6InNwLXBhbmRhLW1hcmtldCJ9.PE7HgmQtdB0J1kQoYk4VieZfBs0CZFwedo2ttRBAHWY"; + + const response = await fetch( + `https://panda-market-api.vercel.app/products/${productId}/comments`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + content: editContent, + }), + } + ); + return response; +} diff --git a/src/components/AllProducts/AllProducts.jsx b/src/components/AllProducts/AllProducts.tsx similarity index 65% rename from src/components/AllProducts/AllProducts.jsx rename to src/components/AllProducts/AllProducts.tsx index 29b8067a3..10ffe802c 100644 --- a/src/components/AllProducts/AllProducts.jsx +++ b/src/components/AllProducts/AllProducts.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from "react"; -import { getData } from "../../api"; +import { getData, OrderByType, Product } from "../../api"; import ProductItem from "../common/product"; import SearchForm from "../SearchForm/SearchForm"; import Pagination from "../Pagination/Pagination"; @@ -8,11 +8,11 @@ import useDevice from "../../hooks/useDevice"; import { Link } from "react-router-dom"; function AllProducts() { - const [allItemList, setAllItemList] = useState([]); - const [selectedOption, setSelectedOption] = useState("recent"); - const [currentPage, setCurrentPage] = useState(1); - const [totalPages, setTotalPages] = useState(0); - const [size, setSize] = useState(282); + const [allItemList, setAllItemList] = useState([]); + const [selectedOption, setSelectedOption] = useState("recent"); + const [currentPage, setCurrentPage] = useState(1); + const [totalPages, setTotalPages] = useState(0); + const [size, setSize] = useState(282); const { mode } = useDevice(); @@ -41,7 +41,7 @@ function AllProducts() { fetchAllItems(); }, [currentPage, selectedOption, mode]); - const handlePageChange = (pageNumber) => { + const handlePageChange = (pageNumber: number) => { if (pageNumber < 1 || pageNumber > totalPages) return; setCurrentPage(pageNumber); }; @@ -56,18 +56,20 @@ function AllProducts() { />
    - {allItemList.map((item) => ( -
  • - -
  • - ))} + {allItemList.map( + ({ id, images, name, price, favoriteCount }: Product) => ( +
  • + +
  • + ) + )}
([]); + const [currentPage, setCurrentPage] = useState(1); - const [size, setSize] = useState(282); + const [size, setSize] = useState(282); const { mode } = useDevice(); diff --git a/src/components/Pagination/Pagination.jsx b/src/components/Pagination/Pagination.tsx similarity index 86% rename from src/components/Pagination/Pagination.jsx rename to src/components/Pagination/Pagination.tsx index 61f0b48c8..b5a1e50fa 100644 --- a/src/components/Pagination/Pagination.jsx +++ b/src/components/Pagination/Pagination.tsx @@ -1,6 +1,16 @@ import "./Pagination.css"; -function Pagination({ currentPage, totalPages, onPageChange }) { +interface PaginationProps { + currentPage: number; + totalPages: number; + onPageChange: Function; +} + +function Pagination({ + currentPage, + totalPages, + onPageChange, +}: PaginationProps) { const getPageNumbers = () => { let pages = []; const maxPages = 5; diff --git a/src/components/SearchForm/SearchForm.jsx b/src/components/SearchForm/SearchForm.tsx similarity index 74% rename from src/components/SearchForm/SearchForm.jsx rename to src/components/SearchForm/SearchForm.tsx index 58dc42114..76633c1be 100644 --- a/src/components/SearchForm/SearchForm.jsx +++ b/src/components/SearchForm/SearchForm.tsx @@ -2,8 +2,14 @@ import { Link } from "react-router-dom"; import searchImage from "../../assets/images/search.png"; import "./SearchForm.css"; import useDevice from "../../hooks/useDevice"; +import { OrderByType } from "../../api"; -function SearchForm({ selectedOption, setSelectedOption }) { +interface SearchFormProps { + selectedOption: OrderByType; + setSelectedOption: (option: OrderByType) => void; +} + +function SearchForm({ selectedOption, setSelectedOption }: SearchFormProps) { const { mode } = useDevice(); return (
@@ -22,7 +28,7 @@ function SearchForm({ selectedOption, setSelectedOption }) { setAskContent(e.target.value)} + onChange={(e: React.ChangeEvent) => + setAskContent(e.target.value) + } type="text" placeholder="개인정보를 공유 및 요청하거나, 명예 훼손, 무단 광고, 불법 정보 유포시 모니터링 후 삭제될 수 있으며, 이에 대한 민형사상 책임은 게시자에게 있습니다." className="ask-input" @@ -221,7 +247,9 @@ function SpecificProduct({ setEditContent(e.target.value)} + onChange={(e: React.ChangeEvent) => + setEditContent(e.target.value) + } className="edit-comment-input" />
@@ -291,6 +319,6 @@ function SpecificProduct({
); -} +}; export default SpecificProduct; diff --git a/src/components/common/product.jsx b/src/components/common/product.tsx similarity index 84% rename from src/components/common/product.jsx rename to src/components/common/product.tsx index 8fc97e4cb..cdf5e451b 100644 --- a/src/components/common/product.jsx +++ b/src/components/common/product.tsx @@ -3,7 +3,23 @@ import heartImage from "../../assets/images/heart.png"; import useDevice from "../../hooks/useDevice"; import { Link } from "react-router-dom"; -function ProductItem({ id, imageUrl, name, price, likeCount, size }) { +interface ProductItemData { + id: number; + imageUrl: string; + name: string; + price: number; + likeCount: number; + size: number; +} + +function ProductItem({ + id, + imageUrl, + name, + price, + likeCount, + size, +}: ProductItemData) { return (
{ + images: string[]; + favoriteCount: number; +} function ItemPage() { - const { productSlug } = useParams(); - const [item, setItem] = useState(); - const [size, setSize] = useState(486); + const { productSlug } = useParams<{ productSlug: string }>(); + const [item, setItem] = useState(); + const [size, setSize] = useState(486); - const fetchItems = async () => { + const fetchItems = async (): Promise => { try { const data = await getDataById(productSlug); setItem(data); diff --git a/src/pages/additem.jsx b/src/pages/additem.tsx similarity index 81% rename from src/pages/additem.jsx rename to src/pages/additem.tsx index cb2ca914c..dfbcfd1f5 100644 --- a/src/pages/additem.jsx +++ b/src/pages/additem.tsx @@ -1,21 +1,21 @@ -import { useEffect, useRef, useState } from "react"; +import { ChangeEvent, FormEvent, useEffect, useRef, useState } from "react"; import "./AddItem.css"; import registerImage from "../assets/images/registerImage.png"; import useDevice from "../hooks/useDevice"; function Additem() { - const [productName, setProductName] = useState(""); - const [productIntroduction, setProductIntroduction] = useState(""); - const [sellingPrice, setSellingPrice] = useState(""); - const [tagInput, setTagInput] = useState(""); - const [tags, setTags] = useState(["티셔츠", "상의"]); - const [preview, setPreview] = useState(); + const [productName, setProductName] = useState(""); + const [productIntroduction, setProductIntroduction] = useState(""); + const [sellingPrice, setSellingPrice] = useState(""); + const [tagInput, setTagInput] = useState(""); + const [tags, setTags] = useState(["티셔츠", "상의"]); + const [preview, setPreview] = useState(); const { mode } = useDevice(); - const fileInput = useRef(null); + const fileInput = useRef(null); - const handleSubmit = (e) => { + const handleSubmit = (e: FormEvent) => { e.preventDefault(); //페이지 새로고침 방지 console.log({ productName, @@ -26,11 +26,13 @@ function Additem() { }; const handleImageClick = () => { - fileInput.current.click(); + if (fileInput.current) { + fileInput.current.click(); + } }; - const handleProductImage = (e) => { - const file = e.target.files[0]; + const handleProductImage = (e: ChangeEvent) => { + const file = e.target.files?.[0]; if (file) { const imageUrl = URL.createObjectURL(file); setPreview(imageUrl); @@ -41,30 +43,30 @@ function Additem() { const inputNode = fileInput.current; if (!inputNode) return; inputNode.value = ""; - setPreview(null); + setPreview(undefined); }; - const handleTagClearClick = (tagToRemove) => { + const handleTagClearClick = (tagToRemove: string) => { setTags(tags.filter((tag) => tag !== tagToRemove)); }; - const handleProductName = (e) => { + const handleProductName = (e: ChangeEvent) => { setProductName(e.target.value); }; - const handleProductIntroduction = (e) => { + const handleProductIntroduction = (e: ChangeEvent) => { setProductIntroduction(e.target.value); }; - const handleSellingPrice = (e) => { + const handleSellingPrice = (e: ChangeEvent) => { setSellingPrice(e.target.value); }; - const handleTag = (e) => { + const handleTag = (e: ChangeEvent) => { setTagInput(e.target.value); }; - const isFormComplete = () => { + const isFormComplete = (): boolean => { return ( productName.trim() !== "" && productIntroduction.trim() !== "" && @@ -79,7 +81,6 @@ function Additem() {

상품 등록하기