자재관리: - 입고관리 재고조정 다이얼로그 신규 추가, 상세/목록 기능 확장 - 재고현황 컴포넌트 리팩토링 출고관리: - 출하관리 생성/수정/목록/상세 개선 - 차량배차관리 상세/수정/목록 기능 보강 생산관리: - 작업지시서 WIP 생산 모달 신규 추가 - 벤딩WIP/슬랫조인트바 검사 콘텐츠 신규 추가 - 작업자화면 기능 대폭 확장 (카드/목록 개선) - 검사성적서 모달 개선 품질관리: - 실적보고서 관리 페이지 신규 추가 - 검사관리 문서/타입/목데이터 개선 단가관리: - 단가배포 페이지 및 컴포넌트 신규 추가 - 단가표 관리 페이지 및 컴포넌트 신규 추가 공통: - 권한 시스템 추가 개선 (PermissionContext, usePermission, PermissionGuard) - 메뉴 폴링 훅 개선, 레이아웃 수정 - 모바일 줌/패닝 CSS 수정 - locale 유틸 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
307 lines
11 KiB
TypeScript
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 }; }}
|
|
/>
|
|
);
|
|
} |