# Q_Table 리팩토링 완료 요약

## 📅 작업 일자

- 2025-10-29: Version 2.0 (36 states)
- 2025-11-10: **Version 3.0 (162 states)** - 비즈니스 용어 적용 및 State 재설계

## ✅ Version 3.0 주요 변경사항

### State 공간 재설계 (36 → 162 states)

**기존 Version 2.0:**

- State = (Scenario, PriceZone, AcceptanceRate)
- 총 상태 수: 4 × 3 × 3 = **36 states**

**신규 Version 3.0:**

- State = (매출액 가격구간, 유통 구조, 파트너사 종류, 가격 수용률 구간, 입력 금액 구간)
- 총 상태 수: 3 × 3 × 3 × 3 × 2 = **162 states**

| 순번 | 변수                 | 구분 | 설명                                                       |
| :--- | :------------------- | :--- | :--------------------------------------------------------- |
| 1    | **매출액 가격구간**  | 3개  | Low (≤1,000만원), Mid (1,000~3,000만원), High (>3,000만원) |
| 2    | **유통 구조**        | 3개  | 제조, 총판, 유통                                           |
| 3    | **파트너사 종류**    | 3개  | Single (단독), Multiple (다수), None (없음)                |
| 4    | **가격 수용률 구간** | 3개  | Low (<30%), Mid (30~90%), High (>90%)                      |
| 5    | **입력 금액 구간**   | 2개  | PZ1 (A ≤ P ≤ T), PZ2 (P > T)                               |

### 동적 가중치 (W) 설계 업데이트

**기존 Version 2.0:**

```
W = (scenario.weight + price_zone.weight) / 2.0
```

**신규 Version 3.0:**

```
W_raw = w1×S_amount + w2×S_dist + w3×S_partner + w4×S_accept + w5×S_pricezone
W = clip(W_raw, 0.2, 0.8)
```

기본 가중치 계수:

- w1 = 0.20 (매출액 가격구간)
- w2 = 0.25 (유통 구조)
- w3 = 0.20 (파트너사 종류)
- w4 = 0.25 (가격 수용률)
- w5 = 0.10 (입력 금액 구간)

## ✅ 완료된 작업 (Version 2.0)

### 1. 새로운 핵심 컴포넌트 구현

#### **QTable** (`domain/model/q_table.py`)

- ✅ 동적 `action_space_size` 지원 (21개 카드)
- ✅ Q-Learning 업데이트 메서드
- ✅ 직렬화/역직렬화 지원
- ✅ 인덱스 유효성 검증

#### **EpisodePolicy** (`domain/agents/policy.py`)

- ✅ 하드코딩된 크기(9) → 동적 크기로 변경
- ✅ 생성자에서 `action_space_size` 주입
- ✅ `get_action_mask()` 동적 크기 적용

#### **State** (`domain/model/state.py`)

- ✅ `to_index()` 메서드 추가 (State → 1D 인덱스)
- ✅ `from_index()` 메서드 추가 (1D 인덱스 → State)

#### **ActionCardMapper** (`integration/action_card_mapper.py`) 🆕

- ✅ action_id ↔ card_id 양방향 매핑
- ✅ JSON 파일 기반 설정
- ✅ 유틸리티 메서드 제공

#### **action_card_mapping.json** (`integration/data/`) 🆕

- ✅ 21개 카드 매핑 (0-20 → "no_0" ~ "no_20")

#### **GetBestActionUsecase** (`usecase/get_best_action_usecase.py`)

- ✅ State → action_id → card_id 전체 플로우
- ✅ Policy 사용 여부 선택 가능
- ✅ Top-K 추천 기능
- ✅ 사용 가능한 액션/카드 조회

#### **CollectExperienceUsecase** (`usecase/collect_experience_usecase.py`) 🆕

- ✅ 협상 중 Experience 수집
- ✅ 에피소드 단위 관리
- ✅ JSONL 형식 자동 저장
- ✅ 수집 정보 조회

#### **TrainOfflineUsecase** (`usecase/train_offline_usecase.py`) 🆕

- ✅ 저장된 Experience로 Q-Table 학습
- ✅ 배치 학습 및 에포크 반복
- ✅ 특정 에피소드 선택 학습
- ✅ 성능 평가 기능

### 2. 레거시 코드 정리

#### 제거된 파일

```
❌ domain/action_space.py
❌ domain/model/action.py
❌ domain/constants.py
❌ domain/spaces.py
```

#### Legacy로 이동된 파일 (참고용 보관)

```
📦 legacy/environment.py
📦 legacy/calculate_reward_usecase.py
📦 legacy/evaluate_agent_usecase.py
📦 legacy/execute_step_usecase.py
📦 legacy/get_q_value_usecase.py
📦 legacy/get_state_info_usecase.py
📦 legacy/initialize_env_usecase.py
📦 legacy/load_q_table_usecase.py
📦 legacy/train_agent_usecase.py
📦 legacy/update_q_table_usecase.py
```

## 📁 최종 디렉토리 구조

```
negotiation_agent/
│
├── card_management/                    # 카드 관리 (DB 기반)
│   ├── domain/
│   │   ├── model/
│   │   │   └── nego_card.py           ✅ DB 엔티티
│   │   ├── repository/
│   │   │   ├── nego_card_repository.py
│   │   │   └── nego_card_script_repository.py  ✅ JSON CRUD
│   │   └── value/
│   │       └── nego_card_types.py     ✅ 6가지 atomic elements
│   └── data/
│       └── nego_card_scripts.json     ✅ 21개 카드 스크립트
│
├── Q_Table/                            # Q-Learning (추상 action_id)
│   ├── domain/
│   │   ├── model/
│   │   │   ├── state.py               ✅ to_index() 추가
│   │   │   ├── q_table.py             ✅ 새로 생성
│   │   │   └── experience.py          ✅ 새로 생성
│   │   ├── agents/
│   │   │   ├── policy.py              ✅ 동적 크기 수정
│   │   │   └── offline_agent.py
│   │   ├── repository/
│   │   │   ├── action_repository.py
│   │   │   └── experience_repository.py  ✅ 새로 생성
│   │   └── service/
│   │       └── state_calculator.py
│   ├── usecase/
│   │   ├── get_best_action_usecase.py    ✅ 완전 재작성
│   │   ├── collect_experience_usecase.py ✅ 새로 생성
│   │   └── train_offline_usecase.py      ✅ 새로 생성
│   ├── infra/
│   │   ├── data_collector.py
│   │   └── gym/
│   │       └── env_wrapper.py
│   ├── data/                           🆕 데이터 저장소
│   │   └── experiences/
│   │       └── *.jsonl
│   └── legacy/                         📦 레거시 코드 보관
│       ├── README.md
│       └── ... (10개 파일)
│
└── integration/                        🆕 매핑 레이어
    ├── action_card_mapper.py          ✅
    └── data/
        └── action_card_mapping.json   ✅
```

## 🎯 핵심 설계 원칙

### 1. 명확한 책임 분리

- **Q_Table**: action_id (0~20)만 다룸
- **Card_Management**: card_id ("no_0"~"no_20")만 다룸
- **ActionCardMapper**: 둘을 연결하는 단일 책임

### 2. 동적 확장성

- 카드 추가/삭제 시 `action_card_mapping.json`만 수정
- Q-Table과 Policy가 자동으로 새 크기에 대응

### 3. 유지보수성

- 레거시 코드는 legacy/ 폴더에 보관
- 새로운 코드와 명확히 분리

## 🔄 사용 예시

### 1. 추론 (Inference)

```python
from negotiation_agent.Q_Table.domain.model.q_table import QTable
from negotiation_agent.Q_Table.domain.model.visit_table import VisitTable
from negotiation_agent.Q_Table.domain.agents.policy import UCBPolicy
from negotiation_agent.integration.action_card_mapper import ActionCardMapper
from negotiation_agent.Q_Table.usecase.get_best_action_usecase import GetBestActionUsecase

# 매퍼 로드
mapper = ActionCardMapper()
action_space_size = mapper.get_action_space_size()  # 21

# Q-Table 생성 (Version 3.0: 162 states)
q_table = QTable(
    state_space_size=162,  # 3 x 3 x 3 x 3 x 2
    action_space_size=action_space_size  # 21
)

# Policy 생성
visit_table = VisitTable(state_space_size=162, action_space_size=action_space_size)
policy = UCBPolicy(visit_table=visit_table)

# Usecase 생성
usecase = GetBestActionUsecase(
    q_table=q_table,
    policy=policy,
    action_card_mapper=mapper
)

# 협상 추론 (Version 3.0)
from negotiation_agent.Q_Table.domain.model.state import (
    State,
    RevenueRange,
    DistributionStructure,
    PartnerType,
    AcceptanceRate,
    InputPriceZone,
)

state = State(
    revenue_range=RevenueRange.MID,  # 1,000~3,000만원
    distribution=DistributionStructure.WHOLESALER,  # 총판
    partner_type=PartnerType.SINGLE,  # 단독 파트너
    acceptance_rate=AcceptanceRate.MID,  # 30~90%
    input_price_zone=InputPriceZone.PZ1,  # A≤P≤T
)

result = usecase.execute(state)
print(result)
# Output:
# {
#     'action_id': 5,
#     'card_id': 'no_5',
#     'q_value': 0.0,
#     'state_index': 1
# }
```

### 2. Experience 수집

```python
from negotiation_agent.Q_Table.domain.repository.experience_repository import ExperienceRepository
from negotiation_agent.Q_Table.usecase.collect_experience_usecase import CollectExperienceUsecase

# Repository 생성
exp_repo = ExperienceRepository()

# Usecase 생성
collect_usecase = CollectExperienceUsecase(exp_repo)

# 에피소드 시작
collect_usecase.start_episode("ep_001")

# 협상 진행 중...
for step in range(10):
    # 현재 상태
    current_state = State(...)

    # 액션 선택 (GetBestActionUsecase 사용)
    result = usecase.execute(current_state)
    action_id = result['action_id']

    # 액션 실행 후 보상과 다음 상태 받음
    reward = calculate_reward(...)  # 보상 계산
    next_state = get_next_state(...)  # 다음 상태
    done = check_done(...)  # 종료 여부

    # Experience 수집
    collect_usecase.collect(
        state=current_state,
        action_id=action_id,
        reward=reward,
        next_state=next_state,
        done=done
    )

    if done:
        break

# 에피소드 종료
collect_usecase.end_episode()

# 수집 정보 확인
info = collect_usecase.get_collection_info()
print(info)
# {
#     'total_experiences': 10,
#     'episodes': 1,
#     'current_episode': None
# }
```

### 3. 오프라인 학습

```python
from negotiation_agent.Q_Table.usecase.train_offline_usecase import TrainOfflineUsecase

# Usecase 생성
train_usecase = TrainOfflineUsecase(
    q_table=q_table,
    experience_repository=exp_repo
)

# 학습 실행
result = train_usecase.train(
    filename="experiences.jsonl",
    epochs=10,
    batch_size=32
)

print(result)
# {
#     'total_experiences': 1000,
#     'epochs': 10,
#     'updates': 10000,
#     'avg_loss': 0.05
# }

# 성능 평가
eval_result = train_usecase.evaluate(filename="experiences.jsonl")
print(eval_result)
# {
#     'avg_q_value': 0.5,
#     'avg_reward': 1.0,
#     'total_samples': 1000
# }
```

## 📋 향후 작업 (선택사항)

- [x] Experience 수집 Usecase 구현 ✅
- [x] 오프라인 학습 Usecase 구현 ✅
- [ ] Q-Table Repository 구현 (영속성)
- [ ] 카드 추가 시 Q-Table 자동 확장 기능
- [ ] 통계적 Q-value 초기화 기능

## 📚 참고 문서

- 설계 문서: `REFACTORING_GUIDE.md`
- 레거시 코드: `legacy/README.md`
