@su-record/vibe 2.3.0 → 2.3.2

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.
Files changed (98) hide show
  1. package/.claude/settings.json +35 -35
  2. package/.claude/settings.local.json +24 -25
  3. package/.claude/vibe/constitution.md +184 -184
  4. package/.claude/vibe/rules/core/communication-guide.md +104 -104
  5. package/.claude/vibe/rules/core/development-philosophy.md +52 -52
  6. package/.claude/vibe/rules/core/quick-start.md +120 -120
  7. package/.claude/vibe/rules/languages/dart-flutter.md +509 -509
  8. package/.claude/vibe/rules/languages/go.md +396 -396
  9. package/.claude/vibe/rules/languages/java-spring.md +586 -586
  10. package/.claude/vibe/rules/languages/kotlin-android.md +491 -491
  11. package/.claude/vibe/rules/languages/python-django.md +371 -371
  12. package/.claude/vibe/rules/languages/python-fastapi.md +386 -386
  13. package/.claude/vibe/rules/languages/rust.md +425 -425
  14. package/.claude/vibe/rules/languages/swift-ios.md +516 -516
  15. package/.claude/vibe/rules/languages/typescript-nextjs.md +441 -441
  16. package/.claude/vibe/rules/languages/typescript-node.md +375 -375
  17. package/.claude/vibe/rules/languages/typescript-nuxt.md +521 -521
  18. package/.claude/vibe/rules/languages/typescript-react-native.md +446 -446
  19. package/.claude/vibe/rules/languages/typescript-react.md +525 -525
  20. package/.claude/vibe/rules/languages/typescript-vue.md +353 -353
  21. package/.claude/vibe/rules/quality/bdd-contract-testing.md +388 -388
  22. package/.claude/vibe/rules/quality/checklist.md +276 -276
  23. package/.claude/vibe/rules/quality/testing-strategy.md +437 -437
  24. package/.claude/vibe/rules/standards/anti-patterns.md +369 -369
  25. package/.claude/vibe/rules/standards/code-structure.md +291 -291
  26. package/.claude/vibe/rules/standards/complexity-metrics.md +312 -312
  27. package/.claude/vibe/rules/standards/naming-conventions.md +198 -198
  28. package/.claude/vibe/setup.sh +31 -31
  29. package/.claude/vibe/templates/constitution-template.md +184 -184
  30. package/.claude/vibe/templates/contract-backend-template.md +517 -517
  31. package/.claude/vibe/templates/contract-frontend-template.md +594 -594
  32. package/.claude/vibe/templates/feature-template.md +96 -96
  33. package/.claude/vibe/templates/spec-template.md +199 -199
  34. package/CLAUDE.md +345 -323
  35. package/LICENSE +21 -21
  36. package/README.md +744 -724
  37. package/agents/compounder.md +261 -261
  38. package/agents/diagrammer.md +178 -178
  39. package/agents/e2e-tester.md +266 -266
  40. package/agents/explorer.md +48 -48
  41. package/agents/implementer.md +53 -53
  42. package/agents/research/best-practices-agent.md +139 -139
  43. package/agents/research/codebase-patterns-agent.md +147 -147
  44. package/agents/research/framework-docs-agent.md +181 -181
  45. package/agents/research/security-advisory-agent.md +167 -167
  46. package/agents/review/architecture-reviewer.md +107 -107
  47. package/agents/review/complexity-reviewer.md +116 -116
  48. package/agents/review/data-integrity-reviewer.md +88 -88
  49. package/agents/review/git-history-reviewer.md +103 -103
  50. package/agents/review/performance-reviewer.md +86 -86
  51. package/agents/review/python-reviewer.md +152 -152
  52. package/agents/review/rails-reviewer.md +139 -139
  53. package/agents/review/react-reviewer.md +144 -144
  54. package/agents/review/security-reviewer.md +80 -80
  55. package/agents/review/simplicity-reviewer.md +140 -140
  56. package/agents/review/test-coverage-reviewer.md +116 -116
  57. package/agents/review/typescript-reviewer.md +127 -127
  58. package/agents/searcher.md +54 -54
  59. package/agents/simplifier.md +119 -119
  60. package/agents/tester.md +49 -49
  61. package/agents/ui-previewer.md +137 -137
  62. package/commands/vibe.analyze.md +245 -180
  63. package/commands/vibe.reason.md +223 -183
  64. package/commands/vibe.review.md +200 -136
  65. package/commands/vibe.run.md +838 -836
  66. package/commands/vibe.spec.md +419 -383
  67. package/commands/vibe.utils.md +101 -101
  68. package/commands/vibe.verify.md +282 -241
  69. package/dist/cli/index.js +385 -385
  70. package/dist/lib/MemoryManager.d.ts.map +1 -1
  71. package/dist/lib/MemoryManager.js +119 -114
  72. package/dist/lib/MemoryManager.js.map +1 -1
  73. package/dist/lib/PythonParser.js +108 -108
  74. package/dist/lib/gemini-mcp.js +15 -15
  75. package/dist/lib/gemini-oauth.js +35 -35
  76. package/dist/lib/gpt-mcp.js +17 -17
  77. package/dist/lib/gpt-oauth.js +44 -44
  78. package/dist/tools/analytics/getUsageAnalytics.js +12 -12
  79. package/dist/tools/index.d.ts +50 -0
  80. package/dist/tools/index.d.ts.map +1 -0
  81. package/dist/tools/index.js +61 -0
  82. package/dist/tools/index.js.map +1 -0
  83. package/dist/tools/memory/createMemoryTimeline.js +10 -10
  84. package/dist/tools/memory/getMemoryGraph.js +12 -12
  85. package/dist/tools/memory/getSessionContext.js +9 -9
  86. package/dist/tools/memory/linkMemories.js +14 -14
  87. package/dist/tools/memory/listMemories.js +4 -4
  88. package/dist/tools/memory/recallMemory.js +4 -4
  89. package/dist/tools/memory/saveMemory.js +4 -4
  90. package/dist/tools/memory/searchMemoriesAdvanced.js +22 -22
  91. package/dist/tools/planning/generatePrd.js +46 -46
  92. package/dist/tools/prompt/enhancePromptGemini.js +160 -160
  93. package/dist/tools/reasoning/applyReasoningFramework.js +56 -56
  94. package/dist/tools/semantic/analyzeDependencyGraph.js +12 -12
  95. package/hooks/hooks.json +121 -103
  96. package/package.json +73 -69
  97. package/skills/git-worktree.md +178 -178
  98. package/skills/priority-todos.md +236 -236
@@ -1,371 +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 설정
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 설정