Skip to content

도메인: Disclosure (공시 수집/저장/조회 + AI 분석 파이프라인)

개요

DART OpenAPI에서 공시를 수집·저장하고, AI 분석 결과를 analysis_cache 테이블에 캐싱한다. DisclosureMonitor가 주기적으로 신규 공시를 감지하면 AnalysisScheduler가 vLLM으로 분석 후 결과를 저장한다.

데이터 모델 (핵심 테이블)

dartbrief.disclosures

컬럼타입설명
rcept_noVARCHAR(255) PKDART 접수번호 (14자리 숫자)
corp_codeVARCHAR(8)DART 고유 회사 코드
corp_nameVARCHAR(255)회사명
stock_codeVARCHAR(6)종목코드 (nullable)
corp_typeVARCHAR(10)기업 구분 (Y=KOSPI, K=KOSDAQ 등)
report_nameVARCHAR(255)공시 제목
submitterVARCHAR(255)제출인 (nullable)
rcept_dtVARCHAR(8)접수일자 (YYYYMMDD)
remarkVARCHAR(255)비고 (nullable)
created_atTIMESTAMP(6)DB 저장 시각

인덱스: corp_code, rcept_dt, (rcept_dt DESC, created_at DESC) (피드 정렬용)

dartbrief.analysis_cache

컬럼타입설명
rcept_noVARCHAR(255) PK공시 접수번호 (FK 없음 — 독립 캐시)
one_linerVARCHAR(100)deprecated — 더 이상 쓰지 않음 (컬럼만 잔존, drop 마이그레이션 별건). summary 로 통합
summaryTEXTAI 한줄요약 — 목록(비게이트) + reveal 본문 상단에 노출
key_points_jsonTEXT핵심 포인트 (||| 구분자로 join된 문자열)
sentimentVARCHAR(10)감성 분석 (positive/negative/neutral)
modelVARCHAR(255)사용된 LLM 모델명
statusVARCHAR(20)PENDING / PROCESSING / COMPLETED / FAILED
retry_countINTEGER실패 후 재시도 횟수
doc_lengthINT원문 글자 수 (짧은 공시 면제 판단에 사용)
created_atTIMESTAMP(6)

인덱스: status (스케줄러 PENDING 조회용)

동작 플로우

신규 공시 수집 파이프라인

mermaid
sequenceDiagram
    participant Monitor as DisclosureMonitor
    participant DART as DART OpenAPI
    participant DB as PostgreSQL
    participant Scheduler as AnalysisScheduler
    participant vLLM as vLLM

    Note over Monitor: 설정된 interval마다 실행<br/>(장중 시간대 KST 8~20시, 평일만)

    Monitor->>DART: FetchDisclosures(today, today, "Y", page=1, count=100)
    Monitor->>DART: FetchDisclosures(today, today, "K", page=1, count=100)
    DART-->>Monitor: 공시 목록

    Note over Monitor: SaveBatch 는 모든 공시 INSERT (카테고리 필터 X)<br/>본문 없는 공시도 피드 노출, 분석 단계에서 UNAVAILABLE 처리

    Monitor->>DB: SaveBatch — INSERT ON CONFLICT DO NOTHING
    DB-->>Monitor: newOnes (신규 공시 목록)

    loop 신규 공시마다
        Monitor->>DB: analysis_cache INSERT (status=PENDING)
    end

    Note over Scheduler: concurrency 개 영구 워커가 끊김 없이 실행<br/>(죽은 워커가 남긴 stale PROCESSING row 는<br/>ClaimPending 이 staleThreshold 기준으로 흡수 회수)

    par concurrency 개 워커가 각자 claim → 처리 → 반복
        Note over Scheduler: 워커 1개당 일감 1건 claim<br/>(우선순위: PENDING > 재시도 FAILED > BODY_PENDING)
        Scheduler->>DB: ClaimPending / ClaimRetryable / ClaimBodyPending (LIMIT 1)
        Scheduler->>DB: FindByRceptNo (공시 메타 조회)
        Scheduler->>DART: FetchDocumentText (OpenAPI document.xml)
        alt OpenAPI 성공 + 본문 있음
            Note over Scheduler: 정상 경로
        else OpenAPI 실패 (status=014 / zip 에러 / 빈 본문)
            Scheduler->>DART: fetchViaViewer (dsaf001/main.do + report/viewer.do)
            Note over Scheduler: 웹 viewer 는 API 보다 일찍 인덱싱되는 경우가 많아<br/>인덱싱 지연 케이스 회수율 ↑.
        end

        alt 둘 다 빈 본문 + 1차가 status=014
            Scheduler->>DB: MarkBodyPending (status=BODY_PENDING, retry_count++)
            Note over Scheduler: 영구 부재 결론 보류 — DART 정기보고서 인덱싱이<br/>수 시간 지연되는 케이스 다수 (2026-05-13 발견).<br/>워커 풀이 BODY_PENDING 을 회수 (행당 5분 cooldown — ClaimBodyPending).<br/>created_at + 24h 경과 시 MarkFailedPermanent → UNAVAILABLE.
        else 둘 다 빈 본문 + status=014 아님 (인덱싱 지연)
            Scheduler->>DB: RequeuePending — status=PENDING 유지, retry_count++
            Note over Scheduler: LLM 호출 X. 다음 워커가 재시도.<br/>retry_count >= 60 (≈1시간) 도달 시 FAILED.
        else 본문 확보 (API or viewer)
            Scheduler->>vLLM: Analyze(text)
            vLLM-->>Scheduler: Summary, KeyPoints, Sentiment, Model
            Scheduler->>DB: MarkCompleted (status=COMPLETED, doc_length 저장)
        end
    end

공시 조회 (사용자 요청)

mermaid
flowchart TD
    A[GET /api/v1/disclosures/recent] --> B[FindRecent limit=10~50]
    C[GET /api/v1/disclosures?corpCode=X] --> D[FindByCorpCode]
    E[GET /api/v1/disclosures?beginDate=X&endDate=Y] --> F[FindByDateRange]
    G[GET /api/v1/disclosures/rceptNo] --> H[FindByRceptNo]
    I[POST /api/v1/admin/disclosures/fetch] --> J[DART API 직접 호출 후 SaveBatch]
    B & D & F & H & J --> K[(disclosures 테이블)]

수동 fetch (POST /api/v1/admin/disclosures/fetch)

DART API를 즉시 호출해 DB에 저장한다 (디버깅/응답 확인용 동기 호출). 대량 백필은 /admin/backfill (비동기 + 진행률 추적) 사용. 파라미터:

  • beginDate, endDate (필수, YYYYMMDD)
  • corpCode (선택 — 있으면 corp_cls 미설정(전체), 없으면 corp_cls=Y KOSPI. 특정 기업 필터링 미지원 — DART corpCode를 그대로 전달하지 않음)
  • pageNo (기본 1), pageCount (기본 20)
  • 인증: X-Admin-Key 필수 (Admin helper).

룰 / 정책

항목환경변수/설정
모니터링 간격설정값MONITOR_INTERVAL
모니터링 활성화기본 활성DISCLOSURE_MONITOR_AUTO_START=false 로 비활성화
모니터링 시간대KST 8~20시, 평일코드 하드코딩
분석 스케줄러 활성화기본 비활성ANALYSIS_SCHEDULER_AUTO_START=true
submit 워커 수설정값AI_CONCURRENCY — PENDING → DART fetch → vLLM Submit 까지만 처리하는 영구 워커 수. 동기 LLM 대기 없음 (background:true)
워커 빈 폴링 간격2초SCHEDULER_IDLE_POLL_MS — claim 할 일감 없을 때
폴링 루프 사이클 간격3초LLM_POLL_INTERVAL_MSpollLoop 가 in-flight (PROCESSING + llm_response_id NOT NULL) row 들을 GET 으로 회수하는 주기. fan-out 16 goroutine 으로 한 사이클 최대 inflightPollLimit=200 처리
분석 최대 재시도 (LLM/일반 에러)3회코드: retry_count >= 3 → UNAVAILABLE
본문 미도착 최대 재대기60회 (1시간)코드: maxBodyRetries — 초과 시 FAILED
BODY_PENDING 최대 보류 시간24hpolicy.BodyPendingMaxAge — 초과 시 MarkFailedPermanent (UNAVAILABLE)
BODY_PENDING 회수 cooldown5분 (행당)ClaimBodyPendingupdated_at < now-5min 인 row 만 워커 풀이 재시도
stale PROCESSING 회수 임계5분 (고정)staleThresholdPROCESSING + llm_response_id IS NULL 이면서 이 시간 이상 묶인 row 를 ClaimPending 이 회수. in-flight (id NOT NULL) 는 폴링 루프 책임이라 stale 회수 제외 — 폴링 루프가 죽더라도 부팅 시 ListInflight 가 재포착
job 최대 소요 (jobTimeout)90초 (고정)DART fetch + vLLM Submit 까지의 워커 점유 시간 cap. 초과 시 재시도 가능 FAILED
vLLM in-flight 상한AI_CONCURRENCY 와 동일submit 워커 수 = in-flight cap (단일 knob). cap predicate 는 active inflight 만 집계 — status IN ('PROCESSING','BODY_PROCESSING') AND (llm_response_id IS NOT NULL OR updated_at >= now-staleThreshold). BODY_PROCESSING fresh 도 카운트 — DART 본문 fetch 후 곧바로 vLLM submit 으로 전이하므로 cap 슬롯을 미리 차지해야 PROCESSING 누적이 cap 초과 안 함. stale-NULL row 는 cap 슬롯 점유 X (회수 deadlock 방지). 각 Claim tx 가 pg_advisory_xact_lock(claimLockKey) 로 직렬화 → 다음 tx 는 이전 commit 의 active inflight 를 보고 cap 계산 → 동시 워커 race 가 cap 못 깸 (TestClaimPending_StrictCapUnderConcurrentWorkers, TestClaimPending_StaleProcessingDoesNotConsumeCapSlot, TestClaim_BodyProcessingFreshConsumesCapSlot). cap 도달 시 워커 claim 0건 → idlePoll 후 재시도. <=0 입력 시 defaultInflightCap=60 fallback. Claim* 3종 (Pending/Retryable/BodyPending) 은 private claimWithCap helper 공유 — 골격 1군데, selector/order 만 다름
BODY_PENDING UX (Flutter)분석 카드 분기헤더 phosphor Clock + "본문 대기" pill, 본문에 "DART 본문 등록을 기다리는 중이에요" 메시지. 일반 PENDING 의 shimmer skeleton 과 시각적 분기 (PR feat/fe-0513-body-pending-ux, 2026-05-13)
analysis_cache in-memory LRU5000 entryAnalysisRepository.cacheFindByRceptNo read-through. COMPLETED row 만 캐시 (immutable — mutation 경로 없음). Inspect+Reveal 의 DB read 2회를 핫 공시 기준 0회로 감소
HTTP 캐시 TTL (공시 원문)5분main.go CacheRule
HTTP 캐시 TTL (최근 피드)30초main.go CacheRule
HTTP 캐시 TTL (기업별 목록)1분main.go CacheRule
LLM 엔드포인트설정값LLM_BASE_URL, LLM_MODEL, LLM_TIMEOUT_MS (개별 HTTP 호출 timeout — submit/poll 짧음), LLM_POLL_INTERVAL_MS
Cloudflare Access설정값CF_ACCESS_CLIENT_ID, CF_ACCESS_CLIENT_SECRET

KOSPI/KOSDAQ만 모니터링

DisclosureMonitor는 corp_cls=Y(KOSPI), corp_cls=K(KOSDAQ) 두 가지만 조회한다. 비상장사/코넥스 등 기타는 수동 fetch로만 수집 가능.

Edge case

케이스동작
중복 공시 SaveBatchINSERT ON CONFLICT DO NOTHING — 기존 row 유지
본문 없는 카테고리 ([첨부정정], 효력발생안내 등)disclosures INSERT 그대로 진행 → 피드 노출. 분석 단계에서 OpenAPI status=014 → viewer fallback 시도 → viewer 도 빈 응답이면 MarkBodyPending (BODY_PENDING). 24h 이내 워커 풀이 회수 시도, 미회수 시 MarkFailedPermanent → UI UNAVAILABLE. viewer 에 메타 본문이 있으면 ([첨부정정] 정정 사유 등) 분석 진행.
DART OpenAPI status=014viewer fallback 시도. viewer 도 본문 없으면 ErrBodyUnavailable즉시 영구 부재 마킹 X, BODY_PENDING 으로 보류 (PR #82, 2026-05-13). 워커 풀이 행당 5분 cooldown 으로 재시도 — 정기보고서/정정/대량보유 등은 OpenAPI 인덱싱이 1~6시간 지연되는 케이스 다수. 24h 경과 시 영구 부재 확정. GET /ai/analyze 응답 status=BODY_PENDING (대기) 또는 UNAVAILABLE (확정).
OpenAPI 인덱싱 지연 (zip 파싱 실패 / 빈 본문)viewer fallback 시도 (DART 웹 viewer 가 더 일찍 인덱싱). viewer 도 빈 응답이면 RequeuePending → status=PENDING 유지 + retry_count++ → 다음 워커가 재시도. 60회 누적 시 FAILED.
PROCESSING 상태 멈춤 (stuck)죽은 워커가 남긴 row. ClaimPendingstaleThreshold 초과 PROCESSING row 를 claim 대상에 포함해 흡수 회수 (별도 sweep 루프 없음). 정상 배포는 graceful shutdown 으로 in-flight job 마무리 후 종료 → stuck 자체가 거의 안 생김
retry_count >= 3GET analyze 응답에서 status=UNAVAILABLE 반환
장외 시간 (주말/야간)DisclosureMonitor가 isMarketHours() false → fetch skip
분석 없는 rcept_noFindByRceptNo → pgx.ErrNoRows → UNAVAILABLE 응답
vLLM 장애processOne 에러 → MarkFailed → retry_count + 1

관련 API

엔드포인트설명인증
GET /api/v1/disclosures/recent?limit=N최근 공시 N개 (최대 50)불필요
GET /api/v1/disclosures?corpCode=X기업별 공시 목록불필요
GET /api/v1/disclosures?beginDate=X&endDate=Y날짜 범위 공시불필요
GET /api/v1/disclosures/{rceptNo}공시 단건 조회불필요
POST /api/v1/admin/disclosures/fetchDART API 즉시 수집 (디버깅용)X-Admin-Key
GET /api/v1/companies/{corpCode}기업 정보 조회불필요
GET /api/v1/ai/analyze/{rceptNo}분석 메타 + (조건부) 결과필요
POST /api/v1/ai/analyze/{rceptNo}/reveal분석 결과 reveal + quota 차감필요