@things-factory/kpi 9.0.29 → 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 (73) 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 +370 -0
  4. package/client/google-map/google-map-loader.ts +29 -0
  5. package/client/pages/kpi-dashboard/cards/kpi-level1-card.ts +248 -0
  6. package/client/pages/kpi-dashboard/cards/kpi-level2-comparison.ts +369 -0
  7. package/client/pages/kpi-dashboard/cards/kpi-level3-comparison.ts +443 -0
  8. package/client/pages/kpi-dashboard/components/kpi-chart-toggle.ts +72 -0
  9. package/client/pages/kpi-dashboard/components/kpi-left-panel.ts +399 -0
  10. package/client/pages/kpi-dashboard/components/kpi-map-panel.ts +302 -0
  11. package/client/pages/kpi-dashboard/components/kpi-region-popup.ts +355 -0
  12. package/client/pages/kpi-dashboard/kpi-dashboard-map.ts +243 -0
  13. package/client/pages/kpi-dashboard/kpi-dashboard.ts +416 -0
  14. package/client/route.ts +4 -0
  15. package/dist-client/charts/kpi-mini-trend-chart.d.ts +14 -0
  16. package/dist-client/charts/kpi-mini-trend-chart.js +148 -0
  17. package/dist-client/charts/kpi-mini-trend-chart.js.map +1 -0
  18. package/dist-client/charts/kpi-trend-chart.d.ts +25 -0
  19. package/dist-client/charts/kpi-trend-chart.js +186 -0
  20. package/dist-client/charts/kpi-trend-chart.js.map +1 -0
  21. package/dist-client/google-map/common-google-map.d.ts +34 -0
  22. package/dist-client/google-map/common-google-map.js +333 -0
  23. package/dist-client/google-map/common-google-map.js.map +1 -0
  24. package/dist-client/google-map/google-map-loader.d.ts +6 -0
  25. package/dist-client/google-map/google-map-loader.js +22 -0
  26. package/dist-client/google-map/google-map-loader.js.map +1 -0
  27. package/dist-client/pages/kpi-dashboard/cards/kpi-level1-card.d.ts +17 -0
  28. package/dist-client/pages/kpi-dashboard/cards/kpi-level1-card.js +279 -0
  29. package/dist-client/pages/kpi-dashboard/cards/kpi-level1-card.js.map +1 -0
  30. package/dist-client/pages/kpi-dashboard/cards/kpi-level2-comparison.d.ts +19 -0
  31. package/dist-client/pages/kpi-dashboard/cards/kpi-level2-comparison.js +385 -0
  32. package/dist-client/pages/kpi-dashboard/cards/kpi-level2-comparison.js.map +1 -0
  33. package/dist-client/pages/kpi-dashboard/cards/kpi-level3-comparison.d.ts +23 -0
  34. package/dist-client/pages/kpi-dashboard/cards/kpi-level3-comparison.js +465 -0
  35. package/dist-client/pages/kpi-dashboard/cards/kpi-level3-comparison.js.map +1 -0
  36. package/dist-client/pages/kpi-dashboard/components/kpi-chart-toggle.d.ts +8 -0
  37. package/dist-client/pages/kpi-dashboard/components/kpi-chart-toggle.js +78 -0
  38. package/dist-client/pages/kpi-dashboard/components/kpi-chart-toggle.js.map +1 -0
  39. package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.d.ts +22 -0
  40. package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.js +404 -0
  41. package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.js.map +1 -0
  42. package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.d.ts +28 -0
  43. package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.js +298 -0
  44. package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.js.map +1 -0
  45. package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.d.ts +23 -0
  46. package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.js +368 -0
  47. package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.js.map +1 -0
  48. package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.d.ts +29 -0
  49. package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.js +271 -0
  50. package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.js.map +1 -0
  51. package/dist-client/pages/kpi-dashboard/kpi-dashboard.d.ts +21 -0
  52. package/dist-client/pages/kpi-dashboard/kpi-dashboard.js +398 -0
  53. package/dist-client/pages/kpi-dashboard/kpi-dashboard.js.map +1 -1
  54. package/dist-client/route.d.ts +1 -1
  55. package/dist-client/route.js +3 -0
  56. package/dist-client/route.js.map +1 -1
  57. package/dist-client/tsconfig.tsbuildinfo +1 -1
  58. package/dist-server/index.d.ts +1 -0
  59. package/dist-server/index.js +1 -0
  60. package/dist-server/index.js.map +1 -1
  61. package/dist-server/migrations/index.d.ts +1 -0
  62. package/dist-server/migrations/index.js +12 -0
  63. package/dist-server/migrations/index.js.map +1 -0
  64. package/dist-server/tsconfig.tsbuildinfo +1 -1
  65. package/package.json +2 -2
  66. package/server/index.ts +1 -0
  67. package/server/migrations/index.ts +9 -0
  68. package/things-factory.config.js +2 -1
  69. package/translations/en.json +1 -0
  70. package/translations/ja.json +1 -0
  71. package/translations/ko.json +1 -0
  72. package/translations/ms.json +1 -0
  73. package/translations/zh.json +1 -0
@@ -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
+ }
@@ -0,0 +1,370 @@
1
+ import { LitElement, html, css } from 'lit'
2
+ import { customElement, property, state } from 'lit/decorators.js'
3
+ import { ScrollbarStyles } from '@operato/styles'
4
+
5
+ import GoogleMapLoader from './google-map-loader.js'
6
+
7
+ declare global {
8
+ interface Window {
9
+ google: any
10
+ markerClusterer?: any
11
+ }
12
+ }
13
+
14
+ declare const google: any
15
+
16
+ declare namespace google.maps {
17
+ interface MapsLibrary {
18
+ Map: any
19
+ InfoWindow: any
20
+ }
21
+
22
+ interface MarkerLibrary {
23
+ AdvancedMarkerElement: any
24
+ PinElement: any
25
+ }
26
+ }
27
+
28
+ @customElement('common-google-map')
29
+ export class CommonGoogleMap extends LitElement {
30
+ static styles = [
31
+ ScrollbarStyles,
32
+ css`
33
+ :host {
34
+ display: flex;
35
+ }
36
+
37
+ [map] {
38
+ flex: 1;
39
+ }
40
+
41
+ .gm-style .gm-style-iw-c {
42
+ padding: 0;
43
+ }
44
+
45
+ .gm-style .gm-style-iw-d {
46
+ overflow: auto !important;
47
+ }
48
+ .gm-style .gm-style-iw-d + button {
49
+ top: 0 !important;
50
+ right: 0 !important;
51
+ }
52
+ `
53
+ ]
54
+
55
+ @property({ type: Object }) center
56
+ @property({ type: Number }) zoom
57
+ @property({ type: Array }) locations: any[] = []
58
+ @property({ type: Object }) focused
59
+ @property({ type: Array }) polygons
60
+ @property({ type: Array }) polylines
61
+ @property({ type: Array }) markers
62
+ @property({ type: Array }) boundCoords
63
+ @property({ type: Object }) controls
64
+ @property({ type: Number }) clusterZoom = 10
65
+
66
+ @state() map: any = null
67
+ @state() defaultCenter: any = { lat: 36.5, lng: 127.5 }
68
+ private _infoWindow: any = null
69
+ private _markerClusterer: any = null
70
+
71
+ get anchor() {
72
+ return this.renderRoot.querySelector('[map]')
73
+ }
74
+
75
+ async readyMap() {
76
+ await GoogleMapLoader.load()
77
+
78
+ // MarkerClusterer 라이브러리 로드
79
+ if (!window.markerClusterer) {
80
+ await GoogleMapLoader.loadMarkerClusterer()
81
+ }
82
+
83
+ if (this.map) {
84
+ return
85
+ }
86
+
87
+ // DOM이 준비될 때까지 기다림
88
+ await this.updateComplete
89
+
90
+ // anchor가 준비될 때까지 기다림
91
+ let attempts = 0
92
+ const maxAttempts = 20
93
+ while (attempts < maxAttempts) {
94
+ const anchor = this.anchor as HTMLElement
95
+ if (anchor && anchor.offsetWidth > 0) {
96
+ break
97
+ }
98
+ await new Promise(resolve => setTimeout(resolve, 50))
99
+ attempts++
100
+ }
101
+
102
+ if (!this.anchor) {
103
+ console.error('Map anchor element not found')
104
+ return
105
+ }
106
+
107
+ var show = async (center, zoom) => {
108
+ try {
109
+ // Google Maps 최신 API 사용
110
+ const { Map } = (await google.maps.importLibrary('maps')) as google.maps.MapsLibrary
111
+
112
+ const mapOptions = {
113
+ zoom,
114
+ center,
115
+ mapId: 'DEMO_MAP_ID'
116
+ }
117
+
118
+ // controls 속성이 있으면 지도 옵션에 추가
119
+ if (this.controls) {
120
+ Object.assign(mapOptions, this.controls)
121
+ }
122
+
123
+ const map = new Map(this.anchor, mapOptions)
124
+
125
+ this.markers && this.markers.forEach(marker => marker.setMap(map))
126
+
127
+ this.map = map
128
+
129
+ this.dispatchEvent(
130
+ new CustomEvent('map-change', {
131
+ detail: this.map
132
+ })
133
+ )
134
+
135
+ this.resetBounds()
136
+ } catch (e) {
137
+ console.error(e)
138
+ }
139
+ }
140
+
141
+ var { center, zoom = 10 } = this
142
+
143
+ /* center 속성이 설정되어있지 않으면, 현재 위치를 구해서 지도의 center로 설정한다. */
144
+ if (!center && 'geolocation' in navigator && !this.boundCoords?.length) {
145
+ navigator.geolocation.getCurrentPosition(
146
+ ({ coords: { latitude: lat, longitude: lng } }) => show({ lat, lng }, zoom),
147
+ err => {
148
+ console.warn(`navigator.geolocation.getCurrentPosition failed. (${err.code}): ${err.message}`)
149
+ show(this.defaultCenter, zoom)
150
+ },
151
+ {
152
+ /* https://stackoverflow.com/questions/3397585/navigator-geolocation-getcurrentposition-sometimes-works-sometimes-doesnt */
153
+ timeout: 500
154
+ }
155
+ )
156
+ } else {
157
+ show(center, zoom)
158
+ }
159
+ }
160
+
161
+ async buildMarkers(locations: any[] = []) {
162
+ if (!this.map) {
163
+ return
164
+ }
165
+
166
+ if (this.markers) {
167
+ this.markers.forEach(marker => marker.setMap(null))
168
+ this.markers = []
169
+ }
170
+
171
+ // 기존 클러스터 제거
172
+ if (this._markerClusterer) {
173
+ this._markerClusterer.clearMarkers()
174
+ this._markerClusterer = null
175
+ }
176
+
177
+ // Google Maps 최신 API 사용
178
+ const { AdvancedMarkerElement, PinElement } = (await google.maps.importLibrary(
179
+ 'marker'
180
+ )) as google.maps.MarkerLibrary
181
+
182
+ this.markers = locations
183
+ .map(location => {
184
+ // location 객체가 유효한지 확인
185
+ if (!location || typeof location !== 'object') {
186
+ console.warn('Invalid location object:', location)
187
+ return null
188
+ }
189
+
190
+ // lat, lng 값이 유효한지 확인
191
+ const lat = parseFloat(location.lat)
192
+ const lng = parseFloat(location.lng)
193
+
194
+ if (isNaN(lat) || isNaN(lng)) {
195
+ console.warn('Invalid lat/lng values:', location)
196
+ return null
197
+ }
198
+
199
+ // LatLng 객체 생성
200
+ const position = new google.maps.LatLng(lat, lng)
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
+
219
+ // AdvancedMarkerElement 사용
220
+ const marker = new AdvancedMarkerElement({
221
+ position: position,
222
+ map: null, // 클러스터에서 관리하므로 지도에 직접 추가하지 않음
223
+ content: markerElement
224
+ })
225
+
226
+ marker.addListener('click', () => {
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
+ )
236
+ }
237
+ })
238
+
239
+ return marker
240
+ })
241
+ .filter(marker => marker !== null) // null 마커 제거
242
+
243
+ // Google Maps 공식 MarkerClusterer 사용 (예시와 동일한 방식)
244
+ if (this.markers.length > 0 && window.markerClusterer) {
245
+ this._markerClusterer = new window.markerClusterer.MarkerClusterer({
246
+ markers: this.markers,
247
+ map: this.map
248
+ })
249
+ }
250
+ }
251
+
252
+ get infoWindow() {
253
+ if (!this._infoWindow && this.map) {
254
+ this._infoWindow = new google.maps.InfoWindow({
255
+ content: 'loading...'
256
+ })
257
+ }
258
+
259
+ return this._infoWindow
260
+ }
261
+
262
+ setFocus(focus, icon) {
263
+ focus.setZIndex(1)
264
+ focus.setIcon(icon)
265
+ }
266
+
267
+ resetFocus(focus, icon) {
268
+ focus.setZIndex(0)
269
+ focus.setIcon(icon)
270
+ }
271
+
272
+ async changeFocus(after, before) {
273
+ await this.readyMap()
274
+
275
+ // map이 준비되지 않았으면 포커스 변경하지 않음
276
+ if (!this.map) {
277
+ return
278
+ }
279
+
280
+ var locations = this.locations || []
281
+
282
+ if (before) {
283
+ var idx = locations.findIndex(location => {
284
+ // location 객체의 구조를 안전하게 확인
285
+ const beforePos = before?.position
286
+ const locationPos = location?.position
287
+
288
+ return (
289
+ location?.name == before?.name && locationPos?.lat == beforePos?.lat && locationPos?.lng == beforePos?.lng
290
+ )
291
+ })
292
+ idx !== -1 && this.markers && this.resetFocus(this.markers[idx], locations[idx]?.icon)
293
+ }
294
+
295
+ if (after) {
296
+ var idx = locations.findIndex(location => {
297
+ // location 객체의 구조를 안전하게 확인
298
+ const afterPos = after?.position
299
+ const locationPos = location?.position
300
+
301
+ return location?.name == after?.name && locationPos?.lat == afterPos?.lat && locationPos?.lng == afterPos?.lng
302
+ })
303
+ idx !== -1 && this.markers && this.setFocus(this.markers[idx], after?.icon)
304
+ }
305
+ }
306
+
307
+ async updated(changes) {
308
+ if (!this.map) {
309
+ await this.readyMap()
310
+ }
311
+
312
+ if (changes.has('locations')) {
313
+ this.buildMarkers(this.locations)
314
+ }
315
+
316
+ if (changes.has('focused')) {
317
+ this.changeFocus(this.focused, changes.get('focused'))
318
+ }
319
+
320
+ if (changes.has('center')) {
321
+ this.map.setCenter(this.center)
322
+ }
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
+
332
+ if (changes.has('polygons')) {
333
+ ;(changes.get('polygons') || []).forEach(geofence => geofence.setMap(null))
334
+ ;(this.polygons || []).forEach(geofence => geofence.setMap(this.map))
335
+ }
336
+
337
+ if (changes.has('polylines')) {
338
+ ;(changes.get('polylines') || []).forEach(polyline => polyline.setMap(null))
339
+ ;(this.polylines || []).forEach(polyline => polyline.setMap(this.map))
340
+ }
341
+
342
+ if (changes.has('markers')) {
343
+ ;(changes.get('markers') || []).forEach(marker => marker.setMap(null))
344
+ ;(this.markers || []).forEach(marker => marker.setMap(this.map))
345
+ }
346
+
347
+ if (changes.has('boundCoords')) {
348
+ this.resetBounds()
349
+ }
350
+
351
+ // 클러스터링 설정 변경 시 마커 재구성
352
+ if (changes.has('clusterZoom')) {
353
+ this.buildMarkers(this.locations)
354
+ }
355
+ }
356
+
357
+ render() {
358
+ return html` <div map></div> `
359
+ }
360
+
361
+ resetBounds() {
362
+ if (!this.boundCoords || this.boundCoords.length < 1 || !this.map) {
363
+ return
364
+ }
365
+
366
+ var bounds = new google.maps.LatLngBounds()
367
+ this.boundCoords.forEach(coord => bounds.extend(coord))
368
+ this.map.fitBounds(bounds)
369
+ }
370
+ }
@@ -0,0 +1,29 @@
1
+ import ScriptLoader from '@operato/utils/script-loader.js'
2
+
3
+ export default class GoogleMapLoader {
4
+ static loaded = false
5
+ static markerClustererLoaded = false
6
+
7
+ static async load() {
8
+ if (GoogleMapLoader.loaded) {
9
+ return
10
+ }
11
+ var key = 'AIzaSyBgQZb-SFqjQBC_XTxNiz0XapejNwV9PgA'
12
+
13
+ await ScriptLoader.load(
14
+ 'https://maps.googleapis.com/maps/api/js' + (key ? '?key=' + key : '') + '&libraries=places'
15
+ )
16
+ GoogleMapLoader.loaded = true
17
+ }
18
+
19
+ static async loadMarkerClusterer() {
20
+ if (GoogleMapLoader.markerClustererLoaded) {
21
+ return
22
+ }
23
+
24
+ // Google Maps 공식 MarkerClusterer 라이브러리 로드
25
+ await ScriptLoader.load('https://unpkg.com/@googlemaps/markerclusterer/dist/index.min.js')
26
+
27
+ GoogleMapLoader.markerClustererLoaded = true
28
+ }
29
+ }