# 🤝 API & Module Contract

이 문서는 모듈 간의 통신 규약과 데이터 규격을 정의합니다. 
모든 AI 에이전트는 코드를 생성하거나 수정하기 전 본 문서를 참조하여 인터페이스의 정합성을 유지해야 합니다.

---

## 1. 데이터 모델 (Pydantic Schemas)

### 1.1 VOCPost
**위치**: `app/models/model.py`

```python
class VOCPost(BaseModel):
    id: str                    # 게시글 ID (Primary Key)
    title: str                 # 제목
    writer: str                # 작성자
    department: str            # 담당 부서 (예: "차량", "시설")
    date: str                  # 작성일 (YYYY-MM-DD HH:MM:SS)
    status: str                # 처리 상태 (예: "접수", "처리중", "완료")
    channel: str               # 접수 경로 (예: "인터넷", "전화")
    is_public: int             # 공개 여부 (0: 비공개, 1: 공개)
    content: Optional[str]     # 상세 내용 (초기엔 None, 상세 조회 후 채워짐)
    attachment: Optional[str]  # 첨부파일명
    is_related: int = 0        # 관심글 여부 (0: 일반, 1: 관심)
```

**사용처**:
- `VOCScraper`: 크롤링 결과 → VOCPost 변환
- `AppController`: 비즈니스 로직 적용 (is_related 판별)
- `VOCDatabase`: DB 저장/조회

### 1.2 StatisticsReport (신규)
**위치**: `app/models/statistics.py`

```python
class StatisticsReport(BaseModel):
    """
    VOC 통계 분석 보고서 데이터 모델
    
    Attributes:
        period_start (str): 분석 기간 시작일 (YYYY-MM-DD)
        period_end (str): 분석 기간 종료일 (YYYY-MM-DD)
        total_count (int): 총 VOC 건수
        by_department (dict): 부서별 통계 {부서명: 건수}
        by_status (dict): 상태별 통계 {상태: 건수}
        by_date (dict): 날짜별 통계 {날짜: 건수}
        top_keywords (list): 빈출 키워드 TOP 10 [(키워드, 빈도), ...]
        charts (dict): 차트 이미지 경로 {차트명: 파일경로}
        generated_at (str): 보고서 생성 시각 (YYYY-MM-DD HH:MM:SS)
    """
    period_start: str = Field(..., description="분석 기간 시작일 (YYYY-MM-DD)")
    period_end: str = Field(..., description="분석 기간 종료일 (YYYY-MM-DD)")
    total_count: int = Field(0, description="총 VOC 건수", ge=0)
    by_department: dict = Field(default_factory=dict, description="부서별 통계")
    by_status: dict = Field(default_factory=dict, description="상태별 통계")
    by_date: dict = Field(default_factory=dict, description="날짜별 통계")
    top_keywords: list = Field(default_factory=list, description="빈출 키워드 TOP 10")
    charts: dict = Field(default_factory=dict, description="차트 이미지 경로")
    generated_at: Optional[str] = Field(None, description="보고서 생성 시각")
    
    @validator('period_start', 'period_end')
    def validate_date_format(cls, v):
        """날짜 형식 검증 (YYYY-MM-DD)"""
        import re
        from datetime import datetime
        
        pattern = r'^\d{4}-\d{2}-\d{2}$'
        if not re.match(pattern, v):
            raise ValueError(f"날짜 형식이 잘못되었습니다: {v} (YYYY-MM-DD 형식 필요)")
        
        try:
            datetime.strptime(v, "%Y-%m-%d")
        except ValueError as e:
            raise ValueError(f"유효하지 않은 날짜입니다: {v}")
        
        return v
    
    @root_validator
    def validate_period(cls, values):
        """기간 유효성 검증"""
        from datetime import datetime, timedelta
        
        start = values.get('period_start')
        end = values.get('period_end')
        
        if start and end:
            start_date = datetime.strptime(start, "%Y-%m-%d")
            end_date = datetime.strptime(end, "%Y-%m-%d")
            
            # 시작일이 종료일보다 늦으면 에러
            if start_date > end_date:
                raise ValueError("시작일이 종료일보다 늦을 수 없습니다.")
            
            # 기간이 1년을 초과하면 에러
            if (end_date - start_date).days > 365:
                raise ValueError("분석 기간은 최대 1년까지만 가능합니다.")
        
        return values
```

**사용처**:
- `StatisticsService`: 통계 데이터 집계 및 보고서 생성
- `StatisticsDialog`: UI에서 보고서 미리보기
- `AppController`: 보고서 생성 요청 처리

### 1.3 StatisticsOptions (신규)
**위치**: `app/models/statistics.py`

```python
class StatisticsOptions(BaseModel):
    """
    통계 분석 옵션 모델
    
    Attributes:
        period_start (str): 분석 기간 시작일
        period_end (str): 분석 기간 종료일
        include_department (bool): 부서별 통계 포함 여부
        include_status (bool): 상태별 통계 포함 여부
        include_keywords (bool): 키워드 분석 포함 여부
        include_charts (bool): 차트 생성 여부
        output_format (str): 출력 형식 ("excel" | "pdf" | "both")
        output_path (str): 저장 경로
    """
    period_start: str
    period_end: str
    include_department: bool = True
    include_status: bool = True
    include_keywords: bool = True
    include_charts: bool = True
    output_format: str = Field("excel", pattern="^(excel|pdf|both)$")
    output_path: Optional[str] = None
```

---

## 2. 모듈 간 인터페이스

### 2.1 Controller → Manager (위임 패턴)

**Controller 리팩토링 (v3.0)** - AppController는 이제 Manager들에게 작업을 위임합니다.

| 호출자 | 메서드 | 입력 | 출력 | 설명 |
|--------|--------|------|------|------|
| `AppController` | `report_manager.request_create_report(data, parent)` | `dict, Widget` | `None` | 보고서 생성 요청 (ReportManager 위임) |
| `AppController` | `file_manager.open_attachment(data, parent)` | `dict, Widget` | `None` | 첨부파일 열기 (FileManager 위임) |
| `AppController` | `ui_manager.open_list_view(focus_id)` | `str` (선택) | `None` | 히스토리 창 열기 (UIManager 위임) |
| `AppController` | `ui_manager.open_settings(icon, item)` | `any, any` | `None` | 설정 창 열기 (UIManager 위임) |
| `AppController` | `ui_manager.request_detail_popup(voc_id)` | `str` | `None` | 상세 팝업 열기 (UIManager 위임) |
| `AppController` | `scheduler.run_crawling_cycle()` | 없음 | 없음 | 크롤링 사이클 실행 (SchedulerManager 위임) |
| `AppController` | `notifier.show_popup(title, msg, voc_id)` | `str, str, str` | 없음 | 팝업 알림 표시 (NotificationManager 위임) |
| `AppController` | `update_manager.check_for_updates()` | 없음 | `Optional[VersionInfo]` | 업데이트 확인 |
| `AppController` | `update_manager.prepare_update(version_info)` | `VersionInfo` | `tuple[bool, str]` | 업데이트 실행 준비 |
| `AppController` | `update_manager.launch_updater()` | 없음 | `bool` | updater.exe 실행 |

### 2.2 Controller ↔ Service

| 호출자 | 메서드 | 입력 | 출력 | 설명 |
|--------|--------|------|------|------|
| `AppController` | `ReportService.create_hwp_report(data)` | `dict` (VOC 데이터) | `tuple[str, str]` | HWP 보고서 생성 |
| `AppController` | `ReportService.create_pdf_report(data)` | `dict` | `tuple[bool, str]` | PDF 보고서 생성 |
| `AppController` | `ReportService.print_voc_detail(data)` | `dict` | `tuple[bool, str]` | 프린터 출력 |
| `AppController` | `VOCScraper.fetch_list_pages(max_pg, kw, dept)` | `int, list, list` | `dict` | 목록 페이지 크롤링 |
| `AppController` | `VOCScraper.fetch_detail_content(voc_id)` | `str` | `dict` | 상세 내용 크롤링 |

### 2.2 Service ↔ Utils

| 호출자 | 메서드 | 입력 | 출력 | 설명 |
|--------|--------|------|------|------|
| `ReportService` | `VOCParser.parse_basic_info(text)` | `str` | `dict` | 호선, 편성, 호차, 열차번호 추출 |
| `ReportService` | `VOCParser.infer_line_and_direction(text)` | `str` | `tuple[str, str, str]` | 호선, 방향, 역명 유추 |
| `ReportService` | `DateScheduleUtils.parse_schedule_type(text, date)` | `str, str` | `tuple[str, str]` | 날짜 및 스케줄 타입 판별 |
| `ReportService` | `TrainAnalyzer.classify_train_details(num, dir)` | `str, str` | `dict` | 열차 종별/방향 분석 |
| `ReportService` | `TrainAnalyzer.get_schedule_remarks(...)` | `dict, str, date` | `str` | 입고 정보 비고 생성 |
| `ReportService` | `TimetableService.find_train_by_time(...)` | `str, str, str, str` | `dict` | 시각표 기반 열차 검색 |

### 2.3 Controller ↔ StatisticsService (신규)

| 호출자 | 메서드 | 입력 | 출력 | 설명 |
|--------|--------|------|------|------|
| `AppController` | `StatisticsService.generate_report(options)` | `StatisticsOptions` | `tuple[bool, str]` | 통계 보고서 생성 |
| `AppController` | `StatisticsService.get_stats_summary(start, end)` | `str, str` | `StatisticsReport` | 통계 요약 조회 |
| `AppController` | `StatisticsService.export_to_excel(report, path)` | `StatisticsReport, str` | `tuple[bool, str]` | Excel 파일 생성 |
| `AppController` | `StatisticsService.export_to_pdf(report, path)` | `StatisticsReport, str` | `tuple[bool, str]` | PDF 파일 생성 |

### 2.4 StatisticsService ↔ Database (신규)

| 호출자 | 메서드 | 입력 | 출력 | 설명 |
|--------|--------|------|------|------|
| `StatisticsService` | `VOCDatabase.get_stats_by_period(start, end)` | `str, str` | `list[dict]` | 기간별 통계 조회 |
| `StatisticsService` | `VOCDatabase.get_stats_by_department(start, end)` | `str, str` | `list[dict]` | 부서별 통계 조회 |
| `StatisticsService` | `VOCDatabase.get_stats_by_status(start, end)` | `str, str` | `list[dict]` | 상태별 통계 조회 |
| `StatisticsService` | `VOCDatabase.get_all_texts_in_period(start, end)` | `str, str` | `list[str]` | 키워드 분석용 텍스트 조회 |

### 2.5 StatisticsService ↔ ChartGenerator (신규)

| 호출자 | 메서드 | 입력 | 출력 | 설명 |
|--------|--------|------|------|------|
| `StatisticsService` | `ChartGenerator.create_bar_chart(data, title)` | `dict, str` | `str` | 막대 그래프 생성 (파일 경로 반환) |
| `StatisticsService` | `ChartGenerator.create_line_chart(data, title)` | `dict, str` | `str` | 선 그래프 생성 |
| `StatisticsService` | `ChartGenerator.create_pie_chart(data, title)` | `dict, str` | `str` | 파이 차트 생성 |
| `StatisticsService` | `ChartGenerator.create_wordcloud(texts, title)` | `list[str], str` | `str` | 워드 클라우드 생성 |

---

## 3. 반환값 규약

### 3.1 성공/실패 패턴

**기본 패턴**: `tuple[bool, str]`
- `(True, "성공 메시지")`: 성공
- `(False, "에러 메시지")`: 실패

**예외**: `ReportService.create_hwp_report()`
- `("FILE_EXISTS", file_path)`: 기존 파일 존재
- `(True, message)`: 정상 생성
- `(False, error_message)`: 생성 실패

### 3.2 크롤링 결과 패턴

```python
{
    "status": "success" | "session_expired" | "error",
    "data": [VOCPost, ...],  # status == "success"
    "error": str             # status == "error"
}
```

---

## 4. 예외 처리 규약

### 4.1 예외 계층 구조

```
VOCMonitorError (기본)
├── DataError
│   ├── DataValidationError
│   └── DatabaseError
├── ScraperError
│   ├── LoginFailedError
│   ├── SessionExpiredError
│   └── PageLoadError
├── ReportError
│   ├── ReportGenerationError
│   ├── TemplateNotFoundError
│   ├── TrainInfoNotFoundError (부분 실패)
│   └── TimetableServiceError (부분 실패)
├── SystemError
│   ├── ConfigurationError
│   └── DependencyError
└── UIError
    └── DialogCancelledError (사용자 취소)
```

### 4.2 부분 실패 예외

다음 예외는 `is_partial_failure = True` 속성을 가지며, 전체 작업을 중단하지 않습니다:
- `TrainInfoNotFoundError`
- `TimetableServiceError`

**처리 방법**:
```python
try:
    train_info = get_train_info(train_number)
except TrainInfoNotFoundError as e:
    if is_partial_failure(e):
        logger.warning(f"부분 실패: {e.message}")
        train_info = None  # 기본값 사용
    else:
        raise  # 전체 중단
```

### 4.3 에러 코드 체계

| 코드 | 설명 | 로그 레벨 |
|------|------|-----------|
| `DATA_VALIDATION_ERROR` | 데이터 검증 실패 | ERROR |
| `DATABASE_ERROR` | DB 작업 실패 | ERROR |
| `LOGIN_FAILED` | 로그인 실패 | ERROR |
| `SESSION_EXPIRED` | 세션 만료 | WARNING |
| `TRAIN_INFO_NOT_FOUND` | 열차 정보 미조회 | WARNING |
| `TIMETABLE_SERVICE_ERROR` | 시각표 조회 실패 | WARNING |
| `TEMPLATE_NOT_FOUND` | 템플릿 파일 없음 | ERROR |
| `DIALOG_CANCELLED` | 사용자 취소 | INFO |

---

## 5. 설정 파일 규약 (settings.json)

### 5.1 구조

```json
{
    "login": {
        "id": "user_id",
        "pw": "password"
    },
    "crawling": {
        "interval_minutes": 10,
        "max_pages": 2,
        "target_depts": ["차량"],
        "keywords": ["지연", "불만"],
        "filter_mode": "OR",
        "recheck_hours": 3,
        "headless_mode": true,
        "max_retries": 3,
        "retry_delay": 2
    },
    "noti": {
        "sound": true,
        "db_check_interval_minutes": 3,
        "unchecked_check_interval_minutes": 10,
        "unchecked_delay_enabled": true,
        "use_related_filter": true
    },
    "update": {
        "connection_config_path": "app/updater/config.json",
        "environment": "main",
        "check_interval_hours": 1,
        "program_id": "voc_monitor",
        "version_table": "program_versions"
    },
    "report": {
        "output_path": "D:/Reports"
    },
    "report_options": {
        "office": "신평차량사업소",
        "team": "검수1",
        "reporter_name": "홍길동",
        "reporter_tel": "051-1234"
    },
    "master_data": {
        "stations_L1": ["서면", "부산역", "노포", ...],
        "office_tel_map": {
            "신평차량사업소": "051-1111",
            "노포차량사업소": "051-2222"
        }
    }
}
```

### 5.2 설정 항목 설명

| 설정 항목 | 타입 | 기본값 | 설명 |
|-----------|------|--------|------|
| `crawling.interval_minutes` | int | 10 | 크롤링 주기 (분) |
| `crawling.max_pages` | int | 2 | 최대 수집 페이지 수 |
| `crawling.target_depts` | list | ["차량"] | 관심 부서 목록 |
| `crawling.keywords` | list | ["1호선", "1호선 역사명..."] | 관심 키워드 목록 (관심글 판정 기준) |
| `crawling.filter_mode` | str | "OR" | 필터링 모드 ("AND" 또는 "OR") |
| `crawling.recheck_hours` | int | 3 | 상세 재수집 주기 (시간) |
| `crawling.headless_mode` | bool | true | 헤드리스 모드 여부 |
| `crawling.max_retries` | int | 3 | 실패 시 최대 재시도 횟수 |
| `crawling.retry_delay` | int | 2 | 재시도 간격 (초) |
| `noti.db_check_interval_minutes` | int | 3 | 신규 알림 DB 체크 주기 (분) |
| `noti.unchecked_check_interval_minutes` | int | 10 | 미확인 글 재알림 주기 (분) |
| `noti.unchecked_delay_enabled` | bool | true | 미확인 글 30분 경과 조건 사용 여부 |
| `noti.use_related_filter` | bool | true | 관심 조건(키워드/부서) 기반 알림만 사용할지 여부 |
| `update.connection_config_path` | str | "app/updater/config.json" | 업데이터 연결 설정 파일 경로 |
| `update.environment` | str | "main" | 원격 연결 설정 환경 이름 |
| `update.check_interval_hours` | int | 1 | 업데이트 체크 주기(시간) |
| `update.program_id` | str | "voc_monitor" | 버전 조회 program_id |
| `update.version_table` | str | "program_versions" | 버전 정보 테이블명 |

**알림 설정 분류 원칙:**
- `crawling.keywords`, `crawling.target_depts`: 관심글 판정 기준 (중복 없이 단일 기준)
- `noti.*`: 알림 주기/조건 토글/사운드 등 알림 동작 제어

**업데이터 연결 설정 원칙:**
- Supabase URL/KEY는 코드에 하드코딩하지 않고 `app/updater/config.json` + `settings.json(update.*)`로 관리
- 원격 config 로드 실패 시 로컬 fallback을 사용

**빌드 계약:**
- `app/update_build_setup.py`는 단일 `updater.exe`를 생성해야 함
- 메인 패키징(`app/setup.py`)은 `updater_build/dist/updater.exe`를 `updater.exe` 이름으로 포함해야 함

**filter_mode 설명:**
- `"OR"`: 키워드 **또는** 부서가 매칭되면 관심글로 표시 (기본값)
- `"AND"`: 키워드 **그리고** 부서가 모두 매칭되어야 관심글로 표시

### 5.3 접근 방법

```python
# Controller에서 로드
self.settings = json.load(open(settings_file))

# 값 접근
crawl_interval = self.settings['crawling']['interval_minutes']
filter_mode = self.settings['crawling'].get('filter_mode', 'OR')
stations = self.settings['master_data']['stations_L1']
```

---

## 6. 데이터베이스 규약

### 6.1 테이블 스키마

```sql
CREATE TABLE voc_posts (
    id TEXT PRIMARY KEY,
    title TEXT NOT NULL,
    writer TEXT,
    department TEXT,
    date TEXT,
    status TEXT,
    channel TEXT,
    is_public INTEGER DEFAULT 1,
    content TEXT,
    attachment TEXT,
    is_related INTEGER DEFAULT 0,
    checked_at TEXT,
    created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
```

### 6.2 주요 쿼리 메서드

| 메서드 | 설명 | 반환 |
|--------|------|------|
| `upsert_post(data)` | 게시글 저장/업데이트 | `None` |
| `get_all_posts()` | 전체 조회 | `list[sqlite3.Row]` |
| `get_post_by_id(voc_id)` | ID로 조회 | `sqlite3.Row` |
| `get_new_posts_since(timestamp)` | 특정 시간 이후 신규 | `list[sqlite3.Row]` |
| `get_unchecked_related_posts()` | 확인하지 않은 관심글 조회 | `list[sqlite3.Row]` |
| `mark_as_checked(voc_id)` | 읽음 처리 | `None` |

---

## 7. UI 이벤트 규약

### 7.1 Controller → View

| 메서드 | 트리거 | 파라미터 |
|--------|--------|----------|
| `open_list_view(focus_id)` | 트레이 메뉴 클릭 | `focus_id: str` (선택) |
| `open_settings()` | 트레이 메뉴 클릭 | 없음 |
| `check_updates_manual()` | 트레이 메뉴 클릭(업데이트 확인) | 없음 |
| `request_detail_popup(voc_id)` | 테이블 더블클릭 | `voc_id: str` |

### 7.2 View → Controller

| 메서드 | 트리거 | 파라미터 |
|--------|--------|----------|
| `request_create_report(data, parent)` | 보고서 생성 버튼 | `data: dict, parent: Widget` |
| `request_print_voc(data)` | 인쇄 버튼 | `data: dict` |
| `open_attachment(data, parent)` | 첨부파일 버튼 | `data: dict, parent: Widget` |
| `mark_as_read(voc_id)` | 상세 조회 | `voc_id: str` |

---

## 8. 자동 업데이트 모듈 (Updater Module) ⭐ 신규

### 8.1 모듈 구조

```
app/updater/
├── __init__.py           # 모듈 초기화
├── __version__.py        # 버전 정보
├── update_manager.py     # 메인 프로그램용 업데이트 관리자
├── updater_gui.py        # updater.exe용 GUI
└── updatelog.md          # 업데이트 로그
```

### 8.2 데이터 모델

#### UpdateConfig
**위치**: `app/updater/update_manager.py`

```python
class UpdateConfig(BaseModel):
    """
    업데이트 구성 모델
    
    updater.exe로 전달되는 구성 정보
    
    Attributes:
        download_url (str): 다운로드 URL (zip 파일)
        target_path (str): 설치 대상 경로
        version (str): 업데이트할 버전
        restart_exe (str): 업데이트 후 실행할 실행파일명
    """
    download_url: str = Field(..., description="다운로드 URL")
    target_path: str = Field(..., description="설치 대상 경로")
    version: str = Field(..., description="업데이트 버전")
    restart_exe: str = Field(default="voc_noti.exe", description="재시작 실행파일")
```

#### VersionInfo
**위치**: `app/updater/update_manager.py`

```python
class VersionInfo(BaseModel):
    """
    Supabase에서 조회한 버전 정보
    
    Attributes:
        version (str): 버전 번호
        is_stable (bool): 안정판 여부
        release_note (str): 배포 노트
        download_url (str): 다운로드 URL
        min_required_version (str): 최소 요구 버전
    """
    version: str
    is_stable: bool
    release_note: Optional[str] = None
    download_url: str
    min_required_version: Optional[str] = None
```

### 8.3 인터페이스 정의

#### UpdateManager (메인 프로그램용)

| 메서드 | 입력 | 출력 | 설명 |
|--------|------|------|------|
| `check_for_updates()` | 없음 | `Optional[VersionInfo]` | Supabase에서 최신 버전 조회 |
| `is_update_available()` | 없음 | `bool` | 업데이트 필요 여부 |
| `prepare_update(version_info)` | `VersionInfo` | `tuple[bool, str]` | 업데이트 준비 (config.json 생성) |
| `launch_updater()` | 없음 | `bool` | updater.exe 실행 |
| `start_background_check(callback)` | `Callable` | `None` | 백그라운드 업데이트 체크 시작 |

**보강된 계약 사항**:
- `check_for_updates()`는 `supabase_url`, `supabase_key` 누락 시 `ConfigError`를 발생시킴
- `prepare_update()`는 `updater.exe` 미존재 시 `(False, "...")`를 반환
- `prepare_update()`는 config 파일을 임시 파일 생성 후 원자적으로 교체 저장함

#### UpdaterGUI (updater.exe용)

| 메서드 | 입력 | 출력 | 설명 |
|--------|------|------|------|
| `load_config()` | 없음 | `UpdateConfig` | config.json 로드 |
| `download_zip(url, progress_callback)` | `str, Callable` | `tuple[bool, str]` | zip 다운로드 |
| `extract_and_replace(zip_path, target_path)` | `str, str` | `tuple[bool, str]` | 압축 해제 및 파일 교체 |
| `restart_main_app(exe_name)` | `str` | `bool` | 메인 앱 재실행 |
| `show_error(message)` | `str` | `None` | 에러 다이얼로그 표시 |

### 8.4 통신 프로토콜

**config.json 형식** (`%TEMP%/voc_updater_config.json`):
```json
{
    "download_url": "https://supabase.../voc_noti_3.2.0.zip",
    "target_path": "C:/Program Files/voc_noti",
    "version": "3.2.0",
    "restart_exe": "voc_noti.exe"
}
```

### 8.5 Supabase API

**테이블**: `program_version`

**조회 쿼리**:
```sql
SELECT * FROM program_version 
WHERE program_id = 'voc_monitor' 
  AND is_stable = true 
ORDER BY created_at DESC 
LIMIT 1
```

### 8.6 예외 처리

| 예외 | 상황 | 처리 |
|------|------|------|
| `NetworkError` | Supabase 연결 실패 | 로그 기록, 다음 체크까지 대기 |
| `DownloadError` | 다운로드 실패 | 사용자 알림, 재시도 옵션 |
| `ExtractError` | 압축 해제 실패 | 롤백, 에러 로그 |
| `ConfigError` | config.json 읽기 실패 | 에러 다이얼로그, 종료 |

---

## 9. 변경 이력

| 날짜 | 버전 | 변경 내용 |
|------|------|-----------|
| 2026-02-18 | 3.3 | **알림 설정 확장** - `noti.db_check_interval_minutes`, `noti.unchecked_check_interval_minutes`, `noti.unchecked_delay_enabled` 규격 추가 |
| 2026-02-18 | 3.2 | **자동 업데이트 모듈 명세 추가** - UpdateManager, UpdaterGUI 인터페이스 정의, config.json 프로토콜 |
| 2026-02-18 | 3.1 | **크롤링 시스템 개선** - ScraperService 하드코딩 제거 (`target_depts`, `keywords` settings.json 연동), 필터링 로직 고도화 (AND/OR 모드, `_check_filter_match` 메서드), 예외처리 강화 |
| 2026-02-18 | 3.0 | **Controller 리팩토링 완료** - Manager 분리 (ReportManager, FileManager, UIManager), 알람 시스템 개선 (키워드 필터링 제거, 미확인 글 알림, 중복 방지, 영속화) |
| 2026-02-17 | 2.0 | 전면 재작성 (에러 처리 체계 추가) |
| 2026-02-14 | 1.5 | 시각표 서비스 추가 |
| 2026-02-11 | 1.0 | 초기 작성 |

---

작성자: KH.Choi
최종 수정: 2026-02-18
버전: 3.3
