본문 바로가기
카테고리 없음

[C# WinForms] 시스템 설정 저장 시 '변경 이력(Change Log)' 자동 기록 및 Restore 패턴 구현하기

by 공부봇 2026. 1. 8.
반응형

1. 핵심 설계 포인트

  • Audit Trail (감사 추적): 설정을 저장할 때 단순히 덮어쓰지 않고, "어떤 항목이 [기존 값]에서 [새로운 값]으로 바뀌었는지" 로그를 남겨 트러블 슈팅을 용이하게 합니다.
  • DRY (Don't Repeat Yourself): UI 컨트롤에 값을 채워 넣는 코드를 SetControlsFromConfig 함수 하나로 통합하여, Form_Load와 Restore 버튼에서 재사용합니다.
  • UX (사용자 경험): 중요한 변경이나 복원 전에는 반드시 MessageBox로 재확인을 거칩니다.

2. 전체 소스 코드

C#
 
using System;
using System.Collections.Generic;
using System.IO;
using System.Windows.Forms;
using A2_SOP_Linescan.Common;
using A2_SOP_Linescan.Managers;

namespace A2_SOP_Linescan.UI
{
    public partial class FormSystemGeneral : Form
    {
        // ... (생성자 및 초기화 코드 생략)

        #region [Event Handler] Save & Restore

        /// <summary>
        /// [Save] UI의 값을 메모리/파일에 저장하고, 변경된 내역을 로그로 남깁니다.
        /// </summary>
        private void btnSave_Click(object sender, EventArgs e)
        {
            try
            {
                // 1. 사용자 의사 확인
                if (MessageBox.Show(new Form { TopMost = true }, 
                    "설정을 저장하시겠습니까?\n(변경된 내용은 로그에 기록됩니다.)", 
                    "Save Configuration", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes)
                {
                    return;
                }

                // 2. 변경 사항 감지 (Change Detection)
                var changeLogData = new Dictionary<string, object>();
                var config = GlobalDefine.SystemConfig; // 현재 메모리에 있는 설정값

                // Helper 함수를 통해 [Old Value] -> [New Value] 비교
                CheckChange(changeLogData, "EquipmentName", config.EquipmentName, txtEquipmentName.Text);
                
                // Enum 및 Index 처리
                StageType newStageType = (StageType)cmbEquipmentType.SelectedIndex;
                CheckChange(changeLogData, "StageType", config.StageType.ToString(), newStageType.ToString());

                int newLineIndex = cmbLineIndex.SelectedIndex + 1;
                CheckChange(changeLogData, "ProductLine", config.ProductLine.ToString(), newLineIndex.ToString());

                int newPcNumber = cmbEquipmentIndex.SelectedIndex + 1;
                CheckChange(changeLogData, "PcNumber", config.PcNumber.ToString(), newPcNumber.ToString());

                CheckChange(changeLogData, "SaveOrgImgPath", config.SaveOrgImgPath, txtSaveOrgImg.Text);
                CheckChange(changeLogData, "SaveDetectImgPath", config.SaveDetectImgPath, txtSaveDetecImg.Text);

                // 3. 실제 값 적용 (UI -> Memory)
                config.EquipmentName     = txtEquipmentName.Text;
                config.StageType         = newStageType;
                config.ProductLine       = newLineIndex;
                config.PcNumber          = newPcNumber;
                config.SaveOrgImgPath    = txtSaveOrgImg.Text;
                config.SaveDetectImgPath = txtSaveDetecImg.Text;

                // 4. 파일 저장 (Memory -> Disk)
                string filePath = Path.Combine(GlobalDefine.Paths.System, GlobalDefine.Files.System);
                SystemConfig.Save(filePath, config);

                // 5. 로그 기록 (변경된 내용이 있을 때만 상세 기록)
                if (changeLogData.Count > 0)
                {
                    // LogManager가 Dictionary를 받아 "Key=[Old] -> [New]" 형태로 기록
                    LogManager.Instance.SettingChange("FormSystemGeneral", "System Configuration Saved", changeLogData);
                }
                else
                {
                    LogManager.Instance.Init("FormSystemGeneral", "Configuration Saved (No Changes).");
                }

                MessageBox.Show("저장되었습니다.", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information);
            }
            catch (Exception ex)
            {
                LogManager.Instance.Error("FormSystemGeneral", "Save Failed", ex);
                MessageBox.Show($"저장 실패: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }

        /// <summary>
        /// [Restore] 저장되지 않은 변경 사항을 취소하고, 파일에 저장된 최신 상태로 되돌립니다.
        /// </summary>
        private void btnRestore_Click(object sender, EventArgs e)
        {
            try
            {
                if (MessageBox.Show(new Form { TopMost = true },
                    "모든 변경 사항을 취소하고 원래대로 되돌리시겠습니까?",
                    "Restore Configuration", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) != DialogResult.Yes)
                {
                    return;
                }

                // 1. 설정 파일 다시 로드 (Disk -> Memory)
                // 메모리상의 값도 오염되었을 수 있으므로 파일에서 새로 읽어옵니다.
                string filePath = Path.Combine(GlobalDefine.Paths.System, GlobalDefine.Files.System);
                var loadedConfig = SystemConfig.Load(filePath);

                if (loadedConfig == null)
                {
                    MessageBox.Show("설정 파일을 불러올 수 없습니다.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    return;
                }

                // 2. 전역 설정 객체 교체
                GlobalDefine.SystemConfig = loadedConfig;

                // 3. UI 갱신 (Memory -> UI)
                // Form_Load 등에서도 쓰이는 공통 함수 호출
                SetControlsFromConfig(GlobalDefine.SystemConfig);

                // 4. 로그 기록
                LogManager.Instance.Init("FormSystemGeneral", "System Configuration Restored from file.");

                MessageBox.Show("설정이 복원되었습니다.", "Restored", MessageBoxButtons.OK, MessageBoxIcon.Information);
            }
            catch (Exception ex)
            {
                LogManager.Instance.Error("FormSystemGeneral", "Restore Failed", ex);
                MessageBox.Show($"복원 실패: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }

        #endregion

        #region [Helper Methods]

        /// <summary>
        /// 값이 변경되었는지 확인하고, 변경되었다면 로그용 Dictionary에 추가합니다.
        /// 저장 포맷: "[OldValue] -> [NewValue]"
        /// </summary>
        private void CheckChange(Dictionary<string, object> logs, string key, string oldValue, string newValue)
        {
            // Null 처리 및 문자열 비교
            if ((oldValue ?? string.Empty) != (newValue ?? string.Empty))
            {
                logs.Add(key, $"[{oldValue}] -> [{newValue}]");
            }
        }

        /// <summary>
        /// Config 객체의 데이터를 UI 컨트롤에 바인딩합니다.
        /// (Form_Load와 Restore 버튼에서 공통으로 사용)
        /// </summary>
        private void SetControlsFromConfig(SystemConfig config)
        {
            if (config == null) return;

            txtEquipmentName.Text = config.EquipmentName;

            // Enum -> ComboBox Index 변환
            if (Enum.IsDefined(typeof(StageType), config.StageType))
            {
                cmbEquipmentType.SelectedIndex = (int)config.StageType;
            }

            // 1-based Index -> 0-based Index 변환 (범위 체크 포함)
            int lineIdx = config.ProductLine - 1;
            cmbLineIndex.SelectedIndex = (lineIdx >= 0 && lineIdx < cmbLineIndex.Items.Count) ? lineIdx : 0;

            int pcIdx = config.PcNumber - 1;
            cmbEquipmentIndex.SelectedIndex = (pcIdx >= 0 && pcIdx < cmbEquipmentIndex.Items.Count) ? pcIdx : 0;

            txtSaveOrgImg.Text = config.SaveOrgImgPath;
            txtSaveDetecImg.Text = config.SaveDetectImgPath;
        }

        #endregion
    }
}

3. 적용 결과 (로그 파일 예시)

사용자가 장비 이름을 바꾸고 저장하면, 로그 파일(logs\...\SETTING_YYMMDD.log)에는 다음과 같이 명확한 증거가 남습니다.

Plaintext
 
[2026-01-08 14:30:00][SETTING][FormSystemGeneral] System Configuration Saved Equipmen
반응형