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"