Tiny Star

프로젝트/뉴스 요약 (AI)

[뉴스 요약] 프론트 (Vite + React)

하얀지 2025. 6. 20. 12:04

예상보다 잘 나왔다

 

 

계획

  1. 사용자가 뉴스 URL을 입력
  2. 입력된 URL을 백엔드로 POST 요청
  3. 결과 받아서 요약 결과 보여줌
  4. 비로그인 상태에서 일 5회 요청 제한 

 

Vite + React 선택

Vite + React는 빠른 개발, 직관적인 설정, 좋은 확장성 때문에
개인 프로젝트나 MVP 개발에 현 시점에서 가장 합리적인 프론트엔드 구조라고 한다.

💡 React 프로젝트에서 MVP라면?

  • View: React 컴포넌트
  • Model: API, 상태관리
  • Presenter: 컴포넌트 외부 로직 (예: useCase, service 함수)
 
역할 설명
Model 데이터와 비즈니스 로직 (API 호출, DB 등)
View UI (사용자에게 보여지는 화면)
Presenter View와 Model을 연결하고 비즈니스 로직을 수행하는 중간 계층
MVP는 생소해서 찾아봤는데, Presenter는 View에서 넘어온 데이터를 처리하는 부분이라고 생각하면 될 것 같다.
 
아무튼, 해당 프로젝트는 간단한 UI이기 때문에 가볍고 빠른 Vite를 사용하기로 했다.
 
 

Presenter

const handleSubmit = async (url: string) => {
    const key = getTodayKey();
    const currentCount = parseInt(getCookie(key) || '0');

    if (currentCount >= 5) {
      alert('비회원은 하루 최대 5회까지 분석할 수 있습니다.');
      return;
    }

    setLoading(true);
    setResult(null);

    try {
      const data = await analyzeUrl(url);
      setResult(data);
      setCookie(key, String(currentCount + 1), 1);
    } catch (err) {
      console.error(err);
      alert('분석에 실패했습니다.');
    } finally {
      setLoading(false);
    }
  };

로그인 기능은 없지만 하루 최대 5회 제한을 뒀다. 현재는 Database를 사용하지 않기 때문에 쿠키를 사용했다.

 

 

Model

export async function analyzeUrl(newsUrl: string, userId?: string) {
  const res = await fetch(`${API_URL}/api/analyze-url`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      // 추후 로그인 토큰 헤더 추가 가능
      // ...(token && { Authorization: `Bearer ${token}` }),
    },
    body: JSON.stringify({url: newsUrl, userId}),
  });

  if (!res.ok) {
    throw new Error('서버 요청 실패');
  }

  return res.json();
}

백엔드에서 만든 API를 호출하는 부분

로그인 기능을 생각하고 userId는 선택적 파라미터로 선언했다.

 

ESLint: Parsing error: Unexpected token

현재 ESLint가 TypeScript 문법(: string, : boolean 등) 을 이해하지 못하고 있기 때문에 발생

// cmd
npm install -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin

// eslint.config.ts
import tseslint from 'typescript-eslint';

export default [
  {
    languageOptions: {
      parser: tseslint.parser,
      ...

teslint.parser를 추가해서 해결했다.

해결하지 않아도 정상 실행은 가능하다. 

 

 

View

return (
    <div className="min-h-screen bg-gray-50 p-8">
      <div className="max-w-3xl mx-auto bg-white shadow-md rounded-lg p-6">
        <h1 className="text-2xl font-bold mb-6 text-gray-800">
          📰 뉴스 요약 분석기
        </h1>

        <UrlForm onSubmit={handleSubmit} />

        {loading && <p className="text-gray-500 mt-4">요약 중입니다...</p>}

        {result && (
          <div className="mt-6 space-y-6 border-t pt-6">
            {/* 주제 */}
            {result.topic && (
              <p className="text-sm text-gray-600 mb-1">
                {result.topic}
              </p>
            )}

            {/* 제목 */}
            {result.title && (
              <h2 className="text-2xl font-bold text-gray-900">
                {result.title}
              </h2>
            )}

            {/* 요약 */}
            {result.summary && (
              <div className="bg-gray-100 p-4 rounded-lg shadow-inner">
                <p className="text-gray-800 whitespace-pre-line leading-relaxed">
                  {result.summary}
                </p>
              </div>
            )}

            {/* 키워드 */}
            {Array.isArray(result.keywords) && result.keywords.length > 0 && (
              <div className="flex flex-wrap gap-2">
                {result.keywords.map((k: string, i: number) => (
                  <span
                    key={i}
                    className="bg-blue-100 text-blue-800 px-3 py-1 rounded-full text-sm font-medium"
                  >
                  #{k}
                </span>
                ))}
              </div>
            )}

            {/* 원문 링크 */}
            {result.url && (
              <div className="mt-2">
                <a
                  href={result.url}
                  target="_blank"
                  rel="noopener noreferrer"
                  className="text-blue-600 hover:underline text-sm"
                >
                  👉 원문 기사 보기
                </a>
              </div>
            )}
          </div>
        )}
      </div>
    </div>
  );

tailwindcss를 사용하였다.

 

[깃허브 코드]

 

top