# AI 처리 파이프라인

## 1. 기본 방향

AI는 최종 결정권자가 아니다.

서버가 원본 입력을 저장하고, 후보 데이터를 정리한 뒤 AI에 분석을 요청한다. AI는 정해진 JSON으로 판단 결과를 반환하고, 서버는 그 결과를 검증한 뒤 DB에 반영한다.

즉, 전체 구조는 다음과 같다.

```text
입력 저장 -> 전처리 -> 후보 조회 -> AI 요청 -> JSON 검증 -> 업무 반영 -> 로그 저장
```

## 2. 전체 처리 흐름

1. 모바일에서 텍스트 또는 음성 입력을 받는다.
2. 서버가 `raw_inputs`에 원본 입력을 먼저 저장한다.
3. 음성 입력이면 모바일 브라우저 녹음 파일을 저장하고 음성 텍스트 변환을 수행한다.
4. 분석 대상 텍스트를 확정한다.
5. 서버가 담당자 후보와 기존 업무 후보를 조회한다.
6. 토큰 제한에 맞춰 AI 요청 payload를 구성한다.
7. AI에 분석 요청을 보낸다.
8. AI 응답 JSON을 파싱한다.
9. 서버에서 스키마, 상태값, 담당자, 업무 ID를 검증한다.
10. 자동 반영 가능하면 `tasks`와 `task_updates`에 저장한다.
11. 자동 반영이 어렵다면 확인 필요 상태로 저장한다.
12. `ai_analysis_logs`와 `ai_usage_logs`에 결과를 기록한다.

## 3. 서버가 AI에 보내는 데이터

AI에는 전체 DB를 보내지 않는다.

서버에서 필요한 정보만 추려서 보낸다.

```json
{
  "request_type": "task_analysis",
  "schema_version": "v1",
  "current_date": "2026-07-01",
  "timezone": "Asia/Seoul",
  "submitter": {
    "id": 1,
    "name": "관리자"
  },
  "input": {
    "raw_input_id": 1001,
    "input_type": "text",
    "text": "김민수 대리한테 내일까지 견적서 수정본 확인하라고 업무 등록해줘."
  },
  "users": [
    {
      "id": 12,
      "name": "김민수",
      "department": "영업팀",
      "position": "대리",
      "aliases": ["김대리", "민수"]
    }
  ],
  "task_candidates": [
    {
      "id": 101,
      "title": "거래처 견적서 수정",
      "assignee_id": 12,
      "status": "in_progress",
      "priority": "normal",
      "due_date": "2026-07-03",
      "last_update_summary": "단가 항목 수정 요청 반영 중",
      "updated_at": "2026-07-01 09:30:00"
    }
  ],
  "rules": {
    "allowed_statuses": ["new", "in_progress", "waiting", "review_required", "completed", "on_hold", "canceled"],
    "allowed_priorities": ["low", "normal", "high", "urgent"],
    "auto_apply_confidence_threshold": 0.85,
    "max_task_candidates": 10
  }
}
```

## 4. AI가 반환하는 데이터

AI는 반드시 `AI JSON Schema v1` 형식으로만 응답한다.

서버는 응답에 설명 문장이 섞여 있으면 실패로 처리한다.

```json
{
  "schema_version": "v1",
  "intent": "create_task",
  "confidence": 0.91,
  "needs_human_review": false,
  "review_reason": null,
  "matched_task_id": null,
  "matched_task_candidates": [],
  "task": {
    "title": "견적서 수정본 확인",
    "summary": "거래처 요청사항을 반영한 견적서 수정본 확인",
    "description": "김민수 담당자가 내일까지 견적서 수정본을 확인해야 한다.",
    "assignee_name": "김민수",
    "assignee_id": 12,
    "due_date": "2026-07-02",
    "priority": "normal",
    "status": "new"
  },
  "update": {
    "update_type": null,
    "note": null,
    "new_status": null,
    "changed_fields": {}
  },
  "report": {
    "include_in_daily_report": true,
    "report_summary": "김민수 담당자의 견적서 수정본 확인 업무가 신규 등록됨.",
    "risk_note": null
  }
}
```

## 5. 서버 검증 단계

AI 응답은 DB 반영 전에 반드시 서버에서 검증한다.

검증 항목은 다음과 같다.

- JSON 파싱 가능 여부
- `schema_version` 지원 여부
- `intent` 허용값 여부
- `confidence` 숫자 범위
- `needs_human_review` 값 여부
- `status` 허용값 여부
- `priority` 허용값 여부
- `assignee_id`가 실제 사용자 ID인지 여부
- `matched_task_id`가 실제 업무 ID인지 여부
- `due_date` 날짜 형식 여부
- 토큰 사용량 제한 초과 여부

검증 실패 시 업무에 자동 반영하지 않는다.

## 6. 자동 반영 정책

### 신규 업무 생성

조건:

- `intent`가 `create_task`다.
- `confidence`가 기준값 이상이다.
- `needs_human_review`가 `false`다.
- 담당자가 필요한 경우 `assignee_id`가 존재한다.

처리:

- `tasks`에 신규 업무를 생성한다.
- `task_updates`에 생성 이력을 남긴다.
- `raw_inputs.processing_status`를 `applied`로 변경한다.

### 기존 업무 업데이트

조건:

- `intent`가 `update_task`, `complete_task`, `assign_task`, `change_schedule` 중 하나다.
- `matched_task_id`가 존재한다.
- `confidence`가 기준값 이상이다.
- `needs_human_review`가 `false`다.

처리:

- `tasks`의 상태, 담당자, 마감일, 요약 등을 변경한다.
- `task_updates`에 변경 이력을 남긴다.
- 변경 전후 값은 `task_updates.change_json`에 저장한다.
- `raw_inputs.processing_status`를 `applied`로 변경한다.

## 7. 확인 필요 처리

다음 경우에는 자동 반영하지 않고 확인 필요로 보낸다.

- AI 응답 검증 실패
- 신뢰도 기준 미달
- 담당자 없음
- 기존 업무 후보가 여러 개
- 신규 업무와 업데이트 판단이 불명확
- 토큰 제한 초과
- 음성 변환 실패
- AI API 호출 실패

처리:

- `raw_inputs.processing_status`를 `review_required` 또는 `failed`로 변경한다.
- `ai_analysis_logs.needs_human_review`를 1로 저장한다.
- 가능하면 `review_reason`을 저장한다.
- 확인 필요 화면에서 사람이 최종 반영한다.

## 8. 실패 처리

### 음성 변환 실패

- 원본 음성 파일은 유지한다.
- `raw_inputs.processing_status`를 `failed`로 변경한다.
- 사용자는 직접 텍스트를 입력할 수 있다.

### 모바일 녹음 업로드 실패

- `raw_inputs`를 생성하지 않거나 `failed`로 저장한다.
- 업로드 오류 메시지를 기록한다.
- 사용자는 다시 녹음하거나 텍스트 입력으로 전환할 수 있다.

### AI 호출 실패

- 원본 입력은 유지한다.
- `raw_inputs.processing_status`를 `failed`로 변경한다.
- `ai_usage_logs.success`를 0으로 저장한다.
- 오류 메시지를 저장한다.

### JSON 파싱 실패

- AI 원본 응답을 로그에 저장한다.
- 자동 반영하지 않는다.
- 확인 필요로 보낸다.

## 9. 로그 저장

### ai_analysis_logs

AI 판단 내용 전체를 저장한다.

- 원본 입력 ID
- intent
- confidence
- matched_task_id
- matched_user_id
- 추출 상태값
- 추출 마감일
- 전체 JSON
- 검토 필요 여부
- 모델명
- 프롬프트 버전
- 스키마 버전

### ai_usage_logs

AI 호출 비용과 성능 분석을 위해 사용량을 저장한다.

- 기능명
- 모델명
- 프롬프트 버전
- 입력 토큰
- 출력 토큰
- 총 토큰
- 성공 여부
- 오류 메시지

## 10. 보안 원칙

- AI API 키는 브라우저에 노출하지 않는다.
- AI 호출은 반드시 서버에서만 수행한다.
- 사용자 전체 목록과 전체 업무 목록을 한 번에 보내지 않는다.
- 필요한 담당자 후보와 업무 후보만 보낸다.
- 민감한 개인정보는 AI 요청에서 제외하거나 최소화한다.
- AI 응답을 그대로 화면에 HTML로 출력하지 않는다.

## 11. Laravel 8 서비스 구조 초안

Laravel 8 기준으로 다음 서비스 계층을 둔다.

```text
InputService
- 원본 입력 저장
- 음성 파일 저장
- 처리 상태 변경

TranscriptionService
- 음성 텍스트 변환

VoiceRecordingService
- 모바일 녹음 파일 검증
- 음성 파일 저장
- 녹음 길이와 MIME 타입 확인

TaskCandidateService
- 담당자 후보 조회
- 기존 업무 후보 조회
- 후보 점수 계산

AiTaskAnalysisService
- AI 요청 payload 구성
- AI API 호출
- 응답 JSON 파싱

AiResponseValidator
- 스키마 검증
- 상태값 검증
- 담당자/업무 ID 검증

TaskApplyService
- 신규 업무 생성
- 기존 업무 업데이트
- 업무 이력 저장

AiLogService
- 분석 로그 저장
- 토큰 사용량 저장
```

권장 위치:

```text
app/Services/InputService.php
app/Services/TranscriptionService.php
app/Services/VoiceRecordingService.php
app/Services/TaskCandidateService.php
app/Services/AiTaskAnalysisService.php
app/Services/AiResponseValidator.php
app/Services/TaskApplyService.php
app/Services/AiLogService.php
```

## 12. 핵심 원칙

AI는 판단을 제안하고, 서버가 검증하고, DB가 최종 기록한다.

이 원칙을 지켜야 리포트가 흔들리지 않고, AI 오판이 발생해도 사람이 추적하고 수정할 수 있다.
