Skip to content

Commit

Permalink
fix : useScrapToggle에 낙관적 업데이트를 위해 queryKey를 넘기는 방식으로 수정 (#309)
Browse files Browse the repository at this point in the history
* fix: useScrapToggle 함수에 target 대신 queryKey 자체를 넘기도록 수정

* fix: PreviewScrapButton 컴포넌트에서 경로로부터 쿼리키를 만드는 generateQueryKey 함수 구현

* fix: 학습 페이지에서 콘텐츠 목록 불러올 때 로딩중이면 스피너 보여줌

* fix: 포인트 필요한 리스닝 콘텐츠 외부 스크랩 불가능하도록 수정

* fix: 깃 충돌 해결
  • Loading branch information
smosco authored Nov 25, 2024
1 parent 9a409c1 commit 7555f4e
Show file tree
Hide file tree
Showing 11 changed files with 158 additions and 159 deletions.
10 changes: 0 additions & 10 deletions src/api/hooks/usePreview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ export const usePaginatedReadingPreview = (
sort?: string,
direction?: string,
categoryId?: number | undefined,
initialData?: ContentsResponse,
): UseQueryResult<ContentsResponse> => {
return useQuery({
queryKey: [
Expand All @@ -44,13 +43,8 @@ export const usePaginatedReadingPreview = (
direction,
categoryId,
].filter((value) => value !== undefined),
// TODO(@smosco): 외부 스크랩 할 때 setQueryData 또는 invalidate 하기 위해 정확한 queryKey가 필요함
// null 값으로 오면 queryKey가 이상하므로 임시 방편으로 filter를 적용함
// 다만 나중에 sort, direction, categoryId가 들어오는 경우 순서가 유지 되지 않으므로 문제 발생
// 따라서 어디서든지 간에 default 값을 넘길 필요가 있음
queryFn: () =>
fetchPaginatedReadingPreview(page, size, sort, direction, categoryId),
initialData, // 서버에서 받은 데이터를 초기값으로 사용
});
};

Expand All @@ -70,10 +64,6 @@ export const usePaginatedListeningPreview = (
direction,
categoryId,
].filter((value) => value !== undefined),
// TODO(@smosco): 외부 스크랩 할 때 setQueryData 또는 invalidate 하기 위해 정확한 queryKey가 필요함
// null 값으로 오면 queryKey가 이상하므로 임시 방편으로 filter를 적용함
// 다만 나중에 sort, direction, categoryId가 들어오는 경우 순서가 유지 되지 않으므로 문제 발생
// 따라서 어디서든지 간에 default 값을 넘길 필요가 있음
queryFn: () =>
fetchPaginatedListeningPreview(page, size, sort, direction, categoryId),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export default function ListeningDetailClient({

const { toggleScrap } = useScrapToggle({
contentId,
target: 'contentDetail',
queryKey: ['contentDetail', contentId],
});

const { data: isLoginData } = useUserLoginStatus();
Expand Down
19 changes: 13 additions & 6 deletions src/app/(default)/learn/listening/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,23 @@

import { usePaginatedListeningPreview } from '@/api/hooks/usePreview';
import ContentTypeFilter from '@/components/common/ContentTypeFilter';
import LoadingSpinner from '@/components/common/LoadingSpinner';
import Pagination from '@/components/common/Pagination';
import ItemComponent from '@/components/ItemComponentCard';
import { useSetSearchParams } from '@/hooks/useSetSearchParams';

function ListeningPage() {
const { path, searchParams, setSearchParams } = useSetSearchParams();

const currentPage = Number(searchParams.get('page'));
// 기본값 지정해줘야만 null, undefined가 queryparams로 들어가지 않음
const currentPage = Number(searchParams.get('page')) || 1;
const size = Number(searchParams.get('size')) || 10;
const sort = searchParams.get('sort') || 'createdAt';
const direction = searchParams.get('direction') || 'DESC';
const categoryId = Number(searchParams.get('categoryId')) || undefined;

const {
data: listeningContents,
isLoading,
isError,
error,
} = usePaginatedListeningPreview(
Expand All @@ -29,19 +30,25 @@ function ListeningPage() {
);

if (isError) {
return <p className="text-red-500">에러가 발생했습니다: {error.message}</p>;
return (
<p className="text-red-500">
리스닝 콘텐츠 목록을 불러오지 못했어요: {error.message}
</p>
);
}

// 페이지네이션 버튼 누를때마다 페이지 이동

const handlePageChange = (page: number) => {
setSearchParams({ path, params: { page: String(page) } });
};

return (
<main>
<ContentTypeFilter />
{!listeningContents || listeningContents.data.contents.length === 0 ? (

{isLoading && <LoadingSpinner />}

{!isLoading &&
(!listeningContents || listeningContents.data.contents.length === 0) ? (
<div className="flex justify-center items-center mt-8">
콘텐츠가 없습니다
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export default function ReadingDetailClient({
useUpdateLearningProgressOnUnmount(contentId, scrollProgress);
const { toggleScrap } = useScrapToggle({
contentId,
target: 'contentDetail',
queryKey: ['contentDetail', contentId],
});

const { data: isLoginData } = useUserLoginStatus();
Expand Down
16 changes: 11 additions & 5 deletions src/app/(default)/learn/reading/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

import { usePaginatedReadingPreview } from '@/api/hooks/usePreview';
import ContentTypeFilter from '@/components/common/ContentTypeFilter';
import LoadingSpinner from '@/components/common/LoadingSpinner';
import Pagination from '@/components/common/Pagination';
import ItemComponentList from '@/components/ItemComponentList';
import { useSetSearchParams } from '@/hooks/useSetSearchParams';
// import { formatDate } from '@/lib/formatDate';

export default function ReadingPage() {
const { path, searchParams, setSearchParams } = useSetSearchParams();
Expand All @@ -18,6 +18,7 @@ export default function ReadingPage() {

const {
data: readingContents,
isLoading,
isError,
error,
} = usePaginatedReadingPreview(
Expand All @@ -29,7 +30,11 @@ export default function ReadingPage() {
);

if (isError) {
return <p className="text-red-500">에러가 발생했습니다: {error.message}</p>;
return (
<p className="text-red-500">
리딩 콘텐츠 목록을 불러오지 못했어요: {error.message}
</p>
);
}

const handlePageChange = (page: number) => {
Expand All @@ -39,9 +44,11 @@ export default function ReadingPage() {
return (
<main>
<ContentTypeFilter />
{/* TODO(@godhyzzang) : loading중인데도 컨텐트가 없습니다 잠깐 뜨는 경우 있음 */}

{!readingContents || readingContents.data.contents.length === 0 ? (
{isLoading && <LoadingSpinner />}

{!isLoading &&
(!readingContents || readingContents.data.contents.length === 0) ? (
<div className="flex justify-center items-center mt-8">
콘텐츠가 없습니다
</div>
Expand All @@ -52,7 +59,6 @@ export default function ReadingPage() {
<ItemComponentList data={content} key={content.contentId} />
))}
</ul>
{/* TODO(@godhyzzang): 페이지네이션도 url state적용되게 해야함 */}
<Pagination
totalPages={readingContents?.data.totalPages ?? 0}
onPageChange={handlePageChange}
Expand Down
7 changes: 2 additions & 5 deletions src/components/ItemComponentCard.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable no-nested-ternary */
/* eslint-disable @typescript-eslint/no-explicit-any */

'use client';
Expand All @@ -23,11 +24,7 @@ export default function ItemComponentCard({ data }: { data: any }) {
<PreviewScrapButton
contentId={data.contentId}
isScrappedData={data.isScrapped}
target={
data.contentType === 'READING'
? 'readingPreview'
: 'listeningPreview'
}
contentType={data.contentType}
/>
}
bottomLeftButton={
Expand Down
6 changes: 0 additions & 6 deletions src/components/ItemComponentList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,6 @@ export default function ItemComponentList({ data }: { data: any }) {
<PreviewScrapButton
contentId={data.contentId}
isScrappedData={data.isScrapped}
target={
data.contentType === 'READING'
? 'readingPreview'
: 'listeningPreview'
}
/>
}
bottomRightButton={
Expand All @@ -34,7 +29,6 @@ export default function ItemComponentList({ data }: { data: any }) {
leftBadge={
<div className="flex gap-1">
<Badge>{data.category}</Badge>

{data.contentType === 'READING' && (
<div className="flex justify-center items-center bg-gradient-to-l from-blue-500 to-sky-500 rounded-sm shadow-sm ">
<BookOpen className="p-1 h-6 w-6 text-white" />
Expand Down
64 changes: 53 additions & 11 deletions src/components/PreviewScrapButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable no-nested-ternary */
import { useState } from 'react';

import { useSearchParams } from 'next/navigation';
import { useSearchParams, usePathname } from 'next/navigation';

import { Bookmark } from 'lucide-react';

Expand All @@ -13,30 +14,71 @@ import Modal from './common/Modal';
interface PreviewScrapButtonProps {
contentId: number;
isScrappedData: boolean;
target:
| 'readingPreview'
| 'listeningPreview'
| 'contentDetail'
| 'paginatedReadingPreview'
| 'paginatedListeningPreview';
contentType?: 'READING' | 'LISTENING';
}

export default function PreviewScrapButton({
contentId,
isScrappedData,
target,
contentType,
}: PreviewScrapButtonProps) {
const { data: isLoginData } = useUserLoginStatus();
const isLogin = isLoginData?.data;
const [showLoginModal, setShowLoginModal] = useState(false);

const searchParams = useSearchParams();
const page = Number(searchParams.get('page'));
const pathname = usePathname();

const generateQueryKey = () => {
// 기본값 설정
const defaultValues = {
page: 1,
size: 10,
sort: 'createdAt',
direction: 'DESC',
categoryId: null, // 전체인 경우 null로 처리
};

// URL에서 값을 가져오거나 기본값으로 대체
const page = Number(searchParams.get('page')) || defaultValues.page;
const { size } = defaultValues; // 고정값
const sort = searchParams.get('sort') || defaultValues.sort;
const direction = searchParams.get('direction') || defaultValues.direction;
const categoryId = searchParams.get('categoryId')
? Number(searchParams.get('categoryId'))
: defaultValues.categoryId;

// 경로별 queryKey 구성
if (pathname === '/') {
// 메인 페이지
return contentType === 'READING'
? ['readingPreview']
: ['listeningPreview'];
}

if (pathname.startsWith('/learn')) {
// 학습 페이지
const type = pathname.includes('/reading')
? 'paginatedReadingPreview'
: pathname.includes('/listening')
? 'paginatedListeningPreview'
: null;

if (!type) {
throw new Error('Invalid path: unable to determine query type');
}

return [type, page, size, sort, direction, categoryId].filter(
(item) => item !== null,
); // 기본값(categoryId)이 null인 경우 제거
}

throw new Error('Invalid path: no matching query key logic');
};

const { toggleScrap } = useScrapToggle({
contentId,
target,
page,
queryKey: generateQueryKey(),
});

const handleShowLoginModal = (event: React.MouseEvent) => {
Expand Down
4 changes: 2 additions & 2 deletions src/components/common/PointCover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ export default function PointCover({ data, children }: PointCoverProps) {
{children}
{/* 포인트가 필요한 경우 오버레이 표시 */}
{data.isPointRequired && !isConfirmed && (
<div className="absolute inset-0 bg-black/40 flex items-center justify-center z-10 rounded-xl">
<div className="absolute inset-0 bg-black/40 flex items-center justify-center z-30 rounded-xl">
<div className="bg-black/80 rounded-lg p-2 backdrop-blur-sm flex items-center gap-2 text-white">
<CircleParking className="w-4 h-4" />
<span className="text-sm font-medium">포인트 필요</span>
<span className="text-sm font-medium z-50">포인트 필요</span>
</div>
</div>
)}
Expand Down
10 changes: 5 additions & 5 deletions src/components/items/ContentCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,17 @@ export default function ContentCard({
<Card className="overflow-hidden shadow-card hover:shadow-card-hover hover:border-border h-full mr-3">
<CardContent className="p-0 h-full">
<div className="relative w-full h-40 overflow-hidden">
<div className="absolute top-3 left-3 z-20">{topLeftButton}</div>
<div className="absolute top-3 right-3 z-20">{topRightButton}</div>
<div className="absolute top-3 left-3 z-10">{topLeftButton}</div>
<div className="absolute top-3 right-3 z-10">{topRightButton}</div>
<img
src={coverImageUrl}
alt={title}
className="object-cover w-full h-full"
/>
<div className="absolute bottom-3 left-3 z-20">
<div className="absolute bottom-3 left-3 z-10">
{bottomLeftButton}
</div>{' '}
<div className="absolute bottom-3 right-3 z-20">
</div>
<div className="absolute bottom-3 right-3 z-10">
{bottomRightButton}
</div>
</div>
Expand Down
Loading

0 comments on commit 7555f4e

Please sign in to comment.