diff --git a/.gitignore b/.gitignore index 4d29575d..282a8a32 100644 --- a/.gitignore +++ b/.gitignore @@ -1,23 +1,50 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. -# dependencies -/node_modules -/.pnp -.pnp.js +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride -# testing -/coverage +# Icon must end with two \r +Icon -# production -/build -# misc -.DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local - -npm-debug.log* -yarn-debug.log* -yarn-error.log* +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### react ### +.DS_* +*.log +logs +**/*.backup.* +**/*.back.* + +node_modules +bower_components + +*.sublime* + +psd +thumb +sketch + diff --git a/package-lock.json b/package-lock.json index f9a88a4a..825965ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,10 +11,16 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "@types/jest": "^29.5.13", + "@types/node": "^22.7.6", + "@types/react": "^18.3.11", + "@types/react-dom": "^18.3.1", + "axios": "^1.7.7", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.26.1", "react-scripts": "5.0.1", + "typescript": "^5.6.3", "web-vitals": "^2.1.4" } }, @@ -4055,9 +4061,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.13", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.13.tgz", + "integrity": "sha512-wd+MVEZCHt23V0/L642O5APvspWply/rGY5BcW4SUETo2UzPU3Z26qr8jC2qxpimI2jjx9h7+2cj2FwIr01bXg==", + "license": "MIT", "dependencies": { "expect": "^29.0.0", "pretty-format": "^29.0.0" @@ -4306,9 +4313,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.7.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.6.tgz", + "integrity": "sha512-/d7Rnj0/ExXDMcioS78/kf1lMzYk4BZV8MZGTBKzTGZ6/406ukkbYlIsZmMPhcR5KlkunDHQLrtAVmSq7r+mSw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } }, "node_modules/@types/parse-json": { "version": "4.0.0", @@ -4341,19 +4352,20 @@ "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": "18.3.11", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.11.tgz", + "integrity": "sha512-r6QZ069rFTjrEYgFdOck1gK7FLVsgJE7tTz0pQBczlBNUhBNk0MQH4UbnFSwjpQLMkLzgqvBBa+qGpLje16eTQ==", + "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==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", + "license": "MIT", "dependencies": { "@types/react": "*" } @@ -4371,11 +4383,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", @@ -5294,6 +5301,31 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/axobject-query": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", @@ -8303,15 +8335,16 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==" }, "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", "funding": [ { "type": "individual", "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -14384,6 +14417,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -16663,16 +16702,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.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/unbox-primitive": { @@ -16694,6 +16733,12 @@ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==" }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "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 2ba915e7..525d2073 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "@types/node": "^22.7.6", "@types/react": "^18.3.11", "@types/react-dom": "^18.3.1", + "axios": "^1.7.7", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.26.1", diff --git a/src/api/itemApi.tsx b/src/api/itemApi.tsx index e057f0e8..2307ed60 100644 --- a/src/api/itemApi.tsx +++ b/src/api/itemApi.tsx @@ -1,8 +1,8 @@ +import axios from 'axios'; + const BASE_URL = 'https://panda-market-api.vercel.app'; export async function getProducts(params = {}) { - - const query = new URLSearchParams(params).toString(); const allowedParams = ["orderBy", "pageSize", "page", "keyword"]; const invalidParams = Object.keys(params) .filter(key => !allowedParams.includes(key)); @@ -12,70 +12,49 @@ export async function getProducts(params = {}) { } try { - const response = await fetch( - `${BASE_URL}/products?${query}` - ); - if (!response.ok) { - throw new Error(`HTTP error: ${response.status}`); - } - const body = await response.json(); - return body; + const response = await axios.get(`${BASE_URL}/products`, { + params + }); + return response.data; } catch (error) { console.error("Failed to fetch products:", error); throw error; } } -export async function getProductDetail(productId) { +export async function getProductDetail(productId: number) { try { - const response = await fetch( - `${BASE_URL}/products/${productId}` - ); - if (!response.ok) { - throw new Error(`HTTP error: ${response.status}`); - } - - const body = await response.json(); - return body; + const response = await axios.get(`${BASE_URL}/products/${productId}`); + return response.data; } catch (error) { console.error("Failed to fetch product details:", error); throw error; } } -export async function postProductComment(productId, formData) { +export async function postProductComment(productId: number, formData: FormData) { try { - const response = await fetch( - `${BASE_URL}/products/${productId}/comments`, - { - method:'POST', - body:formData, - } - ); - if (!response.ok) { - throw new Error(`HTTP error: ${response.status}`); - } - const body = await response.json(); - return body; + const response = await axios.post(`${BASE_URL}/products/${productId}/comments`, formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }); + return response.data; } catch (error) { - console.error("Failed to fetch product comments:", error); + console.error("Failed to post product comment:", error); throw error; } } -export async function getProductComments(productId, params = {}) { - const { limit, cursor } = params; - const query = new URLSearchParams({ limit, cursor }).toString(); // limit과 cursor만 query로 사용 - +export async function getProductComments(productId: number, params = { limit: 10, cursor: 0 }) { try { - const response = await fetch( - `${BASE_URL}/products/${productId}/comments?${query}` - ); - if (!response.ok) { - throw new Error(`HTTP error: ${response.status}`); - } - const body = await response.json(); - return body; + const response = await axios.get(`${BASE_URL}/products/${productId}/comments`, { + params: { + limit: params.limit, + cursor: params.cursor + } + }); + return response.data; } catch (error) { console.error("Failed to fetch product comments:", error); throw error; diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 00000000..73de449e --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1,9 @@ +declare module '*.module.css' { + const classes: { [key: string]: string }; + export default classes; +} +declare module '*.png' { + const value: string; + export default value; +} + \ No newline at end of file diff --git a/src/pages/AddItem/AddItem.tsx b/src/pages/AddItem/AddItem.tsx index 144d7afe..ded0f94c 100644 --- a/src/pages/AddItem/AddItem.tsx +++ b/src/pages/AddItem/AddItem.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react' +import React, { useEffect, useState } from 'react'; import Header from '../../components/Header/Header'; import './AddItem.css'; import ProductImg from './components/ProductImg/ProductImg'; @@ -7,81 +7,100 @@ import ProductDescription from './components/ProductDescription/ProductDescripti import ProductPrice from './components/ProductPrice/ProductPrice'; import ProductTag from './components/ProductTag/ProductTag'; -function AddItem() { +export interface AddItemValues { + images: File | null; + tags: string[]; + price: string; + description: string; + name: string; +} + +interface ItemProps { + name: keyof AddItemValues; + value: string | globalThis.File | string[] | null; + onChange: (name: keyof AddItemValues, value: string | globalThis.File | string[] | null) => void; +} + +const AddItem: React.FC = () => { const [isValid, setIsValid] = useState(false); - const [values, setValues] = useState({ - imgFile:null, - name:'', - description:'', - price:'', - tags:[], - }) - - const isFormValid = (values) => { - return (values.name.trim() !== '' && - values.description.trim() !== '' && - values.price > 0 && - values.tags.length > 0); - } - const handleSubmit = (e) => { + const [values, setValues] = useState({ + images: null, + name: '', + description: '', + price: '', + tags: [], + }); + + const isFormValid = (values: AddItemValues) => { + return ( + values.name.trim() !== '' && + values.description.trim() !== '' && + parseFloat(values.price) > 0 && + values.tags.length > 0 + ); + }; + + const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); - if(!isFormValid(values)){ + if (!isFormValid(values)) { return; } console.log(values); - } - const handleChange = (name, value) => { - setValues((prevValues) => ({ - ...prevValues, - [name]: value, - })); + }; + + const handleChange = (name: keyof AddItemValues, value: string | File | string[] | null) => { + setValues((prevValues) => ({ + ...prevValues, + [name]: value, + })); }; useEffect(() => { setIsValid(isFormValid(values)); - },[values]); - + }, [values]); + return ( <>
-
-
상품 등록하기
- -
+
+
상품 등록하기
+ +
- - - - - +
- - ) -} + ); +}; export default AddItem; \ No newline at end of file diff --git a/src/pages/AddItem/components/ProductDescription/ProductDescription.tsx b/src/pages/AddItem/components/ProductDescription/ProductDescription.tsx index afe21d22..cbb6c965 100644 --- a/src/pages/AddItem/components/ProductDescription/ProductDescription.tsx +++ b/src/pages/AddItem/components/ProductDescription/ProductDescription.tsx @@ -1,8 +1,15 @@ import React from 'react'; import './ProductDescription.css'; +import { AddItemValues } from '../../AddItem'; -function ProductDescription({ name, value, onChange }) { - const handleChange = (e) => { +interface ProductDescriptionProps { + name: keyof AddItemValues; + value:string; + onChange : (name:keyof AddItemValues, value:string | null) => void; +} + +const ProductDescription:React.FC = ({ name, value, onChange }) => { + const handleChange = (e:React.ChangeEvent) => { onChange(name, e.target.value); }; return ( diff --git a/src/pages/AddItem/components/ProductImg/ProductImg.tsx b/src/pages/AddItem/components/ProductImg/ProductImg.tsx index 614828b1..86f5afe6 100644 --- a/src/pages/AddItem/components/ProductImg/ProductImg.tsx +++ b/src/pages/AddItem/components/ProductImg/ProductImg.tsx @@ -1,16 +1,22 @@ -import React from 'react' -import plusicon from '../../../../assets/images/plusicon.png' +import React, { useEffect, useState, useRef } from 'react'; +import plusicon from '../../../../assets/images/plusicon.png'; import './ProductImg.css'; -import { useEffect, useState, useRef } from 'react'; -import xicon from '../../../../assets/images/xicon.png' +import xicon from '../../../../assets/images/xicon.png'; +import { AddItemValues } from '../../AddItem'; -function ProductImg ({ name, value, onChange }) { - const [preview, setPreview] = useState(); - const [errorMessage, setErrorMessage] = useState(""); - const inputRef = useRef(); +interface ProductImgProps { + name: keyof AddItemValues; + value: globalThis.File | null; + onChange: (name: keyof AddItemValues, value: globalThis.File | null) => void; +} + +const ProductImg: React.FC = ({ name, value, onChange }) => { + const [preview, setPreview] = useState(null); + const [errorMessage, setErrorMessage] = useState(""); + const inputRef = useRef(null); - const handleChange = (e) => { - const file = e.target.files[0]; + const handleChange = (e: React.ChangeEvent) => { + const file: globalThis.File | null = e.target.files?.[0] || null; if (value) { setErrorMessage("*이미지 등록은 최대 1개까지 가능합니다."); return; @@ -18,57 +24,59 @@ function ProductImg ({ name, value, onChange }) { if (file) { onChange(name, file); - setErrorMessage(""); + setErrorMessage(""); + } else { + onChange(name, null); } - onChange(name, file); }; + const handleButtonClick = () => { - document.getElementById('add-button').click(); + inputRef.current?.click(); }; const handleClearClick = () => { const inputNode = inputRef.current; if (!inputNode) return; - + inputNode.value = ''; onChange(name, null); setErrorMessage(""); }; - - useEffect(() => { if (!value) return; const nextPreview = URL.createObjectURL(value); setPreview(nextPreview); }, [value]); - return (
- - +
- {value != null && 이미지 미리보기} - {value && } + {preview && 이미지 미리보기} + {value && ( + + )}
-
- {errorMessage && {errorMessage} } +
+ {errorMessage && {errorMessage}} - - ) -} + ); +}; export default ProductImg; \ No newline at end of file diff --git a/src/pages/AddItem/components/ProductName/ProductName.tsx b/src/pages/AddItem/components/ProductName/ProductName.tsx index 3fb62d21..83011650 100644 --- a/src/pages/AddItem/components/ProductName/ProductName.tsx +++ b/src/pages/AddItem/components/ProductName/ProductName.tsx @@ -1,8 +1,14 @@ import React from 'react'; import './ProductName.css'; +import { AddItemValues } from '../../AddItem'; +interface ProductName { + name: keyof AddItemValues; + value:string; + onChange:(name:keyof AddItemValues, value:string) => void; +} -function ProductName({ name, value, onChange }) { - const handleChange = (e) => { +const ProductName:React.FC = ({ name, value, onChange }) => { + const handleChange = (e:React.ChangeEvent) => { onChange(name, e.target.value); }; diff --git a/src/pages/AddItem/components/ProductPrice/ProductPrice.tsx b/src/pages/AddItem/components/ProductPrice/ProductPrice.tsx index 2806b8ec..ff9571aa 100644 --- a/src/pages/AddItem/components/ProductPrice/ProductPrice.tsx +++ b/src/pages/AddItem/components/ProductPrice/ProductPrice.tsx @@ -1,8 +1,15 @@ import React from 'react' import './ProductPrice.css'; +import { AddItemValues } from '../../AddItem'; -function ProductPrice({ name, value, onChange }) { - const handleChange = (e) => { +interface ProductPrice { + name: keyof AddItemValues; + value:string; + onChange:(name:keyof AddItemValues, value:string) => void; +} + +const ProductPrice:React.FC = ({ name, value, onChange }) => { + const handleChange = (e:React.ChangeEvent) => { onChange(name, e.target.value); }; diff --git a/src/pages/AddItem/components/ProductTag/ProductTag.tsx b/src/pages/AddItem/components/ProductTag/ProductTag.tsx index fa639dba..a4df5bf2 100644 --- a/src/pages/AddItem/components/ProductTag/ProductTag.tsx +++ b/src/pages/AddItem/components/ProductTag/ProductTag.tsx @@ -1,62 +1,70 @@ import React, { useState } from 'react'; import './ProductTag.css'; import xIcon from '../../../../assets/images/xicon.png'; +import { AddItemValues } from '../../AddItem'; -function ProductTag({ name, value, onChange }) { - const [inputValue, setInputValue] = useState(""); - const [tags, setTags] = useState([]); - - const addTag = (e) => { - if (e.key === "Enter") { - e.preventDefault(); - if (!inputValue.trim()) return; - - const updatedTags = [...tags, inputValue.trim()]; - setTags(updatedTags); - onChange(name, updatedTags); - setInputValue(""); - } - }; - - const removeTag = (tagIdx) => { - const updatedTags = tags.filter((tag, idx) => idx !== tagIdx); - setTags(updatedTags); - onChange(name, updatedTags); - }; - - const handleChange = (e) => { - setInputValue(e.target.value); - }; - - return ( -
- - -
- {tags.map((tag, idx) => ( -
-
- #{tag} - removeTag(idx)} - alt="태그 삭제" - /> -
-
- ))} + +interface ProductTagProps { + name: keyof AddItemValues; + value: string[]; + onChange: (name: keyof AddItemValues, value: string[]) => void; +} + +const ProductTag: React.FC = ({ name, value, onChange }) => { + const [inputValue, setInputValue] = useState(""); + const [tags, setTags] = useState(value); + + const addTag = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + if (!inputValue.trim()) return; + + const updatedTags = [...tags, inputValue.trim()]; + setTags(updatedTags); + onChange(name, updatedTags); + setInputValue(""); + } + }; + + const removeTag = (tagIdx: number) => { + const updatedTags = tags.filter((_, idx) => idx !== tagIdx); + setTags(updatedTags); + onChange(name, updatedTags); + }; + + const handleChange = (e: React.ChangeEvent) => { + setInputValue(e.target.value); + }; + + return ( +
+ + +
+ {tags.map((tag, idx) => ( +
+
+ #{tag} + removeTag(idx)} + alt="태그 삭제" + />
-
- ); +
+ ))} +
+
+ ); } -export default ProductTag; +export default ProductTag; \ No newline at end of file diff --git a/src/pages/Market/components/AllItem.tsx b/src/pages/Market/components/AllItem.tsx index 45c1526c..52cd0215 100644 --- a/src/pages/Market/components/AllItem.tsx +++ b/src/pages/Market/components/AllItem.tsx @@ -16,8 +16,26 @@ const getPageSize = () => { } }; +interface Item { + id: number; + name: string; + description: string; + price: number; + tags: string[]; + images: string[]; + ownerId: number; + ownerNickname: string; + favoriteCount: number; + createdAt: string; +} + +interface ItemList { + totalCount: number; + list: Item[]; +} + function AllItem() { - const [itemList, setItemList] = useState([]); + const [itemList, setItemList] = useState({ totalCount: 0, list: [] }); const [pageSize, setPageSize] = useState(getPageSize()); const [order, setOrder] = useState('recent'); const [isDropdownOpen, setIsDropdownOpen] = useState(false); @@ -33,16 +51,25 @@ function AllItem() { }; const fetchSortedData = useCallback(async () => { - const products = await getProducts({ orderBy: order, pageSize }); - setItemList(products.list); + try { + const products = await getProducts({ orderBy: order, pageSize }); + setItemList(products); + } catch (error) { + console.error('Error fetching products:', error); + } }, [order, pageSize]); + useEffect(() => { + fetchSortedData(); + }, [fetchSortedData]); + useEffect(() => { const handleResize = () => { setPageSize(getPageSize()); + fetchSortedData(); }; + window.addEventListener('resize', handleResize); - fetchSortedData(); return () => { window.removeEventListener('resize', handleResize); }; @@ -75,7 +102,7 @@ function AllItem() {
- {itemList?.map((item) => ( + {itemList.list.map((item) => ( ))}
diff --git a/src/pages/Market/components/BestItem.tsx b/src/pages/Market/components/BestItem.tsx index 70f6a073..e1d946ef 100644 --- a/src/pages/Market/components/BestItem.tsx +++ b/src/pages/Market/components/BestItem.tsx @@ -14,32 +14,60 @@ const getPageSize = () => { } }; +interface Item { + id: number; + name: string; + description: string; + price: number; + tags: string[]; + images: string[]; + ownerId: number; + ownerNickname: string; + favoriteCount: number; + createdAt: string; +} + +interface ItemList { + totalCount: number; + list: Item[]; +} + function BestItem() { - const [itemList, setItemList] = useState([]); + const [itemList, setItemList] = useState({ totalCount: 0, list: [] }); const [pageSize, setPageSize] = useState(getPageSize()); const fetchSortedData = useCallback(async () => { - const products = await getProducts({ orderBy: "favorite", pageSize }); - setItemList(products.list); + try { + const products = await getProducts({ orderBy: "favorite", pageSize }); + setItemList(products); + } catch (error) { + console.error('Error fetching best products:', error); + } }, [pageSize]); + useEffect(() => { + fetchSortedData(); + }, [fetchSortedData]); + useEffect(() => { const handleResize = () => { - setPageSize(getPageSize()); + const newPageSize = getPageSize(); + if (newPageSize !== pageSize) { + setPageSize(newPageSize); + } }; window.addEventListener("resize", handleResize); - fetchSortedData(); return () => { window.removeEventListener("resize", handleResize); }; - }, [fetchSortedData]); + }, [pageSize]); return (

베스트 상품

- {itemList?.map((item) => ( + {itemList.list.map((item) => ( ))}
@@ -48,4 +76,4 @@ function BestItem() { ); } -export default BestItem; +export default BestItem; \ No newline at end of file diff --git a/src/pages/Market/components/ItemCard.tsx b/src/pages/Market/components/ItemCard.tsx index 38be0acc..ac741cdd 100644 --- a/src/pages/Market/components/ItemCard.tsx +++ b/src/pages/Market/components/ItemCard.tsx @@ -3,7 +3,24 @@ import likeIcon from '../../../assets/images/likeicon.png'; import styles from '../Market.module.css'; import { Link } from 'react-router-dom'; -function ItemCard({ item }) { +interface Item { + id: number; + name: string; + description: string; + price: number; + tags: string[]; + images: string[]; + ownerId: number; + ownerNickname: string; + favoriteCount: number; + createdAt: string; +} + +interface ItemCardProps { + item: Item; +} + +const ItemCard: React.FC = ({ item }) => { return ( {item.name} @@ -19,4 +36,4 @@ function ItemCard({ item }) { ); } -export default ItemCard; +export default ItemCard; \ No newline at end of file diff --git a/src/pages/ProductDetail/ProductDetail.tsx b/src/pages/ProductDetail/ProductDetail.tsx index 59e2bd6a..cd63f31e 100644 --- a/src/pages/ProductDetail/ProductDetail.tsx +++ b/src/pages/ProductDetail/ProductDetail.tsx @@ -1,25 +1,38 @@ -import React, { useEffect, useCallback, useState } from 'react' +import React, { useEffect, useCallback, useState } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import Header from '../../components/Header/Header'; -import styles from'./ProductDetail.module.css'; +import styles from './ProductDetail.module.css'; import ProductInfo from './components/ProductInfo/ProductInfo'; import { getProductDetail } from '../../api/itemApi'; import Comment from './components/Comment/Comment'; import backicon from '../../assets/images/backicon.png'; +export interface Item { + id: number; + name: string; + description: string; + price: number; + tags: string[]; + images: string[]; + ownerId: number; + ownerNickname: string; + favoriteCount: number; + createdAt: string; +} + function ProductDetail() { - const { productId } = useParams(); - const [item, setItem] = useState(null); + const { productId } = useParams<{ productId :string}>(); + const [item, setItem] = useState(undefined); const [loading, setLoading] = useState(true); - let navigate = useNavigate(); + const navigate = useNavigate(); const fetchProductData = useCallback(async () => { try { - setLoading(true) - const product = await getProductDetail(productId); + setLoading(true); + const product = await getProductDetail(Number(productId)); setItem(product); console.log(product); - } catch(err) { + } catch (err) { console.error(err); } finally { setLoading(false); @@ -28,32 +41,32 @@ function ProductDetail() { useEffect(() => { fetchProductData(); - },[fetchProductData]); - console.log(item) + }, [fetchProductData]); + + console.log(item); if (loading) { - return ; + return
Loading...
; } + return ( <>
- -
+ {item && } +
-
- - ) + ); } export default ProductDetail; \ No newline at end of file diff --git a/src/pages/ProductDetail/components/Comment/Comment.tsx b/src/pages/ProductDetail/components/Comment/Comment.tsx index c3e56a80..47a78189 100644 --- a/src/pages/ProductDetail/components/Comment/Comment.tsx +++ b/src/pages/ProductDetail/components/Comment/Comment.tsx @@ -2,65 +2,82 @@ import React, { useState, useEffect, useCallback } from 'react'; import styles from './Comment.module.css'; import { useParams } from 'react-router-dom'; import { postProductComment, getProductComments } from '../../../../api/itemApi'; -import CommentItem from '../../components/CommentItem/CommentItem'; +import CommentItem from '../CommentItem/CommentItem'; + +interface Comment { + id: number; + writer: Writer; + content: string; + createdAt: string; + updatedAt: string; +} + +interface CommentList { + nextCursor: number; + list: Comment[]; +} + +interface Writer { + image: string; + nickname: string; + id: number; +} function Comment() { - const [value, setValue] = useState(''); - const [isValid, setIsValid] = useState(false); - const { productId } = useParams(); - const [itemList, setItemList] = useState([]); - const LIMIT = 6; - const [cursor, setCursor] = useState(null); - - const handleLoad = useCallback(async () => { - const comments = await getProductComments(productId, { limit: LIMIT, cursor:0 }); - setItemList(comments.list); - console.log(comments.list) - }, [productId, cursor]); + const [value, setValue] = useState(''); + const [isValid, setIsValid] = useState(false); + const { productId } = useParams<{ productId: string }>(); + const [itemList, setItemList] = useState({ nextCursor: 0, list: [] }); + const LIMIT = 6; + + const handleLoad = useCallback(async () => { + const comments = await getProductComments(Number(productId), { limit: LIMIT, cursor: 0 }); + setItemList({ nextCursor: comments.nextCursor, list: comments.list }); + console.log(comments.list); + }, [productId]); - const handleSubmit = async (e) => { - e.preventDefault(); - const formData = new FormData(); - formData.append('comment', value); - await postProductComment(productId, formData); - setValue(''); - await handleLoad(); - }; + const handleSubmit = async (e: React.ChangeEvent) => { + e.preventDefault(); + const formData = new FormData(); + formData.append('comment', value); + await postProductComment(Number(productId), formData); + setValue(''); + await handleLoad(); + }; + useEffect(() => { + setIsValid(value.trim() !== ''); + }, [value]); - useEffect(() => { - setIsValid(value.trim() !== ''); - }, [value]); + useEffect(() => { + handleLoad(); + }, [handleLoad]); - useEffect(() => { - handleLoad(); - }, [handleLoad]); + return ( +
+ +
+ +
+ +
+
- return ( -
- -
- -
- +
+ {itemList.list.map((item) => ( + + ))} +
-
- -
- {itemList?.map((item) => ( - - ))} -
-
- ); + ); } -export default Comment; +export default Comment; \ No newline at end of file diff --git a/src/pages/ProductDetail/components/CommentItem/CommentItem.tsx b/src/pages/ProductDetail/components/CommentItem/CommentItem.tsx index 8a41ae96..bb9f77f0 100644 --- a/src/pages/ProductDetail/components/CommentItem/CommentItem.tsx +++ b/src/pages/ProductDetail/components/CommentItem/CommentItem.tsx @@ -1,7 +1,7 @@ import React from 'react'; import styles from './CommentItem.module.css'; -const elapsedTime = (date) => { +const elapsedTime = (date:string) => { const start = new Date(date); const end = new Date(); @@ -18,7 +18,21 @@ const elapsedTime = (date) => { return `${Math.floor(days)}일 전`; }; -function CommentItem({item}) { +interface Comment { + id:number; + writer:Writer; + content:string; + createdAt:string; + updatedAt:string; + } + +interface Writer { + image: string; + nickname:string; + id:number; +} + +const CommentItem:React.FC<{ item: Comment }> = ({item}) => { return ( <> diff --git a/src/pages/ProductDetail/components/ProductInfo/ProductInfo.tsx b/src/pages/ProductDetail/components/ProductInfo/ProductInfo.tsx index 73f177e8..70adb1e1 100644 --- a/src/pages/ProductDetail/components/ProductInfo/ProductInfo.tsx +++ b/src/pages/ProductDetail/components/ProductInfo/ProductInfo.tsx @@ -1,64 +1,64 @@ -import React from 'react' +import React from 'react'; import './ProductInfo.css'; import profileimg from '../../../../assets/images/profile.png'; import heartIcon from '../../../../assets/images/hearticon.png'; +import { Item } from '../../ProductDetail'; -function formatDate(createdDate) { +function formatDate(createdDate: string) { const date = new Date(createdDate); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); - + return `${year}.${month}.${day}`; } -function ProductInfo( { item }) { - + +interface ProductInfoProps { + item: Item; +} + +const ProductInfo: React.FC = ({ item }) => { return ( - <> -
- 제품 이미지 -
-
-
-
{item.name}
-
{item.price.toLocaleString()}
-
+
+ 제품 이미지 +
+
+
+
{item.name}
+
{item.price.toLocaleString()} 원
{/* Added currency unit */} +
+
+
+ +
{item.description}
+ +
+ {item.tags?.map((tagItem, index) => ( +
+ #{tagItem} +
+ ))}
-
- -
- {item.description} -
- -
- {item.tags?.map((tagItem, index) => ( -
- #{tagItem} -
- ))} - -
+
+
+
+
+ 프로필 이미지 +
+

총명한 판다

+

{formatDate(item.createdAt)}

-
-
- 프로필 이미지 -
-

총명한 판다

-

{formatDate(item.createdAt)}

-
-
-
-
- 좋아요아이콘 -

{item.favoriteCount}

-
-
+
+
+ 좋아요 아이콘 +

{item.favoriteCount}

+
- - ) +
+ ); } -export default ProductInfo \ No newline at end of file +export default ProductInfo; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index f460fe4e..50604266 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,11 @@ "dom.iterable", "esnext" ], + "typeRoots": [ + "./node_modules/@types", + "@types", + "global.d.ts" + ], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true,