diff --git a/.env b/.env new file mode 100644 index 000000000..20b6ae1c8 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +NEXT_PUBLIC_ACCESS_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NDkzLCJzY29wZSI6ImFjY2VzcyIsImlhdCI6MTczMzU2NzgwMywiZXhwIjoxNzMzNTY5NjAzLCJpc3MiOiJzcC1wYW5kYS1tYXJrZXQifQ.15l2pH_P-kh0tMemBn521wnFwugH38QssNljRzNa3Qo" \ No newline at end of file diff --git a/api/commentApi.ts b/api/commentApi.ts new file mode 100644 index 000000000..7a00f56ee --- /dev/null +++ b/api/commentApi.ts @@ -0,0 +1,59 @@ +import axios, { AxiosResponse } from 'axios'; +import axiosInstance from './axios'; + +interface GetCommentResultResponse { + list: Comment[]; + nextCursor: string; +} + +interface CreateCommentResultResponse {} + +// -- Comment API +export const getComment = async ( + productId: number, + params: {}, +): Promise> => { + try { + return await axiosInstance.get(`/products/${productId}/comments`, { + params: { + productId: productId, + limit: 10, + cursor: null, + ...params, + }, + }); + } catch (error) { + if (axios.isAxiosError(error)) { + console.error('Axios Error:', error.response?.data || error.message); + throw new Error('데이터를 불러오는도중 에러가 발생했습니다.'); + } else { + console.error('Unknown Error:', error); + throw new Error('기타 에러입니다.'); + } + } +}; + +export const createComments = async ( + productId: number, + content: { content: string }, +) => { + try { + return await axiosInstance.post( + `/products/${productId}/comments`, + content, + { + headers: { + Authorization: `Bearer ${process.env.NEXT_PUBLIC_ACCESS_TOKEN}`, + }, + }, + ); + } catch (error) { + if (axios.isAxiosError(error)) { + console.error('Axios Error:', error.response?.data || error.message); + throw new Error('데이터를 불러오는도중 에러가 발생했습니다.'); + } else { + console.error('Unknown Error:', error); + throw new Error('기타 에러입니다.'); + } + } +}; diff --git a/api/imageApi.ts b/api/imageApi.ts new file mode 100644 index 000000000..c8a3904ca --- /dev/null +++ b/api/imageApi.ts @@ -0,0 +1,34 @@ +import axios, { AxiosResponse } from 'axios'; +import axiosInstance from './axios'; + +interface UploadImageResponse { + url: string; +} + +export const uploadImage = async ( + file: File, +): Promise => { + const formData = new FormData(); + formData.append('image', file); + + try { + const response: AxiosResponse = await axiosInstance.post( + '/images/upload', + formData, + { + headers: { + 'Content-Type': 'multipart/form-data', + Authorization: `Bearer ${process.env.NEXT_PUBLIC_ACCESS_TOKEN}`, + }, + }, + ); + console.log('파일 업로드 성공:', response.data); + return response.data; + } catch (error: unknown) { + if (axios.isAxiosError(error)) { + console.error('파일 업로드 실패:', error.response?.data || error.message); + } else { + console.error('알 수 없는 오류 발생:', error); + } + } +}; diff --git a/api/productApi.ts b/api/productApi.ts index baafee8f1..dd3191735 100644 --- a/api/productApi.ts +++ b/api/productApi.ts @@ -1,32 +1,30 @@ -import { AxiosResponse } from 'axios'; +import axios, { AxiosResponse } from 'axios'; import axiosInstance from './axios'; +import { Product } from '@/components/Card'; +import { Comment } from '@/components/Comment'; export type OrderType = 'recent' | 'favorite'; -export interface Product { +interface ProductResultResponse { + totalCount: number; // 전체 상품 수 + list: Product[]; // 상품 목록 +} + +export interface GetProduct { page: number; pageSize: number; - orderBy: OrderType; + orderBy: string; keyword?: string; } -export interface ProductResult { - createdAt: string; - favoriteCount: number; - ownerNickname: string; - ownerId: number; +export interface CreateProductProps { + name: string; + description: string; images: string[]; tags: string[]; price: number; - description: string; - name: string; - id: number; -} - -interface ProductResultResponse { - totalCount: number; // 전체 상품 수 - list: ProductResult[]; // 상품 목록 } +// -- Product API // 상품목록가져오기 export const getProducts = ( @@ -42,12 +40,28 @@ export const getProducts = ( }); }; -export const getProductById = (productId: string) => { - return axiosInstance.get(`/products/${productId}`); +export const getProductById = async (productId: number) => { + try { + return await axiosInstance.get(`/products/${productId}`); + } catch (error) { + if (axios.isAxiosError(error)) { + console.error('Axios Error:', error.response?.data || error.message); + throw new Error('데이터를 불러오는도중 에러가 발생했습니다.'); + } else { + console.error('Unknown Error:', error); + throw new Error('기타 에러입니다.'); + } + } }; -export const createProduct = (productData: Product) => { - return axiosInstance.post('/products', productData); +export const createProduct = ( + productData: CreateProductProps, +): Promise => { + return axiosInstance.post('/products', productData, { + headers: { + Authorization: `Bearer ${process.env.NEXT_PUBLIC_ACCESS_TOKEN}`, + }, + }); }; export const updateProduct = ( diff --git a/components/Card.tsx b/components/Card.tsx index 95eb82ad8..8ac129874 100644 --- a/components/Card.tsx +++ b/components/Card.tsx @@ -4,11 +4,23 @@ import Image from 'next/image'; import styles from './Card.module.css'; import heartIcon from '@/public/ic_heart.svg'; import profile from '@/public/profile.svg'; -import { ProductResult } from '@/api/productApi'; import Link from 'next/link'; +export interface Product { + createdAt: string; + favoriteCount: number; + ownerNickname: string; + ownerId: number; + images: string[]; + tags: string[]; + price: number; + description: string; + name: string; + id: number; +} + interface CardProps { - products: ProductResult; + products: Product; } const Card: React.FC = ({ products }) => { @@ -17,8 +29,8 @@ const Card: React.FC = ({ products }) => { return ( <> -
- + +
{name}
@@ -30,29 +42,29 @@ const Card: React.FC = ({ products }) => { >
- -
-
- 프로파일이미지 -
{ownerNickname}
-
{createdAt}
-
-
- 좋아요버튼 -
{favoriteCount}+
+
+
+ 프로파일이미지 +
{ownerNickname}
+
{createdAt}
+
+
+ 좋아요버튼 +
{favoriteCount}+
+
-
+ ); }; diff --git a/components/Comment.module.css b/components/Comment.module.css new file mode 100644 index 000000000..761e65e75 --- /dev/null +++ b/components/Comment.module.css @@ -0,0 +1,44 @@ +.comment { + padding-top: 4rem; + border-bottom: 1px solid #e5e7eb; + background-color: #fcfcfc; +} + +.comment-top { + display: flex; + justify-content: space-between; +} + +.content { + font-weight: 400; + font-size: 1.4rem; + line-height: 2.4rem; + color: #1f2937; +} + +.comment-bottom { + padding: 2.4rem 1.2rem; + display: flex; + align-items: center; + gap: 0.8rem; +} + +.profile { + display: flex; + flex-direction: column; + gap: 0.4em; +} + +.nickName { + font-weight: 400; + font-size: 1.2rem; + line-height: 1.8rem; + color: #4b5563; +} + +.date { + font-weight: 400; + font-size: 1.2rem; + line-height: 1.8rem; + color: #9ca3af; +} diff --git a/components/Comment.tsx b/components/Comment.tsx new file mode 100644 index 000000000..b6a8f4817 --- /dev/null +++ b/components/Comment.tsx @@ -0,0 +1,50 @@ +import Image from 'next/image'; +import React from 'react'; +import profile from '@/public/profile.svg'; +import Iconkebab from '@/public/ic_kebab.svg'; +import styles from './Comment.module.css'; + +interface Writer { + image: string; + nickname: string; + id: number; +} + +export interface Comment { + writer: Writer; + updatedAt: string; + createdAt: string; + content: string; + id: number; +} + +export interface CommentProps { + comment: Comment; +} + +const Comment: React.FC = ({ comment }) => { + return ( +
+
+
{comment.content}
+ +
+
+
+ 프로파일이미지 +
+
+
{comment.writer.nickname}
+
{comment.createdAt}
+
+
+
+ ); +}; + +export default Comment; diff --git a/components/common/Button.tsx b/components/common/Button.tsx index 7c88dddab..b3e71bb8b 100644 --- a/components/common/Button.tsx +++ b/components/common/Button.tsx @@ -1,18 +1,17 @@ import { ReactNode } from 'react'; import styles from './Button.module.css'; -interface ButtonProps { +interface ButtonProps extends React.ButtonHTMLAttributes { children?: ReactNode; addClassName?: string | string[]; handleClick?: () => void; - disabled: boolean; } function Button({ children, addClassName, handleClick, - disabled, + ...props }: ButtonProps) { const buttonClass = Array.isArray(addClassName) ? addClassName.join(' ') @@ -24,7 +23,7 @@ function Button({ type="button" className={`${styles.button} ${buttonClass}`} onClick={handleClick} - disabled={disabled} + {...props} > {children} diff --git a/components/common/Dropdown.tsx b/components/common/Dropdown.tsx index 02dec8c06..d1bdbe84d 100644 --- a/components/common/Dropdown.tsx +++ b/components/common/Dropdown.tsx @@ -6,10 +6,10 @@ import { ScreenType } from '@/hooks/useResize'; import sortImage from '@/public/ic_sort.svg'; import { OrderType } from '@/api/productApi'; -type DropdownItem = { +export type DropdownItem = { id: number; label: string; - value: string; + value: OrderType; }; interface DropdownProps { diff --git a/components/common/FileInput.module.css b/components/common/FileInput.module.css new file mode 100644 index 000000000..5a22ae696 --- /dev/null +++ b/components/common/FileInput.module.css @@ -0,0 +1,39 @@ +.fileInput { + display: none; +} + +.image { + width: 16.8rem; + height: 16.8rem; + border-radius: 1.2rem; + background-color: #f3f4f6; +} + +.image-block { + padding: 4.1rem 4.7rem; + display: flex; + align-items: center; + gap: 1.2rem; + flex-direction: column; +} + +.image-txt { + font-size: 1.6rem; + line-height: 2.6rem; + font-weight: 400; + color: #9ca3af; +} + +@media (min-width: 768px) { +} + +@media (min-width: 1024px) { + .image { + width: 28.2rem; + height: 28.2rem; + } + + .image-block { + padding: 9.8rem 10.4rem; + } +} diff --git a/components/common/FileInput.tsx b/components/common/FileInput.tsx new file mode 100644 index 000000000..136ed94f3 --- /dev/null +++ b/components/common/FileInput.tsx @@ -0,0 +1,72 @@ +import { uploadImage } from '@/api/imageApi'; +import Image from 'next/image'; +import React, { useRef, useState } from 'react'; +import styles from '@/components/common/FileInput.module.css'; +import IconPlus from '@/public/ic_plus.svg'; + +interface FileInputProps { + onChangeFile: (url: string) => void; +} + +const FileInput: React.FC = ({ onChangeFile }) => { + const [image, setImage] = useState(null); + const [preview, setPreview] = useState(null); + const fileInputRef = useRef(null); + + const handleImageChange = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + + if (file) { + setImage(file); + + const reader = new FileReader(); + reader.onloadend = () => { + setPreview(reader.result as string); + }; + reader.readAsDataURL(file); + + // upload + try { + const data = await uploadImage(file); + if (data?.url) onChangeFile(data?.url); + } catch (error) { + console.error('파일 업로드 실패:', error); + } + } + }; + + return ( + <> + +
fileInputRef.current?.click()} + > +
+ {preview ? ( + 미리보기 + ) : ( + <> + 이미지 등록 +
이미지 등록
+ + )} +
+
+ + ); +}; + +export default FileInput; diff --git a/components/common/Search.module.css b/components/common/Input.module.css similarity index 82% rename from components/common/Search.module.css rename to components/common/Input.module.css index e31af968e..7232c6a7b 100644 --- a/components/common/Search.module.css +++ b/components/common/Input.module.css @@ -1,4 +1,4 @@ -.search-container { +.input-container { position: relative; display: flex; align-items: center; @@ -11,13 +11,13 @@ } /* 검색 아이콘 스타일 */ -.search-icon { +.input-icon { position: absolute; left: 2rem; pointer-events: none; } -.search-input { +.input-input { width: 80%; font-size: 1.4rem; border: none; @@ -25,13 +25,13 @@ background-color: #f3f4f6; } -.search-input::placeholder { +.input-input::placeholder { color: #9ca3af; font-size: 1.6rem; font-weight: 400; line-height: 2.6rem; } -.search-input:focus::placeholder { +.input-input:focus::placeholder { color: transparent; /* 포커스 시 placeholder 텍스트 숨기기 */ } diff --git a/components/common/Input.tsx b/components/common/Input.tsx new file mode 100644 index 000000000..60288b0b9 --- /dev/null +++ b/components/common/Input.tsx @@ -0,0 +1,53 @@ +import { FormEvent, InputHTMLAttributes, ReactNode, useState } from 'react'; +import styles from './Input.module.css'; + +interface InputProps extends InputHTMLAttributes { + onChange?: (e: React.ChangeEvent) => void; + onInput?: (query: string) => void; + image?: boolean; + addClassName?: string | string[]; + children?: ReactNode; +} + +const Input = ({ + onChange, + image, + addClassName, + children, + onInput, + ...props +}: InputProps) => { + const inputClass = Array.isArray(addClassName) + ? addClassName.join(' ') + : addClassName || ''; + + const [query, setQuery] = useState(''); + + const handleInputChange = (event: React.ChangeEvent) => { + const value = event.target.value; + setQuery(value); + if (onChange) onChange(event); + }; + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Enter' && query.trim()) { + if (onInput) onInput(query); + } + }; + + return ( +
+ {image && <> {children} } + +
+ ); +}; + +export default Input; diff --git a/components/common/Layout.tsx b/components/common/Layout.tsx index e5b757e3c..ca62f2787 100644 --- a/components/common/Layout.tsx +++ b/components/common/Layout.tsx @@ -1,5 +1,5 @@ import React, { ReactNode } from 'react'; -import Header from '../Header'; +import Header from '@/components/Header'; import styles from './Layout.module.css'; import Head from 'next/head'; diff --git a/components/common/Search.tsx b/components/common/Search.tsx deleted file mode 100644 index 79a975034..000000000 --- a/components/common/Search.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { useState } from 'react'; -import styles from './Search.module.css'; -import Image from 'next/image'; -import searchIcon from '@/public/ic_search.svg'; - -interface SearchProps { - onSearch: (query: string) => void; - addClassName?: string; -} - -const Search = ({ onSearch, addClassName }: SearchProps) => { - const [query, setQuery] = useState(''); - - const handleInputChange = (event: React.ChangeEvent) => { - const value = event.target.value; - setQuery(value); - onSearch(value); - }; - - const handleKeyDown = (event: React.KeyboardEvent) => { - if (event.key === 'Enter' && query.trim()) { - onSearch(query); - } - }; - - return ( -
- {/* TODO search 컴포넌트 css 공통화 */} -
- 검색 아이콘 -
- -
- ); -}; - -export default Search; diff --git a/hooks/useResize.ts b/hooks/useResize.ts index d73000c5b..49ef189bc 100644 --- a/hooks/useResize.ts +++ b/hooks/useResize.ts @@ -1,30 +1,32 @@ +import debounce from 'lodash.debounce'; import { useState, useEffect } from 'react'; export type ScreenType = 'mobile' | 'tablet' | 'desktop' | null; +const BREAKPOINTS = { + mobile: 0, + tablet: 768, + desktop: 1024, +} as const; + const useResize = () => { const [screenType, setScreenType] = useState(null); const [isClient, setIsClient] = useState(false); useEffect(() => { - // 클라이언트에서만 실행되도록 설정, 서버에서는 window객체 x - setIsClient(true); - }, []); - - useEffect(() => { - if (!isClient) return; + if (typeof window === 'undefined') return; - const handleResize = () => { + const handleResize = debounce(() => { const currentWidth = window.innerWidth; - if (currentWidth >= 1024) { + if (currentWidth >= BREAKPOINTS.desktop) { setScreenType('desktop'); - } else if (currentWidth >= 768) { + } else if (currentWidth >= BREAKPOINTS.tablet) { setScreenType('tablet'); } else { setScreenType('mobile'); } - }; + }, 250); window.addEventListener('resize', handleResize); handleResize(); // 초기 크기 설정 diff --git a/next.config.js b/next.config.mjs similarity index 86% rename from next.config.js rename to next.config.mjs index 5d372d61b..e9c88a74a 100644 --- a/next.config.js +++ b/next.config.mjs @@ -1,3 +1,4 @@ +// next.config.mjs /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, @@ -14,4 +15,4 @@ const nextConfig = { }, }; -module.exports = nextConfig; +export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 4617f6181..031f74c5b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,8 @@ "lodash.debounce": "^4.0.8", "next": "13.5.6", "react": "^18", - "react-dom": "^18" + "react-dom": "^18", + "sharp": "^0.33.5" }, "devDependencies": { "@types/lodash.debounce": "^4.0.9", @@ -45,6 +46,16 @@ "node": ">=6.9.0" } }, + "node_modules/@emnapi/runtime": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", + "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -134,6 +145,367 @@ "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@next/env": { "version": "13.5.6", "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.6.tgz", @@ -874,11 +1246,23 @@ "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -889,8 +1273,17 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } }, "node_modules/combined-stream": { "version": "1.0.8", @@ -1008,6 +1401,15 @@ "node": ">=6" } }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -2100,6 +2502,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, "node_modules/is-async-function": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", @@ -2565,18 +2973,6 @@ "loose-envify": "cli.js" } }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -3241,13 +3637,10 @@ } }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -3284,6 +3677,45 @@ "node": ">= 0.4" } }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -3319,6 +3751,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -3785,12 +4226,6 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 301bef6ea..c7b9c0e12 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "lodash.debounce": "^4.0.8", "next": "13.5.6", "react": "^18", - "react-dom": "^18" + "react-dom": "^18", + "sharp": "^0.33.5" }, "devDependencies": { "@types/lodash.debounce": "^4.0.9", diff --git a/pages/_document.tsx b/pages/_document.tsx index 31671fc6b..f37fe3ea0 100644 --- a/pages/_document.tsx +++ b/pages/_document.tsx @@ -11,10 +11,10 @@ export default function Document() { as="style" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable.min.css" /> - + /> */}
diff --git a/pages/addboard.tsx b/pages/addboard.tsx new file mode 100644 index 000000000..601726954 --- /dev/null +++ b/pages/addboard.tsx @@ -0,0 +1,97 @@ +import React, { useState } from 'react'; +import styles from '@/styles/Addboard.module.css'; +import Button from '@/components/common/Button'; +import Input from '@/components/common/Input'; +import useResize from '@/hooks/useResize'; +import FileInput from '@/components/common/FileInput'; +import { createProduct, CreateProductProps } from '@/api/productApi'; + +const Addboard = () => { + const screenType = useResize(); // useResize 훅 사용 + + const [inputData, setInputData] = useState({ + name: '', + description: '', + images: [], + price: 0, + tags: ['1', '2'], + }); + + const handleInputChange = (e: React.ChangeEvent): void => { + const { name, value } = e.target; + setInputData((prevData) => ({ + ...prevData, + [name]: value, + })); + }; + + const onChangeFile = (url: string) => { + setInputData((prevData) => ({ + ...prevData, + images: [url], + })); + }; + + const getTextTitle = () => { + return screenType === 'desktop' ? '게시글 쓰기' : '상품 등록하기'; + }; + + const isFormValid = Object.values(inputData).every((value) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return value !== ''; + }); + + const handleRegistClick = async () => { + try { + await createProduct(inputData); + } catch (error) { + console.error(error); + } + }; + + return ( + <> +
+
+

{getTextTitle()}

+ {/* 버튼 css 안됨 */} + +
+
+
+

*제목

+ +
+
+

*내용

+ +
+
+

이미지

+ +
+
+
+ + ); +}; + +export default Addboard; diff --git a/pages/boards/[id].tsx b/pages/boards/[id].tsx index 44285bf6e..8b977eaa3 100644 --- a/pages/boards/[id].tsx +++ b/pages/boards/[id].tsx @@ -1,7 +1,139 @@ -import React from 'react'; +import React, { use, useEffect, useState } from 'react'; +import Iconkebab from '@/public/ic_kebab.svg'; +import Image from 'next/image'; +import styles from '@/styles/BoardDetail.module.css'; +import profile from '@/public/profile.svg'; +import heartIcon from '@/public/ic_heart.svg'; +import Input from '@/components/common/Input'; +import { GetServerSidePropsContext } from 'next'; +import { getProductById } from '@/api/productApi'; +import Button from '@/components/common/Button'; +import Comment from '@/components/Comment'; +import { Product } from '@/components/Card'; +import { createComments, getComment } from '@/api/commentApi'; -const BoardDetail = () => { - return
; +export async function getServerSideProps(context: GetServerSidePropsContext) { + if (context.params) { + const productId = Number(context.params.id); + + try { + const detailResponse = await getProductById(productId); + const commentResponse = await getComment(productId, {}); + + return { + props: { + detailProduct: detailResponse.data, + detailCommentList: commentResponse.data.list, + }, + }; + } catch (error) { + console.error('데이터를 불러오는 데 실패했습니다:', error); + return { + props: { + detailProduct: {}, + detailCommentList: [], + }, + }; + } + } +} + +const BoardDetail = ({ + detailProduct, + detailCommentList, +}: { + detailProduct: Product; + detailCommentList: Comment[]; +}) => { + const [content, setContent] = useState(''); + const [buttonDisabled, setbuttonDisabled] = useState(false); + + const handleRegisterClick = async () => { + const productId = detailProduct.id; + + try { + const data = await createComments(productId, { content: content }); + } catch (error) { + console.error(error); + } + }; + + useEffect(() => { + setbuttonDisabled(!content); + }, [content]); + + return ( + <> +
+
+
+
+

{detailProduct.name}

+ +
+
+ 프로파일이미지 +
+
+ {detailProduct.ownerNickname} +
+
{detailProduct.createdAt}
+
+
+
+
+
+ 좋아요버튼 +
+ +{detailProduct.favoriteCount} +
+
+
+
+
+
+
{detailProduct.description}
+
+
+
+

댓글달기

+ setContent(e.target.value)} + /> +
+
+ +
+
+ {detailCommentList.map((comment) => { + return ( + <> + + + ); + })} +
+ + ); }; export default BoardDetail; diff --git a/pages/boards/index.tsx b/pages/boards/index.tsx index 128326a41..c242a7cb3 100644 --- a/pages/boards/index.tsx +++ b/pages/boards/index.tsx @@ -1,31 +1,65 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { FormEvent, useEffect, useMemo, useState } from 'react'; import styles from '@/styles/Board.module.css'; import BestCard from '@/components/BestCard'; import Button from '@/components/common/Button'; -import Search from '@/components/common/Search'; -import Card from '@/components/Card'; +import Card, { Product } from '@/components/Card'; import useResize, { ScreenType } from '@/hooks/useResize'; -import { - getProducts, - OrderType, - Product, - ProductResult, -} from '@/api/productApi'; -import Dropdown from '@/components/common/Dropdown'; +import { GetProduct, getProducts, OrderType } from '@/api/productApi'; +import Dropdown, { DropdownItem } from '@/components/common/Dropdown'; import debounce from 'lodash.debounce'; -import { usePathname, useSearchParams } from 'next/navigation'; import { useRouter } from 'next/router'; +import InputIcon from '@/public/ic_search.svg'; +import Input from '@/components/common/Input'; +import Image from 'next/image'; +import { GetServerSidePropsContext } from 'next'; -const items = [ +const items: DropdownItem[] = [ { id: 0, label: '최신순', value: 'recent' }, { id: 1, label: '좋아요순', value: 'favorite' }, ]; -const Board = () => { +export async function getServerSideProps(context: GetServerSidePropsContext) { + try { + const bestProductsResponse = await getProducts({ + orderBy: 'favorite', + page: 1, + pageSize: 1, + }); + const allProductsResponse = await getProducts({ + orderBy: 'recent', + page: 1, + pageSize: 10, + }); + + return { + props: { + initialBestProducts: bestProductsResponse.data.list, + initialAllProducts: allProductsResponse.data.list, + }, + }; + } catch (error) { + console.error('데이터를 불러오는 데 실패했습니다:', error); + return { + props: { + initialBestProducts: [], + initialAllProducts: [], + }, + }; + } +} + +const Board = ({ + initialBestProducts, + initialAllProducts, +}: { + initialBestProducts: Product[]; + initialAllProducts: Product[]; +}) => { const screenType = useResize(); // useResize 훅 사용 const [page, setPage] = useState(1); // 페이지 번호 - const [bestProducts, setBestProducts] = useState([]); - const [allProducts, setAllProducts] = useState([]); + const [bestProducts, setBestProducts] = + useState(initialBestProducts); + const [allProducts, setAllProducts] = useState(initialAllProducts); const [order, setOrder] = useState('recent'); const router = useRouter(); @@ -43,7 +77,7 @@ const Board = () => { return sizeMap[screenType] || 1; }; - const fetchBestProducts = async (param: Product): Promise => { + const fetchBestProducts = async (param: GetProduct): Promise => { try { const response = await getProducts(param); setBestProducts(response.data.list); @@ -66,8 +100,7 @@ const Board = () => { ); const handleClick = () => { - router.push(`/posts?query=테스트!`); - console.log('클릭'); + router.push('addboard'); }; const handleSearch = (query: string) => { @@ -83,7 +116,7 @@ const Board = () => { if (!screenType) return; const size = getSizeForScreenType(screenType); - const param: Product = { + const param = { page: page, pageSize: size, orderBy: 'favorite', @@ -119,7 +152,16 @@ const Board = () => {
- + +
+ 검색 아이콘 +
+ + + + + diff --git a/public/ic_plus.svg b/public/ic_plus.svg new file mode 100644 index 000000000..5bb9abf55 --- /dev/null +++ b/public/ic_plus.svg @@ -0,0 +1,4 @@ + + + + diff --git a/styles/Addboard.module.css b/styles/Addboard.module.css new file mode 100644 index 000000000..a6a43b65b --- /dev/null +++ b/styles/Addboard.module.css @@ -0,0 +1,47 @@ +.addboard-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 2.4rem; +} + +.classs { + justify-content: baseline; +} + +.addboard-header .buttonMiddle { + padding: 1.15rem 2.3rem; + background-color: #3692ff; +} + +.addboard-header .buttonMiddle.disabled { + background-color: #34d399; +} + +.addboard .inputTitle { + display: block; + padding: 1.6rem 2.4rem; + justify-content: left; +} + +.addboard .inputLarge { + height: 20rem; +} + +.addboard-content { + display: flex; + flex-direction: column; + gap: 1.6rem; +} + +.box { + display: flex; + flex-direction: column; + gap: 1.2rem; +} + +@media (min-width: 768px) { + .addboard .inputLarge { + height: 28.2rem; + } +} diff --git a/styles/BoardDetail.module.css b/styles/BoardDetail.module.css new file mode 100644 index 000000000..625e15dd9 --- /dev/null +++ b/styles/BoardDetail.module.css @@ -0,0 +1,98 @@ +.boardDetail { +} + +.board-detail-content { + display: flex; + gap: 1.6rem; + flex-direction: column; + margin-bottom: 3.2rem; +} + +.board-detail-header { + display: flex; + gap: 1.6rem; + flex-direction: column; + border-bottom: 1px solid #e5e7eb; +} + +.board-detail-title { + display: flex; +} + +.profile { + display: flex; + align-items: center; + gap: 1.6rem; + padding-bottom: 1.6rem; +} + +.profile-info { + display: flex; +} + +.nickName { + font-size: 1.4rem; + font-weight: 400; + color: #4b5563; + line-height: 2.4rem; +} +.date { + font-size: 1.4rem; + font-weight: 400; + line-height: 2.4rem; + color: #9ca3af; +} + +.heartCount { + display: flex; + align-items: center; + gap: 0.8rem; + padding: 0.3rem 1.2rem; + border-radius: 3.5rem; + border: 1px solid #e5e7eb; +} + +.heart-box { + display: flex; + gap: 1.6rem; +} + +.liner { + border: 0.1rem solid #e5e7eb; +} +.heartCountNum { + font-size: 1.6rem; + font-weight: 400; + line-height: 2.6rem; + color: #6b7280; +} + +.content { + font-weight: 400; + font-size: 1.4rem; + line-height: 2.6rem; + color: #1f2937; +} + +.boardDetail .board { + height: 10.4rem; + padding: 1.6rem 2.4rem; + justify-content: left; +} + +.comment { + display: flex; + flex-direction: column; + gap: 1.6rem; +} + +.comment-btn { + text-align: right; +} + +.comment .buttonDetail { + padding: 0.8rem 2.3rem; + background-color: #9ca3af; + color: #f3f4f6; + text-align: right; +} diff --git a/styles/globals.css b/styles/globals.css index 89239b332..0a29890f9 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -42,9 +42,22 @@ a { outline: none; } +h1 { + font-size: 2rem; + font-weight: 700; + line-height: 3.2rem; + color: #1f2937; +} + h2 { font-size: 1.8rem; font-weight: 700; line-height: 2.6rem; color: #1f2937; } + +h3 { + font-size: 1.4rem; + font-weight: 700; + line-height: 2.4rem; +} diff --git a/tsconfig.json b/tsconfig.json index 670224f3e..d07aa2fc3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "esnext", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true,