--- 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 ReadServerConfigAsync() { if (!File.Exists(RealtimeFlagPath)) return null; try { var json = await File.ReadAllTextAsync(RealtimeFlagPath); return JsonSerializer.Deserialize(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` 확인.