Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
25 changes: 20 additions & 5 deletions app/(post)/feed/write.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,13 @@ export default function FeedWritePage() {

const hasAnyImage = mediaUrls.length > 0;

const previewMedia: Media[] = mediaUrls.map((m, idx) => ({
position: idx + 1,
mediaUrl: m.mediaUrl,
mediaType: m.mediaType,
}));
const previewMedia: Media[] = mediaUrls
.filter((m) => m.mediaUrl && m.mediaUrl.trim() !== '')
.map((m, idx) => ({
position: idx + 1,
mediaUrl: m.mediaUrl,
mediaType: m.mediaType,
}));

const requestBodyCreate = {
description,
Expand Down Expand Up @@ -214,10 +216,23 @@ export default function FeedWritePage() {
onScrollBeginDrag={Keyboard.dismiss}
>
<View style={{ flex: 1 }}>
{/* {previewMedia.length > 0 && ( */}
<BackgroundImageSlider
mediaUrls={previewMedia}
gradient={{ top: 120 * height, bottom: 420 * height }}
/>
{/* )} */}

{kbVisible && (
<View
pointerEvents="none"
style={{
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0, 0, 0, 0.7)',
zIndex: 1,
}}
/>
)}

{/* 헤더 */}
<Header
Expand Down
18 changes: 13 additions & 5 deletions app/(post)/leenk/[id]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,18 @@ export default function LeenkDetailPage() {
if (!signal?.canceled) {
setLeenkDetail(data);
}
} catch (e) {
if (!signal?.canceled) {
showToast('상세 정보를 불러오지 못했어.', 'error');
router.back();
} catch (e: any) {
if (signal?.canceled) return;

// 삭제된 링크 (404)
if (e?.response?.status === 404) {
showToast('삭제된 링크야!', 'error');

setTimeout(() => {
router.back();
}, 900);

return;
}
} finally {
if (!signal?.canceled) setLoading(false);
Expand Down Expand Up @@ -192,7 +200,7 @@ export default function LeenkDetailPage() {
await deleteLeenk(leenkDetail.id);
showToast('삭제 완료!', 'success');
closeModal();
router.replace('/leenk');
router.replace('/(page)/leenk');
} catch (err) {
console.error('링크 삭제 오류:', err);
showToast('삭제에 실패했어. 잠시 후 다시 시도해 줘.', 'error');
Expand Down
1 change: 1 addition & 0 deletions app/account/notification-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export default function NotificationListPage() {
if (path === 'leenks') {
router.push(`/leenk/${content.leenkId}`);
} else {
console.log('feedId : ', content.feedId);
router.push(`/feed/${content.feedId}`);
}
}
Expand Down
6 changes: 6 additions & 0 deletions app/account/setting/notifications.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
getNotificationsSetting,
patchNotificationsSetting,
} from '@/api/users/notification.api';
import * as Haptics from 'expo-haptics';

export default function SettingNotificationsPage() {
const [toggles, setToggles] = useState({
Expand Down Expand Up @@ -57,6 +58,9 @@ export default function SettingNotificationsPage() {
const handleToggle = async (key: keyof typeof toggles, apiKey: string) => {
const newValue = !toggles[key];

//햅틱
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Soft);

setToggles((prev) => ({
...prev,
[key]: newValue,
Expand All @@ -66,6 +70,8 @@ export default function SettingNotificationsPage() {
await patchNotificationsSetting({ [apiKey]: newValue });
} catch (error) {
console.error('알림 설정 업데이트 실패:', error);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);

setToggles((prev) => ({
...prev,
[key]: !newValue,
Expand Down
18 changes: 12 additions & 6 deletions components/Modal/NotificationModal.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React from 'react';
import { Modal, Platform, TouchableWithoutFeedback } from 'react-native';
import styled from 'styled-components/native';
import { width, height, radius } from '@/theme/globalStyles';
import { width, height, radius, fonts } from '@/theme/globalStyles';
import colors from '@/theme/color';
import { FlatList } from 'react-native-gesture-handler';
import {
Expand Down Expand Up @@ -38,7 +37,7 @@ export default function NotificationModal({
): item is NewLeenkParticipantDetails =>
(item as NewLeenkParticipantDetails).participantName !== undefined;

const isScrollable = data.length > 5;
const isScrollable = data.length >= 4;

return (
<Modal
Expand Down Expand Up @@ -70,8 +69,8 @@ export default function NotificationModal({
</LeftSection>
</Row>
<ContentContainer>
<Title>{item.body}</Title>
<SubText>{item.name}</SubText>
<SubText>{item.body}</SubText>
<Title>{item.name}</Title>
</ContentContainer>
</Item>
);
Expand Down Expand Up @@ -105,7 +104,7 @@ export default function NotificationModal({
</LeftSection>
</Row>
<ContentContainer>
<Title>{item.participantName}</Title>
<StyledTitle>{item.participantName}</StyledTitle>
</ContentContainer>
</Item>
);
Expand Down Expand Up @@ -171,6 +170,9 @@ const Item = styled.View`

const ContentContainer = styled.View`
margin-left: ${28 * width}px;
display: flex;
gap: ${6 * height}px;
align-items: flex-start;
`;

const GradientOverlay = styled(LinearGradient).attrs({
Expand All @@ -183,3 +185,7 @@ const GradientOverlay = styled(LinearGradient).attrs({
height: ${60 * height}px;
z-index: 1;
`;

const StyledTitle = styled(SubText)`
color: ${colors.text[1]};
`;
80 changes: 69 additions & 11 deletions components/Modal/UserListModal.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { Modal, Pressable, Platform, KeyboardAvoidingView } from 'react-native';
import {
Modal,
Pressable,
Platform,
KeyboardAvoidingView,
Animated,
} from 'react-native';
import styled from 'styled-components/native';
import { BlurView } from 'expo-blur';
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';

interface Props {
visible: boolean;
Expand All @@ -19,14 +26,61 @@ 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]);

return (
<Modal visible={visible} transparent animationType="slide">
<Backdrop>
<Modal
key={visible ? 'open' : 'closed'}
visible={visible}
transparent
animationType="none"
>
<AnimatedBackdrop style={{ opacity: fadeAnim }}>
<Pressable style={{ flex: 1 }} onPress={onClose} />
</AnimatedBackdrop>

<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
style={{ flex: 1, justifyContent: 'flex-end' }}
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
style={{ flex: 1, justifyContent: 'flex-end' }}
pointerEvents="box-none"
>
<Animated.View
style={{
transform: [{ translateY: slideAnim }],
}}
>
<SheetContainer>
<SheetBox>
Expand All @@ -42,15 +96,19 @@ export default function UserListModal({
</BlurBackground>
</SheetBox>
</SheetContainer>
</KeyboardAvoidingView>
</Backdrop>
</Animated.View>
</KeyboardAvoidingView>
</Modal>
);
}

const Backdrop = styled.Pressable`
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 SheetContainer = styled.View`
Expand Down
5 changes: 4 additions & 1 deletion components/common/Header/KebabButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ export default function KebabButton({
const iconColor = color === 'white' ? colors.white : colors.black;

return (
<TouchableOpacity onPress={handleKebab || (() => {})}>
<TouchableOpacity
onPress={handleKebab || (() => {})}
hitSlop={{ top: 16, bottom: 16, left: 16, right: 16 }}
>
<KebabIcon color={iconColor} width={18} height={18} />
</TouchableOpacity>
);
Expand Down
5 changes: 5 additions & 0 deletions components/common/ImagePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ export default function ImagePicker({
windowSize={5}
removeClippedSubviews
ListFooterComponent={pagingLoading ? <Loading /> : null}
ListFooterComponentStyle={{
marginTop: 44 * height,
marginBottom: 40 * height,
alignItems: 'center',
}}
renderItem={({ item }) => (
<ThumbnailItem
asset={item}
Expand Down
49 changes: 40 additions & 9 deletions components/common/Toggle.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,52 @@
import styled from 'styled-components/native';
import { width, height } from '@/theme/globalStyles';
import colors from '@/theme/color';
import { Pressable } from 'react-native';
import { Pressable, Animated } from 'react-native';
import { useRef, useEffect } from 'react';

interface ToggleProps {
isOn: boolean;
onToggle: () => void;
}

export default function Toggle({ isOn, onToggle }: ToggleProps) {
const anim = useRef(new Animated.Value(isOn ? 1 : 0)).current;
const isFirstRender = useRef(true);

useEffect(() => {
if (isFirstRender.current) {
// 최초 진입 시 애니메이션 없이 바로 반영
anim.setValue(isOn ? 1 : 0);
isFirstRender.current = false;
return;
}

Animated.timing(anim, {
toValue: isOn ? 1 : 0,
duration: 180,
useNativeDriver: false,
}).start();
}, [isOn, anim]);

const translateX = anim.interpolate({
inputRange: [0, 1],
outputRange: [0, 15 * width], // Thumb 이동 거리
});

const trackColor = anim.interpolate({
inputRange: [0, 1],
outputRange: [colors.gray[400], colors.primary],
});

return (
<ToggleWrapper onPress={onToggle}>
<Track isOn={isOn}>
<Thumb isOn={isOn} />
</Track>
<AnimatedTrack style={{ backgroundColor: trackColor }}>
<AnimatedThumb
style={{
transform: [{ translateX }],
}}
/>
</AnimatedTrack>
</ToggleWrapper>
);
}
Expand All @@ -24,19 +57,17 @@ const ToggleWrapper = styled(Pressable)`
justify-content: center;
`;

const Track = styled.View<{ isOn: boolean }>`
const AnimatedTrack = styled(Animated.View)`
width: 100%;
height: 100%;
border-radius: ${100 * height}px;
background-color: ${({ isOn }) => (isOn ? colors.primary : colors.gray[400])};
padding-horizontal: ${4 * width}px;
padding: 0 ${2.5 * width}px;
justify-content: center;
`;

const Thumb = styled.View<{ isOn: boolean }>`
const AnimatedThumb = styled(Animated.View)`
width: ${16 * height}px;
height: ${16 * height}px;
border-radius: ${100 * height}px;
background-color: ${colors.white};
align-self: ${({ isOn }) => (isOn ? 'flex-end' : 'flex-start')};
`;
Loading