# STEP 8 — 컨트롤러 추가 (`ExperionFastController`)

## 사전 확인 (작업 전 반드시 수행)

1. `src/Web/Controllers/ExperionControllers.cs` 파일을 열어 전체 내용을 읽는다.
2. 아래 항목을 확인하고 기록한다:
   - [x] STEP 7이 완료되어 `ExperionFastService`가 빌드되는가?
   - [x] `ExperionFastController` 클래스가 이미 존재하는가? → 있으면 내용 비교 후 누락 엔드포인트만 추가
   - [x] `PinRequest` record가 이미 존재하는가?
   - [x] 파일 상단에 `using Microsoft.AspNetCore.Mvc;` 가 있는가?

---

## 작업 내용

**파일**: `src/Web/Controllers/ExperionControllers.cs`  
**위치**: 파일 하단 (기존 컨트롤러 마지막 클래스 아래)

```csharp
// ── FastTable / FastRecord ────────────────────────────────────────────────────

[ApiController]
[Route("api/fast")]
public class ExperionFastController : ControllerBase
{
    private readonly IExperionFastService _fastSvc;

    public ExperionFastController(IExperionFastService fastSvc)
        => _fastSvc = fastSvc;

    /// <summary>새 fastSession 시작</summary>
    [HttpPost("start")]
    public async Task<IActionResult> Start([FromBody] FastSessionStartRequest request)
    {
        try
        {
            var session = await _fastSvc.StartSessionAsync(request);
            return Ok(new { id = session.Id, name = session.Name, status = session.Status, startedAt = session.StartedAt });
        }
        catch (ArgumentException ex)      { return BadRequest(new { error = ex.Message }); }
        catch (InvalidOperationException ex) { return Conflict(new { error = ex.Message }); }
    }

    /// <summary>세션 중지</summary>
    [HttpPost("{id:int}/stop")]
    public async Task<IActionResult> Stop(int id)
    {
        try
        {
            await _fastSvc.StopSessionAsync(id);
            return Ok(new { success = true, message = "세션이 중지되었습니다." });
        }
        catch (InvalidOperationException ex) { return NotFound(new { error = ex.Message }); }
    }

    /// <summary>세션 목록 조회</summary>
    [HttpGet("sessions")]
    public async Task<IActionResult> GetSessions()
    {
        var sessions = await _fastSvc.GetSessionsAsync();
        return Ok(new
        {
            total = sessions.Count(),
            items = sessions.Select(s => new
            {
                id            = s.Id,
                name          = s.Name,
                status        = s.Status,
                samplingMs    = s.SamplingMs,
                durationSec   = s.DurationSec,
                tagCount      = s.TagList.Length,
                rowCount      = s.RowCount,
                startedAt     = s.StartedAt,
                endedAt       = s.EndedAt,
                retentionDays = s.RetentionDays,
                pinned        = s.Pinned
            })
        });
    }

    /// <summary>세션 상세 정보</summary>
    [HttpGet("{id:int}")]
    public async Task<IActionResult> GetSession(int id)
    {
        var session = await _fastSvc.GetSessionAsync(id);
        if (session == null) return NotFound();
        return Ok(new
        {
            id            = session.Id,
            name          = session.Name,
            status        = session.Status,
            samplingMs    = session.SamplingMs,
            durationSec   = session.DurationSec,
            tagList       = session.TagList,
            rowCount      = session.RowCount,
            startedAt     = session.StartedAt,
            endedAt       = session.EndedAt,
            retentionDays = session.RetentionDays,
            pinned        = session.Pinned
        });
    }

    /// <summary>레코드 조회 (Long 포맷)</summary>
    [HttpGet("{id:int}/records")]
    public async Task<IActionResult> GetRecords(int id,
        [FromQuery] DateTime? from,
        [FromQuery] DateTime? to,
        [FromQuery] string format = "long")
    {
        var result = await _fastSvc.GetRecordsAsync(id, from, to, format);
        return Ok(new
        {
            sessionId = result.SessionId,
            from      = result.From,
            to        = result.To,
            tagNames  = result.TagNames,
            total     = result.TotalCount,
            items     = result.Items.Select(r => new
            {
                sessionId  = r.SessionId,
                recordedAt = r.RecordedAt,
                tagName    = r.TagName,
                value      = r.Value
            })
        });
    }

    /// <summary>CSV Export (스트리밍)</summary>
    [HttpGet("{id:int}/csv")]
    public async Task<IActionResult> ExportCsv(int id,
        [FromQuery] DateTime? from,
        [FromQuery] DateTime? to)
    {
        var ms = new MemoryStream();
        await _fastSvc.ExportCsvAsync(id, ms, from, to);
        ms.Position = 0;
        return File(ms, "text/csv", $"fast-{id}-{DateTime.Now:yyyyMMddHHmm}.csv");
    }

    /// <summary>세션 삭제</summary>
    [HttpDelete("{id:int}")]
    public async Task<IActionResult> Delete(int id)
    {
        try
        {
            await _fastSvc.DeleteSessionAsync(id);
            return Ok(new { success = true, message = "세션이 삭제되었습니다." });
        }
        catch (InvalidOperationException ex) { return NotFound(new { error = ex.Message }); }
    }

    /// <summary>세션 고정/해제</summary>
    [HttpPost("{id:int}/pin")]
    public async Task<IActionResult> Pin(int id, [FromBody] PinRequest request)
    {
        try
        {
            await _fastSvc.PinSessionAsync(id, request.Pinned);
            return Ok(new { success = true, pinned = request.Pinned });
        }
        catch (InvalidOperationException ex) { return NotFound(new { error = ex.Message }); }
    }
}

public record PinRequest(bool Pinned);
```

---

## 사후 확인 (작업 후 반드시 수행)

1. `ExperionControllers.cs` 파일을 다시 열어 추가된 컨트롤러를 읽는다.
2. 아래 항목을 하나씩 확인한다:
   - [x] `[Route("api/fast")]` 라우트가 맞는가?
   - [x] `POST /start` 엔드포인트 존재
   - [x] `POST /{id}/stop` 엔드포인트 존재
   - [x] `GET /sessions` 엔드포인트 존재
   - [x] `GET /{id}` 엔드포인트 존재
   - [x] `GET /{id}/records` 엔드포인트 존재
   - [x] `GET /{id}/csv` 엔드포인트 존재
   - [x] `DELETE /{id}` 엔드포인트 존재
   - [x] `POST /{id}/pin` 엔드포인트 존재 (총 8개 엔드포인트)
   - [x] `PinRequest` record 존재 (중복 선언 아닌지 확인)
3. `dotnet build src/Web` 실행 → 에러/경고 0개 확인
4. 문제가 있으면 수정 후 다시 빌드 확인

---

## 완료 조건
- `dotnet build src/Web` 결과: 에러 0, 경고 0
- `ExperionFastController` 8개 엔드포인트 모두 존재