Option Explicit

Sub 거래처명별_시트분리()

    '============================================================
    ' 목적:
    '   현재 활성 시트의 데이터를 B열 "거래처명" 기준으로 분리하여
    '   거래처명별 새 시트를 생성하고, 현재 파일에 그대로 저장한다.
    '
    ' 전제:
    '   - 현재 활성 시트가 원본 데이터 시트이다.
    '   - 1행은 헤더이다.
    '   - B열에 거래처명이 있다.
    '   - 원본 시트는 그대로 보존한다.
    '   - 실행 로그는 직접 실행창(Debug.Print)에 출력한다.
    '
    ' 직접 실행창 여는 방법:
    '   VBA 편집기에서 Ctrl + G
    '============================================================

    On Error GoTo ErrHandler

    Dim wb As Workbook
    Dim wsSrc As Worksheet
    Dim wsNew As Worksheet

    Dim lastRow As Long
    Dim lastCol As Long
    Dim dataRange As Range
    Dim visibleRange As Range

    Dim dict As Object
    Dim cell As Range

    Dim clientName As Variant
    Dim newSheetName As String

    Dim createdCount As Long
    Dim skippedCount As Long

    Dim startTime As Double

    '------------------------------------------------------------
    ' (1) 작업 시작 로그 출력
    '------------------------------------------------------------
    startTime = Timer

    Debug.Print String(80, "=")
    Debug.Print "[START] 거래처명별 시트 분리 시작"
    Debug.Print "[TIME ] " & Format(Now, "yyyy-mm-dd hh:nn:ss")
    Debug.Print String(80, "=")

    '------------------------------------------------------------
    ' (2) 현재 통합문서와 현재 활성 시트를 원본으로 지정
    '------------------------------------------------------------
    Set wb = ActiveWorkbook
    Set wsSrc = ActiveSheet

    Debug.Print "[1] 원본 통합문서: " & wb.Name
    Debug.Print "[1] 원본 시트명: " & wsSrc.Name

    '------------------------------------------------------------
    ' (3) 원본 데이터의 마지막 행과 마지막 열 확인
    '     - B열을 기준으로 마지막 행을 찾는다.
    '     - 1행을 기준으로 마지막 열을 찾는다.
    '------------------------------------------------------------
    lastRow = wsSrc.Cells(wsSrc.Rows.Count, "B").End(xlUp).Row
    lastCol = wsSrc.Cells(1, wsSrc.Columns.Count).End(xlToLeft).Column

    Debug.Print "[2] 마지막 행: " & lastRow
    Debug.Print "[2] 마지막 열: " & lastCol

    '------------------------------------------------------------
    ' (4) 데이터가 충분한지 검증
    '     - 헤더만 있고 데이터가 없으면 작업을 중단한다.
    '------------------------------------------------------------
    If lastRow < 2 Then
        MsgBox "분리할 데이터가 없습니다. 2행부터 데이터가 있는지 확인해 주세요.", vbExclamation
        Debug.Print "[STOP] 데이터 행이 없어 작업 중단"
        GoTo CleanExit
    End If

    If lastCol < 2 Then
        MsgBox "B열 거래처명 데이터가 없습니다. 원본 데이터를 확인해 주세요.", vbExclamation
        Debug.Print "[STOP] B열이 존재하지 않아 작업 중단"
        GoTo CleanExit
    End If

    '------------------------------------------------------------
    ' (5) 원본 데이터 전체 범위 지정
    '     - A1부터 마지막 행/마지막 열까지를 데이터 범위로 사용한다.
    '     - 이 범위에 필터를 적용하고, 필터링된 결과를 복사한다.
    '------------------------------------------------------------
    Set dataRange = wsSrc.Range(wsSrc.Cells(1, 1), wsSrc.Cells(lastRow, lastCol))

    Debug.Print "[3] 데이터 범위: " & dataRange.Address(False, False)

    '------------------------------------------------------------
    ' (6) 거래처명 중복 제거용 Dictionary 생성
    '     - B열 2행부터 마지막 행까지 읽는다.
    '     - 빈 값은 제외한다.
    '------------------------------------------------------------
    Set dict = CreateObject("Scripting.Dictionary")
    dict.CompareMode = vbTextCompare

    For Each cell In wsSrc.Range("B2:B" & lastRow)

        If Trim(CStr(cell.Value)) <> "" Then

            If Not dict.Exists(Trim(CStr(cell.Value))) Then
                dict.Add Trim(CStr(cell.Value)), Trim(CStr(cell.Value))
                Debug.Print "[4] 거래처명 추가: " & Trim(CStr(cell.Value))
            End If

        Else
            skippedCount = skippedCount + 1
        End If

    Next cell

    Debug.Print "[4] 고유 거래처 수: " & dict.Count
    Debug.Print "[4] 빈 거래처명 행 수: " & skippedCount

    If dict.Count = 0 Then
        MsgBox "B열에서 거래처명을 찾지 못했습니다.", vbExclamation
        Debug.Print "[STOP] 거래처명 없음"
        GoTo CleanExit
    End If

    '------------------------------------------------------------
    ' (7) 기존 필터가 있으면 해제
    '     - 원본 시트에 이미 필터가 걸려 있으면 결과가 꼬일 수 있으므로 초기화한다.
    '------------------------------------------------------------
    If wsSrc.AutoFilterMode Then
        wsSrc.AutoFilterMode = False
        Debug.Print "[5] 기존 자동 필터 해제 완료"
    End If

    '------------------------------------------------------------
    ' (8) 거래처명별로 필터링하고 새 시트 생성
    '------------------------------------------------------------
    Application.ScreenUpdating = False
    Application.DisplayAlerts = False

    For Each clientName In dict.Keys

        Debug.Print String(80, "-")
        Debug.Print "[6] 처리 시작 거래처명: " & CStr(clientName)

        '--------------------------------------------------------
        ' (8-1) 거래처명을 엑셀 시트명으로 사용할 수 있게 정리
        '       - 사용할 수 없는 문자는 _ 로 변경
        '       - 최대 31자 제한 적용
        '--------------------------------------------------------
        newSheetName = GetSafeSheetName(CStr(clientName))

        Debug.Print "[6-1] 변환된 시트명: " & newSheetName

        '--------------------------------------------------------
        ' (8-2) 같은 이름의 시트가 이미 있으면 삭제
        '       - 기존 결과가 남아 있으면 중복 오류가 발생하므로 삭제 후 재생성한다.
        '       - 원본 시트와 같은 이름이면 삭제하지 않고 건너뛴다.
        '--------------------------------------------------------
        If SheetExists(newSheetName, wb) Then

            If wb.Worksheets(newSheetName).Name = wsSrc.Name Then
                Debug.Print "[WARN] 생성할 시트명이 원본 시트명과 같아 건너뜀: " & newSheetName
                GoTo NextClient
            Else
                Debug.Print "[6-2] 기존 시트 삭제: " & newSheetName
                wb.Worksheets(newSheetName).Delete
            End If

        End If

        '--------------------------------------------------------
        ' (8-3) 원본 데이터에 거래처명 기준 필터 적용
        '       - Field:=2 는 데이터 범위의 두 번째 열, 즉 B열을 의미한다.
        '--------------------------------------------------------
        dataRange.AutoFilter Field:=2, Criteria1:=CStr(clientName)

        Debug.Print "[6-3] 필터 적용 완료: " & CStr(clientName)

        '--------------------------------------------------------
        ' (8-4) 필터링 후 보이는 셀 범위 가져오기
        '       - 헤더와 필터링된 데이터가 함께 포함된다.
        '       - 해당 거래처 데이터가 없으면 오류가 발생할 수 있어 별도 처리한다.
        '--------------------------------------------------------
        On Error Resume Next
        Set visibleRange = dataRange.SpecialCells(xlCellTypeVisible)
        On Error GoTo ErrHandler

        If visibleRange Is Nothing Then
            Debug.Print "[WARN] 필터링된 데이터 없음: " & CStr(clientName)
            GoTo NextClient
        End If

        '--------------------------------------------------------
        ' (8-5) 새 시트 생성
        '       - 새 시트는 통합문서의 맨 뒤에 생성한다.
        '--------------------------------------------------------
        Set wsNew = wb.Worksheets.Add(After:=wb.Worksheets(wb.Worksheets.Count))
        wsNew.Name = newSheetName

        Debug.Print "[6-4] 새 시트 생성 완료: " & wsNew.Name

        '--------------------------------------------------------
        ' (8-6) 필터링된 데이터를 새 시트로 복사
        '       - Copy 메서드를 사용하여 값, 수식, 서식, 열 너비 등 최대한 유지한다.
        '       - 필터로 숨겨진 행은 복사되지 않고 보이는 행만 복사된다.
        '--------------------------------------------------------
        visibleRange.Copy Destination:=wsNew.Range("A1")

        Debug.Print "[6-5] 데이터 복사 완료"

        '--------------------------------------------------------
        ' (8-7) 열 너비 복사
        '       - 일반 Copy만으로 열 너비가 완전히 동일하지 않을 수 있어 별도 복사한다.
        '--------------------------------------------------------
        wsSrc.Range(wsSrc.Cells(1, 1), wsSrc.Cells(1, lastCol)).Copy
        wsNew.Range("A1").PasteSpecial Paste:=xlPasteColumnWidths
        Application.CutCopyMode = False

        Debug.Print "[6-6] 열 너비 복사 완료"

        '--------------------------------------------------------
        ' (8-8) 새 시트의 보기 위치를 A1로 이동
        '--------------------------------------------------------
        wsNew.Activate
        wsNew.Range("A1").Select

        createdCount = createdCount + 1

        Debug.Print "[6-7] 처리 완료 거래처명: " & CStr(clientName)

NextClient:

        '--------------------------------------------------------
        ' (8-9) 다음 반복을 위해 객체 초기화
        '--------------------------------------------------------
        Set visibleRange = Nothing
        Set wsNew = Nothing

    Next clientName

    '------------------------------------------------------------
    ' (9) 원본 시트의 필터 해제 및 원본 시트로 복귀
    '------------------------------------------------------------
    wsSrc.Activate

    If wsSrc.AutoFilterMode Then
        wsSrc.AutoFilterMode = False
        Debug.Print "[7] 원본 시트 자동 필터 해제 완료"
    End If

    wsSrc.Range("A1").Select

    '------------------------------------------------------------
    ' (10) 현재 파일 저장
    '      - 다른 이름으로 저장하지 않고 현재 통합문서에 그대로 저장한다.
    '------------------------------------------------------------
    wb.Save

    Debug.Print "[8] 현재 파일 저장 완료: " & wb.FullName

    '------------------------------------------------------------
    ' (11) 완료 메시지 출력
    '------------------------------------------------------------
    Debug.Print String(80, "=")
    Debug.Print "[END] 거래처명별 시트 분리 완료"
    Debug.Print "[RESULT] 생성된 시트 수: " & createdCount
    Debug.Print "[TIME] 소요 시간: " & Format(Timer - startTime, "0.00") & "초"
    Debug.Print String(80, "=")

    MsgBox "거래처명별 시트 분리가 완료되었습니다." & vbCrLf & _
           "생성된 시트 수: " & createdCount & vbCrLf & _
           "현재 파일에 저장되었습니다.", vbInformation

CleanExit:

    '------------------------------------------------------------
    ' (12) 작업 환경 복구
    '      - 오류 발생 여부와 관계없이 화면 업데이트와 경고창 설정을 원래대로 돌린다.
    '------------------------------------------------------------
    Application.ScreenUpdating = True
    Application.DisplayAlerts = True
    Application.CutCopyMode = False

    Exit Sub

ErrHandler:

    '------------------------------------------------------------
    ' (13) 오류 처리
    '      - 오류 번호와 오류 내용을 직접 실행창과 메시지박스에 출력한다.
    '      - 원본 시트 필터가 남아 있으면 해제한다.
    '------------------------------------------------------------
    Debug.Print String(80, "!")
    Debug.Print "[ERROR] 오류 발생"
    Debug.Print "[ERROR] 번호: " & Err.Number
    Debug.Print "[ERROR] 내용: " & Err.Description
    Debug.Print "[ERROR] 발생 시각: " & Format(Now, "yyyy-mm-dd hh:nn:ss")
    Debug.Print String(80, "!")

    On Error Resume Next

    If Not wsSrc Is Nothing Then
        wsSrc.Activate

        If wsSrc.AutoFilterMode Then
            wsSrc.AutoFilterMode = False
            Debug.Print "[ERROR-RECOVERY] 원본 시트 필터 해제 완료"
        End If
    End If

    Application.ScreenUpdating = True
    Application.DisplayAlerts = True
    Application.CutCopyMode = False

    MsgBox "작업 중 오류가 발생했습니다." & vbCrLf & _
           "오류 번호: " & Err.Number & vbCrLf & _
           "오류 내용: " & Err.Description & vbCrLf & vbCrLf & _
           "자세한 실행 로그는 VBA 직접 실행창(Ctrl + G)을 확인해 주세요.", vbCritical

End Sub


Function GetSafeSheetName(ByVal rawName As String) As String

    '============================================================
    ' 목적:
    '   거래처명을 엑셀 시트명으로 사용할 수 있는 안전한 이름으로 변환한다.
    '
    ' 처리 내용:
    '   - 앞뒤 공백 제거
    '   - 시트명에 사용할 수 없는 문자 제거 또는 대체
    '   - 빈 이름 방지
    '   - 31자 제한 적용
    '============================================================

    Dim safeName As String
    Dim invalidChars As Variant
    Dim ch As Variant

    '------------------------------------------------------------
    ' (1) 앞뒤 공백 제거
    '------------------------------------------------------------
    safeName = Trim(rawName)

    '------------------------------------------------------------
    ' (2) 엑셀 시트명에 사용할 수 없는 문자 목록
    '     사용할 수 없는 문자:
    '     \ / : * ? [ ]
    '------------------------------------------------------------
    invalidChars = Array("\", "/", ":", "*", "?", "[", "]")

    '------------------------------------------------------------
    ' (3) 사용할 수 없는 문자를 _ 로 변경
    '------------------------------------------------------------
    For Each ch In invalidChars
        safeName = Replace(safeName, CStr(ch), "_")
    Next ch

    '------------------------------------------------------------
    ' (4) 작은따옴표로만 구성되거나 빈 이름이 되는 경우 방지
    '------------------------------------------------------------
    safeName = Replace(safeName, "'", "")

    If Len(Trim(safeName)) = 0 Then
        safeName = "거래처명없음"
    End If

    '------------------------------------------------------------
    ' (5) 엑셀 시트명은 최대 31자까지만 가능
    '------------------------------------------------------------
    If Len(safeName) > 31 Then
        safeName = Left(safeName, 31)
    End If

    GetSafeSheetName = safeName

End Function


Function SheetExists(ByVal sheetName As String, ByVal wb As Workbook) As Boolean

    '============================================================
    ' 목적:
    '   특정 이름의 시트가 현재 통합문서에 존재하는지 확인한다.
    '
    ' 반환값:
    '   True  : 존재함
    '   False : 존재하지 않음
    '============================================================

    Dim ws As Worksheet

    '------------------------------------------------------------
    ' (1) 기본값은 False로 설정
    '------------------------------------------------------------
    SheetExists = False

    '------------------------------------------------------------
    ' (2) 통합문서의 모든 시트를 순회하며 이름 비교
    '------------------------------------------------------------
    For Each ws In wb.Worksheets

        If ws.Name = sheetName Then
            SheetExists = True
            Exit Function
        End If

    Next ws

End Function

