Skip to content

Commit 6e23d94

Browse files
authored
Merge pull request #81 from whatjang/feature/sheepyis-search-api#74
✨ Feat: 시장 검색 API 연동 완료
2 parents 7258e46 + 8cb388f commit 6e23d94

21 files changed

Lines changed: 670 additions & 65 deletions

File tree

.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Kakao Developers에서 발급한 REST API Key
2+
NEXT_PUBLIC_KAKAO_REST_API_KEY=
3+
4+
# Backend API 기본 주소
5+
# Example: https://api.example.com
6+
NEXT_PUBLIC_API_BASE_URL=

package-lock.json

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"pretendard": "^1.3.9",
2323
"react": "19.2.4",
2424
"react-dom": "19.2.4",
25+
"react-spinners": "^0.17.1",
2526
"zustand": "^5.0.15"
2627
},
2728
"devDependencies": {

src/app/(main)/search/_components/MarketItem.tsx

Lines changed: 49 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,98 +1,105 @@
1-
import type { LucideIcon } from "lucide-react";
2-
import {
3-
Apple,
4-
Beef,
5-
Carrot,
6-
ChevronRight,
7-
CookingPot,
8-
Drumstick,
9-
Fish,
10-
MapPin,
11-
Navigation,
12-
} from "lucide-react";
1+
import { ChevronRight, MapPin, Navigation } from "lucide-react";
132
import Link from "next/link";
143

15-
import type { Market, MarketSpecialtyIcon } from "@/src/types/market";
4+
import {
5+
DEFAULT_PRODUCT_ICON,
6+
PRODUCT_ICONS,
7+
} from "@/src/constants/marketProductIcons";
8+
import type { MarketSearchItem } from "@/src/types/market/marketSearch";
169

1710
interface MarketItemProps {
18-
market: Market;
11+
market: MarketSearchItem;
1912
}
2013

21-
const specialtyIcons: Record<MarketSpecialtyIcon, LucideIcon> = {
22-
seafood: Fish,
23-
"fried-chicken": Drumstick,
24-
vegetable: Carrot,
25-
meat: Beef,
26-
fruit: Apple,
27-
food: CookingPot,
28-
};
14+
function isToday(date: string) {
15+
const today = new Date();
16+
const year = today.getFullYear();
17+
const month = String(today.getMonth() + 1).padStart(2, "0");
18+
const day = String(today.getDate()).padStart(2, "0");
19+
20+
return date === `${year}-${month}-${day}`;
21+
}
2922

3023
export default function MarketItem({ market }: MarketItemProps) {
31-
const marketDayText = market.marketDays.join(", ");
24+
const marketDayText =
25+
market.open_day_numbers.length > 0
26+
? `${market.open_day_numbers.join("·")}일 장`
27+
: "상설장";
28+
const isMarketDayToday =
29+
market.open_day_numbers.length > 0 && isToday(market.next_open_date);
3230

3331
return (
3432
<li className="min-w-full">
3533
<article className="group overflow-hidden rounded-3xl bg-white">
3634
<div className="bg-light-gray relative aspect-2/1 min-h-40">
37-
{market.isOpenToday && (
35+
{isMarketDayToday && (
3836
<span className="bg-green absolute top-4 left-4 rounded-full px-3 py-1 text-sm font-bold text-white shadow-sm">
3937
오늘 장날 (Today)
4038
</span>
4139
)}
4240
</div>
4341

4442
<div className="flex flex-col gap-2 p-4">
45-
<div className="flex items-center justify-between">
46-
<h3 className="text-green text-lg font-bold">{market.name}</h3>
43+
<div className="flex items-start justify-between gap-3">
44+
<h3 className="text-green line-clamp-2 min-w-0 flex-1 text-lg font-bold">
45+
{market.name}
46+
</h3>
4747

48-
<span className="border-light-brown/20 bg-light-brown/10 text-light-brown rounded-full border px-2 py-0.5 text-xs font-bold">
49-
{marketDayText}일 주기
48+
<span className="border-light-brown/20 bg-light-brown/10 text-light-brown shrink-0 rounded-full border px-2 py-0.5 text-xs font-bold">
49+
{marketDayText}
5050
</span>
5151
</div>
5252

53-
<div className="flex items-center gap-1">
53+
<div className="flex items-start gap-1">
5454
<MapPin className="h-4 w-4" strokeWidth={2} aria-hidden="true" />
5555

56-
<p className="text-xs font-semibold">{market.address}</p>
56+
<p className="text-xs font-semibold">
57+
{market.road_address?.trim() || "주소 정보 알 수 없음"}
58+
</p>
5759
</div>
5860

5961
<ul
6062
className="mt-2 flex flex-wrap items-center gap-2"
6163
aria-label={`${market.name} 대표 상품`}
6264
>
63-
{market.specialties.map((specialty) => {
64-
const SpecialtyIcon = specialtyIcons[specialty.icon];
65+
{market.products.map((product) => {
66+
const ProductIcon =
67+
PRODUCT_ICONS[product] ?? DEFAULT_PRODUCT_ICON;
6568

6669
return (
6770
<li
68-
key={`${market.id}-${specialty.label}`}
71+
key={`${market.market_id}-${product}`}
6972
className="flex items-center gap-1 text-xs font-semibold"
7073
>
71-
<SpecialtyIcon
74+
<ProductIcon
7275
className="text-green h-4 w-4"
7376
strokeWidth={2}
7477
aria-hidden="true"
7578
/>
7679

77-
<span>{specialty.label}</span>
80+
<span>{product}</span>
7881
</li>
7982
);
8083
})}
8184
</ul>
8285

8386
<div className="border-deep-gray text-green mt-2 border-t py-3 font-bold">
8487
<Link
85-
href={`/markets/${market.id}`}
88+
href={`/markets/${market.market_id}`}
8689
className="flex items-center justify-between text-xs"
8790
aria-label={`${market.name} 상세보기`}
8891
>
8992
<span className="flex items-center gap-1">
90-
<Navigation
91-
className="h-4 w-4"
92-
strokeWidth={2}
93-
aria-hidden="true"
94-
/>
95-
{market.distanceKm.toFixed(1)}km 인근
93+
{market.distance_km !== null && (
94+
<>
95+
<Navigation
96+
className="h-4 w-4"
97+
strokeWidth={2}
98+
aria-hidden="true"
99+
/>
100+
{market.distance_km.toFixed(1)}km 인근
101+
</>
102+
)}
96103
</span>
97104

98105
<span className="flex items-center">
Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,73 @@
1-
import { markets } from "@/src/mocks/market";
1+
"use client";
2+
3+
import Spinner from "@/src/components/common/Spinner";
4+
import { useInfiniteScroll } from "@/src/hooks/useInfiniteScroll";
5+
import type { MarketSearchItem } from "@/src/types/market/marketSearch";
26

37
import MarketItem from "./MarketItem";
48

5-
export default function MarketList() {
9+
interface MarketListProps {
10+
markets: MarketSearchItem[];
11+
totalCount: number;
12+
isLoading: boolean;
13+
hasSearched: boolean;
14+
hasNext: boolean;
15+
onLoadMore: () => void;
16+
}
17+
18+
export default function MarketList({
19+
markets,
20+
totalCount,
21+
isLoading,
22+
hasSearched,
23+
hasNext,
24+
onLoadMore,
25+
}: MarketListProps) {
26+
const observerRef = useInfiniteScroll({
27+
hasNext,
28+
isLoading,
29+
onLoadMore,
30+
});
31+
32+
if (!hasSearched) {
33+
return null;
34+
}
35+
36+
if (markets.length === 0 && isLoading) {
37+
return (
38+
<div className="flex w-full justify-center py-10">
39+
<Spinner />
40+
</div>
41+
);
42+
}
43+
644
if (markets.length === 0) {
7-
return <p className="text-center text-sm">검색된 데이터가 없습니다.</p>;
45+
return <p className="py-10 text-center text-xs">검색된 시장이 없습니다.</p>;
846
}
947

1048
return (
11-
<ul className="flex w-full flex-col gap-4">
12-
{markets.map((market) => (
13-
<MarketItem key={market.id} market={market} />
14-
))}
15-
</ul>
49+
<>
50+
<div className="border-green flex items-center justify-between border-b pb-2">
51+
<p className="text-deep-gray text-sm font-bold">검색 결과</p>
52+
53+
<span className="bg-green/10 text-green rounded-full px-2.5 py-1 text-xs font-bold">
54+
{totalCount}
55+
</span>
56+
</div>
57+
58+
<ul className="flex w-full flex-col gap-4">
59+
{markets.map((market) => (
60+
<MarketItem key={market.market_id} market={market} />
61+
))}
62+
</ul>
63+
64+
{isLoading && (
65+
<div className="flex w-full justify-center py-6">
66+
<Spinner />
67+
</div>
68+
)}
69+
70+
{hasNext && <div ref={observerRef} className="h-1" />}
71+
</>
1672
);
1773
}

0 commit comments

Comments
 (0)