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

28 KiB

News API - 개발자 가이드

외부 프론트엔드 시스템에서 Site11 News API를 통합하기 위한 실용적인 가이드입니다.


📌 Quick Start

Base URL

http://localhost:8050/api/v1

첫 API 호출

# 한국어 최신 기사 5개 조회
curl 'http://localhost:8050/api/v1/ko/articles/latest?limit=5'
// 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. 기사 목록 조회

가장 기본적인 엔드포인트입니다. 페이지네이션을 지원합니다.

GET /api/v1/{language}/articles

Query Parameters:

  • page (int, default: 1) - 페이지 번호
  • page_size (int, default: 20, max: 100) - 페이지당 항목 수
  • category (string, optional) - 카테고리 필터

Example Request:

curl 'http://localhost:8050/api/v1/ko/articles?page=1&page_size=10&category=technology'
// 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:

{
  "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. 최신 기사 조회

가장 최근에 생성된 기사를 조회합니다.

GET /api/v1/{language}/articles/latest

Query Parameters:

  • limit (int, default: 10, max: 50) - 조회할 기사 수

Example Request:

curl 'http://localhost:8050/api/v1/en/articles/latest?limit=5'
// 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:

[
  {
    "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. 기사 검색

키워드로 기사를 검색합니다.

GET /api/v1/{language}/articles/search

Query Parameters:

  • q (string, required) - 검색 키워드
  • page (int, default: 1) - 페이지 번호
  • page_size (int, default: 20, max: 100) - 페이지당 항목 수

Example Request:

curl 'http://localhost:8050/api/v1/ko/articles/search?q=AI&page=1&page_size=10'
// 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. 기사 상세 조회

특정 기사의 전체 내용을 조회합니다.

GET /api/v1/{language}/articles/{article_id}

Example Request:

curl 'http://localhost:8050/api/v1/ko/articles/507f1f77bcf86cd799439011'
// 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. 카테고리 목록 조회

해당 언어에서 사용 가능한 모든 카테고리를 조회합니다.

GET /api/v1/{language}/categories

Example Request:

curl 'http://localhost:8050/api/v1/ko/categories'
// 카테고리 필터 컴포넌트
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:

[
  "technology",
  "business",
  "politics",
  "sports",
  "entertainment"
]

🏢 Outlets API

Outlets은 인물(people), 토픽(topics), 기업(companies) 3가지 카테고리로 분류됩니다.

1. Outlet 목록 조회

모든 Outlet 또는 특정 카테고리의 Outlet을 조회합니다.

GET /api/v1/outlets

Query Parameters:

  • category (string, optional) - people, topics, companies 중 하나

Example Request:

# 모든 Outlet 조회
curl 'http://localhost:8050/api/v1/outlets'

# 인물만 조회
curl 'http://localhost:8050/api/v1/outlets?category=people'
// 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):

{
  "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 (전체 조회):

{
  "people": [...],
  "topics": [...],
  "companies": [...]
}

2. Outlet 상세 조회

특정 Outlet의 정보를 조회합니다.

GET /api/v1/outlets/{outlet_id}

Example Request:

curl 'http://localhost:8050/api/v1/outlets/507f1f77bcf86cd799439011'

Response:

{
  "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 기반으로 실시간 쿼리합니다.

GET /api/v1/{language}/outlets/{outlet_id}/articles

Query Parameters:

  • page (int, default: 1) - 페이지 번호
  • page_size (int, default: 20, max: 100) - 페이지당 항목 수

Example Request:

# 한국어로 '온유' 관련 기사 조회
curl 'http://localhost:8050/api/v1/ko/outlets/507f1f77bcf86cd799439011/articles?page_size=10'
// 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. 댓글 목록 조회

특정 기사의 댓글을 조회합니다.

GET /api/v1/comments?articleId={article_id}

Example Request:

curl 'http://localhost:8050/api/v1/comments?articleId=507f1f77bcf86cd799439011'
// 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:

{
  "comments": [
    {
      "id": "comment-id-1",
      "articleId": "507f1f77bcf86cd799439011",
      "nickname": "독자1",
      "content": "좋은 기사 감사합니다!",
      "createdAt": "2024-01-15T11:20:00Z"
    }
  ],
  "total": 42
}

2. 댓글 작성

새로운 댓글을 작성합니다.

POST /api/v1/comments

Request Body:

{
  "articleId": "507f1f77bcf86cd799439011",
  "nickname": "독자1",
  "content": "좋은 기사 감사합니다!"
}

Example Request:

curl -X POST 'http://localhost:8050/api/v1/comments' \
  -H 'Content-Type: application/json' \
  -d '{
    "articleId": "507f1f77bcf86cd799439011",
    "nickname": "독자1",
    "content": "좋은 기사입니다!"
  }'
// 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:

{
  "id": "comment-id-new",
  "articleId": "507f1f77bcf86cd799439011",
  "nickname": "독자1",
  "content": "좋은 기사입니다!",
  "createdAt": "2024-01-15T12:00:00Z"
}

3. 댓글 수 조회

기사의 총 댓글 수를 조회합니다.

GET /api/v1/articles/{article_id}/comment-count

Example Request:

curl 'http://localhost:8050/api/v1/articles/507f1f77bcf86cd799439011/comment-count'

Response:

{
  "count": 42
}

📦 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 클라이언트를 한 곳에서 관리하세요.

// 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;
// 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 예시

// 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 사용 예시

// 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;
    }
  });
}
// 컴포넌트에서 사용
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>
  );
}

🎨 무한 스크롤 구현

// 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 서버 내부 오류

에러 응답 형식

{
  "detail": "Unsupported language: kr"
}

에러 처리 예시

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. 클라이언트 사이드 캐싱

// 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. 이미지 최적화

// 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. 페이지네이션 프리페칭

// 다음 페이지 미리 로드
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. 요청 최적화

// 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.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 이슈가 발생하면 프록시 설정:

// next.config.js
module.exports = {
  async rewrites() {
    return [
      {
        source: '/api/v1/:path*',
        destination: 'http://localhost:8050/api/v1/:path*'
      }
    ];
  }
};

📞 지원 및 문의


마지막 업데이트: 2024-01-15 API 버전: v1.1.0