diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 000000000..33fc3c441 --- /dev/null +++ b/middleware.ts @@ -0,0 +1,20 @@ +import { NextResponse } from 'next/server'; +import { NextRequest } from 'next/server'; +import { ACCESS_TOKEN } from './src/api/apiType'; + +export function middleware(request: NextRequest) { + const jwt = request.cookies.get(ACCESS_TOKEN); + + if (jwt && request.nextUrl.pathname === '/login') { + return NextResponse.redirect(new URL('/', request.url)); + } + if (!jwt && request.nextUrl.pathname === '/addboard') { + return NextResponse.redirect(new URL('/login', request.url)); + } + return NextResponse.next(); +} + +export const config = { + matcher: ['/login', '/addboard'], +}; + diff --git a/next.config.js b/next.config.js index 3776bb093..8794808c0 100644 --- a/next.config.js +++ b/next.config.js @@ -1,6 +1,6 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - reactStrictMode: true, + reactStrictMode: false, }; module.exports = nextConfig; diff --git a/package-lock.json b/package-lock.json index 18c60e601..b0a1d08cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,12 +8,14 @@ "name": "fe-weekly-mission", "version": "0.1.0", "dependencies": { + "@hookform/error-message": "^2.0.1", "@types/js-cookie": "^3.0.6", "axios": "^1.7.2", "js-cookie": "^3.0.5", "next": "13.5.6", "react": "^18", - "react-dom": "^18" + "react-dom": "^18", + "react-hook-form": "^7.52.1" }, "devDependencies": { "@types/jest": "^29.5.12", @@ -722,6 +724,16 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@hookform/error-message": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hookform/error-message/-/error-message-2.0.1.tgz", + "integrity": "sha512-U410sAr92xgxT1idlu9WWOVjndxLdgPUHEB8Schr27C9eh7/xUnITWpCMF93s+lGiG++D4JnbSnrb5A21AdSNg==", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "react-hook-form": "^7.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", @@ -5776,6 +5788,21 @@ "react": "^18.2.0" } }, + "node_modules/react-hook-form": { + "version": "7.52.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.52.1.tgz", + "integrity": "sha512-uNKIhaoICJ5KQALYZ4TOaOLElyM+xipord+Ha3crEFhTntdLvWZqVY49Wqd/0GiVCA/f9NjemLeiNPjG7Hpurg==", + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/package.json b/package.json index cc365e685..25ce2e507 100644 --- a/package.json +++ b/package.json @@ -11,12 +11,14 @@ "test:watch": "jest --watch" }, "dependencies": { + "@hookform/error-message": "^2.0.1", "@types/js-cookie": "^3.0.6", "axios": "^1.7.2", "js-cookie": "^3.0.5", "next": "13.5.6", "react": "^18", - "react-dom": "^18" + "react-dom": "^18", + "react-hook-form": "^7.52.1" }, "devDependencies": { "@types/jest": "^29.5.12", diff --git a/pages/_app.tsx b/pages/_app.tsx index 2a2939c28..4c4bc53ed 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -5,7 +5,7 @@ import '../styles/Reset.css'; export default function App({ Component, pageProps }: AppProps) { return ( -
+
); diff --git a/pages/addboard/[id].tsx b/pages/addboard/[id].tsx deleted file mode 100644 index dd186363f..000000000 --- a/pages/addboard/[id].tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; - -const addboard = () => { - return
; -}; - -export default addboard; diff --git a/pages/board/[id].tsx b/pages/board/[id].tsx new file mode 100644 index 000000000..2ab8294e7 --- /dev/null +++ b/pages/board/[id].tsx @@ -0,0 +1,31 @@ +import { articleType } from '@/src/api/apiType'; +import { GetServerSideProps } from 'next'; +import { AxiosError } from 'axios'; +import React from 'react'; +import { getArticle } from '@/src/api/api'; +import { useRouter } from 'next/router'; +import Board from '@/src/components/Board'; +interface articleProps { + article: articleType; +} +const board: React.FC = ({ article }) => { + return ; +}; + +export const getServerSideProps: GetServerSideProps = async ( + context +) => { + const { id } = context.query; + + try { + const article = await getArticle(id as string); + return { + props: { article }, + }; + } catch (error) { + const err = error as AxiosError; + throw err; + } +}; +export default board; + diff --git a/pages/boards.tsx b/pages/boards.tsx index fef804d03..1228c7f35 100644 --- a/pages/boards.tsx +++ b/pages/boards.tsx @@ -1,6 +1,6 @@ import BestPostsContainer from '@/src/components/BestPostsContainer'; import TotalPosts from '@/src/components/TotalPosts'; -import React from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import style from '../styles/BoardFrame.module.css'; import { GetServerSideProps } from 'next'; import { AxiosError } from 'axios'; @@ -18,8 +18,9 @@ const boards: React.FC = ({ bestPosts }) => { ); }; + export const getServerSideProps: GetServerSideProps = async () => { - const URL = 'page=1&pageSize=3&orderBy=like'; + const URL = `page=1&pageSize=3&orderBy=like`; try { const bestPosts = await getBestPosts(URL); return { diff --git a/pages/index.tsx b/pages/index.tsx index 5b86e056d..b35d375dc 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,5 +1,117 @@ import React from 'react'; -export default function Home() { - return <>; -} +import schoolPanda from '@/src/img/schoolPanda.png'; +import hotItem from '@/src/img/hotIem.png'; +import search from '@/src/img/search.png'; +import bottomImage from '@/src/img/bottomImage.png'; +import registerImage from '@/src/img/registerImage.png'; +import instagramIcon from '@/src/img/instagram.png'; +import facebookIcon from '@/src/img/facebook.png'; +import twitterIcon from '@/src/img/twitter.png'; +import youtubeIcon from '@/src/img/youtube.png'; +import style from '@/styles/Main.module.css'; +import Link from 'next/link'; + +const MainPage = () => { + return ( + <> +
+
+
+

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

+ 구경하러 가기 +
+ 가방팬더 +
+
+
+ 인기상품 +
+

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

+

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

+
+
+
+
+
+
+

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

+

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

+
+ 검색 +
+
+
+
+ 등록이미지 +
+

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

+

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

+
+
+
+
+

+ 믿을 수 있는 +
+ 판다마켓 중고거래 +

+ 가방들고있는판다들 +
+
+ + + + ); +}; + +export default MainPage; diff --git a/pages/login.tsx b/pages/login.tsx new file mode 100644 index 000000000..835193505 --- /dev/null +++ b/pages/login.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import LoginFormContainer from '@/src/components/LoginFormContainer'; + +const LoginPage = () => { + return ; +}; + +export default LoginPage; + diff --git a/pages/signUp.tsx b/pages/signUp.tsx new file mode 100644 index 000000000..e80b76469 --- /dev/null +++ b/pages/signUp.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import SignUpContainer from '@/src/components/SignUpContainer'; + +const SignUpPage = () => { + return ; +}; + +export default SignUpPage; + diff --git a/public/favicon.ico b/public/favicon.ico index 718d6fea4..6b2a63e99 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/src/api/api.ts b/src/api/api.ts index 9c4502941..57e82dab0 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -1,10 +1,14 @@ import axios, { AxiosError, AxiosResponse } from 'axios'; import instance from './axios'; -import { articlesType, writingType } from './apiType'; -import { url } from 'inspector'; +import { + articlesType, + articleType, + commentsType, + commentType, + writingType, +} from './apiType'; import Cookies from 'js-cookie'; -import { form } from '../components/AddBoardForm'; -import { title } from 'process'; +import { formType } from '../components/AddBoardForm'; export const getBestPosts = async (params: string): Promise => { const URL = `/articles?${params}`; @@ -39,27 +43,17 @@ export const getTotalPosts = async (params: string): Promise => { throw error; } }; -export const tempSignUP = async () => { - const TEMP_URL = '/auth/signIn'; - try { - if (!Cookies.get('accessToken')) { - const response = await instance.post(TEMP_URL, { - email: 'leedong0225@icloud.com', - password: 'abcd1234', - }); - const { accessToken, refreshToken } = response.data; - Cookies.set('accessToken', accessToken); - Cookies.set('refreshToken', refreshToken); - } - } catch (error) {} -}; export const postImage = async (image: File | null) => { const URL = '/images/upload'; try { if (image) { - const response = await instance.post(URL, { image: image }); + const response = await instance.post( + URL, + { image: image }, + { headers: { 'Content-Type': 'multipart/form-data' } } + ); const imageUrl = response.data.url; return imageUrl; } @@ -67,14 +61,172 @@ export const postImage = async (image: File | null) => { } catch (error) {} }; -export const postArticles = async (formData: form, imageUrl: string) => { +export const postArticles = async (formData: formType, imageUrl: string) => { const URL = '/articles'; try { - const response = await instance.post(URL, { - image: imageUrl, - content: formData.content, - title: formData.title, - }); + await instance.post( + URL, + { + image: imageUrl, + content: formData.content, + title: formData.title, + }, + { + headers: { + 'Content-Type': 'application/json', + accept: 'application/json', + }, + } + ); } catch (error) {} }; +export const getArticle = async (articleId: string): Promise => { + const URL = `/articles/${articleId}`; + try { + const response: AxiosResponse = await instance.get(URL); + const articleData = response.data; + return articleData; + } catch (error) { + const err = error as AxiosError; + if (err.response) { + console.error('Response error:', err.response.status); + console.error('Response data:', err.response.data); + throw err; + } + console.error(error); + throw error; + } +}; + +export const getComments = async ( + articleId: string, + limit: number, + cursor?: number +): Promise => { + const URL = `/articles/${articleId}/comments?limit=${limit}${ + cursor ? `&cursor=${cursor}` : '' + }`; + try { + const response: AxiosResponse = await instance.get(URL, { + headers: {}, + }); + return response.data.list; + } catch (error) { + const err = error as AxiosError; + if (err.response) { + console.error('Response error:', err.response.status); + console.error('Response data:', err.response.data); + throw err; + } + console.error(error); + throw error; + } +}; + +export const postComment = async (articleId: string, content: string) => { + const URL = `/articles/${articleId}/comments`; + try { + await instance.post( + URL, + { content: content }, + { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + } + ); + } catch (error) { + const err = error as AxiosError; + + if (err.response) { + console.error('Response error:', err.response.status); + console.error('Response data:', err.response.data); + throw err; + } + console.error(error); + throw error; + } +}; + +export const postSignUp = async ( + email: string, + nickname: string, + password: string, + passwordConfirmation: string +) => { + const URL = `auth/signUp`; + + try { + await instance.post( + URL, + { + email: email, + nickname: nickname, + password: password, + passwordConfirmation: passwordConfirmation, + }, + { headers: { 'Content-Type': 'application/json' } } + ); + } catch (error) { + const err = error as AxiosError; + if (err.response) { + console.error('Response error:', err.response.status); + console.error('Response data:', err.response.data); + throw err; + } + console.error(error); + throw error; + } +}; + +export const postSignIn = async (email: string, password: string) => { + const URL = `auth/signIn`; + try { + const response = await instance.post( + URL, + { + email: email, + password: password, + }, + { headers: { 'Content-Type': 'application/json' } } + ); + + Cookies.set('accessToken', response.data.accessToken); + Cookies.set('refreshToken', response.data.refreshToken); + } catch (error) { + const err = error as AxiosError; + if (err.response) { + console.error('Response error:', err.response.status); + console.error('Response data:', err.response.data); + throw err; + } + console.error(error); + throw error; + } +}; + +export const postRefreshToken = async () => { + const URL = `auth/refresh-token`; + try { + const response = await instance.post( + URL, + { + refreshToken: Cookies.get('refreshToken'), + }, + { headers: { 'Content-Type': 'application/json' } } + ); + Cookies.set('accessToken', response.data.accessToken); + } catch (error) { + const err = error as AxiosError; + if (err.response) { + console.error('Response error:', err.response.status); + console.error('Response data:', err.response.data); + throw err; + } + console.error(error); + throw error; + } +}; + diff --git a/src/api/apiType.ts b/src/api/apiType.ts index a33dbe2f8..a545b34d8 100644 --- a/src/api/apiType.ts +++ b/src/api/apiType.ts @@ -1,20 +1,9 @@ -export interface articlesType { +export const ACCESS_TOKEN = 'accessToken'; +export const REFRESH_TOKEN = 'refreshToken'; + +export interface articlesType extends articleType { totalCount?: number; - list: [ - { - updatedAt: string; - createdAt: string; - likeCount: number; - writer: { - nickname: string; - id: number; - }; - image: string | null; - content: string; - title: string; - id: number; - } - ]; + list: articleType[]; } export interface writingType { @@ -31,3 +20,34 @@ export interface writingType { id: number; } +export interface articleType { + updatedAt: string; + createdAt: string; + likeCount: number; + writer: { + nickname: string; + id: number; + }; + image: string | null; + content: string; + title: string; + id: number; +} + +export interface commentsType extends commentType { + list: commentType[]; + nextCursor: number; +} + +export interface commentType { + id: number; + content: string; + createdAt: string; + updatedAt: string; + writer: { + id: number; + nickname: string; + image: string | null; + }; +} + diff --git a/src/api/axios.ts b/src/api/axios.ts index 94c9f3265..cdd7b40c5 100644 --- a/src/api/axios.ts +++ b/src/api/axios.ts @@ -1,23 +1,30 @@ -import axios, { - Axios, - AxiosRequestConfig, - InternalAxiosRequestConfig, -} from 'axios'; - +import axios, { AxiosError } from 'axios'; import Cookies from 'js-cookie'; +import { postRefreshToken } from './api'; +import { ACCESS_TOKEN } from './apiType'; + const instance = axios.create({ baseURL: process.env.NEXT_PUBLIC_API, + headers: { 'Content-Type': 'application/json' }, }); instance.interceptors.request.use( - (config: InternalAxiosRequestConfig) => { - const token = Cookies.get('token'); + (config) => { + const token = Cookies.get(ACCESS_TOKEN); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, - (error) => { + + async (error: AxiosError) => { + const originalRequest = error.config; + if (typeof originalRequest !== 'undefined') { + if (error.response?.status === 401) { + await postRefreshToken(); + return instance.request(originalRequest); + } + } // 요청이 실패할 경우 실행됩니다. return Promise.reject(error); } diff --git a/src/api/test.ts b/src/api/test.ts deleted file mode 100644 index e29e8de9d..000000000 --- a/src/api/test.ts +++ /dev/null @@ -1,17 +0,0 @@ -// api.test.js - -import axios from 'axios'; -import { getBestPosts } from './api'; -import instance from './axios'; - -describe('Integration tests with actual server', () => { - it('fetchData returns data successfully from the server', async () => { - // 예제 API 엔드포인트로 요청을 보냅니다. - getBestPosts('page=1&pageSize=3&orderBy=like'); - - // expect(response.status).toBe(200); - // expect(response.data).toBeDefined(); - // 실제 응답 데이터에 대한 추가 검증을 수행할 수 있습니다. - }); -}); - diff --git a/src/components/AddBoardContainer.tsx b/src/components/AddBoardContainer.tsx index bc2bd6918..d2512286d 100644 --- a/src/components/AddBoardContainer.tsx +++ b/src/components/AddBoardContainer.tsx @@ -1,42 +1,54 @@ -import React, { ChangeEvent, useEffect, useState } from 'react'; -import { postArticles, postImage, tempSignUP } from '../api/api'; +import { useRouter } from 'next/router'; +import React, { ChangeEvent, useState } from 'react'; +import { postArticles, postImage } from '../api/api'; import AddBoardForm from './AddBoardForm'; -import { form } from './AddBoardForm'; +import { formType } from './AddBoardForm'; const AddBoardContainer = () => { - const [formData, setFormData] = useState
({ + const routes = useRouter(); + const [previewImage, setPreviewImage] = useState(''); + const [formData, setFormData] = useState({ title: '', content: '', image: null, }); + const onCancelHandler = (e: React.MouseEvent) => { + URL.revokeObjectURL(previewImage); + setPreviewImage(''); + setFormData((prev) => ({ ...prev, ['image']: null })); + }; + const onChangeHandler = (e: ChangeEvent) => { - if (e.target.name === 'title') { + const targetName = e.target.name; + if (targetName === 'title') { setFormData((prev) => ({ ...prev, ['title']: e.target.value })); - } else if (e.target.name === 'content') { + } else if (targetName === 'content') { setFormData((prev) => ({ ...prev, ['content']: e.target.value })); - } else if (e.target.name === 'image') { + } else if (targetName === 'image') { const file = e.target.files?.[0]; - if (file) setFormData((prev) => ({ ...prev, ['image']: file })); + if (file) { + setFormData((prev) => ({ ...prev, ['image']: file })); + const objectURL = URL.createObjectURL(file); + setPreviewImage(objectURL); + } } }; const onSubmitHandler = async (e: React.FormEvent) => { e.preventDefault(); const url = await postImage(formData.image); - console.log(url); - - // postArticles(formData, url); + const result = await postArticles(formData, url); + routes.push('/boards'); }; - useEffect(() => { - tempSignUP(); - }, []); return ( <> diff --git a/src/components/AddBoardForm.tsx b/src/components/AddBoardForm.tsx index 62186af35..123ff47b6 100644 --- a/src/components/AddBoardForm.tsx +++ b/src/components/AddBoardForm.tsx @@ -1,20 +1,24 @@ import React, { ChangeEvent } from 'react'; import style from '@/styles/AddBoardForm.module.css'; -export interface form { +export interface formType { title: string; content: string; image: File | null; } interface AddBoardFormProps { - formData: form; + formData: formType; + previewImage: string; + onCancelHandler: (e: React.MouseEvent) => void; onChangeHandler: (e: ChangeEvent) => void; onSubmitHandler: (e: React.FormEvent) => void; } const AddBoardForm = ({ formData, + previewImage, onChangeHandler, onSubmitHandler, + onCancelHandler, }: AddBoardFormProps) => { return ( @@ -23,7 +27,11 @@ const AddBoardForm = ({ @@ -38,7 +46,6 @@ const AddBoardForm = ({ className={style.titleInput} placeholder='제목을 입력해주세요' /> -

*내용

이미지

- - - +
+ + {previewImage && ( +
+
+ X +
+ 미리보기이미지 +
+ )} + +
); }; diff --git a/src/components/BestPostsContainer.tsx b/src/components/BestPostsContainer.tsx index 3345f87c9..4566ff4c2 100644 --- a/src/components/BestPostsContainer.tsx +++ b/src/components/BestPostsContainer.tsx @@ -2,7 +2,6 @@ import React, { useEffect, useState } from 'react'; import BestPosts from './BestPosts'; import { writingType } from '../api/apiType'; -const URL = 'page=1&pageSize=3&orderBy=like'; const BestPostsContainer = ({ bestPosts }: { bestPosts: writingType[] }) => { return ; }; diff --git a/src/components/Board.tsx b/src/components/Board.tsx new file mode 100644 index 000000000..5837e97bf --- /dev/null +++ b/src/components/Board.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import style from '@/styles/Board.module.css'; +import { articleType } from '../api/apiType'; +import kebab from '@/src/img/kebab.png'; +import profile from '@/src/img/profile.png'; +import heart from '@/src/img/heart.png'; +import { convertTime } from '../util/convertTime'; +import CommentContainer from './CommentContainer'; +import Link from 'next/link'; +import backButton from '@/src/img/back.png'; +const Board = ({ article }: { article: articleType }) => { + const createdAt = convertTime(article.createdAt); + + return ( +
+
+

{article.title}

+ +
+
+ 프로필이미지 +

{article.writer.nickname}

+

{createdAt}

+ 좋아요 +

{article.likeCount}

+
+

{article.content}

+ + + + 목록으로 돌아가기 + + +
+ ); +}; + +export default Board; + diff --git a/src/components/Comment.tsx b/src/components/Comment.tsx new file mode 100644 index 000000000..bb712b0f5 --- /dev/null +++ b/src/components/Comment.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import kebab from '@/src/img/kebab.png'; +import profile from '@/src/img/profile.png'; +import style from '@/styles/Comment.module.css'; +import { getCreatedTime } from '../util/getCreatedTime'; +interface commentProps { + content: string; + image: string | null; + nickName: string; + createdAt: string; +} +const Comment = ({ content, image, nickName, createdAt }: commentProps) => { + const { day, hours } = getCreatedTime(createdAt); + return ( +
+
+

{content}

+ +
+
+ 프로필 +
+

{nickName}

+

{`${day}일 ${hours}시간 전`}

+
+
+
+ ); +}; + +export default Comment; + diff --git a/src/components/CommentContainer.tsx b/src/components/CommentContainer.tsx new file mode 100644 index 000000000..058e3731b --- /dev/null +++ b/src/components/CommentContainer.tsx @@ -0,0 +1,68 @@ +import { useRouter } from 'next/router'; +import React, { useEffect, useRef, useState } from 'react'; +import { getComments, postComment } from '../api/api'; +import { commentType } from '../api/apiType'; +import Comment from './Comment'; +import CommentInput from './CommentInput'; +import NoComment from './NoComment'; + +const CommentContainer = () => { + const router = useRouter(); + const [comments, setComments] = useState([]); + const id = router.query['id']; + const ref = useRef(null); + + const saveComment = async () => { + try { + if (typeof id === 'string') { + const result = await getComments(id as string, 5); + setComments(result); + } + } catch (error) {} + }; + + const onChangeHandler = (e: React.ChangeEvent) => { + if (ref.current) { + ref.current.value = e.target.value; + } + }; + + const registerCommentHandler = async () => { + const content = ref.current?.value; + try { + await postComment(id as string, content as string); + } catch (error) { + } finally { + window.location.reload(); + } + }; + + useEffect(() => { + saveComment(); + }, []); + + return ( + <> + + {comments.length !== 0 ? ( + comments.map((element) => ( + + )) + ) : ( + + )} + + ); +}; + +export default CommentContainer; + diff --git a/src/components/CommentInput.tsx b/src/components/CommentInput.tsx new file mode 100644 index 000000000..259dadd2a --- /dev/null +++ b/src/components/CommentInput.tsx @@ -0,0 +1,31 @@ +import React, { forwardRef } from 'react'; +import style from '@/styles/CommentInput.module.css'; + +interface CommentInputProps { + registerCommentHandler: () => void; + onChangeHandler: (e: React.ChangeEvent) => void; +} +const CommentInput = forwardRef( + ({ registerCommentHandler, onChangeHandler }, ref) => { + return ( +
+

댓글 달기

+ + +
+ ); + } +); + +export default CommentInput; + diff --git a/src/components/Header.tsx b/src/components/Header.tsx index fe2099e7f..efd828121 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,15 +1,29 @@ -import React, { useContext } from 'react'; +import React, { useEffect, useState } from 'react'; import style from '../../styles/Header.module.css'; import logo from '../img/panda.png'; import profile from '../img/profile.png'; import Link from 'next/link'; +import Cookies from 'js-cookie'; +import { useRouter } from 'next/router'; interface headerProps { - isLogin: boolean; children: React.ReactNode; } -const Header = ({ isLogin, children }: headerProps) => { +const Header = ({ children }: headerProps) => { + const [isLoggedIn, setIsLoggedIn] = useState(false); + const route = useRouter(); + useEffect(() => { + // 쿠키에서 토큰을 가져옴 + const token = Cookies.get('accessToken'); + + // 토큰이 있으면 로그인 상태로 설정 + if (token) { + console.log('test'); + + setIsLoggedIn(true); + } + }, [route.pathname]); return ( <>
@@ -17,16 +31,18 @@ const Header = ({ isLogin, children }: headerProps) => { 로고 -

자유게시판

-

중고마켓

+ 자유게시판 + 중고마켓
- {isLogin ? ( - ) : ( - + + 로그인 + )}
diff --git a/src/components/ListBackButton.tsx b/src/components/ListBackButton.tsx new file mode 100644 index 000000000..24f1c2141 --- /dev/null +++ b/src/components/ListBackButton.tsx @@ -0,0 +1,9 @@ +import Link from 'next/link'; +import React from 'react'; + +const ListBackButton = () => { + return 목록으로 돌아가기; +}; + +export default ListBackButton; + diff --git a/src/components/LoginForm.tsx b/src/components/LoginForm.tsx new file mode 100644 index 000000000..1b266e083 --- /dev/null +++ b/src/components/LoginForm.tsx @@ -0,0 +1,92 @@ +import React, { ChangeEvent } from 'react'; +import style from '@/styles/Login.module.css'; +import bigLogo from '@/src/img/bigLogo.png'; +import kakaoImage from '@/src/img/kakao.png'; +import googleImage from '@/src/img/google.png'; +import openPassword from '@/src/img/openPassword.png'; +import hidePassword from '@/src/img/hidePassword.png'; +import Link from 'next/link'; + +interface Login { + email: string; + password: string; + onChangePassword: (e: ChangeEvent) => void; + onChangeEmail: (e: ChangeEvent) => void; + loginClickHandler: (e: React.FormEvent) => void; +} +interface isHide { + isOpen: boolean; + eyeButtonClickHandler: () => void; +} + +const LoginForm = ({ + email, + password, + onChangeEmail, + onChangePassword, + isOpen, + eyeButtonClickHandler: onClick, + loginClickHandler, +}: Login & isHide) => { + return ( + <> +
+ + + +
+ 이메일 + +
+ 비밀번호 + +
+
+ + +
+

간편 로그인 하기

+
+ + 구글 + + + 카카오 + +
+
+
+
+ 판다 마켓이 처음이신가요? 회원가입 +
+ + ); +}; + +export default LoginForm; + diff --git a/src/components/LoginFormContainer.tsx b/src/components/LoginFormContainer.tsx new file mode 100644 index 000000000..9e4f9bf0d --- /dev/null +++ b/src/components/LoginFormContainer.tsx @@ -0,0 +1,63 @@ +import { useRouter } from 'next/router'; +import React, { ChangeEvent, useState, useEffect } from 'react'; +import { postSignIn } from '../api/api'; +import LoginForm from './LoginForm'; +import { AxiosError } from 'axios'; +import Cookies from 'js-cookie'; +import { redirect } from 'next/navigation'; + +const ACCESS_TOKEN = Cookies.get('accessToken'); + +const LoginFormContainer = () => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [isOpen, setIsOpen] = useState(false); + const route = useRouter(); + + const onChangeEmail = (e: ChangeEvent) => { + setEmail(e.target.value); + }; + + const onChangePassword = (e: ChangeEvent) => { + setPassword(e.target.value); + }; + + const eyeButtonClickHandler = () => { + setIsOpen(!isOpen); + }; + + const loginClickHandler = async (e: React.FormEvent) => { + e.preventDefault(); + + try { + await postSignIn(email, password); + route.push('/'); + } catch (error) { + if (error instanceof AxiosError) { + if (error.response?.status === 400) { + alert(error.response.data.message); + } + } + } + }; + + useEffect(() => { + if (ACCESS_TOKEN) { + route.back(); + } + }, []); + return ( + + ); +}; + +export default LoginFormContainer; + diff --git a/src/components/NoComment.tsx b/src/components/NoComment.tsx new file mode 100644 index 000000000..b201545c9 --- /dev/null +++ b/src/components/NoComment.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import noComment from '@/src/img/noComment.png'; +import style from '@/styles/NoComment.module.css'; +const NoComment = () => { + return ( + 댓글 없음 + ); +}; + +export default NoComment; + diff --git a/src/components/Post.tsx b/src/components/Post.tsx index ef88420cf..253e872e3 100644 --- a/src/components/Post.tsx +++ b/src/components/Post.tsx @@ -10,12 +10,22 @@ interface postItem { likeCount: number; nickName: string; createdAt: string; + id: number; + onClickHandler: (id: number) => void; } -const Post = ({ image, content, likeCount, nickName, createdAt }: postItem) => { +const Post = ({ + image, + content, + likeCount, + nickName, + createdAt, + id, + onClickHandler, +}: postItem) => { const contentDate = convertTime(createdAt); return ( -
+
onClickHandler(id)}>

{content}

{image ? ( diff --git a/src/components/SelectAndSearchContainer.tsx b/src/components/SelectAndSearchContainer.tsx index 60e44d8cd..ff992a5e4 100644 --- a/src/components/SelectAndSearchContainer.tsx +++ b/src/components/SelectAndSearchContainer.tsx @@ -20,11 +20,22 @@ const SelectAndSearchContainer = () => { }; const onChangeKeyDownHandler = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { - router.push(`/boards?keyword=${ref.current?.value}`); + if (ref.current?.value !== '') { + return router.push( + `/boards?keyword=${ref.current?.value}&orderBy=${selectedOption}` + ); + } + router.push(`boards?&orderBy=${selectedOption}`); } }; useEffect(() => { - router.push(`/boards?page=1&pageSize=5&orderBy=${selectedOption}`); + if (ref.current?.value !== '') { + router.push( + `/boards?keyword=${ref.current?.value}&orderBy=${selectedOption}` + ); + } else { + router.push(`/boards?orderBy=${selectedOption}`); + } }, [selectedOption]); return ( { + const { + watch, + register, + formState: { errors }, + setError, + clearErrors, + handleSubmit, + } = useForm({ mode: 'onChange' }); + + const [isShow, setIsShow] = useState({ + passShow: false, + repeatShow: false, + }); + + const route = useRouter(); + + const passwordHideHandler = () => { + setIsShow({ ...isShow, passShow: !isShow.passShow }); + }; + + const repeatPasswordHideHandler = () => { + setIsShow({ ...isShow, repeatShow: !isShow.repeatShow }); + }; + + const serveSignUp = async (data: SignUp) => { + try { + await postSignUp( + data.email, + data.nickName, + data.password, + data.repeatPassword + ); + route.push('/login'); + } catch (error) { + if (error instanceof AxiosError) { + if (error.response?.status === 400) { + alert(error.response.data.message); + } + } + } + }; + + return ( + + ); +}; + +export default SignUpContainer; + diff --git a/src/components/SignUpForm.tsx b/src/components/SignUpForm.tsx new file mode 100644 index 000000000..80b2652ff --- /dev/null +++ b/src/components/SignUpForm.tsx @@ -0,0 +1,160 @@ +import React, { useEffect } from 'react'; +import { Show, SignUp } from './SignUpContainer'; +import styles from '@/styles/signup.module.css'; +import hideEyes from '@/src/img/hidePassword.png'; +import openEyes from '@/src/img/openPassword.png'; +import kakao from '@/src/img/kakao.png'; +import google from '@/src/img/google.png'; +import bigLogo from '@/src/img/bigLogo.png'; +import Link from 'next/link'; +import { + UseFormRegister, + FieldErrors, + UseFormWatch, + UseFormClearErrors, + UseFormSetError, +} from 'react-hook-form'; +import { ErrorMessage } from '@hookform/error-message'; + +interface SignUpInfo { + handleSubmit: () => void; + register: UseFormRegister; + watch: UseFormWatch; + setError: UseFormSetError; + clearErrors: UseFormClearErrors; + errors: FieldErrors; +} + +interface EyesButton { + isShow: Show; + passwordHideHandler: () => void; + repeatPasswordHideHandler: () => void; +} + +const SignUpForm = ({ + clearErrors, + setError, + register, + handleSubmit, + errors, + isShow, + passwordHideHandler, + repeatPasswordHideHandler, + watch, +}: SignUpInfo & EyesButton) => { + useEffect(() => { + if (watch('password') !== watch('repeatPassword')) { + setError('repeatPassword', { + type: 'password-mismatch', + message: '비밀번호가 일치하지 않습니다', + }); + } else { + // 비밀번호 일치시 오류 제거 + clearErrors('repeatPassword'); + } + }, [watch('password'), watch('repeatPassword')]); + + return ( +
+ + + + +
+ 이메일 + +

{message}

} + /> + 닉네임 + +

{message}

} + /> + 비밀번호 + + 비밀번호 확인 + +

{message}

} + /> + + + +
+

간편로그인하기

+ +
+

+ 이미 회원이신가요? 로그인 +

+
+ ); +}; + +export default SignUpForm; + diff --git a/src/components/TotalPosts.tsx b/src/components/TotalPosts.tsx index bc57d539a..0f85b0b8d 100644 --- a/src/components/TotalPosts.tsx +++ b/src/components/TotalPosts.tsx @@ -8,7 +8,7 @@ const TotalPosts = () => {

게시글

- + 글쓰기
diff --git a/src/components/TotalPostsContainer.tsx b/src/components/TotalPostsContainer.tsx index 414eb3bde..1124c4234 100644 --- a/src/components/TotalPostsContainer.tsx +++ b/src/components/TotalPostsContainer.tsx @@ -1,40 +1,56 @@ import { useRouter } from 'next/router'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useRef } from 'react'; import { getTotalPosts } from '../api/api'; import { writingType } from '../api/apiType'; import Post from './Post'; import { AxiosError } from 'axios'; -export const URL = `page=1&pageSize=5`; +import style from '@/styles/Post.module.css'; +import useInfiniteScroll from '../hook/useInfiniteScroll'; + const TotalPostsContainer = () => { const router = useRouter(); const { orderBy, keyword } = router.query; - const [posts, setPosts] = useState([]); + const { scrollRef, limit } = useInfiniteScroll(); + const getPosts = async () => { - if (orderBy) { - try { - const result = await getTotalPosts(`${URL}&orderBy=${orderBy}`); - setPosts(result); - } catch (error) { - const err = error as AxiosError; - } - } else if (keyword) { + if (keyword && orderBy) { try { - const result = await getTotalPosts(`${URL}&keyword=${keyword}`); + const result = await getTotalPosts( + `page=1&pageSize=${limit}&orderBy=${orderBy}&keyword=${keyword}` + ); setPosts(result); } catch (error) { const err = error as AxiosError; } + return; + } + + try { + const result = await getTotalPosts( + `page=1&pageSize=${limit}&orderBy=${orderBy}` + ); + setPosts(result); + } catch (error) { + const err = error as AxiosError; } }; + + const onClickHandler = (id: number) => { + router.push(`/board/${id}`); + }; + useEffect(() => { getPosts(); - }, [orderBy, keyword]); + }, [router.query, limit]); + return ( <> {posts.map((element) => ( { createdAt={element.createdAt} /> ))} +
); }; diff --git a/src/components/boardContainer.tsx b/src/components/boardContainer.tsx new file mode 100644 index 000000000..ccfb81246 --- /dev/null +++ b/src/components/boardContainer.tsx @@ -0,0 +1,7 @@ +import React, { useEffect } from 'react'; + +const boardContainer = () => { + return
; +}; + +export default boardContainer; diff --git a/src/hook/useInfiniteScroll.tsx b/src/hook/useInfiniteScroll.tsx new file mode 100644 index 000000000..283c46978 --- /dev/null +++ b/src/hook/useInfiniteScroll.tsx @@ -0,0 +1,28 @@ +import React, { useRef, useState, useEffect } from 'react'; + +const useInfiniteScroll = () => { + const scrollRef = useRef(null); + const [limit, setLimit] = useState(6); + let preventFirst = 0; + + const callback = (entry: IntersectionObserverEntry[]) => { + if (entry[0].isIntersecting && preventFirst > 0) { + setLimit((prev) => prev + 6); + } + preventFirst++; + }; + + useEffect(() => { + const observer = new IntersectionObserver(callback, { threshold: 1 }); + if (scrollRef.current !== null) { + observer.observe(scrollRef.current); + } + return () => { + observer.disconnect(); + }; + }, []); + return { scrollRef, limit }; +}; + +export default useInfiniteScroll; + diff --git a/src/img/back.png b/src/img/back.png new file mode 100644 index 000000000..030a04614 Binary files /dev/null and b/src/img/back.png differ diff --git a/src/img/bigLogo.png b/src/img/bigLogo.png new file mode 100644 index 000000000..9dd1bab9b Binary files /dev/null and b/src/img/bigLogo.png differ diff --git a/src/img/bottomImage.png b/src/img/bottomImage.png new file mode 100644 index 000000000..2c5b867b3 Binary files /dev/null and b/src/img/bottomImage.png differ diff --git a/src/img/facebook.png b/src/img/facebook.png new file mode 100644 index 000000000..58333d45f Binary files /dev/null and b/src/img/facebook.png differ diff --git a/src/img/google.png b/src/img/google.png new file mode 100644 index 000000000..49852a6bd Binary files /dev/null and b/src/img/google.png differ diff --git a/src/img/hidePassword.png b/src/img/hidePassword.png new file mode 100644 index 000000000..fb4eeaf42 Binary files /dev/null and b/src/img/hidePassword.png differ diff --git a/src/img/hotIem.png b/src/img/hotIem.png new file mode 100644 index 000000000..ee75b082b Binary files /dev/null and b/src/img/hotIem.png differ diff --git a/src/img/instagram.png b/src/img/instagram.png new file mode 100644 index 000000000..98e24ea6a Binary files /dev/null and b/src/img/instagram.png differ diff --git a/src/img/kakao.png b/src/img/kakao.png new file mode 100644 index 000000000..73a01dd9a Binary files /dev/null and b/src/img/kakao.png differ diff --git a/src/img/kebab.png b/src/img/kebab.png new file mode 100644 index 000000000..b390f973f Binary files /dev/null and b/src/img/kebab.png differ diff --git a/src/img/noComment.png b/src/img/noComment.png new file mode 100644 index 000000000..428167b33 Binary files /dev/null and b/src/img/noComment.png differ diff --git a/src/img/openPassword.png b/src/img/openPassword.png new file mode 100644 index 000000000..467cf49aa Binary files /dev/null and b/src/img/openPassword.png differ diff --git a/src/img/registerImage.png b/src/img/registerImage.png new file mode 100644 index 000000000..139c525bf Binary files /dev/null and b/src/img/registerImage.png differ diff --git a/src/img/schoolPanda.png b/src/img/schoolPanda.png new file mode 100644 index 000000000..f051c4c6d Binary files /dev/null and b/src/img/schoolPanda.png differ diff --git a/src/img/search.png b/src/img/search.png new file mode 100644 index 000000000..ef916f3d4 Binary files /dev/null and b/src/img/search.png differ diff --git a/src/img/twitter.png b/src/img/twitter.png new file mode 100644 index 000000000..36b751a47 Binary files /dev/null and b/src/img/twitter.png differ diff --git a/src/img/youtube.png b/src/img/youtube.png new file mode 100644 index 000000000..f51731d40 Binary files /dev/null and b/src/img/youtube.png differ diff --git a/src/util/getCreatedTime.ts b/src/util/getCreatedTime.ts new file mode 100644 index 000000000..ce110c3c2 --- /dev/null +++ b/src/util/getCreatedTime.ts @@ -0,0 +1,11 @@ +export const getCreatedTime = (date: string) => { + const now = new Date(); + const before = new Date(date); + const time = now.getTime() - before.getTime(); + const timeDifferenceInSeconds = Math.floor(time / 1000); + // 초를 시, 분, 초로 변환 + const day = Math.floor(timeDifferenceInSeconds / 86400); + const hours = Math.floor((timeDifferenceInSeconds % 86400) / 3600); + return { day: day, hours: hours }; +}; + diff --git a/styles/AddBoardForm.module.css b/styles/AddBoardForm.module.css index 2171a29c4..9576d4010 100644 --- a/styles/AddBoardForm.module.css +++ b/styles/AddBoardForm.module.css @@ -17,19 +17,23 @@ justify-content: space-between; align-items: center; } - .registerBtn { cursor: pointer; width: 74px; height: 42px; border-radius: 8px; border: none; - background-color: #9ca3af; + font-weight: 600; font-size: 16px; color: #ffffff; } - +.ableBtn { + background-color: #3692ff; +} +.disableBtn { + background-color: #9ca3af; +} .titleInput { margin-top: 10px; width: 100%; @@ -38,13 +42,11 @@ background-color: #f3f4f6; border-radius: 12px; } - .titleInput::placeholder { font-weight: 400; font-size: 16px; color: #9ca3af; } - .content { margin-top: 10px; } @@ -56,14 +58,22 @@ background-color: #f3f4f6; border-radius: 12px; } + .contentInput::placeholder { font-weight: 400; font-size: 16px; color: #9ca3af; } + .image { margin-top: 10px; } + +.imageFrame { + display: flex; + gap: 50px; +} + .imageRegister { margin-top: 10px; display: flex; @@ -75,6 +85,7 @@ border-radius: 12px; background: #f3f4f6; } + .imageRegister :first-child { color: #9ca3af; font-size: 42px; @@ -88,4 +99,19 @@ .imageInput { display: none; } +.cancelBtn { + position: absolute; + right: 10px; + cursor: pointer; +} +.previewImageBox { + position: relative; + width: 282px; + height: 282px; +} + +.previewImage { + width: 100%; + height: 100%; +} diff --git a/styles/Board.module.css b/styles/Board.module.css new file mode 100644 index 000000000..4fe611dfd --- /dev/null +++ b/styles/Board.module.css @@ -0,0 +1,131 @@ +.boardFrame { + margin: 0px auto; + width: 1200px; +} + +.articleFrame { + margin-top: 30px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.title { + font-weight: 700; + font-size: 20px; + color: #1f2937; +} + +.kebabImage { + width: 24px; + height: 24px; +} +.kebabButton { + cursor: pointer; + background-color: #ffffffff; + border: none; + width: 24px; + height: 24px; +} +.boardDataFrame { + margin-top: 10px; + display: flex; + align-items: center; + gap: 5px; + padding-bottom: 10px; + border-bottom: 1px solid #e5e7eb; +} + +.profileImage { + width: 24px; + height: 24px; +} + +.nickName { + font-weight: 400; + font-size: 14px; + color: #4b5563; +} + +.createdAt { + font-weight: 400; + font-size: 12px; + color: #9ca3af; +} + +.likeImage { + width: 24px; + height: 24px; +} + +.likeCount { + font-weight: 400; + font-size: 14px; + color: #6b7280; +} +.content { + margin-top: 15px; + font-weight: 400; + font-size: 16px; + color: #1f2937; +} +.commentFrame { + margin-top: 50px; + gap: 10px; + display: flex; + flex-direction: column; +} +.comment { + font-weight: 600; + font-size: 16px; + color: #111827; +} +.commentInput { + height: 104px; + border: none; + border-radius: 12px; + background-color: #f3f4f6; +} +.registerButton { + align-self: flex-end; + width: 74px; + height: 42px; + border-radius: 8px; + cursor: pointer; + background-color: #9ca3af; + color: #ffffffff; + border: none; +} +.backButton { + position: relative; + width: 240px; + height: 48px; + border-radius: 40px; + text-align: center; + line-height: 48px; + background-color: #3692ff; + display: block; + margin: 30px auto; + text-decoration: none; + color: #ffffffff; +} +.backButtonImage { + width: 24px; + height: 24px; + position: absolute; + top: 12px; + right: 24px; +} + +@media (max-width: 1199px) { + .boardFrame { + width: 696px; + } +} + +@media (max-width: 769px) { + .boardFrame { + width: 343px; + } +} + diff --git a/styles/Comment.module.css b/styles/Comment.module.css new file mode 100644 index 000000000..c862933dd --- /dev/null +++ b/styles/Comment.module.css @@ -0,0 +1,43 @@ +.commentFrame { + margin-top: 10px; + height: 97px; + border-bottom: 1px solid #e5e7eb; +} +.contentFrame { + display: flex; + justify-content: space-between; +} +.kebabButton { + cursor: pointer; + background-color: #ffffff; + border: none; +} +.kebabImage { + width: 24px; + height: 24px; +} +.content { + font-weight: 400; + font-size: 14px; + color: #1f2937; +} +.userInfoFrame { + display: flex; + gap: 4px; + align-items: center; +} +.profile { + width: 32px; + height: 32px; +} +.nickName { + font-weight: 400; + font-size: 12px; + color: #4b5563; +} +.createdAt { + font-weight: 400; + font-size: 12px; + color: #9ca3af; +} + diff --git a/styles/CommentInput.module.css b/styles/CommentInput.module.css new file mode 100644 index 000000000..2c639040b --- /dev/null +++ b/styles/CommentInput.module.css @@ -0,0 +1,29 @@ +.commentFrame { + margin-top: 50px; + gap: 10px; + display: flex; + flex-direction: column; + margin-bottom: 50px; +} +.comment { + font-weight: 600; + font-size: 16px; + color: #111827; +} +.commentInput { + height: 104px; + border: none; + border-radius: 12px; + background-color: #f3f4f6; +} +.registerButton { + align-self: flex-end; + width: 74px; + height: 42px; + border-radius: 8px; + cursor: pointer; + background-color: #9ca3af; + color: #ffffffff; + border: none; +} + diff --git a/styles/Header.module.css b/styles/Header.module.css index 26376f680..b77bfe7e3 100644 --- a/styles/Header.module.css +++ b/styles/Header.module.css @@ -15,14 +15,17 @@ .leftBtn * { margin-right: 30px; } + .leftBtn .logo { cursor: pointer; } -.leftBtn p { + +.leftBtn a { cursor: pointer; font-weight: 700; font-size: 18px; color: #4b5563; + text-decoration: none; } #presentPage { color: #3692ff; @@ -35,12 +38,20 @@ .loginBtn { cursor: pointer; - border: 1px solid; width: 88px; height: 42px; + text-align: center; + text-decoration: none; border-radius: 8px; + line-height: 42px; background-color: #3692ff; color: #ffffff; + border: none; font-size: 16px; } +.profile { + border: none; + background-color: #ffffff; +} + diff --git a/styles/Login.module.css b/styles/Login.module.css new file mode 100644 index 000000000..46634e68b --- /dev/null +++ b/styles/Login.module.css @@ -0,0 +1,130 @@ +.root { + --inputColor: #3692ff; + --fontColor: #1f2937; +} + +.section { + margin-top: 60px; + display: flex; + flex-direction: column; + gap: 24px; + align-items: center; +} + +.section .loginBox { + display: flex; + flex-direction: column; + text-align: left; + font-size: 18px; + font-weight: 700; + color: var(--fontColor); + gap: 24px; +} +.section input { + background: #f3f4f6; + height: 56px; + border-radius: 12px; + width: 640px; + border: none; +} +.email-error-message { + position: relative; + left: 10px; + bottom: 10px; + font-size: 15px; + color: #f74747; +} +.section .error-border { + border: 2px solid red; +} +.section input:focus { + background-color: #ffffff; + border-color: var(--inputColor); +} +.section input::placeholder { + padding: 16px 24px; +} +.email-error-message { + position: relative; + left: 10px; + bottom: 10px; + font-size: 15px; + color: #f74747; +} +.passwordLabel { + display: flex; + align-items: center; + justify-content: center; + position: relative; +} +.passwordLabel img { + cursor: pointer; + position: absolute; + right: 30px; +} +.pw-error-message { + position: relative; + left: 10px; + bottom: 10px; + font-size: 15px; + color: #f74747; +} +.loginButton { + cursor: pointer; + width: 640px; + height: 56px; + border-radius: 40px; + background: #9ca3af; + border: #9ca3af; + color: #ffffff; + font-weight: 600; + font-size: 20px; + text-align: center; +} +.simpleLoginBox { + margin-top: 24px; + background: #e6f2ff; + display: flex; + align-items: center; + width: 640px; + height: 74px; + border-radius: 8px; + justify-content: space-between; +} +.simpleLoginBox p { + padding-left: 24px; +} +.simpleLoginIcons { + width: 100px; + margin-right: 16px; + display: flex; + align-items: center; + justify-content: space-between; +} +.signUp { + display: flex; + justify-content: center; + margin-top: 30px; +} + +@media (max-width: 769px) { + .section .homeBtn > img { + width: 198px; + height: 66px; + } + .section input { + background: #f3f4f6; + + height: 56px; + border: none; + border-radius: 12px; + width: 343px; + } + .loginButton { + width: 343px; + } + .simpleLoginBox { + width: 344px; + } +} + diff --git a/styles/Main.module.css b/styles/Main.module.css new file mode 100644 index 000000000..610337c2c --- /dev/null +++ b/styles/Main.module.css @@ -0,0 +1,499 @@ +/* header */ + +#home { + cursor: pointer; + margin-left: 10px; +} + +#login { + cursor: pointer; + border: none; + background-color: #3692ff; + border-radius: 8px; + padding: 12px 20px; + color: #ffffff; + font-size: 16px; + font-weight: 600; + width: 128px; + height: 48px; + margin-right: 10px; +} + +/* main */ + +.mainFrame { + background: #cfe5ff; +} + +.card { + display: flex; + justify-content: center; + align-items: flex-end; + height: 540px; + width: 100%; +} + +.card .box { + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.box h1 { + font-weight: 700; + font-size: 40px; + line-height: 56px; + color: #374151; +} + +.box a { + width: 355px; + height: 56px; + border-radius: 40px; + background-color: #3692ff; + border: none; + color: #ffffff; + font-weight: 600; + font-size: 20px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + text-decoration: none; +} + +.card img { + width: 996px; + height: 447px; +} + +.section { + background-color: #ffffff; + height: 720px; + display: flex; + justify-content: center; + align-items: center; +} + +.section .confirmBox { + top: 137.5px; + left: 360px; + display: flex; + align-items: center; +} + +.confirmBox .specific { + padding-left: 50px; + text-align: left; +} + +.specific h1 { + font-weight: 700; + font-size: 40px; + line-height: 56px; + color: #374151; +} + +.specific p { + font-size: 24px; + font-weight: 500; + line-height: 28.8px; + color: #374151; +} + +.section .searchBox { + display: flex; + align-items: center; +} + +.searchBox .specific2 { + padding-right: 50px; + text-align: right; +} + +.specific2 h1 { + font-weight: 700; + font-size: 40px; + line-height: 56px; + color: #374151; +} + +.specific2 p { + font-weight: 500; + font-size: 24px; + line-height: 28.8px; + color: #374151; +} + +.card4 { + background-color: #ffffff; + height: 720px; + display: flex; + justify-content: center; + align-items: center; +} + +.section .registerBox { + display: flex; + align-items: center; +} + +.registerBox .specific3 { + padding-left: 50px; +} + +.specific3 h1 { + font-weight: 700; + font-size: 40px; + line-height: 56px; + color: #374151; +} + +.specific3 p { + font-weight: 500; + font-size: 24px; + line-height: 28.8px; + color: #374151; +} + +.bottomCard { + height: 540px; + display: flex; + justify-content: center; + align-items: center; +} + +.bottomCard h2 { + font-weight: 700; + font-size: 40px; + line-height: 56px; + color: #374151; +} + +.bottomCard img { + height: 540px; +} + +.footerFrame { + height: 160px; + background-color: #111827; +} + +.footerFrame .tag { + height: 50%; + display: flex; + align-items: center; + justify-content: center; +} +.footerFrame .tag .mark { + flex-grow: 1; + display: flex; + justify-content: space-between; +} + +.footerFrame .tag .mark .privacy-container { + margin-right: 50px; + display: flex; +} + +.tag .codeit { + margin-left: 50px; + color: #9ca3af; + font-weight: 400; + font-size: 16px; + text-align: center; +} + +.tag .privacy { + font-weight: 400; + font-size: 14px; + text-align: center; + color: #e5e7eb; +} +.tag .faq { + margin-left: 30px; + font-weight: 400; + font-size: 14px; + text-align: center; + color: #e5e7eb; +} + +.social { + margin-right: 40px; + flex-grow: 1; + display: flex; + justify-content: flex-end; + align-items: center; +} +.social a { + padding-right: 10px; +} + +@media (max-width: 1199px) { + .card { + height: 771px; + display: block; + } + + .card .box { + display: flex; + justify-content: center; + gap: 20px; + width: 100%; + height: 320px; + } + + .box h1 { + font-weight: 700; + font-size: 40px; + line-height: 56px; + color: #374151; + } + .box h1 > br { + display: none; + } + + .box a { + width: 355px; + height: 56px; + border-radius: 40px; + background-color: #3692ff; + border: none; + color: #ffffff; + font-weight: 600; + font-size: 20px; + text-align: center; + cursor: pointer; + } + + .card img { + width: 100%; + height: 450px; + } + /* 첫번째 쎅션 */ + + .section { + background-color: #ffffff; + height: 771px; + display: block; + width: 100%; + } + .section .confirmBox { + display: flex; + flex-direction: column; + align-items: center; + } + .section .confirmBox > img { + margin-top: 20px; + height: 454px; + width: 696px; + } + + .confirmBox .specific { + margin-top: 30px; + width: 696px; + display: flex; + flex-direction: column; + } + .specific h1 > br { + display: none; + } + .specific h1 { + font-weight: 700; + font-size: 32px; + line-height: 56px; + color: #374151; + } + + .specific p { + margin-top: 10px; + font-size: 18px; + font-weight: 500; + line-height: 28.8px; + color: #374151; + } + + /* 2번쩨 섹션 */ + + .section .searchBox { + display: flex; + flex-direction: column-reverse; + align-items: center; + } + .searchBox > img { + height: 454px; + width: 696px; + } + .searchBox .specific2 { + margin-top: 30px; + width: 696px; + padding-right: 50px; + text-align: right; + } + + .specific2 h1 { + font-weight: 700; + font-size: 32px; + line-height: 56px; + color: #374151; + } + + .specific2 h1 > br { + display: none; + } + + .specific2 p { + margin-top: 10px; + font-weight: 500; + font-size: 18px; + line-height: 28.8px; + color: #374151; + } + + /* 3번째 쎅션 */ + + .section .registerBox { + display: flex; + flex-direction: column; + align-items: center; + } + .registerBox > img { + width: 696px; + } + .registerBox .specific3 { + margin-top: 30px; + width: 696px; + } + + .specific3 h1 { + font-weight: 700; + font-size: 32px; + line-height: 56px; + color: #374151; + } + + .specific3 h1 > br { + display: none; + } + + .specific3 p { + font-weight: 500; + font-size: 18px; + line-height: 28.8px; + color: #374151; + } + + .bottomCard { + height: 927px; + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: center; + text-align: center; + } + + .bottomCard h2 { + font-size: 40px; + } + + .bottomCard img { + height: 540px; + } +} + +@media (max-width: 769px) { + .card { + height: 541px; + } + .box { + text-align: center; + } + .card .box h1 { + font-size: 32px; + } + .box h1 br { + display: block; + } + .box a { + height: 48px; + width: 154px; + font-size: 16px; + } + .card img { + width: 100%; + height: 221px; + } + .section .confirmBox > img { + margin-top: 50px; + width: 344px; + height: 259px; + } + .section .confirmBox .specific { + padding-left: 350px; + } + .section .confirmBox .specific h1 { + font-size: 24px; + } + .section .confirmBox .specific p { + font-size: 16px; + } + .section .searchBox > img { + margin-top: 50px; + width: 344px; + height: 259px; + } + .section .searchBox .specific2 { + padding-right: 350px; + } + + .section .searchBox .specific2 h1 { + font-size: 24px; + } + + .section .searchBox .specific2 p { + font-size: 16px; + } + .section .registerBox > img { + margin-top: 50px; + width: 344px; + height: 259px; + } + .section .registerBox .specific3 { + padding-left: 350px; + } + .section .registerBox .specific3 h1 { + font-size: 24px; + } + .bottomCard { + height: 540px; + } + .bottomCard h2 { + font-size: 32px; + } + .bottomCard img { + width: 497.91px; + height: 269.95px; + } + .footer .tag { + display: flex; + flex-direction: row-reverse; + } + + .tag .mark { + margin-left: 50px; + + flex-direction: column-reverse; + justify-content: space-around; + align-items: flex-start; + } + .tag .privacy-container { + margin-top: 20%; + } + .tag .mark .codeit { + margin-left: 0px; + margin-top: 50px; + } +} + diff --git a/styles/NoComment.module.css b/styles/NoComment.module.css new file mode 100644 index 000000000..eca82a7f0 --- /dev/null +++ b/styles/NoComment.module.css @@ -0,0 +1,5 @@ +.noComment { + margin: 0 auto; + display: block; +} + diff --git a/styles/Post.module.css b/styles/Post.module.css index 74b38448b..7604f7636 100644 --- a/styles/Post.module.css +++ b/styles/Post.module.css @@ -1,19 +1,21 @@ .postFrame { + cursor: pointer; margin-bottom: 30px; height: 112px; + text-decoration: none; border-bottom: 1px solid #e5e7eb; } + .contentFrame { display: flex; justify-content: space-between; + padding-bottom: 5px; } - .content { font-weight: 600; font-size: 20px; color: #1f2937; } - .contentImage { width: 72px; height: 72px; @@ -29,9 +31,9 @@ } .userInfo { - width: 180px; + width: 200px; display: flex; - justify-content: space-around; + justify-content: space-between; align-items: center; } .userInfo .profileImage { @@ -59,3 +61,7 @@ align-items: center; } +.observer { + height: 50px; +} + diff --git a/styles/signup.module.css b/styles/signup.module.css new file mode 100644 index 000000000..65d05ae0c --- /dev/null +++ b/styles/signup.module.css @@ -0,0 +1,107 @@ +.section { + margin-top: 70px; + display: flex; + flex-direction: column; + align-items: center; +} + +.signUpTable { + display: flex; + flex-direction: column; + text-align: le; + gap: 24px; +} + +.error { + color: #f74747; +} + +.signUpTable label { + display: flex; + justify-content: center; + width: 100%; + height: 56px; +} + +label .inputContainer { + padding: 0px; + width: 100%; + height: 56px; + background: #f3f4f6; + border-radius: 12px; + border: none; +} + +.inputContainer:focus { + background-color: #ffffff; + border-color: var(--inputColor); +} +.inputContainer::placeholder { + padding: 16px 24px; +} + +.signUpButton { + cursor: pointer; + background-color: #9ca3af; + height: 56px; + border: none; + border-radius: 40px; + width: 640px; + font-size: 20px; + font-weight: 600; + color: #ffffff; + text-align: center; +} + +.simpleLoginBox { + border-radius: 8px; + padding-left: 24px; + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 24px; + width: 640px; + height: 74px; + background: #e6f2ff; +} +.simpleLoginIcons { + width: 100px; + margin-right: 16px; + display: flex; + align-items: center; + justify-content: space-between; +} + +.passwordLabel { + position: relative; + display: flex; + align-items: center; +} + +.passwordLabel img { + cursor: pointer; + position: absolute; + right: 50px; +} + +.login { + margin-top: 24px; +} + +@media (max-width: 769px) { + .section .logo > img { + width: 196px; + height: 66px; + } + .section .signUpTable input { + width: 343px; + } + .signUpTable .signUpButton { + width: 343px; + } + + .simpleLoginBox { + width: 343px; + } +} +