diff --git a/.github/pull-request-template.md b/.github/pull-request-template.md deleted file mode 100644 index a8581477..00000000 --- a/.github/pull-request-template.md +++ /dev/null @@ -1,27 +0,0 @@ -## 요구사항 - -### 기본 - -- [x] -- [] -- [] - -### 심화 - -- [x] -- [] - -## 주요 변경사항 - -- -- - -## 스크린샷 - -![image](이미지url) - -## 멘토에게 - -- -- -- 셀프 코드 리뷰를 통해 질문 이어가겠습니다. diff --git a/.github/workflows/auto-label-assign.yml b/.github/workflows/auto-label-assign.yml deleted file mode 100644 index d34fb992..00000000 --- a/.github/workflows/auto-label-assign.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: PR 자동 라벨링 및 담당자 할당 - -on: - pull_request: - types: [opened, reopened] - -jobs: - add-labels-assign: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - - name: Add labels and assign PR creator - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - PR_NUMBER=${{ github.event.pull_request.number }} - PR_CREATOR=${{ github.event.pull_request.user.login }} - - # Add labels - curl -X POST -H "Authorization: token $GITHUB_TOKEN" \ - -H "Accept: application/vnd.github.v3+json" \ - https://api.github.com/repos/${{ github.repository }}/issues/$PR_NUMBER/labels \ - -d '{"labels":["매운맛🔥", "진행 중 🏃"]}' - - # Assign PR creator - curl -X POST -H "Authorization: token $GITHUB_TOKEN" \ - -H "Accept: application/vnd.github.v3+json" \ - https://api.github.com/repos/${{ github.repository }}/issues/$PR_NUMBER/assignees \ - -d "{\"assignees\":[\"$PR_CREATOR\"]}" diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..5ef6a520 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/README.md b/README.md new file mode 100644 index 00000000..eb8aec46 --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/pages/api-reference/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. + +[API routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. + +The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) instead of React pages. + +This project uses [`next/font`](https://nextjs.org/docs/pages/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn-pages-router) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/pages/building-your-application/deploying) for more details. diff --git a/components/ArticleItem.js b/components/ArticleItem.js new file mode 100644 index 00000000..86067dcc --- /dev/null +++ b/components/ArticleItem.js @@ -0,0 +1,44 @@ +import Image from "next/image"; +import styles from "./ArticleItem.module.css"; +import Hearts from "./Hearts"; +import { useRouter } from "next/router"; +import dateFormat from "@/utils/dateFormat"; + +export default function ArticleItem({ article }) { + const router = useRouter(); + const updatedAt = dateFormat(article.updatedAt); + + return ( +
{ + router.push(`/article/${article.id}`); + }} + > +
+
{article.title}
+ 이미지 +
+
+
+ 이미지 +
{article.nickname}
+
{updatedAt}
+
+ +
+
+ ); +} diff --git a/components/ArticleItem.module.css b/components/ArticleItem.module.css new file mode 100644 index 00000000..3322100b --- /dev/null +++ b/components/ArticleItem.module.css @@ -0,0 +1,104 @@ +.article { + width: 100%; + + padding-bottom: 24px; + + display: flex; + flex-direction: column; + gap: 16px; + + border-bottom: var(--Cool-Gray-200, #e5e7eb) solid 1px; + + cursor: pointer; +} + +.content { + width: 100%; + + display: flex; + flex-direction: row; + gap: 8px; + align-self: stretch; +} + +.title { + flex-grow: 1; + + color: var(--Cool-Gray-800, #1f2937); + + /* pretendard/xl-20px-semibold */ + font-family: Pretendard; + font-size: 20px; + font-style: normal; + font-weight: 600; + line-height: 32px; /* 160% */ +} + +.info { + width: 100%; + display: flex; + flex-direction: row; + + justify-content: space-between; + align-items: center; +} + +.user { + display: flex; + flex-direction: row; + gap: 8px; + + align-items: center; +} + +.userImg { + background-color: #d1d5db; + border-radius: 50px; +} + +.nickname { + color: var(--Secondary-600, #4b5563); + + /* pretendard/md-14px-regular */ + font-family: Pretendard; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 24px; /* 171.429% */ +} + +.updatedAt { + color: var(--Secondary-400, #9ca3af); + + /* pretendard/md-14px-regular */ + font-family: Pretendard; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 24px; /* 171.429% */ +} + +.heart { + display: flex; + flex-direction: row; + + gap: 8px; + + align-items: center; +} + +.heartBtn { + width: 24px; + height: 24px; +} + +.heartCount { + color: var(--Secondary-500, #6b7280); + + /* pretendard/lg-16px-regular */ + font-family: Pretendard; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 26px; /* 162.5% */ +} diff --git a/components/ArticleSection.js b/components/ArticleSection.js new file mode 100644 index 00000000..a6abcc0c --- /dev/null +++ b/components/ArticleSection.js @@ -0,0 +1,70 @@ +import CustomButtonSquare from "./CustomButtonSquare"; +import InputBox from "./InputBox"; +import SortOption from "./SortOption"; +import Image from "next/image"; +import ArticleItem from "./ArticleItem"; +import styles from "./ArticleSection.module.css"; +import { useEffect, useState } from "react"; +import axios from "axios"; +import { useRouter } from "next/router"; + +export default function ArticleSection() { + const [keyword, setKeyword] = useState(""); + + const [articles, setArticles] = useState([]); + const [isLoading, setIsLoading] = useState(false); + + const router = useRouter(); + + useEffect(() => { + async function getArticles(keyword) { + setIsLoading(true); + try { + const res = await axios.get("http://localhost:5000/article", { + params: { keyword, limit: 4 }, + }); + setArticles(res.data); + } catch (e) { + console.error(e); + } finally { + setIsLoading(false); + } + } + + getArticles(keyword); + }, [keyword]); + + let count = 1; + + return ( +
+
+
게시글
+ { + router.push(`/postArticle?mode=post`); + }} + valid={true} + /> +
+
+ + +
+ {!isLoading && ( +
+ {articles.map((article) => { + const key = "a" + count; + count += 1; + return ; + })} +
+ )} +
+ ); +} diff --git a/components/ArticleSection.module.css b/components/ArticleSection.module.css new file mode 100644 index 00000000..d89c8b9f --- /dev/null +++ b/components/ArticleSection.module.css @@ -0,0 +1,40 @@ +.section { + width: 1200px; + display: flex; + flex-direction: column; + gap: 24px; +} + +.header { + width: 100%; + height: 42px; + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.title { + color: var(--Secondary-800, #1f2937); + + /* pretendard/xl-20px-bold */ + font-family: Pretendard; + font-size: 20px; + font-style: normal; + font-weight: 700; + line-height: 32px; /* 160% */ +} + +.option { + width: 100%; + height: 42px; + display: flex; + flex-direction: row; + gap: 16px; +} + +.articleList { + width: 100%; + display: flex; + flex-direction: column; + gap: 24px; +} diff --git a/components/BestArticleSection.js b/components/BestArticleSection.js new file mode 100644 index 00000000..6f0122b7 --- /dev/null +++ b/components/BestArticleSection.js @@ -0,0 +1,94 @@ +import DefaultImg from "@/public/default.png"; +import Image from "next/image"; +import axios from "axios"; +import { useEffect, useState } from "react"; +import styles from "./BestArticleSection.module.css"; +import Hearts from "./Hearts"; +import { useRouter } from "next/router"; +import dateFormat from "@/utils/dateFormat"; + +function BestArticle({ article }) { + const router = useRouter(); + + if (!article) { + return
로딩 중...
; + } + + return ( +
{ + router.push(`/article/${article.id}`); + }} + > +
+ 메달 + Best +
+
+
{article.title}
+ 이미지 +
+
+

{article.nickname}

+ +

{dateFormat(article.updatedAt)}

+
+
+ ); +} + +export default function BestArticleSection() { + const [bestArticles, setBestArticles] = useState([]); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + async function getBestArticles() { + setIsLoading(true); + try { + const res = await axios.get("http://localhost:5000/article", { + params: { limit: 3, orderBy: "hearts" }, + }); + console.log(res.data); + setBestArticles(res.data); + } catch (e) { + console.error(e); + } finally { + setIsLoading(false); + } + } + + getBestArticles(); + }, []); + + let count = 1; + + if (isLoading) { + return
로딩 중
; + } + + return ( +
+
베스트 게시글
+
+ {bestArticles.map((bestArticle) => { + const key = "a" + count; + count += 1; + return ; + })} +
+
+ ); +} diff --git a/components/BestArticleSection.module.css b/components/BestArticleSection.module.css new file mode 100644 index 00000000..17f14327 --- /dev/null +++ b/components/BestArticleSection.module.css @@ -0,0 +1,121 @@ +.bestArticleSection { + width: 100%; + display: flex; + flex-direction: column; + gap: 24px; +} + +.title { + color: var(--Cool-Gray-900, #111827); + font-family: Pretendard; + font-size: 20px; + font-style: normal; + font-weight: 700; + line-height: normal; +} + +.list { + display: flex; + flex-direction: row; + gap: 24px; +} + +.item { + display: flex; + width: 384px; + height: 169px; + padding: 0px 24px; + flex-direction: column; + + border-radius: 8px; + background: var(--Cool-Gray-50, #f9fafb); + + cursor: pointer; +} + +.bestMark { + display: flex; + flex-direction: row; + gap: 4px; + + width: 102px; + padding: 2px 24px 2px 22px; + justify-content: center; + align-items: center; + + border-radius: 0px 0px 16px 16px; + background: var(--brand-blue, #3692ff); + + color: #fff; + + /* pretendard/lg-16px-semibold */ + font-family: Pretendard; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 26px; /* 162.5% */ +} + +.content { + display: flex; + flex-direction: row; + gap: 8px; + + margin: 16px 0 18px; +} + +.contentTitle { + flex-grow: 1; + + color: var(--Secondary-800, #1f2937); + + /* pretendard/xl-20px-semibold */ + font-family: Pretendard; + font-size: 20px; + font-style: normal; + font-weight: 600; + line-height: 32px; /* 160% */ +} + +.articleImg { + width: 72px; + height: 72px; + padding: 13.714px 12px; + + border-radius: 6px; + border: 1px solid var(--Cool-Gray-200, #e5e7eb); + background: #fff; +} + +.info { + display: flex; + flex-direction: row; + gap: 8px; + + align-items: center; +} + +.userName { + color: var(--Secondary-600, #4b5563); + + /* pretendard/md-14px-regular */ + font-family: Pretendard; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 24px; /* 171.429% */ +} + +.date { + flex-grow: 1; + display: flex; + justify-content: flex-end; + color: var(--Secondary-400, #9ca3af); + + /* pretendard/md-14px-regular */ + font-family: Pretendard; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 24px; /* 171.429% */ +} diff --git a/components/BigTitle.js b/components/BigTitle.js new file mode 100644 index 00000000..c8cb8c68 --- /dev/null +++ b/components/BigTitle.js @@ -0,0 +1,25 @@ +import { useRouter } from "next/router"; +import styles from "./BigTitle.module.css"; +import Image from "next/image"; + +export default function BigTitle() { + const router = useRouter(); + + return ( +
{ + router.push("/"); + }} + > + 메인 로고 +

판다마켓

+
+ ); +} diff --git a/components/BigTitle.module.css b/components/BigTitle.module.css new file mode 100644 index 00000000..1a53d9b9 --- /dev/null +++ b/components/BigTitle.module.css @@ -0,0 +1,16 @@ +.title { + width: auto; + height: 132px; + display: flex; + flex-direction: row; + gap: 22px; +} + +.titleText { + color: var(--brand-blue, #3692ff); + font-family: "ROKAF Sans"; + font-size: 66.344px; + font-style: normal; + font-weight: 700; + line-height: normal; +} diff --git a/components/CommentInput.js b/components/CommentInput.js new file mode 100644 index 00000000..e85b74d3 --- /dev/null +++ b/components/CommentInput.js @@ -0,0 +1,65 @@ +import { useEffect, useState } from "react"; +import CustomButtonSquare from "./CustomButtonSquare"; +import styles from "./CommentInput.module.css"; +import validInput from "@/utils/validInput"; +import { useUser } from "@/lib/UserContext"; +import { postComments } from "@/utils/commentsApi"; +import testType from "@/utils/validType"; + +export default function CommentInput({ type, id, onRefetch }) { + const [value, setValue] = useState(""); + const [isValid, setIsValid] = useState(false); + const userId = useUser().userId; + + testType(type); + + const header = type === "article" ? "댓글 달기" : "문의하기"; + const placeholder = + type === "article" + ? "댓글을 입력해 주세요" + : "개인정보를 공유 및 요청하거나, 명예 훼손, 무단 광고, 불법 정보 유포시 모니터링 후 삭제될 수 있으며, 이에 대한 민형사상 책임은 게시자에게 있습니다."; + + const handlePostComment = async () => { + if (!userId) { + alert("로그인 후 이용 가능합니다."); + return; + } + + if (!isValid) { + alert("댓글을 작성해 주세요."); + return; + } + + await postComments(type, id, userId, value); + onRefetch(); + }; + + useEffect(() => { + if (validInput(value)) { + setIsValid(true); + } else { + setIsValid(false); + } + }, [value]); + + return ( +
+
{header}
+