From e40f50005d28cc607240bd100276ff8479d1d327 Mon Sep 17 00:00:00 2001 From: jungwoo choi Date: Tue, 4 Nov 2025 08:33:59 +0900 Subject: [PATCH] docs: Add comprehensive News API developer guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- services/news-api/API_GUIDE.md | 1328 ++++++++++++++++++++++++++++++++ 1 file changed, 1328 insertions(+) create mode 100644 services/news-api/API_GUIDE.md 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.title} + )} + + {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 ( +
+ setNickname(e.target.value)} + required + /> +