2.
자기 소개
NRISE Backend Chapter Leader (현재)
ODK Media Backend Leader (3년)
프리랜서 (1년), 혜움세무회계 CTO 4개월 포함
SmartStudy Software engineer (3년 반)
아이티동아 Developer (4년)
정경업 (파이)
3.
발표 주제를 정하기까지
지금까지 Django 사용법에서 시작하여 자아성찰까지 여러 주제 발표 했었음
흔한 고민 이번엔 뭘하지…?
처음으로 돌아가 사용법에 대해 가볍게 이야기 해볼까?
Django는 뉴스 CMS로 시작된 프레임워크
마침 외주로 아이티동아, 게임동아 뉴스 사이트를 재개발
발표 해보자!
4.
요구사항과 구현 목표
기본적인 뉴스 사이트 기능
● 홈, 사이드 위젯 구성과 기사 읽기, 목록, 작성, 포털 배포
기존 데이터 이전
한가지 앱으로 두 사이트 운영
유지 보수 최소화(외주)
기자들이 직접 홈과 위젯을 편집 가능
5.
시작
프로젝트 레이아웃
Docker-Compose
IDE(Pycharm) 연동
개발 환경
6.
프로젝트 레이아웃
api : API 구현 코드 분리
app : 프로젝트 메인에 관련 설정 파일
article : 기사 구현
home : 홈 화면 및 사이드 위젯 등 공용 코드
migrator : 데이터 이전 작업
publish : 기사 배포 관련
search : 검색
docker-compose.yml
배포 관련 스크립트
8.
Docker-compose
Docker-compose를 개발환경에서 쓰면
● DB 등 필요한 서비스를 손쉽게 관리할 수 있음
● 파이썬 라이브러리 완전히 독립적
● 다 까먹어도 'up'명령 한줄이면 개발 환경 구축
# 대충 적은 사용 법
docker-compose build # 이미지 생성
docker-compose up # 서비스 실행
docker-compose down # 서비스 내리기
docker-compose run django bash # shell(bash) 접근
9.
IDE(Pycharm) 연동
Pycharm Settings
● Python Interpreter - Docker-compose
● Language & Frameworks - Django
● Run/Debug Configrations - Django Server, Django tests
IDE에서 바로 테스트 실행 가능
10.
1장
Sites로 멀티 도메인 구현
Article 모델 확장과 Test코드
CBV로 기사 목록, 읽기 구현
돌아가는 기반
12.
한 앱으로 두 사이트 만드는 방법
# models.py
from django.contrib.sites.models import Site
class Article(TimeStampedModel):
site = models.ForeignKey(Site, on_delete=models.PROTECT, db_index=True)
title = models.CharField(max_length=250, db_index=True)
# views.py
from django.contrib.sites.shortcuts import get_current_site
def list_view(request):
articles = Article.objects.filter(site=get_current_site(request))
13.
한 앱으로 두 사이트 만드는 방법
내장된 Sites 모델과 get_current_site 함수로 간단하게 구현
● 실제 적용된 내용은 Category - Article 모델 관계
Sites 내용은 Fixture로 만들어놓고 테스트 코드 및 배포시 사용 가능
- model: sites.site
pk: 1
fields:
domain: it.donga.com
name: IT동아
- model: sites.site
pk: 2
fields:
domain: game.donga.com
name: 게임동아
15.
Manager로 배포 상태의 기사만 다루기
def live_q():
q = Q(published=_lte=timezone.now()) | Q(published=_isnull=True)
return q & Q(active=True)
class LiveManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(live_q())
class Article(TimeStampedModel):
# managers
live_objects = LiveManager()
objects = models.Manager()
live_objects를 쓰면 배포 상태의 기사만 가져오기 쉽다
objects는 Admin에서 쓰이므로 유지
16.
Manager로 배포 상태의 기사만 다루기
# tests.py
class TestArticleModels(TestCase):
def test_live(self):
# 모델 인스턴스 목 생성
baker.make(Article, active=True) # 유효
baker.make(Article, active=False) # 무효
# 전체 기사는 2개지만 배포 기사는 1개인 것을 Test 로 검증
self.assertEqual(Article.objects.count(), 2)
self.assertEqual(Article.live_objects.count(), 1)
17.
간단한 카운트 함수와 테스트 코드
기사를 읽었을 때 카운트가 올라가는 기능을 Model에 구현
# models.py
class Article(TimeStampedModel):
def hit(self):
Article.objects.filter(id=self.pk).update(hit_count=F("hit_count") + 1)
18.
간단한 카운트 함수와 테스트 코드
작동하는지 확인하는 간단한 Test 작성
# tests.py
class TestArticleModels(TestCase):
def test_hit(self):
article = baker.make(Article)
self.assertEqual(article.hit_count, 0)
article.hit() # +1
article.refresh_from_db() # DB에서 값을 다시 읽기
self.assertEqual(article.hit_count, 1)
article.hit()
article.hit() # +2
article.refresh_from_db()
self.assertEqual(article.hit_count, 3)
19.
Signal로 기사 번호 만들기
@receiver(post_save, sender=Article)
def article_auto_number(sender, instance, created, **kwargs):
if not created:
return
if instance.number > 0:
return
queryset = sender.objects.filter(site=instance.site)
max_number = queryset.aggregate(Max("number"))["number=_max"]
queryset.filter(id=instance.id).update(number=max_number + 1)
두 사이트의 기사 숫자가 차이나고
기존 데이터를 가져올 때 옮겨오기 쉽게 하려고
ID를 그냥 쓰지 못하고 number를 만듬
20.
Signal로 기사 번호 만들기
class TestArticleModels(TestCase):
def test_auto_number(self):
a_1 = baker.make(Article, site=self.site_a) # a, number 1
a_7 = baker.make(Article, site=self.site_a, number=7) # a, number 7
a_1.refresh_from_db()
a_7.refresh_from_db()
self.assertEqual(a_1.number, 1)
self.assertEqual(a_7.number, 7)
baker.make(Article, site=self.site_b) # b, number 1
baker.make(Article, site=self.site_b) # b, number 2
b_3 = baker.make(Article, site=self.site_b) # b, number 3
b_3.refresh_from_db()
self.assertEqual(b_3.number, 3)
값이 생성될 때마다 post_save signal이 실행되는지 확인
22.
Function Based View VS Class Based View
Function Based View
● 절차적으로 작성하여 코드가 섞이기 쉬우며
● 기능을 찾아내기 어려운 코드가 되어
● 재활용도 덜 신경쓰게 되는걸 자주 봅니다
Class Based View
● 역할이 명시적으로 나뉘어 코드가 덜 섞이며
● 있는 기능을 잘 쓰는 법을 찾아야 하지만
● 재활용 하기 쉬워 적은 양의 코드로 많은 구현을 할 수 있습니다
23.
기사 목록
class ArticleListView(BaseSideMixin, PageNumbersMixin, ListView):
paginate_by = 10
template_name = 'article/list.html'
category = None # get_queryset에서 할당 후 get_context_data에서 사용
def get_queryset(self):
self.category = get_object_or_404(
Category, site=get_current_site(self.request), code=self.kwargs['category_code'])
return Article.live_objects.filter(category=self.category).only(*ARTICLE_LIST_FIELDS)
def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs)
data.update({'title': self.category.name, 'category': self.category})
return data
24.
기사 목록
상속 받은 클래스
● ListView : Django에서 제공하는 기본 클래스
● BaseSideMixin : 사이트의 사이드바를 구현 (후술)
● PageNumbersMixin : 페이지 번호 방식을 변경하기 위한 구현
속성, 함수
● paginate_by : 페이지당 개수
● template_name : View에서 사용할 템플릿 이이름
● get_queryset : Django ORM에서 가져올 목록용 쿼리, 분류를 함께 구현
● get_context_data : 템플릿에 보여질 context 생성
25.
기사 읽기
class ArticleDetailView(BaseSideMixin, DetailView):
template_name = 'article/detail.html'
slug_url_kwarg = 'number'
slug_field = 'number'
def get_queryset(self):
site = get_current_site(self.request)
return Article.live_objects.filter(site=site).select_related('site', 'category')
def get_context_data(self, **kwargs):
self.object.hit() # 기사 조회수 증가
data = super().get_context_data(**kwargs)
data['tag_names'] = self.object.tags.values_list('name', flat=True)
return data
26.
2장
캐시 적용 및 관리
모양을 바꿀 수 있는 홈, 사이드 위젯
Admin 쓸만하게
운영에 필요한 것들
28.
캐시 적용 및 관리
웹 서비스 대부분의 부하는 DB이며
DB 쿼리를 최대한 덜하도록 캐시를 많이 씁니다
캐시를 적용하는 것은 Django에서 손쉽게 가능하나
관리는 알아서 해야 합니다
약간의 코드로 관리할 수 있게 구현 해보았습니다
29.
일원화된 캐시 키 관리
settings에서 어떤 캐시가 있는지 확인할 수 있게 정리
# settings.py
CACHE_KEYS = {
'article': [
'article_links',
'article_footer',
'article_content',
],
'home': [
'widget_side',
'widget_home',
],
'text_page': [
'text_detail'
]
}
33.
기사 작성 및 수정시마다 캐시 업데이트
Signal 사용
● settings에서 어떤 캐시가 있는지 확인 가능하고
● 데이터 업데이트 시점에 Signal로 캐시를 최신으로 유지
● 뉴스 기사 생성 수가 그리 크지 않으므로 가볍게 선택 가능한 전략
@receiver(post_save, sender=Article)
def article_cache_clear(sender, instance, created, **kwargs):
# settings에서 키 값을 가져옴
for name in settings.CACHE_KEYS['article']:
key = make_template_fragment_key(name, [instance.id])
cache.delete(key)
# 기사 내용이 다른 캐시에서도 쓰이는 경우 찾아서 삭제
cache.delete_many([f'{name}_{instance.site.id}' for name in settings.CACHE_KEYS['home']])
cache.delete(f'{instance.site.domain}_last_published')
35.
홈 위젯 편집 기능을 만든 이유
행사 등을 이유로 특집 영역을 구성하거나
새로운 기사 묶음 목록을 실험하고 싶을 때
매번 개발자가 고치려면 시간과 비용이 듭니다
최신 기사 같은 목록을 위젯으로 구성하고
직접 편집 가능하도록 만들어보았으며
하는 김에 광고 배너도 추가했습니다
37.
모델 코드 일부
class Widget(SortableMixin, TimeStampedModel):
# 분류
site = models.ForeignKey(Site, verbose_name="사이트")
location = models.CharField('위치', choices=WIDGET_CHOICES['location'])
kind = models.CharField('구분', choices=WIDGET_CHOICES['kind'])
# 위젯 속성
title = models.CharField('제목')
title_url = models.URLField('제목 URL')
skin = models.CharField('스킨', choices=WIDGET_CHOICES['skin'])
size = models.IntegerField('최대 기사 숫자', default=5)
# 데이터 직접 입력
html = models.TextField('HTML 직접 입력')
data = JSONField('기사 직접 입력')
articles = models.ManyToManyField(Article, verbose_name='기사 연결')
# 필터
order_by = models.CharField('기사 정렬', choices=WIDGET_CHOICES['order_by'])
categories = models.ManyToManyField(Category, verbose_name='기사 분류 필터')
tags = TaggableManager(verbose_name='기사 태그 필터')
sort = models.PositiveIntegerField('위젯 순서')
38.
모델에서 구현된 함수
def get_data(self, size=None):
# 길고 복잡하여 코드 생략
# 조건에 맞춰서 현재 표시해야할 데이터 생성
# 배너의 경우 html만 읽기
# 직접 적은 기사 내용
# 선택한 기사
# 조건에 맞는 기사 기사
def template_name(self):
if self.kind == 'banner':
return 'widgets/banner.html'
if self.kind == 'article-list' and self.skin:
return f'widgets/{self.skin}.html'
return 'widgets/title-only.html'
39.
손쉽게 사용하기 위한 Manager
class WidgetManager(models.Manager):
def get_queryset(self):
return super().get_queryset().select_related('site')
.prefetch_related('articles', 'tags', 'categories')
def all_by_location(self, site, location):
return [{'obj': i, 'data': i.get_data()} for i in
self.get_queryset().filter(site=site, location=location)]
@cache_widget('widget_side') # 연산이 많이 일어날 수 있어서 캐시 구현
def sides(self, site):
return self.all_by_location(site, 'side')
@cache_widget('widget_home')
def homes(self, site):
return self.all_by_location(site, 'home')
Widget.objects.homes() 간단하게 사용 할 수 있게 함
44.
Toast에디터를 기사 편집 툴로 사용
class AdminToastEditorWidget(widgets.AdminTextareaWidget):
template_name = 'forms/admin-toast-editor.html'
class Media:
css = {"all": (
"https:=/uicdn.toast.com/editor/2.5.2/toastui-editor.min.css",
"https:=/cdnjs.cloudflare.com/ajax/libs/codemirror/5.48.4/codemirror.min.css",
)}
js = ("js/admin-toast-editor.js",)
Admin 커스텀 위젯 기능, toast 에디터를 읽는 js를 추가
45.
기사 제목 필드에 여러 기능 한번에 넣기
html을 일부 하드코딩하여 기사 태그를 제목에 함께 표현
기사를 포털에 배포하는 기능과 배포된 기사를 검색하는 링크 추가
def title_with_tags(self, obj):
return safe_render("""
<strong><a href="{% url 'admin:article_article_change' obj.id %}">
{{ obj.title }}=/a>=/strong><br=>
{% for i in obj.tags.all %}<small>#{{ i.name }}=/small> {% endfor %}
<br=>
<a href="{% url 'admin:publish' obj.id %}">[배포 하기]=/a>
<a href="{% url 'admin:publish_publishedarticle_changelist' %}?article={{ obj.id }}">[배포 기록]=/a>
/ 검색:
<a href="https:=/search.naver.com/search.naver?query={{ obj.title|urlencode }}">[네이버]=/a>
<a href="https:=/search.daum.net/search?q={{ obj.title|urlencode }}">[다음]=/a>
<a href="http:=/search.zum.com/search.zum?query={{ obj.title|urlencode }}">[줌]=/a>
<a href="https:=/search.daum.net/nate?q={{ obj.title|urlencode }}">[네이트]=/a>
""", {"obj": obj})
title_with_tags.short_description = '제목 / 태그 / 배포'
title_with_tags.admin_order_field = 'title'
46.
추가적인 액션 함수
# actions = [export_as_csv, cache_clear] 부분에서 사용
def export_as_csv(modeladmin, request, queryset):
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = f'attachment; filename="articles.csv"'
writer = csv.writer(response)
writer.writerow(['Number', 'URL', 'Title', 'Reporter', 'Published', 'Category'])
articles = queryset.select_related('site', 'category').only(
'site', 'number', 'title', 'reporter_name', 'published', 'category')
for article in articles:
writer.writerow((
article.number,
f'https:=/{article.site.domain}{reverse("article-detail", args=[article.number])}',
article.title, article.reporter_name,
date(article.published, 'Y-m-d H:i'), article.category.name))
return response
def cache_clear(modeladmin, request, queryset):
cache.clear()
47.
3장
기사를 포털에 배포하기
간단한 검색 구현
데이터 이전 스크립트
더 필요한 것들
49.
기사를 포털에 배포하기
한국의 뉴스 사이트는 네이버, 다음 같은 포털에 기사를 배포함
대부분 XML 파일을 생성 후 각 포털 서버에 FTP로 전송
HTTP POST 방식으로 제공하는 곳 생겨나는 중
매체별로 기사 포멧을 변환 후 전송 방식에 맞춰 보내줘야함
현재 각 뉴스 사이트마다 4개의 포탈에 전송 중
50.
Media 모델로 매체별 차이 대응
class Media(TimeStampedModel):
active = models.BooleanField('활성화')
# 식별
site = models.ForeignKey(Site, verbose_name='사이트')
code = models.CharField(verbose_name='매체 구분 코드')
title = models.CharField('매체명')
# 파일명
file_ext = models.CharField('파일명 확장자', choices=[('', '없음'), ('txt', 'txt'), ('xml', 'xml')])
file_name_prefix = models.CharField('파일명 프리픽스', max_length=20, default='', choices=[
('', '없음'), ('out', 'out'), ('news', 'news'),
('itdonga_', 'itdonga_'), ('gamedonga_', 'gamedonga_')])
file_name_type = models.CharField('파일명 타입', choices=[
('number', '{기사번호}'), ('yyyymmdd-8-number', '{년}{월}{일}{기사번호8자}')])
# 내용
encoding = models.CharField('인코딩', max_length=10, default='utf-8', choices=[
('euc-kr', 'EUC-KR'), ('utf-8', 'UTF-8')])
publish_type = models.CharField('배포 유형', max_length=50, choices=PublishType.choices)
extra = JSONField('추가 데이터', help_text='각 설정마다 필요한 추가 값을 JSON 형태로 기록합니다.')
template = models.TextField('템플릿', help_text='기사 생성시 사용하는 템플릿입니다.')
51.
PublishedArticle 모델로 변환된 기사 내용과 이력을 남김
class PublishedArticle(TimeStampedModel):
media = models.ForeignKey(Media, verbose_name='매체', on_delete=models.PROTECT, db_index=True)
article = models.ForeignKey(Article, verbose_name='기사', on_delete=models.SET_NULL,
blank=True, null=True, db_index=True)
state = models.CharField('상태', max_length=6, default='new', blank=True, choices=[
('new', '신규'),
('update', '수정'),
('delete', '삭제'),
])
file_name = models.CharField('파일명', default='', max_length=50, blank=True)
content = models.TextField('변환된 기사 내용', default='', blank=True)
encoding = models.CharField('인코딩', max_length=10, default='utf-8', choices=[
('euc-kr', 'EUC-KR'),
('utf-8', 'UTF-8'),
], blank=True)
52.
배포 함수
# 복잡한 세부 함수들은 생략
def publish(self, article, state='new'):
pa = PublishedArticle.objects.create(
**{'media': self, 'article': article, 'state': state, 'encoding': self.encoding})
tags = [tag.name for tag in article.tags.all()]
pa.file_name = pa.make_file_name()
pa.content = Template(self.template).render(Context({
'obj': pa,
'article': article,
'tags': tags,
'state': pa.converted_state(state),
'site': article.site,
**self.get_codes(article, tags, pa.media.extra.get('tag', {})),
}))
pa.save(update_fields=['content', 'file_name'])
# 기사 변환 후 FTP 업로드 등의 절차를 매체에 따라 수행
self.after_publish(pa)
return pa
54.
간단한 검색 구현
풀 텍스트 서치를 구현하는 것은 많은 비용이 듬
간단한 검색 쿼리에도 DB가 많이 느려질 수 있음
Django에 Postgresql을 쓸 경우 SearchVetor를 사용 가능
단어를 미리 잘라서 저장해놓고 검색
한글은 잘 안되긴 하지만 적당히 쓸만함
55.
간단한 검색 구현
# Article 모델에 필드 추가
search_vector = SearchVectorField(null=True, blank=True)
# Signal 추가
@receiver(post_save, sender=Article)
def article_update_search_vector(sender, instance, created, **kwargs):
update_fields = kwargs.get('update_fields')
# update_fields에 직접 지정된 경우를 피해서 루프도는 것을 방지
if not update_fields or 'search_vector' not in update_fields:
obj = Article.objects.annotate(document=search_vector).get(id=instance.id)
obj.search_vector = obj.document
obj.save(update_fields=['search_vector'])
56.
간단한 검색 구현
# 검색 쿼리
search_query = reduce(
operator.and_,
(SearchQuery(f'{x}:*', search_type='raw') for x in query.split(' '))
)
# 검색할 쿼리를 분할해서 가중치로 넣고 정렬
rank = SearchRank(search_vector, search_query, weights=[0.4, 0.6, 0.8, 1.0])
# 현재 사이트에서는 점수보다 배포 시간을 중시
return queryset.filter(search_vector=search_query)
.annotate(rank=rank).filter(rank=_gte=0.3)
.order_by('-published', '-rank').distinct()
58.
데이터 이전 스크립트
하나의 회사에서 제공하는 두개의 뉴스 사이트
기능은 거의 같았으나 각각 앱과 데이터베이스가 나뉘어 있었음
하나의 앱으로 합쳐서 관리 부담을 덜고자 함
데이터 이전 시 놓치거나 잘못 들어간 필드는 치명적
데이터 제어, 가공, 매치 부분을 분할하여
코드를 쉽게 읽을 수 있는 데이터 이전 스크립트를 작성
59.
실행은 Django Management Commands 사용
class Command(BaseCommand):
help = '예전 데이터베이스를 새 데이터베이스로 마이그레이션'
def add_arguments(self, parser):
parser.add_argument('=-count', dest='count', action='store', type=int)
def handle(self, *args, **options):
count = options['count'] if options['count'] else None
# Mover 클래스를 만들어서 제어
mover = Mover('IT동아')
mover.migrate(count)
mover = Mover('게임동아')
mover.migrate(count)
기존 데이터 DB는 Django에서 읽을 수 있게 manage inspectdb 명령으로 가져옴
60.
Mover 클래스
class Mover:
def =_init=_(self, site_name):
self.site = Site.objects.get(name=site_name)
self.categories = [{'code': i.code, 'category': i} for i in Category.objects.filter(site=self.site)]
def get_xxx():
# 달라진 분류, 필드 내용 등을 가공하는 함수들
def migrate(self, count=None):
model, db_name = (GameArticle, 'legacy-game') if self.site.name == '게임동아' else (ItArticle, 'legacy-it')
queryset = model.objects.using(db_name).all().order_by("updated", "id")
if count:
queryset = queryset[:count]
else:
last_migrated = Article.objects.filter(site=self.site, migrated=_isnull=False).latest('migrated')
queryset = queryset.filter(updated=_gte=last_migrated.migrated)
pbar = tqdm(total=queryset.count(), desc=self.site.name, mininterval=2, miniters=1, ncols=0)
for old in queryset.iterator():
self.make_new_from_old(old)
pbar.update(1)
pbar.close()
61.
필드 매칭은 명시적, 가공은 별도의 함수에서
def make_new_from_old(self, old):
params = {
'reporter_name': old.user_name if old.user_name else '',
'reporter_email': old.user_email if old.user_email else '',
'title': old.title,
'contents_md': old_html_to_new_md(old.contents) if old.contents else '',
'contents_html': old.contents,
'intro': old.intro,
'thumbnail': old.thumbnail,
'category': category,
'hit_count': old.count_view,
'published': created,
'created': created,
'modified': created if self.site.name == '게임동아' and old.id < 60000 else updated,
'migrated': updated,
}
new, _ = Article.objects.update_or_create(site=self.site, number=old.id, defaults=params)
new.tags.add(*tags)
62.
마무리
요약하면
- Docker-compose로 개발 환경 설정
- CBV로 코드 재활용
- Test 코드로 검증
- Sites, Cache, SearchVector, Admin 등
Django 기본 기능 사용
- 복잡한 일은 단계를 나눠서 작성
이것보다 많은 코드가 있지만
시간 관계상 이 정도만 소개합니다
63.
회사 홍보
엔라이즈는 사람과 사람, 사람과 콘텐츠를
연결하여 변화를 만듭니다.
위피: 동네 기반 소셜 데이팅 앱
콰트: 하루 10분 운동 습관 만드는 홈트레이닝
두 가지 서비스를 더 잘하기 위해 뭔가 해낼 것
같은 사람들이 점점 더 모이고 있습니다.
입사한지 반년도 되지 않았지만, 각자가 자신을
드러내며 더 잘하고자 하는 재미있는 회사입니다.
https://nrise.net/
NRISE에서 동료를 구합니다
Il semblerait que vous ayez déjà ajouté cette diapositive à .
Créer un clipboard
Vous avez clippé votre première diapositive !
En clippant ainsi les diapos qui vous intéressent, vous pourrez les revoir plus tard. Personnalisez le nom d’un clipboard pour mettre de côté vos diapositives.
Créer un clipboard
Partager ce SlideShare
Vous avez les pubs en horreur?
Obtenez SlideShare sans publicité
Bénéficiez d'un accès à des millions de présentations, documents, e-books, de livres audio, de magazines et bien plus encore, sans la moindre publicité.
Offre spéciale pour les lecteurs de SlideShare
Juste pour vous: Essai GRATUIT de 60 jours dans la plus grande bibliothèque numérique du monde.
La famille SlideShare vient de s'agrandir. Profitez de l'accès à des millions de livres numériques, livres audio, magazines et bien plus encore sur Scribd.
Apparemment, vous utilisez un bloqueur de publicités qui est en cours d'exécution. En ajoutant SlideShare à la liste blanche de votre bloqueur de publicités, vous soutenez notre communauté de créateurs de contenu.
Vous détestez les publicités?
Nous avons mis à jour notre politique de confidentialité.
Nous avons mis à jour notre politique de confidentialité pour nous conformer à l'évolution des réglementations mondiales en matière de confidentialité et pour vous informer de la manière dont nous utilisons vos données de façon limitée.
Vous pouvez consulter les détails ci-dessous. En cliquant sur Accepter, vous acceptez la politique de confidentialité mise à jour.