Skip to content
Open
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
3 changes: 3 additions & 0 deletions src/assets/icons/chat.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions src/assets/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export { default as MapIcon } from './map.svg';
export { default as BookmarkIcon } from './bookmark.svg';
export { default as MyPageIcon } from './mypage.svg';
export { default as AppLogoIcon } from './logo.svg';
export { default as LogoIcon } from './logoicon.svg';
export { default as LogoLetter } from './logoletter.svg';
export { default as KakaoIcon } from './kakaoicon.svg';
export { default as NaverIcon } from './navericon.svg';
export { default as GoogleIcon } from './googleicon.svg';
Expand Down Expand Up @@ -44,6 +46,7 @@ export { default as WeatherInfoIcon } from './weather-info.svg';
export { default as BackArrow } from './backArrow.svg';
export { default as CameraIcon } from './camera.svg';
export { default as RightArrowIcon } from './rightarrow.svg';
export { default as RightArrow2Icon } from './rightarrow2.svg';
export { default as WishIcon } from './wish.svg';
export { default as KebabMenuIcon } from './kebabmenu.svg';
export { default as Map2Icon } from './map2.svg';
Expand All @@ -65,6 +68,10 @@ export { default as MyLocation } from './mylocation.svg';
export { default as EmptyLocation } from './emptylocation.svg';
export { default as EmptyWish } from './emptywish.svg';
export { default as AlertIcon } from './alert.svg';
export { default as NoticeIcon } from './notice.svg';
export { default as NoticeDotIcon } from './noticedot.svg';
export { default as ChatIcon } from './chat.svg';
export { default as MainPlaneIcon } from './mainplane.svg';
export { default as RouteIcon } from './route.svg';
export { default as WishStar } from './wishstar.svg';

Expand Down
9 changes: 9 additions & 0 deletions src/assets/icons/logoicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions src/assets/icons/logoletter.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/assets/icons/mainplane.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/assets/icons/notice.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/assets/icons/noticedot.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/assets/icons/rightarrow2.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/images/mainjeju.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/images/maintokyo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 3 additions & 2 deletions src/components/ui/ContentContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import { Shadow } from 'react-native-shadow-2';
export interface ContentContainerProps {
children: React.ReactNode;
className?: string;
shadowBorderRadius?: number;
}

// ============ Component ============
export const ContentContainer = React.memo<ContentContainerProps>(
({ children, className }) => {
({ children, className, shadowBorderRadius = 8 }) => {
// 파생 값
const { widthClass, otherClasses } = useMemo(() => {
if (!className) return { widthClass: null, otherClasses: undefined };
Expand Down Expand Up @@ -38,7 +39,7 @@ export const ContentContainer = React.memo<ContentContainerProps>(
offset={[0, 0]}
paintInside={false}
containerStyle={{ width: widthClass ? undefined : '100%', alignSelf: 'stretch' }}
style={{ borderRadius: 8, width: widthClass ? undefined : '100%' }}>
style={{ borderRadius: shadowBorderRadius, width: widthClass ? undefined : '100%' }}>
<View className={`bg-white rounded-lg ${otherClasses ?? ''}`}>
{children}
</View>
Expand Down
3 changes: 2 additions & 1 deletion src/components/ui/CustomBottomSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ export const CustomBottomSheet = ({
borderTopLeftRadius: cornerRadius ?? 16,
borderTopRightRadius: cornerRadius ?? 16,
overflow: 'hidden',

zIndex: 20,
elevation: 20,
},
animatedStyle,
]}
Expand Down
17 changes: 17 additions & 0 deletions src/components/ui/MainRecChip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react';
import { View, Text } from 'react-native';

export interface MainRecChipProps {
label: string;
className?: string;
}

export const MainRecChip: React.FC<MainRecChipProps> = ({ label, className }) => {
return (
<View className={`px-3 py-[2px] rounded-full bg-chip items-center justify-center ${className ?? ''}`}>
<Text className="text-p text-black">{label}</Text>
</View>
);
};

export default MainRecChip;
2 changes: 2 additions & 0 deletions src/components/ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ export { NavigationBar } from './NavigationBar';
export type { NavigationBarProps } from './NavigationBar';
export { Chip } from './Chip';
export type { ChipProps } from './Chip';
export { MainRecChip } from './MainRecChip';
export type { MainRecChipProps } from './MainRecChip';

export { TabNavigation } from './TabNavigation';
export type { TabNavigationProps, TabItem } from './TabNavigation';
Expand Down
2 changes: 1 addition & 1 deletion src/navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export type SearchStackParamList = {
export type RootTabParamList = {
Home: undefined;
Search: NavigatorScreenParams<SearchStackParamList> | undefined;
Map: undefined;
MyTrip: undefined;
Bookmark: undefined;
MyPage: undefined;
};
Expand Down
157 changes: 124 additions & 33 deletions src/screens/HomeScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { useNavigation } from '@react-navigation/native';
import { useNavigation } from '@react-navigation/native';
import type { CompositeNavigationProp } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import React, { useCallback, useState } from 'react';
import { View, Text, TouchableOpacity, Pressable, TextInput } from 'react-native';
import { View, Text, TouchableOpacity, Pressable, TextInput, ScrollView, Image } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { Shadow } from 'react-native-shadow-2';
import type { HomeStackParamList } from '@/navigation';
import type { RootStackParamList } from '@/navigation';
import Animated, { useSharedValue, withTiming, useAnimatedStyle, interpolate } from 'react-native-reanimated';
import CustomBottomSheet from '@/components/ui/CustomBottomSheet';
import { RobotIcon, SendIcon, X } from '@/assets/icons';
import { ContentContainer, MainRecChip } from '@/components/ui';
import { RobotIcon, SendIcon, X, NoticeIcon, LogoIcon, LogoLetter, ChatIcon } from '@/assets/icons';
import { COLORS } from '@/constants/colors';
import { ChatCaseContent } from '@/screens/home/components';
import { ChatCaseContent, MainTripCard } from '@/screens/home/components';
import {
CHAT_HEADER_HEIGHT,
CHAT_INPUT_BOTTOM_SPACING,
Expand All @@ -19,24 +21,58 @@ import {
CHAT_SHEET_HEIGHT,
} from '@/screens/home/constants';

// ============ Types ============
type NavigationProp = CompositeNavigationProp<
NativeStackNavigationProp<HomeStackParamList>,
NativeStackNavigationProp<RootStackParamList>
>;

const RECOMMENDED_DESTINATIONS = [
{
id: '1',
title: '제주도',
country: '한국',
description: '아름다운 자연과 독특한 문화가 있는 한국의 보석 같은 섬',
imageUrl: require('@/assets/images/mainjeju.png'),
tags: ['자연', '맛집', '자연'],
},
{
id: '2',
title: '제주도',
country: '한국',
description: '아름다운 자연과 독특한 문화가 있는 한국의 보석 같은 섬',
imageUrl: require('@/assets/images/mainjeju.png'),
tags: ['자연', '맛집'],
},
];

const HomeScreen: React.FC = () => {
const navigation = useNavigation<NavigationProp>();
const translateY = useSharedValue(CHAT_SHEET_HEIGHT);
const SNAP_MIN = CHAT_SHEET_HEIGHT;
const [isChatOpen, setIsChatOpen] = useState(false);
const [chatCaseOrder, setChatCaseOrder] = useState(-1);
const [hasPlannedTrip, setHasPlannedTrip] = useState(false);
const [isInTripScheduleView, setIsInTripScheduleView] = useState(false);
const currentCaseIndex = chatCaseOrder < 0 ? 0 : chatCaseOrder;
const hasNotification = true;

const handleNavigateToDetail = useCallback(() => {
navigation.navigate('Alert');
}, [navigation]);

const handleNavigateToAddTrip = useCallback(() => {
setHasPlannedTrip(true);
setIsInTripScheduleView(false);
}, []);

const handleOpenTripSchedule = useCallback(() => {
setIsInTripScheduleView(true);
}, []);

const handleNavigateToMyTrip = useCallback(() => {
navigation.navigate('MainTabs', { screen: 'MyTrip' });
}, [navigation]);

const handleOpenChat = useCallback(() => {
setChatCaseOrder((prev) => (prev + 1) % 3);
translateY.value = withTiming(0, { duration: 350 });
Expand All @@ -47,22 +83,12 @@ const HomeScreen: React.FC = () => {
}, [translateY, SNAP_MIN]);

const backdropStyle = useAnimatedStyle(() => {
const opacity = interpolate(translateY.value, [0, SNAP_MIN], [1, 0]);
const opacity = interpolate(translateY.value, [0, SNAP_MIN], [0.3, 0]);
return { opacity };
}, [translateY, SNAP_MIN]);

return (
<SafeAreaView className="flex-1 bg-screenBackground" edges={['top']}>
<View className="flex-1 items-center justify-center">
<Text className="text-h1 font-pretendardBold text-black">홈</Text>
<TouchableOpacity
onPress={handleNavigateToDetail}
className="bg-main px-6 py-3 rounded-lg">
<Text className="text-white font-pretendardSemiBold">알림 리스트</Text>
</TouchableOpacity>
</View>

{/* 배경 오버레이 */}
<Animated.View
pointerEvents={isChatOpen ? 'auto' : 'none'}
style={[
Expand All @@ -73,23 +99,94 @@ const HomeScreen: React.FC = () => {
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.3)',
zIndex: 10,
elevation: 10,
},
backdropStyle,
]}
>
<Pressable style={{ flex: 1 }} onPress={handleCloseChat} />
</Animated.View>

{/* 채팅 플로팅 버튼 */}
{/* TODO: 재원님 여기 만드시면 돼요 */}
<TouchableOpacity
onPress={handleOpenChat}
className="absolute bottom-6 right-6 bg-main rounded-full px-5 py-4"
>
<Text className="text-white font-pretendardSemiBold">채팅</Text>
<ScrollView className="flex-1" showsVerticalScrollIndicator={false}>
<View className="px-4 py-4 flex-row justify-between items-center">
<View className="flex-row items-center">
<LogoIcon width={38} height={38} />
<LogoLetter width={65} height={32} />
</View>
<ContentContainer className="px-[10px] py-[10px] rounded-xl">
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 ContentContainer 내부의 쉐도우는 borderRadius가 8로 잡혀있어 화면 구현 상으로는 알림 밖 컨테이너의 borderRadius 8픽셀로 확인이 됩니다 12픽셀로 보이도록 수정해주세요

<Pressable onPress={handleNavigateToDetail} className="items-center justify-center">
<View className="relative">
<NoticeIcon width={20} height={20} />
{hasNotification && (
<View className="absolute right-[-1px] top-[-1px] h-2 w-2 rounded-full bg-statusError" />
)}
</View>
</Pressable>
</ContentContainer>
</View>

<View className="mx-4 mt-4">
<MainTripCard
hasPlannedTrip={hasPlannedTrip}
isInTripScheduleView={isInTripScheduleView}
onAddTrip={handleNavigateToAddTrip}
onOpenTripSchedule={handleOpenTripSchedule}
onViewAllSchedule={handleNavigateToMyTrip}
/>
</View>

<View className="mt-6 mb-6">
<View className="px-4 mb-2">
<Text className="text-h1 font-pretendardSemiBold text-black mb-[2px]">추천 여행지</Text>
<Text className="text-p text-gray">지금 떠나기 좋은 여행지를 모았어요</Text>
</View>

<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={{ paddingHorizontal: 16, paddingVertical: 8, gap: 16 }}
>
{RECOMMENDED_DESTINATIONS.map((item) => (
<Shadow
key={item.id}
distance={10}
offset={[0, 0]}
startColor="#00000025"
endColor="#00000000"
paintInside={false}
style={{ borderRadius: 8, width: 260 }}
>
<TouchableOpacity className="rounded-lg overflow-hidden bg-white">
<View className="h-40 relative">
<Image source={item.imageUrl} className="w-full h-full" resizeMode="cover" />
<View className="absolute bottom-3 left-4">
<Text className="text-white text-h2 font-pretendardSemiBold">{item.title}</Text>
<Text className="text-white text-p font-pretendardSemiBold mt-1">{item.country}</Text>
</View>
</View>

<View className="p-4">
<Text className="text-gray text-p mb-4" numberOfLines={2}>
{item.description}
</Text>
<View className="flex-row">
{item.tags.map((tag, index) => (
<MainRecChip key={`${item.id}-${tag}-${index}`} label={tag} className={'mr-[6px]'} />
))}
</View>
</View>
</TouchableOpacity>
</Shadow>
))}
</ScrollView>
</View>
</ScrollView>

<TouchableOpacity onPress={handleOpenChat} className="absolute bottom-4 right-4 bg-main rounded-full px-4 py-4">
<ChatIcon width={24} height={24} />
</TouchableOpacity>

{/* 채팅 바텀시트 */}
<CustomBottomSheet
translateY={translateY}
height={CHAT_SHEET_HEIGHT}
Expand All @@ -99,7 +196,6 @@ const HomeScreen: React.FC = () => {
onStateChange={setIsChatOpen}
showIndicator={false}
>
{/* 바텀시트 헤더 */}
<View
className="absolute top-0 left-0 right-0 bg-white px-4 flex-row items-center justify-center"
style={{ height: CHAT_HEADER_HEIGHT }}
Expand All @@ -123,16 +219,10 @@ const HomeScreen: React.FC = () => {
<View className="absolute bottom-0 left-0 right-0 h-px bg-borderGray" />
</View>

{/* 헤더 아래 콘텐츠 영역 */}
<View className="flex-1" style={{ marginTop: CHAT_HEADER_HEIGHT }}>
{/* 케이스별 콘텐츠 */}
<ChatCaseContent currentCaseIndex={currentCaseIndex} />

{/* 하단 입력창 */}
<View
className="absolute left-0 right-0 px-4 flex-row items-center"
style={{ bottom: CHAT_INPUT_BOTTOM_SPACING }}
>
<View className="absolute left-0 right-0 px-4 flex-row items-center" style={{ bottom: CHAT_INPUT_BOTTOM_SPACING }}>
<TextInput
placeholder="AI에게 질문해보세요"
placeholderTextColor={COLORS.gray}
Expand All @@ -151,5 +241,6 @@ const HomeScreen: React.FC = () => {
</SafeAreaView>
);
};

export default HomeScreen;
export { HomeScreen };
export { HomeScreen };
33 changes: 33 additions & 0 deletions src/screens/home/components/MainTripCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';
import { MainTripCardEmpty } from './MainTripCardEmpty';
import { MainTripCardPlanned } from './MainTripCardPlanned';
import { MainTripCardInProgress } from './MainTripCardInProgress';

interface MainTripCardProps {
hasPlannedTrip: boolean;
isInTripScheduleView: boolean;
onAddTrip: () => void;
onOpenTripSchedule: () => void;
onViewAllSchedule: () => void;
}

const MainTripCard: React.FC<MainTripCardProps> = ({
hasPlannedTrip,
isInTripScheduleView,
onAddTrip,
onOpenTripSchedule,
onViewAllSchedule,
}) => {
if (!hasPlannedTrip) {
return <MainTripCardEmpty onAddTrip={onAddTrip} />;
}

if (!isInTripScheduleView) {
return <MainTripCardPlanned onOpenTripSchedule={onOpenTripSchedule} />;
}

return <MainTripCardInProgress onViewAllSchedule={onViewAllSchedule} />;
};

export default MainTripCard;
export { MainTripCard };
Loading