Skip to content

Commit c166ad4

Browse files
Api(client): 아티클 수정 API 연결 (#97)
* feat: 아티클 수정 api axios, query 함수 작성 * feat: 아티클 상세 조회 api axios, query 함수 작성 * feat: article detail 조회 useMutation으로 변경 * fix: read status 바뀐 naming 반영 * feat: tsconfig lib 추가 * feat: og meta data fetch utils & hook 구현 * feat: update date, time utils 추가 * feat: 아티클 수정용 조회 api 연결 * feat: ogimage object cover 추가 * feat: 카테고리 조회 API 연결 * api: 아티클 수정 api 연결 * feat: 카테고리 수정 invalidate query 추가
1 parent 80100db commit c166ad4

File tree

13 files changed

+387
-26
lines changed

13 files changed

+387
-26
lines changed

apps/client/src/pages/myBookmark/MyBookmark.tsx

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ import { belowOf } from '@shared/utils/anchorPosition';
1414
import NoArticles from '@pages/myBookmark/components/NoArticles/NoArticles';
1515
import { Icon } from '@pinback/design-system/icons';
1616
import { useQueryClient } from '@tanstack/react-query';
17-
import { usePutArticleReadStatus } from '@shared/apis/queries';
17+
import {
18+
useGetArticleDetail,
19+
usePutArticleReadStatus,
20+
} from '@shared/apis/queries';
1821

1922
const MyBookmark = () => {
2023
const [activeBadge, setActiveBadge] = useState<'all' | 'notRead'>('all');
@@ -29,9 +32,13 @@ const MyBookmark = () => {
2932
const { data: unreadArticles } = useGetBookmarkUnreadArticles(0, 20);
3033
const { data: categoryArticles } = useGetCategoryBookmarkArticles(
3134
categoryId,
35+
activeBadge === 'all',
3236
1,
3337
10
3438
);
39+
const { mutate: getArticleDetail, data: articleDetail } =
40+
useGetArticleDetail();
41+
3542
const { mutate: updateToReadStatus } = usePutArticleReadStatus();
3643

3744
const {
@@ -119,9 +126,10 @@ const MyBookmark = () => {
119126
},
120127
});
121128
}}
122-
onOptionsClick={(e) =>
123-
openMenu(article.articleId, e.currentTarget)
124-
}
129+
onOptionsClick={(e) => {
130+
e.stopPropagation();
131+
openMenu(article.articleId, e.currentTarget);
132+
}}
125133
/>
126134
))}
127135
</div>
@@ -135,7 +143,8 @@ const MyBookmark = () => {
135143
containerRef={containerRef}
136144
categoryId={menu.categoryId}
137145
getCategoryName={getBookmarkTitle}
138-
onEdit={() => {
146+
onEdit={(id) => {
147+
getArticleDetail(id);
139148
setIsEditOpen(true);
140149
closeMenu();
141150
}}
@@ -153,7 +162,10 @@ const MyBookmark = () => {
153162
onClick={() => setIsEditOpen(false)}
154163
/>
155164
<div className="absolute inset-0 flex items-center justify-center p-4">
156-
<CardEditModal onClose={() => setIsEditOpen(false)} />
165+
<CardEditModal
166+
onClose={() => setIsEditOpen(false)}
167+
prevData={articleDetail}
168+
/>
157169
</div>
158170
</div>
159171
)}

apps/client/src/pages/myBookmark/apis/axios.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@ export const getBookmarkUnreadArticles = async (page: number, size: number) => {
1616

1717
export const getCategoryBookmarkArticles = async (
1818
categoryId: string | null,
19+
readStatus: boolean,
1920
page: number,
2021
size: number
2122
) => {
2223
const { data } = await apiRequest.get(
23-
`/api/v1/articles/category?categoryId=${categoryId}&page=${page}&size=${size}`
24+
`/api/v1/articles/category?categoryId=${categoryId}&read-status=${readStatus}&page=${page}&size=${size}`
2425
);
2526
return data.data;
2627
};

apps/client/src/pages/myBookmark/apis/queries.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,14 @@ export const useGetBookmarkUnreadArticles = (
3333

3434
export const useGetCategoryBookmarkArticles = (
3535
categoryId: string | null,
36+
readStatus: boolean,
3637
page: number,
3738
size: number
3839
): UseQueryResult<CategoryBookmarkArticleResponse, AxiosError> => {
3940
return useQuery({
4041
queryKey: ['categoryBookmarkArticles', categoryId, page, size],
41-
queryFn: () => getCategoryBookmarkArticles(categoryId, page, size),
42+
queryFn: () =>
43+
getCategoryBookmarkArticles(categoryId, readStatus, page, size),
4244
enabled: !!categoryId,
4345
});
4446
};

apps/client/src/pages/remind/Remind.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import { useGetRemindArticles } from '@pages/remind/apis/queries';
99
import { formatLocalDateTime } from '@shared/utils/formatDateTime';
1010
import NoReadArticles from '@pages/remind/components/noReadArticles/NoReadArticles';
1111
import NoUnreadArticles from '@pages/remind/components/noUnreadArticles/NoUnreadArticles';
12-
import { usePutArticleReadStatus } from '@shared/apis/queries';
12+
import {
13+
usePutArticleReadStatus,
14+
useGetArticleDetail,
15+
} from '@shared/apis/queries';
1316
import { useQueryClient } from '@tanstack/react-query';
1417

1518
const Remind = () => {
@@ -37,6 +40,8 @@ const Remind = () => {
3740

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

4146
const handleBadgeClick = (badgeType: 'read' | 'notRead') => {
4247
setActiveBadge(badgeType);
@@ -104,7 +109,8 @@ const Remind = () => {
104109
containerRef={containerRef}
105110
categoryId={menu.categoryId}
106111
getCategoryName={getItemTitle}
107-
onEdit={() => {
112+
onEdit={(id) => {
113+
getArticleDetail(id);
108114
setIsEditOpen(true);
109115
closeMenu();
110116
}}
@@ -122,7 +128,10 @@ const Remind = () => {
122128
onClick={() => setIsEditOpen(false)}
123129
/>
124130
<div className="absolute inset-0 flex items-center justify-center p-4">
125-
<CardEditModal onClose={() => setIsEditOpen(false)} />
131+
<CardEditModal
132+
onClose={() => setIsEditOpen(false)}
133+
prevData={articleDetail}
134+
/>
126135
</div>
127136
</div>
128137
)}

apps/client/src/shared/apis/axios.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import apiRequest from '@shared/apis/setting/axiosInstance';
2+
import { EditArticleRequest } from '@shared/types/api';
23
import { formatLocalDateTime } from '@shared/utils/formatDateTime';
34

45
export const getDashboardCategories = async () => {
@@ -46,6 +47,21 @@ export const putArticleReadStatus = async (articleId: number) => {
4647
return data;
4748
};
4849

50+
export const getArticleDetail = async (articleId: number) => {
51+
const { data } = await apiRequest.get(`/api/v1/articles/${articleId}`);
52+
return data.data;
53+
};
54+
55+
export const putEditArticle = async (
56+
articleId: number,
57+
editArticleData: EditArticleRequest
58+
) => {
59+
const response = await apiRequest.put(`/api/v1/articles/${articleId}`, {
60+
...editArticleData,
61+
});
62+
return response;
63+
};
64+
4965
export const deleteCategory = async (id: number) => {
5066
const response = await apiRequest.delete(`/api/v1/categories/${id}`);
5167
return response;

apps/client/src/shared/apis/queries.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,19 @@ import {
1010
postCategory,
1111
postSignUp,
1212
postSignUpRequest,
13+
putEditArticle,
1314
putCategory,
14-
getAcorns,
1515
putArticleReadStatus,
16+
getArticleDetail,
17+
getAcorns,
1618
} from '@shared/apis/axios';
1719
import { AxiosError } from 'axios';
1820
import {
1921
DashboardCategoriesResponse,
2022
AcornsResponse,
23+
EditArticleRequest,
2124
ArticleReadStatusResponse,
25+
ArticleDetailResponse,
2226
} from '@shared/types/api';
2327

2428
export const useGetDashboardCategories = (): UseQueryResult<
@@ -83,3 +87,25 @@ export const usePutArticleReadStatus = (): UseMutationResult<
8387
mutationFn: (articleId: number) => putArticleReadStatus(articleId),
8488
});
8589
};
90+
91+
export const useGetArticleDetail = (): UseMutationResult<
92+
ArticleDetailResponse,
93+
AxiosError,
94+
number
95+
> => {
96+
return useMutation({
97+
mutationFn: (articleId: number) => getArticleDetail(articleId),
98+
});
99+
};
100+
101+
export const usePutEditArticle = () => {
102+
return useMutation({
103+
mutationFn: ({
104+
articleId,
105+
editArticleData,
106+
}: {
107+
articleId: number;
108+
editArticleData: EditArticleRequest;
109+
}) => putEditArticle(articleId, editArticleData),
110+
});
111+
};

apps/client/src/shared/components/cardEditModal/CardEditModal.tsx

Lines changed: 92 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,38 @@ import {
1313
validateTime,
1414
} from '@pinback/design-system/ui';
1515
import { cn } from '@pinback/design-system/utils';
16-
import { useState } from 'react';
16+
import {
17+
useGetDashboardCategories,
18+
usePutEditArticle,
19+
} from '@shared/apis/queries';
20+
import { usePageMeta } from '@shared/hooks/usePageMeta';
21+
import { ArticleDetailResponse, EditArticleRequest } from '@shared/types/api';
22+
import { updateDate, updateTime } from '@shared/utils/formatDateTime';
23+
import { useQueryClient } from '@tanstack/react-query';
24+
import { useEffect, useState } from 'react';
1725

1826
export interface CardEditModalProps {
1927
onClose: () => void;
28+
prevData: ArticleDetailResponse | undefined;
2029
}
2130

22-
export default function CardEditModal({ onClose }: CardEditModalProps) {
23-
const [isRemindOn, setIsRemindOn] = useState(true);
24-
const [selected, setSelected] = useState<string | null>(null);
31+
export default function CardEditModal({
32+
onClose,
33+
prevData,
34+
}: CardEditModalProps) {
35+
const { meta } = usePageMeta(
36+
'https://www.notion.so/PinBack-23927450eb1c8080a5a1f84a9d483aa9'
37+
);
38+
const { data: category } = useGetDashboardCategories();
39+
const { mutate: editArticle } = usePutEditArticle();
40+
const queryClient = useQueryClient();
41+
42+
const [isRemindOn, setIsRemindOn] = useState(false);
43+
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
2544
const [isPopupOpen, setIsPopupOpen] = useState(false);
2645

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

81+
const saveData = () => {
82+
if (!prevData?.id) {
83+
console.error('Article ID is missing, cannot save.');
84+
setToastIsOpen(true);
85+
return;
86+
}
87+
88+
const editArticleData: EditArticleRequest = {
89+
memo,
90+
categoryId:
91+
category?.categories.find((cat) => cat.name === selectedCategory)?.id ||
92+
-1,
93+
now: new Date().toISOString(),
94+
remindTime: isRemindOn ? `${date}T${time}` : null,
95+
};
96+
97+
editArticle(
98+
{
99+
articleId: prevData?.id,
100+
editArticleData,
101+
},
102+
{
103+
onSuccess: () => {
104+
queryClient.invalidateQueries({
105+
queryKey: ['remindArticles'],
106+
});
107+
queryClient.invalidateQueries({
108+
queryKey: ['bookmarkReadArticles'],
109+
});
110+
queryClient.invalidateQueries({
111+
queryKey: ['bookmarkUnreadArticles'],
112+
});
113+
queryClient.invalidateQueries({
114+
queryKey: ['categoryBookmarkArticles'],
115+
});
116+
onClose();
117+
},
118+
onError: () => {
119+
setToastIsOpen(true);
120+
},
121+
}
122+
);
123+
};
124+
125+
useEffect(() => {
126+
if (prevData) {
127+
setMemo(prevData.memo || '');
128+
setSelectedCategory(prevData.categoryResponse.categoryName || null);
129+
130+
if (prevData.remindAt) {
131+
const [rawDate, rawTime] = prevData.remindAt.split('T');
132+
setDate(updateDate(rawDate));
133+
setTime(updateTime(rawTime));
134+
setIsRemindOn(true);
135+
}
136+
}
137+
}, [prevData]);
138+
65139
return (
66140
<div className="flex flex-col">
67141
<div
@@ -101,16 +175,21 @@ export default function CardEditModal({ onClose }: CardEditModalProps) {
101175
</button>
102176
</header>
103177

104-
<InfoBox title={title} source={source} />
178+
<InfoBox
179+
title={meta.title}
180+
source={meta.description}
181+
imgUrl={meta.imgUrl}
182+
/>
105183

106184
<section className="flex flex-col gap-[0.8rem]">
107185
<p className="caption1-sb text-font-black-1">카테고리</p>
108186
<Dropdown
109-
options={categories}
110-
selectedValue={selected}
111-
onChange={(value) => setSelected(value)}
187+
options={
188+
category?.categories.map((category) => category.name) || []
189+
}
190+
selectedValue={selectedCategory}
191+
onChange={(value) => setSelectedCategory(value)}
112192
placeholder="선택해주세요"
113-
onAddItem={() => setIsPopupOpen(true)}
114193
addItemLabel="추가하기"
115194
/>
116195
</section>
@@ -151,7 +230,7 @@ export default function CardEditModal({ onClose }: CardEditModalProps) {
151230
{timeError && <p className="body3-r text-error">{timeError}</p>}
152231
</section>
153232
{/* TODO: onClick 추후 저장 api 연결후 실패/성공 연결 */}
154-
<Button onClick={() => setToastIsOpen(true)}>저장하기</Button>
233+
<Button onClick={saveData}>저장하기</Button>
155234
</div>
156235
{toastIsOpen && (
157236
<div className="absolute bottom-[2.4rem] left-1/2 -translate-x-1/2">

0 commit comments

Comments
 (0)