Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

(시리즈 글이 6개 있습니다.)
VS.NET IDE: 77. Visual Studio 확장(VSIX) 만드는 방법
; https://www.sysnet.pe.kr/2/0/1515

VS.NET IDE: 78. Visual Studio 확장으로 XmlCodeGenerator 제작하는 방법
; https://www.sysnet.pe.kr/2/0/1518

VS.NET IDE: 115. Visual Studio 확장(VSIX)을 이용해 사용자 메뉴 추가하는 방법
; https://www.sysnet.pe.kr/2/0/11184

VS.NET IDE: 116. Visual Studio 확장(VSIX)을 이용해 사용자 메뉴 추가하는 방법 (2) - 동적 메뉴 구성
; https://www.sysnet.pe.kr/2/0/11185

VS.NET IDE: 117. Visual Studio 확장(VSIX)을 이용해 사용자 매크로를 추가하는 방법
; https://www.sysnet.pe.kr/2/0/11186

VS.NET IDE: 165. Visual Studio 2022를 위한 Extension 마이그레이션
; https://www.sysnet.pe.kr/2/0/12682




Visual Studio 확장(VSIX)을 이용해 사용자 메뉴 추가하는 방법 (2) - 동적 메뉴 구성

지난 글에서 메뉴 추가 방법에 대한 기본적인 내용을 살펴봤습니다.

Visual Studio 확장(VSIX)을 이용해 사용자 메뉴 추가하는 방법
; https://www.sysnet.pe.kr/2/0/11184

그런데, 때로는 메뉴를 동적으로 구성해야 할 때가 있습니다. Visual Studio의 경우 대표적인 사례로 "File" / "Recent Files" 하위 메뉴를 들 수 있는데, 이를 위한 방법 역시 문서화가 너무 잘되어 있습니다. ^^

Dynamically Adding Menu Items
; https://docs.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-2015/extensibility/dynamically-adding-menu-items

위의 글에 따라, 동적으로 추가되는 "매크로 명령어 목록"을 구성해 보겠습니다.

우선, 동적 메뉴로 펼쳐질 서브 메뉴의 부모가 되어줄 Menu가 있어야 하는데, 그 메뉴는 또한 Command 그룹에 속해 있어야 합니다. 기본적으로 .vsct 파일에는 ("Extensibility" / "Custom Command"를 추가한 경우) 한 개의 그룹이 등록되어 있으므로,

<Groups>
    <Group guid="guidMacroCommandPackageCmdSet" id="MyMenuGroup" priority="0x0600">
        <Parent guid="guidSHLMainMenu" id="IDM_VS_MENU_TOOLS"/>
    </Group>
</Groups>

위의 "MyMenuGroup"과 연결될 메뉴만 하나 추가해 주면 됩니다.

<Menus>
    <Menu guid="guidMacroCommandPackageCmdSet" id="MyMenuController" priority="0x1000" type="MenuController">
	<Parent guid="guidMacroCommandPackageCmdSet" id="MyMenuGroup" />
	<CommandFlag>DynamicVisibility</CommandFlag>
	<Strings>
	    <ButtonText>MyMacro List</ButtonText>
	</Strings>
    </Menu>
</Menus>

위의 상태까지만 보면, "Tools" 메뉴에 "MyMacro List"라는 이름의 메뉴가 생성되도록 정의되는데 그 메뉴의 타입이 "MenuController" 유형입니다. (이 유형이어야만 자유롭게 자신의 서브 메뉴를 정의할 수 있는 것 같습니다.)

그다음, 서브 메뉴를 담을 그룹을 하나 정의합니다.

<Groups>
    <Group guid="guidMacroCommandPackageCmdSet" id="MyMenuGroup" priority="0x0600">
        <Parent guid="guidSHLMainMenu" id="IDM_VS_MENU_TOOLS"/>
    </Group>

    <Group guid="guidMacroCommandPackageCmdSet" id="MySubMenuGroup" priority="0x0600">
        <Parent guid="guidMacroCommandPackageCmdSet" id="MyMenuController" />
    </Group>
</Groups>

당연히 이 서브 메뉴 그룹의 부모는 "Tools" / "MyMacro List" 메뉴가 되어야 하므로 "<Parent />"를 통해 메뉴의 id인 "MyMenuController"를 부모로 지정해 준 것입니다. 그리고 이 서브 메뉴(예제에서는 매크로 명령어들)가 나열되기 위한 기본 버튼(이라 쓰고 메뉴라고 읽음) 하나를 지정해 줍니다.

<Buttons>
    <Button guid="guidMacroCommandPackageCmdSet" id="cmdidMyDynamicStartCommand" priority="0x0100">
    <Parent guid="guidMacroCommandPackageCmdSet" id="MySubMenuGroup" />
    <CommandFlag>DynamicItemStart</CommandFlag>
    <CommandFlag>DynamicVisibility</CommandFlag>
    <CommandFlag>TextChanges</CommandFlag>
    <Strings>
        <ButtonText>MacroSubList</ButtonText>
    </Strings>
    </Button>
</Buttons>

마지막으로, 지금까지 설정했던 노드마다 사용되었던 ID에 대한 심벌 등록을 해줍니다.

<GuidSymbol name="guidMacroCommandPackageCmdSet" value="{6d15ea04-448b-4c65-b70c-cbfaa3c105b7}">
    <IDSymbol name="MyMenuGroup" value="0x1000" />
    <IDSymbol name="MySubMenuGroup" value="0x1500" />
    <IDSymbol name="MyMenuController" value ="0x2000"/>
    <IDSymbol name="cmdidMyDynamicStartCommand" value="0x3002" />
</GuidSymbol>

일단, 여기까지의 .vsct 파일 변경과 함께 지난번의 소스 코드에서 MacroCommand의 생성자를 다음과 같이 아무 일도 안 하는 코드로 변경해서 실행하면,

private MacroCommand(Package package)
{
    if (package == null)
    {
        throw new ArgumentNullException("package");
    }
            
    this.package = package;
}

"Tools" 메뉴에 "MyMacro List" 메뉴가 포함되고, 그 하위로 "MacroSubList" 메뉴가 뜹니다.

dynamic_menu_in_vsix_1.png




우리가 원하는 것은, 바로 "MacroSubList"가 나와야 되는 단계의 메뉴를 동적으로 구성하는 것입니다. 이를 위해 MacroCommand의 생성자를 다음과 같이 변경해 줍니다.

public const int CommandId = 0x3002;  // .vsct에서 지정한 cmdidMyDynamicStartCommand 심벌 값
private DTE2 dte2;

private MacroCommand(Package package)
{
    if (package == null)
    {
        throw new ArgumentNullException("package");
    }

    _repo = new MacroRepository(); // 이 클래스 코드는 나중에 설명을 합니다.
            
    this.package = package;

    OleMenuCommandService commandService = this.ServiceProvider.GetService(typeof(IMenuCommandService)) as OleMenuCommandService;
    if (commandService != null)
    {
        CommandID dynamicItemRootId = new CommandID(CommandSet, (int)CommandId);
        DynamicItemMenuCommand dynamicMenuCommand = new DynamicItemMenuCommand(dynamicItemRootId,
            _repo,
            OnInvokedDynamicItem,
            OnBeforeQueryStatusDynamicItem);

        commandService.AddCommand(dynamicMenuCommand);
    }

    dte2 = (DTE2)this.ServiceProvider.GetService(typeof(DTE));
}

가장 다른 점이 있다면 기존의 코드는 MenuCommand 인스턴스를 생성해 commandService.AddCommand에 건네주었던 반면, 이번에는 별도로 정의한 DynamicItemMenuCommand 타입을 생성해서 전달하고 있습니다. 이 타입은 OleMenuCommand를 상속받은 것으로 다음과 같이 코드를 추가하면 됩니다.

using System;
using System.ComponentModel.Design;
using System.Globalization;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using EnvDTE80;

namespace VSMacro
{
    public class DynamicItemMenuCommand : OleMenuCommand
    {
        MacroRepository _repo;

        public DynamicItemMenuCommand(CommandID rootId, MacroRepository repo,
            EventHandler invokeHandler, EventHandler beforeQueryStatusHandler)
            : base(invokeHandler, null /*changeHandler*/, beforeQueryStatusHandler, rootId)
        {
            _repo = repo;
        }

        public override bool DynamicItemMatch(int cmdId)
        {
            if (_repo.ContainsCmd(cmdId))
            {
                this.MatchedCommandId = cmdId;
                return true;
            }

            return false;
        }
    }
}

DynamicItemMenuCommand가 하는 것은 오로지 Visual Studio가 조회하는 Command ID가 올바른지에 대해서만 DynamicItemMatch 가상 함수를 통해 판단해 주는 것입니다.

여기서부터, 구현이 좀 복잡하고 이해가 다소 직관적이지 않은 부분이 나옵니다. 일단, Visual Studio는 (OleMenuCommand를 상속받은) 메뉴가 있는 경우, 그 메뉴의 생성자에 전달된 마지막 인자(OnBeforeQueryStatusDynamicItem)였던 delegate를 호출합니다. 즉, 다음의 코드가 실행됩니다.

private void OnBeforeQueryStatusDynamicItem(object sender, EventArgs args)
{
    DynamicItemMenuCommand matchedCommand = (DynamicItemMenuCommand)sender;

    matchedCommand.Enabled = true;
    matchedCommand.Visible = true;

    // matchedCommand.MatchedCommandId
}

그런데, 이때 실행되는 matchedCommand의 MatchedCommandId가 수상합니다. 예상으로는 생성자 호출 시에 넘겨줬던 CommandID를 따르는 CommandId(0x3002)여야 할 텐데, Visual Studio는 ("MacroSubList"로 이미 하나 점유하고 있던) 첫 번째 메뉴에 대해 matchedCommand.MatchedCommandId == 0으로 설정해서 호출합니다. 따라서, 동적 메뉴의 첫 번째 항목을 구분하는 Command ID는 0이 되고, 그에 따라 텍스트를 다음과 같은 식으로 변경해 줘야 합니다.

private void OnBeforeQueryStatusDynamicItem(object sender, EventArgs args)
{
    DynamicItemMenuCommand matchedCommand = (DynamicItemMenuCommand)sender;

    matchedCommand.Enabled = true;
    matchedCommand.Visible = true;

    if (matchedCommand.MatchedCommandId == 0)
    {
        matchedCommand.Text = "CMD1";
    }
}

즉, 동적 메뉴라고는 하지만 최소 1개는 이미 포함하고 있는 상태의 메뉴를 구성해야 하는 것입니다.

그다음, Visual Studio는 추가로 메뉴가 있는지에 대해 알아내야 하는데 이를 위해 (OleMenuCommand를 상속받은) DynamicItemMenuCommand로 cmdidMyDynamicStartCommand(0x3002) 값에 +1을 하면서 그 값이 유효한 지에 대해 OleMenuCommand.DynamicItemMatch 메서드를 통해 묻습니다. 따라서 표시해야 할 동적 메뉴가 2개 더 있다면 다음과 같이 구현해 줘야 합니다.

public override bool DynamicItemMatch(int cmdId)
{
    if (cmdId == 0)
    {
        return true; // 가장 첫 번째 메뉴 == 유효
    }

    if (cmdId == (MacroCommand.CommandId + 1))
    {
        this.MatchedCommandId = cmdId;
        return true;  // 두 번째 메뉴 == 유효
    }

    if (cmdId == (MacroCommand.CommandId + 2))
    {
        this.MatchedCommandId = cmdId;
        return true; // 세 번째 메뉴 == 유효
    }

    return false; // 이외의 메뉴 ID들은 유효하지 않음.
}

이쯤에서, 위와 같이 구성하는 것이 다소 번거로우니 동적 메뉴를 위한 구성을 담을 MacroRepository 코드를 다음과 같이 작성합니다.

using System.Collections.Generic;

namespace VSMacro
{
    public class MacroRepository
    {
        List<MacroItem> _macros = new List<MacroItem>();

        public MacroRepository()
        {
            _macros.Add(new MacroItem(0, "CMD1", "__cmd1__"));
            _macros.Add(new MacroItem(MacroCommand.CommandId + 1, "CMD2", "__cmd2__"));
            _macros.Add(new MacroItem(MacroCommand.CommandId + 2, "CMD3", "__cmd3__"));
        }

        public string GetCommandTextByPos(int index)
        {
            if (index != 0)
            {
                index = index - MacroCommand.CommandId;
            }

            return _macros[index].Text;
        }

        public string GetCommandSourceCodeByPos(int index)
        {
            if (index != 0)
            {
                index = index - MacroCommand.CommandId;
            }

            return _macros[index].SourceCode;
        }

        public bool ContainsCmd(int cmdId)
        {
            return _macros.Exists((item) => item.Id == cmdId);
        }
    }

    public class MacroItem
    {
        int _id;
        public int Id
        {
            get { return _id; }
        }

        string _text;
        public string Text
        {
            get { return _text; }
        }

        string _sourceCode;
        public string SourceCode
        {
            get { return _sourceCode; }
        }

        public MacroItem()
        {
        }

        public MacroItem(int id, string text, string sourceCode)
        {
            _id = id;
            _text = text;
            _sourceCode = sourceCode;
        }
    }
}

그럼, DynamicItemMenuCommand.DynamicItemMatch 코드를 다음과 같이 변경할 수 있고,

public override bool DynamicItemMatch(int cmdId)
{
    if (_repo.ContainsCmd(cmdId))
    {
        this.MatchedCommandId = cmdId; // 이때 설정한 MatchedCommandId 값 덕분에,
                                        // 연이어 호출되는 OnBeforeQueryStatusDynamicItem 메서드에서
                                        // 방금 전에 Visual Studio가 조회한 동적 메뉴의 ID가 무엇인지 알 수 있게 됨.
        return true;
    }

    return false;
}

MacroCommand.OnBeforeQueryStatusDynamicItem 메서드는 이렇게 정의할 수 있습니다.

private void OnBeforeQueryStatusDynamicItem(object sender, EventArgs args)
{
    DynamicItemMenuCommand matchedCommand = (DynamicItemMenuCommand)sender;

    matchedCommand.Enabled = true;
    matchedCommand.Visible = true;

    matchedCommand.Text = _repo.GetCommandTextByPos(matchedCommand.MatchedCommandId);

    matchedCommand.MatchedCommandId = 0;
}

위의 코드들을 보시면, 좀 이해가 안 되는 부분이 있습니다. 전통적인 생각으로는, 동적 메뉴를 구성한다면 Menu 객체가 하나 있고 그것의 Children 속성에 개별 메뉴에 해당하는 Menu 인스턴스들이 생성될 것으로 예상하게 됩니다. 가령 다음과 같은 코드를 상상할 수 있습니다.

// Visual Studio 측 (가상 코드)
Menu root = new Menu("MyMacro List");
MenuExtension_AddSubMenu(root);

// 확장 모듈 측
MenuExtension_AddSubMenu(root)
{
    root.Children.Add(new Menu("CMD1"));
    root.Children.Add(new Menu("CMD2"));
    root.Children.Add(new Menu("CMD3"));
}

그런데, Visual Studio는 다음과 같은 식으로 동적 메뉴를 구성하고 있습니다.

// Visual Studio 측 (가상 코드)
Menu root = new Menu("MyMacro List");
Menu subMenu = new Menu("MacroSubList");

OleMenuCommand dynamicMenu = new ExtensionMenu(subMenu);

dynamicMenu.MatchedCommandId = 0;
dynamicMenu.OnBeforeQueryStatusDynamicItem(dynamicMenu, EventArgs.Empty); // 이때 subMenu.Text 속성에 "CMD1"을 설정해야 함.

int startId = dynamicMenu.MatchedCommandId + 1;
while (true)
{
    dynamicMenu.MatchedCommandId = startId;

    if (dynamicMenu.DynamicItemMatch(startId) == false)
    {
        break;
    }

    dynamicMenu.OnBeforeQueryStatusDynamicItem(dynamicMenu, EventArgs.Empty); // 이때 2번째 이후의 동적 메뉴의 문자열을 설정
    startId ++;
}

가만 보면, OleMenuCommand 인스턴스 하나로 동적으로 추가해야 할 모든 메뉴를 처리하고 있는 것입니다. 즉, OnBeforeQueryStatusDynamicItem이나 DynamicItemMatch가 호출될 때의 메뉴 인스턴스는 모두 동일하게/단일한 DynamicItemMenuCommand 인스턴스가 되는 것입니다. (그렇습니다, 처리 절차가 일반적이지 않아서 좀 혼란스럽습니다.)

암튼 저렇게 해서 결국 동적 메뉴로 구성된 항목을 사용자가 선택하면 DynamicItemMenuCommand의 3번째 인자로 넘겨준 OnInvokedDynamicItem 메서드를 호출해 줍니다.

private void OnInvokedDynamicItem(object sender, EventArgs args)  
{  
    DynamicItemMenuCommand invokedCommand = (DynamicItemMenuCommand)sender;  

    // invokedCommand.MatchedCommandId
    // invokedCommand.Text
}  

재미있는 것은, 이 메서드가 호출될 때의 invokedCommand.MatchedCommandId는 해당 메뉴의 존재를 확인하기 위해 DynamicItemMatch를 통해 주고받았던 ID라고 장담할 수 없습니다. 왜냐하면 결국 동적 메뉴를 의미하는 DynamicItemMenuCommand 객체는 1개이기 때문에 그 인스턴스에 현재 어떤 값을 설정하고 있느냐에 따라 달라질 수 있기 때문입니다. 즉, 개발자가 작성한 코드가 어떤 식이냐에 따라 MatchedCommandId가 선택된 메뉴의 ID가 맞을 수도/맞지 않을 수도 있습니다.

사실 Visual Studio는 선택된 동적 메뉴의 MatchedCommandId를 보관하고 있지 않는 것처럼(실제로 보관하고 있지 않는 듯) 행동합니다. 그래서, 첫 번째 메뉴가 선택되었을 때 Visual Studio는 OnInvokedDynamicItem을 다음과 같은 문맥으로 호출해 줍니다.

// Visual Studio 측 (가상 코드)

OleMenuCommand dynamicMenu = ...;

dynamicMenu.MatchedCommandId = 0;
dynamicMenu.OnBeforeQueryStatusDynamicItem(dynamicMenu, EventArgs.Empty);
dynamicMenu.OnInvokedDynamicItem(dynamicMenu, EventArgs.Empty);

즉, 첫 번째 메뉴이기 때문에 MatchedCommandId를 0으로 설정하고 OnBeforeQueryStatusDynamicItem 호출로 상태를 확인한 다음 OnInvokedDynamicItem을 호출하는 것입니다.

동적으로 추가된 두 번째 메뉴부터는 말 그대로 .vsct에 등록했던 cmdidMyDynamicStartCommand(0x3002)의 값에 "+ N"을 하면서 이렇게 호출합니다.

// Visual Studio 측 (가상 코드)
OleMenuCommand dynamicMenu = ...;

// 2번째 메뉴가 선택되었다면,
dynamicMenu.DynamicItemMatch(cmdidMyDynamicStartCommand + 1);           // 호출로 인해 dynamicMenu.MatchedCommandId == cmdidMyDynamicStartCommand + 1로 설정됨
dynamicMenu.OnBeforeQueryStatusDynamicItem(dynamicMenu, EventArgs.Empty); // 호출로 인해 dynamicMenu.MatchedCommandId == 0으로 설정됨
dynamicMenu.DynamicItemMatch(cmdidMyDynamicStartCommand + 1);           // 호출로 인해 다시 dynamicMenu.MatchedCommandId == cmdidMyDynamicStartCommand + 1로 설정됨
dynamicMenu.OnInvokedDynamicItem(dynamicMenu, EventArgs.Empty);         // 결국 dynamicMenu.MatchedCommandId == cmdidMyDynamicStartCommand + 1 값으로 호출됨

// 3번째 메뉴가 선택되었다면,
dynamicMenu.DynamicItemMatch(cmdidMyDynamicStartCommand + 2);           // 호출로 인해 dynamicMenu.MatchedCommandId == cmdidMyDynamicStartCommand + 2로 설정됨
dynamicMenu.OnBeforeQueryStatusDynamicItem(dynamicMenu, EventArgs.Empty); // 호출로 인해 dynamicMenu.MatchedCommandId == 0으로 설정됨
dynamicMenu.DynamicItemMatch(cmdidMyDynamicStartCommand + 2);           // 호출로 인해 다시 dynamicMenu.MatchedCommandId == cmdidMyDynamicStartCommand + 1로 설정됨
dynamicMenu.OnInvokedDynamicItem(dynamicMenu, EventArgs.Empty);         // 결국 dynamicMenu.MatchedCommandId == cmdidMyDynamicStartCommand + 1 값으로 호출됨

저 상황을 보고, DynamicItemMatch, OnBeforeQueryStatusDynamicItem의 메서드 구현을 다시 보면 왜 MatchedCommandId의 값에 0이나 cmdId를 명시적으로 설정해 주는 코드가 들어가야 하는지 이해할 수 있습니다.




위와 같은 코드로 Visual Studio를 실행하면 다음과 같은 화면을 볼 수 있고,

dynamic_menu_in_vsix_2.png

각각의 메뉴를 선택하면, MacroItem 객체에 설정했던 SourceCode 속성의 문자열이 메시지 상자로 뜨는 것을 확인할 수 있습니다.

(이 글의 첨부 파일은 예제 코드를 포함합니다.)




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 7/13/2021]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 61  62  63  64  65  66  67  68  [69]  70  71  72  73  74  75  ...
NoWriterDateCnt.TitleFile(s)
12211정성태4/27/202019315개발 환경 구성: 486. WSL에서 Makefile로 공개된 리눅스 환경의 C/C++ 소스 코드 빌드
12210정성태4/20/202020796.NET Framework: 903. .NET Framework의 Strong-named 어셈블리 바인딩 (1) - app.config을 이용한 바인딩 리디렉션 [1]파일 다운로드1
12209정성태4/13/202017465오류 유형: 614. 리눅스 환경에서 C/C++ 프로그램이 Segmentation fault 에러가 발생한 경우 (2)
12208정성태4/12/202016059Linux: 29. 리눅스 환경에서 C/C++ 프로그램이 Segmentation fault 에러가 발생한 경우
12207정성태4/2/202015960스크립트: 19. Windows PowerShell의 NonInteractive 모드
12206정성태4/2/202018526오류 유형: 613. 파일 잠금이 바로 안 풀린다면? - The process cannot access the file '...' because it is being used by another process.
12205정성태4/2/202015167스크립트: 18. Powershell에서는 cmd.exe의 명령어를 지원하진 않습니다.
12204정성태4/1/202015200스크립트: 17. Powershell 명령어에 ';' (semi-colon) 문자가 포함된 경우
12203정성태3/18/202018042오류 유형: 612. warning: 'C:\ProgramData/Git/config' has a dubious owner: '...'.
12202정성태3/18/202021276개발 환경 구성: 486. .NET Framework 프로젝트를 위한 GitLab CI/CD Runner 구성
12201정성태3/18/202018507오류 유형: 611. git-credential-manager.exe: Using credentials for username "Personal Access Token". [1]
12200정성태3/18/202018592VS.NET IDE: 145. NuGet + Github 라이브러리 디버깅 관련 옵션 3가지 - "Enable Just My Code" / "Enable Source Link support" / "Suppress JIT optimization on module load (Managed only)"
12199정성태3/17/202016235오류 유형: 610. C# - CodeDomProvider 사용 시 Unhandled Exception: System.IO.DirectoryNotFoundException: Could not find a part of the path '...\f2_6uod0.tmp'.
12198정성태3/17/202019597오류 유형: 609. SQL 서버 접속 시 "Cannot open user default database. Login failed."
12197정성태3/17/202018957VS.NET IDE: 144. .NET Core 콘솔 응용 프로그램을 배포(publish) 시 docker image 자동 생성 - 두 번째 이야기 [1]
12196정성태3/17/202016028오류 유형: 608. The ServicedComponent being invoked is not correctly configured (Use regsvcs to re-register).
12195정성태3/16/202018341.NET Framework: 902. C# - 프로세스의 모든 핸들을 열람 - 세 번째 이야기
12194정성태3/16/202021035오류 유형: 607. PostgreSQL - Npgsql.NpgsqlException: sorry, too many clients already
12193정성태3/16/202018036개발 환경 구성: 485. docker - SAP Adaptive Server Enterprise 컨테이너 실행 [1]
12192정성태3/14/202020044개발 환경 구성: 484. docker - Sybase Anywhere 16 컨테이너 실행
12191정성태3/14/202021118개발 환경 구성: 483. docker - OracleXE 컨테이너 실행 [1]
12190정성태3/14/202015732오류 유형: 606. Docker Desktop 업그레이드 시 "The process cannot access the file 'C:\Program Files\Docker\Docker\resources\dockerd.exe' because it is being used by another process."
12189정성태3/13/202021340개발 환경 구성: 482. Facebook OAuth 처리 시 상태 정보 전달 방법과 "유효한 OAuth 리디렉션 URI" 설정 규칙
12188정성태3/13/202026110Windows: 169. 부팅 시점에 실행되는 chkdsk 결과를 확인하는 방법
12187정성태3/12/202015710오류 유형: 605. NtpClient was unable to set a manual peer to use as a time source because of duplicate error on '...'.
12186정성태3/12/202017469오류 유형: 604. The SysVol Permissions for one or more GPOs on this domain controller and not in sync with the permissions for the GPOs on the Baseline domain controller.
... 61  62  63  64  65  66  67  68  [69]  70  71  72  73  74  75  ...