원클릭으로
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") 등에서 동기 실행은 허용