# InsuRo 트렌드 인사이트 — Phase 2 (17사이클 미팅 확정)

## 작업 레벨: Lv.4 (한정승인 — 팀장이 전체 일괄 진행)

## 프로젝트
- InsuRo 서버: `/home/jay/projects/InsuRo/server`
- InsuRo 프론트: `/home/jay/projects/InsuRo`

## 필수 참조
- 17사이클 미팅 기록: `/home/jay/workspace/memory/meetings/2026-04-30-keyword-pool-limit-overcome.md`
- 3문서: `/home/jay/workspace/memory/plans/insuro-trend-insight/`
- config: `server/config/trend-insight-config.json`, `server/config/season_calendar.json`
- 래퍼 스크립트: `server/scripts/run_trend_pipeline.sh`

★ 미팅 기록을 반드시 읽고 전체 맥락을 파악한 뒤 작업할 것.

## Phase 1 완료 상태
- DB 5테이블 + 006 마이그레이션(movement 등) + 007(is_pinned) 완료
- keywords 테이블 3,500개 활성
- 크론 2개 (keyword_pool_refresh + run_trend_pipeline)
- 수집 스크립트 5개 + API 4개 + TrendInsightTab UI 완료
- config 2개 (trend-insight-config.json, season_calendar.json) 생성됨
- API 2세트 (Primary + Standby) 등록 예정 (NAVER_API_CLIENT_ID_STANDBY)

## Phase 2 구현 범위 (17사이클 합의 — 10단계 순서)

### Step 1: DB 4테이블 마이그레이션

파일: `server/migrations/008_phase2_tables.sql`

```sql
-- keyword_pool (코어/탐색 풀 관리)
CREATE TABLE IF NOT EXISTS keyword_pool (
  id BIGSERIAL PRIMARY KEY,
  keyword TEXT NOT NULL UNIQUE,
  pool_type TEXT NOT NULL DEFAULT 'core' CHECK (pool_type IN ('core','exploration')),
  monthly_volume INT,
  domain_score NUMERIC(3,2) DEFAULT 0,
  source TEXT DEFAULT 'searchad' CHECK (source IN ('searchad','news','manual','recursive')),
  added_at TIMESTAMPTZ DEFAULT now(),
  last_collected TIMESTAMPTZ,
  is_active BOOLEAN DEFAULT true,
  season_tags TEXT[] DEFAULT '{}',
  meta JSONB DEFAULT '{}'
);
CREATE INDEX IF NOT EXISTS idx_kp_pool_type ON keyword_pool(pool_type) WHERE is_active;

-- news_keyword_candidates (뉴스 키워드 후보 큐)
CREATE TABLE IF NOT EXISTS news_keyword_candidates (
  id BIGSERIAL PRIMARY KEY,
  keyword TEXT NOT NULL,
  source_url TEXT,
  source_title TEXT,
  extracted_at TIMESTAMPTZ DEFAULT now(),
  frequency INT DEFAULT 1,
  domain_score NUMERIC(3,2) DEFAULT 0,
  status TEXT DEFAULT 'pending' CHECK (status IN ('pending','auto_approved','rejected','expired','duplicate')),
  approved_at TIMESTAMPTZ,
  promoted_to TEXT CHECK (promoted_to IN ('core','exploration')),
  expires_at TIMESTAMPTZ,
  meta JSONB DEFAULT '{}'
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_nkc_keyword_active ON news_keyword_candidates(keyword) WHERE status IN ('pending','auto_approved');

-- surge_events (급등 이벤트 로그)
CREATE TABLE IF NOT EXISTS surge_events (
  id BIGSERIAL PRIMARY KEY,
  keyword_id INT REFERENCES keywords(id),
  keyword TEXT NOT NULL,
  detected_at TIMESTAMPTZ DEFAULT now(),
  surge_pct NUMERIC(6,1) NOT NULL,
  baseline_volume INT,
  current_volume INT,
  trigger_type TEXT DEFAULT 'organic' CHECK (trigger_type IN ('organic','news','season','unknown')),
  trigger_detail TEXT,
  notified BOOLEAN DEFAULT false,
  notified_at TIMESTAMPTZ,
  meta JSONB DEFAULT '{}'
);
CREATE INDEX IF NOT EXISTS idx_se_detected ON surge_events(detected_at DESC);

-- keyword_stats_monthly (90일 압축용)
CREATE TABLE IF NOT EXISTS keyword_stats_monthly (
  id BIGSERIAL PRIMARY KEY,
  keyword_id INT,
  keyword TEXT NOT NULL,
  year_month TEXT NOT NULL,
  avg_volume INT,
  max_volume INT,
  min_volume INT,
  data_points INT,
  created_at TIMESTAMPTZ DEFAULT now(),
  UNIQUE(keyword_id, year_month)
);
```

★ Supabase Management API로 실행 (기존 방식과 동일).

### Step 2: 기존 keywords → keyword_pool 초기 마이그레이션

기존 keywords 테이블의 3,500개를 keyword_pool에 복사:
- monthly_search_volume >= 100 → pool_type='core'
- monthly_search_volume < 100 → pool_type='exploration'
- source='searchad'
- domain_score = 카테고리가 '기타'이면 0.5, 나머지 0.8

### Step 3: 급등 감지 로직 강화

파일: `server/scripts/daily_surge_detect.py` (신규)

- 기존 daily_ranking_calc.py의 surge_score를 활용하되, 별도 급등 감지 스크립트
- 기준선: 직전 4주 같은 요일 평균 (keyword_trends에서 계산)
- 임계값: config/trend-insight-config.json의 surge_detection.default_threshold (200%)
- 시즌 기간: season_calendar.json 확인 → surge_detection.season_threshold (300%) 적용
- 급등 감지 시 surge_events 테이블에 INSERT
- Telegram 알림: cokacdir --sendmsg "🔔 키워드 급등 감지\n📌 {keyword} | ▲{surge_pct}%\n원인: {trigger_detail}"
- run_trend_pipeline.sh의 Step 3 이후에 추가

### Step 4: [통합 테스트 게이트]

Step 1~3 완료 후:
- DB 4테이블 존재 확인
- keyword_pool에 3,500개 확인
- daily_surge_detect.py 드라이런 성공
- 기존 크론(run_trend_pipeline.sh) 정상 실행 확인

### Step 5: 뉴스 키워드 파이프라인

파일: `server/scripts/news_keyword_extract.py` (신규)

- 네이버 뉴스 API (GET /v1/search/news.json?query=보험&display=100&sort=date)
- 인증: .env.keys의 NAVER_CLIENT_ID, NAVER_CLIENT_SECRET
- 키워드 추출: 정규식 + 도메인 사전 매칭
  - 도메인 사전: `server/config/insurance_domain_terms.json` (신규, 500개 보험 용어)
  - 패턴: 뉴스 제목에서 도메인 사전 키워드와 매칭
  - 인접어 패턴: "보험|보장|특약" 앞뒤 2글자 이상 명사
- 블랙리스트 필터: keyword_pool_refresh.py의 BLOCKLIST_PATTERNS 재사용
- SearchAd 검증: 월 검색량 50회 이상
- **opt-out 자동 승인**: 도메인 사전 매칭 + SearchAd 검증 통과 → status='auto_approved'
  - 기존 keywords 테이블에 이미 있으면 status='duplicate'
  - 주당 자동 편입 상한 10개
- 승인된 키워드 → keywords 테이블에 INSERT (pool_type='exploration')
- 크론: 매일 08:00, 20:00 (2회)

### Step 6: 시즌 캘린더 부스트 로직

파일: `server/scripts/season_calendar_check.py` (신규)

- season_calendar.json 읽기
- 현재 날짜가 어떤 시즌의 period 안에 있는지 확인
- 해당 시즌의 keywords_pattern과 매칭되는 키워드 → 급등 감지 임계값을 surge_threshold_override로 변경
- 매칭 키워드의 season_tags 업데이트 (keyword_pool)
- run_trend_pipeline.sh의 가장 처음(Step 0)에 실행

### Step 7: 크론 업데이트

run_trend_pipeline.sh에 Step 0(시즌 체크)과 Step 3.5(급등 감지) 추가:

```bash
# Step 0: 시즌 캘린더 체크
log "Step 0: season_calendar_check.py 시작"
python3 scripts/season_calendar_check.py >> "$LOGDIR/season-$(date +%F).log" 2>&1

# ... 기존 Step 1~3 ...

# Step 3.5: 급등 감지
log "Step 3.5: daily_surge_detect.py 시작"
python3 scripts/daily_surge_detect.py >> "$LOGDIR/surge-$(date +%F).log" 2>&1
```

뉴스 크론 추가 (crontab에 1줄):
```
0 8 * * * cd /home/jay/projects/InsuRo/server && source /home/jay/workspace/.env.keys && python3 scripts/news_keyword_extract.py >> /tmp/news-keyword-extract.log 2>&1
```

### Step 8: 대시보드 "오늘 뜨는 키워드" UI

TrendInsightTab.tsx에 "오늘 뜨는 키워드" 섹션 추가:
- surge_events API에서 최근 24시간 급등 키워드 조회
- 최대 5개 표시
- 각 키워드: 급등률, 원인(뉴스 제목 or 시즌 태그), CTA "블로그 주제 추천"
- API: GET /api/insuro/trend-insight/surge-events (신규 엔드포인트)

### Step 9: 90일 데이터 압축

파일: `server/scripts/keyword_data_compress.py` (신규)

- keyword_trends에서 90일 이전 데이터 → keyword_stats_monthly로 롤업
- 월별 평균/최대/최소/데이터포인트 집계
- 원본 삭제 (NOT EXISTS 가드)
- 크론: 매월 1일 04:00 (keyword_pool_refresh 다음)

### Step 10: 통합 검증

- DB 4테이블 + keyword_pool 3,500개
- daily_surge_detect.py 실행 → surge_events 정상
- news_keyword_extract.py 실행 → 뉴스 키워드 추출 + 자동 승인
- season_calendar_check.py 실행 → 시즌 부스트 적용
- run_trend_pipeline.sh 실행 → 전체 파이프라인 정상
- npm run build 성공
- Cloudflare 배포

## affected_files
- `server/migrations/008_phase2_tables.sql` (신규)
- `server/scripts/daily_surge_detect.py` (신규)
- `server/scripts/news_keyword_extract.py` (신규)
- `server/scripts/season_calendar_check.py` (신규)
- `server/scripts/keyword_data_compress.py` (신규)
- `server/config/insurance_domain_terms.json` (신규)
- `server/scripts/run_trend_pipeline.sh` (수정 — Step 0, 3.5 추가)
- `server/main.py` (수정 — surge-events 엔드포인트 추가)
- `src/components/keyword/TrendInsightTab.tsx` (수정 — 급등 섹션 추가)

## 검증 시나리오
1. Supabase에 4테이블 생성 확인
2. keyword_pool에 코어/탐색 분류 확인
3. 인위 급등 데이터 INSERT → surge_events 기록 + Telegram 알림 수신
4. 뉴스 키워드 추출 → news_keyword_candidates에 후보 적재 + 자동 승인
5. 시즌 체크 → 현재 시즌에 해당하는 키워드 부스트 확인
6. 90일 압축 → keyword_stats_monthly 집계 + 원본 삭제
7. npm run build 성공
8. TrendInsightTab에 급등 섹션 표시