Skip to content

Api(client): 아티클 수정 API 연결#97

Merged
constantly-dev merged 12 commits intodevelopfrom
api/#95/article-edit-api
Sep 13, 2025
Merged

Api(client): 아티클 수정 API 연결#97
constantly-dev merged 12 commits intodevelopfrom
api/#95/article-edit-api

Conversation

@constantly-dev
Copy link
Member

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

📌 Related Issues

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

📄 Tasks

  • 아티클 수정 api 연결

⭐ PR Point (To Reviewer)

아티클 수정 API 연결하고, 수정용 이전 데이터 form 연결 등 작업했습니다.
edit modal에 너무 상태도 많고, validation도 많아서 이후에 React Hook Form으로 리팩토링 하고 싶어요..!....

📷 Screenshot

2025-09-14.2.37.23.mov

Summary by CodeRabbit

  • New Features

    • 편집 버튼 클릭 시 최신 글 상세를 불러와 편집 모달에 자동으로 사전 입력됩니다.
    • 북마크·리마인드 화면에서 동일한 편집 흐름을 제공해 일관된 UX를 제공합니다.
    • 링크의 메타(제목·설명·이미지)를 가져와 정보 박스에 표시되며 이미지 렌더링이 개선되었습니다.
    • 북마크 목록에서 읽음/안읽음 필터가 반영되어 목록이 정확히 표시됩니다.
  • Chores

    • 내부 정리 및 빌드 환경(lib 확장) 업데이트로 안정성 향상되었습니다.

@coderabbitai
Copy link

coderabbitai bot commented Sep 13, 2025

Warning

Rate limit exceeded

@constantly-dev has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 0 minutes and 41 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 59788ff and 2fbeeec.

📒 Files selected for processing (1)
  • apps/client/src/shared/components/cardEditModal/CardEditModal.tsx (4 hunks)

Walkthrough

아티클 편집 흐름에 상세조회/수정 API 훅들을 추가하고 편집 시 상세 데이터를 선조회하여 편집 모달에 전달하도록 페이지·훅·타입·컴포넌트 시그니처를 변경함. 또한 OG 메타 조회 유틸·훅, 날짜/시간 포맷 유틸, tsconfig lib 확장 및 InfoBox 이미지 렌더링 수정이 포함됨.

Changes

Cohort / File(s) Summary
Pages — edit wiring
apps/client/src/pages/myBookmark/MyBookmark.tsx, apps/client/src/pages/remind/Remind.tsx
onEdit 시그니처를 (id: number)로 변경. useGetArticleDetail() 뮤테이션(mutate: getArticleDetail)로 편집 전 상세 조회 후 모달 열기. onOptionsClick에서 이벤트 전파 차단(stopPropagation) 추가.
Page-specific APIs
apps/client/src/pages/myBookmark/apis/axios.ts, apps/client/src/pages/myBookmark/apis/queries.ts
getCategoryBookmarkArticlesreadStatus: boolean 파라미터 추가 및 호출 반영. React Query 훅 useGetCategoryBookmarkArticles 시그니처에 readStatus 인자 추가(쿼리 키는 변경되지 않음).
Shared axios APIs
apps/client/src/shared/apis/axios.ts
getArticleDetail(articleId)putEditArticle(articleId, editArticleData) 신규 추가. EditArticleRequest 타입 임포트 사용.
React Query hooks
apps/client/src/shared/apis/queries.ts
새 뮤테이션 훅 useGetArticleDetailusePutEditArticle 추가 및 관련 타입/axios 함수 임포트 확장.
Types
apps/client/src/shared/types/api.ts
EditArticleRequest, ArticleDetailResponse 및 내부 CategoryResponse 타입 추가.
Card edit modal
apps/client/src/shared/components/cardEditModal/CardEditModal.tsx
prevData?: ArticleDetailResponse prop 추가. prevData로부터 memo/리마인더/카테고리 상태 초기화 로직 추가. usePageMeta 통합으로 InfoBox 데이터 소스 변경. 저장 로직에서 usePutEditArticle 사용 추가.
OG/meta utilities & hooks
apps/client/src/shared/utils/fetchOgData.ts, apps/client/src/shared/hooks/usePageMeta.ts
OG 파서/프록시 기반 fetch 유틸(fetchOGData, parseOgData, OGData) 및 URL로부터 페이지 메타를 제공하는 훅 usePageMeta(url) 추가.
Date/time helpers
apps/client/src/shared/utils/formatDateTime.ts
updateDate(date)updateTime(time) 유틸 추가(문자열 정규화 용도).
TS config
apps/client/tsconfig.json
compilerOptions.lib["DOM","DOM.Iterable","ESNext"] 추가.
Design System — InfoBox
packages/design-system/src/components/infobox/InfoBox.tsx
이미지 렌더링을 object-cover 적용한 multi-line <img>로 변경(시각적 처리 변경).

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor 사용자 as User
    participant 페이지 as Page (MyBookmark / Remind)
    participant RQ_get as useGetArticleDetail (mutation)
    participant AX as axios.getArticleDetail
    participant API as Backend /api/v1/articles/:id
    participant 모달 as CardEditModal

    User->>페이지: 편집 클릭 (articleId)
    페이지->>RQ_get: mutate(articleId)
    RQ_get->>AX: GET /api/v1/articles/{id}
    AX->>API: 요청
    API-->>AX: 200 ArticleDetailResponse
    AX-->>RQ_get: 응답 전달
    RQ_get-->>페이지: onSuccess(data)
    페이지->>모달: open(prevData = ArticleDetailResponse)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • jjangminii
  • jllee000

"토끼가 말하네, 편집 전 살짝 훔쳐보자 🐇
GET 한모금 마시고, 모달엔 미리 담아 두네.
메타로 꾸미고, 시간은 다듬어,
깔끔히 고쳐서, 커밋에 당근 건네네. 🥕"


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.

Pre-merge checks

❌ Failed checks (3 warnings)
Check name Status Explanation Resolution
Linked Issues Check ⚠️ Warning 연결된 이슈 기준으로 [#95]의 "아티클 수정 API 연결"은 axios/queries/hooks 및 CardEditModal 연동으로 부분적으로 충족되었으나 삭제 API 연동은 PR에서 확인되지 않으며 '수정용 이전 데이터 조회 데이터 reset' 동작(편집 모달 종료 시 articleDetail 초기화 등)이 명시적으로 구현되어 있지 않아 이 이슈의 모든 요구사항을 완전히 만족했다고 보기 어렵습니다; 또한 PR은 [#25]의 Progress 컴포넌트 요구사항과는 무관합니다. 삭제 API 엔드포인트·hook·UI 흐름을 추가하거나 해당 기능이 의도적으로 제외되었다면 PR 본문에 명확히 표기하고 이슈 연결을 분리해 주세요; 또한 편집용 prevData 상태를 모달 종료 시 reset 하도록 구현하고 관련 테스트 또는 수동 검증 결과를 함께 제출해 주시기 바랍니다.
Out of Scope Changes Check ⚠️ Warning PR에는 아티클 편집 관련 변경 외에 usePageMeta·fetchOgData 유틸, formatDateTime 헬퍼, tsconfig.lib 추가, InfoBox 이미지 스타일 변경 등 범위가 넓은 변경이 포함되어 있으며 getCategoryBookmarkArticles에 readStatus 파라미터를 추가했음에도 쿼리 키(['categoryBookmarkArticles', categoryId, page, size])에 readStatus가 반영되지 않아 캐시 불일치 문제가 발생할 소지가 있습니다. 비관련 변경(usePageMeta·fetchOgData·포맷 유틸·tsconfig·스타일 등)은 별도 PR로 분리하거나 PR 본문에 변경 이유와 영향을 상세히 설명하고, getCategoryBookmarkArticles 변경 시 쿼리 키에 readStatus를 포함해 캐시 정합성을 확보하며 해당 변경이 필요한 이유와 테스트 결과를 커밋/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 PR 제목 "Api(client): 아티클 수정 API 연결"은 변경의 핵심인 아티클 수정 API 연동을 간결하고 명확하게 요약하고 있어 팀원이 히스토리를 훑을 때 주요 변경을 빠르게 이해할 수 있습니다.
Description Check ✅ Passed PR 설명은 리포지토리 템플릿의 주요 섹션(📌 Related Issues, 📄 Tasks, ⭐ PR Point, 📷 Screenshot)을 포함하고 있으며 Related Issue로 "close #95"를 명시하고 있어 의도와 연관 이슈가 분명합니다; Tasks에 "아티클 수정 api 연결"으로 작업 목적도 간단히 기술되어 있고 PR Point에 리팩토링 계획이 언급되어 있어 리뷰어에게 흐름을 전달합니다. 다만 Tasks와 PR Point가 간결하여 변경된 주요 파일/엔드포인트, 검증(재현) 절차, 혹은 호환성 영향(예: API 시그니처나 캐시 키 변경) 같은 구체 정보가 부족합니다. 스크린샷 항목에 첨부 링크가 있으나 변경 전·후 비교나 동작 설명이 추가되면 더 도움이 됩니다. 전체적으로 템플릿을 따르고 있어 대체로 완전하다고 판단됩니다.

@constantly-dev constantly-dev requested review from jjangminii and jllee000 and removed request for jjangminii September 13, 2025 15:19
@constantly-dev constantly-dev self-assigned this Sep 13, 2025
@constantly-dev constantly-dev added the api api 연결 label Sep 13, 2025
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: 4

🧹 Nitpick comments (9)
apps/client/src/shared/types/api.ts (2)

23-36: 상세 응답 타입 네이밍/구조 일관화 제안 (categoryResponse → category, 타입 export 권장)

리스트 응답에서는 article.category를 쓰는데, 상세에서는 categoryResponse로 달라 혼동 여지가 큽니다. 프런트 전역에서 동일하게 category로 맞추고, 타입도 재사용 가능하도록 export하는 편이 유지보수에 유리합니다.

적용 예:

-interface CategoryResponse {
+export interface CategoryResponse {
   categoryId: number;
   categoryName: string;
   categoryColor: string;
 }
 
 export interface ArticleDetailResponse {
   id: number;
   url: string;
   memo: string;
   remindAt: string;
   createdAt: string;
-  categoryResponse: CategoryResponse;
+  category: CategoryResponse;
 }

16-21: EditArticleRequest(now + remindTime) vs 상세 응답(remindAt) 불일치 — 백엔드 계약 확인 필요

  • 발견: apps/client/src/shared/types/api.ts의 EditArticleRequest는 now, remindTime을 사용하지만 ArticleDetailResponse는 remindAt(ISO 문자열)을 반환합니다.
  • 영향: apps/client/src/shared/apis/axios.ts(putEditArticle), apps/client/src/shared/apis/queries.ts, pages/remind/Remind.tsx 등에서 혼용되어 있음.

조치: 백엔드에 요청 페이로드 계약(now+remindTime vs 단일 remindAt)을 명확히 확인하고, 계약에 따라 클라이언트 타입과 전송/변환 로직을 일치시키기.

apps/client/src/pages/remind/Remind.tsx (1)

36-36: 로드/에러 상태 노출 및 데이터 사용을 위한 구조 분해 제안

모달에 데이터를 넘길 계획이라면 data, isPending, isError도 함께 구조 분해해 상태별 UI를 제어하세요.

-const { mutate: getArticleDetail } = useGetArticleDetail();
+const {
+  mutate: getArticleDetail,
+  data: articleDetail,
+  isPending,
+  isError,
+} = useGetArticleDetail();
apps/client/src/pages/myBookmark/MyBookmark.tsx (4)

43-45: 미사용 데이터 구조 분해 혹은 로딩 상태 활용

data: articleDetail를 구조 분해했지만 사용하지 않습니다. 모달에 데이터를 공급하거나, 당장은 미사용이면 구조 분해를 제거하세요. 로딩/에러 상태도 함께 다루면 UX가 좋아집니다.

-const { mutate: getArticleDetail, data: articleDetail } =
-  useGetArticleDetail();
+const {
+  mutate: getArticleDetail,
+  data: articleDetail,
+  isPending,
+  isError,
+} = useGetArticleDetail();

116-120: 모달은 상세 조회 성공 이후에만 오픈하도록 변경

데이터 없이 모달이 먼저 열리면 깜빡임이 발생합니다. 성공 콜백에서 모달을 열고, 실패 시 사용자 피드백을 주세요.

 onEdit={(id) => {
-  getArticleDetail(id);
-  setIsEditOpen(true);
+  getArticleDetail(id, {
+    onSuccess: () => setIsEditOpen(true),
+    onError: (e) => console.error('failed to load article detail', e),
+  });
   closeMenu();
 }}

135-138: 모달에 상세 데이터 연결 및 컴포넌트 시그니처 확장 제안

// data={articleDetail} 주석을 해제하고, CardEditModalArticleDetailResponse를 받도록 시그니처를 확장하면 수정 초기값 주입이 쉬워집니다. 필요시 타입/프롭스 변경 PR 분리 가능합니다.

 <CardEditModal
   onClose={() => setIsEditOpen(false)}
-  // data={articleDetail}
+  data={articleDetail}
 />

원하시면 CardEditModal의 props/내부 바인딩까지 포함한 패치를 만들어드릴게요.


50-50: 주석 처리된 콘솔 정리

머지 전 불필요한 임시 콘솔 주석은 정리하는 편이 깔끔합니다.

apps/client/src/shared/apis/queries.ts (2)

72-80: 상세 조회를 mutation으로 트리거하는 패턴 합리적 — 단, 반환 데이터 정규화 확인

훅 시그니처가 ArticleDetailResponse임을 감안하면 axios 레이어가 동일 타입을 반환하도록 보장되어야 합니다(현재 getArticleDetail이 래핑된 data.data가 아닌 data를 반환 중으로 보입니다).

필요 시 axios의 getArticleDetailreturn data.data로 통일하고, 여기선 그대로 두는 것을 권장합니다.


15-16: 오탈자/네이밍 일관성(nit): Acorns ↔ Arcons

이 파일 내 훅 이름이 useGetArcons(r)인데, 가져오는 함수/타입은 getAcorns/AcornsResponse(c)입니다. 후속 PR에서 Acorns로 통일 추천드립니다.

📜 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 6b2416b and 70018e6.

📒 Files selected for processing (5)
  • apps/client/src/pages/myBookmark/MyBookmark.tsx (4 hunks)
  • apps/client/src/pages/remind/Remind.tsx (3 hunks)
  • apps/client/src/shared/apis/axios.ts (2 hunks)
  • apps/client/src/shared/apis/queries.ts (2 hunks)
  • apps/client/src/shared/types/api.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/pages/myBookmark/MyBookmark.tsx (3)
apps/client/src/shared/apis/axios.ts (1)
  • getArticleDetail (43-46)
apps/client/src/shared/apis/queries.ts (1)
  • useGetArticleDetail (72-80)
apps/client/src/shared/components/cardEditModal/CardEditModal.tsx (1)
  • CardEditModal (22-169)
apps/client/src/shared/apis/queries.ts (2)
apps/client/src/shared/types/api.ts (2)
  • ArticleDetailResponse (29-36)
  • EditArticleRequest (16-21)
apps/client/src/shared/apis/axios.ts (2)
  • getArticleDetail (43-46)
  • putEditArticle (48-56)
apps/client/src/pages/remind/Remind.tsx (2)
apps/client/src/shared/apis/axios.ts (1)
  • getArticleDetail (43-46)
apps/client/src/shared/apis/queries.ts (1)
  • useGetArticleDetail (72-80)
apps/client/src/shared/apis/axios.ts (1)
apps/client/src/shared/types/api.ts (1)
  • EditArticleRequest (16-21)
🔇 Additional comments (2)
apps/client/src/pages/remind/Remind.tsx (1)

12-12: 상세 조회 훅 도입 👍

수정 플로우 진입 전에 상세를 가져오는 방향은 적절합니다.

apps/client/src/pages/myBookmark/MyBookmark.tsx (1)

16-16: 상세 조회 훅 도입 👍

북마크에서도 동일 패턴으로 맞춘 점 좋습니다.

Comment on lines +90 to 111
onEdit={(id) => {
getArticleDetail(id);
setIsEditOpen(true);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

onEdit에 넘어오는 id가 articleId가 아닐 가능성 높음 (현재 categoryId를 전달 중) → 잘못된 상세 조회 호출 위험

위쪽 카드 옵션 클릭에서 openMenu(article.category.categoryId, …)로 열고 있어, OptionsMenuPortalonEdit(id)로 넘어오는 값이 categoryId일 수 있습니다. 이 상태로는 getArticleDetail(id)가 잘못된 id로 호출됩니다.

  • onEdit는 성공 시에만 모달을 열도록 변경 권장:
 onEdit={(id) => {
-  getArticleDetail(id);
-  setIsEditOpen(true);
+  getArticleDetail(id, {
+    onSuccess: () => setIsEditOpen(true),
+    onError: (e) => console.error('failed to load article detail', e),
+  });
   closeMenu();
 }}
  • 또한 카드 옵션 클릭 시 articleId를 전달하도록 변경하세요(참고용, 해당 위치는 별도 라인에 있음):
// 현재
onOptionsClick={(e) => openMenu(article.category.categoryId, e.currentTarget)}
// 수정
onOptionsClick={(e) => openMenu(article.articleId, e.currentTarget)}
🤖 Prompt for AI Agents
In apps/client/src/pages/remind/Remind.tsx around lines 90-92, onEdit currently
calls getArticleDetail(id) and immediately opens the edit modal but the id
passed may be a categoryId from the options menu; change the flow so onEdit
receives the articleId (fix the options click elsewhere to call
openMenu(article.articleId, …) instead of article.category.categoryId) and in
this file call getArticleDetail with await/then and only call
setIsEditOpen(true) after the detail fetch succeeds (e.g., check response or
catch errors before opening the modal).

Comment on lines 43 to 53
export const getArticleDetail = async (articleId: number) => {
const { data } = await apiRequest.get(`/api/v1/articles/${articleId}`);
return data;
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

getArticleDetail 반환 형태 불일치 → 소비 측 타입 불일치 위험

다른 API(getDashboardCategories, getAcorns)는 data.data를 반환하지만 여기선 data 전체를 반환합니다. useGetArticleDetailArticleDetailResponse를 기대하므로 런타임 오류를 유발할 소지가 큽니다.

-import { EditArticleRequest } from '@shared/types/api';
+import { EditArticleRequest, ArticleDetailResponse } from '@shared/types/api';-export const getArticleDetail = async (articleId: number) => {
-  const { data } = await apiRequest.get(`/api/v1/articles/${articleId}`);
-  return data;
-};
+export const getArticleDetail = async (
+  articleId: number,
+): Promise<ArticleDetailResponse> => {
+  const { data } = await apiRequest.get(`/api/v1/articles/${articleId}`);
+  return data.data;
+};
📝 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
export const getArticleDetail = async (articleId: number) => {
const { data } = await apiRequest.get(`/api/v1/articles/${articleId}`);
return data;
};
export const getArticleDetail = async (
articleId: number,
): Promise<ArticleDetailResponse> => {
const { data } = await apiRequest.get(`/api/v1/articles/${articleId}`);
return data.data;
};
🤖 Prompt for AI Agents
In apps/client/src/shared/apis/axios.ts around lines 43 to 46, getArticleDetail
currently returns the full response object (`data`) while other APIs return
`data.data`, causing a mismatch with the consumer's expected
ArticleDetailResponse; change the function to return `data.data` instead of
`data` and ensure the function signature/typing reflects the
ArticleDetailResponse type so callers (e.g., useGetArticleDetail) receive the
correct shape.

Comment on lines 48 to 63
export const putEditArticle = async (
articleId: number,
editArticleData: EditArticleRequest
) => {
const response = await apiRequest.put(`/api/v1/articles/${articleId}`, {
editArticleData,
});
return response;
};
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

❓ Verification inconclusive

putEditArticle: 요청 바디 모양 및 반환 정규화 확인 필요

현재 바디를 { editArticleData }로 중첩 전송하고, 응답은 AxiosResponse 전체를 반환합니다. 백엔드 계약이 평문 바디(필드 직접 전송)를 기대하는지 확인하고, 프런트 사용성 관점에서 .data만 반환하도록 통일을 권장합니다.

검토/합의 후 적용 예:

 export const putEditArticle = async (
   articleId: number,
   editArticleData: EditArticleRequest
 ) => {
-  const response = await apiRequest.put(`/api/v1/articles/${articleId}`, {
-    editArticleData,
-  });
-  return response;
+  const { data } = await apiRequest.put(
+    `/api/v1/articles/${articleId}`,
+    editArticleData,
+  );
+  return data;
 };

putEditArticle — 요청 바디를 직접 전송하고 응답은 .data만 반환하도록 수정 (필수)

  • 요청 바디가 현재 { editArticleData }로 중첩 전송됩니다 — editArticleData를 평문(body 필드 직접)으로 전송하도록 변경하세요.
  • 현재 AxiosResponse 전체를 반환합니다 — 프런트 일관성을 위해 response.data만 반환하도록 변경하세요.

파일: apps/client/src/shared/apis/axios.ts (라인 48-56)

 export const putEditArticle = async (
   articleId: number,
   editArticleData: EditArticleRequest
 ) => {
-  const response = await apiRequest.put(`/api/v1/articles/${articleId}`, {
-    editArticleData,
-  });
-  return response;
+  const { data } = await apiRequest.put(
+    `/api/v1/articles/${articleId}`,
+    editArticleData,
+  );
+  return data;
 };
🤖 Prompt for AI Agents
In apps/client/src/shared/apis/axios.ts around lines 48-56, the PUT handler
currently sends the body wrapped as { editArticleData } and returns the full
AxiosResponse; change the request to send editArticleData directly as the
request body (not nested) and return only response.data so callers receive the
response payload rather than the entire AxiosResponse.

Comment on lines +82 to +111
export const usePutEditArticle = () => {
return useMutation({
mutationFn: ({
articleId,
editArticleData,
}: {
articleId: number;
editArticleData: EditArticleRequest;
}) => putEditArticle(articleId, editArticleData),
});
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

수정 API 훅: 반환값 정규화(.data) 및 후처리 콜백 권장

호출부에서 항상 res.data를 꺼내지 않도록 여기서 정규화하면 사용성이 좋아집니다. 또한 성공 시 관련 쿼리 무효화(리스트/상세)도 고려하세요.

 export const usePutEditArticle = () => {
   return useMutation({
-    mutationFn: ({
+    mutationFn: ({
       articleId,
       editArticleData,
     }: {
       articleId: number;
       editArticleData: EditArticleRequest;
-    }) => putEditArticle(articleId, editArticleData),
+    }) =>
+      putEditArticle(articleId, editArticleData).then((res) => res.data),
   });
 };

원하시면 useQueryClientinvalidateQueries(['articleDetail', id]), 리스트 키 무효화까지 포함해 드리겠습니다.

📝 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
export const usePutEditArticle = () => {
return useMutation({
mutationFn: ({
articleId,
editArticleData,
}: {
articleId: number;
editArticleData: EditArticleRequest;
}) => putEditArticle(articleId, editArticleData),
});
};
export const usePutEditArticle = () => {
return useMutation({
mutationFn: ({
articleId,
editArticleData,
}: {
articleId: number;
editArticleData: EditArticleRequest;
}) =>
putEditArticle(articleId, editArticleData).then((res) => res.data),
});
};
🤖 Prompt for AI Agents
In apps/client/src/shared/apis/queries.ts around lines 82-92, the mutation
currently returns the raw response which forces callers to access res.data and
does not trigger cache updates; change the hook to normalize the result by
returning response.data from mutationFn and add post-success handling using
useQueryClient to invalidate related queries (e.g.,
invalidateQueries(['articleDetail', articleId]) for the edited item and
invalidateQueries(['articleList']) for lists) — use the variables passed to the
mutation (context or mutation variables) to get articleId inside onSuccess so
the detail key can be targeted.

Copy link
Collaborator

@jllee000 jllee000 left a comment

Choose a reason for hiding this comment

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

깔끔하네요! 고생하셨습니다~!

Comment on lines 33 to 35
email: string;
remindDefault: string;
fcmToken: string;
Copy link
Collaborator

Choose a reason for hiding this comment

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

헛 , 로 되어있었나보군요! 빌드에러는 안났었던데,,

@constantly-dev constantly-dev force-pushed the api/#95/article-edit-api branch from 70018e6 to 05c7db6 Compare September 13, 2025 16:37
@github-actions
Copy link

github-actions bot commented Sep 13, 2025

✅ 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: 3

♻️ Duplicate comments (1)
apps/client/src/shared/apis/queries.ts (1)

101-111: 수정 API 훅: 반환값 정규화(.data) 및 후처리 콜백 권장

호출부에서 항상 res.data를 꺼내지 않도록 여기서 정규화하면 사용성이 좋아집니다. 또한 성공 시 관련 쿼리 무효화(리스트/상세)도 고려하세요.

export const usePutEditArticle = () => {
  return useMutation({
    mutationFn: ({
      articleId,
      editArticleData,
    }: {
      articleId: number;
      editArticleData: EditArticleRequest;
-    }) => putEditArticle(articleId, editArticleData),
+    }) =>
+      putEditArticle(articleId, editArticleData).then((res) => res.data),
  });
};

원하시면 useQueryClientinvalidateQueries(['articleDetail', id]), 리스트 키 무효화까지 포함해 드리겠습니다.

🧹 Nitpick comments (10)
apps/client/src/shared/components/cardEditModal/CardEditModal.tsx (2)

126-130: 메타 로딩/에러 및 공백값 대비한 안전한 렌더링.

meta가 로딩 중이거나 일부 필드가 비어 있을 수 있습니다. 안전한 폴백을 적용해 깜빡임·깨짐을 줄이세요.

-        <InfoBox
-          title={meta.title}
-          source={meta.description}
-          imgUrl={meta.imgUrl}
-        />
+        <InfoBox
+          title={meta.title || prevData?.url || ''}
+          source={meta.description || ''}
+          imgUrl={meta.imgUrl || ''}
+        />

필요하다면 loading일 때 스켈레톤을, error일 때 안내 문구를 노출하는 분기 추가도 고려해주세요.


70-72: 리마인드 OFF 시 유효성 에러/입력값 정리.

스위치를 끌 때 에러 메시지와 입력값을 정리해 UX 혼란을 줄이세요.

-  const handleSwitchChange = (checked: boolean) => {
-    setIsRemindOn(checked);
-  };
+  const handleSwitchChange = (checked: boolean) => {
+    setIsRemindOn(checked);
+    if (!checked) {
+      setDate('');
+      setTime('');
+      setDateError('');
+      setTimeError('');
+    }
+  };
apps/client/src/shared/utils/fetchOgData.ts (6)

1-1: 타입 전용 임포트로 번들 미포함화.

AxiosResponse는 타입 전용이므로 import type을 사용해 런타임 번들을 줄이세요.

-import axios, { AxiosResponse } from 'axios';
+import axios, { type AxiosResponse } from 'axios';

95-98: URL 스킴 판별 로직 보강 (대소문자/비-http 스킴 대응).

startsWith('http')는 'HTTPS://' 대문자, 기타 합법적 스킴을 놓칠 수 있습니다. RFC3986 기반의 스킴 정규식을 사용하세요.

-  if (!targetUrl.startsWith('http')) {
+  if (!/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(targetUrl)) {
     targetUrl = `https://${targetUrl}`;
   }

52-56: OG 이미지 탐색 범위 확대 (secure_url/twitter:image:src 포함).

일부 사이트는 og:image:secure_url 또는 twitter:image:src만 제공합니다. 폴백을 추가하세요.

   const image = resolveUrl(
     sourceUrl,
-    getMetaContent('meta[property="og:image"]') ||
-      getMetaContent('meta[name="twitter:image"]')
+    getMetaContent('meta[property="og:image"]') ||
+      getMetaContent('meta[property="og:image:secure_url"]') ||
+      getMetaContent('meta[name="twitter:image:src"]') ||
+      getMetaContent('meta[name="twitter:image"]')
   );

112-123: 에러 원인 보존 및 가시성 향상.

모든 프록시 실패 시 AggregateError의 원인을 보존하면 디버깅이 쉬워집니다(지원 환경에서 Error cause 사용).

-  } catch (error) {
+  } catch (error) {
     // 모든 프록시가 실패한 경우
     console.error('All proxy services failed.', error);
-    throw new Error(
-      '페이지 메타 정보를 가져올 수 없습니다. URL을 확인해주세요.'
-    );
+    throw new Error(
+      '페이지 메타 정보를 가져올 수 없습니다. URL을 확인해주세요.',
+      // TS 4.6+ / 최신 브라우저 지원
+      // 구형 브라우저 타깃이면 feature detection 후 폴백 필요
+      // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
+      { cause: error as unknown }
+    );
   }

32-65: 브라우저/SSR 경계에 대한 가드 고려.

만약 이 유틸이 SSR 경로에서 호출될 가능성이 있으면 DOMParser 존재 여부를 확인하고 의미 있는 에러/폴백을 제공하세요. 현재는 클라이언트 전용으로 보이나 재사용될 수 있습니다.

이 모듈이 서버 사이드(예: Next.js getServerSideProps/route handler)에서 호출될 가능성이 있는지 확인해주세요. 가능하다면 DOMParser 가드를 추가해야 합니다:

if (typeof DOMParser === 'undefined') {
  throw new Error('DOMParser is not available in this environment.');
}

100-110: 취소 가능 API(AbortSignal) 지원 제안.

UX 향상을 위해 라우트 전환/리스트 스크롤 중단 등에 대비한 요청 취소를 지원하세요.

예시(외부 시그널 연동):

export async function fetchOGData(inputUrl: string, timeout = 10000, signal?: AbortSignal): Promise<OGData> {
  // ...
  const fetchPromises = proxyServices.map((proxy) =>
    axios.get(proxy.buildUrl(targetUrl), { timeout, signal }).then(/* ... */)
  );
}
apps/client/tsconfig.json (2)

5-5: Web Streams/AsyncIterable 사용 시 보강 제안 (DOM.AsyncIterable).

만약 ReadableStream 반복(iteration) 등을 타입 정확도로 다루신다면 DOM.AsyncIterable을 함께 넣는 것을 권장합니다.

다음과 같이 보완 가능합니다:

-    "lib": ["DOM", "DOM.Iterable", "ESNext"],
+    "lib": ["DOM", "DOM.Iterable", "DOM.AsyncIterable", "ESNext"],

참고: 서비스 워커/웹 워커 타입이 필요하면 "WebWorker", "WebWorker.ImportScripts", "WebWorker.Iterable"도 추가하세요.


5-5: 공통화 제안: lib 설정을 베이스 preset으로 이동 고려.

앱/패키지별로 lib가 갈라지면 타입 환경이 분기되어 유지보수 비용이 올라갑니다. 동일 요구가 다른 앱에도 생길 가능성이 있다면 @pinback/typescript-config/react.json에 반영하고, 개별 앱에서는 델타만 관리하는 방식을 추천합니다.

📜 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 70018e6 and 8351962.

📒 Files selected for processing (12)
  • apps/client/src/pages/myBookmark/MyBookmark.tsx (5 hunks)
  • apps/client/src/pages/myBookmark/apis/axios.ts (1 hunks)
  • apps/client/src/pages/myBookmark/apis/queries.ts (1 hunks)
  • apps/client/src/pages/remind/Remind.tsx (3 hunks)
  • apps/client/src/shared/apis/axios.ts (2 hunks)
  • apps/client/src/shared/apis/queries.ts (2 hunks)
  • apps/client/src/shared/components/cardEditModal/CardEditModal.tsx (3 hunks)
  • apps/client/src/shared/hooks/usePageMeta.ts (1 hunks)
  • apps/client/src/shared/types/api.ts (1 hunks)
  • apps/client/src/shared/utils/fetchOgData.ts (1 hunks)
  • apps/client/src/shared/utils/formatDateTime.ts (1 hunks)
  • apps/client/tsconfig.json (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/client/src/pages/remind/Remind.tsx
  • apps/client/src/shared/apis/axios.ts
🧰 Additional context used
🧠 Learnings (2)
📓 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-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 (5)
apps/client/src/shared/hooks/usePageMeta.ts (1)
apps/client/src/shared/utils/fetchOgData.ts (1)
  • fetchOGData (91-124)
apps/client/src/pages/myBookmark/MyBookmark.tsx (3)
apps/client/src/shared/apis/axios.ts (1)
  • getArticleDetail (50-53)
apps/client/src/shared/apis/queries.ts (1)
  • useGetArticleDetail (91-99)
apps/client/src/shared/components/cardEditModal/CardEditModal.tsx (1)
  • CardEditModal (26-195)
apps/client/src/pages/myBookmark/apis/queries.ts (2)
apps/client/src/pages/myBookmark/types/api.ts (1)
  • CategoryBookmarkArticleResponse (31-31)
apps/client/src/pages/myBookmark/apis/axios.ts (1)
  • getCategoryBookmarkArticles (17-27)
apps/client/src/shared/apis/queries.ts (2)
apps/client/src/shared/types/api.ts (2)
  • ArticleDetailResponse (34-41)
  • EditArticleRequest (21-26)
apps/client/src/shared/apis/axios.ts (2)
  • getArticleDetail (50-53)
  • putEditArticle (55-63)
apps/client/src/shared/components/cardEditModal/CardEditModal.tsx (2)
apps/client/src/shared/types/api.ts (1)
  • ArticleDetailResponse (34-41)
apps/client/src/shared/utils/formatDateTime.ts (2)
  • updateDate (11-14)
  • updateTime (17-20)
🔇 Additional comments (18)
apps/client/src/shared/utils/formatDateTime.ts (2)

11-14: 날짜 포맷 변환 함수는 올바르게 구현되었습니다.

함수명과 구현이 명확하며, 빈 문자열 처리도 적절합니다.


17-20: 시간 자르기 함수의 구현이 정확합니다.

slice(0, 5)를 사용해 HH:mm:ss에서 초를 제거하는 로직이 올바르며, 주석으로 의도도 명확하게 설명되어 있습니다.

apps/client/src/pages/myBookmark/apis/axios.ts (1)

17-27: API 함수 시그니처 변경이 적절합니다.

readStatus 파라미터 추가와 쿼리 파라미터 read-status=${readStatus} 구현이 올바릅니다. 다른 myBookmark 페이지 컴포넌트에서도 이 시그니처 변경에 맞춰 업데이트되었습니다.

apps/client/src/shared/types/api.ts (3)

21-26: EditArticleRequest 인터페이스 구조가 명확합니다.

필드명과 타입이 명확하게 정의되어 있고, 수정 API에 필요한 데이터를 적절히 포함하고 있습니다.


28-32: CategoryResponse 인터페이스 구조가 적절합니다.

내부 인터페이스로 정의하여 ArticleDetailResponse와의 관계를 명확히 했습니다.


34-41: ArticleDetailResponse 인터페이스가 잘 설계되었습니다.

아티클 상세 정보에 필요한 모든 필드가 적절한 타입으로 정의되어 있고, CategoryResponse와의 관계도 명확합니다.

apps/client/src/pages/myBookmark/MyBookmark.tsx (5)

17-20: import 문 정리가 깔끔합니다.

필요한 훅들을 명확하게 import했습니다.


39-41: 아티클 상세 조회 훅 사용이 적절합니다.

useGetArticleDetail을 사용하여 편집 시 상세 데이터를 가져오는 패턴이 올바릅니다.


129-132: 이벤트 전파 중단 처리가 적절합니다.

e.stopPropagation()을 추가하여 카드 클릭과 옵션 메뉴 클릭의 충돌을 방지했습니다.


146-150: 편집 플로우가 잘 구현되었습니다.

getArticleDetail(id)를 호출하고 편집 모달을 여는 순서가 적절합니다.


165-169: 편집 모달에 이전 데이터 전달이 올바릅니다.

prevData={articleDetail}로 조회한 아티클 상세 데이터를 모달에 전달하여 편집 폼 프리필 기능을 지원합니다.

apps/client/src/shared/apis/queries.ts (1)

91-99: useGetArticleDetail 구현이 적절합니다.

mutation을 사용해 필요할 때 아티클 상세 정보를 조회하는 패턴이 올바릅니다.

apps/client/src/shared/hooks/usePageMeta.ts (4)

4-9: PageMeta 인터페이스 설계가 명확합니다.

필요한 메타 정보 필드들이 적절한 타입으로 정의되어 있습니다.


22-28: URL 유효성 검사가 적절합니다.

URL이 유효하지 않을 때 초기 상태로 리셋하는 로직이 올바르게 구현되었습니다.


30-48: OG 데이터 가져오기 로직이 잘 구현되었습니다.

비동기 처리, 에러 핸들링, 로딩 상태 관리가 모두 적절하게 구현되었고, OGData를 PageMeta로 변환하는 매핑도 명확합니다.


43-44: 에러 처리에서 한국어 메시지 제공이 좋습니다.

err.message가 없을 때 사용자 친화적인 한국어 메시지를 제공하는 것이 적절합니다.

apps/client/src/shared/components/cardEditModal/CardEditModal.tsx (1)

74-86: remindAt 파싱을 더 견고하게 처리하세요.

split('T')는 ISO가 아닌 문자열, 타임존 포함, 공백 포맷에서 깨집니다. 우선 new Date(prevData.remindAt)로 파싱해 유효하면 로컬 날짜(YYYY-MM-DD)·시간(HH:mm)을 직접 추출해 setDate/setTime 하고, 실패하면 기존 split('T') 폴백을 사용하세요.

위치: apps/client/src/shared/components/cardEditModal/CardEditModal.tsx (약 74–86행).
검증: 서버 스키마에서 remindAt 포맷(예: ISO 8601 with timezone 또는 'YYYY-MM-DDTHH:mm')을 확인.

apps/client/tsconfig.json (1)

5-5: DOM/ESNext lib 지정 — base tsconfig의 lib를 완전 대체합니다

apps/client/tsconfig.json의 "lib": ["DOM","DOM.Iterable","ESNext"] 지정은 fetch/DOMParser 등 DOM·최신 ES 타입이 필요할 때 타당합니다. 다만 base(@pinback/typescript-config/react.json)의 기본 lib 세트가 덮어써지므로 다른 표준 lib(빠진 ES 버전 등)가 누락되지 않았는지 확인하세요.

검증 시도 결과: 제공하신 스크립트가 "No files were searched"를 반환해 레포 내 tsconfig 일관성 확인이 불가합니다. 로컬에서 원래 스크립트를 (--debug 포함해) 다시 실행한 출력 또는 해당 결과를 붙여주거나, 제가 재실행할 수 있도록 저장소 접근/환경을 제공하세요.

Comment on lines 34 to 46
export const useGetCategoryBookmarkArticles = (
categoryId: string | null,
readStatus: boolean,
page: number,
size: number
): UseQueryResult<CategoryBookmarkArticleResponse, AxiosError> => {
return useQuery({
queryKey: ['categoryBookmarkArticles', categoryId, page, size],
queryFn: () => getCategoryBookmarkArticles(categoryId, page, size),
queryFn: () =>
getCategoryBookmarkArticles(categoryId, readStatus, page, size),
enabled: !!categoryId,
});
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

쿼리 키에 readStatus 파라미터 추가 권장

readStatus 파라미터가 함수 시그니처에 추가되었지만, 쿼리 키에는 반영되지 않았습니다. 이로 인해 readStatus가 변경되어도 캐시가 무효화되지 않을 수 있습니다.

다음과 같이 수정을 권장합니다:

export const useGetCategoryBookmarkArticles = (
  categoryId: string | null,
  readStatus: boolean,
  page: number,
  size: number
): UseQueryResult<CategoryBookmarkArticleResponse, AxiosError> => {
  return useQuery({
-    queryKey: ['categoryBookmarkArticles', categoryId, page, size],
+    queryKey: ['categoryBookmarkArticles', categoryId, readStatus, page, size],
    queryFn: () =>
      getCategoryBookmarkArticles(categoryId, readStatus, page, size),
    enabled: !!categoryId,
  });
};
📝 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
export const useGetCategoryBookmarkArticles = (
categoryId: string | null,
readStatus: boolean,
page: number,
size: number
): UseQueryResult<CategoryBookmarkArticleResponse, AxiosError> => {
return useQuery({
queryKey: ['categoryBookmarkArticles', categoryId, page, size],
queryFn: () => getCategoryBookmarkArticles(categoryId, page, size),
queryFn: () =>
getCategoryBookmarkArticles(categoryId, readStatus, page, size),
enabled: !!categoryId,
});
};
export const useGetCategoryBookmarkArticles = (
categoryId: string | null,
readStatus: boolean,
page: number,
size: number
): UseQueryResult<CategoryBookmarkArticleResponse, AxiosError> => {
return useQuery({
queryKey: ['categoryBookmarkArticles', categoryId, readStatus, page, size],
queryFn: () =>
getCategoryBookmarkArticles(categoryId, readStatus, page, size),
enabled: !!categoryId,
});
};
🤖 Prompt for AI Agents
In apps/client/src/pages/myBookmark/apis/queries.ts around lines 34 to 46, the
hook includes readStatus in its parameters but the queryKey omits it, so changes
to readStatus won't invalidate or refetch the cache; update the queryKey to
include readStatus (e.g., add readStatus into the array alongside categoryId,
page, size) so the cache key reflects that parameter and ensure enabled logic
remains correct.

Comment on lines 30 to 32
const { meta, loading, error } = usePageMeta(
'https://www.notion.so/PinBack-23927450eb1c8080a5a1f84a9d483aa9'
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

usePageMeta에 하드코드된 Notion URL 제거 필요 (실제 편집 대상 URL 사용).

편집 모달에서 항상 동일한 Notion URL의 메타만 불러옵니다. prevData.url(기사의 실제 URL)을 사용하도록 바꾸지 않으면 InfoBox가 잘못된 정보를 보여줄 수 있습니다.

다음처럼 수정하세요:

-  const { meta, loading, error } = usePageMeta(
-    'https://www.notion.so/PinBack-23927450eb1c8080a5a1f84a9d483aa9'
-  );
+  const { meta, loading, error } = usePageMeta(prevData?.url ?? '');

추가로, usePageMeta가 빈 문자열('')을 입력받았을 때 요청을 생략하도록 방어 로직이 있는지 확인이 필요합니다. 없으면 hook 내부에서 early-return을 넣어주세요.

📝 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
const { meta, loading, error } = usePageMeta(
'https://www.notion.so/PinBack-23927450eb1c8080a5a1f84a9d483aa9'
);
const { meta, loading, error } = usePageMeta(prevData?.url ?? '');
🤖 Prompt for AI Agents
In apps/client/src/shared/components/cardEditModal/CardEditModal.tsx around
lines 30-32, the component currently calls usePageMeta with a hardcoded Notion
URL which causes the modal to always load the same meta; change the call to pass
the actual article URL (e.g., prevData.url or the prop/source that contains the
article's URL) instead of the hardcoded string; additionally, ensure usePageMeta
defensively handles empty strings by returning early and skipping network
requests when given '' (if it doesn't, add an early-return guard inside the hook
to avoid making requests with an empty URL).

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

🧹 Nitpick comments (1)
packages/design-system/src/components/infobox/InfoBox.tsx (1)

13-13: Tailwind 클래스 오타: items-left → items-start

items-left는 유효한 유틸이 아닙니다. 시작 정렬로 의도가 맞다면 아래처럼 수정하세요.

-      <div className="items-left flex flex-col justify-center gap-[0.2rem] text-left">
+      <div className="items-start flex flex-col justify-center gap-[0.2rem] text-left">
📜 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 8351962 and 7b27f46.

📒 Files selected for processing (2)
  • apps/client/src/shared/components/cardEditModal/CardEditModal.tsx (3 hunks)
  • packages/design-system/src/components/infobox/InfoBox.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/client/src/shared/components/cardEditModal/CardEditModal.tsx
🧰 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-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:

  • packages/design-system/src/components/infobox/InfoBox.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:

  • packages/design-system/src/components/infobox/InfoBox.tsx
🔇 Additional comments (1)
packages/design-system/src/components/infobox/InfoBox.tsx (1)

9-12: object-cover 추가는 적절합니다.

비정사각 썸네일의 왜곡을 방지하고 영역을 균일하게 채웁니다.

@@ -6,7 +6,10 @@ interface InfoBoxProps {
const InfoBox = ({ title, source, imgUrl }: InfoBoxProps) => {
return (
<div className="border-main400 flex h-[6.8rem] w-[full] items-center justify-between gap-[0.8rem] rounded-[4px] border bg-white px-[0.8rem] py-[1.2rem]">
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Tailwind 클래스 오타: w-[full] → w-full

임의값 브라켓 표기는 여기서 무효입니다. 전체 너비가 적용되지 않을 수 있어 교정이 필요합니다.

-    <div className="border-main400 flex h-[6.8rem] w-[full] items-center justify-between gap-[0.8rem] rounded-[4px] border bg-white px-[0.8rem] py-[1.2rem]">
+    <div className="border-main400 flex h-[6.8rem] w-full items-center justify-between gap-[0.8rem] rounded-[4px] border bg-white px-[0.8rem] py-[1.2rem]">
📝 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
<div className="border-main400 flex h-[6.8rem] w-[full] items-center justify-between gap-[0.8rem] rounded-[4px] border bg-white px-[0.8rem] py-[1.2rem]">
<div className="border-main400 flex h-[6.8rem] w-full items-center justify-between gap-[0.8rem] rounded-[4px] border bg-white px-[0.8rem] py-[1.2rem]">
🤖 Prompt for AI Agents
In packages/design-system/src/components/infobox/InfoBox.tsx around line 8, the
Tailwind class uses the invalid arbitrary value syntax "w-[full]" which won't
apply full width; replace "w-[full]" with the correct utility "w-full" so the
element spans full width and remove the unnecessary brackets.

Comment on lines +9 to +12
<img
className="h-[4.4rem] w-[4.4rem] rounded-[0.4rem] object-cover"
src={imgUrl}
/>
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

이미지 접근성·회복성 보완(alt/지연로딩/빈 src·에러 대비).

  • alt가 없어 스크린리더 접근성이 떨어집니다.
  • imgUrl이 없거나 실패 시 빈 src/깨진 이미지가 될 수 있어 가드와 플레이스홀더가 필요합니다.
  • lazy/async 디코딩으로 비용을 낮출 수 있습니다.

지난 러닝(InfoBox가 하드코드 아이콘에서 동적 OG 이미지로 전환 예정)을 반영해 안전한 폴백을 제안합니다.

-      <img
-        className="h-[4.4rem] w-[4.4rem] rounded-[0.4rem] object-cover"
-        src={imgUrl}
-      />
+      {imgUrl ? (
+        <img
+          className="h-[4.4rem] w-[4.4rem] rounded-[0.4rem] object-cover"
+          src={imgUrl}
+          alt={title}
+          loading="lazy"
+          decoding="async"
+          referrerPolicy="no-referrer"
+        />
+      ) : (
+        <div
+          className="h-[4.4rem] w-[4.4rem] rounded-[0.4rem] bg-gray-100"
+          aria-hidden
+        />
+      )}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/design-system/src/components/infobox/InfoBox.tsx around lines 9 to
12, the <img> lacks accessibility and resilience: add a descriptive alt
(fallback to an empty string only when purely decorative), guard against
empty/undefined imgUrl by using a validated src variable, enable loading="lazy"
and decoding="async" to reduce cost, and provide an onError handler that swaps
to a local placeholder/fallback image or hides the element to avoid broken
icons; ensure the fallback is tested and the alt text reflects whether the image
is decorative or informative.

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: 3

♻️ Duplicate comments (2)
apps/client/src/pages/remind/Remind.tsx (1)

112-116: onEdit가 잘못된 id(categoryId)로 상세 조회 호출 + 모달을 선오픈함

  • 현재 onOptionsClick에서 article.category.categoryId를 넘기고 있어 onEdit(id)의 id가 articleId가 아닐 가능성이 큽니다. 과거 코멘트와 동일 이슈입니다. 모달은 상세 조회 성공 시에만 여세요. [중복 코멘트]

아래처럼 수정하세요.

  1. 이 블록(모달 오픈 타이밍) 수정:
-        onEdit={(id) => {
-          getArticleDetail(id);
-          setIsEditOpen(true);
-          closeMenu();
-        }}
+        onEdit={(id) => {
+          getArticleDetail(id, {
+            onSuccess: () => {
+              setIsEditOpen(true);
+              closeMenu();
+            },
+            onError: (e) => {
+              console.error('failed to load article detail', e);
+              closeMenu();
+            },
+          });
+        }}
  1. 다른 위치이지만 핵심 원인(넘기는 id)도 함께 교정:
// 수정 전
onOptionsClick={(e) => openMenu(article.category.categoryId, e.currentTarget)}
// 수정 후
onOptionsClick={(e) => openMenu(article.articleId, e.currentTarget)}

검증 스크립트:

#!/bin/bash
# openMenu 호출들이 articleId를 넘기는지 점검
rg -nP -C2 'openMenu\(' apps | sed -n '1,200p'
apps/client/src/shared/components/cardEditModal/CardEditModal.tsx (1)

30-37: 하드코딩된 메타 URL 사용 금지 — 실제 기사 URL(prevData.url)로 조회

현재 Notion URL로 고정 조회되어 InfoBox가 잘못된 정보를 보여줍니다. 과거 코멘트와 동일 이슈입니다. 빈 URL 방어도 필요합니다.

아래처럼 교체하세요(디버그 로그 제거 포함):

-  console.log('prevData in CardEditModal:', prevData); // prevData 확인용 로그
-  const { meta } = usePageMeta(
-    'https://www.notion.so/PinBack-23927450eb1c8080a5a1f84a9d483aa9'
-  );
+  const { meta } = usePageMeta(prevData?.url ?? '');

추가로, usePageMeta('')가 호출되면 요청을 생략하는 early-return이 있는지 확인 바랍니다. 없으면 훅 내부에 추가해주세요.

usePageMeta 훅이 빈 문자열('') 입력 시 네트워크 요청을 스킵하도록 구현되어 있는지 최신 코드/문서를 확인해 주세요.
🧹 Nitpick comments (4)
apps/client/src/pages/remind/Remind.tsx (2)

43-45: 뮤테이션 바인딩 OK (사소한 네이밍 제안)

mutategetArticleDetail라는 이름을 부여하면 fetch 함수와 혼동될 수 있습니다. fetchArticleDetail 등으로 명확히 하면 가독성이 좋아집니다.


131-134: 모달은 상세 데이터 로딩 후 렌더링 권장

prevData={articleDetail}를 전달하지만, 현재는 요청 이전에 모달이 떠서 빈 값/깜빡임이 발생합니다. 성공 시 오픈으로 바꾸면 해결되지만, 추가로 렌더 조건을 보강해 안전하게 하세요.

-      {isEditOpen && (
+      {isEditOpen && articleDetail && (
         <div className="fixed inset-0 z-[1000]" aria-modal="true" role="dialog">
...
-            <CardEditModal
+            <CardEditModal
               onClose={() => setIsEditOpen(false)}
               prevData={articleDetail}
             />
apps/client/src/shared/components/cardEditModal/CardEditModal.tsx (2)

164-168: meta가 미로딩/실패 시 안전 가드 추가

meta가 undefined일 수 있습니다. 널 병합으로 방어하세요.

-        <InfoBox
-          title={meta.title}
-          source={meta.description}
-          imgUrl={meta.imgUrl}
-        />
+        <InfoBox
+          title={meta?.title ?? ''}
+          source={meta?.description ?? ''}
+          imgUrl={meta?.imgUrl ?? ''}
+        />

173-178: 카테고리 선택 UX/검증 보강 제안

현재 문자열 이름으로 매핑합니다. 이름 중복 시 위험합니다. 디자인시스템이 value/label을 지원하면 id 기반으로 바꾸는 것을 권장합니다. 최소한 저장 시 선택 누락을 막는 검증은 위 패치로 반영했습니다.

📜 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 7b27f46 and 59788ff.

📒 Files selected for processing (4)
  • apps/client/src/pages/remind/Remind.tsx (4 hunks)
  • apps/client/src/shared/apis/axios.ts (2 hunks)
  • apps/client/src/shared/components/cardEditModal/CardEditModal.tsx (4 hunks)
  • apps/client/src/shared/types/api.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/client/src/shared/types/api.ts
  • apps/client/src/shared/apis/axios.ts
🧰 Additional context used
🧠 Learnings (2)
📓 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-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 (2)
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 (1)
  • useGetArticleDetail (91-99)
apps/client/src/shared/components/cardEditModal/CardEditModal.tsx (1)
  • CardEditModal (30-234)
apps/client/src/shared/components/cardEditModal/CardEditModal.tsx (3)
apps/client/src/shared/types/api.ts (2)
  • ArticleDetailResponse (34-41)
  • EditArticleRequest (21-26)
apps/client/src/shared/apis/queries.ts (2)
  • useGetDashboardCategories (28-36)
  • usePutEditArticle (101-111)
apps/client/src/shared/utils/formatDateTime.ts (2)
  • updateDate (11-14)
  • updateTime (17-20)
🔇 Additional comments (4)
apps/client/src/pages/remind/Remind.tsx (1)

12-15: 쿼리 훅 import 추가 OK

변경 의도와 일치하며 문제 없습니다.

apps/client/src/shared/components/cardEditModal/CardEditModal.tsx (3)

16-23: 쿼리/훅/유틸 import 추가 OK


27-27: prevData를 옵셔널로 둔 시그니처 OK

호출부 이행만 보장되면 안전합니다.


219-219: 저장 버튼 핸들러 연결 OK

상단 유효성/포맷 보완 패치 적용 시 함께 동작 확인 부탁드립니다.

적용 후 저장 성공/실패 플로우, 토스트, 리스트 갱신 여부를 수동 시나리오 테스트로 확인해주세요.

Comment on lines 80 to 110
const saveData = () => {
if (!prevData?.id) {
console.error('Article ID is missing, cannot save.');
setToastIsOpen(true);
return;
}

const editArticleData: EditArticleRequest = {
memo,
categoryId:
category?.categories.find((cat) => cat.name === selectedCategory)?.id ||
-1,
now: new Date().toISOString(),
remindTime: isRemindOn ? `${date}T${time}` : null,
};

editArticle(
{
articleId: prevData?.id,
editArticleData,
},
{
onSuccess: () => {
onClose();
},
onError: () => {
setToastIsOpen(true);
},
}
);
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

remindTime 포맷 오류(YYYY.MM.DD 사용) + 유효성/카테고리 검증 부재

  • useEffect에서 dateYYYY.MM.DD로 세팅하고 그대로 ${date}T${time}으로 전송 → ISO 아님(서버 파싱 실패 가능).
  • 리마인드 on일 때 날짜/시간 오류를 막지 않음.
  • 카테고리 미선택 시 -1로 전송.

다음과 같이 보완하세요.

   const saveData = () => {
     if (!prevData?.id) {
       console.error('Article ID is missing, cannot save.');
       setToastIsOpen(true);
       return;
     }
 
+    // 리마인드 유효성 검사
+    if (isRemindOn) {
+      if (!date || !time || dateError || timeError) {
+        setToastIsOpen(true);
+        setErrorTxt('리마인드 날짜/시간을 올바르게 입력해주세요');
+        return;
+      }
+    }
+
+    // 카테고리 선택/기본값 결정
+    const selectedCategoryId =
+      category?.categories.find((cat) => cat.name === selectedCategory)?.id ??
+      prevData?.categoryResponse?.id ??
+      -1;
+    if (selectedCategoryId === -1) {
+      setToastIsOpen(true);
+      setErrorTxt('카테고리를 선택해주세요');
+      return;
+    }
+
+    // 저장용 ISO 포맷 변환(YYYY.MM.DD -> YYYY-MM-DD)
+    const dateISO = date ? date.replace(/\./g, '-') : '';
+    const remindTimeISO = isRemindOn ? `${dateISO}T${time}` : null;
 
     const editArticleData: EditArticleRequest = {
       memo,
-      categoryId:
-        category?.categories.find((cat) => cat.name === selectedCategory)?.id ||
-        -1,
+      categoryId: selectedCategoryId,
       now: new Date().toISOString(),
-      remindTime: isRemindOn ? `${date}T${time}` : null,
+      remindTime: remindTimeISO,
     };
 
     editArticle(
       {
         articleId: prevData?.id,
         editArticleData,
       },
       {
         onSuccess: () => {
           onClose();
         },
         onError: () => {
           setToastIsOpen(true);
         },
       }
     );
   };

운영 측면 제안: 성공 시 리스트/상세 쿼리 무효화로 화면 동기화가 필요할 수 있습니다(예: ['remindArticles'], ['articleDetail', prevData.id]). 원하시면 코드 패치를 제안하겠습니다.

🤖 Prompt for AI Agents
In apps/client/src/shared/components/cardEditModal/CardEditModal.tsx around
lines 80–110, fix three issues: ensure remindTime is sent as a proper ISO
timestamp instead of the current YYYY.MM.DD concatenation, add validation to
prevent saving when remind is on but date or time are invalid, and stop sending
a sentinel categoryId (-1) when no category is selected. Convert the stored date
(formatted as YYYY.MM.DD) into a valid ISO date (e.g., parse the parts and build
a new Date or replace dots with dashes and ensure valid month/day) then combine
with time and call toISOString(); before calling editArticle, if isRemindOn
verify both date and time parse to a valid Date and show the toast (or prevent
submit) on failure; for category, require a selectedCategory (or send null/omit
categoryId) instead of -1 and surface a toast/error if missing. Optionally after
successful edit, invalidate relevant queries (e.g., ['remindArticles'] and
['articleDetail', prevData.id]) to refresh UI.

Comment on lines 112 to 124
useEffect(() => {
if (prevData) {
setMemo(prevData.memo || '');

if (prevData.remindAt) {
const [rawDate, rawTime] = prevData.remindAt.split('T');
setDate(updateDate(rawDate));
setTime(updateTime(rawTime));
setIsRemindOn(true);
}
}
}, [prevData]);

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

초기 값 주입 보강 및 날짜 상태 주의

  • memo/리마인드 주입은 좋습니다. 여기에 카테고리 초기 선택값도 넣어주세요.
  • updateDate로 점(.) 포맷을 상태에 저장하면 저장 시 재변환이 필요합니다(위 saveData 패치로 해결).
   useEffect(() => {
     if (prevData) {
       setMemo(prevData.memo || '');
 
       if (prevData.remindAt) {
         const [rawDate, rawTime] = prevData.remindAt.split('T');
         setDate(updateDate(rawDate));
         setTime(updateTime(rawTime));
         setIsRemindOn(true);
       }
+      // 카테고리 초기 선택값
+      setSelectedCategory(prevData.categoryResponse?.name ?? null);
     }
   }, [prevData]);
📝 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
useEffect(() => {
if (prevData) {
setMemo(prevData.memo || '');
if (prevData.remindAt) {
const [rawDate, rawTime] = prevData.remindAt.split('T');
setDate(updateDate(rawDate));
setTime(updateTime(rawTime));
setIsRemindOn(true);
}
}
}, [prevData]);
useEffect(() => {
if (prevData) {
setMemo(prevData.memo || '');
if (prevData.remindAt) {
const [rawDate, rawTime] = prevData.remindAt.split('T');
setDate(updateDate(rawDate));
setTime(updateTime(rawTime));
setIsRemindOn(true);
}
// 카테고리 초기 선택값
setSelectedCategory(prevData.categoryResponse?.name ?? null);
}
}, [prevData]);
🤖 Prompt for AI Agents
In apps/client/src/shared/components/cardEditModal/CardEditModal.tsx around
lines 112 to 124, when prevData is present you currently set memo and remind
values but do not initialize the category selection; add setting of the category
state (e.g., setCategory(prevData.categoryId or prevData.category)) so the UI
reflects the existing selection. Also avoid storing date/time in a
presentation-only format: instead of
setDate(updateDate(rawDate))/setTime(updateTime(rawTime)), store normalized
ISO/raw parts (e.g., setDate(rawDate) and setTime(rawTime) or a consistent
yyyy-mm-dd / HH:mm format) so save logic doesn't need extra re-conversion when
persisting; if you must present dot-formatted strings, generate them only for
display, not for internal state.

@constantly-dev constantly-dev merged commit c166ad4 into develop Sep 13, 2025
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api api 연결

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Api] 아티클 수정/삭제 API 연결 & 수정용 이전 데이터 조회 데이터 reset 기능 추가

2 participants