Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feat/dashboard

This commit is contained in:
dohyeons
2025-10-17 16:25:30 +09:00
27 changed files with 471 additions and 280 deletions

View File

@@ -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"

View File

@@ -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}

View File

@@ -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]">

View File

@@ -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"

View File

@@ -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"

View File

@@ -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>

View File

@@ -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', {

View File

@@ -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>
) : (

View File

@@ -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">

View File

@@ -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">

View File

@@ -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"