370 lines
9.0 KiB
JavaScript
370 lines
9.0 KiB
JavaScript
import React from 'react';
|
|
import { Row, Col, Form, Input, Select, DatePicker, TimePicker, InputNumber, Switch, Button, Checkbox } from 'antd';
|
|
import styled from 'styled-components';
|
|
import dayjs from 'dayjs';
|
|
import { AnimatedTabs } from '../index';
|
|
const { RangePicker } = DatePicker;
|
|
const { TextArea } = Input;
|
|
|
|
/**
|
|
* 위치 지정 가능한 그리드 형태 상세 정보 표시 컴포넌트
|
|
* @param {Array} items - 표시할 항목 배열 (row, col, rowSpan, colSpan 속성 추가)
|
|
* @param {Object} formData - 폼 데이터 객체
|
|
* @param {Function} onChange - 값 변경 시 호출할 함수
|
|
* @param {boolean} disabled - 전체 비활성화 여부
|
|
* @param {number} columns - 그리드의 총 컬럼 수 (기본값: 4)
|
|
*/
|
|
const DetailGrid = ({ items, formData, onChange, disabled = false, columns = 4 }) => {
|
|
// 항목을 행과 열 위치별로 그룹화
|
|
const positionedItems = {};
|
|
|
|
// 각 항목의 위치 및 span 정보 처리
|
|
items.forEach(item => {
|
|
const rowIndex = item.row || 0;
|
|
const colIndex = item.col || 0;
|
|
|
|
if (!positionedItems[rowIndex]) {
|
|
positionedItems[rowIndex] = {};
|
|
}
|
|
|
|
positionedItems[rowIndex][colIndex] = {
|
|
...item,
|
|
rowSpan: item.rowSpan || 1,
|
|
colSpan: item.colSpan || 1
|
|
};
|
|
});
|
|
|
|
// 행 번호 목록 (정렬)
|
|
const rows = Object.keys(positionedItems).map(Number).sort((a, b) => a - b);
|
|
|
|
// 항목에 따른 컴포넌트 렌더링 함수
|
|
const renderComponent = (item) => {
|
|
const {
|
|
type,
|
|
key,
|
|
keys,
|
|
label,
|
|
value,
|
|
options,
|
|
placeholder,
|
|
disabled: itemDisabled,
|
|
width,
|
|
handler,
|
|
min,
|
|
max,
|
|
step,
|
|
format,
|
|
required,
|
|
showTime,
|
|
tabItems,
|
|
activeKey,
|
|
onTabChange,
|
|
maxLength,
|
|
rows: textareaRows
|
|
} = item;
|
|
|
|
// 현재 값 가져오기 (formData에서 또는 항목에서)
|
|
const currentValue = formData[key] !== undefined ? formData[key] : value;
|
|
|
|
// 컴포넌트 공통 속성
|
|
const commonProps = {
|
|
id: key,
|
|
disabled: disabled || itemDisabled,
|
|
style: width ? { width, fontSize: '15px' } : { width: '100%', fontSize: '15px' }
|
|
};
|
|
|
|
// 항목 타입에 따른 컴포넌트 렌더링
|
|
switch (type) {
|
|
case 'text':
|
|
return <Input
|
|
{...commonProps}
|
|
value={currentValue}
|
|
onChange={(e) => onChange(key, e.target.value, handler)}
|
|
placeholder={placeholder || `${label} 입력`}
|
|
/>;
|
|
|
|
case 'number':
|
|
return <InputNumber
|
|
{...commonProps}
|
|
value={currentValue}
|
|
min={min}
|
|
max={max}
|
|
step={step || 1}
|
|
onChange={(value) => onChange(key, value, handler)}
|
|
placeholder={placeholder || `${label} 입력`}
|
|
/>;
|
|
|
|
case 'display':
|
|
return <Input
|
|
{...commonProps}
|
|
value={currentValue || ''}
|
|
readOnly
|
|
style={{
|
|
...commonProps.style,
|
|
backgroundColor: '#f5f5f5',
|
|
cursor: 'default'
|
|
}}
|
|
placeholder={placeholder || ''}
|
|
/>;
|
|
|
|
case 'textarea':
|
|
return <TextArea
|
|
{...commonProps}
|
|
value={currentValue || ''}
|
|
onChange={(e) => onChange(key, e.target.value, handler)}
|
|
placeholder={placeholder}
|
|
maxLength={maxLength}
|
|
rows={textareaRows || 4}
|
|
showCount={!!maxLength}
|
|
/>;
|
|
|
|
case 'select':
|
|
return (
|
|
<Select
|
|
{...commonProps}
|
|
value={currentValue}
|
|
onChange={(value) => onChange(key, value, handler)}
|
|
placeholder={placeholder || `${label} 선택`}
|
|
popupMatchSelectWidth={false}
|
|
>
|
|
{options && options.map((option) => (
|
|
<Select.Option key={option.value} value={option.value}>
|
|
{option.name}
|
|
</Select.Option>
|
|
))}
|
|
</Select>
|
|
);
|
|
|
|
case 'date':
|
|
return (
|
|
<DatePicker
|
|
{...commonProps}
|
|
allowClear={false}
|
|
showTime={showTime || false}
|
|
value={currentValue ? dayjs(currentValue) : null}
|
|
format={format || 'YYYY-MM-DD'}
|
|
onChange={(date) => onChange(key, date, handler)}
|
|
placeholder={placeholder || `${label} 선택`}
|
|
/>
|
|
);
|
|
|
|
case 'dateRange':
|
|
return (
|
|
<RangePicker
|
|
{...commonProps}
|
|
showTime={showTime !== false}
|
|
value={keys ? [
|
|
formData[keys.start] ? dayjs(formData[keys.start]) : null,
|
|
formData[keys.end] ? dayjs(formData[keys.end]) : null
|
|
] : (currentValue ? [
|
|
currentValue.start ? dayjs(currentValue.start) : null,
|
|
currentValue.end ? dayjs(currentValue.end) : null
|
|
] : null)}
|
|
format={format || 'YYYY-MM-DD HH:mm:ss'}
|
|
onChange={(dates) => {
|
|
if (dates && dates.length === 2) {
|
|
// 두 개의 별도 필드에 각각 업데이트
|
|
if (item.keys) {
|
|
// 두 개의 onChange를 순차적으로 호출하는 대신
|
|
// 한 번에 두 필드를 모두 업데이트하는 방식으로 변경
|
|
const updatedData = {
|
|
...formData,
|
|
[item.keys.start]: dates[0],
|
|
[item.keys.end]: dates[1]
|
|
};
|
|
|
|
// handler가 있으면 handler 실행, 없으면 직접 onChange 호출
|
|
if (handler) {
|
|
handler(dates, key, updatedData);
|
|
} else {
|
|
// onChange를 통해 전체 업데이트된 데이터를 전달
|
|
onChange('dateRange_update', updatedData, null);
|
|
}
|
|
} else {
|
|
// 기존 방식 지원 (하위 호환성)
|
|
onChange(key, {
|
|
start: dates[0],
|
|
end: dates[1]
|
|
}, handler);
|
|
}
|
|
} else {
|
|
// 두 필드 모두 비우기
|
|
if (item.keys) {
|
|
const updatedData = {
|
|
...formData,
|
|
[item.keys.start]: null,
|
|
[item.keys.end]: null
|
|
};
|
|
|
|
if (handler) {
|
|
handler(null, key, updatedData);
|
|
} else {
|
|
onChange('dateRange_update', updatedData, null);
|
|
}
|
|
} else {
|
|
onChange(key, { start: null, end: null }, handler);
|
|
}
|
|
}
|
|
}}
|
|
placeholder={[
|
|
item.startLabel || '시작 일시',
|
|
item.endLabel || '종료 일시'
|
|
]}
|
|
/>
|
|
);
|
|
|
|
case 'time':
|
|
return (
|
|
<TimePicker
|
|
{...commonProps}
|
|
value={currentValue ? dayjs(currentValue, 'HH:mm') : null}
|
|
format={format || 'HH:mm'}
|
|
onChange={(time) => onChange(key, time, handler)}
|
|
placeholder={placeholder || `${label} 선택`}
|
|
/>
|
|
);
|
|
|
|
case 'switch':
|
|
return (
|
|
<Switch
|
|
checked={currentValue}
|
|
onChange={(checked) => onChange(key, checked, handler)}
|
|
/>
|
|
);
|
|
|
|
case 'checkbox':
|
|
return (
|
|
<Checkbox
|
|
checked={currentValue}
|
|
disabled={disabled || itemDisabled}
|
|
onChange={(e) => onChange(key, e.target.checked, handler)}
|
|
>
|
|
{item.checkboxLabel}
|
|
</Checkbox>
|
|
);
|
|
|
|
case 'status':
|
|
return <StatusDisplay status={currentValue} />;
|
|
|
|
case 'tab':
|
|
return <AnimatedTabs
|
|
items={tabItems}
|
|
activeKey={activeKey}
|
|
onChange={onTabChange}
|
|
/>
|
|
|
|
case 'custom':
|
|
return item.render ? item.render(formData, onChange) : null;
|
|
|
|
case 'label':
|
|
default:
|
|
return <div style={{
|
|
padding: '4px 11px',
|
|
minHeight: '32px',
|
|
lineHeight: '24px',
|
|
fontSize: '15px',
|
|
color: currentValue ? '#000' : '#bfbfbf'
|
|
}}>
|
|
{currentValue || placeholder || ''}
|
|
</div>;
|
|
}
|
|
};
|
|
|
|
// 각 셀의 폭 계산 (Ant Design의 24-컬럼 시스템 기준)
|
|
const colWidth = 24 / columns;
|
|
|
|
return (
|
|
<GridContainer>
|
|
<Form layout="horizontal">
|
|
{rows.map(rowIndex => {
|
|
const rowItems = positionedItems[rowIndex];
|
|
const cols = Object.keys(rowItems).map(Number).sort((a, b) => a - b);
|
|
|
|
return (
|
|
<Row key={`row-${rowIndex}`} gutter={[16, 16]}>
|
|
{cols.map(colIndex => {
|
|
const item = rowItems[colIndex];
|
|
const itemColSpan = Math.min(item.colSpan * colWidth, 24);
|
|
|
|
return (
|
|
<Col
|
|
key={`${item.key}-${rowIndex}-${colIndex}`}
|
|
span={itemColSpan}
|
|
xs={24}
|
|
sm={itemColSpan}
|
|
>
|
|
<Form.Item
|
|
label={item.label}
|
|
required={item.required}
|
|
tooltip={item.tooltip}
|
|
>
|
|
{renderComponent(item)}
|
|
</Form.Item>
|
|
</Col>
|
|
);
|
|
})}
|
|
</Row>
|
|
);
|
|
})}
|
|
</Form>
|
|
</GridContainer>
|
|
);
|
|
};
|
|
|
|
// 상태 표시 컴포넌트
|
|
const StatusDisplay = ({ status }) => {
|
|
let color = '';
|
|
let text = '';
|
|
|
|
const lowerStatus = typeof status === 'string' ? status.toLowerCase() : status;
|
|
|
|
switch (lowerStatus) {
|
|
case 'wait':
|
|
color = '#FAAD14';
|
|
text = '대기';
|
|
break;
|
|
case 'running':
|
|
color = '#4287f5';
|
|
text = '진행중';
|
|
break;
|
|
case 'finish':
|
|
color = '#d9d9d9';
|
|
text = '만료';
|
|
break;
|
|
case 'fail':
|
|
color = '#ff4d4f';
|
|
text = '실패';
|
|
break;
|
|
case 'delete':
|
|
color = '#ff4d4f';
|
|
text = '삭제';
|
|
break;
|
|
default:
|
|
color = '#DEBB46';
|
|
text = status;
|
|
}
|
|
|
|
return (
|
|
<StatusTag color={color}>
|
|
{text}
|
|
</StatusTag>
|
|
);
|
|
};
|
|
|
|
const GridContainer = styled.div`
|
|
width: 100%;
|
|
padding: 16px 0;
|
|
font-size: 15px;
|
|
`;
|
|
|
|
const StatusTag = styled.div`
|
|
display: inline-block;
|
|
padding: 2px 8px;
|
|
border-radius: 4px;
|
|
background-color: ${props => props.color};
|
|
color: white;
|
|
font-size: 15px;
|
|
font-weight: 500;
|
|
`;
|
|
|
|
export default DetailGrid; |