o2o-plagiarism-ai/docs/API_SPEC_BAIKAL.md
hbyang 52a0fdcdf0 feat: expand infringement case matching to v1.3 precedent mapping
태그 동점 시 첫 케이스만 반환해 나머지를 버리던 문제를 고친다.
A1·A2·A3·A4·A5·A6·A15 는 주/보조 태그가 동일하고 실제 구별자는 원본의
종류라 태그로 좁혀지지 않는다. find_case -> find_cases 로 바꿔 동점군을
전부 내보내고, 확정은 사람이 원본 종류로 한다.

- cases_v1.3.json 신설(v1.2 삭제): 39건 전부에 대표판례·처리구분(●/○) 적재.
  판례 미지정 10건은 사유를 값으로 남긴다.
- detectable_internal 10 -> 19건 (v1.3 IX장 총괄표 ● 기준으로 정정)
- _assign_tags 주 태그 조합 3 -> 5종. 유사도로 판단 가능한 쟁점만 주 태그로
  낸다. 도달 케이스 3 -> 16건(● 19건 중).
- B3·C1·D1 은 게재 사실·편집 개입·성명표시가 필요해 텍스트로 알 수 없다.
  추측하지 않고 LegalContext 대기로 두며 경계를 테스트로 고정한다.
- 케이스 수 검증을 38~39 범위에서 39 로 고정. v1.3 본문 통계 줄(●16/○22=38)이
  같은 문서의 표(●19/○20=39)와 어긋난 것이 38/39 혼재의 원인이었다.
- docs/PRECEDENT_SOURCE_GAP.md: 대표판례이나 적재본에 없어 출처를 제시할 수
  없는 14건. 응답에서도 precedents_without_source 로 구분한다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 15:42:34 +09:00

11 KiB
Raw Permalink Blame History

저작권 탐지 API 양식서 (오투오 → 바이칼)

나누구 앱 저작권 탭 연동용 API 계약. 본문 입력 → 표절 탐지 결과 + 저작권 탭 표시 항목을 반환한다. 본 문서 기준으로 UI를 연동하면 된다. 엔진: o2o-plagiarism-api


0. 기본 정보

항목
Base URL (개발/통합 테스트) https://plagiarism.o2o.kr
프로토콜 HTTPS, Content-Type: application/json (UTF-8)
인증 운영 설정 시 X-API-Key 헤더 사용
Swagger(자동 명세) {BASE}/docs — 실시간 스키마 확인 가능
실서비스 배포 개인정보(자서전) 보호를 위해 Docker 온프레미스 납품 예정 (실사용자 본문은 오투오 서버로 전송 안 함)

1. 저작권 탭 화면 ↔ API 필드 매핑 (핵심)

나누구 저작권 탭의 각 표시 항목은 경량 응답에서 그대로 읽으면 된다. 별도 계산 불필요 — 독창성 환산과 한글 라벨까지 서버가 완료해 제공한다.

화면 표시 예시 API 필드
독창성 98% 98 copyright.originality_percent
저작권 · 유사도 2% 2 copyright.similarity_percent
유사 문장 0건 0건 similar_sentences.label
대조 결과 설명 문장 copyright.description
표절 의심 구간 없음 false copyright.has_suspicion
AI 생성 의심도 낮음 낮음 ai_generation_suspicion.label
판례 판단 침해 의심 낮음 legal_judgment.label
판례 근거 사건번호 포함 문장 legal_judgment.summary

ai_generation_suspicion.level 값은 low, medium, high, unknown이며, 화면은 함께 반환되는 한글 label을 그대로 표시한다.


2. 저작권 탭 경량 검사 — POST /v1/plagiarism/review

모바일 저작권 탭은 이 API를 사용한다. 내부 상세 탐지 결과 중 화면에 필요한 독창성, 유사도, 대조 건수, 유사 문장 수, AI 의심도, 판례 판단만 반환한다. 요청 형식은 기존 POST /v1/plagiarism/detect와 동일하며 응답 계약은 docs/API_GUIDE_BAIKAL.md를 따른다.

3. 상세 표절 탐지 — POST /v1/plagiarism/detect

본문 1건을 검사한다.

요청

{
  "doc_id": "episode-001",
  "text": "창밖으로 보이는 숲은 오늘따라 유난히 푸르게 보였다. ...",
  "metadata": { "title": "에피소드 1", "author": "홍길동", "genre": "자서전" },
  "options": {
    "return_evidence": true,
    "threshold": null,
    "top_k": 5,
    "autobiography_mode": null
  },
  "legal_context": {
    "work_type": "literary",
    "access_evidence": null,
    "protected_expression_reviewed": false,
    "rights_verified": false
  }
}
필드 타입 필수 설명
doc_id string 호출측 문서 식별자 (응답에 그대로 반환)
text string 검사할 본문 (1자 이상)
metadata object title/author/genre/publisher/publication_year (모두 선택)
options.return_evidence bool 일치 구간 하이라이트 반환 (기본 true)
options.threshold float|null 판정 임계값. null이면 서버 기본(0.85)
options.top_k int 최대 매칭 수 (기본 5)
options.autobiography_mode bool|null 자서전 특화 전처리. null이면 서버 설정
legal_context object 사람이 확인한 접근 가능성·창작성·권리관계. 없으면 미확인으로 처리

응답 (저작권 탭에 표시되는 "깨끗한 글" 예시)

{
  "doc_id": "episode-001",
  "is_infringement": false,
  "confidence": 0.32,
  "review_summary": {
    "originality_percent": 98,
    "similarity_percent": 2,
    "similar_sentence_count": 0,
    "compared_count": 37891,
    "has_suspicion": false,
    "ai_suspicion_level": "low"
  },
  "ai_generation": {
    "suspicion_level": "low",
    "score": 0.02,
    "is_stub": false,
    "available": true,
    "model_version": "hgb-nolen-kf-ko-v1-Qwen3.8-27B-gpt-4.1-mini-gpt-4o-mini-Korean-autobiography-v2-auroc0.997",
    "note": "Qwen·GPT 한국어 자서전 범위의 검토 우선순위 점수이며 AI 작성 확정 판정이 아님"
  },
  "matches": [],
  "extracted_elements": {
    "characters": [], "motifs": [], "genre": "자서전", "keywords": ["정원", "숲"]
  },
  "ccl_basis": null,
  "autobiography_mode": true,
  "candidates_before_filter": 0,
  "engine_version": "o2o-plagiarism-2.1.0-kosimcse",
  "analyzed_at": "2026-08-04T09:30:00Z"
}

응답 (표절이 탐지된 경우 — matches가 채워짐)

{
  "doc_id": "episode-002",
  "is_infringement": true,
  "confidence": 0.88,
  "review_summary": {
    "originality_percent": 45,
    "similarity_percent": 55,
    "similar_sentence_count": 1,
    "compared_count": 37891,
    "has_suspicion": true,
    "ai_suspicion_level": "low"
  },
  "ai_generation": { "suspicion_level": "unknown", "score": null, "available": false, "is_stub": false, "model_version": "unavailable", "note": "학습된 모델이 없어 채점하지 않음" },
  "matches": [
    {
      "source_doc": "auto-0003",
      "source_title": "○○○ 자서전",
      "similarity": 0.88,
      "tags": [
        { "tag": "reproduction", "role": "primary", "label_ko": "복제권" },
        { "tag": "citation_missing", "role": "primary", "label_ko": "인용 표시 누락" }
      ],
      "case_id": "A6",
      "case_title": "유명인 자서전 일부 베끼기",
      "infringement_type": "copy",
      "evidence_spans": [ { "start": 12, "end": 48, "matched": "일치한 본문 구간 ..." } ],
      "score_breakdown": { "text_sim": 0.86, "lemma_sim": 0.94, "character_sim": 0.8, "motif_sim": 0.7, "lsh_jaccard": 0.62 },
      "partial_signal": {
        "cluster_id": 2, "verdict": "element_swap_plagiarism", "signature_score": 0.74,
        "per_element": { "lemmas": 0.92, "keywords": 0.88, "characters": 0.0, "motifs": 0.1 },
        "retained_elements": ["lemmas", "keywords"], "changed_elements": ["characters", "motifs"]
      }
    }
  ],
  "ccl_basis": "'○○○ 자서전'와 결합 유사도 88%로 매칭. 주 침해 태그: 복제권, 인용 표시 누락. 추정 케이스 A6 ...",
  "autobiography_mode": true,
  "candidates_before_filter": 4,
  "engine_version": "o2o-plagiarism-2.1.0-kosimcse",
  "analyzed_at": "2026-08-04T09:31:00Z"
}

응답 필드 상세

필드 타입 설명
is_infringement bool 후방호환용 임계 초과 매칭 여부. 법적 침해 확정 아님
confidence float 최상위 매칭 결합 유사도 원값(0~1). 화면 표시는 review_summary 사용 권장
review_summary object 저작권 탭 UI 직접 매핑 (§1 표 참조)
ai_generation object AI 생성 의심도 상세. 현재 학습 모델 사용 시 available=true, is_stub=false
matches[] array 매칭된 원본별 상세 (표절 시). 태그·케이스·근거 구간 포함
matches[].tags[] array 법령 태그. role: primary(주)/secondary(보조), label_ko 한글 표기
matches[].case_id string 39종 침해 케이스 ID. case_candidates[0] 과 같다
matches[].case_candidates[] array 태그 동점 케이스 전부. 태그만으로는 1:1로 좁혀지지 않으므로(A1·A2·A3·A4·A5·A6·A15 는 태그가 동일) 원본의 종류로 사람이 확정한다
matches[].case_candidates[].handling string technical_detection(v1.3 ●) / terms_or_report(○)
matches[].case_candidates[].representative_precedents[] array v1.3 IX장 총괄표의 대표판례 사건번호
matches[].case_candidates[].precedents_without_source[] array 대표판례이나 적재본에 없어 출처 제시 불가. docs/PRECEDENT_SOURCE_GAP.md 참조
matches[].evidence_spans[] array 본문 내 일치 구간 {start, end, matched} — 하이라이트용
matches[].partial_signal object 군집화 기반 부분 표절(인물만 교체 등) 분해 — 침해요소 DB 적재용
ccl_basis string|null 사람이 읽는 판정 근거 문장
legal_risk object|null 판례 기반 위험도, 사건번호, A/B/C 검토 등급과 판단 근거
score_semantics object|null 검색 점수·임계값·일치 범위의 의미
has_similarity_match bool|null 등록 코퍼스 유사 원문 확인 여부
corpus_scope_note string|null 검색 대상 코퍼스 범위 안내
engine_version string 엔진 버전

4. 부가 엔드포인트

Method Path 용도
POST /v1/plagiarism/batch 배치 검사(≤500건). 202 + job_id 반환 → 아래 상태 조회
GET /v1/plagiarism/batch/{job_id} 배치 상태·결과 조회
POST /v1/summary 스토리 요약 (과제2 ②)
GET /v1/precedents 전체 판례 검색·등급·저작물 유형 필터
GET /v1/taxonomy 10종 법령 태그 + 39 케이스 정의 + 케이스별 대표판례 (동일 라벨 공유용)
GET /v1/health 엔진 상태·코퍼스 크기·버전

맞춤 요약 옵션

POST /v1/summarydetail(brief|standard|detailed), emphasis(최대 10개), ratio, max_sentences, use_abstractive를 받는다. ratio를 지정하면 detail보다 우선한다.

판례 조회

GET /v1/precedentsq, grade(A|B|C|unreviewed), work_type, offset, limit 쿼리를 지원한다. 운영 판례 545건 전체가 검색·인용 후보이며 A/B/C 등급은 제외 조건이 아니라 검토 품질과 직접성에 따른 우선순위다.

상태 확인 확장

GET /v1/health에는 corpus_documents, index_backend, ai_model_ready, precedent_count가 추가되었다.


5. 에러 포맷

FastAPI 표준. HTTP 상태코드 + detail.

{ "detail": "에러 메시지" }
코드 상황
400 잘못된 요청(빈 본문 등)
404 배치 job_id 없음
422 스키마 검증 실패
503 분류체계 미로딩

6. 연동 시 주의

  1. AI 생성 의심도는 확정값이 아님available/is_stub/model_version을 확인하고 사람 검토 우선순위로만 사용할 것. 출판 승인/거절 게이트로 쓰지 말 것(오탐 리스크).
  2. 화면 값은 review_summary 사용confidence(원값)는 임베딩 성분 때문에 무관한 글도 높게 나오므로 직접 표시 금지. 독창성 환산은 오투오가 review_summary에서 완료해 제공.
  3. 개인정보 — 실사용자 자서전 본문은 개인정보. 실서비스는 온프레미스(Docker) 납품으로 전환 예정. 개발 단계 REST 호출 시에도 테스트 데이터 사용 권장.
  4. compared_count 는 현재 로딩된 검색 세그먼트 수다. 문서 수와 혼동하지 않는다.
  5. 현재 운영 코퍼스는 612개 문서, 37,891개 세그먼트이며 판례는 545건이다. 데이터 증분 적재 시 수치는 달라질 수 있으므로 화면에서는 API 반환값을 그대로 사용한다.