Skip to content

Feat/#137 타임라인 - 타임라인 엔티티 AI 요약#138

Merged
kingmingyu merged 8 commits into
developfrom
feat/#137
May 24, 2026
Merged

Feat/#137 타임라인 - 타임라인 엔티티 AI 요약#138
kingmingyu merged 8 commits into
developfrom
feat/#137

Conversation

@kingmingyu
Copy link
Copy Markdown
Collaborator

@kingmingyu kingmingyu commented May 21, 2026

📌 관련 이슈

🚀 개요

이번 PR에서 변경된 핵심 내용을 요약해주세요.

타임라인 엔티티에 해당하는 데이터와 비교기간에 해당하는 데이터를 불러와 AI에게 요약을 요청하는 API 구현

📄 작업 내용

구체적인 작업 내용을 설명해주세요.

  1. 이전 타임라인 CURD 관련 로직 수정사항
    • 타임라인 생성 시 과거 데이터가 없는 경우 뿐만 아니라 현재 데이터가 없는 경우도 생성 시 예외처리(AI요약 & Status 판별 로직을 위해)
    • 타임라인 수정 or 삭제 시에도 MEMBER권한의 유저도 가능하도록 수정(이전 pr에서 까먹고 바로 머지해서 수정했습니다!)
  2. 타임라인 AI 요약하기
    • 타임라인의 분석 기간 성과 데이터를 기반으로 AI 요약문 생성 요청 API 구현
    • 요약 요청 시 해당 기간 광고 데이터 존재 여부 검증 후 호출
    • 트랜잭션 커밋 이후 비동기로 AI 요약 호출, 해당 결과를 확인하기 위해서는 타임라인 상세조회에서 확인

📸 스크린샷 / 테스트 결과 (선택)

결과물 확인을 위한 사진이나 테스트 로그를 첨부해주세요.

  • AI 요약 생성 요청
image image
  • AI 요약 결과 (타임라인 상세 조회 시 조회 가능)
image image

✅ 체크리스트

  • 브랜치 전략(GitHub Flow)을 준수했나요?
  • 메서드 단위로 코드가 잘 쪼개져 있나요?
  • 테스트 통과 확인
  • 서버 실행 확인
  • API 동작 확인

🔍 리뷰 포인트 (Review Points)

리뷰어가 중점적으로 확인했으면 하는 부분을 적어주세요. (P1~P4 적용 가이드)

  • (예: 이 로직이 최선일까요? P2)

  • (예: 예외 처리 누락 여부 확인 부탁드립니다. P1)

  • 기존에 있던 조직 데이터 or 플랫폼별 데이터를 넘겨주고 분석 요청하는 프롬프트와는 결과가 좀 달라야 할 것 같아서 프롬프트는 새로 작성했습니다. 타임라인 특성상 단순 분석만 하면 될 것 같아서 2~3문장 결과 위주 요약정도만 제공하면 될 것 같은데 혹시 추가로 AI한테 더 부탁할 내용이 있을까요?? 지금은 딱히 생각나는게 없어서 분석만 넣었습니다.

  • 마찬가지로 타임라인 요약이 여러개 있을 필요는 없을 것 같고 1개만 존재하면 될 것 같아서 TimeLine 엔티티에서 칼럼으로 관리하는 방식이고 상세 조회 시에 같이 반환되어서 타임라인 AI요청 결과보기 같은 API는 따로 만들지 않았습니다..!

image

💬 리뷰어 가이드 (P-Rules)
P1: 필수 반영 (Critical) - 버그 가능성, 컨벤션 위반. 해결 전 머지 불가.
P2: 적극 권장 (Recommended) - 더 나은 대안 제시. 가급적 반영 권장.
P3: 제안 (Suggestion) - 아이디어 공유. 반영 여부는 드라이버 자율.
P4: 단순 확인/칭찬 (Nit) - 사소한 오타, 칭찬 등 피드백.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added AI-powered timeline summary functionality that automatically generates summaries based on performance data.
    • Added timeline summary request API endpoint.
  • Improvements

    • Enhanced performance data validation when creating and updating timelines.
    • Improved member authorization verification for timeline operations.
  • Documentation

    • Expanded timeline API documentation with new summary endpoint.

릴리스 노트

  • 새로운 기능

    • 성과 데이터를 기반으로 자동으로 요약을 생성하는 AI 기반 타임라인 요약 기능 추가
    • 타임라인 요약 요청 API 엔드포인트 추가
  • 개선 사항

    • 타임라인 생성 및 수정 시 성과 데이터 검증 강화
    • 타임라인 작업 시 멤버 권한 검증 개선
  • 문서화

    • 새로운 요약 엔드포인트가 포함된 타임라인 API 문서 확장

Review Change Stack

@kingmingyu kingmingyu requested review from jinnieusLab and ojy0903 May 21, 2026 03:02
@kingmingyu kingmingyu self-assigned this May 21, 2026
@kingmingyu kingmingyu added the ✨ Feature 새로운 기능 추가 label May 21, 2026
@kingmingyu kingmingyu linked an issue May 21, 2026 that may be closed by this pull request
3 tasks
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 21, 2026

Walkthrough

타임라인 AI 요약 기능이 추가됩니다. 사용자가 요약을 요청하면 트랜잭션 커밋 후 비동기로 OpenAI를 통해 요약을 생성하고 저장합니다. 동시에 타임라인 생성/수정/삭제 시 성과 데이터 검증을 강화하고 권한 검증 로직을 단순화합니다.

Changes

Timeline AI Summary Feature

Layer / File(s) Summary
API Contract and Error Codes
src/main/java/com/whereyouad/WhereYouAd/domains/timeline/domain/service/TimelineService.java, src/main/java/com/whereyouad/WhereYouAd/domains/timeline/exception/code/TimelineErrorCode.java
TimelineService 인터페이스에 requestTimelineSummary 메서드 선언이 추가되고, TimelineErrorCodeTIMELINE_NO_METRIC_DATA, TIMELINE_NO_CURRENT_DATA 두 가지 400 오류 코드가 정의됩니다.
Prompt Generation and OpenAI Integration
src/main/java/com/whereyouad/WhereYouAd/infrastructure/client/openai/prompt/PromptBuilder.java, src/main/java/com/whereyouad/WhereYouAd/infrastructure/client/openai/service/OpenApiService.java
PromptBuilder에 타임라인 시스템/사용자 프롬프트 생성 메서드(buildTimelineSystemPrompt, buildTimelineUserPrompt)와 일별 메트릭 CSV 생성(appendDailyMetrics)이 추가되어 활성 지표와 ROAS(소수점 2자리, RoundingMode.HALF_UP)를 포함합니다. OpenApiService.generateTimelineSummary는 프롬프트 생성 후 OpenAI 호출, Feign 예외 처리, 응답 null/blank 검증을 수행합니다.
Asynchronous Summary Execution
src/main/java/com/whereyouad/WhereYouAd/domains/timeline/domain/service/TimelineAsyncService.java, src/main/java/com/whereyouad/WhereYouAd/domains/timeline/domain/service/TimelineAsyncServiceImpl.java
TimelineAsyncService 인터페이스와 @Async @Transactional`` 구현이 추가되어, 타임라인과 분석/비교 기간의 일별 MetricFact를 조회한 후 OpenAI 요약을 생성해 타임라인에 저장합니다. 예외 발생 시 로그만 기록합니다.
Summary Request Orchestration
src/main/java/com/whereyouad/WhereYouAd/domains/timeline/domain/service/TimelineServiceImpl.java
requestTimelineSummary는 사용자/조직/타임라인 소속 검증 후 메트릭 데이터 존재 여부를 확인하고, TransactionSynchronizationManager.registerSynchronization을 사용해 트랜잭션 커밋 이후에 timelineAsyncService.summarizeAsync를 호출하도록 스케줄링합니다. 의존성 주입과 import도 함께 업데이트됩니다.
REST API Endpoint and Documentation
src/main/java/com/whereyouad/WhereYouAd/domains/timeline/presentation/TimelineController.java, src/main/java/com/whereyouad/WhereYouAd/domains/timeline/presentation/docs/TimelineControllerDocs.java
TimelineControllerPOST /{timelineId}/summary 엔드포인트가 추가되어 202 Accepted로 비동기 처리 시작을 응답합니다. TimelineControllerDocs에는 Swagger 문서와 응답 코드(202/400_3/403_2/404_1)가 정의됩니다.
Validation Improvements
src/main/java/com/whereyouad/WhereYouAd/domains/timeline/domain/service/TimelineServiceImpl.java
createTimelineupdateTimeline에 현재 기간 성과 합계(currentFacts) 존재 검증이 추가되어 데이터 없으면 TIMELINE_NO_CURRENT_DATA 오류를 반환합니다. updateTimelinedeleteTimeline의 권한 검증이 단순화되어 ADMIN 역할 체크를 제거하고 멤버 존재 여부만 확인합니다.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • WhereYouAd/WhereYouAd-Backend#134: 이 PR의 비동기 요약 흐름이 MetricFactRepository 쿼리 메서드와 Timeline 엔티티의 요약 필드 지원에 의존합니다.
  • WhereYouAd/WhereYouAd-Backend#122: 두 PR 모두 TimelineServiceImpl, TimelineController, TimelineErrorCode를 함께 수정하며, 이 PR이 초기 타임라인 스캐폴딩 위에 실제 요약 API를 구현합니다.

Suggested reviewers

  • ojy0903
  • jinnieusLab

🔍 시니어 리뷰어 관점의 주요 체크포인트

✅ 잘 설계된 부분

  1. 트랜잭션 안전성 처리가 탄탄합니다

    • TransactionSynchronizationManager.registerSynchronization으로 afterCommit 시점에 비동기 작업을 스케줄링하는 방식은 매우 신중합니다. 요약 요청이 DB에 커밋되지 않은 상태에서 비동기 작업이 시작되는 것을 방지합니다.
    • @Async@Transactional 모두 적용된 TimelineAsyncServiceImpl.summarizeAsync는 독립 트랜잭션에서 실행되므로 좋은 설계입니다.
  2. 예외 처리가 적절합니다

    • OpenAI 요약 생성 중 예외는 로깅만 하고 재전파하지 않으므로, 요약 실패가 사용자 경험을 직접 방해하지 않습니다.
    • Feign 예외(400/401/429)를 유형별로 분류해 AIHandler로 변환하는 구조도 깔끔합니다.
  3. 프롬프트 생성 로직이 체계적입니다

    • appendDailyMetrics에서 Collectors.groupingBy(LocalDate.class, ...)로 일별 집계하고, ROAS 계산 시 RoundingMode.HALF_UP으로 소수점 2자리를 명시적으로 처리하는 점은 금융 데이터 정확성을 고려한 좋은 설계입니다.
    • 활성 지표만 필터링해 프롬프트에 포함하는 것도 토큰 사용을 절약하는 실용적인 선택입니다.

⚠️ 검토 권장 사항

  1. DB 쿼리 N+1 위험성 확인 필요

    // TimelineAsyncServiceImpl.summarizeAsync에서
    metricFactRepository.findByOrgAndPeriodAndGrain(...) // 분석 기간
    metricFactRepository.findByOrgAndPeriodAndGrain(...) // 비교 기간
    • 이 두 쿼리는 각각 대량의 MetricFact를 로드합니다. 만약 Repository의 구현이 Join Fetch나 Batch Size 최적화 없이 구성되어 있다면 성능 문제가 발생할 수 있습니다.
    • 검토 체크: PR #134에서 추가된 findByOrgAndPeriodAndGrain 메서드가 Lazy Loading이 아닌지, 또는 적절한 Fetch 전략을 가지고 있는지 확인해주세요.
  2. currentFacts 검증 로직의 일관성

    // range_612cfdc7ffc1, range_fc52630636ff
    if (currentFacts.isEmpty() || currentFacts.stream().mapToLong(...).sum() == 0)
    • 빈 리스트와 합계 0을 동시에 체크하는 것은 좋으나, mapToLong 스트림 연산이 두 메서드에서 반복됩니다.
    • 제안: currentFacts의 합계가 0인지 확인하는 헬퍼 메서드를 별도로 추출하면 코드 중복을 줄일 수 있습니다.
  3. 권한 검증 로직 단순화의 의도 확인

    // range_6e2823aacd2d, range_c9d7b3279347
    // 기존: OrgMember 조회 후 ADMIN 역할 체크
    // 변경: 멤버 존재 여부만 확인
    • 역할 체크 제거가 의도적인지 확인이 필요합니다. 만약 모든 조직 멤버가 타임라인을 수정/삭제할 수 있도록 변경한 것이라면 프로덕트 요구사항을 명확히 해주세요.
    • 보안 체크: 이 변경이 의도된 권한 완화인지, 아니면 리팩토링 오버사이트인지 확인이 필요합니다.
  4. OpenAI 응답 검증

    // range_fa0dca601f84
    String content = response.getChoices().get(0).getMessage().getContent();
    if (content == null || content.isBlank()) return "";
    • response.getChoices()가 비어있는 경우에 대한 방어가 없습니다.
    • 개선 제안: response.getChoices().isEmpty() 체크를 먼저 수행하고, 필요하면 명시적인 예외를 던지는 것이 디버깅에 도움됩니다.
  5. 비동기 작업 모니터링

    • @Async 메서드의 예외가 로그만 기록되므로, 프로덕션에서 요약 생성 실패를 감지하기 어려울 수 있습니다.
    • 운영 제안: 요약 생성 실패 시 별도의 모니터링 메트릭(예: 실패 카운트, 평균 응답 시간)을 기록하는 것을 고려해보세요.

💡 코드 설명 (주니어 개발자를 위한)

TransactionSynchronizationManager를 사용할까요?

일반적인 @Async 호출:

timelineAsyncService.summarizeAsync(...); // 즉시 별도 스레드에서 실행

이 방식은 요약 요청 DB 저장이 아직 커밋되지 않은 상태에서 비동기 작업이 시작될 수 있습니다.

개선된 방식 (TransactionSynchronizationManager):

TransactionSynchronizationManager.registerSynchronization(
  new TransactionSynchronization() {
    public void afterCommit() {
      timelineAsyncService.summarizeAsync(...);
    }
  }
);

이 방식은 요약 요청이 DB에 완전히 저장된 후에만 비동기 작업을 시작하므로, 데이터 일관성을 보장합니다.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 주요 기능 변경(타임라인 AI 요약)을 명확하게 설명하며 이슈 번호와 함께 간결하게 표현됨.
Description check ✅ Passed PR 설명은 템플릿 구조를 따르며 개요, 작업 내용, 테스트 결과, 체크리스트, 리뷰 포인트를 포함하고 있음.
Linked Issues check ✅ Passed 모든 연결된 이슈 #137의 요구사항(데이터 쿼리, 시스템 프롬프트, 요약 저장)이 구현되었으며 PR 변경사항이 요구사항을 충족함.
Out of Scope Changes check ✅ Passed 모든 변경사항이 타임라인 AI 요약 기능과 관련된 범위 내에 있으며, 부수적인 권한 개선(MEMBER 권한 수정)과 데이터 검증(현재 데이터 확인)은 기능 구현에 필요한 변경임.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#137

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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/whereyouad/WhereYouAd/domains/timeline/domain/service/TimelineAsyncServiceImpl.java`:
- Around line 32-49: Ensure the timeline belongs to the requested org before
loading metrics: in TimelineAsyncServiceImpl, after retrieving the Timeline via
timelineRepository.findById(timelineId), compare timeline.getOrgId() (or
equivalent org field on Timeline) with the provided orgId and if they differ
throw a TimelineException (use an existing error code or add
TIMELINE_ORG_MISMATCH) to stop processing; only proceed to call
metricFactRepository.findByOrgAndPeriodAndGrain(...) when the org IDs match.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/timeline/domain/service/TimelineServiceImpl.java`:
- Around line 277-281: 현재 사전 검증에서 hasData는
기간(timeline.getStartDate()~getEndDate())과 orgId만 검사하지만 비동기 생성에서는 Grain.DAILY로 다시
조회해 실제 요약 데이터가 없을 수 있으므로 검증 조건을 실제 요약 조회와 동일하게 맞추어야 합니다: 변경할 위치는 hasData를 만드는
식(metricFactRepository.existsByTimeBucketBetweenAndOrg(...))이며, 여기에 요약
grain(timeline.getGrain() 또는 상수 Grain.DAILY) 조건을 추가해 예를 들어
metricFactRepository.existsByTimeBucketBetweenAndOrgAndGrain(start, end, orgId,
timeline.getGrain()) 같은 저장소 메서드를 호출하도록 수정하세요; 저장소에 해당 exists 메서드가 없다면 리포지토리에 존재
여부 메서드를 추가해 동일한 grain 기준으로 검사하도록 구현하세요.
🪄 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.yaml

Review profile: CHILL

Plan: Pro

Run ID: d23ed5e2-9b39-48b5-9f95-1065a921d4fd

📥 Commits

Reviewing files that changed from the base of the PR and between 0b2dc0c and 29f4570.

📒 Files selected for processing (9)
  • src/main/java/com/whereyouad/WhereYouAd/domains/timeline/domain/service/TimelineAsyncService.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/timeline/domain/service/TimelineAsyncServiceImpl.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/timeline/domain/service/TimelineService.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/timeline/domain/service/TimelineServiceImpl.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/timeline/exception/code/TimelineErrorCode.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/timeline/presentation/TimelineController.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/timeline/presentation/docs/TimelineControllerDocs.java
  • src/main/java/com/whereyouad/WhereYouAd/infrastructure/client/openai/prompt/PromptBuilder.java
  • src/main/java/com/whereyouad/WhereYouAd/infrastructure/client/openai/service/OpenApiService.java

Comment on lines +277 to +281
boolean hasData = metricFactRepository.existsByTimeBucketBetweenAndOrg(
timeline.getStartDate().atStartOfDay(),
timeline.getEndDate().plusDays(1).atStartOfDay(),
orgId
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

요약 요청 전 데이터 재검증 조건이 실제 요약 조회 조건과 다릅니다.

여기서는 기간+조직만 확인하고, 비동기 생성에서는 Grain.DAILY 조건으로 다시 조회합니다. 그래서 사전 검증은 통과했는데 실제 요약용 데이터는 비어 있는 케이스가 생길 수 있습니다. 동일한 조건(최소한 동일 grain)으로 맞춰주세요.

수정 방향 예시
- boolean hasData = metricFactRepository.existsByTimeBucketBetweenAndOrg(
-         timeline.getStartDate().atStartOfDay(),
-         timeline.getEndDate().plusDays(1).atStartOfDay(),
-         orgId
- );
+ boolean hasData = !metricFactRepository.findByOrgAndPeriodAndGrain(
+         orgId,
+         timeline.getStartDate().atStartOfDay(),
+         timeline.getEndDate().plusDays(1).atStartOfDay(),
+         Grain.DAILY
+ ).isEmpty();
🤖 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/whereyouad/WhereYouAd/domains/timeline/domain/service/TimelineServiceImpl.java`
around lines 277 - 281, 현재 사전 검증에서 hasData는
기간(timeline.getStartDate()~getEndDate())과 orgId만 검사하지만 비동기 생성에서는 Grain.DAILY로 다시
조회해 실제 요약 데이터가 없을 수 있으므로 검증 조건을 실제 요약 조회와 동일하게 맞추어야 합니다: 변경할 위치는 hasData를 만드는
식(metricFactRepository.existsByTimeBucketBetweenAndOrg(...))이며, 여기에 요약
grain(timeline.getGrain() 또는 상수 Grain.DAILY) 조건을 추가해 예를 들어
metricFactRepository.existsByTimeBucketBetweenAndOrgAndGrain(start, end, orgId,
timeline.getGrain()) 같은 저장소 메서드를 호출하도록 수정하세요; 저장소에 해당 exists 메서드가 없다면 리포지토리에 존재
여부 메서드를 추가해 동일한 grain 기준으로 검사하도록 구현하세요.

Copy link
Copy Markdown
Collaborator

@ojy0903 ojy0903 left a comment

Choose a reason for hiding this comment

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

P4: 고생하셨어요! 일단은 피그마상에서도 타임라인 관련 요약은 간단하게 되어있어서 지금처럼 하는 것도 괜찮아 보입니다. 요약도 비교적 짧은 내용이여서 TimeLine 엔티티 필드로 받는거 좋은 것 같아요! 지금은 이렇게 유지하고, 혹시라도 프론트 측에서 좀 더 길게 요약 제시할 필요가 있다고 하면 그때 프롬프트 쪽만 수정하면 될 거 같습니다!!

Copy link
Copy Markdown
Collaborator

@jinnieusLab jinnieusLab left a comment

Choose a reason for hiding this comment

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

P4: 고생하셨습니다! 저희 예산 수정 기능 완료 되면 해당 타임라인 요약 프롬프트에 예산 수정 시점이 존재하면 해당 시점을 기준으로 유의미한 변화가 있었는지 분석하는 내용도 추후에 추가하면 좋을 것 같아요!

@kingmingyu kingmingyu merged commit b41f262 into develop May 24, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 새로운 기능 추가

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: 타임라인 - 타임라인 엔티티 AI 요약

3 participants