Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
fa9a531
feat/#68: 파티 리스트 API 엔드포인트 추가
donut74 Dec 30, 2025
3cef998
feat/#68: 파티 리스트 API 타입 정의 추가
donut74 Dec 30, 2025
5e64313
refactor/#68: 파티 리스트 엔드포인트 이름 변경
donut74 Dec 30, 2025
b57428d
feat/#68: 파티 리스트 조회 API 함수 추가
donut74 Dec 30, 2025
e2013ac
feat/#68: 파티 리스트 조회 React Query hook 추가
donut74 Dec 30, 2025
4bd44f7
feat/#68: 메인 리스트 카드 Api 데이터 연동
donut74 Dec 30, 2025
ceb558c
chore/#68: 파티 리스트 임시 데이터 추가
donut74 Dec 30, 2025
83ca212
feat/#68: 파티 리스트 컴포넌트 추가
donut74 Dec 30, 2025
d229c17
feat/#68: 메인 화면 파티 리스트 API 연동
donut74 Dec 30, 2025
e80cbd2
feat/#68: 파티 리스트 카테고리 필터 기능 추가
donut74 Dec 30, 2025
0576b1f
feat/#68: 카테고리 필터 클릭 기능 추가
donut74 Dec 30, 2025
2224188
chore/#68: 필터화에 맞게 mockdata 수정
donut74 Dec 30, 2025
d96b675
feat/#68: 카테고리 컴포넌트 메인화면 연결
donut74 Dec 30, 2025
d4f6521
style/#68: 카테고리 투명 보더 추가로 위치 이동 없이 수정
donut74 Dec 30, 2025
43828be
chore/#68: 공지 아이콘 추가
donut74 Dec 30, 2025
7fd8a21
feat/#68: 데이터 없을 때 UI 추가
donut74 Dec 30, 2025
98ba564
feat/#68: 마감파티 시에 UI 추가
donut74 Dec 30, 2025
f68e7af
feat/#68: 마감파티 이미지 블러 효과 추가
donut74 Dec 30, 2025
55ce8e4
feat/#68: 이미지 없을 때 기본 이미지 표시
donut74 Dec 30, 2025
e03c837
feat/#68: 메인화면 헤더 location 데이터 연결
donut74 Dec 30, 2025
d635c57
feat/#68: 헤더 닉네임 데이터 연결
donut74 Dec 30, 2025
0ae0aa3
chore/#68: 백 데이터 연결로 mockdata 삭제
donut74 Dec 30, 2025
0bb07a6
feat/#68: 파티 카드 이미지 에러 처리 및 기본 이미지 표시 기능 추가
donut74 Dec 30, 2025
3b74c94
chore/#68: 불필요한 콘솔 로그 제거 및 주석 처리
donut74 Dec 30, 2025
97f1c15
Merge remote-tracking branch 'origin/develop' into feat/68-메인화면-리스트-A…
donut74 Dec 30, 2025
25bad07
feat/#68: 데이터 불러올 때 isLoading을 이용해 빈 데이터 화면이 나오지 않도록 함
donut74 Dec 30, 2025
fd3ab0e
style/#68: 카테고리 activeOpacity 값 조정
donut74 Dec 30, 2025
3b9db8f
feat/#68: 파티 리스트 API 유효성 검사 및 에러 처리 추가
donut74 Dec 31, 2025
be10cb2
style/#68: PartyList paddingBottom 값 조정
donut74 Dec 31, 2025
32e08d0
refactor/#68: 파티 리스트 API 유효성 검사 로직 제거
donut74 Jan 1, 2026
044956d
feat/#68: 파티 카테고리 전역 상태 관리 스토어 추가
donut74 Jan 1, 2026
4c8c397
refactor/#68: PartyList 컴포넌트를 독립적으로 리팩토링
donut74 Jan 1, 2026
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
41 changes: 41 additions & 0 deletions TinyBite/api/partyApi.ts
Copy link
Collaborator

Choose a reason for hiding this comment

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

isValidPartyItem와 isValidPartyListResponse를 구현하게 된 이유가 궁금합니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

타입스크립트의 런타임 에러 방지를 위해 추가했던 로직이었는데,
API 응답 타입이 이미 명시되어 있어 중복되는 검증 로직인것 같네요,,,
해당 부분은 수정하겠습니다!!

Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { PartyListParams, PartyListResponse } from "@/types/party";
import { privateAxios } from "./axios";
import { ENDPOINT } from "./urls";

/**
* 파티 리스트 조회 API
* @param params 파티 리스트 조회 파라미터 (category, latitude, longitude)
* @returns 파티 리스트 응답 데이터
*/
export const getPartyList = async (
params: PartyListParams
): Promise<PartyListResponse> => {
try {
const res = await privateAxios.get(ENDPOINT.PARTY.GET_PARTIES, {
params: {
category: params.category,
latitude: params.latitude,
longitude: params.longitude,
},
});

const responseData = res.data;

// hasNext와 totalCount가 없으면 기본값 설정
return {
activeParties: responseData.activeParties || [],
closedParties: responseData.closedParties || [],
hasNext: responseData.hasNext ?? false,
totalCount: responseData.totalCount ?? 0,
};
} catch (error) {
console.error("파티 리스트 로딩 실패:", error);
// 에러 발생 시 빈 데이터 구조를 던져주면 "파티가 없어요" 화면을 보여줍니다.
return {
activeParties: [],
closedParties: [],
hasNext: false,
totalCount: 0,
};
}
};
3 changes: 3 additions & 0 deletions TinyBite/api/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,7 @@ export const ENDPOINT = {
LOCATION: {
FIND_LOCATION: "/api/v1/auth/location",
},
PARTY: {
GET_PARTIES: "/api/parties",
},
};
33 changes: 3 additions & 30 deletions TinyBite/app/(tabs)/index.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import FloatingMenuButton from "@/components/main/FloatingMenuButton";
import FloatingMenuOverlay from "@/components/main/FloatingMenuOverlay";
import MainCard from "@/components/main/MainCard";
import MainCategory from "@/components/main/MainCategory";
import MainHeader from "@/components/main/MainHeader";
import PartyList from "@/components/main/PartyList";
import { colors } from "@/styles/colors";
import { useRouter } from "expo-router";
import { useState } from "react";

import { ScrollView, StyleSheet, View } from "react-native";
import { StyleSheet, View } from "react-native";

export default function HomeScreen() {
const router = useRouter();
const [isMenuOpen, setIsMenuOpen] = useState(false);

return (
Expand All @@ -21,19 +19,7 @@ export default function HomeScreen() {
<MainCategory />
</View>

<ScrollView
style={styles.scroll}
contentContainerStyle={styles.listWrapper}
>
<View style={styles.cardWrapper}>
<MainCard onPress={() => router.push("/main-card-detail")} />
<MainCard />
<MainCard />
<MainCard />
<MainCard />
<MainCard />
</View>
</ScrollView>
<PartyList />

{/* 플로팅 버튼 - 항상 표시 */}
<View style={styles.floatingButtonContainer}>
Expand All @@ -54,19 +40,6 @@ const styles = StyleSheet.create({
flex: 1,
backgroundColor: colors.background,
},
scroll: {
backgroundColor: colors.background,
},
listWrapper: {
alignItems: "center",
marginTop: 5,
marginLeft: 20,
marginRight: 20,
},
cardWrapper: {
gap: 16,
marginBottom: 16,
},
categoryWrapper: {
marginTop: 15, //카테고리 마진 5 뺀 15
marginBottom: 10, //UI 가림 떄문에 리스트에 마진 5+ 카테고리 마진 5 합친 값 뺀 10
Expand Down
Binary file added TinyBite/assets/images/main/notice-100.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
162 changes: 133 additions & 29 deletions TinyBite/components/main/MainCard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { colors } from "@/styles/colors";
import { textStyles } from "@/styles/typography/textStyles";
import { PartyItem } from "@/types/party";
import { useState } from "react";
import {
Image,
Pressable,
Expand All @@ -9,36 +11,107 @@ import {
ViewStyle,
} from "react-native";

const MainCard = ({
onPress,
containerStyle,
}: {
interface MainCardProps {
item: PartyItem;
onPress?: () => void;
containerStyle?: ViewStyle;
}) => (
<Pressable onPress={onPress} style={[styles.card, containerStyle]}>
<Image
source={require("@/assets/images/mainlist/food1.jpg")}
style={styles.thumbnail}
/>
<View style={styles.cardBody}>
<View>
<Text style={[styles.title, textStyles.body16_B150]}>
후문 엽떡 나누실 분 ㅃㄹ
</Text>
<Text style={[styles.price, textStyles.body15_SB135]}>5,000원</Text>
}

const MainCard = ({ item, onPress, containerStyle }: MainCardProps) => {
// item이 없으면 렌더링하지 않음
if (!item) {
return null;
}

// 이미지 로딩 에러 상태 관리
const [imageError, setImageError] = useState(false);

// 가격 포맷팅 (예: 5000 -> "5,000원")
const formattedPrice = `${item.pricePerPerson.toLocaleString()}원`;

// 카테고리별 아이콘 매핑
const getCategoryIcon = (category: string) => {
switch (category) {
case "ALL":
return require("@/assets/images/main/notice-100.png");
case "DELIVERY":
return require("@/assets/images/main/category/delivery.png");
case "GROCERY":
return require("@/assets/images/main/category/grocery.png");
case "HOUSEHOLD":
return require("@/assets/images/main/category/essentials.png");
default:
return null;
}
};

const hasImage =
item.thumbnailImage && item.thumbnailImage.trim() !== "" && !imageError;
const categoryIcon = getCategoryIcon(item.category);

return (
<Pressable onPress={onPress} style={[styles.card, containerStyle]}>
<View style={styles.thumbnailContainer}>
{hasImage ? (
<>
<Image
source={{ uri: item.thumbnailImage }}
style={styles.thumbnail}
resizeMode="cover"
blurRadius={item.isClosed ? 2 : 0}
onError={() => {
console.warn("이미지 로딩 실패:", item.thumbnailImage);
setImageError(true);
}}
/>
{item.isClosed && <View style={styles.overlay} />}
</>
) : (
<View style={styles.thumbnailPlaceholder}>
{categoryIcon && (
<Image
source={categoryIcon}
style={styles.categoryIcon}
resizeMode="contain"
blurRadius={item.isClosed ? 2 : 0}
/>
)}
{item.isClosed && <View style={styles.overlay} />}
</View>
)}
</View>
<View style={styles.footerRow}>
<View style={styles.badge}>
<Text style={[styles.badgeText, textStyles.body13_SB135]}>1/4명</Text>
<View style={styles.cardBody}>
<View>
<Text
style={[styles.title, textStyles.body16_B150]}
numberOfLines={1}
>
{item.title}
</Text>
<Text style={[styles.price, textStyles.body15_SB135]}>
{formattedPrice}
</Text>
</View>
<View style={styles.footerRow}>
<View style={[styles.badge, item.isClosed && styles.badgeClosed]}>
<Text
style={[
styles.badgeText,
item.isClosed && styles.badgeTextClosed,
textStyles.body13_SB135,
]}
>
{item.isClosed ? "마감" : item.participantStatus}
</Text>
</View>
<Text style={[styles.meta, textStyles.body13_SB135]}>
{item.distance} | {item.timeAgo}
</Text>
</View>
<Text style={[styles.meta, textStyles.body13_SB135]}>
10KM 이내 | 10분 전
</Text>
</View>
</View>
</Pressable>
);
</Pressable>
);
};

export default MainCard;

Expand All @@ -60,10 +133,38 @@ const styles = StyleSheet.create({
shadowRadius: 2,
elevation: 4,
},
thumbnail: {
thumbnailContainer: {
width: 90,
height: 90,
borderRadius: 16,
position: "relative",
overflow: "hidden",
},
thumbnail: {
width: "100%",
height: "100%",
borderRadius: 16,
},
thumbnailPlaceholder: {
width: "100%",
height: "100%",
borderRadius: 16,
backgroundColor: colors.sub,
justifyContent: "center",
alignItems: "center",
},
categoryIcon: {
width: 60,
height: 60,
},
overlay: {
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "rgba(0, 0, 0, 0.4)",
borderRadius: 16,
},
cardBody: {
flex: 1,
Expand All @@ -84,18 +185,21 @@ const styles = StyleSheet.create({
},
badge: {
backgroundColor: colors.main,
padding: 0.5,
width: 51,
height: 26,
borderRadius: 100,
paddingHorizontal: 10,
paddingVertical: 4,
justifyContent: "center",
alignItems: "center",
},
badgeClosed: {
backgroundColor: colors.gray[2],
},
badgeText: {
color: colors.white,
},
badgeTextClosed: {
color: colors.white,
},
meta: {
color: colors.gray[1],
},
Expand Down
Loading