Skip to content

Conversation

@Dobbymin
Copy link
Owner

@Dobbymin Dobbymin commented Nov 23, 2025

📝 요약 (Summary)

AI가 분석한 암호화폐 뉴스를 제공하는 뉴스 페이지를 구현했습니다. 외부 AI News API와 연동하여 뉴스 데이터와 감성 분석 결과를 결합하고, 페이지네이션 기반 무한 스크롤을 위한 API를 구축했으며, Skeleton 로딩 상태를 적용하여 사용자 경험을 개선했습니다.

✅ 주요 변경 사항 (Key Changes)

  • 뉴스 페이지네이션 API 구현 (GET /api/news?page={n})
  • News API와 Analysis API 데이터 병합 로직 구현
  • 뉴스 목록 페이지 UI 구현 (요약 섹션 + 뉴스 카드 리스트)
  • 모든 컴포넌트에 Skeleton 로딩 상태 추가
  • flex/gap 기반 일관된 레이아웃 시스템 구축

💻 상세 구현 내용 (Implementation Details)

1. API Route Handler (/api/news)

기능

  • 쿼리 파라미터로 page 번호를 받아 페이지네이션된 뉴스 반환
  • 페이지당 5개의 뉴스 제공
  • 표준 APIResponse<T> 형식 사용
// 사용 예시
GET /api/news?page=0  // 0~4번 뉴스 (5개)
GET /api/news?page=1  // 5~9번 뉴스 (5개)

에러 처리

  • 페이지 번호 유효성 검사 (음수, NaN 체크)
  • 400: Invalid page parameter
  • 500: Internal server error

2. News Pagination Handler

핵심 로직

  1. 병렬 API 호출
const [newsResponse, analysisResponse] = await Promise.all([
  newsAPI(),  // 뉴스 원본 데이터 (20건)
  analysisAPI()  // AI 감성 분석 결과
]);
  1. 데이터 매칭 (newsId 기반)
// Map을 사용한 O(1) 조회 성능
const analysisMap = new Map(
  analysisData.newsAnalysis.map(item => [item.newsId, item])
);

// news.id와 analysis.newsId를 매칭하여 병합
const combinedNewsAnalysis = newsData.map(news => {
  const analysis = analysisMap.get(news.id) || defaultAnalysis;
  return {
    newsId: news.id,
    title: news.title,
    content: news.content,
    url: news.url,
    source: news.source,
    reason: analysis.reason,  // AI 분석 이유
    keywords: analysis.keywords,
    sentiment: analysis.sentiment,  // positive/negative/neutral
    confidence: analysis.confidence  // AI 신뢰도
  };
});
  1. 페이지네이션 계산
const ITEMS_PER_PAGE = 5;
const totalPages = Math.ceil(totalItems / ITEMS_PER_PAGE);
const paginatedNewsAnalysis = combinedNewsAnalysis.slice(
  page * ITEMS_PER_PAGE,
  (page + 1) * ITEMS_PER_PAGE
);

응답 구조:

{
  newsDate: "2025-11-22",
  totalNews: 20,
  investmentIndex: 65.3,  // 투자 심리 지수
  summary: {
    positive: 10,
    negative: 4,
    neutral: 6
  },
  keywords: ["비트코인", "스트래티지", ...],
  newsAnalysis: [...],  // 5개씩
  pagination: {
    currentPage: 0,
    totalPages: 4,
    totalItems: 20,
    hasNext: true,
    hasPrev: false
  }
}

4. 타입 정의 및 API 통합

  • NewsItem: 원본 뉴스 데이터 구조
  • AnalysisItem: AI 분석 결과 구조 (newsId 포함)
  • NewsAnalysisItem: 병합된 최종 뉴스 + 분석 데이터
  • PaginatedNewsResponse: API 응답 타입
export const newsListAPI = async ({
  page = 0
}: NewsListParams = {}): Promise<NewsListResponse> => {
  const response = await fetch(`/api/news?page=${page}`);
  if (!response.ok) {
    throw new Error("Failed to fetch news list");
  }
  return response.json();
};

🚀 트러블 슈팅 (Trouble Shooting)

1. API 데이터 구조 불일치 문제

문제

  • 초기에는 analysis API의 newsAnalysis에 title, content 등이 있다고 가정
  • 실제 API 응답에는 newsId, reason, keywords, sentiment, confidence만 존재

해결

# 실제 API 호출로 응답 구조 확인
curl "https://ai-news-scrapper.vercel.app/api/crypto-news/raw?type=news&date=latest"
curl "https://ai-news-scrapper.vercel.app/api/crypto-news/raw?type=analysis&date=latest"
  • News API: 뉴스 원본 데이터 (id, title, content, url, source)
  • Analysis API: 분석 데이터만 (newsId, reason, keywords, sentiment, confidence)
  • 두 데이터를 newsId 기준으로 병합하는 로직으로 수정

2. 인덱스 기반 매칭의 한계

문제

  • 초기에는 배열 인덱스로 news와 analysis 매칭
  • 순서가 다르거나 일부 데이터가 누락되면 잘못된 매칭 발생

해결

// Before: O(n²) 또는 잘못된 매칭
const analysis = analysisData.newsAnalysis[index];

// After: Map을 사용한 O(1) 정확한 매칭
const analysisMap = new Map(
  analysisData.newsAnalysis.map(item => [item.newsId, item])
);
const analysis = analysisMap.get(news.id);

⚠️ 알려진 이슈 및 참고 사항 (Known Issues & Notes)

구현되지 않은 기능

  1. 무한 스크롤

    • 현재는 첫 페이지(page=0)만 표시
    • TanStack Query의 useInfiniteQuery 활용 필요
    const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
      queryKey: ['news'],
      queryFn: ({ pageParam = 0 }) => newsListAPI({ page: pageParam }),
      getNextPageParam: (lastPage) =>
        lastPage.data.pagination.hasNext
          ? lastPage.data.pagination.currentPage + 1
          : undefined,
    });

📸 스크린샷 (Screenshots)

2025-11-23.4.12.43.mov

#️⃣ 관련 이슈 (Related Issues)

@Dobbymin Dobbymin requested a review from Copilot November 23, 2025 07:14
@Dobbymin Dobbymin self-assigned this Nov 23, 2025
@Dobbymin Dobbymin added ✨ Feature 새로운 기능 추가 및 구현하는 경우 📡 API 비동기 통신 코드를 짜는 경우 🖥️ Server Next.js의 서버 기능 구현 labels Nov 23, 2025
@Dobbymin Dobbymin linked an issue Nov 23, 2025 that may be closed by this pull request
@vercel
Copy link

vercel bot commented Nov 23, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
dobbit Ready Ready Preview Comment Nov 23, 2025 7:33am

@gemini-code-assist
Copy link

Summary of Changes

Hello @Dobbymin, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

이 PR은 AI가 분석한 암호화폐 뉴스를 제공하는 전용 뉴스 페이지를 구현합니다. 외부 AI 뉴스 API와 연동하여 뉴스 데이터와 감성 분석 결과를 결합하고, 페이지네이션 기반의 API를 구축하여 효율적인 데이터 로딩을 지원합니다. 또한, Skeleton 로딩 상태를 적용하여 사용자 경험을 향상시키고, 일관된 UI/UX를 위한 레이아웃 시스템을 도입했습니다.

Highlights

  • 뉴스 페이지네이션 API 구현: AI가 분석한 암호화폐 뉴스를 페이지별로 제공하는 API 엔드포인트(GET /api/news?page={n})를 구현했습니다.
  • 뉴스 및 분석 데이터 병합 로직: 외부 뉴스 API와 AI 감성 분석 API의 데이터를 newsId를 기준으로 병합하여 통합된 뉴스 분석 정보를 제공합니다.
  • 뉴스 목록 페이지 UI 구현: 뉴스 요약 섹션과 뉴스 카드 리스트를 포함하는 뉴스 목록 페이지의 사용자 인터페이스를 개발했습니다.
  • Skeleton 로딩 상태 적용: 모든 컴포넌트에 Skeleton 로딩 상태를 추가하여 데이터 로딩 중 사용자 경험을 개선했습니다.
  • 일관된 레이아웃 시스템 구축: flex/gap 기반의 일관된 레이아웃 시스템을 적용하여 UI의 통일성을 확보했습니다.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

AI 뉴스 페이지 구현을 위한 PR 잘 보았습니다. 전반적으로 기능 구현이 상세한 설명과 함께 잘 이루어졌습니다. 특히 외부 API 연동 시 Promise.all을 사용한 병렬 처리와 Map을 활용한 데이터 병합 로직은 성능적으로 훌륭한 접근입니다. 몇 가지 개선점을 제안합니다. API 핸들러에서는 응답 객체를 생성하는 헬퍼 함수를 도입하여 코드 중복을 줄이고, 외부 API 응답에 대한 타입 안정성을 높이기 위해 zod와 같은 유효성 검사 라이브러리 사용을 고려해볼 수 있습니다. 또한, API 응답 캐싱을 통해 성능을 더욱 향상시킬 수 있습니다. UI 측면에서는 cn 유틸리티를 일관되게 사용하여 클래스명을 관리하고, 전역적으로 스크롤바를 숨기는 스타일에 대한 접근성 문제를 검토해보는 것이 좋겠습니다. 마지막으로, 컴포넌트에서 map 함수 사용 시 고유한 key 값을 보장하는 방법에 대한 제안과 빈 데이터 상태에 대한 UI 처리도 포함했습니다. 자세한 내용은 각 파일에 남긴 코멘트를 참고해주세요.

Comment on lines 13 to 14
const newsData = newsResponse.data as NewsItem[];
const analysisData = analysisResponse.data as AnalysisItem;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

외부 API(newsAPI, analysisAPI) 호출 결과를 타입 단언(as)을 사용하여 처리하고 있습니다. 이는 런타임에 예기치 않은 데이터 구조로 인해 오류를 발생시킬 수 있습니다. zod와 같은 스키마 유효성 검사 라이브러리를 사용하여 API 응답을 파싱하고 검증하는 것이 더 안전합니다. 이를 통해 타입 안정성을 보장하고 데이터 구조 변경에 더 유연하게 대응할 수 있습니다.

// 예시: zod 스키마 정의
import { z } from 'zod';

const NewsItemSchema = z.array(z.object({ /* ... */ }));
const AnalysisItemSchema = z.object({ /* ... */ });

// 핸들러 내부
const newsData = NewsItemSchema.parse(newsResponse.data);
const analysisData = AnalysisItemSchema.parse(analysisResponse.data);

@layer base {
* {
@apply border-border outline-ring/50;
@apply scrollbar-hide border-border outline-ring/50;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

* 셀렉터에 scrollbar-hide를 적용하여 전역적으로 스크롤바를 숨기는 것은 접근성 문제를 야기할 수 있습니다. 스크롤바는 사용자가 콘텐츠를 탐색할 수 있다는 시각적 단서를 제공하는데, 이를 숨기면 일부 사용자는 스크롤 가능한 영역을 인지하지 못할 수 있습니다. 꼭 필요한 컴포넌트에만 개별적으로 적용하거나, 스크롤바를 숨기더라도 다른 시각적 힌트를 제공하는 것을 고려해보세요.

    @apply border-border outline-ring/50;

Comment on lines 5 to 7
export const newsPaginationHandler = async (page: number = 0): Promise<PaginatedNewsResponse> => {
// 두 API를 병렬로 호출
const [newsResponse, analysisResponse] = await Promise.all([newsAPI(), analysisAPI()]);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

현재 구조에서는 /api/news 엔드포인트에 대한 모든 요청이 외부 API를 호출하게 됩니다. 뉴스 데이터는 실시간성이 아주 높지 않을 수 있으므로, newsAPIanalysisAPI에서 받아온 데이터를 일정 시간(예: 5분) 동안 캐싱하면 성능을 크게 향상시키고 외부 API에 대한 부하를 줄일 수 있습니다. 간단한 인메모리 캐시나 Next.js의 데이터 캐싱 기능을 활용하는 것을 고려해보세요.

Comment on lines 26 to 30
{keywords.map((k) => (
<span key={k} className='text-xs text-text-muted-dark'>
# {k}
</span>
))}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

keywords 배열을 map으로 순회할 때 k(키워드 문자열)를 key로 사용하고 있습니다. 만약 키워드 배열에 중복된 값이 있다면 React에서 key 중복 경고가 발생하고, 예기치 않은 렌더링 문제가 발생할 수 있습니다. map의 두 번째 인자인 index를 조합하여 고유한 key를 보장하는 것이 더 안전합니다.

Suggested change
{keywords.map((k) => (
<span key={k} className='text-xs text-text-muted-dark'>
# {k}
</span>
))}
{keywords.map((k, index) => (
<span key={`${k}-${index}`} className='text-xs text-text-muted-dark'>
# {k}
</span>
))}

Comment on lines +10 to +17
const { newsDate, totalNews, investmentIndex, summary, keywords, newsAnalysis } = newsData || {
newsDate: "",
totalNews: 0,
investmentIndex: 0,
summary: { positive: 0, negative: 0, neutral: 0 },
keywords: [],
newsAnalysis: [],
};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

newsDataundefined일 경우를 대비한 기본값이 길고 복잡합니다. 이 기본 객체를 DEFAULT_NEWS_DATA와 같은 상수로 추출하면 코드 가독성을 높이고, 다른 곳에서 재사용하기도 용이해집니다.

Suggested change
const { newsDate, totalNews, investmentIndex, summary, keywords, newsAnalysis } = newsData || {
newsDate: "",
totalNews: 0,
investmentIndex: 0,
summary: { positive: 0, negative: 0, neutral: 0 },
keywords: [],
newsAnalysis: [],
};
const DEFAULT_NEWS_DATA = {
newsDate: "",
totalNews: 0,
investmentIndex: 0,
summary: { positive: 0, negative: 0, neutral: 0 },
keywords: [],
newsAnalysis: [],
};
const { newsDate, totalNews, investmentIndex, summary, keywords, newsAnalysis } = newsData || DEFAULT_NEWS_DATA;

Comment on lines 15 to 23
: keywords.map((k) => (
<Badge
key={k}
variant='secondary'
className='bg-white/5 text-gray-300 transition-colors hover:bg-white/10'
>
# {k}
</Badge>
))}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

keywords 배열을 map으로 순회할 때 k(키워드 문자열)를 key로 사용하고 있습니다. 키워드에 중복이 있을 경우 key 중복 문제가 발생할 수 있습니다. mapindex를 함께 사용하여 고유한 key를 만들어주는 것이 좋습니다.

          : keywords.map((k, index) => (
              <Badge
                key={`${k}-${index}`}
                variant='secondary'
                className='bg-white/5 text-gray-300 transition-colors hover:bg-white/10'
              >
                # {k}
              </Badge>
            ))

Comment on lines +29 to +31
<span className={`text-3xl font-bold ${investmentIndex >= 50 ? "text-positive" : "text-negative"}`}>
{investmentIndex}
</span>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

투자 심리 지수의 색상을 동적으로 결정하기 위해 템플릿 리터럴을 사용하고 있습니다. cn 유틸리티를 사용하면 조건부 클래스를 더 명확하고 안전하게 관리할 수 있습니다.

Suggested change
<span className={`text-3xl font-bold ${investmentIndex >= 50 ? "text-positive" : "text-negative"}`}>
{investmentIndex}
</span>
<span className={cn("text-3xl font-bold", investmentIndex >= 50 ? "text-positive" : "text-negative")}>
{investmentIndex}
</span>

Comment on lines +11 to +32
if (isLoading) {
return (
<section className='flex flex-col gap-6'>
{Array.from({ length: 5 }).map((_, index) => (
<NewsCard
key={index}
news={{
newsId: index,
title: "",
content: "",
url: "",
source: "",
reason: "",
keywords: [],
sentiment: "neutral",
confidence: 0,
}}
isLoading
/>
))}
</section>
);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

isLoading 상태일 때, NewsCard에 빈 news 객체를 전달하고 있습니다. NewsCard 컴포넌트가 isLoading prop을 받을 때 내부적으로 스켈레톤 UI를 렌더링하도록 수정하면, 이 컴포넌트에서 불필요한 더미 데이터를 생성하지 않아도 되어 코드가 더 간결해지고 NewsCard의 재사용성이 높아집니다.

Comment on lines +35 to +41
return (
<section className='flex flex-col gap-6'>
{newsAnalysis.map((news) => (
<NewsCard key={news.newsId} news={news} />
))}
</section>
);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

로딩이 끝났지만 newsAnalysis 배열이 비어있는 경우, 사용자에게 아무것도 표시되지 않습니다. "표시할 뉴스가 없습니다."와 같은 메시지를 보여주어 사용자 경험을 개선하는 것이 좋습니다.

  if (newsAnalysis.length === 0) {
    return <section className='py-10 text-center text-text-muted-dark'>표시할 뉴스가 없습니다.</section>;
  }

  return (
    <section className='flex flex-col gap-6'>
      {newsAnalysis.map((news) => (
        <NewsCard key={news.newsId} news={news} />
      ))}
    </section>
  );

@@ -0,0 +1,30 @@
import { Minus, TrendingDown, TrendingUp } from "lucide-react";

export const getSentimentConfig = (sentiment: string) => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

getSentimentConfig 함수의 sentiment 파라미터 타입이 string으로 되어 있습니다. entities에 정의된 Sentiment 타입("positive" | "negative" | "neutral")을 사용하면 타입 안정성을 높이고, 함수 사용 시 잘못된 문자열이 전달되는 것을 방지할 수 있습니다.

import { Sentiment } from "@/entities";

export const getSentimentConfig = (sentiment: Sentiment) => {

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements an AI-powered news page that displays cryptocurrency news with sentiment analysis. The implementation fetches news data from an external API, merges it with AI analysis results (sentiment, keywords, confidence scores), and presents it through a paginated interface with comprehensive skeleton loading states.

  • Adds a new /api/news endpoint that serves paginated news data with AI analysis
  • Implements a news pagination handler that merges news content with AI sentiment analysis
  • Creates a complete news UI with summary statistics, sentiment distribution visualization, and individual news cards
  • Adds skeleton loading states throughout for improved UX

Reviewed changes

Copilot reviewed 41 out of 43 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/app/api/news/route.ts New API route handler for paginated news requests with validation
src/entities/news/handler/news-pagination.handler.ts Core handler that merges news and analysis data, implements pagination logic
src/entities/news/model/types/news.type.ts Type definitions for news, analysis, and paginated response structures
src/entities/news/model/apis/news-list.api.ts Client-side API function for fetching paginated news
src/entities/news/model/hooks/useGetNewsList.ts React Query hook for news data fetching
src/app/news/page.tsx Main news page component integrating all features
src/features/news/ui/SummaryNewsSection.tsx Summary section with date, total count, investment index
src/features/news/ui/NewsListSection.tsx News cards list with loading states
src/features/news/components/features/summary-news/ Components for distribution bar, keywords, and header
src/features/news/components/features/news-list/NewsCard.tsx Individual news card with title, content, analysis, metadata
src/features/news/components/common/news-card/ Reusable components for news card sections (title, content, reason, metadata)
src/features/news/utils/ Utility functions for content cleaning, sentiment config, progress width calculation
src/shared/components/ui/progress.tsx New Progress UI component using Radix UI primitives
src/shared/components/ui/badge.tsx New Badge UI component with variants
package.json Added @radix-ui/react-progress and tailwind-scrollbar-hide dependencies
src/app/globals.css Global scrollbar-hide utility applied
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 9 to 15
} from "../model";

const ITEMS_PER_PAGE = 5;

export const newsPaginationHandler = async (page: number = 0): Promise<PaginatedNewsResponse> => {
try {
// 두 API를 병렬로 호출
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The newsResponse and analysisResponse do not have a success property. According to the newsAPI() and analysisAPI() implementations in the codebase, they return the parsed JSON data directly (NewsItem[] and AnalysisItem respectively), not wrapped in an APIResponse object. This condition will always evaluate to false, causing the handler to throw an error even when the APIs succeed.

Consider removing this check or updating the API functions to return APIResponse objects:

// Option 1: Remove the check (rely on try-catch for errors)
const newsData = newsResponse as NewsItem[];
const analysisData = analysisResponse as AnalysisItem;

// Option 2: Update newsAPI() and analysisAPI() to return APIResponse<T>
Suggested change
} from "../model";
const ITEMS_PER_PAGE = 5;
export const newsPaginationHandler = async (page: number = 0): Promise<PaginatedNewsResponse> => {
try {
// 두 API를 병렬로 호출
const newsData = newsResponse as NewsItem[];
const analysisData = analysisResponse as AnalysisItem;

Copilot uses AI. Check for mistakes.
const config = getSentimentConfig(news.sentiment);

return (
<article className='flex flex-col gap-4 overflow-hidden bg-surface-dark p-6 transition-all hover:border-white/20'>
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The group-hover:line-clamp-none class in ContentBox won't work because the parent article element in NewsCard (line 17) is missing the group class. Add group to the article's className:

<article className='group flex flex-col gap-4 overflow-hidden bg-surface-dark p-6 transition-all hover:border-white/20'>

Also note that line 49 of TitleBox.tsx has group-hover:text-blue-400 which has the same issue.

Suggested change
<article className='flex flex-col gap-4 overflow-hidden bg-surface-dark p-6 transition-all hover:border-white/20'>
<article className='group flex flex-col gap-4 overflow-hidden bg-surface-dark p-6 transition-all hover:border-white/20'>

Copilot uses AI. Check for mistakes.
Comment on lines +43 to +50
return {
newsId: news.id,
title: news.title,
content: news.content,
url: news.url,
source: news.source,
reason: analysis.reason,
keywords: analysis.keywords,
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pagination logic doesn't handle the case where the requested page exceeds available pages. If a user requests page=10 but there are only 4 pages of data, an empty array will be returned without any indication that the page is out of bounds.

Consider adding validation before slicing:

if (page >= totalPages && totalPages > 0) {
  throw new Error(`Page ${page} exceeds total pages ${totalPages}`);
}

Or return an error response from the API route handler.

Copilot uses AI. Check for mistakes.
const ITEMS_PER_PAGE = 5;

export const newsPaginationHandler = async (page: number = 0): Promise<PaginatedNewsResponse> => {
try {
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These type assertions using as are unnecessary and potentially unsafe. Since the success check above will always fail (see previous comment), this code is unreachable. Once the success check is fixed, you can safely remove these assertions since TypeScript can infer the types from the API function return types.

Suggested change
try {
const analysisData = analysisResponse.data;

Copilot uses AI. Check for mistakes.
Comment on lines +9 to +22
function Progress({ className, value, ...props }: React.ComponentProps<typeof ProgressPrimitive.Root>) {
return (
<ProgressPrimitive.Root
data-slot='progress'
className={cn("relative h-2 w-full overflow-hidden rounded-full bg-primary/20", className)}
{...props}
>
<ProgressPrimitive.Indicator
data-slot='progress-indicator'
className='h-full w-full flex-1 bg-primary transition-all'
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
);
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Progress component is missing an aria-label or aria-labelledby attribute to provide context for screen readers. While the value prop provides the progress value, users with assistive technology need to know what the progress bar represents.

Consider adding an aria-label prop:

function Progress({ className, value, "aria-label": ariaLabel, ...props }: React.ComponentProps<typeof ProgressPrimitive.Root>) {
  return (
    <ProgressPrimitive.Root
      data-slot='progress'
      className={cn("relative h-2 w-full overflow-hidden rounded-full bg-primary/20", className)}
      aria-label={ariaLabel}
      {...props}
    >

Copilot uses AI. Check for mistakes.
@Dobbymin Dobbymin merged commit c6b4b35 into main Nov 23, 2025
3 checks passed
@Dobbymin Dobbymin deleted the feat#12-ai-news branch November 23, 2025 07:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

📡 API 비동기 통신 코드를 짜는 경우 ✨ Feature 새로운 기능 추가 및 구현하는 경우 🖥️ Server Next.js의 서버 기능 구현

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AI 뉴스 페이지

2 participants