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
24 changes: 18 additions & 6 deletions apps/client/src/pages/myBookmark/MyBookmark.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ 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';
import {
useGetArticleDetail,
usePutArticleReadStatus,
} from '@shared/apis/queries';

const MyBookmark = () => {
const [activeBadge, setActiveBadge] = useState<'all' | 'notRead'>('all');
Expand All @@ -29,9 +32,13 @@ const MyBookmark = () => {
const { data: unreadArticles } = useGetBookmarkUnreadArticles(0, 20);
const { data: categoryArticles } = useGetCategoryBookmarkArticles(
categoryId,
activeBadge === 'all',
1,
10
);
const { mutate: getArticleDetail, data: articleDetail } =
useGetArticleDetail();

const { mutate: updateToReadStatus } = usePutArticleReadStatus();

const {
Expand Down Expand Up @@ -119,9 +126,10 @@ const MyBookmark = () => {
},
});
}}
onOptionsClick={(e) =>
openMenu(article.articleId, e.currentTarget)
}
onOptionsClick={(e) => {
e.stopPropagation();
openMenu(article.articleId, e.currentTarget);
}}
/>
))}
</div>
Expand All @@ -135,7 +143,8 @@ const MyBookmark = () => {
containerRef={containerRef}
categoryId={menu.categoryId}
getCategoryName={getBookmarkTitle}
onEdit={() => {
onEdit={(id) => {
getArticleDetail(id);
setIsEditOpen(true);
closeMenu();
}}
Expand All @@ -153,7 +162,10 @@ const MyBookmark = () => {
onClick={() => setIsEditOpen(false)}
/>
<div className="absolute inset-0 flex items-center justify-center p-4">
<CardEditModal onClose={() => setIsEditOpen(false)} />
<CardEditModal
onClose={() => setIsEditOpen(false)}
prevData={articleDetail}
/>
</div>
</div>
)}
Expand Down
3 changes: 2 additions & 1 deletion apps/client/src/pages/myBookmark/apis/axios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ export const getBookmarkUnreadArticles = async (page: number, size: number) => {

export const getCategoryBookmarkArticles = async (
categoryId: string | null,
readStatus: boolean,
page: number,
size: number
) => {
const { data } = await apiRequest.get(
`/api/v1/articles/category?categoryId=${categoryId}&page=${page}&size=${size}`
`/api/v1/articles/category?categoryId=${categoryId}&read-status=${readStatus}&page=${page}&size=${size}`
);
return data.data;
};
4 changes: 3 additions & 1 deletion apps/client/src/pages/myBookmark/apis/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,14 @@ export const useGetBookmarkUnreadArticles = (

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,
});
};
Comment on lines 34 to 46
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

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

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

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

export const useGetCategoryBookmarkArticles = (
  categoryId: string | null,
  readStatus: boolean,
  page: number,
  size: number
): UseQueryResult<CategoryBookmarkArticleResponse, AxiosError> => {
  return useQuery({
-    queryKey: ['categoryBookmarkArticles', categoryId, page, size],
+    queryKey: ['categoryBookmarkArticles', categoryId, readStatus, page, size],
    queryFn: () =>
      getCategoryBookmarkArticles(categoryId, readStatus, page, size),
    enabled: !!categoryId,
  });
};
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

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

15 changes: 12 additions & 3 deletions apps/client/src/pages/remind/Remind.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ 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 {
usePutArticleReadStatus,
useGetArticleDetail,
} from '@shared/apis/queries';
import { useQueryClient } from '@tanstack/react-query';

const Remind = () => {
Expand Down Expand Up @@ -37,6 +40,8 @@ const Remind = () => {

const getItemTitle = (id: number | null) =>
id == null ? '' : (REMIND_MOCK_DATA.find((d) => d.id === id)?.title ?? '');
const { mutate: getArticleDetail, data: articleDetail } =
useGetArticleDetail();

const handleBadgeClick = (badgeType: 'read' | 'notRead') => {
setActiveBadge(badgeType);
Expand Down Expand Up @@ -104,7 +109,8 @@ const Remind = () => {
containerRef={containerRef}
categoryId={menu.categoryId}
getCategoryName={getItemTitle}
onEdit={() => {
onEdit={(id) => {
getArticleDetail(id);
setIsEditOpen(true);
Comment on lines +112 to 114
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

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

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

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

closeMenu();
}}
Expand All @@ -122,7 +128,10 @@ const Remind = () => {
onClick={() => setIsEditOpen(false)}
/>
<div className="absolute inset-0 flex items-center justify-center p-4">
<CardEditModal onClose={() => setIsEditOpen(false)} />
<CardEditModal
onClose={() => setIsEditOpen(false)}
prevData={articleDetail}
/>
</div>
</div>
)}
Expand Down
16 changes: 16 additions & 0 deletions apps/client/src/shared/apis/axios.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import apiRequest from '@shared/apis/setting/axiosInstance';
import { EditArticleRequest } from '@shared/types/api';
import { formatLocalDateTime } from '@shared/utils/formatDateTime';

export const getDashboardCategories = async () => {
Expand Down Expand Up @@ -46,6 +47,21 @@ export const putArticleReadStatus = async (articleId: number) => {
return data;
};

export const getArticleDetail = async (articleId: number) => {
const { data } = await apiRequest.get(`/api/v1/articles/${articleId}`);
return data.data;
};

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

export const deleteCategory = async (id: number) => {
const response = await apiRequest.delete(`/api/v1/categories/${id}`);
return response;
Expand Down
28 changes: 27 additions & 1 deletion apps/client/src/shared/apis/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,19 @@ import {
postCategory,
postSignUp,
postSignUpRequest,
putEditArticle,
putCategory,
getAcorns,
putArticleReadStatus,
getArticleDetail,
getAcorns,
} from '@shared/apis/axios';
import { AxiosError } from 'axios';
import {
DashboardCategoriesResponse,
AcornsResponse,
EditArticleRequest,
ArticleReadStatusResponse,
ArticleDetailResponse,
} from '@shared/types/api';

export const useGetDashboardCategories = (): UseQueryResult<
Expand Down Expand Up @@ -83,3 +87,25 @@ export const usePutArticleReadStatus = (): UseMutationResult<
mutationFn: (articleId: number) => putArticleReadStatus(articleId),
});
};

export const useGetArticleDetail = (): UseMutationResult<
ArticleDetailResponse,
AxiosError,
number
> => {
return useMutation({
mutationFn: (articleId: number) => getArticleDetail(articleId),
});
};

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

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

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

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

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

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

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

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

105 changes: 92 additions & 13 deletions apps/client/src/shared/components/cardEditModal/CardEditModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,38 @@ import {
validateTime,
} from '@pinback/design-system/ui';
import { cn } from '@pinback/design-system/utils';
import { useState } from 'react';
import {
useGetDashboardCategories,
usePutEditArticle,
} from '@shared/apis/queries';
import { usePageMeta } from '@shared/hooks/usePageMeta';
import { ArticleDetailResponse, EditArticleRequest } from '@shared/types/api';
import { updateDate, updateTime } from '@shared/utils/formatDateTime';
import { useQueryClient } from '@tanstack/react-query';
import { useEffect, useState } from 'react';

export interface CardEditModalProps {
onClose: () => void;
prevData: ArticleDetailResponse | undefined;
}

export default function CardEditModal({ onClose }: CardEditModalProps) {
const [isRemindOn, setIsRemindOn] = useState(true);
const [selected, setSelected] = useState<string | null>(null);
export default function CardEditModal({
onClose,
prevData,
}: CardEditModalProps) {
const { meta } = usePageMeta(
'https://www.notion.so/PinBack-23927450eb1c8080a5a1f84a9d483aa9'
);
const { data: category } = useGetDashboardCategories();
const { mutate: editArticle } = usePutEditArticle();
const queryClient = useQueryClient();

const [isRemindOn, setIsRemindOn] = useState(false);
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const [isPopupOpen, setIsPopupOpen] = useState(false);

// 입력 필드 상태: 서버에서 받아올 데이터
const [title] = useState('');
const [source] = useState('');
const [memo, setMemo] = useState('');
const [categories] = useState<string[]>([]);
const [categoryTitle, setCategoryTitle] = useState('');
const [date, setDate] = useState('');
const [time, setTime] = useState('');
Expand Down Expand Up @@ -62,6 +78,64 @@ export default function CardEditModal({ onClose }: CardEditModalProps) {
setIsRemindOn(checked);
};

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: () => {
queryClient.invalidateQueries({
queryKey: ['remindArticles'],
});
queryClient.invalidateQueries({
queryKey: ['bookmarkReadArticles'],
});
queryClient.invalidateQueries({
queryKey: ['bookmarkUnreadArticles'],
});
queryClient.invalidateQueries({
queryKey: ['categoryBookmarkArticles'],
});
onClose();
},
onError: () => {
setToastIsOpen(true);
},
}
);
};

useEffect(() => {
if (prevData) {
setMemo(prevData.memo || '');
setSelectedCategory(prevData.categoryResponse.categoryName || null);

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

return (
<div className="flex flex-col">
<div
Expand Down Expand Up @@ -101,16 +175,21 @@ export default function CardEditModal({ onClose }: CardEditModalProps) {
</button>
</header>

<InfoBox title={title} source={source} />
<InfoBox
title={meta.title}
source={meta.description}
imgUrl={meta.imgUrl}
/>

<section className="flex flex-col gap-[0.8rem]">
<p className="caption1-sb text-font-black-1">카테고리</p>
<Dropdown
options={categories}
selectedValue={selected}
onChange={(value) => setSelected(value)}
options={
category?.categories.map((category) => category.name) || []
}
selectedValue={selectedCategory}
onChange={(value) => setSelectedCategory(value)}
placeholder="선택해주세요"
onAddItem={() => setIsPopupOpen(true)}
addItemLabel="추가하기"
/>
</section>
Expand Down Expand Up @@ -151,7 +230,7 @@ export default function CardEditModal({ onClose }: CardEditModalProps) {
{timeError && <p className="body3-r text-error">{timeError}</p>}
</section>
{/* TODO: onClick 추후 저장 api 연결후 실패/성공 연결 */}
<Button onClick={() => setToastIsOpen(true)}>저장하기</Button>
<Button onClick={saveData}>저장하기</Button>
</div>
{toastIsOpen && (
<div className="absolute bottom-[2.4rem] left-1/2 -translate-x-1/2">
Expand Down
Loading
Loading