/** * 팝업관리 상세 페이지 설정 * IntegratedDetailTemplate V2 마이그레이션 */ import { Megaphone } from 'lucide-react'; import type { DetailConfig, FieldDefinition, SectionDefinition } from '@/components/templates/IntegratedDetailTemplate/types'; import type { Popup, PopupFormData, PopupTarget, PopupStatus } from './types'; import { RichTextEditor } from '@/components/board/RichTextEditor'; import { createElement, useState, useEffect } from 'react'; import { sanitizeHTML } from '@/lib/sanitize'; import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/select'; import { getDepartmentList } from './actions'; // ===== 대상 옵션 ===== const TARGET_OPTIONS = [ { value: 'all', label: '전사' }, { value: 'department', label: '부서별' }, ]; // ===== 상태 옵션 ===== const STATUS_OPTIONS = [ { value: 'inactive', label: '사용안함' }, { value: 'active', label: '사용함' }, ]; /** * target 값 인코딩/디코딩 헬퍼 * 'all' → target_type: all, target_id: null * 'department:13' → target_type: department, target_id: 13 */ export function encodeTargetValue(targetType: string, departmentId?: number | null): string { if (targetType === 'department' && departmentId) { return `department:${departmentId}`; } return targetType; } export function decodeTargetValue(value: string): { targetType: PopupTarget; departmentId: number | null } { if (value.startsWith('department:')) { const id = parseInt(value.split(':')[1]); return { targetType: 'department', departmentId: isNaN(id) ? null : id }; } if (value === 'department') { return { targetType: 'department', departmentId: null }; } return { targetType: 'all', departmentId: null }; } // ===== 필드 정의 ===== export const popupFields: FieldDefinition[] = [ { key: 'target', label: '대상', type: 'custom', required: true, validation: [ { type: 'custom', message: '대상을 선택해주세요.', validate: (value) => !!value && value !== '', }, { type: 'custom', message: '부서를 선택해주세요.', validate: (value) => { const str = value as string; if (str === 'department') return false; // 부서 미선택 return true; }, }, ], renderField: ({ value, onChange, mode, disabled }) => { const strValue = (value as string) || 'all'; const { targetType, departmentId } = decodeTargetValue(strValue); if (mode === 'view') { // view 모드에서는 formatValue로 처리 return null; } // Edit/Create 모드: 대상 타입 Select + 조건부 부서 Select return createElement(TargetSelectorField, { targetType, departmentId, onChange, disabled: !!disabled, }); }, formatValue: (value) => { // view 모드에서 표시할 텍스트 — 실제 부서명은 PopupDetailClientV2에서 처리 const strValue = (value as string) || 'all'; if (strValue === 'all') return '전사'; if (strValue.startsWith('department:')) return '부서별'; // 부서명은 아래서 덮어씌움 return '부서별'; }, }, { key: 'startDate', label: '시작일', type: 'date', required: true, validation: [ { type: 'required', message: '시작일을 선택해주세요.' }, ], }, { key: 'endDate', label: '종료일', type: 'date', required: true, validation: [ { type: 'required', message: '종료일을 선택해주세요.' }, { type: 'custom', message: '종료일은 시작일 이후여야 합니다.', validate: (value, formData) => { const startDate = formData.startDate as string; const endDate = value as string; if (!startDate || !endDate) return true; return endDate >= startDate; }, }, ], }, { key: 'title', label: '제목', type: 'text', required: true, placeholder: '제목을 입력해주세요', gridSpan: 2, validation: [ { type: 'required', message: '제목을 입력해주세요.' }, ], }, { key: 'content', label: '내용', type: 'custom', required: true, gridSpan: 2, validation: [ { type: 'custom', message: '내용을 입력해주세요.', validate: (value) => { const content = value as string; return !!content && content.trim() !== '' && content !== '

'; }, }, ], renderField: ({ value, onChange, mode, disabled }) => { if (mode === 'view') { return createElement('div', { className: 'border border-gray-200 rounded-md p-4 bg-gray-50 min-h-[100px] prose prose-sm max-w-none', dangerouslySetInnerHTML: { __html: sanitizeHTML((value as string) || '') }, }); } return createElement(RichTextEditor, { value: (value as string) || '', onChange: onChange, placeholder: '내용을 입력해주세요', minHeight: '200px', disabled: disabled, }); }, formatValue: (value) => { if (!value) return '-'; return createElement('div', { className: 'border border-gray-200 rounded-md p-4 bg-gray-50 min-h-[100px] prose prose-sm max-w-none', dangerouslySetInnerHTML: { __html: sanitizeHTML(value as string) }, }); }, }, { key: 'status', label: '상태', type: 'radio', options: STATUS_OPTIONS, defaultValue: 'inactive', }, { key: 'author', label: '작성자', type: 'text', disabled: true, hideInForm: false, }, { key: 'createdAt', label: '등록일시', type: 'text', disabled: true, hideInForm: false, }, ]; // ===== 섹션 정의 ===== export const popupSections: SectionDefinition[] = [ { id: 'basicInfo', title: '팝업 정보', description: '팝업의 기본 정보를 입력해주세요', fields: ['target', 'startDate', 'endDate', 'title', 'content', 'status', 'author', 'createdAt'], }, ]; // ===== 설정 ===== export const popupDetailConfig: DetailConfig = { title: '팝업관리', description: '팝업 목록을 관리합니다', icon: Megaphone, basePath: '/ko/settings/popup-management', fields: popupFields, sections: popupSections, gridColumns: 2, actions: { submitLabel: '저장', cancelLabel: '취소', showDelete: true, deleteLabel: '삭제', showEdit: true, editLabel: '수정', showBack: true, backLabel: '목록', deleteConfirmMessage: { title: '팝업 삭제', description: '이 팝업을 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다.', }, }, transformInitialData: (data: Popup) => ({ target: encodeTargetValue(data.target, data.targetId), startDate: data.startDate || '', endDate: data.endDate || '', title: data.title || '', content: data.content || '', status: data.status || 'inactive', author: data.author || '', createdAt: data.createdAt || '', }), transformSubmitData: (formData): Partial => { const { targetType, departmentId } = decodeTargetValue(formData.target as string); return { target: targetType, targetDepartmentId: departmentId ? String(departmentId) : undefined, title: formData.title as string, content: formData.content as string, status: formData.status as PopupStatus, startDate: formData.startDate as string, endDate: formData.endDate as string, }; }, }; // ===== 대상 선택 필드 컴포넌트 ===== interface TargetSelectorFieldProps { targetType: string; departmentId: number | null; onChange: (value: unknown) => void; disabled: boolean; } function TargetSelectorField({ targetType, departmentId, onChange, disabled }: TargetSelectorFieldProps) { const [departments, setDepartments] = useState<{ id: number; name: string }[]>([]); const [loading, setLoading] = useState(false); useEffect(() => { if (targetType === 'department' && departments.length === 0) { setLoading(true); getDepartmentList() .then((list: { id: number; name: string }[]) => setDepartments(list)) .finally(() => setLoading(false)); } }, [targetType]); const handleTypeChange = (newType: string) => { if (newType === 'all') { onChange('all'); } else { onChange('department'); } }; const handleDepartmentChange = (deptId: string) => { onChange(`department:${deptId}`); }; return createElement('div', { className: 'space-y-2' }, // 대상 타입 Select createElement(Select, { value: targetType, onValueChange: handleTypeChange, disabled, }, createElement(SelectTrigger, null, createElement(SelectValue, { placeholder: '대상을 선택해주세요' }) ), createElement(SelectContent, null, TARGET_OPTIONS.map(opt => createElement(SelectItem, { key: opt.value, value: opt.value }, opt.label) ) ) ), // 부서별 선택 시 부서 Select 추가 targetType === 'department' && createElement(Select, { value: departmentId ? String(departmentId) : undefined, onValueChange: handleDepartmentChange, disabled: disabled || loading, }, createElement(SelectTrigger, null, createElement(SelectValue, { placeholder: loading ? '부서 목록 로딩 중...' : '부서를 선택해주세요', }) ), createElement(SelectContent, null, departments.map((dept: { id: number; name: string }) => createElement(SelectItem, { key: dept.id, value: String(dept.id) }, dept.name) ) ) ) ); }