히스토리 패널 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:
sheetEasy AI Team
2025-06-26 18:25:25 +09:00
parent 2d8e4524b7
commit e5ee01553a
9 changed files with 489 additions and 514 deletions

View File

@@ -0,0 +1,5 @@
---
description:
globs:
alwaysApply: false
---

411
package-lock.json generated
View File

@@ -9,7 +9,6 @@
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"@tailwindcss/postcss": "^4.1.10",
"@univerjs/core": "^0.8.2",
"@univerjs/design": "^0.8.2",
"@univerjs/docs": "^0.8.2",
@@ -79,6 +78,7 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -91,6 +91,7 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
@@ -1185,18 +1186,6 @@
"node": ">=12"
}
},
"node_modules/@isaacs/fs-minipass": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
"license": "ISC",
"dependencies": {
"minipass": "^7.0.4"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@jest/diff-sequences": {
"version": "30.0.1",
"resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz",
@@ -1467,6 +1456,7 @@
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/set-array": "^1.2.1",
@@ -1481,6 +1471,7 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@@ -1490,6 +1481,7 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@@ -1499,12 +1491,14 @@
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
"devOptional": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@@ -2758,279 +2752,6 @@
"@sinonjs/commons": "^3.0.0"
}
},
"node_modules/@tailwindcss/node": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.10.tgz",
"integrity": "sha512-2ACf1znY5fpRBwRhMgj9ZXvb2XZW8qs+oTfotJ2C5xR0/WNL7UHZ7zXl6s+rUqedL1mNi+0O+WQr5awGowS3PQ==",
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.3.0",
"enhanced-resolve": "^5.18.1",
"jiti": "^2.4.2",
"lightningcss": "1.30.1",
"magic-string": "^0.30.17",
"source-map-js": "^1.2.1",
"tailwindcss": "4.1.10"
}
},
"node_modules/@tailwindcss/node/node_modules/tailwindcss": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.10.tgz",
"integrity": "sha512-P3nr6WkvKV/ONsTzj6Gb57sWPMX29EPNPopo7+FcpkQaNsrNpZ1pv8QmrYI2RqEKD7mlGqLnGovlcYnBK0IqUA==",
"license": "MIT"
},
"node_modules/@tailwindcss/oxide": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.10.tgz",
"integrity": "sha512-v0C43s7Pjw+B9w21htrQwuFObSkio2aV/qPx/mhrRldbqxbWJK6KizM+q7BF1/1CmuLqZqX3CeYF7s7P9fbA8Q==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"detect-libc": "^2.0.4",
"tar": "^7.4.3"
},
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
"@tailwindcss/oxide-android-arm64": "4.1.10",
"@tailwindcss/oxide-darwin-arm64": "4.1.10",
"@tailwindcss/oxide-darwin-x64": "4.1.10",
"@tailwindcss/oxide-freebsd-x64": "4.1.10",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.10",
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.10",
"@tailwindcss/oxide-linux-arm64-musl": "4.1.10",
"@tailwindcss/oxide-linux-x64-gnu": "4.1.10",
"@tailwindcss/oxide-linux-x64-musl": "4.1.10",
"@tailwindcss/oxide-wasm32-wasi": "4.1.10",
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.10",
"@tailwindcss/oxide-win32-x64-msvc": "4.1.10"
}
},
"node_modules/@tailwindcss/oxide-android-arm64": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.10.tgz",
"integrity": "sha512-VGLazCoRQ7rtsCzThaI1UyDu/XRYVyH4/EWiaSX6tFglE+xZB5cvtC5Omt0OQ+FfiIVP98su16jDVHDEIuH4iQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.10.tgz",
"integrity": "sha512-ZIFqvR1irX2yNjWJzKCqTCcHZbgkSkSkZKbRM3BPzhDL/18idA8uWCoopYA2CSDdSGFlDAxYdU2yBHwAwx8euQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-darwin-x64": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.10.tgz",
"integrity": "sha512-eCA4zbIhWUFDXoamNztmS0MjXHSEJYlvATzWnRiTqJkcUteSjO94PoRHJy1Xbwp9bptjeIxxBHh+zBWFhttbrQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-freebsd-x64": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.10.tgz",
"integrity": "sha512-8/392Xu12R0cc93DpiJvNpJ4wYVSiciUlkiOHOSOQNH3adq9Gi/dtySK7dVQjXIOzlpSHjeCL89RUUI8/GTI6g==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.10.tgz",
"integrity": "sha512-t9rhmLT6EqeuPT+MXhWhlRYIMSfh5LZ6kBrC4FS6/+M1yXwfCtp24UumgCWOAJVyjQwG+lYva6wWZxrfvB+NhQ==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.10.tgz",
"integrity": "sha512-3oWrlNlxLRxXejQ8zImzrVLuZ/9Z2SeKoLhtCu0hpo38hTO2iL86eFOu4sVR8cZc6n3z7eRXXqtHJECa6mFOvA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.10.tgz",
"integrity": "sha512-saScU0cmWvg/Ez4gUmQWr9pvY9Kssxt+Xenfx1LG7LmqjcrvBnw4r9VjkFcqmbBb7GCBwYNcZi9X3/oMda9sqQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.10.tgz",
"integrity": "sha512-/G3ao/ybV9YEEgAXeEg28dyH6gs1QG8tvdN9c2MNZdUXYBaIY/Gx0N6RlJzfLy/7Nkdok4kaxKPHKJUlAaoTdA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.10.tgz",
"integrity": "sha512-LNr7X8fTiKGRtQGOerSayc2pWJp/9ptRYAa4G+U+cjw9kJZvkopav1AQc5HHD+U364f71tZv6XamaHKgrIoVzA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.10.tgz",
"integrity": "sha512-d6ekQpopFQJAcIK2i7ZzWOYGZ+A6NzzvQ3ozBvWFdeyqfOZdYHU66g5yr+/HC4ipP1ZgWsqa80+ISNILk+ae/Q==",
"bundleDependencies": [
"@napi-rs/wasm-runtime",
"@emnapi/core",
"@emnapi/runtime",
"@tybys/wasm-util",
"@emnapi/wasi-threads",
"tslib"
],
"cpu": [
"wasm32"
],
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.4.3",
"@emnapi/runtime": "^1.4.3",
"@emnapi/wasi-threads": "^1.0.2",
"@napi-rs/wasm-runtime": "^0.2.10",
"@tybys/wasm-util": "^0.9.0",
"tslib": "^2.8.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.10.tgz",
"integrity": "sha512-i1Iwg9gRbwNVOCYmnigWCCgow8nDWSFmeTUU5nbNx3rqbe4p0kRbEqLwLJbYZKmSSp23g4N6rCDmm7OuPBXhDA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.10.tgz",
"integrity": "sha512-sGiJTjcBSfGq2DVRtaSljq5ZgZS2SDHSIfhOylkBvHVjwOsodBhnb3HdmiKkVuUGKD0I7G63abMOVaskj1KpOA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/postcss": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.10.tgz",
"integrity": "sha512-B+7r7ABZbkXJwpvt2VMnS6ujcDoR2OOcFaqrLIo1xbcdxje4Vf+VgJdBzNNbrAjBj/rLZ66/tlQ1knIGNLKOBQ==",
"license": "MIT",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"@tailwindcss/node": "4.1.10",
"@tailwindcss/oxide": "4.1.10",
"postcss": "^8.4.41",
"tailwindcss": "4.1.10"
}
},
"node_modules/@tailwindcss/postcss/node_modules/tailwindcss": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.10.tgz",
"integrity": "sha512-P3nr6WkvKV/ONsTzj6Gb57sWPMX29EPNPopo7+FcpkQaNsrNpZ1pv8QmrYI2RqEKD7mlGqLnGovlcYnBK0IqUA==",
"license": "MIT"
},
"node_modules/@testing-library/dom": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
@@ -7765,15 +7486,6 @@
"node": ">= 6"
}
},
"node_modules/chownr": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=18"
}
},
"node_modules/ci-info": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.2.0.tgz",
@@ -8205,7 +7917,10 @@
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
"dev": true,
"license": "Apache-2.0",
"optional": true,
"peer": true,
"engines": {
"node": ">=8"
}
@@ -8341,19 +8056,6 @@
"once": "^1.4.0"
}
},
"node_modules/enhanced-resolve": {
"version": "5.18.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
"integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
"tapable": "^2.2.0"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/entities": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
@@ -9984,7 +9686,10 @@
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
"integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
@@ -10175,7 +9880,10 @@
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
"integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
"dev": true,
"license": "MPL-2.0",
"optional": true,
"peer": true,
"dependencies": {
"detect-libc": "^2.0.3"
},
@@ -10206,11 +9914,13 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
@@ -10226,11 +9936,13 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
@@ -10246,11 +9958,13 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"freebsd"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
@@ -10266,11 +9980,13 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
@@ -10286,11 +10002,13 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
@@ -10306,11 +10024,13 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
@@ -10326,11 +10046,13 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
@@ -10346,11 +10068,13 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
@@ -10366,11 +10090,13 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
@@ -10386,11 +10112,13 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
@@ -10647,6 +10375,7 @@
"version": "0.30.17",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0"
@@ -10750,38 +10479,12 @@
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/minizlib": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz",
"integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==",
"license": "MIT",
"dependencies": {
"minipass": "^7.1.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/mkdirp": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
"integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
"license": "MIT",
"bin": {
"mkdirp": "dist/cjs/src/bin.js"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/monaco-editor": {
"version": "0.52.2",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz",
@@ -11138,6 +10841,7 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"devOptional": true,
"license": "ISC"
},
"node_modules/picomatch": {
@@ -11177,6 +10881,7 @@
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"devOptional": true,
"funding": [
{
"type": "opencollective",
@@ -12315,6 +12020,7 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"devOptional": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@@ -12672,32 +12378,6 @@
"jiti": "bin/jiti.js"
}
},
"node_modules/tapable": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz",
"integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/tar": {
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz",
"integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==",
"license": "ISC",
"dependencies": {
"@isaacs/fs-minipass": "^4.0.0",
"chownr": "^3.0.0",
"minipass": "^7.1.2",
"minizlib": "^3.0.1",
"mkdirp": "^3.0.1",
"yallist": "^5.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
@@ -12728,15 +12408,6 @@
"node": ">= 6"
}
},
"node_modules/tar/node_modules/yallist": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=18"
}
},
"node_modules/thenify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",

View File

@@ -18,7 +18,6 @@
"test:coverage": "vitest run --coverage"
},
"dependencies": {
"@tailwindcss/postcss": "^4.1.10",
"@univerjs/core": "^0.8.2",
"@univerjs/design": "^0.8.2",
"@univerjs/docs": "^0.8.2",

View File

@@ -1,6 +1,6 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
tailwindcss: {},
autoprefixer: {},
},
}

View File

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

View File

@@ -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,8 +186,42 @@ const PromptInput: React.FC<PromptInputProps> = ({
rows={5}
/>
<div style={{ width: "1rem" }} />
{/* 버튼들을 세로로 배치 */}
<div className="flex flex-col gap-3">
{/* 히스토리 버튼 - 맨 위 */}
{onHistoryToggle && (
<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"
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"
@@ -195,6 +233,7 @@ const PromptInput: React.FC<PromptInputProps> = ({
{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">
Press Enter to send, Shift+Enter for new line |

View 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;

View File

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

View File

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