Attribute VB_Name = "취합매크로" '============================================================================== ' 취합매크로 - 지점별 시트 취합 / 지점별 파일 나누기 ' (견본) (주)바이브먼트 · AI 에이전트 작성 · 데이터는 모두 가상 ' ' 매크로 목록 (Alt+F8 에서 보이는 것) ' 1) 취합하기 : 지점 시트 전부를 [취합] 시트 한 곳에 모은다 ' 2) 지점별파일로저장 : [취합] 시트를 지점별 새 통합문서(.xlsx)로 나눠 저장한다 ' ' 설정은 [설정] 시트 A열(항목) / B열(값)에서 읽는다. 시트 이름을 코드에 박지 않는다. ' 코드에 고정된 이름은 설정 시트 이름("설정") 하나뿐이다. '============================================================================== Option Explicit ' 설정 시트 이름 - 이 하나만 코드에 고정 Private Const CONFIG_SHEET As String = "설정" ' 설정 항목 이름 ([설정] 시트 A열에 적힌 글자와 같아야 한다) Private Const KEY_TARGET As String = "취합 시트 이름" Private Const KEY_EXCLUDE As String = "제외할 시트(쉼표로 구분)" Private Const KEY_HEADER_ROW As String = "제목 행 번호" Private Const KEY_SOURCE_TITLE As String = "출처 열 제목" Private Const KEY_SUBFOLDER As String = "저장 하위 폴더 이름" Private Const KEY_PREFIX As String = "파일 이름 앞말" ' 설정이 비었을 때 쓰는 기본값 Private Const DEF_TARGET As String = "취합" Private Const DEF_EXCLUDE As String = "사용법,설정" Private Const DEF_HEADER_ROW As Long = 1 Private Const DEF_SOURCE_TITLE As String = "지점" Private Const DEF_SUBFOLDER As String = "지점별_파일" Private Const DEF_PREFIX As String = "" ' 저장 형식: 51 = xlOpenXMLWorkbook(.xlsx). 이름 상수 대신 숫자를 쓰면 LibreOffice 등에서도 동작 Private Const FILE_FORMAT_XLSX As Long = 51 ' 화면/계산 설정 복원용 Private mOldCalc As Long Private mOldEvents As Boolean Private mFastOn As Boolean '------------------------------------------------------------------------------ ' 1) 취합하기 ' - 취합 시트와 제외 시트를 뺀 모든 시트를 왼쪽부터 순서대로 돈다 ' - 제목(헤더) 행은 맨 위에 한 번만, 맨 앞에 출처(시트 이름) 열을 붙인다 ' - 실행할 때마다 기존 취합 내용을 지우고 처음부터 다시 만든다 ' - 빈 시트 / 제목만 있는 시트 / 제목이 첫 지점과 다른 시트는 건너뛰고 알려준다 '------------------------------------------------------------------------------ Public Sub 취합하기() Dim wb As Workbook Dim ws As Worksheet Dim wsT As Worksheet Dim targetName As String Dim excludeList As String Dim sourceTitle As String Dim headerRow As Long Dim lastRow As Long Dim lastCol As Long Dim baseCols As Long Dim baseHeader As String Dim thisHeader As String Dim nRows As Long Dim destRow As Long Dim totalRows As Long Dim sheetCount As Long Dim c As Long Dim skipEmpty As String Dim skipHeaderOnly As String Dim skipMismatch As String Dim msg As String Dim errNum As Long Dim errDesc As String On Error GoTo ErrHandler BeginFast Set wb = ThisWorkbook targetName = ReadConfig(KEY_TARGET, DEF_TARGET) excludeList = ReadConfig(KEY_EXCLUDE, DEF_EXCLUDE) sourceTitle = ReadConfig(KEY_SOURCE_TITLE, DEF_SOURCE_TITLE) headerRow = CLng(Val(ReadConfig(KEY_HEADER_ROW, CStr(DEF_HEADER_ROW)))) If headerRow < 1 Then headerRow = DEF_HEADER_ROW ' 취합 시트 준비 (없으면 맨 뒤에 새로 만든다) 후 기존 내용 전부 지우기 Set wsT = GetOrCreateSheet(wb, targetName) If wsT.AutoFilterMode Then wsT.AutoFilterMode = False wsT.Cells.Clear destRow = 2 ' 1행은 제목, 데이터는 2행부터 baseCols = 0 For Each ws In wb.Worksheets If ws.Name <> wsT.Name And Not IsExcluded(ws.Name, excludeList) Then lastRow = LastUsedRow(ws) If lastRow = 0 Then ' 완전히 빈 시트 skipEmpty = AddToList(skipEmpty, ws.Name) ElseIf lastRow <= headerRow Then ' 제목 행만 있고 데이터가 없는 시트 skipHeaderOnly = AddToList(skipHeaderOnly, ws.Name) Else lastCol = ws.Cells(headerRow, ws.Columns.Count).End(xlToLeft).Column thisHeader = HeaderKey(ws, headerRow, lastCol) If baseCols = 0 Then ' 처음 만난 지점 시트의 제목을 취합 시트 1행에 한 번만 쓴다 baseCols = lastCol baseHeader = thisHeader wsT.Cells(1, 1).Value = sourceTitle wsT.Cells(1, 2).Resize(1, baseCols).Value = _ ws.Cells(headerRow, 1).Resize(1, baseCols).Value ' 숫자·날짜 서식은 첫 데이터 행의 서식을 열마다 따른다 For c = 1 To baseCols wsT.Columns(c + 1).NumberFormat = ws.Cells(headerRow + 1, c).NumberFormat Next c End If If thisHeader <> baseHeader Then ' 열 구성이 다르면 섞지 않고 건너뛴다 (잘못 붙는 것보다 안전) skipMismatch = AddToList(skipMismatch, ws.Name) Else nRows = lastRow - headerRow ' 값만 한 번에 옮긴다 (수식·서식이 아니라 값) wsT.Cells(destRow, 2).Resize(nRows, baseCols).Value = _ ws.Cells(headerRow + 1, 1).Resize(nRows, baseCols).Value ' 출처 열에 시트 이름 채우기 wsT.Cells(destRow, 1).Resize(nRows, 1).Value = ws.Name destRow = destRow + nRows totalRows = totalRows + nRows sheetCount = sheetCount + 1 End If End If End If Next ws ' 결과 메시지 If totalRows = 0 Then msg = "취합할 데이터가 없습니다." & vbCrLf & _ "지점 시트에 " & headerRow & "행 제목 아래로 데이터가 있는지 확인해 주세요." Else ' 제목 행 꾸미기 + 열 너비 맞추기 With wsT.Cells(1, 1).Resize(1, baseCols + 1) .Font.Bold = True .Interior.Color = RGB(221, 235, 247) End With wsT.Cells(1, 1).Resize(destRow - 1, baseCols + 1).Columns.AutoFit msg = "취합 완료" & vbCrLf & vbCrLf & _ "- 취합한 시트: " & sheetCount & "개" & vbCrLf & _ "- 취합한 행: " & totalRows & "행 (제목 행 제외)" & vbCrLf & _ "- 결과 시트: [" & wsT.Name & "]" End If If Len(skipEmpty) > 0 Then msg = msg & vbCrLf & "- 건너뜀(빈 시트): " & skipEmpty If Len(skipHeaderOnly) > 0 Then msg = msg & vbCrLf & "- 건너뜀(제목만 있음): " & skipHeaderOnly If Len(skipMismatch) > 0 Then msg = msg & vbCrLf & "- 건너뜀(제목이 첫 지점과 다름): " & skipMismatch EndFast wsT.Activate MsgBox msg, vbInformation, "취합하기" Exit Sub ErrHandler: ' 정리 작업이 Err 를 지우기 전에 먼저 받아 둔다 errNum = Err.Number errDesc = Err.Description EndFast MsgBox "취합 중 오류가 났습니다." & vbCrLf & vbCrLf & _ "오류 번호: " & errNum & vbCrLf & _ "내용: " & errDesc, vbExclamation, "취합하기" End Sub '------------------------------------------------------------------------------ ' 2) 지점별파일로저장 ' - [취합] 시트의 출처 열(1열) 값별로 새 통합문서를 만들어 저장한다 ' - 저장 위치: 이 파일이 있는 폴더 아래 [저장 하위 폴더 이름] 폴더 (없으면 만든다) ' - 같은 이름 파일이 있으면 덮어쓴다 '------------------------------------------------------------------------------ Public Sub 지점별파일로저장() Dim wb As Workbook Dim wsT As Worksheet Dim wbNew As Workbook Dim wsNew As Worksheet Dim targetName As String Dim folderPath As String Dim filePath As String Dim prefix As String Dim sep As String Dim data As Variant Dim header As Variant Dim outArr() As Variant Dim branches As Collection Dim branch As Variant Dim lastRow As Long Dim lastCol As Long Dim r As Long Dim c As Long Dim k As Long Dim nMatch As Long Dim fileCount As Long Dim savedList As String Dim oldAlerts As Boolean Dim errNum As Long Dim errDesc As String On Error GoTo ErrHandler Set wb = ThisWorkbook oldAlerts = Application.DisplayAlerts ' 저장 위치 확인 - 한 번도 저장하지 않은 파일은 폴더가 없다 If Len(wb.Path) = 0 Then MsgBox "이 파일을 먼저 저장해 주세요." & vbCrLf & _ "저장된 폴더 아래에 지점별 파일 폴더를 만듭니다.", vbExclamation, "지점별파일로저장" Exit Sub End If ' OneDrive·SharePoint 동기화 폴더에서 열면 Path 가 웹 주소(https://...)라 폴더를 못 만든다 If LCase$(Left$(wb.Path, 4)) = "http" Then MsgBox "이 파일이 OneDrive/SharePoint 웹 경로로 열려 있어 폴더를 만들 수 없습니다." & vbCrLf & _ "PC의 일반 폴더(예: 바탕화면)에 복사해 열고 다시 실행해 주세요.", vbExclamation, "지점별파일로저장" Exit Sub End If targetName = ReadConfig(KEY_TARGET, DEF_TARGET) If Not SheetExists(wb, targetName) Then MsgBox "[" & targetName & "] 시트가 없습니다. 먼저 '취합하기'를 실행해 주세요.", _ vbExclamation, "지점별파일로저장" Exit Sub End If Set wsT = wb.Worksheets(targetName) lastRow = LastUsedRow(wsT) If lastRow < 2 Then MsgBox "[" & targetName & "] 시트에 데이터가 없습니다. 먼저 '취합하기'를 실행해 주세요.", _ vbExclamation, "지점별파일로저장" Exit Sub End If lastCol = wsT.Cells(1, wsT.Columns.Count).End(xlToLeft).Column ' 저장 폴더 만들기 (Windows / Mac 모두 Application.PathSeparator 사용) sep = Application.PathSeparator folderPath = wb.Path & sep & ReadConfig(KEY_SUBFOLDER, DEF_SUBFOLDER) If Len(Dir(folderPath, vbDirectory)) = 0 Then MkDir folderPath prefix = ReadConfig(KEY_PREFIX, DEF_PREFIX) BeginFast Application.DisplayAlerts = False ' 같은 이름 파일 덮어쓰기 확인창 끄기 ' 데이터를 배열로 한 번에 읽는다 (1행 = 제목) data = wsT.Cells(1, 1).Resize(lastRow, lastCol).Value header = wsT.Cells(1, 1).Resize(1, lastCol).Value ' 출처 열(1열)의 지점 이름을 처음 나온 순서대로 중복 없이 모은다 ' (Mac 에는 Scripting.Dictionary 가 없어 Collection 을 쓴다) Set branches = New Collection For r = 2 To lastRow If Len(Trim$(CStr(data(r, 1)))) > 0 Then If Not InCollection(branches, CStr(data(r, 1))) Then branches.Add CStr(data(r, 1)), CStr(data(r, 1)) End If End If Next r For Each branch In branches ' 이 지점 행 수 세기 nMatch = 0 For r = 2 To lastRow If CStr(data(r, 1)) = branch Then nMatch = nMatch + 1 Next r ' 이 지점 행만 담은 배열 만들기 ReDim outArr(1 To nMatch, 1 To lastCol) k = 0 For r = 2 To lastRow If CStr(data(r, 1)) = branch Then k = k + 1 For c = 1 To lastCol outArr(k, c) = data(r, c) Next c End If Next r ' 새 통합문서(시트 1장)에 제목 + 데이터 쓰기 Set wbNew = Workbooks.Add(xlWBATWorksheet) Set wsNew = wbNew.Worksheets(1) wsNew.Name = Left$(SafeName(CStr(branch)), 31) wsNew.Cells(1, 1).Resize(1, lastCol).Value = header wsNew.Cells(2, 1).Resize(nMatch, lastCol).Value = outArr For c = 1 To lastCol wsNew.Columns(c).NumberFormat = wsT.Cells(2, c).NumberFormat Next c With wsNew.Cells(1, 1).Resize(1, lastCol) .Font.Bold = True .Interior.Color = RGB(221, 235, 247) End With wsNew.Cells(1, 1).Resize(nMatch + 1, lastCol).Columns.AutoFit filePath = folderPath & sep & prefix & SafeName(CStr(branch)) & ".xlsx" wbNew.SaveAs Filename:=filePath, FileFormat:=FILE_FORMAT_XLSX wbNew.Close SaveChanges:=False Set wbNew = Nothing fileCount = fileCount + 1 savedList = savedList & vbCrLf & "- " & prefix & SafeName(CStr(branch)) & ".xlsx (" & nMatch & "행)" Next branch Application.DisplayAlerts = oldAlerts EndFast wb.Activate MsgBox "지점별 파일 " & fileCount & "개를 저장했습니다." & vbCrLf & _ "폴더: " & folderPath & vbCrLf & savedList, vbInformation, "지점별파일로저장" Exit Sub ErrHandler: ' 정리 작업이 Err 를 지우기 전에 먼저 받아 둔다 errNum = Err.Number errDesc = Err.Description ' 만들다 만 새 통합문서가 있으면 저장하지 않고 닫는다 CloseQuietly wbNew Set wbNew = Nothing Application.DisplayAlerts = oldAlerts EndFast MsgBox "지점별 저장 중 오류가 났습니다." & vbCrLf & vbCrLf & _ "오류 번호: " & errNum & vbCrLf & _ "내용: " & errDesc, vbExclamation, "지점별파일로저장" End Sub '============================================================================== ' 아래는 내부 도우미 (Alt+F8 목록에 안 보이도록 Private) '============================================================================== ' 화면 갱신·자동 계산·이벤트를 잠시 끈다 (속도 + 화면 깜빡임 방지) Private Sub BeginFast() If mFastOn Then Exit Sub mOldCalc = Application.Calculation mOldEvents = Application.EnableEvents Application.ScreenUpdating = False Application.EnableEvents = False Application.Calculation = xlCalculationManual mFastOn = True End Sub ' 끈 것을 되돌린다 (오류가 나도 반드시 호출. BeginFast 전이면 아무것도 안 함) Private Sub EndFast() On Error Resume Next If Not mFastOn Then Exit Sub Application.Calculation = mOldCalc Application.EnableEvents = mOldEvents Application.ScreenUpdating = True mFastOn = False End Sub ' 통합문서를 저장하지 않고 닫는다 (이미 닫혔거나 Nothing 이어도 오류 없음) Private Sub CloseQuietly(ByVal target As Workbook) On Error Resume Next If Not target Is Nothing Then target.Close SaveChanges:=False End Sub ' [설정] 시트 A열에서 항목 이름을 찾아 B열 값을 돌려준다. 없거나 비면 기본값 Private Function ReadConfig(ByVal key As String, ByVal defaultValue As String) As String Dim ws As Worksheet Dim lastRow As Long Dim r As Long Dim v As String ReadConfig = defaultValue If Not SheetExists(ThisWorkbook, CONFIG_SHEET) Then Exit Function Set ws = ThisWorkbook.Worksheets(CONFIG_SHEET) lastRow = ws.Cells(ws.Rows.Count, 1).End(xlUp).Row For r = 1 To lastRow If Trim$(CStr(ws.Cells(r, 1).Value)) = key Then v = Trim$(CStr(ws.Cells(r, 2).Value)) If Len(v) > 0 Then ReadConfig = v Exit Function End If Next r End Function ' 시트가 있는지 확인 Private Function SheetExists(ByVal wb As Workbook, ByVal sheetName As String) As Boolean Dim ws As Worksheet For Each ws In wb.Worksheets If ws.Name = sheetName Then SheetExists = True Exit Function End If Next ws End Function ' 시트를 가져오고, 없으면 맨 뒤에 만든다 Private Function GetOrCreateSheet(ByVal wb As Workbook, ByVal sheetName As String) As Worksheet If SheetExists(wb, sheetName) Then Set GetOrCreateSheet = wb.Worksheets(sheetName) Else Set GetOrCreateSheet = wb.Worksheets.Add(After:=wb.Worksheets(wb.Worksheets.Count)) GetOrCreateSheet.Name = sheetName End If End Function ' 제외 목록(쉼표 구분)에 있는 시트인지 확인 (앞뒤 공백 무시) Private Function IsExcluded(ByVal sheetName As String, ByVal excludeList As String) As Boolean Dim parts() As String Dim i As Long If Len(Trim$(excludeList)) = 0 Then Exit Function parts = Split(excludeList, ",") For i = LBound(parts) To UBound(parts) If Trim$(parts(i)) = sheetName Then IsExcluded = True Exit Function End If Next i End Function ' 시트에서 값이 들어 있는 마지막 행 (빈 시트면 0) ' 서식만 남은 빈 셀에 속지 않도록 UsedRange 대신 값을 뒤에서부터 찾는다 Private Function LastUsedRow(ByVal ws As Worksheet) As Long Dim found As Range Set found = ws.Cells.Find(What:="*", After:=ws.Cells(1, 1), LookIn:=xlFormulas, _ LookAt:=xlPart, SearchOrder:=xlByRows, SearchDirection:=xlPrevious) If found Is Nothing Then LastUsedRow = 0 Else LastUsedRow = found.Row End If End Function ' 제목 행을 비교용 문자열로 만든다 (앞뒤 공백 무시) Private Function HeaderKey(ByVal ws As Worksheet, ByVal headerRow As Long, ByVal lastCol As Long) As String Dim c As Long Dim s As String For c = 1 To lastCol s = s & Trim$(CStr(ws.Cells(headerRow, c).Value)) & "|" Next c HeaderKey = s End Function ' 목록 문자열에 항목 덧붙이기 ("A, B, C") Private Function AddToList(ByVal listText As String, ByVal item As String) As String If Len(listText) = 0 Then AddToList = item Else AddToList = listText & ", " & item End If End Function ' Collection 에 키가 있는지 확인 Private Function InCollection(ByVal col As Collection, ByVal key As String) As Boolean Dim tmp As Variant On Error Resume Next tmp = col.Item(key) InCollection = (Err.Number = 0) Err.Clear On Error GoTo 0 End Function ' 파일·시트 이름에 쓸 수 없는 글자를 _ 로 바꾼다 Private Function SafeName(ByVal s As String) As String Dim bad As Variant Dim i As Long bad = Array("\", "/", ":", "*", "?", """", "<", ">", "|", "[", "]") For i = LBound(bad) To UBound(bad) s = Replace(s, bad(i), "_") Next i SafeName = Trim$(s) End Function