aiwf 0.3.21 → 0.3.23
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/package.json +3 -2
- package/src/cli/index.js +39 -336
- package/src/lib/installer.js +310 -90
- package/src/utils/messages.js +24 -0
- package/src/DEPENDENCY_MAP.md +0 -90
- package/src/cli/cache-cli.js +0 -459
- package/src/cli/checkpoint-cli.js +0 -417
- package/src/cli/language-cli.js +0 -286
- package/src/cli/sprint-cli.js +0 -287
- package/src/commands/ai-tool.js +0 -383
- package/src/commands/compress.js +0 -60
- package/src/commands/create-project.js +0 -160
- package/src/commands/evaluate.js +0 -459
- package/src/commands/persona.js +0 -309
- package/src/commands/sprint-task.js +0 -262
- package/src/commands/token.js +0 -312
- package/src/lib/ai-persona-manager.js +0 -711
- package/src/lib/cache-system.js +0 -332
- package/src/lib/context-engine.js +0 -568
- package/src/lib/github-integration.js +0 -402
- package/src/lib/memory-profiler.js +0 -471
- package/src/lib/metrics-collector.js +0 -885
- package/src/lib/offline-detector.js +0 -386
- package/src/lib/resource-loader-enhanced.js +0 -399
- package/src/lib/resource-loader.js +0 -250
- package/src/lib/resources/commands/ai-persona.js +0 -602
- package/src/lib/resources/commands/compress-context.js +0 -389
- package/src/lib/resources/commands/evaluate.js +0 -246
- package/src/lib/resources/commands/persona-context-apply.js +0 -562
- package/src/lib/resources/commands/token-tracking.js +0 -443
- package/src/lib/resources/config/commit-patterns.js +0 -103
- package/src/lib/resources/config/language.json +0 -6
- package/src/lib/resources/personas/PERSONA_INDEX.md +0 -57
- package/src/lib/resources/personas/analyst.json +0 -44
- package/src/lib/resources/personas/architect/best_practices.md +0 -136
- package/src/lib/resources/personas/architect/knowledge_base.md +0 -377
- package/src/lib/resources/personas/architect.json +0 -44
- package/src/lib/resources/personas/backend/best_practices.md +0 -766
- package/src/lib/resources/personas/backend/knowledge_base.md +0 -1070
- package/src/lib/resources/personas/data_analyst/best_practices.md +0 -594
- package/src/lib/resources/personas/data_analyst/knowledge_base.md +0 -1057
- package/src/lib/resources/personas/developer.json +0 -44
- package/src/lib/resources/personas/developer.md +0 -37
- package/src/lib/resources/personas/evaluation_criteria.json +0 -137
- package/src/lib/resources/personas/frontend/best_practices.md +0 -445
- package/src/lib/resources/personas/frontend/knowledge_base.md +0 -729
- package/src/lib/resources/personas/persona-index.json +0 -31
- package/src/lib/resources/personas/reviewer.json +0 -44
- package/src/lib/resources/personas/security/best_practices.md +0 -307
- package/src/lib/resources/personas/security/knowledge_base.md +0 -498
- package/src/lib/resources/personas/tester.json +0 -44
- package/src/lib/resources/templates/README.md +0 -78
- package/src/lib/resources/templates/api-server/config.json +0 -64
- package/src/lib/resources/templates/api-server/template/.aiwf/config.json +0 -56
- package/src/lib/resources/templates/api-server/template/.aiwf/feature-ledger.json +0 -42
- package/src/lib/resources/templates/api-server/template/.aiwf/personas/backend-engineer.json +0 -40
- package/src/lib/resources/templates/api-server/template/.aiwf/scripts/cli.js +0 -111
- package/src/lib/resources/templates/api-server/template/.env.example +0 -29
- package/src/lib/resources/templates/api-server/template/.eslintrc.json +0 -23
- package/src/lib/resources/templates/api-server/template/README.md +0 -171
- package/src/lib/resources/templates/api-server/template/jest.config.js +0 -26
- package/src/lib/resources/templates/api-server/template/nodemon.json +0 -9
- package/src/lib/resources/templates/api-server/template/package.json +0 -61
- package/src/lib/resources/templates/api-server/template/src/app.ts +0 -60
- package/src/lib/resources/templates/api-server/template/src/config/swagger.ts +0 -38
- package/src/lib/resources/templates/api-server/template/src/controllers/aiwfController.ts +0 -54
- package/src/lib/resources/templates/api-server/template/src/controllers/authController.ts +0 -128
- package/src/lib/resources/templates/api-server/template/src/controllers/statusController.ts +0 -28
- package/src/lib/resources/templates/api-server/template/src/index.ts +0 -28
- package/src/lib/resources/templates/api-server/template/src/middleware/aiwfMiddleware.ts +0 -50
- package/src/lib/resources/templates/api-server/template/src/middleware/authMiddleware.ts +0 -41
- package/src/lib/resources/templates/api-server/template/src/middleware/errorHandler.ts +0 -42
- package/src/lib/resources/templates/api-server/template/src/middleware/notFoundHandler.ts +0 -14
- package/src/lib/resources/templates/api-server/template/src/middleware/requestLogger.ts +0 -20
- package/src/lib/resources/templates/api-server/template/src/routes/aiwf.ts +0 -31
- package/src/lib/resources/templates/api-server/template/src/routes/index.ts +0 -13
- package/src/lib/resources/templates/api-server/template/src/routes/v1/index.ts +0 -67
- package/src/lib/resources/templates/api-server/template/src/utils/logger.ts +0 -37
- package/src/lib/resources/templates/api-server/template/tests/app.test.ts +0 -40
- package/src/lib/resources/templates/api-server/template/tsconfig.json +0 -43
- package/src/lib/resources/templates/npm-library/config.json +0 -61
- package/src/lib/resources/templates/npm-library/template/LICENSE +0 -21
- package/src/lib/resources/templates/npm-library/template/README.md +0 -201
- package/src/lib/resources/templates/npm-library/template/package.json +0 -78
- package/src/lib/resources/templates/web-app/config.json +0 -53
- package/src/lib/resources/templates/web-app/template/.aiwf/config.json +0 -54
- package/src/lib/resources/templates/web-app/template/.aiwf/feature-ledger.json +0 -37
- package/src/lib/resources/templates/web-app/template/.aiwf/personas/fullstack-developer.json +0 -40
- package/src/lib/resources/templates/web-app/template/.aiwf/scripts/cli.js +0 -110
- package/src/lib/resources/templates/web-app/template/.eslintrc.cjs +0 -20
- package/src/lib/resources/templates/web-app/template/README.md +0 -151
- package/src/lib/resources/templates/web-app/template/index.html +0 -14
- package/src/lib/resources/templates/web-app/template/package.json +0 -44
- package/src/lib/resources/templates/web-app/template/postcss.config.js +0 -6
- package/src/lib/resources/templates/web-app/template/public/vite.svg +0 -1
- package/src/lib/resources/templates/web-app/template/src/App.tsx +0 -21
- package/src/lib/resources/templates/web-app/template/src/components/Layout.tsx +0 -57
- package/src/lib/resources/templates/web-app/template/src/components/aiwf/ContextStatus.tsx +0 -107
- package/src/lib/resources/templates/web-app/template/src/components/aiwf/TokenUsage.tsx +0 -71
- package/src/lib/resources/templates/web-app/template/src/index.css +0 -60
- package/src/lib/resources/templates/web-app/template/src/main.tsx +0 -10
- package/src/lib/resources/templates/web-app/template/src/pages/AiwfDashboard.tsx +0 -57
- package/src/lib/resources/templates/web-app/template/src/pages/HomePage.tsx +0 -89
- package/src/lib/resources/templates/web-app/template/src/pages/NotFound.tsx +0 -25
- package/src/lib/resources/templates/web-app/template/src/stores/aiwfStore.ts +0 -126
- package/src/lib/resources/templates/web-app/template/src/types/global.d.ts +0 -9
- package/src/lib/resources/templates/web-app/template/src/vite-env.d.ts +0 -1
- package/src/lib/resources/templates/web-app/template/tailwind.config.js +0 -30
- package/src/lib/resources/templates/web-app/template/tsconfig.json +0 -37
- package/src/lib/resources/templates/web-app/template/tsconfig.node.json +0 -10
- package/src/lib/resources/templates/web-app/template/vite.config.ts +0 -23
- package/src/lib/resources/utils/background-monitor.js +0 -223
- package/src/lib/resources/utils/compression-metrics.js +0 -1031
- package/src/lib/resources/utils/compression-strategies.js +0 -557
- package/src/lib/resources/utils/content-normalizer.js +0 -455
- package/src/lib/resources/utils/context-compressor.js +0 -583
- package/src/lib/resources/utils/context-rule-parser.js +0 -180
- package/src/lib/resources/utils/context-token-monitor.js +0 -463
- package/src/lib/resources/utils/context-update-manager.js +0 -502
- package/src/lib/resources/utils/git-utils.js +0 -220
- package/src/lib/resources/utils/importance-classifier.js +0 -645
- package/src/lib/resources/utils/information-filter.js +0 -710
- package/src/lib/resources/utils/persona-aware-compressor.js +0 -598
- package/src/lib/resources/utils/prompt-injector.js +0 -242
- package/src/lib/resources/utils/simplified-evaluator.js +0 -119
- package/src/lib/resources/utils/text-summarizer.js +0 -492
- package/src/lib/resources/utils/token-counter.js +0 -177
- package/src/lib/resources/utils/token-monitor.js +0 -484
- package/src/lib/resources/utils/token-reporter.js +0 -452
- package/src/lib/resources/utils/token-storage.js +0 -433
- package/src/lib/resources/utils/token-tracker.js +0 -283
- package/src/lib/state/priority-calculator.js +0 -217
- package/src/lib/state/state-index.js +0 -155
- package/src/lib/state/task-scanner.js +0 -328
- package/src/lib/task-analyzer.js +0 -599
- package/src/lib/template-cache-system.js +0 -559
- package/src/lib/template-downloader.js +0 -467
- package/src/lib/template-version-manager.js +0 -491
- package/src/lib/token-optimizer.js +0 -517
- package/src/utils/engineering-guard.js +0 -399
|
@@ -1,766 +0,0 @@
|
|
|
1
|
-
# Backend 페르소나 Best Practices
|
|
2
|
-
|
|
3
|
-
## 개요
|
|
4
|
-
Backend 페르소나는 서버 사이드 로직, 데이터베이스 설계, API 개발에 중점을 둡니다. 확장성, 성능, 데이터 무결성을 최우선으로 고려합니다.
|
|
5
|
-
|
|
6
|
-
## 핵심 원칙
|
|
7
|
-
|
|
8
|
-
### 1. API 설계 원칙
|
|
9
|
-
- RESTful 원칙 준수
|
|
10
|
-
- GraphQL 스키마 최적화
|
|
11
|
-
- API 버전 관리
|
|
12
|
-
- 일관된 응답 형식
|
|
13
|
-
|
|
14
|
-
### 2. 데이터베이스 설계
|
|
15
|
-
```sql
|
|
16
|
-
-- 정규화와 성능의 균형
|
|
17
|
-
CREATE TABLE users (
|
|
18
|
-
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
19
|
-
email VARCHAR(255) UNIQUE NOT NULL,
|
|
20
|
-
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
21
|
-
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
-- 인덱스 전략
|
|
25
|
-
CREATE INDEX idx_users_email ON users(email);
|
|
26
|
-
CREATE INDEX idx_users_created_at ON users(created_at DESC);
|
|
27
|
-
|
|
28
|
-
-- 파티셔닝 (대용량 테이블)
|
|
29
|
-
CREATE TABLE orders (
|
|
30
|
-
id BIGSERIAL,
|
|
31
|
-
user_id UUID REFERENCES users(id),
|
|
32
|
-
order_date DATE NOT NULL,
|
|
33
|
-
total_amount DECIMAL(10,2),
|
|
34
|
-
PRIMARY KEY (id, order_date)
|
|
35
|
-
) PARTITION BY RANGE (order_date);
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
### 3. 마이크로서비스 아키텍처
|
|
39
|
-
```javascript
|
|
40
|
-
// 서비스 간 통신 패턴
|
|
41
|
-
class OrderService {
|
|
42
|
-
constructor(eventBus, userService, inventoryService) {
|
|
43
|
-
this.eventBus = eventBus;
|
|
44
|
-
this.userService = userService;
|
|
45
|
-
this.inventoryService = inventoryService;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async createOrder(orderData) {
|
|
49
|
-
// Saga 패턴으로 분산 트랜잭션 처리
|
|
50
|
-
const saga = new OrderSaga();
|
|
51
|
-
|
|
52
|
-
try {
|
|
53
|
-
// 1. 사용자 검증
|
|
54
|
-
const user = await this.userService.validateUser(orderData.userId);
|
|
55
|
-
saga.addCompensation(() => this.userService.rollback(user));
|
|
56
|
-
|
|
57
|
-
// 2. 재고 확인 및 예약
|
|
58
|
-
const reservation = await this.inventoryService.reserve(orderData.items);
|
|
59
|
-
saga.addCompensation(() => this.inventoryService.release(reservation));
|
|
60
|
-
|
|
61
|
-
// 3. 주문 생성
|
|
62
|
-
const order = await this.createOrderRecord(orderData);
|
|
63
|
-
saga.addCompensation(() => this.deleteOrder(order.id));
|
|
64
|
-
|
|
65
|
-
// 4. 이벤트 발행
|
|
66
|
-
await this.eventBus.publish('order.created', {
|
|
67
|
-
orderId: order.id,
|
|
68
|
-
userId: user.id,
|
|
69
|
-
items: orderData.items
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
return order;
|
|
73
|
-
} catch (error) {
|
|
74
|
-
await saga.compensate();
|
|
75
|
-
throw error;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
## API 개발 Best Practices
|
|
82
|
-
|
|
83
|
-
### RESTful API 설계
|
|
84
|
-
```javascript
|
|
85
|
-
// Express.js 라우터
|
|
86
|
-
class UserController {
|
|
87
|
-
// GET /api/v1/users?page=1&limit=20&sort=created_at:desc
|
|
88
|
-
async getUsers(req, res) {
|
|
89
|
-
const { page = 1, limit = 20, sort, filter } = req.query;
|
|
90
|
-
|
|
91
|
-
try {
|
|
92
|
-
const users = await this.userService.findAll({
|
|
93
|
-
pagination: { page, limit },
|
|
94
|
-
sorting: this.parseSortParam(sort),
|
|
95
|
-
filters: this.parseFilterParam(filter)
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
res.json({
|
|
99
|
-
success: true,
|
|
100
|
-
data: users,
|
|
101
|
-
meta: {
|
|
102
|
-
page: parseInt(page),
|
|
103
|
-
limit: parseInt(limit),
|
|
104
|
-
total: users.total,
|
|
105
|
-
totalPages: Math.ceil(users.total / limit)
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
} catch (error) {
|
|
109
|
-
this.handleError(error, res);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// POST /api/v1/users
|
|
114
|
-
async createUser(req, res) {
|
|
115
|
-
try {
|
|
116
|
-
// 입력 검증
|
|
117
|
-
const validatedData = await this.validator.validate(req.body, UserSchema);
|
|
118
|
-
|
|
119
|
-
// 비즈니스 로직
|
|
120
|
-
const user = await this.userService.create(validatedData);
|
|
121
|
-
|
|
122
|
-
// 201 Created with Location header
|
|
123
|
-
res.status(201)
|
|
124
|
-
.location(`/api/v1/users/${user.id}`)
|
|
125
|
-
.json({
|
|
126
|
-
success: true,
|
|
127
|
-
data: user
|
|
128
|
-
});
|
|
129
|
-
} catch (error) {
|
|
130
|
-
this.handleError(error, res);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Error handling
|
|
135
|
-
handleError(error, res) {
|
|
136
|
-
if (error instanceof ValidationError) {
|
|
137
|
-
return res.status(400).json({
|
|
138
|
-
success: false,
|
|
139
|
-
error: {
|
|
140
|
-
code: 'VALIDATION_ERROR',
|
|
141
|
-
message: error.message,
|
|
142
|
-
details: error.details
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (error instanceof NotFoundError) {
|
|
148
|
-
return res.status(404).json({
|
|
149
|
-
success: false,
|
|
150
|
-
error: {
|
|
151
|
-
code: 'NOT_FOUND',
|
|
152
|
-
message: error.message
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// 500 Internal Server Error
|
|
158
|
-
console.error('Unexpected error:', error);
|
|
159
|
-
res.status(500).json({
|
|
160
|
-
success: false,
|
|
161
|
-
error: {
|
|
162
|
-
code: 'INTERNAL_ERROR',
|
|
163
|
-
message: 'An unexpected error occurred'
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
### GraphQL 스키마 설계
|
|
171
|
-
```graphql
|
|
172
|
-
# Schema Definition
|
|
173
|
-
type User {
|
|
174
|
-
id: ID!
|
|
175
|
-
email: String!
|
|
176
|
-
profile: Profile!
|
|
177
|
-
orders(
|
|
178
|
-
first: Int = 10
|
|
179
|
-
after: String
|
|
180
|
-
filter: OrderFilter
|
|
181
|
-
): OrderConnection!
|
|
182
|
-
createdAt: DateTime!
|
|
183
|
-
updatedAt: DateTime!
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
type Profile {
|
|
187
|
-
firstName: String!
|
|
188
|
-
lastName: String!
|
|
189
|
-
avatar: String
|
|
190
|
-
preferences: UserPreferences!
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
# Pagination using Relay specification
|
|
194
|
-
type OrderConnection {
|
|
195
|
-
edges: [OrderEdge!]!
|
|
196
|
-
pageInfo: PageInfo!
|
|
197
|
-
totalCount: Int!
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
type OrderEdge {
|
|
201
|
-
node: Order!
|
|
202
|
-
cursor: String!
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
# Input types
|
|
206
|
-
input CreateUserInput {
|
|
207
|
-
email: String!
|
|
208
|
-
password: String!
|
|
209
|
-
profile: ProfileInput!
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
# Queries
|
|
213
|
-
type Query {
|
|
214
|
-
user(id: ID!): User
|
|
215
|
-
users(
|
|
216
|
-
first: Int
|
|
217
|
-
after: String
|
|
218
|
-
filter: UserFilter
|
|
219
|
-
orderBy: UserOrderBy
|
|
220
|
-
): UserConnection!
|
|
221
|
-
me: User @auth
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
# Mutations
|
|
225
|
-
type Mutation {
|
|
226
|
-
createUser(input: CreateUserInput!): CreateUserPayload!
|
|
227
|
-
updateUser(id: ID!, input: UpdateUserInput!): UpdateUserPayload!
|
|
228
|
-
deleteUser(id: ID!): DeleteUserPayload!
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
# Subscriptions
|
|
232
|
-
type Subscription {
|
|
233
|
-
userUpdated(userId: ID!): User!
|
|
234
|
-
orderCreated(userId: ID!): Order!
|
|
235
|
-
}
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
## 데이터베이스 최적화
|
|
239
|
-
|
|
240
|
-
### 쿼리 최적화
|
|
241
|
-
```javascript
|
|
242
|
-
// Query Builder Pattern
|
|
243
|
-
class UserRepository {
|
|
244
|
-
constructor(db) {
|
|
245
|
-
this.db = db;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// N+1 문제 방지
|
|
249
|
-
async findWithOrders(userId) {
|
|
250
|
-
const query = `
|
|
251
|
-
SELECT
|
|
252
|
-
u.id, u.email, u.created_at,
|
|
253
|
-
o.id as order_id, o.total_amount, o.order_date,
|
|
254
|
-
oi.product_id, oi.quantity, oi.price
|
|
255
|
-
FROM users u
|
|
256
|
-
LEFT JOIN orders o ON u.id = o.user_id
|
|
257
|
-
LEFT JOIN order_items oi ON o.id = oi.order_id
|
|
258
|
-
WHERE u.id = $1
|
|
259
|
-
ORDER BY o.order_date DESC
|
|
260
|
-
`;
|
|
261
|
-
|
|
262
|
-
const result = await this.db.query(query, [userId]);
|
|
263
|
-
return this.mapResultToUser(result.rows);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Batch loading
|
|
267
|
-
async findByIds(userIds) {
|
|
268
|
-
if (userIds.length === 0) return [];
|
|
269
|
-
|
|
270
|
-
const query = `
|
|
271
|
-
SELECT * FROM users
|
|
272
|
-
WHERE id = ANY($1::uuid[])
|
|
273
|
-
`;
|
|
274
|
-
|
|
275
|
-
const result = await this.db.query(query, [userIds]);
|
|
276
|
-
|
|
277
|
-
// 순서 보장
|
|
278
|
-
const userMap = new Map(
|
|
279
|
-
result.rows.map(row => [row.id, row])
|
|
280
|
-
);
|
|
281
|
-
|
|
282
|
-
return userIds.map(id => userMap.get(id)).filter(Boolean);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// 동적 쿼리 빌더
|
|
286
|
-
async search(criteria) {
|
|
287
|
-
let query = 'SELECT * FROM users WHERE 1=1';
|
|
288
|
-
const params = [];
|
|
289
|
-
let paramIndex = 1;
|
|
290
|
-
|
|
291
|
-
if (criteria.email) {
|
|
292
|
-
query += ` AND email ILIKE $${paramIndex}`;
|
|
293
|
-
params.push(`%${criteria.email}%`);
|
|
294
|
-
paramIndex++;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
if (criteria.createdAfter) {
|
|
298
|
-
query += ` AND created_at >= $${paramIndex}`;
|
|
299
|
-
params.push(criteria.createdAfter);
|
|
300
|
-
paramIndex++;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
if (criteria.sortBy) {
|
|
304
|
-
const validColumns = ['email', 'created_at'];
|
|
305
|
-
if (validColumns.includes(criteria.sortBy)) {
|
|
306
|
-
query += ` ORDER BY ${criteria.sortBy} ${
|
|
307
|
-
criteria.sortOrder === 'desc' ? 'DESC' : 'ASC'
|
|
308
|
-
}`;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
query += ` LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`;
|
|
313
|
-
params.push(criteria.limit || 20, criteria.offset || 0);
|
|
314
|
-
|
|
315
|
-
return this.db.query(query, params);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
### 캐싱 전략
|
|
321
|
-
```javascript
|
|
322
|
-
// Redis 캐싱 레이어
|
|
323
|
-
class CacheService {
|
|
324
|
-
constructor(redis, ttl = 3600) {
|
|
325
|
-
this.redis = redis;
|
|
326
|
-
this.ttl = ttl;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Cache-Aside Pattern
|
|
330
|
-
async get(key, fetchFn) {
|
|
331
|
-
// 1. 캐시 확인
|
|
332
|
-
const cached = await this.redis.get(key);
|
|
333
|
-
if (cached) {
|
|
334
|
-
return JSON.parse(cached);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// 2. 캐시 미스 시 데이터 조회
|
|
338
|
-
const data = await fetchFn();
|
|
339
|
-
|
|
340
|
-
// 3. 캐시 저장
|
|
341
|
-
await this.redis.setex(
|
|
342
|
-
key,
|
|
343
|
-
this.ttl,
|
|
344
|
-
JSON.stringify(data)
|
|
345
|
-
);
|
|
346
|
-
|
|
347
|
-
return data;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// Write-Through Pattern
|
|
351
|
-
async set(key, data, ttl = this.ttl) {
|
|
352
|
-
await this.redis.setex(
|
|
353
|
-
key,
|
|
354
|
-
ttl,
|
|
355
|
-
JSON.stringify(data)
|
|
356
|
-
);
|
|
357
|
-
return data;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// Cache Invalidation
|
|
361
|
-
async invalidate(pattern) {
|
|
362
|
-
const keys = await this.redis.keys(pattern);
|
|
363
|
-
if (keys.length > 0) {
|
|
364
|
-
await this.redis.del(...keys);
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// Distributed Lock (분산 환경)
|
|
369
|
-
async acquireLock(lockKey, ttl = 10) {
|
|
370
|
-
const identifier = uuidv4();
|
|
371
|
-
const result = await this.redis.set(
|
|
372
|
-
`lock:${lockKey}`,
|
|
373
|
-
identifier,
|
|
374
|
-
'NX',
|
|
375
|
-
'EX',
|
|
376
|
-
ttl
|
|
377
|
-
);
|
|
378
|
-
|
|
379
|
-
return result === 'OK' ? identifier : null;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
async releaseLock(lockKey, identifier) {
|
|
383
|
-
const script = `
|
|
384
|
-
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
385
|
-
return redis.call("del", KEYS[1])
|
|
386
|
-
else
|
|
387
|
-
return 0
|
|
388
|
-
end
|
|
389
|
-
`;
|
|
390
|
-
|
|
391
|
-
return this.redis.eval(
|
|
392
|
-
script,
|
|
393
|
-
1,
|
|
394
|
-
`lock:${lockKey}`,
|
|
395
|
-
identifier
|
|
396
|
-
);
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
```
|
|
400
|
-
|
|
401
|
-
## 메시지 큐와 이벤트 처리
|
|
402
|
-
|
|
403
|
-
### RabbitMQ 패턴
|
|
404
|
-
```javascript
|
|
405
|
-
// Publisher/Subscriber Pattern
|
|
406
|
-
class EventPublisher {
|
|
407
|
-
constructor(channel) {
|
|
408
|
-
this.channel = channel;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
async publish(exchange, routingKey, message) {
|
|
412
|
-
const messageBuffer = Buffer.from(JSON.stringify({
|
|
413
|
-
...message,
|
|
414
|
-
timestamp: new Date().toISOString(),
|
|
415
|
-
messageId: uuidv4()
|
|
416
|
-
}));
|
|
417
|
-
|
|
418
|
-
await this.channel.publish(
|
|
419
|
-
exchange,
|
|
420
|
-
routingKey,
|
|
421
|
-
messageBuffer,
|
|
422
|
-
{
|
|
423
|
-
persistent: true,
|
|
424
|
-
contentType: 'application/json',
|
|
425
|
-
headers: {
|
|
426
|
-
'x-retry-count': 0
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
);
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// Consumer with Error Handling
|
|
434
|
-
class EventConsumer {
|
|
435
|
-
constructor(channel, options = {}) {
|
|
436
|
-
this.channel = channel;
|
|
437
|
-
this.maxRetries = options.maxRetries || 3;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
async consume(queue, handler) {
|
|
441
|
-
await this.channel.consume(queue, async (msg) => {
|
|
442
|
-
if (!msg) return;
|
|
443
|
-
|
|
444
|
-
try {
|
|
445
|
-
const content = JSON.parse(msg.content.toString());
|
|
446
|
-
await handler(content);
|
|
447
|
-
|
|
448
|
-
// Acknowledge message
|
|
449
|
-
this.channel.ack(msg);
|
|
450
|
-
} catch (error) {
|
|
451
|
-
console.error('Error processing message:', error);
|
|
452
|
-
|
|
453
|
-
// Retry logic
|
|
454
|
-
const retryCount = (msg.properties.headers['x-retry-count'] || 0) + 1;
|
|
455
|
-
|
|
456
|
-
if (retryCount <= this.maxRetries) {
|
|
457
|
-
// Requeue with delay
|
|
458
|
-
setTimeout(() => {
|
|
459
|
-
this.channel.sendToQueue(
|
|
460
|
-
queue,
|
|
461
|
-
msg.content,
|
|
462
|
-
{
|
|
463
|
-
...msg.properties,
|
|
464
|
-
headers: {
|
|
465
|
-
...msg.properties.headers,
|
|
466
|
-
'x-retry-count': retryCount
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
);
|
|
470
|
-
}, Math.pow(2, retryCount) * 1000); // Exponential backoff
|
|
471
|
-
} else {
|
|
472
|
-
// Send to DLQ
|
|
473
|
-
await this.sendToDeadLetter(msg, error);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
// Reject without requeue
|
|
477
|
-
this.channel.nack(msg, false, false);
|
|
478
|
-
}
|
|
479
|
-
});
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
async sendToDeadLetter(msg, error) {
|
|
483
|
-
const dlq = `${msg.fields.routingKey}.dlq`;
|
|
484
|
-
await this.channel.sendToQueue(
|
|
485
|
-
dlq,
|
|
486
|
-
msg.content,
|
|
487
|
-
{
|
|
488
|
-
headers: {
|
|
489
|
-
'x-death-reason': error.message,
|
|
490
|
-
'x-original-queue': msg.fields.routingKey
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
);
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
```
|
|
497
|
-
|
|
498
|
-
## 보안 구현
|
|
499
|
-
|
|
500
|
-
### 인증/인가 미들웨어
|
|
501
|
-
```javascript
|
|
502
|
-
// JWT Authentication
|
|
503
|
-
class AuthMiddleware {
|
|
504
|
-
constructor(jwtService, userService) {
|
|
505
|
-
this.jwtService = jwtService;
|
|
506
|
-
this.userService = userService;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
authenticate() {
|
|
510
|
-
return async (req, res, next) => {
|
|
511
|
-
try {
|
|
512
|
-
const token = this.extractToken(req);
|
|
513
|
-
if (!token) {
|
|
514
|
-
return res.status(401).json({
|
|
515
|
-
error: 'No authentication token provided'
|
|
516
|
-
});
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
const payload = await this.jwtService.verify(token);
|
|
520
|
-
|
|
521
|
-
// Token blacklist check
|
|
522
|
-
if (await this.isTokenBlacklisted(payload.jti)) {
|
|
523
|
-
return res.status(401).json({
|
|
524
|
-
error: 'Token has been revoked'
|
|
525
|
-
});
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
req.user = await this.userService.findById(payload.sub);
|
|
529
|
-
next();
|
|
530
|
-
} catch (error) {
|
|
531
|
-
if (error.name === 'TokenExpiredError') {
|
|
532
|
-
return res.status(401).json({
|
|
533
|
-
error: 'Token has expired'
|
|
534
|
-
});
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
return res.status(401).json({
|
|
538
|
-
error: 'Invalid authentication token'
|
|
539
|
-
});
|
|
540
|
-
}
|
|
541
|
-
};
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
authorize(...roles) {
|
|
545
|
-
return (req, res, next) => {
|
|
546
|
-
if (!req.user) {
|
|
547
|
-
return res.status(401).json({
|
|
548
|
-
error: 'Authentication required'
|
|
549
|
-
});
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
if (roles.length > 0 && !roles.includes(req.user.role)) {
|
|
553
|
-
return res.status(403).json({
|
|
554
|
-
error: 'Insufficient permissions'
|
|
555
|
-
});
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
next();
|
|
559
|
-
};
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
extractToken(req) {
|
|
563
|
-
const authHeader = req.headers.authorization;
|
|
564
|
-
if (authHeader && authHeader.startsWith('Bearer ')) {
|
|
565
|
-
return authHeader.substring(7);
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
return req.cookies?.accessToken;
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
```
|
|
572
|
-
|
|
573
|
-
## 테스팅 전략
|
|
574
|
-
|
|
575
|
-
### 단위 테스트
|
|
576
|
-
```javascript
|
|
577
|
-
describe('UserService', () => {
|
|
578
|
-
let userService;
|
|
579
|
-
let userRepository;
|
|
580
|
-
let emailService;
|
|
581
|
-
|
|
582
|
-
beforeEach(() => {
|
|
583
|
-
userRepository = {
|
|
584
|
-
findByEmail: jest.fn(),
|
|
585
|
-
create: jest.fn(),
|
|
586
|
-
update: jest.fn()
|
|
587
|
-
};
|
|
588
|
-
|
|
589
|
-
emailService = {
|
|
590
|
-
sendWelcomeEmail: jest.fn()
|
|
591
|
-
};
|
|
592
|
-
|
|
593
|
-
userService = new UserService(userRepository, emailService);
|
|
594
|
-
});
|
|
595
|
-
|
|
596
|
-
describe('createUser', () => {
|
|
597
|
-
it('should create user with hashed password', async () => {
|
|
598
|
-
const userData = {
|
|
599
|
-
email: 'test@example.com',
|
|
600
|
-
password: 'password123'
|
|
601
|
-
};
|
|
602
|
-
|
|
603
|
-
userRepository.findByEmail.mockResolvedValue(null);
|
|
604
|
-
userRepository.create.mockResolvedValue({
|
|
605
|
-
id: 'user-123',
|
|
606
|
-
...userData,
|
|
607
|
-
password: 'hashed-password'
|
|
608
|
-
});
|
|
609
|
-
|
|
610
|
-
const user = await userService.createUser(userData);
|
|
611
|
-
|
|
612
|
-
expect(userRepository.findByEmail).toHaveBeenCalledWith(userData.email);
|
|
613
|
-
expect(userRepository.create).toHaveBeenCalledWith({
|
|
614
|
-
email: userData.email,
|
|
615
|
-
password: expect.stringMatching(/^\$2[aby]\$/)
|
|
616
|
-
});
|
|
617
|
-
expect(emailService.sendWelcomeEmail).toHaveBeenCalledWith(userData.email);
|
|
618
|
-
expect(user).not.toHaveProperty('password');
|
|
619
|
-
});
|
|
620
|
-
|
|
621
|
-
it('should throw error if email already exists', async () => {
|
|
622
|
-
userRepository.findByEmail.mockResolvedValue({ id: 'existing-user' });
|
|
623
|
-
|
|
624
|
-
await expect(
|
|
625
|
-
userService.createUser({ email: 'test@example.com', password: 'pass' })
|
|
626
|
-
).rejects.toThrow('Email already registered');
|
|
627
|
-
});
|
|
628
|
-
});
|
|
629
|
-
});
|
|
630
|
-
```
|
|
631
|
-
|
|
632
|
-
### 통합 테스트
|
|
633
|
-
```javascript
|
|
634
|
-
describe('API Integration Tests', () => {
|
|
635
|
-
let app;
|
|
636
|
-
let db;
|
|
637
|
-
|
|
638
|
-
beforeAll(async () => {
|
|
639
|
-
// Test database setup
|
|
640
|
-
db = await createTestDatabase();
|
|
641
|
-
app = createApp(db);
|
|
642
|
-
});
|
|
643
|
-
|
|
644
|
-
afterAll(async () => {
|
|
645
|
-
await db.close();
|
|
646
|
-
});
|
|
647
|
-
|
|
648
|
-
describe('POST /api/users', () => {
|
|
649
|
-
it('should create user and return 201', async () => {
|
|
650
|
-
const response = await request(app)
|
|
651
|
-
.post('/api/users')
|
|
652
|
-
.send({
|
|
653
|
-
email: 'newuser@example.com',
|
|
654
|
-
password: 'SecurePass123!',
|
|
655
|
-
profile: {
|
|
656
|
-
firstName: 'John',
|
|
657
|
-
lastName: 'Doe'
|
|
658
|
-
}
|
|
659
|
-
});
|
|
660
|
-
|
|
661
|
-
expect(response.status).toBe(201);
|
|
662
|
-
expect(response.headers.location).toMatch(/\/api\/users\/[\w-]+/);
|
|
663
|
-
expect(response.body).toMatchObject({
|
|
664
|
-
success: true,
|
|
665
|
-
data: {
|
|
666
|
-
email: 'newuser@example.com',
|
|
667
|
-
profile: {
|
|
668
|
-
firstName: 'John',
|
|
669
|
-
lastName: 'Doe'
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
});
|
|
673
|
-
|
|
674
|
-
// Verify database
|
|
675
|
-
const user = await db.query(
|
|
676
|
-
'SELECT * FROM users WHERE email = $1',
|
|
677
|
-
['newuser@example.com']
|
|
678
|
-
);
|
|
679
|
-
expect(user.rows).toHaveLength(1);
|
|
680
|
-
});
|
|
681
|
-
});
|
|
682
|
-
});
|
|
683
|
-
```
|
|
684
|
-
|
|
685
|
-
## 모니터링과 로깅
|
|
686
|
-
|
|
687
|
-
### 구조화된 로깅
|
|
688
|
-
```javascript
|
|
689
|
-
class Logger {
|
|
690
|
-
constructor(service) {
|
|
691
|
-
this.service = service;
|
|
692
|
-
this.winston = winston.createLogger({
|
|
693
|
-
format: winston.format.combine(
|
|
694
|
-
winston.format.timestamp(),
|
|
695
|
-
winston.format.errors({ stack: true }),
|
|
696
|
-
winston.format.json()
|
|
697
|
-
),
|
|
698
|
-
defaultMeta: { service },
|
|
699
|
-
transports: [
|
|
700
|
-
new winston.transports.Console({
|
|
701
|
-
format: winston.format.simple()
|
|
702
|
-
}),
|
|
703
|
-
new winston.transports.File({
|
|
704
|
-
filename: 'error.log',
|
|
705
|
-
level: 'error'
|
|
706
|
-
}),
|
|
707
|
-
new winston.transports.File({
|
|
708
|
-
filename: 'combined.log'
|
|
709
|
-
})
|
|
710
|
-
]
|
|
711
|
-
});
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
log(level, message, meta = {}) {
|
|
715
|
-
this.winston.log({
|
|
716
|
-
level,
|
|
717
|
-
message,
|
|
718
|
-
...meta,
|
|
719
|
-
timestamp: new Date().toISOString()
|
|
720
|
-
});
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
// Request logging middleware
|
|
724
|
-
requestLogger() {
|
|
725
|
-
return (req, res, next) => {
|
|
726
|
-
const start = Date.now();
|
|
727
|
-
|
|
728
|
-
res.on('finish', () => {
|
|
729
|
-
const duration = Date.now() - start;
|
|
730
|
-
|
|
731
|
-
this.log('info', 'HTTP Request', {
|
|
732
|
-
method: req.method,
|
|
733
|
-
path: req.path,
|
|
734
|
-
statusCode: res.statusCode,
|
|
735
|
-
duration,
|
|
736
|
-
ip: req.ip,
|
|
737
|
-
userAgent: req.get('user-agent'),
|
|
738
|
-
userId: req.user?.id
|
|
739
|
-
});
|
|
740
|
-
});
|
|
741
|
-
|
|
742
|
-
next();
|
|
743
|
-
};
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
```
|
|
747
|
-
|
|
748
|
-
## 배포 전 체크리스트
|
|
749
|
-
|
|
750
|
-
### 성능
|
|
751
|
-
- [ ] 데이터베이스 인덱스 최적화
|
|
752
|
-
- [ ] N+1 쿼리 제거
|
|
753
|
-
- [ ] 캐싱 전략 구현
|
|
754
|
-
- [ ] 비동기 처리 적용
|
|
755
|
-
|
|
756
|
-
### 보안
|
|
757
|
-
- [ ] 환경 변수로 민감 정보 관리
|
|
758
|
-
- [ ] SQL Injection 방지
|
|
759
|
-
- [ ] Rate Limiting 설정
|
|
760
|
-
- [ ] CORS 정책 구성
|
|
761
|
-
|
|
762
|
-
### 안정성
|
|
763
|
-
- [ ] 에러 처리 및 복구 메커니즘
|
|
764
|
-
- [ ] 서킷 브레이커 패턴
|
|
765
|
-
- [ ] 헬스체크 엔드포인트
|
|
766
|
-
- [ ] 우아한 종료(Graceful Shutdown)
|