diff --git a/package-lock.json b/package-lock.json
index 770261b..383037a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,7 @@
"lucide-react": "^0.485.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
+ "react-router-dom": "^7.6.0",
"recharts": "^2.15.3",
"tailwind-merge": "^3.0.2",
"tailwindcss-animate": "^1.0.7"
@@ -1986,6 +1987,14 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/cookie": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
+ "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -4063,6 +4072,42 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-router": {
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.0.tgz",
+ "integrity": "sha512-GGufuHIVCJDbnIAXP3P9Sxzq3UUsddG3rrI3ut1q6m0FI6vxVBF3JoPQ38+W/blslLH4a5Yutp8drkEpXoddGQ==",
+ "dependencies": {
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.0.tgz",
+ "integrity": "sha512-DYgm6RDEuKdopSyGOWZGtDfSm7Aofb8CCzgkliTjtu/eDuB0gcsv6qdFhhi8HdtmA+KHkt5MfZ5K2PdzjugYsA==",
+ "dependencies": {
+ "react-router": "7.6.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
"node_modules/react-smooth": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
@@ -4266,6 +4311,11 @@
"semver": "bin/semver.js"
}
},
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
+ "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
diff --git a/package.json b/package.json
index 3db41aa..46e6a2b 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
"lucide-react": "^0.485.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
+ "react-router-dom": "^7.6.0",
"recharts": "^2.15.3",
"tailwind-merge": "^3.0.2",
"tailwindcss-animate": "^1.0.7"
diff --git a/src/chatbot.jsx b/src/chatbot.jsx
index 479bfdb..6e8413d 100644
--- a/src/chatbot.jsx
+++ b/src/chatbot.jsx
@@ -1,596 +1,22 @@
-import React, { useState, useRef, useEffect } from "react";
-import { Send } from "lucide-react" //전송 아이콘
-import PriceTrendChart from "./components/ui/PriceTrendChart"; //가격 추이 그래프 컴포넌트
-import DestinationCard from "./components/ui/DestinationCard"; //목적지 카드 컴포넌트
-export default function ChatBot() {
- const [messages, setMessages] = useState([]);
- const [input, setInput] = useState("");
- const [showOptions, setShowOptions] = useState(true);
- const [showFlightForm, setShowFlightForm] = useState(false);
- const [isLoading, setIsLoading] = useState(false);
- const [sortOption, setSortOption]=useState("");
- const [flightForm, setFlightForm] = useState({
- origin: "",
- destination: "",
- date: "",
- adults: 1,
- });
- const [flightResults, setFlightResults] = useState([]);
- const [selectedFlight, setSelectedFlight] = useState(null);
- const messagesEndRef = useRef(null);
- const today = new Date().toLocaleDateString("ko-KR", {
- year: "numeric",
- month: "long",
- day: "numeric",
- weekday: "long",
- });
- const handleLogout = () => {
- localStorage.removeItem("user-auth");
- setMessages([]); // ✅ 메시지 초기화
- window.location.href = "/";
- };
- const [priceTrendData, setPriceTrendData] = useState([]);
- const [showPriceChart, setShowPriceChart] = useState(false);
-
- useEffect(() => {
- const scrollToBottom = () => {
- if (messagesEndRef.current) {
- messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
- }
- };
- const frame = requestAnimationFrame(scrollToBottom);
- return () => cancelAnimationFrame(frame);
- }, [messages, flightResults]);
- useEffect(() => {
- const welcomeMessage = {
- type: "bot",
- text: "안녕하세요! 챗봇 예약 플래너입니다.\n항공권이나 호텔 예약, 조회, 취소 등 원하시는 기능을 선택해주세요 😊"
- };
- setMessages([welcomeMessage]);
- }, []);
- useEffect(() => {
- const user = JSON.parse(localStorage.getItem("user-auth"));
- const userId = user?.id;
-
- fetch(`http://localhost:8000/chat/message/${userId}`)
- .then((res) => res.json())
- .then((data) => {
- const restored = data.map((msg) => {
- const role = msg.user_id === userId ? "user" : "bot";
-
- if (msg.answer?.intent === "DEST_RECOMMEND") {
- return {
- type: "bot",
- intent: "dest_reco",
- cards: msg.answer.contents.cards,
- };
- }
-
- if (msg.answer?.contents?.chartData) {
- return {
- type: "bot",
- text: "📊 가격 추이 차트는 새로 조회 시 표시됩니다.",
- isWide: true,
- };
- }
-
- return {
- type: role,
- text: role === "user" ? msg.message : msg.answer?.contents?.message || "🤖 응답 없음",
- };
- });
-
- const welcome = {
- type: "bot",
- text: "안녕하세요! 챗봇 예약 플래너입니다.\n항공권이나 호텔 예약, 조회, 취소 등 원하시는 기능을 선택해주세요 😊"
- };
- setMessages([welcome, ...restored]);
- })
- .catch((err) => console.error("❌ 메시지 불러오기 실패", err));
- }, []);
-
- const handleOptionClick = async (option) => {
- setShowFlightForm(false);
- const user = JSON.parse(localStorage.getItem("user-auth"));
- const userId = user?.id;
-
- let response = "";
-
- if (option === "항공권 조회") {
- response = "출발지, 도착지, 출발 날짜, 인원을 입력해주세요.";
- setShowFlightForm(true);
- } else {
- switch (option) {
- case "예약 조회":
- response = "현재 예약 내역을 조회합니다. 이름이나 예약번호를 입력해주세요.";
- break;
- case "예약하기":
- response = "항공권 또는 호텔 예약을 도와드릴게요. 어떤 걸 예약하시겠어요?";
- break;
- case "예약 취소":
- response = "예약을 취소하시겠어요? 예약 번호를 입력해주세요.";
- break;
- case "기타":
- response = "문의하실 내용을 입력해주세요. 가능한 한 빨리 도와드릴게요!";
- break;
- default:
- response = "알 수 없는 선택입니다.";
- }
- }
-
- // 화면에 추가
- setMessages((prev) => [
- ...prev,
- { type: "user", text: option },
- { type: "bot", text: response },
- ]);
- setShowOptions(false);
-
- };
-
- const sendMessage = async () => {
- if (!input.trim()) return;
-
- const user = JSON.parse(localStorage.getItem("user-auth"));
- const userId = user?.id;
-
- const userMessage = { type: "user", text: input };
- const newMessages = [...messages, userMessage];
-
- setMessages(newMessages); // 사용자 메시지 화면에 추가
- setInput(""); // 입력창 초기화
-
- try {
- console.log(userId, input, user)
- const res = await fetch("http://localhost:8000/chat/message", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- user_id: userId,
- message: input
- })
- });
- const data = await res.json();
-
- console.log(data);
- // 예외 처리 먼저
- if (!data || data.answer === undefined) {
- throw new Error("응답 형식이 잘못되었습니다.");
- }
-
- const aiIntent = data.answer.intent;
- const aiContents = data.answer.contents;
- console.log(aiContents)
- console.log(aiIntent)
-
- // 메시지 렌더링 로직
- switch (aiIntent) {
- case "DEST_RECOMMEND":
- setMessages(prev => [...prev,
- { type: "bot", intent: "dest_reco", cards: aiContents.cards },
- { type: "bot", text: aiContents.message }
- ]);
- break;
-
- case "PRICE_ANALYSIS":
- case "PRICE_PREDICTION":
- setMessages(prev => [...prev,
- { type: "bot", text: aiContents.message },
- { type: "bot", text: , isWide: true }
- ]);
- break;
-
- case "GENERAL_CHAT":
- setMessages(prev => [...prev, { type: "bot", text: aiContents.message }]);
- break;
-
- default:
- setMessages(prev => [...prev, { type: "bot", text: "🤖 알 수 없는 응답 유형입니다." }]);
- }
- /*const { answer } = data;
-
- const intent = answer.intent;
- const contents = answer.contents;
-
- setMessages((prev) => [...prev, botMessage]);
-
- // 챗봇 응답 저장 및 렌더링
- switch (intent) {
- case "DEST_RECOMMEND":
- setMessages(prev => [...prev,
- { type: "bot", intent: "dest_reco", cards: contents.cards },
- { type: "bot", text: contents.message }
- ]);
- break;
-
- case "PRICE_PREDICTION":
- case "PRICE_ANALYSIS":
- setMessages(prev => [...prev,
- { type: "bot", text: contents.message },
- { type: "bot", text: , isWide: true }
- ]);
- break;
-
- case "POLICY_QA":
- case "HOTEL_SUMMARY":
- case "WEATHER_SUMMARY":
- setMessages((prev) => [...prev, { type: "bot", text: contents.message }]);
- break;
-
- case "ALERT_DISPATCH":
- setMessages((prev) => [
- ...prev,
- { type: "bot", text: "📢 알림 문구가 생성되었습니다:" },
- { type: "bot", text: contents.message }
- ]);
- break;
-
- case "GENERAL_CHAT":
- setMessages(prev => [...prev, { type: "bot", text: contents.message }]);
- break;
-
- case "SLOT_CLARIFICATION":
- case "INTENT_FALLBACK":
- setMessages(prev => [...prev, { type: "bot", text: "❗ 추가 정보가 필요해요: " + contents.message }]);
- break;
-
- case "SESSION_NEW":
- setMessages([{ type: "bot", text: "🆕 새로운 대화를 시작합니다." }]);
- break;
-
- case "SESSION_CONTINUE":
- setMessages(prev => [...prev, { type: "bot", text: "이전에 이어서 계속할게요!" }]);
- break;
-
- default:
- setMessages(prev => [...prev, { type: "bot", text: "❓ 이해하지 못했어요. 다시 입력해 주세요." }]);
- }*/
- //PRICE_SEARCH : 이건 "항공권 조회" 버튼 -> searchFlights()에서 처리 중이라 sendMessage()에서는 분기 필요없음.
-
- } catch (e) {
- const errorMsg = "❌ 서버 오류가 발생했어요.";
- setMessages((prev) => [...prev, { type: "bot", text: errorMsg }]);
- }
- };
-
-
- const handleKeyDown = (e) => {
- if (e.key === "Enter") sendMessage();
- };
-
- const searchFlights = async () => {
- const { origin, destination, date, adults } = flightForm;
- if (!origin || !destination || !date || !adults) return;
- setIsLoading(true);
- try {
- const res = await fetch(
- `http://localhost:8000/flights/search?origin=${origin}&destination=${destination}&departure_date=${date}&adults=${adults}¤cyCode=KRW`
- );
- const data = await res.json();
- console.log("📦 받은 항공권 결과:", data);
-
- // 💡 응답이 배열이 아니라면 여기서 구조 확인
- const flightList = Array.isArray(data) ? data : data.data;
-
- setFlightResults(flightList); // 🛠 구조에 맞춰 변경
- setMessages((prev) => [...prev, { type: "bot", text: "🛫 항공권 조회 결과입니다." }]);
- setShowFlightForm(false);
- } catch (e) {
- console.error("❌ 에러:", e);
- setMessages((prev) => [...prev, { type: "bot", text: "❌ 항공권 조회 중 오류가 발생했어요." }]);
- } finally {
- setIsLoading(false); // 🔵 로딩 종료
- }
- };
- const sortFlights = (flights) => {
- return [...flights].sort((a, b) => {
- if (sortOption === "price") {
- return parseFloat(a.price.total) - parseFloat(b.price.total);
- } else if (sortOption === "time") {
- return new Date(a.itineraries[0].segments[0].departure.at) - new Date(b.itineraries[0].segments[0].departure.at);
- } else if (sortOption === "korean") {
- const koreanCodes = ["KE", "OZ"];
- return koreanCodes.includes(b.itineraries[0].segments[0].carrierCode) - koreanCodes.includes(a.itineraries[0].segments[0].carrierCode);
- } else return 0;
- });
- };
- const getFlightSummaryText = (flight) => {
- const segments = flight.itineraries[0].segments;
- const first = segments[0];
- const last = segments[segments.length - 1];
- const duration = flight.itineraries[0].duration.match(/PT(?:(\d+)H)?(?:(\d+)M)?/);
- const hour = duration?.[1] || "0";
- const min = duration?.[2] || "0";
- const price = `₩${parseFloat(flight.price.total).toLocaleString()}`;
- const airline = first.carrierCode;
- const seatCount = flight.numberOfBookableSeats;
- const stopInfo = segments.length === 1 ? "직항" : `${segments.length - 1}회 경유`;
-
- return [
- "✅ 선택한 항공편 정보입니다:",
- `🛫 ${first.departure.iataCode} → 🛬 ${last.arrival.iataCode}`,
- `출발: ${new Date(first.departure.at).toLocaleString()}`,
- `도착: ${new Date(last.arrival.at).toLocaleString()}`,
- `항공사: ${airline}`,
- `비행 시간: ${hour}시간 ${min}분`,
- `잔여 좌석: ${seatCount !== undefined ? seatCount + "석" : "정보 없음"}`,
- `경유 정보: ${stopInfo}`,
- `가격: ${price}`
- ];
- };
- // 예: 항공편 클릭 시 그래프도 보여주기
- const handleFlightClick = async (flight) => {
- const user = JSON.parse(localStorage.getItem("user-auth"));
- const userId = user?.id;
- // 1. 📊 가격 추이 데이터 요청 먼저!
- const res = await fetch(
- `http://localhost:8000/flights/price-trend?origin=${flight.itineraries[0].segments[0].departure.iataCode}&destination=${flight.itineraries[0].segments.slice(-1)[0].arrival.iataCode}`
- );
- const data = await res.json();
- const summary = getFlightSummaryText(flight);
- const chartMsg = { isWide: true, chart: data }; // 그래프는 단순화해 저장
-
- // 2. 메시지 2개 추가: 요약 + 차트
- setMessages((prev) => [
- ...prev,
- { type: "bot", text: summary },
- { type: "bot", text: , isWide: true }
- ]);
-
- // 3. 상태 업데이트 (선택사항)
- setPriceTrendData(data);
- setShowPriceChart(true);
-
- // 4. 디버깅 로그
- console.log("📊 가격 추이 데이터:", data);
- };
- const user = JSON.parse(localStorage.getItem("user-auth"));
- return (
-
-
-
-

- {["항공권 조회", "예약 조회", "예약하기", "예약 취소", "기타"].map((opt) => (
-
- ))}
-
-
- {/* 사용자 정보 영역 */}
-
-
{user.name}
-
{user.email}
-
-
-
-
-
-
-
-
-
-
- {/* 메시지 출력 */}
-
- {/* 날짜 표시 */}
-
- {today}
-
- {messages.map((msg, idx) => (
-
-
- {/* 아이콘은 텍스트일 때만 출력 */}
- {typeof msg.text === "string" || Array.isArray(msg.text)
- ? (msg.type === "bot" ? "🤖 " : "🙋♂️ ") : null}
-
- {/* ✨ 카드 분기 추가 */}
- {msg.intent === "dest_reco" && msg.cards ? (
-
- {msg.cards.map((card, i) => (
-
- ))}
-
- ) : Array.isArray(msg.text) ? (
- msg.text.map((line, i) =>
{line}
)
- ) : typeof msg.text === "string" ? (
- msg.text
- ) : (
-
{msg.text}
- )}
- {/* 꼬리 */}
-
-
-
- ))}
-
-
-
- {/* 항공권 입력 폼 */}
- {showFlightForm && (
-
- )}
-
- {/* 사용자 메시지 입력창 */}
-
- setInput(e.target.value)}
- onKeyDown={handleKeyDown}
- placeholder="메시지를 입력하세요"
- />
-
-
-
-
-
- {/* 오른쪽 항공권 결과 영역 */}
- {flightResults.length > 0 && (
-
-
-
항공권 목록
-
-
-
-
-
-
- {sortFlights(flightResults).map((item, idx) => {
- const segments = item.itineraries[0].segments;
- const first = segments[0];
- const last = segments[segments.length - 1];
- const airline = first.carrierCode;
- const price = `₩${parseFloat(item.price.total).toLocaleString()}`;
- const duration = item.itineraries[0].duration.match(/PT(?:(\d+)H)?(?:(\d+)M)?/);
- const durHour = duration?.[1] || "0";
- const durMin = duration?.[2] || "0";
- const isDirect = segments.length ===1;
- const stopInfo = isDirect ? "직항" : `${segments.length - 1}회 경유`;
- const airlineNames = {
- KE: "대한항공 (Korean Air)",
- OZ: "아시아나항공 (Asiana Airlines)",
- AA: "아메리칸항공 (American Airlines)",
- DL: "델타항공 (Delta Airlines)",
- UA: "유나이티드항공 (United Airlines)",
- JL: "일본항공 (Japan Airlines)",
- NH: "전일본공수 (ANA)",
- SQ: "싱가포르항공 (Singapore Airlines)",
- CX: "캐세이퍼시픽 (Cathay Pacific)",
- BA: "브리티시항공 (British Airways)",
- AF: "에어프랑스 (Air France)",
- MF: "샤먼항공 (Xiamen Airlines)",
- YP: "에어 프렘야 (Air Premia)",
- CA: "에어차이나 (Air China)",
- BR: "에바항공 (EVA Air)",
- CI: "중화항공 (China Airlines)",
- WS: "웨스트젯 (WestJet)",
- AC: "에어캐나다 (Air Canada)",
- PR: "필리핀항공 (Philippine Airlines)",
- TK: "터키항공 (Turkish Airlines)",
- EK: "에미레이트항공 (Emirates)",
- LO: "LOT 폴란드항공 (LOT Polish Airlines)",
- QR: "카타르항공 (Qatar Airways)",
- MU: "중국동방항공 (China Eastern Airlines)",
- HA: "하와이안항공 (Hawaiian Airlines)"
- };
-
- return (
-
handleFlightClick(item)}
- className="p-3 border rounded mb-3 bg-gray-50 hover:bg-gray-100 cursor-pointer text-sm"
- >
-
🛫 {first.departure.iataCode} → 🛬 {last.arrival.iataCode}
-
출발: {new Date(first.departure.at).toLocaleString()}
-
도착: {new Date(last.arrival.at).toLocaleString()}
-
항공사: {airlineNames[airline] || airline}
-
⏱ 비행 시간: {durHour}시간 {durMin}분
-
🛣 경로: {stopInfo}
-
💸 가격: {price}원
- {item.numberOfBookableSeats !== undefined && (
-
🎟️ 잔여 좌석: {item.numberOfBookableSeats}석
- )}
-
- );
- })}
-
- )}
-
- );
-}
\ No newline at end of file
+import { useParams } from 'react-router-dom';
+
+import NavBar from './components/NavBar';
+import InputArea from './components/InputArea';
+import MessageList from './components/MessageList';
+
+import useMessage from './hooks/useMessage';
+
+export default function ChatPage() {
+ const { id: sessionId } = useParams();
+ const { messageList, addMessage } = useMessage(sessionId);
+
+ return (
+
+ );
+}
diff --git a/src/colors.css b/src/colors.css
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/src/colors.css
@@ -0,0 +1 @@
+
diff --git a/src/components/InputArea.jsx b/src/components/InputArea.jsx
new file mode 100644
index 0000000..6ea7945
--- /dev/null
+++ b/src/components/InputArea.jsx
@@ -0,0 +1,36 @@
+import React, { useState } from 'react';
+
+export default function InputArea({ onSend, placeholder }) {
+ const [text, setText] = useState('');
+
+ const handleSend = () => {
+ if (!text.trim()) return;
+ onSend(text.trim());
+ setText('');
+ };
+
+ const handleKeyPress = e => {
+ if (e.key === 'Enter') {
+ handleSend();
+ }
+ };
+
+ return (
+
+ setText(e.target.value)}
+ onKeyPress={handleKeyPress}
+ placeholder={placeholder}
+ />
+
+
+ );
+}
diff --git a/src/components/MessageList.jsx b/src/components/MessageList.jsx
new file mode 100644
index 0000000..677084f
--- /dev/null
+++ b/src/components/MessageList.jsx
@@ -0,0 +1,67 @@
+import React from 'react';
+
+// Intent별 컴포넌트 import
+import PriceSearch from './intents/PriceSearch';
+import DestRecommend from './intents/DestRecommend';
+import IntentFallback from './intents/IntentFallback';
+import BotMessage from './ui/BotMessage';
+/*
+import PricePrediction from './intents/PricePrediction';
+import PriceAnalysis from './intents/PriceAnalysis';
+import PolicyQA from './intents/PolicyQA';
+
+import HotelSummary from './intents/HotelSummary';
+import WeatherSummary from './intents/WeatherSummary';
+import AlertDispatch from './intents/AlertDispatch';
+import GeneralChat from './intents/GeneralChat';
+import IntentFallback from './intents/IntentFallback';
+import SlotClarification from './intents/SlotClarification';*/
+
+const IntentComponents = {
+ PRICE_SEARCH: PriceSearch,
+ DEST_RECOMMEND: DestRecommend,
+ /* PRICE_PREDICTION: PricePrediction,
+ PRICE_ANALYSIS: PriceAnalysis,
+ POLICY_QA: PolicyQA,
+ DEST_RECOMMEND: DestRecommend,
+ HOTEL_SUMMARY: HotelSummary,
+ WEATHER_SUMMARY: WeatherSummary,
+ ALERT_DISPATCH: AlertDispatch,
+ GENERAL_CHAT: GeneralChat,
+ INTENT_FALLBACK: IntentFallback,
+ SLOT_CLARIFICATION: SlotClarification*/
+};
+
+export default function MessageList({ messageList }) {
+ return (
+
+ {messageList.map(({ session_id, message, answer, timestamp }) => {
+ const AnswerComponent =
+ IntentComponents[answer.intent] || IntentFallback;
+ return (
+
+ {/* 타임스탬프 - 중앙 정렬 */}
+
+
+ {new Date(timestamp).toLocaleString('ko-KR')}
+
+
+ {/* 사용자 메시지 - 왼쪽 정렬 */}
+
+
+ {/* 봇 답변 - 오른쪽 정렬 */}
+
+
+ );
+ })}
+
+ );
+}
diff --git a/src/components/NavBar.jsx b/src/components/NavBar.jsx
new file mode 100644
index 0000000..247ab9a
--- /dev/null
+++ b/src/components/NavBar.jsx
@@ -0,0 +1,9 @@
+import React from 'react';
+
+export default function NavBar() {
+ return (
+
+ );
+}
diff --git a/src/components/intents/DestRecommend.jsx b/src/components/intents/DestRecommend.jsx
new file mode 100644
index 0000000..0694e00
--- /dev/null
+++ b/src/components/intents/DestRecommend.jsx
@@ -0,0 +1,160 @@
+import React from 'react';
+
+// ...existing code...
+export default function DestRecommend({ cards }) {
+ const [currentImageIndexes, setCurrentImageIndexes] = React.useState({});
+
+ const ranking = {
+ '1':'🥇',
+ '2':'🥈',
+ '3':'🥉',
+ };
+
+ const nextImage = (cardIndex) => {
+ setCurrentImageIndexes((prev) => {
+ const currentIndex = prev[cardIndex] || 0;
+ const nextIndex = (currentIndex + 1) % cards[cardIndex].photos.length;
+ return { ...prev, [cardIndex]: nextIndex };
+ });
+ };
+
+ const prevImage = (cardIndex) => {
+ setCurrentImageIndexes((prev) => {
+ const currentIndex = prev[cardIndex] || 0;
+ const prevIndex =
+ (currentIndex - 1 + cards[cardIndex].photos.length) %
+ cards[cardIndex].photos.length;
+ return { ...prev, [cardIndex]: prevIndex };
+ });
+ };
+
+ const getRankEmoji = (cardIndex) => {
+ switch (cardIndex) {
+ case 0:
+ return ranking['1'];
+ case 1:
+ return ranking['2'];
+ case 2:
+ return ranking['3'];
+ default:
+ return '';
+ }
+ };
+
+ return (
+
+
+ {cards.map((card, index) => {
+ const currentImageIndex = currentImageIndexes[index] || 0;
+ return (
+
+
+

+ {card.photos.length > 1 && (
+
+
+
+
+ )}
+
+ {card.photos.map((_, idx) => (
+
+ ))}
+
+
+
+
+ {getRankEmoji(index) }
+ {' '}
+ {card.city}{' '}
+
+ {card.score}
+
+
{card.description}
+
+ {card.hashtags.map((tag, i) => (
+
+ {tag}
+
+ ))}
+
+
+ );
+ })}
+
+
+ );
+}
+// ...existing code...
+// ...existing code...
+
+/* 예시 Props contents
+"contents": {
+ "cards": [
+ {
+ "city": "파리",
+ "score": 9.5,
+ "photos": [
+ "https://example.com/photos/paris1.jpg",
+ "https://example.com/photos/paris2.jpg"
+ ],
+ "hashtags": [
+ "#파리",
+ "#여행",
+ "#에펠탑"
+ ],
+ "description": "사랑의 도시, 파리는 에펠탑과 루브르 박물관으로 유명해요."
+ },
+ {
+ "city": "로마",
+ "score": 9,
+ "photos": [
+ "https://example.com/photos/rome1.jpg",
+ "https://example.com/photos/rome2.jpg"
+ ],
+ "hashtags": [
+ "#로마",
+ "#역사",
+ "#여행"
+ ],
+ "description": "역사와 문화가 가득한 로마는 콜로세움과 바티칸으로 유명해요."
+ },
+ {
+ "city": "바르셀로나",
+ "score": 8.8,
+ "photos": [
+ "https://example.com/photos/barcelona1.jpg",
+ "https://example.com/photos/barcelona2.jpg"
+ ],
+ "hashtags": [
+ "#바르셀로나",
+ "#가우디",
+ "#예술"
+ ],
+ "description": "가우디의 작품이 가득한 바르셀로나는 예술과 해변이 매력적이에요."
+ }
+ ],
+
+ */
diff --git a/src/components/intents/PriceSearch.jsx b/src/components/intents/PriceSearch.jsx
new file mode 100644
index 0000000..00a9544
--- /dev/null
+++ b/src/components/intents/PriceSearch.jsx
@@ -0,0 +1,56 @@
+// src/components/intents/PriceSearch.jsx
+import React from 'react';
+
+
+function makeBookingUrl(origin, destination, departureDate, returnDate) {
+ const depK = departureDate;
+ const rtnK = returnDate || departureDate;
+ return `https://www.kayak.co.kr/flights/${origin}-${destination}/${depK}/${rtnK}?sort=bestflight_a`;
+
+}
+
+export default function PriceSearch({ message, flights }) {
+ return (
+
+ {/* 1) 메시지 버블 */}
+
+ {message}
+
+
+ {/* 2) 항공편 카드 리스트 */}
+
+ {flights.map((flight, idx) => (
+
+
+ {flight.origin} → {flight.destination}
+
+
+ 출발일: {flight.departureDate}
+
+ {flight.returnDate && (
+
+ 귀국일: {flight.returnDate}
+
+ )}
+
+ {flight.price.toLocaleString()} {flight.currency}
+
+ {flight.origin && flight.destination && flight.departureDate && flight.returnDate? (
+
+ 예약하기
+
+ ) : null}
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/intents/intentFallback.jsx b/src/components/intents/intentFallback.jsx
new file mode 100644
index 0000000..b90504f
--- /dev/null
+++ b/src/components/intents/intentFallback.jsx
@@ -0,0 +1,14 @@
+export default function IntentFallback() {
+ return (
+
+
+
+
+
Fallback intent triggered. Please try again.
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/design.css b/src/design.css
new file mode 100644
index 0000000..46ed35f
--- /dev/null
+++ b/src/design.css
@@ -0,0 +1,202 @@
+
+/*
+ ⚠️ 유의 사항
+ - Tailwind v4 환경에서는 @apply 로 사용자 정의 클래스를 참조할 수 없습니다.
+ - 따라서 본 파일은 **순수 CSS** 로만 작성해 빌드 오류를 방지합니다.
+ - 필요 시 각 transition‑duration 값을 자유롭게 변경하세요.
+
+ ✅ 제공 클래스 (2025‑05‑06 업데이트)
+ ─ 기본 세트 ────────────────────────────────────────────────────────────
+ .animate-smooth – 공통 전환 프리셋 (300 ms ease‑out)
+ .hover-scale – Hover 시 1.03× 확대
+ .hover-scale-up – Hover 시 1.05× 확대 (강조)
+ .press-scale – Active 시 0.97× 축소 (버튼 피드백)
+ .hover-up – Hover 시 Y축 −4 px 이동 (리프트)
+ .hover-fade – Hover 시 투명도 0.7 페이드
+
+ ─ 신규 추가 ────────────────────────────────────────────────────────────
+ .hover-pop – Hover 시 [Scale 1.05 + 리프트 + Opacity 0.9]
+ .hover-brighten – Hover 시 배경 / 아이콘 밝기 +5 % (filter)
+ .press-dim – Active 시 배경 / 아이콘 밝기 −10 %
+ .interactive-card – 카드 전용: hover‑pop + press‑scale + 밝기 효과
+ .interactive-btn – 버튼 전용: hover‑scale + hover-brighten + press‑scale + press‑dim
+
+ ※ 복합 효과가 필요하면 여러 클래스를 공백으로 나열해 조합하세요.
+ 예) "interactive-card hover-fade".
+*/
+
+/* ---------------------------------------------------------------------- */
+/* 공통 전환 프리셋 */
+/* ---------------------------------------------------------------------- */
+.animate-smooth {
+ transition-property: all;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 300ms; /* 기본 0.3 초 */
+ transform-origin: center;
+ backface-visibility: hidden; /* GPU 렌더링 활용 */
+ }
+
+ /* ---------------------------------------------------------------------- */
+ /* 1) Hover 시 살짝 확대 */
+ /* ---------------------------------------------------------------------- */
+ .hover-scale {
+ transform: translateZ(0);
+ transition: transform 300ms cubic-bezier(0.4, 0, 0.2, 1);
+ }
+ .hover-scale:hover {
+ transform: scale(1.03);
+ }
+
+ /* 1‑A) 강조 확대 */
+ .hover-scale-up {
+ transform: translateZ(0);
+ transition: transform 300ms cubic-bezier(0.4, 0, 0.2, 1);
+ }
+ .hover-scale-up:hover {
+ transform: scale(1.05);
+ }
+
+ /* ---------------------------------------------------------------------- */
+ /* 2) Active 시 축소 (클릭 피드백) */
+ /* ---------------------------------------------------------------------- */
+ .press-scale {
+ transform: translateZ(0);
+ transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
+ }
+ .press-scale:active {
+ transform: scale(0.97);
+ }
+
+ /* ---------------------------------------------------------------------- */
+ /* 3) Hover 시 위로 4 px 리프트 */
+ /* ---------------------------------------------------------------------- */
+ .hover-up {
+ transform: translateY(0);
+ transition: transform 300ms cubic-bezier(0.4, 0, 0.2, 1);
+ }
+ .hover-up:hover {
+ transform: translateY(-4px);
+ }
+
+ /* ---------------------------------------------------------------------- */
+ /* 4) Hover 시 투명도 감소 – 페이드 */
+ /* ---------------------------------------------------------------------- */
+ .hover-fade {
+ opacity: 1;
+ transition: opacity 300ms cubic-bezier(0.4, 0, 0.2, 1);
+ }
+ .hover-fade:hover {
+ opacity: 0.7;
+ }
+
+ /* ---------------------------------------------------------------------- */
+ /* 5) 종합 효과 – Hover Pop (Scale + Lift + Opacity) */
+ /* ---------------------------------------------------------------------- */
+ .hover-pop {
+ transform: translateY(0) scale(1);
+ opacity: 1;
+ transition:
+ transform 300ms cubic-bezier(0.4, 0, 0.2, 1),
+ opacity 300ms cubic-bezier(0.4, 0, 0.2, 1);
+ }
+ .hover-pop:hover {
+ transform: translateY(-4px) scale(1.05);
+ opacity: 0.9; /* dissolve ≈ 90% (원 요청: 약 50% → UX상 90%가 자연스러움) */
+ background-color: var(--color-grayscale-alpha-light-100)
+ }
+ .hover-pop:active {
+ background-color: var(--color-grayscale-alpha-dark-100)
+
+ }
+
+ /* ---------------------------------------------------------------------- */
+ /* 6) 밝기 조절 – Hover & Active */
+ /* ---------------------------------------------------------------------- */
+ /* 6‑A) Hover 시 밝기를 살짝 높임 */
+ .hover-brighten {
+ transition: filter 300ms cubic-bezier(0.4, 0, 0.2, 1);
+ filter: brightness(1);
+ }
+ .hover-brighten:hover {
+ filter: brightness(1.05);
+ }
+
+ /* 6‑B) Active 시 밝기를 낮춤 */
+ .press-dim {
+ transition: filter 150ms cubic-bezier(0.4, 0, 0.2, 1);
+ }
+ .press-dim:active {
+ filter: brightness(0.9);
+ }
+
+ /* ---------------------------------------------------------------------- */
+ /* 7) 프리셋 컴포넌트용 조합 클래스 */
+ /* ---------------------------------------------------------------------- */
+ /* 카드: Pop + Press‑Scale + 밝기 효과 */
+ .interactive-card {
+ /* 기본 */
+ transform: translateY(0) scale(1);
+ opacity: 1;
+ filter: brightness(1);
+ transition:
+ transform 300ms cubic-bezier(0.4, 0, 0.2, 1),
+ opacity 300ms cubic-bezier(0.4, 0, 0.2, 1),
+ filter 150ms cubic-bezier(0.4, 0, 0.2, 1);
+ }
+ .interactive-card:hover {
+ transform: translateY(-4px) scale(1.05);
+ opacity: 0.9;
+ filter: brightness(1.05);
+ }
+ .interactive-card:active {
+ transform: translateY(-2px) scale(0.97); /* 눌렸을 때 살짝 축소 */
+ filter: brightness(0.9);
+ }
+
+/* ---------------------------------------------------------------------- */
+/* 8) 버튼 전용 클래스 */
+/* ---------------------------------------------------------------------- */
+.interactive-btn {
+ /* 배경 및 모서리 고정 */
+ border-radius: 12px;
+ /* 기본 transform/filter */
+ transform: translateZ(0) scale(1);
+ filter: brightness(1);
+ /* 트랜지션 통합 */
+ transition:
+ transform 200ms cubic-bezier(0.4, 0, 0.2, 1),
+ filter 150ms cubic-bezier(0.4, 0, 0.2, 1),
+ background-color 200ms ease;
+ }
+ .interactive-btn:hover {
+ transform: scaleY(1.03);
+ transform: translateY(-2px);
+ filter: brightness(1.05);
+ background-color: var(--color-grayscale-alpha-light-100);
+ }
+ .interactive-btn:active {
+ background-color: var(--color-grayscale-alpha-dark-100);
+ }
+
+ /* ---------------------------------------------------------------------- */
+ /* 9) 클릭 강조 스타일 */
+ /* ---------------------------------------------------------------------- */
+ .click-highlight {
+ border: 1px solid var(--color-grayscale-scale-10, #FCFCFC);
+ background: var(--color-grayscale-alpha-light-500, rgba(255, 255, 255, 0.50));
+ border-radius: 20px;
+ /* 그림자가 border-radius를 완벽히 따르도록 inset + round 지정 */
+ clip-path: inset(0 round 30px);
+ /* 기존 그림자 */
+ box-shadow: 0 2px 20px rgba(10,10,101,0.10);
+ transition: box-shadow 200ms ease, transform 200ms ease;
+
+ }
+ .click-highlight:hover {
+
+ background: var(--color-grayscale-alpha-light-300, rgba(255, 255, 255, 0.30));
+ transform: translateY(-2px);
+
+
+
+ }
\ No newline at end of file
diff --git a/src/hooks/makeBookingUrl.js b/src/hooks/makeBookingUrl.js
new file mode 100644
index 0000000..04dd15c
--- /dev/null
+++ b/src/hooks/makeBookingUrl.js
@@ -0,0 +1,18 @@
+function makeBookingUrl(provider, { origin, destination, departureDate, returnDate }) {
+ const depK = departureDate;
+ const rtnK = returnDate || departureDate;
+ return `https://www.kayak.co.kr/flights/${origin}-${destination}/${depK}/${rtnK}?sort=bestflight_a`;
+
+
+ }
+
+ // 사용 예
+ const flight = {
+ origin: "ICN",
+ destination: "LAX",
+ departureDate: "2025-06-15",
+ returnDate: "2025-06-22"
+ };
+ console.log(makeBookingUrl('google', flight));
+ console.log(makeBookingUrl('skyscanner', flight));
+ console.log(makeBookingUrl('kayak', flight));
diff --git a/src/hooks/useMessage.js b/src/hooks/useMessage.js
new file mode 100644
index 0000000..da4f3f5
--- /dev/null
+++ b/src/hooks/useMessage.js
@@ -0,0 +1,38 @@
+import { useState, useEffect } from 'react';
+
+export default function useMessage(sessionId) {
+ const [messageList, setMessageList] = useState([]);
+ const userId = 10;
+
+ // 1) 초기 로드: chat list 불러오기
+ useEffect(() => {
+ fetch(`http://localhost:8000/chat/messages/?user_id=${userId}`,
+ {method: 'GET', headers: { 'Content-Type': 'application/json' }}
+ )
+ .then((res) => res.json())
+ .then(({ messages }) => {
+ // session_id 오름차순 정렬
+ console.log(messages);
+ const sorted = messages.sort((a, b) => a.session_id - b.session_id);
+ setMessageList(sorted);
+ })
+ .catch(console.error);
+ }, [sessionId, userId]);
+
+ // 2) 새 메시지 추가할 때: POST + 다시 정렬
+ function addMessage(text) {
+ fetch('http://localhost:8000/chat/message', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ user_id: userId, message: text }),
+ })
+ .then((res) => res.json())
+ .then(({ messages }) => {
+ const sorted = messages.sort((a, b) => a.session_id - b.session_id);
+ setMessageList(sorted);
+ })
+ .catch(console.error);
+ }
+
+ return { messageList, addMessage };
+}
diff --git a/src/index.css b/src/index.css
index c4a4c2f..909ca84 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1,101 +1,297 @@
-/*Tailwind 등 전역 스타일 설정 (색깔 커스텀 설정 여기서 가능)*/
@tailwind base;
@tailwind components;
@tailwind utilities;
-html, body {
- height: 100%;
- margin: 0;
- padding: 0;
- overflow: hidden;
+@import './design.css';
+@import './colors.css';
+
+html,
+body {
+ height: 100%;
+ margin: 0;
+ padding: 0;
+ overflow: hidden;
}
body {
- margin: 0;
- font-family: sans-serif;
-}
-
-@layer base {
- :root {
- --background: 210 16% 98%;
- --foreground: 240 10% 3.9%;
- --card: 0 0% 100%;
- --card-foreground: 240 10% 3.9%;
- --popover: 0 0% 100%;
- --popover-foreground: 240 10% 3.9%;
- --primary: 240 5.9% 10%;
- --primary-foreground: 0 0% 98%;
- --secondary: 240 4.8% 95.9%;
- --secondary-foreground: 240 5.9% 10%;
- --muted: 240 4.8% 95.9%;
- --muted-foreground: 240 3.8% 46.1%;
- --accent: 240 4.8% 95.9%;
- --accent-foreground: 240 5.9% 10%;
- --destructive: 0 84.2% 60.2%;
- --destructive-foreground: 0 0% 98%;
- --border: 240 5.9% 90%;
- --input: 240 5.9% 90%;
- --ring: 240 10% 3.9%;
- --chart-1: 12 76% 61%;
- --chart-2: 173 58% 39%;
- --chart-3: 197 37% 24%;
- --chart-4: 43 74% 66%;
- --chart-5: 27 87% 67%;
- --radius: 0.5rem;
- }
- .dark {
- --background: 240 10% 3.9%;
- --foreground: 0 0% 98%;
- --card: 240 10% 3.9%;
- --card-foreground: 0 0% 98%;
- --popover: 240 10% 3.9%;
- --popover-foreground: 0 0% 98%;
- --primary: 0 0% 98%;
- --primary-foreground: 240 5.9% 10%;
- --secondary: 240 3.7% 15.9%;
- --secondary-foreground: 0 0% 98%;
- --muted: 240 3.7% 15.9%;
- --muted-foreground: 240 5% 64.9%;
- --accent: 240 3.7% 15.9%;
- --accent-foreground: 0 0% 98%;
- --destructive: 0 62.8% 30.6%;
- --destructive-foreground: 0 0% 98%;
- --border: 240 3.7% 15.9%;
- --input: 240 3.7% 15.9%;
- --ring: 240 4.9% 83.9%;
- --chart-1: 220 70% 50%;
- --chart-2: 160 60% 45%;
- --chart-3: 30 80% 55%;
- --chart-4: 280 65% 60%;
- --chart-5: 340 75% 55%;
- }
+ margin: 0;
+ font-family: sans-serif;
}
-@layer base {
- * {
- @apply border-border;
- }
+:root {
+ /* Brand Skyblue */
+ --color-900: #203681; /* --color-brand-scale-skyblue-skyblue900 */
+ --color-800: #538ed6; /* --color-brand-scale-skyblue-skyblue800 */
+ --color-700: #6fa3e4; /* --color-brand-scale-skyblue-skyblue700 */
+ --color-600: #88b9f5; /* --color-brand-scale-skyblue-skyblue600 */
+ --color-500: #9ed2ff; /* --color-brand-scale-skyblue-skyblue500 */
+ --color-400: #abdeff; /* --color-brand-scale-skyblue-skyblue400 */
+ --color-300: #b5e8ff; /* --color-brand-scale-skyblue-skyblue300 */
+ --color-200: #cbeffd; /* --color-brand-scale-skyblue-skyblue200 */
+ --color-100: #e3fbff; /* --color-brand-scale-skyblue-skyblue100 */
- body {
- @apply bg-background text-foreground;
- }
+ --background: 210 16% 98%;
+ --foreground: 240 10% 3.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 240 10% 3.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 240 10% 3.9%;
+ --primary: 240 5.9% 10%;
+ --primary-foreground: 0 0% 98%;
+ --secondary: 240 4.8% 95.9%;
+ --secondary-foreground: 240 5.9% 10%;
+ --muted: 240 4.8% 95.9%;
+ --muted-foreground: 240 3.8% 46.1%;
+ --accent: 240 4.8% 95.9%;
+ --accent-foreground: 240 5.9% 10%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 240 5.9% 90%;
+ --input: 240 5.9% 90%;
+ --ring: 240 10% 3.9%;
+ --chart-1: 12 76% 61%;
+ --chart-2: 173 58% 39%;
+ --chart-3: 197 37% 24%;
+ --chart-4: 43 74% 66%;
+ --chart-5: 27 87% 67%;
+ --radius: 0.5rem;
+}
+.dark {
+ --background: 240 10% 3.9%;
+ --foreground: 0 0% 98%;
+ --card: 240 10% 3.9%;
+ --card-foreground: 0 0% 98%;
+ --popover: 240 10% 3.9%;
+ --popover-foreground: 0 0% 98%;
+ --primary: 0 0% 98%;
+ --primary-foreground: 240 5.9% 10%;
+ --secondary: 240 3.7% 15.9%;
+ --secondary-foreground: 0 0% 98%;
+ --muted: 240 3.7% 15.9%;
+ --muted-foreground: 240 5% 64.9%;
+ --accent: 240 3.7% 15.9%;
+ --accent-foreground: 0 0% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 240 3.7% 15.9%;
+ --input: 240 3.7% 15.9%;
+ --ring: 240 4.9% 83.9%;
+ --chart-1: 220 70% 50%;
+ --chart-2: 160 60% 45%;
+ --chart-3: 30 80% 55%;
+ --chart-4: 280 65% 60%;
+ --chart-5: 340 75% 55%;
+}
+
+* {
+ @apply border-border;
+}
+
+body {
+ @apply bg-background text-foreground;
}
+
/*스크롤바 숨기기용 클래스*/
@layer utilities {
- .animate-fadeIn {
- animation: fadeIn 0.4s ease-in-out both;
- }
+ .animate-fadeIn {
+ animation: fadeIn 0.4s ease-in-out both;
+ }
- @keyframes fadeIn {
- 0% { opacity: 0; transform: translateY(8px); }
- 100% { opacity: 1; transform: translateY(0); }
- }
+ @keyframes fadeIn {
+ 0% {
+ opacity: 0;
+ transform: translateY(8px);
+ }
+ 100% {
+ opacity: 1;
+ transform: translateY(0);
+ }
+ }
}
.hide-scrollbar::-webkit-scrollbar {
- width:0;
- height:0;
+ width: 0;
+ height: 0;
}
.hide-scrollbar {
- -ms-overflow-style: none;
- scrollbar-width: none;
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+}
+
+
+
+/* ---------------------------------------------------------------------- */
+/* 공통 전환 프리셋 */
+/* ---------------------------------------------------------------------- */
+.animate-smooth {
+ transition-property: all;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 300ms; /* 기본 0.3 초 */
+ transform-origin: center;
+ backface-visibility: hidden; /* GPU 렌더링 활용 */
+}
+
+/* ---------------------------------------------------------------------- */
+/* 1) Hover 시 살짝 확대 */
+/* ---------------------------------------------------------------------- */
+.hover-scale {
+ transform: translateZ(0);
+ transition: transform 300ms cubic-bezier(0.4, 0, 0.2, 1);
+}
+.hover-scale:hover {
+ transform: scale(1.03);
+}
+
+/* 1‑A) 강조 확대 */
+.hover-scale-up {
+ transform: translateZ(0);
+ transition: transform 300ms cubic-bezier(0.4, 0, 0.2, 1);
+}
+.hover-scale-up:hover {
+ transform: scale(1.05);
+}
+
+/* ---------------------------------------------------------------------- */
+/* 2) Active 시 축소 (클릭 피드백) */
+/* ---------------------------------------------------------------------- */
+.press-scale {
+ transform: translateZ(0);
+ transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
+}
+.press-scale:active {
+ transform: scale(0.97);
+}
+
+/* ---------------------------------------------------------------------- */
+/* 3) Hover 시 위로 4 px 리프트 */
+/* ---------------------------------------------------------------------- */
+.hover-up {
+ transform: translateY(0);
+ transition: transform 300ms cubic-bezier(0.4, 0, 0.2, 1);
+}
+.hover-up:hover {
+ transform: translateY(-4px);
+}
+
+/* ---------------------------------------------------------------------- */
+/* 4) Hover 시 투명도 감소 – 페이드 */
+/* ---------------------------------------------------------------------- */
+.hover-fade {
+ opacity: 1;
+ transition: opacity 300ms cubic-bezier(0.4, 0, 0.2, 1);
+}
+.hover-fade:hover {
+ opacity: 0.7;
+}
+
+/* ---------------------------------------------------------------------- */
+/* 5) 종합 효과 – Hover Pop (Scale + Lift + Opacity) */
+/* ---------------------------------------------------------------------- */
+.hover-pop {
+ transform: translateY(0) scale(1);
+ opacity: 1;
+ transition:
+ transform 300ms cubic-bezier(0.4, 0, 0.2, 1),
+ opacity 300ms cubic-bezier(0.4, 0, 0.2, 1);
+}
+.hover-pop:hover {
+ transform: translateY(-4px) scale(1.05);
+ opacity: 0.9; /* dissolve ≈ 90% (원 요청: 약 50% → UX상 90%가 자연스러움) */
+ background-color: var(--color-grayscale-alpha-light-100)
+}
+ .hover-pop:active {
+ background-color: var(--color-grayscale-alpha-dark-100)
+
+ }
+
+/* ---------------------------------------------------------------------- */
+/* 6) 밝기 조절 – Hover & Active */
+/* ---------------------------------------------------------------------- */
+/* 6‑A) Hover 시 밝기를 살짝 높임 */
+.hover-brighten {
+ transition: filter 300ms cubic-bezier(0.4, 0, 0.2, 1);
+ filter: brightness(1);
+}
+.hover-brighten:hover {
+ filter: brightness(1.05);
+}
+
+/* 6‑B) Active 시 밝기를 낮춤 */
+.press-dim {
+ transition: filter 150ms cubic-bezier(0.4, 0, 0.2, 1);
+}
+.press-dim:active {
+ filter: brightness(0.9);
+}
+
+/* ---------------------------------------------------------------------- */
+/* 7) 프리셋 컴포넌트용 조합 클래스 */
+/* ---------------------------------------------------------------------- */
+/* 카드: Pop + Press‑Scale + 밝기 효과 */
+.interactive-card {
+ /* 기본 */
+ transform: translateY(0) scale(1);
+ opacity: 1;
+ filter: brightness(1);
+ transition:
+ transform 300ms cubic-bezier(0.4, 0, 0.2, 1),
+ opacity 300ms cubic-bezier(0.4, 0, 0.2, 1),
+ filter 150ms cubic-bezier(0.4, 0, 0.2, 1);
+}
+.interactive-card:hover {
+ transform: translateY(-4px) scale(1.05);
+ opacity: 0.9;
+ filter: brightness(1.05);
+ cursor: pointer;
+}
+.interactive-card:active {
+ transform: translateY(-2px) scale(0.97); /* 눌렸을 때 살짝 축소 */
+ filter: brightness(0.9);
+}
+
+/* ---------------------------------------------------------------------- */
+/* 8) 버튼 전용 클래스 */
+/* ---------------------------------------------------------------------- */
+.interactive-btn {
+ /* 배경 및 모서리 고정 */
+ border-radius: 12px;
+ /* 기본 transform/filter */
+ transform: translateZ(0) scale(1);
+ filter: brightness(1);
+ /* 트랜지션 통합 */
+ transition:
+ transform 200ms cubic-bezier(0.4, 0, 0.2, 1),
+ filter 150ms cubic-bezier(0.4, 0, 0.2, 1),
+ background-color 200ms ease;
+}
+.interactive-btn:hover {
+ transform: scaleY(1.03);
+ transform: translateY(-2px);
+ filter: brightness(1.05);
+ background-color: var(--color-grayscale-alpha-light-100);
+}
+.interactive-btn:active {
+ background-color: var(--color-grayscale-alpha-dark-100);
+}
+
+/* ---------------------------------------------------------------------- */
+/* 9) 클릭 강조 스타일 */
+/* ---------------------------------------------------------------------- */
+.click-highlight {
+ border: 1px solid var(--color-grayscale-scale-10, #FCFCFC);
+ background: var(--color-grayscale-alpha-light-500, rgba(255, 255, 255, 0.50));
+ border-radius: 20px;
+ /* 그림자가 border-radius를 완벽히 따르도록 inset + round 지정 */
+ clip-path: inset(0 round 30px);
+ /* 기존 그림자 */
+ box-shadow: 0 2px 20px rgba(10,10,101,0.10);
+ transition: box-shadow 200ms ease, transform 200ms ease;
+
+}
+.click-highlight:hover {
+
+ background: var(--color-grayscale-alpha-light-300, rgba(255, 255, 255, 0.30));
+ transform: translateY(-2px);
+
+
+
}
\ No newline at end of file
diff --git a/tailwind.config.js b/tailwind.config.js
index d819162..e8a24e2 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -57,6 +57,15 @@ export default {
'4': 'hsl(var(--chart-4))',
'5': 'hsl(var(--chart-5))',
},
+ "brand-100": "var(--color-100)",
+ "brand-200": "var(--color-200)",
+ "brand-300": "var(--color-300)",
+ "brand-400": "var(--color-400)",
+ "brand-900": "var(--color-900)",
+ "brand-800": "var(--color-800)",
+ "brand-700": "var(--color-700)",
+ "brand-600": "var(--color-600)",
+ "brand-500": "var(--color-500)",
// 🎨 내가 추가한 고정 컬러 팔레트
theme: {
@@ -82,5 +91,5 @@ export default {
},
},
},
- plugins: [require("tailwindcss-animate")],
+// plugins: [require("tailwindcss-animate")],
}
\ No newline at end of file