Files
sam-react-prod/src/components/material/StockStatus/StockStatusDetail.tsx
유병철 36322a0927 feat(WEB): Phase 4-5 V2 URL 패턴 통합 마이그레이션 완료
Phase 5 완료 (5개 페이지):
- 입금관리, 출금관리, 단가관리(건설): Pattern A (기존 V2 컴포넌트 활용)
- 판매수주관리, 품목관리: Pattern B (View/Edit 컴포넌트 분리)

신규 컴포넌트:
- OrderSalesDetailView.tsx, OrderSalesDetailEdit.tsx
- ItemDetailView.tsx, ItemDetailEdit.tsx

기타 정리:
- backup 파일 삭제 (Dashboard, ItemMasterDataManagement 등)
- 타입 정의 개선 (건설 도메인 types.ts)
- useAuthGuard 훅 정리

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-20 09:00:27 +09:00

294 lines
11 KiB
TypeScript

'use client';
/**
* 재고현황 상세 페이지
* API 연동 버전 (2025-12-26)
*/
import { useState, useMemo, useCallback, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { Package, AlertCircle, List } from 'lucide-react';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { PageLayout } from '@/components/organisms/PageLayout';
import { ServerErrorPage } from '@/components/common/ServerErrorPage';
import { getStockById } from './actions';
import {
ITEM_TYPE_LABELS,
ITEM_TYPE_STYLES,
STOCK_STATUS_LABELS,
LOT_STATUS_LABELS,
} from './types';
import type { StockDetail, LotDetail } from './types';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
interface StockStatusDetailProps {
id: string;
}
export function StockStatusDetail({ id }: StockStatusDetailProps) {
const router = useRouter();
// API 데이터 상태
const [detail, setDetail] = useState<StockDetail | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// API 데이터 로드
const loadData = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const result = await getStockById(id);
if (result.success && result.data) {
setDetail(result.data);
} else {
setError(result.error || '재고 정보를 찾을 수 없습니다.');
}
} catch (err) {
if (isNextRedirectError(err)) throw err;
console.error('[StockStatusDetail] loadData error:', err);
setError('데이터를 불러오는 중 오류가 발생했습니다.');
} finally {
setIsLoading(false);
}
}, [id]);
// 데이터 로드
useEffect(() => {
loadData();
}, [loadData]);
// 가장 오래된 LOT 찾기 (FIFO 권장용)
const oldestLot = useMemo(() => {
if (!detail || detail.lots.length === 0) return null;
return detail.lots.reduce((oldest, lot) =>
lot.daysElapsed > oldest.daysElapsed ? lot : oldest
);
}, [detail]);
// 총 수량 계산
const totalQty = useMemo(() => {
if (!detail) return 0;
return detail.lots.reduce((sum, lot) => sum + lot.qty, 0);
}, [detail]);
// 목록으로 돌아가기
const handleGoBack = useCallback(() => {
router.push('/ko/material/stock-status');
}, [router]);
// 로딩 상태 표시
if (isLoading) {
return (
<PageLayout>
<ContentLoadingSpinner text="재고 정보를 불러오는 중..." />
</PageLayout>
);
}
// 에러 상태 표시
if (error || !detail) {
return (
<ServerErrorPage
title="재고 정보를 불러올 수 없습니다"
message={error || '재고 정보를 찾을 수 없습니다.'}
onRetry={loadData}
showBackButton={true}
showHomeButton={true}
/>
);
}
return (
<PageLayout>
<div className="space-y-6">
{/* 헤더 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Package className="w-6 h-6" />
<h1 className="text-xl font-semibold"> </h1>
<span className="text-muted-foreground">{detail.itemCode}</span>
<Badge variant="outline" className="text-xs">
{STOCK_STATUS_LABELS[detail.status]}
</Badge>
</div>
<Button variant="outline" onClick={handleGoBack}>
<List className="h-4 w-4 mr-2" />
</Button>
</div>
{/* 기본 정보 */}
<Card>
<CardHeader className="pb-4">
<CardTitle className="text-base font-medium"> </CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<div className="text-sm text-muted-foreground mb-1"></div>
<div className="font-medium">{detail.itemCode}</div>
</div>
<div>
<div className="text-sm text-muted-foreground mb-1"></div>
<div className="font-medium">{detail.itemName}</div>
</div>
<div>
<div className="text-sm text-muted-foreground mb-1"></div>
<Badge className={`text-xs ${ITEM_TYPE_STYLES[detail.itemType]}`}>
{ITEM_TYPE_LABELS[detail.itemType]}
</Badge>
</div>
<div>
<div className="text-sm text-muted-foreground mb-1"></div>
<div className="font-medium">{detail.category}</div>
</div>
<div>
<div className="text-sm text-muted-foreground mb-1"></div>
<div className="font-medium">{detail.specification || '-'}</div>
</div>
<div>
<div className="text-sm text-muted-foreground mb-1"></div>
<div className="font-medium">{detail.unit}</div>
</div>
</div>
</CardContent>
</Card>
{/* 재고 현황 */}
<Card>
<CardHeader className="pb-4">
<CardTitle className="text-base font-medium"> </CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-3 gap-6">
<div>
<div className="text-sm text-muted-foreground mb-1"> </div>
<div className="text-2xl font-bold">
{detail.currentStock} <span className="text-base font-normal">{detail.unit}</span>
</div>
</div>
<div>
<div className="text-sm text-muted-foreground mb-1"> </div>
<div className="text-lg font-medium">
{detail.safetyStock} <span className="text-sm font-normal">{detail.unit}</span>
</div>
</div>
<div>
<div className="text-sm text-muted-foreground mb-1"> </div>
<div className="font-medium">{detail.location}</div>
</div>
<div>
<div className="text-sm text-muted-foreground mb-1">LOT </div>
<div className="font-medium">{detail.lotCount}</div>
</div>
<div>
<div className="text-sm text-muted-foreground mb-1"> </div>
<div className="font-medium">{detail.lastReceiptDate}</div>
</div>
<div>
<div className="text-sm text-muted-foreground mb-1"> </div>
<Badge variant="outline" className="text-xs">
{STOCK_STATUS_LABELS[detail.status]}
</Badge>
</div>
</div>
</CardContent>
</Card>
{/* LOT별 상세 재고 */}
<Card>
<CardHeader className="pb-4">
<div className="flex items-center justify-between">
<CardTitle className="text-base font-medium">LOT별 </CardTitle>
<div className="text-sm text-muted-foreground">
FIFO · LOT부터
</div>
</div>
</CardHeader>
<CardContent className="p-0">
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow className="bg-muted/50">
<TableHead className="w-[60px] text-center">FIFO</TableHead>
<TableHead className="min-w-[100px]">LOT번호</TableHead>
<TableHead className="w-[100px]"></TableHead>
<TableHead className="w-[70px] text-center"></TableHead>
<TableHead className="min-w-[100px]"></TableHead>
<TableHead className="min-w-[120px]"></TableHead>
<TableHead className="w-[80px] text-center"></TableHead>
<TableHead className="w-[60px] text-center"></TableHead>
<TableHead className="w-[80px] text-center"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{detail.lots.map((lot: LotDetail) => (
<TableRow key={lot.id}>
<TableCell className="text-center">
<div className="inline-flex items-center justify-center w-6 h-6 rounded-full bg-primary text-primary-foreground text-xs font-medium">
{lot.fifoOrder}
</div>
</TableCell>
<TableCell className="font-medium">{lot.lotNo}</TableCell>
<TableCell>{lot.receiptDate}</TableCell>
<TableCell className="text-center">
<span className={lot.daysElapsed > 30 ? 'text-orange-600 font-medium' : ''}>
{lot.daysElapsed}
</span>
</TableCell>
<TableCell>{lot.supplier}</TableCell>
<TableCell>{lot.poNumber}</TableCell>
<TableCell className="text-center">
{lot.qty} {lot.unit}
</TableCell>
<TableCell className="text-center">{lot.location}</TableCell>
<TableCell className="text-center">
<Badge variant="outline" className="text-xs">
{LOT_STATUS_LABELS[lot.status]}
</Badge>
</TableCell>
</TableRow>
))}
{/* 합계 행 */}
<TableRow className="bg-muted/30 font-medium">
<TableCell colSpan={6} className="text-right">
:
</TableCell>
<TableCell className="text-center">
{totalQty} {detail.unit}
</TableCell>
<TableCell colSpan={2} />
</TableRow>
</TableBody>
</Table>
</div>
</CardContent>
</Card>
{/* FIFO 권장 메시지 */}
{oldestLot && oldestLot.daysElapsed > 30 && (
<div className="flex items-start gap-2 p-4 bg-orange-50 border border-orange-200 rounded-lg">
<AlertCircle className="w-5 h-5 text-orange-600 flex-shrink-0 mt-0.5" />
<div className="text-sm text-orange-800">
<span className="font-medium">FIFO :</span> LOT {oldestLot.lotNo}{' '}
{oldestLot.daysElapsed} . .
</div>
</div>
)}
</div>
</PageLayout>
);
}