장고 마스터의 길: 제네릭 뷰 16가지로 웹 개발 효율성 극대화하기

안녕하세요, 열정 가득한 Django 개발자 여러분! 오늘은 정말 흥미진진한 주제를 가지고 왔습니다. 바로 “제네릭 뷰 16가지”에 대해 깊이 있게 파헤쳐 볼 건데요.

여러분도 아시다시피, Django로 웹 개발을 하다 보면 반복되는 코드 때문에 좌절감을 느낄 때가 있죠. 하지만 걱정 마세요! Django의 제네릭 뷰를 마스터하면 그런 고민은 눈 녹듯이 사라질 거예요.

이 포스트에서는 Django의 제네릭 뷰 16가지 각각에 대해 자세히 알아보고, 실제 코드 예제와 함께 어떻게 활용할 수 있는지 살펴볼 거예요. 초보자부터 중급자까지 모두가 이해할 수 있도록 쉽게 설명해드릴 테니, 끝까지 함께해 주세요!

제네릭 뷰 16가지
( Django 제네릭 뷰 16가지 )

Django 제네릭 뷰의 마법: 16가지 뷰 완전 정복

Django의 제네릭 뷰는 웹 개발에서 자주 사용되는 패턴을 추상화하여 개발자의 생산성을 크게 향상시키는 마법 같은 도구입니다. 이 제네릭 뷰 16가지를 마스터하면, 여러분의 Django 개발 실력은 한 단계 더 업그레이드될 거예요. 자, 이제 하나씩 살펴볼까요?

이 테이블은 Django의 제네릭 뷰16가지를 한눈에 볼 수 있게 정리한 것입니다. 각 뷰의 분류, 이름, 그리고 주요 기능을 간단히 설명하고 있어 제네릭 뷰의 전체적인 구조와 용도를 쉽게 파악할 수 있습니다.

제네릭 뷰 분류제네릭 뷰 이름뷰의 기능(역할)
Base ViewView기본 뷰 클래스, HTTP 메서드에 따른 처리
Base ViewTemplateView지정된 템플릿 렌더링
Base ViewRedirectView다른 URL로 리다이렉트
Generic Display ViewListView객체 목록 표시
Generic Display ViewDetailView단일 객체의 상세 정보 표시
Generic Edit ViewFormView폼 처리
Generic Edit ViewCreateView새로운 객체 생성
Generic Edit ViewUpdateView기존 객체 수정
Generic Edit ViewDeleteView객체 삭제
Generic Date ViewArchiveIndexView날짜별 최신 객체 목록 표시
Generic Date ViewYearArchiveView특정 년도의 객체 목록 표시
Generic Date ViewMonthArchiveView특정 월의 객체 목록 표시
Generic Date ViewWeekArchiveView특정 주의 객체 목록 표시
Generic Date ViewDayArchiveView특정 일의 객체 목록 표시
Generic Date ViewTodayArchiveView오늘 날짜의 객체 목록 표시
Generic Date ViewDateDetailView특정 날짜의 객체 상세 정보 표시

1. View: 모든 것의 시작점

View는 모든 클래스 기반 뷰의 기본이 되는 클래스입니다. 다른 모든 제네릭 뷰들이 이 View 클래스를 상속받아 만들어졌죠. 간단한 예제를 통해 View의 사용법을 알아봅시다.

from django.views import View
from django.http import HttpResponse

class MyView(View):
    def get(self, request):
        return HttpResponse('Hello, Django world!')

    def post(self, request):
        return HttpResponse('POST Request.')

코드 해설:

  1. django.views에서 View 클래스를 임포트합니다.
  2. HttpResponse를 임포트하여 HTTP 응답을 반환할 수 있게 합니다.
  3. MyView 클래스를 정의하고 View 클래스를 상속받습니다.
  4. get 메서드를 정의하여 GET 요청을 처리합니다.
  5. post 메서드를 정의하여 POST 요청을 처리합니다.

이 간단한 예제만으로도 View 클래스의 강력함을 느낄 수 있죠? HTTP 메서드에 따라 다른 로직을 쉽게 구현할 수 있습니다.

2. TemplateView: 정적 페이지의 강자

TemplateView는 단순히 템플릿을 렌더링하는 데 사용됩니다. 동적 데이터 처리가 필요 없는 정적 페이지를 만들 때 아주 유용하죠.

from django.views.generic import TemplateView

class HomePageView(TemplateView):
    template_name = "home.html"

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['welcome_message'] = "Welcome to Django."
        return context

코드 해설:

  1. django.views.generic에서 TemplateView를 임포트합니다.
  2. HomePageView 클래스를 정의하고 TemplateView를 상속받습니다.
  3. template_name 속성을 설정하여 사용할 템플릿을 지정합니다.
  4. get_context_data 메서드를 오버라이드하여 추가적인 컨텍스트 데이터를 전달합니다.
  5. super()를 호출하여 기본 컨텍스트 데이터를 가져옵니다.
  6. ‘welcome_message’ 키로 환영 메시지를 컨텍스트에 추가합니다.

이렇게 하면 ‘home.html’ 템플릿에서 {{ welcome_message }}를 사용하여 환영 메시지를 표시할 수 있습니다. 정말 간단하죠?

3. RedirectView: URL 이동의 마법사

RedirectView는 사용자를 다른 URL로 리다이렉트할 때 사용합니다. URL 단축이나 오래된 URL을 새 URL로 리다이렉트할 때 아주 유용하답니다.

from django.views.generic import RedirectView

class OldUrlRedirectView(RedirectView):
    url = '/new-awesome-page/'
    permanent = True

코드 해설:

  1. django.views.generic에서 RedirectView를 임포트합니다.
  2. OldUrlRedirectView 클래스를 정의하고 RedirectView를 상속받습니다.
  3. url 속성을 설정하여 리다이렉트할 대상 URL을 지정합니다.
  4. permanent를 True로 설정하여 영구적인 리다이렉트(301)를 사용합니다.

이 뷰를 사용하면 오래된 URL로 접근하는 사용자를 자동으로 새로운 페이지로 안내할 수 있습니다. SEO에도 좋고, 사용자 경험도 개선할 수 있죠!

4. ListView: 목록 표시의 챔피언

ListView는 객체들의 목록을 표시할 때 사용하는 제네릭 뷰입니다. 블로그 포스트 목록이나 제품 카탈로그 같은 것을 만들 때 아주 유용하죠.

from django.views.generic import ListView
from .models import Book

class BookListView(ListView):
    model = Book
    template_name = 'book_list.html'
    context_object_name = 'books'
    paginate_by = 10

    def get_queryset(self):
        return Book.objects.filter(is_published=True).order_by('-publication_date')

코드 해설:

  1. django.views.generic에서 ListView를 임포트합니다.
  2. 사용할 모델(Book)을 임포트합니다.
  3. BookListView 클래스를 정의하고 ListView를 상속받습니다.
  4. model 속성을 설정하여 사용할 모델을 지정합니다.
  5. template_name으로 사용할 템플릿을 지정합니다.
  6. context_object_name으로 템플릿에서 사용할 객체 리스트의 이름을 지정합니다.
  7. paginate_by로 페이지당 표시할 객체 수를 설정합니다.
  8. get_queryset 메서드를 오버라이드하여 커스텀 쿼리셋을 반환합니다.

이 뷰를 사용하면 출판된 책들의 목록을 최신순으로 정렬하여 페이지네이션과 함께 표시할 수 있습니다. 정말 편리하죠?

5. DetailView: 상세 정보의 달인

DetailView는 단일 객체의 상세 정보를 표시할 때 사용합니다. 예를 들어, 특정 블로그 포스트나 제품의 상세 페이지를 만들 때 아주 유용합니다.

from django.views.generic import DetailView
from .models import Book

class BookDetailView(DetailView):
    model = Book
    template_name = 'book_detail.html'
    context_object_name = 'book'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['related_books'] = Book.objects.filter(author=self.object.author).exclude(pk=self.object.pk)[:3]
        return context

코드 해설:

  1. django.views.generic에서 DetailView를 임포트합니다.
  2. 사용할 모델(Book)을 임포트합니다.
  3. BookDetailView 클래스를 정의하고 DetailView를 상속받습니다.
  4. model, template_name, context_object_name 속성을 설정합니다.
  5. get_context_data 메서드를 오버라이드하여 추가 컨텍스트 데이터를 제공합니다.
  6. 같은 저자의 다른 책들을 관련 책으로 추가합니다 (최대 3개).

이 뷰를 사용하면 책의 상세 정보와 함께 관련된 다른 책들도 함께 표시할 수 있습니다. 사용자 경험을 한층 더 개선할 수 있죠!

6. FormView: 폼 처리의 마법사

FormView는 폼 처리를 위한 제네릭 뷰입니다. 사용자로부터 데이터를 입력받고 처리해야 할 때 아주 유용하답니다.

from django.views.generic import FormView
from django.urls import reverse_lazy
from .forms import ContactForm

class ContactFormView(FormView):
    template_name = 'contact.html'
    form_class = ContactForm
    success_url = reverse_lazy('contact_success')

    def form_valid(self, form):
        form.send_email()
        return super().form_valid(form)

코드 해설:

  1. django.views.generic에서 FormView를 임포트합니다.
  2. reverse_lazy 함수를 임포트하여 URL 역참조를 지연 평가합니다.
  3. 사용할 폼 클래스(ContactForm)를 임포트합니다.
  4. ContactFormView 클래스를 정의하고 FormView를 상속받습니다.
  5. template_name, form_class, success_url 속성을 설정합니다.
  6. form_valid 메서드를 오버라이드하여 폼이 유효할 때의 동작을 정의합니다.
  7. 폼의 send_email 메서드를 호출하여 이메일을 보냅니다.

이 뷰를 사용하면 연락처 폼을 쉽게 처리할 수 있습니다. 폼이 유효하면 이메일을 보내고 성공 페이지로 리다이렉트하죠.

7. CreateView: 객체 생성의 마법사

CreateView는 새로운 객체를 생성할 때 사용하는 제네릭 뷰입니다. 예를 들어, 새로운 블로그 포스트를 작성하거나 새 제품을 추가할 때 사용할 수 있죠.

from django.views.generic import CreateView
from django.urls import reverse_lazy
from .models import Book

class BookCreateView(CreateView):
    model = Book
    fields = ['title', 'author', 'description', 'publication_date']
    template_name = 'book_form.html'
    success_url = reverse_lazy('book-list')

    def form_valid(self, form):
        form.instance.created_by = self.request.user
        return super().form_valid(form)

코드 해설:

  1. django.views.generic에서 CreateView를 임포트합니다.
  2. reverse_lazy 함수와 Book 모델을 임포트합니다.
  3. BookCreateView 클래스를 정의하고 CreateView를 상속받습니다.
  4. model, fields, template_name, success_url 속성을 설정합니다.
  5. form_valid 메서드를 오버라이드하여 폼이 유효할 때의 동작을 정의합니다.
  6. 현재 로그인한 사용자를 책의 생성자로 설정합니다.

이 뷰를 사용하면 새로운 책을 데이터베이스에 추가할 수 있습니다. 폼이 제출되면 자동으로 현재 로그인한 사용자를 책의 생성자로 설정하고, 책 목록 페이지로 리다이렉트합니다.

8. UpdateView: 객체 수정의 마법사

UpdateView는 기존 객체를 수정할 때 사용하는 제네릭 뷰입니다. 블로그 포스트를 수정하거나 제품 정보를 업데이트할 때 아주 유용하죠.

from django.views.generic import UpdateView
from django.urls import reverse_lazy
from .models import Book

class BookUpdateView(UpdateView):
    model = Book
    fields = ['title', 'author', 'description', 'publication_date']
    template_name = 'book_update.html'

    def get_success_url(self):
        return reverse_lazy('book-detail', kwargs={'pk': self.object.pk})

    def form_valid(self, form):
        form.instance.updated_by = self.request.user
        return super().form_valid(form)

코드 해설:

  1. django.views.generic에서 UpdateView를 임포트합니다.
  2. reverse_lazy 함수와 Book 모델을 임포트합니다.
  3. BookUpdateView 클래스를 정의하고 UpdateView를 상속받습니다.
  4. model, fields, template_name 속성을 설정합니다.
  5. get_success_url 메서드를 오버라이드하여 동적으로 성공 URL을 생성합니다.
  6. form_valid 메서드에서 현재 사용자를 업데이트한 사용자로 설정합니다.

이 뷰를 사용하면 기존 책 정보를 쉽게 수정할 수 있어요. 수정이 완료되면 해당 책의 상세 페이지로 리다이렉트됩니다. 편리하죠?

9. DeleteView: 객체 삭제의 달인

DeleteView는 객체를 삭제할 때 사용하는 제네릭 뷰입니다. 불필요한 데이터를 제거할 때 아주 유용하답니다.

from django.views.generic import DeleteView
from django.urls import reverse_lazy
from django.contrib.auth.mixins import UserPassesTestMixin
from .models import Book

class BookDeleteView(UserPassesTestMixin, DeleteView):
    model = Book
    template_name = 'book_confirm_delete.html'
    success_url = reverse_lazy('book-list')

    def test_func(self):
        return self.request.user.is_staff or self.get_object().created_by == self.request.user

코드 해설:

  1. django.views.generic에서 DeleteView를 임포트합니다.
  2. UserPassesTestMixin을 임포트하여 권한 체크를 수행합니다.
  3. BookDeleteView 클래스를 정의하고 UserPassesTestMixin, DeleteView를 상속받습니다.
  4. model, template_name, success_url 속성을 설정합니다.
  5. test_func 메서드를 오버라이드하여 삭제 권한을 체크합니다.

이 뷰를 사용하면 책을 안전하게 삭제할 수 있어요. 스태프 사용자나 책을 생성한 사용자만 삭제할 수 있도록 권한을 제한했습니다. 보안도 챙기고 기능도 챙기고, 일석이조네요!

10. ArchiveIndexView: 날짜별 아카이브의 마법사

ArchiveIndexView는 날짜별로 정렬된 객체의 최신 목록을 표시할 때 사용합니다. 블로그 아카이브 페이지를 만들 때 아주 유용하죠.

from django.views.generic.dates import ArchiveIndexView
from .models import BlogPost

class BlogArchiveIndexView(ArchiveIndexView):
    model = BlogPost
    date_field = "published_date"
    template_name = "blog_archive.html"
    context_object_name = "latest_posts"
    allow_future = False
    paginate_by = 10

코드 해설:

  1. django.views.generic.dates에서 ArchiveIndexView를 임포트합니다.
  2. BlogPost 모델을 임포트합니다.
  3. BlogArchiveIndexView 클래스를 정의하고 ArchiveIndexView를 상속받습니다.
  4. model, date_field, template_name, context_object_name 속성을 설정합니다.
  5. allow_future를 False로 설정하여 미래 날짜의 포스트를 표시하지 않습니다.
  6. paginate_by로 페이지당 표시할 포스트 수를 설정합니다.

이 뷰를 사용하면 블로그 포스트를 날짜별로 정렬하여 최신 순으로 표시할 수 있어요. 페이지네이션까지 자동으로 처리해주니 정말 편리하죠?

11. YearArchiveView: 연도별 아카이브의 달인

YearArchiveView는 특정 년도의 객체 목록을 표시할 때 사용합니다. 연간 보고서나 연도별 블로그 포스트를 표시할 때 유용해요.

from django.views.generic.dates import YearArchiveView
from .models import BlogPost

class BlogYearArchiveView(YearArchiveView):
    model = BlogPost
    date_field = "published_date"
    make_object_list = True
    template_name = "blog_year_archive.html"
    year_format = '%Y'

코드 해설:

  1. django.views.generic.dates에서 YearArchiveView를 임포트합니다.
  2. BlogPost 모델을 임포트합니다.
  3. BlogYearArchiveView 클래스를 정의하고 YearArchiveView를 상속받습니다.
  4. model, date_field, template_name 속성을 설정합니다.
  5. make_object_list를 True로 설정하여 해당 년도의 모든 객체 목록을 생성합니다.
  6. year_format으로 년도 형식을 지정합니다.

이 뷰를 사용하면 특정 년도의 블로그 포스트를 쉽게 표시할 수 있어요. URL에서 년도를 받아 해당 년도의 포스트만 보여주니 아카이브 기능을 구현하기 아주 좋죠!

12. MonthArchiveView: 월별 아카이브의 마법사

MonthArchiveView는 특정 월의 객체 목록을 표시할 때 사용합니다. 월간 보고서나 월별 블로그 포스트를 표시하기에 딱이에요.

from django.views.generic.dates import MonthArchiveView
from .models import BlogPost

class BlogMonthArchiveView(MonthArchiveView):
    model = BlogPost
    date_field = "published_date"
    template_name = "blog_month_archive.html"
    month_format = '%m'
    allow_future = False

코드 해설:

  1. django.views.generic.dates에서 MonthArchiveView를 임포트합니다.
  2. BlogPost 모델을 임포트합니다.
  3. BlogMonthArchiveView 클래스를 정의하고 MonthArchiveView를 상속받습니다.
  4. model, date_field, template_name 속성을 설정합니다.
  5. month_format으로 월 형식을 지정합니다.
  6. allow_future를 False로 설정하여 미래 월의 포스트를 표시하지 않습니다.

이 뷰를 사용하면 특정 월의 블로그 포스트를 간편하게 표시할 수 있어요. URL에서 년도와 월을 받아 해당 월의 포스트만 보여주니 월별 아카이브 기능을 구현하기 아주 좋습니다!

자, 여기까지 12개의 제네릭 뷰에 대해 알아보았습니다. 나머지 4개의 뷰도 계속해서 살펴볼까요?

13. WeekArchiveView: 주간 아카이브의 달인

WeekArchiveView는 특정 주의 객체 목록을 표시할 때 사용합니다. 주간 보고서나 주별 블로그 포스트를 표시하기에 아주 유용해요.

from django.views.generic.dates import WeekArchiveView
from .models import BlogPost

class BlogWeekArchiveView(WeekArchiveView):
    model = BlogPost
    date_field = "published_date"
    template_name = "blog_week_archive.html"
    week_format = "%W"
    allow_future = False

코드 해설:

  1. django.views.generic.dates에서 WeekArchiveView를 임포트합니다.
  2. BlogPost 모델을 임포트합니다.
  3. BlogWeekArchiveView 클래스를 정의하고 WeekArchiveView를 상속받습니다.
  4. model, date_field, template_name 속성을 설정합니다.
  5. week_format으로 주 형식을 지정합니다 (%W는 연중 몇 번째 주인지를 나타냅니다).
  6. allow_future를 False로 설정하여 미래 주의 포스트를 표시하지 않습니다.

이 뷰를 사용하면 특정 주의 블로그 포스트를 쉽게 표시할 수 있어요. 주간 리뷰나 주간 요약 기능을 구현할 때 아주 유용하답니다!

14. DayArchiveView: 일별 아카이브의 마법사

DayArchiveView는 특정 날짜의 객체 목록을 표시할 때 사용합니다. 일일 보고서나 특정 날짜의 블로그 포스트를 표시하기에 딱이에요.

from django.views.generic.dates import DayArchiveView
from .models import BlogPost

class BlogDayArchiveView(DayArchiveView):
    model = BlogPost
    date_field = "published_date"
    template_name = "blog_day_archive.html"
    month_format = '%m'
    allow_future = False
    context_object_name = 'posts'

코드 해설:

  1. django.views.generic.dates에서 DayArchiveView를 임포트합니다.
  2. BlogPost 모델을 임포트합니다.
  3. BlogDayArchiveView 클래스를 정의하고 DayArchiveView를 상속받습니다.
  4. model, date_field, template_name 속성을 설정합니다.
  5. month_format으로 월 형식을 지정합니다.
  6. allow_future를 False로 설정하여 미래 날짜의 포스트를 표시하지 않습니다.
  7. context_object_name으로 템플릿에서 사용할 객체 리스트의 이름을 지정합니다.

이 뷰를 사용하면 특정 날짜의 블로그 포스트를 간편하게 표시할 수 있어요. 일일 다이어리나 특정 날짜의 이벤트를 보여줄 때 아주 유용하답니다!

15. TodayArchiveView: 오늘의 아카이브 달인

TodayArchiveView는 오늘 날짜의 객체 목록을 표시할 때 사용합니다. 오늘의 뉴스나 오늘 발행된 블로그 포스트를 표시하기에 아주 좋아요.

from django.views.generic.dates import TodayArchiveView
from .models import BlogPost

class BlogTodayArchiveView(TodayArchiveView):
    model = BlogPost
    date_field = "published_date"
    template_name = "blog_today_archive.html"
    context_object_name = 'todays_posts'

코드 해설:

  1. django.views.generic.dates에서 TodayArchiveView를 임포트합니다.
  2. BlogPost 모델을 임포트합니다.
  3. BlogTodayArchiveView 클래스를 정의하고 TodayArchiveView를 상속받습니다.
  4. model, date_field, template_name 속성을 설정합니다.
  5. context_object_name으로 템플릿에서 사용할 객체 리스트의 이름을 지정합니다.

이 뷰를 사용하면 오늘 발행된 블로그 포스트를 쉽게 표시할 수 있어요. “오늘의 특집” 같은 기능을 구현할 때 아주 유용하답니다!

16. DateDetailView: 날짜별 상세 정보의 마법사

DateDetailView는 특정 날짜의 특정 객체 상세 정보를 표시할 때 사용합니다. 날짜별로 고유한 URL을 가진 블로그 포스트를 표시할 때 아주 유용해요.

from django.views.generic.dates import DateDetailView
from .models import BlogPost

class BlogDateDetailView(DateDetailView):
    model = BlogPost
    date_field = "published_date"
    month_format = '%m'
    template_name = "blog_detail.html"
    context_object_name = 'post'
    allow_future = False

코드 해설:

  1. django.views.generic.dates에서 DateDetailView를 임포트합니다.
  2. BlogPost 모델을 임포트합니다.
  3. BlogDateDetailView 클래스를 정의하고 DateDetailView를 상속받습니다.
  4. model, date_field, template_name 속성을 설정합니다.
  5. month_format으로 월 형식을 지정합니다.
  6. context_object_name으로 템플릿에서 사용할 객체의 이름을 지정합니다.
  7. allow_future를 False로 설정하여 미래 날짜의 포스트를 표시하지 않습니다.

이 뷰를 사용하면 특정 날짜에 발행된 특정 블로그 포스트의 상세 정보를 표시할 수 있어요. URL에 날짜 정보를 포함시켜 SEO에도 좋고, 사용자에게 정보를 명확히 전달할 수 있답니다!

마무리

자, 이렇게 해서 Django의 16가지 제네릭 뷰에 대해 모두 알아보았습니다. 이 제네릭 뷰들을 잘 활용하면 웹 개발 시간을 크게 단축할 수 있고, 코드의 재사용성도 높일 수 있어요.

하지만 제네릭 뷰를 사용할 때 주의할 점도 있습니다. 때로는 너무 많은 커스터마이징이 필요한 경우, 오히려 일반 함수 기반 뷰를 사용하는 것이 더 간단할 수 있어요. 또한, 제네릭 뷰의 동작 방식을 정확히 이해하지 못하면 예상치 못한 결과가 발생할 수 있습니다.

그래도 대부분의 경우, 제네릭 뷰는 Django 개발을 훨씬 더 효율적으로 만들어줍니다. 특히 CRUD(Create, Read, Update, Delete) 작업을 구현할 때 제네릭 뷰의 진가가 발휘되죠.

여러분도 이제 Django의 제네릭 뷰 16가지를 모두 알게 되었습니다. 이 지식을 바탕으로 더욱 효율적이고 강력한 Django 애플리케이션을 만들어보세요.

#용어 해설

  1. 제네릭 뷰(Generic View): 자주 사용되는 뷰 패턴을 추상화하여 제공하는 Django의 기능
  2. CRUD: Create(생성), Read(읽기), Update(갱신), Delete(삭제)의 약자로, 데이터 처리의 기본적인 4가지 기능
  3. 컨텍스트(Context): 템플릿에 전달되는 변수들의 집합
  4. 쿼리셋(Queryset): 데이터베이스로부터 전달받은 객체들의 목록
  5. 페이지네이션(Pagination): 많은 수의 객체를 여러 페이지로 나누어 표시하는 기능
  6. URL 역참조: URL 패턴의 이름을 사용하여 해당 URL을 생성하는 기능
  7. 미들웨어(Middleware): 요청과 응답 처리 중간에서 작동하는 시스템
  8. ORM(Object-Relational Mapping): 객체와 관계형 데이터베이스를 연결해주는 기술
  9. 템플릿(Template): HTML 파일에 Python 변수와 태그를 사용할 수 있게 해주는 Django의 기능
  10. CBV(Class-Based View): 클래스를 기반으로 작성된 뷰
테리 이모티콘
( 즐겁게 코딩을 합시다! )

유사한 게시물