Files
ExperionCrawler/CLAUDE.md
2026-04-14 09:56:37 +00:00

20 KiB
Raw Blame History

ExperionCrawler — 작업 이력

작업 규칙

  • 복잡한 작업은 항상 todo 목록 먼저 생성
  • 각 단계 시작 전 todo 목록 확인
  • 단계 완료 후 즉시 completed 표시

완료된 작업

노드맵 대시보드 구현 (2026-04-14)

node_map_master 테이블을 조회·탐색할 수 있는 웹 대시보드를 풀스택으로 구현했다.

수정된 파일

파일 내용
src/Core/Application/Interfaces/IExperionServices.cs IExperionDbServiceGetMasterStatsAsync() / QueryMasterAsync() 추가, NodeMapStats / NodeMapQueryResult record 추가
src/Infrastructure/Database/ExperionDbContext.cs ExperionDbService에 두 메서드 구현 (통계·필터 조회, 페이지네이션)
src/Web/Controllers/ExperionControllers.cs ExperionNodeMapController 추가 (GET /api/nodemap/stats, GET /api/nodemap/query)
src/Web/wwwroot/index.html 사이드바 05번 탭 추가, #pane-nm-dash 섹션 추가 (통계 카드·필터폼·페이지네이션·테이블)
src/Web/wwwroot/js/app.js nmLoad() / nmQuery() / nmPrev() / nmNext() / nmReset() 구현, 탭 클릭 핸들러에 nmLoad() 호출 추가
src/Web/wwwroot/css/style.css .nm-stat-row, .nm-cls, .nm-dtype, .pg, .btn-sm 등 대시보드 전용 스타일 추가

빌드 결과

  • 경고 3건 (기존 경고 동일), 에러 0건 — 빌드 성공

주의 사항

  • 인증서 관련 코드(ExperionCertificateService.cs, 인증서 컨트롤러)는 일절 수정하지 않음

이름 필터 드롭다운 OR 조건 검색 (2026-04-14)

노드맵 대시보드의 이름 검색을 텍스트 입력에서 name 컬럼 고유값 풀다운 메뉴 4개로 교체, OR 조건 최대 4개 동시 선택 가능하도록 확장했다.

수정된 파일

파일 내용
src/Core/Application/Interfaces/IExperionServices.cs GetNameListAsync() 추가; QueryMasterAsync 파라미터 string? nameIEnumerable<string>? names
src/Infrastructure/Database/ExperionDbContext.cs GetNameListAsync() 구현 (distinct + 오름차순 정렬); QueryMasterAsync에서 nameList.Contains(x.Name) → EF가 WHERE name IN (...) SQL 생성
src/Web/Controllers/ExperionControllers.cs GET /api/nodemap/names 엔드포인트 추가; Query 액션 파라미터 string? nameList<string>? names (ASP.NET Core가 ?names=A&names=B 자동 바인딩)
src/Web/wwwroot/index.html "이름 검색" 텍스트 입력 제거 → nf-name-1 ~ nf-name-4 4개 <select> 드롭다운 추가
src/Web/wwwroot/js/app.js nmLoad()에서 /api/nodemap/names 병렬 호출 후 4개 드롭다운 채우기; nmQuery()에서 선택 이름들을 params.append('names', nm)로 OR 전송; nmReset()에서 4개 드롭다운 초기화
src/Web/wwwroot/css/style.css .nm-name-selects (4열 그리드, 900px 이하 2열) 추가

빌드 결과

  • 경고 3건 (기존 경고 동일), 에러 0건 — 빌드 성공

구현 완료 (2026-04-14, todo.md 전항목)

빌드 결과

  • 경고 6건 (기존 3건 + 신규 3건 OPC SDK deprecated API 경고), 에러 0건 — 빌드 성공

버그 수정 이력 (2026-04-14)

버그 1 — OPC UA 연결 시 OS TCP 타임아웃(최대 127초) 문제

증상

  • 접속 테스트 버튼을 눌렀을 때 수분간 응답 없는 것처럼 보임
  • ExperionRealtimeService: "연결 오류, 30초 후 재시도" 로그가 매우 늦게 출력됨
  • 오류: System.Net.Sockets.SocketException (110): Connection timed out

원인

Linux에서 OPC UA 서버 IP가 응답 없음(firewall/unreachable)이면 OS TCP SYN 재전송 타임아웃이 최대 127초까지 걸림. TransportQuotas.OperationTimeout은 OPC UA 프로토콜 레벨 타임아웃이라 TCP connect 단계에는 적용되지 않음.

수정 파일

파일 수정 내용
ExperionOpcClient.cs SelectEndpointAsyncCancellationTokenSource(10초) 추가 — DiscoveryClient 생성 시 10초 타임아웃 적용
ExperionRealtimeService.cs 동일하게 SelectEndpointAsync 10초 타임아웃 적용

결과

서버 미응답 시 127초 대기 → 10초 이내 실패 처리


버그 2 — PostgreSQL sorry, too many clients already (SQLSTATE 53300)

증상

구독 시작 후 실시간 값 수신 시 터미널에 다량의 에러:

Npgsql.PostgresException (0x80004005): 53300: sorry, too many clients already
at ExperionDbService.UpdateLiveValueAsync(...)
at ExperionRealtimeService.<<OnNotification>b__0>d.MoveNext()

원인

OnNotification 콜백이 포인트마다 Task.Run → 새 DI 스코프 → 새 DbContext → 새 DB 커넥션을 열었음. 2000여개 포인트가 동시에 값 변경 콜백을 받으면 순식간에 PostgreSQL max_connections(기본 100) 초과.

값 변경 콜백 × 2000개 → Task.Run × 2000개 → DB 커넥션 × 2000개 → 💥

수정 파일

파일 수정 내용
IExperionServices.cs BatchUpdateLiveValuesAsync(IEnumerable<LiveValueUpdate>) 인터페이스 추가, LiveValueUpdate record 추가
ExperionDbContext.cs BatchUpdateLiveValuesAsync 구현 — 단일 DbContext에서 순차 ExecuteUpdateAsync
ExperionRealtimeService.cs OnNotification에서 Task.Run 제거 → ConcurrentDictionary에 최신값만 기록. 별도 FlushLoopAsync 태스크가 500ms마다 단일 DbContext로 배치 업데이트

수정 후 구조

값 변경 콜백 × N개 → ConcurrentDictionary[nodeId] = 최신값
                              ↓ 500ms마다
                   단일 DbContext → BatchUpdateLiveValuesAsync → DB 커넥션 1개

결과

  • DB 커넥션 동시 사용 수: 2000개 → 최대 1개
  • 500ms 내 중복 변경은 최신값 1건만 DB에 반영 (deduplication)
  • 빌드: 경고 6건(기존 동일), 에러 0건

버그 3 — 대시보드 탭 진입 시 자동 API 호출로 인한 CPU/브라우저 버벅임

증상

  • 노드맵 대시보드 탭 진입 시 CPU 과부하, 페이지 버벅임
  • 포인트빌더 탭 진입 시 동일 증상
  • 이력 조회 탭 진입 시 한참 동안 열리지 않음

원인 (항목별)

자동 호출 API 무거운 이유
노드맵 대시보드 /api/nodemap/stats + /api/nodemap/names + /api/nodemap/query stats: 5가지 집계 쿼리(COUNT×4, MAX, DISTINCT). 결과로 전체 조회까지 자동 실행
포인트빌더 /api/nodemap/names + /api/nodemap/stats stats 집계 쿼리 (포인트빌더 dataType 드롭다운 채우기 용도)
이력 조회 /api/history/tagnames → 드롭다운 8개에 2000개 옵션 삽입 8 × 2000 = 16,000개 DOM <option> 생성으로 브라우저 freeze

수정 내용

공통 원칙: 탭 진입 시 API 호출 0건. 사용자가 명시적으로 버튼을 눌렀을 때만 실행.

파일 수정 내용
app.js 탭 클릭 핸들러에서 nmLoad(), pbLoad(), histLoad() 자동 호출 제거
app.js nmReset() 에서 nmQuery(0) 자동 호출 제거
app.js nmLoad()nmLoadNames()로 분리 (이름 드롭다운만, 버튼 클릭 시 호출)
app.js nmLoad() 내부의 통계 카드 렌더링 + nmQuery(0) 자동 호출 제거
app.js pbLoad() 에서 /api/nodemap/stats 호출 제거
app.js histLoad() 는 유지하되 탭 자동 호출 제거, "▼ 옵션 불러오기" 버튼 클릭 시에만 실행
index.html 노드맵 대시보드: 통계 카드(nm-stat-row) 제거, 데이터타입 select → text input
index.html 노드맵 대시보드: 이름 드롭다운에 "▼ 옵션 불러오기" 버튼 추가
index.html 포인트빌더: 데이터타입 select 2개 → text input 2개 (Double, Int32 등 직접 입력)
index.html 이력 조회: 태그 선택 드롭다운에 "▼ 옵션 불러오기" 버튼 추가

결과 (탭별 진입 시 API 호출 수)

이전 이후
노드맵 대시보드 stats + names + query = 3건 0건
포인트빌더 names + stats = 2건 names = 1건
이력 조회 tagnames = 1건 + DOM 16,000개 생성 0건

주의 사항

  • /api/nodemap/stats 엔드포인트는 서버에 남아있으나 프론트엔드에서 호출하지 않음
  • 이름/태그 드롭다운은 "▼ 옵션 불러오기" 버튼으로 수동 로드
  • 데이터타입 필터는 text input 직접 입력 방식으로 변경 (API 불필요)

버그 4 — 포인트빌더 탭 진입 시 여전히 버벅임 (2026-04-14)

증상

버그 3 수정 이후에도 포인트빌더 탭 진입 시 버벅임 지속.

원인

버그 3 수정 시 탭 핸들러에서 pbLoad() 제거를 누락. app.jsif (tab === 'pb') pbLoad() 가 그대로 남아 있었음. pbLoad()/api/nodemap/names 호출 → 8개 드롭다운에 전체 name 목록 삽입 → DOM 부하.

수정 파일

파일 수정 내용
app.js 탭 핸들러에서 if (tab === 'pb') pbLoad() 제거
index.html 포인트빌더 이름 선택 레이블 옆에 "▼ 옵션 불러오기" 버튼 추가 (onclick="pbLoad()")

결과

포인트빌더 탭 진입 시 API 호출 0건


기능 추가 — 실시간 구독 자동 재시작 플래그 (2026-04-14)

배경

앱 재기동 시 구독이 자동으로 재시작되지 않아 매번 수동으로 구독 시작 버튼을 눌러야 했음. 히스토리 스냅샷이 구독 여부와 무관하게 무조건 실행되어 livevalue = NULL 행이 저장되는 문제도 존재.

설계

  • 구독 시작 시 서버 설정을 realtime_autostart.json 파일로 저장 (앱 실행 디렉토리)
  • 앱 기동 시 (IHostedService.StartAsync) 파일 존재 여부 확인 → 있으면 자동 구독 시작
  • 구독 중지 시 파일 삭제 → 재기동 후 자동 시작 없음
  • ExperionHistoryServiceIExperionRealtimeService.GetStatus().Running 확인 → OFF이면 스냅샷 건너뜀

수정 파일

파일 수정 내용
ExperionRealtimeService.cs StartAsync(cfg)realtime_autostart.json 저장; StopAsync() 시 파일 삭제; StartAsync(CancellationToken) (IHostedService)에서 파일 읽어 자동 재시작
ExperionHistoryService.cs IExperionRealtimeService 생성자 주입; 스냅샷 전 GetStatus().Running 체크 → false이면 continue

동작 흐름

구독 시작 버튼 → realtime_autostart.json 저장 → OPC UA 구독 시작
앱 재기동      → 파일 감지 → 자동 구독 시작
구독 중지 버튼 → 파일 삭제 → 재기동 후 자동 시작 안 함
히스토리 서비스 → Running=false이면 스냅샷 건너뜀

기능 추가 — 수동 포인트 추가 시 OPC UA 핫 추가 및 유효성 검증 (2026-04-14)

배경

수동으로 포인트를 추가해도 기존 구독에는 반영되지 않아 구독 재시작이 필요했음. 잘못된 node_id 입력 시 DB에만 저장되고 livevalue가 영원히 NULL인 문제도 존재.

설계

  • 수동 추가 시 DB 저장 후 구독 중이면 MonitoredItem 핫 추가 (ApplyChanges())
  • OPC UA 서버 응답 상태 확인 → bad 상태코드이면 subscription 제거 + DB 롤백 + 에러 반환
  • 구독 중이 아닌 경우 DB에만 저장 → 다음 구독 시작 시 자동 포함

수정 파일

파일 수정 내용
IExperionServices.cs IExperionRealtimeServiceAddMonitoredItemAsync(string nodeId) 추가 (반환: (bool Success, string Message))
ExperionRealtimeService.cs AddMonitoredItemAsync 구현 — MonitoredItem 생성, ApplyChanges(), 상태 확인, bad이면 롤백
ExperionControllers.cs ExperionPointBuilderControllerIExperionRealtimeService 주입; Add 엔드포인트에서 DB 저장 후 AddMonitoredItemAsync 호출 → 실패 시 DeleteRealtimePointAsync로 DB 롤백

동작 흐름

수동 추가 요청
  ├── DB 저장
  ├── 구독 중 아님 → 성공 ("다음 구독 시작 시 자동 포함")
  └── 구독 중
        ├── OPC UA ApplyChanges() → Good → 즉시 구독 포함, 성공
        └── OPC UA → Bad → subscription 제거 + DB 롤백 + 에러 반환

빌드 결과

  • 경고 8건 (기존 6건 + OPC SDK deprecated 2건), 에러 0건 — 빌드 성공

성능 분석 — 1,699포인트 기준 CPU 부하 추정 (2026-04-14)

전제 조건

  • 실시간 포인트: 1,699개
  • 히스토리 스냅샷 주기: 60초
  • 실시간 배치 flush 주기: 500ms

히스토리 스냅샷 (60초마다)

  • 작업: realtime_table 1,699행 SELECT → history_table INSERT 1,699행
  • 특성: 1분에 1번 순간 burst, 수십 ms 수준
  • 앱 CPU: EF Core 객체 생성 1,699개 → 거의 무시 가능
  • 결론: 평균 CPU 기여 < 1%

실시간 livevalue 갱신 (500ms마다 배치)

  • 작업: ExecuteUpdateAsync × (변경된 포인트 수)건 / 500ms
  • OPC UA는 값이 바뀔 때만 콜백 → 전 포인트가 동시에 변경되는 경우는 드묾
  • 실제 변경 수: 수십~수백건/500ms가 일반적
  • 결론: 변경 포인트 수에 비례, 대부분의 경우 낮음

종합

작업 주기 예상 CPU
히스토리 스냅샷 60초/회 무시 가능 (< 1%)
실시간 배치 업데이트 500ms/회 변경 포인트 수에 비례
합계 - 단일 코어 기준 5~15% 이내

실제 병목은 CPU보다 PostgreSQL I/O와 커넥션 처리쪽이 먼저 나타남. 현재 구조(단일 DbContext, 배치 flush)는 이미 최적화된 상태.


성능 분석 — 멀티모니터 4대 실시간 폴링 부하 (2026-04-14)

시나리오

  • 웹페이지에서 realtime_table 조회, 페이지당 200개, 2초 간격 갱신
  • 멀티모니터 4대에서 4개의 브라우저 탭/창이 동시 동작

부하 추정

항목 계산 평가
서버 요청 수 4탭 × 1회/2초 = 2 req/s 무시 가능
DB 쿼리 SELECT 200행 × 2회/s 경량
응답 크기 200행 × ~150 bytes ≈ 30KB/응답 소량
네트워크 4 × 30KB / 2s = 60KB/s 거의 없음
브라우저 RAM 탭당 60100MB × 4 = **240400MB** 보통 수준

결론: 서버 부하 크지 않음. 일반 개발용 PC(i5급, 8GB RAM)에서 충분히 감당 가능.

실질적 병목 — 브라우저 DOM 재렌더링

현재 pbRender()tbl.innerHTML로 테이블 전체를 교체하는 방식 (full re-render).

  • 200행 × 4탭 × 2초마다 전체 재생성 → 체감 가능한 CPU 사용

결정 사항

실시간 모니터링 페이지 구현 시 반드시 incremental DOM update 방식 사용

  • 이미 그려진 <td> 셀의 .textContent만 갱신 (값이 바뀐 셀만)
  • innerHTML 전체 교체 금지
  • 구조 변경(행 추가/삭제) 시에만 DOM 재구성 허용

구현 계획 (참고용)

Task 1 — RealtimeTable + 포인트빌더 대시보드

개요

  • realtime_table PostgreSQL 테이블 생성: id, tagname, node_id, livevalue, timestamp
  • tagname: node_id.Substring(node_id.LastIndexOf(':') + 1) (마지막 ':' 오른쪽 문자열, 없으면 전체)
  • 소스: node_map_master WHERE name IN (...) AND data_type = 'Double'

수정 파일

파일 내용
ExperionEntities.cs RealtimePoint 엔티티 추가 (realtime_table 매핑)
IExperionServices.cs IExperionDbServiceBuildRealtimeTableAsync, GetRealtimePointsAsync, AddRealtimePointAsync, DeleteRealtimePointAsync 추가
ExperionDbContext.cs DbSet<RealtimePoint>, 테이블 DDL, 4개 서비스 메서드 구현
ExperionControllers.cs ExperionPointBuilderController 추가 (POST /api/pointbuilder/build, GET /api/pointbuilder/points, POST /api/pointbuilder/add, DELETE /api/pointbuilder/{id})
index.html 06번 탭 '포인트빌더' 추가 — name 드롭다운 8개, dataType 드롭다운, 빌드 버튼, 수동 node_id 입력, 포인트 테이블
app.js pbLoad(), pbBuild(), pbAddManual(), pbDelete(id), pbRender() 구현
style.css 포인트빌더 전용 스타일 추가

설계 결정

  • BuildRealtimeTableAsync는 기존 레코드를 모두 지우고 재생성 (TRUNCATE + INSERT)
  • 수동 추가(AddRealtimePointAsync)는 tagname을 자동 추출해서 삽입
  • 약 2000건 → 페이지네이션 불필요, 전체 목록을 클라이언트 측 테이블로 렌더링

Task 2 — OPC UA 실시간 구독 (livevalue 업데이트)

개요

  • OPC UA Subscription + MonitoredItem API 사용 (값 변경 시에만 콜백)
  • IExperionRealtimeService 인터페이스 + ExperionRealtimeService BackgroundService 신규 파일
  • 서버 접속 설정은 appsettings.json에서 읽음 (기존 ExperionServerConfig 구조 재사용)
  • 값 변경 콜백 → realtime_table.livevalue 업데이트 + timestamp 갱신

수정 파일

파일 내용
IExperionServices.cs IExperionRealtimeService 인터페이스, IExperionDbServiceUpdateLiveValueAsync 추가
ExperionDbContext.cs UpdateLiveValueAsync 구현
ExperionRealtimeService.cs (신규) BackgroundService 구현 — Subscription 생성, MonitoredItem 등록, 콜백 처리
ExperionControllers.cs ExperionRealtimeController 추가 (POST /api/realtime/start, POST /api/realtime/stop, GET /api/realtime/status)
Program.cs AddHostedService<ExperionRealtimeService>() 등록
index.html + app.js 포인트빌더 탭에 실시간 시작/정지 버튼, 상태 표시, livevalue 폴링(3초) 추가

설계 결정

  • OPC UA Subscription: PublishingInterval = 1000ms
  • MonitoredItem: SamplingInterval = 500ms, DeadBandType = None
  • 값 변경 없으면 콜백 없음 → DB 업데이트 없음 (OPC UA 규약 준수)
  • 서비스 재시작 시 자동 재연결 로직 포함 (30초 재시도)

Task 3 — HistoryTable (시계열 스냅샷)

개요

  • history_table: id, tagname, node_id, value, recorded_at
  • ExperionHistoryService BackgroundService → 설정된 주기(기본 60초)마다 realtime_table 전체를 스냅샷
  • 주기는 appsettings.json: "HistoryIntervalSeconds": 60 에서 읽음

수정 파일

파일 내용
ExperionEntities.cs HistoryRecord 엔티티 추가 (history_table 매핑)
IExperionServices.cs IExperionDbServiceSnapshotToHistoryAsync 추가
ExperionDbContext.cs DbSet<HistoryRecord>, 테이블 DDL, SnapshotToHistoryAsync 구현
ExperionHistoryService.cs (신규) BackgroundService — 주기적 SnapshotToHistoryAsync 호출
Program.cs AddHostedService<ExperionHistoryService>() 등록

Task 4 — HistoryTable 웹페이지

개요

  • 07번 탭 '이력 조회' 추가
  • tagname 드롭다운 최대 8개 선택 (다중 선택으로 열 구성)
  • 시작 시간 / 종료 시간 범위 필터
  • 결과 테이블: tagname이 열 헤더, recorded_at이 행

수정 파일

파일 내용
IExperionServices.cs IExperionDbServiceGetTagNamesAsync, QueryHistoryAsync 추가; HistoryQueryResult record 추가
ExperionDbContext.cs GetTagNamesAsync, QueryHistoryAsync 구현
ExperionControllers.cs ExperionHistoryController 추가 (GET /api/history/tagnames, GET /api/history/query)
index.html 07번 탭 '이력 조회' + #pane-hist 섹션 추가
app.js histLoad(), histQuery(), histRender() 구현
style.css 이력 조회 전용 스타일 추가