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
57 changes: 38 additions & 19 deletions apps/client/src/pages/myBookmark/MyBookmark.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,63 @@
import { REMIND_MOCK_DATA } from "@pages/remind/constants";
import { Badge, Card } from "@pinback/design-system/ui";
import { useState } from "react";
import { Badge, Card } from '@pinback/design-system/ui';
import { useState } from 'react';
import {
useGetBookmarkArticles,
useGetBookmarkUnreadArticles,
} from './apis/queries';

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

const handleBadgeClick = (badgeType: string) => {
const { data: readArticles } = useGetBookmarkArticles(1, 10);
const { data: unreadArticles } = useGetBookmarkUnreadArticles(1, 10);

const handleBadgeClick = (badgeType: 'all' | 'notRead') => {
setActiveBadge(badgeType);
};

return (
<div className="flex flex-col pl-[8rem] py-[5.2rem]">
<div className="flex flex-col py-[5.2rem] pl-[8rem]">
<p className="head3">나의 북마크</p>
<div className="mt-[3rem] flex gap-[2.4rem]">
<Badge
text="전체보기"
countNum={5}
countNum={readArticles?.totalArticle || 0}
onClick={() => handleBadgeClick('all')}
isActive={activeBadge === 'all'}
/>
<Badge
text="안 읽음"
countNum={10}
countNum={readArticles?.totalUnreadArticle || 0}
onClick={() => handleBadgeClick('notRead')}
isActive={activeBadge === 'notRead'}
/>
</div>

<div className="scrollbar-hide mt-[2.6rem] flex flex-wrap gap-[1.6rem] overflow-y-auto scroll-smooth max-w-[104rem]">
<div className="scrollbar-hide mt-[2.6rem] flex max-w-[104rem] flex-wrap gap-[1.6rem] overflow-y-auto scroll-smooth">
{/* TODO: API 연결 후 수정 */}
{REMIND_MOCK_DATA.map((data) => (
<Card
key={data.id}
type="bookmark"
title={data.title}
content={data.content}
category={data.category}
date="2024.08.15"
/>
))}
{activeBadge === 'all' &&
readArticles?.articles.map((article) => (
<Card
key={article.articleId}
type="bookmark"
title={article.url}
content={article.memo}
// category={article.category.categoryName}
date={new Date(article.createdAt).toLocaleDateString('ko-KR')}
/>
))}

{activeBadge === 'notRead' &&
unreadArticles?.articles.map((article) => (
<Card
key={article.articleId}
type="bookmark"
title={article.url}
content={article.memo}
// category={article.}
date={new Date(article.createdAt).toLocaleDateString('ko-KR')}
/>
))}
</div>
</div>
);
Expand Down
15 changes: 15 additions & 0 deletions apps/client/src/pages/myBookmark/apis/axios.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import apiRequest from '@shared/apis/setting/axiosInstance';

export const getBookmarkArticles = async (page: number, size: number) => {
const { data } = await apiRequest.get(
`/api/v1/articles?page=${page}&size=${size}`
);
return data.data;
};

export const getBookmarkUnreadArticles = async (page: number, size: number) => {
const { data } = await apiRequest.get(
`/api/v1/articles/unread?page=${page}&size=${size}`
);
return data.data;
};
Comment on lines +10 to +15
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

axios params 사용 및 반환 타입 명시

동일하게 params 사용과 제네릭으로 안전성을 높여주세요.

-export const getBookmarkUnreadArticles = async (page: number, size: number) => {
-  const { data } = await apiRequest.get(
-    `/api/v1/articles/unread?page=${page}&size=${size}`
-  );
-  return data.data;
-};
+export const getBookmarkUnreadArticles = async (page: number, size: number): Promise<UnreadBookmarkArticleResponse> => {
+  const { data } = await apiRequest.get<ApiResponse<UnreadBookmarkArticleResponse>>(
+    '/api/v1/articles/unread',
+    { params: { page, size } }
+  );
+  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 getBookmarkUnreadArticles = async (page: number, size: number) => {
const { data } = await apiRequest.get(
`/api/v1/articles/unread?page=${page}&size=${size}`
);
return data.data;
};
export const getBookmarkUnreadArticles = async (page: number, size: number): Promise<UnreadBookmarkArticleResponse> => {
const { data } = await apiRequest.get<ApiResponse<UnreadBookmarkArticleResponse>>(
'/api/v1/articles/unread',
{ params: { page, size } }
);
return data.data;
};
🤖 Prompt for AI Agents
In apps/client/src/pages/myBookmark/apis/axios.ts around lines 10 to 15, the
request builds the query string inline and doesn't use axios params or generics
for typing; change the call to use apiRequest.get<TResponse> with a typed
response/interface for the returned shape and pass { params: { page, size } }
instead of interpolating into the URL, and annotate the function return type
(e.g., Promise<YourDataType>) so consumers get compile-time safety.

27 changes: 27 additions & 0 deletions apps/client/src/pages/myBookmark/apis/queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { useQuery, UseQueryResult } from '@tanstack/react-query';
import { AxiosError } from 'axios';
import { getBookmarkArticles, getBookmarkUnreadArticles } from './axios';
import {
BookmarkArticleResponse,
UnreadBookmarkArticleResponse,
} from '@pages/myBookmark/types/api';

export const useGetBookmarkArticles = (
page: number,
size: number
): UseQueryResult<BookmarkArticleResponse, AxiosError> => {
return useQuery({
queryKey: ['bookmarkReadArticles', page, size],
queryFn: () => getBookmarkArticles(page, size),
});
};

export const useGetBookmarkUnreadArticles = (
page: number,
size: number
): UseQueryResult<UnreadBookmarkArticleResponse, AxiosError> => {
return useQuery({
queryKey: ['bookmarkUnreadArticles', page, size],
queryFn: () => getBookmarkUnreadArticles(page, size),
});
};
20 changes: 20 additions & 0 deletions apps/client/src/pages/myBookmark/types/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
interface BookmarkArticle {
articleId: number;
url: string;
memo: string;
createdAt: string;
isRead: boolean;
}

// 북마크 전체 조회
export interface BookmarkArticleResponse {
totalArticle: number;
totalUnreadArticle: number;
articles: BookmarkArticle[];
}

// 북마크 안 읽음 조회
export interface UnreadBookmarkArticleResponse {
totalUnreadArticle: number;
articles: BookmarkArticle[];
}
31 changes: 20 additions & 11 deletions apps/client/src/pages/remind/Remind.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,51 @@
import { Badge, Card } from '@pinback/design-system/ui';
import { useState } from 'react';
import { REMIND_MOCK_DATA } from './constants';
import { useGetRemindArticles } from './apis/queries';
import { formatLocalDateTime } from '@shared/utils/formatDateTime';

const Remind = () => {
const [activeBadge, setActiveBadge] = useState('notRead');
const formattedDate = formatLocalDateTime();

const { data } = useGetRemindArticles(
formattedDate,
activeBadge === 'read',
1,
10
);
Comment on lines +8 to +15
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

쿼리 키가 매 렌더마다 바뀌어 재요청/루프 위험: nowDate를 한 번만 고정하세요.

formatLocalDateTime()가 초 단위까지 포함한 현재시각 문자열을 매 렌더마다 생성합니다. 이 값이 queryKey에 들어가 재렌더마다 키가 바뀌고, 불필요한 재요청이나 갱신 루프를 유발할 수 있습니다. useRef로 컴포넌트 수명 동안 고정하세요.

- import { useState } from 'react';
+ import { useState, useRef } from 'react';

- const formattedDate = formatLocalDateTime();
+ const nowRef = useRef(formatLocalDateTime());
+ const formattedDate = nowRef.current;

Also applies to: 2-2

🤖 Prompt for AI Agents
In apps/client/src/pages/remind/Remind.tsx around lines 8 to 15,
formatLocalDateTime() is called on every render producing a new timestamp that
becomes part of the query key and triggers unnecessary refetch loops; capture
the time once for the component lifetime (e.g., useRef to store
formatLocalDateTime() on first render) and use that stable value in
useGetRemindArticles instead of calling formatLocalDateTime() directly so the
query key no longer changes on each render.


const handleBadgeClick = (badgeType: string) => {
setActiveBadge(badgeType);
};

return (
<div className="flex flex-col pl-[8rem] py-[5.2rem]">
<div className="flex flex-col py-[5.2rem] pl-[8rem]">
<p className="head3">리마인드</p>
<div className="mt-[3rem] flex gap-[2.4rem]">
<Badge
text="안 읽음"
countNum={5}
countNum={data?.unreadArticleCount || 0}
onClick={() => handleBadgeClick('notRead')}
isActive={activeBadge === 'notRead'}
/>
<Badge
text="읽음"
countNum={10}
countNum={data?.readArticleCount || 0}
onClick={() => handleBadgeClick('read')}
isActive={activeBadge === 'read'}
/>
</div>

<div className="scrollbar-hide mt-[2.6rem] flex flex-wrap gap-[1.6rem] overflow-y-auto scroll-smooth max-w-[104rem]">
<div className="scrollbar-hide mt-[2.6rem] flex max-w-[104rem] flex-wrap gap-[1.6rem] overflow-y-auto scroll-smooth">
{/* TODO: API 연결 후 수정 */}
{REMIND_MOCK_DATA.map((data) => (
{data?.articles?.map((article) => (
<Card
key={data.id}
key={article.articleId}
type="remind"
title={data.title}
content={data.content}
timeRemaining={data.timeRemaining}
category={data.category}
title={article.url}
content={article.memo}
timeRemaining={article.remindAt}
category={article.category.categoryName}
/>
))}
</div>
Expand Down
13 changes: 13 additions & 0 deletions apps/client/src/pages/remind/apis/axios.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import apiRequest from '@shared/apis/setting/axiosInstance';

export const getRemindArticles = async (
nowDate: string,
readStatus: boolean,
page: number,
size: number
) => {
const { data } = await apiRequest.get(
`/api/v1/articles/remind?now=${nowDate}&readStatus=${readStatus}&page=${page}&size=${size}`
);
return data.data;
};
Comment on lines +3 to +13
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

쿼리 문자열 직접 합성 지양하고 axios params 사용 + 반환 타입 명시

직접 문자열 합성은 인코딩 이슈(nowDate에 : 등)와 유지보수 비용을 유발합니다. axios params와 제네릭으로 응답 타입을 명확히 해주세요.

+import { ArticleListResponse } from '@pages/remind/types/api';
+
+type ApiResponse<T> = { data: T };
+
 export const getRemindArticles = async (
   nowDate: string,
   readStatus: boolean,
   page: number,
   size: number
-) => {
-  const { data } = await apiRequest.get(
-    `/api/v1/articles/remind?now=${nowDate}&readStatus=${readStatus}&page=${page}&size=${size}`
-  );
+) : Promise<ArticleListResponse> => {
+  const { data } = await apiRequest.get<ApiResponse<ArticleListResponse>>(
+    '/api/v1/articles/remind',
+    { params: { now: nowDate, readStatus, page, size } }
+  );
   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 getRemindArticles = async (
nowDate: string,
readStatus: boolean,
page: number,
size: number
) => {
const { data } = await apiRequest.get(
`/api/v1/articles/remind?now=${nowDate}&readStatus=${readStatus}&page=${page}&size=${size}`
);
return data.data;
};
import { ArticleListResponse } from '@pages/remind/types/api';
type ApiResponse<T> = { data: T };
export const getRemindArticles = async (
nowDate: string,
readStatus: boolean,
page: number,
size: number
): Promise<ArticleListResponse> => {
const { data } = await apiRequest.get<ApiResponse<ArticleListResponse>>(
'/api/v1/articles/remind',
{ params: { now: nowDate, readStatus, page, size } }
);
return data.data;
};
🤖 Prompt for AI Agents
In apps/client/src/pages/remind/apis/axios.ts around lines 3 to 13, the function
builds the query string manually which can cause encoding issues and lacks an
explicit return type; change the axios call to use the params option (passing
nowDate, readStatus, page, size as separate keys) and add a TypeScript generic
for the expected response shape (e.g., ApiResponse<{ data: Article[] }> or the
correct DTO) on apiRequest.get so the function returns a typed value (update the
function signature to return Promise<YourReturnType> and return the typed
response.data).

16 changes: 16 additions & 0 deletions apps/client/src/pages/remind/apis/queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useQuery, UseQueryResult } from '@tanstack/react-query';
import { AxiosError } from 'axios';
import { getRemindArticles } from './axios';
import { ArticleListResponse } from '@pages/remind/types/api';

export const useGetRemindArticles = (
nowDate: string,
readStatus: boolean,
page: number,
size: number
): UseQueryResult<ArticleListResponse, AxiosError> => {
return useQuery({
queryKey: ['remindArticles', nowDate, readStatus, page, size],
queryFn: () => getRemindArticles(nowDate, readStatus, page, size),
});
};
22 changes: 22 additions & 0 deletions apps/client/src/pages/remind/types/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// 리마인드 전체 조회
interface Category {
categoryId: number;
categoryName: string;
categoryColor: string;
}
Comment on lines +2 to +6
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

공유 Category 타입과의 혼동 방지(이름 충돌 회피)

@shared/types/api.ts에도 Category가 있어 의미가 다른 동일 명칭이 혼선을 줄 수 있습니다. 지역 타입명을 구체화하거나(예: RemindCategory) 또는 공유 타입을 확장/별칭 처리하세요.

-interface Category {
+interface RemindCategory {
   categoryId: number;
   categoryName: string;
   categoryColor: string;
 }

그리고 Line 15의 참조를 함께 갱신하세요:

-  category: Category;
+  category: RemindCategory;
🤖 Prompt for AI Agents
In apps/client/src/pages/remind/types/api.ts around lines 2 to 6, the local
interface named Category conflicts with a shared Category type in
@shared/types/api.ts; rename the local type to a more specific name (e.g.,
RemindCategory) or create an alias/extend the shared type, and update any
references (including the use at line 15) to the new name so there is no
ambiguous/duplicate Category identifier.


interface ArticleWithCategory {
articleId: number;
url: string;
memo: string;
createdAt: string;
isRead: boolean;
remindAt: string;
category: Category;
}

export interface ArticleListResponse {
readArticleCount: number;
unreadArticleCount: number;
articles: ArticleWithCategory[];
}
Loading