detailGrid 탭 추가
This commit is contained in:
@@ -1,526 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { getOptionsArray } from '../../../utils';
|
|
||||||
import {
|
|
||||||
SelectInput,
|
|
||||||
SearchBarAlert
|
|
||||||
} from '../../../styles/Components';
|
|
||||||
import {
|
|
||||||
FormInput, FormInputSuffix, FormInputSuffixWrapper,
|
|
||||||
FormLabel,
|
|
||||||
FormRowGroup,
|
|
||||||
FormStatusBar,
|
|
||||||
FormStatusLabel,
|
|
||||||
FormStatusWarning,
|
|
||||||
} from '../../../styles/ModuleComponents';
|
|
||||||
import { CheckBox, SingleDatePicker, SingleTimePicker } from '../../common';
|
|
||||||
import Button from '../../common/button/Button';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import ImageUploadBtn from '../../ServiceManage/ImageUploadBtn';
|
|
||||||
|
|
||||||
const CaliForm = ({
|
|
||||||
config, // 폼 설정 JSON
|
|
||||||
mode, // 'create', 'update', 'view' 중 하나
|
|
||||||
initialData, // 초기 데이터
|
|
||||||
externalData, // 외부 데이터(옵션 등)
|
|
||||||
onSubmit, // 제출 핸들러
|
|
||||||
onCancel, // 취소 핸들러
|
|
||||||
className, // 추가 CSS 클래스
|
|
||||||
onFieldValidation, // 필드 유효성 검사 콜백
|
|
||||||
formRef // 폼 ref
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [formData, setFormData] = useState({ ...(config?.initData || {}), ...(initialData || {}) });
|
|
||||||
const [errors, setErrors] = useState({});
|
|
||||||
const [isFormValid, setIsFormValid] = useState(false);
|
|
||||||
|
|
||||||
// 필드 변경 핸들러
|
|
||||||
const handleFieldChange = (fieldId, value) => {
|
|
||||||
setFormData(prev => ({
|
|
||||||
...prev,
|
|
||||||
[fieldId]: value
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
// 날짜 변경 핸들러
|
|
||||||
const handleDateChange = (fieldId, date) => {
|
|
||||||
if (!date) return;
|
|
||||||
setFormData(prev => ({
|
|
||||||
...prev,
|
|
||||||
[fieldId]: date
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
// 시간 변경 핸들러
|
|
||||||
const handleTimeChange = (fieldId, time) => {
|
|
||||||
if (!time) return;
|
|
||||||
|
|
||||||
const newDateTime = formData[fieldId] ? new Date(formData[fieldId]) : new Date();
|
|
||||||
newDateTime.setHours(time.getHours(), time.getMinutes(), 0, 0);
|
|
||||||
|
|
||||||
setFormData(prev => ({
|
|
||||||
...prev,
|
|
||||||
[fieldId]: newDateTime
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
// 폼 유효성 검사
|
|
||||||
useEffect(() => {
|
|
||||||
const validateForm = () => {
|
|
||||||
const newErrors = {};
|
|
||||||
let isValid = true;
|
|
||||||
|
|
||||||
if (!config) return false;
|
|
||||||
|
|
||||||
// 필수 필드 검사
|
|
||||||
const requiredFields = config.fields
|
|
||||||
.filter(f =>
|
|
||||||
f.visibleOn.includes(mode) &&
|
|
||||||
f.validations?.includes("required")
|
|
||||||
)
|
|
||||||
.map(f => f.id);
|
|
||||||
|
|
||||||
requiredFields.forEach(fieldId => {
|
|
||||||
if (!formData[fieldId] && formData[fieldId] !== 0) {
|
|
||||||
newErrors[fieldId] = t('REQUIRED_FIELD');
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 조건부 유효성 검사
|
|
||||||
if (config.validations && config.validations[mode]) {
|
|
||||||
for (const validation of config.validations[mode]) {
|
|
||||||
const conditionResult = evaluateCondition(validation.condition, {
|
|
||||||
...formData,
|
|
||||||
current_time: new Date().getTime()
|
|
||||||
});
|
|
||||||
|
|
||||||
if (conditionResult) {
|
|
||||||
// 전체 폼 검증 오류
|
|
||||||
newErrors._form = t(validation.message);
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setErrors(newErrors);
|
|
||||||
setIsFormValid(isValid);
|
|
||||||
|
|
||||||
if (onFieldValidation) {
|
|
||||||
onFieldValidation(isValid, newErrors);
|
|
||||||
}
|
|
||||||
|
|
||||||
return isValid;
|
|
||||||
};
|
|
||||||
|
|
||||||
validateForm();
|
|
||||||
}, [config, formData, mode, t, onFieldValidation]);
|
|
||||||
|
|
||||||
// 간단한 조건식 평가 함수
|
|
||||||
const evaluateCondition = (conditionStr, context) => {
|
|
||||||
try {
|
|
||||||
const fn = new Function(...Object.keys(context), `return ${conditionStr}`);
|
|
||||||
return fn(...Object.values(context));
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error evaluating condition:', e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 필드 렌더링
|
|
||||||
const renderField = (field) => {
|
|
||||||
const isEditable = field.editableOn.includes(mode);
|
|
||||||
const value = formData[field.id] !== undefined ? formData[field.id] : '';
|
|
||||||
const hasError = errors[field.id];
|
|
||||||
|
|
||||||
switch (field.type) {
|
|
||||||
case 'text':
|
|
||||||
return (
|
|
||||||
<div className="form-field">
|
|
||||||
<FormInput
|
|
||||||
type="text"
|
|
||||||
value={value}
|
|
||||||
onChange={e => handleFieldChange(field.id, e.target.value)}
|
|
||||||
disabled={!isEditable}
|
|
||||||
width={field.width}
|
|
||||||
className={hasError ? 'error' : ''}
|
|
||||||
/>
|
|
||||||
{hasError && <div className="field-error">{hasError}</div>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'number':
|
|
||||||
return (
|
|
||||||
<div className="form-field">
|
|
||||||
<FormInput
|
|
||||||
type="number"
|
|
||||||
value={value}
|
|
||||||
onChange={e => handleFieldChange(field.id, Number(e.target.value))}
|
|
||||||
disabled={!isEditable}
|
|
||||||
width={field.width}
|
|
||||||
min={field.min}
|
|
||||||
max={field.max}
|
|
||||||
step={field.step || 1}
|
|
||||||
className={hasError ? 'error' : ''}
|
|
||||||
/>
|
|
||||||
{hasError && <div className="field-error">{hasError}</div>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'select':
|
|
||||||
let options = [];
|
|
||||||
|
|
||||||
if (field.optionsKey) {
|
|
||||||
// 옵션 설정에서 가져오기
|
|
||||||
options = getOptionsArray(field.optionsKey);
|
|
||||||
} else if (field.dataSource && externalData) {
|
|
||||||
// 외부 데이터 소스 사용
|
|
||||||
const dataSource = externalData[field.dataSource] || [];
|
|
||||||
|
|
||||||
options = dataSource.map(item => ({
|
|
||||||
value: item[field.valueField],
|
|
||||||
label: field.displayFormat
|
|
||||||
? field.displayFormat.replace('{value}', item[field.valueField])
|
|
||||||
.replace('{display}', item[field.displayField])
|
|
||||||
: `${item[field.displayField]}(${item[field.valueField]})`
|
|
||||||
}));
|
|
||||||
} else if (field.options) {
|
|
||||||
options = field.options;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="form-field">
|
|
||||||
<SelectInput
|
|
||||||
value={value}
|
|
||||||
onChange={e => handleFieldChange(field.id, e.target.value)}
|
|
||||||
disabled={!isEditable}
|
|
||||||
width={field.width}
|
|
||||||
className={hasError ? 'error' : ''}
|
|
||||||
>
|
|
||||||
{options.map((option, index) => (
|
|
||||||
<option key={index} value={option.value}>
|
|
||||||
{option.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</SelectInput>
|
|
||||||
{hasError && <div className="field-error">{hasError}</div>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'datePicker':
|
|
||||||
return (
|
|
||||||
<div className="form-field">
|
|
||||||
<SingleDatePicker
|
|
||||||
label={field.label}
|
|
||||||
disabled={!isEditable}
|
|
||||||
dateLabel={field.dateLabel}
|
|
||||||
onDateChange={date => handleDateChange(field.id, date)}
|
|
||||||
selectedDate={value}
|
|
||||||
minDate={field.minDate}
|
|
||||||
maxDate={field.maxDate}
|
|
||||||
className={hasError ? 'error' : ''}
|
|
||||||
/>
|
|
||||||
{hasError && <div className="field-error">{hasError}</div>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'timePicker':
|
|
||||||
return (
|
|
||||||
<div className="form-field">
|
|
||||||
<SingleTimePicker
|
|
||||||
label={field.label}
|
|
||||||
disabled={!isEditable}
|
|
||||||
selectedTime={value}
|
|
||||||
onTimeChange={time => handleTimeChange(field.id, time)}
|
|
||||||
className={hasError ? 'error' : ''}
|
|
||||||
/>
|
|
||||||
{hasError && <div className="field-error">{hasError}</div>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'status':
|
|
||||||
let statusText = "";
|
|
||||||
if (field.optionsKey && formData[field.statusField]) {
|
|
||||||
const statusOptions = getOptionsArray(field.optionsKey);
|
|
||||||
const statusItem = statusOptions.find(item => item.value === formData[field.statusField]);
|
|
||||||
statusText = statusItem ? statusItem.name : "등록";
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="form-field">
|
|
||||||
<FormStatusBar>
|
|
||||||
<FormStatusLabel>
|
|
||||||
{field.label}: {statusText}
|
|
||||||
</FormStatusLabel>
|
|
||||||
{mode === 'update' && field.warningMessage && (
|
|
||||||
<FormStatusWarning>
|
|
||||||
{t(field.warningMessage)}
|
|
||||||
</FormStatusWarning>
|
|
||||||
)}
|
|
||||||
</FormStatusBar>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'dateTimeRange':
|
|
||||||
return (
|
|
||||||
<div className="form-field">
|
|
||||||
<div className="date-time-range">
|
|
||||||
<SingleDatePicker
|
|
||||||
label={field.startDateLabel}
|
|
||||||
disabled={!isEditable}
|
|
||||||
dateLabel={field.startDateLabel}
|
|
||||||
onDateChange={date => handleDateChange(field.startDateField, date)}
|
|
||||||
selectedDate={formData[field.startDateField]}
|
|
||||||
/>
|
|
||||||
<SingleTimePicker
|
|
||||||
disabled={!isEditable}
|
|
||||||
selectedTime={formData[field.startDateField]}
|
|
||||||
onTimeChange={time => handleTimeChange(field.startDateField, time)}
|
|
||||||
/>
|
|
||||||
<SingleDatePicker
|
|
||||||
label={field.endDateLabel}
|
|
||||||
disabled={!isEditable}
|
|
||||||
dateLabel={field.endDateLabel}
|
|
||||||
onDateChange={date => handleDateChange(field.endDateField, date)}
|
|
||||||
selectedDate={formData[field.endDateField]}
|
|
||||||
/>
|
|
||||||
<SingleTimePicker
|
|
||||||
disabled={!isEditable}
|
|
||||||
selectedTime={formData[field.endDateField]}
|
|
||||||
onTimeChange={time => handleTimeChange(field.endDateField, time)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{hasError && <div className="field-error">{hasError}</div>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'imageUpload':
|
|
||||||
const imageLanguage = field.language;
|
|
||||||
const imageList = formData.image_list || [];
|
|
||||||
const imageData = imageList.find(img => img.language === imageLanguage) || { content: '' };
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="form-field">
|
|
||||||
<LanguageWrapper>
|
|
||||||
<LanguageLabel>{imageLanguage}</LanguageLabel>
|
|
||||||
<ImageUploadBtn
|
|
||||||
onImageUpload={(file, fileName) => {
|
|
||||||
const updatedImageList = [...imageList];
|
|
||||||
const index = updatedImageList.findIndex(img => img.language === imageLanguage);
|
|
||||||
|
|
||||||
if (index !== -1) {
|
|
||||||
updatedImageList[index] = {
|
|
||||||
...updatedImageList[index],
|
|
||||||
content: fileName
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
updatedImageList.push({
|
|
||||||
language: imageLanguage,
|
|
||||||
content: fileName
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleFieldChange('image_list', updatedImageList);
|
|
||||||
}}
|
|
||||||
onFileDelete={() => {
|
|
||||||
const updatedImageList = [...imageList];
|
|
||||||
const index = updatedImageList.findIndex(img => img.language === imageLanguage);
|
|
||||||
|
|
||||||
if (index !== -1) {
|
|
||||||
updatedImageList[index] = {
|
|
||||||
...updatedImageList[index],
|
|
||||||
content: ''
|
|
||||||
};
|
|
||||||
|
|
||||||
handleFieldChange('image_list', updatedImageList);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
fileName={imageData.content}
|
|
||||||
disabled={!isEditable}
|
|
||||||
/>
|
|
||||||
</LanguageWrapper>
|
|
||||||
{hasError && <div className="field-error">{hasError}</div>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'checkbox':
|
|
||||||
return (
|
|
||||||
<div className="form-field">
|
|
||||||
<CheckBox
|
|
||||||
label={field.label}
|
|
||||||
id={field.id}
|
|
||||||
checked={formData[field.id] || false}
|
|
||||||
setData={e => handleFieldChange(field.id, e.target.checked)}
|
|
||||||
disabled={!isEditable}
|
|
||||||
/>
|
|
||||||
{hasError && <div className="field-error">{hasError}</div>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'textWithSuffix':
|
|
||||||
const linkLanguage = field.suffix;
|
|
||||||
const linkList = formData.link_list || [];
|
|
||||||
const linkData = linkList.find(link => link.language === linkLanguage) || { content: '' };
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="form-field">
|
|
||||||
{field.label && <FormLabel>{field.label}</FormLabel>}
|
|
||||||
<FormInputSuffixWrapper>
|
|
||||||
<FormInput
|
|
||||||
type="text"
|
|
||||||
value={linkData.content}
|
|
||||||
onChange={e => {
|
|
||||||
const updatedLinkList = [...linkList];
|
|
||||||
const index = updatedLinkList.findIndex(link => link.language === linkLanguage);
|
|
||||||
|
|
||||||
if (index !== -1) {
|
|
||||||
updatedLinkList[index] = {
|
|
||||||
...updatedLinkList[index],
|
|
||||||
content: e.target.value
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
updatedLinkList.push({
|
|
||||||
language: linkLanguage,
|
|
||||||
content: e.target.value
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleFieldChange('link_list', updatedLinkList);
|
|
||||||
}}
|
|
||||||
disabled={!isEditable}
|
|
||||||
width={field.width}
|
|
||||||
suffix="true"
|
|
||||||
/>
|
|
||||||
<FormInputSuffix>{linkLanguage}</FormInputSuffix>
|
|
||||||
</FormInputSuffixWrapper>
|
|
||||||
{hasError && <div className="field-error">{hasError}</div>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 조건부 렌더링을 위한 필드 필터링
|
|
||||||
const getVisibleFields = () => {
|
|
||||||
if (!config) return [];
|
|
||||||
|
|
||||||
return config.fields.filter(field => {
|
|
||||||
if (!field.visibleOn.includes(mode)) return false;
|
|
||||||
|
|
||||||
// 조건부 표시 필드 처리
|
|
||||||
if (field.conditional) {
|
|
||||||
const { field: condField, operator, value } = field.conditional;
|
|
||||||
|
|
||||||
if (operator === "==" && formData[condField] !== value) return false;
|
|
||||||
if (operator === "!=" && formData[condField] === value) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 그리드 기반 필드 렌더링
|
|
||||||
const renderGridFields = () => {
|
|
||||||
if (!config) return null;
|
|
||||||
|
|
||||||
const visibleFields = getVisibleFields();
|
|
||||||
const { rows, columns } = config.grid;
|
|
||||||
|
|
||||||
// 그리드 레이아웃 생성
|
|
||||||
return (
|
|
||||||
<div className="form-grid" style={{ display: 'grid', gridTemplateColumns: `repeat(${columns}, 1fr)`, gap: '10px' }}>
|
|
||||||
{visibleFields.map((field) => {
|
|
||||||
const { row, col, width } = field.position;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={field.id}
|
|
||||||
className="form-cell"
|
|
||||||
style={{
|
|
||||||
gridRow: row + 1,
|
|
||||||
gridColumn: `${col + 1} / span ${width}`,
|
|
||||||
padding: '5px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FormRowGroup>
|
|
||||||
<FormLabel>{field.label}{field.validations?.includes("required") && <span className="required">*</span>}</FormLabel>
|
|
||||||
{renderField(field)}
|
|
||||||
</FormRowGroup>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 버튼 렌더링
|
|
||||||
const renderButtons = () => {
|
|
||||||
if (!config || !config.actions || !config.actions[mode]) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="form-actions">
|
|
||||||
{config.actions[mode].map(action => (
|
|
||||||
<Button
|
|
||||||
key={action.id}
|
|
||||||
text={action.label}
|
|
||||||
theme={action.theme}
|
|
||||||
handleClick={() => {
|
|
||||||
if (action.action === 'submit') {
|
|
||||||
if (isFormValid) {
|
|
||||||
onSubmit(formData);
|
|
||||||
}
|
|
||||||
} else if (action.action === 'close' || action.action === 'cancel') {
|
|
||||||
onCancel();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={action.action === 'submit' && !isFormValid}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!config) return <div>로딩 중...</div>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`json-config-form ${className || ''}`} ref={formRef}>
|
|
||||||
<div className="form-content">
|
|
||||||
{renderGridFields()}
|
|
||||||
|
|
||||||
{errors._form && (
|
|
||||||
<SearchBarAlert $marginTop="15px" $align="right">
|
|
||||||
{errors._form}
|
|
||||||
</SearchBarAlert>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-footer">
|
|
||||||
{renderButtons()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CaliForm;
|
|
||||||
|
|
||||||
const LanguageWrapper = styled.div`
|
|
||||||
width: ${props => props.width || '100%'};
|
|
||||||
//margin-bottom: 20px;
|
|
||||||
padding-bottom: 20px;
|
|
||||||
padding-left: 90px;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
margin-bottom: 0;
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const LanguageLabel = styled.h4`
|
|
||||||
color: #444;
|
|
||||||
margin: 0 0 10px 20px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
`;
|
|
||||||
@@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import { Row, Col, Form, Input, Select, DatePicker, TimePicker, InputNumber, Switch, Button, Checkbox } from 'antd';
|
import { Row, Col, Form, Input, Select, DatePicker, TimePicker, InputNumber, Switch, Button, Checkbox } from 'antd';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import { AnimatedTabs } from '../index';
|
||||||
const { RangePicker } = DatePicker;
|
const { RangePicker } = DatePicker;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,7 +53,10 @@ const DetailGrid = ({ items, formData, onChange, disabled = false, columns = 4 }
|
|||||||
max,
|
max,
|
||||||
format,
|
format,
|
||||||
required,
|
required,
|
||||||
showTime
|
showTime,
|
||||||
|
tabItems,
|
||||||
|
activeKey,
|
||||||
|
onTabChange
|
||||||
} = item;
|
} = item;
|
||||||
|
|
||||||
// 현재 값 가져오기 (formData에서 또는 항목에서)
|
// 현재 값 가져오기 (formData에서 또는 항목에서)
|
||||||
@@ -105,6 +109,7 @@ const DetailGrid = ({ items, formData, onChange, disabled = false, columns = 4 }
|
|||||||
return (
|
return (
|
||||||
<DatePicker
|
<DatePicker
|
||||||
{...commonProps}
|
{...commonProps}
|
||||||
|
showTime={showTime || false}
|
||||||
value={currentValue ? dayjs(currentValue) : null}
|
value={currentValue ? dayjs(currentValue) : null}
|
||||||
format={format || 'YYYY-MM-DD'}
|
format={format || 'YYYY-MM-DD'}
|
||||||
onChange={(date) => onChange(key, date, handler)}
|
onChange={(date) => onChange(key, date, handler)}
|
||||||
@@ -191,8 +196,8 @@ const DetailGrid = ({ items, formData, onChange, disabled = false, columns = 4 }
|
|||||||
case 'tab':
|
case 'tab':
|
||||||
return <AnimatedTabs
|
return <AnimatedTabs
|
||||||
items={tabItems}
|
items={tabItems}
|
||||||
activeKey={activeLanguage}
|
activeKey={activeKey}
|
||||||
onChange={handleTabChange}
|
onChange={onTabChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
case 'custom':
|
case 'custom':
|
||||||
@@ -249,13 +254,15 @@ const StatusDisplay = ({ status }) => {
|
|||||||
let color = '';
|
let color = '';
|
||||||
let text = '';
|
let text = '';
|
||||||
|
|
||||||
switch (status) {
|
const lowerStatus = typeof status === 'string' ? status.toLowerCase() : status;
|
||||||
|
|
||||||
|
switch (lowerStatus) {
|
||||||
case 'wait':
|
case 'wait':
|
||||||
color = '#faad14';
|
color = '#FAAD14';
|
||||||
text = '대기';
|
text = '대기';
|
||||||
break;
|
break;
|
||||||
case 'running':
|
case 'running':
|
||||||
color = '#52c41a';
|
color = '#4287f5';
|
||||||
text = '진행중';
|
text = '진행중';
|
||||||
break;
|
break;
|
||||||
case 'finish':
|
case 'finish':
|
||||||
@@ -271,7 +278,7 @@ const StatusDisplay = ({ status }) => {
|
|||||||
text = '삭제';
|
text = '삭제';
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
color = '#1890ff';
|
color = '#DEBB46';
|
||||||
text = status;
|
text = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,38 +5,72 @@ import { motion, AnimatePresence } from 'framer-motion';
|
|||||||
|
|
||||||
// 통합된 애니메이션 탭 컴포넌트
|
// 통합된 애니메이션 탭 컴포넌트
|
||||||
const AnimatedTabs = ({ items, activeKey, onChange }) => {
|
const AnimatedTabs = ({ items, activeKey, onChange }) => {
|
||||||
|
// 각 항목의 children을 애니메이션 래퍼로 감싸기
|
||||||
|
const tabItems = items.map(item => ({
|
||||||
|
key: item.key,
|
||||||
|
label: item.label,
|
||||||
|
children: (
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.div
|
||||||
|
key={activeKey}
|
||||||
|
initial={{ opacity: 0, x: 50 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
exit={{ opacity: 0, x: -50 }}
|
||||||
|
transition={{
|
||||||
|
type: "spring",
|
||||||
|
stiffness: 300,
|
||||||
|
damping: 30
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.children}
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledTabs
|
<StyledTabs
|
||||||
activeKey={activeKey}
|
activeKey={activeKey}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
centered={true}
|
centered={true}
|
||||||
>
|
items={tabItems}
|
||||||
{items.map(item => (
|
/>
|
||||||
<Tabs.TabPane
|
|
||||||
tab={item.label}
|
|
||||||
key={item.key}
|
|
||||||
>
|
|
||||||
<AnimatePresence mode="wait">
|
|
||||||
<motion.div
|
|
||||||
key={activeKey}
|
|
||||||
initial={{ opacity: 0, x: 50 }}
|
|
||||||
animate={{ opacity: 1, x: 0 }}
|
|
||||||
exit={{ opacity: 0, x: -50 }}
|
|
||||||
transition={{
|
|
||||||
type: "spring",
|
|
||||||
stiffness: 300,
|
|
||||||
damping: 30
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item.children}
|
|
||||||
</motion.div>
|
|
||||||
</AnimatePresence>
|
|
||||||
</Tabs.TabPane>
|
|
||||||
))}
|
|
||||||
</StyledTabs>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// const AnimatedTabs = ({ items, activeKey, onChange }) => {
|
||||||
|
// return (
|
||||||
|
// <StyledTabs
|
||||||
|
// activeKey={activeKey}
|
||||||
|
// onChange={onChange}
|
||||||
|
// centered={true}
|
||||||
|
// >
|
||||||
|
// {items.map(item => (
|
||||||
|
// <Tabs.TabPane
|
||||||
|
// tab={item.label}
|
||||||
|
// key={item.key}
|
||||||
|
// >
|
||||||
|
// <AnimatePresence mode="wait">
|
||||||
|
// <motion.div
|
||||||
|
// key={activeKey}
|
||||||
|
// initial={{ opacity: 0, x: 50 }}
|
||||||
|
// animate={{ opacity: 1, x: 0 }}
|
||||||
|
// exit={{ opacity: 0, x: -50 }}
|
||||||
|
// transition={{
|
||||||
|
// type: "spring",
|
||||||
|
// stiffness: 300,
|
||||||
|
// damping: 30
|
||||||
|
// }}
|
||||||
|
// >
|
||||||
|
// {item.children}
|
||||||
|
// </motion.div>
|
||||||
|
// </AnimatePresence>
|
||||||
|
// </Tabs.TabPane>
|
||||||
|
// ))}
|
||||||
|
// </StyledTabs>
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
|
||||||
const StyledTabs = styled(Tabs)`
|
const StyledTabs = styled(Tabs)`
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import CDivider from './CDivider';
|
|||||||
import TopButton from './button/TopButton';
|
import TopButton from './button/TopButton';
|
||||||
import AntButton from './button/AntButton';
|
import AntButton from './button/AntButton';
|
||||||
import DetailLayout from './Layout/DetailLayout';
|
import DetailLayout from './Layout/DetailLayout';
|
||||||
|
import AnimatedTabs from './control/AnimatedTabs';
|
||||||
|
|
||||||
import CaliTable from './Custom/CaliTable'
|
import CaliTable from './Custom/CaliTable'
|
||||||
|
|
||||||
@@ -56,5 +57,6 @@ export { DateTimeInput,
|
|||||||
FrontPagination,
|
FrontPagination,
|
||||||
DownloadProgress,
|
DownloadProgress,
|
||||||
CaliTable,
|
CaliTable,
|
||||||
DetailLayout
|
DetailLayout,
|
||||||
|
AnimatedTabs
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user