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:
유병철
2026-02-03 09:09:05 +09:00
parent ca6247286a
commit f0987127eb
10 changed files with 578 additions and 230 deletions

View File

@@ -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(() => {

View File

@@ -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>

View File

@@ -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';

View File

@@ -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}
// 통계 카드