@zurigo/maps 1.0.6 → 1.0.8

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.
@@ -9,48 +9,71 @@ const solar_panel_calculator_1 = require("../lib/solar-panel-calculator");
9
9
  // Drawing constraints
10
10
  const MIN_ZOOM_FOR_DRAWING = 1; // Minimum zoom level to allow drawing (effectively disabled)
11
11
  const DEFAULT_MAX_AREA_LIMIT = 20000; // Default maximum area in m² for drawn shapes
12
+ // Color constants
13
+ const COLORS = {
14
+ // Shape colors
15
+ SHAPE_DEFAULT: "#f97316", // Bright orange - default state
16
+ SHAPE_SELECTED: "#dc2626", // Red - selected state
17
+ SHAPE_DRAWING: "#f97316", // Dark orange - drawing state
18
+ // Panel visualization colors
19
+ PANEL_FILL: "#fbbf24", // Golden yellow - panel fill
20
+ PANEL_STROKE: "#f59e0b", // Orange - panel stroke
21
+ };
12
22
  // Shape styles
13
23
  const RECTANGLE_STYLES = {
14
24
  default: {
15
- fillColor: "#1e40af",
25
+ fillColor: COLORS.SHAPE_DEFAULT,
16
26
  fillOpacity: 0.3,
17
- strokeColor: "#1e40af",
27
+ strokeColor: COLORS.SHAPE_DEFAULT,
18
28
  strokeWeight: 3,
19
29
  },
20
30
  selected: {
21
- fillColor: "#dc2626",
31
+ fillColor: COLORS.SHAPE_SELECTED,
22
32
  fillOpacity: 0.6,
23
- strokeColor: "#dc2626",
33
+ strokeColor: COLORS.SHAPE_SELECTED,
24
34
  strokeWeight: 5,
25
35
  },
26
36
  };
27
37
  const POLYGON_STYLES = {
28
38
  default: {
29
- fillColor: "#059669",
39
+ fillColor: COLORS.SHAPE_DEFAULT,
30
40
  fillOpacity: 0.3,
31
- strokeColor: "#059669",
41
+ strokeColor: COLORS.SHAPE_DEFAULT,
32
42
  strokeWeight: 2,
33
43
  },
34
44
  selected: {
35
- fillColor: "#dc2626",
45
+ fillColor: COLORS.SHAPE_SELECTED,
36
46
  fillOpacity: 0.5,
37
- strokeColor: "#dc2626",
47
+ strokeColor: COLORS.SHAPE_SELECTED,
38
48
  strokeWeight: 3,
39
49
  },
40
50
  };
41
51
  const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center, zoom, onMapLoad, className = "w-full h-full", animateToLocation = true, showMeasurements = false, clearTrigger, existingPolygons, readOnly = false, region = "korea", maxAreaLimit = DEFAULT_MAX_AREA_LIMIT, }) => {
52
+ // Text translations based on region
53
+ const texts = {
54
+ drawRectangle: region === "china" ? "Draw Rectangle" : "사각형으로 그리기",
55
+ drawPolygon: region === "china" ? "Draw Polygon" : "다각형으로 그리기",
56
+ panels: region === "china" ? "panels" : "개 패널",
57
+ areaTooLarge: region === "china"
58
+ ? "Shape is too large. Maximum allowed area is"
59
+ : "도형이 너무 큽니다. 최대 허용 면적은",
60
+ currentArea: region === "china" ? "Current area:" : "현재 면적:",
61
+ };
42
62
  const mapRef = (0, react_1.useRef)(null);
43
- const drawingManagerRef = (0, react_1.useRef)(null);
44
63
  const polygonsRef = (0, react_1.useRef)([]);
45
64
  const rectanglesRef = (0, react_1.useRef)([]);
46
65
  const measurementLabelsRef = (0, react_1.useRef)([]);
47
66
  const spacingWarningOverlaysRef = (0, react_1.useRef)([]);
48
67
  const capacityLabelsRef = (0, react_1.useRef)([]);
49
68
  const panelLayoutOverlaysRef = (0, react_1.useRef)([]); // 패널 레이아웃 오버레이용
69
+ const shapePanelMapRef = (0, react_1.useRef)(new WeakMap()); // 각 도형별 패널 추적
70
+ const shapeCapacityLabelMapRef = (0, react_1.useRef)(new WeakMap()); // 각 도형별 용량 라벨 추적
50
71
  const rotationHandleRef = (0, react_1.useRef)(null);
51
72
  const selectedRectangleRef = (0, react_1.useRef)(null);
52
73
  const selectedPolygonRef = (0, react_1.useRef)(null);
53
74
  const rotationListenerRef = (0, react_1.useRef)(null);
75
+ const startPointMarkerRef = (0, react_1.useRef)(null);
76
+ const vertexMarkersRef = (0, react_1.useRef)([]);
54
77
  const currentShapeRef = (0, react_1.useRef)(null);
55
78
  const [isAnimating, setIsAnimating] = (0, react_1.useState)(false);
56
79
  const [previousCenter, setPreviousCenter] = (0, react_1.useState)(center);
@@ -59,6 +82,7 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
59
82
  const [undoStack, setUndoStack] = (0, react_1.useState)([]);
60
83
  const [redoStack, setRedoStack] = (0, react_1.useState)([]);
61
84
  const [isRotating, setIsRotating] = (0, react_1.useState)(false);
85
+ const [activeDrawingMode, setActiveDrawingMode] = (0, react_1.useState)(null);
62
86
  // Suppress unused variable warning - redoStack is for future undo/redo implementation
63
87
  void redoStack;
64
88
  const mapOptions = (0, config_1.getMapOptions)(region, Object.assign({ center,
@@ -74,7 +98,7 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
74
98
  const { map, isLoaded, error } = (0, google_maps_1.useGoogleMap)(mapRef, Object.assign(Object.assign({}, mapOptions), { onMapLoad: (mapInstance) => {
75
99
  // readOnly 모드가 아닐 때만 Drawing Manager 초기화
76
100
  if (!readOnly) {
77
- initializeDrawingManager(mapInstance);
101
+ initializeDrawing(mapInstance);
78
102
  // Add map click listener to deselect shapes
79
103
  mapInstance.addListener("click", () => {
80
104
  // Add a small delay to allow shape click events to fire first
@@ -136,7 +160,7 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
136
160
  polygonsRef.current.push(polygon);
137
161
  // Add capacity label if capacity data exists
138
162
  if (polygonData.capacity && polygonData.centroid) {
139
- addExistingPolygonCapacityLabel(polygonData.centroid, polygonData.capacity, map, polygonData.geometry);
163
+ addExistingPolygonCapacityLabel(polygon, polygonData.centroid, polygonData.capacity, map, polygonData.geometry);
140
164
  }
141
165
  // Add panel layout visualization for existing polygon
142
166
  const layoutInfo = (0, solar_panel_calculator_1.calculatePanelLayoutFromPolygon)(polygon);
@@ -151,243 +175,505 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
151
175
  // Google Maps Geometry API를 사용한 간단한 면적 계산
152
176
  return google.maps.geometry.spherical.computeArea(path);
153
177
  };
154
- const initializeDrawingManager = (mapInstance) => {
155
- var _a, _b;
156
- if (!((_b = (_a = window.google) === null || _a === void 0 ? void 0 : _a.maps) === null || _b === void 0 ? void 0 : _b.drawing))
178
+ // Shared draw options for both preview and final shapes (mirrors the old
179
+ // DrawingManager polygonOptions / rectangleOptions).
180
+ const getPolygonDrawOptions = () => ({
181
+ fillColor: COLORS.SHAPE_DRAWING,
182
+ fillOpacity: 0.3,
183
+ strokeWeight: 3,
184
+ strokeColor: COLORS.SHAPE_DEFAULT,
185
+ strokeOpacity: 1.0,
186
+ zIndex: 1,
187
+ clickable: true,
188
+ draggable: true,
189
+ editable: false,
190
+ });
191
+ const getRectangleDrawOptions = () => ({
192
+ fillColor: COLORS.SHAPE_DRAWING,
193
+ fillOpacity: 0.3,
194
+ strokeWeight: 3,
195
+ strokeColor: COLORS.SHAPE_DEFAULT,
196
+ strokeOpacity: 1.0,
197
+ zIndex: 1,
198
+ clickable: true,
199
+ draggable: true,
200
+ editable: false,
201
+ });
202
+ // Post-completion wiring for a finished polygon. Identical behaviour to the
203
+ // old `polygoncomplete` listener body — only the shape's *creation* changed.
204
+ const finalizePolygon = (polygon, mapInstance) => {
205
+ // 먼저 간단한 면적 체크
206
+ const polygonPath = polygon.getPath();
207
+ const pathArray = [];
208
+ for (let i = 0; i < polygonPath.getLength(); i++) {
209
+ pathArray.push(polygonPath.getAt(i));
210
+ }
211
+ const simpleArea = calculateSimpleArea(pathArray);
212
+ // Check maximum area limit first (before heavy calculations)
213
+ if (simpleArea > maxAreaLimit) {
214
+ polygon.setMap(null);
215
+ alert(`${texts.areaTooLarge} ${maxAreaLimit.toLocaleString()}m².\n${texts.currentArea} ${simpleArea.toFixed(1).replace(/\B(?=(\d{3})+(?!\d))/g, ",")}m²`);
157
216
  return;
158
- const drawingModes = [
159
- google.maps.drawing.OverlayType.POLYGON,
160
- google.maps.drawing.OverlayType.RECTANGLE,
161
- ];
162
- const drawingManager = new google.maps.drawing.DrawingManager({
163
- drawingMode: null, // Start with no drawing mode active
164
- drawingControl: true, // Always show the drawing controls
165
- drawingControlOptions: {
166
- position: google.maps.ControlPosition.TOP_CENTER,
167
- drawingModes: drawingModes,
168
- },
169
- polygonOptions: {
170
- fillColor: "#10b981",
171
- fillOpacity: 0.3,
172
- strokeWeight: 3,
173
- strokeColor: "#059669",
174
- zIndex: 1,
175
- clickable: true,
176
- draggable: true,
177
- editable: false,
178
- },
179
- rectangleOptions: {
180
- fillColor: "#3b82f6",
181
- fillOpacity: 0.3,
182
- strokeWeight: 3,
183
- strokeColor: "#1e40af",
184
- zIndex: 1,
185
- clickable: true,
186
- draggable: true,
187
- editable: false,
188
- },
217
+ }
218
+ // Store polygon reference
219
+ polygonsRef.current.push(polygon);
220
+ // Add click listener for selection
221
+ polygon.addListener("click", (event) => {
222
+ event.stop();
223
+ selectPolygonWithMap(polygon, mapInstance);
189
224
  });
190
- drawingManager.setMap(mapInstance);
191
- drawingManagerRef.current = drawingManager;
192
- // Add zoom level listener to control drawing availability
193
- const updateDrawingControls = () => {
194
- const currentZoom = mapInstance.getZoom() || 0;
195
- if (currentZoom < MIN_ZOOM_FOR_DRAWING) {
196
- // Disable drawing controls and exit any active drawing mode
197
- drawingManager.setDrawingMode(null);
198
- drawingManager.setOptions({
199
- drawingControl: false,
200
- });
225
+ // Track dragging state to disable path listeners during drag
226
+ let isDragging = false;
227
+ polygon.addListener("dragstart", () => {
228
+ isDragging = true;
229
+ removeRotationHandle();
230
+ clearShapePanelOverlays(polygon);
231
+ clearShapeCapacityLabel(polygon);
232
+ });
233
+ polygon.addListener("dragend", () => {
234
+ isDragging = false;
235
+ if (selectedPolygon === polygon) {
236
+ setTimeout(() => {
237
+ addRotationHandle(polygon, mapInstance);
238
+ }, 50);
201
239
  }
202
- else {
203
- // Enable drawing controls
204
- drawingManager.setOptions({
205
- drawingControl: true,
206
- });
240
+ clearCapacityLabels();
241
+ clearPanelLayoutOverlays();
242
+ refreshAllLabelsAndVisualizationsWithCalculation(mapInstance);
243
+ });
244
+ // Add path change listeners for real-time editing
245
+ const path = polygon.getPath();
246
+ const handlePathChange = () => {
247
+ if (isDragging)
248
+ return;
249
+ savePolygonState(polygon);
250
+ onPolygonEdit === null || onPolygonEdit === void 0 ? void 0 : onPolygonEdit(polygon);
251
+ const targetMap = polygon.getMap();
252
+ if (targetMap) {
253
+ clearCapacityLabels();
254
+ clearPanelLayoutOverlays();
255
+ if (showMeasurements) {
256
+ clearMeasurements();
257
+ }
258
+ refreshAllLabelsAndVisualizationsWithCalculation(targetMap);
207
259
  }
208
260
  };
209
- // Initial check
210
- updateDrawingControls();
211
- // Listen for zoom changes
212
- mapInstance.addListener("zoom_changed", updateDrawingControls);
213
- drawingManager.addListener("polygoncomplete", (polygon) => {
214
- drawingManager.setDrawingMode(null);
215
- // 먼저 간단한 면적 체크
216
- const polygonPath = polygon.getPath();
217
- const pathArray = [];
218
- for (let i = 0; i < polygonPath.getLength(); i++) {
219
- pathArray.push(polygonPath.getAt(i));
220
- }
261
+ path.addListener("set_at", handlePathChange);
262
+ path.addListener("insert_at", handlePathChange);
263
+ path.addListener("remove_at", handlePathChange);
264
+ // 한 번만 계산하여 모든 작업 수행
265
+ const layoutInfo = (0, solar_panel_calculator_1.calculatePanelLayoutFromPolygon)(polygon);
266
+ addPolygonCapacityLabelWithLayoutInfo(polygon, layoutInfo, layoutInfo.capacity, mapInstance);
267
+ addPanelLayoutVisualizationWithLayoutInfo(polygon, layoutInfo, mapInstance);
268
+ if (showMeasurements) {
269
+ addPolygonMeasurementsWithLayoutInfo(polygon, layoutInfo, layoutInfo.capacity, mapInstance);
270
+ }
271
+ onPolygonComplete === null || onPolygonComplete === void 0 ? void 0 : onPolygonComplete(polygon);
272
+ // Auto-select the newly created polygon immediately
273
+ selectPolygonWithMap(polygon, mapInstance);
274
+ };
275
+ // Post-completion wiring for a finished rectangle. Identical behaviour to the
276
+ // old `rectanglecomplete` listener body.
277
+ const finalizeRectangle = (rectangle, mapInstance) => {
278
+ // 먼저 간단한 면적 체크
279
+ const rectBounds = rectangle.getBounds();
280
+ if (rectBounds) {
281
+ const ne = rectBounds.getNorthEast();
282
+ const sw = rectBounds.getSouthWest();
283
+ const nw = new google.maps.LatLng(ne.lat(), sw.lng());
284
+ const se = new google.maps.LatLng(sw.lat(), ne.lng());
285
+ const pathArray = [sw, nw, ne, se];
221
286
  const simpleArea = calculateSimpleArea(pathArray);
222
- // Check maximum area limit first (before heavy calculations)
223
287
  if (simpleArea > maxAreaLimit) {
224
- // Remove the polygon immediately
225
- polygon.setMap(null);
226
- // Show error message
227
- alert(`도형이 너무 큽니다. 최대 허용 면적은 ${maxAreaLimit.toLocaleString()}m²입니다.\n현재 면적: ${simpleArea
288
+ rectangle.setMap(null);
289
+ alert(`${texts.areaTooLarge} ${maxAreaLimit.toLocaleString()}m².\n${texts.currentArea} ${simpleArea
228
290
  .toFixed(1)
229
291
  .replace(/\B(?=(\d{3})+(?!\d))/g, ",")}m²`);
230
- return; // Exit early without processing
292
+ return;
231
293
  }
232
- // Store polygon reference
233
- polygonsRef.current.push(polygon);
234
- // Add click listener for selection
235
- polygon.addListener("click", (event) => {
236
- event.stop(); // Prevent map click event
237
- selectPolygonWithMap(polygon, mapInstance);
238
- });
239
- // Add drag listeners to hide/show rotation handle
240
- polygon.addListener("dragstart", () => {
241
- removeRotationHandle();
242
- });
243
- polygon.addListener("dragend", () => {
244
- if (selectedPolygon === polygon) {
245
- setTimeout(() => {
246
- addRotationHandle(polygon, mapInstance);
247
- }, 50);
248
- }
249
- // 기존 레이블들 제거하고 새로 그리기
250
- clearCapacityLabels();
251
- clearPanelLayoutOverlays();
252
- refreshAllLabelsAndVisualizationsWithCalculation(mapInstance);
253
- });
254
- // Add path change listeners for real-time editing
255
- const path = polygon.getPath();
256
- const handlePathChange = () => {
257
- savePolygonState(polygon);
258
- onPolygonEdit === null || onPolygonEdit === void 0 ? void 0 : onPolygonEdit(polygon);
259
- const targetMap = polygon.getMap();
294
+ }
295
+ // Store rectangle reference
296
+ rectanglesRef.current.push(rectangle);
297
+ rectangle.addListener("click", (event) => {
298
+ event.stop();
299
+ selectRectangleWithMap(rectangle, mapInstance);
300
+ });
301
+ rectangle.addListener("dragstart", () => {
302
+ removeRotationHandle();
303
+ clearShapePanelOverlays(rectangle);
304
+ clearShapeCapacityLabel(rectangle);
305
+ });
306
+ rectangle.addListener("dragend", () => {
307
+ if (selectedRectangle === rectangle) {
308
+ setTimeout(() => {
309
+ addRotationHandle(rectangle, mapInstance);
310
+ }, 50);
311
+ }
312
+ clearCapacityLabels();
313
+ clearPanelLayoutOverlays();
314
+ refreshAllLabelsAndVisualizationsWithCalculation(mapInstance);
315
+ });
316
+ rectangle.addListener("bounds_changed", () => {
317
+ saveRectangleState(rectangle);
318
+ const bounds = rectangle.getBounds();
319
+ if (bounds) {
320
+ const ne = bounds.getNorthEast();
321
+ const sw = bounds.getSouthWest();
322
+ const nw = new google.maps.LatLng(ne.lat(), sw.lng());
323
+ const se = new google.maps.LatLng(sw.lat(), ne.lng());
324
+ const polygonFromRect = new google.maps.Polygon({
325
+ paths: [sw, nw, ne, se],
326
+ });
327
+ onPolygonEdit === null || onPolygonEdit === void 0 ? void 0 : onPolygonEdit(polygonFromRect);
328
+ const targetMap = rectangle.getMap();
260
329
  if (targetMap) {
261
- // 한 번만 계산
262
- const layoutInfo = (0, solar_panel_calculator_1.calculatePanelLayoutFromPolygon)(polygon);
263
- // 기존 레이블들 제거하고 새로 그리기
264
330
  clearCapacityLabels();
265
331
  clearPanelLayoutOverlays();
266
332
  if (showMeasurements) {
267
333
  clearMeasurements();
268
334
  }
269
- // 모든 shapes에 대해 다시 그리기
270
335
  refreshAllLabelsAndVisualizationsWithCalculation(targetMap);
271
336
  }
272
- };
273
- path.addListener("set_at", handlePathChange);
274
- path.addListener("insert_at", handlePathChange);
275
- path.addListener("remove_at", handlePathChange);
276
- // 번만 계산하여 모든 작업 수행
277
- const layoutInfo = (0, solar_panel_calculator_1.calculatePanelLayoutFromPolygon)(polygon);
278
- // 계산 결과를 함수에 전달
279
- addPolygonCapacityLabelWithLayoutInfo(polygon, layoutInfo, layoutInfo.capacity, mapInstance);
280
- addPanelLayoutVisualizationWithLayoutInfo(polygon, layoutInfo, mapInstance);
337
+ }
338
+ });
339
+ const bounds = rectangle.getBounds();
340
+ if (bounds) {
341
+ const ne = bounds.getNorthEast();
342
+ const sw = bounds.getSouthWest();
343
+ const nw = new google.maps.LatLng(ne.lat(), sw.lng());
344
+ const se = new google.maps.LatLng(sw.lat(), ne.lng());
345
+ const rectPolygon = new google.maps.Polygon({
346
+ paths: [sw, nw, ne, se],
347
+ });
348
+ const layoutInfo = (0, solar_panel_calculator_1.calculatePanelLayoutFromPolygon)(rectPolygon);
349
+ addRectangleCapacityLabelWithLayoutInfo(rectangle, layoutInfo, layoutInfo.capacity, mapInstance);
350
+ addRectanglePanelLayoutVisualizationWithLayoutInfo(rectangle, layoutInfo, mapInstance);
281
351
  if (showMeasurements) {
282
- addPolygonMeasurementsWithLayoutInfo(polygon, layoutInfo, layoutInfo.capacity, mapInstance);
352
+ addRectangleMeasurementsWithLayoutInfo(rectangle, layoutInfo, layoutInfo.capacity, mapInstance);
283
353
  }
284
- onPolygonComplete === null || onPolygonComplete === void 0 ? void 0 : onPolygonComplete(polygon);
285
- // Auto-select the newly created polygon immediately
286
- selectPolygonWithMap(polygon, mapInstance);
354
+ const polygonFromRect = new google.maps.Polygon({
355
+ paths: [sw, nw, ne, se],
356
+ });
357
+ onPolygonComplete === null || onPolygonComplete === void 0 ? void 0 : onPolygonComplete(polygonFromRect);
358
+ selectRectangleWithMap(rectangle, mapInstance);
359
+ }
360
+ };
361
+ const initializeDrawing = (mapInstance) => {
362
+ // Drawing mode state (replaces DrawingManager.getDrawingMode/setDrawingMode).
363
+ let currentMode = null;
364
+ // In-progress polygon (click-based) state.
365
+ let tempPath = [];
366
+ // Open polyline preview (NOT a closed polygon) so the shape only looks
367
+ // "finished" once the user closes it on the start vertex / double-click.
368
+ let previewLine = null;
369
+ // In-progress rectangle (drag-based) state.
370
+ let rectStart = null;
371
+ let previewRect = null;
372
+ // Assigned once the buttons exist; safe to call as a no-op before then.
373
+ let setMode = () => { };
374
+ // Create custom drawing control buttons
375
+ const controlDiv = document.createElement("div");
376
+ controlDiv.style.margin = "10px";
377
+ controlDiv.style.display = "flex";
378
+ controlDiv.style.gap = "8px";
379
+ // Rectangle button
380
+ const rectangleButton = document.createElement("button");
381
+ rectangleButton.innerHTML = `
382
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
383
+ <rect x="3" y="3" width="18" height="18" rx="2"/>
384
+ </svg>
385
+ <span>${texts.drawRectangle}</span>
386
+ `;
387
+ rectangleButton.style.cssText = `
388
+ display: flex;
389
+ align-items: center;
390
+ gap: 6px;
391
+ background: white;
392
+ border: none;
393
+ padding: 10px 16px;
394
+ border-radius: 6px;
395
+ box-shadow: 0 2px 6px rgba(0,0,0,0.3);
396
+ cursor: pointer;
397
+ font-size: 14px;
398
+ font-weight: 500;
399
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
400
+ color: #333;
401
+ transition: all 0.2s;
402
+ `;
403
+ rectangleButton.addEventListener("mouseenter", () => {
404
+ rectangleButton.style.background = "#f3f4f6";
287
405
  });
288
- drawingManager.addListener("rectanglecomplete", (rectangle) => {
289
- drawingManager.setDrawingMode(null);
290
- // 먼저 간단한 면적 체크
291
- const rectBounds = rectangle.getBounds();
292
- if (rectBounds) {
293
- const ne = rectBounds.getNorthEast();
294
- const sw = rectBounds.getSouthWest();
295
- const nw = new google.maps.LatLng(ne.lat(), sw.lng());
296
- const se = new google.maps.LatLng(sw.lat(), ne.lng());
297
- const pathArray = [sw, nw, ne, se];
298
- const simpleArea = calculateSimpleArea(pathArray);
299
- // Check maximum area limit first (before heavy calculations)
300
- if (simpleArea > maxAreaLimit) {
301
- // Remove the rectangle immediately
302
- rectangle.setMap(null);
303
- // Show error message
304
- alert(`도형이 너무 큽니다. 최대 허용 면적은 ${maxAreaLimit.toLocaleString()}m²입니다.\n현재 면적: ${simpleArea
305
- .toFixed(1)
306
- .replace(/\B(?=(\d{3})+(?!\d))/g, ",")}m²`);
307
- return; // Exit early without processing
308
- }
406
+ rectangleButton.addEventListener("mouseleave", () => {
407
+ const isActive = currentMode === "rectangle";
408
+ rectangleButton.style.background = isActive ? "#3b82f6" : "white";
409
+ rectangleButton.style.color = isActive ? "white" : "#333";
410
+ });
411
+ rectangleButton.addEventListener("click", () => {
412
+ setMode(currentMode === "rectangle" ? null : "rectangle");
413
+ });
414
+ // Polygon button
415
+ const polygonButton = document.createElement("button");
416
+ polygonButton.innerHTML = `
417
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
418
+ <polygon points="12,2 22,8.5 22,15.5 12,22 2,15.5 2,8.5" stroke-linejoin="round"/>
419
+ </svg>
420
+ <span>${texts.drawPolygon}</span>
421
+ `;
422
+ polygonButton.style.cssText = `
423
+ display: flex;
424
+ align-items: center;
425
+ gap: 6px;
426
+ background: white;
427
+ border: none;
428
+ padding: 10px 16px;
429
+ border-radius: 6px;
430
+ box-shadow: 0 2px 6px rgba(0,0,0,0.3);
431
+ cursor: pointer;
432
+ font-size: 14px;
433
+ font-weight: 500;
434
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
435
+ color: #333;
436
+ transition: all 0.2s;
437
+ `;
438
+ polygonButton.addEventListener("mouseenter", () => {
439
+ polygonButton.style.background = "#f3f4f6";
440
+ });
441
+ polygonButton.addEventListener("mouseleave", () => {
442
+ const isActive = currentMode === "polygon";
443
+ polygonButton.style.background = isActive ? "#3b82f6" : "white";
444
+ polygonButton.style.color = isActive ? "white" : "#333";
445
+ });
446
+ polygonButton.addEventListener("click", () => {
447
+ setMode(currentMode === "polygon" ? null : "polygon");
448
+ });
449
+ controlDiv.appendChild(rectangleButton);
450
+ controlDiv.appendChild(polygonButton);
451
+ // Add custom controls to map
452
+ mapInstance.controls[google.maps.ControlPosition.TOP_CENTER].push(controlDiv);
453
+ // Add zoom level listener to control drawing availability
454
+ const updateDrawingControls = () => {
455
+ const currentZoom = mapInstance.getZoom() || 0;
456
+ if (currentZoom < MIN_ZOOM_FOR_DRAWING) {
457
+ // Disable drawing mode and hide custom controls
458
+ setMode(null);
459
+ controlDiv.style.display = "none";
309
460
  }
310
- // Store rectangle reference
311
- rectanglesRef.current.push(rectangle);
312
- // Add click listener for selection
313
- rectangle.addListener("click", (event) => {
314
- event.stop(); // Prevent map click event
315
- selectRectangleWithMap(rectangle, mapInstance);
461
+ else {
462
+ // Show custom controls
463
+ controlDiv.style.display = "flex";
464
+ }
465
+ };
466
+ // Initial check
467
+ updateDrawingControls();
468
+ // Listen for zoom changes
469
+ mapInstance.addListener("zoom_changed", updateDrawingControls);
470
+ const removeStartPointMarker = () => {
471
+ if (startPointMarkerRef.current) {
472
+ startPointMarkerRef.current.setMap(null);
473
+ startPointMarkerRef.current = null;
474
+ }
475
+ };
476
+ const addVertexMarker = (position, isStartPoint = false) => {
477
+ const marker = new google.maps.Marker({
478
+ position: position,
479
+ map: mapInstance,
480
+ icon: {
481
+ path: google.maps.SymbolPath.CIRCLE,
482
+ fillColor: isStartPoint ? "#10b981" : "#3b82f6", // Green for start, blue for other points
483
+ fillOpacity: 1,
484
+ strokeColor: "#ffffff",
485
+ strokeWeight: 2,
486
+ scale: isStartPoint ? 6 : 5, // Start point slightly larger
487
+ },
488
+ zIndex: 10000,
489
+ clickable: false,
316
490
  });
317
- // Add drag listeners to hide/show rotation handle
318
- rectangle.addListener("dragstart", () => {
319
- removeRotationHandle();
491
+ vertexMarkersRef.current.push(marker);
492
+ return marker;
493
+ };
494
+ const clearVertexMarkers = () => {
495
+ vertexMarkersRef.current.forEach((marker) => marker.setMap(null));
496
+ vertexMarkersRef.current = [];
497
+ };
498
+ // ── Custom click/drag drawing (replaces DrawingManager) ──────────────
499
+ const updateButtonStyles = () => {
500
+ const apply = (btn, active) => {
501
+ btn.style.background = active ? "#3b82f6" : "white";
502
+ btn.style.color = active ? "white" : "#333";
503
+ };
504
+ apply(rectangleButton, currentMode === "rectangle");
505
+ apply(polygonButton, currentMode === "polygon");
506
+ };
507
+ // Discard any in-progress polygon vertices/preview.
508
+ const cancelPolygonDrawing = () => {
509
+ tempPath = [];
510
+ if (previewLine) {
511
+ previewLine.setMap(null);
512
+ previewLine = null;
513
+ }
514
+ removeStartPointMarker();
515
+ clearVertexMarkers();
516
+ };
517
+ // Discard any in-progress rectangle drag.
518
+ const cancelRectangleDrawing = () => {
519
+ rectStart = null;
520
+ if (previewRect) {
521
+ previewRect.setMap(null);
522
+ previewRect = null;
523
+ }
524
+ };
525
+ setMode = (mode) => {
526
+ if (currentMode === mode)
527
+ return;
528
+ // Tear down whatever the previous mode left in progress.
529
+ if (currentMode === "polygon")
530
+ cancelPolygonDrawing();
531
+ if (currentMode === "rectangle")
532
+ cancelRectangleDrawing();
533
+ currentMode = mode;
534
+ setActiveDrawingMode(mode);
535
+ updateButtonStyles();
536
+ // Reset map interaction, then apply per-mode tweaks.
537
+ mapInstance.setOptions({
538
+ draggable: true,
539
+ disableDoubleClickZoom: false,
540
+ draggableCursor: null,
320
541
  });
321
- rectangle.addListener("dragend", () => {
322
- if (selectedRectangle === rectangle) {
323
- setTimeout(() => {
324
- addRotationHandle(rectangle, mapInstance);
325
- }, 50);
542
+ if (mode === "polygon") {
543
+ // Keep panning available between clicks, but suppress the dbl-click
544
+ // zoom (used to finish the polygon) and show a crosshair cursor.
545
+ mapInstance.setOptions({
546
+ disableDoubleClickZoom: true,
547
+ draggableCursor: "crosshair",
548
+ });
549
+ }
550
+ else if (mode === "rectangle") {
551
+ // Disable panning so the drag draws the rectangle instead.
552
+ mapInstance.setOptions({
553
+ draggable: false,
554
+ draggableCursor: "crosshair",
555
+ });
556
+ }
557
+ };
558
+ // ── Polygon: click to add vertices, dbl-click / start-point to close ──
559
+ const updatePolygonPreview = () => {
560
+ if (!previewLine) {
561
+ previewLine = new google.maps.Polyline({
562
+ strokeColor: COLORS.SHAPE_DEFAULT,
563
+ strokeOpacity: 1.0,
564
+ strokeWeight: 3,
565
+ clickable: false,
566
+ zIndex: 1,
567
+ map: mapInstance,
568
+ });
569
+ }
570
+ previewLine.setPath(tempPath);
571
+ };
572
+ const completePolygonDrawing = () => {
573
+ // Drop a trailing duplicate vertex (dbl-click adds the same point twice).
574
+ const pts = [...tempPath];
575
+ while (pts.length >= 2) {
576
+ const a = pts[pts.length - 1];
577
+ const b = pts[pts.length - 2];
578
+ if (Math.abs(a.lat() - b.lat()) < 1e-7 &&
579
+ Math.abs(a.lng() - b.lng()) < 1e-7) {
580
+ pts.pop();
326
581
  }
327
- // 기존 레이블들 제거하고 새로 그리기
328
- clearCapacityLabels();
329
- clearPanelLayoutOverlays();
330
- refreshAllLabelsAndVisualizationsWithCalculation(mapInstance);
331
- });
332
- // Add bounds change listeners for real-time editing
333
- rectangle.addListener("bounds_changed", () => {
334
- saveRectangleState(rectangle);
335
- // Convert rectangle to polygon for callback compatibility
336
- const bounds = rectangle.getBounds();
337
- if (bounds) {
338
- const ne = bounds.getNorthEast();
339
- const sw = bounds.getSouthWest();
340
- const nw = new google.maps.LatLng(ne.lat(), sw.lng());
341
- const se = new google.maps.LatLng(sw.lat(), ne.lng());
342
- // Create a polygon from rectangle bounds for callback
343
- const polygonFromRect = new google.maps.Polygon({
344
- paths: [sw, nw, ne, se],
345
- });
346
- onPolygonEdit === null || onPolygonEdit === void 0 ? void 0 : onPolygonEdit(polygonFromRect);
347
- const targetMap = rectangle.getMap();
348
- if (targetMap) {
349
- // 기존 레이블들 제거하고 새로 그리기
350
- clearCapacityLabels();
351
- clearPanelLayoutOverlays();
352
- if (showMeasurements) {
353
- clearMeasurements();
354
- }
355
- // 모든 shapes에 대해 다시 그리기
356
- refreshAllLabelsAndVisualizationsWithCalculation(targetMap);
357
- }
582
+ else {
583
+ break;
358
584
  }
359
- });
360
- // 번만 계산하여 모든 작업 수행
361
- const bounds = rectangle.getBounds();
362
- if (bounds) {
363
- const ne = bounds.getNorthEast();
364
- const sw = bounds.getSouthWest();
365
- const nw = new google.maps.LatLng(ne.lat(), sw.lng());
366
- const se = new google.maps.LatLng(sw.lat(), ne.lng());
367
- const rectPolygon = new google.maps.Polygon({
368
- paths: [sw, nw, ne, se],
369
- });
370
- const layoutInfo = (0, solar_panel_calculator_1.calculatePanelLayoutFromPolygon)(rectPolygon);
371
- // 계산 결과를 함수에 전달
372
- addRectangleCapacityLabelWithLayoutInfo(rectangle, layoutInfo, layoutInfo.capacity, mapInstance);
373
- addRectanglePanelLayoutVisualizationWithLayoutInfo(rectangle, layoutInfo, mapInstance);
374
- if (showMeasurements) {
375
- addRectangleMeasurementsWithLayoutInfo(rectangle, layoutInfo, layoutInfo.capacity, mapInstance);
585
+ }
586
+ if (pts.length < 3) {
587
+ cancelPolygonDrawing();
588
+ setMode(null);
589
+ return;
590
+ }
591
+ const polygon = new google.maps.Polygon(Object.assign(Object.assign({ paths: pts }, getPolygonDrawOptions()), { map: mapInstance }));
592
+ cancelPolygonDrawing();
593
+ setMode(null);
594
+ finalizePolygon(polygon, mapInstance);
595
+ };
596
+ mapInstance.addListener("click", (e) => {
597
+ if (currentMode !== "polygon" || !e.latLng)
598
+ return;
599
+ // Click on (or near) the start vertex closes the loop.
600
+ if (tempPath.length >= 3) {
601
+ const start = tempPath[0];
602
+ if (Math.abs(start.lat() - e.latLng.lat()) < 1e-7 &&
603
+ Math.abs(start.lng() - e.latLng.lng()) < 1e-7) {
604
+ completePolygonDrawing();
605
+ return;
376
606
  }
377
607
  }
378
- // Convert rectangle to polygon for callback compatibility
379
- if (bounds) {
380
- const ne = bounds.getNorthEast();
381
- const sw = bounds.getSouthWest();
382
- const nw = new google.maps.LatLng(ne.lat(), sw.lng());
383
- const se = new google.maps.LatLng(sw.lat(), ne.lng());
384
- const polygonFromRect = new google.maps.Polygon({
385
- paths: [sw, nw, ne, se],
608
+ tempPath = [...tempPath, e.latLng];
609
+ const isFirst = tempPath.length === 1;
610
+ const marker = addVertexMarker(e.latLng, isFirst);
611
+ if (isFirst) {
612
+ // Make the start marker clickable so users can close on it.
613
+ marker.setOptions({ clickable: true });
614
+ marker.addListener("click", () => {
615
+ if (currentMode === "polygon" && tempPath.length >= 3) {
616
+ completePolygonDrawing();
617
+ }
386
618
  });
387
- onPolygonComplete === null || onPolygonComplete === void 0 ? void 0 : onPolygonComplete(polygonFromRect);
388
- // Auto-select the newly created rectangle immediately
389
- selectRectangleWithMap(rectangle, mapInstance);
390
619
  }
620
+ updatePolygonPreview();
621
+ });
622
+ mapInstance.addListener("dblclick", () => {
623
+ if (currentMode === "polygon" && tempPath.length >= 3) {
624
+ completePolygonDrawing();
625
+ }
626
+ });
627
+ // Right-click / Escape cancels in-progress drawing.
628
+ mapInstance.addListener("rightclick", () => {
629
+ if (currentMode)
630
+ setMode(null);
631
+ });
632
+ document.addEventListener("keydown", (ev) => {
633
+ if (ev.key === "Escape" && currentMode)
634
+ setMode(null);
635
+ });
636
+ // ── Rectangle: press-drag-release ────────────────────────────────────
637
+ const boundsFromCorners = (a, b) => {
638
+ const bounds = new google.maps.LatLngBounds();
639
+ bounds.extend(a);
640
+ bounds.extend(b);
641
+ return bounds;
642
+ };
643
+ mapInstance.addListener("mousedown", (e) => {
644
+ if (currentMode !== "rectangle" || !e.latLng)
645
+ return;
646
+ rectStart = e.latLng;
647
+ previewRect = new google.maps.Rectangle(Object.assign(Object.assign({}, getRectangleDrawOptions()), { clickable: false, draggable: false, editable: false, map: mapInstance, bounds: boundsFromCorners(rectStart, rectStart) }));
648
+ });
649
+ mapInstance.addListener("mousemove", (e) => {
650
+ if (currentMode !== "rectangle" ||
651
+ !rectStart ||
652
+ !previewRect ||
653
+ !e.latLng)
654
+ return;
655
+ previewRect.setBounds(boundsFromCorners(rectStart, e.latLng));
656
+ });
657
+ mapInstance.addListener("mouseup", (e) => {
658
+ var _a;
659
+ if (currentMode !== "rectangle" || !rectStart)
660
+ return;
661
+ const start = rectStart;
662
+ const end = (_a = e.latLng) !== null && _a !== void 0 ? _a : start;
663
+ const bounds = boundsFromCorners(start, end);
664
+ if (previewRect) {
665
+ previewRect.setMap(null);
666
+ previewRect = null;
667
+ }
668
+ rectStart = null;
669
+ // Ignore zero-size drags (a plain click without movement).
670
+ if (bounds.getNorthEast().equals(bounds.getSouthWest())) {
671
+ setMode(null);
672
+ return;
673
+ }
674
+ const rectangle = new google.maps.Rectangle(Object.assign(Object.assign({}, getRectangleDrawOptions()), { bounds, map: mapInstance }));
675
+ setMode(null);
676
+ finalizeRectangle(rectangle, mapInstance);
391
677
  });
392
678
  };
393
679
  // Helper function to determine optimal zoom level
@@ -400,7 +686,7 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
400
686
  return 16; // Good default for solar site analysis
401
687
  };
402
688
  // Add capacity label for existing polygon
403
- const addExistingPolygonCapacityLabel = (centroid, capacity, mapInstance, geometry) => {
689
+ const addExistingPolygonCapacityLabel = (polygon, centroid, capacity, mapInstance, geometry) => {
404
690
  var _a;
405
691
  // Calculate label position above polygon if geometry is available
406
692
  let labelPosition;
@@ -439,7 +725,7 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
439
725
  const labelElement = document.createElement("div");
440
726
  labelElement.innerHTML = `
441
727
  <div style="background: rgba(255,255,255,0.95); padding: 6px 10px; border-radius: 6px; border: 1px solid #e5e7eb; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; pointer-events: none;">
442
- <div style="font-size: 14px; font-weight: 600; color: #059669; text-align: center;">
728
+ <div style="font-size: 14px; font-weight: 600; color: #2563eb; text-align: center;">
443
729
  ${formatCapacity(capacity)} kW
444
730
  </div>
445
731
  ${layoutInfo
@@ -472,12 +758,12 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
472
758
  encodeURIComponent(`
473
759
  <svg xmlns="http://www.w3.org/2000/svg" width="${layoutInfo ? "120" : "80"}" height="${layoutInfo ? "40" : "30"}" viewBox="0 0 ${layoutInfo ? "120" : "80"} ${layoutInfo ? "40" : "30"}">
474
760
  <rect x="0" y="0" width="${layoutInfo ? "120" : "80"}" height="${layoutInfo ? "40" : "30"}" fill="rgba(255,255,255,0.95)" stroke="#e5e7eb" stroke-width="1" rx="6"/>
475
- <text x="${layoutInfo ? "60" : "40"}" y="${layoutInfo ? "18" : "20"}" text-anchor="middle" font-family="Arial" font-size="14" font-weight="600" fill="#059669">
761
+ <text x="${layoutInfo ? "60" : "40"}" y="${layoutInfo ? "18" : "20"}" text-anchor="middle" font-family="Arial" font-size="14" font-weight="600" fill="#1e40af">
476
762
  ${formatCapacity(capacity)} kW
477
763
  </text>
478
764
  ${layoutInfo
479
765
  ? `<text x="60" y="32" text-anchor="middle" font-family="Arial" font-size="10" fill="#6b7280">
480
- ${layoutInfo.totalPanels} 패널
766
+ ${layoutInfo.totalPanels} ${texts.panels}
481
767
  </text>`
482
768
  : ""}
483
769
  </svg>
@@ -490,6 +776,8 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
490
776
  capacityMarker = marker;
491
777
  }
492
778
  capacityLabelsRef.current.push(capacityMarker);
779
+ // Store in WeakMap for shape-specific tracking
780
+ shapeCapacityLabelMapRef.current.set(polygon, capacityMarker);
493
781
  };
494
782
  // Polygon selection and editing functions
495
783
  // Helper functions for shape styling
@@ -605,10 +893,16 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
605
893
  selectPolygon(newPolygon);
606
894
  });
607
895
  // Add drag listeners
896
+ let isNewPolygonDragging = false;
608
897
  newPolygon.addListener("dragstart", () => {
898
+ isNewPolygonDragging = true;
609
899
  removeRotationHandle();
900
+ // Hide panel layout and capacity label for this shape only during drag
901
+ clearShapePanelOverlays(newPolygon);
902
+ clearShapeCapacityLabel(newPolygon);
610
903
  });
611
904
  newPolygon.addListener("dragend", () => {
905
+ isNewPolygonDragging = false;
612
906
  if (selectedPolygon === newPolygon && map) {
613
907
  setTimeout(() => {
614
908
  addRotationHandle(newPolygon, map);
@@ -622,6 +916,9 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
622
916
  const newPath = newPolygon.getPath();
623
917
  let newEditingTimer = null;
624
918
  const handleNewPathChange = () => {
919
+ // Skip path change handling during drag
920
+ if (isNewPolygonDragging)
921
+ return;
625
922
  savePolygonState(newPolygon);
626
923
  onPolygonEdit === null || onPolygonEdit === void 0 ? void 0 : onPolygonEdit(newPolygon);
627
924
  if (selectedPolygon === newPolygon) {
@@ -1064,10 +1361,16 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
1064
1361
  selectPolygonWithMap(rotatedPolygon, targetMap);
1065
1362
  });
1066
1363
  // Add drag listeners
1364
+ let isRotatedPolygonDragging = false;
1067
1365
  rotatedPolygon.addListener("dragstart", () => {
1366
+ isRotatedPolygonDragging = true;
1068
1367
  removeRotationHandle();
1368
+ // Hide panel layout and capacity label for this shape only during drag
1369
+ clearShapePanelOverlays(rotatedPolygon);
1370
+ clearShapeCapacityLabel(rotatedPolygon);
1069
1371
  });
1070
1372
  rotatedPolygon.addListener("dragend", () => {
1373
+ isRotatedPolygonDragging = false;
1071
1374
  if (selectedPolygon === rotatedPolygon) {
1072
1375
  setTimeout(() => {
1073
1376
  addRotationHandle(rotatedPolygon, targetMap);
@@ -1082,6 +1385,9 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
1082
1385
  const path = rotatedPolygon.getPath();
1083
1386
  let rotatedEditingTimer = null;
1084
1387
  const handleRotatedPathChange = () => {
1388
+ // Skip path change handling during drag
1389
+ if (isRotatedPolygonDragging)
1390
+ return;
1085
1391
  savePolygonState(rotatedPolygon);
1086
1392
  onPolygonEdit === null || onPolygonEdit === void 0 ? void 0 : onPolygonEdit(rotatedPolygon);
1087
1393
  // Update capacity label and measurements when converted polygon changes
@@ -1122,7 +1428,7 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
1122
1428
  setSelectedRectangle(null);
1123
1429
  setSelectedPolygon(rotatedPolygon);
1124
1430
  rotatedPolygon.setOptions({
1125
- strokeColor: "#dc2626",
1431
+ strokeColor: COLORS.SHAPE_SELECTED,
1126
1432
  strokeWeight: 3,
1127
1433
  fillOpacity: 0.5,
1128
1434
  });
@@ -1209,11 +1515,11 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
1209
1515
  const labelElement = document.createElement("div");
1210
1516
  labelElement.innerHTML = `
1211
1517
  <div style="background: rgba(255,255,255,0.95); padding: 6px 10px; border-radius: 6px; border: 1px solid #e5e7eb; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; pointer-events: none;">
1212
- <div style="font-size: 14px; font-weight: 600; color: #059669; text-align: center;">
1518
+ <div style="font-size: 14px; font-weight: 600; color: #2563eb; text-align: center;">
1213
1519
  ${formatCapacity(capacity)} kW
1214
1520
  </div>
1215
1521
  <div style="font-size: 11px; color: #6b7280; text-align: center; margin-top: 2px;">
1216
- ${layoutInfo.totalPanels} 패널
1522
+ ${layoutInfo.totalPanels} ${texts.panels}
1217
1523
  </div>
1218
1524
  </div>
1219
1525
  `;
@@ -1239,7 +1545,7 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
1239
1545
  encodeURIComponent(`
1240
1546
  <svg xmlns="http://www.w3.org/2000/svg" width="120" height="40" viewBox="0 0 120 40">
1241
1547
  <rect x="0" y="0" width="120" height="40" fill="rgba(255,255,255,0.95)" stroke="#e5e7eb" stroke-width="1" rx="6"/>
1242
- <text x="60" y="18" text-anchor="middle" font-family="Arial" font-size="14" font-weight="600" fill="#059669">
1548
+ <text x="60" y="18" text-anchor="middle" font-family="Arial" font-size="14" font-weight="600" fill="#1e40af">
1243
1549
  ${formatCapacity(capacity)} kW
1244
1550
  </text>
1245
1551
  <text x="60" y="32" text-anchor="middle" font-family="Arial" font-size="10" fill="#6b7280">
@@ -1255,6 +1561,8 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
1255
1561
  capacityMarker = marker;
1256
1562
  }
1257
1563
  capacityLabelsRef.current.push(capacityMarker);
1564
+ // Store in WeakMap for shape-specific tracking
1565
+ shapeCapacityLabelMapRef.current.set(polygon, capacityMarker);
1258
1566
  };
1259
1567
  const addRectangleCapacityLabelWithLayoutInfo = (rectangle, layoutInfo, capacity, mapInstance) => {
1260
1568
  var _a;
@@ -1275,7 +1583,7 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
1275
1583
  ${formatCapacity(capacity)} kW
1276
1584
  </div>
1277
1585
  <div style="font-size: 11px; color: #6b7280; text-align: center; margin-top: 2px;">
1278
- ${layoutInfo.totalPanels} 패널 (${layoutInfo.columns}×${layoutInfo.rows})
1586
+ ${layoutInfo.totalPanels} ${texts.panels} (${layoutInfo.columns}×${layoutInfo.rows})
1279
1587
  </div>
1280
1588
  </div>
1281
1589
  `;
@@ -1317,6 +1625,8 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
1317
1625
  capacityMarker = marker;
1318
1626
  }
1319
1627
  capacityLabelsRef.current.push(capacityMarker);
1628
+ // Store in WeakMap for shape-specific tracking
1629
+ shapeCapacityLabelMapRef.current.set(rectangle, capacityMarker);
1320
1630
  };
1321
1631
  // layoutInfo를 파라미터로 받는 측정 정보 함수들
1322
1632
  const addPolygonMeasurementsWithLayoutInfo = (polygon, layoutInfo, capacity, mapInstance) => {
@@ -1377,6 +1687,8 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
1377
1687
  if (layoutInfo.totalPanels === 0) {
1378
1688
  return;
1379
1689
  }
1690
+ // Track panels for this specific shape
1691
+ const shapePanels = [];
1380
1692
  try {
1381
1693
  // 새로운 그리드 기반 패널 위치 정보 사용
1382
1694
  if (layoutInfo.validPanelPositions &&
@@ -1424,16 +1736,19 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
1424
1736
  const panelPolygon = new google.maps.Polygon({
1425
1737
  paths: rotatedPanelCorners,
1426
1738
  map: mapInstance,
1427
- fillColor: "#fbbf24", // 황금색
1739
+ fillColor: COLORS.PANEL_FILL,
1428
1740
  fillOpacity: 0.4,
1429
- strokeColor: "#f59e0b",
1741
+ strokeColor: COLORS.PANEL_STROKE,
1430
1742
  strokeWeight: 1,
1431
1743
  strokeOpacity: 0.8,
1432
1744
  clickable: false,
1433
1745
  zIndex: 2000,
1434
1746
  });
1435
1747
  panelLayoutOverlaysRef.current.push(panelPolygon);
1748
+ shapePanels.push(panelPolygon);
1436
1749
  });
1750
+ // Store panels for this shape
1751
+ shapePanelMapRef.current.set(polygon, shapePanels);
1437
1752
  }
1438
1753
  else {
1439
1754
  // 기존 fallback 방식 사용
@@ -1449,6 +1764,8 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
1449
1764
  if (layoutInfo.totalPanels === 0) {
1450
1765
  return;
1451
1766
  }
1767
+ // Track panels for this specific shape
1768
+ const shapePanels = [];
1452
1769
  try {
1453
1770
  // 폴리곤의 바운딩 박스 계산
1454
1771
  const path = polygon.getPath();
@@ -1496,17 +1813,20 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
1496
1813
  const panelRect = new google.maps.Rectangle({
1497
1814
  bounds: panelBounds,
1498
1815
  map: mapInstance,
1499
- fillColor: "#fbbf24",
1816
+ fillColor: COLORS.PANEL_FILL,
1500
1817
  fillOpacity: 0.6,
1501
- strokeColor: "#f59e0b",
1818
+ strokeColor: COLORS.PANEL_STROKE,
1502
1819
  strokeWeight: 1,
1503
1820
  strokeOpacity: 0.8,
1504
1821
  clickable: false,
1505
1822
  zIndex: 2000,
1506
1823
  });
1507
1824
  panelLayoutOverlaysRef.current.push(panelRect);
1825
+ shapePanels.push(panelRect);
1508
1826
  }
1509
1827
  }
1828
+ // Store panels for this shape
1829
+ shapePanelMapRef.current.set(polygon, shapePanels);
1510
1830
  }
1511
1831
  catch (error) {
1512
1832
  console.warn("Failed to add fallback panel layout visualization:", error);
@@ -1516,6 +1836,8 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
1516
1836
  if (layoutInfo.totalPanels === 0) {
1517
1837
  return;
1518
1838
  }
1839
+ // Track panels for this specific shape
1840
+ const shapePanels = [];
1519
1841
  try {
1520
1842
  const bounds = rectangle.getBounds();
1521
1843
  if (!bounds)
@@ -1583,9 +1905,9 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
1583
1905
  const panelPolygon = new google.maps.Polygon({
1584
1906
  paths: rotatedCorners,
1585
1907
  map: mapInstance,
1586
- fillColor: "#fbbf24", // 황금색
1908
+ fillColor: COLORS.PANEL_FILL,
1587
1909
  fillOpacity: 0.6,
1588
- strokeColor: "#f59e0b",
1910
+ strokeColor: COLORS.PANEL_STROKE,
1589
1911
  strokeWeight: 1,
1590
1912
  strokeOpacity: 0.8,
1591
1913
  clickable: false,
@@ -1593,8 +1915,11 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
1593
1915
  });
1594
1916
  // Rectangle 타입 배열에 추가하기 위해 캐스팅 (실제로는 Polygon이지만)
1595
1917
  panelLayoutOverlaysRef.current.push(panelPolygon);
1918
+ shapePanels.push(panelPolygon);
1596
1919
  }
1597
1920
  }
1921
+ // Store panels for this shape
1922
+ shapePanelMapRef.current.set(rectangle, shapePanels);
1598
1923
  }
1599
1924
  catch (error) {
1600
1925
  console.warn("Failed to add rectangle panel layout visualization:", error);
@@ -1611,6 +1936,21 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
1611
1936
  });
1612
1937
  capacityLabelsRef.current = [];
1613
1938
  };
1939
+ // Clear capacity label for a specific shape only
1940
+ const clearShapeCapacityLabel = (shape) => {
1941
+ const label = shapeCapacityLabelMapRef.current.get(shape);
1942
+ if (label) {
1943
+ if (label instanceof google.maps.marker.AdvancedMarkerElement) {
1944
+ label.map = null;
1945
+ }
1946
+ else {
1947
+ label.setMap(null);
1948
+ }
1949
+ shapeCapacityLabelMapRef.current.delete(shape);
1950
+ // Also remove from main array
1951
+ capacityLabelsRef.current = capacityLabelsRef.current.filter((l) => l !== label);
1952
+ }
1953
+ };
1614
1954
  const refreshAllLabelsAndVisualizationsWithCalculation = (mapInstance) => {
1615
1955
  // Polygons 처리
1616
1956
  polygonsRef.current.forEach((polygon) => {
@@ -1653,6 +1993,20 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
1653
1993
  });
1654
1994
  panelLayoutOverlaysRef.current = [];
1655
1995
  };
1996
+ // Clear panel overlays for a specific shape only
1997
+ const clearShapePanelOverlays = (shape) => {
1998
+ const shapePanels = shapePanelMapRef.current.get(shape);
1999
+ if (shapePanels) {
2000
+ shapePanels.forEach((panel) => {
2001
+ if (panel && typeof panel.setMap === "function") {
2002
+ panel.setMap(null);
2003
+ }
2004
+ });
2005
+ shapePanelMapRef.current.delete(shape);
2006
+ // Also remove from main array
2007
+ panelLayoutOverlaysRef.current = panelLayoutOverlaysRef.current.filter((overlay) => !shapePanels.includes(overlay));
2008
+ }
2009
+ };
1656
2010
  const clearSpacingWarnings = () => {
1657
2011
  spacingWarningOverlaysRef.current.forEach((circle) => circle.setMap(null));
1658
2012
  spacingWarningOverlaysRef.current = [];
@@ -1715,9 +2069,6 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
1715
2069
  }, [showMeasurements, map]);
1716
2070
  (0, react_1.useEffect)(() => {
1717
2071
  return () => {
1718
- if (drawingManagerRef.current) {
1719
- drawingManagerRef.current.setMap(null);
1720
- }
1721
2072
  // Clean up polygons
1722
2073
  polygonsRef.current.forEach((polygon) => {
1723
2074
  polygon.setMap(null);
@@ -1737,9 +2088,9 @@ const GoogleMaps = ({ onPolygonComplete, onPolygonEdit, onPolygonDelete, center,
1737
2088
  };
1738
2089
  }, []);
1739
2090
  if (error) {
1740
- return ((0, jsx_runtime_1.jsx)("div", { className: `${className} relative`, children: (0, jsx_runtime_1.jsx)("div", { className: "absolute inset-0 flex items-center justify-center bg-red-50 border border-red-200 rounded", children: (0, jsx_runtime_1.jsxs)("div", { className: "text-red-600 text-center p-4", children: [(0, jsx_runtime_1.jsx)("p", { className: "font-medium", children: "Map loading failed" }), (0, jsx_runtime_1.jsx)("p", { className: "text-sm mt-1", children: error.message })] }) }) }));
2091
+ return ((0, jsx_runtime_1.jsx)("div", { className: `${className} relative`, children: (0, jsx_runtime_1.jsx)("div", { className: "absolute inset-0 flex items-center justify-center bg-red-50 border border-red-200", children: (0, jsx_runtime_1.jsxs)("div", { className: "text-red-600 text-center p-4", children: [(0, jsx_runtime_1.jsx)("p", { className: "font-medium", children: "Map loading failed" }), (0, jsx_runtime_1.jsx)("p", { className: "text-sm mt-1", children: error.message })] }) }) }));
1741
2092
  }
1742
- return ((0, jsx_runtime_1.jsxs)("div", { className: `${className} relative`, children: [(0, jsx_runtime_1.jsx)("div", { ref: mapRef, className: "w-full h-full rounded-lg" }), !isLoaded && ((0, jsx_runtime_1.jsx)("div", { className: "absolute inset-0 flex items-center justify-center bg-gray-100 rounded-lg", children: (0, jsx_runtime_1.jsxs)("div", { className: "text-gray-600 flex items-center space-x-2", children: [(0, jsx_runtime_1.jsx)("div", { className: "animate-spin rounded-full h-5 w-5 border-b-2 border-blue-600" }), (0, jsx_runtime_1.jsx)("span", { children: "Loading map..." })] }) })), isAnimating && ((0, jsx_runtime_1.jsxs)("div", { className: "absolute top-4 left-4 bg-blue-600 text-white px-3 py-1 rounded-full text-sm flex items-center space-x-2 shadow-lg", children: [(0, jsx_runtime_1.jsx)("div", { className: "animate-spin rounded-full h-3 w-3 border border-white border-t-transparent" }), (0, jsx_runtime_1.jsx)("span", { children: "Navigating to location..." })] }))] }));
2093
+ return ((0, jsx_runtime_1.jsxs)("div", { className: `${className} relative`, children: [(0, jsx_runtime_1.jsx)("div", { ref: mapRef, className: "w-full h-full" }), !isLoaded && ((0, jsx_runtime_1.jsx)("div", { className: "absolute inset-0 flex items-center justify-center bg-gray-100 ", children: (0, jsx_runtime_1.jsxs)("div", { className: "text-gray-600 flex items-center space-x-2", children: [(0, jsx_runtime_1.jsx)("div", { className: "animate-spin rounded-full h-5 w-5 border-b-2 border-blue-600" }), (0, jsx_runtime_1.jsx)("span", { children: "Loading map..." })] }) })), isAnimating && ((0, jsx_runtime_1.jsxs)("div", { className: "absolute top-4 left-4 bg-blue-600 text-white px-3 py-1 rounded-full text-sm flex items-center space-x-2 shadow-lg", children: [(0, jsx_runtime_1.jsx)("div", { className: "animate-spin rounded-full h-3 w-3 border border-white border-t-transparent" }), (0, jsx_runtime_1.jsx)("span", { children: "Navigating to location..." })] }))] }));
1743
2094
  };
1744
2095
  exports.default = GoogleMaps;
1745
2096
  //# sourceMappingURL=google-maps.js.map