From 38b3762b2c9e05a9b59a9651437aac0c309879b9 Mon Sep 17 00:00:00 2001 From: TtiPo Date: Tue, 21 Jul 2026 00:24:14 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20=ED=97=A4=EB=8D=94=20AI=20=EC=B1=84?= =?UTF-8?q?=ED=8C=85=20=EC=96=B4=EC=8B=9C=EC=8A=A4=ED=84=B4=ED=8A=B8=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사용하지 않는 기능이라 헤더 버튼과 관련 로직을 모두 걷어낸다. 업무 생성 화면의 AI 할 일 추천(/ai/suggest-checklist)은 그대로 유지한다. - 프론트: AgentChatPanel, ai.store, 헤더 버튼·패널 마킹, useAi 의 대화 함수와 types/ai 의 대화 타입 제거. 채팅 전용이던 shared/utils/markdown.ts 도 함께 삭제 - 백엔드: AiController(대화 5개 엔드포인트), AiConversation·AiMessage 엔티티, SendMessageDto, AiService 의 채팅 로직 제거. AiModule 의 TaskModule 의존도 해제 - ai_conversations·ai_messages 테이블 삭제 마이그레이션 추가(down 은 초기 스키마 복원) - 문서: api-contract 의 채팅 API 절 삭제, integration-progress 8-6 은 이력이라 삭제 대신 제거됨 표기 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../1783900000000-DropAiConversations.ts | 35 + backend/src/modules/ai/ai.controller.ts | 97 --- backend/src/modules/ai/ai.module.ts | 17 +- backend/src/modules/ai/ai.service.ts | 279 +------ .../src/modules/ai/dto/send-message.dto.ts | 11 - .../ai/entities/ai-conversation.entity.ts | 33 - .../modules/ai/entities/ai-message.entity.ts | 30 - docs/api-contract.md | 17 +- docs/integration-progress.md | 7 +- frontend/src/components/AgentChatPanel.vue | 718 ------------------ frontend/src/composables/useAi.ts | 52 +- frontend/src/layouts/AppShell.vue | 41 - frontend/src/shared/utils/markdown.ts | Bin 3057 -> 0 bytes frontend/src/stores/ai.store.ts | 119 --- frontend/src/types/ai.ts | 27 - 15 files changed, 55 insertions(+), 1428 deletions(-) create mode 100644 backend/src/migrations/1783900000000-DropAiConversations.ts delete mode 100644 backend/src/modules/ai/ai.controller.ts delete mode 100644 backend/src/modules/ai/dto/send-message.dto.ts delete mode 100644 backend/src/modules/ai/entities/ai-conversation.entity.ts delete mode 100644 backend/src/modules/ai/entities/ai-message.entity.ts delete mode 100644 frontend/src/components/AgentChatPanel.vue delete mode 100644 frontend/src/shared/utils/markdown.ts delete mode 100644 frontend/src/stores/ai.store.ts diff --git a/backend/src/migrations/1783900000000-DropAiConversations.ts b/backend/src/migrations/1783900000000-DropAiConversations.ts new file mode 100644 index 0000000..836529b --- /dev/null +++ b/backend/src/migrations/1783900000000-DropAiConversations.ts @@ -0,0 +1,35 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +// AI 채팅(헤더 어시스턴트) 기능 제거 — 대화/메시지 테이블 삭제. +// 업무 생성 화면의 할 일 추천(/ai/suggest-checklist)은 DB 를 쓰지 않으므로 영향이 없다. +// down() 은 초기 스키마(CreateInitialSchema)의 정의를 그대로 되살린다(데이터는 복구되지 않음). +export class DropAiConversations1783900000000 implements MigrationInterface { + name = 'DropAiConversations1783900000000'; + + public async up(queryRunner: QueryRunner): Promise { + // 메시지 → 대화 순서로 FK 를 먼저 끊고 테이블을 지운다 + await queryRunner.query( + `ALTER TABLE "ai_messages" DROP CONSTRAINT "FK_de21fcb2d1df7fd6ca70f555b6d"`, + ); + await queryRunner.query( + `ALTER TABLE "ai_conversations" DROP CONSTRAINT "FK_12fdbf99ca0da93085d61edd3bb"`, + ); + await queryRunner.query(`DROP TABLE "ai_messages"`); + await queryRunner.query(`DROP TABLE "ai_conversations"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "ai_conversations" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "title" character varying(120) NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "user_id" uuid NOT NULL, CONSTRAINT "PK_60db12765b82858ba00c8aa4ae2" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "ai_messages" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "role" character varying NOT NULL, "content" text NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "conversation_id" uuid NOT NULL, CONSTRAINT "PK_a390434d4a515ba18a41bc996c2" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `ALTER TABLE "ai_conversations" ADD CONSTRAINT "FK_12fdbf99ca0da93085d61edd3bb" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "ai_messages" ADD CONSTRAINT "FK_de21fcb2d1df7fd6ca70f555b6d" FOREIGN KEY ("conversation_id") REFERENCES "ai_conversations"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + } +} diff --git a/backend/src/modules/ai/ai.controller.ts b/backend/src/modules/ai/ai.controller.ts deleted file mode 100644 index 3f17fc5..0000000 --- a/backend/src/modules/ai/ai.controller.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { - Body, - Controller, - DefaultValuePipe, - Delete, - Get, - HttpCode, - HttpStatus, - Param, - ParseIntPipe, - ParseUUIDPipe, - Post, - Query, - UseGuards, -} from '@nestjs/common'; -import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; -import { Throttle } from '@nestjs/throttler'; -import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; -import { CurrentUser } from '../auth/decorators/current-user.decorator'; -import type { PublicUser } from '../user/user.service'; -import type { PaginatedResult } from '../../common/pagination/pagination'; -import { - AiService, - type ConversationDetail, - type ConversationSummary, - type SendResult, -} from './ai.service'; -import { SendMessageDto } from './dto/send-message.dto'; - -// AI 채팅 컨트롤러 — 인증 필요. 대화는 본인 소유만 접근(서비스에서 스코프). -@ApiTags('AI') -@Controller('ai/conversations') -@UseGuards(JwtAuthGuard) -export class AiController { - constructor(private readonly aiService: AiService) {} - - @Get() - @ApiOperation({ summary: '내 대화 목록 (최근순, 페이지네이션)' }) - @ApiResponse({ status: 200, description: '목록 조회 성공' }) - list( - @CurrentUser() user: PublicUser, - @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number, - @Query('size', new DefaultValuePipe(20), ParseIntPipe) size: number, - ): Promise> { - return this.aiService.listConversations(user.id, page, size); - } - - @Get(':id') - @ApiOperation({ summary: '대화 상세 (메시지 포함)' }) - @ApiResponse({ status: 200, description: '조회 성공' }) - @ApiResponse({ status: 404, description: '대화 없음 (RES_001)' }) - get( - @CurrentUser() user: PublicUser, - @Param('id', ParseUUIDPipe) id: string, - ): Promise { - return this.aiService.getConversation(user.id, id); - } - - @Post() - @HttpCode(HttpStatus.CREATED) - @Throttle({ default: { limit: 1000, ttl: 60_000 } }) - @ApiOperation({ summary: '새 대화 시작 (첫 메시지)' }) - @ApiResponse({ status: 201, description: '생성 + AI 응답' }) - start( - @CurrentUser() user: PublicUser, - @Body() dto: SendMessageDto, - ): Promise { - return this.aiService.startConversation(user.id, dto.content); - } - - @Post(':id/messages') - @HttpCode(HttpStatus.OK) - @Throttle({ default: { limit: 1000, ttl: 60_000 } }) - @ApiOperation({ summary: '대화에 메시지 추가' }) - @ApiResponse({ status: 200, description: 'AI 응답' }) - @ApiResponse({ status: 404, description: '대화 없음 (RES_001)' }) - send( - @CurrentUser() user: PublicUser, - @Param('id', ParseUUIDPipe) id: string, - @Body() dto: SendMessageDto, - ): Promise { - return this.aiService.addMessage(user.id, id, dto.content); - } - - @Delete(':id') - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: '대화 삭제' }) - @ApiResponse({ status: 200, description: '삭제 성공' }) - @ApiResponse({ status: 404, description: '대화 없음 (RES_001)' }) - async remove( - @CurrentUser() user: PublicUser, - @Param('id', ParseUUIDPipe) id: string, - ): Promise { - await this.aiService.deleteConversation(user.id, id); - return null; - } -} diff --git a/backend/src/modules/ai/ai.module.ts b/backend/src/modules/ai/ai.module.ts index 9077339..9cf3aca 100644 --- a/backend/src/modules/ai/ai.module.ts +++ b/backend/src/modules/ai/ai.module.ts @@ -1,22 +1,13 @@ import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { TaskModule } from '../task/task.module'; import { ProjectModule } from '../project/project.module'; -import { AiController } from './ai.controller'; import { AiSuggestController } from './ai-suggest.controller'; import { AiService } from './ai.service'; -import { AiConversation } from './entities/ai-conversation.entity'; -import { AiMessage } from './entities/ai-message.entity'; -// AI 모듈 — Claude(Anthropic) 채팅 프록시 + 대화/메시지 영속. -// Task/ProjectModule 을 가져와 사용자 업무·프로젝트 컨텍스트를 시스템 프롬프트에 주입한다. +// AI 모듈 — Claude(Anthropic) 기반 업무 할 일(체크리스트) 추천. +// ProjectModule 을 가져와 프로젝트 정보·첨부 문서 텍스트를 프롬프트에 주입한다. @Module({ - imports: [ - TypeOrmModule.forFeature([AiConversation, AiMessage]), - TaskModule, - ProjectModule, - ], - controllers: [AiController, AiSuggestController], + imports: [ProjectModule], + controllers: [AiSuggestController], providers: [AiService], }) export class AiModule {} diff --git a/backend/src/modules/ai/ai.service.ts b/backend/src/modules/ai/ai.service.ts index 54ea142..23b018e 100644 --- a/backend/src/modules/ai/ai.service.ts +++ b/backend/src/modules/ai/ai.service.ts @@ -1,41 +1,13 @@ import { HttpStatus, Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; import { BusinessException } from '../../common/exceptions/business.exception'; -import { - paginated, - resolvePage, - type PaginatedResult, -} from '../../common/pagination/pagination'; -import { TaskService } from '../task/task.service'; -import type { TaskStatus } from '../task/entities/task.entity'; import { ProjectService } from '../project/project.service'; -import { AiConversation } from './entities/ai-conversation.entity'; -import { AiMessage } from './entities/ai-message.entity'; // Anthropic Messages API 응답(필요 필드만) interface AnthropicResponse { content?: { type: string; text?: string }[]; } -// 외부 노출 형태 -export interface ConversationSummary { - id: string; - title: string; - updatedAt: Date; -} -export interface ConversationDetail { - id: string; - title: string; - messages: { role: 'user' | 'assistant'; content: string }[]; -} -export interface SendResult { - conversationId: string; - title: string; - reply: string; -} - // 할 일 추천 — 카테고리별 항목 export interface ChecklistSuggestionGroup { category: string; @@ -46,36 +18,6 @@ const ANTHROPIC_URL = 'https://api.anthropic.com/v1/messages'; // AI 요청 타임아웃 기본값(ms) — env AI_TIMEOUT_MS 로 재정의. 프론트와 동일 값을 사용한다. const DEFAULT_AI_TIMEOUT_MS = 60_000; const MAX_TOKENS = 1024; -const TITLE_MAX = 60; -// 컨텍스트로 주입할 담당 업무 최대 건수(토큰 절약) -const CONTEXT_TASK_LIMIT = 30; -// 컨텍스트로 주입할 프로젝트 최대 개수 -const CONTEXT_PROJECT_LIMIT = 30; - -// 주제 한정(가드레일) + 데이터 근거 지시 -const SYSTEM_PROMPT = [ - '당신은 업무 협업 도구 "Relay" 의 AI 어시스턴트입니다.', - '', - '[역할]', - "- 업무 관리, 일정·마감, 협업, Relay 사용법, 그리고 아래 '사용자 데이터' 에 관한 질문에 답합니다.", - '- 업무와 무관한 주제(일반 상식, 시사, 번역, 코딩 과제, 잡담 등)는 정중히 거절하고 업무 관련 질문을 하도록 안내하세요.', - '', - '[규칙]', - '- 한국어로 간결하고 명확하게 답하세요. 불필요한 인사말·사과·이모지 남발을 피하고, 과도한 존칭("회원님" 등) 대신 자연스러운 존댓말을 쓰세요.', - '- 목록이나 강조가 필요하면 마크다운(굵게 **텍스트**, 목록 - 또는 1.)을 사용하세요. 표·복잡한 서식은 피하세요.', - "- 아래 '사용자 데이터' 에 근거해 답하고, 데이터에 없는 내용은 추측하지 말고 모른다고 하세요.", - "- 마감/일정은 제공된 '오늘 날짜' 를 기준으로 계산하세요.", - '- 프로젝트는 한글 표시 제목으로 설명하세요.', -].join('\n'); - -// 업무 상태 → 한국어 라벨(컨텍스트 표기용) -const STATUS_LABEL: Record = { - todo: '할 일', - prog: '진행 중', - review: '검토 대기', - changes: '수정 요청', - done: '완료', -}; // 할 일 추천용 시스템 프롬프트 — JSON 으로만 응답하도록 강제 const SUGGEST_SYSTEM_PROMPT = [ @@ -95,7 +37,7 @@ const SUGGEST_SYSTEM_PROMPT = [ '{"groups":[{"category":"필수 기능","items":["..."]},{"category":"관리 기능","items":["..."]},{"category":"보안 기능","items":["..."]},{"category":"부가 기능","items":["..."]}]}', ].join('\n'); -// AI(Claude) 채팅 서비스 — 대화/메시지를 DB 에 보관하고 Anthropic Messages API 를 호출. +// AI(Claude) 서비스 — 업무 생성 화면의 할 일(체크리스트) 추천을 담당한다. // 키(CLAUDE_API_KEY)는 서버 env 에만 두고 클라이언트에 노출하지 않는다. @Injectable() export class AiService { @@ -106,12 +48,7 @@ export class AiService { constructor( config: ConfigService, - private readonly taskService: TaskService, private readonly projectService: ProjectService, - @InjectRepository(AiConversation) - private readonly convRepo: Repository, - @InjectRepository(AiMessage) - private readonly msgRepo: Repository, ) { this.apiKey = (config.get('CLAUDE_API_KEY') ?? '').trim(); this.model = ( @@ -121,7 +58,7 @@ export class AiService { const t = Number(config.get('AI_TIMEOUT_MS')); this.timeoutMs = Number.isFinite(t) && t > 0 ? t : DEFAULT_AI_TIMEOUT_MS; if (!this.apiKey) { - this.logger.warn('CLAUDE_API_KEY 미설정 — AI 채팅 비활성화'); + this.logger.warn('CLAUDE_API_KEY 미설정 — AI 추천 비활성화'); } } @@ -129,69 +66,6 @@ export class AiService { return this.apiKey.length > 0; } - // 내 대화 목록(최근순, 페이지네이션) - async listConversations( - userId: string, - page?: number, - size?: number, - ): Promise> { - const params = resolvePage(page, size); - const [rows, total] = await this.convRepo.findAndCount({ - where: { user: { id: userId } }, - order: { updatedAt: 'DESC' }, - skip: params.skip, - take: params.take, - }); - return paginated( - rows.map((c) => ({ id: c.id, title: c.title, updatedAt: c.updatedAt })), - total, - params, - ); - } - - // 대화 상세(본인 소유만) — 메시지 시간순 - async getConversation( - userId: string, - conversationId: string, - ): Promise { - const conv = await this.getOwnedConversation(userId, conversationId); - const messages = await this.msgRepo.find({ - where: { conversation: { id: conv.id } }, - order: { createdAt: 'ASC' }, - }); - return { - id: conv.id, - title: conv.title, - messages: messages.map((m) => ({ role: m.role, content: m.content })), - }; - } - - // 새 대화 시작 — 첫 메시지로 대화 생성 후 응답 - async startConversation( - userId: string, - content: string, - ): Promise { - this.ensureEnabled(); - const conv = await this.convRepo.save( - this.convRepo.create({ - user: { id: userId }, - title: this.makeTitle(content), - }), - ); - return this.appendAndReply(conv, content, userId); - } - - // 기존 대화에 메시지 추가 후 응답(본인 소유만) - async addMessage( - userId: string, - conversationId: string, - content: string, - ): Promise { - this.ensureEnabled(); - const conv = await this.getOwnedConversation(userId, conversationId); - return this.appendAndReply(conv, content, userId); - } - // 할 일(체크리스트) 추천 — 프로젝트 설명 + 업무 제목/내용을 바탕으로 카테고리별 항목 제안 async suggestChecklist( userId: string, @@ -322,91 +196,6 @@ export class AiService { ); } - // 대화 삭제(본인 소유만) — 메시지 CASCADE - async deleteConversation( - userId: string, - conversationId: string, - ): Promise { - const conv = await this.getOwnedConversation(userId, conversationId); - await this.convRepo.remove(conv); - } - - // 사용자 메시지 저장 → 전체 이력으로 Claude 호출 → 응답 저장 → 대화 updatedAt 갱신. - // Claude 실패 시 방금 저장한 사용자 메시지를 롤백(빈 대화면 대화도 삭제)해 고아 데이터 방지. - private async appendAndReply( - conv: AiConversation, - content: string, - userId: string, - ): Promise { - const userMsg = await this.msgRepo.save( - this.msgRepo.create({ - conversation: { id: conv.id }, - role: 'user', - content, - }), - ); - const history = await this.msgRepo.find({ - where: { conversation: { id: conv.id } }, - order: { createdAt: 'ASC' }, - }); - - let reply: string; - try { - const system = await this.buildSystemPrompt(userId); - reply = await this.callClaude( - history.map((m) => ({ role: m.role, content: m.content })), - system, - ); - } catch (e) { - // 롤백 — 방금 사용자 메시지 제거, 남은 메시지가 없으면(첫 메시지였다면) 대화도 삭제 - await this.msgRepo.delete(userMsg.id); - const remaining = await this.msgRepo.count({ - where: { conversation: { id: conv.id } }, - }); - if (remaining === 0) { - await this.convRepo.delete(conv.id); - } - throw e; - } - - await this.msgRepo.save( - this.msgRepo.create({ - conversation: { id: conv.id }, - role: 'assistant', - content: reply, - }), - ); - // 최근 대화로 정렬되도록 updatedAt 갱신(@UpdateDateColumn) - await this.convRepo.save(conv); - return { conversationId: conv.id, title: conv.title, reply }; - } - - // 소유권 확인 — 본인 대화가 아니면 404(존재 비노출) - private async getOwnedConversation( - userId: string, - conversationId: string, - ): Promise { - const conv = await this.convRepo.findOne({ - where: { id: conversationId, user: { id: userId } }, - }); - if (!conv) { - throw new BusinessException( - 'RES_001', - '대화를 찾을 수 없습니다.', - HttpStatus.NOT_FOUND, - ); - } - return conv; - } - - private makeTitle(content: string): string { - const firstLine = content.trim().split('\n')[0].trim(); - if (!firstLine) return '새 대화'; - return firstLine.length > TITLE_MAX - ? `${firstLine.slice(0, TITLE_MAX)}…` - : firstLine; - } - private ensureEnabled(): void { if (!this.enabled) { throw new BusinessException( @@ -417,70 +206,6 @@ export class AiService { } } - // 시스템 프롬프트 = 가드레일 + 현재 사용자 데이터 컨텍스트 - private async buildSystemPrompt(userId: string): Promise { - const context = await this.buildUserContext(userId); - return `${SYSTEM_PROMPT}\n\n--- 사용자 데이터 ---\n${context}`; - } - - // 현재 사용자의 담당/지시 업무를 요약해 컨텍스트 문자열로 — 실패해도 채팅은 진행 - private async buildUserContext(userId: string): Promise { - const today = new Date(Date.now() + 9 * 60 * 60 * 1000) - .toISOString() - .slice(0, 10); // KST 기준 오늘 - try { - const assigned = await this.taskService.listAssigned( - userId, - 1, - CONTEXT_TASK_LIMIT, - ); - const issued = await this.taskService.listIssued(userId, 1, 1); - const projects = await this.projectService.findAll( - 1, - CONTEXT_PROJECT_LIMIT, - ); - - const lines = [`오늘 날짜: ${today} (KST)`, '']; - - // 담당 업무 - lines.push(`내가 담당한 업무 (${assigned.total}건):`); - if (assigned.items.length === 0) { - lines.push('- (없음)'); - } else { - for (const t of assigned.items) { - const due = t.dueDate ? t.dueDate.slice(0, 10) : '없음'; - const [done, totalC] = t.checklist; - lines.push( - `- [${t.projectName}] #${t.id} ${t.title} — 상태: ${STATUS_LABEL[t.status]}, 마감: ${due}, 체크리스트: ${done}/${totalC}`, - ); - } - if (assigned.total > assigned.items.length) { - lines.push(`- … 외 ${assigned.total - assigned.items.length}건`); - } - } - lines.push('', `내가 지시한 업무: ${issued.total}건`); - - // 프로젝트 목록(인증 사용자는 전체 열람 가능) - lines.push('', `프로젝트 목록 (${projects.total}개):`); - if (projects.items.length === 0) { - lines.push('- (없음)'); - } else { - for (const p of projects.items) { - lines.push(`- ${p.name} — 업무 ${p.doneCount}/${p.totalCount}건`); - } - if (projects.total > projects.items.length) { - lines.push(`- … 외 ${projects.total - projects.items.length}개`); - } - } - return lines.join('\n'); - } catch (e) { - this.logger.warn( - `사용자 컨텍스트 조회 실패: ${e instanceof Error ? e.message : String(e)}`, - ); - return `오늘 날짜: ${today} (KST)\n(사용자 업무 데이터를 불러오지 못했습니다.)`; - } - } - // Anthropic Messages API 호출 — 실패 시 내부/키 노출 없이 일반 문구 private async callClaude( messages: { role: 'user' | 'assistant'; content: string }[], diff --git a/backend/src/modules/ai/dto/send-message.dto.ts b/backend/src/modules/ai/dto/send-message.dto.ts deleted file mode 100644 index 11c863c..0000000 --- a/backend/src/modules/ai/dto/send-message.dto.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { IsNotEmpty, IsString, MaxLength } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; - -// 메시지 전송 — 대화 생성/이어가기 공통. 이력은 서버가 DB 에서 불러온다. -export class SendMessageDto { - @ApiProperty({ description: '사용자 메시지 내용' }) - @IsString() - @IsNotEmpty({ message: '메시지 내용을 입력해 주세요.' }) - @MaxLength(8000, { message: '메시지가 너무 깁니다.' }) - content!: string; -} diff --git a/backend/src/modules/ai/entities/ai-conversation.entity.ts b/backend/src/modules/ai/entities/ai-conversation.entity.ts deleted file mode 100644 index 1db268e..0000000 --- a/backend/src/modules/ai/entities/ai-conversation.entity.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { - Column, - CreateDateColumn, - Entity, - JoinColumn, - ManyToOne, - PrimaryGeneratedColumn, - UpdateDateColumn, - type Relation, -} from 'typeorm'; -import { User } from '../../user/entities/user.entity'; - -// AI 대화 — 사용자별 채팅 스레드. 메시지는 AiMessage 로 분리(1:N). -@Entity('ai_conversations') -export class AiConversation { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @ManyToOne(() => User, { onDelete: 'CASCADE', nullable: false }) - @JoinColumn({ name: 'user_id' }) - user!: Relation; - - // 첫 사용자 메시지로 자동 생성되는 제목 - @Column({ type: 'varchar', length: 120 }) - title!: string; - - @CreateDateColumn({ name: 'created_at' }) - createdAt!: Date; - - // 새 메시지 추가 시 갱신(최근 대화 정렬용) - @UpdateDateColumn({ name: 'updated_at' }) - updatedAt!: Date; -} diff --git a/backend/src/modules/ai/entities/ai-message.entity.ts b/backend/src/modules/ai/entities/ai-message.entity.ts deleted file mode 100644 index 3a5f395..0000000 --- a/backend/src/modules/ai/entities/ai-message.entity.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { - Column, - CreateDateColumn, - Entity, - JoinColumn, - ManyToOne, - PrimaryGeneratedColumn, - type Relation, -} from 'typeorm'; -import { AiConversation } from './ai-conversation.entity'; - -// AI 대화 메시지 1건 — 대화에 종속(CASCADE). -@Entity('ai_messages') -export class AiMessage { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @ManyToOne(() => AiConversation, { onDelete: 'CASCADE', nullable: false }) - @JoinColumn({ name: 'conversation_id' }) - conversation!: Relation; - - @Column({ type: 'varchar' }) - role!: 'user' | 'assistant'; - - @Column({ type: 'text' }) - content!: string; - - @CreateDateColumn({ name: 'created_at' }) - createdAt!: Date; -} diff --git a/docs/api-contract.md b/docs/api-contract.md index bc13b89..847bd86 100644 --- a/docs/api-contract.md +++ b/docs/api-contract.md @@ -336,26 +336,19 @@ todo ─시작→ prog ─승인요청→ review ─승인→ done Google/Kakao OAuth(passport, 키 미설정 시 자동 비활성) + 이메일 가입 인증(미인증 로그인 차단, 그랜드페더링). 메일 발송은 `MailService` 플레이스홀더(백엔드 로그). 상세는 §1 참조. -### AI 채팅 (`/api/ai/conversations`) — **✅ 완료 (DB 영속, 다중 대화)** +### AI 할 일 추천 (`/api/ai`) — **✅ 완료** | 메서드 | 경로 | 요청 | 응답 | 비고 | |---|---|---|---|---| -| GET | `/ai/conversations` | `?page&size` | `{items:[{id,title,updatedAt}],total,...}` | 내 대화 목록(최근순) | -| GET | `/ai/conversations/:id` | — | `{id,title,messages:[{role,content}]}` | 대화 상세(본인 소유만) | -| POST | `/ai/conversations` | `{ content }` | `{ conversationId, title, reply }` | 새 대화 시작(첫 메시지). throttle 20/분 | -| POST | `/ai/conversations/:id/messages` | `{ content }` | `{ conversationId, title, reply }` | 메시지 추가. throttle 20/분 | -| DELETE | `/ai/conversations/:id` | — | `null` | 대화 삭제(메시지 CASCADE) | | POST | `/ai/suggest-checklist` | `{ repoId, title, content? }` | `{ groups: [{category, items[]}] }` | 업무 작성 시 할 일(체크리스트) 추천. throttle 20/분 | -- **대화/메시지를 DB 보관**(`AiConversation`·`AiMessage`, user CASCADE). 모든 접근은 **본인 소유 스코프**(타 사용자 대화는 404=RES_001, IDOR 차단). 클라는 "이 대화에 새 메시지 1건"만 보내고 **서버가 DB 이력으로 Claude 호출**(이전의 클라 전체-이력 전송 방식 폐기). -- `AiService` 가 Anthropic Messages API(`x-api-key`) 호출 — **키(`CLAUDE_API_KEY`)는 서버 env 에만**. 모델 `CLAUDE_MODEL`(기본 `claude-sonnet-4-6`). 미설정 503 / 외부오류 502(내부·키 비노출). **Claude 실패 시 방금 사용자 메시지·빈 대화 롤백**(고아 데이터 방지). -- **주제 한정 + 데이터 인지(시스템 프롬프트)**: 업무/Relay 관련만 답하고 무관한 주제는 정중히 거절(가드레일, 프롬프트 레벨·소프트). 매 요청 시 **오늘 날짜(KST) + 담당 업무(최대 30건: 제목/상태/마감/체크리스트) + 지시 업무 수 + 저장소 목록(최대 30개: 이름/공개범위/업무수)** 을 시스템 프롬프트에 주입(`TaskService.listAssigned/listIssued` + `RepoService.findAll`). "내 마감 임박 업무"·"저장소 뭐 있어" 같은 질문에 실데이터로 응답. 조회 실패해도 채팅은 진행(베스트에포트). -- 프론트: `components/AgentChatPanel.vue`(우측 슬라이드, **채팅 ↔ 지난 대화 목록** 전환·열기·삭제), 버튼은 **AppShell 헤더 상시 노출(모든 화면)**. `stores/ai.store` 가 목록·현재 대화 관리 → **DB 영속이라 다른 기기·재로그인에도 기록 유지**. AI 응답은 **마크다운 렌더**(`shared/utils/markdown.ts` — escapeHtml 후 신뢰 태그만 조립, 외부 의존성 0, XSS 안전: 굵게/목록/코드/제목/링크). -- **할 일 추천**(`POST /ai/suggest-checklist`): `AiService.suggestChecklist` 가 저장소 표시제목/설명 + 업무 제목/내용을 Claude 에 보내 **JSON(카테고리별 항목)** 으로 받아 파싱(코드펜스/잡텍스트 방어, 빈 결과 502). 프론트 **TaskCreatePage**: "취소 / **AI 추천** / 지시 보내기" 버튼 + 체크리스트 아래 추천 패널(카테고리별 항목 클릭 추가·전체 추가·중복 스킵). +- `AiService` 가 Anthropic Messages API(`x-api-key`) 호출 — **키(`CLAUDE_API_KEY`)는 서버 env 에만**. 모델 `CLAUDE_MODEL`(기본 `claude-sonnet-4-6`). 미설정 503 / 외부오류 502(내부·키 비노출). +- **할 일 추천**(`POST /ai/suggest-checklist`): `AiService.suggestChecklist` 가 저장소 표시제목/설명 + 업무 제목/내용(+첨부 문서 추출 텍스트)을 Claude 에 보내 **JSON(카테고리별 항목)** 으로 받아 파싱(코드펜스/잡텍스트 방어, 빈 결과 502). 프론트 **TaskCreatePage**: "취소 / **AI 추천** / 지시 보내기" 버튼 + 체크리스트 아래 추천 패널(카테고리별 항목 클릭 추가·전체 추가·중복 스킵). +- DB 를 쓰지 않는다(무상태). 헤더의 AI 채팅 어시스턴트는 제거되었으며 대화/메시지 테이블도 삭제됨(`DropAiConversations1783900000000`). ### 그 외(미착수) -메일 실제 발송(SMTP) — 현재 로그 대체. 보안 후속(계정잠금/CAPTCHA 등). (페이지네이션·실시간 알림·Redis 캐싱·소셜로그인·이메일인증·refresh 회전·AI 채팅은 완료.) +메일 실제 발송(SMTP) — 현재 로그 대체. 보안 후속(계정잠금/CAPTCHA 등). (페이지네이션·실시간 알림·Redis 캐싱·소셜로그인·이메일인증·refresh 회전은 완료.) --- diff --git a/docs/integration-progress.md b/docs/integration-progress.md index f641bf8..028da5e 100644 --- a/docs/integration-progress.md +++ b/docs/integration-progress.md @@ -309,7 +309,12 @@ - **L6** CORS `localhost:3000`·Swagger `/api-docs` 비운영 한정. 검증: 백엔드 build/lint 0·20 tests, 프론트 변경 없음. -#### 8-6. AI 채팅 패널 ✅ (2026-06-19) +#### 8-6. AI 채팅 패널 — **🗑 제거됨 (2026-07-21)** + +> **이 절은 과거 이력이다.** 헤더의 AI 채팅 어시스턴트는 "사용하지 않는다"는 판단으로 전부 제거되었다. +> 제거 범위: `AgentChatPanel.vue`·`stores/ai.store.ts`·`shared/utils/markdown.ts`·`AiController`·`AiConversation`/`AiMessage` 엔티티·`SendMessageDto`·`AiService` 의 채팅 로직, +> DB 테이블은 `DropAiConversations1783900000000` 마이그레이션으로 삭제. +> **업무 생성 화면의 AI 할 일 추천(`POST /ai/suggest-checklist`)은 그대로 유지된다** — 아래 설명 중 채팅 관련 내용만 폐기된 것이다. **지시**: 기존 목업 UI(가짜 "relay order draft" 터미널)는 버리고, **Claude API로 채팅만** 되도록. 키는 백엔드 env(`CLAUDE_API_KEY`)에 등록됨. diff --git a/frontend/src/components/AgentChatPanel.vue b/frontend/src/components/AgentChatPanel.vue deleted file mode 100644 index ee1e55b..0000000 --- a/frontend/src/components/AgentChatPanel.vue +++ /dev/null @@ -1,718 +0,0 @@ - - -