✨Feat: 메인 홈 반려동물 선택형 건강 레포트 UI 및 데이터 연동 - #61
Conversation
- 메인페이지 정보 조회 API 타입 및 react-query 훅 추가 - 선택된 반려동물 기준 최신 AI 건강 레포트 노출 로직 구현 - 공지사항/바로가기 섹션을 데이터 기반 UI로 재구성 - 반려동물 없음, 레포트 없음, 로딩, 에러 상태 UI 추가 `DoDo-Project#60`
- 메인 홈 API 연동 및 반려동물 선택 상태 추가 - 반려동물 미등록 시 기존 홈 UI 유지 - 반려동물 등록 시 선택형 프로필 카드와 건강 레포트 UI 노출 - healthReportContent JSON 파싱 후 recommendations 우선 노출 - 레포트 영역에 title, summary, content 순서 적용 - 우측 프로필 카드 정보 구조 및 썸네일 크기/정렬 개선 `DoDo-Project#60`
|
@sooloin is attempting to deploy a commit to the sooloin's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Code Review
This pull request implements the main dashboard features for logged-in users, including fetching pet profiles and AI health reports, handling loading/error states, and displaying interactive pet selection and report summaries. The review feedback highlights several improvement opportunities: preventing UI flickering during background refetches by using isLoading instead of isFetching, optimizing the getLatestReportByPet function to avoid unnecessary array sorting and date object creation, safely handling potential non-object values returned by JSON.parse in extractHealthReportRecommendations, and ensuring raw JSON is not rendered to the user when recommendations are empty.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| <HealthReportSection | ||
| isLoading={isLoading || isFetching} |
There was a problem hiding this comment.
배경에서 데이터를 새로고침할 때(background refetch) isFetching이 true가 되는데, 이로 인해 이미 화면에 표시되고 있던 데이터가 사라지고 스켈레톤 UI가 다시 노출되는 깜빡임 현상(UX 저하)이 발생할 수 있습니다.
React Query의 캐싱 이점을 살려 이미 데이터가 있을 때는 기존 데이터를 계속 보여주고, 최초 로딩 시에만 스켈레톤을 보여주도록 isLoading만 사용하는 것이 좋습니다.
| <HealthReportSection | |
| isLoading={isLoading || isFetching} | |
| <HealthReportSection | |
| isLoading={isLoading} |
| export function getLatestReportByPet<T extends { petId: number; checkupDate: string }>(petId: number, reports: T[]) { | ||
| return [...reports] | ||
| .filter((report) => report.petId === petId) | ||
| .sort((left, right) => new Date(right.checkupDate).getTime() - new Date(left.checkupDate).getTime())[0]; | ||
| } |
There was a problem hiding this comment.
getLatestReportByPet 함수는 매 렌더링마다 전체 리포트 배열을 복사하고 필터링한 뒤, 정렬 과정에서 new Date() 객체를 반복적으로 생성하여 성능 저하를 유발할 수 있습니다.
배열을 정렬하는 대신 단일 루프($O(N)$)를 돌며 가장 최신 리포트를 찾는 방식으로 최적화하면 불필요한 객체 생성과 정렬 비용을 크게 줄일 수 있습니다.
| export function getLatestReportByPet<T extends { petId: number; checkupDate: string }>(petId: number, reports: T[]) { | |
| return [...reports] | |
| .filter((report) => report.petId === petId) | |
| .sort((left, right) => new Date(right.checkupDate).getTime() - new Date(left.checkupDate).getTime())[0]; | |
| } | |
| export function getLatestReportByPet<T extends { petId: number; checkupDate: string }>(petId: number, reports: T[]): T | undefined { | |
| let latest: T | undefined = undefined; | |
| let latestTime = -1; | |
| for (const report of reports) { | |
| if (report.petId === petId) { | |
| const time = new Date(report.checkupDate).getTime(); | |
| if (!Number.isNaN(time) && time > latestTime) { | |
| latestTime = time; | |
| latest = report; | |
| } | |
| } | |
| } | |
| return latest; | |
| } |
| const parsed = JSON.parse(content) as ParsedHealthReportContent; | ||
| if (!Array.isArray(parsed.recommendations)) { | ||
| return []; | ||
| } |
There was a problem hiding this comment.
JSON.parse(content)의 결과가 null이거나 객체가 아닌 경우(예: "null", "123" 등), parsed.recommendations에 접근할 때 TypeError가 발생하여 catch 블록으로 빠지게 됩니다. 예외 발생에 의존하기보다 안전하게 타입 및 null 체크를 수행하는 것이 좋습니다.
| const parsed = JSON.parse(content) as ParsedHealthReportContent; | |
| if (!Array.isArray(parsed.recommendations)) { | |
| return []; | |
| } | |
| const parsed = JSON.parse(content) as ParsedHealthReportContent | null; | |
| if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.recommendations)) { | |
| return []; | |
| } |
| const reportPrimaryContent = | ||
| reportRecommendations[0] || (selectedReport ? summarizeContent(selectedReport.healthReportContent, 96) : ''); |
There was a problem hiding this comment.
만약 selectedReport.healthReportContent가 JSON 문자열이고 recommendations가 비어있는 경우, reportPrimaryContent는 raw JSON 문자열 전체를 summarizeContent하여 화면에 그대로 노출하게 됩니다. 이는 사용자에게 깨진 JSON 텍스트를 보여주는 UX 문제를 야기할 수 있습니다.
healthReportContent가 JSON 형식일 때의 안전한 폴백 메시지를 정의하거나, JSON 내부의 다른 텍스트 필드(예: content 등)를 파싱하여 노출하도록 개선하는 것이 좋습니다.
- 배경 refetch 시 스켈레톤 깜빡임이 발생하지 않도록 isLoading만 사용 - 최신 건강 레포트 탐색 로직을 단일 순회 방식으로 최적화 - healthReportContent JSON 파싱 시 null 및 타입 안전성 체크 추가 - recommendation 부재 시 raw JSON이 노출되지 않도록 안전한 본문 fallback 처리 `DoDo-Project#60`
📄 작업 내용 (Description)
메인 홈 상단 영역을 메인 정보 조회 API 기반으로 연결
반려동물 등록 여부에 따라 홈 UI가 분기되도록 구현
🔗 관련 이슈 (Related Issues)
✅ 체크리스트 (Checklist)
Style)Test)📸 스크린샷 (Screenshots)
💬 기타 사항 (Etc)