Files
sheeteasyAI/src/components/sheet/PromptInput.tsx
2025-06-27 14:47:47 +09:00

257 lines
9.0 KiB
TypeScript

import React, { useEffect, useRef, useState } from "react";
import { useAppStore } from "../../stores/useAppStore";
import { aiProcessor } from "../../utils/aiProcessor";
interface PromptInputProps {
value: string;
onChange?: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
onExecute?: () => void;
disabled?: boolean;
maxLength?: number;
onHistoryToggle?: () => void;
historyCount?: number;
}
/**
* 에디트 화면 하단 고정 프롬프트 입력창 컴포넌트
* - 이미지 참고: 입력창, Execute 버튼, 안내문구, 글자수 카운트, 하단 고정
* - 유니버 시트에서 셀 선택 시 자동으로 셀 주소 삽입 기능 포함
* - 선택된 셀 정보 실시간 표시 및 시각적 피드백 제공
* - 현재 선택된 셀 정보 상태바 표시
* - AI 프로세서 연동으로 전송하기 버튼 기능 구현
*/
const PromptInput: React.FC<PromptInputProps> = ({
value,
onChange,
onExecute,
disabled: _disabled = true,
maxLength = 500,
onHistoryToggle,
historyCount,
}) => {
const textareaRef = useRef<HTMLTextAreaElement>(null);
const [, setShowCellInsertFeedback] = useState(false);
const [, setLastInsertedCell] = useState<string | null>(null);
const [, setCurrentSelectedCell] = useState<string | null>(null);
const [, setProcessingMessage] = useState<string>("");
const cellAddressToInsert = useAppStore((state) => state.cellAddressToInsert);
const setCellAddressToInsert = useAppStore(
(state) => state.setCellAddressToInsert,
);
const isProcessing = useAppStore((state) => state.isProcessing);
/**
* 현재 선택된 셀 추적
*/
useEffect(() => {
if (cellAddressToInsert) {
setCurrentSelectedCell(cellAddressToInsert);
}
}, [cellAddressToInsert]);
/**
* 전송하기 버튼 클릭 핸들러 - 상위 컴포넌트 onExecute 사용 또는 기본 로직
*/
const handleExecute = async () => {
if (!value.trim()) {
alert("프롬프트를 입력해주세요.");
return;
}
if (isProcessing || aiProcessor.isCurrentlyProcessing()) {
alert("이미 처리 중입니다. 잠시 후 다시 시도해주세요.");
return;
}
// 상위 컴포넌트에서 onExecute를 전달받은 경우 해당 함수 사용
if (onExecute) {
console.log("🚀 상위 컴포넌트 onExecute 함수 호출");
await onExecute();
return;
}
// 폴백: 기본 AI 프로세서 직접 호출
setProcessingMessage("AI가 요청을 처리하고 있습니다...");
try {
console.log("🚀 전송하기 버튼 클릭 - 프롬프트:", value);
// AI 프로세서에 프롬프트 전송 (테스트 모드)
const result = await aiProcessor.processPrompt(value, true);
console.log("🎉 AI 처리 결과:", result);
if (result.success) {
setProcessingMessage(`✅ 완료: ${result.message}`);
// 성공 시 프롬프트 입력창 초기화 (선택사항)
if (onChange && textareaRef.current) {
textareaRef.current.value = "";
const syntheticEvent = {
target: textareaRef.current,
currentTarget: textareaRef.current,
} as React.ChangeEvent<HTMLTextAreaElement>;
onChange(syntheticEvent);
}
// 3초 후 메시지 숨김
setTimeout(() => {
setProcessingMessage("");
}, 3000);
} else {
setProcessingMessage(`❌ 실패: ${result.message}`);
// 에러 메시지는 5초 후 숨김
setTimeout(() => {
setProcessingMessage("");
}, 5000);
}
} catch (error) {
console.error("❌ AI 프로세싱 오류:", error);
setProcessingMessage("❌ 처리 중 오류가 발생했습니다.");
setTimeout(() => {
setProcessingMessage("");
}, 5000);
}
};
/**
* 셀 주소 삽입 효과
* cellAddressToInsert가 변경되면 textarea의 현재 커서 위치에 해당 주소를 삽입
*/
useEffect(() => {
if (cellAddressToInsert && textareaRef.current && onChange) {
console.log(`🎯 PromptInput: 셀 주소 "${cellAddressToInsert}" 삽입 시작`);
const textarea = textareaRef.current;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const currentValue = textarea.value;
console.log(
`📍 PromptInput: 현재 커서 위치 ${start}-${end}, 현재 값: "${currentValue}"`,
);
// 현재 커서 위치에 셀 주소 삽입
const newValue =
currentValue.slice(0, start) +
cellAddressToInsert +
currentValue.slice(end);
console.log(`✏️ PromptInput: 새 값: "${newValue}"`);
// textarea 값 업데이트
textarea.value = newValue;
// 커서 위치를 삽입된 텍스트 뒤로 이동
const newCursorPosition = start + cellAddressToInsert.length;
textarea.selectionStart = textarea.selectionEnd = newCursorPosition;
// 상위 컴포넌트의 onChange 콜백 호출 (상태 동기화)
const syntheticEvent = {
target: textarea,
currentTarget: textarea,
} as React.ChangeEvent<HTMLTextAreaElement>;
onChange(syntheticEvent);
// 포커스를 textarea로 이동
textarea.focus();
// 시각적 피드백 표시
setLastInsertedCell(cellAddressToInsert);
setShowCellInsertFeedback(true);
// 2초 후 피드백 숨김
setTimeout(() => {
setShowCellInsertFeedback(false);
}, 2000);
console.log(`✅ PromptInput: 셀 주소 "${cellAddressToInsert}" 삽입 완료`);
// 셀 주소 삽입 상태 초기화 (중복 삽입 방지)
setCellAddressToInsert(null);
}
}, [cellAddressToInsert, onChange, setCellAddressToInsert]);
return (
<div className="w-[60%] mx-auto bg-white z-10 flex flex-col items-center py-4 px-2">
<div className="w-full max-w-3xl flex items-end gap-2">
<textarea
ref={textareaRef}
className="flex-1 resize-none rounded-lg border border-gray-300 bg-gray-50 px-4 py-3 text-base text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-400 disabled:bg-gray-100 disabled:cursor-not-allowed min-h-[48px] max-h-32 shadow-sm"
placeholder="질문을 입력하세요.
예시) A1부터 A10까지 합계를 구해서 B1에 입력하는 수식을 입력해줘
💡 팁: 시트에서 셀을 선택하면 자동으로 주소가 입력됩니다"
value={value}
onChange={onChange}
disabled={isProcessing}
maxLength={maxLength}
rows={5}
/>
<div style={{ width: "1rem" }} />
{/* 버튼들을 세로로 배치 */}
<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">
Press Enter to send, Shift+Enter for new line |
</span>
<span className="text-xs text-gray-400">
{value.length}/{maxLength}
</span>
</div>
</div>
);
};
export default PromptInput;