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 + /> +