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
42 changes: 42 additions & 0 deletions app/(tabs)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { GestureHandlerRootView } from "react-native-gesture-handler";
import { useSharedValue } from "react-native-reanimated";

import { Ionicons } from "@expo/vector-icons";
import { useLocalSearchParams } from "expo-router";
import styled from "styled-components/native";

import CustomBottomSheet from "@/components/bottomSheet/CustomBottomSheet";
Expand All @@ -24,8 +25,10 @@ import { SightInfo } from "@/types/sight";

import { theme } from "@/styles/theme";

import { getSightDetail } from "@/api/sight/getSight";
import { useHeaderButtonStore } from "@/store/useHeaderButtonStore";
import { RouteCartItem, useRouteCartStore } from "@/store/useRouteCartStore";
import { useSightStore } from "@/store/useSightStore";

export default function Index() {
const bottomSheetRef = useRef<any>(null);
Expand All @@ -38,6 +41,7 @@ export default function Index() {

const [searchText, setSearchText] = useState("");
const [showResults, setShowResults] = useState(false);
const { sightId } = useLocalSearchParams<{ sightId?: string }>();
Copy link
Member

Choose a reason for hiding this comment

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

파라미터명을 더 정확히 사용하는것도 좋을거 같아요.
코드를 읽을때도 한번에 이해가 어렵기도 하고 sightId는 여러 인터페이스 안에서 사용되고 있는 변수명이기도 해서요.


const {
sights,
Expand Down Expand Up @@ -67,6 +71,44 @@ export default function Index() {
fetchCurations();
}, [fetchCurations]);

useEffect(() => {
if (sightId && location) {
const fetchAndMove = async () => {
try {
const detail = await getSightDetail({
id: sightId,
longitude: location.longitude,
latitude: location.latitude,
});

// 지도 이동
mapRef.current?.moveToLocation({
latitude: detail.latitude,
longitude: detail.longitude,
latitudeDelta: 0.01,
longitudeDelta: 0.01,
});

// 선택된 관광지 설정
const sight: SightInfo = {
id: sightId,
title: detail.title,
longitude: detail.longitude,
latitude: detail.latitude,
geoHash: "",
};

useSightStore.getState().selectSight(sight);
useSightStore.getState().setSightDetail(detail);
} catch (error) {
console.error("관광지 조회 실패:", error);
}
};
Comment on lines +76 to +106
Copy link
Member

Choose a reason for hiding this comment

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

fetchAndMove 메서드를 훅으로 빼내서 활용해도 좋을거 같습니다


fetchAndMove();
}
}, [sightId, location.latitude, location.longitude]);
Copy link
Member

Choose a reason for hiding this comment

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

기존에 관광지 세부정보를 보여주는 방식과는 다른 방식을 사용했는데 어떻게 사용하든 통합을 하는게 좋을거 같습니다.


const handleSearch = useCallback(async () => {
if (!searchText.trim()) return;

Expand Down
2 changes: 2 additions & 0 deletions app/(tabs)/myPage/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export default function MyPageLayout() {
/>

<Stack.Screen name="editProfile" options={{ title: "회원정보수정" }} />
<Stack.Screen name="pastTrip" options={{ title: "지난 여행" }} />
<Stack.Screen name="pastTripDetail" options={{ title: "지난 여행 상세" }} />
</Stack>
);
}
2 changes: 1 addition & 1 deletion app/(tabs)/myPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ function LoggedInView({
<BookmarkIcon width={24} height={24} color={theme.colors.white} />
<TabLabel>북마크</TabLabel>
</TabButton>
<TabButton onPress={() => console.log("지난 여행")}>
<TabButton onPress={() => router.push("/myPage/pastTrip" as any)}>
<LastTripIcon width={24} height={24} color={theme.colors.white} />
<TabLabel>지난 여행</TabLabel>
</TabButton>
Expand Down
288 changes: 288 additions & 0 deletions app/(tabs)/myPage/pastTrip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
import { useCallback, useEffect, useState } from "react";

import { ActivityIndicator, Alert, FlatList, Modal } from "react-native";

import { useRouter } from "expo-router";
import { Pencil, Trash2 } from "lucide-react-native";
import styled from "styled-components/native";

import { useCompletedRoute } from "@/hooks/route/useCompletedRoute";

import { CompletedRouteSummary } from "@/types/completedRoute";

import { theme } from "@/styles/theme";

export default function PastTrip() {
const router = useRouter();
const {
routes,
hasNext,
isLoading,
fetchRoutes,
modifyRouteName,
deleteRoute,
} = useCompletedRoute();

const [editModalVisible, setEditModalVisible] = useState(false);
const [editingRoute, setEditingRoute] = useState<CompletedRouteSummary | null>(null);
const [editName, setEditName] = useState("");

useEffect(() => {
fetchRoutes(true);
}, []);

const handleItemPress = (routeId: number) => {
router.push({
pathname: "/myPage/pastTripDetail",
params: { routeId },
});
};

const handleEditPress = (route: CompletedRouteSummary) => {
setEditingRoute(route);
setEditName(route.name);
setEditModalVisible(true);
};

const handleEditSubmit = async () => {
if (!editingRoute || !editName.trim()) return;

try {
await modifyRouteName(editingRoute.routeId, editName.trim());
setEditModalVisible(false);
setEditingRoute(null);
setEditName("");
} catch (error) {
Alert.alert("오류", "이름 수정에 실패했습니다.");
}
};

const handleDeletePress = (route: CompletedRouteSummary) => {
Alert.alert(
"여행 삭제",
`"${route.name}"기록을 정말 삭제하시겠습니까?\n삭제된 데이터는 복구할 수 없습니다.`,
[
{ text: "취소", style: "cancel" },
{
text: "삭제",
style: "destructive",
onPress: async () => {
try {
await deleteRoute(route.routeId);
} catch (error) {
Alert.alert("오류", "삭제에 실패했습니다.");
}
},
},
]
);
};

const renderItem = useCallback(
({ item }: { item: CompletedRouteSummary }) => (
<RouteItem onPress={() => handleItemPress(item.routeId)}>
<RouteInfo>
<RouteName>{item.name}</RouteName>
<RouteDate>{item.completedAt}</RouteDate>
</RouteInfo>
<ButtonGroup>
<ActionButton onPress={() => handleEditPress(item)}>
<Pencil size={18} color={theme.colors.text.textSecondary} />
</ActionButton>
<ActionButton onPress={() => handleDeletePress(item)}>
<Trash2 size={18} color={theme.colors.text.textSecondary} />
</ActionButton>
</ButtonGroup>
</RouteItem>
),
[]
);

return (
<Container>
<Header>
<HeaderTitle>지난 여행</HeaderTitle>
</Header>

<FlatList
data={routes}
keyExtractor={(item) => item.routeId.toString()}
renderItem={renderItem}
refreshing={isLoading && routes.length === 0}
onRefresh={() => fetchRoutes(true)}
onEndReached={() => {
if (hasNext && !isLoading) {
fetchRoutes(false);
}
}}
onEndReachedThreshold={0.5}
ListFooterComponent={
isLoading && routes.length > 0 ? (
<ActivityIndicator style={{ padding: 20 }} />
) : null
}
ListEmptyComponent={
!isLoading ? (
<EmptyContainer>
<EmptyText>지난 여행이 없습니다</EmptyText>
</EmptyContainer>
) : null
}
contentContainerStyle={{ flexGrow: 1 }}
/>

<Modal
visible={editModalVisible}
transparent
animationType="fade"
onRequestClose={() => setEditModalVisible(false)}
>
<ModalOverlay>
<ModalContent>
<ModalTitle>여행 이름 수정</ModalTitle>
<ModalInput
value={editName}
onChangeText={setEditName}
placeholder="여행 이름 입력"
placeholderTextColor={theme.colors.text.textTertiary}
autoFocus
/>
<ModalButtonGroup>
<ModalButton onPress={() => setEditModalVisible(false)}>
<ModalButtonText>취소</ModalButtonText>
</ModalButton>
<ModalButton onPress={handleEditSubmit} primary>
<ModalButtonText primary>확인</ModalButtonText>
</ModalButton>
</ModalButtonGroup>
</ModalContent>
</ModalOverlay>
</Modal>
</Container>
);
}

const Container = styled.View`
flex: 1;
background-color: ${theme.colors.white};
`;

const Header = styled.View`
height: 50px;
justify-content: center;
align-items: center;
border-bottom-width: 1px;
border-bottom-color: ${theme.colors.grey.neutral200};
`;

const HeaderTitle = styled.Text`
font-family: ${theme.typography.fontFamily.semiBold};
font-size: ${theme.typography.fontSize.lg}px;
color: ${theme.colors.text.textPrimary};
`;

const RouteItem = styled.TouchableOpacity`
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom-width: 1px;
border-bottom-color: ${theme.colors.grey.neutral200};
`;

const RouteInfo = styled.View`
flex: 1;
`;

const RouteName = styled.Text`
font-family: ${theme.typography.fontFamily.medium};
font-size: ${theme.typography.fontSize.md}px;
color: ${theme.colors.text.textPrimary};
margin-bottom: 4px;
`;

const RouteDate = styled.Text`
font-family: ${theme.typography.fontFamily.regular};
font-size: ${theme.typography.fontSize.xs}px;
color: ${theme.colors.text.textTertiary};
`;

const ButtonGroup = styled.View`
flex-direction: row;
gap: 8px;
`;

const ActionButton = styled.TouchableOpacity`
padding: 8px 12px;
`;

const ActionButtonText = styled.Text`
font-family: ${theme.typography.fontFamily.medium};
font-size: ${theme.typography.fontSize.xs}px;
color: ${theme.colors.text.textSecondary};
`;

const EmptyContainer = styled.View`
flex: 1;
justify-content: center;
align-items: center;
`;

const EmptyText = styled.Text`
font-family: ${theme.typography.fontFamily.regular};
font-size: ${theme.typography.fontSize.sm}px;
color: ${theme.colors.text.textTertiary};
`;

const ModalOverlay = styled.View`
flex: 1;
background-color: ${theme.colors.background.modalBackground};
justify-content: center;
align-items: center;
padding: 20px;
`;

const ModalContent = styled.View`
width: 100%;
background-color: ${theme.colors.white};
border-radius: ${theme.borderRadius.md}px;
padding: 20px;
`;

const ModalTitle = styled.Text`
font-family: ${theme.typography.fontFamily.semiBold};
font-size: ${theme.typography.fontSize.md}px;
color: ${theme.colors.text.textPrimary};
margin-bottom: 16px;
text-align: center;
`;

const ModalInput = styled.TextInput`
background-color: ${theme.colors.background.background50};
border-radius: ${theme.borderRadius.md}px;
padding: 12px 16px;
font-size: ${theme.typography.fontSize.md}px;
color: ${theme.colors.text.textPrimary};
margin-bottom: 16px;
`;

const ModalButtonGroup = styled.View`
flex-direction: row;
gap: 12px;
`;

const ModalButton = styled.TouchableOpacity<{ primary?: boolean }>`
flex: 1;
padding: 12px;
border-radius: ${theme.borderRadius.md}px;
background-color: ${(props) =>
props.primary ? theme.colors.main.primary : theme.colors.grey.neutral200};
align-items: center;
`;

const ModalButtonText = styled.Text<{ primary?: boolean }>`
font-family: ${theme.typography.fontFamily.medium};
font-size: ${theme.typography.fontSize.sm}px;
color: ${(props) =>
props.primary ? theme.colors.white : theme.colors.text.textPrimary};
`;
Loading