- |
+ |
No.
|
@@ -242,10 +260,19 @@ export function ChecklistDetail({ checklist }: ChecklistDetailProps) {
}`}
>
e.stopPropagation()}
>
-
+
+
+ handleMoveItem(index, index - 1)}
+ onMoveDown={() => handleMoveItem(index, index + 1)}
+ isFirst={index === 0}
+ isLast={index === items.length - 1}
+ size="xs"
+ />
+
|
{index + 1}
diff --git a/src/components/items/ItemMasterDataManagement/components/DraggableField.tsx b/src/components/items/ItemMasterDataManagement/components/DraggableField.tsx
index c96e9fb1..879c2476 100644
--- a/src/components/items/ItemMasterDataManagement/components/DraggableField.tsx
+++ b/src/components/items/ItemMasterDataManagement/components/DraggableField.tsx
@@ -7,6 +7,7 @@ import {
Edit,
Unlink
} from 'lucide-react';
+import { ReorderButtons } from '@/components/molecules';
// 입력방식 옵션 (ItemMasterDataManagement에서 사용하는 상수)
const INPUT_TYPE_OPTIONS = [
@@ -25,9 +26,11 @@ interface DraggableFieldProps {
moveField: (dragFieldId: number, hoverFieldId: number) => void;
onDelete: () => void;
onEdit?: () => void;
+ prevFieldId?: number;
+ nextFieldId?: number;
}
-export function DraggableField({ field, index, moveField, onDelete, onEdit }: DraggableFieldProps) {
+export function DraggableField({ field, index, moveField, onDelete, onEdit, prevFieldId, nextFieldId }: DraggableFieldProps) {
const [isDragging, setIsDragging] = useState(false);
const handleDragStart = (e: React.DragEvent) => {
@@ -79,7 +82,14 @@ export function DraggableField({ field, index, moveField, onDelete, onEdit }: Dr
>
-
+
+ prevFieldId !== undefined && moveField(field.id, prevFieldId)}
+ onMoveDown={() => nextFieldId !== undefined && moveField(field.id, nextFieldId)}
+ isFirst={prevFieldId === undefined}
+ isLast={nextFieldId === undefined}
+ size="xs"
+ />
{field.field_name}
{INPUT_TYPE_OPTIONS.find(t => t.value === field.field_type)?.label || field.field_type}
diff --git a/src/components/items/ItemMasterDataManagement/components/DraggableSection.tsx b/src/components/items/ItemMasterDataManagement/components/DraggableSection.tsx
index 0e2171f9..0db73067 100644
--- a/src/components/items/ItemMasterDataManagement/components/DraggableSection.tsx
+++ b/src/components/items/ItemMasterDataManagement/components/DraggableSection.tsx
@@ -10,10 +10,12 @@ import {
X,
Unlink
} from 'lucide-react';
+import { ReorderButtons } from '@/components/molecules';
interface DraggableSectionProps {
section: ItemSection;
index: number;
+ totalSections: number;
moveSection: (dragIndex: number, hoverIndex: number) => void;
onDelete: () => void;
onEditTitle: (id: number, title: string) => void;
@@ -28,6 +30,7 @@ interface DraggableSectionProps {
export function DraggableSection({
section,
index,
+ totalSections,
moveSection,
onDelete,
onEditTitle,
@@ -87,7 +90,14 @@ export function DraggableSection({
-
+
+ moveSection(index, index - 1)}
+ onMoveDown={() => moveSection(index, index + 1)}
+ isFirst={index === 0}
+ isLast={index === totalSections - 1}
+ size="xs"
+ />
{editingSectionId === section.id ? (
diff --git a/src/components/items/ItemMasterDataManagement/tabs/HierarchyTab/index.tsx b/src/components/items/ItemMasterDataManagement/tabs/HierarchyTab/index.tsx
index be2e2f3a..cb01efe5 100644
--- a/src/components/items/ItemMasterDataManagement/tabs/HierarchyTab/index.tsx
+++ b/src/components/items/ItemMasterDataManagement/tabs/HierarchyTab/index.tsx
@@ -380,6 +380,7 @@ export function HierarchyTab({
key={`section-${section.id}-${index}`}
section={section}
index={index}
+ totalSections={selectedPage.sections.length}
moveSection={(dragIndex, hoverIndex) => {
moveSection(dragIndex, hoverIndex);
}}
@@ -469,7 +470,7 @@ export function HierarchyTab({
) : (
section.fields
.sort((a, b) => (a.order_no ?? 0) - (b.order_no ?? 0))
- .map((field, fieldIndex) => (
+ .map((field, fieldIndex, sortedFields) => (
handleEditField(String(section.id), field)}
+ prevFieldId={fieldIndex > 0 ? sortedFields[fieldIndex - 1].id : undefined}
+ nextFieldId={fieldIndex < sortedFields.length - 1 ? sortedFields[fieldIndex + 1].id : undefined}
/>
))
)}
diff --git a/src/components/molecules/ReorderButtons.tsx b/src/components/molecules/ReorderButtons.tsx
new file mode 100644
index 00000000..26c1ed1a
--- /dev/null
+++ b/src/components/molecules/ReorderButtons.tsx
@@ -0,0 +1,61 @@
+import { ChevronUp, ChevronDown } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { cn } from '@/lib/utils';
+
+interface ReorderButtonsProps {
+ onMoveUp: () => void;
+ onMoveDown: () => void;
+ isFirst: boolean;
+ isLast: boolean;
+ disabled?: boolean;
+ size?: 'sm' | 'xs';
+ className?: string;
+}
+
+export function ReorderButtons({
+ onMoveUp,
+ onMoveDown,
+ isFirst,
+ isLast,
+ disabled = false,
+ size = 'sm',
+ className,
+}: ReorderButtonsProps) {
+ const iconSize = size === 'xs' ? 'h-3 w-3' : 'h-4 w-4';
+ const btnSize = size === 'xs' ? 'h-5 w-5' : 'h-6 w-6';
+
+ return (
+
+
+
+
+ );
+}
diff --git a/src/components/molecules/index.ts b/src/components/molecules/index.ts
index 0d81d69a..18d26d22 100644
--- a/src/components/molecules/index.ts
+++ b/src/components/molecules/index.ts
@@ -13,4 +13,6 @@ export { YearQuarterFilter } from "./YearQuarterFilter";
export type { Quarter } from "./YearQuarterFilter";
export { GenericCRUDDialog } from "./GenericCRUDDialog";
-export type { GenericCRUDDialogProps, CRUDFieldDefinition } from "./GenericCRUDDialog";
\ No newline at end of file
+export type { GenericCRUDDialogProps, CRUDFieldDefinition } from "./GenericCRUDDialog";
+
+export { ReorderButtons } from "./ReorderButtons";
\ No newline at end of file
diff --git a/src/components/pricing-table-management/PricingTableForm.tsx b/src/components/pricing-table-management/PricingTableForm.tsx
index 90a6a4e9..46b57e2c 100644
--- a/src/components/pricing-table-management/PricingTableForm.tsx
+++ b/src/components/pricing-table-management/PricingTableForm.tsx
@@ -346,23 +346,23 @@ export function PricingTableForm({ mode, initialData }: PricingTableFormProps) {
{/* 거래등급별 판매단가 */}
-
-
+
+
- |
+ |
거래등급
|
-
+ |
마진율
|
-
+ |
판매단가
|
-
+ |
비고
|
-
+ |
diff --git a/src/components/process-management/ProcessDetail.tsx b/src/components/process-management/ProcessDetail.tsx
index 06325e84..2c3bffaf 100644
--- a/src/components/process-management/ProcessDetail.tsx
+++ b/src/components/process-management/ProcessDetail.tsx
@@ -11,7 +11,8 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { useRouter } from 'next/navigation';
-import { ArrowLeft, Edit, GripVertical, Plus, Package, Trash2 } from 'lucide-react';
+import { ArrowLeft, Edit, GripVertical, Plus, Trash2 } from 'lucide-react';
+import { ReorderButtons } from '@/components/molecules';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
@@ -49,7 +50,7 @@ export function ProcessDetail({ process, onProcessUpdate }: ProcessDetailProps)
// 드래그 상태
const [dragIndex, setDragIndex] = useState(null);
const [dragOverIndex, setDragOverIndex] = useState(null);
- const dragNodeRef = useRef(null);
+ const dragNodeRef = useRef(null);
// 개별 품목 목록 추출
const individualItems = process.classificationRules
@@ -103,7 +104,7 @@ export function ProcessDetail({ process, onProcessUpdate }: ProcessDetailProps)
};
// ===== 드래그&드롭 (HTML5 네이티브) =====
- const handleDragStart = useCallback((e: React.DragEvent, index: number) => {
+ const handleDragStart = useCallback((e: React.DragEvent, index: number) => {
setDragIndex(index);
dragNodeRef.current = e.currentTarget;
e.dataTransfer.effectAllowed = 'move';
@@ -115,7 +116,7 @@ export function ProcessDetail({ process, onProcessUpdate }: ProcessDetailProps)
});
}, []);
- const handleDragOver = useCallback((e: React.DragEvent, index: number) => {
+ const handleDragOver = useCallback((e: React.DragEvent, index: number) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
setDragOverIndex(index);
@@ -130,7 +131,7 @@ export function ProcessDetail({ process, onProcessUpdate }: ProcessDetailProps)
dragNodeRef.current = null;
}, []);
- const handleDrop = useCallback((e: React.DragEvent, dropIndex: number) => {
+ const handleDrop = useCallback((e: React.DragEvent, dropIndex: number) => {
e.preventDefault();
if (dragIndex === null || dragIndex === dropIndex) return;
@@ -152,6 +153,23 @@ export function ProcessDetail({ process, onProcessUpdate }: ProcessDetailProps)
handleDragEnd();
}, [dragIndex, handleDragEnd, process.id]);
+ // 화살표 버튼으로 순서 변경
+ const handleMoveStep = useCallback((fromIndex: number, toIndex: number) => {
+ setSteps((prev) => {
+ const updated = [...prev];
+ const [moved] = updated.splice(fromIndex, 1);
+ updated.splice(toIndex, 0, moved);
+ const reordered = updated.map((step, i) => ({ ...step, order: i + 1 }));
+
+ reorderProcessSteps(
+ process.id,
+ reordered.map((s) => ({ id: s.id, order: s.order }))
+ );
+
+ return reordered;
+ });
+ }, [process.id]);
+
return (
{/* 헤더 */}
@@ -165,7 +183,7 @@ export function ProcessDetail({ process, onProcessUpdate }: ProcessDetailProps)
{/* Row 1: 공정번호 | 공정명 | 담당부서 | 담당자 */}
-
+
공정번호
{process.processCode}
@@ -184,7 +202,7 @@ export function ProcessDetail({ process, onProcessUpdate }: ProcessDetailProps)
{/* Row 2: 구분 | 생산일자 | 상태 */}
-
+
구분
{process.processCategory || '없음'}
@@ -203,7 +221,7 @@ export function ProcessDetail({ process, onProcessUpdate }: ProcessDetailProps)
{/* Row 3: 중간검사여부 | 중간검사양식 | 작업일지여부 | 작업일지양식 */}
-
+
중간검사 여부
@@ -234,43 +252,37 @@ export function ProcessDetail({ process, onProcessUpdate }: ProcessDetailProps)
{/* 품목 설정 정보 */}
-
-
-
-
-
- 품목 설정 정보
-
-
- 품목을 선택하면 이 공정으로 분류됩니다
-
-
-
-
- {itemCount}개
-
-
-
+
+
+
+ 품목 설정 정보
+
+
+ {itemCount}개
+
+
+
+ 품목을 선택하면 이 공정으로 분류됩니다
+
{individualItems.length > 0 && (
-
-
+
+
{individualItems.map((item) => (
- {item.code}
- {item.name}
+ {item.code}
))}
@@ -307,34 +319,59 @@ export function ProcessDetail({ process, onProcessUpdate }: ProcessDetailProps)
등록된 단계가 없습니다. [단계 등록] 버튼으로 추가해주세요.
) : (
-
+ <>
+ {/* 모바일: 카드 리스트 */}
+
+ {steps.map((step, index) => (
+ handleDragStart(e, index)}
+ onDragOver={(e) => handleDragOver(e, index)}
+ onDragEnd={handleDragEnd}
+ onDrop={(e) => handleDrop(e, index)}
+ onClick={() => handleStepClick(step.id)}
+ className={`flex items-center gap-2 px-3 py-2.5 cursor-pointer hover:bg-muted/50 ${
+ dragOverIndex === index && dragIndex !== index
+ ? 'border-t-2 border-t-primary'
+ : ''
+ }`}
+ >
+ handleMoveStep(index, index - 1)}
+ onMoveDown={() => handleMoveStep(index, index + 1)}
+ isFirst={index === 0}
+ isLast={index === steps.length - 1}
+ size="xs"
+ />
+ {index + 1}
+
+
+ {step.stepCode}
+ {step.stepName}
+
+
+
+ 필수
+ 승인
+ 검사
+
+
+ ))}
+
+ {/* 데스크탑: 테이블 */}
+
- |
- {/* 드래그 핸들 헤더 */}
- |
-
- No.
- |
-
- 단계코드
- |
-
- 단계명
- |
-
- 필수여부
- |
-
- 승인여부
- |
-
- 검사여부
- |
-
- 사용
- |
+ |
+ No. |
+ 단계코드 |
+ 단계명 |
+ 필수여부 |
+ 승인여부 |
+ 검사여부 |
+ 사용 |
@@ -353,58 +390,39 @@ export function ProcessDetail({ process, onProcessUpdate }: ProcessDetailProps)
: ''
}`}
>
- e.stopPropagation()}
- >
-
+ | e.stopPropagation()}>
+
+
+ handleMoveStep(index, index - 1)}
+ onMoveDown={() => handleMoveStep(index, index + 1)}
+ isFirst={index === 0}
+ isLast={index === steps.length - 1}
+ size="xs"
+ />
+
|
-
- {index + 1}
- |
-
- {step.stepCode}
- |
-
- {step.stepName}
+ | {index + 1} |
+ {step.stepCode} |
+ {step.stepName} |
+
+ {step.isRequired ? 'Y' : 'N'}
|
-
- {step.isRequired ? 'Y' : 'N'}
-
+ {step.needsApproval ? 'Y' : 'N'}
|
-
- {step.needsApproval ? 'Y' : 'N'}
-
- |
-
-
- {step.needsInspection ? 'Y' : 'N'}
-
+ {step.needsInspection ? 'Y' : 'N'}
|
-
- {step.isActive ? 'Y' : 'N'}
-
+ {step.isActive ? 'Y' : 'N'}
|
))}
+ >
)}
diff --git a/src/components/process-management/ProcessForm.tsx b/src/components/process-management/ProcessForm.tsx
index df713219..b5125085 100644
--- a/src/components/process-management/ProcessForm.tsx
+++ b/src/components/process-management/ProcessForm.tsx
@@ -13,7 +13,8 @@
import { useState, useCallback, useEffect, useRef, useMemo } from 'react';
import { useRouter } from 'next/navigation';
-import { Plus, GripVertical, Trash2, Package } from 'lucide-react';
+import { Plus, GripVertical, Trash2 } from 'lucide-react';
+import { ReorderButtons } from '@/components/molecules';
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
import { processCreateConfig, processEditConfig } from './processConfig';
import { Button } from '@/components/ui/button';
@@ -261,7 +262,7 @@ export function ProcessForm({ mode, initialData }: ProcessFormProps) {
// 드래그&드롭
const dragNodeRef = useRef (null);
- const handleDragStart = useCallback((e: React.DragEvent, index: number) => {
+ const handleDragStart = useCallback((e: React.DragEvent, index: number) => {
setDragIndex(index);
dragNodeRef.current = e.currentTarget;
e.dataTransfer.effectAllowed = 'move';
@@ -272,7 +273,7 @@ export function ProcessForm({ mode, initialData }: ProcessFormProps) {
});
}, []);
- const handleDragOver = useCallback((e: React.DragEvent, index: number) => {
+ const handleDragOver = useCallback((e: React.DragEvent, index: number) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
setDragOverIndex(index);
@@ -287,8 +288,18 @@ export function ProcessForm({ mode, initialData }: ProcessFormProps) {
setDragOverIndex(null);
}, []);
+ // 화살표 버튼으로 순서 변경 (로컬 state만 업데이트)
+ const handleMoveStep = useCallback((fromIndex: number, toIndex: number) => {
+ setSteps((prev) => {
+ const updated = [...prev];
+ const [moved] = updated.splice(fromIndex, 1);
+ updated.splice(toIndex, 0, moved);
+ return updated.map((step, i) => ({ ...step, order: i + 1 }));
+ });
+ }, []);
+
const handleDrop = useCallback(
- (e: React.DragEvent, dropIndex: number) => {
+ (e: React.DragEvent, dropIndex: number) => {
e.preventDefault();
if (dragIndex === null || dragIndex === dropIndex) {
handleDragEnd();
@@ -388,7 +399,7 @@ export function ProcessForm({ mode, initialData }: ProcessFormProps) {
{/* Row 1: 공정번호(수정시) | 공정명 | 담당부서 | 담당자 */}
-
+
{isEdit && initialData?.processCode && (
@@ -439,7 +450,7 @@ export function ProcessForm({ mode, initialData }: ProcessFormProps) {
{/* Row 2: 구분 | 생산일자 | 상태 */}
-
+
{/* Row 3: 중간검사여부 | 중간검사양식 | 작업일지여부 | 작업일지양식 */}
-
+
diff --git a/src/components/templates/IntegratedDetailTemplate/components/DetailActions.tsx b/src/components/templates/IntegratedDetailTemplate/components/DetailActions.tsx
index 31ce318e..b6617ba9 100644
--- a/src/components/templates/IntegratedDetailTemplate/components/DetailActions.tsx
+++ b/src/components/templates/IntegratedDetailTemplate/components/DetailActions.tsx
@@ -13,10 +13,11 @@
'use client';
import type { ReactNode } from 'react';
-import { ArrowLeft, Save, Trash2, X, Edit } from 'lucide-react';
+import { ArrowLeft, Save, Trash2, X, Edit, Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { useMenuStore } from '@/stores/menuStore';
+import type { ActionItem } from '../types';
export interface DetailActionsProps {
/** 현재 모드 */
@@ -49,8 +50,10 @@ export interface DetailActionsProps {
onDelete?: () => void;
onEdit?: () => void;
onSubmit?: () => void;
- /** 추가 액션 (삭제 버튼 앞에 표시) */
+ /** 추가 액션 (삭제 버튼 앞에 표시, 자유 JSX) */
extraActions?: ReactNode;
+ /** 추가 액션 아이템 (config 배열, 모바일 아이콘 패턴 자동 적용) */
+ extraActionItems?: ActionItem[];
/** 하단 고정 (sticky) 모드 */
sticky?: boolean;
/** 추가 클래스 */
@@ -69,6 +72,7 @@ export function DetailActions({
onEdit,
onSubmit,
extraActions,
+ extraActionItems,
sticky = false,
className,
}: DetailActionsProps) {
@@ -104,9 +108,9 @@ export function DetailActions({
// Fixed 스타일: 화면 하단에 고정 (사이드바 상태에 따라 동적 계산)
// 모바일: 좌우 여백 16px (left-4 right-4)
- // 태블릿/데스크탑: 사이드바 펼침(w-64=256px), 접힘(w-24=96px) + 여백 고려
+ // 태블릿/데스크탑: 사이드바 펼침(w-64=256px), 접힘(w-24=96px) + 콘텐츠 패딩(24px) 맞춤
const stickyStyles = sticky
- ? `fixed bottom-4 left-4 right-4 px-4 py-3 bg-background/95 backdrop-blur rounded-xl border shadow-lg z-50 transition-all duration-300 md:bottom-6 md:px-6 md:right-[48px] ${sidebarCollapsed ? 'md:left-[156px]' : 'md:left-[316px]'}`
+ ? `fixed bottom-4 left-4 right-4 px-4 py-3 bg-background/95 backdrop-blur rounded-xl border shadow-lg z-50 transition-all duration-300 md:bottom-6 md:px-6 md:right-[24px] ${sidebarCollapsed ? 'md:left-[120px]' : 'md:left-[280px]'}`
: '';
// 공통 레이아웃: 왼쪽(뒤로) | 오른쪽(액션들)
@@ -134,6 +138,24 @@ export function DetailActions({
{extraActions}
+ {/* config 배열 기반 추가 버튼 (모바일 아이콘 패턴 자동 적용) */}
+ {extraActionItems?.filter(item => !item.hidden).map((item, idx) => {
+ const Icon = item.loading ? Loader2 : item.icon;
+ return (
+
+
+ {item.label}
+
+ );
+ })}
+
{/* 삭제 버튼: view, edit 모드에서 표시 (create는 삭제할 대상 없음) */}
{!isCreateMode && canDelete && showDelete && onDelete && (
>(
renderForm,
renderField,
headerActions,
+ headerActionItems,
beforeContent,
afterContent,
buttonPosition = 'bottom',
@@ -360,12 +361,13 @@ function IntegratedDetailTemplateInner>(
onEdit={handleEdit}
onSubmit={handleSubmit}
extraActions={headerActions}
+ extraActionItems={headerActionItems}
sticky={shouldSticky}
className={additionalClass}
/>
);
}, [
- mode, isSubmitting, permissions, actions, headerActions, shouldSticky,
+ mode, isSubmitting, permissions, actions, headerActions, headerActionItems, shouldSticky,
navigateToList, handleDelete, handleEdit, handleCancel, handleSubmit, onDelete
]);
diff --git a/src/components/templates/IntegratedDetailTemplate/types.ts b/src/components/templates/IntegratedDetailTemplate/types.ts
index 61bd6ef7..fa3cdeee 100644
--- a/src/components/templates/IntegratedDetailTemplate/types.ts
+++ b/src/components/templates/IntegratedDetailTemplate/types.ts
@@ -224,8 +224,10 @@ export interface IntegratedDetailTemplateProps> {
error?: string;
}
) => ReactNode | null;
- /** 헤더 우측 추가 액션 */
+ /** 헤더 우측 추가 액션 (자유 JSX - Badge 등 비버튼 요소용) */
headerActions?: ReactNode;
+ /** 헤더 우측 추가 버튼 (config 배열 - 모바일 아이콘 패턴 자동 적용) */
+ headerActionItems?: ActionItem[];
/** 폼 앞에 추가 콘텐츠 */
beforeContent?: ReactNode;
/** 폼 뒤에 추가 콘텐츠 */
@@ -242,6 +244,26 @@ export interface IntegratedDetailTemplateProps> {
isSubmitting?: boolean;
}
+// ===== 액션 아이템 (headerActionItems용) =====
+export interface ActionItem {
+ /** 아이콘 (lucide-react) */
+ icon: LucideIcon;
+ /** 버튼 라벨 (데스크탑에서 표시, 모바일은 아이콘만) */
+ label: string;
+ /** 클릭 핸들러 */
+ onClick: () => void;
+ /** 버튼 variant */
+ variant?: 'default' | 'outline' | 'destructive' | 'secondary' | 'ghost';
+ /** 커스텀 클래스 (예: 'bg-green-600 hover:bg-green-700') */
+ className?: string;
+ /** 비활성화 여부 */
+ disabled?: boolean;
+ /** 조건부 숨김 */
+ hidden?: boolean;
+ /** true면 Loader2 스피너로 아이콘 대체 */
+ loading?: boolean;
+}
+
// ===== API 응답 타입 =====
export interface ApiResponse {
success: boolean;
| |