Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,19 @@ interface ComparisonActionButtonsProps {
rightButtonText?: string;
onLeftClick: () => void;
onRightClick: () => void;
className?: string;
}

const ComparisonActionButtons: React.FC<ComparisonActionButtonsProps> = ({
leftButtonText,
rightButtonText,
onLeftClick,
onRightClick,
className = '',
}) => {
return (
<div
className="flex flex-col justify-center items-center text-center gap-6 fixed bottom-[50px] max-w-[560px]"
className={`flex flex-col justify-center items-center text-center gap-6 ${className}`}
style={{ width: 'calc(100% - 40px)' }}
>
<div className="flex w-full">
Expand Down
36 changes: 32 additions & 4 deletions client/src/components/ComparePage/ComparisonResult.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import togetherIcon from '@/assets/image/card_family.png';
import premiumAddonsIcon from '@/assets/icon/special.png';
import mediaAddonsIcon from '@/assets/icon/media.png';
Expand All @@ -22,13 +22,33 @@ const ComparisonResult: React.FC<ComparisonResultProps> = ({
}) => {
const [isModalOpen, setIsModalOpen] = useState(false);
const [pendingLink, setPendingLink] = useState<string | null>(null);
const [showButtons, setShowButtons] = useState(false);

const handleDetailClick = (detailUrl?: string) => {
if (detailUrl) {
setPendingLink(detailUrl); // 링크만 저장
setIsModalOpen(true); // 모달 먼저 띄우기
}
};
useEffect(() => {
const scrollContainer = document.querySelector('.scroll-target');
if (!scrollContainer) return;

const handleScroll = () => {
const scrollTop = scrollContainer.scrollTop;
const containerHeight = scrollContainer.clientHeight;
const scrollHeight = scrollContainer.scrollHeight;

if (scrollTop + containerHeight >= scrollHeight - 100) {
setShowButtons(true);
} else {
setShowButtons(false);
}
};

scrollContainer.addEventListener('scroll', handleScroll);
return () => scrollContainer.removeEventListener('scroll', handleScroll);
}, []);

// 데이터 값 계산
const leftDataValue = selectedLeft
Expand Down Expand Up @@ -159,27 +179,35 @@ const ComparisonResult: React.FC<ComparisonResultProps> = ({
/>
</div>
</div>

<ComparisonActionButtons
leftButtonText={selectedLeft ? '자세히 보기' : ''}
rightButtonText={selectedRight ? '자세히 보기' : ''}
onLeftClick={() => handleDetailClick(selectedLeft?.detailUrl)}
onRightClick={() => handleDetailClick(selectedRight?.detailUrl)}
className={`fixed bottom-[50px] max-w-[560px] transition-all duration-500
${showButtons ? 'opacity-100 translate-y-0 pointer-events-auto' : 'opacity-0 translate-y-5 pointer-events-none'}
`}
/>

<Modal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
modalTitle="요금제 자세히 알아보기"
modalDesc="요금제 상세 페이지는 외부 사이트로 연결됩니다. 계속 진행하시겠습니까?"
modalDesc={
<>
요금제 상세 페이지는 외부 사이트로 연결됩니다.
<br />
계속 진행하시겠습니까?
</>
}
>
<Button
fullWidth
variant="secondary"
size="medium"
onClick={() => setIsModalOpen(false)}
>
취소
닫기
</Button>
<Button
fullWidth
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/common/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ interface ModalProps {
isOpen: boolean;
onClose: () => void;
modalTitle: string;
modalDesc?: string;
modalDesc?: React.ReactNode;
children: React.ReactNode;
}

Expand Down
86 changes: 86 additions & 0 deletions client/src/components/common/ToastAlert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';

interface ToastAlertProps {
message: string;
onClose?: () => void;
}

function ToastAlert({ message, onClose }: ToastAlertProps) {
const [visible, setVisible] = useState(true);
const [isClosing, setIsClosing] = useState(false);

useEffect(() => {
const timer = setTimeout(() => {
setIsClosing(true);
}, 2500);

return () => clearTimeout(timer);
}, []);

const handleAnimationEnd = () => {
if (isClosing) {
setVisible(false);
if (onClose) onClose();
}
};

if (!visible) return null;

return createPortal(
<div
style={{
position: 'fixed',
top: '15px',
left: '50%',
transform: 'translateX(-50%)',
width: 'calc(100% - 20px)',
maxWidth: '560px',
padding: '16px 20px',
backgroundColor: 'rgba(255, 255, 255, 1)',
borderRadius: '12px',
boxShadow: '0 0 10px rgba(0, 0, 0, 0.08)',
fontSize: '14px',
lineHeight: '19px',
textAlign: 'center',
color: '#6B7280',
zIndex: 9999,
animation: isClosing
? 'slideUp 0.3s ease forwards'
: 'slideDown 0.3s ease forwards',
whiteSpace: 'pre-line',
}}
onAnimationEnd={handleAnimationEnd}
>
{message}
<style>
{`
@keyframes slideDown {
from {
opacity: 0.7;
transform: translate(-50%, -20px);
}
to {
opacity: 1;
transform: translate(-50%, 0);
}
}

@keyframes slideUp {
from {
opacity: 1;
transform: translate(-50%, 0);
}
to {
opacity: 0;
transform: translate(-50%, -20px);
}
}
`}
</style>
</div>,
document.body,
);
}

export default ToastAlert;
72 changes: 53 additions & 19 deletions client/src/pages/ChatbotPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import Modal from '@/components/common/Modal';
import Button from '@/components/common/Button';
import LoadingSpinner from '@/components/common/LoadingSpinner';
import type { UserProfile } from '@/utils/chatStorage';
import ToastAlert from '@/components/common/ToastAlert';

// 사용자 정보 타입 제거 (chatStorage에서 import)

Expand Down Expand Up @@ -81,7 +82,9 @@ const MemoizedInputBox = React.memo(InputBox);
const MemoizedUserBubble = React.memo(UserBubble);
const MemoizedBotBubbleFrame = React.memo(BotBubbleFrame);
const MemoizedLoadingBubble = React.memo(LoadingBubble);

const isMobile =
/iPhone|iPad|iPod|Android/.test(navigator.userAgent) &&
!/Macintosh|Windows/.test(navigator.userAgent);
const ChatbotPage = () => {
const [input, setInput] = useState('');
const {
Expand All @@ -102,7 +105,7 @@ const ChatbotPage = () => {
const [showBackModal, setShowBackModal] = useState(false);
const [showCallModal, setShowCallModal] = useState(false);
const hasInitializedForUrlParams = useRef(false); // URL 파라미터 초기화 여부 추적

const [toastMessage, setToastMessage] = useState('');
// 사용자 정보 확인: URL 파라미터에서만 읽음 - 메모이제이션으로 최적화
const urlUserProfile = useMemo(
() => parseUserProfileFromURL(searchParams),
Expand Down Expand Up @@ -456,7 +459,7 @@ const ChatbotPage = () => {
fullWidth
onClick={handleClose}
>
돌아가기
닫기
</Button>

<Button
Expand All @@ -471,33 +474,64 @@ const ChatbotPage = () => {
새로 시작하기
</Button>
</Modal>

{toastMessage && (
<ToastAlert
message={toastMessage}
onClose={() => setToastMessage('')}
/>
)}
<Modal
isOpen={showCallModal}
onClose={() => setShowCallModal(false)}
modalTitle="고객센터 080-019-7000"
modalDesc="상담원 연결을 시작할 경우, 이전에 진행한 상담은 모두 초기화됩니다."
modalTitle="고객센터 연결 | 080-019-7000"
modalDesc={
<>
상담원 연결을 진행할 경우, 이전에 진행한 상담은 모두 초기화됩니다.
<br />※ 전화 연결은 모바일 환경에서만 가능합니다.
</>
}
>
<Button
variant="secondary"
size="medium"
fullWidth
onClick={handleClose}
>
돌아가기
</Button>

<Button
variant="primary"
size="medium"
fullWidth
onClick={() => {
handleNewChat();
handleClose();
}}
>
전화하기
닫기
</Button>
{isMobile ? (
<a
href="tel:0800197000"
onClick={async (e) => {
e.stopPropagation();
await navigator.clipboard.writeText('0800197000');
}}
className="w-full"
>
<Button variant="primary" size="medium" fullWidth>
전화하기
</Button>
</a>
) : (
<Button
variant="primary"
size="medium"
fullWidth
onClick={async () => {
try {
await navigator.clipboard.writeText('080-019-7000');
setToastMessage(
'전화 연결 기능은 모바일에서만 지원됩니다. 고객센터 번호가 복사되었습니다.',
);
} catch {
setToastMessage('전화 연결 기능은 모바일에서만 지원됩니다.');
}
handleClose();
}}
>
전화하기
</Button>
)}
</Modal>
</>
);
Expand Down
2 changes: 1 addition & 1 deletion client/src/pages/ComparePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ const ComparePage: React.FC = () => {
<div className="h-[100dvh] flex flex-col scroll-auto">
<Header title="요금제 비교하기" onBackClick={() => openModal()} />

<div className="flex-1 overflow-y-auto px-5 hide-scrollbar">
<div className="flex-1 overflow-y-auto px-5 hide-scrollbar scroll-target">
<ComparePageTitle>
비교하고 싶은
<br />
Expand Down
Loading
Loading