Skip to content

Commit 9576bba

Browse files
author
chaewon41
committed
feat: 북마크 재클릭 시 폴더에서 뉴스 및 용어 삭제 기능 구현
1 parent 8d26b0d commit 9576bba

File tree

3 files changed

+233
-37
lines changed

3 files changed

+233
-37
lines changed

src/app/study/[id]/page.tsx

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -91,26 +91,20 @@ export default function NewsDetailPage() {
9191
setIsSaveBottomSheetOpen(true);
9292
};
9393

94-
// 기사 저장용 핸들러
95-
const handleSelectCategory = async (categoryId: number | null) => {
96-
if (!newsId || categoryId === null) {
97-
return;
98-
}
99-
100-
try {
101-
// 선택한 폴더에 뉴스 저장 API 호출
102-
const articleId = parseInt(newsId, 10);
103-
await saveNewsToStorage(articleId, [categoryId]);
104-
// 저장 후 저장된 폴더 목록 다시 조회하여 북마크 상태 업데이트
94+
// 기사 저장/삭제 완료 후 북마크 상태 업데이트
95+
const handleToggleComplete = async () => {
96+
const articleId = parseInt(newsId, 10);
97+
if (!isNaN(articleId)) {
10598
await fetchSavedFolders(articleId);
106-
setIsSaveBottomSheetOpen(false);
107-
} catch (err) {
108-
console.error("뉴스 저장 실패:", err);
109-
// 에러 발생 시에도 바텀시트는 닫음 (사용자 경험을 위해)
110-
setIsSaveBottomSheetOpen(false);
11199
}
112100
};
113101

102+
// 기사 저장용 핸들러 (호환성 유지, 실제로는 NewCategoryBottomSheet에서 처리)
103+
const handleSelectCategory = async (categoryId: number | null) => {
104+
// NewCategoryBottomSheet에서 내부적으로 처리하므로 여기서는 아무것도 하지 않음
105+
// 필요시 추가 로직 구현 가능
106+
};
107+
114108
const handleAddNewCategory = () => {
115109
// TODO: 새 카테고리 추가 기능 구현
116110
console.log("새 카테고리 추가");
@@ -461,7 +455,9 @@ export default function NewsDetailPage() {
461455
categories={ARCHIVE_CATEGORIES}
462456
onSelectCategory={handleSelectCategory}
463457
onAddNewCategory={handleAddNewCategory}
458+
onToggleComplete={handleToggleComplete}
464459
itemId={newsId ? parseInt(newsId, 10) : undefined}
460+
folderType="NEWS"
465461
/>
466462

467463
{/* 단어 설명 카드 */}

src/components/study/NewCategoryBottomSheet.tsx

Lines changed: 144 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
"use client";
22

3-
import { useState, useEffect, useCallback } from "react";
3+
import { useState, useEffect, useCallback, useMemo } from "react";
44
import {
55
Sheet,
66
SheetContent,
77
SheetTitle,
88
} from "@/components/ui/sheet";
9-
import { getStorageFolders, createStorageFolder, getStorageFoldersByItemId, type StorageFolder } from "@/lib/api/storage";
9+
import { getStorageFolders, createStorageFolder, getStorageFoldersByItemId, getStorageNews, deleteNewsFromStorage, saveNewsToStorage, getStorageTerms, deleteTermFromStorage, saveTermToStorage, type StorageFolder } from "@/lib/api/storage";
1010

1111
type Category = {
1212
category_id: number;
@@ -18,12 +18,13 @@ type NewCategoryBottomSheetProps = {
1818
open: boolean;
1919
onOpenChange: (open: boolean) => void;
2020
categories: Category[];
21-
onSelectCategory: (categoryId: number | null) => void;
21+
onSelectCategory?: (categoryId: number | null) => void; // 선택적: 내부에서 처리할 수도 있음
2222
onAddNewCategory: () => void;
2323
itemId?: number; // 뉴스 ID 또는 단어 ID
2424
folderType?: "NEWS" | "TERM"; // 폴더 타입
2525
title?: string; // 바텀시트 제목
2626
subtitle?: string; // 바텀시트 부제목
27+
onToggleComplete?: () => void; // 저장/삭제 완료 후 콜백
2728
};
2829

2930
// 새 카테고리 생성 폼 컴포넌트
@@ -222,14 +223,22 @@ export function NewCategoryBottomSheet({
222223
folderType = "NEWS",
223224
title,
224225
subtitle,
226+
onToggleComplete,
225227
}: NewCategoryBottomSheetProps) {
226228
const [isCreatingNewCategory, setIsCreatingNewCategory] = useState(false);
227229
const [newCategoryName, setNewCategoryName] = useState("");
228230
const [folders, setFolders] = useState<Category[]>([]);
231+
const [optimisticFolders, setOptimisticFolders] = useState<Category[] | null>(null);
229232
const [loading, setLoading] = useState(false);
230233
const [savedFolderIds, setSavedFolderIds] = useState<number[]>([]);
231234

232-
// 폴더 목록 조회 함수
235+
// displayCategories: optimisticFolders가 있으면 사용, 없으면 folders가 있으면 folders 사용, 없으면 categories prop 사용
236+
const displayCategories = useMemo(() => {
237+
if (optimisticFolders) return optimisticFolders;
238+
return folders.length > 0 ? folders : categories;
239+
}, [optimisticFolders, folders, categories]);
240+
241+
// 폴더 목록 조회 함수 (로딩 상태 포함)
233242
const fetchFolders = useCallback(async () => {
234243
try {
235244
setLoading(true);
@@ -240,14 +249,33 @@ export function NewCategoryBottomSheet({
240249
count: folder.itemCount,
241250
}));
242251
setFolders(folderList);
252+
return response;
243253
} catch (err) {
244254
console.warn("폴더 목록 조회 실패:", err);
245255
setFolders([]);
256+
throw err;
246257
} finally {
247258
setLoading(false);
248259
}
249260
}, [folderType]);
250261

262+
// 폴더 목록 조회 함수 (백그라운드 동기화용, 로딩 상태 없음)
263+
const syncFolders = useCallback(async () => {
264+
try {
265+
const response = await getStorageFolders(folderType);
266+
const folderList: Category[] = response.data.map((folder: StorageFolder) => ({
267+
category_id: folder.folderId,
268+
name: folder.folderName,
269+
count: folder.itemCount,
270+
}));
271+
setFolders(folderList);
272+
return response;
273+
} catch (err) {
274+
console.warn("폴더 목록 동기화 실패:", err);
275+
throw err;
276+
}
277+
}, [folderType]);
278+
251279
// 저장된 폴더 목록 조회 함수
252280
const fetchSavedFolders = useCallback(async () => {
253281
if (!itemId) {
@@ -281,9 +309,117 @@ export function NewCategoryBottomSheet({
281309
}
282310
}, [open]);
283311

284-
const handleCategorySelect = (categoryId: number | null) => {
285-
onSelectCategory(categoryId);
286-
onOpenChange(false);
312+
// 폴더에서 아이템 삭제 (savedItemId 찾기)
313+
const findSavedItemId = useCallback(async (folderId: number): Promise<number | null> => {
314+
if (!itemId) return null;
315+
316+
try {
317+
if (folderType === "NEWS") {
318+
// 뉴스의 경우: 폴더의 뉴스 목록에서 해당 newsId 찾기
319+
const response = await getStorageNews({ folderId, size: 1000 });
320+
const savedItem = response.data.news.find((item) => item.newsId === itemId);
321+
return savedItem?.savedItemId || null;
322+
} else {
323+
// 용어의 경우: 폴더의 용어 목록에서 해당 termId 찾기
324+
const response = await getStorageTerms({ folderId, size: 1000 });
325+
const savedItem = response.data.terms.find((item) => item.termId === itemId);
326+
return savedItem?.savedItemId || null;
327+
}
328+
} catch (err) {
329+
console.error("savedItemId 조회 실패:", err);
330+
return null;
331+
}
332+
}, [itemId, folderType]);
333+
334+
const handleCategorySelect = async (categoryId: number | null) => {
335+
if (!itemId || categoryId === null) {
336+
if (onSelectCategory) {
337+
onSelectCategory(categoryId);
338+
}
339+
onOpenChange(false);
340+
return;
341+
}
342+
343+
const isSaved = savedFolderIds.includes(categoryId);
344+
345+
// Optimistic update: 로컬 상태 먼저 업데이트
346+
const prevSavedFolderIds = [...savedFolderIds];
347+
const currentDisplayCategories = displayCategories;
348+
const prevDisplayCategories = [...currentDisplayCategories];
349+
350+
if (isSaved) {
351+
// UI 먼저 업데이트 (삭제)
352+
setSavedFolderIds((prev) => prev.filter((id) => id !== categoryId));
353+
setOptimisticFolders(
354+
currentDisplayCategories.map((category) =>
355+
category.category_id === categoryId
356+
? { ...category, count: Math.max(0, (category.count || 0) - 1) }
357+
: category
358+
)
359+
);
360+
} else {
361+
// UI 먼저 업데이트 (저장)
362+
setSavedFolderIds((prev) => [...prev, categoryId]);
363+
setOptimisticFolders(
364+
currentDisplayCategories.map((category) =>
365+
category.category_id === categoryId
366+
? { ...category, count: (category.count || 0) + 1 }
367+
: category
368+
)
369+
);
370+
}
371+
372+
try {
373+
if (isSaved) {
374+
// 이미 저장된 폴더면 삭제
375+
const savedItemId = await findSavedItemId(categoryId);
376+
if (savedItemId) {
377+
if (folderType === "NEWS") {
378+
await deleteNewsFromStorage(savedItemId);
379+
} else {
380+
await deleteTermFromStorage(savedItemId);
381+
}
382+
}
383+
} else {
384+
// 저장되지 않은 폴더면 저장
385+
if (folderType === "NEWS") {
386+
await saveNewsToStorage(itemId, [categoryId]);
387+
} else {
388+
await saveTermToStorage(itemId, [categoryId]);
389+
}
390+
}
391+
392+
// 백그라운드에서 동기화 (에러가 나도 UI는 이미 업데이트됨, 로딩 상태 없이 조용히 업데이트)
393+
Promise.all([
394+
fetchSavedFolders(),
395+
syncFolders(),
396+
]).then(() => {
397+
// 동기화 완료 후 optimistic 상태 초기화
398+
setOptimisticFolders(null);
399+
}).catch((err) => {
400+
console.error("동기화 실패:", err);
401+
// 에러 발생 시 이전 상태로 롤백
402+
setSavedFolderIds(prevSavedFolderIds);
403+
setOptimisticFolders(prevDisplayCategories);
404+
});
405+
406+
// 기존 onSelectCategory 콜백 호출 (호환성 유지)
407+
if (onSelectCategory) {
408+
onSelectCategory(categoryId);
409+
}
410+
411+
// 완료 콜백 호출
412+
if (onToggleComplete) {
413+
onToggleComplete();
414+
}
415+
416+
// 바텀시트는 닫지 않음 (여러 폴더 선택 가능하도록)
417+
} catch (err) {
418+
console.error("폴더 저장/삭제 실패:", err);
419+
// 에러 발생 시 이전 상태로 롤백
420+
setSavedFolderIds(prevSavedFolderIds);
421+
setOptimisticFolders(prevDisplayCategories);
422+
}
287423
};
288424

289425
const handleAddNewCategoryClick = () => {
@@ -328,7 +464,7 @@ export function NewCategoryBottomSheet({
328464
/>
329465
) : (
330466
<CategorySelectList
331-
categories={folders}
467+
categories={displayCategories.length > 0 ? displayCategories : folders.length > 0 ? folders : categories}
332468
onSelectCategory={handleCategorySelect}
333469
onAddNewCategory={handleAddNewCategoryClick}
334470
loading={loading}

0 commit comments

Comments
 (0)