Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feat/dashboard
This commit is contained in:
@@ -256,7 +256,8 @@ export function DashboardViewer({
|
||||
|
||||
return (
|
||||
<DashboardProvider>
|
||||
<div className="flex h-full items-start justify-center overflow-auto bg-gray-100 p-8">
|
||||
{/* overflow-auto 제거 - 외부 페이지 스크롤 사용 */}
|
||||
<div className="flex h-full items-start justify-center bg-gray-100 p-8">
|
||||
{/* 고정 크기 캔버스 (편집 화면과 동일한 레이아웃) */}
|
||||
<div
|
||||
className="relative overflow-hidden rounded-lg"
|
||||
|
||||
@@ -161,7 +161,7 @@ export default function BookingAlertWidget({ element }: BookingAlertWidgetProps)
|
||||
<div className="border-b border-gray-200 bg-white px-4 py-3">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-lg font-bold text-gray-800">🔔 {element?.customTitle || "예약 요청 알림"}</h3>
|
||||
<h3 className="text-lg font-bold text-gray-800">{element?.customTitle || "예약 요청 알림"}</h3>
|
||||
{newCount > 0 && (
|
||||
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-red-500 text-xs font-bold text-white">
|
||||
{newCount}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* - 대시보드 위젯으로 사용 가능
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { DashboardElement } from '@/components/admin/dashboard/types';
|
||||
|
||||
@@ -117,11 +117,62 @@ export default function CalculatorWidget({ element, className = '' }: Calculator
|
||||
setDisplay(String(value / 100));
|
||||
};
|
||||
|
||||
// 키보드 입력 처리
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
const key = event.key;
|
||||
|
||||
// 숫자 키 (0-9)
|
||||
if (/^[0-9]$/.test(key)) {
|
||||
event.preventDefault();
|
||||
handleNumber(key);
|
||||
}
|
||||
// 연산자 키
|
||||
else if (key === '+' || key === '-' || key === '*' || key === '/') {
|
||||
event.preventDefault();
|
||||
handleOperation(key);
|
||||
}
|
||||
// 소수점
|
||||
else if (key === '.') {
|
||||
event.preventDefault();
|
||||
handleDecimal();
|
||||
}
|
||||
// Enter 또는 = (계산)
|
||||
else if (key === 'Enter' || key === '=') {
|
||||
event.preventDefault();
|
||||
handleEquals();
|
||||
}
|
||||
// Escape 또는 c (초기화)
|
||||
else if (key === 'Escape' || key.toLowerCase() === 'c') {
|
||||
event.preventDefault();
|
||||
handleClear();
|
||||
}
|
||||
// Backspace (지우기)
|
||||
else if (key === 'Backspace') {
|
||||
event.preventDefault();
|
||||
handleBackspace();
|
||||
}
|
||||
// % (퍼센트)
|
||||
else if (key === '%') {
|
||||
event.preventDefault();
|
||||
handlePercent();
|
||||
}
|
||||
};
|
||||
|
||||
// 이벤트 리스너 등록
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
// 클린업
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [display, previousValue, operation, waitingForOperand]);
|
||||
|
||||
return (
|
||||
<div className={`h-full w-full p-3 bg-gradient-to-br from-slate-50 to-gray-100 ${className}`}>
|
||||
<div className="h-full flex flex-col gap-2">
|
||||
{/* 제목 */}
|
||||
<h3 className="text-base font-semibold text-gray-900 text-center">🧮 {element?.customTitle || "계산기"}</h3>
|
||||
<h3 className="text-base font-semibold text-gray-900 text-center">{element?.customTitle || "계산기"}</h3>
|
||||
|
||||
{/* 디스플레이 */}
|
||||
<div className="bg-white border-2 border-gray-200 rounded-lg p-4 shadow-inner min-h-[80px]">
|
||||
|
||||
@@ -150,7 +150,7 @@ export default function CustomerIssuesWidget({ element }: CustomerIssuesWidgetPr
|
||||
<div className="flex h-full flex-col overflow-hidden bg-background p-4">
|
||||
{/* 헤더 */}
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-foreground">⚠️ 고객 클레임/이슈</h3>
|
||||
<h3 className="text-lg font-semibold text-foreground">고객 클레임/이슈</h3>
|
||||
<button
|
||||
onClick={loadData}
|
||||
className="rounded-full p-1 text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
||||
|
||||
@@ -130,7 +130,7 @@ export default function DeliveryTodayStatsWidget({ element }: DeliveryTodayStats
|
||||
<div className="flex h-full flex-col overflow-hidden bg-white p-4">
|
||||
{/* 헤더 */}
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-gray-800">📅 오늘 처리 현황</h3>
|
||||
<h3 className="text-lg font-semibold text-gray-800">오늘 처리 현황</h3>
|
||||
<button
|
||||
onClick={loadData}
|
||||
className="rounded-full p-1 text-gray-500 hover:bg-gray-100"
|
||||
|
||||
@@ -132,7 +132,7 @@ export default function DocumentWidget({ element }: DocumentWidgetProps) {
|
||||
{/* 헤더 */}
|
||||
<div className="border-b border-gray-200 bg-white px-4 py-3">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h3 className="text-lg font-bold text-gray-800">📂 {element?.customTitle || "문서 관리"}</h3>
|
||||
<h3 className="text-lg font-bold text-gray-800">{element?.customTitle || "문서 관리"}</h3>
|
||||
<button className="rounded-lg bg-primary px-3 py-1.5 text-sm text-white transition-colors hover:bg-primary/90">
|
||||
+ 업로드
|
||||
</button>
|
||||
|
||||
@@ -139,7 +139,7 @@ export default function ExchangeWidget({
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-base font-semibold text-gray-900 mb-1">💱 {element?.customTitle || "환율"}</h3>
|
||||
<h3 className="text-base font-semibold text-gray-900 mb-1">{element?.customTitle || "환율"}</h3>
|
||||
<p className="text-xs text-gray-500">
|
||||
{lastUpdated
|
||||
? `업데이트: ${lastUpdated.toLocaleTimeString('ko-KR', {
|
||||
|
||||
@@ -158,7 +158,7 @@ export default function MapSummaryWidget({ element }: MapSummaryWidgetProps) {
|
||||
{/* 헤더 */}
|
||||
<div className="mb-2 flex flex-shrink-0 items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-sm font-bold text-gray-900">📍 {displayTitle}</h3>
|
||||
<h3 className="text-sm font-bold text-gray-900">{displayTitle}</h3>
|
||||
{element?.dataSource?.query ? (
|
||||
<p className="text-xs text-gray-500">총 {markers.length.toLocaleString()}개 마커</p>
|
||||
) : (
|
||||
|
||||
@@ -64,10 +64,10 @@ export default function TodoWidget({ element }: TodoWidgetProps) {
|
||||
|
||||
// 외부 DB 조회 (dataSource가 설정된 경우)
|
||||
if (element?.dataSource?.query) {
|
||||
console.log("🔍 TodoWidget - 외부 DB 조회 시작");
|
||||
console.log("📝 Query:", element.dataSource.query);
|
||||
console.log("🔗 ConnectionId:", element.dataSource.externalConnectionId);
|
||||
console.log("🔗 ConnectionType:", element.dataSource.connectionType);
|
||||
// console.log("🔍 TodoWidget - 외부 DB 조회 시작");
|
||||
// console.log("📝 Query:", element.dataSource.query);
|
||||
// console.log("🔗 ConnectionId:", element.dataSource.externalConnectionId);
|
||||
// console.log("🔗 ConnectionType:", element.dataSource.connectionType);
|
||||
|
||||
// 현재 DB vs 외부 DB 분기
|
||||
const apiUrl = element.dataSource.connectionType === "external" && element.dataSource.externalConnectionId
|
||||
@@ -83,8 +83,8 @@ export default function TodoWidget({ element }: TodoWidgetProps) {
|
||||
query: element.dataSource.query,
|
||||
};
|
||||
|
||||
console.log("🌐 API URL:", apiUrl);
|
||||
console.log("📦 Request Body:", requestBody);
|
||||
// console.log("🌐 API URL:", apiUrl);
|
||||
// console.log("📦 Request Body:", requestBody);
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
method: "POST",
|
||||
@@ -95,29 +95,29 @@ export default function TodoWidget({ element }: TodoWidgetProps) {
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
console.log("📡 Response status:", response.status);
|
||||
// console.log("📡 Response status:", response.status);
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
console.log("✅ API 응답:", result);
|
||||
console.log("📦 result.data:", result.data);
|
||||
console.log("📦 result.data.rows:", result.data?.rows);
|
||||
// console.log("✅ API 응답:", result);
|
||||
// console.log("📦 result.data:", result.data);
|
||||
// console.log("📦 result.data.rows:", result.data?.rows);
|
||||
|
||||
// API 응답 형식에 따라 데이터 추출
|
||||
const rows = result.data?.rows || result.data || [];
|
||||
console.log("📊 추출된 rows:", rows);
|
||||
// console.log("📊 추출된 rows:", rows);
|
||||
|
||||
const externalTodos = mapExternalDataToTodos(rows);
|
||||
console.log("📋 변환된 Todos:", externalTodos);
|
||||
console.log("📋 변환된 Todos 개수:", externalTodos.length);
|
||||
// console.log("📋 변환된 Todos:", externalTodos);
|
||||
// console.log("📋 변환된 Todos 개수:", externalTodos.length);
|
||||
|
||||
setTodos(externalTodos);
|
||||
setStats(calculateStatsFromTodos(externalTodos));
|
||||
|
||||
console.log("✅ setTodos, setStats 호출 완료!");
|
||||
// console.log("✅ setTodos, setStats 호출 완료!");
|
||||
} else {
|
||||
const errorText = await response.text();
|
||||
console.error("❌ API 오류:", errorText);
|
||||
// console.error("❌ API 오류:", errorText);
|
||||
}
|
||||
}
|
||||
// 내장 API 조회 (기본)
|
||||
@@ -323,67 +323,71 @@ export default function TodoWidget({ element }: TodoWidgetProps) {
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col bg-gradient-to-br from-slate-50 to-blue-50">
|
||||
{/* 헤더 */}
|
||||
<div className="border-b border-gray-200 bg-white px-4 py-3">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-gray-800">✅ {element?.customTitle || "To-Do / 긴급 지시"}</h3>
|
||||
{selectedDate && (
|
||||
<div className="mt-1 flex items-center gap-1 text-xs text-green-600">
|
||||
<CalendarIcon className="h-3 w-3" />
|
||||
<span className="font-semibold">{formatSelectedDate()} 할일</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowAddForm(!showAddForm)}
|
||||
className="flex items-center gap-1 rounded-lg bg-primary px-3 py-1.5 text-sm text-white transition-colors hover:bg-primary/90"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
추가
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 통계 */}
|
||||
{stats && (
|
||||
<div className="grid grid-cols-4 gap-2 text-xs">
|
||||
<div className="rounded bg-blue-50 px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-blue-700">{stats.pending}</div>
|
||||
<div className="text-blue-600">대기</div>
|
||||
</div>
|
||||
<div className="rounded bg-amber-50 px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-amber-700">{stats.inProgress}</div>
|
||||
<div className="text-amber-600">진행중</div>
|
||||
</div>
|
||||
<div className="rounded bg-red-50 px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-red-700">{stats.urgent}</div>
|
||||
<div className="text-red-600">긴급</div>
|
||||
</div>
|
||||
<div className="rounded bg-rose-50 px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-rose-700">{stats.overdue}</div>
|
||||
<div className="text-rose-600">지연</div>
|
||||
</div>
|
||||
{/* 제목 - 항상 표시 */}
|
||||
<div className="border-b border-gray-200 bg-white px-4 py-2">
|
||||
<h3 className="text-lg font-bold text-gray-800">{element?.customTitle || "To-Do / 긴급 지시"}</h3>
|
||||
{selectedDate && (
|
||||
<div className="mt-1 flex items-center gap-1 text-xs text-green-600">
|
||||
<CalendarIcon className="h-3 w-3" />
|
||||
<span className="font-semibold">{formatSelectedDate()} 할일</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 필터 */}
|
||||
<div className="mt-3 flex gap-2">
|
||||
{(["all", "pending", "in_progress", "completed"] as const).map((f) => (
|
||||
<button
|
||||
key={f}
|
||||
onClick={() => setFilter(f)}
|
||||
className={`rounded px-3 py-1 text-xs font-medium transition-colors ${
|
||||
filter === f
|
||||
? "bg-primary text-white"
|
||||
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
||||
}`}
|
||||
>
|
||||
{f === "all" ? "전체" : f === "pending" ? "대기" : f === "in_progress" ? "진행중" : "완료"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 헤더 (추가 버튼, 통계, 필터) - showHeader가 false일 때만 숨김 */}
|
||||
{element?.showHeader !== false && (
|
||||
<div className="border-b border-gray-200 bg-white px-4 py-3">
|
||||
<div className="mb-3 flex items-center justify-end">
|
||||
<button
|
||||
onClick={() => setShowAddForm(!showAddForm)}
|
||||
className="flex items-center gap-1 rounded-lg bg-primary px-3 py-1.5 text-sm text-white transition-colors hover:bg-primary/90"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
추가
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 통계 */}
|
||||
{stats && (
|
||||
<div className="grid grid-cols-4 gap-2 text-xs mb-3">
|
||||
<div className="rounded bg-blue-50 px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-blue-700">{stats.pending}</div>
|
||||
<div className="text-blue-600">대기</div>
|
||||
</div>
|
||||
<div className="rounded bg-amber-50 px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-amber-700">{stats.inProgress}</div>
|
||||
<div className="text-amber-600">진행중</div>
|
||||
</div>
|
||||
<div className="rounded bg-red-50 px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-red-700">{stats.urgent}</div>
|
||||
<div className="text-red-600">긴급</div>
|
||||
</div>
|
||||
<div className="rounded bg-rose-50 px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-rose-700">{stats.overdue}</div>
|
||||
<div className="text-rose-600">지연</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 필터 */}
|
||||
<div className="flex gap-2">
|
||||
{(["all", "pending", "in_progress", "completed"] as const).map((f) => (
|
||||
<button
|
||||
key={f}
|
||||
onClick={() => setFilter(f)}
|
||||
className={`rounded px-3 py-1 text-xs font-medium transition-colors ${
|
||||
filter === f
|
||||
? "bg-primary text-white"
|
||||
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
||||
}`}
|
||||
>
|
||||
{f === "all" ? "전체" : f === "pending" ? "대기" : f === "in_progress" ? "진행중" : "완료"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 추가 폼 */}
|
||||
{showAddForm && (
|
||||
<div className="border-b border-gray-200 bg-white p-4">
|
||||
|
||||
@@ -97,7 +97,7 @@ export default function VehicleListWidget({ element, refreshInterval = 30000 }:
|
||||
{/* 헤더 */}
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-gray-900">📋 차량 목록</h3>
|
||||
<h3 className="text-lg font-bold text-gray-900">차량 목록</h3>
|
||||
<p className="text-xs text-gray-500">마지막 업데이트: {lastUpdate.toLocaleTimeString("ko-KR")}</p>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={loadVehicles} disabled={isLoading} className="h-8 w-8 p-0">
|
||||
|
||||
@@ -280,9 +280,12 @@ export default function WeatherWidget({
|
||||
if (loading && !weather) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-50 rounded-lg border p-6">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<RefreshCw className="h-8 w-8 animate-spin text-blue-500" />
|
||||
<p className="text-sm text-gray-600">날씨 정보 불러오는 중...</p>
|
||||
<div className="text-center">
|
||||
<p className="text-sm font-semibold text-gray-800 mb-1">실제 기상청 API 연결 중...</p>
|
||||
<p className="text-xs text-gray-500">실시간 관측 데이터를 가져오고 있습니다</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -290,10 +293,27 @@ export default function WeatherWidget({
|
||||
|
||||
// 에러 상태
|
||||
if (error || !weather) {
|
||||
const isTestMode = error?.includes('API 키가 설정되지 않았습니다');
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center bg-gradient-to-br from-red-50 to-orange-50 rounded-lg border p-6">
|
||||
<div className={`flex h-full flex-col items-center justify-center rounded-lg border p-6 ${
|
||||
isTestMode
|
||||
? 'bg-gradient-to-br from-yellow-50 to-orange-50'
|
||||
: 'bg-gradient-to-br from-red-50 to-orange-50'
|
||||
}`}>
|
||||
<Cloud className="h-12 w-12 text-gray-400 mb-2" />
|
||||
<p className="text-sm text-gray-600 text-center mb-3">{error || '날씨 정보를 불러올 수 없습니다.'}</p>
|
||||
<div className="text-center mb-3">
|
||||
<p className="text-sm font-semibold text-gray-800 mb-1">
|
||||
{isTestMode ? '⚠️ 테스트 모드' : '❌ 연결 실패'}
|
||||
</p>
|
||||
<p className="text-xs text-gray-600">
|
||||
{error || '날씨 정보를 불러올 수 없습니다.'}
|
||||
</p>
|
||||
{isTestMode && (
|
||||
<p className="text-xs text-yellow-700 mt-2">
|
||||
임시 데이터가 표시됩니다
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
|
||||
Reference in New Issue
Block a user