Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added public/images/landing/landing_promotion.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 17 additions & 15 deletions src/api/expert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<getExpertResponse[]> {
const res = await api.get<{ data: getExpertResponse[] }>('/v1/experts');
return res.data.data;
}

export async function GetFeedBackExpert(
businessPlanId: number
): Promise<getFeedBackExpertResponse> {
if (!Number.isFinite(businessPlanId) || businessPlanId <= 0) {
throw new Error('유효하지 않는 아이디입니다.');
}
const { data } = await api.get<getFeedBackExpertResponse>(
'/v1/expert-applications',
{ params: { businessPlanId } }
);
return data;
}

export async function ApplyFeedback({
businessPlanId,
expertId,
Expand Down Expand Up @@ -92,3 +81,16 @@ export async function GetExpertDetail(

return res.data.data;
}

export async function GetExpertReportDetail(
expertId: number
): Promise<ExpertReportDetailResponse[]> {
const res = await api.get<{ data: ExpertReportDetailResponse[] }>(
`/v1/experts/${expertId}/business-plans/ai-reports`,
{
params: { expertId },
}
);

return res.data.data;
}
66 changes: 34 additions & 32 deletions src/app/_components/landing/LandingChecklist.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,46 @@
import Image from 'next/image';
import React from 'react';
import ArrowIcon from '@/assets/icons/chevron_right.svg';

const LandingChecklist = () => {
return (
<div className="h-[897px] bg-white px-[132px] py-[120px]">
<div className="ds-heading gradation inline-block font-semibold">
항목별 체크리스트{' '}
</div>
<div className="mt-3 text-[42px] leading-[150%] font-bold tracking-[-0.84px] text-black">
스타라이트는 이런 분들께 추천드려요.
</div>
<div className="w-full bg-black px-[132px] py-40">
<div className="flex w-full flex-row gap-[61px]">
<div className="flex flex-col gap-[100px]">
<h2 className="text-[42px] leading-[150%] font-bold text-white">
2026년 지원사업, <br />
사업계획서에서 탈락하지 않게 만드는 방법
</h2>

<div className="mt-[60px] flex w-full flex-row items-start justify-center gap-9">
<Image
src="/images/landing/landing_checklist_1.png"
alt="랜딩 체크리스트1"
width={368}
height={480}
className="h-[480px] w-[368px]"
priority
unoptimized={true}
/>
<div className="flex flex-col gap-6">
<p className="ds-title font-semibold text-gray-300">
2026 지원사업 대비 모든 기능 무료 프로모션 (~1/10)
</p>

<Image
src="/images/landing/landing_checklist_2.png"
alt="랜딩 체크리스트2"
width={368}
height={480}
className="h-[480px] w-[368px]"
priority
unoptimized={true}
/>
<div className="flex flex-row items-start gap-3">
{['10일', '4시간', '19분', '20초'].map((time) => (
<div
key={time}
className="ds-heading flex h-[86px] w-[120px] items-center justify-center rounded-lg bg-gray-900 py-10 font-semibold text-white"
>
{time}
</div>
))}
</div>
Comment on lines +17 to +31
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check if the file exists and read the content
wc -l src/app/_components/landing/LandingChecklist.tsx

Repository: StartUpLight/STARLIGHT_FE

Length of output: 120


🏁 Script executed:

# Read the relevant lines and surrounding context
cat -n src/app/_components/landing/LandingChecklist.tsx | head -40

Repository: StartUpLight/STARLIGHT_FE

Length of output: 1822


🏁 Script executed:

# Search for any existing timer or countdown logic in the file
rg -A 5 -B 5 "useEffect|useState|setInterval|countdown|timer" src/app/_components/landing/LandingChecklist.tsx

Repository: StartUpLight/STARLIGHT_FE

Length of output: 51


🏁 Script executed:

# Search for any other timer/countdown implementations in the project
rg -i "countdown|timer|setInterval|endDate" src/ --type tsx --type ts -A 3 | head -50

Repository: StartUpLight/STARLIGHT_FE

Length of output: 96


🏁 Script executed:

# Check if there are other similar promotional sections with timers
rg "map.*time|days.*hours.*minutes.*seconds" src/ --type tsx -B 2 -A 2

Repository: StartUpLight/STARLIGHT_FE

Length of output: 96


🏁 Script executed:

# Search for any timer/countdown implementations without file type specification
rg -i "countdown|setInterval|endDate|useEffect.*timer" src/ -A 3 | head -80

Repository: StartUpLight/STARLIGHT_FE

Length of output: 467


🏁 Script executed:

# Check component structure more broadly for patterns
rg "use client|'use client'" src/app/_components/ | head -20

Repository: StartUpLight/STARLIGHT_FE

Length of output: 582


정적 타이머를 동적 카운트다운으로 구현하거나 제거하세요.

현재 타이머 값(['10일', '4시간', '19분', '20초'])이 하드코딩되어 있어 실제로 카운트다운되지 않습니다. 사용자는 타이머가 실시간으로 업데이트될 것으로 기대하므로, 이는 혼란스러운 UX를 제공합니다.

프로모션 종료 시점(~1/10)까지 실제 카운트다운 로직을 구현하거나, 타이머 UI를 제거하고 정적 텍스트로 표시하는 것을 권장합니다. 카운트다운을 구현하는 경우 'use client' 지시문을 추가하여 클라이언트 컴포넌트로 전환해야 합니다.

🤖 Prompt for AI Agents
In src/app/_components/landing/LandingChecklist.tsx around lines 14 to 28, the
countdown blocks use a hardcoded array ['10일','4시간','19분','20초'] and must be
replaced with either a real client-side countdown or a static label: to
implement a live countdown, convert the file to a client component by adding
'use client' at the top, create state (e.g., remainingMs) and a useEffect that
sets an interval (1000ms) to compute time left until the promotion end date
(~Jan 10) by subtracting Date.now() from the target Date, derive
days/hours/minutes/seconds from remainingMs, update state on each tick and clear
the interval on unmount, then render the dynamic values instead of the hardcoded
array; alternatively, if you prefer to keep it static, remove the mapped array
and replace it with a single descriptive text element showing the end date or a
non-updating label.

<button className="ds-title flex h-[64px] w-[516px] cursor-pointer items-center justify-center rounded-lg bg-white px-8 font-semibold text-gray-900">
2026 지원사업 준비 시작하기
<ArrowIcon />
</button>
</div>
</div>

<Image
src="/images/landing/landing_checklist_3.png"
alt="랜딩 체크리스트3"
width={368}
height={480}
className="h-[480px] w-[368px]"
src="/images/landing/landing_promotion.png"
alt="프로모션 이미지"
width={479}
height={412}
className="h-[412px] w-[479px]"
priority
quality={100}
unoptimized={true}
/>
</div>
Expand Down
67 changes: 67 additions & 0 deletions src/app/_components/landing/LandingPaySection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import Check from '@/assets/icons/big_check.svg';
import RightIcon from '@/assets/icons/white_right.svg';

const LandingPaySection = () => {
return (
<div className="flex h-[978px] w-full flex-col items-center justify-center gap-[67px] px-[375px] py-[120px]">
<div className="text-[52px] leading-[150%] font-semibold text-gray-900">
<span className="relative inline-block">
<span className="absolute inset-0 flex items-center">
<span className="bg-primary-500 h-[5px] w-full"></span>
</span>

<span className="relative">
300,000원{' '}
<span className="text-[32px] font-medium text-gray-700">
/ 시간당 비대면 멘토링
</span>
</span>
</span>
</div>

<div className="flex w-full flex-col items-center">
<div className="bg-primary-500 inline-flex w-full items-center justify-center rounded-t-2xl py-3">
<p className="ds-title font-medium text-white">Lite 이용권의 기능</p>
</div>

<div className="flex w-full flex-col rounded-b-2xl bg-gray-900 px-[60px] py-[50px]">
<p className="text-[48px] font-semibold text-white">
49,000원{' '}
<span className="text-[24px] font-medium text-gray-300">
/ 시간당 비대면 멘토링
</span>
</p>

<div className="mt-5 flex flex-row items-center gap-[6px]">
<Check />
<p className="text-[22px] text-white">전문가 비대면 멘토링 1회</p>
</div>

<div className="ds-subtitle mt-3 flex w-full flex-col gap-1 px-6 font-medium text-gray-300">
<li>사업계획서 PDF/텍스트 기반 심층 검토</li>
<li>강·약점 구체 코멘트</li>
<li>AI 리포트 무제한 포함</li>
</div>
</div>

<button className="bg-primary-500 hover:bg-primary-700 mt-12 flex h-[64px] w-[516px] cursor-pointer items-center justify-center rounded-lg px-8">
<p className="ds-title font-semibold text-white">구매하기</p>
<RightIcon />
</button>

<div className="mt-12 flex flex-col">
<p className="ds-text font-normal text-gray-600">
*전문가 대면 멘토링 평균 약 30만 원 수준에서 구조 개선을 통해 최대
약 4.9만 원대까지 절감했습니다.
</p>
<p className="ds-text text-center font-normal text-gray-600">
*전문가 대면 멘토링 평균 비용은 1시간 기준 일반적인 시장 시세를
참고하였습니다.
</p>
</div>
</div>
</div>
);
};

export default LandingPaySection;
18 changes: 9 additions & 9 deletions src/app/_components/landing/LandingRelation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,6 @@ const LandingRelation = () => {

return (
<div>
<Image
src="/images/landing/landing_final.png"
alt="랜딩 관련기관"
width={1440}
height={420}
className="w-full"
priority
/>

<div className="mt-[119px] flex w-full flex-col bg-white px-[132px] pb-[235px]">
<div className="text-[42px] leading-[150%] font-bold tracking-[-0.84px] text-gray-900">
관련 기관
Expand Down Expand Up @@ -79,6 +70,15 @@ const LandingRelation = () => {
))}
</ul>
</div>

<Image
src="/images/landing/landing_final.png"
alt="랜딩 관련기관"
width={1440}
height={420}
className="w-full"
priority
/>
</div>
);
};
Expand Down
25 changes: 5 additions & 20 deletions src/app/expert/components/ExpertCard.tsx
Original file line number Diff line number Diff line change
@@ -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<number>((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 (
<div className="ds-subtext mt-10 text-center text-gray-600">로딩 중</div>
);
Expand Down
62 changes: 30 additions & 32 deletions src/app/expert/detail/components/BusinessPlanDropdown.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>(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) => {
Expand All @@ -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 (
<div className="w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-[13px] text-gray-800">
로딩 중
Expand All @@ -76,9 +74,9 @@ const BusinessPlanDropdown = () => {
>
<span>
{selectedPlan
? `${selectedPlan.title}`
: plans.length > 0
? `${plans[0].title}`
? `${selectedPlan.businessPlanTitle}`
: hasNoPlans
? '사업계획서를 먼저 작성해주세요.'
: '사업계획서를 선택하세요'}
</span>
{selectedPlan ? (
Expand All @@ -89,13 +87,13 @@ const BusinessPlanDropdown = () => {
</button>

{isOpen && (
<div className="absolute z-50 mt-2 h-[222px] w-[276px] overflow-y-auto rounded-lg bg-white shadow-[0_0_10px_0_rgba(0,0,0,0.10)]">
{plans.length === 0 ? (
<div className="absolute z-50 mt-2 max-h-[300px] w-[276px] overflow-y-auto rounded-lg bg-white shadow-[0_0_10px_0_rgba(0,0,0,0.10)]">
{reportDetails.length === 0 ? (
<div className="ds-subtext px-3 py-2 font-medium text-gray-800">
등록된 사업계획서가 없습니다.
</div>
) : (
plans.map((plan) => {
reportDetails.map((plan) => {
const isSelected = plan.businessPlanId === planId;
return (
<button
Expand All @@ -108,7 +106,7 @@ const BusinessPlanDropdown = () => {
: 'hover:bg-primary-50 bg-white text-gray-800'
}`}
>
{plan.title}
{plan.businessPlanTitle}
</button>
);
})
Expand Down
Loading