+ |
{cargo.tracking_number || cargo.trackingNumber || "-"}
|
-
- {cargo.customer_name || cargo.customerName || "-"}
- |
-
- {cargo.destination || "-"}
- |
-
- {cargo.weight ? `${cargo.weight}kg` : "-"}
- |
+ {cargo.customer_name || cargo.customerName || "-"} |
+ {cargo.destination || "-"} |
+ {cargo.weight ? `${cargo.weight}kg` : "-"} |
);
}
-
diff --git a/frontend/components/dashboard/widgets/CustomStatsWidget.tsx b/frontend/components/dashboard/widgets/CustomStatsWidget.tsx
index 85f3cde0..a42be32e 100644
--- a/frontend/components/dashboard/widgets/CustomStatsWidget.tsx
+++ b/frontend/components/dashboard/widgets/CustomStatsWidget.tsx
@@ -41,11 +41,11 @@ export default function CustomStatsWidget({ element, refreshInterval = 60000 }:
const lastQueryRef = React.useRef(""); // 마지막 쿼리 추적
// localStorage 키 생성 (쿼리 기반으로 고유하게 - 편집/보기 모드 공유)
- const queryHash = element?.dataSource?.query
+ const queryHash = element?.dataSource?.query
? btoa(element.dataSource.query) // 전체 쿼리를 base64로 인코딩
: "default";
const storageKey = `custom-stats-widget-${queryHash}`;
-
+
// console.log("🔑 storageKey:", storageKey, "(쿼리:", element?.dataSource?.query?.substring(0, 30) + "...)");
// 쿼리가 변경되면 초기화 상태 리셋
@@ -148,174 +148,174 @@ export default function CustomStatsWidget({ element, refreshInterval = 60000 }:
// 3. 컬럼명 한글 번역 매핑
const columnNameTranslation: { [key: string]: string } = {
// 일반
- "id": "ID",
- "name": "이름",
- "title": "제목",
- "description": "설명",
- "status": "상태",
- "type": "유형",
- "category": "카테고리",
- "date": "날짜",
- "time": "시간",
- "created_at": "생성일",
- "updated_at": "수정일",
- "deleted_at": "삭제일",
-
+ id: "ID",
+ name: "이름",
+ title: "제목",
+ description: "설명",
+ status: "상태",
+ type: "유형",
+ category: "카테고리",
+ date: "날짜",
+ time: "시간",
+ created_at: "생성일",
+ updated_at: "수정일",
+ deleted_at: "삭제일",
+
// 물류/운송
- "tracking_number": "운송장 번호",
- "customer": "고객",
- "origin": "출발지",
- "destination": "목적지",
- "estimated_delivery": "예상 도착",
- "actual_delivery": "실제 도착",
- "delay_reason": "지연 사유",
- "priority": "우선순위",
- "cargo_weight": "화물 중량",
- "total_weight": "총 중량",
- "weight": "중량",
- "distance": "거리",
- "total_distance": "총 거리",
- "delivery_time": "배송 시간",
- "delivery_duration": "배송 소요시간",
- "is_on_time": "정시 도착 여부",
- "on_time": "정시",
-
+ tracking_number: "운송장 번호",
+ customer: "고객",
+ origin: "출발지",
+ destination: "목적지",
+ estimated_delivery: "예상 도착",
+ actual_delivery: "실제 도착",
+ delay_reason: "지연 사유",
+ priority: "우선순위",
+ cargo_weight: "화물 중량",
+ total_weight: "총 중량",
+ weight: "중량",
+ distance: "거리",
+ total_distance: "총 거리",
+ delivery_time: "배송 시간",
+ delivery_duration: "배송 소요시간",
+ is_on_time: "정시 도착 여부",
+ on_time: "정시",
+
// 수량/금액
- "quantity": "수량",
- "qty": "수량",
- "amount": "금액",
- "price": "가격",
- "cost": "비용",
- "fee": "수수료",
- "total": "합계",
- "sum": "총합",
-
+ quantity: "수량",
+ qty: "수량",
+ amount: "금액",
+ price: "가격",
+ cost: "비용",
+ fee: "수수료",
+ total: "합계",
+ sum: "총합",
+
// 비율/효율
- "rate": "비율",
- "ratio": "비율",
- "percent": "퍼센트",
- "percentage": "백분율",
- "efficiency": "효율",
-
+ rate: "비율",
+ ratio: "비율",
+ percent: "퍼센트",
+ percentage: "백분율",
+ efficiency: "효율",
+
// 생산/처리
- "throughput": "처리량",
- "output": "산출량",
- "production": "생산량",
- "volume": "용량",
-
+ throughput: "처리량",
+ output: "산출량",
+ production: "생산량",
+ volume: "용량",
+
// 재고/설비
- "stock": "재고",
- "inventory": "재고",
- "equipment": "설비",
- "facility": "시설",
- "machine": "기계",
-
+ stock: "재고",
+ inventory: "재고",
+ equipment: "설비",
+ facility: "시설",
+ machine: "기계",
+
// 평가
- "score": "점수",
- "rating": "평점",
- "point": "점수",
- "grade": "등급",
-
+ score: "점수",
+ rating: "평점",
+ point: "점수",
+ grade: "등급",
+
// 기타
- "temperature": "온도",
- "temp": "온도",
- "speed": "속도",
- "velocity": "속도",
- "count": "개수",
- "number": "번호",
+ temperature: "온도",
+ temp: "온도",
+ speed: "속도",
+ velocity: "속도",
+ count: "개수",
+ number: "번호",
};
// 4. 키워드 기반 자동 라벨링 및 단위 설정
const columnConfig: {
- [key: string]: {
- keywords: string[];
- unit: string;
- color: string;
- icon: string;
+ [key: string]: {
+ keywords: string[];
+ unit: string;
+ color: string;
+ icon: string;
aggregation: "sum" | "avg" | "max" | "min"; // 집계 방식
koreanLabel?: string; // 한글 라벨
};
} = {
// 무게/중량 - 합계
- weight: {
- keywords: ["weight", "cargo_weight", "total_weight", "tonnage", "ton"],
- unit: "톤",
- color: "green",
+ weight: {
+ keywords: ["weight", "cargo_weight", "total_weight", "tonnage", "ton"],
+ unit: "톤",
+ color: "green",
icon: "⚖️",
aggregation: "sum",
- koreanLabel: "총 운송량"
+ koreanLabel: "총 운송량",
},
// 거리 - 합계
- distance: {
- keywords: ["distance", "total_distance", "km", "kilometer"],
- unit: "km",
- color: "blue",
+ distance: {
+ keywords: ["distance", "total_distance", "km", "kilometer"],
+ unit: "km",
+ color: "blue",
icon: "🛣️",
aggregation: "sum",
- koreanLabel: "누적 거리"
+ koreanLabel: "누적 거리",
},
// 시간/기간 - 평균
- time: {
- keywords: ["time", "duration", "delivery_time", "delivery_duration", "hour", "minute"],
- unit: "분",
- color: "orange",
- icon: "⏱️",
+ time: {
+ keywords: ["time", "duration", "delivery_time", "delivery_duration", "hour", "minute"],
+ unit: "분",
+ color: "orange",
+ icon: "⏱️",
aggregation: "avg",
- koreanLabel: "평균 배송시간"
+ koreanLabel: "평균 배송시간",
},
// 수량/개수 - 합계
- quantity: {
- keywords: ["quantity", "qty", "count", "number"],
- unit: "개",
- color: "purple",
+ quantity: {
+ keywords: ["quantity", "qty", "count", "number"],
+ unit: "개",
+ color: "purple",
icon: "📦",
aggregation: "sum",
- koreanLabel: "총 수량"
+ koreanLabel: "총 수량",
},
// 금액/가격 - 합계
- amount: {
- keywords: ["amount", "price", "cost", "fee", "total", "sum"],
- unit: "원",
- color: "yellow",
+ amount: {
+ keywords: ["amount", "price", "cost", "fee", "total", "sum"],
+ unit: "원",
+ color: "yellow",
icon: "💰",
aggregation: "sum",
- koreanLabel: "총 금액"
+ koreanLabel: "총 금액",
},
// 비율/퍼센트 - 평균
- rate: {
- keywords: ["rate", "ratio", "percent", "efficiency", "%"],
- unit: "%",
- color: "cyan",
- icon: "📈",
+ rate: {
+ keywords: ["rate", "ratio", "percent", "efficiency", "%"],
+ unit: "%",
+ color: "cyan",
+ icon: "📈",
aggregation: "avg",
- koreanLabel: "평균 비율"
+ koreanLabel: "평균 비율",
},
// 처리량 - 합계
- throughput: {
- keywords: ["throughput", "output", "production", "volume"],
- unit: "개",
- color: "pink",
+ throughput: {
+ keywords: ["throughput", "output", "production", "volume"],
+ unit: "개",
+ color: "pink",
icon: "⚡",
aggregation: "sum",
- koreanLabel: "총 처리량"
+ koreanLabel: "총 처리량",
},
// 재고 - 평균 (현재 재고는 평균이 의미있음)
- stock: {
- keywords: ["stock", "inventory"],
- unit: "개",
- color: "teal",
+ stock: {
+ keywords: ["stock", "inventory"],
+ unit: "개",
+ color: "teal",
icon: "📦",
aggregation: "avg",
- koreanLabel: "평균 재고"
+ koreanLabel: "평균 재고",
},
// 설비/장비 - 평균
- equipment: {
- keywords: ["equipment", "facility", "machine"],
- unit: "대",
- color: "gray",
+ equipment: {
+ keywords: ["equipment", "facility", "machine"],
+ unit: "대",
+ color: "gray",
icon: "🏭",
aggregation: "avg",
- koreanLabel: "평균 가동 설비"
+ koreanLabel: "평균 가동 설비",
},
// 점수/평점 - 평균
score: {
@@ -324,7 +324,7 @@ export default function CustomStatsWidget({ element, refreshInterval = 60000 }:
color: "indigo",
icon: "⭐",
aggregation: "avg",
- koreanLabel: "평균 점수"
+ koreanLabel: "평균 점수",
},
// 온도 - 평균
temperature: {
@@ -333,7 +333,7 @@ export default function CustomStatsWidget({ element, refreshInterval = 60000 }:
color: "red",
icon: "🌡️",
aggregation: "avg",
- koreanLabel: "평균 온도"
+ koreanLabel: "평균 온도",
},
// 속도 - 평균
speed: {
@@ -342,7 +342,7 @@ export default function CustomStatsWidget({ element, refreshInterval = 60000 }:
color: "blue",
icon: "🚀",
aggregation: "avg",
- koreanLabel: "평균 속도"
+ koreanLabel: "평균 속도",
},
};
@@ -363,17 +363,19 @@ export default function CustomStatsWidget({ element, refreshInterval = 60000 }:
icon = config.icon;
aggregation = config.aggregation;
matchedConfig = config;
-
+
// 한글 라벨 사용 또는 자동 변환
if (config.koreanLabel) {
label = config.koreanLabel;
} else {
// 집계 방식에 따라 접두어 추가
const prefix = aggregation === "avg" ? "평균 " : aggregation === "sum" ? "총 " : "";
- label = prefix + key
- .replace(/_/g, " ")
- .replace(/([A-Z])/g, " $1")
- .trim();
+ label =
+ prefix +
+ key
+ .replace(/_/g, " ")
+ .replace(/([A-Z])/g, " $1")
+ .trim();
}
break;
}
@@ -383,41 +385,45 @@ export default function CustomStatsWidget({ element, refreshInterval = 60000 }:
if (!matchedConfig) {
// 컬럼명 번역 시도
const translatedName = columnNameTranslation[key.toLowerCase()];
-
+
if (translatedName) {
// 번역된 이름이 있으면 사용
label = translatedName;
} else {
// 컬럼명에 avg, average, mean이 포함되면 평균으로 간주
- if (key.toLowerCase().includes("avg") ||
- key.toLowerCase().includes("average") ||
- key.toLowerCase().includes("mean")) {
+ if (
+ key.toLowerCase().includes("avg") ||
+ key.toLowerCase().includes("average") ||
+ key.toLowerCase().includes("mean")
+ ) {
aggregation = "avg";
-
+
// 언더스코어로 분리된 각 단어 번역 시도
- const cleanKey = key.replace(/avg|average|mean/gi, "").replace(/_/g, " ").trim();
+ const cleanKey = key
+ .replace(/avg|average|mean/gi, "")
+ .replace(/_/g, " ")
+ .trim();
const words = cleanKey.split(/[_\s]+/);
- const translatedWords = words.map(word =>
- columnNameTranslation[word.toLowerCase()] || word
- );
+ const translatedWords = words.map((word) => columnNameTranslation[word.toLowerCase()] || word);
label = "평균 " + translatedWords.join(" ");
- }
+ }
// total, sum이 포함되면 합계로 간주
else if (key.toLowerCase().includes("total") || key.toLowerCase().includes("sum")) {
aggregation = "sum";
-
+
// 언더스코어로 분리된 각 단어 번역 시도
- const cleanKey = key.replace(/total|sum/gi, "").replace(/_/g, " ").trim();
+ const cleanKey = key
+ .replace(/total|sum/gi, "")
+ .replace(/_/g, " ")
+ .trim();
const words = cleanKey.split(/[_\s]+/);
- const translatedWords = words.map(word =>
- columnNameTranslation[word.toLowerCase()] || word
- );
+ const translatedWords = words.map((word) => columnNameTranslation[word.toLowerCase()] || word);
label = "총 " + translatedWords.join(" ");
}
// 기본값 - 각 단어별로 번역 시도
else {
const words = key.split(/[_\s]+/);
- const translatedWords = words.map(word => {
+ const translatedWords = words.map((word) => {
const translated = columnNameTranslation[word.toLowerCase()];
if (translated) {
return translated;
@@ -473,25 +479,25 @@ export default function CustomStatsWidget({ element, refreshInterval = 60000 }:
Object.keys(firstRow).forEach((key) => {
const lowerKey = key.toLowerCase();
const matchedKey = Object.keys(booleanMapping).find((k) => lowerKey.includes(k));
-
+
if (matchedKey) {
const label = booleanMapping[matchedKey];
-
+
// 이미 추가된 라벨이면 스킵
if (addedBooleanLabels.has(label)) {
return;
}
-
+
const validItems = data.filter((item: any) => item[key] !== null && item[key] !== undefined);
-
+
if (validItems.length > 0) {
const trueCount = validItems.filter((item: any) => {
const val = item[key];
return val === true || val === "true" || val === 1 || val === "1" || val === "Y";
}).length;
-
+
const rate = (trueCount / validItems.length) * 100;
-
+
statsItems.push({
label,
value: rate,
@@ -499,7 +505,7 @@ export default function CustomStatsWidget({ element, refreshInterval = 60000 }:
color: "purple",
icon: "✅",
});
-
+
addedBooleanLabels.add(label);
}
}
@@ -507,27 +513,27 @@ export default function CustomStatsWidget({ element, refreshInterval = 60000 }:
// console.log("📊 생성된 통계 항목:", statsItems.map(s => s.label));
setAllStats(statsItems);
-
+
// 초기화가 아직 안됐으면 localStorage에서 설정 불러오기
if (!isInitializedRef.current) {
const saved = localStorage.getItem(storageKey);
// console.log("💾 저장된 설정:", saved);
-
+
if (saved) {
try {
const savedLabels = JSON.parse(saved);
// console.log("✅ 저장된 라벨:", savedLabels);
-
+
const filtered = statsItems.filter((s) => savedLabels.includes(s.label));
// console.log("🔍 필터링된 통계:", filtered.map(s => s.label));
// console.log(`📊 일치율: ${filtered.length}/${savedLabels.length} (${Math.round(filtered.length / savedLabels.length * 100)}%)`);
-
+
// 50% 이상 일치하면 저장된 설정 사용
const matchRate = filtered.length / savedLabels.length;
if (matchRate >= 0.5 && filtered.length > 0) {
setStats(filtered);
// 실제 표시되는 라벨로 업데이트
- const actualLabels = filtered.map(s => s.label);
+ const actualLabels = filtered.map((s) => s.label);
setSelectedStats(actualLabels);
selectedStatsRef.current = actualLabels;
// localStorage도 업데이트하여 다음에는 정확히 일치하도록
@@ -562,11 +568,11 @@ export default function CustomStatsWidget({ element, refreshInterval = 60000 }:
// 이미 초기화됐으면 현재 선택된 통계 유지
const currentSelected = selectedStatsRef.current;
// console.log("🔄 현재 선택된 통계:", currentSelected);
-
+
if (currentSelected.length > 0) {
const filtered = statsItems.filter((s) => currentSelected.includes(s.label));
// console.log("🔍 필터링 결과:", filtered.map(s => s.label));
-
+
if (filtered.length > 0) {
setStats(filtered);
} else {
@@ -624,9 +630,7 @@ export default function CustomStatsWidget({ element, refreshInterval = 60000 }:
⚠️
{error}
- {!element?.dataSource?.query && (
- 톱니바퀴 아이콘을 클릭하여 쿼리를 설정하세요
- )}
+ {!element?.dataSource?.query && 쿼리를 설정하세요 }
|