달력과 투두리스트 합침, 배경색변경가능, 위젯끼리 밀어내는 기능과 세밀한 그리드 추가, 범용위젯 복구

This commit is contained in:
leeheejin
2025-10-17 09:49:02 +09:00
parent 7097775343
commit fa08a26079
13 changed files with 992 additions and 113 deletions

View File

@@ -8,6 +8,7 @@ import { generateCalendarDays, getMonthName, navigateMonth } from "./calendarUti
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Button } from "@/components/ui/button";
import { Settings, ChevronLeft, ChevronRight, Calendar } from "lucide-react";
import { useDashboard } from "@/contexts/DashboardContext";
interface CalendarWidgetProps {
element: DashboardElement;
@@ -21,11 +22,19 @@ interface CalendarWidgetProps {
* - 내장 설정 UI
*/
export function CalendarWidget({ element, onConfigUpdate }: CalendarWidgetProps) {
// Context에서 선택된 날짜 관리
const { selectedDate, setSelectedDate } = useDashboard();
// 현재 표시 중인 년/월
const today = new Date();
const [currentYear, setCurrentYear] = useState(today.getFullYear());
const [currentMonth, setCurrentMonth] = useState(today.getMonth());
const [settingsOpen, setSettingsOpen] = useState(false);
// 날짜 클릭 핸들러
const handleDateClick = (date: Date) => {
setSelectedDate(date);
};
// 기본 설정값
const config = element.calendarConfig || {
@@ -98,7 +107,15 @@ export function CalendarWidget({ element, onConfigUpdate }: CalendarWidgetProps)
{/* 달력 콘텐츠 */}
<div className="flex-1 overflow-hidden">
{config.view === "month" && <MonthView days={calendarDays} config={config} isCompact={isCompact} />}
{config.view === "month" && (
<MonthView
days={calendarDays}
config={config}
isCompact={isCompact}
selectedDate={selectedDate}
onDateClick={handleDateClick}
/>
)}
{/* 추후 WeekView, DayView 추가 가능 */}
</div>

View File

@@ -7,12 +7,14 @@ interface MonthViewProps {
days: CalendarDay[];
config: CalendarConfig;
isCompact?: boolean; // 작은 크기 (2x2, 3x3)
selectedDate?: Date | null; // 선택된 날짜
onDateClick?: (date: Date) => void; // 날짜 클릭 핸들러
}
/**
* 월간 달력 뷰 컴포넌트
*/
export function MonthView({ days, config, isCompact = false }: MonthViewProps) {
export function MonthView({ days, config, isCompact = false, selectedDate, onDateClick }: MonthViewProps) {
const weekDayNames = getWeekDayNames(config.startWeekOn);
// 테마별 스타일
@@ -43,10 +45,27 @@ export function MonthView({ days, config, isCompact = false }: MonthViewProps) {
const themeStyles = getThemeStyles();
// 날짜가 선택된 날짜인지 확인
const isSelected = (day: CalendarDay) => {
if (!selectedDate || !day.isCurrentMonth) return false;
return (
selectedDate.getFullYear() === day.date.getFullYear() &&
selectedDate.getMonth() === day.date.getMonth() &&
selectedDate.getDate() === day.date.getDate()
);
};
// 날짜 클릭 핸들러
const handleDayClick = (day: CalendarDay) => {
if (!day.isCurrentMonth || !onDateClick) return;
onDateClick(day.date);
};
// 날짜 셀 스타일 클래스
const getDayCellClass = (day: CalendarDay) => {
const baseClass = "flex aspect-square items-center justify-center rounded-lg transition-colors";
const sizeClass = isCompact ? "text-xs" : "text-sm";
const cursorClass = day.isCurrentMonth ? "cursor-pointer" : "cursor-default";
let colorClass = "text-gray-700";
@@ -54,6 +73,10 @@ export function MonthView({ days, config, isCompact = false }: MonthViewProps) {
if (!day.isCurrentMonth) {
colorClass = "text-gray-300";
}
// 선택된 날짜
else if (isSelected(day)) {
colorClass = "text-white font-bold";
}
// 오늘
else if (config.highlightToday && day.isToday) {
colorClass = "text-white font-bold";
@@ -67,9 +90,16 @@ export function MonthView({ days, config, isCompact = false }: MonthViewProps) {
colorClass = "text-red-600";
}
const bgClass = config.highlightToday && day.isToday ? "" : "hover:bg-gray-100";
let bgClass = "";
if (isSelected(day)) {
bgClass = ""; // 선택된 날짜는 배경색이 style로 적용됨
} else if (config.highlightToday && day.isToday) {
bgClass = "";
} else {
bgClass = "hover:bg-gray-100";
}
return `${baseClass} ${sizeClass} ${colorClass} ${bgClass}`;
return `${baseClass} ${sizeClass} ${colorClass} ${bgClass} ${cursorClass}`;
};
return (
@@ -97,9 +127,13 @@ export function MonthView({ days, config, isCompact = false }: MonthViewProps) {
<div
key={index}
className={getDayCellClass(day)}
onClick={() => handleDayClick(day)}
style={{
backgroundColor:
config.highlightToday && day.isToday ? themeStyles.todayBg : undefined,
backgroundColor: isSelected(day)
? "#10b981" // 선택된 날짜는 초록색
: config.highlightToday && day.isToday
? themeStyles.todayBg
: undefined,
color:
config.showHolidays && day.isHoliday && day.isCurrentMonth
? themeStyles.holidayText

View File

@@ -0,0 +1,336 @@
"use client";
import React, { useState, useCallback, useEffect } from "react";
import { DashboardElement, ChartDataSource, QueryResult } from "../types";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { ChevronLeft, ChevronRight, Save, X } from "lucide-react";
import { DataSourceSelector } from "../data-sources/DataSourceSelector";
import { DatabaseConfig } from "../data-sources/DatabaseConfig";
import { ApiConfig } from "../data-sources/ApiConfig";
import { QueryEditor } from "../QueryEditor";
interface TodoWidgetConfigModalProps {
isOpen: boolean;
element: DashboardElement;
onClose: () => void;
onSave: (updates: Partial<DashboardElement>) => void;
}
/**
* To-Do 위젯 설정 모달
* - 2단계 설정: 데이터 소스 → 쿼리 입력/테스트
*/
export function TodoWidgetConfigModal({ isOpen, element, onClose, onSave }: TodoWidgetConfigModalProps) {
const [currentStep, setCurrentStep] = useState<1 | 2>(1);
const [title, setTitle] = useState(element.title || "✅ To-Do / 긴급 지시");
const [dataSource, setDataSource] = useState<ChartDataSource>(
element.dataSource || { type: "database", connectionType: "current", refreshInterval: 0 },
);
const [queryResult, setQueryResult] = useState<QueryResult | null>(null);
// 모달 열릴 때 element에서 설정 로드
useEffect(() => {
if (isOpen) {
setTitle(element.title || "✅ To-Do / 긴급 지시");
if (element.dataSource) {
setDataSource(element.dataSource);
}
setCurrentStep(1);
}
}, [isOpen, element.id]);
// 데이터 소스 타입 변경
const handleDataSourceTypeChange = useCallback((type: "database" | "api") => {
if (type === "database") {
setDataSource((prev) => ({
...prev,
type: "database",
connectionType: "current",
}));
} else {
setDataSource((prev) => ({
...prev,
type: "api",
method: "GET",
}));
}
setQueryResult(null);
}, []);
// 데이터 소스 업데이트
const handleDataSourceUpdate = useCallback((updates: Partial<ChartDataSource>) => {
setDataSource((prev) => ({ ...prev, ...updates }));
}, []);
// 쿼리 실행 결과 처리
const handleQueryTest = useCallback(
(result: QueryResult) => {
console.log("🎯 TodoWidget - handleQueryTest 호출됨!");
console.log("📊 쿼리 결과:", result);
console.log("📝 rows 개수:", result.rows?.length);
console.log("❌ error:", result.error);
setQueryResult(result);
console.log("✅ setQueryResult 호출 완료!");
// 강제 리렌더링 확인
setTimeout(() => {
console.log("🔄 1초 후 queryResult 상태:", result);
}, 1000);
},
[],
);
// 저장
const handleSave = useCallback(() => {
if (!dataSource.query || !queryResult || queryResult.error) {
alert("쿼리를 입력하고 테스트를 먼저 실행해주세요.");
return;
}
if (!queryResult.rows || queryResult.rows.length === 0) {
alert("쿼리 결과가 없습니다. 데이터가 반환되는 쿼리를 입력해주세요.");
return;
}
onSave({
title,
dataSource,
});
onClose();
}, [title, dataSource, queryResult, onSave, onClose]);
// 다음 단계로
const handleNext = useCallback(() => {
if (currentStep === 1) {
if (dataSource.type === "database") {
if (!dataSource.connectionId && dataSource.connectionType === "external") {
alert("외부 데이터베이스를 선택해주세요.");
return;
}
} else if (dataSource.type === "api") {
if (!dataSource.url) {
alert("API URL을 입력해주세요.");
return;
}
}
setCurrentStep(2);
}
}, [currentStep, dataSource]);
// 이전 단계로
const handlePrev = useCallback(() => {
if (currentStep === 2) {
setCurrentStep(1);
}
}, [currentStep]);
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/50">
<div className="relative flex h-[90vh] w-[90vw] max-w-6xl flex-col rounded-lg bg-white shadow-xl">
{/* 헤더 */}
<div className="flex items-center justify-between border-b border-gray-200 px-6 py-4">
<div>
<h2 className="text-xl font-bold text-gray-800">To-Do </h2>
<p className="mt-1 text-sm text-gray-500">
To-Do
</p>
</div>
<button
onClick={onClose}
className="rounded-lg p-2 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700"
>
<X className="h-5 w-5" />
</button>
</div>
{/* 진행 상태 */}
<div className="border-b border-gray-200 bg-gray-50 px-6 py-3">
<div className="flex items-center gap-4">
<div className={`flex items-center gap-2 ${currentStep === 1 ? "text-primary" : "text-gray-400"}`}>
<div
className={`flex h-8 w-8 items-center justify-center rounded-full font-semibold ${
currentStep === 1 ? "bg-primary text-white" : "bg-gray-200"
}`}
>
1
</div>
<span className="font-medium"> </span>
</div>
<ChevronRight className="h-4 w-4 text-gray-400" />
<div className={`flex items-center gap-2 ${currentStep === 2 ? "text-primary" : "text-gray-400"}`}>
<div
className={`flex h-8 w-8 items-center justify-center rounded-full font-semibold ${
currentStep === 2 ? "bg-primary text-white" : "bg-gray-200"
}`}
>
2
</div>
<span className="font-medium"> </span>
</div>
</div>
</div>
{/* 본문 */}
<div className="flex-1 overflow-y-auto p-6">
{/* Step 1: 데이터 소스 선택 */}
{currentStep === 1 && (
<div className="space-y-6">
<div>
<Label className="text-base font-semibold"></Label>
<Input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="예: ✅ 오늘의 할 일"
className="mt-2"
/>
</div>
<div>
<Label className="text-base font-semibold"> </Label>
<DataSourceSelector
dataSource={dataSource}
onTypeChange={handleDataSourceTypeChange}
/>
</div>
{dataSource.type === "database" && (
<DatabaseConfig dataSource={dataSource} onUpdate={handleDataSourceUpdate} />
)}
{dataSource.type === "api" && <ApiConfig dataSource={dataSource} onUpdate={handleDataSourceUpdate} />}
</div>
)}
{/* Step 2: 쿼리 입력 및 테스트 */}
{currentStep === 2 && (
<div className="space-y-6">
<div>
<div className="mb-4 rounded-lg bg-blue-50 p-4">
<h3 className="mb-2 font-semibold text-blue-900">💡 </h3>
<p className="mb-2 text-sm text-blue-700">
To-Do :
</p>
<ul className="space-y-1 text-sm text-blue-600">
<li>
<code className="rounded bg-blue-100 px-1 py-0.5">id</code> - ID ( )
</li>
<li>
<code className="rounded bg-blue-100 px-1 py-0.5">title</code>,{" "}
<code className="rounded bg-blue-100 px-1 py-0.5">task</code>,{" "}
<code className="rounded bg-blue-100 px-1 py-0.5">name</code> - ()
</li>
<li>
<code className="rounded bg-blue-100 px-1 py-0.5">description</code>,{" "}
<code className="rounded bg-blue-100 px-1 py-0.5">desc</code>,{" "}
<code className="rounded bg-blue-100 px-1 py-0.5">content</code> -
</li>
<li>
<code className="rounded bg-blue-100 px-1 py-0.5">priority</code> - (urgent, high,
normal, low)
</li>
<li>
<code className="rounded bg-blue-100 px-1 py-0.5">status</code> - (pending, in_progress,
completed)
</li>
<li>
<code className="rounded bg-blue-100 px-1 py-0.5">assigned_to</code>,{" "}
<code className="rounded bg-blue-100 px-1 py-0.5">assignedTo</code>,{" "}
<code className="rounded bg-blue-100 px-1 py-0.5">user</code> -
</li>
<li>
<code className="rounded bg-blue-100 px-1 py-0.5">due_date</code>,{" "}
<code className="rounded bg-blue-100 px-1 py-0.5">dueDate</code>,{" "}
<code className="rounded bg-blue-100 px-1 py-0.5">deadline</code> -
</li>
<li>
<code className="rounded bg-blue-100 px-1 py-0.5">is_urgent</code>,{" "}
<code className="rounded bg-blue-100 px-1 py-0.5">isUrgent</code>,{" "}
<code className="rounded bg-blue-100 px-1 py-0.5">urgent</code> -
</li>
</ul>
</div>
<QueryEditor
dataSource={dataSource}
onDataSourceChange={handleDataSourceUpdate}
onQueryTest={handleQueryTest}
/>
</div>
{/* 디버그: 항상 표시되는 테스트 메시지 */}
<div className="mt-4 rounded-lg bg-yellow-50 border-2 border-yellow-500 p-4">
<p className="text-sm font-bold text-yellow-900">
🔍 디버그: queryResult = {queryResult ? "있음" : "없음"}
</p>
{queryResult && (
<p className="text-xs text-yellow-700 mt-1">
rows: {queryResult.rows?.length}, error: {queryResult.error || "없음"}
</p>
)}
</div>
{queryResult && !queryResult.error && queryResult.rows && queryResult.rows.length > 0 && (
<div className="mt-4 rounded-lg bg-green-50 border-2 border-green-500 p-4">
<h3 className="mb-2 font-semibold text-green-900"> !</h3>
<p className="text-sm text-green-700">
<strong>{queryResult.rows.length}</strong> To-Do .
</p>
<div className="mt-3 rounded bg-white p-3">
<p className="mb-2 text-xs font-semibold text-gray-600"> :</p>
<pre className="overflow-x-auto text-xs text-gray-700">
{JSON.stringify(queryResult.rows[0], null, 2)}
</pre>
</div>
</div>
)}
</div>
)}
</div>
{/* 하단 버튼 */}
<div className="flex items-center justify-between border-t border-gray-200 px-6 py-4">
<div>
{currentStep > 1 && (
<Button onClick={handlePrev} variant="outline">
<ChevronLeft className="mr-1 h-4 w-4" />
</Button>
)}
</div>
<div className="flex gap-2">
<Button onClick={onClose} variant="outline">
</Button>
{currentStep < 2 ? (
<Button onClick={handleNext}>
<ChevronRight className="ml-1 h-4 w-4" />
</Button>
) : (
<Button
onClick={handleSave}
disabled={(() => {
const isDisabled = !queryResult || queryResult.error || !queryResult.rows || queryResult.rows.length === 0;
console.log("💾 저장 버튼 disabled:", isDisabled);
console.log("💾 queryResult:", queryResult);
return isDisabled;
})()}
>
<Save className="mr-1 h-4 w-4" />
</Button>
)}
</div>
</div>
</div>
</div>
);
}