히스토리 패널 UI 작업 완료
- T-008 태스크 완료: 히스토리 패널 UI 마크업 구현 (슬라이드 인) - 히스토리, UNDO, 전송하기 버튼을 세로로 균등 간격 배치 - Tailwind CSS v3/v4 버전 충돌 문제 해결 - v4 패키지 완전 제거 (@tailwindcss/postcss 등) - PostCSS 설정을 v3 방식으로 수정 - CSS 파일에서 수동 클래스 정의 제거 - Vite 캐시 완전 삭제로 설정 변경 반영 - 히스토리 패널 기능 개선 - 우측 슬라이드 인 애니메이션 - 파일 업로드 시에만 표시 - 상태별 아이콘과 시간순 로그 리스트 - 재적용 및 전체 삭제 기능 - 새로운 rule 파일 생성: tailwind-css-management.mdc
This commit is contained in:
@@ -15,10 +15,12 @@ import "@univerjs/presets/lib/styles/preset-sheets-core.css";
|
||||
import { cn } from "../../lib/utils";
|
||||
import LuckyExcel from "@zwight/luckyexcel";
|
||||
import PromptInput from "./PromptInput";
|
||||
import HistoryPanel from "../ui/historyPanel";
|
||||
import { useAppStore } from "../../stores/useAppStore";
|
||||
import { rangeToAddress } from "../../utils/cellUtils";
|
||||
import { CellSelectionHandler } from "../../utils/cellSelectionHandler";
|
||||
import { aiProcessor } from "../../utils/aiProcessor";
|
||||
import type { HistoryEntry } from "../../types/ai";
|
||||
|
||||
// 전역 고유 키 생성
|
||||
const GLOBAL_UNIVER_KEY = "__GLOBAL_UNIVER_INSTANCE__";
|
||||
@@ -337,6 +339,52 @@ const TestSheetViewer: React.FC = () => {
|
||||
// CellSelectionHandler 인스턴스 생성
|
||||
const cellSelectionHandler = useRef(new CellSelectionHandler());
|
||||
|
||||
// 히스토리 관련 상태 추가
|
||||
const [isHistoryOpen, setIsHistoryOpen] = useState(false);
|
||||
const [history, setHistory] = useState<HistoryEntry[]>([]);
|
||||
|
||||
// 히스토리 관련 핸들러
|
||||
const handleHistoryToggle = () => {
|
||||
console.log("🔄 히스토리 토글:", !isHistoryOpen);
|
||||
setIsHistoryOpen(!isHistoryOpen);
|
||||
};
|
||||
|
||||
const handleHistoryClear = () => {
|
||||
if (window.confirm("모든 히스토리를 삭제하시겠습니까?")) {
|
||||
setHistory([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleHistoryReapply = (entry: HistoryEntry) => {
|
||||
// 히스토리 항목 재적용 로직
|
||||
setPrompt(entry.prompt);
|
||||
console.log("🔄 히스토리 재적용:", entry);
|
||||
// TODO: 실제 액션 재실행 로직 구현
|
||||
};
|
||||
|
||||
// 새 히스토리 항목 추가 함수
|
||||
const addHistoryEntry = (
|
||||
prompt: string,
|
||||
range: string,
|
||||
sheetName: string,
|
||||
actions: any[],
|
||||
status: "success" | "error" | "pending",
|
||||
error?: string,
|
||||
) => {
|
||||
const newEntry: HistoryEntry = {
|
||||
id: `history-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
timestamp: new Date(),
|
||||
prompt,
|
||||
range,
|
||||
sheetName,
|
||||
actions,
|
||||
status,
|
||||
error,
|
||||
};
|
||||
|
||||
setHistory((prev) => [newEntry, ...prev]); // 최신 항목을 맨 위에
|
||||
};
|
||||
|
||||
// Univer 초기화 함수 (Presets 기반)
|
||||
const initializeUniver = useCallback(
|
||||
async (workbookData?: any) => {
|
||||
@@ -718,7 +766,23 @@ const TestSheetViewer: React.FC = () => {
|
||||
<div style={{ height: "1rem" }} />
|
||||
|
||||
{/* 프롬프트 입력창 - Univer 하단에 이어서 */}
|
||||
<PromptInput value={prompt} onChange={(e) => setPrompt(e.target.value)} />
|
||||
<PromptInput
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
onHistoryToggle={handleHistoryToggle}
|
||||
historyCount={history.length}
|
||||
/>
|
||||
|
||||
{/* 히스토리 패널 - 파일이 업로드된 후에만 표시 */}
|
||||
{!showUploadOverlay && (
|
||||
<HistoryPanel
|
||||
isOpen={isHistoryOpen}
|
||||
onClose={() => setIsHistoryOpen(false)}
|
||||
history={history}
|
||||
onReapply={handleHistoryReapply}
|
||||
onClear={handleHistoryClear}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 파일 업로드 오버레이 - 레이어 분리 */}
|
||||
{showUploadOverlay && (
|
||||
|
||||
@@ -8,6 +8,8 @@ interface PromptInputProps {
|
||||
onExecute?: () => void;
|
||||
disabled?: boolean;
|
||||
maxLength?: number;
|
||||
onHistoryToggle?: () => void;
|
||||
historyCount?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -24,6 +26,8 @@ const PromptInput: React.FC<PromptInputProps> = ({
|
||||
onExecute,
|
||||
disabled = true,
|
||||
maxLength = 500,
|
||||
onHistoryToggle,
|
||||
historyCount,
|
||||
}) => {
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const [showCellInsertFeedback, setShowCellInsertFeedback] = useState(false);
|
||||
@@ -182,18 +186,53 @@ const PromptInput: React.FC<PromptInputProps> = ({
|
||||
rows={5}
|
||||
/>
|
||||
<div style={{ width: "1rem" }} />
|
||||
<button
|
||||
className="ml-2 px-6 py-2 rounded-lg text-white font-semibold text-base shadow transition disabled:opacity-60 disabled:cursor-not-allowed"
|
||||
style={{
|
||||
background: isProcessing
|
||||
? "#6b7280"
|
||||
: "linear-gradient(90deg, #a18fff 0%, #6f6fff 100%)",
|
||||
}}
|
||||
onClick={handleExecute}
|
||||
disabled={isProcessing || !value.trim()}
|
||||
>
|
||||
{isProcessing ? "처리 중..." : "전송하기"}
|
||||
</button>
|
||||
|
||||
{/* 버튼들을 세로로 배치 */}
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* 히스토리 버튼 - 맨 위 */}
|
||||
{onHistoryToggle && (
|
||||
<button
|
||||
className="px-4 py-2 rounded-lg text-gray-600 border border-gray-300 hover:bg-gray-50 font-semibold text-base shadow transition disabled:opacity-60 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||
onClick={onHistoryToggle}
|
||||
disabled={isProcessing}
|
||||
aria-label="작업 히스토리 보기"
|
||||
>
|
||||
📝
|
||||
{historyCount !== undefined && historyCount > 0 && (
|
||||
<span className="text-xs bg-blue-500 text-white rounded-full px-2 py-0.5 min-w-[20px] h-5 flex items-center justify-center">
|
||||
{historyCount > 99 ? "99+" : historyCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* UNDO 버튼 - 중간 */}
|
||||
<button
|
||||
className="px-4 py-2 rounded-lg text-gray-600 border border-gray-300 hover:bg-gray-50 font-semibold text-base shadow transition disabled:opacity-60 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||
onClick={() => {
|
||||
// TODO: UNDO 기능 구현
|
||||
console.log("🔄 UNDO 버튼 클릭");
|
||||
}}
|
||||
disabled={isProcessing}
|
||||
aria-label="실행 취소"
|
||||
>
|
||||
↶
|
||||
</button>
|
||||
|
||||
{/* 전송하기 버튼 - 맨 아래 */}
|
||||
<button
|
||||
className="px-6 py-2 rounded-lg text-white font-semibold text-base shadow transition disabled:opacity-60 disabled:cursor-not-allowed"
|
||||
style={{
|
||||
background: isProcessing
|
||||
? "#6b7280"
|
||||
: "linear-gradient(90deg, #a18fff 0%, #6f6fff 100%)",
|
||||
}}
|
||||
onClick={handleExecute}
|
||||
disabled={isProcessing || !value.trim()}
|
||||
>
|
||||
{isProcessing ? "처리 중..." : "전송하기"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full max-w-3xl flex justify-between items-center mt-1 px-1">
|
||||
<span className="text-xs text-gray-500">
|
||||
|
||||
287
src/components/ui/historyPanel.tsx
Normal file
287
src/components/ui/historyPanel.tsx
Normal file
@@ -0,0 +1,287 @@
|
||||
import React from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "./card";
|
||||
import { Button } from "./button";
|
||||
import type { HistoryPanelProps, HistoryEntry } from "../../types/ai";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
/**
|
||||
* 히스토리 패널 컴포넌트
|
||||
* 우측에서 슬라이드 인하는 방식으로 작동
|
||||
*/
|
||||
const HistoryPanel: React.FC<HistoryPanelProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
history,
|
||||
onReapply,
|
||||
onClear,
|
||||
}) => {
|
||||
// 키보드 접근성: Escape 키로 패널 닫기
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape" && isOpen) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
// 포커스 트랩을 위해 패널에 포커스 설정
|
||||
const panel = document.getElementById("history-panel");
|
||||
if (panel) {
|
||||
panel.focus();
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
// 시간 포맷팅 함수
|
||||
const formatTime = (date: Date): string => {
|
||||
return new Intl.DateTimeFormat("ko-KR", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
}).format(date);
|
||||
};
|
||||
|
||||
// 날짜 포맷팅 함수
|
||||
const formatDate = (date: Date): string => {
|
||||
const today = new Date();
|
||||
const yesterday = new Date(today);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
|
||||
if (date.toDateString() === today.toDateString()) {
|
||||
return "오늘";
|
||||
} else if (date.toDateString() === yesterday.toDateString()) {
|
||||
return "어제";
|
||||
} else {
|
||||
return new Intl.DateTimeFormat("ko-KR", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
}).format(date);
|
||||
}
|
||||
};
|
||||
|
||||
// 상태별 아이콘 및 색상
|
||||
const getStatusIcon = (status: HistoryEntry["status"]) => {
|
||||
switch (status) {
|
||||
case "success":
|
||||
return <span className="text-green-500">✅</span>;
|
||||
case "error":
|
||||
return <span className="text-red-500">❌</span>;
|
||||
case "pending":
|
||||
return <span className="text-yellow-500 animate-pulse">⏳</span>;
|
||||
default:
|
||||
return <span className="text-gray-400">❓</span>;
|
||||
}
|
||||
};
|
||||
|
||||
// 액션 요약 생성
|
||||
const getActionSummary = (actions: HistoryEntry["actions"]): string => {
|
||||
if (actions.length === 0) return "액션 없음";
|
||||
|
||||
const actionTypes = actions.map((action) => {
|
||||
switch (action.type) {
|
||||
case "formula":
|
||||
return "수식";
|
||||
case "style":
|
||||
return "스타일";
|
||||
case "chart":
|
||||
return "차트";
|
||||
default:
|
||||
return "기타";
|
||||
}
|
||||
});
|
||||
|
||||
return `${actionTypes.join(", ")} (${actions.length}개)`;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 백드롭 오버레이 */}
|
||||
{isOpen && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-60 z-40 transition-opacity duration-300"
|
||||
onClick={onClose}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 히스토리 패널 */}
|
||||
<div
|
||||
id="history-panel"
|
||||
className={cn(
|
||||
"bg-white shadow-2xl z-50",
|
||||
"transform transition-transform duration-300 ease-in-out",
|
||||
"flex flex-col border-l border-gray-200",
|
||||
isOpen ? "translate-x-0" : "translate-x-full",
|
||||
)}
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: 65,
|
||||
right: 0,
|
||||
height: "100vh",
|
||||
width: "384px", // w-96 = 384px
|
||||
backgroundColor: "#ffffff",
|
||||
zIndex: 50,
|
||||
}}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="history-panel-title"
|
||||
aria-describedby="history-panel-description"
|
||||
tabIndex={-1}
|
||||
>
|
||||
{/* 헤더 */}
|
||||
<div className="flex-shrink-0 border-b border-gray-200 p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2
|
||||
id="history-panel-title"
|
||||
className="text-lg font-semibold text-gray-900"
|
||||
>
|
||||
📝 작업 히스토리
|
||||
</h2>
|
||||
<p
|
||||
id="history-panel-description"
|
||||
className="text-sm text-gray-500 mt-1"
|
||||
>
|
||||
AI 프롬프트 실행 기록 ({history.length}개)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 닫기 버튼 */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onClose}
|
||||
aria-label="히스토리 패널 닫기"
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
✕
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 전체 삭제 버튼 */}
|
||||
{history.length > 0 && onClear && (
|
||||
<div className="mt-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onClear}
|
||||
className="w-full text-red-600 border-red-200 hover:bg-red-50"
|
||||
aria-label="모든 히스토리 삭제"
|
||||
>
|
||||
🗑️ 전체 삭제
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 히스토리 목록 */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{history.length === 0 ? (
|
||||
// 빈 상태
|
||||
<div className="flex flex-col items-center justify-center h-full text-gray-500">
|
||||
<div className="text-4xl mb-4">📋</div>
|
||||
<p className="text-center px-4">
|
||||
아직 실행된 AI 프롬프트가 없습니다.
|
||||
<br />
|
||||
<span className="text-sm text-gray-400">
|
||||
프롬프트를 입력하고 실행해보세요!
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
// 히스토리 항목들
|
||||
<div className="p-4 space-y-3">
|
||||
{history.map((entry, index) => (
|
||||
<Card
|
||||
key={entry.id}
|
||||
className={cn(
|
||||
"transition-all duration-200 hover:shadow-md",
|
||||
entry.status === "error" && "border-red-200 bg-red-50",
|
||||
entry.status === "success" &&
|
||||
"border-green-200 bg-green-50",
|
||||
entry.status === "pending" &&
|
||||
"border-yellow-200 bg-yellow-50",
|
||||
)}
|
||||
>
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
{getStatusIcon(entry.status)}
|
||||
<div className="text-xs text-gray-500">
|
||||
{formatDate(entry.timestamp)}{" "}
|
||||
{formatTime(entry.timestamp)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
#{history.length - index}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="pt-0">
|
||||
{/* 프롬프트 */}
|
||||
<div className="mb-3">
|
||||
<p className="text-sm font-medium text-gray-900 mb-1">
|
||||
프롬프트:
|
||||
</p>
|
||||
<p className="text-sm text-gray-700 bg-gray-100 rounded-md p-2 break-words">
|
||||
{entry.prompt}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 범위 및 시트 정보 */}
|
||||
<div className="mb-3 text-xs text-gray-600">
|
||||
<span className="font-medium">범위:</span> {entry.range} |
|
||||
<span className="font-medium ml-1">시트:</span>{" "}
|
||||
{entry.sheetName}
|
||||
</div>
|
||||
|
||||
{/* 액션 요약 */}
|
||||
<div className="mb-3">
|
||||
<p className="text-xs text-gray-600">
|
||||
<span className="font-medium">실행된 액션:</span>{" "}
|
||||
{getActionSummary(entry.actions)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 에러 메시지 */}
|
||||
{entry.status === "error" && entry.error && (
|
||||
<div className="mb-3 p-2 bg-red-100 border border-red-200 rounded-md">
|
||||
<p className="text-xs text-red-700">
|
||||
<span className="font-medium">오류:</span>{" "}
|
||||
{entry.error}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 재적용 버튼 */}
|
||||
{entry.status === "success" && onReapply && (
|
||||
<div className="mt-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onReapply(entry)}
|
||||
className="w-full text-blue-600 border-blue-200 hover:bg-blue-50"
|
||||
aria-label={`프롬프트 재적용: ${entry.prompt.slice(0, 20)}...`}
|
||||
>
|
||||
🔄 다시 적용
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default HistoryPanel;
|
||||
148
src/index.css
148
src/index.css
@@ -11,109 +11,6 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* 필요한 색상 클래스들 추가 */
|
||||
.text-gray-500 { color: #6b7280; }
|
||||
.text-gray-600 { color: #4b5563; }
|
||||
.text-gray-900 { color: #111827; }
|
||||
.text-blue-600 { color: #2563eb; }
|
||||
.text-blue-700 { color: #1d4ed8; }
|
||||
.text-blue-800 { color: #1e40af; }
|
||||
|
||||
.bg-gray-50 { background-color: #f9fafb; }
|
||||
.bg-blue-50 { background-color: #eff6ff; }
|
||||
.bg-blue-100 { background-color: #dbeafe; }
|
||||
.bg-blue-200 { background-color: #bfdbfe; }
|
||||
|
||||
.border-gray-300 { border-color: #d1d5db; }
|
||||
.border-blue-200 { border-color: #bfdbfe; }
|
||||
.border-blue-400 { border-color: #60a5fa; }
|
||||
.border-blue-500 { border-color: #3b82f6; }
|
||||
|
||||
.hover\:border-blue-400:hover { border-color: #60a5fa; }
|
||||
.hover\:bg-blue-50:hover { background-color: #eff6ff; }
|
||||
|
||||
.focus\:ring-blue-500:focus {
|
||||
--tw-ring-color: #3b82f6;
|
||||
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
||||
}
|
||||
|
||||
/* 추가 유틸리티 클래스들 */
|
||||
.max-w-7xl { max-width: 80rem; }
|
||||
.max-w-2xl { max-width: 42rem; }
|
||||
.max-w-lg { max-width: 32rem; }
|
||||
.max-w-md { max-width: 28rem; }
|
||||
|
||||
.h-16 { height: 4rem; }
|
||||
.h-20 { height: 5rem; }
|
||||
.h-24 { height: 6rem; }
|
||||
.w-20 { width: 5rem; }
|
||||
.w-24 { width: 6rem; }
|
||||
|
||||
.h-6 { height: 1.5rem; }
|
||||
.w-6 { width: 1.5rem; }
|
||||
.h-10 { height: 2.5rem; }
|
||||
.w-10 { width: 2.5rem; }
|
||||
.h-12 { height: 3rem; }
|
||||
.w-12 { width: 3rem; }
|
||||
|
||||
.space-x-4 > :not([hidden]) ~ :not([hidden]) { margin-left: 1rem; }
|
||||
.space-x-2 > :not([hidden]) ~ :not([hidden]) { margin-left: 0.5rem; }
|
||||
.space-y-1 > :not([hidden]) ~ :not([hidden]) { margin-top: 0.25rem; }
|
||||
.space-y-2 > :not([hidden]) ~ :not([hidden]) { margin-top: 0.5rem; }
|
||||
.space-y-4 > :not([hidden]) ~ :not([hidden]) { margin-top: 1rem; }
|
||||
|
||||
.p-3 { padding: 0.75rem; }
|
||||
.p-4 { padding: 1rem; }
|
||||
.p-8 { padding: 2rem; }
|
||||
.p-12 { padding: 3rem; }
|
||||
.px-4 { padding-left: 1rem; padding-right: 1rem; }
|
||||
.py-8 { padding-top: 2rem; padding-bottom: 2rem; }
|
||||
|
||||
.mb-2 { margin-bottom: 0.5rem; }
|
||||
.mb-3 { margin-bottom: 0.75rem; }
|
||||
.mb-4 { margin-bottom: 1rem; }
|
||||
.mb-6 { margin-bottom: 1.5rem; }
|
||||
.mb-8 { margin-bottom: 2rem; }
|
||||
.mt-6 { margin-top: 1.5rem; }
|
||||
|
||||
.text-xs { font-size: 0.75rem; line-height: 1rem; }
|
||||
.text-sm { font-size: 0.875rem; line-height: 1.25rem; }
|
||||
.text-base { font-size: 1rem; line-height: 1.5rem; }
|
||||
.text-lg { font-size: 1.125rem; line-height: 1.75rem; }
|
||||
.text-xl { font-size: 1.25rem; line-height: 1.75rem; }
|
||||
.text-2xl { font-size: 1.5rem; line-height: 2rem; }
|
||||
.text-4xl { font-size: 2.25rem; line-height: 2.5rem; }
|
||||
.text-6xl { font-size: 3.75rem; line-height: 1; }
|
||||
|
||||
.font-medium { font-weight: 500; }
|
||||
.font-semibold { font-weight: 600; }
|
||||
.font-bold { font-weight: 700; }
|
||||
|
||||
.rounded-lg { border-radius: 0.5rem; }
|
||||
.rounded-md { border-radius: 0.375rem; }
|
||||
|
||||
.shadow-sm { box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); }
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.sm\:px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.md\:h-24 { height: 6rem; }
|
||||
.md\:w-24 { width: 6rem; }
|
||||
.md\:h-12 { height: 3rem; }
|
||||
.md\:w-12 { width: 3rem; }
|
||||
.md\:p-12 { padding: 3rem; }
|
||||
.md\:text-base { font-size: 1rem; line-height: 1.5rem; }
|
||||
.md\:text-lg { font-size: 1.125rem; line-height: 1.75rem; }
|
||||
.md\:text-2xl { font-size: 1.5rem; line-height: 2rem; }
|
||||
.md\:text-6xl { font-size: 3.75rem; line-height: 1; }
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.lg\:px-8 { padding-left: 2rem; padding-right: 2rem; }
|
||||
}
|
||||
|
||||
/* 전역 스타일 */
|
||||
html, body, #root {
|
||||
height: 100%;
|
||||
@@ -199,61 +96,53 @@ html, body, #root {
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2);
|
||||
opacity: 0.7;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
opacity: .5;
|
||||
}
|
||||
}
|
||||
|
||||
/* 에러 메시지 스타일 */
|
||||
.error-message {
|
||||
color: #dc2626;
|
||||
background-color: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
color: #dc2626;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.75rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
/* 성공 메시지 스타일 */
|
||||
.success-message {
|
||||
color: #059669;
|
||||
background-color: #f0fdf4;
|
||||
border: 1px solid #bbf7d0;
|
||||
color: #16a34a;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.75rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
/* 정보 메시지 스타일 */
|
||||
.info-message {
|
||||
color: #2563eb;
|
||||
background-color: #eff6ff;
|
||||
border: 1px solid #bfdbfe;
|
||||
color: #2563eb;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.75rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
/* 반응형 디자인 */
|
||||
/* 모바일 반응형 */
|
||||
@media (max-width: 768px) {
|
||||
.file-upload-area {
|
||||
padding: 15px;
|
||||
padding: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.univer-container {
|
||||
min-height: 400px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,6 +151,7 @@ html, body, #root {
|
||||
.file-upload-area {
|
||||
border-color: #374151;
|
||||
background-color: #1f2937;
|
||||
color: #f9fafb;
|
||||
}
|
||||
|
||||
.file-upload-area:hover {
|
||||
|
||||
@@ -48,3 +48,23 @@ export interface AIHistory {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// 히스토리 관련 타입 추가
|
||||
export interface HistoryEntry {
|
||||
id: string;
|
||||
timestamp: Date;
|
||||
prompt: string;
|
||||
range: string;
|
||||
sheetName: string;
|
||||
actions: AIAction[];
|
||||
status: "success" | "error" | "pending";
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface HistoryPanelProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
history: HistoryEntry[];
|
||||
onReapply?: (entry: HistoryEntry) => void;
|
||||
onClear?: () => void;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user