Skip to content

refactor: 콘텐츠 배치 리펙토링 및 콘텐츠 API 인가 적용#260

Merged
plzslp merged 6 commits into
devfrom
refactor/253-content-batch
Jul 3, 2026
Merged

refactor: 콘텐츠 배치 리펙토링 및 콘텐츠 API 인가 적용#260
plzslp merged 6 commits into
devfrom
refactor/253-content-batch

Conversation

@plzslp

@plzslp plzslp commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

관련 이슈

작업 내용

  • 기존 ItemProcessor에서 existsBySourceAndExternalId() 메서드로 인해 DB 호출이 청크 사이즈(최대100) 만큼 발생하던 문제 해결
  • 콘텐츠 API 인가 적용

변경 사항

  • Processor에서 DB 의존성 제거 -> Content로 변환만 수행
  • ContentItemWriter에서 청크 단위 IN 쿼리로 기존 항목 일괄 필터링
  • Processor, Writer 관련 테스트 수정
  • SecurityConfig: /api/contents, /api/admin/** 경로 규칙 추가, EnableMethodSecurity 추가
  • ContentController: POST/PATCH/DELETE: hasRole ADMIN 추가
  • ContentCollectionController: 클래스 레벨에 hasRole ADMIN 추가

테스트 내용

  • 테스트 코드 작성
  • 로컬 테스트 완료
  • API 테스트 완료
  • 기타 테스트 완료

체크리스트

  • 코드 컨벤션을 지켰습니다.
  • 불필요한 코드를 제거했습니다.
  • 주석이 필요한 부분에 추가했습니다.
  • 관련 문서를 수정했습니다.
  • 리뷰어가 이해하기 쉽도록 작성했습니다.

리뷰 포인트

  • 중점적으로 봐줬으면 하는 부분을 작성해주세요.

스크린샷 / 참고 자료

  • 필요한 경우 첨부해주세요.

🔗 관련 이슈

  • close #253
  • close #147

📝 작업 내용

이번 PR은 (1) 콘텐츠 수집 배치의 중복 체크로 인해 발생하던 과도한 DB 조회와 책임 혼선을 줄이고, (2) 콘텐츠 API 및 관리자용 콘텐츠 수집 경로에 대한 인가를 명확히 적용하는 데 목적이 있습니다.

기존에는 배치의 Processor가 아이템 단위로 existsBySourceAndExternalId() 조회를 수행하며 중복을 판단했습니다. 이 방식은 청크 크기만큼 쿼리가 늘어날 수 있고, Processor가 “매핑” 외에 “영속성 조회/중복 판단”까지 떠안게 되어 역할이 흐려졌습니다. 또한 중복 반환(스킵) 방식이 바뀌면서 filter_count 기록 흐름도 함께 정리할 필요가 있었습니다.

또한 콘텐츠 생성/수정/삭제 및 관리자용 수집 API는 ADMIN 전용으로 제한되어야 했지만, 권한 규칙이 충분히 반영되지 않은 상태였던 부분을 보완했습니다.

주요 변경사항

  1. 배치 중복 체크 책임 이동 (Processor → Writer)

    • Processor에서는 DTO → Entity/메타데이터 변환만 수행하도록 단순화했고, ContentRepository 기반 존재 조회/스킵 분기 로직을 제거했습니다.
    • Writer에서는 청크 전체의 externalId를 모은 뒤 source + externalId 목록 기준 IN 쿼리로 기존 콘텐츠를 한 번에 조회하고, 이미 존재하는 항목은 제외한 뒤 신규 데이터만 저장하도록 흐름을 변경했습니다.
    • 이에 따라 기존의 아이템별 스킵/반환 방식이 사라져 filter_count는 더 이상 기록되지 않게 정리했습니다.
  2. Repository 확장: 일괄 조회용 쿼리 추가

    • ContentRepositoryfindExternalIdsBySourceAndExternalIdIn(...)(source + externalId 목록 IN 조회) 메서드를 추가하고, 아이템 단건 존재 여부 확인 메서드는 제거했습니다.
    • 이로써 Writer가 청크 단위로 필요한 최소 정보만 한 번에 가져올 수 있게 했습니다.
  3. 콘텐츠 API 인가 적용(ADMIN 제한)

    • SecurityConfig에 요청 경로 기반 인가 규칙을 추가하고, 메서드 수준 보안 활성화를 위해 @EnableMethodSecurity를 추가했습니다.
    • ContentController생성/수정/삭제(POST/PATCH/DELETE /api/contents*)는 @PreAuthorize("hasRole('ADMIN')")로 제한했습니다.
    • ContentCollectionController의 관리자용 수집 엔드포인트(/api/admin/** 하위)는 클래스 레벨 @PreAuthorize("hasRole('ADMIN')")로 제한했습니다.
    • 선택한 접근은 “요청 경로 규칙 + 메서드 보안”의 조합으로, 엔드포인트별 의도를 명시적으로 강제하는 데 초점을 맞췄습니다.

📌 리뷰 시 특히 주의할 변경/트레이드오프

  • 배치 중복 스킵의 판단 주체가 Processor에서 Writer로 이동하면서, 청크 단위로 조회 후 필터링이 이루어집니다. 따라서 기존처럼 아이템 처리 시점에 즉시 스킵/카운트가 발생하는 패턴은 사라집니다(예: filter_count 기록 중단).
  • SecurityConfig의 경로 매칭 규칙과 @PreAuthorize 메서드 제한이 함께 적용됩니다. 실제 역할(ROLE_ prefix 포함 여부 등)이 기대와 일치하는지 확인이 필요합니다.

📊 변경 효과 요약 (가능한 경우에만)

  • 제공된 변경 목록 기준 수정 파일: 13개
  • 정량 지표(배치 처리 시간/쿼리 횟수 감소량 등)는 이번 PR에서 측정하지 않음.

@plzslp plzslp self-assigned this Jul 3, 2026
@plzslp plzslp added the refactor label Jul 3, 2026
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@plzslp, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 27 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 54119805-b325-4954-8a53-e9bbcf82f3a2

📥 Commits

Reviewing files that changed from the base of the PR and between fe08805 and 351d8f2.

📒 Files selected for processing (2)
  • src/main/java/com/codeit/team5/mopl/content/batch/writer/ContentItemWriter.java
  • src/test/java/com/codeit/team5/mopl/content/batch/writer/ContentItemWriterTest.java
📝 Walkthrough

Walkthrough

이번 PR은 두 축으로 나뉩니다. 배치 수집에서는 TMDB/ SportsDB Processor의 DB 중복 확인을 제거하고, ContentRepository에 externalId 일괄 조회 쿼리를 추가한 뒤, ContentItemWriter가 DB 기준으로 신규 항목만 저장하도록 바꿨습니다. 동시에 관련 테스트와 BatchConfig가 갱신됐습니다. 보안 쪽에서는 @EnableMethodSecurity를 활성화하고, 콘텐츠 생성/수정/삭제 및 관리자 수집 경로에 ADMIN 권한 제한을 추가했습니다.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed 배치 리팩토링과 콘텐츠 API 인가 적용을 함께 요약해 주요 변경점을 명확히 드러냅니다.
Description check ✅ Passed 필수 섹션(관련 이슈, 작업 내용, 변경 사항, 테스트, 체크리스트)을 대부분 채워 템플릿 요구를 충족합니다.
Linked Issues check ✅ Passed Processor→Writer 이동, IN 쿼리 추가, ADMIN 인가와 @PreAuthorize 적용 등 핵심 요구사항이 반영되었습니다.
Out of Scope Changes check ✅ Passed 배치 리팩토링과 콘텐츠 인가에 직접 관련된 변경만 보이며, 눈에 띄는 무관한 변경은 없습니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/253-content-batch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/java/com/codeit/team5/mopl/config/SecurityConfig.java`:
- Around line 65-69: The ADMIN authorization is duplicated between
SecurityConfig’s URL matchers and the controllers’ `@PreAuthorize` annotations, so
choose a single source of truth and align the other layer accordingly. Update
the relevant access rules in SecurityConfig and the affected controller methods
so /api/contents and /api/admin/** follow one consistent authorization strategy,
using the existing matcher setup and `@PreAuthorize` usage as the reference
points.

In
`@src/main/java/com/codeit/team5/mopl/content/batch/writer/ContentItemWriter.java`:
- Around line 36-53: Add an early return in ContentItemWriter.write for empty
chunks before accessing items.get(0), since Spring Batch can call write() with a
filtered/skip-only Chunk that has no items. Update the logic around the initial
items List and source extraction so that items.isEmpty() returns immediately,
then only compute ContentSource source and query
contentRepository.findExternalIdsBySourceAndExternalIdIn when there is at least
one item.

In
`@src/main/java/com/codeit/team5/mopl/content/repository/ContentRepository.java`:
- Around line 20-26: Remove the unused existsBySourceAndExternalId method from
ContentRepository if there are no remaining call sites, and keep the repository
API centered on findExternalIdsBySourceAndExternalIdIn for the batch flow.
Verify any references in the content persistence path still compile after
deleting the method, and update any related tests or usages tied to
ContentRepository accordingly.

In
`@src/test/java/com/codeit/team5/mopl/content/batch/writer/ContentItemWriterTest.java`:
- Around line 98-118: Add a regression test for empty chunks in
ContentItemWriterTest to cover the items.get(0) path inside
ContentItemWriter.write(). Create a test that calls writer.write(new
Chunk<>(List.of())) and asserts it completes without throwing, so the empty
input case is safely handled. Use the existing
write_allExistingInDb_skipsAllSaves pattern as a guide and keep the focus on the
write() method behavior rather than repository interactions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9e65c0a5-7910-4cce-b234-fbc4b59ec63f

📥 Commits

Reviewing files that changed from the base of the PR and between 89e7695 and 43753fb.

📒 Files selected for processing (13)
  • src/main/java/com/codeit/team5/mopl/config/BatchConfig.java
  • src/main/java/com/codeit/team5/mopl/config/SecurityConfig.java
  • src/main/java/com/codeit/team5/mopl/content/batch/processor/SportsDbEventItemProcessor.java
  • src/main/java/com/codeit/team5/mopl/content/batch/processor/TmdbMovieItemProcessor.java
  • src/main/java/com/codeit/team5/mopl/content/batch/processor/TmdbTvSeriesItemProcessor.java
  • src/main/java/com/codeit/team5/mopl/content/batch/writer/ContentItemWriter.java
  • src/main/java/com/codeit/team5/mopl/content/controller/ContentCollectionController.java
  • src/main/java/com/codeit/team5/mopl/content/controller/ContentController.java
  • src/main/java/com/codeit/team5/mopl/content/repository/ContentRepository.java
  • src/test/java/com/codeit/team5/mopl/content/batch/processor/SportsDbEventItemProcessorTest.java
  • src/test/java/com/codeit/team5/mopl/content/batch/processor/TmdbMovieItemProcessorTest.java
  • src/test/java/com/codeit/team5/mopl/content/batch/processor/TmdbTvSeriesItemProcessorTest.java
  • src/test/java/com/codeit/team5/mopl/content/batch/writer/ContentItemWriterTest.java
💤 Files with no reviewable changes (3)
  • src/main/java/com/codeit/team5/mopl/content/batch/processor/SportsDbEventItemProcessor.java
  • src/main/java/com/codeit/team5/mopl/content/batch/processor/TmdbTvSeriesItemProcessor.java
  • src/main/java/com/codeit/team5/mopl/content/batch/processor/TmdbMovieItemProcessor.java

Comment thread src/main/java/com/codeit/team5/mopl/config/SecurityConfig.java
Comment thread src/main/java/com/codeit/team5/mopl/content/batch/writer/ContentItemWriter.java Outdated
Comment thread src/main/java/com/codeit/team5/mopl/content/repository/ContentRepository.java Outdated
@plzslp plzslp requested review from conradrado and metDaisy July 3, 2026 02:12
metDaisy
metDaisy previously approved these changes Jul 3, 2026
conradrado
conradrado previously approved these changes Jul 3, 2026

@conradrado conradrado left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

크게 문제없어 보입니다~ 고생하셨습니다~

plzslp added 5 commits July 3, 2026 13:12
- ProcessorTest: ContentRepository mock 및 중복 검증 제거
- ContentItemWriterTest: IN 쿼리 stub 추가 및 DB 기반 중복 제거, early return 케이스 추가
- SecurityConfig에 경로 규칙 추가
- EnableMethodSecurity 활성화
- ContentController 각 메서드에 PreAuthorize 적용
- ContentCollectionController 클래스 레벨에 PreAuthorize 적용
- 빈 청크 반환처리 추가 및 테스트 코드 추가
- 사용되지 않는 ContentRepository 메서드 제거
@plzslp plzslp dismissed stale reviews from conradrado and metDaisy via fe08805 July 3, 2026 04:12
@plzslp plzslp force-pushed the refactor/253-content-batch branch from a9773d7 to fe08805 Compare July 3, 2026 04:12

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/com/codeit/team5/mopl/content/controller/ContentController.java (1)

59-100: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

ADMIN 인가에 대한 403 테스트를 추가하세요
@AutoConfigureMockMvc(addFilters = false) 상태에서는 성공 케이스만으로 @PreAuthorize("hasRole('ADMIN')")가 비-ADMIN 요청을 막는지 드러나지 않습니다. @WithMockUser(roles = "USER")로 403, roles = "ADMIN"으로 201/200을 각각 넣으면 이번 변경의 권한 규칙을 더 안전하게 보호할 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/codeit/team5/mopl/content/controller/ContentController.java`
around lines 59 - 100, Add authorization coverage for the ADMIN-only content
endpoints in the controller tests, since MockMvc with filters disabled does not
prove `@PreAuthorize`("hasRole('ADMIN')") is enforced. Update the tests around
ContentController to include a non-admin case using `@WithMockUser`(roles =
"USER") that expects 403 for postContent and patchContent, and keep/admin-add
equivalent success cases with `@WithMockUser`(roles = "ADMIN") for the same
ContentController methods.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/java/com/codeit/team5/mopl/config/SecurityConfig.java`:
- Line 90: Open access is currently too broad because
SecurityConfig.anyRequest().permitAll() leaves unspecified API routes public,
including the missing /api/contents/{contentId}/watching-sessions path. Update
the SecurityConfig authorization rules so the default for anyRequest is
authenticated(), and keep only the truly public endpoints explicitly marked with
permitAll(); use the existing SecurityConfig configuration block to locate and
adjust the matcher order.

In
`@src/main/java/com/codeit/team5/mopl/content/batch/writer/ContentItemWriter.java`:
- Around line 43-57: `ContentItemWriter` currently filters only against IDs
already in the database, so duplicate `externalId` values inside the same chunk
can still reach `saveAll` and violate the `contents(source, external_id)`
uniqueness rule. After the existing `findExternalIdsBySourceAndExternalIdIn`
filter in `write`, add a second de-duplication pass over `deduplicatedItems`
using a stable key-based approach such as `LinkedHashMap` so only the first
occurrence of each `item.content().getExternalId()` is kept while preserving
order. Keep the existing DB-based filtering and the early return/logging path
intact.

In
`@src/test/java/com/codeit/team5/mopl/content/batch/writer/ContentItemWriterTest.java`:
- Around line 76-129: 현재 추가된 DB 기준 테스트들은 좋지만, `ContentItemWriter`의 청크 내부 중복 처리에
대한 회귀 테스트가 빠져 있습니다. `write_deduplicatesAgainstDb`,
`write_emptyChunk_doesNothing`, `write_allExistingInDb_skipsAllSaves`와 같은 스타일로,
같은 청크에 동일한 externalId를 가진 `ContentWithMetaData` 두 개가 들어오고 DB에는 없을 때
`ContentItemWriter.write`가 중복 저장을 막는지 검증하는 테스트를 다시 추가하세요. 이 테스트는
`ContentItemWriter`의 청크 내 deduplication 로직을 직접 검증하도록 구성하면 됩니다.

---

Outside diff comments:
In
`@src/main/java/com/codeit/team5/mopl/content/controller/ContentController.java`:
- Around line 59-100: Add authorization coverage for the ADMIN-only content
endpoints in the controller tests, since MockMvc with filters disabled does not
prove `@PreAuthorize`("hasRole('ADMIN')") is enforced. Update the tests around
ContentController to include a non-admin case using `@WithMockUser`(roles =
"USER") that expects 403 for postContent and patchContent, and keep/admin-add
equivalent success cases with `@WithMockUser`(roles = "ADMIN") for the same
ContentController methods.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 322f3629-1e23-4616-890e-d6f280621ce2

📥 Commits

Reviewing files that changed from the base of the PR and between 43753fb and fe08805.

📒 Files selected for processing (13)
  • src/main/java/com/codeit/team5/mopl/config/BatchConfig.java
  • src/main/java/com/codeit/team5/mopl/config/SecurityConfig.java
  • src/main/java/com/codeit/team5/mopl/content/batch/processor/SportsDbEventItemProcessor.java
  • src/main/java/com/codeit/team5/mopl/content/batch/processor/TmdbMovieItemProcessor.java
  • src/main/java/com/codeit/team5/mopl/content/batch/processor/TmdbTvSeriesItemProcessor.java
  • src/main/java/com/codeit/team5/mopl/content/batch/writer/ContentItemWriter.java
  • src/main/java/com/codeit/team5/mopl/content/controller/ContentCollectionController.java
  • src/main/java/com/codeit/team5/mopl/content/controller/ContentController.java
  • src/main/java/com/codeit/team5/mopl/content/repository/ContentRepository.java
  • src/test/java/com/codeit/team5/mopl/content/batch/processor/SportsDbEventItemProcessorTest.java
  • src/test/java/com/codeit/team5/mopl/content/batch/processor/TmdbMovieItemProcessorTest.java
  • src/test/java/com/codeit/team5/mopl/content/batch/processor/TmdbTvSeriesItemProcessorTest.java
  • src/test/java/com/codeit/team5/mopl/content/batch/writer/ContentItemWriterTest.java
💤 Files with no reviewable changes (3)
  • src/main/java/com/codeit/team5/mopl/content/batch/processor/TmdbMovieItemProcessor.java
  • src/main/java/com/codeit/team5/mopl/content/batch/processor/SportsDbEventItemProcessor.java
  • src/main/java/com/codeit/team5/mopl/content/batch/processor/TmdbTvSeriesItemProcessor.java

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/com/codeit/team5/mopl/content/controller/ContentController.java (1)

59-100: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

ADMIN 인가에 대한 403 테스트를 추가하세요
@AutoConfigureMockMvc(addFilters = false) 상태에서는 성공 케이스만으로 @PreAuthorize("hasRole('ADMIN')")가 비-ADMIN 요청을 막는지 드러나지 않습니다. @WithMockUser(roles = "USER")로 403, roles = "ADMIN"으로 201/200을 각각 넣으면 이번 변경의 권한 규칙을 더 안전하게 보호할 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/codeit/team5/mopl/content/controller/ContentController.java`
around lines 59 - 100, Add authorization coverage for the ADMIN-only content
endpoints in the controller tests, since MockMvc with filters disabled does not
prove `@PreAuthorize`("hasRole('ADMIN')") is enforced. Update the tests around
ContentController to include a non-admin case using `@WithMockUser`(roles =
"USER") that expects 403 for postContent and patchContent, and keep/admin-add
equivalent success cases with `@WithMockUser`(roles = "ADMIN") for the same
ContentController methods.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/java/com/codeit/team5/mopl/config/SecurityConfig.java`:
- Line 90: Open access is currently too broad because
SecurityConfig.anyRequest().permitAll() leaves unspecified API routes public,
including the missing /api/contents/{contentId}/watching-sessions path. Update
the SecurityConfig authorization rules so the default for anyRequest is
authenticated(), and keep only the truly public endpoints explicitly marked with
permitAll(); use the existing SecurityConfig configuration block to locate and
adjust the matcher order.

In
`@src/main/java/com/codeit/team5/mopl/content/batch/writer/ContentItemWriter.java`:
- Around line 43-57: `ContentItemWriter` currently filters only against IDs
already in the database, so duplicate `externalId` values inside the same chunk
can still reach `saveAll` and violate the `contents(source, external_id)`
uniqueness rule. After the existing `findExternalIdsBySourceAndExternalIdIn`
filter in `write`, add a second de-duplication pass over `deduplicatedItems`
using a stable key-based approach such as `LinkedHashMap` so only the first
occurrence of each `item.content().getExternalId()` is kept while preserving
order. Keep the existing DB-based filtering and the early return/logging path
intact.

In
`@src/test/java/com/codeit/team5/mopl/content/batch/writer/ContentItemWriterTest.java`:
- Around line 76-129: 현재 추가된 DB 기준 테스트들은 좋지만, `ContentItemWriter`의 청크 내부 중복 처리에
대한 회귀 테스트가 빠져 있습니다. `write_deduplicatesAgainstDb`,
`write_emptyChunk_doesNothing`, `write_allExistingInDb_skipsAllSaves`와 같은 스타일로,
같은 청크에 동일한 externalId를 가진 `ContentWithMetaData` 두 개가 들어오고 DB에는 없을 때
`ContentItemWriter.write`가 중복 저장을 막는지 검증하는 테스트를 다시 추가하세요. 이 테스트는
`ContentItemWriter`의 청크 내 deduplication 로직을 직접 검증하도록 구성하면 됩니다.

---

Outside diff comments:
In
`@src/main/java/com/codeit/team5/mopl/content/controller/ContentController.java`:
- Around line 59-100: Add authorization coverage for the ADMIN-only content
endpoints in the controller tests, since MockMvc with filters disabled does not
prove `@PreAuthorize`("hasRole('ADMIN')") is enforced. Update the tests around
ContentController to include a non-admin case using `@WithMockUser`(roles =
"USER") that expects 403 for postContent and patchContent, and keep/admin-add
equivalent success cases with `@WithMockUser`(roles = "ADMIN") for the same
ContentController methods.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 322f3629-1e23-4616-890e-d6f280621ce2

📥 Commits

Reviewing files that changed from the base of the PR and between 43753fb and fe08805.

📒 Files selected for processing (13)
  • src/main/java/com/codeit/team5/mopl/config/BatchConfig.java
  • src/main/java/com/codeit/team5/mopl/config/SecurityConfig.java
  • src/main/java/com/codeit/team5/mopl/content/batch/processor/SportsDbEventItemProcessor.java
  • src/main/java/com/codeit/team5/mopl/content/batch/processor/TmdbMovieItemProcessor.java
  • src/main/java/com/codeit/team5/mopl/content/batch/processor/TmdbTvSeriesItemProcessor.java
  • src/main/java/com/codeit/team5/mopl/content/batch/writer/ContentItemWriter.java
  • src/main/java/com/codeit/team5/mopl/content/controller/ContentCollectionController.java
  • src/main/java/com/codeit/team5/mopl/content/controller/ContentController.java
  • src/main/java/com/codeit/team5/mopl/content/repository/ContentRepository.java
  • src/test/java/com/codeit/team5/mopl/content/batch/processor/SportsDbEventItemProcessorTest.java
  • src/test/java/com/codeit/team5/mopl/content/batch/processor/TmdbMovieItemProcessorTest.java
  • src/test/java/com/codeit/team5/mopl/content/batch/processor/TmdbTvSeriesItemProcessorTest.java
  • src/test/java/com/codeit/team5/mopl/content/batch/writer/ContentItemWriterTest.java
💤 Files with no reviewable changes (3)
  • src/main/java/com/codeit/team5/mopl/content/batch/processor/TmdbMovieItemProcessor.java
  • src/main/java/com/codeit/team5/mopl/content/batch/processor/SportsDbEventItemProcessor.java
  • src/main/java/com/codeit/team5/mopl/content/batch/processor/TmdbTvSeriesItemProcessor.java
🛑 Comments failed to post (3)
src/main/java/com/codeit/team5/mopl/config/SecurityConfig.java (1)

90-90: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 컨트롤러 매핑과 SecurityConfig 화이트리스트 비교: 누락된 공개/보호 경로 탐지
rg -nP '@(Get|Post|Put|Patch|Delete|Request)Mapping' --type=java -g '!**/test/**' -C1
echo '--- SecurityConfig matchers ---'
rg -nP 'requestMatchers|anyRequest' src/main/java/com/codeit/team5/mopl/config/SecurityConfig.java

Repository: sb10-part4-team5/sb10-mopl-team5

Length of output: 170


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate relevant files
printf '== SecurityConfig ==\n'
fd -a 'SecurityConfig.java' .

printf '\n== Controller files ==\n'
fd -a '.java' src/main/java | rg '/controller/|Controller\.java$|/web/'

Repository: sb10-part4-team5/sb10-mopl-team5

Length of output: 3483


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect SecurityConfig and nearby routing definitions
SEC="$(fd -a 'SecurityConfig.java' src/main/java | head -n 1)"
printf 'SecurityConfig: %s\n' "$SEC"
wc -l "$SEC"
cat -n "$SEC" | sed -n '1,220p'

printf '\n== Controllers with mappings ==\n'
rg -n '@(Get|Post|Put|Patch|Delete|Request)Mapping' src/main/java -g '*.java' -C 2

Repository: sb10-part4-team5/sb10-mopl-team5

Length of output: 29373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# List request matcher usages to see whether anyRequest().permitAll() is intentional
rg -n 'requestMatchers|anyRequest|csrf|authorizeHttpRequests' src/main/java -g '*.java' -C 3

Repository: sb10-part4-team5/sb10-mopl-team5

Length of output: 14506


🏁 Script executed:

#!/bin/bash
set -euo pipefail

SEC="$(fd -a 'SecurityConfig.java' src/main/java | head -n 1)"
printf '== %s ==\n' "$SEC"
wc -l "$SEC"
cat -n "$SEC" | sed -n '1,220p'

printf '\n== Controller mappings ==\n'
rg -n '@(Get|Post|Put|Patch|Delete|Request)Mapping' src/main/java -g '*.java' -C 2

printf '\n== Security matchers ==\n'
rg -n 'requestMatchers|anyRequest|csrf|authorizeHttpRequests' src/main/java -g '*.java' -C 3

Repository: sb10-part4-team5/sb10-mopl-team5

Length of output: 43718


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect watching-session endpoints to see whether they are intended to be protected
FILE="$(fd -a 'WatchingSessionController.java' src/main/java | head -n 1)"
printf '== %s ==\n' "$FILE"
cat -n "$FILE" | sed -n '1,220p'

printf '\n== Interface / API contract ==\n'
API="$(fd -a 'WatchingSessionRestApi.java' src/main/java | head -n 1)"
printf '%s\n' "$API"
cat -n "$API" | sed -n '1,220p'

Repository: sb10-part4-team5/sb10-mopl-team5

Length of output: 4975


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Extract all request mappings and compare them with SecurityConfig patterns
python3 - <<'PY'
import re, pathlib

root = pathlib.Path('src/main/java')
patterns = []
security = root / 'com/codeit/team5/mopl/config/SecurityConfig.java'
text = security.read_text()

for line in text.splitlines():
    m = re.search(r'requestMatchers\((.*)\)', line)
    if m:
        patterns.append(line.strip())

print("SECURITY_MATCHER_LINES")
for p in patterns:
    print(p)

print("\nREQUEST_MAPPINGS")
for path in root.rglob('*.java'):
    s = path.read_text()
    if '`@RequestMapping`' in s or '`@GetMapping`' in s or '`@PostMapping`' in s or '`@PatchMapping`' in s or '`@DeleteMapping`' in s:
        for m in re.finditer(r'@(RequestMapping|GetMapping|PostMapping|PatchMapping|DeleteMapping)\s*(?:\(([^)]*)\))?', s):
            annot = m.group(1)
            args = m.group(2) or ''
            if '/api/' in args or '/api' in args:
                print(f"{path}: @{annot}({args})")
PY

Repository: sb10-part4-team5/sb10-mopl-team5

Length of output: 2715


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show whether the watching-session routes are already covered by existing matchers
FILE="$(fd -a 'WatchingSessionController.java' src/main/java | head -n 1)"
API="$(fd -a 'WatchingSessionRestApi.java' src/main/java | head -n 1)"

printf '== %s ==\n' "$FILE"
cat -n "$FILE" | sed -n '1,220p'

printf '\n== %s ==\n' "$API"
cat -n "$API" | sed -n '1,220p'

Repository: sb10-part4-team5/sb10-mopl-team5

Length of output: 4950


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Identify any controller endpoints that are not matched by SecurityConfig and may fall through to permitAll()
python3 - <<'PY'
import re, pathlib, os

sec = pathlib.Path('src/main/java/com/codeit/team5/mopl/config/SecurityConfig.java').read_text()

# collect matcher entries roughly
matchers = []
for line in sec.splitlines():
    line = line.strip()
    if '.requestMatchers(' in line or '.anyRequest()' in line:
        matchers.append(line)

print("MATCHER_LINES")
for m in matchers:
    print(m)

print("\nPOTENTIAL_API_PATHS")
for path in pathlib.Path('src/main/java').rglob('*.java'):
    txt = path.read_text()
    if '/api/' not in txt:
        continue
    if any(a in txt for a in ['`@GetMapping`', '`@PostMapping`', '`@PatchMapping`', '`@DeleteMapping`', '`@RequestMapping`']):
        # print request mapping annotations with /api paths
        for line in txt.splitlines():
            if '`@RequestMapping`' in line or '`@GetMapping`' in line or '`@PostMapping`' in line or '`@PatchMapping`' in line or '`@DeleteMapping`' in line:
                if '/api/' in line or '/api"' in line or "/api'" in line:
                    print(f"{path}: {line.strip()}")
PY

Repository: sb10-part4-team5/sb10-mopl-team5

Length of output: 2427


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="$(fd -a 'WatchingSessionController.java' src/main/java | head -n 1)"
API="$(fd -a 'WatchingSessionRestApi.java' src/main/java | head -n 1)"

printf '== %s ==\n' "$FILE"
cat -n "$FILE" | sed -n '1,220p'

printf '\n== %s ==\n' "$API"
cat -n "$API" | sed -n '1,220p'

Repository: sb10-part4-team5/sb10-mopl-team5

Length of output: 4950


anyRequest().permitAll()은 이 API에서 누락된 경로를 그대로 공개합니다.
현재 명시 규칙에 없는 /api/contents/{contentId}/watching-sessions가 인증 없이 열립니다. 기본값을 authenticated()로 잠그고, 정말 공개가 필요한 경로만 permitAll()로 풀어두는 쪽이 안전합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/codeit/team5/mopl/config/SecurityConfig.java` at line 90,
Open access is currently too broad because
SecurityConfig.anyRequest().permitAll() leaves unspecified API routes public,
including the missing /api/contents/{contentId}/watching-sessions path. Update
the SecurityConfig authorization rules so the default for anyRequest is
authenticated(), and keep only the truly public endpoints explicitly marked with
permitAll(); use the existing SecurityConfig configuration block to locate and
adjust the matcher order.
src/main/java/com/codeit/team5/mopl/content/batch/writer/ContentItemWriter.java (1)

43-57: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '== file list ==\n'
git ls-files | rg 'ContentItemWriter|ContentSource|ContentWithMetaData|ContentRepository|batch/writer|content/repository|content/.+Entity|content/.+java$'

printf '\n== outline: writer ==\n'
ast-grep outline src/main/java/com/codeit/team5/mopl/content/batch/writer/ContentItemWriter.java --view expanded || true

printf '\n== outline: repository/entity candidates ==\n'
for f in $(git ls-files | rg 'ContentRepository|ContentSource|Content.*Entity|Content.*Repository'); do
  echo "--- $f"
  ast-grep outline "$f" --view expanded || true
done

printf '\n== relevant searches ==\n'
rg -n "findExternalIdsBySourceAndExternalIdIn|saveAll\\(|externalId|unique|UNIQUE|`@Column`|`@Table`|`@Index`|`@UniqueConstraint`|source.*externalId|externalId.*source" src/main/java src/test/java

Repository: sb10-part4-team5/sb10-mopl-team5

Length of output: 31122


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '== ContentItemWriter.java ==\n'
cat -n src/main/java/com/codeit/team5/mopl/content/batch/writer/ContentItemWriter.java | sed -n '1,220p'

printf '\n== Content.java ==\n'
cat -n src/main/java/com/codeit/team5/mopl/content/entity/Content.java | sed -n '1,180p'

printf '\n== ContentRepository.java ==\n'
cat -n src/main/java/com/codeit/team5/mopl/content/repository/ContentRepository.java | sed -n '1,120p'

printf '\n== ContentItemWriterTest.java ==\n'
cat -n src/test/java/com/codeit/team5/mopl/content/batch/writer/ContentItemWriterTest.java | sed -n '1,340p'

Repository: sb10-part4-team5/sb10-mopl-team5

Length of output: 28010


청크 내부 externalId 중복도 다시 제거해야 합니다

지금 로직은 DB에 이미 있는 값만 걸러서, 같은 청크 안의 신규 중복은 그대로 saveAll로 들어갑니다. contents(source, external_id) 유니크 제약이 있으니 여기서 충돌하면 청크 전체가 롤백될 수 있습니다.
DB 필터 뒤에 LinkedHashMap으로 한 번 더 묶으면 순서도 유지됩니다. 순서가 중요하지 않다면 distinct()도 가능하지만, 중복 제거 기준이 명확한 LinkedHashMap 쪽이 더 안전합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/codeit/team5/mopl/content/batch/writer/ContentItemWriter.java`
around lines 43 - 57, `ContentItemWriter` currently filters only against IDs
already in the database, so duplicate `externalId` values inside the same chunk
can still reach `saveAll` and violate the `contents(source, external_id)`
uniqueness rule. After the existing `findExternalIdsBySourceAndExternalIdIn`
filter in `write`, add a second de-duplication pass over `deduplicatedItems`
using a stable key-based approach such as `LinkedHashMap` so only the first
occurrence of each `item.content().getExternalId()` is kept while preserving
order. Keep the existing DB-based filtering and the early return/logging path
intact.
src/test/java/com/codeit/team5/mopl/content/batch/writer/ContentItemWriterTest.java (1)

76-129: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

DB 기준 시나리오 테스트는 탄탄합니다.

write_deduplicatesAgainstDb, write_emptyChunk_doesNothing, write_allExistingInDb_skipsAllSaves 모두 새 로직을 잘 검증하고 있습니다. 다만 이전에 있던 "청크 내부 externalId 중복" 테스트가 대체 없이 삭제되었는데, ContentItemWriter.java에 남긴 코멘트처럼 이 시나리오(같은 청크에 동일 externalId 두 건, 둘 다 DB에 없음)에 대한 회귀 테스트를 하나 추가해 두면 향후 재발을 조기에 잡을 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/java/com/codeit/team5/mopl/content/batch/writer/ContentItemWriterTest.java`
around lines 76 - 129, 현재 추가된 DB 기준 테스트들은 좋지만, `ContentItemWriter`의 청크 내부 중복 처리에
대한 회귀 테스트가 빠져 있습니다. `write_deduplicatesAgainstDb`,
`write_emptyChunk_doesNothing`, `write_allExistingInDb_skipsAllSaves`와 같은 스타일로,
같은 청크에 동일한 externalId를 가진 `ContentWithMetaData` 두 개가 들어오고 DB에는 없을 때
`ContentItemWriter.write`가 중복 저장을 막는지 검증하는 테스트를 다시 추가하세요. 이 테스트는
`ContentItemWriter`의 청크 내 deduplication 로직을 직접 검증하도록 구성하면 됩니다.

@plzslp plzslp merged commit a165cd4 into dev Jul 3, 2026
3 checks passed
@plzslp plzslp deleted the refactor/253-content-batch branch July 3, 2026 05:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Refactor] 기존 콘텐츠 수집 배치 처리 리펙토링 [Feat] 콘텐츠 관리 인가 적용

3 participants