# 개발 기획서 — `/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) 동작
# 예상 응답: 전체 커맨드 목록 (기존과 동일 흐름, 내용 개선)
```