[THREE.JS-EX] 카메라(Camera), 2분할


이해가 되지 않는 부분은 아래의 코드처럼 카메라 생성 시 초기화로 카메라를 y축으로 180도 회전시켰는가… 이다.

cameraOrtho.rotation.y = Math.PI;
cameraPerspective.rotation.y = Math.PI;

이 글의 예제 코드는 THREE.JS 공식 사이트의 EXAMPLES에서 제공되는 코드를 이해하고 제 나름대로의 코드로 재구성한 것입니다.

[THREE.JS-EX] TransformControls를 이용하여 Spline 편집


_initialize 함수를 보면 아래의 변수가 정의되는 것을 볼 수 있습니다.

this._splineHelperObjects = [];
this._positions = [];
this._splines = {};

_splineHelperObjects는 스플라인의 제어점에 대한 Mesh 객체를 담고 있습니다. _positions는 _splineHelperObjects에 저장된 Mesh들의 position 속성 객체들 항목으로 참조하고 있고 _splines은 key-value 컨테이너로 uniform, centripetal, chordal를 key로 하여 THREE.CatmullRomCurve3 객체를 value로 가집니다. 이때 이 value에 mesh를 속성으로하여 THREE.Line 객체를 갖습니다.

이 글의 예제 코드는 THREE.JS 공식 사이트의 EXAMPLES에서 제공되는 코드를 이해하고 제 나름대로의 코드로 재구성한 것입니다.

OpenAPI를 이용한 현재 기상(기온, 강수량, 바람성분, 습도, 강수형태, 풍향, 풍속) 정보 얻기

공공데이터포털(data.go.kr)에서 “동네예보”로 검색하면 아래와 같은 결과 중 기상청_동네예보 조회서비스에 대한 활용신청 버튼을 클릭하여 OpenAPI 사용 신청을 합니다.

저 같은 경우 고맙게도 사용 신청 후 즉시 사용할 수 있었는데요. 마이페이지에서 오픈API의 개발계정을 클릭하면 앞서 신청한 서비스가 표시됩니다. 표시되는 서비스 중 앞서 신청한 서비스의 제목을 클릭하면 이 중 초단기실황조회의 확인 버튼을 클릭하면 아래와 같은 화면이 표시됩니다.

현재의 기상 정보를 얻을 목적이므로 위의 서비스 중에서 초단기실황조회에 대한 미리보기의 확인 버튼을 클릭하면 다음과 같이 테스트해 볼 수 있는 UI가 표시됩니다.

서비스 키와 날짜, 시간, 위치값(위의 경우 nx와 ny로 지정하며 광진구 화양동)을 입력하고 미리보기 버튼을 클릭하면 아래의 결과가 표시됩니다.



   
00 NORMAL_SERVICE
XML 20210526 1200 PTY 61 126 0 20210526 1200 REH 61 126 37 20210526 1200 RN1 61 126 0 20210526 1200 T1H 61 126 22.6 20210526 1200 UUU 61 126 1.9 20210526 1200 VEC 61 126 210 20210526 1200 VVV 61 126 3.3 20210526 1200 WSD 61 126 3.8 10 1 8

총 8개의 정보가 표시됩니다. category 태그를 통해 어떤 종류인지 확인할 수 있으며 T1H(기온 ℃), RN1(1시간 강수량 mm), UUU(동서바람성분 m/s), VVV(남북바람성분 m/s), REH(습도 %), PTY(강수형태), VEC(풍향 deg), WSD(풍속 m/s)입니다. 값이 900이상 -900이하일 경우 누락된 값으로 판단해야 합니다. 아울러 정보 중 강수형태는 코드값으로 값에 대한 의미는 없음(0), 비(1), 비/눈(2), 눈(3), 소나기(4), 빗방울(5), 빗방울/눈날림(6), 눈날림(7)입니다.

초단기실행 정보는 10분 단위로 업데이트 된다고 합니다. 그리고 관측지점은 nx와 ny로 지정하는데 이 값은 일반적인 경위도값이 아닌 이 서비스를 위한 기준 격자 번호입니다. 해당 서비스에 대한 엑셀 문서를 통해서 각 행정구역에 대한 nx와 ny를 파악할 수 있도 있고 경위도를 통해 변환할 수 있는 C언어 코드를 문서를 통해 제공하고 있습니다.

이 글은 현재 기상 정보를 파악하기 위해 필요한 최소한의 내용을 설명하고 있으므로 전체적인 내용 파악이 필요할 경우 해당 서비스에 대한 활용가이드 문서를 참고하시기 바랍니다.

아래의 코드는 위의 서비스를 실제 시스템에 반영할 때 발생하는 CORS 문제를 피하기 위한 프록시(Proxy) 서버를 사용해 호출하는 코드입니다.

let proxy = "http://111.111.111.111/Xr";
let urlHeader = "http://apis.data.go.kr/1360000/VilageFcstInfoService/getUltraSrtNcst";
let queryString = "serviceKey=__ENCODED_KEY__&pageNo=1&numOfRows=10&dataType=XML&base_date=20210526&base_time=0600&nx=18&ny=1";
queryString = encodeURI(queryString);
url = proxy + "?reqPrx|" + urlHeader + "|" + queryString;

const xhr = new XMLHttpRequest();
xhr.open("GET", url);
        
xhr.onreadystatechange = function (evt) {
    if (xhr.readyState === 4) {
        if (xhr.status === 200) {
            console.log(xhr.responseText);
        } else {
            throw new Error("Error");
        }
    }
};

xhr.send(null);

웹에서 Binary Data를 파일로 저장하기

웹에서 ArrayBuffer를 파일로 저장하기 위한 코드에 대한 내용입니다.

웹에서는 PC에 저장된 파일을 읽거나 쓸때 반드시 사람의 행위가 수반되어야 합니다. 즉, 버튼과 같은 UI를 사람이 의도하여 클릭을 해야만 한다는 것입니다. 이를 위해 필요한 UI에 대한 DOM 요소를 아래처럼 생성해 둡니다.

<button id="exportFile">Export To File</button>

그리고 위의 버튼에 대한 클릭 이벤트에 대한 코드는 다음과 같이 지정하구요.

const exportButton = document.getElementById( 'exportFile' );
exportButton.addEventListener( 'click', exportFile );

위의 exportFile 함수에 대한 코드는 다음과 같습니다.

function exportFile() {
    const buffer = createData(); /* ArrayBuffer 형식의 데이터를 생성하는 함수 */ 
    const blob = new Blob( [ buffer ], 
        { type: 'application/octet-stream' } )
    link.href = URL.createObjectURL( blob );
    link.download = 'file.dat';
    link.click();
}

위의 코드에서 link는 다음처럼 동적으로 생성된 a 테그 요소입니다.

const link = document.createElement( 'a' );
link.style.display = 'none';
document.body.appendChild( link );

결과적으로 사용자가 버튼을 클릭하면 a Tag가 클릭한 것으로 처리되어 파일 저장이 진행됩니다.

여기서 추가로 만약 저장하고자 하는 데이터가 바이너리가 아닌 텍스트라면 위의 코드 중 변경해야할 부분은 어딜까요? exportFile 함수에 대한 코드 중 3,4번의 buffer가 단순한 문자열 값이며 type: ‘text/plain’로 지정해 주면 됩니다.

DBMS 살펴보기

운영 중인 시스템에서 사용하는 DBMS를 살펴보는 방식은 해당 DBMS의 내용을 기술해 놓은 문서를 보거나 실제 DBMS에 저장된 데이터를 DB 툴을 통해 살펴보는 방식이 있습니다. 이 포스트는 운영중인 DBMS의 내용을 문서나 DB 툴 없이 java 코드를 통해 살펴보는 내용을 정리한 글입니다.

DBMS를 파악함에 있어서 관련 문서나 담당자의 설명 없이 해당 DBMS에 저장된 데이터를 통해 파악하는 것은 정확하지 않은 추측일 가능성이 큽니다. 관련 문서나 담당자의 설명과 DB 툴이나 이 글의 내용을 통해 DBMS에 실제로 저장된 데이터를 살펴보는 것 모두를 병행하여 코로스체킹(Cross Checking)을 해야 합니다.

흔히 가장 많이 사용하는 DBMS인 MySQL을 토대로 하였고 Java의 JDBC를 사용하였습니다. 테스트한 서버의 MySQL의 버전이 무엇인지는 모르겠으나 사용한 JDBC에 대한 jar 라이브러리는 mysql-connector-java-8.0.22.jar입니다.

이 프로그램의 결과는 해당 DBMS를 JDBC로 연결하고 해당 DB에 저장된 테이블 이름들을 가져온 후 각 테이블에 대해 최근에 저장된 단 3개의 필드값을 지정된 파일에 저장하는 것입니다. 테이블들이 많을 경우 제외할 테이블을 지정할 수 있습니다.

코드는 다음과 같습니다.

package tstMySQL;

import java.io.File;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

public class MainEntry {
    public static void main(String[] args) {
        File file = new File("d:/text.txt");
        PrintWriter writer = null;
			
        Connection con = null;
        PreparedStatement pstmt = null;   
        ResultSet rs = null;
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
            con = DriverManager.getConnection("jdbc:mysql://localhost:3306/geos", "ID", "#PW");
            pstmt = con.prepareStatement("SHOW TABLES");
            rs = pstmt.executeQuery();
            ArrayList<String> tableNames = new ArrayList<String>();
            
            while(rs.next()) {
                String tableName = rs.getString(1);
                tableNames.add(tableName);
            }
            
            rs.close();
            pstmt.close();
            
            writer = new PrintWriter(new FileWriter(file));
            
            String[] ignoredTablesArray = { "etc", "abc" };
            Set<String> ignoredTables = new HashSet<String>(Arrays.asList(ignoredTablesArray));
            
            Iterator<String> iter = tableNames.iterator();
            while(iter.hasNext()) {
            	String tableName = iter.next();
            	if(ignoredTables.contains(tableName)) continue;
            	
            	writer.println("# TableName : " + tableName);
            
            	pstmt = con.prepareStatement("SELECT * FROM " + tableName + " ORDER BY ID DESC LIMIT 3");
            	rs = pstmt.executeQuery();
            	ResultSetMetaData rsmd = rs.getMetaData();        
                int fieldCount = rsmd.getColumnCount();

                while(rs.next()) {
                    for(int iField=0; iField<fieldCount; iField++) {
                        Object obj = rs.getObject(iField+1);
                        String value = obj == null ? "NULL" : obj.toString();
                        String fieldName = rsmd.getColumnName(iField+1);
                        writer.println("  " + fieldName + " : " + value);
                    }
                    writer.println();
                }

                rs.close();
                pstmt.close();
                            }
        } catch(Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if(rs != null) rs.close();
                if(pstmt != null)pstmt.close();
                if(con != null) con.close();
                if(writer != null) writer.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

27번 코드에 DBMS 연결을 위한 정보를 정확히 입력해야 합니다. 42번 코드는 파악할 필요가 없는 테이블의 이름으로 변경하면 되구요. 52번째 코드에서 각 테이블에 대해 살펴보고자 하는 SQL 문을 지정하고 있는데요. 여기서는 최근에 저장된 3개의 레코드만을 조회하고 있습니다. 이 부분은 DBMS의 테이블에 맞는 쿼리로 구성해야 합니다. 최종 결과는 19번 코드에서 지정한 D:/text.txt 파일로 저장됩니다.