+ {messages.map((message, index) => {
+ const key =
+ message.type === 'NoticeMessage'
+ ? message.messageId
+ : `${message.messageId}-${message.filteredLevel}`;
+
+ return (
+
+ );
+ })}
+
+ );
+};
+
+export default Messages;
diff --git a/src/features/service-menu/config.ts b/src/features/service-menu/config.ts
index d9417218..68e905f8 100644
--- a/src/features/service-menu/config.ts
+++ b/src/features/service-menu/config.ts
@@ -1,5 +1,6 @@
+import { IdCard, Mailbox, MessageSquareText } from 'lucide-react';
+
import { ROUTE } from '@/shared/config';
-import { MailBoxIcon, MessagesIcon, ProfileCardIcon } from '@/shared/ui';
interface Menu {
key: string;
@@ -13,18 +14,18 @@ export const SERVICE_MENU: Menu[] = [
key: 'chat',
name: '채팅',
path: ROUTE.chat,
- icon: MessagesIcon,
+ icon: MessageSquareText,
},
{
key: 'channel',
name: '채널',
path: ROUTE.channel,
- icon: MailBoxIcon,
+ icon: Mailbox,
},
{
key: 'profile',
name: '프로필',
path: ROUTE.profile,
- icon: ProfileCardIcon,
+ icon: IdCard,
},
];
diff --git a/src/features/service-menu/ui/menu.tsx b/src/features/service-menu/ui/menu.tsx
index 0c812b75..1f7e2119 100644
--- a/src/features/service-menu/ui/menu.tsx
+++ b/src/features/service-menu/ui/menu.tsx
@@ -2,36 +2,37 @@
import Link from 'next/link';
import { usePathname } from 'next/navigation';
-import { useState } from 'react';
import { tv } from 'tailwind-variants';
import { SERVICE_MENU } from '../config';
const wrapper = tv({
base: [
- 'flex w-full max-md:absolute max-md:bottom-0 max-md:justify-around',
+ // mobile
+ 'z-10 flex w-full bg-background max-md:fixed max-md:bottom-0 max-md:justify-around',
+ // desktop
'md:h-full md:w-fit md:flex-col md:gap-2 md:bg-default-900 md:pt-2',
],
});
const link = tv({
base: [
- 'h-service-menu flex flex-col items-center justify-center gap-1 px-3 text-xs text-default-500',
- 'hover:text-default-800 md:hover:text-default-200',
- 'md:text-sm',
+ 'mx-1 flex size-service-menu flex-col items-center justify-center gap-1 rounded-md text-xs text-default-400',
+ 'md:hover:bg-default-800',
+ 'md:text-sm md:text-default-500',
],
variants: {
selected: {
true: [
- 'font-semibold text-default-800',
- 'md:font-medium md:text-default-200',
+ 'cursor-default font-semibold text-default-800',
+ 'md:font-medium md:text-default-100',
],
},
},
});
const Menu = () => {
- const [pathname, setPathname] = useState(usePathname()?.split('/')[2]);
+ const pathname = usePathname()?.split('/')[2];
return (
@@ -40,7 +41,6 @@ const Menu = () => {
key={item.key}
href={item.path}
className={link({ selected: item.key === pathname })}
- onClick={() => setPathname(item.key)}
>
{item.name}
diff --git a/src/pages/channel/config.ts b/src/pages/channel/config.ts
index 2f822c26..6bd573df 100644
--- a/src/pages/channel/config.ts
+++ b/src/pages/channel/config.ts
@@ -1,4 +1,4 @@
-import { type ChipProps } from '@heroui/chip';
+import { type ChipProps } from '@heroui/react';
export const CHANNEL_ACTION_KEY = {
create: 'channel-create',
diff --git a/src/pages/channel/ui/button-channel-add.tsx b/src/pages/channel/ui/button-channel-add.tsx
index c61c0df8..e5e9ca0d 100644
--- a/src/pages/channel/ui/button-channel-add.tsx
+++ b/src/pages/channel/ui/button-channel-add.tsx
@@ -1,13 +1,13 @@
'use client';
-import { Button } from '@heroui/button';
+import { Button } from '@heroui/react';
+import { Plus } from 'lucide-react';
-import { PlusIcon } from '@/shared/ui';
import { useServicePopup } from '@/widgets/service-content';
import { CHANNEL_ACTION_KEY } from '../config';
-import { listItem } from './styles';
import { useChannelFormStore } from '../store/form';
+import { listItem } from './styles';
const ButtonChannelAdd = () => {
const open = useServicePopup((state) => state.open);
@@ -24,7 +24,7 @@ const ButtonChannelAdd = () => {
className={listItem({ class: 'flex-col text-default-500' })}
onPress={onOpen}
>
-
+
채널 추가
);
diff --git a/src/pages/channel/ui/channel/button-link-copy.tsx b/src/pages/channel/ui/channel/button-link-copy.tsx
index 14839cfe..8e4a363c 100644
--- a/src/pages/channel/ui/channel/button-link-copy.tsx
+++ b/src/pages/channel/ui/channel/button-link-copy.tsx
@@ -1,7 +1,9 @@
'use client';
+import { Link2 } from 'lucide-react';
+
import { DOMAIN_NAME, ROUTE } from '@/shared/config';
-import { Button, LinkIcon } from '@/shared/ui';
+import { Button } from '@/shared/ui';
interface Props {
link: string;
@@ -18,7 +20,7 @@ const ButtonLinkCopy = ({ link }: Props) => {
return (
-
+
);
};
diff --git a/src/pages/channel/ui/channel/list-item.tsx b/src/pages/channel/ui/channel/list-item.tsx
index b1a4b108..7b749a3f 100644
--- a/src/pages/channel/ui/channel/list-item.tsx
+++ b/src/pages/channel/ui/channel/list-item.tsx
@@ -1,4 +1,4 @@
-import { Card, CardBody, CardFooter, CardHeader } from '@heroui/card';
+import { Card, CardBody, CardFooter, CardHeader } from '@heroui/react';
import ChipChannelFilter from '@/pages/channel/ui/chip-channel-filter';
import ChipChannelType from '@/pages/channel/ui/chip-channel-type';
diff --git a/src/pages/channel/ui/chip-channel-filter.tsx b/src/pages/channel/ui/chip-channel-filter.tsx
index cee4deab..b7c080ad 100644
--- a/src/pages/channel/ui/chip-channel-filter.tsx
+++ b/src/pages/channel/ui/chip-channel-filter.tsx
@@ -1,4 +1,4 @@
-import { Chip } from '@heroui/chip';
+import { Chip } from '@heroui/react';
import { CHANNEL_FILTER_LEVEL } from '../config';
diff --git a/src/pages/channel/ui/chip-channel-type.tsx b/src/pages/channel/ui/chip-channel-type.tsx
index 0dd3a3ae..95cf174b 100644
--- a/src/pages/channel/ui/chip-channel-type.tsx
+++ b/src/pages/channel/ui/chip-channel-type.tsx
@@ -1,4 +1,4 @@
-import { Chip } from '@heroui/chip';
+import { Chip } from '@heroui/react';
import { CHANNEL_TYPE_NAME } from '../config';
diff --git a/src/pages/channel/ui/form-channel-create.tsx b/src/pages/channel/ui/form-channel-create.tsx
index 53a4a029..416a788d 100644
--- a/src/pages/channel/ui/form-channel-create.tsx
+++ b/src/pages/channel/ui/form-channel-create.tsx
@@ -1,17 +1,17 @@
'use client';
-import { CardBody, CardFooter } from '@heroui/card';
+import { CardBody, CardFooter } from '@heroui/react';
import { Button } from '@/shared/ui';
import { useServicePopup } from '@/widgets/service-content';
import { createChannel } from '../api';
+import { useChannelStore } from '../store/channel';
+import { useChannelFormStore } from '../store/form';
import FormHeader from './form-header';
import InputChannelDescription from './input-channel-description';
import InputChannelFilterLevel from './input-channel-filter-level';
import InputChannelName from './input-channel-name';
-import { useChannelStore } from '../store/channel';
-import { useChannelFormStore } from '../store/form';
const CreateButton = () => {
const isInvalid = useChannelFormStore(
diff --git a/src/pages/channel/ui/form-channel-update.tsx b/src/pages/channel/ui/form-channel-update.tsx
index b2d3ae8c..86563505 100644
--- a/src/pages/channel/ui/form-channel-update.tsx
+++ b/src/pages/channel/ui/form-channel-update.tsx
@@ -1,6 +1,6 @@
'use client';
-import { CardBody, CardFooter } from '@heroui/card';
+import { CardBody, CardFooter } from '@heroui/react';
import { Button } from '@/shared/ui';
import { useServicePopup } from '@/widgets/service-content';
diff --git a/src/pages/channel/ui/form-header.tsx b/src/pages/channel/ui/form-header.tsx
index 5275203e..22f53e04 100644
--- a/src/pages/channel/ui/form-header.tsx
+++ b/src/pages/channel/ui/form-header.tsx
@@ -1,4 +1,4 @@
-import { CardHeader } from '@heroui/card';
+import { CardHeader } from '@heroui/react';
const FormHeader: FC<{
title: string;
diff --git a/src/pages/channel/ui/input-channel-filter-level.tsx b/src/pages/channel/ui/input-channel-filter-level.tsx
index 10a81d7a..73fccada 100644
--- a/src/pages/channel/ui/input-channel-filter-level.tsx
+++ b/src/pages/channel/ui/input-channel-filter-level.tsx
@@ -1,4 +1,4 @@
-import { Slider, type SliderProps } from '@heroui/slider';
+import { Slider, type SliderProps } from '@heroui/react';
import { CHANNEL_FILTER_LEVEL } from '../config';
import { useChannelFormStore } from '../store/form';
diff --git a/src/pages/channel/ui/styles.ts b/src/pages/channel/ui/styles.ts
index 979121ca..effb606d 100644
--- a/src/pages/channel/ui/styles.ts
+++ b/src/pages/channel/ui/styles.ts
@@ -1,4 +1,4 @@
-import { type InputProps } from '@heroui/input';
+import { type InputProps } from '@heroui/react';
import { tv } from 'tailwind-variants';
export const listItem = tv({
diff --git a/src/pages/chat-loading/ui/skeleton-chatting-list-item.tsx b/src/pages/chat-loading/ui/skeleton-chatting-list-item.tsx
index 05762a91..bc2e9794 100644
--- a/src/pages/chat-loading/ui/skeleton-chatting-list-item.tsx
+++ b/src/pages/chat-loading/ui/skeleton-chatting-list-item.tsx
@@ -1,4 +1,4 @@
-import { Skeleton } from '@heroui/skeleton';
+import { Skeleton } from '@heroui/react';
const SkeletonChattingListItem = () => {
return (
diff --git a/src/pages/chat/lib.ts b/src/pages/chat/lib.ts
deleted file mode 100644
index 97f3050e..00000000
--- a/src/pages/chat/lib.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-export const getCounterpart = (
- chattingRoom: ChattingRoom.RawData,
- currentMemberId?: MemberId,
-) => {
- if (chattingRoom.channelType === 'group' || !currentMemberId) {
- return undefined;
- }
- return chattingRoom.userList.find((id) => id !== currentMemberId);
-};
diff --git a/src/pages/chat/ui/chatting/list-item-nickname.tsx b/src/pages/chat/ui/chatting/list-item-nickname.tsx
index bd63dc2a..f1f114a9 100644
--- a/src/pages/chat/ui/chatting/list-item-nickname.tsx
+++ b/src/pages/chat/ui/chatting/list-item-nickname.tsx
@@ -1,6 +1,6 @@
-import { Skeleton } from '@heroui/skeleton';
+import { Skeleton } from '@heroui/react';
-import { useChattingRoomStore } from '../../store/chatting-room';
+import { useChattingRoomStore } from '@/entities/chatting-room';
const Nickname: FC<{
chattingRoom: ChattingRoom;
diff --git a/src/pages/chat/ui/chatting/list-item-preview.tsx b/src/pages/chat/ui/chatting/list-item-preview.tsx
index bc04f964..8a414bab 100644
--- a/src/pages/chat/ui/chatting/list-item-preview.tsx
+++ b/src/pages/chat/ui/chatting/list-item-preview.tsx
@@ -1,8 +1,8 @@
'use client';
-import { Chip } from '@heroui/chip';
+import { Chip } from '@heroui/react';
-import { useChattingRoomStore } from '../../store/chatting-room';
+import { useChattingRoomStore } from '@/entities/chatting-room';
const Preview: FC<{
chattingRoomId: ChattingRoom['roomId'];
diff --git a/src/pages/chat/ui/chatting/list-item.tsx b/src/pages/chat/ui/chatting/list-item.tsx
index 389dc3fe..ad54b367 100644
--- a/src/pages/chat/ui/chatting/list-item.tsx
+++ b/src/pages/chat/ui/chatting/list-item.tsx
@@ -1,6 +1,6 @@
'use client';
-import { Avatar } from '@heroui/avatar';
+import { Avatar } from '@heroui/react';
import { CHATTING_ACTION_KEY } from '@/pages/chat/config';
import { Button } from '@/shared/ui';
@@ -13,7 +13,7 @@ import Preview from './list-item-preview';
const ChattingListItem: FC<{ chatting: ChattingRoom }> = ({ chatting }) => {
const onClick = () => {
useServicePopup.getState().open(CHATTING_ACTION_KEY.enter);
- useChattingStore.getState().setRoomId(chatting.roomId);
+ useChattingStore.getState().setRoom(chatting);
};
return (
diff --git a/src/pages/chat/ui/chatting/list.tsx b/src/pages/chat/ui/chatting/list.tsx
index c38eaf4b..a979777b 100644
--- a/src/pages/chat/ui/chatting/list.tsx
+++ b/src/pages/chat/ui/chatting/list.tsx
@@ -1,9 +1,8 @@
'use client';
+import { useChattingRoomStore } from '@/entities/chatting-room';
import ChattingListItem from '@/pages/chat/ui/chatting/list-item';
-import { useChattingRoomStore } from '../../store/chatting-room';
-
const compareDate = (a: ChattingRoom, b: ChattingRoom) => {
if (a.lastMessage.createdDate > b.lastMessage.createdDate) {
return -1;
diff --git a/src/pages/chat/ui/page.tsx b/src/pages/chat/ui/page.tsx
index 6f2845ea..b2f31f0e 100644
--- a/src/pages/chat/ui/page.tsx
+++ b/src/pages/chat/ui/page.tsx
@@ -1,3 +1,4 @@
+import { ChattingRoomProvider } from '@/entities/chatting-room';
import { WebsocketProvider as WebsocketConnector } from '@/features/websocket';
import { requestChattingMemberMap } from '@/pages/chat/api/member';
import { Chatting } from '@/widgets/chatting';
@@ -5,7 +6,6 @@ import ServiceContent from '@/widgets/service-content';
import { requestChattingList } from '../api/chatting';
import { CHATTING_ACTION_KEY } from '../config';
-import { ChattingRoomProvider } from '../store/chatting-room';
import ChattingList from './chatting/list';
const ChattingPage = async () => {
diff --git a/src/pages/coming-soon/ui/index.tsx b/src/pages/coming-soon/ui/index.tsx
deleted file mode 100644
index 085b86ea..00000000
--- a/src/pages/coming-soon/ui/index.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import Image from 'next/image';
-
-const ComingSoon = () => {
- return (
-
-
-
-
-
-
-
- COMING SOON
-
-
-
- AI 안전 채팅을 통해 당신을 지켜드립니다.
-
-
-
-
- );
-};
-
-export default ComingSoon;
diff --git a/src/pages/guest-chat/ui/page.tsx b/src/pages/guest-chat/ui/page.tsx
index 87b25af1..fe0ea6e3 100644
--- a/src/pages/guest-chat/ui/page.tsx
+++ b/src/pages/guest-chat/ui/page.tsx
@@ -27,7 +27,7 @@ const GuestChattingPage: FC<{
-
+
diff --git a/src/pages/guest/ui/enter-as-guest-footer.tsx b/src/pages/guest/ui/enter-as-guest-footer.tsx
index b0ec801a..8c53a1ac 100644
--- a/src/pages/guest/ui/enter-as-guest-footer.tsx
+++ b/src/pages/guest/ui/enter-as-guest-footer.tsx
@@ -1,4 +1,4 @@
-import { ModalFooter } from '@heroui/modal';
+import { ModalFooter } from '@heroui/react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
@@ -6,7 +6,6 @@ import { signIn } from '@/features/login';
import { ROUTE } from '@/shared/config';
import { useTokenStore } from '@/shared/store';
import { Button } from '@/shared/ui';
-import { useChattingStore } from '@/widgets/chatting';
import { guestSignUp, requestEnterChatting } from '../api';
@@ -39,7 +38,6 @@ const EnterAsGuestFooter: FC<{
return;
}
- useChattingStore.getState().setRoomId(roomId);
router.push(`${ROUTE.guest}/${link}/${roomId}`);
};
diff --git a/src/pages/guest/ui/enter-as-guest.tsx b/src/pages/guest/ui/enter-as-guest.tsx
index 3f12d809..48437861 100644
--- a/src/pages/guest/ui/enter-as-guest.tsx
+++ b/src/pages/guest/ui/enter-as-guest.tsx
@@ -6,7 +6,7 @@ import {
ModalContent,
ModalHeader,
useDisclosure,
-} from '@heroui/modal';
+} from '@heroui/react';
import { Button } from '@/shared/ui';
diff --git a/src/pages/guest/ui/enter-as-member.tsx b/src/pages/guest/ui/enter-as-member.tsx
index a3680df6..f42dc83e 100644
--- a/src/pages/guest/ui/enter-as-member.tsx
+++ b/src/pages/guest/ui/enter-as-member.tsx
@@ -5,7 +5,6 @@ import { useRouter } from 'next/navigation';
import { useMemberStore } from '@/entities/member';
import { ROUTE } from '@/shared/config';
import { Button } from '@/shared/ui';
-import { useChattingStore } from '@/widgets/chatting';
import { requestEnterChatting } from '../api';
@@ -29,7 +28,7 @@ const EnterAsMember: FC<{
alert('채팅방에 입장에 실패했습니다.');
return;
}
- useChattingStore.getState().setRoomId(roomId);
+
router.push(`${ROUTE.guest}/${link}/${roomId}`);
};
diff --git a/src/pages/guest/ui/enter-chatting.tsx b/src/pages/guest/ui/enter-chatting.tsx
index 434ffc0d..9d310e61 100644
--- a/src/pages/guest/ui/enter-chatting.tsx
+++ b/src/pages/guest/ui/enter-chatting.tsx
@@ -1,4 +1,4 @@
-import { CardBody, CardFooter, CardHeader } from '@heroui/card';
+import { CardBody, CardFooter, CardHeader } from '@heroui/react';
import { MessageReceived } from '@/entities/message';
@@ -12,7 +12,13 @@ const EnterChatting: FC<{
{info.title}
-
+
{children}
>
diff --git a/src/pages/landing/api.ts b/src/pages/landing/api.ts
new file mode 100644
index 00000000..3c058a5d
--- /dev/null
+++ b/src/pages/landing/api.ts
@@ -0,0 +1,29 @@
+'use client';
+
+import axios from 'axios';
+
+import { FILTERING_API_BASE_URL, FILTERING_PREDICTION } from '@/shared/config';
+
+import { TUTORIAL_CHAT_FILTER_LEVEL } from './config';
+
+const baseURL =
+ process.env.NODE_ENV === 'development'
+ ? '/filtering'
+ : FILTERING_API_BASE_URL;
+
+const client = axios.create({
+ baseURL,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+});
+
+export const fetchFilteringPrediction = async (text: string) => {
+ return client.post('/predict', { text }).then((res) => {
+ const { prediction } = res.data;
+ console.debug('prediction:', prediction);
+ return prediction === FILTERING_PREDICTION.neutral
+ ? 0
+ : TUTORIAL_CHAT_FILTER_LEVEL;
+ });
+};
diff --git a/src/pages/landing/config.ts b/src/pages/landing/config.ts
new file mode 100644
index 00000000..a7cc35a8
--- /dev/null
+++ b/src/pages/landing/config.ts
@@ -0,0 +1 @@
+export const TUTORIAL_CHAT_FILTER_LEVEL = 100;
diff --git a/src/pages/landing/index.ts b/src/pages/landing/index.ts
new file mode 100644
index 00000000..0c0a8664
--- /dev/null
+++ b/src/pages/landing/index.ts
@@ -0,0 +1 @@
+export { default } from './ui/page';
diff --git a/src/pages/landing/lib/tutorial-chatting.ts b/src/pages/landing/lib/tutorial-chatting.ts
new file mode 100644
index 00000000..baa51d11
--- /dev/null
+++ b/src/pages/landing/lib/tutorial-chatting.ts
@@ -0,0 +1,27 @@
+'use client';
+
+let id = 0;
+
+export const createMessage = (
+ content: string,
+ senderId: string,
+ filteredLevel = 0,
+): Message.Common => {
+ const messageId = id++;
+ const date = new Date();
+ const createdDate = date.toISOString();
+ const time = date.toLocaleTimeString('ko-KR', {
+ hour: '2-digit',
+ minute: '2-digit',
+ });
+
+ return {
+ type: 'Common',
+ content,
+ messageId,
+ createdDate,
+ time,
+ senderId,
+ filteredLevel,
+ };
+};
diff --git a/src/pages/landing/store/assist-data.ts b/src/pages/landing/store/assist-data.ts
new file mode 100644
index 00000000..0ed195fa
--- /dev/null
+++ b/src/pages/landing/store/assist-data.ts
@@ -0,0 +1,60 @@
+import { create } from 'zustand';
+
+interface States {
+ email: string;
+ name: string;
+ lastName: string;
+ company: string;
+ isAgreed: boolean;
+
+ emailError: string;
+ nameError: string;
+ lastNameError: string;
+ companyError: string;
+}
+
+interface Actions {
+ setEmail: (email: string) => void;
+ setName: (name: string) => void;
+ setLastName: (lastName: string) => void;
+ setCompany: (company: string) => void;
+ setIsAgreed: (isAgreed: boolean) => void;
+
+ setEmailError: (emailError: string) => void;
+ setNameError: (nameError: string) => void;
+ setLastNameError: (lastNameError: string) => void;
+ setCompanyError: (companyError: string) => void;
+
+ reset: () => void;
+}
+
+export const useAssistDataStore = create
((set) => ({
+ email: '',
+ name: '',
+ lastName: '',
+ company: '',
+ isAgreed: false,
+ setEmail: (email) => set({ email }),
+ setName: (name) => set({ name }),
+ setLastName: (lastName) => set({ lastName }),
+ setCompany: (company) => set({ company }),
+ setIsAgreed: (isAgreed) => set({ isAgreed }),
+
+ emailError: '',
+ nameError: '',
+ lastNameError: '',
+ companyError: '',
+ setEmailError: (emailError) => set({ emailError }),
+ setNameError: (nameError) => set({ nameError }),
+ setLastNameError: (lastNameError) => set({ lastNameError }),
+ setCompanyError: (companyError) => set({ companyError }),
+
+ reset: () =>
+ set({
+ email: '',
+ name: '',
+ lastName: '',
+ company: '',
+ isAgreed: false,
+ }),
+}));
diff --git a/src/pages/landing/ui/anchor-point.tsx b/src/pages/landing/ui/anchor-point.tsx
new file mode 100644
index 00000000..0ae158aa
--- /dev/null
+++ b/src/pages/landing/ui/anchor-point.tsx
@@ -0,0 +1,51 @@
+'use client';
+
+import { Link as HeroUILink } from '@heroui/react';
+import { useEffect, useRef, useState } from 'react';
+import { createPortal } from 'react-dom';
+
+interface Props {
+ label: string;
+ anchorPortalId: string;
+}
+
+const AnchorPoint = ({ label, anchorPortalId }: Props) => {
+ const ref = useRef(null);
+ const [portalContainer, setPortalContainer] = useState(
+ null,
+ );
+
+ const scroll = () => {
+ const offset = 64;
+ const elementPosition = ref.current?.getBoundingClientRect().top ?? 0; // 뷰포트 기준 위치
+ const offsetPosition = window.scrollY + elementPosition - offset;
+
+ window.scrollTo({
+ top: offsetPosition,
+ behavior: 'smooth',
+ });
+ };
+
+ useEffect(() => {
+ setPortalContainer(document.getElementById(anchorPortalId));
+ }, []);
+
+ return (
+
+ {portalContainer &&
+ createPortal(
+
+ {label}
+ ,
+ portalContainer,
+ )}
+
+ );
+};
+
+export default AnchorPoint;
diff --git a/src/pages/landing/ui/page.tsx b/src/pages/landing/ui/page.tsx
new file mode 100644
index 00000000..786cf1c4
--- /dev/null
+++ b/src/pages/landing/ui/page.tsx
@@ -0,0 +1,36 @@
+import { Hero } from '@/shared/ui';
+import { Footer } from '@/widgets/footer';
+import Header from '@/widgets/header';
+
+import AnchorPoint from './anchor-point';
+import SupportDescription from './support/description';
+import SupportForm from './support/form';
+import TutorialChatting from './tutorial-chatting/chatting';
+import TutorialChattingDescription from './tutorial-chatting/description';
+
+const LandingPage = () => {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default LandingPage;
diff --git a/src/pages/landing/ui/styles.ts b/src/pages/landing/ui/styles.ts
new file mode 100644
index 00000000..803ce2e9
--- /dev/null
+++ b/src/pages/landing/ui/styles.ts
@@ -0,0 +1,9 @@
+import { tv } from 'tailwind-variants';
+
+export const description = tv({
+ slots: {
+ wrapper: 'flex flex-col gap-4',
+ title: ['font-bold', 'text-5xl md:text-6xl lg:text-7xl'],
+ description: 'text-lg sm:text-xl',
+ },
+});
diff --git a/src/pages/landing/ui/support/agreement.tsx b/src/pages/landing/ui/support/agreement.tsx
new file mode 100644
index 00000000..12a64628
--- /dev/null
+++ b/src/pages/landing/ui/support/agreement.tsx
@@ -0,0 +1,38 @@
+'use client';
+
+import { Checkbox } from '@heroui/react';
+import Link from 'next/link';
+
+import { useAssistDataStore } from '@/pages/landing/store/assist-data';
+
+const Agreement = () => {
+ const isAgreed = useAssistDataStore((state) => state.isAgreed);
+ const setIsAgreed = useAssistDataStore((state) => state.setIsAgreed);
+
+ return (
+
+
+ {'CAMUS의 '}
+
+ 서비스 약관
+
+ {' 및 '}
+
+ 개인정보 보호정책
+
+ {
+ ' 에 동의합니다. 이는 CAMUS로부터 마케팅 정보를 수신하는 것에 대한 내 동의를 포함합니다. 마케팅 커뮤니케이션 수신을 언제든지 취소할 수 있습니다.'
+ }
+
+
+ );
+};
+
+export default Agreement;
diff --git a/src/pages/landing/ui/support/check-circle.tsx b/src/pages/landing/ui/support/check-circle.tsx
new file mode 100644
index 00000000..93b63617
--- /dev/null
+++ b/src/pages/landing/ui/support/check-circle.tsx
@@ -0,0 +1,19 @@
+export const CheckCircle = ({ size = 24, ...props }: IconSvgProps) => {
+ return (
+
+
+
+ );
+};
diff --git a/src/pages/landing/ui/support/description.tsx b/src/pages/landing/ui/support/description.tsx
new file mode 100644
index 00000000..18ac1ec8
--- /dev/null
+++ b/src/pages/landing/ui/support/description.tsx
@@ -0,0 +1,37 @@
+import { description } from '../styles';
+import { CheckCircle } from './check-circle';
+
+const SupportDescription = () => {
+ const styles = description();
+
+ return (
+
+
+ {'지금 영업팀에 '}
+
+ 문의하세요.
+
+
+ {'CAMUS가 앱 내외에서 제공하는 더욱 혁신적인 '}
+
+ 고객 커뮤니케이션을 확인해보세요.
+
+
+ {['활용 사례 상담', '주요 기능 살펴보기', '맞춤 견적 받기'].map(
+ (item) => (
+
+
+ {item}
+
+ ),
+ )}
+
+
+ );
+};
+
+export default SupportDescription;
diff --git a/src/pages/landing/ui/support/form.tsx b/src/pages/landing/ui/support/form.tsx
new file mode 100644
index 00000000..de0a2fa6
--- /dev/null
+++ b/src/pages/landing/ui/support/form.tsx
@@ -0,0 +1,88 @@
+'use client';
+
+import { LogIn } from 'lucide-react';
+import Link from 'next/link';
+import { tv } from 'tailwind-variants';
+
+import { ROUTE } from '@/shared/config';
+
+import Agreement from './agreement';
+import SupportInput from './input';
+import RequestButton from './request-button';
+
+const createStyle = tv({
+ slots: {
+ wrapper: [
+ 'w-full max-w-[500px] md:max-w-[620px] lg:w-1/2',
+ 'flex flex-col gap-6',
+ ],
+ form: ['px-6 py-8', 'border border-2 border-indigo-600'],
+ inputWrapper: 'xs:flex-row xs:gap-2',
+ login: ['flex items-center justify-between px-8 py-6 text-lg'],
+ },
+ compoundSlots: [
+ {
+ slots: ['form', 'inputWrapper'],
+ class: 'flex flex-col gap-5',
+ },
+ {
+ slots: ['form', 'login'],
+ class: 'rounded-large bg-background',
+ },
+ ],
+});
+
+const SupportForm = () => {
+ const styles = createStyle();
+
+ return (
+
+
+
state.email}
+ setValueSelector={(state) => state.setEmail}
+ errorSelector={(state) => state.emailError}
+ />
+
+ state.lastName}
+ setValueSelector={(state) => state.setLastName}
+ errorSelector={(state) => state.lastNameError}
+ />
+ state.name}
+ setValueSelector={(state) => state.setName}
+ errorSelector={(state) => state.nameError}
+ />
+
+ state.company}
+ setValueSelector={(state) => state.setCompany}
+ errorSelector={(state) => state.companyError}
+ />
+
+
+
+
+ 이미 CAMUS 계정이 있으신가요?
+
+ 로그인
+
+
+
+
+ );
+};
+
+export default SupportForm;
diff --git a/src/pages/landing/ui/support/input.tsx b/src/pages/landing/ui/support/input.tsx
new file mode 100644
index 00000000..11052a83
--- /dev/null
+++ b/src/pages/landing/ui/support/input.tsx
@@ -0,0 +1,42 @@
+import { Input } from '@heroui/react';
+
+import { useAssistDataStore } from '@/pages/landing/store/assist-data';
+
+type Selector = Parameters[0];
+type State = Parameters[0];
+
+interface Props {
+ label: string;
+ placeholder: string;
+ valueSelector: (state: State) => string;
+ setValueSelector: (state: State) => (value: string) => void;
+ errorSelector?: (state: State) => string;
+}
+
+const SupportInput = ({
+ valueSelector,
+ setValueSelector,
+ errorSelector,
+ ...props
+}: Props) => {
+ const value = useAssistDataStore(valueSelector);
+ const setValue = useAssistDataStore(setValueSelector);
+ const error = useAssistDataStore(errorSelector ?? (() => undefined));
+
+ return (
+
+ );
+};
+
+export default SupportInput;
diff --git a/src/pages/landing/ui/support/request-button.tsx b/src/pages/landing/ui/support/request-button.tsx
new file mode 100644
index 00000000..92d4316e
--- /dev/null
+++ b/src/pages/landing/ui/support/request-button.tsx
@@ -0,0 +1,85 @@
+'use client';
+
+import { useCallback, useEffect } from 'react';
+
+import { useAssistDataStore } from '@/pages/landing/store/assist-data';
+import { EMAIL_REGEX } from '@/shared/config';
+import { Button } from '@/shared/ui';
+
+type SetErrorFunction = (errorMessage: string) => void;
+
+const RequestButton = () => {
+ const resetForm = useAssistDataStore((state) => state.reset);
+
+ useEffect(() => {
+ return () => {
+ resetForm();
+ };
+ }, []);
+
+ const handleClick = useCallback(async () => {
+ let isInvalid = false;
+ const {
+ email,
+ setEmailError,
+ lastName,
+ setLastNameError,
+ name,
+ setNameError,
+ company,
+ setCompanyError,
+ isAgreed,
+ } = useAssistDataStore.getState();
+
+ const validate = (
+ isValid: boolean,
+ setError: SetErrorFunction,
+ errorMessage: string,
+ ) => {
+ if (isValid) {
+ setError('');
+ return;
+ }
+ setError(errorMessage);
+ isInvalid = true;
+ };
+
+ validate(
+ EMAIL_REGEX.test(email),
+ setEmailError,
+ '이메일 형식이 올바르지 않습니다.',
+ );
+
+ const requiredFields: [string, SetErrorFunction][] = [
+ [lastName, setLastNameError],
+ [name, setNameError],
+ [company, setCompanyError],
+ ];
+
+ requiredFields.forEach(([value, setError]) =>
+ validate(Boolean(value), setError, '필수 입력 항목입니다.'),
+ );
+
+ if (isInvalid) return;
+
+ if (!isAgreed) {
+ alert('서비스 약관 및 개인정보 보호정책에 동의해주세요.');
+ return;
+ }
+
+ alert('성공적으로 제출되었습니다.');
+ resetForm();
+ }, []);
+
+ return (
+
+ 제출하기
+
+ );
+};
+
+export default RequestButton;
diff --git a/src/pages/landing/ui/tutorial-chatting/chatting-body.tsx b/src/pages/landing/ui/tutorial-chatting/chatting-body.tsx
new file mode 100644
index 00000000..b41565c2
--- /dev/null
+++ b/src/pages/landing/ui/tutorial-chatting/chatting-body.tsx
@@ -0,0 +1,45 @@
+'use client';
+
+import { CardBody, CardFooter } from '@heroui/react';
+import { useState } from 'react';
+
+import { Messages } from '@/features/messages';
+import { TUTORIAL_CHAT_MEMBER_ID } from '@/shared/config';
+import { ChattingInput } from '@/widgets/chatting';
+
+import { fetchFilteringPrediction } from '../../api';
+import { TUTORIAL_CHAT_FILTER_LEVEL } from '../../config';
+import { createMessage } from '../../lib/tutorial-chatting';
+
+const TutorialChattingBody = () => {
+ const [messages, setMessages] = useState([
+ createMessage('안녕하세요', TUTORIAL_CHAT_MEMBER_ID.ai),
+ createMessage('무엇을 도와드릴까요?', TUTORIAL_CHAT_MEMBER_ID.ai),
+ ]);
+
+ const handleSendMessage = (value: string) => {
+ const message = createMessage(value, TUTORIAL_CHAT_MEMBER_ID.user);
+ setMessages((prev) => [...prev, message]);
+
+ fetchFilteringPrediction(value).then((filterLevel) => {
+ message.filteredLevel = filterLevel;
+ setMessages((prev) => [...prev]);
+ });
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+ >
+ );
+};
+
+export default TutorialChattingBody;
diff --git a/src/pages/landing/ui/tutorial-chatting/chatting.tsx b/src/pages/landing/ui/tutorial-chatting/chatting.tsx
new file mode 100644
index 00000000..c03a3c3a
--- /dev/null
+++ b/src/pages/landing/ui/tutorial-chatting/chatting.tsx
@@ -0,0 +1,41 @@
+import { Card } from '@heroui/react';
+
+import { ChattingRoomProvider } from '@/entities/chatting-room';
+import { TUTORIAL_CHAT_MEMBER_ID } from '@/shared/config';
+import { Mockup } from '@/shared/ui';
+import { ChattingHeader, ChattingTitle } from '@/widgets/chatting';
+
+import TutorialChattingBody from './chatting-body';
+
+const createMember = (
+ uuid: string,
+ name: string,
+ profileLink: null | string,
+): Member => {
+ return { uuid, username: name, nickname: name, profileLink, role: 'b2c' };
+};
+
+const TutorialChatting = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default TutorialChatting;
diff --git a/src/pages/landing/ui/tutorial-chatting/description.tsx b/src/pages/landing/ui/tutorial-chatting/description.tsx
new file mode 100644
index 00000000..690b639b
--- /dev/null
+++ b/src/pages/landing/ui/tutorial-chatting/description.tsx
@@ -0,0 +1,36 @@
+import { tv } from 'tailwind-variants';
+
+import { description } from '../styles';
+
+const createStyle = tv({
+ extend: description,
+ slots: {
+ wrapper: [
+ 'max-w-[666px] lg:max-xl:grow',
+ 'items-center xl:items-start',
+ 'text-center xl:text-left',
+ ],
+ title: 'xl:text-6xl',
+ },
+});
+
+const TutorialChattingDescription = () => {
+ const styles = createStyle();
+
+ return (
+
+
+ {'문맥까지 이해하는 '}
+
+ AI 필터링
+
+
+ {'실시간 감지와 정교한 필터링으로 악성 채팅을 차단하고, '}
+
+ 누구나 안심하고 대화할 수 있는 커뮤니케이션 공간을 만듭니다.
+
+
+ );
+};
+
+export default TutorialChattingDescription;
diff --git a/src/pages/coming-soon/index.ts b/src/pages/not-found/index.ts
similarity index 100%
rename from src/pages/coming-soon/index.ts
rename to src/pages/not-found/index.ts
diff --git a/src/pages/not-found/ui/home-button.tsx b/src/pages/not-found/ui/home-button.tsx
new file mode 100644
index 00000000..4017e375
--- /dev/null
+++ b/src/pages/not-found/ui/home-button.tsx
@@ -0,0 +1,22 @@
+'use client';
+
+import { useRouter } from 'next/navigation';
+
+import { ROUTE } from '@/shared/config';
+import { Button } from '@/shared/ui';
+
+const HomeButton = () => {
+ const router = useRouter();
+
+ return (
+ router.push(ROUTE.home)}
+ >
+ Home
+
+ );
+};
+
+export default HomeButton;
diff --git a/src/pages/not-found/ui/index.tsx b/src/pages/not-found/ui/index.tsx
new file mode 100644
index 00000000..68ef9ad4
--- /dev/null
+++ b/src/pages/not-found/ui/index.tsx
@@ -0,0 +1,30 @@
+import Image from 'next/image';
+
+import { BasicLayout } from '@/widgets/basic-layout';
+
+import HomeButton from './home-button';
+
+const ComingSoon = () => {
+ return (
+
+
+ Page not found
+
+ {`We can't seem to find the page you are looking for.`}
+
+
+
+
+
+ );
+};
+
+export default ComingSoon;
diff --git a/src/pages/signup/ui/agreement.tsx b/src/pages/signup/ui/agreement.tsx
index 6b069a2c..4269f40b 100644
--- a/src/pages/signup/ui/agreement.tsx
+++ b/src/pages/signup/ui/agreement.tsx
@@ -1,6 +1,6 @@
'use client';
-import { Checkbox } from '@heroui/checkbox';
+import { Checkbox } from '@heroui/react';
import Link from 'next/link';
import { useSignupDataStore } from '../store/signup-data';
diff --git a/src/pages/signup/ui/banner/index.tsx b/src/pages/signup/ui/banner/index.tsx
index 95f3f2c1..d849f4ed 100644
--- a/src/pages/signup/ui/banner/index.tsx
+++ b/src/pages/signup/ui/banner/index.tsx
@@ -1,4 +1,4 @@
-import { CheckIcon } from '@/shared/ui';
+import { Check } from 'lucide-react';
import Animation from './animation';
@@ -22,7 +22,7 @@ const Banner = () => {
{DETAILS.map((detail) => (
-
+
{detail}
))}
diff --git a/src/pages/signup/ui/enterprise-select.tsx b/src/pages/signup/ui/enterprise-select.tsx
index 87dae6a3..bf690da2 100644
--- a/src/pages/signup/ui/enterprise-select.tsx
+++ b/src/pages/signup/ui/enterprise-select.tsx
@@ -1,15 +1,15 @@
'use client';
+import { CircleCheck } from 'lucide-react';
import { tv } from 'tailwind-variants';
import { useSignupDataStore } from '@/pages/signup/store/signup-data';
-import { CheckCircleIcon } from '@/shared/ui';
import { ENTERPRISE, PERSONAL } from '../constants';
const style = tv({
base: [
- 'flex items-center justify-center font-medium duration-300',
+ 'flex items-center justify-center gap-1.5 font-medium duration-300',
'h-10 min-w-[4rem] rounded-lg px-4 text-sm',
],
variants: {
@@ -31,7 +31,7 @@ const EnterpriseSelect = () => {
className={style({ selected: selectedKey === PERSONAL })}
onClick={() => onSelect(PERSONAL)}
>
-
+
Personal
{
className={style({ selected: selectedKey === ENTERPRISE })}
onClick={() => onSelect(ENTERPRISE)}
>
-
+
Enterprise
diff --git a/src/shared/config/global.d.ts b/src/shared/config/global.d.ts
index b3bec077..fb62ae6d 100644
--- a/src/shared/config/global.d.ts
+++ b/src/shared/config/global.d.ts
@@ -4,3 +4,7 @@ type ErrorPage = FC<{
error: Error & { digest?: string };
reset: () => void;
}>;
+
+interface IconSvgProps extends React.ComponentProps<'svg'> {
+ size?: number;
+}
diff --git a/src/shared/config/index.ts b/src/shared/config/index.ts
index 39f8fcc7..cdb50fe3 100644
--- a/src/shared/config/index.ts
+++ b/src/shared/config/index.ts
@@ -14,6 +14,7 @@ export const PASSWORD_REGEX =
export const ACCESS_TOKEN = 'a-t';
export const ROUTE = {
+ home: '/',
login: '/signin',
signup: '/signup',
chat: '/service/chat',
@@ -25,3 +26,17 @@ export const ROUTE = {
export const MEATADATA = {
title: 'CAMUS',
};
+
+export const TUTORIAL_CHAT_MEMBER_ID = {
+ ai: 'tutorial-ai',
+ user: 'tutorial-user',
+};
+
+export const FILTERING_API_BASE_URL =
+ process.env.NEXT_PUBLIC_FILTERING_API_BASE_URL;
+
+export const FILTERING_PREDICTION = {
+ abuse: 0,
+ harassment: 1,
+ neutral: 2,
+};
diff --git a/src/shared/ui/component/button.tsx b/src/shared/ui/component/button.tsx
index 8b3d866e..db485581 100644
--- a/src/shared/ui/component/button.tsx
+++ b/src/shared/ui/component/button.tsx
@@ -1,4 +1,4 @@
-import { Button, type ButtonProps } from '@heroui/button';
+import { Button, type ButtonProps } from '@heroui/react';
interface Props extends Omit