Conversation
|
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 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. 📒 Files selected for processing (1)
Walkthrough아티클 편집 흐름에 상세조회/수정 API 훅들을 추가하고 편집 시 상세 데이터를 선조회하여 편집 모달에 전달하도록 페이지·훅·타입·컴포넌트 시그니처를 변경함. 또한 OG 메타 조회 유틸·훅, 날짜/시간 포맷 유틸, tsconfig lib 확장 및 InfoBox 이미지 렌더링 수정이 포함됨. Changes
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)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
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 Pre-merge checks❌ Failed checks (3 warnings)
✅ Passed checks (2 passed)
|
There was a problem hiding this comment.
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}주석을 해제하고,CardEditModal이ArticleDetailResponse를 받도록 시그니처를 확장하면 수정 초기값 주입이 쉬워집니다. 필요시 타입/프롭스 변경 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의
getArticleDetail을return 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
📒 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: 상세 조회 훅 도입 👍북마크에서도 동일 패턴으로 맞춘 점 좋습니다.
| onEdit={(id) => { | ||
| getArticleDetail(id); | ||
| setIsEditOpen(true); |
There was a problem hiding this comment.
onEdit에 넘어오는 id가 articleId가 아닐 가능성 높음 (현재 categoryId를 전달 중) → 잘못된 상세 조회 호출 위험
위쪽 카드 옵션 클릭에서 openMenu(article.category.categoryId, …)로 열고 있어, OptionsMenuPortal의 onEdit(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).
| export const getArticleDetail = async (articleId: number) => { | ||
| const { data } = await apiRequest.get(`/api/v1/articles/${articleId}`); | ||
| return data; | ||
| }; |
There was a problem hiding this comment.
getArticleDetail 반환 형태 불일치 → 소비 측 타입 불일치 위험
다른 API(getDashboardCategories, getAcorns)는 data.data를 반환하지만 여기선 data 전체를 반환합니다. useGetArticleDetail이 ArticleDetailResponse를 기대하므로 런타임 오류를 유발할 소지가 큽니다.
-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.
| 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.
| export const putEditArticle = async ( | ||
| articleId: number, | ||
| editArticleData: EditArticleRequest | ||
| ) => { | ||
| const response = await apiRequest.put(`/api/v1/articles/${articleId}`, { | ||
| editArticleData, | ||
| }); | ||
| return response; | ||
| }; |
There was a problem hiding this comment.
💡 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.
| export const usePutEditArticle = () => { | ||
| return useMutation({ | ||
| mutationFn: ({ | ||
| articleId, | ||
| editArticleData, | ||
| }: { | ||
| articleId: number; | ||
| editArticleData: EditArticleRequest; | ||
| }) => putEditArticle(articleId, editArticleData), | ||
| }); | ||
| }; |
There was a problem hiding this comment.
🛠️ 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),
});
};원하시면 useQueryClient로 invalidateQueries(['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.
| 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.
| email: string; | ||
| remindDefault: string; | ||
| fcmToken: string; |
There was a problem hiding this comment.
헛 , 로 되어있었나보군요! 빌드에러는 안났었던데,,
70018e6 to
05c7db6
Compare
|
✅ Storybook chromatic 배포 확인: |
There was a problem hiding this comment.
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), }); };원하시면
useQueryClient로invalidateQueries(['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
📒 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 포함해) 다시 실행한 출력 또는 해당 결과를 붙여주거나, 제가 재실행할 수 있도록 저장소 접근/환경을 제공하세요.
| 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, | ||
| }); | ||
| }; |
There was a problem hiding this comment.
🛠️ 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.
| 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.
| const { meta, loading, error } = usePageMeta( | ||
| 'https://www.notion.so/PinBack-23927450eb1c8080a5a1f84a9d483aa9' | ||
| ); |
There was a problem hiding this comment.
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.
| 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).
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
packages/design-system/src/components/infobox/InfoBox.tsx (1)
13-13: Tailwind 클래스 오타: items-left → items-startitems-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
📒 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]"> | |||
There was a problem hiding this comment.
🛠️ 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.
| <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.
| <img | ||
| className="h-[4.4rem] w-[4.4rem] rounded-[0.4rem] object-cover" | ||
| src={imgUrl} | ||
| /> |
There was a problem hiding this comment.
🛠️ 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.
There was a problem hiding this comment.
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가 아닐 가능성이 큽니다. 과거 코멘트와 동일 이슈입니다. 모달은 상세 조회 성공 시에만 여세요. [중복 코멘트]아래처럼 수정하세요.
- 이 블록(모달 오픈 타이밍) 수정:
- 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(); + }, + }); + }}
- 다른 위치이지만 핵심 원인(넘기는 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 (사소한 네이밍 제안)
mutate에getArticleDetail라는 이름을 부여하면 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
📒 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상단 유효성/포맷 보완 패치 적용 시 함께 동작 확인 부탁드립니다.
적용 후 저장 성공/실패 플로우, 토스트, 리스트 갱신 여부를 수동 시나리오 테스트로 확인해주세요.
| 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); | ||
| }, | ||
| } | ||
| ); | ||
| }; |
There was a problem hiding this comment.
remindTime 포맷 오류(YYYY.MM.DD 사용) + 유효성/카테고리 검증 부재
useEffect에서date를YYYY.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.
| 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]); | ||
|
|
There was a problem hiding this comment.
🛠️ 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.
| 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.
📌 Related Issues
📄 Tasks
⭐ 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
Chores