@things-factory/kpi 9.0.30 → 9.0.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/client/charts/kpi-mini-trend-chart.ts +125 -0
  2. package/client/charts/kpi-trend-chart.ts +163 -0
  3. package/client/google-map/common-google-map.ts +45 -7
  4. package/client/google-map/google-map-loader.ts +1 -1
  5. package/client/pages/kpi-dashboard/components/kpi-chart-toggle.ts +1 -2
  6. package/client/pages/kpi-dashboard/components/kpi-left-panel.ts +399 -0
  7. package/client/pages/kpi-dashboard/components/kpi-map-panel.ts +110 -30
  8. package/client/pages/kpi-dashboard/components/kpi-region-popup.ts +355 -0
  9. package/client/pages/kpi-dashboard/kpi-dashboard-map.ts +46 -589
  10. package/dist-client/charts/kpi-mini-trend-chart.d.ts +14 -0
  11. package/dist-client/charts/kpi-mini-trend-chart.js +148 -0
  12. package/dist-client/charts/kpi-mini-trend-chart.js.map +1 -0
  13. package/dist-client/charts/kpi-trend-chart.d.ts +25 -0
  14. package/dist-client/charts/kpi-trend-chart.js +186 -0
  15. package/dist-client/charts/kpi-trend-chart.js.map +1 -0
  16. package/dist-client/google-map/common-google-map.js +40 -7
  17. package/dist-client/google-map/common-google-map.js.map +1 -1
  18. package/dist-client/google-map/google-map-loader.js +1 -1
  19. package/dist-client/google-map/google-map-loader.js.map +1 -1
  20. package/dist-client/pages/kpi-dashboard/components/kpi-chart-toggle.js +1 -2
  21. package/dist-client/pages/kpi-dashboard/components/kpi-chart-toggle.js.map +1 -1
  22. package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.d.ts +22 -0
  23. package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.js +404 -0
  24. package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.js.map +1 -0
  25. package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.d.ts +6 -1
  26. package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.js +100 -25
  27. package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.js.map +1 -1
  28. package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.d.ts +23 -0
  29. package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.js +368 -0
  30. package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.js.map +1 -0
  31. package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.d.ts +6 -15
  32. package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.js +43 -585
  33. package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.js.map +1 -1
  34. package/dist-client/tsconfig.tsbuildinfo +1 -1
  35. package/package.json +2 -2
  36. package/client/google-map/script-loader.ts +0 -173
  37. package/dist-client/google-map/script-loader.d.ts +0 -3
  38. package/dist-client/google-map/script-loader.js +0 -144
  39. package/dist-client/google-map/script-loader.js.map +0 -1
@@ -0,0 +1,125 @@
1
+ import { LitElement, html, css } from 'lit'
2
+ import { customElement, property } from 'lit/decorators.js'
3
+ import * as d3 from 'd3'
4
+
5
+ @customElement('kpi-mini-trend-chart')
6
+ export class KpiMiniTrendChart extends LitElement {
7
+ @property({ type: Array }) data: number[] = []
8
+ @property({ type: Number }) width: number = 60
9
+ @property({ type: Number }) height: number = 30
10
+ @property({ type: String }) lineColor: string = '#2196f3'
11
+ @property({ type: Number }) strokeWidth: number = 1.5
12
+ @property({ type: Boolean }) showPoints: boolean = true
13
+ @property({ type: Number }) pointRadius: number = 1.5
14
+
15
+ static styles = css`
16
+ :host {
17
+ display: block;
18
+ width: 100%;
19
+ height: 100%;
20
+ }
21
+
22
+ .mini-chart {
23
+ width: 100%;
24
+ height: 100%;
25
+ background: #f8f9fa;
26
+ border-radius: 4px;
27
+ display: flex;
28
+ align-items: center;
29
+ justify-content: center;
30
+ }
31
+
32
+ .trend-line {
33
+ fill: none;
34
+ stroke-linecap: round;
35
+ stroke-linejoin: round;
36
+ }
37
+
38
+ .data-point {
39
+ fill: white;
40
+ stroke-width: 1;
41
+ }
42
+ `
43
+
44
+ render() {
45
+ return html`
46
+ <div class="mini-chart">
47
+ <svg
48
+ id="mini-trend"
49
+ width=${this.width}
50
+ height=${this.height}
51
+ viewBox="0 0 ${this.width} ${this.height}"
52
+ preserveAspectRatio="xMidYMid meet"
53
+ ></svg>
54
+ </div>
55
+ `
56
+ }
57
+
58
+ updated() {
59
+ this.drawMiniTrend()
60
+ }
61
+
62
+ drawMiniTrend() {
63
+ if (!this.data || this.data.length === 0) return
64
+
65
+ const svg = d3.select(this.renderRoot.querySelector('#mini-trend'))
66
+ svg.selectAll('*').remove()
67
+
68
+ const margin = { top: 2, right: 2, bottom: 2, left: 2 }
69
+ const width = this.width - margin.left - margin.right
70
+ const height = this.height - margin.top - margin.bottom
71
+
72
+ // 스케일 설정
73
+ const xScale = d3
74
+ .scaleLinear()
75
+ .domain([0, this.data.length - 1])
76
+ .range([0, width])
77
+
78
+ const yScale = d3
79
+ .scaleLinear()
80
+ .domain([0, d3.max(this.data) || 100])
81
+ .range([height, 0])
82
+
83
+ // SVG 설정
84
+ svg.attr('width', this.width).attr('height', this.height)
85
+ const g = svg.append('g').attr('transform', `translate(${margin.left},${margin.top})`)
86
+
87
+ // 라인 생성기
88
+ const line = d3
89
+ .line<number>()
90
+ .x((d, i) => xScale(i))
91
+ .y(d => yScale(d))
92
+ .curve(d3.curveMonotoneX)
93
+
94
+ // 트렌드 라인 그리기
95
+ g.append('path')
96
+ .datum(this.data)
97
+ .attr('class', 'trend-line')
98
+ .attr('d', line as any)
99
+ .attr('stroke', this.lineColor)
100
+ .attr('stroke-width', this.strokeWidth)
101
+
102
+ // 데이터 포인트 그리기 (첫 번째와 마지막 포인트만)
103
+ if (this.showPoints && this.data.length > 0) {
104
+ // 첫 번째 포인트
105
+ g.append('circle')
106
+ .attr('class', 'data-point')
107
+ .attr('cx', xScale(0))
108
+ .attr('cy', yScale(this.data[0]))
109
+ .attr('r', this.pointRadius)
110
+ .attr('stroke', '#4caf50')
111
+ .attr('fill', 'white')
112
+
113
+ // 마지막 포인트
114
+ if (this.data.length > 1) {
115
+ g.append('circle')
116
+ .attr('class', 'data-point')
117
+ .attr('cx', xScale(this.data.length - 1))
118
+ .attr('cy', yScale(this.data[this.data.length - 1]))
119
+ .attr('r', this.pointRadius)
120
+ .attr('stroke', '#2196f3')
121
+ .attr('fill', 'white')
122
+ }
123
+ }
124
+ }
125
+ }
@@ -0,0 +1,163 @@
1
+ import { LitElement, html, css } from 'lit'
2
+ import { customElement, property } from 'lit/decorators.js'
3
+ import * as d3 from 'd3'
4
+
5
+ @customElement('kpi-trend-chart')
6
+ export class KpiTrendChart extends LitElement {
7
+ @property({ type: Array }) data: { date: string; value: number; color?: string }[] = []
8
+ @property({ type: String }) valueKey: string = 'value'
9
+ @property({ type: String }) dateKey: string = 'date'
10
+ @property({ type: Number }) width: number = 400
11
+ @property({ type: Number }) height: number = 200
12
+ @property({ type: String }) lineColor: string = '#2196f3'
13
+ @property({ type: Number }) strokeWidth: number = 2
14
+ @property({ type: Boolean }) showPoints: boolean = true
15
+ @property({ type: Number }) pointRadius: number = 4
16
+
17
+ private chartWidth = 0
18
+ private chartHeight = 0
19
+ private resizeObserver?: ResizeObserver
20
+
21
+ static styles = css`
22
+ :host {
23
+ display: block;
24
+ width: 100%;
25
+ height: 100%;
26
+ }
27
+
28
+ .chart-container {
29
+ width: 100%;
30
+ height: 100%;
31
+ }
32
+
33
+ .trend-line {
34
+ fill: none;
35
+ stroke-linecap: round;
36
+ stroke-linejoin: round;
37
+ }
38
+
39
+ .data-point {
40
+ fill: white;
41
+ stroke-width: 2;
42
+ }
43
+
44
+ .axis line,
45
+ .axis path {
46
+ stroke: #ddd;
47
+ }
48
+
49
+ .axis text {
50
+ font-size: 10px;
51
+ fill: #666;
52
+ }
53
+ `
54
+
55
+ render() {
56
+ return html`
57
+ <div class="chart-container">
58
+ <svg
59
+ id="trend-chart"
60
+ width=${this.chartWidth || this.width}
61
+ height=${this.chartHeight || this.height}
62
+ viewBox="0 0 ${this.chartWidth || this.width} ${this.chartHeight || this.height}"
63
+ preserveAspectRatio="xMidYMid meet"
64
+ ></svg>
65
+ </div>
66
+ `
67
+ }
68
+
69
+ connectedCallback() {
70
+ super.connectedCallback()
71
+ this.resizeObserver = new ResizeObserver(entries => {
72
+ for (const entry of entries) {
73
+ const rect = entry.contentRect
74
+ this.chartWidth = rect.width
75
+ this.chartHeight = rect.height
76
+ this.requestUpdate()
77
+ }
78
+ })
79
+ this.resizeObserver.observe(this)
80
+ }
81
+
82
+ disconnectedCallback() {
83
+ this.resizeObserver?.disconnect()
84
+ super.disconnectedCallback()
85
+ }
86
+
87
+ updated() {
88
+ this.drawTrendChart()
89
+ }
90
+
91
+ drawTrendChart() {
92
+ if (!this.data || this.data.length === 0) return
93
+
94
+ const svg = d3.select(this.renderRoot.querySelector('#trend-chart'))
95
+ svg.selectAll('*').remove()
96
+
97
+ const margin = { top: 20, right: 20, bottom: 30, left: 40 }
98
+ const width = this.chartWidth || this.width
99
+ const height = this.chartHeight || this.height
100
+ const chartWidth = width - margin.left - margin.right
101
+ const chartHeight = height - margin.top - margin.bottom
102
+
103
+ // 데이터 파싱
104
+ const parsedData = this.data.map(d => ({
105
+ date: new Date(d[this.dateKey]),
106
+ value: +d[this.valueKey],
107
+ color: d.color || this.lineColor
108
+ }))
109
+
110
+ // 스케일 설정
111
+ const xScale = d3
112
+ .scaleTime()
113
+ .domain(d3.extent(parsedData, d => d.date) as [Date, Date])
114
+ .range([0, chartWidth])
115
+
116
+ const yScale = d3
117
+ .scaleLinear()
118
+ .domain([0, d3.max(parsedData, d => d.value) || 100])
119
+ .range([chartHeight, 0])
120
+
121
+ // SVG 설정
122
+ svg.attr('width', width).attr('height', height)
123
+ const g = svg.append('g').attr('transform', `translate(${margin.left},${margin.top})`)
124
+
125
+ // 축 생성
126
+ const xAxis = d3.axisBottom(xScale).tickFormat(d3.timeFormat('%m/%d')).ticks(5)
127
+
128
+ const yAxis = d3.axisLeft(yScale).ticks(5)
129
+
130
+ g.append('g').attr('class', 'axis').attr('transform', `translate(0,${chartHeight})`).call(xAxis)
131
+
132
+ g.append('g').attr('class', 'axis').call(yAxis)
133
+
134
+ // 라인 생성기
135
+ const line = d3
136
+ .line<{ date: Date; value: number; color: string }>()
137
+ .x(d => xScale(d.date))
138
+ .y(d => yScale(d.value))
139
+ .curve(d3.curveMonotoneX)
140
+
141
+ // 트렌드 라인 그리기
142
+ g.append('path')
143
+ .datum(parsedData)
144
+ .attr('class', 'trend-line')
145
+ .attr('d', line as any)
146
+ .attr('stroke', this.lineColor)
147
+ .attr('stroke-width', this.strokeWidth)
148
+
149
+ // 데이터 포인트 그리기
150
+ if (this.showPoints) {
151
+ g.selectAll('.data-point')
152
+ .data(parsedData)
153
+ .enter()
154
+ .append('circle')
155
+ .attr('class', 'data-point')
156
+ .attr('cx', d => xScale(d.date))
157
+ .attr('cy', d => yScale(d.value))
158
+ .attr('r', this.pointRadius)
159
+ .attr('stroke', d => d.color)
160
+ .attr('fill', 'white')
161
+ }
162
+ }
163
+ }
@@ -109,11 +109,18 @@ export class CommonGoogleMap extends LitElement {
109
109
  // Google Maps 최신 API 사용
110
110
  const { Map } = (await google.maps.importLibrary('maps')) as google.maps.MapsLibrary
111
111
 
112
- const map = new Map(this.anchor, {
112
+ const mapOptions = {
113
113
  zoom,
114
114
  center,
115
115
  mapId: 'DEMO_MAP_ID'
116
- })
116
+ }
117
+
118
+ // controls 속성이 있으면 지도 옵션에 추가
119
+ if (this.controls) {
120
+ Object.assign(mapOptions, this.controls)
121
+ }
122
+
123
+ const map = new Map(this.anchor, mapOptions)
117
124
 
118
125
  this.markers && this.markers.forEach(marker => marker.setMap(map))
119
126
 
@@ -192,17 +199,40 @@ export class CommonGoogleMap extends LitElement {
192
199
  // LatLng 객체 생성
193
200
  const position = new google.maps.LatLng(lat, lng)
194
201
 
202
+ // 커스텀 마커 콘텐츠가 있으면 사용
203
+ let markerElement
204
+ if (location.markerContent) {
205
+ // HTML 문자열을 DOM 요소로 변환
206
+ const tempDiv = document.createElement('div')
207
+ tempDiv.innerHTML = location.markerContent
208
+ markerElement = tempDiv.firstElementChild
209
+ } else {
210
+ // 기본 핀 사용
211
+ markerElement = new PinElement({
212
+ background: '#1976d2',
213
+ borderColor: '#1565c0',
214
+ glyphColor: '#ffffff',
215
+ scale: 1.2
216
+ })
217
+ }
218
+
195
219
  // AdvancedMarkerElement 사용
196
220
  const marker = new AdvancedMarkerElement({
197
221
  position: position,
198
- map: null // 클러스터에서 관리하므로 지도에 직접 추가하지 않음
222
+ map: null, // 클러스터에서 관리하므로 지도에 직접 추가하지 않음
223
+ content: markerElement
199
224
  })
200
225
 
201
226
  marker.addListener('click', () => {
202
- if (location?.content) {
203
- var infowindow = this.infoWindow
204
- infowindow.open(this.map, marker)
205
- infowindow.setContent(location.content)
227
+ // InfoWindow 대신 커스텀 이벤트 발생
228
+ if (location?.region) {
229
+ this.dispatchEvent(
230
+ new CustomEvent('region-click', {
231
+ detail: { region: location.region },
232
+ bubbles: true,
233
+ composed: true
234
+ })
235
+ )
206
236
  }
207
237
  })
208
238
 
@@ -291,6 +321,14 @@ export class CommonGoogleMap extends LitElement {
291
321
  this.map.setCenter(this.center)
292
322
  }
293
323
 
324
+ if (changes.has('controls')) {
325
+ // controls가 변경되면 기존 지도의 옵션만 업데이트
326
+ if (this.map && this.controls) {
327
+ // Google Maps API의 setOptions 메서드 사용
328
+ this.map.setOptions(this.controls)
329
+ }
330
+ }
331
+
294
332
  if (changes.has('polygons')) {
295
333
  ;(changes.get('polygons') || []).forEach(geofence => geofence.setMap(null))
296
334
  ;(this.polygons || []).forEach(geofence => geofence.setMap(this.map))
@@ -1,4 +1,4 @@
1
- import ScriptLoader from './script-loader.js'
1
+ import ScriptLoader from '@operato/utils/script-loader.js'
2
2
 
3
3
  export default class GoogleMapLoader {
4
4
  static loaded = false
@@ -1,6 +1,5 @@
1
- import { html, css } from 'lit'
1
+ import { LitElement, html, css } from 'lit'
2
2
  import { customElement, property } from 'lit/decorators.js'
3
- import { LitElement } from 'lit'
4
3
 
5
4
  @customElement('kpi-chart-toggle')
6
5
  export class KpiChartToggle extends LitElement {