feat(WEB): Phase 6 IntegratedDetailTemplate 마이그레이션 완료
Phase 6 마이그레이션 (41개 컴포넌트 완료): - 건설/시공: 협력업체, 시공관리, 기성관리, 발주관리, 계약관리 등 - 영업: 견적관리(V2), 고객관리(V2), 수주관리 - 회계: 청구관리, 매입관리, 매출관리, 거래처관리, 악성채권 등 - 생산: 작업지시, 검수관리 - 출고: 출하관리 - 자재: 입고관리, 재고현황 - 고객센터: 문의관리, 이벤트관리, 공지관리 - 인사: 직원관리 - 설정: 권한관리 주요 변경사항: - 34개 xxxConfig.ts 파일 생성 (설정 기반 페이지 구성) - PageLayout/PageHeader → IntegratedDetailTemplate 통합 - 일관된 타이틀/버튼 영역 (목록, 상세, 수정, 삭제) - 1112줄 코드 감소 (중복 제거) 프로젝트 공통화 현황 분석 문서 추가: - 상세 페이지 62%, 목록 페이지 82% 공통화 달성 - 추가 공통화 기회 및 로드맵 정리 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
|
||||
/**
|
||||
* 입고 상세 페이지
|
||||
* IntegratedDetailTemplate 마이그레이션 완료 (2026-01-20)
|
||||
*
|
||||
* 상태에 따라 다른 UI 표시:
|
||||
* - 검사대기: 입고증, 목록, 검사등록 버튼
|
||||
* - 배송중/발주완료: 목록, 입고처리 버튼 (입고증 없음)
|
||||
@@ -9,14 +11,14 @@
|
||||
* - 입고완료: 입고증, 목록 버튼
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { useState, useCallback, useEffect, useMemo } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Package, FileText, List, ClipboardCheck, Download, AlertCircle } from 'lucide-react';
|
||||
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
|
||||
import { FileText, ClipboardCheck, Download } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||
import { receivingConfig } from './receivingConfig';
|
||||
import { ServerErrorPage } from '@/components/common/ServerErrorPage';
|
||||
import { getReceivingById, processReceiving } from './actions';
|
||||
import { RECEIVING_STATUS_LABELS, RECEIVING_STATUS_STYLES } from './types';
|
||||
@@ -116,84 +118,58 @@ export function ReceivingDetail({ id }: Props) {
|
||||
router.push('/ko/material/receiving-management');
|
||||
}, [router]);
|
||||
|
||||
// 로딩 상태 표시
|
||||
if (isLoading) {
|
||||
// 커스텀 헤더 액션 (상태별 버튼들)
|
||||
const customHeaderActions = useMemo(() => {
|
||||
if (!detail) return null;
|
||||
|
||||
// 상태별 버튼 구성
|
||||
const showInspectionButton = detail.status === 'inspection_pending';
|
||||
const showReceivingProcessButton =
|
||||
detail.status === 'order_completed' || detail.status === 'shipping';
|
||||
const showReceiptButton =
|
||||
detail.status === 'inspection_pending' || detail.status === 'completed';
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<ContentLoadingSpinner text="입고 정보를 불러오는 중..." />
|
||||
</PageLayout>
|
||||
<>
|
||||
{/* 발주번호와 상태 뱃지 */}
|
||||
<span className="text-lg text-muted-foreground">{detail.orderNo}</span>
|
||||
<Badge className={`${RECEIVING_STATUS_STYLES[detail.status]}`}>
|
||||
{RECEIVING_STATUS_LABELS[detail.status]}
|
||||
</Badge>
|
||||
{showReceiptButton && (
|
||||
<Button variant="outline" onClick={handleOpenReceipt}>
|
||||
<FileText className="w-4 h-4 mr-1.5" />
|
||||
입고증
|
||||
</Button>
|
||||
)}
|
||||
{showInspectionButton && (
|
||||
<Button onClick={handleGoToInspection}>
|
||||
<ClipboardCheck className="w-4 h-4 mr-1.5" />
|
||||
검사등록
|
||||
</Button>
|
||||
)}
|
||||
{showReceivingProcessButton && (
|
||||
<Button onClick={handleOpenReceivingProcessDialog}>
|
||||
<Download className="w-4 h-4 mr-1.5" />
|
||||
입고처리
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}, [detail, handleOpenReceipt, handleGoToInspection, handleOpenReceivingProcessDialog]);
|
||||
|
||||
// 폼 콘텐츠 렌더링
|
||||
const renderFormContent = useCallback(() => {
|
||||
if (!detail) return null;
|
||||
|
||||
// 입고 정보 표시 여부: 검사대기, 입고대기, 입고완료
|
||||
const showReceivingInfo =
|
||||
detail.status === 'inspection_pending' ||
|
||||
detail.status === 'receiving_pending' ||
|
||||
detail.status === 'completed';
|
||||
|
||||
// 에러 상태 표시
|
||||
if (error || !detail) {
|
||||
return (
|
||||
<ServerErrorPage
|
||||
title="입고 정보를 불러올 수 없습니다"
|
||||
message={error || '입고 정보를 찾을 수 없습니다.'}
|
||||
onRetry={loadData}
|
||||
showBackButton={true}
|
||||
showHomeButton={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 상태별 버튼 구성
|
||||
// 검사등록 버튼: 검사대기만
|
||||
const showInspectionButton = detail.status === 'inspection_pending';
|
||||
// 입고처리 버튼: 발주완료, 배송중만
|
||||
const showReceivingProcessButton =
|
||||
detail.status === 'order_completed' || detail.status === 'shipping';
|
||||
// 입고증 버튼: 검사대기, 입고완료만 (입고대기, 발주완료, 배송중은 없음)
|
||||
const showReceiptButton =
|
||||
detail.status === 'inspection_pending' || detail.status === 'completed';
|
||||
|
||||
// 입고 정보 표시 여부: 검사대기, 입고대기, 입고완료
|
||||
const showReceivingInfo =
|
||||
detail.status === 'inspection_pending' ||
|
||||
detail.status === 'receiving_pending' ||
|
||||
detail.status === 'completed';
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="space-y-6">
|
||||
{/* 헤더 */}
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm: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-lg text-muted-foreground">{detail.orderNo}</span>
|
||||
<Badge className={`${RECEIVING_STATUS_STYLES[detail.status]}`}>
|
||||
{RECEIVING_STATUS_LABELS[detail.status]}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{showReceiptButton && (
|
||||
<Button variant="outline" onClick={handleOpenReceipt}>
|
||||
<FileText className="w-4 h-4 mr-1.5" />
|
||||
입고증
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="outline" onClick={handleGoBack}>
|
||||
<List className="w-4 h-4 mr-1.5" />
|
||||
목록
|
||||
</Button>
|
||||
{showInspectionButton && (
|
||||
<Button onClick={handleGoToInspection}>
|
||||
<ClipboardCheck className="w-4 h-4 mr-1.5" />
|
||||
검사등록
|
||||
</Button>
|
||||
)}
|
||||
{showReceivingProcessButton && (
|
||||
<Button onClick={handleOpenReceivingProcessDialog}>
|
||||
<Download className="w-4 h-4 mr-1.5" />
|
||||
입고처리
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 발주 정보 */}
|
||||
<Card>
|
||||
<CardHeader className="pb-4">
|
||||
@@ -280,21 +256,53 @@ export function ReceivingDetail({ id }: Props) {
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}, [detail]);
|
||||
|
||||
// 에러 상태 표시
|
||||
if (!isLoading && (error || !detail)) {
|
||||
return (
|
||||
<ServerErrorPage
|
||||
title="입고 정보를 불러올 수 없습니다"
|
||||
message={error || '입고 정보를 찾을 수 없습니다.'}
|
||||
onRetry={loadData}
|
||||
showBackButton={true}
|
||||
showHomeButton={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<IntegratedDetailTemplate
|
||||
config={receivingConfig}
|
||||
mode="view"
|
||||
initialData={detail || {}}
|
||||
itemId={id}
|
||||
isLoading={isLoading}
|
||||
headerActions={customHeaderActions}
|
||||
renderView={() => renderFormContent()}
|
||||
renderForm={() => renderFormContent()}
|
||||
/>
|
||||
|
||||
{/* 입고증 다이얼로그 */}
|
||||
<ReceivingReceiptDialog
|
||||
open={isReceiptDialogOpen}
|
||||
onOpenChange={setIsReceiptDialogOpen}
|
||||
detail={detail}
|
||||
/>
|
||||
{detail && (
|
||||
<ReceivingReceiptDialog
|
||||
open={isReceiptDialogOpen}
|
||||
onOpenChange={setIsReceiptDialogOpen}
|
||||
detail={detail}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 입고처리 다이얼로그 */}
|
||||
<ReceivingProcessDialog
|
||||
open={isReceivingProcessDialogOpen}
|
||||
onOpenChange={setIsReceivingProcessDialogOpen}
|
||||
detail={detail}
|
||||
onComplete={handleReceivingComplete}
|
||||
/>
|
||||
{detail && (
|
||||
<ReceivingProcessDialog
|
||||
open={isReceivingProcessDialogOpen}
|
||||
onOpenChange={setIsReceivingProcessDialogOpen}
|
||||
detail={detail}
|
||||
onComplete={handleReceivingComplete}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 성공 다이얼로그 */}
|
||||
<SuccessDialog
|
||||
@@ -303,6 +311,6 @@ export function ReceivingDetail({ id }: Props) {
|
||||
lotNo={successDialog.lotNo}
|
||||
onClose={handleSuccessDialogClose}
|
||||
/>
|
||||
</PageLayout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Package } from 'lucide-react';
|
||||
import type { DetailConfig } from '@/components/templates/IntegratedDetailTemplate/types';
|
||||
|
||||
/**
|
||||
* 입고관리 상세 페이지 Config
|
||||
*
|
||||
* 참고: 이 config는 타이틀/버튼 영역만 정의
|
||||
* 폼 내용은 renderView에서 처리
|
||||
*
|
||||
* 특이사항:
|
||||
* - view 모드만 지원 (edit 없음)
|
||||
* - 상태별 동적 버튼 (입고증, 검사등록, 입고처리)
|
||||
* - 다이얼로그: 입고증, 입고처리, 성공
|
||||
*/
|
||||
export const receivingConfig: DetailConfig = {
|
||||
title: '입고 상세',
|
||||
description: '입고 정보를 조회합니다',
|
||||
icon: Package,
|
||||
basePath: '/material/receiving-management',
|
||||
fields: [], // renderView 사용으로 필드 정의 불필요
|
||||
gridColumns: 3,
|
||||
actions: {
|
||||
showBack: true,
|
||||
showDelete: false,
|
||||
showEdit: false, // 수정 기능 없음
|
||||
backLabel: '목록',
|
||||
},
|
||||
};
|
||||
@@ -2,14 +2,13 @@
|
||||
|
||||
/**
|
||||
* 재고현황 상세 페이지
|
||||
* IntegratedDetailTemplate 마이그레이션 완료 (2026-01-20)
|
||||
* 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 { AlertCircle } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import {
|
||||
@@ -20,7 +19,8 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||
import { stockStatusConfig } from './stockStatusConfig';
|
||||
import { ServerErrorPage } from '@/components/common/ServerErrorPage';
|
||||
import { getStockById } from './actions';
|
||||
import {
|
||||
@@ -85,51 +85,26 @@ export function StockStatusDetail({ id }: StockStatusDetailProps) {
|
||||
return detail.lots.reduce((sum, lot) => sum + lot.qty, 0);
|
||||
}, [detail]);
|
||||
|
||||
// 목록으로 돌아가기
|
||||
const handleGoBack = useCallback(() => {
|
||||
router.push('/ko/material/stock-status');
|
||||
}, [router]);
|
||||
// 커스텀 헤더 액션 (품목코드와 상태 뱃지)
|
||||
const customHeaderActions = useMemo(() => {
|
||||
if (!detail) return null;
|
||||
|
||||
// 로딩 상태 표시
|
||||
if (isLoading) {
|
||||
return (
|
||||
<PageLayout>
|
||||
<ContentLoadingSpinner text="재고 정보를 불러오는 중..." />
|
||||
</PageLayout>
|
||||
<>
|
||||
<span className="text-muted-foreground">{detail.itemCode}</span>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{STOCK_STATUS_LABELS[detail.status]}
|
||||
</Badge>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}, [detail]);
|
||||
|
||||
// 폼 콘텐츠 렌더링
|
||||
const renderFormContent = useCallback(() => {
|
||||
if (!detail) return null;
|
||||
|
||||
// 에러 상태 표시
|
||||
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">
|
||||
@@ -289,6 +264,32 @@ export function StockStatusDetail({ id }: StockStatusDetailProps) {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
}, [detail, totalQty, oldestLot]);
|
||||
|
||||
// 에러 상태 표시
|
||||
if (!isLoading && (error || !detail)) {
|
||||
return (
|
||||
<ServerErrorPage
|
||||
title="재고 정보를 불러올 수 없습니다"
|
||||
message={error || '재고 정보를 찾을 수 없습니다.'}
|
||||
onRetry={loadData}
|
||||
showBackButton={true}
|
||||
showHomeButton={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<IntegratedDetailTemplate
|
||||
config={stockStatusConfig}
|
||||
mode="view"
|
||||
initialData={detail || {}}
|
||||
itemId={id}
|
||||
isLoading={isLoading}
|
||||
headerActions={customHeaderActions}
|
||||
renderView={() => renderFormContent()}
|
||||
renderForm={() => renderFormContent()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
28
src/components/material/StockStatus/stockStatusConfig.ts
Normal file
28
src/components/material/StockStatus/stockStatusConfig.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Package } from 'lucide-react';
|
||||
import type { DetailConfig } from '@/components/templates/IntegratedDetailTemplate/types';
|
||||
|
||||
/**
|
||||
* 재고현황 상세 페이지 Config
|
||||
*
|
||||
* 참고: 이 config는 타이틀/버튼 영역만 정의
|
||||
* 폼 내용은 renderView에서 처리
|
||||
*
|
||||
* 특이사항:
|
||||
* - view 모드만 지원 (edit 없음)
|
||||
* - LOT별 상세 재고 테이블
|
||||
* - FIFO 권장 메시지
|
||||
*/
|
||||
export const stockStatusConfig: DetailConfig = {
|
||||
title: '재고 상세',
|
||||
description: '재고 정보를 조회합니다',
|
||||
icon: Package,
|
||||
basePath: '/material/stock-status',
|
||||
fields: [], // renderView 사용으로 필드 정의 불필요
|
||||
gridColumns: 3,
|
||||
actions: {
|
||||
showBack: true,
|
||||
showDelete: false,
|
||||
showEdit: false, // 수정 기능 없음
|
||||
backLabel: '목록',
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user