diff --git a/public/images/landing/landing_promotion.png b/public/images/landing/landing_promotion.png new file mode 100644 index 0000000..a292016 Binary files /dev/null and b/public/images/landing/landing_promotion.png differ diff --git a/src/api/expert.ts b/src/api/expert.ts index 50f1a28..2576252 100644 --- a/src/api/expert.ts +++ b/src/api/expert.ts @@ -4,30 +4,19 @@ import { expertReportsResponse, getExpertReportsResponse, getExpertResponse, - getFeedBackExpertResponse, getUserExpertReportResponse, } from '@/types/expert/expert.type'; import api from './api'; -import { ExpertDetailResponse } from '@/types/expert/expert.detail'; +import { + ExpertDetailResponse, + ExpertReportDetailResponse, +} from '@/types/expert/expert.detail'; export async function GetExpert(): Promise { const res = await api.get<{ data: getExpertResponse[] }>('/v1/experts'); return res.data.data; } -export async function GetFeedBackExpert( - businessPlanId: number -): Promise { - if (!Number.isFinite(businessPlanId) || businessPlanId <= 0) { - throw new Error('유효하지 않는 아이디입니다.'); - } - const { data } = await api.get( - '/v1/expert-applications', - { params: { businessPlanId } } - ); - return data; -} - export async function ApplyFeedback({ businessPlanId, expertId, @@ -92,3 +81,16 @@ export async function GetExpertDetail( return res.data.data; } + +export async function GetExpertReportDetail( + expertId: number +): Promise { + const res = await api.get<{ data: ExpertReportDetailResponse[] }>( + `/v1/experts/${expertId}/business-plans/ai-reports`, + { + params: { expertId }, + } + ); + + return res.data.data; +} diff --git a/src/app/_components/landing/LandingChecklist.tsx b/src/app/_components/landing/LandingChecklist.tsx index d4e1490..e87109d 100644 --- a/src/app/_components/landing/LandingChecklist.tsx +++ b/src/app/_components/landing/LandingChecklist.tsx @@ -1,44 +1,52 @@ +'use client'; import Image from 'next/image'; -import React from 'react'; +import ArrowIcon from '@/assets/icons/chevron_right.svg'; +import { useRouter } from 'next/navigation'; const LandingChecklist = () => { + const router = useRouter(); return ( -
-
- 항목별 체크리스트{' '} -
-
- 스타라이트는 이런 분들께 추천드려요. -
+
+
+
+

+ 2026년 지원사업,
+ 사업계획서에서 탈락하지 않게 만드는 방법 +

-
- 랜딩 체크리스트1 +
+

+ 2026 지원사업 대비 모든 기능 무료 프로모션 (~1/10) +

- 랜딩 체크리스트2 +
+ {['10일', '4시간', '19분', '20초'].map((time) => ( +
+ {time} +
+ ))} +
+ +
+
랜딩 체크리스트3
diff --git a/src/app/_components/landing/LandingPaySection.tsx b/src/app/_components/landing/LandingPaySection.tsx new file mode 100644 index 0000000..032eac7 --- /dev/null +++ b/src/app/_components/landing/LandingPaySection.tsx @@ -0,0 +1,73 @@ +'use client'; +import Check from '@/assets/icons/big_check.svg'; +import RightIcon from '@/assets/icons/white_right.svg'; +import { useRouter } from 'next/navigation'; + +const LandingPaySection = () => { + const router = useRouter(); + return ( +
+
+ + + + + + + 300,000원{' '} + + / 시간당 비대면 멘토링 + + + +
+ +
+
+

Lite 이용권의 기능

+
+ +
+

+ 49,000원{' '} + + / 시간당 비대면 멘토링 + +

+ +
+ +

전문가 비대면 멘토링 1회

+
+ +
+
  • 사업계획서 PDF/텍스트 기반 심층 검토
  • +
  • 강·약점 구체 코멘트
  • +
  • AI 리포트 무제한 포함
  • +
    +
    + + + +
    +

    + *전문가 대면 멘토링 평균 약 30만 원 수준에서 구조 개선을 통해 최대 + 약 4.9만 원대까지 절감했습니다. +

    +

    + *전문가 대면 멘토링 평균 비용은 1시간 기준 일반적인 시장 시세를 + 참고하였습니다. +

    +
    +
    +
    + ); +}; + +export default LandingPaySection; diff --git a/src/app/_components/landing/LandingRelation.tsx b/src/app/_components/landing/LandingRelation.tsx index b431e85..1372aec 100644 --- a/src/app/_components/landing/LandingRelation.tsx +++ b/src/app/_components/landing/LandingRelation.tsx @@ -39,15 +39,6 @@ const LandingRelation = () => { return (
    - 랜딩 관련기관 -
    관련 기관 @@ -79,6 +70,15 @@ const LandingRelation = () => { ))}
    + + 랜딩 관련기관
    ); }; diff --git a/src/app/expert/components/ExpertCard.tsx b/src/app/expert/components/ExpertCard.tsx index 7828b57..0bf0870 100644 --- a/src/app/expert/components/ExpertCard.tsx +++ b/src/app/expert/components/ExpertCard.tsx @@ -1,46 +1,31 @@ 'use client'; -import React, { useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; import ExpertTab from './ExpertTab'; -import { useGetExpert, useGetFeedBackExpert } from '@/hooks/queries/useExpert'; +import { useGetExpert } from '@/hooks/queries/useExpert'; import { adaptMentor, MentorProps } from '@/types/expert/expert.props'; import MentorCard from './MentorCard'; -import { useBusinessStore } from '@/store/business.store'; import { TAB_LABELS, TabLabel } from '@/types/expert/label'; const ExpertCard = () => { const tabs = ['전체', ...TAB_LABELS]; const [activeTab, setActiveTab] = useState('전체'); - const businessPlanId = useBusinessStore((s) => s.planId); - const id = businessPlanId ?? undefined; - const { data: experts = [], isLoading: expertsLoading } = useGetExpert(); - const { data: feedback, isLoading: feedbackLoading } = useGetFeedBackExpert( - id, - { enabled: id !== undefined } - ); - - const expertsApply = useMemo( - () => new Set((feedback?.data ?? []).map(Number)), - [feedback] - ); const list = useMemo(() => { return experts.map((e) => { const mentor = adaptMentor(e); - const status: MentorProps['status'] = expertsApply.has(Number(e.id)) - ? 'done' - : 'active'; + const status: MentorProps['status'] = 'active'; return { ...mentor, status }; }); - }, [experts, expertsApply]); + }, [experts]); const filtered = activeTab === '전체' ? list : list.filter((m) => m.categories.includes(activeTab as TabLabel)); - if (expertsLoading || feedbackLoading) { + if (expertsLoading) { return (
    로딩 중
    ); diff --git a/src/app/expert/detail/components/BusinessPlanDropdown.tsx b/src/app/expert/detail/components/BusinessPlanDropdown.tsx index bb6b7e6..65a8b40 100644 --- a/src/app/expert/detail/components/BusinessPlanDropdown.tsx +++ b/src/app/expert/detail/components/BusinessPlanDropdown.tsx @@ -1,36 +1,36 @@ 'use client'; -import { useState, useRef, useEffect, useMemo } from 'react'; -import { useGetMyBusinessPlans } from '@/hooks/queries/useMy'; -import { BusinessPlanItem } from '@/types/mypage/mypage.type'; +import { useState, useRef, useEffect } from 'react'; +import { useExpertReportDetail } from '@/hooks/queries/useExpert'; import { useBusinessStore } from '@/store/business.store'; +import { useUserStore } from '@/store/user.store'; import DropDownIcon from '@/assets/icons/drop_down.svg'; import PurpleDropDownIcon from '@/assets/icons/puple_drop_down.svg'; -import { useGradeQueries } from '@/hooks/queries/useGradeQueries'; +import { ExpertReportDetailResponse } from '@/types/expert/expert.detail'; -const BusinessPlanDropdown = () => { +interface BusinessPlanDropdownProps { + expertId: number; + hasNoPlans?: boolean; +} + +const BusinessPlanDropdown = ({ + expertId, + hasNoPlans = false, +}: BusinessPlanDropdownProps) => { const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef(null); const planId = useBusinessStore((s) => s.planId); const setPlanId = useBusinessStore((s) => s.setPlanId); + const user = useUserStore((s) => s.user); - const { data: businessPlansData, isLoading } = useGetMyBusinessPlans({ - page: 1, - size: 100, - }); - - const allPlans: BusinessPlanItem[] = businessPlansData?.data?.content ?? []; - const gradeQueries = useGradeQueries(allPlans); - - const plans = useMemo(() => { - return allPlans.filter((plan, index) => { - const gradeData = gradeQueries[index]?.data; - const totalScore = gradeData?.data?.totalScore ?? 0; - return totalScore >= 70; - }); - }, [allPlans, gradeQueries]); + const { data: reportDetails = [], isLoading } = useExpertReportDetail( + expertId, + { enabled: !!user } + ); - const selectedPlan = plans.find((plan) => plan.businessPlanId === planId); + const selectedPlan = reportDetails.find( + (plan) => plan.businessPlanId === planId + ); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -48,14 +48,12 @@ const BusinessPlanDropdown = () => { }; }, []); - const handleSelect = (plan: BusinessPlanItem) => { + const handleSelect = (plan: ExpertReportDetailResponse) => { setPlanId(plan.businessPlanId); setIsOpen(false); }; - const isGradesLoading = gradeQueries.some((query) => query.isLoading); - - if (isLoading || isGradesLoading) { + if (isLoading) { return (
    로딩 중 @@ -76,9 +74,9 @@ const BusinessPlanDropdown = () => { > {selectedPlan - ? `${selectedPlan.title}` - : plans.length > 0 - ? `${plans[0].title}` + ? `${selectedPlan.businessPlanTitle}` + : hasNoPlans + ? '사업계획서를 먼저 작성해주세요.' : '사업계획서를 선택하세요'} {selectedPlan ? ( @@ -89,13 +87,13 @@ const BusinessPlanDropdown = () => { {isOpen && ( -
    - {plans.length === 0 ? ( +
    + {reportDetails.length === 0 ? (
    등록된 사업계획서가 없습니다.
    ) : ( - plans.map((plan) => { + reportDetails.map((plan) => { const isSelected = plan.businessPlanId === planId; return ( ); }) diff --git a/src/app/expert/detail/components/ExpertDetailSidebar.tsx b/src/app/expert/detail/components/ExpertDetailSidebar.tsx index bbb98d7..09c1223 100644 --- a/src/app/expert/detail/components/ExpertDetailSidebar.tsx +++ b/src/app/expert/detail/components/ExpertDetailSidebar.tsx @@ -5,7 +5,9 @@ import { useExpertStore } from '@/store/expert.store'; import { useBusinessStore } from '@/store/business.store'; import { useEvaluationStore } from '@/store/report.store'; import { useUserStore } from '@/store/user.store'; +import { useExpertReportDetail } from '@/hooks/queries/useExpert'; import GrayPlus from '@/assets/icons/gray_plus.svg'; +import GrayCheck from '@/assets/icons/gray_check.svg'; import WhitePlus from '@/assets/icons/white_plus.svg'; import BusinessPlanDropdown from './BusinessPlanDropdown'; import { ExpertDetailResponse } from '@/types/expert/expert.detail'; @@ -20,13 +22,56 @@ const ExpertDetailSidebar = ({ expert }: ExpertDetailSidebarProps) => { const planId = useBusinessStore((s) => s.planId); const hasExpertUnlocked = useEvaluationStore((s) => s.hasExpertUnlocked); const user = useUserStore((s) => s.user); - const isMember = !!user; + const hasAccessToken = + typeof window !== 'undefined' && !!localStorage.getItem('accessToken'); + const isMember = hasAccessToken && !!user; + + const { data: reportDetails = [], isLoading: isLoadingReports } = + useExpertReportDetail(expert.id, { + enabled: !!user, + }); + + const selectedPlan = reportDetails.find( + (plan) => plan.businessPlanId === planId + ); const canUseExpert = isMember && hasExpertUnlocked; - const disabled = !canUseExpert || !planId; + const isSelectedPlanOver70 = selectedPlan?.isOver70 ?? false; + const hasRequested = (selectedPlan?.requestCount ?? 0) > 0; + + const shouldShowCreateButton = !isMember + ? true + : !isLoadingReports && reportDetails.length === 0; + + const disabled = shouldShowCreateButton + ? false + : hasRequested || !canUseExpert || !planId || !isSelectedPlanOver70; + + const ButtonIcon = shouldShowCreateButton + ? WhitePlus + : hasRequested + ? GrayPlus + : disabled && !isSelectedPlanOver70 + ? GrayCheck + : disabled + ? GrayPlus + : WhitePlus; + + const buttonText = shouldShowCreateButton + ? '사업계획서 생성' + : hasRequested + ? '신청완료' + : '전문가 연결'; const handleConnect = () => { - if (!expert || disabled) return; + if (!expert) return; + + if (shouldShowCreateButton) { + router.push('/business'); + return; + } + + if (disabled) return; setSelectedMentor({ id: expert.id, @@ -55,27 +100,26 @@ const ExpertDetailSidebar = ({ expert }: ExpertDetailSidebarProps) => {
    - -

    + +

    * 70점 이상의 사업계획서만 전문가 연결이 가능해요.

    ); diff --git a/src/app/page.tsx b/src/app/page.tsx index d940e80..772740c 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,6 +1,7 @@ import Landing from './_components/landing/Landing'; import LandingBlackSection from './_components/landing/LandingBlackSection'; import LandingChecklist from './_components/landing/LandingChecklist'; +import LandingPaySection from './_components/landing/LandingPaySection'; import LandingRelation from './_components/landing/LandingRelation'; const page = () => { @@ -27,6 +28,7 @@ const page = () => { +
    ); diff --git a/src/app/pay/components/PaymentAmount.tsx b/src/app/pay/components/PaymentAmount.tsx index c64bc8a..3844796 100644 --- a/src/app/pay/components/PaymentAmount.tsx +++ b/src/app/pay/components/PaymentAmount.tsx @@ -18,13 +18,21 @@ const PaymentAmount = () => {
    -
    +
    총 결제 금액 - - {totalAmount.toLocaleString()}원 - +
    + + + + + + {totalAmount.toLocaleString()}원 + + + 0원 +
    diff --git a/src/assets/icons/big_check.svg b/src/assets/icons/big_check.svg new file mode 100644 index 0000000..028f120 --- /dev/null +++ b/src/assets/icons/big_check.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/icons/chevron_right.svg b/src/assets/icons/chevron_right.svg new file mode 100644 index 0000000..2f557a2 --- /dev/null +++ b/src/assets/icons/chevron_right.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/icons/white_right.svg b/src/assets/icons/white_right.svg new file mode 100644 index 0000000..28c2607 --- /dev/null +++ b/src/assets/icons/white_right.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/hooks/queries/useExpert.ts b/src/hooks/queries/useExpert.ts index 66ed895..dca12db 100644 --- a/src/hooks/queries/useExpert.ts +++ b/src/hooks/queries/useExpert.ts @@ -1,5 +1,8 @@ -import { GetExpert, GetExpertDetail, GetFeedBackExpert } from '@/api/expert'; -import { getFeedBackExpertResponse } from '@/types/expert/expert.type'; +import { + GetExpert, + GetExpertDetail, + GetExpertReportDetail, +} from '@/api/expert'; import { useQuery } from '@tanstack/react-query'; export function useGetExpert() { @@ -9,27 +12,6 @@ export function useGetExpert() { }); } -export function useGetFeedBackExpert( - businessPlanId?: number, - options?: { enabled?: boolean } -) { - const hasToken = - typeof window !== 'undefined' && !!localStorage.getItem('accessToken'); - - const hasPlanId = - typeof businessPlanId === 'number' && - businessPlanId > 0 && - (options?.enabled ?? true); - - const enabled = hasToken && hasPlanId; - - return useQuery({ - queryKey: ['GetFeedBackExpert', enabled ? businessPlanId : 'disabled'], - queryFn: () => GetFeedBackExpert(businessPlanId as number), - enabled, - }); -} - export function useExpertDetail(expertId: number) { return useQuery({ queryKey: ['GetExpertDetail', expertId], @@ -37,3 +19,15 @@ export function useExpertDetail(expertId: number) { enabled: expertId > 0, }); } + +export function useExpertReportDetail( + expertId: number, + options?: { enabled?: boolean } +) { + const hasToken = localStorage.getItem('accessToken'); + return useQuery({ + queryKey: ['GetExpertReportDetail', expertId, hasToken], + queryFn: () => GetExpertReportDetail(expertId), + enabled: expertId > 0 && !!hasToken && (options?.enabled ?? true), + }); +} diff --git a/src/types/expert/expert.detail.ts b/src/types/expert/expert.detail.ts index 7427bef..2002431 100644 --- a/src/types/expert/expert.detail.ts +++ b/src/types/expert/expert.detail.ts @@ -18,3 +18,10 @@ export interface ExpertDetailResponse { }[]; tags: string[]; } + +export interface ExpertReportDetailResponse { + businessPlanId: number; + businessPlanTitle: string; + requestCount: number; + isOver70: boolean; +} diff --git a/src/types/expert/expert.type.ts b/src/types/expert/expert.type.ts index c4c1abc..24e0db8 100644 --- a/src/types/expert/expert.type.ts +++ b/src/types/expert/expert.type.ts @@ -14,11 +14,6 @@ export interface getExpertResponse { workedPeriod: number; } -export interface getFeedBackExpertResponse { - result: string; - data: number[]; -} - export interface applyFeedBackProps { expertId: number; businessPlanId: number;