웹 3D 라이브러리(Three.js)를 이용한 메타버스 환경 구축 및 인터랙티브 웹 개발

안녕하세요, GIS Developer 김형준입니다.

오는 5월 25일부터 3일간 메타버스 환경 구축 및 인터랙티브 웹 개발이라는 주제를 가지고 강의를 진행합니다. 메타버스 환경 구축은 Blender라는 3차원 모델링 툴을 사용하고 인터렉티브 웹 개발은 three.js 라이브러리를 활용합니다. Javascript을 이미 알고 있다는 가정 하에 Blender나 three.js를 전혀 모르시는 분들도 이해하실 수 있도록 진행할 계획입니다.

아래의 영상은 교육 내용 중 실습 예제 중 하나입니다.

교육장소는 서울 판교에 있는 메타버스 캠퍼스입니다. 교육비는 무료이지만 참여할 수 있는 인원 수에 제한이 있습니다. 참여 신청을 위한 링크는 아래의 이미지를 클릭하시면 됩니다. 많은 참여 바랍니다.

지오코딩 서비스(GEOCODER-XR) 기능 개선

OpenAPI 형태의 서비스 단위로 제공되는 GEOCODER-XR이 고객의 요구사항에 맞춰 개선되었습니다. 다음과 같이 3가지 내용이 변경되었습니다.

  1. 지오코딩 결과에 대한 정좌표, 인근좌표, 대표좌표에 대한 구분(기존에는 인근좌표와 대표좌표의 구분이 없었음)
  2. 결과가 여러 개인 경우 정확도 순서에 따라 정렬해서 모든 결과를 다 제공하도록 변경
  3. 결과 형식을 JSON으로 변경(기존은 XML 형식였음)

예를들어 주소 중 “중앙동”으로 검색을 하기 위한 REST 방식의 OpenAPI는 다음과 같습니다.

http://localhost:8080/Gp?command=gc;addr=중앙동

위의 호출에 대한 결과는 다음과 같습니다.

[
   {
      "user-input":"중앙동",
      "full-address":"경기도 성남시 중원구 중앙동",
      "coordinate":[
         37.43959765091859,
         127.1505276103632
      ],
      "vicinity":true,
      "type":"대표좌표"
   },
   {
      "user-input":"중앙동",
      "full-address":"경상남도 통영시 중앙동",
      "coordinate":[
         34.84482143430977,
         128.42308057122085
      ],
      "vicinity":true,
      "type":"대표좌표"
   },
   {
      "user-input":"중앙동",
      "full-address":"경상남도 창원시 진해구 중앙동",
      "coordinate":[
         35.14902784720422,
         128.66126916577852
      ],
      "vicinity":true,
      "type":"대표좌표"
   },

   ...
]

JSON 형식이며 중앙동이라는 이름의 행정명칭은 우리나라에 여러 개이므로 복수의 결과를 모두 전달하고 vicinity가 true이므로 인근좌표이면서 type을 통해 “대표좌표”라는 것을 파악할 수 있습니다.

이번에는 “중앙로10″으로 호출하기 위해 다음 API를 사용합니다.

http://localhost:8080/Gp?command=gc;addr=중앙로10

결과는 다음과 같습니다.

[
   {
      "user-input":"중앙로10",
      "full-address":"경기도 가평군 중앙로 10 (가평읍)",
      "coordinate":[
         37.82638794989058,
         127.5132600931772
      ],
      "vicinity":false,
      "type":"도로명주소"
   },
   {
      "user-input":"중앙로10",
      "full-address":"강원도 영월군 중앙로 10 (영월읍)",
      "coordinate":[
         37.18337775533778,
         128.46505641724204
      ],
      "vicinity":false,
      "type":"도로명주소"
   },
   {
      "user-input":"중앙로10",
      "full-address":"강원도 원주시 중앙로 10 (인동)",
      "coordinate":[
         37.34571078558531,
         127.95473309264851
      ],
      "vicinity":false,
      "type":"도로명주소"
   },

   ...
]

vicinity 값이 false이므로 “정좌표”를 의미합니다.

마지막으로 정확히 1건으로 매칭되는 주소에 대한 경우입니다.

http://localhost:8080/Gp?command=gc;addr=아차산로7나길18

결과는 다음과 같습니다.

[
   {
      "user-input":"아차산로7나길18",
      "full-address":"서울특별시 성동구 아차산로7나길 18 (성수동2가)",
      "coordinate":[
         37.54770170889392,
         127.05746040023593
      ],
      "vicinity":false,
      "type":"도로명주소"
   }
]

SharedArrayBuffer 사용을 위한 Live-Server 서버 설정

일단 SharedArrayBuffer API를 웹브라우저에서 사용하기 위해서는 다음과 같은 해더값을 웹서버에 추가해줘야 합니다.

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

Tomcat이나 Apache 등과 웹서버가 아닌 VS.Code의 확장 기능으로써의 웹 서버인 Live-Server에서 위의 해더값을 추가하기 위해서 먼저 live-server가 설치된 폴더(필자의 경우 C:\Users\GEOSERVICE-PT\.vscode\extensions\ritwickdey.liveserver-5.7.5\node_modules\live-server)의 index.js 파일을 열고 “if (cors) {” 문자열에 대한 코드를 찾은 후 이 코드 바로 직전에 다음 코드를 추가해주면 됩니다.

app.use((req, res, next) => {
    res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
    res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
    next();
});

GWC 라이브러리, UI 구성 예 (gwcCreateModalDialog, Tree 등)

다음과 같은 UI 구성을 목표라고 할때…

대화상자 형태의 레이아웃이므로 gwcCreateModalDialog API를 이용한 클래스를 작성합니다.

class DownloadLayerList {
    constructor() {
        const dlg = gwcCreateModalDialog("레이어 목록 불러오기");
        dlg.content = `
            <div class="download-layer-list-dialog h-center content">
                <div class="vertical-linear-layout">
                    <div class="horizontal-linear-layout v-center">
                        <gwc-label content="필터" outline-type="none"></gwc-label>
                        <gwc-textinput hint="필터링할 값을 입력하세요."></gwc-textinput>
                        <gwc-toolbutton class="refresh" icon="../images/reset.svg"></gwc-toolbutton>
                    </div>
                    <gwc-vscrollview>
                        <content>
                            <gwc-tree></gwc-tree>
                        </content>
                    </gwc-vscrollview>
                    <div class="horizontal-linear-layout h-center v-space"> 
                        <gwc-button class="btnConfirm" title="불러오기" disabled=true></gwc-button>
                        <gwc-button class="btnCancel" title="취소"></gwc-button>
                    </div>
                </div>                    
            </div>
        `;

        dlg.show();
        GeoServiceWebComponentManager.instance.update();

        ....

대화상자의 크기 조정이 필요하다면 아래의 코드를 추가합니다.

        dlg.width = "30em"
        dlg.resizablePanel.resizableLeft = true;
        dlg.resizablePanel.resizableRight = true;
        dlg.resizablePanel.resizableTop = true;
        dlg.resizablePanel.resizableBottom = true;
        dlg.resizablePanel.minWidth = 350;
        dlg.resizablePanel.minHeight = 300;
        dlg.resizablePanel.addEventListener("change", (event) => {
            const { mode, oldHeight, newHeight } = event.detail;
            if(mode === "BOTTOM" || mode == "TOP") {
                const vscrollview = dlg.content.querySelector("gwc-vscrollview");
                const height = parseFloat(window.getComputedStyle(vscrollview).getPropertyValue("height"));
                vscrollview.style.height = `${height - (oldHeight - newHeight)}px`;
                vscrollview.refresh();
            }            
        });
    }

    ....

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


.download-layer-list-dialog {
    padding: 0.5em;
}

.download-layer-list-dialog gwc-textinput {
    flex: 1; /* 대화상자의 가로 크기를 재조정 했을 때 gwc-textinput의 크기도 재조정됨  */
}

.download-layer-list-dialog gwc-vscrollview {
    position: relative;
    height: 16em;
    background: rgba(0,0,0,0.8);
    border-radius: 0.5em;
    overflow: hidden;
}

.download-layer-list-dialog gwc-tree {
    width: 100%; /* tree의 항목의 크기를 대화상자의 폭을 가득 채우도록 조정함 */
    padding: 0.5em 0.5em 0.5em 0.5em;
}

.download-layer-list-dialog gwc-tree .gwc-tree-folder-files-file {
    width: 100%;
}

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

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

    const date = new Date(item.used_time);
    file.tag = `${date.getFullYear()}-${date.getMonth()+1}-${date.getDate()} ${date.getHours()}:${date.getMinutes()}`;
    file.label = item.title;
});