Skip to content

Commit

Permalink
Merge pull request #76 from bssm-portfolio/feature/admin-feature
Browse files Browse the repository at this point in the history
관리자 기능 추가 (추천목록, 관리자 포트폴리오 삭제)
  • Loading branch information
pkiop authored Mar 15, 2023
2 parents 0eaf5d2 + 0002676 commit ad7fd9b
Show file tree
Hide file tree
Showing 11 changed files with 149 additions and 44 deletions.
1 change: 1 addition & 0 deletions apis/httpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
24 changes: 24 additions & 0 deletions components/Icon/RecommendIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from "react";

export default function RecommendIcon({
size = 12,
fill = "#3E73FB",
}: {
size?: number;
fill?: string;
}) {
return (
<svg
width={size}
height={size}
viewBox="0 0 13 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12.5627 5.99861L11.1933 4.4158L11.4373 2.31185L9.29453 1.86833L8.19404 0L6.28136 0.884269L4.36868 0L3.28205 1.8711L1.12543 2.31462L1.36937 4.41857L0 5.99861L1.36937 7.59529L1.12543 9.69924L3.28205 10.1428L4.36868 12L6.28136 11.1157L8.19404 12L9.29453 10.1428L11.4373 9.69924L11.1933 7.59529L12.5627 5.99861ZM5.68538 8.03881L3.5343 5.90159L4.37145 5.08108L5.68815 6.45322L8.20513 3.96396L9.03396 4.71795L5.68538 8.03604V8.03881Z"
fill={fill}
/>
</svg>
);
}
87 changes: 54 additions & 33 deletions components/app/PortfolioDetail.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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";
Expand All @@ -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;
Expand All @@ -30,6 +30,7 @@ interface PortfolioDetailProps {
isMyPortfolio: boolean;
bookmarks: number;
views: number;
recommendStatus: RecommendStatus;
}

export default function Detail({
Expand All @@ -39,6 +40,7 @@ export default function Detail({
isMyPortfolio,
bookmarks,
views,
recommendStatus,
}: PortfolioDetailProps) {
const { openToast } = useOverlay();
const { user: userInfo } = useUser();
Expand Down Expand Up @@ -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 (
<div className="mt-small bg-white p-4 rounded">
<div className="w-full h-full flex justify-between flex-col sm:flex-row">
Expand Down Expand Up @@ -154,36 +172,39 @@ export default function Detail({
</Button>
</CopyToClipboard>

{userInfo.memberRoleType === "ROLE_ADMIN" ||
(true && (
<>
<div className="flex items-center bg-primary-dark_gray px-[0.75rem] py-[0.75rem] rounded-full text-white gap-[0.5rem]">
<span className="text-[0.75rem] font-normal">
노출순위 변경하기:
</span>
<Input className="w-[1.25rem] h-[1.25rem]" />
<PencelIcon />
</div>
<Kebab.Provider className="z-30">
<Kebab.Menu className="rounded">
<Kebab.Item
className="pb-[0.3125rem] rounded-t bg-white"
onClick={() => 1}
>
<EditIcon className="w-3 h-3 mr-3" />
<span>수정</span>
</Kebab.Item>
<Kebab.Item
className="pt-[0.3125rem] rounded-b bg-white"
onClick={() => 1}
>
<TrashCanIcon className="w-3 h-3 mr-3" />
<span>삭제</span>
</Kebab.Item>
</Kebab.Menu>
</Kebab.Provider>
</>
))}
{userInfo.memberRoleType === "ROLE_ADMIN" && (
<>
<button
type="button"
onClick={handleRecommend}
className={classNames(
"flex items-center p-[0.65rem] rounded-full text-black gap-[0.375rem] shadow text-[0.75rem]",
{
"bg-primary-light_gray text-black border-black border-[0.5px]":
recommendStatus === "NONE",
"bg-blue text-white": recommendStatus === "RECOMMEND",
},
)}
>
<RecommendIcon
size={18}
fill={recommendStatus === "NONE" ? "black" : "white"}
/>
프로젝트 추천
</button>
<Kebab.Provider className="z-30">
<Kebab.Menu className="rounded">
<Kebab.Item
className="pt-[0.3125rem] rounded-b bg-white"
onClick={handleDelete}
>
<TrashCanIcon className="w-3 h-3 mr-3" />
<span>관리자 권한으로 삭제</span>
</Kebab.Item>
</Kebab.Menu>
</Kebab.Provider>
</>
)}
</div>
{portfolio.contributorList.length > 0 && (
<div className="mb-large flex justify-end">
Expand Down
7 changes: 3 additions & 4 deletions components/atoms/DetailButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ButtonHTMLAttributes, ReactNode } from "react";
type ButtonStatus = "active" | "disabled";

interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
status: ButtonStatus;
status?: ButtonStatus;
children: ReactNode;
className?: string;
}
Expand All @@ -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
Expand All @@ -34,7 +33,7 @@ export default function DetailButton({
<button
type={type === "submit" ? "submit" : "button"}
onClick={onClick}
className={classNames(getButtonCss(status), className)}
className={classNames(className, getButtonCss(status))}
>
{children}
</button>
Expand Down
11 changes: 9 additions & 2 deletions components/common/Portfolio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -25,7 +26,7 @@ export default function PortfolioView({ portfolio, onClick }: PortfolioProps) {

return (
<div
className="flex flex-col flex-1 cursor-pointer min-w-[22.5rem] max-w-[30rem] shadow p-[1.25rem] rounded-lg bg-white"
className="flex flex-col flex-1 cursor-pointer min-w-[22.5rem] max-w-[22.5rem] shadow p-[1.25rem] rounded-lg bg-white"
onClick={onClick}
>
<div className="relative h-[11.25rem]">
Expand Down Expand Up @@ -81,7 +82,13 @@ export default function PortfolioView({ portfolio, onClick }: PortfolioProps) {
</div>
</div>

<div className="text-sxx mt-auto mb-0">
<div className="flex text-sxx mt-auto mb-0 gap-[0.625rem]">
{portfolio.recommendStatus === "RECOMMEND" && (
<div className="flex gap-[0.25rem] text-blue font-bold">
<RecommendIcon />
[추천 프로젝트]
</div>
)}
{`조회수 ${portfolio.views}회 · ${getTimeAgo(portfolio.createdDate)}`}
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions fixtures/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const portfolio: Portfolio = {
views: 0,
comments: 0,
createdDate: new Date(),
recommendStatus: "NONE",
};

const profileDescription = {
Expand Down
9 changes: 8 additions & 1 deletion layouts/Main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<section className="relative z-10 bg-background_blue overflow-hidden flex justify-center min-h-[calc(100vh-4.625rem)]">
<div className="flex flex-col z-20 w-[90vw]">
<div>{title}</div>
<div>{filter}</div>
{recommend && <div>{recommend}</div>}
<div>{app}</div>
</div>
<div className="absolute top-[37.5rem] -left-[1000px] -rotate-[8deg] bg-somago_yellow w-[5000px] h-full -z-10" />
Expand Down
15 changes: 15 additions & 0 deletions models/portfolio/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
32 changes: 29 additions & 3 deletions pages/index.tsx
Original file line number Diff line number Diff line change
@@ -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<SortType>("ALL");
const { pages, isFetchingNextPage, fetchNextPage, customHasNextPage } =
usePortfolioList(
{ size: 12 },
keyword !== "ALL" ? { sortType: keyword } : {},
);
const { data } = useRecommendPortfolio();

return (
<MainLayout
Expand All @@ -26,6 +30,28 @@ export default function Home() {
}
title={<MainTitle />}
filter={<MainFilter keyword={keyword} setKeyword={setKeyword} />}
recommend={
["ALL", "BOOKMARKS"].includes(keyword) ? (
<>
<div className="flex justify-center items-center rounded-full gap-[0.375rem] text-blue font-bold bg-white w-[8.5rem] h-[2.25rem] text-sm mb-[2.5rem]">
<RecommendIcon size={24} />
추천 프로젝트
</div>
<div className="flex flex-wrap gap-[1.5rem]">
{data?.list.map((portfolio: Portfolio) => (
<PortfolioView
portfolio={portfolio}
onClick={() =>
router.push(`/portfolio/${portfolio.portfolioId}`)
}
key={portfolio.portfolioId}
/>
))}
</div>
<hr className="text-white my-10" />
</>
) : undefined
}
/>
);
}
3 changes: 2 additions & 1 deletion pages/portfolio/[portfolioId]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -81,6 +81,7 @@ export default function PortfolioIdPage({ portfolio }: PortfolioIdPageProps) {
bookmarks={bookmarks}
views={views}
isMyPortfolio={isMyPortfolio}
recommendStatus={recommendStatus}
/>
}
comment={
Expand Down
3 changes: 3 additions & 0 deletions types/portfolio.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -87,6 +89,7 @@ export interface Filter {
schoolGrade?: SchoolGradeType;
sortType?: SortType;
sortDirectionType?: SortDirectionType;
recommendStatus?: RecommendStatus;
}
export type SearchFilterPropertyType =
| "uploadDateType"
Expand Down

0 comments on commit ad7fd9b

Please sign in to comment.