From 4434ba473ae3f7449049d9a25885ce075929a389 Mon Sep 17 00:00:00 2001 From: Son Daehyeon Date: Tue, 17 Sep 2024 09:11:13 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Activity=20=EC=96=B4=EB=93=9C=EB=AF=BC?= =?UTF-8?q?=20=ED=8E=98=EC=9D=B4=EC=A7=80=20(without=20editor)=20(#44)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Admin Project Page 추가 * feat: Admin Social Page 추가 * feat: Admin Study Category Page 추가 --- eslint.config.mjs | 20 +- src/api/domain/Member.ts | 8 +- src/api/domain/activity/Project.ts | 4 +- src/api/domain/activity/Social.ts | 12 +- src/api/domain/activity/Study.ts | 18 +- src/app/activity/study/page.tsx | 14 +- src/app/admin/activity/project/page.tsx | 87 +++++-- src/app/admin/activity/social/page.tsx | 90 +++++-- .../admin/activity/study/category/page.tsx | 229 ++++++++++++++++++ src/component/Header.tsx | 3 +- src/component/ProjectCard.tsx | 3 +- src/component/admin/AdminSideBar.tsx | 1 + 12 files changed, 409 insertions(+), 80 deletions(-) create mode 100644 src/app/admin/activity/study/category/page.tsx diff --git a/eslint.config.mjs b/eslint.config.mjs index 492c3da..438e52f 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -3,17 +3,11 @@ import prettierConfig from 'eslint-config-prettier'; import prettierRecommended from 'eslint-plugin-prettier/recommended'; import tseslint from 'typescript-eslint'; -export default tseslint.config( - { - files: ['**/*.js', '**/*.mjs', '**/*.ts'], - extends: [eslint.configs.recommended, ...tseslint.configs.recommended], - rules: { - 'no-console': 'warn', - }, +export default tseslint.config({ + files: ['**/*.js', '**/*.mjs', '**/*.jsx', '**/*.ts', '**/*.tsx'], + extends: [eslint.configs.recommended, ...tseslint.configs.recommended, prettierRecommended], + rules: { + ...prettierConfig.rules, + 'no-console': 'warn', }, - { - files: ['**/*.js', '**/*.mjs', '**/*.ts'], - extends: [prettierRecommended], - rules: prettierConfig.rules, - }, -); +}); diff --git a/src/api/domain/Member.ts b/src/api/domain/Member.ts index c7976c2..4b10a47 100644 --- a/src/api/domain/Member.ts +++ b/src/api/domain/Member.ts @@ -109,8 +109,8 @@ export interface UpdateMyPasswordRequestDto { export interface EachGetMembersResponseDto { _id: string; - createdAt: Date; - updatedAt: Date; + createdAt: string; + updatedAt: string; name: string; avatar: string; description: string | null; @@ -198,8 +198,8 @@ export const RoleKoreanToRole = (role: string): Role => { export interface MemberType { _id: string; - createdAt: Date; - updatedAt: Date; + createdAt: string; + updatedAt: string; name: string; studentId: string; email: string; diff --git a/src/api/domain/activity/Project.ts b/src/api/domain/activity/Project.ts index 56bc4b4..cae36b7 100644 --- a/src/api/domain/activity/Project.ts +++ b/src/api/domain/activity/Project.ts @@ -91,8 +91,8 @@ export interface GetProjectsPageResponseDto { export interface ProjectType { _id: string; - createdAt: Date; - updatedAt: Date; + createdAt: string; + updatedAt: string; type: 'Project' | 'Study' | 'Social'; author: MemberType; title: string; diff --git a/src/api/domain/activity/Social.ts b/src/api/domain/activity/Social.ts index a3f91df..40fffd9 100644 --- a/src/api/domain/activity/Social.ts +++ b/src/api/domain/activity/Social.ts @@ -7,6 +7,10 @@ export class Social { return this.request.get('/activity/social/detail?socialId=' + data.socialId); } + public async getSocialsPage(): Promise { + return this.request.get('/activity/social/max'); + } + public async getSocials(data: GetSocialsRequestDto): Promise { return this.request.get('/activity/social?page=' + data.page); } @@ -71,6 +75,10 @@ export interface GetSocialResponseDto { social: SocialType; } +export interface GetSocialsPageResponseDto { + page: number; +} + export interface GetSocialsResponseDto { socials: SocialType[]; } @@ -79,8 +87,8 @@ export interface GetSocialsResponseDto { export interface SocialType { _id: string; - createdAt: Date; - updatedAt: Date; + createdAt: string; + updatedAt: string; type: 'Project' | 'Study' | 'Social'; title: string; contents: Content[]; diff --git a/src/api/domain/activity/Study.ts b/src/api/domain/activity/Study.ts index 8938342..459fc30 100644 --- a/src/api/domain/activity/Study.ts +++ b/src/api/domain/activity/Study.ts @@ -85,8 +85,14 @@ export interface CreateStudyResponseDto { study: StudyType; } +export interface EachGetCategoriesResponseDto { + _id: string; + name: string; + dependencies: number; +} + export interface GetCategoriesResponseDto { - categories: Category[]; + categories: EachGetCategoriesResponseDto[]; } export interface GetStudiesResponse { @@ -101,21 +107,21 @@ export interface GetStudiesPageResponse { export interface StudyType { _id: string; - createdAt: Date; - updatedAt: Date; + createdAt: string; + updatedAt: string; type: 'Project' | 'Study' | 'Social'; title: string; content: string; author: string; image: string; link: string; - uploadedAt: Date; + uploadedAt: string; category: Category; } export interface Category { _id: string; - createdAt: Date; - updatedAt: Date; + createdAt: string; + updatedAt: string; name: string; } diff --git a/src/app/activity/study/page.tsx b/src/app/activity/study/page.tsx index de58117..37eaca7 100644 --- a/src/app/activity/study/page.tsx +++ b/src/app/activity/study/page.tsx @@ -5,7 +5,7 @@ import { FaAngleDown } from 'react-icons/fa'; import { StudyCard } from '@/component'; -import { Category, StudyType, WinkApi } from '@/api'; +import { EachGetCategoriesResponseDto, StudyType, WinkApi } from '@/api'; import { AnimatePresence, motion } from 'framer-motion'; @@ -14,14 +14,13 @@ const StudyPage = () => { const [maxPage, setMaxPage] = useState(1); const [studies, setStudies] = useState([]); - const [categories, setCategories] = useState([]); + const [categories, setCategories] = useState([]); const [isOpen, setIsOpen] = useState(false); - const [selectedCategory, setSelectedCategory] = useState({ - createdAt: new Date(), - updatedAt: new Date(), + const [selectedCategory, setSelectedCategory] = useState({ _id: 'all', name: 'All', + dependencies: 0, }); const filteredStudies = @@ -32,10 +31,7 @@ const StudyPage = () => { useEffect(() => { const fetchCategories = async () => { const { categories } = await WinkApi.Activity.Study.getCategories(); - setCategories([ - { createdAt: new Date(), updatedAt: new Date(), _id: 'all', name: 'All' }, - ...categories, - ]); + setCategories([{ _id: 'all', name: 'All', dependencies: 0 }, ...categories]); }; const fetchMaxPage = async () => { diff --git a/src/app/admin/activity/project/page.tsx b/src/app/admin/activity/project/page.tsx index 4cbb1bf..d4a7de6 100644 --- a/src/app/admin/activity/project/page.tsx +++ b/src/app/admin/activity/project/page.tsx @@ -1,12 +1,13 @@ 'use client'; -import { useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { FaEdit } from 'react-icons/fa'; import { FaTrashCan } from 'react-icons/fa6'; +import { toast } from 'react-toastify'; import Link from 'next/link'; -import { AdminIconButton, AdminSearchBar, AdminTablePaging, AdminTitle } from '@/component'; +import { AdminIconButton, AdminSearchBar, AdminTablePaging, AdminTitle, Modal } from '@/component'; import { ProjectType, WinkApi } from '@/api'; @@ -20,33 +21,56 @@ const AdminActivityProjectPage = () => { const [query, setQuery] = useState(''); - useEffect(() => { - async function fetchingStudies() { - const { studies } = await WinkApi.Activity.Study.getStudies({ page }); - setProjects(studies); - } + const [deleteModal, setDeleteModal] = useState(null); + useEffect(() => { async function fetchMaxPage() { - const { page } = await WinkApi.Activity.Study.getStudiesPage(); + const { page } = await WinkApi.Activity.Project.getProjectsPage(); setMaxPage(page); } (async () => { - await fetchingStudies(); await fetchMaxPage(); })(); - }, [page]); + }, []); + + useEffect(() => { + (async () => { + await fetchProjects(); + })(); + }, [page, query]); + + async function fetchProjects() { + if (query) { + const { projects } = await WinkApi.Activity.Project.searchProjects({ query }); + setProjects(projects); + } else { + const { projects } = await WinkApi.Activity.Project.getProjects({ page }); + setProjects(projects); + } + } + + const handleDelete = async () => { + if (!deleteModal) return; + + await WinkApi.Activity.ProjectAdmin.deleteProject({ projectId: deleteModal._id }); + await fetchProjects(); + + toast.warn(`"${deleteModal.title}" 프로젝트가 삭제되었습니다.`); + + setDeleteModal(null); + }; return (
- +
} - text="스터디 추가" + text="프로젝트 추가" className="bg-wink-500 hover:bg-wink-600 border-0 text-white" onClick={() => {}} /> @@ -67,21 +91,48 @@ const AdminActivityProjectPage = () => {
- {projects.map((study) => ( -
- - {study.title} + {projects.map((project) => ( +
+ + {project.title} -
{formatDate(study.uploadedAt)}
+
{formatDate(project.createdAt)}
{}} />
- {}} /> + setDeleteModal(project)} + />
))} + setDeleteModal(null)}> +

정말로 삭제하시겠습니까?

+

{deleteModal?.title}

+
+ + + +
+
+ {!query && }
); diff --git a/src/app/admin/activity/social/page.tsx b/src/app/admin/activity/social/page.tsx index a3ab98c..19d260c 100644 --- a/src/app/admin/activity/social/page.tsx +++ b/src/app/admin/activity/social/page.tsx @@ -1,52 +1,74 @@ 'use client'; -import { useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { FaEdit } from 'react-icons/fa'; import { FaTrashCan } from 'react-icons/fa6'; +import { toast } from 'react-toastify'; -import Link from 'next/link'; +import { AdminIconButton, AdminSearchBar, AdminTablePaging, AdminTitle, Modal } from '@/component'; -import { AdminIconButton, AdminSearchBar, AdminTablePaging, AdminTitle } from '@/component'; - -import { StudyType, WinkApi } from '@/api'; +import { SocialType, WinkApi } from '@/api'; import { formatDate } from '@/util'; const AdminActivitySocialPage = () => { - const [studies, setStudies] = useState([]); + const [socials, setSocials] = useState([]); const [page, setPage] = useState(1); const [maxPage, setMaxPage] = useState(1); const [query, setQuery] = useState(''); - useEffect(() => { - async function fetchingStudies() { - const { studies } = await WinkApi.Activity.Study.getStudies({ page }); - setStudies(studies); - } + const [deleteModal, setDeleteModal] = useState(null); + useEffect(() => { async function fetchMaxPage() { - const { page } = await WinkApi.Activity.Study.getStudiesPage(); + const { page } = await WinkApi.Activity.Social.getSocialsPage(); setMaxPage(page); } (async () => { - await fetchingStudies(); await fetchMaxPage(); })(); - }, [page]); + }, []); + + useEffect(() => { + (async () => { + await fetchSocials(); + })(); + }, [page, query]); + + async function fetchSocials() { + if (query) { + const { socials } = await WinkApi.Activity.Social.searchSocials({ query }); + setSocials(socials); + } else { + const { socials } = await WinkApi.Activity.Social.getSocials({ page }); + setSocials(socials); + } + } + + const handleDelete = async () => { + if (!deleteModal) return; + + await WinkApi.Activity.SocialAdmin.deleteSocial({ socialId: deleteModal._id }); + await fetchSocials(); + + toast.warn(`"${deleteModal.title}" 친목 활동이 삭제되었습니다.`); + + setDeleteModal(null); + }; return (
- +
} - text="스터디 추가" + text="친목 활동 추가" className="bg-wink-500 hover:bg-wink-600 border-0 text-white" onClick={() => {}} /> @@ -67,21 +89,43 @@ const AdminActivitySocialPage = () => {
- {studies.map((study) => ( -
- - {study.title} - -
{formatDate(study.uploadedAt)}
+ {socials.map((social) => ( +
+
{social.title}
+
{formatDate(social.createdAt)}
{}} />
- {}} /> + setDeleteModal(social)} + />
))} + setDeleteModal(null)}> +

정말로 삭제하시겠습니까?

+

{deleteModal?.title}

+
+ + + +
+
+ {!query && }
); diff --git a/src/app/admin/activity/study/category/page.tsx b/src/app/admin/activity/study/category/page.tsx new file mode 100644 index 0000000..b5387e6 --- /dev/null +++ b/src/app/admin/activity/study/category/page.tsx @@ -0,0 +1,229 @@ +'use client'; + +import React, { useEffect, useState } from 'react'; +import { FaEdit } from 'react-icons/fa'; +import { FaTrashCan } from 'react-icons/fa6'; +import { toast } from 'react-toastify'; + +import { AdminIconButton, AdminTitle, FormContainer, Modal, TextField } from '@/component'; + +import { useForm } from '@/hook'; + +import { EachGetCategoriesResponseDto, WinkApi } from '@/api'; + +import * as yup from 'yup'; + +type Inputs = 'category'; + +const AdminActivityStudyCategoryPage = () => { + const [categories, setCategories] = useState([]); + + const [createModal, setCreateModal] = useState(false); + const [modifyModal, setModifyModal] = useState(null); + const [deleteModal, setDeleteModal] = useState(null); + + const { values, setValues, errors, setErrors, onChange, validate } = useForm( + yup.object({ + category: yup.string().required('카테고리 이름을 입력해주세요.'), + }), + ); + + useEffect(() => { + (async () => { + await fetchCategories(); + })(); + }, []); + + async function fetchCategories() { + const { categories } = await WinkApi.Activity.Study.getCategories(); + setCategories(categories); + } + + const handleCreate = async () => { + if (!(await validate())) { + return; + } + + await WinkApi.Activity.StudyAdmin.createCategory(values); + await fetchCategories(); + + setCreateModal(false); + setValues({ category: '' }); + setErrors({ category: '' }); + + toast.success(`"${values.category}" 카테고리 추가되었습니다.`); + }; + + const handleModify = async () => { + if (!(await validate())) { + return; + } + + await WinkApi.Activity.StudyAdmin.updateCategory({ + categoryId: modifyModal!._id, + category: values.category, + }); + await fetchCategories(); + + setModifyModal(null); + setValues({ category: '' }); + setErrors({ category: '' }); + + toast.success( + `"${modifyModal?.name}" 카테고리를 "${values.category}" 카테고리로 수정했습니다.`, + ); + }; + + const handleDelete = async () => { + if (!deleteModal) return; + + await WinkApi.Activity.StudyAdmin.deleteCategory({ categoryId: deleteModal._id }); + await fetchCategories(); + + toast.warn(`"${deleteModal.name}" 카테고리가 삭제되었습니다.`); + + setDeleteModal(null); + }; + + return ( +
+ + +
+ } + text="카테고리 추가" + className="bg-wink-500 hover:bg-wink-600 border-0 text-white" + onClick={() => setCreateModal(true)} + /> +
+ +
+
+ 이름 +
+
+ 의존성 +
+
+ 수정 +
+
+ 삭제 +
+
+ + {categories.map((category) => ( +
+
{category.name}
+
{category.dependencies}개
+
+ { + setModifyModal(category); + setValues({ category: category.name }); + setErrors({ category: '' }); + }} + /> +
+
+ setDeleteModal(category)} + /> +
+
+ ))} + + setCreateModal(false)}> +

스터디 추가

+ + +
+ +
+
+ +
+ + + +
+
+ + setModifyModal(null)}> +

스터디 변경

+ + +
+ +
+
+ +
+ + + +
+
+ + setDeleteModal(null)}> +

정말로 삭제하시겠습니까?

+

+ 이 카테고리({deleteModal?.name})를 삭제하면{' '} + {deleteModal?.dependencies}개의 스터디가 모두 삭제됩니다. +

+
+ + + +
+
+
+ ); +}; + +export default AdminActivityStudyCategoryPage; diff --git a/src/component/Header.tsx b/src/component/Header.tsx index 121e2bb..71ef6b8 100644 --- a/src/component/Header.tsx +++ b/src/component/Header.tsx @@ -15,6 +15,7 @@ import { PERMIT_ROLES } from '@/guard'; import { WinkApi } from '@/api'; import logo from '@/public/logo.png'; +import avatar from '@/public/profile.svg'; import { AnimatePresence, motion } from 'framer-motion'; @@ -136,7 +137,7 @@ export const Header: React.FC = () => { onClick={() => handleDropdownToggle('profile')} > Profile = ({ tags, image, }: ProjectCardProps) => { - console.log(image); return (