메인 컨텐츠로 가기

developerWorks 이용 약관에 동의하시는 경우 제출을 클릭하십시오. 이용 약관 보기.

developerWorks에 처음 로그인하면 developerWorks프로파일이 생성됩니다.귀하의 프로파일에서 동의하신 내용이 공개되지만 이 사항은 언제든지 변경 가능합니다. 귀하의 성명(숨김으로 체크되어 있어도 표시됩니다)과 디스플레이 이름은 게시한 컨텐츠나 사이트 엑세스시 표시됩니다.

모든 정보가 안전하게 전송되었습니다.

  • 닫기 [x]

처음 developerWorks에 로그인할 때 프로파일이 작성되므로, 이를 위해 디스플레이 이름을 선택해야 합니다. 선택하신 디스플레이 이름은 developerWorks에 게시한 컨텐츠에 표시됩니다.

3글자 이상 31글자 이하의 길이로 사용 가능합니다. dW커뮤니티 내에서는 보안상 이메일주소를 제외한 다른 이름을 지정하셔야 합니다.

developerWorks 이용 약관에 동의하시는 경우 제출을 클릭하십시오. 이용 약관 보기.

모든 정보가 안전하게 전송되었습니다.

  • 닫기 [x]

XForms를 사용하여 회계 도구 만들기, Part 6: 요약하기 (한글)

Nicholas Chase, 자유기고가, Backstop Media
Nicholas Chase는 루슨트 테크놀러지스, 썬 마이크로시스템즈, 오라클 및 탬파베이 버커니어스와 같은 회사의 웹 사이트 개발에 참여해왔다. Nick은 고등학교 물리 교사를 비롯, 저준위(low-level) 핵 폐기물 시설 매니저, 온라인 과학소설 잡지 편집자, 멀티미디어 엔지니어, 오라클 강사, 그리고 한 회사의 CTO 등을 맡기도 했다. 그는 XML Primer Plus(Sams)를 비롯한, 다양한 책의 저자이기도 하다.

요약:  6부로 구성된 본 연재에서는 X-Trapolate라는 온라인 회계 도구를 만들 때 MySQL, PHP와 함께 XForms의 기능을 활용하는 방법에 대해 다룹니다. 뛰어난 프로그래밍 기술이라도 문제를 풀 수 있는 능력을 넘어서는 다양한 문제를 안고 있습니다. 이번 연재에서는 실시간 계산과 뛰어난 인터랙션 등 XForms를 통해 효과적으로 해결할 수 있는 몇 가지 문제를 집중적으로 다룹니다. Part 6에서는 지금까지의 내용을 총 정리하고 애플리케이션 결과물에 결함은 없는지 앞으로의 가능성은 무엇인지 살펴보겠습니다.

이 연재 자세히 보기

원문 게재일:  2007 년 10 월 23 일
난이도:  중급 영어로:  보기
페이지뷰:  2392 회
의견:  


개요

본 연재에 대해 간략히 정리하자면

  • Part 1은 연재의 전반적인 내용을 요약해 보여주고, 각 연재에서 XForms 명세의 어떤 부분을 다루는지를 설명한다.
  • Part 2는 로그인과 계정 관리를 다룬다.
  • Part 3은 자산 관리와 관련된 폼 개발을 다룬다.
  • Part 4는 자산 관리 개발과 비즈니스의 다양한 회계 보고에 대해 연이어 다룬다.
  • Part 5는 부채 관리 및 관련 기능 개선에 대해 더 다룬다.
  • Part 6는 연재의 마지막으로, 개발된 도구들을 요약하고 도구들을 향상시킬 방법 등을 제안한다.

이 글은 스토리지 및 레퍼런스용 데이터베이스로 MySQL을, 그리고 PHP 백엔드와 XForms를 사용하여 애플리케이션을 만드는 데 관련된 실제적인 몇 가지 문제점에 초점을 둔다. 이 글을 이해하려면 XForms 및 PHP에 익숙해야 한다(익숙하지 않다면 시작하기 전에 참고자료를 통해 확인하기 바란다).

지금까지 배운 것

지금까지 다뤄온 많은 부분을 먼저 점검해 보자. 다른 실제 프로젝트와 마찬가지로 X-Trapolate 애플리케이션 역시 시작 단계에서 그렸던 것과는 약간 다른 결과가 나왔다. 다행히 처음 의도했던 것과 같은 애플리케이션을 가지게 되었지만 여전히 문제점은 남아있다. 그 문제점들을 점검해보자.

Part 1에서는 애플리케이션의 전반적인 계획과 각 부에서 XForms의 어느 부분을 다루는지 설명했다. 전체 연재의 소개 부분이니 연재를 계속 읽어오지 않았다면 참조 바란다.

Part 2는 사용자 인증 문제를 다룬다. 여기서 새 계정을 만들 때 필요한 기본 로그인 폼과 등록 폼을 만든다. 시작은 로그인 폼으로 충분했지만 새 기능이 등장하면서 필요 없어졌다. 하지만 새 기능은 사용자의 부서를 고려하지도, 등록 프로세스가 모든 사용자 데이터를 받아들이지도 않았다. 이는 자산 관리에 결정적인 부분이다. 가장 중요한 것은 튜토리얼이 이어지는 내용을 보호하지 못하는, 완전히 고립된 폼을 다룬다는 것이다. 본 Part 6에서는 인증 시스템 수정을 포함하여 이 모든 문제들을 해결하는 방법을 다룰 것이다. 이를 통해 성공적으로 로그인한 사용자만이 폼을 볼 수 있도록 할 것이다.

Part 3에서는 회사의 재정 부분을 다루기 시작한다. 청구 폼에서 시작하여 미 청구 계정을 보여주는 폼을 만든다. 이 폼으로 지불을 승인하고, 계산서를 보내고, 계정을 징수회사에 넘기는 업무 등을 할 수 있다. 또한 업무에 따라 맞춤화된 서신을 쉽게 프린트할 수 있다. 하지만 회사가 지불해야 할 송장(invoice)은 만들 수 없다. 이 문제는 다른 애플리케이션과의 통합 또는 이 기능을 현 애플리케이션에 추가함으로써 해결할 수 있다(송장을 점검하는 것은 Part 4에서 다룬다).

Part 3는 또한 예산 폼을 만드는 것을 상세히 설명한다. 예산 폼을 이용해 부서 사용자들은 자신의 부서와 하위 부서의 예상 이윤과 손실을 볼 수 있고 이것들을 실제 숫자와 쉽게 비교할 수 있다. 이 폼을 이용하면 또한 새 부서 구조를 애플리케이션에 추가할 수 있는데, 이는 XForms의 관점에서 보자면 굉장한 일이지만 결국 다른 계정 관리 분야에서 다뤄질 것이다.

Part 4에서는 이 애플리케이션의 가장 중요한 부분을 다룬다. 여기서는 기존 송장과 그 내용을 확인할 수 있는 폼을 설명하지만 최종적으로는 외부의 청구 애플리케이션과 통합해야 한다. 송장을 만들 수 없기 때문이다.

이 문제는 또한 사용자가 장비에 관련된 문제를 보고하는 방법과 조달 사용자들이 어떤 문제가 해결돼야 하는지 볼 수 있는 방법을 제공하는, Part 4에서 만든 자산 관리 폼에서도 골치거리가 된다. 자산 관리 폼은 새 자산을 만드는 기능을 제공하지 않는다.

Part 5에서는 폼 두 개를 만든다. 하나는 채무 또는 회사가 지불해야 하는 항목뿐 아니라 새 항목을 만들 수 있는 채무 폼이다. 이 폼은 XPath 조작을 사용하여 훌륭한 기능을 수행한다. Part 5는 또한 XForms 데이터를 사용하여 간단한 막대 그래프를 만드는 방법을 보여준다. 꼭 필요한 것은 아니지만 SVG(Scalable Vector Graphics) 같은 기술을 사용하여 이 폼을 확장한다면 좀 더 멋진 모습으로 만들 수 있다.

송장이나 자산처럼 새 데이터를 추가하는 기능은 본 연재에서 이미 다룬 기술로 쉽게 해결할 수 있고 등록 폼에서 생략된 부분을 고치면 곧 제대로 된 모습을 볼 수 있을 것이다. 이 기술은 사실 데이터베이스에 추가해야 할 때 어느 "것"에나 사용할 수 있다.

하지만 여전히 인증 문제가 남아있다. 이 글은 또한 로그인과 인증 시스템을 마무리 짓는 프로세스를 다룰 것이다.


등록 섹션 고치기

등록 폼은 여러 상황에 적용되기 때문에 일단 이를 고치는 것으로 시작하자. 그렇게 되면 이 폼은 사용자의 성(姓)과 이름, 아이디, 비밀번호뿐 아니라 필요한 모든 데이터를 가진다.

애플리케이션의 완전한 코드는 소스를 다운로드할 수 있으므로 폼의 원래 버전에서 바뀐 것만 표시하겠다.

일단 빠진 데이터를 등록 폼 자체에 추가한다. Listing 1을 보자.


Listing 1. register.xhtml에 추가한 것
                
...
    <xforms:model id="acctToolRegModel" >
      <xforms:instance id="acctToolRegInst" >
         <registerForm xmlns="">
            <password/>
            <username/>
            <unameopt1/>
            <unameopt2/>
            <unameopt3/>
            <prefix/>
            <firstName/>
            <lastName/>
            <street />
            <city />
            <state />
            <zip />
            <phone />
            <deptId />
            <company />
            <submitRegistration/>
         </registerForm>
      </xforms:instance>
...
        <xforms:secret class="required" bind="passwordbind" >
           <xforms:label>Password: </xforms:label>
        </xforms:secret><br/>

        <xforms:input ref="street" >
           <xforms:label>Street: </xforms:label>
        </xforms:input><br/>

        <xforms:input ref="city" >
           <xforms:label>City: </xforms:label>
        </xforms:input><br/>

        <xforms:input ref="state" >
           <xforms:label>State: </xforms:label>
        </xforms:input><br/>

        <xforms:input ref="zip" >
           <xforms:label>Zip: </xforms:label>
        </xforms:input><br/>

        <xforms:input ref="phone" >
           <xforms:label>Phone: </xforms:label>
        </xforms:input><br/>

        <xforms:select1 ref="deptId">
           <xforms:label>Department: </xforms:label>
            <xforms:item>
               <xforms:label>Procurement</xforms:label>
               <xforms:value>123</xforms:value>
            </xforms:item>
            <xforms:item>
               <xforms:label>Human Resources</xforms:label>
               <xforms:value>129</xforms:value>
            </xforms:item>
            <xforms:item>
               <xforms:label>Marketing</xforms:label>
               <xforms:value>333</xforms:value>
            </xforms:item>
        </xforms:select1><br/>

        <xforms:input ref="company" >
           <xforms:label>Company: </xforms:label>
        </xforms:input><br/>

        <xforms:submit submission="submit_registration" 
                   bind="submitRegistrationbind">
           <xforms:label>Register</xforms:label>
        </xforms:submit><br/>
        <hr/>
     </p>
  </body>
</html>

Listing 1의 거의 대부분은 인스턴스와 폼에 간단히 값을 추가하는 것일 뿐이다. 부서 정보는 풀다운 메뉴(pulldown menu) 형식으로 추가되었기에 기존 부서만 추가될 수 있다는 것을 주목하자. 여기서는 정적인 엔트리로 나타나지만 실제로는 PHP 스크립트와 함께 인스턴스를 파퓰레이트함으로써 데이터베이스에서 이를 가져오고자 할 것이다(예는 적절한 메뉴 항목 제공하기 부분을 참조하자).

결과는 그림 1과 같다.


그림 1. 새 등록 폼
새 등록 폼

결과는 역시 추가 데이터가 더해져야 하는 PHP 스크립트로 작동한다(Listing 2).


Listing 2. register.php에 추가하는 것
                
<?php

   if (!isset($HTTP_RAW_POST_DATA)) $HTTP_RAW_POST_DATA = 
                                file_get_contents("php://input");
   $xml = $HTTP_RAW_POST_DATA;
   $doc = new DomDocument('1.0');
   $doc->loadXML($xml);

   $sqldb = mysql_connect('localhost', 'root');
   if (!$sqldb) 
       die('Could not connect to MySQL server at localhost because: '
                                                    . mysql_error());

   $sqlseldb = mysql_select_db('acct1', $sqldb);
   if (!$sqlseldb) 
      die ('Can\'t select the acct1 database on MySQL server @ ' . 
                         'localhost because: ' . mysql_error());

   // first check to see if that username is already taken
   $uname = $doc->getElementsByTagName("username")->item(0)->nodeValue;
   $sqlQuery = sprintf('SELECT * FROM accounts WHERE username=\'%s\'', 
                                 mysql_real_escape_string($uname));

   $queryData = mysql_query($sqlQuery, $sqldb);
   if (!$queryData)
      die ('Could not query the accounts table in the acct db because:'   
                                            . mysql_error());

   if(mysql_num_rows($queryData) > 0)
   {
      mysql_close($sqldb);
      $doc->getElementsByTagName("message")->item(0)->nodeValue = 
         'The username ' . $uname . 
                           ' is already taken.  Choose another.';
      echo $doc->saveXML();
      exit;
   }
   else
   {
      $pass =  mysql_real_escape_string(
         $doc->getElementsByTagName("password")->item(0)->nodeValue);
      $fname =  mysql_real_escape_string(
        $doc->getElementsByTagName("firstName")->item(0)->nodeValue);
      $prefix =  mysql_real_escape_string(
        $doc->getElementsByTagName("prefix")->item(0)->nodeValue);
      $lname =  mysql_real_escape_string(
        $doc->getElementsByTagName("lastName")->item(0)->nodeValue);
      $street =  mysql_real_escape_string(
        $doc->getElementsByTagName("street")->item(0)->nodeValue);
      $city =  mysql_real_escape_string(
        $doc->getElementsByTagName("city")->item(0)->nodeValue);
      $state =  mysql_real_escape_string(
        $doc->getElementsByTagName("state")->item(0)->nodeValue);
      $zip =  mysql_real_escape_string(
        $doc->getElementsByTagName("zip")->item(0)->nodeValue);
      $phone =  mysql_real_escape_string(
        $doc->getElementsByTagName("phone")->item(0)->nodeValue);
      $deptId =  mysql_real_escape_string(
        $doc->getElementsByTagName("deptId")->item(0)->nodeValue);
      $company =  mysql_real_escape_string(
        $doc->getElementsByTagName("company")->item(0)->nodeValue);

      $sqlQuery = "INSERT INTO accounts ( username, password, " . 
                   "firstName, lastName, prefix, street, city, ".
                   "state, zip, phone, deptId, company) VALUES ( ".
        sprintf("'%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', ".
                           "'%s', '%s', '%s', '%s');", 
                $uname, $pass, $fname, $lname, $prefix, $street, $city, 
                $state, $zip, $phone, $deptId, $company);
      $queryData = mysql_query($sqlQuery, $sqldb);
      if (!$queryData) 
        die ('Could not insert token into accounts table on MySQL ".
                             "server because:  ' . mysql_error());
      mysql_close($sqldb);
      $host= $_SERVER['HTTP_HOST'];
      $reldir= rtrim(dirname($_SERVER['PHP_SELF']), '/\\');
      header("Location: http://$host$reldir/login.xhtml");
      exit;
   }
?>

변화된 부분이 두루 있으므로 여기에 전체 스크립트를 넣었다. 전체 스크립트는 다른 추가한 것의 예 역할을 하므로 애플리케이션에 이를 반영해야 한다. 이제 데이터베이스에 연결하여 제출된 인스턴스를 로드하자. 여기서부터 먼저 신청한 아이디가 이전에 채택됐는지 여부를 확인해야 한다. 단일 인용부호, 세미콜론 등이 빠져 SQL이 망가지는 것을 보호할 수 있도록 mysql_real_escape_string() 함수를 사용하자. 사용자가 제출한 데이터를 사용하여 SQL 구문을 만들기 전에 항상 데이터가 깨끗한지 확인해야 한다.

신청한 아이디가 이미 채택됐다면 XForms 폼에 메시지를 보내야 할 것이다. 이는 메시지를 XML 인스턴스에 추가함으로써 반환되도록 한다. 이 때 일반적인 텍스트 오류 메시지는 폼에 나타나지 않는다.

아이디가 사용 가능하다면 기존 데이터를 추출하여 깨끗이 해야 함을 기억하자. 그리고 나면 insert 구문에서 이를 사용하여 직접 데이터를 데이터베이스에 추가할 수 있다. 완성되면 데이터베이스를 닫고 HTTP 헤더를 사용하여 브라우저에 로그인 페이지를 띄운다.

이 기법은 새 자산, 송장 등을 추가하는 데 사용할 수 있다. 이제 로그인 프로세스 자체로 이동하자.


로그인 프로세스 업그레이드하기

일반적인 애플리케이션에서는 사용자를 인증하는 페이지만 보이고 이후 인증 여부에 따라 추가 페이지들이 보인다. PHP로 만들어진 페이지의 경우 세션 변수나 다른 지속적 지표를 확인하면 되므로 이 프로세스는 쉽다. 하지만 XHTML과 같은 정적 폼인 경우 프로세스가 약간 더 복잡해진다.

이 때 초기 데이터 인스턴스를 동적으로 로드하고 그 인스턴스에 포함된 데이터 구조 상의 페이지 내용을 기반으로 하는 것이 하나의 옵션이 될 수 있다. 예를 들어, 인스턴스에 loggedIn 요소가 들어 있을 때 데이터는 모두 관련이 있어야 한다(이 예는 곧 설명하겠다). 하지만 이 옵션을 택한다면 여태까지 한 모든 작업을 완전히 바꿔야 한다.

두 번째 옵션은 PHP "래퍼"를 만드는 것으로, 이는 사용자가 로그인되었을 때만 정적 XHTML을 보여준다. 이 옵션의 장점은 기존 페이지에 약간의 수정을 가하거나 아예 수정을 하지 않아도 해결된다는 것이다. 그러므로 이를 택하겠다.

먼저, 이 프로세스를 쉽게 하기 위해서는 로그인 페이지 자체로 필요한 모든 것을 할 수 있는지를 확인해야 한다. 원래는 간단히 아이디와 비밀번호만 확인한 후 loginToken을 만들었다. 이제 다음으로 넘어가자.

다행히 필요한 모든 기능(사용자의 부서 ID를 이 세션에 추가하는 것 등)은 login_logout.php 스크립트에 이미 만들었다. 그러므로 첫 단계로 로그인 폼을 이 스크립트에 넣는다. Listing 3을 보자.


Listing 3. login.xhtml 고치기
                
...

      <xforms:submission id="submit_login" 
                         action="login_logout.php" method="post"  
                         instance="acctToolLoginInst" 
                         replace="instance" 
                         ref="instance('acctToolLoginInst')" >
      </xforms:submission>
    </xforms:model>
  </head>
  <body>
     <p>
       
...
            <xforms:output class="errormessage" 
                  ref="instance('acctToolLoginInst')/message" />

            <xforms:trigger
                     model="acctToolLoginModel" 
                     ref="instance('acctToolLoginInst')/loggedIn">
               <xforms:label>Menu</xforms:label>
               <xforms:load ev:event="DOMActivate" 
                        resource="appManager.php?module=appMenu"/>
            </xforms:trigger><br/>

     </p>
  </body>
</html>

첫 변경 작업은 매우 명확하다. 이 폼을 강화된 PHP 로그인 스크립트에 넣는 것이다. 하지만 사용자가 성공적으로 로그인을 했다면 어떻게 되는가? 이 기능은 Part 2에서 남겨두었던 문제다. 이제 해결해보자.

사용자가 성공적으로 로그인했다면 응답은 새 loggedIn 요소를 포함할 것이고 -- 곧 다루겠다 -- 그렇게 되면 Menu 트리거가 나타난다. 이 트리거는 사용자에게 적합한 선택만을 포함하는 새 페이지를 로드한다. 이 기능은 페이지 보호하기적절한 메뉴 제공하기에서 만들겠지만, 그 전에 login_logout.php 스크립트에 몇 가지 변경작업을 했다.

login_logout.php 스크립트는 "범용" 스크립트로 만들어졌고 사용자가 각 페이지 별로 로그인과 로그아웃을 할 수 있게끔 설계됐다. 이제 사용자가 한 곳에서 로그인하는 데 이 스크립트가 쓰이므로 단순화할 수 있다. Listing 4을 보자.


Listing 4. login_logout.php
                
<?php

session_start();

unset($_SESSION['loginToken']);
unset($_SESSION['username']);
unset($_SESSION['at_deptId']);
unset($_SESSION['acctId']);

if (!isset($HTTP_RAW_POST_DATA)) 
    $HTTP_RAW_POST_DATA = file_get_contents("php://input");
$xml = $HTTP_RAW_POST_DATA;
$doc = new DomDocument('1.0');

if ($xml != "") {               
   $doc->loadXML($xml);
} else {
   header("Location: login.xhtml");
}

if ($doc->GetElementsByTagName('loginLogout')->item(0)->nodeValue 
                                                      == 'Logout'){

   $doc->GetElementsByTagName('deptId')->item(0)->nodeValue = "";
   $doc->GetElementsByTagName('loginLogout')->item(0)->nodeValue = 
                                                           "Login";
   $doc->GetElementsByTagName("username")->item(0)->nodeValue = "";
   $doc->GetElementsByTagName("loginToken")->item(0)->nodeValue = "";
   $doc->GetElementsByTagName('login')->item(0)->nodeValue=1;

   echo $doc->saveXML();
   exit;
}

$pass = $doc->getElementsByTagName("password")->item(0)->nodeValue;
$uname = $doc->getElementsByTagName("username")->item(0)->nodeValue;

if ($uname == '' && $pass == ''){
   echo $doc->saveXML();
   exit;
}

$sqldb = mysql_connect('localhost', 'root');
if (!$sqldb) 
   die('Could not connect to MySQL server at localhost because: ' . 
                                                     mysql_error());

$sqlseldb = mysql_select_db('acct1', $sqldb);
if (!$sqlseldb) 
      die ('Can\'t select the acct database on MySQL server @ ".
                            "localhost because: ' . mysql_error());

$sqlQuery = sprintf(
  "SELECT * FROM accounts WHERE username='%s' and password='%s'", 
  mysql_real_escape_string($uname), mysql_real_escape_string($pass));
$queryData = mysql_query($sqlQuery, $sqldb);

if (!$queryData) die ('Could not insert token into tokentable on ".
                     "MySQL server because:  ' . mysql_error());

$doc->getElementsByTagName("password")->item(0)->nodeValue = '';
$numRows = mysql_num_rows($queryData);
if($numRows <= 0)
{
   mysql_close($sqldb);
   $doc->getElementsByTagName("message")->item(0)->nodeValue = 
                                    'Invalid login credentials!';
   echo $doc->saveXML();
   exit;
}
else
{
   $row = mysql_fetch_assoc($queryData);
   $_SESSION['at_deptId'] = $row['deptId'];
   $_SESSION['acctId'] = $row['acctId'];
   $_SESSION['username'] = $uname;
   $loginToken = rand();
   $_SESSION['loginToken'] = $loginToken;
   $now = localtime();
   $datetime = sprintf("%04d-%02d-%02d",$now[5]+1900,$now[4],$now[3]);
   $sqlQuery = sprintf(
            "INSERT INTO logintokens (logintoken, username, creation)". 
                            "VALUES ( '%d', '%s', '%s');", 
            $_SESSION['loginToken'],
            mysql_real_escape_string($username),
            $datetime);
   $queryData = mysql_query($sqlQuery, $sqldb);
   if (!$queryData) 
        die ('Could not insert token into tokentable on MySQL server". 
                                     " because:  ' . mysql_error());
   mysql_close($sqldb);

   $doc->getElementsByTagName("password")->item(0)->nodeValue = '';
   $doc->GetElementsByTagName("errorMessage")->item(0)->nodeValue = 
                      sprintf('%s Logged In',$_SESSION['username']);
   $doc->getElementsByTagName('loginLogout')->item(0)->nodeValue = 
                      "Logout";
   $doc->GetElementsByTagName("loginToken")->item(0)->nodeValue = 
                      $_SESSION['loginToken'];
   $doc->getElementsByTagName('deptId')->item(0)->nodeValue = 
                      $_SESSION['at_deptId'];
   $doc->GetElementsByTagName('login')->item(0)->nodeValue=0;

   $notLoggedElement = 
            $doc->getElementsByTagname('notLoggedIn')->item(0);
   $notLoggedElement->parentNode
            ->appendChild($doc->createElement('loggedIn'));
   $notLoggedElement->parentNode->removeChild($notLoggedElement);

   echo $doc->saveXML();
   exit;
}
?>

관련된 세션 변수를 설정하지 않음으로써 사용자가 로그아웃되었는지 먼저 확인하자. 사용자가 성공적으로 로그인한다면 또 다른 loginToken을 갖게 될 것이고, 그렇지 않다면 로그아웃 상태가 될 것이다.

다음으로 로그인 정보가 실제 제출되었는지 확인한다. 제출되지 않았다면 간단히 로그인 페이지로 되돌아가면 된다.

스크립트는 여전히 명시적인 로그아웃 요청을 다룰 수 있고 적절한 인스턴스 정보를 만들고 반환할 수 있다. 그러므로 각각의 페이지에 있는 링크는 여전히 작동한다.

사용자가 아이디와 비밀번호를 제출하지 않았다면 스크립트는 원래 인스턴스를 로그인 페이지로 반환한다. 그러나 정보가 그대로 있다면 데이터베이스와 비교해 확인한다. 로그인 정보 승인에 실패하면 스크립트는 그림 2 처럼 message 요소(errormessage 요소의 반대인)에 영향을 주는 메시지를 삽입한다.


그림 2. 로그인 실패
로그인 실패

이와 반대로 로그인 정보가 승인되면 logintokens 테이블인 세션을 파퓰레이트하고 기존처럼 반환한다. 하지만 여기서 변경해야 할 사항은 loggedIn 요소를 또한 반환된 인스턴스에 추가하고 notLoggedIn 요소를 제거하는 것이다.

인스턴스에 이런 변화를 주는 것은 로그인 폼보다는 Menu 버튼에 적절하다. 그림 3과 같다.


그림 3. 성공적인 로그인
성공적인 로그인

Menu 버튼을 클릭하면 사용자는 페이지 목록을 볼 수 있지만 성공적으로 로그인하지 않았다면 직접 방문은 할 수 없게 된다.


페이지 보호하기

Menu 버튼을 클릭하면 사용자는 http://<myhost>/appManager.php?module=appMenu로 이동한다.

이 페이지는 민감한 정보를 제공하기 전에 사용자가 로그인되었는지 확인하는 PHP 스크립트다. Listing 5를 보자.


Listing 5. appManager.php
                
<?php

session_start();

$loggedIn = false;

if (isset($_SESSION['loginToken'])){

   $sqldb = mysql_connect('localhost', 'root');
   if (!$sqldb) 
        die('Could not connect to MySQL server at localhost: ' . 
                                               mysql_error());

   $sqlseldb = mysql_select_db('acct1', $sqldb);
   if (!$sqlseldb) 
        die ('Can\'t select the acct database on MySQL server '.
                         "@ localhost because: ' . mysql_error());

   $sqlQuery = 
      sprintf("SELECT * FROM logintokens WHERE logintoken='%s'", 
              mysql_real_escape_string($_SESSION['loginToken']));

   $queryData = mysql_query($sqlQuery, $sqldb);

   if ( mysql_fetch_assoc($queryData)){
       $loggedIn = true;
   }

} 

$module = $_GET['module'];

if ($loggedIn){
    $filename = $module.".xhtml";
    header("Content-type: text/xml");
    echo file_get_contents($filename);
} else {
    header("Location: login.xhtml");
}
?>

먼저 세션을 초기화하고 loggedIn 변수를 false로 설정한다. 데이터베이스에서 검증한 것처럼 세션에서 적절한 loginToken을 찾지 않는 한 그 상태로 남아 있을 것이다(이는 또한 데이터베이스에서 토큰을 만료시키는 등의 작업을 할 수 있도록 한다).

그리고 나서 querystring에서 페이지 또는 "모듈"을 찾아온다. 사용자가 로그인 상태라면 파일이 XML 형태인지 먼저 확인한 후 브라우저에 적절한 파일의 내용을 보낼 것이다(브라우저는 이를 PHP 스크립트라고 생각하기 때문에 자동으로 작동하진 않을 것이다). 사용자가 로그인 상태가 아니라면 그냥 로그인 페이지로 돌려보낸다.

이제 이 예에서 모든 페이지는 같은 디렉터리에 있게 되는데, 이 디렉터리는 안전하게 보인다고도, 안전하지 않다고도 할 수 없다. 그러나 웹 서버를 사용하여 접근할 수 없는 디렉터리에 페이지를 보낼 수밖에 없다. 페이지를 보내면 appManager.php를 통해서만 페이지에 접근할 수 있고, 사용자가 로그인 상태가 아니라면 접근 자체가 불가능하다.

또한 사용자가 접근할 수 있는 파일의 권한을 확인하기 위해 이 파일을 확장할 수 있다. 예를 들어 관리를 위해서만 예산 페이지를 한정할 수 있는 식으로 말이다.

이제 애플리케이션은 메뉴를 제외하고는 거의 완벽해졌다.


적절한 메뉴 제공하기

이제 메뉴는 로그인한 사용자만 접근할 수 있게 됐다. 하지만 사용자가 권한이 있는 옵션만 가질 수 있는지 여부를 어떻게 확인할 수 있을까? 어쨌든 메뉴 페이지 자체는 정적인 XHTML 페이지다. Listing 6을 보자.


Listing 6. 메뉴 페이지
                
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" 
"http://www.w3.org/TR/xhtml1/D/tdxhtml1-strict.dtd">
<html
   xmlns="http://www.w3.org/1999/xhtml"
   xmlns:xforms="http://www.w3.org/2002/xforms" 
   xmlns:ev="http://www.w3.org/2001/xml-events"
>
  <head>
    <title>XForms Accounting Menu Page</title>
    <link rel="stylesheet" href="style.css" type="text/css"/>
    <xforms:model id="menuPage" >

        <xforms:instance xmlns="" src="appMenu.php" />

    </xforms:model>
  </head>
  <body>
     <p>
       
        <xforms:repeat nodeset="//link">

            <xforms:trigger>
               <xforms:label ref="." />
               <xforms:action ev:event="DOMActivate">
                    <xforms:load ref="./@href" show="replace" />
               </xforms:action>
            </xforms:trigger>

        </xforms:repeat>

     </p>
  </body>
</html>

중요한 것은 인스턴스에 있다. 옵션은 정적 목록을 가지는 대신에 PHP 스크립트인 appMenu.php로부터 데이터를 받는다. 그리고 나면 이들 각 요소는 트리거를 만들고 요소를 로드하는 데 쓰일 수 있다. 이 요소는 브라우저를 어디로 보낼지 결정하는 각 링크 요소의 href 속성에 있는 데이터를 사용한다.

appMenu.php 스크립트는 간단하다. Listing 7을 보자.


Listing 7. appMenu.php
                
<?php
    header("Content-type: text/xml");

    $deptId = $_SESSION['deptId'];

    if ($deptId = 123){

?>
     <links>
         <link href="appManager.php?module=billing">Billing</link>
         <link href="appManager.php?module=budget">Budget</link>
         <link href="appManager.php?module=invoices">Invoices</link>
         <link href=
      "appManager.php?module=assetManagement">Asset Management</link>
         <link href="appManager.php?module=payables">Payables</link>
         <link href="appManager.php?module=analyze">Analyze</link>
     </links>

<?php

} else {

?>

     <links>
         <link href="appManager.php?module=billing">Billing</link>
         <link href="appManager.php?module=budget">Budget</link>
         <link href="appManager.php?module=invoices">Invoices</link>
         <link href="appManager.php?module=payables">Payables</link>
         <link href="appManager.php?module=analyze">Analyze</link>
     </links>


<?php

}

?>

appManager.php를 통해서만 XForms 폼에 접속할 수 있으므로 여기서 사용자는 이미 로그인 상태여야 한다. 즉, 부서 ID를 사용하여 어떤 페이지를 보여줄지 동적으로 결정할 수 있다. 이들 각 링크는 appManager.php 스크립트를 통해 페이지를 보여준다는 것에 주목하자. 결과는 그림 4와 같다.


그림 4. 메뉴 페이지
메뉴 페이지

원하는 대로 페이지를 꾸밀 수 있음은 물론이다.


요약

지금까지 6회 연재를 통해 XForms, PHP, MySQL을 사용하여 회계 도구를 만드는 방법을 살펴봤다. 이 도구는 사용자가 PHP에 로그인했는지 여부를 판단하는 동작 없이 송장을 확인하고 XForms의 강점을 이용한 예산 계획 같은 동작을 수행한다. 이 방법으로 애플리케이션을 만들면 약점을 최소화하며 강점을 최대한 활용할 수 있다.

그리고 이것이 실제 세계에서의 개발의 모든 것이다.

기사의 원문보기



다운로드 하십시오

설명이름크기다운로드 방식
Part 6 샘플 코드xtrapolate.zip42KBHTTP

다운로드 방식에 대한 정보


참고자료

교육

제품 및 기술 얻기

  • XForms 권고안: W3C에서 유지 관리한다.

  • 미리 설정되어 바로 사용할 수 있는 PHP와 MySQL을 포함하는 WAMP 다운로드

  • 인터넷 익스플로러에서 XForms를 렌더링하기 위해 사용할 수 있는 오픈 소스 컨트롤인 MozzIE를 사용해 보라.

토론

필자소개

Nicholas Chase는 루슨트 테크놀러지스, 썬 마이크로시스템즈, 오라클 및 탬파베이 버커니어스와 같은 회사의 웹 사이트 개발에 참여해왔다. Nick은 고등학교 물리 교사를 비롯, 저준위(low-level) 핵 폐기물 시설 매니저, 온라인 과학소설 잡지 편집자, 멀티미디어 엔지니어, 오라클 강사, 그리고 한 회사의 CTO 등을 맡기도 했다. 그는 XML Primer Plus(Sams)를 비롯한, 다양한 책의 저자이기도 하다.

잘못된 도움말 신고

부정사용 신고

감사합니다. 이 항목은 운영자가 관심을 표시했습니다.


잘못된 도움말 신고

부정사용 신고

제출실패 신고. 나중에 다시 실행해주세요.


디벨로퍼웍스 로그인


IBM ID가 필요하세요?
IBM ID를 잊으셨습니까?


비밀번호를 잊으셨습니까?
비밀번호 변경

developerWorks 이용 약관에 동의하시는 경우 제출을 클릭하십시오. 이용 약관.

 


developerWorks에 처음 로그인하면 developerWorks프로파일이 생성됩니다.귀하의 프로파일에서 동의하신 내용이 공개되지만 이 사항은 언제든지 변경 가능합니다. 귀하의 성명(숨김으로 체크되어 있어도 표시됩니다)과 디스플레이 이름은 게시한 컨텐츠나 사이트 엑세스시 표시됩니다.

화면상에 보여지는 닉네임을 정하세요.

처음 developerWorks에 로그인할 때 프로파일이 작성되므로, 이를 위해 디스플레이 이름을 선택해야 합니다. 선택하신 디스플레이 이름은 developerWorks에 게시한 컨텐츠에 표시됩니다.

3글자 이상 31글자 이하의 길이로 사용 가능합니다. dW커뮤니티 내에서는 보안상 이메일주소를 제외한 다른 이름을 지정하셔야 합니다.

3개의 &이나 대쉬를 포함해주시고 31글자내로 제한해주세요.


developerWorks 이용 약관에 동의하시는 경우 제출을 클릭하십시오. 이용 약관.

 


아티클 순위

의견

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=20
Zone=XML
ArticleID=263101
ArticleTitle=XForms를 사용하여 회계 도구 만들기, Part 6: 요약하기 (한글)
publish-date=10232007
author1-email=ibmquestions@nicholaschase.com
author1-email-cc=dwxed@us.ibm.com

태그

Help
검색 필드를 사용하여 My developerWorks 내에서 해당 태그가 사용된 모든 종류의 컨텐츠를 검색하십시오.

태그를 더 많이 보거나 적게 보기 위해 슬라이더 막대를 사용하십시오.

인기 태그는 특정 컨텐츠 존(예를 들어, 자바, 리눅스, WebSphere)의 최고 인기 태그를 보여줍니다.

내 태그는 특정 컨텐츠 존(예를 들어, 자바, 리눅스, WebSphere)의 귀하의 태그를 보여줍니다.

검색 필드를 사용하여 My developerWorks 내에서 해당 태그가 사용된 모든 종류의 컨텐츠를 검색하십시오. 인기 태그는 특정 컨텐츠 존(예를 들어, 자바, 리눅스, WebSphere)의 최고 인기 태그를 보여줍니다. 내 태그는 특정 컨텐츠 존(예를 들어, 자바, 리눅스, WebSphere)의 귀하의 태그를 보여줍니다.