diff --git a/claudedocs/dev/[REF] all-pages-test-urls.md b/claudedocs/dev/[REF] all-pages-test-urls.md index 885745cc..abd27a48 100644 --- a/claudedocs/dev/[REF] all-pages-test-urls.md +++ b/claudedocs/dev/[REF] all-pages-test-urls.md @@ -90,12 +90,14 @@ http://localhost:3000/ko/sales/quote-management/test/1/edit # ๐Ÿงช ๊ฒฌ์  ์ˆ˜ | **๊ณต์ •๊ด€๋ฆฌ** | `/ko/master-data/process-management` | โœ… | | **๋‹จ๊ฐ€ํ‘œ๊ด€๋ฆฌ** | `/ko/master-data/pricing-table-management` | ๐Ÿ†• NEW | | **โ”” ๋‹จ๊ฐ€๋ฐฐํฌ๊ด€๋ฆฌ** | `/ko/master-data/price-distribution` | ๐Ÿ†• NEW | +| **์ ๊ฒ€ํ‘œ๊ด€๋ฆฌ** | `/ko/master-data/checklist-management` | ๐Ÿ†• NEW | ``` http://localhost:3000/ko/master-data/item-master-data-management http://localhost:3000/ko/master-data/process-management # ๊ณต์ •๊ด€๋ฆฌ http://localhost:3000/ko/master-data/pricing-table-management # ๐Ÿ†• ๋‹จ๊ฐ€ํ‘œ๊ด€๋ฆฌ http://localhost:3000/ko/master-data/price-distribution # ๐Ÿ†• ๋‹จ๊ฐ€๋ฐฐํฌ๊ด€๋ฆฌ +http://localhost:3000/ko/master-data/checklist-management # ๐Ÿ†• ์ ๊ฒ€ํ‘œ๊ด€๋ฆฌ ``` --- diff --git a/src/app/[locale]/(protected)/master-data/checklist-management/[id]/items/[itemId]/page.tsx b/src/app/[locale]/(protected)/master-data/checklist-management/[id]/items/[itemId]/page.tsx new file mode 100644 index 00000000..779c22b9 --- /dev/null +++ b/src/app/[locale]/(protected)/master-data/checklist-management/[id]/items/[itemId]/page.tsx @@ -0,0 +1,22 @@ +'use client'; + +/** + * ์ ๊ฒ€ํ‘œ ํ•ญ๋ชฉ ์ƒ์„ธ/์ˆ˜์ •/๋“ฑ๋ก ํŽ˜์ด์ง€ + * + * - /[id]/items/[itemId] โ†’ ์ƒ์„ธ ๋ณด๊ธฐ + * - /[id]/items/[itemId]?mode=edit โ†’ ์ˆ˜์ • + * - /[id]/items/new โ†’ ๋“ฑ๋ก + */ + +import { use } from 'react'; +import { ItemDetailClient } from '@/components/checklist-management'; + +export default function ItemDetailPage({ + params, +}: { + params: Promise<{ id: string; itemId: string }>; +}) { + const { id, itemId } = use(params); + + return ; +} diff --git a/src/app/[locale]/(protected)/master-data/checklist-management/[id]/page.tsx b/src/app/[locale]/(protected)/master-data/checklist-management/[id]/page.tsx new file mode 100644 index 00000000..7bea8c26 --- /dev/null +++ b/src/app/[locale]/(protected)/master-data/checklist-management/[id]/page.tsx @@ -0,0 +1,21 @@ +'use client'; + +/** + * ์ ๊ฒ€ํ‘œ ์ƒ์„ธ/์ˆ˜์ • ํŽ˜์ด์ง€ + * + * - /[id] โ†’ ์ƒ์„ธ ๋ณด๊ธฐ (view ๋ชจ๋“œ) + * - /[id]?mode=edit โ†’ ์ˆ˜์ • (edit ๋ชจ๋“œ) + */ + +import { use } from 'react'; +import { ChecklistDetailClient } from '@/components/checklist-management'; + +export default function ChecklistDetailPage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = use(params); + + return ; +} diff --git a/src/app/[locale]/(protected)/master-data/checklist-management/page.tsx b/src/app/[locale]/(protected)/master-data/checklist-management/page.tsx new file mode 100644 index 00000000..5e169ee5 --- /dev/null +++ b/src/app/[locale]/(protected)/master-data/checklist-management/page.tsx @@ -0,0 +1,30 @@ +'use client'; + +/** + * ์ ๊ฒ€ํ‘œ ๋ชฉ๋ก/๋“ฑ๋ก ํŽ˜์ด์ง€ + */ + +import { Suspense } from 'react'; +import { useSearchParams } from 'next/navigation'; +import ChecklistListClient from '@/components/checklist-management/ChecklistListClient'; +import { ChecklistDetailClient } from '@/components/checklist-management'; +import { ListPageSkeleton } from '@/components/ui/skeleton'; + +function ChecklistManagementContent() { + const searchParams = useSearchParams(); + const mode = searchParams.get('mode'); + + if (mode === 'new') { + return ; + } + + return ; +} + +export default function ChecklistManagementPage() { + return ( + }> + + + ); +} diff --git a/src/app/[locale]/(protected)/quality/qms/components/AuditProgressBar.tsx b/src/app/[locale]/(protected)/quality/qms/components/AuditProgressBar.tsx index 04d78aca..66b937fc 100644 --- a/src/app/[locale]/(protected)/quality/qms/components/AuditProgressBar.tsx +++ b/src/app/[locale]/(protected)/quality/qms/components/AuditProgressBar.tsx @@ -48,7 +48,7 @@ export function AuditProgressBar({ activeDay === 1 ? 'bg-blue-50 border-blue-200' : 'bg-gray-50 border-gray-200' )}>
- 1์ผ์ฐจ: ๊ธฐ์ค€/๋งค๋‰ด์–ผ + ๊ธฐ์ค€/๋งค๋‰ด์–ผ ์‹ฌ์‚ฌ
- 2์ผ์ฐจ: ๋กœํŠธ์ถ”์  + ๋กœํŠธ ์ถ”์  ์‹ฌ์‚ฌ - 1์ผ์ฐจ: ๊ธฐ์ค€/๋งค๋‰ด์–ผ + ๊ธฐ์ค€/๋งค๋‰ด์–ผ ์‹ฌ์‚ฌ 1์ผ์ฐจ - 2์ผ์ฐจ: ๋กœํŠธ์ถ”์  + ๋กœํŠธ ์ถ”์  ์‹ฌ์‚ฌ 2์ผ์ฐจ - 1์ผ์ฐจ: ๊ธฐ์ค€/๋งค๋‰ด์–ผ + ๊ธฐ์ค€/๋งค๋‰ด์–ผ ์‹ฌ์‚ฌ 1์ผ์ฐจ
@@ -128,7 +128,7 @@ export function DayTabs({ activeDay, onDayChange, day1Progress, day2Progress }: {/* 2์ผ์ฐจ ์ง„ํ–‰๋ฅ  */}
- 2์ผ์ฐจ: ๋กœํŠธ์ถ”์  + ๋กœํŠธ ์ถ”์  ์‹ฌ์‚ฌ 2์ผ์ฐจ
diff --git a/src/app/[locale]/(protected)/quality/qms/mockData.ts b/src/app/[locale]/(protected)/quality/qms/mockData.ts index cf6a0e07..29fe4632 100644 --- a/src/app/[locale]/(protected)/quality/qms/mockData.ts +++ b/src/app/[locale]/(protected)/quality/qms/mockData.ts @@ -291,7 +291,7 @@ export const DEFAULT_DOCUMENTS: Document[] = [ { id: 'def-8', type: 'quality', title: 'ํ’ˆ์งˆ๊ด€๋ฆฌ์„œ', count: 0, items: [] }, ]; -// ===== 1์ผ์ฐจ: ๊ธฐ์ค€/๋งค๋‰ด์–ผ ์‹ฌ์‚ฌ Mock ๋ฐ์ดํ„ฐ ===== +// ===== ๊ธฐ์ค€/๋งค๋‰ด์–ผ ์‹ฌ์‚ฌ ์‹ฌ์‚ฌ Mock ๋ฐ์ดํ„ฐ ===== // 1์ผ์ฐจ ์ ๊ฒ€ํ‘œ ์นดํ…Œ๊ณ ๋ฆฌ export const MOCK_DAY1_CATEGORIES: ChecklistCategory[] = [ diff --git a/src/app/[locale]/(protected)/quality/qms/page.tsx b/src/app/[locale]/(protected)/quality/qms/page.tsx index c92289eb..4828ea23 100644 --- a/src/app/[locale]/(protected)/quality/qms/page.tsx +++ b/src/app/[locale]/(protected)/quality/qms/page.tsx @@ -255,7 +255,7 @@ export default function QualityInspectionPage() { : 'bg-white border-gray-200 text-gray-700 hover:border-blue-300' }`} > - 1์ผ์ฐจ: ๊ธฐ์ค€/๋งค๋‰ด์–ผ ์‹ฌ์‚ฌ + ๊ธฐ์ค€/๋งค๋‰ด์–ผ ์‹ฌ์‚ฌ ์‹ฌ์‚ฌ
)} @@ -282,7 +282,7 @@ export default function QualityInspectionPage() { /> {activeDay === 1 ? ( - // ===== 1์ผ์ฐจ: ๊ธฐ์ค€/๋งค๋‰ด์–ผ ์‹ฌ์‚ฌ ===== + // ===== ๊ธฐ์ค€/๋งค๋‰ด์–ผ ์‹ฌ์‚ฌ ์‹ฌ์‚ฌ =====
{/* ์ขŒ์ธก: ์ ๊ฒ€ํ‘œ ํ•ญ๋ชฉ */}
) : ( - // ===== 2์ผ์ฐจ: ๋กœํŠธ์ถ”์  ์‹ฌ์‚ฌ ===== + // ===== ๋กœํŠธ ์ถ”์  ์‹ฌ์‚ฌ ์‹ฌ์‚ฌ =====
state.sidebarCollapsed); + const { canUpdate } = usePermission(); + + const [items, setItems] = useState([]); + const [isItemsLoading, setIsItemsLoading] = useState(true); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + + // ๋“œ๋ž˜๊ทธ ์ƒํƒœ + const [dragIndex, setDragIndex] = useState(null); + const [dragOverIndex, setDragOverIndex] = useState(null); + const dragNodeRef = useRef(null); + + // ํ•ญ๋ชฉ ๋ชฉ๋ก ๋กœ๋“œ + useEffect(() => { + const loadItems = async () => { + setIsItemsLoading(true); + const result = await getChecklistItems(checklist.id); + if (result.success && result.data) { + setItems(result.data); + } + setIsItemsLoading(false); + }; + loadItems(); + }, [checklist.id]); + + // ๋„ค๋น„๊ฒŒ์ด์…˜ + const handleEdit = () => { + router.push(`/ko/master-data/checklist-management/${checklist.id}?mode=edit`); + }; + + const handleList = () => { + router.push('/ko/master-data/checklist-management'); + }; + + const handleAddItem = () => { + router.push(`/ko/master-data/checklist-management/${checklist.id}/items/new`); + }; + + const handleItemClick = (itemId: string) => { + router.push(`/ko/master-data/checklist-management/${checklist.id}/items/${itemId}`); + }; + + const handleDelete = async () => { + setIsDeleting(true); + try { + const result = await deleteChecklist(checklist.id); + if (result.success) { + toast.success('์ ๊ฒ€ํ‘œ๊ฐ€ ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'); + router.push('/ko/master-data/checklist-management'); + } else { + toast.error(result.error || '์‚ญ์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); + } + } catch { + toast.error('์‚ญ์ œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.'); + } finally { + setIsDeleting(false); + setDeleteDialogOpen(false); + } + }; + + // ===== ๋“œ๋ž˜๊ทธ&๋“œ๋กญ ===== + const handleDragStart = useCallback( + (e: React.DragEvent, index: number) => { + setDragIndex(index); + dragNodeRef.current = e.currentTarget; + e.dataTransfer.effectAllowed = 'move'; + requestAnimationFrame(() => { + if (dragNodeRef.current) dragNodeRef.current.style.opacity = '0.4'; + }); + }, + [] + ); + + const handleDragOver = useCallback( + (e: React.DragEvent, index: number) => { + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; + setDragOverIndex(index); + }, + [] + ); + + const handleDragEnd = useCallback(() => { + if (dragNodeRef.current) dragNodeRef.current.style.opacity = '1'; + setDragIndex(null); + setDragOverIndex(null); + dragNodeRef.current = null; + }, []); + + const handleDrop = useCallback( + (e: React.DragEvent, dropIndex: number) => { + e.preventDefault(); + if (dragIndex === null || dragIndex === dropIndex) return; + + setItems((prev) => { + const updated = [...prev]; + const [moved] = updated.splice(dragIndex, 1); + updated.splice(dropIndex, 0, moved); + const reordered = updated.map((item, i) => ({ ...item, order: i + 1 })); + + reorderChecklistItems( + checklist.id, + reordered.map((it) => ({ id: it.id, order: it.order })) + ); + + return reordered; + }); + + handleDragEnd(); + }, + [dragIndex, handleDragEnd, checklist.id] + ); + + return ( + + + +
+ {/* ๊ธฐ๋ณธ ์ •๋ณด */} + + + ๊ธฐ๋ณธ ์ •๋ณด + + +
+
+
์ ๊ฒ€ํ‘œ ๋ฒˆํ˜ธ
+
{checklist.checklistCode}
+
+
+
์ ๊ฒ€ํ‘œ
+
{checklist.checklistName}
+
+
+
์ƒํƒœ
+ + {checklist.status} + +
+
+
+
+ + {/* ํ•ญ๋ชฉ ํ…Œ์ด๋ธ” */} + + +
+ + ํ•ญ๋ชฉ ๋ชฉ๋ก + {!isItemsLoading && ( + + ์ด {items.length}๊ฑด + + )} + + +
+
+ + {isItemsLoading ? ( +
๋กœ๋”ฉ ์ค‘...
+ ) : items.length === 0 ? ( +
+ ๋“ฑ๋ก๋œ ํ•ญ๋ชฉ์ด ์—†์Šต๋‹ˆ๋‹ค. [ํ•ญ๋ชฉ ๋“ฑ๋ก] ๋ฒ„ํŠผ์œผ๋กœ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”. +
+ ) : ( +
+ + + + + + + + + + + + + {items.map((item, index) => ( + handleDragStart(e, index)} + onDragOver={(e) => handleDragOver(e, index)} + onDragEnd={handleDragEnd} + onDrop={(e) => handleDrop(e, index)} + onClick={() => handleItemClick(item.id)} + className={`border-b cursor-pointer transition-colors hover:bg-muted/50 ${ + dragOverIndex === index && dragIndex !== index + ? 'border-t-2 border-t-primary' + : '' + }`} + > + + + + + + + + + ))} + +
+ + No. + + ํ•ญ๋ชฉ ๋ฒˆํ˜ธ + + ์ˆœ์„œ + + ํ•ญ๋ชฉ๋ช… + + ๋ฌธ์„œ + + ์‚ฌ์šฉ +
e.stopPropagation()} + > + + + {index + 1} + {item.itemCode} + {item.order} + {item.itemName} + {item.documentCount} + + + {item.status} + +
+
+ )} +
+
+
+ + {/* ํ•˜๋‹จ ์•ก์…˜ ๋ฒ„ํŠผ (sticky) */} +
+ + {canUpdate && ( +
+ + +
+ )} +
+ + +
+ ); +} diff --git a/src/components/checklist-management/ChecklistDetailClient.tsx b/src/components/checklist-management/ChecklistDetailClient.tsx new file mode 100644 index 00000000..47d20f3d --- /dev/null +++ b/src/components/checklist-management/ChecklistDetailClient.tsx @@ -0,0 +1,122 @@ +'use client'; + +/** + * ์ ๊ฒ€ํ‘œ ์ƒ์„ธ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ + * + * ๋ผ์šฐํŒ…: + * - /[id] โ†’ ์ƒ์„ธ ๋ณด๊ธฐ (view) + * - /[id]?mode=edit โ†’ ์ˆ˜์ • (edit) + * - ?mode=new โ†’ ๋“ฑ๋ก (create) + */ + +import { useState, useEffect } from 'react'; +import { useSearchParams } from 'next/navigation'; +import { ChecklistDetail } from './ChecklistDetail'; +import { ChecklistForm } from './ChecklistForm'; +import { getChecklistById } from './actions'; +import type { Checklist } from '@/types/checklist'; +import { DetailPageSkeleton } from '@/components/ui/skeleton'; +import { ErrorCard } from '@/components/ui/error-card'; +import { toast } from 'sonner'; + +type DetailMode = 'view' | 'edit' | 'create'; + +interface ChecklistDetailClientProps { + checklistId?: string; +} + +const BASE_PATH = '/ko/master-data/checklist-management'; + +export function ChecklistDetailClient({ checklistId }: ChecklistDetailClientProps) { + const searchParams = useSearchParams(); + const modeFromQuery = searchParams.get('mode') as DetailMode | null; + const isNewMode = !checklistId || checklistId === 'new'; + + const [mode, setMode] = useState(() => { + if (isNewMode) return 'create'; + if (modeFromQuery === 'edit') return 'edit'; + return 'view'; + }); + + const [checklistData, setChecklistData] = useState(null); + const [isLoading, setIsLoading] = useState(!isNewMode); + const [error, setError] = useState(null); + + useEffect(() => { + if (isNewMode) { + setIsLoading(false); + return; + } + + const loadData = async () => { + setIsLoading(true); + setError(null); + try { + const result = await getChecklistById(checklistId!); + if (result.success && result.data) { + setChecklistData(result.data); + } else { + setError(result.error || '์ ๊ฒ€ํ‘œ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.'); + toast.error('์ ๊ฒ€ํ‘œ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); + } + } catch { + setError('์ ๊ฒ€ํ‘œ ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.'); + toast.error('์ ๊ฒ€ํ‘œ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); + } finally { + setIsLoading(false); + } + }; + + loadData(); + }, [checklistId, isNewMode]); + + useEffect(() => { + if (!isNewMode && modeFromQuery === 'edit') { + setMode('edit'); + } else if (!isNewMode && !modeFromQuery) { + setMode('view'); + } + }, [modeFromQuery, isNewMode]); + + if (isLoading) { + return ; + } + + if (error && !isNewMode) { + return ( + + ); + } + + if (mode === 'create') { + return ; + } + + if (mode === 'edit' && checklistData) { + return ; + } + + if (mode === 'view' && checklistData) { + return ; + } + + return ( + + ); +} diff --git a/src/components/checklist-management/ChecklistForm.tsx b/src/components/checklist-management/ChecklistForm.tsx new file mode 100644 index 00000000..ed91209e --- /dev/null +++ b/src/components/checklist-management/ChecklistForm.tsx @@ -0,0 +1,172 @@ +'use client'; + +/** + * ์ ๊ฒ€ํ‘œ ๋“ฑ๋ก/์ˆ˜์ • ํผ ์ปดํฌ๋„ŒํŠธ + * + * ๊ธฐํš์„œ ์Šคํฌ๋ฆฐ์ƒท 2 ๊ธฐ์ค€: + * - ๊ธฐ๋ณธ ์ •๋ณด: ์ ๊ฒ€ํ‘œ ๋ฒˆํ˜ธ(์ž๋™), ์ ๊ฒ€ํ‘œ๋ช…, ์ƒํƒœ + */ + +import { useState, useCallback } from 'react'; +import { useRouter } 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 { toast } from 'sonner'; +import type { Checklist } from '@/types/checklist'; +import { createChecklist, updateChecklist } from './actions'; +import type { DetailConfig } from '@/components/templates/IntegratedDetailTemplate/types'; + +const createConfig: DetailConfig = { + title: '์ ๊ฒ€ํ‘œ', + description: '์ƒˆ๋กœ์šด ์ ๊ฒ€ํ‘œ๋ฅผ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค', + basePath: '', + fields: [], + actions: { + showBack: true, + showEdit: false, + showDelete: false, + showSave: true, + submitLabel: '๋“ฑ๋ก', + }, +}; + +const editConfig: DetailConfig = { + ...createConfig, + description: '์ ๊ฒ€ํ‘œ ์ •๋ณด๋ฅผ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค', + actions: { + ...createConfig.actions, + submitLabel: '์ €์žฅ', + }, +}; + +interface ChecklistFormProps { + mode: 'create' | 'edit'; + initialData?: Checklist; +} + +export function ChecklistForm({ mode, initialData }: ChecklistFormProps) { + const router = useRouter(); + const isEdit = mode === 'edit'; + + const [checklistName, setChecklistName] = useState(initialData?.checklistName || ''); + const [status, setStatus] = useState(initialData?.status || '์‚ฌ์šฉ'); + const [isLoading, setIsLoading] = useState(false); + + const handleSubmit = async (): Promise<{ success: boolean; error?: string }> => { + if (!checklistName.trim()) { + toast.error('์ ๊ฒ€ํ‘œ๋ช…์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.'); + return { success: false, error: '์ ๊ฒ€ํ‘œ๋ช…์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.' }; + } + + const formData = { + checklistName: checklistName.trim(), + status: status as '์‚ฌ์šฉ' | '๋ฏธ์‚ฌ์šฉ', + }; + + setIsLoading(true); + try { + if (isEdit && initialData?.id) { + const result = await updateChecklist(initialData.id, formData); + if (result.success) { + toast.success('์ ๊ฒ€ํ‘œ๊ฐ€ ์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'); + router.push(`/ko/master-data/checklist-management/${initialData.id}`); + return { success: true }; + } else { + toast.error(result.error || '์ˆ˜์ •์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); + return { success: false, error: result.error }; + } + } else { + const result = await createChecklist(formData); + if (result.success && result.data) { + toast.success('์ ๊ฒ€ํ‘œ๊ฐ€ ๋“ฑ๋ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'); + router.push(`/ko/master-data/checklist-management/${result.data.id}`); + return { success: true }; + } else { + toast.error(result.error || '๋“ฑ๋ก์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); + return { success: false, error: result.error }; + } + } + } catch { + toast.error('์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.'); + return { success: false, error: '์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.' }; + } finally { + setIsLoading(false); + } + }; + + const handleCancel = () => { + if (isEdit && initialData?.id) { + router.push(`/ko/master-data/checklist-management/${initialData.id}`); + } else { + router.push('/ko/master-data/checklist-management'); + } + }; + + const renderFormContent = useCallback( + () => ( +
+ + + ๊ธฐ๋ณธ ์ •๋ณด + + +
+
+ + +
+
+ + setChecklistName(e.target.value)} + placeholder="์ ๊ฒ€ํ‘œ๋ช…์„ ์ž…๋ ฅํ•˜์„ธ์š”" + /> +
+
+ + +
+
+
+
+
+ ), + [checklistName, status, initialData?.checklistCode] + ); + + const config = isEdit ? editConfig : createConfig; + + return ( + + ); +} diff --git a/src/components/checklist-management/ChecklistListClient.tsx b/src/components/checklist-management/ChecklistListClient.tsx new file mode 100644 index 00000000..5a8acb12 --- /dev/null +++ b/src/components/checklist-management/ChecklistListClient.tsx @@ -0,0 +1,519 @@ +'use client'; + +/** + * ์ ๊ฒ€ํ‘œ ๋ชฉ๋ก - UniversalListPage ๊ธฐ๋ฐ˜ + * + * ๊ณต์ • ๋ชฉ๋ก(ProcessListClient)๊ณผ ๋™์ผํ•œ ํŒจํ„ด: + * - ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ํ•„ํ„ฐ๋ง (์ƒํƒœ๋ณ„) + * - ๋“œ๋ž˜๊ทธ&๋“œ๋กญ ์ˆœ์„œ ๋ณ€๊ฒฝ + * - ์ƒํƒœ ํ† ๊ธ€ + */ + +import { useState, useMemo, useCallback, useEffect, useRef } from 'react'; +import { useRouter } from 'next/navigation'; +import { ClipboardList, Plus, GripVertical } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { TableCell, TableRow, TableHead } from '@/components/ui/table'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Badge } from '@/components/ui/badge'; +import { + UniversalListPage, + type UniversalListConfig, + type SelectionHandlers, + type RowClickHandlers, + type ListParams, +} from '@/components/templates/UniversalListPage'; +import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard'; +import { toast } from 'sonner'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; +import type { Checklist } from '@/types/checklist'; +import { + getChecklistList, + deleteChecklist, + deleteChecklists, + toggleChecklistStatus, + getChecklistStats, + reorderChecklists, +} from './actions'; + +export default function ChecklistListClient() { + const router = useRouter(); + + // ===== ์ƒํƒœ ===== + const [allChecklists, setAllChecklists] = useState([]); + const [stats, setStats] = useState({ total: 0, active: 0, inactive: 0 }); + const [isLoading, setIsLoading] = useState(false); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [deleteTargetId, setDeleteTargetId] = useState(null); + + // ๋‚ ์งœ ๋ฒ”์œ„ ์ƒํƒœ + const [startDate, setStartDate] = useState('2025-01-01'); + const [endDate, setEndDate] = useState('2025-12-31'); + + // ๊ฒ€์ƒ‰์–ด ์ƒํƒœ + const [searchQuery, setSearchQuery] = useState(''); + + // ๋“œ๋ž˜๊ทธ&๋“œ๋กญ ์ˆœ์„œ ๋ณ€๊ฒฝ ์ƒํƒœ + const [isOrderChanged, setIsOrderChanged] = useState(false); + const dragIdRef = useRef(null); + const dragNodeRef = useRef(null); + const allChecklistsRef = useRef(allChecklists); + allChecklistsRef.current = allChecklists; + + // ===== ๋ฐ์ดํ„ฐ ๋กœ๋“œ ===== + const loadData = useCallback(async () => { + setIsLoading(true); + try { + const [listResult, statsResult] = await Promise.all([ + getChecklistList(), + getChecklistStats(), + ]); + + if (listResult.success && listResult.data) { + setAllChecklists(listResult.data.items); + } + if (statsResult.success && statsResult.data) { + setStats(statsResult.data); + } + } catch { + toast.error('๋ฐ์ดํ„ฐ ๋กœ๋“œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); + } finally { + setIsLoading(false); + } + }, []); + + useEffect(() => { + loadData(); + }, [loadData]); + + // ===== ํ•ธ๋“ค๋Ÿฌ ===== + const handleRowClick = useCallback( + (checklist: Checklist) => { + router.push(`/ko/master-data/checklist-management/${checklist.id}`); + }, + [router] + ); + + const handleCreate = useCallback(() => { + router.push('/ko/master-data/checklist-management?mode=new'); + }, [router]); + + const handleDeleteConfirm = useCallback(async () => { + if (!deleteTargetId) return; + setIsLoading(true); + try { + const result = await deleteChecklist(deleteTargetId); + if (result.success) { + toast.success('์ ๊ฒ€ํ‘œ๊ฐ€ ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'); + setAllChecklists((prev) => prev.filter((c) => c.id !== deleteTargetId)); + } else { + toast.error(result.error || '์‚ญ์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); + } + } catch { + toast.error('์‚ญ์ œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.'); + } finally { + setIsLoading(false); + setDeleteDialogOpen(false); + setDeleteTargetId(null); + } + }, [deleteTargetId]); + + const handleBulkDelete = useCallback( + async (selectedIds: string[]) => { + if (selectedIds.length === 0) { + toast.warning('์‚ญ์ œํ•  ํ•ญ๋ชฉ์„ ์„ ํƒํ•ด์ฃผ์„ธ์š”.'); + return; + } + setIsLoading(true); + try { + const result = await deleteChecklists(selectedIds); + if (result.success) { + toast.success(`${result.deletedCount}๊ฐœ ํ•ญ๋ชฉ์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`); + await loadData(); + } else { + toast.error(result.error || '์ผ๊ด„ ์‚ญ์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); + } + } catch { + toast.error('์ผ๊ด„ ์‚ญ์ œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.'); + } finally { + setIsLoading(false); + } + }, + [loadData] + ); + + const handleToggleStatus = useCallback( + async (checklistId: string) => { + setIsLoading(true); + try { + const result = await toggleChecklistStatus(checklistId); + if (result.success && result.data) { + toast.success('์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'); + setAllChecklists((prev) => + prev.map((c) => (c.id === checklistId ? result.data! : c)) + ); + } else { + toast.error(result.error || '์ƒํƒœ ๋ณ€๊ฒฝ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); + } + } catch { + toast.error('์ƒํƒœ ๋ณ€๊ฒฝ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.'); + } finally { + setIsLoading(false); + } + }, + [] + ); + + // ===== ๋“œ๋ž˜๊ทธ&๋“œ๋กญ ===== + const handleDragStart = useCallback( + (e: React.DragEvent, id: string) => { + dragIdRef.current = id; + dragNodeRef.current = e.currentTarget; + e.dataTransfer.effectAllowed = 'move'; + requestAnimationFrame(() => { + if (dragNodeRef.current) dragNodeRef.current.style.opacity = '0.4'; + }); + }, + [] + ); + + const handleDragOver = useCallback( + (e: React.DragEvent) => { + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; + e.currentTarget.classList.add('border-t-2', 'border-t-primary'); + }, + [] + ); + + const handleDragLeave = useCallback( + (e: React.DragEvent) => { + const related = e.relatedTarget as Node; + if (!e.currentTarget.contains(related)) { + e.currentTarget.classList.remove('border-t-2', 'border-t-primary'); + } + }, + [] + ); + + const handleDragEnd = useCallback(() => { + if (dragNodeRef.current) dragNodeRef.current.style.opacity = '1'; + dragIdRef.current = null; + dragNodeRef.current = null; + }, []); + + const handleDrop = useCallback( + (e: React.DragEvent, dropId: string) => { + e.preventDefault(); + e.currentTarget.classList.remove('border-t-2', 'border-t-primary'); + const dragId = dragIdRef.current; + if (!dragId || dragId === dropId) { + handleDragEnd(); + return; + } + setAllChecklists((prev) => { + const updated = [...prev]; + const dragIdx = updated.findIndex((c) => c.id === dragId); + const dropIdx = updated.findIndex((c) => c.id === dropId); + if (dragIdx === -1 || dropIdx === -1) return prev; + const [moved] = updated.splice(dragIdx, 1); + updated.splice(dropIdx, 0, moved); + return updated; + }); + setIsOrderChanged(true); + handleDragEnd(); + }, + [handleDragEnd] + ); + + const handleSaveOrder = useCallback(async () => { + setIsLoading(true); + try { + const orderData = allChecklistsRef.current.map((c, idx) => ({ + id: c.id, + order: idx + 1, + })); + const result = await reorderChecklists(orderData); + if (result.success) { + toast.success('์ˆœ์„œ๊ฐ€ ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'); + setIsOrderChanged(false); + } else { + toast.error(result.error || '์ˆœ์„œ ์ €์žฅ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); + } + } catch { + toast.error('์ˆœ์„œ ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.'); + } finally { + setIsLoading(false); + } + }, []); + + // ===== UniversalListPage Config ===== + const config: UniversalListConfig = useMemo( + () => ({ + title: '์ ๊ฒ€ํ‘œ ๋ชฉ๋ก', + icon: ClipboardList, + basePath: '/master-data/checklist-management', + idField: 'id', + + actions: { + getList: async (params?: ListParams) => { + try { + const [listResult, statsResult] = await Promise.all([ + getChecklistList(), + getChecklistStats(), + ]); + if (listResult.success && listResult.data) { + setAllChecklists(listResult.data.items); + if (statsResult.success && statsResult.data) { + setStats(statsResult.data); + } + return { + success: true, + data: listResult.data.items, + totalCount: listResult.data.items.length, + totalPages: 1, + }; + } + return { success: false, error: '๋ฐ์ดํ„ฐ ๋กœ๋“œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.' }; + } catch { + return { success: false, error: '์„œ๋ฒ„ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.' }; + } + }, + deleteItem: async (id: string) => { + const result = await deleteChecklist(id); + return { success: result.success, error: result.error }; + }, + deleteBulk: async (ids: string[]) => { + const result = await deleteChecklists(ids); + return { success: result.success, error: result.error }; + }, + }, + + // ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ (showCheckbox: false โ†’ ์ˆ˜๋™ ๊ด€๋ฆฌ) + showCheckbox: false, + columns: [ + { key: 'drag', label: '', className: 'w-[40px]' }, + { key: 'checkbox', label: '', className: 'w-[50px]' }, + { key: 'no', label: 'No.', className: 'w-[60px] text-center' }, + { key: 'checklistCode', label: '์ ๊ฒ€ํ‘œ ๋ฒˆํ˜ธ', className: 'w-[120px]' }, + { key: 'checklistName', label: '์ ๊ฒ€ํ‘œ', className: 'min-w-[200px]' }, + { key: 'items', label: 'ํ•ญ๋ชฉ', className: 'w-[80px] text-center' }, + { key: 'documents', label: '๋ฌธ์„œ', className: 'w-[80px] text-center' }, + { key: 'status', label: '์ƒํƒœ', className: 'w-[80px] text-center' }, + ], + + // ์ปค์Šคํ…€ ํ…Œ์ด๋ธ” ํ—ค๋” (๋“œ๋ž˜๊ทธ โ†’ ์ „์ฒด์„ ํƒ ์ฒดํฌ๋ฐ•์Šค โ†’ No. โ†’ ๋ฐ์ดํ„ฐ ์ˆœ) + renderCustomTableHeader: ({ displayData, selectedItems, onToggleSelectAll }) => ( + <> + + + 0 && selectedItems.size === displayData.length} + onCheckedChange={onToggleSelectAll} + /> + + No. + ์ ๊ฒ€ํ‘œ ๋ฒˆํ˜ธ + ์ ๊ฒ€ํ‘œ + ํ•ญ๋ชฉ + ๋ฌธ์„œ + ์ƒํƒœ + + ), + + clientSideFiltering: true, + itemsPerPage: 20, + + hideSearch: true, + searchValue: searchQuery, + onSearchChange: setSearchQuery, + + dateRangeSelector: { + enabled: true, + showPresets: true, + startDate, + endDate, + onStartDateChange: setStartDate, + onEndDateChange: setEndDate, + }, + + filterConfig: [ + { + key: 'status', + label: '์ƒํƒœ', + type: 'single' as const, + options: [ + { value: '์‚ฌ์šฉ', label: '์‚ฌ์šฉ' }, + { value: '๋ฏธ์‚ฌ์šฉ', label: '๋ฏธ์‚ฌ์šฉ' }, + ], + allOptionLabel: '์ „์ฒด', + }, + ], + initialFilters: { status: '' }, + + customFilterFn: ( + items: Checklist[], + filterValues: Record + ) => { + const statusFilter = filterValues.status as string; + if (!statusFilter) return items; + return items.filter((item) => item.status === statusFilter); + }, + + searchFilter: (item, searchValue) => { + if (!searchValue || !searchValue.trim()) return true; + const search = searchValue.toLowerCase().trim(); + return ( + (item.checklistCode || '').toLowerCase().includes(search) || + (item.checklistName || '').toLowerCase().includes(search) + ); + }, + + headerActions: () => ( + + ), + + createButton: { + label: '์ ๊ฒ€ํ‘œ ๋“ฑ๋ก', + onClick: handleCreate, + icon: Plus, + }, + + onBulkDelete: handleBulkDelete, + + renderTableRow: ( + checklist: Checklist, + index: number, + globalIndex: number, + handlers: SelectionHandlers & RowClickHandlers + ) => ( + handleDragStart(e, checklist.id)} + onDragOver={handleDragOver} + onDragLeave={handleDragLeave} + onDragEnd={handleDragEnd} + onDrop={(e) => handleDrop(e, checklist.id)} + className={`cursor-pointer hover:bg-muted/50 ${handlers.isSelected ? 'bg-blue-50' : ''}`} + onClick={() => handleRowClick(checklist)} + > + e.stopPropagation()} + > + + + e.stopPropagation()}> + + + + {globalIndex} + + {checklist.checklistCode} + {checklist.checklistName} + {checklist.itemCount} + {checklist.documentCount} + e.stopPropagation()} + > + handleToggleStatus(checklist.id)} + > + {checklist.status} + + + + ), + + renderMobileCard: ( + checklist: Checklist, + index: number, + globalIndex: number, + handlers: SelectionHandlers & RowClickHandlers + ) => ( + handleRowClick(checklist)} + headerBadges={ + <> + + #{globalIndex} + + + {checklist.checklistCode} + + + } + title={checklist.checklistName} + statusBadge={ + { + e.stopPropagation(); + handleToggleStatus(checklist.id); + }} + > + {checklist.status} + + } + infoGrid={ +
+ + +
+ } + /> + ), + }), + [ + handleCreate, + handleRowClick, + handleToggleStatus, + handleBulkDelete, + startDate, + endDate, + searchQuery, + isOrderChanged, + handleSaveOrder, + handleDragStart, + handleDragOver, + handleDragLeave, + handleDragEnd, + handleDrop, + ] + ); + + return ( + <> + + + + ); +} diff --git a/src/components/checklist-management/ItemDetail.tsx b/src/components/checklist-management/ItemDetail.tsx new file mode 100644 index 00000000..f3bfac73 --- /dev/null +++ b/src/components/checklist-management/ItemDetail.tsx @@ -0,0 +1,223 @@ +'use client'; + +/** + * ํ•ญ๋ชฉ ์ƒ์„ธ ๋ทฐ ์ปดํฌ๋„ŒํŠธ + * + * ๊ธฐํš์„œ ์Šคํฌ๋ฆฐ์ƒท 3 ๊ธฐ์ค€: + * - ๊ธฐ๋ณธ ์ •๋ณด: ํ•ญ๋ชฉ ๋ฒˆํ˜ธ, ํ•ญ๋ชฉ๋ช…, ์†Œ๊ฐœ, ์ƒํƒœ + * - ๋ฌธ์„œ ์ •๋ณด: ์ˆœ์„œ, ๋ฌธ์„œ ๋ฒˆํ˜ธ, ๋ฌธ์„œ, ๊ฐœ์ •, ์‹œํ–‰์ผ + * - ํ•˜๋‹จ: ์‚ญ์ œ, ์ˆ˜์ • ๋ฒ„ํŠผ + */ + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { ArrowLeft, Edit, GripVertical, Trash2 } 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 { PageHeader } from '@/components/organisms/PageHeader'; +import { useMenuStore } from '@/store/menuStore'; +import { usePermission } from '@/hooks/usePermission'; +import { toast } from 'sonner'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog'; +import { deleteChecklistItem } from './actions'; +import type { ChecklistItem } from '@/types/checklist'; + +interface ItemDetailProps { + item: ChecklistItem; + checklistId: string; +} + +export function ItemDetail({ item, checklistId }: ItemDetailProps) { + const router = useRouter(); + const sidebarCollapsed = useMenuStore((state) => state.sidebarCollapsed); + const { canUpdate, canDelete } = usePermission(); + const [showDeleteDialog, setShowDeleteDialog] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + + const handleEdit = () => { + router.push( + `/ko/master-data/checklist-management/${checklistId}/items/${item.id}?mode=edit` + ); + }; + + const handleBack = () => { + router.push(`/ko/master-data/checklist-management/${checklistId}`); + }; + + const handleDelete = async () => { + setIsDeleting(true); + try { + const result = await deleteChecklistItem(checklistId, item.id); + if (result.success) { + toast.success('ํ•ญ๋ชฉ์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'); + router.push(`/ko/master-data/checklist-management/${checklistId}`); + } else { + toast.error(result.error || '์‚ญ์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); + } + } catch { + toast.error('์‚ญ์ œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.'); + } finally { + setIsDeleting(false); + setShowDeleteDialog(false); + } + }; + + const documents = item.documents || []; + + return ( + + + +
+ {/* ๊ธฐ๋ณธ ์ •๋ณด */} + + + ๊ธฐ๋ณธ ์ •๋ณด + + +
+
+
ํ•ญ๋ชฉ ๋ฒˆํ˜ธ
+
{item.itemCode}
+
+
+
ํ•ญ๋ชฉ๋ช…
+
{item.itemName}
+
+
+
์†Œ๊ฐœ
+
{item.description || '-'}
+
+
+
์ƒํƒœ
+ + {item.status} + +
+
+
+
+ + {/* ๋ฌธ์„œ ์ •๋ณด */} + + + ๋ฌธ์„œ ์ •๋ณด + + + {documents.length === 0 ? ( +
+ ๋“ฑ๋ก๋œ ๋ฌธ์„œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. +
+ ) : ( +
+ + + + + + + + + + + + {documents.map((doc) => ( + + + + + + + + + ))} + +
+ + ์ˆœ์„œ + + ๋ฌธ์„œ ๋ฒˆํ˜ธ + + ๋ฌธ์„œ + + ๊ฐœ์ • + + ์‹œํ–‰์ผ +
+ + + {doc.order} + + {doc.documentCode} + + {doc.documentName} + {doc.revision}{doc.effectiveDate}
+
+ )} +
+
+
+ + {/* ํ•˜๋‹จ ์•ก์…˜ ๋ฒ„ํŠผ (sticky) */} +
+ +
+ {canDelete && ( + + )} + {canUpdate && ( + + )} +
+
+ + + + + ํ•ญ๋ชฉ ์‚ญ์ œ + + '{item.itemName}' ํ•ญ๋ชฉ์„ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ? ์ด ์ž‘์—…์€ ๋˜๋Œ๋ฆด ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + + + + ์ทจ์†Œ + + {isDeleting ? '์‚ญ์ œ ์ค‘...' : '์‚ญ์ œ'} + + + + +
+ ); +} diff --git a/src/components/checklist-management/ItemDetailClient.tsx b/src/components/checklist-management/ItemDetailClient.tsx new file mode 100644 index 00000000..77ea1db5 --- /dev/null +++ b/src/components/checklist-management/ItemDetailClient.tsx @@ -0,0 +1,110 @@ +'use client'; + +/** + * ํ•ญ๋ชฉ ์ƒ์„ธ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ + * + * ๋ผ์šฐํŒ…: + * - /[id]/items/[itemId] โ†’ ์ƒ์„ธ ๋ณด๊ธฐ + * - /[id]/items/[itemId]?mode=edit โ†’ ์ˆ˜์ • + * - /[id]/items/new โ†’ ๋“ฑ๋ก + */ + +import { useState, useEffect } from 'react'; +import { useSearchParams } from 'next/navigation'; +import { ItemDetail } from './ItemDetail'; +import { ItemForm } from './ItemForm'; +import { getChecklistItemById } from './actions'; +import type { ChecklistItem } from '@/types/checklist'; +import { DetailPageSkeleton } from '@/components/ui/skeleton'; +import { ErrorCard } from '@/components/ui/error-card'; +import { toast } from 'sonner'; + +type DetailMode = 'view' | 'edit' | 'create'; + +interface ItemDetailClientProps { + checklistId: string; + itemId: string; +} + +export function ItemDetailClient({ checklistId, itemId }: ItemDetailClientProps) { + const searchParams = useSearchParams(); + const modeFromQuery = searchParams.get('mode') as DetailMode | null; + const isNewMode = itemId === 'new'; + + const [mode] = useState(() => { + if (isNewMode) return 'create'; + if (modeFromQuery === 'edit') return 'edit'; + return 'view'; + }); + + const [itemData, setItemData] = useState(null); + const [isLoading, setIsLoading] = useState(!isNewMode); + const [error, setError] = useState(null); + + useEffect(() => { + if (isNewMode) { + setIsLoading(false); + return; + } + + const loadData = async () => { + setIsLoading(true); + setError(null); + try { + const result = await getChecklistItemById(checklistId, itemId); + if (result.success && result.data) { + setItemData(result.data); + } else { + setError(result.error || 'ํ•ญ๋ชฉ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.'); + toast.error('ํ•ญ๋ชฉ์„ ๋ถˆ๋Ÿฌ์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); + } + } catch { + setError('ํ•ญ๋ชฉ ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.'); + toast.error('ํ•ญ๋ชฉ์„ ๋ถˆ๋Ÿฌ์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); + } finally { + setIsLoading(false); + } + }; + + loadData(); + }, [checklistId, itemId, isNewMode]); + + if (isLoading) { + return ; + } + + if (error && !isNewMode) { + return ( + + ); + } + + if (mode === 'create') { + return ; + } + + if (mode === 'edit' && itemData) { + return ; + } + + if (mode === 'view' && itemData) { + return ; + } + + return ( + + ); +} diff --git a/src/components/checklist-management/ItemForm.tsx b/src/components/checklist-management/ItemForm.tsx new file mode 100644 index 00000000..76009f4c --- /dev/null +++ b/src/components/checklist-management/ItemForm.tsx @@ -0,0 +1,350 @@ +'use client'; + +/** + * ํ•ญ๋ชฉ ๋“ฑ๋ก/์ˆ˜์ • ํผ ์ปดํฌ๋„ŒํŠธ + * + * ๊ธฐํš์„œ ์Šคํฌ๋ฆฐ์ƒท 3 ๊ธฐ์ค€: + * - ๊ธฐ๋ณธ ์ •๋ณด: ํ•ญ๋ชฉ ๋ฒˆํ˜ธ(์ž๋™), ํ•ญ๋ชฉ๋ช…, ์†Œ๊ฐœ, ์ƒํƒœ + * - ๋ฌธ์„œ ์ •๋ณด: ์ˆœ์„œ, ๋ฌธ์„œ ๋ฒˆํ˜ธ, ๋ฌธ์„œ, ๊ฐœ์ •, ์‹œํ–‰์ผ + ํ–‰ ์ถ”๊ฐ€/์‚ญ์ œ + */ + +import { useState, useCallback } from 'react'; +import { useRouter } from 'next/navigation'; +import { Plus, Trash2, GripVertical } from 'lucide-react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Button } from '@/components/ui/button'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate'; +import { toast } from 'sonner'; +import type { ChecklistItem, ChecklistDocumentFormData } from '@/types/checklist'; +import { createChecklistItem, updateChecklistItem } from './actions'; +import type { DetailConfig } from '@/components/templates/IntegratedDetailTemplate/types'; + +const itemCreateConfig: DetailConfig = { + title: 'ํ•ญ๋ชฉ', + description: '์ƒˆ๋กœ์šด ํ•ญ๋ชฉ์„ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค', + basePath: '', + fields: [], + actions: { + showBack: true, + showEdit: false, + showDelete: false, + showSave: true, + submitLabel: '๋“ฑ๋ก', + }, +}; + +const itemEditConfig: DetailConfig = { + ...itemCreateConfig, + description: 'ํ•ญ๋ชฉ ์ •๋ณด๋ฅผ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค', + actions: { + ...itemCreateConfig.actions, + submitLabel: '์ €์žฅ', + }, +}; + +interface ItemFormProps { + mode: 'create' | 'edit'; + checklistId: string; + initialData?: ChecklistItem; +} + +export function ItemForm({ mode, checklistId, initialData }: ItemFormProps) { + const router = useRouter(); + const isEdit = mode === 'edit'; + + const [itemName, setItemName] = useState(initialData?.itemName || ''); + const [description, setDescription] = useState(initialData?.description || ''); + const [status, setStatus] = useState(initialData?.status || '์‚ฌ์šฉ'); + + // ๋ฌธ์„œ ๋ชฉ๋ก ์ƒํƒœ + const [documents, setDocuments] = useState(() => { + if (initialData?.documents && initialData.documents.length > 0) { + return initialData.documents.map((doc) => ({ + id: doc.id, + documentCode: doc.documentCode, + documentName: doc.documentName, + revision: doc.revision, + effectiveDate: doc.effectiveDate, + order: doc.order, + })); + } + return []; + }); + + const handleAddDocument = () => { + setDocuments((prev) => [ + ...prev, + { + documentCode: '', + documentName: '', + revision: '', + effectiveDate: '', + order: prev.length + 1, + }, + ]); + }; + + const handleRemoveDocument = (index: number) => { + setDocuments((prev) => { + const updated = prev.filter((_, i) => i !== index); + return updated.map((doc, i) => ({ ...doc, order: i + 1 })); + }); + }; + + const handleDocumentChange = ( + index: number, + field: keyof ChecklistDocumentFormData, + value: string | number + ) => { + setDocuments((prev) => + prev.map((doc, i) => (i === index ? { ...doc, [field]: value } : doc)) + ); + }; + + const handleSubmit = async (): Promise<{ success: boolean; error?: string }> => { + if (!itemName.trim()) { + toast.error('ํ•ญ๋ชฉ๋ช…์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.'); + return { success: false, error: 'ํ•ญ๋ชฉ๋ช…์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.' }; + } + + const formData = { + itemName: itemName.trim(), + description: description.trim(), + status: status as '์‚ฌ์šฉ' | '๋ฏธ์‚ฌ์šฉ', + documents, + }; + + try { + if (isEdit && initialData?.id) { + const result = await updateChecklistItem(checklistId, initialData.id, formData); + if (result.success) { + toast.success('ํ•ญ๋ชฉ์ด ์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'); + router.push( + `/ko/master-data/checklist-management/${checklistId}/items/${initialData.id}` + ); + return { success: true }; + } else { + toast.error(result.error || '์ˆ˜์ •์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); + return { success: false, error: result.error }; + } + } else { + const result = await createChecklistItem(checklistId, formData); + if (result.success && result.data) { + toast.success('ํ•ญ๋ชฉ์ด ๋“ฑ๋ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'); + router.push(`/ko/master-data/checklist-management/${checklistId}`); + return { success: true }; + } else { + toast.error(result.error || '๋“ฑ๋ก์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); + return { success: false, error: result.error }; + } + } + } catch { + toast.error('์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.'); + return { success: false, error: '์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.' }; + } + }; + + const handleCancel = () => { + if (isEdit && initialData?.id) { + router.push( + `/ko/master-data/checklist-management/${checklistId}/items/${initialData.id}` + ); + } else { + router.push(`/ko/master-data/checklist-management/${checklistId}`); + } + }; + + const renderFormContent = useCallback( + () => ( +
+ {/* ๊ธฐ๋ณธ ์ •๋ณด */} + + + ๊ธฐ๋ณธ ์ •๋ณด + + +
+
+ + +
+
+ + setItemName(e.target.value)} + placeholder="ํ•ญ๋ชฉ๋ช…์„ ์ž…๋ ฅํ•˜์„ธ์š”" + /> +
+
+ + setDescription(e.target.value)} + placeholder="์†Œ๊ฐœ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”" + /> +
+
+ + +
+
+
+
+ + {/* ๋ฌธ์„œ ์ •๋ณด */} + + +
+ + ๋ฌธ์„œ ์ •๋ณด + + ์ด {documents.length}๊ฑด + + + +
+
+ + {documents.length === 0 ? ( +
+ ๋“ฑ๋ก๋œ ๋ฌธ์„œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. [๋ฌธ์„œ ์ถ”๊ฐ€] ๋ฒ„ํŠผ์œผ๋กœ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”. +
+ ) : ( +
+ + + + + + + + + + + + + {documents.map((doc, index) => ( + + + + + + + + + + ))} + +
+ + ์ˆœ์„œ + + ๋ฌธ์„œ ๋ฒˆํ˜ธ + + ๋ฌธ์„œ + + ๊ฐœ์ • + + ์‹œํ–‰์ผ + + ์‚ญ์ œ +
+ + + {doc.order} + + + handleDocumentChange(index, 'documentCode', e.target.value) + } + placeholder="๋ฌธ์„œ ๋ฒˆํ˜ธ" + className="h-8" + /> + + + handleDocumentChange(index, 'documentName', e.target.value) + } + placeholder="๋ฌธ์„œ๋ช…" + className="h-8" + /> + + + handleDocumentChange(index, 'revision', e.target.value) + } + placeholder="REV" + className="h-8" + /> + + + handleDocumentChange(index, 'effectiveDate', e.target.value) + } + className="h-8" + /> + + +
+
+ )} +
+
+
+ ), + [itemName, description, status, documents, initialData?.itemCode] + ); + + const config = isEdit ? itemEditConfig : itemCreateConfig; + + return ( + + ); +} diff --git a/src/components/checklist-management/actions.ts b/src/components/checklist-management/actions.ts new file mode 100644 index 00000000..ead690d9 --- /dev/null +++ b/src/components/checklist-management/actions.ts @@ -0,0 +1,342 @@ +'use server'; + +/** + * ์ ๊ฒ€ํ‘œ ๊ด€๋ฆฌ Server Actions + * + * ํ˜„์žฌ: Mock ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ + * ์ถ”ํ›„: executeServerAction์œผ๋กœ ๋ฐฑ์—”๋“œ API ์—ฐ๋™ ์ „ํ™˜ + */ + +import type { + Checklist, + ChecklistFormData, + ChecklistItem, + ChecklistItemFormData, + ChecklistDocument, +} from '@/types/checklist'; + +// ============================================================================ +// Mock ๋ฐ์ดํ„ฐ +// ============================================================================ + +const MOCK_DOCUMENTS: ChecklistDocument[] = [ + { + id: 'doc-1', + itemId: 'item-1', + documentCode: 'QM-std-1-1-1', + documentName: '๋ฌธ์„œ๋ช….pdf', + revision: 'REV12', + effectiveDate: '2026-01-05', + order: 1, + }, + { + id: 'doc-2', + itemId: 'item-1', + documentCode: 'QM-std-1-1-1', + documentName: '๋ฌธ์„œ๋ช….pdf', + revision: 'REV12', + effectiveDate: '2026-01-06', + order: 2, + }, +]; + +const MOCK_ITEMS: ChecklistItem[] = [ + { + id: 'item-1', + checklistId: 'cl-1', + itemCode: '123123', + itemName: '1. ์ˆ˜์ž…๊ฒ€์‚ฌ ๊ธฐ์ค€ ํ™•์ธ', + description: '์†Œ๊ฐœ ๋ฌธ๊ตฌ', + documentCount: 3, + status: '์‚ฌ์šฉ', + order: 1, + documents: MOCK_DOCUMENTS, + createdAt: '2026-01-01T00:00:00Z', + updatedAt: '2026-01-01T00:00:00Z', + }, + { + id: 'item-2', + checklistId: 'cl-1', + itemCode: '123123', + itemName: 'ํ•ญ๋ชฉ๋ช…', + description: '', + documentCount: 3, + status: '์‚ฌ์šฉ', + order: 2, + documents: [], + createdAt: '2026-01-01T00:00:00Z', + updatedAt: '2026-01-01T00:00:00Z', + }, + { + id: 'item-3', + checklistId: 'cl-1', + itemCode: '123123', + itemName: 'ํ•ญ๋ชฉ๋ช…', + description: '', + documentCount: 3, + status: '์‚ฌ์šฉ', + order: 3, + documents: [], + createdAt: '2026-01-01T00:00:00Z', + updatedAt: '2026-01-01T00:00:00Z', + }, +]; + +const MOCK_CHECKLISTS: Checklist[] = [ + { + id: 'cl-1', + checklistCode: '๋ณ€ํ˜ธ๋ช…', + checklistName: '์ ๊ฒ€ํ‘œ๋ช…', + itemCount: 3, + documentCount: 6, + status: '์‚ฌ์šฉ', + order: 1, + createdAt: '2025-09-01T00:00:00Z', + updatedAt: '2025-09-01T00:00:00Z', + }, + { + id: 'cl-2', + checklistCode: '๋ณ€ํ˜ธ๋ช…', + checklistName: '์ ๊ฒ€ํ‘œ๋ช…', + itemCount: 3, + documentCount: 6, + status: '๋ฏธ์‚ฌ์šฉ', + order: 2, + createdAt: '2025-09-01T00:00:00Z', + updatedAt: '2025-09-01T00:00:00Z', + }, + { + id: 'cl-3', + checklistCode: '๋ณ€ํ˜ธ๋ช…', + checklistName: '์ ๊ฒ€ํ‘œ๋ช…', + itemCount: 3, + documentCount: 6, + status: '์‚ฌ์šฉ', + order: 3, + createdAt: '2025-09-01T00:00:00Z', + updatedAt: '2025-09-01T00:00:00Z', + }, + { + id: 'cl-4', + checklistCode: '๋ณ€ํ˜ธ๋ช…', + checklistName: '์ ๊ฒ€ํ‘œ๋ช…', + itemCount: 3, + documentCount: 6, + status: '๋ฏธ์‚ฌ์šฉ', + order: 4, + createdAt: '2025-09-01T00:00:00Z', + updatedAt: '2025-09-01T00:00:00Z', + }, + { + id: 'cl-5', + checklistCode: '๋ณ€ํ˜ธ๋ช…', + checklistName: '์ ๊ฒ€ํ‘œ๋ช…', + itemCount: 3, + documentCount: 6, + status: '์‚ฌ์šฉ', + order: 5, + createdAt: '2025-09-01T00:00:00Z', + updatedAt: '2025-09-01T00:00:00Z', + }, + { + id: 'cl-6', + checklistCode: '๋ณ€ํ˜ธ๋ช…', + checklistName: '์ ๊ฒ€ํ‘œ๋ช…', + itemCount: 3, + documentCount: 6, + status: '๋ฏธ์‚ฌ์šฉ', + order: 6, + createdAt: '2025-09-01T00:00:00Z', + updatedAt: '2025-09-01T00:00:00Z', + }, + { + id: 'cl-7', + checklistCode: '๋ณ€ํ˜ธ๋ช…', + checklistName: '์ ๊ฒ€ํ‘œ๋ช…', + itemCount: 3, + documentCount: 6, + status: '์‚ฌ์šฉ', + order: 7, + createdAt: '2025-09-01T00:00:00Z', + updatedAt: '2025-09-01T00:00:00Z', + }, +]; + +// ============================================================================ +// ์ ๊ฒ€ํ‘œ CRUD +// ============================================================================ + +export async function getChecklistList(): Promise<{ + success: boolean; + data?: { items: Checklist[] }; + error?: string; +}> { + return { success: true, data: { items: [...MOCK_CHECKLISTS] } }; +} + +export async function getChecklistById(id: string): Promise<{ + success: boolean; + data?: Checklist; + error?: string; +}> { + const checklist = MOCK_CHECKLISTS.find((c) => c.id === id); + if (!checklist) return { success: false, error: '์ ๊ฒ€ํ‘œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.' }; + return { success: true, data: { ...checklist } }; +} + +export async function createChecklist(data: ChecklistFormData): Promise<{ + success: boolean; + data?: Checklist; + error?: string; +}> { + const newChecklist: Checklist = { + id: `cl-${Date.now()}`, + checklistCode: `CL-${String(MOCK_CHECKLISTS.length + 1).padStart(3, '0')}`, + checklistName: data.checklistName, + itemCount: 0, + documentCount: 0, + status: data.status, + order: MOCK_CHECKLISTS.length + 1, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + return { success: true, data: newChecklist }; +} + +export async function updateChecklist( + id: string, + data: ChecklistFormData +): Promise<{ success: boolean; data?: Checklist; error?: string }> { + const checklist = MOCK_CHECKLISTS.find((c) => c.id === id); + if (!checklist) return { success: false, error: '์ ๊ฒ€ํ‘œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.' }; + const updated = { + ...checklist, + checklistName: data.checklistName, + status: data.status, + updatedAt: new Date().toISOString(), + }; + return { success: true, data: updated }; +} + +export async function deleteChecklist(id: string): Promise<{ + success: boolean; + error?: string; +}> { + return { success: true }; +} + +export async function deleteChecklists(ids: string[]): Promise<{ + success: boolean; + deletedCount?: number; + error?: string; +}> { + return { success: true, deletedCount: ids.length }; +} + +export async function toggleChecklistStatus(id: string): Promise<{ + success: boolean; + data?: Checklist; + error?: string; +}> { + const checklist = MOCK_CHECKLISTS.find((c) => c.id === id); + if (!checklist) return { success: false, error: '์ ๊ฒ€ํ‘œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.' }; + const toggled = { + ...checklist, + status: checklist.status === '์‚ฌ์šฉ' ? '๋ฏธ์‚ฌ์šฉ' as const : '์‚ฌ์šฉ' as const, + }; + return { success: true, data: toggled }; +} + +export async function reorderChecklists( + items: { id: string; order: number }[] +): Promise<{ success: boolean; error?: string }> { + return { success: true }; +} + +export async function getChecklistStats(): Promise<{ + success: boolean; + data?: { total: number; active: number; inactive: number }; + error?: string; +}> { + const active = MOCK_CHECKLISTS.filter((c) => c.status === '์‚ฌ์šฉ').length; + return { + success: true, + data: { + total: MOCK_CHECKLISTS.length, + active, + inactive: MOCK_CHECKLISTS.length - active, + }, + }; +} + +// ============================================================================ +// ์ ๊ฒ€ํ‘œ ํ•ญ๋ชฉ CRUD +// ============================================================================ + +export async function getChecklistItems(checklistId: string): Promise<{ + success: boolean; + data?: ChecklistItem[]; + error?: string; +}> { + return { success: true, data: [...MOCK_ITEMS] }; +} + +export async function getChecklistItemById( + checklistId: string, + itemId: string +): Promise<{ success: boolean; data?: ChecklistItem; error?: string }> { + const item = MOCK_ITEMS.find((i) => i.id === itemId); + if (!item) return { success: false, error: 'ํ•ญ๋ชฉ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.' }; + return { success: true, data: { ...item, documents: [...MOCK_DOCUMENTS] } }; +} + +export async function createChecklistItem( + checklistId: string, + data: ChecklistItemFormData +): Promise<{ success: boolean; data?: ChecklistItem; error?: string }> { + const newItem: ChecklistItem = { + id: `item-${Date.now()}`, + checklistId, + itemCode: String(Date.now()).slice(-6), + itemName: data.itemName, + description: data.description, + documentCount: data.documents.length, + status: data.status, + order: MOCK_ITEMS.length + 1, + documents: [], + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + return { success: true, data: newItem }; +} + +export async function updateChecklistItem( + checklistId: string, + itemId: string, + data: ChecklistItemFormData +): Promise<{ success: boolean; data?: ChecklistItem; error?: string }> { + const item = MOCK_ITEMS.find((i) => i.id === itemId); + if (!item) return { success: false, error: 'ํ•ญ๋ชฉ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.' }; + const updated = { + ...item, + itemName: data.itemName, + description: data.description, + status: data.status, + updatedAt: new Date().toISOString(), + }; + return { success: true, data: updated }; +} + +export async function deleteChecklistItem( + checklistId: string, + itemId: string +): Promise<{ success: boolean; error?: string }> { + return { success: true }; +} + +export async function reorderChecklistItems( + checklistId: string, + items: { id: string; order: number }[] +): Promise<{ success: boolean; error?: string }> { + return { success: true }; +} \ No newline at end of file diff --git a/src/components/checklist-management/index.ts b/src/components/checklist-management/index.ts new file mode 100644 index 00000000..747c5c2f --- /dev/null +++ b/src/components/checklist-management/index.ts @@ -0,0 +1,7 @@ +export { default as ChecklistListClient } from './ChecklistListClient'; +export { ChecklistDetailClient } from './ChecklistDetailClient'; +export { ChecklistDetail } from './ChecklistDetail'; +export { ChecklistForm } from './ChecklistForm'; +export { ItemDetailClient } from './ItemDetailClient'; +export { ItemDetail } from './ItemDetail'; +export { ItemForm } from './ItemForm'; diff --git a/src/components/orders/documents/SalesOrderDocument.tsx b/src/components/orders/documents/SalesOrderDocument.tsx index a45bf417..e36069c1 100644 --- a/src/components/orders/documents/SalesOrderDocument.tsx +++ b/src/components/orders/documents/SalesOrderDocument.tsx @@ -1,13 +1,13 @@ "use client"; /** - * ์ˆ˜์ฃผ์„œ ๋ฌธ์„œ ์ปดํฌ๋„ŒํŠธ - * - ์Šคํฌ๋ฆฐ์ƒท ๊ธฐ๋ฐ˜ ๋””์ž์ธ - * - ์ œ๋ชฉ ์ขŒ์ธก, ๊ฒฐ์žฌ๋ž€ ์šฐ์ธก - * - ์‹ ์ฒญ์—…์ฒด/์‹ ์ฒญ๋‚ด์šฉ/๋‚ฉํ’ˆ์ •๋ณด 3์—ด ๊ตฌ์กฐ - * - ์Šคํฌ๋ฆฐ, ๋ชจํ„ฐ, ์ ˆ๊ณก๋ฌผ ํ…Œ์ด๋ธ” + * ์ˆ˜์ฃผ์„œ ๋ฌธ์„œ ์ปดํฌ๋„ŒํŠธ (๊ธฐํš์„œ D1.8 ๊ธฐ์ค€ ๋ฆฌ๋””์ž์ธ) + * - ์ถœ๊ณ ์ฆ(ShipmentOrderDocument)๊ณผ ๋™์ผํ•œ ์ž์žฌ ์„น์…˜ ๊ตฌ์กฐ + * - ์ˆ˜์ฃผ์„œ ์ „์šฉ ํ—ค๋” (๊ฒฐ์žฌ๋ž€, ๋กœํŠธ๋ฒˆํ˜ธ, ์ œํ’ˆ์ฝ”๋“œ, ์ธ์ •๋ฒˆํ˜ธ) + * - ๋ฐฐ์ฐจ์ •๋ณด/LOT ์ปฌ๋Ÿผ ์—†์Œ */ +import { useState } from "react"; import { getTodayString } from "@/utils/date"; import { OrderItem } from "../actions"; import { ProductInfo } from "./OrderDocumentModal"; @@ -15,8 +15,8 @@ import { ConstructionApprovalTable } from "@/components/document-system"; interface SalesOrderDocumentProps { documentNumber?: string; - orderNumber: string; // ๋กœํŠธ๋ฒˆํ˜ธ - certificationNumber?: string; // ์ธ์ •๋ฒˆํ˜ธ + orderNumber: string; + certificationNumber?: string; orderDate?: string; client: string; siteName?: string; @@ -35,23 +35,64 @@ interface SalesOrderDocumentProps { remarks?: string; } -/** - * ์ˆ˜๋Ÿ‰ ํฌ๋งท ํ•จ์ˆ˜ - */ -function formatQuantity(quantity: number, unit?: string): string { - const countableUnits = ["EA", "SET", "PCS", "๊ฐœ", "์„ธํŠธ", "BOX", "ROLL"]; - const upperUnit = (unit || "").toUpperCase(); +// ===== ๋ฌธ์„œ ์ „์šฉ ๋ชฉ๋ฐ์ดํ„ฐ (์ถœ๊ณ ์ฆ๊ณผ ๋™์ผ ๊ตฌ์กฐ) ===== - if (countableUnits.includes(upperUnit)) { - return Math.round(quantity).toLocaleString(); - } +const MOCK_SCREEN_ROWS = [ + { no: 1, type: '์ด(๋งˆ)', code: 'FA123', openW: 4300, openH: 4300, madeW: 4300, madeH: 3000, guideRail: '๋ฐฑ๋ฉดํ˜•', shaft: 5, caseInch: 5, bracket: '500X300 380X180', capacity: 300, finish: 'SUS๋งˆ๊ฐ' }, + { no: 2, type: '์ด(๋งˆ)', code: 'FA123', openW: 4300, openH: 4300, madeW: 4300, madeH: 3000, guideRail: '๋ฐฑ๋ฉดํ˜•', shaft: 5, caseInch: 5, bracket: '500X300 380X180', capacity: 300, finish: 'SUS๋งˆ๊ฐ' }, +]; - const rounded = Math.round(quantity * 10000) / 10000; - return rounded.toLocaleString(undefined, { - minimumFractionDigits: 0, - maximumFractionDigits: 4 - }); -} +const MOCK_STEEL_ROWS = [ + { no: 1, code: 'FA123', openW: 4300, openH: 3000, madeW: 4300, madeH: 3000, guideRail: '๋ฐฑ๋ฉดํ˜•', shaft: 5, jointBar: 5, caseInch: 5, bracket: '500X300 380X180', capacity: 300, finish: 'SUS๋งˆ๊ฐ' }, + { no: 2, code: 'FA123', openW: 4300, openH: 3000, madeW: 4300, madeH: 3000, guideRail: '๋ฐฑ๋ฉดํ˜•', shaft: 5, jointBar: 5, caseInch: 5, bracket: '500X300 380X180', capacity: 300, finish: 'SUS๋งˆ๊ฐ' }, +]; + +const MOCK_MOTOR_LEFT = [ + { item: '๋ชจํ„ฐ', type: '380V ๋‹จ์ƒ', spec: 'KD-150K', qty: 6 }, + { item: '๋ธŒ๋ผ์ผ“ํŠธ', type: '-', spec: '380X180', qty: 6 }, + { item: '์•ต๊ธ€', type: '๋ฐ‘์นจํ†ต ์˜๊ธˆ', spec: '40*40*380', qty: 4 }, +]; + +const MOCK_MOTOR_RIGHT = [ + { item: '์ „๋™๊ฐœํ๊ธฐ', type: '๋ฆด๋ฐ•์Šค', spec: '-', qty: 1 }, + { item: '์ „๋™๊ฐœํ๊ธฐ', type: '๋งค์ž…', spec: '-', qty: 1 }, +]; + +const MOCK_GUIDE_RAIL_ITEMS = [ + { name: 'ํ•ญ๋ชฉ๋ช…', spec: 'L: 3,000', qty: 22 }, + { name: 'ํ•˜๋ถ€BASE', spec: '130X80', qty: 22 }, +]; + +const MOCK_GUIDE_SMOKE = { name: '์—ฐ๊ธฐ์ฐจ๋‹จ์žฌ(W50)', spec: '2,438', qty: 4 }; + +const MOCK_CASE_ITEMS = [ + { name: '500X330', spec: 'L: 4,000', qty: 3 }, + { name: '500X330', spec: 'L: 5,000', qty: 4 }, + { name: '์ƒ๋ถ€๋ฎ๊ฐœ', spec: '1219X389', qty: 55 }, + { name: '์ธก๋ฉด๋ถ€ (๋งˆ๊ตฌ๋ฆฌ)', spec: '500X355', qty: '500X355' }, +]; + +const MOCK_CASE_SMOKE = { name: '์—ฐ๊ธฐ์ฐจ๋‹จ์žฌ(W80)', spec: '3,000', qty: 4 }; + +const MOCK_BOTTOM_SCREEN = [ + { name: 'ํ•˜๋‹จ๋งˆ๊ฐ์žฌ', spec: '60X40', l1: 'L: 3,000', q1: 6, name2: 'ํ•˜๋‹จ๋งˆ๊ฐ์žฌ', spec2: '60X40', l2: 'L: 4,000', q2: 6 }, + { name: 'ํ•˜๋‹จ๋ณด๊ฐ•์—˜๋น„', spec: '60X17', l1: 'L: 3,000', q1: 6, name2: 'ํ•˜๋‹จ๋ณด๊ฐ•์—˜๋น„', spec2: '60X17', l2: 'L: 4,000', q2: 6 }, + { name: 'ํ•˜๋‹จ๋ณด๊ฐ•ํ‰์ฒ ', spec: '-', l1: 'L: 3,000', q1: 6, name2: 'ํ•˜๋‹จ๋ณด๊ฐ•ํ‰์ฒ ', spec2: '-', l2: 'L: 4,000', q2: 6 }, + { name: 'ํ•˜๋‹จ๋ฌด๊ฒŒํ‰์ฒ ', spec: '50X12T', l1: 'L: 3,000', q1: 6, name2: 'ํ•˜๋‹จ๋ฌด๊ฒŒํ‰์ฒ ', spec2: '50X12T', l2: 'L: 4,000', q2: 6 }, +]; + +const MOCK_BOTTOM_STEEL = { spec: '60X40', length: 'L: 3,000', qty: 22 }; + +const MOCK_SUBSIDIARY = [ + { leftItem: '๊ฐ๊ธฐ์‚ฌํ”„ํŠธ', leftSpec: '4์ธ์น˜ 4500', leftQty: 6, rightItem: '๊ฐํŒŒ์ดํ”„', rightSpec: '6000', rightQty: 4 }, + { leftItem: '์กฐ์ธํŠธ๋ฐ”', leftSpec: '300', leftQty: 6, rightItem: 'ํ™˜๋ด‰', rightSpec: '3000', rightQty: 5 }, +]; + +// ===== ๊ณตํ†ต ์Šคํƒ€์ผ ===== +const thBase = 'border-r border-gray-400 px-1 py-1'; +const tdBase = 'border-r border-gray-300 px-1 py-1'; +const tdCenter = `${tdBase} text-center`; +const imgPlaceholder = 'flex items-center justify-center border border-dashed border-gray-300 text-gray-400'; export function SalesOrderDocument({ documentNumber = "ABC123", @@ -73,53 +114,14 @@ export function SalesOrderDocument({ products = [], remarks, }: SalesOrderDocumentProps) { - // ์Šคํฌ๋ฆฐ ์ œํ’ˆ๋งŒ ํ•„ํ„ฐ๋ง - const screenProducts = products.filter(p => - p.productCategory?.includes("์Šคํฌ๋ฆฐ") || - p.productName?.includes("์Šคํฌ๋ฆฐ") || - p.productName?.includes("๋ฐฉํ™”") || - p.productName?.includes("์…”ํ„ฐ") - ); + const [bottomFinishView, setBottomFinishView] = useState<'screen' | 'steel'>('screen'); - // ๋ชจํ„ฐ ์•„์ดํ…œ ํ•„ํ„ฐ๋ง - const motorItems = items.filter(item => - item.itemName?.toLowerCase().includes("๋ชจํ„ฐ") || - item.type?.includes("๋ชจํ„ฐ") || - item.itemCode?.startsWith("MT") - ); - - // ๋ธŒ๋ผ์ผ“ ์•„์ดํ…œ ํ•„ํ„ฐ๋ง - const bracketItems = items.filter(item => - item.itemName?.includes("๋ธŒ๋ผ์ผ“") || - item.type?.includes("๋ธŒ๋ผ์ผ“") - ); - - // ๊ฐ€์ด๋“œ๋ ˆ์ผ ์•„์ดํ…œ ํ•„ํ„ฐ๋ง - const guideRailItems = items.filter(item => - item.itemName?.includes("๊ฐ€์ด๋“œ") || - item.itemName?.includes("๋ ˆ์ผ") || - item.type?.includes("๊ฐ€์ด๋“œ") - ); - - // ์ผ€์ด์Šค ์•„์ดํ…œ ํ•„ํ„ฐ๋ง - const caseItems = items.filter(item => - item.itemName?.includes("์ผ€์ด์Šค") || - item.itemName?.includes("์…”ํ„ฐ๋ฐ•์Šค") || - item.type?.includes("์ผ€์ด์Šค") - ); - - // ํ•˜๋‹จ๋งˆ๊ฐ์žฌ ์•„์ดํ…œ ํ•„ํ„ฐ๋ง - const bottomFinishItems = items.filter(item => - item.itemName?.includes("ํ•˜๋‹จ") || - item.itemName?.includes("๋งˆ๊ฐ") || - item.type?.includes("ํ•˜๋‹จ๋งˆ๊ฐ") - ); + const motorRows = Math.max(MOCK_MOTOR_LEFT.length, MOCK_MOTOR_RIGHT.length); return (
- {/* ํ—ค๋”: ์ˆ˜์ฃผ์„œ ์ œ๋ชฉ (์ขŒ์ธก) + ๊ฒฐ์žฌ๋ž€ (์šฐ์ธก) */} + {/* ========== ํ—ค๋”: ์ˆ˜์ฃผ์„œ ์ œ๋ชฉ + ๊ฒฐ์žฌ๋ž€ ========== */}
- {/* ์ˆ˜์ฃผ์„œ ์ œ๋ชฉ (์ขŒ์ธก) */}

์ˆ˜ ์ฃผ ์„œ

@@ -129,30 +131,26 @@ export function SalesOrderDocument({
- - {/* ๊ฒฐ์žฌ๋ž€ (์šฐ์ธก) */} - +
- {/* ์ƒํ’ˆ๋ช… / ์ œํ’ˆ๋ช… / ๋กœํŠธ๋ฒˆํ˜ธ / ์ธ์ •๋ฒˆํ˜ธ */} + {/* ========== ๋กœํŠธ๋ฒˆํ˜ธ / ์ œํ’ˆ๋ช… / ์ œํ’ˆ์ฝ”๋“œ / ์ธ์ •๋ฒˆํ˜ธ ========== */} - - - - + + + +
์ƒํ’ˆ๋ช…{products[0]?.productCategory || "-"}์ œํ’ˆ๋ช…{products[0]?.productName || "-"} ๋กœํŠธ๋ฒˆํ˜ธ {orderNumber}์ œํ’ˆ๋ช…{products[0]?.productName || "-"}์ œํ’ˆ์ฝ”๋“œKWS01 ์ธ์ •๋ฒˆํ˜ธ {certificationNumber}
- {/* 3์—ด ์„น์…˜: ์‹ ์ฒญ์—…์ฒด | ์‹ ์ฒญ๋‚ด์šฉ | ๋‚ฉํ’ˆ์ •๋ณด */} + {/* ========== 3์—ด ์„น์…˜: ์‹ ์ฒญ์—…์ฒด | ์‹ ์ฒญ๋‚ด์šฉ | ๋‚ฉํ’ˆ์ •๋ณด ========== */}
{/* ์‹ ์ฒญ์—…์ฒด */} @@ -165,21 +163,17 @@ export function SalesOrderDocument({ {orderDate} - ์—…์ฒด๋ช… + ์ˆ˜์ฃผ์ฒ˜ {client} ์ˆ˜์ฃผ ๋‹ด๋‹น์ž {manager} - + ๋‹ด๋‹น์ž ์—ฐ๋ฝ์ฒ˜ {managerContact} - - ๋ฐฐ์†ก์ง€ ์ฃผ์†Œ - {address} -
@@ -201,14 +195,10 @@ export function SalesOrderDocument({ ์ถœ๊ณ ์ผ {expectedShipDate} - + ์…”ํ„ฐ์ถœ์ˆ˜๋Ÿ‰ {shutterCount}๊ฐœ์†Œ - -   -   -
@@ -230,377 +220,411 @@ export function SalesOrderDocument({ ์ธ์ˆ˜์ž์—ฐ๋ฝ์ฒ˜ {recipientContact} - + ๋ฐฐ์†ก๋ฐฉ๋ฒ• {deliveryMethod} - -   -   -
+ {/* ๋ฐฐ์†ก์ง€ ์ฃผ์†Œ - ํ•œ ์ค„ ๋ณ‘ํ•ฉ */} +
+
๋ฐฐ์†ก์ง€ ์ฃผ์†Œ
+
{address}
+

์•„๋ž˜์™€ ๊ฐ™์ด ์ฃผ๋ฌธํ•˜์˜ค๋‹ˆ ํ’ˆ์งˆ ๋ฐ ๋‚ฉ๊ธฐ์ผ์„ ์ค€์ˆ˜ํ•˜์—ฌ ์ฃผ์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.

- {/* 1. ์Šคํฌ๋ฆฐ ํ…Œ์ด๋ธ” */} + {/* ========== 1. ์Šคํฌ๋ฆฐ ========== */}

1. ์Šคํฌ๋ฆฐ

- - - - - - - - - + + + + + + + + + - - - - - - + + + + + + - {screenProducts.length > 0 ? ( - screenProducts.map((product, index) => ( - - - - - - - - - - - - - - - - )) - ) : ( - - + {MOCK_SCREEN_ROWS.map((row) => ( + + + + + + + + + + + + + + - )} + ))}
Noํ’ˆ๋ฅ˜๋ถ€ํ˜ธ์˜คํ”ˆ์‚ฌ์ด์ฆˆ์ œ์ž‘์‚ฌ์ด์ฆˆ๊ฐ€์ด๋“œ๋ ˆ์ผ
์œ ํ˜•
์ƒคํ”„ํŠธ
(์ธ์น˜)
์ผ€์ด์Šค
(์ธ์น˜)
๋ชจํ„ฐNoํ’ˆ๋ฅ˜๋ถ€ํ˜ธ์˜คํ”ˆ์‚ฌ์ด์ฆˆ์ œ์ž‘์‚ฌ์ด์ฆˆ๊ฐ€์ด๋“œ
๋ ˆ์ผ
์‚ฌํ”„ํŠธ
(์ธ์น˜)
์ผ€์ด์Šค
(์ธ์น˜)
๋ชจํ„ฐ ๋งˆ๊ฐ
๊ฐ€๋กœ์„ธ๋กœ๊ฐ€๋กœ์„ธ๋กœ๋ธŒ๋ผ์ผ“ํŠธ์šฉ๋Ÿ‰Kg๊ฐ€๋กœ์„ธ๋กœ๊ฐ€๋กœ์„ธ๋กœ๋ธŒ๋ผ์ผ“ํŠธ์šฉ๋Ÿ‰Kg
{index + 1}{product.productCategory || "-"}{product.code || "-"}{product.openWidth || "-"}{product.openHeight || "-"}{product.openWidth || "-"}{product.openHeight || "-"}๋ฐฑ๋ฉดํ˜•
(120X70)
55380X180300SUS๋งˆ๊ฐ
- ๋“ฑ๋ก๋œ ์Šคํฌ๋ฆฐ ์ œํ’ˆ์ด ์—†์Šต๋‹ˆ๋‹ค -
{row.no}{row.type}{row.code}{row.openW.toLocaleString()}{row.openH.toLocaleString()}{row.madeW.toLocaleString()}{row.madeH.toLocaleString()}{row.guideRail}{row.shaft}{row.caseInch}{row.bracket}{row.capacity}{row.finish}
- {/* 2. ๋ชจํ„ฐ ํ…Œ์ด๋ธ” */} + {/* ========== 2. ์ฒ ์žฌ ========== */}
-

2. ๋ชจํ„ฐ

+

2. ์ฒ ์žฌ

- - - - - - - - + + + + + + + + + + + + + + + + + + - {(motorItems.length > 0 || bracketItems.length > 0) ? ( - <> - {/* ๋ชจํ„ฐ ํ–‰ */} - - - - - - - - - - - {/* ๋ธŒ๋ผ์ผ“ํŠธ ํ–‰ */} - - - - - - - - - - - {/* ๋ธŒ๋ผ์ผ“ํŠธ ์ถ”๊ฐ€ ํ–‰ (๋ฐ‘์นจํ†ต ์˜๊ธˆ) */} - - - - - - - - - - - - ) : ( - - + {MOCK_STEEL_ROWS.map((row) => ( + + + + + + + + + + + + + + - )} + ))}
ํ•ญ๋ชฉ๊ตฌ๋ถ„๊ทœ๊ฒฉ์ˆ˜๋Ÿ‰ํ•ญ๋ชฉ๊ตฌ๋ถ„๊ทœ๊ฒฉ์ˆ˜๋Ÿ‰No.๋ถ€ํ˜ธ์˜คํ”ˆ์‚ฌ์ด์ฆˆ์ œ์ž‘์‚ฌ์ด์ฆˆ๊ฐ€์ด๋“œ
๋ ˆ์ผ
์‚ฌํ”„ํŠธ
(์ธ์น˜)
์กฐ์ธํŠธ๋ฐ”
(๊ทœ๊ฒฉ)
์ผ€์ด์Šค
(์ธ์น˜)
๋ชจํ„ฐ๋งˆ๊ฐ
๊ฐ€๋กœ์„ธ๋กœ๊ฐ€๋กœ์„ธ๋กœ๋ธŒ๋ผ์ผ“ํŠธ์šฉ๋Ÿ‰Kg
๋ชจํ„ฐ(380V ๋‹จ์ƒ)๋ชจํ„ฐ ์šฉ๋Ÿ‰{motorItems[0]?.spec || "KD-150K"}{motorItems[0] ? formatQuantity(motorItems[0].quantity, motorItems[0].unit) : "6"}๋ชจํ„ฐ(380V ๋‹จ์ƒ)๋ชจํ„ฐ ์šฉ๋Ÿ‰{motorItems[1]?.spec || "KD-150K"}{motorItems[1] ? formatQuantity(motorItems[1].quantity, motorItems[1].unit) : "6"}
๋ธŒ๋ผ์ผ“ํŠธ๋ธŒ๋ผ์ผ“ํŠธ{bracketItems[0]?.spec || "380X180 [2-4\"]"}{bracketItems[0] ? formatQuantity(bracketItems[0].quantity, bracketItems[0].unit) : "6"}๋ธŒ๋ผ์ผ“ํŠธ๋ธŒ๋ผ์ผ“ํŠธ{bracketItems[1]?.spec || "380X180 [2-4\"]"}{bracketItems[1] ? formatQuantity(bracketItems[1].quantity, bracketItems[1].unit) : "6"}
๋ธŒ๋ผ์ผ“ํŠธ๋ฐ‘์นจํ†ต ์˜๊ธˆ{bracketItems[2]?.spec || "โˆ 40-40 L380"}{bracketItems[2] ? formatQuantity(bracketItems[2].quantity, bracketItems[2].unit) : "44"}
- ๋“ฑ๋ก๋œ ๋ชจํ„ฐ/๋ธŒ๋ผ์ผ“ ํ’ˆ๋ชฉ์ด ์—†์Šต๋‹ˆ๋‹ค -
{row.no}{row.code}{row.openW.toLocaleString()}{row.openH.toLocaleString()}{row.madeW.toLocaleString()}{row.madeH.toLocaleString()}{row.guideRail}{row.shaft}{row.jointBar}{row.caseInch}{row.bracket}{row.capacity}{row.finish}
- {/* 3. ์ ˆ๊ณก๋ฌผ */} + {/* ========== 3. ๋ชจํ„ฐ ========== */}
-

3. ์ ˆ๊ณก๋ฌผ

- - {/* 3-1. ๊ฐ€์ด๋“œ๋ ˆ์ผ */} -
-

3-1. ๊ฐ€์ด๋“œ๋ ˆ์ผ - EGI 1.5ST + ๋งˆ๊ฐ์žฌ EGI 1.1ST + ๋ณ„๋„๋งˆ๊ฐ์žฌ SUS 1.1ST

-
- - - - - - - - - - - - - {guideRailItems.length > 0 ? ( - <> - {/* 1ํ–‰: L: 3,000 / 22 */} - - - - - - - - - {/* 2ํ–‰: ํ•˜๋ถ€BASE */} - - - - - - - {/* 3ํ–‰: ๋นˆ ํ–‰ */} - - - - - - - {/* 4ํ–‰: ์ œํ’ˆ๋ช… */} - - - - - - - - ) : ( - - +

3. ๋ชจํ„ฐ

+
+
๋ฐฑ๋ฉดํ˜• (120X70)๊ธธ์ด์ˆ˜๋Ÿ‰์ธก๋ฉดํ˜• (120X120)๊ธธ์ด์ˆ˜๋Ÿ‰
-
- IMG -
-
L: 3,00022 -
- IMG -
-
L: 3,00022
ํ•˜๋ถ€BASE
[130X80]
22
์ œํ’ˆ๋ช…KSS01์ œํ’ˆ๋ช…KSS01
- ๋“ฑ๋ก๋œ ๊ฐ€์ด๋“œ๋ ˆ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค -
+ + + + + + + + + + + + + + {Array.from({ length: motorRows }).map((_, i) => { + const left = MOCK_MOTOR_LEFT[i]; + const right = MOCK_MOTOR_RIGHT[i]; + return ( + + + + + + + + + - )} - -
ํ•ญ๋ชฉ๊ตฌ๋ถ„๊ทœ๊ฒฉ์ˆ˜๋Ÿ‰ํ•ญ๋ชฉ๊ตฌ๋ถ„๊ทœ๊ฒฉ์ˆ˜๋Ÿ‰
{left?.item || ''}{left?.type || ''}{left?.spec || ''}{left?.qty ?? ''}{right?.item || ''}{right?.type || ''}{right?.spec || ''}{right?.qty ?? ''}
-
- - {/* ์—ฐ๊ธฐ์ฐจ๋‹จ์žฌ ์ •๋ณด */} -
- - - - - - - - - - - - - - - - -
-
์—ฐ๊ธฐ์ฐจ๋‹จ์žฌ(W50)
-
โ€ข ๊ฐ€์ด๋“œ๋ ˆ์ผ ๋งˆ๊ฐ์žฌ
-
์–‘์ธก์— ์„ค์น˜
-
-
EGI 0.8T +
-
ํ™”์ด๋ฒ„๊ธ€๋ผ์Šค์ฝ”ํŒ…์ง๋ฌผ
-
-
- IMG -
-
๊ทœ๊ฒฉ3,0004,000
์ˆ˜๋Ÿ‰441
-
- -
- โ€ข ๋ณ„๋„ ์ถ”๊ฐ€์‚ฌํ•ญ -
-
- - {/* 3-2. ์ผ€์ด์Šค(์…”ํ„ฐ๋ฐ•์Šค) */} -
-

3-2. ์ผ€์ด์Šค(์…”ํ„ฐ๋ฐ•์Šค) - EGI 1.5ST

-
- - - - - - - - - - - - - {caseItems.length > 0 ? ( - - - - - - - - - ) : ( - - - - )} - -
 ๊ทœ๊ฒฉ๊ธธ์ด์ˆ˜๋Ÿ‰์ธก๋ฉด๋ถ€์ˆ˜๋Ÿ‰
-
- IMG -
-
- 500X330
(150X300,
400K์›) -
- L: 4,000
L: 5,000
์ƒ๋ถ€๋ฎ๊ฐœ
(1219X389) -
- 3
4
55 -
500X35522
- ๋“ฑ๋ก๋œ ์ผ€์ด์Šค๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค -
-
- - {/* ์—ฐ๊ธฐ์ฐจ๋‹จ์žฌ ์ •๋ณด */} -
- - - - - - - - - - - - - - -
-
์—ฐ๊ธฐ์ฐจ๋‹จ์žฌ(W50)
-
โ€ข ํŒ๋„ฌ๋ถ€, ์ „๋ฉด๋ถ€
-
๊ฐ์‹ธ์— ์„ค์น˜
-
-
EGI 0.8T +
-
ํ™”์ด๋ฒ„๊ธ€๋ผ์Šค์ฝ”ํŒ…์ง๋ฌผ
-
-
- IMG -
-
๊ทœ๊ฒฉ3,000
์ˆ˜๋Ÿ‰44
-
-
- - {/* 3-3. ํ•˜๋‹จ๋งˆ๊ฐ์žฌ */} -
-

3-3. ํ•˜๋‹จ๋งˆ๊ฐ์žฌ - ํ•˜๋‹จ๋งˆ๊ฐ์žฌ(EGI 1.5ST) + ํ•˜๋‹จ๋ณด๊ฐ•์•จ๋น„(EGI 1.5ST) + ํ•˜๋‹จ ๋ณด๊ฐ•์ฒ (EGI 1.1ST) + ํ•˜๋‹จ ๋ฌด๊ฒŒํ˜• ์ฒ (50X12T)

-
- - - - - - - - - - - - - - - - {bottomFinishItems.length > 0 ? ( - - - - - - - - - - - - ) : ( - - - - )} - -
๊ตฌ์„ฑํ’ˆ๊ธธ์ด์ˆ˜๋Ÿ‰๊ตฌ์„ฑํ’ˆ๊ธธ์ด์ˆ˜๋Ÿ‰๊ตฌ์„ฑํ’ˆ๊ธธ์ด์ˆ˜๋Ÿ‰
ํ•˜๋‹จ๋งˆ๊ฐ์žฌ
(60X40)
L: 4,00011ํ•˜๋‹จ๋ณด๊ฐ•
(60X17)
L: 4,00011ํ•˜๋‹จ๋ฌด๊ฒŒ
[50X12T]
L: 4,00011
- ๋“ฑ๋ก๋œ ํ•˜๋‹จ๋งˆ๊ฐ์žฌ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค -
-
+ ); + })} + +
- {/* ํŠน์ด์‚ฌํ•ญ */} + {/* ========== 4. ์ ˆ๊ณก๋ฌผ ========== */} +
+

4. ์ ˆ๊ณก๋ฌผ

+ + {/* 4-1. ๊ฐ€์ด๋“œ๋ ˆ์ผ */} +
+

4-1. ๊ฐ€์ด๋“œ๋ ˆ์ผ - EGI 1.5ST + ๋งˆ๊ฐ์žฌ EGI 1.1ST + ๋ณ„๋„๋งˆ๊ฐ์žฌ SUS 1.1ST

+ +
+ + + + + + + + + + + {MOCK_GUIDE_RAIL_ITEMS.map((item, i) => ( + + {i === 0 && ( + + )} + + + + + ))} + +
๋ฐฑ๋ฉดํ˜• (120X70)ํ•ญ๋ชฉ๊ทœ๊ฒฉ์ˆ˜๋Ÿ‰
+
IMG
+
{item.name}{item.spec}{item.qty}
+
+ + {/* ์—ฐ๊ธฐ์ฐจ๋‹จ์žฌ */} +
+ + + + + + + + + + + + + + + + + +
 ํ•ญ๋ชฉ๊ทœ๊ฒฉ์ˆ˜๋Ÿ‰
+
IMG
+
{MOCK_GUIDE_SMOKE.name}{MOCK_GUIDE_SMOKE.spec}{MOCK_GUIDE_SMOKE.qty}
+
+ +

+ * ๊ฐ€์ด๋“œ๋ ˆ์ผ ๋งˆ๊ฐ์žฌ ์–‘์ธก์— ์„ค์น˜ - EGI 0.8T + ํ™”์ด๋ฒ„๊ธ€๋ผ์Šค์ฝ”ํŒ…์ง๋ฌผ +

+
+ + {/* 4-2. ์ผ€์ด์Šค(์…”ํ„ฐ๋ฐ•์Šค) */} +
+

4-2. ์ผ€์ด์Šค(์…”ํ„ฐ๋ฐ•์Šค) - EGI 1.5ST

+ +
+ + + + + + + + + + + {MOCK_CASE_ITEMS.map((item, i) => ( + + {i === 0 && ( + + )} + + + + + ))} + +
 ํ•ญ๋ชฉ๊ทœ๊ฒฉ์ˆ˜๋Ÿ‰
+
IMG
+
{item.name}{item.spec}{item.qty}
+
+ + {/* ์—ฐ๊ธฐ์ฐจ๋‹จ์žฌ */} +
+ + + + + + + + + + + + + + + + + +
 ํ•ญ๋ชฉ๊ทœ๊ฒฉ์ˆ˜๋Ÿ‰
+
IMG
+
{MOCK_CASE_SMOKE.name}{MOCK_CASE_SMOKE.spec}{MOCK_CASE_SMOKE.qty}
+
+ +

+ * ์ „๋ฉด๋ถ€, ํŒ๋„ฌ๋ถ€ ์–‘์ธก์— ์„ค์น˜ - EGI 0.8T + ํ™”์ด๋ฒ„๊ธ€๋ผ์Šค์ฝ”ํŒ…์ง๋ฌผ +

+
+ + {/* 4-3. ํ•˜๋‹จ๋งˆ๊ฐ์žฌ (ํ† ๊ธ€: ์Šคํฌ๋ฆฐ / ์ ˆ์žฌ) */} +
+
+ + +
+ + {bottomFinishView === 'screen' ? ( + <> +

+ 4-3. ํ•˜๋‹จ๋งˆ๊ฐ์žฌ - ํ•˜๋‹จ๋งˆ๊ฐ์žฌ(EGI 1.5ST) + ํ•˜๋‹จ๋ณด๊ฐ•์—˜๋น„(EGI 1.5ST) + ํ•˜๋‹จ ๋ณด๊ฐ•ํ‰์ฒ (EGI 1.1ST) + ํ•˜๋‹จ ๋ฌด๊ฒŒํ‰์ฒ (50X12T) +

+
+ + + + + + + + + + + + + + + {MOCK_BOTTOM_SCREEN.map((row, i) => ( + + + + + + + + + + + ))} + +
ํ•ญ๋ชฉ๊ทœ๊ฒฉ๊ธธ์ด์ˆ˜๋Ÿ‰ํ•ญ๋ชฉ๊ทœ๊ฒฉ๊ธธ์ด์ˆ˜๋Ÿ‰
{row.name}{row.spec}{row.l1}{row.q1}{row.name2}{row.spec2}{row.l2}{row.q2}
+
+ + ) : ( + <> +

+ 4-3. ํ•˜๋‹จ๋งˆ๊ฐ์žฌ -EGI 1.5ST +

+
+ + + + + + + + + + + + + + + + + +
ํ•˜๋‹จ๋งˆ๊ฐ์žฌ๊ทœ๊ฒฉ๊ธธ์ด์ˆ˜๋Ÿ‰
+
IMG
+
{MOCK_BOTTOM_STEEL.spec}{MOCK_BOTTOM_STEEL.length}{MOCK_BOTTOM_STEEL.qty}
+
+ + )} +
+
+ + {/* ========== 5. ๋ถ€์ž์žฌ ========== */} +
+

5. ๋ถ€์ž์žฌ

+
+ + + + + + + + + + + + + {MOCK_SUBSIDIARY.map((row, i) => ( + + + + + + + + + ))} + +
ํ•ญ๋ชฉ๊ทœ๊ฒฉ์ˆ˜๋Ÿ‰ํ•ญ๋ชฉ๊ทœ๊ฒฉ์ˆ˜๋Ÿ‰
{row.leftItem}{row.leftSpec}{row.leftQty}{row.rightItem}{row.rightSpec}{row.rightQty}
+
+
+ + {/* ========== ํŠน์ด์‚ฌํ•ญ ========== */} {remarks && (

ใ€ ํŠน์ด์‚ฌํ•ญ ใ€‘

@@ -611,4 +635,4 @@ export function SalesOrderDocument({ )}
); -} +} \ No newline at end of file diff --git a/src/components/outbound/ShipmentManagement/documents/ShipmentOrderDocument.tsx b/src/components/outbound/ShipmentManagement/documents/ShipmentOrderDocument.tsx index 449387a1..763c571b 100644 --- a/src/components/outbound/ShipmentManagement/documents/ShipmentOrderDocument.tsx +++ b/src/components/outbound/ShipmentManagement/documents/ShipmentOrderDocument.tsx @@ -1,11 +1,12 @@ 'use client'; /** - * ์ถœ๊ณ  ๋ฌธ์„œ ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ (์ˆ˜์ฃผ์„œ ๋ ˆ์ด์•„์›ƒ ๊ธฐ๋ฐ˜) - * - ์ถœ๊ณ ์ฆ, ๋‚ฉํ’ˆํ™•์ธ์„œ์—์„œ ์ œ๋ชฉ๋งŒ ๋ณ€๊ฒฝํ•˜์—ฌ ์‚ฌ์šฉ - * - ์ˆ˜์ฃผ์„œ(SalesOrderDocument)์™€ ๋™์ผํ•œ ๋ ˆ์ด์•„์›ƒ + * ์ถœ๊ณ  ๋ฌธ์„œ ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ (๊ธฐํš์„œ D1.8 ๊ธฐ์ค€ ๋ฆฌ๋””์ž์ธ) + * - ์ถœ๊ณ ์ฆ: showDispatchInfo + showLotColumn + * - ๋‚ฉํ’ˆํ™•์ธ์„œ: ๊ธฐ๋ณธ๊ฐ’ (๋ฐฐ์ฐจ์ •๋ณด ์—†์Œ, LOT ์ปฌ๋Ÿผ ์—†์Œ) */ +import { useState } from 'react'; import type { ShipmentDetail } from '../types'; import { DELIVERY_METHOD_LABELS } from '../types'; import { ConstructionApprovalTable } from '@/components/document-system'; @@ -14,56 +15,78 @@ interface ShipmentOrderDocumentProps { title: string; data: ShipmentDetail; showDispatchInfo?: boolean; + showLotColumn?: boolean; } -export function ShipmentOrderDocument({ title, data, showDispatchInfo = false }: ShipmentOrderDocumentProps) { - // ์Šคํฌ๋ฆฐ ์ œํ’ˆ ํ•„ํ„ฐ๋ง (productGroups ๊ธฐ๋ฐ˜) - const screenProducts = data.productGroups.filter(g => - g.productName?.includes('์Šคํฌ๋ฆฐ') || - g.productName?.includes('๋ฐฉํ™”') || - g.productName?.includes('์…”ํ„ฐ') - ); +// ===== ๋ฌธ์„œ ์ „์šฉ ๋ชฉ๋ฐ์ดํ„ฐ ===== - // ์ „์ฒด ๋ถ€ํ’ˆ ๋ชฉ๋ก - const allParts = [ - ...data.productGroups.flatMap(g => g.parts), - ...data.otherParts, - ]; +const MOCK_SCREEN_ROWS = [ + { no: 1, type: '์ด(๋งˆ)', code: 'FA123', openW: 4300, openH: 4300, madeW: 4300, madeH: 3000, guideRail: '๋ฐฑ๋ฉดํ˜•', shaft: 5, caseInch: 5, bracket: '500X300 380X180', capacity: 300, finish: 'SUS๋งˆ๊ฐ' }, + { no: 2, type: '์ด(๋งˆ)', code: 'FA123', openW: 4300, openH: 4300, madeW: 4300, madeH: 3000, guideRail: '๋ฐฑ๋ฉดํ˜•', shaft: 5, caseInch: 5, bracket: '500X300 380X180', capacity: 300, finish: 'SUS๋งˆ๊ฐ' }, +]; - // ๋ชจํ„ฐ ์•„์ดํ…œ ํ•„ํ„ฐ๋ง - const motorItems = allParts.filter(part => - part.itemName?.includes('๋ชจํ„ฐ') - ); +const MOCK_STEEL_ROWS = [ + { no: 1, code: 'FA123', openW: 4300, openH: 3000, madeW: 4300, madeH: 3000, guideRail: '๋ฐฑ๋ฉดํ˜•', shaft: 5, jointBar: 5, caseInch: 5, bracket: '500X300 380X180', capacity: 300, finish: 'SUS๋งˆ๊ฐ' }, + { no: 2, code: 'FA123', openW: 4300, openH: 3000, madeW: 4300, madeH: 3000, guideRail: '๋ฐฑ๋ฉดํ˜•', shaft: 5, jointBar: 5, caseInch: 5, bracket: '500X300 380X180', capacity: 300, finish: 'SUS๋งˆ๊ฐ' }, +]; - // ๋ธŒ๋ผ์ผ“ ์•„์ดํ…œ ํ•„ํ„ฐ๋ง - const bracketItems = allParts.filter(part => - part.itemName?.includes('๋ธŒ๋ผ์ผ“') - ); +const MOCK_MOTOR_LEFT = [ + { item: '๋ชจํ„ฐ', type: '380V ๋‹จ์ƒ', spec: 'KD-150K', qty: 6, lot: '123123' }, + { item: '๋ธŒ๋ผ์ผ“ํŠธ', type: '-', spec: '380X180', qty: 6, lot: '123123' }, + { item: '์•ต๊ธ€', type: '๋ฐ‘์นจํ†ต ์˜๊ธˆ', spec: '40*40*380', qty: 4, lot: '123123' }, +]; - // ๊ฐ€์ด๋“œ๋ ˆ์ผ ์•„์ดํ…œ ํ•„ํ„ฐ๋ง - const guideRailItems = allParts.filter(part => - part.itemName?.includes('๊ฐ€์ด๋“œ') || - part.itemName?.includes('๋ ˆ์ผ') - ); +const MOCK_MOTOR_RIGHT = [ + { item: '์ „๋™๊ฐœํ๊ธฐ', type: '๋ฆด๋ฐ•์Šค', spec: '-', qty: 1, lot: '123123' }, + { item: '์ „๋™๊ฐœํ๊ธฐ', type: '๋งค์ž…', spec: '-', qty: 1, lot: '123123' }, +]; - // ์ผ€์ด์Šค ์•„์ดํ…œ ํ•„ํ„ฐ๋ง - const caseItems = allParts.filter(part => - part.itemName?.includes('์ผ€์ด์Šค') || - part.itemName?.includes('์…”ํ„ฐ๋ฐ•์Šค') - ); +const MOCK_GUIDE_RAIL_ITEMS = [ + { name: 'ํ•ญ๋ชฉ๋ช…', spec: 'L: 3,000', qty: 22 }, + { name: 'ํ•˜๋ถ€BASE', spec: '130X80', qty: 22 }, +]; - // ํ•˜๋‹จ๋งˆ๊ฐ์žฌ ์•„์ดํ…œ ํ•„ํ„ฐ๋ง - const bottomFinishItems = allParts.filter(part => - part.itemName?.includes('ํ•˜๋‹จ') || - part.itemName?.includes('๋งˆ๊ฐ') - ); +const MOCK_GUIDE_SMOKE = { name: '์—ฐ๊ธฐ์ฐจ๋‹จ์žฌ(W50)', spec: '2,438', qty: 4 }; + +const MOCK_CASE_ITEMS = [ + { name: '500X330', spec: 'L: 4,000', qty: 3 }, + { name: '500X330', spec: 'L: 5,000', qty: 4 }, + { name: '์ƒ๋ถ€๋ฎ๊ฐœ', spec: '1219X389', qty: 55 }, + { name: '์ธก๋ฉด๋ถ€ (๋งˆ๊ตฌ๋ฆฌ)', spec: '500X355', qty: '500X355' }, +]; + +const MOCK_CASE_SMOKE = { name: '์—ฐ๊ธฐ์ฐจ๋‹จ์žฌ(W80)', spec: '3,000', qty: 4 }; + +const MOCK_BOTTOM_SCREEN = [ + { name: 'ํ•˜๋‹จ๋งˆ๊ฐ์žฌ', spec: '60X40', l1: 'L: 3,000', q1: 6, name2: 'ํ•˜๋‹จ๋งˆ๊ฐ์žฌ', spec2: '60X40', l2: 'L: 4,000', q2: 6 }, + { name: 'ํ•˜๋‹จ๋ณด๊ฐ•์—˜๋น„', spec: '60X17', l1: 'L: 3,000', q1: 6, name2: 'ํ•˜๋‹จ๋ณด๊ฐ•์—˜๋น„', spec2: '60X17', l2: 'L: 4,000', q2: 6 }, + { name: 'ํ•˜๋‹จ๋ณด๊ฐ•ํ‰์ฒ ', spec: '-', l1: 'L: 3,000', q1: 6, name2: 'ํ•˜๋‹จ๋ณด๊ฐ•ํ‰์ฒ ', spec2: '-', l2: 'L: 4,000', q2: 6 }, + { name: 'ํ•˜๋‹จ๋ฌด๊ฒŒํ‰์ฒ ', spec: '50X12T', l1: 'L: 3,000', q1: 6, name2: 'ํ•˜๋‹จ๋ฌด๊ฒŒํ‰์ฒ ', spec2: '50X12T', l2: 'L: 4,000', q2: 6 }, +]; + +const MOCK_BOTTOM_STEEL = { spec: '60X40', length: 'L: 3,000', qty: 22 }; + +const MOCK_SUBSIDIARY = [ + { leftItem: '๊ฐ๊ธฐ์‚ฌํ”„ํŠธ', leftSpec: '4์ธ์น˜ 4500', leftQty: 6, rightItem: '๊ฐํŒŒ์ดํ”„', rightSpec: '6000', rightQty: 4 }, + { leftItem: '์กฐ์ธํŠธ๋ฐ”', leftSpec: '300', leftQty: 6, rightItem: 'ํ™˜๋ด‰', rightSpec: '3000', rightQty: 5 }, +]; + +// ===== ๊ณตํ†ต ์Šคํƒ€์ผ ===== +const thBase = 'border-r border-gray-400 px-1 py-1'; +const tdBase = 'border-r border-gray-300 px-1 py-1'; +const tdCenter = `${tdBase} text-center`; +const imgPlaceholder = 'flex items-center justify-center border border-dashed border-gray-300 text-gray-400'; + +export function ShipmentOrderDocument({ title, data, showDispatchInfo = false, showLotColumn = false }: ShipmentOrderDocumentProps) { + const [bottomFinishView, setBottomFinishView] = useState<'screen' | 'steel'>('screen'); const deliveryMethodLabel = DELIVERY_METHOD_LABELS[data.deliveryMethod] || '-'; const fullAddress = [data.address, data.addressDetail].filter(Boolean).join(' ') || data.deliveryAddress || '-'; + const motorRows = Math.max(MOCK_MOTOR_LEFT.length, MOCK_MOTOR_RIGHT.length); return (
- {/* ํ—ค๋”: ์ œ๋ชฉ (์ขŒ์ธก) + ๊ฒฐ์žฌ๋ž€ (์šฐ์ธก) */} + {/* ========== ํ—ค๋”: ์ œ๋ชฉ + ๊ฒฐ์žฌ๋ž€ ========== */}

{title}

@@ -74,30 +97,26 @@ export function ShipmentOrderDocument({ title, data, showDispatchInfo = false }:
- - {/* ๊ฒฐ์žฌ๋ž€ (์šฐ์ธก) */} - +
- {/* ์ƒํ’ˆ๋ช… / ์ œํ’ˆ๋ช… / ๋กœํŠธ๋ฒˆํ˜ธ / ์ธ์ •๋ฒˆํ˜ธ */} + {/* ========== ๋กœํŠธ๋ฒˆํ˜ธ / ์ œํ’ˆ๋ช… / ์ œํ’ˆ์ฝ”๋“œ / ์ธ์ •๋ฒˆํ˜ธ ========== */} - - - - + + + + - +
์ƒํ’ˆ๋ช…{data.productGroups[0]?.productName || '-'}์ œํ’ˆ๋ช…{data.products[0]?.itemName || '-'} ๋กœํŠธ๋ฒˆํ˜ธ {data.lotNo}์ œํ’ˆ๋ช…{data.productGroups[0]?.productName || '-'}์ œํ’ˆ์ฝ”๋“œKWS01 ์ธ์ •๋ฒˆํ˜ธ-ABC1234
- {/* 3์—ด ์„น์…˜: ์‹ ์ฒญ์—…์ฒด | ์‹ ์ฒญ๋‚ด์šฉ | ๋‚ฉํ’ˆ์ •๋ณด */} + {/* ========== 3์—ด ์„น์…˜: ์‹ ์ฒญ์—…์ฒด | ์‹ ์ฒญ๋‚ด์šฉ | ๋‚ฉํ’ˆ์ •๋ณด ========== */}
{/* ์‹ ์ฒญ์—…์ฒด */} @@ -110,21 +129,17 @@ export function ShipmentOrderDocument({ title, data, showDispatchInfo = false }: {data.scheduledDate} - ์—…์ฒด๋ช… + ์ˆ˜์ฃผ์ฒ˜ {data.customerName} ์ˆ˜์ฃผ ๋‹ด๋‹น์ž {data.registrant || '-'} - + ๋‹ด๋‹น์ž ์—ฐ๋ฝ์ฒ˜ {data.driverContact || '-'} - - ๋ฐฐ์†ก์ง€ ์ฃผ์†Œ - {fullAddress} -
@@ -146,14 +161,10 @@ export function ShipmentOrderDocument({ title, data, showDispatchInfo = false }: ์ถœ๊ณ ์ผ {data.shipmentDate || data.scheduledDate} - + ์…”ํ„ฐ์ถœ์ˆ˜๋Ÿ‰ {data.productGroups.length}๊ฐœ์†Œ - -   -   -
@@ -175,21 +186,22 @@ export function ShipmentOrderDocument({ title, data, showDispatchInfo = false }: ์ธ์ˆ˜์ž์—ฐ๋ฝ์ฒ˜ {data.receiverContact || '-'} - + ๋ฐฐ์†ก๋ฐฉ๋ฒ• {deliveryMethodLabel} - -   -   -
+ {/* ๋ฐฐ์†ก์ง€ ์ฃผ์†Œ - ํ•œ ์ค„ ๋ณ‘ํ•ฉ */} +
+
๋ฐฐ์†ก์ง€ ์ฃผ์†Œ
+
{fullAddress}
+
- {/* ๋ฐฐ์ฐจ์ •๋ณด (์ถœ๊ณ ์ฆ์—์„œ๋งŒ ํ‘œ์‹œ) */} + {/* ========== ๋ฐฐ์ฐจ์ •๋ณด (์ถœ๊ณ ์ฆ์—์„œ๋งŒ) ========== */} {showDispatchInfo && (() => { const dispatch = data.vehicleDispatches[0]; return ( @@ -221,366 +233,406 @@ export function ShipmentOrderDocument({ title, data, showDispatchInfo = false }: ); })()} -

์•„๋ž˜์™€ ๊ฐ™์ด ์ฃผ๋ฌธํ•˜์˜ค๋‹ˆ ํ’ˆ์งˆ ๋ฐ ๋‚ฉ๊ธฐ์ผ์„ ์ค€์ˆ˜ํ•˜์—ฌ ์ฃผ์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.

+ {/* ========== ์ž์žฌ ๋ฐ ์ฒ ๊ฑฐ ๋‚ด์—ญ ํ—ค๋” ========== */} +
+ ์ž์žฌ ๋ฐ ์ฒ ๊ฑฐ ๋‚ด์—ญ +
- {/* 1. ์Šคํฌ๋ฆฐ ํ…Œ์ด๋ธ” */} + {/* ========== 1. ์Šคํฌ๋ฆฐ ========== */}

1. ์Šคํฌ๋ฆฐ

- - - - - - - - - + + + + + + + + + - - - - - - + + + + + + - {screenProducts.length > 0 ? ( - screenProducts.map((group, index) => { - // specification์—์„œ ๊ฐ€๋กœx์„ธ๋กœ ํŒŒ์‹ฑ (์˜ˆ: "2000 x 2500 mm") - const specMatch = group.specification?.match(/(\d+)\s*[xXร—]\s*(\d+)/); - const width = specMatch ? specMatch[1] : '-'; - const height = specMatch ? specMatch[2] : '-'; - - return ( - - - - - - - - - - - - - - - - ); - }) - ) : ( - - + {MOCK_SCREEN_ROWS.map((row) => ( + + + + + + + + + + + + + + - )} + ))}
Noํ’ˆ๋ฅ˜๋ถ€ํ˜ธ์˜คํ”ˆ์‚ฌ์ด์ฆˆ์ œ์ž‘์‚ฌ์ด์ฆˆ๊ฐ€์ด๋“œ๋ ˆ์ผ
์œ ํ˜•
์ƒคํ”„ํŠธ
(์ธ์น˜)
์ผ€์ด์Šค
(์ธ์น˜)
๋ชจํ„ฐNoํ’ˆ๋ฅ˜๋ถ€ํ˜ธ์˜คํ”ˆ์‚ฌ์ด์ฆˆ์ œ์ž‘์‚ฌ์ด์ฆˆ๊ฐ€์ด๋“œ
๋ ˆ์ผ
์‚ฌํ”„ํŠธ
(์ธ์น˜)
์ผ€์ด์Šค
(์ธ์น˜)
๋ชจํ„ฐ ๋งˆ๊ฐ
๊ฐ€๋กœ์„ธ๋กœ๊ฐ€๋กœ์„ธ๋กœ๋ธŒ๋ผ์ผ“ํŠธ์šฉ๋Ÿ‰Kg๊ฐ€๋กœ์„ธ๋กœ๊ฐ€๋กœ์„ธ๋กœ๋ธŒ๋ผ์ผ“ํŠธ์šฉ๋Ÿ‰Kg
{index + 1}{group.productName || '-'}-{width}{height}{width}{height}๋ฐฑ๋ฉดํ˜•
(120X70)
55380X180300SUS๋งˆ๊ฐ
- ๋“ฑ๋ก๋œ ์Šคํฌ๋ฆฐ ์ œํ’ˆ์ด ์—†์Šต๋‹ˆ๋‹ค -
{row.no}{row.type}{row.code}{row.openW.toLocaleString()}{row.openH.toLocaleString()}{row.madeW.toLocaleString()}{row.madeH.toLocaleString()}{row.guideRail}{row.shaft}{row.caseInch}{row.bracket}{row.capacity}{row.finish}
- {/* 2. ๋ชจํ„ฐ ํ…Œ์ด๋ธ” */} + {/* ========== 2. ์ ˆ์žฌ ========== */}
-

2. ๋ชจํ„ฐ

+

2. ์ ˆ์žฌ

- - - - - - - - + + + + + + + + + + + + + + + + + + - {(motorItems.length > 0 || bracketItems.length > 0) ? ( - <> - {/* ๋ชจํ„ฐ ํ–‰ */} - - - - - - - - - - - {/* ๋ธŒ๋ผ์ผ“ํŠธ ํ–‰ */} - - - - - - - - - - - {/* ๋ธŒ๋ผ์ผ“ํŠธ ์ถ”๊ฐ€ ํ–‰ */} - - - - - - - - - - - - ) : ( - - + {MOCK_STEEL_ROWS.map((row) => ( + + + + + + + + + + + + + + - )} + ))}
ํ•ญ๋ชฉ๊ตฌ๋ถ„๊ทœ๊ฒฉ์ˆ˜๋Ÿ‰ํ•ญ๋ชฉ๊ตฌ๋ถ„๊ทœ๊ฒฉ์ˆ˜๋Ÿ‰No.๋ถ€ํ˜ธ์˜คํ”ˆ์‚ฌ์ด์ฆˆ์ œ์ž‘์‚ฌ์ด์ฆˆ๊ฐ€์ด๋“œ
๋ ˆ์ผ
์‚ฌํ”„ํŠธ
(์ธ์น˜)
์กฐ์ธํŠธ๋ฐ”
(๊ทœ๊ฒฉ)
์ผ€์ด์Šค
(์ธ์น˜)
๋ชจํ„ฐ๋งˆ๊ฐ
๊ฐ€๋กœ์„ธ๋กœ๊ฐ€๋กœ์„ธ๋กœ๋ธŒ๋ผ์ผ“ํŠธ์šฉ๋Ÿ‰Kg
๋ชจํ„ฐ(380V ๋‹จ์ƒ)๋ชจํ„ฐ ์šฉ๋Ÿ‰{motorItems[0]?.specification || 'KD-150K'}{motorItems[0]?.quantity ?? '-'}๋ชจํ„ฐ(380V ๋‹จ์ƒ)๋ชจํ„ฐ ์šฉ๋Ÿ‰{motorItems[1]?.specification || 'KD-150K'}{motorItems[1]?.quantity ?? '-'}
๋ธŒ๋ผ์ผ“ํŠธ๋ธŒ๋ผ์ผ“ํŠธ{bracketItems[0]?.specification || '380X180 [2-4"]'}{bracketItems[0]?.quantity ?? '-'}๋ธŒ๋ผ์ผ“ํŠธ๋ธŒ๋ผ์ผ“ํŠธ{bracketItems[1]?.specification || '380X180 [2-4"]'}{bracketItems[1]?.quantity ?? '-'}
๋ธŒ๋ผ์ผ“ํŠธ๋ฐ‘์นจํ†ต ์˜๊ธˆ{bracketItems[2]?.specification || 'โˆ 40-40 L380'}{bracketItems[2]?.quantity ?? '-'}
- ๋“ฑ๋ก๋œ ๋ชจํ„ฐ/๋ธŒ๋ผ์ผ“ ํ’ˆ๋ชฉ์ด ์—†์Šต๋‹ˆ๋‹ค -
{row.no}{row.code}{row.openW.toLocaleString()}{row.openH.toLocaleString()}{row.madeW.toLocaleString()}{row.madeH.toLocaleString()}{row.guideRail}{row.shaft}{row.jointBar}{row.caseInch}{row.bracket}{row.capacity}{row.finish}
- {/* 3. ์ ˆ๊ณก๋ฌผ */} + {/* ========== 3. ๋ชจํ„ฐ ========== */}
-

3. ์ ˆ๊ณก๋ฌผ

- - {/* 3-1. ๊ฐ€์ด๋“œ๋ ˆ์ผ */} -
-

3-1. ๊ฐ€์ด๋“œ๋ ˆ์ผ - EGI 1.5ST + ๋งˆ๊ฐ์žฌ EGI 1.1ST + ๋ณ„๋„๋งˆ๊ฐ์žฌ SUS 1.1ST

-
- - - - - - - - - - - - - {guideRailItems.length > 0 ? ( - <> - - - - - - - - - - - - - - - - - - - - - - - - - - - - ) : ( - - +

3. ๋ชจํ„ฐ

+
+
๋ฐฑ๋ฉดํ˜• (120X70)๊ธธ์ด์ˆ˜๋Ÿ‰์ธก๋ฉดํ˜• (120X120)๊ธธ์ด์ˆ˜๋Ÿ‰
-
- IMG -
-
L: 3,000{guideRailItems[0]?.quantity ?? 22} -
- IMG -
-
L: 3,000{guideRailItems[1]?.quantity ?? 22}
ํ•˜๋ถ€BASE
[130X80]
22
์ œํ’ˆ๋ช…KSS01์ œํ’ˆ๋ช…KSS01
- ๋“ฑ๋ก๋œ ๊ฐ€์ด๋“œ๋ ˆ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค -
+ + + + + + + {showLotColumn && } + + + + + {showLotColumn && } + + + + {Array.from({ length: motorRows }).map((_, i) => { + const left = MOCK_MOTOR_LEFT[i]; + const right = MOCK_MOTOR_RIGHT[i]; + return ( + + + + + + {showLotColumn && } + + + + + {showLotColumn && } - )} - -
ํ•ญ๋ชฉ๊ตฌ๋ถ„๊ทœ๊ฒฉ์ˆ˜๋Ÿ‰์ž…๊ณ  LOTํ•ญ๋ชฉ๊ตฌ๋ถ„๊ทœ๊ฒฉ์ˆ˜๋Ÿ‰์ž…๊ณ  LOT
{left?.item || ''}{left?.type || ''}{left?.spec || ''}{left?.qty ?? ''}{left?.lot || ''}{right?.item || ''}{right?.type || ''}{right?.spec || ''}{right?.qty ?? ''}{right?.lot || ''}
-
- - {/* ์—ฐ๊ธฐ์ฐจ๋‹จ์žฌ ์ •๋ณด */} -
- - - - - - - - - - - - - - - - -
-
์—ฐ๊ธฐ์ฐจ๋‹จ์žฌ(W50)
-
โ€ข ๊ฐ€์ด๋“œ๋ ˆ์ผ ๋งˆ๊ฐ์žฌ
-
์–‘์ธก์— ์„ค์น˜
-
-
EGI 0.8T +
-
ํ™”์ด๋ฒ„๊ธ€๋ผ์Šค์ฝ”ํŒ…์ง๋ฌผ
-
-
- IMG -
-
๊ทœ๊ฒฉ3,0004,000
์ˆ˜๋Ÿ‰441
-
- -
- โ€ข ๋ณ„๋„ ์ถ”๊ฐ€์‚ฌํ•ญ -
-
- - {/* 3-2. ์ผ€์ด์Šค(์…”ํ„ฐ๋ฐ•์Šค) */} -
-

3-2. ์ผ€์ด์Šค(์…”ํ„ฐ๋ฐ•์Šค) - EGI 1.5ST

-
- - - - - - - - - - - - - {caseItems.length > 0 ? ( - - - - - - - - - ) : ( - - - - )} - -
 ๊ทœ๊ฒฉ๊ธธ์ด์ˆ˜๋Ÿ‰์ธก๋ฉด๋ถ€์ˆ˜๋Ÿ‰
-
- IMG -
-
- 500X330
(150X300,
400K์›) -
- L: 4,000
L: 5,000
์ƒ๋ถ€๋ฎ๊ฐœ
(1219X389) -
- 3
4
55 -
500X35522
- ๋“ฑ๋ก๋œ ์ผ€์ด์Šค๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค -
-
- - {/* ์—ฐ๊ธฐ์ฐจ๋‹จ์žฌ ์ •๋ณด */} -
- - - - - - - - - - - - - - -
-
์—ฐ๊ธฐ์ฐจ๋‹จ์žฌ(W50)
-
โ€ข ํŒ๋„ฌ๋ถ€, ์ „๋ฉด๋ถ€
-
๊ฐ์‹ธ์— ์„ค์น˜
-
-
EGI 0.8T +
-
ํ™”์ด๋ฒ„๊ธ€๋ผ์Šค์ฝ”ํŒ…์ง๋ฌผ
-
-
- IMG -
-
๊ทœ๊ฒฉ3,000
์ˆ˜๋Ÿ‰44
-
-
- - {/* 3-3. ํ•˜๋‹จ๋งˆ๊ฐ์žฌ */} -
-

3-3. ํ•˜๋‹จ๋งˆ๊ฐ์žฌ - ํ•˜๋‹จ๋งˆ๊ฐ์žฌ(EGI 1.5ST) + ํ•˜๋‹จ๋ณด๊ฐ•์•จ๋น„(EGI 1.5ST) + ํ•˜๋‹จ ๋ณด๊ฐ•์ฒ (EGI 1.1ST) + ํ•˜๋‹จ ๋ฌด๊ฒŒํ˜• ์ฒ (50X12T)

-
- - - - - - - - - - - - - - - - {bottomFinishItems.length > 0 ? ( - - - - - - - - - - - - ) : ( - - - - )} - -
๊ตฌ์„ฑํ’ˆ๊ธธ์ด์ˆ˜๋Ÿ‰๊ตฌ์„ฑํ’ˆ๊ธธ์ด์ˆ˜๋Ÿ‰๊ตฌ์„ฑํ’ˆ๊ธธ์ด์ˆ˜๋Ÿ‰
ํ•˜๋‹จ๋งˆ๊ฐ์žฌ
(60X40)
L: 4,00011ํ•˜๋‹จ๋ณด๊ฐ•
(60X17)
L: 4,00011ํ•˜๋‹จ๋ฌด๊ฒŒ
[50X12T]
L: 4,00011
- ๋“ฑ๋ก๋œ ํ•˜๋‹จ๋งˆ๊ฐ์žฌ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค -
-
+ ); + })} + +
- {/* ํŠน์ด์‚ฌํ•ญ */} + {/* ========== 4. ์ ˆ๊ณก๋ฌผ ========== */} +
+

4. ์ ˆ๊ณก๋ฌผ

+ + {/* 4-1. ๊ฐ€์ด๋“œ๋ ˆ์ผ */} +
+

4-1. ๊ฐ€์ด๋“œ๋ ˆ์ผ - EGI 1.5ST + ๋งˆ๊ฐ์žฌ EGI 1.1ST + ๋ณ„๋„๋งˆ๊ฐ์žฌ SUS 1.1ST

+ + {/* ๋ฉ”์ธ ํ…Œ์ด๋ธ” */} +
+ + + + + + + + + + + {MOCK_GUIDE_RAIL_ITEMS.map((item, i) => ( + + {i === 0 && ( + + )} + + + + + ))} + +
๋ฐฑ๋ฉดํ˜• (120X70)ํ•ญ๋ชฉ๊ทœ๊ฒฉ์ˆ˜๋Ÿ‰
+
IMG
+
{item.name}{item.spec}{item.qty}
+
+ + {/* ์—ฐ๊ธฐ์ฐจ๋‹จ์žฌ */} +
+ + + + + + + + + + + + + + + + + +
 ํ•ญ๋ชฉ๊ทœ๊ฒฉ์ˆ˜๋Ÿ‰
+
IMG
+
{MOCK_GUIDE_SMOKE.name}{MOCK_GUIDE_SMOKE.spec}{MOCK_GUIDE_SMOKE.qty}
+
+ +

+ * ๊ฐ€์ด๋“œ๋ ˆ์ผ ๋งˆ๊ฐ์žฌ ์–‘์ธก์— ์„ค์น˜ - EGI 0.8T + ํ™”์ด๋ฒ„๊ธ€๋ผ์Šค์ฝ”ํŒ…์ง๋ฌผ +

+
+ + {/* 4-2. ์ผ€์ด์Šค(์…”ํ„ฐ๋ฐ•์Šค) */} +
+

4-2. ์ผ€์ด์Šค(์…”ํ„ฐ๋ฐ•์Šค) - EGI 1.5ST

+ + {/* ๋ฉ”์ธ ํ…Œ์ด๋ธ” */} +
+ + + + + + + + + + + {MOCK_CASE_ITEMS.map((item, i) => ( + + {i === 0 && ( + + )} + + + + + ))} + +
 ํ•ญ๋ชฉ๊ทœ๊ฒฉ์ˆ˜๋Ÿ‰
+
IMG
+
{item.name}{item.spec}{item.qty}
+
+ + {/* ์—ฐ๊ธฐ์ฐจ๋‹จ์žฌ */} +
+ + + + + + + + + + + + + + + + + +
 ํ•ญ๋ชฉ๊ทœ๊ฒฉ์ˆ˜๋Ÿ‰
+
IMG
+
{MOCK_CASE_SMOKE.name}{MOCK_CASE_SMOKE.spec}{MOCK_CASE_SMOKE.qty}
+
+ +

+ * ์ „๋ฉด๋ถ€, ํŒ๋„ฌ๋ถ€ ์–‘์ธก์— ์„ค์น˜ - EGI 0.8T + ํ™”์ด๋ฒ„๊ธ€๋ผ์Šค์ฝ”ํŒ…์ง๋ฌผ +

+
+ + {/* 4-3. ํ•˜๋‹จ๋งˆ๊ฐ์žฌ (ํ† ๊ธ€: ์Šคํฌ๋ฆฐ / ์ ˆ์žฌ) */} +
+ {/* ํ† ๊ธ€ ๋ฒ„ํŠผ */} +
+ + +
+ + {bottomFinishView === 'screen' ? ( + <> +

+ 4-3. ํ•˜๋‹จ๋งˆ๊ฐ์žฌ - ํ•˜๋‹จ๋งˆ๊ฐ์žฌ(EGI 1.5ST) + ํ•˜๋‹จ๋ณด๊ฐ•์—˜๋น„(EGI 1.5ST) + ํ•˜๋‹จ ๋ณด๊ฐ•ํ‰์ฒ (EGI 1.1ST) + ํ•˜๋‹จ ๋ฌด๊ฒŒํ‰์ฒ (50X12T) +

+
+ + + + + + + + + + + + + + + {MOCK_BOTTOM_SCREEN.map((row, i) => ( + + + + + + + + + + + ))} + +
ํ•ญ๋ชฉ๊ทœ๊ฒฉ๊ธธ์ด์ˆ˜๋Ÿ‰ํ•ญ๋ชฉ๊ทœ๊ฒฉ๊ธธ์ด์ˆ˜๋Ÿ‰
{row.name}{row.spec}{row.l1}{row.q1}{row.name2}{row.spec2}{row.l2}{row.q2}
+
+ + ) : ( + <> +

+ 4-3. ํ•˜๋‹จ๋งˆ๊ฐ์žฌ -EGI 1.5ST +

+
+ + + + + + + + + + + + + + + + + +
ํ•˜๋‹จ๋งˆ๊ฐ์žฌ๊ทœ๊ฒฉ๊ธธ์ด์ˆ˜๋Ÿ‰
+
IMG
+
{MOCK_BOTTOM_STEEL.spec}{MOCK_BOTTOM_STEEL.length}{MOCK_BOTTOM_STEEL.qty}
+
+ + )} +
+
+ + {/* ========== 5. ๋ถ€์ž์žฌ ========== */} +
+

5. ๋ถ€์ž์žฌ

+
+ + + + + + + + + + + + + {MOCK_SUBSIDIARY.map((row, i) => ( + + + + + + + + + ))} + +
ํ•ญ๋ชฉ๊ทœ๊ฒฉ์ˆ˜๋Ÿ‰ํ•ญ๋ชฉ๊ทœ๊ฒฉ์ˆ˜๋Ÿ‰
{row.leftItem}{row.leftSpec}{row.leftQty}{row.rightItem}{row.rightSpec}{row.rightQty}
+
+
+ + {/* ========== ํŠน์ด์‚ฌํ•ญ ========== */} {data.remarks && (

ใ€ ํŠน์ด์‚ฌํ•ญ ใ€‘

diff --git a/src/components/outbound/ShipmentManagement/documents/ShippingSlip.tsx b/src/components/outbound/ShipmentManagement/documents/ShippingSlip.tsx index b25b7bcc..f76d7e80 100644 --- a/src/components/outbound/ShipmentManagement/documents/ShippingSlip.tsx +++ b/src/components/outbound/ShipmentManagement/documents/ShippingSlip.tsx @@ -13,5 +13,5 @@ interface ShippingSlipProps { } export function ShippingSlip({ data }: ShippingSlipProps) { - return ; + return ; } diff --git a/src/components/process-management/ProcessDetail.tsx b/src/components/process-management/ProcessDetail.tsx index a3f77d1e..857368f6 100644 --- a/src/components/process-management/ProcessDetail.tsx +++ b/src/components/process-management/ProcessDetail.tsx @@ -11,7 +11,7 @@ import { useState, useEffect, useCallback, useRef } from 'react'; import { useRouter } from 'next/navigation'; -import { ArrowLeft, Edit, GripVertical, Plus, Package } from 'lucide-react'; +import { ArrowLeft, Edit, GripVertical, Plus, Package, Trash2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; @@ -19,7 +19,9 @@ import { PageLayout } from '@/components/organisms/PageLayout'; import { PageHeader } from '@/components/organisms/PageHeader'; import { useMenuStore } from '@/store/menuStore'; import { usePermission } from '@/hooks/usePermission'; -import { getProcessSteps, reorderProcessSteps } from './actions'; +import { toast } from 'sonner'; +import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; +import { getProcessSteps, reorderProcessSteps, deleteProcess } from './actions'; import type { Process, ProcessStep } from '@/types/process'; interface ProcessDetailProps { @@ -35,6 +37,10 @@ export function ProcessDetail({ process }: ProcessDetailProps) { const [steps, setSteps] = useState([]); const [isStepsLoading, setIsStepsLoading] = useState(true); + // ์‚ญ์ œ ๋‹ค์ด์–ผ๋กœ๊ทธ ์ƒํƒœ + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + // ๋“œ๋ž˜๊ทธ ์ƒํƒœ const [dragIndex, setDragIndex] = useState(null); const [dragOverIndex, setDragOverIndex] = useState(null); @@ -75,6 +81,24 @@ export function ProcessDetail({ process }: ProcessDetailProps) { router.push(`/ko/master-data/process-management/${process.id}/steps/${stepId}`); }; + const handleDelete = async () => { + setIsDeleting(true); + try { + const result = await deleteProcess(process.id); + if (result.success) { + toast.success('๊ณต์ •์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'); + router.push('/ko/master-data/process-management'); + } else { + toast.error(result.error || '์‚ญ์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); + } + } catch { + toast.error('์‚ญ์ œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.'); + } finally { + setIsDeleting(false); + setDeleteDialogOpen(false); + } + }; + // ===== ๋“œ๋ž˜๊ทธ&๋“œ๋กญ (HTML5 ๋„ค์ดํ‹ฐ๋ธŒ) ===== const handleDragStart = useCallback((e: React.DragEvent, index: number) => { setDragIndex(index); @@ -343,12 +367,27 @@ export function ProcessDetail({ process }: ProcessDetailProps) { ๋ชฉ๋ก์œผ๋กœ {canUpdate && ( - +
+ + +
)}
+ + {/* ์‚ญ์ œ ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ */} + ); } diff --git a/src/components/process-management/ProcessListClient.tsx b/src/components/process-management/ProcessListClient.tsx index eada0796..16c43380 100644 --- a/src/components/process-management/ProcessListClient.tsx +++ b/src/components/process-management/ProcessListClient.tsx @@ -9,10 +9,11 @@ * - ์‚ญ์ œ ๋‹ค์ด์–ผ๋กœ๊ทธ */ -import { useState, useMemo, useCallback, useEffect } from 'react'; +import { useState, useMemo, useCallback, useEffect, useRef } from 'react'; import { useRouter } from 'next/navigation'; -import { Wrench, Plus } from 'lucide-react'; -import { TableCell, TableRow } from '@/components/ui/table'; +import { Wrench, Plus, GripVertical } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { TableCell, TableRow, TableHead } from '@/components/ui/table'; import { Checkbox } from '@/components/ui/checkbox'; import { Badge } from '@/components/ui/badge'; import { @@ -26,7 +27,7 @@ import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard'; import { toast } from 'sonner'; import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import type { Process } from '@/types/process'; -import { getProcessList, deleteProcess, deleteProcesses, toggleProcessActive, getProcessStats } from './actions'; +import { getProcessList, deleteProcess, deleteProcesses, toggleProcessActive, getProcessStats, reorderProcesses } from './actions'; interface ProcessListClientProps { initialData?: Process[]; @@ -50,6 +51,13 @@ export default function ProcessListClient({ initialData = [], initialStats }: Pr // ๊ฒ€์ƒ‰์–ด ์ƒํƒœ const [searchQuery, setSearchQuery] = useState(''); + // ๋“œ๋ž˜๊ทธ&๋“œ๋กญ ์ˆœ์„œ ๋ณ€๊ฒฝ ์ƒํƒœ + const [isOrderChanged, setIsOrderChanged] = useState(false); + const dragProcessIdRef = useRef(null); + const dragNodeRef = useRef(null); + const allProcessesRef = useRef(allProcesses); + allProcessesRef.current = allProcesses; + // ===== ๋ฐ์ดํ„ฐ ๋กœ๋“œ ===== const loadData = useCallback(async () => { setIsLoading(true); @@ -177,6 +185,78 @@ export default function ProcessListClient({ initialData = [], initialStats }: Pr } }, [allProcesses]); + // ===== ๋“œ๋ž˜๊ทธ&๋“œ๋กญ ์ˆœ์„œ ๋ณ€๊ฒฝ ===== + const handleDragStart = useCallback((e: React.DragEvent, processId: string) => { + dragProcessIdRef.current = processId; + dragNodeRef.current = e.currentTarget; + e.dataTransfer.effectAllowed = 'move'; + requestAnimationFrame(() => { + if (dragNodeRef.current) { + dragNodeRef.current.style.opacity = '0.4'; + } + }); + }, []); + + const handleDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; + e.currentTarget.classList.add('border-t-2', 'border-t-primary'); + }, []); + + const handleDragLeave = useCallback((e: React.DragEvent) => { + const related = e.relatedTarget as Node; + if (!e.currentTarget.contains(related)) { + e.currentTarget.classList.remove('border-t-2', 'border-t-primary'); + } + }, []); + + const handleDragEnd = useCallback(() => { + if (dragNodeRef.current) { + dragNodeRef.current.style.opacity = '1'; + } + dragProcessIdRef.current = null; + dragNodeRef.current = null; + }, []); + + const handleProcessDrop = useCallback((e: React.DragEvent, dropProcessId: string) => { + e.preventDefault(); + e.currentTarget.classList.remove('border-t-2', 'border-t-primary'); + const dragId = dragProcessIdRef.current; + if (!dragId || dragId === dropProcessId) { + handleDragEnd(); + return; + } + setAllProcesses((prev) => { + const updated = [...prev]; + const dragIdx = updated.findIndex(p => p.id === dragId); + const dropIdx = updated.findIndex(p => p.id === dropProcessId); + if (dragIdx === -1 || dropIdx === -1) return prev; + const [moved] = updated.splice(dragIdx, 1); + updated.splice(dropIdx, 0, moved); + return updated; + }); + setIsOrderChanged(true); + handleDragEnd(); + }, [handleDragEnd]); + + const handleSaveOrder = useCallback(async () => { + setIsLoading(true); + try { + const orderData = allProcessesRef.current.map((p, idx) => ({ id: p.id, order: idx + 1 })); + const result = await reorderProcesses(orderData); + if (result.success) { + toast.success('์ˆœ์„œ๊ฐ€ ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'); + setIsOrderChanged(false); + } else { + toast.error(result.error || '์ˆœ์„œ ์ €์žฅ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); + } + } catch { + toast.error('์ˆœ์„œ ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.'); + } finally { + setIsLoading(false); + } + }, []); + // ===== UniversalListPage Config ===== const config: UniversalListConfig = useMemo( () => ({ @@ -230,8 +310,11 @@ export default function ProcessListClient({ initialData = [], initialStats }: Pr }, }, - // ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ + // ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ (showCheckbox: false + renderCustomTableHeader๋กœ ์ˆ˜๋™ ๊ด€๋ฆฌ) + showCheckbox: false, columns: [ + { key: 'drag', label: '', className: 'w-[40px]' }, + { key: 'checkbox', label: '', className: 'w-[50px]' }, { key: 'no', label: '๋ฒˆํ˜ธ', className: 'w-[60px] text-center' }, { key: 'processCode', label: '๊ณต์ •๋ฒˆํ˜ธ', className: 'w-[120px]' }, { key: 'processName', label: '๊ณต์ •๋ช…', className: 'min-w-[200px]' }, @@ -240,6 +323,25 @@ export default function ProcessListClient({ initialData = [], initialStats }: Pr { key: 'status', label: '์ƒํƒœ', className: 'w-[80px] text-center' }, ], + // ์ปค์Šคํ…€ ํ…Œ์ด๋ธ” ํ—ค๋” (๋“œ๋ž˜๊ทธ โ†’ ์ „์ฒด์„ ํƒ ์ฒดํฌ๋ฐ•์Šค โ†’ ๋ฒˆํ˜ธ โ†’ ๋ฐ์ดํ„ฐ ์ˆœ) + renderCustomTableHeader: ({ displayData, selectedItems, onToggleSelectAll }) => ( + <> + + + 0 && selectedItems.size === displayData.length} + onCheckedChange={onToggleSelectAll} + /> + + ๋ฒˆํ˜ธ + ๊ณต์ •๋ฒˆํ˜ธ + ๊ณต์ •๋ช… + ๋‹ด๋‹น๋ถ€์„œ + ํ’ˆ๋ชฉ + ์ƒํƒœ + + ), + // ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ํ•„ํ„ฐ๋ง clientSideFiltering: true, itemsPerPage: 20, @@ -292,6 +394,13 @@ export default function ProcessListClient({ initialData = [], initialStats }: Pr ); }, + // ์ˆœ์„œ ๋ณ€๊ฒฝ ์ €์žฅ ๋ฒ„ํŠผ + headerActions: () => ( + + ), + // ๋“ฑ๋ก ๋ฒ„ํŠผ (๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ์—์„œ ์˜ค๋ฅธ์ชฝ์— ๋ Œ๋”๋ง) createButton: { label: '๊ณต์ • ๋“ฑ๋ก', @@ -315,9 +424,21 @@ export default function ProcessListClient({ initialData = [], initialStats }: Pr return ( handleDragStart(e, process.id)} + onDragOver={handleDragOver} + onDragLeave={handleDragLeave} + onDragEnd={handleDragEnd} + onDrop={(e) => handleProcessDrop(e, process.id)} className={`cursor-pointer hover:bg-muted/50 ${handlers.isSelected ? 'bg-blue-50' : ''}`} onClick={() => handleRowClick(process)} > + e.stopPropagation()} + > + + e.stopPropagation()}> { + const result = await executeServerAction({ + url: `${API_URL}/api/v1/processes/reorder`, + method: 'PATCH', + body: { + items: processes.map((p) => ({ + id: parseInt(p.id, 10), + sort_order: p.order, + })), + }, + errorMessage: '๊ณต์ • ์ˆœ์„œ ๋ณ€๊ฒฝ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.', + }); + if (result.__authError) return { success: false, __authError: true }; + return { success: result.success, error: result.error }; +} + /** * ๊ณต์ • ์˜ต์…˜ ๋ชฉ๋ก (๋“œ๋กญ๋‹ค์šด์šฉ) */ diff --git a/src/lib/utils/menuRefresh.ts b/src/lib/utils/menuRefresh.ts index be2f165f..61a9eaa1 100644 --- a/src/lib/utils/menuRefresh.ts +++ b/src/lib/utils/menuRefresh.ts @@ -91,7 +91,9 @@ export async function refreshMenus(): Promise { const data = await response.json(); - if (!data.menus || !Array.isArray(data.menus)) { + // ๋ฐฑ์—”๋“œ ApiResponse::success() ์‘๋‹ต ํ˜•์‹: { success, message, data: [...] } + const apiMenus = data.data; + if (!apiMenus || !Array.isArray(apiMenus)) { return { success: false, updated: false, @@ -100,7 +102,7 @@ export async function refreshMenus(): Promise { } // 3. ๋ฉ”๋‰ด ๋ณ€ํ™˜ - const transformedMenus = transformApiMenusToMenuItems(data.menus); + const transformedMenus = transformApiMenusToMenuItems(apiMenus); const newHash = generateMenuHash(transformedMenus); // 4. ๋ณ€๊ฒฝ ์—†์œผ๋ฉด ์—…๋ฐ์ดํŠธ ์Šคํ‚ต @@ -159,7 +161,9 @@ export async function forceRefreshMenus(): Promise { const data = await response.json(); - if (!data.menus || !Array.isArray(data.menus)) { + // ๋ฐฑ์—”๋“œ ApiResponse::success() ์‘๋‹ต ํ˜•์‹: { success, message, data: [...] } + const apiMenus = data.data; + if (!apiMenus || !Array.isArray(apiMenus)) { return { success: false, updated: false, @@ -167,7 +171,7 @@ export async function forceRefreshMenus(): Promise { }; } - const transformedMenus = transformApiMenusToMenuItems(data.menus); + const transformedMenus = transformApiMenusToMenuItems(apiMenus); // localStorage ์—…๋ฐ์ดํŠธ const userData = localStorage.getItem('user'); diff --git a/src/types/checklist.ts b/src/types/checklist.ts new file mode 100644 index 00000000..b838e93a --- /dev/null +++ b/src/types/checklist.ts @@ -0,0 +1,73 @@ +/** + * ์ ๊ฒ€ํ‘œ ๊ด€๋ฆฌ ํƒ€์ž… ์ •์˜ + */ + +// ============================================================================ +// ์ ๊ฒ€ํ‘œ (Checklist) +// ============================================================================ + +export interface Checklist { + id: string; + checklistCode: string; // ์ ๊ฒ€ํ‘œ ๋ฒˆํ˜ธ + checklistName: string; // ์ ๊ฒ€ํ‘œ๋ช… + itemCount: number; // ํ•ญ๋ชฉ ์ˆ˜ + documentCount: number; // ๋ฌธ์„œ ์ˆ˜ + status: '์‚ฌ์šฉ' | '๋ฏธ์‚ฌ์šฉ'; + order: number; // ์ •๋ ฌ ์ˆœ์„œ + items?: ChecklistItem[]; // ํ•˜์œ„ ํ•ญ๋ชฉ ๋ชฉ๋ก + createdAt: string; + updatedAt: string; +} + +export interface ChecklistFormData { + checklistName: string; + status: '์‚ฌ์šฉ' | '๋ฏธ์‚ฌ์šฉ'; +} + +// ============================================================================ +// ์ ๊ฒ€ํ‘œ ํ•ญ๋ชฉ (Checklist Item) +// ============================================================================ + +export interface ChecklistItem { + id: string; + checklistId: string; // ์†Œ์† ์ ๊ฒ€ํ‘œ ID + itemCode: string; // ํ•ญ๋ชฉ ๋ฒˆํ˜ธ + itemName: string; // ํ•ญ๋ชฉ๋ช… + description: string; // ์†Œ๊ฐœ + documentCount: number; // ๋ฌธ์„œ ์ˆ˜ + status: '์‚ฌ์šฉ' | '๋ฏธ์‚ฌ์šฉ'; + order: number; // ์ •๋ ฌ ์ˆœ์„œ + documents?: ChecklistDocument[]; // ํ•˜์œ„ ๋ฌธ์„œ ๋ชฉ๋ก + createdAt: string; + updatedAt: string; +} + +export interface ChecklistItemFormData { + itemName: string; + description: string; + status: '์‚ฌ์šฉ' | '๋ฏธ์‚ฌ์šฉ'; + documents: ChecklistDocumentFormData[]; +} + +// ============================================================================ +// ์ ๊ฒ€ํ‘œ ๋ฌธ์„œ (Checklist Document) +// ============================================================================ + +export interface ChecklistDocument { + id: string; + itemId: string; // ์†Œ์† ํ•ญ๋ชฉ ID + documentCode: string; // ๋ฌธ์„œ ๋ฒˆํ˜ธ + documentName: string; // ๋ฌธ์„œ๋ช… (ํŒŒ์ผ๋ช…) + revision: string; // ๊ฐœ์ • (REV12 ๋“ฑ) + effectiveDate: string; // ์‹œํ–‰์ผ + order: number; // ์ •๋ ฌ ์ˆœ์„œ +} + +export interface ChecklistDocumentFormData { + id?: string; + documentCode: string; + documentName: string; + revision: string; + effectiveDate: string; + order: number; +} \ No newline at end of file