codesyncer 1.1.0 → 2.0.1
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/README.ko.md +96 -23
- package/README.md +96 -23
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +120 -393
- package/dist/commands/init.js.map +1 -1
- package/dist/templates/en/comment_guide.md +460 -202
- package/dist/templates/en/setup_guide.md +296 -0
- package/dist/templates/ko/comment_guide.md +461 -203
- package/dist/templates/ko/setup_guide.md +296 -0
- package/package.json +1 -1
- package/src/templates/en/comment_guide.md +460 -202
- package/src/templates/en/setup_guide.md +296 -0
- package/src/templates/ko/comment_guide.md +461 -203
- package/src/templates/ko/setup_guide.md +296 -0
|
@@ -1,324 +1,582 @@
|
|
|
1
1
|
# 주석 작성 가이드
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> **주석으로 모든 컨텍스트 관리** - 코드가 곧 문서
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## 🎯 핵심 원칙
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
**모든 결정과 맥락은 코드에 직접 기록합니다.**
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
- ❌ 별도 문서에 품질 기준 작성 → AI가 읽지 못함
|
|
12
|
+
- ✅ 코드 주석으로 품질 기준 설명 → 영구 보존
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 📋 주석 태그 시스템 (10가지)
|
|
17
|
+
|
|
18
|
+
### 기본 태그 (5가지)
|
|
19
|
+
|
|
20
|
+
| 태그 | 용도 | 필수 정보 |
|
|
21
|
+
|------|------|----------|
|
|
22
|
+
| `@codesyncer-inference` | 추론 + 근거 | "무엇" + "왜" |
|
|
23
|
+
| `@codesyncer-decision` | 결정 사항 | [날짜] + 이유 |
|
|
24
|
+
| `@codesyncer-todo` | 확인 필요 | 구체적인 작업 |
|
|
25
|
+
| `@codesyncer-context` | 비즈니스 맥락 | 도메인 지식 |
|
|
26
|
+
| `@codesyncer-rule` | 특별 규칙 | 예외 사항 |
|
|
27
|
+
|
|
28
|
+
### 확장 태그 (5가지) - 컨텍스트 완전 보존
|
|
29
|
+
|
|
30
|
+
| 태그 | 용도 | 언제 사용 |
|
|
31
|
+
|------|------|----------|
|
|
32
|
+
| `@codesyncer-why` | 이유 상세 설명 | 코드만으로 이해 어려울 때 |
|
|
33
|
+
| `@codesyncer-tradeoff` | 장단점 | 선택의 trade-off 있을 때 |
|
|
34
|
+
| `@codesyncer-alternative` | 대안들 | 다른 방법 고려했을 때 |
|
|
35
|
+
| `@codesyncer-pattern` | 패턴명 | 재사용 가능한 패턴 |
|
|
36
|
+
| `@codesyncer-reference` | 참조 링크 | 외부 문서/이슈 참조 |
|
|
18
37
|
|
|
19
38
|
### 레거시 호환
|
|
20
39
|
|
|
21
|
-
기존 `@claude-*` 태그도 완전히 호환됩니다:
|
|
22
40
|
```typescript
|
|
23
|
-
@claude
|
|
24
|
-
@claude-inference = @codesyncer-inference
|
|
25
|
-
@claude-decision = @codesyncer-decision
|
|
26
|
-
@claude-todo = @codesyncer-todo
|
|
27
|
-
@claude-context = @codesyncer-context
|
|
41
|
+
@claude-* = @codesyncer-* // 기존 태그도 완전 호환
|
|
28
42
|
```
|
|
29
43
|
|
|
30
44
|
---
|
|
31
45
|
|
|
32
|
-
##
|
|
33
|
-
|
|
34
|
-
### 1. 📄 파일 레벨 (JSDoc)
|
|
46
|
+
## 💡 실전 예시: 모든 컨텍스트를 주석으로
|
|
35
47
|
|
|
36
|
-
|
|
48
|
+
### 1️⃣ 품질 기준을 주석으로 관리
|
|
37
49
|
|
|
38
50
|
```typescript
|
|
39
51
|
/**
|
|
40
|
-
*
|
|
52
|
+
* 결제 처리 서비스
|
|
41
53
|
*
|
|
42
|
-
* @codesyncer-context
|
|
43
|
-
* @codesyncer-rule
|
|
44
|
-
* @
|
|
45
|
-
*
|
|
54
|
+
* @codesyncer-context 실시간 카드 결제 처리 (PG사: Stripe)
|
|
55
|
+
* @codesyncer-rule 모든 금액은 정수로 처리 (소수점 오류 방지)
|
|
56
|
+
* @codesyncer-pattern Transaction Script (단순 결제는 도메인 모델 불필요)
|
|
57
|
+
*
|
|
58
|
+
* 품질 기준:
|
|
59
|
+
* - 타임아웃: 30초 (PG사 권장)
|
|
60
|
+
* - 재시도: 3회 (멱등성 보장 필수)
|
|
61
|
+
* - 로깅: 모든 결제 시도 기록
|
|
62
|
+
* - 에러 처리: 사용자 친화적 메시지
|
|
46
63
|
*/
|
|
47
|
-
|
|
64
|
+
export class PaymentService {
|
|
65
|
+
/**
|
|
66
|
+
* 결제 실행
|
|
67
|
+
*
|
|
68
|
+
* @codesyncer-why 동기 처리로 구현 (결제 완료 즉시 확인 필요)
|
|
69
|
+
* @codesyncer-tradeoff 동기: 빠른 피드백 | 비동기: 높은 처리량
|
|
70
|
+
* @codesyncer-decision [2024-11-12] 동기 방식 선택 (UX 우선)
|
|
71
|
+
*/
|
|
72
|
+
async processPayment(
|
|
73
|
+
amount: number,
|
|
74
|
+
cardToken: string
|
|
75
|
+
): Promise<PaymentResult> {
|
|
76
|
+
// @codesyncer-inference: 최소 금액 100원 (PG사 정책)
|
|
77
|
+
if (amount < 100) {
|
|
78
|
+
throw new ValidationError('최소 결제 금액은 100원입니다');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// @codesyncer-why: 멱등성 키 생성 (중복 결제 방지)
|
|
82
|
+
const idempotencyKey = this.generateIdempotencyKey(amount, cardToken);
|
|
83
|
+
|
|
84
|
+
// @codesyncer-pattern: Retry with Exponential Backoff
|
|
85
|
+
return await this.retryWithBackoff(async () => {
|
|
86
|
+
return await stripe.charge({
|
|
87
|
+
amount,
|
|
88
|
+
source: cardToken,
|
|
89
|
+
idempotencyKey
|
|
90
|
+
});
|
|
91
|
+
}, {
|
|
92
|
+
maxRetries: 3,
|
|
93
|
+
initialDelay: 1000
|
|
94
|
+
});
|
|
95
|
+
}
|
|
48
96
|
|
|
49
|
-
|
|
97
|
+
/**
|
|
98
|
+
* @codesyncer-pattern Exponential Backoff
|
|
99
|
+
* @codesyncer-reference https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
|
|
100
|
+
*/
|
|
101
|
+
private async retryWithBackoff<T>(
|
|
102
|
+
fn: () => Promise<T>,
|
|
103
|
+
options: RetryOptions
|
|
104
|
+
): Promise<T> {
|
|
105
|
+
// ... 구현
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
50
109
|
|
|
51
|
-
|
|
110
|
+
### 2️⃣ 복잡한 비즈니스 로직 설명
|
|
52
111
|
|
|
53
|
-
```
|
|
112
|
+
```typescript
|
|
54
113
|
/**
|
|
55
|
-
*
|
|
114
|
+
* 할인 계산기
|
|
56
115
|
*
|
|
57
|
-
* @codesyncer-context
|
|
58
|
-
*
|
|
59
|
-
*
|
|
116
|
+
* @codesyncer-context 복합 할인 정책 (중복 적용 가능)
|
|
117
|
+
* - 회원 등급 할인: 5-15%
|
|
118
|
+
* - 쿠폰 할인: 고정 금액 또는 비율
|
|
119
|
+
* - 프로모션 할인: 특정 조건 충족 시
|
|
120
|
+
*
|
|
121
|
+
* @codesyncer-decision [2024-11-10] 할인 순서 고정 (마케팅팀 합의)
|
|
122
|
+
* 1. 회원 등급 할인
|
|
123
|
+
* 2. 쿠폰 할인
|
|
124
|
+
* 3. 프로모션 할인
|
|
125
|
+
*
|
|
126
|
+
* @codesyncer-why 순서가 중요함 (최종 금액이 달라짐)
|
|
127
|
+
* @codesyncer-alternative 할인율 합산 후 적용 → 거부됨 (복잡한 케이스 처리 어려움)
|
|
60
128
|
*/
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
129
|
+
function calculateFinalPrice(
|
|
130
|
+
basePrice: number,
|
|
131
|
+
user: User,
|
|
132
|
+
coupon?: Coupon,
|
|
133
|
+
promotion?: Promotion
|
|
134
|
+
): number {
|
|
135
|
+
// @codesyncer-context: 모든 중간 계산 저장 (환불 시 추적용)
|
|
136
|
+
const breakdown: PriceBreakdown = {
|
|
137
|
+
basePrice,
|
|
138
|
+
discounts: []
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
let currentPrice = basePrice;
|
|
142
|
+
|
|
143
|
+
// Step 1: 회원 등급 할인
|
|
144
|
+
// @codesyncer-inference: GOLD 15%, SILVER 10%, BRONZE 5% (일반적 패턴)
|
|
145
|
+
const memberDiscount = this.calculateMemberDiscount(user.tier);
|
|
146
|
+
if (memberDiscount > 0) {
|
|
147
|
+
currentPrice -= memberDiscount;
|
|
148
|
+
breakdown.discounts.push({
|
|
149
|
+
type: 'MEMBER',
|
|
150
|
+
amount: memberDiscount
|
|
151
|
+
});
|
|
152
|
+
}
|
|
69
153
|
|
|
70
|
-
|
|
71
|
-
// @codesyncer-
|
|
72
|
-
|
|
154
|
+
// Step 2: 쿠폰 할인
|
|
155
|
+
// @codesyncer-rule: 쿠폰은 할인된 금액에 적용 (중요!)
|
|
156
|
+
if (coupon) {
|
|
157
|
+
const couponDiscount = this.applyCoupon(currentPrice, coupon);
|
|
158
|
+
currentPrice -= couponDiscount;
|
|
159
|
+
breakdown.discounts.push({
|
|
160
|
+
type: 'COUPON',
|
|
161
|
+
amount: couponDiscount,
|
|
162
|
+
couponId: coupon.id
|
|
163
|
+
});
|
|
164
|
+
}
|
|
73
165
|
|
|
74
|
-
//
|
|
75
|
-
|
|
166
|
+
// Step 3: 프로모션 할인
|
|
167
|
+
// @codesyncer-todo: 프로모션 중복 적용 정책 확인 필요
|
|
168
|
+
if (promotion) {
|
|
169
|
+
const promoDiscount = this.applyPromotion(currentPrice, promotion);
|
|
170
|
+
currentPrice -= promoDiscount;
|
|
171
|
+
breakdown.discounts.push({
|
|
172
|
+
type: 'PROMOTION',
|
|
173
|
+
amount: promoDiscount,
|
|
174
|
+
promotionId: promotion.id
|
|
175
|
+
});
|
|
176
|
+
}
|
|
76
177
|
|
|
77
|
-
// @codesyncer-
|
|
78
|
-
|
|
79
|
-
// @codesyncer-inference: deleted_at 플래그 사용 (복구 기능용)
|
|
80
|
-
return db.update(id, { deleted_at: new Date() });
|
|
178
|
+
// @codesyncer-rule: 최종 금액은 0 이상이어야 함
|
|
179
|
+
return Math.max(0, currentPrice);
|
|
81
180
|
}
|
|
82
|
-
|
|
83
|
-
const maxRetry = 3; // @codesyncer-inference: 3회 재시도 (안정성)
|
|
84
181
|
```
|
|
85
182
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
## ✅ 좋은 주석 예시
|
|
183
|
+
### 3️⃣ 성능 최적화 기록
|
|
89
184
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
```tsx
|
|
185
|
+
```typescript
|
|
93
186
|
/**
|
|
94
|
-
*
|
|
187
|
+
* 주문 목록 조회 API
|
|
95
188
|
*
|
|
96
|
-
* @codesyncer-context
|
|
97
|
-
* -
|
|
98
|
-
* - 3만원 미만: 3,000원
|
|
99
|
-
* - 제주/도서산간: +3,000원
|
|
189
|
+
* @codesyncer-context 주문이 많은 사용자는 10만 건 이상 (성능 이슈)
|
|
190
|
+
* @codesyncer-decision [2024-11-12] 페이지네이션 + 인덱스 + 캐싱
|
|
100
191
|
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
192
|
+
* 성능 목표:
|
|
193
|
+
* - 응답 시간: < 500ms (P95)
|
|
194
|
+
* - 동시 접속: 1000 TPS
|
|
195
|
+
* - 캐시 히트율: > 80%
|
|
103
196
|
*/
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
197
|
+
export class OrderController {
|
|
198
|
+
/**
|
|
199
|
+
* @codesyncer-pattern Cursor-based Pagination
|
|
200
|
+
* @codesyncer-why Offset 페이징은 뒤로 갈수록 느려짐 (OFFSET 10000)
|
|
201
|
+
* @codesyncer-tradeoff Cursor: 빠름 | Offset: 페이지 번호 표시 가능
|
|
202
|
+
* @codesyncer-alternative Offset 페이징 → 테스트 결과 P95 3초 (거부)
|
|
203
|
+
* @codesyncer-reference https://use-the-index-luke.com/no-offset
|
|
204
|
+
*/
|
|
205
|
+
async getOrders(userId: string, cursor?: string, limit = 20) {
|
|
206
|
+
// @codesyncer-inference: Redis 캐싱 5분 (실시간성 vs 성능)
|
|
207
|
+
const cacheKey = `orders:${userId}:${cursor}`;
|
|
208
|
+
const cached = await redis.get(cacheKey);
|
|
209
|
+
if (cached) {
|
|
210
|
+
return JSON.parse(cached);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// @codesyncer-pattern: Index Hint
|
|
214
|
+
// @codesyncer-why userId + createdAt 복합 인덱스 사용 강제
|
|
215
|
+
const orders = await db.query(`
|
|
216
|
+
SELECT /*+ INDEX(orders idx_user_created) */
|
|
217
|
+
id, total, status, created_at
|
|
218
|
+
FROM orders
|
|
219
|
+
WHERE user_id = ?
|
|
220
|
+
${cursor ? 'AND created_at < ?' : ''}
|
|
221
|
+
ORDER BY created_at DESC
|
|
222
|
+
LIMIT ?
|
|
223
|
+
`, cursor ? [userId, cursor, limit] : [userId, limit]);
|
|
224
|
+
|
|
225
|
+
const result = {
|
|
226
|
+
data: orders,
|
|
227
|
+
nextCursor: orders.length === limit
|
|
228
|
+
? orders[orders.length - 1].created_at
|
|
229
|
+
: null
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// @codesyncer-inference: 5분 TTL (주문은 자주 변경되지 않음)
|
|
233
|
+
await redis.setex(cacheKey, 300, JSON.stringify(result));
|
|
234
|
+
|
|
235
|
+
return result;
|
|
116
236
|
}
|
|
117
|
-
|
|
118
|
-
const baseFee = BASIC_FEE;
|
|
119
|
-
const extraFee = EXTRA_FEE_REGIONS.includes(region) ? 3000 : 0;
|
|
120
|
-
|
|
121
|
-
return baseFee + extraFee;
|
|
122
237
|
}
|
|
123
238
|
```
|
|
124
239
|
|
|
125
|
-
###
|
|
240
|
+
### 4️⃣ 보안 요구사항 명시
|
|
126
241
|
|
|
127
|
-
```
|
|
242
|
+
```typescript
|
|
128
243
|
/**
|
|
129
|
-
* 사용자
|
|
244
|
+
* 사용자 인증 미들웨어
|
|
245
|
+
*
|
|
246
|
+
* @codesyncer-context 금융 서비스 (보안 최우선)
|
|
247
|
+
* @codesyncer-rule OWASP Top 10 준수 필수
|
|
130
248
|
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
249
|
+
* 보안 체크리스트:
|
|
250
|
+
* ✅ SQL Injection 방지 (Prepared Statement)
|
|
251
|
+
* ✅ XSS 방지 (CSP 헤더)
|
|
252
|
+
* ✅ CSRF 방지 (토큰 검증)
|
|
253
|
+
* ✅ Rate Limiting (분당 100 요청)
|
|
254
|
+
* ✅ 민감 정보 로깅 금지
|
|
133
255
|
*/
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
256
|
+
export async function authenticate(req: Request, res: Response, next: NextFunction) {
|
|
257
|
+
try {
|
|
258
|
+
// @codesyncer-rule: 토큰은 httpOnly 쿠키에서만 (XSS 방지)
|
|
259
|
+
const token = req.cookies.access_token;
|
|
260
|
+
|
|
261
|
+
if (!token) {
|
|
262
|
+
// @codesyncer-why: 401 vs 403 구분 (보안 best practice)
|
|
263
|
+
// 401: 인증 안됨 | 403: 권한 없음
|
|
264
|
+
return res.status(401).json({ error: 'Authentication required' });
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// @codesyncer-decision [2024-11-12] JWT 대신 세션 사용 (더 안전)
|
|
268
|
+
// @codesyncer-tradeoff JWT: Stateless | Session: Revoke 가능
|
|
269
|
+
const session = await sessionStore.get(token);
|
|
270
|
+
|
|
271
|
+
if (!session) {
|
|
272
|
+
// @codesyncer-why: 에러 메시지 최소화 (공격자에게 정보 제공 최소화)
|
|
273
|
+
return res.status(401).json({ error: 'Invalid token' });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// @codesyncer-pattern: Session Rotation
|
|
277
|
+
// @codesyncer-reference: OWASP Session Management Cheat Sheet
|
|
278
|
+
if (session.shouldRotate()) {
|
|
279
|
+
const newToken = await sessionStore.rotate(session.id);
|
|
280
|
+
res.cookie('access_token', newToken, {
|
|
281
|
+
httpOnly: true,
|
|
282
|
+
secure: true,
|
|
283
|
+
sameSite: 'strict'
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
req.user = session.user;
|
|
288
|
+
next();
|
|
289
|
+
|
|
290
|
+
} catch (error) {
|
|
291
|
+
// @codesyncer-rule: 민감 정보 로깅 금지
|
|
292
|
+
logger.error('Authentication error', {
|
|
293
|
+
// ❌ token, password, email 등 민감 정보 절대 금지
|
|
294
|
+
ip: req.ip,
|
|
295
|
+
userAgent: req.get('user-agent')
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
return res.status(500).json({ error: 'Internal server error' });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
```
|
|
139
302
|
|
|
140
|
-
|
|
141
|
-
passwordHash: string;
|
|
303
|
+
### 5️⃣ 에러 핸들링 전략
|
|
142
304
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
305
|
+
```typescript
|
|
306
|
+
/**
|
|
307
|
+
* 외부 API 호출 래퍼
|
|
308
|
+
*
|
|
309
|
+
* @codesyncer-context 외부 서비스 불안정 (SLA 95%)
|
|
310
|
+
* @codesyncer-pattern Circuit Breaker + Retry + Timeout
|
|
311
|
+
* @codesyncer-reference Netflix Hystrix pattern
|
|
312
|
+
*
|
|
313
|
+
* 에러 처리 전략:
|
|
314
|
+
* - Timeout: 30초
|
|
315
|
+
* - Retry: 3회 (Exponential Backoff)
|
|
316
|
+
* - Circuit Breaker: 5번 실패 시 open
|
|
317
|
+
* - Fallback: 캐시된 데이터 반환
|
|
318
|
+
*/
|
|
319
|
+
export class ExternalApiClient {
|
|
320
|
+
private circuitBreaker = new CircuitBreaker({
|
|
321
|
+
failureThreshold: 5,
|
|
322
|
+
resetTimeout: 60000
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* @codesyncer-why 모든 에러를 한 곳에서 처리 (일관성)
|
|
327
|
+
* @codesyncer-alternative 각 호출마다 try-catch → 코드 중복 심함
|
|
328
|
+
*/
|
|
329
|
+
async call<T>(
|
|
330
|
+
endpoint: string,
|
|
331
|
+
options?: RequestOptions
|
|
332
|
+
): Promise<Result<T>> {
|
|
333
|
+
// @codesyncer-pattern: Circuit Breaker
|
|
334
|
+
if (this.circuitBreaker.isOpen()) {
|
|
335
|
+
logger.warn('Circuit breaker is open', { endpoint });
|
|
336
|
+
return this.getFallback<T>(endpoint);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
try {
|
|
340
|
+
// @codesyncer-inference: 30초 타임아웃 (외부 API 권장값)
|
|
341
|
+
const response = await this.retryWithTimeout(
|
|
342
|
+
() => fetch(endpoint, options),
|
|
343
|
+
{ timeout: 30000, maxRetries: 3 }
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
this.circuitBreaker.recordSuccess();
|
|
347
|
+
return Result.ok(response.data);
|
|
348
|
+
|
|
349
|
+
} catch (error) {
|
|
350
|
+
this.circuitBreaker.recordFailure();
|
|
351
|
+
|
|
352
|
+
// @codesyncer-pattern: Error Classification
|
|
353
|
+
if (error instanceof TimeoutError) {
|
|
354
|
+
logger.warn('API timeout', { endpoint, duration: error.duration });
|
|
355
|
+
return this.getFallback<T>(endpoint);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (error instanceof NetworkError) {
|
|
359
|
+
logger.error('Network error', { endpoint, error });
|
|
360
|
+
return this.getFallback<T>(endpoint);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// @codesyncer-why: 예상치 못한 에러는 전파 (상위에서 처리)
|
|
364
|
+
throw error;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
146
367
|
|
|
147
|
-
|
|
148
|
-
|
|
368
|
+
/**
|
|
369
|
+
* @codesyncer-pattern: Fallback with Stale Cache
|
|
370
|
+
* @codesyncer-why 오래된 데이터라도 없는 것보다 낫다
|
|
371
|
+
*/
|
|
372
|
+
private async getFallback<T>(endpoint: string): Promise<Result<T>> {
|
|
373
|
+
const staleData = await cache.getStale<T>(endpoint);
|
|
374
|
+
if (staleData) {
|
|
375
|
+
logger.info('Returning stale cache', { endpoint });
|
|
376
|
+
return Result.ok(staleData, { isStale: true });
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return Result.error('Service unavailable');
|
|
380
|
+
}
|
|
149
381
|
}
|
|
150
382
|
```
|
|
151
383
|
|
|
152
|
-
###
|
|
384
|
+
### 6️⃣ 테스트 전략 문서화
|
|
153
385
|
|
|
154
|
-
```
|
|
386
|
+
```typescript
|
|
155
387
|
/**
|
|
156
|
-
*
|
|
388
|
+
* 결제 서비스 테스트
|
|
389
|
+
*
|
|
390
|
+
* @codesyncer-context 결제는 critical path (버그 허용 불가)
|
|
157
391
|
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
392
|
+
* 테스트 전략:
|
|
393
|
+
* - Unit: 모든 public 메서드
|
|
394
|
+
* - Integration: PG사 API 호출 (Mock)
|
|
395
|
+
* - E2E: 실제 결제 플로우 (Staging)
|
|
396
|
+
* - 커버리지 목표: 95% 이상
|
|
397
|
+
*
|
|
398
|
+
* @codesyncer-rule 결제 로직 수정 시 QA 필수 승인
|
|
161
399
|
*/
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
400
|
+
describe('PaymentService', () => {
|
|
401
|
+
describe('processPayment', () => {
|
|
402
|
+
/**
|
|
403
|
+
* @codesyncer-pattern: AAA (Arrange-Act-Assert)
|
|
404
|
+
* @codesyncer-why 테스트 가독성과 유지보수성
|
|
405
|
+
*/
|
|
406
|
+
it('should process payment successfully', async () => {
|
|
407
|
+
// Arrange: 테스트 데이터 준비
|
|
408
|
+
const service = new PaymentService();
|
|
409
|
+
const amount = 10000;
|
|
410
|
+
const cardToken = 'tok_test_1234';
|
|
411
|
+
|
|
412
|
+
// @codesyncer-inference: PG사 API는 Mock (실제 과금 방지)
|
|
413
|
+
const mockStripe = jest.spyOn(stripe, 'charge')
|
|
414
|
+
.mockResolvedValue({ id: 'ch_1234', status: 'succeeded' });
|
|
415
|
+
|
|
416
|
+
// Act: 실제 실행
|
|
417
|
+
const result = await service.processPayment(amount, cardToken);
|
|
418
|
+
|
|
419
|
+
// Assert: 결과 검증
|
|
420
|
+
expect(result.isSuccess).toBe(true);
|
|
421
|
+
expect(result.data.status).toBe('succeeded');
|
|
422
|
+
|
|
423
|
+
// @codesyncer-why: 호출 파라미터 검증 (올바른 값 전달 확인)
|
|
424
|
+
expect(mockStripe).toHaveBeenCalledWith({
|
|
425
|
+
amount,
|
|
426
|
+
source: cardToken,
|
|
427
|
+
idempotencyKey: expect.any(String)
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* @codesyncer-pattern: Edge Case Testing
|
|
433
|
+
* @codesyncer-why 경계 조건에서 버그 많이 발생
|
|
434
|
+
*/
|
|
435
|
+
it('should reject payment below minimum amount', async () => {
|
|
436
|
+
const service = new PaymentService();
|
|
437
|
+
|
|
438
|
+
// @codesyncer-context: 최소 금액 100원 (PG사 정책)
|
|
439
|
+
await expect(
|
|
440
|
+
service.processPayment(99, 'tok_test')
|
|
441
|
+
).rejects.toThrow('최소 결제 금액은 100원입니다');
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
});
|
|
175
445
|
```
|
|
176
446
|
|
|
177
447
|
---
|
|
178
448
|
|
|
179
|
-
##
|
|
449
|
+
## 🎯 주석 작성 원칙
|
|
180
450
|
|
|
181
|
-
###
|
|
451
|
+
### ✅ DO (해야 할 것)
|
|
182
452
|
|
|
183
|
-
```
|
|
184
|
-
//
|
|
185
|
-
// @codesyncer-inference:
|
|
186
|
-
const
|
|
453
|
+
```typescript
|
|
454
|
+
// ✅ 구체적인 이유와 근거
|
|
455
|
+
// @codesyncer-inference: 페이지 크기 20 (사용자 연구 결과, 스크롤 3번 이내)
|
|
456
|
+
const PAGE_SIZE = 20;
|
|
187
457
|
|
|
188
|
-
//
|
|
189
|
-
// @codesyncer-decision:
|
|
190
|
-
const API_URL = '/api/new';
|
|
458
|
+
// ✅ 날짜와 맥락
|
|
459
|
+
// @codesyncer-decision: [2024-11-12] PostgreSQL 선택 (복잡한 쿼리 + ACID 필요)
|
|
191
460
|
|
|
192
|
-
//
|
|
193
|
-
// @codesyncer-
|
|
194
|
-
function doSomething() {}
|
|
461
|
+
// ✅ Trade-off 명시
|
|
462
|
+
// @codesyncer-tradeoff: 캐싱으로 성능 50% 개선, 메모리 사용 20% 증가
|
|
195
463
|
|
|
196
|
-
//
|
|
197
|
-
// @codesyncer-
|
|
198
|
-
const IMPORTANT_VALUE = 42;
|
|
199
|
-
```
|
|
464
|
+
// ✅ 대안 기록
|
|
465
|
+
// @codesyncer-alternative: MongoDB 검토 → JSON 스키마 변경 빈번해 거부
|
|
200
466
|
|
|
201
|
-
|
|
467
|
+
// ✅ 패턴 명시 (재사용)
|
|
468
|
+
// @codesyncer-pattern: Repository Pattern (데이터 접근 추상화)
|
|
469
|
+
```
|
|
202
470
|
|
|
203
|
-
|
|
204
|
-
// ✅ 구체적인 근거
|
|
205
|
-
// @codesyncer-inference: 기본값 10 (일반적인 재시도 대기 시간)
|
|
206
|
-
const RETRY_DELAY = 10;
|
|
471
|
+
### ❌ DON'T (하지 말 것)
|
|
207
472
|
|
|
208
|
-
|
|
209
|
-
//
|
|
210
|
-
|
|
473
|
+
```typescript
|
|
474
|
+
// ❌ 너무 모호
|
|
475
|
+
// @codesyncer-inference: 이렇게 함
|
|
476
|
+
const value = 10;
|
|
211
477
|
|
|
212
|
-
//
|
|
213
|
-
// @codesyncer-
|
|
214
|
-
function
|
|
478
|
+
// ❌ 코드 그대로 반복
|
|
479
|
+
// @codesyncer-context: 사용자 생성 // 코드 보면 알 수 있음
|
|
480
|
+
function createUser() {}
|
|
215
481
|
|
|
216
|
-
//
|
|
217
|
-
// @codesyncer-
|
|
218
|
-
const
|
|
482
|
+
// ❌ 근거 없음
|
|
483
|
+
// @codesyncer-decision: 변경함
|
|
484
|
+
const API_URL = '/new';
|
|
219
485
|
```
|
|
220
486
|
|
|
221
487
|
---
|
|
222
488
|
|
|
223
489
|
## 🔍 주석 검색
|
|
224
490
|
|
|
225
|
-
###
|
|
491
|
+
### 프로젝트 전체 검색
|
|
226
492
|
|
|
227
493
|
```bash
|
|
228
|
-
# 모든 추론
|
|
494
|
+
# 모든 추론 찾기
|
|
229
495
|
grep -r "@codesyncer-inference" ./src
|
|
230
496
|
|
|
231
|
-
#
|
|
497
|
+
# 확인 필요한 TODO
|
|
232
498
|
grep -r "@codesyncer-todo" ./src
|
|
233
499
|
|
|
234
|
-
# 의논 결정 사항
|
|
500
|
+
# 의논 후 결정 사항
|
|
235
501
|
grep -r "@codesyncer-decision" ./src
|
|
236
502
|
|
|
237
|
-
#
|
|
238
|
-
grep -r "@codesyncer-
|
|
503
|
+
# 패턴 찾기 (재사용)
|
|
504
|
+
grep -r "@codesyncer-pattern" ./src
|
|
239
505
|
|
|
240
|
-
#
|
|
241
|
-
grep -r "@codesyncer-
|
|
506
|
+
# 특정 패턴 찾기
|
|
507
|
+
grep -r "@codesyncer-pattern.*Retry" ./src
|
|
242
508
|
```
|
|
243
509
|
|
|
244
510
|
### VS Code 검색
|
|
245
511
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
512
|
+
```
|
|
513
|
+
Cmd/Ctrl + Shift + F
|
|
514
|
+
→ @codesyncer-todo
|
|
515
|
+
→ src/**/*.{ts,tsx,js,jsx}
|
|
516
|
+
```
|
|
249
517
|
|
|
250
518
|
---
|
|
251
519
|
|
|
252
520
|
## 📊 주석 통계
|
|
253
521
|
|
|
254
|
-
ARCHITECTURE.md
|
|
522
|
+
ARCHITECTURE.md에 자동 집계:
|
|
255
523
|
|
|
256
524
|
```markdown
|
|
257
525
|
## 주석 태그 통계
|
|
258
526
|
- @codesyncer-inference: 45개
|
|
259
527
|
- @codesyncer-decision: 12개
|
|
260
|
-
- @codesyncer-
|
|
261
|
-
- @codesyncer-
|
|
262
|
-
- @codesyncer-context: 15개
|
|
528
|
+
- @codesyncer-pattern: 8개
|
|
529
|
+
- @codesyncer-todo: 3개
|
|
263
530
|
```
|
|
264
531
|
|
|
265
|
-
"통계 업데이트"
|
|
532
|
+
명령어: `"통계 업데이트"`
|
|
266
533
|
|
|
267
534
|
---
|
|
268
535
|
|
|
269
|
-
## 💡
|
|
536
|
+
## 💡 주석이 문서를 대체하는 이유
|
|
270
537
|
|
|
271
|
-
###
|
|
272
|
-
|
|
273
|
-
```tsx
|
|
274
|
-
// ❌ @codesyncer-inference: useState 사용
|
|
275
|
-
// ✅ @codesyncer-inference: useState 사용 (간단한 로컬 상태, Zustand 불필요)
|
|
538
|
+
### 기존 방식의 문제
|
|
276
539
|
```
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
540
|
+
❌ 별도 문서 작성
|
|
541
|
+
→ AI가 읽지 못함
|
|
542
|
+
→ 코드와 문서 불일치
|
|
543
|
+
→ 문서 업데이트 안됨
|
|
544
|
+
|
|
545
|
+
❌ 긴 가이드 문서
|
|
546
|
+
→ AI context 초과
|
|
547
|
+
→ 실제로 적용 안됨
|
|
548
|
+
→ 까먹음
|
|
283
549
|
```
|
|
284
550
|
|
|
285
|
-
###
|
|
286
|
-
|
|
287
|
-
```tsx
|
|
288
|
-
// ❌ @codesyncer-todo: 수정 필요
|
|
289
|
-
// ✅ @codesyncer-todo: 에러 바운더리 추가 (API 실패 시 폴백 UI)
|
|
551
|
+
### 주석 기반의 장점
|
|
290
552
|
```
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
```tsx
|
|
302
|
-
// ❌ @codesyncer-rule: TypeScript 사용 (이건 당연함)
|
|
303
|
-
// ✅ @codesyncer-rule: 이 파일만 any 타입 허용 (외부 라이브러리 타입 없음)
|
|
553
|
+
✅ 코드에 직접 기록
|
|
554
|
+
→ 영구 보존
|
|
555
|
+
→ Git으로 버전 관리
|
|
556
|
+
→ 코드와 항상 일치
|
|
557
|
+
|
|
558
|
+
✅ 필요한 곳에만
|
|
559
|
+
→ Context 효율적
|
|
560
|
+
→ 검색 가능
|
|
561
|
+
→ AI가 실제 참고
|
|
304
562
|
```
|
|
305
563
|
|
|
306
564
|
---
|
|
307
565
|
|
|
308
566
|
## 🎯 체크리스트
|
|
309
567
|
|
|
310
|
-
코드 작성
|
|
568
|
+
코드 작성 후:
|
|
311
569
|
|
|
312
|
-
- [ ]
|
|
313
|
-
- [ ]
|
|
314
|
-
- [ ]
|
|
315
|
-
- [ ]
|
|
316
|
-
- [ ]
|
|
317
|
-
- [ ]
|
|
570
|
+
- [ ] 모든 추론에 `@codesyncer-inference` + 근거
|
|
571
|
+
- [ ] 결정 사항에 `@codesyncer-decision` + [날짜] + 이유
|
|
572
|
+
- [ ] Trade-off 있으면 `@codesyncer-tradeoff` 명시
|
|
573
|
+
- [ ] 재사용 패턴에 `@codesyncer-pattern` 표시
|
|
574
|
+
- [ ] 확인 필요한 부분 `@codesyncer-todo`
|
|
575
|
+
- [ ] 복잡한 로직에 `@codesyncer-why` 설명
|
|
318
576
|
|
|
319
577
|
---
|
|
320
578
|
|
|
321
|
-
**버전**:
|
|
579
|
+
**버전**: 2.0.0
|
|
322
580
|
**마지막 업데이트**: [TODAY]
|
|
323
581
|
|
|
324
|
-
|
|
582
|
+
*주석이 곧 문서입니다. 모든 컨텍스트를 코드에 기록하세요.*
|