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
5 changes: 5 additions & 0 deletions api/login/apple.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ export const appleLogin = async (idToken: string) => {
const { code, message, data } = response.data;
return { code, message, data };
} catch (error: any) {
console.log('APPLE LOGIN ERROR', error);
console.log('message', error?.message);
console.log('response', error?.response);
console.log('response data', error?.response?.data);

if (error.response?.data) {
const { code, message } = error.response.data;
return { code, message };
Expand Down
18 changes: 16 additions & 2 deletions components/InAppNotificationProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import Animated, {
useAnimatedStyle,
runOnJS,
} from 'react-native-reanimated';
import { router, type Href } from 'expo-router';
import { router, usePathname, type Href } from 'expo-router';
import colors from '@/theme/color';
import { LeenkIcon, LogoNotify } from '@/assets';
import styled from 'styled-components/native';
Expand Down Expand Up @@ -47,6 +47,17 @@ const ROUTES = {
feed: '/feed/[id]' as const,
};

const BLOCKED_PATH_PREFIXES = ['/', '/signup'];

export const isBlockedRoute = (pathname: string) => {
return BLOCKED_PATH_PREFIXES.some((prefix) => {
if (prefix === '/') {
return pathname === '/';
}
return pathname === prefix || pathname.startsWith(prefix + '/');
});
};

// FCM data → 앱 내 라우팅
function navigateFromData(raw?: Record<string, unknown>) {
const d = raw ?? {};
Expand Down Expand Up @@ -74,6 +85,9 @@ export function InAppNotificationProvider({
const [payload, setPayload] = useState<InAppPayload | null>(null);
const [visible, setVisible] = useState(false);
const hideTimerRef = useRef<NodeJS.Timeout | null>(null);
const pathname = usePathname();

const blocked = isBlockedRoute(pathname);

// 애니메이션: 위에서 아래로 슬라이드 인/아웃
const ty = useSharedValue(-200);
Expand Down Expand Up @@ -128,7 +142,7 @@ export function InAppNotificationProvider({
<Ctx.Provider value={ctxValue}>
{children}

{visible && (
{visible && !blocked && (
<Overlay pointerEvents="box-none">
<SafeAreaView edges={['top']} style={{ pointerEvents: 'box-none' }}>
<CardWrap style={aStyle}>
Expand Down
132 changes: 78 additions & 54 deletions components/Modal/TextInputModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import { useToastStore } from '@/stores/toastStore';
import { reportFeed } from '@/api/feed/feed.api';
import { reportLeenk } from '@/api/leenk/leenk.post.api';
import { postBirthdayLetter } from '@/api/extra/birthday/birthday.post.api';
import { useFadeSlideAnimation } from '@/hooks/useFadeSlideAnimation';
import { Animated } from 'react-native';

interface TextInputModalProps {
type: 'feed' | 'leenk' | 'birthday';
Expand All @@ -38,6 +40,12 @@ export default function TextInputModal({
(type === 'leenk' && modalType === 'leenkReport') ||
(type === 'birthday' && modalType === 'birthdayLetter');

const { backdropStyle, sheetStyle } = useFadeSlideAnimation({
visible: isOpen,
initialOffset: 400,
preset: 'soft',
});

const targetId = type === 'feed' ? feedId : leenkId;

const modalConfig = {
Expand Down Expand Up @@ -74,12 +82,16 @@ export default function TextInputModal({

const { title, subtitle, buttonText, onSubmit } = modalConfig[type];

const handleClose = () => {
setText('');
closeModal();
};

const handleSubmit = async () => {
try {
await onSubmit();
if (type === 'birthday') {
closeModal();
setText('');
handleClose();

requestAnimationFrame(() => {
openModal('birthdayLetterFinish');
Expand All @@ -88,79 +100,91 @@ export default function TextInputModal({
return;
}

closeModal();
setText('');
handleClose();
} catch (error) {
const errorMessage = type === 'birthday' ? '전송 실패!' : '신고 실패!';
console.error(`${type} 처리 실패:`, error);
showToast(errorMessage, 'error');

closeModal();
setText('');
handleClose();
}
};

return (
<Modal
visible={isOpen}
transparent
animationType="slide"
animationType="none"
onRequestClose={closeModal}
>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<Backdrop>
<Pressable style={{ flex: 1 }} onPress={closeModal} />

<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={Platform.OS === 'ios' ? -20 * height : 0}
style={{ flex: 1, justifyContent: 'flex-end' }}
>
<SheetContainer>
<SheetBox>
<Title>{title}</Title>
<SubText>{subtitle}</SubText>

<Textarea
placeholder="텍스트를 입력해 주세요"
value={text}
onChangeText={setText}
maxLength={type === 'birthday' ? 40 : 100}
minHeight={30}
maxHeight={40}
/>

<ButtonWrapper>
<CustomButton
variant="primary"
size="lg"
onPress={handleSubmit}
disabled={text.length === 0}
>
{buttonText}
</CustomButton>
<CancelButton onPress={closeModal}>
<CancelText>취소</CancelText>
</CancelButton>
</ButtonWrapper>
</SheetBox>
</SheetContainer>
</KeyboardAvoidingView>
</Backdrop>
</TouchableWithoutFeedback>
<ModalRoot>
{/* Backdrop */}
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<AnimatedBackdrop style={backdropStyle}>
<Pressable
style={{ flex: 1 }}
onPress={() => {
Keyboard.dismiss();
handleClose();
}}
/>
</AnimatedBackdrop>
</TouchableWithoutFeedback>

{/* Sheet */}
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={Platform.OS === 'ios' ? -20 * height : 0}
style={{ flex: 1, justifyContent: 'flex-end' }}
pointerEvents="box-none"
>
<AnimatedSheet style={sheetStyle}>
<SheetBox>
<Title>{title}</Title>
<SubText>{subtitle}</SubText>
<Textarea
placeholder="텍스트를 입력해 주세요"
value={text}
onChangeText={setText}
maxLength={type === 'birthday' ? 40 : 100}
minHeight={30}
maxHeight={40}
/>
<ButtonWrapper>
<CustomButton
variant="primary"
size="lg"
onPress={handleSubmit}
disabled={text.length === 0}
>
{buttonText}
</CustomButton>
<CancelButton onPress={handleClose}>
<CancelText>취소</CancelText>
</CancelButton>
</ButtonWrapper>
</SheetBox>
</AnimatedSheet>
</KeyboardAvoidingView>
</ModalRoot>
</Modal>
);
}

const Backdrop = styled.View`
const ModalRoot = styled.View`
flex: 1;
justify-content: flex-end;
background-color: rgba(0, 0, 0, 0.3);
`;

const SheetContainer = styled.View`
flex: 1;
justify-content: flex-end;
const AnimatedBackdrop = styled(Animated.View)`
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
`;

const AnimatedSheet = styled(Animated.View)`
padding: ${20 * height}px ${16 * width}px ${30 * height}px;
`;

Expand Down
47 changes: 6 additions & 41 deletions components/Modal/UserListModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import colors from '@/theme/color';
import { fonts, fontSize, radius, height, width } from '@/theme/globalStyles';
import { FeedReactedUser, FeedConnectedUser } from '@/types/feed';
import UserListModalContent from '../feed/UserListModalContent';
import { useEffect, useRef } from 'react';
import { useFadeSlideAnimation } from '@/hooks/useFadeSlideAnimation';

interface Props {
visible: boolean;
Expand All @@ -26,40 +26,9 @@ export default function UserListModal({
list,
onClose,
}: Props) {
const fadeAnim = useRef(new Animated.Value(0)).current;
const slideAnim = useRef(new Animated.Value(500)).current;

useEffect(() => {
if (visible) {
Animated.parallel([
Animated.timing(fadeAnim, {
toValue: 1,
duration: 120,
useNativeDriver: true,
}),
Animated.spring(slideAnim, {
toValue: 0,
damping: 20,
stiffness: 220,
mass: 0.6,
useNativeDriver: true,
}),
]).start();
} else {
Animated.parallel([
Animated.timing(fadeAnim, {
toValue: 0,
duration: 300,
useNativeDriver: true,
}),
Animated.timing(slideAnim, {
toValue: 500,
duration: 350,
useNativeDriver: true,
}),
]).start();
}
}, [visible, fadeAnim, slideAnim]);
const { backdropStyle, sheetStyle } = useFadeSlideAnimation({
visible,
});

return (
<Modal
Expand All @@ -68,7 +37,7 @@ export default function UserListModal({
transparent
animationType="none"
>
<AnimatedBackdrop style={{ opacity: fadeAnim }}>
<AnimatedBackdrop style={backdropStyle}>
<Pressable style={{ flex: 1 }} onPress={onClose} />
</AnimatedBackdrop>

Expand All @@ -77,11 +46,7 @@ export default function UserListModal({
style={{ flex: 1, justifyContent: 'flex-end' }}
pointerEvents="box-none"
>
<Animated.View
style={{
transform: [{ translateY: slideAnim }],
}}
>
<Animated.View style={sheetStyle}>
<SheetContainer>
<SheetBox>
<BlurBackground intensity={20} tint="light">
Expand Down
81 changes: 81 additions & 0 deletions hooks/useFadeSlideAnimation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
UserListModal, TextInputModal에 적용되는 모달 뒷배경 그림자 애니메이션 훅
*/

import { useEffect, useRef } from 'react';
import { Animated } from 'react-native';
import { height } from '@/theme/globalStyles';

type AnimationPreset = 'default' | 'soft';

const SPRING_PRESETS = {
default: {
damping: 20,
stiffness: 220,
mass: 0.6,
},
soft: {
damping: 38,
stiffness: 130,
mass: 1.8,
},
};

interface Options {
visible: boolean;
initialOffset?: number;
preset?: AnimationPreset;
}

export function useFadeSlideAnimation({
visible,
initialOffset = 500 * height,
preset = 'default',
}: Options) {
const fadeAnim = useRef(new Animated.Value(0)).current;
const slideAnim = useRef(new Animated.Value(initialOffset)).current;

const springConfig = SPRING_PRESETS[preset];

useEffect(() => {
if (visible) {
slideAnim.setValue(initialOffset);

Animated.parallel([
Animated.timing(fadeAnim, {
toValue: 1,
duration: 120,
useNativeDriver: true,
}),

Animated.spring(slideAnim, {
toValue: 0,
...springConfig,
useNativeDriver: true,
}),
]).start();
} else {
Animated.sequence([
Animated.timing(fadeAnim, {
toValue: 0,
duration: 300,
useNativeDriver: true,
}),
Animated.spring(slideAnim, {
toValue: initialOffset,
stiffness: 140,
damping: 28,
mass: 1.0,
useNativeDriver: true,
}),
]).start();
}
}, [visible, fadeAnim, slideAnim, initialOffset]);

return {
backdropStyle: { opacity: fadeAnim },
sheetStyle: {
transform: [{ translateY: slideAnim }],
},
};
}
Loading