엑셀/엑셀 매크로 사무자동화 이해

입출고 Undo, Redo 구현 - 코드분석

내일도화이팅 2023. 7. 30. 22:40

안녕하세요. 내일도 화이팅입니다.

 

어제 업로드한 Undo와 Redo 코드 글은 다들 보셨나요?

 

오늘은 그 코드들을 한줄한줄 분석해보려합니다.

 

총 8개의 모듈이 추가되었고, 4개의 모듈이 변경되었으며, 시트는 총 4개가 추가되었습니다.

 

1. 

Call save_past("입고", "delete", delete_number)
Worksheets("Redo 입고데이터").Range("A:Y").Delete xlToLeft

 

사실 변경된 모듈들은 그렇게 많이 변경되진 않았습니다. 간단한 코드 몇줄이 추가되었죠.

 

나중에 소개드리겠지만, save_past는 제가 만든 과거를 기억하는 프로시저입니다.

 

데이터에 변화를 주는 행동은 크게 입고 견적서 추가, 입고 삭제, 출고 견적서 추가, 출고 삭제, Undo 실행, Redo 실행 으로 6가지 입니다. 이 중 Undo를 실행하여 명령을 되돌릴 수 있는 명령은 입고 견적서 추가, 입고 삭제, 출고 견적서 추가, 출고 삭제, Redo 실행입니다.(Redo 실행은 나중에 설명드리도록 하겠습니다.)

 

save_past는 위 명령들 중 어디서 어떤 명령을 했는지 인수로 정보를 받는 함수입니다.

 

그래서  입고 시 "입고", 출고 시 "출고", 추가 시 "input", 삭제 시 "delete"를 넘기도록 만들었죠.

 

또한 삭제 혹은 추가를 실행할 견적서의 번호도 인수로 넘겼습니다.

 

삭제는 입출고 삭제에서 사용자가 inputBox에 직접 입력한 값을 담은  delete_number,

 

추가는 견적서의 번호를 의미하는 Worksheets( kind & "견적서").Cells(3, "G").Value를 인수로 넣었죠.

※ kind는 견적서의 종류 "입고" or "출고"

 

아래 코드는 뭘까요?

Worksheets("Redo 입고데이터").Range("A:Y").Delete xlToLeft 는 Redo를 전체 삭제하는 코드를 의미합니다.

 

A:E, F:J, K:O, P:T, U:Y 총 5가지의 Redo 저장본을 삭제하죠.

 

[그림 1] Redo를 삭제하는 이유 - 4번을 추가 후 Undo 실행

Redo를 삭제하는 이유를 그림과 함께 예를 들어 설명드려보겠습니다.

 

사용자가 4번 견적서를 생성한 후 Undo(뒤로가기)를 실행했습니다. 그러면 사용자의 앞선 행동은 취소가 되고(Undo 데이터의 데이터가 기존 데이터로 옮겨지고) Redo(앞으로가기) 데이터에 기존 데이터가 옮겨집니다.

 

그 후 사용자가 1번 견적서를 추가하여 데이터는 [그림 1]에서 아래의 [그림 2]로 넘어가게 되겠죠.

[그림 2] Redo 데이터를 삭제하는 이유 - 1번 견적서를 기존 데이터에 추가

그런데, 아뿔싸 과장님께서 4번 견적서도 추가해야된다고 합니다..

 

4번 견적서는 Undo를 통해 추가를 취소했고.. Undo로 정정된 데이터는 Redo에 있다고 하니까 Redo를 실행했는데요..

그랬더니...

[그림 3] Redo 데이터를 삭제하는 이유 - 날아가버린 1번 견적서

기존 데이터에서 1번 견적서가 사라져버렸습니다.

 

때문에, 개발자들은 선택을 해야했죠..

 

아.. 4번을 추가 못하게 막는게 현명한 판단일까? 1번을 날리는게 현명한 판단일까?

아니면 Redo를 누르면 Undo 후 명령(1번을 생성한다는 명령) + Undo 전 명령(4번을 생성한다는 명령) 모두 받아들여

1, 2, 3, 4번 견적서가 모두 나오게 할까..

 

그리고 대부분의 프로그램은 4번을 추가 못하게 즉, Redo를 삭제하는 방법을 택했죠..

 

사용자가 Undo 후에 어떤 행동을 했는지 기억해서 다시 추가하는 것보다 Undo를 왜 했는지 기억하도록 하는게 합리적이라고 판단하였다는 뜻입니다.

 

Undo 후에 한 여러가지 활동은 기억 못하더라도 왜 내가 Undo를 했는지는 기억을 할 수 있을 확률이 높으니까요..

[그림 4] Redo를 삭제하는 이유 - 역시 파란색이 나은가?

고양이를 파란색으로 칠했다가 생각해보니 앞서 색칠한 주황색이 더 어울릴 것 같아 Undo(되돌리기)를 실행하였습니다. 그리고 수염과 눈썹을 그리니.. 아... 어울리지가 않네요.. 역시 파란색 고양이가 나았나봅니다. Redo에 파란색 고양이가 있을테니 Redo를 하면 파란색 고양이가 나오겠죠? Redo를 실행했습니다.

[그림 5] Redo를 삭제하는 이유 - 어.. 내가 그 다음 뭘 그렸더라?

뭔가 그렸던게 많이 사라졌네요.. 근데 제가 주황색 고양이를 그린다음 뭘 추가했었죠...?

 

즉, 사용자가 파란색 고양이에서 Undo를 했었다는 것은 기억을 해도 Undo 후에 Redo까지 행동은 너무 많기 때문에 기억하기가 힘듭니다.

 

그리고 아까전에 언급한 Undo 후 변경 사항과 Redo에 있는 데이터가 모두 나오게 하면 안되냐는 경우에 대해 언급했었죠?

 

하지만, 아쉽게도 그 역시 조금 힘듭니다. 데이터가 중첩되는 문제가 발생할 수 도 있고.. 사용자의 목적이 다를 수 도 있죠..

 

사용자는 [그림 3]처럼 1번 견적서 대신 4번 견적서를 넣고 싶어 Redo를 한 것인데, 1, 2, 3, 4번이 한꺼번에 나오게 되면 1번 견적서를 삭제하거나 삭제 후 Redo를 하거나 해야되니까요.. 즉 수동적 작업은 필연적입니다..

 

때문에 헷깔리지 않게 그냥 Undo 후 추가 행동을 하면 Redo를 삭제하자가 합리적인 방법이라는 거죠.. 물론 프로그램에 따라 Redo를 모두 기억해서 Undo를 한 후 추가 행동을 하고 그 추가 행동을 다시 그 전에 Undo한 시점까지 Undo하고 Redo를 하면 어떤 사항으로 Redo를 할껀지 고르게 하는 프로그램도 존재합니다. 하지만, 저장을 많이할 뿐 본질적으로는 똑같죠.

 

2. save_past(과거를 저장)

Sub save_past(kind As String, reverse_order As String, number As String)
    Worksheets("Undo " & kind & "데이터").Range("A:E").Delete xlToLeft
    Worksheets(kind).Range("B:F").Copy Worksheets("Undo " & kind & "데이터").Range("U:Y")
    Worksheets("Undo " & kind & "데이터").Cells(1, "U").Value = number
    Worksheets("Undo " & kind & "데이터").Cells(1, "V").Value = reverse_order
End Sub

 

Undo를 실행한다는 것은 기존의 데이터를 실행하기 전 데이터로 대체한다는 의미와 같습니다.

 

사용자가 언제 Undo를 하기를 원하는지 모르니 사용자가 어떠한 명령(추가, 삭제, Redo)을 실행할 때마다 Undo에 데이터를 저장해야 합니다.

 

그리고 위의 코드들은 모드 그 과정을 담은 코드입니다.

 

Worksheets("Undo " & kind & "데이터").Range("A:E").Delete xlToLeft는 직역하면

"Undo " & kind & "데이터" 라는 이름의 시트에서 A:E를 삭제하고 왼쪽으로 밀어라 라는 뜻입니다.

 

kind는 1번에서 말씀드린 것처럼 입고 아니면 출고인데, 그렇단말은 "Undo 입고데이터"라는 시트와 "Undo 출고데이터"라는 시트가 따로 있다는 뜻입니다.

 

따로둔 이유는 사용자의 편의를 위해서 입니다. 출고를 다입력하니 재고가 이상해서 봤더니 입고에 데이터를 잘못 넣었습니다. 그 때 Undo를 했는데 고쳐지라는 입고의 데이터는 안고쳐지고 잘 입력한 출고 데이터가 Undo되면 매우 곤란하겠죠..

 

그리고 삭제를 하는 이유는 데이터 용량의 한계때문입니다. 저는 Undo도 그렇고 Redo도 그렇고 A:E, F:J, K:O, P:T, U:Y로 총 5개의 데이터가 저장되게 프로그래밍했는데요. 필요에 따라 늘이거나 줄일 수 도 있습니다.

 

A:E를 삭제하고 왼쪽으로 밀면

A:E -> 삭제

F:J -> A:E

K:O -> F:J

P:T -> K:O

U:Y -> P:T

Z:AD -> U:Y

로 데이터가 바뀌게 되는데요.

Z:AD는 아무것도 없는 데이터니, U:Y는 비어있는 셀들의 범위가 될 것이고 그러면 사용자의 행동을 저장할 공간이 생기게되죠.

 

Worksheets(kind).Range("B:F").Copy Worksheets("Undo " & kind & "데이터").Range("U:Y")

그리고 위의 코드는 기존 데이터의 B:F를 Undo에 저장하겠다는 코드입니다.

 

Worksheets("Undo " & kind & "데이터").Cells(1, "U").Value = number

Worksheets("Undo " & kind & "데이터").Cells(1, "V").Value = reverse_order

 

위 두 코드는 셀 위에 다른 특별한 문자열을 저장하려는 목적이있는데요.

 

맞습니다. 1행 U열에는 save_past의 인수로 받은 주문번호, 1행 V열에는 사용자가 내린 명령을 저장하게 되죠.

 

왜 변수이름이 reverse_order인지는 Undo 코드 설명해드리면서 설명해드릴게요.

 

3. Undo 구현

Sub undo_입고()
    If (IsEmpty(Worksheets("Undo 입고데이터").Cells(2, "U").Value)) Then
        msg = MsgBox("Undo하실 데이터가 없습니다.", vbYesOnly, "Undo 실패")
    Else
        Call save_future("입고")
        Call undo("입고")
    End If
End Sub

 

Undo 입고(출고)데이터 셀이 비어있다는 것은 Undo할 것이 없다는 것이므로 Undo 명령을 수행하면 안됩니다. 때문에 사용자에게 Undo를 할 수 없다는 경고 메세지를 출력하고 실행을 중지합니다.

 

Sub undo(kind As String)
    Dim in_product As Boolean
    
    in_product = False
    If (kind Like "입고") Then
        in_product = True
    End If
    
    '재고 갱신
    If (Worksheets("Undo " & kind & "데이터").Cells(1, "V") Like "input") Then
        Dim i As Integer
        Dim j As Integer
        
        i = 3
        j = 3
        Do While (Not IsEmpty(Cells(i, 2).Value))
            If (Cells(i, 2).Value Like Worksheets("Undo " & kind & "데이터").Cells(1, "U").Value) Then
                j = 3
                Do While (Not IsEmpty(Worksheets("재고관리").Cells(j, 2)))
                    If (Cells(i, 4).Value Like Worksheets("재고관리").Cells(j, 2).Value) Then
                        If (in_product) Then
                            Worksheets("재고관리").Cells(j, 3).Value = Worksheets("재고관리").Cells(j, 3).Value - Worksheets("입고").Cells(i, 6).Value
                        Else
                            Worksheets("재고관리").Cells(j, 3).Value = Worksheets("재고관리").Cells(j, 3).Value + Worksheets("출고").Cells(i, 6).Value
                        End If
                        Exit Do
                    End If
                    j = j + 1
                Loop
            End If
            i = i + 1
        Loop
    Else
        Dim count_inven As Integer
        count_inven = 3
            
        Do While (Not IsEmpty(Worksheets("재고관리").Cells(count_inven, 2).Value))
            count_inven = count_inven + 1
        Loop

        If (in_product) Then
            For i = 3 To 100
                For j = 3 To count_inven - 1
                    If (Worksheets("Undo 입고데이터").Cells(1, "U").Value Like Worksheets("Undo 입고데이터").Cells(i, "U").Value _
                    And Worksheets("재고관리").Cells(j, 2).Value Like Worksheets("Undo 입고데이터").Cells(i, "W").Value) Then
                        Worksheets("재고관리").Cells(j, 3).Value = Worksheets("재고관리").Cells(j, 3).Value + Worksheets("Undo 입고데이터").Cells(i, "Y").Value
                        Exit For
                    End If
                Next j
            Next i
        Else
            For i = 3 To 100
                For j = 3 To count_inven - 1
                    If (Worksheets("Undo 출고데이터").Cells(1, "U").Value Like Worksheets("Undo 출고데이터").Cells(i, "U").Value _
                    And Worksheets("재고관리").Cells(j, 2).Value Like Worksheets("Undo 출고데이터").Cells(i, "W").Value) Then
                        Worksheets("재고관리").Cells(j, 3).Value = Worksheets("재고관리").Cells(j, 3).Value - Worksheets("Undo 출고데이터").Cells(i, "Y").Value
                        Exit For
                    End If
                Next j
            Next i
        End If
    End If
    '데이터 갱신
    Worksheets("Undo " & kind & "데이터").Range("U:Y").Cut Worksheets(kind).Range("B:F")
    Range("B1:F1").Value = ""
    Worksheets("Undo " & kind & "데이터").Range("A:E").Insert
End Sub

 

드디어 Undo가 실행이되었는데요. 코드가 길죠?

 

코드가 긴 이유는 재고관리 때문인데요. 사실 시각적으로 보이는 데이터야 앞전 데이터를 저장했다가 덮어쓰기 하면 된다고 쳐도 재고량을 덮어쓰기 하게되면 출고와 입고를 번갈아가며 다룰 때 재고는 혼선을 빚게됩니다.

 

입고를 통해 재고를 추가한 뒤 출고를 통해 재고를 줄였는데 입고에서 Undo를 하면 입고의 추가전 재고가 덮어쓰기 될 것이고 그러면 출고에서 추가한 뒤의 재고는 날라가게 됩니다.

 

※ 물론 출고든 입고든 어떠한 행동을 실행할때 메모리를 더 써서 재고용 Undo와 Redo를 만드는 방법도 있고 그냥 견적서의 재고를 계속 연산(입고는 더하고 출고는 빼고)하여 재고를 최신화시키는 방법 또한 존재합니다.

 

그래서 고민하던찰나 세가지 규칙을 찾아냅니다.

1) Undo는 삽입 또는 삭제 연산의 반대 -> 삽입(삭제)을 한 후 Undo를 했다면 재고에 삭제(삽입) 연산을 실행하면됨.

2) Undo는 Redo의 반대 -> Redo에 저장되어있는 명령을 반대로 수행하면 됨.

3) Redo는 삽입 또는 삭제 연산과 동일 -> Redo에 저장되어 있는 명령을 그대로 수행하면됨.

 

때문에 위 코드는 길지만 사실 별로 어려운 내용이아닙니다. 1) ~ 3)번의 내용과 "Undo의 데이터를 복사하여 Redo 시트와 기존 데이터 시트에 붙여넣기 하라"라는 내용입니다.

 

3. Sub save_future(kind As String)
    Worksheets("Redo " & kind & "데이터").Range("A:E").Delete xlToLeft
    Worksheets(kind).Range("B:F").Copy Worksheets("Redo " & kind & "데이터").Range("U:Y")
    Worksheets("Redo " & kind & "데이터").Range("U1:V1").Value = Worksheets("Undo " & kind & "데이터").Range("U1:V1").Value
End Sub


만약 셀이 비어있지않다면, 미래를 저장하는 save_future을 통해 기존 데이터를 저장합니다. 사용자가 Undo 후 마음이 바껴 Redo할 수 있으니 기존 데이터가 Undo에 의해 덮어쓰기되어 날라가지않도록 하기 위함입니다.

 

위 코드는 그 내용을 담고 있습니다. "오래된 데이터인 A:E를 삭제하고 Undo에 지워질 셀의 내용을 Redo 데이터에 저장하라." 라는 뜻입니다.

 

4. Redo 구현

Sub redo_입고()
    If (IsEmpty(Worksheets("Redo 입고데이터").Cells(2, "U").Value)) Then
        msg = MsgBox("Redo하실 데이터가 없습니다.", vbYesOnly, "Redo 실패")
    Else
        Call save_past("입고", Worksheets("Redo 입고데이터").Cells(1, "V"), Worksheets("Redo 입고데이터").Cells(1, "U"))
        Call redo("입고")
    End If
End Sub
Undo와 마찬가지로 Redo할 것이 있는지를 검토하고 없다면 경고메세지를 보냅니다.

 

그리고 Redo 또한 실행 후 Undo를 실행할 수 있으므로 Undo를 할 수 있도록 save_past를 호출하여 과거를 저장합니다.

 

Sub redo(kind As String)
    Dim in_product As Boolean
    
    in_product = False
    If (kind Like "입고") Then
        in_product = True
    End If
    
    '재고 갱신
    If (Worksheets("Redo " & kind & "데이터").Cells(1, "V") Like "delete") Then
        Dim i As Integer
        Dim j As Integer
        
        i = 3
        Do While (Not IsEmpty(Cells(i, 2).Value))
            If (Cells(i, 2).Value Like Worksheets("Redo " & kind & "데이터").Cells(1, "U").Value) Then
                j = 3
                Do While (Not IsEmpty(Worksheets("재고관리").Cells(j, 2)))
                    If (Cells(i, 4).Value Like Worksheets("재고관리").Cells(j, 2).Value) Then
                        If (in_product) Then
                            Worksheets("재고관리").Cells(j, 3).Value = Worksheets("재고관리").Cells(j, 3).Value - Worksheets("입고").Cells(i, 6).Value
                        Else
                            Worksheets("재고관리").Cells(j, 3).Value = Worksheets("재고관리").Cells(j, 3).Value + Worksheets("출고").Cells(i, 6).Value
                        End If
                        Exit Do
                    End If
                    j = j + 1
                Loop
            End If
            i = i + 1
        Loop
    Else
        Dim count_inven As Integer
        count_inven = 3
            
        Do While (Not IsEmpty(Worksheets("재고관리").Cells(count_inven, 2).Value))
            count_inven = count_inven + 1
        Loop
            
        If (in_product) Then
            For i = 3 To 100
                For j = 3 To count_inven - 1
                    If (Worksheets("Redo 입고데이터").Cells(1, "U").Value Like Worksheets("Redo 입고데이터").Cells(i, "U").Value _
                    And Worksheets("재고관리").Cells(j, 2).Value Like Worksheets("Redo 입고데이터").Cells(i, "W").Value) Then
                        Worksheets("재고관리").Cells(j, 3).Value = Worksheets("재고관리").Cells(j, 3).Value + Worksheets("Redo 입고데이터").Cells(i, "Y").Value
                        Exit For
                    End If
                Next j
            Next i
        Else
            For i = 3 To 100
                For j = 3 To count_inven - 1
                    If (Worksheets("Redo 출고데이터").Cells(1, "U").Value Like Worksheets("Redo 출고데이터").Cells(i, "U").Value _
                    And Worksheets("재고관리").Cells(j, 2).Value Like Worksheets("Redo 출고데이터").Cells(i, "W").Value) Then
                        Worksheets("재고관리").Cells(j, 3).Value = Worksheets("재고관리").Cells(j, 3).Value - Worksheets("Redo 출고데이터").Cells(i, "Y").Value
                        Exit For
                    End If
                Next j
            Next i
        End If
    End If
    '데이터 갱신
    Worksheets("Redo " & kind & "데이터").Range("U:Y").Cut Worksheets(kind).Range("B:F")
    Range("B1:F1").Value = ""
    Worksheets("Redo " & kind & "데이터").Range("A:E").Insert
End Sub

Redo 또한 Undo와 마찬가지입니다. 하지만, 기존의 명령들과 반대로 행동해야했던 Undo와 다르게 Redo는 앞서 있었던 명령 그대로 실행합니다.

 

즉, 저장된 명령이 삭제라면 삭제를 수행한 후 재고를 줄이면되고, 추가라면 추가를 수행한 후 재고를 늘이면 됩니다.

 

긴 코드였는데 따라와주셔서 감사합니다. 아직 전달력이 부족한 면이 있어 고치려고 노력중입니다.

 

열심히 공부해서 실력도 높이고 있으니 다음 포스팅은 오늘 포스팅 보다 더 나은 모습 약속드리겠습니다.

 

다음 포스팅은 제가 지금까지 짜왔던 코드들에서 불필요한 알고리즘을 줄이고 가독성을 높이며, 코드들을 캡슐화해서 유지 보수하기 좋은 코드로 변경해보도록 하겠습니다.

 

도움이 되셨다면 좋아요, 댓글 부탁드립니다.

 

모든 피드백과 질문 환영합니다.

 

오늘도 감사합니다.

 

입출고 Undo, Redo 구현 - 코드제공, 파일제공(복붙용)

https://ksm30546.tistory.com/19

 

입출고 Undo, Redo 구현 - 코드제공, 파일제공(복붙용)

※ 코드와 파일은 상업적 이용, 개인적 이용이 모두 가능합니다. 다만, 블로그나 게시판 업로드 목적이라면 출처를 꼭 남겨주세요. Undo와 Redo 시연영상 1 Undo와 Redo 시연영상 2 안녕하세요. 오랜만

ksm30546.tistory.com