Skip to content
Merged
12 changes: 4 additions & 8 deletions apps/client/src/pages/myBookmark/MyBookmark.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Badge, Card, PopupContainer } from '@pinback/design-system/ui';
import { Badge, PopupContainer } from '@pinback/design-system/ui';
import { useState } from 'react';
import {
useGetBookmarkArticles,
Expand All @@ -20,6 +20,7 @@ import {
usePutArticleReadStatus,
} from '@shared/apis/queries';
import NoUnreadArticles from '@pages/myBookmark/components/noUnreadArticles/NoUnreadArticles';
import FetchCard from './components/fetchCard/FetchCard';

const MyBookmark = () => {
const [activeBadge, setActiveBadge] = useState<'all' | 'notRead'>('all');
Expand Down Expand Up @@ -144,16 +145,11 @@ const MyBookmark = () => {
{articlesToDisplay && articlesToDisplay.length > 0 ? (
<div className="scrollbar-hide mt-[2.6rem] flex h-screen flex-wrap gap-[1.6rem] overflow-y-auto scroll-smooth">
{articlesToDisplay.map((article) => (
<Card
<FetchCard
key={article.articleId}
type="bookmark"
title={article.url}
content={article.memo}
category={article.category.categoryName}
date={new Date(article.createdAt).toLocaleDateString('ko-KR')}
article={article} // article 객체를 통째로 넘겨줍니다.
onClick={() => {
window.open(article.url, '_blank');

updateToReadStatus(article.articleId, {
onSuccess: () => {
// TODO: 쿼리키 팩토리 패턴 적용
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { BookmarkArticle } from '@pages/myBookmark/types/api';
import { Card } from '@pinback/design-system/ui';
import { useGetPageMeta } from '@shared/apis/queries';
import React from 'react';

interface FetchCardProps {
article: BookmarkArticle;
onClick: () => void;
onOptionsClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
}

const FetchCard = ({ article, onClick, onOptionsClick }: FetchCardProps) => {
const { data: meta, isPending, isError: error } = useGetPageMeta(article.url);

if (isPending) {
return (
<div className="bg-gray200 h-[35.6rem] w-[24.8rem] animate-pulse rounded-[1.2rem]" />
);
}

const displayTitle = !error && meta.title ? meta.title : '제목 없음';
const displayImageUrl = !error ? meta.image : undefined;

return (
<Card
type="bookmark"
title={displayTitle}
imageUrl={displayImageUrl}
content={article.memo}
category={article.category.categoryName}
date={new Date(article.createdAt).toLocaleDateString('ko-KR')}
onClick={onClick}
onOptionsClick={onOptionsClick}
/>
);
};

export default FetchCard;
2 changes: 1 addition & 1 deletion apps/client/src/pages/myBookmark/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ interface Category {
categoryColor: string;
}

interface BookmarkArticle {
export interface BookmarkArticle {
articleId: number;
url: string;
memo: string;
Expand Down
25 changes: 11 additions & 14 deletions apps/client/src/pages/remind/Remind.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useMemo, useState } from 'react';
import { Badge, Card, PopupContainer } from '@pinback/design-system/ui';
import { Badge, PopupContainer } from '@pinback/design-system/ui';
import CardEditModal from '@shared/components/cardEditModal/CardEditModal';
import OptionsMenuPortal from '@shared/components/sidebar/OptionsMenuPortal';
import { useAnchoredMenu } from '@shared/hooks/useAnchoredMenu';
Expand All @@ -16,6 +16,7 @@ import {
} from '@shared/apis/queries';
import { useQueryClient } from '@tanstack/react-query';
import NoRemindArticles from './components/noRemindArticles/NoRemindArticles';
import FetchCard from './components/fetchCard/FetchCard';

const Remind = () => {
const [isEditOpen, setIsEditOpen] = useState(false);
Expand All @@ -25,13 +26,15 @@ const Remind = () => {

const formattedDate = useMemo(() => {
return formatLocalDateTime();
}, [activeBadge]);
}, []);

const queryClient = useQueryClient();

const { mutate: getArticleDetail, data: articleDetail } =
useGetArticleDetail();
Comment on lines +33 to +34
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

🧩 Analysis chain

useGetArticleDetail이 mutation으로 사용되고 있음

useGetArticleDetail이 데이터를 가져오는 용도임에도 불구하고 useMutation으로 구현되어 있습니다. 이는 일반적인 패턴이 아니며, useQuery를 사용하는 것이 더 적절합니다.

현재 구현을 확인하기 위해 다음 스크립트를 실행합니다:

useQuery를 사용하도록 리팩토링하는 것을 권장합니다:

export const useGetArticleDetail = (articleId: number, enabled = false) => {
  return useQuery({
    queryKey: ['articleDetail', articleId],
    queryFn: () => getArticleDetail(articleId),
    enabled: enabled && !!articleId,
  });
};

🏁 Script executed:

#!/bin/bash
# useGetArticleDetail 훅의 구현 확인
rg -A 10 "useGetArticleDetail" apps/client/src/shared/apis/queries.ts

# 일반적으로 GET 요청은 useQuery를 사용하는 패턴 확인
rg "useGet" apps/client/src/shared/apis/queries.ts -A 3

Length of output: 776


useGetArticleDetail: useMutation → useQuery로 리팩토링 필요

apps/client/src/shared/apis/queries.ts의 useGetArticleDetail이 조회 훅임에도 useMutation으로 구현되어 있습니다 — 조회(GET)는 useQuery가 적절하므로 훅을 useQuery로 교체하고 호출부(apps/client/src/pages/remind/Remind.tsx:33-34)도 함께 수정하세요.

제안 구현 예시:

export const useGetArticleDetail = (articleId?: number, enabled = false) =>
  useQuery({
    queryKey: ['articleDetail', articleId],
    queryFn: () => getArticleDetail(articleId!),
    enabled: enabled && !!articleId,
  });

영향 호출 예시 변경:

  • 현재: const { mutate: getArticleDetail, data: articleDetail } = useGetArticleDetail();
  • 변경: const { data: articleDetail } = useGetArticleDetail(articleId, !!articleId);
🤖 Prompt for AI Agents
In apps/client/src/pages/remind/Remind.tsx around lines 33-34, the component
currently destructures a useGetArticleDetail hook implemented as a mutation
(mutate) but it should use a query; update the call to the refactored
useGetArticleDetail(query) signature by passing the articleId and an enabled
flag and remove mutate usage — e.g., replace the current destructure that gets
mutate and data with simply { data: articleDetail } =
useGetArticleDetail(articleId, !!articleId); also ensure the hook implementation
in apps/client/src/shared/apis/queries.ts is converted from useMutation to
useQuery with queryKey ['articleDetail', articleId], queryFn calling
getArticleDetail(articleId!) and enabled set to enabled && !!articleId, and
adjust any imports accordingly.

const { mutate: updateToReadStatus } = usePutArticleReadStatus();
const { mutate: deleteArticle } = useDeleteRemindArticle();
const { data } = useGetRemindArticles(
const { data, isPending } = useGetRemindArticles(
formattedDate,
activeBadge === 'read',
0,
Expand All @@ -48,8 +51,6 @@ const Remind = () => {

const getItemTitle = (id: number | null) =>
id == null ? '' : (REMIND_MOCK_DATA.find((d) => d.id === id)?.title ?? '');
const { mutate: getArticleDetail, data: articleDetail } =
useGetArticleDetail();

const handleDeleteArticle = (id: number) => {
deleteArticle(id, {
Expand Down Expand Up @@ -80,9 +81,9 @@ const Remind = () => {
};

// TODO: 로딩 상태 디자인 필요
// if (isPending) {
// return <div>Loading...</div>;
// }
if (isPending) {
return <div>Loading...</div>;
}

return (
<div className="flex flex-col py-[5.2rem] pl-[8rem] pr-[5rem]">
Expand All @@ -105,13 +106,9 @@ const Remind = () => {
{data?.articles && data.articles.length > 0 ? (
<div className="scrollbar-hide mt-[2.6rem] flex flex-wrap gap-[1.6rem] overflow-y-auto scroll-smooth">
{data.articles.map((article) => (
<Card
<FetchCard
key={article.articleId}
type="remind"
title={article.url}
content={article.memo}
timeRemaining={article.remindAt}
category={article.category.categoryName}
article={article}
onClick={() => {
window.open(article.url, '_blank');

Expand Down
38 changes: 38 additions & 0 deletions apps/client/src/pages/remind/components/fetchCard/FetchCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
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<HTMLButtonElement>) => void;
}

const FetchCard = ({ article, onClick, onOptionsClick }: FetchCardProps) => {
const { data: meta, isPending, isError: error } = useGetPageMeta(article.url);

if (isPending) {
return (
<div className="bg-gray200 h-[35.6rem] w-[24.8rem] animate-pulse rounded-[1.2rem]" />
);
}

const displayTitle = !error && meta.title ? meta.title : '제목 없음';
const displayImageUrl = !error ? meta.image : undefined;

return (
<Card
type="remind"
title={displayTitle}
imageUrl={displayImageUrl}
content={article.memo}
timeRemaining={article.remindAt}
category={article.category.categoryName}
onClick={onClick}
onOptionsClick={onOptionsClick}
/>
);
};

export default FetchCard;
2 changes: 1 addition & 1 deletion apps/client/src/pages/remind/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ interface Category {
categoryColor: string;
}

interface ArticleWithCategory {
export interface ArticleWithCategory {
articleId: number;
url: string;
memo: string;
Expand Down
11 changes: 11 additions & 0 deletions apps/client/src/shared/apis/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
ArticleReadStatusResponse,
ArticleDetailResponse,
} from '@shared/types/api';
import { fetchOGData } from '@shared/utils/fetchOgData';

export const useGetDashboardCategories = (): UseQueryResult<
DashboardCategoriesResponse,
Expand Down Expand Up @@ -125,3 +126,13 @@ export const usePutEditArticle = () => {
}) => putEditArticle(articleId, editArticleData),
});
};

export const useGetPageMeta = (url: string) => {
return useQuery({
queryKey: ['ogMeta', url],
queryFn: () => fetchOGData(url),
enabled: !!url,
staleTime: Infinity,
retry: false,
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export interface OptionsMenuButtonProps {

const ITEM_STYLE =
'body4-r text-font-black-1 h-[3.6rem] w-full ' +
'flex items-center justify-center ' +
'flex items-center px-[0.8rem] ' +
'hover:bg-gray100 focus-visible:bg-gray100 active:bg-gray200 ' +
'outline-none transition-colors';

Expand Down
7 changes: 1 addition & 6 deletions apps/client/src/shared/hooks/usePageMeta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ export interface PageMeta {
}

export const usePageMeta = (url: string) => {
// 컴포넌트에서 사용할 상태들
const [meta, setMeta] = useState<PageMeta>({
url: '',
title: '',
Expand All @@ -20,9 +19,7 @@ export const usePageMeta = (url: string) => {
const [error, setError] = useState<string | null>(null);

useEffect(() => {
// URL이 유효하지 않으면 아무 작업도 하지 않음
if (!url || url.trim().length < 5) {
// 초기 상태로 리셋
setMeta({ url: '', title: '', description: '', imgUrl: '' });
return;
}
Expand All @@ -33,7 +30,6 @@ export const usePageMeta = (url: string) => {
try {
const ogData = await fetchOGData(url);

// fetchOGData의 결과(OGData)를 컴포넌트에서 사용할 형태(PageMeta)로 변환
setMeta({
url: ogData.url,
title: ogData.title,
Expand All @@ -48,8 +44,7 @@ export const usePageMeta = (url: string) => {
};

getOgData();
}, [url]); // url이 변경될 때마다 이 useEffect는 다시 실행됩니다.
}, [url]);

// 훅의 결과물로 상태와 관련된 값들을 객체로 반환
return { meta, loading, error };
};
13 changes: 7 additions & 6 deletions apps/client/src/shared/utils/fetchOgData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,13 @@ const proxyServices = [
`https://corsproxy.io/?${encodeURIComponent(targetUrl)}`,
extractHtml: (response: AxiosResponse) => response.data,
},
{
name: 'AllOrigins',
buildUrl: (targetUrl: string) =>
`https://api.allorigins.win/get?url=${encodeURIComponent(targetUrl)}`,
extractHtml: (response: AxiosResponse) => response.data?.contents,
},
// TODO: 2개까지 보낼 필요 없어서 일단 주석처리
// {
// name: 'AllOrigins',
// buildUrl: (targetUrl: string) =>
// `https://api.allorigins.win/get?url=${encodeURIComponent(targetUrl)}`,
// extractHtml: (response: AxiosResponse) => response.data?.contents,
// },
Comment on lines +79 to +85
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

프록시 서비스 간소화를 검증해 주세요.

AllOrigins 프록시를 제거하고 CORSProxy.io만 남겨둔 변경사항입니다. 단일 프록시 의존성으로 인한 장애 가능성을 고려해야 합니다.

다음 스크립트를 실행하여 CORSProxy.io 서비스의 안정성과 가용성을 확인해주세요:


🏁 Script executed:

#!/bin/bash
# CORSProxy.io 서비스 가용성과 응답 시간 테스트

echo "Testing CORSProxy.io availability and response time..."

# 테스트용 URL들
TEST_URLS=("https://example.com" "https://github.com" "https://stackoverflow.com")

for url in "${TEST_URLS[@]}"; do
  echo "Testing: $url"
  
  # CORSProxy.io를 통한 요청 테스트
  proxy_url="https://corsproxy.io/?$(echo "$url" | sed 's/:/%3A/g; s/\//%2F/g')"
  
  # 응답 시간과 상태 코드 확인
  response=$(curl -s -w "%{http_code}|%{time_total}" -o /dev/null "$proxy_url" --max-time 15)
  
  if [ $? -eq 0 ]; then
    status_code=$(echo "$response" | cut -d'|' -f1)
    time_total=$(echo "$response" | cut -d'|' -f2)
    echo "  Status: $status_code, Time: ${time_total}s"
  else
    echo "  Request failed or timed out"
  fi
  
  echo ""
done

Length of output: 242


CORSProxy.io가 테스트에서 403 응답 — AllOrigins 제거는 위험합니다.

스크립트 결과: example.com / github.com / stackoverflow.com 모두 CORSProxy.io를 통해 403을 반환했습니다. 현 구성으로는 OG 데이터 수집 실패 가능성이 높습니다.

  • 조치: apps/client/src/shared/utils/fetchOgData.ts에 AllOrigins를 fallback으로 복원하거나 백엔드 서버사이드 프록시로 대체하세요.
  • 추가: 4xx/5xx 에러 시 재시도·fallback·로깅 로직을 추가하고, CORSProxy.io의 사용 제한(정책/차단/레이트리밋)을 확인하세요.
🤖 Prompt for AI Agents
In apps/client/src/shared/utils/fetchOgData.ts around lines 79 to 85, removing
the AllOrigins fallback makes OG fetching brittle because CORSProxy.io returns
403 in tests; restore AllOrigins as a fallback option (or replace with a backend
server-side proxy) and update the fetch logic to: detect 4xx/5xx responses,
implement a retry strategy with backoff, attempt the fallback proxy when
CORSProxy.io fails, and log failures with response details and the proxy used;
also add a note/config to document CORSProxy.io usage limits and alternative
endpoints so the fallback/proxy choice is configurable.

];

/**
Expand Down
Loading