@su-record/vibe 0.4.3 โ 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agent/rules/languages/go.md +396 -0
- package/.agent/rules/languages/java-spring.md +586 -0
- package/.agent/rules/languages/kotlin-android.md +491 -0
- package/.agent/rules/languages/python-django.md +371 -0
- package/.agent/rules/languages/rust.md +425 -0
- package/.agent/rules/languages/swift-ios.md +516 -0
- package/.agent/rules/languages/typescript-node.md +375 -0
- package/.agent/rules/languages/typescript-vue.md +353 -0
- package/.claude/commands/vibe.analyze.md +166 -54
- package/.claude/settings.local.json +4 -1
- package/bin/vibe +140 -24
- package/package.json +1 -1
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
# ๐ข TypeScript + Node.js Backend ํ์ง ๊ท์น
|
|
2
|
+
|
|
3
|
+
## ํต์ฌ ์์น (core์์ ์์)
|
|
4
|
+
|
|
5
|
+
```markdown
|
|
6
|
+
โ
๋จ์ผ ์ฑ
์ (SRP)
|
|
7
|
+
โ
์ค๋ณต ์ ๊ฑฐ (DRY)
|
|
8
|
+
โ
์ฌ์ฌ์ฉ์ฑ
|
|
9
|
+
โ
๋ฎ์ ๋ณต์ก๋
|
|
10
|
+
โ
ํจ์ โค 30์ค
|
|
11
|
+
โ
์ค์ฒฉ โค 3๋จ๊ณ
|
|
12
|
+
โ
Cyclomatic complexity โค 10
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Express.js ๊ท์น
|
|
16
|
+
|
|
17
|
+
### 1. ๋ผ์ฐํฐ ๊ตฌ์กฐํ
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
// โ
routes/user.routes.ts
|
|
21
|
+
import { Router } from 'express';
|
|
22
|
+
import { UserController } from '@/controllers/user.controller';
|
|
23
|
+
import { authMiddleware } from '@/middleware/auth';
|
|
24
|
+
import { validate } from '@/middleware/validate';
|
|
25
|
+
import { createUserSchema, updateUserSchema } from '@/schemas/user.schema';
|
|
26
|
+
|
|
27
|
+
const router = Router();
|
|
28
|
+
const controller = new UserController();
|
|
29
|
+
|
|
30
|
+
router.get('/', controller.findAll);
|
|
31
|
+
router.get('/:id', controller.findOne);
|
|
32
|
+
router.post('/', validate(createUserSchema), controller.create);
|
|
33
|
+
router.put('/:id', authMiddleware, validate(updateUserSchema), controller.update);
|
|
34
|
+
router.delete('/:id', authMiddleware, controller.delete);
|
|
35
|
+
|
|
36
|
+
export default router;
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 2. Controller ํจํด
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
// โ
controllers/user.controller.ts
|
|
43
|
+
import { Request, Response, NextFunction } from 'express';
|
|
44
|
+
import { UserService } from '@/services/user.service';
|
|
45
|
+
import { CreateUserDto, UpdateUserDto } from '@/dto/user.dto';
|
|
46
|
+
|
|
47
|
+
export class UserController {
|
|
48
|
+
private userService = new UserService();
|
|
49
|
+
|
|
50
|
+
findAll = async (req: Request, res: Response, next: NextFunction) => {
|
|
51
|
+
try {
|
|
52
|
+
const { page = 1, limit = 10 } = req.query;
|
|
53
|
+
const users = await this.userService.findAll({
|
|
54
|
+
page: Number(page),
|
|
55
|
+
limit: Number(limit),
|
|
56
|
+
});
|
|
57
|
+
res.json(users);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
next(error);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
findOne = async (req: Request, res: Response, next: NextFunction) => {
|
|
64
|
+
try {
|
|
65
|
+
const user = await this.userService.findById(req.params.id);
|
|
66
|
+
if (!user) {
|
|
67
|
+
return res.status(404).json({ message: '์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค' });
|
|
68
|
+
}
|
|
69
|
+
res.json(user);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
next(error);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
create = async (req: Request, res: Response, next: NextFunction) => {
|
|
76
|
+
try {
|
|
77
|
+
const dto: CreateUserDto = req.body;
|
|
78
|
+
const user = await this.userService.create(dto);
|
|
79
|
+
res.status(201).json(user);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
next(error);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 3. Service ๋ ์ด์ด
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
// โ
services/user.service.ts
|
|
91
|
+
import { prisma } from '@/lib/prisma';
|
|
92
|
+
import { CreateUserDto, UpdateUserDto } from '@/dto/user.dto';
|
|
93
|
+
import { hashPassword } from '@/utils/crypto';
|
|
94
|
+
import { AppError } from '@/utils/errors';
|
|
95
|
+
|
|
96
|
+
export class UserService {
|
|
97
|
+
async findAll(options: { page: number; limit: number }) {
|
|
98
|
+
const { page, limit } = options;
|
|
99
|
+
const skip = (page - 1) * limit;
|
|
100
|
+
|
|
101
|
+
const [users, total] = await Promise.all([
|
|
102
|
+
prisma.user.findMany({ skip, take: limit }),
|
|
103
|
+
prisma.user.count(),
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
data: users,
|
|
108
|
+
meta: {
|
|
109
|
+
page,
|
|
110
|
+
limit,
|
|
111
|
+
total,
|
|
112
|
+
totalPages: Math.ceil(total / limit),
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async findById(id: string) {
|
|
118
|
+
return prisma.user.findUnique({ where: { id } });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async create(dto: CreateUserDto) {
|
|
122
|
+
const existing = await prisma.user.findUnique({
|
|
123
|
+
where: { email: dto.email },
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (existing) {
|
|
127
|
+
throw new AppError('์ด๋ฏธ ์กด์ฌํ๋ ์ด๋ฉ์ผ์
๋๋ค', 409);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const hashedPassword = await hashPassword(dto.password);
|
|
131
|
+
|
|
132
|
+
return prisma.user.create({
|
|
133
|
+
data: {
|
|
134
|
+
...dto,
|
|
135
|
+
password: hashedPassword,
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## NestJS ๊ท์น
|
|
143
|
+
|
|
144
|
+
### 1. Module ๊ตฌ์กฐ
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
// โ
user/user.module.ts
|
|
148
|
+
import { Module } from '@nestjs/common';
|
|
149
|
+
import { UserController } from './user.controller';
|
|
150
|
+
import { UserService } from './user.service';
|
|
151
|
+
import { UserRepository } from './user.repository';
|
|
152
|
+
|
|
153
|
+
@Module({
|
|
154
|
+
controllers: [UserController],
|
|
155
|
+
providers: [UserService, UserRepository],
|
|
156
|
+
exports: [UserService],
|
|
157
|
+
})
|
|
158
|
+
export class UserModule {}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### 2. Controller (NestJS)
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
// โ
user/user.controller.ts
|
|
165
|
+
import {
|
|
166
|
+
Controller,
|
|
167
|
+
Get,
|
|
168
|
+
Post,
|
|
169
|
+
Put,
|
|
170
|
+
Delete,
|
|
171
|
+
Param,
|
|
172
|
+
Body,
|
|
173
|
+
Query,
|
|
174
|
+
UseGuards,
|
|
175
|
+
ParseIntPipe,
|
|
176
|
+
} from '@nestjs/common';
|
|
177
|
+
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
|
178
|
+
import { UserService } from './user.service';
|
|
179
|
+
import { CreateUserDto, UpdateUserDto, UserQueryDto } from './dto';
|
|
180
|
+
import { JwtAuthGuard } from '@/auth/guards/jwt-auth.guard';
|
|
181
|
+
import { CurrentUser } from '@/auth/decorators/current-user.decorator';
|
|
182
|
+
|
|
183
|
+
@ApiTags('users')
|
|
184
|
+
@Controller('users')
|
|
185
|
+
export class UserController {
|
|
186
|
+
constructor(private readonly userService: UserService) {}
|
|
187
|
+
|
|
188
|
+
@Get()
|
|
189
|
+
@ApiOperation({ summary: '์ฌ์ฉ์ ๋ชฉ๋ก ์กฐํ' })
|
|
190
|
+
async findAll(@Query() query: UserQueryDto) {
|
|
191
|
+
return this.userService.findAll(query);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
@Get(':id')
|
|
195
|
+
@ApiOperation({ summary: '์ฌ์ฉ์ ์์ธ ์กฐํ' })
|
|
196
|
+
async findOne(@Param('id', ParseIntPipe) id: number) {
|
|
197
|
+
return this.userService.findById(id);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
@Post()
|
|
201
|
+
@ApiOperation({ summary: '์ฌ์ฉ์ ์์ฑ' })
|
|
202
|
+
async create(@Body() dto: CreateUserDto) {
|
|
203
|
+
return this.userService.create(dto);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
@Put(':id')
|
|
207
|
+
@UseGuards(JwtAuthGuard)
|
|
208
|
+
@ApiBearerAuth()
|
|
209
|
+
@ApiOperation({ summary: '์ฌ์ฉ์ ์์ ' })
|
|
210
|
+
async update(
|
|
211
|
+
@Param('id', ParseIntPipe) id: number,
|
|
212
|
+
@Body() dto: UpdateUserDto,
|
|
213
|
+
@CurrentUser() user: User,
|
|
214
|
+
) {
|
|
215
|
+
return this.userService.update(id, dto, user);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### 3. Service (NestJS)
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
// โ
user/user.service.ts
|
|
224
|
+
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
|
|
225
|
+
import { UserRepository } from './user.repository';
|
|
226
|
+
import { CreateUserDto, UpdateUserDto } from './dto';
|
|
227
|
+
import * as bcrypt from 'bcrypt';
|
|
228
|
+
|
|
229
|
+
@Injectable()
|
|
230
|
+
export class UserService {
|
|
231
|
+
constructor(private readonly userRepository: UserRepository) {}
|
|
232
|
+
|
|
233
|
+
async findAll(query: UserQueryDto) {
|
|
234
|
+
return this.userRepository.findAll(query);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async findById(id: number) {
|
|
238
|
+
const user = await this.userRepository.findById(id);
|
|
239
|
+
if (!user) {
|
|
240
|
+
throw new NotFoundException('์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค');
|
|
241
|
+
}
|
|
242
|
+
return user;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async create(dto: CreateUserDto) {
|
|
246
|
+
const existing = await this.userRepository.findByEmail(dto.email);
|
|
247
|
+
if (existing) {
|
|
248
|
+
throw new ConflictException('์ด๋ฏธ ์กด์ฌํ๋ ์ด๋ฉ์ผ์
๋๋ค');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const hashedPassword = await bcrypt.hash(dto.password, 10);
|
|
252
|
+
return this.userRepository.create({
|
|
253
|
+
...dto,
|
|
254
|
+
password: hashedPassword,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### 4. DTO์ Validation
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
// โ
user/dto/create-user.dto.ts
|
|
264
|
+
import { IsEmail, IsString, MinLength, IsOptional } from 'class-validator';
|
|
265
|
+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
266
|
+
|
|
267
|
+
export class CreateUserDto {
|
|
268
|
+
@ApiProperty({ example: 'user@example.com' })
|
|
269
|
+
@IsEmail({}, { message: '์ฌ๋ฐ๋ฅธ ์ด๋ฉ์ผ ํ์์ด ์๋๋๋ค' })
|
|
270
|
+
email: string;
|
|
271
|
+
|
|
272
|
+
@ApiProperty({ example: 'password123' })
|
|
273
|
+
@IsString()
|
|
274
|
+
@MinLength(8, { message: '๋น๋ฐ๋ฒํธ๋ 8์ ์ด์์ด์ด์ผ ํฉ๋๋ค' })
|
|
275
|
+
password: string;
|
|
276
|
+
|
|
277
|
+
@ApiProperty({ example: 'ํ๊ธธ๋' })
|
|
278
|
+
@IsString()
|
|
279
|
+
name: string;
|
|
280
|
+
|
|
281
|
+
@ApiPropertyOptional({ example: '010-1234-5678' })
|
|
282
|
+
@IsOptional()
|
|
283
|
+
@IsString()
|
|
284
|
+
phone?: string;
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## ๊ณตํต ๊ท์น
|
|
289
|
+
|
|
290
|
+
### ์๋ฌ ์ฒ๋ฆฌ
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
// โ
utils/errors.ts
|
|
294
|
+
export class AppError extends Error {
|
|
295
|
+
constructor(
|
|
296
|
+
message: string,
|
|
297
|
+
public statusCode: number = 500,
|
|
298
|
+
public code?: string,
|
|
299
|
+
) {
|
|
300
|
+
super(message);
|
|
301
|
+
this.name = 'AppError';
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// โ
middleware/error.middleware.ts
|
|
306
|
+
import { Request, Response, NextFunction } from 'express';
|
|
307
|
+
import { AppError } from '@/utils/errors';
|
|
308
|
+
|
|
309
|
+
export function errorHandler(
|
|
310
|
+
err: Error,
|
|
311
|
+
req: Request,
|
|
312
|
+
res: Response,
|
|
313
|
+
next: NextFunction,
|
|
314
|
+
) {
|
|
315
|
+
console.error(err);
|
|
316
|
+
|
|
317
|
+
if (err instanceof AppError) {
|
|
318
|
+
return res.status(err.statusCode).json({
|
|
319
|
+
success: false,
|
|
320
|
+
message: err.message,
|
|
321
|
+
code: err.code,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
res.status(500).json({
|
|
326
|
+
success: false,
|
|
327
|
+
message: '์๋ฒ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค',
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Validation (Zod)
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
// โ
schemas/user.schema.ts
|
|
336
|
+
import { z } from 'zod';
|
|
337
|
+
|
|
338
|
+
export const createUserSchema = z.object({
|
|
339
|
+
body: z.object({
|
|
340
|
+
email: z.string().email('์ฌ๋ฐ๋ฅธ ์ด๋ฉ์ผ ํ์์ด ์๋๋๋ค'),
|
|
341
|
+
password: z.string().min(8, '๋น๋ฐ๋ฒํธ๋ 8์ ์ด์์ด์ด์ผ ํฉ๋๋ค'),
|
|
342
|
+
name: z.string().min(1, '์ด๋ฆ์ ์
๋ ฅํด์ฃผ์ธ์'),
|
|
343
|
+
phone: z.string().optional(),
|
|
344
|
+
}),
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
export type CreateUserInput = z.infer<typeof createUserSchema>['body'];
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
## ํ์ผ ๊ตฌ์กฐ
|
|
351
|
+
|
|
352
|
+
```
|
|
353
|
+
src/
|
|
354
|
+
โโโ controllers/ # ๋ผ์ฐํธ ํธ๋ค๋ฌ
|
|
355
|
+
โโโ services/ # ๋น์ฆ๋์ค ๋ก์ง
|
|
356
|
+
โโโ repositories/ # ๋ฐ์ดํฐ ์ก์ธ์ค
|
|
357
|
+
โโโ dto/ # Data Transfer Objects
|
|
358
|
+
โโโ schemas/ # Validation ์คํค๋ง (Zod)
|
|
359
|
+
โโโ middleware/ # Express ๋ฏธ๋ค์จ์ด
|
|
360
|
+
โโโ routes/ # ๋ผ์ฐํฐ ์ ์
|
|
361
|
+
โโโ utils/ # ์ ํธ๋ฆฌํฐ
|
|
362
|
+
โโโ types/ # TypeScript ํ์
|
|
363
|
+
โโโ config/ # ์ค์
|
|
364
|
+
โโโ lib/ # ์ธ๋ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ํผ
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
## ์ฒดํฌ๋ฆฌ์คํธ
|
|
368
|
+
|
|
369
|
+
- [ ] Controller โ Service โ Repository ๋ ์ด์ด ๋ถ๋ฆฌ
|
|
370
|
+
- [ ] DTO๋ก ์
์ถ๋ ฅ ํ์
์ ์
|
|
371
|
+
- [ ] Zod/class-validator๋ก ์
๋ ฅ ๊ฒ์ฆ
|
|
372
|
+
- [ ] ์ปค์คํ
์๋ฌ ํด๋์ค ์ฌ์ฉ
|
|
373
|
+
- [ ] ์๋ฌ ๋ฏธ๋ค์จ์ด๋ก ์ค์ ์ฒ๋ฆฌ
|
|
374
|
+
- [ ] `any` ํ์
์ฌ์ฉ ๊ธ์ง
|
|
375
|
+
- [ ] async/await + try/catch ๋๋ ์๋ฌ ๋ฏธ๋ค์จ์ด
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
# ๐ข TypeScript + Vue/Nuxt ํ์ง ๊ท์น
|
|
2
|
+
|
|
3
|
+
## ํต์ฌ ์์น (core์์ ์์)
|
|
4
|
+
|
|
5
|
+
```markdown
|
|
6
|
+
โ
๋จ์ผ ์ฑ
์ (SRP)
|
|
7
|
+
โ
์ค๋ณต ์ ๊ฑฐ (DRY)
|
|
8
|
+
โ
์ฌ์ฌ์ฉ์ฑ
|
|
9
|
+
โ
๋ฎ์ ๋ณต์ก๋
|
|
10
|
+
โ
ํจ์ โค 30์ค, Template โค 100์ค
|
|
11
|
+
โ
์ค์ฒฉ โค 3๋จ๊ณ
|
|
12
|
+
โ
Cyclomatic complexity โค 10
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Vue 3 + TypeScript ํนํ ๊ท์น
|
|
16
|
+
|
|
17
|
+
### 1. Composition API ์ฌ์ฉ (Options API ์ง์)
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
// โ Options API (๋ ๊ฑฐ์)
|
|
21
|
+
export default {
|
|
22
|
+
data() {
|
|
23
|
+
return { count: 0 };
|
|
24
|
+
},
|
|
25
|
+
methods: {
|
|
26
|
+
increment() {
|
|
27
|
+
this.count++;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// โ
Composition API + script setup
|
|
33
|
+
<script setup lang="ts">
|
|
34
|
+
import { ref, computed, onMounted } from 'vue';
|
|
35
|
+
|
|
36
|
+
const count = ref(0);
|
|
37
|
+
const doubled = computed(() => count.value * 2);
|
|
38
|
+
|
|
39
|
+
function increment() {
|
|
40
|
+
count.value++;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
onMounted(() => {
|
|
44
|
+
console.log('์ปดํฌ๋ํธ ๋ง์ดํธ๋จ');
|
|
45
|
+
});
|
|
46
|
+
</script>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 2. ํ์
์์ ํ Props/Emits
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
// โ
Props ํ์
์ ์
|
|
53
|
+
interface Props {
|
|
54
|
+
userId: string;
|
|
55
|
+
title?: string;
|
|
56
|
+
items: Item[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
60
|
+
title: '๊ธฐ๋ณธ ์ ๋ชฉ',
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// โ
Emits ํ์
์ ์
|
|
64
|
+
interface Emits {
|
|
65
|
+
(e: 'update', value: string): void;
|
|
66
|
+
(e: 'delete', id: number): void;
|
|
67
|
+
(e: 'select', item: Item): void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const emit = defineEmits<Emits>();
|
|
71
|
+
|
|
72
|
+
// ์ฌ์ฉ
|
|
73
|
+
emit('update', '์ ๊ฐ');
|
|
74
|
+
emit('delete', 123);
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 3. Composables๋ก ๋ก์ง ๋ถ๋ฆฌ
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
// โ
composables/useUser.ts
|
|
81
|
+
import { ref, computed } from 'vue';
|
|
82
|
+
import type { User } from '@/types';
|
|
83
|
+
|
|
84
|
+
export function useUser(userId: string) {
|
|
85
|
+
const user = ref<User | null>(null);
|
|
86
|
+
const isLoading = ref(false);
|
|
87
|
+
const error = ref<string | null>(null);
|
|
88
|
+
|
|
89
|
+
const fullName = computed(() =>
|
|
90
|
+
user.value ? `${user.value.firstName} ${user.value.lastName}` : ''
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
async function fetchUser() {
|
|
94
|
+
isLoading.value = true;
|
|
95
|
+
error.value = null;
|
|
96
|
+
try {
|
|
97
|
+
const response = await api.getUser(userId);
|
|
98
|
+
user.value = response.data;
|
|
99
|
+
} catch (e) {
|
|
100
|
+
error.value = '์ฌ์ฉ์๋ฅผ ๋ถ๋ฌ์ค์ง ๋ชปํ์ต๋๋ค';
|
|
101
|
+
} finally {
|
|
102
|
+
isLoading.value = false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
user,
|
|
108
|
+
isLoading,
|
|
109
|
+
error,
|
|
110
|
+
fullName,
|
|
111
|
+
fetchUser,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ์ปดํฌ๋ํธ์์ ์ฌ์ฉ
|
|
116
|
+
<script setup lang="ts">
|
|
117
|
+
const { user, isLoading, fetchUser } = useUser(props.userId);
|
|
118
|
+
|
|
119
|
+
onMounted(fetchUser);
|
|
120
|
+
</script>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 4. Pinia ์ํ ๊ด๋ฆฌ
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
// โ
stores/user.ts
|
|
127
|
+
import { defineStore } from 'pinia';
|
|
128
|
+
import type { User } from '@/types';
|
|
129
|
+
|
|
130
|
+
interface UserState {
|
|
131
|
+
currentUser: User | null;
|
|
132
|
+
users: User[];
|
|
133
|
+
isLoading: boolean;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export const useUserStore = defineStore('user', {
|
|
137
|
+
state: (): UserState => ({
|
|
138
|
+
currentUser: null,
|
|
139
|
+
users: [],
|
|
140
|
+
isLoading: false,
|
|
141
|
+
}),
|
|
142
|
+
|
|
143
|
+
getters: {
|
|
144
|
+
isLoggedIn: (state) => !!state.currentUser,
|
|
145
|
+
userCount: (state) => state.users.length,
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
actions: {
|
|
149
|
+
async login(email: string, password: string) {
|
|
150
|
+
this.isLoading = true;
|
|
151
|
+
try {
|
|
152
|
+
const user = await authApi.login(email, password);
|
|
153
|
+
this.currentUser = user;
|
|
154
|
+
} finally {
|
|
155
|
+
this.isLoading = false;
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
logout() {
|
|
160
|
+
this.currentUser = null;
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Setup Store ์คํ์ผ (๊ถ์ฅ)
|
|
166
|
+
export const useUserStore = defineStore('user', () => {
|
|
167
|
+
const currentUser = ref<User | null>(null);
|
|
168
|
+
const isLoggedIn = computed(() => !!currentUser.value);
|
|
169
|
+
|
|
170
|
+
async function login(email: string, password: string) {
|
|
171
|
+
currentUser.value = await authApi.login(email, password);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return { currentUser, isLoggedIn, login };
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### 5. Nuxt 3 ํนํ ๊ท์น
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
// โ
Server API Routes (server/api/)
|
|
182
|
+
// server/api/users/[id].get.ts
|
|
183
|
+
export default defineEventHandler(async (event) => {
|
|
184
|
+
const id = getRouterParam(event, 'id');
|
|
185
|
+
|
|
186
|
+
if (!id) {
|
|
187
|
+
throw createError({
|
|
188
|
+
statusCode: 400,
|
|
189
|
+
message: 'ID๊ฐ ํ์ํฉ๋๋ค',
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const user = await prisma.user.findUnique({ where: { id } });
|
|
194
|
+
|
|
195
|
+
if (!user) {
|
|
196
|
+
throw createError({
|
|
197
|
+
statusCode: 404,
|
|
198
|
+
message: '์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค',
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return user;
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// โ
useFetch / useAsyncData
|
|
206
|
+
<script setup lang="ts">
|
|
207
|
+
// SSR ์ง์ ๋ฐ์ดํฐ ํ์นญ
|
|
208
|
+
const { data: user, pending, error } = await useFetch<User>(
|
|
209
|
+
`/api/users/${props.userId}`
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
// ์บ์ฑ ํค ์ง์
|
|
213
|
+
const { data: posts } = await useAsyncData(
|
|
214
|
+
`user-${props.userId}-posts`,
|
|
215
|
+
() => $fetch(`/api/users/${props.userId}/posts`)
|
|
216
|
+
);
|
|
217
|
+
</script>
|
|
218
|
+
|
|
219
|
+
// โ
Middleware
|
|
220
|
+
// middleware/auth.ts
|
|
221
|
+
export default defineNuxtRouteMiddleware((to, from) => {
|
|
222
|
+
const { isLoggedIn } = useUserStore();
|
|
223
|
+
|
|
224
|
+
if (!isLoggedIn && to.path !== '/login') {
|
|
225
|
+
return navigateTo('/login');
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### 6. ์ปดํฌ๋ํธ ๊ตฌ์กฐ
|
|
231
|
+
|
|
232
|
+
```vue
|
|
233
|
+
<!-- โ
๊ถ์ฅ ์ปดํฌ๋ํธ ๊ตฌ์กฐ -->
|
|
234
|
+
<script setup lang="ts">
|
|
235
|
+
// 1. ํ์
import
|
|
236
|
+
import type { User, Item } from '@/types';
|
|
237
|
+
|
|
238
|
+
// 2. ์ปดํฌ๋ํธ import
|
|
239
|
+
import UserAvatar from '@/components/UserAvatar.vue';
|
|
240
|
+
|
|
241
|
+
// 3. Props/Emits
|
|
242
|
+
interface Props {
|
|
243
|
+
user: User;
|
|
244
|
+
editable?: boolean;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
248
|
+
editable: false,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const emit = defineEmits<{
|
|
252
|
+
(e: 'update', user: User): void;
|
|
253
|
+
}>();
|
|
254
|
+
|
|
255
|
+
// 4. Composables
|
|
256
|
+
const { isLoading, save } = useUserForm();
|
|
257
|
+
|
|
258
|
+
// 5. Reactive state
|
|
259
|
+
const formData = ref({ ...props.user });
|
|
260
|
+
const isEditing = ref(false);
|
|
261
|
+
|
|
262
|
+
// 6. Computed
|
|
263
|
+
const canSave = computed(() =>
|
|
264
|
+
formData.value.name.length > 0 && !isLoading.value
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
// 7. Methods
|
|
268
|
+
async function handleSave() {
|
|
269
|
+
await save(formData.value);
|
|
270
|
+
emit('update', formData.value);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 8. Lifecycle
|
|
274
|
+
onMounted(() => {
|
|
275
|
+
console.log('์ปดํฌ๋ํธ ์ค๋น๋จ');
|
|
276
|
+
});
|
|
277
|
+
</script>
|
|
278
|
+
|
|
279
|
+
<template>
|
|
280
|
+
<div class="user-card">
|
|
281
|
+
<UserAvatar :src="user.avatar" />
|
|
282
|
+
<h2>{{ user.name }}</h2>
|
|
283
|
+
<button
|
|
284
|
+
v-if="editable"
|
|
285
|
+
:disabled="!canSave"
|
|
286
|
+
@click="handleSave"
|
|
287
|
+
>
|
|
288
|
+
์ ์ฅ
|
|
289
|
+
</button>
|
|
290
|
+
</div>
|
|
291
|
+
</template>
|
|
292
|
+
|
|
293
|
+
<style scoped>
|
|
294
|
+
.user-card {
|
|
295
|
+
padding: 1rem;
|
|
296
|
+
border-radius: 8px;
|
|
297
|
+
}
|
|
298
|
+
</style>
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
## ์ํฐํจํด
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
// โ v-if์ v-for ํจ๊ป ์ฌ์ฉ
|
|
305
|
+
<li v-for="user in users" v-if="user.isActive">
|
|
306
|
+
|
|
307
|
+
// โ
computed๋ก ํํฐ๋ง
|
|
308
|
+
const activeUsers = computed(() => users.value.filter(u => u.isActive));
|
|
309
|
+
<li v-for="user in activeUsers">
|
|
310
|
+
|
|
311
|
+
// โ Props ์ง์ ์์
|
|
312
|
+
props.user.name = '์ ์ด๋ฆ';
|
|
313
|
+
|
|
314
|
+
// โ
emit์ผ๋ก ๋ถ๋ชจ์๊ฒ ์๋ฆผ
|
|
315
|
+
emit('update', { ...props.user, name: '์ ์ด๋ฆ' });
|
|
316
|
+
|
|
317
|
+
// โ $refs ๋จ์ฉ
|
|
318
|
+
this.$refs.input.focus();
|
|
319
|
+
|
|
320
|
+
// โ
template ref + expose
|
|
321
|
+
const inputRef = ref<HTMLInputElement>();
|
|
322
|
+
defineExpose({ focus: () => inputRef.value?.focus() });
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## ํ์ผ ๊ตฌ์กฐ (Nuxt 3)
|
|
326
|
+
|
|
327
|
+
```
|
|
328
|
+
project/
|
|
329
|
+
โโโ components/
|
|
330
|
+
โ โโโ ui/ # ๊ธฐ๋ณธ UI ์ปดํฌ๋ํธ
|
|
331
|
+
โ โโโ features/ # ๊ธฐ๋ฅ๋ณ ์ปดํฌ๋ํธ
|
|
332
|
+
โ โโโ layouts/ # ๋ ์ด์์ ์ปดํฌ๋ํธ
|
|
333
|
+
โโโ composables/ # Composition ํจ์
|
|
334
|
+
โโโ stores/ # Pinia ์คํ ์ด
|
|
335
|
+
โโโ server/
|
|
336
|
+
โ โโโ api/ # API ๋ผ์ฐํธ
|
|
337
|
+
โ โโโ middleware/ # ์๋ฒ ๋ฏธ๋ค์จ์ด
|
|
338
|
+
โ โโโ utils/ # ์๋ฒ ์ ํธ๋ฆฌํฐ
|
|
339
|
+
โโโ pages/ # ํ์ผ ๊ธฐ๋ฐ ๋ผ์ฐํ
|
|
340
|
+
โโโ middleware/ # ํด๋ผ์ด์ธํธ ๋ฏธ๋ค์จ์ด
|
|
341
|
+
โโโ types/ # TypeScript ํ์
|
|
342
|
+
โโโ utils/ # ์ ํธ๋ฆฌํฐ ํจ์
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
## ์ฒดํฌ๋ฆฌ์คํธ
|
|
346
|
+
|
|
347
|
+
- [ ] Composition API + `<script setup>` ์ฌ์ฉ
|
|
348
|
+
- [ ] Props/Emits ํ์
์ ์
|
|
349
|
+
- [ ] Composables๋ก ๋ก์ง ๋ถ๋ฆฌ
|
|
350
|
+
- [ ] Pinia Setup Store ์คํ์ผ ์ฌ์ฉ
|
|
351
|
+
- [ ] `any` ํ์
์ฌ์ฉ ๊ธ์ง
|
|
352
|
+
- [ ] v-if/v-for ๋ถ๋ฆฌ
|
|
353
|
+
- [ ] scoped ์คํ์ผ ์ฌ์ฉ
|