Skip to content

Commit 5fd511b

Browse files
authored
Merge pull request #90 from StartUpLight/SRLT-121-전문가-상세페이지-디테일
[SRLT-121] 전문가 상세페이지 왼쪽 사이드바 전문가 피드백 부분 연결
2 parents eafc618 + d7bf6d7 commit 5fd511b

File tree

7 files changed

+135
-110
lines changed

7 files changed

+135
-110
lines changed

src/api/expert.ts

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,19 @@ import {
44
expertReportsResponse,
55
getExpertReportsResponse,
66
getExpertResponse,
7-
getFeedBackExpertResponse,
87
getUserExpertReportResponse,
98
} from '@/types/expert/expert.type';
109
import api from './api';
11-
import { ExpertDetailResponse } from '@/types/expert/expert.detail';
10+
import {
11+
ExpertDetailResponse,
12+
ExpertReportDetailResponse,
13+
} from '@/types/expert/expert.detail';
1214

1315
export async function GetExpert(): Promise<getExpertResponse[]> {
1416
const res = await api.get<{ data: getExpertResponse[] }>('/v1/experts');
1517
return res.data.data;
1618
}
1719

18-
export async function GetFeedBackExpert(
19-
businessPlanId: number
20-
): Promise<getFeedBackExpertResponse> {
21-
if (!Number.isFinite(businessPlanId) || businessPlanId <= 0) {
22-
throw new Error('유효하지 않는 아이디입니다.');
23-
}
24-
const { data } = await api.get<getFeedBackExpertResponse>(
25-
'/v1/expert-applications',
26-
{ params: { businessPlanId } }
27-
);
28-
return data;
29-
}
30-
3120
export async function ApplyFeedback({
3221
businessPlanId,
3322
expertId,
@@ -92,3 +81,16 @@ export async function GetExpertDetail(
9281

9382
return res.data.data;
9483
}
84+
85+
export async function GetExpertReportDetail(
86+
expertId: number
87+
): Promise<ExpertReportDetailResponse[]> {
88+
const res = await api.get<{ data: ExpertReportDetailResponse[] }>(
89+
`/v1/experts/${expertId}/business-plans/ai-reports`,
90+
{
91+
params: { expertId },
92+
}
93+
);
94+
95+
return res.data.data;
96+
}

src/app/expert/components/ExpertCard.tsx

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,31 @@
11
'use client';
2-
import React, { useMemo, useState } from 'react';
2+
import { useMemo, useState } from 'react';
33
import ExpertTab from './ExpertTab';
4-
import { useGetExpert, useGetFeedBackExpert } from '@/hooks/queries/useExpert';
4+
import { useGetExpert } from '@/hooks/queries/useExpert';
55
import { adaptMentor, MentorProps } from '@/types/expert/expert.props';
66
import MentorCard from './MentorCard';
7-
import { useBusinessStore } from '@/store/business.store';
87
import { TAB_LABELS, TabLabel } from '@/types/expert/label';
98

109
const ExpertCard = () => {
1110
const tabs = ['전체', ...TAB_LABELS];
1211
const [activeTab, setActiveTab] = useState('전체');
1312

14-
const businessPlanId = useBusinessStore((s) => s.planId);
15-
const id = businessPlanId ?? undefined;
16-
1713
const { data: experts = [], isLoading: expertsLoading } = useGetExpert();
18-
const { data: feedback, isLoading: feedbackLoading } = useGetFeedBackExpert(
19-
id,
20-
{ enabled: id !== undefined }
21-
);
22-
23-
const expertsApply = useMemo(
24-
() => new Set<number>((feedback?.data ?? []).map(Number)),
25-
[feedback]
26-
);
2714

2815
const list = useMemo(() => {
2916
return experts.map((e) => {
3017
const mentor = adaptMentor(e);
31-
const status: MentorProps['status'] = expertsApply.has(Number(e.id))
32-
? 'done'
33-
: 'active';
18+
const status: MentorProps['status'] = 'active';
3419
return { ...mentor, status };
3520
});
36-
}, [experts, expertsApply]);
21+
}, [experts]);
3722

3823
const filtered =
3924
activeTab === '전체'
4025
? list
4126
: list.filter((m) => m.categories.includes(activeTab as TabLabel));
4227

43-
if (expertsLoading || feedbackLoading) {
28+
if (expertsLoading) {
4429
return (
4530
<div className="ds-subtext mt-10 text-center text-gray-600">로딩 중</div>
4631
);

src/app/expert/detail/components/BusinessPlanDropdown.tsx

Lines changed: 30 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,36 @@
11
'use client';
22

3-
import { useState, useRef, useEffect, useMemo } from 'react';
4-
import { useGetMyBusinessPlans } from '@/hooks/queries/useMy';
5-
import { BusinessPlanItem } from '@/types/mypage/mypage.type';
3+
import { useState, useRef, useEffect } from 'react';
4+
import { useExpertReportDetail } from '@/hooks/queries/useExpert';
65
import { useBusinessStore } from '@/store/business.store';
6+
import { useUserStore } from '@/store/user.store';
77
import DropDownIcon from '@/assets/icons/drop_down.svg';
88
import PurpleDropDownIcon from '@/assets/icons/puple_drop_down.svg';
9-
import { useGradeQueries } from '@/hooks/queries/useGradeQueries';
9+
import { ExpertReportDetailResponse } from '@/types/expert/expert.detail';
1010

11-
const BusinessPlanDropdown = () => {
11+
interface BusinessPlanDropdownProps {
12+
expertId: number;
13+
hasNoPlans?: boolean;
14+
}
15+
16+
const BusinessPlanDropdown = ({
17+
expertId,
18+
hasNoPlans = false,
19+
}: BusinessPlanDropdownProps) => {
1220
const [isOpen, setIsOpen] = useState(false);
1321
const dropdownRef = useRef<HTMLDivElement>(null);
1422
const planId = useBusinessStore((s) => s.planId);
1523
const setPlanId = useBusinessStore((s) => s.setPlanId);
24+
const user = useUserStore((s) => s.user);
1625

17-
const { data: businessPlansData, isLoading } = useGetMyBusinessPlans({
18-
page: 1,
19-
size: 100,
20-
});
21-
22-
const allPlans: BusinessPlanItem[] = businessPlansData?.data?.content ?? [];
23-
const gradeQueries = useGradeQueries(allPlans);
24-
25-
const plans = useMemo(() => {
26-
return allPlans.filter((plan, index) => {
27-
const gradeData = gradeQueries[index]?.data;
28-
const totalScore = gradeData?.data?.totalScore ?? 0;
29-
return totalScore >= 70;
30-
});
31-
}, [allPlans, gradeQueries]);
26+
const { data: reportDetails = [], isLoading } = useExpertReportDetail(
27+
expertId,
28+
{ enabled: !!user }
29+
);
3230

33-
const selectedPlan = plans.find((plan) => plan.businessPlanId === planId);
31+
const selectedPlan = reportDetails.find(
32+
(plan) => plan.businessPlanId === planId
33+
);
3434

3535
useEffect(() => {
3636
const handleClickOutside = (event: MouseEvent) => {
@@ -48,14 +48,12 @@ const BusinessPlanDropdown = () => {
4848
};
4949
}, []);
5050

51-
const handleSelect = (plan: BusinessPlanItem) => {
51+
const handleSelect = (plan: ExpertReportDetailResponse) => {
5252
setPlanId(plan.businessPlanId);
5353
setIsOpen(false);
5454
};
5555

56-
const isGradesLoading = gradeQueries.some((query) => query.isLoading);
57-
58-
if (isLoading || isGradesLoading) {
56+
if (isLoading) {
5957
return (
6058
<div className="w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-[13px] text-gray-800">
6159
로딩 중
@@ -76,9 +74,9 @@ const BusinessPlanDropdown = () => {
7674
>
7775
<span>
7876
{selectedPlan
79-
? `${selectedPlan.title}`
80-
: plans.length > 0
81-
? `${plans[0].title}`
77+
? `${selectedPlan.businessPlanTitle}`
78+
: hasNoPlans
79+
? '사업계획서를 먼저 작성해주세요.'
8280
: '사업계획서를 선택하세요'}
8381
</span>
8482
{selectedPlan ? (
@@ -89,13 +87,13 @@ const BusinessPlanDropdown = () => {
8987
</button>
9088

9189
{isOpen && (
92-
<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)]">
93-
{plans.length === 0 ? (
90+
<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)]">
91+
{reportDetails.length === 0 ? (
9492
<div className="ds-subtext px-3 py-2 font-medium text-gray-800">
9593
등록된 사업계획서가 없습니다.
9694
</div>
9795
) : (
98-
plans.map((plan) => {
96+
reportDetails.map((plan) => {
9997
const isSelected = plan.businessPlanId === planId;
10098
return (
10199
<button
@@ -108,7 +106,7 @@ const BusinessPlanDropdown = () => {
108106
: 'hover:bg-primary-50 bg-white text-gray-800'
109107
}`}
110108
>
111-
{plan.title}
109+
{plan.businessPlanTitle}
112110
</button>
113111
);
114112
})

src/app/expert/detail/components/ExpertDetailSidebar.tsx

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import { useExpertStore } from '@/store/expert.store';
55
import { useBusinessStore } from '@/store/business.store';
66
import { useEvaluationStore } from '@/store/report.store';
77
import { useUserStore } from '@/store/user.store';
8+
import { useExpertReportDetail } from '@/hooks/queries/useExpert';
89
import GrayPlus from '@/assets/icons/gray_plus.svg';
10+
import GrayCheck from '@/assets/icons/gray_check.svg';
911
import WhitePlus from '@/assets/icons/white_plus.svg';
1012
import BusinessPlanDropdown from './BusinessPlanDropdown';
1113
import { ExpertDetailResponse } from '@/types/expert/expert.detail';
@@ -20,13 +22,56 @@ const ExpertDetailSidebar = ({ expert }: ExpertDetailSidebarProps) => {
2022
const planId = useBusinessStore((s) => s.planId);
2123
const hasExpertUnlocked = useEvaluationStore((s) => s.hasExpertUnlocked);
2224
const user = useUserStore((s) => s.user);
23-
const isMember = !!user;
25+
const hasAccessToken =
26+
typeof window !== 'undefined' && !!localStorage.getItem('accessToken');
27+
const isMember = hasAccessToken && !!user;
28+
29+
const { data: reportDetails = [], isLoading: isLoadingReports } =
30+
useExpertReportDetail(expert.id, {
31+
enabled: !!user,
32+
});
33+
34+
const selectedPlan = reportDetails.find(
35+
(plan) => plan.businessPlanId === planId
36+
);
2437

2538
const canUseExpert = isMember && hasExpertUnlocked;
26-
const disabled = !canUseExpert || !planId;
39+
const isSelectedPlanOver70 = selectedPlan?.isOver70 ?? false;
40+
const hasRequested = (selectedPlan?.requestCount ?? 0) > 0;
41+
42+
const shouldShowCreateButton = !isMember
43+
? true
44+
: !isLoadingReports && reportDetails.length === 0;
45+
46+
const disabled = shouldShowCreateButton
47+
? false
48+
: hasRequested || !canUseExpert || !planId || !isSelectedPlanOver70;
49+
50+
const ButtonIcon = shouldShowCreateButton
51+
? WhitePlus
52+
: hasRequested
53+
? GrayPlus
54+
: disabled && !isSelectedPlanOver70
55+
? GrayCheck
56+
: disabled
57+
? GrayPlus
58+
: WhitePlus;
59+
60+
const buttonText = shouldShowCreateButton
61+
? '사업계획서 생성'
62+
: hasRequested
63+
? '신청완료'
64+
: '전문가 연결';
2765

2866
const handleConnect = () => {
29-
if (!expert || disabled) return;
67+
if (!expert) return;
68+
69+
if (shouldShowCreateButton) {
70+
router.push('/business');
71+
return;
72+
}
73+
74+
if (disabled) return;
3075

3176
setSelectedMentor({
3277
id: expert.id,
@@ -55,27 +100,26 @@ const ExpertDetailSidebar = ({ expert }: ExpertDetailSidebarProps) => {
55100
</div>
56101

57102
<div className="mt-8 w-full">
58-
<BusinessPlanDropdown />
59-
<p className="ds-caption mt-2 font-medium text-gray-600">
103+
<BusinessPlanDropdown
104+
expertId={expert.id}
105+
hasNoPlans={shouldShowCreateButton}
106+
/>
107+
<p className="ds-caption text-primary-500 mt-2 font-medium">
60108
* 70점 이상의 사업계획서만 전문가 연결이 가능해요.
61109
</p>
62110
</div>
63111

64112
<button
65113
onClick={handleConnect}
66-
disabled={disabled}
114+
disabled={shouldShowCreateButton ? false : disabled}
67115
className={`ds-text mt-8 flex w-full items-center justify-center gap-1 rounded-lg px-8 py-[10px] font-medium ${
68-
disabled
69-
? 'cursor-not-allowed bg-gray-200 text-gray-500'
70-
: 'bg-primary-500 hover:bg-primary-700 cursor-pointer text-white'
116+
shouldShowCreateButton || (!disabled && !hasRequested)
117+
? 'bg-primary-500 hover:bg-primary-700 cursor-pointer text-white'
118+
: 'cursor-not-allowed bg-gray-200 text-gray-500'
71119
}`}
72120
>
73-
{disabled ? (
74-
<GrayPlus className="h-5 w-5 shrink-0" />
75-
) : (
76-
<WhitePlus className="h-5 w-5 shrink-0" />
77-
)}
78-
<span>전문가 연결</span>
121+
<ButtonIcon className="h-5 w-5 shrink-0" />
122+
<span>{buttonText}</span>
79123
</button>
80124
</aside>
81125
);

src/hooks/queries/useExpert.ts

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
import { GetExpert, GetExpertDetail, GetFeedBackExpert } from '@/api/expert';
2-
import { getFeedBackExpertResponse } from '@/types/expert/expert.type';
1+
import {
2+
GetExpert,
3+
GetExpertDetail,
4+
GetExpertReportDetail,
5+
} from '@/api/expert';
36
import { useQuery } from '@tanstack/react-query';
47

58
export function useGetExpert() {
@@ -9,31 +12,22 @@ export function useGetExpert() {
912
});
1013
}
1114

12-
export function useGetFeedBackExpert(
13-
businessPlanId?: number,
14-
options?: { enabled?: boolean }
15-
) {
16-
const hasToken =
17-
typeof window !== 'undefined' && !!localStorage.getItem('accessToken');
18-
19-
const hasPlanId =
20-
typeof businessPlanId === 'number' &&
21-
businessPlanId > 0 &&
22-
(options?.enabled ?? true);
23-
24-
const enabled = hasToken && hasPlanId;
25-
26-
return useQuery<getFeedBackExpertResponse>({
27-
queryKey: ['GetFeedBackExpert', enabled ? businessPlanId : 'disabled'],
28-
queryFn: () => GetFeedBackExpert(businessPlanId as number),
29-
enabled,
30-
});
31-
}
32-
3315
export function useExpertDetail(expertId: number) {
3416
return useQuery({
3517
queryKey: ['GetExpertDetail', expertId],
3618
queryFn: () => GetExpertDetail(expertId),
3719
enabled: expertId > 0,
3820
});
3921
}
22+
23+
export function useExpertReportDetail(
24+
expertId: number,
25+
options?: { enabled?: boolean }
26+
) {
27+
const hasToken = localStorage.getItem('accessToken');
28+
return useQuery({
29+
queryKey: ['GetExpertReportDetail', expertId, hasToken],
30+
queryFn: () => GetExpertReportDetail(expertId),
31+
enabled: expertId > 0 && !!hasToken && (options?.enabled ?? true),
32+
});
33+
}

src/types/expert/expert.detail.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,10 @@ export interface ExpertDetailResponse {
1818
}[];
1919
tags: string[];
2020
}
21+
22+
export interface ExpertReportDetailResponse {
23+
businessPlanId: number;
24+
businessPlanTitle: string;
25+
requestCount: number;
26+
isOver70: boolean;
27+
}

0 commit comments

Comments
 (0)