@wondev/dotenv-example 1.0.2 → 1.0.3
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/AGENTS.md +38 -0
- package/CLAUDE.md +1 -40
- package/bin/index.js +64 -55
- package/package.json +8 -2
- package/prompts/20260102_165844.md +3 -0
- package/prompts/20260102_170026.md +3 -0
- package/prompts/20260102_170120.md +4 -0
- package/prompts/20260102_170500.md +3 -0
- package/prompts/20260102_170839.md +3 -0
- package/prompts/20260102_171046.md +3 -0
- package/prompts/20260102_171147.md +38 -0
- package/prompts/20260102_171336.md +76 -0
- package/prompts/20260102_171546.md +40 -0
- package/.claude/README.md +0 -60
- package/.claude/commands/business_logic.md +0 -143
- package/.claude/commands/generate-prd.md +0 -175
- package/.claude/commands/gotobackend.md +0 -569
- package/.claude/commands/playwrightMCP_install.md +0 -113
- package/.claude/commands/setting_dev.md +0 -731
- package/.claude/commands/tech-lead.md +0 -404
- package/.claude/commands/user-flow.md +0 -839
- package/.claude/settings.local.json +0 -9
- package/.cursor/README.md +0 -10
- package/.cursor/mcp.json +0 -31
- package/.cursor/rules/common/cursor-rules.mdc +0 -53
- package/.cursor/rules/common/git-convention.mdc +0 -86
- package/.cursor/rules/common/self-improve.mdc +0 -72
- package/.cursor/rules/common/tdd.mdc +0 -81
- package/.cursor/rules/common/vibe-coding.mdc +0 -114
- package/.cursor/rules/supabase/supabase-bootstrap-auth.mdc +0 -236
- package/.cursor/rules/supabase/supabase-create-db-functions.mdc +0 -136
- package/.cursor/rules/supabase/supabase-create-migration.mdc +0 -50
- package/.cursor/rules/supabase/supabase-create-rls-policies.mdc +0 -248
- package/.cursor/rules/supabase/supabase-declarative-database-schema.mdc +0 -78
- package/.cursor/rules/supabase/supabase-postgres-sql-style-guide.mdc +0 -133
- package/.cursor/rules/supabase/supabase-writing-edge-functions.mdc +0 -105
- package/.cursor/rules/web/design-rules.mdc +0 -381
- package/.cursor/rules/web/nextjs-convention.mdc +0 -237
- package/.cursor/rules/web/playwright-test-guide.mdc +0 -176
- package/.cursor/rules/web/toss-frontend.mdc +0 -695
- package/.env +0 -4
|
@@ -1,731 +0,0 @@
|
|
|
1
|
-
### 즉시 시작 가능한 개발 환경 설정
|
|
2
|
-
```bash
|
|
3
|
-
# Next.js 15 프로젝트 생성
|
|
4
|
-
pnpm create next-app@latest . --typescript --tailwind --app
|
|
5
|
-
|
|
6
|
-
# shadcn/ui 초기화
|
|
7
|
-
pnpx shadcn@latest init
|
|
8
|
-
|
|
9
|
-
# 필수 패키지 설치
|
|
10
|
-
pnpm add @tanstack/react-query jotai lucide-react zod
|
|
11
|
-
pnpm add -D vitest @vitejs/plugin-react jsdom @testing-library/react vite-tsconfig-paths
|
|
12
|
-
|
|
13
|
-
# 폴더 구조 생성
|
|
14
|
-
mkdir -p src/{actions,lib/{db/local/{repositories,models,utils},auth,},hooks,states,types}
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
# Next.js 웹 서비스 기술 스택 & 폴더 구조 가이드 (로컬 스토리지 기반)
|
|
18
|
-
|
|
19
|
-
## 🚀 기술 스택
|
|
20
|
-
|
|
21
|
-
### 프론트엔드
|
|
22
|
-
- **Next.js 15** - React 19 기반 풀스택 프레임워크
|
|
23
|
-
- **Tailwind CSS v3** - 유틸리티 퍼스트 CSS 프레임워크
|
|
24
|
-
- **shadcn/ui** - Tailwind 기반 고품질 컴포넌트 라이브러리
|
|
25
|
-
- **Lucide React** - 가벼운 아이콘 라이브러리
|
|
26
|
-
|
|
27
|
-
### 상태 관리 & 데이터
|
|
28
|
-
- **Jotai** - 원자 단위 전역 상태 관리
|
|
29
|
-
- **TanStack Query (React Query)** - 서버 상태 관리 및 데이터 페칭
|
|
30
|
-
- **Local Storage** - 로컬 데이터 저장소
|
|
31
|
-
|
|
32
|
-
### 개발 환경
|
|
33
|
-
- **TypeScript** - 타입 안전성
|
|
34
|
-
- **Vitest** - 빠른 테스트 러너
|
|
35
|
-
- **pnpm** - 효율적인 패키지 매니저
|
|
36
|
-
- **Vercel** - 최적화된 배포 플랫폼
|
|
37
|
-
|
|
38
|
-
## 📁 프로젝트 폴더 구조
|
|
39
|
-
|
|
40
|
-
```
|
|
41
|
-
/
|
|
42
|
-
├── .env.local # 환경 변수
|
|
43
|
-
├── .env.example # 환경 변수 예시
|
|
44
|
-
├── .gitignore
|
|
45
|
-
├── package.json
|
|
46
|
-
├── pnpm-lock.yaml
|
|
47
|
-
├── next.config.js
|
|
48
|
-
├── tailwind.config.ts
|
|
49
|
-
├── tsconfig.json
|
|
50
|
-
├── vitest.config.mts
|
|
51
|
-
├── README.md
|
|
52
|
-
│
|
|
53
|
-
├── public/ # 정적 파일
|
|
54
|
-
│ ├── favicon.ico
|
|
55
|
-
│ ├── images/
|
|
56
|
-
│ └── icons/
|
|
57
|
-
│
|
|
58
|
-
├── src/
|
|
59
|
-
│ ├── app/ # Next.js App Router (라우팅 전용)
|
|
60
|
-
│ │ ├── globals.css
|
|
61
|
-
│ │ ├── layout.tsx
|
|
62
|
-
│ │ ├── page.tsx
|
|
63
|
-
│ │ ├── loading.tsx
|
|
64
|
-
│ │ ├── error.tsx
|
|
65
|
-
│ │ ├── not-found.tsx
|
|
66
|
-
│ │ │
|
|
67
|
-
│ │ ├── (auth)/ # 라우트 그룹
|
|
68
|
-
│ │ │ ├── login/
|
|
69
|
-
│ │ │ │ └── page.tsx
|
|
70
|
-
│ │ │ └── register/
|
|
71
|
-
│ │ │ └── page.tsx
|
|
72
|
-
│ │ │
|
|
73
|
-
│ │ ├── dashboard/
|
|
74
|
-
│ │ │ ├── layout.tsx
|
|
75
|
-
│ │ │ ├── page.tsx
|
|
76
|
-
│ │ │ └── settings/
|
|
77
|
-
│ │ │ └── page.tsx
|
|
78
|
-
│ │ │
|
|
79
|
-
│ │ └── api/ # API 라우트 (최소한으로 사용)
|
|
80
|
-
│ │ └── webhook/
|
|
81
|
-
│ │ └── route.ts
|
|
82
|
-
│ │
|
|
83
|
-
│ ├── actions/ # Server Actions (API 대신 우선 사용)
|
|
84
|
-
│ │ ├── auth-actions.ts
|
|
85
|
-
│ │ ├── user-actions.ts
|
|
86
|
-
│ │ └── post-actions.ts
|
|
87
|
-
│ │
|
|
88
|
-
│ ├── components/ # 재사용 가능한 컴포넌트
|
|
89
|
-
│ │ ├── ui/ # shadcn/ui 컴포넌트
|
|
90
|
-
│ │ │ ├── button.tsx
|
|
91
|
-
│ │ │ ├── input.tsx
|
|
92
|
-
│ │ │ ├── card.tsx
|
|
93
|
-
│ │ │ └── dialog.tsx
|
|
94
|
-
│ │ │
|
|
95
|
-
│ │ ├── forms/ # 폼 관련 컴포넌트
|
|
96
|
-
│ │ │ ├── login-form.tsx
|
|
97
|
-
│ │ │ ├── register-form.tsx
|
|
98
|
-
│ │ │ └── contact-form.tsx
|
|
99
|
-
│ │ │
|
|
100
|
-
│ │ ├── layout/ # 레이아웃 컴포넌트
|
|
101
|
-
│ │ │ ├── header.tsx
|
|
102
|
-
│ │ │ ├── footer.tsx
|
|
103
|
-
│ │ │ ├── sidebar.tsx
|
|
104
|
-
│ │ │ └── navigation.tsx
|
|
105
|
-
│ │ │
|
|
106
|
-
│ │ ├── features/ # 기능별 컴포넌트
|
|
107
|
-
│ │ │ ├── auth/
|
|
108
|
-
│ │ │ │ ├── auth-provider.tsx
|
|
109
|
-
│ │ │ │ └── protected-route.tsx
|
|
110
|
-
│ │ │ ├── dashboard/
|
|
111
|
-
│ │ │ │ ├── stats-card.tsx
|
|
112
|
-
│ │ │ │ └── recent-activity.tsx
|
|
113
|
-
│ │ │ └── posts/
|
|
114
|
-
│ │ │ ├── post-list.tsx
|
|
115
|
-
│ │ │ ├── post-item.tsx
|
|
116
|
-
│ │ │ └── post-create.tsx
|
|
117
|
-
│ │ │
|
|
118
|
-
│ │ └── common/ # 공통 컴포넌트
|
|
119
|
-
│ │ ├── loading-spinner.tsx
|
|
120
|
-
│ │ ├── error-boundary.tsx
|
|
121
|
-
│ │ ├── confirmation-dialog.tsx
|
|
122
|
-
│ │ └── theme-provider.tsx
|
|
123
|
-
│ │
|
|
124
|
-
│ ├── hooks/ # 커스텀 훅
|
|
125
|
-
│ │ ├── use-auth.ts
|
|
126
|
-
│ │ ├── use-local-storage.ts
|
|
127
|
-
│ │ ├── use-local-db.ts
|
|
128
|
-
│ │ ├── use-debounce.ts
|
|
129
|
-
│ │ └── use-media-query.ts
|
|
130
|
-
│ │
|
|
131
|
-
│ ├── lib/ # 라이브러리 설정 및 유틸리티
|
|
132
|
-
│ │ ├── utils.ts # 공통 유틸 함수 (cn 함수 등)
|
|
133
|
-
│ │ ├── validations.ts # Zod 스키마
|
|
134
|
-
│ │ ├── constants.ts # 앱 전역 상수
|
|
135
|
-
│ │ │
|
|
136
|
-
│ │ ├── auth/ # 인증 관련
|
|
137
|
-
│ │ │ ├── config.ts
|
|
138
|
-
│ │ │ └── providers.ts
|
|
139
|
-
│ │ │
|
|
140
|
-
│ │ └── db/ # 로컬 스토리지 DB 구현
|
|
141
|
-
│ │ ├── storage.ts # 로컬 스토리지 래퍼
|
|
142
|
-
│ │ ├── database-service.ts # DB 서비스 레이어
|
|
143
|
-
│ │ ├── mock-data.ts # 초기 목 데이터
|
|
144
|
-
│ │ ├── repositories/
|
|
145
|
-
│ │ │ ├── base-repository.ts
|
|
146
|
-
│ │ │ ├── user-repository.ts
|
|
147
|
-
│ │ │ ├── post-repository.ts
|
|
148
|
-
│ │ │ ├── comment-repository.ts
|
|
149
|
-
│ │ │ └── tag-repository.ts
|
|
150
|
-
│ │ ├── models/
|
|
151
|
-
│ │ │ ├── user.ts
|
|
152
|
-
│ │ │ ├── post.ts
|
|
153
|
-
│ │ │ ├── comment.ts
|
|
154
|
-
│ │ │ └── tag.ts
|
|
155
|
-
│ │ └── utils/
|
|
156
|
-
│ │ ├── query-builder.ts
|
|
157
|
-
│ │ ├── relationships.ts
|
|
158
|
-
│ │ └── validation.ts
|
|
159
|
-
│ │
|
|
160
|
-
│ ├── states/ # 전역 상태 (Jotai atoms)
|
|
161
|
-
│ │ ├── auth-store.ts
|
|
162
|
-
│ │ ├── ui-store.ts
|
|
163
|
-
│ │ └── user-store.ts
|
|
164
|
-
│ │
|
|
165
|
-
│ ├── styles/ # 스타일 관련
|
|
166
|
-
│ │ ├── globals.css # Tailwind 설정
|
|
167
|
-
│ │ └── components.css # 커스텀 컴포넌트 스타일
|
|
168
|
-
│ │
|
|
169
|
-
│ └── types/ # TypeScript 타입 정의
|
|
170
|
-
│ ├── auth.ts
|
|
171
|
-
│ ├── database.ts
|
|
172
|
-
│ ├── api.ts
|
|
173
|
-
│ └── global.d.ts
|
|
174
|
-
│
|
|
175
|
-
├── tests/ # 테스트 파일
|
|
176
|
-
│ ├── __mocks__/
|
|
177
|
-
│ ├── setup.ts
|
|
178
|
-
│ ├── components/
|
|
179
|
-
│ │ └── button.test.tsx
|
|
180
|
-
│ ├── hooks/
|
|
181
|
-
│ │ └── use-auth.test.ts
|
|
182
|
-
│ └── pages/
|
|
183
|
-
│ └── home.test.tsx
|
|
184
|
-
│
|
|
185
|
-
├── docs/ # 프로젝트 문서
|
|
186
|
-
│ ├── api.md
|
|
187
|
-
│ ├── deployment.md
|
|
188
|
-
│ └── contributing.md
|
|
189
|
-
│
|
|
190
|
-
└── scripts/ # 빌드/배포 스크립트
|
|
191
|
-
├── build.sh
|
|
192
|
-
└── deploy.sh
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
## 🛠️ 핵심 설정 파일
|
|
196
|
-
|
|
197
|
-
### package.json
|
|
198
|
-
```json
|
|
199
|
-
{
|
|
200
|
-
"name": "my-nextjs-app",
|
|
201
|
-
"version": "0.1.0",
|
|
202
|
-
"private": true,
|
|
203
|
-
"scripts": {
|
|
204
|
-
"dev": "next dev",
|
|
205
|
-
"build": "next build",
|
|
206
|
-
"start": "next start",
|
|
207
|
-
"lint": "next lint",
|
|
208
|
-
"test": "vitest",
|
|
209
|
-
"test:watch": "vitest --watch"
|
|
210
|
-
},
|
|
211
|
-
"dependencies": {
|
|
212
|
-
"next": "^15.0.0",
|
|
213
|
-
"react": "^19.0.0",
|
|
214
|
-
"react-dom": "^19.0.0",
|
|
215
|
-
"@tanstack/react-query": "^5.0.0",
|
|
216
|
-
"jotai": "^2.0.0",
|
|
217
|
-
"lucide-react": "^0.400.0",
|
|
218
|
-
"tailwindcss": "^3.4.0",
|
|
219
|
-
"zod": "^3.22.0"
|
|
220
|
-
},
|
|
221
|
-
"devDependencies": {
|
|
222
|
-
"@types/node": "^20.0.0",
|
|
223
|
-
"@types/react": "^19.0.0",
|
|
224
|
-
"@types/react-dom": "^19.0.0",
|
|
225
|
-
"typescript": "^5.0.0",
|
|
226
|
-
"vitest": "^2.0.0",
|
|
227
|
-
"@vitejs/plugin-react": "^4.0.0",
|
|
228
|
-
"jsdom": "^25.0.0",
|
|
229
|
-
"@testing-library/react": "^16.0.0"
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
### vitest.config.mts
|
|
235
|
-
```typescript
|
|
236
|
-
import { defineConfig } from 'vitest/config'
|
|
237
|
-
import react from '@vitejs/plugin-react'
|
|
238
|
-
import tsconfigPaths from 'vite-tsconfig-paths'
|
|
239
|
-
|
|
240
|
-
export default defineConfig({
|
|
241
|
-
plugins: [tsconfigPaths(), react()],
|
|
242
|
-
test: {
|
|
243
|
-
environment: 'jsdom',
|
|
244
|
-
setupFiles: ['./tests/setup.ts'],
|
|
245
|
-
},
|
|
246
|
-
})
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
## 🗄️ 로컬 스토리지 DB 구조 및 구현
|
|
250
|
-
|
|
251
|
-
### 로컬 스토리지 DB 아키텍처
|
|
252
|
-
|
|
253
|
-
```typescript
|
|
254
|
-
// src/lib/db/storage.ts - 로컬 스토리지 래퍼
|
|
255
|
-
export class LocalStorage {
|
|
256
|
-
private static instance: LocalStorage
|
|
257
|
-
private prefix = 'myapp_'
|
|
258
|
-
|
|
259
|
-
static getInstance(): LocalStorage {
|
|
260
|
-
if (!LocalStorage.instance) {
|
|
261
|
-
LocalStorage.instance = new LocalStorage()
|
|
262
|
-
}
|
|
263
|
-
return LocalStorage.instance
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
set<T>(key: string, value: T): void {
|
|
267
|
-
try {
|
|
268
|
-
localStorage.setItem(
|
|
269
|
-
`${this.prefix}${key}`,
|
|
270
|
-
JSON.stringify(value)
|
|
271
|
-
)
|
|
272
|
-
} catch (error) {
|
|
273
|
-
console.error('LocalStorage set error:', error)
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
get<T>(key: string): T | null {
|
|
278
|
-
try {
|
|
279
|
-
const item = localStorage.getItem(`${this.prefix}${key}`)
|
|
280
|
-
return item ? JSON.parse(item) : null
|
|
281
|
-
} catch (error) {
|
|
282
|
-
console.error('LocalStorage get error:', error)
|
|
283
|
-
return null
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
remove(key: string): void {
|
|
288
|
-
localStorage.removeItem(`${this.prefix}${key}`)
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
clear(): void {
|
|
292
|
-
Object.keys(localStorage)
|
|
293
|
-
.filter(key => key.startsWith(this.prefix))
|
|
294
|
-
.forEach(key => localStorage.removeItem(key))
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
```
|
|
298
|
-
|
|
299
|
-
### 베이스 리포지토리 패턴
|
|
300
|
-
|
|
301
|
-
```typescript
|
|
302
|
-
// src/lib/db/repositories/base-repository.ts
|
|
303
|
-
export abstract class BaseRepository<T extends { id: string }> {
|
|
304
|
-
protected storage = LocalStorage.getInstance()
|
|
305
|
-
protected abstract tableName: string
|
|
306
|
-
|
|
307
|
-
protected generateId(): string {
|
|
308
|
-
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
async findAll(): Promise<T[]> {
|
|
312
|
-
const data = this.storage.get<T[]>(this.tableName)
|
|
313
|
-
return data || []
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
async findById(id: string): Promise<T | null> {
|
|
317
|
-
const items = await this.findAll()
|
|
318
|
-
return items.find(item => item.id === id) || null
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
async create(data: Omit<T, 'id'>): Promise<T> {
|
|
322
|
-
const items = await this.findAll()
|
|
323
|
-
const newItem = { ...data, id: this.generateId() } as T
|
|
324
|
-
items.push(newItem)
|
|
325
|
-
this.storage.set(this.tableName, items)
|
|
326
|
-
return newItem
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
async update(id: string, data: Partial<T>): Promise<T | null> {
|
|
330
|
-
const items = await this.findAll()
|
|
331
|
-
const index = items.findIndex(item => item.id === id)
|
|
332
|
-
|
|
333
|
-
if (index === -1) return null
|
|
334
|
-
|
|
335
|
-
items[index] = { ...items[index], ...data }
|
|
336
|
-
this.storage.set(this.tableName, items)
|
|
337
|
-
return items[index]
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
async delete(id: string): Promise<boolean> {
|
|
341
|
-
const items = await this.findAll()
|
|
342
|
-
const filteredItems = items.filter(item => item.id !== id)
|
|
343
|
-
|
|
344
|
-
if (filteredItems.length === items.length) return false
|
|
345
|
-
|
|
346
|
-
this.storage.set(this.tableName, filteredItems)
|
|
347
|
-
return true
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
async deleteAll(): Promise<void> {
|
|
351
|
-
this.storage.remove(this.tableName)
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
### 모델 정의
|
|
357
|
-
|
|
358
|
-
```typescript
|
|
359
|
-
// src/lib/db/models/user.ts
|
|
360
|
-
export interface User {
|
|
361
|
-
id: string
|
|
362
|
-
email: string
|
|
363
|
-
name: string
|
|
364
|
-
avatar?: string
|
|
365
|
-
createdAt: Date
|
|
366
|
-
updatedAt: Date
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// src/lib/db/models/post.ts
|
|
370
|
-
export interface Post {
|
|
371
|
-
id: string
|
|
372
|
-
title: string
|
|
373
|
-
content: string
|
|
374
|
-
authorId: string
|
|
375
|
-
tags: string[]
|
|
376
|
-
status: 'draft' | 'published' | 'archived'
|
|
377
|
-
createdAt: Date
|
|
378
|
-
updatedAt: Date
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// src/lib/db/models/comment.ts
|
|
382
|
-
export interface Comment {
|
|
383
|
-
id: string
|
|
384
|
-
content: string
|
|
385
|
-
postId: string
|
|
386
|
-
authorId: string
|
|
387
|
-
parentId?: string // 대댓글용
|
|
388
|
-
createdAt: Date
|
|
389
|
-
updatedAt: Date
|
|
390
|
-
}
|
|
391
|
-
```
|
|
392
|
-
|
|
393
|
-
### 구체적인 리포지토리 구현
|
|
394
|
-
|
|
395
|
-
```typescript
|
|
396
|
-
// src/lib/db/repositories/user-repository.ts
|
|
397
|
-
import { BaseRepository } from './base-repository'
|
|
398
|
-
import { User } from '../models/user'
|
|
399
|
-
|
|
400
|
-
export class UserRepository extends BaseRepository<User> {
|
|
401
|
-
protected tableName = 'users'
|
|
402
|
-
|
|
403
|
-
async findByEmail(email: string): Promise<User | null> {
|
|
404
|
-
const users = await this.findAll()
|
|
405
|
-
return users.find(user => user.email === email) || null
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
async findByName(name: string): Promise<User[]> {
|
|
409
|
-
const users = await this.findAll()
|
|
410
|
-
return users.filter(user =>
|
|
411
|
-
user.name.toLowerCase().includes(name.toLowerCase())
|
|
412
|
-
)
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// src/lib/db/repositories/post-repository.ts
|
|
417
|
-
import { BaseRepository } from './base-repository'
|
|
418
|
-
import { Post } from '../models/post'
|
|
419
|
-
|
|
420
|
-
export class PostRepository extends BaseRepository<Post> {
|
|
421
|
-
protected tableName = 'posts'
|
|
422
|
-
|
|
423
|
-
async findByAuthor(authorId: string): Promise<Post[]> {
|
|
424
|
-
const posts = await this.findAll()
|
|
425
|
-
return posts.filter(post => post.authorId === authorId)
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
async findByStatus(status: Post['status']): Promise<Post[]> {
|
|
429
|
-
const posts = await this.findAll()
|
|
430
|
-
return posts.filter(post => post.status === status)
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
async findByTag(tag: string): Promise<Post[]> {
|
|
434
|
-
const posts = await this.findAll()
|
|
435
|
-
return posts.filter(post => post.tags.includes(tag))
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
async search(query: string): Promise<Post[]> {
|
|
439
|
-
const posts = await this.findAll()
|
|
440
|
-
return posts.filter(post =>
|
|
441
|
-
post.title.toLowerCase().includes(query.toLowerCase()) ||
|
|
442
|
-
post.content.toLowerCase().includes(query.toLowerCase())
|
|
443
|
-
)
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
```
|
|
447
|
-
|
|
448
|
-
### 초기 목 데이터
|
|
449
|
-
|
|
450
|
-
```typescript
|
|
451
|
-
// src/lib/db/mock-data.ts
|
|
452
|
-
import { User } from './models/user'
|
|
453
|
-
import { Post } from './models/post'
|
|
454
|
-
import { Comment } from './models/comment'
|
|
455
|
-
|
|
456
|
-
export const mockUsers: User[] = [
|
|
457
|
-
{
|
|
458
|
-
id: '1',
|
|
459
|
-
email: 'john@example.com',
|
|
460
|
-
name: 'John Doe',
|
|
461
|
-
avatar: 'https://avatar.vercel.sh/john',
|
|
462
|
-
createdAt: new Date('2024-01-01'),
|
|
463
|
-
updatedAt: new Date('2024-01-01')
|
|
464
|
-
},
|
|
465
|
-
{
|
|
466
|
-
id: '2',
|
|
467
|
-
email: 'jane@example.com',
|
|
468
|
-
name: 'Jane Smith',
|
|
469
|
-
avatar: 'https://avatar.vercel.sh/jane',
|
|
470
|
-
createdAt: new Date('2024-01-02'),
|
|
471
|
-
updatedAt: new Date('2024-01-02')
|
|
472
|
-
}
|
|
473
|
-
]
|
|
474
|
-
|
|
475
|
-
export const mockPosts: Post[] = [
|
|
476
|
-
{
|
|
477
|
-
id: '1',
|
|
478
|
-
title: 'Getting Started with Next.js',
|
|
479
|
-
content: 'Next.js is a powerful React framework...',
|
|
480
|
-
authorId: '1',
|
|
481
|
-
tags: ['nextjs', 'react', 'tutorial'],
|
|
482
|
-
status: 'published',
|
|
483
|
-
createdAt: new Date('2024-01-10'),
|
|
484
|
-
updatedAt: new Date('2024-01-10')
|
|
485
|
-
},
|
|
486
|
-
{
|
|
487
|
-
id: '2',
|
|
488
|
-
title: 'Advanced TypeScript Tips',
|
|
489
|
-
content: 'Here are some advanced TypeScript techniques...',
|
|
490
|
-
authorId: '2',
|
|
491
|
-
tags: ['typescript', 'javascript', 'tips'],
|
|
492
|
-
status: 'published',
|
|
493
|
-
createdAt: new Date('2024-01-15'),
|
|
494
|
-
updatedAt: new Date('2024-01-15')
|
|
495
|
-
}
|
|
496
|
-
]
|
|
497
|
-
|
|
498
|
-
export const mockComments: Comment[] = [
|
|
499
|
-
{
|
|
500
|
-
id: '1',
|
|
501
|
-
content: 'Great article! Very helpful.',
|
|
502
|
-
postId: '1',
|
|
503
|
-
authorId: '2',
|
|
504
|
-
createdAt: new Date('2024-01-11'),
|
|
505
|
-
updatedAt: new Date('2024-01-11')
|
|
506
|
-
}
|
|
507
|
-
]
|
|
508
|
-
|
|
509
|
-
// 초기 데이터 로드 함수
|
|
510
|
-
export function initializeMockData(): void {
|
|
511
|
-
const storage = LocalStorage.getInstance()
|
|
512
|
-
|
|
513
|
-
// 이미 데이터가 있는지 확인
|
|
514
|
-
const existingUsers = storage.get<User[]>('users')
|
|
515
|
-
if (!existingUsers || existingUsers.length === 0) {
|
|
516
|
-
storage.set('users', mockUsers)
|
|
517
|
-
storage.set('posts', mockPosts)
|
|
518
|
-
storage.set('comments', mockComments)
|
|
519
|
-
console.log('Mock data initialized')
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
```
|
|
523
|
-
|
|
524
|
-
### 데이터베이스 서비스 레이어
|
|
525
|
-
|
|
526
|
-
```typescript
|
|
527
|
-
// src/lib/db/database-service.ts
|
|
528
|
-
import { UserRepository } from './repositories/user-repository'
|
|
529
|
-
import { PostRepository } from './repositories/post-repository'
|
|
530
|
-
import { CommentRepository } from './repositories/comment-repository'
|
|
531
|
-
import { initializeMockData } from './mock-data'
|
|
532
|
-
|
|
533
|
-
export class DatabaseService {
|
|
534
|
-
private static instance: DatabaseService
|
|
535
|
-
|
|
536
|
-
public users: UserRepository
|
|
537
|
-
public posts: PostRepository
|
|
538
|
-
public comments: CommentRepository
|
|
539
|
-
|
|
540
|
-
private constructor() {
|
|
541
|
-
this.users = new UserRepository()
|
|
542
|
-
this.posts = new PostRepository()
|
|
543
|
-
this.comments = new CommentRepository()
|
|
544
|
-
|
|
545
|
-
// 초기 데이터 로드
|
|
546
|
-
initializeMockData()
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
static getInstance(): DatabaseService {
|
|
550
|
-
if (!DatabaseService.instance) {
|
|
551
|
-
DatabaseService.instance = new DatabaseService()
|
|
552
|
-
}
|
|
553
|
-
return DatabaseService.instance
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
// 모든 테이블 초기화
|
|
557
|
-
async clearAllData(): Promise<void> {
|
|
558
|
-
await this.users.deleteAll()
|
|
559
|
-
await this.posts.deleteAll()
|
|
560
|
-
await this.comments.deleteAll()
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
// 백업 생성
|
|
564
|
-
async backup(): Promise<string> {
|
|
565
|
-
const data = {
|
|
566
|
-
users: await this.users.findAll(),
|
|
567
|
-
posts: await this.posts.findAll(),
|
|
568
|
-
comments: await this.comments.findAll(),
|
|
569
|
-
timestamp: new Date().toISOString()
|
|
570
|
-
}
|
|
571
|
-
return JSON.stringify(data, null, 2)
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
// 백업 복원
|
|
575
|
-
async restore(backupData: string): Promise<void> {
|
|
576
|
-
try {
|
|
577
|
-
const data = JSON.parse(backupData)
|
|
578
|
-
|
|
579
|
-
await this.clearAllData()
|
|
580
|
-
|
|
581
|
-
// 데이터 복원
|
|
582
|
-
const storage = LocalStorage.getInstance()
|
|
583
|
-
storage.set('users', data.users)
|
|
584
|
-
storage.set('posts', data.posts)
|
|
585
|
-
storage.set('comments', data.comments)
|
|
586
|
-
|
|
587
|
-
console.log('Data restored successfully')
|
|
588
|
-
} catch (error) {
|
|
589
|
-
console.error('Failed to restore data:', error)
|
|
590
|
-
throw error
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
// 편의를 위한 싱글톤 인스턴스 export
|
|
596
|
-
export const db = DatabaseService.getInstance()
|
|
597
|
-
```
|
|
598
|
-
|
|
599
|
-
### 사용 예시
|
|
600
|
-
|
|
601
|
-
```typescript
|
|
602
|
-
// src/hooks/use-local-db.ts
|
|
603
|
-
import { useState, useEffect } from 'react'
|
|
604
|
-
import { db } from '@/lib/db/database-service'
|
|
605
|
-
import { User, Post } from '@/lib/db/models'
|
|
606
|
-
|
|
607
|
-
export function useUsers() {
|
|
608
|
-
const [users, setUsers] = useState<User[]>([])
|
|
609
|
-
const [loading, setLoading] = useState(true)
|
|
610
|
-
|
|
611
|
-
useEffect(() => {
|
|
612
|
-
const loadUsers = async () => {
|
|
613
|
-
try {
|
|
614
|
-
const data = await db.users.findAll()
|
|
615
|
-
setUsers(data)
|
|
616
|
-
} catch (error) {
|
|
617
|
-
console.error('Failed to load users:', error)
|
|
618
|
-
} finally {
|
|
619
|
-
setLoading(false)
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
loadUsers()
|
|
624
|
-
}, [])
|
|
625
|
-
|
|
626
|
-
const createUser = async (userData: Omit<User, 'id'>) => {
|
|
627
|
-
try {
|
|
628
|
-
const newUser = await db.users.create(userData)
|
|
629
|
-
setUsers(prev => [...prev, newUser])
|
|
630
|
-
return newUser
|
|
631
|
-
} catch (error) {
|
|
632
|
-
console.error('Failed to create user:', error)
|
|
633
|
-
throw error
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
return { users, loading, createUser }
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
// 컴포넌트에서 사용
|
|
641
|
-
function UserList() {
|
|
642
|
-
const { users, loading, createUser } = useUsers()
|
|
643
|
-
|
|
644
|
-
if (loading) return <div>Loading...</div>
|
|
645
|
-
|
|
646
|
-
return (
|
|
647
|
-
<div>
|
|
648
|
-
{users.map(user => (
|
|
649
|
-
<div key={user.id}>{user.name}</div>
|
|
650
|
-
))}
|
|
651
|
-
</div>
|
|
652
|
-
)
|
|
653
|
-
}
|
|
654
|
-
```
|
|
655
|
-
|
|
656
|
-
## 🎯 개발 워크플로우
|
|
657
|
-
|
|
658
|
-
### 1. 기능 구현 프로세스
|
|
659
|
-
1. **요구사항 분석** → 구체적인 구현 계획 수립
|
|
660
|
-
2. **계획 검토** → 사용자 승인 후 진행
|
|
661
|
-
3. **단계적 구현** → 작은 단위로 세분화하여 진행
|
|
662
|
-
4. **로깅 및 디버깅** → 각 단계마다 충분한 로그 추가
|
|
663
|
-
5. **테스트 및 검증** → 각 단계별 동작 확인
|
|
664
|
-
|
|
665
|
-
### 2. 명명 규칙
|
|
666
|
-
- **파일명**: `kebab-case` (예: `user-profile.tsx`)
|
|
667
|
-
- **컴포넌트**: `PascalCase` (예: `UserProfile`)
|
|
668
|
-
- **함수/변수**: `camelCase` (예: `getUserData`)
|
|
669
|
-
- **상수**: `UPPER_SNAKE_CASE` (예: `API_BASE_URL`)
|
|
670
|
-
|
|
671
|
-
### 3. 컴포넌트 구조 패턴
|
|
672
|
-
```typescript
|
|
673
|
-
// 1. Imports (외부 라이브러리 → 내부 모듈)
|
|
674
|
-
import { useState } from 'react'
|
|
675
|
-
import { Button } from '@/components/ui/button'
|
|
676
|
-
|
|
677
|
-
// 2. Types/Interfaces
|
|
678
|
-
interface UserProfileProps {
|
|
679
|
-
userId: string
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
// 3. Main Component
|
|
683
|
-
export function UserProfile({ userId }: UserProfileProps) {
|
|
684
|
-
// 4. Hooks and State
|
|
685
|
-
const [loading, setLoading] = useState(false)
|
|
686
|
-
|
|
687
|
-
// 5. Event Handlers
|
|
688
|
-
const handleSave = () => {
|
|
689
|
-
// 구현
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
// 6. Early Returns
|
|
693
|
-
if (loading) return <div>Loading...</div>
|
|
694
|
-
|
|
695
|
-
// 7. Main Render
|
|
696
|
-
return (
|
|
697
|
-
<div>
|
|
698
|
-
{/* 컴포넌트 내용 */}
|
|
699
|
-
</div>
|
|
700
|
-
)
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
// 8. Sub-components (필요한 경우)
|
|
704
|
-
function ProfileHeader() {
|
|
705
|
-
return <div>Header</div>
|
|
706
|
-
}
|
|
707
|
-
```
|
|
708
|
-
|
|
709
|
-
## 🚀 React 19 & Next.js 15 최적화
|
|
710
|
-
|
|
711
|
-
### Server Components 우선 사용
|
|
712
|
-
- **'use client' 최소화** - 클라이언트 컴포넌트 사용을 필요한 경우에만 제한
|
|
713
|
-
- **Server Actions 활용** - API 라우트 대신 Server Actions 우선 사용
|
|
714
|
-
|
|
715
|
-
### Async Runtime APIs 사용
|
|
716
|
-
```typescript
|
|
717
|
-
// 올바른 사용법
|
|
718
|
-
const cookieStore = await cookies()
|
|
719
|
-
const headersList = await headers()
|
|
720
|
-
const params = await props.params
|
|
721
|
-
```
|
|
722
|
-
|
|
723
|
-
### 성능 최적화
|
|
724
|
-
- **Code Splitting** - 동적 import 활용
|
|
725
|
-
- **Image Optimization** - Next.js Image 컴포넌트 사용
|
|
726
|
-
- **Bundle Analysis** - `@next/bundle-analyzer` 활용
|
|
727
|
-
|
|
728
|
-
### 보안
|
|
729
|
-
- **환경 변수 관리** - `.env.local` 사용
|
|
730
|
-
- **CSRF 보호** - Server Actions 활용
|
|
731
|
-
- **타입 안전성** - Zod를 통한 런타임 검증
|