feat(WEB): QMS 검사 모달 개선, 전자결재/생산대시보드/템플릿 기능 수정
- QMS: InspectionModal/InspectionModalV2 개선, mockData 정리 - 전자결재: DocumentCreate 기능 수정 - 생산대시보드: ProductionDashboard 개선 - 템플릿: IntegratedDetailTemplate/UniversalListPage 기능 수정 - 문서: i18n 가이드 업데이트, 문서뷰어 아키텍처 계획 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useCallback, useEffect, useTransition, useRef } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { usePermission } from '@/hooks/usePermission';
|
||||
import { format } from 'date-fns';
|
||||
import { Trash2, Send, Save, Eye, Loader2 } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
@@ -87,6 +88,7 @@ export function DocumentCreate() {
|
||||
const searchParams = useSearchParams();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const { currentUser } = useAuth();
|
||||
const { canCreate, canDelete } = usePermission();
|
||||
|
||||
// 수정 모드 / 복제 모드 상태
|
||||
const documentId = searchParams.get('id');
|
||||
@@ -546,20 +548,22 @@ export function DocumentCreate() {
|
||||
<Eye className="w-4 h-4 mr-1" />
|
||||
미리보기
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleDelete}
|
||||
disabled={isPending}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-1" />
|
||||
삭제
|
||||
</Button>
|
||||
{canDelete && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleDelete}
|
||||
disabled={isPending}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-1" />
|
||||
삭제
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={handleSubmit}
|
||||
disabled={isPending}
|
||||
disabled={isPending || !canCreate}
|
||||
>
|
||||
{isPending ? (
|
||||
<Loader2 className="w-4 h-4 mr-1 animate-spin" />
|
||||
@@ -583,7 +587,7 @@ export function DocumentCreate() {
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}, [handlePreview, handleDelete, handleSubmit, handleSaveDraft, isPending, isEditMode]);
|
||||
}, [handlePreview, handleDelete, handleSubmit, handleSaveDraft, isPending, isEditMode, canCreate, canDelete]);
|
||||
|
||||
// 폼 컨텐츠 렌더링
|
||||
const renderFormContent = useCallback(() => {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { usePermission } from '@/hooks/usePermission';
|
||||
import { Factory, Clock, PlayCircle, CheckCircle2, AlertTriangle, Timer, Users } from 'lucide-react';
|
||||
import { StatCardGridSkeleton } from '@/components/ui/skeleton';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
@@ -27,6 +28,8 @@ import { STATUS_LABELS } from './types';
|
||||
|
||||
export default function ProductionDashboard() {
|
||||
const router = useRouter();
|
||||
const screenPerm = usePermission('/production/screen');
|
||||
const workOrderPerm = usePermission('/production/work-order-management');
|
||||
|
||||
// ===== 상태 관리 =====
|
||||
const [selectedTab, setSelectedTab] = useState<string>('all');
|
||||
@@ -152,13 +155,17 @@ export default function ProductionDashboard() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={handleWorkerScreenClick}>
|
||||
<Users className="mr-2 h-4 w-4" />
|
||||
작업자 화면
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleWorkOrderListClick}>
|
||||
작업지시 목록
|
||||
</Button>
|
||||
{screenPerm.canView && (
|
||||
<Button variant="outline" onClick={handleWorkerScreenClick}>
|
||||
<Users className="mr-2 h-4 w-4" />
|
||||
작업자 화면
|
||||
</Button>
|
||||
)}
|
||||
{workOrderPerm.canView && (
|
||||
<Button variant="outline" onClick={handleWorkOrderListClick}>
|
||||
작업지시 목록
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo, forwardRef, useImperativeHandle } from 'react';
|
||||
import { useRouter, useParams } from 'next/navigation';
|
||||
import { usePermission } from '@/hooks/usePermission';
|
||||
import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog';
|
||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { PageHeader } from '@/components/organisms/PageHeader';
|
||||
@@ -53,6 +54,7 @@ function IntegratedDetailTemplateInner<T extends Record<string, unknown>>(
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const locale = (params.locale as string) || 'ko';
|
||||
const { canCreate: permCanCreate, canUpdate: permCanUpdate, canDelete: permCanDelete } = usePermission();
|
||||
|
||||
// ===== 상태 =====
|
||||
const [mode, setMode] = useState<DetailMode>(initialMode);
|
||||
@@ -104,14 +106,15 @@ function IntegratedDetailTemplateInner<T extends Record<string, unknown>>(
|
||||
}), [formData, config, initialData]);
|
||||
|
||||
// ===== 권한 계산 =====
|
||||
// config.permissions가 명시적으로 설정되면 우선, 아니면 usePermission() fallback
|
||||
const permissions = useMemo(() => {
|
||||
const p = config.permissions || {};
|
||||
return {
|
||||
canEdit: typeof p.canEdit === 'function' ? p.canEdit() : p.canEdit ?? true,
|
||||
canDelete: typeof p.canDelete === 'function' ? p.canDelete() : p.canDelete ?? true,
|
||||
canCreate: typeof p.canCreate === 'function' ? p.canCreate() : p.canCreate ?? true,
|
||||
canEdit: typeof p.canEdit === 'function' ? p.canEdit() : (p.canEdit !== undefined ? p.canEdit : permCanUpdate),
|
||||
canDelete: typeof p.canDelete === 'function' ? p.canDelete() : (p.canDelete !== undefined ? p.canDelete : permCanDelete),
|
||||
canCreate: typeof p.canCreate === 'function' ? p.canCreate() : (p.canCreate !== undefined ? p.canCreate : permCanCreate),
|
||||
};
|
||||
}, [config.permissions]);
|
||||
}, [config.permissions, permCanUpdate, permCanDelete, permCanCreate]);
|
||||
|
||||
// ===== 모드 헬퍼 =====
|
||||
const isViewMode = mode === 'view';
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||
import { useRouter, useParams } from 'next/navigation';
|
||||
import { usePermission } from '@/hooks/usePermission';
|
||||
import { toast } from 'sonner';
|
||||
import { Download, Loader2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -43,6 +44,7 @@ export function UniversalListPage<T>({
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const locale = (params.locale as string) || 'ko';
|
||||
const { canCreate: permCanCreate, canDelete: permCanDelete } = usePermission();
|
||||
|
||||
// ===== 상태 관리 =====
|
||||
// 원본 데이터 (클라이언트 사이드 필터링용)
|
||||
@@ -825,7 +827,7 @@ export function UniversalListPage<T>({
|
||||
onToggle: () => toggleSelection(id),
|
||||
onRowClick: () => handleRowClick(item),
|
||||
onEdit: () => handleEdit(item),
|
||||
onDelete: () => handleDeleteClick(item),
|
||||
onDelete: permCanDelete ? () => handleDeleteClick(item) : undefined,
|
||||
});
|
||||
},
|
||||
[config, effectiveGetItemId, handleDeleteClick, handleEdit, handleRowClick, effectiveSelectedItems, toggleSelection]
|
||||
@@ -838,7 +840,7 @@ export function UniversalListPage<T>({
|
||||
onToggle,
|
||||
onRowClick: () => handleRowClick(item),
|
||||
onEdit: () => handleEdit(item),
|
||||
onDelete: () => handleDeleteClick(item),
|
||||
onDelete: permCanDelete ? () => handleDeleteClick(item) : undefined,
|
||||
});
|
||||
},
|
||||
[config, handleDeleteClick, handleEdit, handleRowClick]
|
||||
@@ -874,7 +876,7 @@ export function UniversalListPage<T>({
|
||||
}
|
||||
// 공통 헤더 옵션 (달력/등록버튼)
|
||||
dateRangeSelector={config.dateRangeSelector}
|
||||
createButton={config.createButton}
|
||||
createButton={permCanCreate ? config.createButton : undefined}
|
||||
// 탭 콘텐츠
|
||||
tabsContent={config.tabsContent}
|
||||
// 통계 카드
|
||||
|
||||
Reference in New Issue
Block a user