Feat(client): Card meta data 반영 & 카테고리 params 동기화#132
Feat(client): Card meta data 반영 & 카테고리 params 동기화#132constantly-dev merged 10 commits intodevelopfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Walkthrough북마크/리마인드 카드 렌더링을 기존 Card 직접 사용에서 FetchCard 컴포넌트로 교체하고, URL 메타데이터를 조회하는 useGetPageMeta 훅을 추가했습니다. 관련 타입을 export로 전환했으며, 리마인드 편집 흐름에서 상세 조회 훅을 연동했습니다. 옵션 메뉴 버튼 스타일을 조정했고, OG 프록시 구성을 단순화했습니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant U as User
participant Page as MyBookmark/Remind Page
participant FC as FetchCard (bookmark/remind)
participant Q as useGetPageMeta (React Query)
participant OG as fetchOGData
U->>Page: 페이지 진입
Page->>FC: 각 article로 FetchCard 렌더
FC->>Q: useGetPageMeta(url) 쿼리 시작
Q->>OG: fetchOGData(url)
OG-->>Q: meta { title, image } 또는 오류
alt 로딩 중
FC-->>U: 스켈레톤 표시
else 성공/오류
FC-->>U: Card 렌더(메타 기반 타이틀/이미지, 폴백 처리)
end
U->>FC: 카드 클릭
FC-->>Page: onClick(article)
Page->>Page: updateToReadStatus, 관련 쿼리 무효화
sequenceDiagram
autonumber
participant U as User
participant Remind as Remind Page
participant API as useGetArticleDetail
participant Modal as CardEditModal
U->>Remind: 옵션 → 편집 선택
Remind->>API: 상세 데이터 조회
API-->>Remind: articleDetail
Remind->>Modal: open(prevData=articleDetail)
Modal-->>U: 편집 UI 표시
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
✅ Storybook chromatic 배포 확인: |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/client/src/pages/remind/Remind.tsx (1)
146-150: 편집 시 articleDetail 데이터 가져오기 타이밍 문제
getArticleDetail을 호출한 직후setIsEditOpen(true)를 실행하는데, 비동기 호출이므로articleDetail이 아직 준비되지 않았을 수 있습니다.mutation의 onSuccess 콜백을 활용하여 데이터가 준비된 후 모달을 여는 것이 안전합니다:
onEdit={(id) => { - getArticleDetail(id); - setIsEditOpen(true); - closeMenu(); + getArticleDetail(id, { + onSuccess: () => { + setIsEditOpen(true); + closeMenu(); + }, + onError: (error) => { + console.error('아티클 상세 정보 조회 실패:', error); + closeMenu(); + } + }); }}
🧹 Nitpick comments (6)
apps/client/src/pages/remind/components/fetchCard/FetchCard.tsx (3)
13-13: useQuery 훅의 반환 값 구조분해 할당 수정 필요
useGetPageMeta훅이 React Query의useQuery를 사용하므로, 반환되는 객체에서isError를 사용해야 합니다. 현재isError: error로 별칭을 지정하고 있는데, 이는 혼동을 일으킬 수 있습니다.다음과 같이 수정하세요:
- const { data: meta, isPending, isError: error } = useGetPageMeta(article.url); + const { data: meta, isPending, isError } = useGetPageMeta(article.url);그리고 21-22번 라인도 함께 수정:
- const displayTitle = !error && meta.title ? meta.title : '제목 없음'; - const displayImageUrl = !error ? meta.image : undefined; + const displayTitle = !isError && meta?.title ? meta.title : '제목 없음'; + const displayImageUrl = !isError ? meta?.image : undefined;
21-22: Optional chaining을 사용한 안전한 속성 접근
meta객체가undefined일 수 있으므로 optional chaining을 사용하여 더 안전하게 접근해야 합니다.- const displayTitle = !error && meta.title ? meta.title : '제목 없음'; - const displayImageUrl = !error ? meta.image : undefined; + const displayTitle = !isError && meta?.title ? meta.title : '제목 없음'; + const displayImageUrl = !isError && meta?.image ? meta.image : undefined;
15-19: 스켈레톤 UI 크기 일치 확인 — 재사용 컴포넌트 분리 권장
- 검증: FetchCard의 스켈레톤(w-[24.8rem], h-[35.6rem])이 design-system의 RemindCard(className="h-[35.6rem]")와 BaseCard(className에 "w-[24.8rem]")와 일치합니다. (파일: apps/client/src/pages/remind/components/fetchCard/FetchCard.tsx, packages/design-system/src/components/card/RemindCard.tsx, packages/design-system/src/components/card/BaseCard.tsx)
- 권장(선택적): 중복 제거 및 유지보수성 향상을 위해 CardSkeleton(또는 BaseCard 크기 상수/prop)으로 분리하여 FetchCard가 재사용하도록 리팩토링하세요.
apps/client/src/pages/remind/Remind.tsx (3)
27-29: formattedDate의 의존성 배열이 비어있어 날짜가 업데이트되지 않음
formattedDate가 컴포넌트 마운트 시에만 계산되고 이후 업데이트되지 않습니다. 이는 사용자가 페이지를 오래 열어둔 경우 날짜가 변경되어도 반영되지 않을 수 있습니다.실시간 날짜 반영이 필요한 경우 다음과 같이 수정하세요:
const formattedDate = useMemo(() => { return formatLocalDateTime(); - }, []); + }, []); // 현재 날짜가 변경되어도 업데이트가 필요하지 않다면 이대로 유지, 필요하다면 interval 설정 고려또는 날짜가 변경될 때마다 자동으로 업데이트하려면:
const [formattedDate, setFormattedDate] = useState(formatLocalDateTime()); useEffect(() => { const interval = setInterval(() => { setFormattedDate(formatLocalDateTime()); }, 60000); // 1분마다 업데이트 return () => clearInterval(interval); }, []);
84-86: 임시 로딩 UI 개선 필요TODO 주석에 명시된 대로 로딩 상태에 대한 적절한 디자인이 필요합니다. 현재는 단순한 텍스트만 표시되고 있습니다.
로딩 스켈레톤 UI를 구현하시겠습니까? 다음과 같은 개선안을 제안합니다:
- if (isPending) { - return <div>Loading...</div>; - } + if (isPending) { + return ( + <div className="flex flex-col py-[5.2rem] pl-[8rem] pr-[5rem]"> + <div className="h-8 w-32 bg-gray-200 animate-pulse rounded" /> + <div className="mt-[3rem] flex gap-[2.4rem]"> + <div className="h-10 w-24 bg-gray-200 animate-pulse rounded-full" /> + <div className="h-10 w-24 bg-gray-200 animate-pulse rounded-full" /> + </div> + <div className="mt-[2.6rem] flex flex-wrap gap-[1.6rem]"> + {[1, 2, 3].map((i) => ( + <div key={i} className="bg-gray-200 h-[35.6rem] w-[24.8rem] animate-pulse rounded-[1.2rem]" /> + ))} + </div> + </div> + ); + }이 작업을 위한 새로운 이슈를 생성할까요?
187-187: 조건부 렌더링 로직 개선
articleDetail이 없으면 모달이 렌더링되지 않지만, 사용자는 편집 버튼을 클릭했는데 아무 반응이 없는 것처럼 보일 수 있습니다.로딩 상태를 추가하여 사용자 경험을 개선하세요:
- {isEditOpen && articleDetail && ( + {isEditOpen && ( <div className="fixed inset-0 z-[1000]" aria-modal="true" role="dialog"> <div className="absolute inset-0 bg-black/60 backdrop-blur-[2px]" onClick={() => setIsEditOpen(false)} /> <div className="absolute inset-0 flex items-center justify-center p-4"> - <CardEditModal - onClose={() => setIsEditOpen(false)} - prevData={articleDetail} - /> + {articleDetail ? ( + <CardEditModal + onClose={() => setIsEditOpen(false)} + prevData={articleDetail} + /> + ) : ( + <div className="bg-white rounded-lg p-8"> + <div className="animate-spin h-8 w-8 border-4 border-primary border-t-transparent rounded-full" /> + </div> + )} </div> </div> )}
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
apps/client/src/pages/myBookmark/MyBookmark.tsx(3 hunks)apps/client/src/pages/myBookmark/components/fetchCard/FetchCard.tsx(1 hunks)apps/client/src/pages/myBookmark/types/api.ts(1 hunks)apps/client/src/pages/remind/Remind.tsx(5 hunks)apps/client/src/pages/remind/components/fetchCard/FetchCard.tsx(1 hunks)apps/client/src/pages/remind/types/api.ts(1 hunks)apps/client/src/shared/apis/queries.ts(2 hunks)apps/client/src/shared/components/optionsMenuButton/OptionsMenuButton.tsx(1 hunks)apps/client/src/shared/hooks/usePageMeta.ts(1 hunks)apps/client/src/shared/utils/fetchOgData.ts(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#2
File: pnpm-workspace.yaml:3-3
Timestamp: 2025-08-18T13:48:59.065Z
Learning: constantly-dev는 docs 디렉터리를 컨벤션 문서 추가용으로 사용할 예정이라고 명시했습니다.
🧬 Code graph analysis (4)
apps/client/src/shared/apis/queries.ts (1)
apps/client/src/shared/utils/fetchOgData.ts (1)
fetchOGData(92-125)
apps/client/src/pages/myBookmark/components/fetchCard/FetchCard.tsx (2)
apps/client/src/pages/myBookmark/types/api.ts (1)
BookmarkArticle(7-14)apps/client/src/shared/apis/queries.ts (1)
useGetPageMeta(130-138)
apps/client/src/pages/remind/components/fetchCard/FetchCard.tsx (2)
apps/client/src/pages/remind/types/api.ts (1)
ArticleWithCategory(8-16)apps/client/src/shared/apis/queries.ts (1)
useGetPageMeta(130-138)
apps/client/src/pages/remind/Remind.tsx (3)
apps/client/src/shared/apis/axios.ts (1)
getArticleDetail(50-53)apps/client/src/shared/apis/queries.ts (3)
useGetArticleDetail(108-116)usePutArticleReadStatus(92-100)useDeleteRemindArticle(102-106)apps/client/src/pages/remind/apis/queries.ts (1)
useGetRemindArticles(6-16)
🔇 Additional comments (12)
apps/client/src/pages/remind/types/api.ts (1)
8-8: 인터페이스 export 변경이 적절합니다.
ArticleWithCategory인터페이스를 export하여 다른 모듈에서 사용할 수 있도록 한 변경사항이 적절합니다. 새로운 FetchCard 컴포넌트에서 이 타입을 사용하게 되므로 필요한 변경입니다.apps/client/src/pages/myBookmark/types/api.ts (1)
7-7: 타입 export 변경이 올바릅니다.
BookmarkArticle인터페이스를 export하여 새로운 FetchCard 컴포넌트에서 사용할 수 있도록 한 변경사항입니다. 컴포넌트 간 타입 안전성을 보장하기 위한 적절한 수정입니다.apps/client/src/shared/components/optionsMenuButton/OptionsMenuButton.tsx (1)
12-12: 스타일 변경이 적절합니다.옵션 메뉴 버튼의 스타일을
justify-center에서px-[0.8rem]으로 변경하여 좌측 정렬과 적절한 패딩을 적용한 개선사항입니다. 사용자 경험이 향상될 것으로 예상됩니다.apps/client/src/shared/apis/queries.ts (2)
28-28: fetchOGData import가 올바릅니다.새로운
useGetPageMeta훅을 위해 필요한 import입니다.
130-138: React Query 훅 구현이 적절합니다.새로운
useGetPageMeta훅의 구현이 올바릅니다:
enabled: !!url로 URL이 있을 때만 실행staleTime: Infinity로 무한 캐싱 설정retry: false로 재시도 비활성화이러한 설정은 페이지 메타데이터의 특성상 적절한 선택입니다.
apps/client/src/shared/hooks/usePageMeta.ts (1)
47-47: 코드 정리가 완료되었습니다.인라인 주석을 제거한 변경사항입니다. 기능적 동작은 변경되지 않았으며, 코드가 더 깔끔해졌습니다.
apps/client/src/pages/myBookmark/components/fetchCard/FetchCard.tsx (4)
1-4: import 구문이 올바릅니다.필요한 타입과 컴포넌트들을 적절히 import하고 있습니다.
6-10: props 인터페이스 정의가 적절합니다.FetchCard 컴포넌트의 props를 명확하게 정의했습니다.
onOptionsClick을 선택적 prop으로 처리한 것이 적절합니다.
12-23: 메타데이터 로딩 상태 처리가 적절합니다.
useGetPageMeta훅을 사용하여 메타데이터를 가져오고, 로딩 중에는 스켈레톤 UI를 표시하는 구현이 좋습니다. 에러 상태에 대한 폴백 처리도 잘 되어 있습니다.
24-36: Card 컴포넌트 렌더링이 적절합니다.메타데이터를 활용하여 Card 컴포넌트에 필요한 props를 올바르게 전달하고 있습니다. 날짜 형식을 한국어로 변환하는 부분도 적절합니다.
apps/client/src/pages/myBookmark/MyBookmark.tsx (2)
1-1: FetchCard import로 변경이 완료되었습니다.기존 Card 컴포넌트에서 새로운 FetchCard 컴포넌트로 변경하고 관련 import를 적절히 수정했습니다.
Also applies to: 23-23
148-178: FetchCard 컴포넌트로의 전환이 적절합니다.기존의 개별 props 전달 방식에서
article객체를 통째로 전달하는 방식으로 변경했습니다. 메타데이터를 자동으로 가져오는 FetchCard의 특성을 잘 활용한 구현입니다.onClick과onOptionsClick핸들러도 그대로 유지되어 기존 기능이 보존됩니다.
| const { mutate: getArticleDetail, data: articleDetail } = | ||
| useGetArticleDetail(); |
There was a problem hiding this comment.
🛠️ 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 3Length 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.
| // TODO: 2개까지 보낼 필요 없어서 일단 주석처리 | ||
| // { | ||
| // name: 'AllOrigins', | ||
| // buildUrl: (targetUrl: string) => | ||
| // `https://api.allorigins.win/get?url=${encodeURIComponent(targetUrl)}`, | ||
| // extractHtml: (response: AxiosResponse) => response.data?.contents, | ||
| // }, |
There was a problem hiding this comment.
🧩 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 ""
doneLength 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.
📌 Related Issues
📄 Tasks
Card meta data 반영 & 카테고리 params 동기화
⭐ PR Point (To Reviewer)
📷 Screenshot
Summary by CodeRabbit