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
21 changes: 18 additions & 3 deletions app/(tabs)/(folder)/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { Colors, Typography } from '@/constants/theme';
import { getSavedLinkErrorMessage, useSavedLinks, type SavedLink } from '@/context/saved-links-context';
import { useFolders } from '@/context/folders-context';
import type { AnchorPosition } from '@/components/ui/folder-card';
import { useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { showAlert } from '@/utils/guarded-alert';

type MoreMenuState = {
Expand All @@ -26,7 +26,7 @@ export default function FolderDetailScreen() {
const router = useRouter();
const folderId = Number(id);

const { links, toggleBookmark, assignCategory, updateTitle } = useSavedLinks();
const { links, bookmarkingLinkIds, toggleBookmark, assignCategory, updateTitle } = useSavedLinks();
const { folders, refreshFolders } = useFolders();
const folderLinks = links.filter((l) => l.categoryId === folderId);
const folderName = folders.find((f) => f.id === folderId)?.name ?? '폴더';
Expand All @@ -46,6 +46,20 @@ export default function FolderDetailScreen() {
setMenuState({ visible: true, anchor, linkId });
};

const handleBookmark = useCallback(
async (linkId: number) => {
try {
await toggleBookmark(linkId);
} catch (error) {
showAlert(
'북마크 변경 실패',
getSavedLinkErrorMessage(error, '북마크 상태를 변경하지 못했습니다. 잠시 후 다시 시도해주세요.'),
);
}
},
[toggleBookmark],
);

const handleDelete = async (linkId: number) => {
if (deletingFromFolderIdsRef.current.has(linkId)) {
return;
Expand Down Expand Up @@ -150,7 +164,8 @@ export default function FolderDetailScreen() {
originalUrl={link.originalUrl}
finalUrl={link.finalUrl}
bookmarked={link.isBookmarked}
onBookmark={() => toggleBookmark(link.id)}
bookmarkDisabled={bookmarkingLinkIds.has(link.id)}
onBookmark={() => handleBookmark(link.id)}
onMore={(anchor: AnchorPosition) => handleMore(link.id, anchor)}
/>
))}
Expand Down
7 changes: 3 additions & 4 deletions app/(tabs)/(home)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export default function HomeScreen() {
}>();
const { width: windowWidth } = useWindowDimensions();
const tabBarHeight = useBottomTabBarHeight();
const { links, toggleBookmark, deleteLink, updateTitle } = useSavedLinks();
const { links, bookmarkingLinkIds, toggleBookmark, deleteLink, updateTitle } = useSavedLinks();
const { refreshFolders } = useFolders();
const [menuState, setMenuState] = useState<{ visible: boolean; anchor?: AnchorPosition; linkId?: number }>({ visible: false });
const [editingLink, setEditingLink] = useState<SavedLink | null>(null);
Expand Down Expand Up @@ -284,9 +284,8 @@ export default function HomeScreen() {
originalUrl={link.originalUrl}
finalUrl={link.finalUrl}
bookmarked={link.isBookmarked}
onBookmark={() => {
void handleBookmark(link.id);
}}
bookmarkDisabled={bookmarkingLinkIds.has(link.id)}
onBookmark={() => handleBookmark(link.id)}
onMore={(anchor) => handleMore(link.id, anchor)}
/>
))}
Expand Down
6 changes: 3 additions & 3 deletions app/(tabs)/(home)/saved-links.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export default function SavedLinksScreen() {
hasNext,
refreshLinks,
loadMoreLinks,
bookmarkingLinkIds,
toggleBookmark,
deleteLink,
updateTitle,
Expand Down Expand Up @@ -227,9 +228,8 @@ export default function SavedLinksScreen() {
originalUrl={item.originalUrl}
finalUrl={item.finalUrl}
bookmarked={item.isBookmarked}
onBookmark={() => {
void handleBookmark(item.id);
}}
bookmarkDisabled={bookmarkingLinkIds.has(item.id)}
onBookmark={() => handleBookmark(item.id)}
onMore={(anchor) => openMoreMenu(item, anchor)}
/>
)}
Expand Down
21 changes: 16 additions & 5 deletions components/ui/card-link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ export interface CardLinkProps {
bookmarked?: boolean;
icon?: boolean;
disabled?: boolean;
onBookmark?: () => void;
bookmarkDisabled?: boolean;
onBookmark?: () => void | Promise<void>;
onMore?: (anchor: AnchorPosition) => void;
onPress?: () => void;
}
Expand All @@ -46,6 +47,7 @@ export function CardLink({
bookmarked = false,
icon = true,
disabled = false,
bookmarkDisabled = false,
onBookmark,
onMore,
onPress,
Expand All @@ -57,7 +59,8 @@ export function CardLink({
const normalizedVerdict = verdict && verdict in VERDICT_LABELS ? verdict : undefined;
const statusLabel = normalizedVerdict ? VERDICT_LABELS[normalizedVerdict] : (getFirstText(label) ?? '결과 없음');
const statusColors = normalizedVerdict ? VERDICT_COLORS[normalizedVerdict] : undefined;
const guardedBookmark = useGuardedPress(onBookmark, { disabled });
const isBookmarkDisabled = disabled || bookmarkDisabled;
const guardedBookmark = useGuardedPress(onBookmark, { disabled: isBookmarkDisabled });
const guardedMore = useGuardedPress(onMore, { disabled });

const handleBookmarkPress = (event: GestureResponderEvent) => {
Expand Down Expand Up @@ -124,14 +127,19 @@ export function CardLink({
{icon && (
<View style={styles.iconRow}>
<Pressable
onPress={disabled ? undefined : handleBookmarkPress}
onPress={isBookmarkDisabled ? undefined : handleBookmarkPress}
hitSlop={8}
style={({ pressed }) => pressed && !disabled && styles.pressed}
style={({ pressed }) => [
pressed && !isBookmarkDisabled && styles.pressed,
bookmarkDisabled && styles.bookmarkDisabled,
]}
accessibilityRole="button"
accessibilityState={{ disabled: isBookmarkDisabled }}
>
<IconSymbol
name={bookmarked ? 'bookmark.fill' : 'bookmark'}
size={18}
color={bookmarked ? Colors.brand.primary : disabled ? Colors.brand.textHint : Colors.brand.textSecondary}
color={bookmarked ? Colors.brand.primary : isBookmarkDisabled ? Colors.brand.textHint : Colors.brand.textSecondary}
/>
</Pressable>
<Pressable
Expand Down Expand Up @@ -238,4 +246,7 @@ const styles = StyleSheet.create({
pressed: {
opacity: 0.6,
},
bookmarkDisabled: {
opacity: 0.45,
},
});
33 changes: 31 additions & 2 deletions context/saved-links-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import { getSiteName } from '@/utils/analysis-result-display';
const DEFAULT_PAGE_SIZE = 50;
const LOGIN_REQUIRED_MESSAGE =
'로그인 상태를 확인할 수 없습니다. 다시 로그인한 뒤 시도해주세요.';
const NETWORK_REQUEST_FAILED_MESSAGE =
'네트워크 연결 상태를 확인한 뒤 다시 시도해주세요.';

export interface SavedLink extends SavedLinkResponse {
description: string;
Expand All @@ -28,6 +30,7 @@ interface SavedLinksContextValue {
isLoading: boolean;
isLoadingMore: boolean;
errorMessage: string;
bookmarkingLinkIds: ReadonlySet<number>;
hasNext: boolean;
nextCursor: string | null;
refreshLinks: (query?: SavedLinkListQuery) => Promise<void>;
Expand All @@ -47,10 +50,14 @@ export function SavedLinksProvider({ children }: { children: React.ReactNode })
const [isLoading, setIsLoading] = useState(false);
const [isLoadingMore, setIsLoadingMore] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const [bookmarkingLinkIds, setBookmarkingLinkIds] = useState<ReadonlySet<number>>(
() => new Set(),
);
const [hasNext, setHasNext] = useState(false);
const [nextCursor, setNextCursor] = useState<string | null>(null);
const lastQueryRef = useRef<SavedLinkListQuery>({ size: DEFAULT_PAGE_SIZE });
const getTokenRef = useRef(getToken);
const bookmarkingLinkIdsRef = useRef<Set<number>>(new Set());

useEffect(() => {
getTokenRef.current = getToken;
Expand Down Expand Up @@ -138,7 +145,14 @@ export function SavedLinksProvider({ children }: { children: React.ReactNode })
}, []);

const toggleBookmark = useCallback(async (id: number) => {
const previousLinks = links;
if (bookmarkingLinkIdsRef.current.has(id)) {
return;
}

bookmarkingLinkIdsRef.current.add(id);
setBookmarkingLinkIds(new Set(bookmarkingLinkIdsRef.current));

const previousIsBookmarked = links.find((link) => link.id === id)?.isBookmarked;
setLinks((prev) =>
prev.map((link) =>
link.id === id ? { ...link, isBookmarked: !link.isBookmarked } : link,
Expand All @@ -153,8 +167,18 @@ export function SavedLinksProvider({ children }: { children: React.ReactNode })
),
);
} catch (error) {
setLinks(previousLinks);
if (previousIsBookmarked != null) {
setLinks((prev) =>
prev.map((link) =>
link.id === id ? { ...link, isBookmarked: previousIsBookmarked } : link,
),
);
}

throw error;
} finally {
bookmarkingLinkIdsRef.current.delete(id);
setBookmarkingLinkIds(new Set(bookmarkingLinkIdsRef.current));
}
}, [links]);

Expand Down Expand Up @@ -223,6 +247,7 @@ export function SavedLinksProvider({ children }: { children: React.ReactNode })
isLoading,
isLoadingMore,
errorMessage,
bookmarkingLinkIds,
hasNext,
nextCursor,
refreshLinks,
Expand Down Expand Up @@ -251,6 +276,10 @@ export function getSavedLinkErrorMessage(error: unknown, fallbackMessage: string
return LOGIN_REQUIRED_MESSAGE;
}

if (error.message === 'Network request failed') {
return NETWORK_REQUEST_FAILED_MESSAGE;
}

if (error.message) {
return error.message;
}
Expand Down
Loading