Files
sam-react-prod/src/components/material/StockStatus/StockStatusDetail.tsx
유병철 c1b63b850a feat(WEB): 자재/출고/생산/품질/단가 기능 대폭 개선 및 신규 페이지 추가
자재관리:
- 입고관리 재고조정 다이얼로그 신규 추가, 상세/목록 기능 확장
- 재고현황 컴포넌트 리팩토링

출고관리:
- 출하관리 생성/수정/목록/상세 개선
- 차량배차관리 상세/수정/목록 기능 보강

생산관리:
- 작업지시서 WIP 생산 모달 신규 추가
- 벤딩WIP/슬랫조인트바 검사 콘텐츠 신규 추가
- 작업자화면 기능 대폭 확장 (카드/목록 개선)
- 검사성적서 모달 개선

품질관리:
- 실적보고서 관리 페이지 신규 추가
- 검사관리 문서/타입/목데이터 개선

단가관리:
- 단가배포 페이지 및 컴포넌트 신규 추가
- 단가표 관리 페이지 및 컴포넌트 신규 추가

공통:
- 권한 시스템 추가 개선 (PermissionContext, usePermission, PermissionGuard)
- 메뉴 폴링 훅 개선, 레이아웃 수정
- 모바일 줌/패닝 CSS 수정
- locale 유틸 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 12:46:19 +09:00

307 lines
11 KiB
TypeScript

'use client';
/**
* 재고현황 상세/수정 페이지
*
* 기획서 기준:
* - 기본 정보: 자재번호, 품목코드, 품목유형, 품목명, 규격, 단위, 재고량 (읽기 전용)
* - 수정 가능: 안전재고, 상태 (사용/미사용)
*/
import { useState, useCallback, useEffect } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
import { stockStatusConfig } from './stockStatusConfig';
import { ServerErrorPage } from '@/components/common/ServerErrorPage';
import { getStockById, updateStock } from './actions';
import { USE_STATUS_LABELS, ITEM_TYPE_LABELS } from './types';
import type { ItemType } from './types';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
import { toast } from 'sonner';
interface StockStatusDetailProps {
id: string;
}
// 상세 페이지용 데이터 타입
interface StockDetailData {
id: string;
stockNumber: string;
itemCode: string;
itemType: ItemType;
itemName: string;
specification: string;
unit: string;
calculatedQty: number;
safetyStock: number;
useStatus: 'active' | 'inactive';
}
export function StockStatusDetail({ id }: StockStatusDetailProps) {
const router = useRouter();
const searchParams = useSearchParams();
const initialMode = searchParams.get('mode') === 'edit' ? 'edit' : 'view';
// API 데이터 상태
const [detail, setDetail] = useState<StockDetailData | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// 폼 데이터 (수정 모드용)
const [formData, setFormData] = useState<{
safetyStock: number;
useStatus: 'active' | 'inactive';
}>({
safetyStock: 0,
useStatus: 'active',
});
// 저장 중 상태
const [isSaving, setIsSaving] = useState(false);
// API 데이터 로드
const loadData = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const result = await getStockById(id);
if (result.success && result.data) {
const data = result.data;
// API 응답을 상세 페이지용 데이터로 변환
const detailData: StockDetailData = {
id: data.id,
stockNumber: data.id, // stockNumber가 없으면 id 사용
itemCode: data.itemCode,
itemType: (data.itemType || 'RM') as ItemType,
itemName: data.itemName,
specification: data.specification || '-',
unit: data.unit,
calculatedQty: data.currentStock, // 재고량
safetyStock: data.safetyStock,
useStatus: data.status === null ? 'active' : 'active', // 기본값
};
setDetail(detailData);
setFormData({
safetyStock: detailData.safetyStock,
useStatus: detailData.useStatus,
});
} 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]);
// 폼 값 변경 핸들러
const handleInputChange = (field: keyof typeof formData, value: string | number) => {
setFormData((prev) => ({
...prev,
[field]: field === 'useStatus' ? value : Number(value),
}));
};
// 저장 핸들러
const handleSave = async () => {
if (!detail) return;
setIsSaving(true);
try {
const result = await updateStock(id, formData);
if (result.success) {
toast.success('재고 정보가 저장되었습니다.');
// 상세 데이터 업데이트
setDetail((prev) =>
prev
? {
...prev,
safetyStock: formData.safetyStock,
useStatus: formData.useStatus,
}
: null
);
// view 모드로 전환
router.push(`/ko/material/stock-status/${id}?mode=view`);
} else {
toast.error(result.error || '저장에 실패했습니다.');
}
} catch (err) {
if (isNextRedirectError(err)) throw err;
console.error('[StockStatusDetail] handleSave error:', err);
toast.error('저장 중 오류가 발생했습니다.');
} finally {
setIsSaving(false);
}
};
// 읽기 전용 필드 렌더링 (수정 모드에서 구분용)
const renderReadOnlyField = (label: string, value: string | number, isEditMode = false) => (
<div>
<Label className="text-sm text-muted-foreground">{label}</Label>
{isEditMode ? (
// 수정 모드: 읽기 전용임을 명확히 표시 (어두운 배경 + cursor-not-allowed)
<div className="mt-1.5 px-3 py-2 bg-gray-200 border border-gray-300 rounded-md text-sm text-gray-500 cursor-not-allowed select-none">
{value}
</div>
) : (
// 보기 모드: 일반 텍스트 스타일
<div className="mt-1.5 px-3 py-2 bg-gray-50 border rounded-md text-sm">
{value}
</div>
)}
</div>
);
// 상세 보기 모드 렌더링
const renderViewContent = useCallback(() => {
if (!detail) return null;
return (
<Card>
<CardHeader className="pb-4">
<CardTitle className="text-base font-medium"> </CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-6">
{/* Row 1: 자재번호, 품목코드, 품목유형, 품목명 */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{renderReadOnlyField('자재번호', detail.stockNumber)}
{renderReadOnlyField('품목코드', detail.itemCode)}
{renderReadOnlyField('품목유형', ITEM_TYPE_LABELS[detail.itemType] || '-')}
{renderReadOnlyField('품목명', detail.itemName)}
</div>
{/* Row 2: 규격, 단위, 재고량, 안전재고 */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{renderReadOnlyField('규격', detail.specification)}
{renderReadOnlyField('단위', detail.unit)}
{renderReadOnlyField('재고량', detail.calculatedQty)}
{renderReadOnlyField('안전재고', detail.safetyStock)}
</div>
{/* Row 3: 상태 */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{renderReadOnlyField('상태', USE_STATUS_LABELS[detail.useStatus])}
</div>
</div>
</CardContent>
</Card>
);
}, [detail]);
// 수정 모드 렌더링
const renderFormContent = useCallback(() => {
if (!detail) return null;
return (
<Card>
<CardHeader className="pb-4">
<CardTitle className="text-base font-medium"> </CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-6">
{/* Row 1: 자재번호, 품목코드, 품목유형, 품목명 (읽기 전용) */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{renderReadOnlyField('자재번호', detail.stockNumber, true)}
{renderReadOnlyField('품목코드', detail.itemCode, true)}
{renderReadOnlyField('품목유형', ITEM_TYPE_LABELS[detail.itemType] || '-', true)}
{renderReadOnlyField('품목명', detail.itemName, true)}
</div>
{/* Row 2: 규격, 단위, 재고량 (읽기 전용) + 안전재고 (수정 가능) */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{renderReadOnlyField('규격', detail.specification, true)}
{renderReadOnlyField('단위', detail.unit, true)}
{renderReadOnlyField('재고량', detail.calculatedQty, true)}
{/* 안전재고 (수정 가능) */}
<div>
<Label htmlFor="safetyStock" className="text-sm text-muted-foreground">
</Label>
<Input
id="safetyStock"
type="number"
value={formData.safetyStock}
onChange={(e) => handleInputChange('safetyStock', e.target.value)}
className="mt-1.5 border-gray-300 focus:border-blue-500 focus:ring-blue-500"
min={0}
/>
</div>
</div>
{/* Row 3: 상태 (수정 가능) */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<Label htmlFor="useStatus" className="text-sm text-muted-foreground">
</Label>
<Select
key={`useStatus-${formData.useStatus}`}
value={formData.useStatus}
onValueChange={(value) => handleInputChange('useStatus', value)}
>
<SelectTrigger className="mt-1.5 border-gray-300 focus:border-blue-500 focus:ring-blue-500">
<SelectValue placeholder="상태 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="active"></SelectItem>
<SelectItem value="inactive"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
</CardContent>
</Card>
);
}, [detail, formData]);
// 에러 상태 표시
if (!isLoading && (error || !detail)) {
return (
<ServerErrorPage
title="재고 정보를 불러올 수 없습니다"
message={error || '재고 정보를 찾을 수 없습니다.'}
onRetry={loadData}
showBackButton={true}
showHomeButton={true}
/>
);
}
return (
<IntegratedDetailTemplate
config={stockStatusConfig}
mode={initialMode as 'view' | 'edit'}
initialData={(detail || undefined) as Record<string, unknown> | undefined}
itemId={id}
isLoading={isLoading}
renderView={() => renderViewContent()}
renderForm={() => renderFormContent()}
onSubmit={async () => { await handleSave(); return { success: true }; }}
/>
);
}