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
29 changes: 29 additions & 0 deletions apps/dog-walk/api/reactQuery/block/useInsertBlockCourse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useMutation } from "@tanstack/react-query";
import { supabase } from "@/api/supabaseClient";

interface BlockCoursePayload {
userId: string;
courseId: number;
}

const insertBlockCourse = async ({ userId, courseId }: BlockCoursePayload) => {
const { data, error } = await supabase
.from("blocked_courses")
.insert({
created_at: new Date(),
user_id: userId,
course_id: courseId,
})
.select("id")
.single();

if (error) throw error;

return data?.id ?? null;
};

export const useInsertBlockCourse = () => {
return useMutation({
mutationFn: (payload: BlockCoursePayload) => insertBlockCourse(payload),
});
};
27 changes: 18 additions & 9 deletions apps/dog-walk/api/reactQuery/course/useFindCourse.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,33 @@
import { useQuery } from "@tanstack/react-query";
import { supabase } from "@/api/supabaseClient";
import { getBlockedCourseIds } from "@/api/utils/applyBlockFilter";
import { queryKeys } from "../queryKeys";

const fetchCourse = async (id: number) => {
const fetchCourse = async (id: number, userId?: string) => {
if (!id) throw new Error("Course ID 없음");

const { data, error } = await supabase
.from("walking_courses")
.select("*")
.eq("id", id)
.single();
const blockedIds = await getBlockedCourseIds(userId);

let query = supabase.from("walking_courses").select("*").eq("id", id);

if (blockedIds.length > 0) {
query = query.not("id", "in", `(${blockedIds.join(",")})`);
}

const { data, error } = await query.single();

if (error) throw error;
return data;
};

export const useFindCourse = (id: number) => {
export const useFindCourse = (id: number, userId?: string) => {
return useQuery({
queryKey: [queryKeys.course.findCourse, id],
queryFn: () => fetchCourse(id),
queryKey: [queryKeys.course.findCourse, id, userId],
queryFn: () => fetchCourse(id, userId),
enabled: !!id,
staleTime: 0,
gcTime: 0,
refetchOnMount: "always",
refetchOnWindowFocus: true,
});
};
23 changes: 20 additions & 3 deletions apps/dog-walk/api/reactQuery/course/useFindNearbyCourses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { CourseRow } from "@/types/course";

import { useQuery } from "@tanstack/react-query";
import { supabase } from "@/api/supabaseClient";
import { getBlockedCourseIds } from "@/api/utils/applyBlockFilter";
import { queryKeys } from "../queryKeys";

// NOTE: 지구 반지름
Expand All @@ -27,18 +28,28 @@ function haversineMeters(a: Point, b: Point) {
const findNearbyCourses = async ({
latitude,
longitude,
userId,
}: {
latitude: number;
longitude: number;
userId?: string;
}) => {
if (!(latitude && longitude)) throw new Error("현재 위치 정보가 없음");

const { data, error } = await supabase
const blockedIds = await getBlockedCourseIds(userId);

let query = supabase
.from("walking_courses")
.select(
"id,start_lat,start_lng,image_url,start_name,end_name,total_distance,total_time,average_rating",
);

if (blockedIds.length > 0) {
query = query.not("id", "in", `(${blockedIds.join(",")})`);
}

const { data, error } = await query;

if (error) throw error;

const current: Point = { lat: latitude, lng: longitude };
Expand All @@ -59,13 +70,19 @@ const findNearbyCourses = async ({
export const useFindNearbyCourses = ({
latitude,
longitude,
userId,
}: {
latitude: number;
longitude: number;
userId?: string;
}) => {
return useQuery({
queryKey: [queryKeys.course.findNearbyCourses, latitude, longitude],
queryFn: () => findNearbyCourses({ latitude, longitude }),
queryKey: [queryKeys.course.findNearbyCourses, latitude, longitude, userId],
queryFn: () => findNearbyCourses({ latitude, longitude, userId }),
enabled: !!latitude && !!longitude,
staleTime: 0,
gcTime: 0,
refetchOnMount: "always",
refetchOnWindowFocus: true,
});
};
25 changes: 19 additions & 6 deletions apps/dog-walk/api/reactQuery/course/useFindPopularCourses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,23 @@ import type { CourseRow } from "@/types/course";

import { useQuery } from "@tanstack/react-query";
import { supabase } from "@/api/supabaseClient";
import { getBlockedCourseIds } from "@/api/utils/applyBlockFilter";
import { queryKeys } from "../queryKeys";

const findPopularCourses = async () => {
const { data, error } = await supabase
const findPopularCourses = async (userId?: string) => {
const blockedIds = await getBlockedCourseIds(userId);

let query = supabase
.from("walking_courses")
.select(
"id,start_lat,start_lng,image_url,start_name,end_name,total_distance,total_time,average_rating",
)
);

if (blockedIds.length > 0) {
query = query.not("id", "in", `(${blockedIds.join(",")})`);
}

const { data, error } = await query
.order("average_rating", { ascending: false })
.limit(10);

Expand All @@ -18,9 +27,13 @@ const findPopularCourses = async () => {
return data as CourseRow[];
};

export const useFindPopularCourses = () => {
export const useFindPopularCourses = (userId?: string) => {
return useQuery({
queryKey: [queryKeys.course.findPopularCourses],
queryFn: findPopularCourses,
queryKey: [queryKeys.course.findPopularCourses, userId],
queryFn: () => findPopularCourses(userId),
staleTime: 0,
gcTime: 0,
refetchOnMount: "always",
Copy link
Member

Choose a reason for hiding this comment

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

요거 다 들어가는거면 쿼리클라이언트에 들어가는게 나으려나요?!

refetchOnWindowFocus: true,
});
};
19 changes: 19 additions & 0 deletions apps/dog-walk/api/utils/applyBlockFilter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { supabase } from "../supabaseClient";

export const getBlockedCourseIds = async (
userId?: string,
): Promise<number[]> => {
if (!userId) return [];

const { data, error } = await supabase
.from("blocked_courses")
.select("course_id")
.eq("user_id", userId);

if (error) {
console.error("Failed to fetch blocked courses:", error);
return [];
}

return data.map((item) => item.course_id);
};
12 changes: 10 additions & 2 deletions apps/dog-walk/app/(screens)/detail/[id].tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { useLocalSearchParams } from "expo-router";
import { useAtomValue } from "jotai";
import { memo, useMemo, useState } from "react";
import { Dimensions, FlatList, Image, ScrollView, View } from "react-native";
import { useFindCourse } from "@/api/reactQuery/course/useFindCourse";
import Images from "@/assets/images";
import { userAtom } from "@/atoms/userAtom";
import IconText from "@/components/atoms/IconText";
import CustomSafeAreaView from "@/components/CustomSafeAriaView";
import DetailDescription from "@/components/organisms/DetailDescription";
Expand All @@ -18,6 +20,8 @@ import { IconTextType, TabKeyType } from "@/types/displayType";
import { formatDistanceKm } from "@/utils/number";

export default function DetailScreen() {
const userInfo = useAtomValue(userAtom);

const screenWidth = Dimensions.get("window").width - 32;

const { id } = useLocalSearchParams();
Expand All @@ -26,7 +30,7 @@ export default function DetailScreen() {

const [isFavorite, setIsFavorite] = useState(false);

const { data } = useFindCourse(Number(id));
const { data } = useFindCourse(Number(id), userInfo.id);

const {
total_time = 0,
Expand Down Expand Up @@ -100,7 +104,11 @@ export default function DetailScreen() {

return (
<CustomSafeAreaView>
<DetailHeaderBar isFavorite={isFavorite} setIsFavorite={setIsFavorite} />
<DetailHeaderBar
courseId={Number(id)}
isFavorite={isFavorite}
setIsFavorite={setIsFavorite}
/>
<ScrollView className="flex-1" showsVerticalScrollIndicator={false}>
<VStack className="flex-1 px-4">
<Image
Expand Down
97 changes: 70 additions & 27 deletions apps/dog-walk/app/(tabs)/index.tsx
Original file line number Diff line number Diff line change
@@ -1,75 +1,118 @@
import type { ReviewDataType } from "@/types/review";

import { Ionicons } from "@expo/vector-icons";
import { useRouter } from "expo-router";
import {
FlatList,
ScrollView,
Text,
TouchableOpacity,
View,
} from "react-native";
import { useFocusEffect, useRouter } from "expo-router";
import { useAtomValue } from "jotai";
import { useCallback } from "react";
import { FlatList, ScrollView, TouchableOpacity, View } from "react-native";
import { useFindPopularCourses } from "@/api/reactQuery/course/useFindPopularCourses";
import { useFindLatestReviews } from "@/api/reactQuery/review/useFindLatestReviews";
import { userAtom } from "@/atoms/userAtom";
import CustomSafeAreaView from "@/components/CustomSafeAriaView";
import CourseCard from "@/components/card/CourseCard";
import CourseCardSkeleton from "@/components/card/CourseCardSkeleton";
import ReviewCard from "@/components/card/ReviewCard";
import ReviewCardSkeleton from "@/components/card/ReviewCardSkeleton";
import EmptyCourse from "@/components/molecules/EmptyCourse";
import { HStack } from "@/components/ui/hstack";
import { Text } from "@/components/ui/text";

export default function HomeScreen() {
const router = useRouter();

const { data: recommededCourse } = useFindPopularCourses();
const userInfo = useAtomValue(userAtom);

const { data: latestReview = [] } = useFindLatestReviews();
const {
data: recommededCourse = [],
refetch,
isLoading,
} = useFindPopularCourses(userInfo.id);

const { data: latestReview = [], isLoading: isReviewLoading } =
useFindLatestReviews();

useFocusEffect(
useCallback(() => {
refetch();
}, [refetch]),
);

return (
<CustomSafeAreaView>
<ScrollView className="flex-1 px-4" showsVerticalScrollIndicator={false}>
<View className="py-4">
<Text className="text-slate-600 text-sm">안녕하세요 👋</Text>
<Text className="font-bold text-2xl">댕댕이와 산책해요</Text>
<Text className="text-slate-600" size="sm">
안녕하세요 👋
</Text>
<Text className="font-bold" size="2xl">
댕댕이와 산책해요
</Text>
</View>
<TouchableOpacity
className="h-12 w-full rounded-l bg-slate-50 px-2"
onPress={() => router.push("/search")}
>
<View className="flex h-full flex-row items-center">
<Ionicons className="pr-2" name="search" />
<Text className="text-slate-500 text-sm">산책 코스 검색하기</Text>
<Text className="text-slate-500" size="sm">
산책 코스 검색하기
</Text>
</View>
</TouchableOpacity>

<View>
<View className="flex w-full flex-row items-center justify-between py-4">
<Text className="font-bold text-lg">추천 산책 코스</Text>
<Text className="font-bold" size="lg">
추천 산책 코스
</Text>
<TouchableOpacity
className=""
onPress={() => router.push("/search")}
>
<View className="flex flex-row items-center">
<Text className="text-slate-500 text-sm">전체보기</Text>
<Text className="text-slate-500" size="sm">
전체보기
</Text>
<Ionicons className="pl-2" name="arrow-forward" />
</View>
</TouchableOpacity>
</View>
<View>
<FlatList
contentContainerStyle={{ gap: 16 }}
data={recommededCourse}
horizontal
keyExtractor={(item) => `popular_course_${item.id}`}
nestedScrollEnabled={true}
renderItem={CourseCard}
showsHorizontalScrollIndicator={false}
/>
{isLoading ? (
<HStack className="gap-4">
<CourseCardSkeleton />
<CourseCardSkeleton />
</HStack>
) : (
<FlatList
contentContainerStyle={{
gap: 16,
width: recommededCourse.length === 0 ? "100%" : "auto",
}}
data={recommededCourse}
horizontal
keyExtractor={(item) => `popular_course_${item.id}`}
ListEmptyComponent={<EmptyCourse />}
nestedScrollEnabled={true}
renderItem={CourseCard}
showsHorizontalScrollIndicator={false}
/>
)}
</View>
</View>
<View>
<Text className="flex-start py-4 font-bold text-lg">최근 리뷰</Text>
<View>
{latestReview.map((item: ReviewDataType) => (
<ReviewCard item={item} key={item.id} />
))}
{isReviewLoading ? (
<>
<ReviewCardSkeleton />
<ReviewCardSkeleton />
</>
) : (
latestReview.map((item: ReviewDataType) => (
<ReviewCard item={item} key={item.id} />
))
)}
</View>
</View>
</ScrollView>
Expand Down
Loading
Loading