2009년 12월 22일 화요일

동일한 요청의 다중전송 방지

골치 아픈 문제 때문에 고민하다가 너무나 간단한 해결책을 찾아냈다.
보통 웹이 아닌 클라이언트/서버 프로그램에서 클라이언트의 화면에서 '조회' 등의 버튼을 한번만 클릭하고 결과를 기다려야 하는데 사용자가 조회시간이 좀 걸린다고 여러번 클릭하는 경우가 있다. 이걸 막아야 하는 일이 생겨서 고민했는데 의외로 간단하다.

보통 서버로 요청을 보내기 위해서는 RemoteObject를 통해서 서버의 메소드를 호출하는데, 이 RemoteObjectconcurrency라는 속성이 있다. 이 속성의 디폴트값은 multiple이라서 같은 메소드의 호출응답을 받기 전에 호출해도 계속 요청이 날아가게 되어 있다. 그런데, 속성의 값을 single로 하면, 서버 메소드 호출에 대한 응답을 받기 전에 또 화면에서 호출하면 에러를 발생시킨다.
이때 발생하는 에러이벤트는 FaultEvent이고 event.fault.faultCode == "ConcurrencyError" 조건을 만족하면 조회중이니 잠시 기다려 달라는 메시지를 뿌려 주면 된다. 물론 이러는 동안에도 처음 요청은 잘 갔다가 응답까지 잘 받는다.

보니까 RemoteObject 뿐만 아니라, method 태그에도 이 속성을 사용할 수 있는 것 같다. RemoteObject에 사용하면 그 안에 포함된 메소드 전부에 적용이 되고, method 태그에 사용하면 메소드 단위로 제어가 가능한 것 같다. 테스트해 보질 않아서 더이상의 설명은 패쓰~

2009년 12월 16일 수요일

Hibernate에서 CLOB 컬럼 사용하기

Hibernate에서 오라클 CLOB 컬럼을 사용하려면,

매핑파일에서 컬럼의 type을 text로 설정하고,
[code xml] <property name="clob"  type="text" column="CLOBCOLUMN" /> [/code]

이 컬럼에 해당하는 Value Object의 변수는 String으로 선언해서 사용하면 된다.
[code java] private String clob; [/code]

혹시 이렇게 했는데 에러가 나면 최신 오라클 JDBC 드라이버로 바꿔 보는 정도의 센스는 기본~

2009년 10월 26일 월요일

한글엔 maxChars 대신 maxBytes


기본적으로 Flex의 TextInput에는 readOnly(editable=false) 상태일 때 배경색이 바뀌질 않는다. enabled가 false일 경우에는 스타일에서 배경색을 지정할 수 있을 뿐이다.
그런데, readOnly 상태일 때 내용만 마우스로 선택해서 복사하고 싶을 때는 아무래도 readOnly 상태여야 하는데 입맛에 맞질 않아 readOnly 상태일 때 지정된 배경색이 되도록 하는 컴포넌트를 하나 만들었다. (아래에서 set editable 메소드)
그렇게 잘 쓰고 있었는데 이번에 DB컬럼의 바이트길이에 따라서 TextInput의 입력길이를 제한해야 할 일이 생겼다.(어디나 그렇겠지만..) 그런데, TextInput에는 maxChars라는 문자수 기준의 입력제한만 있어서 한글과 같이 멀티바이트 문자를 사용하는 언어는 맞아 떨어지질 않기 때문에 아래와 같이 기능을 추가한 컴포넌트를 만들었다.
전에 자바스크립트로도 이런 비슷한 기능을 만들어 봐서 ActionScript의 API만 찾으니 그렇게 오래 시간이 걸리지 않았다. 다만, ByteArray 클래스의 writeMultiByte라는 메소드의 두번째 인자가 꼭 있어야 하니 미리 입력되는 Character Set을 알고 있어야 하는 단점이 있긴 하다. 자바처럼 그냥 getBytes 하나로 처리할 수 있으면 언어에 상관없이 사용할 수 있을 텐데.. 아쉽다. 어딘가 환경설정 파일에 세팅해 놓고 그것을 읽게 하면 될 수도 있을 것 같다.

또 아쉬운 점은 KEY_DOWN으로 처리하면 규정길이를 넘는 문자를 입력해도 입력되는 문자가 안 보일 텐데 이 이벤트가 안 먹어서 KEY_UP 이벤트를 처리하니 입력되는 문자가 보였다가 지워져서 약간 모냥이 빠진다.
FOCUS_OUT 이벤트는 마지막으로 길이를 넘어가는 부분에 한글을 입력한 상태에서 탭이나 마우스로 포커스가 벗어나면 넘어간 문자가 안 지워져서 포커스가 벗어날 때도 확인해서 넘어가는 문자를 지우기 위해 이벤트리스너를 달았다.

참고로 addEventListener 메소드의 다섯번째 인자인 weakReference를 true로 한 것은, 이렇게 하면 이 컴포넌트가 지워질 때 리스너에 상관없이 Garbage Collection 대상이 되도록 하기 위해서다.

package com.customcomp
{
    import flash.events.Event;
    import flash.events.FocusEvent;
    import flash.events.KeyboardEvent;
    import flash.utils.ByteArray;
   
    import mx.controls.TextInput;

    // editable 속성이 true일 때는 배경색이 흰색, false일 때는 배경색이 옅은 회색인 TextInput 컴포넌트
    public class TextBox extends mx.controls.TextInput
    {
        // 한글 등 multi-byte 문자들의 입력길이 제한을 위해서 사용하는 속성
        public var maxBytes:int = -1;
       
        public function TextBox()
        {
            super();
           
            // Key 입력시마다 입력내용의 바이트길이를 체크해 넘는 만큼 잘라낸다
            this.addEventListener(KeyboardEvent.KEY_UP, maxBytesHandler, false, 0, true);
            // 포커스가 벗어날 때 입력내용의 바이트길이를 체크해 넘는 만큼 잘라낸다
            this.addEventListener(FocusEvent.FOCUS_OUT, maxBytesHandler, false, 0, true);
        }
       
        override public function set editable(value:Boolean):void {
            super.editable = value;
           
            if (value == false)
                this.setStyle("backgroundColor", 0xEEEEEE);
            else
                this.setStyle("backgroundColor", 0xFFFFFF);
        }
       
        public function maxBytesHandler(event:Event):void {
            if (this.maxBytes != -1 && getByteLength(this.text) > this.maxBytes) {
                this.text = getTrimmedString(this.text, this.maxBytes);
            }
        }
       
        // 문자열의 바이트 길이를 리턴
        private function getByteLength(str:String):int {
            var byteArr:ByteArray = new ByteArray();
            byteArr.writeMultiByte(str, "euc-kr");
           
            return byteArr.length;
        }
       
        // str에서 maxLength 바이트를 넘어가는 문자들을 잘라내고 리턴
        private function getTrimmedString(str:String, maxLength:int):String {
            var tempString:String = str;
           
            for (var i:int = str.length; i > 0; i--) {
                if (getByteLength(tempString) <= maxLength)
                    return tempString;
                else {
                    tempString = tempString.substr(0, tempString.length-1);
                }
            }
           
            return "";
        }
    }
}

maxBytes 속성을 사용하기 위해서는 여기서 사용한 컴포넌트를 사용하고 속성에 maxBytes를 사용하면 된다.
다 아는 얘기겠지만, 루트 컴포넌트에는 미리 namespace를 선언해 둬야 한다.

<mx:Panel xmlns:custom="com.customcomp.*" ...>
...
<custom:TextBox id="someId" maxBytes="10" .../>

이렇게 사용하면 someID라는 TextInput에 10바이트가 넘는 문자를 입력하면 자동으로 잘라준다.

2009년 9월 23일 수요일

Flex Application에 제목 넣기

Flex Application 파일명이 index.mxml이면 이 Application을 보려면 index.html로 들어가면 된다. 그런데, 그냥 만들어 놓기만 하면 웹브라우저에 제목이 보이질 않아 여러 창을 띄웠을 때 찾기가 힘들다.
이럴 경우에는, index.mxml의 Application 태그에 pageTitle 이란 속성을 추가하고 여기에 웹브라우저에 보여줄 제목을 값으로 넣어 주면 된다.

좀더 자세히 들어가면, html-template 폴더의 index.template.html 파일의 ${title} 부분에 Application 태그의 pageTitle 속성값이 들어가서 이렇게 보이는 것이다.

ActionScript Reflection (Dynamic Instantiation)

package test
{
    public class ExampleClass
    {
        public static function get instance():Class
        {
            return ExampleClass;
        }
       
        public function getMessage():String {
            return "message";
        }
    }
}

우선 동적으로 객체를 생성할 수 있도록 위와 같이 instance라는 함수를 만들어 둔다.

private function getMessage():void {
    var instance:Object =
              flash.utils.getDefinitionByName("test.ExampleClass");

    var exam:ExampleClass = new instance() as ExampleClass;
    Alert.show(exam.getMessage());
}

이렇게 클래스명을 이용해서 객체를 생성해서, 함수를 호출하면 Alert에 "message"라는 문자열이 보이게 된다.

2009년 9월 22일 화요일

Tooltip과 Datatip의 스타일 지정

Application 전체의 폰트 지정은 아래와 같이 하면 매번 별도의 설정이 필요하지 않다.

global {
    fontFamily:"맑은 고딕";
    fontSize: 11;
}


그런데, Tooltip과 Datatip의 글자가 너무 작아서 보기가 힘든데 아무리 찾아도 글자크기를 키우는 방법을 찾기 힘들었다.
아래처럼만 해 주면 Application 전체에서 나타나는 Tooltip과 Datatip의 폰트를 설정할 수 있다.

ToolTip
{
    fontSize: 11;
}

DataTip
{
    fontSize: 11;
}

2009년 9월 18일 금요일

includeInLayout 속성


화면 컴포넌트 중에 일부를 보였다, 안 보였다 하게 제어해야 할 경우가 있다.
이것을 위해서 visible 속성에 true, false를 주면 안 보이긴 하는데 그 자리가 비어 있어서 영 보기가 안 좋다.
이럴 경우에는 includeInLayout 속성에 true, false를 주면 그 컴포넌트가 원래 코드에 포함되지 않은 것처럼 빈자리 없이 보인다.

단, 절대좌표를 가진 컴포넌트는 당연히 visible을 사용할 때와 똑같이 보일 것이고, Grid, HBox, VBox 등의 Layout Componet에 올라가 있을 경우에만 제대로 동작을 한다.
Flex 하다 보면 다들 느끼겠지만 화면크기가 모든 사용자에게 똑같으면 상관없지만 그렇지 않은 관계로 대부분의 화면 컴포넌트들을 HBox, VBox에 올려 놓고 왼쪽정렬이나 오른쪽정렬을 하는 것이 가장 편하다.

2009년 9월 3일 목요일

Java VO를 ActionScript VO로 변환하기

BlazeDS를 사용하려면 동일한 내용을 담는 Vo를 Java 버전과 ActionScript 버전의 짝을 만들어 줘야 한다.
근데 이번에 한꺼번에 대량 작업을 하려다 보니 너무 귀찮아져서 Java VO 디렉토리를 쭉 훑어서 특정 디렉토리 아래에 ActionScript VO로 변환해서 저장해 주는 초간단/초허접 프로그램을 만들어 보았다.
필요하신 분들은 용도에 맞춰 고쳐 쓰시고, 고쳐 달라고 하기는 없기~

package com.my.vo.data;

import com.my.vo.CommonVo;

// Generated 2009. 8. 24 ???? 12:54:51 by Hibernate Tools 3.2.4.GA

/**
 * Foo generated by hbm2java
 */
public class FooVo extends CommonVo{

    /**
     *
     */
    private static final long serialVersionUID = 4526158581216990458L;
   
    public FooIdVo id;
    public String barName;
    public String barType;
    public int barLength;

    ...
}
이것이 원본 Java VO


package vo.data
{
    import vo.CommonVo;
   
    [Bindable]
    [RemoteClass(alias="com.my.vo.data.FooVo")]

    public class FooVo extends CommonVo
    {       
        public var id:FooIdVo;
        public var barName:String;
        public var barDesc:String;
        public var barType:String;
        public var barLength:int;
    }
}
이것이 원하는 형태의 ActionScript VO


package flex;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;

public class ConvertJavaToAS {
    public static final String packagePrefix = "com.my.";

    public static void main(String[] args) throws Exception {
        String javaDir = "C:/workspace/com/my/vo";
        String asDir = "D:/temp/vo";
       
        File inDir = new File(javaDir);
       
        traverse(inDir, asDir);
    }

    public static void traverse(File in, String out) throws Exception {
        File[] files = in.listFiles();
        for (int i = 0; i < files.length; i++) {
            if (files[i].isDirectory())
                traverse(files[i], out);
            else if (files[i].getName().endsWith("Vo.java")){
                convert(files[i], out);
            }
        }
    }
   
    public static void convert(File java, String out) throws Exception {
        BufferedReader br = new BufferedReader(new FileReader(java));
       
        String strPackage = "";
        String strClass = "";
        StringBuilder strVar = new StringBuilder();
       
        String line = null;
        while ( (line = br.readLine()) != null ) {
            if (!strClass.equals("") && line.trim().startsWith("public " + strClass + "("))
                break;
            else if (line.trim().startsWith("package")) {
                String[] strs = line.trim().split(" ");
                strPackage = strs[1].substring(0, strs[1].length()-1);
            }
            else if (line.trim().startsWith("public class")) {
                String[] strs = line.trim().split(" ");
                strClass = strs[2];
            }
            else if (line.trim().startsWith("public")) {
                String[] strs = line.trim().split(" ");
                strVar.append("\t\tpublic var ")
                      .append(strs[2].substring(0, strs[2].length()-1)).append(":")
                      .append(mapDataType(strs[1])).append(";\n");
            }
        }
       
        br.close();
       
        save(strPackage, strClass, strVar.toString(), out);
    }
   
    public static String mapDataType(String javaType) {
        if (javaType.equals("BigDecimal")) {
            return "String";
        }
        else
            return javaType;
    }
   
    public static void save(String pack, String clazz, String vars, String out) throws Exception {
        String subdir = pack.substring(pack.lastIndexOf(".")+1);
        String outDir = out + "/" + subdir;
        File asDir = new File(outDir);
        if (!asDir.exists())
            asDir.mkdirs();
       
        String asPackage = pack.substring(packagePrefix.length());
       
        BufferedWriter bw = new BufferedWriter(new FileWriter(outDir + "/" + clazz + ".as"));
        bw.write("package " + asPackage + "\n{\n");
        bw.write("\timport vo.CommonVo\n\n");
        bw.write("\t[Bindable]\n");
        bw.write("\t[RemoteClass(alias=\"" + pack + "." + clazz + "\")]\n\n");
        bw.write("\tpublic class " + clazz + " extends CommonVo\n\t{\n");
        bw.write(vars);
        bw.write("\t}\n}");
       
        bw.flush();
        bw.close();
    }
}

이것이 변환 프로그램


사용시 수정해야 할 것들은 굵은 글씨도 표시된 부분

1. packagePrefix : Java VO와 ActionScript VO의 패키지 경로를 다르게 사용할 경우 Java VO의 package에서 잘라내야 할 앞부분. 같은 패키지 경로를 사용하면 이걸 빈 문자열로 설정하면 된다.
2. javaDir : Java VO 들이 있는 최상위 디렉토리. 이 아래에 있는 모든 Java VO를 찾아서 ActionScript VO로 변환한다.
3. asDir : 생성된 ActionScript VO들을 저장할 디렉토리. 패키지에 따라 하위디렉토리를 만들어서 저장.
4. Vo.java : 나는 일단 Java VO 파일의 postfix가 Vo.java라서 이렇게 썼고, 필요에 따라서 고쳐 쓰시길.
5. mapDataType : Java의 데이터타입을 AS이 데이터타입으로 매핑시켜 주는 메소드. 원하는만큼 추가해 주면 됨.
6. CommonVo : 나는 공통으로 상속할 상위클래스가 있어서 이렇게 썼고, 필요없으면 빼고 다르면 고쳐서 쓰시길...


띄어 쓰기 같은 건 무조건 한칸으로 가정하고 처리했으니까 Hibernate Tools로 생성한 VO를 사용하거나, 줄 잘 맞춰서 직접 정성껏 만든 VO를 사용해야 원하는 결과를 얻을 수 있다.

2009년 8월 21일 금요일

HQL UNION 제약사항

요즘 Hibernate가 필요해서 이것저것 해 보고 있다.
특히 장점은 한 번 프로그램을 작성해 놓으면 설정만으로 다른 종류의 DBMS에서도 그대로 사용이 가능하다는 거다. 그래서, 열심히 기존에 있던 오라클용 SQL을 HQL로 변경해서 테스트해 보는데 이거 제약이 좀 있다.
참고로, HQL 테스트환경은 Hibernate Tools를 Eclipse에 설치해서 Configuration 잡아 주고, Hibernate perspective에서 Hibernate Configurations 탭을 통해 HQL Editor를 띄우면 일반 DB client처럼 쿼리 결과도 볼 수 있고, HQL로부터 실제로 생성되는 SQL도 볼 수 있다.

일단, 인터넷에서 HQL에서 UNION이 지원되나 찾아 보면 안 된다는 내용이 대세다.
하지만, 내가 오라클을 써서 그런지 내가 쓰는 Hibernate 버전에서 지원되는 건지 모르겠지만 UNION만은 잘 된다. 3개를 묶어도 잘 된다. 그런데, UNION을 IN의 subquery에서 사용하면 못 한다고 에러가 난다.

[code sql] from t1 where col1 in (    select col2 as name1    from t2    where col3 = 'a'    union    select col2 as name1    from t3    where col3 = 'a' ) [/code]
처음엔 에러 메시지가 union 부분이라고 해서 union을 못 쓰는 줄 알았는데, 하다보니 in 안의 subquery에서 사용해서 그런 거였다. 결국 그래서 아래와 같이 바꿨다.

[code sql] from t1 where col1 in (   select col2 as name1   from t2
   where col3 = 'a' ) union from t1 where col1 in (   select col2 as name1   from t3   where col3 = 'a' ) [/code]
덕분에 쿼리가 왕창 길어졌다. ㅡ.ㅡ;
이렇게 쿼리 만드는 일이 거의 없나? 하긴 나는 원래 그냥 UNION도 거의 안 써 봤으니...
암튼 잘 되기만 하면 된다.

2009년 8월 20일 목요일

Flex + BlazeDS + Java 웹프로젝트용 Ant Build.xml

Eclipse + Flex Builder로 개발한 웹애플리케이션을 WAR로 export하면 Flex 소스코드까지 그대로 묶여서 들어간다.
그래서, Flex나 Java 코드는 다시 compile하지 않고 웹애플리케이션으로 deploy할 때 꼭 필요한 컴파일된 파일들과 리소스 파일들을 별도의 디렉토리에 복사해 WAR로 묶은 다음 지정된 위치로 복사해 주는 기능이다.
사실 그냥 WAR로 묶어도 별 문제는 없을 것 같지만 혹시나 소스가 공개될 수도 있고, Flex 소스가 다 들어가면 WAR 파일이 너무 커져서 만들어 봤다.

 <!-- 현재 프로젝트의 컴파일된 코드를 WAR로 묶어 webapp.dir 속성에 지정된 디렉토리에 복사 . 추가로 컴파일은 하지 않음 -->
<project name="MyWebApp" basedir="." default="deploy">

    <!-- Directory to deploy the generated WAR file -->
    <property name="webapp.dir" value="C:/apache-tomcat-5.5.26/webapps" />
   
    <property name="web.app.name" value="FLEXAPP" />
    <property name="package.name" value="${web.app.name}.war" />
    <property name="dest.dir" value="target" />
    
    <property name="flex.file.dir" value="bin-debug"/>
    <property name="web.content.dir" value="WebContent"/>

    <target name="prepare">
        <mkdir dir="${dest.dir}" />
    </target>

    <target name="package" depends="prepare">
      <echo>Packaging...</echo>
     
      <!-- copy Flex and Java Web files -->
      <copy todir="${dest.dir}" overwrite="false">
        <fileset dir="${flex.file.dir}"/>
        <fileset dir="${web.content.dir}"/>
      </copy>
        
      <!-- package war file -->
      <war destfile="${dest.dir}/${package.name}"
              webxml="${dest.dir}/WEB-INF/web.xml"
              basedir="${dest.dir}"/>
    </target>

    <!-- deploy the generated war file -->
    <target name="deploy" depends="package">
      <echo>Deploying...</echo>
      <copy file="${dest.dir}/${package.name}"
              tofile="${webapp.dir}/${package.name}"
              overwrite="true" />
      <delete file="${dest.dir}/${package.name}" />
    </target>

</project>


2009년 7월 1일 수요일

Flex I18N 메시지 컴파일용 Ant Build.xml

플렉스를 하면서 다국어 처리를 하게 되었는데 할 때마다 명령 프롬프트에서 스크립트 실행하기도 귀찮고 함께 JSP도 사용하는데 따로 관리하려니 너무 머리가 아파서, 하나의 properties를 이용해서 mxmlc를 이용해 플렉스용 메시지파일을 생성하고, JSP용 properties 파일까지 만들어 주는 Ant 빌드파일을 만들었다. 다국어 메시지 파일을 한 번 만들고 끝날 게 아니라면 유용하게 사용할 수 있다.


기능 :
현재 다국어 메시지 파일을 Flex에서 사용할 수 있도록 swf로 컴파일하고 bin.debug.dir 속성의 디렉토리에 복사한 후에, 추가로 JSP에서 사용할 수 있도록 변환하고 java.locale.dir 속성의 디렉토리에 복사

사용법 :
나는 이클립스에서 사용하는데, 플렉스 프로젝트 루트디렉토리에 이 내용으로 build.xml을 만들고, 마우스 오른쪽 클릭하고 "Run As > Ant Build" 를 선택하면 실행된다.

-- 기본적인 다국어 컴파일에 관한 내용은 '예제로 배우는 플렉스'를 참고하시길.

<project name="LocaleCompile" basedir="." default="ko_kr">

    <property name="resource.dir" value="flex_src/resource" />
    <property name="bin.debug.dir" value="bin-debug/resource" />
    <property name="java.locale.dir" value="src/locale" />
  
    <property name="en.us.locale" value="en_US" />
    <property name="en.us.file" value="${en.us.locale}_ResourceModule.swf" />
    <property name="ko.kr.locale" value="ko_KR" />
    <property name="ko.kr.file" value="${ko.kr.locale}_ResourceModule.swf" />
  
    <property name="resource.bundles" value="collections,containers,controls,core,effects,formatters,flexsong,logging,SharedResources,skins,states,styles,utils,validators" />
  
    <!-- english -->
    <target name="en_us">
        <!-- flex -->
        <exec dir="${resource.dir}" executable="mxmlc">
            <arg line="-locale=${en.us.locale} -source-path=locale/{locale} -include-resource-bundles=${resource.bundles} -output ${en.us.file}"/>
        </exec>
        <copy file="${resource.dir}/${en.us.file}" tofile="${bin.debug.dir}/${en.us.file}"/>
      
        <!-- java -->
        <exec dir="${resource.dir}/locale/${en.us.locale}" executable="native2ascii">
            <arg line="-encoding UTF-8 flexsong.properties ${basedir}/${java.locale.dir}/flexsong_${en.us.locale}.properties"/>
        </exec>
    </target>
  
    <!-- korean -->
    <target name="ko_kr" depends="en_us">
        <!-- flex -->
        <exec dir="${resource.dir}" executable="mxmlc">
          <arg line="-locale=${ko.kr.locale} -source-path=locale/{locale} -include-resource-bundles=${resource.bundles} -output ${ko.kr.file}"/>
        </exec>
        <copy file="${resource.dir}/${ko.kr.file}" tofile="${bin.debug.dir}/${ko.kr.file}"/>
      
        <!-- java (JSP의 charset이 UTF-8이므로 encoding을 동일하게 처리하기 위해 encoding 옵션에 UTF-8을 추가했다) -->
        <exec dir="${resource.dir}/locale/${ko.kr.locale}" executable="native2ascii">
          <arg line="-encoding UTF-8 flexsong.properties ${basedir}/${java.locale.dir}/flexsong_${ko.kr.locale}.properties"/>
        </exec>
    </target>

</project>

2009년 5월 20일 수요일

DataGrid Paging 구현

일반 웹처럼 DataGrid에서 아래에 페이지번호를 달아서 페이징을 하면 좀 이상할 것 같아서, 우아하게 DataGrid의 스크롤이 맨 아래에 닿으면 다음 페이지를 읽어서 DataGrid의 아래에 추가해 주는 방법을 생각해냈다. 뭐, 사실 Grid를 사용하는 대부분의 프로그램들이 이렇게 한다. 하지만, Flex의 DataGrid에는 명시적으로 스크롤이 맨 아래에 닿았을 때 발생시키는 이벤트가 없어서 좀 고민을 많이 했다.

DataGrid에 scroll="scrollHandler(event)"

이렇게 scroll 이벤트를 이벤트핸들러에 연결시킨다.


아래는 이벤트핸들러 메소드

private function scrollHandler(event:ScrollEvent):void {
    if (grid.maxVerticalScrollPosition == event.position
       && event.delta > 0) {

        // do something
        Alert.show("end of Vertical scroll");
    }
}

event.delta > 0
이 부분은 스크롤을 맨 아래에 닿은 상태에서 계속 당기면 scroll 이벤트가 계속 발생하기 때문에, 끝에 닿은 상태에서 더 이상 못 움직일 때는 이벤트 처리를 하지 않도록 하기 위해서 사용했다.

이렇게 간단하게 끝날 문제는 사실 아니다. 스크롤바를 맨 아래서 올렸다 내렸다 하면 자꾸 이벤트가 발생하니까 같은 페이지를 중복해서 요청하지 않도록 하는 코드도 들어가야 할 것 같다.

2009년 5월 19일 화요일

id를 이용한 정적/동적 컴포넌트 참조

Flex에서 ActionScript를 사용하다 보면 컴포넌트를 직접 사용할 수도 있지만 id 문자열을 이용해 참조해야 할 경우가 종종 생긴다.

<mx:TextInput id="tiName" />

mxml 파일에 이런 TextInput이 선언되었다면,

tiName.text = "tiger";
이렇게 할 수도 있지만,

this["tiName"].text = "tiger";
이렇게 id 문자열을 이용해서도 참조가 가능하다.

지금까지는 정적으로 선언된 컴포넌트를 참조하는 법을 보았는데, 이외에 동적을 컴포넌트를 생성하는 경우도 가끔은 생긴다.

var textName:TextInput = new TextInput();
textName.id = "tiName";
someComp.addChild(textName);

이렇게 동적으로 생성한 컴포넌트는 this["id"] 형태로는 찾을 수가 없다. 그래서, 별도의 저장장소에 저장해 두고 사용해야 한다.

id가 원래 현재문서에서 unique한 것이므로, id를 key로 생성된 컴포넌트를 value로 해서 Dictionary에 저장해서 사용하면 적당할 것이다.

var dynamicComps:Dictionary = new Dictionary();
dynamicComps["tiName"] = textName;

위와 같이 저장하고, 참조하고 싶을 때는 dynamicComps["tiName"] 를 사용하면 된다.

Dictionary에 대한 보다 자세한 설명은 아래 링크 참조
http://flexsong.tistory.com/8

아주 유용한 이클립스 단축키

Ctrl + 마우스왼쪽클릭
변수에 마우스 포인터를 올려 놓은 상태에선 변수 선언 위치로, 클래스명 위에서 하면 클래스 소스로 이동한다.

Alt + ←

이전 편집 위치로 이동
파일간 이동이든, 파일내 이동이든 이 단축키를 누르면 이전 편집위치로 이동한다. 계속 누르면 계속 그 이전으로 이동한다. 'Ctrl + 마우스왼쪽클릭'으로 한참 이동했다가 다시 돌아올 때 유용하다.

Alt + →
'Alt + ←'과 반대로 이후 편집 위치로 이동

F4
이클립스의 프로젝트 익스플로러에서 클래스를 선택한 후에 F4를 누르면, 그 클래스의 상위 클래스구조를 트리형태로 보여준다.

Ctrl + Space
변수명이나 클래스명 등의 일부를 입력하고 이 단축키를 누르면 입력한 문자열로 시작하는 변수명과 클래스명 리스트가 나타나 선택할 수 있고, 선택대상이 하나면 리스트 없이 자동으로 입력된다. 클래스 변수 뒤에 '.'을 찍고 이 단축키를 누르면 해당 클래스에서 사용할 수 있는 변수와 메소드 리스트가 나타나 선택할 수 있다.

Ctrl + k
특정 문자열을 선택한 후에 이 단축키를 누르면 바로 다음에 나타나는 동일한 문자열로 이동한다. 울트라에디트나 아크로에디트의 'F3'과 동일한 기능이다. 단, 이 찾기 옵션은 이전에 'Ctrl + F'에서 사용한 옵션과 동일하게 적용되므로 원하는대로 못 찾는다면 이전에 '대소문자 구분'이나 '전체 단어' 등의 옵션 등이 선택했었는지 'Ctrl + F'를 눌러 확인해 보아야 한다.

Ctrl + Shift + k
Ctrl + k와 반대로 특정 문자열이 나타나는 바로 이전 위치로 이동한다. 울트라에디트나 아크로에디트의 'Shift + F3'과 동일한 기능이다.


물론, 위의 단축키들은 Flex 코드 개발시에도 동일하게 작동한다.

2009년 5월 15일 금요일

Flex에서 Java의 HashMap과 같은 기능 사용

Flex에도 Java의 HashMap과 같이 key=value 형태로 저장하고 조회할 수 있는 클래스가 있다. 그것이 바로 Dictionary.

아래와 같이 사용할 수 있다.
var dict:Dictionary = new Dictionary(); // 이렇게 생성해서
dict[key] = value; // 이렇게 값을 저장하고
dict[key]          // 이렇게 값을 꺼낸다
 
key와 value에는 아마도 Object가 들어가는 걸로 봐서 어떤 값이든 들어가는 것 같다. HashMap에서도 그랬듯이 나는 key에는 String, value에는 String 또는 Object를 넣는 데 주로 사용할 듯하다.


혹시나 이해가 안 갈까 봐, 아래는 간단한 예제 코드
var dict:Dictionary = new Dictionary();
dict["userid"] = "tiger";
Alert.show("User ID : " + dict["userid"]);

결과는 User ID : tiger


단, HashMap처럼 keySet이나 contains 같은 메소드는 전혀 없고, 오직 넣고 꺼내기만 된다.

2009년 5월 14일 목요일

Flex에서 HTML 페이지 띄우기

Flex에서 HTML(ASP, PHP, JSP 등 포함)를 띄울 일이 종종 생긴다.
이를 위해서 IFrame이라는 Flex Component가 있는데 이걸 제대로 사용하려면 wmode 같은 걸 설정하는데 이게 아무래도 Adobe 컴포넌트가 아니라서 DataGrid 스크롤이 휠로는 안 되는 등 뭔가 깔끔하게 처리되는 맛이 떨어진다. 그래서, 아예 웹브라우저를 하나 생성하는 게 가장 간편하고 확실한 방법인 듯하다.

아래 코드는 Flex에서 웹브라우저 팝업을 띄우는 예제이다.


// 작은 사이즈의 팝업으로 뜬다
if (ExternalInterface.available)
    ExternalInterface.call("window.open", url, "_blank", "width=800,height=600");
else {
    try {
        // 새로운 탭 또는 현재 웹브라우저와 같은 브라우저로 뜬다          
        navigateToURL(new URLRequest(url));
    }
    catch (e:Error) {
        Alert.show("Cannot open Popup : " + e.message);
    }
}


첫번째(if)에서 ExternalInterface.call은 현재 Flex가 동작하고 있는 웹브라우저에 자바스크립트 명령을 실행하는 방법이다. 즉, window.open(url, "_blank", "width=800,height=600"); 라는 자바스크립트 코드가 웹브라우저에서 실행된다. 이게 첫번째 옵션인 이유는 여기는 open 메소드의 세번째 인자로 다양한 옵션을 줄 수 있고, 팝업 차단이 되어 있어도 뜬다고 하기(확인 못 해 봤음) 때문이다. 나는 사이즈 옵션을 이용해 큰 브라우저가 아닌 작은 팝업을 만들고 싶었기 때문에 이것을 가장 우선으로 처리했다.
두번째(else)에서는
navigateToURL라는 메소드를 이용해 URL을 웹브라우저에 띄운다. 그런데, 여기는 별다른 옵션을 줄 수 없어 HTML 페이지가 새로운 탭에 열리거나 현재 웹브라우저와 같은 크기의 창이 열려 현재 Flex 화면을 가리기 때문에 ExternalInterface를 사용할 수 없을 경우에 어쩔 수 없이 사용하도록 처리했다.

참고:
navigateToURL을 이용해서 동적으로 생성된 컨텐츠를 다운로드 받게 할 수도 있다. navigateToURL에서 받는 URL의 페이지(ASP, PHP, JSP, Servlet 등)의 Contents Type을 text/html이 아닌 다른 값(예를 들면, Excel은 application/vnd.ms-excel)으로 하면 브라우저 창 대신 다운로드창이 떠서 다운로드할 수 있다.

2009년 4월 23일 목요일

Flex3 유용한 사이트

http://examples.adobe.com/flex3/componentexplorer/explorer.html
- Flex3 각 컴포넌트들의 실제 작동하는 예제와 소스코드를 제공

http://livedocs.adobe.com/flex/3/langref/
- Flex3 모든 컴포넌트들의 상속관계, 속성, 스타일 등을 찾을 수 있다

http://livedocs.adobe.com/flex/3/html/index.html
- Flex3 help 페이지인데 주로 왼쪽의 검색창에서 알고 싶은 내용의 설명이나 예제를 찾는 데 사용

http://blog.flexexamples.com/
- Flex에 대해 어느 정도 감을 잡고 이것저것 응용하기 시작하면서부터 필요한 아주 다양한 예제가 있는 곳

2009년 4월 16일 목요일

Flex 3 + BlazeDS

기본적으로 Flex는 화면단만을 개발할 수 있다.
(기본제공 컴포넌트가 다양해서 일반적인 화면은 쉽게 개발할 수 있고, 기본제공 컴포넌트로 구현할 수 없는 복잡한 화면은 기본 컴포넌트를 상속해서 확장하거나 ActionScript 코딩을 통해 구현하면 된다.)
다수의 사용자가 접속하는 시스템의 경우 애플리케이션 서버에 DB 등에 접속하는 코드가 있고, 이 코드와 Flex로 구현한 화면간의 통신을 구현해야 한다.
화면단의 Flex와 서버간의 통신기능을 제공하는 유료제품으로 Adobe의 LiveCycle Data Service가 있다. 그런데, 이것을 사용해서 시스템을 구축하면 운영서버의 CPU수에 따라 라이센스 비용을 지불해야 한다. 이 비용이 꽤 비싼 것으로 알고 있다. 우리나라에선 Flex Builder도 외국에 비해 상당히 비싼데, LCDS까지 이렇게 비싸니 우리나라에서 Flex가 빨리 보급되지 못 하는 이유 중 하나라고 생각된다.
LCDS에 대한 대안으로 아이러니하게도 Adobe에서 BlazeDS라는 것을 제공한다. 이것은 오픈소스로 무료로 사용할 수 있다. LCDS는 Flex Builder에서 편리하게 개발할 수 있는 기능을 제공하지만, BlazeDS는 Flex Builder에는 기능이 제공되지 않아 약간의 수작업이 필요하다. 하지만 약간의 유틸리티를 이용하면서 구성만 잘 하면 비용대비 효과가 우수한 시스템을 구축할 수 있다.

(이하 내용은 제가 사용하면서 예측한 내용으로 사실과 다를 수 있음을 알려 드립니다)
BlazeDS는 Flex로 구현된 화면단과 J2EE로 구현된 서버단간에 HTTP로 데이터를 원활히 주고 받을 수 있도록 연결해 주는 역할을 수행한다. 서버단에서 수행되는 메커니즘을 보면 Struts하고 유사하다고 보면 된다. 화면에서 특정 이름으로 서버단의 BlazeDS 서블릿을 호출하면, 서블릿은 그 이름에 해당하는 클래스를 XML 설정파일에서 찾아 객체를 생성하고 해당 메소드에 인자를 넘겨 호출한 후 결과를 리턴한다. 여기서 중요한 역할이 화면단에서 ActionScript 형태로 만들어 보낸 인자를 그에 맞는 Java 객체로 변환해서 메소드에 전달하는 것과 해당 메소드의 처리결과(Java 객체)를 Flex 화면에서 받을 수 있도록 전달해 주는 것이다. BlazeDS의 이런 역할 덕분에 Flex 화면과 J2EE 웹컨테이너간의 통신이 가능하다.
여기서, 서블릿이 특정 이름에 대해 호출하는 클래스는 특별한 제약이 없이 POJO면 가능하다. 즉, 여기부턴 자유롭게 구성이 가능하므로 예를 들면 Spring으로 이후를 구성할 수도 있다는 뜻이다. 아니면 Struts2의 Action처럼 클래스를 구현하고 DB처리는 Hibernate나 iBatis로 구현해도 된다.

대형 시스템을 구성한다면 BlazeDS를 이용해서 서버단 라이센스 비용을 대폭 줄일 수 있겠고, 대신 Flex Builder는 개발자수만큼 사야 될 것이다. Flex Builder는 기본과 Pro 버전이 있는데 기본이 1명당 40만원 정도였던 것 같고, Pro는 백만원이 넘었던 것으로 기억한다.(환율따라 자주 변함) Pro는 Advanced DataGrid라고 DataGrid에 트리형태의 표현 등 기능이 추가된 것과 차트 기능이 추가된다. 본인은 유료 Pro 버전을 쓰고 있는데 Advanced DataGrid는 별로 쓸 일이 없었고, 차트 기능은 편리해서 잘 사용하고 있다. 개발자가 많다면 대부분은 기본버전을 구매하고, 한두명만 Pro버전을 구매해서 이 사람들에게 차트개발을 몰아주는 것도 방법이 될 수 있을 것 같다. 솔직히 Flex 써 보니 사용하기도 괜찮고 모양도 예쁘게 만들어져서 좋긴 한데 우리나라에선 너무 비싸게 파는 것 같다.

참고로 Flex Builder는 3.0 말고 3.2 이상을 쓰는 것이 좋다. 맨 처음 3.0 썼는데 소스 하나 고치고 저장할 때마다 빌드시간이 너무 오래 걸려서 짜증났는데, 3.2 쓰면서 빌드 시간이 거의 5초 이내로 줄어서 쓸만해졌다.
참고 또 하나, Flex와 BlazeDS로 현재 개발환경을 구성한다면 Eclipse 3.4 (Ganymede)에 Flex Builder 3.2 Eclipse Plugin 버전을 사용하면 된다. Eclipse 3.3에서 3.4로 버전업 되면서 다른 건 몰라도 Search 결과는 참 보기 좋게 바뀌었다. 그래서, 3.4 강추!
-> 얘길 들어보니 Flex Builder 3.2를 써도 Eclipse 3.3을 쓰면 빌드 시간이 여전히 길다고 하니 반드시 Flex Builder 3.2에 Eclipse 3.4의 조합이 최적인 것 같다.

이와 관련된 링크나 설치방법, 샘플 등은 본인이 워낙 게으른 관계로 제공하지 못 하고, 그냥 열심히 해 보시라는 말 밖에 못 드리는 점 양해 바랍니다.

2009년 4월 13일 월요일

로딩속도 향상

ViewStack이나 TabNavigator 등 다른 컴포넌트들을 포함하면서
필요에 따라 일부는 보여주고, 나머지는 숨기는 레이아웃 컴포넌트들이 있다.
이런 것들은 creationPolicy라는 속성이 있는데,
이 속성값을 all로 하면 ViewStack이나 TabNavigator를 생성할 때 포함된 컴포넌트까지 모두 생성하고(포함된 컴포넌트가 많을수록 로딩속도가 느려짐),
auto로 하면 포함된 컴포넌트를 보여달라는 요청이 올 때에 컴포넌트를 생성한다.
(ViewStack은 해당 화면을 보여 줄 때, TabNavigator는 해당 탭을 선택했을 때)

그러므로, Application 안의 ViewStack에 업무화면들을 잔뜩 쌓아 놓고 메뉴에 따라 보여주는 형태라면 이 ViewStack의 creationPolicy를 auto로 하는 것이 로딩속도 향상에 좋다.
책을 보면 로딩속도 향상을 위해 작은 단위로 module을 만들어 놓고 필요할 때마다 moduleLoader 같은 걸로 띄우라고 하는데 귀찮아서 이것저것 보던 중에 creationPolicy라는 좋은 속성을 발견해서 쉽게 해결했다.
단, ViewStack에 creationPolicy를 설정하면 그 안에 포함된 화면 안에 있는 ViewStack이나 TabNavigator들에 creationPolicy 설정하지 않으면 상위 컴포넌트의 creationPolicy를 따라간다.

경험상 Application 안의 ViewStack 외에 그 안에서 사용하는 ViewStack이나 TabNavigator는 creationPolicy를 all로 하는 게 좋다. auto로 하면 안 보이는 탭이나 화면의 컴포넌트들이 null 상태인데, 이 때 안 보이는 컴포넌트들을 참조하려고 하면 맨 처음 한 번은 null 오류가 발생하기 때문이다.

FormItem 안의 내용을 옆으로 배치

일반적으로 입력/수정 폼 형태에는 Form 컴포넌트를 사용하는 게 좋다.
그런데, 한 내용에 가로로 2개의 입력이 필요할 경우에는 FormItem의 direction 속성을 horizontal로 설정하면 된다. 이 속성을 사용하지 않으면 세로로 위치한다.

<mx:FormItem label="비밀번호" labelWidth="200" direction="horizontal">
  <mx:TextInput id="pw1" width="200"/>
  <mx:TextInput id="pw2" width="200"/>
</mx:FormItem>

Label, TextInput에 손 모양 커서 적용

Label과 TextInput에 click 이벤트 처리하면 스크립트를 실행시킬 수는 있다.
그런데, click 이벤트 처리한다고 여기에 마우스를 올렸을 때 자동으로 마우스 커서가 손모양으로 바뀌진 않는다.
아래의 3개 속성을 설정해 주어야 된다.

useHandCursor="true"
buttonMode="true"
mouseChildren="false"

물론 다른 컴포넌트도 적용할 수 있을 것 같긴 한데 내가 적용해 본 것은 여기까지.
내 생각엔 useHandCursor 하나만으로 적용되어야 할 것 같은데 3개나 해 줘야 하다니...

2009년 4월 12일 일요일

문자열 클립보드에 저장

문자열 클립보드에 저장

System.setClipboard(dataString);

이 스크립트가 실행된 뒤엔 Ctrl+V하면 아마 이 내용이 붙겠지.

2009년 3월 2일 월요일

다국어 코드 적용하기


대단한 프로그램은 아니고,
이번에 프로그램을 한참 개발하고 다국어 버전으로 변경할 일이 있었는데
30여개의 소스 파일을 일일이 뒤지면서 한글로 만들어 놓은 코드를 다국어 처리코드로 바꾸는 게 여간 힘든 작업이 아니었다.
(눈과 팔이 너무 아팠다. ^^;)
만만하게 보고 그냥 무식하게 하려다가, 결국 반 정도를 한 후에 프로그램을 만들었다. ^^;

아래에 있는 소스가 자바로 만든 변환 프로그램.

[code java]
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;

public class I18NReplace {
    private static final String I18N_CODE_PREFIX = "{resourceManager.getString('rb','";
    private static final String I18N_CODE_POSTFIX = "')}";

    /**
     * @param args
     */
    public static void main(String[] args) throws Exception {
        HashMap map = new HashMap();
        BufferedReader br = null;
        BufferedReader brSrc = null;

        try {
            // 다국어 코드, 값이 정의된 리소스번들 파일
            br = new BufferedReader(new InputStreamReader(
                                    new FileInputStream("code.properties"), "UTF-8"));
            // 다국어 버전으로 변환할 소스 파일
            brSrc = new BufferedReader(new InputStreamReader(
                                    new FileInputStream("test.mxml"), "UTF-8"));
           
            String line = null;
            // 다국어 코드, 값을 HashMap에 저장
            while ( (line = br.readLine()) != null ) {
                String[] arr = line.split("=");
                if (arr.length == 2) {
                    map.put(arr[0], arr[1]);
                }
            }
           
            Object[] keys  = map.keySet().toArray();
            // 길이가 긴 문자열부터 변환하기 위해 문자열 길이 내림차순으로 키를 정렬
            Arrays.sort(keys, new KeyOrderComparator());
           
            String srcLine = null;
            while ( (srcLine = brSrc.readLine()) != null ) {
                // 주석 라인은 skip
                if (!srcLine.trim().startsWith("//") &&
                    !srcLine.trim().startsWith("/*") &&
                    !srcLine.trim().startsWith("<!--")) {
                    for (int i = 0; i < keys.length; i++) {
                        if (srcLine.indexOf((String)keys[i]) > -1) {
                            srcLine = srcLine.replaceAll((String)keys[i],
                                    I18N_CODE_PREFIX + (String)map.get(keys[i]) + I18N_CODE_POSTFIX);
                        }
                    }
                }
               
                System.out.println(srcLine);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (br != null)
                    br.close();
            } catch (Exception e) {}
           
            try {
                if (brSrc != null)
                    brSrc.close();
            } catch (Exception e) {}
        }
    }
}

class KeyOrderComparator implements Comparator {
    /**
     * 문자열의 길이에 따라 내림차순 정렬
     */
    public int compare(Object key1, Object key2) {
        return ((String)key2).length() - ((String)key1).length();
    }
}
[/code]
I18NReplace.java

[code]
데이터=data
[/code]
code.properties

[code xml]
<?xml version="1.0" encoding="utf-8"?>
<mx:TitleWindow xmlns:mx="http://www.adobe.com/2006/mxml" title="데이터">
...
</mx:TitleWindow>
[/code]
test.mxml


사용법이 복잡하진 않지만, 급하게 대강 만든 프로그램이라 GUI 등의 친절한 기능은 없다.

1. code.properties는 코드와 값을 넣은 건데 일반적인 순서와는 반대다.
이유는, 다국어 적용하기 전에 '데이터'라고 코딩을 했는데 이를 다국어 처리하기 위해서는
한글용 리소스번들 파일에 'data=데이터' 라는 내용이 있어야 한다.
그리고, '데이터' 대신에 다국어 처리 코드( Flex를 예로 들면, {resourceManager.getString('rb','data')} )를 넣으면
언어 설정에 따라 한국어일 때는 '데이터', 영어일 때는 'Data'라는 내용이 보이게 된다.
그런데, 지금 할 일이 '데이터'라고 하드코딩되어 있는 부분을 {resourceManager.getString('rb','data')}로 바꾸는 것이니까
code.properties 파일에는 '데이터=data' 즉, message=key 형태로 되어 있어야 한다.

2. test.mxml은 '데이터'라는 내용이 하드코딩되어 있는 소스 파일

3. I18N_CODE_PREFIX는 다국어처리코드에서 key 앞에 붙는 코드, I18N_CODE_POSTFIX는 key 뒤에 붙는 코드

참고로, 중간에 Arrays.sort 는 문자열길이에 따라 내림차순으로 정렬하는 건데,
그래야 '데이터 관리'라는 문자열이 소스에서 나왔을 때, '데이터 관리'라는 게 code.properties에 있을 때 우선적으로 처리하고
이게 없으면 '데이터'나 '관리'를 따로 변환하게 되어 원치 않는 결과가 나오게 된다.
이렇게 '데이터 관리'를 하나의 단어로 정의하는 것은 언어에 따라 어순이 바뀌거나,
두 단어 이상이 합쳐져서 만들어진 단어의 경우 언어에 따라 다른 의미가 될 수 있어서이다.

또, 참고로 파일을 읽을 때 "UTF-8"라는 설정이 있는데 이것은 "UTF-8" 인코딩으로 저장한 파일을 읽기 위해 사용한 것이다.
소스 파일이나 리소스번들 파일의 인코딩 형태에 따라 빼거나 고쳐서 사용하면 된다.


이렇게 설정한 상태에서 이 I18NReplace을 실행하면 치환된 결과가 표준출력으로 나오고,
이 결과를 원래 소스에 붙인 다음에 잘못 된 부분을 수정해서 저장하면 다국어 처리가 끝난다.

꼭 다국어 처리 변환에만 사용할 필요는 없고, 어떤 규칙에 따라 문자열을 치환해야 하는 경우 사용하면 되겠다.
당연한 얘기지만, code.properties 파일 정의시 띄어쓰기는 정확히 맞춰 주어야 한다.

2009년 2월 13일 금요일

Java 한글 Project


자바에서의 한글처리에 대해 대강 조금만 알거나
잘 모르고 지나쳤었는데 나름 상세한 정보가 있는 페이지

http://java.yb2u.com/korean/index.jsp


2009년 2월 12일 목요일

오라클에서 SYSDATE를 이용한 시간 연산

[code sql] SELECT TO_CHAR(SYSDATE, 'YYYY/MM/DD HH:MI SS') "NOW", TO_CHAR(SYSDATE+1, 'YYYY/MM/DD HH:MI SS') "1 DAY AFTER", TO_CHAR(SYSDATE+1/24, 'YYYY/MM/DD HH:MI SS') "1 HOUR AFTER", TO_CHAR(SYSDATE+1/24/60, 'YYYY/MM/DD HH:MI SS') "1 MINUTE AFTER", TO_CHAR(SYSDATE+1/24/60/60, 'YYYY/MM/DD HH:MI SS') "1 SECOND AFTER" FROM DUAL [/code]
당연히,
SYSDATE에 숫자를 더하면 현재시간 이후를 의미하고,
SYSDATE에서 숫자를 빼면 현재시간 이전을 의미

이런 쿼리를 사용하면 오라클이 아닌 다른 DB를 사용하게 되면
프로그램 고치는 일이 엄청나게 커지니까 오라클만 쓸 생각일 경우에만 사용해야 할 듯
시스템 운영자와 사이가 안 좋을 땐 이런 지뢰를 심어 놓는 방법이... ㅋㅋ