jun-claude-code 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/CLAUDE.md +169 -0
- package/.claude/agents/architect.md +163 -0
- package/.claude/agents/code-reviewer.md +116 -0
- package/.claude/agents/code-writer.md +141 -0
- package/.claude/agents/context-collector.md +86 -0
- package/.claude/agents/context-manager.md +183 -0
- package/.claude/agents/designer.md +189 -0
- package/.claude/agents/explore.md +127 -0
- package/.claude/agents/git-manager.md +244 -0
- package/.claude/agents/impact-analyzer.md +153 -0
- package/.claude/agents/qa-tester.md +199 -0
- package/.claude/agents/task-planner.md +160 -0
- package/.claude/hooks/skill-forced.sh +176 -0
- package/.claude/hooks/workflow-enforced.sh +165 -0
- package/.claude/settings.json +18 -0
- package/.claude/skills/Backend/SKILL.md +147 -0
- package/.claude/skills/Coding/SKILL.md +184 -0
- package/.claude/skills/Documentation/SKILL.md +290 -0
- package/.claude/skills/Git/SKILL.md +45 -0
- package/.claude/skills/Git/git.md +323 -0
- package/.claude/skills/Git/pr-apply.md +87 -0
- package/.claude/skills/Git/pr-review.md +69 -0
- package/.claude/skills/React/SKILL.md +159 -0
- package/.claude/skills/React/react-hook-form.md +222 -0
- package/.claude/skills/React/tailwind-styled.md +165 -0
- package/.claude/skills/React/tanstack-router.md +184 -0
- package/README.md +94 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +30 -0
- package/dist/copy.d.ts +8 -0
- package/dist/copy.js +173 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -0
- package/package.json +35 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: react-hook-form
|
|
3
|
+
description: 폼 구현 시 사용. Zod 스키마 정의, useForm 훅 설정, 필드 에러 처리, TRPC mutation 연동 패턴 제공.
|
|
4
|
+
keywords: [react-hook-form, zod, form, 폼, validation, 검증, useForm, register, handleSubmit]
|
|
5
|
+
estimated_tokens: ~500
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# React Hook Form + Zod 패턴
|
|
9
|
+
|
|
10
|
+
## 핵심 역할
|
|
11
|
+
|
|
12
|
+
- Zod 스키마 기반 폼 검증
|
|
13
|
+
- useForm 훅을 통한 폼 상태 관리
|
|
14
|
+
- TRPC mutation과의 연동
|
|
15
|
+
- 타입 안전한 폼 처리
|
|
16
|
+
|
|
17
|
+
## 파일 구조
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
src/features/auth/
|
|
21
|
+
├── components/
|
|
22
|
+
│ └── LoginForm.tsx # 폼 컴포넌트
|
|
23
|
+
├── schemas/
|
|
24
|
+
│ └── loginFormSchema.ts # Zod 스키마 (별도 파일)
|
|
25
|
+
└── hooks/
|
|
26
|
+
└── useLoginMutation.ts # TRPC mutation 훅
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 필수 준수 사항
|
|
30
|
+
|
|
31
|
+
| 규칙 | 올바른 예 | 잘못된 예 |
|
|
32
|
+
|------|----------|----------|
|
|
33
|
+
| 스키마 파일 분리 | `*FormSchema.ts` 별도 파일 | 컴포넌트 내 스키마 정의 |
|
|
34
|
+
| 타입 추론 | `z.infer<typeof schema>` | 수동 타입 정의 |
|
|
35
|
+
| 에러 표시 | `formState.errors` 사용 | 커스텀 에러 상태 |
|
|
36
|
+
| 로딩 상태 | `mutation.isPending` 사용 | 별도 loading 상태 |
|
|
37
|
+
|
|
38
|
+
## 스키마 정의 패턴
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
// schemas/loginFormSchema.ts
|
|
42
|
+
import { z } from 'zod'
|
|
43
|
+
|
|
44
|
+
export const loginFormSchema = z.object({
|
|
45
|
+
email: z
|
|
46
|
+
.string()
|
|
47
|
+
.min(1, '이메일을 입력해주세요')
|
|
48
|
+
.email('올바른 이메일 형식이 아닙니다'),
|
|
49
|
+
password: z
|
|
50
|
+
.string()
|
|
51
|
+
.min(1, '비밀번호를 입력해주세요')
|
|
52
|
+
.min(8, '비밀번호는 8자 이상이어야 합니다'),
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
export type LoginFormData = z.infer<typeof loginFormSchema>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 조건부 검증
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
export const registerFormSchema = z.object({
|
|
62
|
+
password: z.string().min(8),
|
|
63
|
+
confirmPassword: z.string(),
|
|
64
|
+
}).refine((data) => data.password === data.confirmPassword, {
|
|
65
|
+
message: '비밀번호가 일치하지 않습니다',
|
|
66
|
+
path: ['confirmPassword'],
|
|
67
|
+
})
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## 폼 컴포넌트 패턴
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
// components/LoginForm.tsx
|
|
74
|
+
import { useForm } from 'react-hook-form'
|
|
75
|
+
import { zodResolver } from '@hookform/resolvers/zod'
|
|
76
|
+
import { loginFormSchema, LoginFormData } from '../schemas/loginFormSchema'
|
|
77
|
+
import { trpc } from '@/utils/trpc'
|
|
78
|
+
|
|
79
|
+
export function LoginForm() {
|
|
80
|
+
const {
|
|
81
|
+
register,
|
|
82
|
+
handleSubmit,
|
|
83
|
+
formState: { errors },
|
|
84
|
+
} = useForm<LoginFormData>({
|
|
85
|
+
resolver: zodResolver(loginFormSchema),
|
|
86
|
+
defaultValues: {
|
|
87
|
+
email: '',
|
|
88
|
+
password: '',
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const loginMutation = trpc.auth.login.useMutation({
|
|
93
|
+
onSuccess: (data) => {
|
|
94
|
+
// 성공 처리
|
|
95
|
+
},
|
|
96
|
+
onError: (error) => {
|
|
97
|
+
// 에러 처리
|
|
98
|
+
},
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
const onSubmit = (data: LoginFormData) => {
|
|
102
|
+
loginMutation.mutate(data)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<form onSubmit={handleSubmit(onSubmit)}>
|
|
107
|
+
<div>
|
|
108
|
+
<input
|
|
109
|
+
{...register('email')}
|
|
110
|
+
type="email"
|
|
111
|
+
placeholder="이메일"
|
|
112
|
+
/>
|
|
113
|
+
{errors.email && (
|
|
114
|
+
<span className="error">{errors.email.message}</span>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div>
|
|
119
|
+
<input
|
|
120
|
+
{...register('password')}
|
|
121
|
+
type="password"
|
|
122
|
+
placeholder="비밀번호"
|
|
123
|
+
/>
|
|
124
|
+
{errors.password && (
|
|
125
|
+
<span className="error">{errors.password.message}</span>
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<button
|
|
130
|
+
type="submit"
|
|
131
|
+
disabled={loginMutation.isPending}
|
|
132
|
+
>
|
|
133
|
+
{loginMutation.isPending ? '로그인 중...' : '로그인'}
|
|
134
|
+
</button>
|
|
135
|
+
</form>
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## 필드별 검증 (safeParse)
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
// 실시간 필드 검증이 필요한 경우
|
|
144
|
+
const validateEmail = (value: string) => {
|
|
145
|
+
const result = loginFormSchema.shape.email.safeParse(value)
|
|
146
|
+
return result.success ? true : result.error.errors[0].message
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// useForm에서 사용
|
|
150
|
+
const { register } = useForm<LoginFormData>({
|
|
151
|
+
mode: 'onBlur', // blur 시 검증
|
|
152
|
+
})
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## 에러 처리 패턴
|
|
156
|
+
|
|
157
|
+
### 폼 에러 표시
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
function FormField({ name, register, errors }: FormFieldProps) {
|
|
161
|
+
return (
|
|
162
|
+
<div className="form-field">
|
|
163
|
+
<input {...register(name)} />
|
|
164
|
+
{errors[name] && (
|
|
165
|
+
<p className="error-message">{errors[name]?.message}</p>
|
|
166
|
+
)}
|
|
167
|
+
</div>
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### 서버 에러 처리
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
const loginMutation = trpc.auth.login.useMutation({
|
|
176
|
+
onError: (error) => {
|
|
177
|
+
// TRPC 에러를 폼 에러로 변환
|
|
178
|
+
if (error.data?.code === 'UNAUTHORIZED') {
|
|
179
|
+
setError('password', {
|
|
180
|
+
message: '이메일 또는 비밀번호가 올바르지 않습니다'
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
})
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Watch 패턴
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
const { watch, register } = useForm<FormData>()
|
|
191
|
+
|
|
192
|
+
// 특정 필드 감시
|
|
193
|
+
const watchedEmail = watch('email')
|
|
194
|
+
|
|
195
|
+
// 전체 폼 감시
|
|
196
|
+
const formValues = watch()
|
|
197
|
+
|
|
198
|
+
// 조건부 렌더링
|
|
199
|
+
{watchedEmail && <p>입력된 이메일: {watchedEmail}</p>}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## 체크리스트
|
|
203
|
+
|
|
204
|
+
### 스키마 설정
|
|
205
|
+
- [ ] 스키마를 별도 파일(*FormSchema.ts)로 분리
|
|
206
|
+
- [ ] z.infer로 타입 추론
|
|
207
|
+
- [ ] 에러 메시지 한글화
|
|
208
|
+
|
|
209
|
+
### 폼 구현
|
|
210
|
+
- [ ] zodResolver 사용
|
|
211
|
+
- [ ] defaultValues 설정
|
|
212
|
+
- [ ] register로 필드 등록
|
|
213
|
+
|
|
214
|
+
### 에러 처리
|
|
215
|
+
- [ ] formState.errors로 에러 표시
|
|
216
|
+
- [ ] 서버 에러는 setError로 처리
|
|
217
|
+
- [ ] 필드별 에러 메시지 표시
|
|
218
|
+
|
|
219
|
+
### TRPC 연동
|
|
220
|
+
- [ ] useMutation 사용
|
|
221
|
+
- [ ] isPending으로 로딩 상태 표시
|
|
222
|
+
- [ ] onSuccess/onError 핸들러 구현
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tailwind-styled
|
|
3
|
+
description: tailwind-styled-components로 스타일링 시 사용. DOM depth 최소화 원칙, wrapper 제거 패턴, 조건부 스타일 적용법 제공.
|
|
4
|
+
keywords: [tailwind, styled-components, tw, DOM, depth, wrapper, 스타일링]
|
|
5
|
+
estimated_tokens: ~350
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# tailwind-styled-components 스킬
|
|
9
|
+
|
|
10
|
+
## 핵심 원칙
|
|
11
|
+
|
|
12
|
+
**DOM depth를 최소화하라.**
|
|
13
|
+
- 불필요한 wrapper div 제거
|
|
14
|
+
- 스타일만을 위한 중첩 요소 금지
|
|
15
|
+
- 하나의 Styled Component가 하나의 DOM 요소
|
|
16
|
+
|
|
17
|
+
## 필수 준수 사항
|
|
18
|
+
|
|
19
|
+
### DOM Depth 최소화
|
|
20
|
+
|
|
21
|
+
| 잘못된 예 | 올바른 예 |
|
|
22
|
+
|----------|----------|
|
|
23
|
+
| `<Wrapper><Inner><Content>...</Content></Inner></Wrapper>` | `<Container>...</Container>` |
|
|
24
|
+
| `<div><div className="...">...</div></div>` | `<StyledDiv>...</StyledDiv>` |
|
|
25
|
+
|
|
26
|
+
### 예제: 불필요한 중첩 제거
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
// ❌ Bad - 불필요한 DOM depth
|
|
30
|
+
const Card = () => (
|
|
31
|
+
<div>
|
|
32
|
+
<div className="p-4">
|
|
33
|
+
<div className="bg-white rounded-lg shadow">
|
|
34
|
+
<div className="flex flex-col gap-4">
|
|
35
|
+
{content}
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// ✅ Good - 최소한의 DOM depth
|
|
43
|
+
const CardContainer = tw.div`
|
|
44
|
+
p-4
|
|
45
|
+
bg-white
|
|
46
|
+
rounded-lg
|
|
47
|
+
shadow
|
|
48
|
+
flex
|
|
49
|
+
flex-col
|
|
50
|
+
gap-4
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
const Card = () => (
|
|
54
|
+
<CardContainer>
|
|
55
|
+
{content}
|
|
56
|
+
</CardContainer>
|
|
57
|
+
);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 예제: Tailwind 디자인 변환
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
// 원본 Tailwind (depth: 3)
|
|
64
|
+
<div className="p-5">
|
|
65
|
+
<div className="bg-white rounded-t-[32px]">
|
|
66
|
+
<div className="flex flex-col gap-5">
|
|
67
|
+
<h2>Title</h2>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
// ✅ 변환 후 (depth: 1)
|
|
73
|
+
const StepContent = tw.div`
|
|
74
|
+
p-5
|
|
75
|
+
bg-white
|
|
76
|
+
rounded-t-[32px]
|
|
77
|
+
flex
|
|
78
|
+
flex-col
|
|
79
|
+
gap-5
|
|
80
|
+
`;
|
|
81
|
+
|
|
82
|
+
<StepContent>
|
|
83
|
+
<Title>Title</Title>
|
|
84
|
+
</StepContent>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Styled Component 네이밍
|
|
88
|
+
|
|
89
|
+
| 용도 | 네이밍 패턴 | 예시 |
|
|
90
|
+
|------|------------|------|
|
|
91
|
+
| 컨테이너 | `*Container`, `*Wrapper` | `CardContainer` |
|
|
92
|
+
| 섹션 | `*Section`, `*Area` | `HeaderSection` |
|
|
93
|
+
| 아이템 | `*Item`, `*Row` | `ListItem` |
|
|
94
|
+
| 텍스트 | `Title`, `Label`, `Text` | `SectionLabel` |
|
|
95
|
+
| 입력 | `*Input`, `*Field` | `PhoneInput` |
|
|
96
|
+
| 버튼 | `*Button`, `*Btn` | `SubmitButton` |
|
|
97
|
+
|
|
98
|
+
## Props 기반 동적 스타일링
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import tw from 'tailwind-styled-components';
|
|
102
|
+
|
|
103
|
+
interface ButtonProps {
|
|
104
|
+
$primary?: boolean;
|
|
105
|
+
$size?: 'sm' | 'md' | 'lg';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const Button = tw.button<ButtonProps>`
|
|
109
|
+
rounded-lg
|
|
110
|
+
font-medium
|
|
111
|
+
transition-colors
|
|
112
|
+
|
|
113
|
+
${({ $primary }) =>
|
|
114
|
+
$primary
|
|
115
|
+
? 'bg-blue-500 text-white hover:bg-blue-600'
|
|
116
|
+
: 'bg-gray-200 text-gray-800 hover:bg-gray-300'
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
${({ $size }) => {
|
|
120
|
+
switch ($size) {
|
|
121
|
+
case 'sm': return 'px-3 py-1.5 text-sm';
|
|
122
|
+
case 'lg': return 'px-6 py-3 text-lg';
|
|
123
|
+
default: return 'px-4 py-2 text-base';
|
|
124
|
+
}
|
|
125
|
+
}}
|
|
126
|
+
`;
|
|
127
|
+
|
|
128
|
+
// 사용
|
|
129
|
+
<Button $primary $size="lg">Submit</Button>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## 기존 HTML 요소 확장
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
// 기존 컴포넌트에 스타일 추가
|
|
136
|
+
const StyledLink = tw(Link)`
|
|
137
|
+
text-blue-500
|
|
138
|
+
hover:underline
|
|
139
|
+
`;
|
|
140
|
+
|
|
141
|
+
// input 확장
|
|
142
|
+
const Input = tw.input`
|
|
143
|
+
w-full
|
|
144
|
+
px-4
|
|
145
|
+
py-2
|
|
146
|
+
border
|
|
147
|
+
border-gray-300
|
|
148
|
+
rounded-lg
|
|
149
|
+
focus:outline-none
|
|
150
|
+
focus:ring-2
|
|
151
|
+
focus:ring-blue-500
|
|
152
|
+
`;
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## 체크리스트
|
|
156
|
+
|
|
157
|
+
### DOM 구조
|
|
158
|
+
- [ ] 스타일만을 위한 wrapper div가 있는가? → 제거
|
|
159
|
+
- [ ] 중첩된 div를 하나로 합칠 수 있는가? → 합치기
|
|
160
|
+
- [ ] depth가 3 이상인가? → 리팩토링 고려
|
|
161
|
+
|
|
162
|
+
### Styled Component
|
|
163
|
+
- [ ] 의미 있는 이름인가?
|
|
164
|
+
- [ ] transient props($prefix)를 사용하는가?
|
|
165
|
+
- [ ] 재사용 가능한 컴포넌트인가?
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tanstack-router
|
|
3
|
+
description: TanStack Router로 라우팅 구현 시 사용. 파일 기반 라우트 구조, 동적 경로, beforeLoad 인증 guard 패턴 제공.
|
|
4
|
+
keywords: [tanstack, router, 라우팅, 파일기반, createFileRoute, navigate, beforeLoad, guard]
|
|
5
|
+
estimated_tokens: ~450
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# TanStack Router 패턴
|
|
9
|
+
|
|
10
|
+
## 핵심 역할
|
|
11
|
+
|
|
12
|
+
- 파일 기반 라우팅 설정
|
|
13
|
+
- 동적 라우트 및 레이아웃 구성
|
|
14
|
+
- 인증 guard 구현
|
|
15
|
+
- 타입 안전한 네비게이션
|
|
16
|
+
|
|
17
|
+
## 파일 기반 라우팅 구조
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
src/routes/
|
|
21
|
+
├── __root.tsx # 루트 레이아웃
|
|
22
|
+
├── index.tsx # / 경로
|
|
23
|
+
├── _layout.tsx # 공통 레이아웃 (언더스코어)
|
|
24
|
+
├── _layout/
|
|
25
|
+
│ ├── dashboard.tsx # /dashboard
|
|
26
|
+
│ └── settings.tsx # /settings
|
|
27
|
+
├── products/
|
|
28
|
+
│ ├── index.tsx # /products
|
|
29
|
+
│ └── $productId.tsx # /products/:productId (동적)
|
|
30
|
+
└── auth/
|
|
31
|
+
├── login.tsx # /auth/login
|
|
32
|
+
└── register.tsx # /auth/register
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## 필수 준수 사항
|
|
36
|
+
|
|
37
|
+
| 규칙 | 올바른 예 | 잘못된 예 |
|
|
38
|
+
|------|----------|----------|
|
|
39
|
+
| 동적 라우트 | `$slug.tsx`, `$productId.tsx` | `:slug.tsx`, `[slug].tsx` |
|
|
40
|
+
| 레이아웃 라우트 | `_layout.tsx` (언더스코어) | `layout.tsx` |
|
|
41
|
+
| 루트 라우트 | `__root.tsx` (더블 언더스코어) | `root.tsx` |
|
|
42
|
+
| 네비게이션 훅 | `Route.useNavigate()` | `useNavigate()` 직접 import |
|
|
43
|
+
|
|
44
|
+
## 라우트 정의 패턴
|
|
45
|
+
|
|
46
|
+
### 기본 라우트
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
// routes/products/index.tsx
|
|
50
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
51
|
+
|
|
52
|
+
export const Route = createFileRoute('/products/')({
|
|
53
|
+
component: ProductsPage,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
function ProductsPage() {
|
|
57
|
+
return <div>Products List</div>
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 동적 라우트
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
// routes/products/$productId.tsx
|
|
65
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
66
|
+
|
|
67
|
+
export const Route = createFileRoute('/products/$productId')({
|
|
68
|
+
component: ProductDetailPage,
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
function ProductDetailPage() {
|
|
72
|
+
const { productId } = Route.useParams()
|
|
73
|
+
return <div>Product: {productId}</div>
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 루트 레이아웃
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
// routes/__root.tsx
|
|
81
|
+
import { createRootRoute, Outlet } from '@tanstack/react-router'
|
|
82
|
+
|
|
83
|
+
export const Route = createRootRoute({
|
|
84
|
+
component: RootLayout,
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
function RootLayout() {
|
|
88
|
+
return (
|
|
89
|
+
<div>
|
|
90
|
+
<Header />
|
|
91
|
+
<Outlet />
|
|
92
|
+
<Footer />
|
|
93
|
+
</div>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 레이아웃 라우트 (그룹화)
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// routes/_layout.tsx
|
|
102
|
+
import { createFileRoute, Outlet } from '@tanstack/react-router'
|
|
103
|
+
|
|
104
|
+
export const Route = createFileRoute('/_layout')({
|
|
105
|
+
component: LayoutWrapper,
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
function LayoutWrapper() {
|
|
109
|
+
return (
|
|
110
|
+
<div className="with-sidebar">
|
|
111
|
+
<Sidebar />
|
|
112
|
+
<main>
|
|
113
|
+
<Outlet />
|
|
114
|
+
</main>
|
|
115
|
+
</div>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## 네비게이션
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
function ProductCard({ productId }: { productId: string }) {
|
|
124
|
+
const navigate = Route.useNavigate()
|
|
125
|
+
|
|
126
|
+
const handleClick = () => {
|
|
127
|
+
navigate({ to: '/products/$productId', params: { productId } })
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return <button onClick={handleClick}>View Product</button>
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## 인증 Guard (beforeLoad)
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
// routes/_authenticated.tsx
|
|
138
|
+
import { createFileRoute, redirect } from '@tanstack/react-router'
|
|
139
|
+
|
|
140
|
+
export const Route = createFileRoute('/_authenticated')({
|
|
141
|
+
beforeLoad: async ({ context }) => {
|
|
142
|
+
const { isAuthenticated } = context.auth
|
|
143
|
+
|
|
144
|
+
if (!isAuthenticated) {
|
|
145
|
+
throw redirect({
|
|
146
|
+
to: '/auth/login',
|
|
147
|
+
search: { redirect: location.pathname },
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
component: AuthenticatedLayout,
|
|
152
|
+
})
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### 역할 기반 Guard
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
export const Route = createFileRoute('/_admin')({
|
|
159
|
+
beforeLoad: async ({ context }) => {
|
|
160
|
+
const { user } = context.auth
|
|
161
|
+
|
|
162
|
+
if (!user || user.role !== 'admin') {
|
|
163
|
+
throw redirect({ to: '/unauthorized' })
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
})
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## 체크리스트
|
|
170
|
+
|
|
171
|
+
### 라우트 설정 시
|
|
172
|
+
- [ ] 동적 파라미터에 `$` 프리픽스 사용
|
|
173
|
+
- [ ] 레이아웃 라우트에 `_` 프리픽스 사용
|
|
174
|
+
- [ ] 루트 레이아웃은 `__root.tsx`
|
|
175
|
+
|
|
176
|
+
### 네비게이션
|
|
177
|
+
- [ ] `Route.useNavigate()` 사용
|
|
178
|
+
- [ ] 타입 안전한 params 전달
|
|
179
|
+
- [ ] 동적 파라미터는 params 객체로 전달
|
|
180
|
+
|
|
181
|
+
### 인증
|
|
182
|
+
- [ ] beforeLoad에서 인증 체크
|
|
183
|
+
- [ ] redirect로 리다이렉트 처리
|
|
184
|
+
- [ ] 원래 경로 저장 (redirect search param)
|
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# jun-claude-code
|
|
2
|
+
|
|
3
|
+
Claude Code 설정 템플릿 CLI입니다. 미리 정의된 `.claude` 설정을 `~/.claude`로 복사하여 새 프로젝트에서 빠르게 Claude Code 환경을 구축할 수 있습니다.
|
|
4
|
+
|
|
5
|
+
## 설치
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# npx로 바로 실행
|
|
9
|
+
npx jun-claude-code
|
|
10
|
+
|
|
11
|
+
# 또는 전역 설치
|
|
12
|
+
npm install -g jun-claude-code
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## 사용법
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# 기본 실행 (기존 파일이 있으면 덮어쓰기 확인)
|
|
19
|
+
jun-claude-code
|
|
20
|
+
|
|
21
|
+
# 미리보기 (실제 복사 없이 복사될 파일 목록 확인)
|
|
22
|
+
jun-claude-code --dry-run
|
|
23
|
+
|
|
24
|
+
# 강제 덮어쓰기 (확인 없이 모든 파일 복사)
|
|
25
|
+
jun-claude-code --force
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## 포함된 설정
|
|
29
|
+
|
|
30
|
+
### Agents (`.claude/agents/`)
|
|
31
|
+
|
|
32
|
+
| Agent | 설명 |
|
|
33
|
+
|-------|------|
|
|
34
|
+
| `explore` | 빠른 코드베이스 탐색 |
|
|
35
|
+
| `task-planner` | 작업 계획 수립 |
|
|
36
|
+
| `code-writer` | 코드 작성 |
|
|
37
|
+
| `code-reviewer` | 셀프 코드 리뷰 |
|
|
38
|
+
| `git-manager` | Git 작업 (커밋, PR) |
|
|
39
|
+
| `impact-analyzer` | 사이드이펙트 분석 |
|
|
40
|
+
| `qa-tester` | 테스트/빌드 검증 |
|
|
41
|
+
| `architect` | 아키텍처 설계 |
|
|
42
|
+
| `designer` | UI/UX 스타일링 |
|
|
43
|
+
| `context-collector` | Context 수집 |
|
|
44
|
+
| `context-manager` | Context 문서 관리 |
|
|
45
|
+
|
|
46
|
+
### Skills (`.claude/skills/`)
|
|
47
|
+
|
|
48
|
+
| Skill | 설명 |
|
|
49
|
+
|-------|------|
|
|
50
|
+
| `Coding` | 공통 코딩 원칙 (SRP, 응집도, 가독성) |
|
|
51
|
+
| `Git` | Git 커밋/PR 규칙 |
|
|
52
|
+
| `Backend` | 백엔드 개발 원칙 (레이어, TypeORM) |
|
|
53
|
+
|
|
54
|
+
### Hooks (`.claude/hooks/`)
|
|
55
|
+
|
|
56
|
+
- 워크플로우 순서 강제 프로토콜
|
|
57
|
+
- Skill/Agent 평가 프로토콜
|
|
58
|
+
|
|
59
|
+
## 커스터마이징
|
|
60
|
+
|
|
61
|
+
설치 후 `~/.claude/` 디렉토리에서 필요에 맞게 수정하세요.
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
~/.claude/
|
|
65
|
+
├── CLAUDE.md # 작업 가이드
|
|
66
|
+
├── agents/ # Agent 정의 수정/추가
|
|
67
|
+
├── skills/ # Skill 가이드 수정/추가
|
|
68
|
+
├── hooks/ # 자동 실행 스크립트
|
|
69
|
+
└── context/ # 프로젝트별 Context (직접 추가)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 프로젝트별 설정
|
|
73
|
+
|
|
74
|
+
프로젝트 루트에 `.claude/` 폴더를 만들어 프로젝트별 설정을 추가할 수 있습니다.
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
your-project/
|
|
78
|
+
├── .claude/
|
|
79
|
+
│ ├── context/ # 프로젝트 아키텍처, 도메인 지식
|
|
80
|
+
│ └── skills/ # 프로젝트 전용 스킬
|
|
81
|
+
└── CLAUDE.md # 프로젝트 설명
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## 핵심 원칙
|
|
85
|
+
|
|
86
|
+
이 템플릿의 핵심 원칙:
|
|
87
|
+
|
|
88
|
+
1. **Context 절약**: Main Agent의 Context Window 보존을 위해 Subagent에 작업 위임
|
|
89
|
+
2. **워크플로우 준수**: Planning → Validation → Implementation → Review
|
|
90
|
+
3. **Git 작업 위임**: 모든 Git 작업은 `git-manager` Agent 사용
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const copy_1 = require("./copy");
|
|
6
|
+
const program = new commander_1.Command();
|
|
7
|
+
program
|
|
8
|
+
.name('jun-claude-code')
|
|
9
|
+
.description('Copy .claude configuration files to your home directory (~/.claude)')
|
|
10
|
+
.version('1.0.0')
|
|
11
|
+
.option('-d, --dry-run', 'Preview files to be copied without actually copying')
|
|
12
|
+
.option('-f, --force', 'Overwrite existing files without confirmation')
|
|
13
|
+
.action(async (options) => {
|
|
14
|
+
try {
|
|
15
|
+
await (0, copy_1.copyClaudeFiles)({
|
|
16
|
+
dryRun: options.dryRun,
|
|
17
|
+
force: options.force,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (error instanceof Error) {
|
|
22
|
+
console.error('Error:', error.message);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
console.error('An unexpected error occurred');
|
|
26
|
+
}
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
program.parse();
|