feat(WEB): 생산/검사 기능 대폭 확장 및 작업자화면 검사입력 추가

생산관리:
- WipProductionModal 기능 개선
- WorkOrderDetail/Edit 확장 (+265줄)
- 검사성적서 콘텐츠 5종 대폭 확장 (벤딩/벤딩WIP/스크린/슬랫/슬랫조인트바)
- InspectionReportModal 기능 강화

작업자화면:
- WorkerScreen 기능 대폭 확장 (+211줄)
- WorkItemCard 개선
- InspectionInputModal 신규 추가 (작업자 검사입력)

공정관리:
- StepForm 검사항목 설정 기능 추가
- InspectionSettingModal 신규 추가
- InspectionPreviewModal 신규 추가
- process.ts 타입 확장 (+102줄)

자재관리:
- StockStatus 상세/목록/타입/목데이터 개선

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-02-05 21:43:28 +09:00
parent 32d6e3bbbd
commit efcc645e24
21 changed files with 2559 additions and 328 deletions

View File

@@ -44,6 +44,7 @@ interface StockDetailData {
unit: string;
calculatedQty: number;
safetyStock: number;
wipStatus: 'active' | 'inactive';
useStatus: 'active' | 'inactive';
}
@@ -57,7 +58,7 @@ export function StockStatusDetail({ id }: StockStatusDetailProps) {
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// 폼 데이터 (수정 모드용)
// 폼 데이터 (수정 모드용) - wipStatus는 읽기 전용이므로 제외
const [formData, setFormData] = useState<{
safetyStock: number;
useStatus: 'active' | 'inactive';
@@ -90,6 +91,7 @@ export function StockStatusDetail({ id }: StockStatusDetailProps) {
unit: data.unit,
calculatedQty: data.currentStock, // 재고량
safetyStock: data.safetyStock,
wipStatus: 'active', // 재공품 상태 (기본값: 사용)
useStatus: data.status === null ? 'active' : 'active', // 기본값
};
setDetail(detailData);
@@ -201,8 +203,9 @@ export function StockStatusDetail({ id }: StockStatusDetailProps) {
{renderReadOnlyField('안전재고', detail.safetyStock)}
</div>
{/* Row 3: 상태 */}
{/* Row 3: 재공품, 상태 */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{renderReadOnlyField('재공품', USE_STATUS_LABELS[detail.wipStatus])}
{renderReadOnlyField('상태', USE_STATUS_LABELS[detail.useStatus])}
</div>
</div>
@@ -252,8 +255,11 @@ export function StockStatusDetail({ id }: StockStatusDetailProps) {
</div>
</div>
{/* Row 3: 상태 (수정 가능) */}
{/* Row 3: 재공품 (읽기 전용), 상태 (수정 가능) */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{/* 재공품 (읽기 전용) */}
{renderReadOnlyField('재공품', USE_STATUS_LABELS[detail.wipStatus], true)}
{/* 상태 (수정 가능) */}
<div>
<Label htmlFor="useStatus" className="text-sm text-muted-foreground">

View File

@@ -60,6 +60,7 @@ export function StockStatusList() {
// ===== 검색 및 필터 상태 =====
const [searchTerm, setSearchTerm] = useState('');
const [filterValues, setFilterValues] = useState<Record<string, string | string[]>>({
wipStatus: 'all',
useStatus: 'all',
});
@@ -134,6 +135,7 @@ export function StockStatusList() {
{ header: '단위', key: 'unit' },
{ header: '재고량', key: 'calculatedQty' },
{ header: '안전재고', key: 'safetyStock' },
{ header: '재공품', key: 'wipQty' },
{ header: '상태', key: 'useStatus', transform: (value) => USE_STATUS_LABELS[value as 'active' | 'inactive'] || '-' },
];
@@ -156,6 +158,7 @@ export function StockStatusList() {
actualQty: hasStock ? (parseFloat(String(stock?.actual_qty ?? stock?.stock_qty)) || 0) : 0,
stockQty: hasStock ? (parseFloat(String(stock?.stock_qty)) || 0) : 0,
safetyStock: hasStock ? (parseFloat(String(stock?.safety_stock)) || 0) : 0,
wipQty: hasStock ? (parseFloat(String(stock?.wip_qty)) || 0) : 0,
lotCount: hasStock ? (Number(stock?.lot_count) || 0) : 0,
lotDaysElapsed: hasStock ? (Number(stock?.days_elapsed) || 0) : 0,
status: hasStock ? (stock?.status as StockStatusType | null) : null,
@@ -194,14 +197,23 @@ export function StockStatusList() {
},
];
// ===== 필터 설정 (전체/사용/미사용) =====
// ===== 필터 설정 (재공품, 상태) =====
// 참고: IntegratedListTemplateV2에서 자동으로 '전체' 옵션을 추가하므로 options에서 제외
const filterConfig: FilterFieldConfig[] = [
{
key: 'wipStatus',
label: '재공품',
type: 'single',
options: [
{ value: 'active', label: '사용' },
{ value: 'inactive', label: '미사용' },
],
},
{
key: 'useStatus',
label: '상태',
type: 'single',
options: [
{ value: 'all', label: '전체' },
{ value: 'active', label: '사용' },
{ value: 'inactive', label: '미사용' },
],
@@ -219,6 +231,7 @@ export function StockStatusList() {
{ key: 'unit', label: '단위', className: 'w-[60px] text-center' },
{ key: 'calculatedQty', label: '재고량', className: 'w-[80px] text-center' },
{ key: 'safetyStock', label: '안전재고', className: 'w-[80px] text-center' },
{ key: 'wipQty', label: '재공품', className: 'w-[80px] text-center' },
{ key: 'useStatus', label: '상태', className: 'w-[80px] text-center' },
];
@@ -250,6 +263,7 @@ export function StockStatusList() {
<TableCell className="text-center">{item.unit}</TableCell>
<TableCell className="text-center">{item.calculatedQty}</TableCell>
<TableCell className="text-center">{item.safetyStock}</TableCell>
<TableCell className="text-center">{item.wipQty}</TableCell>
<TableCell className="text-center">
<span className={item.useStatus === 'inactive' ? 'text-gray-400' : ''}>
{USE_STATUS_LABELS[item.useStatus]}
@@ -296,6 +310,7 @@ export function StockStatusList() {
<InfoField label="단위" value={item.unit} />
<InfoField label="재고량" value={`${item.calculatedQty}`} />
<InfoField label="안전재고" value={`${item.safetyStock}`} />
<InfoField label="재공품" value={`${item.wipQty}`} />
</div>
}
actions={
@@ -361,6 +376,13 @@ export function StockStatusList() {
customFilterFn: (items, fv) => {
if (!items || items.length === 0) return items;
return items.filter((item) => {
// 재공품 필터 (사용: wipQty > 0, 미사용: wipQty === 0)
const wipStatusVal = fv.wipStatus as string;
if (wipStatusVal && wipStatusVal !== 'all') {
if (wipStatusVal === 'active' && item.wipQty === 0) return false;
if (wipStatusVal === 'inactive' && item.wipQty > 0) return false;
}
// 상태 필터
const useStatusVal = fv.useStatus as string;
if (useStatusVal && useStatusVal !== 'all' && item.useStatus !== useStatusVal) {
return false;

View File

@@ -137,6 +137,7 @@ function transformApiToListItem(data: ItemApiData): StockItem {
actualQty: hasStock ? (parseFloat(String((stock as unknown as Record<string, unknown>).actual_qty ?? stock.stock_qty)) || 0) : 0,
stockQty: hasStock ? (parseFloat(String(stock.stock_qty)) || 0) : 0,
safetyStock: hasStock ? (parseFloat(String(stock.safety_stock)) || 0) : 0,
wipQty: hasStock ? (parseFloat(String((stock as unknown as Record<string, unknown>).wip_qty)) || 0) : 0,
lotCount: hasStock ? (stock.lot_count || 0) : 0,
lotDaysElapsed: hasStock ? (stock.days_elapsed || 0) : 0,
status: hasStock ? stock.status : null,

View File

@@ -51,6 +51,7 @@ const rawMaterialItems: StockItem[] = [
actualQty: 500,
stockQty: 500,
safetyStock: 100,
wipQty: 30,
lotCount: 3,
lotDaysElapsed: 21,
status: 'normal',
@@ -70,6 +71,7 @@ const rawMaterialItems: StockItem[] = [
actualQty: 350,
stockQty: 350,
safetyStock: 80,
wipQty: 20,
lotCount: 2,
lotDaysElapsed: 15,
status: 'normal',
@@ -89,6 +91,7 @@ const rawMaterialItems: StockItem[] = [
actualQty: 280,
stockQty: 280,
safetyStock: 70,
wipQty: 15,
lotCount: 2,
lotDaysElapsed: 18,
status: 'normal',
@@ -108,6 +111,7 @@ const rawMaterialItems: StockItem[] = [
actualQty: 420,
stockQty: 420,
safetyStock: 90,
wipQty: 25,
lotCount: 4,
lotDaysElapsed: 12,
status: 'normal',
@@ -139,6 +143,7 @@ const bentPartItems: StockItem[] = Array.from({ length: 41 }, (_, i) => {
actualQty: stockQty,
stockQty,
safetyStock,
wipQty: seededInt(seed + 5, 0, 50),
lotCount: seededInt(seed + 2, 1, 5),
lotDaysElapsed: seededInt(seed + 3, 0, 45),
status: getStockStatus(stockQty, safetyStock),
@@ -172,6 +177,7 @@ const purchasedPartItems: StockItem[] = [
actualQty: stockQty,
stockQty,
safetyStock,
wipQty: seededInt(seed + 5, 0, 30),
lotCount: seededInt(seed + 2, 2, 5),
lotDaysElapsed: seededInt(seed + 3, 0, 40),
status: getStockStatus(stockQty, safetyStock),
@@ -202,6 +208,7 @@ const purchasedPartItems: StockItem[] = [
actualQty: stockQty,
stockQty,
safetyStock,
wipQty: seededInt(seed + 5, 0, 25),
lotCount: seededInt(seed + 2, 2, 4),
lotDaysElapsed: seededInt(seed + 3, 0, 35),
status: getStockStatus(stockQty, safetyStock),
@@ -234,6 +241,7 @@ const purchasedPartItems: StockItem[] = [
actualQty: stockQty,
stockQty,
safetyStock,
wipQty: seededInt(seed + 5, 0, 10),
lotCount: seededInt(seed + 2, 1, 3),
lotDaysElapsed: seededInt(seed + 3, 0, 30),
status: getStockStatus(stockQty, safetyStock),
@@ -264,6 +272,7 @@ const purchasedPartItems: StockItem[] = [
actualQty: stockQty,
stockQty,
safetyStock,
wipQty: seededInt(seed + 5, 0, 100),
lotCount: seededInt(seed + 2, 3, 6),
lotDaysElapsed: seededInt(seed + 3, 0, 25),
status: getStockStatus(stockQty, safetyStock),
@@ -292,6 +301,7 @@ const purchasedPartItems: StockItem[] = [
actualQty: stockQty,
stockQty,
safetyStock,
wipQty: seededInt(seed + 5, 0, 20),
lotCount: seededInt(seed + 2, 2, 4),
lotDaysElapsed: seededInt(seed + 3, 0, 20),
status: getStockStatus(stockQty, safetyStock),
@@ -322,6 +332,7 @@ const purchasedPartItems: StockItem[] = [
actualQty: stockQty,
stockQty,
safetyStock,
wipQty: seededInt(seed + 5, 0, 40),
lotCount: seededInt(seed + 2, 2, 5),
lotDaysElapsed: seededInt(seed + 3, 0, 30),
status: getStockStatus(stockQty, safetyStock),
@@ -346,6 +357,7 @@ const subMaterialItems: StockItem[] = [
actualQty: 5000,
stockQty: 5000,
safetyStock: 1000,
wipQty: 100,
lotCount: 3,
lotDaysElapsed: 28,
status: 'normal',
@@ -365,6 +377,7 @@ const subMaterialItems: StockItem[] = [
actualQty: 120,
stockQty: 120,
safetyStock: 30,
wipQty: 10,
lotCount: 1,
lotDaysElapsed: 5,
status: 'normal',
@@ -384,6 +397,7 @@ const subMaterialItems: StockItem[] = [
actualQty: 800,
stockQty: 800,
safetyStock: 200,
wipQty: 50,
lotCount: 2,
lotDaysElapsed: 12,
status: 'normal',
@@ -403,6 +417,7 @@ const subMaterialItems: StockItem[] = [
actualQty: 200,
stockQty: 200,
safetyStock: 50,
wipQty: 15,
lotCount: 5,
lotDaysElapsed: 37,
status: 'normal',
@@ -422,6 +437,7 @@ const subMaterialItems: StockItem[] = [
actualQty: 150,
stockQty: 150,
safetyStock: 40,
wipQty: 8,
lotCount: 2,
lotDaysElapsed: 10,
status: 'normal',
@@ -441,6 +457,7 @@ const subMaterialItems: StockItem[] = [
actualQty: 3000,
stockQty: 3000,
safetyStock: 500,
wipQty: 200,
lotCount: 4,
lotDaysElapsed: 8,
status: 'normal',
@@ -460,6 +477,7 @@ const subMaterialItems: StockItem[] = [
actualQty: 2500,
stockQty: 2500,
safetyStock: 400,
wipQty: 150,
lotCount: 3,
lotDaysElapsed: 15,
status: 'normal',
@@ -483,6 +501,7 @@ const consumableItems: StockItem[] = [
actualQty: 200,
stockQty: 200,
safetyStock: 50,
wipQty: 20,
lotCount: 2,
lotDaysElapsed: 8,
status: 'normal',
@@ -502,6 +521,7 @@ const consumableItems: StockItem[] = [
actualQty: 350,
stockQty: 350,
safetyStock: 80,
wipQty: 30,
lotCount: 3,
lotDaysElapsed: 5,
status: 'normal',

View File

@@ -64,6 +64,7 @@ export interface StockItem {
actualQty: number; // 실제 재고량 (Stock.actual_qty)
stockQty: number; // Stock.stock_qty (없으면 0)
safetyStock: number; // Stock.safety_stock (없으면 0)
wipQty: number; // 재공품 수량 (Stock.wip_qty, 없으면 0)
lotCount: number; // Stock.lot_count (없으면 0)
lotDaysElapsed: number; // Stock.days_elapsed (없으면 0)
status: StockStatusType | null; // Stock.status (없으면 null)