필터 및 화면 수정사항 반영

This commit is contained in:
2025-12-18 17:01:24 +09:00
parent 4d0f8f3b6b
commit abc2f20495
19 changed files with 417 additions and 5574 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,78 @@
# 혈액대사판정시험(MPT) 검사항목 및 권장수치
## 개요
번식능력 검사를 위한 혈액대사판정시험(MPT) 검사항목으로, 5개 카테고리 총 16개 항목으로 구성됩니다.
---
## 1. 에너지 카테고리
| 항목 | 권장수치 | 단위 |
|------|---------|------|
| 혈당 | 40-84 | mg/dL |
| 콜레스테롤 | 74-252 | mg/dL |
| 유리지방산(NEFA) | 115-660 | μEq/L |
---
## 2. 단백질 카테고리
| 항목 | 권장수치 | 단위 |
|------|---------|------|
| 총단백질 | 6.2-7.7 | g/dL |
| 알부민 | 3.3-4.3 | g/dL |
| 총글로블린 | 9.1-36.1 | g/dL |
| A/G | 0.1-0.4 | - |
| 요소태질소(BUN) | 11.7-18.9 | mg/dL |
---
## 3. 간기능 카테고리
| 항목 | 권장수치 | 단위 |
|------|---------|------|
| AST | 47-92 | U/L |
| GGT | 11-32 | U/L |
| 지방간 지수 | -1.2 ~ 9.9 | - |
---
## 4. 미네랄 카테고리
| 항목 | 권장수치 | 단위 |
|------|---------|------|
| 칼슘 | 8.1-10.6 | mg/dL |
| 인 | 6.2-8.9 | mg/dL |
| 칼슘/인 | 1.2-1.3 | - |
| 마그네슘 | 1.6-3.3 | mg/dL |
---
## 5. 별도 카테고리
| 항목 | 권장수치 | 단위 |
|------|---------|------|
| 크레아틴 | 1.0-1.3 | mg/dL |
---
## 결과 판정 기준
| 판정 | 설명 |
|------|------|
| 낮음 | 권장수치 하한 미만 |
| 권장범위 | 권장수치 범위 내 |
| 높음 | 권장수치 상한 초과 |
---
## 시각화 방식
- **폴리곤(레이더) 차트**: 5개 카테고리를 5각형 구조로 표현
- **가로 막대 도표**: 각 항목별 낮음/권장범위/높음 표시
- **색상 구분**: 우수/적정/부족으로 구분
---
## 참고
> 혈액대사판정시험의 주요 5가지 항목에 대한 시군 및 농가 수치비교를 위해 표준화한 자료입니다.
> 본 결과자료를 통한 종합평가는 보은군 평균과 농가 평균을 비교하여 상대적 차이의 수준을 나타냅니다.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,719 +0,0 @@
# 프론트엔드 API 연동 가이드
> **작성일**: 2025-10-26
> **버전**: 1.0
> **대상**: 한우 유전체 분석 시스템 프론트엔드 개발자
## 📋 목차
1. [개요](#1-개요)
2. [인증 흐름](#2-인증-흐름)
3. [사용자-농장-개체 관계](#3-사용자-농장-개체-관계)
4. [API 연동 방법](#4-api-연동-방법)
5. [주요 구현 사례](#5-주요-구현-사례)
6. [문제 해결](#6-문제-해결)
7. [추가 구현 권장사항](#7-추가-구현-권장사항)
---
## 1. 개요
### 1.1 시스템 구조
```
사용자 (User) 1:N 농장 (Farm) 1:N 개체 (Cow)
```
- **User**: 로그인한 사용자 (농가, 컨설턴트, 기관담당자)
- **Farm**: 사용자가 소유한 농장 (한 사용자가 여러 농장 소유 가능)
- **Cow**: 농장에 속한 개체 (한우)
### 1.2 주요 기술 스택
**백엔드**:
- NestJS 10.x
- TypeORM
- JWT 인증
- PostgreSQL
**프론트엔드**:
- Next.js 15.5.3 (App Router)
- TypeScript
- Zustand (상태관리)
- Axios (HTTP 클라이언트)
### 1.3 API Base URL
```
개발: http://localhost:4000
운영: 환경변수 NEXT_PUBLIC_API_URL 사용
```
---
## 2. 인증 흐름
### 2.1 JWT 기반 인증
모든 API 요청은 JWT 토큰을 필요로 합니다 (일부 Public 엔드포인트 제외).
**전역 Guard 적용**:
```typescript
// backend/src/main.ts
app.useGlobalGuards(new JwtAuthGuard(reflector));
```
### 2.2 로그인 프로세스
```typescript
// 1. 사용자 로그인
const response = await authApi.login({
userId: 'user123',
userPassword: 'password123'
});
// 2. 응답 구조
{
accessToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
refreshToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
user: {
userNo: 1,
userName: '홍길동',
userEmail: 'hong@example.com',
// ...
}
}
// 3. 토큰 자동 저장 (auth-store.ts에서 처리)
localStorage.setItem('accessToken', response.accessToken);
localStorage.setItem('refreshToken', response.refreshToken);
```
### 2.3 자동 토큰 주입
`apiClient`가 모든 요청에 자동으로 토큰을 추가합니다:
```typescript
// frontend/src/lib/api-client.ts (자동 처리)
apiClient.interceptors.request.use((config) => {
const token = localStorage.getItem('accessToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
```
**개발자는 별도로 토큰을 관리할 필요 없음**
---
## 3. 사용자-농장-개체 관계
### 3.1 데이터 모델
```typescript
// User Entity
{
pkUserNo: number; // 사용자 번호
userId: string; // 로그인 ID
userName: string; // 이름
userSe: 'FARM' | 'CNSLT' | 'ORGAN'; // 사용자 구분
farms: FarmModel[]; // 소유 농장 목록
}
// Farm Entity
{
pkFarmNo: number; // 농장 번호
fkUserNo: number; // 사용자 번호 (FK)
farmCode: string; // 농장 코드 (F000001)
farmName: string; // 농장명
farmAddress: string; // 농장 주소
cows: CowModel[]; // 농장의 개체 목록
}
// Cow Entity
{
pkCowNo: string; // 개체번호 (12자리)
fkFarmNo: number; // 농장 번호 (FK)
cowSex: 'M' | 'F'; // 성별
cowBirthDt: Date; // 생년월일
cowStatus: string; // 개체 상태
delYn: 'Y' | 'N'; // 삭제 여부
}
```
### 3.2 관계 API 호출 순서
```typescript
// 올바른 순서:
// 1. 로그인한 사용자의 농장 목록 조회
const farms = await farmApi.findAll(); // GET /farm
// 2. 특정 농장의 개체 조회
const cows = await cowApi.findByFarmNo(farms[0].pkFarmNo); // GET /cow/farm/:farmNo
```
**❌ 잘못된 방법**: farmNo를 하드코딩
```typescript
const cows = await cowApi.findByFarmNo(1); // ❌ 다른 사용자의 데이터 접근 불가
```
---
## 4. API 연동 방법
### 4.1 API 모듈 구조
```
frontend/src/lib/api/
├── api-client.ts # Axios 인스턴스 + 인터셉터
├── index.ts # 모든 API export
├── auth.api.ts # 인증 API
├── farm.api.ts # 농장 API
├── cow.api.ts # 개체 API
├── kpn.api.ts # KPN API
└── dashboard.api.ts # 대시보드 API
```
### 4.2 Import 방법
```typescript
import { cowApi, farmApi, authApi } from '@/lib/api';
```
### 4.3 주요 Farm API
```typescript
// 1. 현재 사용자의 농장 목록 조회
const farms = await farmApi.findAll();
// GET /farm
// 응답: FarmDto[]
// 2. 농장 상세 조회
const farm = await farmApi.findOne(farmNo);
// GET /farm/:id
// 응답: FarmDto
// 3. 농장 생성
const newFarm = await farmApi.create({
userNo: 1,
farmName: '행복농장',
farmAddress: '충청북도 보은군...',
farmBizNo: '123-45-67890'
});
// POST /farm
```
### 4.4 주요 Cow API
```typescript
// 1. 특정 농장의 개체 목록 조회
const cows = await cowApi.findByFarmNo(farmNo);
// GET /cow/farm/:farmNo
// 응답: CowDto[]
// 2. 개체 상세 조회
const cow = await cowApi.findOne(cowNo);
// GET /cow/:cowNo
// 응답: CowDto
// 3. 개체 검색
const results = await cowApi.search('KOR001', farmNo, 20);
// GET /cow/search?keyword=KOR001&farmNo=1&limit=20
// 응답: CowDto[]
// 4. 개체 랭킹 조회 (필터 + 정렬)
const ranking = await cowApi.getRanking({
filterOptions: {
filters: [/* 필터 조건 */]
},
rankingOptions: {
criteria: 'GENE',
order: 'DESC'
}
});
// POST /cow/ranking
// 응답: RankingResult<CowDto>
```
---
## 5. 주요 구현 사례
### 5.1 개체 목록 페이지 (/cow/page.tsx)
**완전한 구현 예시** (하드코딩 없음):
```typescript
'use client'
import { useState, useEffect } from 'react'
import { cowApi, farmApi } from '@/lib/api'
import type { Cow } from '@/types/cow.types'
export default function CowListPage() {
const [cows, setCows] = useState<Cow[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
const fetchCows = async () => {
try {
setLoading(true)
setError(null)
// 1단계: 사용자의 농장 목록 조회
const farms = await farmApi.findAll()
if (!farms || farms.length === 0) {
setError('등록된 농장이 없습니다. 농장을 먼저 등록해주세요.')
setLoading(false)
return
}
// 2단계: 첫 번째 농장의 개체 조회
// TODO: 여러 농장이 있을 경우 사용자가 선택할 수 있도록 UI 추가
const farmNo = farms[0].pkFarmNo
const data = await cowApi.findByFarmNo(farmNo)
setCows(data)
} catch (err) {
console.error('개체 데이터 조회 실패:', err)
setError(err instanceof Error ? err.message : '데이터를 불러오는데 실패했습니다')
} finally {
setLoading(false)
}
}
fetchCows()
}, [])
if (loading) return <div> ...</div>
if (error) return <div>: {error}</div>
return (
<div>
<h1> </h1>
{cows.map(cow => (
<div key={cow.pkCowNo}>
<p>: {cow.pkCowNo}</p>
<p>: {cow.cowSex === 'M' ? '수소' : '암소'}</p>
</div>
))}
</div>
)
}
```
### 5.2 개체 상세 페이지 (/cow/[cowNo]/page.tsx)
```typescript
'use client'
import { useState, useEffect } from 'react'
import { useParams } from 'next/navigation'
import { cowApi } from '@/lib/api'
import type { Cow } from '@/types/cow.types'
export default function CowDetailPage() {
const params = useParams()
const cowNo = params.cowNo as string
const [cow, setCow] = useState<Cow | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
const fetchCow = async () => {
try {
const data = await cowApi.findOne(cowNo)
setCow(data)
} catch (err) {
console.error('개체 상세 조회 실패:', err)
} finally {
setLoading(false)
}
}
fetchCow()
}, [cowNo])
if (loading) return <div> ...</div>
if (!cow) return <div> </div>
return (
<div>
<h1> : {cow.pkCowNo}</h1>
<p>: {cow.fkFarmNo}</p>
<p>: {cow.cowSex === 'M' ? '수소' : '암소'}</p>
<p>: {cow.cowBirthDt ? new Date(cow.cowBirthDt).toLocaleDateString() : '-'}</p>
</div>
)
}
```
### 5.3 Dashboard 통계 페이지
```typescript
'use client'
import { useState, useEffect } from 'react'
import { farmApi, cowApi } from '@/lib/api'
export default function DashboardPage() {
const [stats, setStats] = useState({
totalFarms: 0,
totalCows: 0,
farms: []
})
useEffect(() => {
const fetchStats = async () => {
try {
// 1. 사용자의 농장 목록 조회
const farms = await farmApi.findAll()
// 2. 각 농장의 개체 수 집계
let totalCows = 0
const farmsWithCows = await Promise.all(
farms.map(async (farm) => {
const cows = await cowApi.findByFarmNo(farm.pkFarmNo)
totalCows += cows.length
return {
...farm,
cowCount: cows.length
}
})
)
setStats({
totalFarms: farms.length,
totalCows,
farms: farmsWithCows
})
} catch (err) {
console.error('통계 조회 실패:', err)
}
}
fetchStats()
}, [])
return (
<div>
<h1></h1>
<p> : {stats.totalFarms}</p>
<p> : {stats.totalCows}</p>
{stats.farms.map(farm => (
<div key={farm.pkFarmNo}>
<p>{farm.farmName}: {farm.cowCount}</p>
</div>
))}
</div>
)
}
```
---
## 6. 문제 해결
### 6.1 인증 에러 (401 Unauthorized)
**증상**:
```
{"statusCode":401,"message":["인증이 필요합니다. 로그인 후 이용해주세요."]}
```
**원인**:
- localStorage에 토큰이 없음
- 토큰 만료
**해결 방법**:
```typescript
// 1. 로그인 상태 확인
const { isAuthenticated } = useAuthStore()
if (!isAuthenticated) {
router.push('/login')
return
}
// 2. 토큰 갱신 (필요 시)
await authApi.refreshToken(refreshToken)
```
### 6.2 농장이 없는 경우
**증상**:
```
등록된 농장이 없습니다.
```
**해결 방법**:
```typescript
if (!farms || farms.length === 0) {
return (
<div>
<p> .</p>
<button onClick={() => router.push('/farm/create')}>
</button>
</div>
)
}
```
### 6.3 CORS 에러
**증상**:
```
Access to XMLHttpRequest has been blocked by CORS policy
```
**해결 방법**:
백엔드에서 CORS 설정 확인:
```typescript
// backend/src/main.ts
app.enableCors({
origin: 'http://localhost:3000',
credentials: true,
});
```
---
## 7. 추가 구현 권장사항
### 7.1 여러 농장 선택 UI
현재는 첫 번째 농장만 사용하지만, 사용자가 여러 농장을 소유한 경우 선택할 수 있어야 합니다.
**구현 예시**:
```typescript
'use client'
import { useState, useEffect } from 'react'
import { farmApi, cowApi } from '@/lib/api'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
export default function CowListPage() {
const [farms, setFarms] = useState([])
const [selectedFarmNo, setSelectedFarmNo] = useState<number | null>(null)
const [cows, setCows] = useState([])
useEffect(() => {
const fetchFarms = async () => {
const data = await farmApi.findAll()
setFarms(data)
// 첫 번째 농장 자동 선택
if (data.length > 0) {
setSelectedFarmNo(data[0].pkFarmNo)
}
}
fetchFarms()
}, [])
useEffect(() => {
if (!selectedFarmNo) return
const fetchCows = async () => {
const data = await cowApi.findByFarmNo(selectedFarmNo)
setCows(data)
}
fetchCows()
}, [selectedFarmNo])
return (
<div>
<Select value={String(selectedFarmNo)} onValueChange={(val) => setSelectedFarmNo(Number(val))}>
<SelectTrigger>
<SelectValue placeholder="농장 선택" />
</SelectTrigger>
<SelectContent>
{farms.map(farm => (
<SelectItem key={farm.pkFarmNo} value={String(farm.pkFarmNo)}>
{farm.farmName} ({farm.farmCode})
</SelectItem>
))}
</SelectContent>
</Select>
<div>
{cows.map(cow => (
<div key={cow.pkCowNo}>{cow.pkCowNo}</div>
))}
</div>
</div>
)
}
```
### 7.2 유전자 정보 포함 API 사용
현재 `GET /cow/farm/:farmNo`는 기본 정보만 반환합니다.
유전자 정보가 필요한 경우 `POST /cow/ranking` API를 사용하세요.
**구현 예시**:
```typescript
// 유전자 정보를 포함한 개체 조회
const ranking = await cowApi.getRanking({
filterOptions: {
filters: [
{
field: 'cow.fkFarmNo',
operator: 'equals',
value: farmNo
}
]
},
rankingOptions: {
criteria: 'GENE',
order: 'DESC'
}
})
// ranking.items에 SNP, Trait 등 모든 관계 데이터 포함됨
const cowsWithGenes = ranking.items
```
**백엔드 참조**:
- `backend/src/cow/cow.service.ts:122-140` - `createRankingQueryBuilder()`
- SNP, Trait, Repro, MPT 데이터를 모두 leftJoin으로 포함
### 7.3 에러 바운더리 추가
```typescript
// components/ErrorBoundary.tsx
'use client'
import { useEffect } from 'react'
export default function ErrorBoundary({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
console.error('에러 발생:', error)
}, [error])
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<h2 className="text-2xl font-bold mb-4"> </h2>
<p className="mb-4">{error.message}</p>
<button
onClick={reset}
className="px-4 py-2 bg-blue-500 text-white rounded"
>
</button>
</div>
)
}
```
### 7.4 로딩 스켈레톤 UI
```typescript
// components/CowListSkeleton.tsx
export default function CowListSkeleton() {
return (
<div className="grid grid-cols-3 gap-4">
{[1, 2, 3, 4, 5, 6].map((i) => (
<div key={i} className="border rounded p-4 animate-pulse">
<div className="h-6 bg-gray-200 rounded mb-2"></div>
<div className="h-4 bg-gray-200 rounded"></div>
</div>
))}
</div>
)
}
// 사용
if (loading) return <CowListSkeleton />
```
---
## 8. 참고 자료
### 8.1 백엔드 API 문서
- **Cow API**: `backend/src/cow/cow.controller.ts`
- **Farm API**: `backend/src/farm/farm.controller.ts`
- **Auth API**: `backend/src/auth/auth.controller.ts`
### 8.2 프론트엔드 코드 위치
```
frontend/src/
├── app/
│ ├── cow/
│ │ ├── page.tsx # 개체 목록 (완성)
│ │ └── [cowNo]/page.tsx # 개체 상세
│ └── dashboard/page.tsx # 대시보드
├── lib/api/
│ ├── cow.api.ts # Cow API
│ ├── farm.api.ts # Farm API (신규 추가)
│ └── index.ts # API export
├── types/
│ ├── cow.types.ts # Cow 타입
│ └── auth.types.ts # Auth 타입
└── store/
└── auth-store.ts # 인증 상태 관리
```
### 8.3 타입 정의 참조
**백엔드 Entity → 프론트엔드 Types 매핑**:
| 백엔드 Entity | 프론트엔드 Types | 설명 |
|--------------|-----------------|------|
| `UsersModel` | `UserDto` | 사용자 |
| `FarmModel` | `FarmDto` | 농장 |
| `CowModel` | `CowDto` | 개체 |
| `KpnModel` | `KpnDto` | KPN |
**필드명 주의사항**:
- 백엔드: `pkCowNo`, `fkFarmNo`, `cowBirthDt`, `cowSex`
- 프론트엔드도 동일하게 사용 (DTO 변환 없음)
---
## 9. 체크리스트
개발 시 확인사항:
- [ ] JWT 토큰이 localStorage에 저장되는가?
- [ ] API 호출 시 Authorization 헤더가 자동으로 추가되는가?
- [ ] farmNo를 하드코딩하지 않고 `farmApi.findAll()`로 조회하는가?
- [ ] 농장이 없는 경우를 처리했는가?
- [ ] 에러 발생 시 사용자에게 적절한 메시지를 보여주는가?
- [ ] 로딩 상태를 표시하는가?
- [ ] 여러 농장이 있는 경우를 고려했는가?
---
## 10. 문의
질문이나 문제가 있는 경우:
1. 백엔드 API 문서 확인: `backend/doc/기능요구사항전체정리.md`
2. PRD 문서 확인: `E:/repo5/prd/`
3. 코드 참조:
- 완성된 `/cow/page.tsx` 구현 참조
- `lib/api-client.ts` 인터셉터 참조
- `store/auth-store.ts` 인증 흐름 참조
---
**문서 작성**: Claude Code
**최종 수정일**: 2025-10-26

View File

@@ -59,6 +59,8 @@ export class AuthService {
}
const isPasswordValid = await bcrypt.compare(userPassword, user.userPw);
const inputHash = await bcrypt.hash(userPassword, 10);
this.logger.log(`[DEBUG] 입력 해시: ${inputHash}, DB 해시: ${user.userPw}`);
if (!isPasswordValid) {
this.logger.warn(`[LOGIN] 비밀번호 불일치 - userId: ${userId}`);
throw new UnauthorizedException('아이디 또는 비밀번호가 틀렸습니다');

View File

@@ -26,8 +26,12 @@ export const INVALID_CHIP_DAM_NAMES = ['불일치', '이력제부재'];
/** ===============================개별 제외 개체 목록 (분석불가 등 특수 사유) 하단 개체 정보없음 확인필요=================*/
export const EXCLUDED_COW_IDS = [
'KOR002191642861',
// 일치인데 정보가 없음 / 김정태님 유전체 내역 빠짐 1두
// 일치인데 정보가 없음
// 김정태님 유전체 내역 빠짐 1두
// 근데 유전자 검사내역은 있음
// 일단 모근 1회분량이고 재검사어려움 , 모근상태 불량으로 인한 DNA분해로 인해 분석불가 상태로 넣음
// 분석불가로 넣으면 유전자가 조회가 안됨
// 유전자가 조회될수 있는 조건은 불일치와 이력제부재만 가능 // 분석불가는 아예안되는듯
];
//=================================================================================================================
@@ -60,13 +64,7 @@ export function isValidGenomeAnalysis(
chipDamName: string | null | undefined,
cowId?: string | null,
): boolean {
// 1. 아비 일치 확인
if (chipSireName !== VALID_CHIP_SIRE_NAME) return false;
// 2. 어미 제외 조건 확인
if (chipDamName && INVALID_CHIP_DAM_NAMES.includes(chipDamName)) return false;
// 3. 개별 제외 개체 확인
// 개별 제외 개체만 확인 (부/모 불일치여도 유전자 데이터 있으면 표시)
if (cowId && EXCLUDED_COW_IDS.includes(cowId)) return false;
return true;
@@ -74,15 +72,15 @@ export function isValidGenomeAnalysis(
/**
* SQL WHERE 조건 생성 (TypeORM QueryBuilder용)
* 주의: cowId 제외 목록은 SQL에 포함되지 않으므로 별도 필터링 필요
* 부/모 불일치여도 유전자 데이터 있으면 표시하므로 조건 제거
*
* @param alias - 테이블 별칭 (예: 'request', 'genome')
* @returns SQL 조건 문자열
* @returns SQL 조건 문자열 (항상 true)
*
* @example
* queryBuilder.andWhere(getValidGenomeConditionSQL('request'));
*/
export function getValidGenomeConditionSQL(alias: string): string {
const damConditions = INVALID_CHIP_DAM_NAMES.map(name => `${alias}.chipDamName != '${name}'`).join(' AND ');
return `${alias}.chipSireName = '${VALID_CHIP_SIRE_NAME}' AND (${alias}.chipDamName IS NULL OR (${damConditions}))`;
// 부/모 불일치 조건 제거 - 유전자 데이터 있으면 모두 표시
return '1=1';
}

View File

@@ -9,6 +9,32 @@
* @constant
*/
export const MPT_NORMAL_RANGES = {
// ========== 에너지 카테고리 ==========
/**
* 혈당 (Glucose)
* 단위: mg/dL
*/
glucose: { min: 40, max: 84 },
/**
* 콜레스테롤 (Cholesterol)
* 단위: mg/dL
*/
cholesterol: { min: 74, max: 252 },
/**
* 유리지방산 (NEFA)
* 단위: μEq/L
*/
nefa: { min: 115, max: 660 },
// ========== 단백질 카테고리 ==========
/**
* 총단백질 (Total Protein)
* 단위: g/dL
*/
totalProtein: { min: 6.2, max: 7.7 },
/**
* 알부민 (Albumin)
* 단위: g/dL
@@ -17,7 +43,7 @@ export const MPT_NORMAL_RANGES = {
/**
* 총 글로불린 (Total Globulin)
* 단위: g/L
* 단위: g/dL
*/
totalGlobulin: { min: 9.1, max: 36.1 },
@@ -74,6 +100,13 @@ export const MPT_NORMAL_RANGES = {
* 단위: mg/dL
*/
magnesium: { min: 1.6, max: 3.3 },
// ========== 기타 카테고리 ==========
/**
* 크레아티닌 (Creatinine)
* 단위: mg/dL
*/
creatinine: { min: 1.0, max: 1.3 },
} as const;
/**