From 33df4cbcdd65c7f5c4f676c2e70a7cdd5caa7408 Mon Sep 17 00:00:00 2001 From: uiuuoq Date: Tue, 28 Apr 2026 13:32:30 +0900 Subject: [PATCH] =?UTF-8?q?DP-394:=20=ED=99=88=20=EA=B2=80=EC=83=89=20?= =?UTF-8?q?=EC=98=A4=EB=B2=84=EB=A0=88=EC=9D=B4=20=ED=8A=B8=EB=A0=8C?= =?UTF-8?q?=EB=93=9C=20=EB=B6=84=EC=84=9D=20API=20=EC=8A=A4=ED=8E=99=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=20=EB=B0=8F=20UI=20=EB=B3=B4=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../features/home/search/HomeRangeTabs.tsx | 32 +- .../home/search/HomeSearchOverlay.tsx | 57 +-- .../search/HomeSearchResultAccordionItem.tsx | 5 +- .../home/search/HomeSearchResultsSection.tsx | 3 + .../home/search/HomeTopPostsSection.tsx | 27 +- .../search/HomeTrendingKeywordsSection.tsx | 36 +- lib/mock/home-search-trend.ts | 327 ++++++++---------- types/search.ts | 24 +- 8 files changed, 271 insertions(+), 240 deletions(-) diff --git a/components/features/home/search/HomeRangeTabs.tsx b/components/features/home/search/HomeRangeTabs.tsx index 3327768..0173d33 100644 --- a/components/features/home/search/HomeRangeTabs.tsx +++ b/components/features/home/search/HomeRangeTabs.tsx @@ -1,26 +1,38 @@ "use client"; import { cn } from "@/lib/utils"; -import type { TrendRange } from "@/lib/mock/home-search-trend"; +import type { TrendRange } from "@/types/search"; const TABS: { value: TrendRange; label: string }[] = [ - { value: "day", label: "일" }, - { value: "week", label: "주" }, - { value: "month", label: "월" }, + { value: "daily", label: "일간" }, + { value: "weekly", label: "주간" }, + { value: "monthly", label: "월간" }, ]; interface HomeRangeTabsProps { - range: TrendRange; - onChange: (range: TrendRange) => void; + unit: TrendRange; + onChange: (unit: TrendRange) => void; dateLabel: string; /** 외부에서 좌우 패딩 등 레이아웃을 제어할 때 사용 */ className?: string; } -export function HomeRangeTabs({ range, onChange, dateLabel, className }: HomeRangeTabsProps) { +export function HomeRangeTabs({ + unit, + onChange, + dateLabel, + className, +}: HomeRangeTabsProps) { return ( -
- {dateLabel || " "} +
+ + {dateLabel || " "} +
{TABS.map((tab) => ( ))} diff --git a/lib/mock/home-search-trend.ts b/lib/mock/home-search-trend.ts index 5233133..de73303 100644 --- a/lib/mock/home-search-trend.ts +++ b/lib/mock/home-search-trend.ts @@ -7,396 +7,371 @@ import type { export type { TrendRange, TrendTopPost, TrendKeywordItem, HomeTrendData }; const MOCK_HOME_TREND: Record = { - day: { - dateLabel: "2026-04-23 기준", + daily: { + unit: "daily", + periodStart: "2026-04-28", + periodEnd: "2026-04-28", + dateLabel: "4월 28일", topPosts: [ { rank: 1, id: "trend-day-1", - title: "React 서버 컴포넌트와 클라이언트 컴포넌트 경계 완전 이해", + title: + "Understanding the Boundary Between React Server and Client Components", + translatedTitle: + "리액트 서버 컴포넌트와 클라이언트 컴포넌트 경계 완전 이해", sourceName: "velog", tags: ["React", "Next.js"], viewCount: 1247, thumbnailUrl: "https://picsum.photos/seed/react-server/400/225", category: "Frontend", changeRate: 18.4, + isMyInterest: false, }, { rank: 2, id: "trend-day-2", - title: "TypeScript 5.4 NoInfer 유틸리티 타입 파헤치기", + title: "Deep Dive into TypeScript 5.4 NoInfer Utility Type", + translatedTitle: "타입스크립트 5.4 NoInfer 유틸리티 타입 파헤치기", sourceName: "toss_tech", tags: ["TypeScript"], viewCount: 982, thumbnailUrl: null, category: "Frontend", changeRate: 7.2, + isMyInterest: false, }, { rank: 3, id: "trend-day-3", - title: "AWS Lambda + API Gateway로 서버리스 아키텍처 구축하기", + title: + "Building Serverless Architecture with AWS Lambda and API Gateway", + translatedTitle: + "AWS Lambda + API Gateway로 서버리스 아키텍처 구축하기", sourceName: "naver_d2", tags: ["AWS", "Serverless"], viewCount: 834, thumbnailUrl: "https://picsum.photos/seed/aws-lambda/400/225", category: "DevOps", changeRate: -3.1, + isMyInterest: false, }, { rank: 4, id: "trend-day-4", - title: "Tailwind CSS v4 마이그레이션 실전 가이드", + title: "Practical Guide to Tailwind CSS v4 Migration", + translatedTitle: "Tailwind CSS v4 마이그레이션 실전 가이드", sourceName: "velog", tags: ["Tailwind", "CSS"], viewCount: 721, thumbnailUrl: null, category: "Frontend", changeRate: 24.6, + isMyInterest: false, }, { rank: 5, id: "trend-day-5", - title: "PostgreSQL 쿼리 성능 분석과 인덱스 전략", + title: "PostgreSQL Query Performance Analysis and Index Strategy", + translatedTitle: "PostgreSQL 쿼리 성능 분석과 인덱스 전략", sourceName: "kakao_tech", tags: ["PostgreSQL", "DB"], viewCount: 658, thumbnailUrl: "https://picsum.photos/seed/postgresql/400/225", category: "Backend", changeRate: 0, + isMyInterest: false, }, ], topPostsSummary: "오늘은 React 서버 컴포넌트와 TypeScript 5.4 관련 글이 조회수 1위를 다투고 있습니다. AWS 서버리스 구축 가이드와 Tailwind CSS v4 마이그레이션 글도 빠르게 조회수가 오르고 있으며, PostgreSQL 인덱스 최적화 글이 백엔드 개발자들 사이에서 주목받고 있습니다.", - collectionSummary: "", - trendingKeywords: [ - { rank: 1, keyword: "React", count: 124, deltaType: "up", deltaValue: 3 }, - { rank: 2, keyword: "TypeScript", count: 98, deltaType: "new" }, - { rank: 3, keyword: "AWS", count: 84, deltaType: "down", deltaValue: 2 }, - { rank: 4, keyword: "Next.js", count: 79, deltaType: "same" }, - { - rank: 5, - keyword: "Tailwind", - count: 63, - deltaType: "up", - deltaValue: 1, - }, - { rank: 6, keyword: "PostgreSQL", count: 58, deltaType: "new" }, - { rank: 7, keyword: "Python", count: 52, deltaType: "up", deltaValue: 5 }, - { - rank: 8, - keyword: "Docker", - count: 47, - deltaType: "down", - deltaValue: 1, - }, - { rank: 9, keyword: "Kotlin", count: 38, deltaType: "same" }, - { rank: 10, keyword: "Redis", count: 31, deltaType: "up", deltaValue: 2 }, + collectionSummary: null, + trendingTags: [ + { rank: 1, keyword: "React", count: 124, state: "up", rankChange: 3 }, + { rank: 2, keyword: "TypeScript", count: 98, state: "new", rankChange: 0 }, + { rank: 3, keyword: "AWS", count: 84, state: "down", rankChange: 2 }, + { rank: 4, keyword: "Next.js", count: 79, state: "same", rankChange: 0 }, + { rank: 5, keyword: "Tailwind", count: 63, state: "up", rankChange: 1 }, + { rank: 6, keyword: "PostgreSQL", count: 58, state: "new", rankChange: 0 }, + { rank: 7, keyword: "Python", count: 52, state: "up", rankChange: 5 }, + { rank: 8, keyword: "Docker", count: 47, state: "down", rankChange: 1 }, + { rank: 9, keyword: "Kotlin", count: 38, state: "same", rankChange: 0 }, + { rank: 10, keyword: "Redis", count: 31, state: "up", rankChange: 2 }, ], }, - week: { - dateLabel: "2026-04-17 ~ 04-23 기준", + weekly: { + unit: "weekly", + periodStart: "2026-04-22", + periodEnd: "2026-04-28", + dateLabel: "4월 22일 주간", topPosts: [ { rank: 1, id: "trend-week-1", - title: "2025년 프론트엔드 생태계 총정리 — React, Vue, Svelte 심층 비교", + title: + "2025 Frontend Ecosystem Overview — In-Depth React, Vue, Svelte Comparison", + translatedTitle: + "2025년 프론트엔드 생태계 총정리 — React, Vue, Svelte 심층 비교", sourceName: "naver_d2", tags: ["Frontend", "React"], viewCount: 8412, thumbnailUrl: "https://picsum.photos/seed/frontend-ecosystem/400/225", category: "Frontend", changeRate: 32.1, + isMyInterest: false, }, { rank: 2, id: "trend-week-2", - title: "Spring Boot 3 + Kotlin 실전 도입기 — 팀 전환 6개월 후기", + title: + "Spring Boot 3 + Kotlin in Practice — 6 Months After Team Migration", + translatedTitle: + "Spring Boot 3 + Kotlin 실전 도입기 — 팀 전환 6개월 후기", sourceName: "우아한형제들", tags: ["Spring", "Kotlin"], viewCount: 6238, thumbnailUrl: null, category: "Backend", changeRate: 14.7, + isMyInterest: false, }, { rank: 3, id: "trend-week-3", - title: "Docker 없이 Kubernetes 로컬 개발 환경 구성하는 법", + title: "Setting Up a Local Kubernetes Dev Environment Without Docker", + translatedTitle: "Docker 없이 Kubernetes 로컬 개발 환경 구성하는 법", sourceName: "kakao_tech", tags: ["Kubernetes", "Docker"], viewCount: 5847, thumbnailUrl: "https://picsum.photos/seed/kubernetes-local/400/225", category: "DevOps", changeRate: -5.3, + isMyInterest: false, }, { rank: 4, id: "trend-week-4", - title: "LLM API로 RAG 시스템 직접 만들기 — LangChain + ChromaDB", + title: "Build a RAG System with LLM API — LangChain + ChromaDB", + translatedTitle: + "LLM API로 RAG 시스템 직접 만들기 — LangChain + ChromaDB", sourceName: "toss_tech", tags: ["AI", "Python", "LLM"], viewCount: 4921, thumbnailUrl: "https://picsum.photos/seed/rag-langchain/400/225", category: "AI", changeRate: 61.8, + isMyInterest: false, }, { rank: 5, id: "trend-week-5", - title: "모노레포 전환기 — Turborepo 도입 후 빌드 시간 70% 단축", + title: + "Monorepo Migration — 70% Faster Builds After Adopting Turborepo", + translatedTitle: + "모노레포 전환기 — Turborepo 도입 후 빌드 시간 70% 단축", sourceName: "oliveyoung_tech", tags: ["DevOps", "Monorepo"], viewCount: 4103, thumbnailUrl: null, category: "DevOps", changeRate: 9.2, + isMyInterest: false, }, ], topPostsSummary: "이번 주는 프론트엔드 생태계 비교 글과 Spring Boot + Kotlin 조합 글이 높은 조회수를 기록했습니다. 특히 AI/LLM 관련 실전 구현 글의 성장세가 두드러지며, RAG 시스템 구축 가이드가 빠르게 상위권에 진입했습니다. Kubernetes 로컬 환경 구성과 모노레포 전환 후기 등 인프라·DevOps 분야도 꾸준한 관심을 받고 있습니다.", collectionSummary: "이번 주 총 312개의 새로운 콘텐츠가 수집되었습니다. Velog 출처가 38%로 가장 많았으며, Naver D2와 Kakao Tech 블로그의 심층 기술 글이 고르게 수집되었습니다. AI·DevOps 분야 콘텐츠가 전주 대비 23% 증가했으며, Rust와 Go 관련 글도 꾸준히 유입되고 있습니다.", - trendingKeywords: [ - { rank: 1, keyword: "React", count: 841, deltaType: "up", deltaValue: 2 }, - { rank: 2, keyword: "TypeScript", count: 762, deltaType: "same" }, + trendingTags: [ + { rank: 1, keyword: "React", count: 841, state: "up", rankChange: 2 }, { - rank: 3, - keyword: "Next.js", - count: 695, - deltaType: "up", - deltaValue: 1, - }, - { rank: 4, keyword: "AI", count: 643, deltaType: "up", deltaValue: 4 }, - { - rank: 5, - keyword: "Docker", - count: 521, - deltaType: "down", - deltaValue: 1, - }, - { rank: 6, keyword: "Kubernetes", count: 482, deltaType: "new" }, - { - rank: 7, - keyword: "Python", - count: 438, - deltaType: "up", - deltaValue: 3, - }, - { - rank: 8, - keyword: "Spring", - count: 401, - deltaType: "down", - deltaValue: 2, - }, - { rank: 9, keyword: "AWS", count: 378, deltaType: "up", deltaValue: 1 }, - { rank: 10, keyword: "LLM", count: 325, deltaType: "new" }, - { rank: 11, keyword: "Kotlin", count: 287, deltaType: "same" }, - { - rank: 12, - keyword: "Monorepo", - count: 241, - deltaType: "up", - deltaValue: 2, - }, + rank: 2, + keyword: "TypeScript", + count: 762, + state: "same", + rankChange: 0, + }, + { rank: 3, keyword: "Next.js", count: 695, state: "up", rankChange: 1 }, + { rank: 4, keyword: "AI", count: 643, state: "up", rankChange: 4 }, + { rank: 5, keyword: "Docker", count: 521, state: "down", rankChange: 1 }, + { + rank: 6, + keyword: "Kubernetes", + count: 482, + state: "new", + rankChange: 0, + }, + { rank: 7, keyword: "Python", count: 438, state: "up", rankChange: 3 }, + { rank: 8, keyword: "Spring", count: 401, state: "down", rankChange: 2 }, + { rank: 9, keyword: "AWS", count: 378, state: "up", rankChange: 1 }, + { rank: 10, keyword: "LLM", count: 325, state: "new", rankChange: 0 }, + { rank: 11, keyword: "Kotlin", count: 287, state: "same", rankChange: 0 }, + { rank: 12, keyword: "Monorepo", count: 241, state: "up", rankChange: 2 }, { rank: 13, keyword: "PostgreSQL", count: 198, - deltaType: "down", - deltaValue: 3, + state: "down", + rankChange: 3, }, - { rank: 14, keyword: "Redis", count: 154, deltaType: "same" }, + { rank: 14, keyword: "Redis", count: 154, state: "same", rankChange: 0 }, { rank: 15, keyword: "GraphQL", count: 121, - deltaType: "down", - deltaValue: 1, + state: "down", + rankChange: 1, }, ], }, - month: { - dateLabel: "2026-04 기준", + monthly: { + unit: "monthly", + periodStart: "2026-04-01", + periodEnd: "2026-04-28", + dateLabel: "4월", topPosts: [ { rank: 1, id: "trend-month-1", - title: "AI 코딩 어시스턴트 실전 도입기 — 팀 생산성 2배 올린 방법", + title: + "AI Coding Assistant in Practice — How We Doubled Team Productivity", + translatedTitle: + "AI 코딩 어시스턴트 실전 도입기 — 팀 생산성 2배 올린 방법", sourceName: "naver_d2", tags: ["AI", "DevOps"], viewCount: 52341, thumbnailUrl: "https://picsum.photos/seed/ai-coding-assistant/400/225", category: "AI", changeRate: 89.3, + isMyInterest: false, }, { rank: 2, id: "trend-month-2", - title: "Next.js 16 App Router 실전 마이그레이션 완전 정복", + title: "Mastering Next.js 16 App Router Migration", + translatedTitle: "Next.js 16 App Router 실전 마이그레이션 완전 정복", sourceName: "kakao_tech", tags: ["Next.js", "React"], viewCount: 41827, thumbnailUrl: null, category: "Frontend", changeRate: 27.4, + isMyInterest: false, }, { rank: 3, id: "trend-month-3", - title: "MSA 전환 3년 후기 — 잘한 것과 실패한 것", + title: "3 Years of MSA Migration — What Worked and What Failed", + translatedTitle: "MSA 전환 3년 후기 — 잘한 것과 실패한 것", sourceName: "우아한형제들", tags: ["MSA", "Backend"], viewCount: 38452, thumbnailUrl: "https://picsum.photos/seed/msa-migration/400/225", category: "Backend", changeRate: -8.6, + isMyInterest: false, }, { rank: 4, id: "trend-month-4", - title: "Python asyncio 완전 정복 — 비동기 프로그래밍의 모든 것", + title: "Mastering Python asyncio — Everything About Async Programming", + translatedTitle: + "Python asyncio 완전 정복 — 비동기 프로그래밍의 모든 것", sourceName: "velog", tags: ["Python", "Async"], viewCount: 29873, thumbnailUrl: null, category: "Backend", changeRate: 11.2, + isMyInterest: false, }, { rank: 5, id: "trend-month-5", - title: "클라우드 비용 최적화 — AWS 청구서를 50% 줄인 방법", + title: "Cloud Cost Optimization — How We Cut AWS Bills by 50%", + translatedTitle: "클라우드 비용 최적화 — AWS 청구서를 50% 줄인 방법", sourceName: "toss_tech", tags: ["AWS", "Cloud"], viewCount: 24561, thumbnailUrl: "https://picsum.photos/seed/cloud-cost-aws/400/225", category: "DevOps", changeRate: 43.7, + isMyInterest: false, }, ], topPostsSummary: "이번 달은 AI 코딩 어시스턴트 도입과 Next.js 마이그레이션 관련 글이 압도적인 조회수를 기록했습니다. MSA 전환 후기와 같은 실전 경험 글이 꾸준히 높은 관심을 받았으며, 클라우드 비용 최적화 주제가 새롭게 주목받고 있습니다. Python 비동기 프로그래밍 심층 가이드도 꾸준한 인기를 유지했습니다.", collectionSummary: "이번 달 총 1,284개의 새로운 콘텐츠가 수집되었습니다. AI/ML 분야 콘텐츠가 전월 대비 41% 급증하며 가장 빠른 성장세를 보였습니다. Next.js, TypeScript 관련 글은 꾸준한 생산량을 유지하고 있으며, Rust와 MLOps 분야 콘텐츠가 새롭게 증가하고 있습니다. 국내 기업 기술 블로그 출처 비율이 처음으로 45%를 넘었습니다.", - trendingKeywords: [ - { rank: 1, keyword: "AI", count: 5241, deltaType: "up", deltaValue: 2 }, - { rank: 2, keyword: "React", count: 4827, deltaType: "same" }, - { - rank: 3, - keyword: "Next.js", - count: 4312, - deltaType: "up", - deltaValue: 1, - }, + trendingTags: [ + { rank: 1, keyword: "AI", count: 5241, state: "up", rankChange: 2 }, + { rank: 2, keyword: "React", count: 4827, state: "same", rankChange: 0 }, + { rank: 3, keyword: "Next.js", count: 4312, state: "up", rankChange: 1 }, { rank: 4, keyword: "TypeScript", count: 3987, - deltaType: "up", - deltaValue: 1, - }, - { - rank: 5, - keyword: "Docker", - count: 3621, - deltaType: "down", - deltaValue: 1, - }, - { rank: 6, keyword: "AWS", count: 3284, deltaType: "up", deltaValue: 2 }, - { - rank: 7, - keyword: "Python", - count: 2951, - deltaType: "up", - deltaValue: 1, + state: "up", + rankChange: 1, }, - { rank: 8, keyword: "Kubernetes", count: 2614, deltaType: "new" }, + { rank: 5, keyword: "Docker", count: 3621, state: "down", rankChange: 1 }, + { rank: 6, keyword: "AWS", count: 3284, state: "up", rankChange: 2 }, + { rank: 7, keyword: "Python", count: 2951, state: "up", rankChange: 1 }, { - rank: 9, - keyword: "Spring", - count: 2341, - deltaType: "down", - deltaValue: 3, - }, - { rank: 10, keyword: "LLM", count: 2087, deltaType: "up", deltaValue: 5 }, - { rank: 11, keyword: "MLOps", count: 1854, deltaType: "new" }, - { - rank: 12, - keyword: "Rust", - count: 1621, - deltaType: "up", - deltaValue: 3, + rank: 8, + keyword: "Kubernetes", + count: 2614, + state: "new", + rankChange: 0, }, + { rank: 9, keyword: "Spring", count: 2341, state: "down", rankChange: 3 }, + { rank: 10, keyword: "LLM", count: 2087, state: "up", rankChange: 5 }, + { rank: 11, keyword: "MLOps", count: 1854, state: "new", rankChange: 0 }, + { rank: 12, keyword: "Rust", count: 1621, state: "up", rankChange: 3 }, { rank: 13, keyword: "GraphQL", count: 1387, - deltaType: "down", - deltaValue: 2, + state: "down", + rankChange: 2, }, - { rank: 14, keyword: "Redis", count: 1154, deltaType: "same" }, + { rank: 14, keyword: "Redis", count: 1154, state: "same", rankChange: 0 }, { rank: 15, keyword: "PostgreSQL", count: 987, - deltaType: "down", - deltaValue: 1, - }, - { - rank: 16, - keyword: "Kafka", - count: 824, - deltaType: "up", - deltaValue: 2, + state: "down", + rankChange: 1, }, - { - rank: 17, - keyword: "MSA", - count: 741, - deltaType: "down", - deltaValue: 4, - }, - { rank: 18, keyword: "Kotlin", count: 654, deltaType: "same" }, + { rank: 16, keyword: "Kafka", count: 824, state: "up", rankChange: 2 }, + { rank: 17, keyword: "MSA", count: 741, state: "down", rankChange: 4 }, + { rank: 18, keyword: "Kotlin", count: 654, state: "same", rankChange: 0 }, { rank: 19, keyword: "Flutter", count: 521, - deltaType: "down", - deltaValue: 2, - }, - { - rank: 20, - keyword: "Svelte", - count: 387, - deltaType: "up", - deltaValue: 1, + state: "down", + rankChange: 2, }, + { rank: 20, keyword: "Svelte", count: 387, state: "up", rankChange: 1 }, ], }, }; function getDayDateLabel(): string { const now = new Date(); - const hour = now.getHours(); - const batchHour = hour < 8 ? 0 : hour < 16 ? 8 : 16; - const batchTime = `${String(batchHour).padStart(2, "0")}:00`; - const year = now.getFullYear(); - const month = String(now.getMonth() + 1).padStart(2, "0"); - const day = String(now.getDate()).padStart(2, "0"); - return `${year}-${month}-${day} ${batchTime} 기준`; + const month = now.getMonth() + 1; + const day = now.getDate(); + return `${month}월 ${day}일`; } /** 탭 전환 시 로딩 상태가 보이도록 딜레이 포함 */ -export async function fetchHomeTrend( - range: TrendRange, -): Promise { +export async function fetchHomeTrend(unit: TrendRange): Promise { await new Promise((resolve) => setTimeout(resolve, 500)); - if (range === "day") { - return { ...MOCK_HOME_TREND.day, dateLabel: getDayDateLabel() }; + if (unit === "daily") { + return { ...MOCK_HOME_TREND.daily, dateLabel: getDayDateLabel() }; } - return MOCK_HOME_TREND[range]; + return MOCK_HOME_TREND[unit]; } diff --git a/types/search.ts b/types/search.ts index fabb3e9..19286d8 100644 --- a/types/search.ts +++ b/types/search.ts @@ -1,9 +1,10 @@ -export type TrendRange = "day" | "week" | "month"; +export type TrendRange = "daily" | "weekly" | "monthly"; export interface TrendTopPost { rank: number; id: string; title: string; + translatedTitle: string | null; sourceName: string; tags: string[]; viewCount: number; @@ -11,16 +12,17 @@ export interface TrendTopPost { category: string; /** 전 기간 대비 조회수 증감률 (%). 양수: 증가, 음수: 감소, 0: 변화 없음 */ changeRate: number; + isMyInterest: boolean; } -export type TrendKeywordDeltaType = "new" | "up" | "down" | "same"; +export type TrendKeywordState = "new" | "up" | "down" | "same"; export interface TrendKeywordItem { keyword: string; rank: number; count?: number; - deltaType?: TrendKeywordDeltaType; - deltaValue?: number; + state: TrendKeywordState; + rankChange: number; isMyInterest?: boolean; } @@ -36,11 +38,15 @@ export interface SearchResultItem { } export interface HomeTrendData { + unit: TrendRange; + periodStart: string; + periodEnd: string; dateLabel: string; topPosts: TrendTopPost[]; - /** day 범위에서는 빈 문자열 (섹션 자체를 숨김) */ - topPostsSummary: string; - /** day 범위에서는 빈 문자열 (섹션 자체를 숨김) */ - collectionSummary: string; - trendingKeywords: TrendKeywordItem[]; + /** LLM 실패 시 null. 전 단위에서 null 가능 */ + topPostsSummary: string | null; + /** LLM 실패 시 null. daily 단위에서는 항상 null */ + collectionSummary: string | null; + /** 데이터 없으면 null (빈 배열 아님) */ + trendingTags: TrendKeywordItem[] | null; }