Files
site11/services/news-api/API_GUIDE.md
jungwoo choi e40f50005d docs: Add comprehensive News API developer guide
Create external developer-focused API documentation for News API service
with practical integration examples for frontend systems.

Features:
- 10 major sections covering all API endpoints
- Complete TypeScript type definitions
- Real-world React/Next.js integration examples
- Axios client setup and React Query patterns
- Infinite scroll implementation
- Error handling strategies
- Performance optimization tips

API Coverage:
- Articles API (6 endpoints): list, latest, search, detail, categories
- Outlets API (3 endpoints): list, detail, outlet articles
- Comments API (3 endpoints): list, create, count
- Multi-language support (9 languages)
- Pagination and filtering

Code Examples:
- Copy-paste ready code snippets
- React hooks and components
- Next.js App Router examples
- React Query integration
- Infinite scroll with Intersection Observer
- Client-side caching strategies

Developer Experience:
- TypeScript-first approach
- Practical, executable examples
- Quick start guide
- API reference table
- Error handling patterns
- Performance optimization tips

Target Audience: External frontend developers integrating with News API

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 08:33:59 +09:00

1329 lines
28 KiB
Markdown

# 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 <div>Loading...</div>;
return (
<div>
{articles.map(article => (
<div key={article.id}>
<h2>{article.title}</h2>
<p>{article.summary}</p>
</div>
))}
</div>
);
}
```
**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 <div>Loading...</div>;
if (error) return <div>Error loading article</div>;
return (
<article>
<h1>{article.title}</h1>
<p className="summary">{article.summary}</p>
{article.images.length > 0 && (
<img src={article.images[0]} alt={article.title} />
)}
{article.subtopics.map((subtopic, idx) => (
<section key={idx}>
<h2>{subtopic.title}</h2>
{subtopic.content.map((paragraph, pIdx) => (
<p key={pIdx}>{paragraph}</p>
))}
</section>
))}
{article.references.length > 0 && (
<div className="references">
<h3>참고 자료</h3>
<ul>
{article.references.map((ref, idx) => (
<li key={idx}>
<a href={ref.link} target="_blank" rel="noopener">
{ref.title} - {ref.source}
</a>
</li>
))}
</ul>
</div>
)}
</article>
);
}
```
**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 (
<select onChange={(e) => onSelectCategory(e.target.value)}>
<option value="">All Categories</option>
{categories.map(cat => (
<option key={cat} value={cat}>{cat}</option>
))}
</select>
);
}
```
**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<Outlet[]> => {
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<ArticleList | null>(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 <div>Loading...</div>;
return (
<div>
<h2> {articles.total}개의 기사</h2>
{articles.articles.map(article => (
<ArticleCard key={article.id} article={article} />
))}
{/* 페이지네이션 */}
<Pagination
current={page}
total={articles.total_pages}
onChange={setPage}
/>
</div>
);
}
```
**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<Comment[]>([]);
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 (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="닉네임"
value={nickname}
onChange={e => setNickname(e.target.value)}
required
/>
<textarea
placeholder="댓글 내용"
value={content}
onChange={e => setContent(e.target.value)}
required
/>
<button type="submit">댓글 등록</button>
</form>
);
}
```
**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<ArticleList>(`/${language}/articles`, {
params: { page, page_size: pageSize, category }
}),
getLatestArticles: (language: Language, limit = 10) =>
apiClient.get<Article[]>(`/${language}/articles/latest`, {
params: { limit }
}),
searchArticles: (language: Language, query: string, page = 1, pageSize = 20) =>
apiClient.get<ArticleList>(`/${language}/articles/search`, {
params: { q: query, page, page_size: pageSize }
}),
getArticleById: (language: Language, articleId: string) =>
apiClient.get<Article>(`/${language}/articles/${articleId}`),
getCategories: (language: Language) =>
apiClient.get<string[]>(`/${language}/categories`),
// Outlets
getOutlets: (category?: 'people' | 'topics' | 'companies') =>
apiClient.get<any>('/outlets', { params: category ? { category } : {} }),
getOutletById: (outletId: string) =>
apiClient.get<Outlet>(`/outlets/${outletId}`),
getOutletArticles: (language: Language, outletId: string, page = 1, pageSize = 20) =>
apiClient.get<ArticleList>(`/${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<Comment>('/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 (
<div>
<h1>Articles ({data.total})</h1>
<div className="grid grid-cols-3 gap-4">
{data.articles.map(article => (
<ArticleCard key={article.id} article={article} />
))}
</div>
<Pagination
current={page}
total={data.total_pages}
baseUrl={`/articles/${params.lang}`}
/>
</div>
);
}
```
### 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 <Skeleton />;
if (error) return <ErrorMessage />;
return (
<div>
{data.articles.map(article => (
<ArticleCard key={article.id} {...article} />
))}
</div>
);
}
```
---
## 🎨 무한 스크롤 구현
```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 (
<div>
{data?.pages.map((page, i) => (
<div key={i}>
{page.articles.map(article => (
<ArticleCard key={article.id} article={article} />
))}
</div>
))}
{/* 트리거 요소 */}
<div ref={ref} className="h-10">
{isFetchingNextPage && <Spinner />}
</div>
</div>
);
}
```
---
## ⚠️ 에러 처리
### 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 (
<QueryClientProvider client={queryClient}>
<YourApp />
</QueryClientProvider>
);
}
```
### 2. 이미지 최적화
```typescript
// Next.js Image 컴포넌트 사용
import Image from 'next/image';
function ArticleImage({ src, alt }: { src: string; alt: string }) {
return (
<Image
src={src}
alt={alt}
width={800}
height={450}
loading="lazy"
placeholder="blur"
blurDataURL="/placeholder.jpg"
/>
);
}
```
### 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 (
<input
value={query}
onChange={e => 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