diff --git a/.eslintrc.cjs b/.eslintrc.cjs index a349ba84..b7881cdf 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -26,6 +26,7 @@ module.exports = { ], '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-unused-vars': 'off', + 'react-refresh/only-export-components': 'off', }, settings: { 'import/resolver': { diff --git a/package-lock.json b/package-lock.json index 06de9bab..2287836e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@tanstack/react-query": "^5.17.19", "@toss/use-overlay": "^1.3.8", "axios": "^1.5.1", - "concept-be-design-system": "^0.4.12", + "concept-be-design-system": "^0.5.3", "react": "^18.2.0", "react-dom": "^18.2.0", "react-helmet-async": "^2.0.4", @@ -2480,9 +2480,9 @@ "license": "MIT" }, "node_modules/concept-be-design-system": { - "version": "0.4.12", - "resolved": "https://registry.npmjs.org/concept-be-design-system/-/concept-be-design-system-0.4.12.tgz", - "integrity": "sha512-ktgcRqgo1PKlRkcu4qU33fdNTeZZZVor+pF1QF/ibaemagYjvESsgiluLHwxqxxhUOK+sGEhVy9dUPWUEkSF8g==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/concept-be-design-system/-/concept-be-design-system-0.5.3.tgz", + "integrity": "sha512-ThU8egZDs1krKizh6YqXiTvlQcUQV46HFD1lGVlsTThrvH1m2YRkPht6NwbZ2rvsO+K5e8QGS9CM1PzZUSiJiA==", "dependencies": { "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", diff --git a/package.json b/package.json index e98064ad..6d2efb0b 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "@tanstack/react-query": "^5.17.19", "@toss/use-overlay": "^1.3.8", "axios": "^1.5.1", - "concept-be-design-system": "^0.4.12", + "concept-be-design-system": "^0.5.3", "react": "^18.2.0", "react-dom": "^18.2.0", "react-helmet-async": "^2.0.4", diff --git a/src/components/ErrorBoundary/ApiErrorBoundary.tsx b/src/components/ErrorBoundary/ApiErrorBoundary.tsx index 309f2669..a1525877 100644 --- a/src/components/ErrorBoundary/ApiErrorBoundary.tsx +++ b/src/components/ErrorBoundary/ApiErrorBoundary.tsx @@ -114,7 +114,7 @@ class ErrorBoundary extends Component { if (this.state.errorDetail === 'not-found') { return ( <> - + ; ); diff --git a/src/components/ErrorBoundary/StayDuringRoutingAlert.tsx b/src/components/ErrorBoundary/StayDuringRoutingAlert.tsx index 3e5136de..76a83177 100644 --- a/src/components/ErrorBoundary/StayDuringRoutingAlert.tsx +++ b/src/components/ErrorBoundary/StayDuringRoutingAlert.tsx @@ -1,8 +1,7 @@ import { useOverlay } from '@toss/use-overlay'; +import { Alert } from 'concept-be-design-system'; import { useCallback, useEffect } from 'react'; -import Alert from '../Modal/Alert'; - interface OpenAlertProps { content: string; buttonContent?: string; diff --git a/src/components/Modal/Alert.tsx b/src/components/Modal/Alert.tsx deleted file mode 100644 index 5cbe18c9..00000000 --- a/src/components/Modal/Alert.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import styled from '@emotion/styled'; -import { Flex, Text, theme } from 'concept-be-design-system'; - -interface ModalProps { - content: string; - buttonContent?: string; - isOpen: boolean; - onClose: () => void; -} - -const Alert = ({ content, buttonContent = '확인', isOpen, onClose }: ModalProps) => { - return ( - <> - {isOpen && ( - - - - - {content} - - - - {buttonContent} - - - - - )} - - ); -}; - -export default Alert; - -const Wrapper = styled.div` - display: flex; - justify-content: center; - align-items: center; - position: fixed; - height: 100%; - width: 100%; - inset: 0; - z-index: 10; -`; - -const ModalWrapper = styled.div` - display: flex; - flex-direction: column; - align-items: center; - justify-content: space-between; - width: 280px; - height: 167px; - border-radius: 14px; - box-shadow: - rgba(0, 0, 0, 0.2) 0px 11px 15px -7px, - rgba(0, 0, 0, 0.14) 0px 24px 38px 3px, - rgba(0, 0, 0, 0.12) 0px 9px 46px 8px; - background-color: #fff; - color: inherit; - z-index: 11; - white-space: pre-wrap; -`; - -const ContentWrapper = styled.div` - width: 149px; - text-align: center; - font-size: ${theme.font.suit14r.fontSize}px; - font-weight: ${theme.font.suit14r.fontWeight}; - line-height: 160%; - word-break: keep-all; -`; - -const Overlay = styled.div` - position: fixed; - display: flex; - align-items: center; - justify-content: center; - background-color: rgba(0, 0, 0, 0.54); - inset: 0; -`; diff --git a/src/components/Modal/Confirm.tsx b/src/components/Modal/Confirm.tsx deleted file mode 100644 index ad1e8d65..00000000 --- a/src/components/Modal/Confirm.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import styled from '@emotion/styled'; -import { Flex, Text, theme } from 'concept-be-design-system'; - -interface ModalProps { - content: string; - closeButtonContent?: string; - confirmButtonContent?: string; - isOpen: boolean; - onClose: () => void; - onConfirm?: () => void; -} - -const Confirm = ({ - content, - closeButtonContent = '취소', - confirmButtonContent = '확인', - isOpen, - onClose, - onConfirm, -}: ModalProps) => { - const onClickConfirm = () => { - if (onConfirm) onConfirm(); - - onClose(); - }; - - return ( - <> - {isOpen && ( - - - - - {content} - - - {closeButtonContent !== '' && ( - - - {closeButtonContent} - - - )} - {confirmButtonContent !== '' && ( - - - {confirmButtonContent} - - - )} - - - - )} - - ); -}; - -export default Confirm; - -const Wrapper = styled.div` - display: flex; - justify-content: center; - align-items: center; - position: fixed; - height: 100%; - width: 100%; - inset: 0; - z-index: 10; -`; - -const ModalWrapper = styled.div` - display: flex; - flex-direction: column; - align-items: center; - justify-content: space-between; - width: 280px; - height: 167px; - border-radius: 14px; - box-shadow: - rgba(0, 0, 0, 0.2) 0px 11px 15px -7px, - rgba(0, 0, 0, 0.14) 0px 24px 38px 3px, - rgba(0, 0, 0, 0.12) 0px 9px 46px 8px; - background-color: #fff; - color: inherit; - z-index: 11; - white-space: pre-wrap; - word-break: keep-all; -`; - -const ContentWrapper = styled.div` - width: 149px; - text-align: center; - font-size: ${theme.font.suit14r.fontSize}px; - font-weight: ${theme.font.suit14r.fontWeight}; - line-height: 160%; -`; - -const Overlay = styled.div` - position: fixed; - display: flex; - align-items: center; - justify-content: center; - background-color: rgba(0, 0, 0, 0.54); - inset: 0; -`; diff --git a/src/components/Padding.tsx b/src/components/Padding.tsx deleted file mode 100644 index f773a78c..00000000 --- a/src/components/Padding.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import styled from '@emotion/styled'; - -interface PaddingProps { - top?: number | string; - left?: number | string; - right?: number | string; - bottom?: number | string; -} - -const Padding = ({ top, left, right, bottom }: PaddingProps) => { - return ; -}; - -export default Padding; - -const SpacerBox = styled.div` - padding-top: ${(props) => props.top}px; - padding-bottom: ${(props) => props.bottom}px; - padding-left: ${(props) => props.left}px; - padding-right: ${(props) => props.right}px; -`; diff --git a/src/components/Skeleton/Skeleton.tsx b/src/components/Skeleton/Skeleton.tsx deleted file mode 100644 index 775479c3..00000000 --- a/src/components/Skeleton/Skeleton.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { css, keyframes } from '@emotion/react'; -import { theme } from 'concept-be-design-system'; -import { ComponentPropsWithoutRef } from 'react'; - -export interface SkeletonProps extends ComponentPropsWithoutRef<'div'> { - width?: string; - height?: string; - /** - * Skeleton 모양 - * - * @default 'square' - */ - variant?: 'square' | 'circle'; -} - -const Skeleton = ({ width = '100%', height = '24px', variant = 'square', className = '', ...rest }: SkeletonProps) => { - return
; -}; - -export default Skeleton; - -const skeletonAnimation = keyframes` - 0% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } - 100% { - background-position: 0% 50%; - } -`; - -export const genSkeletonStyling = (width: string, height: string, variant: 'square' | 'circle') => { - return css({ - width, - height: variant === 'square' ? height : width, - borderRadius: variant === 'square' ? '4px' : '50%', - - background: `linear-gradient(-90deg,${theme.color.l2}, ${theme.color.l1}, ${theme.color.l2}, ${theme.color.l1})`, - backgroundSize: ' 400%', - - animation: `${skeletonAnimation} 5s infinite ease-out`, - }); -}; diff --git a/src/components/Spinner/Spinner.tsx b/src/components/Spinner/Spinner.tsx deleted file mode 100644 index 99554ed8..00000000 --- a/src/components/Spinner/Spinner.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import styled from '@emotion/styled'; - -type Props = { - backdrop?: boolean; -}; - -const Spinner = ({ backdrop = false }: Props) => ( - <> - {backdrop && } - - - - -); - -export default Spinner; - -const Position = styled.div` - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); -`; - -const SpinnerContainer = styled.div` - width: 24px; - height: 24px; - border: 3px solid rgba(195, 195, 195, 0.6); - border-radius: 50%; - border-top-color: #636767; - animation: spin 900ms linear infinite; - - @keyframes spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } - } -`; - -const Backdrop = styled.div` - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(0, 0, 0, 0.1); - display: flex; - align-items: center; - justify-content: center; - z-index: 9; -`; diff --git a/src/constants/index.ts b/src/constants/index.ts index bde9ae99..a3f78617 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1,4 +1,6 @@ export const BASE_URL = 'https://conceptbe.kr/api'; -// 서버 작업으로 인한 임시 상수값 +// 댓글 작성 시 ID 값 export const ROOT_COMMENT_ID = '0'; + +export const NICKNAME_REG_EXP = /[^a-zA-Z0-9가-힣]|[ㄱ-ㅎㅏ-ㅣ]/; diff --git a/src/hooks/useAlert.tsx b/src/hooks/useAlert.tsx index d10cb7af..2aeaa960 100644 --- a/src/hooks/useAlert.tsx +++ b/src/hooks/useAlert.tsx @@ -1,8 +1,7 @@ import { useOverlay } from '@toss/use-overlay'; +import { Alert } from 'concept-be-design-system'; import { useCallback } from 'react'; -import Alert from '../components/Modal/Alert'; - interface OpenAlertProps { content: string; buttonContent?: string; diff --git a/src/hooks/useConfirm.tsx b/src/hooks/useConfirm.tsx index bff9e78b..04d5e72d 100644 --- a/src/hooks/useConfirm.tsx +++ b/src/hooks/useConfirm.tsx @@ -1,8 +1,7 @@ import { useOverlay } from '@toss/use-overlay'; +import { Confirm } from 'concept-be-design-system'; import { useCallback } from 'react'; -import Confirm from '../components/Modal/Confirm'; - interface OpenConfirmProps { content: string; confirmButtonContent?: string; diff --git a/src/pages/Feed/Feed.page.tsx b/src/pages/Feed/Feed.page.tsx index 381ae085..e4c0e566 100644 --- a/src/pages/Feed/Feed.page.tsx +++ b/src/pages/Feed/Feed.page.tsx @@ -7,7 +7,6 @@ import BestIdeaCardListSection from './components/BestIdeaCardListSection/BestId import FilterBottomSheet from './components/FilterBottomSheet/FilterBottomSheet'; import NewIdeaCardListSection from './components/NewIdeaCardListSection/NewIdeaCardListSection'; import { getUserNickname } from './utils/getUserNickname'; -import Padding from '../../components/Padding'; import SEOMeta from '../../components/SEOMeta/SEOMeta'; import Logo from '../../layouts/Logo'; import { useWritingInfoQuery } from '../Write/hooks/queries/useWritingInfoQuery'; @@ -15,7 +14,7 @@ import { useWritingInfoQuery } from '../Write/hooks/queries/useWritingInfoQuery' const Feed = () => { const navigate = useNavigate(); const [isFilterBottomSheetOpen, setIsFilterBottomSheetOpen] = useState(false); - const { branches, purposes, recruitmentPlaces, skillCategoryResponses } = useWritingInfoQuery(); + const { branches, purposes, recruitmentPlaces, cooperationWays, skillCategoryResponses } = useWritingInfoQuery(); const closeFilterBottomSheet = () => { setIsFilterBottomSheetOpen(false); @@ -65,7 +64,7 @@ const Feed = () => { - + {/* Pop Up 애니메이션이 사라짐에 따라 조건부 렌더링 로직을 제거해야할 것 같습니다. 초기화하는 로직을 직접 작성하도록 하겠습니다.*/} @@ -73,6 +72,7 @@ const Feed = () => { branches={branches} purposes={purposes} recruitmentPlaces={recruitmentPlaces} + cooperationWays={cooperationWays} skillCategoryResponses={skillCategoryResponses} open={isFilterBottomSheetOpen} onClose={closeFilterBottomSheet} diff --git a/src/pages/Feed/components/BestIdeaCard/BestIdeaCardSkeleton.tsx b/src/pages/Feed/components/BestIdeaCard/BestIdeaCardSkeleton.tsx index ee4199e2..50227157 100644 --- a/src/pages/Feed/components/BestIdeaCard/BestIdeaCardSkeleton.tsx +++ b/src/pages/Feed/components/BestIdeaCard/BestIdeaCardSkeleton.tsx @@ -1,7 +1,7 @@ import styled from '@emotion/styled'; +import { Skeleton } from 'concept-be-design-system'; import { BestIdeaCardWrapper } from './BestIdeaCard'; -import Skeleton from '../../../../components/Skeleton/Skeleton'; const BestIdeaCardSkeleton = () => { return ( diff --git a/src/pages/Feed/components/FilterBottomSheet/FilterBottomSheet.tsx b/src/pages/Feed/components/FilterBottomSheet/FilterBottomSheet.tsx index 5ae4261e..607c6a5c 100644 --- a/src/pages/Feed/components/FilterBottomSheet/FilterBottomSheet.tsx +++ b/src/pages/Feed/components/FilterBottomSheet/FilterBottomSheet.tsx @@ -16,14 +16,9 @@ import { } from 'concept-be-design-system'; import RecruitmentPlaceSection from '../../../Write/components/RecruitmentPlaceSection'; -import { Idea } from '../../../Write/types'; +import { CooperationWay, Idea } from '../../../Write/types'; import { useFilterParams } from '../../context/filterContext'; - -const cooperationWays = [ - { id: 1, name: '상관없음' }, - { id: 2, name: '온라인' }, - { id: 3, name: '오프라인' }, -]; +import useFilteredBottomSheetState from '../../hooks/useFilteredBottomSheetState'; type Props = { open: boolean; @@ -31,6 +26,7 @@ type Props = { onApply: () => void; branches: Idea['branches']; purposes: Idea['purposes']; + cooperationWays: CooperationWay[]; recruitmentPlaces: Idea['regions']; skillCategoryResponses: Idea['skillCategoryResponses']; }; @@ -42,69 +38,37 @@ const FilterBottomSheet = ({ branches, purposes, recruitmentPlaces, + cooperationWays, skillCategoryResponses, }: Props) => { const { filterParams, updateFilterParams, resetFilterParams } = useFilterParams(); - - const branchOptions = branches.map((properties) => ({ - checked: filterParams?.branchIds?.includes(properties.id) ? true : false, - ...properties, - })); - const purposeOptions = purposes.map((properties) => ({ - checked: filterParams?.purposeIds?.includes(properties.id) ? true : false, - ...properties, - })); - const { checkboxValue, onChangeCheckbox, onResetCheckbox } = useCheckbox({ - branches: branchOptions, - purposes: purposeOptions, + const { + filteredBranches, + filteredPurposes, + filteredCooperationWays, + filteredRecruitmentPlace, + filteredSkillCategory1Depth, + filteredSkillCategory2Depth, + } = useFilteredBottomSheetState({ + filterParams, + branches, + purposes, + recruitmentPlaces, + cooperationWays, + skillCategoryResponses, }); - const cooperationWayOptions = - filterParams?.cooperationWay === undefined - ? cooperationWays.map((properties) => { - // 필터 선택 안 되어 있을 경우 상관없음이 기본값(id === 1) - return properties.id === 1 ? { checked: true, ...properties } : { checked: false, ...properties }; - }) - : cooperationWays.map((properties) => ({ - checked: filterParams?.cooperationWay === properties.name ? true : false, - ...properties, - })); - const { radioValue, onChangeRadio, onResetRadio } = useRadio({ - cooperationWays: cooperationWayOptions, + const { checkboxValue, selectedCheckboxId, onChangeCheckbox, onResetCheckbox } = useCheckbox({ + branches: filteredBranches, + purposes: filteredPurposes, + }); + const { radioValue, selectedRadioName, onChangeRadio, onResetRadio } = useRadio({ + cooperationWays: filteredCooperationWays, }); - - const getSkillCategory1DepthFrom2DepthSkillId = (id: number) => { - const skillCategory1Depth = skillCategoryResponses.find((item) => - item.skillResponses.find((skill) => skill.id === id), - ); - - if (skillCategory1Depth === undefined) { - throw new Error('skillCategory1Depth skill category not found'); - } - return skillCategory1Depth; - }; - - const get2DepthNameFrom2DepthId = (id: number) => { - const name = skillCategoryResponses - .find((item) => item.skillResponses.find((skill) => skill.id === id)) - ?.skillResponses.find((skill) => skill.id === id)?.name; - - if (name === undefined) { - throw new Error('2depth skill category not found'); - } - - return name; - }; const { dropdownValue, onClickDropdown, onResetDropdown } = useDropdown({ - recruitmentPlace: recruitmentPlaces.find((place) => place.id === filterParams?.recruitmentPlaceId)?.name ?? '', - skillCategory1Depth: - filterParams?.skillCategoryIds?.[0] !== undefined - ? getSkillCategory1DepthFrom2DepthSkillId(filterParams?.skillCategoryIds?.[0]).name - : undefined ?? '', - skillCategory2Depth: - filterParams?.skillCategoryIds?.[0] !== undefined - ? get2DepthNameFrom2DepthId(filterParams?.skillCategoryIds?.[0]) - : undefined ?? '', + recruitmentPlace: filteredRecruitmentPlace, + skillCategory1Depth: filteredSkillCategory1Depth, + skillCategory2Depth: filteredSkillCategory2Depth, }); const skillCategory1DepthItems = skillCategoryResponses.map((item) => ({ id: item.id, name: item.name })); @@ -119,14 +83,17 @@ const FilterBottomSheet = ({ return id; }; - const branchIds = checkboxValue.branches.filter((branch) => branch.checked).map((branch) => branch.id); - const purposeIds = checkboxValue.purposes.filter((branch) => branch.checked).map((purpose) => purpose.id); - const cooperationWay = radioValue.cooperationWays.find((cooperationWay) => cooperationWay.checked)?.name; - const recruitmentPlaceId = recruitmentPlaces.find((place) => place.name === dropdownValue.recruitmentPlace)?.id; const skillCategoryId = get2DepthIdFrom2DepthName(dropdownValue.skillCategory2Depth); const skillCategoryIds = skillCategoryId ? [skillCategoryId] : undefined; - updateFilterParams({ branchIds, purposeIds, cooperationWay, recruitmentPlaceId, skillCategoryIds }); + updateFilterParams({ + branchIds: selectedCheckboxId.branches, + purposeIds: selectedCheckboxId.purposes, + cooperationWay: selectedRadioName.cooperationWays, + recruitmentPlaceId: recruitmentPlaces.find((place) => place.name === dropdownValue.recruitmentPlace)?.id, + skillCategoryIds, + }); + onApply(); }; @@ -146,8 +113,17 @@ const FilterBottomSheet = ({ return ( - onClose()}> - + + @@ -240,7 +216,7 @@ const FilterContent = styled.div` display: flex; flex-direction: column; gap: 25px; - padding: 0 22px 60px 22px; + padding: 46px 22px 60px 22px; `; const FilterBottom = styled.div` diff --git a/src/pages/Feed/components/NewIdeaCardListSection/NewIdeaCardListSection.tsx b/src/pages/Feed/components/NewIdeaCardListSection/NewIdeaCardListSection.tsx index 75c99692..739e50ea 100644 --- a/src/pages/Feed/components/NewIdeaCardListSection/NewIdeaCardListSection.tsx +++ b/src/pages/Feed/components/NewIdeaCardListSection/NewIdeaCardListSection.tsx @@ -1,5 +1,5 @@ import styled from '@emotion/styled'; -import { Spacer, Text } from 'concept-be-design-system'; +import { SVGProfileMessageDots, Spacer, Text } from 'concept-be-design-system'; import { Fragment, Suspense, useRef } from 'react'; import NewIdeaCardListSkeleton from './NewIdeaCardListSkeleton'; @@ -7,6 +7,7 @@ import useConfirm from '../../../../hooks/useConfirm'; import { useDeleteIdea } from '../../../components/NewIdeaCard/hooks/mutations/useDeleteIdea'; import NewIdeaCard from '../../../components/NewIdeaCard/NewIdeaCard'; import useNavigatePage from '../../../hooks/useNavigatePage'; +import EmptyTabContentSection from '../../../Profile/components/EmptyTabContentSection'; import { useFilterParams } from '../../context/filterContext'; import { useIdeasQuery } from '../../hooks/queries/useIdeasQuery'; import { useFeedInfiniteFetch } from '../../hooks/useFeedInfiniteFetch'; @@ -30,6 +31,12 @@ const CardList = () => { } }; + if (ideas.length === 0) { + return ( + + ); + } + return ( <> {ideas.map((idea, idx) => { diff --git a/src/pages/Feed/hooks/useFilteredBottomSheetState.ts b/src/pages/Feed/hooks/useFilteredBottomSheetState.ts new file mode 100644 index 00000000..9e14fe9b --- /dev/null +++ b/src/pages/Feed/hooks/useFilteredBottomSheetState.ts @@ -0,0 +1,82 @@ +import { CooperationWay, Idea } from '../../Write/types'; +import { FilterParams } from '../context/filterContext'; + +interface Props { + filterParams: FilterParams | undefined; + branches: Idea['branches']; + purposes: Idea['purposes']; + recruitmentPlaces: Idea['regions']; + cooperationWays: CooperationWay[]; + skillCategoryResponses: Idea['skillCategoryResponses']; +} + +const useFilteredBottomSheetState = ({ + filterParams, + branches, + purposes, + recruitmentPlaces, + cooperationWays, + skillCategoryResponses, +}: Props) => { + const filteredBranches = branches.map((properties) => ({ + ...properties, + checked: filterParams?.branchIds?.includes(properties.id) ? true : false, + })); + const filteredPurposes = purposes.map((properties) => ({ + ...properties, + checked: filterParams?.purposeIds?.includes(properties.id) ? true : false, + })); + + const filteredCooperationWays = + filterParams?.cooperationWay === undefined + ? cooperationWays + : cooperationWays.map((properties) => ({ + ...properties, + checked: filterParams?.cooperationWay === properties.name ? true : false, + })); + + const getSkillCategory1DepthFrom2DepthSkillId = (id: number) => { + const skillCategory1Depth = skillCategoryResponses.find((item) => + item.skillResponses.find((skill) => skill.id === id), + ); + + if (skillCategory1Depth === undefined) { + throw new Error('skillCategory1Depth skill category not found'); + } + return skillCategory1Depth; + }; + + const get2DepthNameFrom2DepthId = (id: number) => { + const skillCategory2Depth = skillCategoryResponses + .find((item) => item.skillResponses.find((skill) => skill.id === id)) + ?.skillResponses.find((skill) => skill.id === id)?.name; + + if (skillCategory2Depth === undefined) { + throw new Error('2depth skill category not found'); + } + + return skillCategory2Depth; + }; + + const filteredRecruitmentPlace = + recruitmentPlaces.find((place) => place.id === filterParams?.recruitmentPlaceId)?.name ?? ''; + const filteredSkillCategory1Depth = + filterParams?.skillCategoryIds?.[0] !== undefined + ? getSkillCategory1DepthFrom2DepthSkillId(filterParams?.skillCategoryIds?.[0]).name + : undefined ?? ''; + const filteredSkillCategory2Depth = + filterParams?.skillCategoryIds?.[0] !== undefined + ? get2DepthNameFrom2DepthId(filterParams?.skillCategoryIds?.[0]) + : undefined ?? ''; + + return { + filteredBranches, + filteredPurposes, + filteredCooperationWays, + filteredRecruitmentPlace, + filteredSkillCategory1Depth, + filteredSkillCategory2Depth, + }; +}; + +export default useFilteredBottomSheetState; diff --git a/src/pages/Feed/utils/formatCommentDate.ts b/src/pages/Feed/utils/formatCommentDate.ts index 27bf6b3c..53147324 100644 --- a/src/pages/Feed/utils/formatCommentDate.ts +++ b/src/pages/Feed/utils/formatCommentDate.ts @@ -1,7 +1,7 @@ export function formatCommentDate(date: string) { const now = new Date(); const commentDate = new Date(date); - const diffTime = Math.abs(now - commentDate); + const diffTime = Math.abs(now.getTime() - commentDate.getTime()); const diffHours = Math.floor(diffTime / (1000 * 60 * 60)); if (diffHours < 24) { @@ -14,7 +14,7 @@ export function formatCommentDate(date: string) { const minutes = commentDate.getMinutes(); // 숫자가 한 자리일 때 앞에 0을 붙여주는 함수 - const padZero = (num) => num.toString().padStart(2, '0'); + const padZero = (num: number) => num.toString().padStart(2, '0'); return `${year}.${padZero(month)}.${padZero(day)} ${padZero(hours)}:${padZero(minutes)}`; } diff --git a/src/pages/FeedDetail/FeedDetail.page.tsx b/src/pages/FeedDetail/FeedDetail.page.tsx index 11674780..bf4a3f4e 100644 --- a/src/pages/FeedDetail/FeedDetail.page.tsx +++ b/src/pages/FeedDetail/FeedDetail.page.tsx @@ -3,10 +3,10 @@ import { useNavigate, useParams } from 'react-router-dom'; import Comments from './components/Comments'; import ModifyDropdown from './components/ModifyDropdown'; +import ProfileInfo from './components/ProfileInfo'; import ReactionBar from './components/ReactionBar'; import { CommentFocusProvider } from './contexts/CommentFocusContext'; import useFeedDetailQuery from './hooks/queries/useFeedDetailQuery'; -import ProfileInfo from '../../components/ProfileInfo'; import SEOMeta from '../../components/SEOMeta/SEOMeta'; import useConfirm from '../../hooks/useConfirm'; import Back from '../../layouts/Back'; diff --git a/src/pages/FeedDetail/components/EditComment.tsx b/src/pages/FeedDetail/components/EditComment.tsx index 139e5700..1e1196a8 100644 --- a/src/pages/FeedDetail/components/EditComment.tsx +++ b/src/pages/FeedDetail/components/EditComment.tsx @@ -30,10 +30,6 @@ const EditComment = ({ const { editComment } = usePatchComment({ feedId, commentId, - onSuccess: () => { - initEditCommentTextarea(); - onCloseEditCommentTextarea(); - }, }); const onChangeTextarea = (e: ChangeEvent) => { @@ -47,7 +43,17 @@ const EditComment = ({ }; const onSubmitComment = () => { - editComment({ content: commentInput }); + if (!commentInput) return; + + editComment( + { content: commentInput }, + { + onSuccess: () => { + initEditCommentTextarea(); + onCloseEditCommentTextarea(); + }, + }, + ); }; return ( diff --git a/src/components/ProfileInfo.tsx b/src/pages/FeedDetail/components/ProfileInfo.tsx similarity index 94% rename from src/components/ProfileInfo.tsx rename to src/pages/FeedDetail/components/ProfileInfo.tsx index 2df9c50b..14ab660b 100644 --- a/src/components/ProfileInfo.tsx +++ b/src/pages/FeedDetail/components/ProfileInfo.tsx @@ -1,6 +1,6 @@ import { Text, Box, Flex, ImageView, PNGDefaultProfileInfo36 } from 'concept-be-design-system'; -import useNavigatePage from '../pages/hooks/useNavigatePage'; +import useNavigatePage from '../../hooks/useNavigatePage'; interface Props { memberId: number; diff --git a/src/pages/FeedDetail/components/WriteComment.tsx b/src/pages/FeedDetail/components/WriteComment.tsx index 7c564855..320c885d 100644 --- a/src/pages/FeedDetail/components/WriteComment.tsx +++ b/src/pages/FeedDetail/components/WriteComment.tsx @@ -34,6 +34,8 @@ const WriteComment = ({ feedId, myImageUrl, myNickname }: Props) => { }; const onSubmitComment = () => { + if (!commentInput) return; + postComment( { ideaId: feedId, parentId: ROOT_COMMENT_ID, content: commentInput }, { diff --git a/src/pages/FeedDetail/components/WriteRecomment.tsx b/src/pages/FeedDetail/components/WriteRecomment.tsx index 8b15cd89..d58e5a01 100644 --- a/src/pages/FeedDetail/components/WriteRecomment.tsx +++ b/src/pages/FeedDetail/components/WriteRecomment.tsx @@ -30,6 +30,8 @@ const WriteRecomment = ({ feedId, parentCommentId, myImageUrl, myNickname, onClo }; const onSubmitComment = () => { + if (!recommentInput) return; + postComment( { ideaId: feedId, parentId: parentCommentId, content: recommentInput }, { diff --git a/src/pages/FeedDetail/hooks/mutations/usePatchComment.ts b/src/pages/FeedDetail/hooks/mutations/usePatchComment.ts index fe794412..0168de0f 100644 --- a/src/pages/FeedDetail/hooks/mutations/usePatchComment.ts +++ b/src/pages/FeedDetail/hooks/mutations/usePatchComment.ts @@ -11,19 +11,17 @@ interface CommentPayload { interface Props { feedId: string; commentId: string; - onSuccess?: () => void; } const _editComment = (commentId: string, payload: CommentPayload) => http.patch(`/comments/${commentId}`, payload); -const usePatchComment = ({ feedId, commentId, onSuccess }: Props) => { +const usePatchComment = ({ feedId, commentId }: Props) => { const openAlert = useAlert(); const queryClient = useQueryClient(); const { mutate: editComment, ...rest } = useMutation({ mutationFn: (payload: CommentPayload) => _editComment(commentId, payload), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['comments', feedId] }); - if (onSuccess) onSuccess(); }, onError: (error: AxiosError<{ message: string }>) => { openAlert({ content: error.response?.data.message ?? '댓글 수정에 실패했습니다.' }); diff --git a/src/pages/Login/OauthRedirect.tsx b/src/pages/Login/OauthRedirect.tsx index 436f2853..18c0c39d 100644 --- a/src/pages/Login/OauthRedirect.tsx +++ b/src/pages/Login/OauthRedirect.tsx @@ -1,8 +1,8 @@ +import { Spinner } from 'concept-be-design-system'; import { useCallback, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { getIsMember, getLogin } from '../../api'; -import Spinner from '../../components/Spinner/Spinner'; interface Props { serverName: 'kakao' | 'naver'; diff --git a/src/pages/NeedAuth.tsx b/src/pages/NeedAuth.tsx new file mode 100644 index 00000000..904f366c --- /dev/null +++ b/src/pages/NeedAuth.tsx @@ -0,0 +1,24 @@ +import { ReactNode, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; + +interface Props { + children: ReactNode; +} + +const getUserTokenInLocalStorage = () => localStorage.getItem('userToken'); + +const NeedAuth = ({ children }: Props) => { + const navigate = useNavigate(); + + useEffect(() => { + const isAuthored = !!getUserTokenInLocalStorage(); + + if (!isAuthored) { + navigate('/login'); + } + }); + + return <>{children}; +}; + +export default NeedAuth; diff --git a/src/pages/Profile/More.page.tsx b/src/pages/Profile/More.page.tsx index e5eb4edc..07388521 100644 --- a/src/pages/Profile/More.page.tsx +++ b/src/pages/Profile/More.page.tsx @@ -1,10 +1,9 @@ import styled from '@emotion/styled'; -import { BottomSheet, Divider, Header, Spacer, Text, theme } from 'concept-be-design-system'; +import { BottomSheet, Divider, Header, Text, theme } from 'concept-be-design-system'; import { useState } from 'react'; import useDeleteAccount from './hooks/mutations/useDeleteAccount'; import SEOMeta from '../../components/SEOMeta/SEOMeta'; -import Spinner from '../../components/Spinner/Spinner'; import Privacy from '../../components/Terms/Privacy'; import UsageTerms from '../../components/Terms/UsageTerms'; import useConfirm from '../../hooks/useConfirm'; @@ -16,7 +15,7 @@ const More = () => { const [moreState, setMoreState] = useState(''); const isLoggedIn = Boolean(localStorage.getItem('user')) && Boolean(localStorage.getItem('userToken')); const { goFeedPage, goLoginPage } = useNavigatePage(); - const { deleteAccount, isPending: isDeleteAccountPending } = useDeleteAccount(); + const { deleteAccount } = useDeleteAccount(); const openConfirm = useConfirm(); const onMoreClick = (string: string) => { @@ -46,8 +45,8 @@ const More = () => { return ( <> - {isDeleteAccountPending && } - + +
@@ -87,13 +86,15 @@ const More = () => { - + 기타 문의 사항 - - - 기타 문의사항이 있으실 경우, ABCDEFG123456@gmail.com으로 연락주세요 - diff --git a/src/pages/Profile/components/MyProfile/MyProfile.page.tsx b/src/pages/Profile/components/MyProfile/MyProfile.page.tsx index 491a43fd..b8c2de8d 100644 --- a/src/pages/Profile/components/MyProfile/MyProfile.page.tsx +++ b/src/pages/Profile/components/MyProfile/MyProfile.page.tsx @@ -1,11 +1,10 @@ import styled from '@emotion/styled'; -import { Header, TabLayout, theme, SVGHeaderSetting, Box } from 'concept-be-design-system'; +import { Header, TabLayout, theme, SVGHeaderSetting, Box, Spinner } from 'concept-be-design-system'; import { Suspense } from 'react'; import { useNavigate } from 'react-router-dom'; import BookmarkSection from './BookmarkSection'; import IdeaSection from './IdeaSection'; -import Spinner from '../../../../components/Spinner/Spinner'; import Logo from '../../../../layouts/Logo'; import { Member } from '../../types'; import ProfileInfoSection from '../ProfileInfoSection'; diff --git a/src/pages/Profile/components/OtherProfile/OtherProfile.page.tsx b/src/pages/Profile/components/OtherProfile/OtherProfile.page.tsx index 5edc8af5..71ef0624 100644 --- a/src/pages/Profile/components/OtherProfile/OtherProfile.page.tsx +++ b/src/pages/Profile/components/OtherProfile/OtherProfile.page.tsx @@ -1,9 +1,8 @@ import styled from '@emotion/styled'; -import { Text, theme, Header } from 'concept-be-design-system'; +import { Text, theme, Header, Spinner } from 'concept-be-design-system'; import { Suspense } from 'react'; import IdeaSection from './IdeaSection'; -import Spinner from '../../../../components/Spinner/Spinner'; import Back from '../../../../layouts/Back'; import { Member } from '../../types'; import ProfileInfoSection from '../ProfileInfoSection'; diff --git a/src/pages/Profile/components/ProfileInfoSection.tsx b/src/pages/Profile/components/ProfileInfoSection.tsx index 152f2d0e..49225169 100644 --- a/src/pages/Profile/components/ProfileInfoSection.tsx +++ b/src/pages/Profile/components/ProfileInfoSection.tsx @@ -2,7 +2,6 @@ import styled from '@emotion/styled'; import { theme, Badge, Spacer, Text, Flex, ImageView, PNGDefaultProfileBackground } from 'concept-be-design-system'; import { useNavigate } from 'react-router-dom'; -import Padding from '../../../components/Padding'; import HyperLinkText from '../../components/HyperLinkText/HyperLinkText'; import { Member } from '../types'; @@ -43,7 +42,7 @@ const ProfileInfoSection = ({ memberInfo }: Props) => { - + {/* 프로필설정 */} @@ -68,9 +67,9 @@ const ProfileInfoSection = ({ memberInfo }: Props) => { - {skills.map(({ skillId, skillName }) => ( + {skills.map(({ skillId, skillName, level }) => ( - {skillName} + {`${skillName}, ${level}`} ))} diff --git a/src/pages/Profile/hooks/mutations/useDeleteAccount.ts b/src/pages/Profile/hooks/mutations/useDeleteAccount.ts index 4ec4c5af..775af616 100644 --- a/src/pages/Profile/hooks/mutations/useDeleteAccount.ts +++ b/src/pages/Profile/hooks/mutations/useDeleteAccount.ts @@ -22,12 +22,16 @@ const useDeleteAccount = () => { onSuccess: async () => { localStorage.removeItem('user'); localStorage.removeItem('userToken'); - await openConfirm({ content: '탈퇴하였습니다.', closeButtonContent: '' }); + await openConfirm({ + content: '회원 탈퇴를 완료했습니다. 그간 서비스를 이용해 주셔서 감사합니다.', + closeButtonContent: '', + }); goFeedPage(); }, onError: async (error: DeleteAccountError) => { await openConfirm({ - content: error.response?.data.message ?? '탈퇴에 실패했습니다. 메일로 문의 부탁드립니다.', + content: + error.response?.data.message ?? '회원 탈퇴를 실패했습니다. 기타 문의 사항을 클릭해 메일로 문의해 주세요.', closeButtonContent: '', }); }, diff --git a/src/pages/ProfileEdit/ProfileEdit.page.tsx b/src/pages/ProfileEdit/ProfileEdit.page.tsx index 3d4bb740..f9f451f8 100644 --- a/src/pages/ProfileEdit/ProfileEdit.page.tsx +++ b/src/pages/ProfileEdit/ProfileEdit.page.tsx @@ -23,6 +23,8 @@ import { FormEvent } from 'react'; import useProfileEditQuery from './hooks/useProfileEditQuery.ts'; import usePutProfileMutation from './hooks/usePutProfileMutation.ts'; import { DropdownValue, FieldValue } from './types'; +import { NICKNAME_REG_EXP } from '../../constants/index.ts'; +import useAlert from '../../hooks/useAlert.tsx'; import Back from '../../layouts/Back.tsx'; import { getUserId } from '../Profile/utils/getUserId.ts'; import useCheckDuplicateNickname from '../SignUp/hooks/useCheckDuplicateNickname.ts'; @@ -39,13 +41,14 @@ interface CheckboxOption { } const ProfileEdit = () => { + const openAlert = useAlert(); const { mainSkills, detailSkills, skillLevels, regions, purposes, my } = useProfileEditQuery(); const { fieldValue, fieldErrorValue, setFieldErrorValue, onChangeField } = useField({ nickname: my.nickname ?? '', company: my.workingPlace ?? '', intro: my.introduction ?? '', }); - const { checkboxValue, onChangeCheckbox } = useCheckbox({ + const { checkboxValue, selectedCheckboxId, onChangeCheckbox } = useCheckbox({ goal: my.joinPurposes ?? purposes, }); const { dropdownValue, onResetDropdown, onClickDropdown } = useDropdown({ @@ -69,7 +72,7 @@ const ProfileEdit = () => { const validateInput = () => { return [ { - validateFn: (input: string) => /[~!@#$%";'^,&*()_+|=>`?:{[\]}\s]/g.test(input), + validateFn: (input: string) => NICKNAME_REG_EXP.test(input), errorMessage: '사용 불가한 닉네임입니다.', }, { @@ -82,12 +85,32 @@ const ProfileEdit = () => { const onSubmit = (e: FormEvent) => { e.preventDefault(); + if (!fieldValue.nickname) { + openAlert({ content: '닉네임을 입력해 주세요.' }); + return; + } + + if (!dropdownValue.mainSkill) { + openAlert({ content: '대표 스킬을 선택해 주세요.' }); + return; + } + + if (selectedSkillDepths.length === 0) { + openAlert({ content: '세부 스킬을 하나 이상 선택해 주세요.' }); + return; + } + + if (selectedCheckboxId.goal.length === 0) { + openAlert({ content: '가입 목적을 하나 이상 선택해 주세요.' }); + return; + } + putProfile({ nickname: fieldValue.nickname, mainSkillId: mainSkills.find(({ name }) => dropdownValue.mainSkill === name)?.id || 0, profileImageUrl: my.profileImageUrl || '', skills: selectedSkillDepths.map(({ id, name }) => ({ skillId: id, level: name.split(', ')[1] })), - joinPurposes: checkboxValue.goal.filter(({ checked }) => checked).map(({ id }) => id), + joinPurposes: selectedCheckboxId.goal, livingPlaceId: regions.find((place) => place.name === dropdownValue.region)?.id || 1, workingPlace: fieldValue.company, introduction: fieldValue.intro, @@ -298,7 +321,7 @@ const ProfileEdit = () => { onValidate={validateInput} maxLength={10} > - + diff --git a/src/pages/SignUp/SignUp.page.tsx b/src/pages/SignUp/SignUp.page.tsx index c84efc7b..9da8273f 100644 --- a/src/pages/SignUp/SignUp.page.tsx +++ b/src/pages/SignUp/SignUp.page.tsx @@ -28,6 +28,8 @@ import useSignUpQuery from './hooks/useSignUpQuery.ts'; import useValidateUserInfo from './hooks/useValidateUserInfo.ts'; import { DropdownValue, FieldValue } from './types'; import SEOMeta from '../../components/SEOMeta/SEOMeta.tsx'; +import { NICKNAME_REG_EXP } from '../../constants/index.ts'; +import useAlert from '../../hooks/useAlert.tsx'; import { OauthMemberInfo } from '../../types/login.ts'; interface CheckboxValue { @@ -41,6 +43,7 @@ interface CheckboxOption { } const SignUpPage = () => { + const openAlert = useAlert(); const { state: memberInfo }: { state: OauthMemberInfo | null } = useLocation(); const { postSignUp } = useSignUpMutation(); const { mainSkills, detailSkills, skillLevels, regions, purposes } = useSignUpQuery(); @@ -49,7 +52,7 @@ const SignUpPage = () => { company: '', intro: '', }); - const { checkboxValue, onChangeCheckbox } = useCheckbox({ + const { checkboxValue, selectedCheckboxId, onChangeCheckbox } = useCheckbox({ goal: purposes, }); const { dropdownValue, onResetDropdown, onClickDropdown } = useDropdown({ @@ -72,7 +75,7 @@ const SignUpPage = () => { const validateInput = () => { return [ { - validateFn: (input: string) => /[~!@#$%";'^,&*()_+|=>`?:{[\]}\s]/g.test(input), + validateFn: (input: string) => NICKNAME_REG_EXP.test(input), errorMessage: '사용 불가한 닉네임입니다.', }, { @@ -85,12 +88,32 @@ const SignUpPage = () => { const onSubmit = (e: FormEvent) => { e.preventDefault(); + if (!fieldValue.nickname) { + openAlert({ content: '닉네임을 입력해 주세요.' }); + return; + } + + if (!dropdownValue.mainSkill) { + openAlert({ content: '대표 스킬을 선택해 주세요.' }); + return; + } + + if (selectedSkillDepths.length === 0) { + openAlert({ content: '세부 스킬을 하나 이상 선택해 주세요.' }); + return; + } + + if (selectedCheckboxId.goal.length === 0) { + openAlert({ content: '가입 목적을 하나 이상 선택해 주세요.' }); + return; + } + postSignUp({ nickname: fieldValue.nickname, mainSkillId: mainSkills.find(({ name }) => dropdownValue.mainSkill === name)?.id || 0, profileImageUrl: memberInfo?.profileImageUrl || '', skills: selectedSkillDepths.map(({ id, name }) => ({ skillId: id, level: name.split(', ')[1] })), - joinPurposes: checkboxValue.goal.filter(({ checked }) => checked).map(({ id }) => id), + joinPurposes: selectedCheckboxId.goal, livingPlaceId: regions.find((place) => place.name === dropdownValue.region)?.id || 1, workingPlace: fieldValue.company, introduction: fieldValue.intro, @@ -314,7 +337,7 @@ const SignUpPage = () => { - + diff --git a/src/pages/SignUp/hooks/useCheckDuplicateNickname.ts b/src/pages/SignUp/hooks/useCheckDuplicateNickname.ts index 98f72ba0..7017ee9e 100644 --- a/src/pages/SignUp/hooks/useCheckDuplicateNickname.ts +++ b/src/pages/SignUp/hooks/useCheckDuplicateNickname.ts @@ -1,6 +1,7 @@ import { Dispatch, SetStateAction, useEffect, useRef } from 'react'; import { getCheckDuplicateNickname } from '../../../api'; +import { NICKNAME_REG_EXP } from '../../../constants'; import { getUserNickname } from '../../Feed/utils/getUserNickname'; import { FieldValue } from '../types'; @@ -14,7 +15,9 @@ const useCheckDuplicateNickname = ({ nickname, setFieldErrorValue }: Props) => { const timerId = useRef(null); useEffect(() => { - if (userNickname === nickname || nickname.length < 2) return; + if (userNickname === nickname || nickname.length < 2 || NICKNAME_REG_EXP.test(nickname)) { + return; + } if (timerId.current) { const timerIdCurrent = timerId.current; diff --git a/src/pages/Write/Write.page.tsx b/src/pages/Write/Write.page.tsx index a40f76d7..35a2f40b 100644 --- a/src/pages/Write/Write.page.tsx +++ b/src/pages/Write/Write.page.tsx @@ -30,54 +30,36 @@ import { get2DepthCountsBy1Depth } from './utils/get2DepthCountsBy1Depth'; import SEOMeta from '../../components/SEOMeta/SEOMeta'; import useAlert from '../../hooks/useAlert'; -const cooperationWays = [ - { id: 1, name: '상관없음' }, - { id: 2, name: '온라인' }, - { id: 3, name: '오프라인' }, -]; - const WritePage = () => { const openAlert = useAlert(); const { postIdeas } = usePostIdeasMutation(); - const { branches, purposes, recruitmentPlaces, skillCategoryResponses } = useWritingInfoQuery(); + const { branches, purposes, recruitmentPlaces, cooperationWays, skillCategoryResponses } = useWritingInfoQuery(); const [title, setTitle] = useState(''); const [introduce, setIntroduce] = useState(''); + const [isOpenBottomSheet, setIsOpenBottomSheet] = useState(false); + const [selectedTeamRecruitment1Depth, setSelectedTeamRecruitment1Depth] = useState(skillCategoryResponses[0].name); + const [selectedSkillResponses, setSelectedSkillResponses] = useState([]); - const branchOptions = branches.map((properties) => ({ checked: false, ...properties })); - const purposeOptions = purposes.map((properties) => ({ checked: false, ...properties })); - const { checkboxValue, onChangeCheckbox } = useCheckbox({ - branches: branchOptions, - purposes: purposeOptions, - }); - - const cooperationWayOptions = cooperationWays.map((properties) => { - // 협업방식: 상관없음이 기본값(id === 1) - return properties.id === 1 ? { checked: true, ...properties } : { checked: false, ...properties }; + const { checkboxValue, selectedCheckboxId, onChangeCheckbox } = useCheckbox({ + branches, + purposes, }); - - const { radioValue, onChangeRadio } = useRadio({ - cooperationWays: cooperationWayOptions, + const { radioValue, selectedRadioName, onChangeRadio } = useRadio({ + cooperationWays, }); - - // 모집 지역도 백엔드와 형식 논의해야할듯(id 추가..?) const { dropdownValue, onClickDropdown } = useDropdown({ recruitmentPlace: '', }); - const [isOpenBottomSheet, setIsOpenBottomSheet] = useState(false); - - const [selectedTeamRecruitment1Depth, setSelectedTeamRecruitment1Depth] = useState(skillCategoryResponses[0].name); - const [selectedSkillResponses, setSelectedSkillResponses] = useState([]); - const sheetLeftItems = skillCategoryResponses.map((item) => item.name); const sheetRightItems = skillCategoryResponses.find((item) => item.name === selectedTeamRecruitment1Depth) ?.skillResponses; - const branchIds = checkboxValue.branches.filter((branch) => branch.checked).map((branch) => branch.id); - const purposeIds = checkboxValue.purposes.filter((branch) => branch.checked).map((purpose) => purpose.id); - const cooperationWay = radioValue.cooperationWays.find((cooperationWay) => cooperationWay.checked)?.name; - const canSubmit = branchIds.length > 0 && purposeIds.length > 0 && !!cooperationWay; + const canSubmit = + selectedCheckboxId.branches.length > 0 && + selectedCheckboxId.purposes.length > 0 && + !!selectedRadioName.cooperationWays; if (!sheetRightItems) { console.error('sheetRightItems is null'); @@ -85,44 +67,36 @@ const WritePage = () => { } const writeIdea = () => { - const recruitmentPlaceId = - recruitmentPlaces.find((place) => place.name === dropdownValue.recruitmentPlace)?.id || 1; - const skillCategoryIds = selectedSkillResponses.map((selectedSkillResponse) => selectedSkillResponse.id); - // TODO: 글쓰기 필수 조건 누락 시 토스트 띄워주기 (alert -> toast) if (!title) { - openAlert({ content: '제목을 입력해 주세요' }); + openAlert({ content: '제목을 입력해 주세요.' }); return; } if (introduce.length < 10) { - openAlert({ content: '본문 내용을 10자 이상 입력해 주세요' }); - return; - } - if (!branchIds.length) { - openAlert({ content: '분야를 1개 이상 선택해 주세요' }); + openAlert({ content: '본문 내용을 10자 이상 입력해 주세요.' }); return; } - if (!purposeIds.length) { - openAlert({ content: '목적을 1개 이상 선택해 주세요' }); + if (!selectedCheckboxId.branches.length) { + openAlert({ content: '분야를 1개 이상 선택해 주세요.' }); return; } - if (!cooperationWay) { - openAlert({ content: '협업방식을 선택해 주세요' }); + if (!selectedCheckboxId.purposes.length) { + openAlert({ content: '목적을 1개 이상 선택해 주세요.' }); return; } - if (!recruitmentPlaceId) { - openAlert({ content: '모집지역을 선택해주세요.' }); + if (!selectedRadioName.cooperationWays) { + openAlert({ content: '협업방식을 선택해 주세요.' }); return; } postIdeas({ title, introduce, - recruitmentPlaceId, - cooperationWay, - branchIds, - purposeIds, - skillCategoryIds, + recruitmentPlaceId: recruitmentPlaces.find((place) => place.name === dropdownValue.recruitmentPlace)?.id || 1, + cooperationWay: selectedRadioName.cooperationWays, + branchIds: selectedCheckboxId.branches, + purposeIds: selectedCheckboxId.purposes, + skillCategoryIds: selectedSkillResponses.map((selectedSkillResponse) => selectedSkillResponse.id), }); }; @@ -136,7 +110,7 @@ const WritePage = () => { const onClickTeamRecruitment = (selected: Info) => { if (selectedSkillResponses.length >= 10) { - openAlert({ content: '10개 이상 선택할 수 없습니다.' }); + openAlert({ content: '최대 10개까지 선택할 수 있습니다.' }); return; } setSelectedSkillResponses((prev) => @@ -153,9 +127,9 @@ const WritePage = () => { title !== '' || introduce !== '' || selectedSkillResponses.length > 0 || - branchIds.length > 0 || - purposeIds.length > 0 || - cooperationWay !== '상관없음' || + selectedCheckboxId.branches.length > 0 || + selectedCheckboxId.purposes.length > 0 || + selectedRadioName.cooperationWays !== '상관없음' || !!dropdownValue.recruitmentPlace; return ( diff --git a/src/pages/Write/components/TitleAndIntroduceSection.tsx b/src/pages/Write/components/TitleAndIntroduceSection.tsx index 9073957e..73af240a 100644 --- a/src/pages/Write/components/TitleAndIntroduceSection.tsx +++ b/src/pages/Write/components/TitleAndIntroduceSection.tsx @@ -29,11 +29,16 @@ const TitleAndIntroduceSection = ({ title, introduce, onTitleChange, onIntroduce return ( <> - +
diff --git a/src/pages/Write/hooks/queries/useWritingInfoQuery.ts b/src/pages/Write/hooks/queries/useWritingInfoQuery.ts index d80f2e43..a580b7b6 100644 --- a/src/pages/Write/hooks/queries/useWritingInfoQuery.ts +++ b/src/pages/Write/hooks/queries/useWritingInfoQuery.ts @@ -3,19 +3,43 @@ import { useSuspenseQuery } from '@tanstack/react-query'; import { http } from '../../../../api/http'; import { Idea } from '../../types'; +const cooperations = [ + { id: 1, name: '상관없음' }, + { id: 2, name: '온라인' }, + { id: 3, name: '오프라인' }, +]; + const getWritingInfo = async () => { return http.get('/ideas/writing'); }; export const useWritingInfoQuery = () => { - const { data: writingInfo, ...rest } = useSuspenseQuery({ queryKey: ['writingInfo'], queryFn: getWritingInfo }); + const { data: writingInfo, ...rest } = useSuspenseQuery({ + queryKey: ['writingInfo'], + queryFn: getWritingInfo, + select: (data) => { + const branches = data.branches.map((properties) => ({ checked: false, ...properties })); + const purposes = data.purposes.map((properties) => ({ checked: false, ...properties })); + const cooperationWays = cooperations.map((properties) => + properties.id === 1 ? { checked: true, ...properties } : { checked: false, ...properties }, + ); + + return { + ...data, + branches, + purposes, + cooperationWays, + }; + }, + }); - const { branches, purposes, regions: recruitmentPlaces, skillCategoryResponses } = writingInfo; + const { branches, purposes, regions: recruitmentPlaces, cooperationWays, skillCategoryResponses } = writingInfo; return { branches, purposes, recruitmentPlaces, + cooperationWays, skillCategoryResponses, ...rest, }; diff --git a/src/pages/Write/types/index.ts b/src/pages/Write/types/index.ts index e9c8269e..74f40e4c 100644 --- a/src/pages/Write/types/index.ts +++ b/src/pages/Write/types/index.ts @@ -26,3 +26,9 @@ export type Idea = { skillResponses: Info[]; // IT기획, 게임기획, 제품기획, 사업기획 }[]; }; + +export type CooperationWay = { + id: number; + name: string; + checked: boolean; +}; diff --git a/src/pages/WriteEdit/WriteEdit.page.tsx b/src/pages/WriteEdit/WriteEdit.page.tsx index 1f5fbe2a..9313c7e9 100644 --- a/src/pages/WriteEdit/WriteEdit.page.tsx +++ b/src/pages/WriteEdit/WriteEdit.page.tsx @@ -25,62 +25,22 @@ import Header from './components/Header'; import RecruitmentPlaceSection from './components/RecruitmentPlaceSection'; import TitleAndIntroduceSection from './components/TitleAndIntroduceSection'; import { usePutIdea } from './hooks/mutations/usePutIdea'; -import { useIdeaDetailQuery } from './hooks/queries/useIdeaDetailQuery'; -import { useWritingInfoQuery } from './hooks/queries/useWritingInfoQuery'; +import { useWritingEditInfoQuery } from './hooks/queries/useWritingInfoQuery'; import { Info } from './types'; import { get2DepthCountsBy1Depth } from './utils/get2DepthCountsBy1Depth'; import useAlert from '../../hooks/useAlert'; -const cooperationWays = [ - { id: 1, name: '상관없음' }, - { id: 2, name: '온라인' }, - { id: 3, name: '오프라인' }, -]; - const WriteEditPage = () => { const openAlert = useAlert(); const location = useLocation(); - const { ideaDetail } = useIdeaDetailQuery(Number(location.state.ideaId)); const { putIdea } = usePutIdea(); - const { branches, purposes, recruitmentPlaces, skillCategoryResponses } = useWritingInfoQuery(); + const { ideaDetail, branches, purposes, recruitmentPlaces, cooperationWays, skillCategoryResponses } = + useWritingEditInfoQuery(Number(location.state.ideaId)); const [title, setTitle] = useState(ideaDetail.title); const [introduce, setIntroduce] = useState(ideaDetail.introduce); - - const branchOptions = branches.map((properties) => - ideaDetail.branchList.includes(properties.name) - ? { checked: true, ...properties } - : { checked: false, ...properties }, - ); - const purposeOptions = purposes.map((properties) => - ideaDetail.purposeList.includes(properties.name) - ? { checked: true, ...properties } - : { checked: false, ...properties }, - ); - const { checkboxValue, onChangeCheckbox } = useCheckbox({ - branches: branchOptions, - purposes: purposeOptions, - }); - - const cooperationWayOptions = cooperationWays.map((properties) => { - // 협업방식: 상관없음이 기본값(id === 1) - return properties.name === ideaDetail.cooperationWay - ? { checked: true, ...properties } - : { checked: false, ...properties }; - }); - - const { radioValue, onChangeRadio } = useRadio({ - cooperationWays: cooperationWayOptions, - }); - - // 모집 지역도 백엔드와 형식 논의해야할듯(id 추가..?) - const { dropdownValue, onClickDropdown } = useDropdown({ - recruitmentPlace: ideaDetail.recruitmentPlace, - }); - const [isOpenBottomSheet, setIsOpenBottomSheet] = useState(false); - const [selectedTeamRecruitment1Depth, setSelectedTeamRecruitment1Depth] = useState(skillCategoryResponses[0].name); const [selectedSkillResponses, setSelectedSkillResponses] = useState( skillCategoryResponses @@ -89,14 +49,25 @@ const WriteEditPage = () => { .filter((item) => ideaDetail.skillCategories.includes(item.name)), ); + const { checkboxValue, selectedCheckboxId, onChangeCheckbox } = useCheckbox({ + branches, + purposes, + }); + const { radioValue, selectedRadioName, onChangeRadio } = useRadio({ + cooperationWays, + }); + const { dropdownValue, onClickDropdown } = useDropdown({ + recruitmentPlace: ideaDetail.recruitmentPlace, + }); + const sheetLeftItems = skillCategoryResponses.map((item) => item.name); const sheetRightItems = skillCategoryResponses.find((item) => item.name === selectedTeamRecruitment1Depth) ?.skillResponses; - const branchIds = checkboxValue.branches.filter((branch) => branch.checked).map((branch) => branch.id); - const purposeIds = checkboxValue.purposes.filter((branch) => branch.checked).map((purpose) => purpose.id); - const cooperationWay = radioValue.cooperationWays.find((cooperationWay) => cooperationWay.checked)?.name; - const canSubmit = branchIds.length > 0 && purposeIds.length > 0 && !!cooperationWay; + const canSubmit = + selectedCheckboxId.branches.length > 0 && + selectedCheckboxId.purposes.length > 0 && + !!selectedRadioName.cooperationWays; if (!sheetRightItems) { console.error('sheetRightItems is null'); @@ -104,32 +75,25 @@ const WriteEditPage = () => { } const writeIdea = () => { - const recruitmentPlaceId = recruitmentPlaces.find((place) => place.name === dropdownValue.recruitmentPlace)?.id; - const skillCategoryIds = selectedSkillResponses.map((selectedSkillResponse) => selectedSkillResponse.id); - // TODO: 글쓰기 필수 조건 누락 시 토스트 띄워주기 (alert -> toast) if (!title) { - openAlert({ content: '제목을 입력해 주세요' }); + openAlert({ content: '제목을 입력해 주세요.' }); return; } if (introduce.length < 10) { - openAlert({ content: '본문 내용을 10자 이상 입력해 주세요' }); - return; - } - if (!branchIds.length) { - openAlert({ content: '분야를 1개 이상 선택해 주세요' }); + openAlert({ content: '본문 내용을 10자 이상 입력해 주세요.' }); return; } - if (!purposeIds.length) { - openAlert({ content: '목적을 1개 이상 선택해 주세요' }); + if (!selectedCheckboxId.branches.length) { + openAlert({ content: '분야를 1개 이상 선택해 주세요.' }); return; } - if (!cooperationWay) { - openAlert({ content: '협업방식을 선택해 주세요' }); + if (!selectedCheckboxId.purposes.length) { + openAlert({ content: '목적을 1개 이상 선택해 주세요.' }); return; } - if (!recruitmentPlaceId) { - openAlert({ content: '모집지역을 선택해주세요.' }); + if (!selectedRadioName.cooperationWays) { + openAlert({ content: '협업방식을 선택해 주세요.' }); return; } @@ -138,11 +102,11 @@ const WriteEditPage = () => { idea: { title, introduce, - recruitmentPlaceId, - cooperationWay, - branchIds, - purposeIds, - skillCategoryIds, + recruitmentPlaceId: recruitmentPlaces.find((place) => place.name === dropdownValue.recruitmentPlace)?.id || 1, + cooperationWay: selectedRadioName.cooperationWays, + branchIds: selectedCheckboxId.branches, + purposeIds: selectedCheckboxId.purposes, + skillCategoryIds: selectedSkillResponses.map((selectedSkillResponse) => selectedSkillResponse.id), }, }); }; @@ -157,7 +121,7 @@ const WriteEditPage = () => { const onClickTeamRecruitment = (selected: Info) => { if (selectedSkillResponses.length >= 10) { - openAlert({ content: '10개 이상 선택할 수 없습니다.' }); + openAlert({ content: '최대 10개까지 선택할 수 있습니다.' }); return; } setSelectedSkillResponses((prev) => @@ -245,7 +209,7 @@ const WriteEditPage = () => { return ( {item.name} - onDeleteTeamRecruitment(item.id)} /> + onDeleteTeamRecruitment(item.id)} cursor="pointer" /> ); })} @@ -258,17 +222,21 @@ const WriteEditPage = () => { setIsOpenBottomSheet(false)}> { setIsOpenBottomSheet(false); }} + cursor="pointer" /> - 팀원선택 + 팀원 선택 { setIsOpenBottomSheet(false); }} + cursor="pointer" /> @@ -340,6 +308,7 @@ const Sheet_BodyBox = styled.div` const Sheet_Left = styled.div` width: 38%; + cursor: pointer; `; const Sheet_leftItem = styled.div<{ checked: boolean }>` @@ -369,6 +338,7 @@ const Sheet_radioDiv = styled.div` height: 54px; border-bottom: 1px solid rgba(0, 0, 0, 0.1); + cursor: pointer; `; const TeamLabelBox = styled.div` diff --git a/src/pages/WriteEdit/components/Header.tsx b/src/pages/WriteEdit/components/Header.tsx index 657fee4d..4abe64eb 100644 --- a/src/pages/WriteEdit/components/Header.tsx +++ b/src/pages/WriteEdit/components/Header.tsx @@ -24,7 +24,7 @@ const Header = ({ onClickCheckButton, isCheckButtonEnabled }: Props) => { 게시글 수정 ); diff --git a/src/pages/WriteEdit/components/TitleAndIntroduceSection.tsx b/src/pages/WriteEdit/components/TitleAndIntroduceSection.tsx index 65194bae..97dd8b61 100644 --- a/src/pages/WriteEdit/components/TitleAndIntroduceSection.tsx +++ b/src/pages/WriteEdit/components/TitleAndIntroduceSection.tsx @@ -29,11 +29,11 @@ const TitleAndIntroduceSection = ({ title, introduce, onTitleChange, onIntroduce return ( <> - +
diff --git a/src/pages/WriteEdit/hooks/queries/useWritingInfoQuery.ts b/src/pages/WriteEdit/hooks/queries/useWritingInfoQuery.ts index d80f2e43..fc5b0b45 100644 --- a/src/pages/WriteEdit/hooks/queries/useWritingInfoQuery.ts +++ b/src/pages/WriteEdit/hooks/queries/useWritingInfoQuery.ts @@ -1,22 +1,51 @@ import { useSuspenseQuery } from '@tanstack/react-query'; +import { useIdeaDetailQuery } from './useIdeaDetailQuery'; import { http } from '../../../../api/http'; import { Idea } from '../../types'; +const cooperations = [ + { id: 1, name: '상관없음' }, + { id: 2, name: '온라인' }, + { id: 3, name: '오프라인' }, +]; + const getWritingInfo = async () => { return http.get('/ideas/writing'); }; -export const useWritingInfoQuery = () => { - const { data: writingInfo, ...rest } = useSuspenseQuery({ queryKey: ['writingInfo'], queryFn: getWritingInfo }); +export const useWritingEditInfoQuery = (ideaId: number) => { + const { ideaDetail } = useIdeaDetailQuery(ideaId); + const { data: writingInfo, ...rest } = useSuspenseQuery({ + queryKey: ['writingInfo'], + queryFn: getWritingInfo, + select: (data) => { + const branches = data.branches.map((properties) => + ideaDetail.branchList.includes(properties.name) + ? { checked: true, ...properties } + : { checked: false, ...properties }, + ); + const purposes = data.purposes.map((properties) => + ideaDetail.purposeList.includes(properties.name) + ? { checked: true, ...properties } + : { checked: false, ...properties }, + ); + const cooperationWays = cooperations.map((properties) => { + // 협업방식: 상관없음이 기본값(id === 1) + return properties.name === ideaDetail.cooperationWay + ? { checked: true, ...properties } + : { checked: false, ...properties }; + }); + return { + ...data, + branches, + purposes, + cooperationWays, + }; + }, + }); - const { branches, purposes, regions: recruitmentPlaces, skillCategoryResponses } = writingInfo; + const { branches, purposes, regions: recruitmentPlaces, cooperationWays, skillCategoryResponses } = writingInfo; - return { - branches, - purposes, - recruitmentPlaces, - skillCategoryResponses, - ...rest, - }; + return { ideaDetail, branches, purposes, recruitmentPlaces, cooperationWays, skillCategoryResponses, ...rest }; }; diff --git a/src/pages/components/HyperLinkText/HyperLinkText.tsx b/src/pages/components/HyperLinkText/HyperLinkText.tsx index de5e797f..aa6a5d5a 100644 --- a/src/pages/components/HyperLinkText/HyperLinkText.tsx +++ b/src/pages/components/HyperLinkText/HyperLinkText.tsx @@ -1,5 +1,6 @@ import styled from '@emotion/styled'; import { Text } from 'concept-be-design-system'; +import { Fragment } from 'react'; import { ColorKeyType, FontKeyType } from '../../../styles/theme'; @@ -13,22 +14,30 @@ interface Props { const LINK_REG_EXP = /(https?:\/\/|www\.)/; const convertHyperLinkTexts = ({ font, color, lineHeight = 'normal', children: text }: Props) => { const generatedTexts = text.split('\n').map((line, idx) => { - if (line === '') return
; + if (line === '') return
; return ( - + {line.split(' ').map((word, idx) => { const isFirst = idx === 0; if (LINK_REG_EXP.test(word)) { return ( - + {word} ); } - return <>{isFirst ? word : ` ${word}`}; + return {isFirst ? word : ` ${word}`}; })} ); diff --git a/src/pages/components/NewIdeaCard/NewIdeaCardSkeleton.tsx b/src/pages/components/NewIdeaCard/NewIdeaCardSkeleton.tsx index 35a9ae46..32fdd799 100644 --- a/src/pages/components/NewIdeaCard/NewIdeaCardSkeleton.tsx +++ b/src/pages/components/NewIdeaCard/NewIdeaCardSkeleton.tsx @@ -1,4 +1,4 @@ -import Skeleton from '../../../components/Skeleton/Skeleton'; +import { Skeleton } from 'concept-be-design-system'; const NewIdeaCardSkeleton = () => { return ; diff --git a/src/router.tsx b/src/router.tsx index b656feff..eed2b3d2 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -1,21 +1,23 @@ -import { ReactNode, Suspense } from 'react'; +import { Spinner } from 'concept-be-design-system'; +import { ReactNode, Suspense, lazy } from 'react'; import { createBrowserRouter } from 'react-router-dom'; import ApiErrorBoundary from './components/ErrorBoundary/ApiErrorBoundary'; -import Spinner from './components/Spinner/Spinner'; import MobileView from './layouts/MobileView'; -import Feed from './pages/Feed/Feed.page'; -import FeedDetailPage from './pages/FeedDetail/FeedDetail.page'; -import Agreement from './pages/Login/Agreement'; -import Login from './pages/Login/Login'; -import OauthRedirect from './pages/Login/OauthRedirect'; -import NotFound from './pages/NotFound'; import More from './pages/Profile/More.page'; -import Profile from './pages/Profile/Profile.page'; -import ProfileEdit from './pages/ProfileEdit/ProfileEdit.page'; -import SignUpPage from './pages/SignUp/SignUp.page'; -import WritePage from './pages/Write/Write.page'; -import WriteEditPage from './pages/WriteEdit/WriteEdit.page'; + +const Feed = lazy(() => import('./pages/Feed/Feed.page')); +const FeedDetailPage = lazy(() => import('./pages/FeedDetail/FeedDetail.page')); +const WritePage = lazy(() => import('./pages/Write/Write.page')); +const WriteEditPage = lazy(() => import('./pages/WriteEdit/WriteEdit.page')); +const Agreement = lazy(() => import('./pages/Login/Agreement')); +const OauthRedirect = lazy(() => import('./pages/Login/OauthRedirect')); +const Login = lazy(() => import('./pages/Login/Login')); +const NotFound = lazy(() => import('./pages/NotFound')); +const Profile = lazy(() => import('./pages/Profile/Profile.page')); +const ProfileEdit = lazy(() => import('./pages/ProfileEdit/ProfileEdit.page')); +const SignUpPage = lazy(() => import('./pages/SignUp/SignUp.page')); +const NeedAuth = lazy(() => import('./pages/NeedAuth')); interface RouteElement { path: string; @@ -47,7 +49,11 @@ const routes: RouteElement[] = [ }, { path: '/write', - element: withAsyncBoundary(), + element: withAsyncBoundary( + + + , + ), }, { path: '/write-edit', diff --git a/yarn.lock b/yarn.lock index 0c591255..918981c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1291,10 +1291,10 @@ concat-map@0.0.1: resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== -concept-be-design-system@^0.4.12: - version "0.4.12" - resolved "https://registry.npmjs.org/concept-be-design-system/-/concept-be-design-system-0.4.12.tgz" - integrity sha512-ktgcRqgo1PKlRkcu4qU33fdNTeZZZVor+pF1QF/ibaemagYjvESsgiluLHwxqxxhUOK+sGEhVy9dUPWUEkSF8g== +concept-be-design-system@^0.5.3: + version "0.5.3" + resolved "https://registry.npmjs.org/concept-be-design-system/-/concept-be-design-system-0.5.3.tgz" + integrity sha512-ThU8egZDs1krKizh6YqXiTvlQcUQV46HFD1lGVlsTThrvH1m2YRkPht6NwbZ2rvsO+K5e8QGS9CM1PzZUSiJiA== dependencies: "@emotion/react" "^11.11.1" "@emotion/styled" "^11.11.0"