feat: 프로젝트 문서의 AI 반영 상태 배지 표시
AI 추천이 문서 내용을 반영하지 못해도(추출 실패·미지원 형식) 추천은 정상 응답되어 사용자가 인지할 수 없던 문제를 해소한다. 문서 목록에 반영 상태 배지를 노출한다. - ProjectAttachmentResponse.aiStatus 추가(ready/failed/unsupported) · ready: 추출 성공(반영 가능) · failed: 지원 형식이나 추출 실패 · unsupported: 미지원 형식(이미지·xlsx·ppt·zip 등) - isExtractableType + deriveAiStatus 로 상태 산출 (추출 본문은 응답에 노출하지 않고 상태 판별에만 사용) - listFiles 에서 extracted_text 를 명시적으로 선택(select:false 우회) - 공용 AttachmentAiBadge 컴포넌트 추가 → 프로젝트 상세·설정 문서 목록에 적용 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,26 @@ import { extractRawText } from 'mammoth';
|
||||
// 문서 1건당 추출 텍스트 상한(자) — DB 용량과 AI 토큰 사용량을 함께 보호한다.
|
||||
export const PER_DOC_TEXT_LIMIT = 8000;
|
||||
|
||||
// 문서의 AI 반영 상태 — 목록 배지 표기에 사용
|
||||
// ready: 추출 성공(반영 가능) / failed: 지원 형식이나 추출 실패 / unsupported: 미지원 형식
|
||||
export type AttachmentAiStatus = 'ready' | 'failed' | 'unsupported';
|
||||
|
||||
// 텍스트 추출 대상 형식인지 여부(txt·csv·pdf·docx). 미지원/추출실패 배지 구분에 사용한다.
|
||||
export function isExtractableType(mime: string, name: string): boolean {
|
||||
const lower = name.toLowerCase();
|
||||
return (
|
||||
mime === 'text/plain' ||
|
||||
mime === 'text/csv' ||
|
||||
lower.endsWith('.txt') ||
|
||||
lower.endsWith('.csv') ||
|
||||
mime === 'application/pdf' ||
|
||||
lower.endsWith('.pdf') ||
|
||||
mime ===
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ||
|
||||
lower.endsWith('.docx')
|
||||
);
|
||||
}
|
||||
|
||||
// 첨부 문서에서 평문 텍스트를 추출한다(업로드 시 1회). 추출 불가 형식/실패/빈 결과는 null.
|
||||
// 지원 형식: txt·csv(직접 읽기), pdf(pdf-parse), docx(mammoth).
|
||||
// 그 외(이미지·xlsx·ppt·압축 등)는 추출하지 않고 null 을 반환한다.
|
||||
|
||||
@@ -24,7 +24,11 @@ import {
|
||||
decodeOriginalName,
|
||||
type UploadedDiskFile,
|
||||
} from '../task/upload.config';
|
||||
import { extractAttachmentText } from './extract-text';
|
||||
import {
|
||||
extractAttachmentText,
|
||||
isExtractableType,
|
||||
type AttachmentAiStatus,
|
||||
} from './extract-text';
|
||||
import { CreateProjectDto } from './dto/create-project.dto';
|
||||
import { UpdateProjectDto } from './dto/update-project.dto';
|
||||
|
||||
@@ -37,6 +41,8 @@ export interface ProjectAttachmentResponse {
|
||||
url: string;
|
||||
uploader: PublicUser | null;
|
||||
createdAt: string;
|
||||
// AI 추천 반영 상태 — 추출 본문은 노출하지 않고 상태 플래그만 제공
|
||||
aiStatus: AttachmentAiStatus;
|
||||
}
|
||||
|
||||
// 프로젝트 응답 형태 — 파생 라벨/진행률은 프론트에서 계산
|
||||
@@ -211,11 +217,15 @@ export class ProjectService {
|
||||
|
||||
// 첨부 목록(최신순) — 프로젝트 상세에 포함
|
||||
async listFiles(projectId: string): Promise<ProjectAttachmentResponse[]> {
|
||||
const rows = await this.attachmentRepo.find({
|
||||
where: { project: { id: projectId } },
|
||||
relations: ['uploader'],
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
// extractedText 는 select:false 이지만, AI 반영 상태 배지 산출을 위해 명시적으로 선택한다.
|
||||
// (본문은 응답에 담지 않고 상태 판별에만 사용)
|
||||
const rows = await this.attachmentRepo
|
||||
.createQueryBuilder('a')
|
||||
.leftJoinAndSelect('a.uploader', 'uploader')
|
||||
.addSelect('a.extractedText')
|
||||
.where('a.project_id = :projectId', { projectId })
|
||||
.orderBy('a.created_at', 'DESC')
|
||||
.getMany();
|
||||
return rows.map((a) => this.toAttachmentResponse(a, projectId));
|
||||
}
|
||||
|
||||
@@ -339,9 +349,19 @@ export class ProjectService {
|
||||
url: `/api/projects/${projectId}/files/${att.id}/download`,
|
||||
uploader: att.uploader ? UserService.toPublic(att.uploader) : null,
|
||||
createdAt: att.createdAt.toISOString(),
|
||||
aiStatus: this.deriveAiStatus(att),
|
||||
};
|
||||
}
|
||||
|
||||
// 첨부의 AI 반영 상태 산출 — 추출 본문 유무 + 형식으로 판별
|
||||
private deriveAiStatus(att: ProjectAttachment): AttachmentAiStatus {
|
||||
if (att.extractedText && att.extractedText.trim().length > 0) {
|
||||
return 'ready';
|
||||
}
|
||||
// 본문이 비어 있음 — 지원 형식이면 추출 실패, 아니면 미지원 형식
|
||||
return isExtractableType(att.mime, att.name) ? 'failed' : 'unsupported';
|
||||
}
|
||||
|
||||
// 프로젝트별 업무 완료/전체 수 — 진행률 집계용
|
||||
private async taskCountsByProject(
|
||||
ids: string[],
|
||||
|
||||
Reference in New Issue
Block a user