중간
Some checks failed
deploy-on-main / deploy (push) Failing after 21s

This commit is contained in:
koreacomp5
2025-11-07 23:41:52 +09:00
parent ab81a3da3d
commit bb71b892ca
16 changed files with 245 additions and 113 deletions

View File

@@ -60,13 +60,9 @@ export function BoardToolbar({ boardId }: { boardId: string }) {
<div className="order-2 md:order-1 flex items-center gap-2">
<select aria-label="정렬" className="h-8 px-2 rounded-md border border-neutral-300 bg-white text-sm" defaultValue={sort} onChange={onChangeSort}>
<option value="recent"></option>
<option value="popular"></option>
</select>
<select aria-label="기간" className="h-8 px-2 rounded-md border border-neutral-300 bg-white text-sm" defaultValue={period} onChange={onChangePeriod}>
<option value="all"></option>
<option value="1d">24</option>
<option value="1w">1</option>
<option value="1m">1</option>
<option value="views"></option>
<option value="likes"></option>
<option value="comments"></option>
</select>
</div>
</div>

View File

@@ -10,7 +10,38 @@ type SubItem = { id: string; name: string; href: string };
const fetcher = (url: string) => fetch(url).then((r) => r.json());
export function HeroBanner({ subItems, activeSubId, hideSubOnMobile }: { subItems?: SubItem[]; activeSubId?: string; hideSubOnMobile?: boolean }) {
export function HeroBanner({ subItems, activeSubId, hideSubOnMobile, showPartnerCats }: { subItems?: SubItem[]; activeSubId?: string; hideSubOnMobile?: boolean; showPartnerCats?: boolean }) {
const usePartnerCats = ((!Array.isArray(subItems) || subItems.length === 0) && showPartnerCats !== false);
// 파트너 카테고리 불러오기 (홈 배너 하단 블랙바에 표시)
const { data: catData } = useSWR<{ categories: any[] }>(usePartnerCats ? "/api/partner-categories" : null, fetcher, { revalidateOnFocus: false, dedupingInterval: 5 * 60 * 1000 });
const categories = catData?.categories ?? [];
const [selectedCatId, setSelectedCatId] = useState<string>("");
useEffect(() => {
if (!usePartnerCats) return;
if (!selectedCatId && categories.length > 0) {
let id = "";
try {
id = (window as any).__partnerCategoryId || localStorage.getItem("selectedPartnerCategoryId") || "";
} catch {}
if (!id) id = categories[0].id;
setSelectedCatId(id);
try {
(window as any).__partnerCategoryId = id;
localStorage.setItem("selectedPartnerCategoryId", id);
window.dispatchEvent(new CustomEvent("partnerCategorySelect", { detail: { id } }));
} catch {}
}
}, [usePartnerCats, categories, selectedCatId]);
const onSelectCategory = useCallback((id: string) => {
if (!usePartnerCats) return;
setSelectedCatId(id);
// 전역 이벤트로 선택 전달
try {
(window as any).__partnerCategoryId = id;
localStorage.setItem("selectedPartnerCategoryId", id);
window.dispatchEvent(new CustomEvent("partnerCategorySelect", { detail: { id } }));
} catch {}
}, [usePartnerCats]);
// SWR 캐시 사용: 페이지 전환 시 재요청/깜빡임 최소화
const { data } = useSWR<{ banners: Banner[] }>("/api/banners", fetcher, {
revalidateOnFocus: false,
@@ -78,23 +109,46 @@ export function HeroBanner({ subItems, activeSubId, hideSubOnMobile }: { subItem
<section className="relative w-full overflow-hidden rounded-xl bg-neutral-900 text-white" aria-roledescription="carousel">
<SelectedBanner className="h-56 sm:h-72 md:h-[264px]" />
{/* 분리된 하단 블랙 바: 높이 58px, 중앙 서브카테고리 (스켈레톤 상태에서도 동일 레이아웃 유지) */}
<div className={`${hideSubOnMobile ? "hidden md:flex" : "flex"} h-[58px] bg-black rounded-xl items-center justify-center px-2`}>
{Array.isArray(subItems) && subItems.length > 0 && (
<div className="flex flex-wrap items-center gap-[8px]">
{subItems.map((s) => (
<Link
key={s.id}
href={s.href}
<div className={`${hideSubOnMobile ? "hidden md:flex" : "flex"} h-[58px] bg-black rounded-xl items-end justify-center px-2`}>
{usePartnerCats ? (
<div className="flex flex-wrap items-center h-[74%] gap-[24px]">
{categories.map((c: any) => (
<button
key={c.id}
onClick={() => onSelectCategory(c.id)}
className={
s.id === activeSubId
? "px-3 h-[28px] rounded-full bg-[#F94B37] text-white text-[12px] leading-[28px] whitespace-nowrap"
: "px-3 h-[28px] rounded-full bg-transparent text-white/85 hover:bg-[#F94B37] hover:text-white text-[12px] leading-[28px] whitespace-nowrap"
(selectedCatId || categories[0]?.id) === c.id
? "px-3 h-full inline-flex items-center bg-white text-[#d73b29] text-[20px] font-[700] rounded-tl-[14px] rounded-tr-[14px] whitespace-nowrap"
: "px-3 h-full inline-flex items-center bg-transparent text-[#d5d5d5] hover:bg-white hover:text-[#d73b29] hover:font-[700] hover:rounded-tl-[14px] hover:rounded-tr-[14px] text-[20px] font-[500] whitespace-nowrap"
}
>
{s.name}
</Link>
{c.name}
</button>
))}
</div>
) : (
Array.isArray(subItems) && subItems.length > 0 && (
<div className="flex flex-wrap items-center h-[74%] gap-[24px]">
{subItems.map((s) => (
<Link
key={s.id}
href={s.href}
className={
s.id === activeSubId
? "px-3 h-full inline-flex items-center bg-white text-[#d73b29] text-[20px] font-[700] rounded-tl-[14px] rounded-tr-[14px] whitespace-nowrap"
: "px-3 h-full inline-flex items-center bg-transparent text-[#d5d5d5] hover:bg-white hover:text-[#d73b29] hover:font-[700] hover:rounded-tl-[14px] hover:rounded-tr-[14px] text-[20px] font-[500] whitespace-nowrap"
}
>
{s.name}
</Link>
))}
</div>
)
)}
{!usePartnerCats && (!Array.isArray(subItems) || subItems.length === 0) && (
<div className="flex items-center gap-[8px]">
<span className="px-3 h-[28px] rounded-full bg-transparent text-white/85 text-[12px] leading-[28px] whitespace-nowrap cursor-default"></span>
</div>
)}
</div>
</section>
@@ -177,23 +231,46 @@ export function HeroBanner({ subItems, activeSubId, hideSubOnMobile }: { subItem
</div>
{/* 분리된 하단 블랙 바: 높이 58px, 중앙 서브카테고리 */}
<div className={`${hideSubOnMobile ? "hidden md:flex" : "flex"} h-[58px] bg-black rounded-xl items-center justify-center px-2`}>
{Array.isArray(subItems) && subItems.length > 0 && (
<div className="flex flex-wrap items-center gap-[8px]">
{subItems.map((s) => (
<Link
key={s.id}
href={s.href}
<div className={`${hideSubOnMobile ? "hidden md:flex" : "flex"} h-[58px] bg-black rounded-xl items-end justify-center px-2`}>
{usePartnerCats ? (
<div className="flex flex-wrap items-center h-[74%] gap-[24px]">
{categories.map((c: any) => (
<button
key={c.id}
onClick={() => onSelectCategory(c.id)}
className={
s.id === activeSubId
? "px-3 h-[28px] rounded-full bg-[#F94B37] text-white text-[12px] leading-[28px] whitespace-nowrap"
: "px-3 h-[28px] rounded-full bg-transparent text-white/85 hover:bg-[#F94B37] hover:text-white text-[12px] leading-[28px] whitespace-nowrap"
(selectedCatId || categories[0]?.id) === c.id
? "px-3 h-full inline-flex items-center bg-white text-[#d73b29] text-[20px] font-[700] rounded-tl-[14px] rounded-tr-[14px] whitespace-nowrap"
: "px-3 h-full inline-flex items-center bg-transparent text-[#d5d5d5] hover:bg-white hover:text-[#d73b29] hover:font-[700] hover:rounded-tl-[14px] hover:rounded-tr-[14px] text-[20px] font-[500] whitespace-nowrap"
}
>
{s.name}
</Link>
{c.name}
</button>
))}
</div>
) : (
Array.isArray(subItems) && subItems.length > 0 && (
<div className="flex flex-wrap items-center h-[74%] gap-[24px]">
{subItems.map((s) => (
<Link
key={s.id}
href={s.href}
className={
s.id === activeSubId
? "px-3 h-full inline-flex items-center bg-white text-[#d73b29] text-[20px] font-[700] rounded-tl-[14px] rounded-tr-[14px] whitespace-nowrap"
: "px-3 h-full inline-flex items-center bg-transparent text-[#d5d5d5] hover:bg-white hover:text-[#d73b29] hover:font-[700] hover:rounded-tl-[14px] hover:rounded-tr-[14px] text-[20px] font-[500] whitespace-nowrap"
}
>
{s.name}
</Link>
))}
</div>
)
)}
{!usePartnerCats && (!Array.isArray(subItems) || subItems.length === 0) && (
<div className="flex items-center gap-[8px]">
<span className="px-3 h-[28px] rounded-full bg-transparent text-white/85 text-[12px] leading-[28px] whitespace-nowrap cursor-default"></span>
</div>
)}
</div>

View File

@@ -25,6 +25,9 @@ export default function HorizontalCardScroller({ items }: HorizontalCardScroller
const CARD_WIDTH = 384;
const CARD_GAP = 16; // Tailwind gap-4
const SCROLL_STEP = CARD_WIDTH + CARD_GAP;
useEffect(() => {
console.log(items);
}, [items]);
const updateThumb = useCallback(() => {
const scroller = scrollRef.current;
@@ -145,7 +148,7 @@ export default function HorizontalCardScroller({ items }: HorizontalCardScroller
</div>
</div>
<div className="pointer-events-none absolute bottom-[20px] left-1/2 -translate-x-1/2 z-20 flex items-center gap-6">
<div className="pointer-events-none absolute bottom-[-5px] left-1/2 -translate-x-1/2 z-20 flex items-center gap-6">
<button
type="button"
aria-label="이전"

View File

@@ -0,0 +1,63 @@
"use client";
import useSWR from "swr";
import { useEffect, useMemo, useState } from "react";
import HorizontalCardScroller from "@/app/components/HorizontalCardScroller";
const fetcher = (url: string) => fetch(url).then((r) => r.json());
export default function PartnerScroller() {
const { data: catData } = useSWR<{ categories: any[] }>("/api/partner-categories", fetcher);
const categories = catData?.categories ?? [];
const defaultCatId = categories[0]?.id || "";
const [selectedId, setSelectedId] = useState<string>(defaultCatId);
useEffect(() => {
if (!selectedId) {
let id = "";
try {
id = (window as any).__partnerCategoryId || localStorage.getItem("selectedPartnerCategoryId") || "";
} catch {}
if (!id) id = defaultCatId;
if (id) setSelectedId(id);
}
}, [defaultCatId, selectedId]);
// Listen to HeroBanner selection events
useEffect(() => {
// initialize from global if present
try {
const initial = (window as any).__partnerCategoryId;
const stored = localStorage.getItem("selectedPartnerCategoryId");
if (initial) setSelectedId(initial);
else if (stored) setSelectedId(stored);
} catch {}
const handler = (e: Event) => {
const ce = e as CustomEvent<{ id: string }>;
if (ce?.detail?.id) setSelectedId(ce.detail.id);
};
window.addEventListener("partnerCategorySelect", handler as EventListener);
return () => window.removeEventListener("partnerCategorySelect", handler as EventListener);
}, []);
const partnersQuery = useMemo(() => (selectedId ? `/api/partners?categoryId=${encodeURIComponent(selectedId)}` : "/api/partners"), [selectedId]);
const { data: partnersData } = useSWR<{ partners: any[] }>(partnersQuery, fetcher);
const partners = partnersData?.partners ?? [];
// Fallback to approved partner requests if no partners
const { data: reqData } = useSWR<{ items: any[] }>(partners.length === 0 ? "/api/partner-shops" : null, fetcher);
const activeCategoryName = useMemo(() => categories.find((c: any) => c.id === selectedId)?.name, [categories, selectedId]);
const fallbackItems = useMemo(() => {
if (!reqData?.items) return [] as any[];
const filtered = activeCategoryName ? reqData.items.filter((it: any) => it.category === activeCategoryName) : reqData.items;
return filtered.map((s: any) => ({ id: s.id, region: s.region, name: s.name, address: s.address, image: s.imageUrl || "/sample.jpg" }));
}, [reqData, activeCategoryName]);
const items = partners.length > 0
? partners.map((p: any) => ({ id: p.id, region: p.address ? String(p.address).split(" ")[0] : p.category, name: p.name, address: p.address || "", image: p.imageUrl || "/sample.jpg" }))
: fallbackItems;
if (items.length === 0) return null;
return <HorizontalCardScroller items={items} />;
}

View File

@@ -24,7 +24,7 @@ type Resp = {
page: number;
pageSize: number;
items: Item[];
sort: "recent" | "popular";
sort: "recent" | "popular" | "views" | "likes" | "comments";
};
const fetcher = (url: string) => fetch(url).then((r) => r.json());
@@ -35,7 +35,7 @@ function stripHtml(html: string | null | undefined): string {
return html.replace(/<[^>]*>/g, "").trim();
}
export function PostList({ boardId, sort = "recent", q, tag, author, authorId, start, end, variant = "default", newPostHref, titleHoverOrange, pageSizeOverride, compact }: { boardId?: string; sort?: "recent" | "popular"; q?: string; tag?: string; author?: string; authorId?: string; start?: string; end?: string; variant?: "default" | "board"; newPostHref?: string; titleHoverOrange?: boolean; pageSizeOverride?: number; compact?: boolean }) {
export function PostList({ boardId, sort = "recent", q, tag, author, authorId, start, end, variant = "default", newPostHref, titleHoverOrange, pageSizeOverride, compact }: { boardId?: string; sort?: "recent" | "popular" | "views" | "likes" | "comments"; q?: string; tag?: string; author?: string; authorId?: string; start?: string; end?: string; variant?: "default" | "board"; newPostHref?: string; titleHoverOrange?: boolean; pageSizeOverride?: number; compact?: boolean }) {
const sp = useSearchParams();
const listContainerRef = useRef<HTMLDivElement | null>(null);
const [lockedMinHeight, setLockedMinHeight] = useState<number | null>(null);