@su-record/vibe 0.4.4 โ†’ 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,371 @@
1
+ # ๐Ÿ Python + Django ํ’ˆ์งˆ ๊ทœ์น™
2
+
3
+ ## ํ•ต์‹ฌ ์›์น™ (core์—์„œ ์ƒ์†)
4
+
5
+ ```markdown
6
+ โœ… ๋‹จ์ผ ์ฑ…์ž„ (SRP)
7
+ โœ… ์ค‘๋ณต ์ œ๊ฑฐ (DRY)
8
+ โœ… ์žฌ์‚ฌ์šฉ์„ฑ
9
+ โœ… ๋‚ฎ์€ ๋ณต์žก๋„
10
+ โœ… ํ•จ์ˆ˜ โ‰ค 30์ค„
11
+ โœ… ์ค‘์ฒฉ โ‰ค 3๋‹จ๊ณ„
12
+ โœ… Cyclomatic complexity โ‰ค 10
13
+ ```
14
+
15
+ ## Django ํŠนํ™” ๊ทœ์น™
16
+
17
+ ### 1. Model ์„ค๊ณ„
18
+
19
+ ```python
20
+ # โœ… models.py
21
+ from django.db import models
22
+ from django.contrib.auth.models import AbstractUser
23
+ from django.utils import timezone
24
+
25
+
26
+ class BaseModel(models.Model):
27
+ """๊ณตํ†ต ํ•„๋“œ๋ฅผ ๊ฐ€์ง„ ์ถ”์ƒ ๋ชจ๋ธ"""
28
+ created_at = models.DateTimeField(auto_now_add=True)
29
+ updated_at = models.DateTimeField(auto_now=True)
30
+
31
+ class Meta:
32
+ abstract = True
33
+
34
+
35
+ class User(AbstractUser):
36
+ """์ปค์Šคํ…€ ์‚ฌ์šฉ์ž ๋ชจ๋ธ"""
37
+ email = models.EmailField(unique=True)
38
+ phone = models.CharField(max_length=20, blank=True)
39
+ profile_image = models.ImageField(upload_to='profiles/', blank=True)
40
+
41
+ USERNAME_FIELD = 'email'
42
+ REQUIRED_FIELDS = ['username']
43
+
44
+ class Meta:
45
+ db_table = 'users'
46
+ verbose_name = '์‚ฌ์šฉ์ž'
47
+ verbose_name_plural = '์‚ฌ์šฉ์ž๋“ค'
48
+
49
+ def __str__(self):
50
+ return self.email
51
+
52
+
53
+ class Post(BaseModel):
54
+ """๊ฒŒ์‹œ๊ธ€ ๋ชจ๋ธ"""
55
+ author = models.ForeignKey(
56
+ User,
57
+ on_delete=models.CASCADE,
58
+ related_name='posts',
59
+ verbose_name='์ž‘์„ฑ์ž'
60
+ )
61
+ title = models.CharField(max_length=200, verbose_name='์ œ๋ชฉ')
62
+ content = models.TextField(verbose_name='๋‚ด์šฉ')
63
+ is_published = models.BooleanField(default=False, verbose_name='๊ฒŒ์‹œ ์—ฌ๋ถ€')
64
+ published_at = models.DateTimeField(null=True, blank=True)
65
+
66
+ class Meta:
67
+ db_table = 'posts'
68
+ ordering = ['-created_at']
69
+ verbose_name = '๊ฒŒ์‹œ๊ธ€'
70
+ verbose_name_plural = '๊ฒŒ์‹œ๊ธ€๋“ค'
71
+
72
+ def __str__(self):
73
+ return self.title
74
+
75
+ def publish(self):
76
+ """๊ฒŒ์‹œ๊ธ€ ๋ฐœํ–‰"""
77
+ self.is_published = True
78
+ self.published_at = timezone.now()
79
+ self.save(update_fields=['is_published', 'published_at'])
80
+ ```
81
+
82
+ ### 2. View (Class-Based Views ๊ถŒ์žฅ)
83
+
84
+ ```python
85
+ # โœ… views.py
86
+ from django.views.generic import ListView, DetailView, CreateView, UpdateView
87
+ from django.contrib.auth.mixins import LoginRequiredMixin
88
+ from django.urls import reverse_lazy
89
+ from .models import Post
90
+ from .forms import PostForm
91
+
92
+
93
+ class PostListView(ListView):
94
+ """๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก ๋ทฐ"""
95
+ model = Post
96
+ template_name = 'posts/list.html'
97
+ context_object_name = 'posts'
98
+ paginate_by = 10
99
+
100
+ def get_queryset(self):
101
+ queryset = super().get_queryset()
102
+ return queryset.filter(is_published=True).select_related('author')
103
+
104
+
105
+ class PostDetailView(DetailView):
106
+ """๊ฒŒ์‹œ๊ธ€ ์ƒ์„ธ ๋ทฐ"""
107
+ model = Post
108
+ template_name = 'posts/detail.html'
109
+ context_object_name = 'post'
110
+
111
+ def get_queryset(self):
112
+ return super().get_queryset().select_related('author')
113
+
114
+
115
+ class PostCreateView(LoginRequiredMixin, CreateView):
116
+ """๊ฒŒ์‹œ๊ธ€ ์ƒ์„ฑ ๋ทฐ"""
117
+ model = Post
118
+ form_class = PostForm
119
+ template_name = 'posts/form.html'
120
+ success_url = reverse_lazy('posts:list')
121
+
122
+ def form_valid(self, form):
123
+ form.instance.author = self.request.user
124
+ return super().form_valid(form)
125
+ ```
126
+
127
+ ### 3. Django REST Framework
128
+
129
+ ```python
130
+ # โœ… serializers.py
131
+ from rest_framework import serializers
132
+ from .models import Post, User
133
+
134
+
135
+ class UserSerializer(serializers.ModelSerializer):
136
+ """์‚ฌ์šฉ์ž ์‹œ๋ฆฌ์–ผ๋ผ์ด์ €"""
137
+ class Meta:
138
+ model = User
139
+ fields = ['id', 'email', 'username', 'profile_image']
140
+ read_only_fields = ['id']
141
+
142
+
143
+ class PostSerializer(serializers.ModelSerializer):
144
+ """๊ฒŒ์‹œ๊ธ€ ์‹œ๋ฆฌ์–ผ๋ผ์ด์ €"""
145
+ author = UserSerializer(read_only=True)
146
+ author_id = serializers.PrimaryKeyRelatedField(
147
+ queryset=User.objects.all(),
148
+ source='author',
149
+ write_only=True
150
+ )
151
+
152
+ class Meta:
153
+ model = Post
154
+ fields = [
155
+ 'id', 'title', 'content', 'author', 'author_id',
156
+ 'is_published', 'created_at', 'updated_at'
157
+ ]
158
+ read_only_fields = ['id', 'created_at', 'updated_at']
159
+
160
+ def validate_title(self, value):
161
+ if len(value) < 5:
162
+ raise serializers.ValidationError('์ œ๋ชฉ์€ 5์ž ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค')
163
+ return value
164
+
165
+
166
+ # โœ… views.py (DRF)
167
+ from rest_framework import viewsets, permissions, status
168
+ from rest_framework.decorators import action
169
+ from rest_framework.response import Response
170
+ from django_filters.rest_framework import DjangoFilterBackend
171
+
172
+
173
+ class PostViewSet(viewsets.ModelViewSet):
174
+ """๊ฒŒ์‹œ๊ธ€ ViewSet"""
175
+ queryset = Post.objects.all()
176
+ serializer_class = PostSerializer
177
+ permission_classes = [permissions.IsAuthenticatedOrReadOnly]
178
+ filter_backends = [DjangoFilterBackend]
179
+ filterset_fields = ['is_published', 'author']
180
+
181
+ def get_queryset(self):
182
+ queryset = super().get_queryset()
183
+ return queryset.select_related('author')
184
+
185
+ def perform_create(self, serializer):
186
+ serializer.save(author=self.request.user)
187
+
188
+ @action(detail=True, methods=['post'])
189
+ def publish(self, request, pk=None):
190
+ """๊ฒŒ์‹œ๊ธ€ ๋ฐœํ–‰ ์•ก์…˜"""
191
+ post = self.get_object()
192
+
193
+ if post.author != request.user:
194
+ return Response(
195
+ {'error': '์ž‘์„ฑ์ž๋งŒ ๋ฐœํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค'},
196
+ status=status.HTTP_403_FORBIDDEN
197
+ )
198
+
199
+ post.publish()
200
+ return Response({'status': '๋ฐœํ–‰๋˜์—ˆ์Šต๋‹ˆ๋‹ค'})
201
+ ```
202
+
203
+ ### 4. Service ๋ ˆ์ด์–ด (Fat Model ๋ฐฉ์ง€)
204
+
205
+ ```python
206
+ # โœ… services/post_service.py
207
+ from django.db import transaction
208
+ from django.core.exceptions import PermissionDenied
209
+ from ..models import Post, User
210
+
211
+
212
+ class PostService:
213
+ """๊ฒŒ์‹œ๊ธ€ ๊ด€๋ จ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง"""
214
+
215
+ @staticmethod
216
+ def create_post(author: User, title: str, content: str) -> Post:
217
+ """๊ฒŒ์‹œ๊ธ€ ์ƒ์„ฑ"""
218
+ post = Post.objects.create(
219
+ author=author,
220
+ title=title,
221
+ content=content
222
+ )
223
+ return post
224
+
225
+ @staticmethod
226
+ def publish_post(post: Post, user: User) -> Post:
227
+ """๊ฒŒ์‹œ๊ธ€ ๋ฐœํ–‰"""
228
+ if post.author != user:
229
+ raise PermissionDenied('์ž‘์„ฑ์ž๋งŒ ๋ฐœํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค')
230
+
231
+ post.publish()
232
+ return post
233
+
234
+ @staticmethod
235
+ @transaction.atomic
236
+ def bulk_publish(post_ids: list[int], user: User) -> int:
237
+ """์—ฌ๋Ÿฌ ๊ฒŒ์‹œ๊ธ€ ์ผ๊ด„ ๋ฐœํ–‰"""
238
+ posts = Post.objects.filter(
239
+ id__in=post_ids,
240
+ author=user,
241
+ is_published=False
242
+ )
243
+
244
+ count = posts.update(
245
+ is_published=True,
246
+ published_at=timezone.now()
247
+ )
248
+ return count
249
+ ```
250
+
251
+ ### 5. Form ๋ฐ Validation
252
+
253
+ ```python
254
+ # โœ… forms.py
255
+ from django import forms
256
+ from django.core.exceptions import ValidationError
257
+ from .models import Post
258
+
259
+
260
+ class PostForm(forms.ModelForm):
261
+ """๊ฒŒ์‹œ๊ธ€ ํผ"""
262
+ class Meta:
263
+ model = Post
264
+ fields = ['title', 'content', 'is_published']
265
+ widgets = {
266
+ 'title': forms.TextInput(attrs={
267
+ 'class': 'form-control',
268
+ 'placeholder': '์ œ๋ชฉ์„ ์ž…๋ ฅํ•˜์„ธ์š”'
269
+ }),
270
+ 'content': forms.Textarea(attrs={
271
+ 'class': 'form-control',
272
+ 'rows': 10
273
+ }),
274
+ }
275
+
276
+ def clean_title(self):
277
+ title = self.cleaned_data.get('title')
278
+ if len(title) < 5:
279
+ raise ValidationError('์ œ๋ชฉ์€ 5์ž ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค')
280
+ return title
281
+
282
+ def clean(self):
283
+ cleaned_data = super().clean()
284
+ title = cleaned_data.get('title')
285
+ content = cleaned_data.get('content')
286
+
287
+ if title and content and title in content:
288
+ raise ValidationError('๋ณธ๋ฌธ์— ์ œ๋ชฉ์ด ํฌํ•จ๋˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค')
289
+
290
+ return cleaned_data
291
+ ```
292
+
293
+ ### 6. Custom Manager์™€ QuerySet
294
+
295
+ ```python
296
+ # โœ… managers.py
297
+ from django.db import models
298
+
299
+
300
+ class PostQuerySet(models.QuerySet):
301
+ """๊ฒŒ์‹œ๊ธ€ QuerySet"""
302
+
303
+ def published(self):
304
+ return self.filter(is_published=True)
305
+
306
+ def by_author(self, user):
307
+ return self.filter(author=user)
308
+
309
+ def recent(self, days=7):
310
+ from django.utils import timezone
311
+ from datetime import timedelta
312
+ cutoff = timezone.now() - timedelta(days=days)
313
+ return self.filter(created_at__gte=cutoff)
314
+
315
+
316
+ class PostManager(models.Manager):
317
+ """๊ฒŒ์‹œ๊ธ€ Manager"""
318
+
319
+ def get_queryset(self):
320
+ return PostQuerySet(self.model, using=self._db)
321
+
322
+ def published(self):
323
+ return self.get_queryset().published()
324
+
325
+ def by_author(self, user):
326
+ return self.get_queryset().by_author(user)
327
+
328
+
329
+ # ๋ชจ๋ธ์—์„œ ์‚ฌ์šฉ
330
+ class Post(BaseModel):
331
+ # ... fields ...
332
+ objects = PostManager()
333
+ ```
334
+
335
+ ## ํŒŒ์ผ ๊ตฌ์กฐ
336
+
337
+ ```
338
+ app_name/
339
+ โ”œโ”€โ”€ migrations/ # DB ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜
340
+ โ”œโ”€โ”€ management/
341
+ โ”‚ โ””โ”€โ”€ commands/ # ์ปค์Šคํ…€ ๋ช…๋ น์–ด
342
+ โ”œโ”€โ”€ services/ # ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง
343
+ โ”œโ”€โ”€ api/
344
+ โ”‚ โ”œโ”€โ”€ serializers.py # DRF ์‹œ๋ฆฌ์–ผ๋ผ์ด์ €
345
+ โ”‚ โ”œโ”€โ”€ views.py # DRF ๋ทฐ
346
+ โ”‚ โ””โ”€โ”€ urls.py # API ๋ผ์šฐํŒ…
347
+ โ”œโ”€โ”€ templates/ # HTML ํ…œํ”Œ๋ฆฟ
348
+ โ”œโ”€โ”€ static/ # ์ •์  ํŒŒ์ผ
349
+ โ”œโ”€โ”€ tests/
350
+ โ”‚ โ”œโ”€โ”€ test_models.py
351
+ โ”‚ โ”œโ”€โ”€ test_views.py
352
+ โ”‚ โ””โ”€โ”€ test_services.py
353
+ โ”œโ”€โ”€ models.py # ๋ชจ๋ธ (๋˜๋Š” models/ ๋””๋ ‰ํ† ๋ฆฌ)
354
+ โ”œโ”€โ”€ views.py # ๋ทฐ
355
+ โ”œโ”€โ”€ forms.py # ํผ
356
+ โ”œโ”€โ”€ managers.py # ์ปค์Šคํ…€ ๋งค๋‹ˆ์ €
357
+ โ”œโ”€โ”€ admin.py # Admin ์„ค์ •
358
+ โ”œโ”€โ”€ urls.py # URL ๋ผ์šฐํŒ…
359
+ โ””โ”€โ”€ apps.py # ์•ฑ ์„ค์ •
360
+ ```
361
+
362
+ ## ์ฒดํฌ๋ฆฌ์ŠคํŠธ
363
+
364
+ - [ ] Model์— `__str__`, `Meta` ์ •์˜
365
+ - [ ] CBV ์‚ฌ์šฉ (๊ถŒ์žฅ)
366
+ - [ ] Service ๋ ˆ์ด์–ด๋กœ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๋ถ„๋ฆฌ
367
+ - [ ] select_related/prefetch_related๋กœ N+1 ๋ฐฉ์ง€
368
+ - [ ] DRF Serializer๋กœ ์ž…์ถœ๋ ฅ ๊ฒ€์ฆ
369
+ - [ ] Custom Manager/QuerySet ํ™œ์šฉ
370
+ - [ ] Type hints ์‚ฌ์šฉ (Python 3.10+)
371
+ - [ ] ํ•œ๊ธ€ verbose_name ์„ค์ •