refactor: 모달 Content 컴포넌트 분리 및 파일 입력 UI 공통화
- 모달 컴포넌트에서 Content 분리하여 재사용성 향상 - EstimateDocumentContent, DirectConstructionContent 등 - WorkLogContent, QuotePreviewContent, ReceivingReceiptContent - 파일 입력 공통 UI 컴포넌트 추가 - file-dropzone, file-input, file-list, image-upload - 폼 컴포넌트 코드 정리 및 중복 제거 (-4,056줄) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
154
src/components/document-system/components/ApprovalLine.tsx
Normal file
154
src/components/document-system/components/ApprovalLine.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 결재란 공통 컴포넌트
|
||||
*
|
||||
* @example
|
||||
* // 3열 결재란 (작성/승인)
|
||||
* <ApprovalLine type="3col" writer="홍길동" writerDate="01/22" />
|
||||
*
|
||||
* // 4열 결재란 (작성/검토/승인)
|
||||
* <ApprovalLine type="4col" writer="홍길동" reviewer="김검토" />
|
||||
*
|
||||
* // 외부 송부 시 숨김
|
||||
* <ApprovalLine visible={false} />
|
||||
*/
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface ApprovalPerson {
|
||||
name?: string;
|
||||
date?: string;
|
||||
department?: string;
|
||||
}
|
||||
|
||||
export interface ApprovalLineProps {
|
||||
/** 결재란 유형: 3열(작성/승인) 또는 4열(작성/검토/승인) */
|
||||
type?: '3col' | '4col';
|
||||
/** 외부 송부 시 숨김 */
|
||||
visible?: boolean;
|
||||
/** 문서 모드: internal(내부), external(외부송부) */
|
||||
mode?: 'internal' | 'external';
|
||||
/** 작성자 정보 */
|
||||
writer?: ApprovalPerson;
|
||||
/** 검토자 정보 (4col에서만 사용) */
|
||||
reviewer?: ApprovalPerson;
|
||||
/** 승인자 정보 */
|
||||
approver?: ApprovalPerson;
|
||||
/** 부서명 표시 여부 */
|
||||
showDepartment?: boolean;
|
||||
/** 부서 라벨 (기본값: 작성/검토/승인에 따라 다름) */
|
||||
departmentLabels?: {
|
||||
writer?: string;
|
||||
reviewer?: string;
|
||||
approver?: string;
|
||||
};
|
||||
/** 추가 className */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ApprovalLine({
|
||||
type = '3col',
|
||||
visible = true,
|
||||
mode = 'internal',
|
||||
writer,
|
||||
reviewer,
|
||||
approver,
|
||||
showDepartment = true,
|
||||
departmentLabels = {
|
||||
writer: '판매/전진',
|
||||
reviewer: '생산',
|
||||
approver: '품질',
|
||||
},
|
||||
className,
|
||||
}: ApprovalLineProps) {
|
||||
// 외부 송부 모드이거나 visible이 false면 렌더링 안함
|
||||
if (!visible || mode === 'external') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const is4Col = type === '4col';
|
||||
|
||||
return (
|
||||
<table className={cn('border-collapse text-xs', className)}>
|
||||
<tbody>
|
||||
{/* 헤더 행: 결재 + 작성/검토/승인 */}
|
||||
<tr>
|
||||
<td
|
||||
rowSpan={showDepartment ? 3 : 2}
|
||||
className="w-8 text-center font-medium bg-gray-100 border border-gray-300 align-middle"
|
||||
>
|
||||
<div className="flex flex-col items-center px-1">
|
||||
<span>결</span>
|
||||
<span>재</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="w-16 p-2 text-center font-medium bg-gray-100 border border-gray-300">
|
||||
작성
|
||||
</td>
|
||||
{is4Col && (
|
||||
<td className="w-16 p-2 text-center font-medium bg-gray-100 border border-gray-300">
|
||||
검토
|
||||
</td>
|
||||
)}
|
||||
<td className="w-16 p-2 text-center font-medium bg-gray-100 border border-gray-300">
|
||||
승인
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{/* 서명 행: 이름 + 날짜 */}
|
||||
<tr>
|
||||
<td className="w-16 p-2 text-center border border-gray-300 h-10">
|
||||
{writer?.name && (
|
||||
<>
|
||||
<div>{writer.name}</div>
|
||||
{writer.date && (
|
||||
<div className="text-[10px] text-gray-500">{writer.date}</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
{is4Col && (
|
||||
<td className="w-16 p-2 text-center border border-gray-300 h-10">
|
||||
{reviewer?.name && (
|
||||
<>
|
||||
<div>{reviewer.name}</div>
|
||||
{reviewer.date && (
|
||||
<div className="text-[10px] text-gray-500">{reviewer.date}</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
<td className="w-16 p-2 text-center border border-gray-300 h-10">
|
||||
{approver?.name && (
|
||||
<>
|
||||
<div>{approver.name}</div>
|
||||
{approver.date && (
|
||||
<div className="text-[10px] text-gray-500">{approver.date}</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{/* 부서 행 (선택적) */}
|
||||
{showDepartment && (
|
||||
<tr>
|
||||
<td className="w-16 p-2 text-center bg-gray-50 border border-gray-300 text-[10px]">
|
||||
{departmentLabels.writer}
|
||||
</td>
|
||||
{is4Col && (
|
||||
<td className="w-16 p-2 text-center bg-gray-50 border border-gray-300 text-[10px]">
|
||||
{departmentLabels.reviewer}
|
||||
</td>
|
||||
)}
|
||||
<td className="w-16 p-2 text-center bg-gray-50 border border-gray-300 text-[10px]">
|
||||
{departmentLabels.approver}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
160
src/components/document-system/components/DocumentHeader.tsx
Normal file
160
src/components/document-system/components/DocumentHeader.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 문서 헤더 공통 컴포넌트
|
||||
*
|
||||
* @example
|
||||
* // 기본 사용
|
||||
* <DocumentHeader
|
||||
* title="작업일지"
|
||||
* documentCode="WL-SCR"
|
||||
* subtitle="스크린 생산부서"
|
||||
* approval={{ type: '4col', writer: { name: '홍길동', date: '01/22' } }}
|
||||
* />
|
||||
*
|
||||
* // 로고 포함
|
||||
* <DocumentHeader
|
||||
* logo={{ text: 'KD', subtext: '정동기업' }}
|
||||
* title="작업일지"
|
||||
* approval={{ type: '4col' }}
|
||||
* />
|
||||
*
|
||||
* // 결재선 위에 추가 정보
|
||||
* <DocumentHeader
|
||||
* title="견적서"
|
||||
* topInfo={<div>LOT: KD-WO-260122</div>}
|
||||
* approval={{ type: '3col' }}
|
||||
* />
|
||||
*
|
||||
* // 외부 송부 (결재선 숨김)
|
||||
* <DocumentHeader title="견적서" mode="external" />
|
||||
*/
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ApprovalLine, ApprovalLineProps } from './ApprovalLine';
|
||||
|
||||
export interface DocumentHeaderLogo {
|
||||
/** 로고 텍스트 (예: 'KD') */
|
||||
text: string;
|
||||
/** 로고 서브텍스트 (예: '정동기업') */
|
||||
subtext?: string;
|
||||
/** 로고 이미지 URL (text 대신 사용) */
|
||||
imageUrl?: string;
|
||||
}
|
||||
|
||||
export interface DocumentHeaderProps {
|
||||
/** 문서 제목 */
|
||||
title: string;
|
||||
/** 문서 코드 (예: 'WL-SCR') */
|
||||
documentCode?: string;
|
||||
/** 부제목 (예: '스크린 생산부서') */
|
||||
subtitle?: string;
|
||||
/** 로고 설정 */
|
||||
logo?: DocumentHeaderLogo;
|
||||
/** 결재선 위에 표시할 추가 정보 */
|
||||
topInfo?: ReactNode;
|
||||
/** 결재란 설정 (null이면 숨김) */
|
||||
approval?: Omit<ApprovalLineProps, 'mode'> | null;
|
||||
/** 문서 모드: internal(내부), external(외부송부) */
|
||||
mode?: 'internal' | 'external';
|
||||
/** 레이아웃 유형 */
|
||||
layout?: 'default' | 'centered' | 'simple';
|
||||
/** 추가 className */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function DocumentHeader({
|
||||
title,
|
||||
documentCode,
|
||||
subtitle,
|
||||
logo,
|
||||
topInfo,
|
||||
approval,
|
||||
mode = 'internal',
|
||||
layout = 'default',
|
||||
className,
|
||||
}: DocumentHeaderProps) {
|
||||
const isExternal = mode === 'external';
|
||||
const showApproval = approval !== null && !isExternal;
|
||||
|
||||
// 간단한 레이아웃 (제목만)
|
||||
if (layout === 'simple') {
|
||||
return (
|
||||
<div className={cn('mb-6', className)}>
|
||||
<h1 className="text-2xl font-bold text-center tracking-widest">{title}</h1>
|
||||
{documentCode && (
|
||||
<p className="text-sm text-gray-500 text-center mt-1">{documentCode}</p>
|
||||
)}
|
||||
{subtitle && (
|
||||
<p className="text-sm font-medium text-center mt-1">{subtitle}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 중앙 정렬 레이아웃 (견적서 스타일)
|
||||
if (layout === 'centered') {
|
||||
return (
|
||||
<div className={cn('flex justify-between items-start mb-6', className)}>
|
||||
<div className="flex-1">
|
||||
<h1 className="text-3xl font-bold text-center tracking-[0.3em]">{title}</h1>
|
||||
{(documentCode || subtitle) && (
|
||||
<div className="text-sm mt-4 text-center">
|
||||
{documentCode && <span>문서번호: {documentCode}</span>}
|
||||
{documentCode && subtitle && <span className="mx-2">|</span>}
|
||||
{subtitle && <span>{subtitle}</span>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{showApproval && approval && (
|
||||
<ApprovalLine {...approval} mode={mode} className="ml-4" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 기본 레이아웃 (로고 + 제목 + 결재란)
|
||||
return (
|
||||
<div className={cn('flex border border-gray-300 mb-6', className)}>
|
||||
{/* 좌측: 로고 영역 */}
|
||||
{logo && (
|
||||
<div className="w-24 border-r border-gray-300 flex flex-col items-center justify-center p-3 shrink-0">
|
||||
{logo.imageUrl ? (
|
||||
<img src={logo.imageUrl} alt={logo.text} className="h-8" />
|
||||
) : (
|
||||
<span className="text-2xl font-bold">{logo.text}</span>
|
||||
)}
|
||||
{logo.subtext && (
|
||||
<span className="text-xs text-gray-500">{logo.subtext}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 중앙: 문서 제목 */}
|
||||
<div className="flex-1 flex flex-col items-center justify-center p-3 border-r border-gray-300">
|
||||
<h1 className="text-xl font-bold tracking-widest mb-1">{title}</h1>
|
||||
{documentCode && (
|
||||
<p className="text-xs text-gray-500">{documentCode}</p>
|
||||
)}
|
||||
{subtitle && (
|
||||
<p className="text-sm font-medium mt-1">{subtitle}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 우측: 결재선 위 정보 + 결재란 */}
|
||||
{(showApproval || topInfo) && (
|
||||
<div className="shrink-0 flex flex-col">
|
||||
{topInfo && (
|
||||
<div className="p-2 text-xs border-b border-gray-300">
|
||||
{topInfo}
|
||||
</div>
|
||||
)}
|
||||
{showApproval && approval && (
|
||||
<ApprovalLine {...approval} mode={mode} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
94
src/components/document-system/components/InfoTable.tsx
Normal file
94
src/components/document-system/components/InfoTable.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 정보 테이블 공통 컴포넌트 (라벨-값 구조)
|
||||
*
|
||||
* @example
|
||||
* <InfoTable
|
||||
* rows={[
|
||||
* [{ label: '발주처', value: '(주)현대건설' }, { label: '현장명', value: '판교 테크노밸리' }],
|
||||
* [{ label: '작업일자', value: '2026-01-22' }, { label: 'LOT NO.', value: 'KD-WO-260122' }],
|
||||
* ]}
|
||||
* />
|
||||
*
|
||||
* // 단일 열
|
||||
* <InfoTable
|
||||
* columns={1}
|
||||
* rows={[
|
||||
* [{ label: '비고', value: '특이사항 없음' }],
|
||||
* ]}
|
||||
* />
|
||||
*/
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface InfoTableCell {
|
||||
/** 라벨 */
|
||||
label: string;
|
||||
/** 값 (문자열 또는 ReactNode) */
|
||||
value: ReactNode;
|
||||
/** 라벨 너비 (기본: w-24) */
|
||||
labelWidth?: string;
|
||||
/** 값 colSpan */
|
||||
colSpan?: number;
|
||||
}
|
||||
|
||||
export interface InfoTableProps {
|
||||
/** 행 데이터 (2차원 배열: 행 > 셀) */
|
||||
rows: InfoTableCell[][];
|
||||
/** 열 개수 (기본: 2) */
|
||||
columns?: 1 | 2 | 3 | 4;
|
||||
/** 라벨 너비 (기본: w-24) */
|
||||
labelWidth?: string;
|
||||
/** 추가 className */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function InfoTable({
|
||||
rows,
|
||||
columns = 2,
|
||||
labelWidth = 'w-24',
|
||||
className,
|
||||
}: InfoTableProps) {
|
||||
return (
|
||||
<div className={cn('border border-gray-300', className)}>
|
||||
{rows.map((row, rowIndex) => (
|
||||
<div
|
||||
key={rowIndex}
|
||||
className={cn(
|
||||
'grid',
|
||||
columns === 1 && 'grid-cols-1',
|
||||
columns === 2 && 'grid-cols-2',
|
||||
columns === 3 && 'grid-cols-3',
|
||||
columns === 4 && 'grid-cols-4',
|
||||
rowIndex < rows.length - 1 && 'border-b border-gray-300'
|
||||
)}
|
||||
>
|
||||
{row.map((cell, cellIndex) => (
|
||||
<div
|
||||
key={cellIndex}
|
||||
className={cn(
|
||||
'flex',
|
||||
cellIndex < row.length - 1 && 'border-r border-gray-300'
|
||||
)}
|
||||
style={cell.colSpan ? { gridColumn: `span ${cell.colSpan}` } : undefined}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'bg-gray-100 p-3 text-sm font-medium border-r border-gray-300 flex items-center shrink-0',
|
||||
cell.labelWidth || labelWidth
|
||||
)}
|
||||
>
|
||||
{cell.label}
|
||||
</div>
|
||||
<div className="flex-1 p-3 text-sm flex items-center">
|
||||
{cell.value}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
45
src/components/document-system/components/SectionHeader.tsx
Normal file
45
src/components/document-system/components/SectionHeader.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 섹션 헤더 공통 컴포넌트
|
||||
*
|
||||
* @example
|
||||
* <SectionHeader>작업내역</SectionHeader>
|
||||
* <SectionHeader variant="light">특이사항</SectionHeader>
|
||||
*/
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface SectionHeaderProps {
|
||||
/** 섹션 제목 */
|
||||
children: ReactNode;
|
||||
/** 스타일 변형: dark(검정), light(회색), primary(파랑) */
|
||||
variant?: 'dark' | 'light' | 'primary';
|
||||
/** 추가 className */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const variantStyles = {
|
||||
dark: 'bg-gray-800 text-white',
|
||||
light: 'bg-gray-100 text-gray-800 border-b border-gray-300',
|
||||
primary: 'bg-blue-600 text-white',
|
||||
};
|
||||
|
||||
export function SectionHeader({
|
||||
children,
|
||||
variant = 'dark',
|
||||
className,
|
||||
}: SectionHeaderProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'p-2.5 text-sm font-medium text-center',
|
||||
variantStyles[variant],
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
11
src/components/document-system/components/index.ts
Normal file
11
src/components/document-system/components/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
// 문서 공통 컴포넌트
|
||||
export { ApprovalLine } from './ApprovalLine';
|
||||
export { DocumentHeader } from './DocumentHeader';
|
||||
export { SectionHeader } from './SectionHeader';
|
||||
export { InfoTable } from './InfoTable';
|
||||
|
||||
// Types
|
||||
export type { ApprovalPerson, ApprovalLineProps } from './ApprovalLine';
|
||||
export type { DocumentHeaderLogo, DocumentHeaderProps } from './DocumentHeader';
|
||||
export type { SectionHeaderProps } from './SectionHeader';
|
||||
export type { InfoTableCell, InfoTableProps } from './InfoTable';
|
||||
@@ -1,6 +1,14 @@
|
||||
// Main Component
|
||||
export { DocumentViewer } from './viewer';
|
||||
|
||||
// Document Components (공통 문서 요소)
|
||||
export {
|
||||
ApprovalLine,
|
||||
DocumentHeader,
|
||||
SectionHeader,
|
||||
InfoTable,
|
||||
} from './components';
|
||||
|
||||
// Hooks
|
||||
export { useZoom, useDrag } from './viewer/hooks';
|
||||
|
||||
@@ -8,6 +16,17 @@ export { useZoom, useDrag } from './viewer/hooks';
|
||||
export { DOCUMENT_PRESETS, getPreset, mergeWithPreset } from './presets';
|
||||
|
||||
// Types
|
||||
export type {
|
||||
// Document Component Types
|
||||
ApprovalPerson,
|
||||
ApprovalLineProps,
|
||||
DocumentHeaderLogo,
|
||||
DocumentHeaderProps,
|
||||
SectionHeaderProps,
|
||||
InfoTableCell,
|
||||
InfoTableProps,
|
||||
} from './components';
|
||||
|
||||
export type {
|
||||
DocumentConfig,
|
||||
DocumentViewerProps,
|
||||
|
||||
Reference in New Issue
Block a user