@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.
@@ -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 ์Šคํƒ€์ผ ์‚ฌ์šฉ