org-network / frontend /src /components /ops /OpsDefaults.tsx
5minbetter's picture
deploy: initial clean workspace without lfs history
33d9e63
Raw
History Blame Contribute Delete
30.9 kB
/**
* @license
* SPDX-License-Identifier: Apache-2.0
*/
import React from 'react';
import { NetworkType, StrengthOption, Question } from '../../types';
import { defaultsService, SystemDefaults } from '../../services/defaultsService';
import { Tabs } from '../common/Tabs';
import { Button } from '../common/Button';
import { Input } from '../common/Input';
import { TextArea } from '../common/TextArea';
import { PjtBodyCard } from '../pjt/PjtBodyCard';
import { QuestionRow } from '../pjt/QuestionRow';
import { Modal } from '../common/Modal';
import {
Plus,
Save,
Trash2,
ChevronUp,
ChevronDown,
RotateCcw,
ArrowLeft,
} from 'lucide-react';
import {
Table,
TableHeader,
TableBody,
TableRow,
TableHead,
TableCell,
} from '../common/Table';
interface OpsDefaultsProps {
onNavigateHome: () => void;
}
export const OpsDefaults: React.FC<OpsDefaultsProps> = ({ onNavigateHome }) => {
const [activeTab, setActiveTab] = React.useState<'questions' | 'network_types' | 'strength_scales' | 'notice_template'>('questions');
const [defaults, setDefaults] = React.useState<SystemDefaults | null>(null);
// Modal alert/confirm configuration state
const [modalConfig, setModalConfig] = React.useState<{
isOpen: boolean;
title: string;
message: string;
onConfirm?: () => void;
showCancel?: boolean;
}>({
isOpen: false,
title: '',
message: '',
});
const showAlert = (title: string, message: string) => {
setModalConfig({
isOpen: true,
title,
message,
showCancel: false,
});
};
const showConfirm = (title: string, message: string, onConfirm: () => void) => {
setModalConfig({
isOpen: true,
title,
message,
onConfirm,
showCancel: true,
});
};
// Load defaults on mount
React.useEffect(() => {
defaultsService.getDefaults().then((data) => {
setDefaults(data);
});
}, []);
// 1. Question states & actions
const [newQuestionText, setNewQuestionText] = React.useState('');
const [newNetworkType, setNewNetworkType] = React.useState('work');
const [newMaxSelections, setNewMaxSelections] = React.useState(5);
const handleUpdateQuestion = (qId: string, updates: Partial<Question>) => {
if (!defaults) return;
const updatedQs = defaults.QUESTIONS.map((q) =>
q.question_id === qId ? { ...q, ...updates } : q
);
setDefaults({ ...defaults, QUESTIONS: updatedQs });
};
const handleMoveQuestion = (index: number, direction: 'up' | 'down') => {
if (!defaults) return;
const targetIdx = direction === 'up' ? index - 1 : index + 1;
if (targetIdx < 0 || targetIdx >= defaults.QUESTIONS.length) return;
const copy = [...defaults.QUESTIONS];
const temp = copy[index];
copy[index] = copy[targetIdx];
copy[targetIdx] = temp;
const remapped = copy.map((q, idx) => ({ ...q, order_no: idx + 1 }));
setDefaults({ ...defaults, QUESTIONS: remapped });
};
const handleDeleteQuestion = (qId: string) => {
if (!defaults) return;
showConfirm(
'기본 질문 삭제',
'이 질문 문항을 기본 템플릿에서 삭제하시겠습니까? 이후 생성되는 프로젝트의 기본 문항 목록에 영향을 줍니다.',
() => {
const updatedQs = defaults.QUESTIONS.filter((q) => q.question_id !== qId);
const remapped = updatedQs.map((q, idx) => ({ ...q, order_no: idx + 1 }));
setDefaults({ ...defaults, QUESTIONS: remapped });
}
);
};
const handleAddQuestion = (e: React.FormEvent) => {
e.preventDefault();
if (!defaults || !newQuestionText.trim()) return;
const newQ: Question = {
question_id: `Q${defaults.QUESTIONS.length + 1}_${Date.now().toString().slice(-4)}`,
network_type: newNetworkType,
question_text: newQuestionText.trim(),
max_selections: Number(newMaxSelections),
is_required: true,
order_no: defaults.QUESTIONS.length + 1,
strength_options: defaults.STRENGTH_OPTIONS, // Use template strength options
};
setDefaults({
...defaults,
QUESTIONS: [...defaults.QUESTIONS, newQ],
});
setNewQuestionText('');
showAlert('알림', '기본 질문 문항이 추가되었습니다. 저장하기를 누르시면 최종 저장됩니다.');
};
// 2. Network type states & actions
const [newTypeName, setNewTypeName] = React.useState('');
const [newTypeEnglish, setNewTypeEnglish] = React.useState('');
const handleUpdateNetworkType = (index: number, field: 'value' | 'label', val: string) => {
if (!defaults) return;
const updated = [...defaults.NETWORK_TYPES];
updated[index] = {
...updated[index],
[field]: field === 'value' ? val.trim().toLowerCase().replace(/\s+/g, '_') : val,
};
setDefaults({ ...defaults, NETWORK_TYPES: updated });
};
const handleMoveNetworkType = (index: number, direction: 'up' | 'down') => {
if (!defaults) return;
const targetIdx = direction === 'up' ? index - 1 : index + 1;
if (targetIdx < 0 || targetIdx >= defaults.NETWORK_TYPES.length) return;
const copy = [...defaults.NETWORK_TYPES];
const temp = copy[index];
copy[index] = copy[targetIdx];
copy[targetIdx] = temp;
setDefaults({ ...defaults, NETWORK_TYPES: copy });
};
const handleDeleteNetworkType = (code: string) => {
if (!defaults) return;
showConfirm(
'기본 네트워크 분류 삭제',
'이 네트워크 분류를 기본 템플릿에서 삭제하시겠습니까?',
() => {
const updated = defaults.NETWORK_TYPES.filter((t) => t.value !== code);
setDefaults({ ...defaults, NETWORK_TYPES: updated });
}
);
};
const handleAddNetworkType = (e: React.FormEvent) => {
e.preventDefault();
if (!defaults) return;
const english = newTypeEnglish.trim().toLowerCase().replace(/\s+/g, '_');
const name = newTypeName.trim();
if (!english || !name) return;
if (defaults.NETWORK_TYPES.some((t) => t.value === english)) {
showAlert('알림', '이미 존재하는 영문 분류 코드입니다.');
return;
}
setDefaults({
...defaults,
NETWORK_TYPES: [...defaults.NETWORK_TYPES, { value: english, label: name }],
});
setNewTypeEnglish('');
setNewTypeName('');
showAlert('알림', '새로운 네트워크 분류가 임시 추가되었습니다. 저장하기를 누르시면 최종 저장됩니다.');
};
// 3. Strength Option states & actions
const [newStrengthLabel, setNewStrengthLabel] = React.useState('');
const handleUpdateStrengthLabel = (index: number, label: string) => {
if (!defaults) return;
const updated = [...defaults.STRENGTH_OPTIONS];
updated[index] = { ...updated[index], label };
setDefaults({ ...defaults, STRENGTH_OPTIONS: updated });
};
const handleMoveStrength = (index: number, direction: 'up' | 'down') => {
if (!defaults) return;
const targetIdx = direction === 'up' ? index - 1 : index + 1;
if (targetIdx < 0 || targetIdx >= defaults.STRENGTH_OPTIONS.length) return;
const copy = [...defaults.STRENGTH_OPTIONS];
const temp = copy[index];
copy[index] = copy[targetIdx];
copy[targetIdx] = temp;
const updated = copy.map((opt, idx) => ({
...opt,
value: String(idx + 1),
}));
setDefaults({ ...defaults, STRENGTH_OPTIONS: updated });
};
const handleDeleteStrength = (value: string) => {
if (!defaults) return;
showConfirm(
'기본 관계 척도 삭제',
'이 관계 빈도 척도를 기본 템플릿에서 삭제하시겠습니까?',
() => {
const filtered = defaults.STRENGTH_OPTIONS.filter((opt) => opt.value !== value);
const updated = filtered.map((opt, idx) => ({
...opt,
value: String(idx + 1),
}));
setDefaults({ ...defaults, STRENGTH_OPTIONS: updated });
}
);
};
const handleAddStrength = (e: React.FormEvent) => {
e.preventDefault();
if (!defaults || !newStrengthLabel.trim()) return;
const nextIndex = defaults.STRENGTH_OPTIONS.length + 1;
const val = String(nextIndex);
const lbl = newStrengthLabel.trim();
setDefaults({
...defaults,
STRENGTH_OPTIONS: [...defaults.STRENGTH_OPTIONS, { value: val, label: lbl }],
});
setNewStrengthLabel('');
showAlert('알림', '새로운 관계 빈도 척도가 임시 추가되었습니다. 저장하기를 누르시면 최종 저장됩니다.');
};
// 4. Notice template actions
const handleUpdateNoticeTemplate = (text: string) => {
if (!defaults) return;
setDefaults({ ...defaults, NOTICE_TEMPLATE: text });
};
// Save / Factory Reset
const handleSaveAll = () => {
if (!defaults) return;
defaultsService.saveDefaults(defaults)
.then(() => {
showAlert('성공', '시스템 초기값 설정이 성공적으로 저장되었습니다. 이후 생성되는 프로젝트에 자동 적용됩니다.');
})
.catch((err) => {
console.error(err);
showAlert('오류', '시스템 초기값 설정 저장 중 오류가 발생했습니다.');
});
};
const handleFactoryReset = () => {
showConfirm(
'시스템 초기값 완전 초기화',
'모든 설정을 공장 출시 시점의 defaults.json 파일 내용으로 초기화하시겠습니까? 기존 커스텀 초기 설정은 모두 삭제됩니다.',
() => {
defaultsService.resetDefaults()
.then((initial) => {
setDefaults(initial);
showAlert('알림', '모든 초기값 설정이 공장 출시 상태로 초기화되었습니다.');
})
.catch((err) => {
console.error(err);
showAlert('오류', '시스템 초기값 초기화 중 오류가 발생했습니다.');
});
}
);
};
if (!defaults) {
return (
<div className="flex items-center justify-center p-12 text-sm font-semibold text-slate-500">
설정 정보 로딩 중...
</div>
);
}
return (
<div className="space-y-6 font-sans antialiased" id="ops-defaults-view">
{/* Top Header Controls */}
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 pb-2">
<div>
<button
onClick={onNavigateHome}
className="flex items-center gap-1.5 text-sm font-semibold text-slate-500 hover:text-slate-800 transition-colors border-0 bg-transparent cursor-pointer p-0 mb-1"
>
<ArrowLeft className="w-4 h-4" />
<span>홈으로 이동</span>
</button>
<h2 className="text-xl md:text-2xl font-bold text-slate-900 tracking-tight">시스템 초기값 설정 관리</h2>
<p className="text-sm text-slate-400 font-medium">
새 프로젝트 생성 시 자동으로 적용되는 기본 문항 및 템플릿을 설정합니다.
</p>
</div>
<div className="flex items-center gap-2 w-full md:w-auto">
<Button
onClick={handleFactoryReset}
variant="outline"
size="md"
leftIcon={<RotateCcw className="w-4 h-4" />}
id="factory-reset-defaults-btn"
className="flex-1 md:flex-initial"
>
초기 기본값 리셋
</Button>
<Button
onClick={handleSaveAll}
size="md"
leftIcon={<Save className="w-4 h-4" />}
id="save-defaults-btn"
className="flex-1 md:flex-initial"
>
전체 저장하기
</Button>
</div>
</div>
{/* Tabs */}
<Tabs
options={[
{ key: 'network_types', label: '네트워크 분류', mobileLabel: '분류' },
{ key: 'questions', label: '기본 질문', mobileLabel: '질문' },
{ key: 'strength_scales', label: '기본 척도', mobileLabel: '척도' },
{ key: 'notice_template', label: '안내문 템플릿', mobileLabel: '안내문' },
]}
activeKey={activeTab}
onChange={(key) => setActiveTab(key as any)}
/>
{/* Pane content matching active tab */}
<div className="grid grid-cols-1 gap-6" id="defaults-settings-pane">
{/* 1. Questions Tab */}
{activeTab === 'questions' && (
<div className="space-y-6">
<PjtBodyCard
title="기본 질문 목록"
info="새 프로젝트 생성 시 적용될 기본 질문 목록입니다."
>
<div className="border-0 md:border md:border-slate-100 md:rounded-lg overflow-x-auto">
<Table id="defaults-questions-table" className="w-full block md:table [&_td]:py-2">
<TableHeader className="hidden md:table-header-group">
<TableRow className="bg-slate-50 border-b border-slate-100 text-slate-400 text-sm font-bold uppercase tracking-tight">
<TableHead className="p-2 text-center">순서</TableHead>
<TableHead className="p-2 text-center min-w-30">유형</TableHead>
<TableHead className="p-2 text-center min-w-100">질문 문구</TableHead>
<TableHead className="p-2 text-center">최대 지목 인원</TableHead>
<TableHead className="p-2 text-center">정렬</TableHead>
<TableHead className="p-2 text-center">작업</TableHead>
</TableRow>
</TableHeader>
<TableBody className="block md:table-row-group space-y-3 md:space-y-0">
{defaults.QUESTIONS.map((question, idx) => (
<QuestionRow
key={question.question_id}
question={question}
index={idx}
isFirst={idx === 0}
isLast={idx === defaults.QUESTIONS.length - 1}
onMove={(direction) => handleMoveQuestion(idx, direction)}
onDelete={() => handleDeleteQuestion(question.question_id)}
onUpdateQuestion={handleUpdateQuestion}
networkTypes={defaults.NETWORK_TYPES}
/>
))}
{defaults.QUESTIONS.length === 0 && (
<TableRow>
<TableCell colSpan={6} className="p-6 text-center text-slate-400 block md:table-cell">
등록된 기본 질문이 없습니다. 아래에서 새로 추가해주세요.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</PjtBodyCard>
<PjtBodyCard
title="새로운 기본 질문 추가"
info="템플릿에 등록할 기본 네트워크 질문 문항을 생성합니다."
>
<form onSubmit={handleAddQuestion} className="space-y-4" id="new-default-q-form">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<Input
label="네트워크 분류 *"
type="select"
value={newNetworkType}
onChange={(e) => setNewNetworkType(e.target.value)}
options={defaults.NETWORK_TYPES}
required
/>
<Input
label="최대 지목 인원 수 *"
type="number"
min={1}
max={20}
value={newMaxSelections}
onChange={(e) => setNewMaxSelections(Number(e.target.value))}
required
/>
</div>
<Input
label="질문 문구 *"
type="text"
value={newQuestionText}
onChange={(e) => setNewQuestionText(e.target.value)}
placeholder="예: 업무적으로 조언을 구하는 사람은?"
required
/>
<Button
type="submit"
id="add-default-question-btn"
leftIcon={<Plus className="w-4 h-4" />}
className="w-full"
>
기본 질문 목록에 추가
</Button>
</form>
</PjtBodyCard>
</div>
)}
{/* 2. Network Types Tab */}
{activeTab === 'network_types' && (
<div className="space-y-6">
<PjtBodyCard
title="기본 네트워크 분류 목록"
info="설문 문항 분류 필터 및 분석 등에 기본 제공되는 네트워크 유형입니다."
>
<div className="border border-slate-100 rounded-lg overflow-x-auto">
<Table id="defaults-network-type-table" className="text-xs sm:text-sm">
<TableHeader>
<TableRow className="bg-slate-50 border-b border-slate-100 text-slate-400 font-bold uppercase tracking-tight">
<TableHead className="p-2 text-center">순서</TableHead>
<TableHead className="p-2 text-center min-w-30">영문 코드</TableHead>
<TableHead className="p-2 text-center min-w-30">분류 표시 문구</TableHead>
<TableHead className="p-2 text-center">정렬</TableHead>
<TableHead className="p-2 text-center">작업</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{defaults.NETWORK_TYPES.map((type, idx) => (
<TableRow key={idx}>
<TableCell className="p-3 text-center font-semibold text-slate-500">{idx + 1}</TableCell>
<TableCell className="p-3">
<input
type="text"
value={type.value}
onChange={(e) => handleUpdateNetworkType(idx, 'value', e.target.value)}
className="w-full px-2 py-1 text-sm border border-slate-200 focus:border-orange-500 focus:ring-1 focus:ring-orange-500/20 rounded outline-none text-slate-600"
required
/>
</TableCell>
<TableCell className="p-3">
<input
type="text"
value={type.label}
onChange={(e) => handleUpdateNetworkType(idx, 'label', e.target.value)}
className="w-full px-2 py-1 text-sm border border-slate-200 focus:border-orange-500 focus:ring-1 focus:ring-orange-500/20 rounded outline-none text-slate-800"
required
/>
</TableCell>
<TableCell className="p-3 text-center">
<div className="flex justify-center gap-1">
<button
type="button"
onClick={() => handleMoveNetworkType(idx, 'up')}
disabled={idx === 0}
className="p-1 text-slate-400 hover:text-slate-600 disabled:opacity-30 rounded cursor-pointer"
>
<ChevronUp className="w-4 h-4" />
</button>
<button
type="button"
onClick={() => handleMoveNetworkType(idx, 'down')}
disabled={idx === defaults.NETWORK_TYPES.length - 1}
className="p-1 text-slate-400 hover:text-slate-600 disabled:opacity-30 rounded cursor-pointer"
>
<ChevronDown className="w-4 h-4" />
</button>
</div>
</TableCell>
<TableCell className="p-3 text-center">
<button
type="button"
onClick={() => handleDeleteNetworkType(type.value)}
className="p-1 text-slate-300 hover:text-red-500 hover:bg-red-50 rounded cursor-pointer inline-flex items-center justify-center"
>
<Trash2 className="w-4 h-4" />
</button>
</TableCell>
</TableRow>
))}
{defaults.NETWORK_TYPES.length === 0 && (
<TableRow>
<TableCell colSpan={5} className="p-6 text-center text-slate-400">등록된 기본 네트워크 분류가 없습니다.</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</PjtBodyCard>
<PjtBodyCard
title="네트워크 분류 추가"
info="기본 템플릿에 적용될 네트워크 유형 분류 옵션을 추가합니다."
>
<form onSubmit={handleAddNetworkType} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<Input
label="영문 분류 코드 *"
value={newTypeEnglish}
onChange={(e) => setNewTypeEnglish(e.target.value)}
placeholder="예: work, cooperation"
required
/>
<Input
label="분류 표시 문구 *"
value={newTypeName}
onChange={(e) => setNewTypeName(e.target.value)}
placeholder="예: 업무 협력, 동적 네트워크"
required
/>
</div>
<Button
type="submit"
size="md"
leftIcon={<Plus className="w-4 h-4" />}
id="add-default-network-type-btn"
className="w-full"
>
기본 분류에 추가
</Button>
</form>
</PjtBodyCard>
</div>
)}
{/* 3. Strength Scales Tab */}
{activeTab === 'strength_scales' && (
<div className="space-y-6">
<PjtBodyCard
title="기본 관계 척도 목록"
info="지목한 동료와의 관계 빈도/강도를 묻는 척도의 초기 템플릿입니다."
>
<div className="border border-slate-100 rounded-lg overflow-x-auto">
<Table id="defaults-scales-table" className="text-xs sm:text-sm">
<TableHeader>
<TableRow className="bg-slate-50 border-b border-slate-100 text-slate-400 font-bold uppercase tracking-tight">
<TableHead className="p-2 text-center">순서</TableHead>
<TableHead className="p-2 text-center min-w-20">척도 표시 문구</TableHead>
<TableHead className="p-2 text-center">정렬</TableHead>
<TableHead className="p-2 text-center">작업</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{defaults.STRENGTH_OPTIONS.map((opt, idx) => (
<TableRow key={idx}>
<TableCell className="p-3 text-center font-semibold text-slate-500">{idx + 1}</TableCell>
<TableCell className="p-3">
<input
type="text"
value={opt.label}
onChange={(e) => handleUpdateStrengthLabel(idx, e.target.value)}
className="w-full px-2 py-1 text-sm border border-slate-200 focus:border-orange-500 focus:ring-1 focus:ring-orange-500/20 rounded outline-none text-slate-800"
required
/>
</TableCell>
<TableCell className="p-3 text-center">
<div className="flex justify-center gap-0.5">
<button
type="button"
onClick={() => handleMoveStrength(idx, 'up')}
disabled={idx === 0}
className="p-1 text-slate-400 hover:text-slate-600 disabled:opacity-30 rounded cursor-pointer"
>
<ChevronUp className="w-4 h-4" />
</button>
<button
type="button"
onClick={() => handleMoveStrength(idx, 'down')}
disabled={idx === defaults.STRENGTH_OPTIONS.length - 1}
className="p-1 text-slate-400 hover:text-slate-600 disabled:opacity-30 rounded cursor-pointer"
>
<ChevronDown className="w-4 h-4" />
</button>
</div>
</TableCell>
<TableCell className="p-3 text-center">
<button
type="button"
onClick={() => handleDeleteStrength(opt.value)}
className="p-1 text-slate-300 hover:text-red-500 hover:bg-red-50 rounded cursor-pointer inline-flex items-center justify-center"
>
<Trash2 className="w-4 h-4" />
</button>
</TableCell>
</TableRow>
))}
{defaults.STRENGTH_OPTIONS.length === 0 && (
<TableRow>
<TableCell colSpan={4} className="p-6 text-center text-slate-400">등록된 기본 척도가 없습니다.</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</PjtBodyCard>
<PjtBodyCard
title="새로운 척도 추가"
info="기본 템플릿에 관계 강도/빈도 척도 옵션을 추가합니다."
>
<form onSubmit={handleAddStrength} className="space-y-4">
<div className="flex items-end gap-4 w-full">
<div className="flex-1 w-full">
<Input
label="척도 표시 문구 *"
type="text"
value={newStrengthLabel}
onChange={(e) => setNewStrengthLabel(e.target.value)}
placeholder="예: 거의 매일, 주 1~2회"
required
/>
</div>
<Button type="submit" className="w-auto">추가</Button>
</div>
</form>
</PjtBodyCard>
</div>
)}
{/* 4. Notice Template Tab */}
{activeTab === 'notice_template' && (
<div className="space-y-6">
<PjtBodyCard
title="기본 안내문 템플릿 편집"
info="임직원 설문 안내 시 활용되는 공통 공지 템플릿입니다."
>
<div className="space-y-4">
<TextArea
label="본문 내용 *"
value={defaults.NOTICE_TEMPLATE}
onChange={(e) => handleUpdateNoticeTemplate(e.target.value)}
rows={15}
required
className="font-mono"
/>
<div className="p-4 bg-slate-50 border border-slate-100 rounded-lg text-slate-500 text-sm leading-relaxed space-y-2">
<p className="font-bold text-slate-700">⚠️ 템플릿 치환자(Placeholder) 수정 금지</p>
<p>안내문 발송 시점에 프로젝트 정보로 자동 치환되는 특수 키워드들입니다. 템플릿 작성 시 그대로 포함하여 작성해 주세요.</p>
<ul className="list-disc list-inside space-y-1 font-semibold text-slate-600 pl-1">
<li><code className="text-orange-600 font-mono font-bold bg-orange-50 px-1 py-0.5 rounded">{"{start_date}"}</code> : 설문 시작 일자</li>
<li><code className="text-orange-600 font-mono font-bold bg-orange-50 px-1 py-0.5 rounded">{"{end_date}"}</code> : 설문 종료 일자</li>
<li><code className="text-orange-600 font-mono font-bold bg-orange-50 px-1 py-0.5 rounded">{"{common_survey_url}"}</code> : 설문 조사 참여용 공통 URL</li>
<li><code className="text-orange-600 font-mono font-bold bg-orange-50 px-1 py-0.5 rounded">{"{survey_login_id}"}</code> : 설문 참여용 로그인 ID</li>
<li><code className="text-orange-600 font-mono font-bold bg-orange-50 px-1 py-0.5 rounded">{"{survey_password_hash}"}</code> : 설문 참여용 비밀번호</li>
</ul>
</div>
</div>
</PjtBodyCard>
</div>
)}
</div>
{/* Modal dialog */}
<Modal
isOpen={modalConfig.isOpen}
onClose={() => setModalConfig({ ...modalConfig, isOpen: false })}
title={modalConfig.title}
showCloseButton={true}
footerActions={
<div className="flex justify-end gap-2 w-full">
{modalConfig.showCancel && (
<Button variant="secondary" onClick={() => setModalConfig({ ...modalConfig, isOpen: false })}>
취소
</Button>
)}
<Button
variant="primary"
onClick={() => {
modalConfig.onConfirm?.();
setModalConfig({ ...modalConfig, isOpen: false });
}}
>
확인
</Button>
</div>
}
>
<div className="py-2 text-sm text-slate-600 leading-relaxed">
{modalConfig.message}
</div>
</Modal>
</div>
);
};