Skip to content

팀 뉴스 모음과 번역 기능 추가 - #29

Merged
son0307 merged 2 commits into
mainfrom
codex/team-news
Jul 16, 2026
Merged

팀 뉴스 모음과 번역 기능 추가#29
son0307 merged 2 commits into
mainfrom
codex/team-news

Conversation

@son0307

@son0307 son0307 commented Jul 16, 2026

Copy link
Copy Markdown
Owner

변경 사항

  • SerpApi Google News를 통해 신뢰할 수 있는 출판사의 팀 관련 뉴스를 수집하고, 출판사 도메인 검증과 URL 중복 제거를 수행하며 검색 결과의 순서를 DB에 보존합니다.
  • 일일 자동 동기화, 90일 보관 데이터 정리, DB 기반 팀 뉴스 조회 API, 관리자 수동 새로고침 기능을 추가합니다.
  • 새로 저장된 영문 뉴스 제목을 OpenAI Responses API의 구조화된 출력을 사용해 번역하며, 관리자가 번역되지 않은 개별 기사의 번역을 다시 요청할 수 있도록 합니다.
  • 팀 상세 화면에 뉴스 탭을 추가하고 한국어/영문 제목 전환, 원문 기사 링크, 로딩·오류·빈 목록 상태, 마지막 수집 시각을 표시합니다.

변경 이유

기사 본문을 크롤링하거나 저장하지 않으면서 팀과 관련된 신뢰도 높은 뉴스를 제공하기 위한 기능입니다. 검색 결과를 DB에 저장하므로 외부 서비스에 일시적인 장애가 발생하더라도 기존 뉴스를 계속 조회할 수 있습니다.

API 및 설정 변경

  • GET /api/v1/teams/{teamId}/news API를 추가합니다.
  • 관리자가 팀 뉴스를 새로 수집하거나 개별 기사의 번역을 요청할 수 있는 API를 추가합니다.
  • SerpApi, OpenAI, 자동 동기화 일정, 번역 배치 크기, 데이터 보관 기간을 환경 변수로 설정할 수 있도록 합니다.

검증

  • .\gradlew.bat test --tests "com.son.soccerStreaming.news.*"
  • npm run build
    로컬 프런트엔드 개발 서버가 실행 중이지 않아 브라우저 검증은 수행하지 못했습니다.

후속 작업

외부 API 클라이언트의 타임아웃, HTTP 상태 코드별 재시도, 서킷 브레이커, 모니터링은 별도 작업에서 전체적으로 점검할 예정입니다.

@son0307 son0307 changed the title Add team news collection and title translation 팀 뉴스 모음과 번역 기능 추가 Jul 16, 2026
@son0307
son0307 marked this pull request as ready for review July 16, 2026 07:36
@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown

Codex PR Review

PR 요약

요약

팀 상세 화면에서 관련 뉴스를 조회하고, 저장된 뉴스 제목을 한국어로 번역해 보여주는 기능이 추가되었습니다. 외부 뉴스/번역 API 결과를 DB에 저장해 재조회 가능하게 하고, 관리자용 수동 새로고침 및 개별 기사 번역 API도 함께 제공합니다.

주요 변경사항

  • SerpApi Google News 기반 팀 뉴스 수집 클라이언트와 신뢰 도메인 필터링, URL 정규화/중복 제거 로직 추가
  • 팀 뉴스 저장용 엔티티, 저장/조회 리포지토리, 수집 상태 관리 로직 추가
  • OpenAI Responses API를 이용한 뉴스 제목 번역 클라이언트 및 번역 배치 처리 추가
  • 관리자용 팀 뉴스 새로고침, 개별 기사 번역 API 추가
  • 팀 상세 화면에 뉴스 탭, 한국어/영문 제목 전환, 원문 링크, 로딩/오류/빈 상태, 마지막 수집 시각 표시 추가
  • 뉴스 관련 설정값을 application.yml과 설정 프로퍼티로 분리
  • 뉴스 수집/번역/SerpApi/OpenAI 클라이언트에 대한 테스트 추가

영향 범위

  • 팀 상세 UI
  • 팀 뉴스 조회 API
  • 관리자 뉴스 수집/번역 API
  • 외부 연동 설정(SerpApi, OpenAI)
  • 뉴스 저장/정리 및 자동 동기화
  • 프론트엔드 API 타입/캐시 처리

검증

  • .\gradlew.bat test --tests "com.son.soccerStreaming.news.*"
  • npm run build

시니어 개발자 리뷰

  • [P1] frontend/src/pages/TeamDetailPage.tsx:176-207 - 뉴스 탭 전환 시 이전 요청 응답이 새 팀 화면을 덮어쓸 수 있음

    • 문제: activeTab === "news"일 때만 뉴스 로딩을 시작하지만, numericTeamId 변경 시 newsState 초기화와 newsRequestId 증가만 하고, 이미 진행 중인 fetchTeamNews/refreshTeamNews 요청의 응답이 늦게 돌아오면 현재 팀이 바뀐 뒤에도 상태를 갱신할 여지가 있습니다. 특히 retryNews()refreshNewsFromProviders()activeNewsTeamId.current/newsRequestId.current를 함께 보지만, 초기 로딩 경로와 새로고침 경로의 상태 정리가 일관적이지 않아 팀 전환 직후 뉴스 탭에 이전 팀 데이터가 잠깐 보이거나 오류 메시지가 꼬일 수 있습니다.
    • 영향: 모바일에서 팀 상세를 빠르게 넘길 때 뉴스 탭에 잘못된 기사 목록/수집 시각이 노출될 수 있습니다. 사용자 신뢰도 저하와 디버깅 난이도 상승으로 이어집니다.
    • 수정 방향: 팀 ID 변경 시점에 모든 뉴스 관련 진행 요청을 명시적으로 무효화하고, fetchTeamNews 결과를 반영하기 전에 현재 numericTeamId와 요청 시점 팀 ID를 함께 검증하세요. 초기 로딩/재시도/수동 새로고침 모두 같은 취소 토큰 패턴으로 통일하는 것이 안전합니다.
  • [P1] src/main/java/com/son/soccerStreaming/news/service/NewsPersistenceService.java:27-44 - 동일 URL 기사 업데이트 시 최신 게시일/URL 정합성이 깨질 수 있음

    • 문제: 기존 NewsArticle를 찾은 뒤 updateMetadata(...)에서 originalUrl, publishedAt, publisherName, publisherDomain을 무조건 덮어쓰고 있습니다. 그런데 urlHashNewsUrlNormalizer.normalize(item.url()) 결과로 계산하므로, 사소한 쿼리 정규화 차이나 provider가 같은 기사를 다른 canonical URL로 주면 동일 기사로 합쳐지면서 원본 URL이 최신 응답으로 바뀔 수 있습니다. 더 중요한 건 publishedAtnull 또는 더 오래된 값으로 덮일 수 있어 정렬/노출 품질이 흔들립니다.
    • 영향: DB에 저장된 팀 뉴스의 원문 링크가 수집 시점마다 바뀌거나, 오래된/잘못된 발행 시각으로 표시될 수 있습니다. 결과적으로 사용자가 같은 기사를 여러 번 다른 링크로 보게 되거나 최신성이 떨어져 보입니다.
    • 수정 방향: 기존 엔티티가 있을 때는 publishedAtoriginalUrl을 무조건 덮어쓰지 말고, 더 신뢰할 수 있는 값일 때만 갱신하세요. 최소한 publishedAt은 기존 값이 있으면 보존하고, originalUrl은 canonical URL만 유지하도록 제한하는 편이 안전합니다.
  • [P2] src/main/java/com/son/soccerStreaming/news/client/SerpApiNewsClient.java:105-126 - publisherDomain 추출이 예외에 취약함

    • 문제: publisherDomain(String value)URI.create(value)getHost()를 바로 호출합니다. flatten(...)에서 isAllowedUrl(link)는 먼저 검사하지만, 그 외 경로에서 publisherDomain(...)이 재사용되거나 허용 URL 검증이 조금만 바뀌면 NullPointerException/IllegalArgumentException이 쉽게 발생합니다. 또한 URI.create(value)는 상대 경로나 비정상 URL에서 예외를 던집니다.
    • 영향: 외부 뉴스 응답 형식이 조금만 흔들려도 전체 수집이 실패할 수 있습니다. 이런 실패는 일일 동기화나 관리자 새로고침에서 체감 장애로 이어집니다.
    • 수정 방향: publisherDomainisAllowedUrl처럼 방어적으로 처리하고, null/비정상 URL이면 빈 문자열이나 대체 publisher를 반환하도록 하세요. 최소한 호출부가 예외를 흡수하는 구조를 유지해야 합니다.
  • [P2] src/main/java/com/son/soccerStreaming/news/service/NewsTranslationPersistenceService.java:17-35 - 번역 반영이 영속화되지 않을 가능성

    • 문제: markAutoTranslationAttempted(...)applyResults(...)는 둘 다 @Transactional이지만, findAllById(...)로 가져온 엔티티를 수정만 하고 명시적 save(...)가 없습니다. JPA 영속성 컨텍스트 안에서는 동작할 수 있지만, 현재 서비스 메서드들이 다른 트랜잭션 경계에서 호출되거나 read-only 설정과 섞일 경우 변경 감지가 기대대로 동작하지 않을 수 있습니다. 특히 NewsTitleTranslationService에서 이 서비스 메서드를 호출하는 동안 예외가 나면 autoTranslationAttempted만 먼저 세팅되고 번역은 반영되지 않는 불일치 상태가 될 수 있습니다.
    • 영향: 자동 번역 후보가 영구적으로 소모되었는데 실제 번역은 저장되지 않는 데이터 불일치가 발생할 수 있습니다. 이후 배치에서 같은 기사를 다시 번역하지 않아 빈 제목이 남을 위험이 있습니다.
    • 수정 방향: 변경 대상 엔티티를 저장하거나, 최소한 호출 메서드 전체를 하나의 @Transactional 경계로 묶어 변경 감지가 확실히 일어나도록 하세요. applyResultsmarkAutoTranslationAttempted를 같은 트랜잭션에서 처리하는 것도 안전합니다.
  • [P3] frontend/src/pages/TeamDetailPage.tsx:607-679 - 뉴스 링크 텍스트가 길면 모바일에서 행이 과도하게 커질 수 있음

    • 문제: 뉴스 항목에서 기사 제목과 발행처/날짜가 한 줄 래핑되도록 되어 있지만, 번역 제목은 길어질 가능성이 높고 strong에만 overflow-wrap:anywhere가 적용되어 있습니다. publisherName/날짜 줄도 긴 경우 모바일에서 행 높이가 많이 늘어나 스크롤 가독성이 떨어질 수 있습니다.
    • 영향: 작은 화면에서 뉴스 목록이 빠르게 훑기 어려워지고, 한 화면에 노출되는 기사 수가 줄어듭니다.
    • 수정 방향: 발행처/날짜 영역도 적절히 줄바꿈/말줄임 처리를 추가해 카드 높이 폭증을 줄이세요.

보안 리뷰

  • [High] src/main/java/com/son/soccerStreaming/news/controller/AdminTeamNewsController.java:전체 - 관리자 뉴스 수집/번역 API의 인증·인가 보장 근거가 diff에 없음

    • 문제: /api/v1/admin/teams/{teamId}/news/refresh, /translate 엔드포인트가 추가되었지만, 이 PR diff 안에서는 관리자 권한 검증이나 CSRF 방어가 적용되는지 확인되지 않습니다. 프런트에서도 currentUser?.role === "ADMIN"은 UI 제어일 뿐 서버 보안이 아닙니다.
    • 공격/노출 시나리오: 비인가 사용자가 관리자용 새로고침/번역 API를 호출해 외부 API 사용량을 소모시키거나, 팀 뉴스 번역 상태를 변경할 수 있습니다. 팀 ID를 바꿔 다른 팀의 뉴스 데이터를 갱신하는 식의 권한 우회 가능성도 있습니다.
    • 수정 방향: 컨트롤러 또는 전역 보안 설정에서 관리자 전용 권한 체크를 명시적으로 적용하고, 필요한 경우 팀 단위 접근 제어도 검증하세요. POST 엔드포인트에 대한 CSRF 정책도 함께 확인하세요.
  • [High] src/main/java/com/son/soccerStreaming/news/service/AdminNewsTranslationService.java:18-38 - 기사 ID 기반 접근에 대한 팀 경계 검증이 저장소 쿼리 외부에서만 의존됨

    • 문제: findByTeamIdAndArticleId(teamId, articleId)로 관계를 찾긴 하지만, 이후 실제 번역/저장 로직은 articleId만 기준으로 동작합니다. 현재 구조상 다른 팀과 공유되는 동일 기사 관계가 있을 경우, 한 팀의 관리자 요청이 공용 기사 엔터티에 번역값을 기록하게 됩니다.
    • 공격/노출 시나리오: 한 팀에서 번역한 제목이 동일 기사로 연결된 다른 팀 뉴스에도 반영되어, 팀 간 데이터 경계가 약해질 수 있습니다. 관리자가 아닌 권한 수준의 팀 운영자 모델이 있다면 더 큰 경계 위반이 됩니다.
    • 수정 방향: 기사 엔터티를 공유할지 분리할지 정책을 명확히 하고, 공유 엔터티라면 번역 변경이 모든 팀에 미치는 영향과 권한을 재검토하세요. 팀별 독립 번역이 필요하면 팀-기사 조합 기준으로 번역 상태를 분리하세요.
  • [Medium] src/main/java/com/son/soccerStreaming/news/client/SerpApiNewsClient.java:37-65, 104-129 - 외부 제공 URL을 저장·노출하는 경로에 대한 정규화/검증 경계가 불완전

    • 문제: isAllowedUrl()로 허용 도메인을 제한하지만, 실제 저장은 normalize() 이후 결과를 사용합니다. publisherDomain()URI.create(value).getHost()를 직접 사용해 예외 가능성이 있고, URL 정규화/검증이 서로 다른 기준으로 이뤄집니다.
    • 공격/노출 시나리오: SerpApi 응답이 비정상 URL을 포함하면 예외로 동기화가 중단되거나, 정규화/검증 불일치로 예상치 못한 URL이 저장될 수 있습니다. 결과적으로 저장된 originalUrl이 프런트에서 외부 링크로 바로 사용되므로, 검증 우회 시 악성 링크 노출 위험이 있습니다.
    • 수정 방향: 검증과 정규화를 하나의 함수/흐름으로 묶고, 파싱 실패 시 안전하게 폐기하세요. 저장 직전 최종 URL에 대해 다시 한 번 허용 목록 검사를 적용하는 것이 좋습니다.
  • [Medium] src/main/java/com/son/soccerStreaming/news/client/OpenAiTitleTranslationClient.java:41-66 - OpenAI 응답 구조에 대한 신뢰가 높아 외부 응답 주입에 취약

    • 문제: 구조화 출력이라도 최종 번역 문자열을 거의 그대로 저장합니다. 길이 제한은 있지만, 번역문에 HTML/스크립트성 문자열이 섞일 가능성에 대한 서버측 무해화가 없습니다.
    • 공격/노출 시나리오: 번역 결과가 그대로 프런트에 표시되면, 이후 렌더링 방식 변경이나 다른 소비자에서 dangerouslySetInnerHTML 같은 방식이 도입될 때 XSS 전파원이 될 수 있습니다.
    • 수정 방향: 서버에서 번역 결과를 순수 텍스트로만 다루고, 프런트 렌더링도 텍스트 노드 기준을 유지하세요. 필요 시 저장 전에 제어문자/비정상 태그를 정규화하세요.
  • [Low] frontend/src/pages/TeamDetailPage.tsx:611-681 - 외부 기사 링크를 새 탭으로 직접 오픈

    • 문제: originalUrl을 그대로 href에 사용합니다. 현재는 서버 측 허용 목록이 있으나, 방어가 완벽하지 않거나 후속 변경으로 검증이 느슨해지면 외부 링크 기반 위험이 생깁니다.
    • 공격/노출 시나리오: 허용되지 않은 URL이 저장되면 사용자가 악성 사이트로 이동할 수 있습니다. rel="noopener noreferrer"는 적용되어 있지만, 링크 대상 자체의 신뢰성은 서버 검증에 전적으로 의존합니다.
    • 수정 방향: 서버 저장 단계에서 허용 목록 검증을 강제하고, 클라이언트는 추가로 URL 스킴/도메인 표시를 제공해 사용자가 출처를 확인할 수 있게 하세요.
  • [Low] src/main/java/com/son/soccerStreaming/news/service/NewsCollectionService.java:28-34 - 팀명과 URL 관련 정보가 경고 로그에 남음

    • 문제: 에러 로그에 teamName, teamId가 포함됩니다. 비밀값은 아니지만 운영 로그에 외부 검색/수집 대상이 축적됩니다.
    • 공격/노출 시나리오: 로그 접근 권한이 넓은 환경에서는 어느 팀을 언제 수집했는지 추적 가능해집니다.
    • 수정 방향: 필요 최소한의 식별자만 남기고, 로그 보존 정책을 점검하세요.
      Model: gpt-5.4-mini
      Diff was truncated to 90000 characters; review may be incomplete.

@son0307
son0307 merged commit 69671ec into main Jul 16, 2026
1 check passed
@son0307
son0307 deleted the codex/team-news branch July 16, 2026 07:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant