-
Notifications
You must be signed in to change notification settings - Fork 2
dev 최신화 #92
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
[SRLT-121] 전문가 상세페이지 왼쪽 사이드바 전문가 피드백 부분 연결
Walkthrough피드백 기반 API와 훅을 제거하고 전문가 리포트 상세 API/타입/훅으로 대체했으며, 이를 사용하는 여러 전문가 관련 컴포넌트와 랜딩 페이지에 새로운 결제 섹션을 추가 및 레이아웃을 조정했습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant UI as Client UI (Sidebar / Dropdown)
participant Hook as useExpertReportDetail
participant API as GetExpertReportDetail (frontend API)
participant Server as Experts Service
Note over UI,Hook: 사용자 상호작용 — 전문가 상세 페이지 로드 / 드롭다운 열기
UI->>Hook: useExpertReportDetail(expertId) 요청 (enabled: token present)
Hook->>API: 호출 GetExpertReportDetail(expertId)
API->>Server: GET /v1/experts/{expertId}/business-plans/ai-reports
Server-->>API: 200 OK (list of ExpertReportDetailResponse)
API-->>Hook: 반환된 데이터 배열
Hook-->>UI: 쿼리 결과 전달 (reportDetails)
alt reportDetails 존재
UI->>UI: 렌더 드롭다운 항목 (businessPlanTitle)
UI->>UI: 선택 시 isOver70 등 검사 및 버튼 상태 결정
else 빈 상태
UI->>UI: 빈 상태 메시지 / 생성 흐름 제안
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested labels
Suggested reviewers
시
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
🧹 Nitpick comments (6)
src/app/_components/landing/LandingPaySection.tsx (2)
6-6: 반응형 레이아웃 개선을 권장합니다.고정된 수평 패딩
px-[375px]는 작은 화면에서 레이아웃 문제나 가로 스크롤을 유발할 수 있습니다. 다양한 화면 크기에 대응하기 위해 반응형 패딩이나 max-width 패턴을 사용하는 것을 권장합니다.🔎 반응형 레이아웃 개선 예시
- <div className="flex h-[978px] w-full flex-col items-center justify-center gap-[67px] px-[375px] py-[120px]"> + <div className="flex h-auto min-h-[978px] w-full flex-col items-center justify-center gap-[67px] px-6 py-[120px] sm:px-12 lg:px-[132px] xl:px-[375px]">
22-45: LGTM! 향후 컨텐츠 관리 개선 고려가격 카드가 깔끔하게 구현되었습니다. 현재 가격과 기능이 하드코딩되어 있는데, 랜딩 페이지의 특성상 허용 가능하지만, 향후 컨텐츠를 자주 변경해야 하거나 A/B 테스트가 필요한 경우 props나 CMS 기반 구조로 리팩토링을 고려해볼 수 있습니다.
src/api/expert.ts (1)
85-96: 중복된params전달 확인 필요
expertId가 이미 URL 경로(/v1/experts/${expertId}/business-plans/ai-reports)에 포함되어 있으므로, Line 91의params: { expertId }는 불필요합니다. 이는 동일한 ID를 경로와 쿼리 파라미터로 두 번 전송하게 됩니다.🔎 제안된 수정
export async function GetExpertReportDetail( expertId: number ): Promise<ExpertReportDetailResponse[]> { const res = await api.get<{ data: ExpertReportDetailResponse[] }>( - `/v1/experts/${expertId}/business-plans/ai-reports`, - { - params: { expertId }, - } + `/v1/experts/${expertId}/business-plans/ai-reports` ); return res.data.data; }참고:
GetExpertDetail함수(Line 72-83)에서도 동일한 패턴이 사용되고 있어, 의도된 것일 수 있습니다. 백엔드 API 요구사항을 확인해 주세요.src/app/expert/detail/components/ExpertDetailSidebar.tsx (2)
50-64: 복잡한 중첩 삼항 연산자 가독성 개선 제안
ButtonIcon과buttonText의 중첩 삼항 연산자가 가독성을 저하시킵니다. 유지보수성을 위해 헬퍼 함수나 객체 맵핑으로 리팩토링을 고려해 주세요.🔎 제안된 리팩토링 예시
const getButtonConfig = () => { if (shouldShowCreateButton) { return { icon: WhitePlus, text: '사업계획서 생성' }; } if (hasRequested) { return { icon: GrayPlus, text: '신청완료' }; } if (disabled && !isSelectedPlanOver70) { return { icon: GrayCheck, text: '전문가 연결' }; } if (disabled) { return { icon: GrayPlus, text: '전문가 연결' }; } return { icon: WhitePlus, text: '전문가 연결' }; }; const { icon: ButtonIcon, text: buttonText } = getButtonConfig();
29-32: 동일 데이터 중복 fetch 검토
ExpertDetailSidebar와BusinessPlanDropdown모두useExpertReportDetail(expert.id)를 호출합니다. React Query가 동일한 queryKey로 요청을 중복 제거하지만,reportDetails를 props로 전달하면 코드 중복을 줄이고 컴포넌트 간 일관성을 보장할 수 있습니다.현재 구조가 컴포넌트 독립성을 위해 의도된 것인지 확인해 주세요. 의도된 것이 아니라면 데이터를 props로 전달하는 방식을 고려해 보세요.
Also applies to: 103-106
src/app/expert/detail/components/BusinessPlanDropdown.tsx (1)
66-87:hasNoPlans가 true일 때 드롭다운 비활성화 검토
hasNoPlans가 true일 때도 드롭다운을 클릭할 수 있어 빈 목록이 표시됩니다. 사용자 경험 개선을 위해 해당 경우 드롭다운 버튼을 비활성화하거나 클릭을 방지하는 것을 고려해 보세요.🔎 제안된 수정
<button type="button" - onClick={() => setIsOpen(!isOpen)} + onClick={() => !hasNoPlans && setIsOpen(!isOpen)} + disabled={hasNoPlans} className={`ds-subtext flex w-full cursor-pointer items-center justify-between rounded-lg px-3 py-2 text-left font-medium outline-none ${ selectedPlan ? 'bg-primary-50 text-primary-500' - : 'bg-gray-100 text-gray-500' + : hasNoPlans + ? 'cursor-not-allowed bg-gray-100 text-gray-400' + : 'bg-gray-100 text-gray-500' }`} >
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (4)
public/images/landing/landing_promotion.pngis excluded by!**/*.pngsrc/assets/icons/big_check.svgis excluded by!**/*.svgsrc/assets/icons/chevron_right.svgis excluded by!**/*.svgsrc/assets/icons/white_right.svgis excluded by!**/*.svg
📒 Files selected for processing (11)
src/api/expert.tssrc/app/_components/landing/LandingChecklist.tsxsrc/app/_components/landing/LandingPaySection.tsxsrc/app/_components/landing/LandingRelation.tsxsrc/app/expert/components/ExpertCard.tsxsrc/app/expert/detail/components/BusinessPlanDropdown.tsxsrc/app/expert/detail/components/ExpertDetailSidebar.tsxsrc/app/page.tsxsrc/hooks/queries/useExpert.tssrc/types/expert/expert.detail.tssrc/types/expert/expert.type.ts
💤 Files with no reviewable changes (1)
- src/types/expert/expert.type.ts
🧰 Additional context used
🧬 Code graph analysis (5)
src/app/expert/components/ExpertCard.tsx (3)
src/types/expert/label.ts (2)
TAB_LABELS(1-7)TabLabel(9-9)src/hooks/queries/useExpert.ts (1)
useGetExpert(8-13)src/types/expert/expert.props.ts (2)
adaptMentor(29-39)MentorProps(4-14)
src/api/expert.ts (1)
src/types/expert/expert.detail.ts (1)
ExpertReportDetailResponse(22-27)
src/app/expert/detail/components/BusinessPlanDropdown.tsx (4)
src/store/business.store.ts (1)
useBusinessStore(44-379)src/store/user.store.ts (1)
useUserStore(15-54)src/hooks/queries/useExpert.ts (1)
useExpertReportDetail(23-33)src/types/expert/expert.detail.ts (1)
ExpertReportDetailResponse(22-27)
src/app/expert/detail/components/ExpertDetailSidebar.tsx (1)
src/hooks/queries/useExpert.ts (1)
useExpertReportDetail(23-33)
src/hooks/queries/useExpert.ts (1)
src/api/expert.ts (1)
GetExpertReportDetail(85-96)
🔇 Additional comments (12)
src/app/page.tsx (2)
4-4: LGTM!새로운 컴포넌트 임포트가 올바르게 추가되었습니다.
31-31: LGTM!프로모션 섹션이 적절한 위치에 배치되었습니다.
src/app/_components/landing/LandingRelation.tsx (1)
73-81: LGTM!이미지 컴포넌트의 렌더링 순서 변경이 적절합니다. 레이아웃 조정만 이루어졌으며 기능적 문제는 없습니다.
src/app/_components/landing/LandingChecklist.tsx (2)
6-12: LGTM!새로운 프로모션 헤더 섹션이 깔끔하게 구현되었습니다.
36-45: LGTM!프로모션 이미지로의 변경이 적절하며,
quality={100}설정으로 고품질 렌더링이 보장됩니다.src/app/expert/components/ExpertCard.tsx (3)
2-4: LGTM!피드백 기반 로직 제거에 따른 의존성 정리가 적절히 수행되었습니다.
15-21: LGTM! 향후 상태 로직 통합 고려피드백 기반 상태 로직을 제거하고 status를 'active'로 단순화한 것은 API 리팩토링의 일환으로 적절합니다.
향후
useExpertReportDetail에서 제공하는 데이터를 활용하여 전문가별 상태나 메타데이터를 표시할 필요가 있을 경우, 해당 시점에 통합을 검토하시기 바랍니다.Based on learnings from the AI summary, this change aligns with the broader migration from feedback-based data to ExpertReportDetail data flow.
28-32: LGTM!로딩 상태 체크가 피드백 의존성 제거에 맞게 올바르게 단순화되었습니다.
src/app/_components/landing/LandingPaySection.tsx (2)
1-3: LGTM!아이콘 임포트가 올바르게 구성되었습니다.
52-61: LGTM!가격 정보에 대한 설명 텍스트가 명확하게 제공되어 사용자 이해를 돕습니다.
src/types/expert/expert.detail.ts (1)
22-27: LGTM!새로운
ExpertReportDetailResponse인터페이스가 잘 정의되어 있습니다. 필드명이 명확하고 타입이 적절합니다.src/app/expert/detail/components/BusinessPlanDropdown.tsx (1)
16-33: LGTM!
BusinessPlanDropdown컴포넌트가useExpertReportDetail훅과 잘 통합되었습니다. Props 인터페이스가 명확하고, 로딩 상태 및 빈 상태 처리가 적절합니다.
| <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> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# Check if the file exists and read the content
wc -l src/app/_components/landing/LandingChecklist.tsxRepository: 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 -40Repository: 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.tsxRepository: 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 -50Repository: 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 2Repository: 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 -80Repository: 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 -20Repository: 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.
| 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), | ||
| }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SSR 환경에서 localStorage 접근 시 에러 발생 가능
Line 27에서 localStorage.getItem('accessToken')을 직접 호출하고 있습니다. 이 훅이 서버 사이드에서 실행될 경우 localStorage is not defined 에러가 발생할 수 있습니다.
🔎 제안된 수정
export function useExpertReportDetail(
expertId: number,
options?: { enabled?: boolean }
) {
- const hasToken = localStorage.getItem('accessToken');
+ const hasToken =
+ typeof window !== 'undefined'
+ ? localStorage.getItem('accessToken')
+ : null;
return useQuery({
queryKey: ['GetExpertReportDetail', expertId, hasToken],
queryFn: () => GetExpertReportDetail(expertId),
enabled: expertId > 0 && !!hasToken && (options?.enabled ?? true),
});
}참고: ExpertDetailSidebar.tsx에서는 이미 typeof window !== 'undefined' 가드를 사용하고 있습니다(Line 25-26).
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 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), | |
| }); | |
| } | |
| export function useExpertReportDetail( | |
| expertId: number, | |
| options?: { enabled?: boolean } | |
| ) { | |
| const hasToken = | |
| typeof window !== 'undefined' | |
| ? localStorage.getItem('accessToken') | |
| : null; | |
| return useQuery({ | |
| queryKey: ['GetExpertReportDetail', expertId, hasToken], | |
| queryFn: () => GetExpertReportDetail(expertId), | |
| enabled: expertId > 0 && !!hasToken && (options?.enabled ?? true), | |
| }); | |
| } |
🤖 Prompt for AI Agents
In src/hooks/queries/useExpert.ts around lines 23-33, the hook directly calls
localStorage.getItem('accessToken') which will throw "localStorage is not
defined" during SSR; replace that call with a safe guard such as computing
hasToken only when typeof window !== 'undefined' (e.g. const hasToken = typeof
window !== 'undefined' ? localStorage.getItem('accessToken') : null) and then
use that hasToken in queryKey and the enabled condition (enabled: expertId > 0
&& !!hasToken && (options?.enabled ?? true)); ensure no other direct
window/localStorage access remains in this function so the hook is safe to
import/run on the server.
parknari02
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
확인했습니다. 수고하셨습니다 ~!
[SRLT-123] 랜딩페이지 버튼 라우터 연결
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/app/pay/components/PaymentAmount.tsx (1)
4-5: 가격 데이터를 props로 받도록 개선을 권장합니다.현재
unitPrice와totalAmount가 하드코딩되어 있어 컴포넌트의 재사용성과 유지보수성이 제한됩니다. 프로모션 가격이 변경되거나 다른 요금제에서 이 컴포넌트를 재사용할 경우 코드 수정이 필요합니다.props를 통해 동적으로 가격을 전달받도록 리팩토링하는 것을 권장합니다.
🔎 제안하는 리팩토링
+interface PaymentAmountProps { + unitPrice?: number; + totalAmount?: number; + originalAmount?: number; +} + -const PaymentAmount = () => { - const unitPrice = 49000; - const totalAmount = 49000; +const PaymentAmount = ({ + unitPrice = 49000, + totalAmount = 49000, + originalAmount = 49000 +}: PaymentAmountProps = {}) => {
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/app/_components/landing/LandingChecklist.tsxsrc/app/_components/landing/LandingPaySection.tsxsrc/app/pay/components/PaymentAmount.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
- src/app/_components/landing/LandingChecklist.tsx
- src/app/_components/landing/LandingPaySection.tsx
| <div className="flex items-center gap-3"> | ||
| <span className="relative inline-block"> | ||
| <span className="absolute inset-0 flex items-center"> | ||
| <span className="bg-primary-500 h-[3px] w-full"></span> | ||
| </span> | ||
| <span className="ds-text relative font-semibold text-gray-900"> | ||
| {totalAmount.toLocaleString()}원 | ||
| </span> | ||
| </span> | ||
| <span className="ds-text font-semibold text-gray-900">0원</span> | ||
| </div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
할인 정보 표시 및 접근성 개선이 필요합니다.
현재 구현에서 49,000원이 취소선 처리되고 0원이 표시되는데, 다음 사항들을 고려해주세요:
-
UX 명확성: 100% 할인이 적용된 이유나 조건에 대한 설명이 없어 사용자가 혼란스러울 수 있습니다. "프로모션 할인", "이벤트 가격" 등의 레이블을 추가하는 것을 권장합니다.
-
접근성: 취소선이 순수하게 시각적 요소(색상 바)로만 구현되어 있어 스크린 리더 사용자에게 의미가 전달되지 않습니다.
🔎 접근성 및 UX 개선 제안
<div className="flex items-center gap-3">
- <span className="relative inline-block">
- <span className="absolute inset-0 flex items-center">
- <span className="bg-primary-500 h-[3px] w-full"></span>
- </span>
- <span className="ds-text relative font-semibold text-gray-900">
+ <del
+ className="ds-text font-semibold text-gray-500 decoration-primary-500 decoration-2"
+ aria-label="정가"
+ >
- {totalAmount.toLocaleString()}원
+ {totalAmount.toLocaleString()}원
- </span>
- </span>
+ </del>
- <span className="ds-text font-semibold text-gray-900">0원</span>
+ <div className="flex flex-col items-end">
+ <span className="ds-text font-semibold text-primary-500">0원</span>
+ <span className="ds-caption text-gray-500">프로모션 가격</span>
+ </div>
</div>이렇게 하면:
- 시맨틱
<del>태그로 스크린 리더가 취소된 가격임을 인식 - 할인된 가격에 대한 명확한 컨텍스트 제공
- Tailwind의 기본
text-decoration유틸리티 활용으로 더 안정적인 구현
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <div className="flex items-center gap-3"> | |
| <span className="relative inline-block"> | |
| <span className="absolute inset-0 flex items-center"> | |
| <span className="bg-primary-500 h-[3px] w-full"></span> | |
| </span> | |
| <span className="ds-text relative font-semibold text-gray-900"> | |
| {totalAmount.toLocaleString()}원 | |
| </span> | |
| </span> | |
| <span className="ds-text font-semibold text-gray-900">0원</span> | |
| </div> | |
| <div className="flex items-center gap-3"> | |
| <del | |
| className="ds-text font-semibold text-gray-500 decoration-primary-500 decoration-2" | |
| aria-label="정가" | |
| > | |
| {totalAmount.toLocaleString()}원 | |
| </del> | |
| <div className="flex flex-col items-end"> | |
| <span className="ds-text font-semibold text-primary-500">0원</span> | |
| <span className="ds-caption text-gray-500">프로모션 가격</span> | |
| </div> | |
| </div> |
✍️ 작업 내용 요약
Summary by CodeRabbit
릴리스 노트
새로운 기능
업데이트
✏️ Tip: You can customize this high-level summary in your review settings.