# Copyrighter 운영 연결 상태

기준 포트는 `9500`이다.

## 이번에 연결된 항목

- 9500 API 서버 골격 생성 완료
- 정적 운영자 콘솔을 9500 서버에서 함께 제공
- `web/operator-gui/app.js`가 `/api/bootstrap` API 응답으로 초기 데이터를 갱신하도록 변경
- SQLite 저장소 연결 완료
- 로컬 제출 이미지 폴더 연결 완료
- 기본 헬스체크 추가
- `.env` 또는 프로세스 환경변수 기반 외부 API/로컬 LLM 설정 로딩
- Naver 이미지 검색 API 연결: 수동 검색에서 텍스트 쿼리만 전송
- Google Cloud Vision Web Detection 연결: 신규 로컬 제출 분석 시 파생 이미지 전송
- 내부 Ollama 연결: 재분석 시 기존 근거 기반 LLM 요약 생성
- 증거 상태 기록: 판단에 사용, 무관, 오탐, 보류를 증거별로 저장
- 보류/반려 판정 기반 주의 후보 자동 생성
- 주의 후보 이미지 유사도 매칭, 확정 DB 편입, 오탐 제외 흐름 추가

## 실행 방법

`.env.example`을 참고해 루트에 `.env`를 만들고 필요한 키만 채운다. 키가 없는 외부 API provider는 자동으로 disabled 상태가 된다. Ollama LLM은 로컬 기본값으로 켜진다.

```text
NAVER_CLIENT_ID=
NAVER_CLIENT_SECRET=
GOOGLE_CLOUD_VISION_API_KEY=
COPYRIGHTER_GOOGLE_FACE_CROP_SEARCH=false
OLLAMA_BASE_URL=http://localhost:11434
OLLAMA_MODEL=qwen2.5:0.5b-instruct
```

```powershell
cd C:\Users\USER\Desktop\complete\copyrighter
python run_copyrighter_server.py
```

브라우저:

```text
http://127.0.0.1:9500/
```

헬스체크:

```text
http://127.0.0.1:9500/health
http://127.0.0.1:9500/api/providers/health
```

기본 저장 위치:

```text
data/copyrighter.sqlite3
data/submissions/submissions.json
data/submissions/images/
```

## env 키와 연결 동작

| env | 용도 | 연결되는 흐름 |
| --- | --- | --- |
| `NAVER_CLIENT_ID` | Naver Open API 클라이언트 ID | `POST /api/search/manual`에서 provider가 `naver`일 때 사용 |
| `NAVER_CLIENT_SECRET` | Naver Open API 클라이언트 Secret | Naver 요청 헤더에 사용 |
| `NAVER_SEARCH_DISPLAY` | 검색 결과 개수, 기본 `10` | Naver 이미지 검색 쿼리 |
| `NAVER_SEARCH_PAGES` | 이미지 검색 페이지 수, 기본 `1`, 최대 `10` | `display` 단위로 다음 결과 페이지까지 가져온다. 페이지 수만큼 API 호출량이 늘어난다. |
| `NAVER_SEARCH_SORT` | 정렬, 기본 `sim` | Naver 이미지 검색 쿼리 |
| `NAVER_BLOG_SEARCH_DISPLAY` | 블로그 검색 결과 개수, 기본 `3` | 이미지 검색이 직접 매칭을 만들지 못할 때 원문 페이지 대표 이미지를 찾는 보조 검색 |
| `NAVER_BLOG_SEARCH_PAGES` | 블로그 검색 페이지 수, 기본 `1`, 최대 `10` | 블로그 보조 검색의 다음 결과 페이지까지 가져온다. 페이지 수만큼 API 호출량이 늘어난다. |
| `NAVER_BLOG_SEARCH_SORT` | 블로그 정렬, 기본 `sim` | Naver 블로그 검색 쿼리 |
| `NAVER_WEB_SEARCH_DISPLAY` | 웹문서 검색 결과 개수, 기본 `3` | 이미지/블로그 검색이 매칭 이미지를 만들지 못할 때 일반 웹문서 페이지 대표 이미지를 찾는 보조 검색 |
| `NAVER_WEB_SEARCH_PAGES` | 웹문서 검색 페이지 수, 기본 `1`, 최대 `10` | 웹문서 보조 검색의 다음 결과 페이지까지 가져온다. 페이지 수만큼 API 호출량이 늘어난다. |
| `GOOGLE_CLOUD_VISION_API_KEY` | Cloud Vision REST API 키 | 신규 제출 seed 분석에서 Web Detection 사용 |
| `GOOGLE_CLOUD_VISION_PARENT` | 선택 project/location parent | Cloud Vision 요청 body에 선택적으로 포함 |
| `COPYRIGHTER_GOOGLE_FACE_CROP_SEARCH` | 얼굴 영역 Google Web Detection 사용 여부, 기본 `false` | 재분석 시 감지된 얼굴 영역 crop만 별도 파생 이미지로 보내 웹 근거를 수집한다. 동일인 판정이나 얼굴 인식 점수로 쓰지 않는다. |
| `GOOGLE_CUSTOM_SEARCH_IMAGE_RESULTS` | Google 이미지 검색 1페이지 결과 개수, 기본 `3` | Google Custom Search 이미지 검색 쿼리 |
| `GOOGLE_CUSTOM_SEARCH_IMAGE_PAGES` | Google 이미지 검색 페이지 수, 기본 `1`, 최대 `10` | `num` 단위로 다음 결과 페이지까지 가져온다. 페이지 수만큼 API 호출량이 늘어난다. |
| `GOOGLE_CUSTOM_SEARCH_WEB_RESULTS` | Google 웹 검색 1페이지 결과 개수, 기본 `3` | 이미지 검색이 직접 매칭을 만들지 못할 때 웹 검색 결과 페이지의 대표 이미지를 찾는다. |
| `GOOGLE_CUSTOM_SEARCH_WEB_PAGES` | Google 웹 검색 페이지 수, 기본 `1`, 최대 `10` | 웹 검색의 다음 결과 페이지까지 가져온다. 페이지 수만큼 API 호출량이 늘어난다. |
| `COPYRIGHTER_AUTO_NAVER_QUERY_LIMIT` | Google 근거에서 자동 생성해 실행할 Naver 쿼리 수, 기본 `3`, 최대 `10` | Google 페이지 제목과 엔티티를 우선순위로 정렬해 여러 텍스트 이미지 검색을 자동 실행한다. |
| `COPYRIGHTER_AUTO_NAVER_BLOG_QUERY_LIMIT` | 이미지 검색 매칭이 없을 때 추가 실행할 Naver 블로그 쿼리 수, 기본 `1`, 최대 `10` | 블로그 검색 결과 페이지의 대표 이미지를 추출해 제출 이미지와 지문 비교한다. |
| `COPYRIGHTER_AUTO_NAVER_WEB_QUERY_LIMIT` | 이미지/블로그 검색 매칭이 없을 때 추가 실행할 Naver 웹문서 쿼리 수, 기본 `1`, 최대 `10` | 웹문서 검색 결과 페이지의 대표 이미지를 추출해 제출 이미지와 지문 비교한다. |
| `COPYRIGHTER_SEARCH_RESULT_COMPARE_LIMIT` | 검색 결과 이미지 URL을 내려받아 제출 이미지와 지문 비교할 건수, 기본 `3`, 최대 `20` | Naver/Google 결과 이미지를 로컬 저장소에 저장한 뒤 pHash로 제출 이미지와 직접 비교한다. |
| `COPYRIGHTER_SEARCH_RESULT_PAGE_IMAGE_LIMIT` | 검색 결과 원문 페이지에서 추출할 대표 이미지 수, 기본 `3`, 최대 `10` | 결과 자체에 이미지 URL이 없으면 `og:image`, `twitter:image`, `img` 후보를 제한된 수만 내려받아 제출 이미지와 비교한다. |
| `COPYRIGHTER_SEARCH_RESULT_SIMILARITY_THRESHOLD` | 검색 결과 이미지 유사도 필터 임계치, 기본 `0.9`, 범위 `0.0`~`1.0` | Naver/Google 결과 후보 이미지의 pHash 유사도 기반 매칭 임계치를 조절한다. |
| `OLLAMA_BASE_URL` | Ollama 로컬 서버 URL, 기본 `http://localhost:11434` | `POST /api/submissions/{id}/rerun-enrichment`에서 LLM 요약 사용 |
| `OLLAMA_MODEL` | Ollama 모델, 기본 `qwen2.5:0.5b-instruct` | Ollama `/api/generate` payload의 `model` |
| `COPYRIGHTER_NAVER_DAILY_LIMIT` | Naver 일일 호출 제한 | 로컬 policy gate |
| `COPYRIGHTER_GOOGLE_DAILY_LIMIT` | Google 일일 호출 제한 | 로컬 policy gate |
| `COPYRIGHTER_LLM_DAILY_LIMIT` | LLM 일일 호출 제한 | provider 상태 표시용 |

환경변수는 `.env`보다 우선한다. 예를 들어 PowerShell에서 이미 `$env:OLLAMA_MODEL`이 있으면 `.env` 값으로 덮어쓰지 않는다.

## 공식 문서 기준

- Naver 이미지 검색 API: https://developers.naver.com/docs/serviceapi/search/image/image.md
- Google Cloud Vision Web Detection: https://cloud.google.com/vision/docs/detecting-web
- Ollama Generate API: https://docs.ollama.com/api/generate
- qwen2.5 0.5B Instruct 모델: https://ollama.com/library/qwen2.5:0.5b-instruct

## 현재 API

```text
GET  /health
GET  /api/providers/health
GET  /api/bootstrap
GET  /api/review-queue
GET  /api/submissions/{submission_id}/review
POST /api/submissions/reload
POST /api/submissions/{submission_id}/rerun-enrichment
POST /api/submissions/{submission_id}/decision
POST /api/evidence/{evidence_id}/status
POST /api/knowledge/{entry_id}/promote-watchlist
POST /api/knowledge/{entry_id}/exclude-watchlist
POST /api/search/manual
GET  /api/providers
PATCH /api/providers/{provider_id}
POST /api/providers/emergency-disable
GET  /api/audit-events
GET  /media/{image_path}
```

## 로컬 이미지 저장 방식

제출 이미지는 두 방식으로 넣을 수 있다.

- 빠른 방식: 이미지 파일을 `data/submissions/images/` 아래에 복사한 뒤 화면에서 `새 제출 불러오기`를 누른다. 이 경우 제출 ID와 제목은 파일명 기준으로 자동 생성된다.
- 명시 방식: `data/submissions/submissions.json`에 ID, 제목, 크기, 제출 시간을 직접 등록한 뒤 화면에서 `새 제출 불러오기`를 누른다.

예시:

```json
[
  {
    "id": "SUB-LOCAL-001",
    "title": "로컬 얼굴 이미지 샘플",
    "file": "images/local-face.svg",
    "width": 1200,
    "height": 900,
    "submitted_at": "2026-05-26 10:00"
  }
]
```

이미지 파일은 `data/submissions/images/` 아래에 둔다. 서버는 이 폴더 밖으로 나가는 경로를 거부한다.

서버를 재시작하지 않아도 된다. 운영 콘솔의 심사 큐 상단에서 `새 제출 불러오기`를 누르면 `submissions.json` 변경분과 폴더에 새로 복사한 이미지가 SQLite DB로 import된다.

## 운영자 판정 흐름

1. 제출 이미지를 선택하고 상단의 `선택 재분석` 또는 행 안의 증거를 확인한다.
2. 증거 행에서 `판단에 사용`, `무관`, `오탐`, `보류`를 표시한다. 이 표시는 케이스 기록과 점수 반영 여부를 정리할 뿐, 기준 DB 후보를 만들지는 않는다.
3. 케이스 판정을 먼저 내린다. `승인`은 자동 후보를 만들지 않는다. `보류`와 `반려`는 제출 이미지 지문과 선택된 근거를 묶어 `주의 후보`를 만든다.
4. 이후 같은 이미지가 들어오면 `주의 후보 근거` 그룹에 별도로 표시되고 높은 위험 신호로 반영된다. 그래도 최종 판정은 자동 변경되지 않는다.
5. 지식 DB 화면에서 주의 후보를 검토한 뒤 `확정 DB 편입` 또는 `오탐 제외`를 누른다. 제외된 후보는 다음 내부 유사도 분석에서 사용하지 않는다.

## 외부 API 연결 상태

연결 완료:

- Naver: `https://openapi.naver.com/v1/search/image`에 `GET` 요청을 보낸다. `query`, `display`, `start`, `sort`를 쿼리스트링으로 보내고 `X-Naver-Client-Id`, `X-Naver-Client-Secret` 헤더를 사용한다.
- Google: `https://vision.googleapis.com/v1/images:annotate`에 `WEB_DETECTION` 요청을 보낸다. 서버는 내부 파생 이미지 bytes를 base64로 인코딩해 전송한다.
- Provider 상태: 외부 API는 키가 있으면 enabled, 없으면 disabled와 missing env 사유를 `/api/providers`에 표시한다.
- Provider Controls: 화면의 provider 활성/비활성 상태는 DB에 저장된다.

아직 운영 검증이 필요한 것:

- 실제 운영 키로 Naver/Google 샘플 호출 품질 확인
- 공급자별 재시도/backoff 정책
- 일일 한도 초과 시 관리자 알림
- API 키를 Windows 서비스/작업 스케줄러/배포 환경의 시크릿 저장소로 옮기는 운영 방식

운영 경계:

- Naver에는 텍스트 쿼리만 보낸다.
- Naver로 원본 이미지나 파생 이미지를 보내지 않는다.
- Google에는 승인된 파생 이미지만 보낸다.
- Google Cloud Vision 사용 전 계약, DPA, 데이터 보존, 메타데이터 로깅, 리전, 자격 증명 정책을 확인한다.

## LLM 연결 상태

연결 완료:

- Ollama 로컬 API에 `POST /api/generate` 요청을 보낸다.
- 기본 URL은 `http://localhost:11434`, 기본 모델은 `qwen2.5:0.5b-instruct`다.
- Ollama 요청은 API 키를 쓰지 않는다.
- 응답 스트리밍은 끄고 `stream: false`로 한 번에 요약을 받는다.
- `rerun-enrichment`는 이미 저장된 내부/Naver/Google 근거만 LLM 입력으로 넘긴다.
- LLM 요약은 `source_evidence_ids` 또는 출처 URL을 가진 보조 근거로만 저장한다.
- LLM 실패는 기존 근거를 낮추지 않고 failure evidence로 남긴다.

설치 명령:

```powershell
ollama pull qwen2.5:0.5b-instruct
```

아직 운영 검증이 필요한 것:

- 실제 운영 PC에서 `ollama pull qwen2.5:0.5b-instruct` 후 샘플 재분석 호출 품질 확인
- 프롬프트/응답 로그 저장 여부와 마스킹 정책 확정
- LLM 요약을 신청자에게 노출하지 않는 정책 재검증

LLM이 해도 되는 일:

- 검색 쿼리 후보 생성
- 증거 요약
- 중복/상충 증거 정리
- 운영자가 읽기 쉬운 근거 메모 생성

LLM이 하면 안 되는 일:

- 최종 승인/보류/반려 결정
- 단독 점수 산정
- 출처 없는 유명인/작품/IP 단정
- 신청자에게 보여줄 자동 설명 생성

## 아직 안 된 것: 운영용 인증과 배포

현재 9500 서버는 로컬 실행용이다. 실운영 전에 아래가 필요하다.

- 로그인/세션 또는 사내 SSO
- 일반 운영자와 관리자 권한 분리
- Provider Controls 관리자 전용 접근
- 운영자 결정/보정/지식 DB 변경 감사 로그 강화
- 9500 서버 프로세스 관리 방식 확정
- 백업, 복구, 모니터링, 알림
## Google Custom Search 설정

| env | 용도 | 연결되는 흐름 |
| --- | --- | --- |
| `GOOGLE_CUSTOM_SEARCH_API_KEY` | Google Programmable Search JSON API 키 | Google Vision 텍스트 단서를 Google 이미지/웹 검색으로 확장 |
| `GOOGLE_CUSTOM_SEARCH_CX` | Programmable Search Engine ID | Custom Search JSON API `cx` 값 |
| `GOOGLE_CUSTOM_SEARCH_IMAGE_RESULTS` | 이미지 검색 결과 개수, 기본 `3` | 텍스트 쿼리 기반 이미지 결과를 내려받아 제출 이미지와 pHash 비교 |
| `GOOGLE_CUSTOM_SEARCH_WEB_RESULTS` | 웹 검색 결과 개수, 기본 `3` | 이미지 검색이 비었을 때 웹 결과 페이지 대표 이미지를 비교 |
| `COPYRIGHTER_AUTO_GOOGLE_CUSTOM_QUERY_LIMIT` | 자동 Google Custom Search 쿼리 수, 기본 `2`, 최대 `10` | 제출 이미지를 보내지 않고 텍스트 쿼리만 전송 |
| `COPYRIGHTER_GOOGLE_CUSTOM_SEARCH_DAILY_LIMIT` | Google Custom Search 일일 호출 제한 | 로컬 policy gate |

자동 검색 쿼리 원천:

- Google 페이지 제목과 엔티티가 가장 높은 우선순위다.
- Google best guess label만 있는 경우에도 `person`, `gentleman`, `portrait` 같은 일반어는 버리고, `IU official profile`처럼 구체적인 문구만 낮은 우선순위 쿼리로 사용한다.
- 얼굴 영역 Google Web Detection은 동일 인물 판정 근거로 쓰지 않는다. 다만 페이지 제목이나 엔티티가 구체적이면 낮은 우선순위의 텍스트 검색 쿼리로만 재사용한다.
- 페이지 제목이 영어권 문구면 보조 쿼리에 `image`를 붙이고, 한국어 문구면 `이미지`를 붙인다. 검색엔진에 깨진 한글 템플릿을 보내지 않는다.
- Google Custom Search와 Naver에는 제출 이미지를 보내지 않고 텍스트 쿼리만 보낸 뒤, 검색 결과 이미지나 원문 페이지 대표 이미지를 내려받아 로컬에서 pHash 비교한다.

설정 진단:

- `/api/providers/health`와 공급자 화면은 `requiredEnv`, `configuredEnv`를 비밀값 없이 표시한다.
- `google_search`가 disabled이면 우선 `GOOGLE_CUSTOM_SEARCH_API_KEY`, `GOOGLE_CUSTOM_SEARCH_CX`가 둘 다 `.env`에 있는지 확인한다.
