mpt 페이지 추가

This commit is contained in:
2025-12-16 16:46:12 +09:00
parent 3d022a1305
commit eab96e0bfc
6 changed files with 1234 additions and 62 deletions

View File

@@ -0,0 +1,497 @@
'use client'
import { useEffect, useState } from 'react'
import { Card, CardContent } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { mptApi, MptDto } from "@/lib/api"
import { Activity, CheckCircle2, XCircle } from 'lucide-react'
import { CowNumberDisplay } from "@/components/common/cow-number-display"
import { CowDetail } from "@/types/cow.types"
import { GenomeRequestDto } from "@/lib/api"
import { MPT_REFERENCE_RANGES } from "@/constants/mpt-reference"
// 혈액화학검사 카테고리별 항목
const MPT_CATEGORIES = [
{ name: '에너지 대사', items: ['glucose', 'cholesterol', 'nefa', 'bcs'], color: 'bg-muted/50' },
{ name: '단백질 대사', items: ['totalProtein', 'albumin', 'globulin', 'agRatio', 'bun'], color: 'bg-muted/50' },
{ name: '간기능', items: ['ast', 'ggt', 'fattyLiverIdx'], color: 'bg-muted/50' },
{ name: '미네랄', items: ['calcium', 'phosphorus', 'caPRatio', 'magnesium'], color: 'bg-muted/50' },
{ name: '기타', items: ['creatinine'], color: 'bg-muted/50' },
]
// 측정값 상태 판정
function getMptValueStatus(key: string, value: number | null): 'normal' | 'warning' | 'danger' | 'unknown' {
if (value === null || value === undefined) return 'unknown'
const ref = MPT_REFERENCE_RANGES[key]
if (!ref || ref.lowerLimit === null || ref.upperLimit === null) return 'unknown'
if (value >= ref.lowerLimit && value <= ref.upperLimit) return 'normal'
const margin = (ref.upperLimit - ref.lowerLimit) * 0.1
if (value >= ref.lowerLimit - margin && value <= ref.upperLimit + margin) return 'warning'
return 'danger'
}
interface MptTableProps {
cowShortNo?: string
cowNo?: string
farmNo?: number
cow?: CowDetail | null
genomeRequest?: GenomeRequestDto | null
}
export function MptTable({ cowShortNo, cowNo, farmNo, cow, genomeRequest }: MptTableProps) {
const [mptData, setMptData] = useState<MptDto[]>([])
const [selectedMpt, setSelectedMpt] = useState<MptDto | null>(null)
const [loading, setLoading] = useState(false)
useEffect(() => {
const fetchMptData = async () => {
if (!cowShortNo) return
setLoading(true)
try {
const data = await mptApi.findByCowShortNo(cowShortNo)
setMptData(data)
if (data.length > 0) {
setSelectedMpt(data[0])
}
} catch (error) {
console.error('MPT 데이터 로드 실패:', error)
} finally {
setLoading(false)
}
}
fetchMptData()
}, [cowShortNo])
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"></div>
<p className="text-muted-foreground"> ...</p>
</div>
</div>
)
}
return (
<div className="space-y-6">
{/* 개체 정보 섹션 */}
<h3 className="text-lg lg:text-xl font-bold text-foreground"> </h3>
<Card className="bg-white border border-border shadow-sm rounded-2xl overflow-hidden">
<CardContent className="p-0">
{/* 데스크탑: 가로 그리드 */}
<div className="hidden lg:grid lg:grid-cols-4 divide-x divide-border">
<div>
<div className="bg-muted/50 px-5 py-3 border-b border-border">
<span className="text-base font-semibold text-muted-foreground"></span>
</div>
<div className="px-5 py-4">
<CowNumberDisplay cowId={cowNo || ''} variant="highlight" className="text-2xl font-bold text-foreground" />
</div>
</div>
<div>
<div className="bg-muted/50 px-5 py-3 border-b border-border">
<span className="text-base font-semibold text-muted-foreground"></span>
</div>
<div className="px-5 py-4">
<span className="text-2xl font-bold text-foreground">
{cow?.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR') : '-'}
</span>
</div>
</div>
<div>
<div className="bg-muted/50 px-5 py-3 border-b border-border">
<span className="text-base font-semibold text-muted-foreground"></span>
</div>
<div className="px-5 py-4">
<span className="text-2xl font-bold text-foreground">
{cow?.cowBirthDt
? `${Math.floor((new Date().getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월`
: '-'}
</span>
</div>
</div>
<div>
<div className="bg-muted/50 px-5 py-3 border-b border-border">
<span className="text-base font-semibold text-muted-foreground"></span>
</div>
<div className="px-5 py-4">
<span className="text-2xl font-bold text-foreground">
{cow?.cowSex === 'F' ? '암' : cow?.cowSex === 'M' ? '수' : '-'}
</span>
</div>
</div>
</div>
{/* 모바일: 좌우 배치 리스트 */}
<div className="lg:hidden divide-y divide-border">
<div className="flex items-center">
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground"></span>
<div className="flex-1 px-4 py-3.5">
<CowNumberDisplay cowId={cowNo || ''} variant="highlight" className="text-base font-bold text-foreground" />
</div>
</div>
<div className="flex items-center">
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground"></span>
<span className="flex-1 px-4 py-3.5 text-base font-bold text-foreground">
{cow?.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString('ko-KR') : '-'}
</span>
</div>
<div className="flex items-center">
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground"></span>
<span className="flex-1 px-4 py-3.5 text-base font-bold text-foreground">
{cow?.cowBirthDt
? `${Math.floor((new Date().getTime() - new Date(cow.cowBirthDt).getTime()) / (1000 * 60 * 60 * 24 * 30.44))}개월`
: '-'}
</span>
</div>
<div className="flex items-center">
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground"></span>
<span className="flex-1 px-4 py-3.5 text-base font-bold text-foreground">
{cow?.cowSex === 'F' ? '암' : cow?.cowSex === 'M' ? '수' : '-'}
</span>
</div>
</div>
</CardContent>
</Card>
{/* 혈통정보 섹션 */}
<h3 className="text-lg lg:text-xl font-bold text-foreground"></h3>
<Card className="bg-white border border-border shadow-sm rounded-2xl overflow-hidden">
<CardContent className="p-0">
{/* 데스크탑: 가로 그리드 */}
<div className="hidden lg:grid lg:grid-cols-2 divide-x divide-border">
<div>
<div className="bg-muted/50 px-5 py-3 border-b border-border">
<span className="text-base font-semibold text-muted-foreground"> KPN번호</span>
</div>
<div className="px-5 py-4 flex items-center justify-between gap-3">
<span className="text-2xl font-bold text-foreground break-all">
{cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'}
</span>
{(() => {
const chipSireName = genomeRequest?.chipSireName
if (chipSireName === '일치') {
return (
<span className="flex items-center gap-1.5 bg-muted/50 text-primary-foreground text-sm font-semibold px-3 py-1.5 rounded-full shrink-0">
<CheckCircle2 className="w-4 h-4" />
<span></span>
</span>
)
} else if (chipSireName && chipSireName !== '일치') {
return (
<span className="flex items-center gap-1.5 bg-red-500 text-white text-sm font-semibold px-3 py-1.5 rounded-full shrink-0">
<XCircle className="w-4 h-4" />
<span></span>
</span>
)
} else {
return null
}
})()}
</div>
</div>
<div>
<div className="bg-muted/50 px-5 py-3 border-b border-border">
<span className="text-base font-semibold text-muted-foreground"> </span>
</div>
<div className="px-5 py-4 flex items-center justify-between gap-3">
{cow?.damCowId && cow.damCowId !== '0' ? (
<CowNumberDisplay cowId={cow.damCowId} variant="highlight" className="text-2xl font-bold text-foreground" />
) : (
<span className="text-2xl font-bold text-foreground">-</span>
)}
{(() => {
const chipDamName = genomeRequest?.chipDamName
if (chipDamName === '일치') {
return (
<span className="flex items-center gap-1.5 bg-muted/50 text-primary-foreground text-sm font-semibold px-3 py-1.5 rounded-full shrink-0">
<CheckCircle2 className="w-4 h-4" />
<span></span>
</span>
)
} else if (chipDamName === '불일치') {
return (
<span className="flex items-center gap-1.5 bg-red-500 text-white text-sm font-semibold px-3 py-1.5 rounded-full shrink-0">
<XCircle className="w-4 h-4" />
<span></span>
</span>
)
} else if (chipDamName === '이력제부재') {
return (
<span className="flex items-center gap-1.5 bg-amber-500 text-white text-sm font-semibold px-3 py-1.5 rounded-full shrink-0">
<XCircle className="w-4 h-4" />
<span></span>
</span>
)
} else {
return null
}
})()}
</div>
</div>
</div>
{/* 모바일: 좌우 배치 리스트 */}
<div className="lg:hidden divide-y divide-border">
<div className="flex items-center">
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground"> KPN번호</span>
<div className="flex-1 px-4 py-3.5 flex items-center justify-between gap-2">
<span className="text-base font-bold text-foreground break-all">
{cow?.sireKpn && cow.sireKpn !== '0' ? cow.sireKpn : '-'}
</span>
{(() => {
const chipSireName = genomeRequest?.chipSireName
if (chipSireName === '일치') {
return (
<span className="flex items-center gap-1 bg-muted/50 text-primary-foreground text-xs font-semibold px-2 py-1 rounded-full shrink-0">
<CheckCircle2 className="w-3 h-3" />
<span></span>
</span>
)
} else if (chipSireName && chipSireName !== '일치') {
return (
<span className="flex items-center gap-1 bg-red-500 text-white text-xs font-semibold px-2 py-1 rounded-full shrink-0">
<XCircle className="w-3 h-3" />
<span></span>
</span>
)
} else {
return null
}
})()}
</div>
</div>
<div className="flex items-center">
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground"> </span>
<div className="flex-1 px-4 py-3.5 flex items-center justify-between gap-2">
{cow?.damCowId && cow.damCowId !== '0' ? (
<CowNumberDisplay cowId={cow.damCowId} variant="highlight" className="text-base font-bold text-foreground" />
) : (
<span className="text-base font-bold text-foreground">-</span>
)}
{(() => {
const chipDamName = genomeRequest?.chipDamName
if (chipDamName === '일치') {
return (
<span className="flex items-center gap-1 bg-muted/50 text-primary-foreground text-xs font-semibold px-2 py-1 rounded-full shrink-0">
<CheckCircle2 className="w-3 h-3" />
<span></span>
</span>
)
} else if (chipDamName === '불일치') {
return (
<span className="flex items-center gap-1 bg-red-500 text-white text-xs font-semibold px-2 py-1 rounded-full shrink-0">
<XCircle className="w-3 h-3" />
<span></span>
</span>
)
} else if (chipDamName === '이력제부재') {
return (
<span className="flex items-center gap-1 bg-amber-500 text-white text-xs font-semibold px-2 py-1 rounded-full shrink-0">
<XCircle className="w-3 h-3" />
<span></span>
</span>
)
} else {
return null
}
})()}
</div>
</div>
</div>
</CardContent>
</Card>
{/* 검사 정보 */}
{selectedMpt && (
<>
<h3 className="text-lg lg:text-xl font-bold text-foreground"> </h3>
<Card className="bg-white border border-border shadow-sm rounded-2xl overflow-hidden">
<CardContent className="p-0">
<div className="hidden lg:grid lg:grid-cols-4 divide-x divide-border">
<div>
<div className="bg-muted/50 px-5 py-3 border-b border-border">
<span className="text-base font-semibold text-muted-foreground"></span>
</div>
<div className="px-5 py-4">
<span className="text-2xl font-bold text-foreground">
{selectedMpt.testDt ? new Date(selectedMpt.testDt).toLocaleDateString('ko-KR') : '-'}
</span>
</div>
</div>
<div>
<div className="bg-muted/50 px-5 py-3 border-b border-border">
<span className="text-base font-semibold text-muted-foreground"></span>
</div>
<div className="px-5 py-4">
<span className="text-2xl font-bold text-foreground">
{selectedMpt.monthAge ? `${selectedMpt.monthAge}개월` : '-'}
</span>
</div>
</div>
<div>
<div className="bg-muted/50 px-5 py-3 border-b border-border">
<span className="text-base font-semibold text-muted-foreground"></span>
</div>
<div className="px-5 py-4">
<span className="text-2xl font-bold text-foreground">
{selectedMpt.parity ? `${selectedMpt.parity}산차` : '-'}
</span>
</div>
</div>
<div>
<div className="bg-muted/50 px-5 py-3 border-b border-border">
<span className="text-base font-semibold text-muted-foreground"></span>
</div>
<div className="px-5 py-4">
<span className="text-2xl font-bold text-foreground">
{selectedMpt.milkYield ? `${selectedMpt.milkYield}kg` : '-'}
</span>
</div>
</div>
</div>
{/* 모바일 */}
<div className="lg:hidden divide-y divide-border">
<div className="flex items-center">
<span className="w-24 shrink-0 bg-muted/50 px-4 py-3.5 text-sm font-medium text-muted-foreground"></span>
<span className="flex-1 px-4 py-3.5 text-base font-bold text-foreground">
{selectedMpt.testDt ? new Date(selectedMpt.testDt).toLocaleDateString('ko-KR') : '-'}
</span>
</div>
<div className="flex items-center">
<span className="w-24 shrink-0 bg-muted/50 px-4 py-3.5 text-sm font-medium text-muted-foreground"></span>
<span className="flex-1 px-4 py-3.5 text-base font-bold text-foreground">
{selectedMpt.monthAge ? `${selectedMpt.monthAge}개월` : '-'}
</span>
</div>
<div className="flex items-center">
<span className="w-24 shrink-0 bg-muted/50 px-4 py-3.5 text-sm font-medium text-muted-foreground"></span>
<span className="flex-1 px-4 py-3.5 text-base font-bold text-foreground">
{selectedMpt.parity ? `${selectedMpt.parity}산차` : '-'}
</span>
</div>
<div className="flex items-center">
<span className="w-24 shrink-0 bg-muted/50 px-4 py-3.5 text-sm font-medium text-muted-foreground"></span>
<span className="flex-1 px-4 py-3.5 text-base font-bold text-foreground">
{selectedMpt.milkYield ? `${selectedMpt.milkYield}kg` : '-'}
</span>
</div>
</div>
</CardContent>
</Card>
</>
)}
{/* 혈액화학검사 결과 테이블 */}
<h3 className="text-lg lg:text-xl font-bold text-foreground"> </h3>
<Card className="bg-white border border-border shadow-sm rounded-2xl overflow-hidden">
<CardContent className="p-0">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="bg-muted/50 border-b border-border">
<th className="px-4 py-3 text-left text-sm font-semibold text-muted-foreground w-28"></th>
<th className="px-4 py-3 text-left text-sm font-semibold text-muted-foreground"></th>
<th className="px-4 py-3 text-center text-sm font-semibold text-muted-foreground w-24"></th>
<th className="px-4 py-3 text-center text-sm font-semibold text-muted-foreground w-20"></th>
<th className="px-4 py-3 text-center text-sm font-semibold text-muted-foreground w-20"></th>
<th className="px-4 py-3 text-center text-sm font-semibold text-muted-foreground w-20"></th>
<th className="px-4 py-3 text-center text-sm font-semibold text-muted-foreground w-20"></th>
</tr>
</thead>
<tbody>
{MPT_CATEGORIES.map((category) => (
category.items.map((itemKey, itemIdx) => {
const ref = MPT_REFERENCE_RANGES[itemKey]
const value = selectedMpt ? (selectedMpt[itemKey as keyof MptDto] as number | null) : null
const status = getMptValueStatus(itemKey, value)
return (
<tr key={itemKey} className="border-b border-border hover:bg-muted/30">
{itemIdx === 0 && (
<td
rowSpan={category.items.length}
className={`px-4 py-3 text-sm font-semibold text-foreground ${category.color} align-middle text-center`}
>
{category.name}
</td>
)}
<td className="px-4 py-3 text-sm font-medium text-foreground">{ref?.name || itemKey}</td>
<td className="px-4 py-3 text-center">
<span className={`text-lg font-bold ${
status === 'normal' ? 'text-green-600' :
status === 'warning' ? 'text-amber-600' :
status === 'danger' ? 'text-red-600' :
'text-muted-foreground'
}`}>
{value !== null && value !== undefined ? value.toFixed(2) : '-'}
</span>
</td>
<td className="px-4 py-3 text-center text-sm text-muted-foreground">{ref?.lowerLimit ?? '-'}</td>
<td className="px-4 py-3 text-center text-sm text-muted-foreground">{ref?.upperLimit ?? '-'}</td>
<td className="px-4 py-3 text-center text-sm text-muted-foreground">{ref?.unit || '-'}</td>
<td className="px-4 py-3 text-center">
{value !== null && value !== undefined ? (
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-semibold ${
status === 'normal' ? 'bg-green-100 text-green-700' :
status === 'warning' ? 'bg-amber-100 text-amber-700' :
status === 'danger' ? 'bg-red-100 text-red-700' :
'bg-slate-100 text-slate-500'
}`}>
{status === 'normal' ? '정상' :
status === 'warning' ? '주의' :
status === 'danger' ? '이상' : '-'}
</span>
) : (
<span className="text-muted-foreground">-</span>
)}
</td>
</tr>
)
})
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
{/* 검사 이력 (여러 검사 결과가 있을 경우) */}
{mptData.length > 1 && (
<>
<h3 className="text-lg lg:text-xl font-bold text-foreground"> </h3>
<Card className="bg-white border border-border shadow-sm rounded-2xl overflow-hidden">
<CardContent className="p-4">
<div className="flex flex-wrap gap-2">
{mptData.map((mpt, idx) => (
<Button
key={mpt.pkMptNo}
variant={selectedMpt?.pkMptNo === mpt.pkMptNo ? "default" : "outline"}
size="sm"
onClick={() => setSelectedMpt(mpt)}
>
{mpt.testDt ? new Date(mpt.testDt).toLocaleDateString('ko-KR') : `검사 ${idx + 1}`}
</Button>
))}
</div>
</CardContent>
</Card>
</>
)}
{/* 데이터 없음 안내 */}
{/* {!selectedMpt && (
<Card className="bg-slate-50 border border-border rounded-2xl">
<CardContent className="p-8 text-center">
<Activity className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
<h3 className="text-lg font-semibold text-foreground mb-2">혈액화학검사 데이터 없음</h3>
<p className="text-sm text-muted-foreground">
이 개체는 아직 혈액화학검사(MPT) 결과가 등록되지 않았습니다.
</p>
</CardContent>
</Card>
)} */}
</div>
)
}

View File

@@ -0,0 +1,392 @@
'use client'
import { useSearchParams, useRouter } from "next/navigation"
import { AppSidebar } from "@/components/layout/app-sidebar"
import { SiteHeader } from "@/components/layout/site-header"
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { useToast } from "@/hooks/use-toast"
import { mptApi, MptDto, cowApi } from "@/lib/api"
import { CowDetail } from "@/types/cow.types"
import {
ArrowLeft,
Activity,
Search,
} from 'lucide-react'
import { useEffect, useState } from 'react'
import { CowNumberDisplay } from "@/components/common/cow-number-display"
import { AuthGuard } from "@/components/auth/auth-guard"
// 혈액화학검사 항목별 참조값 (정상 범위)
const MPT_REFERENCE_VALUES: Record<string, { min: number; max: number; unit: string; name: string }> = {
// 에너지 대사
glucose: { min: 45, max: 75, unit: 'mg/dL', name: '혈당' },
cholesterol: { min: 80, max: 120, unit: 'mg/dL', name: '콜레스테롤' },
nefa: { min: 0, max: 0.4, unit: 'mEq/L', name: 'NEFA(유리지방산)' },
bcs: { min: 2.5, max: 3.5, unit: '점', name: 'BCS' },
// 단백질 대사
totalProtein: { min: 6.5, max: 8.5, unit: 'g/dL', name: '총단백질' },
albumin: { min: 3.0, max: 3.6, unit: 'g/dL', name: '알부민' },
globulin: { min: 3.0, max: 5.0, unit: 'g/dL', name: '글로불린' },
agRatio: { min: 0.6, max: 1.2, unit: '', name: 'A/G 비율' },
bun: { min: 8, max: 25, unit: 'mg/dL', name: 'BUN(요소태질소)' },
// 간기능
ast: { min: 45, max: 110, unit: 'U/L', name: 'AST' },
ggt: { min: 10, max: 36, unit: 'U/L', name: 'GGT' },
fattyLiverIdx: { min: 0, max: 30, unit: '', name: '지방간지수' },
// 미네랄
calcium: { min: 8.5, max: 11.5, unit: 'mg/dL', name: '칼슘' },
phosphorus: { min: 4.0, max: 7.5, unit: 'mg/dL', name: '인' },
caPRatio: { min: 1.0, max: 2.0, unit: '', name: 'Ca/P 비율' },
magnesium: { min: 1.8, max: 2.5, unit: 'mg/dL', name: '마그네슘' },
creatinine: { min: 1.0, max: 2.0, unit: 'mg/dL', name: '크레아틴' },
}
// 카테고리별 항목 그룹핑
const MPT_CATEGORIES = [
{
name: '에너지 대사',
items: ['glucose', 'cholesterol', 'nefa', 'bcs'],
color: 'bg-orange-500',
},
{
name: '단백질 대사',
items: ['totalProtein', 'albumin', 'globulin', 'agRatio', 'bun'],
color: 'bg-blue-500',
},
{
name: '간기능',
items: ['ast', 'ggt', 'fattyLiverIdx'],
color: 'bg-green-500',
},
{
name: '미네랄',
items: ['calcium', 'phosphorus', 'caPRatio', 'magnesium', 'creatinine'],
color: 'bg-purple-500',
},
]
// 측정값 상태 판정 (정상/주의/위험)
function getValueStatus(key: string, value: number | null): 'normal' | 'warning' | 'danger' | 'unknown' {
if (value === null || value === undefined) return 'unknown'
const ref = MPT_REFERENCE_VALUES[key]
if (!ref) return 'unknown'
if (value >= ref.min && value <= ref.max) return 'normal'
// 10% 이내 범위 이탈은 주의
const margin = (ref.max - ref.min) * 0.1
if (value >= ref.min - margin && value <= ref.max + margin) return 'warning'
return 'danger'
}
export default function MptPage() {
const searchParams = useSearchParams()
const router = useRouter()
const cowShortNo = searchParams.get('cowShortNo')
const farmNo = searchParams.get('farmNo')
const { toast } = useToast()
const [searchInput, setSearchInput] = useState(cowShortNo || '')
const [mptData, setMptData] = useState<MptDto[]>([])
const [selectedMpt, setSelectedMpt] = useState<MptDto | null>(null)
const [cow, setCow] = useState<CowDetail | null>(null)
const [loading, setLoading] = useState(false)
// 검색 실행
const handleSearch = async () => {
if (!searchInput.trim()) {
toast({
title: '검색어를 입력해주세요',
variant: 'destructive',
})
return
}
setLoading(true)
try {
const data = await mptApi.findByCowShortNo(searchInput.trim())
setMptData(data)
if (data.length > 0) {
setSelectedMpt(data[0]) // 가장 최근 검사 결과 선택
} else {
setSelectedMpt(null)
toast({
title: '검사 결과가 없습니다',
description: `개체번호 ${searchInput}의 혈액화학검사 결과를 찾을 수 없습니다.`,
})
}
} catch (error) {
console.error('MPT 데이터 로드 실패:', error)
toast({
title: '데이터 로드 실패',
variant: 'destructive',
})
} finally {
setLoading(false)
}
}
// 초기 로드
useEffect(() => {
if (cowShortNo) {
handleSearch()
}
}, [])
const handleBack = () => {
router.back()
}
return (
<AuthGuard>
<SidebarProvider>
<AppSidebar />
<SidebarInset>
<SiteHeader />
<main className="flex-1 overflow-y-auto bg-white min-h-screen">
<div className="w-full p-6 sm:px-6 lg:px-8 sm:py-6 space-y-6">
{/* 헤더 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 sm:gap-4">
<Button
onClick={handleBack}
variant="ghost"
size="sm"
className="text-muted-foreground hover:text-foreground hover:bg-muted gap-1.5 -ml-2 px-2 sm:px-3"
>
<ArrowLeft className="h-7 w-7 sm:h-4 sm:w-4" />
<span className="hidden sm:inline text-sm"></span>
</Button>
<div className="w-10 h-10 sm:w-14 sm:h-14 bg-primary rounded-xl flex items-center justify-center flex-shrink-0">
<Activity className="h-5 w-5 sm:h-7 sm:w-7 text-primary-foreground" />
</div>
<div>
<h1 className="text-lg sm:text-3xl lg:text-4xl font-bold text-foreground"></h1>
<p className="text-sm sm:text-lg text-muted-foreground">Metabolic Profile Test</p>
</div>
</div>
</div>
{/* 검색 영역 */}
<Card className="bg-white border border-border shadow-sm rounded-2xl">
<CardContent className="p-4 sm:p-6">
<div className="flex gap-3">
<Input
placeholder="개체 요약번호 입력 (예: 4049)"
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
className="flex-1"
/>
<Button onClick={handleSearch} disabled={loading}>
<Search className="h-4 w-4 mr-2" />
</Button>
</div>
</CardContent>
</Card>
{loading && (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"></div>
<p className="text-muted-foreground"> ...</p>
</div>
</div>
)}
{!loading && selectedMpt && (
<>
{/* 개체 정보 */}
<h3 className="text-lg lg:text-xl font-bold text-foreground"> </h3>
<Card className="bg-white border border-border shadow-sm rounded-2xl overflow-hidden">
<CardContent className="p-0">
<div className="hidden lg:grid lg:grid-cols-4 divide-x divide-border">
<div>
<div className="bg-muted/50 px-5 py-3 border-b border-border">
<span className="text-base font-semibold text-muted-foreground"></span>
</div>
<div className="px-5 py-4">
<span className="text-2xl font-bold text-foreground">{selectedMpt.cowShortNo || '-'}</span>
</div>
</div>
<div>
<div className="bg-muted/50 px-5 py-3 border-b border-border">
<span className="text-base font-semibold text-muted-foreground"></span>
</div>
<div className="px-5 py-4">
<span className="text-2xl font-bold text-foreground">
{selectedMpt.testDt ? new Date(selectedMpt.testDt).toLocaleDateString('ko-KR') : '-'}
</span>
</div>
</div>
<div>
<div className="bg-muted/50 px-5 py-3 border-b border-border">
<span className="text-base font-semibold text-muted-foreground"></span>
</div>
<div className="px-5 py-4">
<span className="text-2xl font-bold text-foreground">
{selectedMpt.monthAge ? `${selectedMpt.monthAge}개월` : '-'}
</span>
</div>
</div>
<div>
<div className="bg-muted/50 px-5 py-3 border-b border-border">
<span className="text-base font-semibold text-muted-foreground"></span>
</div>
<div className="px-5 py-4">
<span className="text-2xl font-bold text-foreground">
{selectedMpt.parity ? `${selectedMpt.parity}산차` : '-'}
</span>
</div>
</div>
</div>
{/* 모바일 */}
<div className="lg:hidden divide-y divide-border">
<div className="flex items-center">
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground"></span>
<span className="flex-1 px-4 py-3.5 text-base font-bold text-foreground">{selectedMpt.cowShortNo || '-'}</span>
</div>
<div className="flex items-center">
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground"></span>
<span className="flex-1 px-4 py-3.5 text-base font-bold text-foreground">
{selectedMpt.testDt ? new Date(selectedMpt.testDt).toLocaleDateString('ko-KR') : '-'}
</span>
</div>
<div className="flex items-center">
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground"></span>
<span className="flex-1 px-4 py-3.5 text-base font-bold text-foreground">
{selectedMpt.monthAge ? `${selectedMpt.monthAge}개월` : '-'}
</span>
</div>
<div className="flex items-center">
<span className="w-28 shrink-0 bg-muted/50 px-4 py-3.5 text-base font-medium text-muted-foreground"></span>
<span className="flex-1 px-4 py-3.5 text-base font-bold text-foreground">
{selectedMpt.parity ? `${selectedMpt.parity}산차` : '-'}
</span>
</div>
</div>
</CardContent>
</Card>
{/* 혈액화학검사 결과 테이블 */}
<h3 className="text-lg lg:text-xl font-bold text-foreground"> </h3>
<Card className="bg-white border border-border shadow-sm rounded-2xl overflow-hidden">
<CardContent className="p-0">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="bg-muted/50 border-b border-border">
<th className="px-4 py-3 text-left text-sm font-semibold text-muted-foreground w-28"></th>
<th className="px-4 py-3 text-left text-sm font-semibold text-muted-foreground"></th>
<th className="px-4 py-3 text-center text-sm font-semibold text-muted-foreground w-24"></th>
<th className="px-4 py-3 text-center text-sm font-semibold text-muted-foreground w-20"></th>
<th className="px-4 py-3 text-center text-sm font-semibold text-muted-foreground w-20"></th>
<th className="px-4 py-3 text-center text-sm font-semibold text-muted-foreground w-20"></th>
<th className="px-4 py-3 text-center text-sm font-semibold text-muted-foreground w-20"></th>
</tr>
</thead>
<tbody>
{MPT_CATEGORIES.map((category, catIdx) => (
category.items.map((itemKey, itemIdx) => {
const ref = MPT_REFERENCE_VALUES[itemKey]
const value = selectedMpt[itemKey as keyof MptDto] as number | null
const status = getValueStatus(itemKey, value)
return (
<tr key={itemKey} className="border-b border-border hover:bg-muted/30">
{itemIdx === 0 && (
<td
rowSpan={category.items.length}
className={`px-4 py-3 text-sm font-semibold text-white ${category.color} align-middle text-center`}
>
{category.name}
</td>
)}
<td className="px-4 py-3 text-sm font-medium text-foreground">{ref?.name || itemKey}</td>
<td className="px-4 py-3 text-center">
<span className={`text-lg font-bold ${
status === 'normal' ? 'text-green-600' :
status === 'warning' ? 'text-amber-600' :
status === 'danger' ? 'text-red-600' :
'text-muted-foreground'
}`}>
{value !== null && value !== undefined ? value.toFixed(2) : '-'}
</span>
</td>
<td className="px-4 py-3 text-center text-sm text-muted-foreground">{ref?.min ?? '-'}</td>
<td className="px-4 py-3 text-center text-sm text-muted-foreground">{ref?.max ?? '-'}</td>
<td className="px-4 py-3 text-center text-sm text-muted-foreground">{ref?.unit || '-'}</td>
<td className="px-4 py-3 text-center">
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-semibold ${
status === 'normal' ? 'bg-green-100 text-green-700' :
status === 'warning' ? 'bg-amber-100 text-amber-700' :
status === 'danger' ? 'bg-red-100 text-red-700' :
'bg-slate-100 text-slate-500'
}`}>
{status === 'normal' ? '정상' :
status === 'warning' ? '주의' :
status === 'danger' ? '이상' : '-'}
</span>
</td>
</tr>
)
})
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
{/* 검사 이력 (여러 검사 결과가 있을 경우) */}
{mptData.length > 1 && (
<>
<h3 className="text-lg lg:text-xl font-bold text-foreground"> </h3>
<Card className="bg-white border border-border shadow-sm rounded-2xl overflow-hidden">
<CardContent className="p-4">
<div className="flex flex-wrap gap-2">
{mptData.map((mpt, idx) => (
<Button
key={mpt.pkMptNo}
variant={selectedMpt?.pkMptNo === mpt.pkMptNo ? "default" : "outline"}
size="sm"
onClick={() => setSelectedMpt(mpt)}
>
{mpt.testDt ? new Date(mpt.testDt).toLocaleDateString('ko-KR') : `검사 ${idx + 1}`}
</Button>
))}
</div>
</CardContent>
</Card>
</>
)}
</>
)}
{!loading && !selectedMpt && cowShortNo && (
<div className="flex flex-col items-center justify-center h-64 text-center">
<Activity className="h-16 w-16 text-muted-foreground/30 mb-4" />
<p className="text-lg font-medium text-muted-foreground"> </p>
<p className="text-sm text-muted-foreground"> .</p>
</div>
)}
{!loading && !selectedMpt && !cowShortNo && (
<div className="flex flex-col items-center justify-center h-64 text-center">
<Search className="h-16 w-16 text-muted-foreground/30 mb-4" />
<p className="text-lg font-medium text-muted-foreground"> </p>
<p className="text-sm text-muted-foreground"> .</p>
</div>
)}
</div>
</main>
</SidebarInset>
</SidebarProvider>
</AuthGuard>
)
}