diff --git a/src/api/admin/customerService/Inquiry.api.ts b/src/api/admin/customerService/inquiry.api.ts similarity index 100% rename from src/api/admin/customerService/Inquiry.api.ts rename to src/api/admin/customerService/inquiry.api.ts diff --git a/src/api/admin/customerService/Notice.api.ts b/src/api/admin/customerService/notice.api.ts similarity index 100% rename from src/api/admin/customerService/Notice.api.ts rename to src/api/admin/customerService/notice.api.ts diff --git a/src/api/admin/tag.api.ts b/src/api/admin/tag.api.ts new file mode 100644 index 00000000..7d1d467c --- /dev/null +++ b/src/api/admin/tag.api.ts @@ -0,0 +1,69 @@ +import type { ApiCommonBasicType } from '../../models/apiCommon'; +import type { TagFormType } from '../../models/tags'; +import { httpClient } from '../http.api'; + +export const postSkillTag = async (formData: FormData) => { + try { + await httpClient.post(`/skill-tag`, formData); + } catch (e) { + console.error(e); + throw e; + } +}; + +export const putSkillTag = async ({ + formData, + id, +}: { + formData: FormData; + id: number; +}) => { + try { + await httpClient.put(`/skill-tag/${id}`, formData); + } catch (e) { + console.error(e); + throw e; + } +}; + +export const deleteSkillTag = async (id: number) => { + try { + await httpClient.delete(`/skill-tag/${id}`); + } catch (e) { + console.error(e); + throw e; + } +}; + +export const postPositionTag = async (name: string) => { + try { + await httpClient.post(`/position-tag`, { name }); + } catch (e) { + console.error(e); + throw e; + } +}; + +export const putPositionTag = async ({ + name, + id, +}: { + name: string; + id: number; +}) => { + try { + await httpClient.put(`/position-tag/${id}`, { name }); + } catch (e) { + console.error(e); + throw e; + } +}; + +export const deletePositionTag = async (id: number) => { + try { + await httpClient.delete(`/position-tag/${id}`); + } catch (e) { + console.error(e); + throw e; + } +}; diff --git a/src/components/admin/adminTags/AdminTagCRUD.styled.ts b/src/components/admin/adminTags/AdminTagCRUD.styled.ts new file mode 100644 index 00000000..55b660eb --- /dev/null +++ b/src/components/admin/adminTags/AdminTagCRUD.styled.ts @@ -0,0 +1,84 @@ +import styled from 'styled-components'; +import { SendButton } from '../../user/customerService/inquiry/Inquiry.styled'; + +export const CRUDContainer = styled.form` + width: 100%; + height: 100%; +`; + +export const CRUDWrapper = styled.div` + width: 70%; + height: 100%; + display: flex; + gap: 1rem; + font-size: 1.2rem; + justify-content: space-between; +`; + +export const InfoContainer = styled.div` + display: flex; + height: 100%; + flex-direction: column; + gap: 1.5rem; + justify-content: center; + /* align-items: center; */ +`; + +export const CRUDButtonWrapper = styled.div` + display: grid; + gap: 1rem; +`; + +export const CRUDButton = styled(SendButton)` + height: 2.3rem; +`; + +export const CRUDTitleWrapper = styled.div` + display: flex; + align-items: center; + gap: 1rem; +`; + +export const CRUDTitleHead = styled.span``; + +export const CRUDTitle = styled.input` + border-bottom: 1px solid ${({ theme }) => theme.color.placeholder}; + padding-left: 0.3rem; + font-size: 1rem; +`; + +export const CRUDDefaultButton = styled.button` + svg { + width: 1rem; + height: 1rem; + } +`; + +export const CRUDImgWrapper = styled.div` + display: flex; + align-items: center; + gap: 1rem; +`; + +export const CRUDImgHead = styled.span``; + +export const CRUDImg = styled.img` + width: 3rem; + border: 1px solid ${({ theme }) => theme.color.grey}; +`; + +export const CRUDImgExplore = styled(SendButton)` + cursor: pointer; + border-radius: ${({ theme }) => theme.borderRadius.primary}; + padding: 0.4rem 1rem; +`; + +export const CRUDImgExplain = styled.span` + max-width: 10rem; +`; + +export const CRUDImgInput = styled.input` + visibility: hidden; + width: 0; + height: 0; +`; diff --git a/src/components/admin/adminTags/AdminTagCRUD.tsx b/src/components/admin/adminTags/AdminTagCRUD.tsx new file mode 100644 index 00000000..f99c0935 --- /dev/null +++ b/src/components/admin/adminTags/AdminTagCRUD.tsx @@ -0,0 +1,254 @@ +import * as S from './AdminTagCRUD.styled'; +import defaultImg from './../../../assets/defaultImg.png'; +import React, { useEffect, useRef, useState } from 'react'; +import type { PositionTag, SkillTag, TagFormType } from '../../../models/tags'; +import Modal from '../../common/modal/Modal'; +import { useModal } from '../../../hooks/useModal'; +import { MODAL_MESSAGE } from '../../../constants/user/modalMessage'; +import { useSearchFilteringTags } from '../../../hooks/user/useSearchFilteringTags'; +import { XMarkIcon } from '@heroicons/react/24/outline'; + +interface TagState { + type: string; + label: string; + needImgFile: boolean; + handlePostTag: (params: T) => void; + handlePutTag: ({ params, id }: { params: T; id: number }) => void; + handleDeleteTag: (id: number) => void; +} + +interface AdminTagCRUDProps { + state: TagState; + itemId: number | null; + onGetItemId: (id: number | null) => void; +} + +interface FormDataType extends TagFormType { + preview: string; +} + +type Skill = Omit; + +type CRUDDataType = Skill | PositionTag; + +type SubmitButtonType = '등록' | '수정' | '삭제'; + +export default function AdminTagCRUD({ + state, + itemId, + onGetItemId, +}: AdminTagCRUDProps) { + const fileInputRef = useRef(null); + const textInputRef = useRef(null); + const { isOpen, message, handleModalOpen, handleModalClose } = useModal(); + const { skillTagsData, positionTagsData } = useSearchFilteringTags(); + + const [buttonType, setButtonType] = useState('등록'); + const [formState, setFormState] = useState({ + name: '', + preview: '', + img: undefined, + }); + + const data: CRUDDataType = + state.type === 'skill' + ? skillTagsData.filter((list) => list.id === itemId)[0] + : positionTagsData.filter((list) => list.id === itemId)[0]; + + const handleSubmitTag = (e: React.FormEvent) => { + e.preventDefault(); + + const formData = new FormData(e.currentTarget as HTMLFormElement); + + const name = String(formData.get('name')); + + const isValid = { + name: formState.name.trim() !== '', + preview: formState.preview.trim() !== '', + }; + + if (!isValid.name) { + return handleModalOpen(MODAL_MESSAGE.emptyTag); + } + if (state.type === 'skill' && !isValid.preview && !itemId) { + return handleModalOpen(MODAL_MESSAGE.emptySkillImg); + } + + switch (buttonType) { + case '등록': + { + const duplication = + state.type === 'skill' + ? skillTagsData.filter((data) => data.name === name) + : positionTagsData.filter((data) => data.name === name); + if (duplication.length > 0) { + return handleModalOpen(MODAL_MESSAGE.duplicationTag); + } + if (state.type === 'skill') { + state.handlePostTag(formData as T); + } else { + state.handlePostTag(name as T); + } + } + break; + case '수정': + { + const duplication = + state.type === 'skill' + ? skillTagsData + .filter((data) => data.id !== itemId) + .filter((data) => data.name === name) + : positionTagsData + .filter((data) => data.id !== itemId) + .filter((data) => data.name === name); + if (duplication.length > 0) { + return handleModalOpen(MODAL_MESSAGE.duplicationTag); + } + if (!itemId) return; + if (state.type === 'skill') { + state.handlePutTag({ params: formData, id: itemId } as { + params: T; + id: number; + }); + } else { + state.handlePutTag({ params: name, id: itemId } as { + params: T; + id: number; + }); + } + } + + break; + case '삭제': + if (itemId) { + state.handleDeleteTag(itemId); + } + break; + default: + break; + } + handleClickReset(); + }; + + const handleChangeValue = (e: React.ChangeEvent) => { + const name = e.target.value; + setFormState((prev) => ({ ...prev, name })); + }; + + const handleChangeFile = (e: React.ChangeEvent) => { + const img = e.target.files?.[0]; + const preview = img ? URL.createObjectURL(img) : ''; + setFormState((prev) => ({ ...prev, preview, img })); + }; + + const handleClickReset = () => { + setFormState({ + name: '', + preview: '', + img: undefined, + }); + onGetItemId(null); + setTimeout(() => { + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + if (textInputRef.current) { + textInputRef.current.value = ''; + } + }, 1000); + }; + + const handleClickChangeButtonType = ( + e: React.MouseEvent + ) => { + const id = e.currentTarget.id as SubmitButtonType; + setButtonType(id); + }; + + useEffect(() => { + if (data) { + if (state.type === 'skill') { + const skillData = data as unknown as Skill; + if (skillData.img) { + const preview = skillData.img as string; + setFormState((prev) => ({ ...prev, name: data.name, preview })); + } + } else { + setFormState((prev) => ({ ...prev, name: data.name })); + } + } + }, [data, state]); + + useEffect(() => { + return () => { + if (formState.preview) { + URL.revokeObjectURL(formState.preview); + } + }; + }, [formState]); + + return ( + + + + + {state.label}: + + {itemId && ( + + + + )} + + {state.type === 'skill' && ( + + 이미지: + + + 파일찾기 + + + + )} + + + + {itemId ? '수정' : '등록'} + + {Boolean(itemId) && ( + + 삭제 + + )} + + + + {message} + + + ); +} diff --git a/src/components/admin/adminTags/AdminTagsBasic.styled.ts b/src/components/admin/adminTags/AdminTagsBasic.styled.ts new file mode 100644 index 00000000..dc5b271b --- /dev/null +++ b/src/components/admin/adminTags/AdminTagsBasic.styled.ts @@ -0,0 +1,25 @@ +import styled from 'styled-components'; + +export const Container = styled.main` + width: 100%; + display: flex; + gap: 3rem; + flex-direction: column; + align-items: center; +`; + +export const CRUDContainer = styled.section` + width: 90%; + min-height: 11rem; + border: 1px solid ${({ theme }) => theme.color.placeholder}; + border-radius: ${({ theme }) => theme.borderRadius.large}; + display: flex; + justify-content: center; + padding: 2rem 3rem; +`; + +export const ItemContainer = styled.section` + width: 90%; + border: 1px solid ${({ theme }) => theme.color.placeholder}; + border-radius: ${({ theme }) => theme.borderRadius.large}; +`; diff --git a/src/components/admin/adminTags/AdminTagsBasic.tsx b/src/components/admin/adminTags/AdminTagsBasic.tsx new file mode 100644 index 00000000..671b9f7a --- /dev/null +++ b/src/components/admin/adminTags/AdminTagsBasic.tsx @@ -0,0 +1,83 @@ +import { useState } from 'react'; +import * as S from './AdminTagsBasic.styled'; +import { useAdminSkillTag } from '../../../hooks/admin/useAdminTag'; +import { useLocation } from 'react-router-dom'; +import AdminTagCRUD from './AdminTagCRUD'; +import AdminSkillTagItems from './skills/AdminSkillTagItems'; +import type { TagFormType } from '../../../models/tags'; +import AdminPositionItems from './positions/AdminPositionItems'; + +export type TWitchTag = 'skill' | 'position'; + +export default function AdminTagsBasic() { + const location = useLocation(); + const pathname = location.pathname; + const witchTag: TWitchTag = pathname.includes('skill') ? 'skill' : 'position'; + + const { + postSkillTagMutate, + putSkillTagMutate, + deleteSkillTagMutate, + postPositionTagMutate, + putPositionTagMutate, + deletePositionTagMutate, + } = useAdminSkillTag(); + const [itemId, setItemId] = useState(null); + const selectTagId = itemId ? [itemId] : []; + + const tagState = { + skill: { + type: 'skill', + label: '스킬', + needImgFile: true, + handlePostTag: (formData: FormData) => + postSkillTagMutate.mutate(formData), + handlePutTag: ({ params, id }: { params: FormData; id: number }) => + putSkillTagMutate.mutate({ formData: params, id }), + handleDeleteTag: (id: number) => deleteSkillTagMutate.mutate(id), + }, + position: { + type: 'position', + label: '포지션', + needImgFile: false, + handlePostTag: (name: string) => postPositionTagMutate.mutate({ name }), + handlePutTag: ({ params, id }: { params: string; id: number }) => + putPositionTagMutate.mutate({ name: params, id }), + handleDeleteTag: (id: number) => deletePositionTagMutate.mutate(id), + }, + }; + + const handleGetItemId = (id: number | null) => { + setItemId(id); + }; + + return ( + + + {witchTag === 'skill' ? ( + + state={tagState.skill} + itemId={itemId} + onGetItemId={handleGetItemId} + /> + ) : ( + + state={tagState.position} + itemId={itemId} + onGetItemId={handleGetItemId} + /> + )} + + + {witchTag === 'skill' ? ( + + ) : ( + + )} + + + ); +} diff --git a/src/components/admin/adminTags/positions/AdminPositionItems.styled.ts b/src/components/admin/adminTags/positions/AdminPositionItems.styled.ts new file mode 100644 index 00000000..8bb7c5c6 --- /dev/null +++ b/src/components/admin/adminTags/positions/AdminPositionItems.styled.ts @@ -0,0 +1,8 @@ +import styled from 'styled-components'; + +export const Container = styled.section` + padding: 2rem; + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +`; diff --git a/src/components/admin/adminTags/positions/AdminPositionItems.tsx b/src/components/admin/adminTags/positions/AdminPositionItems.tsx new file mode 100644 index 00000000..395808d5 --- /dev/null +++ b/src/components/admin/adminTags/positions/AdminPositionItems.tsx @@ -0,0 +1,26 @@ +import { useSearchFilteringTags } from '../../../../hooks/user/useSearchFilteringTags'; +import PositionButton from '../../../common/positionButton/PositionButton'; +import * as S from './AdminPositionItems.styled'; + +interface AdminPositionItemsProps { + onGetItemId: (id: number) => void; +} + +export default function AdminPositionItems({ + onGetItemId, +}: AdminPositionItemsProps) { + const { positionTagsData } = useSearchFilteringTags(); + + return ( + + {positionTagsData.map((list) => ( + onGetItemId(list.id)} + /> + ))} + + ); +} diff --git a/src/components/admin/adminTags/positions/AdminPositionTags.tsx b/src/components/admin/adminTags/positions/AdminPositionTags.tsx new file mode 100644 index 00000000..5d4fb814 --- /dev/null +++ b/src/components/admin/adminTags/positions/AdminPositionTags.tsx @@ -0,0 +1,5 @@ +import AdminTagsBasic from '../AdminTagsBasic'; + +export default function AdminPositionTags() { + return ; +} diff --git a/src/components/admin/adminTags/skills/AdminSkillTagItems.styled.ts b/src/components/admin/adminTags/skills/AdminSkillTagItems.styled.ts new file mode 100644 index 00000000..505a0efe --- /dev/null +++ b/src/components/admin/adminTags/skills/AdminSkillTagItems.styled.ts @@ -0,0 +1,3 @@ +import styled from 'styled-components'; + +export const SkillTagItemWrapper = styled.section``; diff --git a/src/components/admin/adminTags/skills/AdminSkillTagItems.tsx b/src/components/admin/adminTags/skills/AdminSkillTagItems.tsx new file mode 100644 index 00000000..e3385a79 --- /dev/null +++ b/src/components/admin/adminTags/skills/AdminSkillTagItems.tsx @@ -0,0 +1,30 @@ +import * as S from './AdminSkillTagItems.styled'; +import SkillTagBox from '../../../common/skillTagBox/SkillTagBox'; + +interface AdminSKillTagItemsProps { + onGetItemId: (id: number) => void; + selectTagId: number[]; +} + +export default function AdminSkillTagItems({ + onGetItemId, + selectTagId, +}: AdminSKillTagItemsProps) { + const handleClickGetId = (e: React.MouseEvent) => { + e.stopPropagation(); + const target = e.target as HTMLElement; + + const id = Number( + target.dataset.id || target.closest('[data-id]')?.getAttribute('data-id') + ); + + if (!id) return; + onGetItemId(id); + }; + + return ( + + + + ); +} diff --git a/src/components/admin/adminTags/skills/AdminSkillTags.tsx b/src/components/admin/adminTags/skills/AdminSkillTags.tsx new file mode 100644 index 00000000..ee314178 --- /dev/null +++ b/src/components/admin/adminTags/skills/AdminSkillTags.tsx @@ -0,0 +1,5 @@ +import AdminTagsBasic from '../AdminTagsBasic'; + +export default function AdminSkillTags() { + return ; +} diff --git a/src/components/common/admin/sidebar/sidebarList/AdminSidebarList.tsx b/src/components/common/admin/sidebar/sidebarList/AdminSidebarList.tsx index 19caf835..7502472f 100644 --- a/src/components/common/admin/sidebar/sidebarList/AdminSidebarList.tsx +++ b/src/components/common/admin/sidebar/sidebarList/AdminSidebarList.tsx @@ -18,7 +18,8 @@ const iconMap = { notice: , faq: , banner: , - tags: , + skillTags: , + positionTags: , allUser: , reports: , inquiries: , diff --git a/src/components/common/positionButton/PositionButton.styled.ts b/src/components/common/positionButton/PositionButton.styled.ts index 86d6acdd..58d284ec 100644 --- a/src/components/common/positionButton/PositionButton.styled.ts +++ b/src/components/common/positionButton/PositionButton.styled.ts @@ -1,7 +1,5 @@ import styled, { css } from 'styled-components'; -export const Container = styled.div``; - export const PositionButton = styled.button<{ $isSelected: boolean; $isHover: boolean; diff --git a/src/components/common/positionButton/PositionButton.tsx b/src/components/common/positionButton/PositionButton.tsx index 18384464..8a0508f5 100644 --- a/src/components/common/positionButton/PositionButton.tsx +++ b/src/components/common/positionButton/PositionButton.tsx @@ -2,29 +2,28 @@ import * as S from './PositionButton.styled'; interface PositionButtonProps { position: string; - onClick?: (e: React.MouseEvent) => void; + onClickSelect?: (e: React.MouseEvent) => void; isSelected?: boolean; isHover?: boolean; - fontSize: boolean; + fontSize?: boolean; } export default function PositionButton({ position, - onClick, + onClickSelect, isSelected = false, isHover = false, fontSize = false, }: PositionButtonProps) { return ( - - - {position} - - + onClickSelect?.(e)} + > + {position} + ); } diff --git a/src/components/common/skillTagBox/SkillTagBox.tsx b/src/components/common/skillTagBox/SkillTagBox.tsx index d59aa050..50a58037 100644 --- a/src/components/common/skillTagBox/SkillTagBox.tsx +++ b/src/components/common/skillTagBox/SkillTagBox.tsx @@ -1,12 +1,12 @@ import React from 'react'; -import { useSearchFilteringSkillTag } from '../../../hooks/user/useSearchFilteringSkillTag'; +import { useSearchFilteringTags } from '../../../hooks/user/useSearchFilteringTags'; import SkillTag from './skillTag/SkillTag'; import * as S from './SkillTagBox.styled'; import { ArrowUturnLeftIcon } from '@heroicons/react/24/outline'; import { useSaveSearchFiltering } from '../../../hooks/user/useSaveSearchFiltering'; export interface SkillTagBoxProps { - width: string; + width?: string; onHandleSkillTagReset?: React.MouseEventHandler; selectedTag?: number[]; isMain?: boolean; @@ -14,13 +14,13 @@ export interface SkillTagBoxProps { } export default function SkillTagBox({ - width, + width = '100%', onHandleSkillTagReset, selectedTag, isMain = false, isCreate = false, }: SkillTagBoxProps) { - const { skillTagsData } = useSearchFilteringSkillTag(); + const { skillTagsData } = useSearchFilteringTags(); const { searchFilters } = useSaveSearchFiltering(); const searchFiltersSkillTag = searchFilters.skillTag; @@ -36,11 +36,12 @@ export default function SkillTagBox({ skillTagData={skillTagData} key={`skillTagBox-${skillTagData.id}`} $select={ - (isMain && + selectedTag?.includes(skillTagData.id) || + ((isMain && searchFiltersSkillTag?.includes(skillTagData.id)) || (isCreate && selectedTag?.includes(skillTagData.id)) ? true - : false + : false) } /> ))} diff --git a/src/components/common/skillTagBox/skillTag/SkillTag.tsx b/src/components/common/skillTagBox/skillTag/SkillTag.tsx index ea209e0e..a7239736 100644 --- a/src/components/common/skillTagBox/skillTag/SkillTag.tsx +++ b/src/components/common/skillTagBox/skillTag/SkillTag.tsx @@ -14,6 +14,7 @@ export default function SkillTag({ skillTagData, $select }: SkillTagProps) { image={skillTagData.img} skillTag={skillTagData.name} $select={$select} + updatedAt={skillTagData.updatedAt} skillTagId={skillTagData.id} /> {skillTagData.name} diff --git a/src/components/common/skillTagBox/skillTag/skillTagImg/SkillTagImg.tsx b/src/components/common/skillTagBox/skillTag/skillTagImg/SkillTagImg.tsx index 924444a3..d52e4db9 100644 --- a/src/components/common/skillTagBox/skillTag/skillTagImg/SkillTagImg.tsx +++ b/src/components/common/skillTagBox/skillTag/skillTagImg/SkillTagImg.tsx @@ -3,18 +3,24 @@ import * as S from './SkillTagImg.styled'; export interface SkillTagImgProps { image: string; skillTag: string; + updatedAt: string; $select?: boolean; skillTagId?: number; } export default function SkillTagImg({ image, skillTag, + updatedAt, $select, skillTagId, }: SkillTagImgProps) { return ( - + ); } diff --git a/src/components/user/home/projectCardLists/cardList/CardList.tsx b/src/components/user/home/projectCardLists/cardList/CardList.tsx index 4975a5bb..82c0f65a 100644 --- a/src/components/user/home/projectCardLists/cardList/CardList.tsx +++ b/src/components/user/home/projectCardLists/cardList/CardList.tsx @@ -28,7 +28,6 @@ export default function CardList({ list }: CardListProps) { ))} {list.positions.length > listPositionTag.length && ( diff --git a/src/components/user/home/searchFiltering/filteringContents/FilteringContents.tsx b/src/components/user/home/searchFiltering/filteringContents/FilteringContents.tsx index 9288dbcd..bd434ad1 100644 --- a/src/components/user/home/searchFiltering/filteringContents/FilteringContents.tsx +++ b/src/components/user/home/searchFiltering/filteringContents/FilteringContents.tsx @@ -3,14 +3,14 @@ import * as S from './FilteringContents.styled'; import beginner from '../../../../../assets/beginner.svg'; import { ChevronDownIcon } from '@heroicons/react/24/outline'; import React, { useState } from 'react'; -import { useSearchFilteringSkillTag } from '../../../../../hooks/user/useSearchFilteringSkillTag'; +import { useSearchFilteringTags } from '../../../../../hooks/user/useSearchFilteringTags'; import { useOutsideClick } from '../../../../../hooks/user/useOutsideClick'; import { useSaveSearchFiltering } from '../../../../../hooks/user/useSaveSearchFiltering'; import { SEARCH_FILTERING_DEFAULT_VALUE } from '../../../../../constants/user/homeConstants'; import SkillTagBox from '../../../../common/skillTagBox/SkillTagBox'; export default function FilteringContents() { - const { positionTagsData, methodTagsData } = useSearchFilteringSkillTag(); + const { positionTagsData, methodTagsData } = useSearchFilteringTags(); const { searchFilters, handleUpdateFilters } = useSaveSearchFiltering(); const [skillTagButtonToggle, setSkillTagButtonToggle] = useState(false); @@ -18,7 +18,7 @@ export default function FilteringContents() { setSkillTagButtonToggle((prev) => !prev); }; - const handleSkillTagFilterClick = (e: React.MouseEvent) => { + const handleSkillTagFilterClick = (e: React.MouseEvent) => { e.stopPropagation(); const target = e.target as HTMLElement; diff --git a/src/components/user/mypage/myProfile/editProfile/EditProfile.tsx b/src/components/user/mypage/myProfile/editProfile/EditProfile.tsx index d89ead01..eb89563b 100644 --- a/src/components/user/mypage/myProfile/editProfile/EditProfile.tsx +++ b/src/components/user/mypage/myProfile/editProfile/EditProfile.tsx @@ -10,7 +10,7 @@ import { SquaresPlusIcon, XMarkIcon } from '@heroicons/react/24/outline'; import { useNavigate, useOutletContext } from 'react-router-dom'; import MyProfileWrapper from '../MyProfileWrapper'; import type { UserInfo } from '../../../../../models/userInfo'; -import { useSearchFilteringSkillTag } from '../../../../../hooks/user/useSearchFilteringSkillTag'; +import { useSearchFilteringTags } from '../../../../../hooks/user/useSearchFilteringTags'; import { useEditMyProfileInfo } from '../../../../../hooks/user/useMyInfo'; import useNickNameVerification from '../../../../../hooks/user/useNicknameVerification'; import { ROUTES } from '../../../../../constants/routes'; @@ -34,7 +34,7 @@ export default function EditProfile() { scrollRef: React.RefObject; handleModalOpen: (message: string) => void; } = useOutletContext(); - const { skillTagsData, positionTagsData } = useSearchFilteringSkillTag(); + const { skillTagsData, positionTagsData } = useSearchFilteringTags(); const { editMyProfile } = useEditMyProfileInfo(handleModalOpen); const { nicknameMessage, handleDuplicationNickname } = useNickNameVerification(); diff --git a/src/components/user/projectFormComponents/projectInformationInput/ProjectInformationInput.tsx b/src/components/user/projectFormComponents/projectInformationInput/ProjectInformationInput.tsx index 6edd443e..b3a89955 100644 --- a/src/components/user/projectFormComponents/projectInformationInput/ProjectInformationInput.tsx +++ b/src/components/user/projectFormComponents/projectInformationInput/ProjectInformationInput.tsx @@ -6,7 +6,7 @@ import * as S from './ProjectInformationInput.styled'; import Input from '../inputComponent/InputComponent'; import type { ProjectDetailPlusExtended } from '../../../../models/projectDetail'; import type { CreateProjectFormValues } from '../../../../models/createProject'; -import { useSearchFilteringSkillTag } from '../../../../hooks/user/useSearchFilteringSkillTag'; +import { useSearchFilteringTags } from '../../../../hooks/user/useSearchFilteringTags'; import { PROJECT_DATA } from '../../../../constants/user/projectConstants'; interface ProjectInformationProps { @@ -22,7 +22,7 @@ const ProjectInformationInput = ({ setValue, apiData, }: ProjectInformationProps) => { - const { positionTagsData, methodTagsData } = useSearchFilteringSkillTag(); + const { positionTagsData, methodTagsData } = useSearchFilteringTags(); return ( <> diff --git a/src/components/user/projectFormComponents/projectInformationInput/positionComponent/PositionComponent.tsx b/src/components/user/projectFormComponents/projectInformationInput/positionComponent/PositionComponent.tsx index ad267d8e..00d0e4a1 100644 --- a/src/components/user/projectFormComponents/projectInformationInput/positionComponent/PositionComponent.tsx +++ b/src/components/user/projectFormComponents/projectInformationInput/positionComponent/PositionComponent.tsx @@ -1,3 +1,4 @@ +import React from 'react'; import useTagSelectors from '../../../../../hooks/user/ProjectHooks/useTagSelectors'; import type { CreateProjectFormValues } from '../../../../../models/createProject'; import type { PositionTag } from '../../../../../models/tags'; @@ -27,6 +28,13 @@ const MozipCategoryComponent = ({ fieldName: 'position', }); + const handleClickSelect = ( + e: React.MouseEvent, + idx: number + ) => { + handleClick(e, idx + 1); + }; + return ( @@ -36,9 +44,7 @@ const MozipCategoryComponent = ({ ) => - handleClick(e, idx + 1) - } + onClickSelect={(e) => handleClickSelect(e, idx)} key={idx + 1} isHover={true} fontSize={true} diff --git a/src/constants/admin/sidebar.ts b/src/constants/admin/sidebar.ts index 541e8f49..40df363e 100644 --- a/src/constants/admin/sidebar.ts +++ b/src/constants/admin/sidebar.ts @@ -30,9 +30,14 @@ export const SIDEBAR_LIST = { router: ADMIN_ROUTE.banner, }, { - name: 'tags', - title: '태그관리', - router: ADMIN_ROUTE.tags, + name: 'skillTags', + title: '스킬 태그', + router: ADMIN_ROUTE.skillTags, + }, + { + name: 'positionTags', + title: '포지션 태그', + router: ADMIN_ROUTE.positionTags, }, ], user: [ diff --git a/src/constants/routes.ts b/src/constants/routes.ts index d93d7150..3f1cc3c1 100644 --- a/src/constants/routes.ts +++ b/src/constants/routes.ts @@ -37,7 +37,8 @@ export const ADMIN_ROUTE = { notice: 'notice', faq: 'faq', banner: 'banner', - tags: 'tags', + skillTags: 'skill-tags', + positionTags: 'position-tags', users: 'users', reports: 'reports', inquiries: 'inquiries', diff --git a/src/constants/user/modalMessage.ts b/src/constants/user/modalMessage.ts index 0fa52b62..0556fbe4 100644 --- a/src/constants/user/modalMessage.ts +++ b/src/constants/user/modalMessage.ts @@ -27,4 +27,7 @@ export const MODAL_MESSAGE = { alreadyApply: '이미 참여한/지원하신 공고 입니다.', noMemberToEvaluate: '평가 할 멤버가 없습니다.', noTagsData: '대표 스킬/포지션을 입력 하셔야 사용할 수 있습니다.', + duplicationTag: '이미 존재하는 태그입니다.', + emptyTag: '태그명을 입력하세요.', + emptySkillImg: '스킬 이미지를 추가하세요.', } as const; diff --git a/src/hooks/admin/useAdminInquiry.ts b/src/hooks/admin/useAdminInquiry.ts index ebb7d3a1..a24e311c 100644 --- a/src/hooks/admin/useAdminInquiry.ts +++ b/src/hooks/admin/useAdminInquiry.ts @@ -4,7 +4,7 @@ import { getInquiryDetail, patchInquiryAnswer, postInquiryAnswer, -} from '../../api/admin/customerService/Inquiry.api'; +} from '../../api/admin/customerService/inquiry.api'; import type { InquiryAnswerBody } from '../../models/inquiry'; import { AxiosError } from 'axios'; import { CustomerService } from '../queries/keys'; diff --git a/src/hooks/admin/useAdminNotice.ts b/src/hooks/admin/useAdminNotice.ts index e06a5cac..e4345710 100644 --- a/src/hooks/admin/useAdminNotice.ts +++ b/src/hooks/admin/useAdminNotice.ts @@ -3,7 +3,7 @@ import { deleteNotice, postNotice, putNotice, -} from '../../api/admin/customerService/Notice.api'; +} from '../../api/admin/customerService/notice.api'; import { AxiosError } from 'axios'; import { CustomerService } from '../queries/keys'; import { useNavigate } from 'react-router-dom'; diff --git a/src/hooks/admin/useAdminTag.ts b/src/hooks/admin/useAdminTag.ts new file mode 100644 index 00000000..3fc12ec4 --- /dev/null +++ b/src/hooks/admin/useAdminTag.ts @@ -0,0 +1,97 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { + deletePositionTag, + deleteSkillTag, + postPositionTag, + postSkillTag, + putPositionTag, + putSkillTag, +} from '../../api/admin/tag.api'; +import { AxiosError } from 'axios'; +import type { TagFormType } from '../../models/tags'; +import { Tag } from '../queries/keys'; + +export const useAdminSkillTag = () => { + const queryClient = useQueryClient(); + + const postSkillTagMutate = useMutation({ + mutationFn: (formData) => postSkillTag(formData), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: Tag.skillTag, + }); + }, + }); + + const putSkillTagMutate = useMutation< + void, + AxiosError, + { + formData: FormData; + id: number; + } + >({ + mutationFn: ({ formData, id }) => putSkillTag({ formData, id }), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: Tag.skillTag, + }); + }, + }); + + const deleteSkillTagMutate = useMutation({ + mutationFn: (id: number) => deleteSkillTag(id), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: Tag.skillTag, + }); + }, + }); + + const postPositionTagMutate = useMutation< + void, + AxiosError, + Pick + >({ + mutationFn: ({ name }) => postPositionTag(name), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: Tag.positionTag, + }); + }, + }); + + const putPositionTagMutate = useMutation< + void, + AxiosError, + { + name: string; + id: number; + } + >({ + mutationFn: ({ name, id }) => putPositionTag({ name, id }), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: Tag.positionTag, + }); + }, + }); + + const deletePositionTagMutate = useMutation({ + mutationFn: (id: number) => deletePositionTag(id), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: Tag.positionTag, + }); + }, + }); + + return { + postSkillTagMutate, + putSkillTagMutate, + deleteSkillTagMutate, + postPositionTagMutate, + putPositionTagMutate, + deletePositionTagMutate, + }; +}; diff --git a/src/hooks/admin/useGetAllInquiries.ts b/src/hooks/admin/useGetAllInquiries.ts index 8f1d21b9..5f7fc046 100644 --- a/src/hooks/admin/useGetAllInquiries.ts +++ b/src/hooks/admin/useGetAllInquiries.ts @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query'; import { Inquiries } from '../queries/keys'; -import { getAllInquiries } from '../../api/admin/customerService/Inquiry.api'; +import { getAllInquiries } from '../../api/admin/customerService/inquiry.api'; export const useGetAllInquiries = () => { const { @@ -10,7 +10,6 @@ export const useGetAllInquiries = () => { } = useQuery({ queryKey: [Inquiries.allInquiries], queryFn: () => getAllInquiries(), - select: (allInquiries) => allInquiries.slice(0, 5), }); return { allInquiriesData, isLoading, isFetching }; diff --git a/src/hooks/queries/keys.ts b/src/hooks/queries/keys.ts index 8caeccc2..bb4b97a4 100644 --- a/src/hooks/queries/keys.ts +++ b/src/hooks/queries/keys.ts @@ -47,25 +47,31 @@ export const ProjectMemberListEval = { export const ActivityLog = { myComments: ['MyComments'], myInquiries: ['MyInquiries'], -}; +} as const; export const Inquiries = { allInquiries: ['AllInquiries'], -}; +} as const; export const CustomerService = { faq: 'faq', notice: 'notice', noticeDetail: 'noticeDetail', inquiryDetail: 'inquiryDetail', -}; +} as const; export const ReportData = { allReports: ['AllReports'], -}; +} as const; export const UserData = { allUser: ['AllUser'], allUserPreview: ['AllUserPreview'], userInfo: ['userInfo'], -}; +} as const; + +export const Tag = { + skillTag: ['skillTagsData'], + positionTag: ['positionsData'], + method: ['fetchMethodTag'], +} as const; diff --git a/src/hooks/user/useSearchFilteringSkillTag.ts b/src/hooks/user/useSearchFilteringTags.ts similarity index 86% rename from src/hooks/user/useSearchFilteringSkillTag.ts rename to src/hooks/user/useSearchFilteringTags.ts index c5a87130..2dcbe5b4 100644 --- a/src/hooks/user/useSearchFilteringSkillTag.ts +++ b/src/hooks/user/useSearchFilteringTags.ts @@ -6,8 +6,9 @@ import { getPositionTag, getSkillTag, } from '../../api/projectSearchFiltering.api'; +import { Tag } from '../queries/keys'; -export const useSearchFilteringSkillTag = () => { +export const useSearchFilteringTags = () => { const [skillTagsData, setSkillTagsData] = useState([]); const [positionTagsData, setPositionTagsData] = useState([]); const [methodTagsData, setMethodTagsData] = useState([]); @@ -15,19 +16,19 @@ export const useSearchFilteringSkillTag = () => { const queries = useQueries({ queries: [ { - queryKey: ['skillTagsData', skillTagsData], + queryKey: Tag.skillTag, queryFn: () => getSkillTag(), staleTime: Infinity, gcTime: Infinity, }, { - queryKey: ['positionsData', positionTagsData], + queryKey: Tag.positionTag, queryFn: () => getPositionTag(), staleTime: Infinity, gcTime: Infinity, }, { - queryKey: ['fetchMethodTag', methodTagsData], + queryKey: Tag.method, queryFn: () => getMethodTag(), staleTime: Infinity, gcTime: Infinity, diff --git a/src/models/tags.ts b/src/models/tags.ts index bb7922b2..5d206b09 100644 --- a/src/models/tags.ts +++ b/src/models/tags.ts @@ -4,7 +4,7 @@ export interface SkillTag { id: number; name: string; img: string; - createdAt: string; + updatedAt: string; } export interface PositionTag { @@ -30,3 +30,8 @@ export interface ApiPositionTag extends ApiCommonType { export interface ApiMethodTag extends ApiCommonType { data: MethodTag[] | null; } + +export interface TagFormType { + name: string; + img?: File | undefined; +} diff --git a/src/pages/admin/adminTags/AdminTags.tsx b/src/pages/admin/adminTags/AdminTags.tsx deleted file mode 100644 index b8bd4f27..00000000 --- a/src/pages/admin/adminTags/AdminTags.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function AdminTags() { - return
; -} diff --git a/src/pages/admin/adminTags/position/AdminPositionTagsPage.tsx b/src/pages/admin/adminTags/position/AdminPositionTagsPage.tsx new file mode 100644 index 00000000..45b2d834 --- /dev/null +++ b/src/pages/admin/adminTags/position/AdminPositionTagsPage.tsx @@ -0,0 +1,5 @@ +import CommonAdminPage from '../../CommonAdminPage'; + +export default function AdminPositionTagsPage() { + return ; +} diff --git a/src/pages/admin/adminTags/skill/AdminSkillTagsPage.tsx b/src/pages/admin/adminTags/skill/AdminSkillTagsPage.tsx new file mode 100644 index 00000000..67a6d4a9 --- /dev/null +++ b/src/pages/admin/adminTags/skill/AdminSkillTagsPage.tsx @@ -0,0 +1,5 @@ +import CommonAdminPage from '../../CommonAdminPage'; + +export default function AdminSkillTagsPage() { + return ; +} diff --git a/src/routes/AdminRoutes.tsx b/src/routes/AdminRoutes.tsx index f60a7d1c..675ca8bd 100644 --- a/src/routes/AdminRoutes.tsx +++ b/src/routes/AdminRoutes.tsx @@ -2,12 +2,6 @@ import NotFoundPage from '../pages/notFoundPage/NotFoundPage'; import { lazy, Suspense } from 'react'; import { ADMIN_ROUTE } from '../constants/routes'; import ProtectAdminRoute from './ProtectAdminRoute'; -import AdminUserDetail from '../components/admin/adminUserDetail/AdminUserDetail'; -import UserProjects from '../components/user/userPage/userProjectList/UserProjectList'; -import Profile from '../components/user/mypage/myProfile/profile/Profile'; -import { Navigate } from 'react-router-dom'; -import ActivityLog from '../components/user/mypage/activityLog/ActivityLog'; -import Notifications from '../components/user/mypage/notifications/Notifications'; import { Spinner } from '../components/common/loadingSpinner/LoadingSpinner.styled'; const Sidebar = lazy( @@ -33,7 +27,18 @@ const FAQWrite = lazy( () => import('../pages/admin/adminFAQ/adminFAQWrite/AdminFAQWritePage') ); const Banner = lazy(() => import('../pages/admin/adminBanner/AdminBanner')); -const Tags = lazy(() => import('../pages/admin/adminTags/AdminTags')); +const SkillTagPage = lazy( + () => import('../pages/admin/adminTags/skill/AdminSkillTagsPage') +); +const SkillTags = lazy( + () => import('../components/admin/adminTags/skills/AdminSkillTags') +); +const PositionTagPage = lazy( + () => import('../pages/admin/adminTags/position/AdminPositionTagsPage') +); +const PositionTags = lazy( + () => import('../components/admin/adminTags/positions/AdminPositionTags') +); const AdminUser = lazy(() => import('../pages/admin/adminUser/AdminUser')); const Reports = lazy(() => import('../pages/admin/adminReports/AdminReports')); const Inquiries = lazy( @@ -114,42 +119,18 @@ export const AdminRoutes = () => { element: , }, { - path: ADMIN_ROUTE.tags, - element: , + path: ADMIN_ROUTE.skillTags, + element: , + children: [{ index: true, element: }], }, { - path: ADMIN_ROUTE.users, - element: , + path: ADMIN_ROUTE.positionTags, + element: , + children: [{ index: true, element: }], }, { - path: `${ADMIN_ROUTE.users}/:userId`, - element: , - children: [ - { - index: true, - element: , - }, - { - path: `${ADMIN_ROUTE.basic}`, - element: , - }, - { - path: `${ADMIN_ROUTE.log}`, - element: , - }, - { - path: `${ADMIN_ROUTE.appliedProject}`, - element: , - }, - { - path: `${ADMIN_ROUTE.joinedProject}`, - element: , - }, - { - path: `${ADMIN_ROUTE.createdProject}`, - element: , - }, - ], + path: ADMIN_ROUTE.users, + element: , }, { path: ADMIN_ROUTE.reports,