handOver2/core/config.py

910 lines
30 KiB
Python

# -*- coding: utf-8 -*-
"""
설정 관리 모듈
애플리케이션의 설정을 관리합니다.
이 모듈은 다음 기능을 제공합니다:
- 설정 파일 읽기/쓰기
- 기본 설정 값 관리
- 런타임 설정 변경
- 설정 유효성 검사
"""
import os
import json
import configparser
from pathlib import Path
from typing import Any, Dict, Optional, Union, List
from dataclasses import dataclass, field, asdict
from .constants import (
CONFIG_FILE,
DATA_DIR,
LAYOUT_RATIOS,
FONT_FAMILY,
FONT_SIZES,
UI_FONT_SETTINGS,
)
from .logger import get_logger
# 로거 설정
logger = get_logger(__name__)
# ============================================================================
# 설정 데이터 클래스
# ============================================================================
@dataclass
class AppSettings:
"""애플리케이션 일반 설정"""
language: str = "ko"
theme: str = "dark"
font_family: str = FONT_FAMILY
font_size: int = FONT_SIZES["body"]
auto_save: bool = True
auto_save_interval: int = 30 # 초
check_updates: bool = True
update_check_interval: int = 3600 # 초
start_minimized: bool = False
minimize_to_tray: bool = True
show_notifications: bool = True
@dataclass
class LayoutSettings:
"""레이아웃 설정"""
info_bar_ratio: float = LAYOUT_RATIOS["info_bar"]
content_area_ratio: float = LAYOUT_RATIOS["content_area"]
status_bar_ratio: float = LAYOUT_RATIOS["status_bar"]
section_panel_ratio: float = LAYOUT_RATIOS["section_panel"]
todo_panel_ratio: float = LAYOUT_RATIOS["todo_panel"]
daily_inspection_ratio: float = LAYOUT_RATIOS["daily_inspection"]
todo_list_ratio: float = LAYOUT_RATIOS["todo_list"]
memo_ratio: float = LAYOUT_RATIOS["memo"]
window_width: int = 1600
window_height: int = 900
window_x: int = -1 # -1은 화면 중앙
window_y: int = -1
@dataclass
class DatabaseSettings:
"""데이터베이스 설정"""
db_path: str = str(DATA_DIR / "handover.db")
backup_enabled: bool = True
backup_interval: int = 86400 # 24시간
backup_count: int = 7
sync_enabled: bool = False
sync_url: str = ""
sync_key: str = ""
sync_interval: int = 300 # 5분
@dataclass
class WeatherSettings:
"""날씨 설정"""
enabled: bool = True
api_key: str = ""
location_lat: float = 35.1796 # 부산 기본값
location_lon: float = 129.0756
location_name: str = "부산"
update_interval: int = 1800 # 30분
forecast_unit: str = "1시간 단위" # 1시간 단위 또는 3시간 단위
@dataclass
class FieldSetting:
"""필드 설정 데이터 클래스"""
name: str = ""
visible: bool = True
width: int = 100
display_format: Optional[str] = None # "full", "short", "month_day" 등
@dataclass
class UserSettings:
"""사용자 설정 (런타임)"""
current_team: str = "1팀"
current_shift: str = "주간"
last_user_id: int = 0
remember_login: bool = True
# 필드 설정: 팀별로 섹션별 필드 설정 저장
# 형식: {"팀명": {"섹션명": [FieldSetting, ...], ...}, ...}
field_settings: Dict[str, Dict[str, List[Dict[str, Any]]]] = field(default_factory=dict)
@dataclass
class TrainSettings:
"""편성 설정 (A: 구형, B: 신형)"""
# 기본값: 홀수=A(구형), 짝수=B(신형)
# 신평 차량: 6,7,8,9,13,16,32~48
train_1_type: str = "A"
train_2_type: str = "B"
train_3_type: str = "A"
train_4_type: str = "B"
train_5_type: str = "A"
train_6_type: str = "B"
train_7_type: str = "B"
train_8_type: str = "B"
train_9_type: str = "B"
train_10_type: str = "B"
train_11_type: str = "A"
train_12_type: str = "B"
train_13_type: str = "B"
train_14_type: str = "B"
train_15_type: str = "A"
train_16_type: str = "B"
train_17_type: str = "A"
train_18_type: str = "B"
train_19_type: str = "A"
train_20_type: str = "B"
train_21_type: str = "A"
train_22_type: str = "B"
train_23_type: str = "A"
train_24_type: str = "B"
train_25_type: str = "A"
train_26_type: str = "B"
train_27_type: str = "A"
train_28_type: str = "B"
train_29_type: str = "A"
train_30_type: str = "B"
train_31_type: str = "A"
train_32_type: str = "B"
train_33_type: str = "B"
train_34_type: str = "B"
train_35_type: str = "B"
train_36_type: str = "B"
train_37_type: str = "B"
train_38_type: str = "B"
train_39_type: str = "B"
train_40_type: str = "B"
train_41_type: str = "B"
train_42_type: str = "B"
train_43_type: str = "B"
train_44_type: str = "B"
train_45_type: str = "B"
train_46_type: str = "B"
train_47_type: str = "B"
train_48_type: str = "B"
train_49_type: str = "A"
train_50_type: str = "B"
train_51_type: str = "A"
@dataclass
class UIFontSettings:
"""UI 폰트 설정"""
# 인포바
info_bar_title_family: str = FONT_FAMILY
info_bar_title_size: int = 16
info_bar_title_weight: str = "bold"
info_bar_content_family: str = FONT_FAMILY
info_bar_content_size: int = 14
info_bar_content_weight: str = "normal"
# 섹션
section_title_family: str = FONT_FAMILY
section_title_size: int = 16
section_title_weight: str = "bold"
section_header_family: str = FONT_FAMILY
section_header_size: int = 13
section_header_weight: str = "bold"
section_content_family: str = FONT_FAMILY
section_content_size: int = 13
section_content_weight: str = "normal"
# 할일
todo_title_family: str = FONT_FAMILY
todo_title_size: int = 14
todo_title_weight: str = "bold"
todo_content_family: str = FONT_FAMILY
todo_content_size: int = 13
todo_content_weight: str = "normal"
# 메모
memo_title_family: str = FONT_FAMILY
memo_title_size: int = 14
memo_title_weight: str = "bold"
memo_content_family: str = FONT_FAMILY
memo_content_size: int = 13
memo_content_weight: str = "normal"
# 일상검수
daily_title_family: str = FONT_FAMILY
daily_title_size: int = 14
daily_title_weight: str = "bold"
daily_content_family: str = FONT_FAMILY
daily_content_size: int = 13
daily_content_weight: str = "normal"
daily_train_family: str = FONT_FAMILY
daily_train_size: int = 15
daily_train_weight: str = "bold"
# 상태바
status_content_family: str = FONT_FAMILY
status_content_size: int = 12
status_content_weight: str = "normal"
# 다이얼로그
dialog_title_family: str = FONT_FAMILY
dialog_title_size: int = 14
dialog_title_weight: str = "bold"
dialog_label_family: str = FONT_FAMILY
dialog_label_size: int = 12
dialog_label_weight: str = "normal"
dialog_input_family: str = FONT_FAMILY
dialog_input_size: int = 12
dialog_input_weight: str = "normal"
dialog_button_family: str = FONT_FAMILY
dialog_button_size: int = 12
dialog_button_weight: str = "medium"
@dataclass
class FieldSetting:
"""필드 설정 데이터 클래스"""
name: str
visible: bool = True
width: int = 100
display_format: Optional[str] = None # "full", "short", "month_day" 등
@dataclass
class AllSettings:
"""모든 설정을 통합하는 클래스"""
app: AppSettings = field(default_factory=AppSettings)
layout: LayoutSettings = field(default_factory=LayoutSettings)
database: DatabaseSettings = field(default_factory=DatabaseSettings)
weather: WeatherSettings = field(default_factory=WeatherSettings)
user: UserSettings = field(default_factory=UserSettings)
ui_font: UIFontSettings = field(default_factory=UIFontSettings)
train: TrainSettings = field(default_factory=TrainSettings)
# ============================================================================
# 설정 관리자 클래스
# ============================================================================
class ConfigManager:
"""
설정 관리자 클래스
싱글톤 패턴을 사용하여 애플리케이션 전역에서 하나의 인스턴스만 사용합니다.
설정 파일의 읽기/쓰기 및 런타임 설정 변경을 담당합니다.
Attributes:
config_path: 설정 파일 경로
settings: 현재 설정 객체
Examples:
>>> config = ConfigManager()
>>> config.get('app', 'theme')
'dark'
>>> config.set('app', 'theme', 'light')
>>> config.save()
"""
_instance: Optional['ConfigManager'] = None
def __new__(cls, config_path: Path = None):
"""싱글톤 패턴 구현"""
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self, config_path: Path = None):
"""
설정 관리자 초기화
Args:
config_path: 설정 파일 경로 (기본값: CONFIG_FILE)
"""
# 이미 초기화된 경우 건너뛰기
if self._initialized:
return
self.config_path = config_path or CONFIG_FILE
self.settings = AllSettings()
self._parser = configparser.ConfigParser()
# 필드 설정 파일 경로 (레거시, 마이그레이션용)
self.field_settings_file = DATA_DIR / "field_settings.json"
# SettingsManager 인스턴스 (지연 로딩)
self._settings_manager = None
# 설정 파일 로드
self.load()
# 필드 설정 로드 (JSON -> SQLite 마이그레이션 포함)
self._load_field_settings()
self._initialized = True
logger.info(f"설정 관리자 초기화 완료: {self.config_path}")
def load(self) -> bool:
"""
설정 파일에서 설정을 로드합니다.
Returns:
로드 성공 여부
"""
try:
if self.config_path.exists():
self._parser.read(self.config_path, encoding='utf-8')
self._load_settings_from_parser()
logger.info("설정 파일 로드 완료")
return True
else:
logger.warning("설정 파일이 없습니다. 기본 설정을 사용합니다.")
self._create_default_config()
return False
except Exception as e:
logger.error(f"설정 파일 로드 실패: {e}")
self._create_default_config()
return False
def save(self) -> bool:
"""
현재 설정을 파일에 저장합니다.
Returns:
저장 성공 여부
"""
try:
self._save_settings_to_parser()
# 부모 디렉토리 생성
self.config_path.parent.mkdir(parents=True, exist_ok=True)
with open(self.config_path, 'w', encoding='utf-8') as f:
self._parser.write(f)
logger.info("설정 파일 저장 완료")
return True
except Exception as e:
logger.error(f"설정 파일 저장 실패: {e}")
return False
def get(self, section: str, key: str, default: Any = None) -> Any:
"""
설정 값을 가져옵니다.
Args:
section: 설정 섹션 (app, layout, database, weather, user)
key: 설정 키
default: 기본값
Returns:
설정 값
Examples:
>>> config.get('app', 'theme')
'dark'
"""
try:
section_obj = getattr(self.settings, section, None)
if section_obj is None:
logger.warning(f"존재하지 않는 섹션: {section}")
return default
value = getattr(section_obj, key, default)
return value
except Exception as e:
logger.error(f"설정 값 가져오기 실패: {section}.{key} - {e}")
return default
def set(self, section: str, key: str, value: Any) -> bool:
"""
설정 값을 변경합니다.
Args:
section: 설정 섹션
key: 설정 키
value: 새로운 값
Returns:
변경 성공 여부
Examples:
>>> config.set('app', 'theme', 'light')
True
"""
try:
section_obj = getattr(self.settings, section, None)
if section_obj is None:
logger.warning(f"존재하지 않는 섹션: {section}")
return False
if not hasattr(section_obj, key):
logger.warning(f"존재하지 않는 키: {section}.{key}")
return False
setattr(section_obj, key, value)
logger.debug(f"설정 변경: {section}.{key} = {value}")
return True
except Exception as e:
logger.error(f"설정 값 변경 실패: {section}.{key} - {e}")
return False
def get_section(self, section: str) -> Optional[Dict[str, Any]]:
"""
설정 섹션 전체를 딕셔너리로 반환합니다.
Args:
section: 설정 섹션
Returns:
섹션 설정 딕셔너리
"""
try:
section_obj = getattr(self.settings, section, None)
if section_obj is None:
return None
return asdict(section_obj)
except Exception as e:
logger.error(f"섹션 가져오기 실패: {section} - {e}")
return None
def reset_to_default(self, section: str = None):
"""
설정을 기본값으로 초기화합니다.
Args:
section: 초기화할 섹션 (None이면 전체 초기화)
"""
if section:
if section == 'app':
self.settings.app = AppSettings()
elif section == 'layout':
self.settings.layout = LayoutSettings()
elif section == 'database':
self.settings.database = DatabaseSettings()
elif section == 'weather':
self.settings.weather = WeatherSettings()
elif section == 'user':
self.settings.user = UserSettings()
elif section == 'ui_font':
self.settings.ui_font = UIFontSettings()
elif section == 'train':
self.settings.train = TrainSettings()
logger.info(f"섹션 '{section}' 기본값으로 초기화")
else:
self.settings = AllSettings()
logger.info("전체 설정 기본값으로 초기화")
def _load_settings_from_parser(self):
"""파서에서 설정 객체로 로드"""
for section_name in ['app', 'layout', 'database', 'weather', 'user', 'ui_font', 'train']:
if self._parser.has_section(section_name):
section_obj = getattr(self.settings, section_name)
for key in self._parser.options(section_name):
if hasattr(section_obj, key):
raw_value = self._parser.get(section_name, key)
# 타입 변환
expected_type = type(getattr(section_obj, key))
converted_value = self._convert_value(raw_value, expected_type)
setattr(section_obj, key, converted_value)
def _save_settings_to_parser(self):
"""설정 객체에서 파서로 저장"""
for section_name in ['app', 'layout', 'database', 'weather', 'user', 'ui_font', 'train']:
if not self._parser.has_section(section_name):
self._parser.add_section(section_name)
section_obj = getattr(self.settings, section_name)
section_dict = asdict(section_obj)
for key, value in section_dict.items():
# 필드 설정은 JSON으로 저장
if key == 'field_settings':
self._parser.set(section_name, key, json.dumps(value, ensure_ascii=False))
else:
self._parser.set(section_name, key, str(value))
def _create_default_config(self):
"""기본 설정 파일 생성"""
self.settings = AllSettings()
self.save()
logger.info("기본 설정 파일 생성 완료")
@staticmethod
def _convert_value(value: str, target_type: type) -> Any:
"""
문자열 값을 대상 타입으로 변환합니다.
Args:
value: 변환할 문자열
target_type: 대상 타입
Returns:
변환된 값
"""
if target_type == bool:
return value.lower() in ('true', 'yes', '1', 'on')
elif target_type == int:
return int(value)
elif target_type == float:
return float(value)
else:
return value
# ========================================================================
# 편의 메서드
# ========================================================================
@property
def theme(self) -> str:
"""현재 테마 반환"""
return self.settings.app.theme
@theme.setter
def theme(self, value: str):
"""테마 설정"""
self.settings.app.theme = value
@property
def current_team(self) -> str:
"""현재 팀 반환"""
return self.settings.user.current_team
@current_team.setter
def current_team(self, value: str):
"""현재 팀 설정"""
self.settings.user.current_team = value
@property
def current_shift(self) -> str:
"""현재 근무 유형 반환"""
return self.settings.user.current_shift
@current_shift.setter
def current_shift(self, value: str):
"""현재 근무 유형 설정"""
self.settings.user.current_shift = value
def get_layout_sizes(self) -> Dict[str, float]:
"""레이아웃 크기 비율 반환"""
return asdict(self.settings.layout)
def get_ui_font(self, area: str, style: str) -> Dict[str, Any]:
"""
UI 영역별 폰트 설정 가져오기
Args:
area: 영역 (info_bar, section, todo, memo, daily, status, dialog)
style: 스타일 (title, content, header, label, input, button, train 등)
Returns:
{"family": str, "size": int, "weight": str}
Examples:
>>> config.get_ui_font("section", "title")
{"family": "GmarketSans", "size": 16, "weight": "bold"}
"""
ui_font = self.settings.ui_font
# 영역과 스타일 조합으로 속성 이름 생성
prefix = area.replace("_", "") # info_bar -> infobar
# 영역별 prefix 매핑
area_map = {
"info_bar": "info_bar",
"section": "section",
"todo": "todo",
"memo": "memo",
"daily": "daily",
"daily_inspection": "daily",
"status": "status",
"status_bar": "status",
"dialog": "dialog",
}
prefix = area_map.get(area, area)
# 속성 이름 생성
family_attr = f"{prefix}_{style}_family"
size_attr = f"{prefix}_{style}_size"
weight_attr = f"{prefix}_{style}_weight"
# 기본값
default = {"family": FONT_FAMILY, "size": 13, "weight": "normal"}
try:
return {
"family": getattr(ui_font, family_attr, default["family"]),
"size": getattr(ui_font, size_attr, default["size"]),
"weight": getattr(ui_font, weight_attr, default["weight"]),
}
except AttributeError:
logger.warning(f"UI 폰트 설정을 찾을 수 없음: {area}.{style}")
return default
def set_ui_font(self, area: str, style: str, family: str = None, size: int = None, weight: str = None):
"""
UI 영역별 폰트 설정 변경
Args:
area: 영역
style: 스타일
family: 폰트 패밀리 (None이면 변경 안함)
size: 폰트 크기 (None이면 변경 안함)
weight: 폰트 굵기 (None이면 변경 안함)
"""
ui_font = self.settings.ui_font
area_map = {
"info_bar": "info_bar",
"section": "section",
"todo": "todo",
"memo": "memo",
"daily": "daily",
"daily_inspection": "daily",
"status": "status",
"status_bar": "status",
"dialog": "dialog",
}
prefix = area_map.get(area, area)
if family is not None:
setattr(ui_font, f"{prefix}_{style}_family", family)
if size is not None:
setattr(ui_font, f"{prefix}_{style}_size", size)
if weight is not None:
setattr(ui_font, f"{prefix}_{style}_weight", weight)
logger.debug(f"UI 폰트 설정 변경: {area}.{style}")
@property
def ui_font_settings(self) -> 'UIFontSettings':
"""UI 폰트 설정 반환"""
return self.settings.ui_font
# ========================================================================
# 필드 설정 관리 메서드 (SettingsManager 사용)
# ========================================================================
def _get_settings_manager(self):
"""SettingsManager 인스턴스 반환 (지연 로딩)"""
if not hasattr(self, '_settings_manager') or self._settings_manager is None:
from .settings_manager import get_settings_manager
self._settings_manager = get_settings_manager()
return self._settings_manager
def _load_field_settings(self):
"""
필드 설정 로드 (레거시 JSON -> SQLite DB 마이그레이션)
기존 JSON 파일이 있으면 SQLite DB로 마이그레이션하고,
이후에는 DB에서 직접 로드합니다.
"""
try:
settings_mgr = self._get_settings_manager()
# 기존 JSON 파일 마이그레이션 체크
if self.field_settings_file.exists():
logger.info("기존 JSON 필드 설정을 SQLite DB로 마이그레이션합니다.")
self._migrate_json_to_db()
# 기존 section_field_settings.json 파일도 마이그레이션 체크
old_settings_file = DATA_DIR / "section_field_settings.json"
if old_settings_file.exists():
logger.info("기존 section_field_settings.json을 SQLite DB로 마이그레이션합니다.")
self._migrate_old_json_to_db(old_settings_file)
logger.info("필드 설정 로드 완료 (SQLite DB 사용)")
except Exception as e:
logger.error(f"필드 설정 로드 실패: {e}")
def _migrate_json_to_db(self):
"""JSON 파일을 SQLite DB로 마이그레이션"""
try:
with open(self.field_settings_file, 'r', encoding='utf-8') as f:
data = json.load(f)
settings_mgr = self._get_settings_manager()
from .settings_manager import FieldSetting as DBFieldSetting
for team, sections_data in data.items():
for section_name, fields_data in sections_data.items():
fields = []
for field_data in fields_data:
field = DBFieldSetting(
name=field_data.get('name', ''),
visible=field_data.get('visible', True),
width=field_data.get('width', 100),
display_format=field_data.get('display_format')
)
fields.append(field)
settings_mgr.save_field_settings(team, section_name, fields)
# 마이그레이션 완료 후 기존 파일 백업
backup_file = self.field_settings_file.with_suffix('.json.bak')
self.field_settings_file.rename(backup_file)
logger.info(f"JSON 파일을 SQLite DB로 마이그레이션 완료, 백업: {backup_file}")
except Exception as e:
logger.error(f"JSON 마이그레이션 실패: {e}")
def _migrate_old_json_to_db(self, old_file):
"""기존 형식의 JSON 파일을 SQLite DB로 마이그레이션"""
try:
with open(old_file, 'r', encoding='utf-8') as f:
old_data = json.load(f)
settings_mgr = self._get_settings_manager()
from .settings_manager import FieldSetting as DBFieldSetting
for team, team_data in old_data.items():
sections = team_data.get("sections", {})
for section_name, section_data in sections.items():
fields_data = section_data.get("fields", [])
fields = []
for field_data in fields_data:
field = DBFieldSetting(
name=field_data.get('name', ''),
visible=field_data.get('visible', True),
width=field_data.get('width', 100),
display_format=field_data.get('display_format')
)
fields.append(field)
settings_mgr.save_field_settings(team, section_name, fields)
# 마이그레이션 완료 후 기존 파일 백업
backup_file = old_file.with_suffix('.json.bak')
old_file.rename(backup_file)
logger.info(f"기존 JSON 파일을 SQLite DB로 마이그레이션 완료, 백업: {backup_file}")
except Exception as e:
logger.error(f"기존 JSON 마이그레이션 실패: {e}")
def save_field_settings(
self,
team: str,
section_name: str,
fields: List[FieldSetting]
):
"""
필드 설정 저장 (SQLite DB 사용)
Args:
team: 팀 이름 (예: "1팀")
section_name: 섹션 이름 (예: "고장")
fields: 필드 설정 리스트
"""
try:
settings_mgr = self._get_settings_manager()
from .settings_manager import FieldSetting as DBFieldSetting
# FieldSetting -> DBFieldSetting 변환
db_fields = []
for f in fields:
db_field = DBFieldSetting(
name=f.name,
visible=f.visible,
width=f.width,
display_format=f.display_format
)
db_fields.append(db_field)
settings_mgr.save_field_settings(team, section_name, db_fields)
logger.info(f"필드 설정 저장 완료: {team} - {section_name}")
except Exception as e:
logger.error(f"필드 설정 저장 실패: {e}")
raise
def load_field_settings(
self,
team: str,
section_name: str
) -> Optional[List[FieldSetting]]:
"""
필드 설정 로드 (SQLite DB 사용)
Args:
team: 팀 이름
section_name: 섹션 이름
Returns:
필드 설정 리스트 (없으면 None)
"""
try:
settings_mgr = self._get_settings_manager()
db_fields = settings_mgr.load_field_settings(team, section_name)
if not db_fields:
return None
# DBFieldSetting -> FieldSetting 변환
fields = []
for f in db_fields:
field = FieldSetting(
name=f.name,
visible=f.visible,
width=f.width,
display_format=f.display_format
)
fields.append(field)
return fields
except Exception as e:
logger.error(f"필드 설정 로드 실패: {e}")
return None
def get_field_setting(
self,
team: str,
section_name: str,
field_name: str
) -> Optional[FieldSetting]:
"""
특정 필드 설정 가져오기
Args:
team: 팀 이름
section_name: 섹션 이름
field_name: 필드 이름
Returns:
필드 설정 (없으면 None)
"""
fields = self.load_field_settings(team, section_name)
if not fields:
return None
for field_setting in fields:
if field_setting.name == field_name:
return field_setting
return None
def apply_field_settings_to_fields(
self,
team: str,
section_name: str,
fields: List[Any] # FieldConfig 리스트
):
"""
저장된 설정을 필드에 적용
Args:
team: 팀 이름
section_name: 섹션 이름
fields: FieldConfig 리스트
"""
settings_mgr = self._get_settings_manager()
settings_mgr.apply_field_settings_to_fields(team, section_name, fields)
logger.debug(f"필드 설정 적용 완료: {team} - {section_name}")
def reset_team_field_settings(self, team: str):
"""팀별 필드 설정 초기화"""
settings_mgr = self._get_settings_manager()
settings_mgr.reset_team_field_settings(team)
logger.info(f"팀 필드 설정 초기화: {team}")
def reset_all_field_settings(self):
"""모든 필드 설정 초기화"""
settings_mgr = self._get_settings_manager()
settings_mgr.reset_all_field_settings()
logger.info("모든 필드 설정 초기화 완료")
# ============================================================================
# 모듈 레벨 편의 함수
# ============================================================================
def get_config() -> ConfigManager:
"""
설정 관리자 인스턴스를 반환합니다.
Returns:
ConfigManager 인스턴스
"""
return ConfigManager()