Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions public/icons/sidebar/chat.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
79 changes: 79 additions & 0 deletions src/app/agent/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<S.Container isChat={isChatStarted}>
{!isChatStarted && (
<S.LogoSection>
<Image
src="/assets/logo.svg"
alt="M-ADP Logo"
width={100}
height={92}
priority
/>
<S.LogoTitle>M-ADP</S.LogoTitle>
</S.LogoSection>
)}

{isChatStarted && (
<ChatSection
userMessage={USER_MESSAGE}
streamedText={streamedText}
/>
)}

<SearchSection
inputValue={inputValue}
setInputValue={setInputValue}
handleSearch={handleSearch}
handleKeyDown={handleKeyDown}
/>
</S.Container>
);
}
32 changes: 32 additions & 0 deletions src/app/agent/style.ts
Original file line number Diff line number Diff line change
@@ -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;
`;
Comment on lines +4 to +14
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's check the current state of style.ts
cat -n src/app/agent/style.ts

Repository: M-ADP/M-ADP-CLIENT

Length of output: 1065


🏁 Script executed:

# Check how Container is used in page.tsx
rg -n 'isChat' --type=ts --type=tsx -C 3

Repository: M-ADP/M-ADP-CLIENT

Length of output: 89


🏁 Script executed:

# Also check for any imports or usage of Container component
rg -n 'Container' src/app/agent/ --type=ts --type=tsx -C 2

Repository: M-ADP/M-ADP-CLIENT

Length of output: 89


🏁 Script executed:

# Search for isChat usage more broadly
rg 'isChat' --type-add 'typescripts:*.ts' --type-add 'typescripts:*.tsx'

Repository: M-ADP/M-ADP-CLIENT

Length of output: 713


🏁 Script executed:

# Look at the agent page.tsx file directly
find . -name 'page.tsx' -path '*/agent/*' -type f

Repository: M-ADP/M-ADP-CLIENT

Length of output: 86


🏁 Script executed:

# Search for Container usage in agent directory
rg 'Container' src/app/agent/ -A 2 -B 2

Repository: M-ADP/M-ADP-CLIENT

Length of output: 734


Use transient prop syntax for isChat to prevent React warnings about unknown DOM attributes.

Emotion's styled passes non-transient props to underlying DOM elements. Since isChat is not a valid HTML attribute, prefix it with $ to mark it as transient (consumed only by the styled component, not forwarded to the DOM).

Required changes
-export const Container = styled.div<{ isChat?: boolean }>`
+export const Container = styled.div<{ $isChat?: boolean }>`
   display: flex;
   flex-direction: column;
   align-items: center;
-  justify-content: ${({ isChat }) => (isChat ? 'space-between' : 'center')};
+  justify-content: ${({ $isChat }) => ($isChat ? 'space-between' : 'center')};
   width: 100%;
   height: 100%;
   background-color: `#ffffff`;
-  padding: ${({ isChat }) => (isChat ? '40px 0' : '0')};
+  padding: ${({ $isChat }) => ($isChat ? '40px 0' : '0')};
   box-sizing: border-box;
 `;

Also update src/app/agent/page.tsx to pass $isChat instead of isChat:

-<S.Container isChat={isChatStarted}>
+<S.Container $isChat={isChatStarted}>
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 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;
`;
πŸ€– Prompt for AI Agents
In `@src/app/agent/style.ts` around lines 4 - 14, Change the styled Container to
use a transient prop so the boolean does not get forwarded to the DOM: update
the generic prop name from isChat to $isChat (e.g., Container = styled.div<{
$isChat?: boolean }>`...`) and update the template usages to reference ${({
$isChat }) => ...} for justify-content and padding; then update the usage in
src/app/agent/page.tsx to pass $isChat instead of isChat when rendering
<Container>.


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/...
11 changes: 7 additions & 4 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -14,10 +15,12 @@ export default function RootLayout({
return (
<html lang="en">
<body style={{ margin: 0, display: 'flex' }}>
<Sidebar />
<main style={{ flex: 1, minWidth: 0 }}>
{children}
</main>
<EmotionRegistry>
<Sidebar />
<main style={{ flex: 1, minWidth: 0 }}>
{children}
</main>
</EmotionRegistry>
</body>
</html>
);
Expand Down
27 changes: 27 additions & 0 deletions src/app/registry.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<style
data-emotion={`${cache.key} ${Object.keys(cache.inserted).join(' ')}`}
dangerouslySetInnerHTML={{
__html: Object.values(cache.inserted).join(' '),
}}
/>
);
});
Comment on lines +15 to +24
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Styles accumulate and get re-injected on every useServerInsertedHTML call β€” flush after extraction.

cache.inserted is never cleared after extracting styles, so previously inserted CSS rules are re-emitted in subsequent calls. The standard pattern clears the entries after extraction to avoid duplicate <style> tags.

Proposed fix β€” flush inserted cache entries
   useServerInsertedHTML(() => {
+    const entries = { ...cache.inserted };
+    // Flush inserted entries to avoid re-injecting on the next call
+    Object.keys(entries).forEach((key) => {
+      delete cache.inserted[key];
+    });
+    if (Object.keys(entries).length === 0) {
+      return null;
+    }
     return (
       <style
-        data-emotion={`${cache.key} ${Object.keys(cache.inserted).join(' ')}`}
+        data-emotion={`${cache.key} ${Object.keys(entries).join(' ')}`}
         dangerouslySetInnerHTML={{
-          __html: Object.values(cache.inserted).join(' '),
+          __html: Object.values(entries).join(' '),
         }}
       />
     );
   });
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useServerInsertedHTML(() => {
return (
<style
data-emotion={`${cache.key} ${Object.keys(cache.inserted).join(' ')}`}
dangerouslySetInnerHTML={{
__html: Object.values(cache.inserted).join(' '),
}}
/>
);
});
useServerInsertedHTML(() => {
const entries = { ...cache.inserted };
// Flush inserted entries to avoid re-injecting on the next call
Object.keys(entries).forEach((key) => {
delete cache.inserted[key];
});
if (Object.keys(entries).length === 0) {
return null;
}
return (
<style
data-emotion={`${cache.key} ${Object.keys(entries).join(' ')}`}
dangerouslySetInnerHTML={{
__html: Object.values(entries).join(' '),
}}
/>
);
});
🧰 Tools
πŸͺ› ast-grep (0.40.5)

[warning] 18-18: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

πŸͺ› Biome (2.3.14)

[error] 19-19: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

πŸ€– Prompt for AI Agents
In `@src/app/registry.tsx` around lines 15 - 24, The current useServerInsertedHTML
block keeps re-emitting previously extracted CSS because cache.inserted isn't
cleared; after you build the style string from
Object.values(cache.inserted).join(' ') (and use cache.key for data-emotion),
flush the inserted entries so they aren't re-injected on subsequent calls (e.g.,
clear or delete entries on cache.inserted after extraction). Update the
useServerInsertedHTML callback that references cache.key and cache.inserted to
extract the styles, then purge cache.inserted entries immediately after creating
the __html content.


return <CacheProvider value={cache}>{children}</CacheProvider>;
}
72 changes: 72 additions & 0 deletions src/components/ui/Agent/ChatSection/style.ts
Original file line number Diff line number Diff line change
@@ -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;
`;
37 changes: 37 additions & 0 deletions src/components/ui/Agent/ChatSection/ui.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<S.ChatArea>
<S.MessageRow>
<S.Avatar color={colors.primary.default}>
N
</S.Avatar>
<S.UserMessageCard>
{userMessage}
</S.UserMessageCard>
</S.MessageRow>

<S.MessageRow>
<S.Avatar>
<Image
src="/assets/logo.svg"
alt="AI Avatar"
width={24}
height={24}
/>
</S.Avatar>
<S.AIMessageCard>
{streamedText}
</S.AIMessageCard>
</S.MessageRow>
</S.ChatArea>
);
}
64 changes: 64 additions & 0 deletions src/components/ui/Agent/SearchSection/style.ts
Original file line number Diff line number Diff line change
@@ -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]};
}
`;
34 changes: 34 additions & 0 deletions src/components/ui/Agent/SearchSection/ui.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<S.SearchContainer>
<S.InputWrapper>
<S.IconCircle>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 4L14.4 9.6L20 12L14.4 14.4L12 20L9.6 14.4L4 12L9.6 9.6L12 4Z" fill="#969696" />
</svg>
</S.IconCircle>
<S.SearchInput
placeholder="무엇을 μ›ν•˜μ‹œλ‚˜μš”?"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
/>
</S.InputWrapper>

<S.IconCircle className="send-button" onClick={handleSearch}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 19V5M12 5L5 12M12 5L19 12" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</S.IconCircle>
Comment on lines +27 to +31
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Send button div is not keyboard-accessible.

The send button is a div with only an onClick handler β€” it's not focusable and cannot be activated via keyboard. Use a <button> (or at minimum add role="button", tabIndex={0}, and onKeyDown for Enter/Space). This is an accessibility gap that prevents keyboard-only users from submitting.

Proposed fix β€” use a styled button instead

In style.ts, change IconCircle to support rendering as a button:

-      <S.IconCircle className="send-button" onClick={handleSearch}>
+      <S.IconCircle as="button" className="send-button" type="button" onClick={handleSearch} aria-label="Send">
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<S.IconCircle className="send-button" onClick={handleSearch}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 19V5M12 5L5 12M12 5L19 12" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</S.IconCircle>
<S.IconCircle as="button" className="send-button" type="button" onClick={handleSearch} aria-label="Send">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 19V5M12 5L5 12M12 5L19 12" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</S.IconCircle>
πŸ€– Prompt for AI Agents
In `@src/components/ui/Agent/SearchSection/ui.tsx` around lines 27 - 31, The send
control uses S.IconCircle as a non-focusable div with only onClick
(handleSearch) β€” replace it with an actual button element (or update
S.IconCircle in style.ts to render as a <button> / support as="button") and
ensure it has type="button" and onClick={handleSearch}; if you cannot convert it
to a real button, add role="button", tabIndex={0} and an onKeyDown handler that
triggers handleSearch on Enter/Space to make S.IconCircle keyboard-accessible.
Ensure the change references the existing S.IconCircle component and the
handleSearch handler so keyboard users can activate the send action.

</S.SearchContainer>
);
}
Loading