diff --git a/services/news-api/API_GUIDE.md b/services/news-api/API_GUIDE.md
new file mode 100644
index 0000000..0a611af
--- /dev/null
+++ b/services/news-api/API_GUIDE.md
@@ -0,0 +1,1328 @@
+# News API - 개발자 가이드
+
+외부 프론트엔드 시스템에서 Site11 News API를 통합하기 위한 실용적인 가이드입니다.
+
+---
+
+## 📌 Quick Start
+
+### Base URL
+```
+http://localhost:8050/api/v1
+```
+
+### 첫 API 호출
+
+```bash
+# 한국어 최신 기사 5개 조회
+curl 'http://localhost:8050/api/v1/ko/articles/latest?limit=5'
+```
+
+```javascript
+// JavaScript/TypeScript
+const response = await fetch('http://localhost:8050/api/v1/ko/articles/latest?limit=5');
+const articles = await response.json();
+console.log(articles);
+```
+
+### 인증
+현재 버전은 인증이 필요하지 않습니다. (Public API)
+
+---
+
+## 🌍 지원 언어
+
+| 언어 코드 | 언어 |
+|----------|------|
+| `ko` | 한국어 |
+| `en` | English |
+| `zh_cn` | 简体中文 |
+| `zh_tw` | 繁體中文 |
+| `ja` | 日本語 |
+| `fr` | Français |
+| `de` | Deutsch |
+| `es` | Español |
+| `it` | Italiano |
+
+---
+
+## 📰 Articles API
+
+### 1. 기사 목록 조회
+
+가장 기본적인 엔드포인트입니다. 페이지네이션을 지원합니다.
+
+```http
+GET /api/v1/{language}/articles
+```
+
+**Query Parameters:**
+- `page` (int, default: 1) - 페이지 번호
+- `page_size` (int, default: 20, max: 100) - 페이지당 항목 수
+- `category` (string, optional) - 카테고리 필터
+
+**Example Request:**
+```bash
+curl 'http://localhost:8050/api/v1/ko/articles?page=1&page_size=10&category=technology'
+```
+
+```typescript
+// TypeScript + Axios
+import axios from 'axios';
+
+const getArticles = async (
+ language: string,
+ page: number = 1,
+ pageSize: number = 20,
+ category?: string
+) => {
+ const { data } = await axios.get(`/api/v1/${language}/articles`, {
+ params: { page, page_size: pageSize, category }
+ });
+ return data;
+};
+
+// 사용 예시
+const articles = await getArticles('ko', 1, 10, 'technology');
+```
+
+**Response:**
+```json
+{
+ "total": 1523,
+ "page": 1,
+ "page_size": 10,
+ "total_pages": 153,
+ "articles": [
+ {
+ "id": "507f1f77bcf86cd799439011",
+ "news_id": "uuid-string",
+ "title": "AI 기술의 미래",
+ "summary": "인공지능 기술이 우리 생활에 미치는 영향...",
+ "language": "ko",
+ "created_at": "2024-01-15T10:30:00Z",
+ "categories": ["technology", "ai"],
+ "images": ["https://example.com/image1.png"],
+ "source_keyword": "AI기술",
+ "subtopics": [
+ {
+ "title": "AI의 발전",
+ "content": [
+ "AI 기술은 최근 몇 년간 급속도로 발전했습니다.",
+ "특히 대형 언어 모델의 등장으로..."
+ ]
+ }
+ ],
+ "entities": {
+ "people": ["샘 알트만", "일론 머스크"],
+ "organizations": ["OpenAI", "Google"],
+ "countries": ["미국", "한국"]
+ },
+ "references": [
+ {
+ "title": "원문 기사 제목",
+ "link": "https://source.com/article",
+ "source": "TechCrunch",
+ "published": "2024-01-15"
+ }
+ ]
+ }
+ ]
+}
+```
+
+---
+
+### 2. 최신 기사 조회
+
+가장 최근에 생성된 기사를 조회합니다.
+
+```http
+GET /api/v1/{language}/articles/latest
+```
+
+**Query Parameters:**
+- `limit` (int, default: 10, max: 50) - 조회할 기사 수
+
+**Example Request:**
+```bash
+curl 'http://localhost:8050/api/v1/en/articles/latest?limit=5'
+```
+
+```javascript
+// React Hook 예시
+import { useState, useEffect } from 'react';
+
+function useLatestArticles(language, limit = 10) {
+ const [articles, setArticles] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ fetch(`/api/v1/${language}/articles/latest?limit=${limit}`)
+ .then(res => res.json())
+ .then(data => {
+ setArticles(data);
+ setLoading(false);
+ });
+ }, [language, limit]);
+
+ return { articles, loading };
+}
+
+// 컴포넌트에서 사용
+function LatestNews() {
+ const { articles, loading } = useLatestArticles('ko', 5);
+
+ if (loading) return
Loading...
;
+
+ return (
+
+ {articles.map(article => (
+
+
{article.title}
+
{article.summary}
+
+ ))}
+
+ );
+}
+```
+
+**Response:**
+```json
+[
+ {
+ "id": "507f1f77bcf86cd799439011",
+ "news_id": "uuid-string",
+ "title": "Latest News Title",
+ "summary": "Summary text...",
+ "language": "en",
+ "categories": ["business"],
+ "images": ["https://example.com/image.png"],
+ "created_at": "2024-01-15T10:30:00Z",
+ "source_keyword": "business"
+ }
+]
+```
+
+---
+
+### 3. 기사 검색
+
+키워드로 기사를 검색합니다.
+
+```http
+GET /api/v1/{language}/articles/search
+```
+
+**Query Parameters:**
+- `q` (string, required) - 검색 키워드
+- `page` (int, default: 1) - 페이지 번호
+- `page_size` (int, default: 20, max: 100) - 페이지당 항목 수
+
+**Example Request:**
+```bash
+curl 'http://localhost:8050/api/v1/ko/articles/search?q=AI&page=1&page_size=10'
+```
+
+```typescript
+// Next.js API Route 예시
+// pages/api/search.ts
+import type { NextApiRequest, NextApiResponse } from 'next';
+import axios from 'axios';
+
+export default async function handler(
+ req: NextApiRequest,
+ res: NextApiResponse
+) {
+ const { q, language = 'ko', page = 1 } = req.query;
+
+ try {
+ const { data } = await axios.get(
+ `http://localhost:8050/api/v1/${language}/articles/search`,
+ {
+ params: { q, page, page_size: 20 }
+ }
+ );
+ res.status(200).json(data);
+ } catch (error) {
+ res.status(500).json({ error: 'Search failed' });
+ }
+}
+```
+
+**Response:**
+동일한 `ArticleList` 형식 (기사 목록 조회와 동일)
+
+---
+
+### 4. 기사 상세 조회
+
+특정 기사의 전체 내용을 조회합니다.
+
+```http
+GET /api/v1/{language}/articles/{article_id}
+```
+
+**Example Request:**
+```bash
+curl 'http://localhost:8050/api/v1/ko/articles/507f1f77bcf86cd799439011'
+```
+
+```typescript
+// React Query 사용 예시
+import { useQuery } from '@tanstack/react-query';
+import axios from 'axios';
+
+interface ArticleDetailProps {
+ articleId: string;
+ language: string;
+}
+
+function ArticleDetail({ articleId, language }: ArticleDetailProps) {
+ const { data: article, isLoading, error } = useQuery({
+ queryKey: ['article', articleId, language],
+ queryFn: async () => {
+ const { data } = await axios.get(
+ `/api/v1/${language}/articles/${articleId}`
+ );
+ return data;
+ }
+ });
+
+ if (isLoading) return Loading...
;
+ if (error) return Error loading article
;
+
+ return (
+
+ {article.title}
+ {article.summary}
+
+ {article.images.length > 0 && (
+
+ )}
+
+ {article.subtopics.map((subtopic, idx) => (
+
+ {subtopic.title}
+ {subtopic.content.map((paragraph, pIdx) => (
+ {paragraph}
+ ))}
+
+ ))}
+
+ {article.references.length > 0 && (
+
+ )}
+
+ );
+}
+```
+
+**Response:**
+완전한 `Article` 객체 (기사 목록의 단일 항목과 동일한 구조)
+
+---
+
+### 5. 카테고리 목록 조회
+
+해당 언어에서 사용 가능한 모든 카테고리를 조회합니다.
+
+```http
+GET /api/v1/{language}/categories
+```
+
+**Example Request:**
+```bash
+curl 'http://localhost:8050/api/v1/ko/categories'
+```
+
+```javascript
+// 카테고리 필터 컴포넌트
+function CategoryFilter({ language, onSelectCategory }) {
+ const [categories, setCategories] = useState([]);
+
+ useEffect(() => {
+ fetch(`/api/v1/${language}/categories`)
+ .then(res => res.json())
+ .then(setCategories);
+ }, [language]);
+
+ return (
+
+ );
+}
+```
+
+**Response:**
+```json
+[
+ "technology",
+ "business",
+ "politics",
+ "sports",
+ "entertainment"
+]
+```
+
+---
+
+## 🏢 Outlets API
+
+Outlets은 **인물(people)**, **토픽(topics)**, **기업(companies)** 3가지 카테고리로 분류됩니다.
+
+### 1. Outlet 목록 조회
+
+모든 Outlet 또는 특정 카테고리의 Outlet을 조회합니다.
+
+```http
+GET /api/v1/outlets
+```
+
+**Query Parameters:**
+- `category` (string, optional) - `people`, `topics`, `companies` 중 하나
+
+**Example Request:**
+```bash
+# 모든 Outlet 조회
+curl 'http://localhost:8050/api/v1/outlets'
+
+# 인물만 조회
+curl 'http://localhost:8050/api/v1/outlets?category=people'
+```
+
+```typescript
+// TypeScript 타입 정의
+interface OutletTranslations {
+ ko?: string;
+ en?: string;
+ zh_cn?: string;
+ zh_tw?: string;
+ ja?: string;
+ fr?: string;
+ de?: string;
+ es?: string;
+ it?: string;
+}
+
+interface Outlet {
+ source_keyword: string;
+ category: 'people' | 'topics' | 'companies';
+ name_translations: OutletTranslations;
+ description_translations: OutletTranslations;
+ image?: string;
+
+ // Deprecated (하위 호환성)
+ name?: string;
+ description?: string;
+}
+
+// 사용 예시
+const getOutlets = async (category?: string): Promise => {
+ const { data } = await axios.get('/api/v1/outlets', {
+ params: category ? { category } : {}
+ });
+
+ // category 지정 시: { people: [...] } 형식
+ // 전체 조회 시: { people: [...], topics: [...], companies: [...] }
+ return category ? data[category] : data;
+};
+```
+
+**Response (category=people):**
+```json
+{
+ "people": [
+ {
+ "source_keyword": "온유",
+ "category": "people",
+ "name_translations": {
+ "ko": "온유",
+ "en": "Onew",
+ "ja": "オニュ"
+ },
+ "description_translations": {
+ "ko": "샤이니 리더, 뮤지컬 배우",
+ "en": "SHINee leader, musical actor",
+ "ja": "SHINeeのリーダー、ミュージカル俳優"
+ },
+ "image": "https://example.com/onew.jpg"
+ }
+ ]
+}
+```
+
+**Response (전체 조회):**
+```json
+{
+ "people": [...],
+ "topics": [...],
+ "companies": [...]
+}
+```
+
+---
+
+### 2. Outlet 상세 조회
+
+특정 Outlet의 정보를 조회합니다.
+
+```http
+GET /api/v1/outlets/{outlet_id}
+```
+
+**Example Request:**
+```bash
+curl 'http://localhost:8050/api/v1/outlets/507f1f77bcf86cd799439011'
+```
+
+**Response:**
+```json
+{
+ "source_keyword": "온유",
+ "category": "people",
+ "name_translations": {
+ "ko": "온유",
+ "en": "Onew",
+ "ja": "オニュ"
+ },
+ "description_translations": {
+ "ko": "샤이니 리더, 뮤지컬 배우",
+ "en": "SHINee leader, musical actor"
+ },
+ "image": "https://example.com/onew.jpg"
+}
+```
+
+---
+
+### 3. Outlet별 기사 조회 ⭐ (중요)
+
+특정 Outlet 관련 기사를 동적으로 조회합니다. `source_keyword` 기반으로 실시간 쿼리합니다.
+
+```http
+GET /api/v1/{language}/outlets/{outlet_id}/articles
+```
+
+**Query Parameters:**
+- `page` (int, default: 1) - 페이지 번호
+- `page_size` (int, default: 20, max: 100) - 페이지당 항목 수
+
+**Example Request:**
+```bash
+# 한국어로 '온유' 관련 기사 조회
+curl 'http://localhost:8050/api/v1/ko/outlets/507f1f77bcf86cd799439011/articles?page_size=10'
+```
+
+```typescript
+// React 컴포넌트 예시
+function OutletArticles({ outletId, language }: { outletId: string, language: string }) {
+ const [articles, setArticles] = useState(null);
+ const [page, setPage] = useState(1);
+
+ useEffect(() => {
+ const fetchArticles = async () => {
+ const { data } = await axios.get(
+ `/api/v1/${language}/outlets/${outletId}/articles`,
+ { params: { page, page_size: 20 } }
+ );
+ setArticles(data);
+ };
+
+ fetchArticles();
+ }, [outletId, language, page]);
+
+ if (!articles) return Loading...
;
+
+ return (
+
+
총 {articles.total}개의 기사
+ {articles.articles.map(article => (
+
+ ))}
+
+ {/* 페이지네이션 */}
+
+
+ );
+}
+```
+
+**Response:**
+동일한 `ArticleList` 형식
+
+---
+
+## 💬 Comments API
+
+### 1. 댓글 목록 조회
+
+특정 기사의 댓글을 조회합니다.
+
+```http
+GET /api/v1/comments?articleId={article_id}
+```
+
+**Example Request:**
+```bash
+curl 'http://localhost:8050/api/v1/comments?articleId=507f1f77bcf86cd799439011'
+```
+
+```typescript
+// React Hook
+function useComments(articleId: string) {
+ const [comments, setComments] = useState([]);
+
+ useEffect(() => {
+ fetch(`/api/v1/comments?articleId=${articleId}`)
+ .then(res => res.json())
+ .then(data => setComments(data.comments));
+ }, [articleId]);
+
+ return comments;
+}
+```
+
+**Response:**
+```json
+{
+ "comments": [
+ {
+ "id": "comment-id-1",
+ "articleId": "507f1f77bcf86cd799439011",
+ "nickname": "독자1",
+ "content": "좋은 기사 감사합니다!",
+ "createdAt": "2024-01-15T11:20:00Z"
+ }
+ ],
+ "total": 42
+}
+```
+
+---
+
+### 2. 댓글 작성
+
+새로운 댓글을 작성합니다.
+
+```http
+POST /api/v1/comments
+```
+
+**Request Body:**
+```json
+{
+ "articleId": "507f1f77bcf86cd799439011",
+ "nickname": "독자1",
+ "content": "좋은 기사 감사합니다!"
+}
+```
+
+**Example Request:**
+```bash
+curl -X POST 'http://localhost:8050/api/v1/comments' \
+ -H 'Content-Type: application/json' \
+ -d '{
+ "articleId": "507f1f77bcf86cd799439011",
+ "nickname": "독자1",
+ "content": "좋은 기사입니다!"
+ }'
+```
+
+```typescript
+// React Form 예시
+function CommentForm({ articleId }: { articleId: string }) {
+ const [nickname, setNickname] = useState('');
+ const [content, setContent] = useState('');
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ try {
+ const { data } = await axios.post('/api/v1/comments', {
+ articleId,
+ nickname,
+ content
+ });
+
+ alert('댓글이 등록되었습니다!');
+ setNickname('');
+ setContent('');
+ } catch (error) {
+ alert('댓글 등록에 실패했습니다.');
+ }
+ };
+
+ return (
+
+ );
+}
+```
+
+**Response:**
+```json
+{
+ "id": "comment-id-new",
+ "articleId": "507f1f77bcf86cd799439011",
+ "nickname": "독자1",
+ "content": "좋은 기사입니다!",
+ "createdAt": "2024-01-15T12:00:00Z"
+}
+```
+
+---
+
+### 3. 댓글 수 조회
+
+기사의 총 댓글 수를 조회합니다.
+
+```http
+GET /api/v1/articles/{article_id}/comment-count
+```
+
+**Example Request:**
+```bash
+curl 'http://localhost:8050/api/v1/articles/507f1f77bcf86cd799439011/comment-count'
+```
+
+**Response:**
+```json
+{
+ "count": 42
+}
+```
+
+---
+
+## 📦 TypeScript 타입 정의
+
+전체 타입을 한 곳에 모아두고 재사용하세요.
+
+```typescript
+// types/news-api.ts
+
+// Article Types
+export interface Subtopic {
+ title: string;
+ content: string[];
+}
+
+export interface Reference {
+ title: string;
+ link: string;
+ source: string;
+ published?: string;
+}
+
+export interface Entities {
+ people: string[];
+ organizations: string[];
+ groups: string[];
+ countries: string[];
+ events: string[];
+}
+
+export interface Article {
+ id: string;
+ news_id: string;
+ title: string;
+ summary?: string;
+ created_at: string;
+ language: string;
+
+ // Content
+ subtopics: Subtopic[];
+ categories: string[];
+ entities?: Entities;
+
+ // Source
+ source_keyword?: string;
+ source_count?: number;
+ references: Reference[];
+
+ // Media
+ images: string[];
+ image_prompt?: string;
+
+ // Metadata
+ job_id?: string;
+ keyword_id?: string;
+ pipeline_stages: string[];
+ processing_time?: number;
+ translated_languages: string[];
+ ref_news_id?: string;
+ rss_guid?: string;
+}
+
+export interface ArticleList {
+ total: number;
+ page: number;
+ page_size: number;
+ total_pages: number;
+ articles: Article[];
+}
+
+export interface ArticleSummary {
+ id: string;
+ news_id: string;
+ title: string;
+ summary?: string;
+ language: string;
+ categories: string[];
+ images: string[];
+ created_at: string;
+ source_keyword?: string;
+}
+
+// Outlet Types
+export interface OutletTranslations {
+ ko?: string;
+ en?: string;
+ zh_cn?: string;
+ zh_tw?: string;
+ ja?: string;
+ fr?: string;
+ de?: string;
+ es?: string;
+ it?: string;
+}
+
+export interface Outlet {
+ source_keyword: string;
+ category: 'people' | 'topics' | 'companies';
+ name_translations: OutletTranslations;
+ description_translations: OutletTranslations;
+ image?: string;
+
+ // Deprecated
+ name?: string;
+ description?: string;
+}
+
+export interface OutletList {
+ people?: Outlet[];
+ topics?: Outlet[];
+ companies?: Outlet[];
+}
+
+// Comment Types
+export interface Comment {
+ id: string;
+ articleId: string;
+ nickname: string;
+ content: string;
+ createdAt: string;
+}
+
+export interface CommentCreate {
+ articleId: string;
+ nickname: string;
+ content: string;
+}
+
+export interface CommentList {
+ comments: Comment[];
+ total: number;
+}
+
+// Language Type
+export type Language = 'ko' | 'en' | 'zh_cn' | 'zh_tw' | 'ja' | 'fr' | 'de' | 'es' | 'it';
+```
+
+---
+
+## 🔧 Axios 인스턴스 설정
+
+API 클라이언트를 한 곳에서 관리하세요.
+
+```typescript
+// lib/api-client.ts
+import axios from 'axios';
+
+const apiClient = axios.create({
+ baseURL: process.env.NEXT_PUBLIC_NEWS_API_URL || 'http://localhost:8050/api/v1',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ timeout: 10000
+});
+
+// 요청 인터셉터 (로깅, 인증 등)
+apiClient.interceptors.request.use(
+ config => {
+ console.log(`[API] ${config.method?.toUpperCase()} ${config.url}`);
+ return config;
+ },
+ error => Promise.reject(error)
+);
+
+// 응답 인터셉터 (에러 처리)
+apiClient.interceptors.response.use(
+ response => response,
+ error => {
+ if (error.response) {
+ console.error('[API Error]', error.response.status, error.response.data);
+ }
+ return Promise.reject(error);
+ }
+);
+
+export default apiClient;
+```
+
+```typescript
+// lib/news-api.ts
+import apiClient from './api-client';
+import type { Article, ArticleList, Outlet, Comment, CommentCreate, Language } from '@/types/news-api';
+
+export const newsAPI = {
+ // Articles
+ getArticles: (language: Language, page = 1, pageSize = 20, category?: string) =>
+ apiClient.get(`/${language}/articles`, {
+ params: { page, page_size: pageSize, category }
+ }),
+
+ getLatestArticles: (language: Language, limit = 10) =>
+ apiClient.get(`/${language}/articles/latest`, {
+ params: { limit }
+ }),
+
+ searchArticles: (language: Language, query: string, page = 1, pageSize = 20) =>
+ apiClient.get(`/${language}/articles/search`, {
+ params: { q: query, page, page_size: pageSize }
+ }),
+
+ getArticleById: (language: Language, articleId: string) =>
+ apiClient.get(`/${language}/articles/${articleId}`),
+
+ getCategories: (language: Language) =>
+ apiClient.get(`/${language}/categories`),
+
+ // Outlets
+ getOutlets: (category?: 'people' | 'topics' | 'companies') =>
+ apiClient.get('/outlets', { params: category ? { category } : {} }),
+
+ getOutletById: (outletId: string) =>
+ apiClient.get(`/outlets/${outletId}`),
+
+ getOutletArticles: (language: Language, outletId: string, page = 1, pageSize = 20) =>
+ apiClient.get(`/${language}/outlets/${outletId}/articles`, {
+ params: { page, page_size: pageSize }
+ }),
+
+ // Comments
+ getComments: (articleId: string) =>
+ apiClient.get<{ comments: Comment[], total: number }>('/comments', {
+ params: { articleId }
+ }),
+
+ createComment: (comment: CommentCreate) =>
+ apiClient.post('/comments', comment),
+
+ getCommentCount: (articleId: string) =>
+ apiClient.get<{ count: number }>(`/articles/${articleId}/comment-count`)
+};
+```
+
+---
+
+## 🚀 실전 통합 예시
+
+### Next.js App Router 예시
+
+```typescript
+// app/articles/[lang]/page.tsx
+import { newsAPI } from '@/lib/news-api';
+import type { Language } from '@/types/news-api';
+
+interface PageProps {
+ params: { lang: Language };
+ searchParams: { page?: string; category?: string };
+}
+
+export default async function ArticlesPage({ params, searchParams }: PageProps) {
+ const page = parseInt(searchParams.page || '1');
+ const category = searchParams.category;
+
+ const { data } = await newsAPI.getArticles(params.lang, page, 20, category);
+
+ return (
+
+
Articles ({data.total})
+
+ {data.articles.map(article => (
+
+ ))}
+
+
+
+ );
+}
+```
+
+### React Query 사용 예시
+
+```typescript
+// hooks/useArticles.ts
+import { useQuery } from '@tanstack/react-query';
+import { newsAPI } from '@/lib/news-api';
+import type { Language } from '@/types/news-api';
+
+export function useArticles(
+ language: Language,
+ page: number,
+ pageSize: number,
+ category?: string
+) {
+ return useQuery({
+ queryKey: ['articles', language, page, pageSize, category],
+ queryFn: async () => {
+ const { data } = await newsAPI.getArticles(language, page, pageSize, category);
+ return data;
+ },
+ staleTime: 5 * 60 * 1000, // 5분
+ cacheTime: 10 * 60 * 1000 // 10분
+ });
+}
+
+export function useLatestArticles(language: Language, limit = 10) {
+ return useQuery({
+ queryKey: ['articles', 'latest', language, limit],
+ queryFn: async () => {
+ const { data } = await newsAPI.getLatestArticles(language, limit);
+ return data;
+ },
+ staleTime: 1 * 60 * 1000 // 1분
+ });
+}
+
+export function useOutletArticles(
+ language: Language,
+ outletId: string,
+ page: number,
+ pageSize: number
+) {
+ return useQuery({
+ queryKey: ['outlet-articles', language, outletId, page, pageSize],
+ queryFn: async () => {
+ const { data } = await newsAPI.getOutletArticles(language, outletId, page, pageSize);
+ return data;
+ }
+ });
+}
+```
+
+```typescript
+// 컴포넌트에서 사용
+function ArticlesList() {
+ const [page, setPage] = useState(1);
+ const { data, isLoading, error } = useArticles('ko', page, 20);
+
+ if (isLoading) return ;
+ if (error) return ;
+
+ return (
+
+ {data.articles.map(article => (
+
+ ))}
+
+ );
+}
+```
+
+---
+
+## 🎨 무한 스크롤 구현
+
+```typescript
+// components/InfiniteArticleList.tsx
+import { useInfiniteQuery } from '@tanstack/react-query';
+import { useInView } from 'react-intersection-observer';
+import { newsAPI } from '@/lib/news-api';
+
+function InfiniteArticleList({ language }: { language: Language }) {
+ const { ref, inView } = useInView();
+
+ const {
+ data,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage
+ } = useInfiniteQuery({
+ queryKey: ['articles', 'infinite', language],
+ queryFn: async ({ pageParam = 1 }) => {
+ const { data } = await newsAPI.getArticles(language, pageParam, 20);
+ return data;
+ },
+ getNextPageParam: (lastPage) => {
+ return lastPage.page < lastPage.total_pages
+ ? lastPage.page + 1
+ : undefined;
+ }
+ });
+
+ useEffect(() => {
+ if (inView && hasNextPage) {
+ fetchNextPage();
+ }
+ }, [inView, hasNextPage, fetchNextPage]);
+
+ return (
+
+ {data?.pages.map((page, i) => (
+
+ {page.articles.map(article => (
+
+ ))}
+
+ ))}
+
+ {/* 트리거 요소 */}
+
+ {isFetchingNextPage && }
+
+
+ );
+}
+```
+
+---
+
+## ⚠️ 에러 처리
+
+### HTTP 상태 코드
+
+| 코드 | 의미 | 원인 |
+|------|------|------|
+| 200 | 성공 | 정상 응답 |
+| 400 | Bad Request | 잘못된 언어 코드, 파라미터 오류 |
+| 404 | Not Found | 존재하지 않는 기사/outlet ID |
+| 500 | Server Error | 서버 내부 오류 |
+
+### 에러 응답 형식
+
+```json
+{
+ "detail": "Unsupported language: kr"
+}
+```
+
+### 에러 처리 예시
+
+```typescript
+try {
+ const { data } = await newsAPI.getArticleById('ko', articleId);
+ return data;
+} catch (error) {
+ if (axios.isAxiosError(error)) {
+ if (error.response?.status === 404) {
+ console.error('기사를 찾을 수 없습니다.');
+ } else if (error.response?.status === 400) {
+ console.error('잘못된 요청:', error.response.data.detail);
+ } else {
+ console.error('서버 오류가 발생했습니다.');
+ }
+ }
+ throw error;
+}
+```
+
+---
+
+## 🎯 성능 최적화 팁
+
+### 1. 클라이언트 사이드 캐싱
+
+```typescript
+// React Query 설정
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ staleTime: 5 * 60 * 1000, // 5분간 fresh 상태 유지
+ cacheTime: 10 * 60 * 1000, // 10분간 캐시 보관
+ refetchOnWindowFocus: false,
+ retry: 1
+ }
+ }
+});
+
+function App() {
+ return (
+
+
+
+ );
+}
+```
+
+### 2. 이미지 최적화
+
+```typescript
+// Next.js Image 컴포넌트 사용
+import Image from 'next/image';
+
+function ArticleImage({ src, alt }: { src: string; alt: string }) {
+ return (
+
+ );
+}
+```
+
+### 3. 페이지네이션 프리페칭
+
+```typescript
+// 다음 페이지 미리 로드
+const { data } = useArticles('ko', page, 20);
+
+// 다음 페이지 프리페칭
+useEffect(() => {
+ if (data && page < data.total_pages) {
+ queryClient.prefetchQuery({
+ queryKey: ['articles', 'ko', page + 1, 20],
+ queryFn: () => newsAPI.getArticles('ko', page + 1, 20)
+ });
+ }
+}, [page, data]);
+```
+
+### 4. 요청 최적화
+
+```typescript
+// Debounce 검색
+import { useDebouncedValue } from '@/hooks/useDebouncedValue';
+
+function SearchArticles() {
+ const [query, setQuery] = useState('');
+ const debouncedQuery = useDebouncedValue(query, 500);
+
+ const { data } = useQuery({
+ queryKey: ['articles', 'search', debouncedQuery],
+ queryFn: () => newsAPI.searchArticles('ko', debouncedQuery, 1, 20),
+ enabled: debouncedQuery.length > 0
+ });
+
+ return (
+ setQuery(e.target.value)}
+ placeholder="검색..."
+ />
+ );
+}
+```
+
+---
+
+## 📚 API Reference (Quick Reference)
+
+| Method | Endpoint | 설명 | 파라미터 |
+|--------|----------|------|---------|
+| **Articles** |
+| GET | `/{lang}/articles` | 기사 목록 | page, page_size, category |
+| GET | `/{lang}/articles/latest` | 최신 기사 | limit |
+| GET | `/{lang}/articles/search` | 기사 검색 | q, page, page_size |
+| GET | `/{lang}/articles/{id}` | 기사 상세 | - |
+| GET | `/{lang}/categories` | 카테고리 목록 | - |
+| **Outlets** |
+| GET | `/outlets` | Outlet 목록 | category |
+| GET | `/outlets/{id}` | Outlet 상세 | - |
+| GET | `/{lang}/outlets/{id}/articles` | Outlet별 기사 | page, page_size |
+| **Comments** |
+| GET | `/comments` | 댓글 목록 | articleId |
+| POST | `/comments` | 댓글 작성 | body: CommentCreate |
+| GET | `/articles/{id}/comment-count` | 댓글 수 | - |
+
+---
+
+## 🛠️ 개발 환경 설정
+
+### 환경 변수
+
+```env
+# .env.local (Next.js)
+NEXT_PUBLIC_NEWS_API_URL=http://localhost:8050/api/v1
+
+# .env (React)
+REACT_APP_NEWS_API_URL=http://localhost:8050/api/v1
+```
+
+### CORS 설정
+
+개발 환경에서 CORS 이슈가 발생하면 프록시 설정:
+
+```javascript
+// next.config.js
+module.exports = {
+ async rewrites() {
+ return [
+ {
+ source: '/api/v1/:path*',
+ destination: 'http://localhost:8050/api/v1/:path*'
+ }
+ ];
+ }
+};
+```
+
+---
+
+## 📞 지원 및 문의
+
+- **Swagger UI**: `http://localhost:8050/docs`
+- **ReDoc**: `http://localhost:8050/redoc`
+- **GitHub Issues**: [프로젝트 저장소 Issues](https://github.com/your-org/site11)
+
+---
+
+**마지막 업데이트**: 2024-01-15
+**API 버전**: v1.1.0