# CLAUDE.md

이 파일은 Claude Code가 이 저장소의 코드 작업 시 참고하는 가이드입니다.

## 프로젝트 개요

ptyUtils는 Elasticsearch, MySQL 등 다양한 서비스를 다루는 CLI 도구 모음입니다.

## 네임스페이스

모든 PHP 코드는 `platyFramework` 네임스페이스를 사용합니다.

---

## CLI 스크립트 작성 표준 가이드라인

새로운 CLI 스크립트 작성 시 반드시 따라야 하는 규칙입니다.

### 필수 라이브러리 참조

모든 스크립트는 `ptyLibrary_PHP` 폴더의 공통 라이브러리를 사용합니다:

```php
require_once __DIR__ . '/ptyLibrary_PHP/cli/ptyCliOptionParser.php';  // 필수
require_once __DIR__ . '/ptyLibrary_PHP/cli/ptyCliLog.php';           // 로깅 시
require_once __DIR__ . '/ptyLibrary_PHP/cli/ptyCliColor.php';         // 색상 출력 시
require_once __DIR__ . '/ptyLibrary_PHP/mysql/ptyMysqlConfig.php';    // MySQL 사용 시
require_once __DIR__ . '/ptyLibrary_PHP/elastic/ptyElasticConfig.php'; // Elastic 사용 시
require_once __DIR__ . '/ptyLibrary_PHP/ai/ptyAIConfig.php';          // AI API 사용 시
```

### 필수 옵션 (모든 스크립트에 포함)

| 옵션 | 설명 | 구현 필수 |
|------|------|:--------:|
| `--help` | 도움말 출력 | O |
| `--verbose` | 상세 로그 출력 | O |
| `--edit` | 스크립트를 에디터로 열기 | O |
| `--dry-run` | 실제 실행 없이 대상/결과 미리보기 | O (데이터 변경 스크립트) |

### 옵션 처리 우선순위 (중요!)

**`--help`는 반드시 최우선으로 처리되어야 합니다.** 사용자가 `--help`를 입력하면 다른 모든 로직보다 먼저 도움말을 출력하고 종료해야 합니다.

```php
// ============================================================
// 옵션 처리 우선순위
// ============================================================
// 1. --help     : 최우선! 인자 검증/연결 없이 즉시 도움말 출력
// 2. --edit     : 스크립트 편집 (인자 없이도 동작)
// 3. 필수 인자 검증
// 4. 서비스 연결 및 비즈니스 로직
// ============================================================

// 1. --help: 최우선 처리 (인자 유무와 관계없이)
if (isset($options['help'])) {
    echo "사용법: {$argv[0]} <필수인자> [옵션]\n";
    // ... 도움말 출력 ...
    exit(0);
}

// 2. --edit: 스크립트 편집
if (isset($options['edit'])) {
    $editor = getenv('EDITOR') ?: 'vi';
    passthru("$editor " . escapeshellarg(__FILE__));
    exit(0);
}

// 3. 필수 인자 검증 (--help 이후에 처리)
if (count($positionalArgs) < 1) {
    echo "Error: 필수 인자가 누락되었습니다.\n";
    echo "도움말: {$argv[0]} --help\n";
    exit(1);
}

// 4. 서비스 연결 및 비즈니스 로직
try {
    $conn = ServiceConfig::connect($section);
    // ...
}
```

**잘못된 패턴 (금지):**
```php
// ❌ 필수 인자 검증과 --help를 함께 처리하면 안됨
if (count($positionalArgs) < 1 || isset($options['help'])) {
    // 이 패턴은 인자 없이 실행 시 exit(1)로 종료됨
    exit(isset($options['help']) ? 0 : 1);
}
```

**올바른 패턴:**
```php
// ✅ --help를 먼저 독립적으로 처리
if (isset($options['help'])) {
    // 도움말 출력
    exit(0);  // 항상 성공 코드
}

// 그 다음 인자 검증
if (count($positionalArgs) < 1) {
    echo "Error: 필수 인자가 누락되었습니다.\n";
    exit(1);  // 에러 코드
}
```

### 서비스별 필수 옵션

| 서비스 | 옵션 | 설명 |
|--------|------|------|
| MySQL | `--mysql=섹션명` | INI 파일 섹션 (기본값: default) |
| Elasticsearch | `--elastic=섹션명` | INI 파일 섹션 (기본값: default) |
| AI API | `--ai=섹션명` | INI 파일 섹션 (기본값: default) |

### 스크립트 기본 구조

```php
#!/usr/bin/env php
<?php
/**
 * 스크립트 설명
 *
 * 설정 파일: ~/.pty[Service]Config.ini
 *
 * Usage: ./ptyXXX <args> [options]
 */

namespace platyFramework;

require_once __DIR__ . '/ptyLibrary_PHP/cli/ptyCliOptionParser.php';
// ... 필요한 라이브러리 require ...

// ============================================================
// 인자 파싱
// ============================================================
$parsed = ptyCliOptionParser::parse($argv);
$positionalArgs = $parsed['positional'];
$options = $parsed['options'];

// 공통 옵션
$verbose = isset($options['verbose']);
$dryRun = isset($options['dry-run']);

// ============================================================
// 1. --help: 최우선 처리 (인자 유무와 관계없이)
// ============================================================
if (isset($options['help'])) {
    echo "사용법: {$argv[0]} <필수인자> [옵션]\n";
    echo "\n";
    echo "인자:\n";
    echo "  <필수인자>          설명\n";
    echo "\n";
    echo "옵션:\n";
    echo "  --dry-run           실제 실행 없이 대상 목록만 출력\n";
    echo "  --verbose           상세 로그 출력\n";
    echo "  --edit              이 스크립트를 에디터로 열기\n";
    echo "  --help              도움말 출력\n";
    echo "\n";
    // 설정 파일 예시 출력 (해당 시)
    // echo ServiceConfig::getConfigExample() . "\n";
    exit(0);
}

// ============================================================
// 2. --edit: 스크립트 편집
// ============================================================
if (isset($options['edit'])) {
    $editor = getenv('EDITOR') ?: 'vi';
    passthru("$editor " . escapeshellarg(__FILE__));
    exit(0);
}

// ============================================================
// 3. 필수 인자 검증
// ============================================================
if (count($positionalArgs) < 1) {
    echo "Error: 필수 인자가 누락되었습니다.\n";
    echo "도움말: {$argv[0]} --help\n";
    exit(1);
}

// ============================================================
// 메인 로직
// ============================================================
try {
    if ($dryRun) {
        echo "[DRY-RUN] 실제 실행 없이 미리보기 모드\n";
    }

    // ... 비즈니스 로직 ...

    if ($dryRun) {
        echo "[DRY-RUN] 완료\n";
    }

} catch (\Exception $e) {
    echo "Error: " . $e->getMessage() . "\n";
    exit(1);
}
```

### 네이밍 규칙

| 항목 | 규칙 | 예시 |
|------|------|------|
| 스크립트 파일명 | `pty` + 서비스명 + 동작 (CamelCase) | `ptyMysqlBackup`, `ptyElasticGetIndex` |
| 확장자 | 없음 (shebang으로 실행) | `ptyMysqlBackup` (not `.php`) |
| 설정 파일 | `~/.pty[Service]Config.ini` | `~/.ptyMysqlConfig.ini` |

### Exit 코드

| 코드 | 의미 |
|------|------|
| `0` | 정상 종료 (`--help` 포함) |
| `1` | 에러 또는 필수 인자 누락 |

### --dry-run 구현 패턴

```php
$dryRun = isset($options['dry-run']);

// 시작 시 모드 표시
if ($dryRun) {
    logMessage("Mode: DRY-RUN (실제 실행 없음)", true);
}

// 작업 시 분기
if ($dryRun) {
    logMessage("WOULD DO: $action", true);
} else {
    // 실제 작업 수행
    doAction($action);
}

// 완료 시 메시지 분기
if ($dryRun) {
    logMessage("DRY-RUN 완료: $count 대상", true);
} else {
    logMessage("완료: $count 처리됨", true);
}
```

### --edit 구현 패턴

```php
// 도움말 처리보다 먼저 위치 (인자 없이도 동작해야 함)
if (isset($options['edit'])) {
    $editor = getenv('EDITOR') ?: 'vi';
    passthru("$editor " . escapeshellarg(__FILE__));
    exit(0);
}
```

### x_ 접두사 무시 패턴 (선택)

데이터베이스/테이블/인덱스 등에서 `x_` 접두사 항목을 무시하는 옵션:

```php
$ignoreX = ($options['ignore-x'] ?? 'true') !== 'false';  // 기본값: true

// 사용
if ($ignoreX && strpos($name, 'x_') === 0) {
    logMessage("SKIP: $name (x_ prefix)", true);
    continue;
}
```

### 주의사항

1. **glob 패턴 사용 시**: 사용자에게 따옴표 사용 안내
   ```php
   echo "  {$argv[0]} '*' '*'   # 전체 (따옴표 필수!)\n";
   ```

2. **디렉토리 생성**: dry-run 시에는 생성하지 않음
   ```php
   if (!$dryRun && !is_dir($outputDir)) {
       mkdir($outputDir, 0755, true);
   }
   ```

3. **타임스탬프 로그**: 일관된 형식 사용
   ```php
   function logMessage($message, $show = true) {
       if (!$show) return;
       $timestamp = date('Y-m-d H:i:s');
       echo "[$timestamp] $message\n";
   }
   ```

---

## ptyElastic* 스크립트 작성 규칙

새로운 Elasticsearch CLI 스크립트 생성 시 다음 패턴을 따릅니다:

### 필수 구조

```php
#!/usr/bin/env php
<?php
/**
 * 스크립트 설명
 *
 * 설정 파일: ~/.ptyElasticConfig.ini
 *
 * Usage: ./ptyElasticXXX <args> [options]
 */

namespace platyFramework;

require_once __DIR__ . '/ptyLibrary_PHP/cli/ptyCliOptionParser.php';
require_once __DIR__ . '/ptyLibrary_PHP/elastic/ptyElasticConfig.php';

// 인자 파싱
$parsed = ptyCliOptionParser::parse($argv);
$positionalArgs = $parsed['positional'];
$options = $parsed['options'];
$elasticSection = $options['elastic'] ?? 'default';
$verbose = isset($options['verbose']);

// 도움말 또는 필수 인자 확인
if (/* 필수 인자 누락 */ || isset($options['help'])) {
    echo "사용법: {$argv[0]} <필수인자> [옵션]\n";
    echo "\n";
    echo "옵션:\n";
    echo "  --elastic=섹션명    INI 파일 섹션 (기본값: default)\n";
    echo "  --verbose           상세 로그 출력\n";
    echo "  --help              도움말 출력\n";
    echo "\n";
    // ... 추가 도움말 ...
    echo "설정 파일: ~/.ptyElasticConfig.ini\n";
    echo ptyElasticConfig::getConfigExample() . "\n";
    exit(isset($options['help']) ? 0 : 1);
}

try {
    // Elasticsearch 연결
    $connection = ptyElasticConfig::connect($elasticSection);
    $elastic = $connection['client'];
    $elastic->setDebug($verbose);  // --verbose 옵션에 따라 로그 제어
    $config = $connection['config'];
    $authMethod = $connection['authMethod'];

    // ... 비즈니스 로직 ...

} catch (\Exception $e) {
    echo "Error: " . $e->getMessage() . "\n";
    exit(1);
}
```

### 필수 옵션

| 옵션 | 설명 |
|------|------|
| `--elastic=섹션명` | INI 파일 섹션 지정 (기본값: default) |
| `--verbose` | 상세 로그 출력 (기본: 비활성화) |
| `--help` | 도움말 출력 |

### 주요 클래스/함수

- `ptyCliOptionParser::parse($argv)` - CLI 인자 파싱 (positional, options 분리)
- `ptyElasticConfig::connect($section)` - Elasticsearch 연결 및 클라이언트 반환
- `ptyElasticConfig::getConfigExample()` - 설정 파일 예시 문자열 반환
- `$elastic->setDebug($verbose)` - 상세 로그 활성화/비활성화

### Exit 코드

- `0`: 정상 종료 (--help 포함)
- `1`: 에러 또는 필수 인자 누락

## ptyCliLog 사용법

`ptyCliLog`는 **인스턴스 메소드**를 사용하는 클래스입니다. 정적 메소드가 아니므로 반드시 인스턴스를 생성해서 사용해야 합니다.

### 올바른 사용법

```php
// 헬퍼 함수로 인스턴스 생성
$log = ptyCliLog("APP", ptyCliLog::COLOR_CYAN, true);

// 인스턴스 메소드 호출
$log->info("정보 메시지");
$log->success("성공 메시지");
$log->warning("경고 메시지");
$log->error("에러 메시지");
```

### 잘못된 사용법 (금지)

```php
// ❌ 정적 메소드처럼 호출하면 에러 발생
ptyCliLog::info("메시지");  // Fatal error!
```

### 파라미터

- `$prefix`: 로그 prefix (예: "APP", "RUN", "CRON")
- `$color`: 로그 색상 (예: `ptyCliLog::COLOR_CYAN`)
- `$debug`: 디버그 모드 (기본값: true)

## ptyLibrary_PHP 구조

```
ptyLibrary_PHP/
├── cli/
│   ├── ptyCliOptionParser.php  # CLI 옵션 파서
│   ├── ptyCliLog.php           # 컬러 로깅 (인스턴스 메소드)
│   └── ptyCliColor.php         # ANSI 색상 코드
├── elastic/
│   ├── ptyElasticConfig.php    # Elasticsearch 설정 로더
│   └── Elastic.php             # Elasticsearch 클라이언트
└── ...
```

## 설정 파일

### ~/.ptyElasticConfig.ini

```ini
[default]
host=https://localhost:9200
apiKey=your_api_key

# 또는 user/password 방식
# user=elastic
# password="your_password"

[production]
host=https://prod-elastic:9200
apiKey=production_api_key
```

### ~/.ptyMysqlConfig.ini

```ini
[default]
host=localhost
username=root
password="your_password"
database=your_db
charset=utf8mb4
```

## 설치

### ptyInstall.sh - 사용자 설치 스크립트

사용자 홈 디렉토리에 ptyUtils를 설치하는 스크립트입니다.

```bash
# curl 사용
curl -sSL https://gitlab.platyhouse.com/platyhouse/centos/-/raw/master/ptyInstall.sh | bash

# wget 사용
wget -qO- https://gitlab.platyhouse.com/platyhouse/centos/-/raw/master/ptyInstall.sh | bash
```

**동작 순서:**
1. git 설치 확인 (없으면 자동 설치: yum/dnf/apt-get/brew 지원)
2. 기존 설치 확인
   - 기존 설치 있음: `git pull`로 업데이트
   - 신규 설치: `git clone`으로 설치
3. 실행 권한 부여 (`pty*` 스크립트들에 chmod +x)
4. `ptySetBash` 실행 (`~/.bashrc` 또는 `~/.zshrc`에 설정 추가)

**설치 경로:** `~/ptyUtils.git`
**설정 파일:** `~/.bashrc` (macOS: `~/.zshrc`)

**설치 후:**
```bash
source ~/.bashrc   # 또는 새 터미널 열기
```

### ptyInstallGlobal.sh - 전역 설치 스크립트

시스템 전역에 ptyUtils를 설치하는 스크립트입니다. root 권한이 필요합니다.

```bash
# curl 사용
curl -sSL https://gitlab.platyhouse.com/platyhouse/centos/-/raw/master/ptyInstallGlobal.sh | bash

# wget 사용
wget -qO- https://gitlab.platyhouse.com/platyhouse/centos/-/raw/master/ptyInstallGlobal.sh | bash
```

**동작 순서:**
1. root 권한 확인 (필요 시 sudo로 재실행)
2. git 설치 확인 (없으면 자동 설치: yum/dnf/apt-get 지원)
3. 기존 설치 확인
   - 기존 설치 있음: `git pull`로 업데이트
   - 신규 설치: `git clone`으로 설치
4. 실행 권한 부여 (`pty*` 스크립트들에 chmod +x)
5. `ptySetBash --install-global` 실행 (`/etc/bashrc`에 설정 추가)

**설치 경로:** `/var/lib/ptyUtils.git`
**설정 파일:** `/etc/bashrc` (모든 사용자에게 적용)

**설치 후:**
```bash
source /etc/bashrc   # 또는 새 터미널 열기
```

### ptySetBash - Bash 환경 설정

bash/zsh 환경을 설정하는 스크립트입니다. 설치 스크립트에서 자동 호출됩니다.

```bash
./ptySetBash                  # ~/.bashrc 또는 ~/.zshrc에 설정
./ptySetBash --install-global # /etc/bashrc에 설정 (Linux, root 필요)
```

**옵션:**

| 옵션 | 설명 |
|------|------|
| (기본) | `~/.bashrc` (Linux) 또는 `~/.zshrc` (macOS)에 설정 |
| `--install-global` | `/etc/bashrc`에 설정 (Linux 전용, root 필요) |

**설정 내용:**
- 프롬프트 설정 (git branch 표시 포함)
- 히스토리 설정 (HISTSIZE, HISTTIMEFORMAT)
- 유용한 alias (ll, gs, gd, ga, gb, gc, gl, gac)
- git 설정 (color, quotepath, precomposeunicode)
- PATH에 스크립트 경로 추가

---

## 유틸리티 스크립트

### ptyRun - 스크립트 중복 실행 제어

스크립트의 중복 실행을 제어하고 타임아웃을 관리하는 래퍼 스크립트입니다. 모든 stdout/stderr 출력을 실시간으로 표시하고 로그 파일에 저장합니다.

```bash
# 기본 사용 (중복 실행 차단, 로그는 ./ptyRun_logs/에 90일 보관)
ptyRun "php /path/to/script.php"

# 중복 실행 허용
ptyRun "php script.php" --run-duplicate=true

# 최대 실행 시간 설정 (60분)
ptyRun "php script.php" --max-run-time=60

# 로그 저장 안 함
ptyRun "php script.php" --no-log

# 로그 7일만 보관
ptyRun "php script.php" --log-rotate-date=7

# 로그 10개만 보관 + 커스텀 로그 폴더
ptyRun "php script.php" --log-rotate-count=10 --log-dir=/var/log/myapp
```

**옵션:**
- `--run-duplicate=true|false`: 중복 실행 허용 여부 (기본값: **false**)
- `--max-run-time=분`: 최대 실행 시간(분), 초과 시 강제 종료 후 재실행 (기본값: 0 - 무제한)
- `--no-log`: 로그 파일 저장 비활성화 (기본값: 로그 저장 **활성화**)
- `--log-dir=경로`: 로그 저장 폴더 (기본값: 스크립트폴더/ptyRun_logs)
- `--log-rotate-date=일수`: 로그 보관 일수, 초과 시 오래된 로그 삭제 (기본값: **90**)
- `--log-rotate-count=개수`: 로그 보관 개수, 초과 시 오래된 로그 삭제 (기본값: 0 - 무제한)

**로그 파일:**
- 파일명 형식: `스크립트명_YYYYMMDD_HHMMSS.log`
- 저장 위치: 기본값은 ptyRun 스크립트가 있는 폴더의 `ptyRun_logs/`
- stdout/stderr 모두 기록 (stderr는 `[STDERR]` 프리픽스 추가)

### ptyCronBuild - 크론탭 자동 등록

스크립트를 크론탭에 자동으로 등록하는 유틸리티입니다.

```bash
# 5분마다 실행
ptyCronBuild "ptyRun 'php /path/to/script.php' --max-run-time=60" --daily-5min

# 매일 0시 실행
ptyCronBuild "php /path/to/backup.php" --daily

# 1시간마다 실행
ptyCronBuild "php script.php" --daily-hour

# 10분마다 실행하되, 10분 내에 한 번만 실행 (중복 실행 차단 + 최대 10분 타임아웃)
# ptyRun의 기본 동작(--run-duplicate=false)으로 10분 내에 이미 실행 중이면 스킵
# --max-run-time=600으로 10분 초과 시 강제 종료 후 재실행
ptyCronBuild 'ptyRun "cd /home/redmine/git; php git_update.php" --max-run-time=600' --daily-10min
```

**스케줄 옵션:**
- `--daily`: 매일 0시에 실행
- `--daily-5min`: 매일 5분마다 실행
- `--daily-10min`: 매일 10분마다 실행
- `--daily-30min`: 매일 30분마다 실행
- `--daily-hour`: 매일 1시간마다 실행

**ptyRun과 함께 사용 시:**
- `ptyRun`의 기본값(`--run-duplicate=false`)으로 중복 실행이 자동 차단됨
- `--max-run-time`으로 최대 실행 시간 설정 시, 해당 시간 초과 시 프로세스를 강제 종료하고 재실행
- 예: `--daily-10min`과 `--max-run-time=600`을 함께 사용하면 10분마다 크론이 실행되지만, 이전 실행이 아직 진행 중이면 스킵하고, 10분 이상 실행 중이면 강제 종료 후 재실행

### ptyElasticUploadFromMysql - MySQL → Elasticsearch 업로드

MySQL 테이블을 Elasticsearch 인덱스로 업로드하는 도구입니다.

#### 기본 사용법

```bash
./ptyElasticUploadFromMysql <테이블명> <인덱스명> [옵션]
```

#### CLI 옵션

| 옵션 | 설명 | 기본값 |
|------|------|--------|
| `--mysql=섹션명` | MySQL INI 섹션 | `default` |
| `--elastic=섹션명` | Elasticsearch INI 섹션 | `default` |
| `--batch=N` | 배치 크기 | `100` |
| `--recreate` | 기존 인덱스 삭제 후 재생성 | - |
| `--primary=필드명` | Primary Key 필드명 | `id` |
| `--where='조건'` | WHERE 절 추가 | - |
| `--help` | 도움말 출력 | - |

#### 사용 예시

```bash
# 기본 사용
./ptyElasticUploadFromMysql new_law_items law_items

# 인덱스 재생성 + 배치 크기 500
./ptyElasticUploadFromMysql new_law_items law_items --batch=500 --recreate

# 프로덕션 환경
./ptyElasticUploadFromMysql users users --mysql=production --elastic=production

# 조건부 업로드
./ptyElasticUploadFromMysql users users --where='enabled=1'
```

#### 파일 구조

```
ptyElasticUploadFromMysql.php
├── ConfigLoader (클래스)
│   ├── loadMysqlConfig($section)     # ~/.ptyMysqlConfig.ini 로드
│   └── loadElasticConfig($section)   # ~/.ptyElasticConfig.ini 로드
│
├── MySQLToElastic (클래스)
│   ├── 속성
│   │   ├── $pdo                      # PDO 연결
│   │   ├── $elastic                  # Elastic 클라이언트
│   │   ├── $excludedColumns          # elastic.register=0 컬럼들
│   │   └── $usedAnalyzers            # 사용된 analyzer들
│   │
│   ├── 초기화
│   │   ├── initMySQLConnection()     # PDO 연결 (버퍼링 비활성화)
│   │   └── initElasticConnection()   # Elastic 클라이언트 생성
│   │
│   ├── 매핑 생성
│   │   ├── getTableStructure()       # SHOW FULL COLUMNS로 구조 조회
│   │   ├── parseElasticOptions()     # comment에서 elastic.* 파싱
│   │   ├── convertMySQLTypeToElastic() # MySQL→ES 타입 변환
│   │   └── getAnalyzerSettings()     # analyzer/tokenizer 설정 생성
│   │
│   ├── 인덱스 관리
│   │   └── createIndex($recreate)    # 인덱스 생성/재생성
│   │
│   └── 데이터 업로드
│       ├── uploadData($whereClause)  # 메인 업로드 루프
│       └── bulkInsert($rows)         # Bulk API 호출
│
└── 메인 실행 코드 (CLI 파싱 및 실행)
```

#### MySQL 타입 → Elasticsearch 타입 변환 규칙

| MySQL 타입 | Elasticsearch 타입 |
|------------|-------------------|
| `tinyint`, `smallint`, `mediumint`, `int` | `integer` |
| `bigint` | `long` |
| `float`, `double`, `decimal` | `float` |
| `datetime`, `timestamp`, `date` | `date` |
| `tinyint(1)` | `boolean` |
| `*text` (text, longtext 등) | `text` |
| `varchar(<=255)`, `char(<=255)` | `keyword` |
| `varchar(>255)`, `char(>255)` | `text` |
| `enum` | `keyword` |
| `json` | `object` |
| 기타 | `keyword` |

#### MySQL 컬럼 COMMENT에서 elastic.* 설정

`SHOW FULL COLUMNS FROM` 명령으로 컬럼 comment를 읽어 다음 설정을 파싱합니다:

| 설정 | 설명 | 예시 |
|------|------|------|
| `elastic.register=0` | 인덱스에서 제외 (매핑 + 데이터 모두) | `내부데이터, elastic.register=0` |
| `elastic.type=text\|keyword` | ES 타입 강제 지정 | `elastic.type=text` |
| `elastic.analyzer=분석기명` | analyzer 설정 | `elastic.analyzer=lw_nori_analyzer` |

**예시 MySQL DDL:**
```sql
CREATE TABLE law_items (
  id INT PRIMARY KEY,
  title VARCHAR(500) COMMENT '법령.법령명, elastic.type=text, elastic.analyzer=lw_nori_analyzer',
  contents LONGTEXT COMMENT '본문, elastic.type=text, elastic.analyzer=lw_nori_analyzer',
  internal_memo TEXT COMMENT '내부메모, elastic.register=0',
  category_code VARCHAR(10) COMMENT '카테고리코드, elastic.type=keyword'
);
```

#### Nori analyzer 자동 설정

`elastic.analyzer`에 `nori`가 포함된 이름 지정 시 자동 생성:

```json
{
  "analysis": {
    "analyzer": {
      "lw_nori_analyzer": {
        "type": "custom",
        "tokenizer": "lw_nori_tokenizer",
        "char_filter": ["html_strip"],
        "filter": ["lowercase", "stop"]
      }
    },
    "tokenizer": {
      "lw_nori_tokenizer": {
        "type": "nori_tokenizer",
        "decompound_mode": "mixed",
        "discard_punctuation": "true"
      }
    }
  }
}
```

**tokenizer 이름 생성 규칙:**
- `lw_nori_analyzer` → `lw_nori_tokenizer` (analyzer → tokenizer 치환)
- `my_nori` → `my_nori_tokenizer` (_tokenizer 추가)

#### 문서 ID 생성 규칙

1. `serviceName`, `serviceId` 컬럼이 있으면: `{serviceName}_{serviceId}_{primaryKey}`
2. 없으면: `{primaryKey}` 값 사용

#### 메모리 최적화

- 메모리 제한: 2GB (`ini_set('memory_limit', '2048M')`)
- MySQL 버퍼링 비활성화 (`MYSQL_ATTR_USE_BUFFERED_QUERY => false`)
- 배치 처리 후 `gc_collect_cycles()` 호출
- 변수 즉시 `unset()` 처리

#### 에러 처리

| HTTP 코드 | 의미 | 해결 방법 |
|-----------|------|-----------|
| 413 | Request Entity Too Large | `--batch` 값 줄이기 |
| 400 | analyzer not configured | nori 플러그인 설치 확인 |

#### 수정 시 주의사항

- `parseElasticOptions()`: 정규식 패턴 수정 시 기존 comment 형식 호환성 유지
- `getAnalyzerSettings()`: nori 외 analyzer 추가 시 조건 분기 추가
- `bulkInsert()`: `$excludedColumns` 처리 순서 유지 (datetime 변환 후)
- `createIndex()`: 인덱스 존재 시에도 `getTableStructure()` 호출 필수 (excludedColumns 설정)

## ptyMysql* 스크립트

MySQL 관련 CLI 도구 모음입니다. 모든 스크립트는 `~/.ptyMysqlConfig.ini` 설정 파일을 사용합니다.

### 공통 라이브러리: ptyMysqlConfig

`ptyLibrary_PHP/mysql/ptyMysqlConfig.php` - MySQL 설정 로더 클래스

```php
// 설정 로드
$config = ptyMysqlConfig::load('production');
// 반환: ['host', 'username', 'password', 'database', 'charset']

// mysqli 연결 생성
$conn = ptyMysqlConfig::connect('production');
$connection = $conn['connection'];  // mysqli 객체
$config = $conn['config'];          // 설정 배열

// 사용 가능한 섹션 목록
$sections = ptyMysqlConfig::getSections();

// 설정 파일 예시 출력
echo ptyMysqlConfig::getConfigExample();
```

### ptyMysqlInfo - 서버 정보 조회

MySQL 서버의 상세 정보를 조회합니다.

```bash
./ptyMysqlInfo [--mysql=섹션명]
```

**출력 정보:**
- 연결 정보 (Host, User, Section, Charset)
- 서버 정보 (Version, 포트, 데이터 디렉토리, 문자셋, InnoDB 버퍼 풀 등)
- 서버 상태 (Uptime, 연결 수, 쿼리 수, 슬로우 쿼리 등)
- 데이터베이스 목록 (테이블 수, 크기)
- 사용자 목록 (Locked, PW Expired 상태)
- 현재 프로세스 (SHOW FULL PROCESSLIST)
- InnoDB 상태 (버퍼 풀 히트율)
- 주요 설정 변수

### ptyMysqlBackup - 테이블 백업

mysqldump를 사용하여 테이블을 백업합니다.

```bash
./ptyMysqlBackup <database> <table> [옵션]
./ptyMysqlBackup mydb users                    # mydb.users.sql 생성
./ptyMysqlBackup mydb '*'                      # mydb의 모든 테이블 백업
./ptyMysqlBackup '*' '*'                       # 전체 백업 (따옴표 필수!)
./ptyMysqlBackup mydb users --output=/backup   # 지정 경로에 저장
./ptyMysqlBackup '*' '*' --dry-run             # 백업 대상만 미리보기
./ptyMysqlBackup '*' '*' --ignore-x=false      # x_ 접두사 포함 전체 백업
./ptyMysqlBackup mydb users --tgz              # mydb.users.tgz 생성 (압축)
```

**옵션:**

| 옵션 | 설명 | 기본값 |
|------|------|--------|
| `--mysql=섹션명` | INI 파일 섹션 | `default` |
| `--output=경로` | 출력 디렉토리 | `.` |
| `--ignore-x=true\|false` | x_ 접두사 DB/테이블 무시 | `true` |
| `--tgz` | 백업 후 .tgz로 압축 (원본 .sql 삭제) | - |
| `--dry-run` | 실제 백업 없이 대상 목록만 출력 | - |
| `--verbose` | 상세 로그 출력 | - |
| `--edit` | 스크립트를 에디터로 열기 | - |

**백업 파일:**
- 파일명 형식: `{database}.{table}.sql` 또는 `{database}.{table}.tgz` (--tgz 사용 시)
- 파일 헤더에 백업 시간, 실행 명령어 포함
- `x_` 접두사 DB/테이블은 기본 스킵 (`--ignore-x=false`로 포함 가능)

**mysqldump 옵션:**
```
--default-character-set=utf8mb4
--routines --events
--add-drop-table --add-drop-database
--complete-insert --extended-insert=TRUE
--single-transaction --ssl-mode=DISABLED
--max-allowed-packet=1G
```

### ptyMysqlRestore - 복원

SQL 파일 또는 tgz 압축 파일을 MySQL에 복원합니다.

```bash
./ptyMysqlRestore <pattern> [옵션]
./ptyMysqlRestore "*"                          # 모든 sql/tgz 파일 복원
./ptyMysqlRestore "*.sql"                      # 모든 sql 파일만 복원
./ptyMysqlRestore "*.tgz"                      # 모든 tgz 파일 복원 (압축 해제 후)
./ptyMysqlRestore "mydb.*.sql"                 # mydb의 모든 테이블 복원
./ptyMysqlRestore "mydb.users.sql"             # 특정 파일 복원
./ptyMysqlRestore "mydb.users.tgz"             # 특정 tgz 파일 복원
./ptyMysqlRestore "*" --database=mydb_dev      # 모든 파일을 mydb_dev DB에 복원
./ptyMysqlRestore "*" --force                  # 확인 없이 복원
```

**옵션:**

| 옵션 | 설명 | 기본값 |
|------|------|--------|
| `--mysql=섹션명` | INI 파일 섹션 | `default` |
| `--input=경로` | 입력 디렉토리 | `.` |
| `--database=DB명` | 복원 대상 DB (미지정시 파일명에서 추출) | - |
| `--force` | 확인 없이 바로 복원 | - |
| `--verbose` | 상세 로그 출력 | - |

**파일명 형식:** `{database}.{table}.sql` 또는 `{database}.{table}.tgz`

**.tgz 파일 처리:**
- `*` 패턴 사용 시 `.sql`과 `.tgz` 모두 검색
- `.tgz` 파일은 임시 디렉토리에 압축 해제 후 복원
- 복원 완료 후 임시 파일 자동 정리

**복원 전 확인:**
- 서버 정보 표시
- 복원 대상 파일 목록
- 테이블 상태 표시: `[EXISTS]`, `[NEW]`, `[OVERWRITE]`
- `--force` 없으면 사용자 확인 요청

### ptyMysqlOverwrite - 테이블 복사

서로 다른 MySQL 서버/DB 간에 테이블을 복사합니다. 내부적으로 ptyMysqlBackup + ptyMysqlRestore를 사용합니다.

```bash
./ptyMysqlOverwrite --src-mysql=api --dst-mysql=dev \
                    --src-db=lawtice_db --dst-db=lawtice_dev \
                    --src-table=new_law_items --dst-table=new_law_items
```

**필수 옵션:**

| 옵션 | 설명 |
|------|------|
| `--src-mysql=섹션명` | 소스 MySQL INI 섹션 |
| `--dst-mysql=섹션명` | 대상 MySQL INI 섹션 |
| `--src-db=DB명` | 소스 데이터베이스명 |
| `--dst-db=DB명` | 대상 데이터베이스명 |
| `--src-table=테이블명` | 소스 테이블명 |
| `--dst-table=테이블명` | 대상 테이블명 |

**선택 옵션:**

| 옵션 | 설명 |
|------|------|
| `--force` | 확인 없이 바로 실행 |
| `--verbose` | 상세 로그 출력 |

**동작 과정:**
1. **Step 1**: 소스 테이블을 임시 디렉토리에 백업 (`ptyMysqlBackup.php` 사용)
2. **Step 2**: 테이블명 변환 (src-table ≠ dst-table인 경우)
3. **Step 3**: 대상 서버에 복원 (`ptyMysqlRestore.php` 사용)
4. 임시 파일 정리

**Step 2 테이블명 변환 상세:**

mysqldump로 생성된 SQL 파일에는 원본 테이블명이 하드코딩되어 있습니다.
`--src-table`과 `--dst-table`이 다를 경우, SQL 파일 내의 테이블명을 변경해야 합니다.

```sql
-- 변환 전 (src-table=users)
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (...);
INSERT INTO `users` VALUES (...);
LOCK TABLES `users` WRITE;

-- 변환 후 (dst-table=users_backup)
DROP TABLE IF EXISTS `users_backup`;
CREATE TABLE `users_backup` (...);
INSERT INTO `users_backup` VALUES (...);
LOCK TABLES `users_backup` WRITE;
```

- `sed` 명령어로 스트리밍 처리 (대용량 파일도 메모리 문제 없음)
- 테이블명이 같으면 변환 과정 스킵

**사용 시나리오:**
```bash
# 프로덕션 → 개발 환경 동기화
./ptyMysqlOverwrite --src-mysql=production --dst-mysql=development \
                    --src-db=prod_db --dst-db=dev_db \
                    --src-table=users --dst-table=users --force

# 같은 서버에서 테이블 복제
./ptyMysqlOverwrite --src-mysql=default --dst-mysql=default \
                    --src-db=mydb --dst-db=mydb \
                    --src-table=users --dst-table=users_backup
```

### ptyMysql* 스크립트 작성 규칙

새로운 MySQL CLI 스크립트 생성 시 다음 패턴을 따릅니다:

```php
#!/usr/bin/env php
<?php
namespace platyFramework;

require_once __DIR__ . '/ptyLibrary_PHP/cli/ptyCliOptionParser.php';
require_once __DIR__ . '/ptyLibrary_PHP/mysql/ptyMysqlConfig.php';

// 인자 파싱
$parsed = ptyCliOptionParser::parse($argv);
$options = $parsed['options'];
$mysqlSection = $options['mysql'] ?? 'default';

// 도움말
if (isset($options['help'])) {
    echo "사용법: ...\n";
    echo ptyMysqlConfig::getConfigExample() . "\n";
    exit(0);
}

try {
    // MySQL 연결
    $conn = ptyMysqlConfig::connect($mysqlSection);
    $connection = $conn['connection'];
    $config = $conn['config'];

    // ... 비즈니스 로직 ...

    mysqli_close($connection);
} catch (\Exception $e) {
    echo "Error: " . $e->getMessage() . "\n";
    exit(1);
}
```

**필수 옵션:**
- `--mysql=섹션명` - INI 파일 섹션 (기본값: default)
- `--help` - 도움말 출력

## ptyAIGetMessage - AI API CLI 도구

다양한 AI API(Claude, ChatGPT, Gemini, Ollama)를 통해 메시지를 전송하고 응답을 받는 CLI 도구입니다.

### 기본 사용법

```bash
./ptyAIGetMessage.php "메시지" [옵션]
```

### CLI 옵션

| 옵션 | 설명 | 기본값 |
|------|------|--------|
| `--ai=섹션명` | INI 파일 섹션 | `default` |
| `--model=모델명` | 모델 오버라이드 | - |
| `--anthropic-beta=기능명` | Claude 베타 기능 오버라이드 | - |
| `--verbose` | 상세 정보 출력 (토큰 사용량, curl 요청/응답) | - |
| `--json` | JSON 형식으로 출력 | - |
| `--help` | 도움말 출력 | - |

### 사용 예시

```bash
# 기본 사용
./ptyAIGetMessage.php "안녕하세요"

# 특정 섹션 사용
./ptyAIGetMessage.php "안녕" --ai=openai

# 모델 오버라이드
./ptyAIGetMessage.php "안녕" --ai=anthropic --model=claude-3-haiku-20240307

# Claude 베타 기능
./ptyAIGetMessage.php "안녕" --ai=claude --anthropic-beta=context-1m-2025-08-07

# 상세 정보 출력 (curl 요청/응답 포함)
./ptyAIGetMessage.php "안녕" --ai=openai --verbose

# JSON 출력
./ptyAIGetMessage.php "안녕" --ai=google --json
```

### 설정 파일: ~/.ptyAIConfig.ini

```ini
[default]
provider=anthropic
model=claude-sonnet-4-5-20250514
apiKey=your_anthropic_api_key

[claude-beta]
provider=anthropic
model=claude-sonnet-4-5-20250514
apiKey=your_anthropic_api_key
anthropic-beta=context-1m-2025-08-07

[openai]
provider=openai
model=gpt-4o
apiKey=your_openai_api_key

[google]
provider=google
model=gemini-2.0-flash-exp
apiKey=your_google_api_key

[ollama]
provider=ollama
model=llama3
apiUrl=http://localhost:11434
```

### 지원 Provider

| Provider | 설정 필드 | 모델 예시 |
|----------|----------|----------|
| `anthropic` | `apiKey`, `anthropic-beta` | claude-sonnet-4-5-20250514, claude-3-5-sonnet-20241022 |
| `openai` | `apiKey` | gpt-4o, gpt-4-turbo, gpt-3.5-turbo |
| `google` | `apiKey` | gemini-2.0-flash-exp, gemini-1.5-pro |
| `ollama` | `apiUrl` | llama3, mistral, codellama, gemma2 |

### --json 출력 형식

```json
{
    "request": {
        "provider": "anthropic",
        "model": "claude-sonnet-4-5-20250514",
        "message": "안녕",
        "anthropic-beta": "context-1m-2025-08-07"
    },
    "response": {
        "message": "안녕하세요! 무엇을 도와드릴까요?",
        "elapsed_ms": 1234,
        "tokens": {
            "in": 10,
            "out": 25,
            "total": 35
        },
        "_api_result": { ... }
    }
}
```

### 라이브러리 구조

```
ptyLibrary_PHP/ai/
├── ptyAIConfig.php              # AI 설정 로더
├── claude.api.common.model.php  # Anthropic Claude API
├── chatgpt.api.common.model.php # OpenAI ChatGPT API
├── gemini.api.common.model.php  # Google Gemini API
└── ollama.api.common.model.php  # Ollama API
```

### ptyAIConfig 클래스

```php
// 설정 로드
$config = ptyAIConfig::load('openai');
// 반환: ['provider', 'model', 'apiKey', 'apiUrl', 'anthropic-beta']

// 클라이언트 생성
$client = ptyAIConfig::createClient('openai');

// 설정과 클라이언트 함께 반환
$conn = ptyAIConfig::connect('openai');
$client = $conn['client'];
$config = $conn['config'];

// 간단한 메시지 전송
$response = $client->getSimple("안녕하세요");
```

### 각 API 클라이언트 공통 메서드

```php
$client->setModel($model);           // 모델 설정
$client->setMaxTokens($tokens);      // 최대 토큰 설정
$client->setTemperature($temp);      // Temperature 설정
$client->setDebug(true);             // 디버그 모드 (curl 정보 출력)
$client->get($message);              // API 호출 (전체 응답)
$client->getSimple($message);        // API 호출 (텍스트만)
$client->extractText($response);     // 응답에서 텍스트 추출
```

### Claude 전용 메서드

```php
$client->setAnthropicBeta($feature); // anthropic-beta 헤더 설정
```

### Ollama 전용 메서드

```php
$client->setApiUrl($url);            // API URL 설정
```

### 토큰 사용량 응답 위치

| Provider | 입력 토큰 | 출력 토큰 |
|----------|----------|----------|
| anthropic | `usage.input_tokens` | `usage.output_tokens` |
| openai | `usage.prompt_tokens` | `usage.completion_tokens` |
| google | `usageMetadata.promptTokenCount` | `usageMetadata.candidatesTokenCount` |
| ollama | `prompt_eval_count` | `eval_count` |
