Files
operationSystem-front/src/components/common/Layout/DetailGrid.js
bcjang e25bcdc86e 선택 드랍다운 넓이 수정
아이템 백과사전 추가
2025-09-04 10:37:50 +09:00

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;