diff --git a/package-lock.json b/package-lock.json index 06a4aba8..acb2a1c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,13 +15,15 @@ }, "devDependencies": { "@eslint/js": "^9.21.0", - "@types/react": "^19.0.10", - "@types/react-dom": "^19.0.4", + "@types/node": "^22.15.29", + "@types/react": "^18.3.23", + "@types/react-dom": "^18.3.7", "@vitejs/plugin-react": "^4.3.4", "eslint": "^9.21.0", "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.4.19", "globals": "^15.15.0", + "typescript": "^5.8.3", "vite": "^6.2.0" } }, @@ -1368,24 +1370,42 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "22.15.29", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.29.tgz", + "integrity": "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.14", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", + "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/react": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.0.tgz", - "integrity": "sha512-UaicktuQI+9UKyA4njtDOGBD/67t8YEBt2xdfqu8+gP9hqPUPsiXlNPcpS2gVdjmis5GKPG3fCxbQLVgxsQZ8w==", + "version": "18.3.23", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", + "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", "dev": true, "license": "MIT", "dependencies": { + "@types/prop-types": "*", "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "19.1.2", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.2.tgz", - "integrity": "sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==", + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "dev": true, "license": "MIT", "peerDependencies": { - "@types/react": "^19.0.0" + "@types/react": "^18.0.0" } }, "node_modules/@vitejs/plugin-react": { @@ -2736,6 +2756,27 @@ "node": ">= 0.8.0" } }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", diff --git a/package.json b/package.json index 5ecaeaf4..9b7e2ad6 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite", - "build": "vite build", + "build": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview" }, @@ -17,13 +17,15 @@ }, "devDependencies": { "@eslint/js": "^9.21.0", - "@types/react": "^19.0.10", - "@types/react-dom": "^19.0.4", + "@types/node": "^22.15.29", + "@types/react": "^18.3.23", + "@types/react-dom": "^18.3.7", "@vitejs/plugin-react": "^4.3.4", "eslint": "^9.21.0", "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.4.19", "globals": "^15.15.0", + "typescript": "^5.8.3", "vite": "^6.2.0" } } diff --git a/src/App.jsx b/src/App.tsx similarity index 72% rename from src/App.jsx rename to src/App.tsx index 5f49529a..dc2c75aa 100644 --- a/src/App.jsx +++ b/src/App.tsx @@ -1,12 +1,12 @@ import { BrowserRouter, Routes, Route } from 'react-router-dom'; import { HelmetProvider } from 'react-helmet-async'; -import Index from './pages/Index.jsx'; -import Items from './pages/Items.jsx'; -import ItemProduct from './pages/ItemProduct.jsx'; -import AddItem from './pages/AddItem.jsx'; -import Auth from './pages/Auth.jsx'; -import Login from './pages/Login.jsx'; -import Signup from './pages/Signup.jsx'; +import Index from './pages/Index'; +import Items from './pages/Items'; +import ItemProduct from './pages/ItemProduct'; +import AddItem from './pages/AddItem'; +import Auth from './pages/Auth'; +import Login from './pages/Login'; +import Signup from './pages/Signup'; import './reset.css'; import './color.css'; diff --git a/src/api/api.js b/src/api/api.js deleted file mode 100644 index 271a69d0..00000000 --- a/src/api/api.js +++ /dev/null @@ -1,20 +0,0 @@ -const BASE_URL = "https://panda-market-api.vercel.app"; - -export async function getProducts({ page=1, pageSize=10, orderBy="recent", keyword }) { - const query = new URLSearchParams({ page, pageSize, orderBy }).toString(); - const response = await fetch(`${BASE_URL}/products?${query}${keyword ? "&keyword="+keyword : ''}`); - if (!response.ok) { throw Error("Request Error"); } - return (await response.json()); -} - -export async function getProduct({ productId }) { - const response = await fetch(`${BASE_URL}/products/${productId}`); - if (!response.ok) { throw Error("Request Error"); } - return (await response.json()); -} - -export async function getProductComments({ productId, limit }) { - const response = await fetch(`${BASE_URL}/products/${productId}/comments?limit=${limit}`); - if (!response.ok) { throw Error("Request Error"); } - return (await response.json()); -} \ No newline at end of file diff --git a/src/api/api.ts b/src/api/api.ts new file mode 100644 index 00000000..2070964c --- /dev/null +++ b/src/api/api.ts @@ -0,0 +1,31 @@ +import type { Comments, Product, Products } from "./apiTypes"; + +const BASE_URL = "https://panda-market-api.vercel.app"; + +export async function getProducts({ page=1, pageSize=10, orderBy="recent", keyword='' }: { + page?: number; + pageSize?: number; + orderBy?: "recent" | "favorite"; + keyword?: string; +}): Promise { + const response = await fetch(`${BASE_URL}/products?page=${page}&pageSize=${pageSize}&orderBy=${orderBy}${keyword && "&keyword="+keyword}`); + if (!response.ok) { throw Error("Request Error"); } + return (await response.json()) as Products; +} + +export async function getProduct({ productId }: { + productId: number | string; +}): Promise { + const response = await fetch(`${BASE_URL}/products/${productId}`); + if (!response.ok) { throw Error("Request Error"); } + return (await response.json()) as Product; +} + +export async function getProductComments({ productId, limit }: { + productId: number | string; + limit: number; +}): Promise { + const response = await fetch(`${BASE_URL}/products/${productId}/comments?limit=${limit}`); + if (!response.ok) { throw Error("Request Error"); } + return (await response.json()) as Comments; +} \ No newline at end of file diff --git a/src/api/apiTypes.ts b/src/api/apiTypes.ts new file mode 100644 index 00000000..6648adb3 --- /dev/null +++ b/src/api/apiTypes.ts @@ -0,0 +1,35 @@ +export interface Product { + createdAt: string; + favoriteCount: number; + ownerNickname: string; + ownerId: number; + images: string[]; + tags: string[]; + price: number; + description: string; + name: string, + id: number; + isFavorite: boolean; +} + +export interface Products { + totalCount: number; + list: Product[]; +} + +export interface Comment { + writer: { + image: string; + nickname: string; + id: number; + }; + updatedAt: string; + createdAt: string; + content: string; + id: number; +} + +export interface Comments { + nextCursor: number; + list: Comment[] +} \ No newline at end of file diff --git a/src/components/AuthInput.jsx b/src/components/AuthInput.tsx similarity index 51% rename from src/components/AuthInput.jsx rename to src/components/AuthInput.tsx index 954e1222..c149193c 100644 --- a/src/components/AuthInput.jsx +++ b/src/components/AuthInput.tsx @@ -1,53 +1,61 @@ -import { useState } from "react"; +import { useState, type FocusEvent, type FocusEventHandler, type InputHTMLAttributes } from "react"; import Input from "./Input"; import icEyeVisible from "../assets/ic_eye_visible.svg"; import icEyeInvisible from "../assets/ic_eye_invisible.svg"; import "./AuthInput.css"; -function PasswordInput ({ name, className, onFocusout, valid, wrongMessage, ...props }) { +interface PasswordProps extends InputHTMLAttributes { + name: string; + label: string; + className?: string; + onFocusout?: FocusEventHandler; + valid?: string; + wrongMessage: string; +} + +function PasswordInput ({ name, label, className='', onFocusout, valid, wrongMessage, ...props }: PasswordProps) { const [isVisible, setIsVisible] = useState(false); const onClick = () => { setIsVisible((prev) => !prev); } const passwordMatch = () => { try { - const password = document.querySelector("input#password"); - const passwordCheck = document.querySelector("input#password-check"); + const password = document.querySelector("input#password") as HTMLInputElement; + const passwordCheck = document.querySelector("input#password-check") as HTMLInputElement; if (password.value === passwordCheck.value) { passwordCheck.classList.add("correct"); passwordCheck.classList.remove("wrong"); - passwordCheck.nextElementSibling.nextElementSibling.textContent = null; + (passwordCheck.nextElementSibling as HTMLElement).textContent = null; } else if (passwordCheck.value) { passwordCheck.classList.add("wrong"); passwordCheck.classList.remove("correct"); - passwordCheck.nextElementSibling.nextElementSibling.textContent = "비밀번호가 일치하지 않습니다."; + (passwordCheck.nextElementSibling as HTMLElement).textContent = "비밀번호가 일치하지 않습니다."; } } catch (e) {} } return ( - - -
{wrongMessage}
- + +
{wrongMessage}
+ + ); } -export default function AuthInput ({ label, name, type="text", placeholder='', emptyWrongMessage='', invalidWrongMessage='' }) { +export default function AuthInput ({ label='', name='', type="text", placeholder='', emptyWrongMessage='', invalidWrongMessage='' }) { const [valid, setValid] = useState(''); - const [wrongMessage, setWrongMessage] = useState(null); + const [wrongMessage, setWrongMessage] = useState(''); - const onFocusout = (e) => { + const onFocusout = (e: FocusEvent) => { if (!e.target.value) { setValid("wrong"); setWrongMessage(emptyWrongMessage); @@ -56,7 +64,7 @@ export default function AuthInput ({ label, name, type="text", placeholder='', e setWrongMessage(invalidWrongMessage); } else { setValid("correct"); - setWrongMessage(null); + setWrongMessage(''); } } diff --git a/src/components/Button.jsx b/src/components/Button.tsx similarity index 52% rename from src/components/Button.jsx rename to src/components/Button.tsx index c3308bb1..0d7c5e86 100644 --- a/src/components/Button.jsx +++ b/src/components/Button.tsx @@ -1,7 +1,13 @@ +import type { ButtonHTMLAttributes, ReactNode } from "react"; import styles from "./Button.module.css"; -//styleType은 small, medium, large로 나뉜다 -function Button ({ styleType="small", className='', children, ...props }) { +interface Props extends ButtonHTMLAttributes { + styleType?: "small" | "medium" | "large"; + className?: string; + children?: ReactNode; +} + +function Button ({ styleType="small", className='', children, ...props }: Props) { return ( ); -} - -export default ButtonHeart; \ No newline at end of file diff --git a/src/components/ButtonHeart.tsx b/src/components/ButtonHeart.tsx new file mode 100644 index 00000000..b79f8d60 --- /dev/null +++ b/src/components/ButtonHeart.tsx @@ -0,0 +1,24 @@ +import type { ButtonHTMLAttributes, ReactNode } from "react"; +import icHeartLargeActive from "../assets/heart/ic_heart_large_active.svg"; +import icHeartLargeInactive from "../assets/heart/ic_heart_large_inactive.svg"; +import icHeartMediumActive from "../assets/heart/ic_heart_medium_active.svg"; +import icHeartMediumInactive from "../assets/heart/ic_heart_medium_inactive.svg"; +import styles from "./ButtonHeart.module.css"; + +interface Props extends ButtonHTMLAttributes { + isActive?: boolean; + className?: string; + children?: ReactNode; +} + +function ButtonHeart({ isActive=false, className='', children, ...props }: Props) { + return (); +} + +export default ButtonHeart; \ No newline at end of file diff --git a/src/components/Dropdown.jsx b/src/components/Dropdown.tsx similarity index 58% rename from src/components/Dropdown.jsx rename to src/components/Dropdown.tsx index 09fd9736..771c54cd 100644 --- a/src/components/Dropdown.jsx +++ b/src/components/Dropdown.tsx @@ -1,17 +1,25 @@ -import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState, type ButtonHTMLAttributes, type MouseEventHandler } from "react"; import icKebab from "../assets/ic_kebab.svg"; import styles from "./Dropdown.module.css"; -function Dropdown({ className='', onClickEdit, onClickDelete, ...props }) { +interface Props extends ButtonHTMLAttributes { + className?: string; + onClickEdit?: MouseEventHandler; + onClickDelete?: MouseEventHandler; +} + +function Dropdown({ className='', onClickEdit, onClickDelete, ...props }: Props) { const [isOpen, setIsOpen] = useState(false); - const dropdownRef = useRef(null); + const dropdownRef = useRef(null); useEffect(() => { - const handleClickOutside = (e) => { - if (dropdownRef.current && !dropdownRef.current.contains(e.target)) setIsOpen(false); + const handleClickOutside = (e: MouseEvent) => { + if (dropdownRef.current && e.target instanceof Node && !dropdownRef.current.contains(e.target)) { + setIsOpen(false); + } }; document.addEventListener("click", handleClickOutside); - return () => document.removeEventListener("click", handleClickOutside);; + return () => document.removeEventListener("click", handleClickOutside); }, []); return (