@su-record/vibe 0.4.3 โ 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.
- package/.agent/rules/languages/go.md +396 -0
- package/.agent/rules/languages/java-spring.md +586 -0
- package/.agent/rules/languages/kotlin-android.md +491 -0
- package/.agent/rules/languages/python-django.md +371 -0
- package/.agent/rules/languages/rust.md +425 -0
- package/.agent/rules/languages/swift-ios.md +516 -0
- package/.agent/rules/languages/typescript-node.md +375 -0
- package/.agent/rules/languages/typescript-vue.md +353 -0
- package/.claude/commands/vibe.analyze.md +166 -54
- package/.claude/settings.local.json +4 -1
- package/bin/vibe +140 -24
- package/package.json +1 -1
|
@@ -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 ์ค์
|