feat: 순서변경 ▲/▼ 버튼 추가 (터치 지원) + 단가표 테이블 스크롤 수정

- ReorderButtons 공통 컴포넌트 신규 생성 (molecules)
- 패턴B(리스트): RankManagement, TitleManagement, CategoryManagement
- 패턴A(테이블): ProcessDetail, ProcessForm, ChecklistDetail
- 패턴C(컴포넌트): DraggableSection, DraggableField, HierarchyTab
- 모바일: GripVertical 숨김, ▲/▼ 버튼만 표시
- 데스크톱: GripVertical + ▲/▼ 버튼 모두 표시
- 단가표 단가정보 테이블 overflow-hidden → overflow-x-auto + min-w 적용
This commit is contained in:
유병철
2026-02-25 14:28:49 +09:00
parent 4dc0644f8d
commit 0b41b9f813
21 changed files with 619 additions and 363 deletions

View File

@@ -387,14 +387,14 @@ export function PermissionManagement() {
}) => (
<div className="flex items-center gap-2 flex-wrap ml-auto">
{selItems.size > 0 && (
<Button variant="destructive" onClick={handleBulkDelete}>
<Trash2 className="h-4 w-4 mr-2" />
({selItems.size})
<Button variant="destructive" size="sm" className="md:size-default" onClick={handleBulkDelete}>
<Trash2 className="h-4 w-4 md:mr-2" />
<span className="hidden md:inline"> ({selItems.size})</span>
</Button>
)}
<Button onClick={handleAdd}>
<Plus className="h-4 w-4 mr-2" />
<Button size="sm" className="md:size-default" onClick={handleAdd}>
<Plus className="h-4 w-4 md:mr-2" />
<span className="hidden md:inline"> </span>
</Button>
</div>
), [handleBulkDelete, handleAdd]);

View File

@@ -4,6 +4,7 @@ import { useState, useEffect, useCallback } from 'react';
import { PageLayout } from '@/components/organisms/PageLayout';
import { PageHeader } from '@/components/organisms/PageHeader';
import { Award, Plus, GripVertical, Pencil, Trash2, Loader2 } from 'lucide-react';
import { ReorderButtons } from '@/components/molecules';
import { ContentSkeleton } from '@/components/ui/skeleton';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@@ -185,6 +186,33 @@ export function RankManagement() {
setDraggedItem(index);
};
// 화살표 버튼으로 순서 변경
const handleMoveItem = async (fromIndex: number, toIndex: number) => {
const newRanks = [...ranks];
const [moved] = newRanks.splice(fromIndex, 1);
newRanks.splice(toIndex, 0, moved);
const reordered = newRanks.map((rank, idx) => ({ ...rank, order: idx + 1 }));
setRanks(reordered);
try {
const items = reordered.map((rank, idx) => ({
id: rank.id,
sort_order: idx + 1,
}));
const result = await reorderRanks(items);
if (result.success) {
toast.success('순서가 변경되었습니다.');
} else {
toast.error(result.error || '순서 변경에 실패했습니다.');
loadRanks();
}
} catch (error) {
if (isNextRedirectError(error)) throw error;
toast.error('순서 변경에 실패했습니다.');
loadRanks();
}
};
// 키보드로 추가 (한글 IME 조합 중에는 무시)
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.nativeEvent.isComposing) {
@@ -196,7 +224,7 @@ export function RankManagement() {
<PageLayout>
<PageHeader
title="직급관리"
description="사원의 직급을 관리합니다. 드래그하여 순서를 변경할 수 있습니다."
description="사원의 직급을 관리합니다. 화살표 버튼 또는 드래그하여 순서를 변경할 수 있습니다."
icon={Award}
/>
@@ -246,8 +274,18 @@ export function RankManagement() {
draggedItem === index ? 'opacity-50 bg-muted' : ''
}`}
>
{/* 드래그 핸들 */}
<GripVertical className="h-5 w-5 text-muted-foreground flex-shrink-0" />
{/* 드래그 핸들 (PC만) */}
<GripVertical className="h-5 w-5 text-muted-foreground flex-shrink-0 hidden md:block" />
{/* 순서 변경 버튼 */}
<ReorderButtons
onMoveUp={() => handleMoveItem(index, index - 1)}
onMoveDown={() => handleMoveItem(index, index + 1)}
isFirst={index === 0}
isLast={index === ranks.length - 1}
disabled={isSubmitting}
size="xs"
/>
{/* 순서 번호 */}
<span className="text-sm text-muted-foreground w-8">
@@ -295,7 +333,7 @@ export function RankManagement() {
{/* 안내 문구 */}
<p className="text-sm text-muted-foreground">
.
.
</p>
</div>

View File

@@ -4,6 +4,7 @@ import { useState, useEffect, useCallback } from 'react';
import { PageLayout } from '@/components/organisms/PageLayout';
import { PageHeader } from '@/components/organisms/PageHeader';
import { Briefcase, Plus, GripVertical, Pencil, Trash2, Loader2 } from 'lucide-react';
import { ReorderButtons } from '@/components/molecules';
import { ContentSkeleton } from '@/components/ui/skeleton';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@@ -185,6 +186,33 @@ export function TitleManagement() {
setDraggedItem(index);
};
// 화살표 버튼으로 순서 변경
const handleMoveItem = async (fromIndex: number, toIndex: number) => {
const newTitles = [...titles];
const [moved] = newTitles.splice(fromIndex, 1);
newTitles.splice(toIndex, 0, moved);
const reordered = newTitles.map((title, idx) => ({ ...title, order: idx + 1 }));
setTitles(reordered);
try {
const items = reordered.map((title, idx) => ({
id: title.id,
sort_order: idx + 1,
}));
const result = await reorderTitles(items);
if (result.success) {
toast.success('순서가 변경되었습니다.');
} else {
toast.error(result.error || '순서 변경에 실패했습니다.');
loadTitles();
}
} catch (error) {
if (isNextRedirectError(error)) throw error;
toast.error('순서 변경에 실패했습니다.');
loadTitles();
}
};
// 키보드로 추가 (한글 IME 조합 중에는 무시)
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.nativeEvent.isComposing) {
@@ -196,7 +224,7 @@ export function TitleManagement() {
<PageLayout>
<PageHeader
title="직책관리"
description="사원의 직책을 관리합니다. 드래그하여 순서를 변경할 수 있습니다."
description="사원의 직책을 관리합니다. 화살표 버튼 또는 드래그하여 순서를 변경할 수 있습니다."
icon={Briefcase}
/>
@@ -246,8 +274,18 @@ export function TitleManagement() {
draggedItem === index ? 'opacity-50 bg-muted' : ''
}`}
>
{/* 드래그 핸들 */}
<GripVertical className="h-5 w-5 text-muted-foreground flex-shrink-0" />
{/* 드래그 핸들 (PC만) */}
<GripVertical className="h-5 w-5 text-muted-foreground flex-shrink-0 hidden md:block" />
{/* 순서 변경 버튼 */}
<ReorderButtons
onMoveUp={() => handleMoveItem(index, index - 1)}
onMoveDown={() => handleMoveItem(index, index + 1)}
isFirst={index === 0}
isLast={index === titles.length - 1}
disabled={isSubmitting}
size="xs"
/>
{/* 순서 번호 */}
<span className="text-sm text-muted-foreground w-8">
@@ -295,7 +333,7 @@ export function TitleManagement() {
{/* 안내 문구 */}
<p className="text-sm text-muted-foreground">
.
.
</p>
</div>