server-watchdog 0.1.2

A server monitoring and remote control tool via messenger.
Documentation
# 개발 기획서 — `/help` 커맨드 (선언적 커맨드 문서화)

## 1. 배경 및 목표

### 현재 문제

`INVALID_COMMAND_MESSAGE`는 `general.rs`에 하드코딩된 정적 문자열이다.

```rust
// src/application/handler/general.rs:11
pub const INVALID_COMMAND_MESSAGE: &str = r#"Invalid or unknown command.

Available commands:
- /logs <server_name> <lines>
  ...
- /health (server_name)
  ..."#;
```

- 커맨드를 추가해도 이 문자열을 수동으로 업데이트해야 한다 → 이미 `/alarm`, `/event` 누락
- 커맨드 정의(`command.rs`)와 문서(`general.rs`)가 분리되어 있어 동기화 실패 가능성이 항상 존재
- `/help` 커맨드 자체가 없다
- 틀린 사용법을 입력해도 구체적인 피드백이 없다

### 목표

1. 각 커맨드의 문서(설명, 사용법, 예시)를 커맨드 정의 옆에 **선언적으로** 등록한다.
2. 이 선언으로부터 `/help` 응답이 **자동 생성**된다.
3. 향후 커맨드를 추가할 때 registry에 한 줄만 추가하면 문서도 자동 반영된다.

---

## 2. 기능 명세

### 2-1. `/help`

등록된 모든 커맨드 목록을 표시한다.

```
사용 가능한 커맨드:

/help — 커맨드 사용법을 표시합니다
/logs — 서버의 최근 로그를 가져옵니다
/health — 서버 헬스체크 결과를 확인합니다
/alarm — 이벤트 알람을 구독 또는 해제합니다
/event — 설정된 이벤트 목록을 표시합니다

자세한 사용법: /help <command>
예시: /help logs
```

### 2-2. `/help <command>`

특정 커맨드의 상세 사용법을 표시한다. `command` 인자에서 `/` prefix는 있어도 없어도 된다.

```
/logs — 서버의 최근 로그를 가져옵니다

사용법:
  /logs <server_name> <lines>

예시:
  /logs main 100
  /logs api 50
```

### 2-3. 잘못된 커맨드 입력 시

기존 `INVALID_COMMAND_MESSAGE` 대신 `COMMAND_REGISTRY`에서 자동 생성된 목록을 반환한다.  
응답 형식은 `/help`와 동일하다.

### 2-4. 제외 범위

- Telegram `setMyCommands` API 연동은 이번 구현에 포함하지 않는다.  
  (별도 이슈로 분리. 이번 변경과 독립적으로 추가 가능)

---

## 3. 기술 설계

### 3-1. 선언적 방식 — `Command`의 associated constants

별도 파일이나 외부 레지스트리 없이, **`Command`의 `impl` 블록 안에 associated constants로 메타데이터를 선언**한다.  
각 variant가 자신의 문서를 소유하고, `meta(&self)` 메서드로 연결된다.

```
Command::LOGS  ─→  meta 선언
Command::Logs  ─→  meta(&self) 에서 참조
               render_help_*() 에서 사용
```

**`src/application/handler/command.rs` — 전체 구조**

```rust
// 1. 메타데이터 구조체 (같은 파일 상단)
pub struct CommandMeta {
    pub name: &'static str,
    pub description: &'static str,
    pub usage: &'static str,
    pub examples: &'static [&'static str],
}

impl CommandMeta {
    pub const fn new(
        name: &'static str,
        description: &'static str,
        usage: &'static str,
        examples: &'static [&'static str],
    ) -> Self {
        Self { name, description, usage, examples }
    }
}

// 2. 열거형 정의
#[derive(Debug)]
pub enum Command {
    Logs(String, i32),
    HealthCheckAll,
    HealthCheck(String),
    Alarm(AlarmCommand),
    EventList,
    Help(Option<String>),  // 추가: None = 전체, Some("logs") = 개별
    Nothing,
}

// 3. 메타데이터를 associated constants로 Command에 직접 선언
impl Command {
    const HELP: CommandMeta = CommandMeta::new(
        "/help",
        "커맨드 사용법을 표시합니다",
        "/help [command]",
        &["/help", "/help logs"],
    );
    const LOGS: CommandMeta = CommandMeta::new(
        "/logs",
        "서버의 최근 로그를 가져옵니다",
        "/logs <server_name> <lines>",
        &["/logs main 100", "/logs api 50"],
    );
    const HEALTH: CommandMeta = CommandMeta::new(
        "/health",
        "서버 헬스체크 결과를 확인합니다",
        "/health [server_name]",
        &["/health", "/health main"],
    );
    const ALARM: CommandMeta = CommandMeta::new(
        "/alarm",
        "이벤트 알람을 구독 또는 해제합니다",
        "/alarm <add|remove|list> [event_name]",
        &["/alarm list", "/alarm add cpu-high", "/alarm remove cpu-high"],
    );
    const EVENT: CommandMeta = CommandMeta::new(
        "/event",
        "설정된 이벤트 목록을 표시합니다",
        "/event [list]",
        &["/event", "/event list"],
    );

    // 4. 각 variant → 자신의 메타데이터 반환
    pub fn meta(&self) -> Option<&'static CommandMeta> {
        match self {
            Command::Help(_)                            => Some(&Self::HELP),
            Command::Logs(_, _)                         => Some(&Self::LOGS),
            Command::HealthCheck(_) | Command::HealthCheckAll => Some(&Self::HEALTH),
            Command::Alarm(_)                           => Some(&Self::ALARM),
            Command::EventList                          => Some(&Self::EVENT),
            Command::Nothing                            => None,
        }
    }

    // 5. 전체 문서 목록 (help 응답, setMyCommands 등에서 사용)
    pub fn all_docs() -> &'static [&'static CommandMeta] {
        &[&Self::HELP, &Self::LOGS, &Self::HEALTH, &Self::ALARM, &Self::EVENT]
    }
}
```

새 커맨드를 추가할 때의 흐름:
1. `enum Command`에 variant 추가
2. `impl Command`에 associated constant 추가
3. `meta()``match`에 한 줄 추가
4. `all_docs()`의 슬라이스에 한 줄 추가

### 3-2. 렌더링 메서드

`all_docs()`를 기반으로 응답 문자열을 생성하는 메서드를 같은 `impl` 블록에 추가한다.

```rust
impl Command {
    // /help → 전체 목록
    pub fn render_help_all() -> String {
        let list = Self::all_docs()
            .iter()
            .map(|m| format!("{} — {}", m.name, m.description))
            .collect::<Vec<_>>()
            .join("\n");
        format!("사용 가능한 커맨드:\n\n{list}\n\n자세한 사용법: /help <command>")
    }

    // /help logs → 개별 상세
    pub fn render_help_one(name: &str) -> Option<String> {
        let meta = Self::all_docs()
            .iter()
            .find(|m| m.name.trim_start_matches('/') == name)?;
        let examples = meta.examples
            .iter()
            .map(|e| format!("  {e}"))
            .collect::<Vec<_>>()
            .join("\n");
        Some(format!("{} — {}\n\n사용법:\n  {}\n\n예시:\n{examples}", meta.name, meta.description, meta.usage))
    }
}
```

### 3-3. 파싱 규칙 추가

```rust
["/help"]        => Help(None),
["/help", name]  => Help(Some(name.trim_start_matches('/').to_string())),
```

`/help logs`와 `/help /logs` 모두 동일하게 처리하기 위해 `/` prefix를 trim한다.

### 3-4. `Run` 구현

```rust
Command::Help(name) => match name {
    None => Ok(Command::render_help_all()),
    Some(name) => Command::render_help_one(name)
        .ok_or_else(|| anyhow!("알 수 없는 커맨드: /{name}\n\n{}", Command::render_help_all()).into()),
},
Command::Nothing => Ok(Command::render_help_all()),
```

---

## 4. 파일별 변경 명세

| 파일 | 변경 종류 | 내용 |
|------|-----------|------|
| `src/application/handler/command.rs` | 수정 | `CommandMeta` 구조체 추가, associated constants 추가, `meta()` / `all_docs()` / `render_*` 메서드 추가, `Help` variant 추가, `parse()` 확장, `Run` impl 수정 |
| `src/application/handler/general.rs` | 수정 | `INVALID_COMMAND_MESSAGE` const 삭제 |

총 2개 파일. 새 파일 없음. 기존 커맨드 동작은 변경 없음.

---

## 5. 구현 태스크

- [ ] `command.rs``CommandMeta` 구조체 추가 (`const fn new` 포함)
- [ ] `command.rs``impl Command` associated constants 선언 (HELP, LOGS, HEALTH, ALARM, EVENT)
- [ ] `command.rs``meta(&self)` 메서드 추가
- [ ] `command.rs``all_docs()` 메서드 추가
- [ ] `command.rs``render_help_all()`, `render_help_one()` 메서드 추가
- [ ] `command.rs``Help(Option<String>)` variant 추가
- [ ] `command.rs``parse()``/help`, `/help <name>` 파싱 추가
- [ ] `command.rs``Run` impl에 `Help`, `Nothing` 처리 수정
- [ ] `general.rs``INVALID_COMMAND_MESSAGE` 제거

---

## 6. 검증

```bash
# 빌드 확인
cargo build

# /help 동작 — Telegram에서 입력하거나 통합 테스트로 확인
# 예상 응답: 전체 커맨드 목록

# /help logs 동작
# 예상 응답: /logs 상세 사용법

# /help /health 동작 (prefix / 있는 경우)
# 예상 응답: /health 상세 사용법과 동일

# /help nonexistent 동작
# 예상 응답: "알 수 없는 커맨드" + 전체 목록

# 잘못된 커맨드 입력 (/foo) 동작
# 예상 응답: 전체 커맨드 목록 (기존과 동일 흐름, 내용 개선)
```