Skip to content

Comments

Feat(client): 저장 관련 기능 QA 수정 반영 & 일부 fallback 처리 추가#149

Merged
constantly-dev merged 8 commits intodevelopfrom
feat/#146/save-feat-qa
Sep 22, 2025
Merged

Feat(client): 저장 관련 기능 QA 수정 반영 & 일부 fallback 처리 추가#149
constantly-dev merged 8 commits intodevelopfrom
feat/#146/save-feat-qa

Conversation

@constantly-dev
Copy link
Member

@constantly-dev constantly-dev commented Sep 22, 2025

📌 Related Issues

관련된 Issue를 태그해주세요. (e.g. - close #25)

📄 Tasks

  • 저장 관련 기능 QA 수정 반영
  • 일부 fallback 처리 추가

⭐ PR Point (To Reviewer)

📷 Screenshot

Summary by CodeRabbit

  • 신기능
    • 여러 화면에서 메타 로딩 시 스켈레톤 표시를 도입해 로딩 경험을 개선했습니다.
  • 버그 수정
    • 메타 이미지가 없을 때 기본 이미지로 대체해 비어있는 썸네일을 방지했습니다.
    • 사용자 도토리 개수가 미정일 때 0으로 안전 처리하여 표시 오류를 예방했습니다.
  • 스타일
    • 배지 컴포넌트 크기를 고정하고 중앙 정렬로 정돈해 일관된 레이아웃을 제공합니다.
    • 레벨 정보 영역에 로딩 상태일 때 스켈레톤을 표시해 시각적 깜빡임을 줄였습니다.

@vercel
Copy link

vercel bot commented Sep 22, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
pinback-client-client Ready Ready Preview Comment Sep 22, 2025 0:00am
pinback-client-landing Ready Ready Preview Comment Sep 22, 2025 0:00am

@coderabbitai
Copy link

coderabbitai bot commented Sep 22, 2025

Walkthrough

Card 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

Cohort / File(s) Summary
Card modal loading & image fallback
apps/client/src/shared/components/cardEditModal/CardEditModal.tsx
Adds loading check from usePageMeta(prevData.url) to show skeleton before InfoBox; guards InfoBox render; adds noImage fallback when meta.imgUrl is missing.
Sidebar hook and loading UI
apps/client/src/shared/components/sidebar/Sidebar.tsx
Updates useGetArcons usage to { data, isPending }; removes isError; safely reads acornCount with nullish coalescing; shows skeleton for MyLevelItem when loading.
Extension main popup loading UI
apps/extension/src/pages/MainPop.tsx
Gates InfoBox behind loading flag with skeleton placeholder; no changes to add/edit logic or API calls.
Design system badge layout
packages/design-system/src/components/badge/Badge.tsx
Changes base styles to fixed width/height with centered flex alignment while keeping active/inactive variants.

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
Loading
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)
Loading
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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

feat

Suggested reviewers

  • jllee000
  • jjangminii

Poem

작은 배지, 딱 맞춘 틀
로딩이면 뼈대가 빛, 준비되면 내용 가득 🌱
도토리 수는 조심히, 없으면 영(0)이라구요!
팝업도 살짝 숨 쉬고, 카드도 사진 챙겼지 🖼️
깡총—오늘도 흐름은 정갈하게 흘러갑니다 🐇

Pre-merge checks and finishing touches

❌ Failed checks (3 warnings)
Check name Status Explanation Resolution
Linked Issues Check ⚠️ Warning 연결된 이슈 #25(progress bar 구현)를 전혀 구현하지 않았으며, 디자인 시스템 변경 또한 해당 이슈 범위에 맞지 않습니다. #146 아티클 저장 기능 QA 반영에 대한 세부 요구사항이 명시되어 있지 않아 코드 변경이 링크된 이슈 요구사항을 충족하는지 확인하기 어렵습니다. #25 관련 구현을 제거하거나 별도 PR로 분리하고, #146의 QA 수정 요구사항을 구체적으로 문서화하여 코드가 이를 반영하도록 수정해야 합니다.
Out of Scope Changes Check ⚠️ Warning Badge 컴포넌트 레이아웃 변경과 Sidebar 훅의 isError 제거 등은 연결된 이슈(#25, #146)에 언급되지 않은 범위 외 변경 사항으로 보입니다. 이러한 코드 변경은 현재 PR의 목적과 일치하지 않습니다. 범위 외 변경 사항을 별도 PR로 분리하거나 제거하여 PR 목적에 맞는 최소한의 변경만 포함하도록 수정해야 합니다.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed 제목은 저장 관련 기능 QA 수정 반영 및 일부 fallback 처리 추가라는 주요 변경 사항을 명확하게 요약하여 PR의 목적을 전달하고 있으며, Feat(client): 프리픽스 사용을 통해 네이밍 컨벤션에도 부합합니다.
Description Check ✅ Passed PR 설명은 템플릿에 정의된 관련 이슈(#146) 및 작업 목록 섹션을 충실히 채우고 있으며, 필수 정보가 모두 포함되어 있습니다. PR Point와 Screenshot 섹션은 비워져 있지만 해당 섹션들은 선택 정보이므로 전체적인 설명 완전성에는 영향을 주지 않습니다.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#146/save-feat-qa

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 9fcd4c3 and 2e82d74.

⛔ Files ignored due to path filters (1)
  • apps/client/src/assets/client_thumb.svg is 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]);

기본 카테고리('안 읽은 정보')가 생성/편집/삭제/이동 흐름에서 보호되는지 확인 필요.

Comment on lines 220 to 239
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();
},
}
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

수정 플로우에서 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.

Suggested change
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',
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

고정 폭으로 인해 다자리 수 카운트가 잘리거나 겹칠 수 있습니다. 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.

Suggested change
'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.

@github-actions
Copy link

✅ Storybook chromatic 배포 확인:
🐿️ storybook

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 2e82d74 and 081efb9.

📒 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.tsx
  • apps/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('수정 저장에 실패했어요. 다시 시도해주세요.');
+          },
+        }
+      );
+    }

@constantly-dev constantly-dev added the fix 버그 수정하라 러브버그 label Sep 22, 2025
@constantly-dev constantly-dev merged commit 7940692 into develop Sep 22, 2025
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat 기능 개발하라 개발 달려라 달려 fix 버그 수정하라 러브버그

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 아티클 저장 기능 관련 QA 반영

1 participant