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:
유병철
2026-01-29 22:56:01 +09:00
parent 106ce09482
commit 3fc63d0b3e
50 changed files with 5801 additions and 1377 deletions

View File

@@ -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 || [];

View File

@@ -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 || [];

View File

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

View File

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

View File

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

View File

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

View File

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