fix: [material] BOM 트리뷰어 개선 + 입고관리 다이얼로그 보강
This commit is contained in:
@@ -4,7 +4,9 @@
|
||||
* BOM Tree 시각화 컴포넌트
|
||||
*
|
||||
* API: GET /api/proxy/items/{id}/bom/tree
|
||||
* 재귀적 트리 렌더링 + 유형별 뱃지 색상 + 펼침/접힘
|
||||
* 3단계 트리: FG(루트) → CAT(카테고리 그룹) → PT(부품)
|
||||
* CAT 노드: 카테고리 헤더로 렌더링 (접힘/펼침, count 표시)
|
||||
* PT 노드: 품목 행으로 렌더링 (코드, 품목명, 수량, 단위)
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
@@ -22,7 +24,8 @@ interface BomTreeNode {
|
||||
item_type: string;
|
||||
specification?: string;
|
||||
unit?: string;
|
||||
quantity: number;
|
||||
quantity?: number;
|
||||
count?: number; // CAT 노드 — 하위 품목 건수
|
||||
depth: number;
|
||||
children: BomTreeNode[];
|
||||
}
|
||||
@@ -38,60 +41,70 @@ const ITEM_TYPE_COLORS: Record<string, string> = {
|
||||
SF: 'bg-cyan-100 text-cyan-800',
|
||||
};
|
||||
|
||||
const ITEM_TYPE_LABELS: Record<string, string> = {
|
||||
FG: '완제품',
|
||||
PT: '부품',
|
||||
RM: '원자재',
|
||||
SM: '부자재',
|
||||
CS: '소모품',
|
||||
BN: '절곡품',
|
||||
SF: '반제품',
|
||||
};
|
||||
|
||||
// 모든 노드의 ID를 재귀적으로 수집
|
||||
function collectNodeIds(nodes: BomTreeNode[]): Set<number> {
|
||||
const ids = new Set<number>();
|
||||
function walk(node: BomTreeNode) {
|
||||
ids.add(node.id);
|
||||
node.children.forEach(walk);
|
||||
}
|
||||
nodes.forEach(walk);
|
||||
return ids;
|
||||
// 노드별 고유 키 생성 (CAT 노드는 id=0이므로 이름 기반)
|
||||
function getNodeKey(node: BomTreeNode, index: number): string {
|
||||
if (node.item_type === 'CAT') return `cat-${index}-${node.name}`;
|
||||
return `item-${node.id}`;
|
||||
}
|
||||
|
||||
// 개별 트리 노드 컴포넌트
|
||||
function BomTreeNodeItem({
|
||||
// 모든 노드의 키를 재귀적으로 수집
|
||||
function collectAllKeys(nodes: BomTreeNode[]): Set<string> {
|
||||
const keys = new Set<string>();
|
||||
function walk(node: BomTreeNode, index: number) {
|
||||
keys.add(getNodeKey(node, index));
|
||||
node.children?.forEach((child, i) => walk(child, i));
|
||||
}
|
||||
nodes.forEach((node, i) => walk(node, i));
|
||||
return keys;
|
||||
}
|
||||
|
||||
// 카테고리 노드 컴포넌트
|
||||
function CategoryNode({
|
||||
node,
|
||||
level = 0,
|
||||
expandedNodes,
|
||||
nodeKey,
|
||||
isOpen,
|
||||
onToggle,
|
||||
}: {
|
||||
node: BomTreeNode;
|
||||
level?: number;
|
||||
expandedNodes: Set<number>;
|
||||
onToggle: (id: number) => void;
|
||||
nodeKey: string;
|
||||
isOpen: boolean;
|
||||
onToggle: (key: string) => void;
|
||||
}) {
|
||||
const hasChildren = node.children.length > 0;
|
||||
const isOpen = expandedNodes.has(node.id);
|
||||
const hasChildren = node.children && node.children.length > 0;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* 카테고리 헤더 */}
|
||||
<div
|
||||
className="flex items-center gap-2 py-1.5 px-2 hover:bg-gray-50 rounded cursor-default"
|
||||
style={{ paddingLeft: level * 24 + 8 }}
|
||||
className="flex items-center gap-2 py-2 px-3 bg-gray-50 hover:bg-gray-100 cursor-pointer rounded-sm"
|
||||
onClick={() => hasChildren && onToggle(nodeKey)}
|
||||
>
|
||||
{/* 펼침/접힘 */}
|
||||
{hasChildren ? (
|
||||
<button
|
||||
onClick={() => onToggle(node.id)}
|
||||
className="w-5 h-5 flex items-center justify-center text-gray-500 hover:text-gray-800 shrink-0"
|
||||
>
|
||||
{isOpen ? <ChevronDown className="w-4 h-4" /> : <ChevronRight className="w-4 h-4" />}
|
||||
</button>
|
||||
) : (
|
||||
<span className="w-5 shrink-0" />
|
||||
{hasChildren && (
|
||||
isOpen
|
||||
? <ChevronDown className="h-4 w-4 text-gray-500 shrink-0" />
|
||||
: <ChevronRight className="h-4 w-4 text-gray-500 shrink-0" />
|
||||
)}
|
||||
<span className="text-sm font-semibold text-gray-700">{node.name}</span>
|
||||
<Badge variant="outline" className="text-xs">{node.count ?? node.children?.length ?? 0}건</Badge>
|
||||
</div>
|
||||
|
||||
{/* 하위 품목 (접힘/펼침) */}
|
||||
{isOpen && hasChildren && (
|
||||
<div className="border-l-2 border-gray-200 ml-3">
|
||||
{node.children.map((child, i) => (
|
||||
<ItemNode key={child.id || `child-${i}`} node={child} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 품목(PT) 노드 컴포넌트
|
||||
function ItemNode({ node }: { node: BomTreeNode }) {
|
||||
return (
|
||||
<div className="py-1.5 px-3 ml-3 hover:bg-gray-50 rounded-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 유형 뱃지 */}
|
||||
<Badge
|
||||
variant="outline"
|
||||
@@ -100,56 +113,72 @@ function BomTreeNodeItem({
|
||||
{node.item_type}
|
||||
</Badge>
|
||||
|
||||
{/* 코드 */}
|
||||
<code className="text-xs bg-gray-100 px-1.5 py-0.5 rounded text-gray-600 shrink-0">
|
||||
{/* 코드 — PC만 인라인 표시 */}
|
||||
<code className="text-xs text-gray-400 shrink-0 max-w-[180px] truncate hidden md:inline">
|
||||
{node.code}
|
||||
</code>
|
||||
|
||||
{/* 품목명 */}
|
||||
<span className="text-sm truncate">{node.name}</span>
|
||||
<span className="text-sm text-gray-700 flex-1 min-w-0 truncate">{node.name}</span>
|
||||
|
||||
{/* 규격 */}
|
||||
{node.specification && (
|
||||
<span className="text-xs text-muted-foreground truncate hidden md:inline">
|
||||
({node.specification})
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* 수량 */}
|
||||
<span className="text-sm text-blue-600 font-medium ml-auto shrink-0">
|
||||
×{node.quantity}
|
||||
{/* 수량 + 단위 */}
|
||||
<span className="text-xs shrink-0 whitespace-nowrap">
|
||||
{node.quantity != null && (
|
||||
<span className="font-medium text-blue-600">x{node.quantity}</span>
|
||||
)}
|
||||
{node.unit && (
|
||||
<span className="text-muted-foreground ml-0.5">{node.unit}</span>
|
||||
)}
|
||||
</span>
|
||||
|
||||
{/* 단위 */}
|
||||
{node.unit && (
|
||||
<span className="text-xs text-muted-foreground shrink-0">{node.unit}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 자식 노드 */}
|
||||
{isOpen && hasChildren && node.children.map((child) => (
|
||||
<BomTreeNodeItem
|
||||
key={child.id}
|
||||
node={child}
|
||||
level={level + 1}
|
||||
expandedNodes={expandedNodes}
|
||||
onToggle={onToggle}
|
||||
/>
|
||||
))}
|
||||
{/* 코드 2줄 — 모바일만 */}
|
||||
<code className="text-[11px] text-gray-400 pl-6 truncate md:hidden block">
|
||||
{node.code}
|
||||
</code>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 범용 노드 렌더러 (CAT 분기)
|
||||
function BomNodeRenderer({
|
||||
node,
|
||||
index,
|
||||
expandedNodes,
|
||||
onToggle,
|
||||
}: {
|
||||
node: BomTreeNode;
|
||||
index: number;
|
||||
expandedNodes: Set<string>;
|
||||
onToggle: (key: string) => void;
|
||||
}) {
|
||||
const nodeKey = getNodeKey(node, index);
|
||||
const isCategory = node.item_type === 'CAT';
|
||||
|
||||
if (isCategory) {
|
||||
return (
|
||||
<CategoryNode
|
||||
node={node}
|
||||
nodeKey={nodeKey}
|
||||
isOpen={expandedNodes.has(nodeKey)}
|
||||
onToggle={onToggle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <ItemNode node={node} />;
|
||||
}
|
||||
|
||||
interface BomTreeViewerProps {
|
||||
itemId: string;
|
||||
itemType: string;
|
||||
}
|
||||
|
||||
export function BomTreeViewer({ itemId, itemType }: BomTreeViewerProps) {
|
||||
const [treeData, setTreeData] = useState<BomTreeNode[]>([]);
|
||||
const [treeData, setTreeData] = useState<BomTreeNode | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [expandedNodes, setExpandedNodes] = useState<Set<number>>(new Set());
|
||||
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set());
|
||||
const [allExpanded, setAllExpanded] = useState(true);
|
||||
|
||||
// 트리 데이터 로드
|
||||
const loadTree = useCallback(async () => {
|
||||
@@ -160,13 +189,14 @@ export function BomTreeViewer({ itemId, itemType }: BomTreeViewerProps) {
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
const data = Array.isArray(result.data) ? result.data : [result.data];
|
||||
setTreeData(data);
|
||||
// 기본: 2단계까지 펼침
|
||||
const allIds = collectNodeIds(data);
|
||||
setExpandedNodes(allIds);
|
||||
const root = result.data as BomTreeNode;
|
||||
setTreeData(root);
|
||||
// 기본: 모든 카테고리 펼침
|
||||
if (root.children) {
|
||||
setExpandedNodes(collectAllKeys(root.children));
|
||||
}
|
||||
} else {
|
||||
setTreeData([]);
|
||||
setTreeData(null);
|
||||
}
|
||||
} catch {
|
||||
setError('BOM 트리를 불러오는 중 오류가 발생했습니다.');
|
||||
@@ -176,7 +206,6 @@ export function BomTreeViewer({ itemId, itemType }: BomTreeViewerProps) {
|
||||
}, [itemId]);
|
||||
|
||||
useEffect(() => {
|
||||
// FG/PT (또는 한글: 제품/부품)만 BOM 트리 로드
|
||||
const isBomTarget = ['FG', 'PT', '제품', '부품'].includes(itemType);
|
||||
if (isBomTarget) {
|
||||
loadTree();
|
||||
@@ -215,13 +244,13 @@ export function BomTreeViewer({ itemId, itemType }: BomTreeViewerProps) {
|
||||
}
|
||||
|
||||
// 데이터 없음
|
||||
if (treeData.length === 0) {
|
||||
if (!treeData || !treeData.children || treeData.children.length === 0) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Package className="h-5 w-5" />
|
||||
BOM 트리
|
||||
부품 구성 (BOM)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -233,66 +262,65 @@ export function BomTreeViewer({ itemId, itemType }: BomTreeViewerProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const toggleNode = (id: number) => {
|
||||
const toggleNode = (key: string) => {
|
||||
setExpandedNodes((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(id)) next.delete(id);
|
||||
else next.add(id);
|
||||
if (next.has(key)) next.delete(key);
|
||||
else next.add(key);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const expandAll = () => setExpandedNodes(collectNodeIds(treeData));
|
||||
const collapseAll = () => setExpandedNodes(new Set());
|
||||
const expandAll = () => { setExpandedNodes(collectAllKeys(treeData.children)); setAllExpanded(true); };
|
||||
const collapseAll = () => { setExpandedNodes(new Set()); setAllExpanded(false); };
|
||||
const toggleAll = () => { if (allExpanded) collapseAll(); else expandAll(); };
|
||||
|
||||
// 총 노드 수 계산
|
||||
const totalCount = collectNodeIds(treeData).size;
|
||||
// 카테고리 그룹 수 & 총 품목 수
|
||||
const categories = treeData.children.filter(n => n.item_type === 'CAT');
|
||||
const totalItems = categories.reduce((sum, cat) => sum + (cat.count ?? cat.children?.length ?? 0), 0);
|
||||
const groupCount = categories.length;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
{/* PC: 한 줄 레이아웃 */}
|
||||
<div className="hidden md:flex md:items-center md:justify-between">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Package className="h-5 w-5" />
|
||||
BOM 트리
|
||||
부품 구성 (BOM)
|
||||
</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="bg-blue-50 text-blue-700">
|
||||
총 {totalCount}개 품목
|
||||
총 {totalItems}개 품목 · {groupCount}개 그룹
|
||||
</Badge>
|
||||
<Button variant="outline" size="sm" onClick={expandAll} className="h-7 text-xs">
|
||||
<Button variant="outline" size="sm" onClick={toggleAll} className="h-7 text-xs">
|
||||
<ChevronsUpDown className="w-3 h-3 mr-1" />
|
||||
전체 펼치기
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={collapseAll} className="h-7 text-xs">
|
||||
전체 접기
|
||||
{allExpanded ? '전체 접기' : '전체 펼치기'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/* 모바일: 줄바꿈 레이아웃 */}
|
||||
<div className="md:hidden space-y-2">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Package className="h-5 w-5" />
|
||||
부품 구성 (BOM)
|
||||
</CardTitle>
|
||||
<Badge variant="outline" className="bg-blue-50 text-blue-700 w-fit">
|
||||
총 {totalItems}개 품목 · {groupCount}개 그룹
|
||||
</Badge>
|
||||
<Button variant="outline" size="sm" onClick={toggleAll} className="h-7 text-xs w-fit">
|
||||
<ChevronsUpDown className="w-3 h-3 mr-1" />
|
||||
{allExpanded ? '전체 접기' : '전체 펼치기'}
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{/* 범례 */}
|
||||
<div className="flex flex-wrap gap-2 mb-3 pb-3 border-b">
|
||||
{Object.entries(ITEM_TYPE_LABELS).map(([type, label]) => (
|
||||
<div key={type} className="flex items-center gap-1">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`text-[10px] px-1.5 py-0 ${ITEM_TYPE_COLORS[type] || ''}`}
|
||||
>
|
||||
{type}
|
||||
</Badge>
|
||||
<span className="text-xs text-muted-foreground">{label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 트리 */}
|
||||
<div className="border rounded-md">
|
||||
{treeData.map((node) => (
|
||||
<BomTreeNodeItem
|
||||
key={node.id}
|
||||
<div className="space-y-1">
|
||||
{treeData.children.map((node, i) => (
|
||||
<BomNodeRenderer
|
||||
key={getNodeKey(node, i)}
|
||||
node={node}
|
||||
level={0}
|
||||
index={i}
|
||||
expandedNodes={expandedNodes}
|
||||
onToggle={toggleNode}
|
||||
/>
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
VisuallyHidden,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -668,7 +670,11 @@ export function ImportInspectionInputModal({
|
||||
// 캡처 실패 시 무시 — rendered_html 없이 저장 진행
|
||||
}
|
||||
|
||||
// 5. 저장 API 호출
|
||||
// 5. 저장 API 호출 (rendered_html 500KB 초과 시 제외 — 413 에러 방지)
|
||||
const MAX_HTML_SIZE = 500 * 1024;
|
||||
const safeHtml = renderedHtml && renderedHtml.length <= MAX_HTML_SIZE
|
||||
? renderedHtml : undefined;
|
||||
|
||||
const result = await saveInspectionData({
|
||||
templateId: parseInt(template.templateId),
|
||||
itemId,
|
||||
@@ -677,7 +683,7 @@ export function ImportInspectionInputModal({
|
||||
attachments,
|
||||
receivingId,
|
||||
inspectionResult: overallResult,
|
||||
rendered_html: renderedHtml,
|
||||
rendered_html: safeHtml,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
@@ -755,6 +761,7 @@ export function ImportInspectionInputModal({
|
||||
<DialogContent className="w-[95vw] max-w-[500px] sm:max-w-[500px] max-h-[90vh] flex flex-col p-0">
|
||||
<DialogHeader className="px-6 pt-6 pb-0 shrink-0">
|
||||
<DialogTitle className="text-lg font-bold">수입검사</DialogTitle>
|
||||
<VisuallyHidden><DialogDescription>수입검사 항목 입력</DialogDescription></VisuallyHidden>
|
||||
</DialogHeader>
|
||||
|
||||
{isLoadingTemplate ? (
|
||||
|
||||
@@ -19,6 +19,8 @@ import {
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
VisuallyHidden,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
Select,
|
||||
@@ -127,6 +129,7 @@ export function InventoryAdjustmentDialog({ open, onOpenChange, onSave }: Props)
|
||||
<DialogContent className="max-w-[95vw] w-[95vw] max-h-[90vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-lg font-semibold">재고 조정</DialogTitle>
|
||||
<VisuallyHidden><DialogDescription>재고 수량 조정</DialogDescription></VisuallyHidden>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col gap-4 flex-1 min-h-0">
|
||||
|
||||
@@ -67,28 +67,30 @@ interface Props {
|
||||
mode?: 'view' | 'edit' | 'new';
|
||||
}
|
||||
|
||||
// 초기 폼 데이터
|
||||
const INITIAL_FORM_DATA: Partial<ReceivingDetailType> = {
|
||||
materialNo: '',
|
||||
supplierMaterialNo: '',
|
||||
lotNo: '',
|
||||
itemCode: '',
|
||||
itemName: '',
|
||||
specification: '',
|
||||
unit: 'EA',
|
||||
supplier: '',
|
||||
manufacturer: '',
|
||||
receivingQty: undefined,
|
||||
receivingDate: '',
|
||||
createdBy: '',
|
||||
status: 'receiving_pending',
|
||||
remark: '',
|
||||
inspectionDate: '',
|
||||
inspectionResult: '',
|
||||
certificateFile: undefined,
|
||||
certificateFileId: undefined,
|
||||
inventoryAdjustments: [],
|
||||
};
|
||||
// 초기 폼 데이터 (동적 함수 — 세션 사용자 이름 + 오늘 날짜 기본값)
|
||||
function createInitialFormData(): Partial<ReceivingDetailType> {
|
||||
return {
|
||||
materialNo: '',
|
||||
supplierMaterialNo: '',
|
||||
lotNo: '',
|
||||
itemCode: '',
|
||||
itemName: '',
|
||||
specification: '',
|
||||
unit: 'EA',
|
||||
supplier: '',
|
||||
manufacturer: '',
|
||||
receivingQty: undefined,
|
||||
receivingDate: getTodayString(),
|
||||
createdBy: getLoggedInUserName(),
|
||||
status: 'receiving_pending',
|
||||
remark: '',
|
||||
inspectionDate: '',
|
||||
inspectionResult: '',
|
||||
certificateFile: undefined,
|
||||
certificateFileId: undefined,
|
||||
inventoryAdjustments: [],
|
||||
};
|
||||
}
|
||||
|
||||
// localStorage에서 로그인 사용자 정보 가져오기
|
||||
function getLoggedInUser(): { name: string; department: string } {
|
||||
@@ -122,7 +124,7 @@ export function ReceivingDetail({ id, mode = 'view' }: Props) {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// 폼 데이터 (등록/수정 모드용)
|
||||
const [formData, setFormData] = useState<Partial<ReceivingDetailType>>(INITIAL_FORM_DATA);
|
||||
const [formData, setFormData] = useState<Partial<ReceivingDetailType>>(createInitialFormData);
|
||||
|
||||
// 업로드된 파일 상태 (File 객체)
|
||||
const [uploadedFile, setUploadedFile] = useState<File | null>(null);
|
||||
@@ -275,8 +277,14 @@ export function ReceivingDetail({ id, mode = 'view' }: Props) {
|
||||
const result = await createReceiving(saveData);
|
||||
if (result.success) {
|
||||
toast.success('입고가 등록되었습니다.');
|
||||
router.push('/ko/material/receiving-management');
|
||||
return { success: true };
|
||||
const newId = result.data?.id;
|
||||
if (newId) {
|
||||
router.push(`/ko/material/receiving-management/${newId}?mode=view`);
|
||||
} else {
|
||||
router.push('/ko/material/receiving-management');
|
||||
}
|
||||
// 커스텀 네비게이션 처리: error='' → 템플릿의 navigateToList() 호출 방지
|
||||
return { success: false, error: '' };
|
||||
} else {
|
||||
toast.error(result.error || '등록에 실패했습니다.');
|
||||
return { success: false, error: result.error };
|
||||
|
||||
@@ -19,6 +19,8 @@ import {
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
VisuallyHidden,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import type { ReceivingDetail, ReceivingProcessFormData } from './types';
|
||||
@@ -99,6 +101,7 @@ export function ReceivingProcessDialog({ open, onOpenChange, detail, onComplete
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>입고 처리</DialogTitle>
|
||||
<VisuallyHidden><DialogDescription>입고 처리 정보 입력</DialogDescription></VisuallyHidden>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6 mt-4">
|
||||
|
||||
@@ -9,6 +9,9 @@ import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
VisuallyHidden,
|
||||
} from '@/components/ui/dialog';
|
||||
|
||||
interface Props {
|
||||
@@ -27,6 +30,10 @@ export function SuccessDialog({ open, type, lotNo, onClose }: Props) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(newOpen) => !newOpen && onClose()}>
|
||||
<DialogContent className="max-w-sm">
|
||||
<VisuallyHidden>
|
||||
<DialogTitle>처리 완료</DialogTitle>
|
||||
<DialogDescription>처리 결과 안내</DialogDescription>
|
||||
</VisuallyHidden>
|
||||
<div className="flex flex-col items-center text-center py-6 space-y-4">
|
||||
<div className="w-16 h-16 rounded-full bg-green-100 flex items-center justify-center">
|
||||
<CheckCircle2 className="w-10 h-10 text-green-600" />
|
||||
|
||||
Reference in New Issue
Block a user