feat(WEB): API 인프라 리팩토링, CEO 대시보드 현황판 개선 및 문서 시스템 강화

- API: fetch-wrapper/proxy/refresh-token 리팩토링, authenticated-fetch 신규 추가
- CEO 대시보드: EnhancedSections 현황판 기능 개선, dashboard transformers/types 확장
- 문서 시스템: ApprovalLine/DocumentHeader/DocumentToolbar/DocumentViewer 개선
- 작업지시서: 검사보고서/작업일지 문서 컴포넌트 개선 (벤딩/스크린/슬랫)
- 레이아웃: Sidebar/AuthenticatedLayout 수정
- 작업자화면: WorkerScreen 수정

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-01-30 14:16:17 +09:00
parent a486977b80
commit 3ef9570f3b
27 changed files with 554 additions and 451 deletions

View File

@@ -150,15 +150,15 @@ export function ApprovalLine({
{/* 부서 행 (선택적) */}
{showDepartment && (
<tr>
<td className="w-16 p-2 text-center bg-gray-50 border border-gray-300 text-[10px]">
<td className="w-16 p-2 text-center bg-gray-50 border border-gray-300 text-[10px] whitespace-nowrap">
{departmentLabels.writer}
</td>
{is4Col && (
<td className="w-16 p-2 text-center bg-gray-50 border border-gray-300 text-[10px]">
<td className="w-16 p-2 text-center bg-gray-50 border border-gray-300 text-[10px] whitespace-nowrap">
{departmentLabels.reviewer}
</td>
)}
<td className="w-16 p-2 text-center bg-gray-50 border border-gray-300 text-[10px]">
<td className="w-16 p-2 text-center bg-gray-50 border border-gray-300 text-[10px] whitespace-nowrap">
{departmentLabels.approver}
</td>
</tr>

View File

@@ -96,16 +96,16 @@ export function ConstructionApprovalTable({
{/* 부서 행 */}
<tr>
<td className="border border-gray-300 px-3 py-1 text-center text-xs text-gray-500">
<td className="border border-gray-300 px-3 py-1 text-center text-xs text-gray-500 whitespace-nowrap">
{approvers.writer?.department || '부서명'}
</td>
<td className="border border-gray-300 px-3 py-1 text-center text-xs text-gray-500">
<td className="border border-gray-300 px-3 py-1 text-center text-xs text-gray-500 whitespace-nowrap">
{approvers.approver1?.department || '부서명'}
</td>
<td className="border border-gray-300 px-3 py-1 text-center text-xs text-gray-500">
<td className="border border-gray-300 px-3 py-1 text-center text-xs text-gray-500 whitespace-nowrap">
{approvers.approver2?.department || '부서명'}
</td>
<td className="border border-gray-300 px-3 py-1 text-center text-xs text-gray-500">
<td className="border border-gray-300 px-3 py-1 text-center text-xs text-gray-500 whitespace-nowrap">
{approvers.approver3?.department || '부서명'}
</td>
</tr>

View File

@@ -213,7 +213,7 @@ export function DocumentHeader({
<span className="text-2xl font-bold">{logo.text}</span>
)}
{logo.subtext && (
<span className="text-xs text-gray-500">{logo.subtext}</span>
<span className="text-xs text-gray-500 whitespace-nowrap">{logo.subtext}</span>
)}
</div>
)}

View File

@@ -74,7 +74,7 @@ export function QualityApprovalTable({
</tbody>
</table>
{reportDate && (
<div className="text-xs text-right mt-1">: {reportDate}</div>
<div className="text-xs text-right mt-1 whitespace-nowrap">: {reportDate}</div>
)}
</div>
);

View File

@@ -16,6 +16,7 @@ import {
FileOutput,
Mail,
MessageCircle,
Loader2,
} from 'lucide-react';
import { ReactNode } from 'react';
import { Button } from '@/components/ui/button';
@@ -41,6 +42,9 @@ interface DocumentToolbarProps {
onFax?: () => void;
onKakao?: () => void;
toolbarExtra?: ReactNode;
// 로딩 상태
loadingAction?: ActionType | null;
}
/**
@@ -65,8 +69,10 @@ export function DocumentToolbar({
onFax,
onKakao,
toolbarExtra,
loadingAction,
}: DocumentToolbarProps) {
const showZoomControls = features.zoom !== false;
const isAnyLoading = !!loadingAction;
// 액션 버튼 렌더링
const renderActionButton = (action: ActionType) => {
@@ -80,9 +86,14 @@ export function DocumentToolbar({
size="sm"
className="h-8 gap-1 text-xs px-2 sm:px-3"
onClick={onPrint}
disabled={isAnyLoading}
>
<Printer size={14} />
<span className="hidden sm:inline"></span>
{loadingAction === 'print' ? (
<Loader2 size={14} className="animate-spin" />
) : (
<Printer size={14} />
)}
<span className="hidden sm:inline">{loadingAction === 'print' ? '인쇄 중...' : '인쇄'}</span>
</Button>
);
@@ -94,9 +105,14 @@ export function DocumentToolbar({
size="sm"
className="h-8 bg-green-600 hover:bg-green-700 text-white gap-1 text-xs px-2 sm:px-3"
onClick={onDownload}
disabled={isAnyLoading}
>
<Download size={14} />
<span className="hidden sm:inline"></span>
{loadingAction === 'download' ? (
<Loader2 size={14} className="animate-spin" />
) : (
<Download size={14} />
)}
<span className="hidden sm:inline">{loadingAction === 'download' ? '다운로드 중...' : '다운로드'}</span>
</Button>
);
@@ -196,9 +212,14 @@ export function DocumentToolbar({
size="sm"
className="h-8 bg-red-600 hover:bg-red-700 text-white gap-1 text-xs px-2 sm:px-3"
onClick={onPdf}
disabled={isAnyLoading}
>
<Download size={14} />
<span className="hidden sm:inline">PDF</span>
{loadingAction === 'pdf' ? (
<Loader2 size={14} className="animate-spin" />
) : (
<Download size={14} />
)}
<span className="hidden sm:inline">{loadingAction === 'pdf' ? 'PDF 생성 중...' : 'PDF'}</span>
</Button>
);

View File

@@ -1,6 +1,6 @@
'use client';
import React, { useEffect, ReactNode, useCallback } from 'react';
import React, { useEffect, useState, ReactNode, useCallback } from 'react';
import {
Dialog,
DialogContent,
@@ -102,11 +102,23 @@ export function DocumentViewer({
const features: DocumentFeatures = merged.features;
const actions: ActionType[] = merged.actions;
// 로딩 상태
const [loadingAction, setLoadingAction] = useState<ActionType | null>(null);
// 줌 훅
const zoom = useZoom();
const zoomBase = useZoom();
// 드래그 훅
const drag = useDrag({ enabled: features.drag !== false && zoom.zoom > 100 });
const drag = useDrag({ enabled: features.drag !== false && zoomBase.zoom > 100 });
// 맞춤 클릭 시 드래그 위치도 함께 리셋
const zoom = {
...zoomBase,
zoomReset: () => {
zoomBase.zoomReset();
drag.resetPosition();
},
};
// 모달 열릴 때 상태 초기화
useEffect(() => {
@@ -182,6 +194,7 @@ export function DocumentViewer({
}
try {
setLoadingAction('pdf');
toast.loading('PDF 생성 중...', { id: 'pdf-generating' });
// print-area 영역 찾기
@@ -234,6 +247,8 @@ export function DocumentViewer({
} catch (error) {
console.error('PDF 생성 오류:', error);
toast.error('PDF 생성 중 오류가 발생했습니다.', { id: 'pdf-generating' });
} finally {
setLoadingAction(null);
}
}, [onPdf, title, pdfMeta]);
@@ -301,6 +316,7 @@ export function DocumentViewer({
onFax={onFax}
onKakao={onKakao}
toolbarExtra={toolbarExtra}
loadingAction={loadingAction}
/>
{/* 콘텐츠 */}