Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 35 additions & 10 deletions apps/client/src/pages/myBookmark/MyBookmark.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,26 @@ import { useAnchoredMenu } from '@shared/hooks/useAnchoredMenu';
import { belowOf } from '@shared/utils/anchorPosition';
import NoArticles from '@pages/myBookmark/components/NoArticles/NoArticles';
import { Icon } from '@pinback/design-system/icons';
import { useQueryClient } from '@tanstack/react-query';
import { usePutArticleReadStatus } from '@shared/apis/queries';

const MyBookmark = () => {
const [activeBadge, setActiveBadge] = useState<'all' | 'notRead'>('all');
const [isEditOpen, setIsEditOpen] = useState(false);

const [searchParams] = useSearchParams();
const category = searchParams.get('category');
const categoryId = searchParams.get('id');
const [isEditOpen, setIsEditOpen] = useState(false);

const queryClient = useQueryClient();
const { data: articles } = useGetBookmarkArticles(0, 20);
const { data: unreadArticles } = useGetBookmarkUnreadArticles(0, 20);
const { data: categoryArticles } = useGetCategoryBookmarkArticles(
categoryId,
1,
10
);
const { mutate: updateToReadStatus } = usePutArticleReadStatus();

const {
state: menu,
Expand All @@ -32,14 +45,6 @@ const MyBookmark = () => {
const getBookmarkTitle = (id: number | null) =>
id == null ? '' : (REMIND_MOCK_DATA.find((d) => d.id === id)?.title ?? '');

const { data: articles } = useGetBookmarkArticles(0, 20);
const { data: unreadArticles } = useGetBookmarkUnreadArticles(0, 20);
const { data: categoryArticles } = useGetCategoryBookmarkArticles(
categoryId,
1,
10
);

const articlesToDisplay =
activeBadge === 'all' ? articles?.articles : unreadArticles?.articles;

Expand Down Expand Up @@ -93,7 +98,27 @@ const MyBookmark = () => {
content={article.memo}
category={article.category.categoryName}
date={new Date(article.createdAt).toLocaleDateString('ko-KR')}
onClick={() => {}}
onClick={() => {
window.open(article.url, '_blank');

updateToReadStatus(article.articleId, {
onSuccess: () => {
// TODO: 쿼리키 팩토리 패턴 적용
queryClient.invalidateQueries({
queryKey: ['bookmarkReadArticles'],
});
queryClient.invalidateQueries({
queryKey: ['bookmarkUnreadArticles'],
});
queryClient.invalidateQueries({
queryKey: ['categoryBookmarkArticles'],
});
},
onError: (error) => {
console.error(error);
},
});
}}
Comment on lines +101 to +121
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

새 탭 열기 보안 + 읽음 상태 중복 업데이트 방지.

Remind와 동일한 개선이 필요합니다. reverse tabnabbing 차단, notRead에서만 업데이트 호출 권장.

-              onClick={() => {
-                window.open(article.url, '_blank');
-
-                updateToReadStatus(article.articleId, {
-                  onSuccess: () => {
-                    // TODO: 쿼리키 팩토리 패턴 적용
-                    queryClient.invalidateQueries({
-                      queryKey: ['bookmarkReadArticles'],
-                    });
-                    queryClient.invalidateQueries({
-                      queryKey: ['bookmarkUnreadArticles'],
-                    });
-                    queryClient.invalidateQueries({
-                      queryKey: ['categoryBookmarkArticles'],
-                    });
-                  },
-                  onError: (error) => {
-                    console.error(error);
-                  },
-                });
-              }}
+              onClick={() => {
+                const win = window.open(
+                  article.url,
+                  '_blank',
+                  'noopener,noreferrer'
+                );
+                if (win) win.opener = null;
+
+                if (activeBadge === 'notRead') {
+                  updateToReadStatus(article.articleId, {
+                    onSuccess: () => {
+                      // TODO: 쿼리키 팩토리 패턴 적용
+                      queryClient.invalidateQueries({
+                        queryKey: ['bookmarkReadArticles'],
+                      });
+                      queryClient.invalidateQueries({
+                        queryKey: ['bookmarkUnreadArticles'],
+                      });
+                      queryClient.invalidateQueries({
+                        queryKey: ['categoryBookmarkArticles'],
+                      });
+                    },
+                    onError: (error) => {
+                      console.error(error);
+                    },
+                  });
+                }
+              }}
📝 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
onClick={() => {
window.open(article.url, '_blank');
updateToReadStatus(article.articleId, {
onSuccess: () => {
// TODO: 쿼리키 팩토리 패턴 적용
queryClient.invalidateQueries({
queryKey: ['bookmarkReadArticles'],
});
queryClient.invalidateQueries({
queryKey: ['bookmarkUnreadArticles'],
});
queryClient.invalidateQueries({
queryKey: ['categoryBookmarkArticles'],
});
},
onError: (error) => {
console.error(error);
},
});
}}
onClick={() => {
const win = window.open(
article.url,
'_blank',
'noopener,noreferrer'
);
if (win) win.opener = null;
if (activeBadge === 'notRead') {
updateToReadStatus(article.articleId, {
onSuccess: () => {
// TODO: 쿼리키 팩토리 패턴 적용
queryClient.invalidateQueries({
queryKey: ['bookmarkReadArticles'],
});
queryClient.invalidateQueries({
queryKey: ['bookmarkUnreadArticles'],
});
queryClient.invalidateQueries({
queryKey: ['categoryBookmarkArticles'],
});
},
onError: (error) => {
console.error(error);
},
});
}
}}

onOptionsClick={(e) =>
openMenu(article.articleId, e.currentTarget)
}
Expand Down
37 changes: 28 additions & 9 deletions apps/client/src/pages/remind/Remind.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,23 @@ import { useGetRemindArticles } from '@pages/remind/apis/queries';
import { formatLocalDateTime } from '@shared/utils/formatDateTime';
import NoReadArticles from '@pages/remind/components/noReadArticles/NoReadArticles';
import NoUnreadArticles from '@pages/remind/components/noUnreadArticles/NoUnreadArticles';
import { usePutArticleReadStatus } from '@shared/apis/queries';
import { useQueryClient } from '@tanstack/react-query';

const Remind = () => {
const [isEditOpen, setIsEditOpen] = useState(false);
const [activeBadge, setActiveBadge] = useState<'read' | 'notRead'>('notRead');
const formattedDate = formatLocalDateTime();

const queryClient = useQueryClient();

const { mutate: updateToReadStatus } = usePutArticleReadStatus();
const { data } = useGetRemindArticles(
formattedDate,
activeBadge === 'read',
1,
10
);
Comment on lines +17 to +28
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

formattedDate가 매 렌더마다 바뀌어 쿼리 키가 계속 생성됩니다.

쿼리 키(['remindArticles', formattedDate, ...])가 리렌더마다 달라져 과도한 캐시/리패치가 발생합니다. 마운트 시 1회만 고정하세요.

-  const formattedDate = formatLocalDateTime();
+  const [formattedDate] = useState(() => formatLocalDateTime());
📝 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 [activeBadge, setActiveBadge] = useState<'read' | 'notRead'>('notRead');
const formattedDate = formatLocalDateTime();
const queryClient = useQueryClient();
const { mutate: updateToReadStatus } = usePutArticleReadStatus();
const { data } = useGetRemindArticles(
formattedDate,
activeBadge === 'read',
1,
10
);
const [activeBadge, setActiveBadge] = useState<'read' | 'notRead'>('notRead');
const [formattedDate] = useState(() => formatLocalDateTime());
const queryClient = useQueryClient();
const { mutate: updateToReadStatus } = usePutArticleReadStatus();
const { data } = useGetRemindArticles(
formattedDate,
activeBadge === 'read',
1,
10
);
🤖 Prompt for AI Agents
In apps/client/src/pages/remind/Remind.tsx around lines 17-28, formattedDate is
recomputed on every render which makes the react-query key change each render
and causes excessive cache entries and refetches; compute and store the
formatted date once on mount (e.g., useState(() => formatLocalDateTime()) or
useRef/useMemo with empty deps) and use that stable value in the
useGetRemindArticles query key and call so the key stays constant across
renders.


const {
state: menu,
Expand All @@ -23,15 +37,6 @@ const Remind = () => {

const getItemTitle = (id: number | null) =>
id == null ? '' : (REMIND_MOCK_DATA.find((d) => d.id === id)?.title ?? '');
const [activeBadge, setActiveBadge] = useState<'read' | 'notRead'>('notRead');
const formattedDate = formatLocalDateTime();

const { data } = useGetRemindArticles(
formattedDate,
activeBadge === 'read',
1,
10
);

const handleBadgeClick = (badgeType: 'read' | 'notRead') => {
setActiveBadge(badgeType);
Expand Down Expand Up @@ -72,6 +77,20 @@ const Remind = () => {
onOptionsClick: (e) =>
openMenu(article.category.categoryId, e.currentTarget),
})}
onClick={() => {
window.open(article.url, '_blank');

updateToReadStatus(article.articleId, {
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ['remindArticles'],
});
},
onError: (error) => {
console.error(error);
},
});
}}
Comment on lines +80 to +93
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

새 탭 열기 보안(Reverse Tabnabbing) 및 불필요한 중복 호출 방지.

  • window.open 시 noopener,noreferrer 없이 열면 보안 이슈가 있습니다.
  • 이미 읽은 목록(activeBadge === 'read')에서도 읽음 처리 API를 호출합니다. 불필요한 호출을 피하세요.
-              onClick={() => {
-                window.open(article.url, '_blank');
-
-                updateToReadStatus(article.articleId, {
-                  onSuccess: () => {
-                    queryClient.invalidateQueries({
-                      queryKey: ['remindArticles'],
-                    });
-                  },
-                  onError: (error) => {
-                    console.error(error);
-                  },
-                });
-              }}
+              onClick={() => {
+                const win = window.open(
+                  article.url,
+                  '_blank',
+                  'noopener,noreferrer'
+                );
+                if (win) win.opener = null;
+
+                if (activeBadge === 'notRead') {
+                  updateToReadStatus(article.articleId, {
+                    onSuccess: () => {
+                      queryClient.invalidateQueries({
+                        queryKey: ['remindArticles'],
+                      });
+                    },
+                    onError: (error) => {
+                      console.error(error);
+                    },
+                  });
+                }
+              }}
📝 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
onClick={() => {
window.open(article.url, '_blank');
updateToReadStatus(article.articleId, {
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ['remindArticles'],
});
},
onError: (error) => {
console.error(error);
},
});
}}
onClick={() => {
const win = window.open(
article.url,
'_blank',
'noopener,noreferrer'
);
if (win) win.opener = null;
if (activeBadge === 'notRead') {
updateToReadStatus(article.articleId, {
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ['remindArticles'],
});
},
onError: (error) => {
console.error(error);
},
});
}
}}
🤖 Prompt for AI Agents
In apps/client/src/pages/remind/Remind.tsx around lines 80–93, the onClick
handler opens article.url without noopener,noreferrer which is vulnerable to
reverse tabnabbing and also calls updateToReadStatus even when the article is
already marked read; fix by opening the URL with noopener and noreferrer (e.g.
pass 'noopener,noreferrer' or set newWindow.opener = null) to prevent
tabnabbing, and wrap the updateToReadStatus call in a conditional that skips the
API call when activeBadge === 'read' (only call the update and invalidate
queries when the article is not already read).

/>
))}
</div>
Expand Down
7 changes: 7 additions & 0 deletions apps/client/src/shared/apis/axios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ export const postSignUp = async (responsedata: postSignUpRequest) => {
return data;
};

export const putArticleReadStatus = async (articleId: number) => {
const { data } = await apiRequest.put(
`/api/v1/articles/${articleId}/readStatus`
);
return data;
};
Comment on lines +42 to +47
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

반환 형태 일관화 및 응답 타입 지정.

다른 API는 data.data를 반환합니다. 본 함수만 data를 그대로 반환하면 상층 타입과 어긋날 수 있습니다. 응답 제네릭과 반환 타입을 명시해 주세요.

+import { ArticleReadStatusResponse } from '@shared/types/api';
 
-export const putArticleReadStatus = async (articleId: number) => {
-  const { data } = await apiRequest.put(
-    `/api/v1/articles/${articleId}/readStatus`
-  );
-  return data;
-};
+export const putArticleReadStatus = async (
+  articleId: number
+): Promise<ArticleReadStatusResponse> => {
+  const { data } = await apiRequest.put<{ data: ArticleReadStatusResponse }>(
+    `/api/v1/articles/${articleId}/readStatus`
+  );
+  return data.data;
+};

🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "=== search for putArticleReadStatus ==="
rg -n --hidden --no-ignore -S "putArticleReadStatus" || true

echo "=== search for axios.ts under apps/client/src/shared/apis ==="
rg -n --hidden --no-ignore -S "apps/client/src/shared/apis/.*axios\.ts" || true

echo "=== show apps/client/src/shared/apis/axios.ts (if exists) ==="
if [ -f "apps/client/src/shared/apis/axios.ts" ]; then
  sed -n '1,240p' apps/client/src/shared/apis/axios.ts || true
else
  echo "file not found at apps/client/src/shared/apis/axios.ts; listing axios.ts files in repo..."
  rg -n --hidden --no-ignore -S --files | rg "axios\.ts" || true
fi

echo "=== occurrences of 'return data.data' ==="
rg -n --hidden --no-ignore -S "return\s+data\.data" || true

echo "=== search for ArticleReadStatusResponse type ==="
rg -n --hidden --no-ignore -S "ArticleReadStatusResponse" || true

echo "=== locate apiRequest definition/usages ==="
rg -n --hidden --no-ignore -S "apiRequest" || true

echo "=== show top matches for files defining apiRequest (first 200 lines each) ==="
for f in $(rg -n --hidden --no-ignore -S --files-with-matches "apiRequest" | head -n 10); do
  echo "---- $f ----"
  sed -n '1,200p' "$f" || true
done

Length of output: 11952


반환 형태 일관화 및 응답 타입 지정 — apps/client/src/shared/apis/axios.ts:42-47

다른 API들이 data.data를 반환하므로 이 함수도 ArticleReadStatusResponse 제네릭을 지정하고 data.data를 반환하도록 수정하세요. apps/client/src/shared/types/api.ts에 ArticleReadStatusResponse 타입이 존재합니다.

+import { ArticleReadStatusResponse } from '@shared/types/api';
 
-export const putArticleReadStatus = async (articleId: number) => {
-  const { data } = await apiRequest.put(
-    `/api/v1/articles/${articleId}/readStatus`
-  );
-  return data;
-};
+export const putArticleReadStatus = async (
+  articleId: number
+): Promise<ArticleReadStatusResponse> => {
+  const { data } = await apiRequest.put<{ data: ArticleReadStatusResponse }>(
+    `/api/v1/articles/${articleId}/readStatus`
+  );
+  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 putArticleReadStatus = async (articleId: number) => {
const { data } = await apiRequest.put(
`/api/v1/articles/${articleId}/readStatus`
);
return data;
};
import { ArticleReadStatusResponse } from '@shared/types/api';
export const putArticleReadStatus = async (
articleId: number
): Promise<ArticleReadStatusResponse> => {
const { data } = await apiRequest.put<{ data: ArticleReadStatusResponse }>(
`/api/v1/articles/${articleId}/readStatus`
);
return data.data;
};
🤖 Prompt for AI Agents
In apps/client/src/shared/apis/axios.ts around lines 42 to 47, the
putArticleReadStatus function currently returns data directly but should match
other APIs by using the ArticleReadStatusResponse generic and returning
data.data; update the apiRequest.put call to
apiRequest.put<ArticleReadStatusResponse>(...) (import ArticleReadStatusResponse
from apps/client/src/shared/types/api.ts if not already imported) and change the
return to return data.data so the function returns the typed payload.


export const deleteCategory = async (id: number) => {
const response = await apiRequest.delete(`/api/v1/categories/${id}`);
return response;
Expand Down
26 changes: 23 additions & 3 deletions apps/client/src/shared/apis/queries.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
import { useMutation, useQuery, UseQueryResult } from '@tanstack/react-query';
import {
useMutation,
UseMutationResult,
useQuery,
UseQueryResult,
} from '@tanstack/react-query';
import {
deleteCategory,
getDashboardCategories,
postCategory,
postSignUp,
postSignUpRequest,
putCategory,
getAcorns,
putArticleReadStatus,
} from '@shared/apis/axios';
import { AxiosError } from 'axios';
import { DashboardCategoriesResponse, AcornsResponse } from '@shared/types/api';
import { getAcorns } from './axios';
import {
DashboardCategoriesResponse,
AcornsResponse,
ArticleReadStatusResponse,
} from '@shared/types/api';

export const useGetDashboardCategories = (): UseQueryResult<
DashboardCategoriesResponse,
Expand Down Expand Up @@ -63,3 +73,13 @@ export const usePostSignUp = () => {
},
});
};

export const usePutArticleReadStatus = (): UseMutationResult<
ArticleReadStatusResponse,
AxiosError,
number
> => {
return useMutation({
mutationFn: (articleId: number) => putArticleReadStatus(articleId),
});
};
5 changes: 5 additions & 0 deletions apps/client/src/shared/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,8 @@ export type AcornsResponse = {
acornCount: number;
remindDateTime: string;
};

export interface ArticleReadStatusResponse {
acornCount: number;
acornCollected: boolean;
}
Comment on lines +16 to +19
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

API 응답 래핑 일관성 점검 필요 (data vs data.data).

axios.ts의 다른 API들은 data.data를 반환하지만, putArticleReadStatusdata를 그대로 반환합니다. 백엔드 응답이 공통 래퍼({ data: ... })인지 확인 후 타입과 반환 형태를 일치시켜 주세요. 현재 ArticleReadStatusResponse 사용부에서 런타임 타입 미스매치 가능성이 있습니다.

아래처럼 axios 측을 통일하는 것을 권장합니다(실제 응답 스키마에 맞춰 조정).

// apps/client/src/shared/apis/axios.ts
+import { ArticleReadStatusResponse } from '@shared/types/api';

-export const putArticleReadStatus = async (articleId: number) => {
-  const { data } = await apiRequest.put(
-    `/api/v1/articles/${articleId}/readStatus`
-  );
-  return data;
-};
+export const putArticleReadStatus = async (
+  articleId: number
+): Promise<ArticleReadStatusResponse> => {
+  const { data } = await apiRequest.put<{ data: ArticleReadStatusResponse }>(
+    `/api/v1/articles/${articleId}/readStatus`
+  );
+  return data.data;
+};

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== find definitions/usages ==="
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' "ArticleReadStatusResponse" || true
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' "putArticleReadStatus" || true
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' "apiRequest.put" || true

echo "=== show types file (if exists) ==="
if [ -f apps/client/src/shared/types/api.ts ]; then
  sed -n '1,240p' apps/client/src/shared/types/api.ts || true
else
  echo "apps/client/src/shared/types/api.ts not found"
fi

echo "=== show axios api file (if exists) ==="
if [ -f apps/client/src/shared/apis/axios.ts ]; then
  sed -n '1,320p' apps/client/src/shared/apis/axios.ts || true
else
  echo "apps/client/src/shared/apis/axios.ts not found"
fi

echo "=== scan for return patterns in apis directory ==="
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' "return data;|return data.data;" apps/client/src/shared/apis -S || true

echo "=== show occurrences of ArticleReadStatusResponse usage ==="
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' "ArticleReadStatusResponse" -S || true

echo "=== show occurrences of axios response wrapper typing (\"{ data:\") ==="
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' \"<\\{ data: [^}]+\\}\" -S || true || true

Length of output: 2573


API 응답 래핑 불일치 — putArticleReadStatus 반환형 일관화 필요

검증 결과 apps/client/src/shared/apis/axios.ts에서 대부분 엔드포인트는 data.data를 반환하나 putArticleReadStatus는 data를 그대로 반환하고 있어 ArticleReadStatusResponse와 런타임 불일치가 발생할 수 있습니다. 아래처럼 axios 측 반환을 통일하세요.

  • 수정(권장): apps/client/src/shared/apis/axios.ts의 putArticleReadStatus를 data.data 반환으로 변경
// apps/client/src/shared/apis/axios.ts
+import { ArticleReadStatusResponse } from '@shared/types/api';

-export const putArticleReadStatus = async (articleId: number) => {
-  const { data } = await apiRequest.put(
-    `/api/v1/articles/${articleId}/readStatus`
-  );
-  return data;
-};
+export const putArticleReadStatus = async (
+  articleId: number
+): Promise<ArticleReadStatusResponse> => {
+  const { data } = await apiRequest.put<{ data: ArticleReadStatusResponse }>(
+    `/api/v1/articles/${articleId}/readStatus`
+  );
+  return data.data;
+};
  • 추가 검토: postSignUp / postCategory / putCategory 등에서도 반환형(response / data / data.data)이 혼재되어 있으니 전사 정책(예: 모든 API는 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 interface ArticleReadStatusResponse {
acornCount: number;
acornCollected: boolean;
}
import { ArticleReadStatusResponse } from '@shared/types/api';
export const putArticleReadStatus = async (
articleId: number
): Promise<ArticleReadStatusResponse> => {
const { data } = await apiRequest.put<{ data: ArticleReadStatusResponse }>(
`/api/v1/articles/${articleId}/readStatus`
);
return data.data;
};
🤖 Prompt for AI Agents
In apps/client/src/shared/types/api.ts around lines 16 to 19, the
ArticleReadStatusResponse interface is defined but the axios wrapper
putArticleReadStatus currently returns the full axios response (data) instead of
the inner payload (data.data), causing a runtime type mismatch; update
apps/client/src/shared/apis/axios.ts so putArticleReadStatus returns
response.data.data (i.e., the inner payload matching ArticleReadStatusResponse)
and ensure the function signature reflects that return type; additionally scan
and normalize other API wrappers (postSignUp/postCategory/putCategory, etc.) to
consistently return data.data per project policy.

Comment on lines +15 to +19
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

요거 /aritcles/{articleId}/readStatus메서드인 것 같은데
받는데이터가 해당 타입이 맞을까요?? api 노션상에서는 다음과 같아서요!

 "isRead" : true, 
        "finalAcornCount": 1, //현재 최종 도토리 개수
        "isCollected": false // 해당 요청으로 인해 도토리가 수집되었는지 여부

8 changes: 6 additions & 2 deletions packages/design-system/src/components/card/BaseCard.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
interface BaseCardProps {
onClick?: () => void;
children: React.ReactNode;
}

const BaseCard = ({ children }: BaseCardProps) => {
const BaseCard = ({ children, onClick }: BaseCardProps) => {
Comment on lines 1 to +6
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

그냥 onClick이 아닌, 좀더 네이밍이 어떤 기능인지에 맞게 지어주면 좋을 것 같다는 생각도 듭니다!

return (
<div className="border-gray200 w-[24.8rem] overflow-hidden rounded-[1.2rem] border bg-white">
<div
onClick={onClick}
className="border-gray200 w-[24.8rem] overflow-hidden rounded-[1.2rem] border bg-white"
>
Comment on lines +2 to +11
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

클릭 가능한 div의 접근성/키보드 조작 지원 보완.

onClick이 있을 때 role/tabIndex/키보드 핸들링이 없어 키보드 사용자가 사용할 수 없습니다. 최소한 Enter/Space를 지원하고 시각적으로도 클릭 가능함을 표현해 주세요.

-const BaseCard = ({ children, onClick }: BaseCardProps) => {
+const BaseCard = ({ children, onClick }: BaseCardProps) => {
+  const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
+    if (!onClick) return;
+    if (e.key === 'Enter' || e.key === ' ') {
+      e.preventDefault();
+      onClick();
+    }
+  };
   return (
-    <div
-      onClick={onClick}
-      className="border-gray200 w-[24.8rem] overflow-hidden rounded-[1.2rem] border bg-white"
-    >
+    <div
+      onClick={onClick}
+      onKeyDown={handleKeyDown}
+      role={onClick ? 'button' : undefined}
+      tabIndex={onClick ? 0 : undefined}
+      className={`border-gray200 w-[24.8rem] overflow-hidden rounded-[1.2rem] border bg-white ${onClick ? 'cursor-pointer' : ''}`}
+    >
📝 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
onClick?: () => void;
children: React.ReactNode;
}
const BaseCard = ({ children }: BaseCardProps) => {
const BaseCard = ({ children, onClick }: BaseCardProps) => {
return (
<div className="border-gray200 w-[24.8rem] overflow-hidden rounded-[1.2rem] border bg-white">
<div
onClick={onClick}
className="border-gray200 w-[24.8rem] overflow-hidden rounded-[1.2rem] border bg-white"
>
onClick?: () => void;
children: React.ReactNode;
}
const BaseCard = ({ children, onClick }: BaseCardProps) => {
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
if (!onClick) return;
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onClick();
}
};
return (
<div
onClick={onClick}
onKeyDown={handleKeyDown}
role={onClick ? 'button' : undefined}
tabIndex={onClick ? 0 : undefined}
className={`border-gray200 w-[24.8rem] overflow-hidden rounded-[1.2rem] border bg-white ${onClick ? 'cursor-pointer' : ''}`}
>
🤖 Prompt for AI Agents
In packages/design-system/src/components/card/BaseCard.tsx around lines 2 to 11,
the div with onClick is not keyboard-accessible or visually indicated as
interactive; when onClick is provided, make the element behave like a button by
conditionally adding role="button", tabIndex={0}, and a keydown handler that
triggers the onClick callback for Enter and Space keys, add a visible focus
style and cursor-pointer class to indicate clickability, and ensure no
interactive attributes are added when onClick is undefined.

{children}
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ const MyBookmarkCard = ({
category,
imageUrl,
date,
onClick,
onOptionsClick,
}: MyBookmarkCardProps) => {
return (
<BaseCard>
<BaseCard onClick={onClick}>
<div className="flex h-[12rem] w-full items-center justify-center overflow-hidden bg-[#F8F8FA]">
Comment on lines +21 to 26
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

옵션 버튼 클릭 버블링 방지 누락.

RemindCard와 동일하게 옵션 버튼 클릭이 카드 onClick으로 전파될 수 있습니다. 전파를 막아 주세요.

-          <button
+          <button
             type="button"
             aria-label="카테고리 상세"
             className="cursor-pointer self-start"
-            onClick={(e) => onOptionsClick?.(e)}
+            onClick={(e) => {
+              e.stopPropagation();
+              onOptionsClick?.(e);
+            }}
           >
📝 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
onClick,
onOptionsClick,
}: MyBookmarkCardProps) => {
return (
<BaseCard>
<BaseCard onClick={onClick}>
<div className="flex h-[12rem] w-full items-center justify-center overflow-hidden bg-[#F8F8FA]">
onClick,
onOptionsClick,
}: MyBookmarkCardProps) => {
return (
<BaseCard onClick={onClick}>
<div className="flex h-[12rem] w-full items-center justify-center overflow-hidden bg-[#F8F8FA]">
<button
type="button"
aria-label="카테고리 상세"
className="cursor-pointer self-start"
onClick={(e) => {
e.stopPropagation();
onOptionsClick?.(e);
}}
>
🤖 Prompt for AI Agents
In packages/design-system/src/components/card/MyBookmarkCard.tsx around lines 21
to 26, the options button click currently bubbles up and triggers the card's
onClick (same as RemindCard); update the options button click handler to accept
the click event and call event.stopPropagation() before invoking the existing
onOptionsClick callback (or guard if undefined), ensuring the handler is passed
to the options button element so clicks do not propagate to the BaseCard
onClick.

{imageUrl ? (
<img src={imageUrl} className="h-full w-full object-cover" />
Expand Down
3 changes: 2 additions & 1 deletion packages/design-system/src/components/card/RemindCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ const RemindCard = ({
category,
imageUrl,
timeRemaining,
onClick,
onOptionsClick,
}: RemindCardProps) => {
return (
<BaseCard>
<BaseCard onClick={onClick}>
<div className="bg-gray900 flex items-center gap-[0.4rem] py-[1.2rem] pl-[1.6rem] text-sm text-white">
Comment on lines +21 to 26
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

옵션 버튼 클릭 시 카드 onClick까지 전파되는 버그 가능성.

카드에 onClick이 추가되어 옵션 버튼 클릭이 부모로 버블링될 수 있습니다. 옵션 버튼 onClick에서 e.stopPropagation()을 호출해 주세요.

-          <button
+          <button
             type="button"
             aria-label="카테고리 상세"
             className="cursor-pointer self-start"
-            onClick={(e) => onOptionsClick?.(e)}
+            onClick={(e) => {
+              e.stopPropagation();
+              onOptionsClick?.(e);
+            }}
           >
📝 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
onClick,
onOptionsClick,
}: RemindCardProps) => {
return (
<BaseCard>
<BaseCard onClick={onClick}>
<div className="bg-gray900 flex items-center gap-[0.4rem] py-[1.2rem] pl-[1.6rem] text-sm text-white">
onClick,
onOptionsClick,
}: RemindCardProps) => {
return (
<BaseCard onClick={onClick}>
<div className="bg-gray900 flex items-center gap-[0.4rem] py-[1.2rem] pl-[1.6rem] text-sm text-white">
<button
type="button"
aria-label="카테고리 상세"
className="cursor-pointer self-start"
onClick={(e) => {
e.stopPropagation();
onOptionsClick?.(e);
}}
>
🤖 Prompt for AI Agents
In packages/design-system/src/components/card/RemindCard.tsx around lines 21 to
26, the options button click can bubble up to the card's onClick; update the
options button's onClick handler to accept the event parameter and call
e.stopPropagation() (typed as React.MouseEvent) before executing onOptionsClick
to prevent the parent card click from firing.

<Icon name="ic_clock_active" />
<span className="body2-m text-main400 mr-[0.2rem]">
Expand Down
Loading