feat(WEB): 공정관리/작업지시/작업자화면 기능 강화 및 템플릿 개선
- 공정관리: ProcessDetail/ProcessForm/ProcessList 개선, StepDetail/StepForm 신규 추가 - 작업지시: WorkOrderDetail/Edit/List UI 개선, 작업지시서 문서 추가 - 작업자화면: WorkerScreen 대폭 개선, MaterialInputModal/WorkLogModal 수정, WorkItemCard 신규 - 영업주문: 주문 상세 페이지 개선 - 입고관리: 상세/actions 수정 - 템플릿: IntegratedDetailTemplate/IntegratedListTemplateV2/UniversalListPage 기능 확장 - UI: confirm-dialog 개선 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -54,7 +54,7 @@ export function FieldInput({
|
||||
field.readonly ||
|
||||
(typeof field.disabled === 'function'
|
||||
? field.disabled(mode)
|
||||
: field.disabled);
|
||||
: field.disabled) || false;
|
||||
|
||||
// 옵션 (동적 로드된 옵션 우선)
|
||||
const options = dynamicOptions || field.options || [];
|
||||
|
||||
@@ -48,7 +48,7 @@ export function FieldRenderer({
|
||||
field.readonly ||
|
||||
(typeof field.disabled === 'function'
|
||||
? field.disabled(mode)
|
||||
: field.disabled);
|
||||
: field.disabled) || false;
|
||||
|
||||
// 옵션 (동적 로드된 옵션 우선)
|
||||
const options = dynamicOptions || field.options || [];
|
||||
|
||||
@@ -320,7 +320,10 @@ function IntegratedDetailTemplateInner<T extends Record<string, unknown>>(
|
||||
|
||||
// ===== 액션 설정 =====
|
||||
const actions = config.actions || {};
|
||||
const deleteConfirm = actions.deleteConfirmMessage || {};
|
||||
const deleteConfirm = {
|
||||
title: actions.deleteConfirmMessage?.title || '삭제 확인',
|
||||
description: actions.deleteConfirmMessage?.description || '이 항목을 삭제하시겠습니까?',
|
||||
};
|
||||
|
||||
// ===== 버튼 위치 =====
|
||||
const isTopButtons = buttonPosition === 'top';
|
||||
|
||||
@@ -147,6 +147,7 @@ export interface PermissionConfig {
|
||||
}
|
||||
|
||||
// ===== 상세 페이지 설정 =====
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export interface DetailConfig<T = Record<string, unknown>> {
|
||||
/** 페이지 제목 */
|
||||
title: string;
|
||||
@@ -167,9 +168,9 @@ export interface DetailConfig<T = Record<string, unknown>> {
|
||||
/** 권한 설정 */
|
||||
permissions?: PermissionConfig;
|
||||
/** 초기값 변환 (API 응답 → formData) */
|
||||
transformInitialData?: (data: T) => Record<string, unknown>;
|
||||
transformInitialData?: (data: any) => Record<string, unknown>;
|
||||
/** 제출 데이터 변환 (formData → API 요청) */
|
||||
transformSubmitData?: (formData: Record<string, unknown>) => Partial<T>;
|
||||
transformSubmitData?: (formData: Record<string, unknown>) => any;
|
||||
}
|
||||
|
||||
// ===== 컴포넌트 Props =====
|
||||
|
||||
@@ -152,6 +152,8 @@ export interface IntegratedListTemplateV2Props<T = any> {
|
||||
tabs?: TabOption[];
|
||||
activeTab?: string;
|
||||
onTabChange?: (value: string) => void;
|
||||
/** 탭 렌더링 위치: 'card' (기본, 테이블 카드 내부) | 'above-stats' (통계 카드 위) */
|
||||
tabsPosition?: 'card' | 'above-stats';
|
||||
|
||||
// 테이블 헤더 액션 (탭 옆에 표시될 셀렉트박스 등)
|
||||
tableHeaderActions?: ReactNode;
|
||||
@@ -246,6 +248,7 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
tabs,
|
||||
activeTab,
|
||||
onTabChange,
|
||||
tabsPosition = 'card',
|
||||
tableHeaderActions,
|
||||
mobileFilterSlot,
|
||||
filterConfig,
|
||||
@@ -549,8 +552,8 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
<DateRangeSelector
|
||||
startDate={dateRangeSelector.startDate || ''}
|
||||
endDate={dateRangeSelector.endDate || ''}
|
||||
onStartDateChange={dateRangeSelector.onStartDateChange}
|
||||
onEndDateChange={dateRangeSelector.onEndDateChange}
|
||||
onStartDateChange={dateRangeSelector.onStartDateChange || (() => {})}
|
||||
onEndDateChange={dateRangeSelector.onEndDateChange || (() => {})}
|
||||
hidePresets={dateRangeSelector.showPresets === false}
|
||||
hideDateInputs={dateRangeSelector.hideDateInputs}
|
||||
extraActions={
|
||||
@@ -616,6 +619,24 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 탭 - 카드 밖 (tabsPosition === 'above-stats') */}
|
||||
{tabsPosition === 'above-stats' && tabs && tabs.length > 0 && (
|
||||
<div className="overflow-x-auto">
|
||||
<div className="flex gap-2 min-w-max">
|
||||
{tabs.map((tab) => (
|
||||
<TabChip
|
||||
key={tab.value}
|
||||
label={tab.label}
|
||||
count={tab.count}
|
||||
active={activeTab === tab.value}
|
||||
onClick={() => onTabChange?.(tab.value)}
|
||||
color={tab.color as any}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 통계 카드 - 태블릿/데스크톱 */}
|
||||
{stats && stats.length > 0 ? (
|
||||
<div className="hidden md:block">
|
||||
@@ -664,7 +685,7 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
<div className="hidden xl:block mb-4">
|
||||
<div className="flex flex-wrap gap-2 justify-between items-center">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{tabs && tabs.map((tab) => (
|
||||
{tabsPosition !== 'above-stats' && tabs && tabs.map((tab) => (
|
||||
<TabChip
|
||||
key={tab.value}
|
||||
label={tab.label}
|
||||
@@ -702,7 +723,7 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
</div>
|
||||
|
||||
{/* 모바일/태블릿 (~1279px) - TabChip 탭 */}
|
||||
{tabs && tabs.length > 0 && (
|
||||
{tabsPosition !== 'above-stats' && tabs && tabs.length > 0 && (
|
||||
<div className="xl:hidden mb-4 overflow-x-auto">
|
||||
<div className="flex gap-2 min-w-max">
|
||||
{tabs.map((tab) => (
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
IntegratedListTemplateV2,
|
||||
type PaginationConfig,
|
||||
} from '@/components/templates/IntegratedListTemplateV2';
|
||||
import { downloadExcel, downloadSelectedExcel } from '@/lib/utils/excel-download';
|
||||
import { downloadExcel, downloadSelectedExcel, type ExcelColumn } from '@/lib/utils/excel-download';
|
||||
import type {
|
||||
UniversalListPageProps,
|
||||
TabOption,
|
||||
@@ -634,7 +634,7 @@ export function UniversalListPage<T>({
|
||||
|
||||
downloadExcel({
|
||||
data: dataToDownload as Record<string, unknown>[],
|
||||
columns,
|
||||
columns: columns as ExcelColumn<Record<string, unknown>>[],
|
||||
filename,
|
||||
sheetName,
|
||||
});
|
||||
@@ -665,7 +665,7 @@ export function UniversalListPage<T>({
|
||||
|
||||
downloadSelectedExcel({
|
||||
data: selectedData as Record<string, unknown>[],
|
||||
columns,
|
||||
columns: columns as ExcelColumn<Record<string, unknown>>[],
|
||||
selectedIds,
|
||||
idField: 'id',
|
||||
filename: `${filename}_선택`,
|
||||
@@ -893,6 +893,7 @@ export function UniversalListPage<T>({
|
||||
tabs={computedTabs.length > 0 ? computedTabs : undefined}
|
||||
activeTab={activeTab}
|
||||
onTabChange={handleTabChange}
|
||||
tabsPosition={config.tabsPosition}
|
||||
// 필터 시스템
|
||||
filterConfig={config.filterConfig}
|
||||
filterValues={filterValuesObj}
|
||||
|
||||
@@ -123,7 +123,7 @@ export interface SelectionHandlers {
|
||||
|
||||
// ===== 행 클릭 핸들러 =====
|
||||
export interface RowClickHandlers<T> {
|
||||
onRowClick: (item: T) => void;
|
||||
onRowClick?: (item: T) => void;
|
||||
onEdit?: (item: T) => void;
|
||||
onDelete?: (item: T) => void;
|
||||
}
|
||||
@@ -205,6 +205,8 @@ export interface UniversalListConfig<T> {
|
||||
fetchTabs?: () => Promise<TabOption[]>;
|
||||
/** 기본 활성 탭 */
|
||||
defaultTab?: string;
|
||||
/** 탭 렌더링 위치: 'card' (기본, 테이블 카드 내부) | 'above-stats' (통계 카드 위) */
|
||||
tabsPosition?: 'card' | 'above-stats';
|
||||
|
||||
// ===== 통계 카드 =====
|
||||
/** 고정 통계 카드 */
|
||||
@@ -386,6 +388,8 @@ export interface UniversalListConfig<T> {
|
||||
extraFilters?: ReactNode;
|
||||
/** 선택 항목 변경 콜백 (외부에서 선택 상태 동기화 필요 시) */
|
||||
onSelectionChange?: (selectedItems: Set<string>) => void;
|
||||
/** 검색어 변경 콜백 (config 내부에서 설정, 서버 사이드 검색용) */
|
||||
onSearchChange?: (search: string) => void;
|
||||
|
||||
// ===== 커스텀 다이얼로그 슬롯 =====
|
||||
/**
|
||||
@@ -418,6 +422,7 @@ export interface ExternalSelection<T> {
|
||||
selectedItems: Set<string>;
|
||||
onToggleSelection: (id: string) => void;
|
||||
onToggleSelectAll: () => void;
|
||||
setSelectedItems?: (items: Set<string>) => void;
|
||||
getItemId: (item: T) => string;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user