From 743b11319f051dc27445f8e01e2fa2c7bf7de4cf Mon Sep 17 00:00:00 2001 From: Jo Cheonsan Date: Mon, 9 Dec 2024 23:49:13 +0900 Subject: [PATCH] =?UTF-8?q?[Feat]=20=EA=B2=8C=EB=8D=94=EB=A7=81=20?= =?UTF-8?q?=EB=B0=8F=20=ED=8F=AC=ED=8A=B8=ED=8F=B4=EB=A6=AC=EC=98=A4=20?= =?UTF-8?q?=EB=AF=B8=ED=9D=A1=ED=95=9C=20=EB=B6=80=EB=B6=84=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=20(#113)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 게더링 디테일 헤더 게시글 작성자 정보로 수정 * refactor: 게더링 연락 버튼 유저네임 연결 * refactor: 게더링 카드 수정 * refactor: 포트폴리오 카드 링크 추가 부분 추가 * reafactor: 포트폴리오 유저페이지 연결 * feat: 포트폴리오 좋아요 * [Fix] 무한 스크롤 수정 (#107) * fix: useCumtomInfiniteQuery 수정 * feat: 실시간 알림 기능 추가 * docs: README 작성 * feat: 포폴 좋아요한 리스트 훅 * feat: 메인 게더링 그리드 구현 * feat: 게더링 좋아요 리스트 * reafactor: console.log 지우기 * refactor: main.tsx mock 코드 삭제 * refactor: 팔레트 철자 변경 * refactor: alert 정리 * refactor: 태크 키씹힘 수정 * refactor: 게더링 좋아요 수정 * fix: 배포오류 수정 * fix: 배포 오류 수정 --------- Co-authored-by: Cho-heejung <66050038+he2e2@users.noreply.github.com> --- README.md | 2 +- index.html | 2 +- package.json | 2 +- src/app/main.tsx | 6 -- src/features/gathering/api/gathering.api.ts | 52 ++++++++++++ src/features/gathering/index.ts | 5 +- src/features/gathering/lib/hooks/index.ts | 3 + .../gathering/lib/hooks/useCreateGathering.ts | 23 +++-- .../gathering/lib/hooks/useGatheringLike.ts | 22 +++-- .../lib/hooks/useGatheringLikeList.ts | 51 ++++++++++++ .../lib/hooks/useInfiniteGatheringId.ts | 11 +-- .../gathering/lib/hooks/useMainGathering.ts | 21 +++++ .../gathering/model/dto/gathering.dto.ts | 4 +- .../gathering/model/dto/request.dto.ts | 5 +- src/features/gathering/model/options.ts | 2 +- .../GatheringCard/GatheringCard.module.scss | 2 +- .../ui/GatheringCard/GatheringCard.tsx | 33 ++++---- .../GatheringDetailUserInfo.module.scss | 3 + .../GatheringDetailUserInfo.tsx | 16 ++-- .../GatheringInfoItem.module.scss | 10 +++ .../ui/GatheringDetail/GatheringInfoItem.tsx | 26 +++++- .../gathering/ui/GatheringTagInput.tsx | 11 ++- .../gathering/ui/useGatheringMarkdown.ts | 2 +- src/features/portfolio/api/portfolio.api.ts | 20 +++++ .../hooks/useInfinitePortfolioLikeList.ts | 47 +++++++++++ .../portfolio/hooks/usePortfolioLike.ts | 35 ++++++++ src/features/portfolio/index.ts | 2 + src/features/portfolio/model/types.ts | 14 ++++ src/features/portfolio/ui/ContactBtn.tsx | 3 +- .../portfolio/ui/PortfolioCard.module.scss | 35 ++++++++ src/features/portfolio/ui/PortfolioCard.tsx | 77 ++++++++++++----- .../GatheringDetailPage.tsx | 16 ++-- .../PortfolioListPage/PortfolioListPage.tsx | 2 +- .../WriteGatheringPage/WriteGatheringPage.tsx | 23 ++++- src/shared/api/baseApi.ts | 8 +- src/shared/ui/LikeBtn/LikeBtn.module.scss | 4 - src/shared/ui/LikeBtn/LikeBtn.tsx | 15 ++-- src/widgets/ContactModal/ui/ContactModal.tsx | 2 +- .../GatheringDetail/GatheringDetailBtnCon.tsx | 14 +++- .../GatheringDetailGrid.module.scss | 1 + .../GatheringDetail/GatheringDetailGrid.tsx | 3 + .../GatheringDetail/GatheringDetailHeader.tsx | 5 +- src/widgets/GatheringGrid/GatheringGrid.tsx | 6 +- src/widgets/Layout/ui/Footer/Footer.tsx | 2 +- src/widgets/Layout/ui/Header/Header.tsx | 2 +- src/widgets/LikeTab/LikeTab.module.scss | 21 +++++ src/widgets/LikeTab/LikeTab.tsx | 83 +++++++++++++------ .../MainGatheringGrid.module.scss | 45 ++++++++++ .../MainGatheringGrid/MainGatheringGrid.tsx | 36 ++++++++ src/widgets/MainGridItem/MainGridItem.tsx | 4 +- .../MainPortfolioGrid/MainPortfolioGrid.tsx | 2 +- src/widgets/RegisterUser/ProfileStep.tsx | 2 +- src/widgets/SettingUser/SetProfile.tsx | 2 +- src/widgets/index.ts | 2 + 54 files changed, 697 insertions(+), 150 deletions(-) create mode 100644 src/features/gathering/lib/hooks/useGatheringLikeList.ts create mode 100644 src/features/gathering/lib/hooks/useMainGathering.ts create mode 100644 src/features/portfolio/hooks/useInfinitePortfolioLikeList.ts create mode 100644 src/features/portfolio/hooks/usePortfolioLike.ts diff --git a/README.md b/README.md index fe106bb..9e14e4c 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@

자신만의 경험을 색상과 함께 공유하고,
네트워킹을 통해 팔레트를 채워주세요!

-https://www.palettee.site/ +https://www.palette.site/
diff --git a/index.html b/index.html index 1238aea..8bacd25 100644 --- a/index.html +++ b/index.html @@ -7,7 +7,7 @@ name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, viewport-fit=cover, user-scalable=no" /> - Palettee + Palette
diff --git a/package.json b/package.json index 1daeb85..0cd612a 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "palettee", + "name": "palette", "private": true, "version": "1.0.1", "type": "module", diff --git a/src/app/main.tsx b/src/app/main.tsx index e61dbbb..3afbfbd 100644 --- a/src/app/main.tsx +++ b/src/app/main.tsx @@ -4,12 +4,6 @@ import { createRoot } from 'react-dom/client'; import App from './App'; import './styles/globals.scss'; -import { worker } from '@/mocks/browser'; - -if (process.env.NODE_ENV === 'development') { - void worker.start({ onUnhandledRequest: 'warn' }); -} - createRoot(document.getElementById('root')!).render( diff --git a/src/features/gathering/api/gathering.api.ts b/src/features/gathering/api/gathering.api.ts index a3b1c92..3951d2a 100644 --- a/src/features/gathering/api/gathering.api.ts +++ b/src/features/gathering/api/gathering.api.ts @@ -8,6 +8,7 @@ import type { import api from '@/shared/api/baseApi'; +// 기본 게더링 API export const gatheringApi = { getGatherings: async (params: GatheringListParams): Promise => { // params를 URLSearchParams로 변환 @@ -41,6 +42,7 @@ export const gatheringApi = { }); return data; }, + update: async ( gatheringId: string, data: CreateGatheringRequest, @@ -53,10 +55,60 @@ export const gatheringApi = { const { data } = await api.post(`/gathering/${gatheringId}/like`); return data; }, + deleteGathering: async (gatheringId: string): Promise => { await api.delete(`/gathering/${gatheringId}`); }, + completeGathering: async (gatheringId: string): Promise => { await api.patch(`/gathering/${gatheringId}`); }, + + // 좋아요한 게더링 목록 조회 추가 + getGatheringLikeList: async (params: { + size: number; + gatheringId?: number; + }): Promise => { + const queryString = new URLSearchParams(); + + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined) { + queryString.append(key, value.toString()); + } + }); + + const { data } = await api.get( + `/gathering/my-page?${queryString.toString()}`, + ); + return data; + }, +}; + +// 메인 페이지용 게더링 API +export const mainGatheringApi = { + // 메인 페이지용 게더링 목록 조회 (최신 4개) + getMainGatherings: async (): Promise => { + const params: GatheringListParams = { + page: 1, + size: 4, + status: '모집중', // 활성화된 게더링만 표시 + }; + + // params를 URLSearchParams로 변환 + const queryString = new URLSearchParams(); + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined && value !== 'undefined') { + queryString.append(key, value.toString()); + } + }); + + const { data } = await api.get(`/gathering?${queryString.toString()}`); + return data; + }, +}; + +// 두 API 객체를 모두 export +export default { + ...gatheringApi, + ...mainGatheringApi, }; diff --git a/src/features/gathering/index.ts b/src/features/gathering/index.ts index 6d04235..21172a5 100644 --- a/src/features/gathering/index.ts +++ b/src/features/gathering/index.ts @@ -1,5 +1,8 @@ +export * from './lib/hooks'; +export * from './model/index'; export * from './model/options'; export * from './model/types'; +export * from './ui/GatheringCard'; export { GatheringDatePicker } from './ui/GatheringDatePicker'; export * from './ui/GatheringDetail/index'; export { GatheringLinkInput } from './ui/GatheringLinkInput'; @@ -7,5 +10,3 @@ export { GatheringMarkdownEditor } from './ui/GatheringMarkdownEditor'; export { GatheringSelect } from './ui/GatheringSelect'; export { GatheringTagInput } from './ui/GatheringTagInput'; export { GatheringTitleInput } from './ui/GatheringTitIeInput'; -export * from './lib/hooks'; -export * from './model/index'; diff --git a/src/features/gathering/lib/hooks/index.ts b/src/features/gathering/lib/hooks/index.ts index f11a6e4..3ee8222 100644 --- a/src/features/gathering/lib/hooks/index.ts +++ b/src/features/gathering/lib/hooks/index.ts @@ -3,5 +3,8 @@ export * from './useCreateGathering'; export * from './useDeleteGathering'; export * from './useGatheringDetail'; export * from './useGatheringLike'; +export * from './useGatheringLikeList'; export * from './useInfiniteGatheringId'; +export * from './useMainGathering'; export * from './useUpdateGathering'; + diff --git a/src/features/gathering/lib/hooks/useCreateGathering.ts b/src/features/gathering/lib/hooks/useCreateGathering.ts index 41cfbbd..15de53a 100644 --- a/src/features/gathering/lib/hooks/useCreateGathering.ts +++ b/src/features/gathering/lib/hooks/useCreateGathering.ts @@ -5,6 +5,7 @@ import { useNavigate } from 'react-router-dom'; import { gatheringApi } from '../../api/gathering.api'; import type { CreateGatheringRequest, CreateGatheringResponse } from '../../model/dto/request.dto'; +import { errorAlert } from '@/shared/ui'; export const useCreateGathering = () => { const queryClient = useQueryClient(); const navigate = useNavigate(); @@ -27,18 +28,30 @@ export const useCreateGathering = () => { alert('게더링은 생성되었으나 추가 처리 중 오류가 발생했습니다.'); } }, - onError: (error: AxiosError) => { + onError: async (error: AxiosError) => { if (error.response?.status === 401) { - alert('로그인이 필요합니다.'); + await errorAlert({ + title: '로그인 필요', + text: '로그인이 필요한 서비스입니다. 로그인 페이지로 이동합니다.', + }); navigate('/login'); } else if (error.response?.status === 403) { - alert('권한이 부족합니다. 다시 로그인해주세요.'); + await errorAlert({ + title: '권한 없음', + text: '권한이 없습니다. 로그인 후 다시 시도해주세요.', + }); navigate('/login'); } else if (error.response?.status === 400) { - alert('입력하신 정보를 다시 확인해주세요.'); + await errorAlert({ + title: '입력값 오류', + text: '입력값을 확인해주세요', + }); } else { console.error('게더링 생성 실패:', error); - alert('게더링 생성에 실패했습니다. 잠시 후 다시 시도해주세요.'); + await errorAlert({ + title: '게더링 생성 실패', + text: '게더링 생성에 실패했습니다.', + }); } }, }); diff --git a/src/features/gathering/lib/hooks/useGatheringLike.ts b/src/features/gathering/lib/hooks/useGatheringLike.ts index e3beb25..49e4668 100644 --- a/src/features/gathering/lib/hooks/useGatheringLike.ts +++ b/src/features/gathering/lib/hooks/useGatheringLike.ts @@ -20,22 +20,32 @@ export const useGatheringLike = ({ gatheringId, onSuccess, onError }: UseGatheri mutationFn: () => gatheringApi.toggleLike(gatheringId), onMutate: async () => { + // 진행 중인 쿼리 취소 await queryClient.cancelQueries({ queryKey: ['/gatheringDetail', gatheringId], }); + const previousDetail = queryClient.getQueryData([ '/gatheringDetail', gatheringId, ]); - console.log('이전 상태:', previousDetail); + + if (previousDetail?.data) { + queryClient.setQueryData(['/gatheringDetail', gatheringId], { + ...previousDetail, + data: { + ...previousDetail.data, + isLiked: !previousDetail.data.isLiked, + }, + }); + } + return { previousDetail }; }, onSuccess: response => { - console.log('좋아요 API 응답:', response); - const currentDetail = queryClient.getQueryData([ '/gatheringDetail', gatheringId, @@ -51,18 +61,16 @@ export const useGatheringLike = ({ gatheringId, onSuccess, onError }: UseGatheri data: { ...currentDetail.data, likeCounts: newLikeCounts, + isLiked: !!response.data, }, }); - - console.log('캐시 업데이트 완료, 새로운 좋아요 수:', newLikeCounts); } onSuccess?.(response); }, onError: (error, _, context) => { - console.log('에러 발생:', error); - + if (context?.previousDetail) { queryClient.setQueryData(['/gatheringDetail', gatheringId], context.previousDetail); } diff --git a/src/features/gathering/lib/hooks/useGatheringLikeList.ts b/src/features/gathering/lib/hooks/useGatheringLikeList.ts new file mode 100644 index 0000000..5d53b65 --- /dev/null +++ b/src/features/gathering/lib/hooks/useGatheringLikeList.ts @@ -0,0 +1,51 @@ +import { useInfiniteQuery } from '@tanstack/react-query'; + +import { gatheringApi } from '../../api/gathering.api'; +import type { GatheringPageResponse, GatheringItem } from '../../model/dto/gathering.dto'; + +interface UseGatheringLikeListProps { + size?: number; +} + +interface GatheringLikeListParams { + size: number; + likeId: number; +} + +export const useGatheringLikeList = ({ size = 10 }: UseGatheringLikeListProps = {}) => { + const query = useInfiniteQuery({ + queryKey: ['infiniteLikeGatherings'], + queryFn: async ({ pageParam }) => { + const params: GatheringLikeListParams = { + size, + likeId: pageParam as number, + }; + + return gatheringApi.getGatheringLikeList(params); + }, + getNextPageParam: lastPage => { + if (!lastPage?.data?.hasNext) return undefined; + return lastPage?.data.nextId ?? undefined; + }, + initialPageParam: undefined, + }); + + const gatherings = + query.data?.pages?.reduce((acc, page) => { + const content = page.data?.content || []; + + const newGatherings = content.filter( + newGathering => + !acc.some( + existingGathering => existingGathering.gatheringId === newGathering.gatheringId, + ), + ); + + return [...acc, ...newGatherings]; + }, []) ?? []; + + return { + ...query, + gatherings, + }; +}; diff --git a/src/features/gathering/lib/hooks/useInfiniteGatheringId.ts b/src/features/gathering/lib/hooks/useInfiniteGatheringId.ts index f846660..b12d29d 100644 --- a/src/features/gathering/lib/hooks/useInfiniteGatheringId.ts +++ b/src/features/gathering/lib/hooks/useInfiniteGatheringId.ts @@ -50,20 +50,17 @@ export const useInfiniteGatheringId = ({ ...(pageParam ? { gatheringId: pageParam } : {}), }; - console.log('API Request:', params); + const response = await gatheringApi.getGatherings(params); - console.log('API Response:', response); + return response; }, getNextPageParam: (lastPage: GatheringPageResponse) => { - // console.log('Getting next page param:', { - // hasNext: lastPage.data.hasNext, - // nextLikeId: lastPage.data.nextLikeId, - // }); + if (!lastPage.data.hasNext) return undefined; - return lastPage.data.nextLikeId ?? undefined; + return lastPage.data.nextId ?? undefined; }, initialPageParam: undefined, }); diff --git a/src/features/gathering/lib/hooks/useMainGathering.ts b/src/features/gathering/lib/hooks/useMainGathering.ts new file mode 100644 index 0000000..d7e823f --- /dev/null +++ b/src/features/gathering/lib/hooks/useMainGathering.ts @@ -0,0 +1,21 @@ +import { useQuery } from '@tanstack/react-query'; + +import { mainGatheringApi } from '../../api/gathering.api'; + +export const useMainGathering = () => { + const query = useQuery({ + queryKey: ['mainGatheringList'], + queryFn: async () => { + const response = await mainGatheringApi.getMainGatherings(); + return response; + }, + }); + + // items 배열 추출 + const items = query.data?.data.content ?? []; + + return { + ...query, + items, + }; +}; diff --git a/src/features/gathering/model/dto/gathering.dto.ts b/src/features/gathering/model/dto/gathering.dto.ts index 2f54e6d..93dbe51 100644 --- a/src/features/gathering/model/dto/gathering.dto.ts +++ b/src/features/gathering/model/dto/gathering.dto.ts @@ -39,12 +39,14 @@ export interface GatheringItem { deadLine: string; username: string; tags: string[]; + positions: GatheringPosition[]; + subject: string; } export interface GatheringPageResponse { data: { content: GatheringItem[]; hasNext: boolean; - nextLikeId: number | null; + nextId: number | null; }; timeStamp: string; } diff --git a/src/features/gathering/model/dto/request.dto.ts b/src/features/gathering/model/dto/request.dto.ts index c49dea0..4f6dd09 100644 --- a/src/features/gathering/model/dto/request.dto.ts +++ b/src/features/gathering/model/dto/request.dto.ts @@ -27,7 +27,7 @@ export interface CreateGatheringRequest { interface GatheringListContent { content: GatheringDetailContent[]; hasNext: boolean; - nextLikeId: number; + nextId: number; } export interface GetGatheringsParams { @@ -36,7 +36,7 @@ export interface GetGatheringsParams { position?: GatheringPosition; status?: '모집중' | '모집완료' | '기간만료'; size?: number; - nextLikeId?: number; + nextId?: number; } // 게더링 상세 조회 응답 @@ -56,6 +56,7 @@ export interface GatheringDetailContent { title: string; content: string; likeCounts: number; + isLiked: boolean; status: '모집중' | '모집완료' | '기간만료'; } diff --git a/src/features/gathering/model/options.ts b/src/features/gathering/model/options.ts index 7264af2..9d6c5f0 100644 --- a/src/features/gathering/model/options.ts +++ b/src/features/gathering/model/options.ts @@ -4,7 +4,7 @@ export const gatheringFilterOptions: GatheringFilterOptions = { contact: [ { value: '온라인', label: '온라인' }, { value: '오프라인', label: '오프라인' }, - { value: '온라인&오프라인', label: '온라인&오프라인' }, + { value: '온라인 & 오프라인', label: '온라인 & 오프라인' }, ], sort: [ { value: '스터디', label: '스터디' }, diff --git a/src/features/gathering/ui/GatheringCard/GatheringCard.module.scss b/src/features/gathering/ui/GatheringCard/GatheringCard.module.scss index d688e74..3b0283c 100644 --- a/src/features/gathering/ui/GatheringCard/GatheringCard.module.scss +++ b/src/features/gathering/ui/GatheringCard/GatheringCard.module.scss @@ -32,7 +32,7 @@ font-weight: 500; } - &__introduction { + &__sort { display: -webkit-box; // 추가: Webkit 기반 브라우저 지원 display: -moz-box; // 추가: Firefox 지원 max-height: 2.4em; // 추가: line-height * 2 diff --git a/src/features/gathering/ui/GatheringCard/GatheringCard.tsx b/src/features/gathering/ui/GatheringCard/GatheringCard.tsx index ee926e1..b2926be 100644 --- a/src/features/gathering/ui/GatheringCard/GatheringCard.tsx +++ b/src/features/gathering/ui/GatheringCard/GatheringCard.tsx @@ -1,5 +1,5 @@ -import { faHeart, faPhone } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +// import { faHeart, faPhone } from '@fortawesome/free-solid-svg-icons'; +// import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import cn from 'classnames'; import { Link } from 'react-router-dom'; @@ -12,20 +12,24 @@ export interface GatheringCardProps { title: string; className?: string; name?: string; - introduction?: string; - tag?: string[]; + sort?: string; deadline?: string; gatheringId: number; + username: string; + positions: string[]; + subject: string; } export const GatheringCard = ({ title, className, name, - introduction, - tag, + sort, deadline, gatheringId, + username, + positions, + subject, }: GatheringCardProps) => { const handleButtonClick = (e: React.MouseEvent) => { e.preventDefault(); @@ -46,19 +50,20 @@ export const GatheringCard = ({
  • {title}

    {name}

    -
    {introduction}
    -
      {tag?.map((e, i) => )}
    +
    + {sort} / {subject} +
    +
      + {positions?.map((e, i) => )} +
    마감일 {deadline}
    - - + */}
    - +
    diff --git a/src/features/gathering/ui/GatheringDetail/GatheringDetailUserInfo.module.scss b/src/features/gathering/ui/GatheringDetail/GatheringDetailUserInfo.module.scss index 83c97df..594608e 100644 --- a/src/features/gathering/ui/GatheringDetail/GatheringDetailUserInfo.module.scss +++ b/src/features/gathering/ui/GatheringDetail/GatheringDetailUserInfo.module.scss @@ -6,7 +6,10 @@ .profileImg { width: 2rem; height: 2rem; + padding: 0.5rem; + background-color: $primary-color; border-radius: 50%; + fill: $secondary-color; } .name { diff --git a/src/features/gathering/ui/GatheringDetail/GatheringDetailUserInfo.tsx b/src/features/gathering/ui/GatheringDetail/GatheringDetailUserInfo.tsx index dbb4a9a..73d90c9 100644 --- a/src/features/gathering/ui/GatheringDetail/GatheringDetailUserInfo.tsx +++ b/src/features/gathering/ui/GatheringDetail/GatheringDetailUserInfo.tsx @@ -1,23 +1,19 @@ import styles from './GatheringDetailUserInfo.module.scss'; +import Logo from '@/shared/assets/paletteLogo.svg?react'; interface GatheringDetailUserInfoProps { username: string; position?: string; profileImage?: string; } -import { useUserStore } from '@/features/user/model/user.store'; -export const GatheringDetailUserInfo = ({ - username, - // position = 'Front Developer', - // profileImage = '/default-profile.png', -}: GatheringDetailUserInfoProps) => { - const userData = useUserStore(state => state.userData); +export const GatheringDetailUserInfo = ({ username }: GatheringDetailUserInfoProps) => { return (
    - {username} - {userData?.name} - {userData?.role} + {/* {username} */} + + {username} + {/* {userData?.role} */}
    ); }; diff --git a/src/features/gathering/ui/GatheringDetail/GatheringInfoItem.module.scss b/src/features/gathering/ui/GatheringDetail/GatheringInfoItem.module.scss index 2a9bb64..eab0e1b 100644 --- a/src/features/gathering/ui/GatheringDetail/GatheringInfoItem.module.scss +++ b/src/features/gathering/ui/GatheringDetail/GatheringInfoItem.module.scss @@ -14,3 +14,13 @@ color: $primary-color; } } + +.link { + color: $third-color; + text-decoration: none; + word-break: break-all; + + &:hover { + text-decoration: underline; + } +} diff --git a/src/features/gathering/ui/GatheringDetail/GatheringInfoItem.tsx b/src/features/gathering/ui/GatheringDetail/GatheringInfoItem.tsx index 454e6f5..0b66a7f 100644 --- a/src/features/gathering/ui/GatheringDetail/GatheringInfoItem.tsx +++ b/src/features/gathering/ui/GatheringDetail/GatheringInfoItem.tsx @@ -2,12 +2,34 @@ interface GatheringInfoItemProps { label: string; value: string | number | string[]; } + import styles from './GatheringInfoItem.module.scss'; + +const isValidUrl = (str: string) => { + try { + new URL(str); + return true; + } catch { + return false; + } +}; + export const GatheringInfoItem = ({ label, value }: GatheringInfoItemProps) => { + const renderValue = (val: string | number) => { + if (typeof val === 'string' && isValidUrl(val)) { + return ( + + {val} + + ); + } + return {val}; + }; + return (
  • {label} - {value instanceof Array ? ( + {Array.isArray(value) ? (
    {value.map((tag, index) => ( @@ -16,7 +38,7 @@ export const GatheringInfoItem = ({ label, value }: GatheringInfoItemProps) => { ))}
    ) : ( - {value} + renderValue(value) )}
  • ); diff --git a/src/features/gathering/ui/GatheringTagInput.tsx b/src/features/gathering/ui/GatheringTagInput.tsx index b7b6b93..fce404a 100644 --- a/src/features/gathering/ui/GatheringTagInput.tsx +++ b/src/features/gathering/ui/GatheringTagInput.tsx @@ -25,13 +25,16 @@ export const GatheringTagInput = ({ placeholder = '태그를 입력해주세요', }: GatheringTagInputProps) => { const [inputValue, setInputValue] = useState(''); + const [isComposing, setIsComposing] = useState(false); const handleKeyDown = ( e: KeyboardEvent, onChange: HandleChangeFunction, currentValue: string[], ) => { - if ((e.key === 'Enter' || (e.metaKey && e.key === 'Enter')) && inputValue.trim()) { + if (isComposing) return; + + if (e.key === 'Enter' && inputValue.trim()) { e.preventDefault(); if (currentValue.length >= 3) { return; @@ -70,6 +73,12 @@ export const GatheringTagInput = ({ onChange={e => { setInputValue(e.target.value); }} + onCompositionEnd={() => { + setIsComposing(false); + }} + onCompositionStart={() => { + setIsComposing(true); + }} onKeyDown={e => { handleKeyDown(e, onChange, value || []); }} diff --git a/src/features/gathering/ui/useGatheringMarkdown.ts b/src/features/gathering/ui/useGatheringMarkdown.ts index a062c1f..38f30c5 100644 --- a/src/features/gathering/ui/useGatheringMarkdown.ts +++ b/src/features/gathering/ui/useGatheringMarkdown.ts @@ -47,7 +47,7 @@ export const useGatheringMarkdown = ({ editorViewRef }: UseGatheringMarkdownProp ) => { try { // TODO: 이미지 업로드 로직 구현 필요 - console.log('이미지 업로드 기능 준비중', { file }); + // console.log('이미지 업로드 기능 준비중', { file }); } catch (error) { console.error('이미지 업로드 실패:', error); } diff --git a/src/features/portfolio/api/portfolio.api.ts b/src/features/portfolio/api/portfolio.api.ts index e9aaa8f..9caeabe 100644 --- a/src/features/portfolio/api/portfolio.api.ts +++ b/src/features/portfolio/api/portfolio.api.ts @@ -1,8 +1,11 @@ import type { MainPortfolioResponse, PortfolioListApiResponse, + PortfolioLikeListApiResponse, PortfolioParams, PortfolioViewResponse, + PortfolioLikeResponse, + GetPortfolioLikeListParams, } from '../model/types'; import api from '@/shared/api/baseApi'; @@ -38,3 +41,20 @@ export const incrementViewCount = async ( const response = await api.get(`${PORTFOLIO_API_URL}/${portfolioId}`); return response.data; }; +export const togglePortfolioLike = async ( + portFolioId: string | number, +): Promise => { + const response = await api.post( + `${PORTFOLIO_API_URL}/${portFolioId}/likes`, + ); + return response.data; +}; + +export const getPorfolioLikeList = async ( + params?: GetPortfolioLikeListParams, +): Promise => { + const response = await api.get(`${PORTFOLIO_API_URL}/my-page`, { + params, + }); + return response.data; +}; diff --git a/src/features/portfolio/hooks/useInfinitePortfolioLikeList.ts b/src/features/portfolio/hooks/useInfinitePortfolioLikeList.ts new file mode 100644 index 0000000..c3f7238 --- /dev/null +++ b/src/features/portfolio/hooks/useInfinitePortfolioLikeList.ts @@ -0,0 +1,47 @@ +import { useInfiniteQuery } from '@tanstack/react-query'; + +import { getPorfolioLikeList } from '../api/portfolio.api'; +import type { Portfolio, PortfolioLikeListApiResponse } from '../model/types'; + +interface UseInfinitePortfolioLikeListProps { + size?: number; +} + +export const useInfinitePortfolioLikeList = ({ + size = 10, +}: UseInfinitePortfolioLikeListProps = {}) => { + const query = useInfiniteQuery({ + queryKey: ['infiniteLikePortfolios'], + queryFn: async ({ pageParam }) => { + const params = { + size, + ...(pageParam !== undefined ? { portFolioId: pageParam as number } : {}), + }; + + return getPorfolioLikeList(params); + }, + getNextPageParam: lastPage => { + if (!lastPage?.data?.hasNext) return undefined; + return lastPage?.data.nextId; + }, + initialPageParam: undefined, + }); + + const portfolios = + query.data?.pages?.reduce((acc, page) => { + if (!page?.data?.content) return acc; + + const newPortfolios = page.data.content.filter( + newPortfolio => + !acc.some( + existingPortfolio => existingPortfolio.portFolioId === newPortfolio.portFolioId, + ), + ); + return [...acc, ...newPortfolios]; + }, []) ?? []; + + return { + ...query, + portfolios, + }; +}; diff --git a/src/features/portfolio/hooks/usePortfolioLike.ts b/src/features/portfolio/hooks/usePortfolioLike.ts new file mode 100644 index 0000000..4cee032 --- /dev/null +++ b/src/features/portfolio/hooks/usePortfolioLike.ts @@ -0,0 +1,35 @@ +import { useMutation } from '@tanstack/react-query'; +import { useState } from 'react'; + +import { togglePortfolioLike } from '../api/portfolio.api'; + +interface UsePortfolioLikeProps { + portFolioId: string | number; + initialIsLiked?: boolean; +} + +export const usePortfolioLike = ({ + portFolioId, + initialIsLiked = false, +}: UsePortfolioLikeProps) => { + const [isLiked, setIsLiked] = useState(initialIsLiked); + + const { mutate: toggleLike, isPending } = useMutation({ + mutationFn: () => togglePortfolioLike(portFolioId), + onMutate: () => { + // 옵티미스틱 업데이트 + setIsLiked(prev => !prev); + }, + onError: error => { + // 실패시 원래 상태로 복구 + setIsLiked(prev => !prev); + console.error('좋아요 처리 실패:', error); + }, + }); + + return { + isLiked, + toggleLike, + isPending, + }; +}; diff --git a/src/features/portfolio/index.ts b/src/features/portfolio/index.ts index 11627b1..cdf78e5 100644 --- a/src/features/portfolio/index.ts +++ b/src/features/portfolio/index.ts @@ -1,4 +1,6 @@ +export { useInfinitePortfolioLikeList } from './hooks/useInfinitePortfolioLikeList'; export { useMainPortfolioList } from './hooks/useMainPortfolioList'; +export { usePortfolioLike } from './hooks/usePortfolioLike'; export { usePortfolioList } from './hooks/usePortfolioList'; export { usePortfolioView } from './hooks/usePortfolioView'; export type { Portfolio } from './model/types'; diff --git a/src/features/portfolio/model/types.ts b/src/features/portfolio/model/types.ts index 9a3578f..79b47b1 100644 --- a/src/features/portfolio/model/types.ts +++ b/src/features/portfolio/model/types.ts @@ -146,3 +146,17 @@ export type MainPortfolioResponse = ApiResponse; //포트폴리오 조회수 증가 API 응답 타입 export type PortfolioViewResponse = ApiResponse; + +//포트폴리오 좋아요 토글 API 응답 타입 +export type PortfolioLikeResponse = ApiResponse; + +interface PortfolioLikeList { + content: Portfolio[]; + hasNext: boolean; + nextId: number; +} +export type PortfolioLikeListApiResponse = ApiResponse; +export interface GetPortfolioLikeListParams { + size?: number; + portFolioId?: number | undefined; +} diff --git a/src/features/portfolio/ui/ContactBtn.tsx b/src/features/portfolio/ui/ContactBtn.tsx index 5ddc26b..4f00b6b 100644 --- a/src/features/portfolio/ui/ContactBtn.tsx +++ b/src/features/portfolio/ui/ContactBtn.tsx @@ -11,8 +11,7 @@ export const ContactBtn = ({ userName }: ContactBtnProps) => { const open = useModalStore(state => state.actions.open); const onOpenModal = () => { - console.log('openContModal'); - open('contact', userName); // userName을 두 번째 인자로 전달 + open('contact', userName); }; return ( diff --git a/src/features/portfolio/ui/PortfolioCard.module.scss b/src/features/portfolio/ui/PortfolioCard.module.scss index 9e6ccfc..ba253c6 100644 --- a/src/features/portfolio/ui/PortfolioCard.module.scss +++ b/src/features/portfolio/ui/PortfolioCard.module.scss @@ -16,6 +16,11 @@ gap: 1rem; } +.heart { + width: 1rem !important; + height: 1rem !important; +} + .cardHeader { position: relative; width: 100%; @@ -57,9 +62,25 @@ } .name { + position: relative; font-size: 1.25rem; font-weight: 600; color: $primary-color; + + &::after { + position: absolute; + bottom: -2px; + left: 0; + width: 0; + height: 2px; + content: ''; + background-color: $primary-color; + transition: width 0.3s ease; + } + + &:hover::after { + width: 100%; + } } .job { @@ -88,3 +109,17 @@ .container:hover .cardImg img { transform: scale(1.05); } + +.userLinks { + display: flex; + gap: 1rem; + + a { + color: $primary-color; + transition: color 0.3s ease; + + &:hover { + color: $primary-color, 10%; + } + } +} diff --git a/src/features/portfolio/ui/PortfolioCard.tsx b/src/features/portfolio/ui/PortfolioCard.tsx index 29b3bc8..941ab8d 100644 --- a/src/features/portfolio/ui/PortfolioCard.tsx +++ b/src/features/portfolio/ui/PortfolioCard.tsx @@ -1,3 +1,5 @@ +import { faLink } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Link } from 'react-router-dom'; import { ContactBtn } from './ContactBtn'; @@ -5,9 +7,10 @@ import styles from './PortfolioCard.module.scss'; import { getJobGroupDisplayName } from '../utils/jobGroupConverter'; import type { Portfolio } from '@/features'; -import { usePortfolioView } from '@/features'; -import Heart from '@/shared/assets/heart.svg'; +import { usePortfolioView, usePortfolioLike } from '@/features'; import profileImg from '@/shared/assets/paletteLogo.svg'; +import { LikeBtn } from '@/shared/ui/LikeBtn/LikeBtn'; + type PortfolioCardProps = Portfolio; export const PortfolioCard = ({ @@ -20,18 +23,28 @@ export const PortfolioCard = ({ memberImageUrl, jobTitle, userId, + relatedUrl, }: PortfolioCardProps) => { const { mutate: incrementView } = usePortfolioView({ - onSuccess: response => { - console.log('조회수 증가 성공:', response.data); - }, - onError: error => { - console.error('조회수 증가 실패:', error); - }, + onSuccess: () => {}, + onError: () => {}, + }); + + const { isLiked, toggleLike, isPending } = usePortfolioLike({ + portFolioId, }); - const handleClick = (e: React.MouseEvent) => { - e.preventDefault(); // 우선 페이지 이동을 막습니다 + const handleCardClick = (e: React.MouseEvent) => { + const clickedElement = e.target as HTMLElement; + if ( + clickedElement.closest('button') || + clickedElement.closest('a') || + clickedElement.closest(`.${styles.userLinks}`) + ) { + e.stopPropagation(); + return; + } + incrementView(portFolioId, { onSuccess: () => { window.location.href = portFolioUrl; @@ -39,8 +52,16 @@ export const PortfolioCard = ({ }); }; + const formatUrl = (url: string) => { + if (!url) return ''; + if (url.startsWith('http://') || url.startsWith('https://')) { + return url; + } + return `https://${url}`; + }; + return ( -
    +
    @@ -50,25 +71,43 @@ export const PortfolioCard = ({
    - {username} - + />
    {getJobGroupDisplayName(majorJobGroup)}/ + {getJobGroupDisplayName(minorJobGroup)} - {getJobGroupDisplayName(minorJobGroup)}
    @{jobTitle}
    {introduction}
    +
    diff --git a/src/pages/GatheringDetailPage/GatheringDetailPage.tsx b/src/pages/GatheringDetailPage/GatheringDetailPage.tsx index 4e260fb..0b3e26a 100644 --- a/src/pages/GatheringDetailPage/GatheringDetailPage.tsx +++ b/src/pages/GatheringDetailPage/GatheringDetailPage.tsx @@ -6,6 +6,7 @@ import { MarkdownPreview } from '@/features'; import { GatheringDetailUserInfo } from '@/features/gathering'; import { useGatheringDetail, useGatheringLike } from '@/features/gathering/lib/hooks'; import { Loader, TripleDot } from '@/shared/ui'; +import { customToast, errorAlert } from '@/shared/ui'; import { LikeBtn } from '@/shared/ui/LikeBtn/LikeBtn'; import { GatheringDetailBtnCon, GatheringDetailHeader, GatheringDetailGrid } from '@/widgets'; @@ -15,11 +16,11 @@ export const GatheringDetailPage = () => { const { mutate: toggleLike, isPending } = useGatheringLike({ gatheringId: gatheringId!, - onSuccess: response => { - console.log('좋아요 성공:', response); + onSuccess: () => { + customToast({ text: '이 게시물에 좋아요를 눌렀습니다.', timer: 3000, icon: 'success' }); }, - onError: error => { - console.error('좋아요 실패:', error); + onError: () => { + errorAlert({ title: '좋아요 실패', text: '좋아요를 누르는데 실패했습니다.' }); }, }); if (isLoading) { @@ -45,13 +46,13 @@ export const GatheringDetailPage = () => { } const gatheringDetail = data?.data; - console.log('gatheringDetail:', gatheringDetail); return (
    - + {
    - {gatheringDetail.likeCounts}
    diff --git a/src/pages/PortfolioListPage/PortfolioListPage.tsx b/src/pages/PortfolioListPage/PortfolioListPage.tsx index 63e03e4..ab18fe1 100644 --- a/src/pages/PortfolioListPage/PortfolioListPage.tsx +++ b/src/pages/PortfolioListPage/PortfolioListPage.tsx @@ -122,7 +122,7 @@ export const PortfolioListPage = () => { /> diff --git a/src/widgets/ContactModal/ui/ContactModal.tsx b/src/widgets/ContactModal/ui/ContactModal.tsx index 170d84f..f2e0b83 100644 --- a/src/widgets/ContactModal/ui/ContactModal.tsx +++ b/src/widgets/ContactModal/ui/ContactModal.tsx @@ -34,7 +34,7 @@ export const ContactModal = ({ isOpen, onClose, username }: ContactModalProps) =
    -

    {username} 님에게 연락하기

    +

    {username}님에게 연락하기

    {chatOptions.map(option => ( diff --git a/src/widgets/GatheringDetail/GatheringDetailBtnCon.tsx b/src/widgets/GatheringDetail/GatheringDetailBtnCon.tsx index d97b8d2..62ee10e 100644 --- a/src/widgets/GatheringDetail/GatheringDetailBtnCon.tsx +++ b/src/widgets/GatheringDetail/GatheringDetailBtnCon.tsx @@ -54,8 +54,14 @@ export const GatheringDetailBtnCon = ({ gatheringId, userId }: GatheringDetailBt navigate(path); }; - const handleEdit = () => { - handleNavigate(`/gathering/edit/${gatheringId}`); + const handleEdit = async () => { + const result = await customConfirm({ + title: '게더링 수정', + text: '게더링을 수정하시겠습니까?', + confirmButtonText: '수정', + cancelButtonText: '취소', + }); + if (result.isConfirmed && gatheringId) handleNavigate(`/gathering/edit/${gatheringId}`); }; const handleDelete = async () => { @@ -66,7 +72,7 @@ export const GatheringDetailBtnCon = ({ gatheringId, userId }: GatheringDetailBt cancelButtonText: '취소', }); - if (result && gatheringId) { + if (result.isConfirmed && gatheringId) { deleteGathering(gatheringId); } }; @@ -83,7 +89,7 @@ export const GatheringDetailBtnCon = ({ gatheringId, userId }: GatheringDetailBt cancelButtonText: '취소', }); - if (result && gatheringId) { + if (result.isConfirmed && gatheringId) { completeGathering(gatheringId); } }; diff --git a/src/widgets/GatheringDetail/GatheringDetailGrid.module.scss b/src/widgets/GatheringDetail/GatheringDetailGrid.module.scss index 8a50b14..86b4074 100644 --- a/src/widgets/GatheringDetail/GatheringDetailGrid.module.scss +++ b/src/widgets/GatheringDetail/GatheringDetailGrid.module.scss @@ -2,6 +2,7 @@ display: grid; grid-template-columns: repeat(2, 3fr); width: 100%; + padding: 0 1.5rem; margin-top: 3rem; @media (width <= 768px) { diff --git a/src/widgets/GatheringDetail/GatheringDetailGrid.tsx b/src/widgets/GatheringDetail/GatheringDetailGrid.tsx index 753f876..19c47c3 100644 --- a/src/widgets/GatheringDetail/GatheringDetailGrid.tsx +++ b/src/widgets/GatheringDetail/GatheringDetailGrid.tsx @@ -13,6 +13,7 @@ interface GatheringDetailGridProps { deadLine: string; positions: string[]; gatheringTag: string[]; + contactUrl: string; } export const GatheringDetailGrid = ({ @@ -26,6 +27,7 @@ export const GatheringDetailGrid = ({ deadLine, positions, gatheringTag, + contactUrl, }: GatheringDetailGridProps) => { return (
      @@ -39,6 +41,7 @@ export const GatheringDetailGrid = ({ +
    ); }; diff --git a/src/widgets/GatheringDetail/GatheringDetailHeader.tsx b/src/widgets/GatheringDetail/GatheringDetailHeader.tsx index 1dce288..7c8f9a5 100644 --- a/src/widgets/GatheringDetail/GatheringDetailHeader.tsx +++ b/src/widgets/GatheringDetail/GatheringDetailHeader.tsx @@ -5,14 +5,15 @@ import { Breadcrumb } from '@/shared/ui'; interface GatheringDetailHeaderProps { title?: string; + username: string; } -export const GatheringDetailHeader = ({ title }: GatheringDetailHeaderProps) => { +export const GatheringDetailHeader = ({ title, username }: GatheringDetailHeaderProps) => { return (

    {title}

    - +
    ); }; diff --git a/src/widgets/GatheringGrid/GatheringGrid.tsx b/src/widgets/GatheringGrid/GatheringGrid.tsx index 2387e4c..ffd047e 100644 --- a/src/widgets/GatheringGrid/GatheringGrid.tsx +++ b/src/widgets/GatheringGrid/GatheringGrid.tsx @@ -15,11 +15,13 @@ export const GatheringGrid = ({ items }: GatheringGridProps) => { ))} diff --git a/src/widgets/Layout/ui/Footer/Footer.tsx b/src/widgets/Layout/ui/Footer/Footer.tsx index aeed41c..fd3b47b 100644 --- a/src/widgets/Layout/ui/Footer/Footer.tsx +++ b/src/widgets/Layout/ui/Footer/Footer.tsx @@ -3,7 +3,7 @@ import styles from './Footer.module.scss'; export const Footer = () => { return (
    - Palettee + Palette

    ⓒ2024. ZeroOne all rights reserved

    ); diff --git a/src/widgets/Layout/ui/Header/Header.tsx b/src/widgets/Layout/ui/Header/Header.tsx index c6a477a..a6705a5 100644 --- a/src/widgets/Layout/ui/Header/Header.tsx +++ b/src/widgets/Layout/ui/Header/Header.tsx @@ -143,7 +143,7 @@ export const Header = () => { - Palettee + Palette {isMobile ? ( <> diff --git a/src/widgets/LikeTab/LikeTab.module.scss b/src/widgets/LikeTab/LikeTab.module.scss index 4e34a25..d8e88ba 100644 --- a/src/widgets/LikeTab/LikeTab.module.scss +++ b/src/widgets/LikeTab/LikeTab.module.scss @@ -44,3 +44,24 @@ } } } + +.portfolioGrid, +.gatheringGrid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 2rem; + width: 100%; + + @media (width <= 768px) { + gap: 1.5rem; + } +} + +.loading { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100px; + margin-top: 1rem; +} diff --git a/src/widgets/LikeTab/LikeTab.tsx b/src/widgets/LikeTab/LikeTab.tsx index 9953183..8cd03c0 100644 --- a/src/widgets/LikeTab/LikeTab.tsx +++ b/src/widgets/LikeTab/LikeTab.tsx @@ -1,26 +1,15 @@ import { ArchiveGrid } from '../ArchiveGrid'; -import { GatheringGrid } from '../GatheringGrid'; import styles from './LikeTab.module.scss'; -import type { GatheringItem } from '@/features'; -import { useLikeArchiveList } from '@/features'; +import { + GatheringCard, + PortfolioCard, + useGatheringLikeList, + useInfinitePortfolioLikeList, + useLikeArchiveList, +} from '@/features'; import { Loader, TripleDot } from '@/shared/ui'; -const dummyGatherings: GatheringItem[] = Array.from({ length: 9 }, (_, i) => ({ - gatheringId: i, - userId: i, - contactType: '온라인', - sort: '스터디', - subject: '개발', - period: '1개월', - personnel: '1', - position: ['개발자'], - title: `Sample Gathering`, - deadLine: '2022-12-31', - username: '홍길동', - tags: ['tag1', 'tag2'], -})); - export const LikeTab = ({ activeTab, setActiveTab, @@ -30,16 +19,46 @@ export const LikeTab = ({ }) => { const tabs = ['포트폴리오', '아카이브', '게더링']; + // 아카이브 좋아요 목록 const { items: likeArchives, isPending: isArchiveLoading, - isFetchingNextPage, - ref, + isFetchingNextPage: isArchiveFetchingNext, + ref: archiveRef, } = useLikeArchiveList(); + // 포트폴리오 좋아요 목록 + const { + portfolios, + isLoading: isPortfolioLoading, + isFetchingNextPage: isPortfolioFetchingNext, + // ref: portfolioRef, + } = useInfinitePortfolioLikeList(); + + // 게더링 좋아요 목록 + const { + gatherings, + isLoading: isGatheringLoading, + isFetchingNextPage: isGatheringFetchingNext, + hasNextPage: hasGatheringNextPage, + fetchNextPage: fetchGatheringNextPage, + } = useGatheringLikeList(); + const renderingLikeTap = (activeTab: string) => { if (activeTab === '포트폴리오') { - // return ; + if (!portfolios || isPortfolioLoading) { + return ; + } + return ( + <> +
    + {portfolios.map(portfolio => ( + + ))} +
    +
    {isPortfolioFetchingNext && }
    + + ); } else if (activeTab === '아카이브') { if (!likeArchives || isArchiveLoading) { return ; @@ -47,13 +66,29 @@ export const LikeTab = ({ return ( <> -
    - {isFetchingNextPage && } +
    + {isArchiveFetchingNext && }
    ); } else if (activeTab === '게더링') { - return ; + if (!gatherings || isGatheringLoading) { + return ; + } + return ( + <> +
    + {gatherings.map(gathering => ( + + ))} +
    + {hasGatheringNextPage && ( +
    fetchGatheringNextPage()}> + {isGatheringFetchingNext && } +
    + )} + + ); } }; diff --git a/src/widgets/MainGatheringGrid/MainGatheringGrid.module.scss b/src/widgets/MainGatheringGrid/MainGatheringGrid.module.scss index e69de29..febb459 100644 --- a/src/widgets/MainGatheringGrid/MainGatheringGrid.module.scss +++ b/src/widgets/MainGatheringGrid/MainGatheringGrid.module.scss @@ -0,0 +1,45 @@ +.grid { + display: grid; + grid-template-rows: 1fr; + grid-template-columns: repeat(4, 1fr); + gap: 1rem; + width: 100%; + overflow: hidden; + + & > :nth-child(5) { + display: none; + } + + @media (width <= 1000px) { + grid-template-columns: repeat(3, 1fr); + gap: 1rem; + + & > :nth-child(5), + & > :nth-child(4) { + display: none; + } + } + + @media (width <= 768px) { + grid-template-columns: repeat(2, 1fr); + gap: 1rem; + + & > :nth-child(5), + & > :nth-child(4), + & > :nth-child(3) { + display: none; + } + } + + @media (width <= 580px) { + grid-template-columns: repeat(1, 1fr); + gap: 1rem; + + & > :nth-child(5), + & > :nth-child(4), + & > :nth-child(3), + & > :nth-child(2) { + display: none; + } + } +} diff --git a/src/widgets/MainGatheringGrid/MainGatheringGrid.tsx b/src/widgets/MainGatheringGrid/MainGatheringGrid.tsx index e69de29..55e2425 100644 --- a/src/widgets/MainGatheringGrid/MainGatheringGrid.tsx +++ b/src/widgets/MainGatheringGrid/MainGatheringGrid.tsx @@ -0,0 +1,36 @@ +import styles from './MainGatheringGrid.module.scss'; + +import { GatheringCard, useMainGathering } from '@/features'; +import { Loader, TripleDot } from '@/shared/ui'; + +export const MainGatheringGrid = () => { + const { items, isLoading, isError } = useMainGathering(); + + if (isLoading) return ; + if (isError) { + return ( +
    + + 에러가 발생했습니다. +
    + ); + } + + // 데이터가 없는 경우 + if (!items || items.length === 0) { + return ( +
    + + 검색된 내용이 없습니다. +
    + ); + } + + return ( +
    + {items.map(gathering => ( + + ))} +
    + ); +}; diff --git a/src/widgets/MainGridItem/MainGridItem.tsx b/src/widgets/MainGridItem/MainGridItem.tsx index 574a756..a152014 100644 --- a/src/widgets/MainGridItem/MainGridItem.tsx +++ b/src/widgets/MainGridItem/MainGridItem.tsx @@ -6,7 +6,7 @@ import { useNavigate } from 'react-router-dom'; import styles from './MainGridItem.module.scss'; import { ArchiveCard, usePopularArchive } from '@/features'; -import { MainPortfolioGrid } from '@/widgets'; +import { MainPortfolioGrid, MainGatheringGrid } from '@/widgets'; export const MainGridItem = ({ type }: { type: string }) => { const navigate = useNavigate(); @@ -68,7 +68,7 @@ export const MainGridItem = ({ type }: { type: string }) => { 현재 모집 중인 게더링 -
    +
    ); diff --git a/src/widgets/MainPortfolioGrid/MainPortfolioGrid.tsx b/src/widgets/MainPortfolioGrid/MainPortfolioGrid.tsx index 4def1b8..4e92e18 100644 --- a/src/widgets/MainPortfolioGrid/MainPortfolioGrid.tsx +++ b/src/widgets/MainPortfolioGrid/MainPortfolioGrid.tsx @@ -5,7 +5,7 @@ import { Loader, TripleDot } from '@/shared/ui'; export const MainPortfolioGrid = () => { const { data, isLoading, isError } = useMainPortfolioList(); - console.log(data); + // console.log(data); if (isLoading) return ; if (isError) diff --git a/src/widgets/RegisterUser/ProfileStep.tsx b/src/widgets/RegisterUser/ProfileStep.tsx index 33d60e8..63bcd9a 100644 --- a/src/widgets/RegisterUser/ProfileStep.tsx +++ b/src/widgets/RegisterUser/ProfileStep.tsx @@ -58,7 +58,7 @@ export const ProfileStep = ({ setStage }: ProfileStepProps) => { socials: data.url.map(link => link.value), s3StoredImageUrls: [], }; - console.log(postUserData); + // console.log(postUserData); createUser( { data: postUserData, diff --git a/src/widgets/SettingUser/SetProfile.tsx b/src/widgets/SettingUser/SetProfile.tsx index 7da13ac..2c6d153 100644 --- a/src/widgets/SettingUser/SetProfile.tsx +++ b/src/widgets/SettingUser/SetProfile.tsx @@ -72,7 +72,7 @@ export const SetProfile = ({ userData }: SetProfileProps) => { s3StoredImageUrls: [], }; - console.log(putUserData); + // console.log(putUserData); editUser( { diff --git a/src/widgets/index.ts b/src/widgets/index.ts index e235c74..d4175c4 100644 --- a/src/widgets/index.ts +++ b/src/widgets/index.ts @@ -10,6 +10,7 @@ export * from './LikeTab/LikeTab'; export * from './LoginModal/ui/LoginModal'; export * from './MainBanner/MainBanner'; export * from './MainContents/MainContents'; +export * from './MainGatheringGrid/MainGatheringGrid'; export * from './MainPortfolioGrid/MainPortfolioGrid'; export * from './MenuModal/MenuModal'; export * from './NoticeContainer/NoticeContainer'; @@ -19,3 +20,4 @@ export * from './UserContents/UserContents'; export * from './WriteArchive'; export * from './WriteGathering/WriteGatheringDetail'; export * from './WriteGathering/WriteGatheringOpts'; +