diff --git a/.env b/.env new file mode 100644 index 000000000..eb5b868d6 --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +# swagger url +# https://panda-market-api.vercel.app/docs/#/ +NEXT_PUBLIC_API_URL = https://panda-market-api.vercel.app/products diff --git a/.eslintrc.json b/.eslintrc.json index bffb357a7..372241854 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,3 +1,3 @@ { - "extends": "next/core-web-vitals" + "extends": ["next/core-web-vitals", "next/typescript"] } diff --git a/.gitignore b/.gitignore index 8f322f0d8..b5e03c3f4 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,12 @@ # dependencies /node_modules /.pnp -.pnp.js +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions # testing /coverage @@ -24,8 +29,8 @@ npm-debug.log* yarn-debug.log* yarn-error.log* -# local env files -.env*.local +# env files (can opt-in for committing if needed) +# .env* # vercel .vercel diff --git a/README.md b/README.md index a75ac5248..ef0e47e31 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/pages/api-reference/create-next-app). ## Getting Started @@ -18,23 +18,23 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. -[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. +[API routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. -The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. +The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) instead of React pages. -This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. +This project uses [`next/font`](https://nextjs.org/docs/pages/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. ## Learn More To learn more about Next.js, take a look at the following resources: - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +- [Learn Next.js](https://nextjs.org/learn-pages-router) - an interactive Next.js tutorial. -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! ## Deploy on Vercel The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. +Check out our [Next.js deployment documentation](https://nextjs.org/docs/pages/building-your-application/deploying) for more details. diff --git a/app/fqa/page.tsx b/app/fqa/page.tsx new file mode 100644 index 000000000..8a2a6874c --- /dev/null +++ b/app/fqa/page.tsx @@ -0,0 +1,8 @@ +'use client' +import React from 'react' + +function Fqa() { + return
FQA
+} + +export default Fqa diff --git a/app/items/[id]/page.tsx b/app/items/[id]/page.tsx new file mode 100644 index 000000000..05323e729 --- /dev/null +++ b/app/items/[id]/page.tsx @@ -0,0 +1,68 @@ +'use client' +import React, { useEffect } from 'react' +import { Button, UserIconInfo } from '@/components/index' +import '@/styles/css/style.css' +import Comments from '@/components/Comments' +import Image from 'next/image' +import { useParams } from 'next/navigation' +import { ic_heart, ic_X } from '@/public/common' +import { useSelector } from 'react-redux' +import { getProduct, useAppDispatch } from '@/service/reducerSlice' +import { InitialStateType } from '@/store/store' + +function Products() { + const getParams = useParams() + const id = getParams.id ? +getParams.id : 0 + const dispatch = useAppDispatch() + const productData = useSelector((state: InitialStateType) => state.data.product) + + const handleImageClick = () => { + console.log('구현중') + } + + useEffect(() => { + dispatch(getProduct({ id: id })) + }, [productData, id]) + + return ( + productData && ( +
+
+
+
+ itemImg +
+
+

{productData.name}

+

{productData.price}

+
+ 상품 소개 + {productData.description} + 상품 태그 +
+ {productData.tags.map((tag, idx) => { + return ( + + ) + })} +
+
+ + +
+
+
+
+ +
+
+ ) + ) +} + +export default Products diff --git a/app/items/addItem/page.tsx b/app/items/addItem/page.tsx new file mode 100644 index 000000000..e651b729d --- /dev/null +++ b/app/items/addItem/page.tsx @@ -0,0 +1,71 @@ +'use client' +import React, { ChangeEvent, useState } from 'react' +import '@/styles/css/style.css' +import { TextInput, ItemImage, TagInput, Textarea } from '@/components/index' + +function AddItem() { + const [itemName, setItemName] = useState('') + const [itemDetail, setItemDetail] = useState('') + const [itemPrice, setItemPrice] = useState('') + const [itemTagArr, setItemTagArr] = useState>([]) + const [addItemImageURL, setAddItemImageURL] = useState>([]) + + const handleItemNameChange = (e: ChangeEvent) => { + setItemName(e.target.value) + } + const handleItemDetailChange = (e: ChangeEvent) => { + setItemDetail(e.target.value) + } + + const handleItemPriceChange = (e: ChangeEvent) => { + setItemPrice(e.target.value) + } + + const handleTagChange = (value: Array) => { + setItemTagArr(value) + } + + const handleImageChange = () => { + setAddItemImageURL([]) + } + + const handleItamSubmit = async () => { + if (addItemImageURL.length < 1) return alert('상품 이미지를 입력해주세요.') + if (itemName.length < 1) return alert('상품명을 입력해주세요.') + if (itemDetail.length < 1) return alert('상품 소개를 입력해주세요.') + if (itemPrice.length < 1) return alert('상품 가격을 입력해주세요.') + if (itemTagArr.length < 1) return alert('상품 태그를 입력해주세요.') + console.log('postAxios') + } + + return ( +
+
+
+

상품 등록하기

+ +
+
+

상품 이미지

+ +
+ +

상품명

+
+ + +

판매 가격

+
+ +

태그

+
+
+
+ ) +} + +export default AddItem diff --git a/app/items/layout.tsx b/app/items/layout.tsx new file mode 100644 index 000000000..4d5a17641 --- /dev/null +++ b/app/items/layout.tsx @@ -0,0 +1,38 @@ +'use client' +import React from 'react' +import Image from 'next/image' +import Link from 'next/link' +import { usePathname } from 'next/navigation' +import { ic_logo_item_pc, ic_user } from '@/public/common' + +export default function Layout({ children }: { children: React.ReactNode }) { + const pathName = usePathname() + const activeButton = pathName.indexOf('items') + return ( + + +
+
+
+ + pandaLogo + +
+ + 자유 게시판 + + + 중고 마켓 + +
+
+ + userInfo + +
+
+ {children} + + + ) +} diff --git a/app/items/page.tsx b/app/items/page.tsx new file mode 100644 index 000000000..9d155c471 --- /dev/null +++ b/app/items/page.tsx @@ -0,0 +1,18 @@ +import '@/styles/css/style.css' +import BestItemDetail from '@/components/BestItems' +import ItemDetail from '@/components/ItemDetail' + +function Items() { + return ( +
+
+ +
+
+ +
+
+ ) +} + +export default Items diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 000000000..6f186441b --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,13 @@ +'use client' +import { Provider } from 'react-redux' +import store from '@/store/store' + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + + ) +} diff --git a/app/login/page.tsx b/app/login/page.tsx new file mode 100644 index 000000000..562c7dbb6 --- /dev/null +++ b/app/login/page.tsx @@ -0,0 +1,141 @@ +'use client' +import React, { ChangeEvent, FocusEvent, useCallback, useEffect, useState } from 'react' +import * as validation from '@/components/Validation' +import '@/styles/css/style.css' +import Image from 'next/image' +import Link from 'next/link' +import { TextInput } from '@/components/index' +import { useRouter } from 'next/navigation' +import { Button } from '@headlessui/react' +import { panda, ic_google, ic_kakao } from '@/public/common' + +const Login = () => { + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [loginDisabled, setLoginDisabled] = useState(true) + const [emailNotice, setEmailNotice] = useState('') + const [pwdNotice, setPwdNotice] = useState('') + const [passwordType, setPasswordType] = useState('password') + const { emailValidationMsg, inputEmptyCheck } = validation + const router = useRouter() + + const showPassword = () => { + const returnType = passwordType.length > 0 ? '' : 'password' + setPasswordType(returnType) + } + + const handleInputBlur = useCallback( + (inputType: string) => (e: FocusEvent) => { + const inputValue = e.target.value + let noticeMsg = '' + if (inputType === 'email') { + noticeMsg = emailValidationMsg(inputValue) + setEmailNotice(noticeMsg) + } else { + noticeMsg = validation.pwdValidationMsg({ pwd: inputValue }) + setPwdNotice(noticeMsg) + } + + // 알림 활성화 + if (noticeMsg.length > 0) { + e.target.classList.add('input-notice') + } else { + e.target.classList.remove('input-notice') + } + }, + [emailValidationMsg] + ) + + const handleEmailChange = (e: ChangeEvent) => { + setEmail(e.target.value) + } + + const handlePasswordChange = (e: ChangeEvent) => { + setPassword(e.target.value) + } + + const handleMovePage = (value: string) => { + router.push(value) + } + + useEffect(() => { + const checkInput = inputEmptyCheck([email, password]) + const checkNotice = inputEmptyCheck([emailNotice, pwdNotice]) + if (checkInput && !checkNotice) { + setLoginDisabled(true) + } else { + setLoginDisabled(false) + } + }, [email, emailNotice, inputEmptyCheck, password, pwdNotice]) + + return ( +
+
+
+
+ panda + 판다마켓 +
+
+
+ +

이메일

+
+ + {emailNotice} + +
+ +

비밀번호

+
+
+ +
+
+
+ 간편 로그인하기 +
+ + google + + + kakao + +
+
+
+
+
+ 판다마켓이 처음이신가요? + + 회원가입 + +
+
+
+
+
+ ) +} + +export default Login diff --git a/app/noticeBoard/page.tsx b/app/noticeBoard/page.tsx new file mode 100644 index 000000000..3b59ba068 --- /dev/null +++ b/app/noticeBoard/page.tsx @@ -0,0 +1,8 @@ +'use client' +import React from 'react' + +function NoticeBoard() { + return
+} + +export default NoticeBoard diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 000000000..302619b39 --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,142 @@ +'use client' +import React from 'react' +import '@/styles/css/style.css' +import Image from 'next/image' +import { useRouter } from 'next/navigation' +import { logo_pc, login_bt, since, privacy_policy, ic_faq, ic_facebook, ic_instagram, ic_twitter, ic_youtube } from '@/public/common' +import { pc_content_hot_item, pc_content_search, pc_content_register } from '@/public/main' + +function Home() { + const router = useRouter() + + const handleMovePage = (path: string) => { + router.push(path) + } + + return ( + <> +
+
+ Home handleMovePage('/')} /> + 로그인 handleMovePage('/login')} /> +
+
+
+
+
+

+ 일상의 모든 물건을 +
+ 거래해 보세요 +

+ +
+
+
+
+
+ hotItem +
+

Hot item

+

+ 인기 상품을 +
+ 확인해 보세요 +

+

+ 가장 HOT한 중고거래 물품을 + +
+
+ 판다 마켓에서 확인해 보세요 +

+
+
+
+
+
+
+

Search

+

+ 구매를 원하는 + +
+
+ 상품을 검색하세요 +

+

+ 구매하고 싶은 물건은 검색해서 + +
+
+ 쉽게 찾아보세요 +

+
+ search +
+
+
+
+ register +
+

Register

+

+ 판매를 원하는 + +
+
+ 상품을 등록하세요 +

+

+ 어떤 물건이든 판매하고 싶은 상품을 + +
+
+ 쉽게 등록하세요 +

+
+
+
+
+
+
+ 믿을 수 있는 + +
+
+ 판다마켓 중고 거래 +
+
+
+
+
+
+
+
+
+ since +
+
+
+
+
+ policy handleMovePage('/Privacy')} /> + faq handleMovePage('/Fqa')} /> +
+
+
+
+ facebook + instagram + twitter + youtube +
+
+
+ + ) +} + +export default Home diff --git a/app/privacy/page.tsx b/app/privacy/page.tsx new file mode 100644 index 000000000..47fe893a0 --- /dev/null +++ b/app/privacy/page.tsx @@ -0,0 +1,8 @@ +'use client' +import React from 'react' + +function Privacy() { + return
privacy
+} + +export default Privacy diff --git a/app/signin/page.tsx b/app/signin/page.tsx new file mode 100644 index 000000000..d4f9f5658 --- /dev/null +++ b/app/signin/page.tsx @@ -0,0 +1,8 @@ +'use client' +import React from 'react' + +function Signin() { + return
+} + +export default Signin diff --git a/app/signup/page.tsx b/app/signup/page.tsx new file mode 100644 index 000000000..ab87dce7f --- /dev/null +++ b/app/signup/page.tsx @@ -0,0 +1,150 @@ +'use client' +import React, { ChangeEvent, FocusEvent, useCallback, useState } from 'react' +import '@/styles/css/style.css' +import * as validation from '@/components/Validation' +import Link from 'next/link' +import Image from 'next/image' +import { Button, TextInput } from '@/components/index' +import { useRouter } from 'next/navigation' +import { panda, ic_google, ic_kakao } from '@/public/common' +import Password from '@/components/Password' + +type PasswordType = { password: string; pwdConfirm: string; pwdNotice: string; pwdConfirmNotice: string } + +function Signup() { + const router = useRouter() + const { emailValidationMsg, inputEmptyCheck } = validation + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [pwdConfirm, setPwdConfirm] = useState('') + const [nickname, setNickname] = useState('') + const [loginDisabled, setLoginDisabled] = useState(true) + const [emailNotice, setEmailNotice] = useState('') + const [nicknameNotice, setNicknameNotice] = useState('') + const [pwdNotice, setPwdNotice] = useState('') + const [pwdConfirmNotice, setPwdConfirmNotice] = useState('') + + const loginButtonState = useCallback(() => { + const checkInput = inputEmptyCheck([email, nickname, password, pwdConfirm]) + const checkNotice = inputEmptyCheck([emailNotice, nicknameNotice, pwdNotice, pwdConfirmNotice]) + const newLoginDisabled = !(checkInput && !checkNotice) + + // 상태 변경이 필요할 때만 setLoginDisabled 호출 + if (newLoginDisabled !== loginDisabled) { + setLoginDisabled(newLoginDisabled) + } + }, [email, emailNotice, inputEmptyCheck, loginDisabled, nickname, nicknameNotice, password, pwdConfirm, pwdConfirmNotice, pwdNotice]) + + const handleInputBlur = useCallback( + (inputType: string) => (e: FocusEvent) => { + const inputValue = e.target.value + let noticeMsg = '' + if (inputType === 'email') { + noticeMsg = emailValidationMsg(inputValue) + setEmailNotice(noticeMsg) + } else { + if (inputValue.length < 1) noticeMsg = '닉네임을 입력해주세요' + setNicknameNotice(noticeMsg) + } + + // 알림 활성화 + if (noticeMsg.length > 0) { + e.target.classList.add('input-notice') + } else { + e.target.classList.remove('input-notice') + } + loginButtonState() + }, + [emailValidationMsg, loginButtonState] + ) + + const handleEmailChange = (e: ChangeEvent) => { + setEmail(e.target.value) + } + const handleNicknameChange = (e: ChangeEvent) => { + setNickname(e.target.value) + } + + const handlePwdChange = useCallback( + ({ password, pwdConfirm, pwdNotice, pwdConfirmNotice }: PasswordType) => { + setPassword(password) + setPwdConfirm(pwdConfirm) + setPwdNotice(pwdNotice) + setPwdConfirmNotice(pwdConfirmNotice) + loginButtonState() + }, + [loginButtonState] + ) + + const loginClick = () => { + router.push('/signin') + } + + return ( +
+
+
+
+ panda + 판다마켓 +
+
+
+ handleInputBlur('email')} + > +

이메일

+
+ + {emailNotice} + + handleInputBlur('nickname')} + > +

닉네임

+
+ + {nicknameNotice} + + +
+ +
+
+
+ 간편 로그인하기 +
+ + google + + + kakao + +
+
+
+
+
+ 이미 회원이신가요? + + 로그인 + +
+
+
+
+
+ ) +} + +export default Signup diff --git a/app/userInfo/page.tsx b/app/userInfo/page.tsx new file mode 100644 index 000000000..32293baeb --- /dev/null +++ b/app/userInfo/page.tsx @@ -0,0 +1,8 @@ +'use client' +import React from 'react' + +function UserInfo() { + return
+} + +export default UserInfo diff --git a/components/BestItems.tsx b/components/BestItems.tsx new file mode 100644 index 000000000..a2f11cae2 --- /dev/null +++ b/components/BestItems.tsx @@ -0,0 +1,50 @@ +'use client' +import { getBestItem, useAppDispatch } from '@/service/reducerSlice' +import React, { useEffect } from 'react' +import { deviceBestItemCount } from '@/utils/initialDevice' +import { useSelector } from 'react-redux' +import { InitialStateType } from '@/store/store' +import { useRouter } from 'next/navigation' +import { ic_heart, ic_X } from '@/public/common' +import Image from 'next/image' + +const BestItemDetail = () => { + const dispatch = useAppDispatch() + const bestItems = useSelector((state: InitialStateType) => state.data.bestItems || []) + const router = useRouter() + + const handleDetail = (id: number) => { + router.push(`/items/${id}`) + } + + useEffect(() => { + dispatch(getBestItem({ page: '1', pageSize: deviceBestItemCount().toString(), orderBy: 'favorite' })) + }, [dispatch]) + + return ( + <> +
+

베스트 상품

+
+
+ {bestItems.list.map(item => ( +
+
+
+ itemImg handleDetail(item.id)} /> +
+ {item.name} + {`${item.price.toLocaleString()}원`} +
+ heart + {item.favoriteCount} +
+
+
+ ))} +
+ + ) +} + +export default BestItemDetail diff --git a/components/Button.tsx b/components/Button.tsx new file mode 100644 index 000000000..6aa81e91c --- /dev/null +++ b/components/Button.tsx @@ -0,0 +1,18 @@ +'use client' +import React, { ReactNode } from 'react' +interface ButtonStyle { + className: string + disabled?: boolean + onClick?: () => void + children?: ReactNode +} + +const Button = ({ className, disabled = false, onClick, children, ...props }: ButtonStyle) => { + return ( + + ) +} + +export default Button diff --git a/components/Comments.tsx b/components/Comments.tsx new file mode 100644 index 000000000..a72adfcc3 --- /dev/null +++ b/components/Comments.tsx @@ -0,0 +1,92 @@ +'use client' +import React, { ChangeEvent, useCallback, useEffect, useState } from 'react' +import { VerticalSelect, Textarea, Button, UserIconInfo } from '@/components/index' +import { getAxios } from '@/utils/api' +import { ic_return } from '@/public/common' +import Link from 'next/link' +import Image from 'next/image' + +interface CommentsType { + id: number + limit?: number +} + +interface CommentsDataType { + list: Array<{ id: number; content: string; writer: { image: string; nickname: string }; updatedAt: string }> + nextCursor: 0 +} +function Comments({ id, limit = 10 }: CommentsType) { + const [commentData, setCommentData] = useState({ list: [], nextCursor: 0 }) + const [inquiry, setInquiry] = useState('') + + const getComments = useCallback(async () => { + const res = await getAxios({ + path: `${process.env.NEXT_PUBLIC_API_URL}/${id}/comments`, + params: { limit }, + }) + + const response = res as { status: number; data: CommentsDataType } + if (response.status === 200) { + setCommentData(response.data) + } else { + alert('댓글 조회 오류!!') + } + }, [id, limit]) + + const handleSelectChagne = (e: ChangeEvent) => { + // 코멘트 수정 및 삭제 기능 추가 예정 + console.log(e.target.value, inquiry) + } + const handleInquiryChange = (e: ChangeEvent) => { + setInquiry(e.target.value) + } + const handleItamSubmit = () => { + console.log('문의글 저장 기능 추가 예정') + } + + useEffect(() => { + getComments() + }, [getComments]) + + return ( + <> +

문의하기

+