From 12f7513c47850e5df9ac06de1577e8df3d88dd34 Mon Sep 17 00:00:00 2001 From: semin oh Date: Mon, 20 Jul 2026 18:58:31 +0900 Subject: [PATCH] =?UTF-8?q?=EC=88=98=EA=B0=95=EC=8B=A0=EC=B2=AD=20?= =?UTF-8?q?=ED=8A=9C=ED=86=A0=EB=A6=AC=EC=96=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 2 + src/app/index.tsx | 2 + .../course-search/ui/SearchCourseItem.tsx | 3 + .../ui/SortableCourseItem.tsx | 3 + src/features/tour/api/tourApi.ts | 10 + src/features/tour/index.ts | 9 + src/features/tour/model/tourStore.ts | 91 +++ src/features/tour/model/tutorialCourse.ts | 38 ++ src/features/tour/model/types.ts | 26 + src/features/tour/model/useIsDesktop.ts | 23 + src/features/tour/ui/TourController.tsx | 528 ++++++++++++++++++ src/features/tour/ui/tour.css | 147 +++++ src/pages/cart/index.tsx | 41 +- src/pages/enrollment-history/index.tsx | 12 +- src/pages/registration/index.tsx | 135 ++++- src/pages/search/index.tsx | 92 ++- src/shared/api/axios.ts | 2 +- src/shared/ui/Warning/components/Confirm.tsx | 8 +- src/widgets/header/Header.tsx | 80 ++- src/widgets/header/header.css | 6 + vite.config.ts | 54 +- 21 files changed, 1240 insertions(+), 72 deletions(-) create mode 100644 .env.example create mode 100644 src/features/tour/api/tourApi.ts create mode 100644 src/features/tour/index.ts create mode 100644 src/features/tour/model/tourStore.ts create mode 100644 src/features/tour/model/tutorialCourse.ts create mode 100644 src/features/tour/model/types.ts create mode 100644 src/features/tour/model/useIsDesktop.ts create mode 100644 src/features/tour/ui/TourController.tsx create mode 100644 src/features/tour/ui/tour.css diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..90a65f4 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +VITE_API_URL=/ +VITE_API_PROXY_TARGET=https://snuclear.wafflestudio.com diff --git a/src/app/index.tsx b/src/app/index.tsx index d7166e2..3ffea22 100644 --- a/src/app/index.tsx +++ b/src/app/index.tsx @@ -5,6 +5,7 @@ import { Header } from '@widgets/header'; import { Footer } from '@widgets/footer'; import { SideMenu } from '@widgets/side-menu'; import { WarningModal } from '@shared/ui/Warning'; +import { TourController } from '@features/tour'; import { AppRoutes } from './routes'; import './styles/global.css'; @@ -96,6 +97,7 @@ export default function App() { setShowLoginWarningOpen={setShowLoginWarningOpen} /> )} + ); } diff --git a/src/features/course-search/ui/SearchCourseItem.tsx b/src/features/course-search/ui/SearchCourseItem.tsx index d95fa9c..e07bd4d 100644 --- a/src/features/course-search/ui/SearchCourseItem.tsx +++ b/src/features/course-search/ui/SearchCourseItem.tsx @@ -7,6 +7,7 @@ export interface SearchCourseItemProps { isSelected: boolean; cartCount?: number; onSelect: () => void; + checkDataTourId?: string; } export function SearchCourseItem({ @@ -14,6 +15,7 @@ export function SearchCourseItem({ isSelected, cartCount = 0, onSelect, + checkDataTourId, }: SearchCourseItemProps) { const schedule = useMemo( () => formatSchedule(course.placeAndTime, '시간 미정'), @@ -35,6 +37,7 @@ export function SearchCourseItem({ + + ); + } + + const tooltipStyle = getTooltipStyle(rect, meta.placement ?? 'bottom'); + + return ( +
+
+
+
+
+ +
+ +
+ +

{meta.message}

+
+ + +
+ ); +} + +function getTooltipStyle( + rect: TargetRect, + placement: 'top' | 'bottom' | 'left' | 'right' +) { + const gap = 18; + const width = Math.min(320, window.innerWidth - 32); + const horizontalCenter = rect.left + rect.width / 2; + const verticalCenter = rect.top + rect.height / 2; + + if (placement === 'top') { + return { + left: Math.min( + window.innerWidth - width - 16, + Math.max(16, horizontalCenter - width / 2) + ), + top: Math.max(16, rect.top - gap), + transform: 'translateY(-100%)', + width, + }; + } + + if (placement === 'left') { + return { + left: Math.max(16, rect.left - width - gap), + top: Math.max(16, verticalCenter), + transform: 'translateY(-50%)', + width, + }; + } + + if (placement === 'right') { + return { + left: Math.min( + window.innerWidth - width - 16, + rect.left + rect.width + gap + ), + top: Math.max(16, verticalCenter), + transform: 'translateY(-50%)', + width, + }; + } + + return { + left: Math.min( + window.innerWidth - width - 16, + Math.max(16, horizontalCenter - width / 2) + ), + top: Math.min(window.innerHeight - 120, rect.top + rect.height + gap), + width, + }; +} diff --git a/src/features/tour/ui/tour.css b/src/features/tour/ui/tour.css new file mode 100644 index 0000000..81d1737 --- /dev/null +++ b/src/features/tour/ui/tour.css @@ -0,0 +1,147 @@ +.tourIntroCheck { + display: inline-flex; + align-items: center; + gap: 8px; + margin-top: 18px; + font-size: 14px; + color: #555; + cursor: pointer; +} + +.tourIntroCheck input { + width: 16px; + height: 16px; + accent-color: #1b4594; +} + +.tourLayer { + position: fixed; + inset: 0; + z-index: 10000; + pointer-events: none; +} + +.tourBlocker { + position: fixed; + background: rgba(0, 0, 0, 0.72); + pointer-events: auto; +} + +.tourBlocker.top { + top: 0; + left: 0; + right: 0; +} + +.tourBlocker.left { + left: 0; +} + +.tourBlocker.right { + right: 0; +} + +.tourBlocker.bottom { + left: 0; + right: 0; + bottom: 0; +} + +.tourFocusRing { + position: fixed; + outline: 3px solid #ffffff; + outline-offset: 0; + border: 2px solid #376dc8; + border-radius: 8px; + box-shadow: 0 0 22px rgba(255, 255, 255, 0.85); + pointer-events: none; +} + +.tourTooltip { + position: fixed; + z-index: 10002; + background: #ffffff; + color: #111827; + border-radius: 8px; + padding: 14px 16px; + box-shadow: 0 12px 32px rgba(0, 0, 0, 0.22); + border: 1px solid #e5e7eb; + pointer-events: none; + max-width: min(320px, calc(100vw - 32px)); +} + +.tourTooltip p { + margin: 0; + font-size: 15px; + line-height: 1.55; + word-break: keep-all; + overflow-wrap: break-word; + text-align: left; + letter-spacing: 0; +} + +.tourTooltipArrow { + position: absolute; + width: 0; + height: 0; +} + +.tourTooltip.bottom .tourTooltipArrow { + top: -8px; + left: 50%; + transform: translateX(-50%); + border-left: 8px solid transparent; + border-right: 8px solid transparent; + border-bottom: 8px solid #ffffff; +} + +.tourTooltip.top .tourTooltipArrow { + bottom: -8px; + left: 50%; + transform: translateX(-50%); + border-left: 8px solid transparent; + border-right: 8px solid transparent; + border-top: 8px solid #ffffff; +} + +.tourTooltip.left .tourTooltipArrow { + right: -8px; + top: 50%; + transform: translateY(-50%); + border-top: 8px solid transparent; + border-bottom: 8px solid transparent; + border-left: 8px solid #ffffff; +} + +.tourTooltip.right .tourTooltipArrow { + left: -8px; + top: 50%; + transform: translateY(-50%); + border-top: 8px solid transparent; + border-bottom: 8px solid transparent; + border-right: 8px solid #ffffff; +} + +.tourExitBtn { + position: fixed; + top: 24px; + right: 28px; + z-index: 10003; + width: 42px; + height: 42px; + border-radius: 50%; + border: 1px solid rgba(255, 255, 255, 0.65); + background: rgba(0, 0, 0, 0.45); + color: #ffffff; + font-size: 24px; + line-height: 1; + cursor: pointer; + pointer-events: auto; + display: grid; + place-items: center; + padding: 0 0 2px; +} + +.tourExitBtn:hover { + background: rgba(0, 0, 0, 0.68); +} diff --git a/src/pages/cart/index.tsx b/src/pages/cart/index.tsx index 2c33e04..c85a41a 100644 --- a/src/pages/cart/index.tsx +++ b/src/pages/cart/index.tsx @@ -9,11 +9,23 @@ import { useModalStore } from '@shared/model/modalStore'; import { WarningModal } from '@shared/ui/Warning'; import { TimeTable } from '@widgets/timetable'; import { formatSchedule } from '@shared/lib/timeUtils'; +import { createTutorialPreEnroll, useTourStore } from '@features/tour'; import './cart.css'; export default function Cart() { + const tour = useTourStore(); + const isTourActive = tour.isActive; const { data, isLoading } = useCartQuery(); - const cartCourses = Array.isArray(data) ? data : []; + const isDisplayLoading = isTourActive ? false : isLoading; + const cartCourses = useMemo( + () => + isTourActive + ? [createTutorialPreEnroll(tour.cartCount)] + : Array.isArray(data) + ? data + : [], + [data, isTourActive, tour.cartCount] + ); const deleteFromCartMutation = useDeleteFromCartMutation(); const updateCartCountMutation = useUpdateCartCountMutation(); const { @@ -69,6 +81,23 @@ export default function Cart() { }; const handleCartCountChange = async (courseId: number, newValue: string) => { + if (isTourActive) { + const newCount = parseInt(newValue); + const targetCourse = cartCourses.find((item) => item.course.id === courseId); + + if ( + targetCourse && + !isNaN(newCount) && + newCount > targetCourse.course.quota + ) { + tour.setCartOverQuota(); + tour.setStep('cartGoRegistration'); + } + setEditingCourseId(null); + setEditingValue(''); + return; + } + const newCount = parseInt(newValue); if (isNaN(newCount) || newCount < 0) { return; @@ -132,7 +161,7 @@ export default function Cart() {
0 ? ' has-items' : ''}`}> - {isLoading ? ( + {isDisplayLoading ? (

로딩 중...

@@ -275,6 +304,9 @@ export default function Cart() { }} onClick={(e) => e.stopPropagation()} className="cartCountInput" + data-tour-id={ + isTourActive ? 'cart-count' : undefined + } min="0" autoFocus /> @@ -284,8 +316,11 @@ export default function Cart() { onClick={(e) => { e.stopPropagation(); setEditingCourseId(item.course.id); - setEditingValue(String(item.cartCount)); + setEditingValue(''); }} + data-tour-id={ + isTourActive ? 'cart-count' : undefined + } > {item.cartCount} diff --git a/src/pages/enrollment-history/index.tsx b/src/pages/enrollment-history/index.tsx index 2b08237..63ce6b0 100644 --- a/src/pages/enrollment-history/index.tsx +++ b/src/pages/enrollment-history/index.tsx @@ -1,18 +1,23 @@ import { useState, useMemo } from 'react'; import { useEnrolledCoursesQuery } from '@features/registration-practice'; import { useCartQuery } from '@features/cart-management'; +import { tutorialCourse, useTourStore } from '@features/tour'; import { useModalStore } from '@shared/model/modalStore'; import { TimeTable } from '@widgets/timetable'; import { formatSchedule } from '@shared/lib/timeUtils'; import './enrollmentHistory.css'; export default function EnrollmentHistory() { + const tour = useTourStore(); + const isTourResult = tour.isRegistrationSucceeded; const { data: enrolledData, isLoading: isEnrolledLoading } = useEnrolledCoursesQuery(); const { data: cartData, isLoading: isCartLoading } = useCartQuery(); - const isLoading = isEnrolledLoading || isCartLoading; + const isLoading = isTourResult ? false : isEnrolledLoading || isCartLoading; const enrolledCourses = useMemo(() => { + if (isTourResult) return [tutorialCourse]; + const enrolled = Array.isArray(enrolledData) ? enrolledData : []; const cartCourses = Array.isArray(cartData) ? cartData : []; @@ -30,7 +35,7 @@ export default function EnrollmentHistory() { } return merged; - }, [enrolledData, cartData]); + }, [cartData, enrolledData, isTourResult]); const [selectedCourseId, setSelectedCourseId] = useState(null); const { openNotSupported } = useModalStore(); @@ -109,6 +114,9 @@ export default function EnrollmentHistory() {
{ const infoArea = event.currentTarget.querySelector( diff --git a/src/pages/registration/index.tsx b/src/pages/registration/index.tsx index c1ea8d0..2e37639 100644 --- a/src/pages/registration/index.tsx +++ b/src/pages/registration/index.tsx @@ -27,11 +27,14 @@ import { type CourseData, } from '@features/registration-practice'; import { useCartQuery } from '@features/cart-management'; +import { createTutorialPreEnroll, useTourStore } from '@features/tour'; import { useModalStore } from '@shared/model/modalStore'; import { WarningModal } from '@shared/ui/Warning'; import './registration.css'; export default function Registration() { + const tour = useTourStore(); + const isTourActive = tour.isActive; const { pipWindow, openWindow, closeWindow } = usePracticeWindow(); const { openModal, closeModal } = useModalStore(); const isPracticeEndOpen = useModalStore((s) => s.openModals.has('registration/practiceEnd')); @@ -128,7 +131,11 @@ export default function Registration() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [pipWindow]); - const courseList = localCourseList.length > 0 ? localCourseList : null; + const courseList = isTourActive + ? [createTutorialPreEnroll(tour.cartCount)] + : localCourseList.length > 0 + ? localCourseList + : null; // Drag and drop const sensors = useSensors( @@ -155,6 +162,87 @@ export default function Registration() { } }; + const handleTimerChange = (value: number) => { + timer.setStartOffset(value); + if (isTourActive && tour.currentStep === 'registrationTimer') { + tour.setStep('registrationStart'); + } + }; + + const handlePracticeStartClick = () => { + handlePracticeToggle(); + + if (isTourActive && tour.currentStep === 'registrationStart') { + tour.setPracticeReady(); + tour.setStep('registrationWaiting'); + } + }; + + useEffect(() => { + if (!isTourActive || tour.currentStep !== 'registrationWaiting') return; + if (!tour.isPracticeReady) return; + + const targetTime = new Date(timer.currentTime); + targetTime.setHours(8, 30, 0, 0); + + if (timer.currentTime.getTime() >= targetTime.getTime()) { + tour.setStep('registrationCheck'); + } + }, [ + isTourActive, + timer.currentTime, + tour, + tour.currentStep, + tour.isPracticeReady, + ]); + + const handleCourseSelect = (courseData: CourseData) => { + if (isTourActive) { + if (tour.currentStep === 'registrationCheck') { + tour.selectRegistrationCourse(); + tour.setStep('registrationCaptcha'); + } + return; + } + + courseSelection.handleSelectCourse( + courseData.course.id, + 0, + courseData.course.quota, + courseData.cartCount, + courseData.course.courseTitle, + courseData.course.courseNumber, + courseData.course.lectureNumber || '' + ); + }; + + const handleCaptchaChange = (value: string) => { + if (isTourActive) { + tour.setCaptchaInput(value); + if ( + tour.currentStep === 'registrationCaptcha' && + value.replace(/\D/g, '').length > 0 + ) { + tour.setStep('registrationSubmit'); + } + return; + } + + captcha.setCaptchaInput(value); + }; + + const handleSubmit = () => { + if (isTourActive) { + if (tour.currentStep === 'registrationSubmit') { + tour.markRegistrationSucceeded(); + tour.setStep('registrationGoHistory'); + } + return; + } + + attempt.handleRegisterAttempt(); + }; + return (
@@ -175,7 +263,7 @@ export default function Registration() {
@@ -296,9 +385,10 @@ export default function Registration() {
diff --git a/src/pages/search/index.tsx b/src/pages/search/index.tsx index 8d82c4e..0dfb6f3 100644 --- a/src/pages/search/index.tsx +++ b/src/pages/search/index.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useSearchParams, useNavigate } from 'react-router-dom'; import { isAxiosError } from 'axios'; import { @@ -11,6 +11,12 @@ import type { CourseDetailResponse } from '@entities/course'; import { useModalStore } from '@shared/model/modalStore'; import { WarningModal } from '@shared/ui/Warning'; import { Pagination } from '@shared/ui/Pagination'; +import { + TOUR_KEYWORD, + createTutorialPreEnroll, + tutorialCourse, + useTourStore, +} from '@features/tour'; import { hasTimeConflict, extractTimeFromPlaceAndTime, @@ -22,8 +28,11 @@ const PAGE_SIZE = 10; export default function SearchPage() { const [searchParams, setSearchParams] = useSearchParams(); const navigate = useNavigate(); + const tour = useTourStore(); + const isTourActive = tour.isActive; const keyword = searchParams.get('query') || ''; + const isTourSearch = searchParams.get('tour') === '1' && keyword === TOUR_KEYWORD; const currentPage = parseInt(searchParams.get('page') || '0', 10); const captcha = useCaptcha(); @@ -53,12 +62,32 @@ export default function SearchPage() { page: currentPage, size: PAGE_SIZE, }); + const isDisplayLoading = isTourActive ? false : isLoading; + const displayError = isTourActive ? null : error; const addToCartMutation = useAddToCartMutation(); - const courses = data?.items ?? []; - const totalCount = data?.pageInfo.totalElements ?? 0; - const totalPages = data?.pageInfo.totalPages ?? 0; + const shouldUseTourMode = isTourActive || isTourSearch; + const courses = shouldUseTourMode ? [tutorialCourse] : data?.items ?? []; + const totalCount = shouldUseTourMode ? 1 : data?.pageInfo.totalElements ?? 0; + const totalPages = shouldUseTourMode ? 1 : data?.pageInfo.totalPages ?? 0; + + useEffect(() => { + if (!isTourSearch) return; + if (tour.isManuallyStopped) return; + if (!tour.isActive) { + tour.startAtStep('searchCheck', tour.publishedAt); + return; + } + if (keyword !== TOUR_KEYWORD) return; + if ( + tour.currentStep === 'searchIntro' || + tour.currentStep === 'searchType' || + tour.currentStep === null + ) { + tour.setStep('searchCheck'); + } + }, [isTourSearch, keyword, tour, tour.currentStep]); const setPage = (page: number) => { const newParams = new URLSearchParams(searchParams); @@ -67,6 +96,12 @@ export default function SearchPage() { }; const toggleCourseSelection = (courseId: number) => { + if (shouldUseTourMode && tour.currentStep === 'searchCheck') { + tour.selectSearchCourse(); + tour.setStep('searchAddToCart'); + return; + } + setSelectedCourses((prev) => { if (prev.has(courseId)) { return new Set(); @@ -77,6 +112,14 @@ export default function SearchPage() { }; const handleAddToCart = async () => { + if (shouldUseTourMode) { + if (tour.currentStep === 'searchAddToCart') { + tour.setStep('searchGoToCart'); + openModal('search/cart'); + } + return; + } + if (selectedCourses.size === 0) { openModal('search/noCourseSelected'); return; @@ -141,12 +184,16 @@ export default function SearchPage() { onCancel={() => closeModal('search/cart')} onConfirm={() => { closeModal('search/cart'); + if (shouldUseTourMode && tour.currentStep === 'searchGoToCart') { + tour.setStep('cartCount'); + } navigate('/cart'); }} title="장바구니에 담겼습니다." subtitle="지금 바로 장바구니로 이동하시겠습니까?" cancelLabel="아니요, 괜찮습니다." confirmLabel="장바구니로 이동" + confirmButtonDataTourId="search-go-cart" /> '{keyword}' 검색 결과

- {isLoading ? '...' : totalCount} + + {isDisplayLoading ? '...' : totalCount} + 건의 교과목이 검색되었습니다.

@@ -240,21 +289,35 @@ export default function SearchPage() {

- {isLoading &&

검색 중...

} - {error && ( + {isDisplayLoading &&

검색 중...

} + {displayError && (

강의 검색에 실패했습니다.

)} - {!isLoading && !error && courses.length === 0 && keyword && ( -

검색 결과가 없습니다.

- )} - {!isLoading && + {!isDisplayLoading && + !displayError && + courses.length === 0 && + keyword && ( +

검색 결과가 없습니다.

+ )} + {!isDisplayLoading && courses.map((course: CourseDetailResponse) => ( toggleCourseSelection(course.id)} + checkDataTourId={ + shouldUseTourMode ? 'search-course-check' : undefined + } /> ))}
@@ -269,7 +332,7 @@ export default function SearchPage() {

- {!isLoading && !error && ( + {!isDisplayLoading && !displayError && ( 장바구니 담기 diff --git a/src/shared/api/axios.ts b/src/shared/api/axios.ts index 85e23eb..d9a61da 100644 --- a/src/shared/api/axios.ts +++ b/src/shared/api/axios.ts @@ -1,7 +1,7 @@ import axios from 'axios'; export const api = axios.create({ - baseURL: import.meta.env.VITE_API_URL || 'https://snuclear.com/', + baseURL: import.meta.env.VITE_API_URL || '/', headers: { 'Content-Type': 'application/json', }, diff --git a/src/shared/ui/Warning/components/Confirm.tsx b/src/shared/ui/Warning/components/Confirm.tsx index d6f518a..9373a20 100644 --- a/src/shared/ui/Warning/components/Confirm.tsx +++ b/src/shared/ui/Warning/components/Confirm.tsx @@ -13,6 +13,7 @@ interface ConfirmProps { subtitle?: string; cancelLabel?: string; confirmLabel?: string; + confirmButtonDataTourId?: string; children?: ReactNode; enterAction?: 'confirm' | 'cancel'; } @@ -51,6 +52,7 @@ export function Confirm({ subtitle, cancelLabel = '취소', confirmLabel = '확인', + confirmButtonDataTourId, children, enterAction = 'confirm', }: ConfirmProps) { @@ -83,7 +85,11 @@ export function Confirm({ -
diff --git a/src/widgets/header/Header.tsx b/src/widgets/header/Header.tsx index 8da9920..89c293c 100644 --- a/src/widgets/header/Header.tsx +++ b/src/widgets/header/Header.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useState } from 'react'; import { Link, useLocation, useNavigate } from 'react-router-dom'; import { useAuth } from '@features/auth'; +import { TOUR_KEYWORD, useIsDesktop, useTourStore } from '@features/tour'; import { useModalStore } from '@shared/model/modalStore'; import { WarningModal } from '@shared/ui/Warning'; import { MobilePageNav } from '@widgets/mobile-page-nav'; @@ -15,6 +16,8 @@ export default function Header({ handleLogout, onToggleSideMenu }: HeaderProps) const loc = useLocation(); const navigate = useNavigate(); const { user } = useAuth(); + const isDesktop = useIsDesktop(); + const tour = useTourStore(); const { showLoginWarning, showNotSupported, @@ -29,8 +32,22 @@ export default function Header({ handleLogout, onToggleSideMenu }: HeaderProps) const mobileSearchInputRef = React.useRef(null); const handleSearch = () => { - const query = searchInputRef.current?.value || ''; - navigate(`/search?query=${encodeURIComponent(query)}`); + const query = searchInputRef.current?.value.trim() || ''; + + if (tour.isActive && tour.currentStep === 'searchType') { + if (query !== TOUR_KEYWORD) { + searchInputRef.current?.focus(); + return; + } + + tour.setStep('searchCheck'); + navigate(`/search?query=${encodeURIComponent(query)}&tour=1`); + window.scrollTo(0, 0); + return; + } + + const tourParam = tour.isActive ? '&tour=1' : ''; + navigate(`/search?query=${encodeURIComponent(query)}${tourParam}`); window.scrollTo(0, 0); }; @@ -65,6 +82,46 @@ export default function Header({ handleLogout, onToggleSideMenu }: HeaderProps) } }; + const handleSearchInputClick = () => { + if (tour.isActive && tour.currentStep === 'searchIntro') { + tour.setStep('searchType'); + searchInputRef.current?.focus(); + } + }; + + const handleTourStart = () => { + if (!user) { + openLoginWarning(); + return; + } + + tour.start(tour.publishedAt); + navigate('/'); + window.scrollTo(0, 0); + }; + + const handleRegistrationNavClick = ( + e: React.MouseEvent + ) => { + handleProtectedClick(e); + if (!e.defaultPrevented && tour.isActive && tour.currentStep === 'cartGoRegistration') { + tour.setStep('registrationTimer'); + } + }; + + const handleEnrollmentHistoryNavClick = ( + e: React.MouseEvent + ) => { + handleProtectedClick(e); + if ( + !e.defaultPrevented && + tour.isActive && + tour.currentStep === 'registrationGoHistory' + ) { + tour.setStep('historyResult'); + } + }; + useEffect(() => { const isModalOpen = showLoginWarning || showNotSupported; document.body.style.overflow = isModalOpen ? 'hidden' : 'auto'; @@ -112,7 +169,7 @@ export default function Header({ handleLogout, onToggleSideMenu }: HeaderProps)
-
+
@@ -123,6 +180,8 @@ export default function Header({ handleLogout, onToggleSideMenu }: HeaderProps) placeholder="전체 강좌 검색은 돋보기 버튼을 클릭하세요" ref={searchInputRef} onKeyDown={handleKeyDown} + onClick={handleSearchInputClick} + data-tour-id="header-search-input" /> + )}
diff --git a/src/widgets/header/header.css b/src/widgets/header/header.css index 15e5fe0..02971b6 100644 --- a/src/widgets/header/header.css +++ b/src/widgets/header/header.css @@ -273,6 +273,12 @@ color: #222; font-weight: 400; } +.gnbButton { + border: none; + background: transparent; + cursor: pointer; + font-family: inherit; +} .gnbItem.active { color: #376dc8; text-underline-offset: 6px; diff --git a/vite.config.ts b/vite.config.ts index e38aef2..b918020 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,33 +1,39 @@ -import {defineConfig} from 'vite'; +import {defineConfig, loadEnv} from 'vite'; import react from '@vitejs/plugin-react'; import path from 'path'; // https://vite.dev/config/ -export default defineConfig({ - plugins: [ - react({ - babel: { - plugins: [['babel-plugin-react-compiler']], +export default defineConfig(({mode}) => { + const env = loadEnv(mode, process.cwd(), ''); + const apiProxyTarget = + env.VITE_API_PROXY_TARGET || 'https://snuclear.wafflestudio.com'; + + return { + plugins: [ + react({ + babel: { + plugins: [['babel-plugin-react-compiler']], + }, + }), + ], + resolve: { + alias: { + '@app': path.resolve(__dirname, './src/app'), + '@pages': path.resolve(__dirname, './src/pages'), + '@widgets': path.resolve(__dirname, './src/widgets'), + '@features': path.resolve(__dirname, './src/features'), + '@entities': path.resolve(__dirname, './src/entities'), + '@shared': path.resolve(__dirname, './src/shared'), }, - }), - ], - resolve: { - alias: { - '@app': path.resolve(__dirname, './src/app'), - '@pages': path.resolve(__dirname, './src/pages'), - '@widgets': path.resolve(__dirname, './src/widgets'), - '@features': path.resolve(__dirname, './src/features'), - '@entities': path.resolve(__dirname, './src/entities'), - '@shared': path.resolve(__dirname, './src/shared'), }, - }, - server: { - proxy: { - '/api': { - target: 'https://snuclear.com/', - changeOrigin: true, - secure: false, + server: { + proxy: { + '/api': { + target: apiProxyTarget, + changeOrigin: true, + secure: false, + }, }, }, - }, + }; });