Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 8 additions & 0 deletions src/pages/main/api/home.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { apiClient } from '@/shared/api/axios';

import type { MainHomeResponse } from '../model/types';

export async function getMainHome(): Promise<MainHomeResponse> {
const response = await apiClient.get<MainHomeResponse>('/main');
return response.data;
}
106 changes: 106 additions & 0 deletions src/pages/main/model/formatters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
const SPECIES_LABEL: Record<string, string> = {
CANINE: '강아지',
FELINE: '고양이',
};

export function getMainPetSpecies(profile: { species?: string; spercies?: string }) {
return profile.species ?? profile.spercies ?? '';
}

export function formatSpeciesLabel(species: string) {
return SPECIES_LABEL[species] ?? species;
}

export function formatWeightLabel(weight: number) {
if (!Number.isFinite(weight)) return '-';
if (Number.isInteger(weight)) return `${weight} kg`;
return `${weight.toFixed(1)} kg`;
}

export function formatDateLabel(value: string) {
if (!value) return '-';

const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return value.slice(0, 10);
}

return new Intl.DateTimeFormat('ko-KR', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).format(date);
}

export function getLatestReportByPet<T extends { petId: number; checkupDate: string }>(
petId: number,
reports: T[],
): T | undefined {
let latest: T | undefined;
let latestTime = -1;

for (const report of reports) {
if (report.petId !== petId) {
continue;
}

const time = new Date(report.checkupDate).getTime();
if (!Number.isNaN(time) && time > latestTime) {
latestTime = time;
latest = report;
}
}

return latest;
}

export function summarizeContent(value: string, maxLength = 140) {
const normalized = value.replace(/\s+/g, ' ').trim();

if (normalized.length <= maxLength) {
return normalized;
}

return `${normalized.slice(0, maxLength).trimEnd()}...`;
}

interface ParsedHealthReportContent {
recommendations?: unknown;
content?: unknown;
summary?: unknown;
analysis?: unknown;
message?: unknown;
}

export function extractHealthReportRecommendations(content: string): string[] {
if (!content.trim()) return [];

try {
const parsed = JSON.parse(content) as ParsedHealthReportContent | null;
if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.recommendations)) {
return [];
}

return parsed.recommendations.filter((item): item is string => typeof item === 'string' && item.trim().length > 0);
} catch {
return [];
}
}

export function extractHealthReportDisplayContent(content: string): string | null {
if (!content.trim()) return null;

try {
const parsed = JSON.parse(content) as ParsedHealthReportContent | null;
if (!parsed || typeof parsed !== 'object') {
return null;
}

const candidates = [parsed.content, parsed.summary, parsed.analysis, parsed.message];
const text = candidates.find((item): item is string => typeof item === 'string' && item.trim().length > 0);

return text?.trim() ?? null;
} catch {
return content;
}
}
2 changes: 1 addition & 1 deletion src/pages/main/model/quickLinks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export type QuickLinkItem = {
Icon: ComponentType<SVGProps<SVGSVGElement>>;
};

/** 화면 바로가기 메뉴(정적 네비게이션 설정, API 데이터 아님) */
/** 메인 화면 바로가기 메뉴(정적 내비게이션, API 데이터 아님) */
export const QUICK_LINKS: QuickLinkItem[] = [
{ id: 'pet', label: '나의 반려동물', to: '/my', Icon: PetIcon },
{ id: 'walk', label: '산책 기록', to: '/walk', Icon: WalkIcon },
Expand Down
35 changes: 35 additions & 0 deletions src/pages/main/model/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
export interface MainPetProfile {
petId: number;
name: string;
imageFileUrl: string | null;
breed: string;
age: number;
species?: string;
spercies?: string;
sex: string;
weight: number;
}

export interface MainHealthReport {
petId: number;
petName: string;
dashboardId: number;
healthReportTitle: string;
healthReportSummary: string;
healthReportContent: string;
checkupDate: string;
}

export interface MainAnnouncement {
boardTitle: string;
boardContent: string;
imageFileUrl: string | null;
viewCount: number;
}

export interface MainHomeResponse {
message: string;
petProfiles: MainPetProfile[];
healthReports: MainHealthReport[];
announcement: MainAnnouncement[];
}
11 changes: 11 additions & 0 deletions src/pages/main/model/useHomeDashboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useQuery } from '@tanstack/react-query';

import { getMainHome } from '@/pages/main/api/home';
import { queryKeys } from '@/shared/lib/react-query/queryKey';

export function useHomeDashboard() {
return useQuery({
queryKey: queryKeys.main.home(),
queryFn: getMainHome,
});
}
35 changes: 34 additions & 1 deletion src/pages/main/ui/LoggedInHome.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,44 @@
import { useState } from 'react';

import { getApiErrorMessage } from '@/shared/lib/api/errorMessage';

import { getLatestReportByPet } from '../model/formatters';
import { useHomeDashboard } from '../model/useHomeDashboard';
import { HealthReportSection } from './sections/HealthReportSection';
import { HotTopicSection } from './sections/HotTopicSection';
import { NoticeAndQuickLinksSection } from './sections/NoticeAndQuickLinksSection';

export function LoggedInHome() {
const { data, isLoading, isError, error, refetch } = useHomeDashboard();
const [selectedPetId, setSelectedPetId] = useState<number | null>(null);

const petProfiles = data?.petProfiles ?? [];
const healthReports = data?.healthReports ?? [];

const resolvedSelectedPetId =
selectedPetId !== null && petProfiles.some((pet) => pet.petId === selectedPetId)
? selectedPetId
: (petProfiles[0]?.petId ?? null);
const selectedPet = petProfiles.find((pet) => pet.petId === resolvedSelectedPetId) ?? null;
const selectedReport = selectedPet ? getLatestReportByPet(selectedPet.petId, healthReports) : undefined;
const errorMessage = isError
? getApiErrorMessage(error, '메인 정보를 불러오지 못했어요. 잠시 후 다시 시도해 주세요.')
: null;

return (
<div className="flex w-full flex-col gap-4 pb-6 sm:gap-6">
<HealthReportSection />
<HealthReportSection
isLoading={isLoading}
errorMessage={errorMessage}
pets={petProfiles}
selectedPet={selectedPet}
selectedPetId={resolvedSelectedPetId}
selectedReport={selectedReport}
onSelectPet={setSelectedPetId}
onRetry={() => {
void refetch();
}}
/>
<NoticeAndQuickLinksSection />
<HotTopicSection />
</div>
Expand Down
Loading
Loading