#GWC UI Library : Tree

웹 UI 라이브러리인 GWC에서 제공하는 Tree 컴포넌트에 대한 예제 코드입니다.

먼저 DOM 구성은 다음과 같습니다.

그리고 CSS 구성은 다음과 같구요.

.center {
    display: flex;
    width: 100%;
    height: 100%;
    justify-content: center;
    align-items: center;
    flex-direction: column;
    gap: 1em;
}

gwc-vscrollview {
    width: 22em;
    height: 30em;
    background: rgba(0,0,0,0.3);
    border: 1px solid black;
}

gwc-tree {
    width: 100%;
    padding: 0.5em 0.5em;
}

.h-center {
    display: fex;
    justify-content: center;
    align-items: center;
}

js 코드는 다음과 같습니다.

window.onload = () => {
    // 최상위 폴더(root) 
    const rootFolder = tree.rootFolder;
    
    // 폴더 추가
    const gaFolder = rootFolder.addFolder("아카이브");
    const docFolder = rootFolder.addFolder("문서").open();

    rootFolder.addFolder("프로그램");
    const programFolder = rootFolder.getFolder("프로그램");

    const humanGeoFolder = gaFolder.addFolder("인문공간데이터");
    const adminGeoFolder = gaFolder.addFolder("행정경계");

    // 폴더에 파일 추가    
    humanGeoFolder.addFile("인구통계.zip", "url(../examples/images/icon7.png)");
    humanGeoFolder.addFile("유동인구.zip", "url(../examples/images/icon7.png)");
    humanGeoFolder.addFile("유아통계.zip", "url(../examples/images/icon7.png)");

    const koreaFolder = adminGeoFolder.addFolder("대한민국");
    koreaFolder.addFile("시도.zip", "url(../examples/images/icon3.png)");
    koreaFolder.addFile("시군구.zip", "url(../examples/images/icon3.png)");
    koreaFolder.addFile("읍면동.zip", "url(../examples/images/icon3.png)");

    adminGeoFolder.addFile("서울특별시.zip", "url(../examples/images/icon3.png)");
    adminGeoFolder.addFile("경기도.zip", "url(../examples/images/icon3.png)");

    docFolder.addFile("레포트1.pdf", "url(../examples/images/icon6.png)");
    docFolder.addFile("레포트2.pdf", "url(../examples/images/icon6.png)");
    docFolder.addFile("레포트3.pdf", "url(../examples/images/icon6.png)");

    programFolder.addFile("VisualSudio.Code.zip", "url(../examples/images/icon5.png)")
    programFolder.addFile("PhotoShop.zip", "url(../examples/images/icon5.png)")
    programFolder.addFile("리터널_PS5.zip", "url(../examples/images/icon5.png)")

    // 폴더 열고 닫기
    btnProgramOpenClose.addEventListener("click", () => {
        const folder = tree.rootFolder.getFolder("아카이브");
        if(folder.isOpen()) folder.close();
        else folder.open();
    });

    // 특정 폴더에 파일 추가
    btnAddFile.addEventListener("click", () => {
        const folder = tree.rootFolder.getFolder("문서");
        const file = folder.getFile("NEW 레포트.pdf");
        if(file) {
            file.remove();
        } else {
            const file = folder.addFile("NEW 레포트.pdf", "url(../examples/images/icon6.png)");

            // 파일 또는 폴더에 사용자 정의 데이터 추가
            file.setData("생성일자", "2022년 2월 10일");
            console.log(file.getData("생성일자"));
        }
    });

    // 파일(폴더+파일)에 대한 클릭 이벤트
    tree.addEventListener("fileClick", (event) => {
        const file = event.detail.file;
        
        const parentFolderName = file.parentFolder.name;
        const bFolder = file.isFolder();
        const bOpen = file.isOpen();

        label.content = `
            이름: ${file.name} 
            부모폴더: ${parentFolderName?parentFolderName:"없음"} 
            종류: ${bFolder?"폴더":"파일"} 
            ${bFolder?`상태: ${bOpen?"열림":"닫힘"}`:""}
        `;

        vscrollview.refresh(); // 폴더 열기로 인한 트리 컴포넌트 크기 변경에 따른 스크롤뷰 업데이트
    });

    // 트리 컴포넌트의 크기가 가변이므로 스크롤뷰를 업데이트 해줌
    vscrollview.refresh();

    GeoServiceWebComponentManager.instance.update();
};

실행 결과는 다음과 같습니다.

트리의 항목에 대한 팝업 메뉴 기능을 적용할 때에 대한 코드입니다.

// PopupMenu 생성 시작
const popupMenu = gwcCreatePopupMenu();
popupMenu.addMenuItem("menu1", {
    text: "생성",
    _icon: "images/icon1.png",
    onClick: (menuId) => { 
        gwcMessage(`폴더 생성(${menuId})`);
        popupMenu.hide();                
    }
});
popupMenu.addMenuItem("menu2", {
    text: "이름 변경",
    _icon: "images/icon2.png",
    onClick: (menuId) => { 
        gwcMessage(`${popupMenu.fileData.name} 이름 변경(${menuId})`);
        popupMenu.hide();                
    }
});
popupMenu.addMenuItem("menu3", {
    text: "삭제",
    _icon: "images/icon3.png",
    onClick: (menuId) => { 
        gwcMessage(`${popupMenu.fileData.name} 삭제(${menuId})`);
        popupMenu.hide();
    }
});
popupMenu.addMenuItem("menu4", {
    text: "잘라내기",
    _icon: "images/icon4.png",
    onClick: (menuId) => { 
        gwcMessage(`${popupMenu.fileData.name} 잘라내기(${menuId})`);
        popupMenu.hide();
    }
});
popupMenu.addMenuItem("menu5", {
    text: "붙여넣기",
    _icon: "images/icon5.png",
    onClick: (menuId) => { 
        gwcMessage(`${popupMenu.fileData.name}에 붙여넣기(${menuId})`);
        popupMenu.hide();
    }
});
// PopupMenu 생성 완료

// 파일(폴더+파일)에 대한 클릭 이벤트
tree.addEventListener("fileClick", (event) => {
    const file = event.detail.file;
    const contextMenu = event.detail.contextMenu;

    if(contextMenu)  {
        console.log(event.detail.originalEvent);
        popupMenu.fileData = file; // fileData는 임의로 부여한 속성으로 팝업 메뉴 실행 시에 참조됨
        popupMenu.show(event.detail.originalEvent.clientX, event.detail.originalEvent.clientY);
    }
});

위의 코드에 대한 실행 결과는 다음과 같습니다.

트리를 구성하는 항목에 대해서 선택된 항목이라는 피드백을 줄 수 있습니다. 다음 코드를 참고하기 바랍니다.

tree.clearSelection(); // 일단 기존의 선택된 항목에 대해 선택 해제
tree.rootFolder.getFolder("아카이브").selected = true; // 폴더를 얻고 해당 폴더를 선택된 상태로 표시

폴더 또는 파일 항목의 우측에 Tag 정보를 표시할 수 있습니다. 즉, getFolder 또는 getFile을 통해 얻은 item 객체의 tag 속성(get, set)을 설정하면 됩니다. 아래는 파일항목의 우측에 파일의 크기를 표시하는 예시입니다.

tree 컴포넌트는 동일한 계층에 동일한 이름을 가진 항목을 추가할 수 없습니다. 이때 label 속성을 이용해 이름은 다르지만 표시되는 제목만을 변경해 줄 수 있습니다. 코드 예시는 다음과 같습니다.

data.forEach(item => {
    const rootFolder = this.#tree.rootFolder;
    rootFolder.addFile(item.id, "url(../images/layers.svg)").label = item.title;
});

#GWC UI Library : Memo

웹 UI 라이브러리인 GWC에서 제공하는 Memo 컴포넌트에 대한 예제 코드입니다.

먼저 DOM 구성은 다음과 같습니다. gwc-resizable-panel 태그로 감싸서 크기 조정이 가능하도록 했습니다. 이는 옵션입니다.

그리고 CSS 구성은 다음과 같구요.


.center {
    display: flex;
    width: 100%;
    height: 100%;
    justify-content: center;
    align-items: center;
    flex-direction: column;
    gap: 0.5em;
}

.vcenter {
    display: flex;
    align-items: center;
    gap: 0.3em;
}

.hcenter {
    flex-direction: column;
    display: flex;
    _align-items: center;
}

gwc-memo {
    width: 100%;
    height: 100%;
}

js 코드는 다음과 같습니다.

window.onload = () => {
    button1.onclick = () => {
        memo.value = "메모에 대한 내용은 코드로 변경할 수 있어요.\n줄 바꿈도 가능하답니다.";
    }

    button2.onclick = () => {
        gwcMessage(memo.value);
    }

    button3.onclick = () => {
        console.log(memo.disabled);
        memo.disabled = !memo.disabled;
    }

    button4.onclick = () => {
        console.log(memo.readonly);
        memo.readonly = !memo.readonly;
    }

    memo.addEventListener("change", (event) => {
        labelEvent.content = `${memo.value.length}자가 입력됨 (입력가능 문자수 ${memo.maxLength})`;
    })

    GeoServiceWebComponentManager.instance.update();
   
};

실행 결과는 다음과 같습니다.

gwc-memo 태그 선언을 통해 value 속성에 문자열을 지정할 때 문자열에 쌍따옴표가 있을 경우 변환이 필요합니다.

<gwc-memo value="${params.content.replaceAll("\"", "&quot;")}"></gwc-memo>

#GWC UI Library : PopupMenu

웹 UI 라이브러리인 GWC에서 제공하는 PopupMenu 컴포넌트에 대한 예제 코드입니다.

큰 의미는 없으나 DOM 구성은 다음과 같습니다.

그리고 CSS 구성은 다음과 같구요.

.center {
    display: flex;
    width: 100%;
    height: 100%;
    justify-content: center;
    align-items: center;
    gap: 1em;
}

js 코드는 다음과 같습니다.

window.onload = () => {
    const popupMenu = gwcCreatePopupMenu();

    popupMenu.addMenuItem("menu1", {
        text: "녹음 시작",
        icon: "images/icon1.png",
        onClick: (menuId) => { 
            gwcMessage(`녹음 시작 클릭(${menuId})`);
            popupMenu.hide();                
        }
    });

    popupMenu.addMenuItem("menu2", {
        text: "WiFi 활성화",
        icon: "images/icon2.png",
        checked: false,
        onClick: (menuId) => { 
            const bChecked = popupMenu.getMenuChecked(menuId);
            popupMenu.setMenuChecked(menuId, !bChecked);

        }
    });

    popupMenu.addMenuItem("menu3", {
        text: "꿈꾸기",
        icon: "images/icon3.png",
        onClick: (menuId) => { 
            gwcMessage(`상세화하기(${menuId})`);
            popupMenu.hide();
        }
    });

    popupMenu.addMenuItem("menu4", {
        text: "구체화하기",
        icon: "images/icon4.png",
        onClick: (menuId) => { 
            gwcMessage(`구체화하기(${menuId})`);
            popupMenu.hide();
        }
    });

    popupMenu.addMenuItem("menu5", {
        text: "실현하기",
        icon: "images/icon5.png",
        onClick: (menuId) => { 
            gwcMessage(`실현하기(${menuId})`);
            popupMenu.hide();
        }
    });

    const popupMenuZone = document.querySelector(".center");
    popupMenuZone.addEventListener("click", (event) => {
        if(popupMenu.isShown()) {
            popupMenu.hide();
            return;
        }

        if(event.target === popupMenuZone) {
            // 팝업창의 표시를 위해 
            // event.currentTarget.getBoundingClientRect()의 결과값에 대한 left, top을 사용하는 것이 좋음
            popupMenu.show(event.offsetX, event.offsetY);
        }
    });
};

실행 결과는 다음과 같습니다.

PopupMenu를 표시하기 전에 어떤 메뉴 항목을 감추거나 다시 표시해야 할 필요가 있습니다. 이에 대한 이벤트는 showing인데요. 예제 코드는 다음과 같습니다.

popupMenu.addEventListener("showing", (event) => {
    popupMenu.setMenuVisible("menu1", true);
    popupMenu.setMenuVisible("menu2", false);

    // event.cancel = true; -> 이 코드가 작동하면 팝업 메뉴가 표시되지 않음
});

gwcCreatePopupMenu를 응용해서 일반적인 메뉴를 구성할 수 있는데, 이때 서브 메뉴에 대한 구성도 가능합니다. 아래는 코드 예시과 그 결과 이미지입니다.

const menu = gwcCreatePopupMenu();
const shpCsvMenu = gwcCreatePopupMenu();

shpCsvMenu.addMenuItem("menu-shp2csv", {
    text: "SHP 데이터를 CSV로 변환",
    icon: "images/change.svg",
    onClick: (menuId) => {}
});

shpCsvMenu.addMenuItem("menu-xycsv2shp", {
    text: "경위도 좌표 데이터(CSV)를 SHP으로 변환",
    icon: "images/change.svg",
    onClick: (menuId) => {}
});

menu.addSubMenu("menu-shpcsv", shpCsvMenu, { text: "SHP/CSV 변환" })

gwcCreateModalDialog의 resizing 코드 예 (gwc-card에도 적용됨)

하나의 모달 대화상자를 gwcCreateModalDialog 함수를 이용해 만들때 class 단위로 만들면 전체적인 시스템의 UI 기능이 효과적으로 분리됩니다. 먼저 모달 대화상자에 대한 코드를 class로 만듭니다.

class ArchiveManagerDialog {
    constructor() {
        const dlg = gwcCreateModalDialog("아카이브 관리자");
        dlg.content = `
            
`; dlg.width = "50em"; dlg.resizablePanel.resizableLeft = true; dlg.resizablePanel.resizableRight = true; dlg.resizablePanel.resizableTop = true; dlg.resizablePanel.resizableBottom = true; dlg.resizablePanel.minWidth = 450; dlg.resizablePanel.minHeight = 300; dlg.resizablePanel.addEventListener("change", (event) => { if(event.target === dlg.resizablePanel) { const { mode, oldHeight, newHeight } = event.detail; if(mode === "BOTTOM" || mode == "TOP") { const domScrollView = dlg.content.querySelector("gwc-vscrollview"); const height = parseFloat(window.getComputedStyle(domScrollView).getPropertyValue("height")); domScrollView.style.height = `${height - (oldHeight - newHeight)}px`; domScrollView.refresh(); } } }); dlg.show(); GeoServiceWebComponentManager.instance.update(); } }

CSS에 대한 코드는 다음과 같습니다.

.vertical-linear-layout {
    display: flex;
    flex-direction: column;
    gap: 0.3em;
}

.horizontal-linear-layout {
    display: flex;
    gap: 0.3em;
    flex-direction: row;
    _padding: 0 1em;
}

.v-center {
    align-items: center;
}

.h-center {
    justify-content: center;
}

.v-space {
    margin-top: 0.5em;
    margin-bottom: 0.5em;
}

.archive-manager-dialog {
    padding: 0.5em 0.5em 0.5em 0.5em;
}

.archive-manager-dialog gwc-textinput {
    width: 10em;
}

.archive-manager-dialog .search-part {
    margin-left: auto;
}

.archive-manager-dialog gwc-vscrollview {
    height: 30em; /* js 코드로 크기를 조정해야 함 */
    margin: 0 0.2em;
    background: rgba(0,0,0,0.3);
    box-shadow: inset -0.6px -0.6px 0.6px rgba(255,255,255,0.4), 
        inset 0.6px 0.6px 0.6px rgba(0,0,0,0.5);
    border-radius: 0.5em;    
}

.archive-manager-dialog gwc-tree {
    width: 100%;
    padding: 0.5em 0.5em 0.5em 0.5em;
    _border: 1px solid red;    
}

실행 결과는 다음과 같습니다.