라라벨 (Laravel) 로 모빌리언스 결제

moby
Standard

Mobilians is another Korean PG company allows users to pay for purchases with their Korean cell-phone, which is a pretty common practice in Korea. Let’s see how we can leverage this while using Laravel. It’s gonna be challenging for sure though. Everytime I had to deal with IT products from Korean companies, I found it unpleasantly difficult to follow their inferior manuals. We’ll see how it turns out this time soon.

If I understand it correctly, there’re 2 difference ways to make it work. One that forces you to compile a dependency module from a tarball and another without any dependency module. For the sake of simplicity, I’ll go with the latter. Now, it’s time to see their sample codes. (Oh great, you can’t extract the sample zip file on a Mac. It started to tick me off already.)

Configuration

I’ve attached the options that the PG company requires in app/config/services.php

    return array (
        :
        'mobillians' => [
            'svcid' => '120000000000', // a string id assigned to your account
            'url' => 'hanlingo.com', // a URL associated with this transaction
            'mode' => '00' // 00 for a sandbox transaction, 01 for a live transaction
        ],
        :
    );

Controller

After setting up the app/routes.php as follows:

    Route::post('payment/preview', [ 'as' => 'payments.preview', 'uses' => 'PaymentsController@preview' ]);
    Route::post('payments/moby',   [ 'as' => 'payments.moby',    'uses' => 'PaymentsController@postFromMoby' ]);

The only thing you need to set up is a controller to deal with user requests.

    public function preview()
    {
        $data = $this->paymentForm->validate(Input::all());
        $keys = Config::get('services.mobillians');

        $data['transaction_id'] = 'TEESHOT-'.$keys['svcid']."_".$this->randomShit();
        $data['phone'] = $this->me->profile->phone;
        $data['email'] = $this->me->email;
        $data['user_id'] = $this->me->id;
        $data['item_en'] = 'TeeShot Membership ('.(intval($data['product']) - 1).')';
        $data['post_url'] = "http://test.putting.co.kr/payments/moby";

        return View::make('payments.confirmmoby')->with(compact('data', 'keys'));
    }

    public function postFromMoby()
    {
        if (Input::get('Resultcd') != "0000")
            dd( '알수 없는 에러 발생 ('.$this->koreanShit(Input::get('Resultmsg')).', 1)' );

        $data = [
            'transaction_id' => Input::get('Tradeid'),
            'payment_type' => '휴대폰',
            'item_name' => Input::get('Prdtnm'),
            'item_price' => Input::get('Prdtprice'),
            'recurring' => 0,
            'realname' => $this->me->profile->realname,
            'phone' => Input::get('No'),
            'email' => Input::get('Payeremail'),
            'user_id' => Input::get('Userid'),
        ];

        $payment = $this->me->payments()->create($data);

        $this->updateUserRecord($ended_at);

        Event::fire('Teeshot.Events.UserHasPurchased', new UserHasPurchased($this->me, $payment));

        return View::make('payments.moby')->with(compact('data'));
    }

    private function updateUserRecord($date)
    {
        $this->me->expired_at = $date;
        $this->me->save();
    }

    private function randomShit() {
        $microtime = microtime();
        $comps = explode(" ", $microtime);
        return date("YmdHis") . sprintf("%04d", $comps[0] * 10000);
    }

Here’re the view files that I used in my project. First, payments/confirmmoby.blade.php

    @extends('layouts.default')

    @section('content')

        <div class="panel panel-default">
            <div class="panel-heading"><div class="panel-title">이용권 구매</div></div>
            <div class="panel-body">
                <div class="form-group golf-info">
                    {{ Form::open(['name' => 'payForm']) }}

                        <input type="hidden" name="MC_SVCID" id="MC_SVCID" size="30" value="{{ $keys['svcid'] }}">
                        <input type="hidden" name="Siteurl" id="Siteurl" size="30" value="{{ $keys['url'] }}">
                        <input type="hidden" name="PAY_MODE" id="PAY_MODE" size="30" value="{{ $keys['mode'] }}">
                        <input type="hidden" name="Prdtnm" id="Prdtnm" size="30" value="{{ $data['item_en'] }}">
                        <input type="hidden" name="Prdtprice" id="Prdtprice" size="30" value="{{ $data['amount'] }}">
                        <input type="hidden" name="Tradeid" id="Tradeid" size="50" value="{{ $data['transaction_id'] }}">
                        <input type="hidden" name="MC_No" id="MC_No" size="30" value="{{ $data['phone'] }}">
                        <input type="hidden" name="Okurl" id="Okurl" size="50" value="{{ $data['post_url'] }}">
                        <input type="hidden" name="Payeremail" id="Payeremail" size="30" value="{{ $data['email'] }}">
                        <input type="hidden" name="Userid" id="Userid" size="30" value="{{ $data['user_id'] }}">
                        <input type="hidden" name="CASH_GB" id="CASH_GB" size="30" value="MC">
                        <input type="hidden" name="LOGO_YN" id="LOGO_YN" size="30" value="N">
                        <input type="hidden" name="CALL_TYPE" id="CALL_TYPE" size="30" value="P">
                        <input type="hidden" name="Cryptyn" id="Cryptyn" size="30" value="N">
                        <input type="hidden" name="Notiemail" id="Notiemail" size="30" value="admin@admin.com">

                        <input type="hidden" name="MSTR" id="MSTR" size="50" value="">
                        <input type="hidden" name="Notiurl" id="Notiurl" size="50" value="">
                        <input type="hidden" name="Failurl" id="Failurl" size="50" value="">
                        <input type="hidden" name="Closeurl" id="Closeurl" size="50" value="">
                        <input type="hidden" name="MC_PARTPAY" id="MC_PARTPAY" size="30" value="">
                        <input type="hidden" name="MC_DEFAULTCOMMID" id="MC_DEFAULTCOMMID" size="30" value="">
                        <input type="hidden" name="MC_FIXCOMMID" id="MC_FIXCOMMID" size="30" value="">
                        <input type="hidden" name="MC_FIXNO" id="MC_FIXNO" size="30" value="">
                        <input type="hidden" name="MC_Cpcode" id="MC_Cpcode" size="30" value="">
                        <input type="hidden" name="Item" id="Item" size="30" value="">
                        <input type="hidden" name="Prdtcd" id="Prdtcd" size="30" value="">
                        <input type="hidden" name="Sellernm" id="Sellernm" size="30" value="">
                        <input type="hidden" name="Sellertel" id="Sellertel" size="30" value="">
                        <input type="hidden" name="IFRAME_NAME" id="IFRAME_NAME" size="30" value="">
                        <input type="hidden" name="INFOAREA_YN" id="INFOAREA_YN" size="30" value="">
                        <input type="hidden" name="FOOTER_YN" id="FOOTER_YN" size="30" value="">
                        <input type="hidden" name="HEIGHT" id="HEIGHT" size="30" value="">
                        <input type="hidden" name="PRDT_HIDDEN" id="PRDT_HIDDEN" size="30" value="">
                        <input type="hidden" name="EMAIL_HIDDEN" id="EMAIL_HIDDEN" size="30" value="">
                        <input type="hidden" name="CONTRACT_HIDDEN" id="CONTRACT_HIDDEN" size="30" value="">
                        <input type="hidden" name="Cryptstring" id="Cryptstring" size="50" value="">
                        <input type="hidden" name="Crypthash" id="Crypthash" size="50" value="">
                        <input type="hidden" name="MC_AUTHPAY" id="MC_AUTHPAY" size="30" value="">
                        <input type="hidden" name="MC_AUTOPAY" id="MC_AUTOPAY" size="30" value="">

                        <table class="table">
                            <tr>
                                <td>상품명</td>
                                <td>{{ $data['item'] }}</td>
                            </tr>
                            <tr>
                                <td>결제금액</td>
                                <td>{{ preg_replace("/(?<=\d)(?=(?:\d\d\d)+(?!\d))/", ",", $data['amount']) }} 원</td>
                            </tr>
                            <tr>
                                <td>구매수단</td>
                                <td>{{ $data['description'] }}</td>
                            </tr>
                        </table>

                        <div>
                            <p>아래의 결제 버튼을 누르면 모빌리언스를 통하여 결제가 진행됩니다.</p>
                            <div><button class="btn btn-lg btn-primary" id="btnPay">결제</button></div>
                        </div>

                    {{ Form::close() }}
                </div>
            </div>
        </div>

    @stop

    @section('scripts')

        <script src="https://mup.mobilians.co.kr/js/ext/ext_inc_comm.js"></script>
        <script type="text/javascript">
            $('#btnPay').click(function() {
                $(this).hide();
                MCASH_PAYMENT(document.payForm);
            });
       </script>

    @stop

Then, the 2nd view file named payments/moby.blade.php

    @extends('layouts.default')

    @section('content')

        <div class="header">
            <h4>결제결과</h4>
        </div>

        <table class="table">
            <tr><td>결제방법</td><td>{{ $data['payment_type'] }}</td></tr>
            <tr><td>결과내용</td><td>{{ '결제가 정상처리되었습니다.' }}</td></tr>
            <tr><td>거래금액</td><td>{{ $data['item_price'] }} 원</td></tr>
        </table>

        <button class="btn btn-lg btn-success" id="btnClose">창닫기</button>

    @stop

    @section('scripts')
        <script type="text/javascript">

            function redirectToPage(){
                window.opener.location.href="/users/{{ $data['user_id'] }}/payments";
                self.close();
            }

            $(function() {
                $('#btnClose').click(function(e) {
                    redirectToPage();
                });
            });
        </script>
    @stop

Like Inicis PG service I reviewed earlier, this company only uses EUC-KR instead of UTF-8. It means that passing string parameters using UTF-8 Korean will not work out of box. Use UTF-8, suckers.

라라벨 (Laravel) 로 SMS 보내기

sms
Standard

미국같았으면 당연히 Twilio API 를 통해서 SMS 를 보냈겠지만, 국내 사용자를 대상으로 SMS 를 보낼때, 국내 업체를 통하는 것이 2-3 배 저렴하다. 단점은 10년전 만들어진 legacy API class 를 직접 뜯어고쳐서 사용해야 한다는 불편함과, sample 코드에 문제가 있어도 정작 그 회사에서는 아무도 tech support 를 안한다는… 한국 IT 의 암울한 현주소. 어쨌든 들어는 봤었을 법한 후이즈 (Whois) 라는 회사의 서비스를 통해서 SMS 를 보내보자.

해당 회사에서 제공하는 샘플코드에는 2개의 레거시 클래스가 있지만, PHP 5.6 에서는 동작하지 않았다. xmlpc.inc.php 라는 파일은 http://gggeek.github.io/phpxmlrpc/ 에서 최신판으로 바꾸고, app/classes 라는 폴더에 넣는다. (그런 다음 php artisan dump-autoload 명령 실행) 나머지 EmmSMM.php 라는 클래스는 지워버린다.

다른 외부 API 서비스들의 credentials 을 저장하는 것과 마찬가지로, app/config/services.php 파일을 열고 후이즈에서 계약당시 받은 계정 아이디와 비밀번호를 아래와 같이 추가한다.

    return array(
        :
        'whois' => [
            'Id' => 'hanlingo.com',
            'Pass' => 'dumbass!'
        ]

    );

그런 다음, App/Sms 폴더에, 아래와 같은 내용으로 WhoIsSms.php 파일을 만든다.

    <?php namespace Teeshot\Sms;

    class WhoIsSms {

        protected $args;
        protected $host;
        protected $port;
        protected $path;
        protected $errMsg;

        public function __construct($app) {
            $this->args = $app['config']['services.whois'];
            $this->host = "www.whoisweb.net";
            $this->port = 80;
        }

        private function setURL($url) {
            if (!$m = parse_url($url))
                return $this->setError("Can't parse the URL provided.");

            $this->host = $m['host'];
            $this->port = isset($m['port']) ? $m['port'] : 80;
            $this->path = ($m['path']) ? $m['path'] : "/";

            return true;
        }

        private function setError($msg) {
            $this->errMsg = $msg;

            return false;
        }

        private function xmlrpc_send($func, $args) {
            $server = new \xmlrpc_client($this->path, $this->host, $this->port);
            $message = new \xmlrpcmsg($func, array(php_xmlrpc_encode($args)));
            $result = $server->send($message);

            if ($result) {
                if ($ret = $result->value()) {
                    return php_xmlrpc_decode($ret);
                } else {
                    return $this->setError($result->faultCode().":".$result->faultString());
                }
            }

            return $this->setError("Transmission error.");
        }

        public function send($To, $From, $Message, $Date='', $SmsType='') {
            if (is_array($To))
                $this->args['To'] = implode(",", $To);
            else
                $this->args['To'] = $To;
            $this->args['From'] = $From;
            $this->args['Message'] = $Message;
            $this->args['Date'] = $Date;
            $this->args['SmsType'] = $SmsType;

            $this->setURL("http://www.whoisweb.net/emma/API/EmmaSend_All.php");
            foreach ($this->args as $key => $value)
                $args[$key] = base64_encode($value);
            $res = $this->xmlrpc_send('EmmaSend', $args);

            if ($res['Code'] != '00')
                return $this->setError($res['CodeMsg']);
            return $res;
        }

        // $message = '[티샷] 비밀번호: '.$secret;
        public function sendSMS($phone, $message) {
            $result = $this->send($phone, '02-2277-3489', $message, '', '');

            if ($result['Code'] == '00') {
                if (intval($result['LastPoint']) < 100) {
                    // inform admin that there are less than 100 points left
                }
                return null;
            }
            return $this->errMsg;
        }

    }

WhoIsSms::sendSMS(‘보낼 번호’, ‘보낼 메시지’) 형식으로 사용할 수 있도록, 서비스 프로바이더와 파싸드를 만든다.

App/Providers/SmsServiceProvider.php 를 생성하고

    <?php namespace App\Providers;

    use Illuminate\Support\ServiceProvider;
    use App\Sms\WhoIsSms;

    class SmsServiceProvider extends ServiceProvider {

        protected $defer = false;

        public function register()
        {
            $this->app->bindShared('sms', function($app) {
                return new WhoIsSms($app);
            });
        }
    } 

App/Providers/Facades/WhoIsSms.php 를 생성하고

    <?php namespace App\Providers\Facades;

    use Illuminate\Support\Facades\Facade;

    class WhoIsSms extends Facade {

        protected static function getFacadeAccessor() {
            return 'sms';
        }
    }

라고 입력한다. app/config/app.php 의 providers 배열과 aliases 배열안에 아래의 라인을 삽입하면,

    'App\Providers\SmsServiceProvider',

    'WhoIsSms' => 'App\Providers\Facades\WhoIsSms',

이제 당신의 애플리케이션 어디에서든

\App\Providers\Facades\WhoIsSms::sendSMS(‘010-9486-7415’, ‘hello there. I am baymax.’);

라고 사용할 수 있지롱.

라라벨 (Laravel) 로 이니시스 결제

laravel-inicis
Standard

HOW TO RIP OFF KOREAN USERS BY USING INICIS. J/K.

It’s not intended to replace the whole manual from INICIS, one of major Korean PGs to allow you to accept Korean credit cards. The objective of this writing is provide you a feasible way to use INICIS while using Laravel 4.2.x PHP framework. It was a pain in the ass to read through their unbelievably crappy manual though. At the time of this writing, I am using L4 for my current project so bear with me until I upgrade to the version 5 if that’s what you want. So, let’s cut to the chase;

They have this legacy PHP class called INIpay50 which seems to be written in old school days. You may simply want to put them in the following directory;

    {project-root}/app/classes/INIpay50

and autoload ’em with composer.json like this;

    "autoload": {
        "classmap": [
            :
            "app/classes"
        ]
    }

Don’t forget to php artisan dump-audoload after updating composer.json. Another caveat you should keep in mind is that encryption key-file directory and log directory are under the directory as you may need to check out them later.

Unfortunately, the way the legacy classes are rigidly coupled with Views and JavaScripts forces me to follow their fragile coding style at least with my level of understanding of how they work. Too bad.

The following is the migration I work with. You should make a similar one for your own good:

    public function up()
    {
        Schema::create('payments', function(Blueprint $table)
        {
            $table->increments('id');
            $table->integer('user_id')->unsigned();
            $table->string('transaction_id');
            $table->string('payment_type');
            $table->string('item_name');
            $table->integer('item_price');
            $table->tinyInteger('recurring')->default(0);
            $table->string('realname');
            $table->string('phone');
            $table->string('email');
            $table->timestamps();
            $table->dateTime('started_at')->nullable();
            $table->dateTime('ended_at')->nullable();
        });
    }

Then, the model, Payment:

    <?php namespace Teeshot\Payments;

    use Eloquent;
    use Laracasts\Presenter\PresentableTrait;

    class Payment extends Eloquent {

        use PresentableTrait;

        protected $fillable = [ 'user_id', 'transaction_id', 'payment_type', 'item_name', 'item_price', 'recurring', 'realname', 'phone', 'email', 'started_at', 'ended_at' ];

        protected $table = 'payments';
        protected $dates = [ 'started_at', 'ended_at' ];
        protected $presenter = 'Teeshot\Payments\PaymentPresenter';

        public function user()
        {
            return $this->belongsTo('User');
        }
    } 

Then the presenter which allows to separate logics from its View. You can ignore this but I’m attaching this for the integrity of source codes.

    <?php namespace Teeshot\Payments;

    use Carbon\Carbon;
    use Laracasts\Presenter\Presenter;

    class PaymentPresenter extends Presenter {

        public function typeOfPayment($key)
        {

            $arrTypes = [
                'Card'=>'신용카드',
                'VCard'=>'신용카드',
                'DirectBank'=>'실시간 계좌이체',
                'VBank'=>'가상계좌'
            ];

            return $arrTypes[$key];
        }

        public function nameOfCard($key)
        {
            $arrCards = [
                "01"=>"외환",
                "03"=>"롯데",
                "04"=>"현대",
                "06"=>"국민",
                "11"=>"BC",
                "12"=>"삼성",
                "13"=>"LG",
                "14"=>"신한",
                "15"=>"한미",
                "21"=>"해외비자",
                "22"=>"해외마스터",
                "23"=>"JCB",
                "24"=>"해외아멕스",
                "25"=>"해외다이너스"
            ];

            return $arrCards[$key];
        }


        public function nameOfCardIssuer($key)
        {
            $arrIssuers = [
                "02"=>"한국산업은행",
                "03"=>"기업은행",
                "04"=>"국민은행(주택은행)",
                "05"=>"외환은행",
                "07"=>"수협중앙회",
                "11"=>"농협중앙회",
                "12"=>"단위농협",
                "16"=>"축협중앙회",
                "20"=>"우리은행",
                "21"=>"신한은행(조흥은행)",
                "23"=>"제일은행",
                "25"=>"하나은행(서울은행)",
                "26"=>"신한은행",
                "27"=>"한국씨티은행(한미은행)",
                "31"=>"대구은행",
                "32"=>"부산은행",
                "34"=>"광주은행",
                "35"=>"제주은행",
                "37"=>"전북은행",
                "38"=>"강원은행",
                "39"=>"경남은행",
                "41"=>"비씨카드",
                "53"=>"씨티은행",
                "54"=>"홍콩상하이은행",
                "71"=>"우체국",
                "81"=>"하나은행",
                "83"=>"평화은행",
                "87"=>"신세계",
                "88"=>"신한은행(조흥 통합)"
            ];

            return $arrIssuers[$key];
        }
    }

Then, the controller. I believe it wouldn’t be hard to follow the logic. The only thing I want to mention is how the validate method works in the paymentForm class. It’s a simple decorator after validating user inputs, which returns only the data that Payment model requires.

    <?php

    use Teeshot\Forms\PaymentForm;
    use Teeshot\Payments\Payment;
    use Teeshot\Payments\PaymentRepository;
    use Teeshot\Events\UserHasPurchased;
    use Carbon\Carbon;

    class PaymentsController extends \BaseController
    {
        private $paymentForm;
        protected $payments;
        protected $me;

        function __construct(PaymentForm $paymentForm, PaymentRepository $paymentRepository)
        {
            parent::__construct();

            $this->paymentForm = $paymentForm;
            $this->payments = $paymentRepository;
            $this->me = Auth::user();
        }

        public function preview()
        {
            $formData = $this->paymentForm->validate(Input::all());

            $keys = Config::get('services.inicis');
            $transition_id = 'TEESHOT-INICIS-'.date("ymd-His").'-'.sprintf("%09d", $this->me->id);  // INICIS-150325-142412-0000000031

            $inipay = new INIpay50;
            $inipay->SetField("inipayhome", app_path()."/classes/INIpay50");
            $inipay->SetField("type", "chkfake");
            $inipay->SetField("debug", "false"); // to exclude logging
            $inipay->SetField("enctype", "asym");
            $inipay->SetField("admin", $keys['admin']);
            $inipay->SetField("checkopt", "false");
            $inipay->SetField("mid", $keys['mid']);
            $inipay->SetField("price", $formData['amount']);
            $inipay->SetField("nointerest", "no");
            $inipay->SetField("quotabase", "선택:일시불:2개월:3개월:6개월");
            $inipay->startAction();

            if ($inipay->GetResult("ResultCode") != "00")
                dd( '알수없는 에러 ('.$inipay->GetResult("ResultMsg").')' );

            Session::put('INI_MID', $keys['mid']);
            Session::put('INI_ADMIN', $keys['admin']);
            Session::put('INI_OID', $transition_id);
            Session::put('INI_PRICE', $formData['amount']);
            Session::put('INI_RN', $inipay->GetResult("rn"));
            Session::put('INI_ENCTYPE', $inipay->GetResult("enctype"));

            // Add 3 more data that this shit requires
            $formData['today'] = date("Ymd", time());
            $formData['ini_encfield'] = $inipay->GetResult("encfield");
            $formData['ini_certid'] = $inipay->GetResult("certid");

            return View::make('payments.preview')->with(compact('formData'));
        }

        public function bill()
        {
            $inipay = new INIpay50;
            $inipay->SetField("inipayhome", app_path()."/classes/INIpay50");
            $inipay->SetField("type", "securepay");
            $inipay->SetField("pgid", "INIphp".Input::get('pgid'));
            $inipay->SetField("subpgip","203.238.3.10");
            $inipay->SetField("admin", Session::get('INI_ADMIN'));
            $inipay->SetField("debug", "false");
            $inipay->SetField("uid", Input::get('uid'));
            $inipay->SetField("goodname", Input::get('goodname'));
            $inipay->SetField("currency", Input::get('currency'));
            $inipay->SetField("mid",Session::get('INI_MID'));
            $inipay->SetField("rn", Session::get('INI_RN'));
            $inipay->SetField("price", Session::get('INI_PRICE'));
            $inipay->SetField("enctype", Session::get('INI_ENCTYPE'));
            $inipay->SetField("buyername", Input::get('buyername'));
            $inipay->SetField("buyertel",  Input::get('buyertel'));
            $inipay->SetField("buyeremail", Input::get('buyeremail'));
            $inipay->SetField("paymethod", Input::get('paymethod'));
            $inipay->SetField("encrypted", Input::get('encrypted'));
            $inipay->SetField("sessionkey", Input::get('sessionkey'));
            $inipay->SetField("url", "http://teeshot.app:8000"); // REPLACE IT WITH YOUR URL
            $inipay->SetField("cardcode", Input::get('cardcode'));
            $inipay->SetField("parentemail", "your@parent.com");
            $inipay->SetField("joincard", Input::get('joincard'));
            $inipay->SetField("joinexpire", Input::get('joinexpire'));
            $inipay->SetField("id_customer", Input::get('id_customer'));
            $inipay->startAction();

            if ($inipay->GetResult("ResultCode") != "00")
                dd( '알수없는 에러 ('.$inipay->GetResult("ResultMsg").')' );
            else {

                $data = [
                    'transaction_id' => Input::get('oid'),
                    'payment_type' => Input::get('paymethod'),
                    'item_name' => Input::get('goodname'),
                    'item_price' => Input::get('price'),
                    'recurring' => Input::get('recurring'),
                    'realname' => $this->me->profile->realname,
                    'phone' => $this->me->profile->phone,
                    'email' => $this->me->email,
                    'started_at' => Carbon::now()->toDateTimeString(),
                    'ended_at' => Carbon::now()->addDays(31)->toDateTimeString()
                ];

                $payment = $this->me->payments()->create($data);

                $this->me->expired_at = $ended_at;
                $this->me->save();

                Event::fire('Teeshot.Events.UserHasPurchased', new UserHasPurchased($this->me, $payment));

                Session::forget('INI_ADMIN');
                Session::forget('INI_ENCTYPE');
                Session::forget('INI_MID');
                Session::forget('INI_OID');
                Session::forget('INI_PRICE');
                Session::forget('INI_RN');
            }

            return View::make('payments.inicis')->with(compact('inipay', 'payment'));
        }
    }

And, finally, the views:

1st view with options

https://gist.github.com/jinseokoh/edf8b3c3afe5845e16a7

2nd view which loads preview screen and brings in Active X encryption modules;

https://gist.github.com/jinseokoh/1489dacc6d527d6fb97c

3rd view which shows the result;

https://gist.github.com/jinseokoh/d0f2bcd9de475cde3f91

I know there’re rooms to improve. But, hopefully, my codes here would eventually help you get you going with the INICIS PG service without pulling your hair out. Even though I followed their instructions to load the JavaScript file written in UTF-8 Korean code. Their server isn’t smart enough to figure I’m using UTF-8. This is the level of service from a company called themselves number 1 in Korea. guess they’re all too busy doing shit.

Laravel 앱의 repository 를 자동 deploy 하기

deployment
Standard

써놓고 보니 좀 웃긴 제목이다. 지금도 잊혀지지않는 십년전 그 SK 부장의 영어단어 섞어쓰기처럼 한국어도 아니고 영어도 아닌 말이 되어버렸지만, 프로그래머 세계에서 통용되는 표현이라 생각하삼. 어쨌든 오늘 이야기의 주제는, 로컬 개발 내용을 자동으로 프로덕션 서버로 deploy 시키기이다. Laravel Forge 를 사용하는 방법도 하나가 되겠지만, Laravel 을 사용하지 않는 사용자이거나, 무료로 솔루션을 찾는 사용자(= 나?)라면 dploy.io 써비스도 옵션이 될 수 있다. 단, 무료는 1개 repository 만 지원하고 그 이상을 원할 경우 한달에 15불씩 낸다. 흠. Laravel Forge 는 한달에 10불. 어쨌든. 시나리오는 아래와 같다.

나의 맥에서 GitHub 의 repository 로 “git push” 를 하면 해당 commit 내용이 DigitalOcean 에 있는 production server 로 자동 deploy 된다.

어 짱인데? 싶으면 계속 읽고, 아니면 지나쳐 주삼. 준비물은 아래와 같다.

  • GitHub 계정과 1개의 repository
  • dploy.io 계정
  • DigitalOcean 계정 (클라우드 호스팅)
  • git 으로 버젼 컨트롤 하며 작업중인 프로젝트

dploy.io 는 무료이므로 계정을 만들고 로그인을 한다. 아래는 로그인 후의 스크린 샷이다.

Screen Shot 2015-01-14 at 4.56.41 AM

아래 스크린샷 처럼, GitHub 리파지토리를 연결시켜보자. 물론 Bitbucket 사용하는 사람은 그걸로 선택해서, 원하는 리파지토리를 선택하면 된다. 연결시에는 OAuth 를 위해 GitHub 으로 이동해서 dploy.io 사용을 허락할지 물어본다. 이 단계를 마치면 해당 GitHub 의 리파지토리에 Webhook 이 자동으로 생성된다. (GitHub 의 리파지토리 Settings 메뉴를 누르고 Webhooks & Services 섭메뉴를 누르면 확인할 수 있다.)

Screen Shot 2015-01-14 at 4.58.03 AM

아래는 GitHub 리파지토리 연결이 되는 도중의 스크린샷이다.

Screen Shot 2015-01-14 at 4.58.54 AM

완료후 설정아이콘을 누르면, 자동으로 파일들을 deploy 하기 위해서 Environment 와 서버 설정을 하라는 화면이 아래처럼 나온다.

Screen Shot 2015-01-14 at 5.05.21 AM

Hosting Server 옵션으로 여러가지가 있는데, 이 글을 쓰고 있는 현재 DigitiaOcean 을 사용중이므로 이를 선택했다. 그러면 아래와 같은 화면이 나온다. 화면이 길어서 스크린샷을 2개로 나눠 올렸는데, Dropet 선택부 하단에 회색 이탤릭체로 나온 부분에 copy the key 라는 링크를 누르면 퍼블릭키가 나온다. 이 퍼블릭키를 카피해서, DigitalOcean 서버의 ~/.ssh/authorized_keys 에 추가해야만 dploy.io 가 deploy 할 서버로 SSH 연결이 가능해진다. (다시말해, 내 대신 dploy.io 가 GitHub 의 리파지토리 내용을 DigitalOcean 서버에 git pull 할 수 있게 한다.)

Screen Shot 2015-01-14 at 5.34.27 AM

Screen Shot 2015-01-14 at 5.37.50 AM

deploy할 서버의 위치등 모든 내용을 정확히 입력후, 저장하면 아래와 같이 Deploy 준비가 되었다는 메시지가 나온다.

Screen Shot 2015-01-14 at 6.17.47 AM

이제 디테일한 내용들은 각자의 환경에 맞춰 정해주면 된다. 내 경우, Post Deployment Commands 에 chown -R www-data:www-data * 을 집어넣어 파일 소유자를 일괄 변경한다. 그럼 오늘은 여기까지만. ^^

Laravel 로 간단한 web crawler 만들기

spiderman-crawling
Standard

API 를 호출하여 JSON 이나 XML 포맷의 결과를 받아오는 경우가 아니라, HTML 로 구성된 웹페이지를 파싱해서 원하는 데이터 베이스를 구축할 필요가 있는 경우에는 웹크롤링 작업이 필요한데, 이번에는 간단한 웹 크롤러를 만들어본다.

이를 위해 사용한 패키지는 아래의 3가지 패키지이다.

  1. sunra/php-simple-html-dom-parser
  2. egeloen/google-map
  3. willdurand/geocoder

우선 composer.json 에 아래 라인을 추가하고, composer update 명령으로 디펜던시를 불러온다.

"sunra/php-simple-html-dom-parser": "v1.5.0",
"egeloen/google-map": "dev-master",
"willdurand/geocoder": "*",

아래는 골프장 정보를 모업체의 웹페이지 결과로 부터 받아서 추출하는 샘플 내용이다. 키포인트는 sunra/php-simple-html-dom-parser 의 사용으로, DOM element selector 가 jQuery 의 그것과 매우 유사하게 동작하여 매우 쉽게 이용할 수 있다는 것이다. 약간의 regular expression 과의 조합으로 훌륭한 web crawling 이 가능하다. 나머지 2개 패키지는 주소 값을 이용하여 그 위치의 위도/경도 값을 구해오는 geocoding 을 위해서 사용했다.

<?php

use Sunra\PhpSimple\HtmlDomParser;
use Ivory\GoogleMap\Services\Geocoding\Geocoder;
use Ivory\GoogleMap\Services\Geocoding\GeocoderProvider;
use Geocoder\HttpAdapter\CurlHttpAdapter;

class HomeController extends BaseController {

    public function home()
    {
        for($i = 1; $i <= 7; $i++) {
            $this->update($i);
        }
    }

    public function update($club_region)
    {
        $club_type = 3;

        // init the resource
        $url = sprintf("http://gcinfo.xxx.com/gz/searchcc/ajax/area/0000%d/1/1", $club_region);
        $postData = array(
            'pagesize' => '100',
            'membership' => $club_type + 1,
            'facility' => '0',
            'gfeeoption' => '0',
            'gfeefrom' => '0',
            'gfeeto' => '250000',
            'openfrom' => '1960',
            'opento' => '2014'
        );
        $postOutput = $this->postPage($url, $postData);

        $html = HtmlDomParser::str_get_html( $postOutput );
        $elements = $html->find('div[class=onc_cc_data]');

        $idx = 0;
        foreach ($elements as $element) {
            $idx++;

            $club_id = sprintf("%02d%02d%03d", $club_type, $club_region, $idx);
            echo $club_id;
            echo "\n";

            echo $club_type;
            echo "\n";

            echo $club_region;
            echo "\n";

            $href = $element->find('a', 0)->href;

            $pos = strripos($href, '/');
            $webidx = substr($href, $pos + 1);

            $url = 'http://gcinfo.xxx.com'.$href;
            $url2 = 'http://gcinfo.xxx.com/gz/ajax/html/searchcc/home/'.$webidx;
            $url3 = 'http://gcinfo.xxx.com/gz/ajax/html/searchcc/booking/'.$webidx;

            $info = trim($element->find('a', 1)->plaintext);
            $ret = preg_match("/\[(.+)\]\s+(\([A-Z]+\))([^|]+)\s+\|\s+(.+)/", $info, $matches);

            if (!$ret)
                dd('Fatal error. Can\'t match with the regular expression.');

            echo $matches[2];
            echo "\n";

            $region = $matches[1]; 
            echo $matches[1];
            echo "\n";

            $name = $matches[3]; 
            echo $matches[3];
            echo "\n";

            $holes = intval($matches[4]);
            echo intval($matches[4]);
            echo "\n";

            $info = trim($element->find('dd.course_name', 0)->plaintext);
            $info = str_replace(' ', '', $info);
            $courses = ($info) ?: 'n/a';
            echo $courses;
            echo "\n";

            $info = $element->find('dd.infophone', 0)->plaintext;
            $phone = trim(str_replace('문의전화 ', '', $info));
            echo ($phone);
            echo "\n";

            //
            // sub page
            //

            $getOutput = $this->getPage($url);
            $html2 = HtmlDomParser::str_get_html( $getOutput );

            $address = trim($html2->find('div.detail_gc table td', 0)->plaintext);
            echo ($address);
            echo "\n";

            $geocoder = new Geocoder();
            $geocoder->registerProviders(array(
                new GeocoderProvider(new CurlHttpAdapter()),
            ));
            $response = $geocoder->geocode($address);

            $latitude = 0;
            $longitude = 0;

            $results = $response->getResults();
            foreach ($results as $result) {
                $location = $result->getGeometry()->getLocation();
                $latitude = $location->getLatitude();
                $longitude = $location->getLongitude();
            }

            $detail = $html2->find('div.detail_gc a.btn_go_homepage', 0)->href;
            $webpage = ($detail) ?: 'n/a';
            echo $webpage;
            echo "\n";

            //
            // sub page
            //

            $getOutput2 = $this->getPage($url3);
            $html3 = HtmlDomParser::str_get_html( $getOutput2 );

            $weekday_fee = 0;
            $weekend_fee = 0;
            $cart_fee = 0;
            $caddie_fee = 0;

            if ($html3->find('div.greenfee_data td', 0)) {

                $detail = trim($html3->find('div.greenfee_data td', 0)->plaintext);
                $detail = trim(str_replace(',', '', $detail));
                $weekday_fee = intval($detail);
                echo $weekday_fee;
                echo "\n";

                $detail = trim($html3->find('div.greenfee_data td', 1)->plaintext);
                $detail = trim(str_replace(',', '', $detail));
                $weekend_fee = intval($detail);
                echo $weekend_fee;
                echo "\n";

                $detail = trim($html3->find('div.rounding_info li', 0)->plaintext);
                $detail = preg_replace("/\s+/", '', $detail);
                $detail = trim(str_replace('카트피:', '', $detail));
                $detail = trim(str_replace(',', '', $detail));
                $cart_fee = intval($detail);
                echo $cart_fee;
                echo "\n";

                $detail = trim($html3->find('div.rounding_info li', 1)->plaintext);
                $detail = preg_replace("/\s+/", '', $detail);
                $detail = trim(str_replace('캐디피:', '', $detail));
                $detail = trim(str_replace(',', '', $detail));
                $caddie_fee = intval($detail);
                echo $caddie_fee;
                echo "\n";

            } else {
                echo "0\n0\n0\n0\n";
            }

            // write into the DB table here
        }
    }


    public function postPage($url, $data) {
        // init the resource
        $ch = curl_init();

        curl_setopt_array($ch, array(
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => $data,
            CURLOPT_FOLLOWLOCATION => true
        ));

        $output = curl_exec($ch);

        // free
        curl_close($ch);

        return $output;
    }


    public function getPage($url) {
        // init the resource
        $ch = curl_init();

        curl_setopt_array($ch, array(
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
        ));

        $output = curl_exec($ch);

        // free
        curl_close($ch);

        return $output;
    }
}

Laravel 로 웹싸이트 개발하기 #2

devil
Standard

Stapler 패키지의 설명 내용과는 다르게, 여기서는 1-to-1 관계인 User 와 Avatar 를 사용하지 않고, 1-to-many 관계인 User 와 Picture 모델을 정의하여 사용한다. 현재 작업중인 프로젝트의 기술 명세서를 보면 접속한 회원이 다수의 사진을 자유롭게 올릴 수 있어야 한다. Jeffrey Way 의 Generator 를 이용하여 다음과 같이 Picture 모델의 마이그레이션을 생성한다.

php artisan generate:migration create_pictures_table --fields="user_id:integer, title:string:nullable, image_file_name:string, image_file_size:integer, image_content_type:string, image_updated_at:timestamp"

마이그레이션을 하면 DB 는 설정이 되었고, Picture 모델의 정의는 아래와 같이 했다.

<?php namespace Teeshot\Users;

use Codesleeve\Stapler\ORM\StaplerableInterface;
use Codesleeve\Stapler\ORM\EloquentTrait;
use Eloquent;

class Picture extends Eloquent implements StaplerableInterface {

    use EloquentTrait;

    protected $table = 'pictures';
    public $timestamps = true;
    protected $fillable = [ 'image' ];

    /**
     * Constructor
     */
    public function __construct(array $attributes = array()) {
        $this->hasAttachedFile('image', [
            'styles' => [
                'medium' => '320x320#',
                'thumb' => '100x100#'
            ]
        ]);

        parent::__construct($attributes);
    }

    public function user()
    {
        return $this->belongsTo('User');
    }
}

궁극적으론, 뷰에서 아래와 같은 형태의 접근이 가능한 시나리오다.

{{ $user->pictures[index]->image->url() }}
{{ $user->pictures[index]->image->url('thumb') }}

뷰 역시 추가되는 부분이 필요한데, DB 에 이미 기록된 이미지 파일들을 출력하기 위한 내용이다.

<div class="image-list">
    @if (count($pictures) > 0)
        @foreach ($pictures->chunk(4) as $pictureSet)
            <div class="row pictures">
                @foreach ($pictureSet as $picture)
                    <div class="col-sm-3 col-md-3 user-block">
                        <a class="btn btn-danger btn-small" href="#" rel="" rev="{{$picture->id}}"><span class="glyphicon glyphicon-remove" aria-hidden="true"></span></a>
                        <img src="{{ $picture->image->url('thumb') }}" />
                    </div>
                @endforeach
            </div>
        @endforeach
    @else
        <div class="row pictures"></div>
    @endif
</div>
<hr />

해당 뷰 하단에 JavaScript 함수도 아래와 같이 추가 변경한다.

$('#uploader').fileapi({
    url: '{{ route("users.upload") }}',
    accept: 'image/*',
    autoUpload: true,
    multiple: true,
    maxFiles: 5,
    maxSize: 6 * FileAPI.MB,
    imageTransform: { // resize by max side
        maxWidth: 800,
        maxHeight: 600
    },
    elements: {
        empty: { show: '.b-upload__hint' },
        list: '.js-files',
        file: {
            tpl: '.js-file-tpl',
            preview: { el: '.b-thumb__preview', width: 64, height: 64 },
            upload: { show: '.progress' },
            complete: { hide: '.progress' },
            progress: '.js-progress'
        }
    },
    onFileComplete: function (evt, uiEvt){
        var json = uiEvt.result;
        var element = '<div class="col-md-3 user-block"><img src="'+json.name+'"></div>';
        var lastDiv = $(".image-list > div:last");
        var count = lastDiv.children().length;

        if (count < 4) {
            lastDiv.append(element);
        } else {
            var newDiv = $('<div class="row pictures">');
            $(".image-list").append(newDiv);
            newDiv.append(element);
        }
        evt.widget.remove(uiEvt.file);
    }
});

이제 마지막으로 콘트롤러 UsersController 의 upload() 메서드를 다음과 같이 변경하고, 뷰에 필요한 객체 콜렉션을 넘겨줄 edit() 메서드도 추가한다.

public function upload()
{
    $file = Input::file('filedata');

    $picture = new Picture();
    $picture->image = $file;
    $picture->user_id = Auth::user()->id;
    $picture->save();

    return Response::json(['name' => $picture->image->url('thumb')], 200);

}

public function edit()
{
    $user = Auth::user();
    $pictures = $user->pictures()->get();
    return View::make('users.edit', compact('pictures'));
}

지금까지의 작업만으로 프론트앤드를 위한 FileAPI JQuery 라이브러리와 백앤드를 위한 Stapler 패키지가 조합되어 제법 쓸만한 파일 업로드 기능이 동작하게 된다. 이제 /app/config/packages/codesleeve/laravel-stapler 디렉토리에서 stapler.php 파일을 열어서, Stapler 의 스토리지 드라이버를 ‘storage’ => ‘filesystem’ 에서 ‘storage’ => ‘s3’ 로 바꾸면, 그 이후부터의 모든 업로드는 S3로 업로드 된다. 지금까지 구현된 내용의 스크린샷은 아래와 같다.

As simple as that.

Laravel 로 웹싸이트 개발하기 #1

devil
Standard

Laravel 로 웹싸이트 개발하기

2014년 12월 구로동 사무실에서

현재 모바일부터 웹애플리케이션에 이르는 풀스택 개발 프로젝트를 진행하고 있기에, 최신 기술 트랜드를 적용한 부분들 중 일부 내용을 블로그에 써보기로 했다. 웹개발 플랫폼은 Laravel 4.2 프레임웍을 근간으로하여, 온갖 오픈 소스 패키지들과, 유료 3rd party API 들을 최대한 가져다 쓰고 있다. 금번 프로젝트 개발의 백엔드 부분 개발은 물론이며, 프론트엔드 부분까지도 나혼자 스스로 결정해가면서 진행하고 있어서, 그간 안해봤던 내용들을 두루 경험해 볼 수 있는 좋은 시간이 되고 있다. 개를 보기만 하다가 먹어도 보는 그런 느낌? 암튼 그런 묘한… 그런게 있다. 이번 글에서 이야기할 내용은 사용자가 설정 페이지에서 다수의 사진들을 올리고 관리하기 위한, 사용자의 사진을 유지/관리하는데 필요한 내용들이다. 웹 애플리케이션에서 흔히들 많이 구현하는 그런 내용이 되겠다.

우선, 우연히 봤던 이 블로그 (http://revoltvisual.com/journal/5-must-have-laravel-4-packages) 에서 Laravel 5대 패키지 중 하나라고 언급된 Stapler 패키지를 사용한다. 딴 놈이 추천한 패키지를 사용한다는 것만으로도 시간 헛낭비하는게 아닌듯 싶어 뿌듯하다. 이 패키지를 이용해서 User 모델에 Photo 모델을 붙이는 과정을 정리해 보겠다. 특이한 점은, 이미지 파일 저장을 위해서 서버의 파일 시스템을 사용하지 않고, 아마존 S3 를 이용하려는 점이다. 부담스런 서버 호스팅 관리, 효율적인 파일 접근을 위한 디렉토리 구조 생성, 모델간의 연계성 상호 유지 등 관련된 여러가지 고민거리들을 한방에 해결하는 것이 목표다.

한방에 훅

우선 프론트엔드 부에서, 다수의 파일 업로드를 위해 선택한 라이브러리는 mailru/FileAPI 이다. 초기에는 (그래봤자 하루 전 이야기지만 ㅎㅎ) blueimp/jQuery-File-Upload 또는 enyo/dropzone 를 사용할까도 생각해 봤지만, 내가 선호하는 스타일로 커스터마이징에 보다 플렉서블한 FileAPI 데모 페이지를 보고 단숨에 미련없이 결정했다. 존나 멋지다. 근데, 예제의 HTML view 를 보니, 템플릿 엔진도 들어가 있고, 이거 뭐냐 다른 디펜던시가 또 있는건가? 생각하다 한참 헤맸다. 만든 사람도 있는데 셋업을 못한다면 얼마나 쪽팔린 일이냐… 근데 또 다른 디펜던시 그런건 없다. 그냥 예제처럼 쓰면 된다. 늘 그렇듯 오버하면 안된다.

우선 아래 bower 명령으로 jquery 용 FileAPI 를 가져온다.

bower install jquery.fileapi

이제 FileAPI 라이브러리를 이용하는 뷰가 필요하다. 아래는 현재 제작중인 프로젝트에서 사용하는 뷰에서 설명에 필요한 내용 일부를 발췌한 것이다. FileAPI 예제와 비교해 보면, 이 라이브러리의 사용법을 익히는데 많은 도움이 되리라본다.

@extends('layouts.default')

@section('stylesheets')
    <style>
        #uploader .btn {
            cursor: pointer;
            display: inline-block;
            position: relative;
            overflow: hidden;
        }
        #uploader .btn input
        {
            top: -10px;
            right: -40px;
            z-index: 2;
            position: absolute;
            cursor: pointer;
            opacity: 0;
            filter: alpha(opacity=0);
            font-size: 50px;
        }
        #uploader .b-thumb__name {
            font-size: 0.8em;
        }
    </style>
@stop

@section('content')
    <div class="canvas">
        <div class="page-header">
            <div>Title Header</div>
        </div>

        <div class="panel panel-default">
            <div class="panel-heading clearfix">
                <div class="panel-title pull-left">사진</div>
                <button type="button" class="btn btn-default btn-xs pull-right"><span class="glyphicon glyphicon-pencil"></span> 수정</button>
            </div>
            <div class="panel-body">

                <div class="row centered-form">
                    <div class="col-xs-12 col-sm-8 col-md-4 col-sm-offset-2 col-md-offset-4">
                        <div class="panel panel-warning">
                            <div class="panel-heading">
                                <h3 class="panel-title">더많은 사진 올리기</h3>
                            </div>
                            <div class="panel-body">
                                <div id="uploader">

                                  <div class="b-upload__hint">사진을 올리면 주목받을 수 있습니다.</div>
                                  <div class="js-files b-upload__files">
                                     <div class="js-file-tpl b-thumb" data-id="<%=uid%>" title="<%-name%>, <%-sizeText%>">
                                        <div class="b-thumb__preview">
                                           <div class="b-thumb__preview__pic"></div>
                                        </div>
                                        <div class="b-thumb__name"><%-name%></div>
                                        <div class="b-thumb__progress progress progress-striped progress-small active">
                                            <div class="js-progress progress-bar progress-bar-warning"></div>
                                        </div>
                                     </div>
                                  </div>
                                  <hr>
                                  <div class="btn btn-warning btn-small js-fileapi-wrapper">
                                     <span>불러오기</span>
                                     <input type="file" name="filedata">
                                  </div>

                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>

    </div>
@stop

@section('scripts')

    <script>
    window.FileAPI = {
          debug: true,
          staticPath: '/js/jquery.fileapi/FileAPI/' // path to *.swf
    };
    </script>
    <script src="/js/jquery.fileapi/FileAPI/FileAPI.min.js"></script>
    <script src="/js/jquery.fileapi/FileAPI/FileAPI.exif.js"></script>
    <script src="/js/jquery.fileapi/jquery.fileapi.js"></script>

    <script>
        $(function() {
            $('#uploader').fileapi({
                url: '{{ route("users.upload") }}',
                accept: 'image/*',
                autoUpload: true,
                multiple: true,
                imageTransform: { // resize by max side
                    maxWidth: 800,
                    maxHeight: 600
                },
                elements: {
                    empty: { show: '.b-upload__hint' },
                    emptyQueue: { hide: '.js-upload' },
                    list: '.js-files',
                    file: {
                        tpl: '.js-file-tpl',
                        preview: { el: '.b-thumb__preview', width: 64, height: 64 },
                        upload: { show: '.progress' },
                        complete: { hide: '.progress' },
                        progress: '.js-progress'
                    }
                }
            });
        });
    </script>

@stop

일단, 이번 회에는 서버 파일 시스템의 /upload 폴더에 파일 이름을 난수명으로 변경하여 저장하는 가장 일반적인 이미지 파일 처리 방법에 대해서 설명을 하고, 이후에 Stapler 패키지를 이용해서 /system 폴더에 저장하는 방법, 계속해서 서버의 파일 시스템이 아닌 아마존 S3를 사용하는 방법으로 진화시켜 보겠다. 서버로 올라간 파일에 대한 처리는 아래 컨트롤러의 upload 메서드에서 처리한다. 현재는, 단순히 /uploads 폴더에 난수명을 갖는 파일로 옮겨 저장한다.

<?php

use Teeshot\Users\UserRepository;

class UsersController extends \BaseController {

    /**
     * @var UserRepository
     */
    protected $userRepository;

    /**
     * @param UserRepository $userRepository
     */
    function __construct(UserRepository $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    public function upload()
    {
        $file = Input::file('filedata');

        $destinationPath = 'uploads';
        $filename  = str_random(16);
        $extension = $file->getClientOriginalExtension();
        $size      = $file->getSize();
        $fullName  = $filename.'.'.$extension;
        $upload_success = $file->move($destinationPath, $fullName);

        if( $upload_success ) {
            return Response::json(['name' => $fullName, 'size' => $size], 200);
        } else {
            return Response::json('error', 400);
        }
    }
}

이 방법이 이미지 파일 업로드를 위해서 가장 많이 사용하는 기초적인 방법이 되겠다. 물론 이렇게 사용해도 된다. 하지만, 아까도 말했듯이 고민해야할 문제들을 한방에 훅가도록 처리하기 위해서 Stapler 를 사용해 보자. 당장에 아마존S3 를 이용할 것은 아니지만, 궁극적인 목적이 아마존S3 를 이용하는 것이니, 필요한 내용을 미리 미리 셋업하면서 진행해 가겠다. Stapler 메뉴얼에 미안할 정도로 설명이 잘 나와있다. composer.json 에 다음 2줄을 추가하고 업데이트를 시켜서 해당파일 및 디펜던시들을 설치하면 일단, 백엔드쪽 준비는 끝이다.

"codesleeve/laravel-stapler": "1.0.*",
"aws/aws-sdk-php": "2.7.*"

아마존S3 가입하지 않은 사용자라면, 브라우저를 열어 가입하기로 한다. 아마존 AWS 에 가입 후, S3 메뉴를 선택하고 새로운 bucket을 생성한다. 다음은 IAM 메뉴를 선택하여 새로운 사용자 그룹을 만든다. 이 그룹이 갖을 퍼미션은 S3 에 full access 권한이다. 그 다음은 새로운 사용자를 만들어서 access credentials 를 받는다. php artisan config:publish codesleeve/laravel-stapler 명령으로 생성한 config 파일들 중 s3.php 에 필요한 user credentials 값들이므로 카피해 놓는다. region 값은 역시 Stapler 설명서에 잘 나와 있듯이 http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region 에서 찾아보면 된다. 사용자를 생성한 뒤에는 반드시 그 사용자 그룹을 바로 전단계에서 생성한 사용자 그룹으로 설정시킨다. 이로써 이 아마존 S3 에 full access 권한을 갖는 사용자가 탄생하게 되었다.

다음 회에서는 Stapler 를 사용하여, 지정된 모델과 연동하는 이미지 파일들을 자동으로 생성처리하는 법을 살펴보기로 하겠다.

PHP 로 카카오톡 및 네이버 계정으로 로그인 구현

naver-kakao
Standard

Laravel 로 웹싸이트 개발하기

2014년 11월 역삼동 사무실에서

금번에 진행하는 프로젝트의 회원가입 요구사항 중 하나는 Facebook 이나 Twitter 계정으로 로그인이 아니라, 국내에서 많은 사용자층을 확보한, 카카오톡과 네이버 계정으로 로그인을 이용하는 기능의 추가 이다. 예전에 번역했었던 Laravel 웹 애플리케이션 개발이란 책에서도 사용법이 언급되어 있는 hybridauth 패키지를 이용하면, 이미 구현되어있는 다양한 OAuth/OAuth2 클라이언트들을 쉽게 가져다가 쓸 수 있는데, 이번에 그 패키지를 확장해서, 카카오톡과 네이버 계정으로 로그인 기능을 이용할 수 있는 클래스를 작성했습니다. 필요하신 분은 https://github.com/jinseokoh/additional-providers 에서 다운로드 하기 바랍니다.

kakaonaver

Google URL shortener for iOS

apple
Standard

My 2nd public iOS Class on GitHub

I just wrote a class myself to use Google URL Shortener API on iOS devices. This one uses AFNetworking to avoid messy delegate pattern comes with the NSURLConnection API. I couldn’t find any Google URL shortener API class available for iOS. So, this one would definitely help you if you are looking for one using simple and clean block pattern. Here’s the GitHub url. https://github.com/jinseokoh/iOS-GoogleURLShortener

StupidTextField for iOS

apple
Standard

1st public iOS Class on GitHub

Not too long ago I released A UITextField Subclass with a custom keyboard (input view) called StupidTextField. This UITextView subclass comes with a custom keyboard that randomly changes key-board layout everytime it’s activated. Not sure if it’s useful to anyone. But, the project I worked on has a requirement spec like this. It is available here at https://github.com/jinseokoh/StupidTextField.