Feat(client): 저장 관련 기능 QA 수정 반영 & 일부 fallback 처리 추가#149
Feat(client): 저장 관련 기능 QA 수정 반영 & 일부 fallback 처리 추가#149constantly-dev merged 8 commits intodevelopfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughCard and extension UIs now gate InfoBox rendering behind loading states with skeleton placeholders and image fallbacks. Sidebar adjusts to new hook return (drops isError), adds safe acorn count access, and shows a loading skeleton for the level item. Badge component layout switches to fixed-size, centered flex box. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant CardEditModal
participant MetaHook as usePageMeta
participant UI as Skeleton/InfoBox
User->>CardEditModal: Open edit modal (has prevData.url)
CardEditModal->>MetaHook: fetchMeta(prevData.url)
MetaHook-->>CardEditModal: { loading: true }
CardEditModal->>UI: Render Skeleton
MetaHook-->>CardEditModal: { loading: false, imgUrl? }
CardEditModal->>UI: Render InfoBox with imgUrl || noImage
sequenceDiagram
autonumber
actor User
participant Sidebar
participant Hook as useGetArcons
participant UI as Skeleton/MyLevelItem
User->>Sidebar: Open sidebar
Sidebar->>Hook: fetch acorn data
Hook-->>Sidebar: { isPending: true }
Sidebar->>UI: Render level skeleton, acornCount=0
Hook-->>Sidebar: { isPending: false, data? }
Sidebar->>UI: Render MyLevelItem, acornCount=(data?.acornCount ?? 0)
sequenceDiagram
autonumber
actor User
participant MainPop
participant Loader as Loading State
participant UI as Skeleton/InfoBox
User->>MainPop: Open extension popup
MainPop->>Loader: Check loading
alt loading
MainPop->>UI: Render Skeleton
else ready
MainPop->>UI: Render InfoBox
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (3 warnings)
✅ Passed checks (2 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 |
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)
packages/design-system/src/components/badge/Badge.tsx (1)
38-41: 클릭 가능한 div는 키보드 접근성이 없습니다 — button으로 교체하고 상태를 노출하세요역할(button), 포커스 링, aria-pressed를 추가해 접근성을 보장하세요.
- <div - className="flex cursor-pointer items-center justify-center gap-[0.8rem]" - onClick={onClick} - > + <button + type="button" + className="flex items-center justify-center gap-[0.8rem] cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-main500" + onClick={onClick} + aria-pressed={isActive} + disabled={!onClick} + > ... - </div> + </button>Also applies to: 48-48
🧹 Nitpick comments (10)
apps/client/src/shared/components/cardEditModal/CardEditModal.tsx (2)
181-189: Tailwind 클래스 오타로 스켈레톤 스타일 깨짐 (w-[full] → w-full, bg-gray100 → bg-gray-100).현재 클래스는 Tailwind에서 해석되지 않습니다.
다음으로 수정해 주세요:
- <div className="bg-gray100 h-[6.8rem] w/[full] animate-pulse rounded-[4px]" /> + <div className="bg-gray-100 h-[6.8rem] w-full animate-pulse rounded-[4px]" />
37-37: 메타 로딩 실패(error) 케이스도 분기해 안전한 폴백을 렌더링해 주세요.빈 문자열이 InfoBox로 전달될 수 있습니다.
예시 수정안:
- const { meta, loading } = usePageMeta(prevData.url); + const { meta, loading, error } = usePageMeta(prevData.url); ... - {loading ? ( + {loading ? ( <div className="bg-gray-100 h-[6.8rem] w-full animate-pulse rounded-[4px]" /> - ) : ( + ) : error ? ( + <InfoBox + title="메타 정보를 불러올 수 없어요" + source={prevData.url} + imgUrl={noImage} + /> + ) : ( <InfoBox title={meta.title} source={meta.description} imgUrl={meta.imgUrl || noImage} /> )}Also applies to: 181-189
apps/extension/src/pages/MainPop.tsx (2)
169-176: 리마인드 입력 에러 시 저장 차단.유효성 에러 상태(dateError/timeError)에서도 저장이 진행됩니다.
간단한 선 가드를 추가해 주세요:
const handleSave = async () => { + if (isRemindOn && (dateError || timeError)) { + alert('리마인드 날짜/시간을 확인해주세요.'); + return; + }
278-286: Tailwind 클래스 오타로 스켈레톤 스타일 깨짐 (w-[full] → w-full, bg-gray100 → bg-gray-100).CardEditModal과 동일 이슈입니다.
다음으로 수정해 주세요:
- {loading ? ( - <div className="bg-gray100 h-[6.8rem] w/[full] animate-pulse rounded-[4px]" /> + {loading ? ( + <div className="bg-gray-100 h-[6.8rem] w-full animate-pulse rounded-[4px]" />packages/design-system/src/components/badge/Badge.tsx (2)
45-47: countNum 미지정/0일 때 빈 박스가 노출됩니다 — 조건부 렌더링으로 UX 개선카운트가 없거나 0이면 배지를 감추는 편이 일반적입니다.
- <span className={BadgeStyleVariants({ active: isActive })}> - {countNum} - </span> + {(countNum ?? 0) > 0 && ( + <span className={BadgeStyleVariants({ active: isActive })}> + {countNum} + </span> + )}
24-33: 비활성 상태 가독성(흰 글자 + 회색 배경) 점검 및 variant로 텍스트 컬러도 분기 권장현재 베이스에 고정
text-white-bg가 있어 비활성(bg-gray300)에서도 흰 글자가 될 수 있습니다. 대비가 낮을 수 있으므로 상태별 텍스트 컬러도 분기하세요.-const BadgeStyleVariants = cva( - 'text-white-bg sub5-sb rounded-[0.4rem] min-w-[2.8rem] h-[2.8rem] px-[0.4rem] flex items-center justify-center', +const BadgeStyleVariants = cva( + 'sub5-sb rounded-[0.4rem] min-w-[2.8rem] h-[2.8rem] px-[0.4rem] flex items-center justify-center', { variants: { active: { - true: 'bg-main500', - false: 'bg-gray300', + true: 'bg-main500 text-white-bg', + false: 'bg-gray300 text-font-black-1', } as const, }, defaultVariants: { active: false, }, } );apps/extension/src/hooks/useCategoryManager.ts (4)
37-47: 서버 응답 값으로 동기화하고 React Query 캐시 무결성 유지서버가 이름을 트리밍/정규화할 수 있으므로 응답의 categoryName을 사용하세요. 또한 캐시 무효화로 다른 화면과 일관성을 보장하세요.
onSuccess: (res) => { - const newCategory: Category = { - categoryId: res.data.categoryId, - categoryName: categoryTitle, - categoryColor: res.data.categoryColor ?? '#000000', - }; - setOptions((prev) => [...prev, newCategory.categoryName]); + const serverName = (res.data.categoryName ?? categoryTitle).trim(); + const newCategory: Category = { + categoryId: res.data.categoryId, + categoryName: serverName, + categoryColor: res.data.categoryColor ?? '#000000', + }; + setOptions((prev) => + prev.includes(newCategory.categoryName) ? prev : [...prev, newCategory.categoryName] + ); + // 캐시 무효화로 전역 일관성 유지 + queryClient.invalidateQueries({ queryKey: ['categoriesExtension'] });추가: 훅 내부에서 queryClient 선언
// 상단 import // import { useQueryClient } from '@tanstack/react-query'; // v4/v5 // 또는 'react-query' (레거시) 사용 시 경로 확인 필요 const queryClient = useQueryClient();프로젝트의 React Query 버전에 맞는 useQueryClient import 경로(@tanstack/react-query vs react-query)를 확인해 주세요.
Also applies to: 41-41
49-52: alert 대신 인라인 에러 처리(일관된 UX)현재 팝업 alert은 확장 UI 흐름을 끊습니다. 기존 상태값을 활용해 인라인 에러로 통일하세요.
- alert( - err.response?.data?.message ?? - '카테고리 추가 중 오류가 발생했어요 😢' - ); + setIsPopError(true); + setErrorTxt( + err.response?.data?.message ?? '카테고리 추가 중 오류가 발생했어요 😢' + );
18-19: 초기 options 계산 중복 제거초기값과 useEffect 모두에서 같은 매핑을 수행합니다. 헬퍼로 추출해 중복을 제거하세요.
const namesFromResponse = (data?: { categories?: Category[] }) => data?.categories?.map((c) => c.categoryName) ?? []; const [options, setOptions] = useState<string[]>(namesFromResponse(categoryData?.data)); useEffect(() => { setOptions(namesFromResponse(categoryData?.data)); }, [categoryData]);기본 카테고리 ‘안 읽은 정보’가 항상 배열의 첫 번째로 유지되는지(서버 응답 순서/클라이언트 정렬 로직) 확인 부탁드립니다.
41-41: 색상 값 유효성 검증 추가 제안빈 문자열 등 비정상 값이 들어오면 잘못된 색상으로 렌더될 수 있습니다. 간단한 가드로 보강하세요.
예:
const isHexColor = (v?: string) => /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(v ?? ''); const safeColor = isHexColor(res.data.categoryColor) ? res.data.categoryColor : '#000000';그리고:
- categoryColor: res.data.categoryColor ?? '#000000', + categoryColor: safeColor,
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
apps/client/src/assets/client_thumb.svgis excluded by!**/*.svg
📒 Files selected for processing (5)
apps/client/src/shared/components/cardEditModal/CardEditModal.tsx(3 hunks)apps/client/src/shared/components/sidebar/Sidebar.tsx(3 hunks)apps/extension/src/hooks/useCategoryManager.ts(3 hunks)apps/extension/src/pages/MainPop.tsx(3 hunks)packages/design-system/src/components/badge/Badge.tsx(1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 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 디렉터리를 컨벤션 문서 추가용으로 사용할 예정이라고 명시했습니다.
📚 Learning: 2025-07-17T09:18:13.818Z
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#102
File: apps/extension/src/components/modalPop/ModalPop.tsx:166-172
Timestamp: 2025-07-17T09:18:13.818Z
Learning: In apps/extension/src/components/modalPop/ModalPop.tsx, the categories array should include "안 읽은 정보" (Unread Information) as the first default category that cannot be deleted. This default category is used consistently across the client-side dashboard and should be protected from deletion in the extension as well.
Applied to files:
apps/extension/src/hooks/useCategoryManager.ts
📚 Learning: 2025-07-15T20:00:13.756Z
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#80
File: apps/client/src/shared/components/ui/modalPop/ModalPop.tsx:36-41
Timestamp: 2025-07-15T20:00:13.756Z
Learning: In apps/client/src/shared/components/ui/modalPop/ModalPop.tsx, the InfoBox component uses hardcoded values for title, location, and icon URL as temporary test data. These should be replaced with dynamic data from props when implementing actual functionality and should be marked with TODO comments for future changes.
Applied to files:
apps/client/src/shared/components/cardEditModal/CardEditModal.tsx
🧬 Code graph analysis (4)
apps/client/src/shared/components/sidebar/Sidebar.tsx (2)
apps/client/src/shared/apis/queries.ts (1)
useGetArcons(58-63)apps/client/src/shared/components/sidebar/MyLevelItem.tsx (1)
MyLevelItem(13-61)
apps/extension/src/pages/MainPop.tsx (2)
apps/extension/src/apis/axios.ts (2)
postArticle(9-12)putArticle(65-68)apps/extension/src/utils/remindTimeFormat.ts (1)
combineDateTime(29-54)
apps/extension/src/hooks/useCategoryManager.ts (3)
apps/extension/src/apis/query/queries.ts (2)
useGetCategoriesExtension(36-44)usePostCategories(28-32)apps/extension/src/apis/axios.ts (1)
postCategories(35-38)apps/extension/src/types/types.ts (1)
Category(16-20)
apps/client/src/shared/components/cardEditModal/CardEditModal.tsx (1)
apps/client/src/shared/hooks/usePageMeta.ts (1)
usePageMeta(11-50)
🔇 Additional comments (10)
apps/client/src/shared/components/cardEditModal/CardEditModal.tsx (2)
26-26: 폴백 이미지 도입 좋습니다.네트워크/메타 실패 시 안정적으로 시각 정보가 유지됩니다.
59-62: 카테고리 제목 길이 제한(20자) 정책 확인 필요.본 PR 요약/QA 맥락에서 10자로 축소됐다는 변경이 있는지 확인 부탁드립니다. 일관성 유지가 필요합니다.
원칙이 10자라면 아래처럼 변경해 주세요:
- if (categoryTitle.length > 20) { + if (categoryTitle.length > 10) { setIsPopError(true); - setErrorTxt('20자 이내로 작성해주세요'); + setErrorTxt('10자 이내로 작성해주세요');apps/client/src/shared/components/sidebar/Sidebar.tsx (3)
34-34: 아콘 쿼리 로딩 분기 도입 적절합니다.isPending만으로 스켈레톤/콘텐츠 분리한 점 좋습니다.
207-219: 푸터 스켈레톤 처리 LGTM.로딩 시 영역 점유가 유지되어 레이아웃 점프가 없습니다.
123-123: 검증 결과 — data?.acornCount 경로가 맞습니다.
apps/client/src/shared/types/api.ts의 AcornsResponse에 acornCount: number가 최상위 필드로 선언되어 있어 data?.data?.acornCount로 접근할 필요가 없습니다.Likely an incorrect or invalid review comment.
apps/extension/src/pages/MainPop.tsx (1)
202-219: 성공 시 창 닫기(onSuccess) 처리 적절합니다.React Query mutate 옵션을 활용해 UX를 개선했습니다.
packages/design-system/src/components/badge/Badge.tsx (1)
22-22:text-white-bg는 디자인 시스템에 정의된 유틸 클래스이므로 변경 불필요
packages/tailwind-config/shared-styles.css에--color-white-bg: #ffffff가 선언되어 있고, packages/design-system/src/lib/utils.ts 등에서 해당 클래스를 참조·사용하고 있어 그대로 유지해 주세요.Likely an incorrect or invalid review comment.
apps/extension/src/hooks/useCategoryManager.ts (3)
13-16: 문자열 상태 초기화 방식 일관화 OK따옴표 스타일 통일 및 초기화 로직은 무해하며 명확합니다.
Also applies to: 59-62
1-5: import 정리 변경 LGTM형식 통일 외 기능 영향 없습니다.
27-36: 입력 검증 보강 — 공백/빈값·예약어('안 읽은 정보')·중복 방지·상수화 적용 필요apps/extension/src/hooks/useCategoryManager.ts의 saveCategory에 trim·빈값/예약어/중복/길이 검사와 상수화를 적용하세요. 레포 전역에서 '안 읽은 정보' 참조는 발견되지 않아(보호 로직 불명확) 기본 카테고리 보호 여부 추가 확인이 필요합니다.
상수 추가(파일 상단):
const MAX_CATEGORY_NAME_LENGTH = 10; const RESERVED_CATEGORY_NAME = '안 읽은 정보';검증 및 전달값 정규화 적용 예시:
const saveCategory = (onSuccess?: (category: Category) => void) => { - if (categoryTitle.length > 10) { - setIsPopError(true); - setErrorTxt('10자 이내로 작성해주세요'); - return; - } + const name = categoryTitle.trim(); + if (!name) { + setIsPopError(true); + setErrorTxt('카테고리명을 입력해주세요'); + return; + } + if (name === RESERVED_CATEGORY_NAME) { + setIsPopError(true); + setErrorTxt(`'${RESERVED_CATEGORY_NAME}' 이름은 사용할 수 없어요`); + return; + } + if (options.map((n) => n.trim()).includes(name)) { + setIsPopError(true); + setErrorTxt('이미 존재하는 카테고리예요'); + return; + } + if (name.length > MAX_CATEGORY_NAME_LENGTH) { + setIsPopError(true); + setErrorTxt(`${MAX_CATEGORY_NAME_LENGTH}자 이내로 작성해주세요`); + return; + } - postCategories( - { categoryName: categoryTitle }, + postCategories( + { categoryName: name }, { onSuccess: (res) => { const newCategory: Category = { - categoryId: res.data.categoryId, - categoryName: categoryTitle, + categoryId: res.data.categoryId, + categoryName: name, categoryColor: res.data.categoryColor ?? '#000000', }; setOptions((prev) => [...prev, newCategory.categoryName]);기본 카테고리('안 읽은 정보')가 생성/편집/삭제/이동 흐름에서 보호되는지 확인 필요.
apps/extension/src/pages/MainPop.tsx
Outdated
| putArticle( | ||
| { | ||
| articleId: isArticleId, | ||
| data: { | ||
| categoryId: saveData.selectedCategory | ||
| ? parseInt(saveData.selectedCategory) | ||
| : 0, | ||
| memo: saveData.memo, | ||
| now: new Date().toISOString(), | ||
| remindTime: isRemindOn | ||
| ? combineDateTime(saveData.date ?? '', saveData.time ?? '') | ||
| : null, | ||
| }, | ||
| }, | ||
| { | ||
| onSuccess: () => { | ||
| window.close(); | ||
| }, | ||
| } | ||
| ); |
There was a problem hiding this comment.
수정 플로우에서 articleId=0 호출 가능성 가드 필요.
savedData 미주입 등으로 isArticleId가 0이면 /articles/0 호출 위험이 있습니다.
아래처럼 가드를 추가해 주세요:
- } else {
- putArticle(
+ } else {
+ if (!isArticleId) {
+ alert('수정할 아티클 정보를 찾지 못했어요. 다시 시도해주세요.');
+ return;
+ }
+ putArticle(
{
articleId: isArticleId,
data: {
categoryId: saveData.selectedCategory
? parseInt(saveData.selectedCategory)
: 0,
memo: saveData.memo,
now: new Date().toISOString(),
remindTime: isRemindOn
? combineDateTime(saveData.date ?? '', saveData.time ?? '')
: null,
},
},
{
onSuccess: () => {
window.close();
},
}
);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| putArticle( | |
| { | |
| articleId: isArticleId, | |
| data: { | |
| categoryId: saveData.selectedCategory | |
| ? parseInt(saveData.selectedCategory) | |
| : 0, | |
| memo: saveData.memo, | |
| now: new Date().toISOString(), | |
| remindTime: isRemindOn | |
| ? combineDateTime(saveData.date ?? '', saveData.time ?? '') | |
| : null, | |
| }, | |
| }, | |
| { | |
| onSuccess: () => { | |
| window.close(); | |
| }, | |
| } | |
| ); | |
| if (!isArticleId) { | |
| alert('수정할 아티클 정보를 찾지 못했어요. 다시 시도해주세요.'); | |
| return; | |
| } | |
| putArticle( | |
| { | |
| articleId: isArticleId, | |
| data: { | |
| categoryId: saveData.selectedCategory | |
| ? parseInt(saveData.selectedCategory) | |
| : 0, | |
| memo: saveData.memo, | |
| now: new Date().toISOString(), | |
| remindTime: isRemindOn | |
| ? combineDateTime(saveData.date ?? '', saveData.time ?? '') | |
| : null, | |
| }, | |
| }, | |
| { | |
| onSuccess: () => { | |
| window.close(); | |
| }, | |
| } | |
| ); |
🤖 Prompt for AI Agents
In apps/extension/src/pages/MainPop.tsx around lines 220 to 239, the code can
call putArticle with articleId = isArticleId which may be 0 when saved data is
missing, leading to a request to /articles/0; add a guard that verifies
isArticleId is a valid positive integer before calling putArticle (if not, abort
the save flow and handle gracefully—e.g., show an error/toast or return early),
and ensure any downstream logic depends on this validated id so the network call
is never made with 0.
|
|
||
| const BadgeStyleVariants = cva( | ||
| 'text-white-bg sub5-sb rounded-[0.4rem] px-[0.8rem] py-[0.4rem]', | ||
| 'text-white-bg sub5-sb rounded-[0.4rem] w-[2.5rem] h-[2.8rem] flex items-center justify-center', |
There was a problem hiding this comment.
고정 폭으로 인해 다자리 수 카운트가 잘리거나 겹칠 수 있습니다. min-w + padding으로 전환 제안
w-[2.5rem]는 100, 999 같은 3자리 이상에서 오버플로우/겹침 위험이 큽니다. 고정 폭 대신 최소 폭과 내부 여백으로 대응하세요.
- 'text-white-bg sub5-sb rounded-[0.4rem] w-[2.5rem] h:[2.8rem] flex items-center justify-center',
+ 'text-white-bg sub5-sb rounded-[0.4rem] min-w-[2.8rem] h-[2.8rem] px-[0.4rem] flex items-center justify-center',📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 'text-white-bg sub5-sb rounded-[0.4rem] w-[2.5rem] h-[2.8rem] flex items-center justify-center', | |
| 'text-white-bg sub5-sb rounded-[0.4rem] min-w-[2.8rem] h-[2.8rem] px-[0.4rem] flex items-center justify-center', |
🤖 Prompt for AI Agents
In packages/design-system/src/components/badge/Badge.tsx around line 22, the
fixed width class w-[2.5rem] causes multi-digit counts to overflow or overlap;
change it to use a minimum width (e.g., min-w-[2.5rem]) and add horizontal
padding (e.g., px-2) so the badge can grow for longer numbers, and replace or
supplement the fixed height with min-h or vertical padding (py-1) to preserve
vertical centering while keeping the existing flex centering and rounded styles.
|
✅ Storybook chromatic 배포 확인: |
There was a problem hiding this comment.
Actionable comments posted: 0
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/shared/components/cardEditModal/CardEditModal.tsx (1)
88-99: 유효성 오류 상태에서도 remindTime이 생성될 수 있음date/time 값만 존재하면 오류 상태여도 서버로 전달됩니다. 검증 에러가 없을 때만 생성하도록 가드가 필요합니다.
아래로 수정해 주세요.
- const remindTime = - isRemindOn && date && time ? buildUtcIso(date, time) : null; + const canBuildRemind = + isRemindOn && date && time && !dateError && !timeError; + const remindTime = canBuildRemind ? buildUtcIso(date, time) : null;
🧹 Nitpick comments (5)
apps/client/src/shared/components/cardEditModal/CardEditModal.tsx (2)
181-183: Tailwind 클래스 오타: w-[full] → w-full, bg-gray100 → bg-gray-100현재 클래스는 유효하지 않아 스켈레톤 폭/배경색이 적용되지 않을 수 있습니다.
다음으로 교체해 주세요:
-<div className="bg-gray100 h-[6.8rem] w-[full] animate-pulse rounded-[4px]" /> +<div className="bg-gray-100 h-[6.8rem] w-full animate-pulse rounded-[4px]" />
37-37: 로딩 외 오류(failure) 및 빈 메타 데이터에 대한 안전한 렌더링 제어 추가 제안에러 발생 시에도 InfoBox가 비거나 스켈레톤만 남을 수 있습니다. prevData 기반 최소 정보로 폴백하면 UX가 안정적입니다.
아래처럼 error를 구조 분해하고 텍스트도 prevData로 폴백해 주세요.
- const { meta, loading } = usePageMeta(prevData.url); + const { meta, loading, error } = usePageMeta(prevData.url); … - {loading ? ( - <div className="bg-gray-100 h-[6.8rem] w-full animate-pulse rounded-[4px]" /> - ) : ( - <InfoBox - title={meta.title} - source={meta.description} - imgUrl={meta.imgUrl || noImage} - /> - )} + {loading ? ( + <div className="bg-gray-100 h-[6.8rem] w-full animate-pulse rounded-[4px]" /> + ) : ( + <InfoBox + title={meta.title || prevData.title || ''} + source={meta.description || prevData.description || ''} + imgUrl={meta.imgUrl || noImage} + /> + )}Also applies to: 181-189
apps/extension/src/pages/MainPop.tsx (3)
283-285: Tailwind 클래스 오타: w-[full] → w-full, bg-gray100 → bg-gray-100스켈레톤 스타일이 의도대로 적용되지 않을 수 있습니다.
수정안:
- <div className="bg-gray100 h-[6.8rem] w-[full] animate-pulse rounded-[4px]" /> + <div className="bg-gray-100 h-[6.8rem] w-full animate-pulse rounded-[4px]" />
286-291: InfoBox의 이미지 소스는 상태값(imgUrl)로 일관되게 사용상단 useEffect에서 계산한 imgUrl 상태와 InfoBox 표시가 어긋날 수 있습니다. 저장에도 동일 상태를 사용하므로 UI도 상태로 통일하는 게 안전합니다.
- <InfoBox - title={title} - source={description} - imgUrl={initialImgUrl || defaultImageUrl} - /> + <InfoBox + title={title} + source={description} + imgUrl={imgUrl || defaultImageUrl} + />
350-355: 에러 메시지 렌더링 분기 간결화 제안dateError와 timeError를 개별로 노출하면 가독성이 조금 더 좋습니다. 현재 3항 중첩은 읽기가 어렵습니다.
- {dateError ? ( - <p className="body3-r text-error">{dateError}</p> - ) : timeError ? ( - <p className="body3-r text-error">{timeError}</p> - ) : null} + {dateError && <p className="body3-r text-error">{dateError}</p>} + {timeError && <p className="body3-r text-error">{timeError}</p>}
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
apps/client/src/shared/components/cardEditModal/CardEditModal.tsx(3 hunks)apps/extension/src/pages/MainPop.tsx(2 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/App.tsx:10-21
Timestamp: 2025-07-08T11:47:10.642Z
Learning: In apps/extension/src/App.tsx, the InfoBox component currently uses a hardcoded external URL for the icon prop as a temporary static placeholder. The plan is to replace this with dynamic favicon extraction from bookmarked websites in future iterations.
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 디렉터리를 컨벤션 문서 추가용으로 사용할 예정이라고 명시했습니다.
📚 Learning: 2025-07-15T20:00:13.756Z
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#80
File: apps/client/src/shared/components/ui/modalPop/ModalPop.tsx:36-41
Timestamp: 2025-07-15T20:00:13.756Z
Learning: In apps/client/src/shared/components/ui/modalPop/ModalPop.tsx, the InfoBox component uses hardcoded values for title, location, and icon URL as temporary test data. These should be replaced with dynamic data from props when implementing actual functionality and should be marked with TODO comments for future changes.
Applied to files:
apps/extension/src/pages/MainPop.tsxapps/client/src/shared/components/cardEditModal/CardEditModal.tsx
📚 Learning: 2025-07-08T11:47:10.642Z
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/App.tsx:10-21
Timestamp: 2025-07-08T11:47:10.642Z
Learning: In apps/extension/src/App.tsx, the InfoBox component currently uses a hardcoded external URL for the icon prop as a temporary static placeholder. The plan is to replace this with dynamic favicon extraction from bookmarked websites in future iterations.
Applied to files:
apps/extension/src/pages/MainPop.tsx
🧬 Code graph analysis (1)
apps/client/src/shared/components/cardEditModal/CardEditModal.tsx (1)
apps/client/src/shared/hooks/usePageMeta.ts (1)
usePageMeta(11-50)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: storybook
🔇 Additional comments (4)
apps/client/src/shared/components/cardEditModal/CardEditModal.tsx (2)
26-26: 이미지 fallback 추가 👍네이티브 OG 이미지 없을 때 대비한 자산 주입이 적절합니다.
94-96: categoryId 기본값을 0으로 통일하세요미선택 시 -1 사용은 백엔드가 -1을 허용하지 않으면 400 에러를 유발할 수 있습니다.
파일: apps/client/src/shared/components/cardEditModal/CardEditModal.tsx (Lines 94–96)
- category?.categories.find((cat) => cat.name === selectedCategory)?.id ?? - -1, + category?.categories.find((cat) => cat.name === selectedCategory)?.id ?? + 0,레포 전반의 -1/0 사용처를 재검증하고 rg 출력(또는 스크립트 결과)을 첨부하세요 — 제출하신 스크립트가 출력 없이 종료되어 확증 불가합니다.
apps/extension/src/pages/MainPop.tsx (2)
283-291: 로딩 게이팅 + 스켈레톤 도입 좋습니다로딩 중 InfoBox 가리기 패턴 일관 적용 확인했습니다.
217-234: 수정 플로우 가드 부재 + 성공 전 토스트 표출
- articleId가 0/undefined일 때 PUT이
/articles/0로 나갈 수 있습니다. 과거 지적과 동일합니다.- 성공/실패 구분 없이 토스트를 먼저 띄우고 창을 닫고 있어 실패 시에도 성공 토스트가 노출됩니다.
아래처럼 가드 및 mutate 콜백으로 성공 시에만 토스트/닫기 처리해 주세요. 또한 remindTime 역시 유효성 오류 시 전송하지 않도록 가드했습니다.
- } else { - setToastIsOpen(true); - putArticle({ - articleId: isArticleId, - data: { - categoryId: saveData.selectedCategory - ? parseInt(saveData.selectedCategory) - : 0, - memo: saveData.memo, - now: new Date().toISOString(), - remindTime: isRemindOn - ? combineDateTime(saveData.date ?? '', saveData.time ?? '') - : null, - }, - }); - setTimeout(() => { - window.close(); - }, 1000); - } + } else { + if (!isArticleId || isArticleId <= 0) { + alert('수정할 아티클 정보를 찾지 못했어요. 다시 시도해주세요.'); + return; + } + const canBuildRemind = + isRemindOn && !!date && !!time && !dateError && !timeError; + putArticle( + { + articleId: isArticleId, + data: { + categoryId: saveData.selectedCategory + ? parseInt(saveData.selectedCategory) + : 0, + memo: saveData.memo, + now: new Date().toISOString(), + remindTime: canBuildRemind + ? combineDateTime(saveData.date ?? '', saveData.time ?? '') + : null, + }, + }, + { + onSuccess: () => { + setToastIsOpen(true); + setTimeout(() => window.close(), 1000); + }, + onError: () => { + alert('수정 저장에 실패했어요. 다시 시도해주세요.'); + }, + } + ); + }
📌 Related Issues
📄 Tasks
⭐ PR Point (To Reviewer)
📷 Screenshot
Summary by CodeRabbit