diff --git a/app/api/posts/route.ts b/app/api/posts/route.ts index eb6c9a0..fd92f21 100644 --- a/app/api/posts/route.ts +++ b/app/api/posts/route.ts @@ -15,6 +15,7 @@ export async function GET(req: Request) { // 기존 파라미터 const query = searchParams.get('query') || ''; const seriesSlug = searchParams.get('series') || ''; + const tagParam = searchParams.get('tag') || ''; const isCompact = searchParams.get('compact') === 'true'; const isCanViewPrivate = searchParams.get('private') === 'true'; @@ -59,6 +60,13 @@ export async function GET(req: Request) { } as QuerySelector); } + // 태그 필터 + if (tagParam) { + (searchConditions.$and as QuerySelector[]).push({ + tags: tagParam, + } as QuerySelector); + } + // 검색 조건을 만족하는 총 문서 수 계산 const totalPosts = await Post.countDocuments(searchConditions); diff --git a/app/api/tags/route.ts b/app/api/tags/route.ts new file mode 100644 index 0000000..bf87cea --- /dev/null +++ b/app/api/tags/route.ts @@ -0,0 +1,30 @@ +import dbConnect from '@/app/lib/dbConnect'; +import Post from '@/app/models/Post'; + +// GET /api/tags +export async function GET() { + try { + await dbConnect(); + + const tagStats = await Post.aggregate([ + { $match: { isPrivate: { $ne: true } } }, + { $unwind: '$tags' }, + { $group: { _id: '$tags', count: { $sum: 1 } } }, + { $sort: { count: -1 } }, + { $project: { tag: '$_id', count: 1, _id: 0 } }, + ]); + + return Response.json(tagStats, { + status: 200, + headers: { + 'Cache-Control': 'public, max-age=300', + }, + }); + } catch (error) { + console.error('Tags API error:', error); + return Response.json( + { success: false, error: '태그 목록 불러오기 실패', detail: error }, + { status: 500 } + ); + } +} diff --git a/app/entities/common/Footer.tsx b/app/entities/common/Footer.tsx index 684f33d..55230a8 100644 --- a/app/entities/common/Footer.tsx +++ b/app/entities/common/Footer.tsx @@ -79,6 +79,9 @@ const Footer = () => {
Portfolio
+
+ Tags +
Admin
diff --git a/app/entities/post/list/SearchSection.tsx b/app/entities/post/list/SearchSection.tsx index adf972a..26329a5 100644 --- a/app/entities/post/list/SearchSection.tsx +++ b/app/entities/post/list/SearchSection.tsx @@ -15,6 +15,7 @@ interface SearchSectionProps { setQuery: (query: string) => void; resetSearchCondition: () => void; searchSeries: string; + searchTag?: string; } const SearchSection = ({ @@ -22,6 +23,7 @@ const SearchSection = ({ setQuery, resetSearchCondition, searchSeries, + searchTag, }: SearchSectionProps) => { const [searchOpen, setSearchOpen] = useState(false); const [seriesOpen, setSeriesOpen] = useState(false); @@ -76,7 +78,7 @@ const SearchSection = ({ {/* 검색 버튼 및 검색창 */}
- {(query || searchSeries) && ( + {(query || searchSeries || searchTag) && (
{searchSeries} 시리즈에서{' '} )} + {searchTag && ( + + #{searchTag} 태그로{' '} + + )} {query ? query : '전체'}로 검색 중...
)} - {(query || searchSeries) && ( + {(query || searchSeries || searchTag) && ( +
+ ) : tags.length === 0 ? ( +
+

아직 태그가 없습니다

+
+ ) : ( + + )} + + + ); +}; + +export default TagsPage; diff --git a/app/types/Tag.ts b/app/types/Tag.ts new file mode 100644 index 0000000..5665886 --- /dev/null +++ b/app/types/Tag.ts @@ -0,0 +1,14 @@ +export interface TagData { + tag: string; + count: number; +} + +export interface Position3D { + x: number; + y: number; + z: number; +} + +export interface TagWithPosition extends TagData { + position: Position3D; +}