# task-3044 — InfoKeyword 리포트: 공개 노출 차단 + 정리 스크립트 GCS 전환

**레벨**: Lv.2 · **팀**: dev2-team · **회장 승인 완료** (2026-08-28)
**저장소**: `Jeon-Jonghyuk/InfoKeyword` (`/home/jay/projects/InfoKeyword`, 기본 브랜치 **master**)

## ★ 회장 지시
> "로그인 된 사람만 볼 수 있어야 하는게 맞으니까. ok.
>  그런데 **2팀봇이 매일 새벽에 진행하는 정리 작업도 제대로 될 수 있도록** 적용해"

---

## ANU 가 이미 조치한 것 (재작업 금지 — 현재 상태로 전제하라)

```
GCS 버킷 infokeyword-j2h-reports
  · 7일 초과분 546 blob(332.0MB) 삭제 완료 → 남은 것 95 blob(60.9MB)
  · lifecycle 규칙 적용:
      {"action":{"type":"Delete"}, "condition":{"age":7, "matchesPrefix":["reports/"]}}
  · 기존 95 blob 의 allUsers ACL 전량 회수 → 캐시우회 재검증 5/5 HTTP 403
  · UBLA = False (개별 blob ACL 방식)
```
**이미 있는 파일은 막혔다. 그러나 새로 올라오는 파일은 다시 공개된다.**

---

## ① `make_public()` 제거 — 로그인 사용자만 열람 (회장 지시)

```python
# worker/reporter/drive_uploader.py:51
blob.make_public()
public_url = blob.public_url      # → 누구나 열람 가능
```
이 두 줄 때문에 업로드할 때마다 인터넷 전체에 공개된다.
★ ANU 실측: 지우기 전 표본 40건이 **전부 무인증 HTTP 200** 이었고,
파일명에 분석 대상 블로그 URL 이 그대로 들어가 있어
**어느 설계사가 어떤 블로그를 분석했는지가 URL 만으로 드러났다.**

### 바꿀 것
- `make_public()` 을 제거한다
- 공개 URL 대신 **만료 있는 서명 URL(signed URL)** 을 발급하도록 바꾼다.
  ★ 만료 시간을 정하고 **근거를 남겨라**. 리포트 열람 특성상 몇 시간~며칠 중 판단
- ★ 반환 계약이 바뀐다. `report_url` 소비처를 **전수 확인**하고 깨지지 않게 하라:
  ```
  src/app/report/[id]/evidence/page.tsx   <img src> · <a href> 로 직접 사용
  src/app/report/[id]/page.tsx            버튼 표시 조건
  src/types/index.ts                      report_url?: string
  ```
- ★ **InsuRo 는 이 URL 을 쓰지 않는다**(ANU 확인: `main.py:2766` 에서 `report_url` 을
  의도적으로 제거해 저장, DB 4행 중 GCS URL 포함 0행). InsuRo 걱정은 하지 마라
- 서명 URL 이 부적절하다고 판단되면 **인증 프록시 라우트**(로그인 검증 후 스트림)도 대안이다.
  둘 중 무엇을 택했는지 **근거를 보고서에 적어라**

### ★ 주의 — 서명 URL 은 DB 에 영구 저장하면 안 된다
만료되면 죽은 링크가 된다. **blob 경로를 저장하고 열람 시점에 발급**하는 구조가 맞다.
지금 `report_url` 이 결과 JSON 에 박혀 저장되고 있는지 확인하고, 그렇다면 설계를 바꿔라.

---

## ② 정리 스크립트를 GCS 대상으로 전환 (회장 지시)

### 현재 — 구조적 no-op
```python
# scripts/cleanup_drive.py:45
ROOT_FOLDER_NAME = "InfoKeyword-Reports"     # Google Drive 폴더
SCOPES = ['https://www.googleapis.com/auth/drive']
```
그런데 실제 저장은 **GCS**다(`drive_uploader.py` 는 이름만 Drive, 내용은 `google.cloud.storage`).
**Drive 폴더는 비어 있어 매일 "0개 삭제, 성공"으로 끝난다.**
2일 연속 동일했고 6개월간 정리 0회였다.

### 바꿀 것
스크립트가 **GCS 를 대상으로** 동작하게 한다. 다만 **lifecycle 규칙과 역할을 나눠라.**

★ **lifecycle 이 주 삭제 수단이다.** 스크립트가 같은 일을 중복으로 하면 안 된다.
스크립트의 역할은 **점검 + 보완**으로 잡아라:
```
1) 7일 초과 blob 이 남아 있으면 삭제      (lifecycle 지연·누락 보완)
2) allUsers ACL 이 붙은 blob 이 있으면 회수 (make_public 재발 방지 안전망)
3) 현황 보고 — 총 blob 수 / 용량 / 최고령 / 조치 건수
```
- ★ **0건일 때 "성공"이라고만 말하지 마라.** 왜 0건인지(정상 상태인지, 대상을 못 찾은 건지)가
  드러나야 한다. 지금 실패의 본질이 **"0건인데 성공으로 보고"** 였다
- 대상을 하나도 못 찾으면(버킷 접근 실패 등) **명시적으로 오류**로 처리하라
- `--dry-run` 은 유지하라

### 자격증명
```
/home/jay/.config/gcloud/service-accounts/infokeyword-j2h-drive.json
```
★ 프로젝트 `.env`/`.env.local` 에는 경로가 **없다**. `.env.keys`(systemd EnvironmentFile)에도 없다.
ANU 가 시스템에서 찾았다. 스크립트가 이 경로를 어떻게 얻을지 정하고 **하드코딩하지 마라**
(환경변수 + 기본값 형태 권장). **자격증명 값·내용을 로그·커밋에 남기지 마라.**

---

## ③ 스케줄 프롬프트 갱신
```
id 57DC9232 · 0 2 * * * · dev2 소유
현재 prompt: "InfoKeyword 스크린샷 리포트 자동 정리 실행.
             python3 .../scripts/cleanup_drive.py --days 7 실행하고 결과를 간략히 보고하라."
```
- 스크립트 이름·인자가 바뀌면 **스케줄 프롬프트도 함께 갱신**해야 한다
- ★ 스케줄 갱신은 **ANU 가 한다.** 너는 **새 프롬프트 문안만 보고서에 제안**하라
  (`--cron-update` 는 실행하지 마라)

---

## ★ 하지 말 것
- **lifecycle 규칙 변경·삭제 금지** (ANU 가 적용함)
- **GCS blob 대량 삭제 금지** — 스크립트 작성까지가 범위. 실행은 `--dry-run` 으로만
- InsuRo 저장소 변경 금지
- 자격증명 값을 저장소에 커밋
- `report_url` 소비처를 확인 없이 깨뜨리기

## allowed_resources
```yaml
allowed_resources:
  paths:
    - "scripts/cleanup_drive.py"
    - "scripts/cleanup_gcs_reports.py"
    - "worker/reporter/drive_uploader.py"
    - "worker/config.py"
    - "src/app/report/**"
    - "src/types/index.ts"
    - "tests/**"
    - "memory/reports/task-3044.md"
  forbidden_paths:
    - "worker/crawler/**"
    - "worker/analyzer/**"
    - "nextapp/**"
  commands: ["python3", "pytest", "npm", "npx", "bash", "gh"]
  merge_policy: "tiered"
  ttl_hours: 24
```

## 검증 (실측 강제)
1. **업로드 후 무인증 접근 차단 실증** — 실제로 파일을 하나 올리고, 인증 없이 URL 을 열어
   **403 이 나오는지** 확인하라. 올린 테스트 파일은 정리하라
2. **로그인 사용자는 열람 가능** — 서명 URL(또는 프록시)로 실제로 열리는지 확인
3. **`report_url` 소비처 무손상** — evidence 화면이 깨지지 않는지 확인(빌드 + 코드 경로)
4. **정리 스크립트 `--dry-run`** — GCS 를 실제로 조회해 대상 건수를 보고하라.
   현재 상태 기준 예상: 7일 초과 0건(방금 정리함) · allUsers ACL 0건
   ★ **0건이 나오는 게 정상이다. 그 사실이 보고에 드러나는지**가 이 검증의 핵심
5. **실패 경로** — 자격증명이 없거나 버킷 접근이 안 될 때 **오류로 종료**하는지.
   조용히 "0건 성공"이 되면 FAIL
6. **회귀** — `pytest` **base 재측정 기준선**
7. **봉인** — `make_public()` 을 되살리는 변이, 0건을 무조건 성공으로 처리하는 변이를 만들어
   테스트가 FAIL 하는지 확인·복원. 변이가 no-op 이 아님을 `assert` 로 먼저 증명하라

## 완료 조건 (DoD)
**새로 업로드된 리포트가 무인증으로 열리지 않고 로그인 사용자는 볼 수 있으며,
정리 스크립트가 GCS 를 실제로 조회해 조치 건수와 사유를 보고하고,
대상을 못 찾는 상황을 성공으로 위장하지 않는다.**

## ★ 세션 조기 종료 대책
시작 즉시 브랜치 + 드래프트 PR 을 먼저 열어라. **단계마다 커밋하라.**
★ ANU 명세 전제가 틀렸다고 판단되면 구현하지 말고 **반증을 먼저 기록한 뒤 보고하라.**

## 운영 계약
- `origin/master` 기준 `git pull --ff-only` 후 시작
- ★ **worktree 를 써라.** 메인 저장소는 워커 서비스(`infokeyword-worker.service`)가 실행 중이다
- ★ `gh` 호출 시 `GH_TOKEN="$BOT_GITHUB_TOKEN"` 주입 필수 (회장 개인 PAT 금지 — 감사기록 오염)
- 워크플로우 `/home/jay/workspace/prompts/DIRECT-WORKFLOW.md` · QC `/home/jay/workspace/teams/shared/QC-RULES.md`
- `WORKSPACE_ROOT=/home/jay/workspace` · `CHAT_ID=6937032012` · 수집자 key `ANU_KEY=c119085addb0f8b7`
- 완료 경로는 `finish-task.sh` 실행이 유일하다. 수동 `.done` 금지.

## 보고
**PR 생성까지가 범위다. 머지는 ANU 가 한다. 자동 머지 금지. 스케줄 갱신도 ANU 가 한다.**
`memory/reports/task-3044.md` 작성 후 표준 완료 콜백 등록. 콜백 프롬프트 **UTF-8 3900 bytes 이하**.
★ 봉투 첫 줄에 **"무인증 403 실증 여부 + 0건을 성공으로 위장하지 않는지"** 를 담아라.