create-harness-cli 0.1.0
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/LICENSE +21 -0
- package/README.md +141 -0
- package/dist/cli.js +204 -0
- package/dist/detect.js +128 -0
- package/dist/eslintPatch.js +58 -0
- package/dist/manifest.js +63 -0
- package/dist/ponytail.js +56 -0
- package/dist/prompts.js +85 -0
- package/dist/registry.js +334 -0
- package/dist/render.js +43 -0
- package/dist/suggest.js +114 -0
- package/dist/types.js +1 -0
- package/package.json +48 -0
- package/templates/core/AGENTS.md +55 -0
- package/templates/core/CLAUDE.md +11 -0
- package/templates/core/conventions/00-core.md +36 -0
- package/templates/core/conventions/10-architecture.md +54 -0
- package/templates/core/conventions/20-data-fetching.md +51 -0
- package/templates/core/conventions/30-design-system.md +46 -0
- package/templates/core/conventions/40-testing.md +46 -0
- package/templates/core/conventions/50-auth-http.md +45 -0
- package/templates/core/docs/architecture.md +21 -0
- package/templates/core/docs/decisions.md +16 -0
- package/templates/core/docs/product-spec.md +22 -0
- package/templates/core/docs/specs/_template.md +33 -0
- package/templates/core/docs/task-log.md +4 -0
- package/templates/core/gates/claude-settings.json +16 -0
- package/templates/core/gates/cursor-hooks.json +10 -0
- package/templates/core/gates/gate.mjs +115 -0
- package/templates/core/gates/pre-commit-gate.sh +7 -0
- package/templates/core/gates/run-checks.mjs +39 -0
- package/templates/core/workflows/ds-add.md +28 -0
- package/templates/core/workflows/ds-init.md +59 -0
- package/templates/core/workflows/impl.md +26 -0
- package/templates/core/workflows/ship.md +31 -0
- package/templates/core/workflows/spec.md +26 -0
- package/templates/core/workflows/verify.md +27 -0
- package/templates/presets/react-fe/configs/commitlint.config.js +36 -0
- package/templates/presets/react-fe/configs/eslint.harness.config.js +104 -0
- package/templates/presets/react-fe/configs/prettier.config.js +9 -0
- package/templates/presets/react-fe/design-system/_story-template.tsx +56 -0
- package/templates/presets/react-fe/design-system/stylelint.config.js +78 -0
- package/templates/presets/react-fe/design-system/tokens.css +57 -0
- package/templates/presets/react-fe/design-system/tokens.ts +40 -0
- package/templates/presets/react-fe/reference/auth-http/ProtectedRoute.tsx +62 -0
- package/templates/presets/react-fe/reference/auth-http/axiosInstance.ts +103 -0
- package/templates/presets/react-fe/reference/data-fetching/alertDialogStore.ts +30 -0
- package/templates/presets/react-fe/reference/data-fetching/api.ts +11 -0
- package/templates/presets/react-fe/reference/data-fetching/exampleApi.ts +46 -0
- package/templates/presets/react-fe/reference/data-fetching/exampleQueryKeys.ts +14 -0
- package/templates/presets/react-fe/reference/data-fetching/index.ts +25 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 단일 axios 인스턴스 — 모든 HTTP 호출은 이 인스턴스를 통한다.
|
|
3
|
+
* (규칙: 50-auth-http. 새 인스턴스 생성·axios 직접 호출 금지)
|
|
4
|
+
*
|
|
5
|
+
* - access token은 메모리에만 보관 (localStorage 금지)
|
|
6
|
+
* - 요청 인터셉터가 Authorization 헤더 자동 첨부
|
|
7
|
+
* - 401 시 refresh 1회 시도 (refreshPromise로 동시 요청 중복 제거) 후 원 요청 재시도
|
|
8
|
+
* - refresh까지 실패하면 'auth:logout' 이벤트만 발행 — 라우팅은 인증 훅의 책임
|
|
9
|
+
*/
|
|
10
|
+
import axios from 'axios'
|
|
11
|
+
import type { AxiosError, InternalAxiosRequestConfig } from 'axios'
|
|
12
|
+
|
|
13
|
+
export const axiosInstance = axios.create({
|
|
14
|
+
baseURL: import.meta.env.VITE_API_URL,
|
|
15
|
+
headers: {
|
|
16
|
+
'Content-Type': 'application/json',
|
|
17
|
+
},
|
|
18
|
+
withCredentials: true,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
let currentAccessToken: string | null = null
|
|
22
|
+
|
|
23
|
+
export const setAccessToken = (token: string | null) => {
|
|
24
|
+
currentAccessToken = token
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const getAccessToken = () => currentAccessToken
|
|
28
|
+
|
|
29
|
+
// 요청 인터셉터: 토큰을 Authorization 헤더에 자동 첨부 (refresh 요청 제외)
|
|
30
|
+
axiosInstance.interceptors.request.use(
|
|
31
|
+
(config: InternalAxiosRequestConfig) => {
|
|
32
|
+
const isRefreshRequest = (config.url ?? '').includes('/auth/refresh')
|
|
33
|
+
if (currentAccessToken && config.headers && !isRefreshRequest) {
|
|
34
|
+
config.headers.Authorization = `Bearer ${currentAccessToken}`
|
|
35
|
+
}
|
|
36
|
+
return config
|
|
37
|
+
},
|
|
38
|
+
(error) => Promise.reject(error),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
interface IRefreshResponse {
|
|
42
|
+
data: {
|
|
43
|
+
accessToken: string
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 동시에 만료된 요청 N개가 refresh를 N번 부르지 않도록 중복 제거
|
|
48
|
+
let refreshPromise: Promise<string> | null = null
|
|
49
|
+
|
|
50
|
+
const refreshAccessToken = (): Promise<string> => {
|
|
51
|
+
if (refreshPromise) return refreshPromise
|
|
52
|
+
|
|
53
|
+
refreshPromise = axiosInstance
|
|
54
|
+
.post<IRefreshResponse>('/auth/refresh')
|
|
55
|
+
.then((response) => {
|
|
56
|
+
const token = response.data.data.accessToken
|
|
57
|
+
setAccessToken(token)
|
|
58
|
+
return token
|
|
59
|
+
})
|
|
60
|
+
.finally(() => {
|
|
61
|
+
refreshPromise = null
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
return refreshPromise
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 응답 인터셉터: 401 → refresh 1회 → 원 요청 재시도, 실패 시 로그아웃 이벤트
|
|
68
|
+
axiosInstance.interceptors.response.use(
|
|
69
|
+
(response) => response,
|
|
70
|
+
async (error: AxiosError) => {
|
|
71
|
+
const originalRequest = error.config as InternalAxiosRequestConfig & {
|
|
72
|
+
_retry?: boolean
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const requestUrl = originalRequest?.url ?? ''
|
|
76
|
+
const isAuthRequest =
|
|
77
|
+
requestUrl.includes('/auth/login') ||
|
|
78
|
+
requestUrl.includes('/auth/refresh') ||
|
|
79
|
+
requestUrl.includes('/auth/logout')
|
|
80
|
+
|
|
81
|
+
if (
|
|
82
|
+
error.response?.status === 401 &&
|
|
83
|
+
originalRequest &&
|
|
84
|
+
!originalRequest._retry &&
|
|
85
|
+
!isAuthRequest &&
|
|
86
|
+
currentAccessToken !== null
|
|
87
|
+
) {
|
|
88
|
+
originalRequest._retry = true
|
|
89
|
+
try {
|
|
90
|
+
const token = await refreshAccessToken()
|
|
91
|
+
if (originalRequest.headers) {
|
|
92
|
+
originalRequest.headers.Authorization = `Bearer ${token}`
|
|
93
|
+
}
|
|
94
|
+
return axiosInstance(originalRequest)
|
|
95
|
+
} catch {
|
|
96
|
+
setAccessToken(null)
|
|
97
|
+
window.dispatchEvent(new CustomEvent('auth:logout'))
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return Promise.reject(error)
|
|
102
|
+
},
|
|
103
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 전역 클라이언트 상태(Zustand) 참조 구현 — 확인 버튼 단일 액션 알림 다이얼로그.
|
|
3
|
+
* (규칙: 20-data-fetching 상태 4분류 — "애플리케이션 상태"에 해당)
|
|
4
|
+
*
|
|
5
|
+
* 서버 캐시(TanStack Query 데이터)를 이런 스토어에 복사하지 않는다.
|
|
6
|
+
*/
|
|
7
|
+
import { create } from 'zustand'
|
|
8
|
+
|
|
9
|
+
interface IAlertDialogState {
|
|
10
|
+
isOpen: boolean
|
|
11
|
+
title: string
|
|
12
|
+
message: string
|
|
13
|
+
onConfirm: (() => void) | null
|
|
14
|
+
open: (params: {
|
|
15
|
+
title: string
|
|
16
|
+
message: string
|
|
17
|
+
onConfirm?: () => void
|
|
18
|
+
}) => void
|
|
19
|
+
close: () => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const useAlertDialogStore = create<IAlertDialogState>((set) => ({
|
|
23
|
+
isOpen: false,
|
|
24
|
+
title: '',
|
|
25
|
+
message: '',
|
|
26
|
+
onConfirm: null,
|
|
27
|
+
open: ({ title, message, onConfirm }) =>
|
|
28
|
+
set({ isOpen: true, title, message, onConfirm: onConfirm ?? null }),
|
|
29
|
+
close: () => set({ isOpen: false, title: '', message: '', onConfirm: null }),
|
|
30
|
+
}))
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 3계층 데이터 패턴의 1층: 원시 API 호출. (규칙: 20-data-fetching)
|
|
3
|
+
* - axiosInstance만 사용, IApiResponse<T> 언래핑까지 담당
|
|
4
|
+
* - 이 파일은 폴더 밖에서 직접 import 금지 (index.ts 공개 API로만)
|
|
5
|
+
*
|
|
6
|
+
* "Example" 도메인은 이 패턴을 모방하기 위한 참조 구현입니다.
|
|
7
|
+
* 실제 도메인을 추가할 때 이 폴더 구조(Api / QueryKeys / index)를 복사하세요.
|
|
8
|
+
*/
|
|
9
|
+
import type { IApiResponse } from '../../types/api'
|
|
10
|
+
import { axiosInstance } from '../../utils/axiosInstance'
|
|
11
|
+
|
|
12
|
+
export interface IExampleItem {
|
|
13
|
+
id: string
|
|
14
|
+
name: string
|
|
15
|
+
isActive: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 페이지네이션 형태는 도메인마다 다를 수 있어 모듈 안에 둔다
|
|
19
|
+
// (types/api.ts 가 기존 프로젝트 파일과 충돌해도 이 모듈은 컴파일된다)
|
|
20
|
+
export interface IPaginatedData<TItem> {
|
|
21
|
+
items: TItem[]
|
|
22
|
+
page: number
|
|
23
|
+
pageSize: number
|
|
24
|
+
totalCount: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface IExampleFilters {
|
|
28
|
+
keyword?: string
|
|
29
|
+
page?: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function getExampleList(
|
|
33
|
+
filters: IExampleFilters,
|
|
34
|
+
): Promise<IPaginatedData<IExampleItem>> {
|
|
35
|
+
const response = await axiosInstance.get<
|
|
36
|
+
IApiResponse<IPaginatedData<IExampleItem>>
|
|
37
|
+
>('/examples', { params: filters })
|
|
38
|
+
return response.data.data
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function getExampleDetail(id: string): Promise<IExampleItem> {
|
|
42
|
+
const response = await axiosInstance.get<IApiResponse<IExampleItem>>(
|
|
43
|
+
`/examples/${encodeURIComponent(id)}`,
|
|
44
|
+
)
|
|
45
|
+
return response.data.data
|
|
46
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 3계층 데이터 패턴의 2층: queryKey 팩토리. (규칙: 20-data-fetching)
|
|
3
|
+
* 계층적 키 — 상위 키로 무효화하면 하위가 전부 무효화된다.
|
|
4
|
+
*/
|
|
5
|
+
import type { IExampleFilters } from './exampleApi'
|
|
6
|
+
|
|
7
|
+
export const exampleQueryKeys = {
|
|
8
|
+
all: ['example'] as const,
|
|
9
|
+
lists: () => [...exampleQueryKeys.all, 'list'] as const,
|
|
10
|
+
list: (filters: IExampleFilters) =>
|
|
11
|
+
[...exampleQueryKeys.lists(), filters] as const,
|
|
12
|
+
details: () => [...exampleQueryKeys.all, 'detail'] as const,
|
|
13
|
+
detail: (id: string) => [...exampleQueryKeys.details(), id] as const,
|
|
14
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 3계층 데이터 패턴의 3층: 공개 API. (규칙: 20-data-fetching)
|
|
3
|
+
* 컴포넌트·훅은 이 파일이 export하는 것만 쓴다. 내부 파일 deep import 금지.
|
|
4
|
+
*/
|
|
5
|
+
import { useQuery } from '@tanstack/react-query'
|
|
6
|
+
|
|
7
|
+
import { getExampleDetail, getExampleList } from './exampleApi'
|
|
8
|
+
import type { IExampleFilters } from './exampleApi'
|
|
9
|
+
import { exampleQueryKeys } from './exampleQueryKeys'
|
|
10
|
+
|
|
11
|
+
export type { IExampleFilters, IExampleItem } from './exampleApi'
|
|
12
|
+
export { exampleQueryKeys } from './exampleQueryKeys'
|
|
13
|
+
|
|
14
|
+
export const useExampleListQuery = (filters: IExampleFilters) =>
|
|
15
|
+
useQuery({
|
|
16
|
+
queryKey: exampleQueryKeys.list(filters),
|
|
17
|
+
queryFn: () => getExampleList(filters),
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
export const useExampleDetailQuery = (id: string) =>
|
|
21
|
+
useQuery({
|
|
22
|
+
queryKey: exampleQueryKeys.detail(id),
|
|
23
|
+
queryFn: () => getExampleDetail(id),
|
|
24
|
+
enabled: id.length > 0,
|
|
25
|
+
})
|