Files
sam-react-prod/src/components/settings/PopupManagement/popupDetailConfig.ts
유병철 397eb2c19c feat: 공지 팝업 시스템 구현 및 캘린더/어음/팝업관리 개선
- NoticePopupModal: 공지 팝업 컨테이너/actions 신규 구현
- AuthenticatedLayout에 공지 팝업 연동
- CalendarSection: 일정 타입 확장 및 UI 개선
- BillManagementClient: 기능 확장
- PopupManagement: popupDetailConfig 대폭 확장, 상세/폼 개선
- BoardForm/BoardManagement: 게시판 폼 개선
- LoginPage, logout, userStorage: 인증 관련 소폭 수정
- dashboard types 정비
- claudedocs: 공지팝업 구현, 캘린더 어음연동/일정타입, API changelog 문서 추가
2026-03-10 15:16:41 +09:00

325 lines
9.7 KiB
TypeScript

/**
* 팝업관리 상세 페이지 설정
* 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 !== '<p></p>';
},
},
],
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<Popup> = {
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<PopupFormData> => {
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)
)
)
)
);
}