---
name: fastTable/fastRecord 구현 검증 결과
description: roo-fasttable-implementation.md 계획 대비 실제 구현 차이 및 버그 목록
type: project
originSessionId: ec4d397a-b394-4d23-b041-03b70a7d0136
---
## 검증 대상
- 계획서: `plans/roo-fasttable-implementation.md`
- 검증 시점: 2026-04-29
- 빌드 결과: 경고 0건, 에러 0건 (빌드는 통과)

## Step별 구현 상태

| Step | 파일 | 상태 | 비고 |
|------|------|------|------|
| 1 | ExperionEntities.cs | ✅ 완료 | |
| 2 | IExperionServices.cs | ✅ 완료 | |
| 3 | IExperionServices.cs | ✅ 완료 | |
| 4 | ExperionDbContext.cs | ✅ 완료 | |
| 5 | ExperionDbContext.cs (DDL) | ⚠️ 차이 | `tag_list JSONB` (계획: TEXT). EnsureCreatedAsync가 먼저 실행되어 실제로는 text 타입이 됨 → 런타임 문제 없음 |
| 6 | ExperionDbContext.cs (메서드) | ⚠️ 차이 | GetFastSessionsAsync 정렬 역전, UpdateFastSessionRowCountAsync 구현 방식 다름 |
| 7 | ExperionDbContext.cs (메서드) | ❌ 버그 | CSV Export 헤더 오류 |
| 8~11 | ExperionFastService.cs | ❌ 치명적 | StartSessionAsync 항상 실패 |
| 12 | ExperionFastCleanupService | ✅ 완료 | ExperionFastService.cs 파일에 같이 위치 (문제없음) |
| 13 | ExperionControllers.cs | ✅ 완료 | FastPinRequest → PinRequest로 이름 다름 (동작 동일) |
| 14 | Program.cs | ✅ 완료 | DI 패턴 3줄 패턴 정확히 구현 |
| 15 | appsettings.json | ✅ 완료 | Fast 섹션 추가됨 |
| 16 | index.html | ✅ 완료 | Bootstrap 방식으로 구현 (계획과 스타일 다르나 기능 동일) |
| 17 | app.js | ⚠️ 차이 | 태그 목록 로딩 방식 다름 (전역변수 의존) |
| 18 | style.css | ⚠️ 생략 | Bootstrap 사용하여 별도 CSS 불필요 |

---

## 버그 목록

### Bug 1 — StartSessionAsync 항상 실패 (치명적)
**파일**: `src/Infrastructure/OpcUa/ExperionFastService.cs:99-116`

현재 코드:
```csharp
var cfg = await _configProvider.GetConfigAsync(new ExperionServerConfig()); // 빈 설정!
if (string.IsNullOrEmpty(cfg?.ServerConfiguration?.BaseAddresses?.Count > 0 ? cfg.ServerConfiguration.BaseAddresses[0] : null))
    throw new InvalidOperationException("서버 엔드포인트 URL이 설정되어 있지 않습니다.");

if (!await _opcClient.IsConnectedAsync(cfg))
    throw new InvalidOperationException("OPC UA 서버에 연결되어 있지 않습니다.");

// 노드 유효성 사전 검증
foreach (var tagName in request.TagList)
{
    var nodeId = await db.GetNodeIdByTagNameAsync(tagName);
    if (string.IsNullOrEmpty(nodeId))
        throw new ArgumentException($"태그 '{tagName}'의 nodeId를 찾을 수 없습니다.");

    var readResult = await _opcClient.ReadTagAsync(new ExperionServerConfig(), nodeId); // 빈 설정!
    if (!readResult.Success)
        throw new ArgumentException($"태그 '{tagName}' 읽기 실패: {readResult.ErrorMessage}");
}
```

문제: `new ExperionServerConfig()`는 ServerHostName이 빈 문자열이라 EndpointUrl = `opc.tcp://:4840`. ApplicationConfiguration의 BaseAddresses가 비어 있어 "서버 엔드포인트 URL이 설정되어 있지 않습니다." 항상 throw.

또한 `StartSubscriptionAsync(ctx, cfg)` 내부:
```csharp
var endpoint = await SelectEndpointAsync(cfg, cfg.ServerConfiguration?.BaseAddresses?[0] ?? string.Empty);
var session = await CreateSessionAsync(cfg, endpoint, new ExperionServerConfig()); // 빈 UserName/Password
```

수정 방법 (계획서 Step 9 참조): `realtime_autostart.json`에서 ExperionServerConfig를 읽어야 함.
```csharp
private static readonly string RealtimeFlagPath = Path.GetFullPath("realtime_autostart.json");

private static async Task<ExperionServerConfig?> ReadServerConfigAsync()
{
    if (!File.Exists(RealtimeFlagPath)) return null;
    try
    {
        var json = await File.ReadAllTextAsync(RealtimeFlagPath);
        return JsonSerializer.Deserialize<ExperionServerConfig>(json);
    }
    catch { return null; }
}
```

그리고 `StartSessionAsync` 시작 부분을:
```csharp
var serverCfg = await ReadServerConfigAsync();
if (serverCfg == null)
    throw new InvalidOperationException("OPC UA 서버 설정을 찾을 수 없습니다. 실시간 구독을 먼저 시작하세요.");

var appConfig = await _configProvider.GetConfigAsync(serverCfg);
// IsConnectedAsync 체크 제거 or appConfig 사용
```

`StartSubscriptionAsync` 시그니처도 변경:
```csharp
private async Task StartSubscriptionAsync(FastSessionContext ctx, ExperionServerConfig serverCfg)
{
    var appConfig = await _configProvider.GetConfigAsync(serverCfg);
    var endpoint  = await SelectEndpointAsync(appConfig, serverCfg.EndpointUrl);
    var identity  = new UserIdentity(serverCfg.UserName, System.Text.Encoding.UTF8.GetBytes(serverCfg.Password));
    // ...
}
```

`IExperionOpcClient` 생성자 주입도 제거 가능 (사전 검증 로직 삭제). Program.cs의 DI 등록은 이미 정상.

---

### Bug 2 — CSV Export 헤더 오류
**파일**: `src/Infrastructure/Database/ExperionDbContext.cs:828-829`

현재 코드 (잘못됨):
```csharp
csv.WriteRecord(new { RecordedAt = "recorded_at", TagNames = tagNames.Select((t, i) => $"tag{i+1}") });
await writer.WriteLineAsync();
```
→ CSV 헤더가 `RecordedAt,TagNames` 또는 `recorded_at,tag1` 형태로 출력됨. 태그명이 아님.

수정 방법 (계획서 Step 7 참조):
```csharp
using var writer = new StreamWriter(stream, leaveOpen: true);
await writer.WriteLineAsync("recorded_at," + string.Join(",", tagNames));

foreach (var g in records.GroupBy(x => x.RecordedAt).OrderBy(g => g.Key))
{
    var values = g.ToDictionary(r => r.TagName, r => r.Value);
    var row = g.Key.ToString("o") + "," +
              string.Join(",", tagNames.Select(t => values.TryGetValue(t, out var v) ? $"\"{v}\"" : ""));
    await writer.WriteLineAsync(row);
}
await writer.FlushAsync();
```
CsvHelper 의존성 제거. `CsvHelper` using도 제거 필요.

---

### Bug 3 — 세션 목록 정렬 역전 (경미)
**파일**: `src/Infrastructure/Database/ExperionDbContext.cs:760`

현재:
```csharp
.OrderBy(x => x.StartedAt)  // 오래된 것이 위
```
계획:
```csharp
.OrderByDescending(x => x.StartedAt)  // 최신이 위
```

---

### Bug 4 — 신규 세션 모달에서 태그 목록이 비어 있을 수 있음 (경미)
**파일**: `src/Web/wwwroot/js/app.js`, `btn-fast-new` 이벤트 핸들러

현재 코드:
```javascript
(typeof tagNames !== 'undefined' ? tagNames : []).forEach(name => { ... });
```
`tagNames` 전역 변수가 실시간 탭 방문 전에는 비어 있음 → 태그 선택 불가.

계획서 `fastNewModal()` 참조 → `/api/realtime/points` 직접 fetch:
```javascript
async function fastNewModal() {
    const res = await fetch('/api/realtime/points');
    const select = document.getElementById('fast-tag-select');
    select.innerHTML = '';
    if (res.ok) {
        const data = await res.json();
        (data.items || []).forEach(p => {
            const opt = document.createElement('option');
            opt.value = p.tagName || p.TagName;
            opt.textContent = p.tagName || p.TagName;
            select.appendChild(opt);
        });
    }
    // ...
}
```

---

## 수정 우선순위
1. **Bug 1** (치명적): StartSessionAsync — realtime_autostart.json 기반 서버 설정 읽기로 전면 수정
2. **Bug 2** (중요): ExportFastRecordsToCsvAsync — CsvHelper 제거, 수동 CSV 작성으로 교체
3. **Bug 3** (경미): GetFastSessionsAsync 정렬 방향 수정
4. **Bug 4** (경미): fastNewModal 태그 목록 직접 fetch로 수정

**Why:** Bug 1은 fastRecord 기능이 전혀 동작하지 않게 만드는 근본 원인. realtime_autostart.json에 OPC UA 서버 접속 정보가 있음 (실시간 구독 시작 시 저장됨).
**How to apply:** Roo에게 위 4개 버그를 순서대로 수정 지시. 각 수정 후 `dotnet build` 확인.
