Skip to content

Commit

Permalink
Merge pull request #22 from MARU-EGG/USER-type-store
Browse files Browse the repository at this point in the history
[USER] Feat: type store
  • Loading branch information
swgvenghy authored Aug 4, 2024
2 parents 43275f9 + dd8a66d commit b71b06f
Show file tree
Hide file tree
Showing 13 changed files with 261 additions and 55 deletions.
38 changes: 37 additions & 1 deletion package-lock.json

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

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"swr": "^2.2.5",
"tailwind-merge": "^2.3.0",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
"web-vitals": "^2.1.4",
"zustand": "^4.5.4"
},
"scripts": {
"start": "react-scripts start",
Expand Down
2 changes: 1 addition & 1 deletion src/api/post-question.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { server_axiosInstance } from '../utils/axios';

export async function postQuestion(category: string, type: string, content: string): Promise<any> {
export async function postQuestion(category: string | undefined, type: string, content: string): Promise<any> {
const data = {
type,
category,
Expand Down
2 changes: 1 addition & 1 deletion src/routes/admin-routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import { Route, Routes } from 'react-router-dom';
import PrivateRoute from './private-route';
import FileList from '../ui/pages/admin/file-list';
import AdminLayout from '../ui/components/admin/Layout/admin-layout';
import AdminLayout from '../ui/components/molecule/admin/Layout/admin-layout';
import Login from '../ui/pages/admin/login';

const AdminRoutes: React.FC = () => {
Expand Down
23 changes: 23 additions & 0 deletions src/store/chat-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { create } from 'zustand';

interface ChatState {
messages: { content: string; role: 'user' | 'system' }[];
loading: boolean;
addMessage: (message: { content: string; role: 'user' | 'system' }) => void;
setLoading: (loading: boolean) => void;
updateLastMessage: (content: string) => void;
}

const useChatStore = create<ChatState>((set) => ({
messages: [],
loading: false,
addMessage: (message) => set((state) => ({ messages: [...state.messages, message] })),
setLoading: (loading) => set({ loading }),
updateLastMessage: (content) =>
set((state) => {
const updatedMessages = [...state.messages];
updatedMessages[updatedMessages.length - 1].content = content;
return { messages: updatedMessages };
}),
}));
export default useChatStore;
13 changes: 13 additions & 0 deletions src/store/type-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { create } from 'zustand';

interface TypeState {
type: null | 'SUSI' | 'PYEONIP' | 'JEONGSI';
setSelectedType: (button: TypeState['type']) => void;
}

const useTypeStore = create<TypeState>()((set) => ({
type: null,
setSelectedType: (button) => set({ type: button }),
}));

export default useTypeStore;
28 changes: 15 additions & 13 deletions src/ui/components/atom/chat-card/chat-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,30 @@ import React from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { cn } from '../../../../utils/style';

import maru from '../../../../assets/maru-egg.png';
interface ChatCardProps {
content: string;
role: 'user' | 'system';
}

const ChatCard = ({ content, role }: ChatCardProps) => {
return (
<div
className={cn('flex h-auto flex-col-reverse', {
'items-end justify-end': role === 'user',
'items-start justify-start': role !== 'user',
})}
>
<div>
{role === 'user' ? null : <img src={maru} className="mb-2 h-8 w-8" />}
<div
className={cn('flex w-auto max-w-72 rounded-md bg-white px-5 py-3 text-black', {
'justify-end bg-primary-blue text-white': role === 'user',
'justify-start text-left': role !== 'user',
className={cn('flex h-auto flex-col-reverse py-3', {
'items-end justify-end': role === 'user',
'items-start justify-start': role !== 'user',
})}
>
<div className="px-4 py-3 text-body1">
<ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>
<div
className={cn('flex w-auto rounded-md bg-white px-5 py-3 text-black', {
'justify-end bg-primary-blue text-white': role === 'user',
'justify-start text-left': role !== 'user',
})}
>
<div className="px-4 py-3 text-body1">
<ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>
</div>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ const meta = {
layout: 'centered',
},
args: {
type: 'question',
category: 'general',
type: 'SUSI',
},
} satisfies Meta<typeof ChatForm>;

Expand All @@ -20,7 +19,7 @@ type Story = StoryObj<typeof meta>;

export const Default: Story = {
args: {
type: 'question',
category: 'general',
type: 'SUSI',
category: 'PAST_QUESTIONS',
},
};
21 changes: 12 additions & 9 deletions src/ui/components/molecule/user/chat-form/chat-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import IconButton from '../../../atom/icon/icon-button';
import { ReactComponent as SendIcon } from '../../../../../assets/Send.svg';
import { postQuestion } from '../../../../../api/post-question';
import { cn } from '../../../../../utils/style';
import useChatStore from '../../../../../store/chat-store';

interface ChatFormProps {
type: string;
category: string;
type: 'SUSI' | 'PYEONIP' | 'JEONGSI';
category?: 'PAST_QUESTIONS' | 'INTERVIEW_PRACTICAL_TEST' | 'PASSING_RESULT' | 'ADMISSION_GUIDELINE';
}

const ChatForm = ({ type, category }: ChatFormProps) => {
Expand All @@ -22,20 +23,22 @@ const ChatForm = ({ type, category }: ChatFormProps) => {
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
try {
const response = await postQuestion(category, type, content);
console.log('전송 성공:', response);
useChatStore.getState().addMessage({ content, role: 'user' });
useChatStore.getState().addMessage({ content: '답변을 생성중입니다...', role: 'system' });
useChatStore.getState().setLoading(true);
setContent('');
const response = await postQuestion(category, type, content);
useChatStore.getState().updateLastMessage(response.answer.content);
useChatStore.getState().setLoading(false);
setDisabled(true);
} catch (error) {
console.error('전송 실패:', error);
useChatStore.getState().setLoading(false);
useChatStore.getState().updateLastMessage('답변 생성에 실패했습니다. 새로고침해주세요');
}
};

return (
<form
className={cn('flex w-96 flex-nowrap rounded-2xl px-4 py-2', 'bg-background-default')}
onSubmit={handleSubmit}
>
<form className={cn('flex flex-nowrap rounded-2xl px-4 py-2', 'bg-background-default')} onSubmit={handleSubmit}>
<TextInput value={content} onValueChange={handleChange} placeholder="메시지를 입력해주세요." />
<IconButton type="submit" disabled={disabled}>
<SendIcon />
Expand Down
27 changes: 15 additions & 12 deletions src/ui/components/molecule/user/header/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@ import maruEgg from '../../../../../assets/maru-egg.png';
import IconButton from '../../../atom/icon/icon-button';

interface HeaderProps {
type: string;
type: null | 'SUSI' | 'PYEONIP' | 'JEONGSI';
}

const Header = ({ type }: HeaderProps) => {
const navigate = useNavigate();
const [menuOpen, setMenuOpen] = useState(false);

const handleRefreshClick = () => {
navigate('/');
window.location.reload();
console.log('새로고침');
};

Expand All @@ -23,21 +22,25 @@ const Header = ({ type }: HeaderProps) => {
};

return (
<div className="flex items-center justify-between px-3 py-2">
<div className="fixed flex w-full items-center justify-between bg-white px-3 py-2">
<IconButton onClick={handleRefreshClick}>
<RefreshIcon />
</IconButton>
<div className="flex items-center">
<img className="mr-2 h-8 w-8" src={maruEgg} alt="마루에그 캐릭터" />
<div className="mr-8 text-title text-primary-blue">명지대학교 입학처 챗봇</div>
<ul role="list" className="flex list-disc items-center marker:text-primary-blue">
<li className="pl-0">
<div className="flex text-body2">
<p className="text-primary-blue">{type}&nbsp;</p>
<p>질문중</p>
</div>
</li>
</ul>
{type && (
<ul role="list" className="flex list-disc items-center marker:text-primary-blue">
<li className="pl-0">
<div className="flex text-body2">
<p className="text-primary-blue">
{type === 'SUSI' ? '수시' : type === 'JEONGSI' ? '정시' : '편입'}&nbsp;
</p>
<p>질문중</p>
</div>
</li>
</ul>
)}
</div>
<IconButton onClick={handleMenuClick}>
<MenuIcon />
Expand Down
10 changes: 4 additions & 6 deletions src/ui/pages/admin/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export default function Login() {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');

try {
await handleLogin(email, password);
} catch (err: any) {
Expand All @@ -28,7 +27,6 @@ export default function Login() {
Sign in to your account
</h2>
</div>

<div className="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
<form className="space-y-6" onSubmit={handleSubmit}>
<div>
Expand All @@ -44,7 +42,7 @@ export default function Login() {
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className="block w-full rounded-md border-0 py-1.5 px-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-lg sm:leading-6"
className="block w-full rounded-md border-0 px-2 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-lg sm:leading-6"
/>
</div>
</div>
Expand All @@ -64,7 +62,7 @@ export default function Login() {
required
value={password}
onChange={(e) => setPassword(e.target.value)}
className="block w-full rounded-md border-0 py-1.5 px-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-lg sm:leading-6"
className="block w-full rounded-md border-0 px-2 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-lg sm:leading-6"
/>
</div>
</div>
Expand All @@ -75,9 +73,9 @@ export default function Login() {
className="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-lg font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
>
{isLoading ? (
'Sign in'
) : (
<Spin indicator={<LoadingOutlined style={{ color: 'white' }} spin />} size="large" />
) : (
'Sign in'
)}
</button>
</div>
Expand Down
47 changes: 43 additions & 4 deletions src/ui/pages/maru-egg.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,51 @@
import React from 'react';

import Header from '../components/molecule/user/header/header';
import useTypeStore from '../../store/type-store';
import ChatCard from '../components/atom/chat-card/chat-card';
import ChatForm from '../components/molecule/user/chat-form/chat-form';
import useChatStore from '../../store/chat-store';

const MaruEgg: React.FC = () => {
const { setSelectedType, type } = useTypeStore();
const { messages } = useChatStore();
const handleButtonClick = (selectedType: 'SUSI' | 'PYEONIP' | 'JEONGSI') => {
setSelectedType(selectedType);
};

return (
<div className="min-w-[360px]">
<div className="bg-background-default"></div>
<Header type="수시" />
<div className="h-full min-w-[360px] bg-background-default">
<Header type={type} />

<div className="w-full px-4 pb-24 pt-16">
<ChatCard
content={`안녕하세요 입학처 챗봇 MARU-EGG입니다!
궁금하신 내용 안내 도와드리겠습니다.
알아보고 싶은 전형을 선택해주세요!`}
role="system"
/>
<button onClick={() => handleButtonClick('SUSI')}>Select SUSI</button>
<button onClick={() => handleButtonClick('PYEONIP')}>Select PYEONIP</button>
<button onClick={() => handleButtonClick('JEONGSI')}>Select JEONGSI</button>
{type !== null && (
<ChatCard role="user" content={type === 'SUSI' ? '수시' : type === 'JEONGSI' ? '정시' : '편입'} />
)}
{type !== null && (
<ChatCard
role="system"
content={`안녕하세요 입학처 챗봇 MARU-EGG입니다!
궁금하신 내용 안내 도와드리겠습니다.
`}
/>
)}
{messages.map((msg, index) => {
return <ChatCard key={index} content={msg.content} role={msg.role} />;
})}
</div>
{type !== null && (
<div className="fixed bottom-0 w-full bg-white px-4 py-3">
<ChatForm type={type} />
</div>
)}
</div>
);
};
Expand Down
Loading

0 comments on commit b71b06f

Please sign in to comment.