diff --git a/backend-node/src/services/RoleService_backup.ts b/backend-node/src/services/RoleService_backup.ts deleted file mode 100644 index 2932a2cc..00000000 --- a/backend-node/src/services/RoleService_backup.ts +++ /dev/null @@ -1,554 +0,0 @@ -import { query } from "../database/db"; -import { logger } from "../utils/logger"; - -/** - * 권한 그룹 인터페이스 - */ -export interface RoleGroup { - objid: number; - authName: string; - authCode: string; - companyCode: string; - status: string; - writer: string; - regdate: Date; - memberCount?: number; - menuCount?: number; - memberNames?: string; -} - -/** - * 권한 그룹 멤버 인터페이스 - */ -export interface RoleMember { - objid: number; - masterObjid: number; - userId: string; - userName?: string; - deptName?: string; - positionName?: string; - writer: string; - regdate: Date; -} - -/** - * 메뉴 권한 인터페이스 - */ -export interface MenuPermission { - objid: number; - menuObjid: number; - authObjid: number; - menuName?: string; - createYn: string; - readYn: string; - updateYn: string; - deleteYn: string; - writer: string; - regdate: Date; -} - -/** - * 권한 그룹 서비스 - */ -export class RoleService { - /** - * 회사별 권한 그룹 목록 조회 - * @param companyCode - 회사 코드 (undefined 시 전체 조회) - * @param search - 검색어 - */ - static async getRoleGroups( - companyCode?: string, - search?: string - ): Promise { - try { - let sql = ` - SELECT - objid, - auth_name AS "authName", - auth_code AS "authCode", - company_code AS "companyCode", - status, - writer, - regdate, - (SELECT COUNT(*) FROM authority_sub_user asu WHERE asu.master_objid = am.objid) AS "memberCount", - (SELECT COUNT(*) FROM rel_menu_auth rma WHERE rma.auth_objid = am.objid) AS "menuCount", - (SELECT STRING_AGG(ui.user_name, ', ' ORDER BY ui.user_name) - FROM authority_sub_user asu - JOIN user_info ui ON asu.user_id = ui.user_id - WHERE asu.master_objid = am.objid) AS "memberNames" - FROM authority_master am - WHERE 1=1 - `; - - const params: any[] = []; - let paramIndex = 1; - - // 회사 코드 필터 (companyCode가 undefined면 전체 조회) - if (companyCode) { - sql += ` AND company_code = $${paramIndex}`; - params.push(companyCode); - paramIndex++; - } - - // 검색어 필터 - if (search && search.trim()) { - sql += ` AND (auth_name ILIKE $${paramIndex} OR auth_code ILIKE $${paramIndex})`; - params.push(`%${search.trim()}%`); - paramIndex++; - } - - sql += ` ORDER BY regdate DESC`; - - logger.info("권한 그룹 조회 SQL", { sql, params }); - const result = await query(sql, params); - logger.info("권한 그룹 조회 결과", { count: result.length }); - return result; - } catch (error) { - logger.error("권한 그룹 목록 조회 실패", { error, companyCode, search }); - throw error; - } - } - - /** - * 권한 그룹 상세 조회 - */ - static async getRoleGroupById(objid: number): Promise { - try { - const sql = ` - SELECT - objid, - auth_name AS "authName", - auth_code AS "authCode", - company_code AS "companyCode", - status, - writer, - regdate - FROM authority_master - WHERE objid = $1 - `; - - const result = await query(sql, [objid]); - return result.length > 0 ? result[0] : null; - } catch (error) { - logger.error("권한 그룹 상세 조회 실패", { error, objid }); - throw error; - } - } - - /** - * 권한 그룹 생성 - */ - static async createRoleGroup(data: { - authName: string; - authCode: string; - companyCode: string; - writer: string; - }): Promise { - try { - const sql = ` - INSERT INTO authority_master (objid, auth_name, auth_code, company_code, status, writer, regdate) - VALUES (nextval('seq_authority_master'), $1, $2, $3, 'active', $4, NOW()) - RETURNING objid, auth_name AS "authName", auth_code AS "authCode", - company_code AS "companyCode", status, writer, regdate - `; - - const result = await query(sql, [ - data.authName, - data.authCode, - data.companyCode, - data.writer, - ]); - - logger.info("권한 그룹 생성 성공", { - objid: result[0].objid, - authName: data.authName, - }); - return result[0]; - } catch (error) { - logger.error("권한 그룹 생성 실패", { error, data }); - throw error; - } - } - - /** - * 권한 그룹 수정 - */ - static async updateRoleGroup( - objid: number, - data: { - authName?: string; - authCode?: string; - status?: string; - } - ): Promise { - try { - const updates: string[] = []; - const params: any[] = []; - let paramIndex = 1; - - if (data.authName !== undefined) { - updates.push(`auth_name = $${paramIndex}`); - params.push(data.authName); - paramIndex++; - } - - if (data.authCode !== undefined) { - updates.push(`auth_code = $${paramIndex}`); - params.push(data.authCode); - paramIndex++; - } - - if (data.status !== undefined) { - updates.push(`status = $${paramIndex}`); - params.push(data.status); - paramIndex++; - } - - if (updates.length === 0) { - throw new Error("수정할 데이터가 없습니다"); - } - - params.push(objid); - - const sql = ` - UPDATE authority_master - SET ${updates.join(", ")} - WHERE objid = $${paramIndex} - RETURNING objid, auth_name AS "authName", auth_code AS "authCode", - company_code AS "companyCode", status, writer, regdate - `; - - const result = await query(sql, params); - - if (result.length === 0) { - throw new Error("권한 그룹을 찾을 수 없습니다"); - } - - logger.info("권한 그룹 수정 성공", { objid, updates }); - return result[0]; - } catch (error) { - logger.error("권한 그룹 수정 실패", { error, objid, data }); - throw error; - } - } - - /** - * 권한 그룹 삭제 - */ - static async deleteRoleGroup(objid: number): Promise { - try { - // CASCADE로 연결된 데이터도 함께 삭제됨 (authority_sub_user, rel_menu_auth) - await query("DELETE FROM authority_master WHERE objid = $1", [objid]); - logger.info("권한 그룹 삭제 성공", { objid }); - } catch (error) { - logger.error("권한 그룹 삭제 실패", { error, objid }); - throw error; - } - } - - /** - * 권한 그룹 멤버 목록 조회 - */ - static async getRoleMembers(masterObjid: number): Promise { - try { - const sql = ` - SELECT - asu.objid, - asu.master_objid AS "masterObjid", - asu.user_id AS "userId", - ui.user_name AS "userName", - ui.dept_name AS "deptName", - ui.position_name AS "positionName", - asu.writer, - asu.regdate - FROM authority_sub_user asu - JOIN user_info ui ON asu.user_id = ui.user_id - WHERE asu.master_objid = $1 - ORDER BY ui.user_name - `; - - const result = await query(sql, [masterObjid]); - return result; - } catch (error) { - logger.error("권한 그룹 멤버 조회 실패", { error, masterObjid }); - throw error; - } - } - - /** - * 권한 그룹 멤버 추가 (여러 명) - */ - static async addRoleMembers( - masterObjid: number, - userIds: string[], - writer: string - ): Promise { - try { - // 이미 존재하는 멤버 제외 - const existingSql = ` - SELECT user_id - FROM authority_sub_user - WHERE master_objid = $1 AND user_id = ANY($2) - `; - const existing = await query<{ user_id: string }>(existingSql, [ - masterObjid, - userIds, - ]); - const existingIds = new Set( - existing.map((row: { user_id: string }) => row.user_id) - ); - - const newUserIds = userIds.filter((userId) => !existingIds.has(userId)); - - if (newUserIds.length === 0) { - logger.info("추가할 신규 멤버가 없습니다", { masterObjid, userIds }); - return; - } - - // 배치 삽입 - const values = newUserIds - .map( - (_, index) => - `(nextval('seq_authority_sub_user'), $1, $${index + 2}, $${newUserIds.length + 2}, NOW())` - ) - .join(", "); - - const sql = ` - INSERT INTO authority_sub_user (objid, master_objid, user_id, writer, regdate) - VALUES ${values} - `; - - await query(sql, [masterObjid, ...newUserIds, writer]); - - // 히스토리 기록 - for (const userId of newUserIds) { - await this.insertAuthorityHistory(masterObjid, userId, "ADD", writer); - } - - logger.info("권한 그룹 멤버 추가 성공", { - masterObjid, - count: newUserIds.length, - }); - } catch (error) { - logger.error("권한 그룹 멤버 추가 실패", { error, masterObjid, userIds }); - throw error; - } - } - - /** - * 권한 그룹 멤버 제거 (여러 명) - */ - static async removeRoleMembers( - masterObjid: number, - userIds: string[], - writer: string - ): Promise { - try { - await query( - "DELETE FROM authority_sub_user WHERE master_objid = $1 AND user_id = ANY($2)", - [masterObjid, userIds] - ); - - // 히스토리 기록 - for (const userId of userIds) { - await this.insertAuthorityHistory( - masterObjid, - userId, - "REMOVE", - writer - ); - } - - logger.info("권한 그룹 멤버 제거 성공", { - masterObjid, - count: userIds.length, - }); - } catch (error) { - logger.error("권한 그룹 멤버 제거 실패", { error, masterObjid, userIds }); - throw error; - } - } - - /** - * 권한 히스토리 기록 - */ - private static async insertAuthorityHistory( - masterObjid: number, - userId: string, - historyType: "ADD" | "REMOVE", - writer: string - ): Promise { - try { - const sql = ` - INSERT INTO authority_master_history - (objid, parent_objid, parent_name, parent_code, user_id, active, history_type, writer, reg_date) - SELECT - nextval('seq_authority_master'), - $1, - am.auth_name, - am.auth_code, - $2, - am.status, - $3, - $4, - NOW() - FROM authority_master am - WHERE am.objid = $1 - `; - - await query(sql, [masterObjid, userId, historyType, writer]); - } catch (error) { - logger.error("권한 히스토리 기록 실패", { - error, - masterObjid, - userId, - historyType, - }); - // 히스토리 기록 실패는 메인 작업을 중단하지 않음 - } - } - - /** - * 메뉴 권한 목록 조회 - */ - static async getMenuPermissions( - authObjid: number - ): Promise { - try { - const sql = ` - SELECT - rma.objid, - rma.menu_objid AS "menuObjid", - rma.auth_objid AS "authObjid", - mi.menu_name_kor AS "menuName", - mi.menu_code AS "menuCode", - mi.menu_url AS "menuUrl", - rma.create_yn AS "createYn", - rma.read_yn AS "readYn", - rma.update_yn AS "updateYn", - rma.delete_yn AS "deleteYn", - rma.execute_yn AS "executeYn", - rma.export_yn AS "exportYn", - rma.writer, - rma.regdate - FROM rel_menu_auth rma - LEFT JOIN menu_info mi ON rma.menu_objid = mi.objid - WHERE rma.auth_objid = $1 - ORDER BY mi.menu_name_kor - `; - - const result = await query(sql, [authObjid]); - return result; - } catch (error) { - logger.error("메뉴 권한 조회 실패", { error, authObjid }); - throw error; - } - } - - /** - * 메뉴 권한 설정 (여러 메뉴) - */ - static async setMenuPermissions( - authObjid: number, - permissions: Array<{ - menuObjid: number; - createYn: string; - readYn: string; - updateYn: string; - deleteYn: string; - }>, - writer: string - ): Promise { - try { - // 기존 권한 삭제 - await query("DELETE FROM rel_menu_auth WHERE auth_objid = $1", [ - authObjid, - ]); - - // 새로운 권한 삽입 - if (permissions.length > 0) { - const values = permissions - .map( - (_, index) => - `(nextval('seq_rel_menu_auth'), $${index * 5 + 2}, $1, $${index * 5 + 3}, $${index * 5 + 4}, $${index * 5 + 5}, $${index * 5 + 6}, $${permissions.length * 5 + 2}, NOW())` - ) - .join(", "); - - const params = permissions.flatMap((p) => [ - p.menuObjid, - p.createYn, - p.readYn, - p.updateYn, - p.deleteYn, - ]); - - const sql = ` - INSERT INTO rel_menu_auth (objid, menu_objid, auth_objid, create_yn, read_yn, update_yn, delete_yn, writer, regdate) - VALUES ${values} - `; - - await query(sql, [authObjid, ...params, writer]); - } - - logger.info("메뉴 권한 설정 성공", { - authObjid, - count: permissions.length, - }); - } catch (error) { - logger.error("메뉴 권한 설정 실패", { error, authObjid, permissions }); - throw error; - } - } - - /** - * 사용자가 속한 권한 그룹 목록 조회 - */ - static async getUserRoleGroups( - userId: string, - companyCode: string - ): Promise { - try { - const sql = ` - SELECT - am.objid, - am.auth_name AS "authName", - am.auth_code AS "authCode", - am.company_code AS "companyCode", - am.status, - am.writer, - am.regdate - FROM authority_master am - JOIN authority_sub_user asu ON am.objid = asu.master_objid - WHERE asu.user_id = $1 - AND am.company_code = $2 - AND am.status = 'active' - ORDER BY am.auth_name - `; - - const result = await query(sql, [userId, companyCode]); - return result; - } catch (error) { - logger.error("사용자 권한 그룹 조회 실패", { - error, - userId, - companyCode, - }); - throw error; - } - } - - /** - * 전체 메뉴 목록 조회 (권한 설정용) - */ - /** - * 전체 메뉴 목록 조회 (권한 설정용) - */ - static async getAllMenus(companyCode?: string): Promise { - try { - logger.info("🔍 전체 메뉴 목록 조회 시작", { companyCode }); - - let whereConditions: string[] = ["status = 'active'"]; - const params: any[] = []; - let paramIndex = 1; - - // 회사 코드 필터 (선택적) diff --git a/backend-node/src/services/RoleService_getAllMenus_fixed.ts b/backend-node/src/services/RoleService_getAllMenus_fixed.ts deleted file mode 100644 index 9dd1689d..00000000 --- a/backend-node/src/services/RoleService_getAllMenus_fixed.ts +++ /dev/null @@ -1,66 +0,0 @@ - /** - * 전체 메뉴 목록 조회 (권한 설정용) - */ - static async getAllMenus(companyCode?: string): Promise { - try { - logger.info("🔍 전체 메뉴 목록 조회 시작", { companyCode }); - - let whereConditions: string[] = ["status = 'active'"]; - const params: any[] = []; - let paramIndex = 1; - - // 회사 코드 필터 (선택적) - // 공통 메뉴(*)와 특정 회사 메뉴를 모두 조회 - if (companyCode) { - whereConditions.push(`(company_code = \$${paramIndex} OR company_code = '*')`); - params.push(companyCode); - paramIndex++; - logger.info("📋 회사 코드 필터 적용", { companyCode }); - } else { - logger.info("📋 회사 코드 필터 없음 (전체 조회)"); - } - - const whereClause = whereConditions.join(" AND "); - - const sql = ` - SELECT - objid, - menu_name_kor AS "menuName", - menu_name_eng AS "menuNameEng", - menu_code AS "menuCode", - menu_url AS "menuUrl", - menu_type AS "menuType", - parent_obj_id AS "parentObjid", - seq AS "sortOrder", - company_code AS "companyCode" - FROM menu_info - WHERE ${whereClause} - ORDER BY seq, menu_name_kor - `; - - logger.info("🔍 SQL 쿼리 실행", { - whereClause, - params, - sql: sql.substring(0, 200) + "...", - }); - - const result = await query(sql, params); - - logger.info("✅ 메뉴 목록 조회 성공", { - count: result.length, - companyCode, - menus: result.map((m) => ({ - objid: m.objid, - name: m.menuName, - code: m.menuCode, - companyCode: m.companyCode, - })), - }); - - return result; - } catch (error) { - logger.error("❌ 메뉴 목록 조회 실패", { error, companyCode }); - throw error; - } - } -} diff --git a/frontend/components/admin/dashboard/VehicleMapConfigPanel.tsx b/frontend/components/admin/dashboard/VehicleMapConfigPanel.tsx index 55df37d0..9b9e9567 100644 --- a/frontend/components/admin/dashboard/VehicleMapConfigPanel.tsx +++ b/frontend/components/admin/dashboard/VehicleMapConfigPanel.tsx @@ -1,7 +1,9 @@ -'use client'; +"use client"; -import React, { useState, useCallback } from 'react'; -import { ChartConfig, QueryResult } from './types'; +import React, { useState, useCallback } from "react"; +import { ChartConfig, QueryResult, MarkerColorRule } from "./types"; +import { Plus, Trash2 } from "lucide-react"; +import { v4 as uuidv4 } from "uuid"; interface VehicleMapConfigPanelProps { config?: ChartConfig; @@ -18,24 +20,80 @@ export function VehicleMapConfigPanel({ config, queryResult, onConfigChange }: V const [currentConfig, setCurrentConfig] = useState(config || {}); // 설정 업데이트 - const updateConfig = useCallback((updates: Partial) => { - const newConfig = { ...currentConfig, ...updates }; - setCurrentConfig(newConfig); - onConfigChange(newConfig); - }, [currentConfig, onConfigChange]); + const updateConfig = useCallback( + (updates: Partial) => { + const newConfig = { ...currentConfig, ...updates }; + setCurrentConfig(newConfig); + onConfigChange(newConfig); + }, + [currentConfig, onConfigChange], + ); // 사용 가능한 컬럼 목록 const availableColumns = queryResult?.columns || []; const sampleData = queryResult?.rows?.[0] || {}; + // 마커 색상 모드 변경 + const handleMarkerColorModeChange = useCallback( + (mode: "single" | "conditional") => { + if (mode === "single") { + updateConfig({ + markerColorMode: "single", + markerColorColumn: undefined, + markerColorRules: undefined, + markerDefaultColor: "#3b82f6", // 파란색 + }); + } else { + updateConfig({ + markerColorMode: "conditional", + markerColorRules: [], + markerDefaultColor: "#6b7280", // 회색 + }); + } + }, + [updateConfig], + ); + + // 마커 색상 규칙 추가 + const addColorRule = useCallback(() => { + const newRule: MarkerColorRule = { + id: uuidv4(), + value: "", + color: "#3b82f6", + label: "", + }; + const currentRules = currentConfig.markerColorRules || []; + updateConfig({ markerColorRules: [...currentRules, newRule] }); + }, [currentConfig.markerColorRules, updateConfig]); + + // 마커 색상 규칙 삭제 + const deleteColorRule = useCallback( + (id: string) => { + const currentRules = currentConfig.markerColorRules || []; + updateConfig({ markerColorRules: currentRules.filter((rule) => rule.id !== id) }); + }, + [currentConfig.markerColorRules, updateConfig], + ); + + // 마커 색상 규칙 업데이트 + const updateColorRule = useCallback( + (id: string, updates: Partial) => { + const currentRules = currentConfig.markerColorRules || []; + updateConfig({ + markerColorRules: currentRules.map((rule) => (rule.id === id ? { ...rule, ...updates } : rule)), + }); + }, + [currentConfig.markerColorRules, updateConfig], + ); + return (

🗺️ 지도 설정

{/* 쿼리 결과가 없을 때 */} {!queryResult && ( -
-
+
+
💡 먼저 SQL 쿼리를 실행하여 데이터를 가져온 후 지도를 설정할 수 있습니다.
@@ -49,10 +107,10 @@ export function VehicleMapConfigPanel({ config, queryResult, onConfigChange }: V updateConfig({ title: e.target.value })} placeholder="차량 위치 지도" - className="w-full px-2 py-1.5 border border-gray-300 rounded-lg text-xs" + className="w-full rounded-lg border border-gray-300 px-2 py-1.5 text-xs" />
@@ -60,12 +118,12 @@ export function VehicleMapConfigPanel({ config, queryResult, onConfigChange }: V
updateConfig({ longitudeColumn: e.target.value })} - className="w-full px-2 py-1.5 border border-gray-300 rounded-lg text-xs" + className="w-full rounded-lg border border-gray-300 px-2 py-1.5 text-xs" > {availableColumns.map((col) => ( @@ -98,13 +156,11 @@ export function VehicleMapConfigPanel({ config, queryResult, onConfigChange }: V {/* 라벨 컬럼 (선택사항) */}
- +
- {/* 상태 컬럼 (선택사항) */} -
- - + {/* 마커 색상 설정 */} +
+
🎨 마커 색상 설정
+ + {/* 색상 모드 선택 */} +
+ +
+ + +
+
+ + {/* 단일 색상 모드 */} + {(currentConfig.markerColorMode || "single") === "single" && ( +
+ +
+ updateConfig({ markerDefaultColor: e.target.value })} + className="h-8 w-12 cursor-pointer rounded border border-gray-300" + /> + updateConfig({ markerDefaultColor: e.target.value })} + placeholder="#3b82f6" + className="flex-1 rounded-lg border border-gray-300 px-2 py-1.5 text-xs" + /> +
+

모든 마커가 동일한 색상으로 표시됩니다

+
+ )} + + {/* 조건부 색상 모드 */} + {currentConfig.markerColorMode === "conditional" && ( +
+ {/* 색상 조건 컬럼 선택 */} +
+ + +

이 컬럼의 값에 따라 마커 색상이 결정됩니다

+
+ + {/* 기본 색상 */} +
+ +
+ updateConfig({ markerDefaultColor: e.target.value })} + className="h-8 w-12 cursor-pointer rounded border border-gray-300" + /> + updateConfig({ markerDefaultColor: e.target.value })} + placeholder="#6b7280" + className="flex-1 rounded-lg border border-gray-300 px-2 py-1.5 text-xs" + /> +
+

규칙에 매칭되지 않는 경우 사용할 색상

+
+ + {/* 색상 규칙 목록 */} +
+
+ + +
+ + {/* 규칙 리스트 */} + {(currentConfig.markerColorRules || []).length === 0 ? ( +
+

추가 버튼을 눌러 색상 규칙을 만드세요

+
+ ) : ( +
+ {(currentConfig.markerColorRules || []).map((rule) => ( +
+ {/* 규칙 헤더 */} +
+ 규칙 + +
+ + {/* 조건 값 */} +
+ + updateColorRule(rule.id, { value: e.target.value })} + placeholder="예: active, inactive" + className="w-full rounded border border-gray-300 px-2 py-1 text-xs" + /> +
+ + {/* 색상 */} +
+ +
+ updateColorRule(rule.id, { color: e.target.value })} + className="h-8 w-12 cursor-pointer rounded border border-gray-300" + /> + updateColorRule(rule.id, { color: e.target.value })} + placeholder="#3b82f6" + className="flex-1 rounded border border-gray-300 px-2 py-1 text-xs" + /> +
+
+ + {/* 라벨 (선택사항) */} +
+ + updateColorRule(rule.id, { label: e.target.value })} + placeholder="예: 활성, 비활성" + className="w-full rounded border border-gray-300 px-2 py-1 text-xs" + /> +
+
+ ))} +
+ )} +
+
+ )}
{/* 날씨 정보 표시 옵션 */} -
- -

- 마커 팝업에 해당 위치의 날씨 정보를 함께 표시합니다 -

-
+
+ +

마커 팝업에 해당 위치의 날씨 정보를 함께 표시합니다

+
-
- -

- 현재 발효 중인 기상특보(주의보/경보)를 지도에 색상 영역으로 표시합니다 -

-
+
+ +

+ 현재 발효 중인 기상특보(주의보/경보)를 지도에 색상 영역으로 표시합니다 +

+
{/* 설정 미리보기 */} -
-
📋 설정 미리보기
-
-
위도: {currentConfig.latitudeColumn || '미설정'}
-
경도: {currentConfig.longitudeColumn || '미설정'}
-
라벨: {currentConfig.labelColumn || '없음'}
-
상태: {currentConfig.statusColumn || '없음'}
-
날씨 표시: {currentConfig.showWeather ? '활성화' : '비활성화'}
-
기상특보 표시: {currentConfig.showWeatherAlerts ? '활성화' : '비활성화'}
-
데이터 개수: {queryResult.rows.length}개
+
+
📋 설정 미리보기
+
+
+ 위도: {currentConfig.latitudeColumn || "미설정"} +
+
+ 경도: {currentConfig.longitudeColumn || "미설정"} +
+
+ 라벨: {currentConfig.labelColumn || "없음"} +
+
+ 색상 모드: {currentConfig.markerColorMode === "conditional" ? "조건부" : "단일"} +
+ {currentConfig.markerColorMode === "conditional" && ( + <> +
+ 색상 조건 컬럼: {currentConfig.markerColorColumn || "미설정"} +
+
+ 색상 규칙 개수: {(currentConfig.markerColorRules || []).length}개 +
+ + )} +
+ 날씨 표시: {currentConfig.showWeather ? "활성화" : "비활성화"} +
+
+ 기상특보 표시: {currentConfig.showWeatherAlerts ? "활성화" : "비활성화"} +
+
+ 데이터 개수: {queryResult.rows.length}개 +
{/* 필수 필드 확인 */} {(!currentConfig.latitudeColumn || !currentConfig.longitudeColumn) && ( -
-
+
+
⚠️ 위도와 경도 컬럼을 반드시 선택해야 지도에 표시할 수 있습니다.
@@ -192,4 +439,3 @@ export function VehicleMapConfigPanel({ config, queryResult, onConfigChange }: V
); } - diff --git a/frontend/components/admin/dashboard/types.ts b/frontend/components/admin/dashboard/types.ts index 096273c9..61be6e5b 100644 --- a/frontend/components/admin/dashboard/types.ts +++ b/frontend/components/admin/dashboard/types.ts @@ -205,6 +205,20 @@ export interface ChartConfig { statusColumn?: string; // 상태 컬럼 showWeather?: boolean; // 날씨 정보 표시 여부 showWeatherAlerts?: boolean; // 기상특보 영역 표시 여부 + + // 마커 색상 설정 + markerColorMode?: "single" | "conditional"; // 마커 색상 모드 (단일/조건부) + markerColorColumn?: string; // 색상 조건 컬럼 + markerColorRules?: MarkerColorRule[]; // 색상 규칙 배열 + markerDefaultColor?: string; // 기본 마커 색상 +} + +// 마커 색상 규칙 +export interface MarkerColorRule { + id: string; // 고유 ID + value: string; // 컬럼 값 (예: "active", "inactive") + color: string; // 마커 색상 (hex) + label?: string; // 라벨 (선택사항) } export interface QueryResult { diff --git a/frontend/components/dashboard/widgets/MapSummaryWidget.tsx b/frontend/components/dashboard/widgets/MapSummaryWidget.tsx index 58c49814..ae911260 100644 --- a/frontend/components/dashboard/widgets/MapSummaryWidget.tsx +++ b/frontend/components/dashboard/widgets/MapSummaryWidget.tsx @@ -42,6 +42,7 @@ interface MarkerData { name: string; info: any; weather?: WeatherData | null; + markerColor?: string; // 마커 색상 } // 테이블명 한글 번역 @@ -472,6 +473,33 @@ export default function MapSummaryWidget({ element }: MapSummaryWidgetProps) { const latCol = element.chartConfig?.latitudeColumn || "latitude"; const lngCol = element.chartConfig?.longitudeColumn || "longitude"; + // 마커 색상 결정 함수 + const getMarkerColor = (row: any): string => { + const colorMode = element.chartConfig?.markerColorMode || "single"; + + if (colorMode === "single") { + // 단일 색상 모드 + return element.chartConfig?.markerDefaultColor || "#3b82f6"; + } else { + // 조건부 색상 모드 + const colorColumn = element.chartConfig?.markerColorColumn; + const colorRules = element.chartConfig?.markerColorRules || []; + const defaultColor = element.chartConfig?.markerDefaultColor || "#6b7280"; + + if (!colorColumn || colorRules.length === 0) { + return defaultColor; + } + + // 컬럼 값 가져오기 + const columnValue = String(row[colorColumn] || ""); + + // 색상 규칙 매칭 + const matchedRule = colorRules.find((rule) => String(rule.value) === columnValue); + + return matchedRule ? matchedRule.color : defaultColor; + } + }; + // 유효한 좌표 필터링 및 마커 데이터 생성 const markerData = rows .filter((row: any) => row[latCol] && row[lngCol]) @@ -481,6 +509,7 @@ export default function MapSummaryWidget({ element }: MapSummaryWidgetProps) { name: row.name || row.vehicle_number || row.warehouse_name || row.customer_name || "알 수 없음", info: row, weather: null, + markerColor: getMarkerColor(row), // 마커 색상 추가 })); setMarkers(markerData); @@ -693,54 +722,81 @@ export default function MapSummaryWidget({ element }: MapSummaryWidgetProps) { })} {/* 마커 표시 */} - {markers.map((marker, idx) => ( - - -
- {/* 마커 정보 */} -
-
{marker.name}
- {Object.entries(marker.info) - .filter(([key]) => !["latitude", "longitude", "lat", "lng"].includes(key.toLowerCase())) - .map(([key, value]) => ( -
- {key}: {String(value)} -
- ))} -
+ {markers.map((marker, idx) => { + // Leaflet 커스텀 아이콘 생성 (클라이언트 사이드에서만) + let customIcon; + if (typeof window !== "undefined") { + const L = require("leaflet"); + customIcon = L.divIcon({ + className: "custom-marker", + html: ` +
+ `, + iconSize: [30, 30], + iconAnchor: [15, 15], + }); + } - {/* 날씨 정보 */} - {marker.weather && ( -
-
- {getWeatherIcon(marker.weather.weatherMain)} - 현재 날씨 -
-
{marker.weather.weatherDescription}
-
-
- 온도 - {marker.weather.temperature}°C -
-
- 체감온도 - {marker.weather.feelsLike}°C -
-
- 습도 - {marker.weather.humidity}% -
-
- 풍속 - {marker.weather.windSpeed} m/s -
-
+ return ( + + +
+ {/* 마커 정보 */} +
+
{marker.name}
+ {Object.entries(marker.info) + .filter(([key]) => !["latitude", "longitude", "lat", "lng"].includes(key.toLowerCase())) + .map(([key, value]) => ( +
+ {key}: {String(value)} +
+ ))}
- )} -
-
-
- ))} + + {/* 날씨 정보 */} + {marker.weather && ( +
+
+ {getWeatherIcon(marker.weather.weatherMain)} + 현재 날씨 +
+
{marker.weather.weatherDescription}
+
+
+ 온도 + {marker.weather.temperature}°C +
+
+ 체감온도 + {marker.weather.feelsLike}°C +
+
+ 습도 + {marker.weather.humidity}% +
+
+ 풍속 + {marker.weather.windSpeed} m/s +
+
+
+ )} +
+ + + ); + })} {/* 범례 (특보가 있을 때만 표시) */}