diff --git a/apps/client/src/pages/level/Level.tsx b/apps/client/src/pages/level/Level.tsx index 1985bbf8..e33a9628 100644 --- a/apps/client/src/pages/level/Level.tsx +++ b/apps/client/src/pages/level/Level.tsx @@ -1,12 +1,12 @@ -import { Icon } from '@pinback/design-system/icons'; -import { cn } from '@pinback/design-system/utils'; import LevelScene from '@pages/level/components/LevelScene'; -import { getTreeLevel } from '@shared/utils/treeLevel'; import { TreeLevel } from '@pages/level/types/treeLevelType'; +import { Icon } from '@pinback/design-system/icons'; import { Badge } from '@pinback/design-system/ui'; -import { useGetArcons } from '@shared/apis/queries'; -import { lazy, Suspense } from 'react'; +import { cn } from '@pinback/design-system/utils'; +import { useGetAcorns } from '@shared/apis/queries'; import { Balloon } from '@shared/components/balloon/Balloon'; +import { getTreeLevel } from '@shared/utils/treeLevel'; +import { lazy, Suspense } from 'react'; const LevelInfoCard = lazy( () => import('@pages/level/components/LevelInfoCard') @@ -14,7 +14,7 @@ const LevelInfoCard = lazy( const NextAcornTime = lazy(() => import('./components/NextAcornTime')); export default function Level() { - const { data, isPending, isError } = useGetArcons(); + const { data, isPending, isError } = useGetAcorns(); if (isPending) return
; if (isError) return
; diff --git a/apps/client/src/pages/myBookmark/MyBookmark.tsx b/apps/client/src/pages/myBookmark/MyBookmark.tsx index b92d1b92..f32efbc6 100644 --- a/apps/client/src/pages/myBookmark/MyBookmark.tsx +++ b/apps/client/src/pages/myBookmark/MyBookmark.tsx @@ -1,23 +1,23 @@ -import { PopupContainer } from '@pinback/design-system/ui'; -import { useState, useRef, Suspense } from 'react'; -import { useSearchParams } from 'react-router-dom'; +import MyBookmarkContent from '@pages/myBookmark/components/myBookmarkContent/MyBookmarkContent'; import { REMIND_MOCK_DATA } from '@pages/remind/constants'; -import CardEditModal from '@shared/components/cardEditModal/CardEditModal'; -import OptionsMenuPortal from '@shared/components/sidebar/OptionsMenuPortal'; -import { useAnchoredMenu } from '@shared/hooks/useAnchoredMenu'; -import { belowOf } from '@shared/utils/anchorPosition'; import { Icon } from '@pinback/design-system/icons'; -import { useQueryClient } from '@tanstack/react-query'; +import { PopupContainer } from '@pinback/design-system/ui'; import { - useGetArticleDetail, useDeleteRemindArticle, + useGetArticleDetail, usePutArticleReadStatus, } from '@shared/apis/queries'; -import TooltipCard from '@shared/components/tooltipCard/TooltipCard'; -import ArticlesLoadingBoundary from '@shared/components/articlesLoadingBoundary/ArticlesLoadingBoundary'; import ArticlesErrorBoundary from '@shared/components/articlesErrorBoundary/ArticlesErrorBoundary'; +import ArticlesLoadingBoundary from '@shared/components/articlesLoadingBoundary/ArticlesLoadingBoundary'; +import CardEditModal from '@shared/components/cardEditModal/CardEditModal'; +import OptionsMenuPortal from '@shared/components/sidebar/OptionsMenuPortal'; +import TooltipCard from '@shared/components/tooltipCard/TooltipCard'; +import { useAnchoredMenu } from '@shared/hooks/useAnchoredMenu'; +import { belowOf } from '@shared/utils/anchorPosition'; +import { useQueryClient } from '@tanstack/react-query'; +import { Suspense, useRef, useState } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; -import MyBookmarkContent from '@pages/myBookmark/components/myBookmarkContent/MyBookmarkContent'; +import { useSearchParams } from 'react-router-dom'; import Footer from './components/footer/Footer'; const MyBookmark = () => { @@ -58,7 +58,7 @@ const MyBookmark = () => { queryClient.invalidateQueries({ queryKey: ['categoryBookmarkArticles'], }); - queryClient.invalidateQueries({ queryKey: ['arcons'] }); + queryClient.invalidateQueries({ queryKey: ['acorns'] }); setIsDeleteOpen(false); setDeleteTargetId(null); closeMenu(); @@ -163,7 +163,7 @@ const MyBookmark = () => { />
setIsEditOpen(false)} prevData={articleDetail} /> diff --git a/apps/client/src/pages/myBookmark/components/myBookmarkContent/MyBookmarkContent.tsx b/apps/client/src/pages/myBookmark/components/myBookmarkContent/MyBookmarkContent.tsx index 64c752da..6725cc17 100644 --- a/apps/client/src/pages/myBookmark/components/myBookmarkContent/MyBookmarkContent.tsx +++ b/apps/client/src/pages/myBookmark/components/myBookmarkContent/MyBookmarkContent.tsx @@ -1,8 +1,8 @@ -import { useMyBookmarkContentData } from '@pages/myBookmark/hooks/useMyBookmarkContentData'; import NoArticles from '@pages/myBookmark/components/NoArticles/NoArticles'; import NoUnreadArticles from '@pages/myBookmark/components/noUnreadArticles/NoUnreadArticles'; -import { MutableRefObject } from 'react'; +import { useMyBookmarkContentData } from '@pages/myBookmark/hooks/useMyBookmarkContentData'; import { Badge, Card } from '@pinback/design-system/ui'; +import { MutableRefObject } from 'react'; interface MyBookmarkContentProps { activeBadge: 'all' | 'notRead'; @@ -25,12 +25,7 @@ const MyBookmarkContent = ({ queryClient, scrollContainerRef, }: MyBookmarkContentProps) => { - const { - view, - list, - counts, - pagination, - } = useMyBookmarkContentData({ + const { view, list, counts, pagination } = useMyBookmarkContentData({ activeBadge, category, categoryId, @@ -82,7 +77,9 @@ const MyBookmarkContent = ({ : article.category?.categoryName } categoryColor={ - view.isCategoryView ? undefined : article.category?.categoryColor + view.isCategoryView + ? undefined + : article.category?.categoryColor } date={new Date(article.createdAt).toLocaleDateString('ko-KR')} onClick={() => { @@ -101,7 +98,7 @@ const MyBookmarkContent = ({ queryClient.invalidateQueries({ queryKey: ['categoryBookmarkArticles'], }); - queryClient.invalidateQueries({ queryKey: ['arcons'] }); + queryClient.invalidateQueries({ queryKey: ['acorns'] }); }, onError: (error: any) => { console.error(error); diff --git a/apps/client/src/pages/remind/Remind.tsx b/apps/client/src/pages/remind/Remind.tsx index 73880f62..e14703c5 100644 --- a/apps/client/src/pages/remind/Remind.tsx +++ b/apps/client/src/pages/remind/Remind.tsx @@ -1,6 +1,9 @@ -import { useMemo, useRef, useState, useEffect } from 'react'; +import { useGetRemindArticles } from '@pages/remind/apis/queries'; +import NoReadArticles from '@pages/remind/components/noReadArticles/NoReadArticles'; +import NoUnreadArticles from '@pages/remind/components/noUnreadArticles/NoUnreadArticles'; import { Badge, + Card, PopupContainer, trackPageView, } from '@pinback/design-system/ui'; @@ -8,29 +11,26 @@ import CardEditModal from '@shared/components/cardEditModal/CardEditModal'; import OptionsMenuPortal from '@shared/components/sidebar/OptionsMenuPortal'; import { useAnchoredMenu } from '@shared/hooks/useAnchoredMenu'; import { belowOf } from '@shared/utils/anchorPosition'; -import { REMIND_MOCK_DATA } from './constants'; -import { useGetRemindArticles } from '@pages/remind/apis/queries'; import { formatLocalDateTime } from '@shared/utils/formatDateTime'; -import NoReadArticles from '@pages/remind/components/noReadArticles/NoReadArticles'; -import NoUnreadArticles from '@pages/remind/components/noUnreadArticles/NoUnreadArticles'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import { REMIND_MOCK_DATA } from './constants'; + import { - usePutArticleReadStatus, useDeleteRemindArticle, useGetArticleDetail, + usePutArticleReadStatus, } from '@shared/apis/queries'; -import { useQueryClient } from '@tanstack/react-query'; -import NoRemindArticles from './components/noRemindArticles/NoRemindArticles'; -import FetchCard from './components/fetchCard/FetchCard'; -import { useInfiniteScroll } from '@shared/hooks/useInfiniteScroll'; +import JobSelectionFunnel from '@shared/components/jobSelectionFunnel/JobSelectionFunnel'; import TooltipCard from '@shared/components/tooltipCard/TooltipCard'; +import { useInfiniteScroll } from '@shared/hooks/useInfiniteScroll'; +import { useQueryClient } from '@tanstack/react-query'; import Footer from './components/footer/Footer'; -import JobSelectionFunnel from '@shared/components/jobSelectionFunnel/JobSelectionFunnel'; +import NoRemindArticles from './components/noRemindArticles/NoRemindArticles'; const Remind = () => { useEffect(() => { trackPageView('대시보드 페이지 방문'); }, []); - const [isEditOpen, setIsEditOpen] = useState(false); const [activeBadge, setActiveBadge] = useState<'read' | 'notRead'>('notRead'); const [isDeleteOpen, setIsDeleteOpen] = useState(false); @@ -75,9 +75,7 @@ const Remind = () => { .filter((article) => { const now = new Date().getTime(); const remindTime = new Date(article.remindAt).getTime(); - // 만료 시간 = 리마인드 시간 + 24시간 const expirationTime = remindTime + 24 * 60 * 60 * 1000; - return now >= remindTime && now < expirationTime; }) ?? []; @@ -87,13 +85,17 @@ const Remind = () => { const handleDeleteArticle = (id: number) => { deleteArticle(id, { onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['remindArticles'] }); - queryClient.invalidateQueries({ queryKey: ['arcons'] }); + queryClient.invalidateQueries({ + queryKey: ['remindArticles'], + }); + queryClient.invalidateQueries({ + queryKey: ['acorns'], + }); setIsDeleteOpen(false); setDeleteTargetId(null); closeMenu(); - close(); }, + onError: (error) => { console.error('아티클 삭제 실패:', error); }, @@ -106,6 +108,7 @@ const Remind = () => { const EmptyStateComponent = () => { const firstPageData = data?.pages[0]; + if ( firstPageData?.readArticleCount === 0 && firstPageData?.unreadArticleCount === 0 @@ -116,32 +119,28 @@ const Remind = () => { return activeBadge === 'read' ? : ; }; - // TODO: 로딩 상태 디자인 필요 if (isPending) { return
Loading...
; } - // TODO: 임시 - // const unreadArticleCount = data?.pages[0]?.unreadArticleCount || 0; - // const readArticleCount = data?.pages[0]?.readArticleCount || 0; - return (

리마인드

+
handleBadgeClick('notRead')} isActive={activeBadge === 'notRead'} /> + handleBadgeClick('read')} isActive={activeBadge === 'read'} />
+ {articlesToDisplay.length > 0 ? ( @@ -149,34 +148,43 @@ const Remind = () => { ref={scrollContainerRef} className="scrollbar-hide mt-[2.6rem] flex flex-wrap gap-[1.6rem] overflow-y-auto scroll-smooth" > - {articlesToDisplay.map((article) => ( - { - window.open(article.url, '_blank'); + {articlesToDisplay.map((article) => { + const displayTitle = article.title?.trim() + ? article.title + : '제목 없음'; - updateToReadStatus(article.articleId, { - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: ['remindArticles'], - }); - queryClient.invalidateQueries({ - queryKey: ['arcons'], - }); - }, - onError: (error) => { - console.error(error); - }, - }); - }} - onOptionsClick={(e) => { - e.stopPropagation(); - openMenu(article.articleId, e.currentTarget); - }} - /> - ))} -
+ const displayImageUrl = article.thumbnailUrl || undefined; + + return ( + { + window.open(article.url, '_blank'); + updateToReadStatus(article.articleId); + }} + onOptionsClick={(e) => { + e.stopPropagation(); + + openMenu(article.articleId, e.currentTarget); + }} + /> + ); + })} + +
) : ( @@ -239,7 +247,7 @@ const Remind = () => { />
setIsEditOpen(false)} prevData={articleDetail} /> @@ -251,7 +259,6 @@ const Remind = () => {
{ - // TODO: 관심 직무 핀 API 연동 필요 setShowJobSelectionFunnel(false); }} /> diff --git a/apps/client/src/pages/remind/apis/axios.ts b/apps/client/src/pages/remind/apis/axios.ts index ad899020..5efd68a3 100644 --- a/apps/client/src/pages/remind/apis/axios.ts +++ b/apps/client/src/pages/remind/apis/axios.ts @@ -6,7 +6,7 @@ export const getRemindArticles = async ( page: number, size: number ) => { - const { data } = await apiRequest.get(`/api/v2/articles/remind`, { + const { data } = await apiRequest.get(`/api/v3/articles/remind`, { params: { now: nowDate, 'read-status': readStatus, diff --git a/apps/client/src/pages/remind/components/fetchCard/FetchCard.tsx b/apps/client/src/pages/remind/components/fetchCard/FetchCard.tsx deleted file mode 100644 index 5b7f0fb8..00000000 --- a/apps/client/src/pages/remind/components/fetchCard/FetchCard.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { ArticleWithCategory } from '@pages/remind/types/api'; -import { Card } from '@pinback/design-system/ui'; -import { useGetPageMeta } from '@shared/apis/queries'; -import React from 'react'; - -interface FetchCardProps { - article: ArticleWithCategory; - onClick: () => void; - onOptionsClick?: (e: React.MouseEvent) => void; -} - -const FetchCard = ({ article, onClick, onOptionsClick }: FetchCardProps) => { - const { data: meta, isPending, isError: error } = useGetPageMeta(article.url); - - if (isPending) { - return ( -
- ); - } - - const displayTitle = !error && meta.title ? meta.title : '제목 없음'; - const displayImageUrl = !error ? meta.image : undefined; - - return ( - - ); -}; - -export default FetchCard; diff --git a/apps/client/src/pages/remind/types/api.ts b/apps/client/src/pages/remind/types/api.ts index 20d93911..eee763fa 100644 --- a/apps/client/src/pages/remind/types/api.ts +++ b/apps/client/src/pages/remind/types/api.ts @@ -1,4 +1,4 @@ -// 리마인드 전체 조회 +// 리마인드 전체 조회 (v3) interface Category { categoryId: number; categoryName: string; @@ -8,14 +8,19 @@ interface Category { export interface ArticleWithCategory { articleId: number; url: string; + title: string; + thumbnailUrl: string; memo: string; createdAt: string; isRead: boolean; + isReadAfterRemind: boolean; remindAt: string; category: Category; } export interface ArticleListResponse { + hasNext: boolean; + totalArticleCount: number; readArticleCount: number; unreadArticleCount: number; articles: ArticleWithCategory[]; diff --git a/apps/client/src/shared/apis/axios.ts b/apps/client/src/shared/apis/axios.ts index ee4edaca..efec01f4 100644 --- a/apps/client/src/shared/apis/axios.ts +++ b/apps/client/src/shared/apis/axios.ts @@ -48,8 +48,9 @@ export const putArticleReadStatus = async (articleId: number) => { return data; }; +// 이거 업데이트 export const getArticleDetail = async (articleId: number) => { - const { data } = await apiRequest.get(`/api/v1/articles/${articleId}`); + const { data } = await apiRequest.get(`/api/v3/articles/${articleId}`); return data.data; }; diff --git a/apps/client/src/shared/apis/queries.ts b/apps/client/src/shared/apis/queries.ts index 35a9fd9a..47481dfa 100644 --- a/apps/client/src/shared/apis/queries.ts +++ b/apps/client/src/shared/apis/queries.ts @@ -1,38 +1,39 @@ -import { - useMutation, - UseMutationResult, - useQuery, - UseQueryResult, - useSuspenseQuery, -} from '@tanstack/react-query'; import { deleteCategory, - getDashboardCategories, - postCategory, - postSignUp, - postSignUpRequest, - putEditArticle, - putCategory, - putArticleReadStatus, - getArticleDetail, - getAcorns, deleteRemindArticle, + getAcorns, + getArticleDetail, + getDashboardCategories, getGoogleProfile, - getMyProfile, getJobs, + getMyProfile, patchUserJob, patchUserJobRequest, + postCategory, + postSignUp, + postSignUpRequest, + putArticleReadStatus, + putCategory, + putEditArticle, } from '@shared/apis/axios'; -import { AxiosError } from 'axios'; import { - DashboardCategoriesResponse, AcornsResponse, - EditArticleRequest, - ArticleReadStatusResponse, ArticleDetailResponse, + ArticleReadStatusResponse, + DashboardCategoriesResponse, + EditArticleRequest, JobsResponse, } from '@shared/types/api'; import { fetchOGData } from '@shared/utils/fetchOgData'; +import { + useMutation, + UseMutationResult, + useQuery, + useQueryClient, + UseQueryResult, + useSuspenseQuery, +} from '@tanstack/react-query'; +import { AxiosError } from 'axios'; export const useGetDashboardCategories = (): UseQueryResult< DashboardCategoriesResponse, @@ -62,9 +63,9 @@ export const useDeleteCategory = () => { }); }; -export const useGetArcons = (): UseQueryResult => { +export const useGetAcorns = (): UseQueryResult => { return useQuery({ - queryKey: ['arcons'], + queryKey: ['acorns'], queryFn: () => getAcorns(), }); }; @@ -100,8 +101,31 @@ export const usePutArticleReadStatus = (): UseMutationResult< AxiosError, number > => { + const queryClient = useQueryClient(); + return useMutation({ mutationFn: (articleId: number) => putArticleReadStatus(articleId), + + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: ['remindArticles'], + }); + queryClient.invalidateQueries({ + queryKey: ['acorns'], + }); + queryClient.invalidateQueries({ + queryKey: ['bookmarkReadArticles'], + }); + queryClient.invalidateQueries({ + queryKey: ['bookmarkUnreadArticles'], + }); + queryClient.invalidateQueries({ + queryKey: ['categoryBookmarkArticles'], + }); + }, + onError: (error) => { + console.error('읽음 처리 실패:', error); + }, }); }; diff --git a/apps/client/src/shared/components/cardEditModal/CardEditModal.tsx b/apps/client/src/shared/components/cardEditModal/CardEditModal.tsx index 0013428f..495794ce 100644 --- a/apps/client/src/shared/components/cardEditModal/CardEditModal.tsx +++ b/apps/client/src/shared/components/cardEditModal/CardEditModal.tsx @@ -1,3 +1,4 @@ +import noImage from '@assets/client_thumb.svg'; import { Icon } from '@pinback/design-system/icons'; import { AutoDismissToast, @@ -17,13 +18,11 @@ import { useGetDashboardCategories, usePutEditArticle, } from '@shared/apis/queries'; -import { usePageMeta } from '@shared/hooks/usePageMeta'; import { ArticleDetailResponse, EditArticleRequest } from '@shared/types/api'; import { combineDateTime } from '@shared/utils/datetime'; import { updateDate, updateTime } from '@shared/utils/formatDateTime'; import { useQueryClient } from '@tanstack/react-query'; import { useEffect, useState } from 'react'; -import noImage from '@assets/client_thumb.svg'; export interface CardEditModalProps { onClose: () => void; @@ -34,7 +33,6 @@ export default function CardEditModal({ onClose, prevData, }: CardEditModalProps) { - const { meta, loading } = usePageMeta(prevData.url); const { data: category } = useGetDashboardCategories(); const { mutate: editArticle } = usePutEditArticle(); const queryClient = useQueryClient(); @@ -86,7 +84,7 @@ export default function CardEditModal({ } const saveData = () => { - if (!prevData?.id) { + if (!prevData?.articleId) { console.error('Article ID is missing, cannot save.'); setToastIsOpen(true); return; @@ -106,7 +104,7 @@ export default function CardEditModal({ editArticle( { - articleId: prevData?.id, + articleId: prevData?.articleId, editArticleData, }, { @@ -188,15 +186,11 @@ export default function CardEditModal({ - {loading ? ( -
- ) : ( - - )} +

카테고리

diff --git a/apps/client/src/shared/components/sidebar/Sidebar.tsx b/apps/client/src/shared/components/sidebar/Sidebar.tsx index fa36a62b..70de0def 100644 --- a/apps/client/src/shared/components/sidebar/Sidebar.tsx +++ b/apps/client/src/shared/components/sidebar/Sidebar.tsx @@ -1,29 +1,29 @@ -import Lottie from 'lottie-react'; import Chippiface from '@assets/5_chippiface.json'; import { Icon } from '@pinback/design-system/icons'; -import SideItem from './SideItem'; -import AccordionItem from './AccordionItem'; -import CategoryItem from './CategoryItem'; -import CreateItem from './CreateItem'; -import MyLevelItem from './MyLevelItem'; -import { useAnchoredMenu } from '@shared/hooks/useAnchoredMenu'; -import { rightOf } from '@shared/utils/anchorPosition'; -import { useSidebarNav } from '@shared/hooks/useSidebarNav'; -import { useCategoryPopups } from '@shared/hooks/useCategoryPopups'; -import OptionsMenuPortal from './OptionsMenuPortal'; -import PopupPortal from './PopupPortal'; -import ProfilePopupPortal from '../profilePopup/ProfilePopupPortal'; +import { AutoDismissToast } from '@pinback/design-system/ui'; import { + useGetAcorns, useGetDashboardCategories, - useGetArcons, useGetGoogleProfile, useGetMyProfile, } from '@shared/apis/queries'; -import { useEffect, useRef, useState } from 'react'; -import { AutoDismissToast } from '@pinback/design-system/ui'; import { Balloon } from '@shared/components/balloon/Balloon'; +import { useAnchoredMenu } from '@shared/hooks/useAnchoredMenu'; +import { useCategoryPopups } from '@shared/hooks/useCategoryPopups'; +import { useSidebarNav } from '@shared/hooks/useSidebarNav'; +import { rightOf } from '@shared/utils/anchorPosition'; +import Lottie from 'lottie-react'; +import { useEffect, useRef, useState } from 'react'; +import ProfilePopupPortal from '../profilePopup/ProfilePopupPortal'; +import AccordionItem from './AccordionItem'; +import CategoryItem from './CategoryItem'; +import CreateItem from './CreateItem'; import { useCategoryActions } from './hooks/useCategoryActions'; import JobPinGuidePortal from './JobPinGuidePortal'; +import MyLevelItem from './MyLevelItem'; +import OptionsMenuPortal from './OptionsMenuPortal'; +import PopupPortal from './PopupPortal'; +import SideItem from './SideItem'; export function Sidebar() { const [profileOpen, setProfileOpen] = useState(false); @@ -35,7 +35,7 @@ export function Sidebar() { const [guideOpen, setGuideOpen] = useState(false); const { data: categories } = useGetDashboardCategories(); - const { data, isPending: isAcornPending } = useGetArcons(); + const { data, isPending: isAcornPending } = useGetAcorns(); const { data: googleProfileData } = useGetGoogleProfile(); const { data: myProfile } = useGetMyProfile(); diff --git a/apps/client/src/shared/types/api.ts b/apps/client/src/shared/types/api.ts index e95fd1a8..340b7add 100644 --- a/apps/client/src/shared/types/api.ts +++ b/apps/client/src/shared/types/api.ts @@ -32,10 +32,12 @@ interface CategoryResponse { } export interface ArticleDetailResponse { - id: number; + articleId: number; url: string; - memo: string; - remindAt: string; + title: string; + thumbnailUrl: string; + memo: string | null; + remindAt: string | null; createdAt: string; categoryResponse: CategoryResponse; }