diff --git a/.gitignore b/.gitignore index a547bf36..cea8a0ba 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ dist-ssr *.njsproj *.sln *.sw? + +*.env \ No newline at end of file diff --git a/index.html b/index.html index 2cfc72a7..1cacaed7 100644 --- a/index.html +++ b/index.html @@ -17,6 +17,6 @@
- + diff --git a/package-lock.json b/package-lock.json index ab8993f0..5df50a73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "eslint-plugin-react": "^7.34.1", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.6", + "typescript": "^5.9.2", "vite": "^5.2.0" } }, @@ -4688,6 +4689,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", diff --git a/package.json b/package.json index 27c32455..cbd4e3c1 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite", - "build": "vite build", + "build": "tsc && vite build", "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" }, @@ -24,6 +24,7 @@ "eslint-plugin-react": "^7.34.1", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.6", + "typescript": "^5.9.2", "vite": "^5.2.0" } } diff --git a/src/App.jsx b/src/App.tsx similarity index 100% rename from src/App.jsx rename to src/App.tsx diff --git a/src/components/Inquiry.jsx b/src/components/Inquiry.jsx index f1258fca..e669deea 100644 --- a/src/components/Inquiry.jsx +++ b/src/components/Inquiry.jsx @@ -6,7 +6,7 @@ import { import { requestInquiryLists } from "../services/inquiryApi"; import { useNavigate } from "react-router"; import icBack from "../assets/icons/ic_back.svg"; -import useService from "../hooks/useService"; +import useFetch from "../hooks/useService"; import InquiryWriteArea from "./InquiryWriteArea"; import InquiryList from "../pages/components/ItemDetail/InquiryList"; @@ -16,7 +16,7 @@ export default function Inquiry({ id }) { /** * 문의 내역을 가져온다. */ - const { data, isLoading } = useService(() => requestInquiryLists(id)); + const { data, isLoading } = useFetch(() => requestInquiryLists(id)); return ( <> diff --git a/src/components/InquiryWriteArea.jsx b/src/components/InquiryWriteArea.jsx index be264830..66e3d12e 100644 --- a/src/components/InquiryWriteArea.jsx +++ b/src/components/InquiryWriteArea.jsx @@ -1,5 +1,4 @@ import { useState } from "react"; -import usePost from "../hooks/usePost"; import { InquirySubmitButton, InquiryTextArea, @@ -30,7 +29,7 @@ export default function InquiryWriteArea() { }, }; - const { data: success } = usePost(requestPostInquiry(data)); + // const { data: success } = usePost(requestPostInquiry(data)); if (success) { location.reload(true); diff --git a/src/hooks/usePost.jsx b/src/hooks/usePost.jsx deleted file mode 100644 index 681432e1..00000000 --- a/src/hooks/usePost.jsx +++ /dev/null @@ -1,9 +0,0 @@ -import useService from "./useService"; - -const usePost = (postFetching) => { - const { data, isLoading, isError } = useService(() => postFetching); - - return { data, isLoading, isError }; -}; - -export default usePost; diff --git a/src/hooks/useService.jsx b/src/hooks/useService.jsx index eb137f8f..a3943f7f 100644 --- a/src/hooks/useService.jsx +++ b/src/hooks/useService.jsx @@ -3,18 +3,18 @@ import { useEffect, useState } from "react"; /** * 데이터 통신을 위한 공통 커스텀 훅 * @param {Function} fetchFunction 서버와 직접 통신하는 함수 - * @returns {data: object, isLoading: boolean} + * @returns {data: object, isLoading: boolean, error: boolean} */ -const useService = (fetchFunction) => { +const useFetch = (fetchFunction) => { const [isLoading, setIsLoading] = useState(false); const [data, setData] = useState(); const [error, setError] = useState(false); useEffect(() => { - const getService = async (getDataFunction) => { + const fetcher = async (fetching) => { try { setIsLoading(true); - const response = await getDataFunction(); + const response = await fetching(); if (!response) { throw new Error("서버와의 통신에 실패했습니다."); @@ -29,10 +29,10 @@ const useService = (fetchFunction) => { } }; - getService(fetchFunction); + fetcher(fetchFunction); }, []); return { data, isLoading, error }; }; -export default useService; +export default useFetch; diff --git a/src/layout/AuthLayout.jsx b/src/layout/AuthLayout.tsx similarity index 58% rename from src/layout/AuthLayout.jsx rename to src/layout/AuthLayout.tsx index 24bc8e9a..6f924f9e 100644 --- a/src/layout/AuthLayout.jsx +++ b/src/layout/AuthLayout.tsx @@ -1,13 +1,14 @@ -import { useState } from 'react'; -import { Outlet } from 'react-router'; -import visibleEye_off from '../assets/icons/eyes_off.png'; -import visibleEye_on from '../assets/icons/eyes_on.png'; +import { useState } from "react"; +import { Outlet } from "react-router"; +import visibleEye_off from "../assets/icons/eyes_off.png"; +import visibleEye_on from "../assets/icons/eyes_on.png"; +import { isVisibleKey, VisibleValue } from "../types/authType"; export default function AuthLayout() { /** * 비밀번호 보이기 / 가리기 상태 */ - const [visible, setVisible] = useState({ + const [visible, setVisible] = useState({ pw: false, checkPw: false, }); @@ -16,7 +17,9 @@ export default function AuthLayout() { * id 별로 비밀번호 가리기 여부를 변경한다. * @param {string} id */ - const onClickVisible = (id) => { + const onClickVisible = (id: string) => { + if (!isVisibleKey(id)) return; + setVisible((prevState) => ({ ...prevState, [id]: !visible[id], diff --git a/src/layout/Footer.jsx b/src/layout/Footer.tsx similarity index 88% rename from src/layout/Footer.jsx rename to src/layout/Footer.tsx index fc20dca7..7f4769e7 100644 --- a/src/layout/Footer.jsx +++ b/src/layout/Footer.tsx @@ -1,8 +1,8 @@ -import '../styles/layout/footer.css'; -import ic_facebook from '../assets/icons/ic_facebook.svg'; -import ic_twitter from '../assets/icons/ic_twitter.svg'; -import ic_youtube from '../assets/icons/ic_youtube.svg'; -import ic_instagram from '../assets/icons/ic_instagram.svg'; +import "../styles/layout/footer.css"; +import ic_facebook from "../assets/icons/ic_facebook.svg"; +import ic_twitter from "../assets/icons/ic_twitter.svg"; +import ic_youtube from "../assets/icons/ic_youtube.svg"; +import ic_instagram from "../assets/icons/ic_instagram.svg"; /** * 푸터 diff --git a/src/layout/Header.jsx b/src/layout/Header.tsx similarity index 100% rename from src/layout/Header.jsx rename to src/layout/Header.tsx diff --git a/src/layout/MainLayout.jsx b/src/layout/MainLayout.tsx similarity index 60% rename from src/layout/MainLayout.jsx rename to src/layout/MainLayout.tsx index e0e2f203..481b0a02 100644 --- a/src/layout/MainLayout.jsx +++ b/src/layout/MainLayout.tsx @@ -1,6 +1,6 @@ -import Header from './Header'; -import Footer from './Footer'; -import { Outlet } from 'react-router'; +import Header from "./Header"; +import Footer from "./Footer"; +import { Outlet } from "react-router"; export default function MainLayout() { return ( diff --git a/src/main.jsx b/src/main.jsx deleted file mode 100644 index 37fd47b5..00000000 --- a/src/main.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import App from './App.jsx'; -import { BrowserRouter } from 'react-router'; - -ReactDOM.createRoot(document.getElementById('root')).render( - - - - - -); diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 00000000..54517b0e --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App.tsx"; +import { BrowserRouter } from "react-router"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + + + +); diff --git a/src/pages/Home.jsx b/src/pages/Home.tsx similarity index 88% rename from src/pages/Home.jsx rename to src/pages/Home.tsx index 576cb6ae..30418411 100644 --- a/src/pages/Home.jsx +++ b/src/pages/Home.tsx @@ -1,10 +1,10 @@ -import '../styles/style.css'; -import home_top from '../assets/images/Img_home_top.png'; -import hot_item from '../assets/images/hot_item.png'; -import search_img from '../assets/images/search.png'; -import register_img from '../assets/images/register.png'; -import home_bottom from '../assets/images/Img_home_bottom.png'; -import { Link } from 'react-router'; +import "../styles/style.css"; +import home_top from "../assets/images/Img_home_top.png"; +import hot_item from "../assets/images/hot_item.png"; +import search_img from "../assets/images/search.png"; +import register_img from "../assets/images/register.png"; +import home_bottom from "../assets/images/Img_home_bottom.png"; +import { Link } from "react-router"; /** * 메인화면 diff --git a/src/pages/auth/Login.jsx b/src/pages/auth/Login.tsx similarity index 76% rename from src/pages/auth/Login.jsx rename to src/pages/auth/Login.tsx index 76dd1b74..c692a57f 100644 --- a/src/pages/auth/Login.jsx +++ b/src/pages/auth/Login.tsx @@ -1,16 +1,22 @@ -import '../../styles/auth.css'; -import ic_kakao from '../../assets/icons/ic_kakao.png'; -import ic_google from '../../assets/icons/ic_google.png'; -import { Link, useOutletContext } from 'react-router'; -import { useForm } from 'react-hook-form'; -import { useEffect } from 'react'; +import "../../styles/auth.css"; +import ic_kakao from "../../assets/icons/ic_kakao.png"; +import ic_google from "../../assets/icons/ic_google.png"; +import { Link, useOutletContext } from "react-router"; +import { useForm } from "react-hook-form"; +import { useEffect } from "react"; +import { AuthType } from "../../types/authType"; /** * 로그인 화면 */ export default function Login() { - const { visible, setVisible, onClickVisible, visibleEye_off, visibleEye_on } = - useOutletContext(); + const { + visible, + setVisible, + onClickVisible, + visibleEye_off, + visibleEye_on, + }: AuthType = useOutletContext(); /** * 기본적으로 비밀번호 숨김 상태 유지 @@ -28,8 +34,8 @@ export default function Login() { const { register, formState: { errors }, - handleSubmit, - } = useForm({ mode: 'onBlur' }); + // handleSubmit, + } = useForm({ mode: "onBlur" }); return ( <> @@ -40,7 +46,7 @@ export default function Login() {
-
+
{ - onClickVisible(e.target.id); + onClickVisible(e.currentTarget.id); }} />
- {errors.password?.type === 'required' && ( + {errors.password?.type === "required" && (

비밀번호를 입력해주세요

)} - {errors.password?.type === 'minLength' && ( + {errors.password?.type === "minLength" && (

비밀번호를 8자 이상 입력해주세요.

diff --git a/src/pages/auth/Signup.jsx b/src/pages/auth/Signup.tsx similarity index 73% rename from src/pages/auth/Signup.jsx rename to src/pages/auth/Signup.tsx index eea9e1d8..af91acf5 100644 --- a/src/pages/auth/Signup.jsx +++ b/src/pages/auth/Signup.tsx @@ -1,16 +1,22 @@ -import { Link, useOutletContext } from 'react-router'; -import '../../styles/auth.css'; -import ic_kakao from '../../assets/icons/ic_kakao.png'; -import ic_google from '../../assets/icons/ic_google.png'; -import { useEffect, useState } from 'react'; -import { useForm } from 'react-hook-form'; +import { Link, useOutletContext } from "react-router"; +import "../../styles/auth.css"; +import ic_kakao from "../../assets/icons/ic_kakao.png"; +import ic_google from "../../assets/icons/ic_google.png"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { AuthType, SignupType } from "../../types/authType"; /** * 회원가입 화면 */ export default function Signup() { - const { visible, setVisible, onClickVisible, visibleEye_off, visibleEye_on } = - useOutletContext(); + const { + visible, + setVisible, + onClickVisible, + visibleEye_off, + visibleEye_on, + }: AuthType = useOutletContext(); /** * 기본적으로 비밀번호 숨김 상태 유지 @@ -26,16 +32,16 @@ export default function Signup() { * 회원가입 양식 정보를 담고 있는 객체 * 이메일, 닉네임, 비밀번호, 비밀번호 확인 */ - const [signupForm, setSignupForm] = useState({ - email: '', - nickname: '', - pw: '', + const [signupForm, setSignupForm] = useState({ + email: "", + nickname: "", + pw: "", }); - const onValid = (data) => { - console.log('✅ 유효성 검사 성공! 데이터:', data); - setSignupForm(data); - }; + // const onValid = (data) => { + // console.log("✅ 유효성 검사 성공! 데이터:", data); + // setSignupForm(data); + // }; /** * React-Hook-Form 사용 객체 선언 @@ -43,10 +49,10 @@ export default function Signup() { const { register, formState: { errors, isValid }, - handleSubmit, + // handleSubmit, getValues, } = useForm({ - mode: 'onBlur', + mode: "onBlur", }); console.log(isValid); @@ -60,7 +66,7 @@ export default function Signup() { - +
- {errors.nickname?.type === 'required' && ( + {errors.nickname?.type === "required" && (

닉네임을 입력해주세요

)} - {errors.nickname?.type === 'maxLength' && ( + {errors.nickname?.type === "maxLength" && (

닉네임이 너무 깁니다.

)} @@ -115,12 +119,11 @@ export default function Signup() { { - onClickVisible(e.target.id); + onClickVisible(e.currentTarget.id); }} />
- {errors.password?.type === 'required' && ( + {errors.password?.type === "required" && (

비밀번호를 입력해주세요

)} - {errors.password?.type === 'minLength' && ( + {errors.password?.type === "minLength" && (

비밀번호를 8자 이상 입력해주세요.

@@ -151,16 +154,15 @@ export default function Signup() { - getValues('password') === getValues('confirmPassword'), + getValues("password") === getValues("confirmPassword"), })} />
@@ -169,17 +171,17 @@ export default function Signup() { src={visible.checkPw ? visibleEye_on : visibleEye_off} alt="visible-icon" onClick={(e) => { - onClickVisible(e.target.id); + onClickVisible(e.currentTarget.id); }} />
- {errors.confirmPassword?.type === 'minLength' && ( + {errors.confirmPassword?.type === "minLength" && (

비밀번호를 8자 이상 입력해주세요.

)} - {errors.confirmPassword?.type === 'validate' && ( + {errors.confirmPassword?.type === "validate" && (

비밀번호가 일치하지 않습니다.

)} diff --git a/src/pages/items/ItemDetail.jsx b/src/pages/items/ItemDetail.jsx index f21deeb3..9e0105fd 100644 --- a/src/pages/items/ItemDetail.jsx +++ b/src/pages/items/ItemDetail.jsx @@ -7,7 +7,7 @@ import { } from "../../styles/items/ItemDetailStyle"; import Inquiry from "../../components/Inquiry"; -import useService from "../../hooks/useService"; +import useFetch from "../../hooks/useService"; import ProductDetails from "../components/ItemDetail/ProductDetails"; export default function ItemDetail() { @@ -24,7 +24,7 @@ export default function ItemDetail() { /** * 상품 정보 받아오기 */ - const { data, isLoading } = useService(() => requestProductDetail(productId)); + const { data, isLoading } = useFetch(() => requestProductDetail(productId)); return ( <> diff --git a/src/services/inquiryApi.js b/src/services/inquiryApi.js index 0e05488b..e22edd35 100644 --- a/src/services/inquiryApi.js +++ b/src/services/inquiryApi.js @@ -1,11 +1,11 @@ +const BASE_URL = import.meta.env.VITE_BASE_URL; + /** * 문의 데이터를 가져온다. * @param {string} productId */ export const requestInquiryLists = async (productId) => { - const url = new URL( - `https://panda-market-api.vercel.app/products/${productId}/comments` - ); + const url = new URL(`${BASE_URL}products/${productId}/comments`); url.searchParams.append("limit", 3); const response = await fetch(url, { @@ -24,9 +24,7 @@ export const requestInquiryLists = async (productId) => { * @returns */ export const requestPostInquiry = async (inquiryData) => { - const url = new URL( - `https://panda-market-api.vercel.app/products/${inquiryData.productId}/comments` - ); + const url = new URL(`${BASE_URL}products/${inquiryData.productId}/comments`); try { const response = await fetch(url, { diff --git a/src/services/itemsApi.js b/src/services/itemsApi.js index 8148c414..2d26357f 100644 --- a/src/services/itemsApi.js +++ b/src/services/itemsApi.js @@ -1,10 +1,12 @@ +const BASE_URL = import.meta.env.VITE_BASE_URL; + /** * 서버에 상품 목록을 요청한다. * @param {string} orderBy * @returns {object} */ export const requestProductList = async (query) => { - const url = new URL("https://panda-market-api.vercel.app/products"); + const url = new URL(`${BASE_URL}products`); url.searchParams.append("page", query.page); url.searchParams.append("pageSize", query.pageSize); url.searchParams.append("orderBy", query.orderBy); @@ -25,9 +27,7 @@ export const requestProductList = async (query) => { * @returns {json} response */ export const requestProductDetail = async (productId) => { - const url = new URL( - `https://panda-market-api.vercel.app/products/${productId}` - ); + const url = new URL(`${BASE_URL}products/${productId}`); const response = await fetch(url, { method: "get", diff --git a/src/types/authType.ts b/src/types/authType.ts new file mode 100644 index 00000000..abbdf2f8 --- /dev/null +++ b/src/types/authType.ts @@ -0,0 +1,26 @@ +import { SetStateAction } from "react"; + +export interface VisibleValue { + pw: boolean; + checkPw: boolean; +} + +export type VisibleKey = keyof VisibleValue; + +export interface AuthType { + visible: VisibleValue; + setVisible: (prevState: SetStateAction) => void; + onClickVisible: (e: string) => void; + visibleEye_off: string; + visibleEye_on: string; +} + +export function isVisibleKey(value: string): value is VisibleKey { + return value === "pw" || value === "checkPw"; +} + +export interface SignupType { + email: string; + nickname: string; + pw: string; +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..33056b8c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "outDir": "./transpile", + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 00000000..97ede7ee --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.js b/vite.config.ts similarity index 100% rename from vite.config.js rename to vite.config.ts