/** * @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 = ({ onNavigateHome }) => { const [activeTab, setActiveTab] = React.useState<'questions' | 'network_types' | 'strength_scales' | 'notice_template'>('questions'); const [defaults, setDefaults] = React.useState(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) => { 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 (
설정 정보 로딩 중...
); } return (
{/* Top Header Controls */}

시스템 초기값 설정 관리

새 프로젝트 생성 시 자동으로 적용되는 기본 문항 및 템플릿을 설정합니다.

{/* Tabs */} setActiveTab(key as any)} /> {/* Pane content matching active tab */}
{/* 1. Questions Tab */} {activeTab === 'questions' && (
순서 유형 질문 문구 최대 지목 인원 정렬 작업 {defaults.QUESTIONS.map((question, idx) => ( handleMoveQuestion(idx, direction)} onDelete={() => handleDeleteQuestion(question.question_id)} onUpdateQuestion={handleUpdateQuestion} networkTypes={defaults.NETWORK_TYPES} /> ))} {defaults.QUESTIONS.length === 0 && ( 등록된 기본 질문이 없습니다. 아래에서 새로 추가해주세요. )}
setNewNetworkType(e.target.value)} options={defaults.NETWORK_TYPES} required /> setNewMaxSelections(Number(e.target.value))} required />
setNewQuestionText(e.target.value)} placeholder="예: 업무적으로 조언을 구하는 사람은?" required />
)} {/* 2. Network Types Tab */} {activeTab === 'network_types' && (
순서 영문 코드 분류 표시 문구 정렬 작업 {defaults.NETWORK_TYPES.map((type, idx) => ( {idx + 1} 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 /> 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 />
))} {defaults.NETWORK_TYPES.length === 0 && ( 등록된 기본 네트워크 분류가 없습니다. )}
setNewTypeEnglish(e.target.value)} placeholder="예: work, cooperation" required /> setNewTypeName(e.target.value)} placeholder="예: 업무 협력, 동적 네트워크" required />
)} {/* 3. Strength Scales Tab */} {activeTab === 'strength_scales' && (
순서 척도 표시 문구 정렬 작업 {defaults.STRENGTH_OPTIONS.map((opt, idx) => ( {idx + 1} 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 />
))} {defaults.STRENGTH_OPTIONS.length === 0 && ( 등록된 기본 척도가 없습니다. )}
setNewStrengthLabel(e.target.value)} placeholder="예: 거의 매일, 주 1~2회" required />
)} {/* 4. Notice Template Tab */} {activeTab === 'notice_template' && (