بنقرة واحدة
verify-kb-indexing
KB 비동기 인덱싱 파이프라인 검증. 인덱싱 관련 코드 수정 후 사용.
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
KB 비동기 인덱싱 파이프라인 검증. 인덱싱 관련 코드 수정 후 사용.
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
세션 변경사항을 분석하여 검증 스킬 누락을 탐지합니다. 기존 스킬을 동적으로 탐색하고, 새 스킬을 생성하거나 기존 스킬을 업데이트한 뒤 CLAUDE.md를 관리합니다.
REST API 응답 DTO 일관성 및 에러 코드 표준 검증. API 엔드포인트 추가/수정, DTO 변경, 에러 처리 수정 후 사용. API 계약 변경이 프론트엔드에 영향을 줄 수 있을 때 사용.
Flyway DB 마이그레이션 일관성 검증. 마이그레이션 추가/수정 후 사용.
프론트엔드 UI 컴포넌트 품질 검증 (shadcn/ui + Tailwind 기준). UI 컴포넌트 수정 후 사용.
RAG 파이프라인 (답변 작성 + 분석 + 다운로드) 검증. 답변 작성/분석/다운로드 관련 코드 수정 후 사용.
문의 상세 페이지 UI 컴포넌트 검증 (shadcn/ui + Tailwind 기준). 문의 탭(Info/Analysis/Answer/History) 및 업로드 컴포넌트 수정 후 사용.
| name | verify-kb-indexing |
| description | KB 비동기 인덱싱 파이프라인 검증. 인덱싱 관련 코드 수정 후 사용. |
| File | Purpose |
|---|---|
backend/app-api/src/main/java/com/biorad/csrag/app/AsyncConfig.java | @EnableAsync + 스레드 풀 설정 |
backend/app-api/src/main/java/com/biorad/csrag/application/knowledge/KnowledgeIndexingWorker.java | 비동기 인덱싱 워커 |
backend/app-api/src/main/java/com/biorad/csrag/application/knowledge/KnowledgeBaseService.java | KB CRUD + 인덱싱 오케스트레이션 |
backend/app-api/src/main/java/com/biorad/csrag/infrastructure/persistence/knowledge/KnowledgeDocumentJpaEntity.java | KB 문서 엔티티 (상태 머신) |
backend/app-api/src/main/java/com/biorad/csrag/interfaces/rest/KnowledgeBaseController.java | REST 엔드포인트 |
backend/app-api/src/main/java/com/biorad/csrag/interfaces/rest/document/DocumentTextExtractor.java | PDF/DOCX 텍스트 추출 |
backend/app-api/src/main/java/com/biorad/csrag/interfaces/rest/chunk/ChunkingService.java | 청킹 서비스 |
backend/app-api/src/main/java/com/biorad/csrag/interfaces/rest/document/DocumentIndexingService.java | 문의 문서 인덱싱 서비스 |
backend/app-api/src/main/java/com/biorad/csrag/interfaces/rest/vector/QdrantVectorStore.java | Qdrant 벡터 스토어 (컬렉션 자동 생성 + 벡터 삭제) |
backend/app-api/src/main/java/com/biorad/csrag/interfaces/rest/vector/MockVectorStore.java | Mock 벡터 스토어 (동시성 안전 삭제) |
backend/app-api/src/main/java/com/biorad/csrag/interfaces/rest/vector/VectorizingService.java | 청크 벡터화 서비스 (KB 청크의 productFamily를 부모 문서에서 조회) |
backend/app-api/src/main/java/com/biorad/csrag/application/knowledge/DocumentMetadataAnalyzer.java | 문서 메타데이터 분석 (PromptRegistry 기반 LLM 분석) |
파일: KnowledgeIndexingWorker.java
검사: @Async 메서드가 별도 @Component 클래스에 있는지 확인. Spring AOP 프록시 제약으로 같은 클래스 내 @Async 호출은 동작하지 않음.
grep -n "@Async\|@Component" backend/app-api/src/main/java/com/biorad/csrag/application/knowledge/KnowledgeIndexingWorker.java
PASS: @Component와 @Async가 같은 파일에 존재하고, KnowledgeBaseService와 다른 클래스 FAIL: @Async가 KnowledgeBaseService 내부에 존재
파일: AsyncConfig.java, KnowledgeIndexingWorker.java
검사: @Bean 이름과 @Async 참조가 일치하는지 확인.
grep -n "kbIndexingExecutor" backend/app-api/src/main/java/com/biorad/csrag/app/AsyncConfig.java backend/app-api/src/main/java/com/biorad/csrag/application/knowledge/KnowledgeIndexingWorker.java
PASS: AsyncConfig의 @Bean(name = "kbIndexingExecutor")와 워커의 @Async("kbIndexingExecutor")가 일치 FAIL: 이름이 불일치하거나 @Async에 executor 이름이 없음
파일: KnowledgeIndexingWorker.java
검사: 워커의 @Transactional이 REQUIRES_NEW propagation을 사용하는지 확인. 서비스 트랜잭션과 독립적이어야 함.
grep -n "REQUIRES_NEW\|Propagation" backend/app-api/src/main/java/com/biorad/csrag/application/knowledge/KnowledgeIndexingWorker.java
PASS: @Transactional(propagation = Propagation.REQUIRES_NEW) 존재
FAIL: 기본 propagation(REQUIRED) 사용 또는 @Transactional 누락
파일: KnowledgeBaseService.java
검사: 비동기 워커 호출이 afterCommit() 콜백 안에서 실행되는지 확인. 트랜잭션 커밋 전에 워커가 시작되면 데이터를 찾지 못함.
grep -n "afterCommit\|TransactionSynchronization" backend/app-api/src/main/java/com/biorad/csrag/application/knowledge/KnowledgeBaseService.java
PASS: afterCommit() 내에서 indexingWorker.indexOneAsync() 호출
FAIL: afterCommit 없이 직접 워커 호출
파일: KnowledgeDocumentJpaEntity.java
검사: 상태 전이 메서드가 올바른 상태값을 설정하는지 확인.
grep -n "mark\|status\s*=" backend/app-api/src/main/java/com/biorad/csrag/infrastructure/persistence/knowledge/KnowledgeDocumentJpaEntity.java
PASS: markIndexing() → "INDEXING", markIndexed() → "INDEXED", markFailed() → "FAILED" 존재 FAIL: 알 수 없는 상태값 사용 또는 markIndexing() 누락
파일: KnowledgeBaseService.java
검사: INDEXING 상태인 문서에 대한 재인덱싱 요청 시 409를 반환하는지 확인.
grep -n "INDEXING\|CONFLICT\|409" backend/app-api/src/main/java/com/biorad/csrag/application/knowledge/KnowledgeBaseService.java
PASS: status가 "INDEXING"일 때 ResponseStatusException(CONFLICT) throw FAIL: 중복 인덱싱 가드 없음
파일: KnowledgeBaseController.java
검사: 인덱싱 엔드포인트가 202 Accepted를 반환하는지 확인.
grep -n "accepted\|202\|indexing" backend/app-api/src/main/java/com/biorad/csrag/interfaces/rest/KnowledgeBaseController.java
PASS: ResponseEntity.accepted() 사용
FAIL: ResponseEntity.ok() 또는 200 반환
파일: KnowledgeIndexingWorker.java, DocumentIndexingService.java
검사: PDF 텍스트 추출 시 DocumentTextExtractor를 사용하는지 확인 (raw bytes 읽기 금지).
grep -n "DocumentTextExtractor\|extractText\|extractByPage\|new String.*bytes" backend/app-api/src/main/java/com/biorad/csrag/application/knowledge/KnowledgeIndexingWorker.java backend/app-api/src/main/java/com/biorad/csrag/interfaces/rest/document/DocumentIndexingService.java
PASS: DocumentTextExtractor 의존성 주입 및 extractText()/extractByPage() 호출
FAIL: new String(bytes, UTF-8) 등 raw bytes 변환 존재
파일: KnowledgeIndexingWorker.java, DocumentIndexingService.java
검사: OCR 불필요 경로에서 extractByPage() → chunkAndStore(docId, pageTexts, sourceType, sourceId, fileName) 5-arg 오버로드를 호출하는지 확인. RF-03에서 파일명 프리픽스를 위해 5번째 인자 fileName이 추가됨.
grep -n "extractByPage\|chunkAndStore.*pageTexts\|PageText\|getFileName" backend/app-api/src/main/java/com/biorad/csrag/application/knowledge/KnowledgeIndexingWorker.java backend/app-api/src/main/java/com/biorad/csrag/interfaces/rest/document/DocumentIndexingService.java
PASS: 비-OCR 경로에서 extractByPage() 호출 후 chunkAndStore(docId, pageTexts, sourceType, sourceId, fileName) 5-arg 사용, doc.getFileName() 전달
FAIL: fileName 인자 누락 (4-arg 오버로드 사용) 또는 null 전달
파일: KnowledgeIndexingWorker.java
검사: 최상위 try-catch로 모든 예외를 잡아 markFailed()로 상태를 기록하는지 확인.
grep -n "catch\|markFailed\|lastError" backend/app-api/src/main/java/com/biorad/csrag/application/knowledge/KnowledgeIndexingWorker.java
PASS: catch(Exception)에서 markFailed(error) 호출 및 저장 FAIL: 예외 전파 또는 markFailed 누락
파일: KnowledgeIndexingWorker.java
검사: @Async 워커에서 문서 조회 시 ResponseStatusException 대신 orElse(null) + null 체크 + return 패턴을 사용하는지 확인. @Async 메서드에서 ResponseStatusException은 Spring MVC 예외 핸들러에 도달하지 않음.
grep -n "ResponseStatusException\|orElse(null)\|doc == null" backend/app-api/src/main/java/com/biorad/csrag/application/knowledge/KnowledgeIndexingWorker.java
PASS: findById(docId).orElse(null) + if (doc == null) return 패턴, ResponseStatusException import 없음
FAIL: ResponseStatusException 사용 또는 orElseThrow() 사용
파일: QdrantVectorStore.java, KnowledgeBaseService.java
검사: QdrantVectorStore.deleteByDocumentId()가 실패 시 RuntimeException을 throw하고, KnowledgeBaseService.delete()에서 이를 try-catch로 감싸 고스트 벡터 상황을 로깅하되 DB 삭제는 계속 진행하는지 확인.
grep -n "deleteByDocumentId\|RuntimeException\|ghost" backend/app-api/src/main/java/com/biorad/csrag/interfaces/rest/vector/QdrantVectorStore.java backend/app-api/src/main/java/com/biorad/csrag/application/knowledge/KnowledgeBaseService.java
PASS: QdrantVectorStore에서 RuntimeException throw + KnowledgeBaseService에서 try-catch 감싸기 + error 로그 FAIL: QdrantVectorStore에서 예외 삼킴 또는 KnowledgeBaseService에서 벡터 삭제 실패 시 전체 롤백
파일: QdrantVectorStore.java
검사: ensureCollection()이 (1) ReentrantLock으로 동기화하고 (virtual thread 피닝 방지, synchronized 사용 금지), (2) 컬렉션 존재 여부를 GET 요청으로 먼저 확인하고, (3) 없을 때만 PUT으로 생성하며, (4) 생성 실패 시 collectionReady를 true로 설정하지 않는지 확인.
grep -n "ReentrantLock\|collectionLock\|collectionReady\|synchronized" backend/app-api/src/main/java/com/biorad/csrag/interfaces/rest/vector/QdrantVectorStore.java
PASS: ReentrantLock + collectionLock.lock()/unlock() (try-finally) + GET 확인 → PUT 생성 → 실패 시 early return, synchronized 키워드 없음
FAIL: synchronized 사용 (virtual thread 피닝 유발) 또는 GET 확인 없이 바로 PUT 또는 생성 실패에도 collectionReady = true
파일: MockVectorStore.java
검사: deleteByDocumentId()에서 ConcurrentHashMap의 entrySet을 직접 순회하며 삭제하지 않고, 삭제 대상 키를 먼저 수집한 후 별도로 삭제하는지 확인.
grep -n "toRemove\|stream\|forEach.*remove\|removeIf" backend/app-api/src/main/java/com/biorad/csrag/interfaces/rest/vector/MockVectorStore.java
PASS: 키를 List로 수집 후 별도 삭제 또는 removeIf 사용 FAIL: entrySet 순회 중 직접 remove 호출 (ConcurrentModificationException 위험)
파일: KnowledgeIndexingWorker.java
검사: 재인덱싱 시 유령 벡터 방지를 위해 vectorStore.deleteByDocumentId(docId)를 청킹 전에 호출하는지 확인.
grep -n "deleteByDocumentId\|vectorStore" backend/app-api/src/main/java/com/biorad/csrag/application/knowledge/KnowledgeIndexingWorker.java
PASS: vectorStore.deleteByDocumentId(docId) 호출이 텍스트 추출/청킹 전에 존재
FAIL: 벡터 삭제 없이 바로 청킹 시작 (유령 벡터 위험)
파일: ChunkingService.java
검사: resolvePageRange가 오프셋 기반이 아닌 콘텐츠 매칭 방식(findMatchingPage)을 사용하는지 확인. 문장 분리/재결합 시 오프셋 드리프트 문제를 해결하기 위함.
grep -n "findMatchingPage\|normalizedPages\|resolvePageRange" backend/app-api/src/main/java/com/biorad/csrag/interfaces/rest/chunk/ChunkingService.java
PASS: findMatchingPage 메서드 존재, normalizedPages 사전 계산, resolvePageRange가 문자열 콘텐츠를 받음
FAIL: 오프셋 기반 resolvePageRange(int chunkStart, int chunkEnd, ...) 사용
파일: ChunkingService.java
검사: resolvePageRange()에서 MAX_PAGE_SPAN 상수를 사용하여 chunk가 비정상적으로 넓은 페이지 범위에 걸리는 것을 방지하는지 확인. 중복 텍스트가 있는 PDF에서 오매칭 방지.
grep -n "MAX_PAGE_SPAN\|pageEnd - pageStart" backend/app-api/src/main/java/com/biorad/csrag/interfaces/rest/chunk/ChunkingService.java
PASS: MAX_PAGE_SPAN = 3 상수 존재, (pageEnd - pageStart) >= MAX_PAGE_SPAN 검증 후 pageEnd = pageStart 보정
FAIL: MAX_PAGE_SPAN 제한 없음 또는 과도한 span 허용
파일: ChunkingService.java
검사: resolvePageRange()에서 pageEnd < pageStart인 경우 pageEnd = pageStart로 보정하는지 확인. 중복 텍스트로 인해 끝 페이지가 시작 페이지보다 앞으로 매칭되는 오류 방지.
grep -n "pageEnd < pageStart\|searchFromIndex" backend/app-api/src/main/java/com/biorad/csrag/interfaces/rest/chunk/ChunkingService.java
PASS: pageEnd < pageStart → pageEnd = pageStart 보정 + searchFromIndex로 pageEnd 검색을 pageStart 인덱스부터 시작
FAIL: 역전 검증 없음 또는 searchFromIndex 미사용
파일: QdrantVectorStore.java
검사: deleteByDocumentId()가 컬렉션이 존재하지 않을 때 graceful하게 처리하는지 확인 (재인덱싱 첫 실행 시 발생 가능).
grep -n "doesn't exist\|collection_not_found\|deleteByDocumentId" backend/app-api/src/main/java/com/biorad/csrag/interfaces/rest/vector/QdrantVectorStore.java
PASS: msg.contains("doesn't exist") 시 로그만 남기고 skip, 그 외 예외는 RuntimeException throw
FAIL: 컬렉션 미존재 시 무조건 예외 전파
파일: AsyncConfig.java
검사: SimpleAsyncTaskExecutor에서 virtual threads가 활성화되고, concurrencyLimit으로 동시 실행 수가 제한되는지 확인. t4g.small (2 vCPU, 2GB RAM) 환경에서 OpenAI API 과부하 방지.
grep -n "SimpleAsyncTaskExecutor\|setVirtualThreads\|setConcurrencyLimit\|kb-indexing\|doc-indexing" backend/app-api/src/main/java/com/biorad/csrag/app/AsyncConfig.java
PASS: SimpleAsyncTaskExecutor + setVirtualThreads(true) + setConcurrencyLimit(4) 설정, prefix="kb-indexing-" 및 "doc-indexing-"
FAIL: ThreadPoolTaskExecutor 사용 (platform threads) 또는 setVirtualThreads(true) 누락 또는 concurrencyLimit 미설정
파일: VectorizingService.java
검사: KB 청크(sourceType = "KNOWLEDGE_BASE")의 productFamily를 청크 자체에서 읽지 않고, KnowledgeDocumentJpaRepository.findById(documentId)를 통해 부모 문서에서 조회하는지 확인. ChunkingService가 청크에 productFamily를 설정하지 않으므로 청크에서 읽으면 항상 null이 됨.
grep -n "kbDocRepository\|KnowledgeDocumentJpaRepository\|KNOWLEDGE_BASE.*resolvedProductFamily\|getProductFamily" backend/app-api/src/main/java/com/biorad/csrag/interfaces/rest/vector/VectorizingService.java
PASS: kbDocRepository.findById(documentId).map(KnowledgeDocumentJpaEntity::getProductFamily) 호출 존재, KNOWLEDGE_BASE 타입 체크 후 부모 문서에서 productFamily 조회
FAIL: chunk.getProductFamily() 직접 호출 (항상 null 반환) 또는 productFamily 조회 로직 없음
| # | 검사 항목 | 결과 | 상세 |
|---|---|---|---|
| 1 | @Async 클래스 분리 | PASS/FAIL | 워커 클래스 위치 |
| 2 | Executor 이름 일치 | PASS/FAIL | Bean name vs @Async ref |
| 3 | REQUIRES_NEW propagation | PASS/FAIL | 트랜잭션 격리 |
| 4 | afterCommit 콜백 | PASS/FAIL | TSM 사용 여부 |
| 5 | 상태 머신 유효성 | PASS/FAIL | 상태 전이 메서드 |
| 6 | 중복 인덱싱 방지 | PASS/FAIL | 409 Conflict 가드 |
| 7 | HTTP 202 반환 | PASS/FAIL | 비동기 엔드포인트 응답 |
| 8 | TextExtractor 사용 | PASS/FAIL | PDF 추출 방식 |
| 9 | 워커 에러 핸들링 | PASS/FAIL | catch + markFailed |
| 9b | 워커 null-safe 패턴 | PASS/FAIL | orElse(null) + return |
| 9c | 벡터 삭제 예외 전파 | PASS/FAIL | RuntimeException + try-catch |
| 9d | ensureCollection ReentrantLock + GET 우선 | PASS/FAIL | ReentrantLock + GET → PUT → early return |
| 9e | MockVectorStore 동시성 | PASS/FAIL | 키 수집 후 삭제 |
| 10 | 워커 벡터 사전 삭제 | PASS/FAIL | deleteByDocumentId 호출 위치 |
| 11 | 콘텐츠 기반 페이지 매핑 | PASS/FAIL | findMatchingPage 존재 |
| 12 | 컬렉션 미존재 안전 처리 | PASS/FAIL | doesn't exist 처리 |
| 13 | Virtual Thread Executor | PASS/FAIL | SimpleAsyncTaskExecutor + virtualThreads + concurrencyLimit |
| 14 | MAX_PAGE_SPAN 제한 | PASS/FAIL | 과도한 span 허용 |
| 15 | pageEnd 역전/span 검증 | PASS/FAIL | 역전 보정 + searchFromIndex |
| 16 | KB productFamily 부모 조회 | PASS/FAIL | VectorizingService → kbDocRepository |
| 17 | 파일명 프리픽스 (RF-03) | PASS/FAIL | applyFileNamePrefix + 오프셋 드리프트 방지 |
| 18 | 헤딩 인식 청킹 (RF-05) | PASS/FAIL | HEADING_PATTERN + isHeading() |
| 19 | 오버랩 300자 (RF-02) | PASS/FAIL | OVERLAP_CHARS = 300 또는 @Value 프로퍼티 |
| 20 | productFamily 파라미터 전파 | PASS/FAIL | chunkAndStore 오버로드 + setProductFamily |
파일: ChunkingService.java
검사: applyFileNamePrefix() 헬퍼가 존재하고, 청크 콘텐츠 앞에 [fileName] 프리픽스를 추가하는지 확인. 오프셋 계산에는 rawContent(프리픽스 미포함)를 사용하고, 저장 시에만 프리픽스를 적용하는지 확인 (globalOffset 드리프트 방지).
grep -n "applyFileNamePrefix\|rawContent\|globalOffset" backend/app-api/src/main/java/com/biorad/csrag/interfaces/rest/chunk/ChunkingService.java
PASS: applyFileNamePrefix(rawContent, fileName) 존재, 오프셋은 rawContent.length()로 계산, globalOffset에 프리픽스 길이 미포함
FAIL: 프리픽스 적용 후 오프셋 계산 (드리프트 발생) 또는 applyFileNamePrefix 메서드 없음
파일: ChunkingService.java
검사: HEADING_PATTERN 정규식과 isHeading() 메서드가 존재하고, 청크 병합 루프에서 제목 감지 시 새 청크를 시작하는지 확인.
grep -n "HEADING_PATTERN\|isHeading\|heading" backend/app-api/src/main/java/com/biorad/csrag/interfaces/rest/chunk/ChunkingService.java
PASS: HEADING_PATTERN (Markdown heading, 번호 리스트, 대문자 제목) + isHeading() + 병합 루프에서 isHeading(nextSentence) 체크 후 break
FAIL: 헤딩 인식 없이 문장을 무조건 병합
파일: ChunkingService.java
검사: 오버랩이 300자로 설정되어 있는지 확인. OVERLAP_CHARS 상수 또는 @Value("${rag.chunking.overlap:300}") 프로퍼티 기반 설정 모두 허용.
grep -n "OVERLAP_CHARS\|rag.chunking.overlap" backend/app-api/src/main/java/com/biorad/csrag/interfaces/rest/chunk/ChunkingService.java
PASS: OVERLAP_CHARS = 300 상수 또는 @Value("${rag.chunking.overlap:300}") 프로퍼티 설정 (기본값 300)
FAIL: 오버랩이 300이 아닌 다른 값으로 설정
파일: ChunkingService.java
검사: chunkAndStore() 오버로드에 String productFamily 파라미터가 존재하고, chunk 엔티티에 setProductFamily(productFamily) 호출이 있는지 확인.
grep -n "productFamily\|setProductFamily" backend/app-api/src/main/java/com/biorad/csrag/interfaces/rest/chunk/ChunkingService.java
PASS: chunkAndStore(..., String productFamily) 오버로드 + chunk.setProductFamily(productFamily) 호출
FAIL: productFamily 파라미터 없음 또는 setProductFamily 미호출
다음은 위반이 아닙니다:
@Profile("test") 등에서 동기 실행은 허용