'@Folder modRepositoryExport
Option Explicit

' ==========================================================
'  Excel VBA 프로젝트 내보내기 도구
'  대상: 활성 통합 문서의 VBProject
'
'  출력:
'   - UserForm          → FormsCsv / FormCode
'   - 표준 모듈         → Modules
'   - 클래스 모듈       → Classes
'   - ThisWorkbook/Sheet → Documents
' ==========================================================

Private Const vbext_ct_StdModule As Long = 1
Private Const vbext_ct_ClassModule As Long = 2
Private Const vbext_ct_MSForm As Long = 3
Private Const vbext_ct_Document As Long = 100

Private Const vbext_pp_locked As Long = 1

Private Const MAX_FILES_IN_MESSAGE As Long = 20

' 이번 내보내기에서 쓴 파일 경로(소문자) 목록
Private m_exportedFiles As Object

Public Sub ExportRepositoryForActiveWorkbook()

    Dim wb As Workbook
    Dim vbproj As Object
    Dim vbComp As Object
    Dim currentTarget As String
    Dim staleMessage As String

    Dim baseFolder As String
    Dim formsCsvFolder As String
    Dim formCodeFolder As String
    Dim modulesFolder As String
    Dim classesFolder As String
    Dim documentsFolder As String
    
    Set wb = ActiveWorkbook
    
    If wb Is Nothing Then
        MsgBox "활성화된 통합 문서가 없습니다.", vbExclamation
        Exit Sub
    End If
    
    If wb.Path = vbNullString Then
        MsgBox "통합 문서가 아직 저장되지 않았습니다. 먼저 저장해 주세요.", vbExclamation
        Exit Sub
    End If
    
    Set vbproj = GetVBProjectSafe(wb)
    If vbproj Is Nothing Then Exit Sub

    Dim repositoryRoot As String
    Dim vbaFolder As String
    Dim formsFolder As String
    
    repositoryRoot = wb.Path & "\ExcelRepository"
    baseFolder = repositoryRoot & "\" & GetNameWithoutExtension(wb.Name) & "_repository"
    vbaFolder = baseFolder & "\VBA"
    
    formsFolder = vbaFolder & "\Forms"
    formCodeFolder = formsFolder & "\Code"
    formsCsvFolder = formsFolder & "\ControlsCsv"
    modulesFolder = vbaFolder & "\Modules"
    classesFolder = vbaFolder & "\Classes"
    documentsFolder = vbaFolder & "\Documents"
    
    CreateFolderIfNotExists repositoryRoot
    CreateFolderIfNotExists baseFolder
    CreateFolderIfNotExists vbaFolder
    CreateFolderIfNotExists formsFolder
    CreateFolderIfNotExists formsCsvFolder
    CreateFolderIfNotExists formCodeFolder
    CreateFolderIfNotExists modulesFolder
    CreateFolderIfNotExists classesFolder
    CreateFolderIfNotExists documentsFolder

    Set m_exportedFiles = CreateObject("Scripting.Dictionary")

    On Error GoTo Err_Handler

    For Each vbComp In vbproj.VBComponents
        currentTarget = vbComp.Name

        Select Case vbComp.Type
            Case vbext_ct_MSForm
                ExportFormControlsToCsv vbComp, formsCsvFolder
                ExportFormCodeToCls vbComp, formCodeFolder
                
            Case vbext_ct_StdModule
                ExportStdModuleToBas vbComp, modulesFolder
                
            Case vbext_ct_ClassModule
                ExportClassModuleToCls vbComp, classesFolder
                
            Case vbext_ct_Document
                ExportDocumentModuleCodeOnly vbComp, documentsFolder
                
            Case Else
                ' 대상 아님
        End Select
    Next vbComp

    currentTarget = "Workbook 정보"
    Call ExportWorkbookRepository(wb, baseFolder)

    On Error GoTo 0

    ' Excel에서 지운 모듈의 파일이 남아 있으면
    ' Restorer가 그 모듈을 되살리므로 정리한다
    staleMessage = CleanUpStaleFiles(baseFolder, Array( _
                        Array(modulesFolder, "bas"), _
                        Array(classesFolder, "cls"), _
                        Array(documentsFolder, "cls"), _
                        Array(formCodeFolder, "cls"), _
                        Array(formsCsvFolder, "csv")))

    MsgBox "내보내기 완료: " & vbCrLf & baseFolder & staleMessage, vbInformation
    Exit Sub

Err_Handler:
    MsgBox "내보내기 중 오류가 발생해 중단했습니다." & vbCrLf & _
           "대상: " & currentTarget & vbCrLf & _
           Err.Number & ": " & Err.Description & vbCrLf & vbCrLf & _
           "출력 폴더의 파일이 일부만 갱신되었을 수 있습니다.", vbCritical

End Sub

' ==========================================================
'  남은 파일 정리
' ==========================================================

' folderSpecs: Array(Array(폴더, 확장자), ...)
' 반환값: 완료 메시지 뒤에 붙일 안내 문구
Private Function CleanUpStaleFiles(ByVal baseFolder As String, ByVal folderSpecs As Variant) As String

    Dim fso As Object
    Dim file As Object
    Dim staleFiles As Collection
    Dim spec As Variant
    Dim message As String
    Dim i As Long

    Set fso = CreateObject("Scripting.FileSystemObject")
    Set staleFiles = New Collection

    For Each spec In folderSpecs
        If fso.FolderExists(spec(0)) Then
            For Each file In fso.GetFolder(spec(0)).Files
                If LCase$(fso.GetExtensionName(file.Name)) = spec(1) Then
                    If Not m_exportedFiles.Exists(LCase$(file.Path)) Then
                        staleFiles.Add file.Path
                    End If
                End If
            Next file
        End If
    Next spec

    If staleFiles.Count = 0 Then Exit Function

    message = "Excel에는 더 이상 없는 구성 요소의 파일이 " & staleFiles.Count & "개 남아 있습니다." & vbCrLf & _
              "그대로 두면 Restorer가 이 파일들을 다시 Excel로 가져옵니다." & vbCrLf & vbCrLf

    For i = 1 To staleFiles.Count
        If i > MAX_FILES_IN_MESSAGE Then
            message = message & "… 외 " & (staleFiles.Count - MAX_FILES_IN_MESSAGE) & "개" & vbCrLf
            Exit For
        End If
        message = message & "- " & Mid$(staleFiles(i), Len(baseFolder) + 2) & vbCrLf
    Next i

    message = message & vbCrLf & "이 파일들을 삭제할까요?" & vbCrLf & _
              "(Git으로 관리 중이면 나중에 되돌릴 수 있습니다)"

    If MsgBox(message, vbYesNo + vbQuestion + vbDefaultButton2) = vbYes Then
        For i = 1 To staleFiles.Count
            fso.DeleteFile staleFiles(i), True
        Next i
        CleanUpStaleFiles = vbCrLf & vbCrLf & "남아 있던 파일 " & staleFiles.Count & "개를 삭제했습니다."
    Else
        CleanUpStaleFiles = vbCrLf & vbCrLf & "주의: 남아 있던 파일 " & staleFiles.Count & "개를 그대로 두었습니다." & vbCrLf & _
                            "가져오기 전에 직접 정리해 주세요."
    End If

End Function

Private Sub RegisterExportedFile(ByVal filePath As String)

    m_exportedFiles(LCase$(filePath)) = True

End Sub

' ==========================================================
'  UserForm
' ==========================================================

Private Sub ExportFormControlsToCsv(ByVal vbComp As Object, ByVal folderPath As String)

    Dim designer As Object
    Dim ctrl As Object
    Dim filePath As String
    Dim line As String
    Dim allText As String
    
    On Error Resume Next
    Set designer = vbComp.designer
    On Error GoTo 0
    
    If designer Is Nothing Then Exit Sub
    
    filePath = folderPath & "\" & vbComp.Name & ".csv"
    
    allText = "ControlName,ControlType,Caption,Left,Top,Width,Height," & _
              "FontName,FontSize,FontBold,ForeColor,BackColor" & vbCrLf

    For Each ctrl In designer.Controls
    
        line = EscapeCsv(ctrl.Name) & "," & _
               EscapeCsv(TypeName(ctrl)) & "," & _
               EscapeCsv(GetCtrlCaptionSafe(ctrl)) & "," & _
               CStr(ctrl.Left) & "," & _
               CStr(ctrl.Top) & "," & _
               CStr(ctrl.Width) & "," & _
               CStr(ctrl.Height) & "," & _
               EscapeCsv(GetCtrlPropertySafe(ctrl, "Font.Name")) & "," & _
               EscapeCsv(GetCtrlPropertySafe(ctrl, "Font.Size")) & "," & _
               EscapeCsv(GetCtrlPropertySafe(ctrl, "Font.Bold")) & "," & _
               EscapeCsv(GetCtrlPropertySafe(ctrl, "ForeColor")) & "," & _
               EscapeCsv(GetCtrlPropertySafe(ctrl, "BackColor"))
    
        allText = allText & line & vbCrLf
    
    Next ctrl

    Call SaveUtf8NoBom(filePath, allText)
    RegisterExportedFile filePath

End Sub

Private Sub ExportFormCodeToCls(ByVal vbComp As Object, ByVal folderPath As String)

    Dim cm As Object
    Dim codeText As String
    Dim filePath As String
    Dim lineCount As Long
    
    Set cm = vbComp.CodeModule
    lineCount = cm.CountOfLines
    If lineCount <= 0 Then Exit Sub
    
    codeText = cm.lines(1, lineCount)
    codeText = RemoveAttributeLines(codeText)
    
    filePath = folderPath & "\" & vbComp.Name & ".cls"

    Call SaveUtf8NoBom(filePath, codeText)
    RegisterExportedFile filePath

End Sub

' ==========================================================
'  표준 모듈 / 클래스 모듈 / Document 모듈
' ==========================================================

Private Sub ExportStdModuleToBas(ByVal vbComp As Object, ByVal folderPath As String)

    Dim filePath As String
    
    filePath = folderPath & "\" & vbComp.Name & ".bas"

    vbComp.Export filePath
    Call ConvertExportedFileToUtf8(filePath)
    RegisterExportedFile filePath

End Sub

Private Sub ExportClassModuleToCls(ByVal vbComp As Object, ByVal folderPath As String)

    Dim filePath As String
    
    filePath = folderPath & "\" & vbComp.Name & ".cls"

    vbComp.Export filePath
    Call ConvertExportedFileToUtf8(filePath)
    RegisterExportedFile filePath

End Sub

Private Sub ExportDocumentModuleCodeOnly(ByVal vbComp As Object, ByVal folderPath As String)

    Dim filePath As String
    Dim codeText As String
    Dim lineCount As Long

    filePath = folderPath & "\" & vbComp.Name & ".cls"

    With vbComp.CodeModule
        lineCount = .CountOfLines
        If lineCount > 0 Then
            codeText = .lines(1, lineCount)
            codeText = RemoveAttributeLines(codeText)
        End If
    End With

    ' ANSI 파일을 거치지 않고 바로 UTF-8로 저장한다
    ' (거치면 CP949에 없는 문자가 깨진다)
    Call SaveUtf8NoBom(filePath, codeText)
    RegisterExportedFile filePath

End Sub

' ==========================================================
'  헬퍼
' ==========================================================

Private Function GetNameWithoutExtension(ByVal fileName As String) As String

    Dim pos As Long
    
    pos = InStrRev(fileName, ".")
    
    If pos > 0 Then
        GetNameWithoutExtension = Left$(fileName, pos - 1)
    Else
        GetNameWithoutExtension = fileName
    End If

End Function

Private Sub CreateFolderIfNotExists(ByVal folderPath As String)

    Dim fso As Object
    
    Set fso = CreateObject("Scripting.FileSystemObject")
    
    If Not fso.FolderExists(folderPath) Then
        fso.CreateFolder folderPath
    End If

End Sub

Private Function GetCtrlCaptionSafe(ByVal ctrl As Object) As String

    On Error Resume Next
    GetCtrlCaptionSafe = ctrl.caption
    
    If Err.Number <> 0 Then
        GetCtrlCaptionSafe = vbNullString
        Err.Clear
    End If
    
    On Error GoTo 0

End Function

Private Function EscapeCsv(ByVal text As String) As String

    Dim t As String
    
    t = Replace(text, """", """""")
    EscapeCsv = """" & t & """"

End Function

' vbComp.Export 가 만든 ANSI 파일을 UTF-8로 바꾼다.
' 기본값 Attribute 줄만 지우고, VB_PredeclaredId = True 처럼
' 동작에 영향을 주는 Attribute 줄은 남긴다.
Private Sub ConvertExportedFileToUtf8(ByVal filePath As String)

    Dim textData As String

    If Dir$(filePath) = vbNullString Then Exit Sub

    textData = ReadTextFile(filePath)
    textData = RemoveDefaultAttributeLines(textData)

    Call SaveUtf8NoBom(filePath, textData)

End Sub

Private Function GetVBProjectSafe(ByVal wb As Workbook) As Object

    Dim vbproj As Object

    On Error Resume Next
    Set vbproj = wb.VBProject
    On Error GoTo 0

    If vbproj Is Nothing Then
        MsgBox "VBA 프로젝트에 접근할 수 없습니다." & vbCrLf & _
               "보안 센터에서 'VBA 프로젝트 개체 모델에 안전하게 액세스할 수 있음'을 켜 주세요.", vbExclamation
        Exit Function
    End If

    If vbproj.Protection = vbext_pp_locked Then
        MsgBox "VBA 프로젝트가 암호로 잠겨 있습니다. 잠금을 푼 뒤 다시 실행해 주세요.", vbExclamation
        Exit Function
    End If

    Set GetVBProjectSafe = vbproj

End Function

Private Function GetCtrlPropertySafe(ByVal ctrl As Object, ByVal propertyName As String) As String

    On Error GoTo Err_Handler

    Select Case propertyName

        Case "Font.Name"
            GetCtrlPropertySafe = CStr(ctrl.Font.Name)

        Case "Font.Size"
            GetCtrlPropertySafe = CStr(ctrl.Font.Size)

        Case "Font.Bold"
            GetCtrlPropertySafe = CStr(ctrl.Font.Bold)

        Case Else
            GetCtrlPropertySafe = CStr(CallByName(ctrl, propertyName, VbGet))

    End Select

    Exit Function

Err_Handler:
    GetCtrlPropertySafe = vbNullString

End Function


