diff --git a/apis/httpClient.ts b/apis/httpClient.ts index a8ab8d95..9727371d 100644 --- a/apis/httpClient.ts +++ b/apis/httpClient.ts @@ -128,6 +128,7 @@ export default { portfolio: new HttpClient("/api/portfolio", axiosConfig), portfolioViewsAdd: new HttpClient("/api/portfolio/views/add", axiosConfig), portfolioMember: new HttpClient("/api/portfolio/member", axiosConfig), + portfolioRecommend: new HttpClient("/api/portfolio/recommend", axiosConfig), oauth: new HttpClient("/api/login/oauth2", axiosConfig), skill: new HttpClient("/api/skill", axiosConfig), member: new HttpClient("/api/member", axiosConfig), diff --git a/components/Icon/RecommendIcon.tsx b/components/Icon/RecommendIcon.tsx new file mode 100644 index 00000000..ff450c8d --- /dev/null +++ b/components/Icon/RecommendIcon.tsx @@ -0,0 +1,24 @@ +import React from "react"; + +export default function RecommendIcon({ + size = 12, + fill = "#3E73FB", +}: { + size?: number; + fill?: string; +}) { + return ( + + + + ); +} diff --git a/components/app/PortfolioDetail.tsx b/components/app/PortfolioDetail.tsx index 43c37134..583d9f1c 100644 --- a/components/app/PortfolioDetail.tsx +++ b/components/app/PortfolioDetail.tsx @@ -1,5 +1,5 @@ import { getKoreanDate } from "@/utils/date"; -import type { Portfolio } from "@/types/portfolio.interface"; +import type { Portfolio, RecommendStatus } from "@/types/portfolio.interface"; import { CopyToClipboard } from "react-copy-to-clipboard"; import { useRouter } from "next/router"; import useOverlay from "@/hooks/useOverlay"; @@ -8,6 +8,7 @@ import httpClient from "@/apis"; import { useQueryClient } from "@tanstack/react-query"; import KEY from "@/models/key"; import useUser from "@/hooks/useUser"; +import classNames from "classnames"; import Button from "../atoms/DetailButton"; import MemberGroup from "../atoms/MemberGroup"; import Description from "../portfolio/Description"; @@ -18,10 +19,9 @@ import FilledHeartIcon from "../Icon/FilledHeartIcon"; import EditIcon from "../Icon/EditIcon"; import ChipGroup from "../atoms/ChipGroup"; import GithubIcon from "../Icon/GithubIcon"; -import PencelIcon from "../Icon/PencelIcon"; -import Input from "../atoms/Input"; import Kebab from "../common/KebabMenu"; import TrashCanIcon from "../Icon/TrashCanIcon"; +import RecommendIcon from "../Icon/RecommendIcon"; interface PortfolioDetailProps { portfolio: Portfolio; @@ -30,6 +30,7 @@ interface PortfolioDetailProps { isMyPortfolio: boolean; bookmarks: number; views: number; + recommendStatus: RecommendStatus; } export default function Detail({ @@ -39,6 +40,7 @@ export default function Detail({ isMyPortfolio, bookmarks, views, + recommendStatus, }: PortfolioDetailProps) { const { openToast } = useOverlay(); const { user: userInfo } = useUser(); @@ -75,10 +77,26 @@ export default function Detail({ ); }; + const handleRecommend = () => { + httpClient.portfolioRecommend + .put({ portfolioId: portfolio.portfolioId }) + .then(() => queryClient.invalidateQueries([KEY.PORTFOLIO])); + }; + const handleShare = () => { openToast("복사가 완료되었습니다."); }; + const handleDelete = () => { + // eslint-disable-next-line no-restricted-globals + if (confirm("정말로 삭제하시겠습니까?")) { + httpClient.portfolio.delete({ + data: { portfolioId: portfolio.portfolioId }, + }); + router.push("/"); + } + }; + return (
@@ -154,36 +172,39 @@ export default function Detail({ - {userInfo.memberRoleType === "ROLE_ADMIN" || - (true && ( - <> -
- - 노출순위 변경하기: - - - -
- - - 1} - > - - 수정 - - 1} - > - - 삭제 - - - - - ))} + {userInfo.memberRoleType === "ROLE_ADMIN" && ( + <> + + + + + + 관리자 권한으로 삭제 + + + + + )}
{portfolio.contributorList.length > 0 && (
diff --git a/components/atoms/DetailButton.tsx b/components/atoms/DetailButton.tsx index 401d8d35..0d7780cc 100644 --- a/components/atoms/DetailButton.tsx +++ b/components/atoms/DetailButton.tsx @@ -4,7 +4,7 @@ import { ButtonHTMLAttributes, ReactNode } from "react"; type ButtonStatus = "active" | "disabled"; interface ButtonProps extends ButtonHTMLAttributes { - status: ButtonStatus; + status?: ButtonStatus; children: ReactNode; className?: string; } @@ -14,8 +14,7 @@ const getButtonCss = (status: ButtonStatus): string => { bg-${status === "active" ? "primary-light_gray" : "primary-dark_gray"} text-${status === "active" ? "primary-dark_gray" : "primary-light_gray"} rounded-full - px-[0.65rem] - py-small + p-[0.75rem] shadow flex items-center @@ -34,7 +33,7 @@ export default function DetailButton({ diff --git a/components/common/Portfolio.tsx b/components/common/Portfolio.tsx index c10ac62f..1d479d18 100644 --- a/components/common/Portfolio.tsx +++ b/components/common/Portfolio.tsx @@ -8,6 +8,7 @@ import { getFileDownloadUrl } from "@/utils/file"; import classNames from "classnames"; import config from "@/config"; import ChipGroup from "../atoms/ChipGroup"; +import RecommendIcon from "../Icon/RecommendIcon"; interface PortfolioProps { portfolio: Portfolio; @@ -25,7 +26,7 @@ export default function PortfolioView({ portfolio, onClick }: PortfolioProps) { return (
@@ -81,7 +82,13 @@ export default function PortfolioView({ portfolio, onClick }: PortfolioProps) {
-
+
+ {portfolio.recommendStatus === "RECOMMEND" && ( +
+ + [추천 프로젝트] +
+ )} {`조회수 ${portfolio.views}회 · ${getTimeAgo(portfolio.createdDate)}`}
diff --git a/fixtures/index.ts b/fixtures/index.ts index 71e94a5b..ffdda935 100644 --- a/fixtures/index.ts +++ b/fixtures/index.ts @@ -59,6 +59,7 @@ const portfolio: Portfolio = { views: 0, comments: 0, createdDate: new Date(), + recommendStatus: "NONE", }; const profileDescription = { diff --git a/layouts/Main.tsx b/layouts/Main.tsx index 2c0c409e..c3414324 100644 --- a/layouts/Main.tsx +++ b/layouts/Main.tsx @@ -4,14 +4,21 @@ interface MainLayoutProps { app: ReactNode; title: ReactNode; filter: ReactNode; + recommend?: ReactNode; } -export default function MainLayout({ app, title, filter }: MainLayoutProps) { +export default function MainLayout({ + app, + title, + filter, + recommend, +}: MainLayoutProps) { return (
{title}
{filter}
+ {recommend &&
{recommend}
}
{app}
diff --git a/models/portfolio/index.ts b/models/portfolio/index.ts index b51bf100..ebc026e3 100644 --- a/models/portfolio/index.ts +++ b/models/portfolio/index.ts @@ -96,10 +96,25 @@ const usePortfolio = (portfolioId?: number) => { return { data: data || fixture.portfolio }; }; +const useRecommendPortfolio = () => { + const { data } = useQuery(["recommendPortfolio"], () => + httpClient.portfolio + .search({ + pagination: { size: 6, page: 0 }, + filter: { + recommendStatus: "RECOMMEND", + }, + }) + .then((r) => r.data), + ); + return { data }; +}; + export { usePortfolio, usePortfolioList, usePortfolioListById, useCommentList, useMyPortfolioList, + useRecommendPortfolio, }; diff --git a/pages/index.tsx b/pages/index.tsx index 2dfa66b9..cf5a2ac7 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,18 +1,22 @@ import { MainLayout } from "@/layouts"; import MainTitle from "@/components/main/MainTitle"; import MainFilter from "@/components/main/MainFilter"; -import { usePortfolioList } from "@/models/portfolio"; -import { MainPortfolioList } from "@/components"; +import { usePortfolioList, useRecommendPortfolio } from "@/models/portfolio"; +import { MainPortfolioList, Portfolio as PortfolioView } from "@/components"; import { useState } from "react"; -import { SortType } from "@/types/portfolio.interface"; +import { Portfolio, SortType } from "@/types/portfolio.interface"; +import { useRouter } from "next/router"; +import RecommendIcon from "@/components/Icon/RecommendIcon"; export default function Home() { + const router = useRouter(); const [keyword, setKeyword] = useState("ALL"); const { pages, isFetchingNextPage, fetchNextPage, customHasNextPage } = usePortfolioList( { size: 12 }, keyword !== "ALL" ? { sortType: keyword } : {}, ); + const { data } = useRecommendPortfolio(); return ( } filter={} + recommend={ + ["ALL", "BOOKMARKS"].includes(keyword) ? ( + <> +
+ + 추천 프로젝트 +
+
+ {data?.list.map((portfolio: Portfolio) => ( + + router.push(`/portfolio/${portfolio.portfolioId}`) + } + key={portfolio.portfolioId} + /> + ))} +
+
+ + ) : undefined + } /> ); } diff --git a/pages/portfolio/[portfolioId]/index.tsx b/pages/portfolio/[portfolioId]/index.tsx index d12ba392..ed1f685a 100644 --- a/pages/portfolio/[portfolioId]/index.tsx +++ b/pages/portfolio/[portfolioId]/index.tsx @@ -25,7 +25,7 @@ export default function PortfolioIdPage({ portfolio }: PortfolioIdPageProps) { const dateParsedPortfolio: Portfolio = getDateParsedData(portfolio); const type = dateParsedPortfolio.portfolioType; const { - data: { bookmarkYn, followYn, bookmarks, views }, + data: { bookmarkYn, followYn, bookmarks, views, recommendStatus }, } = usePortfolio(dateParsedPortfolio.portfolioId); const { user: userInfo } = useUser(); const isMyPortfolio = @@ -81,6 +81,7 @@ export default function PortfolioIdPage({ portfolio }: PortfolioIdPageProps) { bookmarks={bookmarks} views={views} isMyPortfolio={isMyPortfolio} + recommendStatus={recommendStatus} /> } comment={ diff --git a/types/portfolio.interface.ts b/types/portfolio.interface.ts index b0c15c8a..db8f305b 100644 --- a/types/portfolio.interface.ts +++ b/types/portfolio.interface.ts @@ -10,12 +10,14 @@ export type PortfolioListType = | "upload" | "detail" | "search"; +export type RecommendStatus = "NONE" | "RECOMMEND"; export type Portfolio = { portfolioId: number; writer: PortfolioWriter; portfolioUrl: string; portfolioType: PortfolioType; + recommendStatus: RecommendStatus; title: string; description: string; bookmarkYn: boolean; @@ -87,6 +89,7 @@ export interface Filter { schoolGrade?: SchoolGradeType; sortType?: SortType; sortDirectionType?: SortDirectionType; + recommendStatus?: RecommendStatus; } export type SearchFilterPropertyType = | "uploadDateType"