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
7 changes: 4 additions & 3 deletions app/(auth)/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import { router, useLocalSearchParams } from 'expo-router';
import { useShareIntentContext } from 'expo-share-intent';
import * as WebBrowser from 'expo-web-browser';
import { useEffect, useRef, useState } from 'react';
import { Alert, StyleSheet, Text, View } from 'react-native';
import { StyleSheet, Text, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

import { ApiError } from '@/api/api-client';
import { SocialLoginButton } from '@/components/ui/social-login-button';
import { Colors, Typography } from '@/constants/theme';
import { syncAuthenticatedMember } from '@/services/auth-api';
import { showAlert } from '@/utils/guarded-alert';
import { getSharedUrlFromIntent } from '@/utils/shared-url';

const IMG_WORDMARK = require('@/assets/images/login_wordmark.png');
Expand Down Expand Up @@ -62,7 +63,7 @@ export default function LoginScreen() {
}

shownNoticeRef.current = notice;
Alert.alert(alert.title, alert.message);
showAlert(alert.title, alert.message);
}, [notice]);

const handleGoogleLogin = async () => {
Expand Down Expand Up @@ -98,7 +99,7 @@ export default function LoginScreen() {
}
}

Alert.alert(
showAlert(
'로그인 실패',
'계정 연결 중 서버와 통신하지 못했습니다. 잠시 후 다시 시도해주세요.'
);
Expand Down
22 changes: 18 additions & 4 deletions app/(tabs)/(folder)/[id].tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Alert, ScrollView, StyleSheet, Text, View } from 'react-native';
import { ScrollView, StyleSheet, Text, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useLocalSearchParams, useRouter } from 'expo-router';

Expand All @@ -12,7 +12,8 @@ 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, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { showAlert } from '@/utils/guarded-alert';

type MoreMenuState = {
visible: boolean;
Expand All @@ -35,6 +36,7 @@ export default function FolderDetailScreen() {
const [toastVisible, setToastVisible] = useState(false);
const [addToastVisible, setAddToastVisible] = useState(false);
const [titleToastVisible, setTitleToastVisible] = useState(false);
const deletingFromFolderIdsRef = useRef<Set<number>>(new Set());

useEffect(() => {
if (urlAdded === '1') setAddToastVisible(true);
Expand All @@ -45,20 +47,32 @@ export default function FolderDetailScreen() {
};

const handleDelete = async (linkId: number) => {
if (deletingFromFolderIdsRef.current.has(linkId)) {
return;
}

deletingFromFolderIdsRef.current.add(linkId);

try {
await assignCategory([linkId], null);
await refreshFolders();
setToastVisible(true);
} catch (error) {
Alert.alert(
showAlert(
'폴더에서 삭제 실패',
getSavedLinkErrorMessage(error, '링크를 폴더에서 제외하지 못했습니다. 잠시 후 다시 시도해주세요.'),
);
} finally {
deletingFromFolderIdsRef.current.delete(linkId);
}
};

const confirmDeleteFromFolder = (linkId: number) => {
Alert.alert(
if (deletingFromFolderIdsRef.current.has(linkId)) {
return;
}

showAlert(
'폴더에서 삭제할까요?',
'링크는 삭제되지 않아요.\n현재 폴더에서만 제외돼요.',
[
Expand Down
11 changes: 7 additions & 4 deletions app/(tabs)/(folder)/folder-add-url.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useCallback, useState } from 'react';
import { useCallback, useRef, useState } from 'react';
import {
Alert,
FlatList,
StyleSheet,
Text,
Expand All @@ -14,6 +13,7 @@ import { SelectionCircle } from '@/components/ui/selection-circle';
import { Colors, Typography } from '@/constants/theme';
import { getFolderErrorMessage, useFolders } from '@/context/folders-context';
import { useSavedLinks, type SavedLink } from '@/context/saved-links-context';
import { showAlert } from '@/utils/guarded-alert';

// ─── Selectable card row ──────────────────────────────────────────────────────

Expand Down Expand Up @@ -60,6 +60,7 @@ export default function FolderAddUrlScreen() {

const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set());
const [isAdding, setIsAdding] = useState(false);
const isAddingRef = useRef(false);

const toggleSelect = useCallback((id: number) => {
setSelectedIds((prev) => {
Expand All @@ -74,7 +75,8 @@ export default function FolderAddUrlScreen() {
}, []);

const handleAdd = async () => {
if (isAdding || selectedIds.size === 0) return;
if (isAddingRef.current || selectedIds.size === 0) return;
isAddingRef.current = true;
setIsAdding(true);
try {
await assignCategory([...selectedIds], Number(folderId));
Expand All @@ -84,11 +86,12 @@ export default function FolderAddUrlScreen() {
params: { id: folderId, urlAdded: '1' },
});
} catch (error) {
Alert.alert(
showAlert(
'URL 추가 실패',
getFolderErrorMessage(error, '선택한 URL을 폴더에 추가하지 못했습니다. 잠시 후 다시 시도해주세요.'),
);
} finally {
isAddingRef.current = false;
setIsAdding(false);
}
};
Expand Down
24 changes: 21 additions & 3 deletions app/(tabs)/(folder)/folder-name.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useState } from 'react';
import { useCallback, useRef, useState } from 'react';
import { useFocusEffect } from '@react-navigation/native';
import {
KeyboardAvoidingView,
Platform,
Expand All @@ -13,16 +14,30 @@ import { router, Stack } from 'expo-router';
import { Button } from '@/components/ui/button';
import { Colors, Typography } from '@/constants/theme';
import { useFolders } from '@/context/folders-context';
import { useGuardedPress } from '@/utils/press-guard';

export default function FolderNameScreen() {
const [folderName, setFolderName] = useState('');
const [isFocused, setIsFocused] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const [isNavigating, setIsNavigating] = useState(false);
const isNavigatingRef = useRef(false);
const { folders } = useFolders();

const canProceed = folderName.trim().length > 0;
useFocusEffect(
useCallback(() => {
isNavigatingRef.current = false;
setIsNavigating(false);
}, []),
);

const canProceed = folderName.trim().length > 0 && !isNavigating;

const handleNext = () => {
if (isNavigatingRef.current) {
return;
}

const trimmedName = folderName.trim();
const hasDuplicateName = folders.some(
(folder) => normalizeFolderName(folder.name) === normalizeFolderName(trimmedName),
Expand All @@ -34,11 +49,14 @@ export default function FolderNameScreen() {
}

setErrorMessage('');
isNavigatingRef.current = true;
setIsNavigating(true);
router.push({
pathname: '/(tabs)/(folder)/folder-url-select',
params: { folderName: trimmedName },
});
};
const guardedClearFolderName = useGuardedPress(() => setFolderName(''), { lockMs: 250 });

return (
<>
Expand Down Expand Up @@ -87,7 +105,7 @@ export default function FolderNameScreen() {
/>
{folderName.length > 0 && (
<TouchableOpacity
onPress={() => setFolderName('')}
onPress={guardedClearFolderName}
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
style={styles.clearButton}
>
Expand Down
15 changes: 9 additions & 6 deletions app/(tabs)/(folder)/folder-url-select.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useCallback, useState } from 'react';
import { useCallback, useRef, useState } from 'react';
import {
Alert,
FlatList,
StyleSheet,
Text,
Expand All @@ -14,6 +13,7 @@ import { SelectionCircle } from '@/components/ui/selection-circle';
import { Colors, Typography } from '@/constants/theme';
import { getFolderErrorMessage, useFolders } from '@/context/folders-context';
import { useSavedLinks, type SavedLink } from '@/context/saved-links-context';
import { showAlert } from '@/utils/guarded-alert';

// ─── Selectable card row ──────────────────────────────────────────────────────

Expand Down Expand Up @@ -56,6 +56,7 @@ export default function FolderUrlSelectScreen() {
const { links: allLinks, refreshLinks } = useSavedLinks();
const links = allLinks.filter((l) => l.categoryId === null);
const name = (folderName ?? '새 폴더').trim();
const isCreatingRef = useRef(false);

const toggleSelect = useCallback((id: number) => {
setSelectedIds((prev) => {
Expand All @@ -70,15 +71,17 @@ export default function FolderUrlSelectScreen() {
}, []);

const handleCreate = async () => {
if (isCreating) return;
if (isCreatingRef.current) return;
isCreatingRef.current = true;
setIsCreating(true);
try {
await addFolder(name, [...selectedIds]);
} catch (error) {
Alert.alert(
showAlert(
'폴더 생성 실패',
getFolderErrorMessage(error, '폴더를 생성하지 못했습니다. 잠시 후 다시 시도해주세요.'),
);
isCreatingRef.current = false;
setIsCreating(false);
return;
}
Expand All @@ -87,15 +90,15 @@ export default function FolderUrlSelectScreen() {
await refreshLinks();
} catch {
// Link refresh is best-effort after the folder has already been created.
} finally {
setIsCreating(false);
}

router.dismissAll();
router.replace({
pathname: '/(tabs)/(folder)',
params: { folderCreated: String(Date.now()) },
});
isCreatingRef.current = false;
setIsCreating(false);
};

const selectedCount = selectedIds.size;
Expand Down
Loading
Loading