본문 바로가기
개발 기록/backend

네이버 검색 api를 활용한 뉴스 수집방법 - 키워드 뉴스 요약 구현하기(예제)

by jeong11 2026. 2. 25.

1. 프로젝트 개요 

1. 의도

뉴스 기사들은 많지만, 특정 키워드에 대한 논조가 긍정적인지 부정적인지 한눈에 파악하기는 어렵습니다 

그래서 키워드와 기간을 입력하면 관련 뉴스를 자동 수집하고, 기사 내용을 요약 분석하여 저장 및 조회할 수 있는 자동화 툴 프로그램을 만들었습니다 

 

Naver 검색 API를 사용해 뉴스를 수집합니다 

 

2. 프로젝트 개요와 기술 스텍 

사용자 입력
       ↓ 
run.php
       ↓
Naver API
       ↓
DB 저장 
       ↓
index.php / detail.php 조회

 

핵심 기능 :

키워드 기반 뉴스 검색 , 기사 요약, 감성 분석, 결과 저장 및 조회 

 

기술 스택 

PHP, Aiven(MySQL), Naver Search API, Bootstrap 5 

 

 


2. 개발 전 사전 준비 (네이버 API 발급, OPENAI API 발급)

1. 네이버 API

네이버 Developers 가입 필요 

네이버 Developers에서 애플리케이션 등록 후 Client ID, Client Secret을 발급받으면 됩니다 
간혹 IP 제한으로 가입이 되지 않는 경우가 있는데, 이 경우 모바일 네트워크로 접속하면 정상 가입이 가능합니다 

 

사이트 주소 

https://developers.naver.com/

 

NAVER Developers

네이버 오픈 API들을 활용해 개발자들이 다양한 애플리케이션을 개발할 수 있도록 API 가이드와 SDK를 제공합니다. 제공중인 오픈 API에는 네이버 로그인, 검색, 단축URL, 캡차를 비롯 기계번역, 음

developers.naver.com

 

IP가 막혀있어서 모바일로 가입

와이파이 말고 데이터 켜서 가입하면 됩니다! 

 

애플리케이션 등록 

아래 포스팅에 상세하게 작성

 

네이버 지도 api로 '우리동네 장소 찾기' 만들기 1 - 네이버 지도 api 좌표 문제 해결 방법

1. 진행 순서 시나리오 → 아키텍처 → ERD → 샘플 데이터 검증 → PHP 앱(관리자, 사용자) 시나리오 : 사람이 하는 일을 "트리거-입력-규칙-출력-저장"으로 변경한다아키텍처 : 액터/시스템/데이터

tiny-immj.tistory.com

012
애플리케이션 등록 화면

 

Client ID, Client Secret 두 값을 모두 사용하게 됩니다  

 

 

2. OPENAI API

OPENAI 플랫폼 접속 

OpenAI Platform

 

OpenAI Platform

Explore developer resources, tutorials, API docs, and dynamic examples to get the most out of OpenAI's platform.

platform.openai.com

 

name 작성 후 Create secret key

 

generate 키가 생성되었다로 뜨고 Secret Key가 자동 생성됩니다 

다시 확인이 불가능하니 꼭 복사해서 메모해놓으세요! 

 

 

3. 구현

1. 단계별 구현 

① 1단계 Naver API로 뉴스 가져오기 

→ run.php

  • PHP cURL로 NAVER News API를 호출
  • 인증 헤더를 처리
  • Naver News API로 최신 뉴스 5개를 검색하고 날짜로 필터링
  • 검색 기록을 keyword_runs 테이블을 남기고 뉴스 내용을 news_items 테이블에 저장 

 

② 2단계 뉴스 분석 구현 

기사 본문 요약한 후 감성 점수를 계산하도록 설계 

감성 점수는 -1 ~ 1 범위로 설정, index 화면에서 배지를 통해 긍정/중립/부정을 구분 

 

③ 3단계 데이터베이스 설계와 저장 

→ news_items(분석 결과), keyword_runs(실행기록) 테이블을 설계한 이유 

기사 URL의 SHA256 해시값을 unique key로 지정하여 중복 저장을 방지 

외부 API 기반 시스템에서는 중복 데이터 방지가 중요하다고 판단했습니다 

 

④ 4단계 시각화 

→ index.php, detail.php 

 

2. 파일별 역할과 코드 

전체 프로젝트 구조 분석 

news_insight/
 ├── config.php
 ├── index.php
 ├── detail.php
 └── run.php

 

① config.php 

전역 설정 파일 

<?php
/* ==================================
	DB 설정(Aiven MySQL)
=====================================*/
define('DB_HOST', 'host주소');
define('DB_PORT', 포트번호);
define('DB_USER', 'yourusername');
define('DB_PASS', 'yourpassword');
define('DB_NAME', 'news_insight');

/*==================================
    API KEY
====================================*/
define('NAVER_CLIENT_ID', '네이버클라이언트아이디');
define('NAVER_CLIENT_SECRET', '네이버클라이언트시크릿');
define('OPENAI_API_KEY', getenv('OPENAI_API_KEY'));

/* 타임존 */
date_default_timezone_set('Asia/Seoul');

/* 세션 */
session_start();

 

역할 :

DB 연결 정보 정의 

외부 API 키 정의 

타임존 설정 

세션 시작  

 

 

② index.php 

<?php
require_once 'config.php'

/*============================
 DB 연결 (PDO, Aiven)
==============================*/
try {
    $pdo = new PDO(
      "mysql:host=" . DB_HOST . ";port=" . DB_PORT . ";dbname=" . DB_NAME . ";charset=utf8mb4",
      DB_USER,
      DB_PASS,
      [
    	PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::MYSQL_ALTER_SSL_VERIFY_SERVER_CERT => false,
      ]
    );
} catch () {
	die("DB 연결 실패");
}

/*========================
  뉴스 목록 조회 
=========================*/
$sql = "
   SELECT 
    	news_id,
        title,
        pub_date,
        sentiment_score,
        summary
    FROM news_items
    ORDER BY created_at DESC
    LIMIT 20 
";
$stmt = $pdo->query($sql);
$newsList = $stmt->fetchAll();
?>

<!DOCTYPE html>
<html lang="ko">
    <head>
    	<meta charset="UTF-8">
        <title>News Insight</title>
        
        <!--Bootstrap 5-->
        <link
            href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
            rel="stylesheet"
        >
    </head>
    <body class="bg-light">
    	<div class="container py-5">
            <!--제목-->
            <div class="col text-center">
            	<h2 class="fw-bold"> News Insight</h2>
                <p class="text-muted">키워드 기반 뉴스 요약 & 감성 분석</p>
            </div>
        </div>
        
        <!--뉴스 분석 실행-->
        <div class="row justify-content-center mb-5">
           <div class="col-md-6">
                <div class="card-hasdow-sm">
                    <div class="card-body">
                    	<h5 class="card-title mb-4">뉴스 분석 실행</h5>
                    	
                        <form method="post" action="run.php">
                            <div class="mb-3">
                            	<label class="form-label">키워드</label>
                                <input type="text" name="keyword" class="form-control" required>
                            </div>
                            
                            <div class="mb-3">
                            	<label class="form-label">시작 날짜</label>
                                <input type="date" name="start_date" class="form-control" required>
                            </div>
                            
                            <div class="mb-4">
                            	<label class="form-label">종료날짜</label>
                                <input type="date" name="end_date" class="form-control" required>
                            </div>
                            
                            <div>
                            	<button type="submit" class="btn-primary btn-lg">
                                	뉴스 분석 실행
                                </button>
                            </div>
                        </form>
                        
                    </div>
                </div>
            </div>
        </div>
        
        <!--분석 결과 목록-->
        <div class="row">
           <div class="col">
                <h4 class="mb-3">분석 결과 목록</h4>
            	
                <div class="card shadow-sm">
                    <div class="card-body p-0">
                    	
                        <table>
                            <thead class="table tabble-hover mb-0">
                            	<tr>
                                    <th>제목</th>
                                    <th style="width: 120px;">날짜</th>
                                    <th style="width: 80px;">감성</th>
                                    <th>요약</th>
                                </tr>
                            </thead>
                            <tbody>
                            <?php if(count($newsList) === 0): ?>
                            	<tr>
                                	<td colspan="4" class="text-center text-muted py-4">
                                    	아직 분석된 뉴스가 없습니다. 
                                    </td>
                                </tr>
                                <?php else: ?>
                                    <php foreach ($newsList as $news): ?>
                                    <? php
                                    $score = (int)$news['sentiment_score'];
                                    if($score > 0 ) {
                                    	$badge = 'success';
                                    } elseif($score < 0) {
                                    	$badge = 'danger';
                                    } else {
                                    	$badge = 'secondary';
                                    }
                                    ?>
                                <tr>
                                	<td>
                                    	<a href="detail.php?id=<?= $news['news_id'] ?>" class="text-decoration-none">
                                           <?= htmlspecialchars($news['title']) ?>
                                        </a>
                                    </td>
                                    <td><? = date('Y-m-d', strtotime($news['pub_date'])) ?></td>
                                    <td>
                                    	<span class="badge bg-<?= $badge ?>">
                                        	<?= $score ?>
                                        </span>
                                    </td>
                                    <td>
                                    	<?= htmlspecialchars(mb_strimwidth($news['summary'], 0, 120, '...')) ?>
                                    </td>
                                </tr>
                               <? php endforeach; ?>
                            <?php endif; ?>   
                            </tbody> 
                        </table>
                        
                    </div>
                </div>
            </div>
        
        </div>
        
    </body>
</html>

 

역할 : 

DB 연결 

최근 뉴스 20개 조회 

뉴스 실행 폼 제공 

Bootstrap 기반 UI 렌더링 

 

 

③ detail.php

<?php
require_once 'config.php';

/*========================
 DB연결(PDO)
==========================*/
try {
	$pdo = new PDO(
    	"mysql:host=" . DB_HOST . ";port=" . DB_PORT . ";dbname=" . DB_NAME . ";charset=utf8mb4",
        DB_USER,
        DB_PASS,
        [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT => false,
        ]
    );
} catch(PDOException $e) {
	die("DB 연결 실패");
}

/*==========================
 파라미터 체크
===========================*/
$newsId = $_GET['id'] ?? null;
if(!$newsId) {
	die('잘못된 접근입니다.');
}

/*==========================
 뉴스 조회
============================*/
$sql = "
	SELECT
    	title,
        url,
        pub_date,
        summary,
        keywords_json,
        sentiment_score,
        sentiment_reason
    FROM news_items
    WHERE news_id = :id
";
$stmt = $pdo->prepare($sql);
$stmt->execute(['id' => $newsId]);
$news = $stmt->fetch();

if(!$news) {
	die('뉴스를 찾을 수 없습니다.');
}

/* 감성 점수 색상 */
$score = (int)$news['sentiment_score'];
if ($score >0){
	$badge = 'success';
} elseif ($score <0){
	$badge = 'danger';
} else {
	$badge = 'secondary';
}

$keywords = json_decode($news['keywords_json'], true) ??[];

/*==========================
 URL 보정(http/https 자동 추가)
============================*/
$url = trim($news['url'] ?? '');

if($url !== '' && !preg_match('#^https?://#i', $url)) {
	$url = 'https://' .$url;
}
?>

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <title>뉴스 상세 | News Insight</title>
    
    <!-- Bootstrap5 -->
    <link
    	href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
        rel="stylesheet"
    >
</head>

<body class="bg-light">

<div class="container py-5">
	
    <!--뒤로 가기-->
    <div class="mb-3">
    	<a href="index.php" class="btn btn-outline-secondary btn-sm"> ← 목록으로 </a>
    <?div>
    
    <div class="card shadow-sm">
    	<h4 class="fw-body mb-3"><?= htmlspecialchars($news['title']) ?></h4>
        
        <div class="mb-2 text muted">
        📅 <?= date('Y-m-d H:i', strtorime($news['pub_date'])) ?>
        </div>
        
        <div class="mb-3">
            <span class="badge bg-<?= $badge ?>">
            	감성점수 <?= $score?>
            </span>
        </div>
        
        <hr>
        
        <h6>요약</h6>
        <p><?= nl2br(htmlspecialchars($news['summary'])) ?></p>
        
        <h6>키워드</h6>
        <div class="mb-3">
            <?php if(count($keywords) === 0): ?>
            	<span class="text-muted">없음</span>
            <?php else: ?>
            	<?php foreach ($keywords as $kw): ?>
                    <span class="badge bg-light text-dark border me-1 mb-1">
            			<?= htmlspecialchars($kw) ?>
                    </span>
                 <?php endforeach; ?>
            <?phpendif; ?>
        </div>
        
        <h6>감성 분석 근거</h6>
        <p><?= htmlspecialchars($news['sentiment_reason']) ?></p>
        
        <hr>
        
        <?php if ($url === ''): ?>
            <button class="btn btn-secondary" disabled>
            	원문 기사 URL이 없습니다
            </button>
        <?php else: ?>
        	<a href=" <?=htmlspecialchars($url) ?>"
            	target=" _blank"
                class="btn btn-primary"
                	원문 기사 보기
            </a>
        <?php endif; ?>
    </div>
</div>
</body>

</html>

 

역할 : 

news_id 기반 뉴스 조회 

감성 점수에 따른 badge 처리 

키워드 JSON 디코딩 

URL 보정 처리 

 

 

④ run.php 

<?php
ini_set('display_errors', 1);
error_reporting(E_ALL);

require_once 'config.php';

/* =========================
   1. 사용자 입력
========================= */
$keyword   = trim($_POST['keyword'] ?? '');
$startDate = $_POST['start_date'] ?? '';
$endDate   = $_POST['end_date'] ?? '';

if ($keyword === '' || $startDate === '' || $endDate === '') {
    die('필수 값 누락');
}

/* =========================
   2. Aiven MySQL (PDO + SSL)
========================= */
$pdo = new PDO(
    "mysql:host=" . DB_HOST .
    ";port=" . DB_PORT .
    ";dbname=" . DB_NAME .
    ";charset=utf8mb4",
    DB_USER,
    DB_PASS,
    [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT => false,
        PDO::MYSQL_ATTR_SSL_CA => null
    ]
);

/* =========================
   3. 네이버 뉴스 API
========================= */
$url = "https://openapi.naver.com/v1/search/news.json?" .
       "query=" . urlencode($keyword) .
       "&display=5&sort=date";

$ch = curl_init($url);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "X-Naver-Client-Id: " . NAVER_CLIENT_ID,
        "X-Naver-Client-Secret: " . NAVER_CLIENT_SECRET
    ]
]);

$response = curl_exec($ch);
curl_close($ch);

$data = json_decode($response, true);

if (!isset($data['items'])) {
    die('뉴스 없음');
}

/* =========================
   4. 날짜 필터
========================= */
$filtered = [];

foreach ($data['items'] as $item) {
    $pub = strtotime($item['pubDate']);
    if ($pub >= strtotime($startDate) &&
        $pub <= strtotime($endDate . ' 23:59:59')) {
        $filtered[] = $item;
    }
}

if (count($filtered) === 0) {
    die('조건에 맞는 뉴스 없음');
}

/* =========================
   5. keyword_runs에 run_id 생성
========================= */

$insertRun = $pdo->prepare("
    INSERT INTO keyword_runs (keyword, run_at, api_query_json)
    VALUES (?, NOW(), ?)
");

$apiQueryJson = json_encode([
    'keyword' => $keyword,
    'start_date' => $startDate,
    'end_date' => $endDate,
    'display' => 5,
    'sort' => 'date'
], JSON_UNESCAPED_UNICODE);

$insertRun->execute([$keyword, $apiQueryJson]);

$runId = $pdo->lastInsertId();

/* =========================
   6. news_items 저장 (중복 url_hash 방지)
========================= */

$stmt = $pdo->prepare("
    INSERT INTO news_items
    (run_id, keyword, title, url, url_hash, pub_date, summary, keywords_json, sentiment_score, sentiment_reason)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
");

$insertedCount = 0;
$skippedCount = 0;

foreach ($filtered as $news) {

    $title = strip_tags($news['title']);
    $url = $news['originallink'];

    // URL 해시 생성 (SHA2 256)
    $urlHash = hash('sha256', $url);

    $summary = strip_tags($news['description']);
    $keywords = json_encode([$keyword], JSON_UNESCAPED_UNICODE);

    try {
        $stmt->execute([
            $runId,
            $keyword,
            $title,
            $url,
            $urlHash,
            date('Y-m-d H:i:s', strtotime($news['pubDate'])),
            $summary,
            $keywords,
            1,
            '중립적 보도'
        ]);
        $insertedCount++;
    } catch (PDOException $e) {
        // url_hash UNIQUE 제약으로 중복이면 건너뛰기
        if ($e->errorInfo[1] == 1062) {
            $skippedCount++;
            continue;
        }
        // 그 외 에러는 다시 던짐
        throw $e;
    }
}

/* =========================
   7. keyword_runs 상태 업데이트
========================= */

$status = ($insertedCount > 0) ? 'SUCCESS' : 'PARTIAL';
if ($insertedCount === 0) {
    $status = 'FAILED';
}

$updateRun = $pdo->prepare("
    UPDATE keyword_runs
    SET status = ?, error_msg = ?
    WHERE run_id = ?
");

$errorMsg = ($insertedCount === 0) ? '조건에 맞는 뉴스가 없거나 중복 뉴스만 존재' : null;

$updateRun->execute([$status, $errorMsg, $runId]);

echo " 뉴스 분석 및 저장 완료 (저장: {$insertedCount}건, 중복 스킵: {$skippedCount}건)";

 

역할 : 

사용자 입력 받기 

Aiven MySQL 연결 

네이버 뉴스 API 호출 

날짜 필터링 

keyword_runs 저장

 

4. 결과

√ 키워드 기반 뉴스 수집 

√ 감성 분석 및 저장 구조 완성 

√ 조회 프레임워크 완성 

 

비동기 처리 : 분석시간 고려 run.php를 백그라운드에서 실행하도록 개선하여 사용자 경험을 향상시키는 방안 제시 

프로젝트 실행 화면

 

 

* 배운 점 

외부 API 연동 시 예외 처리의 중요성 

중복 데이터 방지 설계 경험 

데이터 수집과 조회 로직 분리의 필요성