diff --git a/package.json b/package.json
index ea1967b..b8c05b2 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,7 @@
"lint": "eslint"
},
"dependencies": {
+ "@emotion/cache": "^11.14.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@tanstack/react-query": "^5.90.12",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b6b5a5e..1827de9 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -8,6 +8,9 @@ importers:
.:
dependencies:
+ '@emotion/cache':
+ specifier: ^11.14.0
+ version: 11.14.0
'@emotion/react':
specifier: ^11.14.0
version: 11.14.0(@types/react@19.2.7)(react@19.2.0)
diff --git a/public/icons/sidebar/chat.svg b/public/icons/sidebar/chat.svg
new file mode 100644
index 0000000..d0e4046
--- /dev/null
+++ b/public/icons/sidebar/chat.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/app/agent/page.tsx b/src/app/agent/page.tsx
new file mode 100644
index 0000000..f8e73ee
--- /dev/null
+++ b/src/app/agent/page.tsx
@@ -0,0 +1,79 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import Image from 'next/image';
+import * as S from './style';
+import ChatSection from '@/components/ui/Agent/ChatSection/ui';
+import SearchSection from '@/components/ui/Agent/SearchSection/ui';
+
+// 현재 특별한 백엔드 개발 진행이 없어 멋대로 고정된 입출력을 사용함
+const USER_MESSAGE = "최애의 사인 프로젝트의 CPU 사용량을 알고 싶어";
+const AI_MESSAGE = "좋습니다, 다음은 최애의 사인 프로젝트의 CPU 사용량 입니다.";
+
+export default function AgentPage() {
+ const [isChatStarted, setIsChatStarted] = useState(false);
+ const [streamedText, setStreamedText] = useState("");
+ const [inputValue, setInputValue] = useState("");
+
+ useEffect(() => {
+ if (isChatStarted) {
+ let currentIndex = 0;
+ const interval = setInterval(() => {
+ if (currentIndex <= AI_MESSAGE.length) {
+ setStreamedText(AI_MESSAGE.slice(0, currentIndex));
+ currentIndex++;
+ } else {
+ clearInterval(interval);
+ }
+ }, 50);
+
+ return () => clearInterval(interval);
+ }
+ }, [isChatStarted]);
+
+ const handleSearch = (e?: React.FormEvent) => {
+ e?.preventDefault();
+ if (!inputValue.trim()) return;
+ if (!isChatStarted) {
+ setIsChatStarted(true);
+ }
+ setInputValue("");
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter' && !e.nativeEvent.isComposing) {
+ handleSearch();
+ }
+ };
+
+ return (
+
+ {!isChatStarted && (
+
+
+ M-ADP
+
+ )}
+
+ {isChatStarted && (
+
+ )}
+
+
+
+ );
+}
diff --git a/src/app/agent/style.ts b/src/app/agent/style.ts
new file mode 100644
index 0000000..09d5562
--- /dev/null
+++ b/src/app/agent/style.ts
@@ -0,0 +1,32 @@
+import styled from '@emotion/styled';
+import { colors } from '@/styles/colors';
+
+export const Container = styled.div<{ isChat?: boolean }>`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: ${({ isChat }) => (isChat ? 'space-between' : 'center')};
+ width: 100%;
+ height: 100%;
+ background-color: #ffffff;
+ padding: ${({ isChat }) => (isChat ? '40px 0' : '0')};
+ box-sizing: border-box;
+`;
+
+export const LogoSection = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 28px;
+ margin-bottom: 60px;
+`;
+
+export const LogoTitle = styled.span`
+ font-family: 'IBM Plex Sans KR', sans-serif;
+ font-weight: 700;
+ font-size: 80px;
+ line-height: normal;
+ color: ${colors.black[300]};
+ margin: 0;
+`;
+
+// Moved to components/ui/Agent/...
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index b783888..595c4a0 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,5 +1,6 @@
import type { Metadata } from "next";
import Sidebar from "@/components/ui/Sidebar/ui";
+import EmotionRegistry from './registry';
export const metadata: Metadata = {
title: "M-ADP",
@@ -14,10 +15,12 @@ export default function RootLayout({
return (
-
-
- {children}
-
+
+
+
+ {children}
+
+
);
diff --git a/src/app/registry.tsx b/src/app/registry.tsx
new file mode 100644
index 0000000..6c5c15d
--- /dev/null
+++ b/src/app/registry.tsx
@@ -0,0 +1,27 @@
+'use client';
+
+import React, { useState } from 'react';
+import { useServerInsertedHTML } from 'next/navigation';
+import { CacheProvider } from '@emotion/react';
+import createCache from '@emotion/cache';
+
+export default function EmotionRegistry({ children }: { children: React.ReactNode }) {
+ const [cache] = useState(() => {
+ const cache = createCache({ key: 'css' });
+ cache.compat = true;
+ return cache;
+ });
+
+ useServerInsertedHTML(() => {
+ return (
+
+ );
+ });
+
+ return {children};
+}
diff --git a/src/components/ui/Agent/ChatSection/style.ts b/src/components/ui/Agent/ChatSection/style.ts
new file mode 100644
index 0000000..7451c4b
--- /dev/null
+++ b/src/components/ui/Agent/ChatSection/style.ts
@@ -0,0 +1,72 @@
+import styled from '@emotion/styled';
+import { colors } from '@/styles/colors';
+import { typography } from '@/styles/typography';
+
+export const ChatArea = styled.div`
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ max-width: 1240px;
+ height: 100%;
+ overflow-y: auto;
+ padding: 20px;
+ gap: 20px;
+ flex: 1;
+`;
+
+export const MessageRow = styled.div`
+ display: flex;
+ gap: 20px;
+ align-items: flex-start;
+ width: 100%;
+ justify-content: center;
+`;
+
+export const Avatar = styled.div<{ color?: string }>`
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ background-color: ${({ color }) => color || '#e2e8f0'};
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+ font-family: ${typography.text16Regular.fontFamily};
+ font-size: ${typography.text16Regular.fontSize};
+ font-weight: ${typography.text16Regular.fontWeight};
+ line-height: ${typography.text16Regular.lineHeight};
+ color: #ffffff;
+`;
+
+export const UserMessageCard = styled.div`
+ border: 1px solid #e2e8f0;
+ border-radius: 14px;
+ padding: 15px 21px;
+ width: 100%;
+ max-width: 800px;
+ box-sizing: border-box;
+ background-color: #ffffff;
+ color: ${colors.primary.default};
+ font-family: ${typography.text18Regular.fontFamily};
+ font-size: ${typography.text18Regular.fontSize};
+ font-weight: ${typography.text18Regular.fontWeight};
+ line-height: ${typography.text18Regular.lineHeight};
+`;
+
+export const AIMessageCard = styled.div`
+ background: #ffffff;
+ border-radius: 14px;
+ padding: 22px;
+ width: 100%;
+ max-width: 800px;
+ box-sizing: border-box;
+ box-shadow: 14px 27px 45px 4px rgba(112, 144, 176, 0.2);
+ color: #1b2559;
+ font-family: ${typography.text18Regular.fontFamily};
+ font-size: ${typography.text18Regular.fontSize};
+ font-weight: ${typography.text18Regular.fontWeight};
+ line-height: ${typography.text18Regular.lineHeight};
+ white-space: pre-wrap;
+ min-height: 100px;
+`;
diff --git a/src/components/ui/Agent/ChatSection/ui.tsx b/src/components/ui/Agent/ChatSection/ui.tsx
new file mode 100644
index 0000000..d52993a
--- /dev/null
+++ b/src/components/ui/Agent/ChatSection/ui.tsx
@@ -0,0 +1,37 @@
+import Image from 'next/image';
+import { colors } from '@/styles/colors';
+import * as S from './style';
+
+interface ChatSectionProps {
+ userMessage: string;
+ streamedText: string;
+}
+
+export default function ChatSection({ userMessage, streamedText }: ChatSectionProps) {
+ return (
+
+
+
+ N
+
+
+ {userMessage}
+
+
+
+
+
+
+
+
+ {streamedText}
+
+
+
+ );
+}
diff --git a/src/components/ui/Agent/SearchSection/style.ts b/src/components/ui/Agent/SearchSection/style.ts
new file mode 100644
index 0000000..3adb99c
--- /dev/null
+++ b/src/components/ui/Agent/SearchSection/style.ts
@@ -0,0 +1,64 @@
+import styled from '@emotion/styled';
+import { colors } from '@/styles/colors';
+import { typography } from '@/styles/typography';
+
+export const SearchContainer = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ max-width: 800px;
+ height: 52px;
+ padding: 0 20px;
+ background: #ffffff;
+ border: 1px solid ${colors.black[50]};
+ border-radius: 26px;
+ box-sizing: border-box;
+ transition: border-color 0.2s ease;
+ margin-bottom: 20px;
+
+ &:focus-within {
+ border-color: ${colors.primary.default};
+ }
+`;
+
+export const InputWrapper = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ flex: 1;
+`;
+
+export const IconCircle = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 28px;
+ height: 28px;
+ border-radius: 50%;
+ overflow: hidden;
+ flex-shrink: 0;
+
+ &.send-button {
+ background: ${colors.primary.default};
+ cursor: pointer;
+ border: 1px solid ${colors.primary.default};
+ }
+`;
+
+export const SearchInput = styled.input`
+ border: none;
+ background: transparent;
+ width: 100%;
+ padding: 0;
+ font-family: ${typography.text16Medium.fontFamily};
+ font-size: ${typography.text16Medium.fontSize};
+ font-weight: ${typography.text16Medium.fontWeight};
+ line-height: ${typography.text16Medium.lineHeight};
+ color: ${colors.black[75]};
+ outline: none;
+
+ &::placeholder {
+ color: ${colors.black[75]};
+ }
+`;
diff --git a/src/components/ui/Agent/SearchSection/ui.tsx b/src/components/ui/Agent/SearchSection/ui.tsx
new file mode 100644
index 0000000..d6660d6
--- /dev/null
+++ b/src/components/ui/Agent/SearchSection/ui.tsx
@@ -0,0 +1,34 @@
+import * as S from './style';
+
+interface SearchSectionProps {
+ inputValue: string;
+ setInputValue: (value: string) => void;
+ handleSearch: (e?: React.FormEvent) => void;
+ handleKeyDown: (e: React.KeyboardEvent) => void;
+}
+
+export default function SearchSection({ inputValue, setInputValue, handleSearch, handleKeyDown }: SearchSectionProps) {
+ return (
+
+
+
+
+
+ setInputValue(e.target.value)}
+ onKeyDown={handleKeyDown}
+ />
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/ui/Sidebar/ui.tsx b/src/components/ui/Sidebar/ui.tsx
index 17a5b88..fbb29f9 100644
--- a/src/components/ui/Sidebar/ui.tsx
+++ b/src/components/ui/Sidebar/ui.tsx
@@ -18,6 +18,7 @@ const PRIMARY_NAV = [
],
},
{ key: 'report', label: '분석', icon: '/icons/sidebar/analytics.svg', path: '/report' },
+ { key: 'agent', label: 'ChatOps', icon: '/icons/sidebar/chat.svg', path: '/agent' },
];
const SECONDARY_NAV = [