refactor(WEB): CEO 대시보드 대규모 개선 및 문서/권한/스토어 리팩토링

- CEO 대시보드: 섹션별 API 연동 강화 (매출/매입/생산 실데이터 표시)
- DashboardSettingsDialog 드래그 정렬 및 설정 UX 개선
- dashboard transformers 모듈 분리 (파일 분할)
- DocumentTable/DocumentWrapper 공통 문서 컴포넌트 추출
- LineItemsTable organisms 컴포넌트 추가
- PurchaseOrderDocument/InspectionRequestDocument 문서 컴포넌트 리팩토링
- PermissionContext → permissionStore(Zustand) 전환
- useUIStore, stores/utils/userStorage 추가
- favoritesStore/useTableColumnStore 사용자별 저장 지원
- DepositDetail/WithdrawalDetail 삭제 (통합)
- PurchaseDetail/SalesDetail 간소화
- amount.ts/formatters.ts 유틸 확장

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-02-23 20:59:25 +09:00
parent 718be1cfdb
commit 8f4a7ee842
43 changed files with 3489 additions and 3463 deletions

View File

@@ -0,0 +1,93 @@
import { ReactNode } from 'react';
import { cn } from '@/lib/utils';
export interface DocumentTableProps {
children: ReactNode;
/** 테이블 위 섹션 헤더 */
header?: string;
/** 헤더 배경색 (기본: dark) */
headerVariant?: 'dark' | 'light' | 'primary';
/** 하단 마진 (기본: mb-6) */
spacing?: string;
/** 추가 className (table 요소에 적용) */
className?: string;
}
/**
* 문서용 테이블 래퍼
*
* 일관된 border, collapse, text-size를 적용합니다.
* 선택적으로 섹션 헤더를 포함할 수 있습니다.
*
* @example
* // 기본 사용
* <DocumentTable>
* <thead>
* <tr><th className={DOC_STYLES.th}>품목</th></tr>
* </thead>
* <tbody>
* <tr><td className={DOC_STYLES.td}>스크린</td></tr>
* </tbody>
* </DocumentTable>
*
* @example
* // 섹션 헤더 포함
* <DocumentTable header="자재 내역">
* <tbody>...</tbody>
* </DocumentTable>
*/
export function DocumentTable({
children,
header,
headerVariant = 'dark',
spacing = 'mb-6',
className,
}: DocumentTableProps) {
const headerClasses = {
dark: 'bg-gray-800 text-white',
light: 'bg-gray-100 text-gray-900',
primary: 'bg-blue-600 text-white',
};
return (
<div className={spacing}>
{header && (
<div className={cn('text-center py-1 font-bold border-b border-gray-400', headerClasses[headerVariant])}>
{header}
</div>
)}
<table className={cn('w-full border-collapse border border-gray-400', className)}>
{children}
</table>
</div>
);
}
/**
* 문서 테이블 셀 스타일 상수
*
* 문서 내 <th>/<td>에 일관된 스타일을 적용하기 위한 유틸리티.
* DocumentTable 내부에서 직접 className으로 사용합니다.
*
* @example
* <tr>
* <th className={DOC_STYLES.th}>품목명</th>
* <td className={DOC_STYLES.td}>스크린</td>
* <td className={DOC_STYLES.tdCenter}>10</td>
* <td className={DOC_STYLES.tdRight}>1,000,000</td>
* </tr>
*/
export const DOC_STYLES = {
/** 헤더 셀 (회색 배경, 볼드) */
th: 'bg-gray-100 border border-gray-400 px-2 py-1 font-medium text-center',
/** 데이터 셀 (좌측 정렬) */
td: 'border border-gray-300 px-2 py-1',
/** 데이터 셀 (중앙 정렬) */
tdCenter: 'border border-gray-300 px-2 py-1 text-center',
/** 데이터 셀 (우측 정렬, 숫자용) */
tdRight: 'border border-gray-300 px-2 py-1 text-right',
/** 라벨 셀 (key-value 테이블의 라벨) */
label: 'bg-gray-100 border border-gray-300 px-2 py-1 font-medium w-24',
/** 값 셀 (key-value 테이블의 값) */
value: 'border border-gray-300 px-2 py-1',
} as const;

View File

@@ -0,0 +1,48 @@
import { ReactNode, forwardRef } from 'react';
import { cn } from '@/lib/utils';
export interface DocumentWrapperProps {
children: ReactNode;
/** 텍스트 크기 (기본: text-xs) */
fontSize?: 'text-[10px]' | 'text-[11px]' | 'text-xs' | 'text-sm';
/** print-area 클래스 자동 추가 (기본: true) */
printArea?: boolean;
/** 추가 className */
className?: string;
}
/**
* 문서 A4 래퍼
*
* 모든 문서 컴포넌트의 최외곽 컨테이너.
* 일관된 배경색, 패딩, 최소 높이, 프린트 클래스를 제공합니다.
*
* @example
* <DocumentWrapper>
* <DocumentHeader title="발주서" />
* <DocumentTable>...</DocumentTable>
* </DocumentWrapper>
*
* @example
* // 작은 폰트 + 커스텀 클래스
* <DocumentWrapper fontSize="text-[11px]" className="leading-tight">
* ...
* </DocumentWrapper>
*/
export const DocumentWrapper = forwardRef<HTMLDivElement, DocumentWrapperProps>(
function DocumentWrapper({ children, fontSize = 'text-xs', printArea = true, className }, ref) {
return (
<div
ref={ref}
className={cn(
'bg-white p-8 min-h-full',
fontSize,
printArea && 'print-area',
className,
)}
>
{children}
</div>
);
},
);

View File

@@ -1,6 +1,8 @@
// 문서 공통 컴포넌트
export { ApprovalLine } from './ApprovalLine';
export { DocumentHeader } from './DocumentHeader';
export { DocumentWrapper } from './DocumentWrapper';
export { DocumentTable, DOC_STYLES } from './DocumentTable';
export { SectionHeader } from './SectionHeader';
export { InfoTable } from './InfoTable';
export { QualityApprovalTable } from './QualityApprovalTable';
@@ -11,6 +13,8 @@ export { SignatureSection } from './SignatureSection';
// Types
export type { ApprovalPerson, ApprovalLineProps } from './ApprovalLine';
export type { DocumentHeaderLogo, DocumentHeaderProps } from './DocumentHeader';
export type { DocumentWrapperProps } from './DocumentWrapper';
export type { DocumentTableProps } from './DocumentTable';
export type { SectionHeaderProps } from './SectionHeader';
export type { InfoTableCell, InfoTableProps } from './InfoTable';
export type {

View File

@@ -0,0 +1,67 @@
import { useCallback, useRef } from 'react';
import { printElement, printArea } from '@/lib/print-utils';
interface UsePrintHandlerOptions {
/** 인쇄 제목 (브라우저 다이얼로그에 표시) */
title?: string;
/** 추가 CSS 스타일 */
styles?: string;
}
interface UsePrintHandlerReturn {
/** ref를 할당한 요소를 인쇄 */
printRef: React.RefObject<HTMLDivElement | null>;
/** ref 기반 인쇄 실행 */
handlePrint: () => void;
/** .print-area 클래스 기반 인쇄 실행 */
handlePrintArea: () => void;
}
/**
* 문서 인쇄 훅
*
* ref 기반 또는 .print-area 기반으로 인쇄를 실행합니다.
*
* @example
* // ref 기반 사용
* function MyDocument() {
* const { printRef, handlePrint } = usePrintHandler({ title: '발주서' });
* return (
* <>
* <DocumentWrapper ref={printRef}>
* <DocumentHeader title="발주서" />
* ...
* </DocumentWrapper>
* <Button onClick={handlePrint}>인쇄</Button>
* </>
* );
* }
*
* @example
* // .print-area 기반 사용 (DocumentWrapper의 printArea prop 활용)
* function MyDocument() {
* const { handlePrintArea } = usePrintHandler({ title: '검사 성적서' });
* return (
* <>
* <DocumentWrapper>...</DocumentWrapper>
* <Button onClick={handlePrintArea}>인쇄</Button>
* </>
* );
* }
*/
export function usePrintHandler(options: UsePrintHandlerOptions = {}): UsePrintHandlerReturn {
const { title = '문서 인쇄', styles } = options;
const printRef = useRef<HTMLDivElement | null>(null);
const handlePrint = useCallback(() => {
if (printRef.current) {
printElement(printRef.current, { title, styles });
}
}, [title, styles]);
const handlePrintArea = useCallback(() => {
printArea({ title, styles });
}, [title, styles]);
return { printRef, handlePrint, handlePrintArea };
}

View File

@@ -5,6 +5,9 @@ export { DocumentViewer } from './viewer';
export {
ApprovalLine,
DocumentHeader,
DocumentWrapper,
DocumentTable,
DOC_STYLES,
SectionHeader,
InfoTable,
QualityApprovalTable,
@@ -15,6 +18,7 @@ export {
// Hooks
export { useZoom, useDrag } from './viewer/hooks';
export { usePrintHandler } from './hooks/usePrintHandler';
// Presets
export { DOCUMENT_PRESETS, getPreset, mergeWithPreset } from './presets';
@@ -26,6 +30,8 @@ export type {
ApprovalLineProps,
DocumentHeaderLogo,
DocumentHeaderProps,
DocumentWrapperProps,
DocumentTableProps,
SectionHeaderProps,
InfoTableCell,
InfoTableProps,