Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge pull request #23 from keedi/feature/editor-2012-12-23

Add keedi's 2012-12-23 article
  • Loading branch information...
commit 785cbf9e11aeb439a84ed1b767acf74b0da22ba8 2 parents 80974c2 + db6891b
@keedi authored
View
1,661 2012/articles/2012-12-23.mkd
@@ -0,0 +1,1661 @@
+Title: 흔한 개발 이야기
+Package: Seoul.pm
+Category: perl
+Category: Seoul.pm
+Author: keedi
+
+저자
+-----
+
+[@keedi][twitter-keedi] - Seoul.pm 리더, Perl덕후,
+[거침없이 배우는 펄][yes24-4433208]의 공동 역자, keedi.k _at_ gmail.com
+
+
+시작하며
+---------
+
+펄로 개발하는 일은 무척 신선한 경험입니다.
+전형적인 컴파일 언어로 개발하다가 펄을 만나게 되었을 때의 그 짜릿함이란!
+(잘 모르는) 많은 사람들의 우려와 달리 펄은 모듈화하기가 매우 쉬우며,
+큰 규모의 프로그램을 작성하기에 적합한 구조를 가지고 있습니다.
+하지만 많은 분들은 여전히 펄은 원라이너나 짧은 스크립트 정도로만
+쓰고 있거나, 또는 그것이 펄의 전부라고 믿곤 합니다.
+오늘은 짧지만 전형적인 개발 절차를 밟는 과정을 살짝 보여드릴까합니다.
+그야말로 흔한 개발 이야기죠. :)
+
+이것 저것 너무 깊진 않아도 여러 분야를 아우르지만,
+구색도 갖추려면 무엇이 적당할까요?
+역시 모듈화도 해야할테고, 객체지향으로 작성하는 편이 낫겠죠?
+모듈로 만들었으니 패키징도 해야할테고,
+모듈을 사용하는 명령줄 스크립트는 보너스로 넣고,
+자료를 다룬다면 데이터베이스를 사용해야겠죠.
+또 이것을 웹과 연동하면서 모바일 환경에서도 사용하는 정도면 어떨까요?
+기왕이면 모듈에 문제가 있어서 그대로 사용하기에는 애로사항이 있으면 더할나위 없겠군요.
+자, 지금부터 간단한 일정 관리 도구를 만들어 봅시다.
+
+
+주의
+-----
+
+일련의 과정을 보여드리는 것이 목적인만큼 각 단계에 대한 자세한 설명은
+소개하는 모듈과 라이브러리의 공식 문서를 참조해주세요. :)
+
+
+준비물
+-------
+
+데비안 계열의 운영체제를 사용하고 있다면 SQLite 개발 관련 패키지를 설치합니다.
+MySQL이나 PostgreSQL처럼 다른 데이터베이스를 사용한다면 그에 적절한 패키지를 설치합니다.
+
+ #!bash
+ # SQLite
+ $ sudo apt-get install libsqlite3-dev
+ # MySQL
+ $ sudo apt-get install libmysqlclient-dev
+ # PostgreSQL
+ $ sudo apt-get install postgresql-server-dev-all
+
+모듈화 및 객체지향 프로그래밍에 필요한 모듈은 다음과 같습니다.
+
+- [CPAN의 Moo 모듈][cpan-moo]
+- [CPAN의 MooX::Options 모듈][cpan-moox-options]
+- [CPAN의 MooX::Types::MooseLike::Base 모듈][cpan-moox-types-mooselike-base]
+- [CPAN의 namespace::clean 모듈][cpan-namespace-clean]
+
+데이터베이스 접근을 위해 사용한 모듈은 다음과 같습니다.
+SQLite대신 MySQL이나 PostgreSQL을 사용한다면
+[CPAN의 DBD::mysql 모듈][cpan-dbd-mysql]이나
+[CPAN의 DBD::Pg 모듈][cpan-dbd-pg]을 설치합니다.
+
+- [CPAN의 DBD::SQLite 모듈][cpan-dbd-sqlite]
+- [CPAN의 DBIx::Lite 모듈][cpan-dbix-lite]
+
+패키징에 필요한 모듈은 다음과 같습니다.
+무언가 엄청나게 많은 것을 설치하는 것 같지만 걱정하지 마세요.
+거의 대부분의 일은 [Dist::Zilla][cpan-dist-zilla]가 자동으로 처리해줍니다.
+여러분이 해야할 일은 모듈을 설치하고 설정파일을 만드는 일이죠.
+
+- [CPAN의 Dist::Zilla 모듈][cpan-dist-zilla]
+- [CPAN의 Dist::Zilla::Plugin::AutoPrereqs 모듈][cpan-dist-zilla-plugin-autoprereqs]
+- [CPAN의 Dist::Zilla::Plugin::FakeRelease 모듈][cpan-dist-zilla-plugin-fakerelease]
+- [CPAN의 Dist::Zilla::Plugin::InstallGuide 모듈][cpan-dist-zilla-plugin-installguide]
+- [CPAN의 Dist::Zilla::Plugin::MetaResources 모듈][cpan-dist-zilla-plugin-metaresources]
+- [CPAN의 Dist::Zilla::Plugin::PkgVersion 모듈][cpan-dist-zilla-plugin-pkgversion]
+- [CPAN의 Dist::Zilla::Plugin::PodCoverageTests 모듈][cpan-dist-zilla-plugin-podcoveragetests]
+- [CPAN의 Dist::Zilla::Plugin::PodSyntaxTests 모듈][cpan-dist-zilla-plugin-podsyntaxtests]
+- [CPAN의 Dist::Zilla::Plugin::PodWeaver 모듈][cpan-dist-zilla-plugin-podweaver]
+- [CPAN의 Dist::Zilla::Plugin::Prereqs 모듈][cpan-dist-zilla-plugin-prereqs]
+- [CPAN의 Dist::Zilla::Plugin::ReadmeMarkdownFromPod 모듈][cpan-dist-zilla-plugin-readmemarkdownfrompod]
+- [CPAN의 Dist::Zilla::PluginBundle::Basic 모듈][cpan-dist-zilla-pluginbundle-basic]
+- [CPAN의 Dist::Zilla::PluginBundle::Filter 모듈][cpan-dist-zilla-pluginbundle-filter]
+- [CPAN의 Pod::Weaver::PluginBundle::KEEDI 모듈][cpan-pod-weaver-pluginbundle-keedi]
+
+웹앱 작성을 위해 사용한 모듈은 다음과 같습니다.
+
+- [CPAN의 Mojolicious 모듈][cpan-mojolicious]
+- [CPAN의 Mojolicious::Plugin::HamlRenderer 모듈][cpan-mojolicious-plugin-hamlrenderer]
+
+추가로 더 설치해야 하는 모듈은 다음과 같습니다.
+
+- [CPAN의 File::HomeDir 모듈][cpan-file-homedir]
+
+사용하고는 있지만 코어 모듈인 관계로 설치하지 않아도 되는 모듈은 다음과 같습니다.
+
+- [CPAN의 Encode 모듈][cpan-encode]
+- [CPAN의 ExtUtils::MakeMaker 모듈][cpan-extutils-makemaker]
+- [CPAN의 File::Spec 모듈][cpan-file-spec]
+- [CPAN의 Time::Piece 모듈][cpan-time-piece]
+
+
+직접 [CPAN][cpan]을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.
+
+ #!bash
+ $ sudo cpan \
+ DBIx::Lite \
+ Dist::Zilla \
+ Dist::Zilla::Plugin::AutoPrereqs \
+ Dist::Zilla::Plugin::FakeRelease \
+ Dist::Zilla::Plugin::InstallGuide \
+ Dist::Zilla::Plugin::MetaResources \
+ Dist::Zilla::Plugin::PkgVersion \
+ Dist::Zilla::Plugin::PodCoverageTests \
+ Dist::Zilla::Plugin::PodSyntaxTests \
+ Dist::Zilla::Plugin::PodWeaver \
+ Dist::Zilla::Plugin::Prereqs \
+ Dist::Zilla::Plugin::ReadmeMarkdownFromPod \
+ Dist::Zilla::PluginBundle::Basic \
+ Dist::Zilla::PluginBundle::Filter \
+ Encode \
+ ExtUtils::MakeMaker \
+ File::HomeDir \
+ File::Spec \
+ Mojolicious \
+ Mojolicious::Plugin::HamlRenderer \
+ Moo \
+ MooX::Options \
+ MooX::Types::MooseLike::Base \
+ Pod::Weaver::PluginBundle::KEEDI \
+ Time::Piece \
+ namespace::clean
+
+사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나
+[perlbrew][home-perlbrew]를 이용해서 자신만의 Perl을 사용하고 있다면
+다음 명령을 이용해서 모듈을 설치합니다.
+
+ #!bash
+ cpan \
+ DBIx::Lite \
+ Dist::Zilla \
+ Dist::Zilla::Plugin::AutoPrereqs \
+ Dist::Zilla::Plugin::FakeRelease \
+ Dist::Zilla::Plugin::InstallGuide \
+ Dist::Zilla::Plugin::MetaResources \
+ Dist::Zilla::Plugin::PkgVersion \
+ Dist::Zilla::Plugin::PodCoverageTests \
+ Dist::Zilla::Plugin::PodSyntaxTests \
+ Dist::Zilla::Plugin::PodWeaver \
+ Dist::Zilla::Plugin::Prereqs \
+ Dist::Zilla::Plugin::ReadmeMarkdownFromPod \
+ Dist::Zilla::PluginBundle::Basic \
+ Dist::Zilla::PluginBundle::Filter \
+ Encode \
+ ExtUtils::MakeMaker \
+ File::HomeDir \
+ File::Spec \
+ Mojolicious \
+ Mojolicious::Plugin::HamlRenderer \
+ Moo \
+ MooX::Options \
+ MooX::Types::MooseLike::Base \
+ Pod::Weaver::PluginBundle::KEEDI \
+ Time::Piece \
+ namespace::clean
+
+
+GLGLGLGL...
+------------
+
+몇 가지 결정하고 진행해볼까요?
+우리가 만들 모듈은 `MyTodo`, 최종 생성 패키지는 `MyTodo-0.00X.tar.gz` 타르볼 파일입니다.
+패키지 안에는 `mytodo.pl`이라는 명령줄 유틸리티가 있으며,
+웹앱용 구동 파일은 `mytodo-web.pl`이며, 웹앱 설정 파일은 `mytodo-web.conf`라고 하죠.
+
+우선 작업을 진행할 디렉터리를 먼저 만들겠습니다.
+
+ #!bash
+ $ mkdir mytodo
+
+아무래도 모듈화와 패키징에 익숙하지 않다면 그때 그때 파일을 만들기 보다
+일단 디렉터리 구조를 보고 시작하는 편이 이해하기에 더 낫겠죠?
+
+ #!bash
+ $ tree mytodo/
+ mytodo/
+ ├── Changes
+ ├── bin
+ │   └── mytodo.pl
+ ├── dist.ini
+ ├── lib
+ │   ├── MyTodo
+ │   │   ├── Script.pm
+ │   │   └── Util.pm
+ │   └── MyTodo.pm
+ ├── mytodo-web.conf
+ └── mytodo-web.pl
+
+빈 파일이라도 좋으니 우선 디렉터리를 구성하고 시작하는 것이 편합니다.
+앞에서 정한 부분과 다른 부분이 몇가지 있군요.
+`Changes` 파일과 `dist.ini` 파일은 패키징을 위해 사용하며,
+`lib/MyTodo/Script.pm` 파일은 명령줄 유틸리티를 만들때 필요한 함수를 위한 모듈이며,
+`lib/MyTodo/Util.pm` 파일은 `MyTodo`에 넣기에 직접적인 연관이 없는
+함수를 저장하기 위한 모듈입니다.
+
+
+빈 파일 채워넣기
+-----------------
+
+### dist.ini
+
+`dist.ini` 파일은 [Dist::Zilla][cpan-dist-zilla]를 사용하기 위한 설정 파일입니다.
+이름이나 이메일 주소등 필요한 부분을 자신에게 맞게 변경하면 됩니다.
+
+ #!ini
+ name = MyTodo
+ author = Keedi Kim - 김도형 <keedi@cpan.org>
+ license = Perl_5
+ copyright_holder = Keedi Kim
+ copyright_year = 2012
+ version = 0.000
+
+ ;[@Basic]
+ [@Filter]
+ -bundle = @Basic
+ -remove = UploadToCPAN
+ [FakeRelease]
+
+ [AutoPrereqs]
+ [PkgVersion]
+ [ReadmeMarkdownFromPod]
+ [InstallGuide]
+ [Prereqs / RuntimeRequires]
+ [PodCoverageTests]
+ [PodSyntaxTests]
+ [PodWeaver]
+ config_plugin = @KEEDI
+
+
+### Changes
+
+`Changes` 파일은 패키지의 릴리즈별 변경사항을 기록하는 파일입니다.
+펄 모듈이라면 당연히 포함해야 하는 파일이며, CPAN은 강제하고 있습니다.
+여러분을 믿지 못한다면 항상 작성하는 것을 추천합니다.
+
+ #!plain
+ Release history for MyTodo
+
+ 0.xxx
+ First version, released on unsuspecting world.
+
+
+### 펄 모듈
+
+펄 모듈은 우선 기본 형태를 먼저 갖추도록 하죠.
+
+`lib/MyTodo.pm` 파일입니다.
+
+ #!perl
+ package MyTodo;
+ # ABSTRACT: Personal To-Do management
+
+ 1;
+ __END__
+
+ =head1 SYNOPSIS
+
+ use MyTodo;
+
+ my $todo = MyTodo->new;
+
+
+ =head1 DESCRIPTION
+
+ ...
+
+
+`lib/MyTodo/Script.pm` 파일입니다.
+
+ #!perl
+ package MyTodo::Script;
+ # ABSTRACT: MyTodo command line utility options processing
+
+ 1;
+ __END__
+
+ =head1 SYNOPSIS
+
+ ...
+
+
+ =head1 DESCRIPTION
+
+ ...
+
+`lib/MyTodo/Util.pm` 파일입니다.
+
+ #!perl
+ package MyTodo::Util;
+ # ABSTRACT: MyTodo code snippets
+
+ 1;
+ __END__
+
+ =head1 SYNOPSIS
+
+ ...
+
+
+ =head1 DESCRIPTION
+
+ ...
+
+`SYNOPSIS``DESCRIPTION`은 추후 작성하기 편리하게 `...`으로
+위치를 잡아놓은 것을 제외하면 특별한 부분은 없습니다. :)
+
+
+일정 관리 모듈
+---------------
+
+일정 관리 메인 모듈은 `MyTodo.pm` 파일입니다.
+객체지향을 지원하도록 할테니 기본적으로 `new()` 메소드를 사용할 수 있겠죠.
+데이터베이스에 접속해서 자료를 저장, 열람, 갱신, 삭제를 해야하기 때문에
+데이터베이스에 접속하기 위한 파라미터가 필요합니다.
+객체 생성시 지정할 수 있도록 `dsn`, `dbusername`, `dbpassword`, `dbattr`
+속성으로 관리하는 것이 좋을 것 같습니다.
+우리가 만든 모듈을 사용할 사용자(물론 지금은 개발자 자신이겠지만...)가
+직접 데이터베이스에 접근해서 제어하는 것을 막기 위해 모듈화를 했으므로
+기본적인 `add()`, `delete()`, `edit()`, `list()` 메소드도 필요합니다.
+
+객체지향 모듈을 제작하기 위해 다음 모듈을 추가합니다.
+
+ #!perl
+ use Moo;
+ use MooX::Types::MooseLike::Base qw( Str HashRef Maybe );
+ use namespace::clean -except => 'meta';
+
+[Moo][cpan-moo] 모듈을 사용하면 속성값을 추가하는 일은 정말 간단합니다.
+
+ #!perl
+ has dsn => (
+ is => 'ro',
+ isa => Str,
+ required => 1,
+ );
+
+ has dbusername => (
+ is => 'ro',
+ isa => Maybe[Str],
+ );
+
+ has dbpassword => (
+ is => 'ro',
+ isa => Maybe[Str],
+ );
+
+ has dbattr => (
+ is => 'ro',
+ isa => Maybe[HashRef],
+ );
+
+[Moo][cpan-moo] 모듈이 `new()` 생성자는 기본으로 만들어주기 때문에
+CRUD와 관련된 메소드만 추가하면 됩니다.
+객체지향 펄에서 메소드는 함수를 추가하는 것으로 간단히 만들어집니다.
+
+ #!perl
+ sub add {
+ my ( $self, %params ) = @_;
+ # ...
+ }
+
+ sub delete {
+ my ( $self, %params ) = @_;
+ # ...
+ }
+
+ sub edit {
+ my ( $self, %params ) = @_;
+ # ...
+ }
+
+ sub list {
+ my ( $self, %params ) = @_;
+ # ...
+ }
+
+DB에 접근을 하려면 데이터베이스에 접속해야겠죠.
+전통적인 [DBI][cpan-dbi] 모듈을 사용할 수도 있지만
+펄에는 현대적인 ORM 모듈이 무척 많습니다.
+그중에서도 상대적으로 가볍고 손쉬운 사용법이 특징인 [DBIx::Lite][cpan-dbix-lite]를
+사용해서 데이터베이스의 자료를 조작해보죠.
+우선 `DBIx::Lite` 모듈을 추가합니다.
+
+ #!perl
+ use DBIx::Lite;
+
+내부적으로 사용하기 위해 `_dbix` 속성을 추가하고 여기에 `DBIx::Lite` 객체를 저장합니다.
+`_` 기호는 외부로 공개하지 않음을 의미하는 펄 프로그래머들 사이의 관용적인 약례입니다.
+데이터베이스 접속에 필요한 모든 속성값이 갖춰진 다음 객체를 생성할 수 있도록
+`lazy` 형식으로 지정하고 `builder` 메소드를 이용해서 객체를 생성합니다.
+
+ #!perl
+ has _dbix => (
+ is => 'lazy',
+ builder => '_builder_handle',
+ );
+
+ sub _builder_handle {
+ my $self = shift;
+
+ my $dbix = DBIx::Lite->connect(
+ $self->dsn,
+ $self->dbusername,
+ $self->dbpassword,
+ $self->dbattr,
+ );
+ $dbix->schema->table('mytodo')->autopk('id');
+
+ return $dbix;
+ }
+
+하지만 `DBIx::Lite` 객체 생성 시점을 미루더라도 가능하면
+일찍 생성되도록 객체 생성 직후에 바로 생성할 수 있도록
+`BUILDER` 메소드에서 언급을 합니다.
+
+ #!perl
+ sub BUILD {
+ my $self = shift;
+
+ $self->_dbix;
+ }
+
+`lazy` 방식으로 생성되는 속성의 경우 해당 속성이 참조되는 순간까지 최대한
+생성 시점을 늦춰 객체 생성의 오버헤드를 줄여서 성능상의 이점을 얻을 수 있습니다.
+`BUILD` 메소드는 객체 생성 이후 동작을 지정할 수 있는데 이 지점에서
+`_dbix` 속성에 접근하면 해당 속성값의 빌더가 자동으로 호출되면서 `DBIx::Lite`
+객체가 생성됩니다.
+
+
+자료 구조
+----------
+
+데이터베이스에 접속할 준비가 끝났는데, 막상 어떻게 저장을 해야할지에 대한 규칙이 없군요.
+일정 관리에 필요한 자료구조, 지금은 데이터베이스 스키마를 구성해보죠.
+
+간단한 To-Do 수준의 관리니 *해야할 일*과 *하고있는 일*, *해야할 일* 정도로 나누죠.
+*무엇*을 할지도 기록해야할테고, *얼마나 중요한지*도 표시해야 할 것입니다.
+*마감날*이 있을 수도 있습니다.
+그리고 실제로 *기록한 날*과 값을 *변경한 날*도 기록하면 정렬을 할때도 도움이 될 것입니다.
+
+ #!sql
+ CREATE TABLE mytodo (
+ id INTEGER NOT NULL,
+ status CHARACTER(32) NOT NULL,
+ content INTEGER NOT NULL,
+ priority INTEGER DEFAULT 0,
+ deadline DATETIME,
+ updated_on DATETIME NOT NULL,
+ created_on DATETIME NOT NULL,
+ PRIMARY KEY (id)
+ );
+
+짜잔~ 하나의 테이블로 구성된 간단한 스키마가 완성되었습니다.
+SQLite를 기준으로 작성한 스키마이므로 다른 데이터베이스를 사용한다면
+약간 문법을 수정해야 합니다.
+
+이렇게 작성한 스키마는 어떻게 보관하면 좋을까요?
+별도의 파일로 보관하는 것도 나쁘진 않지만 아무래도 모듈과 따로 보관하다보면
+잠시 신경을 쓰지 않으면 금방 모듈의 버전보다 뒤쳐지게 되곤 합니다.
+최선은 아니겠지만 저는 주로 모듈안에 이런 데이터를 저장합니다.
+`MyTodo::Util` 모듈에 저장하고 이를 손쉽게 꺼내 쓸 수 있도록
+간단한 유틸리티를 제작합니다.
+
+`MyTodo/Util.pm` 파일에 다음 내용을 추가합니다.
+
+ #!perl
+ sub sql_sqlite {
+ return (
+ <<'END_SQL',
+ DROP TABLE IF EXISTS mytodo
+ END_SQL
+ <<'END_SQL',
+ CREATE TABLE mytodo (
+ id INTEGER NOT NULL,
+ status CHARACTER(32) NOT NULL,
+ content INTEGER NOT NULL,
+ priority INTEGER DEFAULT 0,
+ deadline DATETIME,
+ updated_on DATETIME NOT NULL,
+ created_on DATETIME NOT NULL,
+ PRIMARY KEY (id)
+ )
+ END_SQL
+ );
+ }
+
+*HERE DOCUMENT*를 적절히 활용하면 많은 양의 문자열을 쉽게 저장할 수 있습니다.
+보관만 해서는 아무 소용이 없겠죠.
+명령줄에서 언제든지 꺼내서 쓸 수 있도록 `bin/mytodo.pl` 파일에
+스키마를 열람할 수 있는 기능을 추가합니다.
+
+먼저 `MyTodo/Script.pm` 파일에 다음 내용을 추가합니다.
+
+ #!perl
+ use Moo;
+ use MooX::Options ( protect_argv => 0 );
+ use namespace::clean -except => [qw/_options_data _options_config/];
+
+ option schema_sqlite => (
+ is => 'ro',
+ doc => 'schema sql for sqlite',
+ order => 99,
+ );
+
+`bin/mytodo.pl` 파일은 다음처럼 작성합니다.
+
+ #!perl
+ #!perl
+ # ABSTRACT: MyTodo command line utility
+ # PODNAME: mytodo.pl
+
+ use 5.010;
+ use utf8;
+ use strict;
+ use warnings;
+
+ use MyTodo::Script;
+ use MyTodo::Util;
+
+ my $opt = MyTodo::Script->new_with_options;
+
+ if ( $opt->schema_sqlite ) {
+ say for map { chomp; "$_;" } MyTodo::Util->sql_sqlite;
+ exit;
+ }
+
+놀랍지만 명령줄에서 실행할 준비가 끝났습니다.
+`--help` 옵션을 이용하면 명령줄 옵션을 확인할 수 있습니다.
+
+ #!bash
+ $ perl -Ilib bin/mytodo.pl --help
+ USAGE: mytodo.pl [-h] [long options...]
+ --schema_sqlite schema sql for sqlite
+ -h --help show this help message
+
+`--schema_sqlite` 옵션을 이용해서 스키마를 출력할 수 있습니다.
+
+ #!bash
+ $ bin/mytodo.pl --schema_sqlite
+ DROP TABLE IF EXISTS mytodo;
+ CREATE TABLE mytodo (
+ id INTEGER NOT NULL,
+ status CHARACTER(32) NOT NULL,
+ content INTEGER NOT NULL,
+ priority INTEGER DEFAULT 0,
+ deadline DATETIME,
+ updated_on DATETIME NOT NULL,
+ created_on DATETIME NOT NULL,
+ PRIMARY KEY (id)
+ );
+
+파이프를 이용하면 간단히 SQLite 데이터베이스 파일을 생성할 수 있습니다.
+
+ #!bash
+ $ mkdir ~/.mytodo
+ $ bin/mytodo.pl --schema_sqlite | sqlite3 ~/.mytodo/mytodo.db
+
+
+CRUD 메소드 구현
+-----------------
+
+### MyTodo
+
+`MyTodo` 메인 모듈에 `add()`, `delete()`, `edit()`, `list()` 메소드를 추가해봅시다.
+
+ #!perl
+ sub add {
+ my $self = shift;
+
+ my $epoch = time;
+ my %params = (
+ status => 'todo',
+ created_on => $epoch,
+ updated_on => $epoch,
+ @_,
+ );
+
+ my $todo = $self->_dbix->table('mytodo')->insert({ %params });
+
+ return $todo;
+ }
+
+ sub delete {
+ my ( $self, %params ) = @_;
+
+ my $id = delete $params{id};
+ return unless $id;
+
+ $self->_dbix->table('mytodo')
+ ->search({ id => $id })
+ ->delete;
+ }
+
+ sub edit {
+ my ( $self, %params ) = @_;
+
+ my $id = delete $params{id};
+ return unless $id;
+
+ $self->_dbix->table('mytodo')
+ ->search({ id => $id })
+ ->update({ %params, updated_on => time });
+ }
+
+ sub list {
+ my $self = shift;
+ my %params = @_;
+
+ my $rs
+ = $self->_dbix->table('mytodo')
+ ->select(qw/
+ id
+ status
+ content
+ priority
+ deadline
+ created_on
+ updated_on
+ /);
+ $rs = $rs->search($_) for @{ $params{search} };
+ $rs = $rs->order_by($_) for @{ $params{order_by} };
+
+ return $rs;
+ }
+
+`DBIx::Lite`의 기본적인 기능을 이용해서 간단히 CRUD를 처리했습니다.
+[공식 문서][cpan-dbix-lite]에서 다음 메소드의 사용법을 참고해보세요.
+
+- `table()`
+- `select()`
+- `update()`
+- `delete()`
+- `search()`
+- `order_by()`
+
+
+### MyTodo::Script
+
+`MyTodo::Script` 모듈의 사용 방법을 보고 눈치채셨겠지만,
+[MooX::Options][cpan-moox-options] 모듈을 이용해서 스크립트에서 사용할
+명령줄 옵션을 OOP 모듈의 속성으로 지정할 수 있습니다.
+CRUD 기능을 완전하게 지원하기 위해서 몇가지 옵션을 더 추가해보죠.
+
+ #!perl
+ use File::HomeDir;
+ use File::Spec::Functions;
+
+ option add => (
+ is => 'ro',
+ short => 'a',
+ doc => 'add todo',
+ order => 1,
+ );
+
+ option delete => (
+ is => 'ro',
+ short => 'd',
+ format => 'i@',
+ doc => 'delete todo',
+ autosplit => ',',
+ order => 1,
+ );
+
+ option edit => (
+ is => 'ro',
+ short => 'e',
+ format => 'i@',
+ doc => 'edit todo',
+ autosplit => ',',
+ order => 1,
+ );
+
+ option list => (
+ is => 'ro',
+ short => 'l',
+ doc => 'list todo',
+ order => 1,
+ );
+
+ option priority => (
+ is => 'ro',
+ short => 'p',
+ format => 'i',
+ doc => 'todo priority',
+ order => 12,
+ );
+
+ option deadline => (
+ is => 'ro',
+ format => 's',
+ doc => 'todo deadline (local time)',
+ order => 13,
+ );
+
+ option status => (
+ is => 'ro',
+ short => 's',
+ format => 's',
+ doc => 'todo status',
+ order => 14,
+ );
+
+ option dsn => (
+ is => 'ro',
+ doc => 'database dsn',
+ format => 's',
+ default => sub {
+ my $home = File::HomeDir->my_home;
+ my $db = catfile( $home, '.mytodo', 'mytodo.db' );
+ return "dbi:SQLite:$db";
+ },
+ order => 21,
+ );
+
+ option dbusername => (
+ is => 'ro',
+ doc => 'database username',
+ format => 's',
+ order => 22,
+ );
+
+ option dbpassword => (
+ is => 'ro',
+ doc => 'database password',
+ format => 's',
+ order => 23,
+ );
+
+ option dbattr => (
+ is => 'ro',
+ doc => 'database attribute',
+ default => sub { [] },
+ format => 's@',
+ order => 24,
+ );
+
+
+### mytodo.pl
+
+추가한 옵션에 대한 액션을 정의하고 실제 동작을 구현해야겠지요.
+완전한 코드를 구경해볼까요?
+
+ #!perl
+ #!perl
+ # ABSTRACT: MyTodo command line utility
+ # PODNAME: mytodo.pl
+
+ use 5.010;
+ use utf8;
+ use strict;
+ use warnings;
+
+ use Encode qw( decode_utf8 encode_utf8 );
+ use Time::Piece;
+
+ use MyTodo;
+ use MyTodo::Script;
+ use MyTodo::Util;
+
+ binmode STDIN, ':utf8';
+ binmode STDOUT, ':utf8';
+
+ my $opt = MyTodo::Script->new_with_options;
+
+ if ( $opt->schema_sqlite ) {
+ say for map { chomp; "$_;" } MyTodo::Util->sql_sqlite;
+ exit;
+ }
+
+ my %dbattrs = map { split /=/ } @{ $opt->dbattr };
+ my $todo = MyTodo->new(
+ dsn => $opt->dsn,
+ dbusername => $opt->dbusername,
+ dbpassword => $opt->dbpassword,
+ dbattr => \%dbattrs,
+ );
+
+ if ( $opt->list ) {
+ my $display_func = sub {
+ my $item = shift;
+
+ my $str = sprintf(
+ "[%-5s] %5s : #%-2d %s",
+ uc $item->status,
+ "\x{2605}" x $item->priority . "\x{2606}" x (5 - $item->priority),
+ # 2605(★), 2606(☆)
+ $item->id,
+ decode_utf8($item->content),
+ );
+ if ($item->deadline) {
+ my $deadline = Time::Seconds->new( $item->deadline - time )->pretty;
+ $deadline =~ s/minus /-/;
+ $deadline =~ s/(\d+) days, /sprintf('%2dD', $1)/e;
+ $deadline =~ s/(\d+) hours, /sprintf('%2dH', $1)/e;
+ $deadline =~ s/(\d+) minutes, /sprintf('%2dM', $1)/e;
+ $deadline =~ s/\d+ seconds$//;
+ $str .= " ($deadline)";
+ }
+ say $str;
+ };
+
+ for my $search (
+ { status => 'doing' },
+ { status => 'todo' },
+ { status => 'done' },
+ )
+ {
+ my $rs = $todo->list(
+ search => [ $search ],
+ order_by => [ '-me.priority' ],
+ );
+ $display_func->($_) while $_ = $rs->next;
+ }
+ exit;
+ }
+
+ if ( $opt->add ) {
+ my $content = shift;
+ my $epoch = time;
+
+ my %params;
+ $params{content} = $content if $content;
+ $params{priority} = $opt->priority if $opt->priority;
+ $params{status} = $opt->status if $opt->status && $opt->status =~ /^(todo|doing|done)$/;
+ if ($opt->deadline) {
+ my $t = Time::Piece->strptime($opt->deadline, "%Y-%m-%dT%H:%M:%S");
+ $params{deadline} = $t->epoch;
+ }
+
+ $todo->add(%params);
+
+ exit;
+ }
+
+ if ( $opt->delete ) {
+ return unless $opt->delete;
+ $todo->delete( id => $opt->delete );
+ exit;
+ }
+
+ if ( $opt->edit ) {
+ my $content = shift;
+ my $epoch = time;
+
+ return unless $opt->edit;
+
+ my %params;
+ $params{id} = $opt->edit;
+ $params{content} = $content if $content;
+ $params{priority} = $opt->priority if $opt->priority;
+ $params{status} = $opt->status if $opt->status && $opt->status =~ /^(todo|doing|done)$/;
+ if ($opt->deadline) {
+ my $t = Time::Piece->strptime($opt->deadline, "%Y-%m-%dT%H:%M:%S");
+ $params{deadline} = $t->epoch;
+ }
+
+ $todo->edit(%params);
+ exit;
+ }
+
+명령줄에서 `--help` 옵션을 이용하면 구현한 모든 옵션을 확인할 수 있습니다.
+
+ #!bash
+ $ perl bin/mytodo.pl --help
+ USAGE: mytodo.pl [-adehlps] [long options...]
+ -a --add add todo
+ -d --delete delete todo
+ -e --edit edit todo
+ -l --list list todo
+ -p --priority todo priority
+ --deadline todo deadline (local time)
+ -s --status todo status
+ --dsn database dsn
+ --dbusername database username
+ --dbpassword database password
+ --dbattr database attribute
+ --schema_sqlite schema sql for sqlite
+ -h --help show this help message
+
+ $
+
+To-Do 목록을 추가하려면 `-a` 옵션을 이용합니다.
+이때 `-p` 옵션으로 중요도를 조정하고 `-s` 옵션을 이용해서
+`todo`, `doing`, `done` 중 하나의 값을 지정할 수 있습니다.
+`-e` 옵션은 수정을 위한 옵션으로 To-Do 목록 아이디를 지정하고 값을 변경할 수 있습니다.
+`-d` 옵션은 삭제를 위한 옵션으로 To-Do 목록 아이디를 지정하면 해당 목록을 지웁니다.
+마지막으로 `-l` 옵션을 이용해서 To-Do 목록을 확인할 수 있습니다.
+
+ #!bash
+ $ todo -a 'writing document for MyTodo'
+ $ todo -a 'writing perl example using LibreOffice SDK' -p3 -sdoing
+ $ todo -l
+ $ todo -e1 -p5
+ $ todo -l
+ $ todo -d1 -d2
+ $ todo -l
+
+
+Let's Patch!
+-------------
+
+사실 현재 버전(3.73)의 [MooX::Options][cpan-moox-options]를 사용하면
+도움말 출력시 옵션이 무작위 순서로 출력됩니다.
+이 문제는 내부적으로 옵션을 객체의 속성으로 저장하고 있다가
+도움말 출력 시점에 각 속성을 해시 형태로 변한한 후 해시의 키를
+추출해서 출력하기 때문에 발생하는 현상입니다.
+펄에서 해시의 순서는 무작위이기 때문에 나타나는 부작용(side-effect)인 셈이죠.
+
+사실 큰 문제는 없지만 아무래도 사용자 입장에서는 일정한 규칙에 따라
+순서대로 출력되는 편이 가독성이나 사용성 면에서 유리합니다.
+이 문제는 비교적 간단하게 해결할 수 있는데 패치는 다음과 같습니다.
+
+ #!diff
+ From cba8e63ceb2a5223a90e7be51b2ff61080db68bd Mon Sep 17 00:00:00 2001
+ From: Keedi Kim <keedi.k@gmail.com>
+ Date: Mon, 17 Dec 2012 14:19:03 +0900
+ Subject: Order attribute when displaying help message
+
+ Sorting option is helpful for users, so added order attribute.
+ First sort keys by order attr value, and default order value set as 0.
+ If order attr is same, trying to sort by it's key name. :-)
+ ---
+ lib/MooX/Options.pm | 7 ++-
+ lib/MooX/Options/Role.pm | 6 ++-
+ t/order.t | 115 ++++++++++++++++++++++++++++++++++++++++++++++
+ 3 files changed, 126 insertions(+), 2 deletions(-)
+ create mode 100644 t/order.t
+
+ diff --git a/lib/MooX/Options.pm b/lib/MooX/Options.pm
+ index 0bbdfc9..43b0d86 100755
+ --- a/lib/MooX/Options.pm
+ +++ b/lib/MooX/Options.pm
+ @@ -17,7 +17,7 @@ use Carp;
+
+ # VERSION
+ my @OPTIONS_ATTRIBUTES
+ - = qw/format short repeatable negativable autosplit doc/;
+ + = qw/format short repeatable negativable autosplit doc order/;
+
+ sub import {
+ my ( undef, @import ) = @_;
+ @@ -121,6 +121,7 @@ sub _filter_attributes {
+ sub _validate_and_filter_options {
+ my (%options) = @_;
+ $options{doc} = $options{documentation} if !defined $options{doc};
+ + $options{order} = 0 if !defined $options{order};
+
+ my %cmdline_options = map { ( $_ => $options{$_} ) }
+ grep { exists $options{$_} } @OPTIONS_ATTRIBUTES, 'required';
+ @@ -420,6 +421,10 @@ Ex :
+ my $t = t->new_with_options;
+ t->verbose # 3
+
+ +=item order
+ +
+ +Specified the order of the attribute.
+ +
+ =back
+
+ =head1 namespace::clean
+ diff --git a/lib/MooX/Options/Role.pm b/lib/MooX/Options/Role.pm
+ index 279c025..9cc201a 100644
+ --- a/lib/MooX/Options/Role.pm
+ +++ b/lib/MooX/Options/Role.pm
+ @@ -67,7 +67,11 @@ sub parse_options {
+ };
+
+ my %has_to_split;
+ - for my $name ( keys %options_data ) {
+ + my @sorted_keys = sort {
+ + $options_data{$a}{order} <=> $options_data{$b}{order} # sort by order
+ + or $a cmp $b # sort by attr name
+ + } keys %options_data;
+ + for my $name (@sorted_keys) {
+ my %data = %{ $options_data{$name} };
+ my $doc = $data{doc};
+ $doc = "no doc for $name" if !defined $doc;
+ diff --git a/t/order.t b/t/order.t
+ new file mode 100644
+ index 0000000..8df6196
+ --- /dev/null
+ +++ b/t/order.t
+ @@ -0,0 +1,115 @@
+ +#!perl
+ +use strict;
+ +use warnings;
+ +use Test::More tests => 3;
+ +use Test::Trap;
+ +
+ +{
+ + package t1;
+ + use Moo;
+ + use MooX::Options;
+ +
+ + option 'first' => (
+ + is => 'ro',
+ + documentation => 'first option',
+ + order => 1,
+ + );
+ +
+ + option 'second' => (
+ + is => 'ro',
+ + documentation => 'second option',
+ + order => 2,
+ + );
+ +
+ + option 'third' => (
+ + is => 'ro',
+ + documentation => 'third option',
+ + order => 3,
+ + );
+ +
+ + option 'fourth' => (
+ + is => 'ro',
+ + documentation => 'fourth option',
+ + order => 4,
+ + );
+ +
+ + 1;
+ +}
+ +
+ +{
+ + package t2;
+ + use Moo;
+ + use MooX::Options;
+ +
+ + option 'first' => (
+ + is => 'ro',
+ + documentation => 'first option',
+ + );
+ +
+ + option 'second' => (
+ + is => 'ro',
+ + documentation => 'second option',
+ + );
+ +
+ + option 'third' => (
+ + is => 'ro',
+ + documentation => 'third option',
+ + );
+ +
+ + option 'fourth' => (
+ + is => 'ro',
+ + documentation => 'fourth option',
+ + );
+ +
+ + 1;
+ +}
+ +
+ +{
+ + package t3;
+ + use Moo;
+ + use MooX::Options;
+ +
+ + option 'first' => (
+ + is => 'ro',
+ + documentation => 'first option',
+ + order => 1,
+ + );
+ +
+ + option 'second' => (
+ + is => 'ro',
+ + documentation => 'second option',
+ + order => 2,
+ + );
+ +
+ + option 'third' => (
+ + is => 'ro',
+ + documentation => 'third option',
+ + );
+ +
+ + option 'fourth' => (
+ + is => 'ro',
+ + documentation => 'fourth option',
+ + );
+ +
+ + 1;
+ +}
+ +
+ +{
+ + my $opt = t1->new_with_options;
+ + trap { $opt->options_usage };
+ + ok $trap->stdout =~ /first.+second.+third.+fourth/gms, 'order work w/ order attribute';
+ +}
+ +
+ +{
+ + my $opt = t2->new_with_options;
+ + trap { $opt->options_usage };
+ + ok $trap->stdout =~ /first.+fourth.+second.+third/gms, 'order work w/o order attribute';
+ +}
+ +
+ +{
+ + my $opt = t3->new_with_options;
+ + trap { $opt->options_usage };
+ + ok $trap->stdout =~ /fourth.+third.+first.+second/gms, 'order work w/ mixed mode';
+ +}
+ +
+ +done_testing;
+ --
+ 1.7.10.4
+
+모듈의 저자에게 패치를 보내기는 했지만 사실 언제 적용이 될지는 알 수가 없습니다.
+해당 패치는 설치한 모듈에 적용해야 하는데, 아무리 perlbrew를 이용해 사용자 계정에
+설치했다손 치더라도 자동으로 설치한 모듈을 직접 수정하는 것은 여러모로 찜찜합니다.
+사실 패치를 하고 그 사실을 잊어버리는 것이 찜찜한 것이겠죠.
+최선은 아니겠지만 저는 이런 경우 항상 로컬 저장소에 패치를 별도로 보관하면서
+제가 실행할 시스템 또는 모듈에만 지역적으로 적용시키곤 합니다.
+이번에도 우리의 `MyTodo` 모듈에만 적용을 시키도록 하겠습니다.
+
+`MooX::Options` 모듈 중 해당 패치를 적용시키려면
+`MooX::Options``MooX::Options::Role` 양쪽 모두에 적용해야 합니다.
+따라서 `MyTodo::Patch::` 하부에 패치를 적용시킨
+상기 두 모듈을 위치시키도록 합니다.
+적용시킨 디렉터리 구조는 다음과 같습니다.
+
+ #!bash
+ $ tree mytodo/
+ mytodo/
+ ├── Changes
+ ├── bin
+ ├── dist.ini
+ └── lib
+    ├── MyTodo
+    │   ├── Patch
+    │   │   └── MooX
+    │   │   ├── Options
+    │   │   │   └── Role.pm
+    │   │   └── Options.pm
+    │   ├── Script.pm
+    │   └── Util.pm
+    └── MyTodo.pm
+
+적용 후 패키지명을 `MyTodo::Patch::`로 시작하도록 변경해야하므로
+다음처럼 추가로 수정 하도록 합니다.
+
+ #!diff
+ diff -urN a/lib/MyTodo/Patch/MooX/Options/Role.pm b/lib/MyTodo/Patch/MooX/Options/Role.pm
+ --- a/lib/MyTodo/Patch/MooX/Options/Role.pm 2012-12-24 16:27:23.923471855 +0900
+ +++ b/lib/MyTodo/Patch/MooX/Options/Role.pm 2012-12-24 16:26:34.707469991 +0900
+ @@ -1,4 +1,4 @@
+ -package MooX::Options::Role;
+ +package MyTodo::Patch::MooX::Options::Role;
+
+ # ABSTRACT: role that is apply to your object
+ use strict;
+ diff -urN a/lib/MyTodo/Patch/MooX/Options.pm b/lib/MyTodo/Patch/MooX/Options.pm
+ --- a/lib/MyTodo/Patch/MooX/Options.pm 2012-12-24 16:27:09.483471308 +0900
+ +++ b/lib/MyTodo/Patch/MooX/Options.pm 2012-12-24 16:26:34.707469991 +0900
+ @@ -1,4 +1,4 @@
+ -package MooX::Options;
+ +package MyTodo::Patch::MooX::Options;
+
+ # ABSTRACT: add option keywords to your object (Mo/Moo/Moose)
+
+ @@ -70,7 +70,7 @@
+ my $options_data = {};
+ my $apply_modifiers = sub {
+ return if $target->can('new_with_options');
+ - $with->('MooX::Options::Role');
+ + $with->('MyTodo::Patch::MooX::Options::Role');
+
+ $around->(
+ _options_data => sub {
+
+이제 `MooX::Options` 모듈을 사용하는 `MyTodo::Script` 모듈 쪽을 수정합니다.
+
+ #!diff
+ diff -urN a/lib/MyTodo/Script.pm b/lib/MyTodo/Script.pm
+ --- a/lib/MyTodo/Script.pm 2012-12-24 16:28:33.971474508 +0900
+ +++ b/lib/MyTodo/Script.pm 2012-12-24 16:26:34.707469991 +0900
+ @@ -2,7 +2,7 @@
+ # ABSTRACT: MyTodo command line utility options processing
+
+ use Moo;
+ -use MooX::Options ( protect_argv => 0 );
+ +use MyTodo::Patch::MooX::Options ( protect_argv => 0 );
+ use namespace::clean -except => [qw/_options_data _options_config/];
+
+ use File::HomeDir;
+
+네, 모든 패치가 끝났습니다.
+사실 앞의 코드에서 속성값을 정의할때 `order`라는 값을 지정했는데,
+이 기능이 원래의 `MooX::Options`에 존재하는 기능이 아니라 방금의
+패치를 하면서 추가된 기능입니다.
+무언가 부족한 부분이 있다면 언제든지 수정해서 원하는 기능을 추가하거나
+성능을 개선시킬 수 있다는 사실이 오픈 소스의 매력이 아닐까요?
+
+
+웹앱과 모바일
+--------------
+
+이제 고지가 눈 앞입니다. 조금만 더 힘내보죠. :)
+
+지금까지의 작업으로 데이터베이스를 이용해서 일정 관리를 할 수 있는 모듈을 만들었고,
+또 그 모듈을 사용해서 명령줄에서 일정 관리를 하는 간단한 유틸리티를 만들었습니다.
+이미 모듈화를 했기 때문에 웹앱으로 만들고 모바일까지 지원하는 것은 그리 어렵지 않습니다.
+[Mojolicious][home-mojolicious] 웹프레임워크와 [jQuery 모바일][home-jquery-mobile]을
+조합해서 웹과 스마트폰에서도 사용이 가능한 웹앱을 만들어 보겠습니다.
+
+
+### 설정
+
+웹앱용 설정 파일인 `mytodo-web.conf` 파일에는 `MyTodo` 모듈을 만들때
+생성자에게 넘겨줄 인자를 저장하도록 하겠습니다.
+파일의 내용은 다음과 같습니다.
+
+ #!perl
+ #!/usr/bin/env perl
+
+ use utf8;
+ use strict;
+ use warnings;
+
+ +{
+ #
+ # mytodo
+ #
+ dsn => "dbi:SQLite:$ENV{HOME}/.mytodo/mytodo.db",
+ dbusername => q{},
+ dbpassword => q{},
+ dbattr => +{ sqlite_unicode => 1},
+ };
+
+MySQL을 사용하거나 PostgreSQL등 다른 데이터베이스를 사용한다면
+그에 적절하게 값을 변경하도록 합니다.
+물론 SQLite 파일의 위치가 다르더라도 `dsn` 값을 수정해야겠지요.
+
+
+### 컨트롤러
+
+`mytodo-web.pl`은 `Mojolicious::Lite` 모듈을 적재해서 웹앱으로써 동작하도록 합니다.
+추가로 설정 파일을 사용하기 위해 `Config` 플러그인을 적재하고,
+[Haml][home-haml]을 사용하기위해 `haml-renderer` 플러그인도 적재합니다.
+그리고 지금까지 작성한 `MyTodo` 모듈을 적재하고 객체를 생성해서
+웹앱이 일정관리를 할 만반의 준비를 갖추도록 합니다.
+
+ #!perl
+ #!/usr/bin/env perl
+
+ use 5.010;
+ use utf8;
+ use Mojolicious::Lite;
+
+ use MyTodo;
+
+ plugin 'Config';
+ plugin 'haml_renderer';
+
+ my $mytodo = MyTodo->new(
+ dsn => app->config->{dsn},
+ dbusername => app->config->{dbusername},
+ dbpassword => app->config->{dbpassword},
+ dbattr => app->config->{dbattr},
+ );
+
+ app->start;
+
+ __DATA__
+
+웹앱을 만들기 위해 작성할 컨트롤러는 `/``/detail/:_id` 단 두 개입니다.
+
+`/` 컨트롤러는 다음과 같습니다.
+`list` 렌더러(뷰)로 연결되는 점을 유의하세요.
+
+ #!perl
+ get '/' => sub {
+ my $self = shift;
+
+ my $todo = $mytodo->list(
+ order_by => [ '-me.priority' ],
+ search => [{ status => 'todo' }],
+ );
+
+ my $doing = $mytodo->list(
+ order_by => [ '-me.priority' ],
+ search => [{ status => 'doing' }],
+ );
+
+ my $done = $mytodo->list(
+ order_by => [ '-me.priority' ],
+ search => [{ status => 'done' }],
+ );
+
+ my $all = $mytodo->list(
+ order_by => [ '-me.priority' ],
+ );
+
+ $self->render(
+ 'list',
+ todo => $todo,
+ doing => $doing,
+ done => $done,
+ all => $all,
+ );
+ };
+
+`/detail/:_id` 컨트롤러는 다음과 같습니다.
+따로 렌더러(뷰)가 없이 직접 json을 반환하는 점을 유의하세요.
+Ajax를 이용한 데이터 송수신을 위해서 추가하는 컨트롤러입니다.
+
+ #!perl
+ get '/detail/:_id' => sub {
+ my $self = shift;
+
+ my $id = $self->param('_id');
+ my $item = $mytodo->_dbix->table('mytodo')->find($id);
+ my $created_on = localtime($item->created_on);
+ my $updated_on = localtime($item->updated_on);
+
+ $self->render_json({
+ content => $item->content,
+ priority => $item->priority,
+ star => "\x{2605}" x $item->priority . "\x{2606}" x (5 - $item->priority),
+ _status => uc($item->status),
+ created_on => $created_on->ymd . ' ' . $created_on->hms,
+ updated_on => $updated_on->ymd . ' ' . $updated_on->hms,
+ });
+ };
+
+
+### 렌더러(뷰)
+
+뷰에서는 jQuery 모바일용 CSS와 자바스크립트를 사용합니다.
+최대한 간단한 디렉터리 구조를 유지하고, 캐시 효과를 높이기 위해
+직접 jQuery 모바일 관련 파일을 유지하지 않고 CDN의 자원을 사용함을 유의하세요.
+로컬에서만 돌리겠다면 `public` 디렉터리를 구성한 다음 적절한 위치에
+다운로드 받고 렌더러의 자원 URI를 수정해야합니다.
+
+ #!perl
+ @@ list.html.ep
+ % layout 'list', navbar => 1, back => 0;
+ % title 'MyTodo';
+ <!-- CONTENT -->
+
+
+ @@ layouts/list.html.haml
+ !!! 5
+ %html
+ %head
+ %title= title
+ = include 'layouts/default/meta'
+ = include 'layouts/default/css'
+ = include 'layouts/default/js'
+
+ %body
+ = include 'layouts/default/items', id => 'todo', items => $todo
+ = include 'layouts/default/items', id => 'doing', items => $doing
+ = include 'layouts/default/items', id => 'done', items => $done
+ = include 'layouts/default/detail'
+
+
+ @@ layouts/default/items.html.ep
+ <!-- <%= uc $id %> -->
+ <div id="<%= $id %>" data-role="page">
+ %= include 'layouts/default/header', navbar => 1, new => 1
+ <div data-role="content">
+ <!-- CONTENT -->
+ <div data-role="fieldcontain">
+ <ul data-role="listview" data-split-icon="arrow-r">
+ % while ( my $item = $items->next ) {
+ <li>
+ <a href="#" style="padding-top: 0px;padding-bottom: 0px;padding-right: 42px;padding-left: 0px;">
+ <label style="border-top-width: 0px;margin-top: 0px;border-bottom-width: 0px;margin-bottom: 0px;border-left-width: 0px;border-right-width: 0px;" data-corners="false">
+ <fieldset data-role="controlgroup" >
+ <input type="checkbox" name="checkbox-2b" id="checkbox-2b" />
+ <label for="checkbox-2b" style="border-top-width: 0px;margin-top: 0px;border-bottom-width: 0px;margin-bottom: 0px;border-left-width: 0px;border-right-width: 0px;">
+ <label style="padding:0;">
+ <h3><%= $item->content %></h3>
+ </label>
+ </label>
+ </fieldset>
+ </label>
+ </a>
+ <a class="slide-reload" href="#detail" id="todo-item-<%= $item->id %>" data-transition="slide">Show details</a>
+ </li>
+ % }
+ </ul>
+ </div>
+ </div>
+ %= include 'layouts/default/footer'
+ </div>
+
+
+ @@ layouts/default/detail.html.ep
+ <!-- DETAIL -->
+ <div id="detail" data-role="page" data-add-back-btn="true">
+ %= include 'layouts/default/header', navbar => 0, new => 0
+ <div data-role="content">
+ <h1 class="todo-content"></h1>
+ <h2 class="todo-status"></h2>
+ <h2 class="todo-priority"></h2>
+ <div class="todo-etc"></div>
+ </div>
+ %= include 'layouts/default/footer'
+ </div>
+
+
+ @@ layouts/default/meta.html.haml
+ / META
+ %meta{:charset => "utf-8"}
+ %meta{:name => "author", content => "Keedi Kim"}
+ %meta{:name => "description", content => "MyTodo"}
+ %meta{:name => "viewport", content => "width=device-width, initial-scale=1"}
+
+
+ @@ layouts/default/css.html.ep
+ <!-- CSS -->
+ <link rel="stylesheet" href="http://code.jquery.com/mobile/1.2.0/jquery.mobile-1.2.0.min.css" />
+
+
+ @@ layouts/default/js.html.ep
+ <!-- Javascript -->
+ <script src="http://code.jquery.com/jquery-1.8.2.min.js"></script>
+ <script src="http://code.jquery.com/mobile/1.2.0/jquery.mobile-1.2.0.min.js"></script>
+ <script>
+ $(document).ready(function() {
+ $('a.force-reload').live('click', function(e) {
+ var url = $(this).attr('href');
+ $.mobile.changePage( url, { reloadPage: true, transition: "none"} );
+ });
+ $('a.slide-reload').live('click', function(e) {
+ var url = $(this).attr('href');
+ var id = this.id.replace( /.*todo-item-/, "" );
+ $.get(
+ '/detail/' + id,
+ function(_data) {
+ $("#detail .todo-content").text(_data.content);
+ $("#detail .todo-status").text(_data._status);
+ $("#detail .todo-priority").text(_data.star);
+ $("#detail .todo-etc").text('');
+ $("#detail .todo-etc").append("<p>created: " + _data.created_on + "</p>");
+ $("#detail .todo-etc").append("<p>updated: " + _data.updated_on + "</p>");
+ },
+ 'json'
+ );
+ });
+ });
+ </script>
+
+
+ @@ layouts/default/navbar.html.ep
+ <!-- NAVBAR -->
+ <div data-role="navbar">
+ <ul>
+ <li><a data-transition="none" class="<%= $id eq 'todo' ? 'ui-btn-active ui-state-persist' : q{} %>" href="#todo"> Todo </a></li>
+ <li><a data-transition="none" class="<%= $id eq 'doing' ? 'ui-btn-active ui-state-persist' : q{} %>" href="#doing"> Doing </a></li>
+ <li><a data-transition="none" class="<%= $id eq 'done' ? 'ui-btn-active ui-state-persist' : q{} %>" href="#done"> Done </a></li>
+ </ul>
+ </div>
+
+
+ @@ layouts/default/header.html.ep
+ <!-- HEADER -->
+ <div data-role="header" data-position="fixed">
+ % if ($new) {
+ <a class="force-reload" href="/" data-icon="refresh">Refresh</a>
+ % }
+ <h1><%= title %></h1>
+ % if ($new) {
+ <a href="#" data-icon="plus" class="ui-btn-right">New</a>
+ % }
+ % if ($navbar) {
+ %= include 'layouts/default/navbar'
+ % }
+ </div>
+
+
+ @@ layouts/default/footer.html.haml
+ / FOOTER
+
+
+Rock 'n Roll!!
+---------------
+
+모두 완료되었습니다.
+다음 명령으로 웹앱을 실행할 수 있습니다.
+
+ #!bash
+ $ PERL5LIB=lib morbo mytodo-web.pl
+ [Mon Dec 24 16:59:00 2012] [debug] Reading config file "/home/askdna/workspace/github/mytodo/mytodo-web.conf".
+ [Mon Dec 24 16:59:00 2012] [info] Listening at "http://*:3000".
+ Server available at http://127.0.0.1:3000.
+ ...
+
+이제 휴대폰을 이용해서 접속하면 다음과 같은 화면을 볼 수 있습니다.
+
+![iPhone에서 접속한 첫 화면][img-01]
+
+*[그림 1.]* iPhone에서 접속한 첫 화면
+
+![상단 네비게이션 바로 이동하는 화면][img-02]
+
+*[그림 2.]* 상단 네비게이션 바로 이동하는 화면
+
+![각각의 To-Do 항목의 세부 사항][img-03]
+
+*[그림 3.]* 각각의 To-Do 항목의 세부 사항
+
+![목록 화면에서 아이템 선택][img-04]
+
+*[그림 4.]* 목록 화면에서 아이템 선택
+
+제법 그럴듯하죠? :)
+
+사실 현재 다음 기능은 빠져 있습니다.
+
+- 새로운 항목 추가하기(우측 상단 New 버튼)
+- 각각의 내용 수정
+- 목록 화면에서 아이템 선택 후 상태 수정
+
+남은 부분은 여러분의 숙제로 남겨두도록 하죠.
+[Mojolicious][home-mojolicious]와 [jQuery 모바일][home-jquery-mobile]
+공식 문서를 참고해서 한 번 도전해보세요! :-)
+
+
+패키징
+-------
+
+여기까지 잘 따라왔다면 이제 패키징은 덤입니다. :-)
+
+`dist.ini` 파일의 `version` 항목을 `0.001`로 수정합니다.
+더불어 `Changes` 파일에 지금까지 작업한 내역을 적절하게 적어 넣고,
+`dist.ini`에 기입한 버전 정보를 적어줍니다.
+다음은 변경 내역입니다.
+
+ #!diff
+ diff -urN a/Changes b/Changes
+ --- a/Changes 2012-12-24 17:15:04.863580223 +0900
+ +++ b/Changes 2012-12-24 17:14:48.943579620 +0900
+ @@ -1,4 +1,7 @@
+ Release history for MyTodo
+
+ -0.XXX
+ - First version, released on unsuspecting world.
+ +0.001
+ + - Add MyTodo main module
+ + - Patch MooX::Options
+ + - Add mytodo.pl utility
+ + - Add mytodo-web.{pl|conf} web app
+ diff -urN a/dist.ini b/dist.ini
+ --- a/dist.ini 2012-12-24 17:13:22.471576343 +0900
+ +++ b/dist.ini 2012-12-24 17:13:48.999577347 +0900
+ @@ -3,7 +3,7 @@
+ license = Perl_5
+ copyright_holder = Keedi Kim
+ copyright_year = 2012
+ -version = 0.000
+ +version = 0.001
+
+ ;[@Basic]
+ [@Filter]
+
+자, 타르볼을 만들어봅시다!
+
+ #!bash
+ $ ls
+ Changes bin dist.ini lib mytodo-web.conf mytodo-web.pl
+ $ dzil build
+ [DZ] beginning to build MyTodo
+ [DZ] guessing dist's main_module is lib/MyTodo.pm
+ [DZ] extracting distribution abstract from lib/MyTodo.pm
+ [@Filter/ExtraTests] rewriting release test xt/release/pod-coverage.t
+ [@Filter/ExtraTests] rewriting release test xt/release/pod-syntax.t
+ [DZ] writing MyTodo in MyTodo-0.001
+ [DZ] building archive with Archive::Tar; install Archive::Tar::Wrapper for improved speed
+ [DZ] writing archive to MyTodo-0.001.tar.gz
+ $ ls
+ Changes MyTodo-0.001 MyTodo-0.001.tar.gz bin dist.ini lib mytodo-web.conf mytodo-web.pl
+ $
+
+유후~! :-D
+
+
+정리하며
+---------
+
+크리스마스 달력 기사라고 하기엔 무척 긴 호흡의 글이 되었네요.
+어떻게 보면 짧은 기사에서 단순한 스크립트가 아닌 완전한 객체지향 모듈을 제작했습니다.
+더불어 ORM을 이용해서 데이터베이스에 접속했으며, 유연한 펄 모듈 덕에
+다양한 데이터베이스를 지원할 수 있었습니다.
+이 모듈을 활용해서 명령줄 유틸리티를 만들었으며,
+명령줄 유틸리티는 아주 다양한 옵션을 지원합니다.
+또한 이를 위해 사용한 모듈은 명령줄 유틸리티가 도움말을 출력할때
+순서를 지정할 수 없는 한계가 있는데, 이를 시스템에 설치한 모듈을
+손대지 않고 로컬 환경에서만 적용할 수 있도록 패치도 했습니다.
+마지막으로 웹과 모바일을 지원하기 위한 미려한 웹앱을 만들었고
+이 때 작성했던 객체지향 모듈을 재사용해서 활용성을 높였습니다.
+그리고 지금까지 작업한 모든 내용은 단 한 줄의 명령을 이용해서
+릴리스용 타르볼을 만들 수도 있게 되었습니다!!
+현재 POD를 이용한 문서화만이 빠져 있는데
+실제 내용을 넣기 위한 모든 플레이스홀더를 이미 만들어 두었으니
+문서화를 마무리하는 것도 여러분에게 맡기도록 하죠.
+
+정말 놀랍지 않나요? 적어도 펄 프로그래머에게 있어 여러분이 만들 수 있는
+프로그램의 한계는 여러분의 상상력에 닿아 있을 것입니다.
+
+Enjoy Your Perl! ;-)
+
+Don't forget [fork me on GitHub][github-mytodo]!! ;-)
+
+
+[img-01]: 2012-12-23-1.png
+[img-02]: 2012-12-23-2.png
+[img-03]: 2012-12-23-3.png
+[img-04]: 2012-12-23-4.png
+
+[cpan-dbd-mysql]: https://metacpan.org/module/DBD::mysql
+[cpan-dbd-pg]: https://metacpan.org/module/DBD::Pg
+[cpan-dbd-sqlite]: https://metacpan.org/module/DBD::SQLite
+[cpan-dbi]: https://metacpan.org/module/DBI
+[cpan-dbix-lite]: https://metacpan.org/module/DBIx::Lite
+[cpan-dist-zilla-plugin-autoprereqs]: https://metacpan.org/module/Dist::Zilla::Plugin::AutoPrereqs
+[cpan-dist-zilla-plugin-fakerelease]: https://metacpan.org/module/Dist::Zilla::Plugin::FakeRelease
+[cpan-dist-zilla-plugin-installguide]: https://metacpan.org/module/Dist::Zilla::Plugin::InstallGuide
+[cpan-dist-zilla-plugin-metaresources]: https://metacpan.org/module/Dist::Zilla::Plugin::MetaResources
+[cpan-dist-zilla-plugin-pkgversion]: https://metacpan.org/module/Dist::Zilla::Plugin::PkgVersion
+[cpan-dist-zilla-plugin-podcoveragetests]: https://metacpan.org/module/Dist::Zilla::Plugin::PodCoverageTests
+[cpan-dist-zilla-plugin-podsyntaxtests]: https://metacpan.org/module/Dist::Zilla::Plugin::PodSyntaxTests
+[cpan-dist-zilla-plugin-podweaver]: https://metacpan.org/module/Dist::Zilla::Plugin::PodWeaver
+[cpan-dist-zilla-plugin-prereqs]: https://metacpan.org/module/Dist::Zilla::Plugin::Prereqs
+[cpan-dist-zilla-plugin-readmemarkdownfrompod]: https://metacpan.org/module/Dist::Zilla::Plugin::ReadmeMarkdownFromPod
+[cpan-dist-zilla-pluginbundle-basic]: https://metacpan.org/module/Dist::Zilla::PluginBundle::Basic
+[cpan-dist-zilla-pluginbundle-filter]: https://metacpan.org/module/Dist::Zilla::PluginBundle::Filter
+[cpan-dist-zilla]: https://metacpan.org/module/Dist::Zilla
+[cpan-encode]: https://metacpan.org/module/Encode
+[cpan-extutils-makemaker]: https://metacpan.org/module/ExtUtils::MakeMaker
+[cpan-file-homedir]: https://metacpan.org/module/File::HomeDir
+[cpan-file-spec]: https://metacpan.org/module/File::Spec
+[cpan-mojolicious-plugin-hamlrenderer]: https://metacpan.org/module/Mojolicious::Plugin::HamlRenderer
+[cpan-mojolicious]: https://metacpan.org/module/Mojolicious
+[cpan-moo]: https://metacpan.org/module/Moo
+[cpan-moox-options]: https://metacpan.org/module/MooX::Options
+[cpan-moox-types-mooselike-base]: https://metacpan.org/module/MooX::Types::MooseLike::Base
+[cpan-namespace-clean]: https://metacpan.org/module/namespace::clean
+[cpan-pod-weaver-pluginbundle-keedi]: https://metacpan.org/module/Pod::Weaver::PluginBundle::KEEDI
+[cpan-time-piece]: https://metacpan.org/module/Time::Piece
+[cpan]: http://www.cpan.org/
+[github-mytodo]: https://github.com/keedi/mytodo
+[home-haml]: http://haml.info/
+[home-jquery-mobile]: http://jquerymobile.com
+[home-mojolicious]: http://mojolicio.us/
+[home-perlbrew]: http://perlbrew.pl/
+[twitter-keedi]: http://twitter.com/#!/keedi
+[yes24-4433208]: http://www.yes24.com/24/goods/4433208
View
BIN  2012/share/static/2012-12-23-1.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  2012/share/static/2012-12-23-2.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  2012/share/static/2012-12-23-3.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  2012/share/static/2012-12-23-4.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Please sign in to comment.
Something went wrong with that request. Please try again.