#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;    
}

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

[PostgreSQL] 문자열 값 INSERT 할 때 …

INSERT 문을 구성할 때 문자열 값이라는 의미를 외따옴표로 시작해서 외따옴표로 끝납니다. 그렇다면 외따옴표 자체를 문자열 값으로 넣고 싶다면… 외따움표를 2개 넣어주면 됩니다. 그리고 줄간행(Carrage Return) 문자는 아스키값 10번이므로 CHR(10)가 되는데.. 이처럼 값이 외따옴표일때와 줄간행 문자에 대한 저장을 위해 다음 코드 예시가 도움이 되길 바랍니다.

const sql = `
    select geoservice_add_archive(
        ${window._userId},
        ${this.#directoryId?this.#directoryId:"NULL"},
        '${savedFileName.replaceAll("'", "''")}',
        '${file.name.replaceAll("'", "''")}',
        ${this.#selectEPSG.disabled?"NULL":this.#selectEPSG.value},
        ${file.size},
        '${this.#textInputTitle.value.replaceAll("'", "''")}',
        '${this.#memo.value.replaceAll("'", "''").replaceAll("\n", "'||CHR(10)||'")}',
        '${this.#selectedFile.MD5}',
        TRUE,
        TRUE
    ) r        
`;

참고로 geoservice_add_archive는 DB 프로시져이므로 무시하고 관심의 대상을 문자열값에 대한 처리에 중집하면 됩니다.