feat: 트렌드 워크스페이스(ECharts) 추가 + 이벤트히스토리 sub_area 정렬

트렌드 P1 (Tab 17 "트렌드"):
- 단일 ECharts 차트 슬롯구조(trState/TR_LAYERS/trRender) — P2/P3 무중단 증분 기반
- 기능: 그룹빌더(realtime 아날로그 클릭선택)/멀티시리즈·이중축/dataZoom(좌우날짜)/
  범례표(색변경·행클릭 강조·보이는구간 통계)/보이는범위 minmax 마커/라이브 현재값/
  트립·이벤트 오버레이(/api/event-history 재사용)/100%환산/Y줌
- 백엔드: trend_group 테이블 + v_analog_points 뷰(숫자 livevalue=아날로그) +
  TrendService/TrendController(/api/trend: analog-points·groups CRUD·live) + DI
- echarts 5.5.1 로컬 번들, DTO는 [JsonPropertyName]로 camelCase 고정

이벤트히스토리 sub_area 정렬(컬럼 일치):
- event_history_table section → sub_area (DDL/인덱스/엔티티/DTO/서비스/컨트롤러/evt UI/MCP/프롬프트)
- 이력 조회 PIVOT 재작성(MAX/CASE), ft.category '계기' → 'instrument'

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
windpacer
2026-05-25 17:42:54 +09:00
parent 930fac2b4f
commit c7e2250bd3
20 changed files with 1354 additions and 51 deletions

View File

@@ -1622,7 +1622,7 @@ async def query_events(
sql = f"""
SELECT id, tagname, prev_value, curr_value, event_type, event_time,
area, section, duration_seconds, metadata
area, sub_area, duration_seconds, metadata
FROM event_history_table
WHERE {' AND '.join(where)}
ORDER BY event_time DESC
@@ -1638,7 +1638,7 @@ async def query_events(
events = [
{"id": r[0], "tag_name": r[1], "prev_value": r[2], "curr_value": r[3],
"event_type": r[4], "event_time": r[5].isoformat() if r[5] else None,
"area": r[6], "section": r[7], "prev_state_duration_s": r[8], "metadata": r[9]}
"area": r[6], "sub_area": r[7], "prev_state_duration_s": r[8], "metadata": r[9]}
for r in rows
]
return json.dumps({
@@ -1678,13 +1678,13 @@ async def active_alarms(area: str | None = None, limit: int = 100) -> str:
WITH latest AS (
SELECT DISTINCT ON (tagname)
id, tagname, curr_value, event_type, event_time,
area, section, duration_seconds, metadata
area, sub_area, duration_seconds, metadata
FROM event_history_table
WHERE event_time >= NOW() - INTERVAL '30 days'
ORDER BY tagname, event_time DESC
)
SELECT id, tagname, curr_value, event_type, event_time,
area, section, duration_seconds, metadata,
area, sub_area, duration_seconds, metadata,
EXTRACT(EPOCH FROM (NOW() - event_time))::bigint AS age_seconds
FROM latest
WHERE event_type IN ('ALARM', 'TRIP')
@@ -1700,7 +1700,7 @@ async def active_alarms(area: str | None = None, limit: int = 100) -> str:
alarms = [
{"id": r[0], "tag_name": r[1], "curr_value": r[2], "event_type": r[3],
"since": r[4].isoformat() if r[4] else None,
"area": r[5], "section": r[6], "prev_state_duration_s": r[7], "metadata": r[8],
"area": r[5], "sub_area": r[6], "prev_state_duration_s": r[7], "metadata": r[8],
"age_seconds": r[9]}
for r in rows
]