@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,396 +1,396 @@
1
- # 🐹 Go 품질 규칙
2
-
3
- ## 핵심 원칙 (core에서 상속)
4
-
5
- ```markdown
6
- ✅ 단일 책임 (SRP)
7
- ✅ 중복 제거 (DRY)
8
- ✅ 재사용성
9
- ✅ 낮은 복잡도
10
- ✅ 함수 ≤ 30줄
11
- ✅ 중첩 ≤ 3단계
12
- ✅ Cyclomatic complexity ≤ 10
13
- ```
14
-
15
- ## Go 특화 규칙
16
-
17
- ### 1. 에러 처리
18
-
19
- ```go
20
- // ❌ 에러 무시
21
- data, _ := ioutil.ReadFile("config.json")
22
-
23
- // ✅ 에러 항상 처리
24
- data, err := ioutil.ReadFile("config.json")
25
- if err != nil {
26
- return fmt.Errorf("설정 파일 읽기 실패: %w", err)
27
- }
28
-
29
- // ✅ 커스텀 에러 타입
30
- type NotFoundError struct {
31
- Resource string
32
- ID string
33
- }
34
-
35
- func (e *NotFoundError) Error() string {
36
- return fmt.Sprintf("%s (ID: %s)를 찾을 수 없습니다", e.Resource, e.ID)
37
- }
38
-
39
- // 사용
40
- func GetUser(id string) (*User, error) {
41
- user, err := repo.FindByID(id)
42
- if err != nil {
43
- return nil, fmt.Errorf("사용자 조회 실패: %w", err)
44
- }
45
- if user == nil {
46
- return nil, &NotFoundError{Resource: "사용자", ID: id}
47
- }
48
- return user, nil
49
- }
50
- ```
51
-
52
- ### 2. 구조체와 인터페이스
53
-
54
- ```go
55
- // ✅ 구조체 정의
56
- type User struct {
57
- ID string `json:"id"`
58
- Email string `json:"email"`
59
- Name string `json:"name"`
60
- CreatedAt time.Time `json:"created_at"`
61
- UpdatedAt time.Time `json:"updated_at"`
62
- }
63
-
64
- // ✅ 생성자 함수
65
- func NewUser(email, name string) *User {
66
- now := time.Now()
67
- return &User{
68
- ID: uuid.New().String(),
69
- Email: email,
70
- Name: name,
71
- CreatedAt: now,
72
- UpdatedAt: now,
73
- }
74
- }
75
-
76
- // ✅ 작은 인터페이스 (Go의 철학)
77
- type Reader interface {
78
- Read(p []byte) (n int, err error)
79
- }
80
-
81
- type Writer interface {
82
- Write(p []byte) (n int, err error)
83
- }
84
-
85
- // ✅ 인터페이스 조합
86
- type ReadWriter interface {
87
- Reader
88
- Writer
89
- }
90
-
91
- // ✅ Repository 인터페이스
92
- type UserRepository interface {
93
- FindByID(ctx context.Context, id string) (*User, error)
94
- FindByEmail(ctx context.Context, email string) (*User, error)
95
- Create(ctx context.Context, user *User) error
96
- Update(ctx context.Context, user *User) error
97
- Delete(ctx context.Context, id string) error
98
- }
99
- ```
100
-
101
- ### 3. Context 사용
102
-
103
- ```go
104
- // ✅ Context 전파
105
- func (s *UserService) GetUser(ctx context.Context, id string) (*User, error) {
106
- // Context를 하위 함수에 전달
107
- user, err := s.repo.FindByID(ctx, id)
108
- if err != nil {
109
- return nil, err
110
- }
111
- return user, nil
112
- }
113
-
114
- // ✅ Context 타임아웃
115
- func (h *Handler) HandleRequest(w http.ResponseWriter, r *http.Request) {
116
- ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
117
- defer cancel()
118
-
119
- result, err := h.service.Process(ctx)
120
- if err != nil {
121
- if errors.Is(err, context.DeadlineExceeded) {
122
- http.Error(w, "요청 시간 초과", http.StatusRequestTimeout)
123
- return
124
- }
125
- http.Error(w, err.Error(), http.StatusInternalServerError)
126
- return
127
- }
128
-
129
- json.NewEncoder(w).Encode(result)
130
- }
131
- ```
132
-
133
- ### 4. HTTP 핸들러 (net/http, Gin, Echo)
134
-
135
- ```go
136
- // ✅ net/http 핸들러
137
- func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
138
- id := chi.URLParam(r, "id")
139
-
140
- user, err := h.service.GetUser(r.Context(), id)
141
- if err != nil {
142
- var notFound *NotFoundError
143
- if errors.As(err, &notFound) {
144
- http.Error(w, err.Error(), http.StatusNotFound)
145
- return
146
- }
147
- http.Error(w, "서버 오류", http.StatusInternalServerError)
148
- return
149
- }
150
-
151
- w.Header().Set("Content-Type", "application/json")
152
- json.NewEncoder(w).Encode(user)
153
- }
154
-
155
- // ✅ Gin 핸들러
156
- func (h *UserHandler) GetUser(c *gin.Context) {
157
- id := c.Param("id")
158
-
159
- user, err := h.service.GetUser(c.Request.Context(), id)
160
- if err != nil {
161
- var notFound *NotFoundError
162
- if errors.As(err, &notFound) {
163
- c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
164
- return
165
- }
166
- c.JSON(http.StatusInternalServerError, gin.H{"error": "서버 오류"})
167
- return
168
- }
169
-
170
- c.JSON(http.StatusOK, user)
171
- }
172
-
173
- // ✅ Echo 핸들러
174
- func (h *UserHandler) GetUser(c echo.Context) error {
175
- id := c.Param("id")
176
-
177
- user, err := h.service.GetUser(c.Request().Context(), id)
178
- if err != nil {
179
- var notFound *NotFoundError
180
- if errors.As(err, &notFound) {
181
- return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
182
- }
183
- return c.JSON(http.StatusInternalServerError, map[string]string{"error": "서버 오류"})
184
- }
185
-
186
- return c.JSON(http.StatusOK, user)
187
- }
188
- ```
189
-
190
- ### 5. 의존성 주입
191
-
192
- ```go
193
- // ✅ 구조체에 의존성 주입
194
- type UserService struct {
195
- repo UserRepository
196
- cache CacheRepository
197
- logger *slog.Logger
198
- }
199
-
200
- func NewUserService(
201
- repo UserRepository,
202
- cache CacheRepository,
203
- logger *slog.Logger,
204
- ) *UserService {
205
- return &UserService{
206
- repo: repo,
207
- cache: cache,
208
- logger: logger,
209
- }
210
- }
211
-
212
- // ✅ 옵션 패턴
213
- type ServerOption func(*Server)
214
-
215
- func WithPort(port int) ServerOption {
216
- return func(s *Server) {
217
- s.port = port
218
- }
219
- }
220
-
221
- func WithTimeout(timeout time.Duration) ServerOption {
222
- return func(s *Server) {
223
- s.timeout = timeout
224
- }
225
- }
226
-
227
- func NewServer(opts ...ServerOption) *Server {
228
- s := &Server{
229
- port: 8080, // 기본값
230
- timeout: 30 * time.Second,
231
- }
232
- for _, opt := range opts {
233
- opt(s)
234
- }
235
- return s
236
- }
237
-
238
- // 사용
239
- server := NewServer(
240
- WithPort(3000),
241
- WithTimeout(60*time.Second),
242
- )
243
- ```
244
-
245
- ### 6. 동시성
246
-
247
- ```go
248
- // ✅ Goroutine + Channel
249
- func ProcessItems(ctx context.Context, items []Item) ([]Result, error) {
250
- results := make(chan Result, len(items))
251
- errs := make(chan error, len(items))
252
-
253
- var wg sync.WaitGroup
254
- for _, item := range items {
255
- wg.Add(1)
256
- go func(item Item) {
257
- defer wg.Done()
258
- result, err := processItem(ctx, item)
259
- if err != nil {
260
- errs <- err
261
- return
262
- }
263
- results <- result
264
- }(item)
265
- }
266
-
267
- // 결과 수집
268
- go func() {
269
- wg.Wait()
270
- close(results)
271
- close(errs)
272
- }()
273
-
274
- var finalResults []Result
275
- for result := range results {
276
- finalResults = append(finalResults, result)
277
- }
278
-
279
- // 첫 번째 에러 반환
280
- select {
281
- case err := <-errs:
282
- return nil, err
283
- default:
284
- return finalResults, nil
285
- }
286
- }
287
-
288
- // ✅ errgroup 사용 (권장)
289
- import "golang.org/x/sync/errgroup"
290
-
291
- func ProcessItems(ctx context.Context, items []Item) ([]Result, error) {
292
- g, ctx := errgroup.WithContext(ctx)
293
- results := make([]Result, len(items))
294
-
295
- for i, item := range items {
296
- i, item := i, item // 클로저 캡처
297
- g.Go(func() error {
298
- result, err := processItem(ctx, item)
299
- if err != nil {
300
- return err
301
- }
302
- results[i] = result
303
- return nil
304
- })
305
- }
306
-
307
- if err := g.Wait(); err != nil {
308
- return nil, err
309
- }
310
- return results, nil
311
- }
312
- ```
313
-
314
- ### 7. 테스트
315
-
316
- ```go
317
- // ✅ 테이블 기반 테스트
318
- func TestAdd(t *testing.T) {
319
- tests := []struct {
320
- name string
321
- a, b int
322
- expected int
323
- }{
324
- {"양수 덧셈", 2, 3, 5},
325
- {"음수 덧셈", -1, -2, -3},
326
- {"영과 덧셈", 0, 5, 5},
327
- }
328
-
329
- for _, tt := range tests {
330
- t.Run(tt.name, func(t *testing.T) {
331
- result := Add(tt.a, tt.b)
332
- if result != tt.expected {
333
- t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, result, tt.expected)
334
- }
335
- })
336
- }
337
- }
338
-
339
- // ✅ Mock 사용 (testify)
340
- type MockUserRepository struct {
341
- mock.Mock
342
- }
343
-
344
- func (m *MockUserRepository) FindByID(ctx context.Context, id string) (*User, error) {
345
- args := m.Called(ctx, id)
346
- if args.Get(0) == nil {
347
- return nil, args.Error(1)
348
- }
349
- return args.Get(0).(*User), args.Error(1)
350
- }
351
-
352
- func TestUserService_GetUser(t *testing.T) {
353
- mockRepo := new(MockUserRepository)
354
- service := NewUserService(mockRepo, nil, slog.Default())
355
-
356
- expectedUser := &User{ID: "123", Name: "테스트"}
357
- mockRepo.On("FindByID", mock.Anything, "123").Return(expectedUser, nil)
358
-
359
- user, err := service.GetUser(context.Background(), "123")
360
-
361
- assert.NoError(t, err)
362
- assert.Equal(t, expectedUser, user)
363
- mockRepo.AssertExpectations(t)
364
- }
365
- ```
366
-
367
- ## 파일 구조
368
-
369
- ```
370
- project/
371
- ├── cmd/
372
- │ └── server/
373
- │ └── main.go # 엔트리포인트
374
- ├── internal/
375
- │ ├── domain/ # 도메인 모델
376
- │ ├── handler/ # HTTP 핸들러
377
- │ ├── service/ # 비즈니스 로직
378
- │ ├── repository/ # 데이터 액세스
379
- │ └── middleware/ # 미들웨어
380
- ├── pkg/ # 외부 공개 패키지
381
- ├── config/ # 설정
382
- ├── migrations/ # DB 마이그레이션
383
- ├── go.mod
384
- └── go.sum
385
- ```
386
-
387
- ## 체크리스트
388
-
389
- - [ ] 에러 항상 처리 (_, err 금지)
390
- - [ ] fmt.Errorf("%w", err)로 에러 래핑
391
- - [ ] Context 첫 번째 인자로 전달
392
- - [ ] 작은 인터페이스 정의
393
- - [ ] 생성자 함수 (NewXxx) 사용
394
- - [ ] 테이블 기반 테스트
395
- - [ ] gofmt, golint, go vet 통과
396
- - [ ] 동시성에서 race condition 주의
1
+ # 🐹 Go 품질 규칙
2
+
3
+ ## 핵심 원칙 (core에서 상속)
4
+
5
+ ```markdown
6
+ ✅ 단일 책임 (SRP)
7
+ ✅ 중복 제거 (DRY)
8
+ ✅ 재사용성
9
+ ✅ 낮은 복잡도
10
+ ✅ 함수 ≤ 30줄
11
+ ✅ 중첩 ≤ 3단계
12
+ ✅ Cyclomatic complexity ≤ 10
13
+ ```
14
+
15
+ ## Go 특화 규칙
16
+
17
+ ### 1. 에러 처리
18
+
19
+ ```go
20
+ // ❌ 에러 무시
21
+ data, _ := ioutil.ReadFile("config.json")
22
+
23
+ // ✅ 에러 항상 처리
24
+ data, err := ioutil.ReadFile("config.json")
25
+ if err != nil {
26
+ return fmt.Errorf("설정 파일 읽기 실패: %w", err)
27
+ }
28
+
29
+ // ✅ 커스텀 에러 타입
30
+ type NotFoundError struct {
31
+ Resource string
32
+ ID string
33
+ }
34
+
35
+ func (e *NotFoundError) Error() string {
36
+ return fmt.Sprintf("%s (ID: %s)를 찾을 수 없습니다", e.Resource, e.ID)
37
+ }
38
+
39
+ // 사용
40
+ func GetUser(id string) (*User, error) {
41
+ user, err := repo.FindByID(id)
42
+ if err != nil {
43
+ return nil, fmt.Errorf("사용자 조회 실패: %w", err)
44
+ }
45
+ if user == nil {
46
+ return nil, &NotFoundError{Resource: "사용자", ID: id}
47
+ }
48
+ return user, nil
49
+ }
50
+ ```
51
+
52
+ ### 2. 구조체와 인터페이스
53
+
54
+ ```go
55
+ // ✅ 구조체 정의
56
+ type User struct {
57
+ ID string `json:"id"`
58
+ Email string `json:"email"`
59
+ Name string `json:"name"`
60
+ CreatedAt time.Time `json:"created_at"`
61
+ UpdatedAt time.Time `json:"updated_at"`
62
+ }
63
+
64
+ // ✅ 생성자 함수
65
+ func NewUser(email, name string) *User {
66
+ now := time.Now()
67
+ return &User{
68
+ ID: uuid.New().String(),
69
+ Email: email,
70
+ Name: name,
71
+ CreatedAt: now,
72
+ UpdatedAt: now,
73
+ }
74
+ }
75
+
76
+ // ✅ 작은 인터페이스 (Go의 철학)
77
+ type Reader interface {
78
+ Read(p []byte) (n int, err error)
79
+ }
80
+
81
+ type Writer interface {
82
+ Write(p []byte) (n int, err error)
83
+ }
84
+
85
+ // ✅ 인터페이스 조합
86
+ type ReadWriter interface {
87
+ Reader
88
+ Writer
89
+ }
90
+
91
+ // ✅ Repository 인터페이스
92
+ type UserRepository interface {
93
+ FindByID(ctx context.Context, id string) (*User, error)
94
+ FindByEmail(ctx context.Context, email string) (*User, error)
95
+ Create(ctx context.Context, user *User) error
96
+ Update(ctx context.Context, user *User) error
97
+ Delete(ctx context.Context, id string) error
98
+ }
99
+ ```
100
+
101
+ ### 3. Context 사용
102
+
103
+ ```go
104
+ // ✅ Context 전파
105
+ func (s *UserService) GetUser(ctx context.Context, id string) (*User, error) {
106
+ // Context를 하위 함수에 전달
107
+ user, err := s.repo.FindByID(ctx, id)
108
+ if err != nil {
109
+ return nil, err
110
+ }
111
+ return user, nil
112
+ }
113
+
114
+ // ✅ Context 타임아웃
115
+ func (h *Handler) HandleRequest(w http.ResponseWriter, r *http.Request) {
116
+ ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
117
+ defer cancel()
118
+
119
+ result, err := h.service.Process(ctx)
120
+ if err != nil {
121
+ if errors.Is(err, context.DeadlineExceeded) {
122
+ http.Error(w, "요청 시간 초과", http.StatusRequestTimeout)
123
+ return
124
+ }
125
+ http.Error(w, err.Error(), http.StatusInternalServerError)
126
+ return
127
+ }
128
+
129
+ json.NewEncoder(w).Encode(result)
130
+ }
131
+ ```
132
+
133
+ ### 4. HTTP 핸들러 (net/http, Gin, Echo)
134
+
135
+ ```go
136
+ // ✅ net/http 핸들러
137
+ func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
138
+ id := chi.URLParam(r, "id")
139
+
140
+ user, err := h.service.GetUser(r.Context(), id)
141
+ if err != nil {
142
+ var notFound *NotFoundError
143
+ if errors.As(err, &notFound) {
144
+ http.Error(w, err.Error(), http.StatusNotFound)
145
+ return
146
+ }
147
+ http.Error(w, "서버 오류", http.StatusInternalServerError)
148
+ return
149
+ }
150
+
151
+ w.Header().Set("Content-Type", "application/json")
152
+ json.NewEncoder(w).Encode(user)
153
+ }
154
+
155
+ // ✅ Gin 핸들러
156
+ func (h *UserHandler) GetUser(c *gin.Context) {
157
+ id := c.Param("id")
158
+
159
+ user, err := h.service.GetUser(c.Request.Context(), id)
160
+ if err != nil {
161
+ var notFound *NotFoundError
162
+ if errors.As(err, &notFound) {
163
+ c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
164
+ return
165
+ }
166
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "서버 오류"})
167
+ return
168
+ }
169
+
170
+ c.JSON(http.StatusOK, user)
171
+ }
172
+
173
+ // ✅ Echo 핸들러
174
+ func (h *UserHandler) GetUser(c echo.Context) error {
175
+ id := c.Param("id")
176
+
177
+ user, err := h.service.GetUser(c.Request().Context(), id)
178
+ if err != nil {
179
+ var notFound *NotFoundError
180
+ if errors.As(err, &notFound) {
181
+ return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
182
+ }
183
+ return c.JSON(http.StatusInternalServerError, map[string]string{"error": "서버 오류"})
184
+ }
185
+
186
+ return c.JSON(http.StatusOK, user)
187
+ }
188
+ ```
189
+
190
+ ### 5. 의존성 주입
191
+
192
+ ```go
193
+ // ✅ 구조체에 의존성 주입
194
+ type UserService struct {
195
+ repo UserRepository
196
+ cache CacheRepository
197
+ logger *slog.Logger
198
+ }
199
+
200
+ func NewUserService(
201
+ repo UserRepository,
202
+ cache CacheRepository,
203
+ logger *slog.Logger,
204
+ ) *UserService {
205
+ return &UserService{
206
+ repo: repo,
207
+ cache: cache,
208
+ logger: logger,
209
+ }
210
+ }
211
+
212
+ // ✅ 옵션 패턴
213
+ type ServerOption func(*Server)
214
+
215
+ func WithPort(port int) ServerOption {
216
+ return func(s *Server) {
217
+ s.port = port
218
+ }
219
+ }
220
+
221
+ func WithTimeout(timeout time.Duration) ServerOption {
222
+ return func(s *Server) {
223
+ s.timeout = timeout
224
+ }
225
+ }
226
+
227
+ func NewServer(opts ...ServerOption) *Server {
228
+ s := &Server{
229
+ port: 8080, // 기본값
230
+ timeout: 30 * time.Second,
231
+ }
232
+ for _, opt := range opts {
233
+ opt(s)
234
+ }
235
+ return s
236
+ }
237
+
238
+ // 사용
239
+ server := NewServer(
240
+ WithPort(3000),
241
+ WithTimeout(60*time.Second),
242
+ )
243
+ ```
244
+
245
+ ### 6. 동시성
246
+
247
+ ```go
248
+ // ✅ Goroutine + Channel
249
+ func ProcessItems(ctx context.Context, items []Item) ([]Result, error) {
250
+ results := make(chan Result, len(items))
251
+ errs := make(chan error, len(items))
252
+
253
+ var wg sync.WaitGroup
254
+ for _, item := range items {
255
+ wg.Add(1)
256
+ go func(item Item) {
257
+ defer wg.Done()
258
+ result, err := processItem(ctx, item)
259
+ if err != nil {
260
+ errs <- err
261
+ return
262
+ }
263
+ results <- result
264
+ }(item)
265
+ }
266
+
267
+ // 결과 수집
268
+ go func() {
269
+ wg.Wait()
270
+ close(results)
271
+ close(errs)
272
+ }()
273
+
274
+ var finalResults []Result
275
+ for result := range results {
276
+ finalResults = append(finalResults, result)
277
+ }
278
+
279
+ // 첫 번째 에러 반환
280
+ select {
281
+ case err := <-errs:
282
+ return nil, err
283
+ default:
284
+ return finalResults, nil
285
+ }
286
+ }
287
+
288
+ // ✅ errgroup 사용 (권장)
289
+ import "golang.org/x/sync/errgroup"
290
+
291
+ func ProcessItems(ctx context.Context, items []Item) ([]Result, error) {
292
+ g, ctx := errgroup.WithContext(ctx)
293
+ results := make([]Result, len(items))
294
+
295
+ for i, item := range items {
296
+ i, item := i, item // 클로저 캡처
297
+ g.Go(func() error {
298
+ result, err := processItem(ctx, item)
299
+ if err != nil {
300
+ return err
301
+ }
302
+ results[i] = result
303
+ return nil
304
+ })
305
+ }
306
+
307
+ if err := g.Wait(); err != nil {
308
+ return nil, err
309
+ }
310
+ return results, nil
311
+ }
312
+ ```
313
+
314
+ ### 7. 테스트
315
+
316
+ ```go
317
+ // ✅ 테이블 기반 테스트
318
+ func TestAdd(t *testing.T) {
319
+ tests := []struct {
320
+ name string
321
+ a, b int
322
+ expected int
323
+ }{
324
+ {"양수 덧셈", 2, 3, 5},
325
+ {"음수 덧셈", -1, -2, -3},
326
+ {"영과 덧셈", 0, 5, 5},
327
+ }
328
+
329
+ for _, tt := range tests {
330
+ t.Run(tt.name, func(t *testing.T) {
331
+ result := Add(tt.a, tt.b)
332
+ if result != tt.expected {
333
+ t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, result, tt.expected)
334
+ }
335
+ })
336
+ }
337
+ }
338
+
339
+ // ✅ Mock 사용 (testify)
340
+ type MockUserRepository struct {
341
+ mock.Mock
342
+ }
343
+
344
+ func (m *MockUserRepository) FindByID(ctx context.Context, id string) (*User, error) {
345
+ args := m.Called(ctx, id)
346
+ if args.Get(0) == nil {
347
+ return nil, args.Error(1)
348
+ }
349
+ return args.Get(0).(*User), args.Error(1)
350
+ }
351
+
352
+ func TestUserService_GetUser(t *testing.T) {
353
+ mockRepo := new(MockUserRepository)
354
+ service := NewUserService(mockRepo, nil, slog.Default())
355
+
356
+ expectedUser := &User{ID: "123", Name: "테스트"}
357
+ mockRepo.On("FindByID", mock.Anything, "123").Return(expectedUser, nil)
358
+
359
+ user, err := service.GetUser(context.Background(), "123")
360
+
361
+ assert.NoError(t, err)
362
+ assert.Equal(t, expectedUser, user)
363
+ mockRepo.AssertExpectations(t)
364
+ }
365
+ ```
366
+
367
+ ## 파일 구조
368
+
369
+ ```
370
+ project/
371
+ ├── cmd/
372
+ │ └── server/
373
+ │ └── main.go # 엔트리포인트
374
+ ├── internal/
375
+ │ ├── domain/ # 도메인 모델
376
+ │ ├── handler/ # HTTP 핸들러
377
+ │ ├── service/ # 비즈니스 로직
378
+ │ ├── repository/ # 데이터 액세스
379
+ │ └── middleware/ # 미들웨어
380
+ ├── pkg/ # 외부 공개 패키지
381
+ ├── config/ # 설정
382
+ ├── migrations/ # DB 마이그레이션
383
+ ├── go.mod
384
+ └── go.sum
385
+ ```
386
+
387
+ ## 체크리스트
388
+
389
+ - [ ] 에러 항상 처리 (_, err 금지)
390
+ - [ ] fmt.Errorf("%w", err)로 에러 래핑
391
+ - [ ] Context 첫 번째 인자로 전달
392
+ - [ ] 작은 인터페이스 정의
393
+ - [ ] 생성자 함수 (NewXxx) 사용
394
+ - [ ] 테이블 기반 테스트
395
+ - [ ] gofmt, golint, go vet 통과
396
+ - [ ] 동시성에서 race condition 주의