django channels 는 웹소켓을 django project 에서 사용할 수 있게 해주는 도구입니다. 웹소켓 관련 유명 도구로는 Socket.io 가 있습니다. 그러나 사용언어는 파이썬이 아닌 자바스크립트입니다. channels는 최근 정식으로 django project 안에 들어가게 되었습니다. 

django project 의 channels 공식 채택 : https://www.djangoproject.com/weblog/2016/sep/09/channels-adopted-official-django-project/

channels 의 공식문서 : https://channels.readthedocs.io/en/latest/


1. channels를 쓰기위한 준비


django channels를 쓰기 위해선 몇 가지 파이썬 패키지를 다운받아야 합니다.

그 전에 파이썬 가상환경을 설정합니다.


$ mkdir example-channels
$ cd example-channels
$ python3.6 -m venv myenv
$ source myenv/bin/activate
cs


그 다음에 pip를 이용해서 패키지를 다운받습니다.


(myenv)$ pip install django==1.10.5 channels==1.0.2 asgi_redis==1.0.0
cs


asgi_redis는 redis를 쓸 경우에 필요하겠지만 이 경우엔 redis를 다운받아서 실행되는지 확인해보고 설치하는 것을 추천합니다. 그렇지 않다면 asgi_redis는 굳이 다운받지 않아도 됩니다. 다른 방법으로도 실행되게 할 수 있습니다. 만약 실행된다면 프로젝트와 앱을 생성합니다. 


(myenv)$ django-admin.py startproject example
(myenv)$ cd example
(myenv)$ python manage.py startapp exam
(myenv)$ python manage.py migrate
cs


2. django 설정


그 후 settings.py 에서 exam 앱과 channels를 등록합니다.


INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'channels',
    'exam',
]
cs


settings.py 에 DATABASE 설정과 모양이 비슷한 설정을 넣어줍니다.


CHANNEL_LAYERS = {
    'default': {
        'BACKEND''asgi_redis.RedisChannelLayer',
        'ROUTING''exam.routing.channel_routing',
    }
}
cs


여기에 등록한대로 exam 앱에 routing.py를 만들어서 channel_routing을 만들어줍니다. 

그 전에 consumers.py는 이렇게 해줍니다. consumers.py는 exam앱 안에 위치시키는 것을 추천합니다.


from channels import Group
 
def ws_connect(message):
    Group('users').add(message.reply_channel)
 
def ws_disconnect(message):
    Group('users').discard(message.reply_channel)   
cs


그 다음에 routing.py를 만들어줘야 합니다. urls.py와 비슷한 역할을 수행합니다. websocket용 urls.py 라고 생각하시면 됩니다. 


from channels.routing import route
from exam.consumers import ws_connect, ws_disconnect
 
 
channel_routing = [
    route('websocket.connect', ws_connect),
    route('websocket.disconnect', ws_disconnect),
]   
cs


3. client에서 실행


이제 jquery를 이용해 websocket을 클라이언트에서 실행해줍니다. 클라이언트는 django 의 templates에 위치한 html 파일에서 동작합니다.


  <script>
    var socket = new WebSocket('ws://' + window.location.host + '/users/');
 
    socket.onopen = function open() {
      console.log('created.');
    };
 
    if (socket.readyState == WebSocket.OPEN) {
      socket.onopen();
    }
  </script>
cs


기본적인 설정이 끝났습니다. runserver 한 후 클라이언트 상에서 위 페이지에 접속하면 로그가 뜰 것입니다.


 WebSocket HANDSHAKING /users/
 WebSocket DISCONNECT /users/
cs


이런식으로 나오면 됩니다.

django 는 모델을 만들고 그 모델을 이용해 object를 만듭니다. object를 가져오는 방법은 여러가지가 있습니다. 그 중 object가 존재하지 않을 경우의 에러를 다루는 방법으로는 DoesNotExist의 도움을 받는 방법을 고를 수 있습니다.

from django.shortcuts import get_object_or_404, get_list_or_404
 
cs

물론 이런 shortcuts 를 가져와서 처리하는 방법도 있습니다. 하지만 이번엔 DoesNotExist를 써 보겠습니다.


1. DoesNotExist 사용방법

try:
    model_your_model = ModelYourModel.objects.get(you=happy)
except ModelYourModel.DoesNotExist:
    pass
cs


이렇게 DoesNotExist 를 이용해서 예외 구문을 만들게 됩니다.

pass 라고 쓴 자리에 예외가 발생할 경우 하고 싶은 걸 작성하면 됩니다. 


2. Backend 사용방법


그리고 django에서 authenticate 함수를 쓸 일이 있습니다. 이건 인증 함수입니다. settings.py에서 그 흔적을 찾을 수 있습니다.


AUTHENTICATION_BACKENDS = (
    'django.contrib.auth.backends.ModelBackend',
)
cs

이 부분입니다. 이 부분이 django 의 authentication 을 관장합니다. 이 ModelBackend를 살펴보겠습니다.

 

class ModelBackend:
 
    def authenticate(self, request, username=None, password=None, **kwargs):
        if username is None:
            username = kwargs.get(UserModel.USERNAME_FIELD)
        try:
            user = UserModel._default_manager.get_by_natural_key(username)
        except UserModel.DoesNotExist:
            UserModel().set_password(password)
        else:
            if user.check_password(password) and self.user_can_authenticate(user):
                return user
 
    def get_user(self, user_id):
        try:
            user = UserModel._default_manager.get(pk=user_id)
        except UserModel.DoesNotExist:
            return None
        return user if self.user_can_authenticate(user) else None
cs


authenticate 함수가 username 과 password 를 받아서 user를 return 하는 것을 알 수 있습니다. 

그러므로 이 backend를 받은 다음에 authenticate 함수를 재정의해 주면 인증에 사용할 수 있게 됩니다.


from django.contrib.auth.backends import ModelBackend
 
class CustomAuthBackend(ModelBackend):
 
    def authenticate(self, username=None, password=None):
        pass
cs


이런 식으로 모양을 맞춰주면 쓸만할 것입니다. 원하는대로 수정하여 사용가능합니다. 비밀번호만을 이용한 로그인도 구현할 수 있을 것입니다.

recaptcha 는 악의적인 공격을 막기 위한 도구입니다. 구글에서 제공하며 사진, 글자 등으로 로봇과 사람을 구별합니다.

https://www.google.com/recaptcha/ 에서 확인 가능합니다. 


1. views.py 설정

django 에서 recaptcha를 쓰려면 django의 views.py 에서 사용하면 됩니다.

url을 이용해서 request와 response를 다뤄야 하므로 몇 가지를 import해서 불러옵니다.


import urllib
from urllib.parse import urlparse
cs


urllib은 파이썬에서 제공하는 url관련 패키지입니다. urlparse는 그 중에서도 url을 바탕으로 파싱하는 작업을 수행하는 모듈입니다.

이렇게 쓸 도구를 불러와서 서버쪽에서 recaptcha와 데이터를 주고받아서 판단하면 됩니다. 

가입은 구글에서 다른 글들을 검색하면 잘 설명되어 있습니다.


        recaptcha_response = request.POST.get('g-recaptcha-response')
        url = 'https://www.google.com/recaptcha/api/siteverify'
        values = {
            'secret': settings.GOOGLE_RECAPTCHA_SECRET_KEY,
            'response': recaptcha_response
        }
        recaptcha_data = urllib.parse.urlencode(values).encode()
        recaptcha_req = urllib.request.Request(url, data=recaptcha_data)
        recaptcha_response = urllib.request.urlopen(recaptcha_req)
        recaptcha_result = json.loads(recaptcha_response.read().decode())
 
        if not recaptcha_result['success']:
cs


여기서 settings.GOOGLE_RECAPTCHA_SECRET_KEY 자리에 각자 부여받은 키를 쓰면 됩니다.

개발용 서버에서 이용하고 싶을 땐 127.0.0.1을 쓰면 자연스럽게 이용가능합니다.


2. html 템플릿 설정


html 태그에선 header등에 <script></script>를 써 주고 구글이 알려준 코드를 쓰면 됩니다.


<script src='https://www.google.com/recaptcha/api.js'></script>
cs

이것을 써주면 됩니다.


<div class="g-recaptcha" data-sitekey="some value that google gave to you"></div>
 
cs


html 템플릿에는 위의 div를 써주면 recaptcha를 유저가 클라이언트에서 볼 수 있고 클릭할 수 있게 됩니다.

자바는 세계적으로 매우 많이 이용되는 언어입니다. 

자바 : https://java.com/ko/ 

컴파일로 실행되는 언어이며 이용자가 많고 커뮤니티도 활발하여 자료가 많이 존재합니다. 스마트폰의 운영체제인 안드로이드가 기반으로 삼고 있는 언어이기도 합니다. 자바의 초급 단계로 계산기 역할을 하는 helloworld를 만들어보겠습니다.


1. helloworld.java

helloworld.java를 만들어 보겠습니다.

public class HelloWorld {
 
    public static void main(String[] args) {
 
        int a = 21;
        int b = 23;
        Integer insA = new Integer(21);
        Integer insB = new Integer(23);
        
        System.out.println("(bbb) int a: " + a + ", b: " + b);
        System.out.println("(bbb) Integer a: " + a + ", b: " + b);
        System.out.println();
        
        swap(a, b);
        swap(insA, insB);
        System.out.println();
        
        System.out.println("(aaa) int a: " + a + ", b: " + b);
        System.out.println("(aaa) Integer a: " + a + ", b: " + b);
    }
}
cs


int a 는 숫자 변수를 담는 자료형입니다. 그리고 System.out.println()은 괄호 안의 값을 출력하는 함수입니다. 계산 결과를 처리하여 출력하도록 설정했는데 위의 결과와 아래의 결과가 다릅니다. 왜냐하면 java는 아래의 swap같은 함수 처리를 해야 정확히 포인터를 계산해서 출력하기 때문입니다. 


2. swap 함수 작성

swap 함수를 따로 만들겠습니다.


    private static void swap(int a, int b){
        int tttaa = a;
        a = b;
        b = tttaa;
        System.out.println("(funn) int a: " + a + ", b: " + b);
    }
    private static void swap(Integer a, Integer b){
        Integer tttaa = new Integer(a);
        a = b;
        b = tttaa;
        System.out.println("(funn) Integer a: " + a + ", b: " + b);
    }
cs


이렇게 하면 함수를 만들게 됩니다. 스왑함수는 println을 포함합니다. 즉각적으로 바로 확인할 수 있게 하기 위해서입니다.

static void는 리턴되는 값이 없음을 의미합니다. 


3. 문자 입력받기

    private static int scanInt(){
        Scanner scn = new Scanner(System.in);
        int result = scn.nextInt();
        scn.close();
        return result;
    }
cs

그 다음 scanInt를 만들어서 숫자를 읽어들일 수 있도록 합니다. Scanner(System.in)은 키보드를 통한 입력을 받는 함수입니다. 자바에서 기본적으로 제공합니다.


public class Unknown1 {
    private int[] input;
    private int n;
    }
}
cs


이 함수를 통해 System.in을 한 번 살펴보겠습니다

.

    public Unknown1(){
        Scanner scnned = new Scanner(System.in);
         String[] array = scnned.nextLine().split(" ");
         input = new int[array.length];
         for(int i = 0; i < array.length; i++){
             input[i] = Integer.parseInt(array[i]);
         }
         scnned.close();
         
         n = input.length;
    }
cs


이 함수를 저기 위의 함수 안에 넣어주도록 합니다. ArrayList를 이용해서 주어진 숫자를 조작하는 함수입니다. 입력받는 인자는 여러번 연속적으로 주어질 수 있으므로 array 를 이용하여 작성합니다. 그러므로 for문을 이용해서 인자가 입력받을 때마다 array에 문자열을 입력하게 됩니다.


1. models.py


models.py에서 사전설정을 합니다.


GAME_STATUS_CHOICES = (
    ("F""First turn Player"),
    ("S""Second turn Player"),
    ("W""First Player has Win"),
    ("L""Second Player has Win"),
    ("D""Draw")
)
cs


views.py에서 보내진 인자에 따라 models.py를 결정할 수 있게 됩니다.

각각 머리글자를 따서 튜플 형태로 작성합니다.


class GamesQuerySet(models.QuerySet):
    def games_user(self, user):
        return self.filter(
            Q(first_player=user) | Q(second_player=user)
        )
 
    def is_active(self):
        return self.filter(
            Q(status='F'| Q(status='S')
        )
 
    def drew_games(self):
        return self.filter(status='D')
cs

여기서 Q를 쓰고 filter를 바로 써 줄 수 있습니다. 좀 더 편리한 코드 작성이 가능합니다.


class Game(models.Model):
    first_turn_player = models.ForeignKey(User, on_delete=models.CASCADE, related_name="first_player")
    second_turn_player = models.ForeignKey(User, on_delete=models.CASCADE, related_name="second_player")
 
    start_datetime = models.DateTimeField(auto_now_add=True)
    end_datetime = models.DateTimeField(auto_now=True)
    status = models.CharField(max_length=1default='F', choices=GAME_STATUS_CHOICES)
 
    objects = GamesQuerySet.as_manager()
 
    def __str__(self):
        return "{0} vs {1}".format(self.first_player, self.second_player)
cs


데이터베이스 사용시엔 필연적으로 model을 지정해야 합니다.

manager도 사용해봤습니다. 그 후 이동에 관련된 모델을 지정합니다.


class Move(models.Model):
    x = models.IntegerField()
    y = models.IntegerField()
    comment = models.CharField(max_length=300, blank=True)
    by_first_player = models.BooleanField()
 
    game = models.ForeignKey(Game, on_delete=models.CASCADE)
cs


이렇게 이동하는 동작을 모델로 지정해줘도 괜찮습니다. 데이터베이스를 이용할 것이니 모델로 지정해주는 것이 ORM사용에도 유리할 것입니다.

유저의 기본정보를 묻는 모델도 만듭니다.

질문 모델은 이런식으로 할 수 있을 것입니다.


class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('published')
 
    def recent_published(self):
        now = timezone.now()
        return now - datetime.timedelta(days=1<= self.pub_date <= now
 
    recent_published.admin_order_field = 'pub_date'
    recent_published.boolean = True
    recent_published.short_description = 'recently created'
 
    def __str__(self):
        return self.question_text
cs


def recent_published(self):를 이용하여 최근에 작성되었는지에 대한 여부를 나타내는 함수를 지정했습니다.

유저는 여기에 있는 질문을 받게 됩니다. 그건 views.py를 이용해서 작성해주시면 됩니다. 

2. admin.py

admin에는 ModelAdmin class를 추가해줍니다.


@admin.register(Game)
class GameAdmin(admin.ModelAdmin):
    display = ("id""first_turn_player""second_turn_player""status")
    editable = ("status", )
 
admin.site.register(Move)
cs


게임플레이 자체는 따로 앱을 지정해서 만듭니다. views.py는 아래와 같이 쓸 수 있을 것입니다.


3. 플레이 관련 앱 - views.py


from gameplay.models import Game
 
@login_required
def home(request):
    my_games = Game.objects.games_for_user(request.user)
    active_games = my_games.active()
    drew_games = my_games.drew_games()
 
    return render(request, "home.html", {"num_games": Game.objects.count(),
                                                "games": active_games,
                                                "drew_games": drew_games})
cs


로그인한 사용자만 이용할 수 있게 한 것입니다.

active()와 drew_games()는 새로 지정해줘야합니다.

render시에 num_games에 게임의 객체의 수를 넣어줍니다. home.html에서 사용될 것입니다.


4. html 템플릿


부트스트랩을 쓰면 템플릿은 이렇게 할 수 있을 것입니다. 기본 로그인 화면입니다. 나머지 게임 화면은 원하시는대로 만드시면 됩니다. 기본 로그인 화면은 예시에 불과합니다.


    <body>
    <nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
      <div class="container">
        <div class="navbar-header">
          <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
            <span class="sr-only">Toggle navigation - 보이지 않는 요소입니다.</span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
          </button>
          <class="navbar-brand" href="/">게임 제목 입니다.</a>
        </div>
        <div id="navbar" class="navbar-collapse collapse">
          <form class="navbar-form navbar-right" role="form">
            <div class="form-group">
<p><h2>이메일</h2></p>
              <input type="text" placeholder="Email" class="form-control">
            </div>
            <div class="form-group">
<p><h2>패스워드</h2></p>
              <input type="password" placeholder="Password" class="form-control">
            </div>
            <button type="submit" class="btn btn-success">Sign in or Login</button>
          </form>
        </div>
      </div>
    </nav>
 
    <div class="container">
 
        {% block content %}
        {% endblock %}
 
    </div>



이번엔 GridLayout을 위한 adapter를 만들어보도록 하겠습니다. 그렇게 큰 차이는 없으나 이번엔 Retrofit를 이용한 통신까지 적용하겠습니다.


1. ViewHolder 지정


public class FlowerAdapter_grid extends RecyclerView.Adapter<FlowerAdapter_grid.Holder> {
 
    private static final Object TAG = FlowerAdapter_grid.class.getSimpleName();
    private final FlowerClickListener mListener;
    private List<Flower> mFlowers;
    public FlowerAdapter_grid(FlowerClickListener listener) {
        mFlowers = new ArrayList<>();
        mListener = listener;
    }
}
cs

GridLayout도 일종의 RecyclerView입니다. 그러므로 RecyclerView에서 GridLayout을 위한 뷰홀더를 작성할 것입니다. 그러기 위해선 ViewHolder의 extends가 필요합니다. 변수지정은 따로 언급하지 않겠습니다.


    @Override
    public Holder onCreateViewHolder(ViewGroup parent, int viewType) {
        View row = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_item_grid, parent, false);
        return new Holder(row);
    }
cs

ViewHolder가 작성될 때 바로 layout에 적용될 수 있게 LayoutInflater.from(parent.getContext()).inflate(R.layout.row_item_grid, parent, false) 이 부분이 필요합니다. 그리고 지난시간에도 그랬듯이 ViewHolder처리 과정에서 Glide도 처리해줍니다. 

    @Override
    public void onBindViewHolder(Holder holder, int position) {
        Flower currFlower = mFlowers.get(position);
        Glide.with(holder.itemView.getContext())
                .load(currFlower.getImage())
                .thumbnail(0.5f)
                .crossFade()
                .into(holder.mImage);
    }
cs


이번엔 Glide를 thumbnail로 쓸 것입니다. 0.5f는 원본의 0.5만큼의 수준으로 thumbnail로 사용한다는 뜻입니다. crossfade()를 통해 애니메이션 효과도 부여할 수 있습니다.


    @Override
    public int getItemCount() {
        return mFlowers.size();
    }
 
    public void addFlower(Flower flower) {
        mFlowers.add(flower);
        notifyDataSetChanged();
    }
cs


이렇게 포스트의 사이즈를 가져오거나 새로운 포스트를 추가할 수 있습니다. 지난번과 코드가 동일합니다.


    public void clear(){
        mFlowers.clear();
        notifyDataSetChanged();
    }
 
    public Flower getSelectedFlower(int position) {
        return mFlowers.get(position);
    }
cs


포스트를 클리어할 때 사용됩니다. 지난번과 같은 코드입니다.


public class MyPagerAdapter extends FragmentPagerAdapter {
 
    public Fragment[] arrFragments;
    public MyPagerAdapter(FragmentManager fm, Fragment[] arrFragmnets) {
        super(fm);
        this.arrFragments = arrFragmnets;
    }
 
    @Override
    public Fragment getItem(int position) {
        return arrFragments[position];
    }
 
    @Override
    public int getCount() {
        return arrFragments.length;
    }
}
cs


이렇게 Fragment에 Adapter를 달아줍니다. 이제 여기선 apiservice도 작성할 것입니다. 위에선 Fragment getItem() 에서 arrFragments의 position 값을 return 합니다. 포스트의 수를 세기 위해 getCount()를 작성했습니다. arrFragments.length가 전체 포스트의 수를 반영합니다.


2. apiService 작성


public interface FlowerApiService {
 
    @GET("posts/")
    Call<List<Flower>> getAllFlowers(@Query("user_lat"String lat, @Query("user_lon"String lon);
}
cs


다른 apiservice 도 만들도록 합니다. apiService는 사전에 만들어둠으로써 나중에 retrofit 통신을 수월하게 해 줍니다. 포스트들의 위도와 경도값을 받는 apiservice를 만들겠습니다.


public interface LoginApiService {
 
    String string = "client_Id";
 
    @POST("rest-auth/login/")
    Call<MyKey> getget(@Body LoginData loginData);
}
cs

이것은 로그인을 위한 apiService입니다. 로그인시에 필요한 데이터를 받습니다. 어디로 데이터를 전송할지 url을 설정할 수 있습니다. 저는 @POST("rest-auth/login/")에서 나와있듯 host주소/rest-auth/login/으로 통신할 것입니다. 


public interface PostApiService {
 
    @Multipart
    @POST("posts/")
    Call<ResponseBody> uploadFile (@Part MultipartBody.Part part, @Part("point") RequestBody pointString, @Part("text") RequestBody textString);
}
cs


이 것은 포스트를 위한 apiService입니다. 이 것을 통해 글의 등록이 가능합니다. 주소가 바뀌었습니다. 위의 주소와 다른 주소로 통신합니다.

그 다음은 유저 등록을 위한 부분입니다. 유저 등록을 위한 주소가 따로 존재합니다.


public interface RegistrationApiService {
    @Multipart
    @POST("rest-auth/registration/")
    Call<ResponseBody> registersecond (@Part("username") RequestBody username, @Part("email") RequestBody email, @Part("password1") RequestBody password1, @Part("password2") RequestBody password2);
}
cs


유저 등록시에 비밀번호와 비밀번호 재확인을 위해 두 번 변수를 입력받기 때문에 @Part 부분이 여러개로 나눠져있습니다. 


3. REST Manager 작성

저는 서버와 클라이언트의 REST통신을 이용하고 있으므로 REST 통신을 위한 apiservice도 만들어주면 편리합니다.

서버에서 http요청의 header값을 살펴보게 되므로 그 header값을 자동으로 지정해주는 RestManager를 작성하겠습니다.



public class RestManager {
 
    static Context mContext;
 
    private FlowerApiService mFlowerApiService;
 
    public FlowerApiService getmFlowerApiService(Context context) {
        if (mFlowerApiService==null) {
 
            mContext = context;
 
            OkHttpClient client = new OkHttpClient();
            OkHttpClient.Builder builder = new OkHttpClient.Builder();
            builder.interceptors().add(new AddCookiesInterceptor(mContext));
            client = builder.build();
 
            Retrofit retrofit = new Retrofit.Builder()
                    .client(client)
                    .baseUrl(Constants.BASE_URL)
                    .addConverterFactory(GsonConverterFactory.create())
                    .build();
 
            mFlowerApiService = retrofit.create(FlowerApiService.class);
        }
 
        return mFlowerApiService;
    }
}
cs

이러면 자동으로 okhttpclient에 헤더와 바디를 추가해줍니다. 원하는 대로 원하는 값을 넣을 수 있으므로 필요하신 대로 작성하셔서 쓰면 됩니다. GsonConverterFactory는 구글에서 제공하는 json데이터 처리용 도구입니다. retrofit를 이용해서 통신을 할 때마다 쉽게 헤더를 조절할 수 있기에 여러가지 만들어두고 사용하시면 편리할 것입니다.


지난 포스트들에서 자주 사용된 Adapter를 설명하겠습니다. Adapter는 데이터 모델들을 원하는 상황에 적용할 수 있게 도와줍니다.


1. 준비물

먼저 만들었던 flower 모델이랑 flowerapi를 가져옵니다. post를 다루기 위한 모델과 api입니다. 덤으로 Glide도 가져옵니다. 여기서 Glide를 처리해 주어야합니다.


import com.bumptech.glide.Glide;
import com.example.keepair.myapplication.R;
import com.example.keepair.myapplication.model.Flower;
cs


2. Adapter ViewHolder 작성

그 다음에 Adapter랑 RecyclerView.Adapter를 만들어줍니다. RecyclerView를 사용하지 않는다면 다른 뷰를 extends 해도 괜찮습니다. Adapter에서 제공하는 Holder를 불러와서 사용합니다.


public class FlowerAdapter extends RecyclerView.Adapter<FlowerAdapter.Holder> {
}
cs

Adapter에 Holder를 붙여줘야 합니다. 그렇지 않으면 제대로 작동하지 않습니다.


    private static final Object TAG = FlowerAdapter.class.getSimpleName();
    private final FlowerClickListener mListener;
    private List<Flower> mFlowers;
cs


이렇게 변수를 지정해줍니다. getSimpleName()은 지난번에 설명한 글에 설명되어 있습니다.

http://codehanry.tistory.com/16 을 참고하시면 됩니다.


    public FlowerAdapter(FlowerClickListener listener) {
        mFlowers = new ArrayList<>();
        mListener = listener;
    }
cs


Adapter에 쓸 ArrayList를 만들도록 합니다. 포스트들의 양이 많으므로 추가하기 쉽게 ArrayList를 사용합니다.


    @Override
    public void onBindViewHolder(Holder holder, int position) {
 
        Flower currFlower = mFlowers.get(position);
 
        holder.mAuthor.setText(currFlower.getAuthor());
 
        holder.mText.setText(currFlower.getText());
        Glide.with(holder.itemView.getContext())
                .load(currFlower.getImage())
                .override(300300)
                .centerCrop()
                .into(holder.mImage);
    }
cs


여기서 뷰홀더를 작성합니다. 뷰홀더는 Adapter에서 Holder를 가져왔으므로 onBindViewHolder를 사용할 수 있게 되었습니다. position을 가져와서 currFlower에 넣어줍니다. position 값은 recyclerView에서의 포스트 위치를 의미하는 인자입니다. 글라이드 옵션도 조절할 수 있습니다. override()를 이용해 크기와 사진의 질을 조정합니다.


다음은 포스트를 클릭할 때의 기능을 구현합니다.


    public class Holder extends RecyclerView.ViewHolder implements View.OnClickListener {
 
        private ImageView mImage;
        private TextView mAuthor, mText;
 
        public Holder(View itemView) {
 
            super(itemView);
 
            mImage = (ImageView) itemView.findViewById(R.id.iv_photo);
            mAuthor = (TextView) itemView.findViewById(R.id.tv_author);
            mText = (TextView) itemView.findViewById(R.id.tv_text);
 
            itemView.setOnClickListener(this);
        }
    }
 
cs


RecyclerView에 결합시킬 것이므로 ViewHolder를 RecyclerView에서 가져오게 됩니다. implements를 통해 onClickListener를 구현하게 됩니다. 


        @Override
        public void onClick(View view) {
            mListener.onClick(getLayoutPosition());
        }
cs


이 부분을 추가해줘야 합니다. 그러면 클릭시에 Position을 가져오는 기능을 담당합니다. 이런 구조에서는 Position의 위치값이 중요하게 여겨집니다. 이게 글 번호를 의미한다고 생각해도 됩니다.


3. 포스트 세부설정 


    @Override
    public int getItemCount() {
        return mFlowers.size();
    }
 
    public void addFlower(Flower flower) {
        mFlowers.add(flower);
        notifyDataSetChanged();
    }
cs

여기서 포스트의 사이즈를 고르고 포스트의 추가를 결정합니다. 포스트의 추가는 자동적으로 이루어지게 됩니다. 앞서 설명한 infinite Scroll과 연관됩니다.


    public void clear(){
        mFlowers.clear();
        notifyDataSetChanged();
    }
 
    public Flower getSelectedFlower(int position) {
        return mFlowers.get(position);
    }
cs


여기서 clear()는 데이터에 변화가 있을 시에 작동합니다. 혹은 변화를 줄 시에 포스트들을 초기화하는 역할을 합니다.

    public interface FlowerClickListener {
        void onClick(int position);
    }

cs


포스트를 클릭했을 시에 위치값을 이용해 그 포스트에 관련한 동작을 수행하고 싶다면 작성합니다. interface를 통해 포스트에 추가적인 동작을 부여할 수 있습니다.

1. settings.py

GeoDjango를 사용하기 위해선 INSTALLED_APPS 와 site_id를 설정해주어야 합니다.


INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django.contrib.sites',
    'django.contrib.gis',
    'blog',
    'rest_framework',
    'rest_framework.authtoken',
    'rest_auth',
    'allauth',
    'allauth.account',
    'rest_auth.registration',
]
SITE_ID = 1
cs


이렇게 rest_framework랑 allauth를 쓰기 위해선 SITE_ID = 1 을 써줘야 합니다.

그리고 현재 사용하고 있는 rest_auth를 위해 settings.py 에 추가할 것이 또 있습니다.


REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.TokenAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.AllowAny',
),
}


이렇게 추가해줘야 token으로 로그인이 가능합니다. 데이터베이스도 gis기능이 있는 걸로 바꿔줘야 합니다. postgresql의 postgis를 쓰겠습니다. 그러면 settings.py에 명시하도록 합니다. postgis 는 postgresql에서 위치 데이터를 위해 개조된 버전이라고 생각됩니다.

postgis : https://postgis.net/


DATABASES = {
'default': {
'ENGINE': 'django.contrib.gis.db.backends.postgis',
'NAME': 'postgres',

'USER': 'foryoumydarling',

'PASSWORD': 'darlingdarling',

'HOST': '127.0.0.1',
'PORT': '5432',
}
}

이런식으로 database도 gis시스템에 맞게 설정해줘야 하는 것을 잊으면 안 됩니다. 127.0.0.1은 로컬호스트를 의미합니다. 컴퓨터 한 대로 다 실행할 것이니 127.0.0.1 로 지정해줍니다.


그 다음엔 url을 정리합니다.

2. urls.py

    url(r'^accounts/', include('allauth.urls')),
    url(r'^rest_auth/', include('rest_auth.urls')),
    url(r'^rest-auth/registration/', include('rest_auth.registration.urls')),
cs


나머지는 원하는 대로 만드시면 됩니다. 아래는 제 코드입니다.


    url(r'^$', TemplateView.as_view(template_name="home.html"), name='home'),
    url(r'^signup/$', TemplateView.as_view(template_name="signup.html"),
        name='signup'),
    url(r'^email-verification/$',
        TemplateView.as_view(template_name="email_verification.html"),
        name='email-verification'),
    url(r'^login/$', TemplateView.as_view(template_name="login.html"),
        name='login'),
    url(r'^logout/$', TemplateView.as_view(template_name="logout.html"),
        name='logout'),
    url(r'^password-reset/$',
        TemplateView.as_view(template_name="password_reset.html"),
        name='password-reset'),
    url(r'^password-reset/confirm/$',
        TemplateView.as_view(template_name="password_reset_confirm.html"),
        name='password-reset-confirm'),
 
cs


이런식으로도 url을 테스트 해 봤습니다. user_details.html은 제가 만들었습니다.

나머지 템플릿도 취향따라 바꾸셔도 됩니다. TemplateView.as_view가 모든 url에 적용되었으니 views.py에서 할 일은 상당히 줄어들 것입니다.


    url(r'^user-details/$',
        TemplateView.as_view(template_name="user_details.html"),
        name='user-details'),
    url(r'^password-change/$',
        TemplateView.as_view(template_name="password_change.html"),
        name='password-change'),
cs


이것도 써보려고 했습니다. password-reset 시에 url에 pk인자를 넘겼습니다. django 2.0 버전에선 pk 인자를 넘기는 방법이 조금 달라졌으니 유의하시기 바랍니다.


    url(r'^password-reset/confirm/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
        TemplateView.as_view(template_name="password_reset_confirm.html"),
        name='password_reset_confirm'),
 
    url(r'^rest-auth/', include('rest_auth.urls')),
    url(r'^rest-auth/registration/', include('rest_auth.registration.urls')),
    url(r'^account/', include('allauth.urls')),
    url(r'^admin/', include(admin.site.urls)),
    url(r'^accounts/profile/$', RedirectView.as_view(url='/', permanent=True), name='profile-redirect'),
cs


include를 통해 전체적인 url구조를 결정했습니다.

이렇게 까지 설정하니 더 설정할 것이 없었습니다. serializers.py를 만들겠습니다.


3. serializers.py

기본적으로 rest_framework에서 제공하는 serializers 모듈을 가져옵니다. serializer는 json같은 형태로 데이터를 변경시켜주는 역할을 합니다.

from rest_framework import serializers
from blog.models import Post
from django.contrib.auth.models import User
from drf_extra_fields.geo_fields import PointField
cs


serializer class를 만들도록 합니다.


class UserSerializer(serializers.ModelSerializer):
    related_postwriter = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
    class Meta:
        model = User
        fields = ('username''email''related_postwriter')
cs


User 모델을 위한 serializer입니다. django의 form과 모양이 상당히 닮아있습니다.


class PostSerializer(serializers.ModelSerializer):
    author = serializers.ReadOnlyField(source='author.username')
    point = PointField(required=True) #이것은 선택사항, 사용자가 결정할 일이므로 신중하게 선택하자.
    class Meta:
        model = Post
        # fields = ('author', 'text', 'image', )
        fields = ('author''text''image''point''created_date')
    def create(self, validated_data):
        validated_data['author'= self.context['request'].user
        return super(PostSerializer, self).create(validated_data)
cs


create 같은 함수를 따로 지정해주어 어떻게 REST통신시에 serializer가 함수를 처리할 지 지정할 수 있습니다.

Post 모델을 위한 serializer입니다.

import 를 잊지 맙시다.


이렇게하면 django-rest-framework의 기본 설정이 끝납니다. 

끝으로, drf_extra_fields는 구글에 검색하면 다운받을 수 있습니다. Point를 편리하게 조작할 수 있게 해 줍니다.


drf_extra_fields : https://github.com/Hipo/drf-extra-fields

준비물

1. 우선 import해야 할 것들이 있습니다.

레이아웃에 필요한 위젯들을 가져옵니다.

import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
cs


이렇게 하면 GridLayoutManager 혹은 LinearLayoutManager를 통해 Activity를 조작할 수 있게 됩니다. 구글에서 제공하는 것이니 믿고 써도 됩니다.


2. 그 전에 만들어 놓은 Adapter들을 가져옵니다.

import com.example.keepair.myapplication.adapter.FlowerAdapter;
import com.example.keepair.myapplication.adapter.FlowerAdapter_grid;
cs


이것들은 나중에 설명할 것입니다. 지금은 여러 데이터들이 GridLayoutManager와 연동될 수 있도록 도와주는 역할을 한다고 알면 될 것 같습니다.


3. 레이아웃으로부터 값들을 가져옵니다.

화면에서 보여지는 것들의 값들을 가져올 필요가 있습니다.
RestManager는 따로 만든 것으로 나중에 다시 포스팅할 것입니다.

    private SwipeRefreshLayout swipeContainer_red;
    private RecyclerView mRecyclerView;
    private RestManager mRestManager;
    private FlowerAdapter_grid mFlowerAdapter;
    private TextView mCoordinatesTextGrid;
cs


2. 함수 작성

4. GridLayout을 위한 onCreateView를 구현합니다.

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_red, container, false);
        swipeContainer_red = (SwipeRefreshLayout) view.findViewById(R.id.swipeContainer_red);
        setRetainInstance(true);
 
        mCoordinatesTextGrid = (TextView) view.findViewById(R.id.tv_coordinates_grid);
        getGridPosts();
        swipeContainer_red.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                getGridPosts();
            }
        });
        configViews(view);
 
        return view;
    }
cs


OnCreateView를 통해 처음에 GridLayout을 불러올 시에 구성될 화면을 구현할 수 있습니다. SwifeRefreshLayout을 사용했습니다.

5. Posts를 가져올 함수를 지정합니다.

    private void getGridPosts() {
}

cs

이제 이 안에 글들을 가져와서 처리만 하면 됩니다.

6. 위에서 구현한 함수를 더 자세히 지정합니다.

위 getGridPosts() 안에 들어갈 내용입니다.

        ReferSharedPreference preferenceCoordinates = new ReferSharedPreference(getContext());
        String lat = preferenceCoordinates.getValue("Lat", "13");
        String lon = preferenceCoordinates.getValue("Lon", "15");
        mCoordinatesTextGrid.setText(lat + "  , " + lon);
        mRestManager = new RestManager();
        Call<List<Flower>> listCall = mRestManager.getmFlowerApiService(getActivity()).getAllFlowers(lat, lon);
        listCall.enqueue(new Callback<List<Flower>>() {
            @Override
            public void onResponse(Call<List<Flower>> call, Response<List<Flower>> response) {
                if (response.isSuccessful()) {
                    mFlowerAdapter.clear();
                    List<Flower> flowerList = response.body();
                    for(int i =0; i<flowerList.size(); i++) {
                        Flower flower = flowerList.get(i);
                        mFlowerAdapter.addFlower(flower);
                    }
                    swipeContainer_red.setRefreshing(false);
                }
            }
            @Override
            public void onFailure(Call<List<Flower>> call, Throwable t) {
            }
        });
cs


여기서 lat과 lon은 좌표값을 가져오는 것입니다. 이번에도 좌표값이 주어지지 않았다면 기본적으로 (13, 15) 좌표를 찾게 됩니다. 그런 다음 만들어놓은 ApiService를 이용해 비동기통신을 하여 그것을 Adapter를 이용해 추가하게 됩니다. Flower를 Post라고 생각하면 편합니다. 그 후 마지막으로 setRefreshing을 false로 설정해 자동적으로 새로고침을 반복하지 않도록 막아줍니다. 나머지 코드는 앞서 설명한 Retrofit의 Call함수입니다. 포스트에 FlowerList를 사용했습니다.


7. 환경설정을 조금만 더 해줍니다.

이렇게하면 RecyclerView의 여러가지 설정을 할 수 있습니다.

RecyclerView가 고정된 사이즈로 3개 항목을 한 줄에 나타내게끔 만들었습니다. 유행하는 스타일입니다.


    private void configViews(View view) {
        mRecyclerView = (RecyclerView) view.findViewById(R.id.rv_flower_red);
        mRecyclerView.setHasFixedSize(true);
        mRecyclerView.setRecycledViewPool(new RecyclerView.RecycledViewPool());
        mRecyclerView.setLayoutManager(new GridLayoutManager(getActivity().getApplicationContext(), 3));
        mFlowerAdapter = new FlowerAdapter_grid(this);
        mRecyclerView.setAdapter(mFlowerAdapter);
    }
cs

8. 포스트를 클릭했을 때의 이벤트를 설정합니다.

    @Override
    public void onClick(int position) {
        Flower selectedFlower = mFlowerAdapter.getSelectedFlower(position);
        Intent intent = new Intent(getContext(), DetailActivity.class);
        intent.putExtra(Constants.REFERENCE.FLOWER, selectedFlower);
        startActivity(intent);
    }
cs



이렇게하면 GridLayout Fragment를 완성합니다. 마지막에 intent로 포스트 클릭시에 DetailActivity 가 실행되도록 했습니다. DetailActivy는 다음에 알아보겠습니다.

1. 준비물

이번엔 register 기능을 구현할 것입니다.

먼저 몇가지를 import하겠습니다. 그 중 okhttp3의 mediatype을 이용할 것입니다. 나머지는 유저 등록을 위한 모델들입니다.


import com.example.keepair.myapplication.apiservice.RegistrationApiService;
import com.example.keepair.myapplication.helper.Constants;
import com.example.keepair.myapplication.loginhelper.ReferSharedPreference;
import com.example.keepair.myapplication.model.MyKey;
 
import okhttp3.MediaType;
cs


이렇게 하면 requestBody를 따로 만들 수 있게 됩니다. okhttp의 requestBody를 만들기 위해 MediaType이 필요합니다. request의 POST 요청을 위해 사용하는 것입니다.


필요한 정보를 가져옵니다. 저장되어있는 정보도 괜찮고 새로 불러올 정보도 괜찮습니다.


    MyKey keygoven;
 
    RegistrationApiService mRegistrationApiService;
    EditText mUsernameRegistrationEditText;
    EditText mPasswordRegistration1EditText;
    EditText mPasswordRegistration2EditText;
    ImageView mRegisterButton;
    RelativeLayout mLayoutRegstration;
 
    ReferSharedPreference mSavedUserInfo;
cs


저는 SharedPreference 의 변형인 ReferSharedPreference를 쓸 것입니다.


2. 함수 구현

그 다음 onCreate를 작성합니다.


    @Override
    protected void onCreate(Bundle savedInstanceState) {
    }
cs


여기 안에 필요한 코드들을 넣겠습니다.


        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.activity_registration);
        Display display = ((WindowManager) getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
        int width = (int) (display.getWidth() * 0.999); //Display 사이즈
        int height = (int) (display.getHeight() * 0.9);  //Display 사이즈
        getWindow().getAttributes().width = width;
        getWindow().getAttributes().height = height;
cs


이렇게 하면 기본 Activity의 사이즈를 정하게 됩니다. display 되는 사이즈라고 생각하면 됩니다. 안드로이드 화면의 크기를 조절하는 것입니다. getWidth()와 getHeight()를 사용했습니다. 지난 포스트에서는 0.7과 0.9를 사용했지만 이번엔 최대 크기를 잡기 위해 비율을 1에 가깝게 했습니다.


그 다음 레이아웃과 저장된 SharedPreference로부터 값을 불러와서 사용될 값으로 넣어주겠습니다.


        mSavedUserInfo = new ReferSharedPreference(getApplicationContext());
        mUsernameRegistrationEditText = (EditText) findViewById(R.id.et_username_registration);
        mPasswordRegistration1EditText = (EditText) findViewById(R.id.et_password_registration_1);
        mPasswordRegistration2EditText = (EditText) findViewById(R.id.et_password_registration_2);
        mRegisterButton = (ImageView) findViewById(R.id.btn_registration);
        mLayoutRegstration = (RelativeLayout) findViewById(R.id.layoutRegistration);
cs


유저 등록에 필요한 정보들은 모두 가져오게 되었습니다. 그 다음엔 이걸 POST요청으로 전송할 수 있게 onClickListener()를 작성합니다.


                final String givenRegistrationUserName = mUsernameRegistrationEditText.getText().toString();
                final String givenRegistrationPassword1 = mPasswordRegistration1EditText.getText().toString();
                String givenRegistrationPassword2 = mPasswordRegistration2EditText.getText().toString();
                String givenEmail = "";
cs


쓸 값들을 변수에 넣어서 준비합니다. okhttp client에서 사용될 것입니다.

그 다음엔 조건에 맞는다면 전송하는 부분을 작성합니다. 항상 등록을 허가할 순 없으니 조건을 구성합니다. if문을 사용했습니다.


                if(givenRegistrationUserName.length() >= 3){
                    if(givenRegistrationPassword1.equals(givenRegistrationPassword2)){
                    }
                }
cs


username의 길이와 password의 일치여부를 확인한 후 둘 다 True면 실행되는 코드입니다.


                            OkHttpClient client = new OkHttpClient();
                            OkHttpClient.Builder builder = new OkHttpClient.Builder();
                            client = builder.build();
 
                            Retrofit retrofit = new Retrofit.Builder()
                                    .client(client)
                                    .addConverterFactory(GsonConverterFactory.create())
                                    .baseUrl(Constants.BASE_URL)
                                    .build();
 
                            mRegistrationApiService = retrofit.create(RegistrationApiService.class);
 
cs


Api Client를 만듭니다. 그리고 request 보내기 전 데이터를 수집합니다. Retrofit가 편리하게 사용됩니다.


                            RequestBody username =
                                    RequestBody.create(
                                            MediaType.parse("multipart/form-data"), givenRegistrationUserName);
                            RequestBody email =
                                    RequestBody.create(
                                            MediaType.parse("multipart/form-data"), givenEmail);
                            RequestBody password1 =
                                    RequestBody.create(
                                            MediaType.parse("multipart/form-data"), givenRegistrationPassword1);
                            RequestBody password2 =
                                    RequestBody.create(
                                            MediaType.parse("multipart/form-data"), givenRegistrationPassword2);
cs


사용자가 입력한 정보를 RequestBody에 넣어 서버로 전송할 것입니다. 이렇게 하면 requestBody 구성이 완료됩니다.

이제 직접 서버와 통신하는 부분을 작성하겠습니다. 지난 시간의 Call 요청을 좀 더 자세하게 구현합니다.


 
                            Call<ResponseBody> call = mRegistrationApiService.registersecond(username, email, password1, password2);
                            call.enqueue(new Callback<ResponseBody>() {
                                @Override
                                public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
                                    if (response.isSuccessful()){
                                        Toast.makeText(getApplicationContext(), "Success", Toast.LENGTH_LONG).show();
                                        mSavedUserInfo.put("SavedUserName", givenRegistrationUserName);
                                        mSavedUserInfo.put("SavedPassword", givenRegistrationPassword1);
                                        Intent intent = new Intent(RegistrationActivity.this, LoginActivity.class);
                                        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
                                        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                                        startActivity(intent);
                                    }
                                    else {
                                        Toast.makeText(getApplicationContext(), "Something wrong, Would you check password conditions?", Toast.LENGTH_LONG).show();
                                    }
                                }
 
                                @Override
                                public void onFailure(Call<ResponseBody> call, Throwable t) {
 
                                }
                            });
cs


이렇게하여 Retrofit의 Call도 구현했습니다. 유저 등록이 끝나면 다시 LoginActivity로 이동하도록 intent를 만들었습니다. intent에서 현재의 activity를 효과적으로 제거할 수 있게 FLAG들을 사용했습니다.


이 다음엔 failure를 구현하면 됩니다. 위에선 response에서의 response.isSuccessful() 과 else가 있고 따로 onFailure()가 존재하지만 원하는 대로 작성하시면 됩니다.

+ Recent posts